@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.browser.es.js
CHANGED
|
@@ -7,7 +7,7 @@ import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transpor
|
|
|
7
7
|
import * as scopedLogger from '@stream-io/logger';
|
|
8
8
|
export { LogLevelEnum } from '@stream-io/logger';
|
|
9
9
|
import { parse, write } from 'sdp-transform';
|
|
10
|
-
import { Subject, Observable, combineLatest, BehaviorSubject, ReplaySubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of } from 'rxjs';
|
|
10
|
+
import { Subject, Observable, combineLatest, BehaviorSubject, ReplaySubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of, Subscription } from 'rxjs';
|
|
11
11
|
import { UAParser } from 'ua-parser-js';
|
|
12
12
|
import { WorkerTimer } from '@stream-io/worker-timer';
|
|
13
13
|
|
|
@@ -67,6 +67,22 @@ const CreateDeviceRequestPushProviderEnum = {
|
|
|
67
67
|
HUAWEI: 'huawei',
|
|
68
68
|
XIAOMI: 'xiaomi',
|
|
69
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* @export
|
|
72
|
+
*/
|
|
73
|
+
const EncryptionSettingsRequestModeEnum = {
|
|
74
|
+
AVAILABLE: 'available',
|
|
75
|
+
DISABLED: 'disabled',
|
|
76
|
+
AUTO_ON: 'auto-on',
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* @export
|
|
80
|
+
*/
|
|
81
|
+
const EncryptionSettingsResponseModeEnum = {
|
|
82
|
+
AVAILABLE: 'available',
|
|
83
|
+
DISABLED: 'disabled',
|
|
84
|
+
AUTO_ON: 'auto-on',
|
|
85
|
+
};
|
|
70
86
|
/**
|
|
71
87
|
* @export
|
|
72
88
|
*/
|
|
@@ -93,6 +109,21 @@ const FrameRecordingSettingsResponseModeEnum = {
|
|
|
93
109
|
DISABLED: 'disabled',
|
|
94
110
|
AUTO_ON: 'auto-on',
|
|
95
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* @export
|
|
114
|
+
*/
|
|
115
|
+
const HLSSettingsRequestQualityTracksEnum = {
|
|
116
|
+
_360P: '360p',
|
|
117
|
+
_480P: '480p',
|
|
118
|
+
_720P: '720p',
|
|
119
|
+
_1080P: '1080p',
|
|
120
|
+
_1440P: '1440p',
|
|
121
|
+
PORTRAIT_360X640: 'portrait-360x640',
|
|
122
|
+
PORTRAIT_480X854: 'portrait-480x854',
|
|
123
|
+
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
124
|
+
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
125
|
+
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
126
|
+
};
|
|
96
127
|
/**
|
|
97
128
|
* @export
|
|
98
129
|
*/
|
|
@@ -101,6 +132,17 @@ const IndividualRecordingSettingsRequestModeEnum = {
|
|
|
101
132
|
DISABLED: 'disabled',
|
|
102
133
|
AUTO_ON: 'auto-on',
|
|
103
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* @export
|
|
137
|
+
*/
|
|
138
|
+
const IndividualRecordingSettingsRequestOutputTypesEnum = {
|
|
139
|
+
AUDIO_ONLY: 'audio_only',
|
|
140
|
+
VIDEO_ONLY: 'video_only',
|
|
141
|
+
AUDIO_VIDEO: 'audio_video',
|
|
142
|
+
SCREENSHARE_AUDIO_ONLY: 'screenshare_audio_only',
|
|
143
|
+
SCREENSHARE_VIDEO_ONLY: 'screenshare_video_only',
|
|
144
|
+
SCREENSHARE_AUDIO_VIDEO: 'screenshare_audio_video',
|
|
145
|
+
};
|
|
104
146
|
/**
|
|
105
147
|
* @export
|
|
106
148
|
*/
|
|
@@ -259,6 +301,14 @@ const RecordSettingsRequestQualityEnum = {
|
|
|
259
301
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
260
302
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
261
303
|
};
|
|
304
|
+
/**
|
|
305
|
+
* @export
|
|
306
|
+
*/
|
|
307
|
+
const RequestPermissionRequestPermissionsEnum = {
|
|
308
|
+
SCREENSHARE: 'screenshare',
|
|
309
|
+
SEND_AUDIO: 'send-audio',
|
|
310
|
+
SEND_VIDEO: 'send-video',
|
|
311
|
+
};
|
|
262
312
|
/**
|
|
263
313
|
* @export
|
|
264
314
|
*/
|
|
@@ -459,6 +509,59 @@ const TranscriptionSettingsResponseModeEnum = {
|
|
|
459
509
|
DISABLED: 'disabled',
|
|
460
510
|
AUTO_ON: 'auto-on',
|
|
461
511
|
};
|
|
512
|
+
/**
|
|
513
|
+
* @export
|
|
514
|
+
*/
|
|
515
|
+
const TranslationSettingsLanguagesEnum = {
|
|
516
|
+
EN: 'en',
|
|
517
|
+
FR: 'fr',
|
|
518
|
+
ES: 'es',
|
|
519
|
+
DE: 'de',
|
|
520
|
+
IT: 'it',
|
|
521
|
+
NL: 'nl',
|
|
522
|
+
PT: 'pt',
|
|
523
|
+
PL: 'pl',
|
|
524
|
+
CA: 'ca',
|
|
525
|
+
CS: 'cs',
|
|
526
|
+
DA: 'da',
|
|
527
|
+
EL: 'el',
|
|
528
|
+
FI: 'fi',
|
|
529
|
+
ID: 'id',
|
|
530
|
+
JA: 'ja',
|
|
531
|
+
RU: 'ru',
|
|
532
|
+
SV: 'sv',
|
|
533
|
+
TA: 'ta',
|
|
534
|
+
TH: 'th',
|
|
535
|
+
TR: 'tr',
|
|
536
|
+
HU: 'hu',
|
|
537
|
+
RO: 'ro',
|
|
538
|
+
ZH: 'zh',
|
|
539
|
+
AR: 'ar',
|
|
540
|
+
TL: 'tl',
|
|
541
|
+
HE: 'he',
|
|
542
|
+
HI: 'hi',
|
|
543
|
+
HR: 'hr',
|
|
544
|
+
KO: 'ko',
|
|
545
|
+
MS: 'ms',
|
|
546
|
+
NO: 'no',
|
|
547
|
+
UK: 'uk',
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* @export
|
|
551
|
+
*/
|
|
552
|
+
const UpdateUserPermissionsRequestGrantPermissionsEnum = {
|
|
553
|
+
SCREENSHARE: 'screenshare',
|
|
554
|
+
SEND_AUDIO: 'send-audio',
|
|
555
|
+
SEND_VIDEO: 'send-video',
|
|
556
|
+
};
|
|
557
|
+
/**
|
|
558
|
+
* @export
|
|
559
|
+
*/
|
|
560
|
+
const UpdateUserPermissionsRequestRevokePermissionsEnum = {
|
|
561
|
+
SCREENSHARE: 'screenshare',
|
|
562
|
+
SEND_AUDIO: 'send-audio',
|
|
563
|
+
SEND_VIDEO: 'send-video',
|
|
564
|
+
};
|
|
462
565
|
/**
|
|
463
566
|
* @export
|
|
464
567
|
*/
|
|
@@ -475,6 +578,14 @@ const VideoSettingsResponseCameraFacingEnum = {
|
|
|
475
578
|
BACK: 'back',
|
|
476
579
|
EXTERNAL: 'external',
|
|
477
580
|
};
|
|
581
|
+
/**
|
|
582
|
+
* @export
|
|
583
|
+
*/
|
|
584
|
+
const WSAuthMessageProductsEnum = {
|
|
585
|
+
CHAT: 'chat',
|
|
586
|
+
VIDEO: 'video',
|
|
587
|
+
FEEDS: 'feeds',
|
|
588
|
+
};
|
|
478
589
|
|
|
479
590
|
class ErrorFromResponse extends Error {
|
|
480
591
|
code;
|
|
@@ -1472,6 +1583,13 @@ class CallState$Type extends MessageType {
|
|
|
1472
1583
|
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1473
1584
|
T: () => Pin,
|
|
1474
1585
|
},
|
|
1586
|
+
{
|
|
1587
|
+
no: 5,
|
|
1588
|
+
name: 'e2ee_enabled',
|
|
1589
|
+
kind: 'scalar',
|
|
1590
|
+
jsonName: 'e2eeEnabled',
|
|
1591
|
+
T: 8 /*ScalarType.BOOL*/,
|
|
1592
|
+
},
|
|
1475
1593
|
]);
|
|
1476
1594
|
}
|
|
1477
1595
|
}
|
|
@@ -4594,6 +4712,19 @@ const isSvcCodec = (codecOrMimeType) => {
|
|
|
4594
4712
|
codecOrMimeType === 'video/av1');
|
|
4595
4713
|
};
|
|
4596
4714
|
|
|
4715
|
+
/**
|
|
4716
|
+
* The default engine: a thin, stateless wrapper over the WebRTC globals.
|
|
4717
|
+
*/
|
|
4718
|
+
const defaultGlobalsEngine = {
|
|
4719
|
+
dispose: () => Promise.resolve(true),
|
|
4720
|
+
};
|
|
4721
|
+
const defaultGlobalsEngineProvider = () => defaultGlobalsEngine;
|
|
4722
|
+
let provider = defaultGlobalsEngineProvider;
|
|
4723
|
+
const setCallMediaEngineProvider = (newProvider) => {
|
|
4724
|
+
provider = newProvider;
|
|
4725
|
+
};
|
|
4726
|
+
const getCallMediaEngineProvider = () => provider;
|
|
4727
|
+
|
|
4597
4728
|
const sfuEventKinds = {
|
|
4598
4729
|
subscriberOffer: undefined,
|
|
4599
4730
|
publisherAnswer: undefined,
|
|
@@ -4801,7 +4932,9 @@ const parseUfragFromCandidate = (candidate) => {
|
|
|
4801
4932
|
};
|
|
4802
4933
|
|
|
4803
4934
|
const ensureExhausted = (x, message) => {
|
|
4804
|
-
|
|
4935
|
+
if (message) {
|
|
4936
|
+
videoLoggerSystem.getLogger('helpers').warn(message, x);
|
|
4937
|
+
}
|
|
4805
4938
|
};
|
|
4806
4939
|
|
|
4807
4940
|
/**
|
|
@@ -4926,7 +5059,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
4926
5059
|
}
|
|
4927
5060
|
};
|
|
4928
5061
|
|
|
4929
|
-
const version = "1.
|
|
5062
|
+
const version = "1.59.0";
|
|
4930
5063
|
const [major, minor, patch] = version.split('.');
|
|
4931
5064
|
let sdkInfo = {
|
|
4932
5065
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5183,6 +5316,27 @@ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
|
|
|
5183
5316
|
*/
|
|
5184
5317
|
const withCancellation = createRunner(wrapWithCancellation);
|
|
5185
5318
|
const pendingPromises = new Map();
|
|
5319
|
+
/**
|
|
5320
|
+
* Wraps an async function so concurrent calls share the in-flight promise
|
|
5321
|
+
* instead of queueing another execution.
|
|
5322
|
+
*
|
|
5323
|
+
* @param cb Async function to wrap.
|
|
5324
|
+
* @returns Wrapped async function.
|
|
5325
|
+
*/
|
|
5326
|
+
function singleFlight(cb) {
|
|
5327
|
+
let pending;
|
|
5328
|
+
return (...args) => {
|
|
5329
|
+
if (pending)
|
|
5330
|
+
return pending;
|
|
5331
|
+
const promise = cb(...args).finally(() => {
|
|
5332
|
+
if (pending === promise) {
|
|
5333
|
+
pending = undefined;
|
|
5334
|
+
}
|
|
5335
|
+
});
|
|
5336
|
+
pending = promise;
|
|
5337
|
+
return promise;
|
|
5338
|
+
};
|
|
5339
|
+
}
|
|
5186
5340
|
function hasPending(tag) {
|
|
5187
5341
|
return pendingPromises.has(tag);
|
|
5188
5342
|
}
|
|
@@ -5909,6 +6063,7 @@ class CallState {
|
|
|
5909
6063
|
settingsSubject = new BehaviorSubject(undefined);
|
|
5910
6064
|
transcribingSubject = new BehaviorSubject(false);
|
|
5911
6065
|
captioningSubject = new BehaviorSubject(false);
|
|
6066
|
+
e2eeEnabledSubject = new BehaviorSubject(false);
|
|
5912
6067
|
endedBySubject = new BehaviorSubject(undefined);
|
|
5913
6068
|
thumbnailsSubject = new BehaviorSubject(undefined);
|
|
5914
6069
|
membersSubject = new BehaviorSubject([]);
|
|
@@ -6067,6 +6222,11 @@ class CallState {
|
|
|
6067
6222
|
* Will provide the closed captioning state of this call.
|
|
6068
6223
|
*/
|
|
6069
6224
|
captioning$;
|
|
6225
|
+
/**
|
|
6226
|
+
* Whether end-to-end encryption is active for this call, as reported by the
|
|
6227
|
+
* SFU in the join response.
|
|
6228
|
+
*/
|
|
6229
|
+
e2eeEnabled$;
|
|
6070
6230
|
/**
|
|
6071
6231
|
* Will provide the user who ended this call.
|
|
6072
6232
|
*/
|
|
@@ -6168,6 +6328,7 @@ class CallState {
|
|
|
6168
6328
|
this.rawRecording$ = duc(this.rawRecordingSubject);
|
|
6169
6329
|
this.transcribing$ = duc(this.transcribingSubject);
|
|
6170
6330
|
this.captioning$ = duc(this.captioningSubject);
|
|
6331
|
+
this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
|
|
6171
6332
|
this.eventHandlers = {
|
|
6172
6333
|
// these events are not updating the call state:
|
|
6173
6334
|
'call.frame_recording_ready': undefined,
|
|
@@ -6270,6 +6431,7 @@ class CallState {
|
|
|
6270
6431
|
clearTimeout(taskId);
|
|
6271
6432
|
this.closedCaptionsTasks.delete(ccKey);
|
|
6272
6433
|
}
|
|
6434
|
+
this.removeAllOrphanedTracks();
|
|
6273
6435
|
};
|
|
6274
6436
|
/**
|
|
6275
6437
|
* Sets the list of criteria that are used to sort the participants.
|
|
@@ -6619,6 +6781,12 @@ class CallState {
|
|
|
6619
6781
|
get transcribing() {
|
|
6620
6782
|
return this.getCurrentValue(this.transcribing$);
|
|
6621
6783
|
}
|
|
6784
|
+
/**
|
|
6785
|
+
* Whether end-to-end encryption is active for this call.
|
|
6786
|
+
*/
|
|
6787
|
+
get e2eeEnabled() {
|
|
6788
|
+
return this.getCurrentValue(this.e2eeEnabled$);
|
|
6789
|
+
}
|
|
6622
6790
|
/**
|
|
6623
6791
|
* Will provide the user who ended this call.
|
|
6624
6792
|
*/
|
|
@@ -6839,6 +7007,18 @@ class CallState {
|
|
|
6839
7007
|
removeOrphanedTrack = (id) => {
|
|
6840
7008
|
this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
|
|
6841
7009
|
};
|
|
7010
|
+
/**
|
|
7011
|
+
* Drops every orphaned track. Call this when the peer connections that own
|
|
7012
|
+
* the stored receivers go away (full leave, reconnect, or migration):
|
|
7013
|
+
* `pc.close()` does not raise the track `ended` event, so the per-track
|
|
7014
|
+
* cleanup never fires and the receivers + their closed PCs would otherwise
|
|
7015
|
+
* leak for the call's lifetime.
|
|
7016
|
+
*
|
|
7017
|
+
* @internal
|
|
7018
|
+
*/
|
|
7019
|
+
removeAllOrphanedTracks = () => {
|
|
7020
|
+
this.orphanedTracks = [];
|
|
7021
|
+
};
|
|
6842
7022
|
/**
|
|
6843
7023
|
* Takes all orphaned tracks with the given track lookup prefix.
|
|
6844
7024
|
* All orphaned tracks with the given track lookup prefix are removed from the call state.
|
|
@@ -6901,7 +7081,7 @@ class CallState {
|
|
|
6901
7081
|
* @param reconnectDetails optional reconnect details.
|
|
6902
7082
|
*/
|
|
6903
7083
|
updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
|
|
6904
|
-
const { participants, participantCount, startedAt, pins } = callState;
|
|
7084
|
+
const { participants, participantCount, startedAt, pins, e2EeEnabled } = callState;
|
|
6905
7085
|
const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
|
|
6906
7086
|
this.setParticipants(() => {
|
|
6907
7087
|
const participantLookup = this.getParticipantLookupBySessionId();
|
|
@@ -6927,6 +7107,7 @@ class CallState {
|
|
|
6927
7107
|
this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
6928
7108
|
this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
|
|
6929
7109
|
this.setServerSidePins(pins);
|
|
7110
|
+
this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
|
|
6930
7111
|
};
|
|
6931
7112
|
updateFromMemberRemoved = (event) => {
|
|
6932
7113
|
this.updateFromCallResponse(event.call);
|
|
@@ -7124,6 +7305,37 @@ class CallState {
|
|
|
7124
7305
|
};
|
|
7125
7306
|
}
|
|
7126
7307
|
|
|
7308
|
+
/**
|
|
7309
|
+
* Detection and selection policy for the two WebRTC Encoded Transform APIs.
|
|
7310
|
+
* Internal: which one the SDK attaches is an RTC-layer detail. Consumers want
|
|
7311
|
+
* `EncryptionManager.isSupported` instead.
|
|
7312
|
+
*/
|
|
7313
|
+
/**
|
|
7314
|
+
* Chrome only, and the reason an RTCPeerConnection carrying E2EE needs the
|
|
7315
|
+
* non-standard `encodedInsertableStreams` flag.
|
|
7316
|
+
*/
|
|
7317
|
+
const hasInsertableStreams = () => typeof RTCRtpSender !== 'undefined' &&
|
|
7318
|
+
'createEncodedStreams' in RTCRtpSender.prototype;
|
|
7319
|
+
/** Whether the standard `RTCRtpScriptTransform` API exists. */
|
|
7320
|
+
const hasScriptTransform = () => typeof RTCRtpScriptTransform !== 'undefined';
|
|
7321
|
+
/**
|
|
7322
|
+
* Which Encoded Transform API to attach E2EE with here.
|
|
7323
|
+
*
|
|
7324
|
+
* - `'insertable'`: legacy Insertable Streams. Used on Chrome, where
|
|
7325
|
+
* `RTCRtpScriptTransform` is still unreliable for E2EE.
|
|
7326
|
+
* - `'script'`: the standard API. Used everywhere else.
|
|
7327
|
+
* - `undefined`: neither exists, so E2EE cannot run.
|
|
7328
|
+
*/
|
|
7329
|
+
const preferredTransform = () => {
|
|
7330
|
+
const insertable = hasInsertableStreams();
|
|
7331
|
+
// Chrome's RTCRtpScriptTransform is still unreliable for E2EE.
|
|
7332
|
+
if (isChrome() && insertable)
|
|
7333
|
+
return 'insertable';
|
|
7334
|
+
if (hasScriptTransform())
|
|
7335
|
+
return 'script';
|
|
7336
|
+
return insertable ? 'insertable' : undefined;
|
|
7337
|
+
};
|
|
7338
|
+
|
|
7127
7339
|
/**
|
|
7128
7340
|
* Flatten the stats report into an array of stats objects.
|
|
7129
7341
|
*
|
|
@@ -8232,6 +8444,7 @@ class BasePeerConnection {
|
|
|
8232
8444
|
state;
|
|
8233
8445
|
dispatcher;
|
|
8234
8446
|
clientPublishOptions;
|
|
8447
|
+
e2ee;
|
|
8235
8448
|
tag;
|
|
8236
8449
|
sfuClient;
|
|
8237
8450
|
onReconnectionNeeded;
|
|
@@ -8253,13 +8466,14 @@ class BasePeerConnection {
|
|
|
8253
8466
|
/**
|
|
8254
8467
|
* Constructs a new `BasePeerConnection` instance.
|
|
8255
8468
|
*/
|
|
8256
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8469
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, e2ee, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8257
8470
|
this.peerType = peerType;
|
|
8258
8471
|
this.sfuClient = sfuClient;
|
|
8259
8472
|
this.state = state;
|
|
8260
8473
|
this.dispatcher = dispatcher;
|
|
8261
8474
|
this.iceRestartDelay = iceRestartDelay;
|
|
8262
8475
|
this.clientPublishOptions = clientPublishOptions;
|
|
8476
|
+
this.e2ee = e2ee;
|
|
8263
8477
|
this.tag = tag;
|
|
8264
8478
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8265
8479
|
this.onIceConnected = onIceConnected;
|
|
@@ -8278,7 +8492,13 @@ class BasePeerConnection {
|
|
|
8278
8492
|
}
|
|
8279
8493
|
}
|
|
8280
8494
|
createPeerConnection = (connectionConfig) => {
|
|
8281
|
-
const
|
|
8495
|
+
const config = { ...connectionConfig };
|
|
8496
|
+
// The legacy Insertable Streams path requires this non-standard flag.
|
|
8497
|
+
if (this.e2ee && hasInsertableStreams()) {
|
|
8498
|
+
// @ts-expect-error not part of the standard lib yet
|
|
8499
|
+
config.encodedInsertableStreams = true;
|
|
8500
|
+
}
|
|
8501
|
+
const pc = new RTCPeerConnection(config);
|
|
8282
8502
|
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
8283
8503
|
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
8284
8504
|
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
@@ -9032,6 +9252,10 @@ class Publisher extends BasePeerConnection {
|
|
|
9032
9252
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9033
9253
|
'maintain-framerate';
|
|
9034
9254
|
await transceiver.sender.setParameters(params);
|
|
9255
|
+
if (this.e2ee) {
|
|
9256
|
+
this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
|
|
9257
|
+
this.logger.debug('E2EE encryptor attached to sender');
|
|
9258
|
+
}
|
|
9035
9259
|
await this.negotiate();
|
|
9036
9260
|
};
|
|
9037
9261
|
/**
|
|
@@ -9601,6 +9825,11 @@ class Subscriber extends BasePeerConnection {
|
|
|
9601
9825
|
throw e;
|
|
9602
9826
|
}
|
|
9603
9827
|
};
|
|
9828
|
+
/**
|
|
9829
|
+
* Whether the given stream is one the SFU echoed back to us
|
|
9830
|
+
* (`allowOwnTracksLoopback`) rather than a local capture stream.
|
|
9831
|
+
*/
|
|
9832
|
+
isSelfSubscribedStream = (stream) => !!stream && !!this.trackedStreams?.has(stream);
|
|
9604
9833
|
handleOnTrack = (e) => {
|
|
9605
9834
|
const { streams, track } = e;
|
|
9606
9835
|
const [primaryStream] = streams;
|
|
@@ -9643,9 +9872,15 @@ class Subscriber extends BasePeerConnection {
|
|
|
9643
9872
|
trackLookupPrefix: trackId,
|
|
9644
9873
|
track: primaryStream,
|
|
9645
9874
|
trackType,
|
|
9875
|
+
receiver: this.e2ee ? e.receiver : undefined,
|
|
9646
9876
|
});
|
|
9647
9877
|
return;
|
|
9648
9878
|
}
|
|
9879
|
+
else if (this.e2ee) {
|
|
9880
|
+
const { userId } = participantToUpdate;
|
|
9881
|
+
this.e2ee.decrypt(e.receiver, userId, TrackType[trackType]);
|
|
9882
|
+
this.logger.debug('E2EE decryptor attached to receiver', userId);
|
|
9883
|
+
}
|
|
9649
9884
|
const streamKindProp = trackTypeToParticipantStreamKey(trackType);
|
|
9650
9885
|
if (!streamKindProp) {
|
|
9651
9886
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
@@ -9684,6 +9919,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9684
9919
|
const target = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9685
9920
|
if (!target)
|
|
9686
9921
|
return;
|
|
9922
|
+
if (target.isLocalParticipant)
|
|
9923
|
+
return;
|
|
9687
9924
|
this.state.updateParticipant(target.sessionId, (p) => {
|
|
9688
9925
|
const current = p.interruptedTracks ?? [];
|
|
9689
9926
|
const has = current.includes(trackType);
|
|
@@ -10426,7 +10663,6 @@ const watchCallRejected = (call) => {
|
|
|
10426
10663
|
*/
|
|
10427
10664
|
const watchCallEnded = (call) => {
|
|
10428
10665
|
return function onCallEnded() {
|
|
10429
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
10430
10666
|
const { callingState } = call.state;
|
|
10431
10667
|
if (callingState !== CallingState.IDLE &&
|
|
10432
10668
|
callingState !== CallingState.LEFT) {
|
|
@@ -10435,9 +10671,13 @@ const watchCallEnded = (call) => {
|
|
|
10435
10671
|
reason: 'call.ended event received',
|
|
10436
10672
|
});
|
|
10437
10673
|
call
|
|
10438
|
-
.leave({
|
|
10674
|
+
.leave({
|
|
10675
|
+
message: 'call.ended event received',
|
|
10676
|
+
reject: false,
|
|
10677
|
+
reason: 'ended',
|
|
10678
|
+
})
|
|
10439
10679
|
.catch((err) => {
|
|
10440
|
-
call.logger.error('Failed to leave call after call.ended
|
|
10680
|
+
call.logger.error('Failed to leave call after call.ended', err);
|
|
10441
10681
|
});
|
|
10442
10682
|
}
|
|
10443
10683
|
};
|
|
@@ -10617,7 +10857,7 @@ const handleRemoteSoftMute = (call) => {
|
|
|
10617
10857
|
/**
|
|
10618
10858
|
* An event responder which handles the `participantJoined` event.
|
|
10619
10859
|
*/
|
|
10620
|
-
const watchParticipantJoined = (state) => {
|
|
10860
|
+
const watchParticipantJoined = (state, e2ee = undefined) => {
|
|
10621
10861
|
return function onParticipantJoined(e) {
|
|
10622
10862
|
const { participant } = e;
|
|
10623
10863
|
if (!participant)
|
|
@@ -10630,7 +10870,7 @@ const watchParticipantJoined = (state) => {
|
|
|
10630
10870
|
// The SFU would send participant info as part of the `join`
|
|
10631
10871
|
// response and then follow up with a `participantJoined` event for
|
|
10632
10872
|
// already announced participants.
|
|
10633
|
-
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
10873
|
+
const orphanedTracks = reconcileOrphanedTracks(state, participant, e2ee);
|
|
10634
10874
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
10635
10875
|
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
10636
10876
|
viewportVisibilityState: {
|
|
@@ -10666,7 +10906,7 @@ const watchParticipantUpdated = (state) => {
|
|
|
10666
10906
|
* An event responder which handles the `trackPublished` event.
|
|
10667
10907
|
* The SFU will send this event when a participant publishes a track.
|
|
10668
10908
|
*/
|
|
10669
|
-
const watchTrackPublished = (state) => {
|
|
10909
|
+
const watchTrackPublished = (state, e2ee = undefined) => {
|
|
10670
10910
|
return function onTrackPublished(e) {
|
|
10671
10911
|
const { type, sessionId } = e;
|
|
10672
10912
|
// An optimization for large calls.
|
|
@@ -10674,7 +10914,7 @@ const watchTrackPublished = (state) => {
|
|
|
10674
10914
|
// events, and instead, it would only provide the participant's information
|
|
10675
10915
|
// once they start publishing a track.
|
|
10676
10916
|
if (e.participant) {
|
|
10677
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10917
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
|
|
10678
10918
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
10679
10919
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10680
10920
|
}
|
|
@@ -10689,12 +10929,12 @@ const watchTrackPublished = (state) => {
|
|
|
10689
10929
|
* An event responder which handles the `trackUnpublished` event.
|
|
10690
10930
|
* The SFU will send this event when a participant unpublishes a track.
|
|
10691
10931
|
*/
|
|
10692
|
-
const watchTrackUnpublished = (state) => {
|
|
10932
|
+
const watchTrackUnpublished = (state, e2ee = undefined) => {
|
|
10693
10933
|
return function onTrackUnpublished(e) {
|
|
10694
10934
|
const { type, sessionId } = e;
|
|
10695
10935
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
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, (p) => ({
|
|
10700
10940
|
pausedTracks: p.pausedTracks?.filter((t) => t !== type),
|
|
@@ -10713,17 +10953,22 @@ const watchTrackUnpublished = (state) => {
|
|
|
10713
10953
|
*
|
|
10714
10954
|
* @param state the call state.
|
|
10715
10955
|
* @param participant the participant.
|
|
10956
|
+
* @param e2ee accessor for the call's encryption manager, if any.
|
|
10716
10957
|
*/
|
|
10717
|
-
const reconcileOrphanedTracks = (state, participant) => {
|
|
10958
|
+
const reconcileOrphanedTracks = (state, participant, e2ee) => {
|
|
10718
10959
|
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
10719
10960
|
if (!orphanTracks.length)
|
|
10720
10961
|
return;
|
|
10962
|
+
const manager = e2ee?.();
|
|
10721
10963
|
const reconciledTracks = {};
|
|
10722
10964
|
for (const orphan of orphanTracks) {
|
|
10723
10965
|
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
10724
10966
|
if (!key)
|
|
10725
10967
|
continue;
|
|
10726
10968
|
reconciledTracks[key] = orphan.track;
|
|
10969
|
+
if (manager && orphan.receiver) {
|
|
10970
|
+
manager.decrypt(orphan.receiver, participant.userId, TrackType[orphan.trackType]);
|
|
10971
|
+
}
|
|
10727
10972
|
}
|
|
10728
10973
|
return reconciledTracks;
|
|
10729
10974
|
};
|
|
@@ -10779,6 +11024,11 @@ const watchAudioLevelChanged = (dispatcher, state) => {
|
|
|
10779
11024
|
*/
|
|
10780
11025
|
const registerEventHandlers = (call, dispatcher) => {
|
|
10781
11026
|
const state = call.state;
|
|
11027
|
+
// Read lazily on each event: setE2EEManager can run after setup() (an app that
|
|
11028
|
+
// inspects call settings via get()/getOrCreate() before deciding to encrypt has
|
|
11029
|
+
// already triggered it), so a value captured here would be a stale undefined
|
|
11030
|
+
// for the whole call and orphaned tracks would never get a decryptor.
|
|
11031
|
+
const e2ee = () => call.e2eeManager;
|
|
10782
11032
|
const eventHandlers = [
|
|
10783
11033
|
call.on('call.ended', watchCallEnded(call)),
|
|
10784
11034
|
watchSfuCallEnded(call),
|
|
@@ -10786,11 +11036,11 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
10786
11036
|
watchSfuErrorReports(dispatcher),
|
|
10787
11037
|
watchConnectionQualityChanged(dispatcher, state),
|
|
10788
11038
|
watchParticipantCountChanged(dispatcher, state),
|
|
10789
|
-
call.on('participantJoined', watchParticipantJoined(state)),
|
|
11039
|
+
call.on('participantJoined', watchParticipantJoined(state, e2ee)),
|
|
10790
11040
|
call.on('participantLeft', watchParticipantLeft(state)),
|
|
10791
11041
|
call.on('participantUpdated', watchParticipantUpdated(state)),
|
|
10792
|
-
call.on('trackPublished', watchTrackPublished(state)),
|
|
10793
|
-
call.on('trackUnpublished', watchTrackUnpublished(state)),
|
|
11042
|
+
call.on('trackPublished', watchTrackPublished(state, e2ee)),
|
|
11043
|
+
call.on('trackUnpublished', watchTrackUnpublished(state, e2ee)),
|
|
10794
11044
|
watchAudioLevelChanged(dispatcher, state),
|
|
10795
11045
|
watchDominantSpeakerChanged(dispatcher, state),
|
|
10796
11046
|
call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
|
|
@@ -11918,13 +12168,14 @@ class PermissionsContext {
|
|
|
11918
12168
|
return false;
|
|
11919
12169
|
const { audio, video, screensharing } = settings;
|
|
11920
12170
|
switch (permission) {
|
|
11921
|
-
case
|
|
12171
|
+
case RequestPermissionRequestPermissionsEnum.SEND_AUDIO:
|
|
11922
12172
|
return audio.access_request_enabled;
|
|
11923
|
-
case
|
|
12173
|
+
case RequestPermissionRequestPermissionsEnum.SEND_VIDEO:
|
|
11924
12174
|
return video.access_request_enabled;
|
|
11925
|
-
case
|
|
12175
|
+
case RequestPermissionRequestPermissionsEnum.SCREENSHARE:
|
|
11926
12176
|
return screensharing.access_request_enabled;
|
|
11927
12177
|
default:
|
|
12178
|
+
ensureExhausted(permission);
|
|
11928
12179
|
return false;
|
|
11929
12180
|
}
|
|
11930
12181
|
};
|
|
@@ -12931,6 +13182,27 @@ class DeviceManager {
|
|
|
12931
13182
|
this.filters.forEach((entry) => entry.stop?.());
|
|
12932
13183
|
}
|
|
12933
13184
|
}
|
|
13185
|
+
reconcileOptimisticStatus = async () => {
|
|
13186
|
+
const target = this.state.optimisticStatus;
|
|
13187
|
+
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
|
|
13188
|
+
try {
|
|
13189
|
+
if (target === 'enabled' && this.state.status !== 'enabled') {
|
|
13190
|
+
await this.unmuteStream();
|
|
13191
|
+
if (!signal.aborted)
|
|
13192
|
+
this.state.setStatus('enabled');
|
|
13193
|
+
}
|
|
13194
|
+
else if (target === 'disabled' && this.state.status === 'enabled') {
|
|
13195
|
+
// mirror whatever disable() does to stop/pause the track per disableMode
|
|
13196
|
+
if (!signal.aborted)
|
|
13197
|
+
this.state.setStatus('disabled');
|
|
13198
|
+
}
|
|
13199
|
+
}
|
|
13200
|
+
finally {
|
|
13201
|
+
if (!signal.aborted)
|
|
13202
|
+
this.state.setPendingStatus(this.state.status);
|
|
13203
|
+
}
|
|
13204
|
+
});
|
|
13205
|
+
};
|
|
12934
13206
|
disableTracks() {
|
|
12935
13207
|
this.getTracks().forEach((track) => {
|
|
12936
13208
|
if (track.enabled)
|
|
@@ -13211,7 +13483,7 @@ class DeviceManager {
|
|
|
13211
13483
|
createSyntheticDevice(selectedDevice, deviceKind);
|
|
13212
13484
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13213
13485
|
}
|
|
13214
|
-
async applyPersistedPreferences(enabledInCallType) {
|
|
13486
|
+
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13215
13487
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13216
13488
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
13217
13489
|
const preferenceList = toPreferenceList(preferences[deviceKey]);
|
|
@@ -13238,13 +13510,13 @@ class DeviceManager {
|
|
|
13238
13510
|
}
|
|
13239
13511
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
13240
13512
|
if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
|
|
13241
|
-
await this.applyMutedState(muted);
|
|
13513
|
+
await this.applyMutedState(muted, forceDisabled);
|
|
13242
13514
|
appliedMute = true;
|
|
13243
13515
|
}
|
|
13244
13516
|
return appliedDevice || appliedMute;
|
|
13245
13517
|
}
|
|
13246
|
-
async applyMutedState(muted) {
|
|
13247
|
-
if (this.state.status !== undefined)
|
|
13518
|
+
async applyMutedState(muted, forceDisabled) {
|
|
13519
|
+
if (forceDisabled || this.state.status !== undefined)
|
|
13248
13520
|
return;
|
|
13249
13521
|
if (muted) {
|
|
13250
13522
|
await this.disable();
|
|
@@ -13564,13 +13836,41 @@ class CameraManager extends DeviceManager {
|
|
|
13564
13836
|
}
|
|
13565
13837
|
}
|
|
13566
13838
|
}
|
|
13839
|
+
enable() {
|
|
13840
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13841
|
+
this.state.setPendingStatus('enabled');
|
|
13842
|
+
return Promise.resolve();
|
|
13843
|
+
}
|
|
13844
|
+
return super.enable();
|
|
13845
|
+
}
|
|
13846
|
+
async disable(forceStopOrOptions) {
|
|
13847
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13848
|
+
this.state.setPendingStatus('disabled');
|
|
13849
|
+
return;
|
|
13850
|
+
}
|
|
13851
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
13852
|
+
if (forceStopOrOptions === undefined)
|
|
13853
|
+
return super.disable();
|
|
13854
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
13855
|
+
return super.disable(forceStopOrOptions);
|
|
13856
|
+
}
|
|
13857
|
+
return super.disable(forceStopOrOptions);
|
|
13858
|
+
}
|
|
13859
|
+
toggle() {
|
|
13860
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13861
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
13862
|
+
return Promise.resolve();
|
|
13863
|
+
}
|
|
13864
|
+
return super.toggle();
|
|
13865
|
+
}
|
|
13567
13866
|
/**
|
|
13568
13867
|
* Applies the video settings to the camera.
|
|
13569
13868
|
*
|
|
13570
13869
|
* @param settings the video settings to apply.
|
|
13571
13870
|
* @param publish whether to publish the stream after applying the settings.
|
|
13871
|
+
* @param forceDisabled whether to keep the camera disabled, regardless of server-side or local preferences.
|
|
13572
13872
|
*/
|
|
13573
|
-
async apply(settings, publish) {
|
|
13873
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
13574
13874
|
// Wait for any in progress camera operation
|
|
13575
13875
|
await this.statusChangeSettled();
|
|
13576
13876
|
await this.selectTargetResolution(settings.target_resolution);
|
|
@@ -13582,8 +13882,7 @@ class CameraManager extends DeviceManager {
|
|
|
13582
13882
|
if (shouldApplyDefaults &&
|
|
13583
13883
|
this.devicePersistence.enabled &&
|
|
13584
13884
|
permissionState === 'granted') {
|
|
13585
|
-
persistedPreferencesApplied =
|
|
13586
|
-
await this.applyPersistedPreferences(enabledInCallType);
|
|
13885
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(enabledInCallType, forceDisabled);
|
|
13587
13886
|
}
|
|
13588
13887
|
// apply a direction and enable the camera only if in "pristine" state,
|
|
13589
13888
|
// and there are no persisted preferences
|
|
@@ -13593,13 +13892,23 @@ class CameraManager extends DeviceManager {
|
|
|
13593
13892
|
const direction = settings.camera_facing === 'front' ? 'front' : 'back';
|
|
13594
13893
|
await this.selectDirection(direction, { enableCamera: false });
|
|
13595
13894
|
}
|
|
13596
|
-
if (
|
|
13895
|
+
if (!forceDisabled &&
|
|
13896
|
+
canPublish &&
|
|
13897
|
+
settings.camera_default_on &&
|
|
13898
|
+
enabledInCallType) {
|
|
13597
13899
|
await this.enable();
|
|
13598
13900
|
}
|
|
13599
13901
|
}
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13902
|
+
if (isReactNative() && publish && canPublish) {
|
|
13903
|
+
// On RN the camera is enabled/disabled optimistically before JOINED. Reconcile now
|
|
13904
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
13905
|
+
await this.reconcileOptimisticStatus();
|
|
13906
|
+
}
|
|
13907
|
+
else {
|
|
13908
|
+
const { mediaStream } = this.state;
|
|
13909
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
13910
|
+
await this.publishStream(mediaStream);
|
|
13911
|
+
}
|
|
13603
13912
|
}
|
|
13604
13913
|
}
|
|
13605
13914
|
getDevices() {
|
|
@@ -13618,7 +13927,10 @@ class CameraManager extends DeviceManager {
|
|
|
13618
13927
|
}
|
|
13619
13928
|
return constraints;
|
|
13620
13929
|
}
|
|
13621
|
-
getStream(constraints) {
|
|
13930
|
+
async getStream(constraints) {
|
|
13931
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
13932
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
13933
|
+
await this.call.ensureMediaFactory();
|
|
13622
13934
|
return getVideoStream(constraints, this.call.tracer);
|
|
13623
13935
|
}
|
|
13624
13936
|
}
|
|
@@ -13634,6 +13946,9 @@ class AudioDeviceManager extends DeviceManager {
|
|
|
13634
13946
|
if (!this.call.state.settings?.audio.hifi_audio_enabled) {
|
|
13635
13947
|
throw new Error('High Fidelity audio is not enabled for this call');
|
|
13636
13948
|
}
|
|
13949
|
+
if (isReactNative() && this.call.hasMediaEngine) {
|
|
13950
|
+
throw new Error('setAudioBitrateProfile must be called before joining the call.');
|
|
13951
|
+
}
|
|
13637
13952
|
this.doSetAudioBitrateProfile(profile);
|
|
13638
13953
|
this.state.setAudioBitrateProfile(profile);
|
|
13639
13954
|
if (this.enabled) {
|
|
@@ -13906,7 +14221,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
13906
14221
|
]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
|
|
13907
14222
|
try {
|
|
13908
14223
|
if (callingState === CallingState.LEFT) {
|
|
13909
|
-
|
|
14224
|
+
// The muted-recording-prepared mode is reset in `callManager.stop()`
|
|
14225
|
+
// (during leave, while the call factory is still alive), not here —
|
|
14226
|
+
// this subscription fires asynchronously and could land after the
|
|
14227
|
+
// factory is disposed, forcing a default-ADM rebuild.
|
|
13910
14228
|
await this.stopSpeakingWhileMutedDetection();
|
|
13911
14229
|
}
|
|
13912
14230
|
if (callingState !== CallingState.JOINED)
|
|
@@ -14006,6 +14324,33 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14006
14324
|
this.subscriptions.push(unsubscribe);
|
|
14007
14325
|
}
|
|
14008
14326
|
}
|
|
14327
|
+
enable() {
|
|
14328
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14329
|
+
this.state.setPendingStatus('enabled');
|
|
14330
|
+
return Promise.resolve();
|
|
14331
|
+
}
|
|
14332
|
+
return super.enable();
|
|
14333
|
+
}
|
|
14334
|
+
async disable(forceStopOrOptions) {
|
|
14335
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14336
|
+
this.state.setPendingStatus('disabled');
|
|
14337
|
+
return;
|
|
14338
|
+
}
|
|
14339
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
14340
|
+
if (forceStopOrOptions === undefined)
|
|
14341
|
+
return super.disable();
|
|
14342
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
14343
|
+
return super.disable(forceStopOrOptions);
|
|
14344
|
+
}
|
|
14345
|
+
return super.disable(forceStopOrOptions);
|
|
14346
|
+
}
|
|
14347
|
+
toggle() {
|
|
14348
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14349
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
14350
|
+
return Promise.resolve();
|
|
14351
|
+
}
|
|
14352
|
+
return super.toggle();
|
|
14353
|
+
}
|
|
14009
14354
|
/**
|
|
14010
14355
|
* Enables noise cancellation for the microphone.
|
|
14011
14356
|
*
|
|
@@ -14145,8 +14490,9 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14145
14490
|
* Applies the audio settings to the microphone.
|
|
14146
14491
|
* @param settings the audio settings to apply.
|
|
14147
14492
|
* @param publish whether to publish the stream after applying the settings.
|
|
14493
|
+
* @param forceDisabled whether to keep the mic disabled, regardless of server-side or local preferences.
|
|
14148
14494
|
*/
|
|
14149
|
-
async apply(settings, publish) {
|
|
14495
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
14150
14496
|
// Wait for any in progress mic operation
|
|
14151
14497
|
await this.statusChangeSettled();
|
|
14152
14498
|
// apply server-side settings only when the device state is pristine
|
|
@@ -14158,23 +14504,33 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14158
14504
|
if (shouldApplyDefaults &&
|
|
14159
14505
|
this.devicePersistence.enabled &&
|
|
14160
14506
|
permissionState === 'granted') {
|
|
14161
|
-
persistedPreferencesApplied = await this.applyPersistedPreferences(true);
|
|
14507
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(true, forceDisabled);
|
|
14162
14508
|
}
|
|
14163
14509
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
14164
14510
|
if (shouldApplyDefaults && !persistedPreferencesApplied) {
|
|
14165
|
-
if (canPublish && settings.mic_default_on) {
|
|
14511
|
+
if (!forceDisabled && canPublish && settings.mic_default_on) {
|
|
14166
14512
|
await this.enable();
|
|
14167
14513
|
}
|
|
14168
14514
|
}
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14515
|
+
if (isReactNative() && publish && canPublish) {
|
|
14516
|
+
// On RN the microphone is enabled/disabled optimistically before JOINED. Reconcile now
|
|
14517
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
14518
|
+
await this.reconcileOptimisticStatus();
|
|
14519
|
+
}
|
|
14520
|
+
else {
|
|
14521
|
+
const { mediaStream } = this.state;
|
|
14522
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
14523
|
+
await this.publishStream(mediaStream);
|
|
14524
|
+
}
|
|
14172
14525
|
}
|
|
14173
14526
|
}
|
|
14174
14527
|
getDevices() {
|
|
14175
14528
|
return getAudioDevices(this.call.tracer);
|
|
14176
14529
|
}
|
|
14177
|
-
getStream(constraints) {
|
|
14530
|
+
async getStream(constraints) {
|
|
14531
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14532
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14533
|
+
await this.call.ensureMediaFactory();
|
|
14178
14534
|
return getAudioStream(constraints, this.call.tracer);
|
|
14179
14535
|
}
|
|
14180
14536
|
doSetAudioBitrateProfile(profile) {
|
|
@@ -14383,6 +14739,9 @@ class ScreenShareManager extends AudioDeviceManager {
|
|
|
14383
14739
|
if (!this.state.audioEnabled) {
|
|
14384
14740
|
constraints.audio = false;
|
|
14385
14741
|
}
|
|
14742
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14743
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14744
|
+
await this.call.ensureMediaFactory();
|
|
14386
14745
|
const stream = await getScreenShareStream(constraints, this.call.tracer);
|
|
14387
14746
|
const [track] = stream.getVideoTracks();
|
|
14388
14747
|
const { contentHint } = this.state.settings || {};
|
|
@@ -14487,10 +14846,14 @@ class SpeakerManager {
|
|
|
14487
14846
|
this.devicePersistence = devicePreferences;
|
|
14488
14847
|
this.setup();
|
|
14489
14848
|
}
|
|
14490
|
-
apply(settings) {
|
|
14491
|
-
|
|
14849
|
+
async apply(settings) {
|
|
14850
|
+
if (isReactNative()) {
|
|
14851
|
+
this.applyRN(settings);
|
|
14852
|
+
return;
|
|
14853
|
+
}
|
|
14854
|
+
await this.applyWeb();
|
|
14492
14855
|
}
|
|
14493
|
-
applyWeb() {
|
|
14856
|
+
async applyWeb() {
|
|
14494
14857
|
const { enabled, storageKey } = this.devicePersistence;
|
|
14495
14858
|
if (!enabled)
|
|
14496
14859
|
return;
|
|
@@ -14502,8 +14865,22 @@ class SpeakerManager {
|
|
|
14502
14865
|
const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
|
|
14503
14866
|
? ''
|
|
14504
14867
|
: preference.selectedDeviceId;
|
|
14505
|
-
if (
|
|
14506
|
-
this.
|
|
14868
|
+
if (!nextDeviceId) {
|
|
14869
|
+
if (this.state.selectedDevice !== nextDeviceId) {
|
|
14870
|
+
this.select(nextDeviceId);
|
|
14871
|
+
}
|
|
14872
|
+
return;
|
|
14873
|
+
}
|
|
14874
|
+
const permissionState = await firstValueFrom(getAudioBrowserPermission(this.call.tracer).asStateObservable());
|
|
14875
|
+
if (permissionState !== 'granted')
|
|
14876
|
+
return;
|
|
14877
|
+
const devices = await firstValueFrom(this.listDevices());
|
|
14878
|
+
const device = this.findDevice(devices, nextDeviceId) ??
|
|
14879
|
+
(preference.selectedDeviceLabel
|
|
14880
|
+
? devices.find((d) => d.label === preference.selectedDeviceLabel)
|
|
14881
|
+
: undefined);
|
|
14882
|
+
if (device && this.state.selectedDevice !== device.deviceId) {
|
|
14883
|
+
this.select(device.deviceId);
|
|
14507
14884
|
}
|
|
14508
14885
|
}
|
|
14509
14886
|
applyRN(settings) {
|
|
@@ -14532,6 +14909,7 @@ class SpeakerManager {
|
|
|
14532
14909
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
14533
14910
|
defaultDevice,
|
|
14534
14911
|
isRingingTypeCall: this.call.ringing,
|
|
14912
|
+
cid: this.call.cid,
|
|
14535
14913
|
});
|
|
14536
14914
|
}
|
|
14537
14915
|
}
|
|
@@ -14540,11 +14918,15 @@ class SpeakerManager {
|
|
|
14540
14918
|
return;
|
|
14541
14919
|
this.areSubscriptionsSetUp = true;
|
|
14542
14920
|
if (deviceIds$ && !isReactNative()) {
|
|
14543
|
-
this.subscriptions.push(createSubscription(combineLatest([
|
|
14921
|
+
this.subscriptions.push(createSubscription(combineLatest([
|
|
14922
|
+
deviceIds$.pipe(pairwise()),
|
|
14923
|
+
this.state.selectedDevice$,
|
|
14924
|
+
]), ([[prevDevices, currentDevices], deviceId]) => {
|
|
14544
14925
|
if (!deviceId)
|
|
14545
14926
|
return;
|
|
14546
|
-
const
|
|
14547
|
-
|
|
14927
|
+
const isDisconnected = this.findDevice(prevDevices, deviceId) &&
|
|
14928
|
+
!this.findDevice(currentDevices, deviceId);
|
|
14929
|
+
if (isDisconnected)
|
|
14548
14930
|
this.select('');
|
|
14549
14931
|
}));
|
|
14550
14932
|
}
|
|
@@ -14591,6 +14973,7 @@ class SpeakerManager {
|
|
|
14591
14973
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
14592
14974
|
this.subscriptions = [];
|
|
14593
14975
|
this.areSubscriptionsSetUp = false;
|
|
14976
|
+
this.defaultDevice = undefined;
|
|
14594
14977
|
};
|
|
14595
14978
|
/**
|
|
14596
14979
|
* Set the volume of the audio elements
|
|
@@ -14625,6 +15008,7 @@ class SpeakerManager {
|
|
|
14625
15008
|
return { audioVolume: volume };
|
|
14626
15009
|
});
|
|
14627
15010
|
}
|
|
15011
|
+
findDevice = (devices, deviceId) => devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
|
|
14628
15012
|
persistSpeakerDevicePreference(selectedDevice) {
|
|
14629
15013
|
const { storageKey } = this.devicePersistence;
|
|
14630
15014
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
@@ -14707,6 +15091,7 @@ class Call {
|
|
|
14707
15091
|
blockedAudioTracker;
|
|
14708
15092
|
subscriber;
|
|
14709
15093
|
publisher;
|
|
15094
|
+
e2eeManager;
|
|
14710
15095
|
/**
|
|
14711
15096
|
* Flag telling whether this call is a "ringing" call.
|
|
14712
15097
|
*/
|
|
@@ -14762,6 +15147,8 @@ class Call {
|
|
|
14762
15147
|
allowOwnTracksLoopback = false;
|
|
14763
15148
|
hasJoinedOnce = false;
|
|
14764
15149
|
deviceSettingsAppliedOnce = false;
|
|
15150
|
+
callManagerStarted = false;
|
|
15151
|
+
leaveGeneration = 0;
|
|
14765
15152
|
credentials;
|
|
14766
15153
|
initialized = false;
|
|
14767
15154
|
acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
|
|
@@ -14780,6 +15167,13 @@ class Call {
|
|
|
14780
15167
|
clientCapabilities = new Set([
|
|
14781
15168
|
ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
|
|
14782
15169
|
]);
|
|
15170
|
+
/**
|
|
15171
|
+
* The in-flight per-call media engine. On web/React this resolves to a thin
|
|
15172
|
+
* globals-backed engine (no provider registered); React Native registers a
|
|
15173
|
+
* provider that owns a per-call native factory.
|
|
15174
|
+
* @internal
|
|
15175
|
+
*/
|
|
15176
|
+
mediaEnginePromise;
|
|
14783
15177
|
/**
|
|
14784
15178
|
* Constructs a new `Call` instance.
|
|
14785
15179
|
*
|
|
@@ -15049,6 +15443,7 @@ class Call {
|
|
|
15049
15443
|
if (callingState === CallingState.LEFT) {
|
|
15050
15444
|
return;
|
|
15051
15445
|
}
|
|
15446
|
+
this.leaveGeneration += 1;
|
|
15052
15447
|
if (callingState === CallingState.JOINING) {
|
|
15053
15448
|
const waitUntilCallJoined = () => {
|
|
15054
15449
|
return new Promise((resolve) => {
|
|
@@ -15082,7 +15477,7 @@ class Call {
|
|
|
15082
15477
|
}
|
|
15083
15478
|
}
|
|
15084
15479
|
}
|
|
15085
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
15480
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, reason === 'ended' ? 'remote' : undefined);
|
|
15086
15481
|
this.statsReporter?.stop();
|
|
15087
15482
|
this.statsReporter = undefined;
|
|
15088
15483
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -15135,12 +15530,14 @@ class Call {
|
|
|
15135
15530
|
this.clientStore.unregisterCall(this);
|
|
15136
15531
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15137
15532
|
isRingingTypeCall: this.ringing,
|
|
15533
|
+
shouldStopCallManager: this.callManagerStarted,
|
|
15138
15534
|
});
|
|
15139
15535
|
this.camera.dispose();
|
|
15140
15536
|
this.microphone.dispose();
|
|
15141
15537
|
this.screenShare.dispose();
|
|
15142
15538
|
this.speaker.dispose();
|
|
15143
15539
|
this.deviceSettingsAppliedOnce = false;
|
|
15540
|
+
this.callManagerStarted = false;
|
|
15144
15541
|
const stopOnLeavePromises = [];
|
|
15145
15542
|
if (this.camera.stopOnLeave) {
|
|
15146
15543
|
stopOnLeavePromises.push(this.camera.disable(true));
|
|
@@ -15152,6 +15549,22 @@ class Call {
|
|
|
15152
15549
|
stopOnLeavePromises.push(this.screenShare.disable(true));
|
|
15153
15550
|
}
|
|
15154
15551
|
await Promise.all(stopOnLeavePromises);
|
|
15552
|
+
// Dispose the per-call media engine last — after peer connections and
|
|
15553
|
+
// local tracks are gone — so the backing factory tears down with no
|
|
15554
|
+
// owned PCs/tracks. A fresh `join()` builds a new engine.
|
|
15555
|
+
if (this.mediaEnginePromise) {
|
|
15556
|
+
const enginePromise = this.mediaEnginePromise;
|
|
15557
|
+
this.mediaEnginePromise = undefined;
|
|
15558
|
+
this.logger.debug('Disposing per-call media factory');
|
|
15559
|
+
await enginePromise
|
|
15560
|
+
.then((engine) => {
|
|
15561
|
+
globalThis.streamRNVideoSDK?.callingX?.unwireAudioEngineSubscription();
|
|
15562
|
+
return engine.dispose();
|
|
15563
|
+
})
|
|
15564
|
+
.catch((err) => {
|
|
15565
|
+
this.logger.warn('Failed to dispose media engine', err);
|
|
15566
|
+
});
|
|
15567
|
+
}
|
|
15155
15568
|
});
|
|
15156
15569
|
};
|
|
15157
15570
|
/**
|
|
@@ -15227,8 +15640,18 @@ class Call {
|
|
|
15227
15640
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
15228
15641
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
15229
15642
|
this.clientStore.setCalls([this, ...calls]);
|
|
15230
|
-
|
|
15231
|
-
|
|
15643
|
+
await this.applyDeviceConfig(settings, { publish: false });
|
|
15644
|
+
};
|
|
15645
|
+
/**
|
|
15646
|
+
* Applies a backend-provided call state snapshot
|
|
15647
|
+
* (call, members, own capabilities) to this instance.
|
|
15648
|
+
*
|
|
15649
|
+
* @internal an internal method and should not be used outside the SDK.
|
|
15650
|
+
*/
|
|
15651
|
+
updateFromCallStateResponse = (response) => {
|
|
15652
|
+
this.state.updateFromCallResponse(response.call);
|
|
15653
|
+
this.state.setMembers(response.members);
|
|
15654
|
+
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15232
15655
|
};
|
|
15233
15656
|
/**
|
|
15234
15657
|
* Loads the information about the call.
|
|
@@ -15241,9 +15664,7 @@ class Call {
|
|
|
15241
15664
|
get = async (params) => {
|
|
15242
15665
|
await this.setup();
|
|
15243
15666
|
const response = await this.streamClient.get(this.streamClientBasePath, params);
|
|
15244
|
-
this.
|
|
15245
|
-
this.state.setMembers(response.members);
|
|
15246
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15667
|
+
this.updateFromCallStateResponse(response);
|
|
15247
15668
|
if (params?.ring) {
|
|
15248
15669
|
this.ringingSubject.next(true);
|
|
15249
15670
|
}
|
|
@@ -15251,13 +15672,7 @@ class Call {
|
|
|
15251
15672
|
this.watching = true;
|
|
15252
15673
|
this.clientStore.registerOrUpdateCall(this);
|
|
15253
15674
|
}
|
|
15254
|
-
|
|
15255
|
-
const skipSpeakerApply = isReactNative()
|
|
15256
|
-
? params?.ring === true
|
|
15257
|
-
? true
|
|
15258
|
-
: this.ringing
|
|
15259
|
-
: false;
|
|
15260
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15675
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15261
15676
|
return response;
|
|
15262
15677
|
};
|
|
15263
15678
|
/**
|
|
@@ -15268,9 +15683,7 @@ class Call {
|
|
|
15268
15683
|
getOrCreate = async (data) => {
|
|
15269
15684
|
await this.setup();
|
|
15270
15685
|
const response = await this.streamClient.post(this.streamClientBasePath, data);
|
|
15271
|
-
this.
|
|
15272
|
-
this.state.setMembers(response.members);
|
|
15273
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15686
|
+
this.updateFromCallStateResponse(response);
|
|
15274
15687
|
if (data?.ring) {
|
|
15275
15688
|
this.ringingSubject.next(true);
|
|
15276
15689
|
}
|
|
@@ -15278,13 +15691,7 @@ class Call {
|
|
|
15278
15691
|
this.watching = true;
|
|
15279
15692
|
this.clientStore.registerOrUpdateCall(this);
|
|
15280
15693
|
}
|
|
15281
|
-
|
|
15282
|
-
const skipSpeakerApply = isReactNative()
|
|
15283
|
-
? data?.ring === true
|
|
15284
|
-
? true
|
|
15285
|
-
: this.ringing
|
|
15286
|
-
: false;
|
|
15287
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15694
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15288
15695
|
return response;
|
|
15289
15696
|
};
|
|
15290
15697
|
/**
|
|
@@ -15348,7 +15755,7 @@ class Call {
|
|
|
15348
15755
|
*
|
|
15349
15756
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15350
15757
|
*/
|
|
15351
|
-
join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15758
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15352
15759
|
const callingState = this.state.callingState;
|
|
15353
15760
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
15354
15761
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -15370,7 +15777,6 @@ class Call {
|
|
|
15370
15777
|
callId: this.id,
|
|
15371
15778
|
getCallSessionId: () => this.state.session?.id ?? '',
|
|
15372
15779
|
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
15373
|
-
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
15374
15780
|
});
|
|
15375
15781
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
15376
15782
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
@@ -15422,7 +15828,7 @@ class Call {
|
|
|
15422
15828
|
callingX?.endCall(this, 'error');
|
|
15423
15829
|
throw error;
|
|
15424
15830
|
}
|
|
15425
|
-
};
|
|
15831
|
+
});
|
|
15426
15832
|
/**
|
|
15427
15833
|
* Will make a single attempt to watch for call related WebSocket events
|
|
15428
15834
|
* and initiate a call session with the server.
|
|
@@ -15432,9 +15838,20 @@ class Call {
|
|
|
15432
15838
|
doJoin = async (data) => {
|
|
15433
15839
|
const connectStartTime = Date.now();
|
|
15434
15840
|
const callingState = this.state.callingState;
|
|
15841
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
15842
|
+
const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
|
|
15435
15843
|
this.joinCallData = data;
|
|
15436
15844
|
this.logger.debug('Starting join flow');
|
|
15437
15845
|
this.state.setCallingState(CallingState.JOINING);
|
|
15846
|
+
// Ensure the per-call media engine exists before any peer connection
|
|
15847
|
+
// (codec probe, subscriber, publisher) or capture happens, so the WebRTC
|
|
15848
|
+
// globals resolve to the call's factory. Idempotent across
|
|
15849
|
+
// reconnect/migration attempts.
|
|
15850
|
+
await this.ensureMediaFactory();
|
|
15851
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
15852
|
+
if (callingX) {
|
|
15853
|
+
callingX.wireAudioEngineSubscription();
|
|
15854
|
+
}
|
|
15438
15855
|
const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
15439
15856
|
const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
15440
15857
|
const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
@@ -15489,6 +15906,11 @@ class Call {
|
|
|
15489
15906
|
// these are throw-away SDPs that the SFU will use to determine
|
|
15490
15907
|
// the capabilities of the client (codec support, etc.)
|
|
15491
15908
|
const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
15909
|
+
// skip if a leave superseded this join so codec detection doesn't resolve to a default factory.
|
|
15910
|
+
if (supersededByLeave()) {
|
|
15911
|
+
this.logger.debug('Join superseded by leave; skipping codec detection');
|
|
15912
|
+
return;
|
|
15913
|
+
}
|
|
15492
15914
|
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
15493
15915
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
15494
15916
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
@@ -15532,6 +15954,12 @@ class Call {
|
|
|
15532
15954
|
throw error;
|
|
15533
15955
|
}
|
|
15534
15956
|
}
|
|
15957
|
+
// If the user left while this join was in flight, bail before re-setting JOINED and before
|
|
15958
|
+
// peer-connection setup below (both run synchronously after this, so one check covers them).
|
|
15959
|
+
if (supersededByLeave()) {
|
|
15960
|
+
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
15961
|
+
return;
|
|
15962
|
+
}
|
|
15535
15963
|
if (!performingMigration) {
|
|
15536
15964
|
// in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
|
|
15537
15965
|
this.state.setCallingState(CallingState.JOINED);
|
|
@@ -15569,12 +15997,21 @@ class Call {
|
|
|
15569
15997
|
}
|
|
15570
15998
|
// device settings should be applied only once, we don't have to
|
|
15571
15999
|
// re-apply them on later reconnections or server-side data fetches
|
|
15572
|
-
if (!this.deviceSettingsAppliedOnce &&
|
|
15573
|
-
|
|
16000
|
+
if (!this.deviceSettingsAppliedOnce &&
|
|
16001
|
+
this.state.settings &&
|
|
16002
|
+
!supersededByLeave()) {
|
|
16003
|
+
await this.applyDeviceConfig(this.state.settings, {
|
|
16004
|
+
publish: true,
|
|
16005
|
+
skipSpeakerApply: false,
|
|
16006
|
+
});
|
|
16007
|
+
this.deviceSettingsAppliedOnce = true;
|
|
16008
|
+
}
|
|
16009
|
+
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
15574
16010
|
globalThis.streamRNVideoSDK?.callManager.start({
|
|
15575
16011
|
isRingingTypeCall: this.ringing,
|
|
16012
|
+
cid: this.cid,
|
|
15576
16013
|
});
|
|
15577
|
-
this.
|
|
16014
|
+
this.callManagerStarted = true;
|
|
15578
16015
|
}
|
|
15579
16016
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
15580
16017
|
// as it's a one-time event and clashes with the potential reconnection attempts.
|
|
@@ -15701,6 +16138,7 @@ class Call {
|
|
|
15701
16138
|
this.sfuStatsReporter = undefined;
|
|
15702
16139
|
if (closePreviousInstances && this.subscriber) {
|
|
15703
16140
|
await this.subscriber.dispose();
|
|
16141
|
+
this.state.removeAllOrphanedTracks();
|
|
15704
16142
|
}
|
|
15705
16143
|
const basePeerConnectionOptions = {
|
|
15706
16144
|
sfuClient,
|
|
@@ -15711,6 +16149,7 @@ class Call {
|
|
|
15711
16149
|
enableTracing,
|
|
15712
16150
|
statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
|
|
15713
16151
|
clientPublishOptions: this.clientPublishOptions,
|
|
16152
|
+
e2ee: this.e2eeManager,
|
|
15714
16153
|
onReconnectionNeeded: (kind, reason, peerType) => {
|
|
15715
16154
|
this.reconnect(kind, reason).catch((err) => {
|
|
15716
16155
|
const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
|
|
@@ -15776,12 +16215,12 @@ class Call {
|
|
|
15776
16215
|
* Retrieves credentials for joining the call.
|
|
15777
16216
|
*
|
|
15778
16217
|
* @internal
|
|
15779
|
-
*
|
|
15780
16218
|
* @param data the join call data.
|
|
15781
16219
|
*/
|
|
15782
16220
|
doJoinRequest = async (data) => {
|
|
15783
16221
|
const location = await this.streamClient.getLocationHint();
|
|
15784
|
-
const
|
|
16222
|
+
const e2ee = !!this.e2eeManager;
|
|
16223
|
+
const request = { ...data, location, e2ee };
|
|
15785
16224
|
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
15786
16225
|
this.state.updateFromCallResponse(joinResponse.call);
|
|
15787
16226
|
this.state.setMembers(joinResponse.members);
|
|
@@ -15800,6 +16239,36 @@ class Call {
|
|
|
15800
16239
|
}
|
|
15801
16240
|
return joinResponse;
|
|
15802
16241
|
};
|
|
16242
|
+
/**
|
|
16243
|
+
* Whether the per-call media engine currently exists. True from join until leave.
|
|
16244
|
+
*
|
|
16245
|
+
* @internal an internal getter and should not be used outside the SDK.
|
|
16246
|
+
*/
|
|
16247
|
+
get hasMediaEngine() {
|
|
16248
|
+
return !!this.mediaEnginePromise;
|
|
16249
|
+
}
|
|
16250
|
+
/**
|
|
16251
|
+
* Ensures a {@link CallMediaEngine} exists for this call's media session and
|
|
16252
|
+
* returns it. Idempotent: the engine is created once via the registered
|
|
16253
|
+
* provider (see `setCallMediaEngineProvider`) and cached until `leave()`
|
|
16254
|
+
* disposes it. Concurrent callers (e.g. camera + microphone enabling in
|
|
16255
|
+
* parallel) share the same engine because the in-flight creation promise is
|
|
16256
|
+
* cached, never the unresolved result.
|
|
16257
|
+
*
|
|
16258
|
+
* @internal
|
|
16259
|
+
*/
|
|
16260
|
+
ensureMediaFactory = async () => {
|
|
16261
|
+
if (!this.mediaEnginePromise) {
|
|
16262
|
+
const provider = getCallMediaEngineProvider();
|
|
16263
|
+
this.logger.debug(`Requesting per-call media factory creation`);
|
|
16264
|
+
this.mediaEnginePromise = Promise.resolve(provider()).catch((err) => {
|
|
16265
|
+
// Drop the cached rejection so a retried join() can rebuild the engine
|
|
16266
|
+
this.mediaEnginePromise = undefined;
|
|
16267
|
+
throw err;
|
|
16268
|
+
});
|
|
16269
|
+
}
|
|
16270
|
+
return this.mediaEnginePromise;
|
|
16271
|
+
};
|
|
15803
16272
|
/**
|
|
15804
16273
|
* Handles the closing of the SFU signal connection.
|
|
15805
16274
|
*
|
|
@@ -16324,12 +16793,16 @@ class Call {
|
|
|
16324
16793
|
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
16325
16794
|
if (!streamStateProp)
|
|
16326
16795
|
continue;
|
|
16327
|
-
this.state.updateParticipant(sessionId, (p) =>
|
|
16328
|
-
|
|
16329
|
-
|
|
16330
|
-
|
|
16331
|
-
|
|
16332
|
-
|
|
16796
|
+
this.state.updateParticipant(sessionId, (p) => {
|
|
16797
|
+
const currentStream = p[streamStateProp];
|
|
16798
|
+
const isEchoedStream = this.subscriber?.isSelfSubscribedStream(currentStream);
|
|
16799
|
+
return {
|
|
16800
|
+
publishedTracks: mediaStream
|
|
16801
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
16802
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
16803
|
+
[streamStateProp]: isEchoedStream ? currentStream : mediaStream,
|
|
16804
|
+
};
|
|
16805
|
+
});
|
|
16333
16806
|
}
|
|
16334
16807
|
};
|
|
16335
16808
|
/**
|
|
@@ -16361,6 +16834,30 @@ class Call {
|
|
|
16361
16834
|
this.tracer.trace('updatePublishOptions', options);
|
|
16362
16835
|
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
16363
16836
|
};
|
|
16837
|
+
/**
|
|
16838
|
+
* Set the E2EE (end-to-end encryption) manager for this call.
|
|
16839
|
+
*
|
|
16840
|
+
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
16841
|
+
* configured for E2EE.
|
|
16842
|
+
*
|
|
16843
|
+
* The manager is kept across {@link leave} so a rejoin of this same instance
|
|
16844
|
+
* stays encrypted: do not dispose it while this call may be joined again.
|
|
16845
|
+
* A disposed manager throws from `encrypt`/`decrypt` rather than silently
|
|
16846
|
+
* publishing nothing, so re-attach a fresh one instead of reusing it.
|
|
16847
|
+
*
|
|
16848
|
+
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
16849
|
+
* built-in AES-GCM scheme, or pass your own implementation.
|
|
16850
|
+
* @throws if called after the peer connections have been built (i.e. after
|
|
16851
|
+
* `join`): those PCs were already configured without an encryptor, so
|
|
16852
|
+
* adopting a manager now would silently publish/receive cleartext for
|
|
16853
|
+
* the live session.
|
|
16854
|
+
*/
|
|
16855
|
+
setE2EEManager = (e2ee) => {
|
|
16856
|
+
if (this.publisher || this.subscriber) {
|
|
16857
|
+
throw new Error('setE2EEManager must be called before join()');
|
|
16858
|
+
}
|
|
16859
|
+
this.e2eeManager = e2ee;
|
|
16860
|
+
};
|
|
16364
16861
|
/**
|
|
16365
16862
|
* Notifies the SFU that a noise cancellation process has started.
|
|
16366
16863
|
*
|
|
@@ -16995,16 +17492,21 @@ class Call {
|
|
|
16995
17492
|
*
|
|
16996
17493
|
* @internal
|
|
16997
17494
|
*/
|
|
16998
|
-
applyDeviceConfig = async (settings,
|
|
17495
|
+
applyDeviceConfig = async (settings, opts) => {
|
|
17496
|
+
const { publish,
|
|
17497
|
+
// Skip speaker setup on RN until joining (explicitly passed as false during join())
|
|
17498
|
+
skipSpeakerApply = isReactNative(), withDisabledDevices, } = opts;
|
|
16999
17499
|
if (!skipSpeakerApply) {
|
|
17000
|
-
this.speaker
|
|
17500
|
+
await this.speaker
|
|
17501
|
+
.apply(settings)
|
|
17502
|
+
.catch((err) => this.logger.warn('Speaker init failed', err));
|
|
17001
17503
|
}
|
|
17002
|
-
await this.camera
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
await this.microphone
|
|
17006
|
-
|
|
17007
|
-
|
|
17504
|
+
await this.camera
|
|
17505
|
+
.apply(settings.video, publish, withDisabledDevices)
|
|
17506
|
+
.catch((err) => this.logger.warn('Camera init failed', err));
|
|
17507
|
+
await this.microphone
|
|
17508
|
+
.apply(settings.audio, publish, withDisabledDevices)
|
|
17509
|
+
.catch((err) => this.logger.warn('Mic init failed', err));
|
|
17008
17510
|
};
|
|
17009
17511
|
/**
|
|
17010
17512
|
* Will begin tracking the given element for visibility changes within the
|
|
@@ -17678,15 +18180,17 @@ class StableWSConnection {
|
|
|
17678
18180
|
this.client.logger.error(`Token not set, can't connect authenticate`);
|
|
17679
18181
|
return;
|
|
17680
18182
|
}
|
|
17681
|
-
const
|
|
18183
|
+
const wsAuthMessage = {
|
|
17682
18184
|
token,
|
|
18185
|
+
products: [WSAuthMessageProductsEnum.VIDEO],
|
|
17683
18186
|
user_details: {
|
|
17684
18187
|
id: user.id,
|
|
17685
18188
|
name: user.name,
|
|
17686
18189
|
image: user.image,
|
|
17687
18190
|
custom: user.custom,
|
|
17688
18191
|
},
|
|
17689
|
-
}
|
|
18192
|
+
};
|
|
18193
|
+
const authMessage = JSON.stringify(wsAuthMessage);
|
|
17690
18194
|
this._log(`onopen() - Sending auth message ${authMessage}`, {}, 'trace');
|
|
17691
18195
|
this.ws?.send(authMessage);
|
|
17692
18196
|
this._log('onopen() - onopen callback', { wsID });
|
|
@@ -18512,11 +19016,11 @@ class StreamClient {
|
|
|
18512
19016
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18513
19017
|
};
|
|
18514
19018
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18515
|
-
"1.
|
|
19019
|
+
"1.59.0";
|
|
18516
19020
|
getUserAgent = () => {
|
|
18517
19021
|
if (!this.cachedUserAgent) {
|
|
18518
19022
|
const { clientAppIdentifier = {} } = this.options;
|
|
18519
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
19023
|
+
const { sdkName = 'js', sdkVersion = "1.59.0", ...extras } = clientAppIdentifier;
|
|
18520
19024
|
this.cachedUserAgent = [
|
|
18521
19025
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18522
19026
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -18879,7 +19383,6 @@ class ClientEventReporter {
|
|
|
18879
19383
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
18880
19384
|
type: ctx?.callType,
|
|
18881
19385
|
id: ctx?.callId,
|
|
18882
|
-
call_cid: cid,
|
|
18883
19386
|
stage: 'JoinInitiated',
|
|
18884
19387
|
join_attempt_id: joinAttemptId,
|
|
18885
19388
|
...(coordinatorConnectId && {
|
|
@@ -19061,7 +19564,6 @@ class ClientEventReporter {
|
|
|
19061
19564
|
startedAt: Date.now(),
|
|
19062
19565
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19063
19566
|
sfuId: this.getSfuId(cid),
|
|
19064
|
-
userSessionId: this.getUserSessionId(cid),
|
|
19065
19567
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19066
19568
|
lastIceState: iceConnectionState,
|
|
19067
19569
|
};
|
|
@@ -19072,9 +19574,6 @@ class ClientEventReporter {
|
|
|
19072
19574
|
peer_connection: role,
|
|
19073
19575
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19074
19576
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19075
|
-
...(pair.userSessionId && {
|
|
19076
|
-
user_session_id: pair.userSessionId,
|
|
19077
|
-
}),
|
|
19078
19577
|
event_type: 'initiated',
|
|
19079
19578
|
});
|
|
19080
19579
|
};
|
|
@@ -19089,9 +19588,6 @@ class ClientEventReporter {
|
|
|
19089
19588
|
peer_connection: role,
|
|
19090
19589
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19091
19590
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19092
|
-
...(pair.userSessionId && {
|
|
19093
|
-
user_session_id: pair.userSessionId,
|
|
19094
|
-
}),
|
|
19095
19591
|
event_type: 'completed',
|
|
19096
19592
|
outcome: 'success',
|
|
19097
19593
|
retry_count_attempt: 0,
|
|
@@ -19109,9 +19605,6 @@ class ClientEventReporter {
|
|
|
19109
19605
|
...this.sessionIdField(cid),
|
|
19110
19606
|
peer_connection: role,
|
|
19111
19607
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19112
|
-
...(pair.userSessionId && {
|
|
19113
|
-
user_session_id: pair.userSessionId,
|
|
19114
|
-
}),
|
|
19115
19608
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19116
19609
|
event_type: 'completed',
|
|
19117
19610
|
outcome: 'failure',
|
|
@@ -19124,7 +19617,6 @@ class ClientEventReporter {
|
|
|
19124
19617
|
this.peerConnectionPairs.delete(key);
|
|
19125
19618
|
};
|
|
19126
19619
|
getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
19127
|
-
getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
19128
19620
|
sessionIdField = (cid) => {
|
|
19129
19621
|
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
19130
19622
|
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
@@ -19136,7 +19628,6 @@ class ClientEventReporter {
|
|
|
19136
19628
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
19137
19629
|
type: ctx?.callType ?? '',
|
|
19138
19630
|
id: ctx?.callId ?? '',
|
|
19139
|
-
call_cid: cid,
|
|
19140
19631
|
stage,
|
|
19141
19632
|
stage_id: pair.sid,
|
|
19142
19633
|
...(pair.joinAttemptIdSnapshot && {
|
|
@@ -19387,6 +19878,7 @@ class StreamVideoClient {
|
|
|
19387
19878
|
}
|
|
19388
19879
|
else {
|
|
19389
19880
|
await call.updateFromRingingEvent(e);
|
|
19881
|
+
await call.get();
|
|
19390
19882
|
}
|
|
19391
19883
|
}
|
|
19392
19884
|
else {
|
|
@@ -19425,6 +19917,13 @@ class StreamVideoClient {
|
|
|
19425
19917
|
this.logger.error(`Failed to init call from event ${e.type}`, err);
|
|
19426
19918
|
}
|
|
19427
19919
|
};
|
|
19920
|
+
/**
|
|
19921
|
+
* Queries the API for calls matching the given filters.
|
|
19922
|
+
* @param data the query data.
|
|
19923
|
+
*/
|
|
19924
|
+
doQueryCalls = (data) => {
|
|
19925
|
+
return this.streamClient.post('/calls', data);
|
|
19926
|
+
};
|
|
19428
19927
|
/**
|
|
19429
19928
|
* Rewatches the given calls with retry logic.
|
|
19430
19929
|
* @param callsToReWatch array of call IDs to rewatch
|
|
@@ -19435,10 +19934,16 @@ class StreamVideoClient {
|
|
|
19435
19934
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
19436
19935
|
try {
|
|
19437
19936
|
this.logger.info(`Rewatching calls ${callsToReWatch.join(', ')} attempt ${attempt + 1}`);
|
|
19438
|
-
await this.
|
|
19937
|
+
const response = await this.doQueryCalls({
|
|
19439
19938
|
watch: true,
|
|
19440
19939
|
filter_conditions: { cid: { $in: callsToReWatch } },
|
|
19441
19940
|
});
|
|
19941
|
+
for (const c of response.calls) {
|
|
19942
|
+
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
19943
|
+
if (call) {
|
|
19944
|
+
call.updateFromCallStateResponse(c);
|
|
19945
|
+
}
|
|
19946
|
+
}
|
|
19442
19947
|
return;
|
|
19443
19948
|
}
|
|
19444
19949
|
catch (err) {
|
|
@@ -19581,9 +20086,11 @@ class StreamVideoClient {
|
|
|
19581
20086
|
* Will query the API for calls matching the given filters.
|
|
19582
20087
|
*
|
|
19583
20088
|
* @param data the query data.
|
|
20089
|
+
* @param opts additional options, for tweaking the API behavior.
|
|
19584
20090
|
*/
|
|
19585
|
-
queryCalls = async (data = {}) => {
|
|
19586
|
-
const
|
|
20091
|
+
queryCalls = async (data = {}, opts = {}) => {
|
|
20092
|
+
const { withDisabledDevices = true } = opts;
|
|
20093
|
+
const response = await this.doQueryCalls(data);
|
|
19587
20094
|
const calls = [];
|
|
19588
20095
|
for (const c of response.calls) {
|
|
19589
20096
|
const call = new Call({
|
|
@@ -19597,7 +20104,10 @@ class StreamVideoClient {
|
|
|
19597
20104
|
clientStore: this.writeableStateStore,
|
|
19598
20105
|
});
|
|
19599
20106
|
call.state.updateFromCallResponse(c.call);
|
|
19600
|
-
await call.applyDeviceConfig(c.call.settings,
|
|
20107
|
+
await call.applyDeviceConfig(c.call.settings, {
|
|
20108
|
+
publish: false,
|
|
20109
|
+
withDisabledDevices,
|
|
20110
|
+
});
|
|
19601
20111
|
if (data.watch) {
|
|
19602
20112
|
await call.setup();
|
|
19603
20113
|
this.writeableStateStore.registerCall(call);
|
|
@@ -19749,5 +20259,519 @@ const humanize = (n) => {
|
|
|
19749
20259
|
return String(n);
|
|
19750
20260
|
};
|
|
19751
20261
|
|
|
19752
|
-
|
|
20262
|
+
/** Default loopback stream wait timeout. */
|
|
20263
|
+
const LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = 10 * 1000;
|
|
20264
|
+
/** Default loopback recording duration. */
|
|
20265
|
+
const DEFAULT_LOOPBACK_RECORDING_DURATION_MS = 10 * 1000;
|
|
20266
|
+
/** Shortest permitted loopback recording duration. */
|
|
20267
|
+
const MIN_LOOPBACK_RECORDING_DURATION_MS = 5 * 1000;
|
|
20268
|
+
/** Longest permitted loopback recording duration. */
|
|
20269
|
+
const MAX_LOOPBACK_RECORDING_DURATION_MS = 2 * 60 * 1000;
|
|
20270
|
+
/** Thrown when loopback tracks do not arrive in time. */
|
|
20271
|
+
class LoopbackStreamsTimeoutError extends Error {
|
|
20272
|
+
constructor() {
|
|
20273
|
+
super('Timed out waiting for loopback streams. Ensure the call was joined ' +
|
|
20274
|
+
'with `allowOwnTracksLoopback: true` and that the SFU is configured ' +
|
|
20275
|
+
'to echo self-sub tracks.');
|
|
20276
|
+
this.name = 'LoopbackStreamsTimeoutError';
|
|
20277
|
+
}
|
|
20278
|
+
}
|
|
20279
|
+
/**
|
|
20280
|
+
* Returns the streams on the given participant that the SFU echoed back to us,
|
|
20281
|
+
* ignoring the local capture streams the publishing path writes to the same
|
|
20282
|
+
* fields.
|
|
20283
|
+
*/
|
|
20284
|
+
const getLoopbackStreams = (call, participant) => {
|
|
20285
|
+
const { subscriber } = call ?? {};
|
|
20286
|
+
const { audioStream, videoStream } = participant ?? {};
|
|
20287
|
+
return {
|
|
20288
|
+
loopbackVideoStream: subscriber?.isSelfSubscribedStream(videoStream)
|
|
20289
|
+
? videoStream
|
|
20290
|
+
: undefined,
|
|
20291
|
+
loopbackAudioStream: subscriber?.isSelfSubscribedStream(audioStream)
|
|
20292
|
+
? audioStream
|
|
20293
|
+
: undefined,
|
|
20294
|
+
};
|
|
20295
|
+
};
|
|
20296
|
+
/**
|
|
20297
|
+
* Returns loopback tracks when all required tracks are present.
|
|
20298
|
+
*
|
|
20299
|
+
* Prefer {@link waitForLoopbackStreams}, which observes call state until the
|
|
20300
|
+
* tracks arrive. This is the synchronous snapshot it is built from.
|
|
20301
|
+
*
|
|
20302
|
+
* @internal
|
|
20303
|
+
*/
|
|
20304
|
+
const getLoopbackTracks = (call, participant, includeVideo) => {
|
|
20305
|
+
if (!participant)
|
|
20306
|
+
return undefined;
|
|
20307
|
+
const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreams(call, participant);
|
|
20308
|
+
const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
|
|
20309
|
+
const videoTrack = includeVideo
|
|
20310
|
+
? loopbackVideoStream?.getVideoTracks()[0]
|
|
20311
|
+
: undefined;
|
|
20312
|
+
if (!audioTrack || (includeVideo && !videoTrack)) {
|
|
20313
|
+
return undefined;
|
|
20314
|
+
}
|
|
20315
|
+
return { audioTrack, videoTrack };
|
|
20316
|
+
};
|
|
20317
|
+
/** Waits for the requested loopback tracks. */
|
|
20318
|
+
const waitForLoopbackStreams = (call, opts) => {
|
|
20319
|
+
const callState = call.state;
|
|
20320
|
+
const { includeVideo, signal, timeoutMs = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, } = opts;
|
|
20321
|
+
return new Promise((resolve, reject) => {
|
|
20322
|
+
const initial = getLoopbackTracks(call, callState.localParticipant, includeVideo);
|
|
20323
|
+
if (initial) {
|
|
20324
|
+
resolve(initial);
|
|
20325
|
+
return;
|
|
20326
|
+
}
|
|
20327
|
+
if (signal.aborted) {
|
|
20328
|
+
resolve(null);
|
|
20329
|
+
return;
|
|
20330
|
+
}
|
|
20331
|
+
let settled = false;
|
|
20332
|
+
const subscriptions = new Subscription();
|
|
20333
|
+
const cleanup = () => {
|
|
20334
|
+
if (settled)
|
|
20335
|
+
return;
|
|
20336
|
+
settled = true;
|
|
20337
|
+
clearTimeout(timeoutId);
|
|
20338
|
+
signal.removeEventListener('abort', onAbort);
|
|
20339
|
+
subscriptions.unsubscribe();
|
|
20340
|
+
};
|
|
20341
|
+
const onAbort = () => {
|
|
20342
|
+
cleanup();
|
|
20343
|
+
resolve(null);
|
|
20344
|
+
};
|
|
20345
|
+
const timeoutId = setTimeout(() => {
|
|
20346
|
+
cleanup();
|
|
20347
|
+
reject(new LoopbackStreamsTimeoutError());
|
|
20348
|
+
}, timeoutMs);
|
|
20349
|
+
signal.addEventListener('abort', onAbort);
|
|
20350
|
+
subscriptions.add(callState.localParticipant$.subscribe((participant) => {
|
|
20351
|
+
const ready = getLoopbackTracks(call, participant, includeVideo);
|
|
20352
|
+
if (ready) {
|
|
20353
|
+
cleanup();
|
|
20354
|
+
resolve(ready);
|
|
20355
|
+
}
|
|
20356
|
+
}));
|
|
20357
|
+
});
|
|
20358
|
+
};
|
|
20359
|
+
/** Enables loopback audio while `fn` runs, then restores it. */
|
|
20360
|
+
const withLoopbackAudioEnabled = async (tracks, fn) => {
|
|
20361
|
+
const restore = enableLoopbackAudio(tracks);
|
|
20362
|
+
try {
|
|
20363
|
+
return await fn();
|
|
20364
|
+
}
|
|
20365
|
+
finally {
|
|
20366
|
+
restore();
|
|
20367
|
+
}
|
|
20368
|
+
};
|
|
20369
|
+
/** Enables loopback audio and returns a restore function. */
|
|
20370
|
+
const enableLoopbackAudio = (tracks) => {
|
|
20371
|
+
const { audioTrack } = tracks;
|
|
20372
|
+
const previous = audioTrack?.enabled ?? false;
|
|
20373
|
+
if (audioTrack) {
|
|
20374
|
+
audioTrack.enabled = true;
|
|
20375
|
+
}
|
|
20376
|
+
return () => {
|
|
20377
|
+
if (audioTrack) {
|
|
20378
|
+
audioTrack.enabled = previous;
|
|
20379
|
+
}
|
|
20380
|
+
};
|
|
20381
|
+
};
|
|
20382
|
+
/** Clamps and rounds a loopback recording duration. */
|
|
20383
|
+
const clampLoopbackRecordingDuration = (durationMs) => Math.round(Math.min(MAX_LOOPBACK_RECORDING_DURATION_MS, Math.max(MIN_LOOPBACK_RECORDING_DURATION_MS, durationMs)));
|
|
20384
|
+
|
|
20385
|
+
/** Single-use `MediaRecorder` wrapper that returns an in-memory blob. */
|
|
20386
|
+
class MediaStreamRecorder {
|
|
20387
|
+
recording = promiseWithResolvers();
|
|
20388
|
+
logger = videoLoggerSystem.getLogger('MediaStreamRecorder');
|
|
20389
|
+
chunks = [];
|
|
20390
|
+
timeoutId;
|
|
20391
|
+
recorder;
|
|
20392
|
+
tracks;
|
|
20393
|
+
maxDurationMs;
|
|
20394
|
+
constructor(stream, options = {}) {
|
|
20395
|
+
if (typeof MediaRecorder === 'undefined') {
|
|
20396
|
+
throw new Error('MediaRecorder is not available in this environment');
|
|
20397
|
+
}
|
|
20398
|
+
this.tracks = stream.getTracks();
|
|
20399
|
+
if (this.tracks.length === 0) {
|
|
20400
|
+
throw new Error('cannot record a MediaStream that has no tracks');
|
|
20401
|
+
}
|
|
20402
|
+
this.maxDurationMs = options.maxDurationMs;
|
|
20403
|
+
this.recorder = new MediaRecorder(new MediaStream(this.tracks));
|
|
20404
|
+
this.recorder.ondataavailable = (event) => {
|
|
20405
|
+
if (event.data?.size)
|
|
20406
|
+
this.chunks.push(event.data);
|
|
20407
|
+
};
|
|
20408
|
+
this.recorder.onstop = this.handleStop;
|
|
20409
|
+
this.recorder.onerror = this.handleError;
|
|
20410
|
+
}
|
|
20411
|
+
/** Starts recording and resolves when recording stops. */
|
|
20412
|
+
start = () => {
|
|
20413
|
+
if (this.isSettled()) {
|
|
20414
|
+
throw new Error('MediaStreamRecorder is single-use: this instance has already ' +
|
|
20415
|
+
'recorded. Construct a new one.');
|
|
20416
|
+
}
|
|
20417
|
+
if (this.recorder.state === 'inactive') {
|
|
20418
|
+
this.tracks.forEach((track) => track.addEventListener('ended', this.stop));
|
|
20419
|
+
if (this.maxDurationMs !== undefined) {
|
|
20420
|
+
this.timeoutId = setTimeout(this.stop, this.maxDurationMs);
|
|
20421
|
+
}
|
|
20422
|
+
this.recorder.start();
|
|
20423
|
+
}
|
|
20424
|
+
return this.recording.promise;
|
|
20425
|
+
};
|
|
20426
|
+
/** Stops recording early. */
|
|
20427
|
+
stop = () => {
|
|
20428
|
+
if (this.recorder.state !== 'inactive')
|
|
20429
|
+
this.recorder.stop();
|
|
20430
|
+
};
|
|
20431
|
+
isSettled = () => this.recording.isResolved() || this.recording.isRejected();
|
|
20432
|
+
detach = () => {
|
|
20433
|
+
clearTimeout(this.timeoutId);
|
|
20434
|
+
this.tracks.forEach((track) => track.removeEventListener('ended', this.stop));
|
|
20435
|
+
};
|
|
20436
|
+
handleStop = () => {
|
|
20437
|
+
this.detach();
|
|
20438
|
+
if (this.chunks.length === 0) {
|
|
20439
|
+
this.logger.warn('MediaRecorder recording produced no data');
|
|
20440
|
+
this.recording.resolve(null);
|
|
20441
|
+
return;
|
|
20442
|
+
}
|
|
20443
|
+
const mimeType = this.chunks[0].type || this.recorder.mimeType;
|
|
20444
|
+
this.recording.resolve({
|
|
20445
|
+
blob: new Blob(this.chunks, { type: mimeType }),
|
|
20446
|
+
});
|
|
20447
|
+
};
|
|
20448
|
+
handleError = (event) => {
|
|
20449
|
+
if (this.isSettled())
|
|
20450
|
+
return;
|
|
20451
|
+
this.detach();
|
|
20452
|
+
const error = event.error;
|
|
20453
|
+
this.logger.error('MediaRecorder error', error);
|
|
20454
|
+
this.recording.reject(error ?? new Error('MediaRecorder failed'));
|
|
20455
|
+
};
|
|
20456
|
+
}
|
|
20457
|
+
|
|
20458
|
+
/**
|
|
20459
|
+
* Tiny, type-safe event emitter.
|
|
20460
|
+
*
|
|
20461
|
+
* Usage styles:
|
|
20462
|
+
* - compose: `private readonly events = new TypedEventEmitter<MyMap>()`
|
|
20463
|
+
* - extend: `class Foo extends TypedEventEmitter<MyMap> {}`
|
|
20464
|
+
*
|
|
20465
|
+
* Listener exceptions (sync throw or rejected promise) are caught and logged,
|
|
20466
|
+
* so one bad listener cannot break dispatch for the rest.
|
|
20467
|
+
*/
|
|
20468
|
+
class TypedEventEmitter {
|
|
20469
|
+
logger;
|
|
20470
|
+
byEvent = new Map();
|
|
20471
|
+
constructor(logger = 'TypedEventEmitter') {
|
|
20472
|
+
this.logger =
|
|
20473
|
+
typeof logger === 'string' ? videoLoggerSystem.getLogger(logger) : logger;
|
|
20474
|
+
}
|
|
20475
|
+
on = (event, fn) => {
|
|
20476
|
+
let listeners = this.byEvent.get(event);
|
|
20477
|
+
if (!listeners) {
|
|
20478
|
+
listeners = new Set();
|
|
20479
|
+
this.byEvent.set(event, listeners);
|
|
20480
|
+
}
|
|
20481
|
+
listeners.add(fn);
|
|
20482
|
+
return () => this.off(event, fn);
|
|
20483
|
+
};
|
|
20484
|
+
off = (event, fn) => {
|
|
20485
|
+
const listeners = this.byEvent.get(event);
|
|
20486
|
+
if (!listeners)
|
|
20487
|
+
return;
|
|
20488
|
+
listeners.delete(fn);
|
|
20489
|
+
if (listeners.size === 0)
|
|
20490
|
+
this.byEvent.delete(event);
|
|
20491
|
+
};
|
|
20492
|
+
emit = (event, payload) => {
|
|
20493
|
+
const listeners = this.byEvent.get(event);
|
|
20494
|
+
if (!listeners || listeners.size === 0)
|
|
20495
|
+
return;
|
|
20496
|
+
for (const listener of [...listeners]) {
|
|
20497
|
+
this.invoke(() => listener(payload), event);
|
|
20498
|
+
}
|
|
20499
|
+
};
|
|
20500
|
+
removeAllListeners = (event) => {
|
|
20501
|
+
if (event === undefined) {
|
|
20502
|
+
this.byEvent.clear();
|
|
20503
|
+
}
|
|
20504
|
+
else {
|
|
20505
|
+
this.byEvent.delete(event);
|
|
20506
|
+
}
|
|
20507
|
+
};
|
|
20508
|
+
invoke = (run, event) => {
|
|
20509
|
+
try {
|
|
20510
|
+
const result = run();
|
|
20511
|
+
if (result && typeof result.then === 'function') {
|
|
20512
|
+
result.catch((err) => {
|
|
20513
|
+
this.logger.warn(`Listener for '${String(event)}' rejected`, err);
|
|
20514
|
+
});
|
|
20515
|
+
}
|
|
20516
|
+
}
|
|
20517
|
+
catch (err) {
|
|
20518
|
+
this.logger.warn(`Listener for '${String(event)}' threw`, err);
|
|
20519
|
+
}
|
|
20520
|
+
};
|
|
20521
|
+
}
|
|
20522
|
+
|
|
20523
|
+
/**
|
|
20524
|
+
* Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
|
|
20525
|
+
* transforms to RTCRtpSenders and RTCRtpReceivers.
|
|
20526
|
+
*/
|
|
20527
|
+
class EncryptionManager extends TypedEventEmitter {
|
|
20528
|
+
algorithm;
|
|
20529
|
+
transform;
|
|
20530
|
+
disposed = false;
|
|
20531
|
+
piped;
|
|
20532
|
+
userId;
|
|
20533
|
+
worker;
|
|
20534
|
+
workerUrl;
|
|
20535
|
+
constructor(userId, worker, workerUrl, algorithm, transform) {
|
|
20536
|
+
super('EncryptionManager');
|
|
20537
|
+
this.userId = userId;
|
|
20538
|
+
this.worker = worker;
|
|
20539
|
+
this.workerUrl = workerUrl;
|
|
20540
|
+
this.algorithm = algorithm;
|
|
20541
|
+
this.transform = transform;
|
|
20542
|
+
this.worker.addEventListener('message', this.handleWorkerMessage);
|
|
20543
|
+
this.worker.addEventListener('error', this.handleWorkerError);
|
|
20544
|
+
}
|
|
20545
|
+
/**
|
|
20546
|
+
* Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
|
|
20547
|
+
* Use it to guard UI, or to avoid calling {@link create} where it would throw.
|
|
20548
|
+
*/
|
|
20549
|
+
static isSupported = () => {
|
|
20550
|
+
return preferredTransform() !== undefined;
|
|
20551
|
+
};
|
|
20552
|
+
/**
|
|
20553
|
+
* Create an EncryptionManager instance and initialize the E2EE Web Worker.
|
|
20554
|
+
*
|
|
20555
|
+
* @param userId - The local user's ID, normally `call.currentUserId`.
|
|
20556
|
+
* @param options - the create options.
|
|
20557
|
+
* @throws {Error} If the browser lacks Encoded Transforms.
|
|
20558
|
+
*
|
|
20559
|
+
* @example
|
|
20560
|
+
* ```ts
|
|
20561
|
+
* if (EncryptionManager.isSupported()) {
|
|
20562
|
+
* const e2ee = await EncryptionManager.create(call.currentUserId);
|
|
20563
|
+
* call.setE2EEManager(e2ee);
|
|
20564
|
+
* e2ee.setSharedKey(0, keyBytes);
|
|
20565
|
+
* }
|
|
20566
|
+
*/
|
|
20567
|
+
static create = async (userId, options) => {
|
|
20568
|
+
const transform = preferredTransform();
|
|
20569
|
+
if (!transform) {
|
|
20570
|
+
throw new Error(`E2EE is not supported in this browser`);
|
|
20571
|
+
}
|
|
20572
|
+
const { e2eeWorker } = await import('./e2ee-worker.browser.es.js');
|
|
20573
|
+
const blob = new Blob([`(${e2eeWorker.toString()})()`], {
|
|
20574
|
+
type: 'application/javascript',
|
|
20575
|
+
});
|
|
20576
|
+
const url = URL.createObjectURL(blob);
|
|
20577
|
+
let worker;
|
|
20578
|
+
try {
|
|
20579
|
+
worker = new Worker(url, { name: 'stream-video-e2ee' });
|
|
20580
|
+
}
|
|
20581
|
+
catch (err) {
|
|
20582
|
+
// e.g. a CSP `worker-src` without `blob:`. Don't leak the object URL.
|
|
20583
|
+
URL.revokeObjectURL(url);
|
|
20584
|
+
throw err;
|
|
20585
|
+
}
|
|
20586
|
+
const algorithm = options?.algorithm ?? 'AES-128-GCM';
|
|
20587
|
+
return new EncryptionManager(userId, worker, url, algorithm, transform);
|
|
20588
|
+
};
|
|
20589
|
+
/**
|
|
20590
|
+
* {@link dispose} terminates the worker, so `postMessage` becomes a silent
|
|
20591
|
+
* no-op and an attached transform points at a dead worker: frames would stall
|
|
20592
|
+
* forever with no error and no event. Throwing is also fail-closed, since a
|
|
20593
|
+
* caller that swallows it still publishes nothing rather than cleartext.
|
|
20594
|
+
*/
|
|
20595
|
+
assertUsable = () => {
|
|
20596
|
+
if (this.disposed)
|
|
20597
|
+
throw new Error(`EncryptionManager is disposed`);
|
|
20598
|
+
};
|
|
20599
|
+
/**
|
|
20600
|
+
* Terminate the worker and release all resources.
|
|
20601
|
+
*
|
|
20602
|
+
* The manager is unusable afterwards and every other method throws. Call
|
|
20603
|
+
* {@link create} for a new one. Safe to call more than once.
|
|
20604
|
+
*/
|
|
20605
|
+
dispose = () => {
|
|
20606
|
+
if (this.disposed)
|
|
20607
|
+
return;
|
|
20608
|
+
this.disposed = true;
|
|
20609
|
+
this.piped = undefined;
|
|
20610
|
+
this.worker.removeEventListener('message', this.handleWorkerMessage);
|
|
20611
|
+
this.worker.removeEventListener('error', this.handleWorkerError);
|
|
20612
|
+
this.worker.terminate();
|
|
20613
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
20614
|
+
this.removeAllListeners();
|
|
20615
|
+
};
|
|
20616
|
+
/**
|
|
20617
|
+
* Set a per-user AES-GCM encryption key in the worker's key store.
|
|
20618
|
+
*
|
|
20619
|
+
* Use it when each participant has their own key from a central authority.
|
|
20620
|
+
* The receiver picks the right one by the `keyIndex` in the frame trailer.
|
|
20621
|
+
*
|
|
20622
|
+
* @param userId - The key owner.
|
|
20623
|
+
* @param keyIndex - Increases with each rotation.
|
|
20624
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20625
|
+
*/
|
|
20626
|
+
setKey = (userId, keyIndex, rawKey) => {
|
|
20627
|
+
this.assertUsable();
|
|
20628
|
+
this.validateKeyIndex(keyIndex);
|
|
20629
|
+
this.validateKeyLength(rawKey);
|
|
20630
|
+
this.worker.postMessage({ type: 'cmd.set_key', userId, keyIndex, rawKey });
|
|
20631
|
+
};
|
|
20632
|
+
/**
|
|
20633
|
+
* Fallback key for any user without a per-user key. The simplest E2EE mode:
|
|
20634
|
+
* one key for everyone, usually passphrase-derived, no distribution needed.
|
|
20635
|
+
* Setting an epoch makes it active for encryption while older epochs remain
|
|
20636
|
+
* available to decrypt in-flight frames until {@link removeSharedKey}.
|
|
20637
|
+
*
|
|
20638
|
+
* @param keyIndex - An integer 0-255, since one trailer byte carries it.
|
|
20639
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20640
|
+
*/
|
|
20641
|
+
setSharedKey = (keyIndex, rawKey) => {
|
|
20642
|
+
this.assertUsable();
|
|
20643
|
+
this.validateKeyIndex(keyIndex);
|
|
20644
|
+
this.validateKeyLength(rawKey);
|
|
20645
|
+
this.worker.postMessage({ type: 'cmd.set_shared_key', keyIndex, rawKey });
|
|
20646
|
+
};
|
|
20647
|
+
/**
|
|
20648
|
+
* Remove one shared-key epoch from the worker's receive key ring.
|
|
20649
|
+
*
|
|
20650
|
+
* If this is the active epoch, shared-key encryption stops until
|
|
20651
|
+
* {@link setSharedKey} succeeds again. An older epoch is not reactivated.
|
|
20652
|
+
*
|
|
20653
|
+
* @param keyIndex - The exact shared-key epoch to remove.
|
|
20654
|
+
*/
|
|
20655
|
+
removeSharedKey = (keyIndex) => {
|
|
20656
|
+
this.assertUsable();
|
|
20657
|
+
this.validateKeyIndex(keyIndex);
|
|
20658
|
+
this.worker.postMessage({ type: 'cmd.remove_shared_key', keyIndex });
|
|
20659
|
+
};
|
|
20660
|
+
/**
|
|
20661
|
+
* Retire one of a user's key epochs, leaving their other epochs usable.
|
|
20662
|
+
*
|
|
20663
|
+
* @param userId - The key owner.
|
|
20664
|
+
* @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
|
|
20665
|
+
*/
|
|
20666
|
+
removeKey = (userId, keyIndex) => {
|
|
20667
|
+
this.assertUsable();
|
|
20668
|
+
this.validateKeyIndex(keyIndex);
|
|
20669
|
+
this.worker.postMessage({ type: 'cmd.remove_key', userId, keyIndex });
|
|
20670
|
+
};
|
|
20671
|
+
/**
|
|
20672
|
+
* Drop every key a user holds, revoking their ability to decrypt later
|
|
20673
|
+
* frames. Call it when a participant leaves.
|
|
20674
|
+
*
|
|
20675
|
+
* To retire one rotated epoch instead, use {@link removeKey}.
|
|
20676
|
+
*/
|
|
20677
|
+
removeAllKeys = (userId) => {
|
|
20678
|
+
this.assertUsable();
|
|
20679
|
+
this.worker.postMessage({ type: 'cmd.remove_all_keys', userId });
|
|
20680
|
+
};
|
|
20681
|
+
/**
|
|
20682
|
+
* Called by the Publisher when it adds a transceiver.
|
|
20683
|
+
*
|
|
20684
|
+
* @param sender - The sender to encrypt.
|
|
20685
|
+
* @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
|
|
20686
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20687
|
+
* @internal
|
|
20688
|
+
*/
|
|
20689
|
+
encrypt = (sender, codec, trackType) => {
|
|
20690
|
+
this.assertUsable();
|
|
20691
|
+
this.pipe(sender, {
|
|
20692
|
+
operation: 'encode',
|
|
20693
|
+
userId: this.userId,
|
|
20694
|
+
codec,
|
|
20695
|
+
trackType,
|
|
20696
|
+
});
|
|
20697
|
+
};
|
|
20698
|
+
/**
|
|
20699
|
+
* Called by the Subscriber when a remote track arrives.
|
|
20700
|
+
*
|
|
20701
|
+
* @param receiver - The receiver to decrypt.
|
|
20702
|
+
* @param userId - The remote user, for key lookup in the worker.
|
|
20703
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20704
|
+
* @internal
|
|
20705
|
+
*/
|
|
20706
|
+
decrypt = (receiver, userId, trackType) => {
|
|
20707
|
+
this.assertUsable();
|
|
20708
|
+
this.pipe(receiver, { operation: 'decode', userId, trackType });
|
|
20709
|
+
};
|
|
20710
|
+
/** Pipe through the worker's transform, tracking targets to avoid double-piping. */
|
|
20711
|
+
pipe = (target, options) => {
|
|
20712
|
+
if (this.transform === 'script') {
|
|
20713
|
+
target.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
20714
|
+
return;
|
|
20715
|
+
}
|
|
20716
|
+
if ((this.piped ??= new WeakSet()).has(target))
|
|
20717
|
+
return;
|
|
20718
|
+
this.piped.add(target);
|
|
20719
|
+
// @ts-expect-error createEncodedStreams is not in the standard typedefs
|
|
20720
|
+
const { readable, writable } = target.createEncodedStreams();
|
|
20721
|
+
this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
|
|
20722
|
+
};
|
|
20723
|
+
/**
|
|
20724
|
+
* Toggle periodic performance reporting from the E2EE worker.
|
|
20725
|
+
*
|
|
20726
|
+
* While on, the worker emits `e2ee.perf_report` once per second with per-track
|
|
20727
|
+
* FPS and crypto timings. Useful for debugging throughput.
|
|
20728
|
+
*/
|
|
20729
|
+
enablePerformanceReporting = (enabled) => {
|
|
20730
|
+
this.assertUsable();
|
|
20731
|
+
this.worker.postMessage({
|
|
20732
|
+
type: 'cmd.enable_performance_reporting',
|
|
20733
|
+
enabled,
|
|
20734
|
+
});
|
|
20735
|
+
};
|
|
20736
|
+
/**
|
|
20737
|
+
* Request a snapshot of the worker's keys. It arrives later as the
|
|
20738
|
+
* `e2ee.key_state` event, listing fingerprints only, never key material.
|
|
20739
|
+
*/
|
|
20740
|
+
requestKeyState = () => {
|
|
20741
|
+
this.assertUsable();
|
|
20742
|
+
this.worker.postMessage({ type: 'cmd.request_key_state' });
|
|
20743
|
+
};
|
|
20744
|
+
handleWorkerMessage = (e) => {
|
|
20745
|
+
const { type, ...payload } = e.data ?? {};
|
|
20746
|
+
if (type === 'e2ee.error') {
|
|
20747
|
+
this.logger.error(e.data.message);
|
|
20748
|
+
return;
|
|
20749
|
+
}
|
|
20750
|
+
const event = type;
|
|
20751
|
+
this.logger.debug('Dispatching', event, payload);
|
|
20752
|
+
this.emit(event, payload);
|
|
20753
|
+
};
|
|
20754
|
+
handleWorkerError = (e) => {
|
|
20755
|
+
this.logger.error('Unhandled worker error:', e.message);
|
|
20756
|
+
};
|
|
20757
|
+
validateKeyLength = (rawKey) => {
|
|
20758
|
+
const is256 = this.algorithm === 'AES-256-GCM';
|
|
20759
|
+
const expected = is256 ? 32 : 16;
|
|
20760
|
+
if (rawKey.byteLength !== expected) {
|
|
20761
|
+
throw new Error(`Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`);
|
|
20762
|
+
}
|
|
20763
|
+
};
|
|
20764
|
+
/**
|
|
20765
|
+
* One trailer byte carries the keyIndex. A larger value would truncate to
|
|
20766
|
+
* `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
|
|
20767
|
+
* every decrypt. Reject it rather than ship a silently broken key epoch.
|
|
20768
|
+
*/
|
|
20769
|
+
validateKeyIndex = (keyIndex) => {
|
|
20770
|
+
if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
|
|
20771
|
+
throw new Error(`keyIndex must be an integer between 0 and 255, got ${keyIndex}`);
|
|
20772
|
+
}
|
|
20773
|
+
};
|
|
20774
|
+
}
|
|
20775
|
+
|
|
20776
|
+
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
|
|
19753
20777
|
//# sourceMappingURL=index.browser.es.js.map
|