UNPKG

@livekit/react-native-webrtc

Version:
370 lines (317 loc) 14.5 kB
import { NativeModules, Platform } from 'react-native'; import { addListener, removeListener } from './EventEmitter'; const { WebRTCModule } = NativeModules; export type SpeechActivityEvent = 'started' | 'ended'; export interface SpeechActivityEventData { event: SpeechActivityEvent; } export interface EngineStateEventData { isPlayoutEnabled: boolean; isRecordingEnabled: boolean; } /** * Raw native event payload. Every engine event carries a `requestId` that must * be echoed back to the matching resolve call so the native side can drop a * response from a round that already timed out. This id is internal and is not * surfaced to app-registered handlers. */ interface EngineEventPayload { requestId: number; } interface EngineStateEventPayload extends EngineEventPayload, EngineStateEventData {} export type AudioDeviceModuleEventType = | 'speechActivity' | 'devicesUpdated'; export type AudioDeviceModuleEventData = | SpeechActivityEventData | EngineStateEventData | Record<string, never>; // Empty object for events with no data export type AudioDeviceModuleEventListener = (data: AudioDeviceModuleEventData) => void; /** * Handler function that must return a number (0 for success, non-zero for error) */ export type AudioEngineEventNoParamsHandler = () => Promise<void>; export type AudioEngineEventHandler = (params: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean; }) => Promise<void>; /** * Event emitter for RTCAudioDeviceModule delegate callbacks. * iOS/macOS only. */ class AudioDeviceModuleEventEmitter { private engineCreatedHandler: AudioEngineEventNoParamsHandler | null = null; private willEnableEngineHandler: AudioEngineEventHandler | null = null; private willStartEngineHandler: AudioEngineEventHandler | null = null; private didStopEngineHandler: AudioEngineEventHandler | null = null; private didDisableEngineHandler: AudioEngineEventHandler | null = null; private willReleaseEngineHandler: AudioEngineEventNoParamsHandler | null = null; private listenersSetUp = false; public setupListeners() { if (Platform.OS !== 'android' && WebRTCModule) { // Reconcile on every call so a recreated native observer (which defaults // every hook to active) is re-synced to the current handler state even // when JS listeners are already registered. addListener appends without // de-duping, so the registration below must run only once. if (this.listenersSetUp) { this.reconcileActiveFlags(); return; } this.listenersSetUp = true; // Setup handlers for blocking delegate methods addListener(this, 'audioDeviceModuleEngineCreated', async (event: unknown) => { const { requestId } = event as EngineEventPayload; let result = 0; if (this.engineCreatedHandler) { try { await this.engineCreatedHandler(); } catch (error) { // If error is a number, use it as the error code, otherwise use -1 result = typeof error === 'number' ? error : -1; } } WebRTCModule.audioDeviceModuleResolveEngineCreated(requestId, result); }); addListener( this, 'audioDeviceModuleEngineWillEnable', async (event: unknown) => { const { requestId, isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventPayload; let result = 0; if (this.willEnableEngineHandler) { try { await this.willEnableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); } catch (error) { // If error is a number, use it as the error code, otherwise use -1 result = typeof error === 'number' ? error : -1; } } WebRTCModule.audioDeviceModuleResolveWillEnableEngine(requestId, result); }, ); addListener( this, 'audioDeviceModuleEngineWillStart', async (event: unknown) => { const { requestId, isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventPayload; let result = 0; if (this.willStartEngineHandler) { try { await this.willStartEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); } catch (error) { // If error is a number, use it as the error code, otherwise use -1 result = typeof error === 'number' ? error : -1; } } WebRTCModule.audioDeviceModuleResolveWillStartEngine(requestId, result); }, ); addListener( this, 'audioDeviceModuleEngineDidStop', async (event: unknown) => { const { requestId, isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventPayload; let result = 0; if (this.didStopEngineHandler) { try { await this.didStopEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); } catch (error) { // If error is a number, use it as the error code, otherwise use -1 result = typeof error === 'number' ? error : -1; } } WebRTCModule.audioDeviceModuleResolveDidStopEngine(requestId, result); }, ); addListener( this, 'audioDeviceModuleEngineDidDisable', async (event: unknown) => { const { requestId, isPlayoutEnabled, isRecordingEnabled } = event as EngineStateEventPayload; let result = 0; if (this.didDisableEngineHandler) { try { await this.didDisableEngineHandler({ isPlayoutEnabled, isRecordingEnabled }); } catch (error) { // If error is a number, use it as the error code, otherwise use -1 result = typeof error === 'number' ? error : -1; } } WebRTCModule.audioDeviceModuleResolveDidDisableEngine(requestId, result); }, ); addListener(this, 'audioDeviceModuleEngineWillRelease', async (event: unknown) => { const { requestId } = event as EngineEventPayload; let result = 0; if (this.willReleaseEngineHandler) { try { await this.willReleaseEngineHandler(); } catch (error) { // If error is a number, use it as the error code, otherwise use -1 result = typeof error === 'number' ? error : -1; } } WebRTCModule.audioDeviceModuleResolveWillReleaseEngine(requestId, result); }); this.reconcileActiveFlags(); } } /** * Subscribe to speech activity events (started/ended) */ addSpeechActivityListener(listener: (data: SpeechActivityEventData) => void) { addListener(listener, 'audioDeviceModuleSpeechActivity', listener as (event: unknown) => void); } /** * Remove a previously registered speech activity listener */ removeSpeechActivityListener(listener: (data: SpeechActivityEventData) => void) { removeListener(listener); } /** * Subscribe to devices updated event (input/output devices changed) */ addDevicesUpdatedListener(listener: () => void) { addListener(listener, 'audioDeviceModuleDevicesUpdated', listener as (event: unknown) => void); } /** * Remove a previously registered devices updated listener */ removeDevicesUpdatedListener(listener: () => void) { removeListener(listener); } /** * Push a handler's active state to native. The native active-flag setters are * iOS/macOS only, so this is gated like setupListeners() to avoid a TypeError * on Android, where these methods do not exist. */ private pushHandlerActive(method: string, isActive: boolean) { if (Platform.OS !== 'android' && WebRTCModule) { WebRTCModule[method](isActive); } } /** * Apply a handler change while keeping the native active flag ordered so a * delegate callback racing registration can never skip a handler that exists. * * On activation (null to non-null) the flag is pushed active *before* the * handler is published, so the worst case is a JS round trip whose listener * sees the not-yet-published handler and resolves 0, never a skipped veto. On * deactivation the handler is cleared first, then the flag pushed inactive, so * the same safe ordering holds in reverse. No push happens when active state * is unchanged. */ private applyHandlerActive(method: string, wasActive: boolean, isActive: boolean, assign: () => void) { if (isActive && !wasActive) { this.pushHandlerActive(method, true); } assign(); if (!isActive && wasActive) { this.pushHandlerActive(method, false); } } /** * Push the current handler state for every hook to native. Native defaults * each hook to active, so this re-syncs a fresh or recreated observer to the * handlers registered now rather than relying on a set/clear transition that * may have already happened. */ private reconcileActiveFlags() { const activeFlags: [string, boolean][] = [ [ 'audioDeviceModuleSetEngineCreatedActive', this.engineCreatedHandler !== null ], [ 'audioDeviceModuleSetWillEnableEngineActive', this.willEnableEngineHandler !== null ], [ 'audioDeviceModuleSetWillStartEngineActive', this.willStartEngineHandler !== null ], [ 'audioDeviceModuleSetDidStopEngineActive', this.didStopEngineHandler !== null ], [ 'audioDeviceModuleSetDidDisableEngineActive', this.didDisableEngineHandler !== null ], [ 'audioDeviceModuleSetWillReleaseEngineActive', this.willReleaseEngineHandler !== null ], ]; for (const [ method, isActive ] of activeFlags) { this.pushHandlerActive(method, isActive); } } /** * Set handler for engine created delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setEngineCreatedHandler(handler: AudioEngineEventNoParamsHandler | null) { this.applyHandlerActive( 'audioDeviceModuleSetEngineCreatedActive', this.engineCreatedHandler !== null, handler !== null, () => { this.engineCreatedHandler = handler; }, ); } /** * Set handler for will enable engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setWillEnableEngineHandler(handler: AudioEngineEventHandler | null) { this.applyHandlerActive( 'audioDeviceModuleSetWillEnableEngineActive', this.willEnableEngineHandler !== null, handler !== null, () => { this.willEnableEngineHandler = handler; }, ); } /** * Set handler for will start engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setWillStartEngineHandler(handler: AudioEngineEventHandler | null) { this.applyHandlerActive( 'audioDeviceModuleSetWillStartEngineActive', this.willStartEngineHandler !== null, handler !== null, () => { this.willStartEngineHandler = handler; }, ); } /** * Set handler for did stop engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setDidStopEngineHandler(handler: AudioEngineEventHandler | null) { this.applyHandlerActive( 'audioDeviceModuleSetDidStopEngineActive', this.didStopEngineHandler !== null, handler !== null, () => { this.didStopEngineHandler = handler; }, ); } /** * Set handler for did disable engine delegate - MUST return 0 for success or error code * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setDidDisableEngineHandler(handler: AudioEngineEventHandler | null) { this.applyHandlerActive( 'audioDeviceModuleSetDidDisableEngineActive', this.didDisableEngineHandler !== null, handler !== null, () => { this.didDisableEngineHandler = handler; }, ); } /** * Set handler for will release engine delegate * This handler blocks the native thread until it returns, throw to cancel audio engine's operation */ setWillReleaseEngineHandler(handler: AudioEngineEventNoParamsHandler | null) { this.applyHandlerActive( 'audioDeviceModuleSetWillReleaseEngineActive', this.willReleaseEngineHandler !== null, handler !== null, () => { this.willReleaseEngineHandler = handler; }, ); } } export const audioDeviceModuleEvents = new AudioDeviceModuleEventEmitter();