@stream-io/video-react-native-sdk 1.36.2 → 1.37.1-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.
Files changed (56) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +81 -0
  3. package/android/src/main/java/com/streamvideo/reactnative/recorder/AudioPipeline.kt +436 -0
  4. package/android/src/main/java/com/streamvideo/reactnative/recorder/EncoderConstants.kt +17 -0
  5. package/android/src/main/java/com/streamvideo/reactnative/recorder/PipelineHost.kt +36 -0
  6. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderPlaybackSamplesSink.kt +60 -0
  7. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderVideoSink.kt +31 -0
  8. package/android/src/main/java/com/streamvideo/reactnative/recorder/TracksRecorderManager.kt +329 -0
  9. package/android/src/main/java/com/streamvideo/reactnative/recorder/VideoPipeline.kt +472 -0
  10. package/dist/commonjs/hooks/index.js +11 -0
  11. package/dist/commonjs/hooks/index.js.map +1 -1
  12. package/dist/commonjs/hooks/useLoopbackRecording.js +243 -0
  13. package/dist/commonjs/hooks/useLoopbackRecording.js.map +1 -0
  14. package/dist/commonjs/utils/internal/callingx/callingx.js +18 -38
  15. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
  16. package/dist/commonjs/utils/push/internal/ios.js +4 -3
  17. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  18. package/dist/commonjs/version.js +1 -1
  19. package/dist/commonjs/version.js.map +1 -1
  20. package/dist/module/hooks/index.js +1 -0
  21. package/dist/module/hooks/index.js.map +1 -1
  22. package/dist/module/hooks/useLoopbackRecording.js +238 -0
  23. package/dist/module/hooks/useLoopbackRecording.js.map +1 -0
  24. package/dist/module/utils/internal/callingx/callingx.js +19 -39
  25. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  26. package/dist/module/utils/push/internal/ios.js +4 -3
  27. package/dist/module/utils/push/internal/ios.js.map +1 -1
  28. package/dist/module/version.js +1 -1
  29. package/dist/module/version.js.map +1 -1
  30. package/dist/typescript/hooks/index.d.ts +1 -0
  31. package/dist/typescript/hooks/index.d.ts.map +1 -1
  32. package/dist/typescript/hooks/useLoopbackRecording.d.ts +85 -0
  33. package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +1 -0
  34. package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +1 -1
  35. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  36. package/dist/typescript/version.d.ts +1 -1
  37. package/dist/typescript/version.d.ts.map +1 -1
  38. package/expo-config-plugin/dist/withAppDelegate.js +14 -177
  39. package/ios/RTCViewPip.swift +6 -6
  40. package/ios/RTCViewPipManager.swift +47 -10
  41. package/ios/StreamInCallManager.swift +2 -6
  42. package/ios/StreamVideoReactNative-Bridging-Header.h +2 -0
  43. package/ios/StreamVideoReactNative.h +5 -18
  44. package/ios/StreamVideoReactNative.m +83 -296
  45. package/ios/TracksRecorder/AudioPipeline.swift +270 -0
  46. package/ios/TracksRecorder/PipelineHost.swift +56 -0
  47. package/ios/TracksRecorder/RecorderAudioRenderTap.swift +154 -0
  48. package/ios/TracksRecorder/RecorderVideoSink.swift +137 -0
  49. package/ios/TracksRecorder/TracksRecorderManager.swift +327 -0
  50. package/ios/TracksRecorder/VideoPipeline.swift +297 -0
  51. package/package.json +8 -8
  52. package/src/hooks/index.ts +1 -0
  53. package/src/hooks/useLoopbackRecording.ts +438 -0
  54. package/src/utils/internal/callingx/callingx.ts +19 -44
  55. package/src/utils/push/internal/ios.ts +4 -3
  56. package/src/version.ts +1 -1
@@ -0,0 +1,438 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { NativeModules } from 'react-native';
3
+ import { combineLatest, distinctUntilChanged, map } from 'rxjs';
4
+ import {
5
+ Call,
6
+ CallingState,
7
+ videoLoggerSystem,
8
+ type StreamVideoParticipant,
9
+ } from '@stream-io/video-client';
10
+ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
11
+
12
+ /** @internal */
13
+ const { StreamVideoReactNative } = NativeModules;
14
+
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
+ export type LoopbackRecordingState = 'idle' | 'awaiting-streams' | 'recording';
30
+
31
+ export type ResolvedStreams = {
32
+ audioTrack?: MediaStreamTrack;
33
+ videoTrack?: MediaStreamTrack;
34
+ };
35
+
36
+ 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
+ */
42
+ 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
+ */
50
+ maxDurationMs?: number;
51
+ }
52
+
53
+ 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
+ */
67
+ startRecording: (
68
+ options?: StartLoopbackRecordingOptions,
69
+ ) => 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
+ */
76
+ stopRecording: () => Promise<void>;
77
+ /**
78
+ * Recursively delete every file under the SDK's recordings directory.
79
+ */
80
+ 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
+ */
86
+ 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
+ */
94
+ 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
+ */
100
+ 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
+ */
106
+ loopbackAudioStream?: MediaStream;
107
+ }
108
+
109
+ /**
110
+ * Records the SFU loopback streams (audio + video) on the local participant
111
+ * to a local MP4 file. Designed for the `selfSubEnabled` 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
+ */
118
+ export function useLoopbackRecording(): UseLoopbackRecordingResult {
119
+ const call = useCall();
120
+ const { useCallCallingState, useParticipantCount } = useCallStateHooks();
121
+ const callingState = useCallCallingState();
122
+ const participantCount = useParticipantCount();
123
+
124
+ const [recordingState, setRecordingState] =
125
+ useState<LoopbackRecordingState>('idle');
126
+ const recordingStateRef = useRef<LoopbackRecordingState>('idle');
127
+ const isMountedRef = useRef(true);
128
+ // Used to abort the awaiting-streams wait on stop / leave / unmount.
129
+ const awaitAbortRef = useRef<AbortController | null>(null);
130
+
131
+ const [loopbackStreams, setLoopbackStreams] = useState<LoopbackStreams>(
132
+ () => {
133
+ if (!call) return {};
134
+ return getLoopbackStreamsFor(
135
+ call.state.localParticipant,
136
+ call.camera.state.mediaStream,
137
+ call.microphone.state.mediaStream,
138
+ );
139
+ },
140
+ );
141
+
142
+ const updateState = useCallback((next: LoopbackRecordingState) => {
143
+ recordingStateRef.current = next;
144
+ if (isMountedRef.current) {
145
+ setRecordingState(next);
146
+ }
147
+ }, []);
148
+
149
+ const stopRecording = useCallback(async (): Promise<void> => {
150
+ const current = recordingStateRef.current;
151
+ if (current === 'idle') {
152
+ return;
153
+ }
154
+
155
+ if (current === 'awaiting-streams') {
156
+ videoLoggerSystem
157
+ .getLogger('useLoopbackRecording')
158
+ .debug('aborting awaiting-streams wait');
159
+ awaitAbortRef.current?.abort();
160
+ return;
161
+ }
162
+
163
+ await StreamVideoReactNative?.stopTrackRecording();
164
+ }, []);
165
+
166
+ const startRecording = useCallback(
167
+ async ({
168
+ includeVideo = true,
169
+ maxDurationMs = DEFAULT_RECORDING_DURATION,
170
+ }: StartLoopbackRecordingOptions = {}): Promise<string | null> => {
171
+ if (!call) {
172
+ return null;
173
+ }
174
+
175
+ if (recordingStateRef.current !== 'idle') {
176
+ console.warn('useLoopbackRecording: a recording is already running');
177
+ return null;
178
+ }
179
+
180
+ if (call.state.participantCount > 1) {
181
+ console.warn(
182
+ 'useLoopbackRecording: cannot start recording with other participants present',
183
+ );
184
+ return null;
185
+ }
186
+
187
+ awaitAbortRef.current = new AbortController();
188
+ updateState('awaiting-streams');
189
+
190
+ let audioTrack: MediaStreamTrack | undefined;
191
+ try {
192
+ const streams = await waitForLoopbackStreams(call, {
193
+ includeVideo,
194
+ signal: awaitAbortRef.current.signal,
195
+ timeoutMs: STREAMS_WAIT_TIMEOUT_MS,
196
+ });
197
+
198
+ if (streams === null) {
199
+ videoLoggerSystem
200
+ .getLogger('useLoopbackRecording')
201
+ .warn('timed out waiting for loopback streams');
202
+ return null;
203
+ }
204
+
205
+ audioTrack = streams.audioTrack;
206
+ const videoTrackId = streams.videoTrack?.id;
207
+
208
+ // The loopback audio track lands disabled (the SDK default-mutes
209
+ // it to prevent echo). Enable it so the native recording
210
+ // pipeline receives PCM; `finally` returns it to muted.
211
+ if (audioTrack) {
212
+ audioTrack.enabled = true;
213
+ }
214
+ updateState('recording');
215
+
216
+ const clampedDuration = Math.min(
217
+ MAX_RECORDING_DURATION,
218
+ Math.max(MIN_RECORDING_DURATION, maxDurationMs),
219
+ );
220
+
221
+ // Pre-size the native encoder to the publisher's max video
222
+ // publish-option dimension. Without this, the encoder locks to
223
+ // whatever the SFU happens to echo *first* and stays there for the
224
+ // rest of the recording even after the network improves and
225
+ // higher layers arrive.
226
+ const publishMaxDim = call.getMaxVideoPublishDimension();
227
+
228
+ const uri: string | null =
229
+ await StreamVideoReactNative.startTrackRecording({
230
+ videoTrackId,
231
+ maxDurationMs: Math.round(clampedDuration),
232
+ targetWidth: publishMaxDim?.width,
233
+ targetHeight: publishMaxDim?.height,
234
+ });
235
+ return uri;
236
+ } finally {
237
+ if (audioTrack) {
238
+ audioTrack.enabled = false;
239
+ }
240
+ awaitAbortRef.current = null;
241
+ updateState('idle');
242
+ }
243
+ },
244
+ [call, updateState],
245
+ );
246
+
247
+ const clearRecordings = useCallback(async (): Promise<void> => {
248
+ await StreamVideoReactNative.clearStreamRecordings();
249
+ }, []);
250
+
251
+ const getRecordings = useCallback(async (): Promise<string[]> => {
252
+ const list: string[] | null | undefined =
253
+ await StreamVideoReactNative.getStreamRecordings();
254
+ return list ?? [];
255
+ }, []);
256
+
257
+ // Auto-stop on call leave / end. Aborts an awaiting-streams wait or
258
+ // signals native finalisation depending on which phase we're in.
259
+ // Without this, leaving the call mid-recording would leave native
260
+ // encoders mid-write while the SFU subscriber tracks end under their
261
+ // feet — undefined final file state.
262
+ useEffect(() => {
263
+ if (
264
+ callingState === CallingState.LEFT ||
265
+ callingState === CallingState.IDLE
266
+ ) {
267
+ videoLoggerSystem
268
+ .getLogger('useLoopbackRecording')
269
+ .debug('auto-stopping recording on call leave / end');
270
+ stopRecording().catch(() => {});
271
+ }
272
+ }, [callingState, stopRecording]);
273
+
274
+ // Auto-stop if another participant joins. Loopback recording is a
275
+ // single-user pre-call test.
276
+ useEffect(() => {
277
+ if (recordingState !== 'idle' && participantCount > 1) {
278
+ stopRecording().catch(() => {});
279
+ videoLoggerSystem
280
+ .getLogger('useLoopbackRecording')
281
+ .debug('auto-stopping recording on participant count change');
282
+ }
283
+ }, [participantCount, recordingState, stopRecording]);
284
+
285
+ useEffect(() => {
286
+ isMountedRef.current = true;
287
+ return () => {
288
+ isMountedRef.current = false;
289
+ videoLoggerSystem
290
+ .getLogger('useLoopbackRecording')
291
+ .debug('auto-stopping recording on unmount');
292
+ stopRecording().catch(() => {});
293
+ };
294
+ }, [stopRecording]);
295
+
296
+ // Subscribe to the local participant, camera and microphone streams and update the loopback streams state.
297
+ useEffect(() => {
298
+ if (!call) return;
299
+
300
+ const subscription = combineLatest([
301
+ call.state.localParticipant$,
302
+ call.camera.state.mediaStream$,
303
+ call.microphone.state.mediaStream$,
304
+ ])
305
+ .pipe(
306
+ map(([participant, cameraStream, microphoneStream]) =>
307
+ getLoopbackStreamsFor(participant, cameraStream, microphoneStream),
308
+ ),
309
+ distinctUntilChanged(
310
+ (a, b) =>
311
+ a.loopbackVideoStream === b.loopbackVideoStream &&
312
+ a.loopbackAudioStream === b.loopbackAudioStream,
313
+ ),
314
+ )
315
+ .subscribe(setLoopbackStreams);
316
+
317
+ return () => subscription.unsubscribe();
318
+ }, [call]);
319
+
320
+ return {
321
+ startRecording,
322
+ stopRecording,
323
+ clearRecordings,
324
+ getRecordings,
325
+ recordingState,
326
+ loopbackVideoStream: loopbackStreams.loopbackVideoStream,
327
+ loopbackAudioStream: loopbackStreams.loopbackAudioStream,
328
+ };
329
+ }
330
+
331
+ function getLoopbackStreamsFor(
332
+ participant: StreamVideoParticipant | undefined,
333
+ cameraStream: MediaStream | undefined,
334
+ microphoneStream: MediaStream | undefined,
335
+ ): LoopbackStreams {
336
+ return {
337
+ loopbackVideoStream:
338
+ participant?.videoStream && participant.videoStream !== cameraStream
339
+ ? participant.videoStream
340
+ : undefined,
341
+ loopbackAudioStream:
342
+ participant?.audioStream && participant.audioStream !== microphoneStream
343
+ ? participant.audioStream
344
+ : undefined,
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Subscribe to `localParticipant$` and resolve once the requested loopback
350
+ * streams are present on the participant. Aborts cleanly on `signal`
351
+ * (resolves `null`) and rejects on timeout.
352
+ */
353
+ function waitForLoopbackStreams(
354
+ call: Call,
355
+ opts: { includeVideo: boolean; signal: AbortSignal; timeoutMs: number },
356
+ ): Promise<ResolvedStreams | null> {
357
+ return new Promise((resolve, reject) => {
358
+ const initial = getLoopbackStreams(
359
+ call.state.localParticipant,
360
+ call.camera.state.mediaStream,
361
+ call.microphone.state.mediaStream,
362
+ opts.includeVideo,
363
+ );
364
+ if (initial) {
365
+ resolve(initial);
366
+ return;
367
+ }
368
+
369
+ const cleanup = () => {
370
+ subscription.unsubscribe();
371
+ if (timeoutId !== undefined) {
372
+ clearTimeout(timeoutId);
373
+ }
374
+ opts.signal.removeEventListener('abort', onAbort);
375
+ };
376
+
377
+ const onAbort = () => {
378
+ cleanup();
379
+ resolve(null);
380
+ };
381
+
382
+ opts.signal.addEventListener('abort', onAbort);
383
+
384
+ const timeoutId = setTimeout(() => {
385
+ cleanup();
386
+ reject(
387
+ new Error(
388
+ 'useLoopbackRecording: timed out waiting for loopback streams. ' +
389
+ 'Ensure the call was joined with `selfSubEnabled: true` and ' +
390
+ 'that the SFU is configured to echo self-sub tracks.',
391
+ ),
392
+ );
393
+ }, opts.timeoutMs);
394
+
395
+ const subscription = combineLatest([
396
+ call.state.localParticipant$,
397
+ call.camera.state.mediaStream$,
398
+ call.microphone.state.mediaStream$,
399
+ ]).subscribe(([participant, cameraStream, microphoneStream]) => {
400
+ const ready = getLoopbackStreams(
401
+ participant,
402
+ cameraStream,
403
+ microphoneStream,
404
+ opts.includeVideo,
405
+ );
406
+ if (ready) {
407
+ cleanup();
408
+ resolve(ready);
409
+ }
410
+ });
411
+ });
412
+ }
413
+
414
+ function getLoopbackStreams(
415
+ participant: StreamVideoParticipant | undefined,
416
+ cameraStream: MediaStream | undefined,
417
+ microphoneStream: MediaStream | undefined,
418
+ includeVideo: boolean,
419
+ ): ResolvedStreams | undefined {
420
+ if (!participant) return undefined;
421
+
422
+ const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreamsFor(
423
+ participant,
424
+ cameraStream,
425
+ microphoneStream,
426
+ );
427
+
428
+ const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
429
+ const videoTrack = includeVideo
430
+ ? loopbackVideoStream?.getVideoTracks()[0]
431
+ : undefined;
432
+
433
+ if (!audioTrack || (includeVideo && !videoTrack)) {
434
+ return undefined;
435
+ }
436
+
437
+ return { audioTrack, videoTrack };
438
+ }
@@ -2,7 +2,7 @@
2
2
  * Internal utils for callingx library usage from video-client.
3
3
  * See @./registerSDKGlobals.ts for more usage details.
4
4
  */
5
- import { AppState, NativeModules, Platform } from 'react-native';
5
+ import { Platform } from 'react-native';
6
6
  import type { EndCallReason } from '@stream-io/react-native-callingx';
7
7
  import { getCallingxLibIfAvailable } from '../../push/libs/callingx';
8
8
  import { waitForAudioSessionActivation } from './audioSessionPromise';
@@ -12,17 +12,8 @@ import type {
12
12
  StreamVideoParticipant,
13
13
  } from '@stream-io/video-client';
14
14
  import { CallingState, videoLoggerSystem } from '@stream-io/video-client';
15
- import { StreamVideoRN } from '../../StreamVideoRN';
16
- const CallingxModule = getCallingxLibIfAvailable();
17
15
 
18
- async function isAppInForeground(): Promise<boolean> {
19
- if (Platform.OS === 'android') {
20
- const nativeModule = NativeModules.StreamVideoAppLifecycle;
21
- const state = await nativeModule.getCurrentAppState();
22
- return state === 'active';
23
- }
24
- return AppState.currentState !== 'background';
25
- }
16
+ const CallingxModule = getCallingxLibIfAvailable();
26
17
 
27
18
  /**
28
19
  * Gets the call display name. To be used for display in native call screen.
@@ -75,7 +66,7 @@ function getCallDisplayNameFromCall(call: Call): string {
75
66
  }
76
67
 
77
68
  export async function registerOutgoingCall(call: Call) {
78
- if (!CallingxModule || !CallingxModule.isSetup) {
69
+ if (!CallingxModule || !CallingxModule.isSetup || call.isSelfSubEnabled) {
79
70
  return;
80
71
  }
81
72
 
@@ -112,7 +103,7 @@ export async function registerOutgoingCall(call: Call) {
112
103
  * 3. Optionally for non-ringing calls also when ongoing calls are enabled.
113
104
  */
114
105
  export async function joinCallingxCall(call: Call, activeCalls: Call[]) {
115
- if (!CallingxModule || !CallingxModule.isSetup) {
106
+ if (!CallingxModule || !CallingxModule.isSetup || call.isSelfSubEnabled) {
116
107
  return;
117
108
  }
118
109
 
@@ -148,19 +139,7 @@ export async function joinCallingxCall(call: Call, activeCalls: Call[]) {
148
139
  }
149
140
  } else if (isIncomingCall) {
150
141
  logger.debug(`joinCallingxCall: Joining incoming call ${call.cid}`);
151
- let skipIncomingPushInForeground = false;
152
- if (Platform.OS === 'ios') {
153
- skipIncomingPushInForeground =
154
- StreamVideoRN.getConfig().push?.ios?.skipIncomingPushInForeground ??
155
- false;
156
- } else {
157
- skipIncomingPushInForeground =
158
- StreamVideoRN.getConfig().push?.android?.skipIncomingPushInForeground ??
159
- false;
160
- }
161
- const shouldSkipDisplayIncoming = skipIncomingPushInForeground
162
- ? await isAppInForeground()
163
- : false;
142
+
164
143
  try {
165
144
  // Leave any existing active ringing calls before joining a new ringing call
166
145
  const activeCallsToLeave = activeCalls.filter(
@@ -177,29 +156,25 @@ export async function joinCallingxCall(call: Call, activeCalls: Call[]) {
177
156
  logger.error(`failed to leave active call ${activeCall.cid}`, e);
178
157
  });
179
158
  }
180
- if (shouldSkipDisplayIncoming) {
181
- await startCallInCallingx();
182
- } else {
183
- // Awaits native CallKit/Telecom registration before answering.
184
- // Safe to call even if the call is already registered (e.g. from VoIP push) --
185
- // iOS early-returns with no error, Android sends the registered broadcast.
186
- const callDisplayName = getCallDisplayNameFromCall(call);
187
- await CallingxModule.displayIncomingCall(
188
- call.cid, // unique id for call
189
- call.state.createdBy?.id ?? callDisplayName, // handle for native call UI (prefer createdBy user id, fallback to call display name)
190
- callDisplayName, // display name for display in call screen
191
- call.state.settings?.video?.enabled ?? false, // is video call?
192
- );
159
+ // Awaits native CallKit/Telecom registration before answering.
160
+ // Safe to call even if the call is already registered (e.g. from VoIP push) --
161
+ // iOS early-returns with no error, Android sends the registered broadcast.
162
+ const callDisplayName = getCallDisplayNameFromCall(call);
163
+ await CallingxModule.displayIncomingCall(
164
+ call.cid, // unique id for call
165
+ call.state.createdBy?.id ?? callDisplayName, // handle for native call UI (prefer createdBy user id, fallback to call display name)
166
+ callDisplayName, // display name for display in call screen
167
+ call.state.settings?.video?.enabled ?? false, // is video call?
168
+ );
193
169
 
194
- await CallingxModule.answerIncomingCall(call.cid);
170
+ await CallingxModule.answerIncomingCall(call.cid);
195
171
 
196
- if (Platform.OS === 'ios') {
197
- await waitForAudioSessionActivation();
198
- }
172
+ if (Platform.OS === 'ios') {
173
+ await waitForAudioSessionActivation();
199
174
  }
200
175
  } catch (error) {
201
176
  logger.error(
202
- `Error joining incoming call in callingx: ${call.cid} shouldSkipDisplayIncoming: ${shouldSkipDisplayIncoming}`,
177
+ `Error joining incoming call in callingx: ${call.cid}`,
203
178
  error,
204
179
  );
205
180
  }
@@ -48,10 +48,9 @@ export const onVoipNotificationReceived = async (
48
48
  }
49
49
 
50
50
  const callingx = getCallingxLib();
51
- if (callingx.isCallTracked(call_cid)) {
52
- //same call_cid is already tracked, so we skip the notification
51
+ if (pushUnsubscriptionCallbacks.has(call_cid)) {
53
52
  logger.debug(
54
- `the same call_cid ${call_cid} is already tracked, skipping the call.ring notification`,
53
+ `the same call_cid ${call_cid} is already being watched, skipping the call.ring notification`,
55
54
  );
56
55
  return;
57
56
  }
@@ -91,6 +90,7 @@ export const onVoipNotificationReceived = async (
91
90
  event,
92
91
  );
93
92
  unsubscribe();
93
+ pushUnsubscriptionCallbacks.delete(call_cid);
94
94
  return;
95
95
  }
96
96
  const _closed = closeCallIfNecessary();
@@ -100,6 +100,7 @@ export const onVoipNotificationReceived = async (
100
100
  event,
101
101
  );
102
102
  unsubscribe();
103
+ pushUnsubscriptionCallbacks.delete(call_cid);
103
104
  }
104
105
  });
105
106
 
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '1.36.2';
1
+ export const version = '1.37.1-beta.0';