Repository navigation
Conversation
b3326be to
cd9b383
Compare
|
Noted as a draft, so I am holding off on a full review until it is marked ready for |
0896d8c to
f0aa999
Compare
fafe2bd to
ddff280
Compare
a47cb11 to
641cd70
Compare
|
Written by Claude (AI). Update this PR once #809 lands. #809 adds a per-DSK
|
srperens
left a comment
There was a problem hiding this comment.
Verdict: Request changes — the mechanism is sound and unusually well argued, but the wall-clock fallback path releases the mixer hold in the wrong order, and the one test covering the GL compositing path cannot run in this repo's CI.
Requested changes
-
Drop the release sender before removing the probe in the fallback path —
backend/src/state/stinger.rs, the_ =>arm at line 501: insertdrop(release_tx);ahead ofpad.remove_probe(probe);. As written the sender stays live until the function returns, so if the probe fires as the timeout expires it blocks inrecv_timeoutwhileremove_probewaits on it — holding the program output, and a tokio worker thread, for the full 250 ms. The comment directly above already states the intended order. -
Either install a GL stack in
.github/workflows/ci.yml(gstreamer1.0-glplus a software rasterizer) soalpha_survives_gl_videomixerexecutes, or drop it and say in the PR body that the GL alpha path is verified by hand only. CLAUDE.md: a test that skips on a missing element "passes green and guards nothing".
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| Every buffer before the target costs one timestamp compare | CONFIRMED |
backend/src/state/stinger.rs:463 — if pts.nseconds() < hold_from_pts short-circuits the || before fired.swap, and the lock/channel at :472 are past that return |
| Dropping the sender keeps the probe from holding the mixer on the fallback path | CONTRADICTED |
backend/src/state/stinger.rs:504 — pad.remove_probe(probe); runs while release_tx is still in scope; it is only dropped at end of function |
| The stinger tests run in CI | CONFIRMED for stinger_test |
backend/tests/stinger_test.rs:389 — panic!("{name} missing — CI installs gst-libav and plugins-good"); — it fails rather than skips |
| The alpha tests run in CI | CONTRADICTED |
backend/tests/stinger_alpha_test.rs:215 — eprintln!("skipping: no usable GL environment"); and .github/workflows/ci.yml:104-107 installs no gstreamer1.0-gl or mesa |
remove_probe blocks on a running callback |
EXTERNAL |
Documented GStreamer behaviour; assumption stated, not settleable here |
Diagnosis — Anchoring the cut to the clip's stream offset rather than a wall-clock delay is the right layer: it removes the race with the aggregator instead of timing around it. Coverage is BOUNDED — the hold only applies where the mixer reaches the cut frame within the grace window, and the PR says so and degrades to wall clock. The BUFFER probe respects the CLAUDE.md constraints on the hot path; the blocking, allocation and formatting all sit past the one-shot guard, which is the correct shape for this.
Radius — SHARED. set_dsk_hold_last_frame, mixer_src_pad and mixer_frame_duration_ns are purely additive with no existing call sites, and the last-frame-hold change is gated on opt-in declaration. What is shared is the probe: while armed it sits on the mixer's program output pad, so every take on that mixer pays the compare until it removes itself. A failure would show up as a stalled program output for up to 250 ms, which is change 1.
Tests & CI — Check (Linux), Build (Linux x86_64/ARM64), Check & Build (WASM), API Contract Check all green at 641cd70. Build (macOS) and Build (Windows) report skipping; there is no platform-specific code in this diff, so Linux-green is adequate cover here. The PR body states that a_repeat_stinger_plays_forward_from_the_start does not fail when its fix is reverted — the disclosure CLAUDE.md asks for, taken at face value.
Design record — backend/src/state/mod.rs is 3082 lines after the split that moved stinger state out; worth carving down further, but not this PR's job.
Confidence: HIGH
641cd70 to
ed84f67
Compare
|
Written by Claude (AI). Both addressed in ed84f67, rebased on main. 1. Release order. Fixed: the fallback arm drops 2. GL test in CI. I took the route the repo already uses rather than installing a GL stack. The macOS job can't run on this PR without the The PR body previously claimed this test executed on Linux CI. It did not; the body now says what each guard covers. Also caught in the rebase: main added a #809 is still open, so the premultiplied refusal stays for now. |
srperens
left a comment
There was a problem hiding this comment.
Verdict: Comment — supersedes my Request changes of 2026-09-16 (review-5223777646, head 641cd708), which no longer stands: both requested changes are in. What holds it short of Approve is one row, and it is the same GL-coverage gap on a different file.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| The probe-teardown race I asked about is fixed | CONFIRMED |
backend/src/state/stinger.rs:506 — drop(release_tx); now precedes backend/src/state/stinger.rs:507 — pad.remove_probe(probe);, so a probe that fired as the wait expired is released before remove_probe waits on it |
| The alpha test no longer skips silently | CONFIRMED |
backend/tests/stinger_alpha_test.rs:244 — strom_types::env::var_opt("STROM_REQUIRE_GL").is_none(), — adopts the precedent already on main in vision_mixer_fx_test.rs and shader_validation_test.rs |
| The four alpha tests executed in this repo's CI | UNVERIFIED |
.github/workflows/ci.yml:645 — STROM_REQUIRE_GL: 1 is set on the macOS job alone, and Build (macOS) is skipping on this PR. In Check (Linux) job 105241909348 the binary reports 4 passed ... finished in 0.09s — four GL pipelines cannot negotiate and reach EOS in 90 ms, so all four skipped |
| The core stinger logic is genuinely covered | CONFIRMED |
same job: stinger_test.rs reports 12 passed; 0 failed ... finished in 18.87s |
Diagnosis — The cut is anchored to the clip's own stream offset with a one-shot BUFFER probe, and the wall-clock arm is the fallback rather than the design. Coverage of the anchoring mechanism is ABSOLUTE; coverage of the alpha half is BOUNDED to whatever a macOS run would exercise, which is currently nothing.
Repo rules — The BUFFER probe at backend/src/state/stinger.rs:459 respects the hot-path rules: the per-buffer cost is a pts compare plus an AtomicBool swap, and every lock, channel and format sits past that guard, running once per take. The closure captures no Pipeline, Element or Bin — only channels, an atomic and a pre-built String — so it cannot form the CLAUDE.md reference cycle. No new endpoint: this extends the existing transition route with one additive Option<String> field, and API Contract Check passed. The three new StromEvent variants are additions, not modifications.
Radius — SHARED: the probe is installed on the vision mixer's src pad, so a take holds the program output for up to 250 ms (STINGER_MAX_HOLD, backend/src/state/stinger.rs:46). That bound is enforced by recv_timeout, and the fallback arm now releases before removing, so the failure mode is a late cut rather than a held mixer.
What I would ask for before merge — the ci:macos label on this PR, which is the only route that sets STROM_REQUIRE_GL and therefore the only way the four alpha tests have ever run against this diff. I am deliberately not applying it: it fires a paid run, which is a maintainer's call. Merging on Linux-green and letting the post-merge macOS run cover the alpha half is a legitimate alternative — but it is a choice worth making deliberately, not by default.
Confidence: HIGH
Superseded by my review at head ed84f67: both requested changes are in, so this CHANGES_REQUESTED sits at a dead head and misreports the PR.
|
Claude (AI), on behalf of @wagenet. Rebased onto main. Conflicts and what changed beyond picking both sides:
Ran on macOS with |
A stinger plays a keyed clip over the program while another transition runs beneath it. It is a transition of type "stinger" naming a Media Player block that declares itself a stinger clip source and feeds a keyed (DSK) input. Declaration is opt-in, so a looping graphic on a keyed input is left alone. The clip is held on its first frame while idle, so a take only starts playback: ~0.5 ms at any resolution, against up to 30.9 ms at 4K unparked. Cut point, the transition beneath and its duration are block properties, since where a clip covers follows from the artwork. The duration beneath is shortened to fit the clip, and a clip that cannot play still runs the transition beneath. The cut is anchored to the clip: its stream offset gives the output frame containing the cut point, and the mixer's output is held on the frame before it until the take is applied. Setting pad properties from another thread otherwise races the aggregator, by a different amount per layout. Keyed inputs fed by a declared source do not hold their last frame, or the frame a clip ended on would be composited when the input is next revealed. A take's claim on the mixer carries a token, so a take whose flow was stopped cannot act on the pipeline that replaced it. Straight alpha only: premultiplied alpha cannot be read from the file and no compositor operator corrects it, so a binding that declares it is refused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a starved process the clip occasionally misses an output frame, and a stinger's keyed pad does not repeat its last frame, so that frame carries no clip pixels. When it was the frame the program changed on, the coverage-to-frame arithmetic underflowed and panicked. Such a take now counts as unreadable instead. At most half the takes may be, so a clip that never reaches the program at the cut, which would be unreadable on every take, still fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A repeat stinger fired right after the previous one completed put that clip's last frame on air before its own first frame: one frame at normal load, two or three on a starved machine. The keyed pad is set not to repeat its last frame, but videoaggregator only expires that frame on an output frame where the pad has nothing queued. The next take queues its first frame ahead of time, so the stale frame stays current until the new one is due, and revealing the keyed input shows it. A finished stinger now waits for the mixer's output to pass the clip's end before re-arming and releasing the mixer. The wait ends early if the take is superseded, and gives up after the mixer's latency plus 500 ms. The repeat test fires the second take as soon as the first completes, keeps every program frame, and judges frames by timestamp against the mixer position at the take, so a stale frame cannot pass as one the appsink held from before it. Frames with no clip on them, a late clip frame on a starved machine, are skipped rather than read as coverage falling. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a loaded machine the mixer's output trails the clock. A take gave up on the anchor at cut point + 500 ms of wall clock and applied the cut to whatever frame the mixer was on, often before the clip was on air, so the program switched with nothing covering it. A take now waits for the mixer to reach the cut frame however late that is, giving up only if the output stops advancing for 5 s. The keyed input comes off air once the mixer passes the clip's end rather than on a wall-clock timer, so a clip that started late plays out. A cut the clip may not cover is reported as StingerFailed with still_running true: the clip never reached the mixer (the keyed input is hidden and the cut made plainly), the mixer stalled, or the clip's frame for the cut point had not reached the mixer when the cut was released. The last is read from a per-take probe on the keyed pad that records the latest clip frame to arrive. A stinger's keyed input now repeats its last frame for two mixer frames instead of none, so a single late clip frame freezes the graphic rather than blanking it. The cut test no longer absorbs a cut with no clip on it: a take either reports, or must cut on the named frame (or read one early). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Media Player now paces every buffer through clocksync and holds it for playout_delay_ms (500 by default, Eyevinn#945) before it reaches the flow. Both break a stinger clip. The delay: a clip is parked on a decoded first frame so a take goes to air at once. With the delay the take waited half a second, and the parked frame showed for a few frames and blinked out before the rest of the clip arrived. a_repeat_stinger_plays_forward_from_the_start fails on that: 11 blank frames between the first clip frame and the rest. The pacing: each frame reached the mixer exactly when it was due, so when the mixer was held on the frame before the cut, the clip frame carrying the cut point was often not there yet. Measured at that check over 24 takes, the clip led the cut frame by -26 to +2 ms; unpaced it leads by 133 to 166 ms. the_cut_lands_on_the_frame_the_cut_point_names reported most takes as uncovered, on every Linux CI run and on 6 of 8 local runs. A player declared as a stinger clip source now ignores sync and playout_delay_ms. Its frames keep their spacing from the first one after a take and wait in the mixer's input queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2551084 to
cc831ee
Compare
An unpaced stinger clip runs ahead of the mixer and fills the Media Player's video appsrc, which is leaky: once 20 MB is queued it drops every new buffer. A 1080p BGRA frame is about 8 MB, so a 1080p clip put about a third of its frames on air and most cuts were reported as uncovered; a 320x180 clip longer than about 5 s lost frames with no report. A declared stinger source's video appsrc is now non-leaky and blocking, so the clip waits for the mixer instead. Video only: a take needs the clip wired to the mixer, which always consumes. An audio output may be left unlinked, and a blocking audio appsrc that never drains stalls the player, and with it the next take's re-arm, for good. Audio stays leaky. a_clip_longer_than_the_output_queue_plays_whole plays a 240-frame clip and fails, with 160 of 240 frames, when the video appsrc is leaky. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The whole-clip guard reports how many frame times the arrived frames span, so a failure shows whether frames were lost at the ends of the clip or in between. Reverting the blocking video appsrc now reads 157 frames spanning 157: the tail is lost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stinger clip's video appsrc waits for room, which parks the Media Player's internal streaming thread in push_sample while the flow takes nothing from it: a keyed input the mixer stopped pulling, or a clip played before its video is wired. Pausing, seeking, switching file and shutting down the internal pipeline all wait for that thread, so they never returned, and neither did stopping the flow. Each of them now lifts the waiting appsrc's limits first. The parked push goes through and frees the thread; the limits come back when the call ends. Changing max-bytes is what wakes the waiting push. a_clip_the_mixer_stops_taking_can_still_be_stopped and an_unwired_stinger_clip_can_still_be_stopped fail without the lift: "stopping the player must return while the mixer takes nothing" and "seeking a clip nothing takes must return". They run on a runtime that is abandoned rather than joined, so a hung take reports as a failure instead of a test that never ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A take re-arms its clip from its own task while an operator may pause, seek or stop it, and the API does not serialize player calls. Each call saved the video appsrc limits it found and put them back when done, so a call that started inside another saved the lifted values (no limit) and could restore them last. The clip's video queue then stayed unbounded until the flow restarted: an unpaced clip decoded whole into memory every take (630 MB for 3 s at 1080p). The first call in now saves the limits and the last one out restores them, under one lock, so no call puts them back while another still waits on the streaming thread. overlapping_player_calls_keep_the_clip_queue_bounded races 20 re-arms against 20 seeks and fails with "the clip's video queue limits must be back as built" when each call restores for itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a_clip_longer_than_the_output_queue_plays_whole failed once in CI with 239 of 240 frames arriving in an unbroken run, so an end frame was missing at the moment the take completed. The failure message now also gives the count 500 ms later and any StingerFailed the take reported, which tells a frame the mixer moved past apart from one never sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a_clip_longer_than_the_output_queue_plays_whole keeps finding 239 of 240 clip frames at the keyed pad in CI, still 239 half a second after the take completes, with no StingerFailed. The test now also counts frames where the media player hands them to the flow, and how many reached the keyed pad before the take. The failure message gives both counts and the frame range each saw, which tells a frame the player never sent from one lost on the way or one that passed before the take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@wagenet, thank you for this PR. It shaped #1010, which we have just opened: feat(vision-mixer): stinger transitions: classic, track matte and mask only. That PR takes several of your ideas:
It also goes further, and we propose to go with it instead of this PR:
Could you test #1010 with your clips and setup? Mixed results, odd formats, and anything this PR handled that #1010 does not, would all be useful to hear about. If #1010 covers what you need, we would close this PR in its favour, with credit to you; it already credits you in the PR description and the commit. 🤖 Generated with Claude Code |
…k only A stinger plays a clip over the program while the source changes beneath it. Three variants, after ATEM, vMix and OBS: - Classic: a graphic with alpha; the program cuts or mixes beneath it at a cut point (by default where the graphic covers most, found by analysing the clip). - Track matte: graphic and matte in one file, side by side or stacked (the OBS layout). The matte switches the program pixel by pixel. - Mask only: the clip is a matte, an animated wipe. The matte variants need the GPU mixer and play as classic on the CPU one. The source is a Media Player in stinger mode wired to the mixer's new stinger input; its playlist is the library and its block holds per-clip settings. A cue parks a clip on its first frame. A take pins the running time its first frame lands at, programs the whole take into the mixer as control-binding keyframes on that timeline, and only then lets the clip go, so nothing waits on wall clock or holds the mixer's output. The matte composites inside glvideomixer: the matte pad takes the matte off the destination alpha (reverse-subtract) and the incoming source blends by it for the clip's frames. Clip frames leave their clocksync ahead of time and are stamped half an output frame into their frame, so a millisecond-rounding container cannot move one into its neighbour's. A stinger source asks for system memory, so a hardware decoder keeps alpha (VideoToolbox gives AYUV64 for ProRes 4444), and the stinger input converts what GL upload cannot take. Operator panel: STING type, the clip library with cue, per-clip settings, take reports, and example clips Strom renders itself (one per variant). API: stinger state, cue, take, clip settings, examples; takes also run through the transition endpoint. Events: StingerCued, StingerStarted, StingerCompleted (with measurements), StingerFailed. Builds on ideas from #755 by wagenet: opt-in sources, parking, events, degrading to a cut when a clip cannot play. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@srperens thanks! I will definitely take a look |
…k only A stinger plays a clip over the program while the source changes beneath it. Three variants, after ATEM, vMix and OBS: - Classic: a graphic with alpha; the program cuts or mixes beneath it at a cut point (by default where the graphic covers most, found by analysing the clip). - Track matte: graphic and matte in one file, side by side or stacked (the OBS layout). The matte switches the program pixel by pixel. - Mask only: the clip is a matte, an animated wipe. The matte variants need the GPU mixer and play as classic on the CPU one. The source is a Media Player in stinger mode wired to the mixer's new stinger input; its playlist is the library and its block holds per-clip settings. A cue parks a clip on its first frame. A take pins the running time its first frame lands at, programs the whole take into the mixer as control-binding keyframes on that timeline, and only then lets the clip go, so nothing waits on wall clock or holds the mixer's output. The matte composites inside glvideomixer: the matte pad takes the matte off the destination alpha (reverse-subtract) and the incoming source blends by it for the clip's frames. Clip frames leave their clocksync ahead of time and are stamped half an output frame into their frame, so a millisecond-rounding container cannot move one into its neighbour's. A stinger source asks for system memory, so a hardware decoder keeps alpha (VideoToolbox gives AYUV64 for ProRes 4444), and the stinger input converts what GL upload cannot take. Operator panel: STING type, the clip library with cue, per-clip settings, take reports, and example clips Strom renders itself (one per variant). API: stinger state, cue, take, clip settings, examples; takes also run through the transition endpoint. Events: StingerCued, StingerStarted, StingerCompleted (with measurements), StingerFailed. Builds on ideas from #755 by wagenet: opt-in sources, parking, events, degrading to a cut when a clip cannot play. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k only A stinger plays a clip over the program while the source changes beneath it. Three variants, after ATEM, vMix and OBS: - Classic: a graphic with alpha; the program cuts or mixes beneath it at a cut point (by default where the graphic covers most, found by analysing the clip). - Track matte: graphic and matte in one file, side by side or stacked (the OBS layout). The matte switches the program pixel by pixel. - Mask only: the clip is a matte, an animated wipe. The matte variants need the GPU mixer and play as classic on the CPU one. The source is a Media Player in stinger mode wired to the mixer's new stinger input; its playlist is the library and its block holds per-clip settings. A cue parks a clip on its first frame. A take pins the running time its first frame lands at, programs the whole take into the mixer as control-binding keyframes on that timeline, and only then lets the clip go, so nothing waits on wall clock or holds the mixer's output. The matte composites inside glvideomixer: the matte pad takes the matte off the destination alpha (reverse-subtract) and the incoming source blends by it for the clip's frames. Clip frames leave their clocksync ahead of time and are stamped half an output frame into their frame, so a millisecond-rounding container cannot move one into its neighbour's. A stinger source asks for system memory, so a hardware decoder keeps alpha (VideoToolbox gives AYUV64 for ProRes 4444), and the stinger input converts what GL upload cannot take. Operator panel: STING type, the clip library with cue, per-clip settings, take reports, and example clips Strom renders itself (one per variant). API: stinger state, cue, take, clip settings, examples; takes also run through the transition endpoint. Events: StingerCued, StingerStarted, StingerCompleted (with measurements), StingerFailed. Builds on ideas from #755 by wagenet: opt-in sources, parking, events, degrading to a cut when a clip cannot play. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k only A stinger plays a clip over the program while the source changes beneath it. Three variants, after ATEM, vMix and OBS: - Classic: a graphic with alpha; the program cuts or mixes beneath it at a cut point (by default where the graphic covers most, found by analysing the clip). - Track matte: graphic and matte in one file, side by side or stacked (the OBS layout). The matte switches the program pixel by pixel. - Mask only: the clip is a matte, an animated wipe. The matte variants need the GPU mixer and play as classic on the CPU one. The source is a Media Player in stinger mode wired to the mixer's new stinger input; its playlist is the library and its block holds per-clip settings. A cue parks a clip on its first frame. A take pins the running time its first frame lands at, programs the whole take into the mixer as control-binding keyframes on that timeline, and only then lets the clip go, so nothing waits on wall clock or holds the mixer's output. The matte composites inside glvideomixer: the matte pad takes the matte off the destination alpha (reverse-subtract) and the incoming source blends by it for the clip's frames. Clip frames leave their clocksync ahead of time and are stamped half an output frame into their frame, so a millisecond-rounding container cannot move one into its neighbour's. A stinger source asks for system memory, so a hardware decoder keeps alpha (VideoToolbox gives AYUV64 for ProRes 4444), and the stinger input converts what GL upload cannot take. Operator panel: STING type, the clip library with cue, per-clip settings, take reports, and example clips Strom renders itself (one per variant). API: stinger state, cue, take, clip settings, examples; takes also run through the transition endpoint. Events: StingerCued, StingerStarted, StingerCompleted (with measurements), StingerFailed. Builds on ideas from #755 by wagenet: opt-in sources, parking, events, degrading to a cut when a clip cannot play. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k only A stinger plays a clip over the program while the source changes beneath it. Three variants, after ATEM, vMix and OBS: - Classic: a graphic with alpha; the program cuts or mixes beneath it at a cut point (by default where the graphic covers most, found by analysing the clip). - Track matte: graphic and matte in one file, side by side or stacked (the OBS layout). The matte switches the program pixel by pixel. - Mask only: the clip is a matte, an animated wipe. The matte variants need the GPU mixer and play as classic on the CPU one. The source is a Media Player in stinger mode wired to the mixer's new stinger input; its playlist is the library and its block holds per-clip settings. A cue parks a clip on its first frame. A take pins the running time its first frame lands at, programs the whole take into the mixer as control-binding keyframes on that timeline, and only then lets the clip go, so nothing waits on wall clock or holds the mixer's output. The matte composites inside glvideomixer: the matte pad takes the matte off the destination alpha (reverse-subtract) and the incoming source blends by it for the clip's frames. Clip frames leave their clocksync ahead of time and are stamped half an output frame into their frame, so a millisecond-rounding container cannot move one into its neighbour's. A stinger source asks for system memory, so a hardware decoder keeps alpha (VideoToolbox gives AYUV64 for ProRes 4444), and the stinger input converts what GL upload cannot take. Operator panel: STING type, the clip library with cue, per-clip settings, take reports, and example clips Strom renders itself (one per variant). API: stinger state, cue, take, clip settings, examples; takes also run through the transition endpoint. Events: StingerCued, StingerStarted, StingerCompleted (with measurements), StingerFailed. Builds on ideas from #755 by wagenet: opt-in sources, parking, events, degrading to a cut when a clip cannot play. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k only (#1010) * feat(vision-mixer): stinger transitions: classic, track matte and mask only A stinger plays a clip over the program while the source changes beneath it. Three variants, after ATEM, vMix and OBS: - Classic: a graphic with alpha; the program cuts or mixes beneath it at a cut point (by default where the graphic covers most, found by analysing the clip). - Track matte: graphic and matte in one file, side by side or stacked (the OBS layout). The matte switches the program pixel by pixel. - Mask only: the clip is a matte, an animated wipe. The matte variants need the GPU mixer and play as classic on the CPU one. The source is a Media Player in stinger mode wired to the mixer's new stinger input; its playlist is the library and its block holds per-clip settings. A cue parks a clip on its first frame. A take pins the running time its first frame lands at, programs the whole take into the mixer as control-binding keyframes on that timeline, and only then lets the clip go, so nothing waits on wall clock or holds the mixer's output. The matte composites inside glvideomixer: the matte pad takes the matte off the destination alpha (reverse-subtract) and the incoming source blends by it for the clip's frames. Clip frames leave their clocksync ahead of time and are stamped half an output frame into their frame, so a millisecond-rounding container cannot move one into its neighbour's. A stinger source asks for system memory, so a hardware decoder keeps alpha (VideoToolbox gives AYUV64 for ProRes 4444), and the stinger input converts what GL upload cannot take. Operator panel: STING type, the clip library with cue, per-clip settings, take reports, and example clips Strom renders itself (one per variant). API: stinger state, cue, take, clip settings, examples; takes also run through the transition endpoint. Events: StingerCued, StingerStarted, StingerCompleted (with measurements), StingerFailed. Builds on ideas from #755 by wagenet: opt-in sources, parking, events, degrading to a cut when a clip cannot play. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(stinger): widen the example blade Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): keep the example blade's trails out of its matte The blade's glow and trails were drawn unclipped, so near the right edge they painted into the matte half of the side-by-side frame, and the trails stayed on screen after the blade had left. Clip the graphic to its half, and scale the trails with the blade's speed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(stinger): library API on the mixer, file guards, take ids What an outside client needs to drive stingers without knowing the Media Player behind them: - POST/DELETE .../stinger/clips: add a clip to the library (idempotent, a local file must exist) or remove one with its settings, on the mixer. - Calls that address a clip by index (cue, take, settings, remove) take an optional `file`; when the library has changed under the client, the call fails with 409 instead of acting on another clip. - Every take has an id: in the take response and in StingerStarted, StingerCompleted (report) and StingerFailed. The take response also names the file played. - from_input/to_input are optional in the transition request: a stinger always takes PGM to PVW; other transitions still require them (400). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): detect a clip's layout from the clip alone Side by side and stacked were read against the program's aspect, so the same file could detect differently on a 4:3 or vertical production. A track-matte clip is two standard frames (16:9, 4:3 or 9:16) next to or above each other, whatever the program runs; the analysis cache no longer keys on the program either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stinger): render only the edge frames in the blade spill test Every frame of the 3840x1080 example in a debug build took seconds of CPU beside the lib's timing-sensitive tests. The spill can only happen as the blade nears the right edge, so check those frames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): warm the mixer up at cue; count late matte frames On Linux CI (GStreamer 1.24.2, llvmpipe) a take that switched to a clip of another size showed the matte one frame late: the stinger branch renegotiated and the matte shader recompiled on the take's first frames. A cue now sends one blank frame in the clip's format through the consumer, so that happens at cue; the stinger pads are hidden between takes, so it never shows. The take report counted lateness on the graphic pad only; the matte pad, with a shader pass of its own, now counts too. The tests read the clip's frame count from its analysis, since 1.24.2's FFV1 decoder drops the last frame of a stream, and print the whole take when a frame is off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stinger): test clips in PNG, not FFV1 GStreamer 1.24.2's FFV1 decoder (Linux CI) drops a stream's last frame now and then, so a clip's analysis and its playback could disagree on its length and a take ran a frame longer than the test expected. PNG frames in QuickTime are lossless, keep alpha, and decode through libpng without frame threads. The FFV1 examples' own test allows the missing frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stinger): show the mixer's pads mid-take when a matte frame is off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stinger): add caps, geometry and shader uniforms to the mid-take pad dump Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stinger): show the program's middle row half way through a matte take Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): keep the matte shader out of FX passthrough make_glshader now starts every glshader in passthrough (#1007). The matte is not an FX slot and must always render: in passthrough the clip reached the mixer as it is, and its own alpha became the matte. The GPU stinger test fails without this. Also report the take with the CPU test's cut assertion, and drop the pad-dump eprintln (the dump stays in the failure message). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): place step keyframes half a frame before their frame The mixer stamps output frames from a rounded origin, so a frame's timestamp can be a nanosecond either side of FrameGrid's. A step key placed exactly on the frame then acted on that frame or the next by chance: depending on the take's phase, the graphic went on air a frame late while the cut landed on time. cpu_stingers_play_classic failed 6 of 8 local runs with 'cut at frame 13'; with the keys half a frame early it passed 8 of 8. The linear mix ramp keeps its exact times. The test rig now collects frames over a span of stream time, not wall time: on a slow runner the debug-build mixer falls behind real time and the window ended before the clip did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): look up overlay state by flow and block id after #1015 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): re-cue when another file lands on the parked index A stinger source counted itself parked on a playlist index, not on a file. Removing the cued clip, or a playlist PUT that put another file at the parked index, left the old clip's first frame parked: the panel showed the new clip ready and the next take played the old one with the new clip's settings. The player now records the playlist entry its internal pipeline holds. `is_parked_on` compares it with the entry at the index, and a cue reloads when they differ, so both the library edit and the playlist endpoint re-cue the right file. Guard tests (stinger_api_test): removing_the_cued_clip_parks_its_successor and a_playlist_put_over_the_parked_index_recues_it check what airs, and both fail with the fix reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): leave a restarted flow's new run alone A stop clears a take's claim, but the take did not look again after its cue and analysis awaits. After a stop and start in that window, `pipelines.get(flow)` returned the new run, and the old take programmed its mixer, then (its clip source gone) aborted and cut the new run's program. The finish step had the same gap after its wait loop. The take now re-checks its claim, and that its clip source is still the registered one, under the `pipelines` read lock before it programs the mixer, before it aborts on a clip that would not play, and before it settles the pads at the end. A stop clears every claim before it takes the pipeline away, so a claim held under that lock means the run the take started on. A take that lost its claim fails without touching anything and without "cut instead". Guard test (stinger_test): a_take_outlived_by_a_restart_leaves_the_new_run_alone. Timing a restart into a real cue or analysis is not reliable with the test's small clips, so a test-only hook (`hold_takes_for_tests`, per flow) holds take A at the point right before programming while the flow restarts. The test then checks that A fails with program_changed false, the new run's PGM is untouched, and a classic take on the new run lands frame by frame. With the re-check removed it fails ("clip 1 could not play ..., cut instead"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): report a late cut frame instead of a clean take A take's cut keyframes fire at the cut point whether or not the clip has reached the graphic pad. A clip running late airs the new source with no graphic over it, yet the take reported `frames_arrived` n/n (every frame that reached the pad, including those the mixer had already passed) and a plain StingerCompleted. The counting probe now counts a graphic frame as arrived only when it is in time, and records whether the classic clip's frame due at the cut point (the newest clip frame that starts before the cut's output frame ends) arrived in time. Still atomics only in the per-buffer probe: one range check and one fetch_or. When that frame was late, the take report carries a new `warning` field and the backend logs it. StingerTakeReport is new in this PR, so the optional field breaks no client; StingerCompleted and StingerFailed keep their fields. openapi.json updated. Guard test (stinger_test): a_late_cut_frame_is_reported holds the take's first clip frame for a second before the graphic pad, and requires a warning and fewer than n frames arrived. With the probe and report change reverted it fails (warning None, 30/30 arrived). The classic take helper now also requires a clean take to carry no warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): undo a take that fails part way through programming `program_stinger` cancels a fade-to-black, resets the pads and then keys alphas, z-orders and blend modes one property at a time. A failure after the first of those left the mixer half programmed: the keyframes already set could change the program on air with no graphic, the error reported `program_changed: false`, and the ended fade-to-black was never broadcast. A failed `spawn_blocking` join around `play_at` returned early in the same state, without the abort the play error path does. `program_stinger` now aborts the take itself (old source alone on air, stinger pads down) when a pad change fails, and its error carries whether it ended a fade-to-black. The take broadcasts VisionMixerFtbChanged for that, and also when it aborts after a clip that would not play. A failed join now goes down the same abort and "cut instead" path as a play error. Guard test (stinger_test): a_take_that_fails_to_program_leaves_the_old_source_on_air. No real keyframe failure can be caused on demand, so a test-only hook (`fail_stinger_programming_for_tests`, per flow) fails programming after every pad change is made. From fade-to-black, the test requires the old source alone on air through where the clip and cut would have aired, an FTB-ended event, and a normal classic take afterwards. Without the abort it fails (the cut airs); without the broadcast it fails (no FTB event). The join-failure path has no guard: it needs a panic inside `play_at`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): honour cut and end the mix with the clip for a downgraded mask A mask-only clip downgraded to classic on a CPU mixer took the operator's cut point but then mixed for the full mix length whatever `beneath` said. It now cuts when `beneath` is `cut`, and clamps the mix to the rest of the clip like the classic branch, so the fade never outlasts the clip. Guards: a_downgraded_mask_with_a_cut_point_honours_cut and a_downgraded_mask_mix_ends_with_the_clip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): pin a take's start to the clip's first video frame Audio and video of a stinger clip source share one Timing, and a take's pinned start was taken from whichever stream reached the bridge first. For a clip whose audio starts before or after its video (an MP4 edit list, B-frames, audio priming) the first video frame then aired off the frame the mixer was programmed for, by the difference. The park probe on the video clocksync now also keeps the last buffer's PTS (one more atomic store). play_at turns the parked frame's PTS into its running time from the clocksync's segment and pins the baseline to it, so the first video frame lands on start_at and audio keeps its offset from video. Without a parked frame the old behaviour stays. An ordinary player never pins, so its timing is unchanged. Guards: - a_pinned_start_lands_the_first_video_frame_when_audio_comes_first (timing.rs unit test, audio placed first). - a_clip_with_early_audio_lands_its_first_video_frame_on_the_take (stinger_test): a classic clip whose PCM audio starts 200 ms before its video, CPU mixer, checked frame by frame on the program output. Which stream reaches the bridge first is a race, so the test holds the take's first video sample for 30 ms through a new #[doc(hidden)] hold_bridge_for_tests hook; the bridge checks it with one relaxed load per sample. Fails 4/4 with the pin taken as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): share the running clip analysis with a take A clip taken straight after it was added decoded twice: once in the background run the add started, and again in the take, whose inline analyze_cached did not look at that run. analyze_cached now runs once at a time per file. A caller that comes while a run is going waits for it and shares its result (a per-file slot with a Condvar), and a guard hands an error to the waiters if the run stops without one, so nobody waits forever. With no run going it analyses as before. The take still re-checks that the flow was not restarted after the analysis await. Guard: a_clip_taken_straight_after_it_was_added_is_analysed_once (stinger_test) adds a 1080p clip and takes it at once, then checks a new #[doc(hidden)] per-file counter of analysis runs is 1. With the old analyze_cached it counts 2 (5/5 runs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(stinger): park probe fires once per cue, not on every buffer The park probe on a stinger clip source's video clocksync stayed on the pad for good, counting every buffer of every take, though only the first frame after a cue matters: it tells the cue the clip is parked and gives the take that frame's PTS to pin the start to video. The probe is now one-shot. It records the parked frame's PTS, bumps the parked-frame count and returns Remove. StingerPlayback::arm_park_probe installs a fresh one each time a frame is expected to park: - The bridge arms it whenever it builds the first video chain of a stinger source. Loading a clip removes the bridge chains, so a cue that loads finds no chain to arm on; the first buffer into the new chain is the loaded clip's first frame. Arming on every new chain also covers the builder-seeded first clip, whose chain appears before or after its cue. A relinked chain (an HLS pad taking over) is not armed. - A cue that rewinds a loaded clip arms on the existing chain before the seek. The probe id is kept in the player with the parked-frame count it was armed at; re-arming removes a previous probe that has not fired yet. The lock is taken once per arm, never in the probe. last_pts is renamed parked_pts, and the take reads the segment from the named park clocksync. The bridge's test-hold check in new_sample is gated by a bool captured when the callback is built, so an ordinary Media Player never loads the BRIDGE_HOLDS_ARMED flag. Guard: a_take_plays_with_no_park_probe_on_its_path (stinger_test) takes a 30-frame clip and checks the parked-frame count moves at most twice over the take and the re-cue after it. With the probe returning Ok again it moves 31 and the test fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): restart a stinger source output that stopped on an error A stinger cue also restarts a source output that stopped on an error, with a flush local to its branches, so a clip some consumer cannot take costs that clip and not every take after it. The GL input adaptation that made a ProRes 4444 clip after another format play at all is on main since #1033; the tests here cover the switch between formats on the stinger path and the recovery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(stinger): broadcast-grade example clips and a stacked track matte Redraw the example stingers so they look like a stinger pack rather than test patterns, and add a fourth clip so every layout has an example. - strom-sweep (classic): pink, cyan and navy slanted panels whoosh in with glowing edges, light streaks and a lens flare; the navy panel covers the whole frame with an extruded, glowing STROM wordmark whose letters fly in with overshoot and leave after a small pull back. - strom-blade (track matte, side by side): a white-hot blade with a chromatic fringe, crackling arcs, speed lines and sparks; the matte's soft switch sits just behind the light. - strom-ribbons (track matte, stacked, new): seven brush ribbons with bristle texture, shading and drop shadows fan out from the middle; the matte reveals the new source behind their heads. - strom-shards (mask only): hexagonal tiles flip open in a staggered wave through grey to white. Moving clips average sub-frame samples for motion blur; mattes are drawn once per frame. Randomness comes from a seeded generator. The examples render in parallel, and examples.rs is split into one module per clip. Tests check full cover of the classic, a matte that is never painted into, goes strictly from 0 to 255 and never back, a clean frame at both ends, a monotonic mask, and the layout, cut point and matte marks of every encoded example, the stacked one included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(stinger): modern ink, ultraviolet and volt palette for the sweep and ribbons examples The sweep's royal blue panel and the ribbons' rainbow of light gradient ends read as pastel. Both now share one palette: ink and graphite, an ultraviolet-to-magenta family and a single acid volt accent, dark at the tail and saturated at the head, with a lighter top highlight on the ribbons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(stinger): reload the library from disk, and a denser clip list A clip file rewritten under the same name kept playing its old content: the player only compared file names. The stinger clip source now records the loaded file's modification time and length, and a parked clip whose file changed since is not parked (is_parked_on is false), so the panel shows it not ready and a plain cue or take loads it again. A file gone from disk keeps the parked clip. POST .../stinger/reload analyses every clip whose file has no analysis for its current content, loads and parks the cued clip again when its file changed, flags clips whose files are gone (new `missing` field on StingerClip) and returns the stinger state. Refused with 409 while a stinger is on air. The analysis cache keys on the same stamp, now at nanosecond resolution. Operator panel: clip tiles in up to four columns at two rows' height, a scrollbar that stays visible on macOS, a clip count, the cued clip kept in view, MISSING and analysing badges, and a RELOAD button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): load a clip again when it cannot seek back to its start A stinger clip from a source that cannot seek (an HTTP server without range requests) played its first take, but the re-park after it failed with "Seek failed": cue() returned the error and left the clip neither parked nor reloaded, and every later cue and take failed the same way until the flow restarted. When pausing and rewinding fails, the cue now loads the clip again, which starts it from its first frame; the bridge arms the park probe on the new chain, and the parked-frame count is read again before the load. A seek that works behaves as before. The guard test serves a Matroska clip from a local HTTP server with `Accept-Ranges: none` and takes it twice; both takes must deliver every frame. CI installs libsoup-3.0-0 explicitly so souphttpsrc loads. The operator guide says stinger clips are meant to be local files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): park a clip's first frame even before the flow runs A stinger source parks its first clip while the flow is still starting. The clocksync pacing probe drops every buffer that arrives before the flow has a running time (so a live stream is never paced against its raw timeline), and the internal pipeline decodes on meanwhile, so the frame that parked was whichever came after the flow reached PLAYING: frame 3, or frame 17 on a loaded machine. Every take of that clip then started that many frames in, and its report counted fewer frames than the clip has. A stinger clip's parked frame needs no offset from the flow: the take sets the clocksyncs and pins the start from that frame. Let it through and remove the probe instead of dropping it. An ordinary player keeps dropping as before. This was the flake in stinger_prores_test (frames_arrived 29 or 27 of 30, or the graphic on air for 16 frames, on the first take of a clip). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(openapi): regenerate the snapshot after rebasing onto the media download API Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stinger): address review findings on takes, the clip source and the library - A mask-only clip downgraded to classic on the CPU mixer no longer raises its grey frames over the program; the cut-frame check skips a clip with no graphic. - Fade-to-black is refused while a stinger is on air, so the take's end no longer brings the program back under a flagged fade. - Classic takes and fade-to-black claim the mixer while they program the pads, closing the window between the on-air check and the pipeline lock. - The Media Player playlist, control, seek and goto endpoints answer 409 while the player is playing a stinger take. - Stinger library edits are serialised and refused during a take, so clips added together all land. - A mix beneath a classic take ends with the clip when the cut point moves up to the output grid (take timing moved to crate::stinger::take_times). - The cue's warm-up frame is skipped when the consumer already took one in the same format, and an unlinked output is not flushed on every cue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: install gsettings-desktop-schemas for the HTTP stinger test libsoup asks GIO for the system proxy, and GIO's proxy lookup aborts the whole test process when the org.gnome.system.proxy schema is missing: stinger_api_test died with SIGTRAP on Linux CI, twice on 038a224. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: compile the GSettings schemas after restoring cached packages cache-apt-pkgs-action restores package files without running triggers, so gschemas.compiled never gains org.gnome.system.proxy on a cache hit, and the HTTP stinger test's process aborts in GIO's proxy lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Problem
The vision mixer has no stinger: a keyed clip played over the program while the cut or transition happens beneath it, at a point in the clip chosen so the artwork covers the switch.
Change
A stinger is triggered through the existing transition endpoint as type
stinger, naming a Media Player block that declares itself a stinger clip source and is wired to a keyed (DSK) input. Cut point, the transition beneath, and its duration are Media Player properties, since where a clip covers follows from the artwork; a take names only its clip. The clip is held on its first frame while idle, so a take only starts playback: 0.5 ms at 4K against 28.4 ms unparked. Declaration is opt-in, so a looping graphic on a keyed input is never parked or unlooped.The cut is anchored to the clip, not timed. A one-shot probe holds the mixer's output on the frame before the clip's cut point until the take is applied, however far the output trails the clock. A wall-clock cut lands on whichever frame the mixer is on, which on a loaded machine is often before the clip is on air.
A cut the clip may not cover is reported as
StingerFailedwithstill_running: true: the clip frame is late at the cut; the clip never reaches the mixer by cut point + 500 ms (the keyed input is taken off air and the cut made plainly); or the mixer output stalls for 5 s (the cut is made on wall clock).A finished stinger releases the mixer only after the mixer's output passes the clip's last frame, because
videoaggregatorkeeps showing an expired frame while the next take's first frame is queued. A take's mixer claim carries a token, so a take from a stopped flow cannot drive its replacement.A declared source plays unpaced, and its video output waits instead of dropping. It ignores the Media Player's
syncandplayout_delay_ms, so the clip runs ahead of the mixer. Its video appsrc is non-leaky and blocking, so when the bridge's queues are full the clip waits rather than losing frames. Audio stays leaky: an audio output may be left unlinked, and a blocking audio appsrc that never drains stalled the player, and with it the next take's re-arm, for good.A waiting push parks the Media Player's internal streaming thread, and pausing, seeking, switching file and shutting the player down all wait for that thread. So each first lifts the waiting appsrc's limits (changing max-bytes wakes the push) and the last overlapping call to return restores them: a take re-arms its clip from its own task while an operator may seek or stop it. Without this, a flow whose mixer stopped pulling the clip, or whose declared clip was played before its video was wired, could not be stopped.
Frames reaching the mixer, and how far the newest clip frame reaches past the cut frame at the cut check (5 takes each, macOS):
Rejected: keep #945's pacing and give the clip a fixed head start
This PR opts stinger sources out of #945's pacing. The alternative stamps a paced clip a fixed lead ahead of its release. At 100 ms in a scratch build every frame arrived at all three sizes, with 72 to 166 ms at the check, but:
The unpaced route reuses the existing
sync = falsepath, keeps the appsrc memory bound, and has no lead to tune. Switching to the paced route would touch only the Media Player builder and timing module.Evidence
the_cut_lands_on_the_frame_the_cut_point_names(take 1 landed on clip frame 1) anda_repeat_stinger_plays_forward_from_the_start("coverage was [3, 3, 3, 3, 6, 10, …]"), 3 of 3 runs.a_clip_longer_than_the_output_queue_plays_whole: "every clip frame must reach the mixer", 160 of 240.a_clip_the_mixer_stops_taking_can_still_be_stopped("stopping the player must return while the mixer takes nothing") andan_unwired_stinger_clip_can_still_be_stopped("seeking a clip nothing takes must return"). Both run on a runtime that is abandoned rather than joined, so a hung take fails instead of never finishing.overlapping_player_calls_keep_the_clip_queue_bounded("the clip's video queue limits must be back as built"), 3 of 3 runs. Unguarded, the queue stayed unbounded: 630 MB for a 3 s 1080p take.the_cut_lands_on_the_frame_the_cut_point_names(10 of 12 takes on clip frame 16, not 15).wait_for_mixer_to_passfailsa_repeat_stinger_plays_forward_from_the_start("coverage was [100, 3, 6, …]": the previous clip's last frame on air).undeclared_source_on_keyed_input_is_left_alone.a_stinger_outliving_its_flow_leaves_the_next_one_aloneguards the claim released on stop. It still passes with the token check inrun_takeremoved, so the token check itself is not guarded.audio_outunlinked and with it on a synced sink; stopping a flow mid-clip at 1080p returned in 21-23 ms, 5 of 5; a 45-frame 1080p clip delivered 45 of 45 on 3 takes.Tests
Linux CI will likely fail
a_clip_longer_than_the_output_queue_plays_wholewith 239 of 240 frames, in about half of runs (10 of 21 so far). The cause is a bug in CI's GStreamer 1.24.2, not in this change.avdec_ffv1with frame threading drops a clip's last frame at end of stream when its output lags its input, and logsCouldn't get codec frame !. Upstream fixed it in 1.24.4, so production (1.26.6) and Windows and macOS CI are not affected. It reproduces on Ubuntu 24.04 with a plaingst-launch-1.0pipeline, without Strom. This PR makes the lag more likely, because the stinger clip's output now waits for room instead of dropping frames. The test stays strict instead of excusing a missing last frame on old GStreamer; #964 proposes running Linux tests on 1.26.Ran at 743c8b2, macOS,
STROM_REQUIRE_GL=1 STROM_REQUIRE_GST_PLUGINS=1:cargo test --test stinger_test: 16/16 in 2 runs. On Linux,Check (Linux)passed at 743c8b2.cargo test --lib mediaplayer30/30;cargo test --test pipeline_lifecycle_test4/4.cargo test --test stinger_alpha_test4/4, includingalpha_survives_gl_videomixerwith a real GL context;cargo test --test openapi_test.cargo clippy --workspace --all-targets -- -D warnings,cargo fmt --check.Skipped in CI:
alpha_survives_gl_videomixerreturns early inCheck (Linux), which sets noSTROM_REQUIRE_GL(the alpha binary finished in 0.07 s), andBuild (macOS)is label-gated. The GL alpha half has run only locally.Not run locally:
cargo test --workspace.For the reviewer
blend_pads, ORC-generated code) appeared in about 80 heavy-load runs across both arms of an A/B, and not in 40 targeted runs. Not attributed to this change.Out of scope
StingerCompletedin 11 CI runs), so nothing on air changes. A fix would change when every take ends, so it is left for a follow-up.StingerCompleted, run as late as the mixer's output does. On a machine where the mixer ran at about a quarter of real time, completion came several seconds after the clip ended on the clock.🤖 Generated with Claude Code