@stream-io/video-react-native-sdk 1.44.0 → 2.0.0-beta.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 -3
- package/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt +112 -26
- package/android/src/main/java/com/streamvideo/reactnative/util/CallAlivePermissionsHelper.kt +68 -37
- package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +12 -6
- package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
- package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js +38 -11
- package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
- package/dist/commonjs/hooks/useLoopbackRecording.js +10 -127
- package/dist/commonjs/hooks/useLoopbackRecording.js.map +1 -1
- package/dist/commonjs/utils/StreamVideoRN/index.js +2 -0
- package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
- package/dist/commonjs/utils/internal/callingx/callingx.js +38 -56
- package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/commonjs/utils/keepCallAliveHeadlessTask.js +43 -4
- package/dist/commonjs/utils/keepCallAliveHeadlessTask.js.map +1 -1
- package/dist/commonjs/utils/push/android.js +7 -167
- package/dist/commonjs/utils/push/android.js.map +1 -1
- package/dist/commonjs/utils/push/internal/android.js +204 -0
- package/dist/commonjs/utils/push/internal/android.js.map +1 -0
- package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js +9 -1
- package/dist/commonjs/utils/push/libs/firebaseMessaging/index.js.map +1 -1
- package/dist/commonjs/utils/push/libs/firebaseMessaging/lib.js +1 -1
- package/dist/commonjs/utils/push/libs/firebaseMessaging/lib.js.map +1 -1
- package/dist/commonjs/utils/push/setupAndroidPushEvents.js +25 -0
- package/dist/commonjs/utils/push/setupAndroidPushEvents.js.map +1 -0
- package/dist/commonjs/utils/push/setupCallingExpEvents.js +6 -2
- package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +1 -1
- package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +6 -7
- package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
- package/dist/commonjs/utils/push/utils.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/commonjs/version.js.map +1 -1
- package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +12 -6
- package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +1 -1
- package/dist/module/hooks/useAndroidKeepCallAliveEffect.js +39 -12
- package/dist/module/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
- package/dist/module/hooks/useLoopbackRecording.js +10 -127
- package/dist/module/hooks/useLoopbackRecording.js.map +1 -1
- package/dist/module/utils/StreamVideoRN/index.js +2 -0
- package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
- package/dist/module/utils/internal/callingx/callingx.js +38 -56
- package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/module/utils/keepCallAliveHeadlessTask.js +41 -3
- package/dist/module/utils/keepCallAliveHeadlessTask.js.map +1 -1
- package/dist/module/utils/push/android.js +9 -169
- package/dist/module/utils/push/android.js.map +1 -1
- package/dist/module/utils/push/internal/android.js +200 -0
- package/dist/module/utils/push/internal/android.js.map +1 -0
- package/dist/module/utils/push/libs/firebaseMessaging/index.js +9 -1
- package/dist/module/utils/push/libs/firebaseMessaging/index.js.map +1 -1
- package/dist/module/utils/push/libs/firebaseMessaging/lib.js +1 -1
- package/dist/module/utils/push/libs/firebaseMessaging/lib.js.map +1 -1
- package/dist/module/utils/push/setupAndroidPushEvents.js +21 -0
- package/dist/module/utils/push/setupAndroidPushEvents.js.map +1 -0
- package/dist/module/utils/push/setupCallingExpEvents.js +6 -2
- package/dist/module/utils/push/setupCallingExpEvents.js.map +1 -1
- package/dist/module/utils/push/setupIosVoipPushEvents.js +6 -7
- package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
- package/dist/module/utils/push/utils.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/module/version.js.map +1 -1
- package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +1 -1
- package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts.map +1 -1
- package/dist/typescript/hooks/useLoopbackRecording.d.ts +12 -69
- package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +1 -1
- package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
- package/dist/typescript/utils/StreamVideoRN/types.d.ts +1 -2
- 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/keepCallAliveHeadlessTask.d.ts +9 -0
- package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts.map +1 -1
- package/dist/typescript/utils/push/android.d.ts +3 -4
- package/dist/typescript/utils/push/android.d.ts.map +1 -1
- package/dist/typescript/utils/push/internal/android.d.ts +8 -0
- package/dist/typescript/utils/push/internal/android.d.ts.map +1 -0
- package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts +6 -4
- package/dist/typescript/utils/push/libs/firebaseMessaging/index.d.ts.map +1 -1
- package/dist/typescript/utils/push/libs/firebaseMessaging/lib.d.ts +2 -2
- package/dist/typescript/utils/push/libs/firebaseMessaging/lib.d.ts.map +1 -1
- package/dist/typescript/utils/push/setupAndroidPushEvents.d.ts +3 -0
- package/dist/typescript/utils/push/setupAndroidPushEvents.d.ts.map +1 -0
- package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +1 -1
- package/dist/typescript/utils/push/setupIosVoipPushEvents.d.ts.map +1 -1
- package/dist/typescript/utils/push/utils.d.ts +2 -2
- package/dist/typescript/utils/push/utils.d.ts.map +1 -1
- package/dist/typescript/version.d.ts +1 -1
- package/dist/typescript/version.d.ts.map +1 -1
- package/expo-config-plugin/dist/withAndroidPermissions.js +1 -1
- package/ios/TracksRecorder/RecorderVideoSink.swift +92 -5
- package/package.json +4 -4
- package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +25 -11
- package/src/hooks/useAndroidKeepCallAliveEffect.ts +59 -21
- package/src/hooks/useLoopbackRecording.ts +36 -248
- package/src/utils/StreamVideoRN/index.ts +2 -0
- package/src/utils/StreamVideoRN/types.ts +1 -2
- package/src/utils/internal/callingx/callingx.ts +49 -68
- package/src/utils/keepCallAliveHeadlessTask.ts +45 -3
- package/src/utils/push/android.ts +12 -237
- package/src/utils/push/internal/android.ts +273 -0
- package/src/utils/push/libs/firebaseMessaging/index.ts +16 -5
- package/src/utils/push/libs/firebaseMessaging/lib.ts +3 -3
- package/src/utils/push/setupAndroidPushEvents.ts +25 -0
- package/src/utils/push/setupCallingExpEvents.ts +6 -3
- package/src/utils/push/setupIosVoipPushEvents.ts +8 -7
- package/src/utils/push/utils.ts +2 -4
- package/src/version.ts +1 -1
|
@@ -1,146 +1,71 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { NativeModules } from 'react-native';
|
|
3
|
-
import { combineLatest } from 'rxjs';
|
|
4
3
|
import {
|
|
5
|
-
Call,
|
|
6
4
|
CallingState,
|
|
5
|
+
DEFAULT_LOOPBACK_RECORDING_DURATION_MS,
|
|
6
|
+
LOOPBACK_STREAMS_WAIT_TIMEOUT_MS,
|
|
7
|
+
clampLoopbackRecordingDuration,
|
|
8
|
+
getLoopbackStreams,
|
|
7
9
|
videoLoggerSystem,
|
|
8
|
-
|
|
10
|
+
waitForLoopbackStreams,
|
|
11
|
+
withLoopbackAudioEnabled,
|
|
12
|
+
type LoopbackStreams,
|
|
13
|
+
type LoopbackTracks,
|
|
9
14
|
} from '@stream-io/video-client';
|
|
15
|
+
|
|
10
16
|
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
11
17
|
|
|
12
18
|
/** @internal */
|
|
13
19
|
const { StreamVideoReactNative } = NativeModules;
|
|
14
20
|
|
|
15
|
-
// Upper bound on how long `startRecording` will wait for the SFU to
|
|
16
|
-
// echo loopback tracks back via the Subscriber. Tuned generously since
|
|
17
|
-
// this includes connection setup; consumers that want shorter feedback
|
|
18
|
-
// should call `stopRecording` to cancel the wait early.
|
|
19
|
-
const STREAMS_WAIT_TIMEOUT_MS = 10 * 1000;
|
|
20
|
-
const DEFAULT_RECORDING_DURATION = 10 * 1000;
|
|
21
|
-
const MIN_RECORDING_DURATION = 5 * 1000;
|
|
22
|
-
const MAX_RECORDING_DURATION = 2 * 60 * 1000;
|
|
23
|
-
|
|
24
|
-
type LoopbackStreams = {
|
|
25
|
-
loopbackVideoStream?: MediaStream;
|
|
26
|
-
loopbackAudioStream?: MediaStream;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
21
|
export type LoopbackRecordingState = 'idle' | 'awaiting-streams' | 'recording';
|
|
30
22
|
|
|
31
|
-
export type ResolvedStreams =
|
|
32
|
-
audioTrack?: MediaStreamTrack;
|
|
33
|
-
videoTrack?: MediaStreamTrack;
|
|
34
|
-
};
|
|
23
|
+
export type ResolvedStreams = LoopbackTracks;
|
|
35
24
|
|
|
36
25
|
export interface StartLoopbackRecordingOptions {
|
|
37
|
-
/**
|
|
38
|
-
* Whether to include the loopback video track in the recording.
|
|
39
|
-
* Defaults to `true`. Set to `false` for an audio-only recording.
|
|
40
|
-
* Audio is always recorded — there is no video-only mode.
|
|
41
|
-
*/
|
|
26
|
+
/** Whether to include loopback video. Audio is always recorded. */
|
|
42
27
|
includeVideo?: boolean;
|
|
43
|
-
/**
|
|
44
|
-
* Maximum recording duration in milliseconds, after which the
|
|
45
|
-
* recording auto-stops and finalises the file.
|
|
46
|
-
*
|
|
47
|
-
* Defaults to `10_000` (10 seconds). Clamped to
|
|
48
|
-
* `[5_000, 120_000]` (5 seconds — 2 minutes).
|
|
49
|
-
*/
|
|
28
|
+
/** Maximum recording duration in milliseconds. */
|
|
50
29
|
maxDurationMs?: number;
|
|
51
30
|
}
|
|
52
31
|
|
|
53
32
|
export interface UseLoopbackRecordingResult {
|
|
54
|
-
/**
|
|
55
|
-
* Start a recording. The hook waits internally for the SFU loopback
|
|
56
|
-
* streams to arrive on `localParticipant`, then begins recording.
|
|
57
|
-
*
|
|
58
|
-
* The returned promise resolves with the produced `file://` URI **at
|
|
59
|
-
* the recording's terminal moment** — whether that is the auto-stop
|
|
60
|
-
* timer expiring, an explicit `stopRecording` call, or a cleanup-
|
|
61
|
-
* driven stop on unmount/leave. Resolves with `null` if no file was
|
|
62
|
-
* produced (writer torn down before any buffer arrived, or
|
|
63
|
-
* `stopRecording` was called while still awaiting streams). Rejects
|
|
64
|
-
* on a fatal error, if a recording is already running, or if the
|
|
65
|
-
* stream-wait times out.
|
|
66
|
-
*/
|
|
33
|
+
/** Starts recording and resolves with a `file://` URI. */
|
|
67
34
|
startRecording: (
|
|
68
35
|
options?: StartLoopbackRecordingOptions,
|
|
69
36
|
) => Promise<string | null>;
|
|
70
|
-
/**
|
|
71
|
-
* Signal an early termination. While `awaiting-streams` this aborts
|
|
72
|
-
* the wait and the pending `startRecording` resolves with `null`.
|
|
73
|
-
* While `recording` this signals native finalisation and resolves
|
|
74
|
-
* once it completes.
|
|
75
|
-
*/
|
|
37
|
+
/** Stops the current recording or stream wait. */
|
|
76
38
|
stopRecording: () => Promise<void>;
|
|
77
|
-
/**
|
|
78
|
-
* Recursively delete every file under the SDK's recordings directory.
|
|
79
|
-
*/
|
|
39
|
+
/** Deletes all SDK recording files. */
|
|
80
40
|
clearRecordings: () => Promise<void>;
|
|
81
|
-
/**
|
|
82
|
-
* List every `file://` URI in the SDK's recordings directory, sorted
|
|
83
|
-
* most-recent first. Returns an empty array if the directory doesn't
|
|
84
|
-
* exist yet.
|
|
85
|
-
*/
|
|
41
|
+
/** Lists SDK recording file URIs. */
|
|
86
42
|
getRecordings: () => Promise<string[]>;
|
|
87
|
-
/**
|
|
88
|
-
* Lifecycle phase of the recording, owned by the hook:
|
|
89
|
-
* - `'idle'`: no recording in progress.
|
|
90
|
-
* - `'awaiting-streams'`: `startRecording` was called but the SFU
|
|
91
|
-
* has not yet echoed the loopback tracks back.
|
|
92
|
-
* - `'recording'`: native pipeline is actively writing.
|
|
93
|
-
*/
|
|
43
|
+
/** Lifecycle phase of the recording. */
|
|
94
44
|
recordingState: LoopbackRecordingState;
|
|
95
|
-
/**
|
|
96
|
-
* The SFU loopback video stream on the local participant, when
|
|
97
|
-
* present. Identified by reference inequality against
|
|
98
|
-
* `call.camera.state.mediaStream`.
|
|
99
|
-
*/
|
|
45
|
+
/** SFU-echoed video stream on the local participant. */
|
|
100
46
|
loopbackVideoStream?: MediaStream;
|
|
101
|
-
/**
|
|
102
|
-
* The SFU loopback audio stream on the local participant, when
|
|
103
|
-
* present. Identified by reference inequality against
|
|
104
|
-
* `call.microphone.state.mediaStream`.
|
|
105
|
-
*/
|
|
47
|
+
/** SFU-echoed audio stream on the local participant. */
|
|
106
48
|
loopbackAudioStream?: MediaStream;
|
|
107
49
|
}
|
|
108
50
|
|
|
109
|
-
/**
|
|
110
|
-
* Records the SFU loopback streams (audio + video) on the local participant
|
|
111
|
-
* to a local MP4 file. Designed for the `allowOwnTracksLoopback` pre-call test mode:
|
|
112
|
-
* the SFU echoes the caller's published tracks back through the Subscriber
|
|
113
|
-
* peer connection. The hook identifies the loopback streams on the local
|
|
114
|
-
* participant by reference inequality against
|
|
115
|
-
* `call.camera.state.mediaStream` / `call.microphone.state.mediaStream` —
|
|
116
|
-
* the canonical references to the local capture — and captures them.
|
|
117
|
-
*/
|
|
51
|
+
/** Records SFU loopback streams to a local MP4 file. */
|
|
118
52
|
export function useLoopbackRecording(): UseLoopbackRecordingResult {
|
|
119
53
|
const call = useCall();
|
|
120
|
-
const {
|
|
121
|
-
|
|
122
|
-
useParticipantCount,
|
|
123
|
-
useLocalParticipant,
|
|
124
|
-
useCameraState,
|
|
125
|
-
useMicrophoneState,
|
|
126
|
-
} = useCallStateHooks();
|
|
54
|
+
const { useCallCallingState, useParticipantCount, useLocalParticipant } =
|
|
55
|
+
useCallStateHooks();
|
|
127
56
|
const callingState = useCallCallingState();
|
|
128
57
|
const participantCount = useParticipantCount();
|
|
129
58
|
const localParticipant = useLocalParticipant();
|
|
130
|
-
const { mediaStream: cameraStream } = useCameraState();
|
|
131
|
-
const { mediaStream: microphoneStream } = useMicrophoneState();
|
|
132
59
|
|
|
133
60
|
const [recordingState, setRecordingState] =
|
|
134
61
|
useState<LoopbackRecordingState>('idle');
|
|
135
62
|
const recordingStateRef = useRef<LoopbackRecordingState>('idle');
|
|
136
63
|
const isMountedRef = useRef(true);
|
|
137
|
-
// Used to abort the awaiting-streams wait on stop / leave / unmount.
|
|
138
64
|
const awaitAbortRef = useRef<AbortController | null>(null);
|
|
139
65
|
|
|
140
66
|
const loopbackStreams = useMemo<LoopbackStreams>(
|
|
141
|
-
() =>
|
|
142
|
-
|
|
143
|
-
[localParticipant, cameraStream, microphoneStream],
|
|
67
|
+
() => getLoopbackStreams(call, localParticipant),
|
|
68
|
+
[call, localParticipant],
|
|
144
69
|
);
|
|
145
70
|
|
|
146
71
|
const updateState = useCallback((next: LoopbackRecordingState) => {
|
|
@@ -177,7 +102,7 @@ export function useLoopbackRecording(): UseLoopbackRecordingResult {
|
|
|
177
102
|
const startRecording = useCallback(
|
|
178
103
|
async ({
|
|
179
104
|
includeVideo = true,
|
|
180
|
-
maxDurationMs =
|
|
105
|
+
maxDurationMs = DEFAULT_LOOPBACK_RECORDING_DURATION_MS,
|
|
181
106
|
}: StartLoopbackRecordingOptions = {}): Promise<string | null> => {
|
|
182
107
|
if (!call) {
|
|
183
108
|
return null;
|
|
@@ -200,12 +125,11 @@ export function useLoopbackRecording(): UseLoopbackRecordingResult {
|
|
|
200
125
|
awaitAbortRef.current = new AbortController();
|
|
201
126
|
updateState('awaiting-streams');
|
|
202
127
|
|
|
203
|
-
let audioTrack: MediaStreamTrack | undefined;
|
|
204
128
|
try {
|
|
205
129
|
const streams = await waitForLoopbackStreams(call, {
|
|
206
130
|
includeVideo,
|
|
207
131
|
signal: awaitAbortRef.current.signal,
|
|
208
|
-
timeoutMs:
|
|
132
|
+
timeoutMs: LOOPBACK_STREAMS_WAIT_TIMEOUT_MS,
|
|
209
133
|
});
|
|
210
134
|
|
|
211
135
|
if (streams === null) {
|
|
@@ -215,38 +139,21 @@ export function useLoopbackRecording(): UseLoopbackRecordingResult {
|
|
|
215
139
|
return null;
|
|
216
140
|
}
|
|
217
141
|
|
|
218
|
-
audioTrack = streams.audioTrack;
|
|
219
|
-
const videoTrackId = streams.videoTrack?.id;
|
|
220
|
-
|
|
221
|
-
// The loopback audio track lands disabled (the SDK default-mutes
|
|
222
|
-
// it to prevent echo). Enable it so the native recording
|
|
223
|
-
// pipeline receives PCM; `finally` returns it to muted.
|
|
224
|
-
if (audioTrack) {
|
|
225
|
-
audioTrack.enabled = true;
|
|
226
|
-
}
|
|
227
142
|
updateState('recording');
|
|
228
143
|
|
|
229
|
-
const clampedDuration = Math.min(
|
|
230
|
-
MAX_RECORDING_DURATION,
|
|
231
|
-
Math.max(MIN_RECORDING_DURATION, maxDurationMs),
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
// Pre-size the native encoder to the publisher's max video
|
|
235
|
-
// publish-option dimension. Without this, the encoder locks to
|
|
236
|
-
// whatever the SFU happens to echo *first* and stays there for the
|
|
237
|
-
// rest of the recording even after the network improves and
|
|
238
|
-
// higher layers arrive.
|
|
239
144
|
const publishMaxDim = call.getMaxVideoPublishDimension();
|
|
240
145
|
|
|
241
146
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
147
|
+
return await withLoopbackAudioEnabled(
|
|
148
|
+
streams,
|
|
149
|
+
(): Promise<string | null> =>
|
|
150
|
+
StreamVideoReactNative.startTrackRecording({
|
|
151
|
+
videoTrackId: streams.videoTrack?.id,
|
|
152
|
+
maxDurationMs: clampLoopbackRecordingDuration(maxDurationMs),
|
|
153
|
+
targetWidth: publishMaxDim?.width,
|
|
154
|
+
targetHeight: publishMaxDim?.height,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
250
157
|
} catch (error) {
|
|
251
158
|
videoLoggerSystem
|
|
252
159
|
.getLogger('useLoopbackRecording')
|
|
@@ -254,9 +161,6 @@ export function useLoopbackRecording(): UseLoopbackRecordingResult {
|
|
|
254
161
|
throw new Error('failed to start recording');
|
|
255
162
|
}
|
|
256
163
|
} finally {
|
|
257
|
-
if (audioTrack) {
|
|
258
|
-
audioTrack.enabled = false;
|
|
259
|
-
}
|
|
260
164
|
awaitAbortRef.current = null;
|
|
261
165
|
updateState('idle');
|
|
262
166
|
}
|
|
@@ -288,11 +192,6 @@ export function useLoopbackRecording(): UseLoopbackRecordingResult {
|
|
|
288
192
|
}
|
|
289
193
|
}, []);
|
|
290
194
|
|
|
291
|
-
// Auto-stop on call leave / end. Aborts an awaiting-streams wait or
|
|
292
|
-
// signals native finalisation depending on which phase we're in.
|
|
293
|
-
// Without this, leaving the call mid-recording would leave native
|
|
294
|
-
// encoders mid-write while the SFU subscriber tracks end under their
|
|
295
|
-
// feet — undefined final file state.
|
|
296
195
|
useEffect(() => {
|
|
297
196
|
if (
|
|
298
197
|
callingState === CallingState.LEFT ||
|
|
@@ -305,8 +204,6 @@ export function useLoopbackRecording(): UseLoopbackRecordingResult {
|
|
|
305
204
|
}
|
|
306
205
|
}, [callingState, stopRecording]);
|
|
307
206
|
|
|
308
|
-
// Auto-stop if another participant joins. Loopback recording is a
|
|
309
|
-
// single-user pre-call test.
|
|
310
207
|
useEffect(() => {
|
|
311
208
|
if (recordingState !== 'idle' && participantCount > 1) {
|
|
312
209
|
stopRecording().catch(() => {});
|
|
@@ -337,112 +234,3 @@ export function useLoopbackRecording(): UseLoopbackRecordingResult {
|
|
|
337
234
|
loopbackAudioStream: loopbackStreams.loopbackAudioStream,
|
|
338
235
|
};
|
|
339
236
|
}
|
|
340
|
-
|
|
341
|
-
function getLoopbackStreamsFor(
|
|
342
|
-
participant: StreamVideoParticipant | undefined,
|
|
343
|
-
cameraStream: MediaStream | undefined,
|
|
344
|
-
microphoneStream: MediaStream | undefined,
|
|
345
|
-
): LoopbackStreams {
|
|
346
|
-
return {
|
|
347
|
-
loopbackVideoStream:
|
|
348
|
-
participant?.videoStream && participant.videoStream !== cameraStream
|
|
349
|
-
? participant.videoStream
|
|
350
|
-
: undefined,
|
|
351
|
-
loopbackAudioStream:
|
|
352
|
-
participant?.audioStream && participant.audioStream !== microphoneStream
|
|
353
|
-
? participant.audioStream
|
|
354
|
-
: undefined,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Subscribe to `localParticipant$` and resolve once the requested loopback
|
|
360
|
-
* streams are present on the participant. Aborts cleanly on `signal`
|
|
361
|
-
* (resolves `null`) and rejects on timeout.
|
|
362
|
-
*/
|
|
363
|
-
function waitForLoopbackStreams(
|
|
364
|
-
call: Call,
|
|
365
|
-
opts: { includeVideo: boolean; signal: AbortSignal; timeoutMs: number },
|
|
366
|
-
): Promise<ResolvedStreams | null> {
|
|
367
|
-
return new Promise((resolve, reject) => {
|
|
368
|
-
const initial = getLoopbackStreams(
|
|
369
|
-
call.state.localParticipant,
|
|
370
|
-
call.camera.state.mediaStream,
|
|
371
|
-
call.microphone.state.mediaStream,
|
|
372
|
-
opts.includeVideo,
|
|
373
|
-
);
|
|
374
|
-
if (initial) {
|
|
375
|
-
resolve(initial);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const cleanup = () => {
|
|
380
|
-
subscription.unsubscribe();
|
|
381
|
-
if (timeoutId !== undefined) {
|
|
382
|
-
clearTimeout(timeoutId);
|
|
383
|
-
}
|
|
384
|
-
opts.signal.removeEventListener('abort', onAbort);
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const onAbort = () => {
|
|
388
|
-
cleanup();
|
|
389
|
-
resolve(null);
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
opts.signal.addEventListener('abort', onAbort);
|
|
393
|
-
|
|
394
|
-
const timeoutId = setTimeout(() => {
|
|
395
|
-
cleanup();
|
|
396
|
-
reject(
|
|
397
|
-
new Error(
|
|
398
|
-
'useLoopbackRecording: timed out waiting for loopback streams. ' +
|
|
399
|
-
'Ensure the call was joined with `allowOwnTracksLoopback: true` and ' +
|
|
400
|
-
'that the SFU is configured to echo self-sub tracks.',
|
|
401
|
-
),
|
|
402
|
-
);
|
|
403
|
-
}, opts.timeoutMs);
|
|
404
|
-
|
|
405
|
-
const subscription = combineLatest([
|
|
406
|
-
call.state.localParticipant$,
|
|
407
|
-
call.camera.state.mediaStream$,
|
|
408
|
-
call.microphone.state.mediaStream$,
|
|
409
|
-
]).subscribe(([participant, cameraStream, microphoneStream]) => {
|
|
410
|
-
const ready = getLoopbackStreams(
|
|
411
|
-
participant,
|
|
412
|
-
cameraStream,
|
|
413
|
-
microphoneStream,
|
|
414
|
-
opts.includeVideo,
|
|
415
|
-
);
|
|
416
|
-
if (ready) {
|
|
417
|
-
cleanup();
|
|
418
|
-
resolve(ready);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function getLoopbackStreams(
|
|
425
|
-
participant: StreamVideoParticipant | undefined,
|
|
426
|
-
cameraStream: MediaStream | undefined,
|
|
427
|
-
microphoneStream: MediaStream | undefined,
|
|
428
|
-
includeVideo: boolean,
|
|
429
|
-
): ResolvedStreams | undefined {
|
|
430
|
-
if (!participant) return undefined;
|
|
431
|
-
|
|
432
|
-
const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreamsFor(
|
|
433
|
-
participant,
|
|
434
|
-
cameraStream,
|
|
435
|
-
microphoneStream,
|
|
436
|
-
);
|
|
437
|
-
|
|
438
|
-
const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
|
|
439
|
-
const videoTrack = includeVideo
|
|
440
|
-
? loopbackVideoStream?.getVideoTracks()[0]
|
|
441
|
-
: undefined;
|
|
442
|
-
|
|
443
|
-
if (!audioTrack || (includeVideo && !videoTrack)) {
|
|
444
|
-
return undefined;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return { audioTrack, videoTrack };
|
|
448
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { StreamVideoConfig } from './types';
|
|
2
2
|
import pushLogoutCallbacks from '../internal/pushLogoutCallback';
|
|
3
3
|
import { setupIosVoipPushEvents } from '../push/setupIosVoipPushEvents';
|
|
4
|
+
import { setupAndroidPushEvents } from '../push/setupAndroidPushEvents';
|
|
4
5
|
import { setupCallingExpEvents } from '../push/setupCallingExpEvents';
|
|
5
6
|
import {
|
|
6
7
|
extractCallingExpOptions,
|
|
@@ -133,6 +134,7 @@ export class StreamVideoRN {
|
|
|
133
134
|
|
|
134
135
|
setupCallingExpEvents(pushConfig);
|
|
135
136
|
setupIosVoipPushEvents(pushConfig);
|
|
137
|
+
setupAndroidPushEvents(pushConfig);
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
static getConfig() {
|
|
@@ -176,8 +176,7 @@ export type StreamVideoConfig = {
|
|
|
176
176
|
*/
|
|
177
177
|
notificationTexts: KeepAliveAndroidNotificationTexts;
|
|
178
178
|
/**
|
|
179
|
-
* The task to run in the foreground service
|
|
180
|
-
* The task must resolve a promise once complete
|
|
179
|
+
* The task to run in the foreground service.
|
|
181
180
|
*/
|
|
182
181
|
taskToRun: (call: Call) => Promise<void>;
|
|
183
182
|
};
|
|
@@ -48,22 +48,31 @@ export function getCallDisplayName(
|
|
|
48
48
|
if (names.length === 0) {
|
|
49
49
|
names = [
|
|
50
50
|
participants.find((participant) => participant.userId === currentUserId)
|
|
51
|
-
?.name
|
|
51
|
+
?.name || 'Call',
|
|
52
52
|
];
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return names.sort().join(', ');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Args shared by the callingx call registration APIs:
|
|
60
|
+
* `(callId, phoneNumber, callerName, hasVideo)`.
|
|
61
|
+
*/
|
|
62
|
+
function getCallingxCallArgs(call: Call): [string, string, string, boolean] {
|
|
63
|
+
const callDisplayName =
|
|
64
|
+
call.state.custom?.display_name ||
|
|
61
65
|
getCallDisplayName(
|
|
62
66
|
call.state.members,
|
|
63
67
|
call.state.participants,
|
|
64
68
|
call.currentUserId,
|
|
65
|
-
)
|
|
66
|
-
|
|
69
|
+
);
|
|
70
|
+
return [
|
|
71
|
+
call.cid, // unique id for call
|
|
72
|
+
call.state.createdBy?.id ?? callDisplayName, // handle for native call UI (prefer createdBy user id, fallback to call display name)
|
|
73
|
+
callDisplayName, // display name for display in call screen
|
|
74
|
+
call.state.settings?.video?.enabled ?? false, // is video call?
|
|
75
|
+
];
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
export async function registerOutgoingCall(call: Call) {
|
|
@@ -84,13 +93,7 @@ export async function registerOutgoingCall(call: Call) {
|
|
|
84
93
|
|
|
85
94
|
try {
|
|
86
95
|
logger.debug(`registerOutgoingCall: Registering outgoing call ${call.cid}`);
|
|
87
|
-
|
|
88
|
-
await CallingxModule.startCall(
|
|
89
|
-
call.cid, // unique id for call
|
|
90
|
-
call.state.createdBy?.id ?? callDisplayName, // handle for native call UI (prefer createdBy user id, fallback to call display name)
|
|
91
|
-
callDisplayName, // display name for display in call screen
|
|
92
|
-
call.state.settings?.video?.enabled ?? false, // is video call?
|
|
93
|
-
);
|
|
96
|
+
await CallingxModule.startCall(...getCallingxCallArgs(call));
|
|
94
97
|
} catch (error) {
|
|
95
98
|
logger.error(
|
|
96
99
|
`registerOutgoingCall: Error registering outgoing call in callingx: ${call.cid}`,
|
|
@@ -119,67 +122,45 @@ export async function joinCallingxCall(call: Call, activeCalls: Call[]) {
|
|
|
119
122
|
const logger = videoLoggerSystem.getLogger('callingx');
|
|
120
123
|
const isOutcomingCall = call.ringing && call.isCreatedByMe;
|
|
121
124
|
const isIncomingCall = call.ringing && !call.isCreatedByMe;
|
|
125
|
+
const isOngoingCall = (c: Call) =>
|
|
126
|
+
!c.ringing &&
|
|
127
|
+
CallingxModule.isOngoingCallsEnabled &&
|
|
128
|
+
!c.isOwnTracksLoopbackAllowed;
|
|
122
129
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await CallingxModule.startCall(
|
|
127
|
-
call.cid, // unique id for call
|
|
128
|
-
call.state.createdBy?.id ?? callDisplayName, // handle for native call UI (prefer createdBy user id, fallback to call display name)
|
|
129
|
-
callDisplayName, // display name for display in call screen
|
|
130
|
-
call.state.settings?.video?.enabled ?? false, // is video call?
|
|
131
|
-
);
|
|
132
|
-
};
|
|
130
|
+
if (!isIncomingCall && !isOutcomingCall && !isOngoingCall(call)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
134
|
+
try {
|
|
135
|
+
const activeCallsToLeave = activeCalls.filter(
|
|
136
|
+
(c) =>
|
|
137
|
+
c.cid !== call.cid &&
|
|
138
|
+
(c.ringing || isOngoingCall(c)) &&
|
|
139
|
+
c.state.callingState !== CallingState.LEFT,
|
|
140
|
+
);
|
|
141
|
+
for (const activeCall of activeCallsToLeave) {
|
|
142
|
+
logger.debug(
|
|
143
|
+
`leaving currently-active-call:${activeCall.cid} before joining the call:${call.cid}`,
|
|
144
144
|
);
|
|
145
|
+
await activeCall.leave({ reason: 'cancel' }).catch((e) => {
|
|
146
|
+
logger.error(`failed to leave active call ${activeCall.cid}`, e);
|
|
147
|
+
});
|
|
145
148
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
(c) =>
|
|
153
|
-
c.cid !== call.cid &&
|
|
154
|
-
c.ringing &&
|
|
155
|
-
c.state.callingState !== CallingState.LEFT,
|
|
156
|
-
);
|
|
157
|
-
for (const activeCall of activeCallsToLeave) {
|
|
158
|
-
logger.debug(
|
|
159
|
-
`leaving active call ${activeCall.cid} before joining ${call.cid}`,
|
|
160
|
-
);
|
|
161
|
-
await activeCall.leave({ reason: 'cancel' }).catch((e) => {
|
|
162
|
-
logger.error(`failed to leave active call ${activeCall.cid}`, e);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
// Awaits native CallKit/Telecom registration before answering.
|
|
166
|
-
// Safe to call even if the call is already registered (e.g. from VoIP push) --
|
|
167
|
-
// iOS early-returns with no error, Android sends the registered broadcast.
|
|
168
|
-
const callDisplayName = getCallDisplayNameFromCall(call);
|
|
169
|
-
await CallingxModule.displayIncomingCall(
|
|
170
|
-
call.cid, // unique id for call
|
|
171
|
-
call.state.createdBy?.id ?? callDisplayName, // handle for native call UI (prefer createdBy user id, fallback to call display name)
|
|
172
|
-
callDisplayName, // display name for display in call screen
|
|
173
|
-
call.state.settings?.video?.enabled ?? false, // is video call?
|
|
174
|
-
);
|
|
175
|
-
|
|
149
|
+
logger.debug(
|
|
150
|
+
`joinCallingxCall: Joining call ${call.cid} isIncoming: ${isIncomingCall} isOutgoing: ${isOutcomingCall}`,
|
|
151
|
+
);
|
|
152
|
+
const callArgs = getCallingxCallArgs(call);
|
|
153
|
+
if (isIncomingCall) {
|
|
154
|
+
await CallingxModule.displayIncomingCall(...callArgs);
|
|
176
155
|
await CallingxModule.answerIncomingCall(call.cid);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
`Error joining incoming call in callingx: ${call.cid}`,
|
|
180
|
-
error,
|
|
181
|
-
);
|
|
156
|
+
} else {
|
|
157
|
+
await CallingxModule.startCall(...callArgs);
|
|
182
158
|
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger.error(
|
|
161
|
+
`startCallingxCall: Error starting call in callingx: ${call.cid} isIncoming: ${isIncomingCall} isOutgoing: ${isOutcomingCall}`,
|
|
162
|
+
error,
|
|
163
|
+
);
|
|
183
164
|
}
|
|
184
165
|
}
|
|
185
166
|
|
|
@@ -13,6 +13,21 @@ export const keepCallAliveCallRef: { current: Call | undefined } = {
|
|
|
13
13
|
current: undefined,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
let resolveRunningTask: (() => void) | undefined;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ends the currently running keep-alive headless task.
|
|
20
|
+
*
|
|
21
|
+
* The task's promise is app-provided (`foregroundService.android.taskToRun`) and by default never
|
|
22
|
+
* resolves, so stopping the native service alone would leave the task registered in
|
|
23
|
+
* `HeadlessJsTaskContext` forever - which keeps RN's timer choreographer callback posted for the
|
|
24
|
+
* rest of the process lifetime. Resolving it here lets React Native unwind the task properly.
|
|
25
|
+
*/
|
|
26
|
+
export const endKeepCallAliveHeadlessTask = () => {
|
|
27
|
+
resolveRunningTask?.();
|
|
28
|
+
resolveRunningTask = undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
16
31
|
function registerKeepCallAliveHeadlessTaskOnce() {
|
|
17
32
|
if (Platform.OS !== 'android') return;
|
|
18
33
|
|
|
@@ -42,10 +57,37 @@ function registerKeepCallAliveHeadlessTaskOnce() {
|
|
|
42
57
|
|
|
43
58
|
const config = StreamVideoRN.getConfig();
|
|
44
59
|
const taskToRun = config.foregroundService.android.taskToRun;
|
|
60
|
+
// A previous task (if any) must not keep hanging around.
|
|
61
|
+
endKeepCallAliveHeadlessTask();
|
|
62
|
+
const taskEnded = new Promise<void>((resolve) => {
|
|
63
|
+
resolveRunningTask = resolve;
|
|
64
|
+
});
|
|
65
|
+
const ownResolver = resolveRunningTask;
|
|
66
|
+
|
|
67
|
+
// React Native stops the foreground service as soon as the promise returned from here
|
|
68
|
+
// settles. `taskToRun` is app-provided and may resolve early - or immediately - so its
|
|
69
|
+
// completion must not end the keep-alive: that would drop the call's background protection
|
|
70
|
+
// while the call is still ongoing. The task's lifetime is owned by the SDK instead, and ends
|
|
71
|
+
// via endKeepCallAliveHeadlessTask() when the call is left.
|
|
72
|
+
(async () => {
|
|
73
|
+
try {
|
|
74
|
+
await taskToRun(call);
|
|
75
|
+
logger.debug(
|
|
76
|
+
'Keep-alive taskToRun completed, keeping the call alive until the call ends',
|
|
77
|
+
);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
logger.error('Keep-alive headless task failed', e);
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
|
|
45
83
|
try {
|
|
46
|
-
await
|
|
47
|
-
}
|
|
48
|
-
|
|
84
|
+
await taskEnded;
|
|
85
|
+
} finally {
|
|
86
|
+
// a newer task may already have installed its own resolver - clearing it here would leave
|
|
87
|
+
// that task with no way to be ended.
|
|
88
|
+
if (resolveRunningTask === ownResolver) {
|
|
89
|
+
resolveRunningTask = undefined;
|
|
90
|
+
}
|
|
49
91
|
}
|
|
50
92
|
},
|
|
51
93
|
);
|