@stream-io/video-client 1.57.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 +13 -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 +831 -55
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +875 -54
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.es.js +864 -55
- 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/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 +1 -1
- 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 +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/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/TypedEventEmitter.ts +71 -0
- package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
- package/src/helpers/ensureExhausted.ts +4 -2
- package/src/permissions/PermissionsContext.ts +10 -5
- 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 +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
|
@@ -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.58.0";
|
|
4931
5064
|
const [major, minor, patch] = version.split('.');
|
|
4932
5065
|
let sdkInfo = {
|
|
4933
5066
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5910,6 +6043,7 @@ class CallState {
|
|
|
5910
6043
|
settingsSubject = new BehaviorSubject(undefined);
|
|
5911
6044
|
transcribingSubject = new BehaviorSubject(false);
|
|
5912
6045
|
captioningSubject = new BehaviorSubject(false);
|
|
6046
|
+
e2eeEnabledSubject = new BehaviorSubject(false);
|
|
5913
6047
|
endedBySubject = new BehaviorSubject(undefined);
|
|
5914
6048
|
thumbnailsSubject = new BehaviorSubject(undefined);
|
|
5915
6049
|
membersSubject = new BehaviorSubject([]);
|
|
@@ -6068,6 +6202,11 @@ class CallState {
|
|
|
6068
6202
|
* Will provide the closed captioning state of this call.
|
|
6069
6203
|
*/
|
|
6070
6204
|
captioning$;
|
|
6205
|
+
/**
|
|
6206
|
+
* Whether end-to-end encryption is active for this call, as reported by the
|
|
6207
|
+
* SFU in the join response.
|
|
6208
|
+
*/
|
|
6209
|
+
e2eeEnabled$;
|
|
6071
6210
|
/**
|
|
6072
6211
|
* Will provide the user who ended this call.
|
|
6073
6212
|
*/
|
|
@@ -6169,6 +6308,7 @@ class CallState {
|
|
|
6169
6308
|
this.rawRecording$ = duc(this.rawRecordingSubject);
|
|
6170
6309
|
this.transcribing$ = duc(this.transcribingSubject);
|
|
6171
6310
|
this.captioning$ = duc(this.captioningSubject);
|
|
6311
|
+
this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
|
|
6172
6312
|
this.eventHandlers = {
|
|
6173
6313
|
// these events are not updating the call state:
|
|
6174
6314
|
'call.frame_recording_ready': undefined,
|
|
@@ -6271,6 +6411,7 @@ class CallState {
|
|
|
6271
6411
|
clearTimeout(taskId);
|
|
6272
6412
|
this.closedCaptionsTasks.delete(ccKey);
|
|
6273
6413
|
}
|
|
6414
|
+
this.removeAllOrphanedTracks();
|
|
6274
6415
|
};
|
|
6275
6416
|
/**
|
|
6276
6417
|
* Sets the list of criteria that are used to sort the participants.
|
|
@@ -6620,6 +6761,12 @@ class CallState {
|
|
|
6620
6761
|
get transcribing() {
|
|
6621
6762
|
return this.getCurrentValue(this.transcribing$);
|
|
6622
6763
|
}
|
|
6764
|
+
/**
|
|
6765
|
+
* Whether end-to-end encryption is active for this call.
|
|
6766
|
+
*/
|
|
6767
|
+
get e2eeEnabled() {
|
|
6768
|
+
return this.getCurrentValue(this.e2eeEnabled$);
|
|
6769
|
+
}
|
|
6623
6770
|
/**
|
|
6624
6771
|
* Will provide the user who ended this call.
|
|
6625
6772
|
*/
|
|
@@ -6840,6 +6987,18 @@ class CallState {
|
|
|
6840
6987
|
removeOrphanedTrack = (id) => {
|
|
6841
6988
|
this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
|
|
6842
6989
|
};
|
|
6990
|
+
/**
|
|
6991
|
+
* Drops every orphaned track. Call this when the peer connections that own
|
|
6992
|
+
* the stored receivers go away (full leave, reconnect, or migration):
|
|
6993
|
+
* `pc.close()` does not raise the track `ended` event, so the per-track
|
|
6994
|
+
* cleanup never fires and the receivers + their closed PCs would otherwise
|
|
6995
|
+
* leak for the call's lifetime.
|
|
6996
|
+
*
|
|
6997
|
+
* @internal
|
|
6998
|
+
*/
|
|
6999
|
+
removeAllOrphanedTracks = () => {
|
|
7000
|
+
this.orphanedTracks = [];
|
|
7001
|
+
};
|
|
6843
7002
|
/**
|
|
6844
7003
|
* Takes all orphaned tracks with the given track lookup prefix.
|
|
6845
7004
|
* All orphaned tracks with the given track lookup prefix are removed from the call state.
|
|
@@ -6902,7 +7061,7 @@ class CallState {
|
|
|
6902
7061
|
* @param reconnectDetails optional reconnect details.
|
|
6903
7062
|
*/
|
|
6904
7063
|
updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
|
|
6905
|
-
const { participants, participantCount, startedAt, pins } = callState;
|
|
7064
|
+
const { participants, participantCount, startedAt, pins, e2EeEnabled } = callState;
|
|
6906
7065
|
const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
|
|
6907
7066
|
this.setParticipants(() => {
|
|
6908
7067
|
const participantLookup = this.getParticipantLookupBySessionId();
|
|
@@ -6928,6 +7087,7 @@ class CallState {
|
|
|
6928
7087
|
this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
6929
7088
|
this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
|
|
6930
7089
|
this.setServerSidePins(pins);
|
|
7090
|
+
this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
|
|
6931
7091
|
};
|
|
6932
7092
|
updateFromMemberRemoved = (event) => {
|
|
6933
7093
|
this.updateFromCallResponse(event.call);
|
|
@@ -7125,6 +7285,37 @@ class CallState {
|
|
|
7125
7285
|
};
|
|
7126
7286
|
}
|
|
7127
7287
|
|
|
7288
|
+
/**
|
|
7289
|
+
* Detection and selection policy for the two WebRTC Encoded Transform APIs.
|
|
7290
|
+
* Internal: which one the SDK attaches is an RTC-layer detail. Consumers want
|
|
7291
|
+
* `EncryptionManager.isSupported` instead.
|
|
7292
|
+
*/
|
|
7293
|
+
/**
|
|
7294
|
+
* Chrome only, and the reason an RTCPeerConnection carrying E2EE needs the
|
|
7295
|
+
* non-standard `encodedInsertableStreams` flag.
|
|
7296
|
+
*/
|
|
7297
|
+
const hasInsertableStreams = () => typeof RTCRtpSender !== 'undefined' &&
|
|
7298
|
+
'createEncodedStreams' in RTCRtpSender.prototype;
|
|
7299
|
+
/** Whether the standard `RTCRtpScriptTransform` API exists. */
|
|
7300
|
+
const hasScriptTransform = () => typeof RTCRtpScriptTransform !== 'undefined';
|
|
7301
|
+
/**
|
|
7302
|
+
* Which Encoded Transform API to attach E2EE with here.
|
|
7303
|
+
*
|
|
7304
|
+
* - `'insertable'`: legacy Insertable Streams. Used on Chrome, where
|
|
7305
|
+
* `RTCRtpScriptTransform` is still unreliable for E2EE.
|
|
7306
|
+
* - `'script'`: the standard API. Used everywhere else.
|
|
7307
|
+
* - `undefined`: neither exists, so E2EE cannot run.
|
|
7308
|
+
*/
|
|
7309
|
+
const preferredTransform = () => {
|
|
7310
|
+
const insertable = hasInsertableStreams();
|
|
7311
|
+
// Chrome's RTCRtpScriptTransform is still unreliable for E2EE.
|
|
7312
|
+
if (isChrome() && insertable)
|
|
7313
|
+
return 'insertable';
|
|
7314
|
+
if (hasScriptTransform())
|
|
7315
|
+
return 'script';
|
|
7316
|
+
return insertable ? 'insertable' : undefined;
|
|
7317
|
+
};
|
|
7318
|
+
|
|
7128
7319
|
/**
|
|
7129
7320
|
* Flatten the stats report into an array of stats objects.
|
|
7130
7321
|
*
|
|
@@ -8233,6 +8424,7 @@ class BasePeerConnection {
|
|
|
8233
8424
|
state;
|
|
8234
8425
|
dispatcher;
|
|
8235
8426
|
clientPublishOptions;
|
|
8427
|
+
e2ee;
|
|
8236
8428
|
tag;
|
|
8237
8429
|
sfuClient;
|
|
8238
8430
|
onReconnectionNeeded;
|
|
@@ -8254,13 +8446,14 @@ class BasePeerConnection {
|
|
|
8254
8446
|
/**
|
|
8255
8447
|
* Constructs a new `BasePeerConnection` instance.
|
|
8256
8448
|
*/
|
|
8257
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8449
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, e2ee, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8258
8450
|
this.peerType = peerType;
|
|
8259
8451
|
this.sfuClient = sfuClient;
|
|
8260
8452
|
this.state = state;
|
|
8261
8453
|
this.dispatcher = dispatcher;
|
|
8262
8454
|
this.iceRestartDelay = iceRestartDelay;
|
|
8263
8455
|
this.clientPublishOptions = clientPublishOptions;
|
|
8456
|
+
this.e2ee = e2ee;
|
|
8264
8457
|
this.tag = tag;
|
|
8265
8458
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8266
8459
|
this.onIceConnected = onIceConnected;
|
|
@@ -8279,7 +8472,13 @@ class BasePeerConnection {
|
|
|
8279
8472
|
}
|
|
8280
8473
|
}
|
|
8281
8474
|
createPeerConnection = (connectionConfig) => {
|
|
8282
|
-
const
|
|
8475
|
+
const config = { ...connectionConfig };
|
|
8476
|
+
// The legacy Insertable Streams path requires this non-standard flag.
|
|
8477
|
+
if (this.e2ee && hasInsertableStreams()) {
|
|
8478
|
+
// @ts-expect-error not part of the standard lib yet
|
|
8479
|
+
config.encodedInsertableStreams = true;
|
|
8480
|
+
}
|
|
8481
|
+
const pc = new RTCPeerConnection(config);
|
|
8283
8482
|
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
8284
8483
|
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
8285
8484
|
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
@@ -9033,6 +9232,10 @@ class Publisher extends BasePeerConnection {
|
|
|
9033
9232
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9034
9233
|
'maintain-framerate';
|
|
9035
9234
|
await transceiver.sender.setParameters(params);
|
|
9235
|
+
if (this.e2ee) {
|
|
9236
|
+
this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
|
|
9237
|
+
this.logger.debug('E2EE encryptor attached to sender');
|
|
9238
|
+
}
|
|
9036
9239
|
await this.negotiate();
|
|
9037
9240
|
};
|
|
9038
9241
|
/**
|
|
@@ -9644,9 +9847,15 @@ class Subscriber extends BasePeerConnection {
|
|
|
9644
9847
|
trackLookupPrefix: trackId,
|
|
9645
9848
|
track: primaryStream,
|
|
9646
9849
|
trackType,
|
|
9850
|
+
receiver: this.e2ee ? e.receiver : undefined,
|
|
9647
9851
|
});
|
|
9648
9852
|
return;
|
|
9649
9853
|
}
|
|
9854
|
+
else if (this.e2ee) {
|
|
9855
|
+
const { userId } = participantToUpdate;
|
|
9856
|
+
this.e2ee.decrypt(e.receiver, userId, TrackType[trackType]);
|
|
9857
|
+
this.logger.debug('E2EE decryptor attached to receiver', userId);
|
|
9858
|
+
}
|
|
9650
9859
|
const streamKindProp = trackTypeToParticipantStreamKey(trackType);
|
|
9651
9860
|
if (!streamKindProp) {
|
|
9652
9861
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
@@ -10618,7 +10827,7 @@ const handleRemoteSoftMute = (call) => {
|
|
|
10618
10827
|
/**
|
|
10619
10828
|
* An event responder which handles the `participantJoined` event.
|
|
10620
10829
|
*/
|
|
10621
|
-
const watchParticipantJoined = (state) => {
|
|
10830
|
+
const watchParticipantJoined = (state, e2ee = undefined) => {
|
|
10622
10831
|
return function onParticipantJoined(e) {
|
|
10623
10832
|
const { participant } = e;
|
|
10624
10833
|
if (!participant)
|
|
@@ -10631,7 +10840,7 @@ const watchParticipantJoined = (state) => {
|
|
|
10631
10840
|
// The SFU would send participant info as part of the `join`
|
|
10632
10841
|
// response and then follow up with a `participantJoined` event for
|
|
10633
10842
|
// already announced participants.
|
|
10634
|
-
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
10843
|
+
const orphanedTracks = reconcileOrphanedTracks(state, participant, e2ee);
|
|
10635
10844
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
10636
10845
|
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
10637
10846
|
viewportVisibilityState: {
|
|
@@ -10667,7 +10876,7 @@ const watchParticipantUpdated = (state) => {
|
|
|
10667
10876
|
* An event responder which handles the `trackPublished` event.
|
|
10668
10877
|
* The SFU will send this event when a participant publishes a track.
|
|
10669
10878
|
*/
|
|
10670
|
-
const watchTrackPublished = (state) => {
|
|
10879
|
+
const watchTrackPublished = (state, e2ee = undefined) => {
|
|
10671
10880
|
return function onTrackPublished(e) {
|
|
10672
10881
|
const { type, sessionId } = e;
|
|
10673
10882
|
// An optimization for large calls.
|
|
@@ -10675,7 +10884,7 @@ const watchTrackPublished = (state) => {
|
|
|
10675
10884
|
// events, and instead, it would only provide the participant's information
|
|
10676
10885
|
// once they start publishing a track.
|
|
10677
10886
|
if (e.participant) {
|
|
10678
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10887
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
|
|
10679
10888
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
10680
10889
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10681
10890
|
}
|
|
@@ -10690,12 +10899,12 @@ const watchTrackPublished = (state) => {
|
|
|
10690
10899
|
* An event responder which handles the `trackUnpublished` event.
|
|
10691
10900
|
* The SFU will send this event when a participant unpublishes a track.
|
|
10692
10901
|
*/
|
|
10693
|
-
const watchTrackUnpublished = (state) => {
|
|
10902
|
+
const watchTrackUnpublished = (state, e2ee = undefined) => {
|
|
10694
10903
|
return function onTrackUnpublished(e) {
|
|
10695
10904
|
const { type, sessionId } = e;
|
|
10696
10905
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
10697
10906
|
if (e.participant) {
|
|
10698
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10907
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
|
|
10699
10908
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
10700
10909
|
state.updateOrAddParticipant(sessionId, participant, (p) => ({
|
|
10701
10910
|
pausedTracks: p.pausedTracks?.filter((t) => t !== type),
|
|
@@ -10714,17 +10923,22 @@ const watchTrackUnpublished = (state) => {
|
|
|
10714
10923
|
*
|
|
10715
10924
|
* @param state the call state.
|
|
10716
10925
|
* @param participant the participant.
|
|
10926
|
+
* @param e2ee accessor for the call's encryption manager, if any.
|
|
10717
10927
|
*/
|
|
10718
|
-
const reconcileOrphanedTracks = (state, participant) => {
|
|
10928
|
+
const reconcileOrphanedTracks = (state, participant, e2ee) => {
|
|
10719
10929
|
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
10720
10930
|
if (!orphanTracks.length)
|
|
10721
10931
|
return;
|
|
10932
|
+
const manager = e2ee?.();
|
|
10722
10933
|
const reconciledTracks = {};
|
|
10723
10934
|
for (const orphan of orphanTracks) {
|
|
10724
10935
|
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
10725
10936
|
if (!key)
|
|
10726
10937
|
continue;
|
|
10727
10938
|
reconciledTracks[key] = orphan.track;
|
|
10939
|
+
if (manager && orphan.receiver) {
|
|
10940
|
+
manager.decrypt(orphan.receiver, participant.userId, TrackType[orphan.trackType]);
|
|
10941
|
+
}
|
|
10728
10942
|
}
|
|
10729
10943
|
return reconciledTracks;
|
|
10730
10944
|
};
|
|
@@ -10780,6 +10994,11 @@ const watchAudioLevelChanged = (dispatcher, state) => {
|
|
|
10780
10994
|
*/
|
|
10781
10995
|
const registerEventHandlers = (call, dispatcher) => {
|
|
10782
10996
|
const state = call.state;
|
|
10997
|
+
// Read lazily on each event: setE2EEManager can run after setup() (an app that
|
|
10998
|
+
// inspects call settings via get()/getOrCreate() before deciding to encrypt has
|
|
10999
|
+
// already triggered it), so a value captured here would be a stale undefined
|
|
11000
|
+
// for the whole call and orphaned tracks would never get a decryptor.
|
|
11001
|
+
const e2ee = () => call.e2eeManager;
|
|
10783
11002
|
const eventHandlers = [
|
|
10784
11003
|
call.on('call.ended', watchCallEnded(call)),
|
|
10785
11004
|
watchSfuCallEnded(call),
|
|
@@ -10787,11 +11006,11 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
10787
11006
|
watchSfuErrorReports(dispatcher),
|
|
10788
11007
|
watchConnectionQualityChanged(dispatcher, state),
|
|
10789
11008
|
watchParticipantCountChanged(dispatcher, state),
|
|
10790
|
-
call.on('participantJoined', watchParticipantJoined(state)),
|
|
11009
|
+
call.on('participantJoined', watchParticipantJoined(state, e2ee)),
|
|
10791
11010
|
call.on('participantLeft', watchParticipantLeft(state)),
|
|
10792
11011
|
call.on('participantUpdated', watchParticipantUpdated(state)),
|
|
10793
|
-
call.on('trackPublished', watchTrackPublished(state)),
|
|
10794
|
-
call.on('trackUnpublished', watchTrackUnpublished(state)),
|
|
11012
|
+
call.on('trackPublished', watchTrackPublished(state, e2ee)),
|
|
11013
|
+
call.on('trackUnpublished', watchTrackUnpublished(state, e2ee)),
|
|
10795
11014
|
watchAudioLevelChanged(dispatcher, state),
|
|
10796
11015
|
watchDominantSpeakerChanged(dispatcher, state),
|
|
10797
11016
|
call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
|
|
@@ -11919,13 +12138,14 @@ class PermissionsContext {
|
|
|
11919
12138
|
return false;
|
|
11920
12139
|
const { audio, video, screensharing } = settings;
|
|
11921
12140
|
switch (permission) {
|
|
11922
|
-
case
|
|
12141
|
+
case RequestPermissionRequestPermissionsEnum.SEND_AUDIO:
|
|
11923
12142
|
return audio.access_request_enabled;
|
|
11924
|
-
case
|
|
12143
|
+
case RequestPermissionRequestPermissionsEnum.SEND_VIDEO:
|
|
11925
12144
|
return video.access_request_enabled;
|
|
11926
|
-
case
|
|
12145
|
+
case RequestPermissionRequestPermissionsEnum.SCREENSHARE:
|
|
11927
12146
|
return screensharing.access_request_enabled;
|
|
11928
12147
|
default:
|
|
12148
|
+
ensureExhausted(permission);
|
|
11929
12149
|
return false;
|
|
11930
12150
|
}
|
|
11931
12151
|
};
|
|
@@ -12932,6 +13152,27 @@ class DeviceManager {
|
|
|
12932
13152
|
this.filters.forEach((entry) => entry.stop?.());
|
|
12933
13153
|
}
|
|
12934
13154
|
}
|
|
13155
|
+
reconcileOptimisticStatus = async () => {
|
|
13156
|
+
const target = this.state.optimisticStatus;
|
|
13157
|
+
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
|
|
13158
|
+
try {
|
|
13159
|
+
if (target === 'enabled' && this.state.status !== 'enabled') {
|
|
13160
|
+
await this.unmuteStream();
|
|
13161
|
+
if (!signal.aborted)
|
|
13162
|
+
this.state.setStatus('enabled');
|
|
13163
|
+
}
|
|
13164
|
+
else if (target === 'disabled' && this.state.status === 'enabled') {
|
|
13165
|
+
// mirror whatever disable() does to stop/pause the track per disableMode
|
|
13166
|
+
if (!signal.aborted)
|
|
13167
|
+
this.state.setStatus('disabled');
|
|
13168
|
+
}
|
|
13169
|
+
}
|
|
13170
|
+
finally {
|
|
13171
|
+
if (!signal.aborted)
|
|
13172
|
+
this.state.setPendingStatus(this.state.status);
|
|
13173
|
+
}
|
|
13174
|
+
});
|
|
13175
|
+
};
|
|
12935
13176
|
disableTracks() {
|
|
12936
13177
|
this.getTracks().forEach((track) => {
|
|
12937
13178
|
if (track.enabled)
|
|
@@ -13565,6 +13806,33 @@ class CameraManager extends DeviceManager {
|
|
|
13565
13806
|
}
|
|
13566
13807
|
}
|
|
13567
13808
|
}
|
|
13809
|
+
enable() {
|
|
13810
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13811
|
+
this.state.setPendingStatus('enabled');
|
|
13812
|
+
return Promise.resolve();
|
|
13813
|
+
}
|
|
13814
|
+
return super.enable();
|
|
13815
|
+
}
|
|
13816
|
+
async disable(forceStopOrOptions) {
|
|
13817
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13818
|
+
this.state.setPendingStatus('disabled');
|
|
13819
|
+
return;
|
|
13820
|
+
}
|
|
13821
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
13822
|
+
if (forceStopOrOptions === undefined)
|
|
13823
|
+
return super.disable();
|
|
13824
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
13825
|
+
return super.disable(forceStopOrOptions);
|
|
13826
|
+
}
|
|
13827
|
+
return super.disable(forceStopOrOptions);
|
|
13828
|
+
}
|
|
13829
|
+
toggle() {
|
|
13830
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13831
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
13832
|
+
return Promise.resolve();
|
|
13833
|
+
}
|
|
13834
|
+
return super.toggle();
|
|
13835
|
+
}
|
|
13568
13836
|
/**
|
|
13569
13837
|
* Applies the video settings to the camera.
|
|
13570
13838
|
*
|
|
@@ -13598,9 +13866,16 @@ class CameraManager extends DeviceManager {
|
|
|
13598
13866
|
await this.enable();
|
|
13599
13867
|
}
|
|
13600
13868
|
}
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13869
|
+
if (isReactNative() && publish && canPublish) {
|
|
13870
|
+
// On RN the camera is enabled/disabled optimistically before JOINED. Reconcile now
|
|
13871
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
13872
|
+
await this.reconcileOptimisticStatus();
|
|
13873
|
+
}
|
|
13874
|
+
else {
|
|
13875
|
+
const { mediaStream } = this.state;
|
|
13876
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
13877
|
+
await this.publishStream(mediaStream);
|
|
13878
|
+
}
|
|
13604
13879
|
}
|
|
13605
13880
|
}
|
|
13606
13881
|
getDevices() {
|
|
@@ -13619,7 +13894,10 @@ class CameraManager extends DeviceManager {
|
|
|
13619
13894
|
}
|
|
13620
13895
|
return constraints;
|
|
13621
13896
|
}
|
|
13622
|
-
getStream(constraints) {
|
|
13897
|
+
async getStream(constraints) {
|
|
13898
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
13899
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
13900
|
+
await this.call.ensureMediaFactory();
|
|
13623
13901
|
return getVideoStream(constraints, this.call.tracer);
|
|
13624
13902
|
}
|
|
13625
13903
|
}
|
|
@@ -13635,6 +13913,9 @@ class AudioDeviceManager extends DeviceManager {
|
|
|
13635
13913
|
if (!this.call.state.settings?.audio.hifi_audio_enabled) {
|
|
13636
13914
|
throw new Error('High Fidelity audio is not enabled for this call');
|
|
13637
13915
|
}
|
|
13916
|
+
if (isReactNative() && this.call.hasMediaEngine) {
|
|
13917
|
+
throw new Error('setAudioBitrateProfile must be called before joining the call.');
|
|
13918
|
+
}
|
|
13638
13919
|
this.doSetAudioBitrateProfile(profile);
|
|
13639
13920
|
this.state.setAudioBitrateProfile(profile);
|
|
13640
13921
|
if (this.enabled) {
|
|
@@ -13907,7 +14188,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
13907
14188
|
]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
|
|
13908
14189
|
try {
|
|
13909
14190
|
if (callingState === CallingState.LEFT) {
|
|
13910
|
-
|
|
14191
|
+
// The muted-recording-prepared mode is reset in `callManager.stop()`
|
|
14192
|
+
// (during leave, while the call factory is still alive), not here —
|
|
14193
|
+
// this subscription fires asynchronously and could land after the
|
|
14194
|
+
// factory is disposed, forcing a default-ADM rebuild.
|
|
13911
14195
|
await this.stopSpeakingWhileMutedDetection();
|
|
13912
14196
|
}
|
|
13913
14197
|
if (callingState !== CallingState.JOINED)
|
|
@@ -14007,6 +14291,33 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14007
14291
|
this.subscriptions.push(unsubscribe);
|
|
14008
14292
|
}
|
|
14009
14293
|
}
|
|
14294
|
+
enable() {
|
|
14295
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14296
|
+
this.state.setPendingStatus('enabled');
|
|
14297
|
+
return Promise.resolve();
|
|
14298
|
+
}
|
|
14299
|
+
return super.enable();
|
|
14300
|
+
}
|
|
14301
|
+
async disable(forceStopOrOptions) {
|
|
14302
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14303
|
+
this.state.setPendingStatus('disabled');
|
|
14304
|
+
return;
|
|
14305
|
+
}
|
|
14306
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
14307
|
+
if (forceStopOrOptions === undefined)
|
|
14308
|
+
return super.disable();
|
|
14309
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
14310
|
+
return super.disable(forceStopOrOptions);
|
|
14311
|
+
}
|
|
14312
|
+
return super.disable(forceStopOrOptions);
|
|
14313
|
+
}
|
|
14314
|
+
toggle() {
|
|
14315
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14316
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
14317
|
+
return Promise.resolve();
|
|
14318
|
+
}
|
|
14319
|
+
return super.toggle();
|
|
14320
|
+
}
|
|
14010
14321
|
/**
|
|
14011
14322
|
* Enables noise cancellation for the microphone.
|
|
14012
14323
|
*
|
|
@@ -14167,15 +14478,25 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14167
14478
|
await this.enable();
|
|
14168
14479
|
}
|
|
14169
14480
|
}
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14481
|
+
if (isReactNative() && publish && canPublish) {
|
|
14482
|
+
// On RN the microphone is enabled/disabled optimistically before JOINED. Reconcile now
|
|
14483
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
14484
|
+
await this.reconcileOptimisticStatus();
|
|
14485
|
+
}
|
|
14486
|
+
else {
|
|
14487
|
+
const { mediaStream } = this.state;
|
|
14488
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
14489
|
+
await this.publishStream(mediaStream);
|
|
14490
|
+
}
|
|
14173
14491
|
}
|
|
14174
14492
|
}
|
|
14175
14493
|
getDevices() {
|
|
14176
14494
|
return getAudioDevices(this.call.tracer);
|
|
14177
14495
|
}
|
|
14178
|
-
getStream(constraints) {
|
|
14496
|
+
async getStream(constraints) {
|
|
14497
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14498
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14499
|
+
await this.call.ensureMediaFactory();
|
|
14179
14500
|
return getAudioStream(constraints, this.call.tracer);
|
|
14180
14501
|
}
|
|
14181
14502
|
doSetAudioBitrateProfile(profile) {
|
|
@@ -14384,6 +14705,9 @@ class ScreenShareManager extends AudioDeviceManager {
|
|
|
14384
14705
|
if (!this.state.audioEnabled) {
|
|
14385
14706
|
constraints.audio = false;
|
|
14386
14707
|
}
|
|
14708
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14709
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14710
|
+
await this.call.ensureMediaFactory();
|
|
14387
14711
|
const stream = await getScreenShareStream(constraints, this.call.tracer);
|
|
14388
14712
|
const [track] = stream.getVideoTracks();
|
|
14389
14713
|
const { contentHint } = this.state.settings || {};
|
|
@@ -14488,10 +14812,14 @@ class SpeakerManager {
|
|
|
14488
14812
|
this.devicePersistence = devicePreferences;
|
|
14489
14813
|
this.setup();
|
|
14490
14814
|
}
|
|
14491
|
-
apply(settings) {
|
|
14492
|
-
|
|
14815
|
+
async apply(settings) {
|
|
14816
|
+
if (isReactNative()) {
|
|
14817
|
+
this.applyRN(settings);
|
|
14818
|
+
return;
|
|
14819
|
+
}
|
|
14820
|
+
await this.applyWeb();
|
|
14493
14821
|
}
|
|
14494
|
-
applyWeb() {
|
|
14822
|
+
async applyWeb() {
|
|
14495
14823
|
const { enabled, storageKey } = this.devicePersistence;
|
|
14496
14824
|
if (!enabled)
|
|
14497
14825
|
return;
|
|
@@ -14503,8 +14831,22 @@ class SpeakerManager {
|
|
|
14503
14831
|
const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
|
|
14504
14832
|
? ''
|
|
14505
14833
|
: preference.selectedDeviceId;
|
|
14506
|
-
if (
|
|
14507
|
-
this.
|
|
14834
|
+
if (!nextDeviceId) {
|
|
14835
|
+
if (this.state.selectedDevice !== nextDeviceId) {
|
|
14836
|
+
this.select(nextDeviceId);
|
|
14837
|
+
}
|
|
14838
|
+
return;
|
|
14839
|
+
}
|
|
14840
|
+
const permissionState = await firstValueFrom(getAudioBrowserPermission(this.call.tracer).asStateObservable());
|
|
14841
|
+
if (permissionState !== 'granted')
|
|
14842
|
+
return;
|
|
14843
|
+
const devices = await firstValueFrom(this.listDevices());
|
|
14844
|
+
const device = this.findDevice(devices, nextDeviceId) ??
|
|
14845
|
+
(preference.selectedDeviceLabel
|
|
14846
|
+
? devices.find((d) => d.label === preference.selectedDeviceLabel)
|
|
14847
|
+
: undefined);
|
|
14848
|
+
if (device && this.state.selectedDevice !== device.deviceId) {
|
|
14849
|
+
this.select(device.deviceId);
|
|
14508
14850
|
}
|
|
14509
14851
|
}
|
|
14510
14852
|
applyRN(settings) {
|
|
@@ -14533,6 +14875,7 @@ class SpeakerManager {
|
|
|
14533
14875
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
14534
14876
|
defaultDevice,
|
|
14535
14877
|
isRingingTypeCall: this.call.ringing,
|
|
14878
|
+
cid: this.call.cid,
|
|
14536
14879
|
});
|
|
14537
14880
|
}
|
|
14538
14881
|
}
|
|
@@ -14541,11 +14884,15 @@ class SpeakerManager {
|
|
|
14541
14884
|
return;
|
|
14542
14885
|
this.areSubscriptionsSetUp = true;
|
|
14543
14886
|
if (deviceIds$ && !isReactNative()) {
|
|
14544
|
-
this.subscriptions.push(createSubscription(combineLatest([
|
|
14887
|
+
this.subscriptions.push(createSubscription(combineLatest([
|
|
14888
|
+
deviceIds$.pipe(pairwise()),
|
|
14889
|
+
this.state.selectedDevice$,
|
|
14890
|
+
]), ([[prevDevices, currentDevices], deviceId]) => {
|
|
14545
14891
|
if (!deviceId)
|
|
14546
14892
|
return;
|
|
14547
|
-
const
|
|
14548
|
-
|
|
14893
|
+
const isDisconnected = this.findDevice(prevDevices, deviceId) &&
|
|
14894
|
+
!this.findDevice(currentDevices, deviceId);
|
|
14895
|
+
if (isDisconnected)
|
|
14549
14896
|
this.select('');
|
|
14550
14897
|
}));
|
|
14551
14898
|
}
|
|
@@ -14592,6 +14939,7 @@ class SpeakerManager {
|
|
|
14592
14939
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
14593
14940
|
this.subscriptions = [];
|
|
14594
14941
|
this.areSubscriptionsSetUp = false;
|
|
14942
|
+
this.defaultDevice = undefined;
|
|
14595
14943
|
};
|
|
14596
14944
|
/**
|
|
14597
14945
|
* Set the volume of the audio elements
|
|
@@ -14626,6 +14974,7 @@ class SpeakerManager {
|
|
|
14626
14974
|
return { audioVolume: volume };
|
|
14627
14975
|
});
|
|
14628
14976
|
}
|
|
14977
|
+
findDevice = (devices, deviceId) => devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
|
|
14629
14978
|
persistSpeakerDevicePreference(selectedDevice) {
|
|
14630
14979
|
const { storageKey } = this.devicePersistence;
|
|
14631
14980
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
@@ -14708,6 +15057,7 @@ class Call {
|
|
|
14708
15057
|
blockedAudioTracker;
|
|
14709
15058
|
subscriber;
|
|
14710
15059
|
publisher;
|
|
15060
|
+
e2eeManager;
|
|
14711
15061
|
/**
|
|
14712
15062
|
* Flag telling whether this call is a "ringing" call.
|
|
14713
15063
|
*/
|
|
@@ -14763,6 +15113,8 @@ class Call {
|
|
|
14763
15113
|
allowOwnTracksLoopback = false;
|
|
14764
15114
|
hasJoinedOnce = false;
|
|
14765
15115
|
deviceSettingsAppliedOnce = false;
|
|
15116
|
+
callManagerStarted = false;
|
|
15117
|
+
leaveGeneration = 0;
|
|
14766
15118
|
credentials;
|
|
14767
15119
|
initialized = false;
|
|
14768
15120
|
acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
|
|
@@ -14781,6 +15133,13 @@ class Call {
|
|
|
14781
15133
|
clientCapabilities = new Set([
|
|
14782
15134
|
ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
|
|
14783
15135
|
]);
|
|
15136
|
+
/**
|
|
15137
|
+
* The in-flight per-call media engine. On web/React this resolves to a thin
|
|
15138
|
+
* globals-backed engine (no provider registered); React Native registers a
|
|
15139
|
+
* provider that owns a per-call native factory.
|
|
15140
|
+
* @internal
|
|
15141
|
+
*/
|
|
15142
|
+
mediaEnginePromise;
|
|
14784
15143
|
/**
|
|
14785
15144
|
* Constructs a new `Call` instance.
|
|
14786
15145
|
*
|
|
@@ -15050,6 +15409,7 @@ class Call {
|
|
|
15050
15409
|
if (callingState === CallingState.LEFT) {
|
|
15051
15410
|
return;
|
|
15052
15411
|
}
|
|
15412
|
+
this.leaveGeneration += 1;
|
|
15053
15413
|
if (callingState === CallingState.JOINING) {
|
|
15054
15414
|
const waitUntilCallJoined = () => {
|
|
15055
15415
|
return new Promise((resolve) => {
|
|
@@ -15136,12 +15496,14 @@ class Call {
|
|
|
15136
15496
|
this.clientStore.unregisterCall(this);
|
|
15137
15497
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15138
15498
|
isRingingTypeCall: this.ringing,
|
|
15499
|
+
shouldStopCallManager: this.callManagerStarted,
|
|
15139
15500
|
});
|
|
15140
15501
|
this.camera.dispose();
|
|
15141
15502
|
this.microphone.dispose();
|
|
15142
15503
|
this.screenShare.dispose();
|
|
15143
15504
|
this.speaker.dispose();
|
|
15144
15505
|
this.deviceSettingsAppliedOnce = false;
|
|
15506
|
+
this.callManagerStarted = false;
|
|
15145
15507
|
const stopOnLeavePromises = [];
|
|
15146
15508
|
if (this.camera.stopOnLeave) {
|
|
15147
15509
|
stopOnLeavePromises.push(this.camera.disable(true));
|
|
@@ -15153,6 +15515,22 @@ class Call {
|
|
|
15153
15515
|
stopOnLeavePromises.push(this.screenShare.disable(true));
|
|
15154
15516
|
}
|
|
15155
15517
|
await Promise.all(stopOnLeavePromises);
|
|
15518
|
+
// Dispose the per-call media engine last — after peer connections and
|
|
15519
|
+
// local tracks are gone — so the backing factory tears down with no
|
|
15520
|
+
// owned PCs/tracks. A fresh `join()` builds a new engine.
|
|
15521
|
+
if (this.mediaEnginePromise) {
|
|
15522
|
+
const enginePromise = this.mediaEnginePromise;
|
|
15523
|
+
this.mediaEnginePromise = undefined;
|
|
15524
|
+
this.logger.debug('Disposing per-call media factory');
|
|
15525
|
+
await enginePromise
|
|
15526
|
+
.then((engine) => {
|
|
15527
|
+
globalThis.streamRNVideoSDK?.callingX?.unwireAudioEngineSubscription();
|
|
15528
|
+
return engine.dispose();
|
|
15529
|
+
})
|
|
15530
|
+
.catch((err) => {
|
|
15531
|
+
this.logger.warn('Failed to dispose media engine', err);
|
|
15532
|
+
});
|
|
15533
|
+
}
|
|
15156
15534
|
});
|
|
15157
15535
|
};
|
|
15158
15536
|
/**
|
|
@@ -15253,11 +15631,7 @@ class Call {
|
|
|
15253
15631
|
this.clientStore.registerOrUpdateCall(this);
|
|
15254
15632
|
}
|
|
15255
15633
|
// Skip speaker setup on RN if ringing was requested or the call is already ringing
|
|
15256
|
-
const skipSpeakerApply = isReactNative()
|
|
15257
|
-
? params?.ring === true
|
|
15258
|
-
? true
|
|
15259
|
-
: this.ringing
|
|
15260
|
-
: false;
|
|
15634
|
+
const skipSpeakerApply = isReactNative();
|
|
15261
15635
|
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15262
15636
|
return response;
|
|
15263
15637
|
};
|
|
@@ -15280,11 +15654,7 @@ class Call {
|
|
|
15280
15654
|
this.clientStore.registerOrUpdateCall(this);
|
|
15281
15655
|
}
|
|
15282
15656
|
// Skip speaker setup on RN if ringing was requested or the call is already ringing
|
|
15283
|
-
const skipSpeakerApply = isReactNative()
|
|
15284
|
-
? data?.ring === true
|
|
15285
|
-
? true
|
|
15286
|
-
: this.ringing
|
|
15287
|
-
: false;
|
|
15657
|
+
const skipSpeakerApply = isReactNative();
|
|
15288
15658
|
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15289
15659
|
return response;
|
|
15290
15660
|
};
|
|
@@ -15433,9 +15803,20 @@ class Call {
|
|
|
15433
15803
|
doJoin = async (data) => {
|
|
15434
15804
|
const connectStartTime = Date.now();
|
|
15435
15805
|
const callingState = this.state.callingState;
|
|
15806
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
15807
|
+
const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
|
|
15436
15808
|
this.joinCallData = data;
|
|
15437
15809
|
this.logger.debug('Starting join flow');
|
|
15438
15810
|
this.state.setCallingState(CallingState.JOINING);
|
|
15811
|
+
// Ensure the per-call media engine exists before any peer connection
|
|
15812
|
+
// (codec probe, subscriber, publisher) or capture happens, so the WebRTC
|
|
15813
|
+
// globals resolve to the call's factory. Idempotent across
|
|
15814
|
+
// reconnect/migration attempts.
|
|
15815
|
+
await this.ensureMediaFactory();
|
|
15816
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
15817
|
+
if (callingX) {
|
|
15818
|
+
callingX.wireAudioEngineSubscription();
|
|
15819
|
+
}
|
|
15439
15820
|
const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
15440
15821
|
const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
15441
15822
|
const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
@@ -15490,6 +15871,11 @@ class Call {
|
|
|
15490
15871
|
// these are throw-away SDPs that the SFU will use to determine
|
|
15491
15872
|
// the capabilities of the client (codec support, etc.)
|
|
15492
15873
|
const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
15874
|
+
// skip if a leave superseded this join so codec detection doesn't resolve to a default factory.
|
|
15875
|
+
if (supersededByLeave()) {
|
|
15876
|
+
this.logger.debug('Join superseded by leave; skipping codec detection');
|
|
15877
|
+
return;
|
|
15878
|
+
}
|
|
15493
15879
|
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
15494
15880
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
15495
15881
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
@@ -15533,6 +15919,12 @@ class Call {
|
|
|
15533
15919
|
throw error;
|
|
15534
15920
|
}
|
|
15535
15921
|
}
|
|
15922
|
+
// If the user left while this join was in flight, bail before re-setting JOINED and before
|
|
15923
|
+
// peer-connection setup below (both run synchronously after this, so one check covers them).
|
|
15924
|
+
if (supersededByLeave()) {
|
|
15925
|
+
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
15926
|
+
return;
|
|
15927
|
+
}
|
|
15536
15928
|
if (!performingMigration) {
|
|
15537
15929
|
// in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
|
|
15538
15930
|
this.state.setCallingState(CallingState.JOINED);
|
|
@@ -15570,12 +15962,18 @@ class Call {
|
|
|
15570
15962
|
}
|
|
15571
15963
|
// device settings should be applied only once, we don't have to
|
|
15572
15964
|
// re-apply them on later reconnections or server-side data fetches
|
|
15573
|
-
if (!this.deviceSettingsAppliedOnce &&
|
|
15965
|
+
if (!this.deviceSettingsAppliedOnce &&
|
|
15966
|
+
this.state.settings &&
|
|
15967
|
+
!supersededByLeave()) {
|
|
15574
15968
|
await this.applyDeviceConfig(this.state.settings, true, false);
|
|
15969
|
+
this.deviceSettingsAppliedOnce = true;
|
|
15970
|
+
}
|
|
15971
|
+
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
15575
15972
|
globalThis.streamRNVideoSDK?.callManager.start({
|
|
15576
15973
|
isRingingTypeCall: this.ringing,
|
|
15974
|
+
cid: this.cid,
|
|
15577
15975
|
});
|
|
15578
|
-
this.
|
|
15976
|
+
this.callManagerStarted = true;
|
|
15579
15977
|
}
|
|
15580
15978
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
15581
15979
|
// as it's a one-time event and clashes with the potential reconnection attempts.
|
|
@@ -15702,6 +16100,7 @@ class Call {
|
|
|
15702
16100
|
this.sfuStatsReporter = undefined;
|
|
15703
16101
|
if (closePreviousInstances && this.subscriber) {
|
|
15704
16102
|
await this.subscriber.dispose();
|
|
16103
|
+
this.state.removeAllOrphanedTracks();
|
|
15705
16104
|
}
|
|
15706
16105
|
const basePeerConnectionOptions = {
|
|
15707
16106
|
sfuClient,
|
|
@@ -15712,6 +16111,7 @@ class Call {
|
|
|
15712
16111
|
enableTracing,
|
|
15713
16112
|
statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
|
|
15714
16113
|
clientPublishOptions: this.clientPublishOptions,
|
|
16114
|
+
e2ee: this.e2eeManager,
|
|
15715
16115
|
onReconnectionNeeded: (kind, reason, peerType) => {
|
|
15716
16116
|
this.reconnect(kind, reason).catch((err) => {
|
|
15717
16117
|
const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
|
|
@@ -15777,12 +16177,12 @@ class Call {
|
|
|
15777
16177
|
* Retrieves credentials for joining the call.
|
|
15778
16178
|
*
|
|
15779
16179
|
* @internal
|
|
15780
|
-
*
|
|
15781
16180
|
* @param data the join call data.
|
|
15782
16181
|
*/
|
|
15783
16182
|
doJoinRequest = async (data) => {
|
|
15784
16183
|
const location = await this.streamClient.getLocationHint();
|
|
15785
|
-
const
|
|
16184
|
+
const e2ee = !!this.e2eeManager;
|
|
16185
|
+
const request = { ...data, location, e2ee };
|
|
15786
16186
|
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
15787
16187
|
this.state.updateFromCallResponse(joinResponse.call);
|
|
15788
16188
|
this.state.setMembers(joinResponse.members);
|
|
@@ -15801,6 +16201,36 @@ class Call {
|
|
|
15801
16201
|
}
|
|
15802
16202
|
return joinResponse;
|
|
15803
16203
|
};
|
|
16204
|
+
/**
|
|
16205
|
+
* Whether the per-call media engine currently exists. True from join until leave.
|
|
16206
|
+
*
|
|
16207
|
+
* @internal an internal getter and should not be used outside the SDK.
|
|
16208
|
+
*/
|
|
16209
|
+
get hasMediaEngine() {
|
|
16210
|
+
return !!this.mediaEnginePromise;
|
|
16211
|
+
}
|
|
16212
|
+
/**
|
|
16213
|
+
* Ensures a {@link CallMediaEngine} exists for this call's media session and
|
|
16214
|
+
* returns it. Idempotent: the engine is created once via the registered
|
|
16215
|
+
* provider (see `setCallMediaEngineProvider`) and cached until `leave()`
|
|
16216
|
+
* disposes it. Concurrent callers (e.g. camera + microphone enabling in
|
|
16217
|
+
* parallel) share the same engine because the in-flight creation promise is
|
|
16218
|
+
* cached, never the unresolved result.
|
|
16219
|
+
*
|
|
16220
|
+
* @internal
|
|
16221
|
+
*/
|
|
16222
|
+
ensureMediaFactory = async () => {
|
|
16223
|
+
if (!this.mediaEnginePromise) {
|
|
16224
|
+
const provider = getCallMediaEngineProvider();
|
|
16225
|
+
this.logger.debug(`Requesting per-call media factory creation`);
|
|
16226
|
+
this.mediaEnginePromise = Promise.resolve(provider()).catch((err) => {
|
|
16227
|
+
// Drop the cached rejection so a retried join() can rebuild the engine
|
|
16228
|
+
this.mediaEnginePromise = undefined;
|
|
16229
|
+
throw err;
|
|
16230
|
+
});
|
|
16231
|
+
}
|
|
16232
|
+
return this.mediaEnginePromise;
|
|
16233
|
+
};
|
|
15804
16234
|
/**
|
|
15805
16235
|
* Handles the closing of the SFU signal connection.
|
|
15806
16236
|
*
|
|
@@ -16362,6 +16792,30 @@ class Call {
|
|
|
16362
16792
|
this.tracer.trace('updatePublishOptions', options);
|
|
16363
16793
|
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
16364
16794
|
};
|
|
16795
|
+
/**
|
|
16796
|
+
* Set the E2EE (end-to-end encryption) manager for this call.
|
|
16797
|
+
*
|
|
16798
|
+
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
16799
|
+
* configured for E2EE.
|
|
16800
|
+
*
|
|
16801
|
+
* The manager is kept across {@link leave} so a rejoin of this same instance
|
|
16802
|
+
* stays encrypted: do not dispose it while this call may be joined again.
|
|
16803
|
+
* A disposed manager throws from `encrypt`/`decrypt` rather than silently
|
|
16804
|
+
* publishing nothing, so re-attach a fresh one instead of reusing it.
|
|
16805
|
+
*
|
|
16806
|
+
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
16807
|
+
* built-in AES-GCM scheme, or pass your own implementation.
|
|
16808
|
+
* @throws if called after the peer connections have been built (i.e. after
|
|
16809
|
+
* `join`): those PCs were already configured without an encryptor, so
|
|
16810
|
+
* adopting a manager now would silently publish/receive cleartext for
|
|
16811
|
+
* the live session.
|
|
16812
|
+
*/
|
|
16813
|
+
setE2EEManager = (e2ee) => {
|
|
16814
|
+
if (this.publisher || this.subscriber) {
|
|
16815
|
+
throw new Error('setE2EEManager must be called before join()');
|
|
16816
|
+
}
|
|
16817
|
+
this.e2eeManager = e2ee;
|
|
16818
|
+
};
|
|
16365
16819
|
/**
|
|
16366
16820
|
* Notifies the SFU that a noise cancellation process has started.
|
|
16367
16821
|
*
|
|
@@ -16998,7 +17452,9 @@ class Call {
|
|
|
16998
17452
|
*/
|
|
16999
17453
|
applyDeviceConfig = async (settings, publish, skipSpeakerApply) => {
|
|
17000
17454
|
if (!skipSpeakerApply) {
|
|
17001
|
-
this.speaker.apply(settings)
|
|
17455
|
+
await this.speaker.apply(settings).catch((err) => {
|
|
17456
|
+
this.logger.warn('Speaker init failed', err);
|
|
17457
|
+
});
|
|
17002
17458
|
}
|
|
17003
17459
|
await this.camera.apply(settings.video, publish).catch((err) => {
|
|
17004
17460
|
this.logger.warn('Camera init failed', err);
|
|
@@ -17677,15 +18133,17 @@ class StableWSConnection {
|
|
|
17677
18133
|
this.client.logger.error(`Token not set, can't connect authenticate`);
|
|
17678
18134
|
return;
|
|
17679
18135
|
}
|
|
17680
|
-
const
|
|
18136
|
+
const wsAuthMessage = {
|
|
17681
18137
|
token,
|
|
18138
|
+
products: [WSAuthMessageProductsEnum.VIDEO],
|
|
17682
18139
|
user_details: {
|
|
17683
18140
|
id: user.id,
|
|
17684
18141
|
name: user.name,
|
|
17685
18142
|
image: user.image,
|
|
17686
18143
|
custom: user.custom,
|
|
17687
18144
|
},
|
|
17688
|
-
}
|
|
18145
|
+
};
|
|
18146
|
+
const authMessage = JSON.stringify(wsAuthMessage);
|
|
17689
18147
|
this._log(`onopen() - Sending auth message ${authMessage}`, {}, 'trace');
|
|
17690
18148
|
this.ws?.send(authMessage);
|
|
17691
18149
|
this._log('onopen() - onopen callback', { wsID });
|
|
@@ -18511,11 +18969,11 @@ class StreamClient {
|
|
|
18511
18969
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18512
18970
|
};
|
|
18513
18971
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18514
|
-
"1.
|
|
18972
|
+
"1.58.0";
|
|
18515
18973
|
getUserAgent = () => {
|
|
18516
18974
|
if (!this.cachedUserAgent) {
|
|
18517
18975
|
const { clientAppIdentifier = {} } = this.options;
|
|
18518
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
18976
|
+
const { sdkName = 'js', sdkVersion = "1.58.0", ...extras } = clientAppIdentifier;
|
|
18519
18977
|
this.cachedUserAgent = [
|
|
18520
18978
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18521
18979
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19748,5 +20206,356 @@ const humanize = (n) => {
|
|
|
19748
20206
|
return String(n);
|
|
19749
20207
|
};
|
|
19750
20208
|
|
|
19751
|
-
|
|
20209
|
+
/**
|
|
20210
|
+
* Tiny, type-safe event emitter.
|
|
20211
|
+
*
|
|
20212
|
+
* Usage styles:
|
|
20213
|
+
* - compose: `private readonly events = new TypedEventEmitter<MyMap>()`
|
|
20214
|
+
* - extend: `class Foo extends TypedEventEmitter<MyMap> {}`
|
|
20215
|
+
*
|
|
20216
|
+
* Listener exceptions (sync throw or rejected promise) are caught and logged,
|
|
20217
|
+
* so one bad listener cannot break dispatch for the rest.
|
|
20218
|
+
*/
|
|
20219
|
+
class TypedEventEmitter {
|
|
20220
|
+
logger;
|
|
20221
|
+
byEvent = new Map();
|
|
20222
|
+
constructor(logger = 'TypedEventEmitter') {
|
|
20223
|
+
this.logger =
|
|
20224
|
+
typeof logger === 'string' ? videoLoggerSystem.getLogger(logger) : logger;
|
|
20225
|
+
}
|
|
20226
|
+
on = (event, fn) => {
|
|
20227
|
+
let listeners = this.byEvent.get(event);
|
|
20228
|
+
if (!listeners) {
|
|
20229
|
+
listeners = new Set();
|
|
20230
|
+
this.byEvent.set(event, listeners);
|
|
20231
|
+
}
|
|
20232
|
+
listeners.add(fn);
|
|
20233
|
+
return () => this.off(event, fn);
|
|
20234
|
+
};
|
|
20235
|
+
off = (event, fn) => {
|
|
20236
|
+
const listeners = this.byEvent.get(event);
|
|
20237
|
+
if (!listeners)
|
|
20238
|
+
return;
|
|
20239
|
+
listeners.delete(fn);
|
|
20240
|
+
if (listeners.size === 0)
|
|
20241
|
+
this.byEvent.delete(event);
|
|
20242
|
+
};
|
|
20243
|
+
emit = (event, payload) => {
|
|
20244
|
+
const listeners = this.byEvent.get(event);
|
|
20245
|
+
if (!listeners || listeners.size === 0)
|
|
20246
|
+
return;
|
|
20247
|
+
for (const listener of [...listeners]) {
|
|
20248
|
+
this.invoke(() => listener(payload), event);
|
|
20249
|
+
}
|
|
20250
|
+
};
|
|
20251
|
+
removeAllListeners = (event) => {
|
|
20252
|
+
if (event === undefined) {
|
|
20253
|
+
this.byEvent.clear();
|
|
20254
|
+
}
|
|
20255
|
+
else {
|
|
20256
|
+
this.byEvent.delete(event);
|
|
20257
|
+
}
|
|
20258
|
+
};
|
|
20259
|
+
invoke = (run, event) => {
|
|
20260
|
+
try {
|
|
20261
|
+
const result = run();
|
|
20262
|
+
if (result && typeof result.then === 'function') {
|
|
20263
|
+
result.catch((err) => {
|
|
20264
|
+
this.logger.warn(`Listener for '${String(event)}' rejected`, err);
|
|
20265
|
+
});
|
|
20266
|
+
}
|
|
20267
|
+
}
|
|
20268
|
+
catch (err) {
|
|
20269
|
+
this.logger.warn(`Listener for '${String(event)}' threw`, err);
|
|
20270
|
+
}
|
|
20271
|
+
};
|
|
20272
|
+
}
|
|
20273
|
+
|
|
20274
|
+
/**
|
|
20275
|
+
* Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
|
|
20276
|
+
* transforms to RTCRtpSenders and RTCRtpReceivers.
|
|
20277
|
+
*/
|
|
20278
|
+
class EncryptionManager extends TypedEventEmitter {
|
|
20279
|
+
algorithm;
|
|
20280
|
+
transform;
|
|
20281
|
+
disposed = false;
|
|
20282
|
+
piped;
|
|
20283
|
+
userId;
|
|
20284
|
+
worker;
|
|
20285
|
+
workerUrl;
|
|
20286
|
+
constructor(userId, worker, workerUrl, algorithm, transform) {
|
|
20287
|
+
super('EncryptionManager');
|
|
20288
|
+
this.userId = userId;
|
|
20289
|
+
this.worker = worker;
|
|
20290
|
+
this.workerUrl = workerUrl;
|
|
20291
|
+
this.algorithm = algorithm;
|
|
20292
|
+
this.transform = transform;
|
|
20293
|
+
this.worker.addEventListener('message', this.handleWorkerMessage);
|
|
20294
|
+
this.worker.addEventListener('error', this.handleWorkerError);
|
|
20295
|
+
}
|
|
20296
|
+
/**
|
|
20297
|
+
* Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
|
|
20298
|
+
* Use it to guard UI, or to avoid calling {@link create} where it would throw.
|
|
20299
|
+
*/
|
|
20300
|
+
static isSupported = () => {
|
|
20301
|
+
return preferredTransform() !== undefined;
|
|
20302
|
+
};
|
|
20303
|
+
/**
|
|
20304
|
+
* Create an EncryptionManager instance and initialize the E2EE Web Worker.
|
|
20305
|
+
*
|
|
20306
|
+
* @param userId - The local user's ID, normally `call.currentUserId`.
|
|
20307
|
+
* @param options - the create options.
|
|
20308
|
+
* @throws {Error} If the browser lacks Encoded Transforms.
|
|
20309
|
+
*
|
|
20310
|
+
* @example
|
|
20311
|
+
* ```ts
|
|
20312
|
+
* if (EncryptionManager.isSupported()) {
|
|
20313
|
+
* const e2ee = await EncryptionManager.create(call.currentUserId);
|
|
20314
|
+
* call.setE2EEManager(e2ee);
|
|
20315
|
+
* e2ee.setSharedKey(0, keyBytes);
|
|
20316
|
+
* }
|
|
20317
|
+
*/
|
|
20318
|
+
static create = async (userId, options) => {
|
|
20319
|
+
const transform = preferredTransform();
|
|
20320
|
+
if (!transform) {
|
|
20321
|
+
throw new Error(`E2EE is not supported in this browser`);
|
|
20322
|
+
}
|
|
20323
|
+
const { e2eeWorker } = await Promise.resolve().then(function () { return e2eeWorker$1; });
|
|
20324
|
+
const blob = new Blob([`(${e2eeWorker.toString()})()`], {
|
|
20325
|
+
type: 'application/javascript',
|
|
20326
|
+
});
|
|
20327
|
+
const url = URL.createObjectURL(blob);
|
|
20328
|
+
let worker;
|
|
20329
|
+
try {
|
|
20330
|
+
worker = new Worker(url, { name: 'stream-video-e2ee' });
|
|
20331
|
+
}
|
|
20332
|
+
catch (err) {
|
|
20333
|
+
// e.g. a CSP `worker-src` without `blob:`. Don't leak the object URL.
|
|
20334
|
+
URL.revokeObjectURL(url);
|
|
20335
|
+
throw err;
|
|
20336
|
+
}
|
|
20337
|
+
const algorithm = options?.algorithm ?? 'AES-128-GCM';
|
|
20338
|
+
return new EncryptionManager(userId, worker, url, algorithm, transform);
|
|
20339
|
+
};
|
|
20340
|
+
/**
|
|
20341
|
+
* {@link dispose} terminates the worker, so `postMessage` becomes a silent
|
|
20342
|
+
* no-op and an attached transform points at a dead worker: frames would stall
|
|
20343
|
+
* forever with no error and no event. Throwing is also fail-closed, since a
|
|
20344
|
+
* caller that swallows it still publishes nothing rather than cleartext.
|
|
20345
|
+
*/
|
|
20346
|
+
assertUsable = () => {
|
|
20347
|
+
if (this.disposed)
|
|
20348
|
+
throw new Error(`EncryptionManager is disposed`);
|
|
20349
|
+
};
|
|
20350
|
+
/**
|
|
20351
|
+
* Terminate the worker and release all resources.
|
|
20352
|
+
*
|
|
20353
|
+
* The manager is unusable afterwards and every other method throws. Call
|
|
20354
|
+
* {@link create} for a new one. Safe to call more than once.
|
|
20355
|
+
*/
|
|
20356
|
+
dispose = () => {
|
|
20357
|
+
if (this.disposed)
|
|
20358
|
+
return;
|
|
20359
|
+
this.disposed = true;
|
|
20360
|
+
this.piped = undefined;
|
|
20361
|
+
this.worker.removeEventListener('message', this.handleWorkerMessage);
|
|
20362
|
+
this.worker.removeEventListener('error', this.handleWorkerError);
|
|
20363
|
+
this.worker.terminate();
|
|
20364
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
20365
|
+
this.removeAllListeners();
|
|
20366
|
+
};
|
|
20367
|
+
/**
|
|
20368
|
+
* Set a per-user AES-GCM encryption key in the worker's key store.
|
|
20369
|
+
*
|
|
20370
|
+
* Use it when each participant has their own key from a central authority.
|
|
20371
|
+
* The receiver picks the right one by the `keyIndex` in the frame trailer.
|
|
20372
|
+
*
|
|
20373
|
+
* @param userId - The key owner.
|
|
20374
|
+
* @param keyIndex - Increases with each rotation.
|
|
20375
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20376
|
+
*/
|
|
20377
|
+
setKey = (userId, keyIndex, rawKey) => {
|
|
20378
|
+
this.assertUsable();
|
|
20379
|
+
this.validateKeyIndex(keyIndex);
|
|
20380
|
+
this.validateKeyLength(rawKey);
|
|
20381
|
+
this.worker.postMessage({ type: 'cmd.set_key', userId, keyIndex, rawKey });
|
|
20382
|
+
};
|
|
20383
|
+
/**
|
|
20384
|
+
* Fallback key for any user without a per-user key. The simplest E2EE mode:
|
|
20385
|
+
* one key for everyone, usually passphrase-derived, no distribution needed.
|
|
20386
|
+
* Setting an epoch makes it active for encryption while older epochs remain
|
|
20387
|
+
* available to decrypt in-flight frames until {@link removeSharedKey}.
|
|
20388
|
+
*
|
|
20389
|
+
* @param keyIndex - An integer 0-255, since one trailer byte carries it.
|
|
20390
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20391
|
+
*/
|
|
20392
|
+
setSharedKey = (keyIndex, rawKey) => {
|
|
20393
|
+
this.assertUsable();
|
|
20394
|
+
this.validateKeyIndex(keyIndex);
|
|
20395
|
+
this.validateKeyLength(rawKey);
|
|
20396
|
+
this.worker.postMessage({ type: 'cmd.set_shared_key', keyIndex, rawKey });
|
|
20397
|
+
};
|
|
20398
|
+
/**
|
|
20399
|
+
* Remove one shared-key epoch from the worker's receive key ring.
|
|
20400
|
+
*
|
|
20401
|
+
* If this is the active epoch, shared-key encryption stops until
|
|
20402
|
+
* {@link setSharedKey} succeeds again. An older epoch is not reactivated.
|
|
20403
|
+
*
|
|
20404
|
+
* @param keyIndex - The exact shared-key epoch to remove.
|
|
20405
|
+
*/
|
|
20406
|
+
removeSharedKey = (keyIndex) => {
|
|
20407
|
+
this.assertUsable();
|
|
20408
|
+
this.validateKeyIndex(keyIndex);
|
|
20409
|
+
this.worker.postMessage({ type: 'cmd.remove_shared_key', keyIndex });
|
|
20410
|
+
};
|
|
20411
|
+
/**
|
|
20412
|
+
* Retire one of a user's key epochs, leaving their other epochs usable.
|
|
20413
|
+
*
|
|
20414
|
+
* @param userId - The key owner.
|
|
20415
|
+
* @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
|
|
20416
|
+
*/
|
|
20417
|
+
removeKey = (userId, keyIndex) => {
|
|
20418
|
+
this.assertUsable();
|
|
20419
|
+
this.validateKeyIndex(keyIndex);
|
|
20420
|
+
this.worker.postMessage({ type: 'cmd.remove_key', userId, keyIndex });
|
|
20421
|
+
};
|
|
20422
|
+
/**
|
|
20423
|
+
* Drop every key a user holds, revoking their ability to decrypt later
|
|
20424
|
+
* frames. Call it when a participant leaves.
|
|
20425
|
+
*
|
|
20426
|
+
* To retire one rotated epoch instead, use {@link removeKey}.
|
|
20427
|
+
*/
|
|
20428
|
+
removeAllKeys = (userId) => {
|
|
20429
|
+
this.assertUsable();
|
|
20430
|
+
this.worker.postMessage({ type: 'cmd.remove_all_keys', userId });
|
|
20431
|
+
};
|
|
20432
|
+
/**
|
|
20433
|
+
* Called by the Publisher when it adds a transceiver.
|
|
20434
|
+
*
|
|
20435
|
+
* @param sender - The sender to encrypt.
|
|
20436
|
+
* @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
|
|
20437
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20438
|
+
* @internal
|
|
20439
|
+
*/
|
|
20440
|
+
encrypt = (sender, codec, trackType) => {
|
|
20441
|
+
this.assertUsable();
|
|
20442
|
+
this.pipe(sender, {
|
|
20443
|
+
operation: 'encode',
|
|
20444
|
+
userId: this.userId,
|
|
20445
|
+
codec,
|
|
20446
|
+
trackType,
|
|
20447
|
+
});
|
|
20448
|
+
};
|
|
20449
|
+
/**
|
|
20450
|
+
* Called by the Subscriber when a remote track arrives.
|
|
20451
|
+
*
|
|
20452
|
+
* @param receiver - The receiver to decrypt.
|
|
20453
|
+
* @param userId - The remote user, for key lookup in the worker.
|
|
20454
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20455
|
+
* @internal
|
|
20456
|
+
*/
|
|
20457
|
+
decrypt = (receiver, userId, trackType) => {
|
|
20458
|
+
this.assertUsable();
|
|
20459
|
+
this.pipe(receiver, { operation: 'decode', userId, trackType });
|
|
20460
|
+
};
|
|
20461
|
+
/** Pipe through the worker's transform, tracking targets to avoid double-piping. */
|
|
20462
|
+
pipe = (target, options) => {
|
|
20463
|
+
if (this.transform === 'script') {
|
|
20464
|
+
target.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
20465
|
+
return;
|
|
20466
|
+
}
|
|
20467
|
+
if ((this.piped ??= new WeakSet()).has(target))
|
|
20468
|
+
return;
|
|
20469
|
+
this.piped.add(target);
|
|
20470
|
+
// @ts-expect-error createEncodedStreams is not in the standard typedefs
|
|
20471
|
+
const { readable, writable } = target.createEncodedStreams();
|
|
20472
|
+
this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
|
|
20473
|
+
};
|
|
20474
|
+
/**
|
|
20475
|
+
* Toggle periodic performance reporting from the E2EE worker.
|
|
20476
|
+
*
|
|
20477
|
+
* While on, the worker emits `e2ee.perf_report` once per second with per-track
|
|
20478
|
+
* FPS and crypto timings. Useful for debugging throughput.
|
|
20479
|
+
*/
|
|
20480
|
+
enablePerformanceReporting = (enabled) => {
|
|
20481
|
+
this.assertUsable();
|
|
20482
|
+
this.worker.postMessage({
|
|
20483
|
+
type: 'cmd.enable_performance_reporting',
|
|
20484
|
+
enabled,
|
|
20485
|
+
});
|
|
20486
|
+
};
|
|
20487
|
+
/**
|
|
20488
|
+
* Request a snapshot of the worker's keys. It arrives later as the
|
|
20489
|
+
* `e2ee.key_state` event, listing fingerprints only, never key material.
|
|
20490
|
+
*/
|
|
20491
|
+
requestKeyState = () => {
|
|
20492
|
+
this.assertUsable();
|
|
20493
|
+
this.worker.postMessage({ type: 'cmd.request_key_state' });
|
|
20494
|
+
};
|
|
20495
|
+
handleWorkerMessage = (e) => {
|
|
20496
|
+
const { type, ...payload } = e.data ?? {};
|
|
20497
|
+
if (type === 'e2ee.error') {
|
|
20498
|
+
this.logger.error(e.data.message);
|
|
20499
|
+
return;
|
|
20500
|
+
}
|
|
20501
|
+
const event = type;
|
|
20502
|
+
this.logger.debug('Dispatching', event, payload);
|
|
20503
|
+
this.emit(event, payload);
|
|
20504
|
+
};
|
|
20505
|
+
handleWorkerError = (e) => {
|
|
20506
|
+
this.logger.error('Unhandled worker error:', e.message);
|
|
20507
|
+
};
|
|
20508
|
+
validateKeyLength = (rawKey) => {
|
|
20509
|
+
const is256 = this.algorithm === 'AES-256-GCM';
|
|
20510
|
+
const expected = is256 ? 32 : 16;
|
|
20511
|
+
if (rawKey.byteLength !== expected) {
|
|
20512
|
+
throw new Error(`Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`);
|
|
20513
|
+
}
|
|
20514
|
+
};
|
|
20515
|
+
/**
|
|
20516
|
+
* One trailer byte carries the keyIndex. A larger value would truncate to
|
|
20517
|
+
* `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
|
|
20518
|
+
* every decrypt. Reject it rather than ship a silently broken key epoch.
|
|
20519
|
+
*/
|
|
20520
|
+
validateKeyIndex = (keyIndex) => {
|
|
20521
|
+
if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
|
|
20522
|
+
throw new Error(`keyIndex must be an integer between 0 and 255, got ${keyIndex}`);
|
|
20523
|
+
}
|
|
20524
|
+
};
|
|
20525
|
+
}
|
|
20526
|
+
|
|
20527
|
+
/**
|
|
20528
|
+
* E2EE worker. The build populates it.
|
|
20529
|
+
*
|
|
20530
|
+
* The actual implementation lives in `e2ee-worker/e2ee-worker-impl.ts` and its
|
|
20531
|
+
* sibling modules (`constants.ts`, `codec.ts`, `crypto.ts`, `utils.ts`,
|
|
20532
|
+
* `types.ts`), all written as real TypeScript with full type checking
|
|
20533
|
+
* and import support.
|
|
20534
|
+
*
|
|
20535
|
+
* ## How it works
|
|
20536
|
+
*
|
|
20537
|
+
* 1. You edit files in `e2ee-worker/` as normal TypeScript
|
|
20538
|
+
* 2. `yarn build` runs Rollup, which invokes `rollup-plugin-inline-worker`
|
|
20539
|
+
* 3. The plugin bundles `e2ee-worker/e2ee-worker-impl.ts` (and its imports)
|
|
20540
|
+
* into a single function via a nested Rollup + TypeScript build
|
|
20541
|
+
* 4. The function replaces this file's export at build time
|
|
20542
|
+
* 5. `EncryptionManager.create()` creates a Worker from `e2eeWorker.toString()`
|
|
20543
|
+
*
|
|
20544
|
+
* In tests, this module is mocked (see `__tests__/EncryptionManager.test.ts`),
|
|
20545
|
+
* so the placeholder is never evaluated.
|
|
20546
|
+
*
|
|
20547
|
+
* @see e2ee-worker/e2ee-worker-impl.ts - the entry point
|
|
20548
|
+
* @see ../../plugins/rollup-plugin-inline-worker.ts - the Rollup plugin
|
|
20549
|
+
* @see EncryptionManager.ts - consumes e2eeWorker
|
|
20550
|
+
*/
|
|
20551
|
+
function e2eeWorker() {
|
|
20552
|
+
// will be populated at build time
|
|
20553
|
+
}
|
|
20554
|
+
|
|
20555
|
+
var e2eeWorker$1 = /*#__PURE__*/Object.freeze({
|
|
20556
|
+
__proto__: null,
|
|
20557
|
+
e2eeWorker: e2eeWorker
|
|
20558
|
+
});
|
|
20559
|
+
|
|
20560
|
+
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
|
|
19752
20561
|
//# sourceMappingURL=index.es.js.map
|