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"));