Corrigiu mirror AR substituição
X-Lovable-Edit-ID: edt-d302fa05-d92a-4f17-a72f-fe197c2d6b69 Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { QRCodeSVG } from 'qrcode.react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { startBroadcast, type BroadcasterHandle } from '@/lib/webrtc/broadcaster';
|
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';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface ShareButtonProps {
|
interface ShareButtonProps {
|
||||||
@@ -91,6 +91,24 @@ export function ShareButton({ variant = 'default', autoOpen, onHandleChange, onV
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handle]);
|
}, [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
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ export function getBroadcastSource(): HTMLCanvasElement | null {
|
|||||||
|
|
||||||
export function onBroadcastSourceChange(cb: (c: HTMLCanvasElement | null) => void) {
|
export function onBroadcastSourceChange(cb: (c: HTMLCanvasElement | null) => void) {
|
||||||
listeners.add(cb);
|
listeners.add(cb);
|
||||||
return () => listeners.delete(cb);
|
return () => { listeners.delete(cb); };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export interface BroadcasterHandle {
|
|||||||
code: string;
|
code: string;
|
||||||
stop: () => Promise<void>;
|
stop: () => Promise<void>;
|
||||||
onViewerCountChange: (cb: (n: number) => void) => () => void;
|
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<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_VIEWERS = 5;
|
const MAX_VIEWERS = 5;
|
||||||
@@ -32,6 +34,7 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
const notify = () => listeners.forEach((cb) => cb(peers.size));
|
const notify = () => listeners.forEach((cb) => cb(peers.size));
|
||||||
|
|
||||||
let channel: RealtimeChannel | null = null;
|
let channel: RealtimeChannel | null = null;
|
||||||
|
let activeStream: MediaStream = stream;
|
||||||
|
|
||||||
const handleMessage = async (msg: SignalMessage) => {
|
const handleMessage = async (msg: SignalMessage) => {
|
||||||
if (msg.type === 'viewer-join') {
|
if (msg.type === 'viewer-join') {
|
||||||
@@ -39,7 +42,7 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
|
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
|
||||||
peers.set(msg.viewerId, pc);
|
peers.set(msg.viewerId, pc);
|
||||||
|
|
||||||
stream.getTracks().forEach((t) => pc.addTrack(t, stream));
|
activeStream.getTracks().forEach((t) => pc.addTrack(t, activeStream));
|
||||||
|
|
||||||
pc.onicecandidate = (ev) => {
|
pc.onicecandidate = (ev) => {
|
||||||
if (ev.candidate && channel) {
|
if (ev.candidate && channel) {
|
||||||
@@ -96,9 +99,14 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
channel = joinSignalingChannel(code, handleMessage);
|
channel = joinSignalingChannel(code, handleMessage);
|
||||||
|
|
||||||
// Stop when the underlying stream ends
|
// Stop when the underlying stream ends
|
||||||
stream.getTracks().forEach((t) => {
|
const attachEndedListeners = (s: MediaStream) => {
|
||||||
t.addEventListener('ended', () => stop());
|
s.getTracks().forEach((t) => {
|
||||||
|
t.addEventListener('ended', () => {
|
||||||
|
if (s === activeStream) stop();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
attachEndedListeners(stream);
|
||||||
|
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
const stop = async () => {
|
const stop = async () => {
|
||||||
@@ -111,9 +119,27 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
await supabase.from('share_rooms').update({ is_active: false, viewer_count: 0 }).eq('code', code);
|
await supabase.from('share_rooms').update({ is_active: false, viewer_count: 0 }).eq('code', code);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const replaceVideoTrack = async (newStream: MediaStream) => {
|
||||||
|
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 {
|
return {
|
||||||
code,
|
code,
|
||||||
stop,
|
stop,
|
||||||
|
replaceVideoTrack,
|
||||||
onViewerCountChange: (cb) => {
|
onViewerCountChange: (cb) => {
|
||||||
listeners.add(cb);
|
listeners.add(cb);
|
||||||
cb(peers.size);
|
cb(peers.size);
|
||||||
|
|||||||
Reference in New Issue
Block a user