@stream-io/video-client 1.13.1 → 1.15.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 +1704 -1762
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1706 -1780
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1704 -1762
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +61 -30
- package/dist/src/StreamSfuClient.d.ts +4 -5
- package/dist/src/devices/CameraManager.d.ts +5 -8
- package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
- package/dist/src/devices/MicrophoneManager.d.ts +7 -2
- package/dist/src/devices/ScreenShareManager.d.ts +1 -2
- package/dist/src/gen/coordinator/index.d.ts +904 -515
- package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
- package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/permissions/PermissionsContext.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
- package/dist/src/rtc/Dispatcher.d.ts +0 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
- package/dist/src/rtc/Publisher.d.ts +32 -86
- package/dist/src/rtc/Subscriber.d.ts +4 -56
- package/dist/src/rtc/TransceiverCache.d.ts +55 -0
- package/dist/src/rtc/codecs.d.ts +1 -15
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/rtc/helpers/tracks.d.ts +1 -0
- package/dist/src/rtc/index.d.ts +3 -0
- package/dist/src/rtc/videoLayers.d.ts +11 -25
- package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
- package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
- package/dist/src/stats/index.d.ts +1 -1
- package/dist/src/stats/types.d.ts +8 -0
- package/dist/src/store/CallState.d.ts +47 -5
- package/dist/src/store/rxUtils.d.ts +15 -1
- package/dist/src/types.d.ts +26 -22
- package/package.json +1 -1
- package/src/Call.ts +310 -271
- package/src/StreamSfuClient.ts +9 -14
- package/src/StreamVideoClient.ts +1 -1
- package/src/__tests__/Call.publishing.test.ts +306 -0
- package/src/devices/CameraManager.ts +33 -16
- package/src/devices/InputMediaDeviceManager.ts +36 -27
- package/src/devices/MicrophoneManager.ts +29 -8
- package/src/devices/ScreenShareManager.ts +6 -8
- package/src/devices/__tests__/CameraManager.test.ts +111 -14
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
- package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
- package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
- package/src/devices/__tests__/mocks.ts +1 -0
- package/src/events/__tests__/internal.test.ts +132 -0
- package/src/events/__tests__/mutes.test.ts +0 -3
- package/src/events/__tests__/speaker.test.ts +92 -0
- package/src/events/participant.ts +3 -4
- package/src/gen/coordinator/index.ts +902 -514
- package/src/gen/video/sfu/event/events.ts +91 -30
- package/src/gen/video/sfu/models/models.ts +105 -13
- package/src/helpers/array.ts +14 -0
- package/src/permissions/PermissionsContext.ts +22 -0
- package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
- package/src/rpc/__tests__/createClient.test.ts +38 -0
- package/src/rpc/createClient.ts +11 -5
- package/src/rtc/BasePeerConnection.ts +240 -0
- package/src/rtc/Dispatcher.ts +0 -9
- package/src/rtc/IceTrickleBuffer.ts +24 -4
- package/src/rtc/Publisher.ts +210 -528
- package/src/rtc/Subscriber.ts +26 -200
- package/src/rtc/TransceiverCache.ts +120 -0
- package/src/rtc/__tests__/Publisher.test.ts +407 -210
- package/src/rtc/__tests__/Subscriber.test.ts +88 -36
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
- package/src/rtc/__tests__/videoLayers.test.ts +161 -54
- package/src/rtc/codecs.ts +1 -131
- package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
- package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
- package/src/rtc/helpers/sdp.ts +30 -0
- package/src/rtc/helpers/tracks.ts +3 -0
- package/src/rtc/index.ts +4 -0
- package/src/rtc/videoLayers.ts +68 -76
- package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
- package/src/stats/SfuStatsReporter.ts +31 -3
- package/src/stats/index.ts +1 -1
- package/src/stats/types.ts +12 -0
- package/src/store/CallState.ts +115 -5
- package/src/store/__tests__/CallState.test.ts +101 -0
- package/src/store/rxUtils.ts +23 -1
- package/src/types.ts +27 -22
- package/dist/src/helpers/sdp-munging.d.ts +0 -24
- package/dist/src/rtc/bitrateLookup.d.ts +0 -2
- package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
- package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
- package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
- package/src/helpers/sdp-munging.ts +0 -265
- package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
- package/src/rtc/__tests__/codecs.test.ts +0 -145
- package/src/rtc/bitrateLookup.ts +0 -61
- package/src/rtc/helpers/iceCandidate.ts +0 -16
- /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
- /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
package/dist/index.cjs.js
CHANGED
|
@@ -5,30 +5,11 @@ var runtime = require('@protobuf-ts/runtime');
|
|
|
5
5
|
var runtimeRpc = require('@protobuf-ts/runtime-rpc');
|
|
6
6
|
var axios = require('axios');
|
|
7
7
|
var twirpTransport = require('@protobuf-ts/twirp-transport');
|
|
8
|
-
var uaParserJs = require('ua-parser-js');
|
|
9
8
|
var rxjs = require('rxjs');
|
|
10
|
-
var
|
|
9
|
+
var sdpTransform = require('sdp-transform');
|
|
10
|
+
var uaParserJs = require('ua-parser-js');
|
|
11
11
|
var https = require('https');
|
|
12
12
|
|
|
13
|
-
function _interopNamespaceDefault(e) {
|
|
14
|
-
var n = Object.create(null);
|
|
15
|
-
if (e) {
|
|
16
|
-
Object.keys(e).forEach(function (k) {
|
|
17
|
-
if (k !== 'default') {
|
|
18
|
-
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
-
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
-
enumerable: true,
|
|
21
|
-
get: function () { return e[k]; }
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
n.default = e;
|
|
27
|
-
return Object.freeze(n);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
var SDP__namespace = /*#__PURE__*/_interopNamespaceDefault(SDP);
|
|
31
|
-
|
|
32
13
|
/* tslint:disable */
|
|
33
14
|
/* eslint-disable */
|
|
34
15
|
/**
|
|
@@ -77,6 +58,48 @@ const ChannelConfigWithInfoBlocklistBehaviorEnum = {
|
|
|
77
58
|
BLOCK: 'block',
|
|
78
59
|
SHADOW_BLOCK: 'shadow_block',
|
|
79
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* All possibility of string to use
|
|
63
|
+
* @export
|
|
64
|
+
*/
|
|
65
|
+
const ChannelOwnCapability = {
|
|
66
|
+
BAN_CHANNEL_MEMBERS: 'ban-channel-members',
|
|
67
|
+
CAST_POLL_VOTE: 'cast-poll-vote',
|
|
68
|
+
CONNECT_EVENTS: 'connect-events',
|
|
69
|
+
CREATE_ATTACHMENT: 'create-attachment',
|
|
70
|
+
CREATE_CALL: 'create-call',
|
|
71
|
+
DELETE_ANY_MESSAGE: 'delete-any-message',
|
|
72
|
+
DELETE_CHANNEL: 'delete-channel',
|
|
73
|
+
DELETE_OWN_MESSAGE: 'delete-own-message',
|
|
74
|
+
FLAG_MESSAGE: 'flag-message',
|
|
75
|
+
FREEZE_CHANNEL: 'freeze-channel',
|
|
76
|
+
JOIN_CALL: 'join-call',
|
|
77
|
+
JOIN_CHANNEL: 'join-channel',
|
|
78
|
+
LEAVE_CHANNEL: 'leave-channel',
|
|
79
|
+
MUTE_CHANNEL: 'mute-channel',
|
|
80
|
+
PIN_MESSAGE: 'pin-message',
|
|
81
|
+
QUERY_POLL_VOTES: 'query-poll-votes',
|
|
82
|
+
QUOTE_MESSAGE: 'quote-message',
|
|
83
|
+
READ_EVENTS: 'read-events',
|
|
84
|
+
SEARCH_MESSAGES: 'search-messages',
|
|
85
|
+
SEND_CUSTOM_EVENTS: 'send-custom-events',
|
|
86
|
+
SEND_LINKS: 'send-links',
|
|
87
|
+
SEND_MESSAGE: 'send-message',
|
|
88
|
+
SEND_POLL: 'send-poll',
|
|
89
|
+
SEND_REACTION: 'send-reaction',
|
|
90
|
+
SEND_REPLY: 'send-reply',
|
|
91
|
+
SEND_TYPING_EVENTS: 'send-typing-events',
|
|
92
|
+
SET_CHANNEL_COOLDOWN: 'set-channel-cooldown',
|
|
93
|
+
SKIP_SLOW_MODE: 'skip-slow-mode',
|
|
94
|
+
SLOW_MODE: 'slow-mode',
|
|
95
|
+
TYPING_EVENTS: 'typing-events',
|
|
96
|
+
UPDATE_ANY_MESSAGE: 'update-any-message',
|
|
97
|
+
UPDATE_CHANNEL: 'update-channel',
|
|
98
|
+
UPDATE_CHANNEL_MEMBERS: 'update-channel-members',
|
|
99
|
+
UPDATE_OWN_MESSAGE: 'update-own-message',
|
|
100
|
+
UPDATE_THREAD: 'update-thread',
|
|
101
|
+
UPLOAD_FILE: 'upload-file',
|
|
102
|
+
};
|
|
80
103
|
/**
|
|
81
104
|
* @export
|
|
82
105
|
*/
|
|
@@ -116,9 +139,11 @@ const OwnCapability = {
|
|
|
116
139
|
SEND_AUDIO: 'send-audio',
|
|
117
140
|
SEND_VIDEO: 'send-video',
|
|
118
141
|
START_BROADCAST_CALL: 'start-broadcast-call',
|
|
142
|
+
START_CLOSED_CAPTIONS_CALL: 'start-closed-captions-call',
|
|
119
143
|
START_RECORD_CALL: 'start-record-call',
|
|
120
144
|
START_TRANSCRIPTION_CALL: 'start-transcription-call',
|
|
121
145
|
STOP_BROADCAST_CALL: 'stop-broadcast-call',
|
|
146
|
+
STOP_CLOSED_CAPTIONS_CALL: 'stop-closed-captions-call',
|
|
122
147
|
STOP_RECORD_CALL: 'stop-record-call',
|
|
123
148
|
STOP_TRANSCRIPTION_CALL: 'stop-transcription-call',
|
|
124
149
|
UPDATE_CALL: 'update-call',
|
|
@@ -149,6 +174,14 @@ const RecordSettingsRequestQualityEnum = {
|
|
|
149
174
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
150
175
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
151
176
|
};
|
|
177
|
+
/**
|
|
178
|
+
* @export
|
|
179
|
+
*/
|
|
180
|
+
const TranscriptionSettingsRequestClosedCaptionModeEnum = {
|
|
181
|
+
AVAILABLE: 'available',
|
|
182
|
+
DISABLED: 'disabled',
|
|
183
|
+
AUTO_ON: 'auto-on',
|
|
184
|
+
};
|
|
152
185
|
/**
|
|
153
186
|
* @export
|
|
154
187
|
*/
|
|
@@ -157,6 +190,14 @@ const TranscriptionSettingsRequestModeEnum = {
|
|
|
157
190
|
DISABLED: 'disabled',
|
|
158
191
|
AUTO_ON: 'auto-on',
|
|
159
192
|
};
|
|
193
|
+
/**
|
|
194
|
+
* @export
|
|
195
|
+
*/
|
|
196
|
+
const TranscriptionSettingsResponseClosedCaptionModeEnum = {
|
|
197
|
+
AVAILABLE: 'available',
|
|
198
|
+
DISABLED: 'disabled',
|
|
199
|
+
AUTO_ON: 'auto-on',
|
|
200
|
+
};
|
|
160
201
|
/**
|
|
161
202
|
* @export
|
|
162
203
|
*/
|
|
@@ -1185,23 +1226,33 @@ class VideoLayer$Type extends runtime.MessageType {
|
|
|
1185
1226
|
*/
|
|
1186
1227
|
const VideoLayer = new VideoLayer$Type();
|
|
1187
1228
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1188
|
-
class
|
|
1229
|
+
class SubscribeOption$Type extends runtime.MessageType {
|
|
1189
1230
|
constructor() {
|
|
1190
|
-
super('stream.video.sfu.models.
|
|
1231
|
+
super('stream.video.sfu.models.SubscribeOption', [
|
|
1191
1232
|
{
|
|
1192
1233
|
no: 1,
|
|
1234
|
+
name: 'track_type',
|
|
1235
|
+
kind: 'enum',
|
|
1236
|
+
T: () => [
|
|
1237
|
+
'stream.video.sfu.models.TrackType',
|
|
1238
|
+
TrackType,
|
|
1239
|
+
'TRACK_TYPE_',
|
|
1240
|
+
],
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
no: 2,
|
|
1193
1244
|
name: 'codecs',
|
|
1194
1245
|
kind: 'message',
|
|
1195
1246
|
repeat: 1 /*RepeatType.PACKED*/,
|
|
1196
|
-
T: () =>
|
|
1247
|
+
T: () => Codec,
|
|
1197
1248
|
},
|
|
1198
1249
|
]);
|
|
1199
1250
|
}
|
|
1200
1251
|
}
|
|
1201
1252
|
/**
|
|
1202
|
-
* @generated MessageType for protobuf message stream.video.sfu.models.
|
|
1253
|
+
* @generated MessageType for protobuf message stream.video.sfu.models.SubscribeOption
|
|
1203
1254
|
*/
|
|
1204
|
-
const
|
|
1255
|
+
const SubscribeOption = new SubscribeOption$Type();
|
|
1205
1256
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1206
1257
|
class PublishOption$Type extends runtime.MessageType {
|
|
1207
1258
|
constructor() {
|
|
@@ -1231,6 +1282,13 @@ class PublishOption$Type extends runtime.MessageType {
|
|
|
1231
1282
|
kind: 'scalar',
|
|
1232
1283
|
T: 5 /*ScalarType.INT32*/,
|
|
1233
1284
|
},
|
|
1285
|
+
{
|
|
1286
|
+
no: 7,
|
|
1287
|
+
name: 'video_dimension',
|
|
1288
|
+
kind: 'message',
|
|
1289
|
+
T: () => VideoDimension,
|
|
1290
|
+
},
|
|
1291
|
+
{ no: 8, name: 'id', kind: 'scalar', T: 5 /*ScalarType.INT32*/ },
|
|
1234
1292
|
]);
|
|
1235
1293
|
}
|
|
1236
1294
|
}
|
|
@@ -1243,7 +1301,7 @@ class Codec$Type extends runtime.MessageType {
|
|
|
1243
1301
|
constructor() {
|
|
1244
1302
|
super('stream.video.sfu.models.Codec', [
|
|
1245
1303
|
{
|
|
1246
|
-
no:
|
|
1304
|
+
no: 16,
|
|
1247
1305
|
name: 'payload_type',
|
|
1248
1306
|
kind: 'scalar',
|
|
1249
1307
|
T: 13 /*ScalarType.UINT32*/,
|
|
@@ -1256,7 +1314,7 @@ class Codec$Type extends runtime.MessageType {
|
|
|
1256
1314
|
T: 13 /*ScalarType.UINT32*/,
|
|
1257
1315
|
},
|
|
1258
1316
|
{
|
|
1259
|
-
no:
|
|
1317
|
+
no: 15,
|
|
1260
1318
|
name: 'encoding_parameters',
|
|
1261
1319
|
kind: 'scalar',
|
|
1262
1320
|
T: 9 /*ScalarType.STRING*/,
|
|
@@ -1320,6 +1378,13 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1320
1378
|
{ no: 8, name: 'stereo', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1321
1379
|
{ no: 9, name: 'red', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1322
1380
|
{ no: 10, name: 'muted', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1381
|
+
{ no: 11, name: 'codec', kind: 'message', T: () => Codec },
|
|
1382
|
+
{
|
|
1383
|
+
no: 12,
|
|
1384
|
+
name: 'publish_option_id',
|
|
1385
|
+
kind: 'scalar',
|
|
1386
|
+
T: 5 /*ScalarType.INT32*/,
|
|
1387
|
+
},
|
|
1323
1388
|
]);
|
|
1324
1389
|
}
|
|
1325
1390
|
}
|
|
@@ -1593,10 +1658,10 @@ var models = /*#__PURE__*/Object.freeze({
|
|
|
1593
1658
|
get PeerType () { return PeerType; },
|
|
1594
1659
|
Pin: Pin,
|
|
1595
1660
|
PublishOption: PublishOption,
|
|
1596
|
-
PublishOptions: PublishOptions,
|
|
1597
1661
|
Sdk: Sdk,
|
|
1598
1662
|
get SdkType () { return SdkType; },
|
|
1599
1663
|
StreamQuality: StreamQuality,
|
|
1664
|
+
SubscribeOption: SubscribeOption,
|
|
1600
1665
|
TrackInfo: TrackInfo,
|
|
1601
1666
|
get TrackType () { return TrackType; },
|
|
1602
1667
|
get TrackUnpublishReason () { return TrackUnpublishReason; },
|
|
@@ -2226,13 +2291,6 @@ class SfuEvent$Type extends runtime.MessageType {
|
|
|
2226
2291
|
oneof: 'eventPayload',
|
|
2227
2292
|
T: () => ParticipantMigrationComplete,
|
|
2228
2293
|
},
|
|
2229
|
-
{
|
|
2230
|
-
no: 26,
|
|
2231
|
-
name: 'codec_negotiation_complete',
|
|
2232
|
-
kind: 'message',
|
|
2233
|
-
oneof: 'eventPayload',
|
|
2234
|
-
T: () => CodecNegotiationComplete,
|
|
2235
|
-
},
|
|
2236
2294
|
{
|
|
2237
2295
|
no: 27,
|
|
2238
2296
|
name: 'change_publish_options',
|
|
@@ -2253,10 +2311,12 @@ class ChangePublishOptions$Type extends runtime.MessageType {
|
|
|
2253
2311
|
super('stream.video.sfu.event.ChangePublishOptions', [
|
|
2254
2312
|
{
|
|
2255
2313
|
no: 1,
|
|
2256
|
-
name: '
|
|
2314
|
+
name: 'publish_options',
|
|
2257
2315
|
kind: 'message',
|
|
2316
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2258
2317
|
T: () => PublishOption,
|
|
2259
2318
|
},
|
|
2319
|
+
{ no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2260
2320
|
]);
|
|
2261
2321
|
}
|
|
2262
2322
|
}
|
|
@@ -2265,15 +2325,15 @@ class ChangePublishOptions$Type extends runtime.MessageType {
|
|
|
2265
2325
|
*/
|
|
2266
2326
|
const ChangePublishOptions = new ChangePublishOptions$Type();
|
|
2267
2327
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
2268
|
-
class
|
|
2328
|
+
class ChangePublishOptionsComplete$Type extends runtime.MessageType {
|
|
2269
2329
|
constructor() {
|
|
2270
|
-
super('stream.video.sfu.event.
|
|
2330
|
+
super('stream.video.sfu.event.ChangePublishOptionsComplete', []);
|
|
2271
2331
|
}
|
|
2272
2332
|
}
|
|
2273
2333
|
/**
|
|
2274
|
-
* @generated MessageType for protobuf message stream.video.sfu.event.
|
|
2334
|
+
* @generated MessageType for protobuf message stream.video.sfu.event.ChangePublishOptionsComplete
|
|
2275
2335
|
*/
|
|
2276
|
-
const
|
|
2336
|
+
const ChangePublishOptionsComplete = new ChangePublishOptionsComplete$Type();
|
|
2277
2337
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
2278
2338
|
class ParticipantMigrationComplete$Type extends runtime.MessageType {
|
|
2279
2339
|
constructor() {
|
|
@@ -2531,6 +2591,20 @@ class JoinRequest$Type extends runtime.MessageType {
|
|
|
2531
2591
|
kind: 'message',
|
|
2532
2592
|
T: () => ReconnectDetails,
|
|
2533
2593
|
},
|
|
2594
|
+
{
|
|
2595
|
+
no: 9,
|
|
2596
|
+
name: 'preferred_publish_options',
|
|
2597
|
+
kind: 'message',
|
|
2598
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2599
|
+
T: () => PublishOption,
|
|
2600
|
+
},
|
|
2601
|
+
{
|
|
2602
|
+
no: 10,
|
|
2603
|
+
name: 'preferred_subscribe_options',
|
|
2604
|
+
kind: 'message',
|
|
2605
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2606
|
+
T: () => SubscribeOption,
|
|
2607
|
+
},
|
|
2534
2608
|
]);
|
|
2535
2609
|
}
|
|
2536
2610
|
}
|
|
@@ -2638,7 +2712,8 @@ class JoinResponse$Type extends runtime.MessageType {
|
|
|
2638
2712
|
no: 4,
|
|
2639
2713
|
name: 'publish_options',
|
|
2640
2714
|
kind: 'message',
|
|
2641
|
-
|
|
2715
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2716
|
+
T: () => PublishOption,
|
|
2642
2717
|
},
|
|
2643
2718
|
]);
|
|
2644
2719
|
}
|
|
@@ -2803,6 +2878,22 @@ class AudioSender$Type extends runtime.MessageType {
|
|
|
2803
2878
|
constructor() {
|
|
2804
2879
|
super('stream.video.sfu.event.AudioSender', [
|
|
2805
2880
|
{ no: 2, name: 'codec', kind: 'message', T: () => Codec },
|
|
2881
|
+
{
|
|
2882
|
+
no: 3,
|
|
2883
|
+
name: 'track_type',
|
|
2884
|
+
kind: 'enum',
|
|
2885
|
+
T: () => [
|
|
2886
|
+
'stream.video.sfu.models.TrackType',
|
|
2887
|
+
TrackType,
|
|
2888
|
+
'TRACK_TYPE_',
|
|
2889
|
+
],
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
no: 4,
|
|
2893
|
+
name: 'publish_option_id',
|
|
2894
|
+
kind: 'scalar',
|
|
2895
|
+
T: 5 /*ScalarType.INT32*/,
|
|
2896
|
+
},
|
|
2806
2897
|
]);
|
|
2807
2898
|
}
|
|
2808
2899
|
}
|
|
@@ -2855,6 +2946,22 @@ class VideoSender$Type extends runtime.MessageType {
|
|
|
2855
2946
|
repeat: 1 /*RepeatType.PACKED*/,
|
|
2856
2947
|
T: () => VideoLayerSetting,
|
|
2857
2948
|
},
|
|
2949
|
+
{
|
|
2950
|
+
no: 4,
|
|
2951
|
+
name: 'track_type',
|
|
2952
|
+
kind: 'enum',
|
|
2953
|
+
T: () => [
|
|
2954
|
+
'stream.video.sfu.models.TrackType',
|
|
2955
|
+
TrackType,
|
|
2956
|
+
'TRACK_TYPE_',
|
|
2957
|
+
],
|
|
2958
|
+
},
|
|
2959
|
+
{
|
|
2960
|
+
no: 5,
|
|
2961
|
+
name: 'publish_option_id',
|
|
2962
|
+
kind: 'scalar',
|
|
2963
|
+
T: 5 /*ScalarType.INT32*/,
|
|
2964
|
+
},
|
|
2858
2965
|
]);
|
|
2859
2966
|
}
|
|
2860
2967
|
}
|
|
@@ -2951,8 +3058,8 @@ var events = /*#__PURE__*/Object.freeze({
|
|
|
2951
3058
|
CallEnded: CallEnded,
|
|
2952
3059
|
CallGrantsUpdated: CallGrantsUpdated,
|
|
2953
3060
|
ChangePublishOptions: ChangePublishOptions,
|
|
3061
|
+
ChangePublishOptionsComplete: ChangePublishOptionsComplete,
|
|
2954
3062
|
ChangePublishQuality: ChangePublishQuality,
|
|
2955
|
-
CodecNegotiationComplete: CodecNegotiationComplete,
|
|
2956
3063
|
ConnectionQualityChanged: ConnectionQualityChanged,
|
|
2957
3064
|
ConnectionQualityInfo: ConnectionQualityInfo,
|
|
2958
3065
|
DominantSpeakerChanged: DominantSpeakerChanged,
|
|
@@ -3099,11 +3206,18 @@ const withHeaders = (headers) => {
|
|
|
3099
3206
|
const withRequestLogger = (logger, level) => {
|
|
3100
3207
|
return {
|
|
3101
3208
|
interceptUnary: (next, method, input, options) => {
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
options
|
|
3105
|
-
}
|
|
3106
|
-
|
|
3209
|
+
let invocation;
|
|
3210
|
+
try {
|
|
3211
|
+
invocation = next(method, input, options);
|
|
3212
|
+
}
|
|
3213
|
+
finally {
|
|
3214
|
+
logger(level, `Invoked SFU RPC method ${method.name}`, {
|
|
3215
|
+
request: invocation?.request,
|
|
3216
|
+
headers: invocation?.requestHeaders,
|
|
3217
|
+
response: invocation?.response,
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
return invocation;
|
|
3107
3221
|
},
|
|
3108
3222
|
};
|
|
3109
3223
|
};
|
|
@@ -3320,374 +3434,98 @@ const retryable = async (rpc, signal) => {
|
|
|
3320
3434
|
return result;
|
|
3321
3435
|
};
|
|
3322
3436
|
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
const setOSInfo = (info) => {
|
|
3342
|
-
osInfo = info;
|
|
3343
|
-
};
|
|
3344
|
-
const getOSInfo = () => {
|
|
3345
|
-
return osInfo;
|
|
3346
|
-
};
|
|
3347
|
-
const setDeviceInfo = (info) => {
|
|
3348
|
-
deviceInfo = info;
|
|
3437
|
+
/**
|
|
3438
|
+
* Returns a generic SDP for the given direction.
|
|
3439
|
+
* We use this SDP to send it as part of our JoinRequest so that the SFU
|
|
3440
|
+
* can use it to determine the client's codec capabilities.
|
|
3441
|
+
*
|
|
3442
|
+
* @param direction the direction of the transceiver.
|
|
3443
|
+
*/
|
|
3444
|
+
const getGenericSdp = async (direction) => {
|
|
3445
|
+
const tempPc = new RTCPeerConnection();
|
|
3446
|
+
tempPc.addTransceiver('video', { direction });
|
|
3447
|
+
tempPc.addTransceiver('audio', { direction });
|
|
3448
|
+
const offer = await tempPc.createOffer();
|
|
3449
|
+
const sdp = offer.sdp ?? '';
|
|
3450
|
+
tempPc.getTransceivers().forEach((t) => {
|
|
3451
|
+
t.stop?.();
|
|
3452
|
+
});
|
|
3453
|
+
tempPc.close();
|
|
3454
|
+
return sdp;
|
|
3349
3455
|
};
|
|
3350
|
-
|
|
3351
|
-
|
|
3456
|
+
/**
|
|
3457
|
+
* Returns whether the codec is an SVC codec.
|
|
3458
|
+
*
|
|
3459
|
+
* @param codecOrMimeType the codec to check.
|
|
3460
|
+
*/
|
|
3461
|
+
const isSvcCodec = (codecOrMimeType) => {
|
|
3462
|
+
if (!codecOrMimeType)
|
|
3463
|
+
return false;
|
|
3464
|
+
codecOrMimeType = codecOrMimeType.toLowerCase();
|
|
3465
|
+
return (codecOrMimeType === 'vp9' ||
|
|
3466
|
+
codecOrMimeType === 'av1' ||
|
|
3467
|
+
codecOrMimeType === 'video/vp9' ||
|
|
3468
|
+
codecOrMimeType === 'video/av1');
|
|
3352
3469
|
};
|
|
3353
|
-
|
|
3354
|
-
|
|
3470
|
+
|
|
3471
|
+
const sfuEventKinds = {
|
|
3472
|
+
subscriberOffer: undefined,
|
|
3473
|
+
publisherAnswer: undefined,
|
|
3474
|
+
connectionQualityChanged: undefined,
|
|
3475
|
+
audioLevelChanged: undefined,
|
|
3476
|
+
iceTrickle: undefined,
|
|
3477
|
+
changePublishQuality: undefined,
|
|
3478
|
+
participantJoined: undefined,
|
|
3479
|
+
participantLeft: undefined,
|
|
3480
|
+
dominantSpeakerChanged: undefined,
|
|
3481
|
+
joinResponse: undefined,
|
|
3482
|
+
healthCheckResponse: undefined,
|
|
3483
|
+
trackPublished: undefined,
|
|
3484
|
+
trackUnpublished: undefined,
|
|
3485
|
+
error: undefined,
|
|
3486
|
+
callGrantsUpdated: undefined,
|
|
3487
|
+
goAway: undefined,
|
|
3488
|
+
iceRestart: undefined,
|
|
3489
|
+
pinsUpdated: undefined,
|
|
3490
|
+
callEnded: undefined,
|
|
3491
|
+
participantUpdated: undefined,
|
|
3492
|
+
participantMigrationComplete: undefined,
|
|
3493
|
+
changePublishOptions: undefined,
|
|
3355
3494
|
};
|
|
3356
|
-
const
|
|
3357
|
-
|
|
3495
|
+
const isSfuEvent = (eventName) => {
|
|
3496
|
+
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
3358
3497
|
};
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3498
|
+
class Dispatcher {
|
|
3499
|
+
constructor() {
|
|
3500
|
+
this.logger = getLogger(['Dispatcher']);
|
|
3501
|
+
this.subscribers = {};
|
|
3502
|
+
this.dispatch = (message, logTag = '0') => {
|
|
3503
|
+
const eventKind = message.eventPayload.oneofKind;
|
|
3504
|
+
if (!eventKind)
|
|
3505
|
+
return;
|
|
3506
|
+
const payload = message.eventPayload[eventKind];
|
|
3507
|
+
this.logger('debug', `Dispatching ${eventKind}, tag=${logTag}`, payload);
|
|
3508
|
+
const listeners = this.subscribers[eventKind];
|
|
3509
|
+
if (!listeners)
|
|
3510
|
+
return;
|
|
3511
|
+
for (const fn of listeners) {
|
|
3512
|
+
try {
|
|
3513
|
+
fn(payload);
|
|
3514
|
+
}
|
|
3515
|
+
catch (e) {
|
|
3516
|
+
this.logger('warn', 'Listener failed with error', e);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3374
3519
|
};
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
deviceState.apple.isLowPowerModeEnabled,
|
|
3385
|
-
},
|
|
3386
|
-
};
|
|
3387
|
-
}
|
|
3388
|
-
};
|
|
3389
|
-
const setPowerState = (powerMode) => {
|
|
3390
|
-
if (!osInfo) {
|
|
3391
|
-
deviceState = { oneofKind: undefined };
|
|
3392
|
-
return;
|
|
3393
|
-
}
|
|
3394
|
-
if (osInfo.name === 'android') {
|
|
3395
|
-
deviceState = {
|
|
3396
|
-
oneofKind: 'android',
|
|
3397
|
-
android: {
|
|
3398
|
-
thermalState: deviceState?.oneofKind === 'android'
|
|
3399
|
-
? deviceState.android.thermalState
|
|
3400
|
-
: AndroidThermalState.UNSPECIFIED,
|
|
3401
|
-
isPowerSaverMode: powerMode,
|
|
3402
|
-
},
|
|
3403
|
-
};
|
|
3404
|
-
}
|
|
3405
|
-
if (osInfo.name.toLowerCase() === 'ios') {
|
|
3406
|
-
deviceState = {
|
|
3407
|
-
oneofKind: 'apple',
|
|
3408
|
-
apple: {
|
|
3409
|
-
thermalState: deviceState?.oneofKind === 'apple'
|
|
3410
|
-
? deviceState.apple.thermalState
|
|
3411
|
-
: AppleThermalState.UNSPECIFIED,
|
|
3412
|
-
isLowPowerModeEnabled: powerMode,
|
|
3413
|
-
},
|
|
3414
|
-
};
|
|
3415
|
-
}
|
|
3416
|
-
};
|
|
3417
|
-
const getDeviceState = () => {
|
|
3418
|
-
return deviceState;
|
|
3419
|
-
};
|
|
3420
|
-
const getClientDetails = () => {
|
|
3421
|
-
if (isReactNative()) {
|
|
3422
|
-
// Since RN doesn't support web, sharing browser info is not required
|
|
3423
|
-
return {
|
|
3424
|
-
sdk: getSdkInfo(),
|
|
3425
|
-
os: getOSInfo(),
|
|
3426
|
-
device: getDeviceInfo(),
|
|
3427
|
-
};
|
|
3428
|
-
}
|
|
3429
|
-
const userAgent = new uaParserJs.UAParser(navigator.userAgent);
|
|
3430
|
-
const { browser, os, device, cpu } = userAgent.getResult();
|
|
3431
|
-
return {
|
|
3432
|
-
sdk: getSdkInfo(),
|
|
3433
|
-
browser: {
|
|
3434
|
-
name: browser.name || navigator.userAgent,
|
|
3435
|
-
version: browser.version || '',
|
|
3436
|
-
},
|
|
3437
|
-
os: {
|
|
3438
|
-
name: os.name || '',
|
|
3439
|
-
version: os.version || '',
|
|
3440
|
-
architecture: cpu.architecture || '',
|
|
3441
|
-
},
|
|
3442
|
-
device: {
|
|
3443
|
-
name: [device.vendor, device.model, device.type]
|
|
3444
|
-
.filter(Boolean)
|
|
3445
|
-
.join(' '),
|
|
3446
|
-
version: '',
|
|
3447
|
-
},
|
|
3448
|
-
};
|
|
3449
|
-
};
|
|
3450
|
-
|
|
3451
|
-
/**
|
|
3452
|
-
* Checks whether the current browser is Safari.
|
|
3453
|
-
*/
|
|
3454
|
-
const isSafari = () => {
|
|
3455
|
-
if (typeof navigator === 'undefined')
|
|
3456
|
-
return false;
|
|
3457
|
-
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent || '');
|
|
3458
|
-
};
|
|
3459
|
-
/**
|
|
3460
|
-
* Checks whether the current browser is Firefox.
|
|
3461
|
-
*/
|
|
3462
|
-
const isFirefox = () => {
|
|
3463
|
-
if (typeof navigator === 'undefined')
|
|
3464
|
-
return false;
|
|
3465
|
-
return navigator.userAgent?.includes('Firefox');
|
|
3466
|
-
};
|
|
3467
|
-
/**
|
|
3468
|
-
* Checks whether the current browser is Google Chrome.
|
|
3469
|
-
*/
|
|
3470
|
-
const isChrome = () => {
|
|
3471
|
-
if (typeof navigator === 'undefined')
|
|
3472
|
-
return false;
|
|
3473
|
-
return navigator.userAgent?.includes('Chrome');
|
|
3474
|
-
};
|
|
3475
|
-
|
|
3476
|
-
var browsers = /*#__PURE__*/Object.freeze({
|
|
3477
|
-
__proto__: null,
|
|
3478
|
-
isChrome: isChrome,
|
|
3479
|
-
isFirefox: isFirefox,
|
|
3480
|
-
isSafari: isSafari
|
|
3481
|
-
});
|
|
3482
|
-
|
|
3483
|
-
/**
|
|
3484
|
-
* Returns back a list of sorted codecs, with the preferred codec first.
|
|
3485
|
-
*
|
|
3486
|
-
* @param kind the kind of codec to get.
|
|
3487
|
-
* @param preferredCodec the codec to prioritize (vp8, h264, vp9, av1...).
|
|
3488
|
-
* @param codecToRemove the codec to exclude from the list.
|
|
3489
|
-
* @param codecPreferencesSource the source of the codec preferences.
|
|
3490
|
-
*/
|
|
3491
|
-
const getPreferredCodecs = (kind, preferredCodec, codecToRemove, codecPreferencesSource) => {
|
|
3492
|
-
const source = codecPreferencesSource === 'receiver' ? RTCRtpReceiver : RTCRtpSender;
|
|
3493
|
-
if (!('getCapabilities' in source))
|
|
3494
|
-
return;
|
|
3495
|
-
const capabilities = source.getCapabilities(kind);
|
|
3496
|
-
if (!capabilities)
|
|
3497
|
-
return;
|
|
3498
|
-
const preferred = [];
|
|
3499
|
-
const partiallyPreferred = [];
|
|
3500
|
-
const unpreferred = [];
|
|
3501
|
-
const preferredCodecMimeType = `${kind}/${preferredCodec.toLowerCase()}`;
|
|
3502
|
-
const codecToRemoveMimeType = codecToRemove && `${kind}/${codecToRemove.toLowerCase()}`;
|
|
3503
|
-
for (const codec of capabilities.codecs) {
|
|
3504
|
-
const codecMimeType = codec.mimeType.toLowerCase();
|
|
3505
|
-
const shouldRemoveCodec = codecMimeType === codecToRemoveMimeType;
|
|
3506
|
-
if (shouldRemoveCodec)
|
|
3507
|
-
continue; // skip this codec
|
|
3508
|
-
const isPreferredCodec = codecMimeType === preferredCodecMimeType;
|
|
3509
|
-
if (!isPreferredCodec) {
|
|
3510
|
-
unpreferred.push(codec);
|
|
3511
|
-
continue;
|
|
3512
|
-
}
|
|
3513
|
-
// h264 is a special case, we want to prioritize the baseline codec with
|
|
3514
|
-
// profile-level-id is 42e01f and packetization-mode=0 for maximum
|
|
3515
|
-
// cross-browser compatibility.
|
|
3516
|
-
// this branch covers the other cases, such as vp8.
|
|
3517
|
-
if (codecMimeType !== 'video/h264') {
|
|
3518
|
-
preferred.push(codec);
|
|
3519
|
-
continue;
|
|
3520
|
-
}
|
|
3521
|
-
const sdpFmtpLine = codec.sdpFmtpLine;
|
|
3522
|
-
if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42')) {
|
|
3523
|
-
// this is not the baseline h264 codec, prioritize it lower
|
|
3524
|
-
partiallyPreferred.push(codec);
|
|
3525
|
-
continue;
|
|
3526
|
-
}
|
|
3527
|
-
if (sdpFmtpLine.includes('packetization-mode=1')) {
|
|
3528
|
-
preferred.unshift(codec);
|
|
3529
|
-
}
|
|
3530
|
-
else {
|
|
3531
|
-
preferred.push(codec);
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
// return a sorted list of codecs, with the preferred codecs first
|
|
3535
|
-
return [...preferred, ...partiallyPreferred, ...unpreferred];
|
|
3536
|
-
};
|
|
3537
|
-
/**
|
|
3538
|
-
* Returns a generic SDP for the given direction.
|
|
3539
|
-
* We use this SDP to send it as part of our JoinRequest so that the SFU
|
|
3540
|
-
* can use it to determine client's codec capabilities.
|
|
3541
|
-
*
|
|
3542
|
-
* @param direction the direction of the transceiver.
|
|
3543
|
-
*/
|
|
3544
|
-
const getGenericSdp = async (direction) => {
|
|
3545
|
-
const tempPc = new RTCPeerConnection();
|
|
3546
|
-
tempPc.addTransceiver('video', { direction });
|
|
3547
|
-
tempPc.addTransceiver('audio', { direction });
|
|
3548
|
-
const offer = await tempPc.createOffer();
|
|
3549
|
-
const sdp = offer.sdp ?? '';
|
|
3550
|
-
tempPc.getTransceivers().forEach((t) => {
|
|
3551
|
-
t.stop?.();
|
|
3552
|
-
});
|
|
3553
|
-
tempPc.close();
|
|
3554
|
-
return sdp;
|
|
3555
|
-
};
|
|
3556
|
-
/**
|
|
3557
|
-
* Returns the optimal video codec for the device.
|
|
3558
|
-
*/
|
|
3559
|
-
const getOptimalVideoCodec = (preferredCodec) => {
|
|
3560
|
-
if (isReactNative()) {
|
|
3561
|
-
const os = getOSInfo()?.name.toLowerCase();
|
|
3562
|
-
if (os === 'android')
|
|
3563
|
-
return preferredOr(preferredCodec, 'vp8');
|
|
3564
|
-
if (os === 'ios' || os === 'ipados') {
|
|
3565
|
-
return supportsH264Baseline() ? 'h264' : 'vp8';
|
|
3566
|
-
}
|
|
3567
|
-
return preferredOr(preferredCodec, 'h264');
|
|
3568
|
-
}
|
|
3569
|
-
if (isSafari())
|
|
3570
|
-
return 'h264';
|
|
3571
|
-
if (isFirefox())
|
|
3572
|
-
return 'vp8';
|
|
3573
|
-
return preferredOr(preferredCodec, 'vp8');
|
|
3574
|
-
};
|
|
3575
|
-
/**
|
|
3576
|
-
* Determines if the platform supports the preferred codec.
|
|
3577
|
-
* If not, it returns the fallback codec.
|
|
3578
|
-
*/
|
|
3579
|
-
const preferredOr = (codec, fallback) => {
|
|
3580
|
-
if (!codec)
|
|
3581
|
-
return fallback;
|
|
3582
|
-
if (!('getCapabilities' in RTCRtpSender))
|
|
3583
|
-
return fallback;
|
|
3584
|
-
const capabilities = RTCRtpSender.getCapabilities('video');
|
|
3585
|
-
if (!capabilities)
|
|
3586
|
-
return fallback;
|
|
3587
|
-
// Safari and Firefox do not have a good support encoding to SVC codecs,
|
|
3588
|
-
// so we disable it for them.
|
|
3589
|
-
if (isSvcCodec(codec) && (isSafari() || isFirefox()))
|
|
3590
|
-
return fallback;
|
|
3591
|
-
const { codecs } = capabilities;
|
|
3592
|
-
const codecMimeType = `video/${codec}`.toLowerCase();
|
|
3593
|
-
return codecs.some((c) => c.mimeType.toLowerCase() === codecMimeType)
|
|
3594
|
-
? codec
|
|
3595
|
-
: fallback;
|
|
3596
|
-
};
|
|
3597
|
-
/**
|
|
3598
|
-
* Returns whether the platform supports the H264 baseline codec.
|
|
3599
|
-
*/
|
|
3600
|
-
const supportsH264Baseline = () => {
|
|
3601
|
-
if (!('getCapabilities' in RTCRtpSender))
|
|
3602
|
-
return false;
|
|
3603
|
-
const capabilities = RTCRtpSender.getCapabilities('video');
|
|
3604
|
-
if (!capabilities)
|
|
3605
|
-
return false;
|
|
3606
|
-
return capabilities.codecs.some((c) => c.mimeType.toLowerCase() === 'video/h264' &&
|
|
3607
|
-
c.sdpFmtpLine?.includes('profile-level-id=42e01f'));
|
|
3608
|
-
};
|
|
3609
|
-
/**
|
|
3610
|
-
* Returns whether the codec is an SVC codec.
|
|
3611
|
-
*
|
|
3612
|
-
* @param codecOrMimeType the codec to check.
|
|
3613
|
-
*/
|
|
3614
|
-
const isSvcCodec = (codecOrMimeType) => {
|
|
3615
|
-
if (!codecOrMimeType)
|
|
3616
|
-
return false;
|
|
3617
|
-
codecOrMimeType = codecOrMimeType.toLowerCase();
|
|
3618
|
-
return (codecOrMimeType === 'vp9' ||
|
|
3619
|
-
codecOrMimeType === 'av1' ||
|
|
3620
|
-
codecOrMimeType === 'video/vp9' ||
|
|
3621
|
-
codecOrMimeType === 'video/av1');
|
|
3622
|
-
};
|
|
3623
|
-
|
|
3624
|
-
const sfuEventKinds = {
|
|
3625
|
-
subscriberOffer: undefined,
|
|
3626
|
-
publisherAnswer: undefined,
|
|
3627
|
-
connectionQualityChanged: undefined,
|
|
3628
|
-
audioLevelChanged: undefined,
|
|
3629
|
-
iceTrickle: undefined,
|
|
3630
|
-
changePublishQuality: undefined,
|
|
3631
|
-
participantJoined: undefined,
|
|
3632
|
-
participantLeft: undefined,
|
|
3633
|
-
dominantSpeakerChanged: undefined,
|
|
3634
|
-
joinResponse: undefined,
|
|
3635
|
-
healthCheckResponse: undefined,
|
|
3636
|
-
trackPublished: undefined,
|
|
3637
|
-
trackUnpublished: undefined,
|
|
3638
|
-
error: undefined,
|
|
3639
|
-
callGrantsUpdated: undefined,
|
|
3640
|
-
goAway: undefined,
|
|
3641
|
-
iceRestart: undefined,
|
|
3642
|
-
pinsUpdated: undefined,
|
|
3643
|
-
callEnded: undefined,
|
|
3644
|
-
participantUpdated: undefined,
|
|
3645
|
-
participantMigrationComplete: undefined,
|
|
3646
|
-
codecNegotiationComplete: undefined,
|
|
3647
|
-
changePublishOptions: undefined,
|
|
3648
|
-
};
|
|
3649
|
-
const isSfuEvent = (eventName) => {
|
|
3650
|
-
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
3651
|
-
};
|
|
3652
|
-
class Dispatcher {
|
|
3653
|
-
constructor() {
|
|
3654
|
-
this.logger = getLogger(['Dispatcher']);
|
|
3655
|
-
this.subscribers = {};
|
|
3656
|
-
this.dispatch = (message, logTag = '0') => {
|
|
3657
|
-
const eventKind = message.eventPayload.oneofKind;
|
|
3658
|
-
if (!eventKind)
|
|
3659
|
-
return;
|
|
3660
|
-
const payload = message.eventPayload[eventKind];
|
|
3661
|
-
this.logger('debug', `Dispatching ${eventKind}, tag=${logTag}`, payload);
|
|
3662
|
-
const listeners = this.subscribers[eventKind];
|
|
3663
|
-
if (!listeners)
|
|
3664
|
-
return;
|
|
3665
|
-
for (const fn of listeners) {
|
|
3666
|
-
try {
|
|
3667
|
-
fn(payload);
|
|
3668
|
-
}
|
|
3669
|
-
catch (e) {
|
|
3670
|
-
this.logger('warn', 'Listener failed with error', e);
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3673
|
-
};
|
|
3674
|
-
this.on = (eventName, fn) => {
|
|
3675
|
-
var _a;
|
|
3676
|
-
((_a = this.subscribers)[eventName] ?? (_a[eventName] = [])).push(fn);
|
|
3677
|
-
return () => {
|
|
3678
|
-
this.off(eventName, fn);
|
|
3679
|
-
};
|
|
3680
|
-
};
|
|
3681
|
-
this.off = (eventName, fn) => {
|
|
3682
|
-
this.subscribers[eventName] = (this.subscribers[eventName] || []).filter((f) => f !== fn);
|
|
3683
|
-
};
|
|
3684
|
-
this.offAll = (eventName) => {
|
|
3685
|
-
if (eventName) {
|
|
3686
|
-
this.subscribers[eventName] = [];
|
|
3687
|
-
}
|
|
3688
|
-
else {
|
|
3689
|
-
this.subscribers = {};
|
|
3690
|
-
}
|
|
3520
|
+
this.on = (eventName, fn) => {
|
|
3521
|
+
var _a;
|
|
3522
|
+
((_a = this.subscribers)[eventName] ?? (_a[eventName] = [])).push(fn);
|
|
3523
|
+
return () => {
|
|
3524
|
+
this.off(eventName, fn);
|
|
3525
|
+
};
|
|
3526
|
+
};
|
|
3527
|
+
this.off = (eventName, fn) => {
|
|
3528
|
+
this.subscribers[eventName] = (this.subscribers[eventName] || []).filter((f) => f !== fn);
|
|
3691
3529
|
};
|
|
3692
3530
|
}
|
|
3693
3531
|
}
|
|
@@ -3701,284 +3539,34 @@ class IceTrickleBuffer {
|
|
|
3701
3539
|
this.subscriberCandidates = new rxjs.ReplaySubject();
|
|
3702
3540
|
this.publisherCandidates = new rxjs.ReplaySubject();
|
|
3703
3541
|
this.push = (iceTrickle) => {
|
|
3542
|
+
const iceCandidate = toIceCandidate(iceTrickle);
|
|
3543
|
+
if (!iceCandidate)
|
|
3544
|
+
return;
|
|
3704
3545
|
if (iceTrickle.peerType === PeerType.SUBSCRIBER) {
|
|
3705
|
-
this.subscriberCandidates.next(
|
|
3546
|
+
this.subscriberCandidates.next(iceCandidate);
|
|
3706
3547
|
}
|
|
3707
3548
|
else if (iceTrickle.peerType === PeerType.PUBLISHER_UNSPECIFIED) {
|
|
3708
|
-
this.publisherCandidates.next(
|
|
3549
|
+
this.publisherCandidates.next(iceCandidate);
|
|
3709
3550
|
}
|
|
3710
3551
|
else {
|
|
3711
3552
|
const logger = getLogger(['sfu-client']);
|
|
3712
3553
|
logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
|
|
3713
3554
|
}
|
|
3714
3555
|
};
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
function getIceCandidate(candidate) {
|
|
3719
|
-
if (!candidate.usernameFragment) {
|
|
3720
|
-
// react-native-webrtc doesn't include usernameFragment in the candidate
|
|
3721
|
-
const splittedCandidate = candidate.candidate.split(' ');
|
|
3722
|
-
const ufragIndex = splittedCandidate.findIndex((s) => s === 'ufrag') + 1;
|
|
3723
|
-
const usernameFragment = splittedCandidate[ufragIndex];
|
|
3724
|
-
return JSON.stringify({ ...candidate, usernameFragment });
|
|
3725
|
-
}
|
|
3726
|
-
else {
|
|
3727
|
-
return JSON.stringify(candidate.toJSON());
|
|
3728
|
-
}
|
|
3729
|
-
}
|
|
3730
|
-
|
|
3731
|
-
const bitrateLookupTable = {
|
|
3732
|
-
h264: {
|
|
3733
|
-
2160: 5000000,
|
|
3734
|
-
1440: 3000000,
|
|
3735
|
-
1080: 2000000,
|
|
3736
|
-
720: 1250000,
|
|
3737
|
-
540: 750000,
|
|
3738
|
-
360: 400000,
|
|
3739
|
-
default: 1250000,
|
|
3740
|
-
},
|
|
3741
|
-
vp8: {
|
|
3742
|
-
2160: 5000000,
|
|
3743
|
-
1440: 2750000,
|
|
3744
|
-
1080: 2000000,
|
|
3745
|
-
720: 1250000,
|
|
3746
|
-
540: 600000,
|
|
3747
|
-
360: 350000,
|
|
3748
|
-
default: 1250000,
|
|
3749
|
-
},
|
|
3750
|
-
vp9: {
|
|
3751
|
-
2160: 3000000,
|
|
3752
|
-
1440: 2000000,
|
|
3753
|
-
1080: 1500000,
|
|
3754
|
-
720: 1250000,
|
|
3755
|
-
540: 500000,
|
|
3756
|
-
360: 275000,
|
|
3757
|
-
default: 1250000,
|
|
3758
|
-
},
|
|
3759
|
-
av1: {
|
|
3760
|
-
2160: 2000000,
|
|
3761
|
-
1440: 1550000,
|
|
3762
|
-
1080: 1000000,
|
|
3763
|
-
720: 600000,
|
|
3764
|
-
540: 350000,
|
|
3765
|
-
360: 200000,
|
|
3766
|
-
default: 600000,
|
|
3767
|
-
},
|
|
3768
|
-
};
|
|
3769
|
-
const getOptimalBitrate = (codec, frameHeight) => {
|
|
3770
|
-
const codecLookup = bitrateLookupTable[codec];
|
|
3771
|
-
if (!codecLookup)
|
|
3772
|
-
throw new Error(`Unknown codec: ${codec}`);
|
|
3773
|
-
let bitrate = codecLookup[frameHeight];
|
|
3774
|
-
if (!bitrate) {
|
|
3775
|
-
const keys = Object.keys(codecLookup).map(Number);
|
|
3776
|
-
const nearest = keys.reduce((a, b) => Math.abs(b - frameHeight) < Math.abs(a - frameHeight) ? b : a);
|
|
3777
|
-
bitrate = codecLookup[nearest];
|
|
3778
|
-
}
|
|
3779
|
-
return bitrate ?? codecLookup.default;
|
|
3780
|
-
};
|
|
3781
|
-
|
|
3782
|
-
const DEFAULT_BITRATE = 1250000;
|
|
3783
|
-
const defaultTargetResolution = {
|
|
3784
|
-
bitrate: DEFAULT_BITRATE,
|
|
3785
|
-
width: 1280,
|
|
3786
|
-
height: 720,
|
|
3787
|
-
};
|
|
3788
|
-
const defaultBitratePerRid = {
|
|
3789
|
-
q: 300000,
|
|
3790
|
-
h: 750000,
|
|
3791
|
-
f: DEFAULT_BITRATE,
|
|
3792
|
-
};
|
|
3793
|
-
/**
|
|
3794
|
-
* In SVC, we need to send only one video encoding (layer).
|
|
3795
|
-
* this layer will have the additional spatial and temporal layers
|
|
3796
|
-
* defined via the scalabilityMode property.
|
|
3797
|
-
*
|
|
3798
|
-
* @param layers the layers to process.
|
|
3799
|
-
*/
|
|
3800
|
-
const toSvcEncodings = (layers) => {
|
|
3801
|
-
// we take the `f` layer, and we rename it to `q`.
|
|
3802
|
-
return layers?.filter((l) => l.rid === 'f').map((l) => ({ ...l, rid: 'q' }));
|
|
3803
|
-
};
|
|
3804
|
-
/**
|
|
3805
|
-
* Converts the rid to a video quality.
|
|
3806
|
-
*/
|
|
3807
|
-
const ridToVideoQuality = (rid) => {
|
|
3808
|
-
return rid === 'q'
|
|
3809
|
-
? VideoQuality.LOW_UNSPECIFIED
|
|
3810
|
-
: rid === 'h'
|
|
3811
|
-
? VideoQuality.MID
|
|
3812
|
-
: VideoQuality.HIGH; // default to HIGH
|
|
3813
|
-
};
|
|
3814
|
-
/**
|
|
3815
|
-
* Determines the most optimal video layers for simulcasting
|
|
3816
|
-
* for the given track.
|
|
3817
|
-
*
|
|
3818
|
-
* @param videoTrack the video track to find optimal layers for.
|
|
3819
|
-
* @param targetResolution the expected target resolution.
|
|
3820
|
-
* @param codecInUse the codec in use.
|
|
3821
|
-
* @param publishOptions the publish options for the track.
|
|
3822
|
-
*/
|
|
3823
|
-
const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetResolution, codecInUse, publishOptions) => {
|
|
3824
|
-
const optimalVideoLayers = [];
|
|
3825
|
-
const settings = videoTrack.getSettings();
|
|
3826
|
-
const { width = 0, height = 0 } = settings;
|
|
3827
|
-
const { scalabilityMode, bitrateDownscaleFactor = 2, maxSimulcastLayers = 3, } = publishOptions || {};
|
|
3828
|
-
const maxBitrate = getComputedMaxBitrate(targetResolution, width, height, codecInUse, publishOptions);
|
|
3829
|
-
let downscaleFactor = 1;
|
|
3830
|
-
let bitrateFactor = 1;
|
|
3831
|
-
const svcCodec = isSvcCodec(codecInUse);
|
|
3832
|
-
const totalLayers = svcCodec ? 3 : Math.min(3, maxSimulcastLayers);
|
|
3833
|
-
for (const rid of ['f', 'h', 'q'].slice(0, totalLayers)) {
|
|
3834
|
-
const layer = {
|
|
3835
|
-
active: true,
|
|
3836
|
-
rid,
|
|
3837
|
-
width: Math.round(width / downscaleFactor),
|
|
3838
|
-
height: Math.round(height / downscaleFactor),
|
|
3839
|
-
maxBitrate: Math.round(maxBitrate / bitrateFactor) || defaultBitratePerRid[rid],
|
|
3840
|
-
maxFramerate: 30,
|
|
3556
|
+
this.dispose = () => {
|
|
3557
|
+
this.subscriberCandidates.complete();
|
|
3558
|
+
this.publisherCandidates.complete();
|
|
3841
3559
|
};
|
|
3842
|
-
if (svcCodec) {
|
|
3843
|
-
// for SVC codecs, we need to set the scalability mode, and the
|
|
3844
|
-
// codec will handle the rest (layers, temporal layers, etc.)
|
|
3845
|
-
layer.scalabilityMode = scalabilityMode || 'L3T2_KEY';
|
|
3846
|
-
}
|
|
3847
|
-
else {
|
|
3848
|
-
// for non-SVC codecs, we need to downscale proportionally (simulcast)
|
|
3849
|
-
layer.scaleResolutionDownBy = downscaleFactor;
|
|
3850
|
-
}
|
|
3851
|
-
downscaleFactor *= 2;
|
|
3852
|
-
bitrateFactor *= bitrateDownscaleFactor;
|
|
3853
|
-
// Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
|
|
3854
|
-
// when deciding which layer to disable when CPU or bandwidth is constrained.
|
|
3855
|
-
// Encodings should be ordered in increasing spatial resolution order.
|
|
3856
|
-
optimalVideoLayers.unshift(layer);
|
|
3857
3560
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
/**
|
|
3863
|
-
* Computes the maximum bitrate for a given resolution.
|
|
3864
|
-
* If the current resolution is lower than the target resolution,
|
|
3865
|
-
* we want to proportionally reduce the target bitrate.
|
|
3866
|
-
* If the current resolution is higher than the target resolution,
|
|
3867
|
-
* we want to use the target bitrate.
|
|
3868
|
-
*
|
|
3869
|
-
* @param targetResolution the target resolution.
|
|
3870
|
-
* @param currentWidth the current width of the track.
|
|
3871
|
-
* @param currentHeight the current height of the track.
|
|
3872
|
-
* @param codecInUse the codec in use.
|
|
3873
|
-
* @param publishOptions the publish options.
|
|
3874
|
-
*/
|
|
3875
|
-
const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, codecInUse, publishOptions) => {
|
|
3876
|
-
// if the current resolution is lower than the target resolution,
|
|
3877
|
-
// we want to proportionally reduce the target bitrate
|
|
3878
|
-
const { width: targetWidth, height: targetHeight, bitrate: targetBitrate, } = targetResolution;
|
|
3879
|
-
const { preferredBitrate } = publishOptions || {};
|
|
3880
|
-
const frameHeight = currentWidth > currentHeight ? currentHeight : currentWidth;
|
|
3881
|
-
const bitrate = preferredBitrate ||
|
|
3882
|
-
(codecInUse ? getOptimalBitrate(codecInUse, frameHeight) : targetBitrate);
|
|
3883
|
-
if (currentWidth < targetWidth || currentHeight < targetHeight) {
|
|
3884
|
-
const currentPixels = currentWidth * currentHeight;
|
|
3885
|
-
const targetPixels = targetWidth * targetHeight;
|
|
3886
|
-
const reductionFactor = currentPixels / targetPixels;
|
|
3887
|
-
return Math.round(bitrate * reductionFactor);
|
|
3888
|
-
}
|
|
3889
|
-
return bitrate;
|
|
3890
|
-
};
|
|
3891
|
-
/**
|
|
3892
|
-
* Browsers have different simulcast constraints for different video resolutions.
|
|
3893
|
-
*
|
|
3894
|
-
* This function modifies the provided list of video layers according to the
|
|
3895
|
-
* current implementation of simulcast constraints in the Chromium based browsers.
|
|
3896
|
-
*
|
|
3897
|
-
* https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
|
|
3898
|
-
*/
|
|
3899
|
-
const withSimulcastConstraints = (settings, optimalVideoLayers) => {
|
|
3900
|
-
let layers;
|
|
3901
|
-
const size = Math.max(settings.width || 0, settings.height || 0);
|
|
3902
|
-
if (size <= 320) {
|
|
3903
|
-
// provide only one layer 320x240 (q), the one with the highest quality
|
|
3904
|
-
layers = optimalVideoLayers.filter((layer) => layer.rid === 'f');
|
|
3905
|
-
}
|
|
3906
|
-
else if (size <= 640) {
|
|
3907
|
-
// provide two layers, 160x120 (q) and 640x480 (h)
|
|
3908
|
-
layers = optimalVideoLayers.filter((layer) => layer.rid !== 'h');
|
|
3909
|
-
}
|
|
3910
|
-
else {
|
|
3911
|
-
// provide three layers for sizes > 640x480
|
|
3912
|
-
layers = optimalVideoLayers;
|
|
3913
|
-
}
|
|
3914
|
-
const ridMapping = ['q', 'h', 'f'];
|
|
3915
|
-
return layers.map((layer, index) => ({
|
|
3916
|
-
...layer,
|
|
3917
|
-
rid: ridMapping[index], // reassign rid
|
|
3918
|
-
}));
|
|
3919
|
-
};
|
|
3920
|
-
const findOptimalScreenSharingLayers = (videoTrack, publishOptions, defaultMaxBitrate = 3000000) => {
|
|
3921
|
-
const { screenShareSettings: preferences } = publishOptions || {};
|
|
3922
|
-
const settings = videoTrack.getSettings();
|
|
3923
|
-
return [
|
|
3924
|
-
{
|
|
3925
|
-
active: true,
|
|
3926
|
-
rid: 'q', // single track, start from 'q'
|
|
3927
|
-
width: settings.width || 0,
|
|
3928
|
-
height: settings.height || 0,
|
|
3929
|
-
scaleResolutionDownBy: 1,
|
|
3930
|
-
maxBitrate: preferences?.maxBitrate ?? defaultMaxBitrate,
|
|
3931
|
-
maxFramerate: preferences?.maxFramerate ?? 30,
|
|
3932
|
-
},
|
|
3933
|
-
];
|
|
3934
|
-
};
|
|
3935
|
-
|
|
3936
|
-
const ensureExhausted = (x, message) => {
|
|
3937
|
-
getLogger(['helpers'])('warn', message, x);
|
|
3938
|
-
};
|
|
3939
|
-
|
|
3940
|
-
const trackTypeToParticipantStreamKey = (trackType) => {
|
|
3941
|
-
switch (trackType) {
|
|
3942
|
-
case TrackType.SCREEN_SHARE:
|
|
3943
|
-
return 'screenShareStream';
|
|
3944
|
-
case TrackType.SCREEN_SHARE_AUDIO:
|
|
3945
|
-
return 'screenShareAudioStream';
|
|
3946
|
-
case TrackType.VIDEO:
|
|
3947
|
-
return 'videoStream';
|
|
3948
|
-
case TrackType.AUDIO:
|
|
3949
|
-
return 'audioStream';
|
|
3950
|
-
case TrackType.UNSPECIFIED:
|
|
3951
|
-
throw new Error('Track type is unspecified');
|
|
3952
|
-
default:
|
|
3953
|
-
ensureExhausted(trackType, 'Unknown track type');
|
|
3954
|
-
}
|
|
3955
|
-
};
|
|
3956
|
-
const muteTypeToTrackType = (muteType) => {
|
|
3957
|
-
switch (muteType) {
|
|
3958
|
-
case 'audio':
|
|
3959
|
-
return TrackType.AUDIO;
|
|
3960
|
-
case 'video':
|
|
3961
|
-
return TrackType.VIDEO;
|
|
3962
|
-
case 'screenshare':
|
|
3963
|
-
return TrackType.SCREEN_SHARE;
|
|
3964
|
-
case 'screenshare_audio':
|
|
3965
|
-
return TrackType.SCREEN_SHARE_AUDIO;
|
|
3966
|
-
default:
|
|
3967
|
-
ensureExhausted(muteType, 'Unknown mute type');
|
|
3561
|
+
}
|
|
3562
|
+
const toIceCandidate = (iceTrickle) => {
|
|
3563
|
+
try {
|
|
3564
|
+
return JSON.parse(iceTrickle.iceCandidate);
|
|
3968
3565
|
}
|
|
3969
|
-
|
|
3970
|
-
const
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
return TrackType.AUDIO;
|
|
3974
|
-
case 'TRACK_TYPE_VIDEO':
|
|
3975
|
-
return TrackType.VIDEO;
|
|
3976
|
-
case 'TRACK_TYPE_SCREEN_SHARE':
|
|
3977
|
-
return TrackType.SCREEN_SHARE;
|
|
3978
|
-
case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
|
|
3979
|
-
return TrackType.SCREEN_SHARE_AUDIO;
|
|
3980
|
-
default:
|
|
3981
|
-
return undefined;
|
|
3566
|
+
catch (e) {
|
|
3567
|
+
const logger = getLogger(['sfu-client']);
|
|
3568
|
+
logger('error', `Failed to parse ICE Trickle`, e, iceTrickle);
|
|
3569
|
+
return undefined;
|
|
3982
3570
|
}
|
|
3983
3571
|
};
|
|
3984
3572
|
|
|
@@ -4122,6 +3710,24 @@ const setCurrentValue = (subject, update) => {
|
|
|
4122
3710
|
subject.next(next);
|
|
4123
3711
|
return next;
|
|
4124
3712
|
};
|
|
3713
|
+
/**
|
|
3714
|
+
* Updates the value of the provided Subject and returns the previous value
|
|
3715
|
+
* and a function to roll back the update.
|
|
3716
|
+
* This is useful when you want to optimistically update a value
|
|
3717
|
+
* and roll back the update if an error occurs.
|
|
3718
|
+
*
|
|
3719
|
+
* @param subject the subject to update.
|
|
3720
|
+
* @param update the update to apply to the subject.
|
|
3721
|
+
*/
|
|
3722
|
+
const updateValue = (subject, update) => {
|
|
3723
|
+
const lastValue = subject.getValue();
|
|
3724
|
+
const value = setCurrentValue(subject, update);
|
|
3725
|
+
return {
|
|
3726
|
+
lastValue,
|
|
3727
|
+
value,
|
|
3728
|
+
rollback: () => setCurrentValue(subject, lastValue),
|
|
3729
|
+
};
|
|
3730
|
+
};
|
|
4125
3731
|
/**
|
|
4126
3732
|
* Creates a subscription and returns a function to unsubscribe.
|
|
4127
3733
|
*
|
|
@@ -4155,7 +3761,8 @@ var rxUtils = /*#__PURE__*/Object.freeze({
|
|
|
4155
3761
|
createSafeAsyncSubscription: createSafeAsyncSubscription,
|
|
4156
3762
|
createSubscription: createSubscription,
|
|
4157
3763
|
getCurrentValue: getCurrentValue,
|
|
4158
|
-
setCurrentValue: setCurrentValue
|
|
3764
|
+
setCurrentValue: setCurrentValue,
|
|
3765
|
+
updateValue: updateValue
|
|
4159
3766
|
});
|
|
4160
3767
|
|
|
4161
3768
|
/**
|
|
@@ -4602,6 +4209,7 @@ class CallState {
|
|
|
4602
4209
|
this.sessionSubject = new rxjs.BehaviorSubject(undefined);
|
|
4603
4210
|
this.settingsSubject = new rxjs.BehaviorSubject(undefined);
|
|
4604
4211
|
this.transcribingSubject = new rxjs.BehaviorSubject(false);
|
|
4212
|
+
this.captioningSubject = new rxjs.BehaviorSubject(false);
|
|
4605
4213
|
this.endedBySubject = new rxjs.BehaviorSubject(undefined);
|
|
4606
4214
|
this.thumbnailsSubject = new rxjs.BehaviorSubject(undefined);
|
|
4607
4215
|
this.membersSubject = new rxjs.BehaviorSubject([]);
|
|
@@ -4612,6 +4220,7 @@ class CallState {
|
|
|
4612
4220
|
this.anonymousParticipantCountSubject = new rxjs.BehaviorSubject(0);
|
|
4613
4221
|
this.participantsSubject = new rxjs.BehaviorSubject([]);
|
|
4614
4222
|
this.callStatsReportSubject = new rxjs.BehaviorSubject(undefined);
|
|
4223
|
+
this.closedCaptionsSubject = new rxjs.BehaviorSubject([]);
|
|
4615
4224
|
// These are tracks that were delivered to the Subscriber's onTrack event
|
|
4616
4225
|
// that we couldn't associate with a participant yet.
|
|
4617
4226
|
// This happens when the participantJoined event hasn't been received yet.
|
|
@@ -4620,10 +4229,18 @@ class CallState {
|
|
|
4620
4229
|
this.logger = getLogger(['CallState']);
|
|
4621
4230
|
/**
|
|
4622
4231
|
* A list of comparators that are used to sort the participants.
|
|
4623
|
-
*
|
|
4624
|
-
* @private
|
|
4625
4232
|
*/
|
|
4626
4233
|
this.sortParticipantsBy = defaultSortPreset;
|
|
4234
|
+
this.closedCaptionsTasks = new Map();
|
|
4235
|
+
/**
|
|
4236
|
+
* Runs the cleanup tasks.
|
|
4237
|
+
*/
|
|
4238
|
+
this.dispose = () => {
|
|
4239
|
+
for (const [ccKey, taskId] of this.closedCaptionsTasks.entries()) {
|
|
4240
|
+
clearTimeout(taskId);
|
|
4241
|
+
this.closedCaptionsTasks.delete(ccKey);
|
|
4242
|
+
}
|
|
4243
|
+
};
|
|
4627
4244
|
/**
|
|
4628
4245
|
* Sets the list of criteria that are used to sort the participants.
|
|
4629
4246
|
* To disable sorting, you can pass `noopComparator()`.
|
|
@@ -4672,6 +4289,15 @@ class CallState {
|
|
|
4672
4289
|
this.setStartedAt = (startedAt) => {
|
|
4673
4290
|
return this.setCurrentValue(this.startedAtSubject, startedAt);
|
|
4674
4291
|
};
|
|
4292
|
+
/**
|
|
4293
|
+
* Sets the closed captioning state of the current call.
|
|
4294
|
+
*
|
|
4295
|
+
* @internal
|
|
4296
|
+
* @param captioning the closed captioning state.
|
|
4297
|
+
*/
|
|
4298
|
+
this.setCaptioning = (captioning) => {
|
|
4299
|
+
return updateValue(this.captioningSubject, captioning);
|
|
4300
|
+
};
|
|
4675
4301
|
/**
|
|
4676
4302
|
* Sets the number of anonymous participants in the current call.
|
|
4677
4303
|
*
|
|
@@ -4770,7 +4396,6 @@ class CallState {
|
|
|
4770
4396
|
}
|
|
4771
4397
|
const thePatch = typeof patch === 'function' ? patch(participant) : patch;
|
|
4772
4398
|
const updatedParticipant = {
|
|
4773
|
-
// FIXME OL: this is not a deep merge, we might want to revisit this
|
|
4774
4399
|
...participant,
|
|
4775
4400
|
...thePatch,
|
|
4776
4401
|
};
|
|
@@ -4832,7 +4457,6 @@ class CallState {
|
|
|
4832
4457
|
*
|
|
4833
4458
|
* @param trackType the kind of subscription to update.
|
|
4834
4459
|
* @param changes the list of subscription changes to do.
|
|
4835
|
-
* @param type the debounce type to use for the update.
|
|
4836
4460
|
*/
|
|
4837
4461
|
this.updateParticipantTracks = (trackType, changes) => {
|
|
4838
4462
|
return this.updateParticipants(Object.entries(changes).reduce((acc, [sessionId, change]) => {
|
|
@@ -4935,6 +4559,14 @@ class CallState {
|
|
|
4935
4559
|
}
|
|
4936
4560
|
return orphans;
|
|
4937
4561
|
};
|
|
4562
|
+
/**
|
|
4563
|
+
* Updates the closed captions settings.
|
|
4564
|
+
*
|
|
4565
|
+
* @param config the new closed captions settings.
|
|
4566
|
+
*/
|
|
4567
|
+
this.updateClosedCaptionSettings = (config) => {
|
|
4568
|
+
this.closedCaptionsSettings = { ...this.closedCaptionsSettings, ...config };
|
|
4569
|
+
};
|
|
4938
4570
|
/**
|
|
4939
4571
|
* Updates the call state with the data received from the server.
|
|
4940
4572
|
*
|
|
@@ -4958,6 +4590,7 @@ class CallState {
|
|
|
4958
4590
|
this.updateParticipantCountFromSession(s);
|
|
4959
4591
|
this.setCurrentValue(this.settingsSubject, call.settings);
|
|
4960
4592
|
this.setCurrentValue(this.transcribingSubject, call.transcribing);
|
|
4593
|
+
this.setCurrentValue(this.captioningSubject, call.captioning);
|
|
4961
4594
|
this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
|
|
4962
4595
|
};
|
|
4963
4596
|
/**
|
|
@@ -5149,6 +4782,35 @@ class CallState {
|
|
|
5149
4782
|
this.setCurrentValue(this.ownCapabilitiesSubject, event.own_capabilities);
|
|
5150
4783
|
}
|
|
5151
4784
|
};
|
|
4785
|
+
this.updateFromClosedCaptions = (event) => {
|
|
4786
|
+
this.setCurrentValue(this.closedCaptionsSubject, (queue) => {
|
|
4787
|
+
const { closed_caption } = event;
|
|
4788
|
+
const keyOf = (c) => `${c.speaker_id}/${c.start_time}`;
|
|
4789
|
+
const currentKey = keyOf(closed_caption);
|
|
4790
|
+
const duplicate = queue.some((caption) => keyOf(caption) === currentKey);
|
|
4791
|
+
if (duplicate)
|
|
4792
|
+
return queue;
|
|
4793
|
+
const nextQueue = [...queue, closed_caption];
|
|
4794
|
+
const { visibilityDurationMs = 2700, maxVisibleCaptions = 2 } = this.closedCaptionsSettings || {};
|
|
4795
|
+
// schedule the removal of the closed caption after the retention time
|
|
4796
|
+
if (visibilityDurationMs > 0) {
|
|
4797
|
+
const taskId = setTimeout(() => {
|
|
4798
|
+
this.setCurrentValue(this.closedCaptionsSubject, (captions) => captions.filter((caption) => caption !== closed_caption));
|
|
4799
|
+
this.closedCaptionsTasks.delete(currentKey);
|
|
4800
|
+
}, visibilityDurationMs);
|
|
4801
|
+
this.closedCaptionsTasks.set(currentKey, taskId);
|
|
4802
|
+
// cancel the cleanup tasks for the closed captions that are no longer in the queue
|
|
4803
|
+
for (let i = 0; i < nextQueue.length - maxVisibleCaptions; i++) {
|
|
4804
|
+
const key = keyOf(nextQueue[i]);
|
|
4805
|
+
const task = this.closedCaptionsTasks.get(key);
|
|
4806
|
+
clearTimeout(task);
|
|
4807
|
+
this.closedCaptionsTasks.delete(key);
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
// trim the queue
|
|
4811
|
+
return nextQueue.slice(-maxVisibleCaptions);
|
|
4812
|
+
});
|
|
4813
|
+
};
|
|
5152
4814
|
this.participants$ = this.participantsSubject.asObservable().pipe(
|
|
5153
4815
|
// maintain stable-sort by mutating the participants stored
|
|
5154
4816
|
// in the original subject
|
|
@@ -5175,6 +4837,7 @@ class CallState {
|
|
|
5175
4837
|
this.settings$ = this.settingsSubject.asObservable();
|
|
5176
4838
|
this.endedBy$ = this.endedBySubject.asObservable();
|
|
5177
4839
|
this.thumbnails$ = this.thumbnailsSubject.asObservable();
|
|
4840
|
+
this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
|
|
5178
4841
|
/**
|
|
5179
4842
|
* Performs shallow comparison of two arrays.
|
|
5180
4843
|
* Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
|
|
@@ -5204,9 +4867,9 @@ class CallState {
|
|
|
5204
4867
|
this.participantCount$ = duc(this.participantCountSubject);
|
|
5205
4868
|
this.recording$ = duc(this.recordingSubject);
|
|
5206
4869
|
this.transcribing$ = duc(this.transcribingSubject);
|
|
4870
|
+
this.captioning$ = duc(this.captioningSubject);
|
|
5207
4871
|
this.eventHandlers = {
|
|
5208
4872
|
// these events are not updating the call state:
|
|
5209
|
-
'call.closed_caption': undefined,
|
|
5210
4873
|
'call.deleted': undefined,
|
|
5211
4874
|
'call.permission_request': undefined,
|
|
5212
4875
|
'call.recording_ready': undefined,
|
|
@@ -5227,6 +4890,16 @@ class CallState {
|
|
|
5227
4890
|
// events that update call state:
|
|
5228
4891
|
'call.accepted': (e) => this.updateFromCallResponse(e.call),
|
|
5229
4892
|
'call.blocked_user': this.blockUser,
|
|
4893
|
+
'call.closed_caption': this.updateFromClosedCaptions,
|
|
4894
|
+
'call.closed_captions_failed': () => {
|
|
4895
|
+
this.setCurrentValue(this.captioningSubject, false);
|
|
4896
|
+
},
|
|
4897
|
+
'call.closed_captions_started': () => {
|
|
4898
|
+
this.setCurrentValue(this.captioningSubject, true);
|
|
4899
|
+
},
|
|
4900
|
+
'call.closed_captions_stopped': () => {
|
|
4901
|
+
this.setCurrentValue(this.captioningSubject, false);
|
|
4902
|
+
},
|
|
5230
4903
|
'call.created': (e) => this.updateFromCallResponse(e.call),
|
|
5231
4904
|
'call.ended': (e) => {
|
|
5232
4905
|
this.updateFromCallResponse(e.call);
|
|
@@ -5284,6 +4957,12 @@ class CallState {
|
|
|
5284
4957
|
get startedAt() {
|
|
5285
4958
|
return this.getCurrentValue(this.startedAt$);
|
|
5286
4959
|
}
|
|
4960
|
+
/**
|
|
4961
|
+
* Returns whether closed captions are enabled in the current call.
|
|
4962
|
+
*/
|
|
4963
|
+
get captioning() {
|
|
4964
|
+
return this.getCurrentValue(this.captioning$);
|
|
4965
|
+
}
|
|
5287
4966
|
/**
|
|
5288
4967
|
* The server-side counted number of anonymous participants connected to the current call.
|
|
5289
4968
|
* This number includes the anonymous participants as well.
|
|
@@ -5447,200 +5126,454 @@ class CallState {
|
|
|
5447
5126
|
get thumbnails() {
|
|
5448
5127
|
return this.getCurrentValue(this.thumbnails$);
|
|
5449
5128
|
}
|
|
5129
|
+
/**
|
|
5130
|
+
* Returns the current queue of closed captions.
|
|
5131
|
+
*/
|
|
5132
|
+
get closedCaptions() {
|
|
5133
|
+
return this.getCurrentValue(this.closedCaptions$);
|
|
5134
|
+
}
|
|
5450
5135
|
}
|
|
5451
5136
|
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5137
|
+
/**
|
|
5138
|
+
* A base class for the `Publisher` and `Subscriber` classes.
|
|
5139
|
+
* @internal
|
|
5140
|
+
*/
|
|
5141
|
+
class BasePeerConnection {
|
|
5142
|
+
/**
|
|
5143
|
+
* Constructs a new `BasePeerConnection` instance.
|
|
5144
|
+
*/
|
|
5145
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, }) {
|
|
5146
|
+
this.isIceRestarting = false;
|
|
5147
|
+
this.subscriptions = [];
|
|
5148
|
+
/**
|
|
5149
|
+
* Disposes the `RTCPeerConnection` instance.
|
|
5150
|
+
*/
|
|
5151
|
+
this.dispose = () => {
|
|
5152
|
+
this.detachEventHandlers();
|
|
5153
|
+
this.pc.close();
|
|
5154
|
+
};
|
|
5155
|
+
/**
|
|
5156
|
+
* Handles events synchronously.
|
|
5157
|
+
* Consecutive events are queued and executed one after the other.
|
|
5158
|
+
*/
|
|
5159
|
+
this.on = (event, fn) => {
|
|
5160
|
+
this.subscriptions.push(this.dispatcher.on(event, (e) => {
|
|
5161
|
+
withoutConcurrency(`pc.${event}`, async () => fn(e)).catch((err) => {
|
|
5162
|
+
this.logger('warn', `Error handling ${event}`, err);
|
|
5163
|
+
});
|
|
5164
|
+
}));
|
|
5165
|
+
};
|
|
5166
|
+
/**
|
|
5167
|
+
* Appends the trickled ICE candidates to the `RTCPeerConnection`.
|
|
5168
|
+
*/
|
|
5169
|
+
this.addTrickledIceCandidates = () => {
|
|
5170
|
+
const { iceTrickleBuffer } = this.sfuClient;
|
|
5171
|
+
const observable = this.peerType === PeerType.SUBSCRIBER
|
|
5172
|
+
? iceTrickleBuffer.subscriberCandidates
|
|
5173
|
+
: iceTrickleBuffer.publisherCandidates;
|
|
5174
|
+
this.unsubscribeIceTrickle?.();
|
|
5175
|
+
this.unsubscribeIceTrickle = createSafeAsyncSubscription(observable, async (candidate) => {
|
|
5176
|
+
return this.pc.addIceCandidate(candidate).catch((e) => {
|
|
5177
|
+
this.logger('warn', `ICE candidate error`, e, candidate);
|
|
5178
|
+
});
|
|
5179
|
+
});
|
|
5180
|
+
};
|
|
5181
|
+
/**
|
|
5182
|
+
* Sets the SFU client to use.
|
|
5183
|
+
*
|
|
5184
|
+
* @param sfuClient the SFU client to use.
|
|
5185
|
+
*/
|
|
5186
|
+
this.setSfuClient = (sfuClient) => {
|
|
5187
|
+
this.sfuClient = sfuClient;
|
|
5188
|
+
};
|
|
5189
|
+
/**
|
|
5190
|
+
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
5191
|
+
* @param selector an optional `MediaStreamTrack` to get the stats for.
|
|
5192
|
+
*/
|
|
5193
|
+
this.getStats = (selector) => {
|
|
5194
|
+
return this.pc.getStats(selector);
|
|
5195
|
+
};
|
|
5196
|
+
/**
|
|
5197
|
+
* Handles the ICECandidate event and
|
|
5198
|
+
* Initiates an ICE Trickle process with the SFU.
|
|
5199
|
+
*/
|
|
5200
|
+
this.onIceCandidate = (e) => {
|
|
5201
|
+
const { candidate } = e;
|
|
5202
|
+
if (!candidate) {
|
|
5203
|
+
this.logger('debug', 'null ice candidate');
|
|
5204
|
+
return;
|
|
5205
|
+
}
|
|
5206
|
+
const iceCandidate = this.toJSON(candidate);
|
|
5207
|
+
this.sfuClient
|
|
5208
|
+
.iceTrickle({ peerType: this.peerType, iceCandidate })
|
|
5209
|
+
.catch((err) => this.logger('warn', `ICETrickle failed`, err));
|
|
5210
|
+
};
|
|
5211
|
+
/**
|
|
5212
|
+
* Converts the ICE candidate to a JSON string.
|
|
5213
|
+
*/
|
|
5214
|
+
this.toJSON = (candidate) => {
|
|
5215
|
+
if (!candidate.usernameFragment) {
|
|
5216
|
+
// react-native-webrtc doesn't include usernameFragment in the candidate
|
|
5217
|
+
const segments = candidate.candidate.split(' ');
|
|
5218
|
+
const ufragIndex = segments.findIndex((s) => s === 'ufrag') + 1;
|
|
5219
|
+
const usernameFragment = segments[ufragIndex];
|
|
5220
|
+
return JSON.stringify({ ...candidate, usernameFragment });
|
|
5221
|
+
}
|
|
5222
|
+
return JSON.stringify(candidate.toJSON());
|
|
5223
|
+
};
|
|
5224
|
+
/**
|
|
5225
|
+
* Handles the ICE connection state change event.
|
|
5226
|
+
*/
|
|
5227
|
+
this.onIceConnectionStateChange = () => {
|
|
5228
|
+
const state = this.pc.iceConnectionState;
|
|
5229
|
+
this.logger('debug', `ICE connection state changed`, state);
|
|
5230
|
+
if (this.state.callingState === exports.CallingState.RECONNECTING)
|
|
5231
|
+
return;
|
|
5232
|
+
// do nothing when ICE is restarting
|
|
5233
|
+
if (this.isIceRestarting)
|
|
5234
|
+
return;
|
|
5235
|
+
if (state === 'failed' || state === 'disconnected') {
|
|
5236
|
+
this.logger('debug', `Attempting to restart ICE`);
|
|
5237
|
+
this.restartIce().catch((e) => {
|
|
5238
|
+
this.logger('error', `ICE restart failed`, e);
|
|
5239
|
+
this.onUnrecoverableError?.();
|
|
5240
|
+
});
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5243
|
+
/**
|
|
5244
|
+
* Handles the ICE candidate error event.
|
|
5245
|
+
*/
|
|
5246
|
+
this.onIceCandidateError = (e) => {
|
|
5247
|
+
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
5248
|
+
`${e.errorCode}: ${e.errorText}`;
|
|
5249
|
+
const iceState = this.pc.iceConnectionState;
|
|
5250
|
+
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
5251
|
+
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
5252
|
+
};
|
|
5253
|
+
/**
|
|
5254
|
+
* Handles the ICE gathering state change event.
|
|
5255
|
+
*/
|
|
5256
|
+
this.onIceGatherChange = () => {
|
|
5257
|
+
this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
5258
|
+
};
|
|
5259
|
+
/**
|
|
5260
|
+
* Handles the signaling state change event.
|
|
5261
|
+
*/
|
|
5262
|
+
this.onSignalingChange = () => {
|
|
5263
|
+
this.logger('debug', `Signaling state changed`, this.pc.signalingState);
|
|
5462
5264
|
};
|
|
5265
|
+
this.peerType = peerType;
|
|
5266
|
+
this.sfuClient = sfuClient;
|
|
5267
|
+
this.state = state;
|
|
5268
|
+
this.dispatcher = dispatcher;
|
|
5269
|
+
this.onUnrecoverableError = onUnrecoverableError;
|
|
5270
|
+
this.logger = getLogger([
|
|
5271
|
+
peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
|
|
5272
|
+
logTag,
|
|
5273
|
+
]);
|
|
5274
|
+
this.pc = new RTCPeerConnection(connectionConfig);
|
|
5275
|
+
this.pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
5276
|
+
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
5277
|
+
this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
5278
|
+
this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
5279
|
+
this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
5463
5280
|
}
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5281
|
+
/**
|
|
5282
|
+
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
5283
|
+
*/
|
|
5284
|
+
detachEventHandlers() {
|
|
5285
|
+
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
5286
|
+
this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
5287
|
+
this.pc.removeEventListener('signalingstatechange', this.onSignalingChange);
|
|
5288
|
+
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
5289
|
+
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
5290
|
+
this.unsubscribeIceTrickle?.();
|
|
5291
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
class TransceiverCache {
|
|
5296
|
+
constructor() {
|
|
5297
|
+
this.cache = [];
|
|
5298
|
+
this.layers = [];
|
|
5299
|
+
/**
|
|
5300
|
+
* An array maintaining the order how transceivers were added to the peer connection.
|
|
5301
|
+
* This is needed because some browsers (Firefox) don't reliably report
|
|
5302
|
+
* trackId and `mid` parameters.
|
|
5303
|
+
*/
|
|
5304
|
+
this.transceiverOrder = [];
|
|
5305
|
+
/**
|
|
5306
|
+
* Adds a transceiver to the cache.
|
|
5307
|
+
*/
|
|
5308
|
+
this.add = (publishOption, transceiver) => {
|
|
5309
|
+
this.cache.push({ publishOption, transceiver });
|
|
5310
|
+
this.transceiverOrder.push(transceiver);
|
|
5311
|
+
};
|
|
5312
|
+
/**
|
|
5313
|
+
* Gets the transceiver for the given publish option.
|
|
5314
|
+
*/
|
|
5315
|
+
this.get = (publishOption) => {
|
|
5316
|
+
return this.findTransceiver(publishOption)?.transceiver;
|
|
5317
|
+
};
|
|
5318
|
+
/**
|
|
5319
|
+
* Gets the last transceiver for the given track type and publish option id.
|
|
5320
|
+
*/
|
|
5321
|
+
this.getWith = (trackType, id) => {
|
|
5322
|
+
return this.findTransceiver({ trackType, id })?.transceiver;
|
|
5323
|
+
};
|
|
5324
|
+
/**
|
|
5325
|
+
* Checks if the cache has the given publish option.
|
|
5326
|
+
*/
|
|
5327
|
+
this.has = (publishOption) => {
|
|
5328
|
+
return !!this.get(publishOption);
|
|
5329
|
+
};
|
|
5330
|
+
/**
|
|
5331
|
+
* Finds the first transceiver that satisfies the given predicate.
|
|
5332
|
+
*/
|
|
5333
|
+
this.find = (predicate) => {
|
|
5334
|
+
return this.cache.find(predicate);
|
|
5335
|
+
};
|
|
5336
|
+
/**
|
|
5337
|
+
* Provides all the items in the cache.
|
|
5338
|
+
*/
|
|
5339
|
+
this.items = () => {
|
|
5340
|
+
return this.cache;
|
|
5341
|
+
};
|
|
5342
|
+
/**
|
|
5343
|
+
* Init index of the transceiver in the cache.
|
|
5344
|
+
*/
|
|
5345
|
+
this.indexOf = (transceiver) => {
|
|
5346
|
+
return this.transceiverOrder.indexOf(transceiver);
|
|
5475
5347
|
};
|
|
5348
|
+
/**
|
|
5349
|
+
* Gets cached video layers for the given track.
|
|
5350
|
+
*/
|
|
5351
|
+
this.getLayers = (publishOption) => {
|
|
5352
|
+
const entry = this.layers.find((item) => item.publishOption.id === publishOption.id &&
|
|
5353
|
+
item.publishOption.trackType === publishOption.trackType);
|
|
5354
|
+
return entry?.layers;
|
|
5355
|
+
};
|
|
5356
|
+
/**
|
|
5357
|
+
* Sets the video layers for the given track.
|
|
5358
|
+
*/
|
|
5359
|
+
this.setLayers = (publishOption, layers = []) => {
|
|
5360
|
+
const entry = this.findLayer(publishOption);
|
|
5361
|
+
if (entry) {
|
|
5362
|
+
entry.layers = layers;
|
|
5363
|
+
}
|
|
5364
|
+
else {
|
|
5365
|
+
this.layers.push({ publishOption, layers });
|
|
5366
|
+
}
|
|
5367
|
+
};
|
|
5368
|
+
this.findTransceiver = (publishOption) => {
|
|
5369
|
+
return this.cache.find((item) => item.publishOption.id === publishOption.id &&
|
|
5370
|
+
item.publishOption.trackType === publishOption.trackType);
|
|
5371
|
+
};
|
|
5372
|
+
this.findLayer = (publishOption) => {
|
|
5373
|
+
return this.layers.find((item) => item.publishOption.id === publishOption.id &&
|
|
5374
|
+
item.publishOption.trackType === publishOption.trackType);
|
|
5375
|
+
};
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
|
|
5379
|
+
const ensureExhausted = (x, message) => {
|
|
5380
|
+
getLogger(['helpers'])('warn', message, x);
|
|
5381
|
+
};
|
|
5382
|
+
|
|
5383
|
+
const trackTypeToParticipantStreamKey = (trackType) => {
|
|
5384
|
+
switch (trackType) {
|
|
5385
|
+
case TrackType.SCREEN_SHARE:
|
|
5386
|
+
return 'screenShareStream';
|
|
5387
|
+
case TrackType.SCREEN_SHARE_AUDIO:
|
|
5388
|
+
return 'screenShareAudioStream';
|
|
5389
|
+
case TrackType.VIDEO:
|
|
5390
|
+
return 'videoStream';
|
|
5391
|
+
case TrackType.AUDIO:
|
|
5392
|
+
return 'audioStream';
|
|
5393
|
+
case TrackType.UNSPECIFIED:
|
|
5394
|
+
throw new Error('Track type is unspecified');
|
|
5395
|
+
default:
|
|
5396
|
+
ensureExhausted(trackType, 'Unknown track type');
|
|
5476
5397
|
}
|
|
5477
5398
|
};
|
|
5399
|
+
const muteTypeToTrackType = (muteType) => {
|
|
5400
|
+
switch (muteType) {
|
|
5401
|
+
case 'audio':
|
|
5402
|
+
return TrackType.AUDIO;
|
|
5403
|
+
case 'video':
|
|
5404
|
+
return TrackType.VIDEO;
|
|
5405
|
+
case 'screenshare':
|
|
5406
|
+
return TrackType.SCREEN_SHARE;
|
|
5407
|
+
case 'screenshare_audio':
|
|
5408
|
+
return TrackType.SCREEN_SHARE_AUDIO;
|
|
5409
|
+
default:
|
|
5410
|
+
ensureExhausted(muteType, 'Unknown mute type');
|
|
5411
|
+
}
|
|
5412
|
+
};
|
|
5413
|
+
const toTrackType = (trackType) => {
|
|
5414
|
+
switch (trackType) {
|
|
5415
|
+
case 'TRACK_TYPE_AUDIO':
|
|
5416
|
+
return TrackType.AUDIO;
|
|
5417
|
+
case 'TRACK_TYPE_VIDEO':
|
|
5418
|
+
return TrackType.VIDEO;
|
|
5419
|
+
case 'TRACK_TYPE_SCREEN_SHARE':
|
|
5420
|
+
return TrackType.SCREEN_SHARE;
|
|
5421
|
+
case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
|
|
5422
|
+
return TrackType.SCREEN_SHARE_AUDIO;
|
|
5423
|
+
default:
|
|
5424
|
+
return undefined;
|
|
5425
|
+
}
|
|
5426
|
+
};
|
|
5427
|
+
const isAudioTrackType = (trackType) => trackType === TrackType.AUDIO || trackType === TrackType.SCREEN_SHARE_AUDIO;
|
|
5428
|
+
|
|
5429
|
+
const defaultBitratePerRid = {
|
|
5430
|
+
q: 300000,
|
|
5431
|
+
h: 750000,
|
|
5432
|
+
f: 1250000,
|
|
5433
|
+
};
|
|
5478
5434
|
/**
|
|
5479
|
-
*
|
|
5480
|
-
*
|
|
5481
|
-
*
|
|
5435
|
+
* In SVC, we need to send only one video encoding (layer).
|
|
5436
|
+
* this layer will have the additional spatial and temporal layers
|
|
5437
|
+
* defined via the scalabilityMode property.
|
|
5438
|
+
*
|
|
5439
|
+
* @param layers the layers to process.
|
|
5482
5440
|
*/
|
|
5483
|
-
const
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
}
|
|
5441
|
+
const toSvcEncodings = (layers) => {
|
|
5442
|
+
if (!layers)
|
|
5443
|
+
return;
|
|
5444
|
+
// we take the highest quality layer, and we assign it to `q` encoder.
|
|
5445
|
+
const withRid = (rid) => (l) => l.rid === rid;
|
|
5446
|
+
const highestLayer = layers.find(withRid('f')) ||
|
|
5447
|
+
layers.find(withRid('h')) ||
|
|
5448
|
+
layers.find(withRid('q'));
|
|
5449
|
+
return [{ ...highestLayer, rid: 'q' }];
|
|
5493
5450
|
};
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5451
|
+
/**
|
|
5452
|
+
* Converts the rid to a video quality.
|
|
5453
|
+
*/
|
|
5454
|
+
const ridToVideoQuality = (rid) => {
|
|
5455
|
+
return rid === 'q'
|
|
5456
|
+
? VideoQuality.LOW_UNSPECIFIED
|
|
5457
|
+
: rid === 'h'
|
|
5458
|
+
? VideoQuality.MID
|
|
5459
|
+
: VideoQuality.HIGH; // default to HIGH
|
|
5460
|
+
};
|
|
5461
|
+
/**
|
|
5462
|
+
* Converts the given video layers to SFU video layers.
|
|
5463
|
+
*/
|
|
5464
|
+
const toVideoLayers = (layers = []) => {
|
|
5465
|
+
return layers.map((layer) => ({
|
|
5466
|
+
rid: layer.rid || '',
|
|
5467
|
+
bitrate: layer.maxBitrate || 0,
|
|
5468
|
+
fps: layer.maxFramerate || 0,
|
|
5469
|
+
quality: ridToVideoQuality(layer.rid || ''),
|
|
5470
|
+
videoDimension: { width: layer.width, height: layer.height },
|
|
5471
|
+
}));
|
|
5472
|
+
};
|
|
5473
|
+
/**
|
|
5474
|
+
* Converts the spatial and temporal layers to a scalability mode.
|
|
5475
|
+
*/
|
|
5476
|
+
const toScalabilityMode = (spatialLayers, temporalLayers) => `L${spatialLayers}T${temporalLayers}${spatialLayers > 1 ? '_KEY' : ''}`;
|
|
5477
|
+
/**
|
|
5478
|
+
* Determines the most optimal video layers for the given track.
|
|
5479
|
+
*
|
|
5480
|
+
* @param videoTrack the video track to find optimal layers for.
|
|
5481
|
+
* @param publishOption the publish options for the track.
|
|
5482
|
+
*/
|
|
5483
|
+
const computeVideoLayers = (videoTrack, publishOption) => {
|
|
5484
|
+
if (isAudioTrackType(publishOption.trackType))
|
|
5485
|
+
return;
|
|
5486
|
+
const optimalVideoLayers = [];
|
|
5487
|
+
const settings = videoTrack.getSettings();
|
|
5488
|
+
const { width = 0, height = 0 } = settings;
|
|
5489
|
+
const { bitrate, codec, fps, maxSpatialLayers = 3, maxTemporalLayers = 3, videoDimension = { width: 1280, height: 720 }, } = publishOption;
|
|
5490
|
+
const maxBitrate = getComputedMaxBitrate(videoDimension, width, height, bitrate);
|
|
5491
|
+
let downscaleFactor = 1;
|
|
5492
|
+
let bitrateFactor = 1;
|
|
5493
|
+
const svcCodec = isSvcCodec(codec?.name);
|
|
5494
|
+
for (const rid of ['f', 'h', 'q'].slice(0, maxSpatialLayers)) {
|
|
5495
|
+
const layer = {
|
|
5496
|
+
active: true,
|
|
5497
|
+
rid,
|
|
5498
|
+
width: Math.round(width / downscaleFactor),
|
|
5499
|
+
height: Math.round(height / downscaleFactor),
|
|
5500
|
+
maxBitrate: maxBitrate / bitrateFactor || defaultBitratePerRid[rid],
|
|
5501
|
+
maxFramerate: fps,
|
|
5502
|
+
};
|
|
5503
|
+
if (svcCodec) {
|
|
5504
|
+
// for SVC codecs, we need to set the scalability mode, and the
|
|
5505
|
+
// codec will handle the rest (layers, temporal layers, etc.)
|
|
5506
|
+
layer.scalabilityMode = toScalabilityMode(maxSpatialLayers, maxTemporalLayers);
|
|
5514
5507
|
}
|
|
5515
|
-
else
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
if (rtpMapLine) {
|
|
5519
|
-
rtpMap.push(rtpMapLine);
|
|
5520
|
-
}
|
|
5521
|
-
else if (fmtpLine) {
|
|
5522
|
-
fmtp.push(fmtpLine);
|
|
5523
|
-
}
|
|
5508
|
+
else {
|
|
5509
|
+
// for non-SVC codecs, we need to downscale proportionally (simulcast)
|
|
5510
|
+
layer.scaleResolutionDownBy = downscaleFactor;
|
|
5524
5511
|
}
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
};
|
|
5512
|
+
downscaleFactor *= 2;
|
|
5513
|
+
bitrateFactor *= 2;
|
|
5514
|
+
// Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
|
|
5515
|
+
// when deciding which layer to disable when CPU or bandwidth is constrained.
|
|
5516
|
+
// Encodings should be ordered in increasing spatial resolution order.
|
|
5517
|
+
optimalVideoLayers.unshift(layer);
|
|
5532
5518
|
}
|
|
5519
|
+
// for simplicity, we start with all layers enabled, then this function
|
|
5520
|
+
// will clear/reassign the layers that are not needed
|
|
5521
|
+
return withSimulcastConstraints(settings, optimalVideoLayers);
|
|
5533
5522
|
};
|
|
5534
5523
|
/**
|
|
5535
|
-
*
|
|
5524
|
+
* Computes the maximum bitrate for a given resolution.
|
|
5525
|
+
* If the current resolution is lower than the target resolution,
|
|
5526
|
+
* we want to proportionally reduce the target bitrate.
|
|
5527
|
+
* If the current resolution is higher than the target resolution,
|
|
5528
|
+
* we want to use the target bitrate.
|
|
5529
|
+
*
|
|
5530
|
+
* @param targetResolution the target resolution.
|
|
5531
|
+
* @param currentWidth the current width of the track.
|
|
5532
|
+
* @param currentHeight the current height of the track.
|
|
5533
|
+
* @param bitrate the target bitrate.
|
|
5536
5534
|
*/
|
|
5537
|
-
const
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
const
|
|
5541
|
-
if (
|
|
5542
|
-
|
|
5535
|
+
const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, bitrate) => {
|
|
5536
|
+
// if the current resolution is lower than the target resolution,
|
|
5537
|
+
// we want to proportionally reduce the target bitrate
|
|
5538
|
+
const { width: targetWidth, height: targetHeight } = targetResolution;
|
|
5539
|
+
if (currentWidth < targetWidth || currentHeight < targetHeight) {
|
|
5540
|
+
const currentPixels = currentWidth * currentHeight;
|
|
5541
|
+
const targetPixels = targetWidth * targetHeight;
|
|
5542
|
+
const reductionFactor = currentPixels / targetPixels;
|
|
5543
|
+
return Math.round(bitrate * reductionFactor);
|
|
5543
5544
|
}
|
|
5545
|
+
return bitrate;
|
|
5544
5546
|
};
|
|
5545
5547
|
/**
|
|
5546
|
-
*
|
|
5547
|
-
*/
|
|
5548
|
-
const toggleDtx = (sdp, enable) => {
|
|
5549
|
-
const opusFmtp = getOpusFmtp(sdp);
|
|
5550
|
-
if (!opusFmtp)
|
|
5551
|
-
return sdp;
|
|
5552
|
-
const matchDtx = /usedtx=(\d)/.exec(opusFmtp.config);
|
|
5553
|
-
const requiredDtxConfig = `usedtx=${enable ? '1' : '0'}`;
|
|
5554
|
-
const newFmtp = matchDtx
|
|
5555
|
-
? opusFmtp.original.replace(/usedtx=(\d)/, requiredDtxConfig)
|
|
5556
|
-
: `${opusFmtp.original};${requiredDtxConfig}`;
|
|
5557
|
-
return sdp.replace(opusFmtp.original, newFmtp);
|
|
5558
|
-
};
|
|
5559
|
-
/**
|
|
5560
|
-
* Returns and SDP with all the codecs except the given codec removed.
|
|
5561
|
-
*/
|
|
5562
|
-
const preserveCodec = (sdp, mid, codec) => {
|
|
5563
|
-
const [kind, codecName] = codec.mimeType.toLowerCase().split('/');
|
|
5564
|
-
const toSet = (fmtpLine) => new Set(fmtpLine.split(';').map((f) => f.trim().toLowerCase()));
|
|
5565
|
-
const equal = (a, b) => {
|
|
5566
|
-
if (a.size !== b.size)
|
|
5567
|
-
return false;
|
|
5568
|
-
for (const item of a)
|
|
5569
|
-
if (!b.has(item))
|
|
5570
|
-
return false;
|
|
5571
|
-
return true;
|
|
5572
|
-
};
|
|
5573
|
-
const codecFmtp = toSet(codec.sdpFmtpLine || '');
|
|
5574
|
-
const parsedSdp = SDP__namespace.parse(sdp);
|
|
5575
|
-
for (const media of parsedSdp.media) {
|
|
5576
|
-
if (media.type !== kind || String(media.mid) !== mid)
|
|
5577
|
-
continue;
|
|
5578
|
-
// find the payload id of the desired codec
|
|
5579
|
-
const payloads = new Set();
|
|
5580
|
-
for (const rtp of media.rtp) {
|
|
5581
|
-
if (rtp.codec.toLowerCase() !== codecName)
|
|
5582
|
-
continue;
|
|
5583
|
-
const match =
|
|
5584
|
-
// vp8 doesn't have any fmtp, we preserve it without any additional checks
|
|
5585
|
-
codecName === 'vp8'
|
|
5586
|
-
? true
|
|
5587
|
-
: media.fmtp.some((f) => f.payload === rtp.payload && equal(toSet(f.config), codecFmtp));
|
|
5588
|
-
if (match) {
|
|
5589
|
-
payloads.add(rtp.payload);
|
|
5590
|
-
}
|
|
5591
|
-
}
|
|
5592
|
-
// find the corresponding rtx codec by matching apt=<preserved-codec-payload>
|
|
5593
|
-
for (const fmtp of media.fmtp) {
|
|
5594
|
-
const match = fmtp.config.match(/(apt)=(\d+)/);
|
|
5595
|
-
if (!match)
|
|
5596
|
-
continue;
|
|
5597
|
-
const [, , preservedCodecPayload] = match;
|
|
5598
|
-
if (payloads.has(Number(preservedCodecPayload))) {
|
|
5599
|
-
payloads.add(fmtp.payload);
|
|
5600
|
-
}
|
|
5601
|
-
}
|
|
5602
|
-
media.rtp = media.rtp.filter((r) => payloads.has(r.payload));
|
|
5603
|
-
media.fmtp = media.fmtp.filter((f) => payloads.has(f.payload));
|
|
5604
|
-
media.rtcpFb = media.rtcpFb?.filter((f) => payloads.has(f.payload));
|
|
5605
|
-
media.payloads = Array.from(payloads).join(' ');
|
|
5606
|
-
}
|
|
5607
|
-
return SDP__namespace.write(parsedSdp);
|
|
5608
|
-
};
|
|
5609
|
-
/**
|
|
5610
|
-
* Enables high-quality audio through SDP munging for the given trackMid.
|
|
5548
|
+
* Browsers have different simulcast constraints for different video resolutions.
|
|
5611
5549
|
*
|
|
5612
|
-
*
|
|
5613
|
-
*
|
|
5614
|
-
*
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
const
|
|
5620
|
-
if (
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
if (!opusRtp)
|
|
5624
|
-
return sdp;
|
|
5625
|
-
const opusFmtp = audioMedia.fmtp.find((f) => f.payload === opusRtp.payload);
|
|
5626
|
-
if (!opusFmtp)
|
|
5627
|
-
return sdp;
|
|
5628
|
-
// enable stereo, if not already enabled
|
|
5629
|
-
if (opusFmtp.config.match(/stereo=(\d)/)) {
|
|
5630
|
-
opusFmtp.config = opusFmtp.config.replace(/stereo=(\d)/, 'stereo=1');
|
|
5631
|
-
}
|
|
5632
|
-
else {
|
|
5633
|
-
opusFmtp.config = `${opusFmtp.config};stereo=1`;
|
|
5550
|
+
* This function modifies the provided list of video layers according to the
|
|
5551
|
+
* current implementation of simulcast constraints in the Chromium based browsers.
|
|
5552
|
+
*
|
|
5553
|
+
* https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
|
|
5554
|
+
*/
|
|
5555
|
+
const withSimulcastConstraints = (settings, optimalVideoLayers) => {
|
|
5556
|
+
let layers;
|
|
5557
|
+
const size = Math.max(settings.width || 0, settings.height || 0);
|
|
5558
|
+
if (size <= 320) {
|
|
5559
|
+
// provide only one layer 320x240 (q), the one with the highest quality
|
|
5560
|
+
layers = optimalVideoLayers.filter((layer) => layer.rid === 'f');
|
|
5634
5561
|
}
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5562
|
+
else if (size <= 640) {
|
|
5563
|
+
// provide two layers, 160x120 (q) and 640x480 (h)
|
|
5564
|
+
layers = optimalVideoLayers.filter((layer) => layer.rid !== 'h');
|
|
5638
5565
|
}
|
|
5639
5566
|
else {
|
|
5640
|
-
|
|
5567
|
+
// provide three layers for sizes > 640x480
|
|
5568
|
+
layers = optimalVideoLayers;
|
|
5641
5569
|
}
|
|
5642
|
-
|
|
5570
|
+
const ridMapping = ['q', 'h', 'f'];
|
|
5571
|
+
return layers.map((layer, index) => ({
|
|
5572
|
+
...layer,
|
|
5573
|
+
rid: ridMapping[index], // reassign rid
|
|
5574
|
+
}));
|
|
5643
5575
|
};
|
|
5576
|
+
|
|
5644
5577
|
/**
|
|
5645
5578
|
* Extracts the mid from the transceiver or the SDP.
|
|
5646
5579
|
*
|
|
@@ -5652,9 +5585,9 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
|
5652
5585
|
if (transceiver.mid)
|
|
5653
5586
|
return transceiver.mid;
|
|
5654
5587
|
if (!sdp)
|
|
5655
|
-
return
|
|
5588
|
+
return String(transceiverInitIndex);
|
|
5656
5589
|
const track = transceiver.sender.track;
|
|
5657
|
-
const parsedSdp =
|
|
5590
|
+
const parsedSdp = sdpTransform.parse(sdp);
|
|
5658
5591
|
const media = parsedSdp.media.find((m) => {
|
|
5659
5592
|
return (m.type === track.kind &&
|
|
5660
5593
|
// if `msid` is not present, we assume that the track is the first one
|
|
@@ -5662,7 +5595,7 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
|
5662
5595
|
});
|
|
5663
5596
|
if (typeof media?.mid !== 'undefined')
|
|
5664
5597
|
return String(media.mid);
|
|
5665
|
-
if (transceiverInitIndex
|
|
5598
|
+
if (transceiverInitIndex < 0)
|
|
5666
5599
|
return '';
|
|
5667
5600
|
return String(transceiverInitIndex);
|
|
5668
5601
|
};
|
|
@@ -5672,164 +5605,87 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
|
5672
5605
|
*
|
|
5673
5606
|
* @internal
|
|
5674
5607
|
*/
|
|
5675
|
-
class Publisher {
|
|
5608
|
+
class Publisher extends BasePeerConnection {
|
|
5676
5609
|
/**
|
|
5677
5610
|
* Constructs a new `Publisher` instance.
|
|
5678
5611
|
*/
|
|
5679
|
-
constructor({
|
|
5680
|
-
|
|
5681
|
-
this.
|
|
5682
|
-
this.publishOptsForTrack = new Map();
|
|
5683
|
-
/**
|
|
5684
|
-
* An array maintaining the order how transceivers were added to the peer connection.
|
|
5685
|
-
* This is needed because some browsers (Firefox) don't reliably report
|
|
5686
|
-
* trackId and `mid` parameters.
|
|
5687
|
-
*
|
|
5688
|
-
* @internal
|
|
5689
|
-
*/
|
|
5690
|
-
this.transceiverInitOrder = [];
|
|
5691
|
-
this.isIceRestarting = false;
|
|
5692
|
-
this.createPeerConnection = (connectionConfig) => {
|
|
5693
|
-
const pc = new RTCPeerConnection(connectionConfig);
|
|
5694
|
-
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
5695
|
-
pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
5696
|
-
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
5697
|
-
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
5698
|
-
pc.addEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
5699
|
-
pc.addEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
5700
|
-
return pc;
|
|
5701
|
-
};
|
|
5702
|
-
/**
|
|
5703
|
-
* Closes the publisher PeerConnection and cleans up the resources.
|
|
5704
|
-
*/
|
|
5705
|
-
this.close = ({ stopTracks }) => {
|
|
5706
|
-
if (stopTracks) {
|
|
5707
|
-
this.stopPublishing();
|
|
5708
|
-
this.transceiverCache.clear();
|
|
5709
|
-
this.trackLayersCache.clear();
|
|
5710
|
-
}
|
|
5711
|
-
this.detachEventHandlers();
|
|
5712
|
-
this.pc.close();
|
|
5713
|
-
};
|
|
5714
|
-
/**
|
|
5715
|
-
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
5716
|
-
* This is useful when we want to replace the `RTCPeerConnection`
|
|
5717
|
-
* instance with a new one (in case of migration).
|
|
5718
|
-
*/
|
|
5719
|
-
this.detachEventHandlers = () => {
|
|
5720
|
-
this.unsubscribeOnIceRestart();
|
|
5721
|
-
this.unsubscribeChangePublishQuality();
|
|
5722
|
-
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
5723
|
-
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
5724
|
-
this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
5725
|
-
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
5726
|
-
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
5727
|
-
this.pc.removeEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
5728
|
-
};
|
|
5612
|
+
constructor({ publishOptions, ...baseOptions }) {
|
|
5613
|
+
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
5614
|
+
this.transceiverCache = new TransceiverCache();
|
|
5729
5615
|
/**
|
|
5730
5616
|
* Starts publishing the given track of the given media stream.
|
|
5731
5617
|
*
|
|
5732
5618
|
* Consecutive calls to this method will replace the stream.
|
|
5733
5619
|
* The previous stream will be stopped.
|
|
5734
5620
|
*
|
|
5735
|
-
* @param mediaStream the media stream to publish.
|
|
5736
5621
|
* @param track the track to publish.
|
|
5737
5622
|
* @param trackType the track type to publish.
|
|
5738
|
-
* @param opts the optional publish options to use.
|
|
5739
5623
|
*/
|
|
5740
|
-
this.
|
|
5741
|
-
if (
|
|
5742
|
-
throw new Error(`
|
|
5743
|
-
}
|
|
5744
|
-
// enable the track if it is disabled
|
|
5745
|
-
if (!track.enabled)
|
|
5746
|
-
track.enabled = true;
|
|
5747
|
-
const transceiver = this.transceiverCache.get(trackType);
|
|
5748
|
-
if (!transceiver || !transceiver.sender.track) {
|
|
5749
|
-
// listen for 'ended' event on the track as it might be ended abruptly
|
|
5750
|
-
// by an external factors such as permission revokes, a disconnected device, etc.
|
|
5751
|
-
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
5752
|
-
const handleTrackEnded = () => {
|
|
5753
|
-
this.logger('info', `Track ${TrackType[trackType]} has ended abruptly`);
|
|
5754
|
-
track.removeEventListener('ended', handleTrackEnded);
|
|
5755
|
-
this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch((err) => this.logger('warn', `Couldn't notify track mute state`, err));
|
|
5756
|
-
};
|
|
5757
|
-
track.addEventListener('ended', handleTrackEnded);
|
|
5758
|
-
this.addTransceiver(trackType, track, opts, mediaStream);
|
|
5624
|
+
this.publish = async (track, trackType) => {
|
|
5625
|
+
if (!this.publishOptions.some((o) => o.trackType === trackType)) {
|
|
5626
|
+
throw new Error(`No publish options found for ${TrackType[trackType]}`);
|
|
5759
5627
|
}
|
|
5760
|
-
|
|
5761
|
-
|
|
5628
|
+
for (const publishOption of this.publishOptions) {
|
|
5629
|
+
if (publishOption.trackType !== trackType)
|
|
5630
|
+
continue;
|
|
5631
|
+
// create a clone of the track as otherwise the same trackId will
|
|
5632
|
+
// appear in the SDP in multiple transceivers
|
|
5633
|
+
const trackToPublish = track.clone();
|
|
5634
|
+
const transceiver = this.transceiverCache.get(publishOption);
|
|
5635
|
+
if (!transceiver) {
|
|
5636
|
+
this.addTransceiver(trackToPublish, publishOption);
|
|
5637
|
+
}
|
|
5638
|
+
else {
|
|
5639
|
+
await transceiver.sender.replaceTrack(trackToPublish);
|
|
5640
|
+
}
|
|
5762
5641
|
}
|
|
5763
|
-
await this.notifyTrackMuteStateChanged(mediaStream, trackType, false);
|
|
5764
5642
|
};
|
|
5765
5643
|
/**
|
|
5766
|
-
* Adds a new transceiver to the peer connection.
|
|
5767
|
-
* This needs to be called when a new track kind is added to the peer connection.
|
|
5768
|
-
* In other cases, use `updateTransceiver` method.
|
|
5644
|
+
* Adds a new transceiver carrying the given track to the peer connection.
|
|
5769
5645
|
*/
|
|
5770
|
-
this.addTransceiver = (
|
|
5771
|
-
const
|
|
5772
|
-
const
|
|
5773
|
-
|
|
5646
|
+
this.addTransceiver = (track, publishOption) => {
|
|
5647
|
+
const videoEncodings = computeVideoLayers(track, publishOption);
|
|
5648
|
+
const sendEncodings = isSvcCodec(publishOption.codec?.name)
|
|
5649
|
+
? toSvcEncodings(videoEncodings)
|
|
5650
|
+
: videoEncodings;
|
|
5774
5651
|
const transceiver = this.pc.addTransceiver(track, {
|
|
5775
5652
|
direction: 'sendonly',
|
|
5776
|
-
|
|
5777
|
-
? [mediaStream]
|
|
5778
|
-
: undefined,
|
|
5779
|
-
sendEncodings: isSvcCodec(codecInUse)
|
|
5780
|
-
? toSvcEncodings(videoEncodings)
|
|
5781
|
-
: videoEncodings,
|
|
5653
|
+
sendEncodings,
|
|
5782
5654
|
});
|
|
5655
|
+
const trackType = publishOption.trackType;
|
|
5783
5656
|
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
5784
|
-
this.
|
|
5785
|
-
this.transceiverCache.set(trackType, transceiver);
|
|
5786
|
-
this.publishOptsForTrack.set(trackType, opts);
|
|
5787
|
-
// handle codec preferences
|
|
5788
|
-
if (!('setCodecPreferences' in transceiver))
|
|
5789
|
-
return;
|
|
5790
|
-
const codecPreferences = this.getCodecPreferences(trackType, trackType === TrackType.VIDEO ? codecInUse : undefined, 'receiver');
|
|
5791
|
-
if (!codecPreferences)
|
|
5792
|
-
return;
|
|
5793
|
-
try {
|
|
5794
|
-
this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
|
|
5795
|
-
transceiver.setCodecPreferences(codecPreferences);
|
|
5796
|
-
}
|
|
5797
|
-
catch (err) {
|
|
5798
|
-
this.logger('warn', `Couldn't set codec preferences`, err);
|
|
5799
|
-
}
|
|
5800
|
-
};
|
|
5801
|
-
/**
|
|
5802
|
-
* Updates the given transceiver with the new track.
|
|
5803
|
-
* Stops the previous track and replaces it with the new one.
|
|
5804
|
-
*/
|
|
5805
|
-
this.updateTransceiver = async (transceiver, track) => {
|
|
5806
|
-
const previousTrack = transceiver.sender.track;
|
|
5807
|
-
// don't stop the track if we are re-publishing the same track
|
|
5808
|
-
if (previousTrack && previousTrack !== track) {
|
|
5809
|
-
previousTrack.stop();
|
|
5810
|
-
}
|
|
5811
|
-
await transceiver.sender.replaceTrack(track);
|
|
5657
|
+
this.transceiverCache.add(publishOption, transceiver);
|
|
5812
5658
|
};
|
|
5813
5659
|
/**
|
|
5814
|
-
*
|
|
5815
|
-
* Underlying track will be stopped and removed from the publisher.
|
|
5816
|
-
* @param trackType the track type to unpublish.
|
|
5817
|
-
* @param stopTrack specifies whether track should be stopped or just disabled
|
|
5660
|
+
* Synchronizes the current Publisher state with the provided publish options.
|
|
5818
5661
|
*/
|
|
5819
|
-
this.
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
(
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5662
|
+
this.syncPublishOptions = async () => {
|
|
5663
|
+
// enable publishing with new options -> [av1, vp9]
|
|
5664
|
+
for (const publishOption of this.publishOptions) {
|
|
5665
|
+
const { trackType } = publishOption;
|
|
5666
|
+
if (!this.isPublishing(trackType))
|
|
5667
|
+
continue;
|
|
5668
|
+
if (this.transceiverCache.has(publishOption))
|
|
5669
|
+
continue;
|
|
5670
|
+
const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
|
|
5671
|
+
i.publishOption.trackType === trackType);
|
|
5672
|
+
if (!item || !item.transceiver)
|
|
5673
|
+
continue;
|
|
5674
|
+
// take the track from the existing transceiver for the same track type,
|
|
5675
|
+
// clone it and publish it with the new publish options
|
|
5676
|
+
const track = item.transceiver.sender.track.clone();
|
|
5677
|
+
this.addTransceiver(track, publishOption);
|
|
5678
|
+
}
|
|
5679
|
+
// stop publishing with options not required anymore -> [vp9]
|
|
5680
|
+
for (const item of this.transceiverCache.items()) {
|
|
5681
|
+
const { publishOption, transceiver } = item;
|
|
5682
|
+
const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
|
|
5683
|
+
option.trackType === publishOption.trackType);
|
|
5684
|
+
if (hasPublishOption)
|
|
5685
|
+
continue;
|
|
5686
|
+
// it is safe to stop the track here, it is a clone
|
|
5687
|
+
transceiver.sender.track?.stop();
|
|
5688
|
+
await transceiver.sender.replaceTrack(null);
|
|
5833
5689
|
}
|
|
5834
5690
|
};
|
|
5835
5691
|
/**
|
|
@@ -5838,57 +5694,52 @@ class Publisher {
|
|
|
5838
5694
|
* @param trackType the track type to check.
|
|
5839
5695
|
*/
|
|
5840
5696
|
this.isPublishing = (trackType) => {
|
|
5841
|
-
const
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
const audioOrVideoOrScreenShareStream = trackTypeToParticipantStreamKey(trackType);
|
|
5850
|
-
if (!audioOrVideoOrScreenShareStream)
|
|
5851
|
-
return;
|
|
5852
|
-
if (isMuted) {
|
|
5853
|
-
this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
|
|
5854
|
-
publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
|
|
5855
|
-
[audioOrVideoOrScreenShareStream]: undefined,
|
|
5856
|
-
}));
|
|
5857
|
-
}
|
|
5858
|
-
else {
|
|
5859
|
-
this.state.updateParticipant(this.sfuClient.sessionId, (p) => {
|
|
5860
|
-
return {
|
|
5861
|
-
publishedTracks: p.publishedTracks.includes(trackType)
|
|
5862
|
-
? p.publishedTracks
|
|
5863
|
-
: [...p.publishedTracks, trackType],
|
|
5864
|
-
[audioOrVideoOrScreenShareStream]: mediaStream,
|
|
5865
|
-
};
|
|
5866
|
-
});
|
|
5697
|
+
for (const item of this.transceiverCache.items()) {
|
|
5698
|
+
if (item.publishOption.trackType !== trackType)
|
|
5699
|
+
continue;
|
|
5700
|
+
const track = item.transceiver.sender.track;
|
|
5701
|
+
if (!track)
|
|
5702
|
+
continue;
|
|
5703
|
+
if (track.readyState === 'live' && track.enabled)
|
|
5704
|
+
return true;
|
|
5867
5705
|
}
|
|
5706
|
+
return false;
|
|
5868
5707
|
};
|
|
5869
5708
|
/**
|
|
5870
|
-
*
|
|
5709
|
+
* Maps the given track ID to the corresponding track type.
|
|
5871
5710
|
*/
|
|
5872
|
-
this.
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
this.pc.removeTrack(s);
|
|
5711
|
+
this.getTrackType = (trackId) => {
|
|
5712
|
+
for (const transceiverId of this.transceiverCache.items()) {
|
|
5713
|
+
const { publishOption, transceiver } = transceiverId;
|
|
5714
|
+
if (transceiver.sender.track?.id === trackId) {
|
|
5715
|
+
return publishOption.trackType;
|
|
5878
5716
|
}
|
|
5879
|
-
}
|
|
5717
|
+
}
|
|
5718
|
+
return undefined;
|
|
5880
5719
|
};
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5720
|
+
/**
|
|
5721
|
+
* Stops the cloned track that is being published to the SFU.
|
|
5722
|
+
*/
|
|
5723
|
+
this.stopTracks = (...trackTypes) => {
|
|
5724
|
+
for (const item of this.transceiverCache.items()) {
|
|
5725
|
+
const { publishOption, transceiver } = item;
|
|
5726
|
+
if (!trackTypes.includes(publishOption.trackType))
|
|
5727
|
+
continue;
|
|
5728
|
+
transceiver.sender.track?.stop();
|
|
5729
|
+
}
|
|
5730
|
+
};
|
|
5731
|
+
this.changePublishQuality = async (videoSender) => {
|
|
5732
|
+
const { trackType, layers, publishOptionId } = videoSender;
|
|
5733
|
+
const enabledLayers = layers.filter((l) => l.active);
|
|
5734
|
+
const tag = 'Update publish quality:';
|
|
5735
|
+
this.logger('info', `${tag} requested layers by SFU:`, enabledLayers);
|
|
5736
|
+
const sender = this.transceiverCache.getWith(trackType, publishOptionId)?.sender;
|
|
5737
|
+
if (!sender) {
|
|
5738
|
+
return this.logger('warn', `${tag} no video sender found.`);
|
|
5887
5739
|
}
|
|
5888
|
-
const params =
|
|
5740
|
+
const params = sender.getParameters();
|
|
5889
5741
|
if (params.encodings.length === 0) {
|
|
5890
|
-
this.logger('warn',
|
|
5891
|
-
return;
|
|
5742
|
+
return this.logger('warn', `${tag} there are no encodings set.`);
|
|
5892
5743
|
}
|
|
5893
5744
|
const [codecInUse] = params.codecs;
|
|
5894
5745
|
const usesSvcCodec = codecInUse && isSvcCodec(codecInUse.mimeType);
|
|
@@ -5930,54 +5781,12 @@ class Publisher {
|
|
|
5930
5781
|
changed = true;
|
|
5931
5782
|
}
|
|
5932
5783
|
}
|
|
5933
|
-
const
|
|
5784
|
+
const activeEncoders = params.encodings.filter((e) => e.active);
|
|
5934
5785
|
if (!changed) {
|
|
5935
|
-
this.logger('info',
|
|
5936
|
-
return;
|
|
5937
|
-
}
|
|
5938
|
-
await videoSender.setParameters(params);
|
|
5939
|
-
this.logger('info', `Update publish quality, enabled rids:`, activeLayers);
|
|
5940
|
-
};
|
|
5941
|
-
/**
|
|
5942
|
-
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
5943
|
-
* @param selector
|
|
5944
|
-
* @returns
|
|
5945
|
-
*/
|
|
5946
|
-
this.getStats = (selector) => {
|
|
5947
|
-
return this.pc.getStats(selector);
|
|
5948
|
-
};
|
|
5949
|
-
this.getCodecPreferences = (trackType, preferredCodec, codecPreferencesSource) => {
|
|
5950
|
-
if (trackType === TrackType.VIDEO) {
|
|
5951
|
-
return getPreferredCodecs('video', preferredCodec || 'vp8', undefined, codecPreferencesSource);
|
|
5952
|
-
}
|
|
5953
|
-
if (trackType === TrackType.AUDIO) {
|
|
5954
|
-
const defaultAudioCodec = this.isRedEnabled ? 'red' : 'opus';
|
|
5955
|
-
const codecToRemove = !this.isRedEnabled ? 'red' : undefined;
|
|
5956
|
-
return getPreferredCodecs('audio', preferredCodec ?? defaultAudioCodec, codecToRemove, codecPreferencesSource);
|
|
5957
|
-
}
|
|
5958
|
-
};
|
|
5959
|
-
this.onIceCandidate = (e) => {
|
|
5960
|
-
const { candidate } = e;
|
|
5961
|
-
if (!candidate) {
|
|
5962
|
-
this.logger('debug', 'null ice candidate');
|
|
5963
|
-
return;
|
|
5786
|
+
return this.logger('info', `${tag} no change:`, activeEncoders);
|
|
5964
5787
|
}
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
iceCandidate: getIceCandidate(candidate),
|
|
5968
|
-
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
5969
|
-
})
|
|
5970
|
-
.catch((err) => {
|
|
5971
|
-
this.logger('warn', `ICETrickle failed`, err);
|
|
5972
|
-
});
|
|
5973
|
-
};
|
|
5974
|
-
/**
|
|
5975
|
-
* Sets the SFU client to use.
|
|
5976
|
-
*
|
|
5977
|
-
* @param sfuClient the SFU client to use.
|
|
5978
|
-
*/
|
|
5979
|
-
this.setSfuClient = (sfuClient) => {
|
|
5980
|
-
this.sfuClient = sfuClient;
|
|
5788
|
+
await sender.setParameters(params);
|
|
5789
|
+
this.logger('info', `${tag} enabled rids:`, activeEncoders);
|
|
5981
5790
|
};
|
|
5982
5791
|
/**
|
|
5983
5792
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
@@ -5992,7 +5801,7 @@ class Publisher {
|
|
|
5992
5801
|
await this.negotiate({ iceRestart: true });
|
|
5993
5802
|
};
|
|
5994
5803
|
this.onNegotiationNeeded = () => {
|
|
5995
|
-
this.negotiate().catch((err) => {
|
|
5804
|
+
withoutConcurrency('publisher.negotiate', () => this.negotiate()).catch((err) => {
|
|
5996
5805
|
this.logger('error', `Negotiation failed.`, err);
|
|
5997
5806
|
this.onUnrecoverableError?.();
|
|
5998
5807
|
});
|
|
@@ -6004,18 +5813,6 @@ class Publisher {
|
|
|
6004
5813
|
*/
|
|
6005
5814
|
this.negotiate = async (options) => {
|
|
6006
5815
|
const offer = await this.pc.createOffer(options);
|
|
6007
|
-
if (offer.sdp) {
|
|
6008
|
-
offer.sdp = toggleDtx(offer.sdp, this.isDtxEnabled);
|
|
6009
|
-
if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
6010
|
-
offer.sdp = this.enableHighQualityAudio(offer.sdp);
|
|
6011
|
-
}
|
|
6012
|
-
if (this.isPublishing(TrackType.VIDEO)) {
|
|
6013
|
-
// Hotfix for platforms that don't respect the ordered codec list
|
|
6014
|
-
// (Firefox, Android, Linux, etc...).
|
|
6015
|
-
// We remove all the codecs from the SDP except the one we want to use.
|
|
6016
|
-
offer.sdp = this.removeUnpreferredCodecs(offer.sdp, TrackType.VIDEO);
|
|
6017
|
-
}
|
|
6018
|
-
}
|
|
6019
5816
|
const trackInfos = this.getAnnouncedTracks(offer.sdp);
|
|
6020
5817
|
if (trackInfos.length === 0) {
|
|
6021
5818
|
throw new Error(`Can't negotiate without announcing any tracks`);
|
|
@@ -6034,238 +5831,121 @@ class Publisher {
|
|
|
6034
5831
|
finally {
|
|
6035
5832
|
this.isIceRestarting = false;
|
|
6036
5833
|
}
|
|
6037
|
-
this.
|
|
6038
|
-
try {
|
|
6039
|
-
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
6040
|
-
await this.pc.addIceCandidate(iceCandidate);
|
|
6041
|
-
}
|
|
6042
|
-
catch (e) {
|
|
6043
|
-
this.logger('warn', `ICE candidate error`, e, candidate);
|
|
6044
|
-
}
|
|
6045
|
-
});
|
|
6046
|
-
};
|
|
6047
|
-
this.enableHighQualityAudio = (sdp) => {
|
|
6048
|
-
const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
|
|
6049
|
-
if (!transceiver)
|
|
6050
|
-
return sdp;
|
|
6051
|
-
const transceiverInitIndex = this.transceiverInitOrder.indexOf(TrackType.SCREEN_SHARE_AUDIO);
|
|
6052
|
-
const mid = extractMid(transceiver, transceiverInitIndex, sdp);
|
|
6053
|
-
return enableHighQualityAudio(sdp, mid);
|
|
5834
|
+
this.addTrickledIceCandidates();
|
|
6054
5835
|
};
|
|
6055
5836
|
/**
|
|
6056
5837
|
* Returns a list of tracks that are currently being published.
|
|
6057
|
-
*
|
|
6058
|
-
* @internal
|
|
6059
|
-
* @param sdp an optional SDP to extract the `mid` from.
|
|
6060
5838
|
*/
|
|
6061
|
-
this.
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
.getTransceivers()
|
|
6065
|
-
.filter((t) => t.direction === 'sendonly' && t.sender.track)
|
|
6066
|
-
.map((transceiver) => {
|
|
6067
|
-
let trackType;
|
|
6068
|
-
this.transceiverCache.forEach((value, key) => {
|
|
6069
|
-
if (value === transceiver)
|
|
6070
|
-
trackType = key;
|
|
6071
|
-
});
|
|
5839
|
+
this.getPublishedTracks = () => {
|
|
5840
|
+
const tracks = [];
|
|
5841
|
+
for (const { transceiver } of this.transceiverCache.items()) {
|
|
6072
5842
|
const track = transceiver.sender.track;
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
if (isTrackLive) {
|
|
6076
|
-
optimalLayers = this.computeLayers(trackType, track) || [];
|
|
6077
|
-
this.trackLayersCache.set(trackType, optimalLayers);
|
|
6078
|
-
}
|
|
6079
|
-
else {
|
|
6080
|
-
// we report the last known optimal layers for ended tracks
|
|
6081
|
-
optimalLayers = this.trackLayersCache.get(trackType) || [];
|
|
6082
|
-
this.logger('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
|
|
6083
|
-
}
|
|
6084
|
-
const layers = optimalLayers.map((optimalLayer) => ({
|
|
6085
|
-
rid: optimalLayer.rid || '',
|
|
6086
|
-
bitrate: optimalLayer.maxBitrate || 0,
|
|
6087
|
-
fps: optimalLayer.maxFramerate || 0,
|
|
6088
|
-
quality: ridToVideoQuality(optimalLayer.rid || ''),
|
|
6089
|
-
videoDimension: {
|
|
6090
|
-
width: optimalLayer.width,
|
|
6091
|
-
height: optimalLayer.height,
|
|
6092
|
-
},
|
|
6093
|
-
}));
|
|
6094
|
-
const isAudioTrack = [
|
|
6095
|
-
TrackType.AUDIO,
|
|
6096
|
-
TrackType.SCREEN_SHARE_AUDIO,
|
|
6097
|
-
].includes(trackType);
|
|
6098
|
-
const trackSettings = track.getSettings();
|
|
6099
|
-
const isStereo = isAudioTrack && trackSettings.channelCount === 2;
|
|
6100
|
-
const transceiverInitIndex = this.transceiverInitOrder.indexOf(trackType);
|
|
6101
|
-
return {
|
|
6102
|
-
trackId: track.id,
|
|
6103
|
-
layers: layers,
|
|
6104
|
-
trackType,
|
|
6105
|
-
mid: extractMid(transceiver, transceiverInitIndex, sdp),
|
|
6106
|
-
stereo: isStereo,
|
|
6107
|
-
dtx: isAudioTrack && this.isDtxEnabled,
|
|
6108
|
-
red: isAudioTrack && this.isRedEnabled,
|
|
6109
|
-
muted: !isTrackLive,
|
|
6110
|
-
};
|
|
6111
|
-
});
|
|
6112
|
-
};
|
|
6113
|
-
this.computeLayers = (trackType, track, opts) => {
|
|
6114
|
-
const { settings } = this.state;
|
|
6115
|
-
const targetResolution = settings?.video
|
|
6116
|
-
.target_resolution;
|
|
6117
|
-
const screenShareBitrate = settings?.screensharing.target_resolution?.bitrate;
|
|
6118
|
-
const publishOpts = opts || this.publishOptsForTrack.get(trackType);
|
|
6119
|
-
const codecInUse = opts?.forceCodec || getOptimalVideoCodec(opts?.preferredCodec);
|
|
6120
|
-
return trackType === TrackType.VIDEO
|
|
6121
|
-
? findOptimalVideoLayers(track, targetResolution, codecInUse, publishOpts)
|
|
6122
|
-
: trackType === TrackType.SCREEN_SHARE
|
|
6123
|
-
? findOptimalScreenSharingLayers(track, publishOpts, screenShareBitrate)
|
|
6124
|
-
: undefined;
|
|
6125
|
-
};
|
|
6126
|
-
this.onIceCandidateError = (e) => {
|
|
6127
|
-
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
6128
|
-
`${e.errorCode}: ${e.errorText}`;
|
|
6129
|
-
const iceState = this.pc.iceConnectionState;
|
|
6130
|
-
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
6131
|
-
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
6132
|
-
};
|
|
6133
|
-
this.onIceConnectionStateChange = () => {
|
|
6134
|
-
const state = this.pc.iceConnectionState;
|
|
6135
|
-
this.logger('debug', `ICE Connection state changed to`, state);
|
|
6136
|
-
if (this.state.callingState === exports.CallingState.RECONNECTING)
|
|
6137
|
-
return;
|
|
6138
|
-
if (state === 'failed' || state === 'disconnected') {
|
|
6139
|
-
this.logger('debug', `Attempting to restart ICE`);
|
|
6140
|
-
this.restartIce().catch((e) => {
|
|
6141
|
-
this.logger('error', `ICE restart error`, e);
|
|
6142
|
-
this.onUnrecoverableError?.();
|
|
6143
|
-
});
|
|
5843
|
+
if (track && track.readyState === 'live')
|
|
5844
|
+
tracks.push(track);
|
|
6144
5845
|
}
|
|
6145
|
-
|
|
6146
|
-
this.onIceGatheringStateChange = () => {
|
|
6147
|
-
this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
6148
|
-
};
|
|
6149
|
-
this.onSignalingStateChange = () => {
|
|
6150
|
-
this.logger('debug', `Signaling state changed`, this.pc.signalingState);
|
|
6151
|
-
};
|
|
6152
|
-
this.logger = getLogger(['Publisher', logTag]);
|
|
6153
|
-
this.pc = this.createPeerConnection(connectionConfig);
|
|
6154
|
-
this.sfuClient = sfuClient;
|
|
6155
|
-
this.state = state;
|
|
6156
|
-
this.isDtxEnabled = isDtxEnabled;
|
|
6157
|
-
this.isRedEnabled = isRedEnabled;
|
|
6158
|
-
this.onUnrecoverableError = onUnrecoverableError;
|
|
6159
|
-
this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
6160
|
-
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
6161
|
-
return;
|
|
6162
|
-
this.restartIce().catch((err) => {
|
|
6163
|
-
this.logger('warn', `ICERestart failed`, err);
|
|
6164
|
-
this.onUnrecoverableError?.();
|
|
6165
|
-
});
|
|
6166
|
-
});
|
|
6167
|
-
this.unsubscribeChangePublishQuality = dispatcher.on('changePublishQuality', ({ videoSenders }) => {
|
|
6168
|
-
withoutConcurrency('publisher.changePublishQuality', async () => {
|
|
6169
|
-
for (const videoSender of videoSenders) {
|
|
6170
|
-
const { layers } = videoSender;
|
|
6171
|
-
const enabledLayers = layers.filter((l) => l.active);
|
|
6172
|
-
await this.changePublishQuality(enabledLayers);
|
|
6173
|
-
}
|
|
6174
|
-
}).catch((err) => {
|
|
6175
|
-
this.logger('warn', 'Failed to change publish quality', err);
|
|
6176
|
-
});
|
|
6177
|
-
});
|
|
6178
|
-
}
|
|
6179
|
-
removeUnpreferredCodecs(sdp, trackType) {
|
|
6180
|
-
const opts = this.publishOptsForTrack.get(trackType);
|
|
6181
|
-
const forceSingleCodec = !!opts?.forceSingleCodec || isReactNative() || isFirefox();
|
|
6182
|
-
if (!opts || !forceSingleCodec)
|
|
6183
|
-
return sdp;
|
|
6184
|
-
const codec = opts.forceCodec || getOptimalVideoCodec(opts.preferredCodec);
|
|
6185
|
-
const orderedCodecs = this.getCodecPreferences(trackType, codec, 'sender');
|
|
6186
|
-
if (!orderedCodecs || orderedCodecs.length === 0)
|
|
6187
|
-
return sdp;
|
|
6188
|
-
const transceiver = this.transceiverCache.get(trackType);
|
|
6189
|
-
if (!transceiver)
|
|
6190
|
-
return sdp;
|
|
6191
|
-
const index = this.transceiverInitOrder.indexOf(trackType);
|
|
6192
|
-
const mid = extractMid(transceiver, index, sdp);
|
|
6193
|
-
const [codecToPreserve] = orderedCodecs;
|
|
6194
|
-
return preserveCodec(sdp, mid, codecToPreserve);
|
|
6195
|
-
}
|
|
6196
|
-
}
|
|
6197
|
-
|
|
6198
|
-
/**
|
|
6199
|
-
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
6200
|
-
* media streams from the SFU.
|
|
6201
|
-
*
|
|
6202
|
-
* @internal
|
|
6203
|
-
*/
|
|
6204
|
-
class Subscriber {
|
|
6205
|
-
/**
|
|
6206
|
-
* Constructs a new `Subscriber` instance.
|
|
6207
|
-
*
|
|
6208
|
-
* @param sfuClient the SFU client to use.
|
|
6209
|
-
* @param dispatcher the dispatcher to use.
|
|
6210
|
-
* @param state the state of the call.
|
|
6211
|
-
* @param connectionConfig the connection configuration to use.
|
|
6212
|
-
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
|
|
6213
|
-
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
6214
|
-
* @param logTag a tag to use for logging.
|
|
6215
|
-
*/
|
|
6216
|
-
constructor({ sfuClient, dispatcher, state, connectionConfig, onUnrecoverableError, logTag, }) {
|
|
6217
|
-
this.isIceRestarting = false;
|
|
6218
|
-
/**
|
|
6219
|
-
* Creates a new `RTCPeerConnection` instance with the given configuration.
|
|
6220
|
-
*
|
|
6221
|
-
* @param connectionConfig the connection configuration to use.
|
|
6222
|
-
*/
|
|
6223
|
-
this.createPeerConnection = (connectionConfig) => {
|
|
6224
|
-
const pc = new RTCPeerConnection(connectionConfig);
|
|
6225
|
-
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
6226
|
-
pc.addEventListener('track', this.handleOnTrack);
|
|
6227
|
-
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6228
|
-
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6229
|
-
pc.addEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
6230
|
-
return pc;
|
|
6231
|
-
};
|
|
6232
|
-
/**
|
|
6233
|
-
* Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
|
|
6234
|
-
*/
|
|
6235
|
-
this.close = () => {
|
|
6236
|
-
this.detachEventHandlers();
|
|
6237
|
-
this.pc.close();
|
|
5846
|
+
return tracks;
|
|
6238
5847
|
};
|
|
6239
5848
|
/**
|
|
6240
|
-
*
|
|
6241
|
-
*
|
|
6242
|
-
* instance with a new one (in case of migration).
|
|
5849
|
+
* Returns a list of tracks that are currently being published.
|
|
5850
|
+
* @param sdp an optional SDP to extract the `mid` from.
|
|
6243
5851
|
*/
|
|
6244
|
-
this.
|
|
6245
|
-
|
|
6246
|
-
this.
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
5852
|
+
this.getAnnouncedTracks = (sdp) => {
|
|
5853
|
+
const trackInfos = [];
|
|
5854
|
+
for (const bundle of this.transceiverCache.items()) {
|
|
5855
|
+
const { transceiver, publishOption } = bundle;
|
|
5856
|
+
const track = transceiver.sender.track;
|
|
5857
|
+
if (!track)
|
|
5858
|
+
continue;
|
|
5859
|
+
trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
|
|
5860
|
+
}
|
|
5861
|
+
return trackInfos;
|
|
6252
5862
|
};
|
|
6253
5863
|
/**
|
|
6254
|
-
* Returns
|
|
6255
|
-
*
|
|
6256
|
-
*
|
|
6257
|
-
*/
|
|
6258
|
-
this.
|
|
6259
|
-
|
|
5864
|
+
* Returns a list of tracks that are currently being published.
|
|
5865
|
+
* This method shall be used for the reconnection flow.
|
|
5866
|
+
* There we shouldn't announce the tracks that have been stopped due to a codec switch.
|
|
5867
|
+
*/
|
|
5868
|
+
this.getAnnouncedTracksForReconnect = () => {
|
|
5869
|
+
const sdp = this.pc.localDescription?.sdp;
|
|
5870
|
+
const trackInfos = [];
|
|
5871
|
+
for (const publishOption of this.publishOptions) {
|
|
5872
|
+
const transceiver = this.transceiverCache.get(publishOption);
|
|
5873
|
+
if (!transceiver || !transceiver.sender.track)
|
|
5874
|
+
continue;
|
|
5875
|
+
trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
|
|
5876
|
+
}
|
|
5877
|
+
return trackInfos;
|
|
6260
5878
|
};
|
|
6261
5879
|
/**
|
|
6262
|
-
*
|
|
6263
|
-
*
|
|
6264
|
-
* @param sfuClient the SFU client to use.
|
|
5880
|
+
* Converts the given transceiver to a `TrackInfo` object.
|
|
6265
5881
|
*/
|
|
6266
|
-
this.
|
|
6267
|
-
|
|
5882
|
+
this.toTrackInfo = (transceiver, publishOption, sdp) => {
|
|
5883
|
+
const track = transceiver.sender.track;
|
|
5884
|
+
const isTrackLive = track.readyState === 'live';
|
|
5885
|
+
const layers = isTrackLive
|
|
5886
|
+
? computeVideoLayers(track, publishOption)
|
|
5887
|
+
: this.transceiverCache.getLayers(publishOption);
|
|
5888
|
+
this.transceiverCache.setLayers(publishOption, layers);
|
|
5889
|
+
const isAudioTrack = isAudioTrackType(publishOption.trackType);
|
|
5890
|
+
const isStereo = isAudioTrack && track.getSettings().channelCount === 2;
|
|
5891
|
+
const transceiverIndex = this.transceiverCache.indexOf(transceiver);
|
|
5892
|
+
const audioSettings = this.state.settings?.audio;
|
|
5893
|
+
return {
|
|
5894
|
+
trackId: track.id,
|
|
5895
|
+
layers: toVideoLayers(layers),
|
|
5896
|
+
trackType: publishOption.trackType,
|
|
5897
|
+
mid: extractMid(transceiver, transceiverIndex, sdp),
|
|
5898
|
+
stereo: isStereo,
|
|
5899
|
+
dtx: isAudioTrack && !!audioSettings?.opus_dtx_enabled,
|
|
5900
|
+
red: isAudioTrack && !!audioSettings?.redundant_coding_enabled,
|
|
5901
|
+
muted: !isTrackLive,
|
|
5902
|
+
codec: publishOption.codec,
|
|
5903
|
+
publishOptionId: publishOption.id,
|
|
5904
|
+
};
|
|
6268
5905
|
};
|
|
5906
|
+
this.publishOptions = publishOptions;
|
|
5907
|
+
this.pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
5908
|
+
this.on('iceRestart', (iceRestart) => {
|
|
5909
|
+
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
5910
|
+
return;
|
|
5911
|
+
this.restartIce().catch((err) => {
|
|
5912
|
+
this.logger('warn', `ICERestart failed`, err);
|
|
5913
|
+
this.onUnrecoverableError?.();
|
|
5914
|
+
});
|
|
5915
|
+
});
|
|
5916
|
+
this.on('changePublishQuality', async (event) => {
|
|
5917
|
+
for (const videoSender of event.videoSenders) {
|
|
5918
|
+
await this.changePublishQuality(videoSender);
|
|
5919
|
+
}
|
|
5920
|
+
});
|
|
5921
|
+
this.on('changePublishOptions', (event) => {
|
|
5922
|
+
this.publishOptions = event.publishOptions;
|
|
5923
|
+
return this.syncPublishOptions();
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
/**
|
|
5927
|
+
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
5928
|
+
* This is useful when we want to replace the `RTCPeerConnection`
|
|
5929
|
+
* instance with a new one (in case of migration).
|
|
5930
|
+
*/
|
|
5931
|
+
detachEventHandlers() {
|
|
5932
|
+
super.detachEventHandlers();
|
|
5933
|
+
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
/**
|
|
5938
|
+
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
5939
|
+
* media streams from the SFU.
|
|
5940
|
+
*
|
|
5941
|
+
* @internal
|
|
5942
|
+
*/
|
|
5943
|
+
class Subscriber extends BasePeerConnection {
|
|
5944
|
+
/**
|
|
5945
|
+
* Constructs a new `Subscriber` instance.
|
|
5946
|
+
*/
|
|
5947
|
+
constructor(opts) {
|
|
5948
|
+
super(PeerType.SUBSCRIBER, opts);
|
|
6269
5949
|
/**
|
|
6270
5950
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
6271
5951
|
*/
|
|
@@ -6328,7 +6008,15 @@ class Subscriber {
|
|
|
6328
6008
|
this.logger('error', `Unknown track type: ${rawTrackType}`);
|
|
6329
6009
|
return;
|
|
6330
6010
|
}
|
|
6011
|
+
// get the previous stream to dispose it later
|
|
6012
|
+
// usually this happens during migration, when the stream is replaced
|
|
6013
|
+
// with a new one but the old one is still in the state
|
|
6331
6014
|
const previousStream = participantToUpdate[streamKindProp];
|
|
6015
|
+
// replace the previous stream with the new one, prevents flickering
|
|
6016
|
+
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
6017
|
+
[streamKindProp]: primaryStream,
|
|
6018
|
+
});
|
|
6019
|
+
// now, dispose the previous stream if it exists
|
|
6332
6020
|
if (previousStream) {
|
|
6333
6021
|
this.logger('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
6334
6022
|
previousStream.getTracks().forEach((t) => {
|
|
@@ -6336,24 +6024,6 @@ class Subscriber {
|
|
|
6336
6024
|
previousStream.removeTrack(t);
|
|
6337
6025
|
});
|
|
6338
6026
|
}
|
|
6339
|
-
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
6340
|
-
[streamKindProp]: primaryStream,
|
|
6341
|
-
});
|
|
6342
|
-
};
|
|
6343
|
-
this.onIceCandidate = (e) => {
|
|
6344
|
-
const { candidate } = e;
|
|
6345
|
-
if (!candidate) {
|
|
6346
|
-
this.logger('debug', 'null ice candidate');
|
|
6347
|
-
return;
|
|
6348
|
-
}
|
|
6349
|
-
this.sfuClient
|
|
6350
|
-
.iceTrickle({
|
|
6351
|
-
iceCandidate: getIceCandidate(candidate),
|
|
6352
|
-
peerType: PeerType.SUBSCRIBER,
|
|
6353
|
-
})
|
|
6354
|
-
.catch((err) => {
|
|
6355
|
-
this.logger('warn', `ICETrickle failed`, err);
|
|
6356
|
-
});
|
|
6357
6027
|
};
|
|
6358
6028
|
this.negotiate = async (subscriberOffer) => {
|
|
6359
6029
|
this.logger('info', `Received subscriberOffer`, subscriberOffer);
|
|
@@ -6361,15 +6031,7 @@ class Subscriber {
|
|
|
6361
6031
|
type: 'offer',
|
|
6362
6032
|
sdp: subscriberOffer.sdp,
|
|
6363
6033
|
});
|
|
6364
|
-
this.
|
|
6365
|
-
try {
|
|
6366
|
-
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
6367
|
-
await this.pc.addIceCandidate(iceCandidate);
|
|
6368
|
-
}
|
|
6369
|
-
catch (e) {
|
|
6370
|
-
this.logger('warn', `ICE candidate error`, [e, candidate]);
|
|
6371
|
-
}
|
|
6372
|
-
});
|
|
6034
|
+
this.addTrickledIceCandidates();
|
|
6373
6035
|
const answer = await this.pc.createAnswer();
|
|
6374
6036
|
await this.pc.setLocalDescription(answer);
|
|
6375
6037
|
await this.sfuClient.sendAnswer({
|
|
@@ -6378,56 +6040,21 @@ class Subscriber {
|
|
|
6378
6040
|
});
|
|
6379
6041
|
this.isIceRestarting = false;
|
|
6380
6042
|
};
|
|
6381
|
-
this.
|
|
6382
|
-
|
|
6383
|
-
this.
|
|
6384
|
-
if (this.state.callingState === exports.CallingState.RECONNECTING)
|
|
6385
|
-
return;
|
|
6386
|
-
// do nothing when ICE is restarting
|
|
6387
|
-
if (this.isIceRestarting)
|
|
6388
|
-
return;
|
|
6389
|
-
if (state === 'failed' || state === 'disconnected') {
|
|
6390
|
-
this.logger('debug', `Attempting to restart ICE`);
|
|
6391
|
-
this.restartIce().catch((e) => {
|
|
6392
|
-
this.logger('error', `ICE restart failed`, e);
|
|
6393
|
-
this.onUnrecoverableError?.();
|
|
6394
|
-
});
|
|
6395
|
-
}
|
|
6396
|
-
};
|
|
6397
|
-
this.onIceGatheringStateChange = () => {
|
|
6398
|
-
this.logger('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
|
|
6399
|
-
};
|
|
6400
|
-
this.onIceCandidateError = (e) => {
|
|
6401
|
-
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
6402
|
-
`${e.errorCode}: ${e.errorText}`;
|
|
6403
|
-
const iceState = this.pc.iceConnectionState;
|
|
6404
|
-
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
6405
|
-
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
6406
|
-
};
|
|
6407
|
-
this.logger = getLogger(['Subscriber', logTag]);
|
|
6408
|
-
this.sfuClient = sfuClient;
|
|
6409
|
-
this.state = state;
|
|
6410
|
-
this.onUnrecoverableError = onUnrecoverableError;
|
|
6411
|
-
this.pc = this.createPeerConnection(connectionConfig);
|
|
6412
|
-
const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
|
|
6413
|
-
this.unregisterOnSubscriberOffer = dispatcher.on('subscriberOffer', (subscriberOffer) => {
|
|
6414
|
-
withoutConcurrency(subscriberOfferConcurrencyTag, () => {
|
|
6415
|
-
return this.negotiate(subscriberOffer);
|
|
6416
|
-
}).catch((err) => {
|
|
6043
|
+
this.pc.addEventListener('track', this.handleOnTrack);
|
|
6044
|
+
this.on('subscriberOffer', async (subscriberOffer) => {
|
|
6045
|
+
return this.negotiate(subscriberOffer).catch((err) => {
|
|
6417
6046
|
this.logger('error', `Negotiation failed.`, err);
|
|
6418
6047
|
});
|
|
6419
6048
|
});
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
});
|
|
6430
|
-
});
|
|
6049
|
+
}
|
|
6050
|
+
/**
|
|
6051
|
+
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
6052
|
+
* This is useful when we want to replace the `RTCPeerConnection`
|
|
6053
|
+
* instance with a new one (in case of migration).
|
|
6054
|
+
*/
|
|
6055
|
+
detachEventHandlers() {
|
|
6056
|
+
super.detachEventHandlers();
|
|
6057
|
+
this.pc.removeEventListener('track', this.handleOnTrack);
|
|
6431
6058
|
}
|
|
6432
6059
|
}
|
|
6433
6060
|
|
|
@@ -6459,6 +6086,16 @@ const createWebSocketSignalChannel = (opts) => {
|
|
|
6459
6086
|
return ws;
|
|
6460
6087
|
};
|
|
6461
6088
|
|
|
6089
|
+
const toRtcConfiguration = (config) => {
|
|
6090
|
+
return {
|
|
6091
|
+
iceServers: config.map((ice) => ({
|
|
6092
|
+
urls: ice.urls,
|
|
6093
|
+
username: ice.username,
|
|
6094
|
+
credential: ice.password,
|
|
6095
|
+
})),
|
|
6096
|
+
};
|
|
6097
|
+
};
|
|
6098
|
+
|
|
6462
6099
|
/**
|
|
6463
6100
|
* Saving a long-lived reference to a promise that can reject can be unsafe,
|
|
6464
6101
|
* since rejecting the promise causes an unhandled rejection error (even if the
|
|
@@ -6743,6 +6380,7 @@ class StreamSfuClient {
|
|
|
6743
6380
|
clearTimeout(this.migrateAwayTimeout);
|
|
6744
6381
|
this.abortController.abort();
|
|
6745
6382
|
this.migrationTask?.resolve();
|
|
6383
|
+
this.iceTrickleBuffer.dispose();
|
|
6746
6384
|
};
|
|
6747
6385
|
this.leaveAndClose = async (reason) => {
|
|
6748
6386
|
await this.joinTask;
|
|
@@ -6775,13 +6413,9 @@ class StreamSfuClient {
|
|
|
6775
6413
|
await this.joinTask;
|
|
6776
6414
|
return retryable(() => this.rpc.iceRestart({ ...data, sessionId: this.sessionId }), this.abortController.signal);
|
|
6777
6415
|
};
|
|
6778
|
-
this.
|
|
6416
|
+
this.updateMuteStates = async (muteStates) => {
|
|
6779
6417
|
await this.joinTask;
|
|
6780
|
-
return this.updateMuteStates({ muteStates
|
|
6781
|
-
};
|
|
6782
|
-
this.updateMuteStates = async (data) => {
|
|
6783
|
-
await this.joinTask;
|
|
6784
|
-
return retryable(() => this.rpc.updateMuteStates({ ...data, sessionId: this.sessionId }), this.abortController.signal);
|
|
6418
|
+
return retryable(() => this.rpc.updateMuteStates({ muteStates, sessionId: this.sessionId }), this.abortController.signal);
|
|
6785
6419
|
};
|
|
6786
6420
|
this.sendStats = async (stats) => {
|
|
6787
6421
|
await this.joinTask;
|
|
@@ -6961,16 +6595,6 @@ StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
|
|
|
6961
6595
|
*/
|
|
6962
6596
|
StreamSfuClient.DISPOSE_OLD_SOCKET = 4002;
|
|
6963
6597
|
|
|
6964
|
-
const toRtcConfiguration = (config) => {
|
|
6965
|
-
return {
|
|
6966
|
-
iceServers: config.map((ice) => ({
|
|
6967
|
-
urls: ice.urls,
|
|
6968
|
-
username: ice.username,
|
|
6969
|
-
credential: ice.password,
|
|
6970
|
-
})),
|
|
6971
|
-
};
|
|
6972
|
-
};
|
|
6973
|
-
|
|
6974
6598
|
/**
|
|
6975
6599
|
* Event handler that watched the delivery of `call.accepted`.
|
|
6976
6600
|
* Once the event is received, the call is joined.
|
|
@@ -7189,6 +6813,21 @@ const handleRemoteSoftMute = (call) => {
|
|
|
7189
6813
|
});
|
|
7190
6814
|
};
|
|
7191
6815
|
|
|
6816
|
+
/**
|
|
6817
|
+
* Adds unique values to an array.
|
|
6818
|
+
*
|
|
6819
|
+
* @param arr the array to add to.
|
|
6820
|
+
* @param values the values to add.
|
|
6821
|
+
*/
|
|
6822
|
+
const pushToIfMissing = (arr, ...values) => {
|
|
6823
|
+
for (const v of values) {
|
|
6824
|
+
if (!arr.includes(v)) {
|
|
6825
|
+
arr.push(v);
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
return arr;
|
|
6829
|
+
};
|
|
6830
|
+
|
|
7192
6831
|
/**
|
|
7193
6832
|
* An event responder which handles the `participantJoined` event.
|
|
7194
6833
|
*/
|
|
@@ -7254,7 +6893,7 @@ const watchTrackPublished = (state) => {
|
|
|
7254
6893
|
}
|
|
7255
6894
|
else {
|
|
7256
6895
|
state.updateParticipant(sessionId, (p) => ({
|
|
7257
|
-
publishedTracks: [...p.publishedTracks, type
|
|
6896
|
+
publishedTracks: pushToIfMissing([...p.publishedTracks], type),
|
|
7258
6897
|
}));
|
|
7259
6898
|
}
|
|
7260
6899
|
};
|
|
@@ -7279,7 +6918,6 @@ const watchTrackUnpublished = (state) => {
|
|
|
7279
6918
|
}
|
|
7280
6919
|
};
|
|
7281
6920
|
};
|
|
7282
|
-
const unique = (v, i, arr) => arr.indexOf(v) === i;
|
|
7283
6921
|
/**
|
|
7284
6922
|
* Reconciles orphaned tracks (if any) for the given participant.
|
|
7285
6923
|
*
|
|
@@ -7429,6 +7067,38 @@ const getSdkVersion = (sdk) => {
|
|
|
7429
7067
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
7430
7068
|
};
|
|
7431
7069
|
|
|
7070
|
+
/**
|
|
7071
|
+
* Checks whether the current browser is Safari.
|
|
7072
|
+
*/
|
|
7073
|
+
const isSafari = () => {
|
|
7074
|
+
if (typeof navigator === 'undefined')
|
|
7075
|
+
return false;
|
|
7076
|
+
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent || '');
|
|
7077
|
+
};
|
|
7078
|
+
/**
|
|
7079
|
+
* Checks whether the current browser is Firefox.
|
|
7080
|
+
*/
|
|
7081
|
+
const isFirefox = () => {
|
|
7082
|
+
if (typeof navigator === 'undefined')
|
|
7083
|
+
return false;
|
|
7084
|
+
return navigator.userAgent?.includes('Firefox');
|
|
7085
|
+
};
|
|
7086
|
+
/**
|
|
7087
|
+
* Checks whether the current browser is Google Chrome.
|
|
7088
|
+
*/
|
|
7089
|
+
const isChrome = () => {
|
|
7090
|
+
if (typeof navigator === 'undefined')
|
|
7091
|
+
return false;
|
|
7092
|
+
return navigator.userAgent?.includes('Chrome');
|
|
7093
|
+
};
|
|
7094
|
+
|
|
7095
|
+
var browsers = /*#__PURE__*/Object.freeze({
|
|
7096
|
+
__proto__: null,
|
|
7097
|
+
isChrome: isChrome,
|
|
7098
|
+
isFirefox: isFirefox,
|
|
7099
|
+
isSafari: isSafari
|
|
7100
|
+
});
|
|
7101
|
+
|
|
7432
7102
|
/**
|
|
7433
7103
|
* Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
|
|
7434
7104
|
*/
|
|
@@ -7445,12 +7115,12 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7445
7115
|
return undefined;
|
|
7446
7116
|
}
|
|
7447
7117
|
};
|
|
7448
|
-
const getStatsForStream = async (kind,
|
|
7118
|
+
const getStatsForStream = async (kind, tracks) => {
|
|
7449
7119
|
const pc = kind === 'subscriber' ? subscriber : publisher;
|
|
7450
7120
|
if (!pc)
|
|
7451
7121
|
return [];
|
|
7452
7122
|
const statsForStream = [];
|
|
7453
|
-
for (
|
|
7123
|
+
for (const track of tracks) {
|
|
7454
7124
|
const report = await pc.getStats(track);
|
|
7455
7125
|
const stats = transform(report, {
|
|
7456
7126
|
// @ts-ignore
|
|
@@ -7475,26 +7145,24 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7475
7145
|
*/
|
|
7476
7146
|
const run = async () => {
|
|
7477
7147
|
const participantStats = {};
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
for (
|
|
7148
|
+
if (sessionIdsToTrack.size > 0) {
|
|
7149
|
+
const sessionIds = new Set(sessionIdsToTrack);
|
|
7150
|
+
for (const participant of state.participants) {
|
|
7481
7151
|
if (!sessionIds.has(participant.sessionId))
|
|
7482
7152
|
continue;
|
|
7483
|
-
const
|
|
7484
|
-
|
|
7485
|
-
: 'subscriber';
|
|
7153
|
+
const { audioStream, isLocalParticipant, sessionId, userId, videoStream, } = participant;
|
|
7154
|
+
const kind = isLocalParticipant ? 'publisher' : 'subscriber';
|
|
7486
7155
|
try {
|
|
7487
|
-
const
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
});
|
|
7156
|
+
const tracks = isLocalParticipant
|
|
7157
|
+
? publisher?.getPublishedTracks() || []
|
|
7158
|
+
: [
|
|
7159
|
+
...(videoStream?.getVideoTracks() || []),
|
|
7160
|
+
...(audioStream?.getAudioTracks() || []),
|
|
7161
|
+
];
|
|
7162
|
+
participantStats[sessionId] = await getStatsForStream(kind, tracks);
|
|
7495
7163
|
}
|
|
7496
7164
|
catch (e) {
|
|
7497
|
-
logger('
|
|
7165
|
+
logger('warn', `Failed to collect ${kind} stats for ${userId}`, e);
|
|
7498
7166
|
}
|
|
7499
7167
|
}
|
|
7500
7168
|
}
|
|
@@ -7504,6 +7172,7 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7504
7172
|
.then((report) => transform(report, {
|
|
7505
7173
|
kind: 'subscriber',
|
|
7506
7174
|
trackKind: 'video',
|
|
7175
|
+
publisher,
|
|
7507
7176
|
}))
|
|
7508
7177
|
.then(aggregate),
|
|
7509
7178
|
publisher
|
|
@@ -7512,6 +7181,7 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7512
7181
|
.then((report) => transform(report, {
|
|
7513
7182
|
kind: 'publisher',
|
|
7514
7183
|
trackKind: 'video',
|
|
7184
|
+
publisher,
|
|
7515
7185
|
}))
|
|
7516
7186
|
.then(aggregate)
|
|
7517
7187
|
: getEmptyStats(),
|
|
@@ -7560,7 +7230,7 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7560
7230
|
* @param opts the transform options.
|
|
7561
7231
|
*/
|
|
7562
7232
|
const transform = (report, opts) => {
|
|
7563
|
-
const { trackKind, kind } = opts;
|
|
7233
|
+
const { trackKind, kind, publisher } = opts;
|
|
7564
7234
|
const direction = kind === 'subscriber' ? 'inbound-rtp' : 'outbound-rtp';
|
|
7565
7235
|
const stats = flatten(report);
|
|
7566
7236
|
const streams = stats
|
|
@@ -7576,6 +7246,16 @@ const transform = (report, opts) => {
|
|
|
7576
7246
|
s.id === transport.selectedCandidatePairId);
|
|
7577
7247
|
roundTripTime = candidatePair?.currentRoundTripTime;
|
|
7578
7248
|
}
|
|
7249
|
+
let trackType;
|
|
7250
|
+
if (kind === 'publisher' && publisher) {
|
|
7251
|
+
const firefox = isFirefox();
|
|
7252
|
+
const mediaSource = stats.find((s) => s.type === 'media-source' &&
|
|
7253
|
+
// Firefox doesn't have mediaSourceId, so we need to guess the media source
|
|
7254
|
+
(firefox ? true : s.id === rtcStreamStats.mediaSourceId));
|
|
7255
|
+
if (mediaSource) {
|
|
7256
|
+
trackType = publisher.getTrackType(mediaSource.trackIdentifier);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7579
7259
|
return {
|
|
7580
7260
|
bytesSent: rtcStreamStats.bytesSent,
|
|
7581
7261
|
bytesReceived: rtcStreamStats.bytesReceived,
|
|
@@ -7586,10 +7266,12 @@ const transform = (report, opts) => {
|
|
|
7586
7266
|
framesPerSecond: rtcStreamStats.framesPerSecond,
|
|
7587
7267
|
jitter: rtcStreamStats.jitter,
|
|
7588
7268
|
kind: rtcStreamStats.kind,
|
|
7269
|
+
mediaSourceId: rtcStreamStats.mediaSourceId,
|
|
7589
7270
|
// @ts-ignore: available in Chrome only, TS doesn't recognize this
|
|
7590
7271
|
qualityLimitationReason: rtcStreamStats.qualityLimitationReason,
|
|
7591
7272
|
rid: rtcStreamStats.rid,
|
|
7592
7273
|
ssrc: rtcStreamStats.ssrc,
|
|
7274
|
+
trackType,
|
|
7593
7275
|
};
|
|
7594
7276
|
});
|
|
7595
7277
|
return {
|
|
@@ -7610,6 +7292,7 @@ const getEmptyStats = (stats) => {
|
|
|
7610
7292
|
highestFrameHeight: 0,
|
|
7611
7293
|
highestFramesPerSecond: 0,
|
|
7612
7294
|
codec: '',
|
|
7295
|
+
codecPerTrackType: {},
|
|
7613
7296
|
timestamp: Date.now(),
|
|
7614
7297
|
};
|
|
7615
7298
|
};
|
|
@@ -7645,6 +7328,12 @@ const aggregate = (stats) => {
|
|
|
7645
7328
|
report.averageRoundTripTimeInMs = Math.round((report.averageRoundTripTimeInMs / streams.length) * 1000);
|
|
7646
7329
|
// we take the first codec we find, as it should be the same for all streams
|
|
7647
7330
|
report.codec = streams[0].codec || '';
|
|
7331
|
+
report.codecPerTrackType = streams.reduce((acc, stream) => {
|
|
7332
|
+
if (stream.trackType) {
|
|
7333
|
+
acc[stream.trackType] = stream.codec || '';
|
|
7334
|
+
}
|
|
7335
|
+
return acc;
|
|
7336
|
+
}, {});
|
|
7648
7337
|
}
|
|
7649
7338
|
const qualityLimitationReason = [
|
|
7650
7339
|
qualityLimitationReasons.has('cpu') && 'cpu',
|
|
@@ -7656,7 +7345,135 @@ const aggregate = (stats) => {
|
|
|
7656
7345
|
if (qualityLimitationReason) {
|
|
7657
7346
|
report.qualityLimitationReasons = qualityLimitationReason;
|
|
7658
7347
|
}
|
|
7659
|
-
return report;
|
|
7348
|
+
return report;
|
|
7349
|
+
};
|
|
7350
|
+
|
|
7351
|
+
const version = "1.15.0";
|
|
7352
|
+
const [major, minor, patch] = version.split('.');
|
|
7353
|
+
let sdkInfo = {
|
|
7354
|
+
type: SdkType.PLAIN_JAVASCRIPT,
|
|
7355
|
+
major,
|
|
7356
|
+
minor,
|
|
7357
|
+
patch,
|
|
7358
|
+
};
|
|
7359
|
+
let osInfo;
|
|
7360
|
+
let deviceInfo;
|
|
7361
|
+
let webRtcInfo;
|
|
7362
|
+
let deviceState = { oneofKind: undefined };
|
|
7363
|
+
const setSdkInfo = (info) => {
|
|
7364
|
+
sdkInfo = info;
|
|
7365
|
+
};
|
|
7366
|
+
const getSdkInfo = () => {
|
|
7367
|
+
return sdkInfo;
|
|
7368
|
+
};
|
|
7369
|
+
const setOSInfo = (info) => {
|
|
7370
|
+
osInfo = info;
|
|
7371
|
+
};
|
|
7372
|
+
const getOSInfo = () => {
|
|
7373
|
+
return osInfo;
|
|
7374
|
+
};
|
|
7375
|
+
const setDeviceInfo = (info) => {
|
|
7376
|
+
deviceInfo = info;
|
|
7377
|
+
};
|
|
7378
|
+
const getDeviceInfo = () => {
|
|
7379
|
+
return deviceInfo;
|
|
7380
|
+
};
|
|
7381
|
+
const getWebRTCInfo = () => {
|
|
7382
|
+
return webRtcInfo;
|
|
7383
|
+
};
|
|
7384
|
+
const setWebRTCInfo = (info) => {
|
|
7385
|
+
webRtcInfo = info;
|
|
7386
|
+
};
|
|
7387
|
+
const setThermalState = (state) => {
|
|
7388
|
+
if (!osInfo) {
|
|
7389
|
+
deviceState = { oneofKind: undefined };
|
|
7390
|
+
return;
|
|
7391
|
+
}
|
|
7392
|
+
if (osInfo.name === 'android') {
|
|
7393
|
+
const thermalState = AndroidThermalState[state] ||
|
|
7394
|
+
AndroidThermalState.UNSPECIFIED;
|
|
7395
|
+
deviceState = {
|
|
7396
|
+
oneofKind: 'android',
|
|
7397
|
+
android: {
|
|
7398
|
+
thermalState,
|
|
7399
|
+
isPowerSaverMode: deviceState?.oneofKind === 'android' &&
|
|
7400
|
+
deviceState.android.isPowerSaverMode,
|
|
7401
|
+
},
|
|
7402
|
+
};
|
|
7403
|
+
}
|
|
7404
|
+
if (osInfo.name.toLowerCase() === 'ios') {
|
|
7405
|
+
const thermalState = AppleThermalState[state] ||
|
|
7406
|
+
AppleThermalState.UNSPECIFIED;
|
|
7407
|
+
deviceState = {
|
|
7408
|
+
oneofKind: 'apple',
|
|
7409
|
+
apple: {
|
|
7410
|
+
thermalState,
|
|
7411
|
+
isLowPowerModeEnabled: deviceState?.oneofKind === 'apple' &&
|
|
7412
|
+
deviceState.apple.isLowPowerModeEnabled,
|
|
7413
|
+
},
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7416
|
+
};
|
|
7417
|
+
const setPowerState = (powerMode) => {
|
|
7418
|
+
if (!osInfo) {
|
|
7419
|
+
deviceState = { oneofKind: undefined };
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7422
|
+
if (osInfo.name === 'android') {
|
|
7423
|
+
deviceState = {
|
|
7424
|
+
oneofKind: 'android',
|
|
7425
|
+
android: {
|
|
7426
|
+
thermalState: deviceState?.oneofKind === 'android'
|
|
7427
|
+
? deviceState.android.thermalState
|
|
7428
|
+
: AndroidThermalState.UNSPECIFIED,
|
|
7429
|
+
isPowerSaverMode: powerMode,
|
|
7430
|
+
},
|
|
7431
|
+
};
|
|
7432
|
+
}
|
|
7433
|
+
if (osInfo.name.toLowerCase() === 'ios') {
|
|
7434
|
+
deviceState = {
|
|
7435
|
+
oneofKind: 'apple',
|
|
7436
|
+
apple: {
|
|
7437
|
+
thermalState: deviceState?.oneofKind === 'apple'
|
|
7438
|
+
? deviceState.apple.thermalState
|
|
7439
|
+
: AppleThermalState.UNSPECIFIED,
|
|
7440
|
+
isLowPowerModeEnabled: powerMode,
|
|
7441
|
+
},
|
|
7442
|
+
};
|
|
7443
|
+
}
|
|
7444
|
+
};
|
|
7445
|
+
const getDeviceState = () => {
|
|
7446
|
+
return deviceState;
|
|
7447
|
+
};
|
|
7448
|
+
const getClientDetails = () => {
|
|
7449
|
+
if (isReactNative()) {
|
|
7450
|
+
// Since RN doesn't support web, sharing browser info is not required
|
|
7451
|
+
return {
|
|
7452
|
+
sdk: getSdkInfo(),
|
|
7453
|
+
os: getOSInfo(),
|
|
7454
|
+
device: getDeviceInfo(),
|
|
7455
|
+
};
|
|
7456
|
+
}
|
|
7457
|
+
const userAgent = new uaParserJs.UAParser(navigator.userAgent);
|
|
7458
|
+
const { browser, os, device, cpu } = userAgent.getResult();
|
|
7459
|
+
return {
|
|
7460
|
+
sdk: getSdkInfo(),
|
|
7461
|
+
browser: {
|
|
7462
|
+
name: browser.name || navigator.userAgent,
|
|
7463
|
+
version: browser.version || '',
|
|
7464
|
+
},
|
|
7465
|
+
os: {
|
|
7466
|
+
name: os.name || '',
|
|
7467
|
+
version: os.version || '',
|
|
7468
|
+
architecture: cpu.architecture || '',
|
|
7469
|
+
},
|
|
7470
|
+
device: {
|
|
7471
|
+
name: [device.vendor, device.model, device.type]
|
|
7472
|
+
.filter(Boolean)
|
|
7473
|
+
.join(' '),
|
|
7474
|
+
version: '',
|
|
7475
|
+
},
|
|
7476
|
+
};
|
|
7660
7477
|
};
|
|
7661
7478
|
|
|
7662
7479
|
class SfuStatsReporter {
|
|
@@ -7692,8 +7509,28 @@ class SfuStatsReporter {
|
|
|
7692
7509
|
});
|
|
7693
7510
|
});
|
|
7694
7511
|
};
|
|
7695
|
-
this.
|
|
7696
|
-
|
|
7512
|
+
this.sendConnectionTime = (connectionTimeSeconds) => {
|
|
7513
|
+
this.sendTelemetryData({
|
|
7514
|
+
data: {
|
|
7515
|
+
oneofKind: 'connectionTimeSeconds',
|
|
7516
|
+
connectionTimeSeconds,
|
|
7517
|
+
},
|
|
7518
|
+
});
|
|
7519
|
+
};
|
|
7520
|
+
this.sendReconnectionTime = (strategy, timeSeconds) => {
|
|
7521
|
+
this.sendTelemetryData({
|
|
7522
|
+
data: {
|
|
7523
|
+
oneofKind: 'reconnection',
|
|
7524
|
+
reconnection: { strategy, timeSeconds },
|
|
7525
|
+
},
|
|
7526
|
+
});
|
|
7527
|
+
};
|
|
7528
|
+
this.sendTelemetryData = (telemetryData) => {
|
|
7529
|
+
// intentionally not awaiting the promise here
|
|
7530
|
+
// to avoid impeding with the ongoing actions.
|
|
7531
|
+
this.run(telemetryData).catch((err) => {
|
|
7532
|
+
this.logger('warn', 'Failed to send telemetry data', err);
|
|
7533
|
+
});
|
|
7697
7534
|
};
|
|
7698
7535
|
this.run = async (telemetryData) => {
|
|
7699
7536
|
const [subscriberStats, publisherStats] = await Promise.all([
|
|
@@ -8261,6 +8098,25 @@ class PermissionsContext {
|
|
|
8261
8098
|
this.hasPermission = (permission) => {
|
|
8262
8099
|
return this.permissions.includes(permission);
|
|
8263
8100
|
};
|
|
8101
|
+
/**
|
|
8102
|
+
* Helper method that checks whether the current user has the permission
|
|
8103
|
+
* to publish the given track type.
|
|
8104
|
+
*/
|
|
8105
|
+
this.canPublish = (trackType) => {
|
|
8106
|
+
switch (trackType) {
|
|
8107
|
+
case TrackType.AUDIO:
|
|
8108
|
+
return this.hasPermission(OwnCapability.SEND_AUDIO);
|
|
8109
|
+
case TrackType.VIDEO:
|
|
8110
|
+
return this.hasPermission(OwnCapability.SEND_VIDEO);
|
|
8111
|
+
case TrackType.SCREEN_SHARE:
|
|
8112
|
+
case TrackType.SCREEN_SHARE_AUDIO:
|
|
8113
|
+
return this.hasPermission(OwnCapability.SCREENSHARE);
|
|
8114
|
+
case TrackType.UNSPECIFIED:
|
|
8115
|
+
return false;
|
|
8116
|
+
default:
|
|
8117
|
+
ensureExhausted(trackType, 'Unknown track type');
|
|
8118
|
+
}
|
|
8119
|
+
};
|
|
8264
8120
|
/**
|
|
8265
8121
|
* Checks if the current user can request a specific permission
|
|
8266
8122
|
* within the call.
|
|
@@ -8899,36 +8755,42 @@ class InputMediaDeviceManager {
|
|
|
8899
8755
|
}
|
|
8900
8756
|
});
|
|
8901
8757
|
}
|
|
8758
|
+
publishStream(stream) {
|
|
8759
|
+
return this.call.publish(stream, this.trackType);
|
|
8760
|
+
}
|
|
8761
|
+
stopPublishStream() {
|
|
8762
|
+
return this.call.stopPublish(this.trackType);
|
|
8763
|
+
}
|
|
8902
8764
|
getTracks() {
|
|
8903
8765
|
return this.state.mediaStream?.getTracks() ?? [];
|
|
8904
8766
|
}
|
|
8905
8767
|
async muteStream(stopTracks = true) {
|
|
8906
|
-
|
|
8768
|
+
const mediaStream = this.state.mediaStream;
|
|
8769
|
+
if (!mediaStream)
|
|
8907
8770
|
return;
|
|
8908
8771
|
this.logger('debug', `${stopTracks ? 'Stopping' : 'Disabling'} stream`);
|
|
8909
8772
|
if (this.call.state.callingState === exports.CallingState.JOINED) {
|
|
8910
|
-
await this.stopPublishStream(
|
|
8773
|
+
await this.stopPublishStream();
|
|
8911
8774
|
}
|
|
8912
8775
|
this.muteLocalStream(stopTracks);
|
|
8913
8776
|
const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
|
|
8914
8777
|
if (allEnded) {
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
typeof this.state.mediaStream.release === 'function') {
|
|
8778
|
+
// @ts-expect-error release() is present in react-native-webrtc
|
|
8779
|
+
if (typeof mediaStream.release === 'function') {
|
|
8918
8780
|
// @ts-expect-error called to dispose the stream in RN
|
|
8919
|
-
|
|
8781
|
+
mediaStream.release();
|
|
8920
8782
|
}
|
|
8921
8783
|
this.state.setMediaStream(undefined, undefined);
|
|
8922
8784
|
this.filters.forEach((entry) => entry.stop?.());
|
|
8923
8785
|
}
|
|
8924
8786
|
}
|
|
8925
|
-
|
|
8787
|
+
disableTracks() {
|
|
8926
8788
|
this.getTracks().forEach((track) => {
|
|
8927
8789
|
if (track.enabled)
|
|
8928
8790
|
track.enabled = false;
|
|
8929
8791
|
});
|
|
8930
8792
|
}
|
|
8931
|
-
|
|
8793
|
+
enableTracks() {
|
|
8932
8794
|
this.getTracks().forEach((track) => {
|
|
8933
8795
|
if (!track.enabled)
|
|
8934
8796
|
track.enabled = true;
|
|
@@ -8948,7 +8810,7 @@ class InputMediaDeviceManager {
|
|
|
8948
8810
|
this.stopTracks();
|
|
8949
8811
|
}
|
|
8950
8812
|
else {
|
|
8951
|
-
this.
|
|
8813
|
+
this.disableTracks();
|
|
8952
8814
|
}
|
|
8953
8815
|
}
|
|
8954
8816
|
async unmuteStream() {
|
|
@@ -8958,7 +8820,7 @@ class InputMediaDeviceManager {
|
|
|
8958
8820
|
if (this.state.mediaStream &&
|
|
8959
8821
|
this.getTracks().every((t) => t.readyState === 'live')) {
|
|
8960
8822
|
stream = this.state.mediaStream;
|
|
8961
|
-
this.
|
|
8823
|
+
this.enableTracks();
|
|
8962
8824
|
}
|
|
8963
8825
|
else {
|
|
8964
8826
|
const defaultConstraints = this.state.defaultConstraints;
|
|
@@ -9052,9 +8914,22 @@ class InputMediaDeviceManager {
|
|
|
9052
8914
|
await this.disable();
|
|
9053
8915
|
}
|
|
9054
8916
|
};
|
|
9055
|
-
|
|
8917
|
+
const createTrackMuteHandler = (muted) => () => {
|
|
8918
|
+
this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
|
|
8919
|
+
this.logger('warn', 'Error while notifying track mute state', err);
|
|
8920
|
+
});
|
|
8921
|
+
};
|
|
8922
|
+
stream.getTracks().forEach((track) => {
|
|
8923
|
+
const muteHandler = createTrackMuteHandler(true);
|
|
8924
|
+
const unmuteHandler = createTrackMuteHandler(false);
|
|
8925
|
+
track.addEventListener('mute', muteHandler);
|
|
8926
|
+
track.addEventListener('unmute', unmuteHandler);
|
|
9056
8927
|
track.addEventListener('ended', handleTrackEnded);
|
|
9057
|
-
this.subscriptions.push(() =>
|
|
8928
|
+
this.subscriptions.push(() => {
|
|
8929
|
+
track.removeEventListener('mute', muteHandler);
|
|
8930
|
+
track.removeEventListener('unmute', unmuteHandler);
|
|
8931
|
+
track.removeEventListener('ended', handleTrackEnded);
|
|
8932
|
+
});
|
|
9058
8933
|
});
|
|
9059
8934
|
}
|
|
9060
8935
|
}
|
|
@@ -9078,8 +8953,8 @@ class InputMediaDeviceManager {
|
|
|
9078
8953
|
await this.statusChangeSettled();
|
|
9079
8954
|
let isDeviceDisconnected = false;
|
|
9080
8955
|
let isDeviceReplaced = false;
|
|
9081
|
-
const currentDevice = this.
|
|
9082
|
-
const prevDevice = this.
|
|
8956
|
+
const currentDevice = this.findDevice(currentDevices, deviceId);
|
|
8957
|
+
const prevDevice = this.findDevice(prevDevices, deviceId);
|
|
9083
8958
|
if (!currentDevice && prevDevice) {
|
|
9084
8959
|
isDeviceDisconnected = true;
|
|
9085
8960
|
}
|
|
@@ -9109,8 +8984,9 @@ class InputMediaDeviceManager {
|
|
|
9109
8984
|
}
|
|
9110
8985
|
}));
|
|
9111
8986
|
}
|
|
9112
|
-
|
|
9113
|
-
|
|
8987
|
+
findDevice(devices, deviceId) {
|
|
8988
|
+
const kind = this.mediaDeviceKind;
|
|
8989
|
+
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
9114
8990
|
}
|
|
9115
8991
|
}
|
|
9116
8992
|
|
|
@@ -9374,14 +9250,35 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9374
9250
|
}
|
|
9375
9251
|
}
|
|
9376
9252
|
/**
|
|
9377
|
-
*
|
|
9253
|
+
* Applies the video settings to the camera.
|
|
9378
9254
|
*
|
|
9379
|
-
* @
|
|
9380
|
-
* @
|
|
9381
|
-
* @param codec the codec to use for encoding the video.
|
|
9255
|
+
* @param settings the video settings to apply.
|
|
9256
|
+
* @param publish whether to publish the stream after applying the settings.
|
|
9382
9257
|
*/
|
|
9383
|
-
|
|
9384
|
-
this.call.
|
|
9258
|
+
async apply(settings, publish) {
|
|
9259
|
+
const hasPublishedVideo = !!this.call.state.localParticipant?.videoStream;
|
|
9260
|
+
const hasPermission = this.call.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO);
|
|
9261
|
+
if (hasPublishedVideo || !hasPermission)
|
|
9262
|
+
return;
|
|
9263
|
+
// Wait for any in progress camera operation
|
|
9264
|
+
await this.statusChangeSettled();
|
|
9265
|
+
const { target_resolution, camera_facing, camera_default_on } = settings;
|
|
9266
|
+
await this.selectTargetResolution(target_resolution);
|
|
9267
|
+
// Set camera direction if it's not yet set
|
|
9268
|
+
if (!this.state.direction && !this.state.selectedDevice) {
|
|
9269
|
+
this.state.setDirection(camera_facing === 'front' ? 'front' : 'back');
|
|
9270
|
+
}
|
|
9271
|
+
if (!publish)
|
|
9272
|
+
return;
|
|
9273
|
+
const { mediaStream } = this.state;
|
|
9274
|
+
if (this.enabled && mediaStream) {
|
|
9275
|
+
// The camera is already enabled (e.g. lobby screen). Publish the stream
|
|
9276
|
+
await this.publishStream(mediaStream);
|
|
9277
|
+
}
|
|
9278
|
+
else if (this.state.status === undefined && camera_default_on) {
|
|
9279
|
+
// Start camera if backend config specifies, and there is no local setting
|
|
9280
|
+
await this.enable();
|
|
9281
|
+
}
|
|
9385
9282
|
}
|
|
9386
9283
|
getDevices() {
|
|
9387
9284
|
return getVideoDevices();
|
|
@@ -9399,12 +9296,6 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9399
9296
|
}
|
|
9400
9297
|
return getVideoStream(constraints);
|
|
9401
9298
|
}
|
|
9402
|
-
publishStream(stream) {
|
|
9403
|
-
return this.call.publishVideoStream(stream);
|
|
9404
|
-
}
|
|
9405
|
-
stopPublishStream(stopTracks) {
|
|
9406
|
-
return this.call.stopPublish(TrackType.VIDEO, stopTracks);
|
|
9407
|
-
}
|
|
9408
9299
|
}
|
|
9409
9300
|
|
|
9410
9301
|
class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
@@ -9732,18 +9623,37 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
9732
9623
|
this.speakingWhileMutedNotificationEnabled = false;
|
|
9733
9624
|
await this.stopSpeakingWhileMutedDetection();
|
|
9734
9625
|
}
|
|
9626
|
+
/**
|
|
9627
|
+
* Applies the audio settings to the microphone.
|
|
9628
|
+
* @param settings the audio settings to apply.
|
|
9629
|
+
* @param publish whether to publish the stream after applying the settings.
|
|
9630
|
+
*/
|
|
9631
|
+
async apply(settings, publish) {
|
|
9632
|
+
if (!publish)
|
|
9633
|
+
return;
|
|
9634
|
+
const hasPublishedAudio = !!this.call.state.localParticipant?.audioStream;
|
|
9635
|
+
const hasPermission = this.call.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO);
|
|
9636
|
+
if (hasPublishedAudio || !hasPermission)
|
|
9637
|
+
return;
|
|
9638
|
+
// Wait for any in progress mic operation
|
|
9639
|
+
await this.statusChangeSettled();
|
|
9640
|
+
// Publish media stream that was set before we joined
|
|
9641
|
+
const { mediaStream } = this.state;
|
|
9642
|
+
if (this.enabled && mediaStream) {
|
|
9643
|
+
// The mic is already enabled (e.g. lobby screen). Publish the stream
|
|
9644
|
+
await this.publishStream(mediaStream);
|
|
9645
|
+
}
|
|
9646
|
+
else if (this.state.status === undefined && settings.mic_default_on) {
|
|
9647
|
+
// Start mic if backend config specifies, and there is no local setting
|
|
9648
|
+
await this.enable();
|
|
9649
|
+
}
|
|
9650
|
+
}
|
|
9735
9651
|
getDevices() {
|
|
9736
9652
|
return getAudioDevices();
|
|
9737
9653
|
}
|
|
9738
9654
|
getStream(constraints) {
|
|
9739
9655
|
return getAudioStream(constraints);
|
|
9740
9656
|
}
|
|
9741
|
-
publishStream(stream) {
|
|
9742
|
-
return this.call.publishAudioStream(stream);
|
|
9743
|
-
}
|
|
9744
|
-
stopPublishStream(stopTracks) {
|
|
9745
|
-
return this.call.stopPublish(TrackType.AUDIO, stopTracks);
|
|
9746
|
-
}
|
|
9747
9657
|
async startSpeakingWhileMutedDetection(deviceId) {
|
|
9748
9658
|
await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
|
|
9749
9659
|
await this.stopSpeakingWhileMutedDetection();
|
|
@@ -9863,7 +9773,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
9863
9773
|
async disableScreenShareAudio() {
|
|
9864
9774
|
this.state.setAudioEnabled(false);
|
|
9865
9775
|
if (this.call.publisher?.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
9866
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO
|
|
9776
|
+
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO);
|
|
9867
9777
|
}
|
|
9868
9778
|
}
|
|
9869
9779
|
/**
|
|
@@ -9889,12 +9799,8 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
9889
9799
|
}
|
|
9890
9800
|
return getScreenShareStream(constraints);
|
|
9891
9801
|
}
|
|
9892
|
-
|
|
9893
|
-
return this.call.
|
|
9894
|
-
}
|
|
9895
|
-
async stopPublishStream(stopTracks) {
|
|
9896
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE, stopTracks);
|
|
9897
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO, stopTracks);
|
|
9802
|
+
async stopPublishStream() {
|
|
9803
|
+
return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
|
|
9898
9804
|
}
|
|
9899
9805
|
/**
|
|
9900
9806
|
* Overrides the default `select` method to throw an error.
|
|
@@ -10104,6 +10010,112 @@ class Call {
|
|
|
10104
10010
|
*/
|
|
10105
10011
|
this.leaveCallHooks = new Set();
|
|
10106
10012
|
this.streamClientEventHandlers = new Map();
|
|
10013
|
+
this.setup = async () => {
|
|
10014
|
+
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
10015
|
+
if (this.initialized)
|
|
10016
|
+
return;
|
|
10017
|
+
this.leaveCallHooks.add(this.on('all', (event) => {
|
|
10018
|
+
// update state with the latest event data
|
|
10019
|
+
this.state.updateFromEvent(event);
|
|
10020
|
+
}));
|
|
10021
|
+
this.leaveCallHooks.add(this.on('changePublishOptions', (event) => {
|
|
10022
|
+
this.currentPublishOptions = event.publishOptions;
|
|
10023
|
+
}));
|
|
10024
|
+
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
10025
|
+
this.registerEffects();
|
|
10026
|
+
this.registerReconnectHandlers();
|
|
10027
|
+
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
10028
|
+
this.state.setCallingState(exports.CallingState.IDLE);
|
|
10029
|
+
}
|
|
10030
|
+
this.initialized = true;
|
|
10031
|
+
});
|
|
10032
|
+
};
|
|
10033
|
+
this.registerEffects = () => {
|
|
10034
|
+
this.leaveCallHooks.add(
|
|
10035
|
+
// handles updating the permissions context when the settings change.
|
|
10036
|
+
createSubscription(this.state.settings$, (settings) => {
|
|
10037
|
+
if (!settings)
|
|
10038
|
+
return;
|
|
10039
|
+
this.permissionsContext.setCallSettings(settings);
|
|
10040
|
+
}));
|
|
10041
|
+
this.leaveCallHooks.add(
|
|
10042
|
+
// handle the case when the user permissions are modified.
|
|
10043
|
+
createSafeAsyncSubscription(this.state.ownCapabilities$, this.handleOwnCapabilitiesUpdated));
|
|
10044
|
+
this.leaveCallHooks.add(
|
|
10045
|
+
// handles the case when the user is blocked by the call owner.
|
|
10046
|
+
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
10047
|
+
if (!blockedUserIds || blockedUserIds.length === 0)
|
|
10048
|
+
return;
|
|
10049
|
+
const currentUserId = this.currentUserId;
|
|
10050
|
+
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
10051
|
+
this.logger('info', 'Leaving call because of being blocked');
|
|
10052
|
+
await this.leave({ reason: 'user blocked' }).catch((err) => {
|
|
10053
|
+
this.logger('error', 'Error leaving call after being blocked', err);
|
|
10054
|
+
});
|
|
10055
|
+
}
|
|
10056
|
+
}));
|
|
10057
|
+
this.leaveCallHooks.add(
|
|
10058
|
+
// cancel auto-drop when call is
|
|
10059
|
+
createSubscription(this.state.session$, (session) => {
|
|
10060
|
+
if (!this.ringing)
|
|
10061
|
+
return;
|
|
10062
|
+
const receiverId = this.clientStore.connectedUser?.id;
|
|
10063
|
+
if (!receiverId)
|
|
10064
|
+
return;
|
|
10065
|
+
const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
|
|
10066
|
+
const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
|
|
10067
|
+
if (isAcceptedByMe || isRejectedByMe) {
|
|
10068
|
+
this.cancelAutoDrop();
|
|
10069
|
+
}
|
|
10070
|
+
}));
|
|
10071
|
+
this.leaveCallHooks.add(
|
|
10072
|
+
// "ringing" mode effects and event handlers
|
|
10073
|
+
createSubscription(this.ringingSubject, (isRinging) => {
|
|
10074
|
+
if (!isRinging)
|
|
10075
|
+
return;
|
|
10076
|
+
const callSession = this.state.session;
|
|
10077
|
+
const receiver_id = this.clientStore.connectedUser?.id;
|
|
10078
|
+
const ended_at = callSession?.ended_at;
|
|
10079
|
+
const created_by_id = this.state.createdBy?.id;
|
|
10080
|
+
const rejected_by = callSession?.rejected_by;
|
|
10081
|
+
const accepted_by = callSession?.accepted_by;
|
|
10082
|
+
let leaveCallIdle = false;
|
|
10083
|
+
if (ended_at) {
|
|
10084
|
+
// call was ended before it was accepted or rejected so we should leave it to idle
|
|
10085
|
+
leaveCallIdle = true;
|
|
10086
|
+
}
|
|
10087
|
+
else if (created_by_id && rejected_by) {
|
|
10088
|
+
if (rejected_by[created_by_id]) {
|
|
10089
|
+
// call was cancelled by the caller
|
|
10090
|
+
leaveCallIdle = true;
|
|
10091
|
+
}
|
|
10092
|
+
}
|
|
10093
|
+
else if (receiver_id && rejected_by) {
|
|
10094
|
+
if (rejected_by[receiver_id]) {
|
|
10095
|
+
// call was rejected by the receiver in some other device
|
|
10096
|
+
leaveCallIdle = true;
|
|
10097
|
+
}
|
|
10098
|
+
}
|
|
10099
|
+
else if (receiver_id && accepted_by) {
|
|
10100
|
+
if (accepted_by[receiver_id]) {
|
|
10101
|
+
// call was accepted by the receiver in some other device
|
|
10102
|
+
leaveCallIdle = true;
|
|
10103
|
+
}
|
|
10104
|
+
}
|
|
10105
|
+
if (leaveCallIdle) {
|
|
10106
|
+
if (this.state.callingState !== exports.CallingState.IDLE) {
|
|
10107
|
+
this.state.setCallingState(exports.CallingState.IDLE);
|
|
10108
|
+
}
|
|
10109
|
+
}
|
|
10110
|
+
else {
|
|
10111
|
+
if (this.state.callingState === exports.CallingState.IDLE) {
|
|
10112
|
+
this.state.setCallingState(exports.CallingState.RINGING);
|
|
10113
|
+
}
|
|
10114
|
+
this.scheduleAutoDrop();
|
|
10115
|
+
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
10116
|
+
}
|
|
10117
|
+
}));
|
|
10118
|
+
};
|
|
10107
10119
|
this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
|
|
10108
10120
|
// update the permission context.
|
|
10109
10121
|
this.permissionsContext.setPermissions(ownCapabilities);
|
|
@@ -10216,14 +10228,15 @@ class Call {
|
|
|
10216
10228
|
this.statsReporter = undefined;
|
|
10217
10229
|
this.sfuStatsReporter?.stop();
|
|
10218
10230
|
this.sfuStatsReporter = undefined;
|
|
10219
|
-
this.subscriber?.
|
|
10231
|
+
this.subscriber?.dispose();
|
|
10220
10232
|
this.subscriber = undefined;
|
|
10221
|
-
this.publisher?.
|
|
10233
|
+
this.publisher?.dispose();
|
|
10222
10234
|
this.publisher = undefined;
|
|
10223
10235
|
await this.sfuClient?.leaveAndClose(reason);
|
|
10224
10236
|
this.sfuClient = undefined;
|
|
10225
10237
|
this.dynascaleManager.setSfuClient(undefined);
|
|
10226
10238
|
this.state.setCallingState(exports.CallingState.LEFT);
|
|
10239
|
+
this.state.dispose();
|
|
10227
10240
|
// Call all leave call hooks, e.g. to clean up global event handlers
|
|
10228
10241
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
10229
10242
|
this.initialized = false;
|
|
@@ -10255,7 +10268,8 @@ class Call {
|
|
|
10255
10268
|
// call.ring event excludes the call creator in the members list
|
|
10256
10269
|
// as the creator does not get the ring event
|
|
10257
10270
|
// so update the member list accordingly
|
|
10258
|
-
const
|
|
10271
|
+
const { created_by, settings } = event.call;
|
|
10272
|
+
const creator = this.state.members.find((m) => m.user.id === created_by.id);
|
|
10259
10273
|
if (!creator) {
|
|
10260
10274
|
this.state.setMembers(event.members);
|
|
10261
10275
|
}
|
|
@@ -10270,7 +10284,7 @@ class Call {
|
|
|
10270
10284
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
10271
10285
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
10272
10286
|
this.clientStore.setCalls([this, ...calls]);
|
|
10273
|
-
await this.applyDeviceConfig(false);
|
|
10287
|
+
await this.applyDeviceConfig(settings, false);
|
|
10274
10288
|
};
|
|
10275
10289
|
/**
|
|
10276
10290
|
* Loads the information about the call.
|
|
@@ -10293,7 +10307,7 @@ class Call {
|
|
|
10293
10307
|
this.watching = true;
|
|
10294
10308
|
this.clientStore.registerCall(this);
|
|
10295
10309
|
}
|
|
10296
|
-
await this.applyDeviceConfig(false);
|
|
10310
|
+
await this.applyDeviceConfig(response.call.settings, false);
|
|
10297
10311
|
return response;
|
|
10298
10312
|
};
|
|
10299
10313
|
/**
|
|
@@ -10315,7 +10329,7 @@ class Call {
|
|
|
10315
10329
|
this.watching = true;
|
|
10316
10330
|
this.clientStore.registerCall(this);
|
|
10317
10331
|
}
|
|
10318
|
-
await this.applyDeviceConfig(false);
|
|
10332
|
+
await this.applyDeviceConfig(response.call.settings, false);
|
|
10319
10333
|
return response;
|
|
10320
10334
|
};
|
|
10321
10335
|
/**
|
|
@@ -10417,19 +10431,32 @@ class Call {
|
|
|
10417
10431
|
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
10418
10432
|
if (previousSfuClient !== sfuClient) {
|
|
10419
10433
|
// prepare a generic SDP and send it to the SFU.
|
|
10420
|
-
//
|
|
10434
|
+
// these are throw-away SDPs that the SFU will use to determine
|
|
10421
10435
|
// the capabilities of the client (codec support, etc.)
|
|
10422
|
-
const
|
|
10423
|
-
|
|
10436
|
+
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
10437
|
+
getGenericSdp('recvonly'),
|
|
10438
|
+
getGenericSdp('sendonly'),
|
|
10439
|
+
]);
|
|
10440
|
+
const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
10441
|
+
const reconnectDetails = isReconnecting
|
|
10424
10442
|
? this.getReconnectDetails(data?.migrating_from, previousSessionId)
|
|
10425
10443
|
: undefined;
|
|
10426
|
-
const
|
|
10427
|
-
|
|
10428
|
-
|
|
10444
|
+
const preferredPublishOptions = !isReconnecting
|
|
10445
|
+
? this.getPreferredPublishOptions()
|
|
10446
|
+
: this.currentPublishOptions || [];
|
|
10447
|
+
const preferredSubscribeOptions = !isReconnecting
|
|
10448
|
+
? this.getPreferredSubscribeOptions()
|
|
10449
|
+
: [];
|
|
10450
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
10451
|
+
subscriberSdp,
|
|
10452
|
+
publisherSdp,
|
|
10429
10453
|
clientDetails,
|
|
10430
10454
|
fastReconnect: performingFastReconnect,
|
|
10431
10455
|
reconnectDetails,
|
|
10456
|
+
preferredPublishOptions,
|
|
10457
|
+
preferredSubscribeOptions,
|
|
10432
10458
|
});
|
|
10459
|
+
this.currentPublishOptions = publishOptions;
|
|
10433
10460
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
10434
10461
|
if (callState) {
|
|
10435
10462
|
this.state.updateFromSfuCallState(callState, sfuClient.sessionId, reconnectDetails);
|
|
@@ -10454,17 +10481,13 @@ class Call {
|
|
|
10454
10481
|
connectionConfig,
|
|
10455
10482
|
clientDetails,
|
|
10456
10483
|
statsOptions,
|
|
10484
|
+
publishOptions: this.currentPublishOptions || [],
|
|
10457
10485
|
closePreviousInstances: !performingMigration,
|
|
10458
10486
|
});
|
|
10459
10487
|
}
|
|
10460
10488
|
// make sure we only track connection timing if we are not calling this method as part of a reconnection flow
|
|
10461
10489
|
if (!performingRejoin && !performingFastReconnect && !performingMigration) {
|
|
10462
|
-
this.sfuStatsReporter?.
|
|
10463
|
-
data: {
|
|
10464
|
-
oneofKind: 'connectionTimeSeconds',
|
|
10465
|
-
connectionTimeSeconds: (Date.now() - connectStartTime) / 1000,
|
|
10466
|
-
},
|
|
10467
|
-
});
|
|
10490
|
+
this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
|
|
10468
10491
|
}
|
|
10469
10492
|
if (performingRejoin) {
|
|
10470
10493
|
const strategy = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
@@ -10475,8 +10498,8 @@ class Call {
|
|
|
10475
10498
|
}
|
|
10476
10499
|
// device settings should be applied only once, we don't have to
|
|
10477
10500
|
// re-apply them on later reconnections or server-side data fetches
|
|
10478
|
-
if (!this.deviceSettingsAppliedOnce) {
|
|
10479
|
-
await this.applyDeviceConfig(true);
|
|
10501
|
+
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
10502
|
+
await this.applyDeviceConfig(this.state.settings, true);
|
|
10480
10503
|
this.deviceSettingsAppliedOnce = true;
|
|
10481
10504
|
}
|
|
10482
10505
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
@@ -10485,6 +10508,8 @@ class Call {
|
|
|
10485
10508
|
// we will spam the other participants with push notifications and `call.ring` events.
|
|
10486
10509
|
delete this.joinCallData?.ring;
|
|
10487
10510
|
delete this.joinCallData?.notify;
|
|
10511
|
+
// reset the reconnect strategy to unspecified after a successful reconnection
|
|
10512
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
|
|
10488
10513
|
this.logger('info', `Joined call ${this.cid}`);
|
|
10489
10514
|
};
|
|
10490
10515
|
/**
|
|
@@ -10494,7 +10519,7 @@ class Call {
|
|
|
10494
10519
|
this.getReconnectDetails = (migratingFromSfuId, previousSessionId) => {
|
|
10495
10520
|
const strategy = this.reconnectStrategy;
|
|
10496
10521
|
const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
|
|
10497
|
-
const announcedTracks = this.publisher?.
|
|
10522
|
+
const announcedTracks = this.publisher?.getAnnouncedTracksForReconnect() || [];
|
|
10498
10523
|
return {
|
|
10499
10524
|
strategy,
|
|
10500
10525
|
announcedTracks,
|
|
@@ -10504,6 +10529,54 @@ class Call {
|
|
|
10504
10529
|
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
10505
10530
|
};
|
|
10506
10531
|
};
|
|
10532
|
+
/**
|
|
10533
|
+
* Prepares the preferred codec for the call.
|
|
10534
|
+
* This is an experimental client feature and subject to change.
|
|
10535
|
+
* @internal
|
|
10536
|
+
*/
|
|
10537
|
+
this.getPreferredPublishOptions = () => {
|
|
10538
|
+
const { preferredCodec, fmtpLine, preferredBitrate, maxSimulcastLayers } = this.clientPublishOptions || {};
|
|
10539
|
+
if (!preferredCodec && !preferredBitrate && !maxSimulcastLayers)
|
|
10540
|
+
return [];
|
|
10541
|
+
const codec = preferredCodec
|
|
10542
|
+
? Codec.create({ name: preferredCodec.split('/').pop(), fmtp: fmtpLine })
|
|
10543
|
+
: undefined;
|
|
10544
|
+
const preferredPublishOptions = [
|
|
10545
|
+
PublishOption.create({
|
|
10546
|
+
trackType: TrackType.VIDEO,
|
|
10547
|
+
codec,
|
|
10548
|
+
bitrate: preferredBitrate,
|
|
10549
|
+
maxSpatialLayers: maxSimulcastLayers,
|
|
10550
|
+
}),
|
|
10551
|
+
];
|
|
10552
|
+
const screenShareSettings = this.screenShare.getSettings();
|
|
10553
|
+
if (screenShareSettings) {
|
|
10554
|
+
preferredPublishOptions.push(PublishOption.create({
|
|
10555
|
+
trackType: TrackType.SCREEN_SHARE,
|
|
10556
|
+
fps: screenShareSettings.maxFramerate,
|
|
10557
|
+
bitrate: screenShareSettings.maxBitrate,
|
|
10558
|
+
}));
|
|
10559
|
+
}
|
|
10560
|
+
return preferredPublishOptions;
|
|
10561
|
+
};
|
|
10562
|
+
/**
|
|
10563
|
+
* Prepares the preferred options for subscribing to tracks.
|
|
10564
|
+
* This is an experimental client feature and subject to change.
|
|
10565
|
+
* @internal
|
|
10566
|
+
*/
|
|
10567
|
+
this.getPreferredSubscribeOptions = () => {
|
|
10568
|
+
const { subscriberCodec, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
10569
|
+
if (!subscriberCodec || !subscriberFmtpLine)
|
|
10570
|
+
return [];
|
|
10571
|
+
return [
|
|
10572
|
+
SubscribeOption.create({
|
|
10573
|
+
trackType: TrackType.VIDEO,
|
|
10574
|
+
codecs: [
|
|
10575
|
+
{ name: subscriberCodec.split('/').pop(), fmtp: subscriberFmtpLine },
|
|
10576
|
+
],
|
|
10577
|
+
}),
|
|
10578
|
+
];
|
|
10579
|
+
};
|
|
10507
10580
|
/**
|
|
10508
10581
|
* Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
|
|
10509
10582
|
* Uses the provided SFU client to restore the ICE connection.
|
|
@@ -10534,9 +10607,9 @@ class Call {
|
|
|
10534
10607
|
* @internal
|
|
10535
10608
|
*/
|
|
10536
10609
|
this.initPublisherAndSubscriber = (opts) => {
|
|
10537
|
-
const { sfuClient, connectionConfig, clientDetails, statsOptions, closePreviousInstances, } = opts;
|
|
10610
|
+
const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, } = opts;
|
|
10538
10611
|
if (closePreviousInstances && this.subscriber) {
|
|
10539
|
-
this.subscriber.
|
|
10612
|
+
this.subscriber.dispose();
|
|
10540
10613
|
}
|
|
10541
10614
|
this.subscriber = new Subscriber({
|
|
10542
10615
|
sfuClient,
|
|
@@ -10555,18 +10628,14 @@ class Call {
|
|
|
10555
10628
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
10556
10629
|
if (!isAnonymous) {
|
|
10557
10630
|
if (closePreviousInstances && this.publisher) {
|
|
10558
|
-
this.publisher.
|
|
10631
|
+
this.publisher.dispose();
|
|
10559
10632
|
}
|
|
10560
|
-
const audioSettings = this.state.settings?.audio;
|
|
10561
|
-
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
10562
|
-
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
10563
10633
|
this.publisher = new Publisher({
|
|
10564
10634
|
sfuClient,
|
|
10565
10635
|
dispatcher: this.dispatcher,
|
|
10566
10636
|
state: this.state,
|
|
10567
10637
|
connectionConfig,
|
|
10568
|
-
|
|
10569
|
-
isRedEnabled,
|
|
10638
|
+
publishOptions,
|
|
10570
10639
|
logTag: String(this.sfuClientTag),
|
|
10571
10640
|
onUnrecoverableError: () => {
|
|
10572
10641
|
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
@@ -10713,47 +10782,31 @@ class Call {
|
|
|
10713
10782
|
* @internal
|
|
10714
10783
|
*/
|
|
10715
10784
|
this.reconnectFast = async () => {
|
|
10716
|
-
|
|
10785
|
+
const reconnectStartTime = Date.now();
|
|
10717
10786
|
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
10718
10787
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
10719
10788
|
await this.join(this.joinCallData);
|
|
10720
|
-
this.sfuStatsReporter?.
|
|
10721
|
-
data: {
|
|
10722
|
-
oneofKind: 'reconnection',
|
|
10723
|
-
reconnection: {
|
|
10724
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
10725
|
-
strategy: WebsocketReconnectStrategy.FAST,
|
|
10726
|
-
},
|
|
10727
|
-
},
|
|
10728
|
-
});
|
|
10789
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.FAST, (Date.now() - reconnectStartTime) / 1000);
|
|
10729
10790
|
};
|
|
10730
10791
|
/**
|
|
10731
10792
|
* Initiates the reconnection flow with the "rejoin" strategy.
|
|
10732
10793
|
* @internal
|
|
10733
10794
|
*/
|
|
10734
10795
|
this.reconnectRejoin = async () => {
|
|
10735
|
-
|
|
10796
|
+
const reconnectStartTime = Date.now();
|
|
10736
10797
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
10737
10798
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
10738
10799
|
await this.join(this.joinCallData);
|
|
10739
10800
|
await this.restorePublishedTracks();
|
|
10740
10801
|
this.restoreSubscribedTracks();
|
|
10741
|
-
this.sfuStatsReporter?.
|
|
10742
|
-
data: {
|
|
10743
|
-
oneofKind: 'reconnection',
|
|
10744
|
-
reconnection: {
|
|
10745
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
10746
|
-
strategy: WebsocketReconnectStrategy.REJOIN,
|
|
10747
|
-
},
|
|
10748
|
-
},
|
|
10749
|
-
});
|
|
10802
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
10750
10803
|
};
|
|
10751
10804
|
/**
|
|
10752
10805
|
* Initiates the reconnection flow with the "migrate" strategy.
|
|
10753
10806
|
* @internal
|
|
10754
10807
|
*/
|
|
10755
10808
|
this.reconnectMigrate = async () => {
|
|
10756
|
-
|
|
10809
|
+
const reconnectStartTime = Date.now();
|
|
10757
10810
|
const currentSfuClient = this.sfuClient;
|
|
10758
10811
|
if (!currentSfuClient) {
|
|
10759
10812
|
throw new Error('Cannot migrate without an active SFU client');
|
|
@@ -10787,20 +10840,12 @@ class Call {
|
|
|
10787
10840
|
this.state.setCallingState(exports.CallingState.JOINED);
|
|
10788
10841
|
}
|
|
10789
10842
|
finally {
|
|
10790
|
-
currentSubscriber?.
|
|
10791
|
-
currentPublisher?.
|
|
10843
|
+
currentSubscriber?.dispose();
|
|
10844
|
+
currentPublisher?.dispose();
|
|
10792
10845
|
// and close the previous SFU client, without specifying close code
|
|
10793
10846
|
currentSfuClient.close();
|
|
10794
10847
|
}
|
|
10795
|
-
this.sfuStatsReporter?.
|
|
10796
|
-
data: {
|
|
10797
|
-
oneofKind: 'reconnection',
|
|
10798
|
-
reconnection: {
|
|
10799
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
10800
|
-
strategy: WebsocketReconnectStrategy.MIGRATE,
|
|
10801
|
-
},
|
|
10802
|
-
},
|
|
10803
|
-
});
|
|
10848
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.MIGRATE, (Date.now() - reconnectStartTime) / 1000);
|
|
10804
10849
|
};
|
|
10805
10850
|
/**
|
|
10806
10851
|
* Registers the various event handlers for reconnection.
|
|
@@ -10877,23 +10922,16 @@ class Call {
|
|
|
10877
10922
|
// the tracks need to be restored in their original order of publishing
|
|
10878
10923
|
// otherwise, we might get `m-lines order mismatch` errors
|
|
10879
10924
|
for (const trackType of this.trackPublishOrder) {
|
|
10925
|
+
let mediaStream;
|
|
10880
10926
|
switch (trackType) {
|
|
10881
10927
|
case TrackType.AUDIO:
|
|
10882
|
-
|
|
10883
|
-
if (audioStream) {
|
|
10884
|
-
await this.publishAudioStream(audioStream);
|
|
10885
|
-
}
|
|
10928
|
+
mediaStream = this.microphone.state.mediaStream;
|
|
10886
10929
|
break;
|
|
10887
10930
|
case TrackType.VIDEO:
|
|
10888
|
-
|
|
10889
|
-
if (videoStream)
|
|
10890
|
-
await this.publishVideoStream(videoStream);
|
|
10931
|
+
mediaStream = this.camera.state.mediaStream;
|
|
10891
10932
|
break;
|
|
10892
10933
|
case TrackType.SCREEN_SHARE:
|
|
10893
|
-
|
|
10894
|
-
if (screenShareStream) {
|
|
10895
|
-
await this.publishScreenShareStream(screenShareStream);
|
|
10896
|
-
}
|
|
10934
|
+
mediaStream = this.screenShare.state.mediaStream;
|
|
10897
10935
|
break;
|
|
10898
10936
|
// screen share audio can't exist without a screen share, so we handle it there
|
|
10899
10937
|
case TrackType.SCREEN_SHARE_AUDIO:
|
|
@@ -10903,6 +10941,8 @@ class Call {
|
|
|
10903
10941
|
ensureExhausted(trackType, 'Unknown track type');
|
|
10904
10942
|
break;
|
|
10905
10943
|
}
|
|
10944
|
+
if (mediaStream)
|
|
10945
|
+
await this.publish(mediaStream, trackType);
|
|
10906
10946
|
}
|
|
10907
10947
|
};
|
|
10908
10948
|
/**
|
|
@@ -10917,105 +10957,111 @@ class Call {
|
|
|
10917
10957
|
};
|
|
10918
10958
|
/**
|
|
10919
10959
|
* Starts publishing the given video stream to the call.
|
|
10920
|
-
*
|
|
10921
|
-
*
|
|
10922
|
-
* Consecutive calls to this method will replace the previously published stream.
|
|
10923
|
-
* The previous video stream will be stopped.
|
|
10924
|
-
*
|
|
10925
|
-
* @param videoStream the video stream to publish.
|
|
10960
|
+
* @deprecated use `call.publish()`.
|
|
10926
10961
|
*/
|
|
10927
10962
|
this.publishVideoStream = async (videoStream) => {
|
|
10928
|
-
|
|
10929
|
-
throw new Error(`Call not joined yet.`);
|
|
10930
|
-
// joining is in progress, and we should wait until the client is ready
|
|
10931
|
-
await this.sfuClient.joinTask;
|
|
10932
|
-
if (!this.permissionsContext.hasPermission(OwnCapability.SEND_VIDEO)) {
|
|
10933
|
-
throw new Error('No permission to publish video');
|
|
10934
|
-
}
|
|
10935
|
-
if (!this.publisher)
|
|
10936
|
-
throw new Error('Publisher is not initialized');
|
|
10937
|
-
const [videoTrack] = videoStream.getVideoTracks();
|
|
10938
|
-
if (!videoTrack)
|
|
10939
|
-
throw new Error('There is no video track in the stream');
|
|
10940
|
-
if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
|
|
10941
|
-
this.trackPublishOrder.push(TrackType.VIDEO);
|
|
10942
|
-
}
|
|
10943
|
-
await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, this.publishOptions);
|
|
10963
|
+
await this.publish(videoStream, TrackType.VIDEO);
|
|
10944
10964
|
};
|
|
10945
10965
|
/**
|
|
10946
10966
|
* Starts publishing the given audio stream to the call.
|
|
10947
|
-
*
|
|
10948
|
-
*
|
|
10949
|
-
* Consecutive calls to this method will replace the audio stream that is currently being published.
|
|
10950
|
-
* The previous audio stream will be stopped.
|
|
10951
|
-
*
|
|
10952
|
-
* @param audioStream the audio stream to publish.
|
|
10967
|
+
* @deprecated use `call.publish()`
|
|
10953
10968
|
*/
|
|
10954
10969
|
this.publishAudioStream = async (audioStream) => {
|
|
10955
|
-
|
|
10956
|
-
throw new Error(`Call not joined yet.`);
|
|
10957
|
-
// joining is in progress, and we should wait until the client is ready
|
|
10958
|
-
await this.sfuClient.joinTask;
|
|
10959
|
-
if (!this.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO)) {
|
|
10960
|
-
throw new Error('No permission to publish audio');
|
|
10961
|
-
}
|
|
10962
|
-
if (!this.publisher)
|
|
10963
|
-
throw new Error('Publisher is not initialized');
|
|
10964
|
-
const [audioTrack] = audioStream.getAudioTracks();
|
|
10965
|
-
if (!audioTrack)
|
|
10966
|
-
throw new Error('There is no audio track in the stream');
|
|
10967
|
-
if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
|
|
10968
|
-
this.trackPublishOrder.push(TrackType.AUDIO);
|
|
10969
|
-
}
|
|
10970
|
-
await this.publisher.publishStream(audioStream, audioTrack, TrackType.AUDIO);
|
|
10970
|
+
await this.publish(audioStream, TrackType.AUDIO);
|
|
10971
10971
|
};
|
|
10972
10972
|
/**
|
|
10973
10973
|
* Starts publishing the given screen-share stream to the call.
|
|
10974
|
-
*
|
|
10975
|
-
* Consecutive calls to this method will replace the previous screen-share stream.
|
|
10976
|
-
* The previous screen-share stream will be stopped.
|
|
10977
|
-
*
|
|
10978
|
-
* @param screenShareStream the screen-share stream to publish.
|
|
10974
|
+
* @deprecated use `call.publish()`
|
|
10979
10975
|
*/
|
|
10980
10976
|
this.publishScreenShareStream = async (screenShareStream) => {
|
|
10977
|
+
await this.publish(screenShareStream, TrackType.SCREEN_SHARE);
|
|
10978
|
+
};
|
|
10979
|
+
/**
|
|
10980
|
+
* Publishes the given media stream.
|
|
10981
|
+
*
|
|
10982
|
+
* @param mediaStream the media stream to publish.
|
|
10983
|
+
* @param trackType the type of the track to announce.
|
|
10984
|
+
*/
|
|
10985
|
+
this.publish = async (mediaStream, trackType) => {
|
|
10981
10986
|
if (!this.sfuClient)
|
|
10982
10987
|
throw new Error(`Call not joined yet.`);
|
|
10983
10988
|
// joining is in progress, and we should wait until the client is ready
|
|
10984
10989
|
await this.sfuClient.joinTask;
|
|
10985
|
-
if (!this.permissionsContext.
|
|
10986
|
-
throw new Error(
|
|
10990
|
+
if (!this.permissionsContext.canPublish(trackType)) {
|
|
10991
|
+
throw new Error(`No permission to publish ${TrackType[trackType]}`);
|
|
10987
10992
|
}
|
|
10988
10993
|
if (!this.publisher)
|
|
10989
10994
|
throw new Error('Publisher is not initialized');
|
|
10990
|
-
const [
|
|
10991
|
-
|
|
10992
|
-
|
|
10995
|
+
const [track] = isAudioTrackType(trackType)
|
|
10996
|
+
? mediaStream.getAudioTracks()
|
|
10997
|
+
: mediaStream.getVideoTracks();
|
|
10998
|
+
if (!track) {
|
|
10999
|
+
throw new Error(`There is no ${TrackType[trackType]} track in the stream`);
|
|
10993
11000
|
}
|
|
10994
|
-
if (
|
|
10995
|
-
|
|
10996
|
-
}
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
this.
|
|
11001
|
+
if (track.readyState === 'ended') {
|
|
11002
|
+
throw new Error(`Can't publish ended tracks.`);
|
|
11003
|
+
}
|
|
11004
|
+
pushToIfMissing(this.trackPublishOrder, trackType);
|
|
11005
|
+
await this.publisher.publish(track, trackType);
|
|
11006
|
+
const trackTypes = [trackType];
|
|
11007
|
+
if (trackType === TrackType.SCREEN_SHARE) {
|
|
11008
|
+
const [audioTrack] = mediaStream.getAudioTracks();
|
|
11009
|
+
if (audioTrack) {
|
|
11010
|
+
pushToIfMissing(this.trackPublishOrder, TrackType.SCREEN_SHARE_AUDIO);
|
|
11011
|
+
await this.publisher.publish(audioTrack, TrackType.SCREEN_SHARE_AUDIO);
|
|
11012
|
+
trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
11005
11013
|
}
|
|
11006
|
-
await this.publisher.publishStream(screenShareStream, screenShareAudioTrack, TrackType.SCREEN_SHARE_AUDIO, opts);
|
|
11007
11014
|
}
|
|
11015
|
+
await this.updateLocalStreamState(mediaStream, ...trackTypes);
|
|
11008
11016
|
};
|
|
11009
11017
|
/**
|
|
11010
11018
|
* Stops publishing the given track type to the call, if it is currently being published.
|
|
11011
|
-
* Underlying track will be stopped and removed from the publisher.
|
|
11012
11019
|
*
|
|
11013
|
-
* @param
|
|
11014
|
-
|
|
11020
|
+
* @param trackTypes the track types to stop publishing.
|
|
11021
|
+
*/
|
|
11022
|
+
this.stopPublish = async (...trackTypes) => {
|
|
11023
|
+
if (!this.sfuClient || !this.publisher)
|
|
11024
|
+
return;
|
|
11025
|
+
this.publisher.stopTracks(...trackTypes);
|
|
11026
|
+
await this.updateLocalStreamState(undefined, ...trackTypes);
|
|
11027
|
+
};
|
|
11028
|
+
/**
|
|
11029
|
+
* Updates the call state with the new stream.
|
|
11030
|
+
*
|
|
11031
|
+
* @param mediaStream the new stream to update the call state with.
|
|
11032
|
+
* If undefined, the stream will be removed from the call state.
|
|
11033
|
+
* @param trackTypes the track types to update the call state with.
|
|
11034
|
+
*/
|
|
11035
|
+
this.updateLocalStreamState = async (mediaStream, ...trackTypes) => {
|
|
11036
|
+
if (!this.sfuClient || !this.sfuClient.sessionId)
|
|
11037
|
+
return;
|
|
11038
|
+
await this.notifyTrackMuteState(!mediaStream, ...trackTypes);
|
|
11039
|
+
const { sessionId } = this.sfuClient;
|
|
11040
|
+
for (const trackType of trackTypes) {
|
|
11041
|
+
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
11042
|
+
if (!streamStateProp)
|
|
11043
|
+
continue;
|
|
11044
|
+
this.state.updateParticipant(sessionId, (p) => ({
|
|
11045
|
+
publishedTracks: mediaStream
|
|
11046
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
11047
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
11048
|
+
[streamStateProp]: mediaStream,
|
|
11049
|
+
}));
|
|
11050
|
+
}
|
|
11051
|
+
};
|
|
11052
|
+
/**
|
|
11053
|
+
* Updates the preferred publishing options
|
|
11054
|
+
*
|
|
11055
|
+
* @internal
|
|
11056
|
+
* @param options the options to use.
|
|
11015
11057
|
*/
|
|
11016
|
-
this.
|
|
11017
|
-
this.logger('
|
|
11018
|
-
|
|
11058
|
+
this.updatePublishOptions = (options) => {
|
|
11059
|
+
this.logger('warn', '[call.updatePublishOptions]: You are manually overriding the publish options for this call. ' +
|
|
11060
|
+
'This is not recommended, and it can cause call stability/compatibility issues. Use with caution.');
|
|
11061
|
+
if (this.state.callingState === exports.CallingState.JOINED) {
|
|
11062
|
+
this.logger('warn', 'Updating publish options after joining the call does not have an effect');
|
|
11063
|
+
}
|
|
11064
|
+
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
11019
11065
|
};
|
|
11020
11066
|
/**
|
|
11021
11067
|
* Notifies the SFU that a noise cancellation process has started.
|
|
@@ -11037,6 +11083,15 @@ class Call {
|
|
|
11037
11083
|
this.logger('warn', 'Failed to notify stop of noise cancellation', err);
|
|
11038
11084
|
});
|
|
11039
11085
|
};
|
|
11086
|
+
/**
|
|
11087
|
+
* Notifies the SFU about the mute state of the given track types.
|
|
11088
|
+
* @internal
|
|
11089
|
+
*/
|
|
11090
|
+
this.notifyTrackMuteState = async (muted, ...trackTypes) => {
|
|
11091
|
+
if (!this.sfuClient)
|
|
11092
|
+
return;
|
|
11093
|
+
await this.sfuClient.updateMuteStates(trackTypes.map((trackType) => ({ trackType, muted })));
|
|
11094
|
+
};
|
|
11040
11095
|
/**
|
|
11041
11096
|
* Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
|
|
11042
11097
|
* This is usually helpful when detailed stats for a specific participant are needed.
|
|
@@ -11181,7 +11236,43 @@ class Call {
|
|
|
11181
11236
|
return this.streamClient.post(`${this.streamClientBasePath}/stop_transcription`);
|
|
11182
11237
|
};
|
|
11183
11238
|
/**
|
|
11184
|
-
*
|
|
11239
|
+
* Starts the closed captions of the call.
|
|
11240
|
+
*/
|
|
11241
|
+
this.startClosedCaptions = async (options) => {
|
|
11242
|
+
const trx = this.state.setCaptioning(true); // optimistic update
|
|
11243
|
+
try {
|
|
11244
|
+
return await this.streamClient.post(`${this.streamClientBasePath}/start_closed_captions`, options);
|
|
11245
|
+
}
|
|
11246
|
+
catch (err) {
|
|
11247
|
+
trx.rollback(); // revert the optimistic update
|
|
11248
|
+
throw err;
|
|
11249
|
+
}
|
|
11250
|
+
};
|
|
11251
|
+
/**
|
|
11252
|
+
* Stops the closed captions of the call.
|
|
11253
|
+
*/
|
|
11254
|
+
this.stopClosedCaptions = async (options) => {
|
|
11255
|
+
const trx = this.state.setCaptioning(false); // optimistic update
|
|
11256
|
+
try {
|
|
11257
|
+
return await this.streamClient.post(`${this.streamClientBasePath}/stop_closed_captions`, options);
|
|
11258
|
+
}
|
|
11259
|
+
catch (err) {
|
|
11260
|
+
trx.rollback(); // revert the optimistic update
|
|
11261
|
+
throw err;
|
|
11262
|
+
}
|
|
11263
|
+
};
|
|
11264
|
+
/**
|
|
11265
|
+
* Updates the closed caption settings.
|
|
11266
|
+
*
|
|
11267
|
+
* @param config the closed caption settings to apply
|
|
11268
|
+
*/
|
|
11269
|
+
this.updateClosedCaptionSettings = (config) => {
|
|
11270
|
+
this.state.updateClosedCaptionSettings(config);
|
|
11271
|
+
};
|
|
11272
|
+
/**
|
|
11273
|
+
* Sends a `call.permission_request` event to all users connected to the call.
|
|
11274
|
+
* The call settings object contains information about which permissions can be requested during a call
|
|
11275
|
+
* (for example, a user might be allowed to request permission to publish audio, but not video).
|
|
11185
11276
|
*/
|
|
11186
11277
|
this.requestPermissions = async (data) => {
|
|
11187
11278
|
const { permissions } = data;
|
|
@@ -11464,70 +11555,14 @@ class Call {
|
|
|
11464
11555
|
*
|
|
11465
11556
|
* @internal
|
|
11466
11557
|
*/
|
|
11467
|
-
this.applyDeviceConfig = async (
|
|
11468
|
-
await this.
|
|
11558
|
+
this.applyDeviceConfig = async (settings, publish) => {
|
|
11559
|
+
await this.camera.apply(settings.video, publish).catch((err) => {
|
|
11469
11560
|
this.logger('warn', 'Camera init failed', err);
|
|
11470
11561
|
});
|
|
11471
|
-
await this.
|
|
11562
|
+
await this.microphone.apply(settings.audio, publish).catch((err) => {
|
|
11472
11563
|
this.logger('warn', 'Mic init failed', err);
|
|
11473
11564
|
});
|
|
11474
11565
|
};
|
|
11475
|
-
this.initCamera = async (options) => {
|
|
11476
|
-
// Wait for any in progress camera operation
|
|
11477
|
-
await this.camera.statusChangeSettled();
|
|
11478
|
-
if (this.state.localParticipant?.videoStream ||
|
|
11479
|
-
!this.permissionsContext.hasPermission('send-video')) {
|
|
11480
|
-
return;
|
|
11481
|
-
}
|
|
11482
|
-
// Set camera direction if it's not yet set
|
|
11483
|
-
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
11484
|
-
let defaultDirection = 'front';
|
|
11485
|
-
const backendSetting = this.state.settings?.video.camera_facing;
|
|
11486
|
-
if (backendSetting) {
|
|
11487
|
-
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
11488
|
-
}
|
|
11489
|
-
this.camera.state.setDirection(defaultDirection);
|
|
11490
|
-
}
|
|
11491
|
-
// Set target resolution
|
|
11492
|
-
const targetResolution = this.state.settings?.video.target_resolution;
|
|
11493
|
-
if (targetResolution) {
|
|
11494
|
-
await this.camera.selectTargetResolution(targetResolution);
|
|
11495
|
-
}
|
|
11496
|
-
if (options.setStatus) {
|
|
11497
|
-
// Publish already that was set before we joined
|
|
11498
|
-
if (this.camera.enabled &&
|
|
11499
|
-
this.camera.state.mediaStream &&
|
|
11500
|
-
!this.publisher?.isPublishing(TrackType.VIDEO)) {
|
|
11501
|
-
await this.publishVideoStream(this.camera.state.mediaStream);
|
|
11502
|
-
}
|
|
11503
|
-
// Start camera if backend config specifies, and there is no local setting
|
|
11504
|
-
if (this.camera.state.status === undefined &&
|
|
11505
|
-
this.state.settings?.video.camera_default_on) {
|
|
11506
|
-
await this.camera.enable();
|
|
11507
|
-
}
|
|
11508
|
-
}
|
|
11509
|
-
};
|
|
11510
|
-
this.initMic = async (options) => {
|
|
11511
|
-
// Wait for any in progress mic operation
|
|
11512
|
-
await this.microphone.statusChangeSettled();
|
|
11513
|
-
if (this.state.localParticipant?.audioStream ||
|
|
11514
|
-
!this.permissionsContext.hasPermission('send-audio')) {
|
|
11515
|
-
return;
|
|
11516
|
-
}
|
|
11517
|
-
if (options.setStatus) {
|
|
11518
|
-
// Publish media stream that was set before we joined
|
|
11519
|
-
if (this.microphone.enabled &&
|
|
11520
|
-
this.microphone.state.mediaStream &&
|
|
11521
|
-
!this.publisher?.isPublishing(TrackType.AUDIO)) {
|
|
11522
|
-
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
11523
|
-
}
|
|
11524
|
-
// Start mic if backend config specifies, and there is no local setting
|
|
11525
|
-
if (this.microphone.state.status === undefined &&
|
|
11526
|
-
this.state.settings?.audio.mic_default_on) {
|
|
11527
|
-
await this.microphone.enable();
|
|
11528
|
-
}
|
|
11529
|
-
}
|
|
11530
|
-
};
|
|
11531
11566
|
/**
|
|
11532
11567
|
* Will begin tracking the given element for visibility changes within the
|
|
11533
11568
|
* configured viewport element (`call.setViewport`).
|
|
@@ -11676,109 +11711,6 @@ class Call {
|
|
|
11676
11711
|
this.screenShare = new ScreenShareManager(this);
|
|
11677
11712
|
this.dynascaleManager = new DynascaleManager(this.state, this.speaker);
|
|
11678
11713
|
}
|
|
11679
|
-
async setup() {
|
|
11680
|
-
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
11681
|
-
if (this.initialized)
|
|
11682
|
-
return;
|
|
11683
|
-
this.leaveCallHooks.add(this.on('all', (event) => {
|
|
11684
|
-
// update state with the latest event data
|
|
11685
|
-
this.state.updateFromEvent(event);
|
|
11686
|
-
}));
|
|
11687
|
-
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
11688
|
-
this.registerEffects();
|
|
11689
|
-
this.registerReconnectHandlers();
|
|
11690
|
-
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
11691
|
-
this.state.setCallingState(exports.CallingState.IDLE);
|
|
11692
|
-
}
|
|
11693
|
-
this.initialized = true;
|
|
11694
|
-
});
|
|
11695
|
-
}
|
|
11696
|
-
registerEffects() {
|
|
11697
|
-
this.leaveCallHooks.add(
|
|
11698
|
-
// handles updating the permissions context when the settings change.
|
|
11699
|
-
createSubscription(this.state.settings$, (settings) => {
|
|
11700
|
-
if (!settings)
|
|
11701
|
-
return;
|
|
11702
|
-
this.permissionsContext.setCallSettings(settings);
|
|
11703
|
-
}));
|
|
11704
|
-
this.leaveCallHooks.add(
|
|
11705
|
-
// handle the case when the user permissions are modified.
|
|
11706
|
-
createSafeAsyncSubscription(this.state.ownCapabilities$, this.handleOwnCapabilitiesUpdated));
|
|
11707
|
-
this.leaveCallHooks.add(
|
|
11708
|
-
// handles the case when the user is blocked by the call owner.
|
|
11709
|
-
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
11710
|
-
if (!blockedUserIds || blockedUserIds.length === 0)
|
|
11711
|
-
return;
|
|
11712
|
-
const currentUserId = this.currentUserId;
|
|
11713
|
-
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
11714
|
-
this.logger('info', 'Leaving call because of being blocked');
|
|
11715
|
-
await this.leave({ reason: 'user blocked' }).catch((err) => {
|
|
11716
|
-
this.logger('error', 'Error leaving call after being blocked', err);
|
|
11717
|
-
});
|
|
11718
|
-
}
|
|
11719
|
-
}));
|
|
11720
|
-
this.leaveCallHooks.add(
|
|
11721
|
-
// cancel auto-drop when call is
|
|
11722
|
-
createSubscription(this.state.session$, (session) => {
|
|
11723
|
-
if (!this.ringing)
|
|
11724
|
-
return;
|
|
11725
|
-
const receiverId = this.clientStore.connectedUser?.id;
|
|
11726
|
-
if (!receiverId)
|
|
11727
|
-
return;
|
|
11728
|
-
const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
|
|
11729
|
-
const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
|
|
11730
|
-
if (isAcceptedByMe || isRejectedByMe) {
|
|
11731
|
-
this.cancelAutoDrop();
|
|
11732
|
-
}
|
|
11733
|
-
}));
|
|
11734
|
-
this.leaveCallHooks.add(
|
|
11735
|
-
// "ringing" mode effects and event handlers
|
|
11736
|
-
createSubscription(this.ringingSubject, (isRinging) => {
|
|
11737
|
-
if (!isRinging)
|
|
11738
|
-
return;
|
|
11739
|
-
const callSession = this.state.session;
|
|
11740
|
-
const receiver_id = this.clientStore.connectedUser?.id;
|
|
11741
|
-
const ended_at = callSession?.ended_at;
|
|
11742
|
-
const created_by_id = this.state.createdBy?.id;
|
|
11743
|
-
const rejected_by = callSession?.rejected_by;
|
|
11744
|
-
const accepted_by = callSession?.accepted_by;
|
|
11745
|
-
let leaveCallIdle = false;
|
|
11746
|
-
if (ended_at) {
|
|
11747
|
-
// call was ended before it was accepted or rejected so we should leave it to idle
|
|
11748
|
-
leaveCallIdle = true;
|
|
11749
|
-
}
|
|
11750
|
-
else if (created_by_id && rejected_by) {
|
|
11751
|
-
if (rejected_by[created_by_id]) {
|
|
11752
|
-
// call was cancelled by the caller
|
|
11753
|
-
leaveCallIdle = true;
|
|
11754
|
-
}
|
|
11755
|
-
}
|
|
11756
|
-
else if (receiver_id && rejected_by) {
|
|
11757
|
-
if (rejected_by[receiver_id]) {
|
|
11758
|
-
// call was rejected by the receiver in some other device
|
|
11759
|
-
leaveCallIdle = true;
|
|
11760
|
-
}
|
|
11761
|
-
}
|
|
11762
|
-
else if (receiver_id && accepted_by) {
|
|
11763
|
-
if (accepted_by[receiver_id]) {
|
|
11764
|
-
// call was accepted by the receiver in some other device
|
|
11765
|
-
leaveCallIdle = true;
|
|
11766
|
-
}
|
|
11767
|
-
}
|
|
11768
|
-
if (leaveCallIdle) {
|
|
11769
|
-
if (this.state.callingState !== exports.CallingState.IDLE) {
|
|
11770
|
-
this.state.setCallingState(exports.CallingState.IDLE);
|
|
11771
|
-
}
|
|
11772
|
-
}
|
|
11773
|
-
else {
|
|
11774
|
-
if (this.state.callingState === exports.CallingState.IDLE) {
|
|
11775
|
-
this.state.setCallingState(exports.CallingState.RINGING);
|
|
11776
|
-
}
|
|
11777
|
-
this.scheduleAutoDrop();
|
|
11778
|
-
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
11779
|
-
}
|
|
11780
|
-
}));
|
|
11781
|
-
}
|
|
11782
11714
|
/**
|
|
11783
11715
|
* A flag indicating whether the call is "ringing" type of call.
|
|
11784
11716
|
*/
|
|
@@ -11797,15 +11729,6 @@ class Call {
|
|
|
11797
11729
|
get isCreatedByMe() {
|
|
11798
11730
|
return this.state.createdBy?.id === this.currentUserId;
|
|
11799
11731
|
}
|
|
11800
|
-
/**
|
|
11801
|
-
* Updates the preferred publishing options
|
|
11802
|
-
*
|
|
11803
|
-
* @internal
|
|
11804
|
-
* @param options the options to use.
|
|
11805
|
-
*/
|
|
11806
|
-
updatePublishOptions(options) {
|
|
11807
|
-
this.publishOptions = { ...this.publishOptions, ...options };
|
|
11808
|
-
}
|
|
11809
11732
|
}
|
|
11810
11733
|
|
|
11811
11734
|
/**
|
|
@@ -12913,7 +12836,7 @@ class StreamClient {
|
|
|
12913
12836
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
12914
12837
|
};
|
|
12915
12838
|
this.getUserAgent = () => {
|
|
12916
|
-
const version = "1.
|
|
12839
|
+
const version = "1.15.0";
|
|
12917
12840
|
return (this.userAgent ||
|
|
12918
12841
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
12919
12842
|
};
|
|
@@ -13213,7 +13136,7 @@ class StreamVideoClient {
|
|
|
13213
13136
|
clientStore: this.writeableStateStore,
|
|
13214
13137
|
});
|
|
13215
13138
|
call.state.updateFromCallResponse(c.call);
|
|
13216
|
-
await call.applyDeviceConfig(false);
|
|
13139
|
+
await call.applyDeviceConfig(c.call.settings, false);
|
|
13217
13140
|
if (data.watch) {
|
|
13218
13141
|
this.writeableStateStore.registerCall(call);
|
|
13219
13142
|
}
|
|
@@ -13439,6 +13362,7 @@ exports.CameraManagerState = CameraManagerState;
|
|
|
13439
13362
|
exports.ChannelConfigWithInfoAutomodBehaviorEnum = ChannelConfigWithInfoAutomodBehaviorEnum;
|
|
13440
13363
|
exports.ChannelConfigWithInfoAutomodEnum = ChannelConfigWithInfoAutomodEnum;
|
|
13441
13364
|
exports.ChannelConfigWithInfoBlocklistBehaviorEnum = ChannelConfigWithInfoBlocklistBehaviorEnum;
|
|
13365
|
+
exports.ChannelOwnCapability = ChannelOwnCapability;
|
|
13442
13366
|
exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnum;
|
|
13443
13367
|
exports.DynascaleManager = DynascaleManager;
|
|
13444
13368
|
exports.ErrorFromResponse = ErrorFromResponse;
|
|
@@ -13461,7 +13385,9 @@ exports.StreamSfuClient = StreamSfuClient;
|
|
|
13461
13385
|
exports.StreamVideoClient = StreamVideoClient;
|
|
13462
13386
|
exports.StreamVideoReadOnlyStateStore = StreamVideoReadOnlyStateStore;
|
|
13463
13387
|
exports.StreamVideoWriteableStateStore = StreamVideoWriteableStateStore;
|
|
13388
|
+
exports.TranscriptionSettingsRequestClosedCaptionModeEnum = TranscriptionSettingsRequestClosedCaptionModeEnum;
|
|
13464
13389
|
exports.TranscriptionSettingsRequestModeEnum = TranscriptionSettingsRequestModeEnum;
|
|
13390
|
+
exports.TranscriptionSettingsResponseClosedCaptionModeEnum = TranscriptionSettingsResponseClosedCaptionModeEnum;
|
|
13465
13391
|
exports.TranscriptionSettingsResponseModeEnum = TranscriptionSettingsResponseModeEnum;
|
|
13466
13392
|
exports.VideoSettingsRequestCameraFacingEnum = VideoSettingsRequestCameraFacingEnum;
|
|
13467
13393
|
exports.VideoSettingsResponseCameraFacingEnum = VideoSettingsResponseCameraFacingEnum;
|