@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
package/dist/index.cjs.js CHANGED
@@ -87,6 +87,22 @@ const CreateDeviceRequestPushProviderEnum = {
87
87
  HUAWEI: 'huawei',
88
88
  XIAOMI: 'xiaomi',
89
89
  };
90
+ /**
91
+ * @export
92
+ */
93
+ const EncryptionSettingsRequestModeEnum = {
94
+ AVAILABLE: 'available',
95
+ DISABLED: 'disabled',
96
+ AUTO_ON: 'auto-on',
97
+ };
98
+ /**
99
+ * @export
100
+ */
101
+ const EncryptionSettingsResponseModeEnum = {
102
+ AVAILABLE: 'available',
103
+ DISABLED: 'disabled',
104
+ AUTO_ON: 'auto-on',
105
+ };
90
106
  /**
91
107
  * @export
92
108
  */
@@ -113,6 +129,21 @@ const FrameRecordingSettingsResponseModeEnum = {
113
129
  DISABLED: 'disabled',
114
130
  AUTO_ON: 'auto-on',
115
131
  };
132
+ /**
133
+ * @export
134
+ */
135
+ const HLSSettingsRequestQualityTracksEnum = {
136
+ _360P: '360p',
137
+ _480P: '480p',
138
+ _720P: '720p',
139
+ _1080P: '1080p',
140
+ _1440P: '1440p',
141
+ PORTRAIT_360X640: 'portrait-360x640',
142
+ PORTRAIT_480X854: 'portrait-480x854',
143
+ PORTRAIT_720X1280: 'portrait-720x1280',
144
+ PORTRAIT_1080X1920: 'portrait-1080x1920',
145
+ PORTRAIT_1440X2560: 'portrait-1440x2560',
146
+ };
116
147
  /**
117
148
  * @export
118
149
  */
@@ -121,6 +152,17 @@ const IndividualRecordingSettingsRequestModeEnum = {
121
152
  DISABLED: 'disabled',
122
153
  AUTO_ON: 'auto-on',
123
154
  };
155
+ /**
156
+ * @export
157
+ */
158
+ const IndividualRecordingSettingsRequestOutputTypesEnum = {
159
+ AUDIO_ONLY: 'audio_only',
160
+ VIDEO_ONLY: 'video_only',
161
+ AUDIO_VIDEO: 'audio_video',
162
+ SCREENSHARE_AUDIO_ONLY: 'screenshare_audio_only',
163
+ SCREENSHARE_VIDEO_ONLY: 'screenshare_video_only',
164
+ SCREENSHARE_AUDIO_VIDEO: 'screenshare_audio_video',
165
+ };
124
166
  /**
125
167
  * @export
126
168
  */
@@ -279,6 +321,14 @@ const RecordSettingsRequestQualityEnum = {
279
321
  PORTRAIT_1080X1920: 'portrait-1080x1920',
280
322
  PORTRAIT_1440X2560: 'portrait-1440x2560',
281
323
  };
324
+ /**
325
+ * @export
326
+ */
327
+ const RequestPermissionRequestPermissionsEnum = {
328
+ SCREENSHARE: 'screenshare',
329
+ SEND_AUDIO: 'send-audio',
330
+ SEND_VIDEO: 'send-video',
331
+ };
282
332
  /**
283
333
  * @export
284
334
  */
@@ -479,6 +529,59 @@ const TranscriptionSettingsResponseModeEnum = {
479
529
  DISABLED: 'disabled',
480
530
  AUTO_ON: 'auto-on',
481
531
  };
532
+ /**
533
+ * @export
534
+ */
535
+ const TranslationSettingsLanguagesEnum = {
536
+ EN: 'en',
537
+ FR: 'fr',
538
+ ES: 'es',
539
+ DE: 'de',
540
+ IT: 'it',
541
+ NL: 'nl',
542
+ PT: 'pt',
543
+ PL: 'pl',
544
+ CA: 'ca',
545
+ CS: 'cs',
546
+ DA: 'da',
547
+ EL: 'el',
548
+ FI: 'fi',
549
+ ID: 'id',
550
+ JA: 'ja',
551
+ RU: 'ru',
552
+ SV: 'sv',
553
+ TA: 'ta',
554
+ TH: 'th',
555
+ TR: 'tr',
556
+ HU: 'hu',
557
+ RO: 'ro',
558
+ ZH: 'zh',
559
+ AR: 'ar',
560
+ TL: 'tl',
561
+ HE: 'he',
562
+ HI: 'hi',
563
+ HR: 'hr',
564
+ KO: 'ko',
565
+ MS: 'ms',
566
+ NO: 'no',
567
+ UK: 'uk',
568
+ };
569
+ /**
570
+ * @export
571
+ */
572
+ const UpdateUserPermissionsRequestGrantPermissionsEnum = {
573
+ SCREENSHARE: 'screenshare',
574
+ SEND_AUDIO: 'send-audio',
575
+ SEND_VIDEO: 'send-video',
576
+ };
577
+ /**
578
+ * @export
579
+ */
580
+ const UpdateUserPermissionsRequestRevokePermissionsEnum = {
581
+ SCREENSHARE: 'screenshare',
582
+ SEND_AUDIO: 'send-audio',
583
+ SEND_VIDEO: 'send-video',
584
+ };
482
585
  /**
483
586
  * @export
484
587
  */
@@ -495,6 +598,14 @@ const VideoSettingsResponseCameraFacingEnum = {
495
598
  BACK: 'back',
496
599
  EXTERNAL: 'external',
497
600
  };
601
+ /**
602
+ * @export
603
+ */
604
+ const WSAuthMessageProductsEnum = {
605
+ CHAT: 'chat',
606
+ VIDEO: 'video',
607
+ FEEDS: 'feeds',
608
+ };
498
609
 
499
610
  class ErrorFromResponse extends Error {
500
611
  code;
@@ -1492,6 +1603,13 @@ class CallState$Type extends runtime.MessageType {
1492
1603
  repeat: 2 /*RepeatType.UNPACKED*/,
1493
1604
  T: () => Pin,
1494
1605
  },
1606
+ {
1607
+ no: 5,
1608
+ name: 'e2ee_enabled',
1609
+ kind: 'scalar',
1610
+ jsonName: 'e2eeEnabled',
1611
+ T: 8 /*ScalarType.BOOL*/,
1612
+ },
1495
1613
  ]);
1496
1614
  }
1497
1615
  }
@@ -4614,6 +4732,19 @@ const isSvcCodec = (codecOrMimeType) => {
4614
4732
  codecOrMimeType === 'video/av1');
4615
4733
  };
4616
4734
 
4735
+ /**
4736
+ * The default engine: a thin, stateless wrapper over the WebRTC globals.
4737
+ */
4738
+ const defaultGlobalsEngine = {
4739
+ dispose: () => Promise.resolve(true),
4740
+ };
4741
+ const defaultGlobalsEngineProvider = () => defaultGlobalsEngine;
4742
+ let provider = defaultGlobalsEngineProvider;
4743
+ const setCallMediaEngineProvider = (newProvider) => {
4744
+ provider = newProvider;
4745
+ };
4746
+ const getCallMediaEngineProvider = () => provider;
4747
+
4617
4748
  const sfuEventKinds = {
4618
4749
  subscriberOffer: undefined,
4619
4750
  publisherAnswer: undefined,
@@ -4821,7 +4952,9 @@ const parseUfragFromCandidate = (candidate) => {
4821
4952
  };
4822
4953
 
4823
4954
  const ensureExhausted = (x, message) => {
4824
- videoLoggerSystem.getLogger('helpers').warn(message, x);
4955
+ if (message) {
4956
+ videoLoggerSystem.getLogger('helpers').warn(message, x);
4957
+ }
4825
4958
  };
4826
4959
 
4827
4960
  /**
@@ -4946,7 +5079,7 @@ const toIceCandidate = (iceTrickle) => {
4946
5079
  }
4947
5080
  };
4948
5081
 
4949
- const version = "1.57.0";
5082
+ const version = "1.58.0";
4950
5083
  const [major, minor, patch] = version.split('.');
4951
5084
  let sdkInfo = {
4952
5085
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5929,6 +6062,7 @@ class CallState {
5929
6062
  settingsSubject = new rxjs.BehaviorSubject(undefined);
5930
6063
  transcribingSubject = new rxjs.BehaviorSubject(false);
5931
6064
  captioningSubject = new rxjs.BehaviorSubject(false);
6065
+ e2eeEnabledSubject = new rxjs.BehaviorSubject(false);
5932
6066
  endedBySubject = new rxjs.BehaviorSubject(undefined);
5933
6067
  thumbnailsSubject = new rxjs.BehaviorSubject(undefined);
5934
6068
  membersSubject = new rxjs.BehaviorSubject([]);
@@ -6087,6 +6221,11 @@ class CallState {
6087
6221
  * Will provide the closed captioning state of this call.
6088
6222
  */
6089
6223
  captioning$;
6224
+ /**
6225
+ * Whether end-to-end encryption is active for this call, as reported by the
6226
+ * SFU in the join response.
6227
+ */
6228
+ e2eeEnabled$;
6090
6229
  /**
6091
6230
  * Will provide the user who ended this call.
6092
6231
  */
@@ -6188,6 +6327,7 @@ class CallState {
6188
6327
  this.rawRecording$ = duc(this.rawRecordingSubject);
6189
6328
  this.transcribing$ = duc(this.transcribingSubject);
6190
6329
  this.captioning$ = duc(this.captioningSubject);
6330
+ this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
6191
6331
  this.eventHandlers = {
6192
6332
  // these events are not updating the call state:
6193
6333
  'call.frame_recording_ready': undefined,
@@ -6290,6 +6430,7 @@ class CallState {
6290
6430
  clearTimeout(taskId);
6291
6431
  this.closedCaptionsTasks.delete(ccKey);
6292
6432
  }
6433
+ this.removeAllOrphanedTracks();
6293
6434
  };
6294
6435
  /**
6295
6436
  * Sets the list of criteria that are used to sort the participants.
@@ -6639,6 +6780,12 @@ class CallState {
6639
6780
  get transcribing() {
6640
6781
  return this.getCurrentValue(this.transcribing$);
6641
6782
  }
6783
+ /**
6784
+ * Whether end-to-end encryption is active for this call.
6785
+ */
6786
+ get e2eeEnabled() {
6787
+ return this.getCurrentValue(this.e2eeEnabled$);
6788
+ }
6642
6789
  /**
6643
6790
  * Will provide the user who ended this call.
6644
6791
  */
@@ -6859,6 +7006,18 @@ class CallState {
6859
7006
  removeOrphanedTrack = (id) => {
6860
7007
  this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
6861
7008
  };
7009
+ /**
7010
+ * Drops every orphaned track. Call this when the peer connections that own
7011
+ * the stored receivers go away (full leave, reconnect, or migration):
7012
+ * `pc.close()` does not raise the track `ended` event, so the per-track
7013
+ * cleanup never fires and the receivers + their closed PCs would otherwise
7014
+ * leak for the call's lifetime.
7015
+ *
7016
+ * @internal
7017
+ */
7018
+ removeAllOrphanedTracks = () => {
7019
+ this.orphanedTracks = [];
7020
+ };
6862
7021
  /**
6863
7022
  * Takes all orphaned tracks with the given track lookup prefix.
6864
7023
  * All orphaned tracks with the given track lookup prefix are removed from the call state.
@@ -6921,7 +7080,7 @@ class CallState {
6921
7080
  * @param reconnectDetails optional reconnect details.
6922
7081
  */
6923
7082
  updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
6924
- const { participants, participantCount, startedAt, pins } = callState;
7083
+ const { participants, participantCount, startedAt, pins, e2EeEnabled } = callState;
6925
7084
  const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
6926
7085
  this.setParticipants(() => {
6927
7086
  const participantLookup = this.getParticipantLookupBySessionId();
@@ -6947,6 +7106,7 @@ class CallState {
6947
7106
  this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
6948
7107
  this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
6949
7108
  this.setServerSidePins(pins);
7109
+ this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
6950
7110
  };
6951
7111
  updateFromMemberRemoved = (event) => {
6952
7112
  this.updateFromCallResponse(event.call);
@@ -7144,6 +7304,37 @@ class CallState {
7144
7304
  };
7145
7305
  }
7146
7306
 
7307
+ /**
7308
+ * Detection and selection policy for the two WebRTC Encoded Transform APIs.
7309
+ * Internal: which one the SDK attaches is an RTC-layer detail. Consumers want
7310
+ * `EncryptionManager.isSupported` instead.
7311
+ */
7312
+ /**
7313
+ * Chrome only, and the reason an RTCPeerConnection carrying E2EE needs the
7314
+ * non-standard `encodedInsertableStreams` flag.
7315
+ */
7316
+ const hasInsertableStreams = () => typeof RTCRtpSender !== 'undefined' &&
7317
+ 'createEncodedStreams' in RTCRtpSender.prototype;
7318
+ /** Whether the standard `RTCRtpScriptTransform` API exists. */
7319
+ const hasScriptTransform = () => typeof RTCRtpScriptTransform !== 'undefined';
7320
+ /**
7321
+ * Which Encoded Transform API to attach E2EE with here.
7322
+ *
7323
+ * - `'insertable'`: legacy Insertable Streams. Used on Chrome, where
7324
+ * `RTCRtpScriptTransform` is still unreliable for E2EE.
7325
+ * - `'script'`: the standard API. Used everywhere else.
7326
+ * - `undefined`: neither exists, so E2EE cannot run.
7327
+ */
7328
+ const preferredTransform = () => {
7329
+ const insertable = hasInsertableStreams();
7330
+ // Chrome's RTCRtpScriptTransform is still unreliable for E2EE.
7331
+ if (isChrome() && insertable)
7332
+ return 'insertable';
7333
+ if (hasScriptTransform())
7334
+ return 'script';
7335
+ return insertable ? 'insertable' : undefined;
7336
+ };
7337
+
7147
7338
  /**
7148
7339
  * Flatten the stats report into an array of stats objects.
7149
7340
  *
@@ -8252,6 +8443,7 @@ class BasePeerConnection {
8252
8443
  state;
8253
8444
  dispatcher;
8254
8445
  clientPublishOptions;
8446
+ e2ee;
8255
8447
  tag;
8256
8448
  sfuClient;
8257
8449
  onReconnectionNeeded;
@@ -8273,13 +8465,14 @@ class BasePeerConnection {
8273
8465
  /**
8274
8466
  * Constructs a new `BasePeerConnection` instance.
8275
8467
  */
8276
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
8468
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, e2ee, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
8277
8469
  this.peerType = peerType;
8278
8470
  this.sfuClient = sfuClient;
8279
8471
  this.state = state;
8280
8472
  this.dispatcher = dispatcher;
8281
8473
  this.iceRestartDelay = iceRestartDelay;
8282
8474
  this.clientPublishOptions = clientPublishOptions;
8475
+ this.e2ee = e2ee;
8283
8476
  this.tag = tag;
8284
8477
  this.onReconnectionNeeded = onReconnectionNeeded;
8285
8478
  this.onIceConnected = onIceConnected;
@@ -8298,7 +8491,13 @@ class BasePeerConnection {
8298
8491
  }
8299
8492
  }
8300
8493
  createPeerConnection = (connectionConfig) => {
8301
- const pc = new RTCPeerConnection(connectionConfig);
8494
+ const config = { ...connectionConfig };
8495
+ // The legacy Insertable Streams path requires this non-standard flag.
8496
+ if (this.e2ee && hasInsertableStreams()) {
8497
+ // @ts-expect-error not part of the standard lib yet
8498
+ config.encodedInsertableStreams = true;
8499
+ }
8500
+ const pc = new RTCPeerConnection(config);
8302
8501
  pc.addEventListener('icecandidate', this.onIceCandidate);
8303
8502
  pc.addEventListener('icecandidateerror', this.onIceCandidateError);
8304
8503
  pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
@@ -9052,6 +9251,10 @@ class Publisher extends BasePeerConnection {
9052
9251
  toRTCDegradationPreference(publishOption.degradationPreference) ??
9053
9252
  'maintain-framerate';
9054
9253
  await transceiver.sender.setParameters(params);
9254
+ if (this.e2ee) {
9255
+ this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
9256
+ this.logger.debug('E2EE encryptor attached to sender');
9257
+ }
9055
9258
  await this.negotiate();
9056
9259
  };
9057
9260
  /**
@@ -9663,9 +9866,15 @@ class Subscriber extends BasePeerConnection {
9663
9866
  trackLookupPrefix: trackId,
9664
9867
  track: primaryStream,
9665
9868
  trackType,
9869
+ receiver: this.e2ee ? e.receiver : undefined,
9666
9870
  });
9667
9871
  return;
9668
9872
  }
9873
+ else if (this.e2ee) {
9874
+ const { userId } = participantToUpdate;
9875
+ this.e2ee.decrypt(e.receiver, userId, TrackType[trackType]);
9876
+ this.logger.debug('E2EE decryptor attached to receiver', userId);
9877
+ }
9669
9878
  const streamKindProp = trackTypeToParticipantStreamKey(trackType);
9670
9879
  if (!streamKindProp) {
9671
9880
  this.logger.error(`Unknown track type: ${rawTrackType}`);
@@ -10637,7 +10846,7 @@ const handleRemoteSoftMute = (call) => {
10637
10846
  /**
10638
10847
  * An event responder which handles the `participantJoined` event.
10639
10848
  */
10640
- const watchParticipantJoined = (state) => {
10849
+ const watchParticipantJoined = (state, e2ee = undefined) => {
10641
10850
  return function onParticipantJoined(e) {
10642
10851
  const { participant } = e;
10643
10852
  if (!participant)
@@ -10650,7 +10859,7 @@ const watchParticipantJoined = (state) => {
10650
10859
  // The SFU would send participant info as part of the `join`
10651
10860
  // response and then follow up with a `participantJoined` event for
10652
10861
  // already announced participants.
10653
- const orphanedTracks = reconcileOrphanedTracks(state, participant);
10862
+ const orphanedTracks = reconcileOrphanedTracks(state, participant, e2ee);
10654
10863
  state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
10655
10864
  ...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
10656
10865
  viewportVisibilityState: {
@@ -10686,7 +10895,7 @@ const watchParticipantUpdated = (state) => {
10686
10895
  * An event responder which handles the `trackPublished` event.
10687
10896
  * The SFU will send this event when a participant publishes a track.
10688
10897
  */
10689
- const watchTrackPublished = (state) => {
10898
+ const watchTrackPublished = (state, e2ee = undefined) => {
10690
10899
  return function onTrackPublished(e) {
10691
10900
  const { type, sessionId } = e;
10692
10901
  // An optimization for large calls.
@@ -10694,7 +10903,7 @@ const watchTrackPublished = (state) => {
10694
10903
  // events, and instead, it would only provide the participant's information
10695
10904
  // once they start publishing a track.
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);
10700
10909
  }
@@ -10709,12 +10918,12 @@ const watchTrackPublished = (state) => {
10709
10918
  * An event responder which handles the `trackUnpublished` event.
10710
10919
  * The SFU will send this event when a participant unpublishes a track.
10711
10920
  */
10712
- const watchTrackUnpublished = (state) => {
10921
+ const watchTrackUnpublished = (state, e2ee = undefined) => {
10713
10922
  return function onTrackUnpublished(e) {
10714
10923
  const { type, sessionId } = e;
10715
10924
  // An optimization for large calls. See `watchTrackPublished`.
10716
10925
  if (e.participant) {
10717
- const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
10926
+ const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
10718
10927
  const participant = Object.assign(e.participant, orphanedTracks);
10719
10928
  state.updateOrAddParticipant(sessionId, participant, (p) => ({
10720
10929
  pausedTracks: p.pausedTracks?.filter((t) => t !== type),
@@ -10733,17 +10942,22 @@ const watchTrackUnpublished = (state) => {
10733
10942
  *
10734
10943
  * @param state the call state.
10735
10944
  * @param participant the participant.
10945
+ * @param e2ee accessor for the call's encryption manager, if any.
10736
10946
  */
10737
- const reconcileOrphanedTracks = (state, participant) => {
10947
+ const reconcileOrphanedTracks = (state, participant, e2ee) => {
10738
10948
  const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
10739
10949
  if (!orphanTracks.length)
10740
10950
  return;
10951
+ const manager = e2ee?.();
10741
10952
  const reconciledTracks = {};
10742
10953
  for (const orphan of orphanTracks) {
10743
10954
  const key = trackTypeToParticipantStreamKey(orphan.trackType);
10744
10955
  if (!key)
10745
10956
  continue;
10746
10957
  reconciledTracks[key] = orphan.track;
10958
+ if (manager && orphan.receiver) {
10959
+ manager.decrypt(orphan.receiver, participant.userId, TrackType[orphan.trackType]);
10960
+ }
10747
10961
  }
10748
10962
  return reconciledTracks;
10749
10963
  };
@@ -10799,6 +11013,11 @@ const watchAudioLevelChanged = (dispatcher, state) => {
10799
11013
  */
10800
11014
  const registerEventHandlers = (call, dispatcher) => {
10801
11015
  const state = call.state;
11016
+ // Read lazily on each event: setE2EEManager can run after setup() (an app that
11017
+ // inspects call settings via get()/getOrCreate() before deciding to encrypt has
11018
+ // already triggered it), so a value captured here would be a stale undefined
11019
+ // for the whole call and orphaned tracks would never get a decryptor.
11020
+ const e2ee = () => call.e2eeManager;
10802
11021
  const eventHandlers = [
10803
11022
  call.on('call.ended', watchCallEnded(call)),
10804
11023
  watchSfuCallEnded(call),
@@ -10806,11 +11025,11 @@ const registerEventHandlers = (call, dispatcher) => {
10806
11025
  watchSfuErrorReports(dispatcher),
10807
11026
  watchConnectionQualityChanged(dispatcher, state),
10808
11027
  watchParticipantCountChanged(dispatcher, state),
10809
- call.on('participantJoined', watchParticipantJoined(state)),
11028
+ call.on('participantJoined', watchParticipantJoined(state, e2ee)),
10810
11029
  call.on('participantLeft', watchParticipantLeft(state)),
10811
11030
  call.on('participantUpdated', watchParticipantUpdated(state)),
10812
- call.on('trackPublished', watchTrackPublished(state)),
10813
- call.on('trackUnpublished', watchTrackUnpublished(state)),
11031
+ call.on('trackPublished', watchTrackPublished(state, e2ee)),
11032
+ call.on('trackUnpublished', watchTrackUnpublished(state, e2ee)),
10814
11033
  watchAudioLevelChanged(dispatcher, state),
10815
11034
  watchDominantSpeakerChanged(dispatcher, state),
10816
11035
  call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
@@ -11938,13 +12157,14 @@ class PermissionsContext {
11938
12157
  return false;
11939
12158
  const { audio, video, screensharing } = settings;
11940
12159
  switch (permission) {
11941
- case OwnCapability.SEND_AUDIO:
12160
+ case RequestPermissionRequestPermissionsEnum.SEND_AUDIO:
11942
12161
  return audio.access_request_enabled;
11943
- case OwnCapability.SEND_VIDEO:
12162
+ case RequestPermissionRequestPermissionsEnum.SEND_VIDEO:
11944
12163
  return video.access_request_enabled;
11945
- case OwnCapability.SCREENSHARE:
12164
+ case RequestPermissionRequestPermissionsEnum.SCREENSHARE:
11946
12165
  return screensharing.access_request_enabled;
11947
12166
  default:
12167
+ ensureExhausted(permission);
11948
12168
  return false;
11949
12169
  }
11950
12170
  };
@@ -12951,6 +13171,27 @@ class DeviceManager {
12951
13171
  this.filters.forEach((entry) => entry.stop?.());
12952
13172
  }
12953
13173
  }
13174
+ reconcileOptimisticStatus = async () => {
13175
+ const target = this.state.optimisticStatus;
13176
+ await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
13177
+ try {
13178
+ if (target === 'enabled' && this.state.status !== 'enabled') {
13179
+ await this.unmuteStream();
13180
+ if (!signal.aborted)
13181
+ this.state.setStatus('enabled');
13182
+ }
13183
+ else if (target === 'disabled' && this.state.status === 'enabled') {
13184
+ // mirror whatever disable() does to stop/pause the track per disableMode
13185
+ if (!signal.aborted)
13186
+ this.state.setStatus('disabled');
13187
+ }
13188
+ }
13189
+ finally {
13190
+ if (!signal.aborted)
13191
+ this.state.setPendingStatus(this.state.status);
13192
+ }
13193
+ });
13194
+ };
12954
13195
  disableTracks() {
12955
13196
  this.getTracks().forEach((track) => {
12956
13197
  if (track.enabled)
@@ -13584,6 +13825,33 @@ class CameraManager extends DeviceManager {
13584
13825
  }
13585
13826
  }
13586
13827
  }
13828
+ enable() {
13829
+ if (isReactNative() && !this.call.hasMediaEngine) {
13830
+ this.state.setPendingStatus('enabled');
13831
+ return Promise.resolve();
13832
+ }
13833
+ return super.enable();
13834
+ }
13835
+ async disable(forceStopOrOptions) {
13836
+ if (isReactNative() && !this.call.hasMediaEngine) {
13837
+ this.state.setPendingStatus('disabled');
13838
+ return;
13839
+ }
13840
+ // forward verbatim to the base, narrowing so the right overload is selected
13841
+ if (forceStopOrOptions === undefined)
13842
+ return super.disable();
13843
+ if (typeof forceStopOrOptions === 'boolean') {
13844
+ return super.disable(forceStopOrOptions);
13845
+ }
13846
+ return super.disable(forceStopOrOptions);
13847
+ }
13848
+ toggle() {
13849
+ if (isReactNative() && !this.call.hasMediaEngine) {
13850
+ this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
13851
+ return Promise.resolve();
13852
+ }
13853
+ return super.toggle();
13854
+ }
13587
13855
  /**
13588
13856
  * Applies the video settings to the camera.
13589
13857
  *
@@ -13617,9 +13885,16 @@ class CameraManager extends DeviceManager {
13617
13885
  await this.enable();
13618
13886
  }
13619
13887
  }
13620
- const { mediaStream } = this.state;
13621
- if (canPublish && publish && this.enabled && mediaStream) {
13622
- await this.publishStream(mediaStream);
13888
+ if (isReactNative() && publish && canPublish) {
13889
+ // On RN the camera is enabled/disabled optimistically before JOINED. Reconcile now
13890
+ // acquires the track and publishes it, so it fully owns the publish.
13891
+ await this.reconcileOptimisticStatus();
13892
+ }
13893
+ else {
13894
+ const { mediaStream } = this.state;
13895
+ if (canPublish && publish && this.enabled && mediaStream) {
13896
+ await this.publishStream(mediaStream);
13897
+ }
13623
13898
  }
13624
13899
  }
13625
13900
  getDevices() {
@@ -13638,7 +13913,10 @@ class CameraManager extends DeviceManager {
13638
13913
  }
13639
13914
  return constraints;
13640
13915
  }
13641
- getStream(constraints) {
13916
+ async getStream(constraints) {
13917
+ // Ensure the call's media factory exists before capture so the resulting
13918
+ // track is owned by it (the WebRTC globals resolve to the live factory).
13919
+ await this.call.ensureMediaFactory();
13642
13920
  return getVideoStream(constraints, this.call.tracer);
13643
13921
  }
13644
13922
  }
@@ -13654,6 +13932,9 @@ class AudioDeviceManager extends DeviceManager {
13654
13932
  if (!this.call.state.settings?.audio.hifi_audio_enabled) {
13655
13933
  throw new Error('High Fidelity audio is not enabled for this call');
13656
13934
  }
13935
+ if (isReactNative() && this.call.hasMediaEngine) {
13936
+ throw new Error('setAudioBitrateProfile must be called before joining the call.');
13937
+ }
13657
13938
  this.doSetAudioBitrateProfile(profile);
13658
13939
  this.state.setAudioBitrateProfile(profile);
13659
13940
  if (this.enabled) {
@@ -13926,7 +14207,10 @@ class MicrophoneManager extends AudioDeviceManager {
13926
14207
  ]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
13927
14208
  try {
13928
14209
  if (callingState === exports.CallingState.LEFT) {
13929
- this.setMutedRecordingPrepared(false);
14210
+ // The muted-recording-prepared mode is reset in `callManager.stop()`
14211
+ // (during leave, while the call factory is still alive), not here —
14212
+ // this subscription fires asynchronously and could land after the
14213
+ // factory is disposed, forcing a default-ADM rebuild.
13930
14214
  await this.stopSpeakingWhileMutedDetection();
13931
14215
  }
13932
14216
  if (callingState !== exports.CallingState.JOINED)
@@ -14026,6 +14310,33 @@ class MicrophoneManager extends AudioDeviceManager {
14026
14310
  this.subscriptions.push(unsubscribe);
14027
14311
  }
14028
14312
  }
14313
+ enable() {
14314
+ if (isReactNative() && !this.call.hasMediaEngine) {
14315
+ this.state.setPendingStatus('enabled');
14316
+ return Promise.resolve();
14317
+ }
14318
+ return super.enable();
14319
+ }
14320
+ async disable(forceStopOrOptions) {
14321
+ if (isReactNative() && !this.call.hasMediaEngine) {
14322
+ this.state.setPendingStatus('disabled');
14323
+ return;
14324
+ }
14325
+ // forward verbatim to the base, narrowing so the right overload is selected
14326
+ if (forceStopOrOptions === undefined)
14327
+ return super.disable();
14328
+ if (typeof forceStopOrOptions === 'boolean') {
14329
+ return super.disable(forceStopOrOptions);
14330
+ }
14331
+ return super.disable(forceStopOrOptions);
14332
+ }
14333
+ toggle() {
14334
+ if (isReactNative() && !this.call.hasMediaEngine) {
14335
+ this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
14336
+ return Promise.resolve();
14337
+ }
14338
+ return super.toggle();
14339
+ }
14029
14340
  /**
14030
14341
  * Enables noise cancellation for the microphone.
14031
14342
  *
@@ -14186,15 +14497,25 @@ class MicrophoneManager extends AudioDeviceManager {
14186
14497
  await this.enable();
14187
14498
  }
14188
14499
  }
14189
- const { mediaStream } = this.state;
14190
- if (canPublish && publish && this.enabled && mediaStream) {
14191
- await this.publishStream(mediaStream);
14500
+ if (isReactNative() && publish && canPublish) {
14501
+ // On RN the microphone is enabled/disabled optimistically before JOINED. Reconcile now
14502
+ // acquires the track and publishes it, so it fully owns the publish.
14503
+ await this.reconcileOptimisticStatus();
14504
+ }
14505
+ else {
14506
+ const { mediaStream } = this.state;
14507
+ if (canPublish && publish && this.enabled && mediaStream) {
14508
+ await this.publishStream(mediaStream);
14509
+ }
14192
14510
  }
14193
14511
  }
14194
14512
  getDevices() {
14195
14513
  return getAudioDevices(this.call.tracer);
14196
14514
  }
14197
- getStream(constraints) {
14515
+ async getStream(constraints) {
14516
+ // Ensure the call's media factory exists before capture so the resulting
14517
+ // track is owned by it (the WebRTC globals resolve to the live factory).
14518
+ await this.call.ensureMediaFactory();
14198
14519
  return getAudioStream(constraints, this.call.tracer);
14199
14520
  }
14200
14521
  doSetAudioBitrateProfile(profile) {
@@ -14403,6 +14724,9 @@ class ScreenShareManager extends AudioDeviceManager {
14403
14724
  if (!this.state.audioEnabled) {
14404
14725
  constraints.audio = false;
14405
14726
  }
14727
+ // Ensure the call's media factory exists before capture so the resulting
14728
+ // track is owned by it (the WebRTC globals resolve to the live factory).
14729
+ await this.call.ensureMediaFactory();
14406
14730
  const stream = await getScreenShareStream(constraints, this.call.tracer);
14407
14731
  const [track] = stream.getVideoTracks();
14408
14732
  const { contentHint } = this.state.settings || {};
@@ -14507,10 +14831,14 @@ class SpeakerManager {
14507
14831
  this.devicePersistence = devicePreferences;
14508
14832
  this.setup();
14509
14833
  }
14510
- apply(settings) {
14511
- return isReactNative() ? this.applyRN(settings) : this.applyWeb();
14834
+ async apply(settings) {
14835
+ if (isReactNative()) {
14836
+ this.applyRN(settings);
14837
+ return;
14838
+ }
14839
+ await this.applyWeb();
14512
14840
  }
14513
- applyWeb() {
14841
+ async applyWeb() {
14514
14842
  const { enabled, storageKey } = this.devicePersistence;
14515
14843
  if (!enabled)
14516
14844
  return;
@@ -14522,8 +14850,22 @@ class SpeakerManager {
14522
14850
  const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
14523
14851
  ? ''
14524
14852
  : preference.selectedDeviceId;
14525
- if (this.state.selectedDevice !== nextDeviceId) {
14526
- this.select(nextDeviceId);
14853
+ if (!nextDeviceId) {
14854
+ if (this.state.selectedDevice !== nextDeviceId) {
14855
+ this.select(nextDeviceId);
14856
+ }
14857
+ return;
14858
+ }
14859
+ const permissionState = await rxjs.firstValueFrom(getAudioBrowserPermission(this.call.tracer).asStateObservable());
14860
+ if (permissionState !== 'granted')
14861
+ return;
14862
+ const devices = await rxjs.firstValueFrom(this.listDevices());
14863
+ const device = this.findDevice(devices, nextDeviceId) ??
14864
+ (preference.selectedDeviceLabel
14865
+ ? devices.find((d) => d.label === preference.selectedDeviceLabel)
14866
+ : undefined);
14867
+ if (device && this.state.selectedDevice !== device.deviceId) {
14868
+ this.select(device.deviceId);
14527
14869
  }
14528
14870
  }
14529
14871
  applyRN(settings) {
@@ -14552,6 +14894,7 @@ class SpeakerManager {
14552
14894
  globalThis.streamRNVideoSDK?.callManager.setup({
14553
14895
  defaultDevice,
14554
14896
  isRingingTypeCall: this.call.ringing,
14897
+ cid: this.call.cid,
14555
14898
  });
14556
14899
  }
14557
14900
  }
@@ -14560,11 +14903,15 @@ class SpeakerManager {
14560
14903
  return;
14561
14904
  this.areSubscriptionsSetUp = true;
14562
14905
  if (deviceIds$ && !isReactNative()) {
14563
- this.subscriptions.push(createSubscription(rxjs.combineLatest([deviceIds$, this.state.selectedDevice$]), ([devices, deviceId]) => {
14906
+ this.subscriptions.push(createSubscription(rxjs.combineLatest([
14907
+ deviceIds$.pipe(rxjs.pairwise()),
14908
+ this.state.selectedDevice$,
14909
+ ]), ([[prevDevices, currentDevices], deviceId]) => {
14564
14910
  if (!deviceId)
14565
14911
  return;
14566
- const device = devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
14567
- if (!device)
14912
+ const isDisconnected = this.findDevice(prevDevices, deviceId) &&
14913
+ !this.findDevice(currentDevices, deviceId);
14914
+ if (isDisconnected)
14568
14915
  this.select('');
14569
14916
  }));
14570
14917
  }
@@ -14611,6 +14958,7 @@ class SpeakerManager {
14611
14958
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
14612
14959
  this.subscriptions = [];
14613
14960
  this.areSubscriptionsSetUp = false;
14961
+ this.defaultDevice = undefined;
14614
14962
  };
14615
14963
  /**
14616
14964
  * Set the volume of the audio elements
@@ -14645,6 +14993,7 @@ class SpeakerManager {
14645
14993
  return { audioVolume: volume };
14646
14994
  });
14647
14995
  }
14996
+ findDevice = (devices, deviceId) => devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
14648
14997
  persistSpeakerDevicePreference(selectedDevice) {
14649
14998
  const { storageKey } = this.devicePersistence;
14650
14999
  const devices = getCurrentValue(this.listDevices()) || [];
@@ -14727,6 +15076,7 @@ class Call {
14727
15076
  blockedAudioTracker;
14728
15077
  subscriber;
14729
15078
  publisher;
15079
+ e2eeManager;
14730
15080
  /**
14731
15081
  * Flag telling whether this call is a "ringing" call.
14732
15082
  */
@@ -14782,6 +15132,8 @@ class Call {
14782
15132
  allowOwnTracksLoopback = false;
14783
15133
  hasJoinedOnce = false;
14784
15134
  deviceSettingsAppliedOnce = false;
15135
+ callManagerStarted = false;
15136
+ leaveGeneration = 0;
14785
15137
  credentials;
14786
15138
  initialized = false;
14787
15139
  acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
@@ -14800,6 +15152,13 @@ class Call {
14800
15152
  clientCapabilities = new Set([
14801
15153
  ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
14802
15154
  ]);
15155
+ /**
15156
+ * The in-flight per-call media engine. On web/React this resolves to a thin
15157
+ * globals-backed engine (no provider registered); React Native registers a
15158
+ * provider that owns a per-call native factory.
15159
+ * @internal
15160
+ */
15161
+ mediaEnginePromise;
14803
15162
  /**
14804
15163
  * Constructs a new `Call` instance.
14805
15164
  *
@@ -15069,6 +15428,7 @@ class Call {
15069
15428
  if (callingState === exports.CallingState.LEFT) {
15070
15429
  return;
15071
15430
  }
15431
+ this.leaveGeneration += 1;
15072
15432
  if (callingState === exports.CallingState.JOINING) {
15073
15433
  const waitUntilCallJoined = () => {
15074
15434
  return new Promise((resolve) => {
@@ -15155,12 +15515,14 @@ class Call {
15155
15515
  this.clientStore.unregisterCall(this);
15156
15516
  globalThis.streamRNVideoSDK?.callManager.stop({
15157
15517
  isRingingTypeCall: this.ringing,
15518
+ shouldStopCallManager: this.callManagerStarted,
15158
15519
  });
15159
15520
  this.camera.dispose();
15160
15521
  this.microphone.dispose();
15161
15522
  this.screenShare.dispose();
15162
15523
  this.speaker.dispose();
15163
15524
  this.deviceSettingsAppliedOnce = false;
15525
+ this.callManagerStarted = false;
15164
15526
  const stopOnLeavePromises = [];
15165
15527
  if (this.camera.stopOnLeave) {
15166
15528
  stopOnLeavePromises.push(this.camera.disable(true));
@@ -15172,6 +15534,22 @@ class Call {
15172
15534
  stopOnLeavePromises.push(this.screenShare.disable(true));
15173
15535
  }
15174
15536
  await Promise.all(stopOnLeavePromises);
15537
+ // Dispose the per-call media engine last — after peer connections and
15538
+ // local tracks are gone — so the backing factory tears down with no
15539
+ // owned PCs/tracks. A fresh `join()` builds a new engine.
15540
+ if (this.mediaEnginePromise) {
15541
+ const enginePromise = this.mediaEnginePromise;
15542
+ this.mediaEnginePromise = undefined;
15543
+ this.logger.debug('Disposing per-call media factory');
15544
+ await enginePromise
15545
+ .then((engine) => {
15546
+ globalThis.streamRNVideoSDK?.callingX?.unwireAudioEngineSubscription();
15547
+ return engine.dispose();
15548
+ })
15549
+ .catch((err) => {
15550
+ this.logger.warn('Failed to dispose media engine', err);
15551
+ });
15552
+ }
15175
15553
  });
15176
15554
  };
15177
15555
  /**
@@ -15272,11 +15650,7 @@ class Call {
15272
15650
  this.clientStore.registerOrUpdateCall(this);
15273
15651
  }
15274
15652
  // Skip speaker setup on RN if ringing was requested or the call is already ringing
15275
- const skipSpeakerApply = isReactNative()
15276
- ? params?.ring === true
15277
- ? true
15278
- : this.ringing
15279
- : false;
15653
+ const skipSpeakerApply = isReactNative();
15280
15654
  await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
15281
15655
  return response;
15282
15656
  };
@@ -15299,11 +15673,7 @@ class Call {
15299
15673
  this.clientStore.registerOrUpdateCall(this);
15300
15674
  }
15301
15675
  // Skip speaker setup on RN if ringing was requested or the call is already ringing
15302
- const skipSpeakerApply = isReactNative()
15303
- ? data?.ring === true
15304
- ? true
15305
- : this.ringing
15306
- : false;
15676
+ const skipSpeakerApply = isReactNative();
15307
15677
  await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
15308
15678
  return response;
15309
15679
  };
@@ -15452,9 +15822,20 @@ class Call {
15452
15822
  doJoin = async (data) => {
15453
15823
  const connectStartTime = Date.now();
15454
15824
  const callingState = this.state.callingState;
15825
+ const joinLeaveGeneration = this.leaveGeneration;
15826
+ const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
15455
15827
  this.joinCallData = data;
15456
15828
  this.logger.debug('Starting join flow');
15457
15829
  this.state.setCallingState(exports.CallingState.JOINING);
15830
+ // Ensure the per-call media engine exists before any peer connection
15831
+ // (codec probe, subscriber, publisher) or capture happens, so the WebRTC
15832
+ // globals resolve to the call's factory. Idempotent across
15833
+ // reconnect/migration attempts.
15834
+ await this.ensureMediaFactory();
15835
+ const callingX = globalThis.streamRNVideoSDK?.callingX;
15836
+ if (callingX) {
15837
+ callingX.wireAudioEngineSubscription();
15838
+ }
15458
15839
  const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
15459
15840
  const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
15460
15841
  const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
@@ -15509,6 +15890,11 @@ class Call {
15509
15890
  // these are throw-away SDPs that the SFU will use to determine
15510
15891
  // the capabilities of the client (codec support, etc.)
15511
15892
  const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } = this.clientPublishOptions || {};
15893
+ // skip if a leave superseded this join so codec detection doesn't resolve to a default factory.
15894
+ if (supersededByLeave()) {
15895
+ this.logger.debug('Join superseded by leave; skipping codec detection');
15896
+ return;
15897
+ }
15512
15898
  const [subscriberSdp, publisherSdp] = await Promise.all([
15513
15899
  getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
15514
15900
  getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
@@ -15552,6 +15938,12 @@ class Call {
15552
15938
  throw error;
15553
15939
  }
15554
15940
  }
15941
+ // If the user left while this join was in flight, bail before re-setting JOINED and before
15942
+ // peer-connection setup below (both run synchronously after this, so one check covers them).
15943
+ if (supersededByLeave()) {
15944
+ this.logger.debug('Join superseded by leave; aborting join flow');
15945
+ return;
15946
+ }
15555
15947
  if (!performingMigration) {
15556
15948
  // in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
15557
15949
  this.state.setCallingState(exports.CallingState.JOINED);
@@ -15589,12 +15981,18 @@ class Call {
15589
15981
  }
15590
15982
  // device settings should be applied only once, we don't have to
15591
15983
  // re-apply them on later reconnections or server-side data fetches
15592
- if (!this.deviceSettingsAppliedOnce && this.state.settings) {
15984
+ if (!this.deviceSettingsAppliedOnce &&
15985
+ this.state.settings &&
15986
+ !supersededByLeave()) {
15593
15987
  await this.applyDeviceConfig(this.state.settings, true, false);
15988
+ this.deviceSettingsAppliedOnce = true;
15989
+ }
15990
+ if (!this.callManagerStarted && !supersededByLeave()) {
15594
15991
  globalThis.streamRNVideoSDK?.callManager.start({
15595
15992
  isRingingTypeCall: this.ringing,
15993
+ cid: this.cid,
15596
15994
  });
15597
- this.deviceSettingsAppliedOnce = true;
15995
+ this.callManagerStarted = true;
15598
15996
  }
15599
15997
  // We shouldn't persist the `ring` and `notify` state after joining the call
15600
15998
  // as it's a one-time event and clashes with the potential reconnection attempts.
@@ -15721,6 +16119,7 @@ class Call {
15721
16119
  this.sfuStatsReporter = undefined;
15722
16120
  if (closePreviousInstances && this.subscriber) {
15723
16121
  await this.subscriber.dispose();
16122
+ this.state.removeAllOrphanedTracks();
15724
16123
  }
15725
16124
  const basePeerConnectionOptions = {
15726
16125
  sfuClient,
@@ -15731,6 +16130,7 @@ class Call {
15731
16130
  enableTracing,
15732
16131
  statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
15733
16132
  clientPublishOptions: this.clientPublishOptions,
16133
+ e2ee: this.e2eeManager,
15734
16134
  onReconnectionNeeded: (kind, reason, peerType) => {
15735
16135
  this.reconnect(kind, reason).catch((err) => {
15736
16136
  const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
@@ -15796,12 +16196,12 @@ class Call {
15796
16196
  * Retrieves credentials for joining the call.
15797
16197
  *
15798
16198
  * @internal
15799
- *
15800
16199
  * @param data the join call data.
15801
16200
  */
15802
16201
  doJoinRequest = async (data) => {
15803
16202
  const location = await this.streamClient.getLocationHint();
15804
- const request = { ...data, location };
16203
+ const e2ee = !!this.e2eeManager;
16204
+ const request = { ...data, location, e2ee };
15805
16205
  const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
15806
16206
  this.state.updateFromCallResponse(joinResponse.call);
15807
16207
  this.state.setMembers(joinResponse.members);
@@ -15820,6 +16220,36 @@ class Call {
15820
16220
  }
15821
16221
  return joinResponse;
15822
16222
  };
16223
+ /**
16224
+ * Whether the per-call media engine currently exists. True from join until leave.
16225
+ *
16226
+ * @internal an internal getter and should not be used outside the SDK.
16227
+ */
16228
+ get hasMediaEngine() {
16229
+ return !!this.mediaEnginePromise;
16230
+ }
16231
+ /**
16232
+ * Ensures a {@link CallMediaEngine} exists for this call's media session and
16233
+ * returns it. Idempotent: the engine is created once via the registered
16234
+ * provider (see `setCallMediaEngineProvider`) and cached until `leave()`
16235
+ * disposes it. Concurrent callers (e.g. camera + microphone enabling in
16236
+ * parallel) share the same engine because the in-flight creation promise is
16237
+ * cached, never the unresolved result.
16238
+ *
16239
+ * @internal
16240
+ */
16241
+ ensureMediaFactory = async () => {
16242
+ if (!this.mediaEnginePromise) {
16243
+ const provider = getCallMediaEngineProvider();
16244
+ this.logger.debug(`Requesting per-call media factory creation`);
16245
+ this.mediaEnginePromise = Promise.resolve(provider()).catch((err) => {
16246
+ // Drop the cached rejection so a retried join() can rebuild the engine
16247
+ this.mediaEnginePromise = undefined;
16248
+ throw err;
16249
+ });
16250
+ }
16251
+ return this.mediaEnginePromise;
16252
+ };
15823
16253
  /**
15824
16254
  * Handles the closing of the SFU signal connection.
15825
16255
  *
@@ -16381,6 +16811,30 @@ class Call {
16381
16811
  this.tracer.trace('updatePublishOptions', options);
16382
16812
  this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
16383
16813
  };
16814
+ /**
16815
+ * Set the E2EE (end-to-end encryption) manager for this call.
16816
+ *
16817
+ * Must be called before {@link join} so the RTCPeerConnection can be
16818
+ * configured for E2EE.
16819
+ *
16820
+ * The manager is kept across {@link leave} so a rejoin of this same instance
16821
+ * stays encrypted: do not dispose it while this call may be joined again.
16822
+ * A disposed manager throws from `encrypt`/`decrypt` rather than silently
16823
+ * publishing nothing, so re-attach a fresh one instead of reusing it.
16824
+ *
16825
+ * @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
16826
+ * built-in AES-GCM scheme, or pass your own implementation.
16827
+ * @throws if called after the peer connections have been built (i.e. after
16828
+ * `join`): those PCs were already configured without an encryptor, so
16829
+ * adopting a manager now would silently publish/receive cleartext for
16830
+ * the live session.
16831
+ */
16832
+ setE2EEManager = (e2ee) => {
16833
+ if (this.publisher || this.subscriber) {
16834
+ throw new Error('setE2EEManager must be called before join()');
16835
+ }
16836
+ this.e2eeManager = e2ee;
16837
+ };
16384
16838
  /**
16385
16839
  * Notifies the SFU that a noise cancellation process has started.
16386
16840
  *
@@ -17017,7 +17471,9 @@ class Call {
17017
17471
  */
17018
17472
  applyDeviceConfig = async (settings, publish, skipSpeakerApply) => {
17019
17473
  if (!skipSpeakerApply) {
17020
- this.speaker.apply(settings);
17474
+ await this.speaker.apply(settings).catch((err) => {
17475
+ this.logger.warn('Speaker init failed', err);
17476
+ });
17021
17477
  }
17022
17478
  await this.camera.apply(settings.video, publish).catch((err) => {
17023
17479
  this.logger.warn('Camera init failed', err);
@@ -17696,15 +18152,17 @@ class StableWSConnection {
17696
18152
  this.client.logger.error(`Token not set, can't connect authenticate`);
17697
18153
  return;
17698
18154
  }
17699
- const authMessage = JSON.stringify({
18155
+ const wsAuthMessage = {
17700
18156
  token,
18157
+ products: [WSAuthMessageProductsEnum.VIDEO],
17701
18158
  user_details: {
17702
18159
  id: user.id,
17703
18160
  name: user.name,
17704
18161
  image: user.image,
17705
18162
  custom: user.custom,
17706
18163
  },
17707
- });
18164
+ };
18165
+ const authMessage = JSON.stringify(wsAuthMessage);
17708
18166
  this._log(`onopen() - Sending auth message ${authMessage}`, {}, 'trace');
17709
18167
  this.ws?.send(authMessage);
17710
18168
  this._log('onopen() - onopen callback', { wsID });
@@ -18530,11 +18988,11 @@ class StreamClient {
18530
18988
  return await this.wsConnection.connect(this.defaultWSTimeout);
18531
18989
  };
18532
18990
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18533
- "1.57.0";
18991
+ "1.58.0";
18534
18992
  getUserAgent = () => {
18535
18993
  if (!this.cachedUserAgent) {
18536
18994
  const { clientAppIdentifier = {} } = this.options;
18537
- const { sdkName = 'js', sdkVersion = "1.57.0", ...extras } = clientAppIdentifier;
18995
+ const { sdkName = 'js', sdkVersion = "1.58.0", ...extras } = clientAppIdentifier;
18538
18996
  this.cachedUserAgent = [
18539
18997
  `stream-video-${sdkName}-v${sdkVersion}`,
18540
18998
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -19767,6 +20225,357 @@ const humanize = (n) => {
19767
20225
  return String(n);
19768
20226
  };
19769
20227
 
20228
+ /**
20229
+ * Tiny, type-safe event emitter.
20230
+ *
20231
+ * Usage styles:
20232
+ * - compose: `private readonly events = new TypedEventEmitter<MyMap>()`
20233
+ * - extend: `class Foo extends TypedEventEmitter<MyMap> {}`
20234
+ *
20235
+ * Listener exceptions (sync throw or rejected promise) are caught and logged,
20236
+ * so one bad listener cannot break dispatch for the rest.
20237
+ */
20238
+ class TypedEventEmitter {
20239
+ logger;
20240
+ byEvent = new Map();
20241
+ constructor(logger = 'TypedEventEmitter') {
20242
+ this.logger =
20243
+ typeof logger === 'string' ? videoLoggerSystem.getLogger(logger) : logger;
20244
+ }
20245
+ on = (event, fn) => {
20246
+ let listeners = this.byEvent.get(event);
20247
+ if (!listeners) {
20248
+ listeners = new Set();
20249
+ this.byEvent.set(event, listeners);
20250
+ }
20251
+ listeners.add(fn);
20252
+ return () => this.off(event, fn);
20253
+ };
20254
+ off = (event, fn) => {
20255
+ const listeners = this.byEvent.get(event);
20256
+ if (!listeners)
20257
+ return;
20258
+ listeners.delete(fn);
20259
+ if (listeners.size === 0)
20260
+ this.byEvent.delete(event);
20261
+ };
20262
+ emit = (event, payload) => {
20263
+ const listeners = this.byEvent.get(event);
20264
+ if (!listeners || listeners.size === 0)
20265
+ return;
20266
+ for (const listener of [...listeners]) {
20267
+ this.invoke(() => listener(payload), event);
20268
+ }
20269
+ };
20270
+ removeAllListeners = (event) => {
20271
+ if (event === undefined) {
20272
+ this.byEvent.clear();
20273
+ }
20274
+ else {
20275
+ this.byEvent.delete(event);
20276
+ }
20277
+ };
20278
+ invoke = (run, event) => {
20279
+ try {
20280
+ const result = run();
20281
+ if (result && typeof result.then === 'function') {
20282
+ result.catch((err) => {
20283
+ this.logger.warn(`Listener for '${String(event)}' rejected`, err);
20284
+ });
20285
+ }
20286
+ }
20287
+ catch (err) {
20288
+ this.logger.warn(`Listener for '${String(event)}' threw`, err);
20289
+ }
20290
+ };
20291
+ }
20292
+
20293
+ /**
20294
+ * Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
20295
+ * transforms to RTCRtpSenders and RTCRtpReceivers.
20296
+ */
20297
+ class EncryptionManager extends TypedEventEmitter {
20298
+ algorithm;
20299
+ transform;
20300
+ disposed = false;
20301
+ piped;
20302
+ userId;
20303
+ worker;
20304
+ workerUrl;
20305
+ constructor(userId, worker, workerUrl, algorithm, transform) {
20306
+ super('EncryptionManager');
20307
+ this.userId = userId;
20308
+ this.worker = worker;
20309
+ this.workerUrl = workerUrl;
20310
+ this.algorithm = algorithm;
20311
+ this.transform = transform;
20312
+ this.worker.addEventListener('message', this.handleWorkerMessage);
20313
+ this.worker.addEventListener('error', this.handleWorkerError);
20314
+ }
20315
+ /**
20316
+ * Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
20317
+ * Use it to guard UI, or to avoid calling {@link create} where it would throw.
20318
+ */
20319
+ static isSupported = () => {
20320
+ return preferredTransform() !== undefined;
20321
+ };
20322
+ /**
20323
+ * Create an EncryptionManager instance and initialize the E2EE Web Worker.
20324
+ *
20325
+ * @param userId - The local user's ID, normally `call.currentUserId`.
20326
+ * @param options - the create options.
20327
+ * @throws {Error} If the browser lacks Encoded Transforms.
20328
+ *
20329
+ * @example
20330
+ * ```ts
20331
+ * if (EncryptionManager.isSupported()) {
20332
+ * const e2ee = await EncryptionManager.create(call.currentUserId);
20333
+ * call.setE2EEManager(e2ee);
20334
+ * e2ee.setSharedKey(0, keyBytes);
20335
+ * }
20336
+ */
20337
+ static create = async (userId, options) => {
20338
+ const transform = preferredTransform();
20339
+ if (!transform) {
20340
+ throw new Error(`E2EE is not supported in this browser`);
20341
+ }
20342
+ const { e2eeWorker } = await Promise.resolve().then(function () { return e2eeWorker$1; });
20343
+ const blob = new Blob([`(${e2eeWorker.toString()})()`], {
20344
+ type: 'application/javascript',
20345
+ });
20346
+ const url = URL.createObjectURL(blob);
20347
+ let worker;
20348
+ try {
20349
+ worker = new Worker(url, { name: 'stream-video-e2ee' });
20350
+ }
20351
+ catch (err) {
20352
+ // e.g. a CSP `worker-src` without `blob:`. Don't leak the object URL.
20353
+ URL.revokeObjectURL(url);
20354
+ throw err;
20355
+ }
20356
+ const algorithm = options?.algorithm ?? 'AES-128-GCM';
20357
+ return new EncryptionManager(userId, worker, url, algorithm, transform);
20358
+ };
20359
+ /**
20360
+ * {@link dispose} terminates the worker, so `postMessage` becomes a silent
20361
+ * no-op and an attached transform points at a dead worker: frames would stall
20362
+ * forever with no error and no event. Throwing is also fail-closed, since a
20363
+ * caller that swallows it still publishes nothing rather than cleartext.
20364
+ */
20365
+ assertUsable = () => {
20366
+ if (this.disposed)
20367
+ throw new Error(`EncryptionManager is disposed`);
20368
+ };
20369
+ /**
20370
+ * Terminate the worker and release all resources.
20371
+ *
20372
+ * The manager is unusable afterwards and every other method throws. Call
20373
+ * {@link create} for a new one. Safe to call more than once.
20374
+ */
20375
+ dispose = () => {
20376
+ if (this.disposed)
20377
+ return;
20378
+ this.disposed = true;
20379
+ this.piped = undefined;
20380
+ this.worker.removeEventListener('message', this.handleWorkerMessage);
20381
+ this.worker.removeEventListener('error', this.handleWorkerError);
20382
+ this.worker.terminate();
20383
+ URL.revokeObjectURL(this.workerUrl);
20384
+ this.removeAllListeners();
20385
+ };
20386
+ /**
20387
+ * Set a per-user AES-GCM encryption key in the worker's key store.
20388
+ *
20389
+ * Use it when each participant has their own key from a central authority.
20390
+ * The receiver picks the right one by the `keyIndex` in the frame trailer.
20391
+ *
20392
+ * @param userId - The key owner.
20393
+ * @param keyIndex - Increases with each rotation.
20394
+ * @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
20395
+ */
20396
+ setKey = (userId, keyIndex, rawKey) => {
20397
+ this.assertUsable();
20398
+ this.validateKeyIndex(keyIndex);
20399
+ this.validateKeyLength(rawKey);
20400
+ this.worker.postMessage({ type: 'cmd.set_key', userId, keyIndex, rawKey });
20401
+ };
20402
+ /**
20403
+ * Fallback key for any user without a per-user key. The simplest E2EE mode:
20404
+ * one key for everyone, usually passphrase-derived, no distribution needed.
20405
+ * Setting an epoch makes it active for encryption while older epochs remain
20406
+ * available to decrypt in-flight frames until {@link removeSharedKey}.
20407
+ *
20408
+ * @param keyIndex - An integer 0-255, since one trailer byte carries it.
20409
+ * @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
20410
+ */
20411
+ setSharedKey = (keyIndex, rawKey) => {
20412
+ this.assertUsable();
20413
+ this.validateKeyIndex(keyIndex);
20414
+ this.validateKeyLength(rawKey);
20415
+ this.worker.postMessage({ type: 'cmd.set_shared_key', keyIndex, rawKey });
20416
+ };
20417
+ /**
20418
+ * Remove one shared-key epoch from the worker's receive key ring.
20419
+ *
20420
+ * If this is the active epoch, shared-key encryption stops until
20421
+ * {@link setSharedKey} succeeds again. An older epoch is not reactivated.
20422
+ *
20423
+ * @param keyIndex - The exact shared-key epoch to remove.
20424
+ */
20425
+ removeSharedKey = (keyIndex) => {
20426
+ this.assertUsable();
20427
+ this.validateKeyIndex(keyIndex);
20428
+ this.worker.postMessage({ type: 'cmd.remove_shared_key', keyIndex });
20429
+ };
20430
+ /**
20431
+ * Retire one of a user's key epochs, leaving their other epochs usable.
20432
+ *
20433
+ * @param userId - The key owner.
20434
+ * @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
20435
+ */
20436
+ removeKey = (userId, keyIndex) => {
20437
+ this.assertUsable();
20438
+ this.validateKeyIndex(keyIndex);
20439
+ this.worker.postMessage({ type: 'cmd.remove_key', userId, keyIndex });
20440
+ };
20441
+ /**
20442
+ * Drop every key a user holds, revoking their ability to decrypt later
20443
+ * frames. Call it when a participant leaves.
20444
+ *
20445
+ * To retire one rotated epoch instead, use {@link removeKey}.
20446
+ */
20447
+ removeAllKeys = (userId) => {
20448
+ this.assertUsable();
20449
+ this.worker.postMessage({ type: 'cmd.remove_all_keys', userId });
20450
+ };
20451
+ /**
20452
+ * Called by the Publisher when it adds a transceiver.
20453
+ *
20454
+ * @param sender - The sender to encrypt.
20455
+ * @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
20456
+ * @param trackType - Optional label; only groups perf stats.
20457
+ * @internal
20458
+ */
20459
+ encrypt = (sender, codec, trackType) => {
20460
+ this.assertUsable();
20461
+ this.pipe(sender, {
20462
+ operation: 'encode',
20463
+ userId: this.userId,
20464
+ codec,
20465
+ trackType,
20466
+ });
20467
+ };
20468
+ /**
20469
+ * Called by the Subscriber when a remote track arrives.
20470
+ *
20471
+ * @param receiver - The receiver to decrypt.
20472
+ * @param userId - The remote user, for key lookup in the worker.
20473
+ * @param trackType - Optional label; only groups perf stats.
20474
+ * @internal
20475
+ */
20476
+ decrypt = (receiver, userId, trackType) => {
20477
+ this.assertUsable();
20478
+ this.pipe(receiver, { operation: 'decode', userId, trackType });
20479
+ };
20480
+ /** Pipe through the worker's transform, tracking targets to avoid double-piping. */
20481
+ pipe = (target, options) => {
20482
+ if (this.transform === 'script') {
20483
+ target.transform = new RTCRtpScriptTransform(this.worker, options);
20484
+ return;
20485
+ }
20486
+ if ((this.piped ??= new WeakSet()).has(target))
20487
+ return;
20488
+ this.piped.add(target);
20489
+ // @ts-expect-error createEncodedStreams is not in the standard typedefs
20490
+ const { readable, writable } = target.createEncodedStreams();
20491
+ this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
20492
+ };
20493
+ /**
20494
+ * Toggle periodic performance reporting from the E2EE worker.
20495
+ *
20496
+ * While on, the worker emits `e2ee.perf_report` once per second with per-track
20497
+ * FPS and crypto timings. Useful for debugging throughput.
20498
+ */
20499
+ enablePerformanceReporting = (enabled) => {
20500
+ this.assertUsable();
20501
+ this.worker.postMessage({
20502
+ type: 'cmd.enable_performance_reporting',
20503
+ enabled,
20504
+ });
20505
+ };
20506
+ /**
20507
+ * Request a snapshot of the worker's keys. It arrives later as the
20508
+ * `e2ee.key_state` event, listing fingerprints only, never key material.
20509
+ */
20510
+ requestKeyState = () => {
20511
+ this.assertUsable();
20512
+ this.worker.postMessage({ type: 'cmd.request_key_state' });
20513
+ };
20514
+ handleWorkerMessage = (e) => {
20515
+ const { type, ...payload } = e.data ?? {};
20516
+ if (type === 'e2ee.error') {
20517
+ this.logger.error(e.data.message);
20518
+ return;
20519
+ }
20520
+ const event = type;
20521
+ this.logger.debug('Dispatching', event, payload);
20522
+ this.emit(event, payload);
20523
+ };
20524
+ handleWorkerError = (e) => {
20525
+ this.logger.error('Unhandled worker error:', e.message);
20526
+ };
20527
+ validateKeyLength = (rawKey) => {
20528
+ const is256 = this.algorithm === 'AES-256-GCM';
20529
+ const expected = is256 ? 32 : 16;
20530
+ if (rawKey.byteLength !== expected) {
20531
+ throw new Error(`Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`);
20532
+ }
20533
+ };
20534
+ /**
20535
+ * One trailer byte carries the keyIndex. A larger value would truncate to
20536
+ * `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
20537
+ * every decrypt. Reject it rather than ship a silently broken key epoch.
20538
+ */
20539
+ validateKeyIndex = (keyIndex) => {
20540
+ if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
20541
+ throw new Error(`keyIndex must be an integer between 0 and 255, got ${keyIndex}`);
20542
+ }
20543
+ };
20544
+ }
20545
+
20546
+ /**
20547
+ * E2EE worker. The build populates it.
20548
+ *
20549
+ * The actual implementation lives in `e2ee-worker/e2ee-worker-impl.ts` and its
20550
+ * sibling modules (`constants.ts`, `codec.ts`, `crypto.ts`, `utils.ts`,
20551
+ * `types.ts`), all written as real TypeScript with full type checking
20552
+ * and import support.
20553
+ *
20554
+ * ## How it works
20555
+ *
20556
+ * 1. You edit files in `e2ee-worker/` as normal TypeScript
20557
+ * 2. `yarn build` runs Rollup, which invokes `rollup-plugin-inline-worker`
20558
+ * 3. The plugin bundles `e2ee-worker/e2ee-worker-impl.ts` (and its imports)
20559
+ * into a single function via a nested Rollup + TypeScript build
20560
+ * 4. The function replaces this file's export at build time
20561
+ * 5. `EncryptionManager.create()` creates a Worker from `e2eeWorker.toString()`
20562
+ *
20563
+ * In tests, this module is mocked (see `__tests__/EncryptionManager.test.ts`),
20564
+ * so the placeholder is never evaluated.
20565
+ *
20566
+ * @see e2ee-worker/e2ee-worker-impl.ts - the entry point
20567
+ * @see ../../plugins/rollup-plugin-inline-worker.ts - the Rollup plugin
20568
+ * @see EncryptionManager.ts - consumes e2eeWorker
20569
+ */
20570
+ function e2eeWorker() {
20571
+ // will be populated at build time
20572
+ }
20573
+
20574
+ var e2eeWorker$1 = /*#__PURE__*/Object.freeze({
20575
+ __proto__: null,
20576
+ e2eeWorker: e2eeWorker
20577
+ });
20578
+
19770
20579
  Object.defineProperty(exports, "AxiosError", {
19771
20580
  enumerable: true,
19772
20581
  get: function () { return axios.AxiosError; }
@@ -19792,11 +20601,16 @@ exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnu
19792
20601
  exports.DeviceManager = DeviceManager;
19793
20602
  exports.DeviceManagerState = DeviceManagerState;
19794
20603
  exports.DynascaleManager = DynascaleManager;
20604
+ exports.EncryptionManager = EncryptionManager;
20605
+ exports.EncryptionSettingsRequestModeEnum = EncryptionSettingsRequestModeEnum;
20606
+ exports.EncryptionSettingsResponseModeEnum = EncryptionSettingsResponseModeEnum;
19795
20607
  exports.ErrorFromResponse = ErrorFromResponse;
19796
20608
  exports.FrameRecordingSettingsRequestModeEnum = FrameRecordingSettingsRequestModeEnum;
19797
20609
  exports.FrameRecordingSettingsRequestQualityEnum = FrameRecordingSettingsRequestQualityEnum;
19798
20610
  exports.FrameRecordingSettingsResponseModeEnum = FrameRecordingSettingsResponseModeEnum;
20611
+ exports.HLSSettingsRequestQualityTracksEnum = HLSSettingsRequestQualityTracksEnum;
19799
20612
  exports.IndividualRecordingSettingsRequestModeEnum = IndividualRecordingSettingsRequestModeEnum;
20613
+ exports.IndividualRecordingSettingsRequestOutputTypesEnum = IndividualRecordingSettingsRequestOutputTypesEnum;
19800
20614
  exports.IndividualRecordingSettingsResponseModeEnum = IndividualRecordingSettingsResponseModeEnum;
19801
20615
  exports.IngressAudioEncodingOptionsRequestChannelsEnum = IngressAudioEncodingOptionsRequestChannelsEnum;
19802
20616
  exports.IngressSourceRequestFpsEnum = IngressSourceRequestFpsEnum;
@@ -19812,6 +20626,7 @@ exports.RawRecordingSettingsRequestModeEnum = RawRecordingSettingsRequestModeEnu
19812
20626
  exports.RawRecordingSettingsResponseModeEnum = RawRecordingSettingsResponseModeEnum;
19813
20627
  exports.RecordSettingsRequestModeEnum = RecordSettingsRequestModeEnum;
19814
20628
  exports.RecordSettingsRequestQualityEnum = RecordSettingsRequestQualityEnum;
20629
+ exports.RequestPermissionRequestPermissionsEnum = RequestPermissionRequestPermissionsEnum;
19815
20630
  exports.RxUtils = rxUtils;
19816
20631
  exports.ScreenShareManager = ScreenShareManager;
19817
20632
  exports.ScreenShareState = ScreenShareState;
@@ -19833,9 +20648,13 @@ exports.TranscriptionSettingsRequestModeEnum = TranscriptionSettingsRequestModeE
19833
20648
  exports.TranscriptionSettingsResponseClosedCaptionModeEnum = TranscriptionSettingsResponseClosedCaptionModeEnum;
19834
20649
  exports.TranscriptionSettingsResponseLanguageEnum = TranscriptionSettingsResponseLanguageEnum;
19835
20650
  exports.TranscriptionSettingsResponseModeEnum = TranscriptionSettingsResponseModeEnum;
20651
+ exports.TranslationSettingsLanguagesEnum = TranslationSettingsLanguagesEnum;
20652
+ exports.UpdateUserPermissionsRequestGrantPermissionsEnum = UpdateUserPermissionsRequestGrantPermissionsEnum;
20653
+ exports.UpdateUserPermissionsRequestRevokePermissionsEnum = UpdateUserPermissionsRequestRevokePermissionsEnum;
19836
20654
  exports.VideoSettingsRequestCameraFacingEnum = VideoSettingsRequestCameraFacingEnum;
19837
20655
  exports.VideoSettingsResponseCameraFacingEnum = VideoSettingsResponseCameraFacingEnum;
19838
20656
  exports.ViewportTracker = ViewportTracker;
20657
+ exports.WSAuthMessageProductsEnum = WSAuthMessageProductsEnum;
19839
20658
  exports.checkIfAudioOutputChangeSupported = checkIfAudioOutputChangeSupported;
19840
20659
  exports.combineComparators = combineComparators;
19841
20660
  exports.conditional = conditional;
@@ -19849,6 +20668,7 @@ exports.getAudioBrowserPermission = getAudioBrowserPermission;
19849
20668
  exports.getAudioDevices = getAudioDevices;
19850
20669
  exports.getAudioOutputDevices = getAudioOutputDevices;
19851
20670
  exports.getAudioStream = getAudioStream;
20671
+ exports.getCallMediaEngineProvider = getCallMediaEngineProvider;
19852
20672
  exports.getClientDetails = getClientDetails;
19853
20673
  exports.getDeviceState = getDeviceState;
19854
20674
  exports.getScreenShareStream = getScreenShareStream;
@@ -19877,6 +20697,7 @@ exports.reactionType = reactionType;
19877
20697
  exports.resolveDeviceId = resolveDeviceId;
19878
20698
  exports.role = role;
19879
20699
  exports.screenSharing = screenSharing;
20700
+ exports.setCallMediaEngineProvider = setCallMediaEngineProvider;
19880
20701
  exports.setDeviceInfo = setDeviceInfo;
19881
20702
  exports.setOSInfo = setOSInfo;
19882
20703
  exports.setPowerState = setPowerState;