@stream-io/video-client 1.57.0 → 1.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/e2ee-worker.browser.es.js +1342 -0
- package/dist/e2ee-worker.browser.es.js.map +1 -0
- package/dist/index.browser.es.js +1141 -117
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1196 -115
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +1174 -117
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +66 -9
- package/dist/src/StreamVideoClient.d.ts +9 -1
- package/dist/src/coordinator/connection/connection.d.ts +1 -1
- package/dist/src/devices/CameraManager.d.ts +8 -1
- package/dist/src/devices/DeviceManager.d.ts +2 -1
- package/dist/src/devices/MicrophoneManager.d.ts +8 -1
- package/dist/src/devices/SpeakerManager.d.ts +2 -1
- package/dist/src/events/participant.d.ts +10 -3
- package/dist/src/gen/coordinator/index.d.ts +1386 -240
- package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
- package/dist/src/helpers/MediaStreamRecorder.d.ts +26 -0
- package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
- package/dist/src/helpers/concurrency.d.ts +8 -0
- package/dist/src/helpers/ensureExhausted.d.ts +1 -1
- package/dist/src/helpers/loopback.d.ts +55 -0
- package/dist/src/permissions/PermissionsContext.d.ts +2 -2
- package/dist/src/reporting/ClientEventReporter.d.ts +0 -2
- package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
- package/dist/src/rtc/Subscriber.d.ts +5 -0
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
- package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
- package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
- package/dist/src/rtc/e2ee/events.d.ts +163 -0
- package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
- package/dist/src/rtc/index.d.ts +1 -0
- package/dist/src/rtc/mediaEngine.d.ts +23 -0
- package/dist/src/rtc/types.d.ts +2 -0
- package/dist/src/store/CallState.d.ts +21 -0
- package/dist/src/types.d.ts +15 -6
- package/index.ts +5 -0
- package/package.json +1 -1
- package/src/Call.ts +311 -155
- package/src/StreamVideoClient.ts +35 -8
- package/src/__tests__/Call.lifecycle.test.ts +33 -2
- package/src/__tests__/Call.publishing.test.ts +42 -1
- package/src/__tests__/Call.test.ts +15 -0
- package/src/__tests__/StreamVideoClient.api.test.ts +33 -0
- package/src/__tests__/StreamVideoClient.rewatch.test.ts +213 -0
- package/src/__tests__/StreamVideoClient.ringing.test.ts +11 -4
- package/src/__tests__/StreamVideoClient.test.ts +14 -12
- package/src/coordinator/connection/connection.ts +6 -3
- package/src/devices/AudioDeviceManager.ts +6 -0
- package/src/devices/CameraManager.ts +67 -8
- package/src/devices/DeviceManager.ts +24 -4
- package/src/devices/MicrophoneManager.ts +66 -8
- package/src/devices/ScreenShareManager.ts +3 -0
- package/src/devices/SpeakerManager.ts +42 -12
- package/src/devices/__tests__/CameraManager.test.ts +27 -1
- package/src/devices/__tests__/DeviceManager.test.ts +49 -1
- package/src/devices/__tests__/MicrophoneManager.test.ts +23 -1
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -20
- package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
- package/src/devices/__tests__/mocks.ts +36 -3
- package/src/events/__tests__/participant.test.ts +134 -1
- package/src/events/call.ts +6 -3
- package/src/events/callEventHandlers.ts +9 -3
- package/src/events/participant.ts +42 -7
- package/src/gen/coordinator/index.ts +1402 -236
- package/src/gen/video/sfu/models/models.ts +15 -0
- package/src/helpers/MediaStreamRecorder.ts +104 -0
- package/src/helpers/TypedEventEmitter.ts +71 -0
- package/src/helpers/__tests__/MediaStreamRecorder.test.ts +324 -0
- package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
- package/src/helpers/__tests__/concurrency.test.ts +74 -0
- package/src/helpers/__tests__/loopback.test.ts +367 -0
- package/src/helpers/concurrency.ts +24 -0
- package/src/helpers/ensureExhausted.ts +4 -2
- package/src/helpers/loopback.ts +200 -0
- package/src/permissions/PermissionsContext.ts +10 -5
- package/src/reporting/ClientEventReporter.ts +15 -28
- package/src/reporting/__tests__/ClientEventReporter.test.ts +3 -7
- package/src/rtc/BasePeerConnection.ts +12 -1
- package/src/rtc/Publisher.ts +8 -0
- package/src/rtc/Subscriber.ts +13 -0
- package/src/rtc/__tests__/Publisher.test.ts +101 -0
- package/src/rtc/__tests__/Subscriber.test.ts +190 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
- package/src/rtc/e2ee/E2EEManager.ts +12 -0
- package/src/rtc/e2ee/EncryptionManager.ts +336 -0
- package/src/rtc/e2ee/SPEC.md +586 -0
- package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
- package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
- package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
- package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
- package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
- package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
- package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
- package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
- package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
- package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
- package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
- package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
- package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
- package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
- package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
- package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
- package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
- package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
- package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
- package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
- package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
- package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
- package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
- package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
- package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
- package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
- package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
- package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
- package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
- package/src/rtc/e2ee/e2ee-worker.ts +27 -0
- package/src/rtc/e2ee/events.ts +179 -0
- package/src/rtc/e2ee/transformSupport.ts +35 -0
- package/src/rtc/helpers/rtcConfiguration.ts +4 -2
- package/src/rtc/index.ts +1 -0
- package/src/rtc/mediaEngine.ts +44 -0
- package/src/rtc/types.ts +2 -0
- package/src/store/CallState.ts +33 -1
- package/src/store/__tests__/CallState.test.ts +82 -1
- package/src/types.ts +23 -5
package/src/Call.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { StreamSfuClient } from './StreamSfuClient';
|
|
|
2
2
|
import { SfuJoinError } from './errors';
|
|
3
3
|
import {
|
|
4
4
|
BasePeerConnectionOpts,
|
|
5
|
+
type CallMediaEngine,
|
|
5
6
|
Dispatcher,
|
|
7
|
+
getCallMediaEngineProvider,
|
|
6
8
|
getGenericSdp,
|
|
7
9
|
isAudioTrackType,
|
|
8
10
|
isSfuEvent,
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
TrackPublishOptions,
|
|
16
18
|
trackTypeToParticipantStreamKey,
|
|
17
19
|
} from './rtc';
|
|
20
|
+
import type { E2EEManager } from './rtc/e2ee/E2EEManager';
|
|
18
21
|
import {
|
|
19
22
|
registerEventHandlers,
|
|
20
23
|
registerRingingCallEventHandlers,
|
|
@@ -30,12 +33,13 @@ import {
|
|
|
30
33
|
getCurrentValue,
|
|
31
34
|
} from './store/rxUtils';
|
|
32
35
|
import { ScopedLogger, videoLoggerSystem } from './logger';
|
|
33
|
-
import
|
|
36
|
+
import {
|
|
34
37
|
AcceptCallResponse,
|
|
35
38
|
BlockUserRequest,
|
|
36
39
|
BlockUserResponse,
|
|
37
40
|
CallRingEvent,
|
|
38
41
|
CallSettingsResponse,
|
|
42
|
+
CallStateResponseFields,
|
|
39
43
|
CollectUserFeedbackRequest,
|
|
40
44
|
CollectUserFeedbackResponse,
|
|
41
45
|
Credentials,
|
|
@@ -76,8 +80,8 @@ import type {
|
|
|
76
80
|
RingCallResponse,
|
|
77
81
|
SendCallEventRequest,
|
|
78
82
|
SendCallEventResponse,
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
SendVideoReactionRequest,
|
|
84
|
+
SendVideoReactionResponse,
|
|
81
85
|
StartClosedCaptionsRequest,
|
|
82
86
|
StartClosedCaptionsResponse,
|
|
83
87
|
StartFrameRecordingRequest,
|
|
@@ -109,6 +113,8 @@ import type {
|
|
|
109
113
|
UpdateCallRequest,
|
|
110
114
|
UpdateCallResponse,
|
|
111
115
|
UpdateUserPermissionsRequest,
|
|
116
|
+
UpdateUserPermissionsRequestGrantPermissionsEnum,
|
|
117
|
+
UpdateUserPermissionsRequestRevokePermissionsEnum,
|
|
112
118
|
UpdateUserPermissionsResponse,
|
|
113
119
|
} from './gen/coordinator';
|
|
114
120
|
import { OwnCapability } from './gen/coordinator';
|
|
@@ -173,7 +179,11 @@ import {
|
|
|
173
179
|
SpeakerManager,
|
|
174
180
|
} from './devices';
|
|
175
181
|
import { normalize } from './devices/devicePersistence';
|
|
176
|
-
import {
|
|
182
|
+
import {
|
|
183
|
+
hasPending,
|
|
184
|
+
singleFlight,
|
|
185
|
+
withoutConcurrency,
|
|
186
|
+
} from './helpers/concurrency';
|
|
177
187
|
import { ensureExhausted } from './helpers/ensureExhausted';
|
|
178
188
|
import { pushToIfMissing } from './helpers/array';
|
|
179
189
|
import {
|
|
@@ -266,6 +276,7 @@ export class Call {
|
|
|
266
276
|
|
|
267
277
|
subscriber?: Subscriber;
|
|
268
278
|
publisher?: Publisher;
|
|
279
|
+
e2eeManager?: E2EEManager;
|
|
269
280
|
|
|
270
281
|
/**
|
|
271
282
|
* Flag telling whether this call is a "ringing" call.
|
|
@@ -328,6 +339,8 @@ export class Call {
|
|
|
328
339
|
private allowOwnTracksLoopback = false;
|
|
329
340
|
private hasJoinedOnce = false;
|
|
330
341
|
private deviceSettingsAppliedOnce = false;
|
|
342
|
+
private callManagerStarted = false;
|
|
343
|
+
private leaveGeneration = 0;
|
|
331
344
|
private credentials?: Credentials;
|
|
332
345
|
|
|
333
346
|
private initialized = false;
|
|
@@ -351,6 +364,14 @@ export class Call {
|
|
|
351
364
|
ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
|
|
352
365
|
]);
|
|
353
366
|
|
|
367
|
+
/**
|
|
368
|
+
* The in-flight per-call media engine. On web/React this resolves to a thin
|
|
369
|
+
* globals-backed engine (no provider registered); React Native registers a
|
|
370
|
+
* provider that owns a per-call native factory.
|
|
371
|
+
* @internal
|
|
372
|
+
*/
|
|
373
|
+
private mediaEnginePromise?: Promise<CallMediaEngine>;
|
|
374
|
+
|
|
354
375
|
/**
|
|
355
376
|
* Constructs a new `Call` instance.
|
|
356
377
|
*
|
|
@@ -699,6 +720,8 @@ export class Call {
|
|
|
699
720
|
return;
|
|
700
721
|
}
|
|
701
722
|
|
|
723
|
+
this.leaveGeneration += 1;
|
|
724
|
+
|
|
702
725
|
if (callingState === CallingState.JOINING) {
|
|
703
726
|
const waitUntilCallJoined = () => {
|
|
704
727
|
return new Promise<void>((resolve) => {
|
|
@@ -735,7 +758,10 @@ export class Call {
|
|
|
735
758
|
}
|
|
736
759
|
}
|
|
737
760
|
}
|
|
738
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(
|
|
761
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(
|
|
762
|
+
this,
|
|
763
|
+
reason === 'ended' ? 'remote' : undefined,
|
|
764
|
+
);
|
|
739
765
|
|
|
740
766
|
this.statsReporter?.stop();
|
|
741
767
|
this.statsReporter = undefined;
|
|
@@ -799,6 +825,7 @@ export class Call {
|
|
|
799
825
|
|
|
800
826
|
globalThis.streamRNVideoSDK?.callManager.stop({
|
|
801
827
|
isRingingTypeCall: this.ringing,
|
|
828
|
+
shouldStopCallManager: this.callManagerStarted,
|
|
802
829
|
});
|
|
803
830
|
|
|
804
831
|
this.camera.dispose();
|
|
@@ -806,6 +833,7 @@ export class Call {
|
|
|
806
833
|
this.screenShare.dispose();
|
|
807
834
|
this.speaker.dispose();
|
|
808
835
|
this.deviceSettingsAppliedOnce = false;
|
|
836
|
+
this.callManagerStarted = false;
|
|
809
837
|
|
|
810
838
|
const stopOnLeavePromises: Promise<void>[] = [];
|
|
811
839
|
if (this.camera.stopOnLeave) {
|
|
@@ -818,6 +846,23 @@ export class Call {
|
|
|
818
846
|
stopOnLeavePromises.push(this.screenShare.disable(true));
|
|
819
847
|
}
|
|
820
848
|
await Promise.all(stopOnLeavePromises);
|
|
849
|
+
|
|
850
|
+
// Dispose the per-call media engine last — after peer connections and
|
|
851
|
+
// local tracks are gone — so the backing factory tears down with no
|
|
852
|
+
// owned PCs/tracks. A fresh `join()` builds a new engine.
|
|
853
|
+
if (this.mediaEnginePromise) {
|
|
854
|
+
const enginePromise = this.mediaEnginePromise;
|
|
855
|
+
this.mediaEnginePromise = undefined;
|
|
856
|
+
this.logger.debug('Disposing per-call media factory');
|
|
857
|
+
await enginePromise
|
|
858
|
+
.then((engine) => {
|
|
859
|
+
globalThis.streamRNVideoSDK?.callingX?.unwireAudioEngineSubscription();
|
|
860
|
+
return engine.dispose();
|
|
861
|
+
})
|
|
862
|
+
.catch((err) => {
|
|
863
|
+
this.logger.warn('Failed to dispose media engine', err);
|
|
864
|
+
});
|
|
865
|
+
}
|
|
821
866
|
});
|
|
822
867
|
};
|
|
823
868
|
|
|
@@ -899,8 +944,19 @@ export class Call {
|
|
|
899
944
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
900
945
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
901
946
|
this.clientStore.setCalls([this, ...calls]);
|
|
902
|
-
|
|
903
|
-
|
|
947
|
+
await this.applyDeviceConfig(settings, { publish: false });
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Applies a backend-provided call state snapshot
|
|
952
|
+
* (call, members, own capabilities) to this instance.
|
|
953
|
+
*
|
|
954
|
+
* @internal an internal method and should not be used outside the SDK.
|
|
955
|
+
*/
|
|
956
|
+
updateFromCallStateResponse = (response: CallStateResponseFields) => {
|
|
957
|
+
this.state.updateFromCallResponse(response.call);
|
|
958
|
+
this.state.setMembers(response.members);
|
|
959
|
+
this.state.setOwnCapabilities(response.own_capabilities);
|
|
904
960
|
};
|
|
905
961
|
|
|
906
962
|
/**
|
|
@@ -924,9 +980,7 @@ export class Call {
|
|
|
924
980
|
params,
|
|
925
981
|
);
|
|
926
982
|
|
|
927
|
-
this.
|
|
928
|
-
this.state.setMembers(response.members);
|
|
929
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
983
|
+
this.updateFromCallStateResponse(response);
|
|
930
984
|
|
|
931
985
|
if (params?.ring) {
|
|
932
986
|
this.ringingSubject.next(true);
|
|
@@ -936,17 +990,7 @@ export class Call {
|
|
|
936
990
|
this.watching = true;
|
|
937
991
|
this.clientStore.registerOrUpdateCall(this);
|
|
938
992
|
}
|
|
939
|
-
|
|
940
|
-
const skipSpeakerApply = isReactNative()
|
|
941
|
-
? params?.ring === true
|
|
942
|
-
? true
|
|
943
|
-
: this.ringing
|
|
944
|
-
: false;
|
|
945
|
-
await this.applyDeviceConfig(
|
|
946
|
-
response.call.settings,
|
|
947
|
-
false,
|
|
948
|
-
skipSpeakerApply,
|
|
949
|
-
);
|
|
993
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
950
994
|
|
|
951
995
|
return response;
|
|
952
996
|
};
|
|
@@ -964,9 +1008,7 @@ export class Call {
|
|
|
964
1008
|
GetOrCreateCallRequest
|
|
965
1009
|
>(this.streamClientBasePath, data);
|
|
966
1010
|
|
|
967
|
-
this.
|
|
968
|
-
this.state.setMembers(response.members);
|
|
969
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
1011
|
+
this.updateFromCallStateResponse(response);
|
|
970
1012
|
|
|
971
1013
|
if (data?.ring) {
|
|
972
1014
|
this.ringingSubject.next(true);
|
|
@@ -977,17 +1019,7 @@ export class Call {
|
|
|
977
1019
|
this.clientStore.registerOrUpdateCall(this);
|
|
978
1020
|
}
|
|
979
1021
|
|
|
980
|
-
|
|
981
|
-
const skipSpeakerApply = isReactNative()
|
|
982
|
-
? data?.ring === true
|
|
983
|
-
? true
|
|
984
|
-
: this.ringing
|
|
985
|
-
: false;
|
|
986
|
-
await this.applyDeviceConfig(
|
|
987
|
-
response.call.settings,
|
|
988
|
-
false,
|
|
989
|
-
skipSpeakerApply,
|
|
990
|
-
);
|
|
1022
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
991
1023
|
|
|
992
1024
|
return response;
|
|
993
1025
|
};
|
|
@@ -1074,112 +1106,113 @@ export class Call {
|
|
|
1074
1106
|
*
|
|
1075
1107
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
1076
1108
|
*/
|
|
1077
|
-
join =
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1109
|
+
join = singleFlight(
|
|
1110
|
+
async ({
|
|
1111
|
+
maxJoinRetries = 3,
|
|
1112
|
+
joinResponseTimeout,
|
|
1113
|
+
rpcRequestTimeout,
|
|
1114
|
+
allowOwnTracksLoopback = false,
|
|
1115
|
+
...data
|
|
1116
|
+
}: JoinCallData & {
|
|
1117
|
+
maxJoinRetries?: number;
|
|
1118
|
+
joinResponseTimeout?: number;
|
|
1119
|
+
rpcRequestTimeout?: number;
|
|
1120
|
+
allowOwnTracksLoopback?: boolean;
|
|
1121
|
+
} = {}): Promise<void> => {
|
|
1122
|
+
const callingState = this.state.callingState;
|
|
1090
1123
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1124
|
+
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
1125
|
+
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
1126
|
+
}
|
|
1094
1127
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1128
|
+
// we need this to be set before the callingx.joinCall() is
|
|
1129
|
+
// called to avoid registering the test call in the CallKit/Telecom
|
|
1130
|
+
this.allowOwnTracksLoopback = allowOwnTracksLoopback;
|
|
1098
1131
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1132
|
+
if (data?.ring) {
|
|
1133
|
+
this.ringingSubject.next(true);
|
|
1134
|
+
}
|
|
1102
1135
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1136
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
1137
|
+
if (callingX) {
|
|
1138
|
+
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
1139
|
+
await callingX.joinCall(this, this.clientStore.calls);
|
|
1140
|
+
}
|
|
1108
1141
|
|
|
1109
|
-
|
|
1142
|
+
await this.setup();
|
|
1110
1143
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
});
|
|
1144
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
1145
|
+
callType: this.type,
|
|
1146
|
+
callId: this.id,
|
|
1147
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
1148
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
1149
|
+
});
|
|
1118
1150
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1151
|
+
this.joinResponseTimeout = joinResponseTimeout;
|
|
1152
|
+
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
1153
|
+
// we will count the number of join failures per SFU.
|
|
1154
|
+
// once the number of failures reaches 2, we will piggyback on the `migrating_from`
|
|
1155
|
+
// field to force the coordinator to provide us another SFU
|
|
1156
|
+
const sfuJoinFailures = new Map<string, number>();
|
|
1157
|
+
const joinData: JoinCallData = data;
|
|
1158
|
+
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
1159
|
+
try {
|
|
1160
|
+
await this.clientEventReporter.withJoinLifecycle(
|
|
1161
|
+
this.cid,
|
|
1162
|
+
'first-attempt',
|
|
1163
|
+
async () => {
|
|
1164
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
1165
|
+
try {
|
|
1166
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
1167
|
+
await this.doJoin(data);
|
|
1168
|
+
delete joinData.migrating_from;
|
|
1169
|
+
delete joinData.migrating_from_list;
|
|
1170
|
+
return;
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
1173
|
+
if (
|
|
1174
|
+
(err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
1175
|
+
(err instanceof SfuJoinError && err.unrecoverable)
|
|
1176
|
+
) {
|
|
1177
|
+
throw err;
|
|
1178
|
+
}
|
|
1147
1179
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
);
|
|
1161
|
-
if (attempt < maxJoinRetries - 1) {
|
|
1162
|
-
this.clientEventReporter.startCorrelation(
|
|
1163
|
-
this.cid,
|
|
1164
|
-
'first-attempt',
|
|
1180
|
+
const switchSfu =
|
|
1181
|
+
err instanceof SfuJoinError &&
|
|
1182
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
1183
|
+
|
|
1184
|
+
const sfuId = this.credentials?.server.edge_name;
|
|
1185
|
+
if (sfuId) {
|
|
1186
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
1187
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
1188
|
+
if (switchSfu || failures >= 2) {
|
|
1189
|
+
joinData.migrating_from = sfuId;
|
|
1190
|
+
joinData.migrating_from_list = Array.from(
|
|
1191
|
+
sfuJoinFailures.keys(),
|
|
1165
1192
|
);
|
|
1193
|
+
if (attempt < maxJoinRetries - 1) {
|
|
1194
|
+
this.clientEventReporter.startCorrelation(
|
|
1195
|
+
this.cid,
|
|
1196
|
+
'first-attempt',
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1166
1199
|
}
|
|
1167
1200
|
}
|
|
1168
|
-
}
|
|
1169
1201
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1202
|
+
if (attempt === maxJoinRetries - 1) {
|
|
1203
|
+
throw err;
|
|
1204
|
+
}
|
|
1172
1205
|
}
|
|
1206
|
+
await sleep(retryInterval(attempt));
|
|
1173
1207
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
};
|
|
1208
|
+
},
|
|
1209
|
+
);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
callingX?.endCall(this, 'error');
|
|
1212
|
+
throw error;
|
|
1213
|
+
}
|
|
1214
|
+
},
|
|
1215
|
+
);
|
|
1183
1216
|
|
|
1184
1217
|
/**
|
|
1185
1218
|
* Will make a single attempt to watch for call related WebSocket events
|
|
@@ -1190,12 +1223,26 @@ export class Call {
|
|
|
1190
1223
|
private doJoin = async (data?: JoinCallData): Promise<void> => {
|
|
1191
1224
|
const connectStartTime = Date.now();
|
|
1192
1225
|
const callingState = this.state.callingState;
|
|
1226
|
+
const joinLeaveGeneration = this.leaveGeneration;
|
|
1227
|
+
const supersededByLeave = () =>
|
|
1228
|
+
this.leaveGeneration !== joinLeaveGeneration;
|
|
1193
1229
|
|
|
1194
1230
|
this.joinCallData = data;
|
|
1195
1231
|
|
|
1196
1232
|
this.logger.debug('Starting join flow');
|
|
1197
1233
|
this.state.setCallingState(CallingState.JOINING);
|
|
1198
1234
|
|
|
1235
|
+
// Ensure the per-call media engine exists before any peer connection
|
|
1236
|
+
// (codec probe, subscriber, publisher) or capture happens, so the WebRTC
|
|
1237
|
+
// globals resolve to the call's factory. Idempotent across
|
|
1238
|
+
// reconnect/migration attempts.
|
|
1239
|
+
await this.ensureMediaFactory();
|
|
1240
|
+
|
|
1241
|
+
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
1242
|
+
if (callingX) {
|
|
1243
|
+
callingX.wireAudioEngineSubscription();
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1199
1246
|
const performingMigration =
|
|
1200
1247
|
this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
1201
1248
|
const performingRejoin =
|
|
@@ -1266,6 +1313,11 @@ export class Call {
|
|
|
1266
1313
|
// the capabilities of the client (codec support, etc.)
|
|
1267
1314
|
const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } =
|
|
1268
1315
|
this.clientPublishOptions || {};
|
|
1316
|
+
// skip if a leave superseded this join so codec detection doesn't resolve to a default factory.
|
|
1317
|
+
if (supersededByLeave()) {
|
|
1318
|
+
this.logger.debug('Join superseded by leave; skipping codec detection');
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1269
1321
|
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
1270
1322
|
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
1271
1323
|
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
@@ -1325,6 +1377,13 @@ export class Call {
|
|
|
1325
1377
|
}
|
|
1326
1378
|
}
|
|
1327
1379
|
|
|
1380
|
+
// If the user left while this join was in flight, bail before re-setting JOINED and before
|
|
1381
|
+
// peer-connection setup below (both run synchronously after this, so one check covers them).
|
|
1382
|
+
if (supersededByLeave()) {
|
|
1383
|
+
this.logger.debug('Join superseded by leave; aborting join flow');
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1328
1387
|
if (!performingMigration) {
|
|
1329
1388
|
// in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
|
|
1330
1389
|
this.state.setCallingState(CallingState.JOINED);
|
|
@@ -1371,12 +1430,24 @@ export class Call {
|
|
|
1371
1430
|
|
|
1372
1431
|
// device settings should be applied only once, we don't have to
|
|
1373
1432
|
// re-apply them on later reconnections or server-side data fetches
|
|
1374
|
-
if (
|
|
1375
|
-
|
|
1433
|
+
if (
|
|
1434
|
+
!this.deviceSettingsAppliedOnce &&
|
|
1435
|
+
this.state.settings &&
|
|
1436
|
+
!supersededByLeave()
|
|
1437
|
+
) {
|
|
1438
|
+
await this.applyDeviceConfig(this.state.settings, {
|
|
1439
|
+
publish: true,
|
|
1440
|
+
skipSpeakerApply: false,
|
|
1441
|
+
});
|
|
1442
|
+
this.deviceSettingsAppliedOnce = true;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
1376
1446
|
globalThis.streamRNVideoSDK?.callManager.start({
|
|
1377
1447
|
isRingingTypeCall: this.ringing,
|
|
1448
|
+
cid: this.cid,
|
|
1378
1449
|
});
|
|
1379
|
-
this.
|
|
1450
|
+
this.callManagerStarted = true;
|
|
1380
1451
|
}
|
|
1381
1452
|
|
|
1382
1453
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
@@ -1542,6 +1613,7 @@ export class Call {
|
|
|
1542
1613
|
this.sfuStatsReporter = undefined;
|
|
1543
1614
|
if (closePreviousInstances && this.subscriber) {
|
|
1544
1615
|
await this.subscriber.dispose();
|
|
1616
|
+
this.state.removeAllOrphanedTracks();
|
|
1545
1617
|
}
|
|
1546
1618
|
const basePeerConnectionOptions: BasePeerConnectionOpts = {
|
|
1547
1619
|
sfuClient,
|
|
@@ -1552,6 +1624,7 @@ export class Call {
|
|
|
1552
1624
|
enableTracing,
|
|
1553
1625
|
statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
|
|
1554
1626
|
clientPublishOptions: this.clientPublishOptions,
|
|
1627
|
+
e2ee: this.e2eeManager,
|
|
1555
1628
|
onReconnectionNeeded: (kind, reason, peerType) => {
|
|
1556
1629
|
this.reconnect(kind, reason).catch((err) => {
|
|
1557
1630
|
const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
|
|
@@ -1628,12 +1701,12 @@ export class Call {
|
|
|
1628
1701
|
* Retrieves credentials for joining the call.
|
|
1629
1702
|
*
|
|
1630
1703
|
* @internal
|
|
1631
|
-
*
|
|
1632
1704
|
* @param data the join call data.
|
|
1633
1705
|
*/
|
|
1634
1706
|
doJoinRequest = async (data?: JoinCallData): Promise<JoinCallResponse> => {
|
|
1635
1707
|
const location = await this.streamClient.getLocationHint();
|
|
1636
|
-
const
|
|
1708
|
+
const e2ee = !!this.e2eeManager;
|
|
1709
|
+
const request: JoinCallRequest = { ...data, location, e2ee };
|
|
1637
1710
|
const joinResponse = await this.streamClient.post<
|
|
1638
1711
|
JoinCallResponse,
|
|
1639
1712
|
JoinCallRequest
|
|
@@ -1663,6 +1736,39 @@ export class Call {
|
|
|
1663
1736
|
return joinResponse;
|
|
1664
1737
|
};
|
|
1665
1738
|
|
|
1739
|
+
/**
|
|
1740
|
+
* Whether the per-call media engine currently exists. True from join until leave.
|
|
1741
|
+
*
|
|
1742
|
+
* @internal an internal getter and should not be used outside the SDK.
|
|
1743
|
+
*/
|
|
1744
|
+
get hasMediaEngine(): boolean {
|
|
1745
|
+
return !!this.mediaEnginePromise;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Ensures a {@link CallMediaEngine} exists for this call's media session and
|
|
1750
|
+
* returns it. Idempotent: the engine is created once via the registered
|
|
1751
|
+
* provider (see `setCallMediaEngineProvider`) and cached until `leave()`
|
|
1752
|
+
* disposes it. Concurrent callers (e.g. camera + microphone enabling in
|
|
1753
|
+
* parallel) share the same engine because the in-flight creation promise is
|
|
1754
|
+
* cached, never the unresolved result.
|
|
1755
|
+
*
|
|
1756
|
+
* @internal
|
|
1757
|
+
*/
|
|
1758
|
+
ensureMediaFactory = async (): Promise<CallMediaEngine> => {
|
|
1759
|
+
if (!this.mediaEnginePromise) {
|
|
1760
|
+
const provider = getCallMediaEngineProvider();
|
|
1761
|
+
|
|
1762
|
+
this.logger.debug(`Requesting per-call media factory creation`);
|
|
1763
|
+
this.mediaEnginePromise = Promise.resolve(provider()).catch((err) => {
|
|
1764
|
+
// Drop the cached rejection so a retried join() can rebuild the engine
|
|
1765
|
+
this.mediaEnginePromise = undefined;
|
|
1766
|
+
throw err;
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
return this.mediaEnginePromise;
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1666
1772
|
/**
|
|
1667
1773
|
* Handles the closing of the SFU signal connection.
|
|
1668
1774
|
*
|
|
@@ -2312,12 +2418,18 @@ export class Call {
|
|
|
2312
2418
|
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
2313
2419
|
if (!streamStateProp) continue;
|
|
2314
2420
|
|
|
2315
|
-
this.state.updateParticipant(sessionId, (p) =>
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2421
|
+
this.state.updateParticipant(sessionId, (p) => {
|
|
2422
|
+
const currentStream = p[streamStateProp];
|
|
2423
|
+
const isEchoedStream =
|
|
2424
|
+
this.subscriber?.isSelfSubscribedStream(currentStream);
|
|
2425
|
+
|
|
2426
|
+
return {
|
|
2427
|
+
publishedTracks: mediaStream
|
|
2428
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
2429
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
2430
|
+
[streamStateProp]: isEchoedStream ? currentStream : mediaStream,
|
|
2431
|
+
};
|
|
2432
|
+
});
|
|
2321
2433
|
}
|
|
2322
2434
|
};
|
|
2323
2435
|
|
|
@@ -2355,6 +2467,31 @@ export class Call {
|
|
|
2355
2467
|
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
2356
2468
|
};
|
|
2357
2469
|
|
|
2470
|
+
/**
|
|
2471
|
+
* Set the E2EE (end-to-end encryption) manager for this call.
|
|
2472
|
+
*
|
|
2473
|
+
* Must be called before {@link join} so the RTCPeerConnection can be
|
|
2474
|
+
* configured for E2EE.
|
|
2475
|
+
*
|
|
2476
|
+
* The manager is kept across {@link leave} so a rejoin of this same instance
|
|
2477
|
+
* stays encrypted: do not dispose it while this call may be joined again.
|
|
2478
|
+
* A disposed manager throws from `encrypt`/`decrypt` rather than silently
|
|
2479
|
+
* publishing nothing, so re-attach a fresh one instead of reusing it.
|
|
2480
|
+
*
|
|
2481
|
+
* @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
|
|
2482
|
+
* built-in AES-GCM scheme, or pass your own implementation.
|
|
2483
|
+
* @throws if called after the peer connections have been built (i.e. after
|
|
2484
|
+
* `join`): those PCs were already configured without an encryptor, so
|
|
2485
|
+
* adopting a manager now would silently publish/receive cleartext for
|
|
2486
|
+
* the live session.
|
|
2487
|
+
*/
|
|
2488
|
+
setE2EEManager = (e2ee: E2EEManager) => {
|
|
2489
|
+
if (this.publisher || this.subscriber) {
|
|
2490
|
+
throw new Error('setE2EEManager must be called before join()');
|
|
2491
|
+
}
|
|
2492
|
+
this.e2eeManager = e2ee;
|
|
2493
|
+
};
|
|
2494
|
+
|
|
2358
2495
|
/**
|
|
2359
2496
|
* Notifies the SFU that a noise cancellation process has started.
|
|
2360
2497
|
*
|
|
@@ -2453,9 +2590,9 @@ export class Call {
|
|
|
2453
2590
|
* @param reaction the reaction to send.
|
|
2454
2591
|
*/
|
|
2455
2592
|
sendReaction = async (
|
|
2456
|
-
reaction:
|
|
2457
|
-
): Promise<
|
|
2458
|
-
return this.streamClient.post
|
|
2593
|
+
reaction: SendVideoReactionRequest,
|
|
2594
|
+
): Promise<SendVideoReactionResponse> => {
|
|
2595
|
+
return this.streamClient.post(
|
|
2459
2596
|
`${this.streamClientBasePath}/reaction`,
|
|
2460
2597
|
reaction,
|
|
2461
2598
|
);
|
|
@@ -2673,7 +2810,7 @@ export class Call {
|
|
|
2673
2810
|
): Promise<RequestPermissionResponse> => {
|
|
2674
2811
|
const { permissions } = data;
|
|
2675
2812
|
const canRequestPermissions = permissions.every((permission) =>
|
|
2676
|
-
this.permissionsContext.canRequest(permission
|
|
2813
|
+
this.permissionsContext.canRequest(permission),
|
|
2677
2814
|
);
|
|
2678
2815
|
if (!canRequestPermissions) {
|
|
2679
2816
|
throw new Error(
|
|
@@ -2698,10 +2835,14 @@ export class Call {
|
|
|
2698
2835
|
* @param userId the id of the user to grant permissions to.
|
|
2699
2836
|
* @param permissions the permissions to grant.
|
|
2700
2837
|
*/
|
|
2701
|
-
grantPermissions = async (
|
|
2838
|
+
grantPermissions = async (
|
|
2839
|
+
userId: string,
|
|
2840
|
+
permissions: string[] | UpdateUserPermissionsRequestGrantPermissionsEnum[],
|
|
2841
|
+
) => {
|
|
2702
2842
|
return this.updateUserPermissions({
|
|
2703
2843
|
user_id: userId,
|
|
2704
|
-
grant_permissions:
|
|
2844
|
+
grant_permissions:
|
|
2845
|
+
permissions as UpdateUserPermissionsRequestGrantPermissionsEnum[],
|
|
2705
2846
|
});
|
|
2706
2847
|
};
|
|
2707
2848
|
|
|
@@ -2717,10 +2858,14 @@ export class Call {
|
|
|
2717
2858
|
* @param userId the id of the user to revoke permissions from.
|
|
2718
2859
|
* @param permissions the permissions to revoke.
|
|
2719
2860
|
*/
|
|
2720
|
-
revokePermissions = async (
|
|
2861
|
+
revokePermissions = async (
|
|
2862
|
+
userId: string,
|
|
2863
|
+
permissions: string[] | UpdateUserPermissionsRequestRevokePermissionsEnum[],
|
|
2864
|
+
) => {
|
|
2721
2865
|
return this.updateUserPermissions({
|
|
2722
2866
|
user_id: userId,
|
|
2723
|
-
revoke_permissions:
|
|
2867
|
+
revoke_permissions:
|
|
2868
|
+
permissions as UpdateUserPermissionsRequestRevokePermissionsEnum[],
|
|
2724
2869
|
});
|
|
2725
2870
|
};
|
|
2726
2871
|
|
|
@@ -3235,18 +3380,29 @@ export class Call {
|
|
|
3235
3380
|
*/
|
|
3236
3381
|
applyDeviceConfig = async (
|
|
3237
3382
|
settings: CallSettingsResponse,
|
|
3238
|
-
|
|
3239
|
-
|
|
3383
|
+
opts: {
|
|
3384
|
+
publish: boolean;
|
|
3385
|
+
skipSpeakerApply?: boolean;
|
|
3386
|
+
withDisabledDevices?: boolean;
|
|
3387
|
+
},
|
|
3240
3388
|
) => {
|
|
3389
|
+
const {
|
|
3390
|
+
publish,
|
|
3391
|
+
// Skip speaker setup on RN until joining (explicitly passed as false during join())
|
|
3392
|
+
skipSpeakerApply = isReactNative(),
|
|
3393
|
+
withDisabledDevices,
|
|
3394
|
+
} = opts;
|
|
3241
3395
|
if (!skipSpeakerApply) {
|
|
3242
|
-
this.speaker
|
|
3396
|
+
await this.speaker
|
|
3397
|
+
.apply(settings)
|
|
3398
|
+
.catch((err) => this.logger.warn('Speaker init failed', err));
|
|
3243
3399
|
}
|
|
3244
|
-
await this.camera
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
await this.microphone
|
|
3248
|
-
|
|
3249
|
-
|
|
3400
|
+
await this.camera
|
|
3401
|
+
.apply(settings.video, publish, withDisabledDevices)
|
|
3402
|
+
.catch((err) => this.logger.warn('Camera init failed', err));
|
|
3403
|
+
await this.microphone
|
|
3404
|
+
.apply(settings.audio, publish, withDisabledDevices)
|
|
3405
|
+
.catch((err) => this.logger.warn('Mic init failed', err));
|
|
3250
3406
|
};
|
|
3251
3407
|
|
|
3252
3408
|
/**
|