From 685f4bed8b46f57c45c64709af9a2243029b0eaf Mon Sep 17 00:00:00 2001 From: juancarmore Date: Mon, 9 Sep 2024 18:45:44 +0200 Subject: [PATCH] openvidu-recording-improved: Refactor code to upload custom recording metadata to S3 when "egress_ended" webhook is received instead of using LiveKit Egress manifest autosave --- .../src/controllers/recording.controller.js | 37 ++++++------------- .../src/controllers/webhook.controller.js | 37 ++++++++++++++++++- .../src/services/s3.service.js | 18 ++++++--- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/advanced-features/openvidu-recording-improved-node/src/controllers/recording.controller.js b/advanced-features/openvidu-recording-improved-node/src/controllers/recording.controller.js index 7e5ae2a6..c0ba5e9d 100644 --- a/advanced-features/openvidu-recording-improved-node/src/controllers/recording.controller.js +++ b/advanced-features/openvidu-recording-improved-node/src/controllers/recording.controller.js @@ -27,7 +27,8 @@ recordingController.post("/start", async (req, res) => { // Use the EncodedFileOutput to save the recording to an MP4 file const fileOutput = new EncodedFileOutput({ fileType: EncodedFileType.MP4, - filepath: `${RECORDINGS_PATH}{room_name}-{room_id}-{time}` + filepath: `${RECORDINGS_PATH}{room_name}-{room_id}-{time}`, + disableManifest: true }); try { @@ -69,6 +70,7 @@ recordingController.post("/stop", async (req, res) => { const recording = { name: file.filename.split("/").pop(), startedAt: Number(egressInfo.startedAt) / 1_000_000, + duration: Number(file.duration) / 1_000_000_000, size: Number(file.size) }; res.json({ message: "Recording stopped", recording }); @@ -83,13 +85,14 @@ recordingController.get("/", async (req, res) => { const roomId = req.query.roomId?.toString(); try { - const keyStart = RECORDINGS_PATH + (roomName ? `${roomName}` + (roomId ? `-${roomId}` : "") : ""); - const keyEnd = ".mp4.json"; + const keyStart = + RECORDINGS_PATH + ".metadata/" + (roomName ? `${roomName}` + (roomId ? `-${roomId}` : "") : ""); + const keyEnd = ".json"; const regex = new RegExp(`^${keyStart}.*${keyEnd}$`); // List all Egress metadata files in the recordings path that match the regex - const payloadKeys = await s3Service.listObjects(RECORDINGS_PATH, regex); - const recordings = await Promise.all(payloadKeys.map((payloadKey) => getRecordingInfo(payloadKey))); + const metadataKeys = await s3Service.listObjects(RECORDINGS_PATH + ".metadata/", regex); + const recordings = await Promise.all(metadataKeys.map((metadataKey) => s3Service.getObjectAsJson(metadataKey))); res.json({ recordings }); } catch (error) { console.error("Error listing recordings.", error); @@ -126,8 +129,9 @@ recordingController.get("/:recordingName", async (req, res) => { recordingController.delete("/:recordingName", async (req, res) => { const { recordingName } = req.params; - const key = RECORDINGS_PATH + recordingName; - const exists = await s3Service.exists(key); + const recordingKey = RECORDINGS_PATH + recordingName; + const metadataKey = RECORDINGS_PATH + ".metadata/" + recordingName.replace(".mp4", ".json"); + const exists = await s3Service.exists(recordingKey); if (!exists) { res.status(404).json({ errorMessage: "Recording not found" }); @@ -136,7 +140,7 @@ recordingController.delete("/:recordingName", async (req, res) => { try { // Delete the recording file and metadata file from S3 - await Promise.all([s3Service.deleteObject(key), s3Service.deleteObject(`${key}.json`)]); + await Promise.all([s3Service.deleteObject(recordingKey), s3Service.deleteObject(metadataKey)]); res.json({ message: "Recording deleted" }); } catch (error) { console.error("Error deleting recording.", error); @@ -153,20 +157,3 @@ const getActiveEgressesByRoom = async (roomName) => { return []; } }; - -const getRecordingInfo = async (payloadKey) => { - // Get the Egress metadata file as JSON - const data = await s3Service.getObjectAsJson(payloadKey); - - // Get the recording file size - const recordingKey = payloadKey.replace(".json", ""); - const size = await s3Service.getObjectSize(recordingKey); - - const recordingName = recordingKey.split("/").pop(); - const recording = { - name: recordingName, - startedAt: Number(data.started_at) / 1000000, - size: size - }; - return recording; -}; diff --git a/advanced-features/openvidu-recording-improved-node/src/controllers/webhook.controller.js b/advanced-features/openvidu-recording-improved-node/src/controllers/webhook.controller.js index 74356b42..e36dd028 100644 --- a/advanced-features/openvidu-recording-improved-node/src/controllers/webhook.controller.js +++ b/advanced-features/openvidu-recording-improved-node/src/controllers/webhook.controller.js @@ -1,8 +1,10 @@ import express, { Router } from "express"; import { WebhookReceiver } from "livekit-server-sdk"; import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } from "../config.js"; +import { S3Service } from "../services/s3.service.js"; const webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET); +const s3Service = new S3Service(); export const webhookController = Router(); webhookController.use(express.raw({ type: "application/webhook+json" })); @@ -11,9 +13,40 @@ webhookController.post("/", async (req, res) => { try { const event = await webhookReceiver.receive(req.body, req.get("Authorization")); console.log(event); + + switch (event.event) { + case "egress_started": + case "egress_updated": + await handleEgressUpdated(event.egressInfo); + break; + case "egress_ended": + await handleEgressEnded(event.egressInfo); + break; + } } catch (error) { console.error("Error validating webhook event.", error); } - + res.status(200).send(); -}); \ No newline at end of file +}); + +const handleEgressUpdated = async (egressInfo) => { + +}; + +const handleEgressEnded = async (egressInfo) => { + const recordingInfo = convertToRecordingInfo(egressInfo); + const metadataName = recordingInfo.name.replace(".mp4", ".json"); + const key = `recordings/.metadata/${metadataName}`; + await s3Service.uploadObject(key, recordingInfo); +}; + +const convertToRecordingInfo = (egressInfo) => { + const file = egressInfo.fileResults[0]; + return { + name: file.filename.split("/").pop(), + startedAt: Number(egressInfo.startedAt) / 1_000_000, + duration: Number(file.duration) / 1_000_000_000, + size: Number(file.size) + }; +}; diff --git a/advanced-features/openvidu-recording-improved-node/src/services/s3.service.js b/advanced-features/openvidu-recording-improved-node/src/services/s3.service.js index e6291a04..3b147a80 100644 --- a/advanced-features/openvidu-recording-improved-node/src/services/s3.service.js +++ b/advanced-features/openvidu-recording-improved-node/src/services/s3.service.js @@ -3,7 +3,8 @@ import { GetObjectCommand, DeleteObjectCommand, ListObjectsV2Command, - HeadObjectCommand + HeadObjectCommand, + PutObjectCommand } from "@aws-sdk/client-s3"; import { AWS_REGION, S3_ACCESS_KEY, S3_BUCKET, S3_ENDPOINT, S3_SECRET_KEY } from "../config.js"; @@ -29,6 +30,16 @@ export class S3Service { return this; } + async uploadObject(key, body) { + const params = { + Bucket: S3_BUCKET, + Key: key, + Body: JSON.stringify(body) + }; + const command = new PutObjectCommand(params); + return this.run(command); + } + async exists(key) { try { await this.headObject(key); @@ -47,11 +58,6 @@ export class S3Service { return this.run(command); } - async getObjectSize(key) { - const { ContentLength } = await this.headObject(key); - return ContentLength; - } - async getObject(key) { const params = { Bucket: S3_BUCKET,