openvidu-recording-advanced-node#
This tutorial improves the basic recording tutorial by doing the following:
- Complete recording metadata: Listen to webhook events and save all necessary metadata in a separate file.
- Real time recording status notification: Implement a custom notification system to inform participants about the recording status by listening to webhook events and updating room metadata.
- Recording deletion notification: Implement a custom notification system that alerts all participants of a recording's deletion by sending data messages.
- Direct access to recording files: Add an additional method to allow access to recording files directly from the S3 bucket by creating a presigned URL.
Running this tutorial#
1. Run LiveKit Server and Egress#
You can run LiveKit and Egress locally or you can use their free tier of LiveKit Cloud, which already includes both services.
Alternatively, you can use OpenVidu, which is a fully compatible LiveKit distribution designed specifically for on-premises environments. It brings notable improvements in terms of performance, observability and development experience. For more information, visit What is OpenVidu?.
-
Download OpenVidu
-
Configure the local deployment
-
Run OpenVidu
To use a production-ready OpenVidu deployment, visit the official OpenVidu deployment guide.
Configure Webhooks
This tutorial have an endpoint to receive webhooks from LiveKit. For this reason, when using a production deployment you need to configure webhooks to point to your local application server in order to make it work. Check the Send Webhooks to a Local Application Server section for more information.
Follow the official instructions to run LiveKit and Egress locally.
Configure Webhooks
This tutorial have an endpoint to receive webhooks from LiveKit. For this reason, when using LiveKit locally you need to configure webhooks to point to your application server in order to make it work. Check the Webhooks section from the official documentation and follow the instructions to configure webhooks.
Use your account in LiveKit Cloud.
Configure Webhooks
This tutorial have an endpoint to receive webhooks from LiveKit. For this reason, when using LiveKit Cloud you need to configure webhooks to point to your local application server in order to make it work. Check the Webhooks section from the official documentation and follow the instructions to configure webhooks.
Expose your local application server
In order to receive webhooks from LiveKit Cloud on your local machine, you need to expose your local application server to the internet. Tools like Ngrok, LocalTunnel, LocalXpose and Zrok can help you achieve this.
These tools provide you with a public URL that forwards requests to your local application server. You can use this URL to receive webhooks from LiveKit Cloud, configuring it as indicated above.
2. Download the tutorial code#
3. Run the application#
To run this application, you need Node installed on your device.
- Navigate into the application directory
- Install dependencies
- Run the application
Once the server is up and running, you can test the application by visiting http://localhost:6080
. You should see a screen like this:
Accessing your application from other devices in your local network
One advantage of running OpenVidu locally is that you can test your application with other devices in your local network very easily without worrying about SSL certificates.
Access your application client through https://xxx-yyy-zzz-www.openvidu-local.dev:6443
, where xxx-yyy-zzz-www
part of the domain is your LAN private IP address with dashes (-) instead of dots (.). For more information, see section Accessing your app from other devices in your network.
Limitation: Playing recordings with the S3
strategy from other devices in your local network is not possible due to MinIO not being exposed. To play recordings from other devices, you need to change the environment variable RECORDING_PLAYBACK_STRATEGY
to PROXY
.
Enhancements#
Refactoring backend#
The backend has been refactored to prevent code duplication and improve readability. The main changes are:
-
Endpoints have been moved to the
controllers
folder, creating a controller for each set of related endpoints:RoomController
for the room creation endpoint.RecordingController
for the recording endpoints.WebhookController
for the webhook endpoint.
-
The
index.js
file now simply sets the route for each controller: -
The configuration of environment variables and constants has been moved to the
config.js
file:config.js export const SERVER_PORT = process.env.SERVER_PORT || 6080; export const APP_NAME = "openvidu-recording-advanced-node"; // 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"; // S3 configuration export const S3_ENDPOINT = process.env.S3_ENDPOINT || "http://localhost:9000"; export const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || "minioadmin"; export const S3_SECRET_KEY = process.env.S3_SECRET_KEY || "minioadmin"; export const AWS_REGION = process.env.AWS_REGION || "us-east-1"; export const S3_BUCKET = process.env.S3_BUCKET || "openvidu"; export const RECORDINGS_PATH = process.env.RECORDINGS_PATH ?? "recordings/"; export const RECORDINGS_METADATA_PATH = ".metadata/"; export const RECORDING_PLAYBACK_STRATEGY = process.env.RECORDING_PLAYBACK_STRATEGY || "S3"; // PROXY or S3 export const RECORDING_FILE_PORTION_SIZE = 5 * 1024 * 1024; // 5MB
-
Operations of the
EgressClient
and functions related to recording management have been moved to theRecordingService
class within theservices
folder.
After refactoring and implementing the improvements, the backend of the application has the following structure:
src
├── controllers
│ ├── recording.controller.js
│ ├── room.controller.js
│ └── webhook.controller.js
├── services
│ ├── recording.service.js
│ ├── room.service.js
│ └── s3.service.js
├── config.js
├── index.js
Where room.service.js
defines the RoomService
class, that contains the logic to manage rooms using the RoomServiceClient
.
Adding room metadata#
In order to store the recording status in the room metadata, we have to create the room explicitly the first time a user joins it, setting the metadata field with an object that contains the recording status. This object also contains the app name, which is used to identify webhook events related to the application. This is done in the POST /token
endpoint:
room.controller.js | |
---|---|
|
- Check if the room exists.
- Create the room if it doesn't exist.
After generating the access token with the required permissions, this endpoint does the following:
-
Checks if the room exists by calling the
exists
method of theRoomService
with theroomName
as a parameter. This method returns a boolean indicating whether the room obtained from thegetRoom
method is notnull
. This other method lists all active rooms that match theroomName
by calling thelistRooms
method of theRoomServiceClient
with an array containing theroomName
as a parameter, and returns the first element of the list if it exists:- List all active rooms that match the
roomName
by calling thelistRooms
method of theRoomServiceClient
with an array containing theroomName
as a parameter. - Return the first element of the list if it exists.
- List all active rooms that match the
-
Creates the room if it doesn't exist by calling the
createRoom
method of theRoomService
with theroomName
as a parameter. This method creates a room with theroomName
and sets the metadata field with an object that contains the app name (defined in theconfig.js
file) and the recording status initialized toSTOPPED
. To achieve this, the method calls thecreateRoom
method of theRoomServiceClient
with an object indicating the room name and metadata:- Set the app name.
- Set the recording status to
STOPPED
. - Create the room with the
roomOptions
object by calling thecreateRoom
method of theRoomServiceClient
.
Handling webhook events#
In previous tutorials, we listened to all webhook events and printed them in the console without doing anything else. In this tutorial, we have to first check if the webhook is related to the application and then act accordingly depending on the event type. This is done in the POST /livekit/webhook
endpoint:
webhook.controller.js | |
---|---|
|
- Check if the webhook is related to the application.
- Destructure the event type and egress info from the webhook event.
- If the event type is
egress_started
oregress_updated
, notify the recording status update. - If the event type is
egress_ended
, handle the egress ended.
After receiving the webhook event, this endpoint does the following:
-
Checks if the webhook is related to the application by calling the
checkWebhookRelatedToMe
function with the webhook event as a parameter. This function returns a boolean indicating whether the app name obtained from the metadata field of the room related to the webhook event is equal to the app name defined in theconfig.js
file:webhook.controller.js const checkWebhookRelatedToMe = async (webhookEvent) => { const { room, egressInfo, ingressInfo } = webhookEvent; // (1)! let roomInfo = room; // (2)! if (!room || !room.metadata) { const roomName = room?.name ?? egressInfo?.roomName ?? ingressInfo?.roomName; // (3)! roomInfo = await roomService.getRoom(roomName); // (4)! if (!roomInfo) { return false; } } const metadata = roomInfo.metadata ? JSON.parse(roomInfo.metadata) : null; // (5)! return metadata?.createdBy === APP_NAME; // (6)! };
- Destructure the room, egress info, and ingress info from the webhook event.
- Check if the room and metadata fields exist.
- If the room or metadata fields don't exist, get the room name from the room, egress info, or ingress info.
- Get the room info by calling the
getRoom
method of theRoomService
with theroomName
as a parameter. - Parse the metadata field of the room info.
- Return whether the app name is equal to the app name defined in the
config.js
file.
-
Destructures the event type and egress info from the webhook event.
-
If the event type is
egress_started
oregress_updated
, calls thenotifyRecordingStatusUpdate
function with the egress info as a parameter. This function notifies all participants in the room related to the egress info about the recording status update. See the Notifying recording status update section for more information. -
If the event type is
egress_ended
, calls thehandleEgressEnded
function with the egress info as a parameter. This function saves the recording metadata in a separate file (see the Saving recording metadata section) and notifies all participants in the room related to the egress info that the recording has been stopped:- Save the recording metadata.
- Notify all participants in the room that the recording has been stopped.
Notifying recording status update#
When the recording status changes, all participants in the room have to be notified. This is done by updating the metadata field of the room with the new recording status, which will trigger the RoomEvent.RoomMetadataChanged
event in the client side. This is implemented in the notifyRecordingStatusUpdate
function:
webhook.controller.js | |
---|---|
|
- Get the room name from the egress info.
- Get the recording status from the egress info status.
- Update the room metadata with the new recording status.
After getting the room name from the egress info, this function does the following:
-
Gets the recording status by calling the
getRecordingStatus
method of theRecordingService
with the egress info status as a parameter. This method returns the recording status based on the egress info status:We distinguish between the following recording statuses:
STARTING
: The recording is starting.STARTED
: The recording is active.STOPPING
: The recording is stopping.STOPPED
: The recording has stopped.FAILED
: The recording has failed.
-
Updates the room metadata with the new recording status by calling the
updateRoomMetadata
method of theRoomService
with theroomName
andrecordingStatus
as parameters. This method updates the metadata field of the room with an object that contains the app name and the new recording status by calling theupdateRoomMetadata
method of theRoomServiceClient
with theroomName
and a stringified object as parameters:- Update the recording status.
- Update the room metadata with the new metadata by calling the
updateRoomMetadata
method of theRoomServiceClient
with theroomName
and a stringified object as parameters.
Saving recording metadata#
When the recording ends, the metadata related to the recording has to be saved in a separate file. This is done in the saveRecordingMetadata
function:
- Convert the egress info to a recording info object.
- Get the metadata key from the recording info name.
- Upload the recording metadata to the S3 bucket.
This method does the following:
-
Converts the egress info to a recording info object by calling the
convertToRecordingInfo
method:recording.service.js convertToRecordingInfo(egressInfo) { const file = egressInfo.fileResults[0]; return { id: egressInfo.egressId, name: file.filename.split("/").pop(), roomName: egressInfo.roomName, roomId: egressInfo.roomId, startedAt: Number(egressInfo.startedAt) / 1_000_000, duration: Number(file.duration) / 1_000_000_000, size: Number(file.size) }; }
Getting recording metadata
In this tutorial, we can access detailed information about the recording directly from the metadata file stored in the S3 bucket, without needing to make additional requests. This is made possible by saving all the necessary data retrieved from the egress info object. Compared to the basic recording tutorial, we are now storing additional details such as the recording name, duration and size.
-
Gets the metadata key from the recordings path and the recordings metadata path, both defined in the
config.js
file, and the recording name replacing the.mp4
extension with.json
: -
Uploads the recording metadata to the S3 bucket by calling the
uploadObject
method of theS3Service
with thekey
andrecordingInfo
as parameters. This method uploads an object to the S3 bucket by sending aPutObjectCommand
with the key and the stringified object as parameters:
Notifying recording deletion#
When a recording is deleted, all participants in the room have to be notified. This is done by sending a data message to all participants in the room. To achieve this, the DELETE /recordings/:recordingName
endpoint has been modified as follows:
recording.controller.js | |
---|---|
|
- Get the room name from the recording metadata.
- Check if the room exists.
- Send a data message to the room indicating that the recording was deleted.
Before deleting the recording, we get the room name from the recording metadata. After deleting the recording, we check if the room exists and, if it does, send a data message to the room indicating that the recording was deleted. This is done by calling the sendDataToRoom
method of the RoomService
with the roomName
and an object containing the recordingName
as parameters:
room.service.js | |
---|---|
|
- Encodes the raw data.
- Sets the topic to
RECORDING_DELETED
. - Sets the destination SIDs to an empty array (all participants in the room).
- Sends the data message to the room by calling the
sendData
method of theRoomServiceClient
with theroomName
,data
,DataPacket_Kind.RELIABLE
andoptions
as parameters.
This method does the following:
- Encodes the raw data by calling the
encode
method of theTextEncoder
with the stringified raw data as a parameter. - Sets the topic of the data message to
RECORDING_DELETED
. - Sets the destination SIDs to an empty array, which means that the message will be sent to all participants in the room.
- Sends the data message to the room by calling the
sendData
method of theRoomServiceClient
with theroomName
,data
,DataPacket_Kind.RELIABLE
andoptions
as parameters. TheDataPacket_Kind.RELIABLE
parameter indicates that the message will be sent reliably.
Accessing recording files directly from the S3 bucket#
In this tutorial, we have added an additional method to allow access to recording files directly from the S3 bucket by creating a presigned URL. To accomplish this, we have created a new endpoint (GET /recordings/:recordingName/url
) to get the recording URL depending on the playback strategy defined in the environment variable RECORDING_PLAYBACK_STRATEGY
, whose value can be PROXY
or S3
:
recording.controller.js | |
---|---|
|
- Check if the recording exists.
- Return the
GET /recordings/:recordingName
endpoint URL if the playback strategy isPROXY
. - Create a presigned URL to access the recording directly from the S3 bucket if the playback strategy is
S3
.
This endpoint does the following:
- Extracts the
recordingName
parameter from the request. - Checks if the recording exists. If it does not exist, it returns a
404
error. - If the playback strategy is
PROXY
, it returns theGET /recordings/:recordingName
endpoint URL to get the recording file from the backend. -
If the playback strategy is
S3
, it creates a presigned URL to access the recording directly from the S3 bucket by calling thegetRecordingUrl
method of theRecordingService
with therecordingName
as a parameter. This method simply calls thegetObjectUrl
method of theS3Service
with the key of the recording as a parameter:This method creates a presigned URL to access the object in the S3 bucket by calling the
getSignedUrl
function from the @aws-sdk/s3-request-presigner package, indicating theS3Client
,GetObjectCommand
and the expiration time in seconds as parameters. In this case, the expiration time is set to 24 hours.Presigned URLs
Presigned URLs are URLs that provide access to an S3 object for a limited time. This is useful when you want to share an object with someone for a limited time without providing them with your AWS credentials.
Compared to the proxy strategy, accessing recording files directly from the S3 bucket via presigned URLs is more efficient, as it reduces server load. However, it presents a security risk, as the URL, once generated, can be used by anyone until it expires.
Handling new room events in the client side#
In the client side, we have to handle the new room events related to the recording status and the recording deletion. This is done by listening to the RoomEvent.RoomMetadataChanged
and RoomEvent.DataReceived
events in the joinRoom
method:
app.js | |
---|---|
|
When a new RoomEvent.RoomMetadataChanged
event is received, we parse the metadata to get the recording status and update the recording info accordingly. The updateRecordingInfo
function has been updated to handle the new recording statuses.
In addition to handling this event, we need to update the recording info in the UI the first time a user joins the room. Once the user has joined, we retrieve the current room metadata and update the UI accordingly. Recordings will be listed unless the recording status is STOPPED
or FAILED
, to prevent listing recordings twice:
app.js | |
---|---|
|
When a new RoomEvent.DataReceived
event is received, we check if the topic of the message is RECORDING_DELETED
. If it is, we decode the payload using a TextDecoder
and parse the message to get the recording name. Then, we remove the recording from the list by calling the deleteRecordingContainer
function.