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