@stream-io/video-client 1.24.0 → 1.25.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 +14 -0
- package/dist/index.browser.es.js +335 -127
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +334 -126
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +335 -127
- 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/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 +66 -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/ScreenShareManager.ts +12 -2
- 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.browser.es.js
CHANGED
|
@@ -6,7 +6,7 @@ export { AxiosError } from 'axios';
|
|
|
6
6
|
import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transport';
|
|
7
7
|
import { ReplaySubject, combineLatest, BehaviorSubject, shareReplay, map, distinctUntilChanged, takeWhile, distinctUntilKeyChanged, fromEventPattern, startWith, concatMap, merge, from, fromEvent, debounceTime, pairwise, of } from 'rxjs';
|
|
8
8
|
import { UAParser } from 'ua-parser-js';
|
|
9
|
-
import { parse } from 'sdp-transform';
|
|
9
|
+
import { parse, write } from 'sdp-transform';
|
|
10
10
|
|
|
11
11
|
/* tslint:disable */
|
|
12
12
|
/**
|
|
@@ -420,7 +420,7 @@ class ErrorFromResponse extends Error {
|
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
// @generated by protobuf-ts 2.
|
|
423
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
424
424
|
// @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
|
|
425
425
|
// tslint:disable
|
|
426
426
|
//
|
|
@@ -653,7 +653,7 @@ class ListValue$Type extends MessageType {
|
|
|
653
653
|
no: 1,
|
|
654
654
|
name: 'values',
|
|
655
655
|
kind: 'message',
|
|
656
|
-
repeat:
|
|
656
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
657
657
|
T: () => Value,
|
|
658
658
|
},
|
|
659
659
|
]);
|
|
@@ -685,7 +685,7 @@ class ListValue$Type extends MessageType {
|
|
|
685
685
|
*/
|
|
686
686
|
const ListValue = new ListValue$Type();
|
|
687
687
|
|
|
688
|
-
// @generated by protobuf-ts 2.
|
|
688
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
689
689
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
|
690
690
|
// tslint:disable
|
|
691
691
|
//
|
|
@@ -821,7 +821,7 @@ class Timestamp$Type extends MessageType {
|
|
|
821
821
|
*/
|
|
822
822
|
const Timestamp = new Timestamp$Type();
|
|
823
823
|
|
|
824
|
-
// @generated by protobuf-ts 2.
|
|
824
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
825
825
|
// @generated from protobuf file "video/sfu/models/models.proto" (package "stream.video.sfu.models", syntax proto3)
|
|
826
826
|
// tslint:disable
|
|
827
827
|
/**
|
|
@@ -963,6 +963,10 @@ var ErrorCode;
|
|
|
963
963
|
* @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_MEDIA_TRANSPORT_FAILURE = 205;
|
|
964
964
|
*/
|
|
965
965
|
ErrorCode[ErrorCode["PARTICIPANT_MEDIA_TRANSPORT_FAILURE"] = 205] = "PARTICIPANT_MEDIA_TRANSPORT_FAILURE";
|
|
966
|
+
/**
|
|
967
|
+
* @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_SIGNAL_LOST = 206;
|
|
968
|
+
*/
|
|
969
|
+
ErrorCode[ErrorCode["PARTICIPANT_SIGNAL_LOST"] = 206] = "PARTICIPANT_SIGNAL_LOST";
|
|
966
970
|
/**
|
|
967
971
|
* @generated from protobuf enum value: ERROR_CODE_CALL_NOT_FOUND = 300;
|
|
968
972
|
*/
|
|
@@ -1243,7 +1247,7 @@ class CallState$Type extends MessageType {
|
|
|
1243
1247
|
no: 1,
|
|
1244
1248
|
name: 'participants',
|
|
1245
1249
|
kind: 'message',
|
|
1246
|
-
repeat:
|
|
1250
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1247
1251
|
T: () => Participant,
|
|
1248
1252
|
},
|
|
1249
1253
|
{ no: 2, name: 'started_at', kind: 'message', T: () => Timestamp },
|
|
@@ -1257,7 +1261,7 @@ class CallState$Type extends MessageType {
|
|
|
1257
1261
|
no: 4,
|
|
1258
1262
|
name: 'pins',
|
|
1259
1263
|
kind: 'message',
|
|
1260
|
-
repeat:
|
|
1264
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1261
1265
|
T: () => Pin,
|
|
1262
1266
|
},
|
|
1263
1267
|
]);
|
|
@@ -1435,7 +1439,7 @@ class SubscribeOption$Type extends MessageType {
|
|
|
1435
1439
|
no: 2,
|
|
1436
1440
|
name: 'codecs',
|
|
1437
1441
|
kind: 'message',
|
|
1438
|
-
repeat:
|
|
1442
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1439
1443
|
T: () => Codec,
|
|
1440
1444
|
},
|
|
1441
1445
|
]);
|
|
@@ -1568,7 +1572,7 @@ class TrackInfo$Type extends MessageType {
|
|
|
1568
1572
|
no: 5,
|
|
1569
1573
|
name: 'layers',
|
|
1570
1574
|
kind: 'message',
|
|
1571
|
-
repeat:
|
|
1575
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1572
1576
|
T: () => VideoLayer,
|
|
1573
1577
|
},
|
|
1574
1578
|
{ no: 6, name: 'mid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
@@ -1935,7 +1939,7 @@ var models = /*#__PURE__*/Object.freeze({
|
|
|
1935
1939
|
get WebsocketReconnectStrategy () { return WebsocketReconnectStrategy; }
|
|
1936
1940
|
});
|
|
1937
1941
|
|
|
1938
|
-
// @generated by protobuf-ts 2.
|
|
1942
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
1939
1943
|
// @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3)
|
|
1940
1944
|
// tslint:disable
|
|
1941
1945
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
@@ -2103,14 +2107,14 @@ class SendStatsRequest$Type extends MessageType {
|
|
|
2103
2107
|
no: 16,
|
|
2104
2108
|
name: 'encode_stats',
|
|
2105
2109
|
kind: 'message',
|
|
2106
|
-
repeat:
|
|
2110
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2107
2111
|
T: () => PerformanceStats,
|
|
2108
2112
|
},
|
|
2109
2113
|
{
|
|
2110
2114
|
no: 17,
|
|
2111
2115
|
name: 'decode_stats',
|
|
2112
2116
|
kind: 'message',
|
|
2113
|
-
repeat:
|
|
2117
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2114
2118
|
T: () => PerformanceStats,
|
|
2115
2119
|
},
|
|
2116
2120
|
{
|
|
@@ -2177,7 +2181,7 @@ class UpdateMuteStatesRequest$Type extends MessageType {
|
|
|
2177
2181
|
no: 3,
|
|
2178
2182
|
name: 'mute_states',
|
|
2179
2183
|
kind: 'message',
|
|
2180
|
-
repeat:
|
|
2184
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2181
2185
|
T: () => TrackMuteState,
|
|
2182
2186
|
},
|
|
2183
2187
|
]);
|
|
@@ -2254,7 +2258,7 @@ class UpdateSubscriptionsRequest$Type extends MessageType {
|
|
|
2254
2258
|
no: 3,
|
|
2255
2259
|
name: 'tracks',
|
|
2256
2260
|
kind: 'message',
|
|
2257
|
-
repeat:
|
|
2261
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2258
2262
|
T: () => TrackSubscriptionDetails,
|
|
2259
2263
|
},
|
|
2260
2264
|
]);
|
|
@@ -2353,7 +2357,7 @@ class SetPublisherRequest$Type extends MessageType {
|
|
|
2353
2357
|
no: 3,
|
|
2354
2358
|
name: 'tracks',
|
|
2355
2359
|
kind: 'message',
|
|
2356
|
-
repeat:
|
|
2360
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2357
2361
|
T: () => TrackInfo,
|
|
2358
2362
|
},
|
|
2359
2363
|
]);
|
|
@@ -2433,7 +2437,7 @@ const SignalServer = new ServiceType('stream.video.sfu.signal.SignalServer', [
|
|
|
2433
2437
|
},
|
|
2434
2438
|
]);
|
|
2435
2439
|
|
|
2436
|
-
// @generated by protobuf-ts 2.
|
|
2440
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
2437
2441
|
// @generated from protobuf file "video/sfu/event/events.proto" (package "stream.video.sfu.event", syntax proto3)
|
|
2438
2442
|
// tslint:disable
|
|
2439
2443
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
@@ -2609,7 +2613,7 @@ class ChangePublishOptions$Type extends MessageType {
|
|
|
2609
2613
|
no: 1,
|
|
2610
2614
|
name: 'publish_options',
|
|
2611
2615
|
kind: 'message',
|
|
2612
|
-
repeat:
|
|
2616
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2613
2617
|
T: () => PublishOption,
|
|
2614
2618
|
},
|
|
2615
2619
|
{ no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
@@ -2648,7 +2652,7 @@ class PinsChanged$Type extends MessageType {
|
|
|
2648
2652
|
no: 1,
|
|
2649
2653
|
name: 'pins',
|
|
2650
2654
|
kind: 'message',
|
|
2651
|
-
repeat:
|
|
2655
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2652
2656
|
T: () => Pin,
|
|
2653
2657
|
},
|
|
2654
2658
|
]);
|
|
@@ -2891,14 +2895,14 @@ class JoinRequest$Type extends MessageType {
|
|
|
2891
2895
|
no: 9,
|
|
2892
2896
|
name: 'preferred_publish_options',
|
|
2893
2897
|
kind: 'message',
|
|
2894
|
-
repeat:
|
|
2898
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2895
2899
|
T: () => PublishOption,
|
|
2896
2900
|
},
|
|
2897
2901
|
{
|
|
2898
2902
|
no: 10,
|
|
2899
2903
|
name: 'preferred_subscribe_options',
|
|
2900
2904
|
kind: 'message',
|
|
2901
|
-
repeat:
|
|
2905
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2902
2906
|
T: () => SubscribeOption,
|
|
2903
2907
|
},
|
|
2904
2908
|
]);
|
|
@@ -2926,14 +2930,14 @@ class ReconnectDetails$Type extends MessageType {
|
|
|
2926
2930
|
no: 3,
|
|
2927
2931
|
name: 'announced_tracks',
|
|
2928
2932
|
kind: 'message',
|
|
2929
|
-
repeat:
|
|
2933
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2930
2934
|
T: () => TrackInfo,
|
|
2931
2935
|
},
|
|
2932
2936
|
{
|
|
2933
2937
|
no: 4,
|
|
2934
2938
|
name: 'subscriptions',
|
|
2935
2939
|
kind: 'message',
|
|
2936
|
-
repeat:
|
|
2940
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2937
2941
|
T: () => TrackSubscriptionDetails,
|
|
2938
2942
|
},
|
|
2939
2943
|
{
|
|
@@ -2976,14 +2980,14 @@ class Migration$Type extends MessageType {
|
|
|
2976
2980
|
no: 2,
|
|
2977
2981
|
name: 'announced_tracks',
|
|
2978
2982
|
kind: 'message',
|
|
2979
|
-
repeat:
|
|
2983
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2980
2984
|
T: () => TrackInfo,
|
|
2981
2985
|
},
|
|
2982
2986
|
{
|
|
2983
2987
|
no: 3,
|
|
2984
2988
|
name: 'subscriptions',
|
|
2985
2989
|
kind: 'message',
|
|
2986
|
-
repeat:
|
|
2990
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
2987
2991
|
T: () => TrackSubscriptionDetails,
|
|
2988
2992
|
},
|
|
2989
2993
|
]);
|
|
@@ -3009,7 +3013,7 @@ class JoinResponse$Type extends MessageType {
|
|
|
3009
3013
|
no: 4,
|
|
3010
3014
|
name: 'publish_options',
|
|
3011
3015
|
kind: 'message',
|
|
3012
|
-
repeat:
|
|
3016
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3013
3017
|
T: () => PublishOption,
|
|
3014
3018
|
},
|
|
3015
3019
|
]);
|
|
@@ -3091,7 +3095,7 @@ class ConnectionQualityChanged$Type extends MessageType {
|
|
|
3091
3095
|
no: 1,
|
|
3092
3096
|
name: 'connection_quality_updates',
|
|
3093
3097
|
kind: 'message',
|
|
3094
|
-
repeat:
|
|
3098
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3095
3099
|
T: () => ConnectionQualityInfo,
|
|
3096
3100
|
},
|
|
3097
3101
|
]);
|
|
@@ -3160,7 +3164,7 @@ class AudioLevelChanged$Type extends MessageType {
|
|
|
3160
3164
|
no: 1,
|
|
3161
3165
|
name: 'audio_levels',
|
|
3162
3166
|
kind: 'message',
|
|
3163
|
-
repeat:
|
|
3167
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3164
3168
|
T: () => AudioLevel,
|
|
3165
3169
|
},
|
|
3166
3170
|
]);
|
|
@@ -3240,7 +3244,7 @@ class VideoSender$Type extends MessageType {
|
|
|
3240
3244
|
no: 3,
|
|
3241
3245
|
name: 'layers',
|
|
3242
3246
|
kind: 'message',
|
|
3243
|
-
repeat:
|
|
3247
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3244
3248
|
T: () => VideoLayerSetting,
|
|
3245
3249
|
},
|
|
3246
3250
|
{
|
|
@@ -3274,14 +3278,14 @@ class ChangePublishQuality$Type extends MessageType {
|
|
|
3274
3278
|
no: 1,
|
|
3275
3279
|
name: 'audio_senders',
|
|
3276
3280
|
kind: 'message',
|
|
3277
|
-
repeat:
|
|
3281
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3278
3282
|
T: () => AudioSender,
|
|
3279
3283
|
},
|
|
3280
3284
|
{
|
|
3281
3285
|
no: 2,
|
|
3282
3286
|
name: 'video_senders',
|
|
3283
3287
|
kind: 'message',
|
|
3284
|
-
repeat:
|
|
3288
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
3285
3289
|
T: () => VideoSender,
|
|
3286
3290
|
},
|
|
3287
3291
|
]);
|
|
@@ -5473,6 +5477,21 @@ class CallState {
|
|
|
5473
5477
|
}
|
|
5474
5478
|
}
|
|
5475
5479
|
|
|
5480
|
+
/**
|
|
5481
|
+
* NegotiationError is thrown when there is an error during the negotiation process.
|
|
5482
|
+
* It extends the built-in Error class and includes an SfuError object for more details.
|
|
5483
|
+
*/
|
|
5484
|
+
class NegotiationError extends Error {
|
|
5485
|
+
/**
|
|
5486
|
+
* Creates an instance of NegotiationError.
|
|
5487
|
+
*/
|
|
5488
|
+
constructor(error) {
|
|
5489
|
+
super(error.message);
|
|
5490
|
+
this.name = 'NegotiationError';
|
|
5491
|
+
this.error = error;
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5476
5495
|
/**
|
|
5477
5496
|
* Flatten the stats report into an array of stats objects.
|
|
5478
5497
|
*
|
|
@@ -5791,7 +5810,7 @@ const aggregate = (stats) => {
|
|
|
5791
5810
|
return report;
|
|
5792
5811
|
};
|
|
5793
5812
|
|
|
5794
|
-
const version = "1.
|
|
5813
|
+
const version = "1.25.0";
|
|
5795
5814
|
const [major, minor, patch] = version.split('.');
|
|
5796
5815
|
let sdkInfo = {
|
|
5797
5816
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -6400,12 +6419,38 @@ class BasePeerConnection {
|
|
|
6400
6419
|
/**
|
|
6401
6420
|
* Constructs a new `BasePeerConnection` instance.
|
|
6402
6421
|
*/
|
|
6403
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher,
|
|
6422
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, logTag, enableTracing, iceRestartDelay = 2500, }) {
|
|
6404
6423
|
this.isIceRestarting = false;
|
|
6405
6424
|
this.isDisposed = false;
|
|
6406
6425
|
this.trackIdToTrackType = new Map();
|
|
6407
6426
|
this.subscriptions = [];
|
|
6408
6427
|
this.lock = Math.random().toString(36).slice(2);
|
|
6428
|
+
this.createPeerConnection = (connectionConfig) => {
|
|
6429
|
+
const pc = new RTCPeerConnection(connectionConfig);
|
|
6430
|
+
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
6431
|
+
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6432
|
+
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6433
|
+
pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
6434
|
+
pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
6435
|
+
pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
6436
|
+
return pc;
|
|
6437
|
+
};
|
|
6438
|
+
/**
|
|
6439
|
+
* Attempts to restart ICE on the `RTCPeerConnection`.
|
|
6440
|
+
* This method intentionally doesn't await the `restartIce()` method,
|
|
6441
|
+
* allowing it to run in the background and handle any errors that may occur.
|
|
6442
|
+
*/
|
|
6443
|
+
this.tryRestartIce = () => {
|
|
6444
|
+
this.restartIce().catch((e) => {
|
|
6445
|
+
const reason = 'restartICE() failed, initiating reconnect';
|
|
6446
|
+
this.logger('error', reason, e);
|
|
6447
|
+
const strategy = e instanceof NegotiationError &&
|
|
6448
|
+
e.error.code === ErrorCode.PARTICIPANT_SIGNAL_LOST
|
|
6449
|
+
? WebsocketReconnectStrategy.FAST
|
|
6450
|
+
: WebsocketReconnectStrategy.REJOIN;
|
|
6451
|
+
this.onReconnectionNeeded?.(strategy, reason);
|
|
6452
|
+
});
|
|
6453
|
+
};
|
|
6409
6454
|
/**
|
|
6410
6455
|
* Handles events synchronously.
|
|
6411
6456
|
* Consecutive events are queued and executed one after the other.
|
|
@@ -6458,6 +6503,18 @@ class BasePeerConnection {
|
|
|
6458
6503
|
this.getTrackType = (trackId) => {
|
|
6459
6504
|
return this.trackIdToTrackType.get(trackId);
|
|
6460
6505
|
};
|
|
6506
|
+
/**
|
|
6507
|
+
* Checks if the `RTCPeerConnection` is healthy.
|
|
6508
|
+
* It checks the ICE connection state and the peer connection state.
|
|
6509
|
+
* If either state is `failed`, `disconnected`, or `closed`,
|
|
6510
|
+
* it returns `false`, otherwise it returns `true`.
|
|
6511
|
+
*/
|
|
6512
|
+
this.isHealthy = () => {
|
|
6513
|
+
const failedStates = new Set(['failed', 'closed']);
|
|
6514
|
+
const iceState = this.pc.iceConnectionState;
|
|
6515
|
+
const connectionState = this.pc.connectionState;
|
|
6516
|
+
return !failedStates.has(iceState) && !failedStates.has(connectionState);
|
|
6517
|
+
};
|
|
6461
6518
|
/**
|
|
6462
6519
|
* Handles the ICECandidate event and
|
|
6463
6520
|
* Initiates an ICE Trickle process with the SFU.
|
|
@@ -6496,9 +6553,7 @@ class BasePeerConnection {
|
|
|
6496
6553
|
this.onConnectionStateChange = async () => {
|
|
6497
6554
|
const state = this.pc.connectionState;
|
|
6498
6555
|
this.logger('debug', `Connection state changed`, state);
|
|
6499
|
-
if (
|
|
6500
|
-
return;
|
|
6501
|
-
if (state === 'connected' || state === 'failed') {
|
|
6556
|
+
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
6502
6557
|
try {
|
|
6503
6558
|
const stats = await this.stats.get();
|
|
6504
6559
|
this.tracer.trace('getstats', stats.delta);
|
|
@@ -6507,6 +6562,12 @@ class BasePeerConnection {
|
|
|
6507
6562
|
this.tracer.trace('getstatsOnFailure', err.toString());
|
|
6508
6563
|
}
|
|
6509
6564
|
}
|
|
6565
|
+
// we can't recover from a failed connection state (contrary to ICE)
|
|
6566
|
+
if (state === 'failed') {
|
|
6567
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, 'Connection failed');
|
|
6568
|
+
return;
|
|
6569
|
+
}
|
|
6570
|
+
this.handleConnectionStateUpdate(state);
|
|
6510
6571
|
};
|
|
6511
6572
|
/**
|
|
6512
6573
|
* Handles the ICE connection state change event.
|
|
@@ -6514,34 +6575,53 @@ class BasePeerConnection {
|
|
|
6514
6575
|
this.onIceConnectionStateChange = () => {
|
|
6515
6576
|
const state = this.pc.iceConnectionState;
|
|
6516
6577
|
this.logger('debug', `ICE connection state changed`, state);
|
|
6517
|
-
|
|
6578
|
+
this.handleConnectionStateUpdate(state);
|
|
6579
|
+
};
|
|
6580
|
+
this.handleConnectionStateUpdate = (state) => {
|
|
6581
|
+
const { callingState } = this.state;
|
|
6582
|
+
if (callingState === CallingState.OFFLINE)
|
|
6518
6583
|
return;
|
|
6519
|
-
if (
|
|
6584
|
+
if (callingState === CallingState.RECONNECTING)
|
|
6520
6585
|
return;
|
|
6521
6586
|
// do nothing when ICE is restarting
|
|
6522
6587
|
if (this.isIceRestarting)
|
|
6523
6588
|
return;
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6589
|
+
switch (state) {
|
|
6590
|
+
case 'failed':
|
|
6591
|
+
// in the `failed` state, we try to restart ICE immediately
|
|
6592
|
+
this.logger('info', 'restartICE due to failed connection');
|
|
6593
|
+
this.tryRestartIce();
|
|
6594
|
+
break;
|
|
6595
|
+
case 'disconnected':
|
|
6596
|
+
// in the `disconnected` state, we schedule a restartICE() after a delay
|
|
6597
|
+
// as the browser might recover the connection in the meantime
|
|
6598
|
+
this.logger('info', 'disconnected connection, scheduling restartICE');
|
|
6599
|
+
clearTimeout(this.iceRestartTimeout);
|
|
6600
|
+
this.iceRestartTimeout = setTimeout(() => {
|
|
6601
|
+
const currentState = this.pc.iceConnectionState;
|
|
6602
|
+
if (currentState === 'disconnected' || currentState === 'failed') {
|
|
6603
|
+
this.tryRestartIce();
|
|
6604
|
+
}
|
|
6605
|
+
}, this.iceRestartDelay);
|
|
6606
|
+
break;
|
|
6607
|
+
case 'connected':
|
|
6608
|
+
// in the `connected` state, we clear the ice restart timeout if it exists
|
|
6609
|
+
if (this.iceRestartTimeout) {
|
|
6610
|
+
this.logger('info', 'connected connection, canceling restartICE');
|
|
6611
|
+
clearTimeout(this.iceRestartTimeout);
|
|
6612
|
+
this.iceRestartTimeout = undefined;
|
|
6613
|
+
}
|
|
6614
|
+
break;
|
|
6534
6615
|
}
|
|
6535
6616
|
};
|
|
6536
6617
|
/**
|
|
6537
6618
|
* Handles the ICE candidate error event.
|
|
6538
6619
|
*/
|
|
6539
6620
|
this.onIceCandidateError = (e) => {
|
|
6540
|
-
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent
|
|
6541
|
-
`${e.errorCode}: ${e.errorText}
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
6621
|
+
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent
|
|
6622
|
+
? `${e.errorCode}: ${e.errorText}`
|
|
6623
|
+
: e;
|
|
6624
|
+
this.logger('debug', 'ICE Candidate error', errorMessage);
|
|
6545
6625
|
};
|
|
6546
6626
|
/**
|
|
6547
6627
|
* Handles the ICE gathering state change event.
|
|
@@ -6559,18 +6639,13 @@ class BasePeerConnection {
|
|
|
6559
6639
|
this.sfuClient = sfuClient;
|
|
6560
6640
|
this.state = state;
|
|
6561
6641
|
this.dispatcher = dispatcher;
|
|
6562
|
-
this.
|
|
6642
|
+
this.iceRestartDelay = iceRestartDelay;
|
|
6643
|
+
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
6563
6644
|
this.logger = getLogger([
|
|
6564
6645
|
peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
|
|
6565
6646
|
logTag,
|
|
6566
6647
|
]);
|
|
6567
|
-
this.pc =
|
|
6568
|
-
this.pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
6569
|
-
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6570
|
-
this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6571
|
-
this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
6572
|
-
this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
6573
|
-
this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
6648
|
+
this.pc = this.createPeerConnection(connectionConfig);
|
|
6574
6649
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
|
|
6575
6650
|
if (enableTracing) {
|
|
6576
6651
|
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
|
|
@@ -6586,7 +6661,9 @@ class BasePeerConnection {
|
|
|
6586
6661
|
* Disposes the `RTCPeerConnection` instance.
|
|
6587
6662
|
*/
|
|
6588
6663
|
dispose() {
|
|
6589
|
-
this.
|
|
6664
|
+
clearTimeout(this.iceRestartTimeout);
|
|
6665
|
+
this.iceRestartTimeout = undefined;
|
|
6666
|
+
this.onReconnectionNeeded = undefined;
|
|
6590
6667
|
this.isDisposed = true;
|
|
6591
6668
|
this.detachEventHandlers();
|
|
6592
6669
|
this.pc.close();
|
|
@@ -6596,11 +6673,12 @@ class BasePeerConnection {
|
|
|
6596
6673
|
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
6597
6674
|
*/
|
|
6598
6675
|
detachEventHandlers() {
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6676
|
+
const pc = this.pc;
|
|
6677
|
+
pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
6678
|
+
pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6679
|
+
pc.removeEventListener('signalingstatechange', this.onSignalingChange);
|
|
6680
|
+
pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6681
|
+
pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
6604
6682
|
this.unsubscribeIceTrickle?.();
|
|
6605
6683
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
6606
6684
|
}
|
|
@@ -6910,6 +6988,45 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
|
6910
6988
|
return '';
|
|
6911
6989
|
return String(transceiverInitIndex);
|
|
6912
6990
|
};
|
|
6991
|
+
/**
|
|
6992
|
+
* Enables stereo in the answer SDP based on the offered stereo in the offer SDP.
|
|
6993
|
+
*
|
|
6994
|
+
* @param offerSdp the offer SDP containing the stereo configuration.
|
|
6995
|
+
* @param answerSdp the answer SDP to be modified.
|
|
6996
|
+
*/
|
|
6997
|
+
const enableStereo = (offerSdp, answerSdp) => {
|
|
6998
|
+
const offeredStereoMids = new Set();
|
|
6999
|
+
const parsedOfferSdp = parse(offerSdp);
|
|
7000
|
+
for (const media of parsedOfferSdp.media) {
|
|
7001
|
+
if (media.type !== 'audio')
|
|
7002
|
+
continue;
|
|
7003
|
+
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
7004
|
+
if (!opus)
|
|
7005
|
+
continue;
|
|
7006
|
+
for (const fmtp of media.fmtp) {
|
|
7007
|
+
if (fmtp.payload === opus.payload && fmtp.config.includes('stereo=1')) {
|
|
7008
|
+
offeredStereoMids.add(media.mid);
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
// No stereo offered, return the original answerSdp
|
|
7013
|
+
if (offeredStereoMids.size === 0)
|
|
7014
|
+
return answerSdp;
|
|
7015
|
+
const parsedAnswerSdp = parse(answerSdp);
|
|
7016
|
+
for (const media of parsedAnswerSdp.media) {
|
|
7017
|
+
if (media.type !== 'audio' || !offeredStereoMids.has(media.mid))
|
|
7018
|
+
continue;
|
|
7019
|
+
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
7020
|
+
if (!opus)
|
|
7021
|
+
continue;
|
|
7022
|
+
for (const fmtp of media.fmtp) {
|
|
7023
|
+
if (fmtp.payload === opus.payload && !fmtp.config.includes('stereo=1')) {
|
|
7024
|
+
fmtp.config += ';stereo=1';
|
|
7025
|
+
}
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
return write(parsedAnswerSdp);
|
|
7029
|
+
};
|
|
6913
7030
|
|
|
6914
7031
|
/**
|
|
6915
7032
|
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
@@ -7023,11 +7140,11 @@ class Publisher extends BasePeerConnection {
|
|
|
7023
7140
|
/**
|
|
7024
7141
|
* Returns true if the given track type is currently being published to the SFU.
|
|
7025
7142
|
*
|
|
7026
|
-
* @param trackType the track type to check.
|
|
7143
|
+
* @param trackType the track type to check. If omitted, checks if any track is being published.
|
|
7027
7144
|
*/
|
|
7028
7145
|
this.isPublishing = (trackType) => {
|
|
7029
7146
|
for (const item of this.transceiverCache.items()) {
|
|
7030
|
-
if (item.publishOption.trackType !== trackType)
|
|
7147
|
+
if (trackType && item.publishOption.trackType !== trackType)
|
|
7031
7148
|
continue;
|
|
7032
7149
|
const track = item.transceiver.sender.track;
|
|
7033
7150
|
if (!track)
|
|
@@ -7150,10 +7267,17 @@ class Publisher extends BasePeerConnection {
|
|
|
7150
7267
|
const { sdp = '' } = offer;
|
|
7151
7268
|
const { response } = await this.sfuClient.setPublisher({ sdp, tracks });
|
|
7152
7269
|
if (response.error)
|
|
7153
|
-
throw new
|
|
7270
|
+
throw new NegotiationError(response.error);
|
|
7154
7271
|
const { sdp: answerSdp } = response;
|
|
7155
7272
|
await this.pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
|
|
7156
7273
|
}
|
|
7274
|
+
catch (err) {
|
|
7275
|
+
// negotiation failed, rollback to the previous state
|
|
7276
|
+
if (this.pc.signalingState === 'have-local-offer') {
|
|
7277
|
+
await this.pc.setLocalDescription({ type: 'rollback' });
|
|
7278
|
+
}
|
|
7279
|
+
throw err;
|
|
7280
|
+
}
|
|
7157
7281
|
finally {
|
|
7158
7282
|
this.isIceRestarting = false;
|
|
7159
7283
|
}
|
|
@@ -7245,11 +7369,7 @@ class Publisher extends BasePeerConnection {
|
|
|
7245
7369
|
this.on('iceRestart', (iceRestart) => {
|
|
7246
7370
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
7247
7371
|
return;
|
|
7248
|
-
this.
|
|
7249
|
-
const reason = `ICE restart failed`;
|
|
7250
|
-
this.logger('warn', reason, err);
|
|
7251
|
-
this.onUnrecoverableError?.(`${reason}: ${err}`);
|
|
7252
|
-
});
|
|
7372
|
+
this.tryRestartIce();
|
|
7253
7373
|
});
|
|
7254
7374
|
this.on('changePublishQuality', async (event) => {
|
|
7255
7375
|
for (const videoSender of event.videoSenders) {
|
|
@@ -7297,11 +7417,13 @@ class Subscriber extends BasePeerConnection {
|
|
|
7297
7417
|
return;
|
|
7298
7418
|
}
|
|
7299
7419
|
const previousIsIceRestarting = this.isIceRestarting;
|
|
7420
|
+
this.isIceRestarting = true;
|
|
7300
7421
|
try {
|
|
7301
|
-
|
|
7302
|
-
await this.sfuClient.iceRestart({
|
|
7422
|
+
const { response } = await this.sfuClient.iceRestart({
|
|
7303
7423
|
peerType: PeerType.SUBSCRIBER,
|
|
7304
7424
|
});
|
|
7425
|
+
if (response.error)
|
|
7426
|
+
throw new NegotiationError(response.error);
|
|
7305
7427
|
}
|
|
7306
7428
|
catch (e) {
|
|
7307
7429
|
// restore the previous state, as our intent for restarting ICE failed
|
|
@@ -7370,6 +7492,9 @@ class Subscriber extends BasePeerConnection {
|
|
|
7370
7492
|
});
|
|
7371
7493
|
this.addTrickledIceCandidates();
|
|
7372
7494
|
const answer = await this.pc.createAnswer();
|
|
7495
|
+
if (answer.sdp) {
|
|
7496
|
+
answer.sdp = enableStereo(subscriberOffer.sdp, answer.sdp);
|
|
7497
|
+
}
|
|
7373
7498
|
await this.pc.setLocalDescription(answer);
|
|
7374
7499
|
await this.sfuClient.sendAnswer({
|
|
7375
7500
|
peerType: PeerType.SUBSCRIBER,
|
|
@@ -7659,11 +7784,15 @@ class StreamSfuClient {
|
|
|
7659
7784
|
*/
|
|
7660
7785
|
this.isLeaving = false;
|
|
7661
7786
|
/**
|
|
7662
|
-
* Flag to indicate if the client is in the process of closing the connection.
|
|
7787
|
+
* Flag to indicate if the client is in the process of clean closing the connection.
|
|
7788
|
+
* When set to `true`, the client will not attempt to reconnect
|
|
7789
|
+
* and will close the WebSocket connection gracefully.
|
|
7790
|
+
* Otherwise, it will close the connection with an error code and
|
|
7791
|
+
* trigger a reconnection attempt.
|
|
7663
7792
|
*/
|
|
7664
|
-
this.
|
|
7665
|
-
this.pingIntervalInMs =
|
|
7666
|
-
this.unhealthyTimeoutInMs =
|
|
7793
|
+
this.isClosingClean = false;
|
|
7794
|
+
this.pingIntervalInMs = 5 * 1000;
|
|
7795
|
+
this.unhealthyTimeoutInMs = 15 * 1000;
|
|
7667
7796
|
/**
|
|
7668
7797
|
* Promise that resolves when the JoinResponse is received.
|
|
7669
7798
|
* Rejects after a certain threshold if the response is not received.
|
|
@@ -7707,7 +7836,7 @@ class StreamSfuClient {
|
|
|
7707
7836
|
// Normally, this shouldn't have any effect, because WS should never emit 'close'
|
|
7708
7837
|
// before emitting 'open'. However, strager things have happened, and we don't
|
|
7709
7838
|
// want to leave signalReady in pending state.
|
|
7710
|
-
reject(new Error(
|
|
7839
|
+
reject(new Error(`SFU WS closed or connection can't be established`));
|
|
7711
7840
|
});
|
|
7712
7841
|
}),
|
|
7713
7842
|
new Promise((resolve, reject) => {
|
|
@@ -7725,7 +7854,7 @@ class StreamSfuClient {
|
|
|
7725
7854
|
this.onSignalClose?.(`${e.code} ${e.reason}`);
|
|
7726
7855
|
};
|
|
7727
7856
|
this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
|
|
7728
|
-
this.
|
|
7857
|
+
this.isClosingClean = code !== StreamSfuClient.ERROR_CONNECTION_UNHEALTHY;
|
|
7729
7858
|
if (this.signalWs.readyState === WebSocket.OPEN) {
|
|
7730
7859
|
this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
|
|
7731
7860
|
this.signalWs.close(code, `js-client: ${reason}`);
|
|
@@ -7965,7 +8094,11 @@ StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
|
|
|
7965
8094
|
* Here, we don't use 1000 (normal closure) because we don't want the
|
|
7966
8095
|
* SFU to clean up the resources associated with the current participant.
|
|
7967
8096
|
*/
|
|
7968
|
-
StreamSfuClient.DISPOSE_OLD_SOCKET =
|
|
8097
|
+
StreamSfuClient.DISPOSE_OLD_SOCKET = 4100;
|
|
8098
|
+
/**
|
|
8099
|
+
* The close code used when the client fails to join the call (on the SFU).
|
|
8100
|
+
*/
|
|
8101
|
+
StreamSfuClient.JOIN_FAILED = 4101;
|
|
7969
8102
|
|
|
7970
8103
|
/**
|
|
7971
8104
|
* Event handler that watched the delivery of `call.accepted`.
|
|
@@ -9479,21 +9612,30 @@ let getDisplayMediaExecId = 0;
|
|
|
9479
9612
|
const getScreenShareStream = async (options, tracer) => {
|
|
9480
9613
|
const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
|
|
9481
9614
|
try {
|
|
9482
|
-
|
|
9483
|
-
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
9484
|
-
video: true,
|
|
9485
|
-
audio: {
|
|
9486
|
-
channelCount: {
|
|
9487
|
-
ideal: 2,
|
|
9488
|
-
},
|
|
9489
|
-
echoCancellation: false,
|
|
9490
|
-
autoGainControl: false,
|
|
9491
|
-
noiseSuppression: false,
|
|
9492
|
-
},
|
|
9615
|
+
const constraints = {
|
|
9493
9616
|
// @ts-expect-error - not present in types yet
|
|
9494
9617
|
systemAudio: 'include',
|
|
9495
9618
|
...options,
|
|
9496
|
-
|
|
9619
|
+
video: typeof options?.video === 'boolean'
|
|
9620
|
+
? options.video // must be 'true'
|
|
9621
|
+
: {
|
|
9622
|
+
width: { max: 2560 },
|
|
9623
|
+
height: { max: 1440 },
|
|
9624
|
+
frameRate: { ideal: 30 },
|
|
9625
|
+
...options?.video,
|
|
9626
|
+
},
|
|
9627
|
+
audio: typeof options?.audio === 'boolean'
|
|
9628
|
+
? options.audio
|
|
9629
|
+
: {
|
|
9630
|
+
channelCount: { ideal: 2 },
|
|
9631
|
+
echoCancellation: false,
|
|
9632
|
+
autoGainControl: false,
|
|
9633
|
+
noiseSuppression: false,
|
|
9634
|
+
...options?.audio,
|
|
9635
|
+
},
|
|
9636
|
+
};
|
|
9637
|
+
tracer?.trace(tag, constraints);
|
|
9638
|
+
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
|
|
9497
9639
|
tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
9498
9640
|
return stream;
|
|
9499
9641
|
}
|
|
@@ -10841,11 +10983,18 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10841
10983
|
getDevices() {
|
|
10842
10984
|
return of([]); // there are no devices to be listed for Screen Share
|
|
10843
10985
|
}
|
|
10844
|
-
getStream(constraints) {
|
|
10986
|
+
async getStream(constraints) {
|
|
10845
10987
|
if (!this.state.audioEnabled) {
|
|
10846
10988
|
constraints.audio = false;
|
|
10847
10989
|
}
|
|
10848
|
-
|
|
10990
|
+
const stream = await getScreenShareStream(constraints, this.call.tracer);
|
|
10991
|
+
const [track] = stream.getVideoTracks();
|
|
10992
|
+
const { contentHint } = this.state.settings || {};
|
|
10993
|
+
if (typeof contentHint !== 'undefined' && track && 'contentHint' in track) {
|
|
10994
|
+
this.call.tracer.trace('navigator.mediaDevices.getDisplayMedia.contentHint', contentHint);
|
|
10995
|
+
track.contentHint = contentHint;
|
|
10996
|
+
}
|
|
10997
|
+
return stream;
|
|
10849
10998
|
}
|
|
10850
10999
|
async stopPublishStream() {
|
|
10851
11000
|
return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
|
|
@@ -11240,10 +11389,13 @@ class Call {
|
|
|
11240
11389
|
* Leave the call and stop the media streams that were published by the call.
|
|
11241
11390
|
*/
|
|
11242
11391
|
this.leave = async ({ reject, reason, message } = {}) => {
|
|
11392
|
+
if (this.state.callingState === CallingState.LEFT) {
|
|
11393
|
+
throw new Error('Cannot leave call that has already been left.');
|
|
11394
|
+
}
|
|
11243
11395
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
11244
11396
|
const callingState = this.state.callingState;
|
|
11245
11397
|
if (callingState === CallingState.LEFT) {
|
|
11246
|
-
|
|
11398
|
+
return;
|
|
11247
11399
|
}
|
|
11248
11400
|
if (callingState === CallingState.JOINING) {
|
|
11249
11401
|
const waitUntilCallJoined = () => {
|
|
@@ -11350,7 +11502,6 @@ class Call {
|
|
|
11350
11502
|
this.state.setMembers(response.members);
|
|
11351
11503
|
this.state.setOwnCapabilities(response.own_capabilities);
|
|
11352
11504
|
if (params?.ring) {
|
|
11353
|
-
// the call response can indicate where the call is still ringing or not
|
|
11354
11505
|
this.ringingSubject.next(true);
|
|
11355
11506
|
}
|
|
11356
11507
|
if (this.streamClient._hasConnectionID()) {
|
|
@@ -11372,7 +11523,6 @@ class Call {
|
|
|
11372
11523
|
this.state.setMembers(response.members);
|
|
11373
11524
|
this.state.setOwnCapabilities(response.own_capabilities);
|
|
11374
11525
|
if (data?.ring) {
|
|
11375
|
-
// the call response can indicate where the call is still ringing or not
|
|
11376
11526
|
this.ringingSubject.next(true);
|
|
11377
11527
|
}
|
|
11378
11528
|
if (this.streamClient._hasConnectionID()) {
|
|
@@ -11552,7 +11702,7 @@ class Call {
|
|
|
11552
11702
|
}
|
|
11553
11703
|
catch (error) {
|
|
11554
11704
|
this.logger('warn', 'Join SFU request failed', error);
|
|
11555
|
-
sfuClient.close(StreamSfuClient.
|
|
11705
|
+
sfuClient.close(StreamSfuClient.JOIN_FAILED, 'Join request failed, connection considered unhealthy');
|
|
11556
11706
|
// restore the previous call state if the join-flow fails
|
|
11557
11707
|
this.state.setCallingState(callingState);
|
|
11558
11708
|
throw error;
|
|
@@ -11695,7 +11845,7 @@ class Call {
|
|
|
11695
11845
|
}
|
|
11696
11846
|
if (this.publisher) {
|
|
11697
11847
|
this.publisher.setSfuClient(nextSfuClient);
|
|
11698
|
-
if (includePublisher) {
|
|
11848
|
+
if (includePublisher && this.publisher.isPublishing()) {
|
|
11699
11849
|
await this.publisher.restartIce();
|
|
11700
11850
|
}
|
|
11701
11851
|
}
|
|
@@ -11717,9 +11867,10 @@ class Call {
|
|
|
11717
11867
|
connectionConfig,
|
|
11718
11868
|
logTag: String(this.sfuClientTag),
|
|
11719
11869
|
enableTracing,
|
|
11720
|
-
|
|
11721
|
-
this.reconnect(
|
|
11722
|
-
|
|
11870
|
+
onReconnectionNeeded: (kind, reason) => {
|
|
11871
|
+
this.reconnect(kind, reason).catch((err) => {
|
|
11872
|
+
const message = `[Reconnect] Error reconnecting after a subscriber error: ${reason}`;
|
|
11873
|
+
this.logger('warn', message, err);
|
|
11723
11874
|
});
|
|
11724
11875
|
},
|
|
11725
11876
|
});
|
|
@@ -11738,9 +11889,10 @@ class Call {
|
|
|
11738
11889
|
publishOptions,
|
|
11739
11890
|
logTag: String(this.sfuClientTag),
|
|
11740
11891
|
enableTracing,
|
|
11741
|
-
|
|
11742
|
-
this.reconnect(
|
|
11743
|
-
|
|
11892
|
+
onReconnectionNeeded: (kind, reason) => {
|
|
11893
|
+
this.reconnect(kind, reason).catch((err) => {
|
|
11894
|
+
const message = `[Reconnect] Error reconnecting after a publisher error: ${reason}`;
|
|
11895
|
+
this.logger('warn', message, err);
|
|
11744
11896
|
});
|
|
11745
11897
|
},
|
|
11746
11898
|
});
|
|
@@ -11824,9 +11976,12 @@ class Call {
|
|
|
11824
11976
|
callingState === CallingState.LEFT)
|
|
11825
11977
|
return;
|
|
11826
11978
|
// normal close, no need to reconnect
|
|
11827
|
-
if (sfuClient.isLeaving || sfuClient.
|
|
11979
|
+
if (sfuClient.isLeaving || sfuClient.isClosingClean)
|
|
11828
11980
|
return;
|
|
11829
|
-
this.
|
|
11981
|
+
const strategy = this.publisher?.isHealthy() && this.subscriber?.isHealthy()
|
|
11982
|
+
? WebsocketReconnectStrategy.FAST
|
|
11983
|
+
: WebsocketReconnectStrategy.REJOIN;
|
|
11984
|
+
this.reconnect(strategy, reason).catch((err) => {
|
|
11830
11985
|
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
11831
11986
|
});
|
|
11832
11987
|
};
|
|
@@ -11847,10 +12002,12 @@ class Call {
|
|
|
11847
12002
|
const reconnectStartTime = Date.now();
|
|
11848
12003
|
this.reconnectStrategy = strategy;
|
|
11849
12004
|
this.reconnectReason = reason;
|
|
12005
|
+
let attempt = 0;
|
|
11850
12006
|
do {
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
12007
|
+
const reconnectingTime = Date.now() - reconnectStartTime;
|
|
12008
|
+
const shouldGiveUpReconnecting = this.disconnectionTimeoutSeconds > 0 &&
|
|
12009
|
+
reconnectingTime / 1000 > this.disconnectionTimeoutSeconds;
|
|
12010
|
+
if (shouldGiveUpReconnecting) {
|
|
11854
12011
|
this.logger('warn', '[Reconnect] Stopping reconnection attempts after reaching disconnection timeout');
|
|
11855
12012
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
11856
12013
|
return;
|
|
@@ -11859,7 +12016,7 @@ class Call {
|
|
|
11859
12016
|
if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
|
|
11860
12017
|
this.reconnectAttempts++;
|
|
11861
12018
|
}
|
|
11862
|
-
const
|
|
12019
|
+
const currentStrategy = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
11863
12020
|
try {
|
|
11864
12021
|
// wait until the network is available
|
|
11865
12022
|
await this.networkAvailableTask?.promise;
|
|
@@ -11867,7 +12024,7 @@ class Call {
|
|
|
11867
12024
|
switch (this.reconnectStrategy) {
|
|
11868
12025
|
case WebsocketReconnectStrategy.UNSPECIFIED:
|
|
11869
12026
|
case WebsocketReconnectStrategy.DISCONNECT:
|
|
11870
|
-
this.logger('debug', `[Reconnect] No-op strategy ${
|
|
12027
|
+
this.logger('debug', `[Reconnect] No-op strategy ${currentStrategy}`);
|
|
11871
12028
|
break;
|
|
11872
12029
|
case WebsocketReconnectStrategy.FAST:
|
|
11873
12030
|
await this.reconnectFast();
|
|
@@ -11886,7 +12043,7 @@ class Call {
|
|
|
11886
12043
|
}
|
|
11887
12044
|
catch (error) {
|
|
11888
12045
|
if (this.state.callingState === CallingState.OFFLINE) {
|
|
11889
|
-
this.logger('
|
|
12046
|
+
this.logger('debug', `[Reconnect] Can't reconnect while offline, stopping reconnection attempts`);
|
|
11890
12047
|
break;
|
|
11891
12048
|
// we don't need to handle the error if the call is offline
|
|
11892
12049
|
// network change event will trigger the reconnection
|
|
@@ -11896,9 +12053,24 @@ class Call {
|
|
|
11896
12053
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
11897
12054
|
return;
|
|
11898
12055
|
}
|
|
11899
|
-
this.logger('warn', `[Reconnect] ${current} (${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
|
|
11900
12056
|
await sleep(500);
|
|
11901
|
-
this.reconnectStrategy
|
|
12057
|
+
const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
12058
|
+
const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
|
|
12059
|
+
this.fastReconnectDeadlineSeconds;
|
|
12060
|
+
// don't immediately switch to the REJOIN strategy, but instead attempt
|
|
12061
|
+
// to reconnect with the FAST strategy for a few times before switching.
|
|
12062
|
+
// in some cases, we immediately switch to the REJOIN strategy.
|
|
12063
|
+
const shouldRejoin = mustPerformRejoin || // if we are past the fast reconnect deadline
|
|
12064
|
+
wasMigrating || // if we were migrating, but the migration failed
|
|
12065
|
+
attempt >= 3 || // after 3 failed attempts
|
|
12066
|
+
!(this.publisher?.isHealthy() ?? true) || // if the publisher is not healthy
|
|
12067
|
+
!(this.subscriber?.isHealthy() ?? true); // if the subscriber is not healthy
|
|
12068
|
+
attempt++;
|
|
12069
|
+
const nextStrategy = shouldRejoin
|
|
12070
|
+
? WebsocketReconnectStrategy.REJOIN
|
|
12071
|
+
: WebsocketReconnectStrategy.FAST;
|
|
12072
|
+
this.reconnectStrategy = nextStrategy;
|
|
12073
|
+
this.logger('info', `[Reconnect] ${currentStrategy} (${this.reconnectAttempts}) failed. Attempting with ${WebsocketReconnectStrategy[nextStrategy]}`, error);
|
|
11902
12074
|
}
|
|
11903
12075
|
} while (this.state.callingState !== CallingState.JOINED &&
|
|
11904
12076
|
this.state.callingState !== CallingState.RECONNECTING_FAILED &&
|
|
@@ -12913,6 +13085,38 @@ class Call {
|
|
|
12913
13085
|
|
|
12914
13086
|
var https = null;
|
|
12915
13087
|
|
|
13088
|
+
const APIErrorCodes = {
|
|
13089
|
+
[-1]: 'InternalSystemError',
|
|
13090
|
+
2: 'AccessKeyError',
|
|
13091
|
+
3: 'AuthenticationFailedError',
|
|
13092
|
+
4: 'InputError',
|
|
13093
|
+
5: 'AuthenticationError',
|
|
13094
|
+
6: 'DuplicateUsernameError',
|
|
13095
|
+
9: 'RateLimitError',
|
|
13096
|
+
16: 'DoesNotExistError',
|
|
13097
|
+
17: 'NotAllowedError',
|
|
13098
|
+
18: 'EventNotSupportedError',
|
|
13099
|
+
19: 'ChannelFeatureNotSupportedError',
|
|
13100
|
+
20: 'MessageTooLongError',
|
|
13101
|
+
21: 'MultipleNestingLevelError',
|
|
13102
|
+
22: 'PayloadTooBigError',
|
|
13103
|
+
23: 'RequestTimeoutError',
|
|
13104
|
+
24: 'MaxHeaderSizeExceededError',
|
|
13105
|
+
40: 'AuthErrorTokenExpired',
|
|
13106
|
+
41: 'AuthErrorTokenNotValidYet',
|
|
13107
|
+
42: 'AuthErrorTokenUsedBeforeIssuedAt',
|
|
13108
|
+
43: 'AuthErrorTokenSignatureInvalid',
|
|
13109
|
+
44: 'CustomCommandEndpointMissingError',
|
|
13110
|
+
45: 'CustomCommandEndpointCallError',
|
|
13111
|
+
46: 'ConnectionIDNotFoundError',
|
|
13112
|
+
60: 'CoolDownError',
|
|
13113
|
+
69: 'ErrWrongRegion',
|
|
13114
|
+
70: 'ErrQueryChannelPermissions',
|
|
13115
|
+
71: 'ErrTooManyConnections',
|
|
13116
|
+
73: 'MessageModerationFailedError',
|
|
13117
|
+
99: 'AppSuspendedError',
|
|
13118
|
+
};
|
|
13119
|
+
|
|
12916
13120
|
/**
|
|
12917
13121
|
* StableWSConnection - A WS connection that reconnects upon failure.
|
|
12918
13122
|
* - the browser will sometimes report that you're online or offline
|
|
@@ -13139,7 +13343,7 @@ class StableWSConnection {
|
|
|
13139
13343
|
message = error.message;
|
|
13140
13344
|
statusCode = error.StatusCode;
|
|
13141
13345
|
}
|
|
13142
|
-
const msg = `WS failed with code: ${code} and reason: ${message}`;
|
|
13346
|
+
const msg = `WS failed with code: ${code}: ${APIErrorCodes[code] || code} and reason: ${message}`;
|
|
13143
13347
|
this._log(msg, { event }, 'warn');
|
|
13144
13348
|
const error = new Error(msg);
|
|
13145
13349
|
error.code = code;
|
|
@@ -14017,7 +14221,7 @@ class StreamClient {
|
|
|
14017
14221
|
this.getUserAgent = () => {
|
|
14018
14222
|
if (!this.cachedUserAgent) {
|
|
14019
14223
|
const { clientAppIdentifier = {} } = this.options;
|
|
14020
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
14224
|
+
const { sdkName = 'js', sdkVersion = "1.25.0", ...extras } = clientAppIdentifier;
|
|
14021
14225
|
this.cachedUserAgent = [
|
|
14022
14226
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
14023
14227
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -14367,13 +14571,17 @@ class StreamVideoClient {
|
|
|
14367
14571
|
* @param type the type of the call.
|
|
14368
14572
|
* @param id the id of the call.
|
|
14369
14573
|
*/
|
|
14370
|
-
this.call = (type, id) => {
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14574
|
+
this.call = (type, id, options = {}) => {
|
|
14575
|
+
const call = options.reuseInstance
|
|
14576
|
+
? this.writeableStateStore.findCall(type, id)
|
|
14577
|
+
: undefined;
|
|
14578
|
+
return (call ??
|
|
14579
|
+
new Call({
|
|
14580
|
+
streamClient: this.streamClient,
|
|
14581
|
+
id: id,
|
|
14582
|
+
type: type,
|
|
14583
|
+
clientStore: this.writeableStateStore,
|
|
14584
|
+
}));
|
|
14377
14585
|
};
|
|
14378
14586
|
/**
|
|
14379
14587
|
* Creates a new guest user with the given data.
|