probe: fallback to basic TCP/UDP connectivity check when capture service is offline

This commit is contained in:
Zac Gaetano 2026-05-22 17:22:30 -04:00
parent 8b57a9a35a
commit 4864db03f3

View file

@ -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;