Skip to content

Implement webRTC Transport layer for screen sharing and for Low-Latency Input#351

Open
PinJinx wants to merge 3 commits into
AOSSIE-Org:mainfrom
PinJinx:WebRTC_impl
Open

Implement webRTC Transport layer for screen sharing and for Low-Latency Input#351
PinJinx wants to merge 3 commits into
AOSSIE-Org:mainfrom
PinJinx:WebRTC_impl

Conversation

@PinJinx
Copy link
Copy Markdown
Contributor

@PinJinx PinJinx commented May 29, 2026

Addressed Issues:

Fixes #208

Description

This PR fundamentally overhauls the WebRTC and WebSocket transport architecture to ensure stable, ultra-low-latency connections. (Note: WebSockets are currently retained as a signaling fallback but are slated to be replaced by HTTP in a future update).

Key Changes:

  • State-Driven Screen Mirroring: Introduces a "Consumer-First" WebRTC architecture model. The Desktop host now actively listens for lifecycle events (consumer_joined / consumer_left) to dynamically initialize and tear down the screen sharing stream.

  • Dual-Channel Input Routing: Formalized the transport layer to eliminate network bottlenecking. High-frequency inputs (move, scroll, zoom, touch) are now routed through an unordered, UDP-like DataChannel (dc-unordered) to prevent head-of-line blocking. Discrete inputs (click, key) are routed through a reliable, TCP-like ordered DataChannel (dc-ordered).

  • Graceful Transport Fallback: Added a catch wrapper to the input router that seamlessly falls back to WebSocket signaling if the WebRTC DataChannels drop, fail, or are still negotiating.

Screenshots/Recordings:

nil

Functional Verification

  • Please check off the behaviors verified with this change.

Screen Mirror

  • Screen MIrror works.

Authentication

  • Connection doesn't work without a valid token.

Basic Gestures

  • One-finger tap: Verified as Left Click.

  • Two-finger tap: Verified as Right Click.

  • Click and drag: Verified selection behavior.

  • Pinch to zoom: Verified zoom functionality (if applicable).

Modes & Settings

  • Cursor mode: Cursor moves smoothly and accurately.

  • Scroll mode: Page scrolls as expected.

  • Sensitivity: Verified changes in cursor speed/sensitivity settings.

  • Copy and Paste: Verified both Copy and Paste functionality.

  • Invert Scrolling: Verified scroll direction toggles correctly.

Advanced Input

  • Key combinations: Verified "hold" behavior for modifiers (e.g., Ctrl+C) and held keys are shown in buffer.

  • Keyboard input: Verified Space, Backspace, and Enter keys work correctly.

  • Glide typing: Verified path drawing and text output.

  • Voice input: Verified speech-to-text functionality for full sentences.

  • Backspace doesn't send the previous input.

Any other gesture or input behavior introduced:

  • New Gestures: Verified any other gesture or input behavior introduced in this PR.

Additional Notes:

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.

  • My code follows the project's code style and conventions

  • I have performed a self-review of my own code

  • I have commented my code, particularly in hard-to-understand areas

  • If applicable, I have made corresponding changes or additions to the documentation

  • If applicable, I have made corresponding changes or additions to tests

  • My changes generate no new warnings or errors

  • I have joined the and I will share a link to this PR with the project maintainers there

  • I have read the

  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.

  • Incase of UI change I've added a demo video.

⚠️ AI Notice - Important!
We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact.

Summary by CodeRabbit

  • New Features

    • Integrated WebRTC peer streaming and real-time signaling.
    • Added native source picker for desktop capture selection.
  • Refactor

    • Screen mirroring now uses direct video stream playback instead of encoded-frame rendering.
    • Provider sharing moved to WebRTC with optimized data channels and adaptive bitrate/codec handling.
  • Bug Fixes / Reliability

    • Improved connection lifecycle, consumer/provider notifications, larger signaling payload handling, and ping/pong latency reporting.
  • Documentation

    • README updated to clarify device support and usage scenarios.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

Walkthrough

Architectural shift from WebSocket binary frame streaming to native WebRTC peer connections for screen mirroring. Replaces canvas-based consumer rendering and manual frame capture with WebRTC MediaStream binding to video elements, adds server-side signaling routing, and refactors provider/consumer state tracking through connection context and event subscriptions.

Changes

WebRTC Media Streaming Implementation

Layer / File(s) Summary
WebRTC Peer Connection Hook
src/hooks/useWebRTC.tsx
New hook managing RTCPeerConnection lifecycle, signaling message handling (offer/answer/ICE), dual labeled data channels (ordered/unordered), adaptive media sender configuration (bitrate limiting, codec preference H.264 > VP9 > AV1), and forwarding incoming tracks via callback.
Connection Provider WebRTC Integration
src/contexts/ConnectionProvider.tsx
Extends connection context with pcRef, createPeerConnection, subscribeMirrorStream, sendInput, and configureMediaSender. Integrates useWebRTC, routes WebRTC signaling types to handleSignalingMessage, forwards MediaStreams to mirror subscribers, and refactors WebSocket handlers to async.
Mirror Stream Consumer
src/hooks/useMirrorStream.ts, src/components/Trackpad/ScreenMirror.tsx
Refactors hook to subscribe to WebRTC MediaStream via connection context instead of decoding binary WebSocket frames. Binds stream to video element, tracks hasStream, and updates UI to toggle video visibility and standby spinner based on stream state.
Capture Provider WebRTC
src/hooks/useCaptureProvider.ts
Reimplements as event-driven WebRTC provider: acquires desktop stream via Electron picker (fallback getDisplayMedia), creates peer connection on consumer_joined, sets up dual data channels for JSON message relay, adds captured track, applies bitrate/codec optimization, and implements clean stopSharing lifecycle.
Input Signaling Path
src/hooks/useRemoteConnection.ts
Routes input messages through sendInput (WebRTC data channel) instead of direct WebSocket, enabling peer-to-peer input when connection is established.
Server Signaling and State
src/server/websocket.ts
Disables binary frame relaying; adds text signaling broadcast for offer, answer, ice-candidate, lan-info. Implements provider/consumer state tracking: start-mirror/stop-mirror notify providers of consumer changes, start-provider notifies if consumer exists, disconnect sends consumer_left to providers.
Root Route Cleanup
src/routes/__root.tsx
Removes local connection status and sharing startup logic from DesktopCaptureProvider; initialization is now event-driven via useCaptureProvider subscriptions.
Docs: README
README.md
Introduces updated project wording describing supported client types and repositioning toward standardization for cloud PC/cloud gaming providers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AOSSIE-Org/Rein#212: Earlier canvas/WebSocket mirror implementation that this PR replaces with WebRTC video binding.
  • AOSSIE-Org/Rein#215: Related changes to src/server/websocket.ts provider/consumer lifecycle and notifications.
  • AOSSIE-Org/Rein#244: Overlapping changes to ping/pong heartbeat and latency plumbing in connection provider and server.

Suggested labels

Typescript Lang, Documentation

Poem

🐰 I hopped from canvas to stream,

No more blobs — the pixels gleam.
Peers now chat and channels hum,
Video wakes, the mirror’s come.
A tiny rabbit claps — behold the stream!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR partially addresses #208: it migrates screen mirroring to WebRTC and adds hardware-accelerated encoding support. However, WebSockets are retained as signaling fallback rather than completely purged, and useCaptureProvider.ts still exists but with refactored internals. Clarify whether WebSocket retention as signaling fallback and useCaptureProvider.ts refactoring (vs. removal) align with issue #208's stated objective to 'completely purge WebSocket and useCaptureProvider.ts hook'.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main architectural change: implementing WebRTC for transport of screen sharing and low-latency input.
Description check ✅ Passed The description comprehensively covers objectives, key changes, and functional verification. However, documentation and test updates are not provided, and UI demo is not applicable.
Out of Scope Changes check ✅ Passed All code changes are tightly scoped to WebRTC/dual-channel input implementation and transport layer refactoring. README update clarifies project scope but remains topically aligned. No extraneous changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/hooks/useRemoteConnection.ts (1)

9-16: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Route combo through sendInput for consistent transport.

sendCombo still calls send, while the hook’s public send is wired to sendInput. In ConnectionProvider, send is WebSocket-only (wsRef.current.send(JSON.stringify(msg))), so combo will take a different path than other inputs.

🔧 Route combos through the same input path
 	const sendCombo = (msg: string[]) => {
-		send({
+		sendInput({
 			type: "combo",
 			keys: msg,
 		})
 	}

Confirm sendInput is intended to be the unified path for all VALID_INPUT_TYPES (including "combo"), and that routing combo through it matches the server’s expectations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useRemoteConnection.ts` around lines 9 - 16, sendCombo currently
calls send directly which bypasses the unified path used by sendInput; update
sendCombo to call sendInput with a payload matching the other inputs (e.g.,
sendInput({ type: "combo", keys: msg })) so combos use the same transport and
validation as other VALID_INPUT_TYPES, and verify that sendInput and
ConnectionProvider (wsRef.current.send(JSON.stringify(...))) accept the "combo"
type and that the server expects that shape; adjust any input
typing/VALID_INPUT_TYPES if needed to include "combo".
src/components/Trackpad/ScreenMirror.tsx (1)

14-17: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Externalize user-visible strings for internationalization.

The TEXTS object contains hardcoded user-facing strings that should be externalized to i18n resource files. As per coding guidelines, user-visible strings should be externalized for internationalization.

🌐 Example i18n integration pattern
+import { useTranslation } from 'next-i18next' // or your i18n library

-const TEXTS = {
-	WAITING: "Waiting for screen...",
-	AUTOMATIC: "Mirroring will start automatically",
-}

 export const ScreenMirror = ({
 	scrollMode,
 	isTracking,
 	handlers,
 }: ScreenMirrorProps) => {
+	const { t } = useTranslation('common')
 	const { status } = useConnection()
 	const videoRef = useRef<HTMLVideoElement>(null)
 	const { hasStream } = useMirrorStream(videoRef, status)

 	return (
 		<div className="absolute inset-0 flex items-center justify-center bg-black overflow-hidden select-none touch-none">
 			{/* ... */}
 			{!hasStream && (
 				<div className="absolute inset-0 flex flex-col items-center justify-center text-gray-400 gap-4">
 					<div className="loading loading-spinner loading-lg text-primary" />
 					<div className="text-center px-6">
-						<p className="font-semibold text-lg">{TEXTS.WAITING}</p>
-						<p className="text-sm opacity-60">{TEXTS.AUTOMATIC}</p>
+						<p className="font-semibold text-lg">{t('mirror.waiting')}</p>
+						<p className="text-sm opacity-60">{t('mirror.automatic')}</p>
 					</div>
 				</div>
 			)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Trackpad/ScreenMirror.tsx` around lines 14 - 17, TEXTS
contains hardcoded user-facing strings; replace it with i18n keys and use the
app's translation helper (e.g., the useTranslation hook or t function) instead
of TEXTS so strings live in resource files; find the TEXTS constant in
ScreenMirror.tsx and: remove the hardcoded entries ("Waiting for screen...",
"Mirroring will start automatically"), add corresponding keys to the i18n
resources, and update all usages in the ScreenMirror component to call
t('your.key.waiting') and t('your.key.automatic') (or the project's equivalent
translation accessor).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Trackpad/ScreenMirror.tsx`:
- Around line 31-39: Add playback error handling and accessibility to the video
element in ScreenMirror.tsx: attach an onError handler (e.g., handleVideoError)
to catch stream/playback failures and surface/log the error and update state
(related to videoRef and hasStream), add an onLoadedMetadata handler (e.g.,
handleLoadedMetadata) to confirm successful attachment and set any ready state,
and include an aria-label (e.g., "Screen share video") to improve screen-reader
accessibility; implement the two handler functions in the component to log
errors, update hasStream/ready flags, and perform any cleanup or retry logic as
needed.

In `@src/hooks/useCaptureProvider.ts`:
- Around line 58-86: The code in useCaptureProvider.ts calls (window as
ElectronWindow).electron.showSourcePicker() and passes the returned sourceId
directly into navigator.mediaDevices.getUserMedia; update this by ensuring the
preload uses contextBridge/exposeInMainWorld to expose showSourcePicker (do not
attach to window directly), validate and sanitize the returned sourceId in the
code that handles sourceId (check type, non-empty string, match an expected
pattern or whitelist of known IDs, and reject/ fallback if it fails validation),
and add a defensive branch in the showSourcePicker handling to fall back to
getDisplayMedia when sourceId is invalid; also confirm the app’s CSP allows
media capture. Reference symbols: useCaptureProvider.ts, ElectronWindow,
electron.showSourcePicker, sourceId, navigator.mediaDevices.getUserMedia, and
getDisplayMedia.
- Around line 103-109: The handler handleIncomingInput currently JSON.parses
event.data and blindly calls send; add basic validation after parsing to ensure
the payload is an object with the expected shape (at minimum check that msg is
an object and has a string 'type' property, and any other required fields for
your protocol) and only call send(msg) when validation passes; if validation
fails, log a warning and do not forward to send. Keep the existing try/catch
around JSON.parse to catch parse errors, and perform type checks (e.g., typeof
msg === 'object' && msg !== null && typeof msg.type === 'string') before
invoking send.
- Around line 62-72: The getUserMedia call inside useCaptureProvider (the block
that sets stream using navigator.mediaDevices.getUserMedia with audio/video
mandatory { chromeMediaSource: "desktop", chromeMediaSourceId: sourceId }) must
keep the Electron-specific mandatory constraints intact; remove the broad "as
unknown as MediaStreamConstraints" cast and instead narrow typing locally—either
declare a local interface/type for the Electron constraint shape and cast to
that, or use a targeted `@ts-expect-error` above the specific mandatory fields—so
chromeMediaSource and chromeMediaSourceId remain inside mandatory and TypeScript
is satisfied without altering the constraint structure.
- Line 89: The current assignment stream.getVideoTracks()[0].onended = () =>
stopSharing() can throw if there are no video tracks; update the
useCaptureProvider logic to first get the video tracks into a variable (e.g.,
const videoTracks = stream.getVideoTracks()), check that videoTracks.length > 0
(or that videoTracks[0] is truthy), and only then assign videoTracks[0].onended
= () => stopSharing(); otherwise skip the assignment or attach an equivalent
fallback so stopSharing isn’t referenced on undefined.
- Around line 32-40: The teardown in useCaptureProvider currently closes
pcRef.current and nulls it but leaves useWebRTC's internal state (like
pendingIceCandidatesRef) intact; add a public method on
ConnectionProvider/useWebRTC named closePeerConnection or resetPeerConnection
that closes pcRef.current, clears pendingIceCandidatesRef and any related
refs/state (e.g., senderRef, local streams, pending offers/answers), and call
that new method from stopSharing in useCaptureProvider (instead of directly
setting pcRef.current = null) so all WebRTC state is consistently reset.

In `@src/hooks/useMirrorStream.ts`:
- Around line 42-46: Cleanup currently unsubscribes and sends "stop-mirror" but
doesn't reset hasStream or clear videoRef.current.srcObject, leaving a frozen
frame visible; in useMirrorStream's cleanup (the returned function inside the
effect) also call the state setter that resets stream presence (e.g.,
setHasStream(false)) and clear the video element by pausing it and nulling
videoRef.current.srcObject (and optionally set videoRef.current.load() or style
opacity if you have helpers) so the standby UI reappears; locate the cleanup
closure that calls unsub() and sendRef.current({ type: "stop-mirror" }) and add
these reset steps there.
- Around line 11-12: Add a type-only React import (import type React from
"react") so useMirrorStream's React.RefObject type resolves; in the effect
cleanup that runs when status changes away from "connected" (inside
useMirrorStream) ensure you reset the hasStream state to false and clear any
held media by setting videoRef.current.srcObject = null (or guard before
assigning), and remove or guard the debug console.log for "[RTC] mirror-stream
received" so it doesn't spam production logs; reference the useMirrorStream
function, the hasStream state variable, and videoRef to locate the spots to
change.

In `@src/hooks/useWebRTC.tsx`:
- Around line 131-171: The async handler handleSignalingMessage should be fully
guarded to prevent unhandled promise rejections: wrap the entire body of
handleSignalingMessage (the branches handling "offer" and "answer") in a
try/catch so any awaited calls like pc.setRemoteDescription, pc.createAnswer,
pc.setLocalDescription, pc.addIceCandidate (and any other async ops on
pcRef.current) are caught; on error log/handle it (e.g. console.error or process
logger) and ensure pendingIceCandidatesRef.current is left in a consistent
state; keep the existing guarding for the "ice-candidate" branch and preserve
usage of createPeerConnection, pcRef, pendingIceCandidatesRef, and sendRef when
implementing the try/catch.

In `@src/server/websocket.ts`:
- Around line 166-178: The WebRTC signaling is being broadcast to all other
clients and payloads are being silently dropped due to MAX_PAYLOAD_SIZE, and
consumers are not tracked per provider so "consumer_left" notifications tear
down sessions incorrectly; update src/server/websocket.ts to route
offer/answer/ice-candidate/lan-info to a specific peer/session using an explicit
peer/session id in the message (or a maintained mapping keyed by
providerId/consumerId) instead of iterating all wss.clients in the broadcast
loop, remove or raise the MAX_PAYLOAD_SIZE check for SDP-bearing messages
(handle large SDP by increasing the limit or allowing chunking) and implement a
per-provider consumer registry so stop-mirror and disconnect handling in the
handlers that call notify providers/emit consumer_left only target the
provider(s) that actually host that consumer and only when that provider's
consumer count falls to zero; factor the repeated “notify providers” loops into
a helper (e.g., notifyProvidersForSession) and update logic in the message
switch and connection close handlers to use these new routing and lifecycle
helpers.
- Around line 218-242: The start/stop mirror handlers and the ws.on("close")
disconnect path must only notify providers when the consumer count transitions
0->1 (consumer_joined) or 1->0 (consumer_left); update the logic around
startMirror(), stopMirror(), and the close handler to compute the number of
active consumers (e.g., scan wss.clients for (client as ExtWebSocket).isConsumer
&& client.readyState === WebSocket.OPEN) and only broadcast consumer_joined if
count_before === 0 and only broadcast consumer_left if count_after === 0 (for
stop-mirror compute count_after excluding the stopping ws, and in close handler
set (ws as ExtWebSocket).isConsumer = false then compute remaining consumers
before sending); factor this into a small helper like
hasConsumers()/countConsumers() and use that in startMirror, stopMirror, and the
ws.on("close") path so providers (useCaptureProvider) only react on first/last
consumer transitions.

---

Outside diff comments:
In `@src/components/Trackpad/ScreenMirror.tsx`:
- Around line 14-17: TEXTS contains hardcoded user-facing strings; replace it
with i18n keys and use the app's translation helper (e.g., the useTranslation
hook or t function) instead of TEXTS so strings live in resource files; find the
TEXTS constant in ScreenMirror.tsx and: remove the hardcoded entries ("Waiting
for screen...", "Mirroring will start automatically"), add corresponding keys to
the i18n resources, and update all usages in the ScreenMirror component to call
t('your.key.waiting') and t('your.key.automatic') (or the project's equivalent
translation accessor).

In `@src/hooks/useRemoteConnection.ts`:
- Around line 9-16: sendCombo currently calls send directly which bypasses the
unified path used by sendInput; update sendCombo to call sendInput with a
payload matching the other inputs (e.g., sendInput({ type: "combo", keys: msg
})) so combos use the same transport and validation as other VALID_INPUT_TYPES,
and verify that sendInput and ConnectionProvider
(wsRef.current.send(JSON.stringify(...))) accept the "combo" type and that the
server expects that shape; adjust any input typing/VALID_INPUT_TYPES if needed
to include "combo".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 12c04cb0-aca0-485f-bf83-3a2b579c466c

📥 Commits

Reviewing files that changed from the base of the PR and between 477620c and 6fda78a.

📒 Files selected for processing (8)
  • src/components/Trackpad/ScreenMirror.tsx
  • src/contexts/ConnectionProvider.tsx
  • src/hooks/useCaptureProvider.ts
  • src/hooks/useMirrorStream.ts
  • src/hooks/useRemoteConnection.ts
  • src/hooks/useWebRTC.tsx
  • src/routes/__root.tsx
  • src/server/websocket.ts

Comment thread src/components/Trackpad/ScreenMirror.tsx
Comment thread src/contexts/ConnectionProvider.tsx Outdated
Comment thread src/hooks/useCaptureProvider.ts Outdated
Comment thread src/hooks/useCaptureProvider.ts Outdated
Comment on lines +58 to +86
const electron = (window as unknown as ElectronWindow).electron
if (electron?.showSourcePicker) {
const sourceId = await electron.showSourcePicker()
if (sourceId) {
stream = await navigator.mediaDevices.getUserMedia({
audio: {
mandatory: { chromeMediaSource: "desktop" },
},
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: sourceId,
},
},
} as unknown as MediaStreamConstraints)
} else {
// Fallback if dialog dismissed with no ID
stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
})
}
} else {
// ── Standard Browser Fallback Flow ──
stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Verify Electron security: validate sourceId and consider CSP implications.

The code accesses window.electron.showSourcePicker() and uses the returned sourceId directly in getUserMedia constraints without validation. Ensure:

  1. The Electron preload script properly exposes showSourcePicker via contextBridge (not directly attaching to window).
  2. The sourceId is validated/sanitized if it could be influenced by untrusted sources.
  3. Content Security Policy allows media capture.

Verify the Electron integration follows security best practices:

#!/bin/bash
# Check if Electron contextBridge is used for IPC exposure

fd -e js -e ts preload --exec rg -l 'contextBridge\|exposeInMainWorld'

# Check for direct window assignments (insecure pattern)
rg -n 'window\.electron\s*=' --type ts --type js
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCaptureProvider.ts` around lines 58 - 86, The code in
useCaptureProvider.ts calls (window as
ElectronWindow).electron.showSourcePicker() and passes the returned sourceId
directly into navigator.mediaDevices.getUserMedia; update this by ensuring the
preload uses contextBridge/exposeInMainWorld to expose showSourcePicker (do not
attach to window directly), validate and sanitize the returned sourceId in the
code that handles sourceId (check type, non-empty string, match an expected
pattern or whitelist of known IDs, and reject/ fallback if it fails validation),
and add a defensive branch in the showSourcePicker handling to fall back to
getDisplayMedia when sourceId is invalid; also confirm the app’s CSP allows
media capture. Reference symbols: useCaptureProvider.ts, ElectronWindow,
electron.showSourcePicker, sourceId, navigator.mediaDevices.getUserMedia, and
getDisplayMedia.

Comment thread src/hooks/useCaptureProvider.ts Outdated
Comment thread src/hooks/useMirrorStream.ts Outdated
Comment thread src/hooks/useMirrorStream.ts
Comment thread src/hooks/useWebRTC.tsx
Comment thread src/server/websocket.ts
Comment on lines +166 to +178
if (
msg.type === "offer" ||
msg.type === "answer" ||
msg.type === "ice-candidate" ||
msg.type === "lan-info"
) {
for (const client of wss.clients) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(msg))
}
}
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

WebRTC signaling routing for one provider with multiple consumers best practices

💡 Result:

For a one-provider, multiple-consumer (one-to-many) architecture in WebRTC, best practices depend heavily on the scale of the audience. Attempting to use a pure peer-to-peer (P2P) mesh—where the provider maintains a direct connection to every consumer—is generally not recommended for more than a few participants because it causes exponential growth in the provider's upstream bandwidth and CPU usage [1][2][3][4]. For robust, scalable applications, the following architectures and signaling practices are recommended: 1. Use a Media Server (SFU/MCU): Instead of direct P2P connections, the provider should push a single stream to a Selective Forwarding Unit (SFU) or Multipoint Control Unit (MCU) [1][2]. The media server then distributes that stream to all consumers [2][5]. This offloads the heavy lifting of encoding and distribution from the provider's client to the infrastructure [2][6]. 2. Signaling Best Practices: The signaling server acts as a matchmaker to exchange SDP offers/answers and ICE candidates [7][8][9]. While WebRTC does not dictate a signaling protocol, common practices include: * Transport: Use WebSockets, gRPC, or Socket.IO to maintain a persistent, bidirectional channel between clients and the signaling server [7][5][10][11]. * Pub/Sub Integration: For systems requiring scale, use a pub/sub message broker (e.g., Redis or NATS) to allow signaling nodes to communicate across a cluster [5]. * Trickle ICE: Always implement Trickle ICE to reduce call setup latency. This allows ICE candidates to be sent to the signaling server and forwarded to the remote peer as they are discovered, rather than waiting for the entire gathering process to complete [1][6][9]. * Separation of Concerns: Keep the signaling server thin. It should only handle metadata (SDP, ICE, room state, and participant discovery) and not handle actual media traffic [7][12][13]. 3. Modern Ingest/Egress Protocols: Rather than building custom signaling logic for every use case, adopt standardized protocols: * WHIP (WebRTC-HTTP Ingest Protocol): Use this for pushing the live stream from the provider to a media server or CDN [2][14]. * WHEP (WebRTC-HTTP Egress Protocol): Use this for pulling the stream from the server to consumer clients [2]. 4. Scaling Massive Broadcasts: If you need to support thousands of concurrent consumers, implement SFU Cascading. In this model, the origin SFU forwards the provider's stream to multiple regional edge SFUs, which then serve local clusters of consumers [5]. This horizontal scaling prevents any single server from becoming a bandwidth bottleneck [5]. Always ensure you have properly configured STUN/TURN servers to facilitate connections for clients behind symmetric NATs or restrictive firewalls, as these are mandatory for high-reliability production environments [7][8][6][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Identify file and print relevant sections with line numbers
FILE="src/server/websocket.ts"

if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi

echo "=== File header / imports (first 120 lines) ==="
nl -ba "$FILE" | sed -n '1,120p'

echo "=== Signaling routing block (120-260) ==="
nl -ba "$FILE" | sed -n '120,260p'

echo "=== Search for MAX_PAYLOAD_SIZE definition and usage ==="
rg -n "MAX_PAYLOAD_SIZE" "$FILE" || true

echo "=== Search for msg.type switch/handlers ==="
rg -n "msg\.type|case\s+['\"]|switch\s*\(msg\.type\)" "$FILE"

echo "=== Inspect around other lifecycle sections referenced (260-450) ==="
nl -ba "$FILE" | sed -n '260,450p'

Repository: AOSSIE-Org/Rein

Length of output: 169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm file exists
ls -la src/server/websocket.ts

FILE="src/server/websocket.ts"

echo "=== Line numbers 1-140 (imports / types / constants) ==="
cat -n "$FILE" | sed -n '1,140p'

echo "=== Line numbers 140-240 (signaling routing block) ==="
cat -n "$FILE" | sed -n '140,240p'

echo "=== MAX_PAYLOAD_SIZE occurrences ==="
rg -n "MAX_PAYLOAD_SIZE" "$FILE" || true

echo "=== msg.type handling (some surrounding context) ==="
rg -n "msg\.type" "$FILE"

# Print broader region around lifecycle referenced in the review comment
echo "=== Line numbers 240-320 (continue handlers) ==="
cat -n "$FILE" | sed -n '240,320p'

echo "=== Line numbers 320-430 (lifecycle/close path) ==="
cat -n "$FILE" | sed -n '320,430p'

# If JSON payload size check uses other names, search for "payload" checks
echo "=== Search for payload size / length checks ==="
rg -n "payload|length|byteLength|MAX_" "$FILE" || true

Repository: AOSSIE-Org/Rein

Length of output: 15123


Fix WebRTC signaling routing and consumer lifecycle handling in src/server/websocket.ts

  • offer/answer/ice-candidate/lan-info are broadcast to every other open client without routing by role/session (lines 166-178), so multiple provider/consumer connections will mix SDP/ICE across sessions and break negotiation.
  • Messages are hard-capped at 10KB (MAX_PAYLOAD_SIZE, lines 63 and 151-154); WebRTC SDP can exceed this (especially with simulcast/non-trickle), causing silent drops.
  • consumer_left is sent to all providers on any stop-mirror and any consumer disconnect (lines 231-240 and 387-397) with no per-consumer tracking/counting, so providers can tear down even while other consumers remain.

Route signaling by explicit peer/session IDs (or a maintained mapping) and track consumers per session/provider; also factor the repeated “notify providers” loops.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/websocket.ts` around lines 166 - 178, The WebRTC signaling is
being broadcast to all other clients and payloads are being silently dropped due
to MAX_PAYLOAD_SIZE, and consumers are not tracked per provider so
"consumer_left" notifications tear down sessions incorrectly; update
src/server/websocket.ts to route offer/answer/ice-candidate/lan-info to a
specific peer/session using an explicit peer/session id in the message (or a
maintained mapping keyed by providerId/consumerId) instead of iterating all
wss.clients in the broadcast loop, remove or raise the MAX_PAYLOAD_SIZE check
for SDP-bearing messages (handle large SDP by increasing the limit or allowing
chunking) and implement a per-provider consumer registry so stop-mirror and
disconnect handling in the handlers that call notify providers/emit
consumer_left only target the provider(s) that actually host that consumer and
only when that provider's consumer count falls to zero; factor the repeated
“notify providers” loops into a helper (e.g., notifyProvidersForSession) and
update logic in the message switch and connection close handlers to use these
new routing and lifecycle helpers.

Comment thread src/server/websocket.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/server/websocket.ts (1)

254-263: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use getConsumerCount() for consistency in start-provider handler.

The hasConsumer check on lines 257-259 doesn't verify readyState === WebSocket.OPEN, inconsistent with the getConsumerCount() helper used elsewhere. A consumer in a closing state would still trigger consumer_joined.

🔧 Proposed fix
 				if (msg.type === "start-provider") {
 					;(ws as ExtWebSocket).isProvider = true
 					logger.info("Client registered as Screen Provider")
-					const hasConsumer = Array.from(wss.clients).some(
-						(c) => (c as ExtWebSocket).isConsumer,
-					)
-					if (hasConsumer) {
+					if (getConsumerCount() > 0) {
 						ws.send(JSON.stringify({ type: "consumer_joined" }))
 					}
 					return
 				}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/websocket.ts` around lines 254 - 263, Replace the ad-hoc consumer
check in the "start-provider" message handler with the existing
getConsumerCount() helper to ensure only OPEN consumers are counted: in the
block handling msg.type === "start-provider" (where (ws as
ExtWebSocket).isProvider is set and you currently compute hasConsumer from
wss.clients), call getConsumerCount() and use its result to decide whether to
send ws.send({ type: "consumer_joined" }); this keeps behavior consistent with
other code paths and respects readyState filtering implemented by
getConsumerCount().
src/components/Trackpad/ScreenMirror.tsx (1)

14-17: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Externalize user-visible strings for internationalization.

The TEXTS object contains user-facing strings that should be moved to i18n resource files to support localization.

As per coding guidelines: "User-visible strings should be externalized to resource files (i18n)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Trackpad/ScreenMirror.tsx` around lines 14 - 17, The TEXTS
constant in ScreenMirror.tsx contains user-facing strings (TEXTS.WAITING and
TEXTS.AUTOMATIC) that must be moved into the i18n resource files and loaded via
the app's localization API; update ScreenMirror to replace direct references to
TEXTS with calls to the i18n helper (e.g., t('screenMirror.waiting') and
t('screenMirror.automatic')), add corresponding keys to the locale resource
files, and remove or repurpose the TEXTS object so no hard-coded UI strings
remain in ScreenMirror.tsx.
src/hooks/useCaptureProvider.ts (1)

139-141: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add bounds check before accessing video track for addTrack.

Line 109 defensively uses optional chaining (?.), but line 139 directly accesses stream.getVideoTracks()[0] without a guard. If the stream has no video tracks (edge case with audio-only capture), addTrack(undefined, stream) will throw.

🛡️ Proposed defensive check
-			const track = stream.getVideoTracks()[0]
+			const track = stream.getVideoTracks()[0]
+			if (!track) {
+				console.warn("[Capture] No video track available")
+				stopSharing()
+				return
+			}
 			const sender = pcRef.current.addTrack(track, stream)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCaptureProvider.ts` around lines 139 - 141, The code assumes a
video track exists when calling stream.getVideoTracks()[0] before
pcRef.current.addTrack; add a defensive check in the useCaptureProvider flow to
safely handle audio-only streams by retrieving the first video track via
optional chaining or checking stream.getVideoTracks().length > 0, only calling
pcRef.current.addTrack(track, stream) and assigning senderRef.current when track
is non-null, and ensure any previous sender is appropriately handled/removed if
absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/components/Trackpad/ScreenMirror.tsx`:
- Around line 14-17: The TEXTS constant in ScreenMirror.tsx contains user-facing
strings (TEXTS.WAITING and TEXTS.AUTOMATIC) that must be moved into the i18n
resource files and loaded via the app's localization API; update ScreenMirror to
replace direct references to TEXTS with calls to the i18n helper (e.g.,
t('screenMirror.waiting') and t('screenMirror.automatic')), add corresponding
keys to the locale resource files, and remove or repurpose the TEXTS object so
no hard-coded UI strings remain in ScreenMirror.tsx.

In `@src/hooks/useCaptureProvider.ts`:
- Around line 139-141: The code assumes a video track exists when calling
stream.getVideoTracks()[0] before pcRef.current.addTrack; add a defensive check
in the useCaptureProvider flow to safely handle audio-only streams by retrieving
the first video track via optional chaining or checking
stream.getVideoTracks().length > 0, only calling pcRef.current.addTrack(track,
stream) and assigning senderRef.current when track is non-null, and ensure any
previous sender is appropriately handled/removed if absent.

In `@src/server/websocket.ts`:
- Around line 254-263: Replace the ad-hoc consumer check in the "start-provider"
message handler with the existing getConsumerCount() helper to ensure only OPEN
consumers are counted: in the block handling msg.type === "start-provider"
(where (ws as ExtWebSocket).isProvider is set and you currently compute
hasConsumer from wss.clients), call getConsumerCount() and use its result to
decide whether to send ws.send({ type: "consumer_joined" }); this keeps behavior
consistent with other code paths and respects readyState filtering implemented
by getConsumerCount().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2293427-1125-4b36-8218-34bc33d80c37

📥 Commits

Reviewing files that changed from the base of the PR and between 6fda78a and d2307ab.

📒 Files selected for processing (6)
  • src/components/Trackpad/ScreenMirror.tsx
  • src/contexts/ConnectionProvider.tsx
  • src/hooks/useCaptureProvider.ts
  • src/hooks/useMirrorStream.ts
  • src/hooks/useWebRTC.tsx
  • src/server/websocket.ts

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

⚠️ This PR has merge conflicts.

Please resolve the merge conflicts before review.

Your PR will only be reviewed by a maintainer after all conflicts have been resolved.

📺 Watch this video to understand why conflicts occur and how to resolve them:
https://www.youtube.com/watch?v=Sqsz1-o7nXk

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/Trackpad/ScreenMirror.tsx (1)

14-17: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

User-visible strings should be externalized for internationalization.

The TEXTS object contains hardcoded English strings displayed in the UI. Per coding guidelines, user-visible strings should be externalized to resource files (i18n).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Trackpad/ScreenMirror.tsx` around lines 14 - 17, The TEXTS
constant in the ScreenMirror component contains hardcoded English UI strings;
replace these with i18n resource lookups by moving "Waiting for screen..." and
"Mirroring will start automatically" into your translation files and changing
TEXTS to use the i18n API (e.g., t('trackpad.waiting') /
t('trackpad.automatic')) or directly call the translation function inside
ScreenMirror; update any imports to include the i18n hook/utility used in the
project (e.g., useTranslation or i18n.t) and ensure keys like trackpad.waiting
and trackpad.automatic are added to the locale files for all supported
languages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 10: Revise the README intro sentence that begins "A cross-platform,
remote desktop..." to correct grammar and improve clarity: change "non touch" to
"non-touch", "bring" to "brings", tighten phrasing around the KISS
parenthetical, and split or reword the long sentence into two concise sentences
that state (1) the project purpose—allowing touch and non-touch devices to act
as a trackpad/keyboard via a local web interface—and (2) potential use cases
(cloud PC/cloud gaming standardization and improved STT for Linux via phone).
Ensure punctuation and capitalization are fixed and the sentence reads smoothly.

---

Outside diff comments:
In `@src/components/Trackpad/ScreenMirror.tsx`:
- Around line 14-17: The TEXTS constant in the ScreenMirror component contains
hardcoded English UI strings; replace these with i18n resource lookups by moving
"Waiting for screen..." and "Mirroring will start automatically" into your
translation files and changing TEXTS to use the i18n API (e.g.,
t('trackpad.waiting') / t('trackpad.automatic')) or directly call the
translation function inside ScreenMirror; update any imports to include the i18n
hook/utility used in the project (e.g., useTranslation or i18n.t) and ensure
keys like trackpad.waiting and trackpad.automatic are added to the locale files
for all supported languages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 38237750-34d8-4a62-8b11-089877c74e24

📥 Commits

Reviewing files that changed from the base of the PR and between d2307ab and 1d11e02.

📒 Files selected for processing (7)
  • README.md
  • src/components/Trackpad/ScreenMirror.tsx
  • src/contexts/ConnectionProvider.tsx
  • src/hooks/useCaptureProvider.ts
  • src/hooks/useMirrorStream.ts
  • src/hooks/useWebRTC.tsx
  • src/server/websocket.ts

Comment thread README.md
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.

WebRTC for RealTime Audio/Video Streaming (Screen Mirror)

2 participants