Skip to content

feat(stream): carry HID input over a WebRTC data channel - #118

Closed
szdziedzic wants to merge 1 commit into
szdziedzic-claude/webrtc-playout-windowfrom
szdziedzic-claude/webrtc-input-channel
Closed

szdziedzic wants to merge 1 commit into
szdziedzic-claude/webrtc-playout-windowfrom
szdziedzic-claude/webrtc-input-channel

Conversation

@szdziedzic

@szdziedzic szdziedzic commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

On the WebRTC transport, viewers open an ordered, reliable data channel labeled input alongside the video. It carries the same [tag][JSON] HID frames as the /ws socket, and the server funnels both into the same DeviceSession dispatch. The WebSocket stays open for screen-config pushes and as the input fallback.

Why

Hosted deployments reach the helper WebSocket through the ngrok HTTP tunnel: every touch move crosses extra relay hops on ordered TCP, where one lost segment stalls the whole gesture stream for a retransmit round trip. Production users saw gestures freeze. The WebRTC media path had already negotiated a direct (or TURN) UDP route between the same two machines, so input now uses it.

How

  • Native: the peer-connection delegate accepts a channel labeled input, retains it until it closes, and hands each message to CaptureEngine's input handler; SimCapture.subscribeInput marshals frames onto the JS thread through the existing NodeAsyncQueue without blocking a libwebrtc thread. Channels with any other label are closed.
  • Server: DeviceSession subscribes to native input when the transport is WebRTC and feeds it into handleHidMessage, the same path /ws uses.
  • Client: useWebRtcStream creates the channel before the offer so SCTP negotiates with the video. HidTransportRouter pins every begin/move/end of one gesture to the transport it began on and only re-evaluates at gesture boundaries, which closes the cross-channel ordering hazard that motivated removing data-channel input earlier. A channel that dies mid-gesture falls back to the socket immediately.
  • Verified: router unit tests; werift e2e tests that negotiate a real peer, send a tap over the channel and see it in the event log, and confirm an unknown label is closed.

Part of the tap-to-photon stack (bottom → top): latency probe → send frames on arrival → 240 Hz seed poll → bitrate floor → playout window → data-channel input → lossy moves lane. Everything is on by default, so a package built from any layer works with the unchanged EAS launch flags (--transport webrtc --webrtc-codec vp8 --max-dimension 960 --video-bitrate 6000000 --video-fps 60).

Measured with scripts/latency-probe (M5 Pro, loopback, iPhone 17 / iOS 26.5, 25 taps): 0.1.51 p50 32–37 ms / p95 ≈ 43 ms → top of stack p50 27 ms / p95 38 ms, no outliers above 40 ms.

🤖 Generated with Claude Code

On the WebRTC transport, viewers now open one ordered, reliable data
channel labeled "input" alongside the video. It carries the exact
[tag][JSON] frames the /ws socket carries, and the server funnels both
into the same DeviceSession HID dispatch. Input therefore rides the
negotiated media path (UDP, direct or TURN) instead of the tunneled TCP
WebSocket, whose relay hops and head-of-line blocking made gestures
freeze on hosted deployments.

The WebSocket stays open in every mode: it still carries screen-config
pushes and remains the input fallback. A gesture-boundary router pins
every begin/move/end of one gesture to the transport it began on, which
closes the cross-channel ordering hazard that motivated removing
data-channel input previously. The server closes data channels with any
other label, and libwebrtc's delegate thread only enqueues messages
onto the existing threadsafe-function queue.

Verified end to end: werift-based tests negotiate a real peer, send a
tap over the channel, and assert it lands in the event log; a live
browser session against the built CLI opened Settings and scrolled via
taps and drags delivered over the channel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@szdziedzic szdziedzic closed this Sep 29, 2026
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.

1 participant