Skip to content

Streaming can't sustain 1080p60 to the encoder — raw-frame CPU pipe to ffmpeg is the ceiling; GPU-direct NVENC needed (vMix/Vectar parity) #521

Description

@iamfatness

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:

  1. 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).
  2. FFmpeg with a hardware input (hwupload_cuda / a shared-handle import) so the raw frame never crosses a CPU pipe.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    backlogRanked in docs/BACKLOG.md

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions