From 4864db03f3b2973e82702ca34b94c9cb3910d4c6 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Fri, 22 May 2026 17:22:30 -0400 Subject: [PATCH] probe: fallback to basic TCP/UDP connectivity check when capture service is offline --- services/mam-api/src/routes/recorders.js | 85 +++++++++++++++++++++--- 1 file changed, 76 insertions(+), 9 deletions(-) diff --git a/services/mam-api/src/routes/recorders.js b/services/mam-api/src/routes/recorders.js index 371989a..01e52ea 100644 --- a/services/mam-api/src/routes/recorders.js +++ b/services/mam-api/src/routes/recorders.js @@ -1,5 +1,7 @@ import express from 'express'; import http from 'http'; +import net from 'net'; +import dgram from 'dgram'; import pool from '../db/pool.js'; import { requireAuth } from '../middleware/auth.js'; import { v4 as uuidv4 } from 'uuid'; @@ -165,9 +167,9 @@ router.post('/', async (req, res, next) => { proxy_enabled: true, proxy_codec: 'h264', proxy_resolution: '1920x1080', - proxy_video_bitrate: '8M', + proxy_video_bitrate: '2M', proxy_audio_codec: 'aac', - proxy_audio_bitrate: '192k', + proxy_audio_bitrate: '128k', proxy_audio_channels: 2, proxy_container: 'mp4', }; @@ -331,10 +333,10 @@ router.post('/:id/start', async (req, res, next) => { `PROXY_ENABLED=${recorder.proxy_enabled !== false ? 'true' : 'false'}`, `PROXY_CODEC=${recorder.proxy_codec || 'h264'}`, `PROXY_RESOLUTION=${recorder.proxy_resolution || '1920x1080'}`, - `PROXY_VIDEO_BITRATE=${recorder.proxy_video_bitrate || '8M'}`, + `PROXY_VIDEO_BITRATE=${recorder.proxy_video_bitrate || '2M'}`, `PROXY_FRAMERATE=${recorder.proxy_framerate || ''}`, `PROXY_AUDIO_CODEC=${recorder.proxy_audio_codec || 'aac'}`, - `PROXY_AUDIO_BITRATE=${recorder.proxy_audio_bitrate || '192k'}`, + `PROXY_AUDIO_BITRATE=${recorder.proxy_audio_bitrate || '128k'}`, `PROXY_AUDIO_CHANNELS=${recorder.proxy_audio_channels ?? 2}`, `PROXY_CONTAINER=${recorder.proxy_container || 'mp4'}`, @@ -648,19 +650,84 @@ router.delete('/:id', async (req, res, next) => { } }); -router.post('/probe', async (req, res, next) => { +// POST /probe - Test source connectivity. +// Tries the capture service first (full SRT/RTMP handshake + metadata). +// Falls back to a basic TCP/UDP reachability check if capture is offline. +router.post('/probe', async (req, res) => { + const { source_type, url } = req.body || {}; + + // Try capture service first โ€” it can do a real SRT/RTMP handshake try { const r = await fetch('http://capture:3001/capture/probe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req.body || {}), - signal: AbortSignal.timeout(15000), + signal: AbortSignal.timeout(5000), }); const data = await r.json().catch(() => ({})); - res.status(r.status).json(data); - } catch (err) { - next(err); + return res.status(r.status).json(data); + } catch (_) { + // capture service not running โ€” fall through to basic connectivity probe } + + if (!url) { + return res.json({ + reachable: false, + mode: 'basic', + note: 'Capture service offline. Provide a URL for connectivity check.', + }); + } + + let parsed; + try { parsed = new URL(url); } catch { + return res.status(400).json({ error: 'Invalid URL' }); + } + + const host = parsed.hostname; + const proto = (parsed.protocol || '').replace(':', '').toLowerCase(); + const isUdp = proto === 'srt' || source_type === 'srt'; + const port = parseInt(parsed.port, 10) || (isUdp ? 9000 : 1935); + + const reachable = await (isUdp ? probeUdp(host, port) : probeTcp(host, port)); + return res.json({ + reachable, + mode: 'basic', + note: `Capture service offline ยท ${isUdp ? 'UDP' : 'TCP'} connectivity check only`, + ...(reachable ? { source: `${host}:${port}` } : { error: `${host}:${port} did not respond` }), + }); }); +function probeTcp(host, port) { + return new Promise((resolve) => { + const sock = new net.Socket(); + let done = false; + const finish = (ok) => { if (!done) { done = true; sock.destroy(); resolve(ok); } }; + sock.setTimeout(4000); + sock.connect(port, host, () => finish(true)); + sock.on('error', () => finish(false)); + sock.on('timeout', () => finish(false)); + }); +} + +function probeUdp(host, port) { + return new Promise((resolve) => { + const sock = dgram.createSocket('udp4'); + let done = false; + const finish = (ok) => { + if (done) return; + done = true; + try { sock.close(); } catch (_) {} + resolve(ok); + }; + sock.on('error', () => finish(false)); + sock.send(Buffer.alloc(16, 0), 0, 16, port, host, (err) => { + if (err) return finish(false); + // If no ICMP port-unreachable arrives within 2.5s, assume port is open + setTimeout(() => finish(true), 2500); + }); + // Hard timeout โ€” give up after 5s regardless + setTimeout(() => finish(false), 5000); + }); +} + export default router;