fix(whip): stop slots without a publisher holding the pipeline out of PLAYING - #749
fix(whip): stop slots without a publisher holding the pipeline out of PLAYING#749wagenet wants to merge 3 commits into
Conversation
1a5becd to
74446fe
Compare
… PLAYING A WHIP Input block builds one decode chain per session slot at flow start, whether or not anyone is publishing to that slot. A decodebin cannot complete READY->PAUSED until data arrives and it can typefind, so a slot with no publisher stays ASYNC forever, and a pipeline with any child still ASYNC never completes its own transition. One idle slot therefore held the whole flow one state short of PLAYING. A production with five remote presenter inputs and a single publisher did not run at all: the mixer never composited, the recorders never left READY, and the input that was publishing decoded into a pipeline that was not running. Connecting every input before starting was the only workaround. Each slot's decodebin is now built with its state locked, so an idle slot sits in NULL and contributes nothing to the pipeline's aggregated state, and with async-handling=true, so a slot claimed by a session that then sends no media cannot drag a running pipeline back out of PLAYING either. allocate_slot unlocks a slot's decodebins when a session claims it, during the SDP exchange and well ahead of the first RTP packet. Neither guard hides a real preroll failure: a decodebin that errors still posts its ERROR message to the pipeline bus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
74446fe to
2d306ea
Compare
srperens
left a comment
There was a problem hiding this comment.
Verdict: Request changes — the fix logic is sound, but the new regression
test cannot pass in CI as submitted, and Check (Linux) is red for a
code-caused reason, not a flake.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
Idle decodebins are built locked, with async-handling set, before they can go ASYNC |
CONFIRMED |
backend/src/blocks/builtin/whip.rs:196 — decodebin.set_property("async-handling", true); immediately followed by backend/src/blocks/builtin/whip.rs:197 — decodebin.set_locked_state(true);, called right after each audio/video decodebin is created |
slot_decodebins holds weak refs, not strong refs, into the config struct |
CONFIRMED |
backend/src/whip_session_manager.rs:64 — pub slot_decodebins: Vec<Vec<gst::glib::WeakRef<gst::Element>>>, — matches the CLAUDE.md rule against capturing strong pipeline/element references |
allocate_slot unlocks and syncs a claimed slot's decodebins, tolerating a torn-down pipeline |
CONFIRMED |
backend/src/whip_session_manager.rs:110 — let Some(decodebin) = weak.upgrade() else { guards backend/src/whip_session_manager.rs:114 — decodebin.set_locked_state(false); |
The new test's REQUIRED element list omits nicesrc/nicesink, which WHIPInputBuilder::build needs |
CONFIRMED |
backend/tests/whip_idle_slot_test.rs:21 — const REQUIRED: &[&str] = &[ — the list through "fakesink", has no nice* entry, so plugins_available() reports available when it is not |
| CI's Linux jobs do not install the libnice GStreamer plugin the test needs | CONFIRMED |
.github/workflows/ci.yml:89 — packages: libunwind-dev libssl-dev libcairo2-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav pkg-config cmake libclang-dev — no gstreamer1.0-nice; the identical list repeats at :309. Only the macOS job installs libnice-gstreamer (:574), and macOS build skips on PR |
All three new tests fail (not skip) in CI on a real MissingPlugin panic, unrelated to the fix under test |
CONFIRMED |
CI log for run 33718105912/job 100531459386: WHIP Input block builds: MissingPlugin("WHIP Input needs WebRTC ICE support, but GStreamer has no nicesrc or nicesink..."), raised from the pre-existing, untouched backend/src/gst/ice_preflight.rs:80 — Err(BlockBuildError::MissingPlugin(ice_missing_message( |
release_slot does not re-lock a slot's decodebins when a session disconnects |
CONFIRMED, non-blocking |
The diff for backend/src/whip_session_manager.rs has no hunk touching release_slot; async-handling set once at construction (prepare_idle_decodebin) is permanent, so a reused slot that goes idle again is still protected against pulling the pipeline out of PLAYING — locked-state was only ever the first-claim guard |
Diagnosis — The production fix matches the failure mode in the PR body:
locking state at construction keeps an unclaimed decodebin out of the
pipeline's aggregate state, and async-handling=true stops a claimed-but-fed-
nothing decodebin from re-entering ASYNC and stalling PLAYING later. Both
guards are real and the test intends to exercise both moments. But the test
never gets that far: build_whip_inputs calls WHIPInputBuilder.build, which
runs the pre-existing require_ice_elements preflight (unrelated to this
diff) and returns MissingPlugin on a CI image with no libnice plugin — a gap
CLAUDE.md calls out directly: check ci.yml's package list before relying on
an element, and add anything missing in the same PR. This PR didn't, so all
three tests fail 100% of the time in Check (Linux), the only job that runs
them.
Radius — LOCAL. The production change is confined to whip.rs and
whip_session_manager.rs, uses WeakRef correctly, and touches no shared
types, endpoints, or StromEvent variants. The blast radius of the finding is
that merging as-is leaves Check (Linux) permanently red on main.
Tests & CI — Check (Linux) fails: all three new tests in
whip_idle_slot_test.rs panic in build_whip_inputs before ever reaching
prepare_idle_decodebin/activate_slot_decoders. Build (Linux x86_64/ARM64), Check & Build (WASM), API Contract Check, sccache preflight are green; Build (macOS)/Build (Windows) skip as usual. Fix:
add gstreamer1.0-nice to both apt package lists (ci.yml:89 and :309) and
nicesrc/nicesink to the test's REQUIRED list, in the same PR.
Repo rules — WeakRef used correctly per the pipeline-object-in-closures
rule. No BUFFER probe. The test-package-list rule (Tests section of
CLAUDE.md) is the one this PR misses.
Confidence: HIGH
whip_idle_slot_test needs nicesrc/nicesink for WHIP's webrtcbin. The Linux jobs had every other GStreamer plugin package but this one; macOS already installs libnice-gstreamer. Add it to check-linux and both Linux build jobs, which must share an identical package list for the apt cache key. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
srperens
left a comment
There was a problem hiding this comment.
This supersedes my review of 2d306ea — the verdict does not stand. That review's sole blocker was Check (Linux) failing 100% of the time because CI had no libnice; 05b1456 adds gstreamer1.0-nice to the apt lists and the job is now green with the new tests actually executing. No backend code changed between the two heads.
Verdict: Comment — CI is fixed and the production logic still holds, but half of the previously requested change is still missing: the test's own REQUIRED list doesn't declare the ICE elements it transitively needs, so it would silently mis-report availability if gstreamer1.0-nice disappears from the package list again.
Requested changes
- Add
"nicesrc","nicesink"toREQUIREDinbackend/tests/whip_idle_slot_test.rs:21-33, soplugins_available()andSTROM_REQUIRE_GST_PLUGINSactually cover the dependencyWHIPInputBuilder::buildpulls in.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
Check (Linux) now installs libnice and the 3 new tests execute for real, not skip |
CONFIRMED |
gh pr checks 749: Check (Linux) pass at 05b1456; job log — test idle_whip_slots_do_not_block_playing ... ok, allocated_slot_decodes_incoming_media ... ok, allocated_slot_without_media_does_not_stall_a_running_pipeline ... ok |
Test's REQUIRED list declares every element the test's own code path needs |
CONTRADICTED |
backend/tests/whip_idle_slot_test.rs:21 — const REQUIRED: &[&str] = &[ through :33 has no nice* entry, but backend/src/blocks/builtin/whip.rs:78 — `ice_preflight::require_ice_elements("WHIP Input")?;` hard-gates the same build path the test drives |
Idle decodebins are built locked, with async-handling set, before they can go ASYNC |
CONFIRMED |
backend/src/blocks/builtin/whip.rs:196 — `decodebin.set_property("async-handling", true);` and backend/src/blocks/builtin/whip.rs:197 — `decodebin.set_locked_state(true);` |
allocate_slot unlocks and syncs a claimed slot's decodebins, tolerating a torn-down pipeline |
CONFIRMED |
backend/src/whip_session_manager.rs:110 — `let Some(decodebin) = weak.upgrade() else {` guards backend/src/whip_session_manager.rs:114 — `decodebin.set_locked_state(false);` |
Diagnosis — Unchanged from my prior review: locking state at construction keeps an unclaimed decodebin out of the pipeline's aggregate state, async-handling=true stops a claimed-but-fed-nothing decodebin from re-entering ASYNC later, and the tests exercise both moments against the real WHIPInputBuilder. Coverage is BOUNDED to WHIP Input's pre-allocated slots — the PR body itself names the same-symptom recorder case as out of scope, covered separately in #750.
Radius — LOCAL: confined to whip.rs/whip_session_manager.rs, WeakRef used correctly per the pipeline-closure rule, no strom-types or StromEvent surface touched. Grepped other decodebin builders (aes67.rs, mpegtssrt_input.rs, whep.rs) — none pre-allocate idle per-slot decodebins the way WHIP Input does, so the fix's shape doesn't generalize to them.
Tests & CI — Check (Linux), Build (Linux x86_64/ARM64), Check & Build (WASM), API Contract Check, sccache preflight all pass at 05b1456; Build (macOS)/Build (Windows) skip as usual. The REQUIRED-list gap above doesn't fail CI today — it's a latent gap in how the test reports its own dependencies, not a current red check.
Confidence: HIGH
Superseded by re-review at 05b1456: the CI-red blocker (missing gstreamer1.0-nice) is fixed, verdict no longer stands. Dismissing per protocol — the reason is the weaker/stale review, not disagreement over its original findings.
…nction The new helper was inserted between build_whipserversrc's doc comment and the function, so the whole block documented the helper and build_whipserversrc was left undocumented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
srperens
left a comment
There was a problem hiding this comment.
Head moved to 461a1bc since my 05b1456 review. The only change (git diff 05b1456..461a1bc -- backend/src/blocks/builtin/whip.rs) moves prepare_idle_decodebin's doc comment onto its own function — no logic changed. Verdict stands: Comment. The one requested change from my last review is still open — backend/tests/whip_idle_slot_test.rs:21 — const REQUIRED: &[&str] = &[ still doesn't list nicesrc/nicesink, so the test would silently mis-report availability if gstreamer1.0-nice disappears from ci.yml again. CI is green at 461a1bc (Check (Linux), Build (Linux x86_64/ARM64), Check & Build (WASM), API Contract Check, sccache preflight; macOS/Windows skip as usual).
Confidence: HIGH
The stall
A WHIP Input block builds one decode chain per session slot at flow start —
appsrc -> decodebin -> convert -> tee— whether or not anyone is publishing to that slot. Adecodebincannot complete READY->PAUSED until data arrives and it can typefind, and a pipeline with any child still ASYNC never completes its own transition.A single slot with no publisher therefore held the whole flow one state short of PLAYING. In a production with five remote presenter inputs and one publisher connected, the mixer never composited, the recorders never left READY, and the input that was publishing decoded into a pipeline that was not running. Under
GST_DEBUG=GST_STATES:5all ten decodebins (5 inputs x video+audio) sat atcurrent READY pending PAUSED, desired next PLAYING.The fix
Each slot's
decodebingets two guards:async-handling=truemakes it absorb its own ASYNC once unlocked, so a slot claimed by a session that then sends no media — an abandoned negotiation, a publisher whose video never negotiates — cannot pull a running pipeline back out of PLAYING.WhipEndpointConfig::allocate_slotunlocks a slot's decodebins when a session claims it, during the SDP exchange and well ahead of the first RTP packet. Unlocking is idempotent, so a slot reused by a later session re-syncs a decodebin that is already running.Neither guard hides a real preroll failure: a
decodebinthat errors still posts its ERROR to the pipeline bus.Verification
Headless backend, five WHIP inputs, one publisher on
p1:gst_statefromGET /api/flows/{id}Paused, indefinitelyPlayingGST_STATES:5on the fixed run shows exactly two decodebins unlocking, at the moment slot 0 is allocated; the other eight stay locked in NULL.Tests
backend/tests/whip_idle_slot_test.rsbuilds real WHIP Input blocks throughWHIPInputBuilderand wires up the internal links they declare, rather than reconstructing the topology by hand:idle_whip_slots_do_not_block_playing— five inputs, nobody publishing, pipeline must reach PLAYING.allocated_slot_decodes_incoming_media— a slot claimed throughallocate_slot, then fed real H.264, must produce decoded frames at that input's output tee. Without it, locking every decodebin for good would satisfy the first test and break WHIP input entirely.allocated_slot_without_media_does_not_stall_a_running_pipeline— covers theasync-handlingguard.Removing both guards fails all three; removing only
async-handlingfails the two allocation tests.Every element the tests need (
appsrc,decodebin,videoconvert,audioconvert,audioresample,tee,videotestsrc,x264enc,h264parse,appsink,fakesink) is in the CI package list, and they honourSTROM_REQUIRE_GST_PLUGINSso a missing element fails rather than skips.Ran locally on macOS: the three new tests, plus
pipeline_lifecycle_test,jitterbuffer_mute_test,failed_start_teardown_test,pipeline_start_failure_testandopenapi_test.Not fixed here: recorders with no data
The same flow has a second, independent cause of the same symptom. With the WHIP slots fixed, a flow with a recorder per input still reports
Paused: thesplitmuxsinkof every recorder whose input is not publishing sits at READY with no data to preroll, and sinks hold the pipeline by design.That needs its own change in the recorder.
async-handling=trueonsplitmuxsinklets media flow (the fed recorder grew to 5.8 MB) but leaves the pipeline reportingPaused, so the recorder needs the same locked-until-data treatment rather than a one-line property.The A/B above therefore used a flow with a recorder on the publishing input only. Against a flow with a recorder on every input, this branch still shows
Pausedfrom that second cause.🤖 Generated with Claude Code