@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @generated by protobuf-ts 2.
|
|
1
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
2
2
|
// @generated from protobuf file "video/sfu/event/events.proto" (package "stream.video.sfu.event", syntax proto3)
|
|
3
3
|
// tslint:disable
|
|
4
4
|
import { MessageType } from '@protobuf-ts/runtime';
|
|
@@ -1048,7 +1048,7 @@ class ChangePublishOptions$Type extends MessageType<ChangePublishOptions> {
|
|
|
1048
1048
|
no: 1,
|
|
1049
1049
|
name: 'publish_options',
|
|
1050
1050
|
kind: 'message',
|
|
1051
|
-
repeat:
|
|
1051
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1052
1052
|
T: () => PublishOption,
|
|
1053
1053
|
},
|
|
1054
1054
|
{ no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
@@ -1089,7 +1089,7 @@ class PinsChanged$Type extends MessageType<PinsChanged> {
|
|
|
1089
1089
|
no: 1,
|
|
1090
1090
|
name: 'pins',
|
|
1091
1091
|
kind: 'message',
|
|
1092
|
-
repeat:
|
|
1092
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1093
1093
|
T: () => Pin,
|
|
1094
1094
|
},
|
|
1095
1095
|
]);
|
|
@@ -1332,14 +1332,14 @@ class JoinRequest$Type extends MessageType<JoinRequest> {
|
|
|
1332
1332
|
no: 9,
|
|
1333
1333
|
name: 'preferred_publish_options',
|
|
1334
1334
|
kind: 'message',
|
|
1335
|
-
repeat:
|
|
1335
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1336
1336
|
T: () => PublishOption,
|
|
1337
1337
|
},
|
|
1338
1338
|
{
|
|
1339
1339
|
no: 10,
|
|
1340
1340
|
name: 'preferred_subscribe_options',
|
|
1341
1341
|
kind: 'message',
|
|
1342
|
-
repeat:
|
|
1342
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1343
1343
|
T: () => SubscribeOption,
|
|
1344
1344
|
},
|
|
1345
1345
|
]);
|
|
@@ -1367,14 +1367,14 @@ class ReconnectDetails$Type extends MessageType<ReconnectDetails> {
|
|
|
1367
1367
|
no: 3,
|
|
1368
1368
|
name: 'announced_tracks',
|
|
1369
1369
|
kind: 'message',
|
|
1370
|
-
repeat:
|
|
1370
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1371
1371
|
T: () => TrackInfo,
|
|
1372
1372
|
},
|
|
1373
1373
|
{
|
|
1374
1374
|
no: 4,
|
|
1375
1375
|
name: 'subscriptions',
|
|
1376
1376
|
kind: 'message',
|
|
1377
|
-
repeat:
|
|
1377
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1378
1378
|
T: () => TrackSubscriptionDetails,
|
|
1379
1379
|
},
|
|
1380
1380
|
{
|
|
@@ -1417,14 +1417,14 @@ class Migration$Type extends MessageType<Migration> {
|
|
|
1417
1417
|
no: 2,
|
|
1418
1418
|
name: 'announced_tracks',
|
|
1419
1419
|
kind: 'message',
|
|
1420
|
-
repeat:
|
|
1420
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1421
1421
|
T: () => TrackInfo,
|
|
1422
1422
|
},
|
|
1423
1423
|
{
|
|
1424
1424
|
no: 3,
|
|
1425
1425
|
name: 'subscriptions',
|
|
1426
1426
|
kind: 'message',
|
|
1427
|
-
repeat:
|
|
1427
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1428
1428
|
T: () => TrackSubscriptionDetails,
|
|
1429
1429
|
},
|
|
1430
1430
|
]);
|
|
@@ -1450,7 +1450,7 @@ class JoinResponse$Type extends MessageType<JoinResponse> {
|
|
|
1450
1450
|
no: 4,
|
|
1451
1451
|
name: 'publish_options',
|
|
1452
1452
|
kind: 'message',
|
|
1453
|
-
repeat:
|
|
1453
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1454
1454
|
T: () => PublishOption,
|
|
1455
1455
|
},
|
|
1456
1456
|
]);
|
|
@@ -1532,7 +1532,7 @@ class ConnectionQualityChanged$Type extends MessageType<ConnectionQualityChanged
|
|
|
1532
1532
|
no: 1,
|
|
1533
1533
|
name: 'connection_quality_updates',
|
|
1534
1534
|
kind: 'message',
|
|
1535
|
-
repeat:
|
|
1535
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1536
1536
|
T: () => ConnectionQualityInfo,
|
|
1537
1537
|
},
|
|
1538
1538
|
]);
|
|
@@ -1601,7 +1601,7 @@ class AudioLevelChanged$Type extends MessageType<AudioLevelChanged> {
|
|
|
1601
1601
|
no: 1,
|
|
1602
1602
|
name: 'audio_levels',
|
|
1603
1603
|
kind: 'message',
|
|
1604
|
-
repeat:
|
|
1604
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1605
1605
|
T: () => AudioLevel,
|
|
1606
1606
|
},
|
|
1607
1607
|
]);
|
|
@@ -1681,7 +1681,7 @@ class VideoSender$Type extends MessageType<VideoSender> {
|
|
|
1681
1681
|
no: 3,
|
|
1682
1682
|
name: 'layers',
|
|
1683
1683
|
kind: 'message',
|
|
1684
|
-
repeat:
|
|
1684
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1685
1685
|
T: () => VideoLayerSetting,
|
|
1686
1686
|
},
|
|
1687
1687
|
{
|
|
@@ -1715,14 +1715,14 @@ class ChangePublishQuality$Type extends MessageType<ChangePublishQuality> {
|
|
|
1715
1715
|
no: 1,
|
|
1716
1716
|
name: 'audio_senders',
|
|
1717
1717
|
kind: 'message',
|
|
1718
|
-
repeat:
|
|
1718
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1719
1719
|
T: () => AudioSender,
|
|
1720
1720
|
},
|
|
1721
1721
|
{
|
|
1722
1722
|
no: 2,
|
|
1723
1723
|
name: 'video_senders',
|
|
1724
1724
|
kind: 'message',
|
|
1725
|
-
repeat:
|
|
1725
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1726
1726
|
T: () => VideoSender,
|
|
1727
1727
|
},
|
|
1728
1728
|
]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @generated by protobuf-ts 2.
|
|
1
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
2
2
|
// @generated from protobuf file "video/sfu/models/models.proto" (package "stream.video.sfu.models", syntax proto3)
|
|
3
3
|
// tslint:disable
|
|
4
4
|
import { MessageType } from '@protobuf-ts/runtime';
|
|
@@ -811,6 +811,10 @@ export enum ErrorCode {
|
|
|
811
811
|
* @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_MEDIA_TRANSPORT_FAILURE = 205;
|
|
812
812
|
*/
|
|
813
813
|
PARTICIPANT_MEDIA_TRANSPORT_FAILURE = 205,
|
|
814
|
+
/**
|
|
815
|
+
* @generated from protobuf enum value: ERROR_CODE_PARTICIPANT_SIGNAL_LOST = 206;
|
|
816
|
+
*/
|
|
817
|
+
PARTICIPANT_SIGNAL_LOST = 206,
|
|
814
818
|
/**
|
|
815
819
|
* @generated from protobuf enum value: ERROR_CODE_CALL_NOT_FOUND = 300;
|
|
816
820
|
*/
|
|
@@ -1084,7 +1088,7 @@ class CallState$Type extends MessageType<CallState> {
|
|
|
1084
1088
|
no: 1,
|
|
1085
1089
|
name: 'participants',
|
|
1086
1090
|
kind: 'message',
|
|
1087
|
-
repeat:
|
|
1091
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1088
1092
|
T: () => Participant,
|
|
1089
1093
|
},
|
|
1090
1094
|
{ no: 2, name: 'started_at', kind: 'message', T: () => Timestamp },
|
|
@@ -1098,7 +1102,7 @@ class CallState$Type extends MessageType<CallState> {
|
|
|
1098
1102
|
no: 4,
|
|
1099
1103
|
name: 'pins',
|
|
1100
1104
|
kind: 'message',
|
|
1101
|
-
repeat:
|
|
1105
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1102
1106
|
T: () => Pin,
|
|
1103
1107
|
},
|
|
1104
1108
|
]);
|
|
@@ -1276,7 +1280,7 @@ class SubscribeOption$Type extends MessageType<SubscribeOption> {
|
|
|
1276
1280
|
no: 2,
|
|
1277
1281
|
name: 'codecs',
|
|
1278
1282
|
kind: 'message',
|
|
1279
|
-
repeat:
|
|
1283
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1280
1284
|
T: () => Codec,
|
|
1281
1285
|
},
|
|
1282
1286
|
]);
|
|
@@ -1409,7 +1413,7 @@ class TrackInfo$Type extends MessageType<TrackInfo> {
|
|
|
1409
1413
|
no: 5,
|
|
1410
1414
|
name: 'layers',
|
|
1411
1415
|
kind: 'message',
|
|
1412
|
-
repeat:
|
|
1416
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1413
1417
|
T: () => VideoLayer,
|
|
1414
1418
|
},
|
|
1415
1419
|
{ no: 6, name: 'mid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @generated by protobuf-ts 2.
|
|
1
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
2
2
|
// @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3)
|
|
3
3
|
// tslint:disable
|
|
4
4
|
import type {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @generated by protobuf-ts 2.
|
|
1
|
+
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
2
2
|
// @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3)
|
|
3
3
|
// tslint:disable
|
|
4
4
|
import {
|
|
@@ -566,14 +566,14 @@ class SendStatsRequest$Type extends MessageType<SendStatsRequest> {
|
|
|
566
566
|
no: 16,
|
|
567
567
|
name: 'encode_stats',
|
|
568
568
|
kind: 'message',
|
|
569
|
-
repeat:
|
|
569
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
570
570
|
T: () => PerformanceStats,
|
|
571
571
|
},
|
|
572
572
|
{
|
|
573
573
|
no: 17,
|
|
574
574
|
name: 'decode_stats',
|
|
575
575
|
kind: 'message',
|
|
576
|
-
repeat:
|
|
576
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
577
577
|
T: () => PerformanceStats,
|
|
578
578
|
},
|
|
579
579
|
{
|
|
@@ -640,7 +640,7 @@ class UpdateMuteStatesRequest$Type extends MessageType<UpdateMuteStatesRequest>
|
|
|
640
640
|
no: 3,
|
|
641
641
|
name: 'mute_states',
|
|
642
642
|
kind: 'message',
|
|
643
|
-
repeat:
|
|
643
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
644
644
|
T: () => TrackMuteState,
|
|
645
645
|
},
|
|
646
646
|
]);
|
|
@@ -717,7 +717,7 @@ class UpdateSubscriptionsRequest$Type extends MessageType<UpdateSubscriptionsReq
|
|
|
717
717
|
no: 3,
|
|
718
718
|
name: 'tracks',
|
|
719
719
|
kind: 'message',
|
|
720
|
-
repeat:
|
|
720
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
721
721
|
T: () => TrackSubscriptionDetails,
|
|
722
722
|
},
|
|
723
723
|
]);
|
|
@@ -817,7 +817,7 @@ class SetPublisherRequest$Type extends MessageType<SetPublisherRequest> {
|
|
|
817
817
|
no: 3,
|
|
818
818
|
name: 'tracks',
|
|
819
819
|
kind: 'message',
|
|
820
|
-
repeat:
|
|
820
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
821
821
|
T: () => TrackInfo,
|
|
822
822
|
},
|
|
823
823
|
]);
|
|
@@ -5,20 +5,32 @@ import type {
|
|
|
5
5
|
} from '../coordinator/connection/types';
|
|
6
6
|
import { CallingState, CallState } from '../store';
|
|
7
7
|
import { createSafeAsyncSubscription } from '../store/rxUtils';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
ErrorCode,
|
|
10
|
+
PeerType,
|
|
11
|
+
TrackType,
|
|
12
|
+
WebsocketReconnectStrategy,
|
|
13
|
+
} from '../gen/video/sfu/models/models';
|
|
14
|
+
import { NegotiationError } from './NegotiationError';
|
|
9
15
|
import { StreamSfuClient } from '../StreamSfuClient';
|
|
10
16
|
import { AllSfuEvents, Dispatcher } from './Dispatcher';
|
|
11
17
|
import { withoutConcurrency } from '../helpers/concurrency';
|
|
12
18
|
import { StatsTracer, Tracer, traceRTCPeerConnection } from '../stats';
|
|
13
19
|
|
|
20
|
+
export type OnReconnectionNeeded = (
|
|
21
|
+
kind: WebsocketReconnectStrategy,
|
|
22
|
+
reason: string,
|
|
23
|
+
) => void;
|
|
24
|
+
|
|
14
25
|
export type BasePeerConnectionOpts = {
|
|
15
26
|
sfuClient: StreamSfuClient;
|
|
16
27
|
state: CallState;
|
|
17
28
|
connectionConfig?: RTCConfiguration;
|
|
18
29
|
dispatcher: Dispatcher;
|
|
19
|
-
|
|
30
|
+
onReconnectionNeeded?: OnReconnectionNeeded;
|
|
20
31
|
logTag: string;
|
|
21
32
|
enableTracing: boolean;
|
|
33
|
+
iceRestartDelay?: number;
|
|
22
34
|
};
|
|
23
35
|
|
|
24
36
|
/**
|
|
@@ -33,7 +45,9 @@ export abstract class BasePeerConnection {
|
|
|
33
45
|
protected readonly dispatcher: Dispatcher;
|
|
34
46
|
protected sfuClient: StreamSfuClient;
|
|
35
47
|
|
|
36
|
-
|
|
48
|
+
private onReconnectionNeeded?: OnReconnectionNeeded;
|
|
49
|
+
private readonly iceRestartDelay: number;
|
|
50
|
+
private iceRestartTimeout?: NodeJS.Timeout;
|
|
37
51
|
protected isIceRestarting = false;
|
|
38
52
|
private isDisposed = false;
|
|
39
53
|
|
|
@@ -41,6 +55,7 @@ export abstract class BasePeerConnection {
|
|
|
41
55
|
|
|
42
56
|
readonly tracer?: Tracer;
|
|
43
57
|
readonly stats: StatsTracer;
|
|
58
|
+
|
|
44
59
|
private readonly subscriptions: (() => void)[] = [];
|
|
45
60
|
private unsubscribeIceTrickle?: () => void;
|
|
46
61
|
protected readonly lock = Math.random().toString(36).slice(2);
|
|
@@ -55,33 +70,23 @@ export abstract class BasePeerConnection {
|
|
|
55
70
|
connectionConfig,
|
|
56
71
|
state,
|
|
57
72
|
dispatcher,
|
|
58
|
-
|
|
73
|
+
onReconnectionNeeded,
|
|
59
74
|
logTag,
|
|
60
75
|
enableTracing,
|
|
76
|
+
iceRestartDelay = 2500,
|
|
61
77
|
}: BasePeerConnectionOpts,
|
|
62
78
|
) {
|
|
63
79
|
this.peerType = peerType;
|
|
64
80
|
this.sfuClient = sfuClient;
|
|
65
81
|
this.state = state;
|
|
66
82
|
this.dispatcher = dispatcher;
|
|
67
|
-
this.
|
|
83
|
+
this.iceRestartDelay = iceRestartDelay;
|
|
84
|
+
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
68
85
|
this.logger = getLogger([
|
|
69
86
|
peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
|
|
70
87
|
logTag,
|
|
71
88
|
]);
|
|
72
|
-
this.pc =
|
|
73
|
-
this.pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
74
|
-
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
75
|
-
this.pc.addEventListener(
|
|
76
|
-
'iceconnectionstatechange',
|
|
77
|
-
this.onIceConnectionStateChange,
|
|
78
|
-
);
|
|
79
|
-
this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
80
|
-
this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
81
|
-
this.pc.addEventListener(
|
|
82
|
-
'connectionstatechange',
|
|
83
|
-
this.onConnectionStateChange,
|
|
84
|
-
);
|
|
89
|
+
this.pc = this.createPeerConnection(connectionConfig);
|
|
85
90
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
|
|
86
91
|
if (enableTracing) {
|
|
87
92
|
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
|
|
@@ -94,11 +99,27 @@ export abstract class BasePeerConnection {
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
private createPeerConnection = (connectionConfig?: RTCConfiguration) => {
|
|
103
|
+
const pc = new RTCPeerConnection(connectionConfig);
|
|
104
|
+
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
105
|
+
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
106
|
+
pc.addEventListener(
|
|
107
|
+
'iceconnectionstatechange',
|
|
108
|
+
this.onIceConnectionStateChange,
|
|
109
|
+
);
|
|
110
|
+
pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
111
|
+
pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
112
|
+
pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
113
|
+
return pc;
|
|
114
|
+
};
|
|
115
|
+
|
|
97
116
|
/**
|
|
98
117
|
* Disposes the `RTCPeerConnection` instance.
|
|
99
118
|
*/
|
|
100
119
|
dispose() {
|
|
101
|
-
this.
|
|
120
|
+
clearTimeout(this.iceRestartTimeout);
|
|
121
|
+
this.iceRestartTimeout = undefined;
|
|
122
|
+
this.onReconnectionNeeded = undefined;
|
|
102
123
|
this.isDisposed = true;
|
|
103
124
|
this.detachEventHandlers();
|
|
104
125
|
this.pc.close();
|
|
@@ -109,17 +130,15 @@ export abstract class BasePeerConnection {
|
|
|
109
130
|
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
110
131
|
*/
|
|
111
132
|
detachEventHandlers() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
const pc = this.pc;
|
|
134
|
+
pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
135
|
+
pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
136
|
+
pc.removeEventListener('signalingstatechange', this.onSignalingChange);
|
|
137
|
+
pc.removeEventListener(
|
|
116
138
|
'iceconnectionstatechange',
|
|
117
139
|
this.onIceConnectionStateChange,
|
|
118
140
|
);
|
|
119
|
-
|
|
120
|
-
'icegatheringstatechange',
|
|
121
|
-
this.onIceGatherChange,
|
|
122
|
-
);
|
|
141
|
+
pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
123
142
|
this.unsubscribeIceTrickle?.();
|
|
124
143
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
125
144
|
}
|
|
@@ -129,6 +148,24 @@ export abstract class BasePeerConnection {
|
|
|
129
148
|
*/
|
|
130
149
|
protected abstract restartIce(): Promise<void>;
|
|
131
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Attempts to restart ICE on the `RTCPeerConnection`.
|
|
153
|
+
* This method intentionally doesn't await the `restartIce()` method,
|
|
154
|
+
* allowing it to run in the background and handle any errors that may occur.
|
|
155
|
+
*/
|
|
156
|
+
protected tryRestartIce = () => {
|
|
157
|
+
this.restartIce().catch((e) => {
|
|
158
|
+
const reason = 'restartICE() failed, initiating reconnect';
|
|
159
|
+
this.logger('error', reason, e);
|
|
160
|
+
const strategy =
|
|
161
|
+
e instanceof NegotiationError &&
|
|
162
|
+
e.error.code === ErrorCode.PARTICIPANT_SIGNAL_LOST
|
|
163
|
+
? WebsocketReconnectStrategy.FAST
|
|
164
|
+
: WebsocketReconnectStrategy.REJOIN;
|
|
165
|
+
this.onReconnectionNeeded?.(strategy, reason);
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
132
169
|
/**
|
|
133
170
|
* Handles events synchronously.
|
|
134
171
|
* Consecutive events are queued and executed one after the other.
|
|
@@ -194,6 +231,22 @@ export abstract class BasePeerConnection {
|
|
|
194
231
|
return this.trackIdToTrackType.get(trackId);
|
|
195
232
|
};
|
|
196
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Checks if the `RTCPeerConnection` is healthy.
|
|
236
|
+
* It checks the ICE connection state and the peer connection state.
|
|
237
|
+
* If either state is `failed`, `disconnected`, or `closed`,
|
|
238
|
+
* it returns `false`, otherwise it returns `true`.
|
|
239
|
+
*/
|
|
240
|
+
isHealthy = () => {
|
|
241
|
+
const failedStates = new Set<
|
|
242
|
+
RTCIceConnectionState | RTCPeerConnectionState
|
|
243
|
+
>(['failed', 'closed']);
|
|
244
|
+
|
|
245
|
+
const iceState = this.pc.iceConnectionState;
|
|
246
|
+
const connectionState = this.pc.connectionState;
|
|
247
|
+
return !failedStates.has(iceState) && !failedStates.has(connectionState);
|
|
248
|
+
};
|
|
249
|
+
|
|
197
250
|
/**
|
|
198
251
|
* Handles the ICECandidate event and
|
|
199
252
|
* Initiates an ICE Trickle process with the SFU.
|
|
@@ -234,8 +287,7 @@ export abstract class BasePeerConnection {
|
|
|
234
287
|
private onConnectionStateChange = async () => {
|
|
235
288
|
const state = this.pc.connectionState;
|
|
236
289
|
this.logger('debug', `Connection state changed`, state);
|
|
237
|
-
if (
|
|
238
|
-
if (state === 'connected' || state === 'failed') {
|
|
290
|
+
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
239
291
|
try {
|
|
240
292
|
const stats = await this.stats.get();
|
|
241
293
|
this.tracer.trace('getstats', stats.delta);
|
|
@@ -243,6 +295,17 @@ export abstract class BasePeerConnection {
|
|
|
243
295
|
this.tracer.trace('getstatsOnFailure', (err as Error).toString());
|
|
244
296
|
}
|
|
245
297
|
}
|
|
298
|
+
|
|
299
|
+
// we can't recover from a failed connection state (contrary to ICE)
|
|
300
|
+
if (state === 'failed') {
|
|
301
|
+
this.onReconnectionNeeded?.(
|
|
302
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
303
|
+
'Connection failed',
|
|
304
|
+
);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.handleConnectionStateUpdate(state);
|
|
246
309
|
};
|
|
247
310
|
|
|
248
311
|
/**
|
|
@@ -251,22 +314,47 @@ export abstract class BasePeerConnection {
|
|
|
251
314
|
private onIceConnectionStateChange = () => {
|
|
252
315
|
const state = this.pc.iceConnectionState;
|
|
253
316
|
this.logger('debug', `ICE connection state changed`, state);
|
|
317
|
+
this.handleConnectionStateUpdate(state);
|
|
318
|
+
};
|
|
254
319
|
|
|
255
|
-
|
|
256
|
-
|
|
320
|
+
private handleConnectionStateUpdate = (
|
|
321
|
+
state: RTCIceConnectionState | RTCPeerConnectionState,
|
|
322
|
+
) => {
|
|
323
|
+
const { callingState } = this.state;
|
|
324
|
+
if (callingState === CallingState.OFFLINE) return;
|
|
325
|
+
if (callingState === CallingState.RECONNECTING) return;
|
|
257
326
|
|
|
258
327
|
// do nothing when ICE is restarting
|
|
259
328
|
if (this.isIceRestarting) return;
|
|
260
329
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
330
|
+
switch (state) {
|
|
331
|
+
case 'failed':
|
|
332
|
+
// in the `failed` state, we try to restart ICE immediately
|
|
333
|
+
this.logger('info', 'restartICE due to failed connection');
|
|
334
|
+
this.tryRestartIce();
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
case 'disconnected':
|
|
338
|
+
// in the `disconnected` state, we schedule a restartICE() after a delay
|
|
339
|
+
// as the browser might recover the connection in the meantime
|
|
340
|
+
this.logger('info', 'disconnected connection, scheduling restartICE');
|
|
341
|
+
clearTimeout(this.iceRestartTimeout);
|
|
342
|
+
this.iceRestartTimeout = setTimeout(() => {
|
|
343
|
+
const currentState = this.pc.iceConnectionState;
|
|
344
|
+
if (currentState === 'disconnected' || currentState === 'failed') {
|
|
345
|
+
this.tryRestartIce();
|
|
346
|
+
}
|
|
347
|
+
}, this.iceRestartDelay);
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case 'connected':
|
|
351
|
+
// in the `connected` state, we clear the ice restart timeout if it exists
|
|
352
|
+
if (this.iceRestartTimeout) {
|
|
353
|
+
this.logger('info', 'connected connection, canceling restartICE');
|
|
354
|
+
clearTimeout(this.iceRestartTimeout);
|
|
355
|
+
this.iceRestartTimeout = undefined;
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
270
358
|
}
|
|
271
359
|
};
|
|
272
360
|
|
|
@@ -275,12 +363,10 @@ export abstract class BasePeerConnection {
|
|
|
275
363
|
*/
|
|
276
364
|
private onIceCandidateError = (e: Event) => {
|
|
277
365
|
const errorMessage =
|
|
278
|
-
e instanceof RTCPeerConnectionIceErrorEvent
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
283
|
-
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
366
|
+
e instanceof RTCPeerConnectionIceErrorEvent
|
|
367
|
+
? `${e.errorCode}: ${e.errorText}`
|
|
368
|
+
: e;
|
|
369
|
+
this.logger('debug', 'ICE Candidate error', errorMessage);
|
|
284
370
|
};
|
|
285
371
|
|
|
286
372
|
/**
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Error as SfuError } from '../gen/video/sfu/models/models';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NegotiationError is thrown when there is an error during the negotiation process.
|
|
5
|
+
* It extends the built-in Error class and includes an SfuError object for more details.
|
|
6
|
+
*/
|
|
7
|
+
export class NegotiationError extends Error {
|
|
8
|
+
/**
|
|
9
|
+
* The SfuError object that contains details about the error.
|
|
10
|
+
*/
|
|
11
|
+
error: SfuError;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates an instance of NegotiationError.
|
|
15
|
+
*/
|
|
16
|
+
constructor(error: SfuError) {
|
|
17
|
+
super(error.message);
|
|
18
|
+
this.name = 'NegotiationError';
|
|
19
|
+
this.error = error;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
BasePeerConnection,
|
|
3
3
|
BasePeerConnectionOpts,
|
|
4
4
|
} from './BasePeerConnection';
|
|
5
|
+
import { NegotiationError } from './NegotiationError';
|
|
5
6
|
import { TransceiverCache } from './TransceiverCache';
|
|
6
7
|
import {
|
|
7
8
|
PeerType,
|
|
@@ -44,11 +45,7 @@ export class Publisher extends BasePeerConnection {
|
|
|
44
45
|
|
|
45
46
|
this.on('iceRestart', (iceRestart) => {
|
|
46
47
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED) return;
|
|
47
|
-
this.
|
|
48
|
-
const reason = `ICE restart failed`;
|
|
49
|
-
this.logger('warn', reason, err);
|
|
50
|
-
this.onUnrecoverableError?.(`${reason}: ${err}`);
|
|
51
|
-
});
|
|
48
|
+
this.tryRestartIce();
|
|
52
49
|
});
|
|
53
50
|
|
|
54
51
|
this.on('changePublishQuality', async (event) => {
|
|
@@ -189,11 +186,11 @@ export class Publisher extends BasePeerConnection {
|
|
|
189
186
|
/**
|
|
190
187
|
* Returns true if the given track type is currently being published to the SFU.
|
|
191
188
|
*
|
|
192
|
-
* @param trackType the track type to check.
|
|
189
|
+
* @param trackType the track type to check. If omitted, checks if any track is being published.
|
|
193
190
|
*/
|
|
194
|
-
isPublishing = (trackType
|
|
191
|
+
isPublishing = (trackType?: TrackType): boolean => {
|
|
195
192
|
for (const item of this.transceiverCache.items()) {
|
|
196
|
-
if (item.publishOption.trackType !== trackType) continue;
|
|
193
|
+
if (trackType && item.publishOption.trackType !== trackType) continue;
|
|
197
194
|
|
|
198
195
|
const track = item.transceiver.sender.track;
|
|
199
196
|
if (!track) continue;
|
|
@@ -338,10 +335,16 @@ export class Publisher extends BasePeerConnection {
|
|
|
338
335
|
|
|
339
336
|
const { sdp = '' } = offer;
|
|
340
337
|
const { response } = await this.sfuClient.setPublisher({ sdp, tracks });
|
|
341
|
-
if (response.error) throw new
|
|
338
|
+
if (response.error) throw new NegotiationError(response.error);
|
|
342
339
|
|
|
343
340
|
const { sdp: answerSdp } = response;
|
|
344
341
|
await this.pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
|
|
342
|
+
} catch (err) {
|
|
343
|
+
// negotiation failed, rollback to the previous state
|
|
344
|
+
if (this.pc.signalingState === 'have-local-offer') {
|
|
345
|
+
await this.pc.setLocalDescription({ type: 'rollback' });
|
|
346
|
+
}
|
|
347
|
+
throw err;
|
|
345
348
|
} finally {
|
|
346
349
|
this.isIceRestarting = false;
|
|
347
350
|
}
|
package/src/rtc/Subscriber.ts
CHANGED
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
BasePeerConnection,
|
|
3
3
|
BasePeerConnectionOpts,
|
|
4
4
|
} from './BasePeerConnection';
|
|
5
|
+
import { NegotiationError } from './NegotiationError';
|
|
5
6
|
import { PeerType } from '../gen/video/sfu/models/models';
|
|
6
7
|
import { SubscriberOffer } from '../gen/video/sfu/event/events';
|
|
7
8
|
import { toTrackType, trackTypeToParticipantStreamKey } from './helpers/tracks';
|
|
9
|
+
import { enableStereo } from './helpers/sdp';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
@@ -54,11 +56,12 @@ export class Subscriber extends BasePeerConnection {
|
|
|
54
56
|
return;
|
|
55
57
|
}
|
|
56
58
|
const previousIsIceRestarting = this.isIceRestarting;
|
|
59
|
+
this.isIceRestarting = true;
|
|
57
60
|
try {
|
|
58
|
-
|
|
59
|
-
await this.sfuClient.iceRestart({
|
|
61
|
+
const { response } = await this.sfuClient.iceRestart({
|
|
60
62
|
peerType: PeerType.SUBSCRIBER,
|
|
61
63
|
});
|
|
64
|
+
if (response.error) throw new NegotiationError(response.error);
|
|
62
65
|
} catch (e) {
|
|
63
66
|
// restore the previous state, as our intent for restarting ICE failed
|
|
64
67
|
this.isIceRestarting = previousIsIceRestarting;
|
|
@@ -154,6 +157,9 @@ export class Subscriber extends BasePeerConnection {
|
|
|
154
157
|
this.addTrickledIceCandidates();
|
|
155
158
|
|
|
156
159
|
const answer = await this.pc.createAnswer();
|
|
160
|
+
if (answer.sdp) {
|
|
161
|
+
answer.sdp = enableStereo(subscriberOffer.sdp, answer.sdp);
|
|
162
|
+
}
|
|
157
163
|
await this.pc.setLocalDescription(answer);
|
|
158
164
|
|
|
159
165
|
await this.sfuClient.sendAnswer({
|