From 83188f341f4b877a743c229fc6a77455150a7368 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 8 Jun 2026 23:55:30 -0400 Subject: [PATCH 1/4] fix(go2rtc): sanitize RTSP source fragments and honor #noaudio (#429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit go2rtc's RTSP source parses the URL fragment with strings.Cut + ParseQuery and only acts on a fixed set of keys (transport, timeout, backchannel, media, pkt_size, log_level, source, mp4). Any other token an operator appends to the camera URL — most commonly "#noaudio" — is silently swallowed: it does nothing and rides along in the stored source. Worse, LightNVR ignored the "#noaudio" intent entirely and still injected the ffmpeg:#audio=aac#audio=opus producer. Sanitize the fragment of native rtsp:// sources before appending our own parameters: drop unsupported tokens (with a warning so it isn't silent), and promote "#noaudio"/"#no-audio" into an actual suppression of the audio producer. This gives operators a working lever to turn the audio pipeline off per-stream, which doubles as a clean isolation test for the remaining WebRTC-audio investigation on #429. Non-RTSP sources (ffmpeg:, wyze://, onvif://, http://) are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/video/go2rtc/go2rtc_stream.c | 91 +++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/video/go2rtc/go2rtc_stream.c b/src/video/go2rtc/go2rtc_stream.c index 2bbeb313..e3e0c4af 100644 --- a/src/video/go2rtc/go2rtc_stream.c +++ b/src/video/go2rtc/go2rtc_stream.c @@ -90,6 +90,88 @@ bool go2rtc_stream_init(const char *binary_path, const char *config_dir, int api return true; } +/* + * go2rtc's RTSP source only understands a fixed set of '#' fragment keys + * (internal/rtsp/rtsp.go: transport, timeout, backchannel, media, pkt_size, + * log_level, source, mp4). It parses the fragment with strings.Cut + ParseQuery, + * so any other token an operator appends to the camera URL — most commonly + * "#noaudio" — is silently swallowed: it neither errors nor does what the + * operator intended, and it rides along in the stored source confusing later + * inspection. We sanitize the fragment of native rtsp:// sources here so only + * supported keys survive, and we promote the common "#noaudio" intent into an + * actual audio-producer suppression instead of a no-op. (#429) + * + * Only valueless flags and the known keys are considered; non-RTSP sources + * (ffmpeg:, wyze://, onvif://, http://, ...) are never passed here, so their + * fragments are left untouched. + */ +static bool go2rtc_rtsp_fragment_key_supported(const char *key, size_t len) { + static const char *const known[] = { + "transport", "timeout", "backchannel", "media", + "pkt_size", "log_level", "source", "mp4", "weak_timeout" + }; + for (size_t i = 0; i < sizeof(known) / sizeof(known[0]); i++) { + if (strlen(known[i]) == len && strncasecmp(known[i], key, len) == 0) { + return true; + } + } + return false; +} + +// Rewrite `url` in place, dropping unsupported fragment tokens. Returns true if a +// "#noaudio" intent token was present so the caller can suppress the audio source. +static bool go2rtc_sanitize_rtsp_fragments(char *url, size_t url_size, const char *stream_id) { + char *hash = strchr(url, '#'); + if (!hash) { + return false; + } + + bool suppress_audio = false; + char rebuilt[URL_BUFFER_SIZE]; + size_t base_len = (size_t)(hash - url); + if (base_len >= sizeof(rebuilt)) { + return false; // pathologically long base; leave the URL untouched + } + memcpy(rebuilt, url, base_len); + rebuilt[base_len] = '\0'; + size_t out = base_len; + + for (const char *p = hash + 1; *p; ) { + const char *tok = p; + const char *end = strchr(p, '#'); + size_t tok_len = end ? (size_t)(end - tok) : strlen(tok); + + // Key is the part before '=' (or the whole token for valueless flags). + const char *eq = memchr(tok, '=', tok_len); + size_t key_len = eq ? (size_t)(eq - tok) : tok_len; + + if ((key_len == 7 && strncasecmp(tok, "noaudio", 7) == 0) || + (key_len == 8 && strncasecmp(tok, "no-audio", 8) == 0)) { + suppress_audio = true; + log_info("Stream %s: '#noaudio' in source URL -> suppressing go2rtc audio producer", + stream_id); + } else if (go2rtc_rtsp_fragment_key_supported(tok, key_len)) { + if (out + tok_len + 1 < url_size && out + tok_len + 1 < sizeof(rebuilt)) { + rebuilt[out++] = '#'; + memcpy(rebuilt + out, tok, tok_len); + out += tok_len; + rebuilt[out] = '\0'; + } + } else { + log_warn("Stream %s: dropping unsupported RTSP source fragment '#%.*s' " + "(go2rtc would ignore it)", stream_id, (int)tok_len, tok); + } + + if (!end) { + break; + } + p = end + 1; + } + + safe_strcpy(url, rebuilt, url_size, 0); + return suppress_audio; +} + bool go2rtc_stream_register(const char *stream_id, const char *stream_url, const char *username, const char *password, bool backchannel_enabled, stream_protocol_t protocol, @@ -159,7 +241,14 @@ bool go2rtc_stream_register(const char *stream_id, const char *stream_url, bool is_rtsp = (strncmp(modified_url, "rtsp://", 7) == 0 || strncmp(modified_url, "rtsps://", 8) == 0); + // Drop fragment tokens go2rtc can't act on (e.g. a hand-added "#noaudio") + // before we append our own, and honor a "#noaudio" request by suppressing the + // audio producer below. Only native rtsp:// sources are sanitized. (#429) + bool suppress_audio = false; + if (is_rtsp) { + suppress_audio = go2rtc_sanitize_rtsp_fragments(modified_url, URL_BUFFER_SIZE, stream_id); + char fragment_params[256] = {0}; int offset = 0; @@ -228,7 +317,7 @@ bool go2rtc_stream_register(const char *stream_id, const char *stream_url, sources[num_sources++] = modified_url; char ffmpeg_audio_source[URL_BUFFER_SIZE]; - if (record_audio) { + if (record_audio && !suppress_audio) { // Emit AAC (for MP4 recording + MPEG-TS HLS) and OPUS (for WebRTC) // from one ffmpeg process — go2rtc cannot transcode AAC->OPUS itself, // so the OPUS track is what actually makes audio audible in the From 3b7a6d4ae221618740acbb4b2d0b7054718250de Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 9 Jun 2026 00:05:28 -0400 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/video/go2rtc/go2rtc_stream.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video/go2rtc/go2rtc_stream.c b/src/video/go2rtc/go2rtc_stream.c index e3e0c4af..277ceb84 100644 --- a/src/video/go2rtc/go2rtc_stream.c +++ b/src/video/go2rtc/go2rtc_stream.c @@ -93,7 +93,7 @@ bool go2rtc_stream_init(const char *binary_path, const char *config_dir, int api /* * go2rtc's RTSP source only understands a fixed set of '#' fragment keys * (internal/rtsp/rtsp.go: transport, timeout, backchannel, media, pkt_size, - * log_level, source, mp4). It parses the fragment with strings.Cut + ParseQuery, + * log_level, source, mp4, weak_timeout). It parses the fragment with strings.Cut + ParseQuery, * so any other token an operator appends to the camera URL — most commonly * "#noaudio" — is silently swallowed: it neither errors nor does what the * operator intended, and it rides along in the stored source confusing later From 90b458f97ad6a128d52e2fef8307ab66d9d97477 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 9 Jun 2026 00:05:40 -0400 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/video/go2rtc/go2rtc_stream.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video/go2rtc/go2rtc_stream.c b/src/video/go2rtc/go2rtc_stream.c index 277ceb84..34650bf9 100644 --- a/src/video/go2rtc/go2rtc_stream.c +++ b/src/video/go2rtc/go2rtc_stream.c @@ -159,7 +159,7 @@ static bool go2rtc_sanitize_rtsp_fragments(char *url, size_t url_size, const cha } } else { log_warn("Stream %s: dropping unsupported RTSP source fragment '#%.*s' " - "(go2rtc would ignore it)", stream_id, (int)tok_len, tok); + "(value redacted; go2rtc would ignore it)", stream_id, (int)key_len, tok); } if (!end) { From 9a46a0b3a786bf32ab97e8c58038422a41a868ab Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 9 Jun 2026 00:05:47 -0400 Subject: [PATCH 4/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/video/go2rtc/go2rtc_stream.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video/go2rtc/go2rtc_stream.c b/src/video/go2rtc/go2rtc_stream.c index 34650bf9..40cfc6f8 100644 --- a/src/video/go2rtc/go2rtc_stream.c +++ b/src/video/go2rtc/go2rtc_stream.c @@ -111,7 +111,7 @@ static bool go2rtc_rtsp_fragment_key_supported(const char *key, size_t len) { "pkt_size", "log_level", "source", "mp4", "weak_timeout" }; for (size_t i = 0; i < sizeof(known) / sizeof(known[0]); i++) { - if (strlen(known[i]) == len && strncasecmp(known[i], key, len) == 0) { + if (strlen(known[i]) == len && strncmp(known[i], key, len) == 0) { return true; } }