import express from 'express'; import { execSync } from 'child_process'; import captureManager from '../capture-manager.js'; const router = express.Router(); const MAM_API_URL = process.env.MAM_API_URL || 'http://mam-api:3000'; /** * GET /devices * List available DeckLink devices */ router.get('/devices', (req, res) => { try { const devices = []; let output = ''; try { output = execSync('ffmpeg -f decklink -list_devices 1 -i dummy 2>&1', { encoding: 'utf-8', }); } catch (error) { // ffmpeg returns non-zero, but stderr is still captured output = error.stderr ? error.stderr.toString() : error.toString(); } // Parse ffmpeg output for DeckLink device names // Format: [decklink @ ...] "DeckLink Quad 2" (input #0) const lines = output.split('\n'); let deviceIndex = 0; for (const line of lines) { const match = line.match(/^\s*\[decklink[^\]]*\]\s+"([^"]+)"/); if (match) { devices.push({ index: deviceIndex, name: match[1], }); deviceIndex++; } } res.json({ devices }); } catch (error) { console.error('Error listing devices:', error); res.status(500).json({ error: 'Failed to list devices' }); } }); /** * GET /status * Get current capture status */ router.get('/status', (req, res) => { try { const status = captureManager.getStatus(); res.json(status); } catch (error) { console.error('Error getting status:', error); res.status(500).json({ error: 'Failed to get status' }); } }); /** * POST /start * Start a new capture session * * Body (SDI): * { project_id, clip_name, device, bin_id?, source_type? } * * Body (SRT/RTMP caller): * { project_id, clip_name, source_type, source_url, bin_id? } * * Body (SRT/RTMP listener): * { project_id, clip_name, source_type, listen: true, listen_port?, stream_key?, bin_id? } */ router.post('/start', async (req, res) => { try { const { project_id, bin_id, clip_name, device, source_type = 'sdi', source_url, listen = false, listen_port, stream_key, } = req.body; if (!project_id || !clip_name) { return res.status(400).json({ error: 'Missing required fields: project_id, clip_name', }); } // Source-specific validation if (source_type === 'sdi') { if (device === undefined || device === null) { return res.status(400).json({ error: 'SDI source requires: device' }); } } else if (source_type === 'srt' || source_type === 'rtmp') { if (!listen && !source_url) { return res.status(400).json({ error: `${source_type.toUpperCase()} caller mode requires: source_url`, }); } } else { return res.status(400).json({ error: `Unknown source_type: ${source_type}. Must be sdi, srt, or rtmp`, }); } const session = await captureManager.start({ projectId: project_id, binId: bin_id || null, clipName: clip_name, device, sourceType: source_type, sourceUrl: source_url, listen, listenPort: listen_port, streamKey: stream_key, }); res.json(session); } catch (error) { console.error('Error starting capture:', error); res.status(500).json({ error: error.message }); } }); /** * POST /stop * Stop the current capture session * Body: { session_id } */ router.post('/stop', async (req, res) => { try { const { session_id } = req.body; if (!session_id) { return res.status(400).json({ error: 'Missing required field: session_id' }); } const completedSession = await captureManager.stop(session_id); // Register asset with mam-api. // If proxyKey is null (SRT/RTMP source), set needsProxy=true so the // worker generates a proxy from the hires file asynchronously. try { const mamResponse = await fetch(`${MAM_API_URL}/api/v1/assets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: completedSession.projectId, binId: completedSession.binId, clipName: completedSession.clipName, sourceType: completedSession.sourceType, hiresKey: completedSession.hiresKey, proxyKey: completedSession.proxyKey, needsProxy: completedSession.proxyKey === null, duration: completedSession.duration, capturedAt: completedSession.startedAt, }), }); if (!mamResponse.ok) { console.warn( `MAM API registration returned ${mamResponse.status}: ${await mamResponse.text()}`, ); } } catch (mamError) { console.warn('Failed to register asset with MAM API:', mamError.message); } res.json(completedSession); } catch (error) { console.error('Error stopping capture:', error); res.status(500).json({ error: error.message }); } }); export default router;