@stream-io/video-client 1.57.0 → 1.58.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 (105) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/e2ee-worker.browser.es.js +1342 -0
  3. package/dist/e2ee-worker.browser.es.js.map +1 -0
  4. package/dist/index.browser.es.js +831 -55
  5. package/dist/index.browser.es.js.map +1 -1
  6. package/dist/index.cjs.js +875 -54
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.es.js +864 -55
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/src/Call.d.ts +52 -6
  12. package/dist/src/coordinator/connection/connection.d.ts +1 -1
  13. package/dist/src/devices/CameraManager.d.ts +6 -0
  14. package/dist/src/devices/DeviceManager.d.ts +1 -0
  15. package/dist/src/devices/MicrophoneManager.d.ts +6 -0
  16. package/dist/src/devices/SpeakerManager.d.ts +2 -1
  17. package/dist/src/events/participant.d.ts +10 -3
  18. package/dist/src/gen/coordinator/index.d.ts +1363 -243
  19. package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
  20. package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
  21. package/dist/src/helpers/ensureExhausted.d.ts +1 -1
  22. package/dist/src/permissions/PermissionsContext.d.ts +2 -2
  23. package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
  24. package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
  25. package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
  26. package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
  27. package/dist/src/rtc/e2ee/events.d.ts +163 -0
  28. package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
  29. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
  30. package/dist/src/rtc/index.d.ts +1 -0
  31. package/dist/src/rtc/mediaEngine.d.ts +23 -0
  32. package/dist/src/rtc/types.d.ts +2 -0
  33. package/dist/src/store/CallState.d.ts +21 -0
  34. package/dist/src/types.d.ts +15 -6
  35. package/index.ts +3 -0
  36. package/package.json +1 -1
  37. package/src/Call.ts +160 -26
  38. package/src/__tests__/Call.test.ts +15 -0
  39. package/src/coordinator/connection/connection.ts +6 -3
  40. package/src/devices/AudioDeviceManager.ts +6 -0
  41. package/src/devices/CameraManager.ts +51 -4
  42. package/src/devices/DeviceManager.ts +17 -0
  43. package/src/devices/MicrophoneManager.ts +55 -5
  44. package/src/devices/ScreenShareManager.ts +3 -0
  45. package/src/devices/SpeakerManager.ts +42 -12
  46. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -20
  47. package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
  48. package/src/devices/__tests__/mocks.ts +36 -3
  49. package/src/events/__tests__/participant.test.ts +134 -1
  50. package/src/events/callEventHandlers.ts +9 -3
  51. package/src/events/participant.ts +42 -7
  52. package/src/gen/coordinator/index.ts +1384 -244
  53. package/src/gen/video/sfu/models/models.ts +15 -0
  54. package/src/helpers/TypedEventEmitter.ts +71 -0
  55. package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
  56. package/src/helpers/ensureExhausted.ts +4 -2
  57. package/src/permissions/PermissionsContext.ts +10 -5
  58. package/src/rtc/BasePeerConnection.ts +12 -1
  59. package/src/rtc/Publisher.ts +8 -0
  60. package/src/rtc/Subscriber.ts +5 -0
  61. package/src/rtc/__tests__/Publisher.test.ts +101 -0
  62. package/src/rtc/__tests__/Subscriber.test.ts +89 -0
  63. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
  64. package/src/rtc/e2ee/E2EEManager.ts +12 -0
  65. package/src/rtc/e2ee/EncryptionManager.ts +336 -0
  66. package/src/rtc/e2ee/SPEC.md +586 -0
  67. package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
  68. package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
  69. package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
  70. package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
  71. package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
  72. package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
  73. package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
  74. package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
  75. package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
  76. package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
  77. package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
  78. package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
  79. package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
  80. package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
  81. package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
  82. package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
  83. package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
  84. package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
  85. package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
  86. package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
  87. package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
  88. package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
  89. package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
  90. package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
  91. package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
  92. package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
  93. package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
  94. package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
  95. package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
  96. package/src/rtc/e2ee/e2ee-worker.ts +27 -0
  97. package/src/rtc/e2ee/events.ts +179 -0
  98. package/src/rtc/e2ee/transformSupport.ts +35 -0
  99. package/src/rtc/helpers/rtcConfiguration.ts +4 -2
  100. package/src/rtc/index.ts +1 -0
  101. package/src/rtc/mediaEngine.ts +44 -0
  102. package/src/rtc/types.ts +2 -0
  103. package/src/store/CallState.ts +33 -1
  104. package/src/store/__tests__/CallState.test.ts +82 -1
  105. package/src/types.ts +23 -5
@@ -67,6 +67,22 @@ const CreateDeviceRequestPushProviderEnum = {
67
67
  HUAWEI: 'huawei',
68
68
  XIAOMI: 'xiaomi',
69
69
  };
70
+ /**
71
+ * @export
72
+ */
73
+ const EncryptionSettingsRequestModeEnum = {
74
+ AVAILABLE: 'available',
75
+ DISABLED: 'disabled',
76
+ AUTO_ON: 'auto-on',
77
+ };
78
+ /**
79
+ * @export
80
+ */
81
+ const EncryptionSettingsResponseModeEnum = {
82
+ AVAILABLE: 'available',
83
+ DISABLED: 'disabled',
84
+ AUTO_ON: 'auto-on',
85
+ };
70
86
  /**
71
87
  * @export
72
88
  */
@@ -93,6 +109,21 @@ const FrameRecordingSettingsResponseModeEnum = {
93
109
  DISABLED: 'disabled',
94
110
  AUTO_ON: 'auto-on',
95
111
  };
112
+ /**
113
+ * @export
114
+ */
115
+ const HLSSettingsRequestQualityTracksEnum = {
116
+ _360P: '360p',
117
+ _480P: '480p',
118
+ _720P: '720p',
119
+ _1080P: '1080p',
120
+ _1440P: '1440p',
121
+ PORTRAIT_360X640: 'portrait-360x640',
122
+ PORTRAIT_480X854: 'portrait-480x854',
123
+ PORTRAIT_720X1280: 'portrait-720x1280',
124
+ PORTRAIT_1080X1920: 'portrait-1080x1920',
125
+ PORTRAIT_1440X2560: 'portrait-1440x2560',
126
+ };
96
127
  /**
97
128
  * @export
98
129
  */
@@ -101,6 +132,17 @@ const IndividualRecordingSettingsRequestModeEnum = {
101
132
  DISABLED: 'disabled',
102
133
  AUTO_ON: 'auto-on',
103
134
  };
135
+ /**
136
+ * @export
137
+ */
138
+ const IndividualRecordingSettingsRequestOutputTypesEnum = {
139
+ AUDIO_ONLY: 'audio_only',
140
+ VIDEO_ONLY: 'video_only',
141
+ AUDIO_VIDEO: 'audio_video',
142
+ SCREENSHARE_AUDIO_ONLY: 'screenshare_audio_only',
143
+ SCREENSHARE_VIDEO_ONLY: 'screenshare_video_only',
144
+ SCREENSHARE_AUDIO_VIDEO: 'screenshare_audio_video',
145
+ };
104
146
  /**
105
147
  * @export
106
148
  */
@@ -259,6 +301,14 @@ const RecordSettingsRequestQualityEnum = {
259
301
  PORTRAIT_1080X1920: 'portrait-1080x1920',
260
302
  PORTRAIT_1440X2560: 'portrait-1440x2560',
261
303
  };
304
+ /**
305
+ * @export
306
+ */
307
+ const RequestPermissionRequestPermissionsEnum = {
308
+ SCREENSHARE: 'screenshare',
309
+ SEND_AUDIO: 'send-audio',
310
+ SEND_VIDEO: 'send-video',
311
+ };
262
312
  /**
263
313
  * @export
264
314
  */
@@ -459,6 +509,59 @@ const TranscriptionSettingsResponseModeEnum = {
459
509
  DISABLED: 'disabled',
460
510
  AUTO_ON: 'auto-on',
461
511
  };
512
+ /**
513
+ * @export
514
+ */
515
+ const TranslationSettingsLanguagesEnum = {
516
+ EN: 'en',
517
+ FR: 'fr',
518
+ ES: 'es',
519
+ DE: 'de',
520
+ IT: 'it',
521
+ NL: 'nl',
522
+ PT: 'pt',
523
+ PL: 'pl',
524
+ CA: 'ca',
525
+ CS: 'cs',
526
+ DA: 'da',
527
+ EL: 'el',
528
+ FI: 'fi',
529
+ ID: 'id',
530
+ JA: 'ja',
531
+ RU: 'ru',
532
+ SV: 'sv',
533
+ TA: 'ta',
534
+ TH: 'th',
535
+ TR: 'tr',
536
+ HU: 'hu',
537
+ RO: 'ro',
538
+ ZH: 'zh',
539
+ AR: 'ar',
540
+ TL: 'tl',
541
+ HE: 'he',
542
+ HI: 'hi',
543
+ HR: 'hr',
544
+ KO: 'ko',
545
+ MS: 'ms',
546
+ NO: 'no',
547
+ UK: 'uk',
548
+ };
549
+ /**
550
+ * @export
551
+ */
552
+ const UpdateUserPermissionsRequestGrantPermissionsEnum = {
553
+ SCREENSHARE: 'screenshare',
554
+ SEND_AUDIO: 'send-audio',
555
+ SEND_VIDEO: 'send-video',
556
+ };
557
+ /**
558
+ * @export
559
+ */
560
+ const UpdateUserPermissionsRequestRevokePermissionsEnum = {
561
+ SCREENSHARE: 'screenshare',
562
+ SEND_AUDIO: 'send-audio',
563
+ SEND_VIDEO: 'send-video',
564
+ };
462
565
  /**
463
566
  * @export
464
567
  */
@@ -475,6 +578,14 @@ const VideoSettingsResponseCameraFacingEnum = {
475
578
  BACK: 'back',
476
579
  EXTERNAL: 'external',
477
580
  };
581
+ /**
582
+ * @export
583
+ */
584
+ const WSAuthMessageProductsEnum = {
585
+ CHAT: 'chat',
586
+ VIDEO: 'video',
587
+ FEEDS: 'feeds',
588
+ };
478
589
 
479
590
  class ErrorFromResponse extends Error {
480
591
  code;
@@ -1472,6 +1583,13 @@ class CallState$Type extends MessageType {
1472
1583
  repeat: 2 /*RepeatType.UNPACKED*/,
1473
1584
  T: () => Pin,
1474
1585
  },
1586
+ {
1587
+ no: 5,
1588
+ name: 'e2ee_enabled',
1589
+ kind: 'scalar',
1590
+ jsonName: 'e2eeEnabled',
1591
+ T: 8 /*ScalarType.BOOL*/,
1592
+ },
1475
1593
  ]);
1476
1594
  }
1477
1595
  }
@@ -4594,6 +4712,19 @@ const isSvcCodec = (codecOrMimeType) => {
4594
4712
  codecOrMimeType === 'video/av1');
4595
4713
  };
4596
4714
 
4715
+ /**
4716
+ * The default engine: a thin, stateless wrapper over the WebRTC globals.
4717
+ */
4718
+ const defaultGlobalsEngine = {
4719
+ dispose: () => Promise.resolve(true),
4720
+ };
4721
+ const defaultGlobalsEngineProvider = () => defaultGlobalsEngine;
4722
+ let provider = defaultGlobalsEngineProvider;
4723
+ const setCallMediaEngineProvider = (newProvider) => {
4724
+ provider = newProvider;
4725
+ };
4726
+ const getCallMediaEngineProvider = () => provider;
4727
+
4597
4728
  const sfuEventKinds = {
4598
4729
  subscriberOffer: undefined,
4599
4730
  publisherAnswer: undefined,
@@ -4801,7 +4932,9 @@ const parseUfragFromCandidate = (candidate) => {
4801
4932
  };
4802
4933
 
4803
4934
  const ensureExhausted = (x, message) => {
4804
- videoLoggerSystem.getLogger('helpers').warn(message, x);
4935
+ if (message) {
4936
+ videoLoggerSystem.getLogger('helpers').warn(message, x);
4937
+ }
4805
4938
  };
4806
4939
 
4807
4940
  /**
@@ -4926,7 +5059,7 @@ const toIceCandidate = (iceTrickle) => {
4926
5059
  }
4927
5060
  };
4928
5061
 
4929
- const version = "1.57.0";
5062
+ const version = "1.58.0";
4930
5063
  const [major, minor, patch] = version.split('.');
4931
5064
  let sdkInfo = {
4932
5065
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5909,6 +6042,7 @@ class CallState {
5909
6042
  settingsSubject = new BehaviorSubject(undefined);
5910
6043
  transcribingSubject = new BehaviorSubject(false);
5911
6044
  captioningSubject = new BehaviorSubject(false);
6045
+ e2eeEnabledSubject = new BehaviorSubject(false);
5912
6046
  endedBySubject = new BehaviorSubject(undefined);
5913
6047
  thumbnailsSubject = new BehaviorSubject(undefined);
5914
6048
  membersSubject = new BehaviorSubject([]);
@@ -6067,6 +6201,11 @@ class CallState {
6067
6201
  * Will provide the closed captioning state of this call.
6068
6202
  */
6069
6203
  captioning$;
6204
+ /**
6205
+ * Whether end-to-end encryption is active for this call, as reported by the
6206
+ * SFU in the join response.
6207
+ */
6208
+ e2eeEnabled$;
6070
6209
  /**
6071
6210
  * Will provide the user who ended this call.
6072
6211
  */
@@ -6168,6 +6307,7 @@ class CallState {
6168
6307
  this.rawRecording$ = duc(this.rawRecordingSubject);
6169
6308
  this.transcribing$ = duc(this.transcribingSubject);
6170
6309
  this.captioning$ = duc(this.captioningSubject);
6310
+ this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
6171
6311
  this.eventHandlers = {
6172
6312
  // these events are not updating the call state:
6173
6313
  'call.frame_recording_ready': undefined,
@@ -6270,6 +6410,7 @@ class CallState {
6270
6410
  clearTimeout(taskId);
6271
6411
  this.closedCaptionsTasks.delete(ccKey);
6272
6412
  }
6413
+ this.removeAllOrphanedTracks();
6273
6414
  };
6274
6415
  /**
6275
6416
  * Sets the list of criteria that are used to sort the participants.
@@ -6619,6 +6760,12 @@ class CallState {
6619
6760
  get transcribing() {
6620
6761
  return this.getCurrentValue(this.transcribing$);
6621
6762
  }
6763
+ /**
6764
+ * Whether end-to-end encryption is active for this call.
6765
+ */
6766
+ get e2eeEnabled() {
6767
+ return this.getCurrentValue(this.e2eeEnabled$);
6768
+ }
6622
6769
  /**
6623
6770
  * Will provide the user who ended this call.
6624
6771
  */
@@ -6839,6 +6986,18 @@ class CallState {
6839
6986
  removeOrphanedTrack = (id) => {
6840
6987
  this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
6841
6988
  };
6989
+ /**
6990
+ * Drops every orphaned track. Call this when the peer connections that own
6991
+ * the stored receivers go away (full leave, reconnect, or migration):
6992
+ * `pc.close()` does not raise the track `ended` event, so the per-track
6993
+ * cleanup never fires and the receivers + their closed PCs would otherwise
6994
+ * leak for the call's lifetime.
6995
+ *
6996
+ * @internal
6997
+ */
6998
+ removeAllOrphanedTracks = () => {
6999
+ this.orphanedTracks = [];
7000
+ };
6842
7001
  /**
6843
7002
  * Takes all orphaned tracks with the given track lookup prefix.
6844
7003
  * All orphaned tracks with the given track lookup prefix are removed from the call state.
@@ -6901,7 +7060,7 @@ class CallState {
6901
7060
  * @param reconnectDetails optional reconnect details.
6902
7061
  */
6903
7062
  updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
6904
- const { participants, participantCount, startedAt, pins } = callState;
7063
+ const { participants, participantCount, startedAt, pins, e2EeEnabled } = callState;
6905
7064
  const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
6906
7065
  this.setParticipants(() => {
6907
7066
  const participantLookup = this.getParticipantLookupBySessionId();
@@ -6927,6 +7086,7 @@ class CallState {
6927
7086
  this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
6928
7087
  this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
6929
7088
  this.setServerSidePins(pins);
7089
+ this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
6930
7090
  };
6931
7091
  updateFromMemberRemoved = (event) => {
6932
7092
  this.updateFromCallResponse(event.call);
@@ -7124,6 +7284,37 @@ class CallState {
7124
7284
  };
7125
7285
  }
7126
7286
 
7287
+ /**
7288
+ * Detection and selection policy for the two WebRTC Encoded Transform APIs.
7289
+ * Internal: which one the SDK attaches is an RTC-layer detail. Consumers want
7290
+ * `EncryptionManager.isSupported` instead.
7291
+ */
7292
+ /**
7293
+ * Chrome only, and the reason an RTCPeerConnection carrying E2EE needs the
7294
+ * non-standard `encodedInsertableStreams` flag.
7295
+ */
7296
+ const hasInsertableStreams = () => typeof RTCRtpSender !== 'undefined' &&
7297
+ 'createEncodedStreams' in RTCRtpSender.prototype;
7298
+ /** Whether the standard `RTCRtpScriptTransform` API exists. */
7299
+ const hasScriptTransform = () => typeof RTCRtpScriptTransform !== 'undefined';
7300
+ /**
7301
+ * Which Encoded Transform API to attach E2EE with here.
7302
+ *
7303
+ * - `'insertable'`: legacy Insertable Streams. Used on Chrome, where
7304
+ * `RTCRtpScriptTransform` is still unreliable for E2EE.
7305
+ * - `'script'`: the standard API. Used everywhere else.
7306
+ * - `undefined`: neither exists, so E2EE cannot run.
7307
+ */
7308
+ const preferredTransform = () => {
7309
+ const insertable = hasInsertableStreams();
7310
+ // Chrome's RTCRtpScriptTransform is still unreliable for E2EE.
7311
+ if (isChrome() && insertable)
7312
+ return 'insertable';
7313
+ if (hasScriptTransform())
7314
+ return 'script';
7315
+ return insertable ? 'insertable' : undefined;
7316
+ };
7317
+
7127
7318
  /**
7128
7319
  * Flatten the stats report into an array of stats objects.
7129
7320
  *
@@ -8232,6 +8423,7 @@ class BasePeerConnection {
8232
8423
  state;
8233
8424
  dispatcher;
8234
8425
  clientPublishOptions;
8426
+ e2ee;
8235
8427
  tag;
8236
8428
  sfuClient;
8237
8429
  onReconnectionNeeded;
@@ -8253,13 +8445,14 @@ class BasePeerConnection {
8253
8445
  /**
8254
8446
  * Constructs a new `BasePeerConnection` instance.
8255
8447
  */
8256
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
8448
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, e2ee, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
8257
8449
  this.peerType = peerType;
8258
8450
  this.sfuClient = sfuClient;
8259
8451
  this.state = state;
8260
8452
  this.dispatcher = dispatcher;
8261
8453
  this.iceRestartDelay = iceRestartDelay;
8262
8454
  this.clientPublishOptions = clientPublishOptions;
8455
+ this.e2ee = e2ee;
8263
8456
  this.tag = tag;
8264
8457
  this.onReconnectionNeeded = onReconnectionNeeded;
8265
8458
  this.onIceConnected = onIceConnected;
@@ -8278,7 +8471,13 @@ class BasePeerConnection {
8278
8471
  }
8279
8472
  }
8280
8473
  createPeerConnection = (connectionConfig) => {
8281
- const pc = new RTCPeerConnection(connectionConfig);
8474
+ const config = { ...connectionConfig };
8475
+ // The legacy Insertable Streams path requires this non-standard flag.
8476
+ if (this.e2ee && hasInsertableStreams()) {
8477
+ // @ts-expect-error not part of the standard lib yet
8478
+ config.encodedInsertableStreams = true;
8479
+ }
8480
+ const pc = new RTCPeerConnection(config);
8282
8481
  pc.addEventListener('icecandidate', this.onIceCandidate);
8283
8482
  pc.addEventListener('icecandidateerror', this.onIceCandidateError);
8284
8483
  pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
@@ -9032,6 +9231,10 @@ class Publisher extends BasePeerConnection {
9032
9231
  toRTCDegradationPreference(publishOption.degradationPreference) ??
9033
9232
  'maintain-framerate';
9034
9233
  await transceiver.sender.setParameters(params);
9234
+ if (this.e2ee) {
9235
+ this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
9236
+ this.logger.debug('E2EE encryptor attached to sender');
9237
+ }
9035
9238
  await this.negotiate();
9036
9239
  };
9037
9240
  /**
@@ -9643,9 +9846,15 @@ class Subscriber extends BasePeerConnection {
9643
9846
  trackLookupPrefix: trackId,
9644
9847
  track: primaryStream,
9645
9848
  trackType,
9849
+ receiver: this.e2ee ? e.receiver : undefined,
9646
9850
  });
9647
9851
  return;
9648
9852
  }
9853
+ else if (this.e2ee) {
9854
+ const { userId } = participantToUpdate;
9855
+ this.e2ee.decrypt(e.receiver, userId, TrackType[trackType]);
9856
+ this.logger.debug('E2EE decryptor attached to receiver', userId);
9857
+ }
9649
9858
  const streamKindProp = trackTypeToParticipantStreamKey(trackType);
9650
9859
  if (!streamKindProp) {
9651
9860
  this.logger.error(`Unknown track type: ${rawTrackType}`);
@@ -10617,7 +10826,7 @@ const handleRemoteSoftMute = (call) => {
10617
10826
  /**
10618
10827
  * An event responder which handles the `participantJoined` event.
10619
10828
  */
10620
- const watchParticipantJoined = (state) => {
10829
+ const watchParticipantJoined = (state, e2ee = undefined) => {
10621
10830
  return function onParticipantJoined(e) {
10622
10831
  const { participant } = e;
10623
10832
  if (!participant)
@@ -10630,7 +10839,7 @@ const watchParticipantJoined = (state) => {
10630
10839
  // The SFU would send participant info as part of the `join`
10631
10840
  // response and then follow up with a `participantJoined` event for
10632
10841
  // already announced participants.
10633
- const orphanedTracks = reconcileOrphanedTracks(state, participant);
10842
+ const orphanedTracks = reconcileOrphanedTracks(state, participant, e2ee);
10634
10843
  state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
10635
10844
  ...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
10636
10845
  viewportVisibilityState: {
@@ -10666,7 +10875,7 @@ const watchParticipantUpdated = (state) => {
10666
10875
  * An event responder which handles the `trackPublished` event.
10667
10876
  * The SFU will send this event when a participant publishes a track.
10668
10877
  */
10669
- const watchTrackPublished = (state) => {
10878
+ const watchTrackPublished = (state, e2ee = undefined) => {
10670
10879
  return function onTrackPublished(e) {
10671
10880
  const { type, sessionId } = e;
10672
10881
  // An optimization for large calls.
@@ -10674,7 +10883,7 @@ const watchTrackPublished = (state) => {
10674
10883
  // events, and instead, it would only provide the participant's information
10675
10884
  // once they start publishing a track.
10676
10885
  if (e.participant) {
10677
- const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
10886
+ const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
10678
10887
  const participant = Object.assign(e.participant, orphanedTracks);
10679
10888
  state.updateOrAddParticipant(sessionId, participant);
10680
10889
  }
@@ -10689,12 +10898,12 @@ const watchTrackPublished = (state) => {
10689
10898
  * An event responder which handles the `trackUnpublished` event.
10690
10899
  * The SFU will send this event when a participant unpublishes a track.
10691
10900
  */
10692
- const watchTrackUnpublished = (state) => {
10901
+ const watchTrackUnpublished = (state, e2ee = undefined) => {
10693
10902
  return function onTrackUnpublished(e) {
10694
10903
  const { type, sessionId } = e;
10695
10904
  // An optimization for large calls. See `watchTrackPublished`.
10696
10905
  if (e.participant) {
10697
- const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
10906
+ const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
10698
10907
  const participant = Object.assign(e.participant, orphanedTracks);
10699
10908
  state.updateOrAddParticipant(sessionId, participant, (p) => ({
10700
10909
  pausedTracks: p.pausedTracks?.filter((t) => t !== type),
@@ -10713,17 +10922,22 @@ const watchTrackUnpublished = (state) => {
10713
10922
  *
10714
10923
  * @param state the call state.
10715
10924
  * @param participant the participant.
10925
+ * @param e2ee accessor for the call's encryption manager, if any.
10716
10926
  */
10717
- const reconcileOrphanedTracks = (state, participant) => {
10927
+ const reconcileOrphanedTracks = (state, participant, e2ee) => {
10718
10928
  const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
10719
10929
  if (!orphanTracks.length)
10720
10930
  return;
10931
+ const manager = e2ee?.();
10721
10932
  const reconciledTracks = {};
10722
10933
  for (const orphan of orphanTracks) {
10723
10934
  const key = trackTypeToParticipantStreamKey(orphan.trackType);
10724
10935
  if (!key)
10725
10936
  continue;
10726
10937
  reconciledTracks[key] = orphan.track;
10938
+ if (manager && orphan.receiver) {
10939
+ manager.decrypt(orphan.receiver, participant.userId, TrackType[orphan.trackType]);
10940
+ }
10727
10941
  }
10728
10942
  return reconciledTracks;
10729
10943
  };
@@ -10779,6 +10993,11 @@ const watchAudioLevelChanged = (dispatcher, state) => {
10779
10993
  */
10780
10994
  const registerEventHandlers = (call, dispatcher) => {
10781
10995
  const state = call.state;
10996
+ // Read lazily on each event: setE2EEManager can run after setup() (an app that
10997
+ // inspects call settings via get()/getOrCreate() before deciding to encrypt has
10998
+ // already triggered it), so a value captured here would be a stale undefined
10999
+ // for the whole call and orphaned tracks would never get a decryptor.
11000
+ const e2ee = () => call.e2eeManager;
10782
11001
  const eventHandlers = [
10783
11002
  call.on('call.ended', watchCallEnded(call)),
10784
11003
  watchSfuCallEnded(call),
@@ -10786,11 +11005,11 @@ const registerEventHandlers = (call, dispatcher) => {
10786
11005
  watchSfuErrorReports(dispatcher),
10787
11006
  watchConnectionQualityChanged(dispatcher, state),
10788
11007
  watchParticipantCountChanged(dispatcher, state),
10789
- call.on('participantJoined', watchParticipantJoined(state)),
11008
+ call.on('participantJoined', watchParticipantJoined(state, e2ee)),
10790
11009
  call.on('participantLeft', watchParticipantLeft(state)),
10791
11010
  call.on('participantUpdated', watchParticipantUpdated(state)),
10792
- call.on('trackPublished', watchTrackPublished(state)),
10793
- call.on('trackUnpublished', watchTrackUnpublished(state)),
11011
+ call.on('trackPublished', watchTrackPublished(state, e2ee)),
11012
+ call.on('trackUnpublished', watchTrackUnpublished(state, e2ee)),
10794
11013
  watchAudioLevelChanged(dispatcher, state),
10795
11014
  watchDominantSpeakerChanged(dispatcher, state),
10796
11015
  call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
@@ -11918,13 +12137,14 @@ class PermissionsContext {
11918
12137
  return false;
11919
12138
  const { audio, video, screensharing } = settings;
11920
12139
  switch (permission) {
11921
- case OwnCapability.SEND_AUDIO:
12140
+ case RequestPermissionRequestPermissionsEnum.SEND_AUDIO:
11922
12141
  return audio.access_request_enabled;
11923
- case OwnCapability.SEND_VIDEO:
12142
+ case RequestPermissionRequestPermissionsEnum.SEND_VIDEO:
11924
12143
  return video.access_request_enabled;
11925
- case OwnCapability.SCREENSHARE:
12144
+ case RequestPermissionRequestPermissionsEnum.SCREENSHARE:
11926
12145
  return screensharing.access_request_enabled;
11927
12146
  default:
12147
+ ensureExhausted(permission);
11928
12148
  return false;
11929
12149
  }
11930
12150
  };
@@ -12931,6 +13151,27 @@ class DeviceManager {
12931
13151
  this.filters.forEach((entry) => entry.stop?.());
12932
13152
  }
12933
13153
  }
13154
+ reconcileOptimisticStatus = async () => {
13155
+ const target = this.state.optimisticStatus;
13156
+ await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
13157
+ try {
13158
+ if (target === 'enabled' && this.state.status !== 'enabled') {
13159
+ await this.unmuteStream();
13160
+ if (!signal.aborted)
13161
+ this.state.setStatus('enabled');
13162
+ }
13163
+ else if (target === 'disabled' && this.state.status === 'enabled') {
13164
+ // mirror whatever disable() does to stop/pause the track per disableMode
13165
+ if (!signal.aborted)
13166
+ this.state.setStatus('disabled');
13167
+ }
13168
+ }
13169
+ finally {
13170
+ if (!signal.aborted)
13171
+ this.state.setPendingStatus(this.state.status);
13172
+ }
13173
+ });
13174
+ };
12934
13175
  disableTracks() {
12935
13176
  this.getTracks().forEach((track) => {
12936
13177
  if (track.enabled)
@@ -13564,6 +13805,33 @@ class CameraManager extends DeviceManager {
13564
13805
  }
13565
13806
  }
13566
13807
  }
13808
+ enable() {
13809
+ if (isReactNative() && !this.call.hasMediaEngine) {
13810
+ this.state.setPendingStatus('enabled');
13811
+ return Promise.resolve();
13812
+ }
13813
+ return super.enable();
13814
+ }
13815
+ async disable(forceStopOrOptions) {
13816
+ if (isReactNative() && !this.call.hasMediaEngine) {
13817
+ this.state.setPendingStatus('disabled');
13818
+ return;
13819
+ }
13820
+ // forward verbatim to the base, narrowing so the right overload is selected
13821
+ if (forceStopOrOptions === undefined)
13822
+ return super.disable();
13823
+ if (typeof forceStopOrOptions === 'boolean') {
13824
+ return super.disable(forceStopOrOptions);
13825
+ }
13826
+ return super.disable(forceStopOrOptions);
13827
+ }
13828
+ toggle() {
13829
+ if (isReactNative() && !this.call.hasMediaEngine) {
13830
+ this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
13831
+ return Promise.resolve();
13832
+ }
13833
+ return super.toggle();
13834
+ }
13567
13835
  /**
13568
13836
  * Applies the video settings to the camera.
13569
13837
  *
@@ -13597,9 +13865,16 @@ class CameraManager extends DeviceManager {
13597
13865
  await this.enable();
13598
13866
  }
13599
13867
  }
13600
- const { mediaStream } = this.state;
13601
- if (canPublish && publish && this.enabled && mediaStream) {
13602
- await this.publishStream(mediaStream);
13868
+ if (isReactNative() && publish && canPublish) {
13869
+ // On RN the camera is enabled/disabled optimistically before JOINED. Reconcile now
13870
+ // acquires the track and publishes it, so it fully owns the publish.
13871
+ await this.reconcileOptimisticStatus();
13872
+ }
13873
+ else {
13874
+ const { mediaStream } = this.state;
13875
+ if (canPublish && publish && this.enabled && mediaStream) {
13876
+ await this.publishStream(mediaStream);
13877
+ }
13603
13878
  }
13604
13879
  }
13605
13880
  getDevices() {
@@ -13618,7 +13893,10 @@ class CameraManager extends DeviceManager {
13618
13893
  }
13619
13894
  return constraints;
13620
13895
  }
13621
- getStream(constraints) {
13896
+ async getStream(constraints) {
13897
+ // Ensure the call's media factory exists before capture so the resulting
13898
+ // track is owned by it (the WebRTC globals resolve to the live factory).
13899
+ await this.call.ensureMediaFactory();
13622
13900
  return getVideoStream(constraints, this.call.tracer);
13623
13901
  }
13624
13902
  }
@@ -13634,6 +13912,9 @@ class AudioDeviceManager extends DeviceManager {
13634
13912
  if (!this.call.state.settings?.audio.hifi_audio_enabled) {
13635
13913
  throw new Error('High Fidelity audio is not enabled for this call');
13636
13914
  }
13915
+ if (isReactNative() && this.call.hasMediaEngine) {
13916
+ throw new Error('setAudioBitrateProfile must be called before joining the call.');
13917
+ }
13637
13918
  this.doSetAudioBitrateProfile(profile);
13638
13919
  this.state.setAudioBitrateProfile(profile);
13639
13920
  if (this.enabled) {
@@ -13906,7 +14187,10 @@ class MicrophoneManager extends AudioDeviceManager {
13906
14187
  ]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
13907
14188
  try {
13908
14189
  if (callingState === CallingState.LEFT) {
13909
- this.setMutedRecordingPrepared(false);
14190
+ // The muted-recording-prepared mode is reset in `callManager.stop()`
14191
+ // (during leave, while the call factory is still alive), not here —
14192
+ // this subscription fires asynchronously and could land after the
14193
+ // factory is disposed, forcing a default-ADM rebuild.
13910
14194
  await this.stopSpeakingWhileMutedDetection();
13911
14195
  }
13912
14196
  if (callingState !== CallingState.JOINED)
@@ -14006,6 +14290,33 @@ class MicrophoneManager extends AudioDeviceManager {
14006
14290
  this.subscriptions.push(unsubscribe);
14007
14291
  }
14008
14292
  }
14293
+ enable() {
14294
+ if (isReactNative() && !this.call.hasMediaEngine) {
14295
+ this.state.setPendingStatus('enabled');
14296
+ return Promise.resolve();
14297
+ }
14298
+ return super.enable();
14299
+ }
14300
+ async disable(forceStopOrOptions) {
14301
+ if (isReactNative() && !this.call.hasMediaEngine) {
14302
+ this.state.setPendingStatus('disabled');
14303
+ return;
14304
+ }
14305
+ // forward verbatim to the base, narrowing so the right overload is selected
14306
+ if (forceStopOrOptions === undefined)
14307
+ return super.disable();
14308
+ if (typeof forceStopOrOptions === 'boolean') {
14309
+ return super.disable(forceStopOrOptions);
14310
+ }
14311
+ return super.disable(forceStopOrOptions);
14312
+ }
14313
+ toggle() {
14314
+ if (isReactNative() && !this.call.hasMediaEngine) {
14315
+ this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
14316
+ return Promise.resolve();
14317
+ }
14318
+ return super.toggle();
14319
+ }
14009
14320
  /**
14010
14321
  * Enables noise cancellation for the microphone.
14011
14322
  *
@@ -14166,15 +14477,25 @@ class MicrophoneManager extends AudioDeviceManager {
14166
14477
  await this.enable();
14167
14478
  }
14168
14479
  }
14169
- const { mediaStream } = this.state;
14170
- if (canPublish && publish && this.enabled && mediaStream) {
14171
- await this.publishStream(mediaStream);
14480
+ if (isReactNative() && publish && canPublish) {
14481
+ // On RN the microphone is enabled/disabled optimistically before JOINED. Reconcile now
14482
+ // acquires the track and publishes it, so it fully owns the publish.
14483
+ await this.reconcileOptimisticStatus();
14484
+ }
14485
+ else {
14486
+ const { mediaStream } = this.state;
14487
+ if (canPublish && publish && this.enabled && mediaStream) {
14488
+ await this.publishStream(mediaStream);
14489
+ }
14172
14490
  }
14173
14491
  }
14174
14492
  getDevices() {
14175
14493
  return getAudioDevices(this.call.tracer);
14176
14494
  }
14177
- getStream(constraints) {
14495
+ async getStream(constraints) {
14496
+ // Ensure the call's media factory exists before capture so the resulting
14497
+ // track is owned by it (the WebRTC globals resolve to the live factory).
14498
+ await this.call.ensureMediaFactory();
14178
14499
  return getAudioStream(constraints, this.call.tracer);
14179
14500
  }
14180
14501
  doSetAudioBitrateProfile(profile) {
@@ -14383,6 +14704,9 @@ class ScreenShareManager extends AudioDeviceManager {
14383
14704
  if (!this.state.audioEnabled) {
14384
14705
  constraints.audio = false;
14385
14706
  }
14707
+ // Ensure the call's media factory exists before capture so the resulting
14708
+ // track is owned by it (the WebRTC globals resolve to the live factory).
14709
+ await this.call.ensureMediaFactory();
14386
14710
  const stream = await getScreenShareStream(constraints, this.call.tracer);
14387
14711
  const [track] = stream.getVideoTracks();
14388
14712
  const { contentHint } = this.state.settings || {};
@@ -14487,10 +14811,14 @@ class SpeakerManager {
14487
14811
  this.devicePersistence = devicePreferences;
14488
14812
  this.setup();
14489
14813
  }
14490
- apply(settings) {
14491
- return isReactNative() ? this.applyRN(settings) : this.applyWeb();
14814
+ async apply(settings) {
14815
+ if (isReactNative()) {
14816
+ this.applyRN(settings);
14817
+ return;
14818
+ }
14819
+ await this.applyWeb();
14492
14820
  }
14493
- applyWeb() {
14821
+ async applyWeb() {
14494
14822
  const { enabled, storageKey } = this.devicePersistence;
14495
14823
  if (!enabled)
14496
14824
  return;
@@ -14502,8 +14830,22 @@ class SpeakerManager {
14502
14830
  const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
14503
14831
  ? ''
14504
14832
  : preference.selectedDeviceId;
14505
- if (this.state.selectedDevice !== nextDeviceId) {
14506
- this.select(nextDeviceId);
14833
+ if (!nextDeviceId) {
14834
+ if (this.state.selectedDevice !== nextDeviceId) {
14835
+ this.select(nextDeviceId);
14836
+ }
14837
+ return;
14838
+ }
14839
+ const permissionState = await firstValueFrom(getAudioBrowserPermission(this.call.tracer).asStateObservable());
14840
+ if (permissionState !== 'granted')
14841
+ return;
14842
+ const devices = await firstValueFrom(this.listDevices());
14843
+ const device = this.findDevice(devices, nextDeviceId) ??
14844
+ (preference.selectedDeviceLabel
14845
+ ? devices.find((d) => d.label === preference.selectedDeviceLabel)
14846
+ : undefined);
14847
+ if (device && this.state.selectedDevice !== device.deviceId) {
14848
+ this.select(device.deviceId);
14507
14849
  }
14508
14850
  }
14509
14851
  applyRN(settings) {
@@ -14532,6 +14874,7 @@ class SpeakerManager {
14532
14874
  globalThis.streamRNVideoSDK?.callManager.setup({
14533
14875
  defaultDevice,
14534
14876
  isRingingTypeCall: this.call.ringing,
14877
+ cid: this.call.cid,
14535
14878
  });
14536
14879
  }
14537
14880
  }
@@ -14540,11 +14883,15 @@ class SpeakerManager {
14540
14883
  return;
14541
14884
  this.areSubscriptionsSetUp = true;
14542
14885
  if (deviceIds$ && !isReactNative()) {
14543
- this.subscriptions.push(createSubscription(combineLatest([deviceIds$, this.state.selectedDevice$]), ([devices, deviceId]) => {
14886
+ this.subscriptions.push(createSubscription(combineLatest([
14887
+ deviceIds$.pipe(pairwise()),
14888
+ this.state.selectedDevice$,
14889
+ ]), ([[prevDevices, currentDevices], deviceId]) => {
14544
14890
  if (!deviceId)
14545
14891
  return;
14546
- const device = devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
14547
- if (!device)
14892
+ const isDisconnected = this.findDevice(prevDevices, deviceId) &&
14893
+ !this.findDevice(currentDevices, deviceId);
14894
+ if (isDisconnected)
14548
14895
  this.select('');
14549
14896
  }));
14550
14897
  }
@@ -14591,6 +14938,7 @@ class SpeakerManager {
14591
14938
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
14592
14939
  this.subscriptions = [];
14593
14940
  this.areSubscriptionsSetUp = false;
14941
+ this.defaultDevice = undefined;
14594
14942
  };
14595
14943
  /**
14596
14944
  * Set the volume of the audio elements
@@ -14625,6 +14973,7 @@ class SpeakerManager {
14625
14973
  return { audioVolume: volume };
14626
14974
  });
14627
14975
  }
14976
+ findDevice = (devices, deviceId) => devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
14628
14977
  persistSpeakerDevicePreference(selectedDevice) {
14629
14978
  const { storageKey } = this.devicePersistence;
14630
14979
  const devices = getCurrentValue(this.listDevices()) || [];
@@ -14707,6 +15056,7 @@ class Call {
14707
15056
  blockedAudioTracker;
14708
15057
  subscriber;
14709
15058
  publisher;
15059
+ e2eeManager;
14710
15060
  /**
14711
15061
  * Flag telling whether this call is a "ringing" call.
14712
15062
  */
@@ -14762,6 +15112,8 @@ class Call {
14762
15112
  allowOwnTracksLoopback = false;
14763
15113
  hasJoinedOnce = false;
14764
15114
  deviceSettingsAppliedOnce = false;
15115
+ callManagerStarted = false;
15116
+ leaveGeneration = 0;
14765
15117
  credentials;
14766
15118
  initialized = false;
14767
15119
  acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
@@ -14780,6 +15132,13 @@ class Call {
14780
15132
  clientCapabilities = new Set([
14781
15133
  ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
14782
15134
  ]);
15135
+ /**
15136
+ * The in-flight per-call media engine. On web/React this resolves to a thin
15137
+ * globals-backed engine (no provider registered); React Native registers a
15138
+ * provider that owns a per-call native factory.
15139
+ * @internal
15140
+ */
15141
+ mediaEnginePromise;
14783
15142
  /**
14784
15143
  * Constructs a new `Call` instance.
14785
15144
  *
@@ -15049,6 +15408,7 @@ class Call {
15049
15408
  if (callingState === CallingState.LEFT) {
15050
15409
  return;
15051
15410
  }
15411
+ this.leaveGeneration += 1;
15052
15412
  if (callingState === CallingState.JOINING) {
15053
15413
  const waitUntilCallJoined = () => {
15054
15414
  return new Promise((resolve) => {
@@ -15135,12 +15495,14 @@ class Call {
15135
15495
  this.clientStore.unregisterCall(this);
15136
15496
  globalThis.streamRNVideoSDK?.callManager.stop({
15137
15497
  isRingingTypeCall: this.ringing,
15498
+ shouldStopCallManager: this.callManagerStarted,
15138
15499
  });
15139
15500
  this.camera.dispose();
15140
15501
  this.microphone.dispose();
15141
15502
  this.screenShare.dispose();
15142
15503
  this.speaker.dispose();
15143
15504
  this.deviceSettingsAppliedOnce = false;
15505
+ this.callManagerStarted = false;
15144
15506
  const stopOnLeavePromises = [];
15145
15507
  if (this.camera.stopOnLeave) {
15146
15508
  stopOnLeavePromises.push(this.camera.disable(true));
@@ -15152,6 +15514,22 @@ class Call {
15152
15514
  stopOnLeavePromises.push(this.screenShare.disable(true));
15153
15515
  }
15154
15516
  await Promise.all(stopOnLeavePromises);
15517
+ // Dispose the per-call media engine last — after peer connections and
15518
+ // local tracks are gone — so the backing factory tears down with no
15519
+ // owned PCs/tracks. A fresh `join()` builds a new engine.
15520
+ if (this.mediaEnginePromise) {
15521
+ const enginePromise = this.mediaEnginePromise;
15522
+ this.mediaEnginePromise = undefined;
15523
+ this.logger.debug('Disposing per-call media factory');
15524
+ await enginePromise
15525
+ .then((engine) => {
15526
+ globalThis.streamRNVideoSDK?.callingX?.unwireAudioEngineSubscription();
15527
+ return engine.dispose();
15528
+ })
15529
+ .catch((err) => {
15530
+ this.logger.warn('Failed to dispose media engine', err);
15531
+ });
15532
+ }
15155
15533
  });
15156
15534
  };
15157
15535
  /**
@@ -15252,11 +15630,7 @@ class Call {
15252
15630
  this.clientStore.registerOrUpdateCall(this);
15253
15631
  }
15254
15632
  // Skip speaker setup on RN if ringing was requested or the call is already ringing
15255
- const skipSpeakerApply = isReactNative()
15256
- ? params?.ring === true
15257
- ? true
15258
- : this.ringing
15259
- : false;
15633
+ const skipSpeakerApply = isReactNative();
15260
15634
  await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
15261
15635
  return response;
15262
15636
  };
@@ -15279,11 +15653,7 @@ class Call {
15279
15653
  this.clientStore.registerOrUpdateCall(this);
15280
15654
  }
15281
15655
  // Skip speaker setup on RN if ringing was requested or the call is already ringing
15282
- const skipSpeakerApply = isReactNative()
15283
- ? data?.ring === true
15284
- ? true
15285
- : this.ringing
15286
- : false;
15656
+ const skipSpeakerApply = isReactNative();
15287
15657
  await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
15288
15658
  return response;
15289
15659
  };
@@ -15432,9 +15802,20 @@ class Call {
15432
15802
  doJoin = async (data) => {
15433
15803
  const connectStartTime = Date.now();
15434
15804
  const callingState = this.state.callingState;
15805
+ const joinLeaveGeneration = this.leaveGeneration;
15806
+ const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
15435
15807
  this.joinCallData = data;
15436
15808
  this.logger.debug('Starting join flow');
15437
15809
  this.state.setCallingState(CallingState.JOINING);
15810
+ // Ensure the per-call media engine exists before any peer connection
15811
+ // (codec probe, subscriber, publisher) or capture happens, so the WebRTC
15812
+ // globals resolve to the call's factory. Idempotent across
15813
+ // reconnect/migration attempts.
15814
+ await this.ensureMediaFactory();
15815
+ const callingX = globalThis.streamRNVideoSDK?.callingX;
15816
+ if (callingX) {
15817
+ callingX.wireAudioEngineSubscription();
15818
+ }
15438
15819
  const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
15439
15820
  const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
15440
15821
  const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
@@ -15489,6 +15870,11 @@ class Call {
15489
15870
  // these are throw-away SDPs that the SFU will use to determine
15490
15871
  // the capabilities of the client (codec support, etc.)
15491
15872
  const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } = this.clientPublishOptions || {};
15873
+ // skip if a leave superseded this join so codec detection doesn't resolve to a default factory.
15874
+ if (supersededByLeave()) {
15875
+ this.logger.debug('Join superseded by leave; skipping codec detection');
15876
+ return;
15877
+ }
15492
15878
  const [subscriberSdp, publisherSdp] = await Promise.all([
15493
15879
  getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
15494
15880
  getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
@@ -15532,6 +15918,12 @@ class Call {
15532
15918
  throw error;
15533
15919
  }
15534
15920
  }
15921
+ // If the user left while this join was in flight, bail before re-setting JOINED and before
15922
+ // peer-connection setup below (both run synchronously after this, so one check covers them).
15923
+ if (supersededByLeave()) {
15924
+ this.logger.debug('Join superseded by leave; aborting join flow');
15925
+ return;
15926
+ }
15535
15927
  if (!performingMigration) {
15536
15928
  // in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
15537
15929
  this.state.setCallingState(CallingState.JOINED);
@@ -15569,12 +15961,18 @@ class Call {
15569
15961
  }
15570
15962
  // device settings should be applied only once, we don't have to
15571
15963
  // re-apply them on later reconnections or server-side data fetches
15572
- if (!this.deviceSettingsAppliedOnce && this.state.settings) {
15964
+ if (!this.deviceSettingsAppliedOnce &&
15965
+ this.state.settings &&
15966
+ !supersededByLeave()) {
15573
15967
  await this.applyDeviceConfig(this.state.settings, true, false);
15968
+ this.deviceSettingsAppliedOnce = true;
15969
+ }
15970
+ if (!this.callManagerStarted && !supersededByLeave()) {
15574
15971
  globalThis.streamRNVideoSDK?.callManager.start({
15575
15972
  isRingingTypeCall: this.ringing,
15973
+ cid: this.cid,
15576
15974
  });
15577
- this.deviceSettingsAppliedOnce = true;
15975
+ this.callManagerStarted = true;
15578
15976
  }
15579
15977
  // We shouldn't persist the `ring` and `notify` state after joining the call
15580
15978
  // as it's a one-time event and clashes with the potential reconnection attempts.
@@ -15701,6 +16099,7 @@ class Call {
15701
16099
  this.sfuStatsReporter = undefined;
15702
16100
  if (closePreviousInstances && this.subscriber) {
15703
16101
  await this.subscriber.dispose();
16102
+ this.state.removeAllOrphanedTracks();
15704
16103
  }
15705
16104
  const basePeerConnectionOptions = {
15706
16105
  sfuClient,
@@ -15711,6 +16110,7 @@ class Call {
15711
16110
  enableTracing,
15712
16111
  statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
15713
16112
  clientPublishOptions: this.clientPublishOptions,
16113
+ e2ee: this.e2eeManager,
15714
16114
  onReconnectionNeeded: (kind, reason, peerType) => {
15715
16115
  this.reconnect(kind, reason).catch((err) => {
15716
16116
  const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
@@ -15776,12 +16176,12 @@ class Call {
15776
16176
  * Retrieves credentials for joining the call.
15777
16177
  *
15778
16178
  * @internal
15779
- *
15780
16179
  * @param data the join call data.
15781
16180
  */
15782
16181
  doJoinRequest = async (data) => {
15783
16182
  const location = await this.streamClient.getLocationHint();
15784
- const request = { ...data, location };
16183
+ const e2ee = !!this.e2eeManager;
16184
+ const request = { ...data, location, e2ee };
15785
16185
  const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
15786
16186
  this.state.updateFromCallResponse(joinResponse.call);
15787
16187
  this.state.setMembers(joinResponse.members);
@@ -15800,6 +16200,36 @@ class Call {
15800
16200
  }
15801
16201
  return joinResponse;
15802
16202
  };
16203
+ /**
16204
+ * Whether the per-call media engine currently exists. True from join until leave.
16205
+ *
16206
+ * @internal an internal getter and should not be used outside the SDK.
16207
+ */
16208
+ get hasMediaEngine() {
16209
+ return !!this.mediaEnginePromise;
16210
+ }
16211
+ /**
16212
+ * Ensures a {@link CallMediaEngine} exists for this call's media session and
16213
+ * returns it. Idempotent: the engine is created once via the registered
16214
+ * provider (see `setCallMediaEngineProvider`) and cached until `leave()`
16215
+ * disposes it. Concurrent callers (e.g. camera + microphone enabling in
16216
+ * parallel) share the same engine because the in-flight creation promise is
16217
+ * cached, never the unresolved result.
16218
+ *
16219
+ * @internal
16220
+ */
16221
+ ensureMediaFactory = async () => {
16222
+ if (!this.mediaEnginePromise) {
16223
+ const provider = getCallMediaEngineProvider();
16224
+ this.logger.debug(`Requesting per-call media factory creation`);
16225
+ this.mediaEnginePromise = Promise.resolve(provider()).catch((err) => {
16226
+ // Drop the cached rejection so a retried join() can rebuild the engine
16227
+ this.mediaEnginePromise = undefined;
16228
+ throw err;
16229
+ });
16230
+ }
16231
+ return this.mediaEnginePromise;
16232
+ };
15803
16233
  /**
15804
16234
  * Handles the closing of the SFU signal connection.
15805
16235
  *
@@ -16361,6 +16791,30 @@ class Call {
16361
16791
  this.tracer.trace('updatePublishOptions', options);
16362
16792
  this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
16363
16793
  };
16794
+ /**
16795
+ * Set the E2EE (end-to-end encryption) manager for this call.
16796
+ *
16797
+ * Must be called before {@link join} so the RTCPeerConnection can be
16798
+ * configured for E2EE.
16799
+ *
16800
+ * The manager is kept across {@link leave} so a rejoin of this same instance
16801
+ * stays encrypted: do not dispose it while this call may be joined again.
16802
+ * A disposed manager throws from `encrypt`/`decrypt` rather than silently
16803
+ * publishing nothing, so re-attach a fresh one instead of reusing it.
16804
+ *
16805
+ * @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
16806
+ * built-in AES-GCM scheme, or pass your own implementation.
16807
+ * @throws if called after the peer connections have been built (i.e. after
16808
+ * `join`): those PCs were already configured without an encryptor, so
16809
+ * adopting a manager now would silently publish/receive cleartext for
16810
+ * the live session.
16811
+ */
16812
+ setE2EEManager = (e2ee) => {
16813
+ if (this.publisher || this.subscriber) {
16814
+ throw new Error('setE2EEManager must be called before join()');
16815
+ }
16816
+ this.e2eeManager = e2ee;
16817
+ };
16364
16818
  /**
16365
16819
  * Notifies the SFU that a noise cancellation process has started.
16366
16820
  *
@@ -16997,7 +17451,9 @@ class Call {
16997
17451
  */
16998
17452
  applyDeviceConfig = async (settings, publish, skipSpeakerApply) => {
16999
17453
  if (!skipSpeakerApply) {
17000
- this.speaker.apply(settings);
17454
+ await this.speaker.apply(settings).catch((err) => {
17455
+ this.logger.warn('Speaker init failed', err);
17456
+ });
17001
17457
  }
17002
17458
  await this.camera.apply(settings.video, publish).catch((err) => {
17003
17459
  this.logger.warn('Camera init failed', err);
@@ -17678,15 +18134,17 @@ class StableWSConnection {
17678
18134
  this.client.logger.error(`Token not set, can't connect authenticate`);
17679
18135
  return;
17680
18136
  }
17681
- const authMessage = JSON.stringify({
18137
+ const wsAuthMessage = {
17682
18138
  token,
18139
+ products: [WSAuthMessageProductsEnum.VIDEO],
17683
18140
  user_details: {
17684
18141
  id: user.id,
17685
18142
  name: user.name,
17686
18143
  image: user.image,
17687
18144
  custom: user.custom,
17688
18145
  },
17689
- });
18146
+ };
18147
+ const authMessage = JSON.stringify(wsAuthMessage);
17690
18148
  this._log(`onopen() - Sending auth message ${authMessage}`, {}, 'trace');
17691
18149
  this.ws?.send(authMessage);
17692
18150
  this._log('onopen() - onopen callback', { wsID });
@@ -18512,11 +18970,11 @@ class StreamClient {
18512
18970
  return await this.wsConnection.connect(this.defaultWSTimeout);
18513
18971
  };
18514
18972
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18515
- "1.57.0";
18973
+ "1.58.0";
18516
18974
  getUserAgent = () => {
18517
18975
  if (!this.cachedUserAgent) {
18518
18976
  const { clientAppIdentifier = {} } = this.options;
18519
- const { sdkName = 'js', sdkVersion = "1.57.0", ...extras } = clientAppIdentifier;
18977
+ const { sdkName = 'js', sdkVersion = "1.58.0", ...extras } = clientAppIdentifier;
18520
18978
  this.cachedUserAgent = [
18521
18979
  `stream-video-${sdkName}-v${sdkVersion}`,
18522
18980
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -19749,5 +20207,323 @@ const humanize = (n) => {
19749
20207
  return String(n);
19750
20208
  };
19751
20209
 
19752
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, 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, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, 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, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
20210
+ /**
20211
+ * Tiny, type-safe event emitter.
20212
+ *
20213
+ * Usage styles:
20214
+ * - compose: `private readonly events = new TypedEventEmitter<MyMap>()`
20215
+ * - extend: `class Foo extends TypedEventEmitter<MyMap> {}`
20216
+ *
20217
+ * Listener exceptions (sync throw or rejected promise) are caught and logged,
20218
+ * so one bad listener cannot break dispatch for the rest.
20219
+ */
20220
+ class TypedEventEmitter {
20221
+ logger;
20222
+ byEvent = new Map();
20223
+ constructor(logger = 'TypedEventEmitter') {
20224
+ this.logger =
20225
+ typeof logger === 'string' ? videoLoggerSystem.getLogger(logger) : logger;
20226
+ }
20227
+ on = (event, fn) => {
20228
+ let listeners = this.byEvent.get(event);
20229
+ if (!listeners) {
20230
+ listeners = new Set();
20231
+ this.byEvent.set(event, listeners);
20232
+ }
20233
+ listeners.add(fn);
20234
+ return () => this.off(event, fn);
20235
+ };
20236
+ off = (event, fn) => {
20237
+ const listeners = this.byEvent.get(event);
20238
+ if (!listeners)
20239
+ return;
20240
+ listeners.delete(fn);
20241
+ if (listeners.size === 0)
20242
+ this.byEvent.delete(event);
20243
+ };
20244
+ emit = (event, payload) => {
20245
+ const listeners = this.byEvent.get(event);
20246
+ if (!listeners || listeners.size === 0)
20247
+ return;
20248
+ for (const listener of [...listeners]) {
20249
+ this.invoke(() => listener(payload), event);
20250
+ }
20251
+ };
20252
+ removeAllListeners = (event) => {
20253
+ if (event === undefined) {
20254
+ this.byEvent.clear();
20255
+ }
20256
+ else {
20257
+ this.byEvent.delete(event);
20258
+ }
20259
+ };
20260
+ invoke = (run, event) => {
20261
+ try {
20262
+ const result = run();
20263
+ if (result && typeof result.then === 'function') {
20264
+ result.catch((err) => {
20265
+ this.logger.warn(`Listener for '${String(event)}' rejected`, err);
20266
+ });
20267
+ }
20268
+ }
20269
+ catch (err) {
20270
+ this.logger.warn(`Listener for '${String(event)}' threw`, err);
20271
+ }
20272
+ };
20273
+ }
20274
+
20275
+ /**
20276
+ * Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
20277
+ * transforms to RTCRtpSenders and RTCRtpReceivers.
20278
+ */
20279
+ class EncryptionManager extends TypedEventEmitter {
20280
+ algorithm;
20281
+ transform;
20282
+ disposed = false;
20283
+ piped;
20284
+ userId;
20285
+ worker;
20286
+ workerUrl;
20287
+ constructor(userId, worker, workerUrl, algorithm, transform) {
20288
+ super('EncryptionManager');
20289
+ this.userId = userId;
20290
+ this.worker = worker;
20291
+ this.workerUrl = workerUrl;
20292
+ this.algorithm = algorithm;
20293
+ this.transform = transform;
20294
+ this.worker.addEventListener('message', this.handleWorkerMessage);
20295
+ this.worker.addEventListener('error', this.handleWorkerError);
20296
+ }
20297
+ /**
20298
+ * Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
20299
+ * Use it to guard UI, or to avoid calling {@link create} where it would throw.
20300
+ */
20301
+ static isSupported = () => {
20302
+ return preferredTransform() !== undefined;
20303
+ };
20304
+ /**
20305
+ * Create an EncryptionManager instance and initialize the E2EE Web Worker.
20306
+ *
20307
+ * @param userId - The local user's ID, normally `call.currentUserId`.
20308
+ * @param options - the create options.
20309
+ * @throws {Error} If the browser lacks Encoded Transforms.
20310
+ *
20311
+ * @example
20312
+ * ```ts
20313
+ * if (EncryptionManager.isSupported()) {
20314
+ * const e2ee = await EncryptionManager.create(call.currentUserId);
20315
+ * call.setE2EEManager(e2ee);
20316
+ * e2ee.setSharedKey(0, keyBytes);
20317
+ * }
20318
+ */
20319
+ static create = async (userId, options) => {
20320
+ const transform = preferredTransform();
20321
+ if (!transform) {
20322
+ throw new Error(`E2EE is not supported in this browser`);
20323
+ }
20324
+ const { e2eeWorker } = await import('./e2ee-worker.browser.es.js');
20325
+ const blob = new Blob([`(${e2eeWorker.toString()})()`], {
20326
+ type: 'application/javascript',
20327
+ });
20328
+ const url = URL.createObjectURL(blob);
20329
+ let worker;
20330
+ try {
20331
+ worker = new Worker(url, { name: 'stream-video-e2ee' });
20332
+ }
20333
+ catch (err) {
20334
+ // e.g. a CSP `worker-src` without `blob:`. Don't leak the object URL.
20335
+ URL.revokeObjectURL(url);
20336
+ throw err;
20337
+ }
20338
+ const algorithm = options?.algorithm ?? 'AES-128-GCM';
20339
+ return new EncryptionManager(userId, worker, url, algorithm, transform);
20340
+ };
20341
+ /**
20342
+ * {@link dispose} terminates the worker, so `postMessage` becomes a silent
20343
+ * no-op and an attached transform points at a dead worker: frames would stall
20344
+ * forever with no error and no event. Throwing is also fail-closed, since a
20345
+ * caller that swallows it still publishes nothing rather than cleartext.
20346
+ */
20347
+ assertUsable = () => {
20348
+ if (this.disposed)
20349
+ throw new Error(`EncryptionManager is disposed`);
20350
+ };
20351
+ /**
20352
+ * Terminate the worker and release all resources.
20353
+ *
20354
+ * The manager is unusable afterwards and every other method throws. Call
20355
+ * {@link create} for a new one. Safe to call more than once.
20356
+ */
20357
+ dispose = () => {
20358
+ if (this.disposed)
20359
+ return;
20360
+ this.disposed = true;
20361
+ this.piped = undefined;
20362
+ this.worker.removeEventListener('message', this.handleWorkerMessage);
20363
+ this.worker.removeEventListener('error', this.handleWorkerError);
20364
+ this.worker.terminate();
20365
+ URL.revokeObjectURL(this.workerUrl);
20366
+ this.removeAllListeners();
20367
+ };
20368
+ /**
20369
+ * Set a per-user AES-GCM encryption key in the worker's key store.
20370
+ *
20371
+ * Use it when each participant has their own key from a central authority.
20372
+ * The receiver picks the right one by the `keyIndex` in the frame trailer.
20373
+ *
20374
+ * @param userId - The key owner.
20375
+ * @param keyIndex - Increases with each rotation.
20376
+ * @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
20377
+ */
20378
+ setKey = (userId, keyIndex, rawKey) => {
20379
+ this.assertUsable();
20380
+ this.validateKeyIndex(keyIndex);
20381
+ this.validateKeyLength(rawKey);
20382
+ this.worker.postMessage({ type: 'cmd.set_key', userId, keyIndex, rawKey });
20383
+ };
20384
+ /**
20385
+ * Fallback key for any user without a per-user key. The simplest E2EE mode:
20386
+ * one key for everyone, usually passphrase-derived, no distribution needed.
20387
+ * Setting an epoch makes it active for encryption while older epochs remain
20388
+ * available to decrypt in-flight frames until {@link removeSharedKey}.
20389
+ *
20390
+ * @param keyIndex - An integer 0-255, since one trailer byte carries it.
20391
+ * @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
20392
+ */
20393
+ setSharedKey = (keyIndex, rawKey) => {
20394
+ this.assertUsable();
20395
+ this.validateKeyIndex(keyIndex);
20396
+ this.validateKeyLength(rawKey);
20397
+ this.worker.postMessage({ type: 'cmd.set_shared_key', keyIndex, rawKey });
20398
+ };
20399
+ /**
20400
+ * Remove one shared-key epoch from the worker's receive key ring.
20401
+ *
20402
+ * If this is the active epoch, shared-key encryption stops until
20403
+ * {@link setSharedKey} succeeds again. An older epoch is not reactivated.
20404
+ *
20405
+ * @param keyIndex - The exact shared-key epoch to remove.
20406
+ */
20407
+ removeSharedKey = (keyIndex) => {
20408
+ this.assertUsable();
20409
+ this.validateKeyIndex(keyIndex);
20410
+ this.worker.postMessage({ type: 'cmd.remove_shared_key', keyIndex });
20411
+ };
20412
+ /**
20413
+ * Retire one of a user's key epochs, leaving their other epochs usable.
20414
+ *
20415
+ * @param userId - The key owner.
20416
+ * @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
20417
+ */
20418
+ removeKey = (userId, keyIndex) => {
20419
+ this.assertUsable();
20420
+ this.validateKeyIndex(keyIndex);
20421
+ this.worker.postMessage({ type: 'cmd.remove_key', userId, keyIndex });
20422
+ };
20423
+ /**
20424
+ * Drop every key a user holds, revoking their ability to decrypt later
20425
+ * frames. Call it when a participant leaves.
20426
+ *
20427
+ * To retire one rotated epoch instead, use {@link removeKey}.
20428
+ */
20429
+ removeAllKeys = (userId) => {
20430
+ this.assertUsable();
20431
+ this.worker.postMessage({ type: 'cmd.remove_all_keys', userId });
20432
+ };
20433
+ /**
20434
+ * Called by the Publisher when it adds a transceiver.
20435
+ *
20436
+ * @param sender - The sender to encrypt.
20437
+ * @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
20438
+ * @param trackType - Optional label; only groups perf stats.
20439
+ * @internal
20440
+ */
20441
+ encrypt = (sender, codec, trackType) => {
20442
+ this.assertUsable();
20443
+ this.pipe(sender, {
20444
+ operation: 'encode',
20445
+ userId: this.userId,
20446
+ codec,
20447
+ trackType,
20448
+ });
20449
+ };
20450
+ /**
20451
+ * Called by the Subscriber when a remote track arrives.
20452
+ *
20453
+ * @param receiver - The receiver to decrypt.
20454
+ * @param userId - The remote user, for key lookup in the worker.
20455
+ * @param trackType - Optional label; only groups perf stats.
20456
+ * @internal
20457
+ */
20458
+ decrypt = (receiver, userId, trackType) => {
20459
+ this.assertUsable();
20460
+ this.pipe(receiver, { operation: 'decode', userId, trackType });
20461
+ };
20462
+ /** Pipe through the worker's transform, tracking targets to avoid double-piping. */
20463
+ pipe = (target, options) => {
20464
+ if (this.transform === 'script') {
20465
+ target.transform = new RTCRtpScriptTransform(this.worker, options);
20466
+ return;
20467
+ }
20468
+ if ((this.piped ??= new WeakSet()).has(target))
20469
+ return;
20470
+ this.piped.add(target);
20471
+ // @ts-expect-error createEncodedStreams is not in the standard typedefs
20472
+ const { readable, writable } = target.createEncodedStreams();
20473
+ this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
20474
+ };
20475
+ /**
20476
+ * Toggle periodic performance reporting from the E2EE worker.
20477
+ *
20478
+ * While on, the worker emits `e2ee.perf_report` once per second with per-track
20479
+ * FPS and crypto timings. Useful for debugging throughput.
20480
+ */
20481
+ enablePerformanceReporting = (enabled) => {
20482
+ this.assertUsable();
20483
+ this.worker.postMessage({
20484
+ type: 'cmd.enable_performance_reporting',
20485
+ enabled,
20486
+ });
20487
+ };
20488
+ /**
20489
+ * Request a snapshot of the worker's keys. It arrives later as the
20490
+ * `e2ee.key_state` event, listing fingerprints only, never key material.
20491
+ */
20492
+ requestKeyState = () => {
20493
+ this.assertUsable();
20494
+ this.worker.postMessage({ type: 'cmd.request_key_state' });
20495
+ };
20496
+ handleWorkerMessage = (e) => {
20497
+ const { type, ...payload } = e.data ?? {};
20498
+ if (type === 'e2ee.error') {
20499
+ this.logger.error(e.data.message);
20500
+ return;
20501
+ }
20502
+ const event = type;
20503
+ this.logger.debug('Dispatching', event, payload);
20504
+ this.emit(event, payload);
20505
+ };
20506
+ handleWorkerError = (e) => {
20507
+ this.logger.error('Unhandled worker error:', e.message);
20508
+ };
20509
+ validateKeyLength = (rawKey) => {
20510
+ const is256 = this.algorithm === 'AES-256-GCM';
20511
+ const expected = is256 ? 32 : 16;
20512
+ if (rawKey.byteLength !== expected) {
20513
+ throw new Error(`Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`);
20514
+ }
20515
+ };
20516
+ /**
20517
+ * One trailer byte carries the keyIndex. A larger value would truncate to
20518
+ * `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
20519
+ * every decrypt. Reject it rather than ship a silently broken key epoch.
20520
+ */
20521
+ validateKeyIndex = (keyIndex) => {
20522
+ if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
20523
+ throw new Error(`keyIndex must be an integer between 0 and 255, got ${keyIndex}`);
20524
+ }
20525
+ };
20526
+ }
20527
+
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 };
19753
20529
  //# sourceMappingURL=index.browser.es.js.map