Repository navigation
perf(whep): convert a video input once, not once per viewer - #949
Merged
Merged
Conversation
webrtcsink builds a full encoding chain per consumer and each chain starts with its own videoconvert, so N viewers convert the same frames N times. The WHEP Output block pinned no raw format, so whatever arrived was what every consumer converted from — and each converted into whatever its encoder's caps preferred, which for VideoToolbox is 16-bit RGBA64_LE and for vp9enc is 4:4:4 Y444. The block now adapts its own video input from the negotiated caps, in the same one-shot probe that already downloaded GL memory: raw video that is not already 8-bit 4:2:0 gets a converter and an NV12 pin spliced in front of the sink. A hardware encoder's per-consumer converter then runs in passthrough. The VP9 and AV1 software encoders do not take NV12, so their consumers still convert, but from NV12 to I420 rather than from RGBA. Encoded input, other GPU memory types and formats deeper than 8 bits are left untouched, and a path that already carries NV12 or I420 gets nothing spliced at all. Measured on the block with two whepclientsrc consumers, 1080p30 RGBA in, CPU per extra consumer: H.265 through vtenc 23.7% and 39.2% of a core before, 1.4% and 1.3% after; VP9 through vp9enc 26.0% and 23.5% before, 17.2% and 14.9% after. The single conversion costs 4 to 8 points of a core once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # backend/src/gst/mod.rs
The converter video_input_bridge builds from the convert mode skipped gpu::configure_video_convert, so on macOS the RGBA to NV12 pass every WHEP viewer depends on ran on one core. A no-op off macOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bridge converts after any gldownload, so the frames are in system memory and autovideoconvert has no GPU path to win. Behind gldownload it also crashed or hung on GStreamer 1.24.2: 9 of 20 runs of gltestsrc ! GL RGBA ! queue ! gldownload ! autovideoconvert ! NV12 in an Ubuntu 24.04 container with surfaceless EGL, against 20 of 20 for videoconvert and for autovideoconvert without GL. CI runs 1.24.2, so video_input_bridge_gl_test segfaulted there 3 of 3 runs today, on this branch and on the unchanged commit that passed yesterday. The WHEP Output now always passes videoconvert, and the tests drop the autovideoconvert cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
srperens
added a commit
that referenced
this pull request
Oct 2, 2026
…move it (#950) vision_mixer_cuda_input_test crashed in about one run in four (11 of 40 locally, and on CI in PR #949) with an ElementFactory instance_of assertion or a segfault. Each test registered a stand-in cudadownload and removed it from the registry afterwards, so the missing-adapter test could see it absent. Registry::remove_feature frees the factory while the element class keeps a plain pointer to it, so the next registration or lookup under that name read freed memory; the lock between the tests did not help. The stand-in is now registered once per process and never removed, and the missing-adapter test moves to its own binary, vision_mixer_cuda_missing_test, where nothing is registered. The shared Media Player -> Vision Mixer harness moves to tests/vision_mixer_cuda/. The test bodies are unchanged. 40 of 40 runs of each binary pass. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 2, 2026
Merged
srperens
added a commit
that referenced
this pull request
Oct 2, 2026
video_input_bridge_gl_test needs a GL context. The Windows CI runner has
no OpenGL driver GStreamer can use ("No GL shader support available"),
so with STROM_REQUIRE_GST_PLUGINS set the test failed and turned the
Windows build on main red since #949. PR CI skips Windows, so #949 never
saw it. Linux CI keeps running the test, with surfaceless EGL.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #799. @wagenet's commit is included unchanged, with him as author. On top of it:
A merge of
main. It resolves the one conflict inbackend/src/gst/mod.rs: fix(vision_mixer): accept CUDA-memory inputs on the GPU backend #948 addedgl_input_frontnext togl_bridge, and this PR renamesgl_bridgetovideo_input_bridge, so both modules are kept.A fix from review: the converter that
video_input_bridgebuilds from the convert mode now goes throughgpu::configure_video_convert, as every other converter does. Without it, the RGBA to NV12 pass that every WHEP viewer depends on ran on one core on macOS, the platform this PR targets. It does nothing on other platforms.The converter is always
videoconvert, neverautovideoconvert. The bridge converts after anygldownload, so the frames are already in system memory andautovideoconverthas no GPU path to gain. Behindgldownload,autovideoconvertalso crashes or hangs on GStreamer 1.24.2, the version CI runs. In an Ubuntu 24.04 container with surfaceless EGL,gltestsrc ! GL RGBA ! queue ! gldownload ! autovideoconvert ! NV12failed 9 of 20 runs.videoconvertin the same place, andautovideoconvertwith no GL in the pipeline, both passed 20 of 20.video_input_bridge_gl_testsegfaulted on CI in 3 of 3 runs today, both on this branch and on the unchanged perf(whep): convert a video input once, not once per viewer #799 commit that passed yesterday, with the same image, packages and toolchain. The tests no longer haveautovideoconvertcases. No test can guard the WHEP Output's choice of converter, because CI has no GPU and its convert mode already picksvideoconvert.Tests run on the merged branch (macOS):
cargo test --lib video_input_bridge(19),cargo test --lib whep(30),gl_memory_link_test(7),vision_mixer_cuda_input_test(3),whip_whep_ice_transport_policy_test(9),pipeline_lifecycle_test(3): all passed. The pre-commit hook ran fmt and clippy, both clean. The fullcargo test --workspacewas not run locally.The original description from #799 follows.
What this changes
webrtcsinkbuilds a full encoding chain per consumer, and each chain starts with its ownvideoconvert(PayloadChainBuilder::build→make_converter_for_video_caps). The WHEP Outputblock pinned no raw format, so N viewers converted the same frames N times — each into whatever its
encoder's caps preferred, which measured as 16-bit
RGBA64_LEfor the VideoToolbox encoders and4:4:4
Y444forvp9enc.The block now adapts its own video input from the negotiated caps, in the one-shot CAPS probe that
already downloaded GL memory.
gl_bridgebecomesvideo_input_bridgeand makes both decisions inone splice:
gldownloadfirst where the input is GL memory, then a converter and anNV12capsfilter where the format is not already 8-bit 4:2:0. Left alone, deliberately:
videoconvertcould not link to it.the per-consumer conversion from one set of encoders to another.
CLAUDE.mdnamedgst::gl_bridgeas the example of deciding from negotiated caps; it now namesgst::video_input_bridge.devicesrc(upstream #842) takesGL_MEMORY_FEATUREfromvideo_input_bridge, which now exports it.Why NV12
NV12 is native to the hardware encoders:
vtenc_h264/vtenc_h265, NVENC and the VA encoders, withx264enclisting it alongside I420. The VP9 and AV1 software encoders (vp9enc,av1enc,rav1enc,svtav1enc) do not accept NV12, so their consumers still convert once each — from NV12to I420 instead of from RGBA. Per extra consumer, feeding the block each format directly:
I420 measured slightly cheaper on both of these; NV12 is for NVENC and VA, which were not measured.
I420 input is passed through, not converted.
A mixer-side pin would serve several sinks with one conversion, and on the GPU backend costs nothing
(#797 makes
output_formatwork there). This pin is conditional, so it is a no-opbehind such a mixer. Three WHEP sinks fed from one CPU mixer still convert three times.
Measured
Apple Silicon, 1080p30, RGBA in, two
whepclientsrcconsumers per run each pinned to one codec, CPUof the process running the real block (
backend/tests/whep_fanout_bench.rs), A/B interleaved, perextra consumer:
vtenc_h265vp9encThe one conversion costs 4 to 8 points of a core, so a single viewer already breaks even. Encoder
input caps confirm the mechanism: before,
vtenc_h265tookRGBA64_LEandvp9enctookY444;after,
NV12andI420. These are debug builds, wherewebrtcsinkis unoptimised, so absolutefigures may run high. H.264 consumers could not be measured: a
whepclientsrcoffering only H.264gets
a=inactivehere, with or without this change.Tests
gst::video_input_bridge, 19 unit tests: 12 on the caps predicates (5 carried over fromgl_bridge), 3 on whatbuild_adaptersreturns and in what order, and 4 live pipelines throughthe bridge — RGBA converted, NV12 and I420 untouched, RGBA converted by
autovideoconvert.backend/tests/video_input_bridge_gl_test.rspushes real GL memory (gltestsrc, RGBA) throughthe bridge with
videoconvertandautovideoconvert, and asserts the splice isgldownload ! <converter> ! capsfilterand the peer gets system-memory NV12. On Linux with nodisplay it requests a surfaceless EGL context (Mesa software GL); under
STROM_REQUIRE_GST_PLUGINS=1, as in CI, a missing element or GL context fails instead ofskipping. An Ubuntu 24.04 container with CI's packages runs that pipeline headless.
blocks::builtin::whep::tests::video_input_is_converted_before_the_sinkbuilds the real block,feeds it RGBA, and asserts
whepserversink'svideo_0pad negotiates NV12.Reverts: with
needs_format_conversionstubbed tofalse, the GL test, the RGBA pipeline tests andthe block test fail; with
needs_gl_downloadstubbed, the GL test fails.Ran on macOS on this head:
cargo test --lib(591 passed), the GL test withSTROM_REQUIRE_GST_PLUGINS=1,pipeline_lifecycle_test,openapi_test, clippy-D warnings,fmt. Linux CI passed on the previous head, which lacked the GL test; this head's CI is its first
Linux run. Not run: Windows, CUDA hosts, the rest of the integration suite.
whep_fanout_bench.rsis#[ignore]d (it needswhepclientsrcand spawns consumer processes) andguards nothing. It fails a run in which any consumer received no RTP buffers.
Not validated on the rig: no browser-level confirmation.
🤖 Generated with Claude Code