Update openvidu-recording-basic-node
This commit is contained in:
parent
5e2a891250
commit
48e1fb79f7
File diff suppressed because it is too large
Load Diff
@ -8,10 +8,10 @@
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.654.0",
|
||||
"@aws-sdk/client-s3": "3.744.0",
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "16.4.5",
|
||||
"express": "4.21.0",
|
||||
"livekit-server-sdk": "^2.7.2"
|
||||
"dotenv": "16.4.7",
|
||||
"express": "5.0.1",
|
||||
"livekit-server-sdk": "^2.9.7"
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,24 +27,33 @@ async function joinRoom() {
|
||||
|
||||
// Specify the actions when events take place in the room
|
||||
// On every new Track received...
|
||||
room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, _publication, participant) => {
|
||||
room.on(
|
||||
LivekitClient.RoomEvent.TrackSubscribed,
|
||||
(track, _publication, participant) => {
|
||||
addTrack(track, participant.identity);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// On every new Track destroyed...
|
||||
room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track, _publication, participant) => {
|
||||
room.on(
|
||||
LivekitClient.RoomEvent.TrackUnsubscribed,
|
||||
(track, _publication, participant) => {
|
||||
track.detach();
|
||||
document.getElementById(track.sid)?.remove();
|
||||
|
||||
if (track.kind === "video") {
|
||||
removeVideoContainer(participant.identity);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// When recording status changes...
|
||||
room.on(LivekitClient.RoomEvent.RecordingStatusChanged, async (isRecording) => {
|
||||
room.on(
|
||||
LivekitClient.RoomEvent.RecordingStatusChanged,
|
||||
async (isRecording) => {
|
||||
await updateRecordingInfo(isRecording);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
// Get the room name and participant name from the form
|
||||
@ -64,7 +73,9 @@ async function joinRoom() {
|
||||
|
||||
// Publish your camera and microphone
|
||||
await room.localParticipant.enableCameraAndMicrophone();
|
||||
const localVideoTrack = this.room.localParticipant.videoTrackPublications.values().next().value.track;
|
||||
const localVideoTrack = this.room.localParticipant.videoTrackPublications
|
||||
.values()
|
||||
.next().value.track;
|
||||
addTrack(localVideoTrack, userName, true);
|
||||
|
||||
// Update recording info
|
||||
@ -84,7 +95,10 @@ function addTrack(track, participantIdentity, local = false) {
|
||||
if (track.kind === "video") {
|
||||
const videoContainer = createVideoContainer(participantIdentity, local);
|
||||
videoContainer.append(element);
|
||||
appendParticipantData(videoContainer, participantIdentity + (local ? " (You)" : ""));
|
||||
appendParticipantData(
|
||||
videoContainer,
|
||||
participantIdentity + (local ? " (You)" : "")
|
||||
);
|
||||
} else {
|
||||
document.getElementById("layout-container").append(element);
|
||||
}
|
||||
@ -129,7 +143,9 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
}
|
||||
|
||||
// Remove recording video when the dialog is closed
|
||||
document.getElementById("recording-video-dialog").addEventListener("close", () => {
|
||||
document
|
||||
.getElementById("recording-video-dialog")
|
||||
.addEventListener("close", () => {
|
||||
const recordingVideo = document.getElementById("recording-video");
|
||||
recordingVideo.src = "";
|
||||
});
|
||||
@ -137,7 +153,8 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
|
||||
function generateFormValues() {
|
||||
document.getElementById("room-name").value = "Test Room";
|
||||
document.getElementById("participant-name").value = "Participant" + Math.floor(Math.random() * 100);
|
||||
document.getElementById("participant-name").value =
|
||||
"Participant" + Math.floor(Math.random() * 100);
|
||||
}
|
||||
|
||||
function createVideoContainer(participantIdentity, local = false) {
|
||||
@ -163,7 +180,9 @@ function appendParticipantData(videoContainer, participantIdentity) {
|
||||
}
|
||||
|
||||
function removeVideoContainer(participantIdentity) {
|
||||
const videoContainer = document.getElementById(`camera-${participantIdentity}`);
|
||||
const videoContainer = document.getElementById(
|
||||
`camera-${participantIdentity}`
|
||||
);
|
||||
videoContainer?.remove();
|
||||
}
|
||||
|
||||
@ -190,7 +209,7 @@ function removeAllLayoutElements() {
|
||||
async function getToken(roomName, participantName) {
|
||||
const [error, body] = await httpRequest("POST", "/token", {
|
||||
roomName,
|
||||
participantName
|
||||
participantName,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
@ -217,7 +236,7 @@ async function updateRecordingInfo(isRecording) {
|
||||
}
|
||||
|
||||
const roomId = await room.getSid();
|
||||
await listRecordings(room.name, roomId);
|
||||
await listRecordings(roomId);
|
||||
}
|
||||
|
||||
async function manageRecording() {
|
||||
@ -248,27 +267,33 @@ async function manageRecording() {
|
||||
|
||||
async function startRecording() {
|
||||
return httpRequest("POST", "/recordings/start", {
|
||||
roomName: room.name
|
||||
roomName: room.name,
|
||||
});
|
||||
}
|
||||
|
||||
async function stopRecording() {
|
||||
return httpRequest("POST", "/recordings/stop", {
|
||||
roomName: room.name
|
||||
roomName: room.name,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteRecording(recordingName) {
|
||||
const [error, _] = await httpRequest("DELETE", `/recordings/${recordingName}`);
|
||||
const [error, _] = await httpRequest(
|
||||
"DELETE",
|
||||
`/recordings/${recordingName}`
|
||||
);
|
||||
|
||||
if (!error || error.status === 404) {
|
||||
const roomId = await room?.getSid();
|
||||
await listRecordings(room?.name, roomId);
|
||||
await listRecordings(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
async function listRecordings(roomName, roomId) {
|
||||
const url = "/recordings" + (roomName ? `?roomName=${roomName}` + (roomId ? `&roomId=${roomId}` : "") : "");
|
||||
async function listRecordings(roomId) {
|
||||
let url = "/recordings";
|
||||
if (roomId) {
|
||||
url += `?roomId=${roomId}`;
|
||||
}
|
||||
const [error, body] = await httpRequest("GET", url);
|
||||
|
||||
if (!error) {
|
||||
@ -277,19 +302,14 @@ async function listRecordings(roomName, roomId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function listRecordingsByRoom() {
|
||||
const roomName = document.getElementById("room-name").value;
|
||||
await listRecordings(roomName);
|
||||
}
|
||||
|
||||
async function httpRequest(method, url, body) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: method !== "GET" ? JSON.stringify(body) : undefined
|
||||
body: method !== "GET" ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const responseBody = await response.json();
|
||||
@ -298,7 +318,7 @@ async function httpRequest(method, url, body) {
|
||||
console.error(responseBody.errorMessage);
|
||||
const error = {
|
||||
status: response.status,
|
||||
message: responseBody.errorMessage
|
||||
message: responseBody.errorMessage,
|
||||
};
|
||||
return [error, undefined];
|
||||
}
|
||||
@ -308,7 +328,7 @@ async function httpRequest(method, url, body) {
|
||||
console.error(error.message);
|
||||
const errorObj = {
|
||||
status: 0,
|
||||
message: error.message
|
||||
message: error.message,
|
||||
};
|
||||
return [errorObj, undefined];
|
||||
}
|
||||
@ -325,8 +345,6 @@ function showRecordingList(recordings) {
|
||||
|
||||
recordings.forEach((recording) => {
|
||||
const recordingName = recording.name;
|
||||
const recordingSize = formatBytes(recording.size ?? 0);
|
||||
const recordingDate = new Date(recording.startedAt).toLocaleString();
|
||||
|
||||
const recordingContainer = document.createElement("div");
|
||||
recordingContainer.className = "recording-container";
|
||||
@ -336,8 +354,6 @@ function showRecordingList(recordings) {
|
||||
<i class="fa-solid fa-file-video"></i>
|
||||
<div class="recording-info">
|
||||
<p class="recording-name">${recordingName}</p>
|
||||
<p class="recording-size">${recordingSize}</p>
|
||||
<p class="recording-date">${recordingDate}</p>
|
||||
</div>
|
||||
<div class="recording-actions">
|
||||
<button title="Play" class="icon-button" onclick="displayRecording('${recordingName}')">
|
||||
@ -354,14 +370,18 @@ function showRecordingList(recordings) {
|
||||
}
|
||||
|
||||
function displayRecording(recordingName) {
|
||||
const recordingVideoDialog = document.getElementById("recording-video-dialog");
|
||||
const recordingVideoDialog = document.getElementById(
|
||||
"recording-video-dialog"
|
||||
);
|
||||
recordingVideoDialog.showModal();
|
||||
const recordingVideo = document.getElementById("recording-video");
|
||||
recordingVideo.src = `/recordings/${recordingName}`;
|
||||
}
|
||||
|
||||
function closeRecording() {
|
||||
const recordingVideoDialog = document.getElementById("recording-video-dialog");
|
||||
const recordingVideoDialog = document.getElementById(
|
||||
"recording-video-dialog"
|
||||
);
|
||||
recordingVideoDialog.close();
|
||||
}
|
||||
|
||||
@ -371,16 +391,3 @@ function removeAllRecordings() {
|
||||
recording.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) {
|
||||
return "0Bytes";
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const decimals = i < 2 ? 0 : 1;
|
||||
|
||||
return (bytes / Math.pow(k, i)).toFixed(decimals) + sizes[i];
|
||||
}
|
||||
|
||||
@ -56,18 +56,6 @@
|
||||
|
||||
<main>
|
||||
<div id="recordings-all">
|
||||
<div id="actions">
|
||||
<button id="refresh-button" title="Refresh" class="icon-button" onclick="listRecordings()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<form onsubmit="listRecordingsByRoom(); return false">
|
||||
<input id="room-name" type="text" placeholder="Room name" />
|
||||
<button title="Search" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h2>Recordings</h2>
|
||||
<div id="recording-list"></div>
|
||||
<dialog id="recording-video-dialog">
|
||||
|
||||
@ -3,19 +3,35 @@ import express from "express";
|
||||
import cors from "cors";
|
||||
import path from "path";
|
||||
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 { S3Service } from "./s3.service.js";
|
||||
|
||||
// Configuration
|
||||
const SERVER_PORT = process.env.SERVER_PORT || 6080;
|
||||
|
||||
// LiveKit configuration
|
||||
const LIVEKIT_URL = process.env.LIVEKIT_URL || "http://localhost:7880";
|
||||
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
|
||||
|
||||
// Initialize services
|
||||
const egressClient = new EgressClient(
|
||||
LIVEKIT_URL,
|
||||
LIVEKIT_API_KEY,
|
||||
LIVEKIT_API_SECRET
|
||||
);
|
||||
const webhookReceiver = new WebhookReceiver(
|
||||
LIVEKIT_API_KEY,
|
||||
LIVEKIT_API_SECRET
|
||||
);
|
||||
const s3Service = new S3Service();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
@ -27,66 +43,73 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
app.use(express.static(path.join(__dirname, "../public")));
|
||||
|
||||
// Generate access tokens for participants to join a room
|
||||
app.post("/token", async (req, res) => {
|
||||
const roomName = req.body.roomName;
|
||||
const participantName = req.body.participantName;
|
||||
|
||||
if (!roomName || !participantName) {
|
||||
res.status(400).json({ errorMessage: "roomName and participantName are required" });
|
||||
return;
|
||||
return res
|
||||
.status(400)
|
||||
.json({ errorMessage: "roomName and participantName are required" });
|
||||
}
|
||||
|
||||
const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
|
||||
identity: participantName
|
||||
identity: participantName,
|
||||
});
|
||||
at.addGrant({ roomJoin: true, room: roomName, roomRecord: true });
|
||||
const token = await at.toJwt();
|
||||
res.json({ token });
|
||||
|
||||
return res.json({ token });
|
||||
});
|
||||
|
||||
const webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
||||
|
||||
// Receive webhooks from LiveKit Server
|
||||
app.post("/livekit/webhook", async (req, res) => {
|
||||
try {
|
||||
const event = await webhookReceiver.receive(req.body, req.get("Authorization"));
|
||||
const event = await webhookReceiver.receive(
|
||||
req.body,
|
||||
req.get("Authorization")
|
||||
);
|
||||
console.log(event);
|
||||
} catch (error) {
|
||||
console.error("Error validating webhook event.", error);
|
||||
}
|
||||
res.status(200).send();
|
||||
return res.status(200).send();
|
||||
});
|
||||
|
||||
const egressClient = new EgressClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
||||
const s3Service = new S3Service();
|
||||
|
||||
// Start a recording
|
||||
app.post("/recordings/start", async (req, res) => {
|
||||
const { roomName } = req.body;
|
||||
|
||||
if (!roomName) {
|
||||
res.status(400).json({ errorMessage: "roomName is required" });
|
||||
return;
|
||||
return res.status(400).json({ errorMessage: "roomName is required" });
|
||||
}
|
||||
|
||||
const activeRecording = await getActiveRecordingByRoom(roomName);
|
||||
|
||||
// Check if there is already an active recording for this room
|
||||
if (activeRecording) {
|
||||
res.status(409).json({ errorMessage: "Recording already started for this room" });
|
||||
return;
|
||||
return res
|
||||
.status(409)
|
||||
.json({ errorMessage: "Recording already started for this room" });
|
||||
}
|
||||
|
||||
// Use the EncodedFileOutput to save the recording to an MP4 file
|
||||
// The room name, time and room ID in the file path help to organize the recordings
|
||||
const fileOutput = new EncodedFileOutput({
|
||||
fileType: EncodedFileType.MP4,
|
||||
filepath: `${RECORDINGS_PATH}{room_name}-{room_id}-{time}`
|
||||
filepath: `${RECORDINGS_PATH}/{room_name}-{time}-{room_id}`,
|
||||
disableManifest: true,
|
||||
});
|
||||
|
||||
try {
|
||||
// Start a RoomCompositeEgress to record all participants in the room
|
||||
const egressInfo = await egressClient.startRoomCompositeEgress(roomName, { file: fileOutput });
|
||||
const egressInfo = await egressClient.startRoomCompositeEgress(roomName, {
|
||||
file: fileOutput,
|
||||
});
|
||||
const recording = {
|
||||
name: egressInfo.fileResults[0].filename.split("/").pop(),
|
||||
startedAt: Number(egressInfo.startedAt) / 1_000_000
|
||||
startedAt: Number(egressInfo.startedAt) / 1_000_000,
|
||||
};
|
||||
res.json({ message: "Recording started", recording });
|
||||
} catch (error) {
|
||||
@ -95,20 +118,21 @@ app.post("/recordings/start", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Stop a recording
|
||||
app.post("/recordings/stop", async (req, res) => {
|
||||
const { roomName } = req.body;
|
||||
|
||||
if (!roomName) {
|
||||
res.status(400).json({ errorMessage: "roomName is required" });
|
||||
return;
|
||||
return res.status(400).json({ errorMessage: "roomName is required" });
|
||||
}
|
||||
|
||||
const activeRecording = await getActiveRecordingByRoom(roomName);
|
||||
|
||||
// Check if there is an active recording for this room
|
||||
if (!activeRecording) {
|
||||
res.status(409).json({ errorMessage: "Recording not started for this room" });
|
||||
return;
|
||||
return res
|
||||
.status(409)
|
||||
.json({ errorMessage: "Recording not started for this room" });
|
||||
}
|
||||
|
||||
try {
|
||||
@ -117,79 +141,39 @@ app.post("/recordings/stop", async (req, res) => {
|
||||
const file = egressInfo.fileResults[0];
|
||||
const recording = {
|
||||
name: file.filename.split("/").pop(),
|
||||
startedAt: Number(egressInfo.startedAt) / 1_000_000,
|
||||
size: Number(file.size)
|
||||
};
|
||||
res.json({ message: "Recording stopped", recording });
|
||||
return res.json({ message: "Recording stopped", recording });
|
||||
} catch (error) {
|
||||
console.error("Error stopping recording.", error);
|
||||
res.status(500).json({ errorMessage: "Error stopping recording" });
|
||||
return res.status(500).json({ errorMessage: "Error stopping recording" });
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// List recordings
|
||||
app.get("/recordings", async (req, res) => {
|
||||
const roomName = req.query.roomName?.toString();
|
||||
const roomId = req.query.roomId?.toString();
|
||||
|
||||
try {
|
||||
const keyStart = RECORDINGS_PATH + (roomName ? `${roomName}-` + (roomId ? roomId : "") : "");
|
||||
const keyEnd = ".mp4.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 sortedRecordings = filterAndSortRecordings(recordings, roomName, roomId);
|
||||
res.json({ recordings: sortedRecordings });
|
||||
const awsResponse = await s3Service.listObjects(RECORDINGS_PATH);
|
||||
let recordings = [];
|
||||
if (awsResponse.Contents) {
|
||||
recordings = awsResponse.Contents.map((recording) => {
|
||||
return {
|
||||
name: recording.Key.split("/").pop(),
|
||||
};
|
||||
});
|
||||
}
|
||||
// Filter recordings by room ID
|
||||
recordings = recordings.filter((recording) =>
|
||||
roomId ? recording.name.includes(roomId) : true
|
||||
);
|
||||
return res.json({ recordings });
|
||||
} catch (error) {
|
||||
console.error("Error listing recordings.", error);
|
||||
res.status(500).json({ errorMessage: "Error listing recordings" });
|
||||
return res.status(500).json({ errorMessage: "Error listing recordings" });
|
||||
}
|
||||
});
|
||||
|
||||
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 = {
|
||||
id: data.egress_id,
|
||||
name: recordingName,
|
||||
roomName: data.room_name,
|
||||
roomId: data.room_id,
|
||||
startedAt: Number(data.started_at) / 1000000,
|
||||
size: size
|
||||
};
|
||||
return recording;
|
||||
};
|
||||
|
||||
const filterAndSortRecordings = (recordings, roomName, roomId) => {
|
||||
let filteredRecordings = recordings;
|
||||
|
||||
if (roomName || roomId) {
|
||||
filteredRecordings = recordings.filter((recording) => {
|
||||
return (!roomName || recording.roomName === roomName) && (!roomId || recording.roomId === roomId);
|
||||
});
|
||||
}
|
||||
|
||||
return filteredRecordings.sort((a, b) => b.startedAt - a.startedAt);
|
||||
};
|
||||
|
||||
// Play a recording
|
||||
app.get("/recordings/:recordingName", async (req, res) => {
|
||||
const { recordingName } = req.params;
|
||||
const { range } = req.headers;
|
||||
@ -197,13 +181,15 @@ app.get("/recordings/:recordingName", async (req, res) => {
|
||||
const exists = await s3Service.exists(key);
|
||||
|
||||
if (!exists) {
|
||||
res.status(404).json({ errorMessage: "Recording not found" });
|
||||
return;
|
||||
return res.status(404).json({ errorMessage: "Recording not found" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the recording file from S3
|
||||
const { stream, size, start, end } = await getRecordingStream(recordingName, range);
|
||||
const { stream, size, start, end } = await getRecordingStream(
|
||||
recordingName,
|
||||
range
|
||||
);
|
||||
|
||||
// Set response headers
|
||||
res.status(206);
|
||||
@ -217,10 +203,48 @@ app.get("/recordings/:recordingName", async (req, res) => {
|
||||
stream.pipe(res).on("finish", () => res.end());
|
||||
} catch (error) {
|
||||
console.error("Error getting recording.", error);
|
||||
res.status(500).json({ errorMessage: "Error getting recording" });
|
||||
return res.status(500).json({ errorMessage: "Error getting recording" });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a recording
|
||||
app.delete("/recordings/:recordingName", async (req, res) => {
|
||||
const { recordingName } = req.params;
|
||||
const key = RECORDINGS_PATH + recordingName;
|
||||
const exists = await s3Service.exists(key);
|
||||
|
||||
if (!exists) {
|
||||
return res.status(404).json({ errorMessage: "Recording not found" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete the recording file and metadata file from S3
|
||||
await Promise.all([s3Service.deleteObject(key)]);
|
||||
res.json({ message: "Recording deleted" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting recording.", error);
|
||||
res.status(500).json({ errorMessage: "Error deleting recording" });
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
app.listen(SERVER_PORT, () => {
|
||||
console.log("Server started on port:", SERVER_PORT);
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const getRecordingStream = async (recordingName, range) => {
|
||||
const key = RECORDINGS_PATH + recordingName;
|
||||
const size = await s3Service.getObjectSize(key);
|
||||
@ -228,33 +252,11 @@ const getRecordingStream = async (recordingName, range) => {
|
||||
// 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 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;
|
||||
const exists = await s3Service.exists(key);
|
||||
|
||||
if (!exists) {
|
||||
res.status(404).json({ errorMessage: "Recording not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete the recording file and metadata file from S3
|
||||
await Promise.all([s3Service.deleteObject(key), s3Service.deleteObject(`${key}.json`)]);
|
||||
res.json({ message: "Recording deleted" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting recording.", error);
|
||||
res.status(500).json({ errorMessage: "Error deleting recording" });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(SERVER_PORT, () => {
|
||||
console.log("Server started on port:", SERVER_PORT);
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
GetObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
ListObjectsV2Command,
|
||||
HeadObjectCommand
|
||||
HeadObjectCommand,
|
||||
} from "@aws-sdk/client-s3";
|
||||
|
||||
// S3 configuration
|
||||
@ -25,10 +25,10 @@ export class S3Service {
|
||||
endpoint: S3_ENDPOINT,
|
||||
credentials: {
|
||||
accessKeyId: S3_ACCESS_KEY,
|
||||
secretAccessKey: S3_SECRET_KEY
|
||||
secretAccessKey: S3_SECRET_KEY,
|
||||
},
|
||||
region: AWS_REGION,
|
||||
forcePathStyle: true
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
S3Service.instance = this;
|
||||
@ -47,7 +47,7 @@ export class S3Service {
|
||||
async headObject(key) {
|
||||
const params = {
|
||||
Bucket: S3_BUCKET,
|
||||
Key: key
|
||||
Key: key,
|
||||
};
|
||||
const command = new HeadObjectCommand(params);
|
||||
return this.run(command);
|
||||
@ -62,35 +62,26 @@ export class S3Service {
|
||||
const params = {
|
||||
Bucket: S3_BUCKET,
|
||||
Key: key,
|
||||
Range: range ? `bytes=${range.start}-${range.end}` : undefined
|
||||
Range: range ? `bytes=${range.start}-${range.end}` : undefined,
|
||||
};
|
||||
const command = new GetObjectCommand(params);
|
||||
const { Body: body } = await this.run(command);
|
||||
return body;
|
||||
}
|
||||
|
||||
async getObjectAsJson(key) {
|
||||
const body = await this.getObject(key);
|
||||
const stringifiedData = await body.transformToString();
|
||||
return JSON.parse(stringifiedData);
|
||||
}
|
||||
|
||||
async listObjects(prefix, regex) {
|
||||
async listObjects(prefix) {
|
||||
const params = {
|
||||
Bucket: S3_BUCKET,
|
||||
Prefix: prefix
|
||||
Prefix: prefix,
|
||||
};
|
||||
const command = new ListObjectsV2Command(params);
|
||||
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 await this.run(command);
|
||||
}
|
||||
|
||||
async deleteObject(key) {
|
||||
const params = {
|
||||
Bucket: S3_BUCKET,
|
||||
Key: key
|
||||
Key: key,
|
||||
};
|
||||
const command = new DeleteObjectCommand(params);
|
||||
return this.run(command);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user