@stream-io/video-client 1.26.0 → 1.27.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 (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.browser.es.js +241 -47
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +241 -46
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +241 -47
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +14 -2
  9. package/dist/src/StreamSfuClient.d.ts +7 -3
  10. package/dist/src/events/internal.d.ts +7 -1
  11. package/dist/src/gen/video/sfu/event/events.d.ts +57 -1
  12. package/dist/src/gen/video/sfu/models/models.d.ts +21 -0
  13. package/dist/src/helpers/array.d.ts +7 -0
  14. package/dist/src/helpers/participantUtils.d.ts +8 -1
  15. package/dist/src/rtc/BasePeerConnection.d.ts +2 -2
  16. package/dist/src/rtc/Dispatcher.d.ts +1 -1
  17. package/dist/src/rtc/signal.d.ts +1 -1
  18. package/dist/src/store/CallState.d.ts +2 -1
  19. package/dist/src/types.d.ts +10 -1
  20. package/package.json +1 -1
  21. package/src/Call.ts +48 -5
  22. package/src/StreamSfuClient.ts +33 -14
  23. package/src/coordinator/connection/connection.ts +0 -1
  24. package/src/events/__tests__/internal.test.ts +78 -0
  25. package/src/events/__tests__/participant.test.ts +66 -0
  26. package/src/events/callEventHandlers.ts +2 -0
  27. package/src/events/internal.ts +28 -1
  28. package/src/events/participant.ts +4 -1
  29. package/src/gen/video/sfu/event/events.ts +104 -0
  30. package/src/gen/video/sfu/models/models.ts +21 -0
  31. package/src/helpers/DynascaleManager.ts +6 -0
  32. package/src/helpers/__tests__/participantUtils.test.ts +167 -0
  33. package/src/helpers/array.ts +16 -0
  34. package/src/helpers/participantUtils.ts +23 -1
  35. package/src/rtc/BasePeerConnection.ts +6 -5
  36. package/src/rtc/Dispatcher.ts +3 -2
  37. package/src/rtc/__tests__/Publisher.test.ts +3 -2
  38. package/src/rtc/__tests__/Subscriber.test.ts +3 -2
  39. package/src/rtc/signal.ts +3 -3
  40. package/src/store/CallState.ts +7 -4
  41. package/src/types.ts +11 -0
package/dist/index.es.js CHANGED
@@ -972,6 +972,10 @@ var ErrorCode;
972
972
  * @generated from protobuf enum value: ERROR_CODE_CALL_NOT_FOUND = 300;
973
973
  */
974
974
  ErrorCode[ErrorCode["CALL_NOT_FOUND"] = 300] = "CALL_NOT_FOUND";
975
+ /**
976
+ * @generated from protobuf enum value: ERROR_CODE_CALL_PARTICIPANT_LIMIT_REACHED = 301;
977
+ */
978
+ ErrorCode[ErrorCode["CALL_PARTICIPANT_LIMIT_REACHED"] = 301] = "CALL_PARTICIPANT_LIMIT_REACHED";
975
979
  /**
976
980
  * @generated from protobuf enum value: ERROR_CODE_REQUEST_VALIDATION_FAILED = 400;
977
981
  */
@@ -1240,6 +1244,24 @@ var AppleThermalState;
1240
1244
  */
1241
1245
  AppleThermalState[AppleThermalState["CRITICAL"] = 4] = "CRITICAL";
1242
1246
  })(AppleThermalState || (AppleThermalState = {}));
1247
+ /**
1248
+ * ClientCapability defines a feature that client supports
1249
+ *
1250
+ * @generated from protobuf enum stream.video.sfu.models.ClientCapability
1251
+ */
1252
+ var ClientCapability;
1253
+ (function (ClientCapability) {
1254
+ /**
1255
+ * @generated from protobuf enum value: CLIENT_CAPABILITY_UNSPECIFIED = 0;
1256
+ */
1257
+ ClientCapability[ClientCapability["UNSPECIFIED"] = 0] = "UNSPECIFIED";
1258
+ /**
1259
+ * Enables SFU pausing inbound video
1260
+ *
1261
+ * @generated from protobuf enum value: CLIENT_CAPABILITY_SUBSCRIBER_VIDEO_PAUSE = 1;
1262
+ */
1263
+ ClientCapability[ClientCapability["SUBSCRIBER_VIDEO_PAUSE"] = 1] = "SUBSCRIBER_VIDEO_PAUSE";
1264
+ })(ClientCapability || (ClientCapability = {}));
1243
1265
  // @generated message type with reflection information, may provide speed optimized methods
1244
1266
  class CallState$Type extends MessageType {
1245
1267
  constructor() {
@@ -1910,6 +1932,7 @@ var models = /*#__PURE__*/Object.freeze({
1910
1932
  get CallEndedReason () { return CallEndedReason; },
1911
1933
  CallGrants: CallGrants,
1912
1934
  CallState: CallState$1,
1935
+ get ClientCapability () { return ClientCapability; },
1913
1936
  ClientDetails: ClientDetails,
1914
1937
  Codec: Codec,
1915
1938
  get ConnectionQuality () { return ConnectionQuality; },
@@ -2599,6 +2622,13 @@ class SfuEvent$Type extends MessageType {
2599
2622
  oneof: 'eventPayload',
2600
2623
  T: () => ChangePublishOptions,
2601
2624
  },
2625
+ {
2626
+ no: 28,
2627
+ name: 'inbound_state_notification',
2628
+ kind: 'message',
2629
+ oneof: 'eventPayload',
2630
+ T: () => InboundStateNotification,
2631
+ },
2602
2632
  ]);
2603
2633
  }
2604
2634
  }
@@ -2906,6 +2936,17 @@ class JoinRequest$Type extends MessageType {
2906
2936
  repeat: 2 /*RepeatType.UNPACKED*/,
2907
2937
  T: () => SubscribeOption,
2908
2938
  },
2939
+ {
2940
+ no: 11,
2941
+ name: 'capabilities',
2942
+ kind: 'enum',
2943
+ repeat: 1 /*RepeatType.PACKED*/,
2944
+ T: () => [
2945
+ 'stream.video.sfu.models.ClientCapability',
2946
+ ClientCapability,
2947
+ 'CLIENT_CAPABILITY_',
2948
+ ],
2949
+ },
2909
2950
  ]);
2910
2951
  }
2911
2952
  }
@@ -3351,6 +3392,48 @@ class CallEnded$Type extends MessageType {
3351
3392
  * @generated MessageType for protobuf message stream.video.sfu.event.CallEnded
3352
3393
  */
3353
3394
  const CallEnded = new CallEnded$Type();
3395
+ // @generated message type with reflection information, may provide speed optimized methods
3396
+ class InboundStateNotification$Type extends MessageType {
3397
+ constructor() {
3398
+ super('stream.video.sfu.event.InboundStateNotification', [
3399
+ {
3400
+ no: 1,
3401
+ name: 'inbound_video_states',
3402
+ kind: 'message',
3403
+ repeat: 2 /*RepeatType.UNPACKED*/,
3404
+ T: () => InboundVideoState,
3405
+ },
3406
+ ]);
3407
+ }
3408
+ }
3409
+ /**
3410
+ * @generated MessageType for protobuf message stream.video.sfu.event.InboundStateNotification
3411
+ */
3412
+ const InboundStateNotification = new InboundStateNotification$Type();
3413
+ // @generated message type with reflection information, may provide speed optimized methods
3414
+ class InboundVideoState$Type extends MessageType {
3415
+ constructor() {
3416
+ super('stream.video.sfu.event.InboundVideoState', [
3417
+ { no: 1, name: 'user_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
3418
+ { no: 2, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
3419
+ {
3420
+ no: 3,
3421
+ name: 'track_type',
3422
+ kind: 'enum',
3423
+ T: () => [
3424
+ 'stream.video.sfu.models.TrackType',
3425
+ TrackType,
3426
+ 'TRACK_TYPE_',
3427
+ ],
3428
+ },
3429
+ { no: 4, name: 'paused', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
3430
+ ]);
3431
+ }
3432
+ }
3433
+ /**
3434
+ * @generated MessageType for protobuf message stream.video.sfu.event.InboundVideoState
3435
+ */
3436
+ const InboundVideoState = new InboundVideoState$Type();
3354
3437
 
3355
3438
  var events = /*#__PURE__*/Object.freeze({
3356
3439
  __proto__: null,
@@ -3371,6 +3454,8 @@ var events = /*#__PURE__*/Object.freeze({
3371
3454
  HealthCheckResponse: HealthCheckResponse,
3372
3455
  ICERestart: ICERestart,
3373
3456
  ICETrickle: ICETrickle,
3457
+ InboundStateNotification: InboundStateNotification,
3458
+ InboundVideoState: InboundVideoState,
3374
3459
  JoinRequest: JoinRequest,
3375
3460
  JoinResponse: JoinResponse,
3376
3461
  LeaveCallRequest: LeaveCallRequest,
@@ -3795,6 +3880,7 @@ const sfuEventKinds = {
3795
3880
  participantUpdated: undefined,
3796
3881
  participantMigrationComplete: undefined,
3797
3882
  changePublishOptions: undefined,
3883
+ inboundStateNotification: undefined,
3798
3884
  };
3799
3885
  const isSfuEvent = (eventName) => {
3800
3886
  return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
@@ -3803,12 +3889,12 @@ class Dispatcher {
3803
3889
  constructor() {
3804
3890
  this.logger = getLogger(['Dispatcher']);
3805
3891
  this.subscribers = {};
3806
- this.dispatch = (message, logTag = '0') => {
3892
+ this.dispatch = (message, tag = '0') => {
3807
3893
  const eventKind = message.eventPayload.oneofKind;
3808
3894
  if (!eventKind)
3809
3895
  return;
3810
3896
  const payload = message.eventPayload[eventKind];
3811
- this.logger('debug', `Dispatching ${eventKind}, tag=${logTag}`, payload);
3897
+ this.logger('debug', `Dispatching ${eventKind}, tag=${tag}`, payload);
3812
3898
  const listeners = this.subscribers[eventKind];
3813
3899
  if (!listeners)
3814
3900
  return;
@@ -4325,6 +4411,24 @@ const hasScreenShareAudio = (p) => p.publishedTracks.includes(TrackType.SCREEN_S
4325
4411
  * @param p the participant.
4326
4412
  */
4327
4413
  const isPinned = (p) => !!p.pin && (p.pin.isLocalPin || p.pin.pinnedAt > 0);
4414
+ /**
4415
+ * Check if a participant has a paused track of the specified type.
4416
+ *
4417
+ * @param p the participant to check.
4418
+ * @param videoTrackType the type of video track to check for ('videoTrack' or 'screenShareTrack').
4419
+ */
4420
+ const hasPausedTrack = (p, videoTrackType) => {
4421
+ if (!p.pausedTracks)
4422
+ return false;
4423
+ const trackType = videoTrackType === 'videoTrack'
4424
+ ? TrackType.VIDEO
4425
+ : videoTrackType === 'screenShareTrack'
4426
+ ? TrackType.SCREEN_SHARE
4427
+ : undefined;
4428
+ if (!trackType)
4429
+ return false;
4430
+ return p.pausedTracks.includes(trackType);
4431
+ };
4328
4432
 
4329
4433
  /**
4330
4434
  * A comparator which sorts participants by the fact that they are the dominant speaker or not.
@@ -4727,17 +4831,17 @@ class CallState {
4727
4831
  *
4728
4832
  * @param sessionId the session ID of the participant to update.
4729
4833
  * @param participant the participant to update or add.
4834
+ * @param patch an optional patch to apply to the participant.
4730
4835
  */
4731
- this.updateOrAddParticipant = (sessionId, participant) => {
4836
+ this.updateOrAddParticipant = (sessionId, participant, patch) => {
4732
4837
  return this.setParticipants((participants) => {
4733
4838
  let add = true;
4734
4839
  const nextParticipants = participants.map((p) => {
4735
4840
  if (p.sessionId === sessionId) {
4736
4841
  add = false;
4737
- return {
4738
- ...p,
4739
- ...participant,
4740
- };
4842
+ const updated = { ...p, ...participant };
4843
+ const thePatch = typeof patch === 'function' ? patch(updated) : patch;
4844
+ return Object.assign(updated, thePatch);
4741
4845
  }
4742
4846
  return p;
4743
4847
  });
@@ -5811,7 +5915,7 @@ const aggregate = (stats) => {
5811
5915
  return report;
5812
5916
  };
5813
5917
 
5814
- const version = "1.26.0";
5918
+ const version = "1.27.0";
5815
5919
  const [major, minor, patch] = version.split('.');
5816
5920
  let sdkInfo = {
5817
5921
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6420,7 +6524,7 @@ class BasePeerConnection {
6420
6524
  /**
6421
6525
  * Constructs a new `BasePeerConnection` instance.
6422
6526
  */
6423
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, logTag, enableTracing, iceRestartDelay = 2500, }) {
6527
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, tag, enableTracing, iceRestartDelay = 2500, }) {
6424
6528
  this.isIceRestarting = false;
6425
6529
  this.isDisposed = false;
6426
6530
  this.trackIdToTrackType = new Map();
@@ -6644,13 +6748,12 @@ class BasePeerConnection {
6644
6748
  this.onReconnectionNeeded = onReconnectionNeeded;
6645
6749
  this.logger = getLogger([
6646
6750
  peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
6647
- logTag,
6751
+ tag,
6648
6752
  ]);
6649
6753
  this.pc = this.createPeerConnection(connectionConfig);
6650
6754
  this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
6651
6755
  if (enableTracing) {
6652
- const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
6653
- this.tracer = new Tracer(tag);
6756
+ this.tracer = new Tracer(`${tag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`);
6654
6757
  this.tracer.trace('create', {
6655
6758
  url: sfuClient.edgeName,
6656
6759
  ...connectionConfig,
@@ -7522,8 +7625,8 @@ class Subscriber extends BasePeerConnection {
7522
7625
  }
7523
7626
 
7524
7627
  const createWebSocketSignalChannel = (opts) => {
7525
- const { endpoint, onMessage, logTag } = opts;
7526
- const logger = getLogger(['SfuClientWS', logTag]);
7628
+ const { endpoint, onMessage, tag } = opts;
7629
+ const logger = getLogger(['SfuClientWS', tag]);
7527
7630
  logger('debug', 'Creating signaling WS channel:', endpoint);
7528
7631
  const ws = new WebSocket(endpoint);
7529
7632
  ws.binaryType = 'arraybuffer'; // do we need this?
@@ -7773,7 +7876,7 @@ class StreamSfuClient {
7773
7876
  /**
7774
7877
  * Constructs a new SFU client.
7775
7878
  */
7776
- constructor({ dispatcher, credentials, sessionId, logTag, joinResponseTimeout = 5000, onSignalClose, streamClient, enableTracing, }) {
7879
+ constructor({ dispatcher, credentials, sessionId, cid, tag, joinResponseTimeout = 5000, onSignalClose, streamClient, enableTracing, }) {
7777
7880
  /**
7778
7881
  * A buffer for ICE Candidates that are received before
7779
7882
  * the Publisher and Subscriber Peer Connections are ready to handle them.
@@ -7803,7 +7906,7 @@ class StreamSfuClient {
7803
7906
  * A controller to abort the current requests.
7804
7907
  */
7805
7908
  this.abortController = new AbortController();
7806
- this.createWebSocket = () => {
7909
+ this.createWebSocket = (params) => {
7807
7910
  const eventsToTrace = {
7808
7911
  callEnded: true,
7809
7912
  changePublishQuality: true,
@@ -7811,10 +7914,11 @@ class StreamSfuClient {
7811
7914
  connectionQualityChanged: true,
7812
7915
  error: true,
7813
7916
  goAway: true,
7917
+ inboundStateNotification: true,
7814
7918
  };
7815
7919
  this.signalWs = createWebSocketSignalChannel({
7816
- logTag: this.logTag,
7817
- endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
7920
+ tag: this.tag,
7921
+ endpoint: `${this.credentials.server.ws_endpoint}?${new URLSearchParams(params).toString()}`,
7818
7922
  onMessage: (message) => {
7819
7923
  this.lastMessageTimestamp = new Date();
7820
7924
  this.scheduleConnectionCheck();
@@ -7822,7 +7926,7 @@ class StreamSfuClient {
7822
7926
  if (eventsToTrace[eventKind]) {
7823
7927
  this.tracer?.trace(eventKind, message);
7824
7928
  }
7825
- this.dispatcher.dispatch(message, this.logTag);
7929
+ this.dispatcher.dispatch(message, this.tag);
7826
7930
  },
7827
7931
  });
7828
7932
  this.signalReady = makeSafePromise(Promise.race([
@@ -7937,7 +8041,7 @@ class StreamSfuClient {
7937
8041
  });
7938
8042
  this.migrateAwayTimeout = setTimeout(() => {
7939
8043
  unsubscribe();
7940
- task.reject(new Error(`Migration (${this.logTag}) failed to complete in ${timeout}ms`));
8044
+ task.reject(new Error(`Migration (${this.tag}) failed to complete in ${timeout}ms`));
7941
8045
  }, timeout);
7942
8046
  return task.promise;
7943
8047
  };
@@ -8037,10 +8141,10 @@ class StreamSfuClient {
8037
8141
  const { server, token } = credentials;
8038
8142
  this.edgeName = server.edge_name;
8039
8143
  this.joinResponseTimeout = joinResponseTimeout;
8040
- this.logTag = logTag;
8041
- this.logger = getLogger(['SfuClient', logTag]);
8144
+ this.tag = tag;
8145
+ this.logger = getLogger(['SfuClient', tag]);
8042
8146
  this.tracer = enableTracing
8043
- ? new Tracer(`${logTag}-${this.edgeName}`)
8147
+ ? new Tracer(`${tag}-${this.edgeName}`)
8044
8148
  : undefined;
8045
8149
  this.rpc = createSignalClient({
8046
8150
  baseUrl: server.url,
@@ -8069,7 +8173,13 @@ class StreamSfuClient {
8069
8173
  this.networkAvailableTask?.resolve();
8070
8174
  }
8071
8175
  });
8072
- this.createWebSocket();
8176
+ this.createWebSocket({
8177
+ attempt: tag,
8178
+ user_id: streamClient.user?.id || '',
8179
+ api_key: streamClient.key,
8180
+ user_session_id: this.sessionId,
8181
+ cid,
8182
+ });
8073
8183
  }
8074
8184
  get isHealthy() {
8075
8185
  return (this.signalWs.readyState === WebSocket.OPEN &&
@@ -8229,6 +8339,36 @@ const watchCallGrantsUpdated = (state) => {
8229
8339
  };
8230
8340
  };
8231
8341
 
8342
+ /**
8343
+ * Adds unique values to an array.
8344
+ *
8345
+ * @param arr the array to add to.
8346
+ * @param values the values to add.
8347
+ */
8348
+ const pushToIfMissing = (arr, ...values) => {
8349
+ for (const v of values) {
8350
+ if (!arr.includes(v)) {
8351
+ arr.push(v);
8352
+ }
8353
+ }
8354
+ return arr;
8355
+ };
8356
+ /**
8357
+ * Removes values from an array if they are present.
8358
+ *
8359
+ * @param arr the array to remove from.
8360
+ * @param values the values to remove.
8361
+ */
8362
+ const removeFromIfPresent = (arr, ...values) => {
8363
+ for (const v of values) {
8364
+ const index = arr.indexOf(v);
8365
+ if (index !== -1) {
8366
+ arr.splice(index, 1);
8367
+ }
8368
+ }
8369
+ return arr;
8370
+ };
8371
+
8232
8372
  const watchConnectionQualityChanged = (dispatcher, state) => {
8233
8373
  return dispatcher.on('connectionQualityChanged', (e) => {
8234
8374
  const { connectionQualityUpdates } = e;
@@ -8295,6 +8435,29 @@ const watchPinsUpdated = (state) => {
8295
8435
  state.setServerSidePins(pins);
8296
8436
  };
8297
8437
  };
8438
+ /**
8439
+ * Watches for inbound state notifications and updates the paused tracks
8440
+ *
8441
+ * @param state the call state to update.
8442
+ */
8443
+ const watchInboundStateNotification = (state) => {
8444
+ return function onInboundStateNotification(e) {
8445
+ const { inboundVideoStates } = e;
8446
+ const current = state.getParticipantLookupBySessionId();
8447
+ const patches = {};
8448
+ for (const { sessionId, trackType, paused } of inboundVideoStates) {
8449
+ const pausedTracks = [...(current[sessionId]?.pausedTracks ?? [])];
8450
+ if (paused) {
8451
+ pushToIfMissing(pausedTracks, trackType);
8452
+ }
8453
+ else {
8454
+ removeFromIfPresent(pausedTracks, trackType);
8455
+ }
8456
+ patches[sessionId] = { pausedTracks };
8457
+ }
8458
+ state.updateParticipants(patches);
8459
+ };
8460
+ };
8298
8461
 
8299
8462
  /**
8300
8463
  * An event handler that handles soft mutes.
@@ -8331,21 +8494,6 @@ const handleRemoteSoftMute = (call) => {
8331
8494
  });
8332
8495
  };
8333
8496
 
8334
- /**
8335
- * Adds unique values to an array.
8336
- *
8337
- * @param arr the array to add to.
8338
- * @param values the values to add.
8339
- */
8340
- const pushToIfMissing = (arr, ...values) => {
8341
- for (const v of values) {
8342
- if (!arr.includes(v)) {
8343
- arr.push(v);
8344
- }
8345
- }
8346
- return arr;
8347
- };
8348
-
8349
8497
  /**
8350
8498
  * An event responder which handles the `participantJoined` event.
8351
8499
  */
@@ -8427,11 +8575,14 @@ const watchTrackUnpublished = (state) => {
8427
8575
  if (e.participant) {
8428
8576
  const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
8429
8577
  const participant = Object.assign(e.participant, orphanedTracks);
8430
- state.updateOrAddParticipant(sessionId, participant);
8578
+ state.updateOrAddParticipant(sessionId, participant, (p) => ({
8579
+ pausedTracks: p.pausedTracks?.filter((t) => t !== type),
8580
+ }));
8431
8581
  }
8432
8582
  else {
8433
8583
  state.updateParticipant(sessionId, (p) => ({
8434
8584
  publishedTracks: p.publishedTracks.filter((t) => t !== type),
8585
+ pausedTracks: p.pausedTracks?.filter((t) => t !== type),
8435
8586
  }));
8436
8587
  }
8437
8588
  };
@@ -8523,6 +8674,7 @@ const registerEventHandlers = (call, dispatcher) => {
8523
8674
  watchDominantSpeakerChanged(dispatcher, state),
8524
8675
  call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
8525
8676
  call.on('pinsUpdated', watchPinsUpdated(state)),
8677
+ call.on('inboundStateNotification', watchInboundStateNotification(state)),
8526
8678
  handleRemoteSoftMute(call),
8527
8679
  ];
8528
8680
  if (call.ringing) {
@@ -9037,6 +9189,12 @@ class DynascaleManager {
9037
9189
  if (context.state === 'suspended') {
9038
9190
  document.addEventListener('click', this.resumeAudioContext);
9039
9191
  }
9192
+ // @ts-expect-error audioSession is available in Safari only
9193
+ const audioSession = navigator.audioSession;
9194
+ if (audioSession) {
9195
+ // https://github.com/w3c/audio-session/blob/main/explainer.md
9196
+ audioSession.type = 'play-and-record';
9197
+ }
9040
9198
  return (this.audioContext = context);
9041
9199
  };
9042
9200
  this.resumeAudioContext = () => {
@@ -11250,6 +11408,12 @@ class Call {
11250
11408
  */
11251
11409
  this.leaveCallHooks = new Set();
11252
11410
  this.streamClientEventHandlers = new Map();
11411
+ /**
11412
+ * A list of capabilities that the client supports and are enabled.
11413
+ */
11414
+ this.clientCapabilities = new Set([
11415
+ ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
11416
+ ]);
11253
11417
  /**
11254
11418
  * Sets up the call instance.
11255
11419
  *
@@ -11658,14 +11822,27 @@ class Call {
11658
11822
  throw new Error(`Illegal State: call.join() shall be called only once`);
11659
11823
  }
11660
11824
  this.state.setCallingState(CallingState.JOINING);
11825
+ // we will count the number of join failures per SFU.
11826
+ // once the number of failures reaches 2, we will piggyback on the `migrating_from`
11827
+ // field to force the coordinator to provide us another SFU
11828
+ const sfuJoinFailures = new Map();
11829
+ const joinData = data;
11661
11830
  maxJoinRetries = Math.max(maxJoinRetries, 1);
11662
11831
  for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
11663
11832
  try {
11664
11833
  this.logger('trace', `Joining call (${attempt})`, this.cid);
11665
- return await this.doJoin(data);
11834
+ await this.doJoin(data);
11835
+ delete joinData.migrating_from;
11836
+ break;
11666
11837
  }
11667
11838
  catch (err) {
11668
11839
  this.logger('warn', `Failed to join call (${attempt})`, this.cid);
11840
+ const sfuId = this.credentials?.server.edge_name || '';
11841
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
11842
+ sfuJoinFailures.set(sfuId, failures);
11843
+ if (failures >= 2) {
11844
+ joinData.migrating_from = sfuId;
11845
+ }
11669
11846
  if (attempt === maxJoinRetries - 1) {
11670
11847
  // restore the previous call state if the join-flow fails
11671
11848
  this.state.setCallingState(callingState);
@@ -11715,7 +11892,8 @@ class Call {
11715
11892
  const isWsHealthy = !!previousSfuClient?.isHealthy;
11716
11893
  const sfuClient = performingRejoin || performingMigration || !isWsHealthy
11717
11894
  ? new StreamSfuClient({
11718
- logTag: String(++this.sfuClientTag),
11895
+ tag: String(this.sfuClientTag++),
11896
+ cid: this.cid,
11719
11897
  dispatcher: this.dispatcher,
11720
11898
  credentials: this.credentials,
11721
11899
  streamClient: this.streamClient,
@@ -11757,6 +11935,7 @@ class Call {
11757
11935
  reconnectDetails,
11758
11936
  preferredPublishOptions,
11759
11937
  preferredSubscribeOptions,
11938
+ capabilities: Array.from(this.clientCapabilities),
11760
11939
  });
11761
11940
  this.currentPublishOptions = publishOptions;
11762
11941
  this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
@@ -11929,7 +12108,7 @@ class Call {
11929
12108
  dispatcher: this.dispatcher,
11930
12109
  state: this.state,
11931
12110
  connectionConfig,
11932
- logTag: String(this.sfuClientTag),
12111
+ tag: sfuClient.tag,
11933
12112
  enableTracing,
11934
12113
  onReconnectionNeeded: (kind, reason) => {
11935
12114
  this.reconnect(kind, reason).catch((err) => {
@@ -11951,7 +12130,7 @@ class Call {
11951
12130
  state: this.state,
11952
12131
  connectionConfig,
11953
12132
  publishOptions,
11954
- logTag: String(this.sfuClientTag),
12133
+ tag: sfuClient.tag,
11955
12134
  enableTracing,
11956
12135
  onReconnectionNeeded: (kind, reason) => {
11957
12136
  this.reconnect(kind, reason).catch((err) => {
@@ -13115,6 +13294,22 @@ class Call {
13115
13294
  this.setDisconnectionTimeout = (timeoutSeconds) => {
13116
13295
  this.disconnectionTimeoutSeconds = timeoutSeconds;
13117
13296
  };
13297
+ /**
13298
+ * Enables the provided client capabilities.
13299
+ */
13300
+ this.enableClientCapabilities = (...capabilities) => {
13301
+ for (const capability of capabilities) {
13302
+ this.clientCapabilities.add(capability);
13303
+ }
13304
+ };
13305
+ /**
13306
+ * Disables the provided client capabilities.
13307
+ */
13308
+ this.disableClientCapabilities = (...capabilities) => {
13309
+ for (const capability of capabilities) {
13310
+ this.clientCapabilities.delete(capability);
13311
+ }
13312
+ };
13118
13313
  this.type = type;
13119
13314
  this.id = id;
13120
13315
  this.cid = `${type}:${id}`;
@@ -13280,7 +13475,6 @@ class StableWSConnection {
13280
13475
  this.onmessage = (wsID, event) => {
13281
13476
  if (this.wsID !== wsID)
13282
13477
  return;
13283
- this._log('onmessage() - onmessage callback', { event, wsID });
13284
13478
  const data = typeof event.data === 'string'
13285
13479
  ? JSON.parse(event.data)
13286
13480
  : null;
@@ -14294,7 +14488,7 @@ class StreamClient {
14294
14488
  this.getUserAgent = () => {
14295
14489
  if (!this.cachedUserAgent) {
14296
14490
  const { clientAppIdentifier = {} } = this.options;
14297
- const { sdkName = 'js', sdkVersion = "1.26.0", ...extras } = clientAppIdentifier;
14491
+ const { sdkName = 'js', sdkVersion = "1.27.0", ...extras } = clientAppIdentifier;
14298
14492
  this.cachedUserAgent = [
14299
14493
  `stream-video-${sdkName}-v${sdkVersion}`,
14300
14494
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -14847,5 +15041,5 @@ class StreamVideoClient {
14847
15041
  }
14848
15042
  StreamVideoClient._instances = new Map();
14849
15043
 
14850
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, InputMediaDeviceManager, InputMediaDeviceManagerState, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RNSpeechDetector, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getLogLevel, getLogger, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio, hasScreenShare, hasScreenShareAudio, hasVideo, isPinned, livestreamOrAudioRoomSortPreset, logLevels, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setLogLevel, setLogger, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking };
15044
+ export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, InputMediaDeviceManager, InputMediaDeviceManagerState, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RNSpeechDetector, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getLogLevel, getLogger, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, isPinned, livestreamOrAudioRoomSortPreset, logLevels, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setLogLevel, setLogger, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking };
14851
15045
  //# sourceMappingURL=index.es.js.map