@stream-io/video-react-native-sdk 1.41.0 → 1.42.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 +26 -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/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/modules/call-manager/CallManager.js +94 -47
- package/dist/commonjs/modules/call-manager/CallManager.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/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/modules/call-manager/CallManager.js +94 -47
- package/dist/module/modules/call-manager/CallManager.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/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/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/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 +287 -0
- package/ios/StreamInCallManager.m +7 -0
- package/ios/StreamInCallManager.swift +257 -14
- package/package.json +8 -8
- package/src/hooks/index.ts +1 -0
- package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +95 -0
- package/src/hooks/useAudioDeviceStatus.ts +90 -0
- package/src/modules/call-manager/CallManager.ts +122 -64
- package/src/modules/call-manager/native-module.d.ts +14 -7
- package/src/modules/call-manager/types.ts +30 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/video-react-native-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.42.0",
|
|
4
4
|
"description": "Stream Video SDK for React Native",
|
|
5
5
|
"author": "https://getstream.io",
|
|
6
6
|
"homepage": "https://getstream.io/video/docs/react-native/",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"!**/.*"
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@stream-io/video-client": "1.
|
|
56
|
-
"@stream-io/video-react-bindings": "1.
|
|
55
|
+
"@stream-io/video-client": "1.56.0",
|
|
56
|
+
"@stream-io/video-react-bindings": "1.19.1",
|
|
57
57
|
"intl-pluralrules": "2.0.1",
|
|
58
58
|
"react-native-url-polyfill": "^3.0.0",
|
|
59
59
|
"rxjs": "~7.8.2",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@react-native-firebase/messaging": ">=17.5.0",
|
|
66
66
|
"@stream-io/noise-cancellation-react-native": ">=0.1.0",
|
|
67
67
|
"@stream-io/react-native-callingx": ">=0.1.0",
|
|
68
|
-
"@stream-io/react-native-webrtc": "^145.
|
|
68
|
+
"@stream-io/react-native-webrtc": "^145.1.1",
|
|
69
69
|
"@stream-io/video-filters-react-native": ">=0.1.0",
|
|
70
70
|
"expo": ">=47.0.0",
|
|
71
71
|
"expo-notifications": "*",
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"@react-native-firebase/messaging": "^24.0.0",
|
|
115
115
|
"@react-native/babel-preset": "0.85.3",
|
|
116
116
|
"@react-native/metro-config": "0.85.3",
|
|
117
|
-
"@stream-io/noise-cancellation-react-native": "^0.9.
|
|
118
|
-
"@stream-io/react-native-callingx": "^0.
|
|
119
|
-
"@stream-io/react-native-webrtc": "145.
|
|
117
|
+
"@stream-io/noise-cancellation-react-native": "^0.9.5",
|
|
118
|
+
"@stream-io/react-native-callingx": "^0.8.1",
|
|
119
|
+
"@stream-io/react-native-webrtc": "145.1.1",
|
|
120
120
|
"@stream-io/typescript-config": "^0.0.0",
|
|
121
|
-
"@stream-io/video-filters-react-native": "^0.14.
|
|
121
|
+
"@stream-io/video-filters-react-native": "^0.14.5",
|
|
122
122
|
"@testing-library/jest-native": "^5.4.3",
|
|
123
123
|
"@testing-library/react-native": "13.3.3",
|
|
124
124
|
"@tsconfig/node18": "^18.2.6",
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
+
};
|
|
@@ -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
|
|
@@ -53,10 +67,8 @@ const getTelecomContext = ():
|
|
|
53
67
|
return { cx: CallingxModule, callId };
|
|
54
68
|
};
|
|
55
69
|
|
|
56
|
-
/** Map a generic Telecom endpoint type to the SDK's endpoint
|
|
57
|
-
const endpointTypeToDisplayName = (
|
|
58
|
-
type: string,
|
|
59
|
-
): AudioDeviceStatus['currentEndpointType'] => {
|
|
70
|
+
/** Map a generic Telecom endpoint type to the SDK's endpoint type. */
|
|
71
|
+
const endpointTypeToDisplayName = (type: string): AudioDeviceEndpointType => {
|
|
60
72
|
switch (type) {
|
|
61
73
|
case 'earpiece':
|
|
62
74
|
return 'Earpiece';
|
|
@@ -71,95 +83,136 @@ const endpointTypeToDisplayName = (
|
|
|
71
83
|
}
|
|
72
84
|
};
|
|
73
85
|
|
|
74
|
-
/** Adapt a callingx endpoints snapshot to the SDK's {@link
|
|
75
|
-
const
|
|
86
|
+
/** Adapt a callingx endpoints snapshot to the SDK's {@link AudioDevicesState}. */
|
|
87
|
+
const snapshotToState = (
|
|
76
88
|
snapshot: CallingxAudioSnapshot,
|
|
77
|
-
):
|
|
78
|
-
|
|
89
|
+
): AudioDevicesState => ({
|
|
90
|
+
// callingx endpoint ids are stable and unique — use them directly as the
|
|
91
|
+
// device id, so `select(device.id)` maps straight back to a Telecom endpoint.
|
|
92
|
+
devices: snapshot.endpoints.map((e) => ({
|
|
93
|
+
id: e.id,
|
|
94
|
+
name: e.name,
|
|
95
|
+
type: endpointTypeToDisplayName(e.type),
|
|
96
|
+
})),
|
|
97
|
+
selectedDeviceId: snapshot.currentEndpoint?.id,
|
|
79
98
|
currentEndpointType: snapshot.currentEndpoint
|
|
80
99
|
? endpointTypeToDisplayName(snapshot.currentEndpoint.type)
|
|
81
100
|
: 'Unknown',
|
|
82
|
-
selectedDevice: snapshot.currentEndpoint?.name ?? '',
|
|
83
101
|
});
|
|
84
102
|
|
|
85
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Cross-platform audio output device picker.
|
|
105
|
+
*/
|
|
106
|
+
class AudioDevicesManager {
|
|
86
107
|
private eventEmitter?: NativeEventEmitter;
|
|
108
|
+
private interruptionReassertSetup = false;
|
|
87
109
|
|
|
88
110
|
/**
|
|
89
|
-
*
|
|
111
|
+
* iOS + CallKit only: re-assert the user's output pick after an interruption ends.
|
|
112
|
+
*
|
|
113
|
+
* On interruption-end iOS clears the ephemeral route override (`setPreferredInput` /
|
|
114
|
+
* `overrideOutputAudioPort`) and doesn't restore it, and callingx re-applies its config
|
|
115
|
+
* only on engine-enable — not the engine-restart of interruption recovery.
|
|
90
116
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
private ensureInterruptionReassert = () => {
|
|
118
|
+
if (this.interruptionReassertSetup) return; // subscibed once per app lifetime
|
|
119
|
+
if (Platform.OS !== 'ios' || !CallingxModule) return;
|
|
120
|
+
this.interruptionReassertSetup = true;
|
|
121
|
+
CallingxModule.addEventListener('didAudioInterruption', (event) => {
|
|
122
|
+
if (event.phase === 'ended') {
|
|
123
|
+
safeNativeCall('reapplyAudioRoute', () =>
|
|
124
|
+
NativeManager.reapplyAudioRoute(),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the current audio device state (available devices + the selected one).
|
|
132
|
+
* Read directly from the audio session, so it works on every path.
|
|
133
|
+
*/
|
|
134
|
+
getStatus = async (): Promise<AudioDevicesState> => {
|
|
135
|
+
// Android Telecom owns routing: read the endpoint snapshot from callingx.
|
|
93
136
|
const tc = getTelecomContext();
|
|
94
137
|
if (tc) {
|
|
95
138
|
const snapshot = await tc.cx.getAvailableAudioEndpoints(tc.callId);
|
|
96
|
-
return
|
|
139
|
+
return snapshotToState(snapshot);
|
|
97
140
|
}
|
|
98
141
|
return NativeManager.getAudioDeviceStatus();
|
|
99
142
|
};
|
|
100
143
|
|
|
101
144
|
/**
|
|
102
|
-
*
|
|
145
|
+
* Switch the audio output to the device with the given id.
|
|
103
146
|
*
|
|
104
|
-
* @param
|
|
147
|
+
* @param deviceId the stable {@link AudioDevice.id} (not the display name).
|
|
105
148
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
149
|
+
select = (deviceId: string): void => {
|
|
150
|
+
// Android Telecom owns routing: the device id is the callingx endpoint id,
|
|
151
|
+
// so route by it directly (no name lookup needed).
|
|
108
152
|
const tc = getTelecomContext();
|
|
109
153
|
if (tc) {
|
|
110
154
|
const { cx, callId } = tc;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
155
|
+
cx.requestAudioEndpointChange(callId, deviceId).catch((error) => {
|
|
156
|
+
videoLoggerSystem
|
|
157
|
+
.getLogger('CallManager')
|
|
158
|
+
.warn(
|
|
159
|
+
`select: failed to route to "${deviceId}" for call ${callId} via Telecom`,
|
|
160
|
+
error,
|
|
116
161
|
);
|
|
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
|
-
});
|
|
162
|
+
});
|
|
130
163
|
return;
|
|
131
164
|
}
|
|
132
|
-
|
|
165
|
+
this.ensureInterruptionReassert();
|
|
166
|
+
safeNativeCall('chooseAudioDeviceEndpoint', () =>
|
|
167
|
+
NativeManager.chooseAudioDeviceEndpoint(deviceId),
|
|
168
|
+
);
|
|
133
169
|
};
|
|
134
170
|
|
|
135
171
|
/**
|
|
136
|
-
* Register a listener for audio device changes.
|
|
137
|
-
*
|
|
172
|
+
* Register a listener for audio device changes. Returns an unsubscribe fn.
|
|
173
|
+
*
|
|
174
|
+
* @param onChange called with the latest {@link AudioDevicesState} on change.
|
|
138
175
|
*/
|
|
139
|
-
|
|
140
|
-
onChange: (
|
|
176
|
+
addChangeListener = (
|
|
177
|
+
onChange: (state: AudioDevicesState) => void,
|
|
141
178
|
): (() => void) => {
|
|
142
|
-
|
|
143
|
-
const
|
|
179
|
+
let active = true;
|
|
180
|
+
const unsubscribes: Array<() => void> = [];
|
|
144
181
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
182
|
+
// callingx-owned route changes (Android Telecom + iOS CallKit). The event is
|
|
183
|
+
// signal-only, so re-read the current state — ignoring a resolve that lands
|
|
184
|
+
// after unsubscribe, and swallowing bridge rejections.
|
|
185
|
+
if (CallingxModule) {
|
|
186
|
+
const cxSub = CallingxModule.addEventListener(
|
|
187
|
+
'didChangeAudioRoute',
|
|
188
|
+
() => {
|
|
189
|
+
this.getStatus()
|
|
190
|
+
.then((state) => {
|
|
191
|
+
if (active) onChange(state);
|
|
192
|
+
})
|
|
193
|
+
.catch((error) => {
|
|
194
|
+
videoLoggerSystem
|
|
195
|
+
.getLogger('CallManager')
|
|
196
|
+
.warn('addChangeListener: getStatus failed', error);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
150
199
|
);
|
|
151
|
-
|
|
200
|
+
unsubscribes.push(() => cxSub.remove());
|
|
152
201
|
}
|
|
153
202
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
203
|
+
// SDK-managed route changes (non-callingx calls).
|
|
204
|
+
this.eventEmitter =
|
|
205
|
+
this.eventEmitter ?? new NativeEventEmitter(NativeManager);
|
|
206
|
+
const sdkSub = this.eventEmitter.addListener(
|
|
207
|
+
AUDIO_DEVICE_CHANGED_EVENT,
|
|
208
|
+
onChange,
|
|
209
|
+
);
|
|
210
|
+
unsubscribes.push(() => sdkSub.remove());
|
|
161
211
|
|
|
162
|
-
return () =>
|
|
212
|
+
return () => {
|
|
213
|
+
active = false;
|
|
214
|
+
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
215
|
+
};
|
|
163
216
|
};
|
|
164
217
|
}
|
|
165
218
|
|
|
@@ -258,7 +311,7 @@ const shouldBypassForCallKit = (): boolean => {
|
|
|
258
311
|
};
|
|
259
312
|
|
|
260
313
|
export class CallManager {
|
|
261
|
-
|
|
314
|
+
audioDevices = new AudioDevicesManager();
|
|
262
315
|
ios = new IOSCallManager();
|
|
263
316
|
speaker = new SpeakerManager();
|
|
264
317
|
|
|
@@ -281,11 +334,14 @@ export class CallManager {
|
|
|
281
334
|
*/
|
|
282
335
|
start = (config?: StreamInCallManagerConfig): void => {
|
|
283
336
|
if (shouldBypassForCallKit()) {
|
|
284
|
-
// Forward only the passive endpoint preference; callingx reads it when
|
|
285
|
-
// CallKit drives session activation.
|
|
286
337
|
if (config?.audioRole === 'communicator' && CallingxModule) {
|
|
287
338
|
const type = config.deviceEndpointType ?? 'speaker';
|
|
288
|
-
|
|
339
|
+
safeNativeCall('setDefaultAudioDeviceEndpointType (callingx)', () =>
|
|
340
|
+
CallingxModule.setDefaultAudioDeviceEndpointType(type),
|
|
341
|
+
);
|
|
342
|
+
safeNativeCall('setDefaultAudioDeviceEndpointType', () =>
|
|
343
|
+
NativeManager.setDefaultAudioDeviceEndpointType(type),
|
|
344
|
+
);
|
|
289
345
|
}
|
|
290
346
|
videoLoggerSystem
|
|
291
347
|
.getLogger('CallManager')
|
|
@@ -298,8 +354,10 @@ export class CallManager {
|
|
|
298
354
|
// Telecom owns routing/focus; forward the sticky preference to callingx and run in
|
|
299
355
|
// telecom-managed mode (StreamInCallManager keeps proximity/keep-screen-on only).
|
|
300
356
|
if (config?.audioRole !== 'listener' && CallingxModule) {
|
|
301
|
-
|
|
302
|
-
|
|
357
|
+
safeNativeCall('setDefaultAudioDeviceEndpointType (callingx)', () =>
|
|
358
|
+
CallingxModule.setDefaultAudioDeviceEndpointType(
|
|
359
|
+
config?.deviceEndpointType ?? 'speaker',
|
|
360
|
+
),
|
|
303
361
|
);
|
|
304
362
|
}
|
|
305
363
|
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';
|
|
@@ -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.42.0';
|