Skip to content

refactor(gst): decide video input adapters in one place, and write down the block contract - #951

Merged
srperens merged 6 commits into
mainfrom
refactor/video-input-decide
Oct 2, 2026
Merged

srperens merged 6 commits into
mainfrom
refactor/video-input-decide

Conversation

@srperens

@srperens srperens commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR puts the video input adapter decisions in one place and writes the block contract down as rules. It is Option D from #682, and it follows #948 and #949.

decide(): one place for the adapter rules

Three mechanisms each worked out on their own what a video input needs in front of it. They now all ask gst::video_adapt::decide(), and each keeps its own timing:

Call site When it decides
video_input_bridge (WHEP Output) on the first CAPS event
gl_link (the linker) when a link is refused
gl_input_front (Vision Mixer GPU inputs) on a query, with the splice done while the pad is idle

The rules, also given in the module doc:

Consumer Producer caps Adapters
GlUpload raw video in CUDA memory only CudaDownload, or MissingFactory when cudadownload is not installed
GlUpload anything else none
Accepts(consumer caps) raw video in GL memory only, and the consumer takes system-memory raw video GlDownload

WHEP Output is treated as a consumer whose caps are corrected, not as a rule of its own. whepserversink advertises video/x-raw(memory:GLMemory) but cannot encode it. The bridge therefore passes the caps the sink can actually take (system-memory raw video), and the download follows from the same rule the linker uses. The NV12 conversion stays a separate, WHEP-only step in the bridge. It saves converting once per viewer and is not a caps adaptation, so it is outside the table.

No behaviour change. The rules moved word for word, and every existing guard passes without edits. Two timing details differ, and neither changes the outcome:

  • The linker now always queries the sink's caps after a refused link.
  • The cudadownload registry lookup happens just before the lock instead of under it.

Compositor deprecated (text only)

The block's name is now "Video Compositor (deprecated)", and its description starts with "Deprecated: no longer maintained; use the Vision Mixer." Its id, properties and behaviour are unchanged. The Vision Mixer is the maintained mixer, so the compositor does not get the new input fronts.

Block guidelines

  • New file docs/BLOCK_GUIDELINES.md, which holds the block rules:
    • Block Input Contract (new): a block emits what it naturally produces, and each consumer adapts its own input. This covers encoded vs raw, memory type, pixel format, resolution and framerate, and audio rate, format and channels. A consumer that cannot adapt fails the flow with an element error that says why. A guard test feeds the consumer something other than its preferred input.
    • GStreamer Memory Formats, with a new rule: avoid autovideoconvert and other auto-pluggers that choose by rank, and extend decide() instead. Behind gldownload on GStreamer 1.24.2, autovideoconvert failed 9 of 20 runs. See perf(whep): convert a video input once, not once per viewer #949.
    • Output Block Inputs, moved unchanged.
  • CLAUDE.md imports the file (@docs/BLOCK_GUIDELINES.md), so agents read the same text as before.
  • docs/CONTRIBUTING.md points human contributors to it.

Part of #682.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactor (no behaviour change)

Testing

Ran locally (macOS), rebased on main after #949 and #950:

  • cargo test --lib: 693 passed, 4 ignored.
  • Integration tests, all passing and unedited by this PR:
    • gl_memory_link_test (7)
    • vision_mixer_cuda_input_test (2)
    • vision_mixer_cuda_missing_test (1)
    • video_input_bridge_gl_test (1)
    • whip_whep_ice_transport_policy_test (9)
    • pipeline_lifecycle_test (3)
    • vision_mixer_fx_test (2)
    • vision_mixer_cpu_test (7)
  • cargo clippy --all-targets -- -D warnings and cargo fmt are clean.

Revert checks (each temporary, then restored):

  • Breaking the cudadownload availability rule, the Accepts consumer check, the CUDA-only rule and the GL-download rule in turn failed 7 tests each time. Among them were the new decide() tests and the bridge's real pipeline tests.
  • Making the WHEP bridge pass the advertised GL caps instead of the corrected ones failed gl_nv12_is_only_downloaded, gl_rgba_is_downloaded_then_converted and video_input_bridge_gl_test.

Not run: the full cargo test --workspace.

Known, not changed (it would be a behaviour change, so it is left for a follow-up): the GL input front's CUDA-only check counts a video/x-raw(ANY) offer as CUDA-only, while the GL rule excludes ANY.

🤖 Generated with Claude Code

srperens and others added 6 commits October 2, 2026 13:43
Three mechanisms each decided what a video input needs in front of it:
the WHEP Output caps-event bridge (gldownload, converter + capsfilter),
the linker's GL-download retry, and the GL input front's CUDA adapter.
Their rules now live in `gst::video_adapt::decide(producer caps,
consumer, factory availability) -> Result<Vec<Adapter>, MissingFactory>`.

Each call site keeps its own timing (CAPS-event probe, link retry,
query probe + IDLE splice) and asks decide() what to insert. The WHEP
bridge builds its elements through the shared `build_elements`. The
rules are moved unchanged, and their unit tests with them; decide()
gets a test per rule.

No behaviour change intended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Vision Mixer is the maintained mixer. Blocks have no deprecation
flag, so the display name and description say it. Id, properties and
behaviour are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
whepserversink is not a consumer class of its own: it advertises GL
memory on its video pads but cannot encode it. Drop Consumer::WebrtcSink
from decide(); the bridge now asks decide() with the caps the sink can
really take (raw video in system memory), so its gldownload follows
from the same rule the linker uses.

The NV12 conversion is a per-viewer cost saving, not a caps adaptation,
so it moves back into the bridge as its own step, with its tests.
Element names and order are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…LAUDE.md

One source for the block contract, the memory-format rules and the output
block input rules: CLAUDE.md imports the file so agents read it as before,
and CONTRIBUTING.md points human contributors to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@srperens
srperens merged commit 0c42d15 into main Oct 2, 2026
9 checks passed
@srperens
srperens deleted the refactor/video-input-decide branch October 2, 2026 11:56
srperens added a commit that referenced this pull request Oct 2, 2026
Resolve the conflict with #951, which moved decide() and the CUDA
memory feature into gst::video_adapt. The widening tests stay; the old
decide() tests already live in video_adapt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
srperens added a commit that referenced this pull request Oct 8, 2026
…nly while needed (#1033)

* fix(vision mixer): adapt GPU inputs to any pixel format, converting only 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>

* fix(vision mixer): list glupload's system-memory formats on GStreamer 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>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant