From 4f75e00f9df7a89c17ac52a1927deaf85ac23352 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Mon, 23 Sep 2024 12:02:52 +0200 Subject: [PATCH] openvidu-recording-basic: Improve video seeking --- .../src/index.js | 27 ++++++++++++++++--- .../src/s3.service.js | 21 +++++++-------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/advanced-features/openvidu-recording-basic-node/src/index.js b/advanced-features/openvidu-recording-basic-node/src/index.js index eba87f9e..bdb53a2a 100644 --- a/advanced-features/openvidu-recording-basic-node/src/index.js +++ b/advanced-features/openvidu-recording-basic-node/src/index.js @@ -14,6 +14,7 @@ const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY || "devkey"; const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET || "secret"; const RECORDINGS_PATH = process.env.RECORDINGS_PATH ?? "recordings/"; +const RECORDING_FILE_PORTION_SIZE = 5 * 1024 * 1024; // 5MB const app = express(); @@ -175,6 +176,7 @@ const getRecordingInfo = async (payloadKey) => { app.get("/recordings/:recordingName", async (req, res) => { const { recordingName } = req.params; + const { range } = req.headers; const key = RECORDINGS_PATH + recordingName; const exists = await s3Service.exists(key); @@ -185,21 +187,38 @@ app.get("/recordings/:recordingName", async (req, res) => { try { // Get the recording file from S3 - const { body, size } = await s3Service.getObject(key); + const { stream, size, start, end } = await getRecordingStream(recordingName, range); - // Set the response headers + // Set response headers + res.status(206); + res.setHeader("Cache-Control", "no-cache"); res.setHeader("Content-Type", "video/mp4"); - res.setHeader("Content-Length", size); res.setHeader("Accept-Ranges", "bytes"); + res.setHeader("Content-Range", `bytes ${start}-${end}/${size}`); + res.setHeader("Content-Length", end - start + 1); // Pipe the recording file to the response - body.pipe(res).on("finish", () => res.end()); + stream.pipe(res).on("finish", () => res.end()); } catch (error) { console.error("Error getting recording.", error); res.status(500).json({ errorMessage: "Error getting recording" }); } }); +const getRecordingStream = async (recordingName, range) => { + const key = RECORDINGS_PATH + recordingName; + const size = await s3Service.getObjectSize(key); + + // Get the requested range + const parts = range?.replace(/bytes=/, "").split("-"); + const start = range ? parseInt(parts[0], 10) : 0; + const endRange = parts[1] ? parseInt(parts[1], 10) : start + RECORDING_FILE_PORTION_SIZE; + const end = Math.min(endRange, size - 1); + + const stream = await s3Service.getObject(key, { start, end }); + return { stream, size, start, end }; +}; + app.delete("/recordings/:recordingName", async (req, res) => { const { recordingName } = req.params; const key = RECORDINGS_PATH + recordingName; diff --git a/advanced-features/openvidu-recording-basic-node/src/s3.service.js b/advanced-features/openvidu-recording-basic-node/src/s3.service.js index f8424552..cb224f54 100644 --- a/advanced-features/openvidu-recording-basic-node/src/s3.service.js +++ b/advanced-features/openvidu-recording-basic-node/src/s3.service.js @@ -54,22 +54,23 @@ export class S3Service { } async getObjectSize(key) { - const { ContentLength } = await this.headObject(key); - return ContentLength; + const { ContentLength: size } = await this.headObject(key); + return size; } - async getObject(key) { + async getObject(key, range) { const params = { Bucket: S3_BUCKET, - Key: key + Key: key, + Range: range ? `bytes=${range.start}-${range.end}` : undefined }; const command = new GetObjectCommand(params); - const { Body: body, ContentLength: size } = await this.run(command); - return { body, size }; + const { Body: body } = await this.run(command); + return body; } async getObjectAsJson(key) { - const { body } = await this.getObject(key); + const body = await this.getObject(key); const stringifiedData = await body.transformToString(); return JSON.parse(stringifiedData); } @@ -83,11 +84,7 @@ export class S3Service { const { Contents: objects } = await this.run(command); // Filter objects by regex and return the keys - return ( - objects - ?.filter((object) => regex.test(object.Key)) - .map((payload) => payload.Key) ?? [] - ); + return objects?.filter((object) => regex.test(object.Key)).map((payload) => payload.Key) ?? []; } async deleteObject(key) {