@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.browser.es.js
CHANGED
|
@@ -67,6 +67,22 @@ const CreateDeviceRequestPushProviderEnum = {
|
|
|
67
67
|
HUAWEI: 'huawei',
|
|
68
68
|
XIAOMI: 'xiaomi',
|
|
69
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* @export
|
|
72
|
+
*/
|
|
73
|
+
const EncryptionSettingsRequestModeEnum = {
|
|
74
|
+
AVAILABLE: 'available',
|
|
75
|
+
DISABLED: 'disabled',
|
|
76
|
+
AUTO_ON: 'auto-on',
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* @export
|
|
80
|
+
*/
|
|
81
|
+
const EncryptionSettingsResponseModeEnum = {
|
|
82
|
+
AVAILABLE: 'available',
|
|
83
|
+
DISABLED: 'disabled',
|
|
84
|
+
AUTO_ON: 'auto-on',
|
|
85
|
+
};
|
|
70
86
|
/**
|
|
71
87
|
* @export
|
|
72
88
|
*/
|
|
@@ -93,6 +109,21 @@ const FrameRecordingSettingsResponseModeEnum = {
|
|
|
93
109
|
DISABLED: 'disabled',
|
|
94
110
|
AUTO_ON: 'auto-on',
|
|
95
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* @export
|
|
114
|
+
*/
|
|
115
|
+
const HLSSettingsRequestQualityTracksEnum = {
|
|
116
|
+
_360P: '360p',
|
|
117
|
+
_480P: '480p',
|
|
118
|
+
_720P: '720p',
|
|
119
|
+
_1080P: '1080p',
|
|
120
|
+
_1440P: '1440p',
|
|
121
|
+
PORTRAIT_360X640: 'portrait-360x640',
|
|
122
|
+
PORTRAIT_480X854: 'portrait-480x854',
|
|
123
|
+
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
124
|
+
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
125
|
+
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
126
|
+
};
|
|
96
127
|
/**
|
|
97
128
|
* @export
|
|
98
129
|
*/
|
|
@@ -101,6 +132,17 @@ const IndividualRecordingSettingsRequestModeEnum = {
|
|
|
101
132
|
DISABLED: 'disabled',
|
|
102
133
|
AUTO_ON: 'auto-on',
|
|
103
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* @export
|
|
137
|
+
*/
|
|
138
|
+
const IndividualRecordingSettingsRequestOutputTypesEnum = {
|
|
139
|
+
AUDIO_ONLY: 'audio_only',
|
|
140
|
+
VIDEO_ONLY: 'video_only',
|
|
141
|
+
AUDIO_VIDEO: 'audio_video',
|
|
142
|
+
SCREENSHARE_AUDIO_ONLY: 'screenshare_audio_only',
|
|
143
|
+
SCREENSHARE_VIDEO_ONLY: 'screenshare_video_only',
|
|
144
|
+
SCREENSHARE_AUDIO_VIDEO: 'screenshare_audio_video',
|
|
145
|
+
};
|
|
104
146
|
/**
|
|
105
147
|
* @export
|
|
106
148
|
*/
|
|
@@ -259,6 +301,14 @@ const RecordSettingsRequestQualityEnum = {
|
|
|
259
301
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
260
302
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
261
303
|
};
|
|
304
|
+
/**
|
|
305
|
+
* @export
|
|
306
|
+
*/
|
|
307
|
+
const RequestPermissionRequestPermissionsEnum = {
|
|
308
|
+
SCREENSHARE: 'screenshare',
|
|
309
|
+
SEND_AUDIO: 'send-audio',
|
|
310
|
+
SEND_VIDEO: 'send-video',
|
|
311
|
+
};
|
|
262
312
|
/**
|
|
263
313
|
* @export
|
|
264
314
|
*/
|
|
@@ -459,6 +509,59 @@ const TranscriptionSettingsResponseModeEnum = {
|
|
|
459
509
|
DISABLED: 'disabled',
|
|
460
510
|
AUTO_ON: 'auto-on',
|
|
461
511
|
};
|
|
512
|
+
/**
|
|
513
|
+
* @export
|
|
514
|
+
*/
|
|
515
|
+
const TranslationSettingsLanguagesEnum = {
|
|
516
|
+
EN: 'en',
|
|
517
|
+
FR: 'fr',
|
|
518
|
+
ES: 'es',
|
|
519
|
+
DE: 'de',
|
|
520
|
+
IT: 'it',
|
|
521
|
+
NL: 'nl',
|
|
522
|
+
PT: 'pt',
|
|
523
|
+
PL: 'pl',
|
|
524
|
+
CA: 'ca',
|
|
525
|
+
CS: 'cs',
|
|
526
|
+
DA: 'da',
|
|
527
|
+
EL: 'el',
|
|
528
|
+
FI: 'fi',
|
|
529
|
+
ID: 'id',
|
|
530
|
+
JA: 'ja',
|
|
531
|
+
RU: 'ru',
|
|
532
|
+
SV: 'sv',
|
|
533
|
+
TA: 'ta',
|
|
534
|
+
TH: 'th',
|
|
535
|
+
TR: 'tr',
|
|
536
|
+
HU: 'hu',
|
|
537
|
+
RO: 'ro',
|
|
538
|
+
ZH: 'zh',
|
|
539
|
+
AR: 'ar',
|
|
540
|
+
TL: 'tl',
|
|
541
|
+
HE: 'he',
|
|
542
|
+
HI: 'hi',
|
|
543
|
+
HR: 'hr',
|
|
544
|
+
KO: 'ko',
|
|
545
|
+
MS: 'ms',
|
|
546
|
+
NO: 'no',
|
|
547
|
+
UK: 'uk',
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* @export
|
|
551
|
+
*/
|
|
552
|
+
const UpdateUserPermissionsRequestGrantPermissionsEnum = {
|
|
553
|
+
SCREENSHARE: 'screenshare',
|
|
554
|
+
SEND_AUDIO: 'send-audio',
|
|
555
|
+
SEND_VIDEO: 'send-video',
|
|
556
|
+
};
|
|
557
|
+
/**
|
|
558
|
+
* @export
|
|
559
|
+
*/
|
|
560
|
+
const UpdateUserPermissionsRequestRevokePermissionsEnum = {
|
|
561
|
+
SCREENSHARE: 'screenshare',
|
|
562
|
+
SEND_AUDIO: 'send-audio',
|
|
563
|
+
SEND_VIDEO: 'send-video',
|
|
564
|
+
};
|
|
462
565
|
/**
|
|
463
566
|
* @export
|
|
464
567
|
*/
|
|
@@ -475,6 +578,14 @@ const VideoSettingsResponseCameraFacingEnum = {
|
|
|
475
578
|
BACK: 'back',
|
|
476
579
|
EXTERNAL: 'external',
|
|
477
580
|
};
|
|
581
|
+
/**
|
|
582
|
+
* @export
|
|
583
|
+
*/
|
|
584
|
+
const WSAuthMessageProductsEnum = {
|
|
585
|
+
CHAT: 'chat',
|
|
586
|
+
VIDEO: 'video',
|
|
587
|
+
FEEDS: 'feeds',
|
|
588
|
+
};
|
|
478
589
|
|
|
479
590
|
class ErrorFromResponse extends Error {
|
|
480
591
|
code;
|
|
@@ -1472,6 +1583,13 @@ class CallState$Type extends MessageType {
|
|
|
1472
1583
|
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1473
1584
|
T: () => Pin,
|
|
1474
1585
|
},
|
|
1586
|
+
{
|
|
1587
|
+
no: 5,
|
|
1588
|
+
name: 'e2ee_enabled',
|
|
1589
|
+
kind: 'scalar',
|
|
1590
|
+
jsonName: 'e2eeEnabled',
|
|
1591
|
+
T: 8 /*ScalarType.BOOL*/,
|
|
1592
|
+
},
|
|
1475
1593
|
]);
|
|
1476
1594
|
}
|
|
1477
1595
|
}
|
|
@@ -4594,6 +4712,19 @@ const isSvcCodec = (codecOrMimeType) => {
|
|
|
4594
4712
|
codecOrMimeType === 'video/av1');
|
|
4595
4713
|
};
|
|
4596
4714
|
|
|
4715
|
+
/**
|
|
4716
|
+
* The default engine: a thin, stateless wrapper over the WebRTC globals.
|
|
4717
|
+
*/
|
|
4718
|
+
const defaultGlobalsEngine = {
|
|
4719
|
+
dispose: () => Promise.resolve(true),
|
|
4720
|
+
};
|
|
4721
|
+
const defaultGlobalsEngineProvider = () => defaultGlobalsEngine;
|
|
4722
|
+
let provider = defaultGlobalsEngineProvider;
|
|
4723
|
+
const setCallMediaEngineProvider = (newProvider) => {
|
|
4724
|
+
provider = newProvider;
|
|
4725
|
+
};
|
|
4726
|
+
const getCallMediaEngineProvider = () => provider;
|
|
4727
|
+
|
|
4597
4728
|
const sfuEventKinds = {
|
|
4598
4729
|
subscriberOffer: undefined,
|
|
4599
4730
|
publisherAnswer: undefined,
|
|
@@ -4801,7 +4932,9 @@ const parseUfragFromCandidate = (candidate) => {
|
|
|
4801
4932
|
};
|
|
4802
4933
|
|
|
4803
4934
|
const ensureExhausted = (x, message) => {
|
|
4804
|
-
|
|
4935
|
+
if (message) {
|
|
4936
|
+
videoLoggerSystem.getLogger('helpers').warn(message, x);
|
|
4937
|
+
}
|
|
4805
4938
|
};
|
|
4806
4939
|
|
|
4807
4940
|
/**
|
|
@@ -4926,7 +5059,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
4926
5059
|
}
|
|
4927
5060
|
};
|
|
4928
5061
|
|
|
4929
|
-
const version = "1.
|
|
5062
|
+
const version = "1.58.0";
|
|
4930
5063
|
const [major, minor, patch] = version.split('.');
|
|
4931
5064
|
let sdkInfo = {
|
|
4932
5065
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5909,6 +6042,7 @@ class CallState {
|
|
|
5909
6042
|
settingsSubject = new BehaviorSubject(undefined);
|
|
5910
6043
|
transcribingSubject = new BehaviorSubject(false);
|
|
5911
6044
|
captioningSubject = new BehaviorSubject(false);
|
|
6045
|
+
e2eeEnabledSubject = new BehaviorSubject(false);
|
|
5912
6046
|
endedBySubject = new BehaviorSubject(undefined);
|
|
5913
6047
|
thumbnailsSubject = new BehaviorSubject(undefined);
|
|
5914
6048
|
membersSubject = new BehaviorSubject([]);
|
|
@@ -6067,6 +6201,11 @@ class CallState {
|
|
|
6067
6201
|
* Will provide the closed captioning state of this call.
|
|
6068
6202
|
*/
|
|
6069
6203
|
captioning$;
|
|
6204
|
+
/**
|
|
6205
|
+
* Whether end-to-end encryption is active for this call, as reported by the
|
|
6206
|
+
* SFU in the join response.
|
|
6207
|
+
*/
|
|
6208
|
+
e2eeEnabled$;
|
|
6070
6209
|
/**
|
|
6071
6210
|
* Will provide the user who ended this call.
|
|
6072
6211
|
*/
|
|
@@ -6168,6 +6307,7 @@ class CallState {
|
|
|
6168
6307
|
this.rawRecording$ = duc(this.rawRecordingSubject);
|
|
6169
6308
|
this.transcribing$ = duc(this.transcribingSubject);
|
|
6170
6309
|
this.captioning$ = duc(this.captioningSubject);
|
|
6310
|
+
this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
|
|
6171
6311
|
this.eventHandlers = {
|
|
6172
6312
|
// these events are not updating the call state:
|
|
6173
6313
|
'call.frame_recording_ready': undefined,
|
|
@@ -6270,6 +6410,7 @@ class CallState {
|
|
|
6270
6410
|
clearTimeout(taskId);
|
|
6271
6411
|
this.closedCaptionsTasks.delete(ccKey);
|
|
6272
6412
|
}
|
|
6413
|
+
this.removeAllOrphanedTracks();
|
|
6273
6414
|
};
|
|
6274
6415
|
/**
|
|
6275
6416
|
* Sets the list of criteria that are used to sort the participants.
|
|
@@ -6619,6 +6760,12 @@ class CallState {
|
|
|
6619
6760
|
get transcribing() {
|
|
6620
6761
|
return this.getCurrentValue(this.transcribing$);
|
|
6621
6762
|
}
|
|
6763
|
+
/**
|
|
6764
|
+
* Whether end-to-end encryption is active for this call.
|
|
6765
|
+
*/
|
|
6766
|
+
get e2eeEnabled() {
|
|
6767
|
+
return this.getCurrentValue(this.e2eeEnabled$);
|
|
6768
|
+
}
|
|
6622
6769
|
/**
|
|
6623
6770
|
* Will provide the user who ended this call.
|
|
6624
6771
|
*/
|
|
@@ -6839,6 +6986,18 @@ class CallState {
|
|
|
6839
6986
|
removeOrphanedTrack = (id) => {
|
|
6840
6987
|
this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
|
|
6841
6988
|
};
|
|
6989
|
+
/**
|
|
6990
|
+
* Drops every orphaned track. Call this when the peer connections that own
|
|
6991
|
+
* the stored receivers go away (full leave, reconnect, or migration):
|
|
6992
|
+
* `pc.close()` does not raise the track `ended` event, so the per-track
|
|
6993
|
+
* cleanup never fires and the receivers + their closed PCs would otherwise
|
|
6994
|
+
* leak for the call's lifetime.
|
|
6995
|
+
*
|
|
6996
|
+
* @internal
|
|
6997
|
+
*/
|
|
6998
|
+
removeAllOrphanedTracks = () => {
|
|
6999
|
+
this.orphanedTracks = [];
|
|
7000
|
+
};
|
|
6842
7001
|
/**
|
|
6843
7002
|
* Takes all orphaned tracks with the given track lookup prefix.
|
|
6844
7003
|
* All orphaned tracks with the given track lookup prefix are removed from the call state.
|
|
@@ -6901,7 +7060,7 @@ class CallState {
|
|
|
6901
7060
|
* @param reconnectDetails optional reconnect details.
|
|
6902
7061
|
*/
|
|
6903
7062
|
updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
|
|
6904
|
-
const { participants, participantCount, startedAt, pins } = callState;
|
|
7063
|
+
const { participants, participantCount, startedAt, pins, e2EeEnabled } = callState;
|
|
6905
7064
|
const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
|
|
6906
7065
|
this.setParticipants(() => {
|
|
6907
7066
|
const participantLookup = this.getParticipantLookupBySessionId();
|
|
@@ -6927,6 +7086,7 @@ class CallState {
|
|
|
6927
7086
|
this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
6928
7087
|
this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
|
|
6929
7088
|
this.setServerSidePins(pins);
|
|
7089
|
+
this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
|
|
6930
7090
|
};
|
|
6931
7091
|
updateFromMemberRemoved = (event) => {
|
|
6932
7092
|
this.updateFromCallResponse(event.call);
|
|
@@ -7124,6 +7284,37 @@ class CallState {
|
|
|
7124
7284
|
};
|
|
7125
7285
|
}
|
|
7126
7286
|
|
|
7287
|
+
/**
|
|
7288
|
+
* Detection and selection policy for the two WebRTC Encoded Transform APIs.
|
|
7289
|
+
* Internal: which one the SDK attaches is an RTC-layer detail. Consumers want
|
|
7290
|
+
* `EncryptionManager.isSupported` instead.
|
|
7291
|
+
*/
|
|
7292
|
+
/**
|
|
7293
|
+
* Chrome only, and the reason an RTCPeerConnection carrying E2EE needs the
|
|
7294
|
+
* non-standard `encodedInsertableStreams` flag.
|
|
7295
|
+
*/
|
|
7296
|
+
const hasInsertableStreams = () => typeof RTCRtpSender !== 'undefined' &&
|
|
7297
|
+
'createEncodedStreams' in RTCRtpSender.prototype;
|
|
7298
|
+
/** Whether the standard `RTCRtpScriptTransform` API exists. */
|
|
7299
|
+
const hasScriptTransform = () => typeof RTCRtpScriptTransform !== 'undefined';
|
|
7300
|
+
/**
|
|
7301
|
+
* Which Encoded Transform API to attach E2EE with here.
|
|
7302
|
+
*
|
|
7303
|
+
* - `'insertable'`: legacy Insertable Streams. Used on Chrome, where
|
|
7304
|
+
* `RTCRtpScriptTransform` is still unreliable for E2EE.
|
|
7305
|
+
* - `'script'`: the standard API. Used everywhere else.
|
|
7306
|
+
* - `undefined`: neither exists, so E2EE cannot run.
|
|
7307
|
+
*/
|
|
7308
|
+
const preferredTransform = () => {
|
|
7309
|
+
const insertable = hasInsertableStreams();
|
|
7310
|
+
// Chrome's RTCRtpScriptTransform is still unreliable for E2EE.
|
|
7311
|
+
if (isChrome() && insertable)
|
|
7312
|
+
return 'insertable';
|
|
7313
|
+
if (hasScriptTransform())
|
|
7314
|
+
return 'script';
|
|
7315
|
+
return insertable ? 'insertable' : undefined;
|
|
7316
|
+
};
|
|
7317
|
+
|
|
7127
7318
|
/**
|
|
7128
7319
|
* Flatten the stats report into an array of stats objects.
|
|
7129
7320
|
*
|
|
@@ -8232,6 +8423,7 @@ class BasePeerConnection {
|
|
|
8232
8423
|
state;
|
|
8233
8424
|
dispatcher;
|
|
8234
8425
|
clientPublishOptions;
|
|
8426
|
+
e2ee;
|
|
8235
8427
|
tag;
|
|
8236
8428
|
sfuClient;
|
|
8237
8429
|
onReconnectionNeeded;
|
|
@@ -8253,13 +8445,14 @@ class BasePeerConnection {
|
|
|
8253
8445
|
/**
|
|
8254
8446
|
* Constructs a new `BasePeerConnection` instance.
|
|
8255
8447
|
*/
|
|
8256
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8448
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, e2ee, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
8257
8449
|
this.peerType = peerType;
|
|
8258
8450
|
this.sfuClient = sfuClient;
|
|
8259
8451
|
this.state = state;
|
|
8260
8452
|
this.dispatcher = dispatcher;
|
|
8261
8453
|
this.iceRestartDelay = iceRestartDelay;
|
|
8262
8454
|
this.clientPublishOptions = clientPublishOptions;
|
|
8455
|
+
this.e2ee = e2ee;
|
|
8263
8456
|
this.tag = tag;
|
|
8264
8457
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8265
8458
|
this.onIceConnected = onIceConnected;
|
|
@@ -8278,7 +8471,13 @@ class BasePeerConnection {
|
|
|
8278
8471
|
}
|
|
8279
8472
|
}
|
|
8280
8473
|
createPeerConnection = (connectionConfig) => {
|
|
8281
|
-
const
|
|
8474
|
+
const config = { ...connectionConfig };
|
|
8475
|
+
// The legacy Insertable Streams path requires this non-standard flag.
|
|
8476
|
+
if (this.e2ee && hasInsertableStreams()) {
|
|
8477
|
+
// @ts-expect-error not part of the standard lib yet
|
|
8478
|
+
config.encodedInsertableStreams = true;
|
|
8479
|
+
}
|
|
8480
|
+
const pc = new RTCPeerConnection(config);
|
|
8282
8481
|
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
8283
8482
|
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
8284
8483
|
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
@@ -9032,6 +9231,10 @@ class Publisher extends BasePeerConnection {
|
|
|
9032
9231
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9033
9232
|
'maintain-framerate';
|
|
9034
9233
|
await transceiver.sender.setParameters(params);
|
|
9234
|
+
if (this.e2ee) {
|
|
9235
|
+
this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
|
|
9236
|
+
this.logger.debug('E2EE encryptor attached to sender');
|
|
9237
|
+
}
|
|
9035
9238
|
await this.negotiate();
|
|
9036
9239
|
};
|
|
9037
9240
|
/**
|
|
@@ -9643,9 +9846,15 @@ class Subscriber extends BasePeerConnection {
|
|
|
9643
9846
|
trackLookupPrefix: trackId,
|
|
9644
9847
|
track: primaryStream,
|
|
9645
9848
|
trackType,
|
|
9849
|
+
receiver: this.e2ee ? e.receiver : undefined,
|
|
9646
9850
|
});
|
|
9647
9851
|
return;
|
|
9648
9852
|
}
|
|
9853
|
+
else if (this.e2ee) {
|
|
9854
|
+
const { userId } = participantToUpdate;
|
|
9855
|
+
this.e2ee.decrypt(e.receiver, userId, TrackType[trackType]);
|
|
9856
|
+
this.logger.debug('E2EE decryptor attached to receiver', userId);
|
|
9857
|
+
}
|
|
9649
9858
|
const streamKindProp = trackTypeToParticipantStreamKey(trackType);
|
|
9650
9859
|
if (!streamKindProp) {
|
|
9651
9860
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
@@ -10617,7 +10826,7 @@ const handleRemoteSoftMute = (call) => {
|
|
|
10617
10826
|
/**
|
|
10618
10827
|
* An event responder which handles the `participantJoined` event.
|
|
10619
10828
|
*/
|
|
10620
|
-
const watchParticipantJoined = (state) => {
|
|
10829
|
+
const watchParticipantJoined = (state, e2ee = undefined) => {
|
|
10621
10830
|
return function onParticipantJoined(e) {
|
|
10622
10831
|
const { participant } = e;
|
|
10623
10832
|
if (!participant)
|
|
@@ -10630,7 +10839,7 @@ const watchParticipantJoined = (state) => {
|
|
|
10630
10839
|
// The SFU would send participant info as part of the `join`
|
|
10631
10840
|
// response and then follow up with a `participantJoined` event for
|
|
10632
10841
|
// already announced participants.
|
|
10633
|
-
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
10842
|
+
const orphanedTracks = reconcileOrphanedTracks(state, participant, e2ee);
|
|
10634
10843
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
10635
10844
|
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
10636
10845
|
viewportVisibilityState: {
|
|
@@ -10666,7 +10875,7 @@ const watchParticipantUpdated = (state) => {
|
|
|
10666
10875
|
* An event responder which handles the `trackPublished` event.
|
|
10667
10876
|
* The SFU will send this event when a participant publishes a track.
|
|
10668
10877
|
*/
|
|
10669
|
-
const watchTrackPublished = (state) => {
|
|
10878
|
+
const watchTrackPublished = (state, e2ee = undefined) => {
|
|
10670
10879
|
return function onTrackPublished(e) {
|
|
10671
10880
|
const { type, sessionId } = e;
|
|
10672
10881
|
// An optimization for large calls.
|
|
@@ -10674,7 +10883,7 @@ const watchTrackPublished = (state) => {
|
|
|
10674
10883
|
// events, and instead, it would only provide the participant's information
|
|
10675
10884
|
// once they start publishing a track.
|
|
10676
10885
|
if (e.participant) {
|
|
10677
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10886
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant, e2ee);
|
|
10678
10887
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
10679
10888
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10680
10889
|
}
|
|
@@ -10689,12 +10898,12 @@ const watchTrackPublished = (state) => {
|
|
|
10689
10898
|
* An event responder which handles the `trackUnpublished` event.
|
|
10690
10899
|
* The SFU will send this event when a participant unpublishes a track.
|
|
10691
10900
|
*/
|
|
10692
|
-
const watchTrackUnpublished = (state) => {
|
|
10901
|
+
const watchTrackUnpublished = (state, e2ee = undefined) => {
|
|
10693
10902
|
return function onTrackUnpublished(e) {
|
|
10694
10903
|
const { type, sessionId } = e;
|
|
10695
10904
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
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, (p) => ({
|
|
10700
10909
|
pausedTracks: p.pausedTracks?.filter((t) => t !== type),
|
|
@@ -10713,17 +10922,22 @@ const watchTrackUnpublished = (state) => {
|
|
|
10713
10922
|
*
|
|
10714
10923
|
* @param state the call state.
|
|
10715
10924
|
* @param participant the participant.
|
|
10925
|
+
* @param e2ee accessor for the call's encryption manager, if any.
|
|
10716
10926
|
*/
|
|
10717
|
-
const reconcileOrphanedTracks = (state, participant) => {
|
|
10927
|
+
const reconcileOrphanedTracks = (state, participant, e2ee) => {
|
|
10718
10928
|
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
10719
10929
|
if (!orphanTracks.length)
|
|
10720
10930
|
return;
|
|
10931
|
+
const manager = e2ee?.();
|
|
10721
10932
|
const reconciledTracks = {};
|
|
10722
10933
|
for (const orphan of orphanTracks) {
|
|
10723
10934
|
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
10724
10935
|
if (!key)
|
|
10725
10936
|
continue;
|
|
10726
10937
|
reconciledTracks[key] = orphan.track;
|
|
10938
|
+
if (manager && orphan.receiver) {
|
|
10939
|
+
manager.decrypt(orphan.receiver, participant.userId, TrackType[orphan.trackType]);
|
|
10940
|
+
}
|
|
10727
10941
|
}
|
|
10728
10942
|
return reconciledTracks;
|
|
10729
10943
|
};
|
|
@@ -10779,6 +10993,11 @@ const watchAudioLevelChanged = (dispatcher, state) => {
|
|
|
10779
10993
|
*/
|
|
10780
10994
|
const registerEventHandlers = (call, dispatcher) => {
|
|
10781
10995
|
const state = call.state;
|
|
10996
|
+
// Read lazily on each event: setE2EEManager can run after setup() (an app that
|
|
10997
|
+
// inspects call settings via get()/getOrCreate() before deciding to encrypt has
|
|
10998
|
+
// already triggered it), so a value captured here would be a stale undefined
|
|
10999
|
+
// for the whole call and orphaned tracks would never get a decryptor.
|
|
11000
|
+
const e2ee = () => call.e2eeManager;
|
|
10782
11001
|
const eventHandlers = [
|
|
10783
11002
|
call.on('call.ended', watchCallEnded(call)),
|
|
10784
11003
|
watchSfuCallEnded(call),
|
|
@@ -10786,11 +11005,11 @@ const registerEventHandlers = (call, dispatcher) => {
|
|
|
10786
11005
|
watchSfuErrorReports(dispatcher),
|
|
10787
11006
|
watchConnectionQualityChanged(dispatcher, state),
|
|
10788
11007
|
watchParticipantCountChanged(dispatcher, state),
|
|
10789
|
-
call.on('participantJoined', watchParticipantJoined(state)),
|
|
11008
|
+
call.on('participantJoined', watchParticipantJoined(state, e2ee)),
|
|
10790
11009
|
call.on('participantLeft', watchParticipantLeft(state)),
|
|
10791
11010
|
call.on('participantUpdated', watchParticipantUpdated(state)),
|
|
10792
|
-
call.on('trackPublished', watchTrackPublished(state)),
|
|
10793
|
-
call.on('trackUnpublished', watchTrackUnpublished(state)),
|
|
11011
|
+
call.on('trackPublished', watchTrackPublished(state, e2ee)),
|
|
11012
|
+
call.on('trackUnpublished', watchTrackUnpublished(state, e2ee)),
|
|
10794
11013
|
watchAudioLevelChanged(dispatcher, state),
|
|
10795
11014
|
watchDominantSpeakerChanged(dispatcher, state),
|
|
10796
11015
|
call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
|
|
@@ -11918,13 +12137,14 @@ class PermissionsContext {
|
|
|
11918
12137
|
return false;
|
|
11919
12138
|
const { audio, video, screensharing } = settings;
|
|
11920
12139
|
switch (permission) {
|
|
11921
|
-
case
|
|
12140
|
+
case RequestPermissionRequestPermissionsEnum.SEND_AUDIO:
|
|
11922
12141
|
return audio.access_request_enabled;
|
|
11923
|
-
case
|
|
12142
|
+
case RequestPermissionRequestPermissionsEnum.SEND_VIDEO:
|
|
11924
12143
|
return video.access_request_enabled;
|
|
11925
|
-
case
|
|
12144
|
+
case RequestPermissionRequestPermissionsEnum.SCREENSHARE:
|
|
11926
12145
|
return screensharing.access_request_enabled;
|
|
11927
12146
|
default:
|
|
12147
|
+
ensureExhausted(permission);
|
|
11928
12148
|
return false;
|
|
11929
12149
|
}
|
|
11930
12150
|
};
|
|
@@ -12931,6 +13151,27 @@ class DeviceManager {
|
|
|
12931
13151
|
this.filters.forEach((entry) => entry.stop?.());
|
|
12932
13152
|
}
|
|
12933
13153
|
}
|
|
13154
|
+
reconcileOptimisticStatus = async () => {
|
|
13155
|
+
const target = this.state.optimisticStatus;
|
|
13156
|
+
await withCancellation(this.statusChangeConcurrencyTag, async (signal) => {
|
|
13157
|
+
try {
|
|
13158
|
+
if (target === 'enabled' && this.state.status !== 'enabled') {
|
|
13159
|
+
await this.unmuteStream();
|
|
13160
|
+
if (!signal.aborted)
|
|
13161
|
+
this.state.setStatus('enabled');
|
|
13162
|
+
}
|
|
13163
|
+
else if (target === 'disabled' && this.state.status === 'enabled') {
|
|
13164
|
+
// mirror whatever disable() does to stop/pause the track per disableMode
|
|
13165
|
+
if (!signal.aborted)
|
|
13166
|
+
this.state.setStatus('disabled');
|
|
13167
|
+
}
|
|
13168
|
+
}
|
|
13169
|
+
finally {
|
|
13170
|
+
if (!signal.aborted)
|
|
13171
|
+
this.state.setPendingStatus(this.state.status);
|
|
13172
|
+
}
|
|
13173
|
+
});
|
|
13174
|
+
};
|
|
12934
13175
|
disableTracks() {
|
|
12935
13176
|
this.getTracks().forEach((track) => {
|
|
12936
13177
|
if (track.enabled)
|
|
@@ -13564,6 +13805,33 @@ class CameraManager extends DeviceManager {
|
|
|
13564
13805
|
}
|
|
13565
13806
|
}
|
|
13566
13807
|
}
|
|
13808
|
+
enable() {
|
|
13809
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13810
|
+
this.state.setPendingStatus('enabled');
|
|
13811
|
+
return Promise.resolve();
|
|
13812
|
+
}
|
|
13813
|
+
return super.enable();
|
|
13814
|
+
}
|
|
13815
|
+
async disable(forceStopOrOptions) {
|
|
13816
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13817
|
+
this.state.setPendingStatus('disabled');
|
|
13818
|
+
return;
|
|
13819
|
+
}
|
|
13820
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
13821
|
+
if (forceStopOrOptions === undefined)
|
|
13822
|
+
return super.disable();
|
|
13823
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
13824
|
+
return super.disable(forceStopOrOptions);
|
|
13825
|
+
}
|
|
13826
|
+
return super.disable(forceStopOrOptions);
|
|
13827
|
+
}
|
|
13828
|
+
toggle() {
|
|
13829
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
13830
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
13831
|
+
return Promise.resolve();
|
|
13832
|
+
}
|
|
13833
|
+
return super.toggle();
|
|
13834
|
+
}
|
|
13567
13835
|
/**
|
|
13568
13836
|
* Applies the video settings to the camera.
|
|
13569
13837
|
*
|
|
@@ -13597,9 +13865,16 @@ class CameraManager extends DeviceManager {
|
|
|
13597
13865
|
await this.enable();
|
|
13598
13866
|
}
|
|
13599
13867
|
}
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13868
|
+
if (isReactNative() && publish && canPublish) {
|
|
13869
|
+
// On RN the camera is enabled/disabled optimistically before JOINED. Reconcile now
|
|
13870
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
13871
|
+
await this.reconcileOptimisticStatus();
|
|
13872
|
+
}
|
|
13873
|
+
else {
|
|
13874
|
+
const { mediaStream } = this.state;
|
|
13875
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
13876
|
+
await this.publishStream(mediaStream);
|
|
13877
|
+
}
|
|
13603
13878
|
}
|
|
13604
13879
|
}
|
|
13605
13880
|
getDevices() {
|
|
@@ -13618,7 +13893,10 @@ class CameraManager extends DeviceManager {
|
|
|
13618
13893
|
}
|
|
13619
13894
|
return constraints;
|
|
13620
13895
|
}
|
|
13621
|
-
getStream(constraints) {
|
|
13896
|
+
async getStream(constraints) {
|
|
13897
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
13898
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
13899
|
+
await this.call.ensureMediaFactory();
|
|
13622
13900
|
return getVideoStream(constraints, this.call.tracer);
|
|
13623
13901
|
}
|
|
13624
13902
|
}
|
|
@@ -13634,6 +13912,9 @@ class AudioDeviceManager extends DeviceManager {
|
|
|
13634
13912
|
if (!this.call.state.settings?.audio.hifi_audio_enabled) {
|
|
13635
13913
|
throw new Error('High Fidelity audio is not enabled for this call');
|
|
13636
13914
|
}
|
|
13915
|
+
if (isReactNative() && this.call.hasMediaEngine) {
|
|
13916
|
+
throw new Error('setAudioBitrateProfile must be called before joining the call.');
|
|
13917
|
+
}
|
|
13637
13918
|
this.doSetAudioBitrateProfile(profile);
|
|
13638
13919
|
this.state.setAudioBitrateProfile(profile);
|
|
13639
13920
|
if (this.enabled) {
|
|
@@ -13906,7 +14187,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
13906
14187
|
]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
|
|
13907
14188
|
try {
|
|
13908
14189
|
if (callingState === CallingState.LEFT) {
|
|
13909
|
-
|
|
14190
|
+
// The muted-recording-prepared mode is reset in `callManager.stop()`
|
|
14191
|
+
// (during leave, while the call factory is still alive), not here —
|
|
14192
|
+
// this subscription fires asynchronously and could land after the
|
|
14193
|
+
// factory is disposed, forcing a default-ADM rebuild.
|
|
13910
14194
|
await this.stopSpeakingWhileMutedDetection();
|
|
13911
14195
|
}
|
|
13912
14196
|
if (callingState !== CallingState.JOINED)
|
|
@@ -14006,6 +14290,33 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14006
14290
|
this.subscriptions.push(unsubscribe);
|
|
14007
14291
|
}
|
|
14008
14292
|
}
|
|
14293
|
+
enable() {
|
|
14294
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14295
|
+
this.state.setPendingStatus('enabled');
|
|
14296
|
+
return Promise.resolve();
|
|
14297
|
+
}
|
|
14298
|
+
return super.enable();
|
|
14299
|
+
}
|
|
14300
|
+
async disable(forceStopOrOptions) {
|
|
14301
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14302
|
+
this.state.setPendingStatus('disabled');
|
|
14303
|
+
return;
|
|
14304
|
+
}
|
|
14305
|
+
// forward verbatim to the base, narrowing so the right overload is selected
|
|
14306
|
+
if (forceStopOrOptions === undefined)
|
|
14307
|
+
return super.disable();
|
|
14308
|
+
if (typeof forceStopOrOptions === 'boolean') {
|
|
14309
|
+
return super.disable(forceStopOrOptions);
|
|
14310
|
+
}
|
|
14311
|
+
return super.disable(forceStopOrOptions);
|
|
14312
|
+
}
|
|
14313
|
+
toggle() {
|
|
14314
|
+
if (isReactNative() && !this.call.hasMediaEngine) {
|
|
14315
|
+
this.state.setPendingStatus(this.state.optimisticStatus === 'enabled' ? 'disabled' : 'enabled');
|
|
14316
|
+
return Promise.resolve();
|
|
14317
|
+
}
|
|
14318
|
+
return super.toggle();
|
|
14319
|
+
}
|
|
14009
14320
|
/**
|
|
14010
14321
|
* Enables noise cancellation for the microphone.
|
|
14011
14322
|
*
|
|
@@ -14166,15 +14477,25 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14166
14477
|
await this.enable();
|
|
14167
14478
|
}
|
|
14168
14479
|
}
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14480
|
+
if (isReactNative() && publish && canPublish) {
|
|
14481
|
+
// On RN the microphone is enabled/disabled optimistically before JOINED. Reconcile now
|
|
14482
|
+
// acquires the track and publishes it, so it fully owns the publish.
|
|
14483
|
+
await this.reconcileOptimisticStatus();
|
|
14484
|
+
}
|
|
14485
|
+
else {
|
|
14486
|
+
const { mediaStream } = this.state;
|
|
14487
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
14488
|
+
await this.publishStream(mediaStream);
|
|
14489
|
+
}
|
|
14172
14490
|
}
|
|
14173
14491
|
}
|
|
14174
14492
|
getDevices() {
|
|
14175
14493
|
return getAudioDevices(this.call.tracer);
|
|
14176
14494
|
}
|
|
14177
|
-
getStream(constraints) {
|
|
14495
|
+
async getStream(constraints) {
|
|
14496
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14497
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14498
|
+
await this.call.ensureMediaFactory();
|
|
14178
14499
|
return getAudioStream(constraints, this.call.tracer);
|
|
14179
14500
|
}
|
|
14180
14501
|
doSetAudioBitrateProfile(profile) {
|
|
@@ -14383,6 +14704,9 @@ class ScreenShareManager extends AudioDeviceManager {
|
|
|
14383
14704
|
if (!this.state.audioEnabled) {
|
|
14384
14705
|
constraints.audio = false;
|
|
14385
14706
|
}
|
|
14707
|
+
// Ensure the call's media factory exists before capture so the resulting
|
|
14708
|
+
// track is owned by it (the WebRTC globals resolve to the live factory).
|
|
14709
|
+
await this.call.ensureMediaFactory();
|
|
14386
14710
|
const stream = await getScreenShareStream(constraints, this.call.tracer);
|
|
14387
14711
|
const [track] = stream.getVideoTracks();
|
|
14388
14712
|
const { contentHint } = this.state.settings || {};
|
|
@@ -14487,10 +14811,14 @@ class SpeakerManager {
|
|
|
14487
14811
|
this.devicePersistence = devicePreferences;
|
|
14488
14812
|
this.setup();
|
|
14489
14813
|
}
|
|
14490
|
-
apply(settings) {
|
|
14491
|
-
|
|
14814
|
+
async apply(settings) {
|
|
14815
|
+
if (isReactNative()) {
|
|
14816
|
+
this.applyRN(settings);
|
|
14817
|
+
return;
|
|
14818
|
+
}
|
|
14819
|
+
await this.applyWeb();
|
|
14492
14820
|
}
|
|
14493
|
-
applyWeb() {
|
|
14821
|
+
async applyWeb() {
|
|
14494
14822
|
const { enabled, storageKey } = this.devicePersistence;
|
|
14495
14823
|
if (!enabled)
|
|
14496
14824
|
return;
|
|
@@ -14502,8 +14830,22 @@ class SpeakerManager {
|
|
|
14502
14830
|
const nextDeviceId = preference.selectedDeviceId === defaultDeviceId
|
|
14503
14831
|
? ''
|
|
14504
14832
|
: preference.selectedDeviceId;
|
|
14505
|
-
if (
|
|
14506
|
-
this.
|
|
14833
|
+
if (!nextDeviceId) {
|
|
14834
|
+
if (this.state.selectedDevice !== nextDeviceId) {
|
|
14835
|
+
this.select(nextDeviceId);
|
|
14836
|
+
}
|
|
14837
|
+
return;
|
|
14838
|
+
}
|
|
14839
|
+
const permissionState = await firstValueFrom(getAudioBrowserPermission(this.call.tracer).asStateObservable());
|
|
14840
|
+
if (permissionState !== 'granted')
|
|
14841
|
+
return;
|
|
14842
|
+
const devices = await firstValueFrom(this.listDevices());
|
|
14843
|
+
const device = this.findDevice(devices, nextDeviceId) ??
|
|
14844
|
+
(preference.selectedDeviceLabel
|
|
14845
|
+
? devices.find((d) => d.label === preference.selectedDeviceLabel)
|
|
14846
|
+
: undefined);
|
|
14847
|
+
if (device && this.state.selectedDevice !== device.deviceId) {
|
|
14848
|
+
this.select(device.deviceId);
|
|
14507
14849
|
}
|
|
14508
14850
|
}
|
|
14509
14851
|
applyRN(settings) {
|
|
@@ -14532,6 +14874,7 @@ class SpeakerManager {
|
|
|
14532
14874
|
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
14533
14875
|
defaultDevice,
|
|
14534
14876
|
isRingingTypeCall: this.call.ringing,
|
|
14877
|
+
cid: this.call.cid,
|
|
14535
14878
|
});
|
|
14536
14879
|
}
|
|
14537
14880
|
}
|
|
@@ -14540,11 +14883,15 @@ class SpeakerManager {
|
|
|
14540
14883
|
return;
|
|
14541
14884
|
this.areSubscriptionsSetUp = true;
|
|
14542
14885
|
if (deviceIds$ && !isReactNative()) {
|
|
14543
|
-
this.subscriptions.push(createSubscription(combineLatest([
|
|
14886
|
+
this.subscriptions.push(createSubscription(combineLatest([
|
|
14887
|
+
deviceIds$.pipe(pairwise()),
|
|
14888
|
+
this.state.selectedDevice$,
|
|
14889
|
+
]), ([[prevDevices, currentDevices], deviceId]) => {
|
|
14544
14890
|
if (!deviceId)
|
|
14545
14891
|
return;
|
|
14546
|
-
const
|
|
14547
|
-
|
|
14892
|
+
const isDisconnected = this.findDevice(prevDevices, deviceId) &&
|
|
14893
|
+
!this.findDevice(currentDevices, deviceId);
|
|
14894
|
+
if (isDisconnected)
|
|
14548
14895
|
this.select('');
|
|
14549
14896
|
}));
|
|
14550
14897
|
}
|
|
@@ -14591,6 +14938,7 @@ class SpeakerManager {
|
|
|
14591
14938
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
14592
14939
|
this.subscriptions = [];
|
|
14593
14940
|
this.areSubscriptionsSetUp = false;
|
|
14941
|
+
this.defaultDevice = undefined;
|
|
14594
14942
|
};
|
|
14595
14943
|
/**
|
|
14596
14944
|
* Set the volume of the audio elements
|
|
@@ -14625,6 +14973,7 @@ class SpeakerManager {
|
|
|
14625
14973
|
return { audioVolume: volume };
|
|
14626
14974
|
});
|
|
14627
14975
|
}
|
|
14976
|
+
findDevice = (devices, deviceId) => devices.find((d) => d.deviceId === deviceId && d.kind === 'audiooutput');
|
|
14628
14977
|
persistSpeakerDevicePreference(selectedDevice) {
|
|
14629
14978
|
const { storageKey } = this.devicePersistence;
|
|
14630
14979
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
@@ -14707,6 +15056,7 @@ class Call {
|
|
|
14707
15056
|
blockedAudioTracker;
|
|
14708
15057
|
subscriber;
|
|
14709
15058
|
publisher;
|
|
15059
|
+
e2eeManager;
|
|
14710
15060
|
/**
|
|
14711
15061
|
* Flag telling whether this call is a "ringing" call.
|
|
14712
15062
|
*/
|
|
@@ -14762,6 +15112,8 @@ class Call {
|
|
|
14762
15112
|
allowOwnTracksLoopback = false;
|
|
14763
15113
|
hasJoinedOnce = false;
|
|
14764
15114
|
deviceSettingsAppliedOnce = false;
|
|
15115
|
+
callManagerStarted = false;
|
|
15116
|
+
leaveGeneration = 0;
|
|
14765
15117
|
credentials;
|
|
14766
15118
|
initialized = false;
|
|
14767
15119
|
acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
|
|
@@ -14780,6 +15132,13 @@ class Call {
|
|
|
14780
15132
|
clientCapabilities = new Set([
|
|
14781
15133
|
ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
|
|
14782
15134
|
]);
|
|
15135
|
+
/**
|
|
15136
|
+
* The in-flight per-call media engine. On web/React this resolves to a thin
|
|
15137
|
+
* globals-backed engine (no provider registered); React Native registers a
|
|
15138
|
+
* provider that owns a per-call native factory.
|
|
15139
|
+
* @internal
|
|
15140
|
+
*/
|
|
15141
|
+
mediaEnginePromise;
|
|
14783
15142
|
/**
|
|
14784
15143
|
* Constructs a new `Call` instance.
|
|
14785
15144
|
*
|
|
@@ -15049,6 +15408,7 @@ class Call {
|
|
|
15049
15408
|
if (callingState === CallingState.LEFT) {
|
|
15050
15409
|
return;
|
|
15051
15410
|
}
|
|
15411
|
+
this.leaveGeneration += 1;
|
|
15052
15412
|
if (callingState === CallingState.JOINING) {
|
|
15053
15413
|
const waitUntilCallJoined = () => {
|
|
15054
15414
|
return new Promise((resolve) => {
|
|
@@ -15135,12 +15495,14 @@ class Call {
|
|
|
15135
15495
|
this.clientStore.unregisterCall(this);
|
|
15136
15496
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
15137
15497
|
isRingingTypeCall: this.ringing,
|
|
15498
|
+
shouldStopCallManager: this.callManagerStarted,
|
|
15138
15499
|
});
|
|
15139
15500
|
this.camera.dispose();
|
|
15140
15501
|
this.microphone.dispose();
|
|
15141
15502
|
this.screenShare.dispose();
|
|
15142
15503
|
this.speaker.dispose();
|
|
15143
15504
|
this.deviceSettingsAppliedOnce = false;
|
|
15505
|
+
this.callManagerStarted = false;
|
|
15144
15506
|
const stopOnLeavePromises = [];
|
|
15145
15507
|
if (this.camera.stopOnLeave) {
|
|
15146
15508
|
stopOnLeavePromises.push(this.camera.disable(true));
|
|
@@ -15152,6 +15514,22 @@ class Call {
|
|
|
15152
15514
|
stopOnLeavePromises.push(this.screenShare.disable(true));
|
|
15153
15515
|
}
|
|
15154
15516
|
await Promise.all(stopOnLeavePromises);
|
|
15517
|
+
// Dispose the per-call media engine last — after peer connections and
|
|
15518
|
+
// local tracks are gone — so the backing factory tears down with no
|
|
15519
|
+
// owned PCs/tracks. A fresh `join()` builds a new engine.
|
|
15520
|
+
if (this.mediaEnginePromise) {
|
|
15521
|
+
const enginePromise = this.mediaEnginePromise;
|
|
15522
|
+
this.mediaEnginePromise = undefined;
|
|
15523
|
+
this.logger.debug('Disposing per-call media factory');
|
|
15524
|
+
await enginePromise
|
|
15525
|
+
.then((engine) => {
|
|
15526
|
+
globalThis.streamRNVideoSDK?.callingX?.unwireAudioEngineSubscription();
|
|
15527
|
+
return engine.dispose();
|
|
15528
|
+
})
|
|
15529
|
+
.catch((err) => {
|
|
15530
|
+
this.logger.warn('Failed to dispose media engine', err);
|
|
15531
|
+
});
|
|
15532
|
+
}
|
|
15155
15533
|
});
|
|
15156
15534
|
};
|
|
15157
15535
|
/**
|
|
@@ -15252,11 +15630,7 @@ class Call {
|
|
|
15252
15630
|
this.clientStore.registerOrUpdateCall(this);
|
|
15253
15631
|
}
|
|
15254
15632
|
// 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;
|
|
15633
|
+
const skipSpeakerApply = isReactNative();
|
|
15260
15634
|
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15261
15635
|
return response;
|
|
15262
15636
|
};
|
|
@@ -15279,11 +15653,7 @@ class Call {
|
|
|
15279
15653
|
this.clientStore.registerOrUpdateCall(this);
|
|
15280
15654
|
}
|
|
15281
15655
|
// 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;
|
|
15656
|
+
const skipSpeakerApply = isReactNative();
|
|
15287
15657
|
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15288
15658
|
return response;
|
|
15289
15659
|
};
|
|
@@ -15432,9 +15802,20 @@ class Call {
|
|
|
15432
15802
|
doJoin = async (data) => {
|
|
15433
15803
|
const connectStartTime = Date.now();
|
|
15434
15804
|
const callingState = this.state.callingState;
|
|
15805
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
15806
|
+
const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
|
|
15435
15807
|
this.joinCallData = data;
|
|
15436
15808
|
this.logger.debug('Starting join flow');
|
|
15437
15809
|
this.state.setCallingState(CallingState.JOINING);
|
|
15810
|
+
// Ensure the per-call media engine exists before any peer connection
|
|
15811
|
+
// (codec probe, subscriber, publisher) or capture happens, so the WebRTC
|
|
15812
|
+
// globals resolve to the call's factory. Idempotent across
|
|
15813
|
+
// reconnect/migration attempts.
|
|
15814
|
+
await this.ensureMediaFactory();
|
|
15815
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
15816
|
+
if (callingX) {
|
|
15817
|
+
callingX.wireAudioEngineSubscription();
|
|
15818
|
+
}
|
|
15438
15819
|
const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
15439
15820
|
const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
15440
15821
|
const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
@@ -15489,6 +15870,11 @@ class Call {
|
|
|
15489
15870
|
// these are throw-away SDPs that the SFU will use to determine
|
|
15490
15871
|
// the capabilities of the client (codec support, etc.)
|
|
15491
15872
|
const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
15873
|
+
// skip if a leave superseded this join so codec detection doesn't resolve to a default factory.
|
|
15874
|
+
if (supersededByLeave()) {
|
|
15875
|
+
this.logger.debug('Join superseded by leave; skipping codec detection');
|
|
15876
|
+
return;
|
|
15877
|
+
}
|
|
15492
15878
|
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
15493
15879
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
15494
15880
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
@@ -15532,6 +15918,12 @@ class Call {
|
|
|
15532
15918
|
throw error;
|
|
15533
15919
|
}
|
|
15534
15920
|
}
|
|
15921
|
+
// If the user left while this join was in flight, bail before re-setting JOINED and before
|
|
15922
|
+
// peer-connection setup below (both run synchronously after this, so one check covers them).
|
|
15923
|
+
if (supersededByLeave()) {
|
|
15924
|
+
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
15925
|
+
return;
|
|
15926
|
+
}
|
|
15535
15927
|
if (!performingMigration) {
|
|
15536
15928
|
// in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
|
|
15537
15929
|
this.state.setCallingState(CallingState.JOINED);
|
|
@@ -15569,12 +15961,18 @@ class Call {
|
|
|
15569
15961
|
}
|
|
15570
15962
|
// device settings should be applied only once, we don't have to
|
|
15571
15963
|
// re-apply them on later reconnections or server-side data fetches
|
|
15572
|
-
if (!this.deviceSettingsAppliedOnce &&
|
|
15964
|
+
if (!this.deviceSettingsAppliedOnce &&
|
|
15965
|
+
this.state.settings &&
|
|
15966
|
+
!supersededByLeave()) {
|
|
15573
15967
|
await this.applyDeviceConfig(this.state.settings, true, false);
|
|
15968
|
+
this.deviceSettingsAppliedOnce = true;
|
|
15969
|
+
}
|
|
15970
|
+
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
15574
15971
|
globalThis.streamRNVideoSDK?.callManager.start({
|
|
15575
15972
|
isRingingTypeCall: this.ringing,
|
|
15973
|
+
cid: this.cid,
|
|
15576
15974
|
});
|
|
15577
|
-
this.
|
|
15975
|
+
this.callManagerStarted = true;
|
|
15578
15976
|
}
|
|
15579
15977
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
15580
15978
|
// as it's a one-time event and clashes with the potential reconnection attempts.
|
|
@@ -15701,6 +16099,7 @@ class Call {
|
|
|
15701
16099
|
this.sfuStatsReporter = undefined;
|
|
15702
16100
|
if (closePreviousInstances && this.subscriber) {
|
|
15703
16101
|
await this.subscriber.dispose();
|
|
16102
|
+
this.state.removeAllOrphanedTracks();
|
|
15704
16103
|
}
|
|
15705
16104
|
const basePeerConnectionOptions = {
|
|
15706
16105
|
sfuClient,
|
|
@@ -15711,6 +16110,7 @@ class Call {
|
|
|
15711
16110
|
enableTracing,
|
|
15712
16111
|
statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
|
|
15713
16112
|
clientPublishOptions: this.clientPublishOptions,
|
|
16113
|
+
e2ee: this.e2eeManager,
|
|
15714
16114
|
onReconnectionNeeded: (kind, reason, peerType) => {
|
|
15715
16115
|
this.reconnect(kind, reason).catch((err) => {
|
|
15716
16116
|
const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
|
|
@@ -15776,12 +16176,12 @@ class Call {
|
|
|
15776
16176
|
* Retrieves credentials for joining the call.
|
|
15777
16177
|
*
|
|
15778
16178
|
* @internal
|
|
15779
|
-
*
|
|
15780
16179
|
* @param data the join call data.
|
|
15781
16180
|
*/
|
|
15782
16181
|
doJoinRequest = async (data) => {
|
|
15783
16182
|
const location = await this.streamClient.getLocationHint();
|
|
15784
|
-
const
|
|
16183
|
+
const e2ee = !!this.e2eeManager;
|
|
16184
|
+
const request = { ...data, location, e2ee };
|
|
15785
16185
|
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
15786
16186
|
this.state.updateFromCallResponse(joinResponse.call);
|
|
15787
16187
|
this.state.setMembers(joinResponse.members);
|
|
@@ -15800,6 +16200,36 @@ class Call {
|
|
|
15800
16200
|
}
|
|
15801
16201
|
return joinResponse;
|
|
15802
16202
|
};
|
|
16203
|
+
/**
|
|
16204
|
+
* Whether the per-call media engine currently exists. True from join until leave.
|
|
16205
|
+
*
|
|
16206
|
+
* @internal an internal getter and should not be used outside the SDK.
|
|
16207
|
+
*/
|
|
16208
|
+
get hasMediaEngine() {
|
|
16209
|
+
return !!this.mediaEnginePromise;
|
|
16210
|
+
}
|
|
16211
|
+
/**
|
|
16212
|
+
* Ensures a {@link CallMediaEngine} exists for this call's media session and
|
|
16213
|
+
* returns it. Idempotent: the engine is created once via the registered
|
|
16214
|
+
* provider (see `setCallMediaEngineProvider`) and cached until `leave()`
|
|
16215
|
+
* disposes it. Concurrent callers (e.g. camera + microphone enabling in
|
|
16216
|
+
* parallel) share the same engine because the in-flight creation promise is
|
|
16217
|
+
* cached, never the unresolved result.
|
|
16218
|
+
*
|
|
16219
|
+
* @internal
|
|
16220
|
+
*/
|
|
16221
|
+
ensureMediaFactory = async () => {
|
|
16222
|
+
if (!this.mediaEnginePromise) {
|
|
16223
|
+
const provider = getCallMediaEngineProvider();
|
|
16224
|
+
this.logger.debug(`Requesting per-call media factory creation`);
|
|
16225
|
+
this.mediaEnginePromise = Promise.resolve(provider()).catch((err) => {
|
|
16226
|
+
// Drop the cached rejection so a retried join() can rebuild the engine
|
|
16227
|
+
this.mediaEnginePromise = undefined;
|
|
16228
|
+
throw err;
|
|
16229
|
+
});
|
|
16230
|
+
}
|
|
16231
|
+
return this.mediaEnginePromise;
|
|
16232
|
+
};
|
|
15803
16233
|
/**
|
|
15804
16234
|
* Handles the closing of the SFU signal connection.
|
|
15805
16235
|
*
|
|
@@ -16361,6 +16791,30 @@ class Call {
|
|
|
16361
16791
|
this.tracer.trace('updatePublishOptions', options);
|
|
16362
16792
|
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
16363
16793
|
};
|
|
16794
|
+
/**
|
|
16795
|
+
* Set the E2EE (end-to-end encryption) manager for this call.
|
|
16796
|
+
*
|
|
16797
|
+
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
16798
|
+
* configured for E2EE.
|
|
16799
|
+
*
|
|
16800
|
+
* The manager is kept across {@link leave} so a rejoin of this same instance
|
|
16801
|
+
* stays encrypted: do not dispose it while this call may be joined again.
|
|
16802
|
+
* A disposed manager throws from `encrypt`/`decrypt` rather than silently
|
|
16803
|
+
* publishing nothing, so re-attach a fresh one instead of reusing it.
|
|
16804
|
+
*
|
|
16805
|
+
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
16806
|
+
* built-in AES-GCM scheme, or pass your own implementation.
|
|
16807
|
+
* @throws if called after the peer connections have been built (i.e. after
|
|
16808
|
+
* `join`): those PCs were already configured without an encryptor, so
|
|
16809
|
+
* adopting a manager now would silently publish/receive cleartext for
|
|
16810
|
+
* the live session.
|
|
16811
|
+
*/
|
|
16812
|
+
setE2EEManager = (e2ee) => {
|
|
16813
|
+
if (this.publisher || this.subscriber) {
|
|
16814
|
+
throw new Error('setE2EEManager must be called before join()');
|
|
16815
|
+
}
|
|
16816
|
+
this.e2eeManager = e2ee;
|
|
16817
|
+
};
|
|
16364
16818
|
/**
|
|
16365
16819
|
* Notifies the SFU that a noise cancellation process has started.
|
|
16366
16820
|
*
|
|
@@ -16997,7 +17451,9 @@ class Call {
|
|
|
16997
17451
|
*/
|
|
16998
17452
|
applyDeviceConfig = async (settings, publish, skipSpeakerApply) => {
|
|
16999
17453
|
if (!skipSpeakerApply) {
|
|
17000
|
-
this.speaker.apply(settings)
|
|
17454
|
+
await this.speaker.apply(settings).catch((err) => {
|
|
17455
|
+
this.logger.warn('Speaker init failed', err);
|
|
17456
|
+
});
|
|
17001
17457
|
}
|
|
17002
17458
|
await this.camera.apply(settings.video, publish).catch((err) => {
|
|
17003
17459
|
this.logger.warn('Camera init failed', err);
|
|
@@ -17678,15 +18134,17 @@ class StableWSConnection {
|
|
|
17678
18134
|
this.client.logger.error(`Token not set, can't connect authenticate`);
|
|
17679
18135
|
return;
|
|
17680
18136
|
}
|
|
17681
|
-
const
|
|
18137
|
+
const wsAuthMessage = {
|
|
17682
18138
|
token,
|
|
18139
|
+
products: [WSAuthMessageProductsEnum.VIDEO],
|
|
17683
18140
|
user_details: {
|
|
17684
18141
|
id: user.id,
|
|
17685
18142
|
name: user.name,
|
|
17686
18143
|
image: user.image,
|
|
17687
18144
|
custom: user.custom,
|
|
17688
18145
|
},
|
|
17689
|
-
}
|
|
18146
|
+
};
|
|
18147
|
+
const authMessage = JSON.stringify(wsAuthMessage);
|
|
17690
18148
|
this._log(`onopen() - Sending auth message ${authMessage}`, {}, 'trace');
|
|
17691
18149
|
this.ws?.send(authMessage);
|
|
17692
18150
|
this._log('onopen() - onopen callback', { wsID });
|
|
@@ -18512,11 +18970,11 @@ class StreamClient {
|
|
|
18512
18970
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18513
18971
|
};
|
|
18514
18972
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18515
|
-
"1.
|
|
18973
|
+
"1.58.0";
|
|
18516
18974
|
getUserAgent = () => {
|
|
18517
18975
|
if (!this.cachedUserAgent) {
|
|
18518
18976
|
const { clientAppIdentifier = {} } = this.options;
|
|
18519
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
18977
|
+
const { sdkName = 'js', sdkVersion = "1.58.0", ...extras } = clientAppIdentifier;
|
|
18520
18978
|
this.cachedUserAgent = [
|
|
18521
18979
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18522
18980
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19749,5 +20207,323 @@ const humanize = (n) => {
|
|
|
19749
20207
|
return String(n);
|
|
19750
20208
|
};
|
|
19751
20209
|
|
|
19752
|
-
|
|
20210
|
+
/**
|
|
20211
|
+
* Tiny, type-safe event emitter.
|
|
20212
|
+
*
|
|
20213
|
+
* Usage styles:
|
|
20214
|
+
* - compose: `private readonly events = new TypedEventEmitter<MyMap>()`
|
|
20215
|
+
* - extend: `class Foo extends TypedEventEmitter<MyMap> {}`
|
|
20216
|
+
*
|
|
20217
|
+
* Listener exceptions (sync throw or rejected promise) are caught and logged,
|
|
20218
|
+
* so one bad listener cannot break dispatch for the rest.
|
|
20219
|
+
*/
|
|
20220
|
+
class TypedEventEmitter {
|
|
20221
|
+
logger;
|
|
20222
|
+
byEvent = new Map();
|
|
20223
|
+
constructor(logger = 'TypedEventEmitter') {
|
|
20224
|
+
this.logger =
|
|
20225
|
+
typeof logger === 'string' ? videoLoggerSystem.getLogger(logger) : logger;
|
|
20226
|
+
}
|
|
20227
|
+
on = (event, fn) => {
|
|
20228
|
+
let listeners = this.byEvent.get(event);
|
|
20229
|
+
if (!listeners) {
|
|
20230
|
+
listeners = new Set();
|
|
20231
|
+
this.byEvent.set(event, listeners);
|
|
20232
|
+
}
|
|
20233
|
+
listeners.add(fn);
|
|
20234
|
+
return () => this.off(event, fn);
|
|
20235
|
+
};
|
|
20236
|
+
off = (event, fn) => {
|
|
20237
|
+
const listeners = this.byEvent.get(event);
|
|
20238
|
+
if (!listeners)
|
|
20239
|
+
return;
|
|
20240
|
+
listeners.delete(fn);
|
|
20241
|
+
if (listeners.size === 0)
|
|
20242
|
+
this.byEvent.delete(event);
|
|
20243
|
+
};
|
|
20244
|
+
emit = (event, payload) => {
|
|
20245
|
+
const listeners = this.byEvent.get(event);
|
|
20246
|
+
if (!listeners || listeners.size === 0)
|
|
20247
|
+
return;
|
|
20248
|
+
for (const listener of [...listeners]) {
|
|
20249
|
+
this.invoke(() => listener(payload), event);
|
|
20250
|
+
}
|
|
20251
|
+
};
|
|
20252
|
+
removeAllListeners = (event) => {
|
|
20253
|
+
if (event === undefined) {
|
|
20254
|
+
this.byEvent.clear();
|
|
20255
|
+
}
|
|
20256
|
+
else {
|
|
20257
|
+
this.byEvent.delete(event);
|
|
20258
|
+
}
|
|
20259
|
+
};
|
|
20260
|
+
invoke = (run, event) => {
|
|
20261
|
+
try {
|
|
20262
|
+
const result = run();
|
|
20263
|
+
if (result && typeof result.then === 'function') {
|
|
20264
|
+
result.catch((err) => {
|
|
20265
|
+
this.logger.warn(`Listener for '${String(event)}' rejected`, err);
|
|
20266
|
+
});
|
|
20267
|
+
}
|
|
20268
|
+
}
|
|
20269
|
+
catch (err) {
|
|
20270
|
+
this.logger.warn(`Listener for '${String(event)}' threw`, err);
|
|
20271
|
+
}
|
|
20272
|
+
};
|
|
20273
|
+
}
|
|
20274
|
+
|
|
20275
|
+
/**
|
|
20276
|
+
* Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
|
|
20277
|
+
* transforms to RTCRtpSenders and RTCRtpReceivers.
|
|
20278
|
+
*/
|
|
20279
|
+
class EncryptionManager extends TypedEventEmitter {
|
|
20280
|
+
algorithm;
|
|
20281
|
+
transform;
|
|
20282
|
+
disposed = false;
|
|
20283
|
+
piped;
|
|
20284
|
+
userId;
|
|
20285
|
+
worker;
|
|
20286
|
+
workerUrl;
|
|
20287
|
+
constructor(userId, worker, workerUrl, algorithm, transform) {
|
|
20288
|
+
super('EncryptionManager');
|
|
20289
|
+
this.userId = userId;
|
|
20290
|
+
this.worker = worker;
|
|
20291
|
+
this.workerUrl = workerUrl;
|
|
20292
|
+
this.algorithm = algorithm;
|
|
20293
|
+
this.transform = transform;
|
|
20294
|
+
this.worker.addEventListener('message', this.handleWorkerMessage);
|
|
20295
|
+
this.worker.addEventListener('error', this.handleWorkerError);
|
|
20296
|
+
}
|
|
20297
|
+
/**
|
|
20298
|
+
* Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
|
|
20299
|
+
* Use it to guard UI, or to avoid calling {@link create} where it would throw.
|
|
20300
|
+
*/
|
|
20301
|
+
static isSupported = () => {
|
|
20302
|
+
return preferredTransform() !== undefined;
|
|
20303
|
+
};
|
|
20304
|
+
/**
|
|
20305
|
+
* Create an EncryptionManager instance and initialize the E2EE Web Worker.
|
|
20306
|
+
*
|
|
20307
|
+
* @param userId - The local user's ID, normally `call.currentUserId`.
|
|
20308
|
+
* @param options - the create options.
|
|
20309
|
+
* @throws {Error} If the browser lacks Encoded Transforms.
|
|
20310
|
+
*
|
|
20311
|
+
* @example
|
|
20312
|
+
* ```ts
|
|
20313
|
+
* if (EncryptionManager.isSupported()) {
|
|
20314
|
+
* const e2ee = await EncryptionManager.create(call.currentUserId);
|
|
20315
|
+
* call.setE2EEManager(e2ee);
|
|
20316
|
+
* e2ee.setSharedKey(0, keyBytes);
|
|
20317
|
+
* }
|
|
20318
|
+
*/
|
|
20319
|
+
static create = async (userId, options) => {
|
|
20320
|
+
const transform = preferredTransform();
|
|
20321
|
+
if (!transform) {
|
|
20322
|
+
throw new Error(`E2EE is not supported in this browser`);
|
|
20323
|
+
}
|
|
20324
|
+
const { e2eeWorker } = await import('./e2ee-worker.browser.es.js');
|
|
20325
|
+
const blob = new Blob([`(${e2eeWorker.toString()})()`], {
|
|
20326
|
+
type: 'application/javascript',
|
|
20327
|
+
});
|
|
20328
|
+
const url = URL.createObjectURL(blob);
|
|
20329
|
+
let worker;
|
|
20330
|
+
try {
|
|
20331
|
+
worker = new Worker(url, { name: 'stream-video-e2ee' });
|
|
20332
|
+
}
|
|
20333
|
+
catch (err) {
|
|
20334
|
+
// e.g. a CSP `worker-src` without `blob:`. Don't leak the object URL.
|
|
20335
|
+
URL.revokeObjectURL(url);
|
|
20336
|
+
throw err;
|
|
20337
|
+
}
|
|
20338
|
+
const algorithm = options?.algorithm ?? 'AES-128-GCM';
|
|
20339
|
+
return new EncryptionManager(userId, worker, url, algorithm, transform);
|
|
20340
|
+
};
|
|
20341
|
+
/**
|
|
20342
|
+
* {@link dispose} terminates the worker, so `postMessage` becomes a silent
|
|
20343
|
+
* no-op and an attached transform points at a dead worker: frames would stall
|
|
20344
|
+
* forever with no error and no event. Throwing is also fail-closed, since a
|
|
20345
|
+
* caller that swallows it still publishes nothing rather than cleartext.
|
|
20346
|
+
*/
|
|
20347
|
+
assertUsable = () => {
|
|
20348
|
+
if (this.disposed)
|
|
20349
|
+
throw new Error(`EncryptionManager is disposed`);
|
|
20350
|
+
};
|
|
20351
|
+
/**
|
|
20352
|
+
* Terminate the worker and release all resources.
|
|
20353
|
+
*
|
|
20354
|
+
* The manager is unusable afterwards and every other method throws. Call
|
|
20355
|
+
* {@link create} for a new one. Safe to call more than once.
|
|
20356
|
+
*/
|
|
20357
|
+
dispose = () => {
|
|
20358
|
+
if (this.disposed)
|
|
20359
|
+
return;
|
|
20360
|
+
this.disposed = true;
|
|
20361
|
+
this.piped = undefined;
|
|
20362
|
+
this.worker.removeEventListener('message', this.handleWorkerMessage);
|
|
20363
|
+
this.worker.removeEventListener('error', this.handleWorkerError);
|
|
20364
|
+
this.worker.terminate();
|
|
20365
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
20366
|
+
this.removeAllListeners();
|
|
20367
|
+
};
|
|
20368
|
+
/**
|
|
20369
|
+
* Set a per-user AES-GCM encryption key in the worker's key store.
|
|
20370
|
+
*
|
|
20371
|
+
* Use it when each participant has their own key from a central authority.
|
|
20372
|
+
* The receiver picks the right one by the `keyIndex` in the frame trailer.
|
|
20373
|
+
*
|
|
20374
|
+
* @param userId - The key owner.
|
|
20375
|
+
* @param keyIndex - Increases with each rotation.
|
|
20376
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20377
|
+
*/
|
|
20378
|
+
setKey = (userId, keyIndex, rawKey) => {
|
|
20379
|
+
this.assertUsable();
|
|
20380
|
+
this.validateKeyIndex(keyIndex);
|
|
20381
|
+
this.validateKeyLength(rawKey);
|
|
20382
|
+
this.worker.postMessage({ type: 'cmd.set_key', userId, keyIndex, rawKey });
|
|
20383
|
+
};
|
|
20384
|
+
/**
|
|
20385
|
+
* Fallback key for any user without a per-user key. The simplest E2EE mode:
|
|
20386
|
+
* one key for everyone, usually passphrase-derived, no distribution needed.
|
|
20387
|
+
* Setting an epoch makes it active for encryption while older epochs remain
|
|
20388
|
+
* available to decrypt in-flight frames until {@link removeSharedKey}.
|
|
20389
|
+
*
|
|
20390
|
+
* @param keyIndex - An integer 0-255, since one trailer byte carries it.
|
|
20391
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
20392
|
+
*/
|
|
20393
|
+
setSharedKey = (keyIndex, rawKey) => {
|
|
20394
|
+
this.assertUsable();
|
|
20395
|
+
this.validateKeyIndex(keyIndex);
|
|
20396
|
+
this.validateKeyLength(rawKey);
|
|
20397
|
+
this.worker.postMessage({ type: 'cmd.set_shared_key', keyIndex, rawKey });
|
|
20398
|
+
};
|
|
20399
|
+
/**
|
|
20400
|
+
* Remove one shared-key epoch from the worker's receive key ring.
|
|
20401
|
+
*
|
|
20402
|
+
* If this is the active epoch, shared-key encryption stops until
|
|
20403
|
+
* {@link setSharedKey} succeeds again. An older epoch is not reactivated.
|
|
20404
|
+
*
|
|
20405
|
+
* @param keyIndex - The exact shared-key epoch to remove.
|
|
20406
|
+
*/
|
|
20407
|
+
removeSharedKey = (keyIndex) => {
|
|
20408
|
+
this.assertUsable();
|
|
20409
|
+
this.validateKeyIndex(keyIndex);
|
|
20410
|
+
this.worker.postMessage({ type: 'cmd.remove_shared_key', keyIndex });
|
|
20411
|
+
};
|
|
20412
|
+
/**
|
|
20413
|
+
* Retire one of a user's key epochs, leaving their other epochs usable.
|
|
20414
|
+
*
|
|
20415
|
+
* @param userId - The key owner.
|
|
20416
|
+
* @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
|
|
20417
|
+
*/
|
|
20418
|
+
removeKey = (userId, keyIndex) => {
|
|
20419
|
+
this.assertUsable();
|
|
20420
|
+
this.validateKeyIndex(keyIndex);
|
|
20421
|
+
this.worker.postMessage({ type: 'cmd.remove_key', userId, keyIndex });
|
|
20422
|
+
};
|
|
20423
|
+
/**
|
|
20424
|
+
* Drop every key a user holds, revoking their ability to decrypt later
|
|
20425
|
+
* frames. Call it when a participant leaves.
|
|
20426
|
+
*
|
|
20427
|
+
* To retire one rotated epoch instead, use {@link removeKey}.
|
|
20428
|
+
*/
|
|
20429
|
+
removeAllKeys = (userId) => {
|
|
20430
|
+
this.assertUsable();
|
|
20431
|
+
this.worker.postMessage({ type: 'cmd.remove_all_keys', userId });
|
|
20432
|
+
};
|
|
20433
|
+
/**
|
|
20434
|
+
* Called by the Publisher when it adds a transceiver.
|
|
20435
|
+
*
|
|
20436
|
+
* @param sender - The sender to encrypt.
|
|
20437
|
+
* @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
|
|
20438
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20439
|
+
* @internal
|
|
20440
|
+
*/
|
|
20441
|
+
encrypt = (sender, codec, trackType) => {
|
|
20442
|
+
this.assertUsable();
|
|
20443
|
+
this.pipe(sender, {
|
|
20444
|
+
operation: 'encode',
|
|
20445
|
+
userId: this.userId,
|
|
20446
|
+
codec,
|
|
20447
|
+
trackType,
|
|
20448
|
+
});
|
|
20449
|
+
};
|
|
20450
|
+
/**
|
|
20451
|
+
* Called by the Subscriber when a remote track arrives.
|
|
20452
|
+
*
|
|
20453
|
+
* @param receiver - The receiver to decrypt.
|
|
20454
|
+
* @param userId - The remote user, for key lookup in the worker.
|
|
20455
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
20456
|
+
* @internal
|
|
20457
|
+
*/
|
|
20458
|
+
decrypt = (receiver, userId, trackType) => {
|
|
20459
|
+
this.assertUsable();
|
|
20460
|
+
this.pipe(receiver, { operation: 'decode', userId, trackType });
|
|
20461
|
+
};
|
|
20462
|
+
/** Pipe through the worker's transform, tracking targets to avoid double-piping. */
|
|
20463
|
+
pipe = (target, options) => {
|
|
20464
|
+
if (this.transform === 'script') {
|
|
20465
|
+
target.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
20466
|
+
return;
|
|
20467
|
+
}
|
|
20468
|
+
if ((this.piped ??= new WeakSet()).has(target))
|
|
20469
|
+
return;
|
|
20470
|
+
this.piped.add(target);
|
|
20471
|
+
// @ts-expect-error createEncodedStreams is not in the standard typedefs
|
|
20472
|
+
const { readable, writable } = target.createEncodedStreams();
|
|
20473
|
+
this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
|
|
20474
|
+
};
|
|
20475
|
+
/**
|
|
20476
|
+
* Toggle periodic performance reporting from the E2EE worker.
|
|
20477
|
+
*
|
|
20478
|
+
* While on, the worker emits `e2ee.perf_report` once per second with per-track
|
|
20479
|
+
* FPS and crypto timings. Useful for debugging throughput.
|
|
20480
|
+
*/
|
|
20481
|
+
enablePerformanceReporting = (enabled) => {
|
|
20482
|
+
this.assertUsable();
|
|
20483
|
+
this.worker.postMessage({
|
|
20484
|
+
type: 'cmd.enable_performance_reporting',
|
|
20485
|
+
enabled,
|
|
20486
|
+
});
|
|
20487
|
+
};
|
|
20488
|
+
/**
|
|
20489
|
+
* Request a snapshot of the worker's keys. It arrives later as the
|
|
20490
|
+
* `e2ee.key_state` event, listing fingerprints only, never key material.
|
|
20491
|
+
*/
|
|
20492
|
+
requestKeyState = () => {
|
|
20493
|
+
this.assertUsable();
|
|
20494
|
+
this.worker.postMessage({ type: 'cmd.request_key_state' });
|
|
20495
|
+
};
|
|
20496
|
+
handleWorkerMessage = (e) => {
|
|
20497
|
+
const { type, ...payload } = e.data ?? {};
|
|
20498
|
+
if (type === 'e2ee.error') {
|
|
20499
|
+
this.logger.error(e.data.message);
|
|
20500
|
+
return;
|
|
20501
|
+
}
|
|
20502
|
+
const event = type;
|
|
20503
|
+
this.logger.debug('Dispatching', event, payload);
|
|
20504
|
+
this.emit(event, payload);
|
|
20505
|
+
};
|
|
20506
|
+
handleWorkerError = (e) => {
|
|
20507
|
+
this.logger.error('Unhandled worker error:', e.message);
|
|
20508
|
+
};
|
|
20509
|
+
validateKeyLength = (rawKey) => {
|
|
20510
|
+
const is256 = this.algorithm === 'AES-256-GCM';
|
|
20511
|
+
const expected = is256 ? 32 : 16;
|
|
20512
|
+
if (rawKey.byteLength !== expected) {
|
|
20513
|
+
throw new Error(`Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`);
|
|
20514
|
+
}
|
|
20515
|
+
};
|
|
20516
|
+
/**
|
|
20517
|
+
* One trailer byte carries the keyIndex. A larger value would truncate to
|
|
20518
|
+
* `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
|
|
20519
|
+
* every decrypt. Reject it rather than ship a silently broken key epoch.
|
|
20520
|
+
*/
|
|
20521
|
+
validateKeyIndex = (keyIndex) => {
|
|
20522
|
+
if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
|
|
20523
|
+
throw new Error(`keyIndex must be an integer between 0 and 255, got ${keyIndex}`);
|
|
20524
|
+
}
|
|
20525
|
+
};
|
|
20526
|
+
}
|
|
20527
|
+
|
|
20528
|
+
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 };
|
|
19753
20529
|
//# sourceMappingURL=index.browser.es.js.map
|