@stream-io/video-client 1.45.0 → 1.46.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.
@@ -18,6 +18,7 @@ import { DynascaleManager } from '../DynascaleManager';
18
18
  import { Call } from '../../Call';
19
19
  import { StreamClient } from '../../coordinator/connection/client';
20
20
  import { StreamVideoWriteableStateStore } from '../../store';
21
+ import { getCurrentValue } from '../../store/rxUtils';
21
22
  import { VisibilityState } from '../../types';
22
23
  import { noopComparator } from '../../sorting';
23
24
  import { TrackType } from '../../gen/video/sfu/models/models';
@@ -647,6 +648,125 @@ describe('DynascaleManager', () => {
647
648
  expect(unregisterSpy).toHaveBeenCalledWith('session-id', 'audioTrack');
648
649
  });
649
650
 
651
+ it('audio: should track blocked audio elements on NotAllowedError', async () => {
652
+ vi.useFakeTimers();
653
+ const audioElement = document.createElement('audio');
654
+ Object.defineProperty(audioElement, 'srcObject', { writable: true });
655
+ const notAllowedError = new DOMException('', 'NotAllowedError');
656
+ vi.spyOn(audioElement, 'play').mockRejectedValue(notAllowedError);
657
+
658
+ // @ts-expect-error incomplete data
659
+ call.state.updateOrAddParticipant('session-id', {
660
+ userId: 'user-id',
661
+ sessionId: 'session-id',
662
+ publishedTracks: [],
663
+ });
664
+
665
+ const cleanup = dynascaleManager.bindAudioElement(
666
+ audioElement,
667
+ 'session-id',
668
+ 'audioTrack',
669
+ );
670
+
671
+ const mediaStream = new MediaStream();
672
+ call.state.updateParticipant('session-id', {
673
+ audioStream: mediaStream,
674
+ });
675
+
676
+ vi.runAllTimers();
677
+ await vi.advanceTimersByTimeAsync(0);
678
+
679
+ expect(getCurrentValue(dynascaleManager.autoplayBlocked$)).toBe(true);
680
+
681
+ cleanup?.();
682
+ expect(getCurrentValue(dynascaleManager.autoplayBlocked$)).toBe(false);
683
+ });
684
+
685
+ it('audio: should unblock audio elements on explicit resumeAudio call', async () => {
686
+ vi.useFakeTimers();
687
+ const audioElement = document.createElement('audio');
688
+ Object.defineProperty(audioElement, 'srcObject', { writable: true });
689
+ const playSpy = vi
690
+ .spyOn(audioElement, 'play')
691
+ .mockRejectedValueOnce(new DOMException('', 'NotAllowedError'))
692
+ .mockResolvedValue(undefined);
693
+
694
+ // @ts-expect-error incomplete data
695
+ call.state.updateOrAddParticipant('session-id', {
696
+ userId: 'user-id',
697
+ sessionId: 'session-id',
698
+ publishedTracks: [],
699
+ });
700
+
701
+ const cleanup = dynascaleManager.bindAudioElement(
702
+ audioElement,
703
+ 'session-id',
704
+ 'audioTrack',
705
+ );
706
+
707
+ const mediaStream = new MediaStream();
708
+ call.state.updateParticipant('session-id', {
709
+ audioStream: mediaStream,
710
+ });
711
+
712
+ vi.runAllTimers();
713
+ await vi.advanceTimersByTimeAsync(0);
714
+
715
+ expect(getCurrentValue(dynascaleManager.autoplayBlocked$)).toBe(true);
716
+
717
+ await dynascaleManager.resumeAudio();
718
+ await vi.advanceTimersByTimeAsync(0);
719
+
720
+ expect(playSpy).toHaveBeenCalledTimes(2);
721
+ expect(getCurrentValue(dynascaleManager.autoplayBlocked$)).toBe(false);
722
+
723
+ cleanup?.();
724
+ });
725
+
726
+ it('audio: should clear blocked state when the audio stream is removed', async () => {
727
+ vi.useFakeTimers();
728
+ const audioElement = document.createElement('audio');
729
+ Object.defineProperty(audioElement, 'srcObject', { writable: true });
730
+ vi.spyOn(audioElement, 'play').mockRejectedValue(
731
+ new DOMException('', 'NotAllowedError'),
732
+ );
733
+
734
+ // @ts-expect-error incomplete data
735
+ call.state.updateOrAddParticipant('session-id', {
736
+ userId: 'user-id',
737
+ sessionId: 'session-id',
738
+ publishedTracks: [],
739
+ });
740
+
741
+ const cleanup = dynascaleManager.bindAudioElement(
742
+ audioElement,
743
+ 'session-id',
744
+ 'audioTrack',
745
+ );
746
+
747
+ const mediaStream = new MediaStream();
748
+ call.state.updateParticipant('session-id', {
749
+ audioStream: mediaStream,
750
+ });
751
+
752
+ vi.runAllTimers();
753
+ await vi.advanceTimersByTimeAsync(0);
754
+
755
+ expect(getCurrentValue(dynascaleManager.autoplayBlocked$)).toBe(true);
756
+
757
+ call.state.updateParticipant('session-id', {
758
+ audioStream: undefined,
759
+ });
760
+
761
+ vi.runAllTimers();
762
+ await vi.advanceTimersByTimeAsync(0);
763
+
764
+ expect(audioElement.srcObject).toBeNull();
765
+ expect(getCurrentValue(dynascaleManager.autoplayBlocked$)).toBe(false);
766
+
767
+ cleanup?.();
768
+ });
769
+
650
770
  it('audio: should warn when binding an already-bound session', () => {
651
771
  const watchdog = dynascaleManager.audioBindingsWatchdog!;
652
772
  // @ts-expect-error private property
@@ -42,7 +42,7 @@ export class StreamVideoWriteableStateStore {
42
42
  * The currently connected user.
43
43
  */
44
44
  get connectedUser(): OwnUserResponse | undefined {
45
- return RxUtils.getCurrentValue(this.connectedUserSubject);
45
+ return this.connectedUserSubject.getValue();
46
46
  }
47
47
 
48
48
  /**
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,68 @@ 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
+ joinCall: (call: Call, activeCalls: Call[]) => Promise<void>;
429
+ endCall: (
430
+ call: Call,
431
+ reason?: StreamRNVideoSDKEndCallReason,
432
+ ) => Promise<void>;
433
+ registerOutgoingCall: (call: Call) => Promise<void>;
434
+ };
435
+
395
436
  export type StreamRNVideoSDKGlobals = {
437
+ callingX: StreamRNVideoSDKCallingX;
396
438
  callManager: {
397
439
  /**
398
440
  * Sets up the in call manager.
399
441
  */
400
442
  setup({
401
443
  defaultDevice,
402
- }: {
403
- defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
404
- }): void;
444
+ isRingingTypeCall,
445
+ }: StreamRNVideoSDKCallManagerSetupParams): void;
405
446
 
406
447
  /**
407
448
  * Starts the in call manager.
408
449
  */
409
- start(): void;
450
+ start({
451
+ isRingingTypeCall,
452
+ }: StreamRNVideoSDKCallManagerRingingParams): void;
410
453
 
411
454
  /**
412
455
  * Stops the in call manager.
413
456
  */
414
- stop(): void;
457
+ stop({ isRingingTypeCall }: StreamRNVideoSDKCallManagerRingingParams): void;
415
458
  };
416
459
  permissions: {
417
460
  /**