// Thin JS wrapper around the Rust LiveKit bridge commands + events. // Mirrors the subset of `livekit-client` that CallContext actually uses // so the adapter can be swapped behind the `VITE_USE_RUST_LIVEKIT` flag. // // Phase B.1 — only connect/disconnect/data-channel/state events wired. // Mic, camera, screen-share, active-speakers, video rendering ship in // later phases. Components that call missing methods get a typed // "not implemented" error so regressions surface immediately. import { invoke } from '@tauri-apps/api/core'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { isTauriRuntime } from './globalShortcut'; export const rustLivekitFlag = (() => { const raw = (import.meta as unknown as { env?: { VITE_USE_RUST_LIVEKIT?: string } }) .env?.VITE_USE_RUST_LIVEKIT; return raw === 'true' || raw === '1'; })(); export function isRustLivekitAvailable(): boolean { return rustLivekitFlag && isTauriRuntime(); } export type NativeRoomState = | { state: 'connecting' } | { state: 'connected' } | { state: 'disconnected' }; export interface NativeParticipantEvent { identity: string; name?: string | undefined; } export interface NativeDataEvent { identity: string; payloadB64: string; reliable: boolean; } type Listener = (payload: T) => void; export class NativeRoom { private unlistens: UnlistenFn[] = []; private stateListeners = new Set>(); private joinListeners = new Set>(); private leaveListeners = new Set>(); private dataListeners = new Set>(); async connect(url: string, token: string): Promise { if (!isRustLivekitAvailable()) { throw new Error('Rust LiveKit backend not available'); } await this.subscribeEvents(); try { await invoke('livekit_connect', { args: { url, token } }); } catch (err: unknown) { await this.teardown(); throw err; } } async disconnect(): Promise { try { await invoke('livekit_disconnect'); } finally { await this.teardown(); } } async sendData(payload: Uint8Array, reliable: boolean): Promise { await invoke('livekit_send_data', { payloadB64: bytesToB64(payload), reliable, }); } onRoomState(fn: Listener): () => void { this.stateListeners.add(fn); return () => this.stateListeners.delete(fn); } onParticipantJoined(fn: Listener): () => void { this.joinListeners.add(fn); return () => this.joinListeners.delete(fn); } onParticipantLeft(fn: Listener): () => void { this.leaveListeners.add(fn); return () => this.leaveListeners.delete(fn); } onDataReceived(fn: Listener): () => void { this.dataListeners.add(fn); return () => this.dataListeners.delete(fn); } private async subscribeEvents(): Promise { // Room state — connected / disconnected. const uState = await listen('livekit:room_state', (evt) => { for (const fn of this.stateListeners) fn(evt.payload); }); const uJoined = await listen('livekit:participant_joined', (evt) => { for (const fn of this.joinListeners) fn(evt.payload); }); const uLeft = await listen('livekit:participant_left', (evt) => { for (const fn of this.leaveListeners) fn(evt.payload); }); const uData = await listen('livekit:data_received', (evt) => { for (const fn of this.dataListeners) fn(evt.payload); }); this.unlistens.push(uState, uJoined, uLeft, uData); } private async teardown(): Promise { for (const unlisten of this.unlistens) { try { unlisten(); } catch { /* unlistens become no-op after first call */ } } this.unlistens = []; this.stateListeners.clear(); this.joinListeners.clear(); this.leaveListeners.clear(); this.dataListeners.clear(); } } function bytesToB64(bytes: Uint8Array): string { let s = ''; for (const b of bytes) s += String.fromCharCode(b); return btoa(s); }