openvidu-recording-basic-node#
This tutorial is a simple video-call application, built upon Node.js server and JavaScript client tutorials, and extends them by adding recording capabilities:
- Start and stop recording a room.
- List all recordings in a room.
- Play a recording.
- Delete a recording.
- List all available recordings and filter them by room name.
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.
Understanding the code#
This application consists of two essential backend files under the src
directory:
index.js
: This file holds the server application and defines the REST API endpoints.s3.service.js
: This file encapsulates the operations to interact with the S3 bucket.
And the following essential frontend files under the public
directory:
index.html
: This is the client application's main HTML file.app.js
: This is the main JavaScript file that interacts with the server application and handles the client application's logic and functionality.style.css
: This file contains the client application's styling.recordings.html
: This file defines the HTML for the general recording page.
Backend#
The server application extends the Node.js server tutorial by adding the following REST API endpoints:
POST /recordings/start
: Starts the recording of a room.POST /recordings/stop
: Stops the recording of a room.GET /recordings
: Lists all recordings stored in the S3 bucket. This endpoint also allows filtering recordings by room name or room ID.GET /recordings/:recordingName
: Retrieves a recording from the S3 bucket and returns it as a stream.DELETE /recordings/:recordingName
: This endpoint deletes a recording from the S3 bucket.
Before we dive into the code of each endpoint, let's first see the changes introduced in the index.js
file:
index.js | |
---|---|
|
- The URL of the LiveKit server.
- The path where recordings will be stored in the S3 bucket.
- The portion size of the recording that will be sent to the client in each request. This value is set to
5 MB
. - Set the
public
directory as the static files location.
There are three new environment variables:
LIVEKIT_URL
: The URL of the LiveKit server.RECORDINGS_PATH
: The path where recordings will be stored in the S3 bucket.RECORDING_FILE_PORTION_SIZE
: The portion size of the recording that will be sent to the client in each request.
Besides, the index.js
file configures the server to serve static files from the public
directory.
It also initializes the EgressClient
, which will help interacting with Egress API to manage recordings, and the S3Service
, which will help interacting with the S3 bucket:
The POST /token
endpoint has been modified to add the roomRecord
permission to the access token, so that participants can start recording a room:
index.js | |
---|---|
|
- Add the
roomRecord
permission to the access token.
Now let's explore the code for each recording feature:
Start recording#
The POST /recordings/start
endpoint starts the recording of a room. It receives the room name of the room to record as parameter and returns the recording metadata:
index.js | |
---|---|
|
- The
getActiveRecordingByRoom
function retrieves the active recording for a room. - If there is already an active recording for the room, the server returns a
409 Conflict
status code. - Use the
EncodedFileOutput
class to export the recording to an external file. - Define the file type as
MP4
. - Define the file path where the recording will be stored. The
{room_name}
,{room_id}
, and{time}
templates will be replaced by the actual room name, room ID and timestamp, respectively. Check out all available filename templates. - Start a
RoomCompositeEgress
to record all participants in the room by calling thestartRoomCompositeEgress
method of theEgressClient
with theroomName
andfileOutput
as parameters. - Extract the recording name from the
fileResults
array. - Return the recording metadata to the client.
This endpoint does the following:
- Obtains the
roomName
parameter from the request body. If it is not available, it returns a400
error. -
Check if there is already an active recording for the room. If there is, it returns a
409
error to prevent starting a new recording. To accomplish this, we use thegetActiveRecordingByRoom
function, which lists all active egresses for a specified room by calling thelistEgress
method of theEgressClient
with theroomName
andactive
parameters, and then returns the egress ID of the first active egress found:index.js const getActiveRecordingByRoom = async (roomName) => { try { // List all active egresses for the room const egresses = await egressClient.listEgress({ roomName, active: true }); return egresses.length > 0 ? egresses[0].egressId : null; } catch (error) { console.error("Error listing egresses.", error); return null; } };
-
Initializes an
EncodedFileOutput
object to export the recording to an external file. It sets the file type asMP4
and defines the file path where the recording will be stored. The{room_name}
,{room_id}
, and{time}
templates will be replaced by the actual room name, room ID, and timestamp, respectively. Check out all available filename templates.Saving recording metadata
The
EncodedFileOutput
class allows you to save the recording metadata to an external file. If you don't explicitly set thedisableManifest
property totrue
, the metadata will be saved in the same folder and with the same name as the recording file, but with a.json
extension. This metadata file will contain information such as the egress ID, the recording start time, and the name and ID of the room recorded.This information may be insufficient depending on your requirements (e.g., you can't get the recording duration). If this is the case, you can follow the steps described in the advanced recording tutorial, where we show how to save all necessary metadata in a separate file listening to webhook events.
-
Starts a
RoomCompositeEgress
to record all participants in the room by calling thestartRoomCompositeEgress
method of theEgressClient
withroomName
andfileOutput
as parameters. - Extracts the recording name from the
fileResults
array. - Returns the recording metadata to the client.
Stop recording#
The POST /recordings/stop
endpoint stops the recording of a room. It receives the room name of the room to stop recording as a parameter and returns the updated recording metadata:
index.js | |
---|---|
|
- The
getActiveRecordingByRoom
function retrieves the active recording for a room. - If there is no active recording for the room, the server returns a
409 Conflict
status code. - Stop the egress to finish the recording by calling the
stopEgress
method of theEgressClient
with the egress ID (activeRecording
) as a parameter. - Return the updated recording metadata to the client.
This endpoint does the following:
- Obtains the
roomName
parameter from the request body. If it is not available, it returns a400
error. - Retrieves all active egresses for the room. If there is no active egress for the room, it returns a
409
error to prevent stopping a non-existent recording. - Extracts the
egressId
from the active egress. - Stops the egress to finish the recording by calling the
stopEgress
method of theEgressClient
with the egress ID (activeRecording
) as a parameter. - Returns the updated recording metadata to the client.
List recordings#
The GET /recordings
endpoint lists all recordings stored in the S3 bucket. This endpoint also allows filtering recordings by room name or room ID:
index.js | |
---|---|
|
- Define the start of the key (path) depending on the
roomName
androomId
query parameters. - Create a regex pattern with the start and end of the key.
- List all Egress metadata files in the recordings path in the S3 bucket that match the regex.
- Retrieve the recording metadata for each recording that matches the regex.
- Filter the recordings by room name and room ID and sort them by start time.
- Return the list of recordings to the client.
This endpoint does the following:
- Extracts the
roomName
androomId
query parameters from the request. - Defines the start of the key (path) depending on the
roomName
androomId
query parameters. - Creates a regex pattern with the start and end of the key.
- Lists all Egress metadata files in the recordings path in the S3 bucket that match the regex. To accomplish this, we use the
listObjects
method of theS3Service
with theRECORDINGS_PATH
andregex
as parameters. -
Retrieves the recording metadata for each recording that matches the regex. To accomplish this, we use the
getRecordingInfo
function, which retrieves the egress metadata file as JSON and the recording file size by calling thegetObjectAsJson
andgetObjectSize
methods of theS3Service
, respectively. It then extracts the recording name from the recording key and returns the recording metadata:index.js const getRecordingInfo = async (payloadKey) => { // Get the egress metadata file as JSON const data = await s3Service.getObjectAsJson(payloadKey); // (1)! // Get the recording file size const recordingKey = payloadKey.replace(".json", ""); const size = await s3Service.getObjectSize(recordingKey); // (2)! const recordingName = recordingKey.split("/").pop(); const recording = { id: data.egress_id, name: recordingName, roomName: data.room_name, roomId: data.room_id, startedAt: Number(data.started_at) / 1000000, size: size }; return recording; };
- Get the egress metadata file as JSON.
- Get the recording file size.
-
Filter the recordings by room name and room ID and sort them by start time. To accomplish this, we use the
filterAndSortRecordings
function:index.js const filterAndSortRecordings = (recordings, roomName, roomId) => { let filteredRecordings = recordings; if (roomName || roomId) { filteredRecordings = recordings.filter((recording) => { return (!roomName || recording.roomName === roomName) && (!roomId || recording.roomId === roomId); // (1)! }); } return filteredRecordings.sort((a, b) => b.startedAt - a.startedAt); // (2)! };
- Filter the recordings by room name and room ID if they are provided.
- Sort the recordings by start time in descending order.
-
Returns the list of recordings to the client.
Get recording#
The GET /recordings/:recordingName
endpoint retrieves a specific portion of a recording from the S3 bucket and returns it as a stream. The server sends the recording file in portions of 5 MB
each time the client requests a range of the recording file. This is done to prevent loading the entire recording file into memory and to allow the client to play the recording while it is being downloaded and seek to a specific time:
index.js | |
---|---|
|
- Check if the recording exists in the S3 bucket.
- Get the recording file from the S3 bucket.
- Set the response status code to
206 Partial Content
. - Set the
Cache-Control
header asno-cache
. - Set the
Content-Type
header asvideo/mp4
. - Set the
Accept-Ranges
header asbytes
. - Set the
Content-Range
header with the start and end of the recording file and its size. - Set the
Content-Length
header as the size of the recording file portion. - Pipe the recording file to the response.
This endpoint does the following:
- Extracts the
recordingName
parameter from the request. - Checks if the recording exists in the S3 bucket by calling the
exists
method of theS3Service
with thekey
as a parameter. If the recording does not exist, it returns a404
error. -
Gets the requested range of the recording file by calling the
getRecordingStream
function:index.js const getRecordingStream = async (recordingName, range) => { const key = RECORDINGS_PATH + recordingName; const size = await s3Service.getObjectSize(key); // (1)! // Get the requested range const parts = range?.replace(/bytes=/, "").split("-"); const start = range ? parseInt(parts[0], 10) : 0; // (2)! const endRange = parts[1] ? parseInt(parts[1], 10) : start + RECORDING_FILE_PORTION_SIZE; // (3)! const end = Math.min(endRange, size - 1); // (4)! const stream = await s3Service.getObject(key, { start, end }); // (5)! return { stream, size, start, end }; };
- Get the size of the recording file.
- Get the start of the requested range.
- Get the end of the requested range or set it to the start plus the established portion size.
- Get the minimum between the end of the requested range and the size of the recording file minus one.
- Get the recording file from the S3 bucket with the requested range.
This function does the following:
- Gets the size of the recording file by calling the
getObjectSize
method of theS3Service
with thekey
as a parameter. - Extracts the start of the requested range from the
range
header. - Extracts the end of the requested range from the
range
header. If the end is not provided, it sets the end to the start plus the established portion size. - Gets the minimum between the end of the requested range and the size of the recording file minus one. This is done to prevent requesting a range that exceeds the recording file size.
- Gets the recording file from the S3 bucket with the requested range by calling the
getObject
method of theS3Service
with thekey
andrange
as parameters.
-
Sets the response headers:
Cache-Control
:no-cache
.Content-Type
:video/mp4
.Accept-Ranges
:bytes
.Content-Range
: The start and end of the recording file and its size.Content-Length
: The size of the recording file portion.
-
Pipes the recording file to the response.
Direct access to S3 bucket
With this approach, the backend acts as a proxy between the client and S3, which may result in increased server resource usage. To avoid this, it is more efficient to provide the client with a presigned URL, allowing direct access to the recording files from the S3 bucket. In the advanced recording tutorial, we show how to implement this method, along with a discussion of its advantages and disadvantages.
Delete recording#
The DELETE /recordings/:recordingName
endpoint deletes a recording from the S3 bucket:
index.js | |
---|---|
|
- Check if the recording exists in the S3 bucket.
- Delete the recording file and metadata file from the S3 bucket.
This endpoint does the following:
- Extracts the
recordingName
parameter from the request. - Checks if the recording exists in the S3 bucket by calling the
exists
method of theS3Service
with thekey
as a parameter. If the recording does not exist, it returns a404
error. - Deletes the recording file and metadata file from the S3 bucket by calling the
deleteObject
method of theS3Service
with thekey
and${key}.json
as a parameter, respectively.
S3 service#
Finally, let's take a look at the s3.service.js
file, which encapsulates the operations to interact with the S3 bucket:
s3.service.js | |
---|---|
|
- The URL of the S3 server.
- The access key of the S3 server.
- The secret key of the S3 server.
- The AWS region of the S3 server.
- The name of the S3 bucket.
- Initialize the
S3Client
with the provided configuration. - Check if an object exists in the S3 bucket.
- Retrieve the metadata of an object in the S3 bucket.
- Retrieve the size of an object in the S3 bucket.
- Retrieve a specified range of bytes from an object in the S3 bucket.
- Retrieve an object from the S3 bucket as JSON.
- List objects in the S3 bucket that match a regex pattern.
- Delete an object from the S3 bucket.
- Execute an S3 command.
This file loads environment variables for the S3 configuration:
S3_ENDPOINT
: The URL of the S3 server.S3_ACCESS_KEY
: The access key of the S3 server.S3_SECRET_KEY
: The secret key of the S3 server.AWS_REGION
: The AWS region of the S3 server.S3_BUCKET
: The name of the S3 bucket.
Then, it defines the S3Service
class as a singleton, which initializes the S3Client
with the provided configuration. The class encapsulates the following methods to interact with the S3 bucket:
exists
: Checks if an object exists in the S3 bucket.headObject
: Retrieves the metadata of an object in the S3 bucket.getObjectSize
: Retrieves the size of an object in the S3 bucket.getObject
: Retrieves an object from the S3 bucket.getObjectAsJson
: Retrieves an object from the S3 bucket as JSON.listObjects
: Lists objects in the S3 bucket that match a regex pattern.deleteObject
: Deletes an object from the S3 bucket.run
: Executes an S3 command.
Frontend#
The client application extends the JavaScript client tutorial by adding recording features, introducing new buttons to facilitate actions such as starting and stopping recording a room, as well as listing, playing and deleting recordings. When these newly introduced buttons are interacted with, the client triggers requests to the REST API endpoints of the server application.
In order to update the user interface of all participants in the room according to the recording status, the client application subscribes to the RoomEvent.RecordingStatusChanged
event, which is triggered when the room changes from being recorded to not being recorded, and vice versa. When this event is triggered, the updateRecordingInfo
function is called to update the recording information of the room displayed on the screen. This function is also called when a participant joins the room, using the current value of the room.recording
property at that moment. This is done in the joinRoom
function of the app.js
file:
Limitations of the RoomEvent.RecordingStatusChanged
event
By using the RoomEvent.RecordingStatusChanged
event, we can only detect when the recording has started or stopped, but not other states like starting
, stopping
or failed
. Additionally, when the recording stops, the event is not triggered until the recorder participant leaves the room, causing a delay of 20 seconds approximately between the stop and when participants are notified.
To overcome these limitations, you can follow the steps described in the advanced recording tutorial, where we implement a custom notification system. This system informs participants about the recording status by listening to webhook events and updating room metadata.
app.js | |
---|---|
|
The updateRecordingInfo
function updates the recording information of the room by changing the recording button's text and color according to the recording status. It also shows or hides the alert message that informs the user that the room is being recorded. Finally, it updates the recording list by calling the listRecordings
function.
This function retrieves all recordings available for the room from the backend and displays their relevant information by invoking the showRecordingList
function:
app.js | |
---|---|
|
The showRecordingList
function creates a new div
element for each recording available in the room and appends it to the recording-list
container. Each div
element contains the recording name, size, and start date, as well as buttons to play and delete the recording.
Recording deletion
When a recording is deleted, it is removed from the recording list, but only for the user who initiated the deletion. Other users will continue to see the recording in their list until it is refreshed.
In the advanced recording tutorial, we show how to implement a custom notification system that alerts all participants of a recording's deletion by sending data messages.
When the user clicks the play button, the displayRecording
function is called to play the recording. This function opens a dialog window with an embedded video element and sets the source of the video to the get recording endpoint of the server application:
General recording page#
The recordings.html
file defines the HTML for the general recording page. This page lists all available recordings from all rooms and allows the user to filter them by room name. It also provides buttons to play and delete each recording.