Repository navigation
fix(whip): consolidate the WHIP Input fixes from #759, #893, #787, #901 and #943 - #957
Merged
Merged
Conversation
…x sees it A WHIP session's appsink -> appsrc bridge forwarded buffers with no PTS. A recorder downstream answers one with "Buffer has no PTS" from qtmux and GST_FLOW_ERROR, which takes the whole flow down: on a live rig, a second participant joining killed the first. The slot bridge now goes through gst::pipeline_bridge::SessionBridge: a buffer with no PTS is dropped and counted (warned on the first and every hundredth), the shared A/V offset is set once per session with compare_exchange and taken from the first stamped buffer, DTS is shifted with PTS, and a sample without caps no longer panics. From #759 (cfc4518, f62e777, 9ba9941), adapted to main: - The slot callback keeps the session_finished gate from #753 and the ingress liveness stamp from #756, wrapped in install_slot_bridge so the wiring test drives the real callback. - The Media Player half of #759 is left out. #945 replaced that bridge's timing with its own, so SessionBridge no longer fits there. Co-Authored-By: Per Enstedt <per.enstedt@eyevinn.se> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A WHIP publisher joining a slot can take the whole flow down with a GPU vision mixer downstream: "Need timestamped buffers!" from videoaggregator. The session's jitterbuffer can release a run of packets from different frames on one PTS; h264parse takes a PTS only when it differs from the previous one, so the later frames of the run leave it with none, and a browser stream has no framerate to derive one from. A video packet that starts a new RTP frame and whose PTS is at most 1 ms behind the previous frame's now gets that PTS plus 1 ns. Packets of one frame keep sharing a PTS, and any other PTS (a B-frame, a step in the sender's clock, a frame far ahead) passes through. On the rig, 5 of 8 runs of publish / switch camera / leave / rejoin died before, 0 of 8 after. From #893 (745ce02, e90f52b, 2540522), adapted to the SessionBridge from #759: the ordering runs on the session PTS before the bridge rebases it, and copies the buffer only when the PTS moves. Co-Authored-By: Per Enstedt <per.enstedt@eyevinn.se> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A WHIP session pipeline is already PLAYING when whipserversrc adds its pads, and each pad gets a `tee -> fakesink + appsink` branch synced into it. The appsink ran with `GstBaseSink`'s default `async=true`, so it answered that state change with ASYNC and waited for a preroll. The pads arrive one after the other within a millisecond, so the second branch lands while the first is still waiting, and the second is left below PLAYING: it takes a single buffer and then blocks its streaming thread for good. That seat's audio or video then never reaches the slot's appsrc, so its decodebin exposes no pad, and the flow's recorder and mixer wait on a track that will never arrive. Nothing in the log says so — `sync_state_with_parent` reports the state change as accepted either way. Turn `async` off, as the fakesink beside it already does. Measured on a five-seat meeting rig, 18 fresh-server trials per arm: 7/18 seats failed to start before, 0/18 after. The branch construction moves into `attach_session_branch` so the regression test builds the real thing rather than a copy of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The thumbnail branch is attached to a flow pipeline that is already running, and its appsink ran with GstBaseSink's default async=true, so it answered that state change with ASYNC and waited for a preroll. The activation guard admits the pipeline in PAUSED with PLAYING pending, which is that wait, and a branch attached inside one is left below PLAYING: it takes a single buffer and blocks its streaming thread. Activation returns without waiting for a preroll, and a branch is created per block from an HTTP request, so a client asking for several blocks' thumbnails serializes on one mutex but not on the async cycles behind it. Same defect and same fix as the WHIP session branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A WHIP Input slot keeps its decode chain while publishers come and go. The video decoder that decodebin plugged for the first session kept that session's reference frames, reorder queue and configuration, and a later session with equal caps sends none to reconfigure it. On macOS, VideoToolbox then decoded a second Safari publisher at a few frames per second and flagged every frame of a third as an error, so the slot showed no video while its audio played. When a session claims a slot whose video decodebin has already run, restart that decodebin from an IDLE probe on the slot appsrc: unlink, set it to NULL, relink so the sticky events are sent again, and sync it with the pipeline. The existing pad-added handler links the new chain when data arrives. Audio keeps its running chain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test reuses a slot with a VP8 publisher after an H.264 one and asserts the VP8 session decodes. With the restart disabled, the kept decoder refuses the VP8 caps and the slot's source stops with not-negotiated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A WHIP session asked its publisher for a keyframe only until video first decoded. After that, nothing asked again. A browser sends a keyframe only when asked, so a loss the jitterbuffer could not repair left the slot's decoder broken for the rest of the session: smeared pictures, or every frame rejected with an error flag, until the watchdog tore the session down and the guest rejoined. The slot's decodebin now reports damage through a per-slot flag: - a gap: the depayloader marks the first access unit after a sequence gap DISCONT; damaged until a keyframe with no gap in front of it leaves the depayloader (a repair keyframe can lose a packet too). - a silent decoder: data lost with no gap (the depayloader drops a fragment it cannot place) makes the decoder reject every frame without a bus message. 30 access units in with no picture out counts as damage, cleared when pictures come out again. Both are buffer probes that touch atomics only. A per-session thread on the session side reads the flag and sends the keyframe request on the whipserversrc pad, where it reaches the publisher: at once, then at most once a second, 10 times per damage episode, and never on healthy video. Each episode logs its recovery time and request count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keyframe requester stops once the slot's video_decoding flag is set, and that flag is set from decodebin's pad-added. Before the decoder restart on reuse, pad-added fired once per flow, so every reconnecting publisher was asked for the full request budget while its video decoded fine (#805). The restart brings pad-added back for each session. The reuse test now re-arms the flag as a new session does and asserts the second session sets it again. With the restart disabled it fails with "the second session decoded, but the slot never reported its video as decoding". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 2, 2026
srperens
added a commit
that referenced
this pull request
Oct 2, 2026
* fix(mediaplayer): drop a buffer with no PTS at the bridge The Media Player's internal -> main pipeline bridge forwarded a buffer with no PTS as it was. A recorder downstream answers one with "Buffer has no PTS" from qtmux and GST_FLOW_ERROR, which takes the whole flow down. It is now dropped and counted, with a warning on the first and every hundredth. The same fix for WHIP Input is in #957; #759 carried both, but #945 gave this bridge its own timing since, so it is fixed here on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mediaplayer): place a DTS-only buffer by its DTS instead of dropping it In passthrough the bridge carries encoded data, where a frame with a DTS and no PTS is legal: an MPEG-TS PES between PTS stamps, or a parser that cannot order B-frames. Dropping those broke the stream for every consumer. Such a buffer is now placed by its DTS and leaves without a PTS; only a buffer with neither is dropped. The test strips the PTS that appsrc gives a DTS-only buffer, so the bridge really sees one. It fails with the earlier drop-without-PTS rule (the DTS-only buffer is missing) and with the original forward-everything rule (the unstamped buffer arrives). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wagenet
added a commit
to wagenet/strom
that referenced
this pull request
Oct 2, 2026
Places the video damage probe, keyframe recovery logging, video PTS ordering and SlotDecodebin items that Eyevinn#957 added to whip.rs and the session manager. Rewrites the pipeline_bridge.rs comment that named whip.rs. A file-level `use super::` import is now written as an absolute path, since the new submodules sit one level below the source file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 5, 2026
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.
Refs #759, #893, #787, #901, #943, #805, #792, #906, #914
Problem
Seven open PRs by @wagenet fix WHIP Input, and nearly all of them change the same two files (
whip.rs,whip_session_manager.rs). Each merge made the rest conflict harder: after #756 and #763 landed, #759 needed its logic merged with main's, not just its text. This PR takes the WHIP Input fixes as one series, evaluated and tested against current main.Each fix below was reproduced by a test that fails with the fix removed, and passes with it.
Change
Commits keep @wagenet as author where the code is his, in dependency order:
fix(whip): stop an unstamped buffer at the session bridgeqtmuxansweredBuffer has no PTSand took the whole flow down. Now dropped and counted ingst::pipeline_bridge::SessionBridge, with the shared A/V offset set once per session (compare_exchange).fix(whip): keep a slot's video PTS moving forwardh264parsestripped the PTS from all but the first, and the GPU vision mixer failed the flow (Need timestamped buffers!). A new frame at most 1 ms behind the previous one is moved 1 ns past it.fix(whip): stop a session's second stream stalling below PLAYINGasync=true; the second pad's branch, added during the first one's preroll, stayed in READY and blocked for good. Rig: 7/18 frozen before, 0/18 after.fix(thumbnail): turn off async on the thumbnail tap appsinkfix(whip): give a reused slot a fresh video decoder(+ test)not-negotiated, and Safari reuses decoded slowly or not at all on VideoToolbox. The videodecodebinis restarted from an IDLE probe on reuse.fix(whip): ask for a keyframe when a running session's video breakstest(whip): guard that a reused slot reports its video as decodingAdapted, not taken as is:
session_finishedgate and fix(whip): judge a session by the media it produces, not what arrives #756's ingress liveness stamp.install_slot_bridgewraps that callback so the wiring test drives the real one. The Media Player half is left out: feat(mediaplayer): play live streams, extra tracks and URLs #945 replaced that bridge's timing with its own, soSessionBridgeno longer fits there.SessionBridgerebases it, and copies the buffer only when the PTS moves. Same result, since the offset is constant.Left out:
decodebinbringspad-addedback for each session, sovideo_decodingis set again and the requester stops. The last commit asserts it; with the restart disabled it fails with "the second session decoded, but the slot never reported its video as decoding". fix(whip): stop asking a reconnecting publisher for needless keyframes #805's stamp-based stop also had the takeover weakness its body describes.Evidence
Each regression test was run with its fix removed:
push_samplean_unstamped_buffer_never_reaches_the_slot_appsrc: "the unstamped buffer must not be here"session_finishedgate removeda_finished_session_stops_feeding_its_slotvideo_frames_on_one_pts_reach_the_slot_in_ordervideo_b_frames_keep_their_pts,video_frame_far_ahead_does_not_hold_back_later_framesasync(false)removedboth_session_branches_keep_running_when_attached_in_successionreused_slot_decodes_through_a_fresh_video_decoder,reused_slot_accepts_a_different_codec(not-negotiated), and the decoding-flag assertiona_lost_packet_damages_the_slot_until_the_next_keyframe,a_keyframe_that_lost_a_packet_does_not_repair_the_slotRig results quoted in the source PRs (7/18 → 0/18 for #787, 5/8 → 0/8 for #893, the #943 loss table) are from @wagenet's rig on the original branches and were not re-run on this branch.
Unverified: the Safari/VideoToolbox decoder reuse failure (#901) cannot run in CI; the fresh-decoder assertion stands in for it.
Tests
Ran (macOS, GStreamer 1.28,
STROM_REQUIRE_GST_PLUGINS=1for the WHIP tests):cargo test --workspace --no-fail-fast(1106 passed, 0 failed),cargo clippy --all-targets -- -D warnings,cargo fmt --check.The new integration tests need
x264enc,vp8enc,rtph264pay/depayand an H.264 decoder, all in CI's package list, and CI setsSTROM_REQUIRE_GST_PLUGINS=1, so a missing element fails instead of skipping.Not run: Linux, Windows, the WASM clippy. A rig run with real browsers follows on the integration branch.
For the reviewer
Out of scope
🤖 Generated with Claude Code