diff --git a/server/config.example.yaml b/server/config.example.yaml index 8a9a0b10..3c97850a 100644 --- a/server/config.example.yaml +++ b/server/config.example.yaml @@ -16,6 +16,7 @@ media: preview: enabled: false codec: "av1" + transport: "rtsp" fps: 10 width: 640 roomBitrateKbps: 200 diff --git a/server/mediamtx/mediamtx.yml b/server/mediamtx/mediamtx.yml index ac3fff8a..8c0e639f 100644 --- a/server/mediamtx/mediamtx.yml +++ b/server/mediamtx/mediamtx.yml @@ -8,7 +8,7 @@ metricsAddress: 0.0.0.0:9998 pprof: no pprofAddress: 127.0.0.1:9999 -rtsp: no +rtsp: yes rtmp: no hls: no diff --git a/server/src/services/media/previewTranscoderService.js b/server/src/services/media/previewTranscoderService.js index 50674295..df2a59fe 100644 --- a/server/src/services/media/previewTranscoderService.js +++ b/server/src/services/media/previewTranscoderService.js @@ -12,6 +12,7 @@ const previewConfig = mediaConfig.preview || {}; const ENABLED = Boolean(previewConfig.enabled); const PREVIEW_CODEC = String(previewConfig.codec || 'av1').toLowerCase(); +const PREVIEW_TRANSPORT = String(previewConfig.transport || 'rtsp').toLowerCase(); const PREVIEW_FPS = Number(previewConfig.fps || 10); const PREVIEW_WIDTH = Number(previewConfig.width || 640); const ROOM_BITRATE_KBPS = Number(previewConfig.roomBitrateKbps || 200); @@ -36,6 +37,10 @@ function buildSrtPublishUrl(streamId) { return `srt://127.0.0.1:9000?streamid=#!::r=${encoded},m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316`; } +function buildRtspPublishUrl(streamId) { + return `rtsp://127.0.0.1:8554/${streamId}`; +} + function sanitizeCodec(codec) { return String(codec || '').toLowerCase().replace(/[^a-z0-9]/g, '') || 'av1'; } @@ -91,6 +96,17 @@ function buildArgs(source) { const gop = Math.max(1, Math.round(GOP_SECONDS * PREVIEW_FPS)); const maxrate = Math.floor(source.bitrateKbps * 1.1); const bufsize = Math.max(1, source.bitrateKbps * 2); + const codec = PREVIEW_CODEC === 'av1' ? 'libsvtav1' : 'libx264'; + const extraCodecArgs = + codec === 'libx264' + ? ['-tune', 'zerolatency', '-profile:v', 'baseline', '-sc_threshold', '0'] + : []; + const outputUrl = + PREVIEW_TRANSPORT === 'rtsp' ? buildRtspPublishUrl(source.outputId) : buildSrtPublishUrl(source.outputId); + const outputArgs = + PREVIEW_TRANSPORT === 'rtsp' + ? ['-f', 'rtsp', '-rtsp_transport', 'tcp'] + : ['-f', 'mpegts']; return [ '-hide_banner', '-loglevel', @@ -105,9 +121,10 @@ function buildArgs(source) { '-vf', `fps=${PREVIEW_FPS},scale=${PREVIEW_WIDTH}:-1`, '-c:v', - PREVIEW_CODEC === 'av1' ? 'libsvtav1' : 'libx264', + codec, '-preset', PRESET, + ...extraCodecArgs, '-g', String(gop), '-keyint_min', @@ -120,9 +137,8 @@ function buildArgs(source) { `${bufsize}k`, '-pix_fmt', 'yuv420p', - '-f', - 'mpegts', - buildSrtPublishUrl(source.outputId), + ...outputArgs, + outputUrl, ]; } diff --git a/webui/src/components/DriverVideoPanel.jsx b/webui/src/components/DriverVideoPanel.jsx index e64e5f71..7e10fb0f 100644 --- a/webui/src/components/DriverVideoPanel.jsx +++ b/webui/src/components/DriverVideoPanel.jsx @@ -24,7 +24,6 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) { return () => clearInterval(timer); }, [session?.mode]); const roverId = session?.assignment?.roverId; - const av1Supported = supportsAv1WebRtc(); const rosterEntry = roverId && session?.roster ? session.roster.find((item) => String(item.id) === String(roverId)) : null; const hasAudio = Boolean(rosterEntry?.media?.audioPublishUrl); @@ -71,6 +70,7 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) { const sources = useVideoRequests(entries); const info = roverId && shouldShowVideo ? sources[roverId] : null; const audioInfo = roverId && hasAudio ? sources[`${roverId}-audio`] : null; + const av1Supported = supportsAv1WebRtc(); const previewEntries = roverId ? [ { diff --git a/webui/src/components/RoomCameraPanel.jsx b/webui/src/components/RoomCameraPanel.jsx index 528a1a6b..d653656e 100644 --- a/webui/src/components/RoomCameraPanel.jsx +++ b/webui/src/components/RoomCameraPanel.jsx @@ -118,7 +118,7 @@ export default function RoomCameraPanel({ feed={feed} label={camera.name || camera.id} videoSession={previewSession} - preferVideo={av1Supported} + preferVideo={Boolean(previewSession?.url)} /> ); diff --git a/webui/src/mini/MiniSummaryApp.jsx b/webui/src/mini/MiniSummaryApp.jsx index 4c31cce7..56575bdd 100644 --- a/webui/src/mini/MiniSummaryApp.jsx +++ b/webui/src/mini/MiniSummaryApp.jsx @@ -36,13 +36,13 @@ function MiniSummaryContent() { enabled: !inLockdown, version: session?.mode, }); - const av1Supported = supportsAv1WebRtc(); const [index, setIndex] = useState(0); const snapshotFeeds = useRoverSnapshots( roster.map((rover) => rover.id), { enabled: !inLockdown, version: session?.mode }, ); + const av1Supported = supportsAv1WebRtc(); const previewEntries = roster.map((rover) => ({ type: 'rover', id: rover.id, @@ -162,7 +162,12 @@ function MiniSummaryContent() { ) : activeCamera ? ( - + ) : (
diff --git a/webui/src/spectate/SpectatorApp.jsx b/webui/src/spectate/SpectatorApp.jsx index b3ab3b7d..1885243e 100644 --- a/webui/src/spectate/SpectatorApp.jsx +++ b/webui/src/spectate/SpectatorApp.jsx @@ -103,11 +103,11 @@ function SpectatorContent() { useSpectatorMode(); const frames = useTelemetryFrames(); const roster = session?.roster ?? []; - const av1Supported = supportsAv1WebRtc(); const snapshotFeeds = useRoverSnapshots( roster.map((rover) => rover.id), { enabled: !inLockdown, version: session?.mode }, ); + const av1Supported = supportsAv1WebRtc(); const previewEntries = roster.map((rover) => ({ type: 'rover', id: rover.id,