From 880d09231d22feeecf631ca133ea1f926e5c828c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 19:04:03 +0000 Subject: [PATCH] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/lib/webrtc/broadcaster.ts | 36 ++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/lib/webrtc/broadcaster.ts b/src/lib/webrtc/broadcaster.ts index 10d5203..96651a9 100644 --- a/src/lib/webrtc/broadcaster.ts +++ b/src/lib/webrtc/broadcaster.ts @@ -14,6 +14,8 @@ export interface BroadcasterHandle { onViewerCountChange: (cb: (n: number) => void) => () => void; /** Swap the outgoing video track on every active peer (e.g. when the XR mirror canvas becomes available). */ replaceVideoTrack: (newStream: MediaStream) => Promise; + /** Low-FPS backup source used when Quest/browser captureStream produces black frames. */ + setFrameSource: (canvas: HTMLCanvasElement | null) => void; } const MAX_VIEWERS = 5; @@ -22,7 +24,10 @@ const MAX_VIEWERS = 5; * Starts broadcasting a MediaStream under a fresh room code. * Returns a handle exposing the code and a stop() to teardown. */ -export async function startBroadcast(stream: MediaStream): Promise { +export async function startBroadcast( + stream: MediaStream, + options: { frameSource?: HTMLCanvasElement | null } = {}, +): Promise { const code = generateRoomCode(); // Insert room @@ -35,6 +40,8 @@ export async function startBroadcast(stream: MediaStream): Promise { if (msg.type === 'viewer-join') { @@ -43,10 +50,11 @@ export async function startBroadcast(stream: MediaStream): Promise pc.addTrack(t, activeStream)); + if (activeStream.getTracks().length === 0) pc.createDataChannel('fallback-frames'); pc.onicecandidate = (ev) => { if (ev.candidate && channel) { - sendSignal(channel, { + void sendSignal(channel, { type: 'ice', viewerId: msg.viewerId, from: 'broadcaster', @@ -67,7 +75,7 @@ export async function startBroadcast(stream: MediaStream): Promise { + if (!channel || !frameSource || !frameCtx || peers.size === 0 || frameBusy) return; + frameBusy = true; + try { + frameCtx.fillStyle = '#1a1a1a'; + frameCtx.fillRect(0, 0, frameCanvas.width, frameCanvas.height); + frameCtx.drawImage(frameSource, 0, 0, frameCanvas.width, frameCanvas.height); + const dataUrl = frameCanvas.toDataURL('image/jpeg', 0.55); + void sendSignal(channel, { type: 'frame', dataUrl, ts: Date.now() }).finally(() => { frameBusy = false; }); + } catch (e) { + frameBusy = false; + console.warn('[broadcaster] frame fallback failed', e); + } + }, 500); // Stop when the underlying stream ends const attachEndedListeners = (s: MediaStream) => { @@ -114,6 +142,7 @@ export async function startBroadcast(stream: MediaStream): Promise pc.close()); peers.clear(); + window.clearInterval(frameTimer); notify(); if (channel) await supabase.removeChannel(channel); await supabase.from('share_rooms').update({ is_active: false, viewer_count: 0 }).eq('code', code); @@ -140,6 +169,7 @@ export async function startBroadcast(stream: MediaStream): Promise { frameSource = canvas; }, onViewerCountChange: (cb) => { listeners.add(cb); cb(peers.size);