Skip to content

Add Gamer Mode: low-latency WebRTC streaming via webrtcbin#220

Draft
anagnorisis2peripeteia wants to merge 2 commits into
pikvm:masterfrom
anagnorisis2peripeteia:feature/gamer-mode-webrtc
Draft

Add Gamer Mode: low-latency WebRTC streaming via webrtcbin#220
anagnorisis2peripeteia wants to merge 2 commits into
pikvm:masterfrom
anagnorisis2peripeteia:feature/gamer-mode-webrtc

Conversation

@anagnorisis2peripeteia
Copy link
Copy Markdown

@anagnorisis2peripeteia anagnorisis2peripeteia commented Jun 1, 2026

Gamer Mode: Low-Latency WebRTC Streaming

Direct WebRTC streaming mode that bypasses the Janus SFU entirely, using GStreamer's webrtcbin for a 1:1 P2P connection between the Pi and the browser.

Why

Current pipeline (ustreamer -> Janus -> browser) adds latency through Janus's jitter buffers and SFU processing. For single-user / gaming scenarios that overhead is unnecessary.

Default:  capture -> ustreamer -> Janus (SFU) -> browser
Gamer:    capture -> GStreamer -> webrtcbin (P2P) -> browser

How to enable

# /etc/kvmd/override.yaml
kvmd:
    streamer:
        mode: gamer
        gamer:
            device: /dev/video0
            fps: 60
            bitrate: 5000

Then select "WebRTC" in the stream mode picker.

Components

  • contrib/gamer-mode/gamer_streamer.py: GStreamer pipeline + signaling. Two transports:
    • --port N standalone (built-in aiohttp server + embedded HTML, for development).
    • default (no --port) stdio JSON-line frames, used when kvmd parents the process.
  • kvmd/apps/kvmd/streamer/gamer.py: GamerStreamer subprocess manager. Spawns the script, reads stdout for SDP / ICE / signal events, writes browser responses to stdin.
  • kvmd/apps/_scheme.py: streamer.mode + streamer.gamer.* config options.
  • kvmd/apps/kvmd/__init__.py: branch on streamer.mode at startup.
  • kvmd/apps/kvmd/server.py: webrtc_signal WebSocket event handler bridging browser <-> subprocess.
  • web/share/js/kvm/stream_webrtc.js: browser-side WebRTC client (native RTCPeerConnection, no Janus lib). Signaling rides on the existing /ws.
  • web/share/js/kvm/stream.js: "webrtc" stream-mode entry.

Latency optimisations

  • latency=0 on webrtcbin (no jitter buffer)
  • tune=zerolatency / speed-preset=ultrafast on x264enc fallback
  • config-interval=-1 on RTP payloader (SPS/PPS with every keyframe)
  • No SFU hop (direct P2P ICE)
  • Auto-detects Pi v4l2h264enc hardware encoder; falls back to x264enc only when absent

V4 multi-port switch handling

The pipeline uses an input-selector with a live v4l2src (sink_0) and a videotestsrc pattern=black (sink_1). A bus error from the capture source (e.g. EDID renegotiation during a port switch) flips the selector to the black branch without tearing down the encoder or the WebRTC session. A recovery timer polls the capture branch every second and swaps back as soon as the device returns to PLAYING. The browser receives signal_lost and signal_restored frames in the meantime.

What is NOT yet done

  • Runtime mode toggle (currently config-time only; both pipelines would need to coexist)
  • Audio support
  • Tested on real Pi hardware (developed against the Lima VM with the x264enc fallback; v4l2h264enc path is auto-selected but unverified)

Dependencies

  • GStreamer 1.x with gst-plugins-bad (webrtcbin)
  • PyGObject (gi.repository.Gst, GstSdp, GstWebRTC)
  • aiohttp (already a kvmd dep; only used by the standalone --port mode)

Replaces the ustreamer + Janus pipeline with a direct GStreamer pipeline
for minimum-latency 1:1 video streaming:

  v4l2src -> v4l2h264enc (hw) or x264enc (sw fallback) -> rtph264pay -> webrtcbin

Key differences from the Janus path:
- No SFU middleman (direct P2P WebRTC)
- No jitter buffers (latency=0 on webrtcbin)
- Zero-latency H.264 encoding (tune=zerolatency)
- Auto-detects Pi hardware encoder, falls back to software

Components:
- contrib/gamer-mode/gamer_streamer.py: standalone GStreamer + signaling server
- kvmd/apps/kvmd/streamer/gamer.py: kvmd subprocess manager
- web/share/js/kvm/stream_webrtc.js: browser WebRTC client (no Janus lib)
- web/share/js/kvm/stream.js: added "webrtc" mode to stream selector
- streamer.mode config option ("default" | "gamer"). When "gamer", kvmd
  instantiates GamerStreamer instead of the ustreamer-backed Streamer.
  streamer.gamer.{device,fps,bitrate} configures the pipeline.

- GamerStreamer now spawns the gamer_streamer.py subprocess and bridges
  JSON-line signaling over its stdin/stdout. The standalone HTTP server
  / embedded HTML page mode (--port) is preserved for development.

- New /ws event "webrtc_signal" relays SDP / ICE frames between the
  browser and the GamerStreamer subprocess. The frontend client uses
  kvmd's /ws (no separate connection on a sidecar port).

- V4 mux signal-loss recovery: the pipeline now feeds an input-selector
  between live v4l2 capture and a videotestsrc black-frame fallback.
  A bus error from v4l2src (e.g. EDID renegotiation during a port
  switch) swaps to the fallback without tearing down the WebRTC
  session; a recovery timer polls the capture branch and swaps back
  when the signal returns. The browser sees signal_lost/signal_restored
  signaling frames in the meantime.
@RSATom
Copy link
Copy Markdown
Contributor

RSATom commented Jun 1, 2026

Did you do any test about latency difference between Janus and GStreamer's webrtcbin based implementations?

@mdevaev
Copy link
Copy Markdown
Member

mdevaev commented Jun 1, 2026

Hi. For things like this, I would prefer that you contact me - I'm open to communication and discussion of issues and available in the Discord public chat. We have invested a lot of time in latency research issues and outlined the main points in the article: https://docs.pikvm.org/latency. I recommend that you take a step back and explore it. Because some of the assumptions that your PR is based on seem wrong to me. In particular, there is no jitter buffer in Janus. The listed H.264 optimizations have been used for a long time.

Anyway, as a justification for this PR, I would like to see a benchmark. You are introducing a huge layer of new code where it would be possible to do a spot optimization of existing components. We did a lot, but we could still have missed something, and a contribution in this direction would be more useful than adding a completely alternative backend.

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.

3 participants