openvidu-recording: Extract S3 related operations to "s3.service.js"
This commit is contained in:
parent
3b0f6c87a4
commit
e505eb0dcc
@ -11,4 +11,4 @@ S3_ACCESS_KEY=minioadmin
|
|||||||
S3_SECRET_KEY=minioadmin
|
S3_SECRET_KEY=minioadmin
|
||||||
AWS_REGION=us-east-1
|
AWS_REGION=us-east-1
|
||||||
S3_BUCKET=openvidu
|
S3_BUCKET=openvidu
|
||||||
DEFAULT_RECORDINGS_PATH=recordings
|
RECORDINGS_PATH=recordings/
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
"name": "openvidu-recording",
|
"name": "openvidu-recording",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Simple video-call application with recording capabilities",
|
"description": "Simple video-call application with recording capabilities",
|
||||||
"main": "index.js",
|
"main": "src/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js"
|
"start": "node src/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.635.0",
|
"@aws-sdk/client-s3": "3.635.0",
|
||||||
|
|||||||
@ -4,13 +4,7 @@ import cors from "cors";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { AccessToken, EgressClient, EncodedFileOutput, EncodedFileType, WebhookReceiver } from "livekit-server-sdk";
|
import { AccessToken, EgressClient, EncodedFileOutput, EncodedFileType, WebhookReceiver } from "livekit-server-sdk";
|
||||||
import {
|
import { S3Service } from "./s3.service.js";
|
||||||
S3Client,
|
|
||||||
GetObjectCommand,
|
|
||||||
DeleteObjectCommand,
|
|
||||||
ListObjectsV2Command,
|
|
||||||
HeadObjectCommand
|
|
||||||
} from "@aws-sdk/client-s3";
|
|
||||||
|
|
||||||
const SERVER_PORT = process.env.SERVER_PORT || 6080;
|
const SERVER_PORT = process.env.SERVER_PORT || 6080;
|
||||||
|
|
||||||
@ -19,13 +13,7 @@ const LIVEKIT_URL = process.env.LIVEKIT_URL || "http://localhost:7880";
|
|||||||
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY || "devkey";
|
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY || "devkey";
|
||||||
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET || "secret";
|
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET || "secret";
|
||||||
|
|
||||||
// S3 configuration
|
const RECORDINGS_PATH = process.env.RECORDINGS_PATH ?? "recordings/";
|
||||||
const S3_ENDPOINT = process.env.S3_ENDPOINT || "http://localhost:9000";
|
|
||||||
const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || "minioadmin";
|
|
||||||
const S3_SECRET_KEY = process.env.S3_SECRET_KEY || "minioadmin";
|
|
||||||
const AWS_REGION = process.env.AWS_REGION || "us-east-1";
|
|
||||||
const S3_BUCKET = process.env.S3_BUCKET || "openvidu";
|
|
||||||
const DEFAULT_RECORDINGS_PATH = process.env.DEFAULT_RECORDINGS_PATH ?? "recordings";
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@ -36,7 +24,7 @@ app.use(express.raw({ type: "application/webhook+json" }));
|
|||||||
// Set the static files location
|
// Set the static files location
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
app.use(express.static(__dirname + "/public"));
|
app.use(express.static(path.join(__dirname, "../public")));
|
||||||
|
|
||||||
app.post("/token", async (req, res) => {
|
app.post("/token", async (req, res) => {
|
||||||
const roomName = req.body.roomName;
|
const roomName = req.body.roomName;
|
||||||
@ -68,15 +56,7 @@ app.post("/livekit/webhook", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const egressClient = new EgressClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
const egressClient = new EgressClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
||||||
const s3Client = new S3Client({
|
const s3Service = new S3Service();
|
||||||
endpoint: S3_ENDPOINT,
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: S3_ACCESS_KEY,
|
|
||||||
secretAccessKey: S3_SECRET_KEY
|
|
||||||
},
|
|
||||||
region: AWS_REGION,
|
|
||||||
forcePathStyle: true
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/recordings/start", async (req, res) => {
|
app.post("/recordings/start", async (req, res) => {
|
||||||
const { roomName } = req.body;
|
const { roomName } = req.body;
|
||||||
@ -95,7 +75,7 @@ app.post("/recordings/start", async (req, res) => {
|
|||||||
|
|
||||||
const fileOutput = new EncodedFileOutput({
|
const fileOutput = new EncodedFileOutput({
|
||||||
fileType: EncodedFileType.MP4,
|
fileType: EncodedFileType.MP4,
|
||||||
filepath: `${DEFAULT_RECORDINGS_PATH}/{room_name}-{room_id}-{time}`
|
filepath: `${RECORDINGS_PATH}{room_name}-{room_id}-{time}`
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -156,19 +136,11 @@ app.get("/recordings", async (req, res) => {
|
|||||||
const roomName = req.query.roomName?.toString();
|
const roomName = req.query.roomName?.toString();
|
||||||
const roomId = req.query.roomId?.toString();
|
const roomId = req.query.roomId?.toString();
|
||||||
|
|
||||||
const command = new ListObjectsV2Command({
|
|
||||||
Bucket: S3_BUCKET,
|
|
||||||
Prefix: DEFAULT_RECORDINGS_PATH + "/"
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { Contents: objects } = await s3Client.send(command);
|
const keyStart = RECORDINGS_PATH + (roomName ? `${roomName}` + (roomId ? `-${roomId}` : "") : "");
|
||||||
const keyStart = DEFAULT_RECORDINGS_PATH + "/" + (roomName ? `${roomName}` + (roomId ? `-${roomId}` : "") : "");
|
const keyEnd = ".mp4.json";
|
||||||
const payloadKeys =
|
const regex = new RegExp(`^${keyStart}.*${keyEnd}$`);
|
||||||
objects
|
const payloadKeys = await s3Service.listObjects(RECORDINGS_PATH, regex);
|
||||||
?.filter((object) => object.Key.startsWith(keyStart) && object.Key.endsWith(".mp4.json"))
|
|
||||||
.map((payload) => payload.Key) ?? [];
|
|
||||||
|
|
||||||
const recordings = await Promise.all(payloadKeys.map((payloadKey) => getRecordingInfo(payloadKey)));
|
const recordings = await Promise.all(payloadKeys.map((payloadKey) => getRecordingInfo(payloadKey)));
|
||||||
res.json({ recordings });
|
res.json({ recordings });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -178,20 +150,10 @@ app.get("/recordings", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getRecordingInfo = async (payloadKey) => {
|
const getRecordingInfo = async (payloadKey) => {
|
||||||
const objectCommand = new GetObjectCommand({
|
const data = await s3Service.getObjectAsJson(payloadKey);
|
||||||
Bucket: S3_BUCKET,
|
|
||||||
Key: payloadKey
|
|
||||||
});
|
|
||||||
const { Body } = await s3Client.send(objectCommand);
|
|
||||||
const stringifiedData = await Body.transformToString();
|
|
||||||
const data = JSON.parse(stringifiedData);
|
|
||||||
|
|
||||||
const recordingKey = payloadKey.replace(".json", "");
|
const recordingKey = payloadKey.replace(".json", "");
|
||||||
const headCommand = new HeadObjectCommand({
|
const size = await s3Service.getObjectSize(recordingKey);
|
||||||
Bucket: S3_BUCKET,
|
|
||||||
Key: recordingKey
|
|
||||||
});
|
|
||||||
const { ContentLength: size } = await s3Client.send(headCommand);
|
|
||||||
|
|
||||||
const recordingName = recordingKey.split("/").pop();
|
const recordingName = recordingKey.split("/").pop();
|
||||||
const recording = {
|
const recording = {
|
||||||
@ -204,25 +166,20 @@ const getRecordingInfo = async (payloadKey) => {
|
|||||||
|
|
||||||
app.get("/recordings/:recordingName", async (req, res) => {
|
app.get("/recordings/:recordingName", async (req, res) => {
|
||||||
const { recordingName } = req.params;
|
const { recordingName } = req.params;
|
||||||
|
const key = RECORDINGS_PATH + recordingName;
|
||||||
const exists = await checkRecordingExists(recordingName);
|
const exists = await s3Service.exists(key);
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
res.status(404).json({ errorMessage: "Recording not found" });
|
res.status(404).json({ errorMessage: "Recording not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new GetObjectCommand({
|
|
||||||
Bucket: S3_BUCKET,
|
|
||||||
Key: `${DEFAULT_RECORDINGS_PATH}/${recordingName}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { Body, ContentLength: fileSize } = await s3Client.send(command);
|
const { body, size } = await s3Service.getObject(key);
|
||||||
res.setHeader("Content-Type", "video/mp4");
|
res.setHeader("Content-Type", "video/mp4");
|
||||||
res.setHeader("Content-Length", fileSize);
|
res.setHeader("Content-Length", size);
|
||||||
res.setHeader("Accept-Ranges", "bytes");
|
res.setHeader("Accept-Ranges", "bytes");
|
||||||
Body.pipe(res).on("finish", () => res.end());
|
body.pipe(res).on("finish", () => res.end());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting recording.", error);
|
console.error("Error getting recording.", error);
|
||||||
res.status(500).json({ errorMessage: "Error getting recording" });
|
res.status(500).json({ errorMessage: "Error getting recording" });
|
||||||
@ -231,27 +188,16 @@ app.get("/recordings/:recordingName", async (req, res) => {
|
|||||||
|
|
||||||
app.delete("/recordings/:recordingName", async (req, res) => {
|
app.delete("/recordings/:recordingName", async (req, res) => {
|
||||||
const { recordingName } = req.params;
|
const { recordingName } = req.params;
|
||||||
|
const key = RECORDINGS_PATH + recordingName;
|
||||||
const exists = await checkRecordingExists(recordingName);
|
const exists = await s3Service.exists(key);
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
res.status(404).json({ errorMessage: "Recording not found" });
|
res.status(404).json({ errorMessage: "Recording not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRecordingCommand = new DeleteObjectCommand({
|
|
||||||
Bucket: S3_BUCKET,
|
|
||||||
Key: `${DEFAULT_RECORDINGS_PATH}/${recordingName}`
|
|
||||||
});
|
|
||||||
const deletePayloadCommand = new DeleteObjectCommand({
|
|
||||||
Bucket: S3_BUCKET,
|
|
||||||
Key: `${DEFAULT_RECORDINGS_PATH}/${recordingName}.json`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Deleting recording:", recordingName);
|
await Promise.all([s3Service.deleteObject(key), s3Service.deleteObject(`${key}.json`)]);
|
||||||
await Promise.all([s3Client.send(deleteRecordingCommand), s3Client.send(deletePayloadCommand)]);
|
|
||||||
console.log("Recording deleted:", recordingName);
|
|
||||||
res.json({ message: "Recording deleted" });
|
res.json({ message: "Recording deleted" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting recording.", error);
|
console.error("Error deleting recording.", error);
|
||||||
@ -259,20 +205,6 @@ app.delete("/recordings/:recordingName", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkRecordingExists = async (recordingName) => {
|
|
||||||
const headCommand = new HeadObjectCommand({
|
|
||||||
Bucket: S3_BUCKET,
|
|
||||||
Key: `${DEFAULT_RECORDINGS_PATH}/${recordingName}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await s3Client.send(headCommand);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
app.listen(SERVER_PORT, () => {
|
app.listen(SERVER_PORT, () => {
|
||||||
console.log("Server started on port:", SERVER_PORT);
|
console.log("Server started on port:", SERVER_PORT);
|
||||||
});
|
});
|
||||||
103
advanced-features/openvidu-recording-node/src/s3.service.js
Normal file
103
advanced-features/openvidu-recording-node/src/s3.service.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
S3Client,
|
||||||
|
GetObjectCommand,
|
||||||
|
DeleteObjectCommand,
|
||||||
|
ListObjectsV2Command,
|
||||||
|
HeadObjectCommand
|
||||||
|
} from "@aws-sdk/client-s3";
|
||||||
|
|
||||||
|
// S3 configuration
|
||||||
|
const S3_ENDPOINT = process.env.S3_ENDPOINT || "http://localhost:9000";
|
||||||
|
const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || "minioadmin";
|
||||||
|
const S3_SECRET_KEY = process.env.S3_SECRET_KEY || "minioadmin";
|
||||||
|
const AWS_REGION = process.env.AWS_REGION || "us-east-1";
|
||||||
|
const S3_BUCKET = process.env.S3_BUCKET || "openvidu";
|
||||||
|
|
||||||
|
export class S3Service {
|
||||||
|
static instance;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (S3Service.instance) {
|
||||||
|
return S3Service.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.s3Client = new S3Client({
|
||||||
|
endpoint: S3_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: S3_ACCESS_KEY,
|
||||||
|
secretAccessKey: S3_SECRET_KEY
|
||||||
|
},
|
||||||
|
region: AWS_REGION,
|
||||||
|
forcePathStyle: true
|
||||||
|
});
|
||||||
|
|
||||||
|
S3Service.instance = this;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(key) {
|
||||||
|
try {
|
||||||
|
await this.headObject(key);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async headObject(key) {
|
||||||
|
const params = {
|
||||||
|
Bucket: S3_BUCKET,
|
||||||
|
Key: key
|
||||||
|
};
|
||||||
|
const command = new HeadObjectCommand(params);
|
||||||
|
return this.run(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObjectSize(key) {
|
||||||
|
const { ContentLength } = await this.headObject(key);
|
||||||
|
return ContentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObject(key) {
|
||||||
|
const params = {
|
||||||
|
Bucket: S3_BUCKET,
|
||||||
|
Key: key
|
||||||
|
};
|
||||||
|
const command = new GetObjectCommand(params);
|
||||||
|
const { Body: body, ContentLength: size } = await this.run(command);
|
||||||
|
return { body, size };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObjectAsJson(key) {
|
||||||
|
const { body } = await this.getObject(key);
|
||||||
|
const stringifiedData = await body.transformToString();
|
||||||
|
return JSON.parse(stringifiedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listObjects(prefix, regex) {
|
||||||
|
const params = {
|
||||||
|
Bucket: S3_BUCKET,
|
||||||
|
Prefix: prefix
|
||||||
|
};
|
||||||
|
const command = new ListObjectsV2Command(params);
|
||||||
|
const { Contents: objects } = await this.run(command);
|
||||||
|
return (
|
||||||
|
objects
|
||||||
|
?.filter((object) => regex.test(object.Key))
|
||||||
|
.map((payload) => payload.Key) ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteObject(key) {
|
||||||
|
const params = {
|
||||||
|
Bucket: S3_BUCKET,
|
||||||
|
Key: key
|
||||||
|
};
|
||||||
|
const command = new DeleteObjectCommand(params);
|
||||||
|
return this.run(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(command) {
|
||||||
|
return this.s3Client.send(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user