@stream-io/video-client 1.24.0 → 1.25.1
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 +20 -0
- package/dist/index.browser.es.js +367 -128
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +366 -127
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +367 -128
- package/dist/index.es.js.map +1 -1
- package/dist/src/StreamSfuClient.d.ts +12 -4
- package/dist/src/StreamVideoClient.d.ts +3 -1
- package/dist/src/coordinator/connection/errors.d.ts +1 -0
- package/dist/src/devices/InputMediaDeviceManager.d.ts +2 -0
- package/dist/src/devices/MicrophoneManager.d.ts +1 -0
- package/dist/src/devices/ScreenShareManager.d.ts +1 -0
- package/dist/src/devices/SpeakerManager.d.ts +2 -0
- package/dist/src/gen/video/sfu/models/models.d.ts +4 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +23 -4
- package/dist/src/rtc/NegotiationError.d.ts +15 -0
- package/dist/src/rtc/Publisher.d.ts +2 -2
- package/dist/src/rtc/helpers/sdp.d.ts +7 -0
- package/dist/src/types.d.ts +11 -0
- package/package.json +1 -1
- package/src/Call.ts +72 -38
- package/src/StreamSfuClient.ts +17 -7
- package/src/StreamVideoClient.ts +17 -7
- package/src/coordinator/connection/connection.ts +2 -1
- package/src/coordinator/connection/errors.ts +31 -0
- package/src/devices/CameraManagerState.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +13 -0
- package/src/devices/MicrophoneManager.ts +3 -0
- package/src/devices/ScreenShareManager.ts +18 -5
- package/src/devices/SpeakerManager.ts +13 -0
- package/src/devices/devices.ts +23 -12
- package/src/events/__tests__/internal.test.ts +1 -0
- package/src/gen/google/protobuf/struct.ts +2 -2
- package/src/gen/google/protobuf/timestamp.ts +1 -1
- package/src/gen/video/sfu/event/events.ts +15 -15
- package/src/gen/video/sfu/models/models.ts +9 -5
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +1 -1
- package/src/gen/video/sfu/signal_rpc/signal.ts +6 -6
- package/src/rtc/BasePeerConnection.ts +132 -46
- package/src/rtc/NegotiationError.ts +21 -0
- package/src/rtc/Publisher.ts +12 -9
- package/src/rtc/Subscriber.ts +8 -2
- package/src/rtc/__tests__/Publisher.test.ts +160 -17
- package/src/rtc/__tests__/Subscriber.test.ts +31 -14
- package/src/rtc/helpers/__tests__/sdp.stereo.test.ts +120 -0
- package/src/rtc/helpers/sdp.ts +43 -1
- package/src/types.ts +12 -0
package/dist/index.cjs.js
CHANGED
|
@@ -422,7 +422,7 @@ class ErrorFromResponse extends Error {
|
|
|
422
422
|
}
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
// @generated by protobuf-ts 2.
|
|
425
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
426
426
|
// @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
|
|
427
427
|
// tslint:disable
|
|
428
428
|
//
|
|
@@ -655,7 +655,7 @@ class ListValue$Type extends runtime.MessageType {
|
|
|
655
655
|
no: 1,
|
|
656
656
|
name: 'values',
|
|
657
657
|
kind: 'message',
|
|
658
|
-
repeat:
|
|
658
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
659
659
|
T: () => Value,
|
|
660
660
|
},
|
|
661
661
|
]);
|
|
@@ -687,7 +687,7 @@ class ListValue$Type extends runtime.MessageType {
|
|
|
687
687
|
*/
|
|
688
688
|
const ListValue = new ListValue$Type();
|
|
689
689
|
|
|
690
|
-
// @generated by protobuf-ts 2.
|
|
690
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
691
691
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
|
692
692
|
// tslint:disable
|
|
693
693
|
//
|
|
@@ -823,7 +823,7 @@ class Timestamp$Type extends runtime.MessageType {
|
|
|
823
823
|
*/
|
|
824
824
|
const Timestamp = new Timestamp$Type();
|
|
825
825
|
|
|
826
|
-
// @generated by protobuf-ts 2.
|
|
826
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
827
827
|
// @generated from protobuf file "video/sfu/models/models.proto" (package "stream.video.sfu.models", syntax proto3)
|
|
828
828
|
// tslint:disable
|
|
829
829
|
/**
|
|
@@ -965,6 +965,10 @@ var ErrorCode;
|
|
|
965
965
|
* @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_MEDIA_TRANSPORT_FAILURE = 205;
|
|
966
966
|
*/
|
|
967
967
|
ErrorCode[ErrorCode["PARTICIPANT_MEDIA_TRANSPORT_FAILURE"] = 205] = "PARTICIPANT_MEDIA_TRANSPORT_FAILURE";
|
|
968
|
+
/**
|
|
969
|
+
* @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_SIGNAL_LOST = 206;
|
|
970
|
+
*/
|
|
971
|
+
ErrorCode[ErrorCode["PARTICIPANT_SIGNAL_LOST"] = 206] = "PARTICIPANT_SIGNAL_LOST";
|
|
968
972
|
/**
|
|
969
973
|
* @generated from protobuf enum value: ERROR_CODE_CALL_NOT_FOUND = 300;
|
|
970
974
|
*/
|
|
@@ -1245,7 +1249,7 @@ class CallState$Type extends runtime.MessageType {
|
|
|
1245
1249
|
no: 1,
|
|
1246
1250
|
name: 'participants',
|
|
1247
1251
|
kind: 'message',
|
|
1248
|
-
repeat:
|
|
1252
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1249
1253
|
T: () => Participant,
|
|
1250
1254
|
},
|
|
1251
1255
|
{ no: 2, name: 'started_at', kind: 'message', T: () => Timestamp },
|
|
@@ -1259,7 +1263,7 @@ class CallState$Type extends runtime.MessageType {
|
|
|
1259
1263
|
no: 4,
|
|
1260
1264
|
name: 'pins',
|
|
1261
1265
|
kind: 'message',
|
|
1262
|
-
repeat:
|
|
1266
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1263
1267
|
T: () => Pin,
|
|
1264
1268
|
},
|
|
1265
1269
|
]);
|
|
@@ -1437,7 +1441,7 @@ class SubscribeOption$Type extends runtime.MessageType {
|
|
|
1437
1441
|
no: 2,
|
|
1438
1442
|
name: 'codecs',
|
|
1439
1443
|
kind: 'message',
|
|
1440
|
-
repeat:
|
|
1444
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1441
1445
|
T: () => Codec,
|
|
1442
1446
|
},
|
|
1443
1447
|
]);
|
|
@@ -1570,7 +1574,7 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1570
1574
|
no: 5,
|
|
1571
1575
|
name: 'layers',
|
|
1572
1576
|
kind: 'message',
|
|
1573
|
-
repeat:
|
|
1577
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1574
1578
|
T: () => VideoLayer,
|
|
1575
1579
|
},
|
|
1576
1580
|
{ no: 6, name: 'mid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
@@ -1937,7 +1941,7 @@ var models = /*#__PURE__*/Object.freeze({
|
|
|
1937
1941
|
get WebsocketReconnectStrategy () { return WebsocketReconnectStrategy; }
|
|
1938
1942
|
});
|
|
1939
1943
|
|
|
1940
|
-
// @generated by protobuf-ts 2.
|
|
1944
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
1941
1945
|
// @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3)
|
|
1942
1946
|
// tslint:disable
|
|
1943
1947
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
@@ -2105,14 +2109,14 @@ class SendStatsRequest$Type extends runtime.MessageType {
|
|
|
2105
2109
|
no: 16,
|
|
2106
2110
|
name: 'encode_stats',
|
|
2107
2111
|
kind: 'message',
|
|
2108
|
-
repeat:
|
|
2112
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2109
2113
|
T: () => PerformanceStats,
|
|
2110
2114
|
},
|
|
2111
2115
|
{
|
|
2112
2116
|
no: 17,
|
|
2113
2117
|
name: 'decode_stats',
|
|
2114
2118
|
kind: 'message',
|
|
2115
|
-
repeat:
|
|
2119
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2116
2120
|
T: () => PerformanceStats,
|
|
2117
2121
|
},
|
|
2118
2122
|
{
|
|
@@ -2179,7 +2183,7 @@ class UpdateMuteStatesRequest$Type extends runtime.MessageType {
|
|
|
2179
2183
|
no: 3,
|
|
2180
2184
|
name: 'mute_states',
|
|
2181
2185
|
kind: 'message',
|
|
2182
|
-
repeat:
|
|
2186
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2183
2187
|
T: () => TrackMuteState,
|
|
2184
2188
|
},
|
|
2185
2189
|
]);
|
|
@@ -2256,7 +2260,7 @@ class UpdateSubscriptionsRequest$Type extends runtime.MessageType {
|
|
|
2256
2260
|
no: 3,
|
|
2257
2261
|
name: 'tracks',
|
|
2258
2262
|
kind: 'message',
|
|
2259
|
-
repeat:
|
|
2263
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2260
2264
|
T: () => TrackSubscriptionDetails,
|
|
2261
2265
|
},
|
|
2262
2266
|
]);
|
|
@@ -2355,7 +2359,7 @@ class SetPublisherRequest$Type extends runtime.MessageType {
|
|
|
2355
2359
|
no: 3,
|
|
2356
2360
|
name: 'tracks',
|
|
2357
2361
|
kind: 'message',
|
|
2358
|
-
repeat:
|
|
2362
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2359
2363
|
T: () => TrackInfo,
|
|
2360
2364
|
},
|
|
2361
2365
|
]);
|
|
@@ -2435,7 +2439,7 @@ const SignalServer = new runtimeRpc.ServiceType('stream.video.sfu.signal.SignalS
|
|
|
2435
2439
|
},
|
|
2436
2440
|
]);
|
|
2437
2441
|
|
|
2438
|
-
// @generated by protobuf-ts 2.
|
|
2442
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
2439
2443
|
// @generated from protobuf file "video/sfu/event/events.proto" (package "stream.video.sfu.event", syntax proto3)
|
|
2440
2444
|
// tslint:disable
|
|
2441
2445
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
@@ -2611,7 +2615,7 @@ class ChangePublishOptions$Type extends runtime.MessageType {
|
|
|
2611
2615
|
no: 1,
|
|
2612
2616
|
name: 'publish_options',
|
|
2613
2617
|
kind: 'message',
|
|
2614
|
-
repeat:
|
|
2618
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2615
2619
|
T: () => PublishOption,
|
|
2616
2620
|
},
|
|
2617
2621
|
{ no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
@@ -2650,7 +2654,7 @@ class PinsChanged$Type extends runtime.MessageType {
|
|
|
2650
2654
|
no: 1,
|
|
2651
2655
|
name: 'pins',
|
|
2652
2656
|
kind: 'message',
|
|
2653
|
-
repeat:
|
|
2657
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2654
2658
|
T: () => Pin,
|
|
2655
2659
|
},
|
|
2656
2660
|
]);
|
|
@@ -2893,14 +2897,14 @@ class JoinRequest$Type extends runtime.MessageType {
|
|
|
2893
2897
|
no: 9,
|
|
2894
2898
|
name: 'preferred_publish_options',
|
|
2895
2899
|
kind: 'message',
|
|
2896
|
-
repeat:
|
|
2900
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2897
2901
|
T: () => PublishOption,
|
|
2898
2902
|
},
|
|
2899
2903
|
{
|
|
2900
2904
|
no: 10,
|
|
2901
2905
|
name: 'preferred_subscribe_options',
|
|
2902
2906
|
kind: 'message',
|
|
2903
|
-
repeat:
|
|
2907
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2904
2908
|
T: () => SubscribeOption,
|
|
2905
2909
|
},
|
|
2906
2910
|
]);
|
|
@@ -2928,14 +2932,14 @@ class ReconnectDetails$Type extends runtime.MessageType {
|
|
|
2928
2932
|
no: 3,
|
|
2929
2933
|
name: 'announced_tracks',
|
|
2930
2934
|
kind: 'message',
|
|
2931
|
-
repeat:
|
|
2935
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2932
2936
|
T: () => TrackInfo,
|
|
2933
2937
|
},
|
|
2934
2938
|
{
|
|
2935
2939
|
no: 4,
|
|
2936
2940
|
name: 'subscriptions',
|
|
2937
2941
|
kind: 'message',
|
|
2938
|
-
repeat:
|
|
2942
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2939
2943
|
T: () => TrackSubscriptionDetails,
|
|
2940
2944
|
},
|
|
2941
2945
|
{
|
|
@@ -2978,14 +2982,14 @@ class Migration$Type extends runtime.MessageType {
|
|
|
2978
2982
|
no: 2,
|
|
2979
2983
|
name: 'announced_tracks',
|
|
2980
2984
|
kind: 'message',
|
|
2981
|
-
repeat:
|
|
2985
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2982
2986
|
T: () => TrackInfo,
|
|
2983
2987
|
},
|
|
2984
2988
|
{
|
|
2985
2989
|
no: 3,
|
|
2986
2990
|
name: 'subscriptions',
|
|
2987
2991
|
kind: 'message',
|
|
2988
|
-
repeat:
|
|
2992
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2989
2993
|
T: () => TrackSubscriptionDetails,
|
|
2990
2994
|
},
|
|
2991
2995
|
]);
|
|
@@ -3011,7 +3015,7 @@ class JoinResponse$Type extends runtime.MessageType {
|
|
|
3011
3015
|
no: 4,
|
|
3012
3016
|
name: 'publish_options',
|
|
3013
3017
|
kind: 'message',
|
|
3014
|
-
repeat:
|
|
3018
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3015
3019
|
T: () => PublishOption,
|
|
3016
3020
|
},
|
|
3017
3021
|
]);
|
|
@@ -3093,7 +3097,7 @@ class ConnectionQualityChanged$Type extends runtime.MessageType {
|
|
|
3093
3097
|
no: 1,
|
|
3094
3098
|
name: 'connection_quality_updates',
|
|
3095
3099
|
kind: 'message',
|
|
3096
|
-
repeat:
|
|
3100
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3097
3101
|
T: () => ConnectionQualityInfo,
|
|
3098
3102
|
},
|
|
3099
3103
|
]);
|
|
@@ -3162,7 +3166,7 @@ class AudioLevelChanged$Type extends runtime.MessageType {
|
|
|
3162
3166
|
no: 1,
|
|
3163
3167
|
name: 'audio_levels',
|
|
3164
3168
|
kind: 'message',
|
|
3165
|
-
repeat:
|
|
3169
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3166
3170
|
T: () => AudioLevel,
|
|
3167
3171
|
},
|
|
3168
3172
|
]);
|
|
@@ -3242,7 +3246,7 @@ class VideoSender$Type extends runtime.MessageType {
|
|
|
3242
3246
|
no: 3,
|
|
3243
3247
|
name: 'layers',
|
|
3244
3248
|
kind: 'message',
|
|
3245
|
-
repeat:
|
|
3249
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3246
3250
|
T: () => VideoLayerSetting,
|
|
3247
3251
|
},
|
|
3248
3252
|
{
|
|
@@ -3276,14 +3280,14 @@ class ChangePublishQuality$Type extends runtime.MessageType {
|
|
|
3276
3280
|
no: 1,
|
|
3277
3281
|
name: 'audio_senders',
|
|
3278
3282
|
kind: 'message',
|
|
3279
|
-
repeat:
|
|
3283
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3280
3284
|
T: () => AudioSender,
|
|
3281
3285
|
},
|
|
3282
3286
|
{
|
|
3283
3287
|
no: 2,
|
|
3284
3288
|
name: 'video_senders',
|
|
3285
3289
|
kind: 'message',
|
|
3286
|
-
repeat:
|
|
3290
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3287
3291
|
T: () => VideoSender,
|
|
3288
3292
|
},
|
|
3289
3293
|
]);
|
|
@@ -5475,6 +5479,21 @@ class CallState {
|
|
|
5475
5479
|
}
|
|
5476
5480
|
}
|
|
5477
5481
|
|
|
5482
|
+
/**
|
|
5483
|
+
* NegotiationError is thrown when there is an error during the negotiation process.
|
|
5484
|
+
* It extends the built-in Error class and includes an SfuError object for more details.
|
|
5485
|
+
*/
|
|
5486
|
+
class NegotiationError extends Error {
|
|
5487
|
+
/**
|
|
5488
|
+
* Creates an instance of NegotiationError.
|
|
5489
|
+
*/
|
|
5490
|
+
constructor(error) {
|
|
5491
|
+
super(error.message);
|
|
5492
|
+
this.name = 'NegotiationError';
|
|
5493
|
+
this.error = error;
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
|
|
5478
5497
|
/**
|
|
5479
5498
|
* Flatten the stats report into an array of stats objects.
|
|
5480
5499
|
*
|
|
@@ -5793,7 +5812,7 @@ const aggregate = (stats) => {
|
|
|
5793
5812
|
return report;
|
|
5794
5813
|
};
|
|
5795
5814
|
|
|
5796
|
-
const version = "1.
|
|
5815
|
+
const version = "1.25.1";
|
|
5797
5816
|
const [major, minor, patch] = version.split('.');
|
|
5798
5817
|
let sdkInfo = {
|
|
5799
5818
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -6402,12 +6421,38 @@ class BasePeerConnection {
|
|
|
6402
6421
|
/**
|
|
6403
6422
|
* Constructs a new `BasePeerConnection` instance.
|
|
6404
6423
|
*/
|
|
6405
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher,
|
|
6424
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, logTag, enableTracing, iceRestartDelay = 2500, }) {
|
|
6406
6425
|
this.isIceRestarting = false;
|
|
6407
6426
|
this.isDisposed = false;
|
|
6408
6427
|
this.trackIdToTrackType = new Map();
|
|
6409
6428
|
this.subscriptions = [];
|
|
6410
6429
|
this.lock = Math.random().toString(36).slice(2);
|
|
6430
|
+
this.createPeerConnection = (connectionConfig) => {
|
|
6431
|
+
const pc = new RTCPeerConnection(connectionConfig);
|
|
6432
|
+
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
6433
|
+
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6434
|
+
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6435
|
+
pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
6436
|
+
pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
6437
|
+
pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
6438
|
+
return pc;
|
|
6439
|
+
};
|
|
6440
|
+
/**
|
|
6441
|
+
* Attempts to restart ICE on the `RTCPeerConnection`.
|
|
6442
|
+
* This method intentionally doesn't await the `restartIce()` method,
|
|
6443
|
+
* allowing it to run in the background and handle any errors that may occur.
|
|
6444
|
+
*/
|
|
6445
|
+
this.tryRestartIce = () => {
|
|
6446
|
+
this.restartIce().catch((e) => {
|
|
6447
|
+
const reason = 'restartICE() failed, initiating reconnect';
|
|
6448
|
+
this.logger('error', reason, e);
|
|
6449
|
+
const strategy = e instanceof NegotiationError &&
|
|
6450
|
+
e.error.code === ErrorCode.PARTICIPANT_SIGNAL_LOST
|
|
6451
|
+
? WebsocketReconnectStrategy.FAST
|
|
6452
|
+
: WebsocketReconnectStrategy.REJOIN;
|
|
6453
|
+
this.onReconnectionNeeded?.(strategy, reason);
|
|
6454
|
+
});
|
|
6455
|
+
};
|
|
6411
6456
|
/**
|
|
6412
6457
|
* Handles events synchronously.
|
|
6413
6458
|
* Consecutive events are queued and executed one after the other.
|
|
@@ -6460,6 +6505,18 @@ class BasePeerConnection {
|
|
|
6460
6505
|
this.getTrackType = (trackId) => {
|
|
6461
6506
|
return this.trackIdToTrackType.get(trackId);
|
|
6462
6507
|
};
|
|
6508
|
+
/**
|
|
6509
|
+
* Checks if the `RTCPeerConnection` is healthy.
|
|
6510
|
+
* It checks the ICE connection state and the peer connection state.
|
|
6511
|
+
* If either state is `failed`, `disconnected`, or `closed`,
|
|
6512
|
+
* it returns `false`, otherwise it returns `true`.
|
|
6513
|
+
*/
|
|
6514
|
+
this.isHealthy = () => {
|
|
6515
|
+
const failedStates = new Set(['failed', 'closed']);
|
|
6516
|
+
const iceState = this.pc.iceConnectionState;
|
|
6517
|
+
const connectionState = this.pc.connectionState;
|
|
6518
|
+
return !failedStates.has(iceState) && !failedStates.has(connectionState);
|
|
6519
|
+
};
|
|
6463
6520
|
/**
|
|
6464
6521
|
* Handles the ICECandidate event and
|
|
6465
6522
|
* Initiates an ICE Trickle process with the SFU.
|
|
@@ -6498,9 +6555,7 @@ class BasePeerConnection {
|
|
|
6498
6555
|
this.onConnectionStateChange = async () => {
|
|
6499
6556
|
const state = this.pc.connectionState;
|
|
6500
6557
|
this.logger('debug', `Connection state changed`, state);
|
|
6501
|
-
if (
|
|
6502
|
-
return;
|
|
6503
|
-
if (state === 'connected' || state === 'failed') {
|
|
6558
|
+
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
6504
6559
|
try {
|
|
6505
6560
|
const stats = await this.stats.get();
|
|
6506
6561
|
this.tracer.trace('getstats', stats.delta);
|
|
@@ -6509,6 +6564,12 @@ class BasePeerConnection {
|
|
|
6509
6564
|
this.tracer.trace('getstatsOnFailure', err.toString());
|
|
6510
6565
|
}
|
|
6511
6566
|
}
|
|
6567
|
+
// we can't recover from a failed connection state (contrary to ICE)
|
|
6568
|
+
if (state === 'failed') {
|
|
6569
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, 'Connection failed');
|
|
6570
|
+
return;
|
|
6571
|
+
}
|
|
6572
|
+
this.handleConnectionStateUpdate(state);
|
|
6512
6573
|
};
|
|
6513
6574
|
/**
|
|
6514
6575
|
* Handles the ICE connection state change event.
|
|
@@ -6516,34 +6577,53 @@ class BasePeerConnection {
|
|
|
6516
6577
|
this.onIceConnectionStateChange = () => {
|
|
6517
6578
|
const state = this.pc.iceConnectionState;
|
|
6518
6579
|
this.logger('debug', `ICE connection state changed`, state);
|
|
6519
|
-
|
|
6580
|
+
this.handleConnectionStateUpdate(state);
|
|
6581
|
+
};
|
|
6582
|
+
this.handleConnectionStateUpdate = (state) => {
|
|
6583
|
+
const { callingState } = this.state;
|
|
6584
|
+
if (callingState === exports.CallingState.OFFLINE)
|
|
6520
6585
|
return;
|
|
6521
|
-
if (
|
|
6586
|
+
if (callingState === exports.CallingState.RECONNECTING)
|
|
6522
6587
|
return;
|
|
6523
6588
|
// do nothing when ICE is restarting
|
|
6524
6589
|
if (this.isIceRestarting)
|
|
6525
6590
|
return;
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6591
|
+
switch (state) {
|
|
6592
|
+
case 'failed':
|
|
6593
|
+
// in the `failed` state, we try to restart ICE immediately
|
|
6594
|
+
this.logger('info', 'restartICE due to failed connection');
|
|
6595
|
+
this.tryRestartIce();
|
|
6596
|
+
break;
|
|
6597
|
+
case 'disconnected':
|
|
6598
|
+
// in the `disconnected` state, we schedule a restartICE() after a delay
|
|
6599
|
+
// as the browser might recover the connection in the meantime
|
|
6600
|
+
this.logger('info', 'disconnected connection, scheduling restartICE');
|
|
6601
|
+
clearTimeout(this.iceRestartTimeout);
|
|
6602
|
+
this.iceRestartTimeout = setTimeout(() => {
|
|
6603
|
+
const currentState = this.pc.iceConnectionState;
|
|
6604
|
+
if (currentState === 'disconnected' || currentState === 'failed') {
|
|
6605
|
+
this.tryRestartIce();
|
|
6606
|
+
}
|
|
6607
|
+
}, this.iceRestartDelay);
|
|
6608
|
+
break;
|
|
6609
|
+
case 'connected':
|
|
6610
|
+
// in the `connected` state, we clear the ice restart timeout if it exists
|
|
6611
|
+
if (this.iceRestartTimeout) {
|
|
6612
|
+
this.logger('info', 'connected connection, canceling restartICE');
|
|
6613
|
+
clearTimeout(this.iceRestartTimeout);
|
|
6614
|
+
this.iceRestartTimeout = undefined;
|
|
6615
|
+
}
|
|
6616
|
+
break;
|
|
6536
6617
|
}
|
|
6537
6618
|
};
|
|
6538
6619
|
/**
|
|
6539
6620
|
* Handles the ICE candidate error event.
|
|
6540
6621
|
*/
|
|
6541
6622
|
this.onIceCandidateError = (e) => {
|
|
6542
|
-
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent
|
|
6543
|
-
`${e.errorCode}: ${e.errorText}
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
6623
|
+
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent
|
|
6624
|
+
? `${e.errorCode}: ${e.errorText}`
|
|
6625
|
+
: e;
|
|
6626
|
+
this.logger('debug', 'ICE Candidate error', errorMessage);
|
|
6547
6627
|
};
|
|
6548
6628
|
/**
|
|
6549
6629
|
* Handles the ICE gathering state change event.
|
|
@@ -6561,18 +6641,13 @@ class BasePeerConnection {
|
|
|
6561
6641
|
this.sfuClient = sfuClient;
|
|
6562
6642
|
this.state = state;
|
|
6563
6643
|
this.dispatcher = dispatcher;
|
|
6564
|
-
this.
|
|
6644
|
+
this.iceRestartDelay = iceRestartDelay;
|
|
6645
|
+
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
6565
6646
|
this.logger = getLogger([
|
|
6566
6647
|
peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
|
|
6567
6648
|
logTag,
|
|
6568
6649
|
]);
|
|
6569
|
-
this.pc =
|
|
6570
|
-
this.pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
6571
|
-
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6572
|
-
this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6573
|
-
this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
6574
|
-
this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
6575
|
-
this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
6650
|
+
this.pc = this.createPeerConnection(connectionConfig);
|
|
6576
6651
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
|
|
6577
6652
|
if (enableTracing) {
|
|
6578
6653
|
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
|
|
@@ -6588,7 +6663,9 @@ class BasePeerConnection {
|
|
|
6588
6663
|
* Disposes the `RTCPeerConnection` instance.
|
|
6589
6664
|
*/
|
|
6590
6665
|
dispose() {
|
|
6591
|
-
this.
|
|
6666
|
+
clearTimeout(this.iceRestartTimeout);
|
|
6667
|
+
this.iceRestartTimeout = undefined;
|
|
6668
|
+
this.onReconnectionNeeded = undefined;
|
|
6592
6669
|
this.isDisposed = true;
|
|
6593
6670
|
this.detachEventHandlers();
|
|
6594
6671
|
this.pc.close();
|
|
@@ -6598,11 +6675,12 @@ class BasePeerConnection {
|
|
|
6598
6675
|
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
6599
6676
|
*/
|
|
6600
6677
|
detachEventHandlers() {
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6678
|
+
const pc = this.pc;
|
|
6679
|
+
pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
6680
|
+
pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6681
|
+
pc.removeEventListener('signalingstatechange', this.onSignalingChange);
|
|
6682
|
+
pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6683
|
+
pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
6606
6684
|
this.unsubscribeIceTrickle?.();
|
|
6607
6685
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
6608
6686
|
}
|
|
@@ -6912,6 +6990,45 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
|
6912
6990
|
return '';
|
|
6913
6991
|
return String(transceiverInitIndex);
|
|
6914
6992
|
};
|
|
6993
|
+
/**
|
|
6994
|
+
* Enables stereo in the answer SDP based on the offered stereo in the offer SDP.
|
|
6995
|
+
*
|
|
6996
|
+
* @param offerSdp the offer SDP containing the stereo configuration.
|
|
6997
|
+
* @param answerSdp the answer SDP to be modified.
|
|
6998
|
+
*/
|
|
6999
|
+
const enableStereo = (offerSdp, answerSdp) => {
|
|
7000
|
+
const offeredStereoMids = new Set();
|
|
7001
|
+
const parsedOfferSdp = sdpTransform.parse(offerSdp);
|
|
7002
|
+
for (const media of parsedOfferSdp.media) {
|
|
7003
|
+
if (media.type !== 'audio')
|
|
7004
|
+
continue;
|
|
7005
|
+
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
7006
|
+
if (!opus)
|
|
7007
|
+
continue;
|
|
7008
|
+
for (const fmtp of media.fmtp) {
|
|
7009
|
+
if (fmtp.payload === opus.payload && fmtp.config.includes('stereo=1')) {
|
|
7010
|
+
offeredStereoMids.add(media.mid);
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
// No stereo offered, return the original answerSdp
|
|
7015
|
+
if (offeredStereoMids.size === 0)
|
|
7016
|
+
return answerSdp;
|
|
7017
|
+
const parsedAnswerSdp = sdpTransform.parse(answerSdp);
|
|
7018
|
+
for (const media of parsedAnswerSdp.media) {
|
|
7019
|
+
if (media.type !== 'audio' || !offeredStereoMids.has(media.mid))
|
|
7020
|
+
continue;
|
|
7021
|
+
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
7022
|
+
if (!opus)
|
|
7023
|
+
continue;
|
|
7024
|
+
for (const fmtp of media.fmtp) {
|
|
7025
|
+
if (fmtp.payload === opus.payload && !fmtp.config.includes('stereo=1')) {
|
|
7026
|
+
fmtp.config += ';stereo=1';
|
|
7027
|
+
}
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
return sdpTransform.write(parsedAnswerSdp);
|
|
7031
|
+
};
|
|
6915
7032
|
|
|
6916
7033
|
/**
|
|
6917
7034
|
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
@@ -7025,11 +7142,11 @@ class Publisher extends BasePeerConnection {
|
|
|
7025
7142
|
/**
|
|
7026
7143
|
* Returns true if the given track type is currently being published to the SFU.
|
|
7027
7144
|
*
|
|
7028
|
-
* @param trackType the track type to check.
|
|
7145
|
+
* @param trackType the track type to check. If omitted, checks if any track is being published.
|
|
7029
7146
|
*/
|
|
7030
7147
|
this.isPublishing = (trackType) => {
|
|
7031
7148
|
for (const item of this.transceiverCache.items()) {
|
|
7032
|
-
if (item.publishOption.trackType !== trackType)
|
|
7149
|
+
if (trackType && item.publishOption.trackType !== trackType)
|
|
7033
7150
|
continue;
|
|
7034
7151
|
const track = item.transceiver.sender.track;
|
|
7035
7152
|
if (!track)
|
|
@@ -7152,10 +7269,17 @@ class Publisher extends BasePeerConnection {
|
|
|
7152
7269
|
const { sdp = '' } = offer;
|
|
7153
7270
|
const { response } = await this.sfuClient.setPublisher({ sdp, tracks });
|
|
7154
7271
|
if (response.error)
|
|
7155
|
-
throw new
|
|
7272
|
+
throw new NegotiationError(response.error);
|
|
7156
7273
|
const { sdp: answerSdp } = response;
|
|
7157
7274
|
await this.pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
|
|
7158
7275
|
}
|
|
7276
|
+
catch (err) {
|
|
7277
|
+
// negotiation failed, rollback to the previous state
|
|
7278
|
+
if (this.pc.signalingState === 'have-local-offer') {
|
|
7279
|
+
await this.pc.setLocalDescription({ type: 'rollback' });
|
|
7280
|
+
}
|
|
7281
|
+
throw err;
|
|
7282
|
+
}
|
|
7159
7283
|
finally {
|
|
7160
7284
|
this.isIceRestarting = false;
|
|
7161
7285
|
}
|
|
@@ -7247,11 +7371,7 @@ class Publisher extends BasePeerConnection {
|
|
|
7247
7371
|
this.on('iceRestart', (iceRestart) => {
|
|
7248
7372
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
7249
7373
|
return;
|
|
7250
|
-
this.
|
|
7251
|
-
const reason = `ICE restart failed`;
|
|
7252
|
-
this.logger('warn', reason, err);
|
|
7253
|
-
this.onUnrecoverableError?.(`${reason}: ${err}`);
|
|
7254
|
-
});
|
|
7374
|
+
this.tryRestartIce();
|
|
7255
7375
|
});
|
|
7256
7376
|
this.on('changePublishQuality', async (event) => {
|
|
7257
7377
|
for (const videoSender of event.videoSenders) {
|
|
@@ -7299,11 +7419,13 @@ class Subscriber extends BasePeerConnection {
|
|
|
7299
7419
|
return;
|
|
7300
7420
|
}
|
|
7301
7421
|
const previousIsIceRestarting = this.isIceRestarting;
|
|
7422
|
+
this.isIceRestarting = true;
|
|
7302
7423
|
try {
|
|
7303
|
-
|
|
7304
|
-
await this.sfuClient.iceRestart({
|
|
7424
|
+
const { response } = await this.sfuClient.iceRestart({
|
|
7305
7425
|
peerType: PeerType.SUBSCRIBER,
|
|
7306
7426
|
});
|
|
7427
|
+
if (response.error)
|
|
7428
|
+
throw new NegotiationError(response.error);
|
|
7307
7429
|
}
|
|
7308
7430
|
catch (e) {
|
|
7309
7431
|
// restore the previous state, as our intent for restarting ICE failed
|
|
@@ -7372,6 +7494,9 @@ class Subscriber extends BasePeerConnection {
|
|
|
7372
7494
|
});
|
|
7373
7495
|
this.addTrickledIceCandidates();
|
|
7374
7496
|
const answer = await this.pc.createAnswer();
|
|
7497
|
+
if (answer.sdp) {
|
|
7498
|
+
answer.sdp = enableStereo(subscriberOffer.sdp, answer.sdp);
|
|
7499
|
+
}
|
|
7375
7500
|
await this.pc.setLocalDescription(answer);
|
|
7376
7501
|
await this.sfuClient.sendAnswer({
|
|
7377
7502
|
peerType: PeerType.SUBSCRIBER,
|
|
@@ -7661,11 +7786,15 @@ class StreamSfuClient {
|
|
|
7661
7786
|
*/
|
|
7662
7787
|
this.isLeaving = false;
|
|
7663
7788
|
/**
|
|
7664
|
-
* Flag to indicate if the client is in the process of closing the connection.
|
|
7789
|
+
* Flag to indicate if the client is in the process of clean closing the connection.
|
|
7790
|
+
* When set to `true`, the client will not attempt to reconnect
|
|
7791
|
+
* and will close the WebSocket connection gracefully.
|
|
7792
|
+
* Otherwise, it will close the connection with an error code and
|
|
7793
|
+
* trigger a reconnection attempt.
|
|
7665
7794
|
*/
|
|
7666
|
-
this.
|
|
7667
|
-
this.pingIntervalInMs =
|
|
7668
|
-
this.unhealthyTimeoutInMs =
|
|
7795
|
+
this.isClosingClean = false;
|
|
7796
|
+
this.pingIntervalInMs = 5 * 1000;
|
|
7797
|
+
this.unhealthyTimeoutInMs = 15 * 1000;
|
|
7669
7798
|
/**
|
|
7670
7799
|
* Promise that resolves when the JoinResponse is received.
|
|
7671
7800
|
* Rejects after a certain threshold if the response is not received.
|
|
@@ -7709,7 +7838,7 @@ class StreamSfuClient {
|
|
|
7709
7838
|
// Normally, this shouldn't have any effect, because WS should never emit 'close'
|
|
7710
7839
|
// before emitting 'open'. However, strager things have happened, and we don't
|
|
7711
7840
|
// want to leave signalReady in pending state.
|
|
7712
|
-
reject(new Error(
|
|
7841
|
+
reject(new Error(`SFU WS closed or connection can't be established`));
|
|
7713
7842
|
});
|
|
7714
7843
|
}),
|
|
7715
7844
|
new Promise((resolve, reject) => {
|
|
@@ -7727,7 +7856,7 @@ class StreamSfuClient {
|
|
|
7727
7856
|
this.onSignalClose?.(`${e.code} ${e.reason}`);
|
|
7728
7857
|
};
|
|
7729
7858
|
this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
|
|
7730
|
-
this.
|
|
7859
|
+
this.isClosingClean = code !== StreamSfuClient.ERROR_CONNECTION_UNHEALTHY;
|
|
7731
7860
|
if (this.signalWs.readyState === WebSocket.OPEN) {
|
|
7732
7861
|
this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
|
|
7733
7862
|
this.signalWs.close(code, `js-client: ${reason}`);
|
|
@@ -7967,7 +8096,11 @@ StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
|
|
|
7967
8096
|
* Here, we don't use 1000 (normal closure) because we don't want the
|
|
7968
8097
|
* SFU to clean up the resources associated with the current participant.
|
|
7969
8098
|
*/
|
|
7970
|
-
StreamSfuClient.DISPOSE_OLD_SOCKET =
|
|
8099
|
+
StreamSfuClient.DISPOSE_OLD_SOCKET = 4100;
|
|
8100
|
+
/**
|
|
8101
|
+
* The close code used when the client fails to join the call (on the SFU).
|
|
8102
|
+
*/
|
|
8103
|
+
StreamSfuClient.JOIN_FAILED = 4101;
|
|
7971
8104
|
|
|
7972
8105
|
/**
|
|
7973
8106
|
* Event handler that watched the delivery of `call.accepted`.
|
|
@@ -9481,21 +9614,30 @@ let getDisplayMediaExecId = 0;
|
|
|
9481
9614
|
const getScreenShareStream = async (options, tracer) => {
|
|
9482
9615
|
const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
|
|
9483
9616
|
try {
|
|
9484
|
-
|
|
9485
|
-
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
9486
|
-
video: true,
|
|
9487
|
-
audio: {
|
|
9488
|
-
channelCount: {
|
|
9489
|
-
ideal: 2,
|
|
9490
|
-
},
|
|
9491
|
-
echoCancellation: false,
|
|
9492
|
-
autoGainControl: false,
|
|
9493
|
-
noiseSuppression: false,
|
|
9494
|
-
},
|
|
9617
|
+
const constraints = {
|
|
9495
9618
|
// @ts-expect-error - not present in types yet
|
|
9496
9619
|
systemAudio: 'include',
|
|
9497
9620
|
...options,
|
|
9498
|
-
|
|
9621
|
+
video: typeof options?.video === 'boolean'
|
|
9622
|
+
? options.video // must be 'true'
|
|
9623
|
+
: {
|
|
9624
|
+
width: { max: 2560 },
|
|
9625
|
+
height: { max: 1440 },
|
|
9626
|
+
frameRate: { ideal: 30 },
|
|
9627
|
+
...options?.video,
|
|
9628
|
+
},
|
|
9629
|
+
audio: typeof options?.audio === 'boolean'
|
|
9630
|
+
? options.audio
|
|
9631
|
+
: {
|
|
9632
|
+
channelCount: { ideal: 2 },
|
|
9633
|
+
echoCancellation: false,
|
|
9634
|
+
autoGainControl: false,
|
|
9635
|
+
noiseSuppression: false,
|
|
9636
|
+
...options?.audio,
|
|
9637
|
+
},
|
|
9638
|
+
};
|
|
9639
|
+
tracer?.trace(tag, constraints);
|
|
9640
|
+
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
|
|
9499
9641
|
tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
9500
9642
|
return stream;
|
|
9501
9643
|
}
|
|
@@ -9543,6 +9685,7 @@ class InputMediaDeviceManager {
|
|
|
9543
9685
|
*/
|
|
9544
9686
|
this.stopOnLeave = true;
|
|
9545
9687
|
this.subscriptions = [];
|
|
9688
|
+
this.areSubscriptionsSetUp = false;
|
|
9546
9689
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
9547
9690
|
this.filters = [];
|
|
9548
9691
|
this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
|
|
@@ -9554,11 +9697,20 @@ class InputMediaDeviceManager {
|
|
|
9554
9697
|
*/
|
|
9555
9698
|
this.dispose = () => {
|
|
9556
9699
|
this.subscriptions.forEach((s) => s());
|
|
9700
|
+
this.subscriptions = [];
|
|
9701
|
+
this.areSubscriptionsSetUp = false;
|
|
9557
9702
|
};
|
|
9558
9703
|
this.call = call;
|
|
9559
9704
|
this.state = state;
|
|
9560
9705
|
this.trackType = trackType;
|
|
9561
9706
|
this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
|
|
9707
|
+
this.setup();
|
|
9708
|
+
}
|
|
9709
|
+
setup() {
|
|
9710
|
+
if (this.areSubscriptionsSetUp) {
|
|
9711
|
+
return;
|
|
9712
|
+
}
|
|
9713
|
+
this.areSubscriptionsSetUp = true;
|
|
9562
9714
|
if (deviceIds$ &&
|
|
9563
9715
|
!isReactNative() &&
|
|
9564
9716
|
(this.trackType === TrackType.AUDIO || this.trackType === TrackType.VIDEO)) {
|
|
@@ -10519,6 +10671,9 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
10519
10671
|
super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO);
|
|
10520
10672
|
this.speakingWhileMutedNotificationEnabled = true;
|
|
10521
10673
|
this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
|
|
10674
|
+
}
|
|
10675
|
+
setup() {
|
|
10676
|
+
super.setup();
|
|
10522
10677
|
this.subscriptions.push(createSafeAsyncSubscription(rxjs.combineLatest([
|
|
10523
10678
|
this.call.state.callingState$,
|
|
10524
10679
|
this.call.state.ownCapabilities$,
|
|
@@ -10796,7 +10951,10 @@ class ScreenShareState extends InputMediaDeviceManagerState {
|
|
|
10796
10951
|
class ScreenShareManager extends InputMediaDeviceManager {
|
|
10797
10952
|
constructor(call) {
|
|
10798
10953
|
super(call, new ScreenShareState(), TrackType.SCREEN_SHARE);
|
|
10799
|
-
|
|
10954
|
+
}
|
|
10955
|
+
setup() {
|
|
10956
|
+
super.setup();
|
|
10957
|
+
this.subscriptions.push(createSubscription(this.call.state.settings$, (settings) => {
|
|
10800
10958
|
const maybeTargetResolution = settings?.screensharing.target_resolution;
|
|
10801
10959
|
if (maybeTargetResolution) {
|
|
10802
10960
|
this.setDefaultConstraints({
|
|
@@ -10843,11 +11001,18 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10843
11001
|
getDevices() {
|
|
10844
11002
|
return rxjs.of([]); // there are no devices to be listed for Screen Share
|
|
10845
11003
|
}
|
|
10846
|
-
getStream(constraints) {
|
|
11004
|
+
async getStream(constraints) {
|
|
10847
11005
|
if (!this.state.audioEnabled) {
|
|
10848
11006
|
constraints.audio = false;
|
|
10849
11007
|
}
|
|
10850
|
-
|
|
11008
|
+
const stream = await getScreenShareStream(constraints, this.call.tracer);
|
|
11009
|
+
const [track] = stream.getVideoTracks();
|
|
11010
|
+
const { contentHint } = this.state.settings || {};
|
|
11011
|
+
if (typeof contentHint !== 'undefined' && track && 'contentHint' in track) {
|
|
11012
|
+
this.call.tracer.trace('navigator.mediaDevices.getDisplayMedia.contentHint', contentHint);
|
|
11013
|
+
track.contentHint = contentHint;
|
|
11014
|
+
}
|
|
11015
|
+
return stream;
|
|
10851
11016
|
}
|
|
10852
11017
|
async stopPublishStream() {
|
|
10853
11018
|
return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
|
|
@@ -10912,6 +11077,7 @@ class SpeakerState {
|
|
|
10912
11077
|
class SpeakerManager {
|
|
10913
11078
|
constructor(call) {
|
|
10914
11079
|
this.subscriptions = [];
|
|
11080
|
+
this.areSubscriptionsSetUp = false;
|
|
10915
11081
|
/**
|
|
10916
11082
|
* Disposes the manager.
|
|
10917
11083
|
*
|
|
@@ -10919,9 +11085,18 @@ class SpeakerManager {
|
|
|
10919
11085
|
*/
|
|
10920
11086
|
this.dispose = () => {
|
|
10921
11087
|
this.subscriptions.forEach((s) => s.unsubscribe());
|
|
11088
|
+
this.subscriptions = [];
|
|
11089
|
+
this.areSubscriptionsSetUp = false;
|
|
10922
11090
|
};
|
|
10923
11091
|
this.call = call;
|
|
10924
11092
|
this.state = new SpeakerState(call.tracer);
|
|
11093
|
+
this.setup();
|
|
11094
|
+
}
|
|
11095
|
+
setup() {
|
|
11096
|
+
if (this.areSubscriptionsSetUp) {
|
|
11097
|
+
return;
|
|
11098
|
+
}
|
|
11099
|
+
this.areSubscriptionsSetUp = true;
|
|
10925
11100
|
if (deviceIds$ && !isReactNative()) {
|
|
10926
11101
|
this.subscriptions.push(rxjs.combineLatest([deviceIds$, this.state.selectedDevice$]).subscribe(([devices, deviceId]) => {
|
|
10927
11102
|
if (!deviceId) {
|
|
@@ -11063,6 +11238,10 @@ class Call {
|
|
|
11063
11238
|
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
11064
11239
|
this.registerEffects();
|
|
11065
11240
|
this.registerReconnectHandlers();
|
|
11241
|
+
this.camera.setup();
|
|
11242
|
+
this.microphone.setup();
|
|
11243
|
+
this.screenShare.setup();
|
|
11244
|
+
this.speaker.setup();
|
|
11066
11245
|
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
11067
11246
|
this.state.setCallingState(exports.CallingState.IDLE);
|
|
11068
11247
|
}
|
|
@@ -11242,10 +11421,13 @@ class Call {
|
|
|
11242
11421
|
* Leave the call and stop the media streams that were published by the call.
|
|
11243
11422
|
*/
|
|
11244
11423
|
this.leave = async ({ reject, reason, message } = {}) => {
|
|
11424
|
+
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
11425
|
+
throw new Error('Cannot leave call that has already been left.');
|
|
11426
|
+
}
|
|
11245
11427
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
11246
11428
|
const callingState = this.state.callingState;
|
|
11247
11429
|
if (callingState === exports.CallingState.LEFT) {
|
|
11248
|
-
|
|
11430
|
+
return;
|
|
11249
11431
|
}
|
|
11250
11432
|
if (callingState === exports.CallingState.JOINING) {
|
|
11251
11433
|
const waitUntilCallJoined = () => {
|
|
@@ -11298,6 +11480,7 @@ class Call {
|
|
|
11298
11480
|
this.microphone.dispose();
|
|
11299
11481
|
this.screenShare.dispose();
|
|
11300
11482
|
this.speaker.dispose();
|
|
11483
|
+
this.deviceSettingsAppliedOnce = false;
|
|
11301
11484
|
const stopOnLeavePromises = [];
|
|
11302
11485
|
if (this.camera.stopOnLeave) {
|
|
11303
11486
|
stopOnLeavePromises.push(this.camera.disable(true));
|
|
@@ -11352,7 +11535,6 @@ class Call {
|
|
|
11352
11535
|
this.state.setMembers(response.members);
|
|
11353
11536
|
this.state.setOwnCapabilities(response.own_capabilities);
|
|
11354
11537
|
if (params?.ring) {
|
|
11355
|
-
// the call response can indicate where the call is still ringing or not
|
|
11356
11538
|
this.ringingSubject.next(true);
|
|
11357
11539
|
}
|
|
11358
11540
|
if (this.streamClient._hasConnectionID()) {
|
|
@@ -11374,7 +11556,6 @@ class Call {
|
|
|
11374
11556
|
this.state.setMembers(response.members);
|
|
11375
11557
|
this.state.setOwnCapabilities(response.own_capabilities);
|
|
11376
11558
|
if (data?.ring) {
|
|
11377
|
-
// the call response can indicate where the call is still ringing or not
|
|
11378
11559
|
this.ringingSubject.next(true);
|
|
11379
11560
|
}
|
|
11380
11561
|
if (this.streamClient._hasConnectionID()) {
|
|
@@ -11554,7 +11735,7 @@ class Call {
|
|
|
11554
11735
|
}
|
|
11555
11736
|
catch (error) {
|
|
11556
11737
|
this.logger('warn', 'Join SFU request failed', error);
|
|
11557
|
-
sfuClient.close(StreamSfuClient.
|
|
11738
|
+
sfuClient.close(StreamSfuClient.JOIN_FAILED, 'Join request failed, connection considered unhealthy');
|
|
11558
11739
|
// restore the previous call state if the join-flow fails
|
|
11559
11740
|
this.state.setCallingState(callingState);
|
|
11560
11741
|
throw error;
|
|
@@ -11697,7 +11878,7 @@ class Call {
|
|
|
11697
11878
|
}
|
|
11698
11879
|
if (this.publisher) {
|
|
11699
11880
|
this.publisher.setSfuClient(nextSfuClient);
|
|
11700
|
-
if (includePublisher) {
|
|
11881
|
+
if (includePublisher && this.publisher.isPublishing()) {
|
|
11701
11882
|
await this.publisher.restartIce();
|
|
11702
11883
|
}
|
|
11703
11884
|
}
|
|
@@ -11719,9 +11900,10 @@ class Call {
|
|
|
11719
11900
|
connectionConfig,
|
|
11720
11901
|
logTag: String(this.sfuClientTag),
|
|
11721
11902
|
enableTracing,
|
|
11722
|
-
|
|
11723
|
-
this.reconnect(
|
|
11724
|
-
|
|
11903
|
+
onReconnectionNeeded: (kind, reason) => {
|
|
11904
|
+
this.reconnect(kind, reason).catch((err) => {
|
|
11905
|
+
const message = `[Reconnect] Error reconnecting after a subscriber error: ${reason}`;
|
|
11906
|
+
this.logger('warn', message, err);
|
|
11725
11907
|
});
|
|
11726
11908
|
},
|
|
11727
11909
|
});
|
|
@@ -11740,9 +11922,10 @@ class Call {
|
|
|
11740
11922
|
publishOptions,
|
|
11741
11923
|
logTag: String(this.sfuClientTag),
|
|
11742
11924
|
enableTracing,
|
|
11743
|
-
|
|
11744
|
-
this.reconnect(
|
|
11745
|
-
|
|
11925
|
+
onReconnectionNeeded: (kind, reason) => {
|
|
11926
|
+
this.reconnect(kind, reason).catch((err) => {
|
|
11927
|
+
const message = `[Reconnect] Error reconnecting after a publisher error: ${reason}`;
|
|
11928
|
+
this.logger('warn', message, err);
|
|
11746
11929
|
});
|
|
11747
11930
|
},
|
|
11748
11931
|
});
|
|
@@ -11826,9 +12009,12 @@ class Call {
|
|
|
11826
12009
|
callingState === exports.CallingState.LEFT)
|
|
11827
12010
|
return;
|
|
11828
12011
|
// normal close, no need to reconnect
|
|
11829
|
-
if (sfuClient.isLeaving || sfuClient.
|
|
12012
|
+
if (sfuClient.isLeaving || sfuClient.isClosingClean)
|
|
11830
12013
|
return;
|
|
11831
|
-
this.
|
|
12014
|
+
const strategy = this.publisher?.isHealthy() && this.subscriber?.isHealthy()
|
|
12015
|
+
? WebsocketReconnectStrategy.FAST
|
|
12016
|
+
: WebsocketReconnectStrategy.REJOIN;
|
|
12017
|
+
this.reconnect(strategy, reason).catch((err) => {
|
|
11832
12018
|
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
11833
12019
|
});
|
|
11834
12020
|
};
|
|
@@ -11849,10 +12035,12 @@ class Call {
|
|
|
11849
12035
|
const reconnectStartTime = Date.now();
|
|
11850
12036
|
this.reconnectStrategy = strategy;
|
|
11851
12037
|
this.reconnectReason = reason;
|
|
12038
|
+
let attempt = 0;
|
|
11852
12039
|
do {
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
12040
|
+
const reconnectingTime = Date.now() - reconnectStartTime;
|
|
12041
|
+
const shouldGiveUpReconnecting = this.disconnectionTimeoutSeconds > 0 &&
|
|
12042
|
+
reconnectingTime / 1000 > this.disconnectionTimeoutSeconds;
|
|
12043
|
+
if (shouldGiveUpReconnecting) {
|
|
11856
12044
|
this.logger('warn', '[Reconnect] Stopping reconnection attempts after reaching disconnection timeout');
|
|
11857
12045
|
this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
|
|
11858
12046
|
return;
|
|
@@ -11861,7 +12049,7 @@ class Call {
|
|
|
11861
12049
|
if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
|
|
11862
12050
|
this.reconnectAttempts++;
|
|
11863
12051
|
}
|
|
11864
|
-
const
|
|
12052
|
+
const currentStrategy = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
11865
12053
|
try {
|
|
11866
12054
|
// wait until the network is available
|
|
11867
12055
|
await this.networkAvailableTask?.promise;
|
|
@@ -11869,7 +12057,7 @@ class Call {
|
|
|
11869
12057
|
switch (this.reconnectStrategy) {
|
|
11870
12058
|
case WebsocketReconnectStrategy.UNSPECIFIED:
|
|
11871
12059
|
case WebsocketReconnectStrategy.DISCONNECT:
|
|
11872
|
-
this.logger('debug', `[Reconnect] No-op strategy ${
|
|
12060
|
+
this.logger('debug', `[Reconnect] No-op strategy ${currentStrategy}`);
|
|
11873
12061
|
break;
|
|
11874
12062
|
case WebsocketReconnectStrategy.FAST:
|
|
11875
12063
|
await this.reconnectFast();
|
|
@@ -11888,7 +12076,7 @@ class Call {
|
|
|
11888
12076
|
}
|
|
11889
12077
|
catch (error) {
|
|
11890
12078
|
if (this.state.callingState === exports.CallingState.OFFLINE) {
|
|
11891
|
-
this.logger('
|
|
12079
|
+
this.logger('debug', `[Reconnect] Can't reconnect while offline, stopping reconnection attempts`);
|
|
11892
12080
|
break;
|
|
11893
12081
|
// we don't need to handle the error if the call is offline
|
|
11894
12082
|
// network change event will trigger the reconnection
|
|
@@ -11898,9 +12086,24 @@ class Call {
|
|
|
11898
12086
|
this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
|
|
11899
12087
|
return;
|
|
11900
12088
|
}
|
|
11901
|
-
this.logger('warn', `[Reconnect] ${current} (${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
|
|
11902
12089
|
await sleep(500);
|
|
11903
|
-
this.reconnectStrategy
|
|
12090
|
+
const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
12091
|
+
const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
|
|
12092
|
+
this.fastReconnectDeadlineSeconds;
|
|
12093
|
+
// don't immediately switch to the REJOIN strategy, but instead attempt
|
|
12094
|
+
// to reconnect with the FAST strategy for a few times before switching.
|
|
12095
|
+
// in some cases, we immediately switch to the REJOIN strategy.
|
|
12096
|
+
const shouldRejoin = mustPerformRejoin || // if we are past the fast reconnect deadline
|
|
12097
|
+
wasMigrating || // if we were migrating, but the migration failed
|
|
12098
|
+
attempt >= 3 || // after 3 failed attempts
|
|
12099
|
+
!(this.publisher?.isHealthy() ?? true) || // if the publisher is not healthy
|
|
12100
|
+
!(this.subscriber?.isHealthy() ?? true); // if the subscriber is not healthy
|
|
12101
|
+
attempt++;
|
|
12102
|
+
const nextStrategy = shouldRejoin
|
|
12103
|
+
? WebsocketReconnectStrategy.REJOIN
|
|
12104
|
+
: WebsocketReconnectStrategy.FAST;
|
|
12105
|
+
this.reconnectStrategy = nextStrategy;
|
|
12106
|
+
this.logger('info', `[Reconnect] ${currentStrategy} (${this.reconnectAttempts}) failed. Attempting with ${WebsocketReconnectStrategy[nextStrategy]}`, error);
|
|
11904
12107
|
}
|
|
11905
12108
|
} while (this.state.callingState !== exports.CallingState.JOINED &&
|
|
11906
12109
|
this.state.callingState !== exports.CallingState.RECONNECTING_FAILED &&
|
|
@@ -12913,6 +13116,38 @@ class Call {
|
|
|
12913
13116
|
}
|
|
12914
13117
|
}
|
|
12915
13118
|
|
|
13119
|
+
const APIErrorCodes = {
|
|
13120
|
+
[-1]: 'InternalSystemError',
|
|
13121
|
+
2: 'AccessKeyError',
|
|
13122
|
+
3: 'AuthenticationFailedError',
|
|
13123
|
+
4: 'InputError',
|
|
13124
|
+
5: 'AuthenticationError',
|
|
13125
|
+
6: 'DuplicateUsernameError',
|
|
13126
|
+
9: 'RateLimitError',
|
|
13127
|
+
16: 'DoesNotExistError',
|
|
13128
|
+
17: 'NotAllowedError',
|
|
13129
|
+
18: 'EventNotSupportedError',
|
|
13130
|
+
19: 'ChannelFeatureNotSupportedError',
|
|
13131
|
+
20: 'MessageTooLongError',
|
|
13132
|
+
21: 'MultipleNestingLevelError',
|
|
13133
|
+
22: 'PayloadTooBigError',
|
|
13134
|
+
23: 'RequestTimeoutError',
|
|
13135
|
+
24: 'MaxHeaderSizeExceededError',
|
|
13136
|
+
40: 'AuthErrorTokenExpired',
|
|
13137
|
+
41: 'AuthErrorTokenNotValidYet',
|
|
13138
|
+
42: 'AuthErrorTokenUsedBeforeIssuedAt',
|
|
13139
|
+
43: 'AuthErrorTokenSignatureInvalid',
|
|
13140
|
+
44: 'CustomCommandEndpointMissingError',
|
|
13141
|
+
45: 'CustomCommandEndpointCallError',
|
|
13142
|
+
46: 'ConnectionIDNotFoundError',
|
|
13143
|
+
60: 'CoolDownError',
|
|
13144
|
+
69: 'ErrWrongRegion',
|
|
13145
|
+
70: 'ErrQueryChannelPermissions',
|
|
13146
|
+
71: 'ErrTooManyConnections',
|
|
13147
|
+
73: 'MessageModerationFailedError',
|
|
13148
|
+
99: 'AppSuspendedError',
|
|
13149
|
+
};
|
|
13150
|
+
|
|
12916
13151
|
/**
|
|
12917
13152
|
* StableWSConnection - A WS connection that reconnects upon failure.
|
|
12918
13153
|
* - the browser will sometimes report that you're online or offline
|
|
@@ -13139,7 +13374,7 @@ class StableWSConnection {
|
|
|
13139
13374
|
message = error.message;
|
|
13140
13375
|
statusCode = error.StatusCode;
|
|
13141
13376
|
}
|
|
13142
|
-
const msg = `WS failed with code: ${code} and reason: ${message}`;
|
|
13377
|
+
const msg = `WS failed with code: ${code}: ${APIErrorCodes[code] || code} and reason: ${message}`;
|
|
13143
13378
|
this._log(msg, { event }, 'warn');
|
|
13144
13379
|
const error = new Error(msg);
|
|
13145
13380
|
error.code = code;
|
|
@@ -14017,7 +14252,7 @@ class StreamClient {
|
|
|
14017
14252
|
this.getUserAgent = () => {
|
|
14018
14253
|
if (!this.cachedUserAgent) {
|
|
14019
14254
|
const { clientAppIdentifier = {} } = this.options;
|
|
14020
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
14255
|
+
const { sdkName = 'js', sdkVersion = "1.25.1", ...extras } = clientAppIdentifier;
|
|
14021
14256
|
this.cachedUserAgent = [
|
|
14022
14257
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
14023
14258
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -14367,13 +14602,17 @@ class StreamVideoClient {
|
|
|
14367
14602
|
* @param type the type of the call.
|
|
14368
14603
|
* @param id the id of the call.
|
|
14369
14604
|
*/
|
|
14370
|
-
this.call = (type, id) => {
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14605
|
+
this.call = (type, id, options = {}) => {
|
|
14606
|
+
const call = options.reuseInstance
|
|
14607
|
+
? this.writeableStateStore.findCall(type, id)
|
|
14608
|
+
: undefined;
|
|
14609
|
+
return (call ??
|
|
14610
|
+
new Call({
|
|
14611
|
+
streamClient: this.streamClient,
|
|
14612
|
+
id: id,
|
|
14613
|
+
type: type,
|
|
14614
|
+
clientStore: this.writeableStateStore,
|
|
14615
|
+
}));
|
|
14377
14616
|
};
|
|
14378
14617
|
/**
|
|
14379
14618
|
* Creates a new guest user with the given data.
|