@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
package/dist/index.es.js CHANGED
@@ -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
  import https from 'https';
@@ -5060,7 +5060,7 @@ const toIceCandidate = (iceTrickle) => {
5060
5060
  }
5061
5061
  };
5062
5062
 
5063
- const version = "1.58.0";
5063
+ const version = "1.59.0";
5064
5064
  const [major, minor, patch] = version.split('.');
5065
5065
  let sdkInfo = {
5066
5066
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5317,6 +5317,27 @@ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
5317
5317
  */
5318
5318
  const withCancellation = createRunner(wrapWithCancellation);
5319
5319
  const pendingPromises = new Map();
5320
+ /**
5321
+ * Wraps an async function so concurrent calls share the in-flight promise
5322
+ * instead of queueing another execution.
5323
+ *
5324
+ * @param cb Async function to wrap.
5325
+ * @returns Wrapped async function.
5326
+ */
5327
+ function singleFlight(cb) {
5328
+ let pending;
5329
+ return (...args) => {
5330
+ if (pending)
5331
+ return pending;
5332
+ const promise = cb(...args).finally(() => {
5333
+ if (pending === promise) {
5334
+ pending = undefined;
5335
+ }
5336
+ });
5337
+ pending = promise;
5338
+ return promise;
5339
+ };
5340
+ }
5320
5341
  function hasPending(tag) {
5321
5342
  return pendingPromises.has(tag);
5322
5343
  }
@@ -9805,6 +9826,11 @@ class Subscriber extends BasePeerConnection {
9805
9826
  throw e;
9806
9827
  }
9807
9828
  };
9829
+ /**
9830
+ * Whether the given stream is one the SFU echoed back to us
9831
+ * (`allowOwnTracksLoopback`) rather than a local capture stream.
9832
+ */
9833
+ isSelfSubscribedStream = (stream) => !!stream && !!this.trackedStreams?.has(stream);
9808
9834
  handleOnTrack = (e) => {
9809
9835
  const { streams, track } = e;
9810
9836
  const [primaryStream] = streams;
@@ -9894,6 +9920,8 @@ class Subscriber extends BasePeerConnection {
9894
9920
  const target = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
9895
9921
  if (!target)
9896
9922
  return;
9923
+ if (target.isLocalParticipant)
9924
+ return;
9897
9925
  this.state.updateParticipant(target.sessionId, (p) => {
9898
9926
  const current = p.interruptedTracks ?? [];
9899
9927
  const has = current.includes(trackType);
@@ -10636,7 +10664,6 @@ const watchCallRejected = (call) => {
10636
10664
  */
10637
10665
  const watchCallEnded = (call) => {
10638
10666
  return function onCallEnded() {
10639
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
10640
10667
  const { callingState } = call.state;
10641
10668
  if (callingState !== CallingState.IDLE &&
10642
10669
  callingState !== CallingState.LEFT) {
@@ -10645,9 +10672,13 @@ const watchCallEnded = (call) => {
10645
10672
  reason: 'call.ended event received',
10646
10673
  });
10647
10674
  call
10648
- .leave({ message: 'call.ended event received', reject: false })
10675
+ .leave({
10676
+ message: 'call.ended event received',
10677
+ reject: false,
10678
+ reason: 'ended',
10679
+ })
10649
10680
  .catch((err) => {
10650
- call.logger.error('Failed to leave call after call.ended ', err);
10681
+ call.logger.error('Failed to leave call after call.ended', err);
10651
10682
  });
10652
10683
  }
10653
10684
  };
@@ -13453,7 +13484,7 @@ class DeviceManager {
13453
13484
  createSyntheticDevice(selectedDevice, deviceKind);
13454
13485
  writePreferences(currentDevice, deviceKey, muted, storageKey);
13455
13486
  }
13456
- async applyPersistedPreferences(enabledInCallType) {
13487
+ async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
13457
13488
  const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
13458
13489
  const preferences = readPreferences(this.devicePersistence.storageKey);
13459
13490
  const preferenceList = toPreferenceList(preferences[deviceKey]);
@@ -13480,13 +13511,13 @@ class DeviceManager {
13480
13511
  }
13481
13512
  const canPublish = this.call.permissionsContext.canPublish(this.trackType);
13482
13513
  if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
13483
- await this.applyMutedState(muted);
13514
+ await this.applyMutedState(muted, forceDisabled);
13484
13515
  appliedMute = true;
13485
13516
  }
13486
13517
  return appliedDevice || appliedMute;
13487
13518
  }
13488
- async applyMutedState(muted) {
13489
- if (this.state.status !== undefined)
13519
+ async applyMutedState(muted, forceDisabled) {
13520
+ if (forceDisabled || this.state.status !== undefined)
13490
13521
  return;
13491
13522
  if (muted) {
13492
13523
  await this.disable();
@@ -13838,8 +13869,9 @@ class CameraManager extends DeviceManager {
13838
13869
  *
13839
13870
  * @param settings the video settings to apply.
13840
13871
  * @param publish whether to publish the stream after applying the settings.
13872
+ * @param forceDisabled whether to keep the camera disabled, regardless of server-side or local preferences.
13841
13873
  */
13842
- async apply(settings, publish) {
13874
+ async apply(settings, publish, forceDisabled = false) {
13843
13875
  // Wait for any in progress camera operation
13844
13876
  await this.statusChangeSettled();
13845
13877
  await this.selectTargetResolution(settings.target_resolution);
@@ -13851,8 +13883,7 @@ class CameraManager extends DeviceManager {
13851
13883
  if (shouldApplyDefaults &&
13852
13884
  this.devicePersistence.enabled &&
13853
13885
  permissionState === 'granted') {
13854
- persistedPreferencesApplied =
13855
- await this.applyPersistedPreferences(enabledInCallType);
13886
+ persistedPreferencesApplied = await this.applyPersistedPreferences(enabledInCallType, forceDisabled);
13856
13887
  }
13857
13888
  // apply a direction and enable the camera only if in "pristine" state,
13858
13889
  // and there are no persisted preferences
@@ -13862,7 +13893,10 @@ class CameraManager extends DeviceManager {
13862
13893
  const direction = settings.camera_facing === 'front' ? 'front' : 'back';
13863
13894
  await this.selectDirection(direction, { enableCamera: false });
13864
13895
  }
13865
- if (canPublish && settings.camera_default_on && enabledInCallType) {
13896
+ if (!forceDisabled &&
13897
+ canPublish &&
13898
+ settings.camera_default_on &&
13899
+ enabledInCallType) {
13866
13900
  await this.enable();
13867
13901
  }
13868
13902
  }
@@ -14457,8 +14491,9 @@ class MicrophoneManager extends AudioDeviceManager {
14457
14491
  * Applies the audio settings to the microphone.
14458
14492
  * @param settings the audio settings to apply.
14459
14493
  * @param publish whether to publish the stream after applying the settings.
14494
+ * @param forceDisabled whether to keep the mic disabled, regardless of server-side or local preferences.
14460
14495
  */
14461
- async apply(settings, publish) {
14496
+ async apply(settings, publish, forceDisabled = false) {
14462
14497
  // Wait for any in progress mic operation
14463
14498
  await this.statusChangeSettled();
14464
14499
  // apply server-side settings only when the device state is pristine
@@ -14470,11 +14505,11 @@ class MicrophoneManager extends AudioDeviceManager {
14470
14505
  if (shouldApplyDefaults &&
14471
14506
  this.devicePersistence.enabled &&
14472
14507
  permissionState === 'granted') {
14473
- persistedPreferencesApplied = await this.applyPersistedPreferences(true);
14508
+ persistedPreferencesApplied = await this.applyPersistedPreferences(true, forceDisabled);
14474
14509
  }
14475
14510
  const canPublish = this.call.permissionsContext.canPublish(this.trackType);
14476
14511
  if (shouldApplyDefaults && !persistedPreferencesApplied) {
14477
- if (canPublish && settings.mic_default_on) {
14512
+ if (!forceDisabled && canPublish && settings.mic_default_on) {
14478
14513
  await this.enable();
14479
14514
  }
14480
14515
  }
@@ -15443,7 +15478,7 @@ class Call {
15443
15478
  }
15444
15479
  }
15445
15480
  }
15446
- globalThis.streamRNVideoSDK?.callingX?.endCall(this);
15481
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, reason === 'ended' ? 'remote' : undefined);
15447
15482
  this.statsReporter?.stop();
15448
15483
  this.statsReporter = undefined;
15449
15484
  const leaveReason = message ?? reason ?? 'user is leaving the call';
@@ -15606,8 +15641,18 @@ class Call {
15606
15641
  // const calls = useCalls().filter((c) => c.ringing);
15607
15642
  const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
15608
15643
  this.clientStore.setCalls([this, ...calls]);
15609
- const skipSpeakerApply = isReactNative();
15610
- await this.applyDeviceConfig(settings, false, skipSpeakerApply);
15644
+ await this.applyDeviceConfig(settings, { publish: false });
15645
+ };
15646
+ /**
15647
+ * Applies a backend-provided call state snapshot
15648
+ * (call, members, own capabilities) to this instance.
15649
+ *
15650
+ * @internal an internal method and should not be used outside the SDK.
15651
+ */
15652
+ updateFromCallStateResponse = (response) => {
15653
+ this.state.updateFromCallResponse(response.call);
15654
+ this.state.setMembers(response.members);
15655
+ this.state.setOwnCapabilities(response.own_capabilities);
15611
15656
  };
15612
15657
  /**
15613
15658
  * Loads the information about the call.
@@ -15620,9 +15665,7 @@ class Call {
15620
15665
  get = async (params) => {
15621
15666
  await this.setup();
15622
15667
  const response = await this.streamClient.get(this.streamClientBasePath, params);
15623
- this.state.updateFromCallResponse(response.call);
15624
- this.state.setMembers(response.members);
15625
- this.state.setOwnCapabilities(response.own_capabilities);
15668
+ this.updateFromCallStateResponse(response);
15626
15669
  if (params?.ring) {
15627
15670
  this.ringingSubject.next(true);
15628
15671
  }
@@ -15630,9 +15673,7 @@ class Call {
15630
15673
  this.watching = true;
15631
15674
  this.clientStore.registerOrUpdateCall(this);
15632
15675
  }
15633
- // Skip speaker setup on RN if ringing was requested or the call is already ringing
15634
- const skipSpeakerApply = isReactNative();
15635
- await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
15676
+ await this.applyDeviceConfig(response.call.settings, { publish: false });
15636
15677
  return response;
15637
15678
  };
15638
15679
  /**
@@ -15643,9 +15684,7 @@ class Call {
15643
15684
  getOrCreate = async (data) => {
15644
15685
  await this.setup();
15645
15686
  const response = await this.streamClient.post(this.streamClientBasePath, data);
15646
- this.state.updateFromCallResponse(response.call);
15647
- this.state.setMembers(response.members);
15648
- this.state.setOwnCapabilities(response.own_capabilities);
15687
+ this.updateFromCallStateResponse(response);
15649
15688
  if (data?.ring) {
15650
15689
  this.ringingSubject.next(true);
15651
15690
  }
@@ -15653,9 +15692,7 @@ class Call {
15653
15692
  this.watching = true;
15654
15693
  this.clientStore.registerOrUpdateCall(this);
15655
15694
  }
15656
- // Skip speaker setup on RN if ringing was requested or the call is already ringing
15657
- const skipSpeakerApply = isReactNative();
15658
- await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
15695
+ await this.applyDeviceConfig(response.call.settings, { publish: false });
15659
15696
  return response;
15660
15697
  };
15661
15698
  /**
@@ -15719,7 +15756,7 @@ class Call {
15719
15756
  *
15720
15757
  * @returns a promise which resolves once the call join-flow has finished.
15721
15758
  */
15722
- join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
15759
+ join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
15723
15760
  const callingState = this.state.callingState;
15724
15761
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
15725
15762
  throw new Error(`Illegal State: call.join() shall be called only once`);
@@ -15741,7 +15778,6 @@ class Call {
15741
15778
  callId: this.id,
15742
15779
  getCallSessionId: () => this.state.session?.id ?? '',
15743
15780
  getSfuId: () => this.credentials?.server.edge_name ?? '',
15744
- getUserSessionId: () => this.sfuClient?.sessionId ?? '',
15745
15781
  });
15746
15782
  this.joinResponseTimeout = joinResponseTimeout;
15747
15783
  this.rpcRequestTimeout = rpcRequestTimeout;
@@ -15793,7 +15829,7 @@ class Call {
15793
15829
  callingX?.endCall(this, 'error');
15794
15830
  throw error;
15795
15831
  }
15796
- };
15832
+ });
15797
15833
  /**
15798
15834
  * Will make a single attempt to watch for call related WebSocket events
15799
15835
  * and initiate a call session with the server.
@@ -15965,7 +16001,10 @@ class Call {
15965
16001
  if (!this.deviceSettingsAppliedOnce &&
15966
16002
  this.state.settings &&
15967
16003
  !supersededByLeave()) {
15968
- await this.applyDeviceConfig(this.state.settings, true, false);
16004
+ await this.applyDeviceConfig(this.state.settings, {
16005
+ publish: true,
16006
+ skipSpeakerApply: false,
16007
+ });
15969
16008
  this.deviceSettingsAppliedOnce = true;
15970
16009
  }
15971
16010
  if (!this.callManagerStarted && !supersededByLeave()) {
@@ -16755,12 +16794,16 @@ class Call {
16755
16794
  const streamStateProp = trackTypeToParticipantStreamKey(trackType);
16756
16795
  if (!streamStateProp)
16757
16796
  continue;
16758
- this.state.updateParticipant(sessionId, (p) => ({
16759
- publishedTracks: mediaStream
16760
- ? pushToIfMissing([...p.publishedTracks], trackType)
16761
- : p.publishedTracks.filter((t) => t !== trackType),
16762
- [streamStateProp]: mediaStream,
16763
- }));
16797
+ this.state.updateParticipant(sessionId, (p) => {
16798
+ const currentStream = p[streamStateProp];
16799
+ const isEchoedStream = this.subscriber?.isSelfSubscribedStream(currentStream);
16800
+ return {
16801
+ publishedTracks: mediaStream
16802
+ ? pushToIfMissing([...p.publishedTracks], trackType)
16803
+ : p.publishedTracks.filter((t) => t !== trackType),
16804
+ [streamStateProp]: isEchoedStream ? currentStream : mediaStream,
16805
+ };
16806
+ });
16764
16807
  }
16765
16808
  };
16766
16809
  /**
@@ -17450,18 +17493,21 @@ class Call {
17450
17493
  *
17451
17494
  * @internal
17452
17495
  */
17453
- applyDeviceConfig = async (settings, publish, skipSpeakerApply) => {
17496
+ applyDeviceConfig = async (settings, opts) => {
17497
+ const { publish,
17498
+ // Skip speaker setup on RN until joining (explicitly passed as false during join())
17499
+ skipSpeakerApply = isReactNative(), withDisabledDevices, } = opts;
17454
17500
  if (!skipSpeakerApply) {
17455
- await this.speaker.apply(settings).catch((err) => {
17456
- this.logger.warn('Speaker init failed', err);
17457
- });
17501
+ await this.speaker
17502
+ .apply(settings)
17503
+ .catch((err) => this.logger.warn('Speaker init failed', err));
17458
17504
  }
17459
- await this.camera.apply(settings.video, publish).catch((err) => {
17460
- this.logger.warn('Camera init failed', err);
17461
- });
17462
- await this.microphone.apply(settings.audio, publish).catch((err) => {
17463
- this.logger.warn('Mic init failed', err);
17464
- });
17505
+ await this.camera
17506
+ .apply(settings.video, publish, withDisabledDevices)
17507
+ .catch((err) => this.logger.warn('Camera init failed', err));
17508
+ await this.microphone
17509
+ .apply(settings.audio, publish, withDisabledDevices)
17510
+ .catch((err) => this.logger.warn('Mic init failed', err));
17465
17511
  };
17466
17512
  /**
17467
17513
  * Will begin tracking the given element for visibility changes within the
@@ -18969,11 +19015,11 @@ class StreamClient {
18969
19015
  return await this.wsConnection.connect(this.defaultWSTimeout);
18970
19016
  };
18971
19017
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18972
- "1.58.0";
19018
+ "1.59.0";
18973
19019
  getUserAgent = () => {
18974
19020
  if (!this.cachedUserAgent) {
18975
19021
  const { clientAppIdentifier = {} } = this.options;
18976
- const { sdkName = 'js', sdkVersion = "1.58.0", ...extras } = clientAppIdentifier;
19022
+ const { sdkName = 'js', sdkVersion = "1.59.0", ...extras } = clientAppIdentifier;
18977
19023
  this.cachedUserAgent = [
18978
19024
  `stream-video-${sdkName}-v${sdkVersion}`,
18979
19025
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -19336,7 +19382,6 @@ class ClientEventReporter {
19336
19382
  user_id: this.streamClient.userID || this.coordinatorConnectUserId,
19337
19383
  type: ctx?.callType,
19338
19384
  id: ctx?.callId,
19339
- call_cid: cid,
19340
19385
  stage: 'JoinInitiated',
19341
19386
  join_attempt_id: joinAttemptId,
19342
19387
  ...(coordinatorConnectId && {
@@ -19518,7 +19563,6 @@ class ClientEventReporter {
19518
19563
  startedAt: Date.now(),
19519
19564
  joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
19520
19565
  sfuId: this.getSfuId(cid),
19521
- userSessionId: this.getUserSessionId(cid),
19522
19566
  wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
19523
19567
  lastIceState: iceConnectionState,
19524
19568
  };
@@ -19529,9 +19573,6 @@ class ClientEventReporter {
19529
19573
  peer_connection: role,
19530
19574
  was_previously_connected: pair.wasPreviouslyConnected,
19531
19575
  ...(pair.sfuId && { sfu_id: pair.sfuId }),
19532
- ...(pair.userSessionId && {
19533
- user_session_id: pair.userSessionId,
19534
- }),
19535
19576
  event_type: 'initiated',
19536
19577
  });
19537
19578
  };
@@ -19546,9 +19587,6 @@ class ClientEventReporter {
19546
19587
  peer_connection: role,
19547
19588
  was_previously_connected: pair.wasPreviouslyConnected,
19548
19589
  ...(pair.sfuId && { sfu_id: pair.sfuId }),
19549
- ...(pair.userSessionId && {
19550
- user_session_id: pair.userSessionId,
19551
- }),
19552
19590
  event_type: 'completed',
19553
19591
  outcome: 'success',
19554
19592
  retry_count_attempt: 0,
@@ -19566,9 +19604,6 @@ class ClientEventReporter {
19566
19604
  ...this.sessionIdField(cid),
19567
19605
  peer_connection: role,
19568
19606
  was_previously_connected: pair.wasPreviouslyConnected,
19569
- ...(pair.userSessionId && {
19570
- user_session_id: pair.userSessionId,
19571
- }),
19572
19607
  ...(pair.sfuId && { sfu_id: pair.sfuId }),
19573
19608
  event_type: 'completed',
19574
19609
  outcome: 'failure',
@@ -19581,7 +19616,6 @@ class ClientEventReporter {
19581
19616
  this.peerConnectionPairs.delete(key);
19582
19617
  };
19583
19618
  getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
19584
- getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
19585
19619
  sessionIdField = (cid) => {
19586
19620
  const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
19587
19621
  return callSessionId ? { call_session_id: callSessionId } : {};
@@ -19593,7 +19627,6 @@ class ClientEventReporter {
19593
19627
  user_id: this.streamClient.userID || this.coordinatorConnectUserId,
19594
19628
  type: ctx?.callType ?? '',
19595
19629
  id: ctx?.callId ?? '',
19596
- call_cid: cid,
19597
19630
  stage,
19598
19631
  stage_id: pair.sid,
19599
19632
  ...(pair.joinAttemptIdSnapshot && {
@@ -19844,6 +19877,7 @@ class StreamVideoClient {
19844
19877
  }
19845
19878
  else {
19846
19879
  await call.updateFromRingingEvent(e);
19880
+ await call.get();
19847
19881
  }
19848
19882
  }
19849
19883
  else {
@@ -19882,6 +19916,13 @@ class StreamVideoClient {
19882
19916
  this.logger.error(`Failed to init call from event ${e.type}`, err);
19883
19917
  }
19884
19918
  };
19919
+ /**
19920
+ * Queries the API for calls matching the given filters.
19921
+ * @param data the query data.
19922
+ */
19923
+ doQueryCalls = (data) => {
19924
+ return this.streamClient.post('/calls', data);
19925
+ };
19885
19926
  /**
19886
19927
  * Rewatches the given calls with retry logic.
19887
19928
  * @param callsToReWatch array of call IDs to rewatch
@@ -19892,10 +19933,16 @@ class StreamVideoClient {
19892
19933
  for (let attempt = 0; attempt < maxRetries; attempt++) {
19893
19934
  try {
19894
19935
  this.logger.info(`Rewatching calls ${callsToReWatch.join(', ')} attempt ${attempt + 1}`);
19895
- await this.queryCalls({
19936
+ const response = await this.doQueryCalls({
19896
19937
  watch: true,
19897
19938
  filter_conditions: { cid: { $in: callsToReWatch } },
19898
19939
  });
19940
+ for (const c of response.calls) {
19941
+ const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
19942
+ if (call) {
19943
+ call.updateFromCallStateResponse(c);
19944
+ }
19945
+ }
19899
19946
  return;
19900
19947
  }
19901
19948
  catch (err) {
@@ -20038,9 +20085,11 @@ class StreamVideoClient {
20038
20085
  * Will query the API for calls matching the given filters.
20039
20086
  *
20040
20087
  * @param data the query data.
20088
+ * @param opts additional options, for tweaking the API behavior.
20041
20089
  */
20042
- queryCalls = async (data = {}) => {
20043
- const response = await this.streamClient.post('/calls', data);
20090
+ queryCalls = async (data = {}, opts = {}) => {
20091
+ const { withDisabledDevices = true } = opts;
20092
+ const response = await this.doQueryCalls(data);
20044
20093
  const calls = [];
20045
20094
  for (const c of response.calls) {
20046
20095
  const call = new Call({
@@ -20054,7 +20103,10 @@ class StreamVideoClient {
20054
20103
  clientStore: this.writeableStateStore,
20055
20104
  });
20056
20105
  call.state.updateFromCallResponse(c.call);
20057
- await call.applyDeviceConfig(c.call.settings, false, isReactNative());
20106
+ await call.applyDeviceConfig(c.call.settings, {
20107
+ publish: false,
20108
+ withDisabledDevices,
20109
+ });
20058
20110
  if (data.watch) {
20059
20111
  await call.setup();
20060
20112
  this.writeableStateStore.registerCall(call);
@@ -20206,6 +20258,202 @@ const humanize = (n) => {
20206
20258
  return String(n);
20207
20259
  };
20208
20260
 
20261
+ /** Default loopback stream wait timeout. */
20262
+ const LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = 10 * 1000;
20263
+ /** Default loopback recording duration. */
20264
+ const DEFAULT_LOOPBACK_RECORDING_DURATION_MS = 10 * 1000;
20265
+ /** Shortest permitted loopback recording duration. */
20266
+ const MIN_LOOPBACK_RECORDING_DURATION_MS = 5 * 1000;
20267
+ /** Longest permitted loopback recording duration. */
20268
+ const MAX_LOOPBACK_RECORDING_DURATION_MS = 2 * 60 * 1000;
20269
+ /** Thrown when loopback tracks do not arrive in time. */
20270
+ class LoopbackStreamsTimeoutError extends Error {
20271
+ constructor() {
20272
+ super('Timed out waiting for loopback streams. Ensure the call was joined ' +
20273
+ 'with `allowOwnTracksLoopback: true` and that the SFU is configured ' +
20274
+ 'to echo self-sub tracks.');
20275
+ this.name = 'LoopbackStreamsTimeoutError';
20276
+ }
20277
+ }
20278
+ /**
20279
+ * Returns the streams on the given participant that the SFU echoed back to us,
20280
+ * ignoring the local capture streams the publishing path writes to the same
20281
+ * fields.
20282
+ */
20283
+ const getLoopbackStreams = (call, participant) => {
20284
+ const { subscriber } = call ?? {};
20285
+ const { audioStream, videoStream } = participant ?? {};
20286
+ return {
20287
+ loopbackVideoStream: subscriber?.isSelfSubscribedStream(videoStream)
20288
+ ? videoStream
20289
+ : undefined,
20290
+ loopbackAudioStream: subscriber?.isSelfSubscribedStream(audioStream)
20291
+ ? audioStream
20292
+ : undefined,
20293
+ };
20294
+ };
20295
+ /**
20296
+ * Returns loopback tracks when all required tracks are present.
20297
+ *
20298
+ * Prefer {@link waitForLoopbackStreams}, which observes call state until the
20299
+ * tracks arrive. This is the synchronous snapshot it is built from.
20300
+ *
20301
+ * @internal
20302
+ */
20303
+ const getLoopbackTracks = (call, participant, includeVideo) => {
20304
+ if (!participant)
20305
+ return undefined;
20306
+ const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreams(call, participant);
20307
+ const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
20308
+ const videoTrack = includeVideo
20309
+ ? loopbackVideoStream?.getVideoTracks()[0]
20310
+ : undefined;
20311
+ if (!audioTrack || (includeVideo && !videoTrack)) {
20312
+ return undefined;
20313
+ }
20314
+ return { audioTrack, videoTrack };
20315
+ };
20316
+ /** Waits for the requested loopback tracks. */
20317
+ const waitForLoopbackStreams = (call, opts) => {
20318
+ const callState = call.state;
20319
+ const { includeVideo, signal, timeoutMs = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, } = opts;
20320
+ return new Promise((resolve, reject) => {
20321
+ const initial = getLoopbackTracks(call, callState.localParticipant, includeVideo);
20322
+ if (initial) {
20323
+ resolve(initial);
20324
+ return;
20325
+ }
20326
+ if (signal.aborted) {
20327
+ resolve(null);
20328
+ return;
20329
+ }
20330
+ let settled = false;
20331
+ const subscriptions = new Subscription();
20332
+ const cleanup = () => {
20333
+ if (settled)
20334
+ return;
20335
+ settled = true;
20336
+ clearTimeout(timeoutId);
20337
+ signal.removeEventListener('abort', onAbort);
20338
+ subscriptions.unsubscribe();
20339
+ };
20340
+ const onAbort = () => {
20341
+ cleanup();
20342
+ resolve(null);
20343
+ };
20344
+ const timeoutId = setTimeout(() => {
20345
+ cleanup();
20346
+ reject(new LoopbackStreamsTimeoutError());
20347
+ }, timeoutMs);
20348
+ signal.addEventListener('abort', onAbort);
20349
+ subscriptions.add(callState.localParticipant$.subscribe((participant) => {
20350
+ const ready = getLoopbackTracks(call, participant, includeVideo);
20351
+ if (ready) {
20352
+ cleanup();
20353
+ resolve(ready);
20354
+ }
20355
+ }));
20356
+ });
20357
+ };
20358
+ /** Enables loopback audio while `fn` runs, then restores it. */
20359
+ const withLoopbackAudioEnabled = async (tracks, fn) => {
20360
+ const restore = enableLoopbackAudio(tracks);
20361
+ try {
20362
+ return await fn();
20363
+ }
20364
+ finally {
20365
+ restore();
20366
+ }
20367
+ };
20368
+ /** Enables loopback audio and returns a restore function. */
20369
+ const enableLoopbackAudio = (tracks) => {
20370
+ const { audioTrack } = tracks;
20371
+ const previous = audioTrack?.enabled ?? false;
20372
+ if (audioTrack) {
20373
+ audioTrack.enabled = true;
20374
+ }
20375
+ return () => {
20376
+ if (audioTrack) {
20377
+ audioTrack.enabled = previous;
20378
+ }
20379
+ };
20380
+ };
20381
+ /** Clamps and rounds a loopback recording duration. */
20382
+ const clampLoopbackRecordingDuration = (durationMs) => Math.round(Math.min(MAX_LOOPBACK_RECORDING_DURATION_MS, Math.max(MIN_LOOPBACK_RECORDING_DURATION_MS, durationMs)));
20383
+
20384
+ /** Single-use `MediaRecorder` wrapper that returns an in-memory blob. */
20385
+ class MediaStreamRecorder {
20386
+ recording = promiseWithResolvers();
20387
+ logger = videoLoggerSystem.getLogger('MediaStreamRecorder');
20388
+ chunks = [];
20389
+ timeoutId;
20390
+ recorder;
20391
+ tracks;
20392
+ maxDurationMs;
20393
+ constructor(stream, options = {}) {
20394
+ if (typeof MediaRecorder === 'undefined') {
20395
+ throw new Error('MediaRecorder is not available in this environment');
20396
+ }
20397
+ this.tracks = stream.getTracks();
20398
+ if (this.tracks.length === 0) {
20399
+ throw new Error('cannot record a MediaStream that has no tracks');
20400
+ }
20401
+ this.maxDurationMs = options.maxDurationMs;
20402
+ this.recorder = new MediaRecorder(new MediaStream(this.tracks));
20403
+ this.recorder.ondataavailable = (event) => {
20404
+ if (event.data?.size)
20405
+ this.chunks.push(event.data);
20406
+ };
20407
+ this.recorder.onstop = this.handleStop;
20408
+ this.recorder.onerror = this.handleError;
20409
+ }
20410
+ /** Starts recording and resolves when recording stops. */
20411
+ start = () => {
20412
+ if (this.isSettled()) {
20413
+ throw new Error('MediaStreamRecorder is single-use: this instance has already ' +
20414
+ 'recorded. Construct a new one.');
20415
+ }
20416
+ if (this.recorder.state === 'inactive') {
20417
+ this.tracks.forEach((track) => track.addEventListener('ended', this.stop));
20418
+ if (this.maxDurationMs !== undefined) {
20419
+ this.timeoutId = setTimeout(this.stop, this.maxDurationMs);
20420
+ }
20421
+ this.recorder.start();
20422
+ }
20423
+ return this.recording.promise;
20424
+ };
20425
+ /** Stops recording early. */
20426
+ stop = () => {
20427
+ if (this.recorder.state !== 'inactive')
20428
+ this.recorder.stop();
20429
+ };
20430
+ isSettled = () => this.recording.isResolved() || this.recording.isRejected();
20431
+ detach = () => {
20432
+ clearTimeout(this.timeoutId);
20433
+ this.tracks.forEach((track) => track.removeEventListener('ended', this.stop));
20434
+ };
20435
+ handleStop = () => {
20436
+ this.detach();
20437
+ if (this.chunks.length === 0) {
20438
+ this.logger.warn('MediaRecorder recording produced no data');
20439
+ this.recording.resolve(null);
20440
+ return;
20441
+ }
20442
+ const mimeType = this.chunks[0].type || this.recorder.mimeType;
20443
+ this.recording.resolve({
20444
+ blob: new Blob(this.chunks, { type: mimeType }),
20445
+ });
20446
+ };
20447
+ handleError = (event) => {
20448
+ if (this.isSettled())
20449
+ return;
20450
+ this.detach();
20451
+ const error = event.error;
20452
+ this.logger.error('MediaRecorder error', error);
20453
+ this.recording.reject(error ?? new Error('MediaRecorder failed'));
20454
+ };
20455
+ }
20456
+
20209
20457
  /**
20210
20458
  * Tiny, type-safe event emitter.
20211
20459
  *
@@ -20557,5 +20805,5 @@ var e2eeWorker$1 = /*#__PURE__*/Object.freeze({
20557
20805
  e2eeWorker: e2eeWorker
20558
20806
  });
20559
20807
 
20560
- 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 };
20808
+ 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 };
20561
20809
  //# sourceMappingURL=index.es.js.map