Skip to content

fix(rtmp): read the live video pipe greedily so the stream drops to live instead of falling behind - #515

Merged
iamfatness merged 1 commit into
mainfrom
fix/rtmp-live-lag-drop-to-live
Sep 14, 2026
Merged

iamfatness merged 1 commit into
mainfrom
fix/rtmp-live-lag-drop-to-live

Conversation

@iamfatness

Copy link
Copy Markdown
Owner

Live incident

A YouTube stream read as "poor / not receiving enough data." The core was healthy (60 fps, 0 dropped), but FFmpeg's stderr showed it falling progressively behind wallclock:

[vist#0:0/rawvideo] Resumed reading at pts 496.8 with rate 1.050 after a lag of 123.061s

The lag grew from 0.7 s to 124 s over ~8 minutes — FFmpeg received 60 fps and pushed only ~47 to the destination, losing ~13 fps continuously. YouTube saw an increasingly delayed, under-rate stream.

Two counters that lie (noted so no one chases them)

  • bytesSent is an estimate (estimatedFrameBytes, hard-assumes 30 fps) — not a real bitrate. The "15.6 Mbps" it implied is fictional.
  • latencyMs = 2100 is a hard-coded constant, not a measurement.

The only real signal was the frame rate: 47/60 reaching the destination.

Cause

-re on the live rawvideo pipe input. That pipe is already realtime (the sender paces writes at the 60 Hz video-output tick), so -re double-throttled it. When the RTMP push couldn't sustain the bitrate, FFmpeg kept reading input at wallclock into its internal buffer while the output drained slower — accumulating unbounded lag instead of staying live.

Fix

Remove -re from the video pipe so FFmpeg reads it greedily. Under backpressure the pipe fills, the sender's write blocks, and AsyncOutputSender's existing newest-wins drops stale frames to stay live — the standard live-encoder behaviour (OBS drops "network" frames the same way). It also gets the first video keyframe + decoder config to the muxer sooner.

Audio -re is deliberately kept (both the real PCM pipe and the anullsrc fallback). Audio is ~256 kbps and never backs up the link, so pacing it costs no live-lag — and pacing it is what preserves the A/V startup ordering that prevents the documented "connects, perfect audio, no video, endpoint closes" failure. aresample=async=1 holds sync while video drops. The asymmetry is commented at both sites so it isn't "fixed" back to symmetric.

Not yet validated live

The audio-ordering interaction only manifests against a real RTMP ingest, so this needs a live soak against YouTube before it's trusted — it has not been run against an endpoint yet. Unit coverage: VideoPipeIsGreedyWhileAudioIsPaced pins the arg shape; all output/sender suites green (RtmpFfmpegArgs 9, supervisor 16, lifecycle 19, AsyncEncoderSink 27).

Not addressed here (environmental)

If the upload genuinely can't sustain the configured bitrate, this makes the stream stay live rather than fall behind, but it will drop frames to do so. The durable answer to sustained under-capacity is a lower target bitrate — separate from this bug.

🤖 Generated with Claude Code

https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ

…ive instead of falling behind

Owner live incident 2026-09-13: a YouTube stream read as "poor / not receiving
enough data." The core was healthy (60fps, 0 dropped), but FFmpeg's stderr showed
it falling progressively behind wallclock — "Resumed reading ... after a lag of
123s", the lag growing 0.7s -> 124s over ~8 minutes. It received 60fps and pushed
only ~47 through to the destination, losing ~13fps continuously.

Cause: `-re` on the LIVE rawvideo pipe input. That pipe is already realtime — the
sender paces writes at the 60Hz video-output tick — so -re double-throttled it.
When the RTMP push could not sustain the bitrate, FFmpeg kept reading input at
wallclock into its internal buffer while the output drained slower, accumulating
unbounded lag rather than staying current. A live encoder must drop to stay live.

Fix: remove -re from the video pipe so FFmpeg reads it greedily. Under
backpressure the pipe fills, the sender's write blocks, and AsyncOutputSender's
existing newest-wins DROPS stale frames to stay live — the standard live-encoder
behaviour. It also gets the first video keyframe + decoder config to the muxer
sooner, so it cannot reintroduce the audio-races-ahead / no-video connection
close that the AUDIO -re guards.

The audio -re is DELIBERATELY KEPT (both the real PCM pipe and the anullsrc
fallback): audio is ~256 kbps and never backs up the link, so pacing it costs no
live-lag, and pacing it is what preserves the A/V startup ordering; aresample=
async=1 holds sync while video drops. The asymmetry is now commented at both
sites so it is not "fixed" back to symmetric.

Not yet validated against a live endpoint — the audio-ordering interaction only
shows against a real RTMP ingest, so this needs a live soak before it is trusted.
Regression test VideoPipeIsGreedyWhileAudioIsPaced pins the arg shape. All
output/sender suites green (RtmpFfmpegArgs 9, supervisor 16, lifecycle 19,
AsyncEncoderSink 27).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
iamfatness added a commit that referenced this pull request Sep 14, 2026
…ist) (#520)

Metrics from a ~5h real test-meeting session (10->16 participants, 8-member wall
at 1080p, streaming to YouTube), read live from /snapshot and the logs. Records
the session numbers and the prioritized list, cross-referencing the issues filed
from it: #515/#514 (P0 in flight), #516/#517/#509 (P1 load), #518/#519 (P2
verify/cleanup), plus the cap-vs-9-camera-on note and the two lying counters.


Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ

Co-authored-by: Claude <noreply@anthropic.com>
@iamfatness
iamfatness merged commit 2fbb6eb into main Sep 14, 2026
19 checks passed
@iamfatness
iamfatness deleted the fix/rtmp-live-lag-drop-to-live branch October 1, 2026 23:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants