From 4a9082a3e92f0a6eb07ecdd6f63e74b9b32c572c Mon Sep 17 00:00:00 2001 From: datagutt Date: Sun, 16 Aug 2026 16:14:06 +0200 Subject: [PATCH] fix(audio): let Target Buffer reach the stalls IRL uplinks actually have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 2.5 hour field log at the 120ms default: 287 underruns, max_gap 1711ms, and repeated cycles of buffer runs dry -> concealment -> backlog burst -> drain. Each cycle inflated the audio->OBS offset by about a second, which holds video back by the same amount to keep lip sync: paced went from its usual 2-3 frames to 29 (86MB, ~1s of video) at the spike, then unwound over ~17s at +5%. So a 1.7s uplink stall turned into roughly twenty seconds of disturbed playback, on top of the frames the sender's ABR had already dropped. Riding the stall out instead of concealing it is the only thing that stops that chain, and the cushion could not be set high enough to do it: the setting stopped at 500ms. Raising it alone would have broken the source outright. The read loop's bleed ceiling is min(buffer_max*3, 1000ms) and priming waits for target plus the OBS output lead, so above a ~920ms target the ceiling sits below the prime threshold: the read loop stops before the buffer can ever reach it and playback never starts. Nothing hit it because the setting stopped at 500ms. Floor the ceiling at buffer_max + 100 so it always clears priming, which changes nothing at any target up to ~700ms and makes the rest reachable. Also raise the pacing byte ceiling to 512MB. Pacing holds one buffer's worth of decoded video, so the two settings are coupled: a 2s target at 1080p60 is 120 frames, ~372MB. Past the ceiling frames go out early, which is the desync this branch spent its time removing. Default settings are unaffected — the same log shows 8MB steady. None of this fixes the sender dropping frames under ABR, which is where the freezes in that log start: video arrivals fell to 711 frames per 30s against 900 nominal. It stops the plugin from adding a second of held video and twenty seconds of catch-up on top of every one of them. --- README.md | 2 +- include/irl-source.h | 2 +- src/receiver.c | 19 +++++++++++++++---- src/settings.c | 7 ++++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 932f330..a5dd177 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ A source you just added sizes itself to the canvas when its first frame arrives, |---|---|---| | URL | | Your pull URL. SRT, RTMP, or anything else FFmpeg can open | | Reconnect Delay | 2s | How long to wait between reconnect attempts | -| Target Buffer | 120ms | How much audio cushion to hold. This is your main latency knob: higher rides out a worse connection, lower is snappier and less forgiving | +| Target Buffer | 120ms | How much audio cushion to hold, 20ms to 2s. This is your main latency knob: higher rides out a worse connection, lower is snappier and less forgiving. If the stats show `underruns` climbing, this is the setting to raise — an underrun means the cushion ran dry, and the concealment that covers it delays video by the same amount to keep lip sync | | Adaptive Latency Control | On | Holds latency near your target by nudging playback speed (up to 2% slow, 5% fast) instead of dropping audio | | FFmpeg Options | | Extra options for the stream reader, `key1=val1 key2=val2` style. Use this to set the SRT `latency`, for example | | Hardware Decode | Auto | Let the GPU decode video. Auto picks whatever your machine supports, Off forces the CPU | diff --git a/include/irl-source.h b/include/irl-source.h index 10d3a03..f0c8a2d 100644 --- a/include/irl-source.h +++ b/include/irl-source.h @@ -151,7 +151,7 @@ struct irl_source; * are due, which is exactly the old behaviour, and counted so it is visible. */ #define IRL_VIDEO_PACING_MAX_FRAMES 512 -#define IRL_VIDEO_PACING_MAX_BYTES (192u * 1024u * 1024u) +#define IRL_VIDEO_PACING_MAX_BYTES (512u * 1024u * 1024u) /* Emit rather than sleep again when this close to due: another wakeup costs * more than the timing error it would remove. */ #define IRL_VIDEO_PACING_SLACK_NS 1000000LL diff --git a/src/receiver.c b/src/receiver.c index 18e60df..a1a13c7 100644 --- a/src/receiver.c +++ b/src/receiver.c @@ -83,12 +83,23 @@ void *irl_receiver_thread(void *data) * buffer to drop audible data. */ if (ctx->audio_stream_idx >= 0 && !ctx->config.low_latency_audio) { - int pace_ms = - (int)os_atomic_load_long( - &ctx->config.buffer_max_ms) * - 3; + int buffer_max_ms = (int)os_atomic_load_long( + &ctx->config.buffer_max_ms); + int pace_ms = buffer_max_ms * 3; if (pace_ms > IRL_BLEED_PACE_FILL_MS) pace_ms = IRL_BLEED_PACE_FILL_MS; + /* The flat cap above is an absolute latency guard, but + * it must never fall to where playback cannot prime: + * priming waits for target + the OBS output lead, and a + * ceiling below that stops the read loop before the + * buffer ever reaches it, so the source would sit + * silent forever. buffer_max is target + 200, so this + * floor clears the prime threshold by ~220ms at every + * target. Only binds above a ~700ms target, which is + * why nothing hit it while the setting stopped at + * 500ms. */ + if (pace_ms < buffer_max_ms + 100) + pace_ms = buffer_max_ms + 100; while (os_atomic_load_bool(&ctx->thread_active) && audio_buffer_fill_ms_locked(&ctx->audio_buf) > pace_ms) { diff --git a/src/settings.c b/src/settings.c index b05ebb9..d51b56a 100644 --- a/src/settings.c +++ b/src/settings.c @@ -60,8 +60,13 @@ obs_properties_t *irl_source_get_properties(void *data) /* ── Audio Buffer ──────────────────────────────────── */ + /* Up to 2s: IRL uplinks routinely stall for over a second (a field log + * showed 1.7s gaps with 287 underruns at the 120ms default), and + * riding those out is the only way to avoid the concealment that + * inflates the A/V mapping and holds video back with it. The old 500ms + * limit could not cover them. */ obs_properties_add_int(props, "buffer_target_ms", - obs_module_text("Target Buffer (ms)"), 20, 500, + obs_module_text("Target Buffer (ms)"), 20, 2000, 10); obs_properties_add_bool(props, "adaptive_speed", obs_module_text("Adaptive Latency Control"));