import { Identity } from "spacetimedb"; import { tables, reducers } from "../../../module_bindings"; import { useTable, useReducer } from "spacetimedb/svelte"; import { LocalMediaService } from "./local-media.svelte"; import { ChannelAudioWebRTCService } from "./channel-audio-webrtc.svelte"; import { ScreenSharingWebRTCService } from "./screen-sharing-webrtc.svelte"; import * as Types from "../../../module_bindings/types"; export class WebRTCService { identity = $state(null); connectedChannelId = $state(); watching = $state([]); #startWatchingReducer = useReducer(reducers.startWatching); #stopWatchingReducer = useReducer(reducers.stopWatching); #setMuteReducer = useReducer(reducers.setMute); #setDeafenReducer = useReducer(reducers.setDeafen); localMedia: LocalMediaService; voice: ChannelAudioWebRTCService; screen: ScreenSharingWebRTCService; constructor(identity: Identity | null, connectedChannelId: bigint | undefined) { this.identity = identity; this.connectedChannelId = connectedChannelId; const [wStore] = useTable(tables.watching); wStore.subscribe(v => this.watching = v); this.localMedia = new LocalMediaService(connectedChannelId); // Pass localMedia's stream and deafen state to voice this.voice = new ChannelAudioWebRTCService( identity, connectedChannelId, this.localMedia.localStream, this.localMedia.isDeafened ); this.screen = new ScreenSharingWebRTCService( identity, connectedChannelId, this.localMedia.localScreenStream ); // Sync state to sub-services $effect(() => { this.localMedia.connectedChannelId = this.connectedChannelId; this.voice.identity = this.identity; this.voice.connectedChannelId = this.connectedChannelId; this.voice.localStream = this.localMedia.localStream; this.voice.isDeafened = this.localMedia.isDeafened; this.screen.identity = this.identity; this.screen.connectedChannelId = this.connectedChannelId; this.screen.localScreenStream = this.localMedia.localScreenStream; }); // Sync mute/deafen to DB $effect(() => { if (this.connectedChannelId) { this.#setMuteReducer({ muted: this.localMedia.isMuted }); } }); $effect(() => { if (this.connectedChannelId) { this.#setDeafenReducer({ deafened: this.localMedia.isDeafened }); } }); // Orchestration $effect(() => { if (this.connectedChannelId && this.identity) { console.log(`[WebRTC] Joined channel ${this.connectedChannelId}, requesting mic...`); this.localMedia.requestMic(); } else { console.log("[WebRTC] Left channel, releasing mic."); this.localMedia.releaseMic(); } }); } startScreenShare = () => { this.localMedia.startScreenShare(() => {}); }; stopScreenShare = () => { this.localMedia.stopScreenShare(() => {}); }; startWatching = (peerIdentity: Identity) => { if (this.connectedChannelId) { this.#startWatchingReducer({ watchee: peerIdentity, channelId: this.connectedChannelId, }); } }; stopWatching = (peerIdentity: Identity) => { this.#stopWatchingReducer({ watchee: peerIdentity }); }; // Facade getters get localStream() { return this.localMedia.localStream; } get localScreenStream() { return this.localMedia.localScreenStream; } get peerStatuses() { return this.voice.peerManager.peerStatuses; } get peers() { return this.screen.peerManager.peers; } get isSharingScreen() { return this.localMedia.isSharingScreen; } get isMuted() { return this.localMedia.isMuted; } get isDeafened() { return this.localMedia.isDeafened; } get peerStats() { return this.voice.peerManager.peerStats; } toggleMute = () => this.localMedia.toggleMute(); toggleDeafen = () => this.localMedia.toggleDeafen(); setPeerAudioPreference = (peerIdHex: string, pref: any) => this.voice.peerManager.setPeerAudioPreference(peerIdHex, pref); }