Compare commits

...

10 Commits

33 changed files with 4712 additions and 840 deletions

View File

@ -0,0 +1,549 @@
# Application Server Tutorials
These tutorials implement an application server that defines a full REST API for interacting with LiveKit in different languages using its corresponding SDKs. This is useful to see an example of how to call all differents endpoints of LiveKit Twirp API.
> [!NOTE]
> By the moment, tutorials are only fully implemented in Node.js, Java and Go. Besides, AgentDispatchClient and SipClient methods are not implemented in any tutorial.
## Testing API endpoints
To test API endpoints, we have defined a [Postman collection](./application-server-tutorials.postman_collection.json) that you can import into your Postman client.
This collection defines a set of variables that requests use in order to reduce the amount of changes you need to make to test the API. This variables are:
- `BASE_URL`: The base URL of the application server. By default, it is set to `http://localhost:6080`.
- `ROOM_URL`: The base URL of all room-related endpoints. By default, it is set to `{{BASE_URL}}/rooms`.
- `EGRESS_URL`: The base URL of all egress-related endpoints. By default, it is set to `{{BASE_URL}}/egresses`.
- `INGRESS_URL`: The base URL of all ingress-related endpoints. By default, it is set to `{{BASE_URL}}/ingresses`.
- `ROOM_NAME`: The name of the room. By default, it is set to `Test Room`.
- `PARTICIPANT_IDENTITY`: The identity of the participant. By default, it is set to `Participant1`.
- `TRACK_ID`: The ID of the track.
- `EGRESS_ID`: The ID of the egress.
- `INGRESS_ID`: The ID of the ingress.
The collection is divided into folders, each one containing requests for a specific endpoint:
- `Room`: Contains requests for room-related endpoints.
- `Egress`: Contains requests for egress-related endpoints.
- `Ingress`: Contains requests for ingress-related endpoints.
In addition, there is another request apart from the folders:
- **Create token**: Generates a token for a participant to join a room.
- **Method**: `POST`
- **URL**: `{{BASE_URL}}/token`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}",
"participantIdentity": "{{PARTICIPANT_IDENTITY}}"
}
```
- **Example Response**:
```json
{
"token": "..."
}
```
### Room requests
The `Room` folder contains the following requests:
- **Create room**: Creates a new room.
- **Method**: `POST`
- **URL**: `{{ROOM_URL}}`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}"
}
```
- **Example Response**:
```json
{
"room": {...}
}
```
- **List rooms**: Retrieves a list of active rooms, optionally filtered by name.
- **Method**: `GET`
- **URL**: `{{ROOM_URL}}?roomName={{ROOM_NAME}}`
- **Example Response**:
```json
{
"rooms": [...]
}
```
- **Update room metadata**: Updates the metadata of a room.
- **Method**: `POST`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/metadata`
- **Body**:
```json
{
"metadata": "Some data"
}
```
- **Example Response**:
```json
{
"room": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, a room must be created and the `ROOM_NAME` variable must be set to the name of the room.
- **Send data**: Sends a data message to all participants in a room.
- **Method**: `POST`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/send-data`
- **Body**:
```json
{
"data": {
"some": "data"
}
}
```
- **Example Response**:
```json
{
"message": "Data message sent"
}
```
> [!IMPORTANT]
> Before sending previous request, a room must be created and the `ROOM_NAME` variable must be set to the name of the room.
- **Delete room**: Deletes a room.
- **Method**: `DELETE`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}`
- **Example Response**:
```json
{
"message": "Room deleted"
}
```
> [!IMPORTANT]
> Before sending previous request, a room must be created and the `ROOM_NAME` variable must be set to the name of the room.
- **List participants**: Retrieves the list of participants in a room.
- **Method**: `GET`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/participants`
- **Example Response**:
```json
{
"participants": [...]
}
```
- **Get participant**: Retrieves information about a specific participant.
- **Method**: `GET`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}`
- **Example Response**:
```json
{
"participant": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly.
- **Update participant**: Updates metadata of a participant.
- **Method**: `PATCH`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}`
- **Body**:
```json
{
"metadata": "Some data"
}
```
- **Example Response**:
```json
{
"participant": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly.
- **Delete participant**: Removes a participant from a room.
- **Method**: `DELETE`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}`
- **Example Response**:
```json
{
"message": "Participant removed"
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly.
- **Mute track**: Mutes a specific track for a participant.
- **Method**: `POST`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}/mute`
- **Body**:
```json
{
"trackId": "{{TRACK_ID}}"
}
```
- **Example Response**:
```json
{
"track": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly. Then, set the `TRACK_ID` variable to the ID of one of the participant's tracks obtained from the response of the `Get participant` request.
- **Subscribe to tracks**: Subscribes a participant to specific tracks.
- **Method**: `POST`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}/subscribe`
- **Body**:
```json
{
"trackIds": ["{{TRACK_ID}}"]
}
```
- **Example Response**:
```json
{
"message": "Participant subscribed to tracks"
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join two participants in a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly. Then, set the `TRACK_ID` variable to the ID of one of the other participant's tracks obtained from the response of the `Get participant` request.
- **Unsubscribe from tracks**: Unsubscribes a participant from specific tracks.
- **Method**: `POST`
- **URL**: `{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}/unsubscribe`
- **Body**:
```json
{
"trackIds": ["{{TRACK_ID}}"]
}
```
- **Example Response**:
```json
{
"message": "Participant unsubscribed from tracks"
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join two participants in a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly. Then, set the `TRACK_ID` variable to the ID of one of the other participant's tracks obtained from the response of the `Get participant` request.
### Egress requests
The `Egress` folder contains the following requests:
- **Create RoomComposite egress**: Starts recording a room with a composite layout.
- **Method**: `POST`
- **URL**: `{{EGRESS_URL}}/room-composite`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}"
}
```
- **Example Response**:
```json
{
"egress": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` correctly.
- **Create stream egress**: Starts streaming a room to an external RTMP server.
- **Method**: `POST`
- **URL**: `{{EGRESS_URL}}/stream`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}",
"streamUrl": "rtmp://live.twitch.tv/app/stream-key"
}
```
- **Example Response**:
```json
{
"egress": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` correctly.
- **Create Participant egress**: Starts recording a specific participant's tracks.
- **Method**: `POST`
- **URL**: `{{EGRESS_URL}}/participant`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}",
"participantIdentity": "{{PARTICIPANT_IDENTITY}}"
}
```
- **Example Response**:
```json
{
"egress": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly.
- **Create TrackComposite egress**: Starts recording specific audio and video tracks.
- **Method**: `POST`
- **URL**: `{{EGRESS_URL}}/track-composite`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}",
"videoTrackId": "{{TRACK_ID}}",
"audioTrackId": "TR_EXAMPLE"
}
```
- **Example Response**:
```json
{
"egress": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly.. Then, set the `TRACK_ID` variable to the ID of the video track obtained from the response of the `Get participant` request and the `audioTrackId` parameter to the ID of the audio track.
- **Create Track egress**: Starts recording a specific track.
- **Method**: `POST`
- **URL**: `{{EGRESS_URL}}/track`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}",
"trackId": "{{TRACK_ID}}"
}
```
- **Example Response**:
```json
{
"egress": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, you need to join a room using one of the [application clients](../application-client) and set the `ROOM_NAME` and `PARTICIPANT_IDENTITY` correctly. Then, set the `TRACK_ID` variable to the ID of one of the tracks obtained from the response of the `Get participant` request.
- **Create Web egress**: Starts recording a webpage.
- **Method**: `POST`
- **URL**: `{{EGRESS_URL}}/web`
- **Body**:
```json
{
"url": "https://openvidu.io"
}
```
- **Example Response**:
```json
{
"egress": {...}
}
```
- **List egresses**: Retrieves the list of egresses, optionally filtered by room name, egressID or active status.
- **Method**: `GET`
- **URL**: `{{EGRESS_URL}}?roomName={{ROOM_NAME}}&active=true`
- **Example Response**:
```json
{
"egresses": [...]
}
```
- **Update egress layout**: Changes the layout of an active RoomComposite egress.
- **Method**: `POST`
- **URL**: `{{EGRESS_URL}}/{{EGRESS_ID}}/layout`
- **Body**:
```json
{
"layout": "speaker"
}
```
- **Example Response**:
```json
{
"egress": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, a RoomComposite egress must be created and the `EGRESS_ID` variable must be set correctly.
- **Add/remove stream URLs**: Adds or removes RTMP stream URLs in an active egress.
- **Method**: `POST`
- **URL**: `{{EGRESS_URL}}/{{EGRESS_ID}}/streams`
- **Body**:
```json
{
"streamUrlsToAdd": ["rtmp://a.rtmp.youtube.com/live2/stream-key"],
"streamUrlsToRemove": []
}
```
- **Example Response**:
```json
{
"egress": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, a stream egress must be created and the `EGRESS_ID` variable must be set correctly.
- **Stop egress**: Terminates an active egress process.
- **Method**: `DELETE`
- **URL**: `{{EGRESS_URL}}/{{EGRESS_ID}}`
- **Example Response**:
```json
{
"message": "Egress stopped"
}
```
> [!IMPORTANT]
> Before sending previous request, an egress must be created and the `EGRESS_ID` variable must be set correctly.
### Ingress requests
The `Ingress` folder contains the following requests:
- **Create RTMP ingress**: Creates an RTMP ingress.
- **Method**: `POST`
- **URL**: `{{INGRESS_URL}}/rtmp`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}",
"participantIdentity": "Ingress-Participant"
}
```
- **Example Response**:
```json
{
"ingress": {...}
}
```
- **Create WHIP ingress**: Creates a WHIP ingress.
- **Method**: `POST`
- **URL**: `{{INGRESS_URL}}/whip`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}",
"participantIdentity": "Ingress-Participant"
}
```
- **Example Response**:
```json
{
"ingress": {...}
}
```
- **Create URL ingress**: Creates a URL ingress.
- **Method**: `POST`
- **URL**: `{{INGRESS_URL}}/url`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}",
"participantIdentity": "Ingress-Participant",
"url": "http://playertest.longtailvideo.com/adaptive/wowzaid3/playlist.m3u8"
}
```
- **Example Response**:
```json
{
"ingress": {...}
}
```
- **List ingresses**: Retrieves the list of ingresses, optionally filtered by room name or ingress ID.
- **Method**: `GET`
- **URL**: `{{INGRESS_URL}}?roomName={{ROOM_NAME}}`
- **Example Response**:
```json
{
"ingresses": [...]
}
```
- **Update ingress**: Updates an existing ingress.
- **Method**: `PATCH`
- **URL**: `{{INGRESS_URL}}/{{INGRESS_ID}}`
- **Body**:
```json
{
"roomName": "{{ROOM_NAME}}"
}
```
- **Example Response**:
```json
{
"ingress": {...}
}
```
> [!IMPORTANT]
> Before sending previous request, an ingress must be created and the `INGRESS_ID` variable must be set correctly.
- **Delete ingress**: Deletes an existing ingress.
- **Method**: `DELETE`
- **URL**: `{{INGRESS_URL}}/{{INGRESS_ID}}`
- **Example Response**:
```json
{
"message": "Ingress deleted"
}
```
> [!IMPORTANT]
> Before sending previous request, an ingress must be created and the `INGRESS_ID` variable must be set correctly.

View File

@ -0,0 +1,797 @@
{
"info": {
"_postman_id": "f3c07b39-211a-4128-9c45-45cf7e7cfdfa",
"name": "OpenVidu Server Tutorials",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "20220126"
},
"item": [
{
"name": "Room",
"item": [
{
"name": "Create room",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{ROOM_URL}}",
"host": [
"{{ROOM_URL}}"
]
}
},
"response": []
},
{
"name": "List rooms",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{ROOM_URL}}?roomName={{ROOM_NAME}}",
"host": [
"{{ROOM_URL}}"
],
"query": [
{
"key": "roomName",
"value": "{{ROOM_NAME}}"
}
]
}
},
"response": []
},
{
"name": "Update room metadata",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"metadata\": \"Some data\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/metadata",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"metadata"
]
}
},
"response": []
},
{
"name": "Send data",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"data\": {\n \"some\": \"data\"\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/send-data",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"send-data"
]
}
},
"response": []
},
{
"name": "Delete room",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}"
]
}
},
"response": []
},
{
"name": "List participants",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/participants",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"participants"
]
}
},
"response": []
},
{
"name": "Get participant",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"participants",
"{{PARTICIPANT_IDENTITY}}"
]
}
},
"response": []
},
{
"name": "Update participant",
"request": {
"method": "PATCH",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"metadata\": \"Some data\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"participants",
"{{PARTICIPANT_IDENTITY}}"
]
}
},
"response": []
},
{
"name": "Delete participant",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"participants",
"{{PARTICIPANT_IDENTITY}}"
]
}
},
"response": []
},
{
"name": "Mute track",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"trackId\": \"{{TRACK_ID}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}/mute",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"participants",
"{{PARTICIPANT_IDENTITY}}",
"mute"
]
}
},
"response": []
},
{
"name": "Subscribe to tracks",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"trackIds\": [\"{{TRACK_ID}}\"]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}/subscribe",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"participants",
"{{PARTICIPANT_IDENTITY}}",
"subscribe"
]
}
},
"response": []
},
{
"name": "Unsubscribe to tracks",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"trackIds\": [\"{{TRACK_ID}}\"]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{ROOM_URL}}/{{ROOM_NAME}}/participants/{{PARTICIPANT_IDENTITY}}/unsubscribe",
"host": [
"{{ROOM_URL}}"
],
"path": [
"{{ROOM_NAME}}",
"participants",
"{{PARTICIPANT_IDENTITY}}",
"unsubscribe"
]
}
},
"response": []
}
]
},
{
"name": "Egress",
"item": [
{
"name": "Create RoomComposite egress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}/room-composite",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"room-composite"
]
}
},
"response": []
},
{
"name": "Create stream egress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\",\n \"streamUrl\": \"rtmp://live.twitch.tv/app/stream-key\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}/stream",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"stream"
]
}
},
"response": []
},
{
"name": "Create Participant egress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\",\n \"participantIdentity\": \"{{PARTICIPANT_IDENTITY}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}/participant",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"participant"
]
}
},
"response": []
},
{
"name": "Create TrackComposite egress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\",\n \"videoTrackId\": \"{{TRACK_ID}}\",\n \"audioTrackId\": \"TR_AMzNsuvnjz8UHm\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}/track-composite",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"track-composite"
]
}
},
"response": []
},
{
"name": "Create Track egress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\",\n \"trackId\": \"{{TRACK_ID}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}/track",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"track"
]
}
},
"response": []
},
{
"name": "Create Web egress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"url\": \"https://openvidu.io\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}/web",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"web"
]
}
},
"response": []
},
{
"name": "List egresses",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}?roomName={{ROOM_NAME}}&active=true",
"host": [
"{{EGRESS_URL}}"
],
"query": [
{
"key": "egressId",
"value": "{{EGRESS_ID}}",
"disabled": true
},
{
"key": "roomName",
"value": "{{ROOM_NAME}}"
},
{
"key": "active",
"value": "true"
}
]
}
},
"response": []
},
{
"name": "Update egress layout",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"layout\": \"speaker\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}/{{EGRESS_ID}}/layout",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"{{EGRESS_ID}}",
"layout"
]
}
},
"response": []
},
{
"name": "Add/remove stream URLs",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"streamUrlsToAdd\": [\"rtmp://a.rtmp.youtube.com/live2/stream-key\"],\n \"streamUrlsToRemove\": []\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{EGRESS_URL}}/{{EGRESS_ID}}/streams",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"{{EGRESS_ID}}",
"streams"
]
}
},
"response": []
},
{
"name": "Stop egress",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{EGRESS_URL}}/{{EGRESS_ID}}",
"host": [
"{{EGRESS_URL}}"
],
"path": [
"{{EGRESS_ID}}"
]
}
},
"response": []
}
]
},
{
"name": "Ingress",
"item": [
{
"name": "Create RTMP ingress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\",\n \"participantIdentity\": \"Ingress-Participant\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{INGRESS_URL}}/rtmp",
"host": [
"{{INGRESS_URL}}"
],
"path": [
"rtmp"
]
}
},
"response": []
},
{
"name": "Create WHIP ingress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\",\n \"participantIdentity\": \"Ingress-Participant\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{INGRESS_URL}}/whip",
"host": [
"{{INGRESS_URL}}"
],
"path": [
"whip"
]
}
},
"response": []
},
{
"name": "Create URL ingress",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\",\n \"participantIdentity\": \"Ingress-Participant\",\n \"url\": \"http://playertest.longtailvideo.com/adaptive/wowzaid3/playlist.m3u8\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{INGRESS_URL}}/url",
"host": [
"{{INGRESS_URL}}"
],
"path": [
"url"
]
}
},
"response": []
},
{
"name": "List ingresses",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{INGRESS_URL}}?roomName={{ROOM_NAME}}",
"host": [
"{{INGRESS_URL}}"
],
"query": [
{
"key": "ingressId",
"value": "{{INGRESS_ID}}",
"disabled": true
},
{
"key": "roomName",
"value": "{{ROOM_NAME}}"
}
]
}
},
"response": []
},
{
"name": "Update ingress",
"request": {
"method": "PATCH",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{INGRESS_URL}}/{{INGRESS_ID}}",
"host": [
"{{INGRESS_URL}}"
],
"path": [
"{{INGRESS_ID}}"
]
}
},
"response": []
},
{
"name": "Delete ingress",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{INGRESS_URL}}/{{INGRESS_ID}}",
"host": [
"{{INGRESS_URL}}"
],
"path": [
"{{INGRESS_ID}}"
]
}
},
"response": []
}
]
},
{
"name": "Create token",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"roomName\": \"{{ROOM_NAME}}\",\n \"participantName\": \"{{PARTICIPANT_IDENTITY}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{BASE_URL}}/token",
"host": [
"{{BASE_URL}}"
],
"path": [
"token"
]
}
},
"response": []
}
],
"variable": [
{
"key": "BASE_URL",
"value": "http://localhost:6080",
"type": "string"
},
{
"key": "ROOM_URL",
"value": "{{BASE_URL}}/rooms",
"type": "string"
},
{
"key": "EGRESS_URL",
"value": "{{BASE_URL}}/egresses",
"type": "string"
},
{
"key": "INGRESS_URL",
"value": "{{BASE_URL}}/ingresses",
"type": "string"
},
{
"key": "ROOM_NAME",
"value": "Test Room",
"type": "string"
},
{
"key": "PARTICIPANT_IDENTITY",
"value": "Participant1",
"type": "string"
},
{
"key": "TRACK_ID",
"value": "TR_VCfQLUfBeb8YpF",
"type": "string"
},
{
"key": "EGRESS_ID",
"value": "EG_rvEMBCq9vdXA",
"type": "string"
},
{
"key": "INGRESS_ID",
"value": "IN_bjXDVmCnFqev",
"type": "string"
}
]
}

View File

@ -1,6 +1,6 @@
# Basic Go # OpenVidu Go
Basic server application built for Go with Gin. It internally uses [livekit-server-sdk-go](https://pkg.go.dev/github.com/livekit/server-sdk-go). OpenVidu server application built for Go with Gin. It internally uses [livekit-server-sdk-go](https://pkg.go.dev/github.com/livekit/server-sdk-go).
For further information, check the [tutorial documentation](https://livekit-tutorials.openvidu.io/tutorials/application-server/go/). For further information, check the [tutorial documentation](https://livekit-tutorials.openvidu.io/tutorials/application-server/go/).

View File

@ -0,0 +1,34 @@
package config
import (
"log"
"os"
"github.com/joho/godotenv"
)
var (
ServerPort string
LivekitUrl string
LivekitApiKey string
LivekitApiSecret string
)
func LoadEnv() {
// Load environment variables from .env file
if err := godotenv.Load(); err != nil {
log.Println("Error loading .env file")
}
ServerPort = getEnv("SERVER_PORT", "6080")
LivekitUrl = getEnv("LIVEKIT_URL", "http://localhost:7880")
LivekitApiKey = getEnv("LIVEKIT_API", "devkey")
LivekitApiSecret = getEnv("LIVEKIT_API_SECRET", "secret")
}
func getEnv(key, defaultValue string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return defaultValue
}

View File

@ -0,0 +1,335 @@
package controllers
import (
"log"
"net/http"
"openvidu/go/config"
"github.com/gin-gonic/gin"
"github.com/livekit/protocol/livekit"
lksdk "github.com/livekit/server-sdk-go/v2"
)
var (
egressClient *lksdk.EgressClient
)
func EgressRoutes(router *gin.Engine) {
// Initialize egress client
egressClient = lksdk.NewEgressClient(config.LivekitUrl, config.LivekitApiKey, config.LivekitApiSecret)
egressGroup := router.Group("/egresses")
{
egressGroup.POST("/room-composite", createRoomCompositeEgress)
egressGroup.POST("/stream", createStreamEgress)
egressGroup.POST("/participant", createParticipantEgress)
egressGroup.POST("/track-composite", createTrackCompositeEgress)
egressGroup.POST("/track", createTrackEgress)
egressGroup.POST("/web", createWebEgress)
egressGroup.GET("/", listEgresses)
egressGroup.POST("/:egressId/layout", updateEgressLayout)
egressGroup.POST("/:egressId/streams", updateEgressStreams)
egressGroup.DELETE("/:egressId", stopEgress)
}
}
// Create a new RoomComposite egress
func createRoomCompositeEgress(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' is required"})
return
}
req := &livekit.RoomCompositeEgressRequest{
RoomName: body.RoomName,
Layout: "grid",
FileOutputs: []*livekit.EncodedFileOutput{
{
FileType: livekit.EncodedFileType_MP4,
Filepath: "{room_name}-{room_id}-{time}",
},
},
}
egress, err := egressClient.StartRoomCompositeEgress(ctx, req)
if err != nil {
errorMessage := "Error creating RoomComposite egress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"egress": egress})
}
// Create a new RoomComposite egress to stream to a URL
func createStreamEgress(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
StreamUrl string `json:"streamUrl" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' and 'streamUrl' are required"})
return
}
req := &livekit.RoomCompositeEgressRequest{
RoomName: body.RoomName,
Layout: "grid",
StreamOutputs: []*livekit.StreamOutput{
{
Protocol: livekit.StreamProtocol_RTMP,
Urls: []string{body.StreamUrl},
},
},
}
egress, err := egressClient.StartRoomCompositeEgress(ctx, req)
if err != nil {
errorMessage := "Error creating RoomComposite egress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"egress": egress})
}
// Create a new Participant egress
func createParticipantEgress(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
ParticipantIdentity string `json:"participantIdentity" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' and 'participantIdentity' are required"})
return
}
req := &livekit.ParticipantEgressRequest{
RoomName: body.RoomName,
Identity: body.ParticipantIdentity,
FileOutputs: []*livekit.EncodedFileOutput{
{
FileType: livekit.EncodedFileType_MP4,
Filepath: "{room_name}-{room_id}-{publisher_identity}-{time}",
},
},
}
egress, err := egressClient.StartParticipantEgress(ctx, req)
if err != nil {
errorMessage := "Error creating Participant egress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"egress": egress})
}
// Create a new TrackComposite egress
func createTrackCompositeEgress(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
VideoTrackId string `json:"videoTrackId" binding:"required"`
AudioTrackId string `json:"audioTrackId" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName', 'videoTrackId' and 'audioTrackId' are required"})
return
}
req := &livekit.TrackCompositeEgressRequest{
RoomName: body.RoomName,
VideoTrackId: body.VideoTrackId,
AudioTrackId: body.AudioTrackId,
FileOutputs: []*livekit.EncodedFileOutput{
{
FileType: livekit.EncodedFileType_MP4,
Filepath: "{room_name}-{room_id}-{publisher_identity}-{time}",
},
},
}
egress, err := egressClient.StartTrackCompositeEgress(ctx, req)
if err != nil {
errorMessage := "Error creating TrackComposite egress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"egress": egress})
}
// Create a new Track egress
func createTrackEgress(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
TrackId string `json:"trackId" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' and 'trackId' are required"})
return
}
req := &livekit.TrackEgressRequest{
RoomName: body.RoomName,
TrackId: body.TrackId,
Output: &livekit.TrackEgressRequest_File{
File: &livekit.DirectFileOutput{
Filepath: "{room_name}-{room_id}-{publisher_identity}-{track_source}-{track_id}-{time}",
},
},
}
egress, err := egressClient.StartTrackEgress(ctx, req)
if err != nil {
errorMessage := "Error creating Track egress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"egress": egress})
}
// Create a new Web egress
func createWebEgress(c *gin.Context) {
var body struct {
Url string `json:"url" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' is required"})
return
}
req := &livekit.WebEgressRequest{
Url: body.Url,
FileOutputs: []*livekit.EncodedFileOutput{
{
FileType: livekit.EncodedFileType_MP4,
Filepath: "{time}",
},
},
}
egress, err := egressClient.StartWebEgress(ctx, req)
if err != nil {
errorMessage := "Error creating Web egress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"egress": egress})
}
// List egresses
// If an egress ID is provided, only that egress is listed
// If a room name is provided, only egresses for that room are listed
// If active is true, only active egresses are listed
func listEgresses(c *gin.Context) {
egressId := c.Query("egressId")
roomName := c.Query("roomName")
active := c.Query("active") == "true"
req := &livekit.ListEgressRequest{
EgressId: egressId,
RoomName: roomName,
Active: active,
}
res, err := egressClient.ListEgress(ctx, req)
if err != nil {
errorMessage := "Error listing egresses"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
egresses := res.Items
if egresses == nil {
egresses = []*livekit.EgressInfo{}
}
c.JSON(http.StatusOK, gin.H{"egresses": egresses})
}
// Update egress layout
func updateEgressLayout(c *gin.Context) {
egressId := c.Param("egressId")
var body struct {
Layout string `json:"layout" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'layout' is required"})
return
}
req := &livekit.UpdateLayoutRequest{
EgressId: egressId,
Layout: body.Layout,
}
egress, err := egressClient.UpdateLayout(ctx, req)
if err != nil {
errorMessage := "Error updating egress layout"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"egress": egress})
}
// Add/remove stream URLs to an egress
func updateEgressStreams(c *gin.Context) {
egressId := c.Param("egressId")
var body struct {
StreamUrlsToAdd []string `json:"streamUrlsToAdd"`
StreamUrlsToRemove []string `json:"streamUrlsToRemove"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'streamUrlsToAdd' and 'streamUrlsToRemove' are required and must be arrays"})
return
}
req := &livekit.UpdateStreamRequest{
EgressId: egressId,
AddOutputUrls: body.StreamUrlsToAdd,
RemoveOutputUrls: body.StreamUrlsToRemove,
}
egress, err := egressClient.UpdateStream(ctx, req)
if err != nil {
errorMessage := "Error updating egress streams"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"egress": egress})
}
// Stop an egress
func stopEgress(c *gin.Context) {
egressId := c.Param("egressId")
req := &livekit.StopEgressRequest{
EgressId: egressId,
}
_, err := egressClient.StopEgress(ctx, req)
if err != nil {
errorMessage := "Error stopping egress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Egress stopped"})
}

View File

@ -0,0 +1,192 @@
package controllers
import (
"log"
"net/http"
"openvidu/go/config"
"github.com/gin-gonic/gin"
"github.com/livekit/protocol/livekit"
lksdk "github.com/livekit/server-sdk-go/v2"
)
var (
ingressClient *lksdk.IngressClient
)
func IngressRoutes(router *gin.Engine) {
// Initialize ingress client
ingressClient = lksdk.NewIngressClient(config.LivekitUrl, config.LivekitApiKey, config.LivekitApiSecret)
ingressGroup := router.Group("/ingresses")
{
ingressGroup.POST("/rtmp", createRTMPIngress)
ingressGroup.POST("/whip", createWHIPIngress)
ingressGroup.POST("/url", createURLIngress)
ingressGroup.GET("/", listIngresses)
ingressGroup.PATCH("/:ingressId", updateIngress)
ingressGroup.DELETE("/:ingressId", deleteIngress)
}
}
// Create a new RTMP ingress
func createRTMPIngress(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
ParticipantIdentity string `json:"participantIdentity" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' and 'participantIdentity' are required"})
return
}
req := &livekit.CreateIngressRequest{
InputType: livekit.IngressInput_RTMP_INPUT,
Name: "rtmp-ingress",
RoomName: body.RoomName,
ParticipantIdentity: body.ParticipantIdentity,
}
ingress, err := ingressClient.CreateIngress(ctx, req)
if err != nil {
errorMessage := "Error creating RTMP ingress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"ingress": ingress})
}
// Create a new WHIP ingress
func createWHIPIngress(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
ParticipantIdentity string `json:"participantIdentity" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' and 'participantIdentity' are required"})
return
}
req := &livekit.CreateIngressRequest{
InputType: livekit.IngressInput_WHIP_INPUT,
Name: "whip-ingress",
RoomName: body.RoomName,
ParticipantIdentity: body.ParticipantIdentity,
}
ingress, err := ingressClient.CreateIngress(ctx, req)
if err != nil {
errorMessage := "Error creating WHIP ingress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"ingress": ingress})
}
// Create a new URL ingress
func createURLIngress(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
ParticipantIdentity string `json:"participantIdentity" binding:"required"`
Url string `json:"url" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName', 'participantIdentity' and 'url' are required"})
return
}
req := &livekit.CreateIngressRequest{
InputType: livekit.IngressInput_URL_INPUT,
Name: "url-ingress",
RoomName: body.RoomName,
ParticipantIdentity: body.ParticipantIdentity,
Url: body.Url,
}
ingress, err := ingressClient.CreateIngress(ctx, req)
if err != nil {
errorMessage := "Error creating URL ingress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"ingress": ingress})
}
// List ingresses
// If an ingress ID is provided, only that ingress is listed
// If a room name is provided, only ingresses for that room are listed
func listIngresses(c *gin.Context) {
ingressId := c.Query("ingressId")
roomName := c.Query("roomName")
req := &livekit.ListIngressRequest{
IngressId: ingressId,
RoomName: roomName,
}
res, err := ingressClient.ListIngress(ctx, req)
if err != nil {
errorMessage := "Error listing ingresses"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
ingresses := res.Items
if ingresses == nil {
ingresses = []*livekit.IngressInfo{}
}
c.JSON(http.StatusOK, gin.H{"ingresses": ingresses})
}
// Update ingress
func updateIngress(c *gin.Context) {
ingressId := c.Param("ingressId")
var body struct {
RoomName string `json:"roomName" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' is required"})
return
}
req := &livekit.UpdateIngressRequest{
IngressId: ingressId,
Name: "updated-ingress",
RoomName: body.RoomName,
}
ingress, err := ingressClient.UpdateIngress(ctx, req)
if err != nil {
errorMessage := "Error updating ingress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"ingress": ingress})
}
// Delete ingress
func deleteIngress(c *gin.Context) {
ingressId := c.Param("ingressId")
req := &livekit.DeleteIngressRequest{
IngressId: ingressId,
}
_, err := ingressClient.DeleteIngress(ctx, req)
if err != nil {
errorMessage := "Error deleting ingress"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Ingress deleted"})
}

View File

@ -0,0 +1,365 @@
package controllers
import (
"context"
"encoding/json"
"log"
"net/http"
"openvidu/go/config"
"github.com/gin-gonic/gin"
"github.com/livekit/protocol/livekit"
lksdk "github.com/livekit/server-sdk-go/v2"
)
var (
roomClient *lksdk.RoomServiceClient
ctx = context.Background()
)
func RoomRoutes(router *gin.Engine) {
// Initialize RoomServiceClient
roomClient = lksdk.NewRoomServiceClient(config.LivekitUrl, config.LivekitApiKey, config.LivekitApiSecret)
roomGroup := router.Group("/rooms")
{
roomGroup.POST("", createRoom)
roomGroup.GET("", listRooms)
roomGroup.POST("/:roomName/metadata", updateRoomMetadata)
roomGroup.POST("/:roomName/send-data", sendData)
roomGroup.DELETE("/:roomName", deleteRoom)
roomGroup.GET("/:roomName/participants", listParticipants)
roomGroup.GET("/:roomName/participants/:participantIdentity", getParticipant)
roomGroup.PATCH("/:roomName/participants/:participantIdentity", updateParticipant)
roomGroup.DELETE("/:roomName/participants/:participantIdentity", removeParticipant)
roomGroup.POST("/:roomName/participants/:participantIdentity/mute", muteParticipant)
roomGroup.POST("/:roomName/participants/:participantIdentity/subscribe", subscribeParticipant)
roomGroup.POST("/:roomName/participants/:participantIdentity/unsubscribe", unsubscribeParticipant)
}
}
// Create a new room
func createRoom(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' is required"})
return
}
req := &livekit.CreateRoomRequest{
Name: body.RoomName,
}
room, err := roomClient.CreateRoom(ctx, req)
if err != nil {
errorMessage := "Error creating room"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusCreated, gin.H{"room": room})
}
// List rooms. If a room name is provided, only that room is listed
func listRooms(c *gin.Context) {
roomName := c.Query("roomName")
var roomNames []string
if roomName != "" {
roomNames = []string{roomName}
}
req := &livekit.ListRoomsRequest{
Names: roomNames,
}
res, err := roomClient.ListRooms(ctx, req)
if err != nil {
errorMessage := "Error listing rooms"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
rooms := res.Rooms
if rooms == nil {
rooms = []*livekit.Room{}
}
c.JSON(http.StatusOK, gin.H{"rooms": rooms})
}
// Update room metadata
func updateRoomMetadata(c *gin.Context) {
roomName := c.Param("roomName")
var body struct {
Metadata string `json:"metadata" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'metadata' is required"})
return
}
req := &livekit.UpdateRoomMetadataRequest{
Room: roomName,
Metadata: body.Metadata,
}
room, err := roomClient.UpdateRoomMetadata(ctx, req)
if err != nil {
errorMessage := "Error updating room metadata"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"room": room})
}
// Send data message to participants in a room
func sendData(c *gin.Context) {
roomName := c.Param("roomName")
var body struct {
Data json.RawMessage `json:"data" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'data' is required"})
return
}
rawData, err := json.Marshal(body.Data)
if err != nil {
log.Println("Error encoding data message:", err)
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": "Error encoding data message"})
return
}
topic := "chat"
req := &livekit.SendDataRequest{
Room: roomName,
Data: rawData,
Kind: livekit.DataPacket_RELIABLE,
Topic: &topic,
DestinationIdentities: []string{},
}
_, err = roomClient.SendData(ctx, req)
if err != nil {
errorMessage := "Error sending data message"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Data message sent"})
}
// Delete a room
func deleteRoom(c *gin.Context) {
roomName := c.Param("roomName")
req := &livekit.DeleteRoomRequest{
Room: roomName,
}
_, err := roomClient.DeleteRoom(ctx, req)
if err != nil {
errorMessage := "Error deleting room"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Room deleted"})
}
// List participants in a room
func listParticipants(c *gin.Context) {
roomName := c.Param("roomName")
req := &livekit.ListParticipantsRequest{
Room: roomName,
}
res, err := roomClient.ListParticipants(ctx, req)
if err != nil {
errorMessage := "Error listing participants"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
participants := res.Participants
if participants == nil {
participants = []*livekit.ParticipantInfo{}
}
c.JSON(http.StatusOK, gin.H{"participants": participants})
}
// Get a participant in a room
func getParticipant(c *gin.Context) {
roomName := c.Param("roomName")
participantIdentity := c.Param("participantIdentity")
req := &livekit.RoomParticipantIdentity{
Room: roomName,
Identity: participantIdentity,
}
participant, err := roomClient.GetParticipant(ctx, req)
if err != nil {
errorMessage := "Error getting participant"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"participant": participant})
}
// Update a participant in a room
func updateParticipant(c *gin.Context) {
roomName := c.Param("roomName")
participantIdentity := c.Param("participantIdentity")
var body struct {
Metadata string `json:"metadata" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'metadata' is required"})
return
}
req := &livekit.UpdateParticipantRequest{
Room: roomName,
Identity: participantIdentity,
Metadata: body.Metadata,
Permission: &livekit.ParticipantPermission{
CanPublish: false,
CanSubscribe: true,
},
}
participant, err := roomClient.UpdateParticipant(ctx, req)
if err != nil {
errorMessage := "Error updating participant"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"participant": participant})
}
// Remove a participant from a room
func removeParticipant(c *gin.Context) {
roomName := c.Param("roomName")
participantIdentity := c.Param("participantIdentity")
req := &livekit.RoomParticipantIdentity{
Room: roomName,
Identity: participantIdentity,
}
_, err := roomClient.RemoveParticipant(ctx, req)
if err != nil {
errorMessage := "Error removing participant"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Participant removed"})
}
// Mute published track of a participant in a room
func muteParticipant(c *gin.Context) {
roomName := c.Param("roomName")
participantIdentity := c.Param("participantIdentity")
var body struct {
TrackId string `json:"trackId" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'trackSid' is required"})
return
}
req := &livekit.MuteRoomTrackRequest{
Room: roomName,
Identity: participantIdentity,
TrackSid: body.TrackId,
Muted: true,
}
res, err := roomClient.MutePublishedTrack(ctx, req)
if err != nil {
errorMessage := "Error muting track"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"track": res.Track})
}
// Subscribe participant to tracks in a room
func subscribeParticipant(c *gin.Context) {
roomName := c.Param("roomName")
participantIdentity := c.Param("participantIdentity")
var body struct {
TrackIds []string `json:"trackIds" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil || len(body.TrackIds) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'trackIds' is required and must be an array"})
return
}
req := &livekit.UpdateSubscriptionsRequest{
Room: roomName,
Identity: participantIdentity,
TrackSids: body.TrackIds,
Subscribe: true,
}
_, err := roomClient.UpdateSubscriptions(ctx, req)
if err != nil {
errorMessage := "Error subscribing participant to tracks"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Participant subscribed to tracks"})
}
// Unsubscribe participant from tracks in a room
func unsubscribeParticipant(c *gin.Context) {
roomName := c.Param("roomName")
participantIdentity := c.Param("participantIdentity")
var body struct {
TrackIds []string `json:"trackIds" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil || len(body.TrackIds) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'trackIds' is required and must be an array"})
return
}
req := &livekit.UpdateSubscriptionsRequest{
Room: roomName,
Identity: participantIdentity,
TrackSids: body.TrackIds,
Subscribe: false,
}
_, err := roomClient.UpdateSubscriptions(ctx, req)
if err != nil {
errorMessage := "Error unsubscribing participant from tracks"
log.Println(errorMessage+":", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errorMessage": errorMessage})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Participant unsubscribed from tracks"})
}

View File

@ -0,0 +1,40 @@
package controllers
import (
"net/http"
"openvidu/go/config"
"github.com/gin-gonic/gin"
"github.com/livekit/protocol/auth"
)
func TokenRoutes(router *gin.Engine) {
router.POST("/token", createToken)
}
func createToken(c *gin.Context) {
var body struct {
RoomName string `json:"roomName" binding:"required"`
ParticipantName string `json:"participantName" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errorMessage": "'roomName' and 'participantName' are required"})
return
}
at := auth.NewAccessToken(config.LivekitApiKey, config.LivekitApiSecret)
grant := &auth.VideoGrant{
RoomJoin: true,
Room: body.RoomName,
}
at.SetVideoGrant(grant).SetIdentity(body.ParticipantName)
token, err := at.ToJWT()
if err != nil {
c.JSON(http.StatusInternalServerError, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
}

View File

@ -0,0 +1,34 @@
package controllers
import (
"encoding/json"
"fmt"
"openvidu/go/config"
"os"
"github.com/gin-gonic/gin"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/webhook"
)
var authProvider *auth.SimpleKeyProvider
func WebhookRoutes(router *gin.Engine) {
// Initialize authProvider
authProvider = auth.NewSimpleKeyProvider(
config.LivekitApiKey, config.LivekitApiSecret,
)
router.POST("/livekit/webhook", receiveWebhook)
}
func receiveWebhook(c *gin.Context) {
webhookEvent, err := webhook.ReceiveWebhookEvent(c.Request, authProvider)
if err != nil {
fmt.Fprintf(os.Stderr, "error validating webhook event: %v", err)
return
}
json, _ := json.MarshalIndent(webhookEvent, "", " ")
fmt.Println("LiveKit Webhook:\n", string(json))
}

View File

@ -1,96 +1,101 @@
module openvidu/basic-go module openvidu/go
go 1.22.2 go 1.24.2
require ( require (
github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/cors v1.7.5
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/livekit/protocol v1.27.0 github.com/livekit/protocol v1.39.0
github.com/livekit/server-sdk-go/v2 v2.9.0
) )
require ( require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1 // indirect buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect buf.build/go/protovalidate v0.12.0 // indirect
buf.build/go/protoyaml v0.6.0 // indirect
cel.dev/expr v0.24.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect github.com/benbjohnson/clock v1.3.5 // indirect
github.com/bufbuild/protovalidate-go v0.6.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/bufbuild/protoyaml-go v0.1.9 // indirect github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/dennwc/iters v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/frostbyte73/core v0.0.12 // indirect github.com/frostbyte73/core v0.1.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gammazero/deque v0.2.1 // indirect github.com/gammazero/deque v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/google/cel-go v0.20.1 // indirect github.com/google/cel-go v0.25.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect github.com/jxskiss/base62 v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lithammer/shortuuid/v4 v4.0.0 // indirect github.com/lithammer/shortuuid/v4 v4.2.0 // indirect
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 // indirect github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 // indirect
github.com/livekit/psrpc v0.6.1-0.20240924010758-9f0a4268a3b9 // indirect github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded // indirect
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nats-io/nats.go v1.36.0 // indirect github.com/nats-io/nats.go v1.42.0 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pion/datachannel v1.5.5 // indirect github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v2 v2.3.13 // indirect github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.25 // indirect github.com/pion/interceptor v0.1.37 // indirect
github.com/pion/logging v0.2.2 // indirect github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns v0.0.12 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.12 // indirect github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.3 // indirect github.com/pion/rtp v1.8.15 // indirect
github.com/pion/sctp v1.8.12 // indirect github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.6 // indirect github.com/pion/sdp/v3 v3.0.11 // indirect
github.com/pion/srtp/v2 v2.0.18 // indirect github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun v0.6.1 // indirect github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.3 // indirect github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v2 v2.1.3 // indirect github.com/pion/turn/v4 v4.0.2 // indirect
github.com/pion/webrtc/v3 v3.2.28 // indirect github.com/pion/webrtc/v4 v4.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect github.com/redis/go-redis/v9 v9.8.0 // indirect
github.com/redis/go-redis/v9 v9.6.1 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/twitchtv/twirp v8.1.3+incompatible // indirect github.com/twitchtv/twirp v8.1.3+incompatible // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.2.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.25.0 // indirect golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/grpc v1.65.0 // indirect google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -1,204 +1,222 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1 h1:2IGhRovxlsOIQgx2ekZWo4wTPAYpck41+18ICxs37is= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1/go.mod h1:Tgn5bgL220vkFOI0KPStlcClPeOJzAv4uT+V8JXGUnw= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= buf.build/go/protovalidate v0.12.0 h1:4GKJotbspQjRCcqZMGVSuC8SjwZ/FmgtSuKDpKUTZew=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0=
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bufbuild/protovalidate-go v0.6.1 h1:uzW8r0CDvqApUChNj87VzZVoQSKhcVdw5UWOE605UIw= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bufbuild/protovalidate-go v0.6.1/go.mod h1:4BR3rKEJiUiTy+sqsusFn2ladOf0kYmA2Reo6BHSBgQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bufbuild/protoyaml-go v0.1.9 h1:anV5UtF1Mlvkkgp4NWA6U/zOnJFng8Orq4Vf3ZUQHBU=
github.com/bufbuild/protoyaml-go v0.1.9/go.mod h1:KCBItkvZOK/zwGueLdH1Wx1RLyFn5rCH7YjQrdty2Wc=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dennwc/iters v1.1.0 h1:PsS3DbOU7GxSUQO0e7SGmzHkPhtwOlwbqggJ++Bgnr8=
github.com/dennwc/iters v1.1.0/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frostbyte73/core v0.0.12 h1:kySA8+Os6eqnPFoExD2T7cehjSAY1MRyIViL0yTy2uc= github.com/frostbyte73/core v0.1.1 h1:ChhJOR7bAKOCPbA+lqDLE2cGKlCG5JXsDvvQr4YaJIA=
github.com/frostbyte73/core v0.0.12/go.mod h1:XsOGqrqe/VEV7+8vJ+3a8qnCIXNbKsoEiu/czs7nrcU= github.com/frostbyte73/core v0.1.1/go.mod h1:mhfOtR+xWAvwXiwor7jnqPMnu4fxbv1F2MwZ0BEpzZo=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58= github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5ATTo469PQPkqzdoU7be46ryiCDO3boc=
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/protocol v1.27.0 h1:qdZ8S4eH11XbBQxpG4eHh9GZC7weyydngWNvH2NTD+w= github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded h1:ylZPdnlX1RW9Z15SD4mp87vT2D2shsk0hpLJwSPcq3g=
github.com/livekit/protocol v1.27.0/go.mod h1:nxRzmQBKSYK64gqr7ABWwt78hvrgiO2wYuCojRYb7Gs= github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded/go.mod h1:mSNtYzSf6iY9xM3UX42VEI+STHvMgHmrYzEHPcdhB8A=
github.com/livekit/psrpc v0.6.1-0.20240924010758-9f0a4268a3b9 h1:33oBjGpVD9tYkDXQU42tnHl8eCX9G6PVUToBVuCUyOs= github.com/livekit/protocol v1.39.0 h1:xmpkEr0+29xiAu+Z/m7CmsGuLKHf5ba5+5rkGdMdZEY=
github.com/livekit/psrpc v0.6.1-0.20240924010758-9f0a4268a3b9/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0= github.com/livekit/protocol v1.39.0/go.mod h1:6HPISM0bkTXTk9RIaQTCe0IDbomBPz7Jwp+N3w5sqL0=
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c h1:WwEr0YBejYbKzk8LSaO9h8h0G9MnE7shyDu8yXQWmEc=
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c/go.mod h1:kmD+AZPkWu0MaXIMv57jhNlbiSZZ/Jx4bzlxBDVmJes=
github.com/livekit/server-sdk-go/v2 v2.9.0 h1:Kan/15kh/aT0Pz8d/YEltp7Sgbpmb4VhHVgCi9Pg6Rw=
github.com/livekit/server-sdk-go/v2 v2.9.0/go.mod h1:rDH1cleKLzbH1N/Pve8CGq2MsvQEQJUzEEEaMQTpkmc=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nats-io/nats.go v1.36.0 h1:suEUPuWzTSse/XhESwqLxXGuj8vGRuPRoG7MoRN/qyU= github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
github.com/nats-io/nats.go v1.36.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v2 v2.3.13 h1:xOxP+4V9nSDlUaGFRf/LvAuGHDXRcjIdsbbXPK/w7c8= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/ice/v2 v2.3.13/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc= github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y= github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.12 h1:bKWiX93XKgDZENEXCijvHRU/wRifm6JV5DGcH6twtSM= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtp v1.8.15 h1:MuhuGn1cxpVCPLNY1lI7F1tQ8Spntpgf12ob+pOYT8s=
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/rtp v1.8.15/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8= github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI=
github.com/pion/sctp v1.8.12 h1:2VX50pedElH+is6FI+OKyRTeN5oy4mrk2HjnGa3UCmY= github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/sctp v1.8.12/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI= github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= github.com/pion/webrtc/v4 v4.1.1 h1:PMFPtLg1kpD2pVtun+LGUzA3k54JdFl87WO0Z1+HKug=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/webrtc/v4 v4.1.1/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4=
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.28 h1:ienStxZ6HcjtH2UlmnFpMM0loENiYjaX437uIUpQSKo=
github.com/pion/webrtc/v3 v3.2.28/go.mod h1:PNRCEuQlibrmuBhOTnol9j6KkIbUG11aHLEfNpUYey0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -206,22 +224,27 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
@ -235,138 +258,74 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f h1:RARaIm8pxYuxyNPbBQf5igT7XdOyCNtat1qAT2ZxjU4= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,84 +1,23 @@
package main package main
import ( import (
"fmt" "openvidu/go/config"
"net/http" "openvidu/go/controllers"
"os"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/webhook"
) )
var SERVER_PORT string
var LIVEKIT_API_KEY string
var LIVEKIT_API_SECRET string
func createToken(context *gin.Context) {
var body struct {
RoomName string `json:"roomName"`
ParticipantName string `json:"participantName"`
}
if err := context.BindJSON(&body); err != nil {
context.JSON(http.StatusBadRequest, err.Error())
return
}
if body.RoomName == "" || body.ParticipantName == "" {
context.JSON(http.StatusBadRequest, gin.H{"errorMessage": "roomName and participantName are required"})
return
}
at := auth.NewAccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
grant := &auth.VideoGrant{
RoomJoin: true,
Room: body.RoomName,
}
at.SetVideoGrant(grant).SetIdentity(body.ParticipantName)
token, err := at.ToJWT()
if err != nil {
context.JSON(http.StatusInternalServerError, err.Error())
return
}
context.JSON(http.StatusOK, gin.H{"token": token})
}
func receiveWebhook(context *gin.Context) {
authProvider := auth.NewSimpleKeyProvider(
LIVEKIT_API_KEY, LIVEKIT_API_SECRET,
)
event, err := webhook.ReceiveWebhookEvent(context.Request, authProvider)
if err != nil {
fmt.Fprintf(os.Stderr, "error validating webhook event: %v", err)
return
}
fmt.Println("LiveKit Webhook", event)
}
func main() { func main() {
loadEnv() config.LoadEnv()
router := gin.Default() router := gin.Default()
router.Use(cors.Default()) router.Use(cors.Default())
router.POST("/token", createToken)
router.POST("/livekit/webhook", receiveWebhook)
router.Run(":" + SERVER_PORT)
}
func loadEnv() { controllers.TokenRoutes(router)
godotenv.Load() // Load environment variables from .env file controllers.WebhookRoutes(router)
SERVER_PORT = getEnv("SERVER_PORT", "6080") controllers.RoomRoutes(router)
LIVEKIT_API_KEY = getEnv("LIVEKIT_API", "devkey") controllers.EgressRoutes(router)
LIVEKIT_API_SECRET = getEnv("LIVEKIT_API_SECRET", "secret") controllers.IngressRoutes(router)
}
func getEnv(key, defaultValue string) string { router.Run(":" + config.ServerPort)
if value, ok := os.LookupEnv(key); ok {
return value
}
return defaultValue
} }

View File

@ -1,12 +1,12 @@
# Basic Java # OpenVidu Java
Basic server application built for Java with Spring Boot. It internally uses [livekit-server-sdk-kotlin](https://github.com/livekit/server-sdk-kotlin). OpenVidu server application built for Java with Spring Boot. It internally uses [livekit-server-sdk-kotlin](https://github.com/livekit/server-sdk-kotlin).
For further information, check the [tutorial documentation](https://livekit-tutorials.openvidu.io/tutorials/application-server/java/). For further information, check the [tutorial documentation](https://livekit-tutorials.openvidu.io/tutorials/application-server/java/).
## Prerequisites ## Prerequisites
- [Java >=17](https://www.java.com/en/download/) - [Java >=21](https://www.java.com/en/download/)
- [Maven](https://maven.apache.org/download.cgi) - [Maven](https://maven.apache.org/download.cgi)
## Run ## Run

View File

@ -6,18 +6,18 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version> <version>3.5.0</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>io.openvidu</groupId> <groupId>io.openvidu</groupId>
<artifactId>basic-java</artifactId> <artifactId>openvidu-java</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>basic-java</name> <name>basic-java</name>
<description>Basic server application built for Java with Spring Boot</description> <description>OpenVidu server application built for Java with Spring Boot</description>
<properties> <properties>
<java.version>17</java.version> <java.version>21</java.version>
</properties> </properties>
<dependencies> <dependencies>
@ -28,7 +28,7 @@
<dependency> <dependency>
<groupId>io.livekit</groupId> <groupId>io.livekit</groupId>
<artifactId>livekit-server</artifactId> <artifactId>livekit-server</artifactId>
<version>0.8.2</version> <version>0.9.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@ -1,13 +1,13 @@
package io.openvidu.basic.java; package io.openvidu.java;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class BasicJavaApplication { public class OpenViduJavaApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(BasicJavaApplication.class, args); SpringApplication.run(OpenViduJavaApplication.class, args);
} }
} }

View File

@ -0,0 +1,404 @@
package io.openvidu.java.controllers;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import io.livekit.server.EgressServiceClient;
import io.livekit.server.EncodedOutputs;
import jakarta.annotation.PostConstruct;
import livekit.LivekitEgress.DirectFileOutput;
import livekit.LivekitEgress.EgressInfo;
import livekit.LivekitEgress.EncodedFileOutput;
import livekit.LivekitEgress.EncodedFileType;
import livekit.LivekitEgress.StreamOutput;
import livekit.LivekitEgress.StreamProtocol;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/egresses")
public class EgressController {
private static final Logger LOGGER = LoggerFactory.getLogger(EgressController.class);
@Value("${livekit.url}")
private String LIVEKIT_URL;
@Value("${livekit.api.key}")
private String LIVEKIT_API_KEY;
@Value("${livekit.api.secret}")
private String LIVEKIT_API_SECRET;
private EgressServiceClient egressClient;
@PostConstruct
public void init() {
egressClient = EgressServiceClient.createClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
}
/**
* Create a new RoomComposite egress
*
* @param params JSON object with roomName
* @return JSON object with the created egress
*/
@PostMapping("/room-composite")
public ResponseEntity<Map<String, Object>> createRoomCompositeEgress(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
if (roomName == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' is required"));
}
try {
EncodedFileOutput output = EncodedFileOutput.newBuilder()
.setFilepath("{room_name}-{room_id}-{time}")
.setFileType(EncodedFileType.MP4)
.build();
EgressInfo egress = egressClient.startRoomCompositeEgress(roomName, output, "grid")
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("egress", convertToJson(egress)));
} catch (Exception e) {
String errorMessage = "Error creating RoomComposite egress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Create a new RoomComposite egress to stream to a URL
*
* @param params JSON object with roomName and streamUrl
* @return JSON object with the created egress
*/
@PostMapping("/stream")
public ResponseEntity<Map<String, Object>> createStreamEgress(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
String streamUrl = params.get("streamUrl");
if (roomName == null || streamUrl == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' and 'streamUrl' are required"));
}
try {
StreamOutput output = StreamOutput.newBuilder()
.setProtocol(StreamProtocol.RTMP)
.addUrls(streamUrl)
.build();
EgressInfo egress = egressClient.startRoomCompositeEgress(roomName, output, "grid")
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("egress", convertToJson(egress)));
} catch (Exception e) {
String errorMessage = "Error creating RoomComposite egress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Create a new Participant egress
*
* @param params JSON object with roomName and participantIdentity
* @return JSON object with the created egress
*/
@PostMapping("/participant")
public ResponseEntity<Map<String, Object>> createParticipantEgress(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
String participantIdentity = params.get("participantIdentity");
if (roomName == null || participantIdentity == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' and 'participantIdentity' are required"));
}
try {
EncodedFileOutput output = EncodedFileOutput.newBuilder()
.setFilepath("{room_name}-{room_id}-{publisher_identity}-{time}")
.setFileType(EncodedFileType.MP4)
.build();
EncodedOutputs outputs = new EncodedOutputs(output, null, null, null);
EgressInfo egress = egressClient.startParticipantEgress(roomName, participantIdentity, outputs, false)
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("egress", convertToJson(egress)));
} catch (Exception e) {
String errorMessage = "Error creating Participant egress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Create a new TrackComposite egress
*
* @param params JSON object with roomName, videoTrackId and audioTrackId
* @return JSON object with the created egress
*/
@PostMapping("/track-composite")
public ResponseEntity<Map<String, Object>> createTrackCompositeEgress(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
String videoTrackId = params.get("videoTrackId");
String audioTrackId = params.get("audioTrackId");
if (roomName == null || videoTrackId == null || audioTrackId == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName', 'videoTrackId' and 'audioTrackId' are required"));
}
try {
EncodedFileOutput output = EncodedFileOutput.newBuilder()
.setFilepath("{room_name}-{room_id}-{publisher_identity}-{time}")
.setFileType(EncodedFileType.MP4)
.build();
EgressInfo egress = egressClient.startTrackCompositeEgress(roomName, output, audioTrackId, videoTrackId)
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("egress", convertToJson(egress)));
} catch (Exception e) {
String errorMessage = "Error creating TrackComposite egress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Create a new Track egress
*
* @param params JSON object with roomName and trackId
* @return JSON object with the created egress
*/
@PostMapping("/track")
public ResponseEntity<Map<String, Object>> createTrackEgress(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
String trackId = params.get("trackId");
if (roomName == null || trackId == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' and 'trackId' are required"));
}
try {
DirectFileOutput output = DirectFileOutput.newBuilder()
.setFilepath("{room_name}-{room_id}-{publisher_identity}-{track_source}-{track_id}-{time}")
.build();
EgressInfo egress = egressClient.startTrackEgress(roomName, output, trackId)
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("egress", convertToJson(egress)));
} catch (Exception e) {
String errorMessage = "Error creating Track egress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Create a new Web egress
*
* @param params JSON object with url
* @return JSON object with the created egress
*/
@PostMapping("/web")
public ResponseEntity<Map<String, Object>> createWebEgress(@RequestBody Map<String, String> params) {
String url = params.get("url");
if (url == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'url' is required"));
}
try {
EncodedFileOutput output = EncodedFileOutput.newBuilder()
.setFilepath("{time}")
.setFileType(EncodedFileType.MP4)
.build();
EgressInfo egress = egressClient.startWebEgress(url, output)
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("egress", convertToJson(egress)));
} catch (Exception e) {
String errorMessage = "Error creating Web egress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* List egresses
* If an egress ID is provided, only that egress is listed
* If a room name is provided, only egresses for that room are listed
* If active is true, only active egresses are listed
*
* @param egressId Optional egress ID to filter
* @param roomName Optional room name to filter
* @param active Optional flag to filter active egresses
* @return JSON object with the list of egresses
*/
@GetMapping
public ResponseEntity<Map<String, Object>> listEgresses(@RequestParam(required = false) String egressId,
@RequestParam(required = false) String roomName, @RequestParam(required = false) Boolean active) {
try {
List<EgressInfo> egresses = egressClient.listEgress(roomName, egressId, active)
.execute()
.body();
return ResponseEntity.ok(Map.of("egresses", convertListToJson(egresses)));
} catch (Exception e) {
String errorMessage = "Error listing egresses";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Update egress layout
*
* @param params JSON object with layout
* @return JSON object with the updated egress
*/
@PostMapping("/{egressId}/layout")
public ResponseEntity<Map<String, Object>> updateEgressLayout(@PathVariable String egressId,
@RequestBody Map<String, String> params) {
String layout = params.get("layout");
if (layout == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'layout' is required"));
}
try {
EgressInfo egress = egressClient.updateLayout(egressId, layout)
.execute()
.body();
return ResponseEntity.ok(Map.of("egress", convertToJson(egress)));
} catch (Exception e) {
String errorMessage = "Error updating egress layout";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Add/remove stream URLs to an egress
*
* @param params JSON object with streamUrlsToAdd and streamUrlsToRemove
* @return JSON object with the updated egress
*/
@PostMapping("/{egressId}/streams")
public ResponseEntity<Map<String, Object>> updateEgressStreams(@PathVariable String egressId,
@RequestBody Map<String, Object> params) {
Object streamUrlsToAddObj = params.get("streamUrlsToAdd");
Object streamUrlsToRemoveObj = params.get("streamUrlsToRemove");
if (!isStringList(streamUrlsToAddObj) || !isStringList(streamUrlsToRemoveObj)) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage",
"'streamUrlsToAdd' and 'streamUrlsToRemove' are required and must be arrays"));
}
List<String> streamUrlsToAdd = convertToStringList(streamUrlsToAddObj);
List<String> streamUrlsToRemove = convertToStringList(streamUrlsToRemoveObj);
try {
EgressInfo egress = egressClient.updateStream(egressId, streamUrlsToAdd, streamUrlsToRemove)
.execute()
.body();
return ResponseEntity.ok(Map.of("egress", convertToJson(egress)));
} catch (Exception e) {
String errorMessage = "Error updating egress streams";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Stop an egress
*
* @return JSON object with success message
*/
@DeleteMapping("/{egressId}")
public ResponseEntity<Map<String, Object>> stopEgress(@PathVariable String egressId) {
try {
egressClient.stopEgress(egressId)
.execute();
return ResponseEntity.ok(Map.of("message", "Egress stopped"));
} catch (Exception e) {
String errorMessage = "Error stopping egress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
private Map<String, Object> convertToJson(EgressInfo egress)
throws InvalidProtocolBufferException, JsonProcessingException, JsonMappingException {
ObjectMapper objectMapper = new ObjectMapper();
String rawJson = JsonFormat.printer().print(egress);
Map<String, Object> json = objectMapper.readValue(rawJson, new TypeReference<Map<String, Object>>() {
});
return json;
}
private List<Map<String, Object>> convertListToJson(List<EgressInfo> egresses) {
List<Map<String, Object>> jsonList = egresses.stream().map(egress -> {
try {
return convertToJson(egress);
} catch (Exception e) {
LOGGER.error("Error parsing egress", e);
return null;
}
}).toList();
return jsonList;
}
private boolean isStringList(Object obj) {
return obj instanceof List<?> list && list.stream().allMatch(String.class::isInstance);
}
private List<String> convertToStringList(Object obj) {
return ((List<?>) obj).stream()
.map(String.class::cast)
.toList();
}
}

View File

@ -0,0 +1,246 @@
package io.openvidu.java.controllers;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import io.livekit.server.IngressServiceClient;
import jakarta.annotation.PostConstruct;
import livekit.LivekitIngress.IngressInfo;
import livekit.LivekitIngress.IngressInput;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/ingresses")
public class IngressController {
private static final Logger LOGGER = LoggerFactory.getLogger(IngressController.class);
@Value("${livekit.url}")
private String LIVEKIT_URL;
@Value("${livekit.api.key}")
private String LIVEKIT_API_KEY;
@Value("${livekit.api.secret}")
private String LIVEKIT_API_SECRET;
private IngressServiceClient ingressClient;
@PostConstruct
public void init() {
ingressClient = IngressServiceClient.createClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
}
/**
* Create a new RTMP ingress
*
* @param params JSON object with roomName and participantIdentity
* @return JSON object with the created ingress
*/
@PostMapping("/rtmp")
public ResponseEntity<Map<String, Object>> createRTMPIngress(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
String participantIdentity = params.get("participantIdentity");
if (roomName == null || participantIdentity == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' and 'participantIdentity' are required"));
}
try {
IngressInfo ingress = ingressClient
.createIngress("rtmp-ingress", roomName, participantIdentity, null, IngressInput.RTMP_INPUT)
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("ingress", convertToJson(ingress)));
} catch (Exception e) {
String errorMessage = "Error creating RTMP ingress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Create a new WHIP ingress
*
* @param params JSON object with roomName and participantIdentity
* @return JSON object with the created ingress
*/
@PostMapping("/whip")
public ResponseEntity<Map<String, Object>> createWHIPIngress(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
String participantIdentity = params.get("participantIdentity");
if (roomName == null || participantIdentity == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' and 'participantIdentity' are required"));
}
try {
IngressInfo ingress = ingressClient
.createIngress("whip-ingress", roomName, participantIdentity, null, IngressInput.WHIP_INPUT)
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("ingress", convertToJson(ingress)));
} catch (Exception e) {
String errorMessage = "Error creating WHIP ingress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Create a new URL ingress
*
* @param params JSON object with roomName, participantIdentity and url
* @return JSON object with the created ingress
*/
@PostMapping("/url")
public ResponseEntity<Map<String, Object>> createURLIngress(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
String participantIdentity = params.get("participantIdentity");
String url = params.get("url");
if (roomName == null || participantIdentity == null || url == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName', 'participantIdentity' and 'url' are required"));
}
try {
IngressInfo ingress = ingressClient
.createIngress("url-ingress", roomName, participantIdentity, null, IngressInput.URL_INPUT, null,
null, null, null, url)
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("ingress", convertToJson(ingress)));
} catch (Exception e) {
String errorMessage = "Error creating URL ingress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* List ingresses
* If an ingress ID is provided, only that ingress is listed
* If a room name is provided, only ingresses for that room are listed
*
* @param ingressId Optional ingress ID to filter
* @param roomName Optional room name to filter
* @return JSON object with the list of ingresses
*/
@GetMapping
public ResponseEntity<Map<String, Object>> listIngresses(@RequestParam(required = false) String ingressId,
@RequestParam(required = false) String roomName) {
try {
List<IngressInfo> ingresses = ingressClient.listIngress(roomName, ingressId)
.execute()
.body();
return ResponseEntity.ok(Map.of("ingresses", convertListToJson(ingresses)));
} catch (Exception e) {
String errorMessage = "Error listing ingresses";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Update ingress
*
* @param params JSON object with roomName
* @return JSON object with the updated ingress
*/
@PatchMapping("/{ingressId}")
public ResponseEntity<Map<String, Object>> updateIngress(@PathVariable String ingressId,
@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
if (roomName == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' is required"));
}
try {
IngressInfo ingress = ingressClient
.updateIngress(ingressId, "updated-ingress", roomName)
.execute()
.body();
return ResponseEntity.ok(Map.of("ingress", convertToJson(ingress)));
} catch (Exception e) {
String errorMessage = "Error updating ingress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Delete ingress
*
* @return JSON object with success message
*/
@DeleteMapping("/{ingressId}")
public ResponseEntity<Map<String, Object>> deleteIngress(@PathVariable String ingressId) {
try {
ingressClient.deleteIngress(ingressId)
.execute();
return ResponseEntity.ok(Map.of("message", "Ingress deleted"));
} catch (Exception e) {
String errorMessage = "Error deleting ingress";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
private Map<String, Object> convertToJson(IngressInfo ingress)
throws InvalidProtocolBufferException, JsonProcessingException, JsonMappingException {
ObjectMapper objectMapper = new ObjectMapper();
String rawJson = JsonFormat.printer().print(ingress);
Map<String, Object> json = objectMapper.readValue(rawJson, new TypeReference<Map<String, Object>>() {
});
return json;
}
private List<Map<String, Object>> convertListToJson(List<IngressInfo> ingresses) {
List<Map<String, Object>> jsonList = ingresses.stream().map(ingress -> {
try {
return convertToJson(ingress);
} catch (Exception e) {
LOGGER.error("Error parsing ingress", e);
return null;
}
}).toList();
return jsonList;
}
}

View File

@ -0,0 +1,401 @@
package io.openvidu.java.controllers;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageOrBuilder;
import com.google.protobuf.util.JsonFormat;
import io.livekit.server.RoomServiceClient;
import jakarta.annotation.PostConstruct;
import livekit.LivekitModels.Room;
import livekit.LivekitModels.TrackInfo;
import livekit.LivekitModels.DataPacket;
import livekit.LivekitModels.ParticipantInfo;
import livekit.LivekitModels.ParticipantPermission;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/rooms")
public class RoomController {
private static final Logger LOGGER = LoggerFactory.getLogger(RoomController.class);
@Value("${livekit.url}")
private String LIVEKIT_URL;
@Value("${livekit.api.key}")
private String LIVEKIT_API_KEY;
@Value("${livekit.api.secret}")
private String LIVEKIT_API_SECRET;
private RoomServiceClient roomClient;
@PostConstruct
public void init() {
roomClient = RoomServiceClient.createClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
}
/**
* Create a new room
*
* @param params JSON object with roomName
* @return JSON object with the created room
*/
@PostMapping
public ResponseEntity<Map<String, Object>> createRoom(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName");
if (roomName == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' is required"));
}
try {
Room room = roomClient.createRoom(roomName)
.execute()
.body();
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("room", convertToJson(room)));
} catch (Exception e) {
String errorMessage = "Error creating room";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* List rooms.
* If a room name is provided, only that room is listed
*
* @param roomName Optional room name to filter
* @return JSON object with the list of rooms
*/
@GetMapping
public ResponseEntity<Map<String, Object>> listRooms(@RequestParam(required = false) String roomName) {
try {
List<String> roomNames = roomName != null ? List.of(roomName) : null;
List<Room> rooms = roomClient.listRooms(roomNames)
.execute()
.body();
return ResponseEntity.ok(Map.of("rooms", convertListToJson(rooms)));
} catch (Exception e) {
String errorMessage = "Error listing rooms";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Update room metadata
*
* @param params JSON object with metadata
* @return JSON object with the updated room
*/
@PostMapping("/{roomName}/metadata")
public ResponseEntity<Map<String, Object>> updateRoomMetadata(@PathVariable String roomName,
@RequestBody Map<String, String> params) {
String metadata = params.get("metadata");
if (metadata == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'metadata' is required"));
}
try {
Room room = roomClient.updateRoomMetadata(roomName, metadata)
.execute()
.body();
return ResponseEntity.ok(Map.of("room", convertToJson(room)));
} catch (Exception e) {
String errorMessage = "Error updating room metadata";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Send data message to participants in a room
*
* @param params JSON object with data
* @return JSON object with success message
*/
@PostMapping("/{roomName}/send-data")
public ResponseEntity<Map<String, Object>> sendData(@PathVariable String roomName,
@RequestBody Map<String, Object> params) {
Object rawData = params.get("data");
if (rawData == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'data' is required"));
}
try {
ObjectMapper objectMapper = new ObjectMapper();
byte[] data = objectMapper.writeValueAsBytes(rawData);
roomClient.sendData(roomName, data, DataPacket.Kind.RELIABLE, List.of(), List.of(), "chat")
.execute();
return ResponseEntity.ok(Map.of("message", "Data message sent"));
} catch (Exception e) {
String errorMessage = "Error sending data message";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Delete a room
*
* @return JSON object with success message
*/
@DeleteMapping("/{roomName}")
public ResponseEntity<Map<String, Object>> deleteRoom(@PathVariable String roomName) {
try {
roomClient.deleteRoom(roomName)
.execute();
return ResponseEntity.ok(Map.of("message", "Room deleted"));
} catch (Exception e) {
String errorMessage = "Error deleting room";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* List participants in a room
*
* @return JSON object with the list of participants
*/
@GetMapping("/{roomName}/participants")
public ResponseEntity<Map<String, Object>> listParticipants(@PathVariable String roomName) {
try {
List<ParticipantInfo> participants = roomClient.listParticipants(roomName)
.execute()
.body();
return ResponseEntity.ok(Map.of("participants", convertListToJson(participants)));
} catch (Exception e) {
String errorMessage = "Error getting participants";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Get a participant in a room
*
* @return JSON object with the participant
*/
@GetMapping("/{roomName}/participants/{participantIdentity}")
public ResponseEntity<Map<String, Object>> getParticipant(@PathVariable String roomName,
@PathVariable String participantIdentity) {
try {
ParticipantInfo participant = roomClient.getParticipant(roomName, participantIdentity)
.execute()
.body();
return ResponseEntity.ok(Map.of("participant", convertToJson(participant)));
} catch (Exception e) {
String errorMessage = "Error getting participant";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Update a participant in a room
*
* @param params JSON object with metadata (optional)
* @return JSON object with the updated participant
*/
@PatchMapping("/{roomName}/participants/{participantIdentity}")
public ResponseEntity<Map<String, Object>> updateParticipant(@PathVariable String roomName,
@PathVariable String participantIdentity, @RequestBody Map<String, String> params) {
String metadata = params.get("metadata");
try {
ParticipantPermission permissions = ParticipantPermission.newBuilder()
.setCanPublish(false)
.setCanSubscribe(true)
.build();
ParticipantInfo participant = roomClient
.updateParticipant(roomName, participantIdentity, null, metadata, permissions)
.execute()
.body();
return ResponseEntity.ok(Map.of("participant", convertToJson(participant)));
} catch (Exception e) {
String errorMessage = "Error updating participant";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Remove a participant from a room
*
* @return JSON object with success message
*/
@DeleteMapping("/{roomName}/participants/{participantIdentity}")
public ResponseEntity<Map<String, Object>> removeParticipant(@PathVariable String roomName,
@PathVariable String participantIdentity) {
try {
roomClient.removeParticipant(roomName, participantIdentity)
.execute();
return ResponseEntity.ok(Map.of("message", "Participant removed"));
} catch (Exception e) {
String errorMessage = "Error removing participant";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Mute published track of a participant in a room
*
* @param params JSON object with trackId
* @return JSON object with updated track
*/
@PostMapping("/{roomName}/participants/{participantIdentity}/mute")
public ResponseEntity<Map<String, Object>> muteParticipant(@PathVariable String roomName,
@PathVariable String participantIdentity, @RequestBody Map<String, String> params) {
String trackId = params.get("trackId");
if (trackId == null) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'trackId' is required"));
}
try {
TrackInfo track = roomClient.mutePublishedTrack(roomName, participantIdentity, trackId, true)
.execute()
.body();
return ResponseEntity.ok(Map.of("track", convertToJson(track)));
} catch (Exception e) {
String errorMessage = "Error muting track";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Subscribe participant to tracks in a room
*
* @param params JSON object with list of trackIds
* @return JSON object with success message
*/
@PostMapping("/{roomName}/participants/{participantIdentity}/subscribe")
public ResponseEntity<Map<String, Object>> subscribeParticipant(@PathVariable String roomName,
@PathVariable String participantIdentity, @RequestBody Map<String, Object> params) {
Object trackIdsObj = params.get("trackIds");
if (!isStringList(trackIdsObj)) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'trackIds' is required and must be an array"));
}
List<String> trackIds = convertToStringList(trackIdsObj);
try {
roomClient.updateSubscriptions(roomName, participantIdentity, trackIds, true)
.execute();
return ResponseEntity.ok(Map.of("message", "Participant subscribed to tracks"));
} catch (Exception e) {
String errorMessage = "Error subscribing participant to tracks";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
/**
* Unsubscribe participant from tracks in a room
*
* @param params JSON object with list of trackIds
* @return JSON object with success message
*/
@PostMapping("/{roomName}/participants/{participantIdentity}/unsubscribe")
public ResponseEntity<Map<String, Object>> unsubscribeParticipant(@PathVariable String roomName,
@PathVariable String participantIdentity, @RequestBody Map<String, Object> params) {
Object trackIdsObj = params.get("trackIds");
if (!isStringList(trackIdsObj)) {
return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'trackIds' is required and must be an array"));
}
List<String> trackIds = convertToStringList(trackIdsObj);
try {
roomClient.updateSubscriptions(roomName, participantIdentity, trackIds, false)
.execute();
return ResponseEntity.ok(Map.of("message", "Participant unsubscribed from tracks"));
} catch (Exception e) {
String errorMessage = "Error unsubscribing participant from tracks";
LOGGER.error(errorMessage, e);
return ResponseEntity.internalServerError()
.body(Map.of("errorMessage", errorMessage));
}
}
private <T extends MessageOrBuilder> Map<String, Object> convertToJson(T object)
throws InvalidProtocolBufferException, JsonProcessingException, JsonMappingException {
ObjectMapper objectMapper = new ObjectMapper();
String rawJson = JsonFormat.printer().print(object);
Map<String, Object> json = objectMapper.readValue(rawJson, new TypeReference<Map<String, Object>>() {
});
return json;
}
private <T extends MessageOrBuilder> List<Map<String, Object>> convertListToJson(List<T> objects) {
List<Map<String, Object>> jsonList = objects.stream().map(object -> {
try {
return convertToJson(object);
} catch (Exception e) {
LOGGER.error("Error parsing egress", e);
return null;
}
}).toList();
return jsonList;
}
private boolean isStringList(Object obj) {
return obj instanceof List<?> list && !list.isEmpty() && list.stream().allMatch(String.class::isInstance);
}
private List<String> convertToStringList(Object obj) {
return ((List<?>) obj).stream()
.map(String.class::cast)
.toList();
}
}

View File

@ -1,4 +1,4 @@
package io.openvidu.basic.java; package io.openvidu.java.controllers;
import java.util.Map; import java.util.Map;
@ -7,18 +7,19 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.livekit.server.AccessToken; import io.livekit.server.AccessToken;
import io.livekit.server.CanPublish;
import io.livekit.server.CanSubscribe;
import io.livekit.server.RoomJoin; import io.livekit.server.RoomJoin;
import io.livekit.server.RoomName; import io.livekit.server.RoomName;
import io.livekit.server.WebhookReceiver;
import livekit.LivekitWebhook.WebhookEvent;
@CrossOrigin(origins = "*") @CrossOrigin(origins = "*")
@RestController @RestController
public class Controller { @RequestMapping("/token")
public class TokenController {
@Value("${livekit.api.key}") @Value("${livekit.api.key}")
private String LIVEKIT_API_KEY; private String LIVEKIT_API_KEY;
@ -27,36 +28,30 @@ public class Controller {
private String LIVEKIT_API_SECRET; private String LIVEKIT_API_SECRET;
/** /**
* Create a new token for a participant to join a room
*
* @param params JSON object with roomName and participantName * @param params JSON object with roomName and participantName
* @return JSON object with the JWT token * @return JSON object with the JWT token
*/ */
@PostMapping(value = "/token") @PostMapping
public ResponseEntity<Map<String, String>> createToken(@RequestBody Map<String, String> params) { public ResponseEntity<Map<String, String>> createToken(@RequestBody Map<String, String> params) {
String roomName = params.get("roomName"); String roomName = params.get("roomName");
String participantName = params.get("participantName"); String participantName = params.get("participantName");
if (roomName == null || participantName == null) { if (roomName == null || participantName == null) {
return ResponseEntity.badRequest().body(Map.of("errorMessage", "roomName and participantName are required")); return ResponseEntity.badRequest()
.body(Map.of("errorMessage", "'roomName' and 'participantName' are required"));
} }
AccessToken token = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET); AccessToken token = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
token.setName(participantName); token.setName(participantName);
token.setIdentity(participantName); token.setIdentity(participantName);
token.addGrants(new RoomJoin(true), new RoomName(roomName)); token.addGrants(
new RoomJoin(true),
new RoomName(roomName),
new CanPublish(true),
new CanSubscribe(true));
return ResponseEntity.ok(Map.of("token", token.toJwt())); return ResponseEntity.ok(Map.of("token", token.toJwt()));
} }
@PostMapping(value = "/livekit/webhook", consumes = "application/webhook+json")
public ResponseEntity<String> receiveWebhook(@RequestHeader("Authorization") String authHeader, @RequestBody String body) {
WebhookReceiver webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
try {
WebhookEvent event = webhookReceiver.receive(body, authHeader);
System.out.println("LiveKit Webhook: " + event.toString());
} catch (Exception e) {
System.err.println("Error validating webhook event: " + e.getMessage());
}
return ResponseEntity.ok("ok");
}
} }

View File

@ -0,0 +1,50 @@
package io.openvidu.java.controllers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.livekit.server.WebhookReceiver;
import jakarta.annotation.PostConstruct;
import livekit.LivekitWebhook.WebhookEvent;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping(value = "/livekit/webhook", consumes = "application/webhook+json")
public class WebhookController {
private static final Logger LOGGER = LoggerFactory.getLogger(WebhookController.class);
@Value("${livekit.api.key}")
private String LIVEKIT_API_KEY;
@Value("${livekit.api.secret}")
private String LIVEKIT_API_SECRET;
private WebhookReceiver webhookReceiver;
@PostConstruct
public void init() {
webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
}
@PostMapping()
public ResponseEntity<String> receiveWebhook(@RequestHeader("Authorization") String authHeader,
@RequestBody String body) {
try {
WebhookEvent webhookEvent = webhookReceiver.receive(body, authHeader);
System.out.println("LiveKit Webhook: " + webhookEvent.toString());
} catch (Exception e) {
LOGGER.error("Error validating webhook event", e);
}
return ResponseEntity.ok("ok");
}
}

View File

@ -1,7 +1,8 @@
spring.application.name=basic-java spring.application.name=openvidu-java
server.port=${SERVER_PORT:6080} server.port=${SERVER_PORT:6080}
server.ssl.enabled=false server.ssl.enabled=false
# LiveKit configuration # LiveKit configuration
livekit.url=${LIVEKIT_URL:http://localhost:7880}
livekit.api.key=${LIVEKIT_API_KEY:devkey} livekit.api.key=${LIVEKIT_API_KEY:devkey}
livekit.api.secret=${LIVEKIT_API_SECRET:secret} livekit.api.secret=${LIVEKIT_API_SECRET:secret}

View File

@ -1,10 +1,10 @@
package io.openvidu.basic.java; package io.openvidu.java;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest @SpringBootTest
class BasicJavaApplicationTests { class OpenViduJavaApplicationTests {
@Test @Test
void contextLoads() { void contextLoads() {

View File

@ -1,6 +1,6 @@
# Basic Node # OpenVidu Node
Basic server application built for Node.js with Express. It internally uses [livekit-server-sdk-js](https://docs.livekit.io/server-sdk-js/). OpenVidu server application built for Node.js with Express. It internally uses [livekit-server-sdk-js](https://docs.livekit.io/server-sdk-js/).
For further information, check the [tutorial documentation](https://livekit-tutorials.openvidu.io/tutorials/application-server/node/). For further information, check the [tutorial documentation](https://livekit-tutorials.openvidu.io/tutorials/application-server/node/).

View File

@ -1,53 +0,0 @@
import "dotenv/config";
import express from "express";
import cors from "cors";
import { AccessToken, WebhookReceiver } from "livekit-server-sdk";
const SERVER_PORT = process.env.SERVER_PORT || 6080;
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY || "devkey";
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET || "secret";
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.raw({ type: "application/webhook+json" }));
app.post("/token", async (req, res) => {
const roomName = req.body.roomName;
const participantName = req.body.participantName;
if (!roomName || !participantName) {
res.status(400).json({ errorMessage: "roomName and participantName are required" });
return;
}
const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
identity: participantName,
});
at.addGrant({ roomJoin: true, room: roomName });
const token = await at.toJwt();
res.json({ token });
});
const webhookReceiver = new WebhookReceiver(
LIVEKIT_API_KEY,
LIVEKIT_API_SECRET
);
app.post("/livekit/webhook", async (req, res) => {
try {
const event = await webhookReceiver.receive(
req.body,
req.get("Authorization")
);
console.log(event);
} catch (error) {
console.error("Error validating webhook event", error);
}
res.status(200).send();
});
app.listen(SERVER_PORT, () => {
console.log("Server started on port:", SERVER_PORT);
});

View File

@ -1,29 +1,29 @@
{ {
"name": "basic-node", "name": "openvidu-node",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "basic-node", "name": "openvidu-node",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"cors": "2.8.5", "cors": "2.8.5",
"dotenv": "16.4.5", "dotenv": "16.5.0",
"express": "5.0.1", "express": "5.1.0",
"livekit-server-sdk": "^2.7.2" "livekit-server-sdk": "2.13.0"
} }
}, },
"node_modules/@bufbuild/protobuf": { "node_modules/@bufbuild/protobuf": {
"version": "1.10.0", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz",
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)" "license": "(Apache-2.0 AND BSD-3-Clause)"
}, },
"node_modules/@livekit/protocol": { "node_modules/@livekit/protocol": {
"version": "1.27.0", "version": "1.39.0",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.27.0.tgz", "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.39.0.tgz",
"integrity": "sha512-jVb4zljNaYKoLiL5MBjGiO1+QKVsxMqXT/c0dwcKUW7NCLjAZXucoQVV1Y79FCbKwVnOCOtI6wwteEntbfk/Qw==", "integrity": "sha512-Zar6711kJk1MjI+63DJoyCxv8deNphIIan7tK9C5f+Zy3v2Pvjs6Yz7cFoOeDK/rY7b7deA2wfYxUi2S5XLp7w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^1.10.0" "@bufbuild/protobuf": "^1.10.0"
@ -42,90 +42,24 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/array-flatten": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz",
"integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==",
"license": "MIT"
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.0.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.1.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-PagxbjvuPH6tv0f/kdVbFGcb79D236SLcDTs6DrQ7GizJ88S1UWP4nMXFEo/I4fdhGRGabvFfFjVGm3M7U8JwA==", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "^3.1.2",
"content-type": "~1.0.5", "content-type": "^1.0.5",
"debug": "3.1.0", "debug": "^4.4.0",
"destroy": "1.2.0", "http-errors": "^2.0.0",
"http-errors": "2.0.0", "iconv-lite": "^0.6.3",
"iconv-lite": "0.5.2", "on-finished": "^2.4.1",
"on-finished": "2.4.1", "qs": "^6.14.0",
"qs": "6.13.0",
"raw-body": "^3.0.0", "raw-body": "^3.0.0",
"type-is": "~1.6.18", "type-is": "^2.0.0"
"unpipe": "1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.10" "node": ">=18"
}
},
"node_modules/body-parser/node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/body-parser/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/body-parser/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/body-parser/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/body-parser/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
} }
}, },
"node_modules/bytes": { "node_modules/bytes": {
@ -137,17 +71,27 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/call-bind": { "node_modules/call-bind-apply-helpers": {
"version": "1.0.7", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"function-bind": "^1.1.2", "function-bind": "^1.1.2"
"get-intrinsic": "^1.2.4", },
"set-function-length": "^1.2.1" "engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -208,18 +152,18 @@
} }
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.7.1", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.2.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.6.0" "node": ">=6.6.0"
@ -239,12 +183,12 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.6", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "^2.1.3"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -255,23 +199,6 @@
} }
} }
}, },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -281,20 +208,10 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.5", "version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -303,6 +220,20 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -319,13 +250,10 @@
} }
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT", "license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@ -339,6 +267,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -355,90 +295,64 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "5.0.1", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.0.1", "body-parser": "^2.2.0",
"content-disposition": "^1.0.0", "content-disposition": "^1.0.0",
"content-type": "~1.0.4", "content-type": "^1.0.5",
"cookie": "0.7.1", "cookie": "^0.7.1",
"cookie-signature": "^1.2.1", "cookie-signature": "^1.2.1",
"debug": "4.3.6", "debug": "^4.4.0",
"depd": "2.0.0", "encodeurl": "^2.0.0",
"encodeurl": "~2.0.0", "escape-html": "^1.0.3",
"escape-html": "~1.0.3", "etag": "^1.8.1",
"etag": "~1.8.1", "finalhandler": "^2.1.0",
"finalhandler": "^2.0.0", "fresh": "^2.0.0",
"fresh": "2.0.0", "http-errors": "^2.0.0",
"http-errors": "2.0.0",
"merge-descriptors": "^2.0.0", "merge-descriptors": "^2.0.0",
"methods": "~1.1.2",
"mime-types": "^3.0.0", "mime-types": "^3.0.0",
"on-finished": "2.4.1", "on-finished": "^2.4.1",
"once": "1.4.0", "once": "^1.4.0",
"parseurl": "~1.3.3", "parseurl": "^1.3.3",
"proxy-addr": "~2.0.7", "proxy-addr": "^2.0.7",
"qs": "6.13.0", "qs": "^6.14.0",
"range-parser": "~1.2.1", "range-parser": "^1.2.1",
"router": "^2.0.0", "router": "^2.2.0",
"safe-buffer": "5.2.1",
"send": "^1.1.0", "send": "^1.1.0",
"serve-static": "^2.1.0", "serve-static": "^2.2.0",
"setprototypeof": "1.2.0", "statuses": "^2.0.1",
"statuses": "2.0.1", "type-is": "^2.0.1",
"type-is": "^2.0.0", "vary": "^1.1.2"
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.0.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "^4.4.0",
"encodeurl": "~1.0.2", "encodeurl": "^2.0.0",
"escape-html": "~1.0.3", "escape-html": "^1.0.3",
"on-finished": "2.4.1", "on-finished": "^2.4.1",
"parseurl": "~1.3.3", "parseurl": "^1.3.3",
"statuses": "2.0.1", "statuses": "^2.0.1"
"unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/finalhandler/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/finalhandler/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -467,16 +381,21 @@
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"has-proto": "^1.0.1", "get-proto": "^1.0.1",
"has-symbols": "^1.0.3", "gopd": "^1.2.0",
"hasown": "^2.0.0" "has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -485,34 +404,23 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gopd": { "node_modules/get-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.1.3" "dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}, },
"funding": { "engines": {
"url": "https://github.com/sponsors/ljharb" "node": ">= 0.4"
} }
}, },
"node_modules/has-property-descriptors": { "node_modules/gopd": {
"version": "1.0.2", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -522,9 +430,9 @@
} }
}, },
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.0.3", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -562,12 +470,12 @@
} }
}, },
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.5.2", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3.0.0"
}, },
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -595,26 +503,27 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/jose": { "node_modules/jose": {
"version": "5.9.6", "version": "5.10.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
"integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
}, },
"node_modules/livekit-server-sdk": { "node_modules/livekit-server-sdk": {
"version": "2.7.2", "version": "2.13.0",
"resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-2.7.2.tgz", "resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-2.13.0.tgz",
"integrity": "sha512-qDNRXeo+WMnY5nKSug7KHJ9er9JIuKi+r7H9ZaSBbmbaOt62i0b4BrHBMFSMr8pAuWzuSxihCFa29q5QvFc5fw==", "integrity": "sha512-fQJI/zEJRPeXKdKMkEfJNYSSnvmuPQsk2Q+X6tPfUrJPy7fnyYPax/icf/CZ8EYZQBhFgSD7WaKOYGSSfGSyZw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@livekit/protocol": "^1.23.0", "@bufbuild/protobuf": "^1.7.2",
"@livekit/protocol": "^1.38.0",
"camelcase-keys": "^9.0.0", "camelcase-keys": "^9.0.0",
"jose": "^5.1.2" "jose": "^5.1.2"
}, },
"engines": { "engines": {
"node": ">=19" "node": ">=18"
} }
}, },
"node_modules/map-obj": { "node_modules/map-obj": {
@ -629,6 +538,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@ -650,40 +568,31 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-db": { "node_modules/mime-db": {
"version": "1.53.0", "version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mime-types": { "node_modules/mime-types": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "^1.53.0" "mime-db": "^1.54.0"
}, },
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": { "node_modules/negotiator": {
@ -705,9 +614,9 @@
} }
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.2", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -769,12 +678,12 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.6" "side-channel": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -819,34 +728,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/router": { "node_modules/router": {
"version": "2.0.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"array-flatten": "3.0.0", "debug": "^4.4.0",
"is-promise": "4.0.0", "depd": "^2.0.0",
"methods": "~1.1.2", "is-promise": "^4.0.0",
"parseurl": "~1.3.3", "parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0", "path-to-regexp": "^8.0.0"
"setprototypeof": "1.2.0",
"utils-merge": "1.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 18"
} }
}, },
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
@ -876,19 +771,18 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/send": { "node_modules/send": {
"version": "1.1.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.5", "debug": "^4.3.5",
"destroy": "^1.2.0",
"encodeurl": "^2.0.0", "encodeurl": "^2.0.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"etag": "^1.8.1", "etag": "^1.8.1",
"fresh": "^0.5.2", "fresh": "^2.0.0",
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"mime-types": "^2.1.35", "mime-types": "^3.0.1",
"ms": "^2.1.3", "ms": "^2.1.3",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"range-parser": "^1.2.1", "range-parser": "^1.2.1",
@ -898,74 +792,21 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/send/node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/send/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/send/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "2.1.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"encodeurl": "^2.0.0", "encodeurl": "^2.0.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"parseurl": "^1.3.3", "parseurl": "^1.3.3",
"send": "^1.0.0" "send": "^1.2.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -973,15 +814,69 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.0.6", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4", "object-inspect": "^1.13.3",
"object-inspect": "^1.13.1" "side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -1009,9 +904,9 @@
} }
}, },
"node_modules/type-fest": { "node_modules/type-fest": {
"version": "4.26.1", "version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)", "license": "(MIT OR CC0-1.0)",
"engines": { "engines": {
"node": ">=16" "node": ">=16"
@ -1021,9 +916,9 @@
} }
}, },
"node_modules/type-is": { "node_modules/type-is": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"content-type": "^1.0.5", "content-type": "^1.0.5",
@ -1043,15 +938,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -1,16 +1,16 @@
{ {
"name": "basic-node", "name": "openvidu-node",
"version": "1.0.0", "version": "1.0.0",
"description": "Basic server application built for Node.js with Express", "description": "OpenVidu server application built for Node.js with Express",
"main": "index.js", "main": "src/index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node index.js" "start": "node src/index.js"
}, },
"dependencies": { "dependencies": {
"cors": "2.8.5", "cors": "2.8.5",
"dotenv": "16.4.5", "dotenv": "16.5.0",
"express": "5.0.1", "express": "5.1.0",
"livekit-server-sdk": "^2.7.2" "livekit-server-sdk": "2.13.0"
} }
} }

View File

@ -0,0 +1,6 @@
export const SERVER_PORT = process.env.SERVER_PORT || 6080;
// LiveKit configuration
export const LIVEKIT_URL = process.env.LIVEKIT_URL || "http://localhost:7880";
export const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY || "devkey";
export const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET || "secret";

View File

@ -0,0 +1,247 @@
import e, { Router } from "express";
import {
DirectFileOutput,
EgressClient,
EncodedFileOutput,
EncodedFileType,
StreamOutput,
StreamProtocol
} from "livekit-server-sdk";
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } from "../config.js";
const egressClient = new EgressClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
export const egressController = Router();
// Create a new RoomComposite egress
egressController.post("/room-composite", async (req, res) => {
const { roomName } = req.body;
if (!roomName) {
res.status(400).json({ errorMessage: "'roomName' is required" });
return;
}
try {
const outputs = {
file: new EncodedFileOutput({
fileType: EncodedFileType.MP4,
filepath: "{room_name}-{room_id}-{time}"
})
};
const options = {
layout: "grid"
};
const egress = await egressClient.startRoomCompositeEgress(roomName, outputs, options);
res.status(201).json({ egress });
} catch (error) {
const errorMessage = "Error creating RoomComposite egress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Create a new RoomComposite egress to stream to a URL
egressController.post("/stream", async (req, res) => {
const { roomName, streamUrl } = req.body;
if (!roomName || !streamUrl) {
res.status(400).json({ errorMessage: "'roomName' and 'streamUrl' are required" });
return;
}
try {
const outputs = {
stream: new StreamOutput({
protocol: StreamProtocol.RTMP,
urls: [streamUrl]
})
};
const egress = await egressClient.startRoomCompositeEgress(roomName, outputs);
res.status(201).json({ egress });
} catch (error) {
const errorMessage = "Error creating RoomComposite egress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Create a new Participant egress
egressController.post("/participant", async (req, res) => {
const { roomName, participantIdentity } = req.body;
if (!roomName || !participantIdentity) {
res.status(400).json({ errorMessage: "'roomName' and 'participantIdentity' are required" });
return;
}
try {
const outputs = {
file: new EncodedFileOutput({
fileType: EncodedFileType.MP4,
filepath: "{room_name}-{room_id}-{publisher_identity}-{time}"
})
};
const options = {
screenShare: false
};
const egress = await egressClient.startParticipantEgress(roomName, participantIdentity, outputs, options);
res.status(201).json({ egress });
} catch (error) {
const errorMessage = "Error creating Participant egress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Create a new TrackComposite egress
egressController.post("/track-composite", async (req, res) => {
const { roomName, videoTrackId, audioTrackId } = req.body;
if (!roomName || !videoTrackId || !audioTrackId) {
res.status(400).json({ errorMessage: "'roomName', 'videoTrackId' and 'audioTrackId' are required" });
return;
}
try {
const outputs = {
file: new EncodedFileOutput({
fileType: EncodedFileType.MP4,
filepath: "{room_name}-{room_id}-{publisher_identity}-{time}"
})
};
const options = {
videoTrackId,
audioTrackId
};
const egress = await egressClient.startTrackCompositeEgress(roomName, outputs, options);
res.status(201).json({ egress });
} catch (error) {
const errorMessage = "Error creating TrackComposite egress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Create a new Track egress
egressController.post("/track", async (req, res) => {
const { roomName, trackId } = req.body;
if (!roomName || !trackId) {
res.status(400).json({ errorMessage: "'roomName' and 'trackId' are required" });
return;
}
try {
const output = new DirectFileOutput({
filepath: "{room_name}-{room_id}-{publisher_identity}-{track_source}-{track_id}-{time}"
});
const egress = await egressClient.startTrackEgress(roomName, output, trackId);
res.status(201).json({ egress });
} catch (error) {
const errorMessage = "Error creating Track egress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Create a new Web egress
egressController.post("/web", async (req, res) => {
const { url } = req.body;
if (!url) {
res.status(400).json({ errorMessage: "'url' is required" });
return;
}
try {
const outputs = {
file: new EncodedFileOutput({
fileType: EncodedFileType.MP4,
filepath: "{time}"
})
};
const egress = await egressClient.startWebEgress(url, outputs);
res.status(201).json({ egress });
} catch (error) {
const errorMessage = "Error creating Web egress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// List egresses
// If an egress ID is provided, only that egress is listed
// If a room name is provided, only egresses for that room are listed
// If active is true, only active egresses are listed
egressController.get("/", async (req, res) => {
const { egressId, roomName, active } = req.query;
try {
const options = {
egressId: egressId ? String(egressId) : undefined,
roomName: roomName ? String(roomName) : undefined,
active: active ? active === "true" : undefined
};
const egresses = await egressClient.listEgress(options);
res.json({ egresses });
} catch (error) {
const errorMessage = "Error listing egresses";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Update egress layout
egressController.post("/:egressId/layout", async (req, res) => {
const { egressId } = req.params;
const { layout } = req.body;
if (!layout) {
res.status(400).json({ errorMessage: "'layout' is required" });
return;
}
try {
const egress = await egressClient.updateLayout(egressId, layout);
res.json({ egress });
} catch (error) {
const errorMessage = "Error updating egress layout";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Add/remove stream URLs to an egress
egressController.post("/:egressId/streams", async (req, res) => {
const { egressId } = req.params;
const { streamUrlsToAdd, streamUrlsToRemove } = req.body;
if (!Array.isArray(streamUrlsToAdd) || !Array.isArray(streamUrlsToRemove)) {
res.status(400).json({ errorMessage: "'streamUrlsToAdd' and 'streamUrlsToRemove' are required and must be arrays" });
return;
}
try {
const egress = await egressClient.updateStream(egressId, streamUrlsToAdd, streamUrlsToRemove);
res.json({ egress });
} catch (error) {
const errorMessage = "Error updating egress streams";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Stop an egress
egressController.delete("/:egressId", async (req, res) => {
const { egressId } = req.params;
try {
await egressClient.stopEgress(egressId);
res.json({ message: "Egress stopped" });
} catch (error) {
const errorMessage = "Error stopping egress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});

View File

@ -0,0 +1,138 @@
import { Router } from "express";
import { IngressClient, IngressInput } from "livekit-server-sdk";
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } from "../config.js";
const ingressClient = new IngressClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
export const ingressController = Router();
// Create a new RTMP ingress
ingressController.post("/rtmp", async (req, res) => {
const { roomName, participantIdentity } = req.body;
if (!roomName || !participantIdentity) {
res.status(400).json({ errorMessage: "'roomName' and 'participantIdentity' are required" });
return;
}
try {
const ingressOptions = {
name: "rtmp-ingress",
roomName,
participantIdentity
};
const ingress = await ingressClient.createIngress(IngressInput.RTMP_INPUT, ingressOptions);
res.status(201).json({ ingress });
} catch (error) {
const errorMessage = "Error creating RTMP ingress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Create a new WHIP ingress
ingressController.post("/whip", async (req, res) => {
const { roomName, participantIdentity } = req.body;
if (!roomName || !participantIdentity) {
res.status(400).json({ errorMessage: "'roomName' and 'participantIdentity' are required" });
return;
}
try {
const ingressOptions = {
name: "whip-ingress",
roomName,
participantIdentity
};
const ingress = await ingressClient.createIngress(IngressInput.WHIP_INPUT, ingressOptions);
res.status(201).json({ ingress });
} catch (error) {
const errorMessage = "Error creating WHIP ingress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Create a new URL ingress
ingressController.post("/url", async (req, res) => {
const { roomName, participantIdentity, url } = req.body;
if (!roomName || !participantIdentity || !url) {
res.status(400).json({ errorMessage: "'roomName', 'participantIdentity' and 'url' are required" });
return;
}
try {
const ingressOptions = {
name: "url-ingress",
roomName,
participantIdentity,
url
};
const ingress = await ingressClient.createIngress(IngressInput.URL_INPUT, ingressOptions);
res.status(201).json({ ingress });
} catch (error) {
const errorMessage = "Error creating URL ingress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// List ingresses
// If an ingress ID is provided, only that ingress is listed
// If a room name is provided, only ingresses for that room are listed
ingressController.get("/", async (req, res) => {
const { ingressId, roomName } = req.query;
try {
const options = {
ingressId: ingressId ? String(ingressId) : undefined,
roomName: roomName ? String(roomName) : undefined
};
const ingresses = await ingressClient.listIngress(options);
res.json({ ingresses });
} catch (error) {
const errorMessage = "Error listing ingresses";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Update ingress
ingressController.patch("/:ingressId", async (req, res) => {
const { ingressId } = req.params;
const { roomName } = req.body;
if (!roomName) {
res.status(400).json({ errorMessage: "'roomName' is required" });
return;
}
try {
const options = {
name: "updated-ingress",
roomName
};
const ingress = await ingressClient.updateIngress(ingressId, options);
res.json({ ingress });
} catch (error) {
const errorMessage = "Error updating ingress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Delete ingress
ingressController.delete("/:ingressId", async (req, res) => {
const { ingressId } = req.params;
try {
await ingressClient.deleteIngress(ingressId);
res.json({ message: "Ingress deleted" });
} catch (error) {
const errorMessage = "Error deleting ingress";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});

View File

@ -0,0 +1,230 @@
import { Router } from "express";
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } from "../config.js";
import { DataPacket_Kind, RoomServiceClient } from "livekit-server-sdk";
const roomClient = new RoomServiceClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
export const roomController = Router();
// Create a new room
roomController.post("/", async (req, res) => {
const { roomName } = req.body;
if (!roomName) {
res.status(400).json({ errorMessage: "'roomName' is required" });
return;
}
try {
const roomOptions = {
name: roomName
};
const room = await roomClient.createRoom(roomOptions);
res.status(201).json({ room });
} catch (error) {
const errorMessage = "Error creating room";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// List rooms. If a room name is provided, only that room is listed
roomController.get("/", async (req, res) => {
const { roomName } = req.query;
try {
const roomNames = roomName ? [String(roomName)] : [];
const rooms = await roomClient.listRooms(roomNames);
res.json({ rooms });
} catch (error) {
const errorMessage = "Error listing rooms";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Update room metadata
roomController.post("/:roomName/metadata", async (req, res) => {
const { roomName } = req.params;
const { metadata } = req.body;
if (!metadata) {
res.status(400).json({ errorMessage: "'metadata' is required" });
return;
}
try {
const room = await roomClient.updateRoomMetadata(roomName, metadata);
res.json({ room });
} catch (error) {
const errorMessage = "Error updating room metadata";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Send data message to participants in a room
roomController.post("/:roomName/send-data", async (req, res) => {
const { roomName } = req.params;
const { data: rawData } = req.body;
if (!rawData) {
res.status(400).json({ errorMessage: "'data' is required" });
return;
}
try {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(rawData));
const options = {
topic: "chat",
destinationSids: [] // Send to all participants
};
await roomClient.sendData(roomName, data, DataPacket_Kind.RELIABLE, options);
res.json({ message: "Data message sent" });
} catch (error) {
const errorMessage = "Error sending data message";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Delete a room
roomController.delete("/:roomName", async (req, res) => {
const { roomName } = req.params;
try {
await roomClient.deleteRoom(roomName);
res.json({ message: "Room deleted" });
} catch (error) {
const errorMessage = "Error deleting room";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// List participants in a room
roomController.get("/:roomName/participants", async (req, res) => {
const { roomName } = req.params;
try {
const participants = await roomClient.listParticipants(roomName);
res.json({ participants });
} catch (error) {
const errorMessage = "Error listing participants";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Get a participant in a room
roomController.get("/:roomName/participants/:participantIdentity", async (req, res) => {
const { roomName, participantIdentity } = req.params;
try {
const participant = await roomClient.getParticipant(roomName, participantIdentity);
res.json({ participant });
} catch (error) {
const errorMessage = "Error getting participant";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Update a participant in a room
roomController.patch("/:roomName/participants/:participantIdentity", async (req, res) => {
const { roomName, participantIdentity } = req.params;
const { metadata } = req.body;
try {
const updateParticipantOptions = {
metadata,
permission: {
canPublish: false,
canSubscribe: true
}
};
const participant = await roomClient.updateParticipant(roomName, participantIdentity, updateParticipantOptions);
res.json({ participant });
} catch (error) {
const errorMessage = "Error updating participant";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Remove a participant from a room
roomController.delete("/:roomName/participants/:participantIdentity", async (req, res) => {
const { roomName, participantIdentity } = req.params;
try {
await roomClient.removeParticipant(roomName, participantIdentity);
res.json({ message: "Participant removed" });
} catch (error) {
const errorMessage = "Error removing participant";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Mute published track of a participant in a room
roomController.post("/:roomName/participants/:participantIdentity/mute", async (req, res) => {
const { roomName, participantIdentity } = req.params;
const { trackId } = req.body;
if (!trackId) {
res.status(400).json({ errorMessage: "'trackId' is required" });
return;
}
try {
const track = await roomClient.mutePublishedTrack(roomName, participantIdentity, trackId, true);
res.json({ track });
} catch (error) {
const errorMessage = "Error muting track";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Subscribe participant to tracks in a room
roomController.post("/:roomName/participants/:participantIdentity/subscribe", async (req, res) => {
const { roomName, participantIdentity } = req.params;
const { trackIds } = req.body;
if (!Array.isArray(trackIds)) {
res.status(400).json({ errorMessage: "'trackIds' is required and must be an array" });
return;
}
try {
await roomClient.updateSubscriptions(roomName, participantIdentity, trackIds, true);
const message = "Participant subscribed to tracks";
res.json({ message });
} catch (error) {
const errorMessage = "Error subscribing participant to tracks";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});
// Unsubscribe participant from tracks in a room
roomController.post("/:roomName/participants/:participantIdentity/unsubscribe", async (req, res) => {
const { roomName, participantIdentity } = req.params;
const { trackIds } = req.body;
if (!Array.isArray(trackIds)) {
res.status(400).json({ errorMessage: "'trackIds' is required and must be an array" });
return;
}
try {
await roomClient.updateSubscriptions(roomName, participantIdentity, trackIds, false);
const message = "Participant unsubscribed from tracks";
res.json({ message });
} catch (error) {
const errorMessage = "Error unsubscribing participant from tracks";
console.error(errorMessage, error);
res.status(500).json({ errorMessage });
}
});

View File

@ -0,0 +1,29 @@
import { Router } from "express";
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } from "../config.js";
import { AccessToken } from "livekit-server-sdk";
export const tokenController = Router();
tokenController.post("/", async (req, res) => {
const { roomName, participantName } = req.body;
if (!roomName || !participantName) {
res.status(400).json({ errorMessage: "'roomName' and 'participantName' are required" });
return;
}
const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
identity: participantName
});
const grant = {
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true
};
at.addGrant(grant);
const token = await at.toJwt();
res.json({ token });
});

View File

@ -0,0 +1,19 @@
import express, { Router } from "express";
import { WebhookReceiver } from "livekit-server-sdk";
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } from "../config.js";
const webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
export const webhookController = Router();
webhookController.use(express.raw({ type: "application/webhook+json" }));
webhookController.post("/", async (req, res) => {
try {
const webhookEvent = await webhookReceiver.receive(req.body, req.get("Authorization"));
console.log(webhookEvent);
} catch (error) {
console.error("Error validating webhook event", error);
}
res.status(200).send();
});

View File

@ -0,0 +1,24 @@
import "dotenv/config.js";
import express from "express";
import cors from "cors";
import { SERVER_PORT } from "./config.js";
import { tokenController } from "./controllers/token.controller.js";
import { webhookController } from "./controllers/webhook.controller.js";
import { roomController } from "./controllers/room.controller.js";
import { egressController } from "./controllers/egress.controller.js";
import { ingressController } from "./controllers/ingress.controller.js";
const app = express();
app.use(cors());
app.use(express.json());
app.use("/token", tokenController);
app.use("/livekit/webhook", webhookController);
app.use("/rooms", roomController);
app.use("/egresses", egressController);
app.use("/ingresses", ingressController);
app.listen(SERVER_PORT, () => {
console.log("Server started on port:", SERVER_PORT);
});