diff --git a/examples/demo/api.ts b/examples/demo/api.ts new file mode 100644 index 0000000000..5228f8eee0 --- /dev/null +++ b/examples/demo/api.ts @@ -0,0 +1,52 @@ +import dotenv from 'dotenv'; +import express from 'express'; +import type { Express } from 'express'; +import { EgressClient, EncodedFileOutput, S3Upload } from 'livekit-server-sdk'; + +dotenv.config({ path: ".env.local" }); +const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY; +const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET; +const LIVEKIT_URL = process.env.LIVEKIT_URL; +const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY; +const S3_SECRET = process.env.S3_SECRET; +const S3_BUCKET = process.env.S3_BUCKET; +const S3_ENDPOINT = process.env.S3_ENDPOINT; + +const app = express(); +app.use(express.json()); + +app.post("/api/start-recording", async (req, res) => { + const { roomName } = req.body; + // Upload the recording to S3 compatible storage + const fileOutput = new EncodedFileOutput({ + filepath: "recording.mp4", + output: { + case: "s3", + value: new S3Upload({ + accessKey: S3_ACCESS_KEY, + secret: S3_SECRET, + bucket: S3_BUCKET, + forcePathStyle: true, + endpoint: S3_ENDPOINT + }), + } + }); + if(!LIVEKIT_URL) { + res.status(500).json({ error: "Server misconfigured" }); + return; + } + const egressClient = new EgressClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET); + const egressInfo = await egressClient.startRoomCompositeEgress(roomName, fileOutput); + const egressId = egressInfo.egressId; + res.json({ + egressId: egressId + }); +}); + +app.post("/api/stop-recording", async (req, res) => { + const { egressId } = req.body; + const egressClient = new EgressClient(LIVEKIT_URL!, LIVEKIT_API_KEY, LIVEKIT_API_SECRET); + await egressClient.stopEgress(egressId); +}); + +export const handler: Express = app; \ No newline at end of file diff --git a/examples/demo/demo.ts b/examples/demo/demo.ts index 9a3043fc51..09af8a97d5 100644 --- a/examples/demo/demo.ts +++ b/examples/demo/demo.ts @@ -57,6 +57,8 @@ let currentRoom: Room | undefined; let startTime: number; +let egressId: any; + const searchParams = new URLSearchParams(window.location.search); const storedUrl = searchParams.get('url') ?? 'ws://localhost:7880'; const storedToken = searchParams.get('token') ?? ''; @@ -598,6 +600,38 @@ const appActions = { }); } }, + + toggleRecording: async () => { + if (!currentRoom) + return; + if (!currentRoom.isRecording) { + const roomName = currentRoom.name; + const response = await fetch("/api/start-recording", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ roomName }) + }); + if (!response.ok) + throw new Error("Failed to start recording!"); + const data = await response.json(); + egressId = data.egressId; + updateButtonsForPublishState(); + } + else { + await fetch("/api/stop-recording", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ egressId }) + }); + + updateButtonsForPublishState(); + setButtonDisabled("start-recording-button", true); + } + } }; declare global { @@ -989,6 +1023,7 @@ function setButtonsForState(connected: boolean) { 'disconnect-room-button', 'flip-video-button', 'send-button', + 'start-recording-button' ]; if (currentRoom && currentRoom.options.e2ee) { connectedSet.push('toggle-e2ee-button', 'e2ee-ratchet-button'); @@ -1091,6 +1126,12 @@ function updateButtonsForPublishState() { `${currentRoom.isE2EEEnabled ? 'Disable' : 'Enable'} E2EE`, currentRoom.isE2EEEnabled, ); + + setButtonState( + 'start-recording-button', + `${currentRoom.isRecording ? 'Stop' : 'Start'} Recording`, + currentRoom.isRecording, + ); } async function acquireDeviceList() { diff --git a/examples/demo/index.html b/examples/demo/index.html index 0beb32c835..bfb4a66044 100644 --- a/examples/demo/index.html +++ b/examples/demo/index.html @@ -145,6 +145,14 @@