Repository navigation
fix(vision mixer): adapt GPU inputs to any pixel format, converting only while needed - #1033
Conversation
…nly while needed A GPU Vision Mixer input starts with glupload, which uploads a fixed list of system-memory formats. A source in any other format (AYUV64, which a ProRes 4444 clip decodes to on macOS, or GBR_10LE) was refused: a flow link from a capsfilter in such a format was not even made, and a format change mid-stream failed the producer with not-negotiated. The GL input front now adapts pixel formats as well as CUDA memory. video_adapt::decide() calls for a videoconvert when the producer offers raw video in system memory in no format glupload uploads (from the change made for the stinger on feat/stinger). The converter is transient. It goes in and comes out on the input's streaming thread, in a probe on the front queue's source pad, as a CAPS event leaves the queue, so frames queued in the old format still go through the old chain. It comes out as soon as the caps are something glupload takes directly, or CUDA memory; cudadownload (CUDA-GL interop) then goes in directly in front of glupload, never in front of the converter, so NVDEC output never takes a CPU round trip. Until the CAPS event, queries are answered for the input as it will be: an ACCEPT_CAPS for a format only the converter takes is accepted, one for caps the input takes without it is answered by what follows the converter. While the converter is in, the answer to a CAPS query lists what glupload or cudadownload takes directly, CUDA memory included, ahead of what only the converter takes, so a decoder still picks GPU memory. Raw video in system memory in any format is added last to the answer only when a query offers nothing the input takes directly, or nothing at all (the linker's link check); other answers are unchanged. The CPU backend already converts on each input; the new tests show #674's cases pass there, and guard it. Fixes #674. Part of #969's direction (consumer-side adapters decided by decide(), no autovideoconvert); follows #951. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 1.24 Before GStreamer 1.24.13, glupload answers a CAPS query with only the caps of the upload method it is using, whenever those meet the filter. With the input's videoconvert in, the converter writes into glupload's own pool, the method is the GL memory one, and an unfiltered query gets GL memory only. The answer built while the converter is in then had no system-memory formats, so no CUDA alternative, and listed nothing only the converter takes. On 1.24.2, NVDEC into an input that had the converter in decoded to system memory and took a CPU round trip. Ask glupload a second time with a system-memory filter, which misses the GL memory method and makes it list every method, and merge the two answers. An unfiltered answer while the converter is in also lists the converter's system-memory caps, last, as it does while the converter is out. Tests: - The CAPS-answer assertions print the answer. - gpu_converter_answers_with_direct_formats_first counts CUDA memory as taken directly, since cudadownload takes it with no conversion. - The NVDEC test encodes with the first working NVENC H.264 encoder (nvcudah264enc, nvautogpuh264enc, nvh264enc): the legacy nvh264enc fails with "Selected preset not supported" on some driver and GPU combinations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
srperens
left a comment
There was a problem hiding this comment.
Verdict: Comment. The design is right and follows BLOCK_GUIDELINES. I found no defect in the code. CI has not run at this head, which blocks approval.
Needed before merge
- CI at
6581f3d. Run 37642337039 was created 2026-10-07T15:06Z, is stillqueuedand has no jobs. Re-trigger:gh run cancel 37642337039 && gh run rerun 37642337039, or push an empty commit. The previous headdc4d16awas red inCheck (Linux), oncuda_after_a_converted_format_bypasses_the_converter(does not offer CUDA memory,vision_mixer_input_format_cuda_test.rs:323).6581f3dfixes that.cargo teststops at the first failing binary, sovision_mixer_input_format_testhas never run in CI at any head.pipeline_lifecycle_testran and passed atdc4d16a.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| Probes fire per event or query, never per buffer | CONFIRMED |
backend/src/gst/gl_input_front.rs:304 — front_src.add_probe(gst::PadProbeType::EVENT_DOWNSTREAM, move. No BUFFER probe is added |
| Shared probe state holds weak references only | CONFIRMED |
backend/src/gst/gl_input_front.rs:221 — upload: glib::WeakRef<gst::Element>, and front_src likewise |
| An ACCEPT_CAPS for a convertible-only format is accepted before the converter is in | CONFIRMED |
backend/src/gst/gl_input_front.rs:343 — q.set_result(convert_sink_caps().is_some()); |
| The new tests guard the change | UNVERIFIED |
Revert rows are on the author's macOS and an L4 rig. No CI run has executed them at this head (above) |
Diagnosis. The cause is right: glupload uploads a fixed format list, and before this PR the input front never adapted pixel format. Coverage is BOUNDED to Vision Mixer GPU inputs, the only caller of install(). The deprecated Compositor still feeds glupload with no front: backend/src/blocks/builtin/compositor.rs:347 — let upload = gst::ElementFactory::make("glupload"). Putting the converter in and out from the queue's own src thread, as the CAPS event leaves, is sound. Nothing else pushes on that link, and the converter has no thread of its own to stop. That makes set_state(Null) and bin.remove() from inside the probe safe.
Behaviour change on every GPU input. Every Vision Mixer GPU input changes, including ones that never need a converter. An unfiltered CAPS query now ends with "any system-memory raw video": backend/src/gst/gl_input_front.rs:489 — if filter.is_none() || offers_only_convertible {. The author flags zig-zag intersection as the open risk. SPECULATIVE (not verified): a producer that intersects that way could now take a CPU conversion it did not need.
Radius. SHARED: one block's GPU inputs. video_adapt::decide() changes its Consumer::GlUpload signature, and gl_input_front.rs is its only caller. The new Adapter::VideoConvert variant breaks no exhaustive match: build_elements and installed both handle it, and no other open PR touches video_adapt.rs.
Overlaps.
- #674 owns this decision. My triage there recommended a fixed BGRA capsfilter (Option 1), and its Ask is still open. This PR rejects that option, and it is right to. A fixed BGRA capsfilter forces a CPU conversion on every input and drops GL/CUDA memory, which BLOCK_GUIDELINES' memory-format rule forbids. That rule postdates my triage. The PR comes from someone who may decide, so I read it as the answer to #674. Merging it closes that Ask.
- #969 keeps the CPU path's
VideoConvertMode. It is out of scope here, as the PR says.
Tests & CI. No checks at 6581f3d (above). macOS and Windows are skipped on pull requests. The platform-specific evidence (ProRes 4444 on VideoToolbox) comes from the author's macOS run only. Merging on Linux green and letting main's macOS run cover it is reasonable.
Confidence: MED
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>
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>
…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>
Closes #674
Problem
A GPU Vision Mixer input starts with
glupload, which uploads a fixed list of system-memory pixel formats. A source in any other format was refused:AYUV64,GBR_10LE) was never made. The linker's caps check failed, the link stayed pending, and the source stopped withnot-linked. The flow never reached PLAYING.AYUV64on macOS) failed the producer withnot-negotiated.On the CPU backend, #674's cases already work on
main: each input has its own converter. Nothing guarded that, though.Change
GPU path (
gst/gl_input_front.rs,gst/video_adapt.rs)decide()gets a rule forConsumer::GlUpload(&caps): raw video in system memory only, in no formatgluploaduploads, needs[VideoConvert]. The upload formats come fromglupload's sink template (gl_upload_system_caps()), so they follow the installed GStreamer. This part comes from the stinger work onfeat/stinger.EVENT_DOWNSTREAMprobe on the front queue's source pad. The probe acts on CAPS events only, never on buffers, and makes its change as the CAPS event leaves the queue. GStreamer reads the pad's peer after the probes have run, so the event and every frame after it go to the new chain. Frames queued in the old format have already gone through the old one. The converter comes out as soon as the caps are somethinggluploadtakes directly, or CUDA memory. Unlinking it, relinking around it, setting it to NULL and removing it from the bin all happen in that probe. Only that thread pushes on that link, so no IDLE wait is needed.cudadownload(CUDA-GL interop) directly in front ofglupload. It never lands in front of the converter. While the converter is in, a CUDA query does not trigger the query-timecudadownloadsplice. Inputs that never needed the converter keep the existing query-timecudadownloadsplice (IDLE probe), unchanged.cudadownloadnot yet in, it is answered fromcudadownload's sink template.glupload's GL and uploadable system-memory entries), the CUDA alternative of those, and only then what the converter adds. While the converter is out, raw video in system memory in any format is appended last, and only when the query's filter offers nothing the input takes directly or there is no filter (the linker's link check). That append is what lets the flow link be made. A filtered query that offers something the input takes directly gets the same answer as before.gluploadthere answers a CAPS query with only the caps of the upload method it is using. With the converter in, that is the GL memory method, so an unfiltered query got GL memory only: no system-memory formats, so no CUDA alternative. On 1.24.2, NVDEC into an input that had the converter in then decoded to system memory, one CPU round trip per frame.takes_directly()asksgluploada second time with a system-memory filter, which makes it list every method, and merges the answers. 1.24.13 and later list every method anyway, so nothing changes there.WeakRef<Element>,WeakRef<Pad>).CPU path: no code change. The per-input converter and
capsfilter_inonmainalready handle #674's cases. The new tests guard them.Rejected:
feat/stinger. Acudadownloadspliced later lands in front of it, and the converter's CAPS answer offers every CPU format.gluploaddirectly and fail.autovideoconvert, per BLOCK_GUIDELINES.Evidence
AYUV64andGBR_10LEfrom a pinned capsfilter, with the converter in:gpu_ayuv64,gpu_gbr_10le. Both fail withdecide()'s VideoConvert rule removed (themain-equivalent behaviour) and with the stagedfeat/stingerfront:start vision mixer pipeline: StateChange(... current: Paused, pending: Playing), from a link the linker never made. They also fail with the any-format entry left out of the CAPS-query answer.gluploadthen receives the source's own format:gpu_converter_comes_out_when_the_format_switches_back. It fails with the removal disabled:NV12: glupload takes it, but the input still runs ["videoconvert"].cudadownloadstand-in), and while the converter is in, CUDA memory is offered ahead of the converter's formats:cuda_after_a_converted_format_bypasses_the_converter. It fails:["videoconvert", ...]);gluploaduploads, and the converter comes out: second half ofgpu_converter_answers_with_direct_formats_first. It fails with the removal disabled. Its first half (direct entries before converter-only ones) also passes with the reordering reverted.videoconvert's own answer already puts passthrough caps first, so for GL/system memory it documents the behaviour and does not guard it. The CUDA ordering is guarded by the CUDA test above.videoconvert, I420, NV12, AYUV64, GBR_10LE):cpu_*. With the per-input converter removed frompipeline_cpu.rs,cpu_ayuv64fails; the other five pass, because the compositor takes those formats itself.gl_input_front::tests::with_the_converter_in_direct_paths_come_first.nvdec_cuda_after_a_converted_format_stays_on_the_gpu(#[ignore], NVIDIA only), run on an NVIDIA L4 with GStreamer 1.24.2.nvh264decsrc:video/x-raw(memory:CUDAMemory), format=NV12;gluploadsink:video/x-raw(memory:GLMemory), format=NV12; adapters["cudadownload"], novideoconvertleft. Without the 1.24 fix it fails: the decoder's frames stay in system memory.gpu_converter_answers_with_direct_formats_firstfails (the answer lists nothing only the converter takes) and so doescuda_after_a_converted_format_bypasses_the_converter(does not offer CUDA memory, the error the first CI run on this PR showed). On 1.28 it cannot be made to fail, sincegluploadthere already lists every method; CI's 1.24.2 is what guards it.Tests
Ran on macOS (GStreamer 1.28.6, native GL), with
STROM_REQUIRE_GL=1 STROM_REQUIRE_GST_PLUGINS=1:vision_mixer_input_format_test: 14 passed (run 4 times)vision_mixer_input_format_cuda_test: 1 passed, 1 ignored (the NVDEC test)vision_mixer_cpu_test7,vision_mixer_fx_test2,vision_mixer_source_resize_test8,pipeline_lifecycle_test4,failed_start_teardown_test3,shader_validation_test2,gl_memory_link_test7,thumbnail_tap_cuda_test3,video_input_bridge_test5,video_input_bridge_gl_test4,vision_mixer_cuda_input_test2,vision_mixer_cuda_missing_test1,openapi_test1: all passedcargo test --workspace --lib: 900 + 28 + 101 passed, 5 ignoredcargo clippy --all-targets --features efp -- -D warnings,cargo fmt --check: cleanSkipped: none. Every GL test ran with a GL context.
On Linux with an NVIDIA L4 (GStreamer 1.24.2, headless EGL), after the 1.24 fix:
vision_mixer_input_format_test: 14 passedvision_mixer_input_format_cuda_test:cuda_after_a_converted_format_bypasses_the_converterskips where the realcudadownloadis installed; with nvcodec hidden (as on CI) it ran and passedvision_mixer_input_format_cuda_test -- --ignored nvdec: passed (encodes withnvcudah264enc; the legacynvh264encfails withSelected preset not supportedon that driver)vision_mixer_cuda_input_test: 2 passedNot run:
cargo test --workspaceintegration suite.GST_GL_MEMORY_VIDEO_FORMATS_STR:AYUV64andGBR_10LEare missing there too, whileA444_10LEuploads on 1.24.2, so it is not used.For the reviewer
cuda_after_a_converted_format_bypasses_the_converterskips. Its appsrc pushes system memory labelled as CUDA memory, which the realcudadownloadcannot take. The ignored NVDEC test covers that host.set_state(Null)andbin.remove()of the removed converter. This follows the dynamic-pipeline pattern, andpipeline_lifecycle_testpasses.Out of scope
VideoConvertMode(autovideoconverton NVIDIA hosts) and the other Retire the global VideoConvertMode and the remaining producer-side memory-format choices #969 items.cudadownloadstays once it is in, as before.🤖 Generated with Claude Code