Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-14 12:50:45 +00:00
parent 74df24b3ba
commit f626134cd4
3 changed files with 256 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
import { supabase } from '@/integrations/supabase/client';
import {
generateRoomCode,
joinSignalingChannel,
sendSignal,
ICE_SERVERS,
type SignalMessage,
} from './signaling';
import type { RealtimeChannel } from '@supabase/supabase-js';
export interface BroadcasterHandle {
code: string;
stop: () => Promise<void>;
onViewerCountChange: (cb: (n: number) => void) => () => void;
}
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<BroadcasterHandle> {
const code = generateRoomCode();
// Insert room
const { error: insErr } = await supabase.from('share_rooms').insert({ code, is_active: true });
if (insErr) throw new Error(`Falha ao criar sala: ${insErr.message}`);
const peers = new Map<string, RTCPeerConnection>();
const listeners = new Set<(n: number) => void>();
const notify = () => listeners.forEach((cb) => cb(peers.size));
let channel: RealtimeChannel | null = null;
const handleMessage = async (msg: SignalMessage) => {
if (msg.type === 'viewer-join') {
if (peers.size >= MAX_VIEWERS) return;
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
peers.set(msg.viewerId, pc);
stream.getTracks().forEach((t) => pc.addTrack(t, stream));
pc.onicecandidate = (ev) => {
if (ev.candidate && channel) {
sendSignal(channel, {
type: 'ice',
viewerId: msg.viewerId,
from: 'broadcaster',
candidate: ev.candidate.toJSON(),
});
}
};
pc.onconnectionstatechange = () => {
if (['failed', 'closed', 'disconnected'].includes(pc.connectionState)) {
peers.delete(msg.viewerId);
pc.close();
notify();
updateViewerCount(code, peers.size);
}
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
if (channel) {
sendSignal(channel, { type: 'offer', viewerId: msg.viewerId, sdp: offer });
}
notify();
updateViewerCount(code, peers.size);
} else if (msg.type === 'answer') {
const pc = peers.get(msg.viewerId);
if (pc && pc.signalingState !== 'stable') {
await pc.setRemoteDescription(msg.sdp);
}
} else if (msg.type === 'ice' && msg.from === 'viewer') {
const pc = peers.get(msg.viewerId);
if (pc) {
try {
await pc.addIceCandidate(msg.candidate);
} catch (e) {
console.warn('[broadcaster] addIceCandidate failed', e);
}
}
} else if (msg.type === 'viewer-leave') {
const pc = peers.get(msg.viewerId);
if (pc) {
pc.close();
peers.delete(msg.viewerId);
notify();
updateViewerCount(code, peers.size);
}
}
};
channel = joinSignalingChannel(code, handleMessage);
// Stop when the underlying stream ends
stream.getTracks().forEach((t) => {
t.addEventListener('ended', () => stop());
});
let stopped = false;
const stop = async () => {
if (stopped) return;
stopped = true;
peers.forEach((pc) => pc.close());
peers.clear();
notify();
if (channel) await supabase.removeChannel(channel);
await supabase.from('share_rooms').update({ is_active: false, viewer_count: 0 }).eq('code', code);
};
return {
code,
stop,
onViewerCountChange: (cb) => {
listeners.add(cb);
cb(peers.size);
return () => listeners.delete(cb);
},
};
}
async function updateViewerCount(code: string, n: number) {
await supabase.from('share_rooms').update({ viewer_count: n }).eq('code', code);
}