Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/components/Trackpad/ScreenMirror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type React from "react"
import { useRef } from "react"
import { useConnection } from "../../contexts/ConnectionProvider"
import { useMirrorStream } from "../../hooks/useMirrorStream"
import { useWebRTCStream } from "../../hooks/useWebRTCStream"

interface ScreenMirrorProps {
scrollMode: boolean
Expand All @@ -23,7 +24,13 @@ export const ScreenMirror = ({
}: ScreenMirrorProps) => {
const { wsRef, status } = useConnection()
const canvasRef = useRef<HTMLCanvasElement>(null)
const { hasFrame } = useMirrorStream(wsRef, canvasRef, status)

// Try WebRTC first (PoC #208), fall back to old method
const { hasFrame: hasWebRTCFrame } = useWebRTCStream(wsRef, canvasRef, status)
const { hasFrame: hasLegacyFrame } = useMirrorStream(wsRef, canvasRef, status)

// Prefer WebRTC if connected
const hasFrame = status === "connected" ? hasWebRTCFrame : hasLegacyFrame
Comment on lines +28 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Concurrent canvas writes and flawed fallback logic.

Two issues with this implementation:

  1. Race condition on canvas: Both hooks are invoked unconditionally. Per useMirrorStream.ts:66-85, the legacy hook continues drawing to the canvas when status === "connected". When WebRTC is also active, both streams write to the same canvas concurrently, causing visual tearing or flickering.

  2. Fallback doesn't actually fall back: The ternary status === "connected" ? hasWebRTCFrame : hasLegacyFrame checks connection status, not frame availability. If WebRTC fails to establish (provider rejected, no answer, etc.), hasWebRTCFrame stays false, hasLegacyFrame is ignored, and the UI shows "Waiting for screen..." indefinitely despite the legacy stream potentially working.

The fallback should check frame availability, not connection status:

🔧 Suggested approach
-	// Prefer WebRTC if connected
-	const hasFrame = status === "connected" ? hasWebRTCFrame : hasLegacyFrame
+	// Prefer WebRTC frames, fall back to legacy
+	const hasFrame = hasWebRTCFrame || hasLegacyFrame

However, this still leaves the concurrent canvas write problem. A cleaner solution would be to:

  • Pass a flag to useMirrorStream to disable it when WebRTC is active, OR
  • Have useWebRTCStream signal success so useMirrorStream can be conditionally skipped entirely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Trackpad/ScreenMirror.tsx` around lines 28 - 33, The current
code unconditionally calls useWebRTCStream and useMirrorStream causing
concurrent canvas writes and uses status to select frames; change it so
useMirrorStream is not invoked while WebRTC is active (or pass a disable flag
into useMirrorStream) and compute hasFrame as a availability OR (hasWebRTCFrame
|| hasLegacyFrame) rather than using status; ensure useWebRTCStream exposes a
success/active signal you can check before skipping the legacy hook, and keep
wsRef, canvasRef, and status passed unchanged to the chosen hook (reference
useWebRTCStream, useMirrorStream, hasWebRTCFrame, hasLegacyFrame, and hasFrame).


return (
<div className="absolute inset-0 flex items-center justify-center bg-black overflow-hidden select-none touch-none">
Expand Down
125 changes: 125 additions & 0 deletions src/hooks/useClipboardSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* useClipboardSync — PoC hook for bidirectional clipboard sync (Issue #97)
*
* Problem: navigator.clipboard.writeText() requires HTTPS, which TanStack
* Router doesn't support yet (https://github.com/TanStack/router/issues/4287).
*
* Solution: Use the WebSocket channel to relay clipboard text between the
* phone client and the desktop server without requiring HTTPS on the client.
*
* Flow:
* COPY → client reads its own clipboard → sends {type:"clipboard-push", text}
* → server calls keyboard.pressKey(Ctrl+C) then writes text to host clipboard
* PASTE → client sends {type:"clipboard-pull"}
* → server reads host clipboard → responds {type:"clipboard-text", text}
* → client writes text into active input via document.execCommand('insertText')
* (works over plain HTTP, no HTTPS required)
*/

import { useCallback, useEffect, useRef } from "react"
Comment on lines +1 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing "use client" directive.

This is a client-side React hook that uses browser APIs (navigator.clipboard, document.activeElement, window.getSelection). It requires the "use client" directive at the top of the file.

Proposed fix
+"use client"
+
 /**
  * useClipboardSync — PoC hook for bidirectional clipboard sync (Issue `#97`)

As per coding guidelines: **/*.{ts,tsx,js,jsx}: NextJS: Ensure that "use client" is being used.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* useClipboardSync PoC hook for bidirectional clipboard sync (Issue #97)
*
* Problem: navigator.clipboard.writeText() requires HTTPS, which TanStack
* Router doesn't support yet (https://github.com/TanStack/router/issues/4287).
*
* Solution: Use the WebSocket channel to relay clipboard text between the
* phone client and the desktop server without requiring HTTPS on the client.
*
* Flow:
* COPY client reads its own clipboard sends {type:"clipboard-push", text}
* server calls keyboard.pressKey(Ctrl+C) then writes text to host clipboard
* PASTE client sends {type:"clipboard-pull"}
* server reads host clipboard responds {type:"clipboard-text", text}
* client writes text into active input via document.execCommand('insertText')
* (works over plain HTTP, no HTTPS required)
*/
import { useCallback, useEffect, useRef } from "react"
"use client"
/**
* useClipboardSync PoC hook for bidirectional clipboard sync (Issue `#97`)
*
* Problem: navigator.clipboard.writeText() requires HTTPS, which TanStack
* Router doesn't support yet (https://github.com/TanStack/router/issues/4287).
*
* Solution: Use the WebSocket channel to relay clipboard text between the
* phone client and the desktop server without requiring HTTPS on the client.
*
* Flow:
* COPY client reads its own clipboard sends {type:"clipboard-push", text}
* server calls keyboard.pressKey(Ctrl+C) then writes text to host clipboard
* PASTE client sends {type:"clipboard-pull"}
* server reads host clipboard responds {type:"clipboard-text", text}
* client writes text into active input via document.execCommand('insertText')
* (works over plain HTTP, no HTTPS required)
*/
import { useCallback, useEffect, useRef } from "react"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useClipboardSync.ts` around lines 1 - 19, The file defining the
useClipboardSync hook is missing the Next.js client-side directive; add the "use
client" directive as the very first line of the file so the hook
(useClipboardSync and its use of navigator.clipboard, document.activeElement,
window.getSelection, etc.) runs on the client; ensure the directive precedes all
imports and comments so React's client-side behavior is enabled for the hook.


export interface ClipboardSyncOptions {
/** Live WebSocket connection to the Rein server */
socket: WebSocket | null
/** Called when a paste payload arrives from the server */
onPasteReceived?: (text: string) => void
}

export function useClipboardSync({
socket,
onPasteReceived,
}: ClipboardSyncOptions) {
const onPasteRef = useRef(onPasteReceived)
useEffect(() => {
onPasteRef.current = onPasteReceived
}, [onPasteReceived])

// Listen for clipboard-text messages arriving from the server
useEffect(() => {
if (!socket) return

const handler = (event: MessageEvent) => {
try {
const msg = JSON.parse(event.data as string) as {
type: string
text?: string
}
if (msg.type === "clipboard-text" && typeof msg.text === "string") {
// Try modern clipboard API first (only works over HTTPS / localhost)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(msg.text).catch(() => {
insertTextFallback(msg.text)
})
Comment on lines +50 to +52
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix non-null assertion to resolve pipeline failure.

The pipeline reports a Biome lint error for the non-null assertion on msg.text!. Since the condition on line 47 already validates msg.text is a string, you can capture it in a variable before the async callback.

Proposed fix
 				if (msg.type === "clipboard-text" && typeof msg.text === "string") {
+					const text = msg.text
 					// Try modern clipboard API first (only works over HTTPS / localhost)
 					if (navigator.clipboard && window.isSecureContext) {
-						navigator.clipboard.writeText(msg.text).catch(() => {
-							insertTextFallback(msg.text!)
+						navigator.clipboard.writeText(text).catch(() => {
+							insertTextFallback(text)
 						})
 					} else {
 						// Fallback: insert into focused element via execCommand (HTTP-safe)
-						insertTextFallback(msg.text)
+						insertTextFallback(text)
 					}
-					onPasteRef.current?.(msg.text)
+					onPasteRef.current?.(text)
 				}
🧰 Tools
🪛 GitHub Actions: CI

[error] 51-51: Biome lint/style error (lint/style/noNonNullAssertion): Forbidden non-null assertion. Update the code to avoid using the non-null assertion operator.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useClipboardSync.ts` around lines 50 - 52, The non-null assertion
on msg.text causes a Biome lint failure; capture the validated string in a local
variable before the async callback and use that variable instead. Inside the
block where you already checked msg.text is a string, add const text = msg.text
and replace navigator.clipboard.writeText(msg.text).catch(() =>
insertTextFallback(msg.text!)) with navigator.clipboard.writeText(text).catch(()
=> insertTextFallback(text)), referencing msg.text,
navigator.clipboard.writeText, and insertTextFallback to locate and update the
code.

} else {
// Fallback: insert into focused element via execCommand (HTTP-safe)
insertTextFallback(msg.text)
}
onPasteRef.current?.(msg.text)
}
} catch {
// Not a JSON message — ignore
}
}

socket.addEventListener("message", handler)
return () => socket.removeEventListener("message", handler)
}, [socket])

/**
* pushCopy — reads the client clipboard and sends it to the server so the
* host clipboard is updated and Ctrl+C is emitted on the focused element.
*/
const pushCopy = useCallback(async () => {
if (!socket || socket.readyState !== WebSocket.OPEN) return

let text = ""
try {
if (navigator.clipboard && window.isSecureContext) {
text = await navigator.clipboard.readText()
} else {
// Fallback: try to read selected text from DOM
text = window.getSelection()?.toString() ?? ""
}
} catch {
text = window.getSelection()?.toString() ?? ""
}

socket.send(JSON.stringify({ type: "clipboard-push", text }))
}, [socket])

/**
* requestPaste — asks the server for the current host clipboard content.
* The server will respond with {type:"clipboard-text", text}.
*/
const requestPaste = useCallback(() => {
if (!socket || socket.readyState !== WebSocket.OPEN) return
socket.send(JSON.stringify({ type: "clipboard-pull" }))
}, [socket])

return { pushCopy, requestPaste }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
* HTTP-safe text insertion — uses the deprecated but universally supported
* execCommand('insertText') which works without HTTPS.
*/
function insertTextFallback(text: string): void {
const active = document.activeElement as
| HTMLInputElement
| HTMLTextAreaElement
| null
if (active && "value" in active) {
const start = active.selectionStart ?? active.value.length
const end = active.selectionEnd ?? active.value.length
active.value = active.value.slice(0, start) + text + active.value.slice(end)
active.selectionStart = active.selectionEnd = start + text.length
active.dispatchEvent(new Event("input", { bubbles: true }))
} else {
// Last resort: execCommand (works in most browsers even on HTTP)
document.execCommand("insertText", false, text)
}
}
149 changes: 149 additions & 0 deletions src/hooks/useWebRTCCaptureProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client"

import { useCallback, useEffect, useRef, useState } from "react"

interface RTCMessage {
type: "offer" | "answer" | "ice-candidate"
payload: RTCSessionDescriptionInit | RTCIceCandidateInit
}
Comment on lines +5 to +8
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unused RTCMessage interface.

The RTCMessage interface is defined but never used in this file. Consider removing it or using it for type-safe message handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useWebRTCCaptureProvider.ts` around lines 5 - 8, The file declares
an unused interface named RTCMessage; either remove the RTCMessage declaration
to eliminate dead code, or apply it to the message handling code (e.g., annotate
incoming/outgoing message variables and the message event handler in
useWebRTCCaptureProvider to use RTCMessage for type-safe handling of "offer" |
"answer" | "ice-candidate" payloads). Update or remove the interface so there
are no unused types reported.


export function useWebRTCCaptureProvider(
wsRef: React.RefObject<WebSocket | null>,
onConnectedChange?: (connected: boolean) => void,
) {
const [isSharing, setIsSharing] = useState(false)
const peerConnectionRef = useRef<RTCPeerConnection | null>(null)
const localStreamRef = useRef<MediaStream | null>(null)

const stopSharing = useCallback(() => {
if (localStreamRef.current) {
for (const track of localStreamRef.current.getTracks()) {
track.stop()
}
localStreamRef.current = null
}

if (peerConnectionRef.current) {
peerConnectionRef.current.close()
peerConnectionRef.current = null
}

if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: "stop-webrtc-provider" }))
}

setIsSharing(false)
onConnectedChange?.(false)
}, [wsRef, onConnectedChange])

const startSharing = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: "monitor",
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 60 },
},
audio: false,
})

localStreamRef.current = stream

const config: RTCConfiguration = {
iceServers: [],
}

const pc = new RTCPeerConnection(config)
peerConnectionRef.current = pc

for (const track of stream.getVideoTracks()) {
pc.addTrack(track, stream)
}

pc.onicecandidate = (event) => {
if (event.candidate && wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: "webrtc-ice",
payload: event.candidate.toJSON(),
}),
)
}
}

pc.onconnectionstatechange = () => {
const state = pc.connectionState
console.log("[WebRTC] Connection state:", state)

if (state === "connected") {
onConnectedChange?.(true)
} else if (
state === "disconnected" ||
state === "failed" ||
state === "closed"
) {
onConnectedChange?.(false)
}
}

const offer = await pc.createOffer()
await pc.setLocalDescription(offer)

if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: "webrtc-offer",
payload: offer,
}),
)
}

stream.getVideoTracks()[0].onended = () => {
stopSharing()
}

setIsSharing(true)
} catch (err) {
console.error("Failed to start WebRTC screen capture:", err)
setIsSharing(false)
}
}, [wsRef, stopSharing, onConnectedChange])
Comment on lines +39 to +111
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing provider registration before sending WebRTC offer.

The hook sends webrtc-offer (line 94-99) and stop-webrtc-provider (line 32), but never sends start-provider to register as a provider on the server. This means the server's isProvider flag is never set, causing:

  1. webrtc-answer messages won't be routed back to this client
  2. ICE candidates from consumers will be misrouted
Proposed fix: Send start-provider before the offer
 			const offer = await pc.createOffer()
 			await pc.setLocalDescription(offer)

 			if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
+				wsRef.current.send(JSON.stringify({ type: "start-provider" }))
 				wsRef.current.send(
 					JSON.stringify({
 						type: "webrtc-offer",
 						payload: offer,
 					}),
 				)
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const startSharing = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: "monitor",
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 60 },
},
audio: false,
})
localStreamRef.current = stream
const config: RTCConfiguration = {
iceServers: [],
}
const pc = new RTCPeerConnection(config)
peerConnectionRef.current = pc
for (const track of stream.getVideoTracks()) {
pc.addTrack(track, stream)
}
pc.onicecandidate = (event) => {
if (event.candidate && wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: "webrtc-ice",
payload: event.candidate.toJSON(),
}),
)
}
}
pc.onconnectionstatechange = () => {
const state = pc.connectionState
console.log("[WebRTC] Connection state:", state)
if (state === "connected") {
onConnectedChange?.(true)
} else if (
state === "disconnected" ||
state === "failed" ||
state === "closed"
) {
onConnectedChange?.(false)
}
}
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: "webrtc-offer",
payload: offer,
}),
)
}
stream.getVideoTracks()[0].onended = () => {
stopSharing()
}
setIsSharing(true)
} catch (err) {
console.error("Failed to start WebRTC screen capture:", err)
setIsSharing(false)
}
}, [wsRef, stopSharing, onConnectedChange])
const startSharing = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: "monitor",
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 60 },
},
audio: false,
})
localStreamRef.current = stream
const config: RTCConfiguration = {
iceServers: [],
}
const pc = new RTCPeerConnection(config)
peerConnectionRef.current = pc
for (const track of stream.getVideoTracks()) {
pc.addTrack(track, stream)
}
pc.onicecandidate = (event) => {
if (event.candidate && wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: "webrtc-ice",
payload: event.candidate.toJSON(),
}),
)
}
}
pc.onconnectionstatechange = () => {
const state = pc.connectionState
console.log("[WebRTC] Connection state:", state)
if (state === "connected") {
onConnectedChange?.(true)
} else if (
state === "disconnected" ||
state === "failed" ||
state === "closed"
) {
onConnectedChange?.(false)
}
}
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: "start-provider" }))
wsRef.current.send(
JSON.stringify({
type: "webrtc-offer",
payload: offer,
}),
)
}
stream.getVideoTracks()[0].onended = () => {
stopSharing()
}
setIsSharing(true)
} catch (err) {
console.error("Failed to start WebRTC screen capture:", err)
setIsSharing(false)
}
}, [wsRef, stopSharing, onConnectedChange])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useWebRTCCaptureProvider.ts` around lines 39 - 111, The
startSharing flow (startSharing) never registers this client as a provider
before sending the webrtc-offer, so the server won't set isProvider and
answers/ICE will be misrouted; before creating/sending the offer (and after
confirming wsRef.current?.readyState === WebSocket.OPEN) send a "start-provider"
message via wsRef.current.send (matching the server's provider registration
contract) so the server marks this client as a provider; ensure this send
happens before the webrtc-offer send (and keep stopSharing behavior that sends
"stop-webrtc-provider" intact) so subsequent webrtc-answer and ICE messages are
routed back correctly.


useEffect(() => {
return () => {
stopSharing()
}
}, [stopSharing])

useEffect(() => {
if (!wsRef.current) return

const handleMessage = (event: MessageEvent) => {
if (peerConnectionRef.current?.remoteDescription) return

try {
const data = JSON.parse(event.data)

if (data.type === "webrtc-answer") {
const answer = new RTCSessionDescription(data.payload)
peerConnectionRef.current?.setRemoteDescription(answer)
} else if (data.type === "webrtc-ice") {
const candidate = new RTCIceCandidate(data.payload)
peerConnectionRef.current?.addIceCandidate(candidate)
}
} catch (err) {
console.error("Error handling WebRTC message:", err)
}
}
Comment on lines +119 to +138
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: ICE candidates blocked after remote description is set.

Line 123 returns early if remoteDescription is already set, which blocks processing of all subsequent messages, including ICE candidates. In WebRTC trickle ICE, candidates often arrive after the answer. This will prevent peer connection establishment.

Proposed fix: Move the early return to only apply to webrtc-answer
 	useEffect(() => {
 		if (!wsRef.current) return

 		const handleMessage = (event: MessageEvent) => {
-			if (peerConnectionRef.current?.remoteDescription) return
-
 			try {
 				const data = JSON.parse(event.data)

 				if (data.type === "webrtc-answer") {
+					if (peerConnectionRef.current?.remoteDescription) return
 					const answer = new RTCSessionDescription(data.payload)
 					peerConnectionRef.current?.setRemoteDescription(answer)
 				} else if (data.type === "webrtc-ice") {
 					const candidate = new RTCIceCandidate(data.payload)
 					peerConnectionRef.current?.addIceCandidate(candidate)
 				}
 			} catch (err) {
 				console.error("Error handling WebRTC message:", err)
 			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!wsRef.current) return
const handleMessage = (event: MessageEvent) => {
if (peerConnectionRef.current?.remoteDescription) return
try {
const data = JSON.parse(event.data)
if (data.type === "webrtc-answer") {
const answer = new RTCSessionDescription(data.payload)
peerConnectionRef.current?.setRemoteDescription(answer)
} else if (data.type === "webrtc-ice") {
const candidate = new RTCIceCandidate(data.payload)
peerConnectionRef.current?.addIceCandidate(candidate)
}
} catch (err) {
console.error("Error handling WebRTC message:", err)
}
}
useEffect(() => {
if (!wsRef.current) return
const handleMessage = (event: MessageEvent) => {
try {
const data = JSON.parse(event.data)
if (data.type === "webrtc-answer") {
if (peerConnectionRef.current?.remoteDescription) return
const answer = new RTCSessionDescription(data.payload)
peerConnectionRef.current?.setRemoteDescription(answer)
} else if (data.type === "webrtc-ice") {
const candidate = new RTCIceCandidate(data.payload)
peerConnectionRef.current?.addIceCandidate(candidate)
}
} catch (err) {
console.error("Error handling WebRTC message:", err)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useWebRTCCaptureProvider.ts` around lines 119 - 138, The current
handleMessage in useEffect incorrectly returns early if
peerConnectionRef.current?.remoteDescription is set, which blocks processing of
subsequent webrtc-ice messages; update handleMessage (used in
useWebRTCCaptureProvider) so the early return only applies when handling a
"webrtc-answer" (i.e., if data.type === "webrtc-answer" and remoteDescription
exists, skip applying the answer), and always allow the "webrtc-ice" branch to
run so new RTCIceCandidate messages are added via
peerConnectionRef.current?.addIceCandidate; keep the JSON.parse and try/catch
but relocate the remoteDescription guard into the webrtc-answer branch.


wsRef.current.addEventListener("message", handleMessage)
return () => wsRef.current?.removeEventListener("message", handleMessage)
}, [wsRef])

return {
isSharing,
startSharing,
stopSharing,
}
}
Loading
Loading