import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import playoutManager from './playout-manager.js'; dotenv.config(); const app = express(); const PORT = process.env.PORT || 3002; app.use(cors()); app.use(express.json()); app.get('/health', (req, res) => res.json({ status: 'ok' })); // Start the channel's output consumer. Body: { outputType, outputConfig, videoFormat } app.post('/channel/start', async (req, res) => { try { const out = await playoutManager.startChannel(req.body || {}); res.json(out); } catch (err) { console.error('[playout] /channel/start error:', err.message); res.status(500).json({ error: err.message }); } }); app.post('/channel/stop', async (req, res) => { try { res.json(await playoutManager.stopChannel()); } catch (err) { res.status(500).json({ error: err.message }); } }); // Load + start a playlist. Body: { items: [...], loop } app.post('/playlist/load', async (req, res) => { try { const { items = [], loop = false } = req.body || {}; res.json(await playoutManager.loadPlaylist({ items, loop })); } catch (err) { console.error('[playout] /playlist/load error:', err.message); res.status(500).json({ error: err.message }); } }); app.post('/transport/skip', async (req, res) => { try { res.json(await playoutManager.skip()); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/transport/pause', async (req, res) => { try { res.json(await playoutManager.pause()); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/transport/resume', async (req, res) => { try { res.json(await playoutManager.resume()); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/status', (req, res) => res.json(playoutManager.getStatus())); // Auto-start: when the sidecar is spawned by mam-api with channel env, bring up // the output consumer immediately so the container is "on air idle" (black/slate) // the moment it boots, mirroring the capture sidecar's bootstrap pattern. async function bootstrap() { const outputType = process.env.OUTPUT_TYPE; if (!outputType) { console.log('[bootstrap] no OUTPUT_TYPE — on-demand sidecar, waiting for /channel/start'); return; } let outputConfig = {}; try { outputConfig = JSON.parse(process.env.OUTPUT_CONFIG || '{}'); } catch (err) { console.error('[bootstrap] bad OUTPUT_CONFIG json:', err.message); } const videoFormat = process.env.VIDEO_FORMAT || '1080i5994'; try { await playoutManager.startChannel({ outputType, outputConfig, videoFormat }); } catch (err) { console.error('[bootstrap] channel start failed:', err.message); } } const server = app.listen(PORT, () => { console.log(`Wild Dragon Playout Service listening on port ${PORT}`); // Give CasparCG a moment to come up (started by the container entrypoint). playoutManager.amcp.connect(); bootstrap(); }); function shutdown(sig) { console.log(`[playout] ${sig} — shutting down`); playoutManager.stopChannel().catch(() => {}).finally(() => { playoutManager.amcp.close(); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 5000); }); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT'));