UNPKG

aethercall

Version:

A scalable WebRTC video calling API built with Node.js and OpenVidu

490 lines (443 loc) 19.7 kB
/** * 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; };