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