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