fix(whip): keep a slot's audio format stable across sessions - #758
Draft
wagenet wants to merge 2 commits into
Draft
fix(whip): keep a slot's audio format stable across sessions#758wagenet wants to merge 2 commits into
wagenet wants to merge 2 commits into
Conversation
A WHIP Input slot is built once and stays in the running pipeline while sessions come and go, and caps travel with every sample pushed into appsrc_audio_<slot>. A second publisher on a free slot could therefore hand a running chain a different channel count than the first. Consumers past the slot's tee have already committed: the seat's recorder answers the change with not-negotiated, which travels back up, stops the appsrc's streaming thread and kills the seat's audio. Video keeps arriving, so splitmuxsink blocks on the stopped track and tee backpressure stalls the seat's other branches, the vision-mixer feed included. Pin the format at the slot boundary with a capsfilter after audioconvert/audioresample, so downstream sees one format for the life of the flow and the change is absorbed on the slot's side of the tee. The capsfilter does not name a rate. The sample rate belongs to the seat's downstream graph rather than the slot -- consumers are shared, and the audio mixer settles the whole graph on 44.1 kHz. A build-time rate makes the caps query through audioconvert/audioresample intersect to nothing whenever downstream chose a different one, and then decodebin's audio pad cannot link at all and the seat gets no audio whatsoever. Instead the rate is locked at runtime to the value downstream actually negotiated, on the first caps event, which cannot conflict with downstream by construction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ot idle A slot's decodebin may be built with its state locked so an idle slot cannot hold the pipeline out of PLAYING, and claiming the slot is what releases it. Without that step the chain under test sits in NULL and never sees a buffer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A WHIP Input slot is built once, at flow build time, and stays in the running pipeline while sessions come and go. Caps travel with every sample the session bridge pushes into
appsrc_audio_<slot>, so a second publisher taking a freed slot can hand a running chain a different audio format than the first one used.Everything past the slot's tee has already committed to that first format — the seat's recorder has an AAC encoder and a muxer, and a muxer will not renegotiate mid-file. It answers with
not-negotiated, which travels back up and stops the appsrc's streaming thread. The seat's audio is then dead for good.It does not stay contained to the audio branch. Video keeps arriving,
splitmuxsinkblocks waiting on the stopped track, the recorder's video queue backs up, and tee backpressure stalls the seat's other branches including its vision-mixer feed. If that seat is the only live source, the program output freezes. This was hit by a real participant reconnecting over a tunnel.The fix
A
capsfilterafter each slot'saudioconvert ! audioresample, so downstream sees one audio format for the life of the flow and a publisher's format change is absorbed on the slot's side of the tee, whereaudioconvertcan deal with it.The capsfilter deliberately does not name a sample rate. This is the part worth reviewing, because pinning one is the obvious thing to do and it is wrong:
rate=48000at build time makes the caps query back throughaudioresample/audioconvertintersect to nothing once downstream has settled on something else.decodebin's audio pad then fails to link at all withNoformat, the framework auto-tees the orphaned pad, and the seat gets no audio whatsoever — in either session. I shipped that version first and caught it only on the real rig; the unit test passed happily, because the test's own downstream accepted 48 kHz.format/layout/channelsare safe to pin at build time becauseaudioconvertsits directly upstream and can always produce them from anything. The Mixer block already pins its own boundary format this way, and for the same reason omitsrate.Verification
Regression test —
backend/tests/whip_slot_caps_reuse_test.rs, two cases (channel count, and sample rate) run through the realbuild_whipserversrcslot chain into the recorder's shape (avenc_aac→mp4mux).It asserts the symptom, not the mechanism: how much of the second session's audio actually reaches the muxer. It fails if the fix is reverted — verified by removing the capsfilter and re-running:
With the fix, 99 reach the muxer. Both cases fail on revert, both pass with it.
The test needs no
gst-plugins-rs— the slot chain is plain core GStreamer, sowhipserversrcis not required. Every element it does need is in the CI package list, and CI setsSTROM_REQUIRE_GST_PLUGINS=1, so a missing element fails rather than skipping green.End to end, headless, on the 5-seat meeting rig: publish to a seat with mono audio, SIGINT the publisher, publish to the same seat with stereo. Negotiated caps at the slot boundary and at the recorder, sampled live via
/api/flows/{id}/pad-caps:rate=44100 ch=1rate=44100 ch=2rate=44100 ch=2rate=44100 ch=2Before, the format changes under a committed consumer; after, it does not. Media confirmed flowing in both sessions via
Pad video_0/Pad audio_0, and the runtime lock logs its choice:Tests run: the full backend suite with
STROM_REQUIRE_GST_PLUGINS=1(544 unit tests plus all integration tests, includingpipeline_lifecycle_test) — all pass.cargo clippy --all-targets --features efp,nvidia -D warningsandcargo fmt --checkclean. Nothing skipped.Scope
Audio only, and only the slot boundary. Two things I deliberately did not touch:
splitmuxsinkblocking when one input track stops and WHIP slot-takeover liveness are being handled separately. I saw thequeue_videobacklog in my runs, but its first warning lands during the first session, before any format change, so it is not attributable to this bug and I make no claim about it here.recorder.rsandwhip_session_manager.rsare untouched.🤖 Generated with Claude Code