@stream-io/video-client 1.56.0 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -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 +867 -71
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +911 -70
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.es.js +900 -71
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +52 -6
- package/dist/src/coordinator/connection/connection.d.ts +1 -1
- package/dist/src/devices/CameraManager.d.ts +6 -0
- package/dist/src/devices/DeviceManager.d.ts +1 -0
- package/dist/src/devices/MicrophoneManager.d.ts +6 -0
- 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 +1363 -243
- package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
- package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +4 -0
- package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
- package/dist/src/helpers/ensureExhausted.d.ts +1 -1
- package/dist/src/permissions/PermissionsContext.d.ts +2 -2
- package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
- 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 +3 -0
- package/package.json +11 -11
- package/src/Call.ts +160 -26
- package/src/__tests__/Call.test.ts +15 -0
- package/src/coordinator/connection/connection.ts +6 -3
- package/src/devices/AudioDeviceManager.ts +6 -0
- package/src/devices/CameraManager.ts +51 -4
- package/src/devices/DeviceManager.ts +17 -0
- package/src/devices/MicrophoneManager.ts +55 -5
- package/src/devices/ScreenShareManager.ts +3 -0
- package/src/devices/SpeakerManager.ts +42 -12
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -22
- package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
- package/src/devices/__tests__/mocks.ts +36 -3
- package/src/devices/devicePersistence.ts +1 -2
- package/src/events/__tests__/participant.test.ts +134 -1
- package/src/events/callEventHandlers.ts +9 -3
- package/src/events/participant.ts +42 -7
- package/src/gen/coordinator/index.ts +1384 -244
- package/src/gen/video/sfu/models/models.ts +15 -0
- package/src/helpers/MediaPlaybackWatchdog.ts +35 -16
- package/src/helpers/TypedEventEmitter.ts +71 -0
- package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +137 -8
- package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
- package/src/helpers/__tests__/ViewportTracker.test.ts +1 -2
- package/src/helpers/ensureExhausted.ts +4 -2
- package/src/permissions/PermissionsContext.ts +10 -5
- package/src/reporting/ClientEventReporter.ts +2 -8
- package/src/rtc/BasePeerConnection.ts +12 -1
- package/src/rtc/Publisher.ts +8 -0
- package/src/rtc/Subscriber.ts +5 -0
- package/src/rtc/__tests__/Publisher.test.ts +101 -0
- package/src/rtc/__tests__/Subscriber.test.ts +89 -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 +3 -2
- package/src/stats/rtc/types.ts +8 -2
- package/src/store/CallState.ts +34 -3
- package/src/store/__tests__/CallState.test.ts +82 -1
- package/src/types.ts +24 -9
package/dist/index.cjs.js
CHANGED
|
@@ -87,6 +87,22 @@ const CreateDeviceRequestPushProviderEnum = {
|
|
|
87
87
|
HUAWEI: 'huawei',
|
|
88
88
|
XIAOMI: 'xiaomi',
|
|
89
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* @export
|
|
92
|
+
*/
|
|
93
|
+
const EncryptionSettingsRequestModeEnum = {
|
|
94
|
+
AVAILABLE: 'available',
|
|
95
|
+
DISABLED: 'disabled',
|
|
96
|
+
AUTO_ON: 'auto-on',
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* @export
|
|
100
|
+
*/
|
|
101
|
+
const EncryptionSettingsResponseModeEnum = {
|
|
102
|
+
AVAILABLE: 'available',
|
|
103
|
+
DISABLED: 'disabled',
|
|
104
|
+
AUTO_ON: 'auto-on',
|
|
105
|
+
};
|
|
90
106
|
/**
|
|
91
107
|
* @export
|
|
92
108
|
*/
|
|
@@ -113,6 +129,21 @@ const FrameRecordingSettingsResponseModeEnum = {
|
|
|
113
129
|
DISABLED: 'disabled',
|
|
114
130
|
AUTO_ON: 'auto-on',
|
|
115
131
|
};
|
|
132
|
+
/**
|
|
133
|
+
* @export
|
|
134
|
+
*/
|
|
135
|
+
const HLSSettingsRequestQualityTracksEnum = {
|
|
136
|
+
_360P: '360p',
|
|
137
|
+
_480P: '480p',
|
|
138
|
+
_720P: '720p',
|
|
139
|
+
_1080P: '1080p',
|
|
140
|
+
_1440P: '1440p',
|
|
141
|
+
PORTRAIT_360X640: 'portrait-360x640',
|
|
142
|
+
PORTRAIT_480X854: 'portrait-480x854',
|
|
143
|
+
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
144
|
+
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
145
|
+
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
146
|
+
};
|
|
116
147
|
/**
|
|
117
148
|
* @export
|
|
118
149
|
*/
|
|
@@ -121,6 +152,17 @@ const IndividualRecordingSettingsRequestModeEnum = {
|
|
|
121
152
|
DISABLED: 'disabled',
|
|
122
153
|
AUTO_ON: 'auto-on',
|
|
123
154
|
};
|
|
155
|
+
/**
|
|
156
|
+
* @export
|
|
157
|
+
*/
|
|
158
|
+
const IndividualRecordingSettingsRequestOutputTypesEnum = {
|
|
159
|
+
AUDIO_ONLY: 'audio_only',
|
|
160
|
+
VIDEO_ONLY: 'video_only',
|
|
161
|
+
AUDIO_VIDEO: 'audio_video',
|
|
162
|
+
SCREENSHARE_AUDIO_ONLY: 'screenshare_audio_only',
|
|
163
|
+
SCREENSHARE_VIDEO_ONLY: 'screenshare_video_only',
|
|
164
|
+
SCREENSHARE_AUDIO_VIDEO: 'screenshare_audio_video',
|
|
165
|
+
};
|
|
124
166
|
/**
|
|
125
167
|
* @export
|
|
126
168
|
*/
|
|
@@ -279,6 +321,14 @@ const RecordSettingsRequestQualityEnum = {
|
|
|
279
321
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
280
322
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
281
323
|
};
|
|
324
|
+
/**
|
|
325
|
+
* @export
|
|
326
|
+
*/
|
|
327
|
+
const RequestPermissionRequestPermissionsEnum = {
|
|
328
|
+
SCREENSHARE: 'screenshare',
|
|
329
|
+
SEND_AUDIO: 'send-audio',
|
|
330
|
+
SEND_VIDEO: 'send-video',
|
|
331
|
+
};
|
|
282
332
|
/**
|
|
283
333
|
* @export
|
|
284
334
|
*/
|
|
@@ -479,6 +529,59 @@ const TranscriptionSettingsResponseModeEnum = {
|
|
|
479
529
|
DISABLED: 'disabled',
|
|
480
530
|
AUTO_ON: 'auto-on',
|
|
481
531
|
};
|
|
532
|
+
/**
|
|
533
|
+
* @export
|
|
534
|
+
*/
|
|
535
|
+
const TranslationSettingsLanguagesEnum = {
|
|
536
|
+
EN: 'en',
|
|
537
|
+
FR: 'fr',
|
|
538
|
+
ES: 'es',
|
|
539
|
+
DE: 'de',
|
|
540
|
+
IT: 'it',
|
|
541
|
+
NL: 'nl',
|
|
542
|
+
PT: 'pt',
|
|
543
|
+
PL: 'pl',
|
|
544
|
+
CA: 'ca',
|
|
545
|
+
CS: 'cs',
|
|
546
|
+
DA: 'da',
|
|
547
|
+
EL: 'el',
|
|
548
|
+
FI: 'fi',
|
|
549
|
+
ID: 'id',
|
|
550
|
+
JA: 'ja',
|
|
551
|
+
RU: 'ru',
|
|
552
|
+
SV: 'sv',
|
|
553
|
+
TA: 'ta',
|
|
554
|
+
TH: 'th',
|
|
555
|
+
TR: 'tr',
|
|
556
|
+
HU: 'hu',
|
|
557
|
+
RO: 'ro',
|
|
558
|
+
ZH: 'zh',
|
|
559
|
+
AR: 'ar',
|
|
560
|
+
TL: 'tl',
|
|
561
|
+
HE: 'he',
|
|
562
|
+
HI: 'hi',
|
|
563
|
+
HR: 'hr',
|
|
564
|
+
KO: 'ko',
|
|
565
|
+
MS: 'ms',
|
|
566
|
+
NO: 'no',
|
|
567
|
+
UK: 'uk',
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* @export
|
|
571
|
+
*/
|
|
572
|
+
const UpdateUserPermissionsRequestGrantPermissionsEnum = {
|
|
573
|
+
SCREENSHARE: 'screenshare',
|
|
574
|
+
SEND_AUDIO: 'send-audio',
|
|
575
|
+
SEND_VIDEO: 'send-video',
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* @export
|
|
579
|
+
*/
|
|
580
|
+
const UpdateUserPermissionsRequestRevokePermissionsEnum = {
|
|
581
|
+
SCREENSHARE: 'screenshare',
|
|
582
|
+
SEND_AUDIO: 'send-audio',
|
|
583
|
+
SEND_VIDEO: 'send-video',
|
|
584
|
+
};
|
|
482
585
|
/**
|
|
483
586
|
* @export
|
|
484
587
|
*/
|
|
@@ -495,6 +598,14 @@ const VideoSettingsResponseCameraFacingEnum = {
|
|
|
495
598
|
BACK: 'back',
|
|
496
599
|
EXTERNAL: 'external',
|
|
497
600
|
};
|
|
601
|
+
/**
|
|
602
|
+
* @export
|
|
603
|
+
*/
|
|
604
|
+
const WSAuthMessageProductsEnum = {
|
|
605
|
+
CHAT: 'chat',
|
|
606
|
+
VIDEO: 'video',
|
|
607
|
+
FEEDS: 'feeds',
|
|
608
|
+
};
|
|
498
609
|
|
|
499
610
|
class ErrorFromResponse extends Error {
|
|
500
611
|
code;
|
|
@@ -1492,6 +1603,13 @@ class CallState$Type extends runtime.MessageType {
|
|
|
1492
1603
|
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1493
1604
|
T: () => Pin,
|
|
1494
1605
|
},
|
|
1606
|
+
{
|
|
1607
|
+
no: 5,
|
|
1608
|
+
name: 'e2ee_enabled',
|
|
1609
|
+
kind: 'scalar',
|
|
1610
|
+
jsonName: 'e2eeEnabled',
|
|
1611
|
+
T: 8 /*ScalarType.BOOL*/,
|
|
1612
|
+
},
|
|
1495
1613
|
]);
|
|
1496
1614
|
}
|
|
1497
1615
|
}
|
|
@@ -4614,6 +4732,19 @@ const isSvcCodec = (codecOrMimeType) => {
|
|
|
4614
4732
|
codecOrMimeType === 'video/av1');
|
|
4615
4733
|
};
|
|
4616
4734
|
|
|
4735
|
+
/**
|
|
4736
|
+
* The default engine: a thin, stateless wrapper over the WebRTC globals.
|
|
4737
|
+
*/
|
|
4738
|
+
const defaultGlobalsEngine = {
|
|
4739
|
+
dispose: () => Promise.resolve(true),
|
|
4740
|
+
};
|
|
4741
|
+
const defaultGlobalsEngineProvider = () => defaultGlobalsEngine;
|
|
4742
|
+
let provider = defaultGlobalsEngineProvider;
|
|
4743
|
+
const setCallMediaEngineProvider = (newProvider) => {
|
|
4744
|
+
provider = newProvider;
|
|
4745
|
+
};
|
|
4746
|
+
const getCallMediaEngineProvider = () => provider;
|
|
4747
|
+
|
|
4617
4748
|
const sfuEventKinds = {
|
|
4618
4749
|
subscriberOffer: undefined,
|
|
4619
4750
|
publisherAnswer: undefined,
|
|
@@ -4821,7 +4952,9 @@ const parseUfragFromCandidate = (candidate) => {
|
|
|
4821
4952
|
};
|
|
4822
4953
|
|
|
4823
4954
|
const ensureExhausted = (x, message) => {
|
|
4824
|
-
|
|
4955
|
+
if (message) {
|
|
4956
|
+
videoLoggerSystem.getLogger('helpers').warn(message, x);
|
|
4957
|
+
}
|
|
4825
4958
|
};
|
|
4826
4959
|
|
|
4827
4960
|
/**
|
|
@@ -4946,7 +5079,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
4946
5079
|
}
|
|
4947
5080
|
};
|
|
4948
5081
|
|
|
4949
|
-
const version = "1.
|
|
5082
|
+
const version = "1.58.0";
|
|
4950
5083
|
const [major, minor, patch] = version.split('.');
|
|
4951
5084
|
let sdkInfo = {
|
|
4952
5085
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5929,6 +6062,7 @@ class CallState {
|
|
|
5929
6062
|
settingsSubject = new rxjs.BehaviorSubject(undefined);
|
|
5930
6063
|
transcribingSubject = new rxjs.BehaviorSubject(false);
|
|
5931
6064
|
captioningSubject = new rxjs.BehaviorSubject(false);
|
|
6065
|
+
e2eeEnabledSubject = new rxjs.BehaviorSubject(false);
|
|
5932
6066
|
endedBySubject = new rxjs.BehaviorSubject(undefined);
|
|
5933
6067
|
thumbnailsSubject = new rxjs.BehaviorSubject(undefined);
|
|
5934
6068
|
membersSubject = new rxjs.BehaviorSubject([]);
|
|
@@ -6087,6 +6221,11 @@ class CallState {
|
|
|
6087
6221
|
* Will provide the closed captioning state of this call.
|
|
6088
6222
|
*/
|
|
6089
6223
|
captioning$;
|
|
6224
|
+
/**
|
|
6225
|
+
* Whether end-to-end encryption is active for this call, as reported by the
|
|
6226
|
+
* SFU in the join response.
|
|
6227
|
+
*/
|
|
6228
|
+
e2eeEnabled$;
|
|
6090
6229
|
/**
|
|
6091
6230
|
* Will provide the user who ended this call.
|
|
6092
6231
|
*/
|
|
@@ -6188,6 +6327,7 @@ class CallState {
|
|
|
6188
6327
|
this.rawRecording$ = duc(this.rawRecordingSubject);
|
|
6189
6328
|
this.transcribing$ = duc(this.transcribingSubject);
|
|
6190
6329
|
this.captioning$ = duc(this.captioningSubject);
|
|
6330
|
+
this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
|
|
6191
6331
|
this.eventHandlers = {
|
|
6192
6332
|
// these events are not updating the call state:
|
|
6193
6333
|
'call.frame_recording_ready': undefined,
|
|
@@ -6290,6 +6430,7 @@ class CallState {
|
|
|
6290
6430
|
clearTimeout(taskId);
|
|
6291
6431
|
this.closedCaptionsTasks.delete(ccKey);
|
|
6292
6432
|
}
|
|
6433
|
+
this.removeAllOrphanedTracks();
|
|
6293
6434
|
};
|
|
6294
6435
|
/**
|
|
6295
6436
|
* Sets the list of criteria that are used to sort the participants.
|
|
@@ -6639,6 +6780,12 @@ class CallState {
|
|
|
6639
6780
|
get transcribing() {
|
|
6640
6781
|
return this.getCurrentValue(this.transcribing$);
|
|
6641
6782
|
}
|
|
6783
|
+
/**
|
|
6784
|
+
* Whether end-to-end encryption is active for this call.
|
|
6785
|
+
*/
|
|
6786
|
+
get e2eeEnabled() {
|
|
6787
|
+
return this.getCurrentValue(this.e2eeEnabled$);
|
|
6788
|
+
}
|
|
6642
6789
|
/**
|
|
6643
6790
|
* Will provide the user who ended this call.
|
|
6644
6791
|
*/
|
|
@@ -6859,6 +7006,18 @@ class CallState {
|
|
|
6859
7006
|
removeOrphanedTrack = (id) => {
|
|
6860
7007
|
this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
|
|
6861
7008
|
};
|
|
7009
|
+
/**
|
|
7010
|
+
* Drops every orphaned track. Call this when the peer connections that own
|
|
7011
|
+
* the stored receivers go away (full leave, reconnect, or migration):
|
|
7012
|
+
* `pc.close()` does not raise the track `ended` event, so the per-track
|
|
7013
|
+
* cleanup never fires and the receivers + their closed PCs would otherwise
|
|
7014
|
+
* leak for the call's lifetime.
|
|
7015
|
+
*
|
|
7016
|
+
* @internal
|
|
7017
|
+
*/
|
|
7018
|
+
removeAllOrphanedTracks = () => {
|
|
7019
|
+
this.orphanedTracks = [];
|
|
7020
|
+
};
|
|
6862
7021
|
/**
|
|
6863
7022
|
* Takes all orphaned tracks with the given track lookup prefix.
|
|
6864
7023
|
* All orphaned tracks with the given track lookup prefix are removed from the call state.
|
|
@@ -6921,7 +7080,7 @@ class CallState {
|
|
|
6921
7080
|
* @param reconnectDetails optional reconnect details.
|
|
6922
7081
|
*/
|
|
6923
7082
|
updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
|
|
6924
|
-
const { participants, participantCount, startedAt, pins } = callState;
|
|
7083
|
+
const { participants, participantCount, startedAt, pins, e2EeEnabled } = callState;
|
|
6925
7084
|
const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
|
|
6926
7085
|
this.setParticipants(() => {
|
|
6927
7086
|
const participantLookup = this.getParticipantLookupBySessionId();
|
|
@@ -6947,6 +7106,7 @@ class CallState {
|
|
|
6947
7106
|
this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
6948
7107
|
this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
|
|
6949
7108
|
this.setServerSidePins(pins);
|
|
7109
|
+
this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
|
|
6950
7110
|
};
|
|
6951
7111
|
updateFromMemberRemoved = (event) => {
|
|
6952
7112
|
this.updateFromCallResponse(event.call);
|
|
@@ -7144,6 +7304,37 @@ class CallState {
|
|
|
7144
7304
|
};
|
|
7145
7305
|
}
|
|
7146
7306
|
|
|
7307
|
+
/**
|
|
7308
|
+
* Detection and selection policy for the two WebRTC Encoded Transform APIs.
|
|
7309
|
+
* Internal: which one the SDK attaches is an RTC-layer detail. Consumers want
|
|
7310
|
+
* `EncryptionManager.isSupported` instead.
|
|
7311
|
+
*/
|
|
7312
|
+
/**
|
|
7313
|
+
* Chrome only, and the reason an RTCPeerConnection carrying E2EE needs the
|
|
7314
|
+
* non-standard `encodedInsertableStreams` flag.
|
|
7315
|
+
*/
|
|
7316
|
+
const hasInsertableStreams = () => typeof RTCRtpSender !== 'undefined' &&
|
|
7317
|
+
'createEncodedStreams' in RTCRtpSender.prototype;
|
|
7318
|
+
/** Whether the standard `RTCRtpScriptTransform` API exists. */
|
|
7319
|
+
const hasScriptTransform = () => typeof RTCRtpScriptTransform !== 'undefined';
|
|
7320
|
+
/**
|
|
7321
|
+
* Which Encoded Transform API to attach E2EE with here.
|
|
7322
|
+
*
|
|
7323
|
+
* - `'insertable'`: legacy Insertable Streams. Used on Chrome, where
|
|
7324
|
+
* `RTCRtpScriptTransform` is still unreliable for E2EE.
|
|
7325
|
+
* - `'script'`: the standard API. Used everywhere else.
|
|
7326
|
+
* - `undefined`: neither exists, so E2EE cannot run.
|
|
7327
|
+
*/
|
|
7328
|
+
const preferredTransform = () => {
|
|
7329
|
+
const insertable = hasInsertableStreams();
|
|
7330
|
+
// Chrome's RTCRtpScriptTransform is still unreliable for E2EE.
|
|
7331
|
+
if (isChrome() && insertable)
|
|
7332
|
+
return 'insertable';
|
|
7333
|
+
if (hasScriptTransform())
|
|
7334
|
+
return 'script';
|
|
7335
|
+
return insertable ? 'insertable' : undefined;
|
|
7336
|
+
};
|
|
7337
|
+
|
|
7147
7338
|
/**
|
|
7148
7339
|
* Flatten the stats report into an array of stats objects.
|
|
7149
7340
|
*
|
|
@@ -8252,6 +8443,7 @@ class BasePeerConnection {
|
|
|
8252
8443
|
state;
|
|
8253
8444
|
dispatcher;
|
|
8254
8445
|
clientPublishOptions;
|
|
8446
|
+
e2ee;
|
|
8255
8447
|
tag;
|
|
8256
8448
|
sfuClient;
|
|
8257
8449
|
onReconnectionNeeded;
|
|
@@ -8273,13 +8465,14 @@ class BasePeerConnection {
|
|
|
8273
8465
|
/**
|
|
8274
8466
|
* Constructs a new `BasePeerConnection` instance.
|
|
8275
8467
|
*/
|
|
8276
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8468
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, e2ee, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8277
8469
|
this.peerType = peerType;
|
|
8278
8470
|
this.sfuClient = sfuClient;
|
|
8279
8471
|
this.state = state;
|
|
8280
8472
|
this.dispatcher = dispatcher;
|
|
8281
8473
|
this.iceRestartDelay = iceRestartDelay;
|
|
8282
8474
|
this.clientPublishOptions = clientPublishOptions;
|
|
8475
|
+
this.e2ee = e2ee;
|
|
8283
8476
|
this.tag = tag;
|
|
8284
8477
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8285
8478
|
this.onIceConnected = onIceConnected;
|
|
@@ -8298,7 +8491,13 @@ class BasePeerConnection {
|
|
|
8298
8491
|
}
|
|
8299
8492
|
}
|
|
8300
8493
|
createPeerConnection = (connectionConfig) => {
|
|
8301
|
-
const
|
|
8494
|
+
const config = { ...connectionConfig };
|
|
8495
|
+
// The legacy Insertable Streams path requires this non-standard flag.
|
|
8496
|
+
if (this.e2ee && hasInsertableStreams()) {
|
|
8497
|
+
// @ts-expect-error not part of the standard lib yet
|
|
8498
|
+
config.encodedInsertableStreams = true;
|
|
8499
|
+
}
|
|
8500
|
+
const pc = new RTCPeerConnection(config);
|
|
8302
8501
|
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
8303
8502
|
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
8304
8503
|
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
@@ -9052,6 +9251,10 @@ class Publisher extends BasePeerConnection {
|
|
|
9052
9251
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9053
9252
|
'maintain-framerate';
|
|
9054
9253
|
await transceiver.sender.setParameters(params);
|
|
9254
|
+
if (this.e2ee) {
|
|
9255
|
+
this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
|
|
9256
|
+
this.logger.debug('E2EE encryptor attached to sender');
|
|
9257
|
+
}
|
|
9055
9258
|
await this.negotiate();
|
|
9056
9259
|
};
|
|
9057
9260
|
/**
|
|
@@ -9663,9 +9866,15 @@ class Subscriber extends BasePeerConnection {
|
|
|
9663
9866
|
trackLookupPrefix: trackId,
|
|
9664
9867
|
track: primaryStream,
|
|
9665
9868
|
trackType,
|
|
9869
|
+
receiver: this.e2ee ? e.receiver : undefined,
|
|
9666
9870
|
});
|
|
9667
9871
|
return;
|
|
9668
9872
|
}
|
|
9873
|
+
else if (this.e2ee) {
|
|
9874
|
+
const { userId } = participantToUpdate;
|
|
9875
|
+
this.e2ee.decrypt(e.receiver, userId, TrackType[trackType]);
|
|
9876
|
+
this.logger.debug('E2EE decryptor attached to receiver', userId);
|
|
9877
|
+
}
|
|
9669
9878
|
const streamKindProp = trackTypeToParticipantStreamKey(trackType);
|
|
9670
9879
|
if (!streamKindProp) {
|
|
9671
9880
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
@@ -10637,7 +10846,7 @@ const handleRemoteSoftMute = (call) => {
|
|
|
10637
10846
|
/**
|
|
10638
10847
|
* An event responder which handles the `participantJoined` event.
|
|
10639
10848
|
*/
|
|
10640
|
-
const watchParticipantJoined = (state) => {
|
|
10849
|
+
const watchParticipantJoined = (state, e2ee = undefined) => {
|
|
10641
10850
|
return function onParticipantJoined(e) {
|
|
10642
10851
|
const { participant } = e;
|
|
10643
10852
|
if (!participant)
|
|
@@ -10650,7 +10859,7 @@ const watchParticipantJoined = (state) => {
|
|
|
10650
10859
|
// The SFU would send participant info as part of the `join`
|
|
10651
10860
|
// response and then follow up with a `participantJoined` event for
|
|
10652
10861
|
// already announced participants.
|
|
10653
|
-
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
10862
|
+
const orphanedTracks = reconcileOrphanedTracks(state, participant, e2ee);
|
|
10654
10863
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
10655
10864
|
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
10656
10865
|
viewportVisibilityState: {
|
|
@@ -10686,7 +10895,7 @@ const watchParticipantUpdated = (state) => {
|
|
|
10686
10895
|
* An event responder which handles the `trackPublished` event.
|
|
10687
10896
|
* The SFU will send this event when a participant publishes a track.
|
|
10688
10897
|
*/
|
|
10689
|
-
const watchTrackPublished = (state) => {
|
|
10898
|
+
const watchTrackPublished = (state, e2ee = undefined) => {
|
|
10690
10899
|
return function onTrackPublished(e) {
|
|
10691
10900
|
const { type, sessionId } = e;
|
|
10692
10901
|
// An optimization for large calls.
|
|
@@ -10694,7 +10903,7 @@ const watchTrackPublished = (state) => {
|
|
|
10694
10903
|
// events, and instead, it would only provide the participant's information
|
|
10695
10904
|
// once they start publishing a track.
|
|
10696
10905
|
if (e.participant) {
|
|
10697
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10906
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
|
|
10698
10907
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
10699
10908
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10700
10909
|
}
|
|
@@ -10709,12 +10918,12 @@ const watchTrackPublished = (state) => {
|
|
|
10709
10918
|
* An event responder which handles the `trackUnpublished` event.
|
|
10710
10919
|
* The SFU will send this event when a participant unpublishes a track.
|
|
10711
10920
|
*/
|
|
10712
|
-
const watchTrackUnpublished = (state) => {
|
|
10921
|
+
const watchTrackUnpublished = (state, e2ee = undefined) => {
|
|
10713
10922
|
return function onTrackUnpublished(e) {
|
|
10714
10923
|
const { type, sessionId } = e;
|
|
10715
10924
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
10716
10925
|
if (e.participant) {
|
|
10717
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10926
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
|
|
10718
10927
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
10719
10928
|
state.updateOrAddParticipant(sessionId, participant, (p) => ({
|
|
10720
10929
|
pausedTracks: p.pausedTracks?.filter((t) => t !== type),
|
|
@@ -10733,17 +10942,22 @@ const watchTrackUnpublished = (state) => {
|
|
|
10733
10942
|
*
|
|
10734
10943
|
* @param state the call state.
|
|
10735
10944
|
* @param participant the participant.
|
|
10945
|
+
* @param e2ee accessor for the call's encryption manager, if any.
|
|
10736
10946
|
*/
|
|
10737
|
-
const reconcileOrphanedTracks = (state, participant) => {
|
|
10947
|
+
const reconcileOrphanedTracks = (state, participant, e2ee) => {
|
|
10738
10948
|
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
10739
10949
|
if (!orphanTracks.length)
|
|
10740
10950
|
return;
|
|
10951
|
+
const manager = e2ee?.();
|
|
10741
10952
|
const reconciledTracks = {};
|
|
10742
10953
|
for (const orphan of orphanTracks) {
|
|
10743
10954
|
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
10744
10955
|
if (!key)
|
|
10745
10956
|
continue;
|
|
10746
10957
|
reconciledTracks[key] = orphan.track;
|
|
10958
|
+
if (manager && orphan.receiver) {
|
|
10959
|
+
manager.decrypt(orphan.receiver, participant.userId, TrackType[orphan.trackType]);
|
|
10960
|
+
}
|
|
10747
10961
|
}
|
|
10748
10962
|
return reconciledTracks;
|
|
10749
10963
|
};
|
|
@@ -10799,6 +11013,11 @@ const watchAudioLevelChanged = (dispatcher, state) => {
|
|
|
10799
11013
|
*/
|
|
10800
11014
|
const registerEventHandlers = (call, dispatcher) => {
|
|
10801
11015
|
const state = call.state;
|
|
11016
|
+
// Read lazily on each event: setE2EEManager can run after setup() (an app that
|
|
11017
|
+
// inspects call settings via get()/getOrCreate() before deciding to encrypt has
|
|
11018
|
+
// already triggered it), so a value captured here would be a stale undefined
|
|
11019
|
+
// for the whole call and orphaned tracks would never get a decryptor.
|
|
11020
|
+
const e2ee = () => call.e2eeManager;
|
|
10802
11021
|
const eventHandlers = [
|
|
10803
11022
|
call.on('call.ended', watchCallEnded(call)),
|
|
10804
11023
|
watchSfuCallEnded(call),
|
|
@@ -10806,11 +11025,11 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
10806
11025
|
watchSfuErrorReports(dispatcher),
|
|
10807
11026
|
watchConnectionQualityChanged(dispatcher, state),
|
|
10808
11027
|
watchParticipantCountChanged(dispatcher, state),
|
|
10809
|
-
call.on('participantJoined', watchParticipantJoined(state)),
|
|
11028
|
+
call.on('participantJoined', watchParticipantJoined(state, e2ee)),
|
|
10810
11029
|
call.on('participantLeft', watchParticipantLeft(state)),
|
|
10811
11030
|
call.on('participantUpdated', watchParticipantUpdated(state)),
|
|
10812
|
-
call.on('trackPublished', watchTrackPublished(state)),
|
|
10813
|
-
call.on('trackUnpublished', watchTrackUnpublished(state)),
|
|
11031
|
+
call.on('trackPublished', watchTrackPublished(state, e2ee)),
|
|
11032
|
+
call.on('trackUnpublished', watchTrackUnpublished(state, e2ee)),
|
|
10814
11033
|
watchAudioLevelChanged(dispatcher, state),
|
|
10815
11034
|
watchDominantSpeakerChanged(dispatcher, state),
|
|
10816
11035
|
call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
|
|
@@ -11216,8 +11435,10 @@ class MediaPlaybackWatchdog {
|
|
|
11216
11435
|
tracer;
|
|
11217
11436
|
controller = new AbortController();
|
|
11218
11437
|
pendingTimer;
|
|
11438
|
+
settleTimer;
|
|
11219
11439
|
attempt = 0;
|
|
11220
11440
|
disposed = false;
|
|
11441
|
+
stopped = false;
|
|
11221
11442
|
constructor(opts) {
|
|
11222
11443
|
this.element = opts.element;
|
|
11223
11444
|
this.kind = opts.kind;
|
|
@@ -11238,33 +11459,49 @@ class MediaPlaybackWatchdog {
|
|
|
11238
11459
|
return;
|
|
11239
11460
|
this.disposed = true;
|
|
11240
11461
|
this.controller.abort();
|
|
11462
|
+
this.clearTimers();
|
|
11463
|
+
};
|
|
11464
|
+
clearTimers = () => {
|
|
11241
11465
|
if (this.pendingTimer)
|
|
11242
11466
|
clearTimeout(this.pendingTimer);
|
|
11243
11467
|
this.pendingTimer = undefined;
|
|
11468
|
+
if (this.settleTimer)
|
|
11469
|
+
clearTimeout(this.settleTimer);
|
|
11470
|
+
this.settleTimer = undefined;
|
|
11244
11471
|
};
|
|
11245
11472
|
onPlaying = () => {
|
|
11246
|
-
if (this.attempt > 0) {
|
|
11247
|
-
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11248
|
-
kind: this.kind,
|
|
11249
|
-
attempts: this.attempt,
|
|
11250
|
-
});
|
|
11251
|
-
}
|
|
11252
|
-
this.attempt = 0;
|
|
11253
11473
|
if (this.pendingTimer)
|
|
11254
11474
|
clearTimeout(this.pendingTimer);
|
|
11255
11475
|
this.pendingTimer = undefined;
|
|
11476
|
+
if (this.attempt === 0 || this.settleTimer)
|
|
11477
|
+
return;
|
|
11478
|
+
this.settleTimer ??= setTimeout(this.onSettled, 1000);
|
|
11479
|
+
};
|
|
11480
|
+
onSettled = () => {
|
|
11481
|
+
this.settleTimer = undefined;
|
|
11482
|
+
if (this.element.paused)
|
|
11483
|
+
return;
|
|
11484
|
+
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11485
|
+
kind: this.kind,
|
|
11486
|
+
attempts: this.attempt,
|
|
11487
|
+
});
|
|
11488
|
+
this.attempt = 0;
|
|
11489
|
+
this.stopped = false;
|
|
11256
11490
|
};
|
|
11257
11491
|
onPauseOrSuspend = (event) => {
|
|
11258
|
-
if (this.disposed)
|
|
11492
|
+
if (this.disposed || this.stopped)
|
|
11259
11493
|
return;
|
|
11260
11494
|
this.tracer.trace('mediaPlayback.paused', {
|
|
11261
11495
|
kind: this.kind,
|
|
11262
11496
|
reason: event.type,
|
|
11263
11497
|
});
|
|
11498
|
+
if (this.settleTimer)
|
|
11499
|
+
clearTimeout(this.settleTimer);
|
|
11500
|
+
this.settleTimer = undefined;
|
|
11264
11501
|
this.scheduleRecovery();
|
|
11265
11502
|
};
|
|
11266
11503
|
scheduleRecovery = () => {
|
|
11267
|
-
if (this.disposed || this.pendingTimer)
|
|
11504
|
+
if (this.disposed || this.stopped || this.pendingTimer)
|
|
11268
11505
|
return;
|
|
11269
11506
|
const skipReason = this.computeSkipReason();
|
|
11270
11507
|
if (skipReason) {
|
|
@@ -11274,6 +11511,15 @@ class MediaPlaybackWatchdog {
|
|
|
11274
11511
|
});
|
|
11275
11512
|
return;
|
|
11276
11513
|
}
|
|
11514
|
+
if (this.attempt >= 10) {
|
|
11515
|
+
this.stopped = true;
|
|
11516
|
+
this.clearTimers();
|
|
11517
|
+
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
11518
|
+
kind: this.kind,
|
|
11519
|
+
attempts: this.attempt,
|
|
11520
|
+
});
|
|
11521
|
+
return;
|
|
11522
|
+
}
|
|
11277
11523
|
const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
|
|
11278
11524
|
this.pendingTimer = setTimeout(this.attemptPlay, delay);
|
|
11279
11525
|
};
|
|
@@ -11309,13 +11555,6 @@ class MediaPlaybackWatchdog {
|
|
|
11309
11555
|
if (this.disposed)
|
|
11310
11556
|
return;
|
|
11311
11557
|
this.logger.warn(`Failed to recover ${this.kind} playback`, err);
|
|
11312
|
-
if (this.attempt >= 10) {
|
|
11313
|
-
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
11314
|
-
kind: this.kind,
|
|
11315
|
-
attempts: this.attempt,
|
|
11316
|
-
});
|
|
11317
|
-
return;
|
|
11318
|
-
}
|
|
11319
11558
|
this.scheduleRecovery();
|
|
11320
11559
|
}
|
|
11321
11560
|
};
|
|
@@ -11918,13 +12157,14 @@ class PermissionsContext {
|
|
|
11918
12157
|
return false;
|
|
11919
12158
|
const { audio, video, screensharing } = settings;
|
|
11920
12159
|
switch (permission) {
|
|
11921
|
-
case
|
|
12160
|
+
case RequestPermissionRequestPermissionsEnum.SEND_AUDIO:
|
|
11922
12161
|
return audio.access_request_enabled;
|
|
11923
|
-
case
|
|
12162
|
+
case RequestPermissionRequestPermissionsEnum.SEND_VIDEO:
|
|
11924
12163
|
return video.access_request_enabled;
|
|
11925
|
-
case
|
|
12164
|
+
case RequestPermissionRequestPermissionsEnum.SCREENSHARE:
|
|
11926
12165
|
return screensharing.access_request_enabled;
|
|
11927
12166
|
default:
|
|
12167
|
+
ensureExhausted(permission);
|
|
11928
12168
|
return false;
|
|
11929
12169
|
}
|
|
11930
12170
|
};
|
|
@@ -12931,6 +13171,27 @@ class DeviceManager {
|
|
|
12931
13171
|
this.filters.forEach((entry) => entry.stop?.());
|
|
12932
13172
|
}
|
|
12933
13173
|
}
|
|
13174
|
+
reconcileOptimisticStatus = async () => {
|
|
13175
|
+
const target = this.state.optimisticStatus;
|
|
13176
|
+
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
|
|
13177
|
+
try {
|
|
13178
|
+
if (target === 'enabled' && this.state.status !== 'enabled') {
|
|
13179
|
+
await this.unmuteStream();
|
|
13180
|
+
if (!signal.aborted)
|
|
13181
|
+
this.state.setStatus('enabled');
|
|
13182
|
+
}
|
|
13183
|
+
else if (target === 'disabled' && this.state.status === 'enabled') {
|
|
13184
|
+
// mirror whatever disable() does to stop/pause the track per disableMode
|
|
13185
|
+
if (!signal.aborted)
|
|
13186
|
+
this.state.setStatus('disabled');
|
|
13187
|
+
}
|
|
13188
|
+
}
|
|
13189
|
+
finally {
|
|
13190
|
+
if (!signal.aborted)
|
|
13191
|
+
this.state.setPendingStatus(this.state.status);
|
|
13192
|
+
}
|
|
13193
|
+
});
|
|
13194
|
+
};
|
|
12934
13195
|
disableTracks() {
|
|
12935
13196
|
this.getTracks().forEach((track) => {
|
|
12936
13197
|
if (track.enabled)
|
|
@@ -13564,6 +13825,33 @@ class CameraManager extends DeviceManager {
|
|
|
13564
13825
|
}
|
|
13565
13826
|
}
|
|
13566
13827
|
}
|
|
13828
|
+
enable() {
|
|
13829
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13830
|
+
this.state.setPendingStatus('enabled');
|
|
13831
|
+
return Promise.resolve();
|
|
13832
|
+
}
|
|
13833
|
+
return super.enable();
|
|
13834
|
+
}
|
|
13835
|
+
async disable(forceStopOrOptions) {
|
|
13836
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13837
|
+
this.state.setPendingStatus('disabled');
|
|
13838
|
+
return;
|
|
13839
|
+
}
|
|
13840
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
13841
|
+
if (forceStopOrOptions === undefined)
|
|
13842
|
+
return super.disable();
|
|
13843
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
13844
|
+
return super.disable(forceStopOrOptions);
|
|
13845
|
+
}
|
|
13846
|
+
return super.disable(forceStopOrOptions);
|
|
13847
|
+
}
|
|
13848
|
+
toggle() {
|
|
13849
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13850
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
13851
|
+
return Promise.resolve();
|
|
13852
|
+
}
|
|
13853
|
+
return super.toggle();
|
|
13854
|
+
}
|
|
13567
13855
|
/**
|
|
13568
13856
|
* Applies the video settings to the camera.
|
|
13569
13857
|
*
|
|
@@ -13597,9 +13885,16 @@ class CameraManager extends DeviceManager {
|
|
|
13597
13885
|
await this.enable();
|
|
13598
13886
|
}
|
|
13599
13887
|
}
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13888
|
+
if (isReactNative() && publish && canPublish) {
|
|
13889
|
+
// On RN the camera is enabled/disabled optimistically before JOINED. Reconcile now
|
|
13890
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
13891
|
+
await this.reconcileOptimisticStatus();
|
|
13892
|
+
}
|
|
13893
|
+
else {
|
|
13894
|
+
const { mediaStream } = this.state;
|
|
13895
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
13896
|
+
await this.publishStream(mediaStream);
|
|
13897
|
+
}
|
|
13603
13898
|
}
|
|
13604
13899
|
}
|
|
13605
13900
|
getDevices() {
|
|
@@ -13618,7 +13913,10 @@ class CameraManager extends DeviceManager {
|
|
|
13618
13913
|
}
|
|
13619
13914
|
return constraints;
|
|
13620
13915
|
}
|
|
13621
|
-
getStream(constraints) {
|
|
13916
|
+
async getStream(constraints) {
|
|
13917
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
13918
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
13919
|
+
await this.call.ensureMediaFactory();
|
|
13622
13920
|
return getVideoStream(constraints, this.call.tracer);
|
|
13623
13921
|
}
|
|
13624
13922
|
}
|
|
@@ -13634,6 +13932,9 @@ class AudioDeviceManager extends DeviceManager {
|
|
|
13634
13932
|
if (!this.call.state.settings?.audio.hifi_audio_enabled) {
|
|
13635
13933
|
throw new Error('High Fidelity audio is not enabled for this call');
|
|
13636
13934
|
}
|
|
13935
|
+
if (isReactNative() && this.call.hasMediaEngine) {
|
|
13936
|
+
throw new Error('setAudioBitrateProfile must be called before joining the call.');
|
|
13937
|
+
}
|
|
13637
13938
|
this.doSetAudioBitrateProfile(profile);
|
|
13638
13939
|
this.state.setAudioBitrateProfile(profile);
|
|
13639
13940
|
if (this.enabled) {
|
|
@@ -13906,7 +14207,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
13906
14207
|
]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
|
|
13907
14208
|
try {
|
|
13908
14209
|
if (callingState === exports.CallingState.LEFT) {
|
|
13909
|
-
|
|
14210
|
+
// The muted-recording-prepared mode is reset in `callManager.stop()`
|
|
14211
|
+
// (during leave, while the call factory is still alive), not here —
|
|
14212
|
+
// this subscription fires asynchronously and could land after the
|
|
14213
|
+
// factory is disposed, forcing a default-ADM rebuild.
|
|
13910
14214
|
await this.stopSpeakingWhileMutedDetection();
|
|
13911
14215
|
}
|
|
13912
14216
|
if (callingState !== exports.CallingState.JOINED)
|
|
@@ -14006,6 +14310,33 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14006
14310
|
this.subscriptions.push(unsubscribe);
|
|
14007
14311
|
}
|
|
14008
14312
|
}
|
|
14313
|
+
enable() {
|
|
14314
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14315
|
+
this.state.setPendingStatus('enabled');
|
|
14316
|
+
return Promise.resolve();
|
|
14317
|
+
}
|
|
14318
|
+
return super.enable();
|
|
14319
|
+
}
|
|
14320
|
+
async disable(forceStopOrOptions) {
|
|
14321
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14322
|
+
this.state.setPendingStatus('disabled');
|
|
14323
|
+
return;
|
|
14324
|
+
}
|
|
14325
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
14326
|
+
if (forceStopOrOptions === undefined)
|
|
14327
|
+
return super.disable();
|
|
14328
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
14329
|
+
return super.disable(forceStopOrOptions);
|
|
14330
|
+
}
|
|
14331
|
+
return super.disable(forceStopOrOptions);
|
|
14332
|
+
}
|
|
14333
|
+
toggle() {
|
|
14334
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14335
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
14336
|
+
return Promise.resolve();
|
|
14337
|
+
}
|
|
14338
|
+
return super.toggle();
|
|
14339
|
+
}
|
|
14009
14340
|
/**
|
|
14010
14341
|
* Enables noise cancellation for the microphone.
|
|
14011
14342
|
*
|
|
@@ -14166,15 +14497,25 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14166
14497
|
await this.enable();
|
|
14167
14498
|
}
|
|
14168
14499
|
}
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14500
|
+
if (isReactNative() && publish && canPublish) {
|
|
14501
|
+
// On RN the microphone is enabled/disabled optimistically before JOINED. Reconcile now
|
|
14502
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
14503
|
+
await this.reconcileOptimisticStatus();
|
|
14504
|
+
}
|
|
14505
|
+
else {
|
|
14506
|
+
const { mediaStream } = this.state;
|
|
14507
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
14508
|
+
await this.publishStream(mediaStream);
|
|
14509
|
+
}
|
|
14172
14510
|
}
|
|
14173
14511
|
}
|
|
14174
14512
|
getDevices() {
|
|
14175
14513
|
return getAudioDevices(this.call.tracer);
|
|
14176
14514
|
}
|
|
14177
|
-
getStream(constraints) {
|
|
14515
|
+
async getStream(constraints) {
|
|
14516
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14517
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14518
|
+
await this.call.ensureMediaFactory();
|
|
14178
14519
|
return getAudioStream(constraints, this.call.tracer);
|
|
14179
14520
|
}
|
|
14180
14521
|
doSetAudioBitrateProfile(profile) {
|
|
@@ -14383,6 +14724,9 @@ class ScreenShareManager extends AudioDeviceManager {
|
|
|
14383
14724
|
if (!this.state.audioEnabled) {
|
|
14384
14725
|
constraints.audio = false;
|
|
14385
14726
|
}
|
|
14727
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14728
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14729
|
+
await this.call.ensureMediaFactory();
|
|
14386
14730
|
const stream = await getScreenShareStream(constraints, this.call.tracer);
|
|
14387
14731
|
const [track] = stream.getVideoTracks();
|
|
14388
14732
|
const { contentHint } = this.state.settings || {};
|
|
@@ -14487,10 +14831,14 @@ class SpeakerManager {
|
|
|
14487
14831
|
this.devicePersistence = devicePreferences;
|
|
14488
14832
|
this.setup();
|
|
14489
14833
|
}
|
|
14490
|
-
apply(settings) {
|
|
14491
|
-
|
|
14834
|
+
async apply(settings) {
|
|
14835
|
+
if (isReactNative()) {
|
|
14836
|
+
this.applyRN(settings);
|
|
14837
|
+
return;
|
|
14838
|
+
}
|
|
14839
|
+
await this.applyWeb();
|
|
14492
14840
|
}
|
|
14493
|
-
applyWeb() {
|
|
14841
|
+
async applyWeb() {
|
|
14494
14842
|
const { enabled, storageKey } = this.devicePersistence;
|
|
14495
14843
|
if (!enabled)
|
|
14496
14844
|
return;
|
|
@@ -14502,8 +14850,22 @@ class SpeakerManager {
|
|
|
14502
14850
|
const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
|
|
14503
14851
|
? ''
|
|
14504
14852
|
: preference.selectedDeviceId;
|
|
14505
|
-
if (
|
|
14506
|
-
this.
|
|
14853
|
+
if (!nextDeviceId) {
|
|
14854
|
+
if (this.state.selectedDevice !== nextDeviceId) {
|
|
14855
|
+
this.select(nextDeviceId);
|
|
14856
|
+
}
|
|
14857
|
+
return;
|
|
14858
|
+
}
|
|
14859
|
+
const permissionState = await rxjs.firstValueFrom(getAudioBrowserPermission(this.call.tracer).asStateObservable());
|
|
14860
|
+
if (permissionState !== 'granted')
|
|
14861
|
+
return;
|
|
14862
|
+
const devices = await rxjs.firstValueFrom(this.listDevices());
|
|
14863
|
+
const device = this.findDevice(devices, nextDeviceId) ??
|
|
14864
|
+
(preference.selectedDeviceLabel
|
|
14865
|
+
? devices.find((d) => d.label === preference.selectedDeviceLabel)
|
|
14866
|
+
: undefined);
|
|
14867
|
+
if (device && this.state.selectedDevice !== device.deviceId) {
|
|
14868
|
+
this.select(device.deviceId);
|
|
14507
14869
|
}
|
|
14508
14870
|
}
|
|
14509
14871
|
applyRN(settings) {
|
|
@@ -14532,6 +14894,7 @@ class SpeakerManager {
|
|
|
14532
14894
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
14533
14895
|
defaultDevice,
|
|
14534
14896
|
isRingingTypeCall: this.call.ringing,
|
|
14897
|
+
cid: this.call.cid,
|
|
14535
14898
|
});
|
|
14536
14899
|
}
|
|
14537
14900
|
}
|
|
@@ -14540,11 +14903,15 @@ class SpeakerManager {
|
|
|
14540
14903
|
return;
|
|
14541
14904
|
this.areSubscriptionsSetUp = true;
|
|
14542
14905
|
if (deviceIds$ && !isReactNative()) {
|
|
14543
|
-
this.subscriptions.push(createSubscription(rxjs.combineLatest([
|
|
14906
|
+
this.subscriptions.push(createSubscription(rxjs.combineLatest([
|
|
14907
|
+
deviceIds$.pipe(rxjs.pairwise()),
|
|
14908
|
+
this.state.selectedDevice$,
|
|
14909
|
+
]), ([[prevDevices, currentDevices], deviceId]) => {
|
|
14544
14910
|
if (!deviceId)
|
|
14545
14911
|
return;
|
|
14546
|
-
const
|
|
14547
|
-
|
|
14912
|
+
const isDisconnected = this.findDevice(prevDevices, deviceId) &&
|
|
14913
|
+
!this.findDevice(currentDevices, deviceId);
|
|
14914
|
+
if (isDisconnected)
|
|
14548
14915
|
this.select('');
|
|
14549
14916
|
}));
|
|
14550
14917
|
}
|
|
@@ -14591,6 +14958,7 @@ class SpeakerManager {
|
|
|
14591
14958
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
14592
14959
|
this.subscriptions = [];
|
|
14593
14960
|
this.areSubscriptionsSetUp = false;
|
|
14961
|
+
this.defaultDevice = undefined;
|
|
14594
14962
|
};
|
|
14595
14963
|
/**
|
|
14596
14964
|
* Set the volume of the audio elements
|
|
@@ -14625,6 +14993,7 @@ class SpeakerManager {
|
|
|
14625
14993
|
return { audioVolume: volume };
|
|
14626
14994
|
});
|
|
14627
14995
|
}
|
|
14996
|
+
findDevice = (devices, deviceId) => devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
|
|
14628
14997
|
persistSpeakerDevicePreference(selectedDevice) {
|
|
14629
14998
|
const { storageKey } = this.devicePersistence;
|
|
14630
14999
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
@@ -14707,6 +15076,7 @@ class Call {
|
|
|
14707
15076
|
blockedAudioTracker;
|
|
14708
15077
|
subscriber;
|
|
14709
15078
|
publisher;
|
|
15079
|
+
e2eeManager;
|
|
14710
15080
|
/**
|
|
14711
15081
|
* Flag telling whether this call is a "ringing" call.
|
|
14712
15082
|
*/
|
|
@@ -14762,6 +15132,8 @@ class Call {
|
|
|
14762
15132
|
allowOwnTracksLoopback = false;
|
|
14763
15133
|
hasJoinedOnce = false;
|
|
14764
15134
|
deviceSettingsAppliedOnce = false;
|
|
15135
|
+
callManagerStarted = false;
|
|
15136
|
+
leaveGeneration = 0;
|
|
14765
15137
|
credentials;
|
|
14766
15138
|
initialized = false;
|
|
14767
15139
|
acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
|
|
@@ -14780,6 +15152,13 @@ class Call {
|
|
|
14780
15152
|
clientCapabilities = new Set([
|
|
14781
15153
|
ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
|
|
14782
15154
|
]);
|
|
15155
|
+
/**
|
|
15156
|
+
* The in-flight per-call media engine. On web/React this resolves to a thin
|
|
15157
|
+
* globals-backed engine (no provider registered); React Native registers a
|
|
15158
|
+
* provider that owns a per-call native factory.
|
|
15159
|
+
* @internal
|
|
15160
|
+
*/
|
|
15161
|
+
mediaEnginePromise;
|
|
14783
15162
|
/**
|
|
14784
15163
|
* Constructs a new `Call` instance.
|
|
14785
15164
|
*
|
|
@@ -15049,6 +15428,7 @@ class Call {
|
|
|
15049
15428
|
if (callingState === exports.CallingState.LEFT) {
|
|
15050
15429
|
return;
|
|
15051
15430
|
}
|
|
15431
|
+
this.leaveGeneration += 1;
|
|
15052
15432
|
if (callingState === exports.CallingState.JOINING) {
|
|
15053
15433
|
const waitUntilCallJoined = () => {
|
|
15054
15434
|
return new Promise((resolve) => {
|
|
@@ -15135,12 +15515,14 @@ class Call {
|
|
|
15135
15515
|
this.clientStore.unregisterCall(this);
|
|
15136
15516
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15137
15517
|
isRingingTypeCall: this.ringing,
|
|
15518
|
+
shouldStopCallManager: this.callManagerStarted,
|
|
15138
15519
|
});
|
|
15139
15520
|
this.camera.dispose();
|
|
15140
15521
|
this.microphone.dispose();
|
|
15141
15522
|
this.screenShare.dispose();
|
|
15142
15523
|
this.speaker.dispose();
|
|
15143
15524
|
this.deviceSettingsAppliedOnce = false;
|
|
15525
|
+
this.callManagerStarted = false;
|
|
15144
15526
|
const stopOnLeavePromises = [];
|
|
15145
15527
|
if (this.camera.stopOnLeave) {
|
|
15146
15528
|
stopOnLeavePromises.push(this.camera.disable(true));
|
|
@@ -15152,6 +15534,22 @@ class Call {
|
|
|
15152
15534
|
stopOnLeavePromises.push(this.screenShare.disable(true));
|
|
15153
15535
|
}
|
|
15154
15536
|
await Promise.all(stopOnLeavePromises);
|
|
15537
|
+
// Dispose the per-call media engine last — after peer connections and
|
|
15538
|
+
// local tracks are gone — so the backing factory tears down with no
|
|
15539
|
+
// owned PCs/tracks. A fresh `join()` builds a new engine.
|
|
15540
|
+
if (this.mediaEnginePromise) {
|
|
15541
|
+
const enginePromise = this.mediaEnginePromise;
|
|
15542
|
+
this.mediaEnginePromise = undefined;
|
|
15543
|
+
this.logger.debug('Disposing per-call media factory');
|
|
15544
|
+
await enginePromise
|
|
15545
|
+
.then((engine) => {
|
|
15546
|
+
globalThis.streamRNVideoSDK?.callingX?.unwireAudioEngineSubscription();
|
|
15547
|
+
return engine.dispose();
|
|
15548
|
+
})
|
|
15549
|
+
.catch((err) => {
|
|
15550
|
+
this.logger.warn('Failed to dispose media engine', err);
|
|
15551
|
+
});
|
|
15552
|
+
}
|
|
15155
15553
|
});
|
|
15156
15554
|
};
|
|
15157
15555
|
/**
|
|
@@ -15252,11 +15650,7 @@ class Call {
|
|
|
15252
15650
|
this.clientStore.registerOrUpdateCall(this);
|
|
15253
15651
|
}
|
|
15254
15652
|
// Skip speaker setup on RN if ringing was requested or the call is already ringing
|
|
15255
|
-
const skipSpeakerApply = isReactNative()
|
|
15256
|
-
? params?.ring === true
|
|
15257
|
-
? true
|
|
15258
|
-
: this.ringing
|
|
15259
|
-
: false;
|
|
15653
|
+
const skipSpeakerApply = isReactNative();
|
|
15260
15654
|
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15261
15655
|
return response;
|
|
15262
15656
|
};
|
|
@@ -15279,11 +15673,7 @@ class Call {
|
|
|
15279
15673
|
this.clientStore.registerOrUpdateCall(this);
|
|
15280
15674
|
}
|
|
15281
15675
|
// Skip speaker setup on RN if ringing was requested or the call is already ringing
|
|
15282
|
-
const skipSpeakerApply = isReactNative()
|
|
15283
|
-
? data?.ring === true
|
|
15284
|
-
? true
|
|
15285
|
-
: this.ringing
|
|
15286
|
-
: false;
|
|
15676
|
+
const skipSpeakerApply = isReactNative();
|
|
15287
15677
|
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15288
15678
|
return response;
|
|
15289
15679
|
};
|
|
@@ -15432,9 +15822,20 @@ class Call {
|
|
|
15432
15822
|
doJoin = async (data) => {
|
|
15433
15823
|
const connectStartTime = Date.now();
|
|
15434
15824
|
const callingState = this.state.callingState;
|
|
15825
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
15826
|
+
const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
|
|
15435
15827
|
this.joinCallData = data;
|
|
15436
15828
|
this.logger.debug('Starting join flow');
|
|
15437
15829
|
this.state.setCallingState(exports.CallingState.JOINING);
|
|
15830
|
+
// Ensure the per-call media engine exists before any peer connection
|
|
15831
|
+
// (codec probe, subscriber, publisher) or capture happens, so the WebRTC
|
|
15832
|
+
// globals resolve to the call's factory. Idempotent across
|
|
15833
|
+
// reconnect/migration attempts.
|
|
15834
|
+
await this.ensureMediaFactory();
|
|
15835
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
15836
|
+
if (callingX) {
|
|
15837
|
+
callingX.wireAudioEngineSubscription();
|
|
15838
|
+
}
|
|
15438
15839
|
const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
15439
15840
|
const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
15440
15841
|
const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
@@ -15489,6 +15890,11 @@ class Call {
|
|
|
15489
15890
|
// these are throw-away SDPs that the SFU will use to determine
|
|
15490
15891
|
// the capabilities of the client (codec support, etc.)
|
|
15491
15892
|
const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
15893
|
+
// skip if a leave superseded this join so codec detection doesn't resolve to a default factory.
|
|
15894
|
+
if (supersededByLeave()) {
|
|
15895
|
+
this.logger.debug('Join superseded by leave; skipping codec detection');
|
|
15896
|
+
return;
|
|
15897
|
+
}
|
|
15492
15898
|
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
15493
15899
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
15494
15900
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
@@ -15532,6 +15938,12 @@ class Call {
|
|
|
15532
15938
|
throw error;
|
|
15533
15939
|
}
|
|
15534
15940
|
}
|
|
15941
|
+
// If the user left while this join was in flight, bail before re-setting JOINED and before
|
|
15942
|
+
// peer-connection setup below (both run synchronously after this, so one check covers them).
|
|
15943
|
+
if (supersededByLeave()) {
|
|
15944
|
+
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
15945
|
+
return;
|
|
15946
|
+
}
|
|
15535
15947
|
if (!performingMigration) {
|
|
15536
15948
|
// in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
|
|
15537
15949
|
this.state.setCallingState(exports.CallingState.JOINED);
|
|
@@ -15569,12 +15981,18 @@ class Call {
|
|
|
15569
15981
|
}
|
|
15570
15982
|
// device settings should be applied only once, we don't have to
|
|
15571
15983
|
// re-apply them on later reconnections or server-side data fetches
|
|
15572
|
-
if (!this.deviceSettingsAppliedOnce &&
|
|
15984
|
+
if (!this.deviceSettingsAppliedOnce &&
|
|
15985
|
+
this.state.settings &&
|
|
15986
|
+
!supersededByLeave()) {
|
|
15573
15987
|
await this.applyDeviceConfig(this.state.settings, true, false);
|
|
15988
|
+
this.deviceSettingsAppliedOnce = true;
|
|
15989
|
+
}
|
|
15990
|
+
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
15574
15991
|
globalThis.streamRNVideoSDK?.callManager.start({
|
|
15575
15992
|
isRingingTypeCall: this.ringing,
|
|
15993
|
+
cid: this.cid,
|
|
15576
15994
|
});
|
|
15577
|
-
this.
|
|
15995
|
+
this.callManagerStarted = true;
|
|
15578
15996
|
}
|
|
15579
15997
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
15580
15998
|
// as it's a one-time event and clashes with the potential reconnection attempts.
|
|
@@ -15701,6 +16119,7 @@ class Call {
|
|
|
15701
16119
|
this.sfuStatsReporter = undefined;
|
|
15702
16120
|
if (closePreviousInstances && this.subscriber) {
|
|
15703
16121
|
await this.subscriber.dispose();
|
|
16122
|
+
this.state.removeAllOrphanedTracks();
|
|
15704
16123
|
}
|
|
15705
16124
|
const basePeerConnectionOptions = {
|
|
15706
16125
|
sfuClient,
|
|
@@ -15711,6 +16130,7 @@ class Call {
|
|
|
15711
16130
|
enableTracing,
|
|
15712
16131
|
statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
|
|
15713
16132
|
clientPublishOptions: this.clientPublishOptions,
|
|
16133
|
+
e2ee: this.e2eeManager,
|
|
15714
16134
|
onReconnectionNeeded: (kind, reason, peerType) => {
|
|
15715
16135
|
this.reconnect(kind, reason).catch((err) => {
|
|
15716
16136
|
const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
|
|
@@ -15776,12 +16196,12 @@ class Call {
|
|
|
15776
16196
|
* Retrieves credentials for joining the call.
|
|
15777
16197
|
*
|
|
15778
16198
|
* @internal
|
|
15779
|
-
*
|
|
15780
16199
|
* @param data the join call data.
|
|
15781
16200
|
*/
|
|
15782
16201
|
doJoinRequest = async (data) => {
|
|
15783
16202
|
const location = await this.streamClient.getLocationHint();
|
|
15784
|
-
const
|
|
16203
|
+
const e2ee = !!this.e2eeManager;
|
|
16204
|
+
const request = { ...data, location, e2ee };
|
|
15785
16205
|
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
15786
16206
|
this.state.updateFromCallResponse(joinResponse.call);
|
|
15787
16207
|
this.state.setMembers(joinResponse.members);
|
|
@@ -15800,6 +16220,36 @@ class Call {
|
|
|
15800
16220
|
}
|
|
15801
16221
|
return joinResponse;
|
|
15802
16222
|
};
|
|
16223
|
+
/**
|
|
16224
|
+
* Whether the per-call media engine currently exists. True from join until leave.
|
|
16225
|
+
*
|
|
16226
|
+
* @internal an internal getter and should not be used outside the SDK.
|
|
16227
|
+
*/
|
|
16228
|
+
get hasMediaEngine() {
|
|
16229
|
+
return !!this.mediaEnginePromise;
|
|
16230
|
+
}
|
|
16231
|
+
/**
|
|
16232
|
+
* Ensures a {@link CallMediaEngine} exists for this call's media session and
|
|
16233
|
+
* returns it. Idempotent: the engine is created once via the registered
|
|
16234
|
+
* provider (see `setCallMediaEngineProvider`) and cached until `leave()`
|
|
16235
|
+
* disposes it. Concurrent callers (e.g. camera + microphone enabling in
|
|
16236
|
+
* parallel) share the same engine because the in-flight creation promise is
|
|
16237
|
+
* cached, never the unresolved result.
|
|
16238
|
+
*
|
|
16239
|
+
* @internal
|
|
16240
|
+
*/
|
|
16241
|
+
ensureMediaFactory = async () => {
|
|
16242
|
+
if (!this.mediaEnginePromise) {
|
|
16243
|
+
const provider = getCallMediaEngineProvider();
|
|
16244
|
+
this.logger.debug(`Requesting per-call media factory creation`);
|
|
16245
|
+
this.mediaEnginePromise = Promise.resolve(provider()).catch((err) => {
|
|
16246
|
+
// Drop the cached rejection so a retried join() can rebuild the engine
|
|
16247
|
+
this.mediaEnginePromise = undefined;
|
|
16248
|
+
throw err;
|
|
16249
|
+
});
|
|
16250
|
+
}
|
|
16251
|
+
return this.mediaEnginePromise;
|
|
16252
|
+
};
|
|
15803
16253
|
/**
|
|
15804
16254
|
* Handles the closing of the SFU signal connection.
|
|
15805
16255
|
*
|
|
@@ -16361,6 +16811,30 @@ class Call {
|
|
|
16361
16811
|
this.tracer.trace('updatePublishOptions', options);
|
|
16362
16812
|
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
16363
16813
|
};
|
|
16814
|
+
/**
|
|
16815
|
+
* Set the E2EE (end-to-end encryption) manager for this call.
|
|
16816
|
+
*
|
|
16817
|
+
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
16818
|
+
* configured for E2EE.
|
|
16819
|
+
*
|
|
16820
|
+
* The manager is kept across {@link leave} so a rejoin of this same instance
|
|
16821
|
+
* stays encrypted: do not dispose it while this call may be joined again.
|
|
16822
|
+
* A disposed manager throws from `encrypt`/`decrypt` rather than silently
|
|
16823
|
+
* publishing nothing, so re-attach a fresh one instead of reusing it.
|
|
16824
|
+
*
|
|
16825
|
+
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
16826
|
+
* built-in AES-GCM scheme, or pass your own implementation.
|
|
16827
|
+
* @throws if called after the peer connections have been built (i.e. after
|
|
16828
|
+
* `join`): those PCs were already configured without an encryptor, so
|
|
16829
|
+
* adopting a manager now would silently publish/receive cleartext for
|
|
16830
|
+
* the live session.
|
|
16831
|
+
*/
|
|
16832
|
+
setE2EEManager = (e2ee) => {
|
|
16833
|
+
if (this.publisher || this.subscriber) {
|
|
16834
|
+
throw new Error('setE2EEManager must be called before join()');
|
|
16835
|
+
}
|
|
16836
|
+
this.e2eeManager = e2ee;
|
|
16837
|
+
};
|
|
16364
16838
|
/**
|
|
16365
16839
|
* Notifies the SFU that a noise cancellation process has started.
|
|
16366
16840
|
*
|
|
@@ -16997,7 +17471,9 @@ class Call {
|
|
|
16997
17471
|
*/
|
|
16998
17472
|
applyDeviceConfig = async (settings, publish, skipSpeakerApply) => {
|
|
16999
17473
|
if (!skipSpeakerApply) {
|
|
17000
|
-
this.speaker.apply(settings)
|
|
17474
|
+
await this.speaker.apply(settings).catch((err) => {
|
|
17475
|
+
this.logger.warn('Speaker init failed', err);
|
|
17476
|
+
});
|
|
17001
17477
|
}
|
|
17002
17478
|
await this.camera.apply(settings.video, publish).catch((err) => {
|
|
17003
17479
|
this.logger.warn('Camera init failed', err);
|
|
@@ -17676,15 +18152,17 @@ class StableWSConnection {
|
|
|
17676
18152
|
this.client.logger.error(`Token not set, can't connect authenticate`);
|
|
17677
18153
|
return;
|
|
17678
18154
|
}
|
|
17679
|
-
const
|
|
18155
|
+
const wsAuthMessage = {
|
|
17680
18156
|
token,
|
|
18157
|
+
products: [WSAuthMessageProductsEnum.VIDEO],
|
|
17681
18158
|
user_details: {
|
|
17682
18159
|
id: user.id,
|
|
17683
18160
|
name: user.name,
|
|
17684
18161
|
image: user.image,
|
|
17685
18162
|
custom: user.custom,
|
|
17686
18163
|
},
|
|
17687
|
-
}
|
|
18164
|
+
};
|
|
18165
|
+
const authMessage = JSON.stringify(wsAuthMessage);
|
|
17688
18166
|
this._log(`onopen() - Sending auth message ${authMessage}`, {}, 'trace');
|
|
17689
18167
|
this.ws?.send(authMessage);
|
|
17690
18168
|
this._log('onopen() - onopen callback', { wsID });
|
|
@@ -18510,11 +18988,11 @@ class StreamClient {
|
|
|
18510
18988
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18511
18989
|
};
|
|
18512
18990
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18513
|
-
"1.
|
|
18991
|
+
"1.58.0";
|
|
18514
18992
|
getUserAgent = () => {
|
|
18515
18993
|
if (!this.cachedUserAgent) {
|
|
18516
18994
|
const { clientAppIdentifier = {} } = this.options;
|
|
18517
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
18995
|
+
const { sdkName = 'js', sdkVersion = "1.58.0", ...extras } = clientAppIdentifier;
|
|
18518
18996
|
this.cachedUserAgent = [
|
|
18519
18997
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18520
18998
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19747,6 +20225,357 @@ const humanize = (n) => {
|
|
|
19747
20225
|
return String(n);
|
|
19748
20226
|
};
|
|
19749
20227
|
|
|
20228
|
+
/**
|
|
20229
|
+
* Tiny, type-safe event emitter.
|
|
20230
|
+
*
|
|
20231
|
+
* Usage styles:
|
|
20232
|
+
* - compose: `private readonly events = new TypedEventEmitter<MyMap>()`
|
|
20233
|
+
* - extend: `class Foo extends TypedEventEmitter<MyMap> {}`
|
|
20234
|
+
*
|
|
20235
|
+
* Listener exceptions (sync throw or rejected promise) are caught and logged,
|
|
20236
|
+
* so one bad listener cannot break dispatch for the rest.
|
|
20237
|
+
*/
|
|
20238
|
+
class TypedEventEmitter {
|
|
20239
|
+
logger;
|
|
20240
|
+
byEvent = new Map();
|
|
20241
|
+
constructor(logger = 'TypedEventEmitter') {
|
|
20242
|
+
this.logger =
|
|
20243
|
+
typeof logger === 'string' ? videoLoggerSystem.getLogger(logger) : logger;
|
|
20244
|
+
}
|
|
20245
|
+
on = (event, fn) => {
|
|
20246
|
+
let listeners = this.byEvent.get(event);
|
|
20247
|
+
if (!listeners) {
|
|
20248
|
+
listeners = new Set();
|
|
20249
|
+
this.byEvent.set(event, listeners);
|
|
20250
|
+
}
|
|
20251
|
+
listeners.add(fn);
|
|
20252
|
+
return () => this.off(event, fn);
|
|
20253
|
+
};
|
|
20254
|
+
off = (event, fn) => {
|
|
20255
|
+
const listeners = this.byEvent.get(event);
|
|
20256
|
+
if (!listeners)
|
|
20257
|
+
return;
|
|
20258
|
+
listeners.delete(fn);
|
|
20259
|
+
if (listeners.size === 0)
|
|
20260
|
+
this.byEvent.delete(event);
|
|
20261
|
+
};
|
|
20262
|
+
emit = (event, payload) => {
|
|
20263
|
+
const listeners = this.byEvent.get(event);
|
|
20264
|
+
if (!listeners || listeners.size === 0)
|
|
20265
|
+
return;
|
|
20266
|
+
for (const listener of [...listeners]) {
|
|
20267
|
+
this.invoke(() => listener(payload), event);
|
|
20268
|
+
}
|
|
20269
|
+
};
|
|
20270
|
+
removeAllListeners = (event) => {
|
|
20271
|
+
if (event === undefined) {
|
|
20272
|
+
this.byEvent.clear();
|
|
20273
|
+
}
|
|
20274
|
+
else {
|
|
20275
|
+
this.byEvent.delete(event);
|
|
20276
|
+
}
|
|
20277
|
+
};
|
|
20278
|
+
invoke = (run, event) => {
|
|
20279
|
+
try {
|
|
20280
|
+
const result = run();
|
|
20281
|
+
if (result && typeof result.then === 'function') {
|
|
20282
|
+
result.catch((err) => {
|
|
20283
|
+
this.logger.warn(`Listener for '${String(event)}' rejected`, err);
|
|
20284
|
+
});
|
|
20285
|
+
}
|
|
20286
|
+
}
|
|
20287
|
+
catch (err) {
|
|
20288
|
+
this.logger.warn(`Listener for '${String(event)}' threw`, err);
|
|
20289
|
+
}
|
|
20290
|
+
};
|
|
20291
|
+
}
|
|
20292
|
+
|
|
20293
|
+
/**
|
|
20294
|
+
* Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
|
|
20295
|
+
* transforms to RTCRtpSenders and RTCRtpReceivers.
|
|
20296
|
+
*/
|
|
20297
|
+
class EncryptionManager extends TypedEventEmitter {
|
|
20298
|
+
algorithm;
|
|
20299
|
+
transform;
|
|
20300
|
+
disposed = false;
|
|
20301
|
+
piped;
|
|
20302
|
+
userId;
|
|
20303
|
+
worker;
|
|
20304
|
+
workerUrl;
|
|
20305
|
+
constructor(userId, worker, workerUrl, algorithm, transform) {
|
|
20306
|
+
super('EncryptionManager');
|
|
20307
|
+
this.userId = userId;
|
|
20308
|
+
this.worker = worker;
|
|
20309
|
+
this.workerUrl = workerUrl;
|
|
20310
|
+
this.algorithm = algorithm;
|
|
20311
|
+
this.transform = transform;
|
|
20312
|
+
this.worker.addEventListener('message', this.handleWorkerMessage);
|
|
20313
|
+
this.worker.addEventListener('error', this.handleWorkerError);
|
|
20314
|
+
}
|
|
20315
|
+
/**
|
|
20316
|
+
* Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
|
|
20317
|
+
* Use it to guard UI, or to avoid calling {@link create} where it would throw.
|
|
20318
|
+
*/
|
|
20319
|
+
static isSupported = () => {
|
|
20320
|
+
return preferredTransform() !== undefined;
|
|
20321
|
+
};
|
|
20322
|
+
/**
|
|
20323
|
+
* Create an EncryptionManager instance and initialize the E2EE Web Worker.
|
|
20324
|
+
*
|
|
20325
|
+
* @param userId - The local user's ID, normally `call.currentUserId`.
|
|
20326
|
+
* @param options - the create options.
|
|
20327
|
+
* @throws {Error} If the browser lacks Encoded Transforms.
|
|
20328
|
+
*
|
|
20329
|
+
* @example
|
|
20330
|
+
* ```ts
|
|
20331
|
+
* if (EncryptionManager.isSupported()) {
|
|
20332
|
+
* const e2ee = await EncryptionManager.create(call.currentUserId);
|
|
20333
|
+
* call.setE2EEManager(e2ee);
|
|
20334
|
+
* e2ee.setSharedKey(0, keyBytes);
|
|
20335
|
+
* }
|
|
20336
|
+
*/
|
|
20337
|
+
static create = async (userId, options) => {
|
|
20338
|
+
const transform = preferredTransform();
|
|
20339
|
+
if (!transform) {
|
|
20340
|
+
throw new Error(`E2EE is not supported in this browser`);
|
|
20341
|
+
}
|
|
20342
|
+
const { e2eeWorker } = await Promise.resolve().then(function () { return e2eeWorker$1; });
|
|
20343
|
+
const blob = new Blob([`(${e2eeWorker.toString()})()`], {
|
|
20344
|
+
type: 'application/javascript',
|
|
20345
|
+
});
|
|
20346
|
+
const url = URL.createObjectURL(blob);
|
|
20347
|
+
let worker;
|
|
20348
|
+
try {
|
|
20349
|
+
worker = new Worker(url, { name: 'stream-video-e2ee' });
|
|
20350
|
+
}
|
|
20351
|
+
catch (err) {
|
|
20352
|
+
// e.g. a CSP `worker-src` without `blob:`. Don't leak the object URL.
|
|
20353
|
+
URL.revokeObjectURL(url);
|
|
20354
|
+
throw err;
|
|
20355
|
+
}
|
|
20356
|
+
const algorithm = options?.algorithm ?? 'AES-128-GCM';
|
|
20357
|
+
return new EncryptionManager(userId, worker, url, algorithm, transform);
|
|
20358
|
+
};
|
|
20359
|
+
/**
|
|
20360
|
+
* {@link dispose} terminates the worker, so `postMessage` becomes a silent
|
|
20361
|
+
* no-op and an attached transform points at a dead worker: frames would stall
|
|
20362
|
+
* forever with no error and no event. Throwing is also fail-closed, since a
|
|
20363
|
+
* caller that swallows it still publishes nothing rather than cleartext.
|
|
20364
|
+
*/
|
|
20365
|
+
assertUsable = () => {
|
|
20366
|
+
if (this.disposed)
|
|
20367
|
+
throw new Error(`EncryptionManager is disposed`);
|
|
20368
|
+
};
|
|
20369
|
+
/**
|
|
20370
|
+
* Terminate the worker and release all resources.
|
|
20371
|
+
*
|
|
20372
|
+
* The manager is unusable afterwards and every other method throws. Call
|
|
20373
|
+
* {@link create} for a new one. Safe to call more than once.
|
|
20374
|
+
*/
|
|
20375
|
+
dispose = () => {
|
|
20376
|
+
if (this.disposed)
|
|
20377
|
+
return;
|
|
20378
|
+
this.disposed = true;
|
|
20379
|
+
this.piped = undefined;
|
|
20380
|
+
this.worker.removeEventListener('message', this.handleWorkerMessage);
|
|
20381
|
+
this.worker.removeEventListener('error', this.handleWorkerError);
|
|
20382
|
+
this.worker.terminate();
|
|
20383
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
20384
|
+
this.removeAllListeners();
|
|
20385
|
+
};
|
|
20386
|
+
/**
|
|
20387
|
+
* Set a per-user AES-GCM encryption key in the worker's key store.
|
|
20388
|
+
*
|
|
20389
|
+
* Use it when each participant has their own key from a central authority.
|
|
20390
|
+
* The receiver picks the right one by the `keyIndex` in the frame trailer.
|
|
20391
|
+
*
|
|
20392
|
+
* @param userId - The key owner.
|
|
20393
|
+
* @param keyIndex - Increases with each rotation.
|
|
20394
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20395
|
+
*/
|
|
20396
|
+
setKey = (userId, keyIndex, rawKey) => {
|
|
20397
|
+
this.assertUsable();
|
|
20398
|
+
this.validateKeyIndex(keyIndex);
|
|
20399
|
+
this.validateKeyLength(rawKey);
|
|
20400
|
+
this.worker.postMessage({ type: 'cmd.set_key', userId, keyIndex, rawKey });
|
|
20401
|
+
};
|
|
20402
|
+
/**
|
|
20403
|
+
* Fallback key for any user without a per-user key. The simplest E2EE mode:
|
|
20404
|
+
* one key for everyone, usually passphrase-derived, no distribution needed.
|
|
20405
|
+
* Setting an epoch makes it active for encryption while older epochs remain
|
|
20406
|
+
* available to decrypt in-flight frames until {@link removeSharedKey}.
|
|
20407
|
+
*
|
|
20408
|
+
* @param keyIndex - An integer 0-255, since one trailer byte carries it.
|
|
20409
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20410
|
+
*/
|
|
20411
|
+
setSharedKey = (keyIndex, rawKey) => {
|
|
20412
|
+
this.assertUsable();
|
|
20413
|
+
this.validateKeyIndex(keyIndex);
|
|
20414
|
+
this.validateKeyLength(rawKey);
|
|
20415
|
+
this.worker.postMessage({ type: 'cmd.set_shared_key', keyIndex, rawKey });
|
|
20416
|
+
};
|
|
20417
|
+
/**
|
|
20418
|
+
* Remove one shared-key epoch from the worker's receive key ring.
|
|
20419
|
+
*
|
|
20420
|
+
* If this is the active epoch, shared-key encryption stops until
|
|
20421
|
+
* {@link setSharedKey} succeeds again. An older epoch is not reactivated.
|
|
20422
|
+
*
|
|
20423
|
+
* @param keyIndex - The exact shared-key epoch to remove.
|
|
20424
|
+
*/
|
|
20425
|
+
removeSharedKey = (keyIndex) => {
|
|
20426
|
+
this.assertUsable();
|
|
20427
|
+
this.validateKeyIndex(keyIndex);
|
|
20428
|
+
this.worker.postMessage({ type: 'cmd.remove_shared_key', keyIndex });
|
|
20429
|
+
};
|
|
20430
|
+
/**
|
|
20431
|
+
* Retire one of a user's key epochs, leaving their other epochs usable.
|
|
20432
|
+
*
|
|
20433
|
+
* @param userId - The key owner.
|
|
20434
|
+
* @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
|
|
20435
|
+
*/
|
|
20436
|
+
removeKey = (userId, keyIndex) => {
|
|
20437
|
+
this.assertUsable();
|
|
20438
|
+
this.validateKeyIndex(keyIndex);
|
|
20439
|
+
this.worker.postMessage({ type: 'cmd.remove_key', userId, keyIndex });
|
|
20440
|
+
};
|
|
20441
|
+
/**
|
|
20442
|
+
* Drop every key a user holds, revoking their ability to decrypt later
|
|
20443
|
+
* frames. Call it when a participant leaves.
|
|
20444
|
+
*
|
|
20445
|
+
* To retire one rotated epoch instead, use {@link removeKey}.
|
|
20446
|
+
*/
|
|
20447
|
+
removeAllKeys = (userId) => {
|
|
20448
|
+
this.assertUsable();
|
|
20449
|
+
this.worker.postMessage({ type: 'cmd.remove_all_keys', userId });
|
|
20450
|
+
};
|
|
20451
|
+
/**
|
|
20452
|
+
* Called by the Publisher when it adds a transceiver.
|
|
20453
|
+
*
|
|
20454
|
+
* @param sender - The sender to encrypt.
|
|
20455
|
+
* @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
|
|
20456
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20457
|
+
* @internal
|
|
20458
|
+
*/
|
|
20459
|
+
encrypt = (sender, codec, trackType) => {
|
|
20460
|
+
this.assertUsable();
|
|
20461
|
+
this.pipe(sender, {
|
|
20462
|
+
operation: 'encode',
|
|
20463
|
+
userId: this.userId,
|
|
20464
|
+
codec,
|
|
20465
|
+
trackType,
|
|
20466
|
+
});
|
|
20467
|
+
};
|
|
20468
|
+
/**
|
|
20469
|
+
* Called by the Subscriber when a remote track arrives.
|
|
20470
|
+
*
|
|
20471
|
+
* @param receiver - The receiver to decrypt.
|
|
20472
|
+
* @param userId - The remote user, for key lookup in the worker.
|
|
20473
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20474
|
+
* @internal
|
|
20475
|
+
*/
|
|
20476
|
+
decrypt = (receiver, userId, trackType) => {
|
|
20477
|
+
this.assertUsable();
|
|
20478
|
+
this.pipe(receiver, { operation: 'decode', userId, trackType });
|
|
20479
|
+
};
|
|
20480
|
+
/** Pipe through the worker's transform, tracking targets to avoid double-piping. */
|
|
20481
|
+
pipe = (target, options) => {
|
|
20482
|
+
if (this.transform === 'script') {
|
|
20483
|
+
target.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
20484
|
+
return;
|
|
20485
|
+
}
|
|
20486
|
+
if ((this.piped ??= new WeakSet()).has(target))
|
|
20487
|
+
return;
|
|
20488
|
+
this.piped.add(target);
|
|
20489
|
+
// @ts-expect-error createEncodedStreams is not in the standard typedefs
|
|
20490
|
+
const { readable, writable } = target.createEncodedStreams();
|
|
20491
|
+
this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
|
|
20492
|
+
};
|
|
20493
|
+
/**
|
|
20494
|
+
* Toggle periodic performance reporting from the E2EE worker.
|
|
20495
|
+
*
|
|
20496
|
+
* While on, the worker emits `e2ee.perf_report` once per second with per-track
|
|
20497
|
+
* FPS and crypto timings. Useful for debugging throughput.
|
|
20498
|
+
*/
|
|
20499
|
+
enablePerformanceReporting = (enabled) => {
|
|
20500
|
+
this.assertUsable();
|
|
20501
|
+
this.worker.postMessage({
|
|
20502
|
+
type: 'cmd.enable_performance_reporting',
|
|
20503
|
+
enabled,
|
|
20504
|
+
});
|
|
20505
|
+
};
|
|
20506
|
+
/**
|
|
20507
|
+
* Request a snapshot of the worker's keys. It arrives later as the
|
|
20508
|
+
* `e2ee.key_state` event, listing fingerprints only, never key material.
|
|
20509
|
+
*/
|
|
20510
|
+
requestKeyState = () => {
|
|
20511
|
+
this.assertUsable();
|
|
20512
|
+
this.worker.postMessage({ type: 'cmd.request_key_state' });
|
|
20513
|
+
};
|
|
20514
|
+
handleWorkerMessage = (e) => {
|
|
20515
|
+
const { type, ...payload } = e.data ?? {};
|
|
20516
|
+
if (type === 'e2ee.error') {
|
|
20517
|
+
this.logger.error(e.data.message);
|
|
20518
|
+
return;
|
|
20519
|
+
}
|
|
20520
|
+
const event = type;
|
|
20521
|
+
this.logger.debug('Dispatching', event, payload);
|
|
20522
|
+
this.emit(event, payload);
|
|
20523
|
+
};
|
|
20524
|
+
handleWorkerError = (e) => {
|
|
20525
|
+
this.logger.error('Unhandled worker error:', e.message);
|
|
20526
|
+
};
|
|
20527
|
+
validateKeyLength = (rawKey) => {
|
|
20528
|
+
const is256 = this.algorithm === 'AES-256-GCM';
|
|
20529
|
+
const expected = is256 ? 32 : 16;
|
|
20530
|
+
if (rawKey.byteLength !== expected) {
|
|
20531
|
+
throw new Error(`Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`);
|
|
20532
|
+
}
|
|
20533
|
+
};
|
|
20534
|
+
/**
|
|
20535
|
+
* One trailer byte carries the keyIndex. A larger value would truncate to
|
|
20536
|
+
* `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
|
|
20537
|
+
* every decrypt. Reject it rather than ship a silently broken key epoch.
|
|
20538
|
+
*/
|
|
20539
|
+
validateKeyIndex = (keyIndex) => {
|
|
20540
|
+
if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
|
|
20541
|
+
throw new Error(`keyIndex must be an integer between 0 and 255, got ${keyIndex}`);
|
|
20542
|
+
}
|
|
20543
|
+
};
|
|
20544
|
+
}
|
|
20545
|
+
|
|
20546
|
+
/**
|
|
20547
|
+
* E2EE worker. The build populates it.
|
|
20548
|
+
*
|
|
20549
|
+
* The actual implementation lives in `e2ee-worker/e2ee-worker-impl.ts` and its
|
|
20550
|
+
* sibling modules (`constants.ts`, `codec.ts`, `crypto.ts`, `utils.ts`,
|
|
20551
|
+
* `types.ts`), all written as real TypeScript with full type checking
|
|
20552
|
+
* and import support.
|
|
20553
|
+
*
|
|
20554
|
+
* ## How it works
|
|
20555
|
+
*
|
|
20556
|
+
* 1. You edit files in `e2ee-worker/` as normal TypeScript
|
|
20557
|
+
* 2. `yarn build` runs Rollup, which invokes `rollup-plugin-inline-worker`
|
|
20558
|
+
* 3. The plugin bundles `e2ee-worker/e2ee-worker-impl.ts` (and its imports)
|
|
20559
|
+
* into a single function via a nested Rollup + TypeScript build
|
|
20560
|
+
* 4. The function replaces this file's export at build time
|
|
20561
|
+
* 5. `EncryptionManager.create()` creates a Worker from `e2eeWorker.toString()`
|
|
20562
|
+
*
|
|
20563
|
+
* In tests, this module is mocked (see `__tests__/EncryptionManager.test.ts`),
|
|
20564
|
+
* so the placeholder is never evaluated.
|
|
20565
|
+
*
|
|
20566
|
+
* @see e2ee-worker/e2ee-worker-impl.ts - the entry point
|
|
20567
|
+
* @see ../../plugins/rollup-plugin-inline-worker.ts - the Rollup plugin
|
|
20568
|
+
* @see EncryptionManager.ts - consumes e2eeWorker
|
|
20569
|
+
*/
|
|
20570
|
+
function e2eeWorker() {
|
|
20571
|
+
// will be populated at build time
|
|
20572
|
+
}
|
|
20573
|
+
|
|
20574
|
+
var e2eeWorker$1 = /*#__PURE__*/Object.freeze({
|
|
20575
|
+
__proto__: null,
|
|
20576
|
+
e2eeWorker: e2eeWorker
|
|
20577
|
+
});
|
|
20578
|
+
|
|
19750
20579
|
Object.defineProperty(exports, "AxiosError", {
|
|
19751
20580
|
enumerable: true,
|
|
19752
20581
|
get: function () { return axios.AxiosError; }
|
|
@@ -19772,11 +20601,16 @@ exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnu
|
|
|
19772
20601
|
exports.DeviceManager = DeviceManager;
|
|
19773
20602
|
exports.DeviceManagerState = DeviceManagerState;
|
|
19774
20603
|
exports.DynascaleManager = DynascaleManager;
|
|
20604
|
+
exports.EncryptionManager = EncryptionManager;
|
|
20605
|
+
exports.EncryptionSettingsRequestModeEnum = EncryptionSettingsRequestModeEnum;
|
|
20606
|
+
exports.EncryptionSettingsResponseModeEnum = EncryptionSettingsResponseModeEnum;
|
|
19775
20607
|
exports.ErrorFromResponse = ErrorFromResponse;
|
|
19776
20608
|
exports.FrameRecordingSettingsRequestModeEnum = FrameRecordingSettingsRequestModeEnum;
|
|
19777
20609
|
exports.FrameRecordingSettingsRequestQualityEnum = FrameRecordingSettingsRequestQualityEnum;
|
|
19778
20610
|
exports.FrameRecordingSettingsResponseModeEnum = FrameRecordingSettingsResponseModeEnum;
|
|
20611
|
+
exports.HLSSettingsRequestQualityTracksEnum = HLSSettingsRequestQualityTracksEnum;
|
|
19779
20612
|
exports.IndividualRecordingSettingsRequestModeEnum = IndividualRecordingSettingsRequestModeEnum;
|
|
20613
|
+
exports.IndividualRecordingSettingsRequestOutputTypesEnum = IndividualRecordingSettingsRequestOutputTypesEnum;
|
|
19780
20614
|
exports.IndividualRecordingSettingsResponseModeEnum = IndividualRecordingSettingsResponseModeEnum;
|
|
19781
20615
|
exports.IngressAudioEncodingOptionsRequestChannelsEnum = IngressAudioEncodingOptionsRequestChannelsEnum;
|
|
19782
20616
|
exports.IngressSourceRequestFpsEnum = IngressSourceRequestFpsEnum;
|
|
@@ -19792,6 +20626,7 @@ exports.RawRecordingSettingsRequestModeEnum = RawRecordingSettingsRequestModeEnu
|
|
|
19792
20626
|
exports.RawRecordingSettingsResponseModeEnum = RawRecordingSettingsResponseModeEnum;
|
|
19793
20627
|
exports.RecordSettingsRequestModeEnum = RecordSettingsRequestModeEnum;
|
|
19794
20628
|
exports.RecordSettingsRequestQualityEnum = RecordSettingsRequestQualityEnum;
|
|
20629
|
+
exports.RequestPermissionRequestPermissionsEnum = RequestPermissionRequestPermissionsEnum;
|
|
19795
20630
|
exports.RxUtils = rxUtils;
|
|
19796
20631
|
exports.ScreenShareManager = ScreenShareManager;
|
|
19797
20632
|
exports.ScreenShareState = ScreenShareState;
|
|
@@ -19813,9 +20648,13 @@ exports.TranscriptionSettingsRequestModeEnum = TranscriptionSettingsRequestModeE
|
|
|
19813
20648
|
exports.TranscriptionSettingsResponseClosedCaptionModeEnum = TranscriptionSettingsResponseClosedCaptionModeEnum;
|
|
19814
20649
|
exports.TranscriptionSettingsResponseLanguageEnum = TranscriptionSettingsResponseLanguageEnum;
|
|
19815
20650
|
exports.TranscriptionSettingsResponseModeEnum = TranscriptionSettingsResponseModeEnum;
|
|
20651
|
+
exports.TranslationSettingsLanguagesEnum = TranslationSettingsLanguagesEnum;
|
|
20652
|
+
exports.UpdateUserPermissionsRequestGrantPermissionsEnum = UpdateUserPermissionsRequestGrantPermissionsEnum;
|
|
20653
|
+
exports.UpdateUserPermissionsRequestRevokePermissionsEnum = UpdateUserPermissionsRequestRevokePermissionsEnum;
|
|
19816
20654
|
exports.VideoSettingsRequestCameraFacingEnum = VideoSettingsRequestCameraFacingEnum;
|
|
19817
20655
|
exports.VideoSettingsResponseCameraFacingEnum = VideoSettingsResponseCameraFacingEnum;
|
|
19818
20656
|
exports.ViewportTracker = ViewportTracker;
|
|
20657
|
+
exports.WSAuthMessageProductsEnum = WSAuthMessageProductsEnum;
|
|
19819
20658
|
exports.checkIfAudioOutputChangeSupported = checkIfAudioOutputChangeSupported;
|
|
19820
20659
|
exports.combineComparators = combineComparators;
|
|
19821
20660
|
exports.conditional = conditional;
|
|
@@ -19829,6 +20668,7 @@ exports.getAudioBrowserPermission = getAudioBrowserPermission;
|
|
|
19829
20668
|
exports.getAudioDevices = getAudioDevices;
|
|
19830
20669
|
exports.getAudioOutputDevices = getAudioOutputDevices;
|
|
19831
20670
|
exports.getAudioStream = getAudioStream;
|
|
20671
|
+
exports.getCallMediaEngineProvider = getCallMediaEngineProvider;
|
|
19832
20672
|
exports.getClientDetails = getClientDetails;
|
|
19833
20673
|
exports.getDeviceState = getDeviceState;
|
|
19834
20674
|
exports.getScreenShareStream = getScreenShareStream;
|
|
@@ -19857,6 +20697,7 @@ exports.reactionType = reactionType;
|
|
|
19857
20697
|
exports.resolveDeviceId = resolveDeviceId;
|
|
19858
20698
|
exports.role = role;
|
|
19859
20699
|
exports.screenSharing = screenSharing;
|
|
20700
|
+
exports.setCallMediaEngineProvider = setCallMediaEngineProvider;
|
|
19860
20701
|
exports.setDeviceInfo = setDeviceInfo;
|
|
19861
20702
|
exports.setOSInfo = setOSInfo;
|
|
19862
20703
|
exports.setPowerState = setPowerState;
|