aethercall
Version:
A scalable WebRTC video calling API built with Node.js and OpenVidu
490 lines (443 loc) • 19.7 kB
JavaScript
/**
* Recording Management Routes
* HTTP endpoints for recording operations
*/
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const logger = require('../../../utils/logger');
module.exports = (dependencies) => {
const router = express.Router();
const { openviduAPI, storage, tokenManager } = dependencies;
/**
* Validation middleware
*/
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation Error',
details: errors.array(),
requestId: req.context?.requestId
});
}
next();
};
/**
* Authentication middleware
*/
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Missing or invalid authorization header',
requestId: req.context?.requestId
});
}
const token = authHeader.substring(7);
const tokenData = tokenManager.validateAPIToken(token);
req.auth = tokenData;
next();
} catch (error) {
res.status(401).json({
error: 'Unauthorized',
message: error.message,
requestId: req.context?.requestId
});
}
};
/**
* POST /api/recordings/start
* Start recording a session
*/
router.post('/start',
authenticateToken,
[
body('sessionId').isString().isLength({ min: 1 }),
body('name').optional().isString().isLength({ min: 1, max: 100 }),
body('outputMode').optional().isIn(['COMPOSED', 'INDIVIDUAL']),
body('recordingLayout').optional().isIn(['BEST_FIT', 'PICTURE_IN_PICTURE', 'VERTICAL_PRESENTATION', 'HORIZONTAL_PRESENTATION', 'CUSTOM']),
body('hasAudio').optional().isBoolean(),
body('hasVideo').optional().isBoolean(),
body('resolution').optional().isString(),
body('frameRate').optional().isInt({ min: 1, max: 60 }),
body('shmSize').optional().isInt({ min: 64 }),
body('customLayout').optional().isString()
],
handleValidationErrors,
async (req, res, next) => {
try {
const {
sessionId,
name,
outputMode = 'COMPOSED',
recordingLayout = 'BEST_FIT',
hasAudio = true,
hasVideo = true,
resolution = '1920x1080',
frameRate = 25,
shmSize = 512,
customLayout
} = req.body;
// Check if session exists and is active
const sessionData = await storage.getSession(sessionId);
if (!sessionData) {
return res.status(404).json({
error: 'Not Found',
message: 'Session not found',
requestId: req.context?.requestId
});
}
if (sessionData.status !== 'active') {
return res.status(400).json({
error: 'Bad Request',
message: 'Session is not active',
requestId: req.context?.requestId
});
}
// Check if recording is already in progress
const activeRecording = sessionData.recordings?.find(r => r.status === 'started');
if (activeRecording) {
return res.status(400).json({
error: 'Bad Request',
message: 'Recording is already in progress for this session',
requestId: req.context?.requestId
});
}
// Prepare recording options
const recordingOptions = {
name: name || `recording-${sessionId}-${Date.now()}`,
outputMode: outputMode,
recordingLayout: recordingLayout,
hasAudio: hasAudio,
hasVideo: hasVideo,
resolution: resolution,
frameRate: frameRate,
shmSize: shmSize
};
if (customLayout && recordingLayout === 'CUSTOM') {
recordingOptions.customLayout = customLayout;
}
// Start recording in OpenVidu
const recording = await openviduAPI.startRecording(sessionId, recordingOptions);
// Store recording data
const recordingData = {
recordingId: recording.id,
sessionId: sessionId,
name: recording.name,
outputMode: recording.outputMode,
recordingLayout: recording.recordingLayout,
hasAudio: recording.hasAudio,
hasVideo: recording.hasVideo,
resolution: recording.resolution,
frameRate: recording.frameRate,
status: recording.status,
startedAt: new Date(),
createdBy: req.auth.clientId,
openviduData: recording
};
await storage.storeRecording(recording.id, recordingData);
// Update session with recording info
if (!sessionData.recordings) {
sessionData.recordings = [];
}
sessionData.recordings.push(recordingData);
await storage.updateSession(sessionId, { recordings: sessionData.recordings });
logger.info(`Recording started for session ${sessionId}`, {
recordingId: recording.id,
name: recording.name,
clientId: req.auth.clientId,
requestId: req.context?.requestId
});
res.status(201).json({
success: true,
data: {
recordingId: recording.id,
sessionId: sessionId,
name: recording.name,
status: recording.status,
outputMode: recording.outputMode,
recordingLayout: recording.recordingLayout,
hasAudio: recording.hasAudio,
hasVideo: recording.hasVideo,
resolution: recording.resolution,
frameRate: recording.frameRate,
startedAt: recordingData.startedAt
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error starting recording:', error);
next(error);
}
}
);
/**
* POST /api/recordings/stop/:recordingId
* Stop recording
*/
router.post('/stop/:recordingId',
authenticateToken,
[
param('recordingId').isString().isLength({ min: 1 })
],
handleValidationErrors,
async (req, res, next) => {
try {
const recordingId = req.params.recordingId;
// Get recording data
const recordingData = await storage.getRecording(recordingId);
if (!recordingData) {
return res.status(404).json({
error: 'Not Found',
message: 'Recording not found',
requestId: req.context?.requestId
});
}
if (recordingData.status !== 'started') {
return res.status(400).json({
error: 'Bad Request',
message: 'Recording is not currently active',
requestId: req.context?.requestId
});
}
// Stop recording in OpenVidu
const stoppedRecording = await openviduAPI.stopRecording(recordingId);
// Update recording data
const updates = {
status: stoppedRecording.status,
size: stoppedRecording.size,
duration: stoppedRecording.duration,
url: stoppedRecording.url,
stoppedAt: new Date(),
openviduData: stoppedRecording
};
await storage.updateRecording(recordingId, updates);
// Update session recordings
const sessionData = await storage.getSession(recordingData.sessionId);
if (sessionData && sessionData.recordings) {
const recordingIndex = sessionData.recordings.findIndex(r => r.recordingId === recordingId);
if (recordingIndex !== -1) {
sessionData.recordings[recordingIndex] = { ...recordingData, ...updates };
await storage.updateSession(recordingData.sessionId, { recordings: sessionData.recordings });
}
}
logger.info(`Recording stopped: ${recordingId}`, {
sessionId: recordingData.sessionId,
duration: stoppedRecording.duration,
size: stoppedRecording.size,
clientId: req.auth.clientId,
requestId: req.context?.requestId
});
res.json({
success: true,
data: {
recordingId: recordingId,
sessionId: recordingData.sessionId,
name: recordingData.name,
status: stoppedRecording.status,
duration: stoppedRecording.duration,
size: stoppedRecording.size,
url: stoppedRecording.url,
stoppedAt: updates.stoppedAt
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error stopping recording:', error);
next(error);
}
}
);
/**
* GET /api/recordings/:recordingId
* Get recording details
*/
router.get('/:recordingId',
authenticateToken,
[
param('recordingId').isString().isLength({ min: 1 })
],
handleValidationErrors,
async (req, res, next) => {
try {
const recordingId = req.params.recordingId;
// Get recording from storage
let recordingData = await storage.getRecording(recordingId);
if (!recordingData) {
return res.status(404).json({
error: 'Not Found',
message: 'Recording not found',
requestId: req.context?.requestId
});
}
// Get fresh data from OpenVidu
try {
const openviduRecording = await openviduAPI.getRecording(recordingId);
recordingData.openviduData = openviduRecording;
// Update storage with fresh data
await storage.updateRecording(recordingId, {
status: openviduRecording.status,
size: openviduRecording.size,
duration: openviduRecording.duration,
url: openviduRecording.url,
openviduData: openviduRecording
});
} catch (openviduError) {
// Recording might not exist in OpenVidu anymore
logger.warn(`OpenVidu recording ${recordingId} not found:`, openviduError.message);
}
res.json({
success: true,
data: {
recordingId: recordingData.recordingId,
sessionId: recordingData.sessionId,
name: recordingData.name,
status: recordingData.status,
outputMode: recordingData.outputMode,
recordingLayout: recordingData.recordingLayout,
hasAudio: recordingData.hasAudio,
hasVideo: recordingData.hasVideo,
resolution: recordingData.resolution,
frameRate: recordingData.frameRate,
duration: recordingData.duration,
size: recordingData.size,
url: recordingData.url,
startedAt: recordingData.startedAt,
stoppedAt: recordingData.stoppedAt
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error fetching recording:', error);
next(error);
}
}
);
/**
* GET /api/recordings
* Get all recordings with optional filtering
*/
router.get('/',
authenticateToken,
[
query('sessionId').optional().isString(),
query('status').optional().isIn(['started', 'stopped', 'ready', 'failed']),
query('limit').optional().isInt({ min: 1, max: 100 }),
query('offset').optional().isInt({ min: 0 })
],
handleValidationErrors,
async (req, res, next) => {
try {
const { sessionId, status, limit = 20, offset = 0 } = req.query;
let recordings = [];
if (sessionId) {
// Get recordings for specific session
recordings = await storage.getSessionRecordings(sessionId);
} else {
// Get all recordings (this would need to be implemented in storage)
const sessions = await storage.getActiveSessions();
for (const session of sessions) {
if (session.recordings) {
recordings.push(...session.recordings);
}
}
}
// Filter by status if provided
if (status) {
recordings = recordings.filter(r => r.status === status);
}
// Sort by creation date (newest first)
recordings.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
// Apply pagination
const total = recordings.length;
const paginatedRecordings = recordings.slice(offset, offset + limit);
res.json({
success: true,
data: paginatedRecordings.map(recording => ({
recordingId: recording.recordingId,
sessionId: recording.sessionId,
name: recording.name,
status: recording.status,
outputMode: recording.outputMode,
duration: recording.duration,
size: recording.size,
url: recording.url,
startedAt: recording.startedAt,
stoppedAt: recording.stoppedAt
})),
pagination: {
total: total,
limit: limit,
offset: offset,
hasMore: offset + limit < total
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error fetching recordings:', error);
next(error);
}
}
);
/**
* DELETE /api/recordings/:recordingId
* Delete a recording
*/
router.delete('/:recordingId',
authenticateToken,
[
param('recordingId').isString().isLength({ min: 1 })
],
handleValidationErrors,
async (req, res, next) => {
try {
const recordingId = req.params.recordingId;
// Get recording data
const recordingData = await storage.getRecording(recordingId);
if (!recordingData) {
return res.status(404).json({
error: 'Not Found',
message: 'Recording not found',
requestId: req.context?.requestId
});
}
// Delete recording from OpenVidu
try {
await openviduAPI.deleteRecording(recordingId);
} catch (openviduError) {
// Recording might already be deleted from OpenVidu
logger.warn(`OpenVidu recording ${recordingId} was not found for deletion:`, openviduError.message);
}
// Update local storage - mark as deleted
await storage.updateRecording(recordingId, {
status: 'deleted',
deletedAt: new Date()
});
// Remove from session recordings
const sessionData = await storage.getSession(recordingData.sessionId);
if (sessionData && sessionData.recordings) {
sessionData.recordings = sessionData.recordings.filter(
r => r.recordingId !== recordingId
);
await storage.updateSession(recordingData.sessionId, { recordings: sessionData.recordings });
}
logger.info(`Recording deleted: ${recordingId}`, {
sessionId: recordingData.sessionId,
clientId: req.auth.clientId,
requestId: req.context?.requestId
});
res.json({
success: true,
message: 'Recording deleted successfully',
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error deleting recording:', error);
next(error);
}
}
);
return router;
};