@livekit/react-native-webrtc
Version:
WebRTC for React Native
370 lines (317 loc) • 14.5 kB
text/typescript
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();