From 7c7fcd2b0d4ce63cbae3a9653be4084a55a01a4d Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Tue, 7 Apr 2026 21:58:30 -0400 Subject: [PATCH] add services/capture/src/routes/capture.js --- services/capture/src/routes/capture.js | 140 +++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 services/capture/src/routes/capture.js diff --git a/services/capture/src/routes/capture.js b/services/capture/src/routes/capture.js new file mode 100644 index 0000000..4c34892 --- /dev/null +++ b/services/capture/src/routes/capture.js @@ -0,0 +1,140 @@ +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: { project_id, bin_id, clip_name, device } + */ +router.post('/start', async (req, res) => { + try { + const { project_id, bin_id, clip_name, device } = req.body; + + if (!project_id || !bin_id || !clip_name || device === undefined) { + return res.status(400).json({ + error: 'Missing required fields: project_id, bin_id, clip_name, device', + }); + } + + const session = await captureManager.start({ + projectId: project_id, + binId: bin_id, + clipName: clip_name, + device, + }); + + 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 + 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, + hiresKey: completedSession.hiresKey, + proxyKey: completedSession.proxyKey, + 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;