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