Skip to content

feat: add RTSP server, Chromecast WebRTC casting, and multi-session support#1329

Open
qoole wants to merge 3 commits intojetkvm:devfrom
qoole:feat/rtsp-chromecast-multi-session
Open

feat: add RTSP server, Chromecast WebRTC casting, and multi-session support#1329
qoole wants to merge 3 commits intojetkvm:devfrom
qoole:feat/rtsp-chromecast-multi-session

Conversation

@qoole
Copy link
Copy Markdown

@qoole qoole commented Mar 24, 2026

Summary

  • RTSP server: Exposes HDMI capture as rtsp://<device-ip>:8554/stream using gortsplib v5. Supports multiple concurrent clients with SPS/PPS caching for late-joiners. Configurable via Settings > Video (enable/disable, port).
  • Chromecast casting: Low-latency WebRTC streaming to Chromecast / Google TV devices via a custom Cast receiver application. Uses CASTV2 protocol with mDNS device discovery. Cast button added to the web UI toolbar. See cast-receiver/README.md for setup.
  • Multi-session WebRTC: Replaces the currentSession singleton with a mutex-protected session registry. Video frames fan out to all active sessions, allowing browser and Chromecast to stream simultaneously.

New files

File Purpose
rtsp.go RTSP server — gortsplib, SPS/PPS caching, multi-client fanout
chromecast.go mDNS discovery, CASTV2 protocol, custom receiver launch
cast-receiver/index.html Custom Cast receiver — WebRTC player for Chromecast
cast-receiver/README.md Setup guide for custom Chromecast receiver
ui/src/components/CastButton.tsx Cast button with device picker
ui/src/hooks/useCast.ts Hook wrapping cast JSON-RPC calls

Modified files

  • native.go — Video fan-out to all WebRTC sessions + RTSP
  • web.go — Session registry (replaces singleton), gosync.RWMutex-protected
  • webrtc.go — Session cleanup uses removeSession() instead of clearing global
  • cloud.gohandleSessionRequest returns session, uses addSession()
  • config.go — Added RTSPEnabled, RTSPPort, CastReceiverAppID fields
  • jsonrpc.go — Registered RTSP, Cast config, and casting RPC methods
  • video.go, usb.go, serial.go, network.go, hw.go, ota.go — Event broadcasting to all sessions via forEachSession()
  • ui/src/routes/devices.$id.settings.video.tsx — RTSP + Cast App ID settings
  • ui/src/components/ActionBar.tsx — Added CastButton to toolbar

Test plan

  • ffplay rtsp://<device-ip>:8554/stream — verify RTSP playback
  • Multiple simultaneous RTSP clients receive frames
  • Cast button discovers Chromecast devices via mDNS
  • Casting starts WebRTC stream on Chromecast with low latency
  • Browser and Chromecast stream simultaneously without conflict
  • Stopping cast disconnects cleanly
  • RTSP enable/disable and port change persist across reboots
  • Cast Receiver App ID configurable via Settings > Video

🤖 Generated with Claude Code


Note

High Risk
Adds new unauthenticated signaling endpoint plus new RTSP server and Chromecast control path, and refactors core WebRTC session lifecycle from a singleton to a multi-session registry; regressions could impact streaming stability and expand network-exposed surface area.

Overview
Adds RTSP streaming of the HDMI capture (via gortsplib) with live SPS/PPS updates for late joiners, plus JSON-RPC and UI settings to enable/disable and change the port.

Introduces Chromecast / Google TV casting by adding mDNS discovery and CASTV2 launch/control of a custom receiver app (cast-receiver/index.html + setup docs), along with UI controls (toolbar Cast button, preferred device selection) and related JSON-RPC methods.

Refactors WebRTC from a currentSession singleton to a mutex-protected multi-session registry, fanning out video frames and JSON-RPC event broadcasts to all sessions, and adjusting session creation/cleanup and update/OTA gating accordingly; also adds an unauthenticated /webrtc/signaling/cast route for Chromecast receivers.

Written by Cursor Bugbot for commit 8ec10cf. This will update automatically on new commits. Configure here.

…upport

Add three major features to the JetKVM firmware:

1. **RTSP server** (`rtsp.go`): Exposes the HDMI capture H.264 stream as
   `rtsp://<device-ip>:8554/stream` using gortsplib v5. Supports multiple
   concurrent clients with SPS/PPS caching for late-joiners. Configurable
   via settings UI (enable/disable, port).

2. **Chromecast casting** (`chromecast.go`): Low-latency WebRTC streaming
   to Chromecast / Google TV devices via a custom Cast receiver application.
   Uses the CASTV2 protocol directly with a buffered connection wrapper to
   prevent channel deadlocks. The receiver HTML (`cast-receiver/`) is hosted
   externally on any HTTPS server and establishes a direct WebRTC connection
   to the JetKVM. Includes mDNS device discovery and a Cast button in the
   web UI toolbar. See `cast-receiver/README.md` for setup instructions.

3. **Multi-session WebRTC**: Replaces the `currentSession` singleton with a
   mutex-protected session registry. Video frames are fanned out to all
   active WebRTC sessions, allowing the browser and Chromecast to stream
   simultaneously. All event broadcasting (USB, serial, OTA, network, video)
   updated to iterate all sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 24, 2026

CLA assistant check
All committers have signed the CLA.

qoole and others added 2 commits March 24, 2026 21:20
- Add CastPreferredDevice to config (persisted to kvm_config.json)
- Star icon in Cast dropdown to set/clear preferred device
- Preferred device shown in Settings > Video with clear button
- rpcQuickCast: casts to preferred device, or first discovered if none set
- rpcSetPreferredCastDevice: save/clear preferred device via RPC
- LCD touch screen can call quickCast for one-tap casting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The /webrtc/signaling/client endpoint is behind auth middleware, which
blocks the Chromecast receiver (cannot send cookies/tokens). Add
/webrtc/signaling/cast as an unauthenticated endpoint for cast receivers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.


// Unauthenticated WebRTC signaling for Chromecast cast receiver.
// The cast receiver runs on a Chromecast and cannot send auth cookies/tokens.
r.GET("/webrtc/signaling/cast", handleLocalWebRTCSignal)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unauthenticated endpoint exposes full device control

High Severity

The /webrtc/signaling/cast route is registered outside the protected middleware group, making it completely unauthenticated. It reuses handleLocalWebRTCSignal, which establishes a full WebRTC session with video streaming, RPC commands, and HID input control. Any device on the network can connect to this endpoint and gain full control of the KVM, completely bypassing password authentication.

Fix in Cursor Fix in Web

}

// Connect to the app's transport
if err := conn.Send(3, &cast.ConnectHeader, castSender, transportID, nsConnection); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global ConnectHeader mutated instead of local copy

Medium Severity

At line 254, a local copy of cast.ConnectHeader is correctly made before passing its address. But at line 291, &cast.ConnectHeader is passed directly, which takes the address of the package-level global and allows Send to mutate it (e.g., via SetRequestId). This corrupts shared state and can cause incorrect behavior on subsequent cast connections.

Fix in Cursor Fix in Web

currentSession = nil
}
scopedLogger.Debug().Msg("ICE Connection State is closed, cleaning up session")
removeSession(session)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyboard macro not canceled on session disconnect

Medium Severity

The old code called cancelKeyboardMacro() when a session was replaced or closed. The new multi-session code in webrtc.go only calls removeSession(session) on ICE connection close, dropping the cancelKeyboardMacro() call entirely. A running keyboard macro will continue executing after the session that initiated it disconnects.

Fix in Cursor Fix in Web


// WriteNALU receives a raw H.264 Annex B frame and fans it out to all RTSP clients.
func (rs *RTSPServer) WriteNALU(frame []byte, duration time.Duration) {
rs.pts += duration
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data race on RTSPServer.pts field

Low Severity

rs.pts is incremented in WriteNALU without any synchronization. While the mu RWMutex protects sps/pps, the pts field is read and written outside any lock. If WriteNALU is ever called from multiple goroutines (or concurrently with any reader), this is a data race.

Fix in Cursor Fix in Web

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.

2 participants