@stream-io/video-client 1.57.0 → 1.59.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.
- package/CHANGELOG.md +28 -0
- package/dist/e2ee-worker.browser.es.js +1342 -0
- package/dist/e2ee-worker.browser.es.js.map +1 -0
- package/dist/index.browser.es.js +1141 -117
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1196 -115
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +1174 -117
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +66 -9
- package/dist/src/StreamVideoClient.d.ts +9 -1
- package/dist/src/coordinator/connection/connection.d.ts +1 -1
- package/dist/src/devices/CameraManager.d.ts +8 -1
- package/dist/src/devices/DeviceManager.d.ts +2 -1
- package/dist/src/devices/MicrophoneManager.d.ts +8 -1
- package/dist/src/devices/SpeakerManager.d.ts +2 -1
- package/dist/src/events/participant.d.ts +10 -3
- package/dist/src/gen/coordinator/index.d.ts +1386 -240
- package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
- package/dist/src/helpers/MediaStreamRecorder.d.ts +26 -0
- package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
- package/dist/src/helpers/concurrency.d.ts +8 -0
- package/dist/src/helpers/ensureExhausted.d.ts +1 -1
- package/dist/src/helpers/loopback.d.ts +55 -0
- package/dist/src/permissions/PermissionsContext.d.ts +2 -2
- package/dist/src/reporting/ClientEventReporter.d.ts +0 -2
- package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
- package/dist/src/rtc/Subscriber.d.ts +5 -0
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
- package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
- package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
- package/dist/src/rtc/e2ee/events.d.ts +163 -0
- package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
- package/dist/src/rtc/index.d.ts +1 -0
- package/dist/src/rtc/mediaEngine.d.ts +23 -0
- package/dist/src/rtc/types.d.ts +2 -0
- package/dist/src/store/CallState.d.ts +21 -0
- package/dist/src/types.d.ts +15 -6
- package/index.ts +5 -0
- package/package.json +1 -1
- package/src/Call.ts +311 -155
- package/src/StreamVideoClient.ts +35 -8
- package/src/__tests__/Call.lifecycle.test.ts +33 -2
- package/src/__tests__/Call.publishing.test.ts +42 -1
- package/src/__tests__/Call.test.ts +15 -0
- package/src/__tests__/StreamVideoClient.api.test.ts +33 -0
- package/src/__tests__/StreamVideoClient.rewatch.test.ts +213 -0
- package/src/__tests__/StreamVideoClient.ringing.test.ts +11 -4
- package/src/__tests__/StreamVideoClient.test.ts +14 -12
- package/src/coordinator/connection/connection.ts +6 -3
- package/src/devices/AudioDeviceManager.ts +6 -0
- package/src/devices/CameraManager.ts +67 -8
- package/src/devices/DeviceManager.ts +24 -4
- package/src/devices/MicrophoneManager.ts +66 -8
- package/src/devices/ScreenShareManager.ts +3 -0
- package/src/devices/SpeakerManager.ts +42 -12
- package/src/devices/__tests__/CameraManager.test.ts +27 -1
- package/src/devices/__tests__/DeviceManager.test.ts +49 -1
- package/src/devices/__tests__/MicrophoneManager.test.ts +23 -1
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -20
- package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
- package/src/devices/__tests__/mocks.ts +36 -3
- package/src/events/__tests__/participant.test.ts +134 -1
- package/src/events/call.ts +6 -3
- package/src/events/callEventHandlers.ts +9 -3
- package/src/events/participant.ts +42 -7
- package/src/gen/coordinator/index.ts +1402 -236
- package/src/gen/video/sfu/models/models.ts +15 -0
- package/src/helpers/MediaStreamRecorder.ts +104 -0
- package/src/helpers/TypedEventEmitter.ts +71 -0
- package/src/helpers/__tests__/MediaStreamRecorder.test.ts +324 -0
- package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
- package/src/helpers/__tests__/concurrency.test.ts +74 -0
- package/src/helpers/__tests__/loopback.test.ts +367 -0
- package/src/helpers/concurrency.ts +24 -0
- package/src/helpers/ensureExhausted.ts +4 -2
- package/src/helpers/loopback.ts +200 -0
- package/src/permissions/PermissionsContext.ts +10 -5
- package/src/reporting/ClientEventReporter.ts +15 -28
- package/src/reporting/__tests__/ClientEventReporter.test.ts +3 -7
- package/src/rtc/BasePeerConnection.ts +12 -1
- package/src/rtc/Publisher.ts +8 -0
- package/src/rtc/Subscriber.ts +13 -0
- package/src/rtc/__tests__/Publisher.test.ts +101 -0
- package/src/rtc/__tests__/Subscriber.test.ts +190 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
- package/src/rtc/e2ee/E2EEManager.ts +12 -0
- package/src/rtc/e2ee/EncryptionManager.ts +336 -0
- package/src/rtc/e2ee/SPEC.md +586 -0
- package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
- package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
- package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
- package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
- package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
- package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
- package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
- package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
- package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
- package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
- package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
- package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
- package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
- package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
- package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
- package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
- package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
- package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
- package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
- package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
- package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
- package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
- package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
- package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
- package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
- package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
- package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
- package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
- package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
- package/src/rtc/e2ee/e2ee-worker.ts +27 -0
- package/src/rtc/e2ee/events.ts +179 -0
- package/src/rtc/e2ee/transformSupport.ts +35 -0
- package/src/rtc/helpers/rtcConfiguration.ts +4 -2
- package/src/rtc/index.ts +1 -0
- package/src/rtc/mediaEngine.ts +44 -0
- package/src/rtc/types.ts +2 -0
- package/src/store/CallState.ts +33 -1
- package/src/store/__tests__/CallState.test.ts +82 -1
- package/src/types.ts +23 -5
package/dist/index.cjs.js
CHANGED
|
@@ -87,6 +87,22 @@ const CreateDeviceRequestPushProviderEnum = {
|
|
|
87
87
|
HUAWEI: 'huawei',
|
|
88
88
|
XIAOMI: 'xiaomi',
|
|
89
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* @export
|
|
92
|
+
*/
|
|
93
|
+
const EncryptionSettingsRequestModeEnum = {
|
|
94
|
+
AVAILABLE: 'available',
|
|
95
|
+
DISABLED: 'disabled',
|
|
96
|
+
AUTO_ON: 'auto-on',
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* @export
|
|
100
|
+
*/
|
|
101
|
+
const EncryptionSettingsResponseModeEnum = {
|
|
102
|
+
AVAILABLE: 'available',
|
|
103
|
+
DISABLED: 'disabled',
|
|
104
|
+
AUTO_ON: 'auto-on',
|
|
105
|
+
};
|
|
90
106
|
/**
|
|
91
107
|
* @export
|
|
92
108
|
*/
|
|
@@ -113,6 +129,21 @@ const FrameRecordingSettingsResponseModeEnum = {
|
|
|
113
129
|
DISABLED: 'disabled',
|
|
114
130
|
AUTO_ON: 'auto-on',
|
|
115
131
|
};
|
|
132
|
+
/**
|
|
133
|
+
* @export
|
|
134
|
+
*/
|
|
135
|
+
const HLSSettingsRequestQualityTracksEnum = {
|
|
136
|
+
_360P: '360p',
|
|
137
|
+
_480P: '480p',
|
|
138
|
+
_720P: '720p',
|
|
139
|
+
_1080P: '1080p',
|
|
140
|
+
_1440P: '1440p',
|
|
141
|
+
PORTRAIT_360X640: 'portrait-360x640',
|
|
142
|
+
PORTRAIT_480X854: 'portrait-480x854',
|
|
143
|
+
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
144
|
+
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
145
|
+
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
146
|
+
};
|
|
116
147
|
/**
|
|
117
148
|
* @export
|
|
118
149
|
*/
|
|
@@ -121,6 +152,17 @@ const IndividualRecordingSettingsRequestModeEnum = {
|
|
|
121
152
|
DISABLED: 'disabled',
|
|
122
153
|
AUTO_ON: 'auto-on',
|
|
123
154
|
};
|
|
155
|
+
/**
|
|
156
|
+
* @export
|
|
157
|
+
*/
|
|
158
|
+
const IndividualRecordingSettingsRequestOutputTypesEnum = {
|
|
159
|
+
AUDIO_ONLY: 'audio_only',
|
|
160
|
+
VIDEO_ONLY: 'video_only',
|
|
161
|
+
AUDIO_VIDEO: 'audio_video',
|
|
162
|
+
SCREENSHARE_AUDIO_ONLY: 'screenshare_audio_only',
|
|
163
|
+
SCREENSHARE_VIDEO_ONLY: 'screenshare_video_only',
|
|
164
|
+
SCREENSHARE_AUDIO_VIDEO: 'screenshare_audio_video',
|
|
165
|
+
};
|
|
124
166
|
/**
|
|
125
167
|
* @export
|
|
126
168
|
*/
|
|
@@ -279,6 +321,14 @@ const RecordSettingsRequestQualityEnum = {
|
|
|
279
321
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
280
322
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
281
323
|
};
|
|
324
|
+
/**
|
|
325
|
+
* @export
|
|
326
|
+
*/
|
|
327
|
+
const RequestPermissionRequestPermissionsEnum = {
|
|
328
|
+
SCREENSHARE: 'screenshare',
|
|
329
|
+
SEND_AUDIO: 'send-audio',
|
|
330
|
+
SEND_VIDEO: 'send-video',
|
|
331
|
+
};
|
|
282
332
|
/**
|
|
283
333
|
* @export
|
|
284
334
|
*/
|
|
@@ -479,6 +529,59 @@ const TranscriptionSettingsResponseModeEnum = {
|
|
|
479
529
|
DISABLED: 'disabled',
|
|
480
530
|
AUTO_ON: 'auto-on',
|
|
481
531
|
};
|
|
532
|
+
/**
|
|
533
|
+
* @export
|
|
534
|
+
*/
|
|
535
|
+
const TranslationSettingsLanguagesEnum = {
|
|
536
|
+
EN: 'en',
|
|
537
|
+
FR: 'fr',
|
|
538
|
+
ES: 'es',
|
|
539
|
+
DE: 'de',
|
|
540
|
+
IT: 'it',
|
|
541
|
+
NL: 'nl',
|
|
542
|
+
PT: 'pt',
|
|
543
|
+
PL: 'pl',
|
|
544
|
+
CA: 'ca',
|
|
545
|
+
CS: 'cs',
|
|
546
|
+
DA: 'da',
|
|
547
|
+
EL: 'el',
|
|
548
|
+
FI: 'fi',
|
|
549
|
+
ID: 'id',
|
|
550
|
+
JA: 'ja',
|
|
551
|
+
RU: 'ru',
|
|
552
|
+
SV: 'sv',
|
|
553
|
+
TA: 'ta',
|
|
554
|
+
TH: 'th',
|
|
555
|
+
TR: 'tr',
|
|
556
|
+
HU: 'hu',
|
|
557
|
+
RO: 'ro',
|
|
558
|
+
ZH: 'zh',
|
|
559
|
+
AR: 'ar',
|
|
560
|
+
TL: 'tl',
|
|
561
|
+
HE: 'he',
|
|
562
|
+
HI: 'hi',
|
|
563
|
+
HR: 'hr',
|
|
564
|
+
KO: 'ko',
|
|
565
|
+
MS: 'ms',
|
|
566
|
+
NO: 'no',
|
|
567
|
+
UK: 'uk',
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* @export
|
|
571
|
+
*/
|
|
572
|
+
const UpdateUserPermissionsRequestGrantPermissionsEnum = {
|
|
573
|
+
SCREENSHARE: 'screenshare',
|
|
574
|
+
SEND_AUDIO: 'send-audio',
|
|
575
|
+
SEND_VIDEO: 'send-video',
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* @export
|
|
579
|
+
*/
|
|
580
|
+
const UpdateUserPermissionsRequestRevokePermissionsEnum = {
|
|
581
|
+
SCREENSHARE: 'screenshare',
|
|
582
|
+
SEND_AUDIO: 'send-audio',
|
|
583
|
+
SEND_VIDEO: 'send-video',
|
|
584
|
+
};
|
|
482
585
|
/**
|
|
483
586
|
* @export
|
|
484
587
|
*/
|
|
@@ -495,6 +598,14 @@ const VideoSettingsResponseCameraFacingEnum = {
|
|
|
495
598
|
BACK: 'back',
|
|
496
599
|
EXTERNAL: 'external',
|
|
497
600
|
};
|
|
601
|
+
/**
|
|
602
|
+
* @export
|
|
603
|
+
*/
|
|
604
|
+
const WSAuthMessageProductsEnum = {
|
|
605
|
+
CHAT: 'chat',
|
|
606
|
+
VIDEO: 'video',
|
|
607
|
+
FEEDS: 'feeds',
|
|
608
|
+
};
|
|
498
609
|
|
|
499
610
|
class ErrorFromResponse extends Error {
|
|
500
611
|
code;
|
|
@@ -1492,6 +1603,13 @@ class CallState$Type extends runtime.MessageType {
|
|
|
1492
1603
|
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1493
1604
|
T: () => Pin,
|
|
1494
1605
|
},
|
|
1606
|
+
{
|
|
1607
|
+
no: 5,
|
|
1608
|
+
name: 'e2ee_enabled',
|
|
1609
|
+
kind: 'scalar',
|
|
1610
|
+
jsonName: 'e2eeEnabled',
|
|
1611
|
+
T: 8 /*ScalarType.BOOL*/,
|
|
1612
|
+
},
|
|
1495
1613
|
]);
|
|
1496
1614
|
}
|
|
1497
1615
|
}
|
|
@@ -4614,6 +4732,19 @@ const isSvcCodec = (codecOrMimeType) => {
|
|
|
4614
4732
|
codecOrMimeType === 'video/av1');
|
|
4615
4733
|
};
|
|
4616
4734
|
|
|
4735
|
+
/**
|
|
4736
|
+
* The default engine: a thin, stateless wrapper over the WebRTC globals.
|
|
4737
|
+
*/
|
|
4738
|
+
const defaultGlobalsEngine = {
|
|
4739
|
+
dispose: () => Promise.resolve(true),
|
|
4740
|
+
};
|
|
4741
|
+
const defaultGlobalsEngineProvider = () => defaultGlobalsEngine;
|
|
4742
|
+
let provider = defaultGlobalsEngineProvider;
|
|
4743
|
+
const setCallMediaEngineProvider = (newProvider) => {
|
|
4744
|
+
provider = newProvider;
|
|
4745
|
+
};
|
|
4746
|
+
const getCallMediaEngineProvider = () => provider;
|
|
4747
|
+
|
|
4617
4748
|
const sfuEventKinds = {
|
|
4618
4749
|
subscriberOffer: undefined,
|
|
4619
4750
|
publisherAnswer: undefined,
|
|
@@ -4821,7 +4952,9 @@ const parseUfragFromCandidate = (candidate) => {
|
|
|
4821
4952
|
};
|
|
4822
4953
|
|
|
4823
4954
|
const ensureExhausted = (x, message) => {
|
|
4824
|
-
|
|
4955
|
+
if (message) {
|
|
4956
|
+
videoLoggerSystem.getLogger('helpers').warn(message, x);
|
|
4957
|
+
}
|
|
4825
4958
|
};
|
|
4826
4959
|
|
|
4827
4960
|
/**
|
|
@@ -4946,7 +5079,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
4946
5079
|
}
|
|
4947
5080
|
};
|
|
4948
5081
|
|
|
4949
|
-
const version = "1.
|
|
5082
|
+
const version = "1.59.0";
|
|
4950
5083
|
const [major, minor, patch] = version.split('.');
|
|
4951
5084
|
let sdkInfo = {
|
|
4952
5085
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5203,6 +5336,27 @@ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
|
|
|
5203
5336
|
*/
|
|
5204
5337
|
const withCancellation = createRunner(wrapWithCancellation);
|
|
5205
5338
|
const pendingPromises = new Map();
|
|
5339
|
+
/**
|
|
5340
|
+
* Wraps an async function so concurrent calls share the in-flight promise
|
|
5341
|
+
* instead of queueing another execution.
|
|
5342
|
+
*
|
|
5343
|
+
* @param cb Async function to wrap.
|
|
5344
|
+
* @returns Wrapped async function.
|
|
5345
|
+
*/
|
|
5346
|
+
function singleFlight(cb) {
|
|
5347
|
+
let pending;
|
|
5348
|
+
return (...args) => {
|
|
5349
|
+
if (pending)
|
|
5350
|
+
return pending;
|
|
5351
|
+
const promise = cb(...args).finally(() => {
|
|
5352
|
+
if (pending === promise) {
|
|
5353
|
+
pending = undefined;
|
|
5354
|
+
}
|
|
5355
|
+
});
|
|
5356
|
+
pending = promise;
|
|
5357
|
+
return promise;
|
|
5358
|
+
};
|
|
5359
|
+
}
|
|
5206
5360
|
function hasPending(tag) {
|
|
5207
5361
|
return pendingPromises.has(tag);
|
|
5208
5362
|
}
|
|
@@ -5929,6 +6083,7 @@ class CallState {
|
|
|
5929
6083
|
settingsSubject = new rxjs.BehaviorSubject(undefined);
|
|
5930
6084
|
transcribingSubject = new rxjs.BehaviorSubject(false);
|
|
5931
6085
|
captioningSubject = new rxjs.BehaviorSubject(false);
|
|
6086
|
+
e2eeEnabledSubject = new rxjs.BehaviorSubject(false);
|
|
5932
6087
|
endedBySubject = new rxjs.BehaviorSubject(undefined);
|
|
5933
6088
|
thumbnailsSubject = new rxjs.BehaviorSubject(undefined);
|
|
5934
6089
|
membersSubject = new rxjs.BehaviorSubject([]);
|
|
@@ -6087,6 +6242,11 @@ class CallState {
|
|
|
6087
6242
|
* Will provide the closed captioning state of this call.
|
|
6088
6243
|
*/
|
|
6089
6244
|
captioning$;
|
|
6245
|
+
/**
|
|
6246
|
+
* Whether end-to-end encryption is active for this call, as reported by the
|
|
6247
|
+
* SFU in the join response.
|
|
6248
|
+
*/
|
|
6249
|
+
e2eeEnabled$;
|
|
6090
6250
|
/**
|
|
6091
6251
|
* Will provide the user who ended this call.
|
|
6092
6252
|
*/
|
|
@@ -6188,6 +6348,7 @@ class CallState {
|
|
|
6188
6348
|
this.rawRecording$ = duc(this.rawRecordingSubject);
|
|
6189
6349
|
this.transcribing$ = duc(this.transcribingSubject);
|
|
6190
6350
|
this.captioning$ = duc(this.captioningSubject);
|
|
6351
|
+
this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
|
|
6191
6352
|
this.eventHandlers = {
|
|
6192
6353
|
// these events are not updating the call state:
|
|
6193
6354
|
'call.frame_recording_ready': undefined,
|
|
@@ -6290,6 +6451,7 @@ class CallState {
|
|
|
6290
6451
|
clearTimeout(taskId);
|
|
6291
6452
|
this.closedCaptionsTasks.delete(ccKey);
|
|
6292
6453
|
}
|
|
6454
|
+
this.removeAllOrphanedTracks();
|
|
6293
6455
|
};
|
|
6294
6456
|
/**
|
|
6295
6457
|
* Sets the list of criteria that are used to sort the participants.
|
|
@@ -6639,6 +6801,12 @@ class CallState {
|
|
|
6639
6801
|
get transcribing() {
|
|
6640
6802
|
return this.getCurrentValue(this.transcribing$);
|
|
6641
6803
|
}
|
|
6804
|
+
/**
|
|
6805
|
+
* Whether end-to-end encryption is active for this call.
|
|
6806
|
+
*/
|
|
6807
|
+
get e2eeEnabled() {
|
|
6808
|
+
return this.getCurrentValue(this.e2eeEnabled$);
|
|
6809
|
+
}
|
|
6642
6810
|
/**
|
|
6643
6811
|
* Will provide the user who ended this call.
|
|
6644
6812
|
*/
|
|
@@ -6859,6 +7027,18 @@ class CallState {
|
|
|
6859
7027
|
removeOrphanedTrack = (id) => {
|
|
6860
7028
|
this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
|
|
6861
7029
|
};
|
|
7030
|
+
/**
|
|
7031
|
+
* Drops every orphaned track. Call this when the peer connections that own
|
|
7032
|
+
* the stored receivers go away (full leave, reconnect, or migration):
|
|
7033
|
+
* `pc.close()` does not raise the track `ended` event, so the per-track
|
|
7034
|
+
* cleanup never fires and the receivers + their closed PCs would otherwise
|
|
7035
|
+
* leak for the call's lifetime.
|
|
7036
|
+
*
|
|
7037
|
+
* @internal
|
|
7038
|
+
*/
|
|
7039
|
+
removeAllOrphanedTracks = () => {
|
|
7040
|
+
this.orphanedTracks = [];
|
|
7041
|
+
};
|
|
6862
7042
|
/**
|
|
6863
7043
|
* Takes all orphaned tracks with the given track lookup prefix.
|
|
6864
7044
|
* All orphaned tracks with the given track lookup prefix are removed from the call state.
|
|
@@ -6921,7 +7101,7 @@ class CallState {
|
|
|
6921
7101
|
* @param reconnectDetails optional reconnect details.
|
|
6922
7102
|
*/
|
|
6923
7103
|
updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
|
|
6924
|
-
const { participants, participantCount, startedAt, pins } = callState;
|
|
7104
|
+
const { participants, participantCount, startedAt, pins, e2EeEnabled } = callState;
|
|
6925
7105
|
const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
|
|
6926
7106
|
this.setParticipants(() => {
|
|
6927
7107
|
const participantLookup = this.getParticipantLookupBySessionId();
|
|
@@ -6947,6 +7127,7 @@ class CallState {
|
|
|
6947
7127
|
this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
6948
7128
|
this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
|
|
6949
7129
|
this.setServerSidePins(pins);
|
|
7130
|
+
this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
|
|
6950
7131
|
};
|
|
6951
7132
|
updateFromMemberRemoved = (event) => {
|
|
6952
7133
|
this.updateFromCallResponse(event.call);
|
|
@@ -7144,6 +7325,37 @@ class CallState {
|
|
|
7144
7325
|
};
|
|
7145
7326
|
}
|
|
7146
7327
|
|
|
7328
|
+
/**
|
|
7329
|
+
* Detection and selection policy for the two WebRTC Encoded Transform APIs.
|
|
7330
|
+
* Internal: which one the SDK attaches is an RTC-layer detail. Consumers want
|
|
7331
|
+
* `EncryptionManager.isSupported` instead.
|
|
7332
|
+
*/
|
|
7333
|
+
/**
|
|
7334
|
+
* Chrome only, and the reason an RTCPeerConnection carrying E2EE needs the
|
|
7335
|
+
* non-standard `encodedInsertableStreams` flag.
|
|
7336
|
+
*/
|
|
7337
|
+
const hasInsertableStreams = () => typeof RTCRtpSender !== 'undefined' &&
|
|
7338
|
+
'createEncodedStreams' in RTCRtpSender.prototype;
|
|
7339
|
+
/** Whether the standard `RTCRtpScriptTransform` API exists. */
|
|
7340
|
+
const hasScriptTransform = () => typeof RTCRtpScriptTransform !== 'undefined';
|
|
7341
|
+
/**
|
|
7342
|
+
* Which Encoded Transform API to attach E2EE with here.
|
|
7343
|
+
*
|
|
7344
|
+
* - `'insertable'`: legacy Insertable Streams. Used on Chrome, where
|
|
7345
|
+
* `RTCRtpScriptTransform` is still unreliable for E2EE.
|
|
7346
|
+
* - `'script'`: the standard API. Used everywhere else.
|
|
7347
|
+
* - `undefined`: neither exists, so E2EE cannot run.
|
|
7348
|
+
*/
|
|
7349
|
+
const preferredTransform = () => {
|
|
7350
|
+
const insertable = hasInsertableStreams();
|
|
7351
|
+
// Chrome's RTCRtpScriptTransform is still unreliable for E2EE.
|
|
7352
|
+
if (isChrome() && insertable)
|
|
7353
|
+
return 'insertable';
|
|
7354
|
+
if (hasScriptTransform())
|
|
7355
|
+
return 'script';
|
|
7356
|
+
return insertable ? 'insertable' : undefined;
|
|
7357
|
+
};
|
|
7358
|
+
|
|
7147
7359
|
/**
|
|
7148
7360
|
* Flatten the stats report into an array of stats objects.
|
|
7149
7361
|
*
|
|
@@ -8252,6 +8464,7 @@ class BasePeerConnection {
|
|
|
8252
8464
|
state;
|
|
8253
8465
|
dispatcher;
|
|
8254
8466
|
clientPublishOptions;
|
|
8467
|
+
e2ee;
|
|
8255
8468
|
tag;
|
|
8256
8469
|
sfuClient;
|
|
8257
8470
|
onReconnectionNeeded;
|
|
@@ -8273,13 +8486,14 @@ class BasePeerConnection {
|
|
|
8273
8486
|
/**
|
|
8274
8487
|
* Constructs a new `BasePeerConnection` instance.
|
|
8275
8488
|
*/
|
|
8276
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8489
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, e2ee, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8277
8490
|
this.peerType = peerType;
|
|
8278
8491
|
this.sfuClient = sfuClient;
|
|
8279
8492
|
this.state = state;
|
|
8280
8493
|
this.dispatcher = dispatcher;
|
|
8281
8494
|
this.iceRestartDelay = iceRestartDelay;
|
|
8282
8495
|
this.clientPublishOptions = clientPublishOptions;
|
|
8496
|
+
this.e2ee = e2ee;
|
|
8283
8497
|
this.tag = tag;
|
|
8284
8498
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8285
8499
|
this.onIceConnected = onIceConnected;
|
|
@@ -8298,7 +8512,13 @@ class BasePeerConnection {
|
|
|
8298
8512
|
}
|
|
8299
8513
|
}
|
|
8300
8514
|
createPeerConnection = (connectionConfig) => {
|
|
8301
|
-
const
|
|
8515
|
+
const config = { ...connectionConfig };
|
|
8516
|
+
// The legacy Insertable Streams path requires this non-standard flag.
|
|
8517
|
+
if (this.e2ee && hasInsertableStreams()) {
|
|
8518
|
+
// @ts-expect-error not part of the standard lib yet
|
|
8519
|
+
config.encodedInsertableStreams = true;
|
|
8520
|
+
}
|
|
8521
|
+
const pc = new RTCPeerConnection(config);
|
|
8302
8522
|
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
8303
8523
|
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
8304
8524
|
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
@@ -9052,6 +9272,10 @@ class Publisher extends BasePeerConnection {
|
|
|
9052
9272
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9053
9273
|
'maintain-framerate';
|
|
9054
9274
|
await transceiver.sender.setParameters(params);
|
|
9275
|
+
if (this.e2ee) {
|
|
9276
|
+
this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
|
|
9277
|
+
this.logger.debug('E2EE encryptor attached to sender');
|
|
9278
|
+
}
|
|
9055
9279
|
await this.negotiate();
|
|
9056
9280
|
};
|
|
9057
9281
|
/**
|
|
@@ -9621,6 +9845,11 @@ class Subscriber extends BasePeerConnection {
|
|
|
9621
9845
|
throw e;
|
|
9622
9846
|
}
|
|
9623
9847
|
};
|
|
9848
|
+
/**
|
|
9849
|
+
* Whether the given stream is one the SFU echoed back to us
|
|
9850
|
+
* (`allowOwnTracksLoopback`) rather than a local capture stream.
|
|
9851
|
+
*/
|
|
9852
|
+
isSelfSubscribedStream = (stream) => !!stream && !!this.trackedStreams?.has(stream);
|
|
9624
9853
|
handleOnTrack = (e) => {
|
|
9625
9854
|
const { streams, track } = e;
|
|
9626
9855
|
const [primaryStream] = streams;
|
|
@@ -9663,9 +9892,15 @@ class Subscriber extends BasePeerConnection {
|
|
|
9663
9892
|
trackLookupPrefix: trackId,
|
|
9664
9893
|
track: primaryStream,
|
|
9665
9894
|
trackType,
|
|
9895
|
+
receiver: this.e2ee ? e.receiver : undefined,
|
|
9666
9896
|
});
|
|
9667
9897
|
return;
|
|
9668
9898
|
}
|
|
9899
|
+
else if (this.e2ee) {
|
|
9900
|
+
const { userId } = participantToUpdate;
|
|
9901
|
+
this.e2ee.decrypt(e.receiver, userId, TrackType[trackType]);
|
|
9902
|
+
this.logger.debug('E2EE decryptor attached to receiver', userId);
|
|
9903
|
+
}
|
|
9669
9904
|
const streamKindProp = trackTypeToParticipantStreamKey(trackType);
|
|
9670
9905
|
if (!streamKindProp) {
|
|
9671
9906
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
@@ -9704,6 +9939,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9704
9939
|
const target = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9705
9940
|
if (!target)
|
|
9706
9941
|
return;
|
|
9942
|
+
if (target.isLocalParticipant)
|
|
9943
|
+
return;
|
|
9707
9944
|
this.state.updateParticipant(target.sessionId, (p) => {
|
|
9708
9945
|
const current = p.interruptedTracks ?? [];
|
|
9709
9946
|
const has = current.includes(trackType);
|
|
@@ -10446,7 +10683,6 @@ const watchCallRejected = (call) => {
|
|
|
10446
10683
|
*/
|
|
10447
10684
|
const watchCallEnded = (call) => {
|
|
10448
10685
|
return function onCallEnded() {
|
|
10449
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
10450
10686
|
const { callingState } = call.state;
|
|
10451
10687
|
if (callingState !== exports.CallingState.IDLE &&
|
|
10452
10688
|
callingState !== exports.CallingState.LEFT) {
|
|
@@ -10455,9 +10691,13 @@ const watchCallEnded = (call) => {
|
|
|
10455
10691
|
reason: 'call.ended event received',
|
|
10456
10692
|
});
|
|
10457
10693
|
call
|
|
10458
|
-
.leave({
|
|
10694
|
+
.leave({
|
|
10695
|
+
message: 'call.ended event received',
|
|
10696
|
+
reject: false,
|
|
10697
|
+
reason: 'ended',
|
|
10698
|
+
})
|
|
10459
10699
|
.catch((err) => {
|
|
10460
|
-
call.logger.error('Failed to leave call after call.ended
|
|
10700
|
+
call.logger.error('Failed to leave call after call.ended', err);
|
|
10461
10701
|
});
|
|
10462
10702
|
}
|
|
10463
10703
|
};
|
|
@@ -10637,7 +10877,7 @@ const handleRemoteSoftMute = (call) => {
|
|
|
10637
10877
|
/**
|
|
10638
10878
|
* An event responder which handles the `participantJoined` event.
|
|
10639
10879
|
*/
|
|
10640
|
-
const watchParticipantJoined = (state) => {
|
|
10880
|
+
const watchParticipantJoined = (state, e2ee = undefined) => {
|
|
10641
10881
|
return function onParticipantJoined(e) {
|
|
10642
10882
|
const { participant } = e;
|
|
10643
10883
|
if (!participant)
|
|
@@ -10650,7 +10890,7 @@ const watchParticipantJoined = (state) => {
|
|
|
10650
10890
|
// The SFU would send participant info as part of the `join`
|
|
10651
10891
|
// response and then follow up with a `participantJoined` event for
|
|
10652
10892
|
// already announced participants.
|
|
10653
|
-
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
10893
|
+
const orphanedTracks = reconcileOrphanedTracks(state, participant, e2ee);
|
|
10654
10894
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
10655
10895
|
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
10656
10896
|
viewportVisibilityState: {
|
|
@@ -10686,7 +10926,7 @@ const watchParticipantUpdated = (state) => {
|
|
|
10686
10926
|
* An event responder which handles the `trackPublished` event.
|
|
10687
10927
|
* The SFU will send this event when a participant publishes a track.
|
|
10688
10928
|
*/
|
|
10689
|
-
const watchTrackPublished = (state) => {
|
|
10929
|
+
const watchTrackPublished = (state, e2ee = undefined) => {
|
|
10690
10930
|
return function onTrackPublished(e) {
|
|
10691
10931
|
const { type, sessionId } = e;
|
|
10692
10932
|
// An optimization for large calls.
|
|
@@ -10694,7 +10934,7 @@ const watchTrackPublished = (state) => {
|
|
|
10694
10934
|
// events, and instead, it would only provide the participant's information
|
|
10695
10935
|
// once they start publishing a track.
|
|
10696
10936
|
if (e.participant) {
|
|
10697
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10937
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
|
|
10698
10938
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
10699
10939
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10700
10940
|
}
|
|
@@ -10709,12 +10949,12 @@ const watchTrackPublished = (state) => {
|
|
|
10709
10949
|
* An event responder which handles the `trackUnpublished` event.
|
|
10710
10950
|
* The SFU will send this event when a participant unpublishes a track.
|
|
10711
10951
|
*/
|
|
10712
|
-
const watchTrackUnpublished = (state) => {
|
|
10952
|
+
const watchTrackUnpublished = (state, e2ee = undefined) => {
|
|
10713
10953
|
return function onTrackUnpublished(e) {
|
|
10714
10954
|
const { type, sessionId } = e;
|
|
10715
10955
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
10716
10956
|
if (e.participant) {
|
|
10717
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10957
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
|
|
10718
10958
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
10719
10959
|
state.updateOrAddParticipant(sessionId, participant, (p) => ({
|
|
10720
10960
|
pausedTracks: p.pausedTracks?.filter((t) => t !== type),
|
|
@@ -10733,17 +10973,22 @@ const watchTrackUnpublished = (state) => {
|
|
|
10733
10973
|
*
|
|
10734
10974
|
* @param state the call state.
|
|
10735
10975
|
* @param participant the participant.
|
|
10976
|
+
* @param e2ee accessor for the call's encryption manager, if any.
|
|
10736
10977
|
*/
|
|
10737
|
-
const reconcileOrphanedTracks = (state, participant) => {
|
|
10978
|
+
const reconcileOrphanedTracks = (state, participant, e2ee) => {
|
|
10738
10979
|
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
10739
10980
|
if (!orphanTracks.length)
|
|
10740
10981
|
return;
|
|
10982
|
+
const manager = e2ee?.();
|
|
10741
10983
|
const reconciledTracks = {};
|
|
10742
10984
|
for (const orphan of orphanTracks) {
|
|
10743
10985
|
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
10744
10986
|
if (!key)
|
|
10745
10987
|
continue;
|
|
10746
10988
|
reconciledTracks[key] = orphan.track;
|
|
10989
|
+
if (manager && orphan.receiver) {
|
|
10990
|
+
manager.decrypt(orphan.receiver, participant.userId, TrackType[orphan.trackType]);
|
|
10991
|
+
}
|
|
10747
10992
|
}
|
|
10748
10993
|
return reconciledTracks;
|
|
10749
10994
|
};
|
|
@@ -10799,6 +11044,11 @@ const watchAudioLevelChanged = (dispatcher, state) => {
|
|
|
10799
11044
|
*/
|
|
10800
11045
|
const registerEventHandlers = (call, dispatcher) => {
|
|
10801
11046
|
const state = call.state;
|
|
11047
|
+
// Read lazily on each event: setE2EEManager can run after setup() (an app that
|
|
11048
|
+
// inspects call settings via get()/getOrCreate() before deciding to encrypt has
|
|
11049
|
+
// already triggered it), so a value captured here would be a stale undefined
|
|
11050
|
+
// for the whole call and orphaned tracks would never get a decryptor.
|
|
11051
|
+
const e2ee = () => call.e2eeManager;
|
|
10802
11052
|
const eventHandlers = [
|
|
10803
11053
|
call.on('call.ended', watchCallEnded(call)),
|
|
10804
11054
|
watchSfuCallEnded(call),
|
|
@@ -10806,11 +11056,11 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
10806
11056
|
watchSfuErrorReports(dispatcher),
|
|
10807
11057
|
watchConnectionQualityChanged(dispatcher, state),
|
|
10808
11058
|
watchParticipantCountChanged(dispatcher, state),
|
|
10809
|
-
call.on('participantJoined', watchParticipantJoined(state)),
|
|
11059
|
+
call.on('participantJoined', watchParticipantJoined(state, e2ee)),
|
|
10810
11060
|
call.on('participantLeft', watchParticipantLeft(state)),
|
|
10811
11061
|
call.on('participantUpdated', watchParticipantUpdated(state)),
|
|
10812
|
-
call.on('trackPublished', watchTrackPublished(state)),
|
|
10813
|
-
call.on('trackUnpublished', watchTrackUnpublished(state)),
|
|
11062
|
+
call.on('trackPublished', watchTrackPublished(state, e2ee)),
|
|
11063
|
+
call.on('trackUnpublished', watchTrackUnpublished(state, e2ee)),
|
|
10814
11064
|
watchAudioLevelChanged(dispatcher, state),
|
|
10815
11065
|
watchDominantSpeakerChanged(dispatcher, state),
|
|
10816
11066
|
call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
|
|
@@ -11938,13 +12188,14 @@ class PermissionsContext {
|
|
|
11938
12188
|
return false;
|
|
11939
12189
|
const { audio, video, screensharing } = settings;
|
|
11940
12190
|
switch (permission) {
|
|
11941
|
-
case
|
|
12191
|
+
case RequestPermissionRequestPermissionsEnum.SEND_AUDIO:
|
|
11942
12192
|
return audio.access_request_enabled;
|
|
11943
|
-
case
|
|
12193
|
+
case RequestPermissionRequestPermissionsEnum.SEND_VIDEO:
|
|
11944
12194
|
return video.access_request_enabled;
|
|
11945
|
-
case
|
|
12195
|
+
case RequestPermissionRequestPermissionsEnum.SCREENSHARE:
|
|
11946
12196
|
return screensharing.access_request_enabled;
|
|
11947
12197
|
default:
|
|
12198
|
+
ensureExhausted(permission);
|
|
11948
12199
|
return false;
|
|
11949
12200
|
}
|
|
11950
12201
|
};
|
|
@@ -12951,6 +13202,27 @@ class DeviceManager {
|
|
|
12951
13202
|
this.filters.forEach((entry) => entry.stop?.());
|
|
12952
13203
|
}
|
|
12953
13204
|
}
|
|
13205
|
+
reconcileOptimisticStatus = async () => {
|
|
13206
|
+
const target = this.state.optimisticStatus;
|
|
13207
|
+
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
|
|
13208
|
+
try {
|
|
13209
|
+
if (target === 'enabled' && this.state.status !== 'enabled') {
|
|
13210
|
+
await this.unmuteStream();
|
|
13211
|
+
if (!signal.aborted)
|
|
13212
|
+
this.state.setStatus('enabled');
|
|
13213
|
+
}
|
|
13214
|
+
else if (target === 'disabled' && this.state.status === 'enabled') {
|
|
13215
|
+
// mirror whatever disable() does to stop/pause the track per disableMode
|
|
13216
|
+
if (!signal.aborted)
|
|
13217
|
+
this.state.setStatus('disabled');
|
|
13218
|
+
}
|
|
13219
|
+
}
|
|
13220
|
+
finally {
|
|
13221
|
+
if (!signal.aborted)
|
|
13222
|
+
this.state.setPendingStatus(this.state.status);
|
|
13223
|
+
}
|
|
13224
|
+
});
|
|
13225
|
+
};
|
|
12954
13226
|
disableTracks() {
|
|
12955
13227
|
this.getTracks().forEach((track) => {
|
|
12956
13228
|
if (track.enabled)
|
|
@@ -13231,7 +13503,7 @@ class DeviceManager {
|
|
|
13231
13503
|
createSyntheticDevice(selectedDevice, deviceKind);
|
|
13232
13504
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13233
13505
|
}
|
|
13234
|
-
async applyPersistedPreferences(enabledInCallType) {
|
|
13506
|
+
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13235
13507
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13236
13508
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
13237
13509
|
const preferenceList = toPreferenceList(preferences[deviceKey]);
|
|
@@ -13258,13 +13530,13 @@ class DeviceManager {
|
|
|
13258
13530
|
}
|
|
13259
13531
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
13260
13532
|
if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
|
|
13261
|
-
await this.applyMutedState(muted);
|
|
13533
|
+
await this.applyMutedState(muted, forceDisabled);
|
|
13262
13534
|
appliedMute = true;
|
|
13263
13535
|
}
|
|
13264
13536
|
return appliedDevice || appliedMute;
|
|
13265
13537
|
}
|
|
13266
|
-
async applyMutedState(muted) {
|
|
13267
|
-
if (this.state.status !== undefined)
|
|
13538
|
+
async applyMutedState(muted, forceDisabled) {
|
|
13539
|
+
if (forceDisabled || this.state.status !== undefined)
|
|
13268
13540
|
return;
|
|
13269
13541
|
if (muted) {
|
|
13270
13542
|
await this.disable();
|
|
@@ -13584,13 +13856,41 @@ class CameraManager extends DeviceManager {
|
|
|
13584
13856
|
}
|
|
13585
13857
|
}
|
|
13586
13858
|
}
|
|
13859
|
+
enable() {
|
|
13860
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13861
|
+
this.state.setPendingStatus('enabled');
|
|
13862
|
+
return Promise.resolve();
|
|
13863
|
+
}
|
|
13864
|
+
return super.enable();
|
|
13865
|
+
}
|
|
13866
|
+
async disable(forceStopOrOptions) {
|
|
13867
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13868
|
+
this.state.setPendingStatus('disabled');
|
|
13869
|
+
return;
|
|
13870
|
+
}
|
|
13871
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
13872
|
+
if (forceStopOrOptions === undefined)
|
|
13873
|
+
return super.disable();
|
|
13874
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
13875
|
+
return super.disable(forceStopOrOptions);
|
|
13876
|
+
}
|
|
13877
|
+
return super.disable(forceStopOrOptions);
|
|
13878
|
+
}
|
|
13879
|
+
toggle() {
|
|
13880
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13881
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
13882
|
+
return Promise.resolve();
|
|
13883
|
+
}
|
|
13884
|
+
return super.toggle();
|
|
13885
|
+
}
|
|
13587
13886
|
/**
|
|
13588
13887
|
* Applies the video settings to the camera.
|
|
13589
13888
|
*
|
|
13590
13889
|
* @param settings the video settings to apply.
|
|
13591
13890
|
* @param publish whether to publish the stream after applying the settings.
|
|
13891
|
+
* @param forceDisabled whether to keep the camera disabled, regardless of server-side or local preferences.
|
|
13592
13892
|
*/
|
|
13593
|
-
async apply(settings, publish) {
|
|
13893
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
13594
13894
|
// Wait for any in progress camera operation
|
|
13595
13895
|
await this.statusChangeSettled();
|
|
13596
13896
|
await this.selectTargetResolution(settings.target_resolution);
|
|
@@ -13602,8 +13902,7 @@ class CameraManager extends DeviceManager {
|
|
|
13602
13902
|
if (shouldApplyDefaults &&
|
|
13603
13903
|
this.devicePersistence.enabled &&
|
|
13604
13904
|
permissionState === 'granted') {
|
|
13605
|
-
persistedPreferencesApplied =
|
|
13606
|
-
await this.applyPersistedPreferences(enabledInCallType);
|
|
13905
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(enabledInCallType, forceDisabled);
|
|
13607
13906
|
}
|
|
13608
13907
|
// apply a direction and enable the camera only if in "pristine" state,
|
|
13609
13908
|
// and there are no persisted preferences
|
|
@@ -13613,13 +13912,23 @@ class CameraManager extends DeviceManager {
|
|
|
13613
13912
|
const direction = settings.camera_facing === 'front' ? 'front' : 'back';
|
|
13614
13913
|
await this.selectDirection(direction, { enableCamera: false });
|
|
13615
13914
|
}
|
|
13616
|
-
if (
|
|
13915
|
+
if (!forceDisabled &&
|
|
13916
|
+
canPublish &&
|
|
13917
|
+
settings.camera_default_on &&
|
|
13918
|
+
enabledInCallType) {
|
|
13617
13919
|
await this.enable();
|
|
13618
13920
|
}
|
|
13619
13921
|
}
|
|
13620
|
-
|
|
13621
|
-
|
|
13622
|
-
|
|
13922
|
+
if (isReactNative() && publish && canPublish) {
|
|
13923
|
+
// On RN the camera is enabled/disabled optimistically before JOINED. Reconcile now
|
|
13924
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
13925
|
+
await this.reconcileOptimisticStatus();
|
|
13926
|
+
}
|
|
13927
|
+
else {
|
|
13928
|
+
const { mediaStream } = this.state;
|
|
13929
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
13930
|
+
await this.publishStream(mediaStream);
|
|
13931
|
+
}
|
|
13623
13932
|
}
|
|
13624
13933
|
}
|
|
13625
13934
|
getDevices() {
|
|
@@ -13638,7 +13947,10 @@ class CameraManager extends DeviceManager {
|
|
|
13638
13947
|
}
|
|
13639
13948
|
return constraints;
|
|
13640
13949
|
}
|
|
13641
|
-
getStream(constraints) {
|
|
13950
|
+
async getStream(constraints) {
|
|
13951
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
13952
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
13953
|
+
await this.call.ensureMediaFactory();
|
|
13642
13954
|
return getVideoStream(constraints, this.call.tracer);
|
|
13643
13955
|
}
|
|
13644
13956
|
}
|
|
@@ -13654,6 +13966,9 @@ class AudioDeviceManager extends DeviceManager {
|
|
|
13654
13966
|
if (!this.call.state.settings?.audio.hifi_audio_enabled) {
|
|
13655
13967
|
throw new Error('High Fidelity audio is not enabled for this call');
|
|
13656
13968
|
}
|
|
13969
|
+
if (isReactNative() && this.call.hasMediaEngine) {
|
|
13970
|
+
throw new Error('setAudioBitrateProfile must be called before joining the call.');
|
|
13971
|
+
}
|
|
13657
13972
|
this.doSetAudioBitrateProfile(profile);
|
|
13658
13973
|
this.state.setAudioBitrateProfile(profile);
|
|
13659
13974
|
if (this.enabled) {
|
|
@@ -13926,7 +14241,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
13926
14241
|
]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
|
|
13927
14242
|
try {
|
|
13928
14243
|
if (callingState === exports.CallingState.LEFT) {
|
|
13929
|
-
|
|
14244
|
+
// The muted-recording-prepared mode is reset in `callManager.stop()`
|
|
14245
|
+
// (during leave, while the call factory is still alive), not here —
|
|
14246
|
+
// this subscription fires asynchronously and could land after the
|
|
14247
|
+
// factory is disposed, forcing a default-ADM rebuild.
|
|
13930
14248
|
await this.stopSpeakingWhileMutedDetection();
|
|
13931
14249
|
}
|
|
13932
14250
|
if (callingState !== exports.CallingState.JOINED)
|
|
@@ -14026,6 +14344,33 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14026
14344
|
this.subscriptions.push(unsubscribe);
|
|
14027
14345
|
}
|
|
14028
14346
|
}
|
|
14347
|
+
enable() {
|
|
14348
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14349
|
+
this.state.setPendingStatus('enabled');
|
|
14350
|
+
return Promise.resolve();
|
|
14351
|
+
}
|
|
14352
|
+
return super.enable();
|
|
14353
|
+
}
|
|
14354
|
+
async disable(forceStopOrOptions) {
|
|
14355
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14356
|
+
this.state.setPendingStatus('disabled');
|
|
14357
|
+
return;
|
|
14358
|
+
}
|
|
14359
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
14360
|
+
if (forceStopOrOptions === undefined)
|
|
14361
|
+
return super.disable();
|
|
14362
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
14363
|
+
return super.disable(forceStopOrOptions);
|
|
14364
|
+
}
|
|
14365
|
+
return super.disable(forceStopOrOptions);
|
|
14366
|
+
}
|
|
14367
|
+
toggle() {
|
|
14368
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14369
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
14370
|
+
return Promise.resolve();
|
|
14371
|
+
}
|
|
14372
|
+
return super.toggle();
|
|
14373
|
+
}
|
|
14029
14374
|
/**
|
|
14030
14375
|
* Enables noise cancellation for the microphone.
|
|
14031
14376
|
*
|
|
@@ -14165,8 +14510,9 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14165
14510
|
* Applies the audio settings to the microphone.
|
|
14166
14511
|
* @param settings the audio settings to apply.
|
|
14167
14512
|
* @param publish whether to publish the stream after applying the settings.
|
|
14513
|
+
* @param forceDisabled whether to keep the mic disabled, regardless of server-side or local preferences.
|
|
14168
14514
|
*/
|
|
14169
|
-
async apply(settings, publish) {
|
|
14515
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
14170
14516
|
// Wait for any in progress mic operation
|
|
14171
14517
|
await this.statusChangeSettled();
|
|
14172
14518
|
// apply server-side settings only when the device state is pristine
|
|
@@ -14178,23 +14524,33 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14178
14524
|
if (shouldApplyDefaults &&
|
|
14179
14525
|
this.devicePersistence.enabled &&
|
|
14180
14526
|
permissionState === 'granted') {
|
|
14181
|
-
persistedPreferencesApplied = await this.applyPersistedPreferences(true);
|
|
14527
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(true, forceDisabled);
|
|
14182
14528
|
}
|
|
14183
14529
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
14184
14530
|
if (shouldApplyDefaults && !persistedPreferencesApplied) {
|
|
14185
|
-
if (canPublish && settings.mic_default_on) {
|
|
14531
|
+
if (!forceDisabled && canPublish && settings.mic_default_on) {
|
|
14186
14532
|
await this.enable();
|
|
14187
14533
|
}
|
|
14188
14534
|
}
|
|
14189
|
-
|
|
14190
|
-
|
|
14191
|
-
|
|
14535
|
+
if (isReactNative() && publish && canPublish) {
|
|
14536
|
+
// On RN the microphone is enabled/disabled optimistically before JOINED. Reconcile now
|
|
14537
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
14538
|
+
await this.reconcileOptimisticStatus();
|
|
14539
|
+
}
|
|
14540
|
+
else {
|
|
14541
|
+
const { mediaStream } = this.state;
|
|
14542
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
14543
|
+
await this.publishStream(mediaStream);
|
|
14544
|
+
}
|
|
14192
14545
|
}
|
|
14193
14546
|
}
|
|
14194
14547
|
getDevices() {
|
|
14195
14548
|
return getAudioDevices(this.call.tracer);
|
|
14196
14549
|
}
|
|
14197
|
-
getStream(constraints) {
|
|
14550
|
+
async getStream(constraints) {
|
|
14551
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14552
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14553
|
+
await this.call.ensureMediaFactory();
|
|
14198
14554
|
return getAudioStream(constraints, this.call.tracer);
|
|
14199
14555
|
}
|
|
14200
14556
|
doSetAudioBitrateProfile(profile) {
|
|
@@ -14403,6 +14759,9 @@ class ScreenShareManager extends AudioDeviceManager {
|
|
|
14403
14759
|
if (!this.state.audioEnabled) {
|
|
14404
14760
|
constraints.audio = false;
|
|
14405
14761
|
}
|
|
14762
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14763
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14764
|
+
await this.call.ensureMediaFactory();
|
|
14406
14765
|
const stream = await getScreenShareStream(constraints, this.call.tracer);
|
|
14407
14766
|
const [track] = stream.getVideoTracks();
|
|
14408
14767
|
const { contentHint } = this.state.settings || {};
|
|
@@ -14507,10 +14866,14 @@ class SpeakerManager {
|
|
|
14507
14866
|
this.devicePersistence = devicePreferences;
|
|
14508
14867
|
this.setup();
|
|
14509
14868
|
}
|
|
14510
|
-
apply(settings) {
|
|
14511
|
-
|
|
14869
|
+
async apply(settings) {
|
|
14870
|
+
if (isReactNative()) {
|
|
14871
|
+
this.applyRN(settings);
|
|
14872
|
+
return;
|
|
14873
|
+
}
|
|
14874
|
+
await this.applyWeb();
|
|
14512
14875
|
}
|
|
14513
|
-
applyWeb() {
|
|
14876
|
+
async applyWeb() {
|
|
14514
14877
|
const { enabled, storageKey } = this.devicePersistence;
|
|
14515
14878
|
if (!enabled)
|
|
14516
14879
|
return;
|
|
@@ -14522,8 +14885,22 @@ class SpeakerManager {
|
|
|
14522
14885
|
const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
|
|
14523
14886
|
? ''
|
|
14524
14887
|
: preference.selectedDeviceId;
|
|
14525
|
-
if (
|
|
14526
|
-
this.
|
|
14888
|
+
if (!nextDeviceId) {
|
|
14889
|
+
if (this.state.selectedDevice !== nextDeviceId) {
|
|
14890
|
+
this.select(nextDeviceId);
|
|
14891
|
+
}
|
|
14892
|
+
return;
|
|
14893
|
+
}
|
|
14894
|
+
const permissionState = await rxjs.firstValueFrom(getAudioBrowserPermission(this.call.tracer).asStateObservable());
|
|
14895
|
+
if (permissionState !== 'granted')
|
|
14896
|
+
return;
|
|
14897
|
+
const devices = await rxjs.firstValueFrom(this.listDevices());
|
|
14898
|
+
const device = this.findDevice(devices, nextDeviceId) ??
|
|
14899
|
+
(preference.selectedDeviceLabel
|
|
14900
|
+
? devices.find((d) => d.label === preference.selectedDeviceLabel)
|
|
14901
|
+
: undefined);
|
|
14902
|
+
if (device && this.state.selectedDevice !== device.deviceId) {
|
|
14903
|
+
this.select(device.deviceId);
|
|
14527
14904
|
}
|
|
14528
14905
|
}
|
|
14529
14906
|
applyRN(settings) {
|
|
@@ -14552,6 +14929,7 @@ class SpeakerManager {
|
|
|
14552
14929
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
14553
14930
|
defaultDevice,
|
|
14554
14931
|
isRingingTypeCall: this.call.ringing,
|
|
14932
|
+
cid: this.call.cid,
|
|
14555
14933
|
});
|
|
14556
14934
|
}
|
|
14557
14935
|
}
|
|
@@ -14560,11 +14938,15 @@ class SpeakerManager {
|
|
|
14560
14938
|
return;
|
|
14561
14939
|
this.areSubscriptionsSetUp = true;
|
|
14562
14940
|
if (deviceIds$ && !isReactNative()) {
|
|
14563
|
-
this.subscriptions.push(createSubscription(rxjs.combineLatest([
|
|
14941
|
+
this.subscriptions.push(createSubscription(rxjs.combineLatest([
|
|
14942
|
+
deviceIds$.pipe(rxjs.pairwise()),
|
|
14943
|
+
this.state.selectedDevice$,
|
|
14944
|
+
]), ([[prevDevices, currentDevices], deviceId]) => {
|
|
14564
14945
|
if (!deviceId)
|
|
14565
14946
|
return;
|
|
14566
|
-
const
|
|
14567
|
-
|
|
14947
|
+
const isDisconnected = this.findDevice(prevDevices, deviceId) &&
|
|
14948
|
+
!this.findDevice(currentDevices, deviceId);
|
|
14949
|
+
if (isDisconnected)
|
|
14568
14950
|
this.select('');
|
|
14569
14951
|
}));
|
|
14570
14952
|
}
|
|
@@ -14611,6 +14993,7 @@ class SpeakerManager {
|
|
|
14611
14993
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
14612
14994
|
this.subscriptions = [];
|
|
14613
14995
|
this.areSubscriptionsSetUp = false;
|
|
14996
|
+
this.defaultDevice = undefined;
|
|
14614
14997
|
};
|
|
14615
14998
|
/**
|
|
14616
14999
|
* Set the volume of the audio elements
|
|
@@ -14645,6 +15028,7 @@ class SpeakerManager {
|
|
|
14645
15028
|
return { audioVolume: volume };
|
|
14646
15029
|
});
|
|
14647
15030
|
}
|
|
15031
|
+
findDevice = (devices, deviceId) => devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
|
|
14648
15032
|
persistSpeakerDevicePreference(selectedDevice) {
|
|
14649
15033
|
const { storageKey } = this.devicePersistence;
|
|
14650
15034
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
@@ -14727,6 +15111,7 @@ class Call {
|
|
|
14727
15111
|
blockedAudioTracker;
|
|
14728
15112
|
subscriber;
|
|
14729
15113
|
publisher;
|
|
15114
|
+
e2eeManager;
|
|
14730
15115
|
/**
|
|
14731
15116
|
* Flag telling whether this call is a "ringing" call.
|
|
14732
15117
|
*/
|
|
@@ -14782,6 +15167,8 @@ class Call {
|
|
|
14782
15167
|
allowOwnTracksLoopback = false;
|
|
14783
15168
|
hasJoinedOnce = false;
|
|
14784
15169
|
deviceSettingsAppliedOnce = false;
|
|
15170
|
+
callManagerStarted = false;
|
|
15171
|
+
leaveGeneration = 0;
|
|
14785
15172
|
credentials;
|
|
14786
15173
|
initialized = false;
|
|
14787
15174
|
acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
|
|
@@ -14800,6 +15187,13 @@ class Call {
|
|
|
14800
15187
|
clientCapabilities = new Set([
|
|
14801
15188
|
ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
|
|
14802
15189
|
]);
|
|
15190
|
+
/**
|
|
15191
|
+
* The in-flight per-call media engine. On web/React this resolves to a thin
|
|
15192
|
+
* globals-backed engine (no provider registered); React Native registers a
|
|
15193
|
+
* provider that owns a per-call native factory.
|
|
15194
|
+
* @internal
|
|
15195
|
+
*/
|
|
15196
|
+
mediaEnginePromise;
|
|
14803
15197
|
/**
|
|
14804
15198
|
* Constructs a new `Call` instance.
|
|
14805
15199
|
*
|
|
@@ -15069,6 +15463,7 @@ class Call {
|
|
|
15069
15463
|
if (callingState === exports.CallingState.LEFT) {
|
|
15070
15464
|
return;
|
|
15071
15465
|
}
|
|
15466
|
+
this.leaveGeneration += 1;
|
|
15072
15467
|
if (callingState === exports.CallingState.JOINING) {
|
|
15073
15468
|
const waitUntilCallJoined = () => {
|
|
15074
15469
|
return new Promise((resolve) => {
|
|
@@ -15102,7 +15497,7 @@ class Call {
|
|
|
15102
15497
|
}
|
|
15103
15498
|
}
|
|
15104
15499
|
}
|
|
15105
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
15500
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, reason === 'ended' ? 'remote' : undefined);
|
|
15106
15501
|
this.statsReporter?.stop();
|
|
15107
15502
|
this.statsReporter = undefined;
|
|
15108
15503
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -15155,12 +15550,14 @@ class Call {
|
|
|
15155
15550
|
this.clientStore.unregisterCall(this);
|
|
15156
15551
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15157
15552
|
isRingingTypeCall: this.ringing,
|
|
15553
|
+
shouldStopCallManager: this.callManagerStarted,
|
|
15158
15554
|
});
|
|
15159
15555
|
this.camera.dispose();
|
|
15160
15556
|
this.microphone.dispose();
|
|
15161
15557
|
this.screenShare.dispose();
|
|
15162
15558
|
this.speaker.dispose();
|
|
15163
15559
|
this.deviceSettingsAppliedOnce = false;
|
|
15560
|
+
this.callManagerStarted = false;
|
|
15164
15561
|
const stopOnLeavePromises = [];
|
|
15165
15562
|
if (this.camera.stopOnLeave) {
|
|
15166
15563
|
stopOnLeavePromises.push(this.camera.disable(true));
|
|
@@ -15172,6 +15569,22 @@ class Call {
|
|
|
15172
15569
|
stopOnLeavePromises.push(this.screenShare.disable(true));
|
|
15173
15570
|
}
|
|
15174
15571
|
await Promise.all(stopOnLeavePromises);
|
|
15572
|
+
// Dispose the per-call media engine last — after peer connections and
|
|
15573
|
+
// local tracks are gone — so the backing factory tears down with no
|
|
15574
|
+
// owned PCs/tracks. A fresh `join()` builds a new engine.
|
|
15575
|
+
if (this.mediaEnginePromise) {
|
|
15576
|
+
const enginePromise = this.mediaEnginePromise;
|
|
15577
|
+
this.mediaEnginePromise = undefined;
|
|
15578
|
+
this.logger.debug('Disposing per-call media factory');
|
|
15579
|
+
await enginePromise
|
|
15580
|
+
.then((engine) => {
|
|
15581
|
+
globalThis.streamRNVideoSDK?.callingX?.unwireAudioEngineSubscription();
|
|
15582
|
+
return engine.dispose();
|
|
15583
|
+
})
|
|
15584
|
+
.catch((err) => {
|
|
15585
|
+
this.logger.warn('Failed to dispose media engine', err);
|
|
15586
|
+
});
|
|
15587
|
+
}
|
|
15175
15588
|
});
|
|
15176
15589
|
};
|
|
15177
15590
|
/**
|
|
@@ -15247,8 +15660,18 @@ class Call {
|
|
|
15247
15660
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
15248
15661
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
15249
15662
|
this.clientStore.setCalls([this, ...calls]);
|
|
15250
|
-
|
|
15251
|
-
|
|
15663
|
+
await this.applyDeviceConfig(settings, { publish: false });
|
|
15664
|
+
};
|
|
15665
|
+
/**
|
|
15666
|
+
* Applies a backend-provided call state snapshot
|
|
15667
|
+
* (call, members, own capabilities) to this instance.
|
|
15668
|
+
*
|
|
15669
|
+
* @internal an internal method and should not be used outside the SDK.
|
|
15670
|
+
*/
|
|
15671
|
+
updateFromCallStateResponse = (response) => {
|
|
15672
|
+
this.state.updateFromCallResponse(response.call);
|
|
15673
|
+
this.state.setMembers(response.members);
|
|
15674
|
+
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15252
15675
|
};
|
|
15253
15676
|
/**
|
|
15254
15677
|
* Loads the information about the call.
|
|
@@ -15261,9 +15684,7 @@ class Call {
|
|
|
15261
15684
|
get = async (params) => {
|
|
15262
15685
|
await this.setup();
|
|
15263
15686
|
const response = await this.streamClient.get(this.streamClientBasePath, params);
|
|
15264
|
-
this.
|
|
15265
|
-
this.state.setMembers(response.members);
|
|
15266
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15687
|
+
this.updateFromCallStateResponse(response);
|
|
15267
15688
|
if (params?.ring) {
|
|
15268
15689
|
this.ringingSubject.next(true);
|
|
15269
15690
|
}
|
|
@@ -15271,13 +15692,7 @@ class Call {
|
|
|
15271
15692
|
this.watching = true;
|
|
15272
15693
|
this.clientStore.registerOrUpdateCall(this);
|
|
15273
15694
|
}
|
|
15274
|
-
|
|
15275
|
-
const skipSpeakerApply = isReactNative()
|
|
15276
|
-
? params?.ring === true
|
|
15277
|
-
? true
|
|
15278
|
-
: this.ringing
|
|
15279
|
-
: false;
|
|
15280
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15695
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15281
15696
|
return response;
|
|
15282
15697
|
};
|
|
15283
15698
|
/**
|
|
@@ -15288,9 +15703,7 @@ class Call {
|
|
|
15288
15703
|
getOrCreate = async (data) => {
|
|
15289
15704
|
await this.setup();
|
|
15290
15705
|
const response = await this.streamClient.post(this.streamClientBasePath, data);
|
|
15291
|
-
this.
|
|
15292
|
-
this.state.setMembers(response.members);
|
|
15293
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15706
|
+
this.updateFromCallStateResponse(response);
|
|
15294
15707
|
if (data?.ring) {
|
|
15295
15708
|
this.ringingSubject.next(true);
|
|
15296
15709
|
}
|
|
@@ -15298,13 +15711,7 @@ class Call {
|
|
|
15298
15711
|
this.watching = true;
|
|
15299
15712
|
this.clientStore.registerOrUpdateCall(this);
|
|
15300
15713
|
}
|
|
15301
|
-
|
|
15302
|
-
const skipSpeakerApply = isReactNative()
|
|
15303
|
-
? data?.ring === true
|
|
15304
|
-
? true
|
|
15305
|
-
: this.ringing
|
|
15306
|
-
: false;
|
|
15307
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15714
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15308
15715
|
return response;
|
|
15309
15716
|
};
|
|
15310
15717
|
/**
|
|
@@ -15368,7 +15775,7 @@ class Call {
|
|
|
15368
15775
|
*
|
|
15369
15776
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15370
15777
|
*/
|
|
15371
|
-
join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15778
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15372
15779
|
const callingState = this.state.callingState;
|
|
15373
15780
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
15374
15781
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -15390,7 +15797,6 @@ class Call {
|
|
|
15390
15797
|
callId: this.id,
|
|
15391
15798
|
getCallSessionId: () => this.state.session?.id ?? '',
|
|
15392
15799
|
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
15393
|
-
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
15394
15800
|
});
|
|
15395
15801
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
15396
15802
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
@@ -15442,7 +15848,7 @@ class Call {
|
|
|
15442
15848
|
callingX?.endCall(this, 'error');
|
|
15443
15849
|
throw error;
|
|
15444
15850
|
}
|
|
15445
|
-
};
|
|
15851
|
+
});
|
|
15446
15852
|
/**
|
|
15447
15853
|
* Will make a single attempt to watch for call related WebSocket events
|
|
15448
15854
|
* and initiate a call session with the server.
|
|
@@ -15452,9 +15858,20 @@ class Call {
|
|
|
15452
15858
|
doJoin = async (data) => {
|
|
15453
15859
|
const connectStartTime = Date.now();
|
|
15454
15860
|
const callingState = this.state.callingState;
|
|
15861
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
15862
|
+
const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
|
|
15455
15863
|
this.joinCallData = data;
|
|
15456
15864
|
this.logger.debug('Starting join flow');
|
|
15457
15865
|
this.state.setCallingState(exports.CallingState.JOINING);
|
|
15866
|
+
// Ensure the per-call media engine exists before any peer connection
|
|
15867
|
+
// (codec probe, subscriber, publisher) or capture happens, so the WebRTC
|
|
15868
|
+
// globals resolve to the call's factory. Idempotent across
|
|
15869
|
+
// reconnect/migration attempts.
|
|
15870
|
+
await this.ensureMediaFactory();
|
|
15871
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
15872
|
+
if (callingX) {
|
|
15873
|
+
callingX.wireAudioEngineSubscription();
|
|
15874
|
+
}
|
|
15458
15875
|
const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
15459
15876
|
const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
15460
15877
|
const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
@@ -15509,6 +15926,11 @@ class Call {
|
|
|
15509
15926
|
// these are throw-away SDPs that the SFU will use to determine
|
|
15510
15927
|
// the capabilities of the client (codec support, etc.)
|
|
15511
15928
|
const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
15929
|
+
// skip if a leave superseded this join so codec detection doesn't resolve to a default factory.
|
|
15930
|
+
if (supersededByLeave()) {
|
|
15931
|
+
this.logger.debug('Join superseded by leave; skipping codec detection');
|
|
15932
|
+
return;
|
|
15933
|
+
}
|
|
15512
15934
|
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
15513
15935
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
15514
15936
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
@@ -15552,6 +15974,12 @@ class Call {
|
|
|
15552
15974
|
throw error;
|
|
15553
15975
|
}
|
|
15554
15976
|
}
|
|
15977
|
+
// If the user left while this join was in flight, bail before re-setting JOINED and before
|
|
15978
|
+
// peer-connection setup below (both run synchronously after this, so one check covers them).
|
|
15979
|
+
if (supersededByLeave()) {
|
|
15980
|
+
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
15981
|
+
return;
|
|
15982
|
+
}
|
|
15555
15983
|
if (!performingMigration) {
|
|
15556
15984
|
// in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
|
|
15557
15985
|
this.state.setCallingState(exports.CallingState.JOINED);
|
|
@@ -15589,12 +16017,21 @@ class Call {
|
|
|
15589
16017
|
}
|
|
15590
16018
|
// device settings should be applied only once, we don't have to
|
|
15591
16019
|
// re-apply them on later reconnections or server-side data fetches
|
|
15592
|
-
if (!this.deviceSettingsAppliedOnce &&
|
|
15593
|
-
|
|
16020
|
+
if (!this.deviceSettingsAppliedOnce &&
|
|
16021
|
+
this.state.settings &&
|
|
16022
|
+
!supersededByLeave()) {
|
|
16023
|
+
await this.applyDeviceConfig(this.state.settings, {
|
|
16024
|
+
publish: true,
|
|
16025
|
+
skipSpeakerApply: false,
|
|
16026
|
+
});
|
|
16027
|
+
this.deviceSettingsAppliedOnce = true;
|
|
16028
|
+
}
|
|
16029
|
+
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
15594
16030
|
globalThis.streamRNVideoSDK?.callManager.start({
|
|
15595
16031
|
isRingingTypeCall: this.ringing,
|
|
16032
|
+
cid: this.cid,
|
|
15596
16033
|
});
|
|
15597
|
-
this.
|
|
16034
|
+
this.callManagerStarted = true;
|
|
15598
16035
|
}
|
|
15599
16036
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
15600
16037
|
// as it's a one-time event and clashes with the potential reconnection attempts.
|
|
@@ -15721,6 +16158,7 @@ class Call {
|
|
|
15721
16158
|
this.sfuStatsReporter = undefined;
|
|
15722
16159
|
if (closePreviousInstances && this.subscriber) {
|
|
15723
16160
|
await this.subscriber.dispose();
|
|
16161
|
+
this.state.removeAllOrphanedTracks();
|
|
15724
16162
|
}
|
|
15725
16163
|
const basePeerConnectionOptions = {
|
|
15726
16164
|
sfuClient,
|
|
@@ -15731,6 +16169,7 @@ class Call {
|
|
|
15731
16169
|
enableTracing,
|
|
15732
16170
|
statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
|
|
15733
16171
|
clientPublishOptions: this.clientPublishOptions,
|
|
16172
|
+
e2ee: this.e2eeManager,
|
|
15734
16173
|
onReconnectionNeeded: (kind, reason, peerType) => {
|
|
15735
16174
|
this.reconnect(kind, reason).catch((err) => {
|
|
15736
16175
|
const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
|
|
@@ -15796,12 +16235,12 @@ class Call {
|
|
|
15796
16235
|
* Retrieves credentials for joining the call.
|
|
15797
16236
|
*
|
|
15798
16237
|
* @internal
|
|
15799
|
-
*
|
|
15800
16238
|
* @param data the join call data.
|
|
15801
16239
|
*/
|
|
15802
16240
|
doJoinRequest = async (data) => {
|
|
15803
16241
|
const location = await this.streamClient.getLocationHint();
|
|
15804
|
-
const
|
|
16242
|
+
const e2ee = !!this.e2eeManager;
|
|
16243
|
+
const request = { ...data, location, e2ee };
|
|
15805
16244
|
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
15806
16245
|
this.state.updateFromCallResponse(joinResponse.call);
|
|
15807
16246
|
this.state.setMembers(joinResponse.members);
|
|
@@ -15820,6 +16259,36 @@ class Call {
|
|
|
15820
16259
|
}
|
|
15821
16260
|
return joinResponse;
|
|
15822
16261
|
};
|
|
16262
|
+
/**
|
|
16263
|
+
* Whether the per-call media engine currently exists. True from join until leave.
|
|
16264
|
+
*
|
|
16265
|
+
* @internal an internal getter and should not be used outside the SDK.
|
|
16266
|
+
*/
|
|
16267
|
+
get hasMediaEngine() {
|
|
16268
|
+
return !!this.mediaEnginePromise;
|
|
16269
|
+
}
|
|
16270
|
+
/**
|
|
16271
|
+
* Ensures a {@link CallMediaEngine} exists for this call's media session and
|
|
16272
|
+
* returns it. Idempotent: the engine is created once via the registered
|
|
16273
|
+
* provider (see `setCallMediaEngineProvider`) and cached until `leave()`
|
|
16274
|
+
* disposes it. Concurrent callers (e.g. camera + microphone enabling in
|
|
16275
|
+
* parallel) share the same engine because the in-flight creation promise is
|
|
16276
|
+
* cached, never the unresolved result.
|
|
16277
|
+
*
|
|
16278
|
+
* @internal
|
|
16279
|
+
*/
|
|
16280
|
+
ensureMediaFactory = async () => {
|
|
16281
|
+
if (!this.mediaEnginePromise) {
|
|
16282
|
+
const provider = getCallMediaEngineProvider();
|
|
16283
|
+
this.logger.debug(`Requesting per-call media factory creation`);
|
|
16284
|
+
this.mediaEnginePromise = Promise.resolve(provider()).catch((err) => {
|
|
16285
|
+
// Drop the cached rejection so a retried join() can rebuild the engine
|
|
16286
|
+
this.mediaEnginePromise = undefined;
|
|
16287
|
+
throw err;
|
|
16288
|
+
});
|
|
16289
|
+
}
|
|
16290
|
+
return this.mediaEnginePromise;
|
|
16291
|
+
};
|
|
15823
16292
|
/**
|
|
15824
16293
|
* Handles the closing of the SFU signal connection.
|
|
15825
16294
|
*
|
|
@@ -16344,12 +16813,16 @@ class Call {
|
|
|
16344
16813
|
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
16345
16814
|
if (!streamStateProp)
|
|
16346
16815
|
continue;
|
|
16347
|
-
this.state.updateParticipant(sessionId, (p) =>
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
|
|
16816
|
+
this.state.updateParticipant(sessionId, (p) => {
|
|
16817
|
+
const currentStream = p[streamStateProp];
|
|
16818
|
+
const isEchoedStream = this.subscriber?.isSelfSubscribedStream(currentStream);
|
|
16819
|
+
return {
|
|
16820
|
+
publishedTracks: mediaStream
|
|
16821
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
16822
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
16823
|
+
[streamStateProp]: isEchoedStream ? currentStream : mediaStream,
|
|
16824
|
+
};
|
|
16825
|
+
});
|
|
16353
16826
|
}
|
|
16354
16827
|
};
|
|
16355
16828
|
/**
|
|
@@ -16381,6 +16854,30 @@ class Call {
|
|
|
16381
16854
|
this.tracer.trace('updatePublishOptions', options);
|
|
16382
16855
|
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
16383
16856
|
};
|
|
16857
|
+
/**
|
|
16858
|
+
* Set the E2EE (end-to-end encryption) manager for this call.
|
|
16859
|
+
*
|
|
16860
|
+
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
16861
|
+
* configured for E2EE.
|
|
16862
|
+
*
|
|
16863
|
+
* The manager is kept across {@link leave} so a rejoin of this same instance
|
|
16864
|
+
* stays encrypted: do not dispose it while this call may be joined again.
|
|
16865
|
+
* A disposed manager throws from `encrypt`/`decrypt` rather than silently
|
|
16866
|
+
* publishing nothing, so re-attach a fresh one instead of reusing it.
|
|
16867
|
+
*
|
|
16868
|
+
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
16869
|
+
* built-in AES-GCM scheme, or pass your own implementation.
|
|
16870
|
+
* @throws if called after the peer connections have been built (i.e. after
|
|
16871
|
+
* `join`): those PCs were already configured without an encryptor, so
|
|
16872
|
+
* adopting a manager now would silently publish/receive cleartext for
|
|
16873
|
+
* the live session.
|
|
16874
|
+
*/
|
|
16875
|
+
setE2EEManager = (e2ee) => {
|
|
16876
|
+
if (this.publisher || this.subscriber) {
|
|
16877
|
+
throw new Error('setE2EEManager must be called before join()');
|
|
16878
|
+
}
|
|
16879
|
+
this.e2eeManager = e2ee;
|
|
16880
|
+
};
|
|
16384
16881
|
/**
|
|
16385
16882
|
* Notifies the SFU that a noise cancellation process has started.
|
|
16386
16883
|
*
|
|
@@ -17015,16 +17512,21 @@ class Call {
|
|
|
17015
17512
|
*
|
|
17016
17513
|
* @internal
|
|
17017
17514
|
*/
|
|
17018
|
-
applyDeviceConfig = async (settings,
|
|
17515
|
+
applyDeviceConfig = async (settings, opts) => {
|
|
17516
|
+
const { publish,
|
|
17517
|
+
// Skip speaker setup on RN until joining (explicitly passed as false during join())
|
|
17518
|
+
skipSpeakerApply = isReactNative(), withDisabledDevices, } = opts;
|
|
17019
17519
|
if (!skipSpeakerApply) {
|
|
17020
|
-
this.speaker
|
|
17520
|
+
await this.speaker
|
|
17521
|
+
.apply(settings)
|
|
17522
|
+
.catch((err) => this.logger.warn('Speaker init failed', err));
|
|
17021
17523
|
}
|
|
17022
|
-
await this.camera
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
await this.microphone
|
|
17026
|
-
|
|
17027
|
-
|
|
17524
|
+
await this.camera
|
|
17525
|
+
.apply(settings.video, publish, withDisabledDevices)
|
|
17526
|
+
.catch((err) => this.logger.warn('Camera init failed', err));
|
|
17527
|
+
await this.microphone
|
|
17528
|
+
.apply(settings.audio, publish, withDisabledDevices)
|
|
17529
|
+
.catch((err) => this.logger.warn('Mic init failed', err));
|
|
17028
17530
|
};
|
|
17029
17531
|
/**
|
|
17030
17532
|
* Will begin tracking the given element for visibility changes within the
|
|
@@ -17696,15 +18198,17 @@ class StableWSConnection {
|
|
|
17696
18198
|
this.client.logger.error(`Token not set, can't connect authenticate`);
|
|
17697
18199
|
return;
|
|
17698
18200
|
}
|
|
17699
|
-
const
|
|
18201
|
+
const wsAuthMessage = {
|
|
17700
18202
|
token,
|
|
18203
|
+
products: [WSAuthMessageProductsEnum.VIDEO],
|
|
17701
18204
|
user_details: {
|
|
17702
18205
|
id: user.id,
|
|
17703
18206
|
name: user.name,
|
|
17704
18207
|
image: user.image,
|
|
17705
18208
|
custom: user.custom,
|
|
17706
18209
|
},
|
|
17707
|
-
}
|
|
18210
|
+
};
|
|
18211
|
+
const authMessage = JSON.stringify(wsAuthMessage);
|
|
17708
18212
|
this._log(`onopen() - Sending auth message ${authMessage}`, {}, 'trace');
|
|
17709
18213
|
this.ws?.send(authMessage);
|
|
17710
18214
|
this._log('onopen() - onopen callback', { wsID });
|
|
@@ -18530,11 +19034,11 @@ class StreamClient {
|
|
|
18530
19034
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18531
19035
|
};
|
|
18532
19036
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18533
|
-
"1.
|
|
19037
|
+
"1.59.0";
|
|
18534
19038
|
getUserAgent = () => {
|
|
18535
19039
|
if (!this.cachedUserAgent) {
|
|
18536
19040
|
const { clientAppIdentifier = {} } = this.options;
|
|
18537
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
19041
|
+
const { sdkName = 'js', sdkVersion = "1.59.0", ...extras } = clientAppIdentifier;
|
|
18538
19042
|
this.cachedUserAgent = [
|
|
18539
19043
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18540
19044
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -18897,7 +19401,6 @@ class ClientEventReporter {
|
|
|
18897
19401
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
18898
19402
|
type: ctx?.callType,
|
|
18899
19403
|
id: ctx?.callId,
|
|
18900
|
-
call_cid: cid,
|
|
18901
19404
|
stage: 'JoinInitiated',
|
|
18902
19405
|
join_attempt_id: joinAttemptId,
|
|
18903
19406
|
...(coordinatorConnectId && {
|
|
@@ -19079,7 +19582,6 @@ class ClientEventReporter {
|
|
|
19079
19582
|
startedAt: Date.now(),
|
|
19080
19583
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19081
19584
|
sfuId: this.getSfuId(cid),
|
|
19082
|
-
userSessionId: this.getUserSessionId(cid),
|
|
19083
19585
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19084
19586
|
lastIceState: iceConnectionState,
|
|
19085
19587
|
};
|
|
@@ -19090,9 +19592,6 @@ class ClientEventReporter {
|
|
|
19090
19592
|
peer_connection: role,
|
|
19091
19593
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19092
19594
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19093
|
-
...(pair.userSessionId && {
|
|
19094
|
-
user_session_id: pair.userSessionId,
|
|
19095
|
-
}),
|
|
19096
19595
|
event_type: 'initiated',
|
|
19097
19596
|
});
|
|
19098
19597
|
};
|
|
@@ -19107,9 +19606,6 @@ class ClientEventReporter {
|
|
|
19107
19606
|
peer_connection: role,
|
|
19108
19607
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19109
19608
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19110
|
-
...(pair.userSessionId && {
|
|
19111
|
-
user_session_id: pair.userSessionId,
|
|
19112
|
-
}),
|
|
19113
19609
|
event_type: 'completed',
|
|
19114
19610
|
outcome: 'success',
|
|
19115
19611
|
retry_count_attempt: 0,
|
|
@@ -19127,9 +19623,6 @@ class ClientEventReporter {
|
|
|
19127
19623
|
...this.sessionIdField(cid),
|
|
19128
19624
|
peer_connection: role,
|
|
19129
19625
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19130
|
-
...(pair.userSessionId && {
|
|
19131
|
-
user_session_id: pair.userSessionId,
|
|
19132
|
-
}),
|
|
19133
19626
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19134
19627
|
event_type: 'completed',
|
|
19135
19628
|
outcome: 'failure',
|
|
@@ -19142,7 +19635,6 @@ class ClientEventReporter {
|
|
|
19142
19635
|
this.peerConnectionPairs.delete(key);
|
|
19143
19636
|
};
|
|
19144
19637
|
getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
19145
|
-
getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
19146
19638
|
sessionIdField = (cid) => {
|
|
19147
19639
|
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
19148
19640
|
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
@@ -19154,7 +19646,6 @@ class ClientEventReporter {
|
|
|
19154
19646
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
19155
19647
|
type: ctx?.callType ?? '',
|
|
19156
19648
|
id: ctx?.callId ?? '',
|
|
19157
|
-
call_cid: cid,
|
|
19158
19649
|
stage,
|
|
19159
19650
|
stage_id: pair.sid,
|
|
19160
19651
|
...(pair.joinAttemptIdSnapshot && {
|
|
@@ -19405,6 +19896,7 @@ class StreamVideoClient {
|
|
|
19405
19896
|
}
|
|
19406
19897
|
else {
|
|
19407
19898
|
await call.updateFromRingingEvent(e);
|
|
19899
|
+
await call.get();
|
|
19408
19900
|
}
|
|
19409
19901
|
}
|
|
19410
19902
|
else {
|
|
@@ -19443,6 +19935,13 @@ class StreamVideoClient {
|
|
|
19443
19935
|
this.logger.error(`Failed to init call from event ${e.type}`, err);
|
|
19444
19936
|
}
|
|
19445
19937
|
};
|
|
19938
|
+
/**
|
|
19939
|
+
* Queries the API for calls matching the given filters.
|
|
19940
|
+
* @param data the query data.
|
|
19941
|
+
*/
|
|
19942
|
+
doQueryCalls = (data) => {
|
|
19943
|
+
return this.streamClient.post('/calls', data);
|
|
19944
|
+
};
|
|
19446
19945
|
/**
|
|
19447
19946
|
* Rewatches the given calls with retry logic.
|
|
19448
19947
|
* @param callsToReWatch array of call IDs to rewatch
|
|
@@ -19453,10 +19952,16 @@ class StreamVideoClient {
|
|
|
19453
19952
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
19454
19953
|
try {
|
|
19455
19954
|
this.logger.info(`Rewatching calls ${callsToReWatch.join(', ')} attempt ${attempt + 1}`);
|
|
19456
|
-
await this.
|
|
19955
|
+
const response = await this.doQueryCalls({
|
|
19457
19956
|
watch: true,
|
|
19458
19957
|
filter_conditions: { cid: { $in: callsToReWatch } },
|
|
19459
19958
|
});
|
|
19959
|
+
for (const c of response.calls) {
|
|
19960
|
+
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
19961
|
+
if (call) {
|
|
19962
|
+
call.updateFromCallStateResponse(c);
|
|
19963
|
+
}
|
|
19964
|
+
}
|
|
19460
19965
|
return;
|
|
19461
19966
|
}
|
|
19462
19967
|
catch (err) {
|
|
@@ -19599,9 +20104,11 @@ class StreamVideoClient {
|
|
|
19599
20104
|
* Will query the API for calls matching the given filters.
|
|
19600
20105
|
*
|
|
19601
20106
|
* @param data the query data.
|
|
20107
|
+
* @param opts additional options, for tweaking the API behavior.
|
|
19602
20108
|
*/
|
|
19603
|
-
queryCalls = async (data = {}) => {
|
|
19604
|
-
const
|
|
20109
|
+
queryCalls = async (data = {}, opts = {}) => {
|
|
20110
|
+
const { withDisabledDevices = true } = opts;
|
|
20111
|
+
const response = await this.doQueryCalls(data);
|
|
19605
20112
|
const calls = [];
|
|
19606
20113
|
for (const c of response.calls) {
|
|
19607
20114
|
const call = new Call({
|
|
@@ -19615,7 +20122,10 @@ class StreamVideoClient {
|
|
|
19615
20122
|
clientStore: this.writeableStateStore,
|
|
19616
20123
|
});
|
|
19617
20124
|
call.state.updateFromCallResponse(c.call);
|
|
19618
|
-
await call.applyDeviceConfig(c.call.settings,
|
|
20125
|
+
await call.applyDeviceConfig(c.call.settings, {
|
|
20126
|
+
publish: false,
|
|
20127
|
+
withDisabledDevices,
|
|
20128
|
+
});
|
|
19619
20129
|
if (data.watch) {
|
|
19620
20130
|
await call.setup();
|
|
19621
20131
|
this.writeableStateStore.registerCall(call);
|
|
@@ -19767,6 +20277,553 @@ const humanize = (n) => {
|
|
|
19767
20277
|
return String(n);
|
|
19768
20278
|
};
|
|
19769
20279
|
|
|
20280
|
+
/** Default loopback stream wait timeout. */
|
|
20281
|
+
const LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = 10 * 1000;
|
|
20282
|
+
/** Default loopback recording duration. */
|
|
20283
|
+
const DEFAULT_LOOPBACK_RECORDING_DURATION_MS = 10 * 1000;
|
|
20284
|
+
/** Shortest permitted loopback recording duration. */
|
|
20285
|
+
const MIN_LOOPBACK_RECORDING_DURATION_MS = 5 * 1000;
|
|
20286
|
+
/** Longest permitted loopback recording duration. */
|
|
20287
|
+
const MAX_LOOPBACK_RECORDING_DURATION_MS = 2 * 60 * 1000;
|
|
20288
|
+
/** Thrown when loopback tracks do not arrive in time. */
|
|
20289
|
+
class LoopbackStreamsTimeoutError extends Error {
|
|
20290
|
+
constructor() {
|
|
20291
|
+
super('Timed out waiting for loopback streams. Ensure the call was joined ' +
|
|
20292
|
+
'with `allowOwnTracksLoopback: true` and that the SFU is configured ' +
|
|
20293
|
+
'to echo self-sub tracks.');
|
|
20294
|
+
this.name = 'LoopbackStreamsTimeoutError';
|
|
20295
|
+
}
|
|
20296
|
+
}
|
|
20297
|
+
/**
|
|
20298
|
+
* Returns the streams on the given participant that the SFU echoed back to us,
|
|
20299
|
+
* ignoring the local capture streams the publishing path writes to the same
|
|
20300
|
+
* fields.
|
|
20301
|
+
*/
|
|
20302
|
+
const getLoopbackStreams = (call, participant) => {
|
|
20303
|
+
const { subscriber } = call ?? {};
|
|
20304
|
+
const { audioStream, videoStream } = participant ?? {};
|
|
20305
|
+
return {
|
|
20306
|
+
loopbackVideoStream: subscriber?.isSelfSubscribedStream(videoStream)
|
|
20307
|
+
? videoStream
|
|
20308
|
+
: undefined,
|
|
20309
|
+
loopbackAudioStream: subscriber?.isSelfSubscribedStream(audioStream)
|
|
20310
|
+
? audioStream
|
|
20311
|
+
: undefined,
|
|
20312
|
+
};
|
|
20313
|
+
};
|
|
20314
|
+
/**
|
|
20315
|
+
* Returns loopback tracks when all required tracks are present.
|
|
20316
|
+
*
|
|
20317
|
+
* Prefer {@link waitForLoopbackStreams}, which observes call state until the
|
|
20318
|
+
* tracks arrive. This is the synchronous snapshot it is built from.
|
|
20319
|
+
*
|
|
20320
|
+
* @internal
|
|
20321
|
+
*/
|
|
20322
|
+
const getLoopbackTracks = (call, participant, includeVideo) => {
|
|
20323
|
+
if (!participant)
|
|
20324
|
+
return undefined;
|
|
20325
|
+
const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreams(call, participant);
|
|
20326
|
+
const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
|
|
20327
|
+
const videoTrack = includeVideo
|
|
20328
|
+
? loopbackVideoStream?.getVideoTracks()[0]
|
|
20329
|
+
: undefined;
|
|
20330
|
+
if (!audioTrack || (includeVideo && !videoTrack)) {
|
|
20331
|
+
return undefined;
|
|
20332
|
+
}
|
|
20333
|
+
return { audioTrack, videoTrack };
|
|
20334
|
+
};
|
|
20335
|
+
/** Waits for the requested loopback tracks. */
|
|
20336
|
+
const waitForLoopbackStreams = (call, opts) => {
|
|
20337
|
+
const callState = call.state;
|
|
20338
|
+
const { includeVideo, signal, timeoutMs = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, } = opts;
|
|
20339
|
+
return new Promise((resolve, reject) => {
|
|
20340
|
+
const initial = getLoopbackTracks(call, callState.localParticipant, includeVideo);
|
|
20341
|
+
if (initial) {
|
|
20342
|
+
resolve(initial);
|
|
20343
|
+
return;
|
|
20344
|
+
}
|
|
20345
|
+
if (signal.aborted) {
|
|
20346
|
+
resolve(null);
|
|
20347
|
+
return;
|
|
20348
|
+
}
|
|
20349
|
+
let settled = false;
|
|
20350
|
+
const subscriptions = new rxjs.Subscription();
|
|
20351
|
+
const cleanup = () => {
|
|
20352
|
+
if (settled)
|
|
20353
|
+
return;
|
|
20354
|
+
settled = true;
|
|
20355
|
+
clearTimeout(timeoutId);
|
|
20356
|
+
signal.removeEventListener('abort', onAbort);
|
|
20357
|
+
subscriptions.unsubscribe();
|
|
20358
|
+
};
|
|
20359
|
+
const onAbort = () => {
|
|
20360
|
+
cleanup();
|
|
20361
|
+
resolve(null);
|
|
20362
|
+
};
|
|
20363
|
+
const timeoutId = setTimeout(() => {
|
|
20364
|
+
cleanup();
|
|
20365
|
+
reject(new LoopbackStreamsTimeoutError());
|
|
20366
|
+
}, timeoutMs);
|
|
20367
|
+
signal.addEventListener('abort', onAbort);
|
|
20368
|
+
subscriptions.add(callState.localParticipant$.subscribe((participant) => {
|
|
20369
|
+
const ready = getLoopbackTracks(call, participant, includeVideo);
|
|
20370
|
+
if (ready) {
|
|
20371
|
+
cleanup();
|
|
20372
|
+
resolve(ready);
|
|
20373
|
+
}
|
|
20374
|
+
}));
|
|
20375
|
+
});
|
|
20376
|
+
};
|
|
20377
|
+
/** Enables loopback audio while `fn` runs, then restores it. */
|
|
20378
|
+
const withLoopbackAudioEnabled = async (tracks, fn) => {
|
|
20379
|
+
const restore = enableLoopbackAudio(tracks);
|
|
20380
|
+
try {
|
|
20381
|
+
return await fn();
|
|
20382
|
+
}
|
|
20383
|
+
finally {
|
|
20384
|
+
restore();
|
|
20385
|
+
}
|
|
20386
|
+
};
|
|
20387
|
+
/** Enables loopback audio and returns a restore function. */
|
|
20388
|
+
const enableLoopbackAudio = (tracks) => {
|
|
20389
|
+
const { audioTrack } = tracks;
|
|
20390
|
+
const previous = audioTrack?.enabled ?? false;
|
|
20391
|
+
if (audioTrack) {
|
|
20392
|
+
audioTrack.enabled = true;
|
|
20393
|
+
}
|
|
20394
|
+
return () => {
|
|
20395
|
+
if (audioTrack) {
|
|
20396
|
+
audioTrack.enabled = previous;
|
|
20397
|
+
}
|
|
20398
|
+
};
|
|
20399
|
+
};
|
|
20400
|
+
/** Clamps and rounds a loopback recording duration. */
|
|
20401
|
+
const clampLoopbackRecordingDuration = (durationMs) => Math.round(Math.min(MAX_LOOPBACK_RECORDING_DURATION_MS, Math.max(MIN_LOOPBACK_RECORDING_DURATION_MS, durationMs)));
|
|
20402
|
+
|
|
20403
|
+
/** Single-use `MediaRecorder` wrapper that returns an in-memory blob. */
|
|
20404
|
+
class MediaStreamRecorder {
|
|
20405
|
+
recording = promiseWithResolvers();
|
|
20406
|
+
logger = videoLoggerSystem.getLogger('MediaStreamRecorder');
|
|
20407
|
+
chunks = [];
|
|
20408
|
+
timeoutId;
|
|
20409
|
+
recorder;
|
|
20410
|
+
tracks;
|
|
20411
|
+
maxDurationMs;
|
|
20412
|
+
constructor(stream, options = {}) {
|
|
20413
|
+
if (typeof MediaRecorder === 'undefined') {
|
|
20414
|
+
throw new Error('MediaRecorder is not available in this environment');
|
|
20415
|
+
}
|
|
20416
|
+
this.tracks = stream.getTracks();
|
|
20417
|
+
if (this.tracks.length === 0) {
|
|
20418
|
+
throw new Error('cannot record a MediaStream that has no tracks');
|
|
20419
|
+
}
|
|
20420
|
+
this.maxDurationMs = options.maxDurationMs;
|
|
20421
|
+
this.recorder = new MediaRecorder(new MediaStream(this.tracks));
|
|
20422
|
+
this.recorder.ondataavailable = (event) => {
|
|
20423
|
+
if (event.data?.size)
|
|
20424
|
+
this.chunks.push(event.data);
|
|
20425
|
+
};
|
|
20426
|
+
this.recorder.onstop = this.handleStop;
|
|
20427
|
+
this.recorder.onerror = this.handleError;
|
|
20428
|
+
}
|
|
20429
|
+
/** Starts recording and resolves when recording stops. */
|
|
20430
|
+
start = () => {
|
|
20431
|
+
if (this.isSettled()) {
|
|
20432
|
+
throw new Error('MediaStreamRecorder is single-use: this instance has already ' +
|
|
20433
|
+
'recorded. Construct a new one.');
|
|
20434
|
+
}
|
|
20435
|
+
if (this.recorder.state === 'inactive') {
|
|
20436
|
+
this.tracks.forEach((track) => track.addEventListener('ended', this.stop));
|
|
20437
|
+
if (this.maxDurationMs !== undefined) {
|
|
20438
|
+
this.timeoutId = setTimeout(this.stop, this.maxDurationMs);
|
|
20439
|
+
}
|
|
20440
|
+
this.recorder.start();
|
|
20441
|
+
}
|
|
20442
|
+
return this.recording.promise;
|
|
20443
|
+
};
|
|
20444
|
+
/** Stops recording early. */
|
|
20445
|
+
stop = () => {
|
|
20446
|
+
if (this.recorder.state !== 'inactive')
|
|
20447
|
+
this.recorder.stop();
|
|
20448
|
+
};
|
|
20449
|
+
isSettled = () => this.recording.isResolved() || this.recording.isRejected();
|
|
20450
|
+
detach = () => {
|
|
20451
|
+
clearTimeout(this.timeoutId);
|
|
20452
|
+
this.tracks.forEach((track) => track.removeEventListener('ended', this.stop));
|
|
20453
|
+
};
|
|
20454
|
+
handleStop = () => {
|
|
20455
|
+
this.detach();
|
|
20456
|
+
if (this.chunks.length === 0) {
|
|
20457
|
+
this.logger.warn('MediaRecorder recording produced no data');
|
|
20458
|
+
this.recording.resolve(null);
|
|
20459
|
+
return;
|
|
20460
|
+
}
|
|
20461
|
+
const mimeType = this.chunks[0].type || this.recorder.mimeType;
|
|
20462
|
+
this.recording.resolve({
|
|
20463
|
+
blob: new Blob(this.chunks, { type: mimeType }),
|
|
20464
|
+
});
|
|
20465
|
+
};
|
|
20466
|
+
handleError = (event) => {
|
|
20467
|
+
if (this.isSettled())
|
|
20468
|
+
return;
|
|
20469
|
+
this.detach();
|
|
20470
|
+
const error = event.error;
|
|
20471
|
+
this.logger.error('MediaRecorder error', error);
|
|
20472
|
+
this.recording.reject(error ?? new Error('MediaRecorder failed'));
|
|
20473
|
+
};
|
|
20474
|
+
}
|
|
20475
|
+
|
|
20476
|
+
/**
|
|
20477
|
+
* Tiny, type-safe event emitter.
|
|
20478
|
+
*
|
|
20479
|
+
* Usage styles:
|
|
20480
|
+
* - compose: `private readonly events = new TypedEventEmitter<MyMap>()`
|
|
20481
|
+
* - extend: `class Foo extends TypedEventEmitter<MyMap> {}`
|
|
20482
|
+
*
|
|
20483
|
+
* Listener exceptions (sync throw or rejected promise) are caught and logged,
|
|
20484
|
+
* so one bad listener cannot break dispatch for the rest.
|
|
20485
|
+
*/
|
|
20486
|
+
class TypedEventEmitter {
|
|
20487
|
+
logger;
|
|
20488
|
+
byEvent = new Map();
|
|
20489
|
+
constructor(logger = 'TypedEventEmitter') {
|
|
20490
|
+
this.logger =
|
|
20491
|
+
typeof logger === 'string' ? videoLoggerSystem.getLogger(logger) : logger;
|
|
20492
|
+
}
|
|
20493
|
+
on = (event, fn) => {
|
|
20494
|
+
let listeners = this.byEvent.get(event);
|
|
20495
|
+
if (!listeners) {
|
|
20496
|
+
listeners = new Set();
|
|
20497
|
+
this.byEvent.set(event, listeners);
|
|
20498
|
+
}
|
|
20499
|
+
listeners.add(fn);
|
|
20500
|
+
return () => this.off(event, fn);
|
|
20501
|
+
};
|
|
20502
|
+
off = (event, fn) => {
|
|
20503
|
+
const listeners = this.byEvent.get(event);
|
|
20504
|
+
if (!listeners)
|
|
20505
|
+
return;
|
|
20506
|
+
listeners.delete(fn);
|
|
20507
|
+
if (listeners.size === 0)
|
|
20508
|
+
this.byEvent.delete(event);
|
|
20509
|
+
};
|
|
20510
|
+
emit = (event, payload) => {
|
|
20511
|
+
const listeners = this.byEvent.get(event);
|
|
20512
|
+
if (!listeners || listeners.size === 0)
|
|
20513
|
+
return;
|
|
20514
|
+
for (const listener of [...listeners]) {
|
|
20515
|
+
this.invoke(() => listener(payload), event);
|
|
20516
|
+
}
|
|
20517
|
+
};
|
|
20518
|
+
removeAllListeners = (event) => {
|
|
20519
|
+
if (event === undefined) {
|
|
20520
|
+
this.byEvent.clear();
|
|
20521
|
+
}
|
|
20522
|
+
else {
|
|
20523
|
+
this.byEvent.delete(event);
|
|
20524
|
+
}
|
|
20525
|
+
};
|
|
20526
|
+
invoke = (run, event) => {
|
|
20527
|
+
try {
|
|
20528
|
+
const result = run();
|
|
20529
|
+
if (result && typeof result.then === 'function') {
|
|
20530
|
+
result.catch((err) => {
|
|
20531
|
+
this.logger.warn(`Listener for '${String(event)}' rejected`, err);
|
|
20532
|
+
});
|
|
20533
|
+
}
|
|
20534
|
+
}
|
|
20535
|
+
catch (err) {
|
|
20536
|
+
this.logger.warn(`Listener for '${String(event)}' threw`, err);
|
|
20537
|
+
}
|
|
20538
|
+
};
|
|
20539
|
+
}
|
|
20540
|
+
|
|
20541
|
+
/**
|
|
20542
|
+
* Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
|
|
20543
|
+
* transforms to RTCRtpSenders and RTCRtpReceivers.
|
|
20544
|
+
*/
|
|
20545
|
+
class EncryptionManager extends TypedEventEmitter {
|
|
20546
|
+
algorithm;
|
|
20547
|
+
transform;
|
|
20548
|
+
disposed = false;
|
|
20549
|
+
piped;
|
|
20550
|
+
userId;
|
|
20551
|
+
worker;
|
|
20552
|
+
workerUrl;
|
|
20553
|
+
constructor(userId, worker, workerUrl, algorithm, transform) {
|
|
20554
|
+
super('EncryptionManager');
|
|
20555
|
+
this.userId = userId;
|
|
20556
|
+
this.worker = worker;
|
|
20557
|
+
this.workerUrl = workerUrl;
|
|
20558
|
+
this.algorithm = algorithm;
|
|
20559
|
+
this.transform = transform;
|
|
20560
|
+
this.worker.addEventListener('message', this.handleWorkerMessage);
|
|
20561
|
+
this.worker.addEventListener('error', this.handleWorkerError);
|
|
20562
|
+
}
|
|
20563
|
+
/**
|
|
20564
|
+
* Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
|
|
20565
|
+
* Use it to guard UI, or to avoid calling {@link create} where it would throw.
|
|
20566
|
+
*/
|
|
20567
|
+
static isSupported = () => {
|
|
20568
|
+
return preferredTransform() !== undefined;
|
|
20569
|
+
};
|
|
20570
|
+
/**
|
|
20571
|
+
* Create an EncryptionManager instance and initialize the E2EE Web Worker.
|
|
20572
|
+
*
|
|
20573
|
+
* @param userId - The local user's ID, normally `call.currentUserId`.
|
|
20574
|
+
* @param options - the create options.
|
|
20575
|
+
* @throws {Error} If the browser lacks Encoded Transforms.
|
|
20576
|
+
*
|
|
20577
|
+
* @example
|
|
20578
|
+
* ```ts
|
|
20579
|
+
* if (EncryptionManager.isSupported()) {
|
|
20580
|
+
* const e2ee = await EncryptionManager.create(call.currentUserId);
|
|
20581
|
+
* call.setE2EEManager(e2ee);
|
|
20582
|
+
* e2ee.setSharedKey(0, keyBytes);
|
|
20583
|
+
* }
|
|
20584
|
+
*/
|
|
20585
|
+
static create = async (userId, options) => {
|
|
20586
|
+
const transform = preferredTransform();
|
|
20587
|
+
if (!transform) {
|
|
20588
|
+
throw new Error(`E2EE is not supported in this browser`);
|
|
20589
|
+
}
|
|
20590
|
+
const { e2eeWorker } = await Promise.resolve().then(function () { return e2eeWorker$1; });
|
|
20591
|
+
const blob = new Blob([`(${e2eeWorker.toString()})()`], {
|
|
20592
|
+
type: 'application/javascript',
|
|
20593
|
+
});
|
|
20594
|
+
const url = URL.createObjectURL(blob);
|
|
20595
|
+
let worker;
|
|
20596
|
+
try {
|
|
20597
|
+
worker = new Worker(url, { name: 'stream-video-e2ee' });
|
|
20598
|
+
}
|
|
20599
|
+
catch (err) {
|
|
20600
|
+
// e.g. a CSP `worker-src` without `blob:`. Don't leak the object URL.
|
|
20601
|
+
URL.revokeObjectURL(url);
|
|
20602
|
+
throw err;
|
|
20603
|
+
}
|
|
20604
|
+
const algorithm = options?.algorithm ?? 'AES-128-GCM';
|
|
20605
|
+
return new EncryptionManager(userId, worker, url, algorithm, transform);
|
|
20606
|
+
};
|
|
20607
|
+
/**
|
|
20608
|
+
* {@link dispose} terminates the worker, so `postMessage` becomes a silent
|
|
20609
|
+
* no-op and an attached transform points at a dead worker: frames would stall
|
|
20610
|
+
* forever with no error and no event. Throwing is also fail-closed, since a
|
|
20611
|
+
* caller that swallows it still publishes nothing rather than cleartext.
|
|
20612
|
+
*/
|
|
20613
|
+
assertUsable = () => {
|
|
20614
|
+
if (this.disposed)
|
|
20615
|
+
throw new Error(`EncryptionManager is disposed`);
|
|
20616
|
+
};
|
|
20617
|
+
/**
|
|
20618
|
+
* Terminate the worker and release all resources.
|
|
20619
|
+
*
|
|
20620
|
+
* The manager is unusable afterwards and every other method throws. Call
|
|
20621
|
+
* {@link create} for a new one. Safe to call more than once.
|
|
20622
|
+
*/
|
|
20623
|
+
dispose = () => {
|
|
20624
|
+
if (this.disposed)
|
|
20625
|
+
return;
|
|
20626
|
+
this.disposed = true;
|
|
20627
|
+
this.piped = undefined;
|
|
20628
|
+
this.worker.removeEventListener('message', this.handleWorkerMessage);
|
|
20629
|
+
this.worker.removeEventListener('error', this.handleWorkerError);
|
|
20630
|
+
this.worker.terminate();
|
|
20631
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
20632
|
+
this.removeAllListeners();
|
|
20633
|
+
};
|
|
20634
|
+
/**
|
|
20635
|
+
* Set a per-user AES-GCM encryption key in the worker's key store.
|
|
20636
|
+
*
|
|
20637
|
+
* Use it when each participant has their own key from a central authority.
|
|
20638
|
+
* The receiver picks the right one by the `keyIndex` in the frame trailer.
|
|
20639
|
+
*
|
|
20640
|
+
* @param userId - The key owner.
|
|
20641
|
+
* @param keyIndex - Increases with each rotation.
|
|
20642
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20643
|
+
*/
|
|
20644
|
+
setKey = (userId, keyIndex, rawKey) => {
|
|
20645
|
+
this.assertUsable();
|
|
20646
|
+
this.validateKeyIndex(keyIndex);
|
|
20647
|
+
this.validateKeyLength(rawKey);
|
|
20648
|
+
this.worker.postMessage({ type: 'cmd.set_key', userId, keyIndex, rawKey });
|
|
20649
|
+
};
|
|
20650
|
+
/**
|
|
20651
|
+
* Fallback key for any user without a per-user key. The simplest E2EE mode:
|
|
20652
|
+
* one key for everyone, usually passphrase-derived, no distribution needed.
|
|
20653
|
+
* Setting an epoch makes it active for encryption while older epochs remain
|
|
20654
|
+
* available to decrypt in-flight frames until {@link removeSharedKey}.
|
|
20655
|
+
*
|
|
20656
|
+
* @param keyIndex - An integer 0-255, since one trailer byte carries it.
|
|
20657
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20658
|
+
*/
|
|
20659
|
+
setSharedKey = (keyIndex, rawKey) => {
|
|
20660
|
+
this.assertUsable();
|
|
20661
|
+
this.validateKeyIndex(keyIndex);
|
|
20662
|
+
this.validateKeyLength(rawKey);
|
|
20663
|
+
this.worker.postMessage({ type: 'cmd.set_shared_key', keyIndex, rawKey });
|
|
20664
|
+
};
|
|
20665
|
+
/**
|
|
20666
|
+
* Remove one shared-key epoch from the worker's receive key ring.
|
|
20667
|
+
*
|
|
20668
|
+
* If this is the active epoch, shared-key encryption stops until
|
|
20669
|
+
* {@link setSharedKey} succeeds again. An older epoch is not reactivated.
|
|
20670
|
+
*
|
|
20671
|
+
* @param keyIndex - The exact shared-key epoch to remove.
|
|
20672
|
+
*/
|
|
20673
|
+
removeSharedKey = (keyIndex) => {
|
|
20674
|
+
this.assertUsable();
|
|
20675
|
+
this.validateKeyIndex(keyIndex);
|
|
20676
|
+
this.worker.postMessage({ type: 'cmd.remove_shared_key', keyIndex });
|
|
20677
|
+
};
|
|
20678
|
+
/**
|
|
20679
|
+
* Retire one of a user's key epochs, leaving their other epochs usable.
|
|
20680
|
+
*
|
|
20681
|
+
* @param userId - The key owner.
|
|
20682
|
+
* @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
|
|
20683
|
+
*/
|
|
20684
|
+
removeKey = (userId, keyIndex) => {
|
|
20685
|
+
this.assertUsable();
|
|
20686
|
+
this.validateKeyIndex(keyIndex);
|
|
20687
|
+
this.worker.postMessage({ type: 'cmd.remove_key', userId, keyIndex });
|
|
20688
|
+
};
|
|
20689
|
+
/**
|
|
20690
|
+
* Drop every key a user holds, revoking their ability to decrypt later
|
|
20691
|
+
* frames. Call it when a participant leaves.
|
|
20692
|
+
*
|
|
20693
|
+
* To retire one rotated epoch instead, use {@link removeKey}.
|
|
20694
|
+
*/
|
|
20695
|
+
removeAllKeys = (userId) => {
|
|
20696
|
+
this.assertUsable();
|
|
20697
|
+
this.worker.postMessage({ type: 'cmd.remove_all_keys', userId });
|
|
20698
|
+
};
|
|
20699
|
+
/**
|
|
20700
|
+
* Called by the Publisher when it adds a transceiver.
|
|
20701
|
+
*
|
|
20702
|
+
* @param sender - The sender to encrypt.
|
|
20703
|
+
* @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
|
|
20704
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20705
|
+
* @internal
|
|
20706
|
+
*/
|
|
20707
|
+
encrypt = (sender, codec, trackType) => {
|
|
20708
|
+
this.assertUsable();
|
|
20709
|
+
this.pipe(sender, {
|
|
20710
|
+
operation: 'encode',
|
|
20711
|
+
userId: this.userId,
|
|
20712
|
+
codec,
|
|
20713
|
+
trackType,
|
|
20714
|
+
});
|
|
20715
|
+
};
|
|
20716
|
+
/**
|
|
20717
|
+
* Called by the Subscriber when a remote track arrives.
|
|
20718
|
+
*
|
|
20719
|
+
* @param receiver - The receiver to decrypt.
|
|
20720
|
+
* @param userId - The remote user, for key lookup in the worker.
|
|
20721
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20722
|
+
* @internal
|
|
20723
|
+
*/
|
|
20724
|
+
decrypt = (receiver, userId, trackType) => {
|
|
20725
|
+
this.assertUsable();
|
|
20726
|
+
this.pipe(receiver, { operation: 'decode', userId, trackType });
|
|
20727
|
+
};
|
|
20728
|
+
/** Pipe through the worker's transform, tracking targets to avoid double-piping. */
|
|
20729
|
+
pipe = (target, options) => {
|
|
20730
|
+
if (this.transform === 'script') {
|
|
20731
|
+
target.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
20732
|
+
return;
|
|
20733
|
+
}
|
|
20734
|
+
if ((this.piped ??= new WeakSet()).has(target))
|
|
20735
|
+
return;
|
|
20736
|
+
this.piped.add(target);
|
|
20737
|
+
// @ts-expect-error createEncodedStreams is not in the standard typedefs
|
|
20738
|
+
const { readable, writable } = target.createEncodedStreams();
|
|
20739
|
+
this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
|
|
20740
|
+
};
|
|
20741
|
+
/**
|
|
20742
|
+
* Toggle periodic performance reporting from the E2EE worker.
|
|
20743
|
+
*
|
|
20744
|
+
* While on, the worker emits `e2ee.perf_report` once per second with per-track
|
|
20745
|
+
* FPS and crypto timings. Useful for debugging throughput.
|
|
20746
|
+
*/
|
|
20747
|
+
enablePerformanceReporting = (enabled) => {
|
|
20748
|
+
this.assertUsable();
|
|
20749
|
+
this.worker.postMessage({
|
|
20750
|
+
type: 'cmd.enable_performance_reporting',
|
|
20751
|
+
enabled,
|
|
20752
|
+
});
|
|
20753
|
+
};
|
|
20754
|
+
/**
|
|
20755
|
+
* Request a snapshot of the worker's keys. It arrives later as the
|
|
20756
|
+
* `e2ee.key_state` event, listing fingerprints only, never key material.
|
|
20757
|
+
*/
|
|
20758
|
+
requestKeyState = () => {
|
|
20759
|
+
this.assertUsable();
|
|
20760
|
+
this.worker.postMessage({ type: 'cmd.request_key_state' });
|
|
20761
|
+
};
|
|
20762
|
+
handleWorkerMessage = (e) => {
|
|
20763
|
+
const { type, ...payload } = e.data ?? {};
|
|
20764
|
+
if (type === 'e2ee.error') {
|
|
20765
|
+
this.logger.error(e.data.message);
|
|
20766
|
+
return;
|
|
20767
|
+
}
|
|
20768
|
+
const event = type;
|
|
20769
|
+
this.logger.debug('Dispatching', event, payload);
|
|
20770
|
+
this.emit(event, payload);
|
|
20771
|
+
};
|
|
20772
|
+
handleWorkerError = (e) => {
|
|
20773
|
+
this.logger.error('Unhandled worker error:', e.message);
|
|
20774
|
+
};
|
|
20775
|
+
validateKeyLength = (rawKey) => {
|
|
20776
|
+
const is256 = this.algorithm === 'AES-256-GCM';
|
|
20777
|
+
const expected = is256 ? 32 : 16;
|
|
20778
|
+
if (rawKey.byteLength !== expected) {
|
|
20779
|
+
throw new Error(`Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`);
|
|
20780
|
+
}
|
|
20781
|
+
};
|
|
20782
|
+
/**
|
|
20783
|
+
* One trailer byte carries the keyIndex. A larger value would truncate to
|
|
20784
|
+
* `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
|
|
20785
|
+
* every decrypt. Reject it rather than ship a silently broken key epoch.
|
|
20786
|
+
*/
|
|
20787
|
+
validateKeyIndex = (keyIndex) => {
|
|
20788
|
+
if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
|
|
20789
|
+
throw new Error(`keyIndex must be an integer between 0 and 255, got ${keyIndex}`);
|
|
20790
|
+
}
|
|
20791
|
+
};
|
|
20792
|
+
}
|
|
20793
|
+
|
|
20794
|
+
/**
|
|
20795
|
+
* E2EE worker. The build populates it.
|
|
20796
|
+
*
|
|
20797
|
+
* The actual implementation lives in `e2ee-worker/e2ee-worker-impl.ts` and its
|
|
20798
|
+
* sibling modules (`constants.ts`, `codec.ts`, `crypto.ts`, `utils.ts`,
|
|
20799
|
+
* `types.ts`), all written as real TypeScript with full type checking
|
|
20800
|
+
* and import support.
|
|
20801
|
+
*
|
|
20802
|
+
* ## How it works
|
|
20803
|
+
*
|
|
20804
|
+
* 1. You edit files in `e2ee-worker/` as normal TypeScript
|
|
20805
|
+
* 2. `yarn build` runs Rollup, which invokes `rollup-plugin-inline-worker`
|
|
20806
|
+
* 3. The plugin bundles `e2ee-worker/e2ee-worker-impl.ts` (and its imports)
|
|
20807
|
+
* into a single function via a nested Rollup + TypeScript build
|
|
20808
|
+
* 4. The function replaces this file's export at build time
|
|
20809
|
+
* 5. `EncryptionManager.create()` creates a Worker from `e2eeWorker.toString()`
|
|
20810
|
+
*
|
|
20811
|
+
* In tests, this module is mocked (see `__tests__/EncryptionManager.test.ts`),
|
|
20812
|
+
* so the placeholder is never evaluated.
|
|
20813
|
+
*
|
|
20814
|
+
* @see e2ee-worker/e2ee-worker-impl.ts - the entry point
|
|
20815
|
+
* @see ../../plugins/rollup-plugin-inline-worker.ts - the Rollup plugin
|
|
20816
|
+
* @see EncryptionManager.ts - consumes e2eeWorker
|
|
20817
|
+
*/
|
|
20818
|
+
function e2eeWorker() {
|
|
20819
|
+
// will be populated at build time
|
|
20820
|
+
}
|
|
20821
|
+
|
|
20822
|
+
var e2eeWorker$1 = /*#__PURE__*/Object.freeze({
|
|
20823
|
+
__proto__: null,
|
|
20824
|
+
e2eeWorker: e2eeWorker
|
|
20825
|
+
});
|
|
20826
|
+
|
|
19770
20827
|
Object.defineProperty(exports, "AxiosError", {
|
|
19771
20828
|
enumerable: true,
|
|
19772
20829
|
get: function () { return axios.AxiosError; }
|
|
@@ -19789,19 +20846,30 @@ exports.CallTypes = CallTypes;
|
|
|
19789
20846
|
exports.CameraManager = CameraManager;
|
|
19790
20847
|
exports.CameraManagerState = CameraManagerState;
|
|
19791
20848
|
exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnum;
|
|
20849
|
+
exports.DEFAULT_LOOPBACK_RECORDING_DURATION_MS = DEFAULT_LOOPBACK_RECORDING_DURATION_MS;
|
|
19792
20850
|
exports.DeviceManager = DeviceManager;
|
|
19793
20851
|
exports.DeviceManagerState = DeviceManagerState;
|
|
19794
20852
|
exports.DynascaleManager = DynascaleManager;
|
|
20853
|
+
exports.EncryptionManager = EncryptionManager;
|
|
20854
|
+
exports.EncryptionSettingsRequestModeEnum = EncryptionSettingsRequestModeEnum;
|
|
20855
|
+
exports.EncryptionSettingsResponseModeEnum = EncryptionSettingsResponseModeEnum;
|
|
19795
20856
|
exports.ErrorFromResponse = ErrorFromResponse;
|
|
19796
20857
|
exports.FrameRecordingSettingsRequestModeEnum = FrameRecordingSettingsRequestModeEnum;
|
|
19797
20858
|
exports.FrameRecordingSettingsRequestQualityEnum = FrameRecordingSettingsRequestQualityEnum;
|
|
19798
20859
|
exports.FrameRecordingSettingsResponseModeEnum = FrameRecordingSettingsResponseModeEnum;
|
|
20860
|
+
exports.HLSSettingsRequestQualityTracksEnum = HLSSettingsRequestQualityTracksEnum;
|
|
19799
20861
|
exports.IndividualRecordingSettingsRequestModeEnum = IndividualRecordingSettingsRequestModeEnum;
|
|
20862
|
+
exports.IndividualRecordingSettingsRequestOutputTypesEnum = IndividualRecordingSettingsRequestOutputTypesEnum;
|
|
19800
20863
|
exports.IndividualRecordingSettingsResponseModeEnum = IndividualRecordingSettingsResponseModeEnum;
|
|
19801
20864
|
exports.IngressAudioEncodingOptionsRequestChannelsEnum = IngressAudioEncodingOptionsRequestChannelsEnum;
|
|
19802
20865
|
exports.IngressSourceRequestFpsEnum = IngressSourceRequestFpsEnum;
|
|
19803
20866
|
exports.IngressVideoLayerRequestCodecEnum = IngressVideoLayerRequestCodecEnum;
|
|
20867
|
+
exports.LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS;
|
|
19804
20868
|
exports.LayoutSettingsRequestNameEnum = LayoutSettingsRequestNameEnum;
|
|
20869
|
+
exports.LoopbackStreamsTimeoutError = LoopbackStreamsTimeoutError;
|
|
20870
|
+
exports.MAX_LOOPBACK_RECORDING_DURATION_MS = MAX_LOOPBACK_RECORDING_DURATION_MS;
|
|
20871
|
+
exports.MIN_LOOPBACK_RECORDING_DURATION_MS = MIN_LOOPBACK_RECORDING_DURATION_MS;
|
|
20872
|
+
exports.MediaStreamRecorder = MediaStreamRecorder;
|
|
19805
20873
|
exports.MicrophoneManager = MicrophoneManager;
|
|
19806
20874
|
exports.MicrophoneManagerState = MicrophoneManagerState;
|
|
19807
20875
|
exports.NoiseCancellationSettingsModeEnum = NoiseCancellationSettingsModeEnum;
|
|
@@ -19812,6 +20880,7 @@ exports.RawRecordingSettingsRequestModeEnum = RawRecordingSettingsRequestModeEnu
|
|
|
19812
20880
|
exports.RawRecordingSettingsResponseModeEnum = RawRecordingSettingsResponseModeEnum;
|
|
19813
20881
|
exports.RecordSettingsRequestModeEnum = RecordSettingsRequestModeEnum;
|
|
19814
20882
|
exports.RecordSettingsRequestQualityEnum = RecordSettingsRequestQualityEnum;
|
|
20883
|
+
exports.RequestPermissionRequestPermissionsEnum = RequestPermissionRequestPermissionsEnum;
|
|
19815
20884
|
exports.RxUtils = rxUtils;
|
|
19816
20885
|
exports.ScreenShareManager = ScreenShareManager;
|
|
19817
20886
|
exports.ScreenShareState = ScreenShareState;
|
|
@@ -19833,10 +20902,15 @@ exports.TranscriptionSettingsRequestModeEnum = TranscriptionSettingsRequestModeE
|
|
|
19833
20902
|
exports.TranscriptionSettingsResponseClosedCaptionModeEnum = TranscriptionSettingsResponseClosedCaptionModeEnum;
|
|
19834
20903
|
exports.TranscriptionSettingsResponseLanguageEnum = TranscriptionSettingsResponseLanguageEnum;
|
|
19835
20904
|
exports.TranscriptionSettingsResponseModeEnum = TranscriptionSettingsResponseModeEnum;
|
|
20905
|
+
exports.TranslationSettingsLanguagesEnum = TranslationSettingsLanguagesEnum;
|
|
20906
|
+
exports.UpdateUserPermissionsRequestGrantPermissionsEnum = UpdateUserPermissionsRequestGrantPermissionsEnum;
|
|
20907
|
+
exports.UpdateUserPermissionsRequestRevokePermissionsEnum = UpdateUserPermissionsRequestRevokePermissionsEnum;
|
|
19836
20908
|
exports.VideoSettingsRequestCameraFacingEnum = VideoSettingsRequestCameraFacingEnum;
|
|
19837
20909
|
exports.VideoSettingsResponseCameraFacingEnum = VideoSettingsResponseCameraFacingEnum;
|
|
19838
20910
|
exports.ViewportTracker = ViewportTracker;
|
|
20911
|
+
exports.WSAuthMessageProductsEnum = WSAuthMessageProductsEnum;
|
|
19839
20912
|
exports.checkIfAudioOutputChangeSupported = checkIfAudioOutputChangeSupported;
|
|
20913
|
+
exports.clampLoopbackRecordingDuration = clampLoopbackRecordingDuration;
|
|
19840
20914
|
exports.combineComparators = combineComparators;
|
|
19841
20915
|
exports.conditional = conditional;
|
|
19842
20916
|
exports.createSoundDetector = createSoundDetector;
|
|
@@ -19845,12 +20919,16 @@ exports.descending = descending;
|
|
|
19845
20919
|
exports.deviceIds$ = deviceIds$;
|
|
19846
20920
|
exports.disposeOfMediaStream = disposeOfMediaStream;
|
|
19847
20921
|
exports.dominantSpeaker = dominantSpeaker;
|
|
20922
|
+
exports.enableLoopbackAudio = enableLoopbackAudio;
|
|
19848
20923
|
exports.getAudioBrowserPermission = getAudioBrowserPermission;
|
|
19849
20924
|
exports.getAudioDevices = getAudioDevices;
|
|
19850
20925
|
exports.getAudioOutputDevices = getAudioOutputDevices;
|
|
19851
20926
|
exports.getAudioStream = getAudioStream;
|
|
20927
|
+
exports.getCallMediaEngineProvider = getCallMediaEngineProvider;
|
|
19852
20928
|
exports.getClientDetails = getClientDetails;
|
|
19853
20929
|
exports.getDeviceState = getDeviceState;
|
|
20930
|
+
exports.getLoopbackStreams = getLoopbackStreams;
|
|
20931
|
+
exports.getLoopbackTracks = getLoopbackTracks;
|
|
19854
20932
|
exports.getScreenShareStream = getScreenShareStream;
|
|
19855
20933
|
exports.getSdkInfo = getSdkInfo;
|
|
19856
20934
|
exports.getVideoBrowserPermission = getVideoBrowserPermission;
|
|
@@ -19877,6 +20955,7 @@ exports.reactionType = reactionType;
|
|
|
19877
20955
|
exports.resolveDeviceId = resolveDeviceId;
|
|
19878
20956
|
exports.role = role;
|
|
19879
20957
|
exports.screenSharing = screenSharing;
|
|
20958
|
+
exports.setCallMediaEngineProvider = setCallMediaEngineProvider;
|
|
19880
20959
|
exports.setDeviceInfo = setDeviceInfo;
|
|
19881
20960
|
exports.setOSInfo = setOSInfo;
|
|
19882
20961
|
exports.setPowerState = setPowerState;
|
|
@@ -19886,5 +20965,7 @@ exports.setWebRTCInfo = setWebRTCInfo;
|
|
|
19886
20965
|
exports.speakerLayoutSortPreset = speakerLayoutSortPreset;
|
|
19887
20966
|
exports.speaking = speaking;
|
|
19888
20967
|
exports.videoLoggerSystem = videoLoggerSystem;
|
|
20968
|
+
exports.waitForLoopbackStreams = waitForLoopbackStreams;
|
|
20969
|
+
exports.withLoopbackAudioEnabled = withLoopbackAudioEnabled;
|
|
19889
20970
|
exports.withParticipantSource = withParticipantSource;
|
|
19890
20971
|
//# sourceMappingURL=index.cjs.js.map
|