diff --git a/src/components/ShareButton.tsx b/src/components/ShareButton.tsx index 5a5d648..d0d7a66 100644 --- a/src/components/ShareButton.tsx +++ b/src/components/ShareButton.tsx @@ -4,7 +4,7 @@ import { QRCodeSVG } from 'qrcode.react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { startBroadcast, type BroadcasterHandle } from '@/lib/webrtc/broadcaster'; -import { getBroadcastSource } from '@/lib/webrtc/broadcastSource'; +import { getBroadcastSource, onBroadcastSourceChange } from '@/lib/webrtc/broadcastSource'; import { toast } from 'sonner'; interface ShareButtonProps { @@ -91,6 +91,24 @@ export function ShareButton({ variant = 'default', autoOpen, onHandleChange, onV // eslint-disable-next-line react-hooks/exhaustive-deps }, [handle]); + // When the XR mirror canvas appears/changes (entering or exiting AR), swap the + // outgoing video track so viewers see the opaque mirror instead of the transparent main canvas. + useEffect(() => { + if (!handle) return; + const swapTo = (canvas: HTMLCanvasElement | null) => { + if (!canvas || typeof canvas.captureStream !== 'function') return; + const newStream = canvas.captureStream(24); + const oldStream = streamRef.current; + streamRef.current = newStream; + handle.replaceVideoTrack(newStream).catch((e) => console.warn('replaceVideoTrack', e)); + if (oldStream) oldStream.getTracks().forEach((t) => t.stop()); + }; + // Apply current value immediately if mirror is already up + const current = getBroadcastSource(); + if (current) swapTo(current); + return onBroadcastSourceChange(swapTo); + }, [handle]); + // Cleanup on unmount useEffect(() => { return () => { diff --git a/src/lib/webrtc/broadcastSource.ts b/src/lib/webrtc/broadcastSource.ts index 163b2cd..5a15131 100644 --- a/src/lib/webrtc/broadcastSource.ts +++ b/src/lib/webrtc/broadcastSource.ts @@ -16,5 +16,5 @@ export function getBroadcastSource(): HTMLCanvasElement | null { export function onBroadcastSourceChange(cb: (c: HTMLCanvasElement | null) => void) { listeners.add(cb); - return () => listeners.delete(cb); + return () => { listeners.delete(cb); }; } diff --git a/src/lib/webrtc/broadcaster.ts b/src/lib/webrtc/broadcaster.ts index fa4b7f6..10d5203 100644 --- a/src/lib/webrtc/broadcaster.ts +++ b/src/lib/webrtc/broadcaster.ts @@ -12,6 +12,8 @@ export interface BroadcasterHandle { code: string; stop: () => Promise; 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; } const MAX_VIEWERS = 5; @@ -32,6 +34,7 @@ export async function startBroadcast(stream: MediaStream): Promise listeners.forEach((cb) => cb(peers.size)); let channel: RealtimeChannel | null = null; + let activeStream: MediaStream = stream; const handleMessage = async (msg: SignalMessage) => { if (msg.type === 'viewer-join') { @@ -39,7 +42,7 @@ export async function startBroadcast(stream: MediaStream): Promise pc.addTrack(t, stream)); + activeStream.getTracks().forEach((t) => pc.addTrack(t, activeStream)); pc.onicecandidate = (ev) => { if (ev.candidate && channel) { @@ -96,9 +99,14 @@ export async function startBroadcast(stream: MediaStream): Promise { - t.addEventListener('ended', () => stop()); - }); + const attachEndedListeners = (s: MediaStream) => { + s.getTracks().forEach((t) => { + t.addEventListener('ended', () => { + if (s === activeStream) stop(); + }); + }); + }; + attachEndedListeners(stream); let stopped = false; const stop = async () => { @@ -111,9 +119,27 @@ export async function startBroadcast(stream: MediaStream): Promise { + const newVideo = newStream.getVideoTracks()[0]; + if (!newVideo) return; + activeStream = newStream; + attachEndedListeners(newStream); + for (const pc of peers.values()) { + const sender = pc.getSenders().find((s) => s.track && s.track.kind === 'video'); + if (sender) { + try { + await sender.replaceTrack(newVideo); + } catch (e) { + console.warn('[broadcaster] replaceTrack failed', e); + } + } + } + }; + return { code, stop, + replaceVideoTrack, onViewerCountChange: (cb) => { listeners.add(cb); cb(peers.size);