The problem
Streaming a 1080p60 program built from 8×1080p Zoom sources cannot sustain 60 fps to the encoder. It runs at speed ≈ 0.73–0.95x (44–57 fps), so the stream falls progressively behind and YouTube reports "not receiving enough video to maintain smooth streaming." Owner's point, correct: vMix and Vizrt Vectar do this on comparable hardware without issue.
What it is NOT (all measured this session, 2026-09-13)
- Not bandwidth — 48 Mbps up; lowering the target bitrate 10→6 Mbps changed nothing.
- Not CPU — total 50%, no single core pegged (top ~70%).
- Not GPU / NVENC — GPU 27–30%, NVENC idle at 3–7% (
nvidia-smi utilization.encoder), encoder.stats.averageFps = whatever it's fed. The encoder is starved, not the bottleneck.
- Not the network path to YouTube — the decisive test: streaming to a localhost RTMP sink (no internet, zero RTT) is also 0.76x / 45 fps. That removes YouTube entirely and it's still capped.
- Not
-re pacing — removed from both video and audio pipes; no change.
What it IS
The architecture: the compositor produces the program frame on the GPU, then we read it back and pipe ~186 MB/s of raw NV12 (1920×1080 × 60 × 3.1 MB) through a Windows pipe to a separate ffmpeg.exe, which reads it single-threaded, (was) converting it, and submits to NVENC. That raw-pipe hop is the ceiling — NVENC sits idle waiting for frames that arrive at ~45–57 fps.
vMix / Vectar keep the frame on the GPU and hand the GPU surface straight to NVENC (NVENC API, or hwupload/CUDA), never round-tripping raw uncompressed video through a CPU pipe to another process.
Fixes landed this session (real, partial)
- nv12 passthrough (was forcing
-pix_fmt yuv420p → per-frame CPU swscale on an idle NVENC): measured 0.733x → 0.955x in one test. Correct regardless. (deploy branch)
-re removed from the real pipes; FFmpeg -stats telemetry added (the ground truth that ended the guessing); video stdin pipe buffer 4 KB→8 MB (unproven).
These help but do not reach a stable 1.0x — the raw-pipe hop remains.
The real fix (this issue)
Encode the program on the GPU and feed NVENC directly, eliminating the raw-frame CPU pipe to an external process. Options, roughly increasing effort:
- In-process NVENC from the compositor's shared texture (the
exportVcamSharedTexture GPU tap already exists — a GPU→NVENC encode path instead of GPU→CPU readback→pipe→ffmpeg).
- FFmpeg with a hardware input (
hwupload_cuda / a shared-handle import) so the raw frame never crosses a CPU pipe.
- At minimum, replace the anonymous pipe with a shared-memory transport (removes the 186 MB/s pipe I/O, but keeps the external process + a copy).
Immediate mitigation for the owner
The pipeline sustains ~45–57 fps comfortably, so 1080p30 streaming is rock-solid today (30 << 45). A steady 30 fps is smooth on YouTube; 60 fps needs the GPU-direct encode above. Worth wiring a stream-fps setting decoupled from the 60 Hz program if it isn't already.
Evidence: FFmpeg -stats logs + nvidia-smi + the localhost-sink A/B, session 2026-09-13.
The problem
Streaming a 1080p60 program built from 8×1080p Zoom sources cannot sustain 60 fps to the encoder. It runs at speed ≈ 0.73–0.95x (44–57 fps), so the stream falls progressively behind and YouTube reports "not receiving enough video to maintain smooth streaming." Owner's point, correct: vMix and Vizrt Vectar do this on comparable hardware without issue.
What it is NOT (all measured this session, 2026-09-13)
nvidia-smi utilization.encoder),encoder.stats.averageFps= whatever it's fed. The encoder is starved, not the bottleneck.-repacing — removed from both video and audio pipes; no change.What it IS
The architecture: the compositor produces the program frame on the GPU, then we read it back and pipe ~186 MB/s of raw NV12 (1920×1080 × 60 × 3.1 MB) through a Windows pipe to a separate
ffmpeg.exe, which reads it single-threaded, (was) converting it, and submits to NVENC. That raw-pipe hop is the ceiling — NVENC sits idle waiting for frames that arrive at ~45–57 fps.vMix / Vectar keep the frame on the GPU and hand the GPU surface straight to NVENC (NVENC API, or
hwupload/CUDA), never round-tripping raw uncompressed video through a CPU pipe to another process.Fixes landed this session (real, partial)
-pix_fmt yuv420p→ per-frame CPU swscale on an idle NVENC): measured 0.733x → 0.955x in one test. Correct regardless. (deploy branch)-reremoved from the real pipes; FFmpeg-statstelemetry added (the ground truth that ended the guessing); video stdin pipe buffer 4 KB→8 MB (unproven).These help but do not reach a stable 1.0x — the raw-pipe hop remains.
The real fix (this issue)
Encode the program on the GPU and feed NVENC directly, eliminating the raw-frame CPU pipe to an external process. Options, roughly increasing effort:
exportVcamSharedTextureGPU tap already exists — a GPU→NVENC encode path instead of GPU→CPU readback→pipe→ffmpeg).hwupload_cuda/ a shared-handle import) so the raw frame never crosses a CPU pipe.Immediate mitigation for the owner
The pipeline sustains ~45–57 fps comfortably, so 1080p30 streaming is rock-solid today (30 << 45). A steady 30 fps is smooth on YouTube; 60 fps needs the GPU-direct encode above. Worth wiring a stream-fps setting decoupled from the 60 Hz program if it isn't already.
Evidence: FFmpeg
-statslogs +nvidia-smi+ the localhost-sink A/B, session 2026-09-13.