Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -14,6 +14,8 @@ export interface BroadcasterHandle {
|
|||||||
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). */
|
/** Swap the outgoing video track on every active peer (e.g. when the XR mirror canvas becomes available). */
|
||||||
replaceVideoTrack: (newStream: MediaStream) => Promise<void>;
|
replaceVideoTrack: (newStream: MediaStream) => Promise<void>;
|
||||||
|
/** Low-FPS backup source used when Quest/browser captureStream produces black frames. */
|
||||||
|
setFrameSource: (canvas: HTMLCanvasElement | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_VIEWERS = 5;
|
const MAX_VIEWERS = 5;
|
||||||
@@ -22,7 +24,10 @@ const MAX_VIEWERS = 5;
|
|||||||
* Starts broadcasting a MediaStream under a fresh room code.
|
* Starts broadcasting a MediaStream under a fresh room code.
|
||||||
* Returns a handle exposing the code and a stop() to teardown.
|
* Returns a handle exposing the code and a stop() to teardown.
|
||||||
*/
|
*/
|
||||||
export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHandle> {
|
export async function startBroadcast(
|
||||||
|
stream: MediaStream,
|
||||||
|
options: { frameSource?: HTMLCanvasElement | null } = {},
|
||||||
|
): Promise<BroadcasterHandle> {
|
||||||
const code = generateRoomCode();
|
const code = generateRoomCode();
|
||||||
|
|
||||||
// Insert room
|
// Insert room
|
||||||
@@ -35,6 +40,8 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
|
|
||||||
let channel: RealtimeChannel | null = null;
|
let channel: RealtimeChannel | null = null;
|
||||||
let activeStream: MediaStream = stream;
|
let activeStream: MediaStream = stream;
|
||||||
|
let frameSource: HTMLCanvasElement | null = options.frameSource ?? null;
|
||||||
|
let frameBusy = false;
|
||||||
|
|
||||||
const handleMessage = async (msg: SignalMessage) => {
|
const handleMessage = async (msg: SignalMessage) => {
|
||||||
if (msg.type === 'viewer-join') {
|
if (msg.type === 'viewer-join') {
|
||||||
@@ -43,10 +50,11 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
peers.set(msg.viewerId, pc);
|
peers.set(msg.viewerId, pc);
|
||||||
|
|
||||||
activeStream.getTracks().forEach((t) => pc.addTrack(t, activeStream));
|
activeStream.getTracks().forEach((t) => pc.addTrack(t, activeStream));
|
||||||
|
if (activeStream.getTracks().length === 0) pc.createDataChannel('fallback-frames');
|
||||||
|
|
||||||
pc.onicecandidate = (ev) => {
|
pc.onicecandidate = (ev) => {
|
||||||
if (ev.candidate && channel) {
|
if (ev.candidate && channel) {
|
||||||
sendSignal(channel, {
|
void sendSignal(channel, {
|
||||||
type: 'ice',
|
type: 'ice',
|
||||||
viewerId: msg.viewerId,
|
viewerId: msg.viewerId,
|
||||||
from: 'broadcaster',
|
from: 'broadcaster',
|
||||||
@@ -67,7 +75,7 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
const offer = await pc.createOffer();
|
const offer = await pc.createOffer();
|
||||||
await pc.setLocalDescription(offer);
|
await pc.setLocalDescription(offer);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
sendSignal(channel, { type: 'offer', viewerId: msg.viewerId, sdp: offer });
|
await sendSignal(channel, { type: 'offer', viewerId: msg.viewerId, sdp: offer });
|
||||||
}
|
}
|
||||||
notify();
|
notify();
|
||||||
updateViewerCount(code, peers.size);
|
updateViewerCount(code, peers.size);
|
||||||
@@ -97,6 +105,26 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
};
|
};
|
||||||
|
|
||||||
channel = joinSignalingChannel(code, handleMessage);
|
channel = joinSignalingChannel(code, handleMessage);
|
||||||
|
await waitForSignalingReady(channel);
|
||||||
|
|
||||||
|
const frameCanvas = document.createElement('canvas');
|
||||||
|
frameCanvas.width = 640;
|
||||||
|
frameCanvas.height = 360;
|
||||||
|
const frameCtx = frameCanvas.getContext('2d', { alpha: false });
|
||||||
|
const frameTimer = window.setInterval(() => {
|
||||||
|
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
|
// Stop when the underlying stream ends
|
||||||
const attachEndedListeners = (s: MediaStream) => {
|
const attachEndedListeners = (s: MediaStream) => {
|
||||||
@@ -114,6 +142,7 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
stopped = true;
|
stopped = true;
|
||||||
peers.forEach((pc) => pc.close());
|
peers.forEach((pc) => pc.close());
|
||||||
peers.clear();
|
peers.clear();
|
||||||
|
window.clearInterval(frameTimer);
|
||||||
notify();
|
notify();
|
||||||
if (channel) await supabase.removeChannel(channel);
|
if (channel) await supabase.removeChannel(channel);
|
||||||
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);
|
||||||
@@ -140,6 +169,7 @@ export async function startBroadcast(stream: MediaStream): Promise<BroadcasterHa
|
|||||||
code,
|
code,
|
||||||
stop,
|
stop,
|
||||||
replaceVideoTrack,
|
replaceVideoTrack,
|
||||||
|
setFrameSource: (canvas) => { frameSource = canvas; },
|
||||||
onViewerCountChange: (cb) => {
|
onViewerCountChange: (cb) => {
|
||||||
listeners.add(cb);
|
listeners.add(cb);
|
||||||
cb(peers.size);
|
cb(peers.size);
|
||||||
|
|||||||
Reference in New Issue
Block a user