mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
switchreplay workers to rtsp
This commit is contained in:
@@ -40,7 +40,8 @@ case "$PATH_NAME" in
|
||||
esac
|
||||
|
||||
exec ffmpeg -hide_banner -loglevel warning -nostdin -y \
|
||||
-i "srt://127.0.0.1:9000?streamid=read:${PATH_NAME}" \
|
||||
-rtsp_transport tcp \
|
||||
-i "rtsp://127.0.0.1:8554/${PATH_NAME}" \
|
||||
-an \
|
||||
-vf "$FILTER" \
|
||||
-q:v "$QUALITY" \
|
||||
|
||||
@@ -69,13 +69,9 @@ function buildMediaMtxConfig({ config, serverPort, snapshotWriterPath }) {
|
||||
{ url: 'stun:stun.cloudflare.com:3478' },
|
||||
],
|
||||
|
||||
/*
|
||||
Several server-local paths still use SRT: PTZ publishing, replay capture, and the snapshot
|
||||
writer. Rover media moves to RTSP, but removing this listener would break those independent
|
||||
consumers, so both listeners remain deliberately enabled.
|
||||
*/
|
||||
srt: true,
|
||||
srtAddress: ':9000',
|
||||
// Every publisher and server-local reader uses RTSP over TCP. Disable SRT
|
||||
// completely so MediaMTX cannot reintroduce the GoSRT/libSRT ACKACK mismatch.
|
||||
srt: false,
|
||||
|
||||
authMethod: 'http',
|
||||
authHTTPAddress: `http://127.0.0.1:${authPort}/mediamtx/auth`,
|
||||
|
||||
@@ -20,6 +20,8 @@ test('generates RTSP over TCP without deployment-specific hardcodes', () => {
|
||||
assert.equal(generated.rtsp, true);
|
||||
assert.equal(generated.rtspAddress, ':8554');
|
||||
assert.deepEqual(generated.rtspTransports, ['tcp']);
|
||||
assert.equal(generated.srt, false);
|
||||
assert.equal(Object.hasOwn(generated, 'srtAddress'), false);
|
||||
assert.equal(Object.hasOwn(generated, 'rtpAddress'), false);
|
||||
assert.equal(Object.hasOwn(generated, 'rtcpAddress'), false);
|
||||
assert.deepEqual(generated.webrtcAdditionalHosts, ['public.example.com', '10.20.30.40']);
|
||||
|
||||
@@ -573,7 +573,7 @@ function schedulePublisherRestart(reason = 'publisher-restart') {
|
||||
function startPublisher() {
|
||||
if (!enabled || !state.rtspUri || publisherProcess) return;
|
||||
const input = addCredentialsToRtsp(state.rtspUri);
|
||||
const output = `srt://127.0.0.1:9000?streamid=publish:${encodeURIComponent(PTZ_STREAM_PATH)}`;
|
||||
const output = `rtsp://127.0.0.1:8554/${encodeURIComponent(PTZ_STREAM_PATH)}`;
|
||||
/*
|
||||
The full-quality autotrack profile is H265, which is the right camera-side
|
||||
feed but has been unreliable through browser WHEP playback. Re-encoding is
|
||||
@@ -607,10 +607,9 @@ function startPublisher() {
|
||||
dead session. The existing exit handler then starts a new process, which is
|
||||
the part that creates a fresh RTSP connection after the camera comes back.
|
||||
|
||||
The mpegts muxer can also hold packets briefly before writing them to SRT.
|
||||
flush_packets/muxdelay/muxpreload are output-side latency knobs; they do not
|
||||
ask the camera or demuxer to discard frames, so they are a safer next step
|
||||
than the stale-frame dropping experiments that made the Reolink feed freeze.
|
||||
The MediaMTX output uses RTSP over TCP, matching every rover publisher and
|
||||
server-local reader. Keeping one media transport avoids the incompatible
|
||||
empty SRT ACKACK packets produced between GoSRT and Fedora's newer libSRT.
|
||||
*/
|
||||
const proc = spawn('ffmpeg', [
|
||||
'-hide_banner',
|
||||
@@ -671,12 +670,10 @@ function startPublisher() {
|
||||
'-2',
|
||||
'-flush_packets',
|
||||
'1',
|
||||
'-muxdelay',
|
||||
'0',
|
||||
'-muxpreload',
|
||||
'0',
|
||||
'-rtsp_transport',
|
||||
'tcp',
|
||||
'-f',
|
||||
'mpegts',
|
||||
'rtsp',
|
||||
output,
|
||||
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
publisherProcess = proc;
|
||||
@@ -1890,7 +1887,7 @@ module.exports = {
|
||||
audio the same way it already mixes rover audio.
|
||||
*/
|
||||
if (!enabled || !isReplayEnabled()) return [];
|
||||
const inputUrl = `srt://127.0.0.1:9000?streamid=read:${encodeURIComponent(PTZ_STREAM_PATH)}`;
|
||||
const inputUrl = `rtsp://127.0.0.1:8554/${encodeURIComponent(PTZ_STREAM_PATH)}`;
|
||||
const label = cameraConfig.name || 'PTZ Camera';
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -15,8 +15,8 @@ function sourceDirForKey(activeSegmentRoot, key) {
|
||||
return path.join(activeSegmentRoot, key);
|
||||
}
|
||||
|
||||
function toSrtReadPath(streamId) {
|
||||
return `srt://127.0.0.1:9000?streamid=read:${encodeURIComponent(streamId)}`;
|
||||
function toRtspReadPath(streamId) {
|
||||
return `rtsp://127.0.0.1:8554/${encodeURIComponent(streamId)}`;
|
||||
}
|
||||
|
||||
function getRoomCameraStream(camera) {
|
||||
@@ -37,9 +37,9 @@ function listDesiredSources() {
|
||||
const sources = [];
|
||||
for (const rover of roverManager.getRoster()) {
|
||||
const roverId = String(rover.id);
|
||||
sources.push({ id: roverId, sourceType: 'rover', kind: 'video', label: rover.name || roverId, inputUrl: toSrtReadPath(roverId) });
|
||||
sources.push({ id: roverId, sourceType: 'rover', kind: 'video', label: rover.name || roverId, inputUrl: toRtspReadPath(roverId) });
|
||||
if (hasRoverAudioCapture(rover)) {
|
||||
sources.push({ id: `${roverId}-audio`, sourceType: 'rover', roverId, kind: 'audio', label: `${rover.name || roverId} audio`, inputUrl: toSrtReadPath(`${roverId}-audio`) });
|
||||
sources.push({ id: `${roverId}-audio`, sourceType: 'rover', roverId, kind: 'audio', label: `${rover.name || roverId} audio`, inputUrl: toRtspReadPath(`${roverId}-audio`) });
|
||||
}
|
||||
}
|
||||
for (const camera of getRoomCameras()) {
|
||||
@@ -57,7 +57,12 @@ function listDesiredSources() {
|
||||
function buildWorkerArgs(activeSegmentRoot, source) {
|
||||
const dir = sourceDirForKey(activeSegmentRoot, sourceKey(source));
|
||||
const pattern = path.join(dir, 'seg-%06d.mp4');
|
||||
const common = ['-hide_banner', '-loglevel', 'warning', '-y', '-fflags', '+genpts', '-use_wallclock_as_timestamps', '1', '-i', source.inputUrl];
|
||||
// MediaMTX and RTSP cameras use TCP so replay capture has one reliable
|
||||
// transport and never falls back to separate RTP/RTCP UDP listeners.
|
||||
const inputTransport = /^rtsps?:\/\//i.test(source.inputUrl)
|
||||
? ['-rtsp_transport', 'tcp']
|
||||
: [];
|
||||
const common = ['-hide_banner', '-loglevel', 'warning', '-y', '-fflags', '+genpts', '-use_wallclock_as_timestamps', '1', ...inputTransport, '-i', source.inputUrl];
|
||||
|
||||
if (source.kind === 'audio') {
|
||||
return [
|
||||
|
||||
@@ -32,13 +32,12 @@ function registerVideoAuthRoute(deps) {
|
||||
});
|
||||
}
|
||||
|
||||
const isSrtLikeProtocol = protocol === 'srt' || protocol === 'srtconn' || protocol.startsWith('srt');
|
||||
const isRtspProtocol = protocol === 'rtsp' || protocol.startsWith('rtsp');
|
||||
const isForwardAudioRead = action === 'read' && streamInfo?.id?.endsWith('-fwd');
|
||||
if ((action === 'read' && isSrtLikeProtocol) || isForwardAudioRead) {
|
||||
return res.status(200).end();
|
||||
}
|
||||
if (action === 'publish' && isSrtLikeProtocol) {
|
||||
const isLoopback = ip === '127.0.0.1' || ip === '::1';
|
||||
// Replay and snapshot workers read MediaMTX through loopback RTSP. They do
|
||||
// not represent a browser session, while non-loopback RTSP readers remain
|
||||
// subject to the normal session authorization below.
|
||||
if (action === 'read' && isRtspProtocol && isLoopback) {
|
||||
return res.status(200).end();
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -5,7 +5,7 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { registerVideoAuthRoute } = require('./httpRoute');
|
||||
|
||||
function createHarness() {
|
||||
function createHarness({ requestIp = '127.0.0.1' } = {}) {
|
||||
let handler;
|
||||
const app = { post: (_path, fn) => { handler = fn; } };
|
||||
registerVideoAuthRoute({
|
||||
@@ -13,7 +13,7 @@ function createHarness() {
|
||||
io: { sockets: { sockets: new Map() } },
|
||||
logger: { info() {}, warn() {} },
|
||||
videoSessions: { getSession: () => null, revokeSession() {} },
|
||||
getRequestIp: () => '127.0.0.1',
|
||||
getRequestIp: () => requestIp,
|
||||
logAdminEvent() {},
|
||||
extractStreamInfoFromBody: (body) => ({ type: 'rover', id: body.path, baseId: body.path }),
|
||||
canAccessStream: () => false,
|
||||
@@ -42,6 +42,16 @@ test('allows an RTSP rover publisher without a browser session', () => {
|
||||
assert.equal(request({ protocol: 'rtsp', action: 'publish', path: 'rover-one' }), 200);
|
||||
});
|
||||
|
||||
test('allows server-local RTSP replay and snapshot readers without a browser session', () => {
|
||||
const { request } = createHarness();
|
||||
assert.equal(request({ protocol: 'rtsp', action: 'read', path: 'rover-one' }), 200);
|
||||
});
|
||||
|
||||
test('continues rejecting an unauthenticated remote RTSP reader', () => {
|
||||
const { request } = createHarness({ requestIp: '192.0.2.10' });
|
||||
assert.equal(request({ protocol: 'rtsp', action: 'read', path: 'rover-one' }), 401);
|
||||
});
|
||||
|
||||
test('continues rejecting an unauthenticated WebRTC read', () => {
|
||||
const { request } = createHarness();
|
||||
assert.equal(request({ protocol: 'webrtc', action: 'read', path: 'rover-one' }), 401);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Video Auth Stream Parsing
|
||||
// Purpose: Parses MediaMTX path/body payloads into normalized stream targets for rover and room media checks.
|
||||
// Scope: Handles native MediaMTX WHEP/WHIP paths and SRT streamid extraction without performing auth decisions.
|
||||
// Scope: Handles native MediaMTX path forms without performing auth decisions.
|
||||
|
||||
const PTZ_STREAM_PATH = 'ptz-camera';
|
||||
|
||||
@@ -44,42 +44,10 @@ function extractStreamInfo(path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractSrtStreamId(rawValue) {
|
||||
const value = decodeURIComponent(String(rawValue || '').trim());
|
||||
if (!value) return '';
|
||||
|
||||
const match = value.match(/(?:^|[?&]|,|#!::)r=([^,&]+)/);
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
if (!/[?&=,:]/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractStreamInfoFromBody(body = {}) {
|
||||
const fromPath = extractStreamInfo((body.path || '').replace(/^\//, ''));
|
||||
if (fromPath) return fromPath;
|
||||
|
||||
const srtId =
|
||||
extractSrtStreamId(body.streamid) ||
|
||||
extractSrtStreamId(body.streamId) ||
|
||||
extractSrtStreamId(body.query);
|
||||
if (!srtId) return null;
|
||||
|
||||
if (srtId === PTZ_STREAM_PATH) {
|
||||
return { type: 'ptz', id: srtId };
|
||||
}
|
||||
|
||||
if (srtId.endsWith('-fwd')) {
|
||||
return { type: 'rover', id: srtId, baseId: srtId.slice(0, -4) };
|
||||
}
|
||||
|
||||
const baseId = srtId.endsWith('-audio') ? srtId.slice(0, -6) : srtId;
|
||||
return { type: 'rover', id: srtId, baseId };
|
||||
// All enabled MediaMTX protocols now report the canonical path directly;
|
||||
// there is no second SRT stream-id syntax to normalize or authorize.
|
||||
return extractStreamInfo((body.path || '').replace(/^\//, ''));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user