@stream-io/video-react-native-sdk 1.41.0 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/android/src/main/java/com/streamvideo/reactnative/audio/AudioDeviceManager.kt +79 -54
- package/android/src/main/java/com/streamvideo/reactnative/audio/utils/AudioManagerUtil.kt +6 -2
- package/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt +3 -3
- package/dist/commonjs/components/Call/RingingCallContent/OutgoingCall.js.map +1 -1
- package/dist/commonjs/components/Participant/FloatingParticipantView/index.js.map +1 -1
- package/dist/commonjs/contexts/ThemeContext.js +10 -0
- package/dist/commonjs/contexts/ThemeContext.js.map +1 -1
- package/dist/commonjs/hooks/index.js +11 -0
- package/dist/commonjs/hooks/index.js.map +1 -1
- package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +73 -0
- package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
- package/dist/commonjs/hooks/useAudioDeviceStatus.js +76 -0
- package/dist/commonjs/hooks/useAudioDeviceStatus.js.map +1 -0
- package/dist/commonjs/hooks/useScreenShareButton.js.map +1 -1
- package/dist/commonjs/modules/call-manager/CallManager.js +94 -47
- package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
- package/dist/commonjs/providers/NoiseCancellation/lib.js.map +1 -1
- package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js.map +1 -1
- package/dist/commonjs/utils/internal/callingx/callingx.js +0 -7
- package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/commonjs/utils/push/setupCallingExpEvents.js +0 -2
- package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/module/components/Call/RingingCallContent/OutgoingCall.js.map +1 -1
- package/dist/module/components/Participant/FloatingParticipantView/index.js.map +1 -1
- package/dist/module/contexts/ThemeContext.js +10 -0
- package/dist/module/contexts/ThemeContext.js.map +1 -1
- package/dist/module/hooks/index.js +1 -0
- package/dist/module/hooks/index.js.map +1 -1
- package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +73 -0
- package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
- package/dist/module/hooks/useAudioDeviceStatus.js +71 -0
- package/dist/module/hooks/useAudioDeviceStatus.js.map +1 -0
- package/dist/module/hooks/useScreenShareButton.js.map +1 -1
- package/dist/module/modules/call-manager/CallManager.js +94 -47
- package/dist/module/modules/call-manager/CallManager.js.map +1 -1
- package/dist/module/providers/NoiseCancellation/lib.js.map +1 -1
- package/dist/module/providers/StreamCall/AudioInterruptionTracer.js.map +1 -1
- package/dist/module/utils/internal/callingx/callingx.js +0 -7
- package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/module/utils/push/setupCallingExpEvents.js +0 -2
- package/dist/module/utils/push/setupCallingExpEvents.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/typescript/components/Participant/FloatingParticipantView/index.d.ts.map +1 -1
- package/dist/typescript/contexts/ThemeContext.d.ts +10 -1
- package/dist/typescript/contexts/ThemeContext.d.ts.map +1 -1
- package/dist/typescript/hooks/index.d.ts +1 -0
- package/dist/typescript/hooks/index.d.ts.map +1 -1
- package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +1 -1
- package/dist/typescript/hooks/useAudioDeviceStatus.d.ts +21 -0
- package/dist/typescript/hooks/useAudioDeviceStatus.d.ts.map +1 -0
- package/dist/typescript/hooks/useScreenShareButton.d.ts +2 -1
- package/dist/typescript/hooks/useScreenShareButton.d.ts.map +1 -1
- package/dist/typescript/modules/call-manager/CallManager.d.ts +25 -11
- package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
- package/dist/typescript/modules/call-manager/types.d.ts +29 -3
- package/dist/typescript/modules/call-manager/types.d.ts.map +1 -1
- package/dist/typescript/providers/NoiseCancellation/lib.d.ts.map +1 -1
- package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts.map +1 -1
- package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
- package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +1 -1
- package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +1 -1
- package/dist/typescript/version.d.ts +1 -1
- package/expo-config-plugin/dist/index.js +2 -0
- package/expo-config-plugin/dist/withAndroidMessagingService.js +313 -0
- package/expo-config-plugin/dist/withIosScreenCapture/addBroadcastExtensionXcodeTarget.js +84 -2
- package/expo-config-plugin/dist/withIosScreenCapture/withPlistUpdates.js +3 -6
- package/expo-config-plugin/dist/withIosScreenCapture/withTarget.js +6 -4
- package/expo-config-plugin/static/AudioUploader.swift +230 -0
- package/expo-config-plugin/static/SampleHandler.swift +67 -14
- package/ios/StreamInCallManager.m +7 -0
- package/ios/StreamInCallManager.swift +258 -15
- package/ios/StreamVideoReactNative.m +15 -5
- package/package.json +27 -25
- package/src/components/Call/RingingCallContent/OutgoingCall.tsx +1 -2
- package/src/components/Participant/FloatingParticipantView/index.tsx +1 -4
- package/src/contexts/ThemeContext.tsx +10 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +95 -0
- package/src/hooks/useAudioDeviceStatus.ts +90 -0
- package/src/hooks/useScreenShareButton.ts +2 -1
- package/src/modules/call-manager/CallManager.ts +123 -66
- package/src/modules/call-manager/native-module.d.ts +14 -7
- package/src/modules/call-manager/types.ts +30 -3
- package/src/providers/NoiseCancellation/lib.ts +1 -2
- package/src/providers/StreamCall/AudioInterruptionTracer.tsx +1 -2
- package/src/utils/StreamVideoRN/types.ts +1 -3
- package/src/utils/internal/callingx/callingx.ts +0 -8
- package/src/utils/push/setupCallingExpEvents.ts +0 -2
- package/src/version.ts +1 -1
- package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js +0 -68
- package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js.map +0 -1
- package/dist/module/utils/internal/callingx/audioSessionPromise.js +0 -63
- package/dist/module/utils/internal/callingx/audioSessionPromise.js.map +0 -1
- package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts +0 -16
- package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts.map +0 -1
- package/src/utils/internal/callingx/audioSessionPromise.ts +0 -65
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Call, CallingState, videoLoggerSystem } from '@stream-io/video-client';
|
|
2
2
|
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
3
3
|
import { useEffect, useMemo } from 'react';
|
|
4
|
+
import { Platform } from 'react-native';
|
|
5
|
+
import { AudioDeviceModule } from '@stream-io/react-native-webrtc';
|
|
4
6
|
import { filter, take } from 'rxjs/operators';
|
|
5
7
|
import { getCallDisplayName } from '../../utils/internal/callingx/callingx';
|
|
6
8
|
import { getCallingxLibIfAvailable } from '../../utils/push/libs/callingx';
|
|
@@ -190,4 +192,97 @@ export const useCallingExpWithCallingStateEffect = () => {
|
|
|
190
192
|
subscription.remove();
|
|
191
193
|
};
|
|
192
194
|
}, [activeCallCid, microphone]);
|
|
195
|
+
|
|
196
|
+
// Trace CallKit audio session activation/deactivation onto the call's tracer.
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
const callingx = getCallingxLibIfAvailable();
|
|
199
|
+
if (!callingx?.isSetup || !activeCall) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const activateSubscription = callingx.addEventListener(
|
|
204
|
+
'didActivateAudioSession',
|
|
205
|
+
() => {
|
|
206
|
+
activeCall.tracer.trace('callingx.didActivateAudioSession', null);
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
const deactivateSubscription = callingx.addEventListener(
|
|
210
|
+
'didDeactivateAudioSession',
|
|
211
|
+
() => {
|
|
212
|
+
activeCall.tracer.trace('callingx.didDeactivateAudioSession', null);
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return () => {
|
|
217
|
+
activateSubscription.remove();
|
|
218
|
+
deactivateSubscription.remove();
|
|
219
|
+
};
|
|
220
|
+
}, [activeCall]);
|
|
221
|
+
|
|
222
|
+
// Trace the iOS ADM recording state to detect silent-mic calls: the engine's
|
|
223
|
+
// input bring-up racing the CallKit audio-session activation could leave
|
|
224
|
+
// recording silently never started (mic track live, zero audio RTP sent).
|
|
225
|
+
// Samples shortly after JOINED and after every didActivateAudioSession, and
|
|
226
|
+
// traces healthy calls too so the failure rate is measurable. Detection only;
|
|
227
|
+
// the engine-availability gate in callingx owns prevention.
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const callingx = getCallingxLibIfAvailable();
|
|
230
|
+
if (Platform.OS !== 'ios' || !callingx?.isSetup || !activeCall) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const timeouts = new Set<ReturnType<typeof setTimeout>>();
|
|
235
|
+
|
|
236
|
+
const traceRecordingState = (trigger: string) => {
|
|
237
|
+
if (activeCall.state.callingState !== CallingState.JOINED) return;
|
|
238
|
+
if (activeCall.microphone.state.status !== 'enabled') return;
|
|
239
|
+
|
|
240
|
+
let state;
|
|
241
|
+
try {
|
|
242
|
+
state = {
|
|
243
|
+
trigger,
|
|
244
|
+
isRecording: AudioDeviceModule.isRecording(),
|
|
245
|
+
isEngineRunning: AudioDeviceModule.isEngineRunning(),
|
|
246
|
+
isMicrophoneMuted: AudioDeviceModule.isMicrophoneMuted(),
|
|
247
|
+
};
|
|
248
|
+
} catch (error: unknown) {
|
|
249
|
+
logger.warn('failed to read the ADM recording state', error);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
activeCall.tracer.trace('ios.audioRecording.state', state);
|
|
254
|
+
if (!state.isRecording) {
|
|
255
|
+
logger.warn('mic is enabled but ADM is not recording', state);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const scheduleCheck = (trigger: string, delayMs: number) => {
|
|
260
|
+
const timeout = setTimeout(() => {
|
|
261
|
+
timeouts.delete(timeout);
|
|
262
|
+
traceRecordingState(trigger);
|
|
263
|
+
}, delayMs);
|
|
264
|
+
timeouts.add(timeout);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const joinedSubscription = activeCall.state.callingState$
|
|
268
|
+
.pipe(
|
|
269
|
+
filter((callingState) => callingState === CallingState.JOINED),
|
|
270
|
+
take(1),
|
|
271
|
+
)
|
|
272
|
+
// delayed so the normal engine bring-up can complete on its own
|
|
273
|
+
.subscribe(() => scheduleCheck('joined', 3000));
|
|
274
|
+
|
|
275
|
+
const activationSubscription = callingx.addEventListener(
|
|
276
|
+
'didActivateAudioSession',
|
|
277
|
+
// delayed so the activation-driven engine start can run first
|
|
278
|
+
() => scheduleCheck('audioSessionActivated', 1000),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return () => {
|
|
282
|
+
joinedSubscription.unsubscribe();
|
|
283
|
+
activationSubscription.remove();
|
|
284
|
+
timeouts.forEach(clearTimeout);
|
|
285
|
+
timeouts.clear();
|
|
286
|
+
};
|
|
287
|
+
}, [activeCall]);
|
|
193
288
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { callManager } from '../modules/call-manager';
|
|
3
|
+
import type { AudioDevicesState } from '../modules/call-manager/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Structural equality for two audio device states to avoid redundant re-renders.
|
|
7
|
+
*/
|
|
8
|
+
const areStatesEqual = (
|
|
9
|
+
a: AudioDevicesState | undefined,
|
|
10
|
+
b: AudioDevicesState,
|
|
11
|
+
): boolean => {
|
|
12
|
+
if (!a) return false;
|
|
13
|
+
if (
|
|
14
|
+
a.selectedDeviceId !== b.selectedDeviceId ||
|
|
15
|
+
a.currentEndpointType !== b.currentEndpointType ||
|
|
16
|
+
a.devices.length !== b.devices.length
|
|
17
|
+
) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return a.devices.every((device, i) => {
|
|
21
|
+
const other = b.devices[i];
|
|
22
|
+
return (
|
|
23
|
+
other !== undefined &&
|
|
24
|
+
device.id === other.id &&
|
|
25
|
+
device.name === other.name &&
|
|
26
|
+
device.type === other.type
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Subscribes to the current audio output device state and keeps it in sync.
|
|
33
|
+
*
|
|
34
|
+
* Works on Android, iOS, and iOS with CallKit
|
|
35
|
+
* (`@stream-io/react-native-callingx`) — the underlying source is selected
|
|
36
|
+
* automatically. Pair with `callManager.audioDevices.select(device.id)` to
|
|
37
|
+
* build a custom audio route picker.
|
|
38
|
+
*
|
|
39
|
+
* @returns the latest {@link AudioDevicesState}, or `undefined` until the
|
|
40
|
+
* initial status resolves.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* const status = useAudioDeviceStatus();
|
|
45
|
+
* const { devices, selectedDeviceId } = status ?? {};
|
|
46
|
+
* // callManager.audioDevices.select(devices[0].id);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export const useAudioDeviceStatus = (): AudioDevicesState | undefined => {
|
|
50
|
+
const [state, setState] = useState<AudioDevicesState>();
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
let active = true;
|
|
54
|
+
let receivedUpdate = false;
|
|
55
|
+
let unsubscribe = () => {};
|
|
56
|
+
|
|
57
|
+
const applyIfChanged = (next: AudioDevicesState) => {
|
|
58
|
+
setState((prev) => (areStatesEqual(prev, next) ? prev : next));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Live change events take precedence over the initial fetch.
|
|
62
|
+
const onChange = (next: AudioDevicesState) => {
|
|
63
|
+
if (!active) return;
|
|
64
|
+
receivedUpdate = true;
|
|
65
|
+
applyIfChanged(next);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Subscribe before fetching so no change between the two is missed.
|
|
70
|
+
unsubscribe = callManager.audioDevices.addChangeListener(onChange);
|
|
71
|
+
callManager.audioDevices
|
|
72
|
+
.getStatus()
|
|
73
|
+
.then((initial) => {
|
|
74
|
+
// Don't let the initial snapshot clobber newer event-driven state,
|
|
75
|
+
// and ignore it entirely after unmount.
|
|
76
|
+
if (active && !receivedUpdate) applyIfChanged(initial);
|
|
77
|
+
})
|
|
78
|
+
.catch(() => {});
|
|
79
|
+
} catch {
|
|
80
|
+
// Native/bridge error during setup — leave state undefined.
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
active = false;
|
|
85
|
+
unsubscribe();
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
return state;
|
|
90
|
+
};
|
|
@@ -37,7 +37,8 @@ export type ScreenShareOptions = {
|
|
|
37
37
|
* (e.g., YouTube video audio) mixed with the user's microphone.
|
|
38
38
|
*
|
|
39
39
|
* - iOS in-app: Audio captured from RPScreenRecorder `.audioApp` buffers.
|
|
40
|
-
* - iOS broadcast: Audio
|
|
40
|
+
* - iOS broadcast: Audio captured in the Broadcast Upload Extension and
|
|
41
|
+
* streamed to the app over a dedicated socket, then mixed into the audio track.
|
|
41
42
|
* - Android: Audio captured via AudioPlaybackCaptureConfiguration (API 29+).
|
|
42
43
|
*
|
|
43
44
|
* Default: `false`.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
|
2
2
|
import type {
|
|
3
|
-
|
|
3
|
+
AudioDeviceEndpointType,
|
|
4
|
+
AudioDevicesState,
|
|
4
5
|
IOSAudioInterruptionEvent,
|
|
5
6
|
StreamInCallManagerConfig,
|
|
6
7
|
} from './types';
|
|
@@ -14,11 +15,24 @@ import { videoLoggerSystem } from '@stream-io/video-client';
|
|
|
14
15
|
const NativeManager = NativeModules.StreamInCallManager;
|
|
15
16
|
const CallingxModule = getCallingxLibIfAvailable();
|
|
16
17
|
const AUDIO_INTERRUPTION_EVENT = 'StreamInCallManagerAudioInterruption';
|
|
18
|
+
const AUDIO_DEVICE_CHANGED_EVENT = 'onAudioDeviceChanged';
|
|
17
19
|
|
|
18
20
|
const invariant = (condition: boolean, message: string) => {
|
|
19
21
|
if (!condition) throw new Error(message);
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Runs a fire-and-forget native call, logging instead of throwing on a bridge
|
|
26
|
+
* error so it can't crash the caller or an event-dispatch loop.
|
|
27
|
+
*/
|
|
28
|
+
const safeNativeCall = (label: string, fn: () => void): void => {
|
|
29
|
+
try {
|
|
30
|
+
fn();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
videoLoggerSystem.getLogger('CallManager').warn(`${label} failed`, error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
22
36
|
/**
|
|
23
37
|
* On Android, whether the current call is managed by Telecom (via callingx).
|
|
24
38
|
* In that mode, audio routing/mode is owned by Telecom and StreamInCallManager audio methods should not be used
|
|
@@ -41,8 +55,7 @@ const isAndroidTelecomManaged = (): boolean => {
|
|
|
41
55
|
* the extra narrowing here is what makes that provable to the type-checker.
|
|
42
56
|
*/
|
|
43
57
|
const getTelecomContext = ():
|
|
44
|
-
|
|
45
|
-
| undefined => {
|
|
58
|
+
{ cx: NonNullable<typeof CallingxModule>; callId: string } | undefined => {
|
|
46
59
|
if (!isAndroidTelecomManaged() || !CallingxModule) {
|
|
47
60
|
return undefined;
|
|
48
61
|
}
|
|
@@ -53,10 +66,8 @@ const getTelecomContext = ():
|
|
|
53
66
|
return { cx: CallingxModule, callId };
|
|
54
67
|
};
|
|
55
68
|
|
|
56
|
-
/** Map a generic Telecom endpoint type to the SDK's endpoint
|
|
57
|
-
const endpointTypeToDisplayName = (
|
|
58
|
-
type: string,
|
|
59
|
-
): AudioDeviceStatus['currentEndpointType'] => {
|
|
69
|
+
/** Map a generic Telecom endpoint type to the SDK's endpoint type. */
|
|
70
|
+
const endpointTypeToDisplayName = (type: string): AudioDeviceEndpointType => {
|
|
60
71
|
switch (type) {
|
|
61
72
|
case 'earpiece':
|
|
62
73
|
return 'Earpiece';
|
|
@@ -71,95 +82,136 @@ const endpointTypeToDisplayName = (
|
|
|
71
82
|
}
|
|
72
83
|
};
|
|
73
84
|
|
|
74
|
-
/** Adapt a callingx endpoints snapshot to the SDK's {@link
|
|
75
|
-
const
|
|
85
|
+
/** Adapt a callingx endpoints snapshot to the SDK's {@link AudioDevicesState}. */
|
|
86
|
+
const snapshotToState = (
|
|
76
87
|
snapshot: CallingxAudioSnapshot,
|
|
77
|
-
):
|
|
78
|
-
|
|
88
|
+
): AudioDevicesState => ({
|
|
89
|
+
// callingx endpoint ids are stable and unique — use them directly as the
|
|
90
|
+
// device id, so `select(device.id)` maps straight back to a Telecom endpoint.
|
|
91
|
+
devices: snapshot.endpoints.map((e) => ({
|
|
92
|
+
id: e.id,
|
|
93
|
+
name: e.name,
|
|
94
|
+
type: endpointTypeToDisplayName(e.type),
|
|
95
|
+
})),
|
|
96
|
+
selectedDeviceId: snapshot.currentEndpoint?.id,
|
|
79
97
|
currentEndpointType: snapshot.currentEndpoint
|
|
80
98
|
? endpointTypeToDisplayName(snapshot.currentEndpoint.type)
|
|
81
99
|
: 'Unknown',
|
|
82
|
-
selectedDevice: snapshot.currentEndpoint?.name ?? '',
|
|
83
100
|
});
|
|
84
101
|
|
|
85
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Cross-platform audio output device picker.
|
|
104
|
+
*/
|
|
105
|
+
class AudioDevicesManager {
|
|
86
106
|
private eventEmitter?: NativeEventEmitter;
|
|
107
|
+
private interruptionReassertSetup = false;
|
|
87
108
|
|
|
88
109
|
/**
|
|
89
|
-
*
|
|
110
|
+
* iOS + CallKit only: re-assert the user's output pick after an interruption ends.
|
|
111
|
+
*
|
|
112
|
+
* On interruption-end iOS clears the ephemeral route override (`setPreferredInput` /
|
|
113
|
+
* `overrideOutputAudioPort`) and doesn't restore it, and callingx re-applies its config
|
|
114
|
+
* only on engine-enable — not the engine-restart of interruption recovery.
|
|
90
115
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
private ensureInterruptionReassert = () => {
|
|
117
|
+
if (this.interruptionReassertSetup) return; // subscibed once per app lifetime
|
|
118
|
+
if (Platform.OS !== 'ios' || !CallingxModule) return;
|
|
119
|
+
this.interruptionReassertSetup = true;
|
|
120
|
+
CallingxModule.addEventListener('didAudioInterruption', (event) => {
|
|
121
|
+
if (event.phase === 'ended') {
|
|
122
|
+
safeNativeCall('reapplyAudioRoute', () =>
|
|
123
|
+
NativeManager.reapplyAudioRoute(),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the current audio device state (available devices + the selected one).
|
|
131
|
+
* Read directly from the audio session, so it works on every path.
|
|
132
|
+
*/
|
|
133
|
+
getStatus = async (): Promise<AudioDevicesState> => {
|
|
134
|
+
// Android Telecom owns routing: read the endpoint snapshot from callingx.
|
|
93
135
|
const tc = getTelecomContext();
|
|
94
136
|
if (tc) {
|
|
95
137
|
const snapshot = await tc.cx.getAvailableAudioEndpoints(tc.callId);
|
|
96
|
-
return
|
|
138
|
+
return snapshotToState(snapshot);
|
|
97
139
|
}
|
|
98
140
|
return NativeManager.getAudioDeviceStatus();
|
|
99
141
|
};
|
|
100
142
|
|
|
101
143
|
/**
|
|
102
|
-
*
|
|
144
|
+
* Switch the audio output to the device with the given id.
|
|
103
145
|
*
|
|
104
|
-
* @param
|
|
146
|
+
* @param deviceId the stable {@link AudioDevice.id} (not the display name).
|
|
105
147
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
148
|
+
select = (deviceId: string): void => {
|
|
149
|
+
// Android Telecom owns routing: the device id is the callingx endpoint id,
|
|
150
|
+
// so route by it directly (no name lookup needed).
|
|
108
151
|
const tc = getTelecomContext();
|
|
109
152
|
if (tc) {
|
|
110
153
|
const { cx, callId } = tc;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
154
|
+
cx.requestAudioEndpointChange(callId, deviceId).catch((error) => {
|
|
155
|
+
videoLoggerSystem
|
|
156
|
+
.getLogger('CallManager')
|
|
157
|
+
.warn(
|
|
158
|
+
`select: failed to route to "${deviceId}" for call ${callId} via Telecom`,
|
|
159
|
+
error,
|
|
116
160
|
);
|
|
117
|
-
|
|
118
|
-
return cx.requestAudioEndpointChange(callId, target.id);
|
|
119
|
-
}
|
|
120
|
-
return undefined;
|
|
121
|
-
})
|
|
122
|
-
.catch((error) => {
|
|
123
|
-
videoLoggerSystem
|
|
124
|
-
.getLogger('CallManager')
|
|
125
|
-
.warn(
|
|
126
|
-
`selectAudioDevice: failed to route to "${endpointName}" for call ${callId} via Telecom`,
|
|
127
|
-
error,
|
|
128
|
-
);
|
|
129
|
-
});
|
|
161
|
+
});
|
|
130
162
|
return;
|
|
131
163
|
}
|
|
132
|
-
|
|
164
|
+
this.ensureInterruptionReassert();
|
|
165
|
+
safeNativeCall('chooseAudioDeviceEndpoint', () =>
|
|
166
|
+
NativeManager.chooseAudioDeviceEndpoint(deviceId),
|
|
167
|
+
);
|
|
133
168
|
};
|
|
134
169
|
|
|
135
170
|
/**
|
|
136
|
-
* Register a listener for audio device changes.
|
|
137
|
-
*
|
|
171
|
+
* Register a listener for audio device changes. Returns an unsubscribe fn.
|
|
172
|
+
*
|
|
173
|
+
* @param onChange called with the latest {@link AudioDevicesState} on change.
|
|
138
174
|
*/
|
|
139
|
-
|
|
140
|
-
onChange: (
|
|
175
|
+
addChangeListener = (
|
|
176
|
+
onChange: (state: AudioDevicesState) => void,
|
|
141
177
|
): (() => void) => {
|
|
142
|
-
|
|
143
|
-
const
|
|
178
|
+
let active = true;
|
|
179
|
+
const unsubscribes: Array<() => void> = [];
|
|
144
180
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
// callingx-owned route changes (Android Telecom + iOS CallKit). The event is
|
|
182
|
+
// signal-only, so re-read the current state — ignoring a resolve that lands
|
|
183
|
+
// after unsubscribe, and swallowing bridge rejections.
|
|
184
|
+
if (CallingxModule) {
|
|
185
|
+
const cxSub = CallingxModule.addEventListener(
|
|
186
|
+
'didChangeAudioRoute',
|
|
187
|
+
() => {
|
|
188
|
+
this.getStatus()
|
|
189
|
+
.then((state) => {
|
|
190
|
+
if (active) onChange(state);
|
|
191
|
+
})
|
|
192
|
+
.catch((error) => {
|
|
193
|
+
videoLoggerSystem
|
|
194
|
+
.getLogger('CallManager')
|
|
195
|
+
.warn('addChangeListener: getStatus failed', error);
|
|
196
|
+
});
|
|
197
|
+
},
|
|
150
198
|
);
|
|
151
|
-
|
|
199
|
+
unsubscribes.push(() => cxSub.remove());
|
|
152
200
|
}
|
|
153
201
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
202
|
+
// SDK-managed route changes (non-callingx calls).
|
|
203
|
+
this.eventEmitter =
|
|
204
|
+
this.eventEmitter ?? new NativeEventEmitter(NativeManager);
|
|
205
|
+
const sdkSub = this.eventEmitter.addListener(
|
|
206
|
+
AUDIO_DEVICE_CHANGED_EVENT,
|
|
207
|
+
onChange,
|
|
208
|
+
);
|
|
209
|
+
unsubscribes.push(() => sdkSub.remove());
|
|
161
210
|
|
|
162
|
-
return () =>
|
|
211
|
+
return () => {
|
|
212
|
+
active = false;
|
|
213
|
+
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
214
|
+
};
|
|
163
215
|
};
|
|
164
216
|
}
|
|
165
217
|
|
|
@@ -258,7 +310,7 @@ const shouldBypassForCallKit = (): boolean => {
|
|
|
258
310
|
};
|
|
259
311
|
|
|
260
312
|
export class CallManager {
|
|
261
|
-
|
|
313
|
+
audioDevices = new AudioDevicesManager();
|
|
262
314
|
ios = new IOSCallManager();
|
|
263
315
|
speaker = new SpeakerManager();
|
|
264
316
|
|
|
@@ -281,11 +333,14 @@ export class CallManager {
|
|
|
281
333
|
*/
|
|
282
334
|
start = (config?: StreamInCallManagerConfig): void => {
|
|
283
335
|
if (shouldBypassForCallKit()) {
|
|
284
|
-
// Forward only the passive endpoint preference; callingx reads it when
|
|
285
|
-
// CallKit drives session activation.
|
|
286
336
|
if (config?.audioRole === 'communicator' && CallingxModule) {
|
|
287
337
|
const type = config.deviceEndpointType ?? 'speaker';
|
|
288
|
-
|
|
338
|
+
safeNativeCall('setDefaultAudioDeviceEndpointType (callingx)', () =>
|
|
339
|
+
CallingxModule.setDefaultAudioDeviceEndpointType(type),
|
|
340
|
+
);
|
|
341
|
+
safeNativeCall('setDefaultAudioDeviceEndpointType', () =>
|
|
342
|
+
NativeManager.setDefaultAudioDeviceEndpointType(type),
|
|
343
|
+
);
|
|
289
344
|
}
|
|
290
345
|
videoLoggerSystem
|
|
291
346
|
.getLogger('CallManager')
|
|
@@ -298,8 +353,10 @@ export class CallManager {
|
|
|
298
353
|
// Telecom owns routing/focus; forward the sticky preference to callingx and run in
|
|
299
354
|
// telecom-managed mode (StreamInCallManager keeps proximity/keep-screen-on only).
|
|
300
355
|
if (config?.audioRole !== 'listener' && CallingxModule) {
|
|
301
|
-
|
|
302
|
-
|
|
356
|
+
safeNativeCall('setDefaultAudioDeviceEndpointType (callingx)', () =>
|
|
357
|
+
CallingxModule.setDefaultAudioDeviceEndpointType(
|
|
358
|
+
config?.deviceEndpointType ?? 'speaker',
|
|
359
|
+
),
|
|
303
360
|
);
|
|
304
361
|
}
|
|
305
362
|
NativeManager.setTelecomManagedMode(true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'react-native';
|
|
2
2
|
import type { NativeModule } from 'react-native';
|
|
3
|
-
import type {
|
|
3
|
+
import type { AudioDevicesState, AudioRole, DeviceEndpointType } from './types';
|
|
4
4
|
|
|
5
5
|
export interface CallManager extends NativeModule {
|
|
6
6
|
/**
|
|
@@ -33,16 +33,23 @@ export interface CallManager extends NativeModule {
|
|
|
33
33
|
setTelecomManagedMode: (enabled: boolean) => void;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Choose an audio device endpoint.
|
|
37
|
-
* @param
|
|
36
|
+
* Choose an audio device endpoint by its stable id.
|
|
37
|
+
* @param deviceId - The id of the audio device to choose (`AudioDevice.id`).
|
|
38
38
|
*/
|
|
39
|
-
chooseAudioDeviceEndpoint: (
|
|
39
|
+
chooseAudioDeviceEndpoint: (deviceId: string) => void;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* Re-applies the current audio output pick (iOS). Used to restore a Bluetooth/wired
|
|
43
|
+
* route after an interruption ends, where the ephemeral route override is lost.
|
|
44
|
+
* No-op if no explicit pick has been made.
|
|
44
45
|
*/
|
|
45
|
-
|
|
46
|
+
reapplyAudioRoute: () => void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the current audio device state (available devices + selected one).
|
|
50
|
+
* @returns The audio devices state.
|
|
51
|
+
*/
|
|
52
|
+
getAudioDeviceStatus: () => Promise<AudioDevicesState>;
|
|
46
53
|
|
|
47
54
|
/**
|
|
48
55
|
* Shows the iOS audio route picker.
|
|
@@ -6,10 +6,37 @@ export type AudioDeviceEndpointType =
|
|
|
6
6
|
| 'Unknown'
|
|
7
7
|
| (string & {});
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* A selectable audio output device.
|
|
11
|
+
*
|
|
12
|
+
* Devices are identified by a stable {@link AudioDevice.id} — never by name —
|
|
13
|
+
* so two devices that share a display name (e.g. two "AirPods Pro") never
|
|
14
|
+
* collide. On iOS the id is the `AVAudioSessionPortDescription.uid`
|
|
15
|
+
* (or the synthetic `'speaker'` / `'earpiece'` for the built-in outputs).
|
|
16
|
+
* On Android it is the `AudioDeviceInfo.id` for SDK-managed routing, or the
|
|
17
|
+
* Telecom endpoint id (a `CallEndpointCompat` identifier) when the call is
|
|
18
|
+
* Telecom-managed via callingx — stable for the lifetime of that call.
|
|
19
|
+
*/
|
|
20
|
+
export type AudioDevice = {
|
|
21
|
+
/** Stable, unique device identifier. Pass this to `callManager.audioDevices.select()`. */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Human-readable label for display (e.g. "AirPods Pro", "Speaker"). Not stable — do not use as a key. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** The kind of endpoint, useful for picking an icon. */
|
|
26
|
+
type: AudioDeviceEndpointType;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The current audio-output device state. Returned by
|
|
31
|
+
* `callManager.audioDevices.getStatus()` and the `useAudioDeviceStatus()` hook.
|
|
32
|
+
*/
|
|
33
|
+
export type AudioDevicesState = {
|
|
34
|
+
/** All currently available output devices. */
|
|
35
|
+
devices: AudioDevice[];
|
|
36
|
+
/** The id of the active device, matching one of `devices[].id` (when known). */
|
|
37
|
+
selectedDeviceId?: string;
|
|
38
|
+
/** The endpoint type of the active device. */
|
|
11
39
|
currentEndpointType: AudioDeviceEndpointType;
|
|
12
|
-
selectedDevice: string;
|
|
13
40
|
};
|
|
14
41
|
|
|
15
42
|
export type AudioRole = 'communicator' | 'listener';
|
|
@@ -17,8 +17,7 @@ export function getNoiseCancellationLibThrowIfNotInstalled() {
|
|
|
17
17
|
|
|
18
18
|
export class NoiseCancellationWrapper {
|
|
19
19
|
private static noiseCancellationInstance:
|
|
20
|
-
|
|
21
|
-
| undefined;
|
|
20
|
+
InstanceType<Type['NoiseCancellation']> | undefined;
|
|
22
21
|
|
|
23
22
|
private constructor() {}
|
|
24
23
|
|
|
@@ -34,8 +34,7 @@ export const AudioInterruptionTracer = () => {
|
|
|
34
34
|
const removeInCallManagerListener =
|
|
35
35
|
callManager.ios.addAudioInterruptionListener(traceAudioInterruption);
|
|
36
36
|
const callingx = getCallingxLibIfAvailable() as
|
|
37
|
-
|
|
|
38
|
-
| undefined;
|
|
37
|
+
CallingxAudioInterruptionEvents | undefined;
|
|
39
38
|
const callingxSubscription = callingx?.addEventListener(
|
|
40
39
|
'didAudioInterruption',
|
|
41
40
|
traceAudioInterruption,
|
|
@@ -17,9 +17,7 @@ export type KeepAliveAndroidNotificationTexts = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export type NonRingingPushEvent =
|
|
20
|
-
| 'call.
|
|
21
|
-
| 'call.notification'
|
|
22
|
-
| 'call.missed';
|
|
20
|
+
'call.live_started' | 'call.notification' | 'call.missed';
|
|
23
21
|
|
|
24
22
|
export type StreamVideoConfig = {
|
|
25
23
|
/**
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { NativeModules, Platform } from 'react-native';
|
|
6
6
|
import type { EndCallReason } from '@stream-io/react-native-callingx';
|
|
7
7
|
import { getCallingxLibIfAvailable } from '../../push/libs/callingx';
|
|
8
|
-
import { waitForAudioSessionActivation } from './audioSessionPromise';
|
|
9
8
|
import type {
|
|
10
9
|
Call,
|
|
11
10
|
MemberResponse,
|
|
@@ -171,9 +170,6 @@ export async function joinCallingxCall(call: Call, activeCalls: Call[]) {
|
|
|
171
170
|
callDisplayName, // display name for display in call screen
|
|
172
171
|
call.state.settings?.video?.enabled ?? false, // is video call?
|
|
173
172
|
);
|
|
174
|
-
if (Platform.OS === 'ios') {
|
|
175
|
-
await waitForAudioSessionActivation();
|
|
176
|
-
}
|
|
177
173
|
};
|
|
178
174
|
|
|
179
175
|
if (
|
|
@@ -220,10 +216,6 @@ export async function joinCallingxCall(call: Call, activeCalls: Call[]) {
|
|
|
220
216
|
);
|
|
221
217
|
|
|
222
218
|
await CallingxModule.answerIncomingCall(call.cid);
|
|
223
|
-
|
|
224
|
-
if (Platform.OS === 'ios') {
|
|
225
|
-
await waitForAudioSessionActivation();
|
|
226
|
-
}
|
|
227
219
|
} catch (error) {
|
|
228
220
|
logger.error(
|
|
229
221
|
`Error joining incoming call in callingx: ${call.cid}`,
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
clearPushWSEventSubscriptions,
|
|
9
9
|
processCallFromPushInBackground,
|
|
10
10
|
} from './internal/utils';
|
|
11
|
-
import { resolvePendingAudioSession } from '../internal/callingx/audioSessionPromise';
|
|
12
11
|
import {
|
|
13
12
|
getCallingxLib,
|
|
14
13
|
type EventData,
|
|
@@ -81,7 +80,6 @@ export function setupCallingExpEvents(pushConfig: NonNullable<PushConfig>) {
|
|
|
81
80
|
|
|
82
81
|
const onDidActivateAudioSession = () => {
|
|
83
82
|
logger.debug('callingExpDidActivateAudioSession');
|
|
84
|
-
resolvePendingAudioSession();
|
|
85
83
|
};
|
|
86
84
|
|
|
87
85
|
const onDidDeactivateAudioSession = () => {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '1.
|
|
1
|
+
export const version = '1.43.0';
|