@stream-io/video-client 1.44.1-beta.1 → 1.44.1-beta.2

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.
@@ -761,7 +761,7 @@ export declare class Call {
761
761
  *
762
762
  * @internal
763
763
  */
764
- applyDeviceConfig: (settings: CallSettingsResponse, publish: boolean, skipSpeakerApply?: boolean) => Promise<void>;
764
+ applyDeviceConfig: (settings: CallSettingsResponse, publish: boolean) => Promise<void>;
765
765
  /**
766
766
  * Will begin tracking the given element for visibility changes within the
767
767
  * configured viewport element (`call.setViewport`).
@@ -5,6 +5,7 @@ import type { RejectReason, StreamClientOptions, TokenProvider, User } from './c
5
5
  import type { Comparator } from './sorting';
6
6
  import type { StreamVideoWriteableStateStore } from './store';
7
7
  import { AxiosError } from 'axios';
8
+ import type { Call } from './Call';
8
9
  export type StreamReaction = Pick<ReactionResponse, 'type' | 'emoji_code' | 'custom'>;
9
10
  export declare enum VisibilityState {
10
11
  UNKNOWN = "UNKNOWN",
@@ -316,22 +317,52 @@ export type StartCallRecordingFnType = {
316
317
  (request: StartRecordingRequest): Promise<StartRecordingResponse>;
317
318
  (request: StartRecordingRequest, type: CallRecordingType): Promise<StartRecordingResponse>;
318
319
  };
320
+ type StreamRNVideoSDKCallManagerRingingParams = {
321
+ isRingingTypeCall: boolean;
322
+ };
323
+ type StreamRNVideoSDKCallManagerSetupParams = StreamRNVideoSDKCallManagerRingingParams & {
324
+ defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
325
+ };
326
+ type StreamRNVideoSDKEndCallReason =
327
+ /** Call ended by the local user (e.g., hanging up). */
328
+ 'local'
329
+ /** Call ended by the remote party, or outgoing call was not answered. */
330
+ | 'remote'
331
+ /** Call was rejected/declined by the user. */
332
+ | 'rejected'
333
+ /** Remote party was busy. */
334
+ | 'busy'
335
+ /** Call was answered on another device. */
336
+ | 'answeredElsewhere'
337
+ /** No response to an incoming call. */
338
+ | 'missed'
339
+ /** Call failed due to an error (e.g., network issue). */
340
+ | 'error'
341
+ /** Call was canceled before the remote party could answer. */
342
+ | 'canceled'
343
+ /** Call restricted (e.g., airplane mode, dialing restrictions). */
344
+ | 'restricted'
345
+ /** Unknown or unspecified disconnect reason. */
346
+ | 'unknown';
347
+ type StreamRNVideoSDKCallingX = {
348
+ startCall: (call: Call, activeCalls: Call[]) => Promise<void>;
349
+ endCall: (call: Call, reason?: StreamRNVideoSDKEndCallReason) => Promise<void>;
350
+ };
319
351
  export type StreamRNVideoSDKGlobals = {
352
+ callingX: StreamRNVideoSDKCallingX;
320
353
  callManager: {
321
354
  /**
322
355
  * Sets up the in call manager.
323
356
  */
324
- setup({ defaultDevice, }: {
325
- defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
326
- }): void;
357
+ setup({ defaultDevice, isRingingTypeCall, }: StreamRNVideoSDKCallManagerSetupParams): void;
327
358
  /**
328
359
  * Starts the in call manager.
329
360
  */
330
- start(): void;
361
+ start({ isRingingTypeCall, }: StreamRNVideoSDKCallManagerRingingParams): void;
331
362
  /**
332
363
  * Stops the in call manager.
333
364
  */
334
- stop(): void;
365
+ stop({ isRingingTypeCall }: StreamRNVideoSDKCallManagerRingingParams): void;
335
366
  };
336
367
  permissions: {
337
368
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.44.1-beta.1",
3
+ "version": "1.44.1-beta.2",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
package/src/Call.ts CHANGED
@@ -172,7 +172,6 @@ import {
172
172
  promiseWithResolvers,
173
173
  } from './helpers/promise';
174
174
  import { GetCallStatsResponse } from './gen/shims';
175
- import { isReactNative } from './helpers/platforms';
176
175
 
177
176
  /**
178
177
  * An object representation of a `Call`.
@@ -421,6 +420,7 @@ export class Call {
421
420
  const currentUserId = this.currentUserId;
422
421
  if (currentUserId && blockedUserIds.includes(currentUserId)) {
423
422
  this.logger.info('Leaving call because of being blocked');
423
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
424
424
  await this.leave({ message: 'user blocked' }).catch((err) => {
425
425
  this.logger.error('Error leaving call after being blocked', err);
426
426
  });
@@ -465,6 +465,10 @@ export class Call {
465
465
  (isAcceptedElsewhere || isRejectedByMe) &&
466
466
  !hasPending(this.joinLeaveConcurrencyTag)
467
467
  ) {
468
+ globalThis.streamRNVideoSDK?.callingX?.endCall(
469
+ this,
470
+ isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected',
471
+ );
468
472
  this.leave().catch(() => {
469
473
  this.logger.error(
470
474
  'Could not leave a call that was accepted or rejected elsewhere',
@@ -636,16 +640,30 @@ export class Call {
636
640
 
637
641
  if (callingState === CallingState.RINGING && reject !== false) {
638
642
  if (reject) {
639
- await this.reject(reason ?? 'decline');
643
+ const reasonToEndCallReason = {
644
+ timeout: 'missed',
645
+ cancel: 'canceled',
646
+ busy: 'busy',
647
+ decline: 'rejected',
648
+ } as const;
649
+ const rejectReason = reason ?? 'decline';
650
+ const endCallReason =
651
+ reasonToEndCallReason[
652
+ rejectReason as keyof typeof reasonToEndCallReason
653
+ ] ?? 'rejected';
654
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
655
+ await this.reject(rejectReason);
640
656
  } else {
641
657
  // if reject was undefined, we still have to cancel the call automatically
642
658
  // when I am the creator and everyone else left the call
643
659
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
644
660
  if (this.isCreatedByMe && !hasOtherParticipants) {
661
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
645
662
  await this.reject('cancel');
646
663
  }
647
664
  }
648
665
  }
666
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this);
649
667
 
650
668
  this.statsReporter?.stop();
651
669
  this.statsReporter = undefined;
@@ -680,7 +698,9 @@ export class Call {
680
698
  this.cancelAutoDrop();
681
699
  this.clientStore.unregisterCall(this);
682
700
 
683
- globalThis.streamRNVideoSDK?.callManager.stop();
701
+ globalThis.streamRNVideoSDK?.callManager.stop({
702
+ isRingingTypeCall: this.ringing,
703
+ });
684
704
 
685
705
  this.camera.dispose();
686
706
  this.microphone.dispose();
@@ -747,8 +767,7 @@ export class Call {
747
767
  // const calls = useCalls().filter((c) => c.ringing);
748
768
  const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
749
769
  this.clientStore.setCalls([this, ...calls]);
750
- const skipSpeakerApply = isReactNative() && true;
751
- await this.applyDeviceConfig(settings, false, skipSpeakerApply);
770
+ await this.applyDeviceConfig(settings, false);
752
771
  };
753
772
 
754
773
  /**
@@ -766,6 +785,7 @@ export class Call {
766
785
  video?: boolean;
767
786
  }): Promise<GetCallResponse> => {
768
787
  await this.setup();
788
+
769
789
  const response = await this.streamClient.get<GetCallResponse>(
770
790
  this.streamClientBasePath,
771
791
  params,
@@ -783,14 +803,8 @@ export class Call {
783
803
  this.watching = true;
784
804
  this.clientStore.registerOrUpdateCall(this);
785
805
  }
786
- const skipSpeakerApply = isReactNative()
787
- ? (params?.ring ?? this.ringing)
788
- : false;
789
- await this.applyDeviceConfig(
790
- response.call.settings,
791
- false,
792
- skipSpeakerApply,
793
- );
806
+
807
+ await this.applyDeviceConfig(response.call.settings, false);
794
808
 
795
809
  return response;
796
810
  };
@@ -802,6 +816,7 @@ export class Call {
802
816
  */
803
817
  getOrCreate = async (data?: GetOrCreateCallRequest) => {
804
818
  await this.setup();
819
+
805
820
  const response = await this.streamClient.post<
806
821
  GetOrCreateCallResponse,
807
822
  GetOrCreateCallRequest
@@ -820,14 +835,7 @@ export class Call {
820
835
  this.clientStore.registerOrUpdateCall(this);
821
836
  }
822
837
 
823
- const skipSpeakerApply = isReactNative()
824
- ? (data?.ring ?? this.ringing)
825
- : false;
826
- await this.applyDeviceConfig(
827
- response.call.settings,
828
- false,
829
- skipSpeakerApply,
830
- );
838
+ await this.applyDeviceConfig(response.call.settings, false);
831
839
 
832
840
  return response;
833
841
  };
@@ -924,60 +932,73 @@ export class Call {
924
932
  joinResponseTimeout?: number;
925
933
  rpcRequestTimeout?: number;
926
934
  } = {}): Promise<void> => {
927
- await this.setup();
928
935
  const callingState = this.state.callingState;
929
936
 
930
937
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
931
938
  throw new Error(`Illegal State: call.join() shall be called only once`);
932
939
  }
933
940
 
941
+ if (data?.ring) {
942
+ this.ringingSubject.next(true);
943
+ }
944
+ const callingX = globalThis.streamRNVideoSDK?.callingX;
945
+ if (callingX) {
946
+ // for Android/iOS, we need to start the call in the callingx library as soon as possible
947
+ await callingX.startCall(this, this.clientStore.calls);
948
+ }
949
+
950
+ await this.setup();
951
+
934
952
  this.joinResponseTimeout = joinResponseTimeout;
935
953
  this.rpcRequestTimeout = rpcRequestTimeout;
936
-
937
954
  // we will count the number of join failures per SFU.
938
955
  // once the number of failures reaches 2, we will piggyback on the `migrating_from`
939
956
  // field to force the coordinator to provide us another SFU
940
957
  const sfuJoinFailures = new Map<string, number>();
941
958
  const joinData: JoinCallData = data;
942
959
  maxJoinRetries = Math.max(maxJoinRetries, 1);
943
- for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
944
- try {
945
- this.logger.trace(`Joining call (${attempt})`, this.cid);
946
- await this.doJoin(data);
947
- delete joinData.migrating_from;
948
- delete joinData.migrating_from_list;
949
- break;
950
- } catch (err) {
951
- this.logger.warn(`Failed to join call (${attempt})`, this.cid);
952
- if (
953
- (err instanceof ErrorFromResponse && err.unrecoverable) ||
954
- (err instanceof SfuJoinError && err.unrecoverable)
955
- ) {
956
- // if the error is unrecoverable, we should not retry as that signals
957
- // that connectivity is good, but the coordinator doesn't allow the user
958
- // to join the call due to some reason (e.g., ended call, expired token...)
959
- throw err;
960
- }
960
+ try {
961
+ for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
962
+ try {
963
+ this.logger.trace(`Joining call (${attempt})`, this.cid);
964
+ await this.doJoin(data);
965
+ delete joinData.migrating_from;
966
+ delete joinData.migrating_from_list;
967
+ break;
968
+ } catch (err) {
969
+ this.logger.warn(`Failed to join call (${attempt})`, this.cid);
970
+ if (
971
+ (err instanceof ErrorFromResponse && err.unrecoverable) ||
972
+ (err instanceof SfuJoinError && err.unrecoverable)
973
+ ) {
974
+ // if the error is unrecoverable, we should not retry as that signals
975
+ // that connectivity is good, but the coordinator doesn't allow the user
976
+ // to join the call due to some reason (e.g., ended call, expired token...)
977
+ throw err;
978
+ }
961
979
 
962
- // immediately switch to a different SFU in case of recoverable join error
963
- const switchSfu =
964
- err instanceof SfuJoinError &&
965
- SfuJoinError.isJoinErrorCode(err.errorEvent);
966
-
967
- const sfuId = this.credentials?.server.edge_name || '';
968
- const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
969
- sfuJoinFailures.set(sfuId, failures);
970
- if (switchSfu || failures >= 2) {
971
- joinData.migrating_from = sfuId;
972
- joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
973
- }
980
+ // immediately switch to a different SFU in case of recoverable join error
981
+ const switchSfu =
982
+ err instanceof SfuJoinError &&
983
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
984
+
985
+ const sfuId = this.credentials?.server.edge_name || '';
986
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
987
+ sfuJoinFailures.set(sfuId, failures);
988
+ if (switchSfu || failures >= 2) {
989
+ joinData.migrating_from = sfuId;
990
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
991
+ }
974
992
 
975
- if (attempt === maxJoinRetries - 1) {
976
- throw err;
993
+ if (attempt === maxJoinRetries - 1) {
994
+ throw err;
995
+ }
977
996
  }
997
+ await sleep(retryInterval(attempt));
978
998
  }
979
-
980
- await sleep(retryInterval(attempt));
999
+ } catch (error) {
1000
+ callingX?.endCall(this, 'error');
1001
+ throw error;
981
1002
  }
982
1003
  };
983
1004
 
@@ -1160,7 +1181,9 @@ export class Call {
1160
1181
  // re-apply them on later reconnections or server-side data fetches
1161
1182
  if (!this.deviceSettingsAppliedOnce && this.state.settings) {
1162
1183
  await this.applyDeviceConfig(this.state.settings, true);
1163
- globalThis.streamRNVideoSDK?.callManager.start();
1184
+ globalThis.streamRNVideoSDK?.callManager.start({
1185
+ isRingingTypeCall: this.ringing,
1186
+ });
1164
1187
  this.deviceSettingsAppliedOnce = true;
1165
1188
  }
1166
1189
 
@@ -1705,6 +1728,7 @@ export class Call {
1705
1728
  if (SfuJoinError.isJoinErrorCode(e)) return;
1706
1729
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED) return;
1707
1730
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
1731
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
1708
1732
  this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
1709
1733
  this.logger.warn(`Can't leave call after disconnect request`, err);
1710
1734
  });
@@ -2811,11 +2835,8 @@ export class Call {
2811
2835
  applyDeviceConfig = async (
2812
2836
  settings: CallSettingsResponse,
2813
2837
  publish: boolean,
2814
- skipSpeakerApply: boolean = false,
2815
2838
  ) => {
2816
- if (!skipSpeakerApply) {
2817
- this.speaker.apply(settings);
2818
- }
2839
+ this.speaker.apply(settings);
2819
2840
  await this.camera.apply(settings.video, publish).catch((err) => {
2820
2841
  this.logger.warn('Camera init failed', err);
2821
2842
  });
@@ -85,6 +85,7 @@ export class SpeakerManager {
85
85
  this.defaultDevice = defaultDevice;
86
86
  globalThis.streamRNVideoSDK?.callManager.setup({
87
87
  defaultDevice,
88
+ isRingingTypeCall: this.call.ringing,
88
89
  });
89
90
  }
90
91
  }
@@ -69,6 +69,7 @@ export const watchCallRejected = (call: Call) => {
69
69
  } else {
70
70
  if (rejectedBy[eventCall.created_by.id]) {
71
71
  call.logger.info('call creator rejected, leaving call');
72
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
72
73
  await call.leave({ message: 'ring: creator rejected' });
73
74
  }
74
75
  }
@@ -85,6 +86,7 @@ export const watchCallEnded = (call: Call) => {
85
86
  callingState !== CallingState.IDLE &&
86
87
  callingState !== CallingState.LEFT
87
88
  ) {
89
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
88
90
  call
89
91
  .leave({ message: 'call.ended event received', reject: false })
90
92
  .catch((err) => {
@@ -113,6 +115,7 @@ export const watchSfuCallEnded = (call: Call) => {
113
115
  // update the call state to reflect the call has ended.
114
116
  call.state.setEndedAt(new Date());
115
117
  const reason = CallEndedReason[e.reason];
118
+ globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
116
119
  await call.leave({ message: `callEnded received: ${reason}` });
117
120
  } catch (err) {
118
121
  call.logger.error(
package/src/types.ts CHANGED
@@ -23,6 +23,7 @@ import type {
23
23
  import type { Comparator } from './sorting';
24
24
  import type { StreamVideoWriteableStateStore } from './store';
25
25
  import { AxiosError } from 'axios';
26
+ import type { Call } from './Call';
26
27
 
27
28
  export type StreamReaction = Pick<
28
29
  ReactionResponse,
@@ -392,26 +393,67 @@ export type StartCallRecordingFnType = {
392
393
  ): Promise<StartRecordingResponse>;
393
394
  };
394
395
 
396
+ type StreamRNVideoSDKCallManagerRingingParams = {
397
+ isRingingTypeCall: boolean;
398
+ };
399
+
400
+ type StreamRNVideoSDKCallManagerSetupParams =
401
+ StreamRNVideoSDKCallManagerRingingParams & {
402
+ defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
403
+ };
404
+
405
+ type StreamRNVideoSDKEndCallReason =
406
+ /** Call ended by the local user (e.g., hanging up). */
407
+ | 'local'
408
+ /** Call ended by the remote party, or outgoing call was not answered. */
409
+ | 'remote'
410
+ /** Call was rejected/declined by the user. */
411
+ | 'rejected'
412
+ /** Remote party was busy. */
413
+ | 'busy'
414
+ /** Call was answered on another device. */
415
+ | 'answeredElsewhere'
416
+ /** No response to an incoming call. */
417
+ | 'missed'
418
+ /** Call failed due to an error (e.g., network issue). */
419
+ | 'error'
420
+ /** Call was canceled before the remote party could answer. */
421
+ | 'canceled'
422
+ /** Call restricted (e.g., airplane mode, dialing restrictions). */
423
+ | 'restricted'
424
+ /** Unknown or unspecified disconnect reason. */
425
+ | 'unknown';
426
+
427
+ type StreamRNVideoSDKCallingX = {
428
+ startCall: (call: Call, activeCalls: Call[]) => Promise<void>;
429
+ endCall: (
430
+ call: Call,
431
+ reason?: StreamRNVideoSDKEndCallReason,
432
+ ) => Promise<void>;
433
+ };
434
+
395
435
  export type StreamRNVideoSDKGlobals = {
436
+ callingX: StreamRNVideoSDKCallingX;
396
437
  callManager: {
397
438
  /**
398
439
  * Sets up the in call manager.
399
440
  */
400
441
  setup({
401
442
  defaultDevice,
402
- }: {
403
- defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
404
- }): void;
443
+ isRingingTypeCall,
444
+ }: StreamRNVideoSDKCallManagerSetupParams): void;
405
445
 
406
446
  /**
407
447
  * Starts the in call manager.
408
448
  */
409
- start(): void;
449
+ start({
450
+ isRingingTypeCall,
451
+ }: StreamRNVideoSDKCallManagerRingingParams): void;
410
452
 
411
453
  /**
412
454
  * Stops the in call manager.
413
455
  */
414
- stop(): void;
456
+ stop({ isRingingTypeCall }: StreamRNVideoSDKCallManagerRingingParams): void;
415
457
  };
416
458
  permissions: {
417
459
  /**