@stream-io/video-client 1.58.0 → 1.59.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 (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/index.browser.es.js +319 -71
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +329 -69
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.es.js +319 -71
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/Call.d.ts +15 -4
  10. package/dist/src/StreamVideoClient.d.ts +9 -1
  11. package/dist/src/devices/CameraManager.d.ts +2 -1
  12. package/dist/src/devices/DeviceManager.d.ts +1 -1
  13. package/dist/src/devices/MicrophoneManager.d.ts +2 -1
  14. package/dist/src/gen/coordinator/index.d.ts +26 -0
  15. package/dist/src/helpers/MediaStreamRecorder.d.ts +26 -0
  16. package/dist/src/helpers/concurrency.d.ts +8 -0
  17. package/dist/src/helpers/loopback.d.ts +55 -0
  18. package/dist/src/reporting/ClientEventReporter.d.ts +0 -2
  19. package/dist/src/rtc/Subscriber.d.ts +5 -0
  20. package/index.ts +2 -0
  21. package/package.json +1 -1
  22. package/src/Call.ts +156 -134
  23. package/src/StreamVideoClient.ts +35 -8
  24. package/src/__tests__/Call.lifecycle.test.ts +33 -2
  25. package/src/__tests__/Call.publishing.test.ts +42 -1
  26. package/src/__tests__/StreamVideoClient.api.test.ts +33 -0
  27. package/src/__tests__/StreamVideoClient.rewatch.test.ts +213 -0
  28. package/src/__tests__/StreamVideoClient.ringing.test.ts +11 -4
  29. package/src/__tests__/StreamVideoClient.test.ts +14 -12
  30. package/src/devices/CameraManager.ts +16 -4
  31. package/src/devices/DeviceManager.ts +7 -4
  32. package/src/devices/MicrophoneManager.ts +11 -3
  33. package/src/devices/__tests__/CameraManager.test.ts +27 -1
  34. package/src/devices/__tests__/DeviceManager.test.ts +49 -1
  35. package/src/devices/__tests__/MicrophoneManager.test.ts +23 -1
  36. package/src/events/call.ts +6 -3
  37. package/src/gen/coordinator/index.ts +26 -0
  38. package/src/helpers/MediaStreamRecorder.ts +104 -0
  39. package/src/helpers/__tests__/MediaStreamRecorder.test.ts +324 -0
  40. package/src/helpers/__tests__/concurrency.test.ts +74 -0
  41. package/src/helpers/__tests__/loopback.test.ts +367 -0
  42. package/src/helpers/concurrency.ts +24 -0
  43. package/src/helpers/loopback.ts +200 -0
  44. package/src/reporting/ClientEventReporter.ts +15 -28
  45. package/src/reporting/__tests__/ClientEventReporter.test.ts +3 -7
  46. package/src/rtc/Subscriber.ts +8 -0
  47. package/src/rtc/__tests__/Subscriber.test.ts +101 -0
@@ -7,7 +7,7 @@ import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transpor
7
7
  import * as scopedLogger from '@stream-io/logger';
8
8
  export { LogLevelEnum } from '@stream-io/logger';
9
9
  import { parse, write } from 'sdp-transform';
10
- import { Subject, Observable, combineLatest, BehaviorSubject, ReplaySubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of } from 'rxjs';
10
+ import { Subject, Observable, combineLatest, BehaviorSubject, ReplaySubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of, Subscription } from 'rxjs';
11
11
  import { UAParser } from 'ua-parser-js';
12
12
  import { WorkerTimer } from '@stream-io/worker-timer';
13
13
 
@@ -5059,7 +5059,7 @@ const toIceCandidate = (iceTrickle) => {
5059
5059
  }
5060
5060
  };
5061
5061
 
5062
- const version = "1.58.0";
5062
+ const version = "1.59.0";
5063
5063
  const [major, minor, patch] = version.split('.');
5064
5064
  let sdkInfo = {
5065
5065
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5316,6 +5316,27 @@ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
5316
5316
  */
5317
5317
  const withCancellation = createRunner(wrapWithCancellation);
5318
5318
  const pendingPromises = new Map();
5319
+ /**
5320
+ * Wraps an async function so concurrent calls share the in-flight promise
5321
+ * instead of queueing another execution.
5322
+ *
5323
+ * @param cb Async function to wrap.
5324
+ * @returns Wrapped async function.
5325
+ */
5326
+ function singleFlight(cb) {
5327
+ let pending;
5328
+ return (...args) => {
5329
+ if (pending)
5330
+ return pending;
5331
+ const promise = cb(...args).finally(() => {
5332
+ if (pending === promise) {
5333
+ pending = undefined;
5334
+ }
5335
+ });
5336
+ pending = promise;
5337
+ return promise;
5338
+ };
5339
+ }
5319
5340
  function hasPending(tag) {
5320
5341
  return pendingPromises.has(tag);
5321
5342
  }
@@ -9804,6 +9825,11 @@ class Subscriber extends BasePeerConnection {
9804
9825
  throw e;
9805
9826
  }
9806
9827
  };
9828
+ /**
9829
+ * Whether the given stream is one the SFU echoed back to us
9830
+ * (`allowOwnTracksLoopback`) rather than a local capture stream.
9831
+ */
9832
+ isSelfSubscribedStream = (stream) => !!stream && !!this.trackedStreams?.has(stream);
9807
9833
  handleOnTrack = (e) => {
9808
9834
  const { streams, track } = e;
9809
9835
  const [primaryStream] = streams;
@@ -9893,6 +9919,8 @@ class Subscriber extends BasePeerConnection {
9893
9919
  const target = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
9894
9920
  if (!target)
9895
9921
  return;
9922
+ if (target.isLocalParticipant)
9923
+ return;
9896
9924
  this.state.updateParticipant(target.sessionId, (p) => {
9897
9925
  const current = p.interruptedTracks ?? [];
9898
9926
  const has = current.includes(trackType);
@@ -10635,7 +10663,6 @@ const watchCallRejected = (call) => {
10635
10663
  */
10636
10664
  const watchCallEnded = (call) => {
10637
10665
  return function onCallEnded() {
10638
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
10639
10666
  const { callingState } = call.state;
10640
10667
  if (callingState !== CallingState.IDLE &&
10641
10668
  callingState !== CallingState.LEFT) {
@@ -10644,9 +10671,13 @@ const watchCallEnded = (call) => {
10644
10671
  reason: 'call.ended event received',
10645
10672
  });
10646
10673
  call
10647
- .leave({ message: 'call.ended event received', reject: false })
10674
+ .leave({
10675
+ message: 'call.ended event received',
10676
+ reject: false,
10677
+ reason: 'ended',
10678
+ })
10648
10679
  .catch((err) => {
10649
- call.logger.error('Failed to leave call after call.ended ', err);
10680
+ call.logger.error('Failed to leave call after call.ended', err);
10650
10681
  });
10651
10682
  }
10652
10683
  };
@@ -13452,7 +13483,7 @@ class DeviceManager {
13452
13483
  createSyntheticDevice(selectedDevice, deviceKind);
13453
13484
  writePreferences(currentDevice, deviceKey, muted, storageKey);
13454
13485
  }
13455
- async applyPersistedPreferences(enabledInCallType) {
13486
+ async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
13456
13487
  const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
13457
13488
  const preferences = readPreferences(this.devicePersistence.storageKey);
13458
13489
  const preferenceList = toPreferenceList(preferences[deviceKey]);
@@ -13479,13 +13510,13 @@ class DeviceManager {
13479
13510
  }
13480
13511
  const canPublish = this.call.permissionsContext.canPublish(this.trackType);
13481
13512
  if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
13482
- await this.applyMutedState(muted);
13513
+ await this.applyMutedState(muted, forceDisabled);
13483
13514
  appliedMute = true;
13484
13515
  }
13485
13516
  return appliedDevice || appliedMute;
13486
13517
  }
13487
- async applyMutedState(muted) {
13488
- if (this.state.status !== undefined)
13518
+ async applyMutedState(muted, forceDisabled) {
13519
+ if (forceDisabled || this.state.status !== undefined)
13489
13520
  return;
13490
13521
  if (muted) {
13491
13522
  await this.disable();
@@ -13837,8 +13868,9 @@ class CameraManager extends DeviceManager {
13837
13868
  *
13838
13869
  * @param settings the video settings to apply.
13839
13870
  * @param publish whether to publish the stream after applying the settings.
13871
+ * @param forceDisabled whether to keep the camera disabled, regardless of server-side or local preferences.
13840
13872
  */
13841
- async apply(settings, publish) {
13873
+ async apply(settings, publish, forceDisabled = false) {
13842
13874
  // Wait for any in progress camera operation
13843
13875
  await this.statusChangeSettled();
13844
13876
  await this.selectTargetResolution(settings.target_resolution);
@@ -13850,8 +13882,7 @@ class CameraManager extends DeviceManager {
13850
13882
  if (shouldApplyDefaults &&
13851
13883
  this.devicePersistence.enabled &&
13852
13884
  permissionState === 'granted') {
13853
- persistedPreferencesApplied =
13854
- await this.applyPersistedPreferences(enabledInCallType);
13885
+ persistedPreferencesApplied = await this.applyPersistedPreferences(enabledInCallType, forceDisabled);
13855
13886
  }
13856
13887
  // apply a direction and enable the camera only if in "pristine" state,
13857
13888
  // and there are no persisted preferences
@@ -13861,7 +13892,10 @@ class CameraManager extends DeviceManager {
13861
13892
  const direction = settings.camera_facing === 'front' ? 'front' : 'back';
13862
13893
  await this.selectDirection(direction, { enableCamera: false });
13863
13894
  }
13864
- if (canPublish && settings.camera_default_on && enabledInCallType) {
13895
+ if (!forceDisabled &&
13896
+ canPublish &&
13897
+ settings.camera_default_on &&
13898
+ enabledInCallType) {
13865
13899
  await this.enable();
13866
13900
  }
13867
13901
  }
@@ -14456,8 +14490,9 @@ class MicrophoneManager extends AudioDeviceManager {
14456
14490
  * Applies the audio settings to the microphone.
14457
14491
  * @param settings the audio settings to apply.
14458
14492
  * @param publish whether to publish the stream after applying the settings.
14493
+ * @param forceDisabled whether to keep the mic disabled, regardless of server-side or local preferences.
14459
14494
  */
14460
- async apply(settings, publish) {
14495
+ async apply(settings, publish, forceDisabled = false) {
14461
14496
  // Wait for any in progress mic operation
14462
14497
  await this.statusChangeSettled();
14463
14498
  // apply server-side settings only when the device state is pristine
@@ -14469,11 +14504,11 @@ class MicrophoneManager extends AudioDeviceManager {
14469
14504
  if (shouldApplyDefaults &&
14470
14505
  this.devicePersistence.enabled &&
14471
14506
  permissionState === 'granted') {
14472
- persistedPreferencesApplied = await this.applyPersistedPreferences(true);
14507
+ persistedPreferencesApplied = await this.applyPersistedPreferences(true, forceDisabled);
14473
14508
  }
14474
14509
  const canPublish = this.call.permissionsContext.canPublish(this.trackType);
14475
14510
  if (shouldApplyDefaults && !persistedPreferencesApplied) {
14476
- if (canPublish && settings.mic_default_on) {
14511
+ if (!forceDisabled && canPublish && settings.mic_default_on) {
14477
14512
  await this.enable();
14478
14513
  }
14479
14514
  }
@@ -15442,7 +15477,7 @@ class Call {
15442
15477
  }
15443
15478
  }
15444
15479
  }
15445
- globalThis.streamRNVideoSDK?.callingX?.endCall(this);
15480
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, reason === 'ended' ? 'remote' : undefined);
15446
15481
  this.statsReporter?.stop();
15447
15482
  this.statsReporter = undefined;
15448
15483
  const leaveReason = message ?? reason ?? 'user is leaving the call';
@@ -15605,8 +15640,18 @@ class Call {
15605
15640
  // const calls = useCalls().filter((c) => c.ringing);
15606
15641
  const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
15607
15642
  this.clientStore.setCalls([this, ...calls]);
15608
- const skipSpeakerApply = isReactNative();
15609
- await this.applyDeviceConfig(settings, false, skipSpeakerApply);
15643
+ await this.applyDeviceConfig(settings, { publish: false });
15644
+ };
15645
+ /**
15646
+ * Applies a backend-provided call state snapshot
15647
+ * (call, members, own capabilities) to this instance.
15648
+ *
15649
+ * @internal an internal method and should not be used outside the SDK.
15650
+ */
15651
+ updateFromCallStateResponse = (response) => {
15652
+ this.state.updateFromCallResponse(response.call);
15653
+ this.state.setMembers(response.members);
15654
+ this.state.setOwnCapabilities(response.own_capabilities);
15610
15655
  };
15611
15656
  /**
15612
15657
  * Loads the information about the call.
@@ -15619,9 +15664,7 @@ class Call {
15619
15664
  get = async (params) => {
15620
15665
  await this.setup();
15621
15666
  const response = await this.streamClient.get(this.streamClientBasePath, params);
15622
- this.state.updateFromCallResponse(response.call);
15623
- this.state.setMembers(response.members);
15624
- this.state.setOwnCapabilities(response.own_capabilities);
15667
+ this.updateFromCallStateResponse(response);
15625
15668
  if (params?.ring) {
15626
15669
  this.ringingSubject.next(true);
15627
15670
  }
@@ -15629,9 +15672,7 @@ class Call {
15629
15672
  this.watching = true;
15630
15673
  this.clientStore.registerOrUpdateCall(this);
15631
15674
  }
15632
- // Skip speaker setup on RN if ringing was requested or the call is already ringing
15633
- const skipSpeakerApply = isReactNative();
15634
- await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
15675
+ await this.applyDeviceConfig(response.call.settings, { publish: false });
15635
15676
  return response;
15636
15677
  };
15637
15678
  /**
@@ -15642,9 +15683,7 @@ class Call {
15642
15683
  getOrCreate = async (data) => {
15643
15684
  await this.setup();
15644
15685
  const response = await this.streamClient.post(this.streamClientBasePath, data);
15645
- this.state.updateFromCallResponse(response.call);
15646
- this.state.setMembers(response.members);
15647
- this.state.setOwnCapabilities(response.own_capabilities);
15686
+ this.updateFromCallStateResponse(response);
15648
15687
  if (data?.ring) {
15649
15688
  this.ringingSubject.next(true);
15650
15689
  }
@@ -15652,9 +15691,7 @@ class Call {
15652
15691
  this.watching = true;
15653
15692
  this.clientStore.registerOrUpdateCall(this);
15654
15693
  }
15655
- // Skip speaker setup on RN if ringing was requested or the call is already ringing
15656
- const skipSpeakerApply = isReactNative();
15657
- await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
15694
+ await this.applyDeviceConfig(response.call.settings, { publish: false });
15658
15695
  return response;
15659
15696
  };
15660
15697
  /**
@@ -15718,7 +15755,7 @@ class Call {
15718
15755
  *
15719
15756
  * @returns a promise which resolves once the call join-flow has finished.
15720
15757
  */
15721
- join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
15758
+ join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
15722
15759
  const callingState = this.state.callingState;
15723
15760
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
15724
15761
  throw new Error(`Illegal State: call.join() shall be called only once`);
@@ -15740,7 +15777,6 @@ class Call {
15740
15777
  callId: this.id,
15741
15778
  getCallSessionId: () => this.state.session?.id ?? '',
15742
15779
  getSfuId: () => this.credentials?.server.edge_name ?? '',
15743
- getUserSessionId: () => this.sfuClient?.sessionId ?? '',
15744
15780
  });
15745
15781
  this.joinResponseTimeout = joinResponseTimeout;
15746
15782
  this.rpcRequestTimeout = rpcRequestTimeout;
@@ -15792,7 +15828,7 @@ class Call {
15792
15828
  callingX?.endCall(this, 'error');
15793
15829
  throw error;
15794
15830
  }
15795
- };
15831
+ });
15796
15832
  /**
15797
15833
  * Will make a single attempt to watch for call related WebSocket events
15798
15834
  * and initiate a call session with the server.
@@ -15964,7 +16000,10 @@ class Call {
15964
16000
  if (!this.deviceSettingsAppliedOnce &&
15965
16001
  this.state.settings &&
15966
16002
  !supersededByLeave()) {
15967
- await this.applyDeviceConfig(this.state.settings, true, false);
16003
+ await this.applyDeviceConfig(this.state.settings, {
16004
+ publish: true,
16005
+ skipSpeakerApply: false,
16006
+ });
15968
16007
  this.deviceSettingsAppliedOnce = true;
15969
16008
  }
15970
16009
  if (!this.callManagerStarted && !supersededByLeave()) {
@@ -16754,12 +16793,16 @@ class Call {
16754
16793
  const streamStateProp = trackTypeToParticipantStreamKey(trackType);
16755
16794
  if (!streamStateProp)
16756
16795
  continue;
16757
- this.state.updateParticipant(sessionId, (p) => ({
16758
- publishedTracks: mediaStream
16759
- ? pushToIfMissing([...p.publishedTracks], trackType)
16760
- : p.publishedTracks.filter((t) => t !== trackType),
16761
- [streamStateProp]: mediaStream,
16762
- }));
16796
+ this.state.updateParticipant(sessionId, (p) => {
16797
+ const currentStream = p[streamStateProp];
16798
+ const isEchoedStream = this.subscriber?.isSelfSubscribedStream(currentStream);
16799
+ return {
16800
+ publishedTracks: mediaStream
16801
+ ? pushToIfMissing([...p.publishedTracks], trackType)
16802
+ : p.publishedTracks.filter((t) => t !== trackType),
16803
+ [streamStateProp]: isEchoedStream ? currentStream : mediaStream,
16804
+ };
16805
+ });
16763
16806
  }
16764
16807
  };
16765
16808
  /**
@@ -17449,18 +17492,21 @@ class Call {
17449
17492
  *
17450
17493
  * @internal
17451
17494
  */
17452
- applyDeviceConfig = async (settings, publish, skipSpeakerApply) => {
17495
+ applyDeviceConfig = async (settings, opts) => {
17496
+ const { publish,
17497
+ // Skip speaker setup on RN until joining (explicitly passed as false during join())
17498
+ skipSpeakerApply = isReactNative(), withDisabledDevices, } = opts;
17453
17499
  if (!skipSpeakerApply) {
17454
- await this.speaker.apply(settings).catch((err) => {
17455
- this.logger.warn('Speaker init failed', err);
17456
- });
17500
+ await this.speaker
17501
+ .apply(settings)
17502
+ .catch((err) => this.logger.warn('Speaker init failed', err));
17457
17503
  }
17458
- await this.camera.apply(settings.video, publish).catch((err) => {
17459
- this.logger.warn('Camera init failed', err);
17460
- });
17461
- await this.microphone.apply(settings.audio, publish).catch((err) => {
17462
- this.logger.warn('Mic init failed', err);
17463
- });
17504
+ await this.camera
17505
+ .apply(settings.video, publish, withDisabledDevices)
17506
+ .catch((err) => this.logger.warn('Camera init failed', err));
17507
+ await this.microphone
17508
+ .apply(settings.audio, publish, withDisabledDevices)
17509
+ .catch((err) => this.logger.warn('Mic init failed', err));
17464
17510
  };
17465
17511
  /**
17466
17512
  * Will begin tracking the given element for visibility changes within the
@@ -18970,11 +19016,11 @@ class StreamClient {
18970
19016
  return await this.wsConnection.connect(this.defaultWSTimeout);
18971
19017
  };
18972
19018
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18973
- "1.58.0";
19019
+ "1.59.0";
18974
19020
  getUserAgent = () => {
18975
19021
  if (!this.cachedUserAgent) {
18976
19022
  const { clientAppIdentifier = {} } = this.options;
18977
- const { sdkName = 'js', sdkVersion = "1.58.0", ...extras } = clientAppIdentifier;
19023
+ const { sdkName = 'js', sdkVersion = "1.59.0", ...extras } = clientAppIdentifier;
18978
19024
  this.cachedUserAgent = [
18979
19025
  `stream-video-${sdkName}-v${sdkVersion}`,
18980
19026
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -19337,7 +19383,6 @@ class ClientEventReporter {
19337
19383
  user_id: this.streamClient.userID || this.coordinatorConnectUserId,
19338
19384
  type: ctx?.callType,
19339
19385
  id: ctx?.callId,
19340
- call_cid: cid,
19341
19386
  stage: 'JoinInitiated',
19342
19387
  join_attempt_id: joinAttemptId,
19343
19388
  ...(coordinatorConnectId && {
@@ -19519,7 +19564,6 @@ class ClientEventReporter {
19519
19564
  startedAt: Date.now(),
19520
19565
  joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
19521
19566
  sfuId: this.getSfuId(cid),
19522
- userSessionId: this.getUserSessionId(cid),
19523
19567
  wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
19524
19568
  lastIceState: iceConnectionState,
19525
19569
  };
@@ -19530,9 +19574,6 @@ class ClientEventReporter {
19530
19574
  peer_connection: role,
19531
19575
  was_previously_connected: pair.wasPreviouslyConnected,
19532
19576
  ...(pair.sfuId && { sfu_id: pair.sfuId }),
19533
- ...(pair.userSessionId && {
19534
- user_session_id: pair.userSessionId,
19535
- }),
19536
19577
  event_type: 'initiated',
19537
19578
  });
19538
19579
  };
@@ -19547,9 +19588,6 @@ class ClientEventReporter {
19547
19588
  peer_connection: role,
19548
19589
  was_previously_connected: pair.wasPreviouslyConnected,
19549
19590
  ...(pair.sfuId && { sfu_id: pair.sfuId }),
19550
- ...(pair.userSessionId && {
19551
- user_session_id: pair.userSessionId,
19552
- }),
19553
19591
  event_type: 'completed',
19554
19592
  outcome: 'success',
19555
19593
  retry_count_attempt: 0,
@@ -19567,9 +19605,6 @@ class ClientEventReporter {
19567
19605
  ...this.sessionIdField(cid),
19568
19606
  peer_connection: role,
19569
19607
  was_previously_connected: pair.wasPreviouslyConnected,
19570
- ...(pair.userSessionId && {
19571
- user_session_id: pair.userSessionId,
19572
- }),
19573
19608
  ...(pair.sfuId && { sfu_id: pair.sfuId }),
19574
19609
  event_type: 'completed',
19575
19610
  outcome: 'failure',
@@ -19582,7 +19617,6 @@ class ClientEventReporter {
19582
19617
  this.peerConnectionPairs.delete(key);
19583
19618
  };
19584
19619
  getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
19585
- getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
19586
19620
  sessionIdField = (cid) => {
19587
19621
  const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
19588
19622
  return callSessionId ? { call_session_id: callSessionId } : {};
@@ -19594,7 +19628,6 @@ class ClientEventReporter {
19594
19628
  user_id: this.streamClient.userID || this.coordinatorConnectUserId,
19595
19629
  type: ctx?.callType ?? '',
19596
19630
  id: ctx?.callId ?? '',
19597
- call_cid: cid,
19598
19631
  stage,
19599
19632
  stage_id: pair.sid,
19600
19633
  ...(pair.joinAttemptIdSnapshot && {
@@ -19845,6 +19878,7 @@ class StreamVideoClient {
19845
19878
  }
19846
19879
  else {
19847
19880
  await call.updateFromRingingEvent(e);
19881
+ await call.get();
19848
19882
  }
19849
19883
  }
19850
19884
  else {
@@ -19883,6 +19917,13 @@ class StreamVideoClient {
19883
19917
  this.logger.error(`Failed to init call from event ${e.type}`, err);
19884
19918
  }
19885
19919
  };
19920
+ /**
19921
+ * Queries the API for calls matching the given filters.
19922
+ * @param data the query data.
19923
+ */
19924
+ doQueryCalls = (data) => {
19925
+ return this.streamClient.post('/calls', data);
19926
+ };
19886
19927
  /**
19887
19928
  * Rewatches the given calls with retry logic.
19888
19929
  * @param callsToReWatch array of call IDs to rewatch
@@ -19893,10 +19934,16 @@ class StreamVideoClient {
19893
19934
  for (let attempt = 0; attempt < maxRetries; attempt++) {
19894
19935
  try {
19895
19936
  this.logger.info(`Rewatching calls ${callsToReWatch.join(', ')} attempt ${attempt + 1}`);
19896
- await this.queryCalls({
19937
+ const response = await this.doQueryCalls({
19897
19938
  watch: true,
19898
19939
  filter_conditions: { cid: { $in: callsToReWatch } },
19899
19940
  });
19941
+ for (const c of response.calls) {
19942
+ const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
19943
+ if (call) {
19944
+ call.updateFromCallStateResponse(c);
19945
+ }
19946
+ }
19900
19947
  return;
19901
19948
  }
19902
19949
  catch (err) {
@@ -20039,9 +20086,11 @@ class StreamVideoClient {
20039
20086
  * Will query the API for calls matching the given filters.
20040
20087
  *
20041
20088
  * @param data the query data.
20089
+ * @param opts additional options, for tweaking the API behavior.
20042
20090
  */
20043
- queryCalls = async (data = {}) => {
20044
- const response = await this.streamClient.post('/calls', data);
20091
+ queryCalls = async (data = {}, opts = {}) => {
20092
+ const { withDisabledDevices = true } = opts;
20093
+ const response = await this.doQueryCalls(data);
20045
20094
  const calls = [];
20046
20095
  for (const c of response.calls) {
20047
20096
  const call = new Call({
@@ -20055,7 +20104,10 @@ class StreamVideoClient {
20055
20104
  clientStore: this.writeableStateStore,
20056
20105
  });
20057
20106
  call.state.updateFromCallResponse(c.call);
20058
- await call.applyDeviceConfig(c.call.settings, false, isReactNative());
20107
+ await call.applyDeviceConfig(c.call.settings, {
20108
+ publish: false,
20109
+ withDisabledDevices,
20110
+ });
20059
20111
  if (data.watch) {
20060
20112
  await call.setup();
20061
20113
  this.writeableStateStore.registerCall(call);
@@ -20207,6 +20259,202 @@ const humanize = (n) => {
20207
20259
  return String(n);
20208
20260
  };
20209
20261
 
20262
+ /** Default loopback stream wait timeout. */
20263
+ const LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = 10 * 1000;
20264
+ /** Default loopback recording duration. */
20265
+ const DEFAULT_LOOPBACK_RECORDING_DURATION_MS = 10 * 1000;
20266
+ /** Shortest permitted loopback recording duration. */
20267
+ const MIN_LOOPBACK_RECORDING_DURATION_MS = 5 * 1000;
20268
+ /** Longest permitted loopback recording duration. */
20269
+ const MAX_LOOPBACK_RECORDING_DURATION_MS = 2 * 60 * 1000;
20270
+ /** Thrown when loopback tracks do not arrive in time. */
20271
+ class LoopbackStreamsTimeoutError extends Error {
20272
+ constructor() {
20273
+ super('Timed out waiting for loopback streams. Ensure the call was joined ' +
20274
+ 'with `allowOwnTracksLoopback: true` and that the SFU is configured ' +
20275
+ 'to echo self-sub tracks.');
20276
+ this.name = 'LoopbackStreamsTimeoutError';
20277
+ }
20278
+ }
20279
+ /**
20280
+ * Returns the streams on the given participant that the SFU echoed back to us,
20281
+ * ignoring the local capture streams the publishing path writes to the same
20282
+ * fields.
20283
+ */
20284
+ const getLoopbackStreams = (call, participant) => {
20285
+ const { subscriber } = call ?? {};
20286
+ const { audioStream, videoStream } = participant ?? {};
20287
+ return {
20288
+ loopbackVideoStream: subscriber?.isSelfSubscribedStream(videoStream)
20289
+ ? videoStream
20290
+ : undefined,
20291
+ loopbackAudioStream: subscriber?.isSelfSubscribedStream(audioStream)
20292
+ ? audioStream
20293
+ : undefined,
20294
+ };
20295
+ };
20296
+ /**
20297
+ * Returns loopback tracks when all required tracks are present.
20298
+ *
20299
+ * Prefer {@link waitForLoopbackStreams}, which observes call state until the
20300
+ * tracks arrive. This is the synchronous snapshot it is built from.
20301
+ *
20302
+ * @internal
20303
+ */
20304
+ const getLoopbackTracks = (call, participant, includeVideo) => {
20305
+ if (!participant)
20306
+ return undefined;
20307
+ const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreams(call, participant);
20308
+ const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
20309
+ const videoTrack = includeVideo
20310
+ ? loopbackVideoStream?.getVideoTracks()[0]
20311
+ : undefined;
20312
+ if (!audioTrack || (includeVideo && !videoTrack)) {
20313
+ return undefined;
20314
+ }
20315
+ return { audioTrack, videoTrack };
20316
+ };
20317
+ /** Waits for the requested loopback tracks. */
20318
+ const waitForLoopbackStreams = (call, opts) => {
20319
+ const callState = call.state;
20320
+ const { includeVideo, signal, timeoutMs = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, } = opts;
20321
+ return new Promise((resolve, reject) => {
20322
+ const initial = getLoopbackTracks(call, callState.localParticipant, includeVideo);
20323
+ if (initial) {
20324
+ resolve(initial);
20325
+ return;
20326
+ }
20327
+ if (signal.aborted) {
20328
+ resolve(null);
20329
+ return;
20330
+ }
20331
+ let settled = false;
20332
+ const subscriptions = new Subscription();
20333
+ const cleanup = () => {
20334
+ if (settled)
20335
+ return;
20336
+ settled = true;
20337
+ clearTimeout(timeoutId);
20338
+ signal.removeEventListener('abort', onAbort);
20339
+ subscriptions.unsubscribe();
20340
+ };
20341
+ const onAbort = () => {
20342
+ cleanup();
20343
+ resolve(null);
20344
+ };
20345
+ const timeoutId = setTimeout(() => {
20346
+ cleanup();
20347
+ reject(new LoopbackStreamsTimeoutError());
20348
+ }, timeoutMs);
20349
+ signal.addEventListener('abort', onAbort);
20350
+ subscriptions.add(callState.localParticipant$.subscribe((participant) => {
20351
+ const ready = getLoopbackTracks(call, participant, includeVideo);
20352
+ if (ready) {
20353
+ cleanup();
20354
+ resolve(ready);
20355
+ }
20356
+ }));
20357
+ });
20358
+ };
20359
+ /** Enables loopback audio while `fn` runs, then restores it. */
20360
+ const withLoopbackAudioEnabled = async (tracks, fn) => {
20361
+ const restore = enableLoopbackAudio(tracks);
20362
+ try {
20363
+ return await fn();
20364
+ }
20365
+ finally {
20366
+ restore();
20367
+ }
20368
+ };
20369
+ /** Enables loopback audio and returns a restore function. */
20370
+ const enableLoopbackAudio = (tracks) => {
20371
+ const { audioTrack } = tracks;
20372
+ const previous = audioTrack?.enabled ?? false;
20373
+ if (audioTrack) {
20374
+ audioTrack.enabled = true;
20375
+ }
20376
+ return () => {
20377
+ if (audioTrack) {
20378
+ audioTrack.enabled = previous;
20379
+ }
20380
+ };
20381
+ };
20382
+ /** Clamps and rounds a loopback recording duration. */
20383
+ const clampLoopbackRecordingDuration = (durationMs) => Math.round(Math.min(MAX_LOOPBACK_RECORDING_DURATION_MS, Math.max(MIN_LOOPBACK_RECORDING_DURATION_MS, durationMs)));
20384
+
20385
+ /** Single-use `MediaRecorder` wrapper that returns an in-memory blob. */
20386
+ class MediaStreamRecorder {
20387
+ recording = promiseWithResolvers();
20388
+ logger = videoLoggerSystem.getLogger('MediaStreamRecorder');
20389
+ chunks = [];
20390
+ timeoutId;
20391
+ recorder;
20392
+ tracks;
20393
+ maxDurationMs;
20394
+ constructor(stream, options = {}) {
20395
+ if (typeof MediaRecorder === 'undefined') {
20396
+ throw new Error('MediaRecorder is not available in this environment');
20397
+ }
20398
+ this.tracks = stream.getTracks();
20399
+ if (this.tracks.length === 0) {
20400
+ throw new Error('cannot record a MediaStream that has no tracks');
20401
+ }
20402
+ this.maxDurationMs = options.maxDurationMs;
20403
+ this.recorder = new MediaRecorder(new MediaStream(this.tracks));
20404
+ this.recorder.ondataavailable = (event) => {
20405
+ if (event.data?.size)
20406
+ this.chunks.push(event.data);
20407
+ };
20408
+ this.recorder.onstop = this.handleStop;
20409
+ this.recorder.onerror = this.handleError;
20410
+ }
20411
+ /** Starts recording and resolves when recording stops. */
20412
+ start = () => {
20413
+ if (this.isSettled()) {
20414
+ throw new Error('MediaStreamRecorder is single-use: this instance has already ' +
20415
+ 'recorded. Construct a new one.');
20416
+ }
20417
+ if (this.recorder.state === 'inactive') {
20418
+ this.tracks.forEach((track) => track.addEventListener('ended', this.stop));
20419
+ if (this.maxDurationMs !== undefined) {
20420
+ this.timeoutId = setTimeout(this.stop, this.maxDurationMs);
20421
+ }
20422
+ this.recorder.start();
20423
+ }
20424
+ return this.recording.promise;
20425
+ };
20426
+ /** Stops recording early. */
20427
+ stop = () => {
20428
+ if (this.recorder.state !== 'inactive')
20429
+ this.recorder.stop();
20430
+ };
20431
+ isSettled = () => this.recording.isResolved() || this.recording.isRejected();
20432
+ detach = () => {
20433
+ clearTimeout(this.timeoutId);
20434
+ this.tracks.forEach((track) => track.removeEventListener('ended', this.stop));
20435
+ };
20436
+ handleStop = () => {
20437
+ this.detach();
20438
+ if (this.chunks.length === 0) {
20439
+ this.logger.warn('MediaRecorder recording produced no data');
20440
+ this.recording.resolve(null);
20441
+ return;
20442
+ }
20443
+ const mimeType = this.chunks[0].type || this.recorder.mimeType;
20444
+ this.recording.resolve({
20445
+ blob: new Blob(this.chunks, { type: mimeType }),
20446
+ });
20447
+ };
20448
+ handleError = (event) => {
20449
+ if (this.isSettled())
20450
+ return;
20451
+ this.detach();
20452
+ const error = event.error;
20453
+ this.logger.error('MediaRecorder error', error);
20454
+ this.recording.reject(error ?? new Error('MediaRecorder failed'));
20455
+ };
20456
+ }
20457
+
20210
20458
  /**
20211
20459
  * Tiny, type-safe event emitter.
20212
20460
  *
@@ -20525,5 +20773,5 @@ class EncryptionManager extends TypedEventEmitter {
20525
20773
  };
20526
20774
  }
20527
20775
 
20528
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
20776
+ export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
20529
20777
  //# sourceMappingURL=index.browser.es.js.map