@reactoo/watchtogether-sdk-js 2.6.68 → 2.6.69
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/dist/watchtogether-sdk.js +6 -6
- package/dist/watchtogether-sdk.min.js +2 -2
- package/example/index.html +23 -11
- package/package.json +1 -1
- package/src/models/room-session.js +8 -5
- package/src/models/room.js +1 -0
- package/src/modules/wt-room.js +495 -53
- package/src/modules/wt-utils.js +29 -1
package/src/modules/wt-room.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import adapter from 'webrtc-adapter';
|
|
4
4
|
import emitter from './wt-emitter';
|
|
5
|
-
import {decodeJanusDisplay, generateUUID, wait} from "./wt-utils";
|
|
5
|
+
import {decodeJanusDisplay, generateUUID, maxJitter, median, wait} from "./wt-utils";
|
|
6
6
|
|
|
7
7
|
class Room {
|
|
8
8
|
|
|
@@ -182,11 +182,15 @@ class RoomSession {
|
|
|
182
182
|
this.sessiontype = type;
|
|
183
183
|
this.initialBitrate = 0;
|
|
184
184
|
this.simulcast = false;
|
|
185
|
-
this.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
this.simulcastMode = 'controlled'; // controlled, manual, auto
|
|
186
|
+
this.simulcastDefaultManualSubstream = 0; // 0 = maximum quality
|
|
187
|
+
|
|
188
|
+
// ordered from low to high
|
|
189
|
+
this.simulcastBitrates = [
|
|
190
|
+
{ rid: 'l', active: true, maxBitrate: 180000, maxFramerate: 20, scaleResolutionDownBy: 3.3333333333333335, priority: "low" },
|
|
191
|
+
{ rid: 'm', active: true, maxBitrate: 800000, maxFramerate: 25, scaleResolutionDownBy: 1.3333333333333333, priority: "low" },
|
|
192
|
+
{ rid: 'h', active: true, maxBitrate: 1700000, maxFramerate: 30, priority: "low" },
|
|
193
|
+
];
|
|
190
194
|
this.recordingFilename = null;
|
|
191
195
|
this.pluginName = RoomSession.sessionTypes[type];
|
|
192
196
|
this.id = null;
|
|
@@ -205,7 +209,15 @@ class RoomSession {
|
|
|
205
209
|
this.isMuted = [];
|
|
206
210
|
this.isVideoEnabled = false;
|
|
207
211
|
this.isAudioEnabed = false;
|
|
208
|
-
this.
|
|
212
|
+
this._statsMaxLength = 21;
|
|
213
|
+
this._upStatsLength = 20;
|
|
214
|
+
this._downStatsLength = 5;
|
|
215
|
+
this._statsTimeoutStopped = true;
|
|
216
|
+
this._statsTimeoutId = null;
|
|
217
|
+
this._statsInterval = 1000;
|
|
218
|
+
this._aqInterval = 2500;
|
|
219
|
+
this._aqTimeoutId = null;
|
|
220
|
+
this._sendMessageTimeout = 5000;
|
|
209
221
|
this._retries = 0;
|
|
210
222
|
this._maxRetries = 5;
|
|
211
223
|
this._keepAliveId = null;
|
|
@@ -388,7 +400,7 @@ class RoomSession {
|
|
|
388
400
|
this.ws.removeEventListener('message', parseResponse);
|
|
389
401
|
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
390
402
|
reject({type: 'warning', id: 3, message: 'send timeout', data: requestData});
|
|
391
|
-
}, this.
|
|
403
|
+
}, this._sendMessageTimeout);
|
|
392
404
|
this._abortController.signal.addEventListener('abort', abortResponse);
|
|
393
405
|
this.ws.send(JSON.stringify(requestData));
|
|
394
406
|
} else {
|
|
@@ -402,7 +414,7 @@ class RoomSession {
|
|
|
402
414
|
return Promise.reject(e);
|
|
403
415
|
}
|
|
404
416
|
else if(e.id === 29 && retry > 0) {
|
|
405
|
-
return wait(this.
|
|
417
|
+
return wait(this._sendMessageTimeout).then(() => this._send(request, ignoreResponse, dontResolveOnAck, retry - 1));
|
|
406
418
|
}
|
|
407
419
|
else if(retry > 0) {
|
|
408
420
|
return this._send(request, ignoreResponse, dontResolveOnAck, retry - 1);
|
|
@@ -533,6 +545,8 @@ class RoomSession {
|
|
|
533
545
|
let list = msg["publishers"] || {};
|
|
534
546
|
let leaving = msg["leaving"];
|
|
535
547
|
let kicked = msg["kicked"];
|
|
548
|
+
let substream = msg["substream"];
|
|
549
|
+
let temporal = msg["temporal"];
|
|
536
550
|
|
|
537
551
|
//let joining = msg["joining"];
|
|
538
552
|
let unpublished = msg["unpublished"];
|
|
@@ -596,6 +610,14 @@ class RoomSession {
|
|
|
596
610
|
}
|
|
597
611
|
else if (event === "event") {
|
|
598
612
|
|
|
613
|
+
if(substream !== undefined && substream !== null) {
|
|
614
|
+
this._log('Substream event:', substream, sender);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if(temporal !== undefined && temporal !== null) {
|
|
618
|
+
this._log('Temporal event:', temporal);
|
|
619
|
+
}
|
|
620
|
+
|
|
599
621
|
if (msg["streams"] !== undefined && msg["streams"] !== null) {
|
|
600
622
|
this._log('Got my own streams back', msg["streams"]);
|
|
601
623
|
}
|
|
@@ -729,7 +751,8 @@ class RoomSession {
|
|
|
729
751
|
}
|
|
730
752
|
|
|
731
753
|
}
|
|
732
|
-
}
|
|
754
|
+
}
|
|
755
|
+
else if (type === "webrtcup") {
|
|
733
756
|
|
|
734
757
|
if(this.simulcast) {
|
|
735
758
|
return;
|
|
@@ -757,13 +780,28 @@ class RoomSession {
|
|
|
757
780
|
let event = msg["videoroom"];
|
|
758
781
|
let error = msg["error"];
|
|
759
782
|
let substream = msg["substream"];
|
|
783
|
+
let mid = msg["mid"];
|
|
784
|
+
let temporal = msg["temporal"];
|
|
785
|
+
|
|
786
|
+
if(substream !== undefined && substream !== null) {
|
|
787
|
+
this._log('Substream: ',substream);
|
|
788
|
+
this._setSelectedSubstream(sender, mid, substream);
|
|
789
|
+
this._resetStats(sender, mid);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if(temporal !== undefined && temporal !== null) {
|
|
793
|
+
this._log('Temporal: ', temporal);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if(type === "webrtcup") {
|
|
797
|
+
this.requestKeyFrame(handle.handleId);
|
|
798
|
+
}
|
|
760
799
|
|
|
761
800
|
if (event === "updated") {
|
|
762
801
|
this._log('Remote has updated tracks', msg);
|
|
763
802
|
if(msg["streams"]) {
|
|
764
803
|
this._updateTransceiverMap(handle.handleId, msg["streams"]);
|
|
765
804
|
}
|
|
766
|
-
|
|
767
805
|
}
|
|
768
806
|
|
|
769
807
|
if (event === "attached") {
|
|
@@ -923,7 +961,11 @@ class RoomSession {
|
|
|
923
961
|
dtmfSender: null,
|
|
924
962
|
trickle: true,
|
|
925
963
|
iceDone: false,
|
|
926
|
-
isIceRestarting: false
|
|
964
|
+
isIceRestarting: false,
|
|
965
|
+
stats: {},
|
|
966
|
+
selectedSubstream: {},
|
|
967
|
+
simulcastSubstreamManualSelect: {},
|
|
968
|
+
simulcastSwitchFailedAttempts: {},
|
|
927
969
|
};
|
|
928
970
|
|
|
929
971
|
if (handleId === this.handleId) {
|
|
@@ -976,7 +1018,11 @@ class RoomSession {
|
|
|
976
1018
|
dtmfSender: null,
|
|
977
1019
|
trickle: true,
|
|
978
1020
|
iceDone: false,
|
|
979
|
-
isIceRestarting: false
|
|
1021
|
+
isIceRestarting: false,
|
|
1022
|
+
stats: {},
|
|
1023
|
+
selectedSubstream: {},
|
|
1024
|
+
simulcastSubstreamManualSelect: {},
|
|
1025
|
+
simulcastSwitchFailedAttempts: {},
|
|
980
1026
|
}
|
|
981
1027
|
};
|
|
982
1028
|
this._participants.push(handle);
|
|
@@ -1053,16 +1099,23 @@ class RoomSession {
|
|
|
1053
1099
|
return;
|
|
1054
1100
|
}
|
|
1055
1101
|
let config = handle.webrtcStuff;
|
|
1056
|
-
config.tracksMap = structuredClone(streams.map(s =>
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1102
|
+
config.tracksMap = structuredClone(streams.map(s => {
|
|
1103
|
+
let source = null;
|
|
1104
|
+
try {
|
|
1105
|
+
source = JSON.parse(s.description)?.source;
|
|
1106
|
+
} catch(e) {}
|
|
1107
|
+
return {
|
|
1108
|
+
active: !s.disabled,
|
|
1109
|
+
description: s.description,
|
|
1110
|
+
source: source,
|
|
1111
|
+
display: s.display,
|
|
1112
|
+
id: s.id,
|
|
1113
|
+
mid: s.mid,
|
|
1114
|
+
mindex: s.mindex,
|
|
1115
|
+
codec: s.codec,
|
|
1116
|
+
type: s.type,
|
|
1117
|
+
}
|
|
1118
|
+
}));
|
|
1066
1119
|
}
|
|
1067
1120
|
|
|
1068
1121
|
_updateRemoteParticipantStreamMap(handleId) {
|
|
@@ -1189,7 +1242,22 @@ class RoomSession {
|
|
|
1189
1242
|
});
|
|
1190
1243
|
}
|
|
1191
1244
|
|
|
1192
|
-
connect(
|
|
1245
|
+
connect(
|
|
1246
|
+
roomId,
|
|
1247
|
+
pin,
|
|
1248
|
+
server,
|
|
1249
|
+
iceServers,
|
|
1250
|
+
token,
|
|
1251
|
+
display,
|
|
1252
|
+
userId,
|
|
1253
|
+
webrtcVersion = 0,
|
|
1254
|
+
initialBitrate = 0,
|
|
1255
|
+
recordingFilename,
|
|
1256
|
+
simulcast = false,
|
|
1257
|
+
simulcastBitrates = this.simulcastBitrates,
|
|
1258
|
+
simulcastMode = this.simulcastMode,
|
|
1259
|
+
simulcastDefaultManualSubstream = this.simulcastDefaultManualSubstream
|
|
1260
|
+
) {
|
|
1193
1261
|
|
|
1194
1262
|
if (this.isConnecting) {
|
|
1195
1263
|
return Promise.reject({type: 'warning', id: 16, message: 'connection already in progress'});
|
|
@@ -1214,8 +1282,16 @@ class RoomSession {
|
|
|
1214
1282
|
this.recordingFilename = recordingFilename;
|
|
1215
1283
|
this.isConnecting = true;
|
|
1216
1284
|
this.simulcast = simulcast;
|
|
1217
|
-
this.
|
|
1218
|
-
|
|
1285
|
+
this.simulcastMode = simulcastMode;
|
|
1286
|
+
this.simulcastDefaultManualSubstream = simulcastDefaultManualSubstream;
|
|
1287
|
+
if(simulcastBitrates !== null) {
|
|
1288
|
+
this.simulcastBitrates = structuredClone(simulcastBitrates).sort((a, b) => {
|
|
1289
|
+
if(a.maxBitrate === b.maxBitrate) {
|
|
1290
|
+
return a.maxFramerate - b.maxFramerate;
|
|
1291
|
+
}
|
|
1292
|
+
return a.maxBitrate - b.maxBitrate;
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1219
1295
|
this.emit('joining', true);
|
|
1220
1296
|
return new Promise((resolve, reject) => {
|
|
1221
1297
|
|
|
@@ -1252,6 +1328,8 @@ class RoomSession {
|
|
|
1252
1328
|
})
|
|
1253
1329
|
.then(() => this._joinRoom(roomId, pin, userId, display))
|
|
1254
1330
|
.then(() => {
|
|
1331
|
+
this._enableStatsWatch();
|
|
1332
|
+
this._enableSubstreamAutoSelect();
|
|
1255
1333
|
this.isConnecting = false;
|
|
1256
1334
|
this.emit('joining', false);
|
|
1257
1335
|
resolve(this);
|
|
@@ -1282,10 +1360,11 @@ class RoomSession {
|
|
|
1282
1360
|
}
|
|
1283
1361
|
|
|
1284
1362
|
this._abortController?.abort?.();
|
|
1363
|
+
this.isDisconnecting = true;
|
|
1285
1364
|
this._stopKeepAlive();
|
|
1286
|
-
|
|
1365
|
+
this._disableStatsWatch();
|
|
1366
|
+
this._disableSubstreamAutoSelect();
|
|
1287
1367
|
let isConnected = this.isConnected;
|
|
1288
|
-
this.isDisconnecting = true;
|
|
1289
1368
|
return Promise.all(this._participants.map(p => this._removeParticipant(p.handleId)))
|
|
1290
1369
|
.finally(() => {
|
|
1291
1370
|
this._wipeListeners();
|
|
@@ -1436,18 +1515,305 @@ class RoomSession {
|
|
|
1436
1515
|
return this._participants.find(p => p.handleId === handleId || (rfid && p.rfid === rfid) || (userId && decodeJanusDisplay(p.userId)?.userId === userId));
|
|
1437
1516
|
}
|
|
1438
1517
|
|
|
1518
|
+
_disableStatsWatch() {
|
|
1519
|
+
if (this._statsTimeoutId) {
|
|
1520
|
+
clearInterval(this._statsTimeout);
|
|
1521
|
+
this._statsTimeoutStopped = true;
|
|
1522
|
+
this._statsTimeoutId = null;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
_enableStatsWatch() {
|
|
1526
|
+
if (this._statsTimeoutId) {
|
|
1527
|
+
clearTimeout(this._statsTimeoutId);
|
|
1528
|
+
this._statsTimeoutId = null;
|
|
1529
|
+
}
|
|
1530
|
+
this._statsTimeoutStopped = false;
|
|
1531
|
+
const loop = () => {
|
|
1532
|
+
let startTime = performance.now();
|
|
1533
|
+
let endTime = null;
|
|
1534
|
+
this._getStats('video')
|
|
1535
|
+
.then(participantsStats => {
|
|
1536
|
+
endTime = performance.now();
|
|
1537
|
+
this._parseVideoStats(participantsStats);
|
|
1538
|
+
})
|
|
1539
|
+
.finally(() => {
|
|
1540
|
+
if (!this._statsTimeoutStopped) {
|
|
1541
|
+
this._statsTimeoutId = setTimeout(loop, this._statsInterval - Math.min((endTime - startTime), this._statsInterval));
|
|
1542
|
+
}
|
|
1543
|
+
})
|
|
1544
|
+
};
|
|
1545
|
+
loop()
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
_enableSubstreamAutoSelect() {
|
|
1549
|
+
|
|
1550
|
+
if(!this.simulcast) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
if(this._aqTimeoutId) {
|
|
1555
|
+
clearTimeout(this._aqTimeoutId);
|
|
1556
|
+
this._aqTimeoutId = null;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
this._aqTimeoutId = setInterval(() => {
|
|
1560
|
+
this._participants.forEach(p => {
|
|
1561
|
+
if(p.handleId !== this.handleId) {
|
|
1562
|
+
|
|
1563
|
+
const transceivers = p.webrtcStuff?.pc?.getTransceivers();
|
|
1564
|
+
const mids = transceivers?.filter(t => t.receiver.track.kind === "video")?.map(t => t.mid) || [];
|
|
1565
|
+
mids.forEach(mid => {
|
|
1566
|
+
|
|
1567
|
+
const source = p.webrtcStuff.tracksMap.find(t => t.mid === mid)?.source;
|
|
1568
|
+
const manualSelectedSubstream = p.webrtcStuff.simulcastSubstreamManualSelect?.[mid];
|
|
1569
|
+
const defaultSelectedSubstream = typeof this.simulcastDefaultManualSubstream === 'object'
|
|
1570
|
+
? (this.simulcastDefaultManualSubstream[source] !== undefined ? this.simulcastDefaultManualSubstream[source] : this.simulcastDefaultManualSubstream['default'])
|
|
1571
|
+
: this.simulcastDefaultManualSubstream;
|
|
1572
|
+
const simulcastMode = typeof this.simulcastMode === 'object'
|
|
1573
|
+
? (this.simulcastMode[source] !== undefined ? this.simulcastMode[source] : this.simulcastMode['default'])
|
|
1574
|
+
: this.simulcastMode;
|
|
1575
|
+
const failedAttempts = p.webrtcStuff.simulcastSwitchFailedAttempts[mid] || 0;
|
|
1576
|
+
|
|
1577
|
+
if((simulcastMode === 'auto' && !manualSelectedSubstream) || failedAttempts > 3) {
|
|
1578
|
+
// do nothing
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
else if(simulcastMode === 'manual' || manualSelectedSubstream) {
|
|
1582
|
+
const currentSubstream = p.webrtcStuff.selectedSubstream[mid];
|
|
1583
|
+
if(manualSelectedSubstream && currentSubstream !== manualSelectedSubstream) {
|
|
1584
|
+
this.selectSubStream(p.handleId, manualSelectedSubstream, undefined, mid, false)
|
|
1585
|
+
.then(() => {
|
|
1586
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid] = 0
|
|
1587
|
+
})
|
|
1588
|
+
.catch(() => {
|
|
1589
|
+
if(!p.webrtcStuff.simulcastSwitchFailedAttempts[mid]) {
|
|
1590
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid] = 1;
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid]++;
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
else if(defaultSelectedSubstream !== currentSubstream) {
|
|
1598
|
+
this.selectSubStream(p.handleId, defaultSelectedSubstream, undefined, mid, false)
|
|
1599
|
+
.then(() => {
|
|
1600
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid] = 0
|
|
1601
|
+
})
|
|
1602
|
+
.catch(() => {
|
|
1603
|
+
if(!p.webrtcStuff.simulcastSwitchFailedAttempts[mid]) {
|
|
1604
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid] = 1;
|
|
1605
|
+
}
|
|
1606
|
+
else {
|
|
1607
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid]++;
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
else if(simulcastMode === 'controlled') {
|
|
1614
|
+
const currentSubstream = p.webrtcStuff.selectedSubstream[mid];
|
|
1615
|
+
const settingsForCurrentSubstream = this.simulcastBitrates?.[this.simulcastBitrates.length - 1 - currentSubstream];
|
|
1616
|
+
let directionDecision = 0;
|
|
1617
|
+
if(p.webrtcStuff?.stats?.[mid]?.length > this._upStatsLength) {
|
|
1618
|
+
const upMedianStats = this._calculateMedianStats(p.webrtcStuff.stats[mid].slice(this._upStatsLength * -1));
|
|
1619
|
+
if(upMedianStats?.framesPerSecond > Math.floor(settingsForCurrentSubstream.maxFramerate * 0.9) || upMedianStats?.freezeDurationSinceLast < (this._upStatsLength * this._statsInterval * 0.1) / 1000 || this._upStatsLength?.freezeCountSinceLast < 3) {
|
|
1620
|
+
directionDecision = 1;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
if(p.webrtcStuff?.stats?.[mid]?.length > this._downStatsLength) {
|
|
1625
|
+
const downMedianStats = this._calculateMedianStats(p.webrtcStuff.stats[mid].slice(this._downStatsLength * -1));
|
|
1626
|
+
if(downMedianStats?.framesPerSecond < Math.floor(settingsForCurrentSubstream.maxFramerate * 0.7) || downMedianStats?.freezeDurationSinceLast > (this._downStatsLength * this._statsInterval * 0.33) / 1000 || downMedianStats?.freezeCountSinceLast > 5 || downMedianStats?.jitter > maxJitter(settingsForCurrentSubstream.maxFramerate)) {
|
|
1627
|
+
directionDecision = -1;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if(directionDecision!== 0) {
|
|
1632
|
+
this._log('directionDecision for mid', mid, directionDecision);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if(directionDecision === -1) {
|
|
1636
|
+
if(currentSubstream < this.simulcastBitrates.length - 1) {
|
|
1637
|
+
this._log('switching to low res', currentSubstream + 1);
|
|
1638
|
+
this.selectSubStream(p.handleId, currentSubstream + 1, undefined, mid, false)
|
|
1639
|
+
.then(() => {
|
|
1640
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid] = 0
|
|
1641
|
+
})
|
|
1642
|
+
.catch(() => {
|
|
1643
|
+
this._resetStats(p.handleId, mid);
|
|
1644
|
+
|
|
1645
|
+
if(!p.webrtcStuff.simulcastSwitchFailedAttempts[mid]) {
|
|
1646
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid] = 1;
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid]++;
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
else if (directionDecision === 1) {
|
|
1655
|
+
if(currentSubstream > 0) {
|
|
1656
|
+
this._log('switching to high res', currentSubstream - 1);
|
|
1657
|
+
this.selectSubStream(p.handleId, currentSubstream - 1, undefined, mid, false)
|
|
1658
|
+
.then(() => {
|
|
1659
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid] = 0
|
|
1660
|
+
})
|
|
1661
|
+
.catch(() => {
|
|
1662
|
+
this._resetStats(p.handleId, mid);
|
|
1663
|
+
if(!p.webrtcStuff.simulcastSwitchFailedAttempts[mid]) {
|
|
1664
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid] = 1;
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
p.webrtcStuff.simulcastSwitchFailedAttempts[mid]++;
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
})
|
|
1676
|
+
}, this._aqInterval);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
_disableSubstreamAutoSelect() {
|
|
1680
|
+
if(this._aqTimeoutId) {
|
|
1681
|
+
clearTimeout(this._aqTimeoutId);
|
|
1682
|
+
this._aqTimeoutId = null;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
_calculateMedianStats(stats) {
|
|
1687
|
+
let medianStats = {
|
|
1688
|
+
framesPerSecond: null,
|
|
1689
|
+
jitter: null,
|
|
1690
|
+
roundTripTime: null,
|
|
1691
|
+
freezeDurationSinceLast: null,
|
|
1692
|
+
freezeCountSinceLast: null,
|
|
1693
|
+
};
|
|
1694
|
+
let keys = Object.keys(medianStats);
|
|
1695
|
+
keys.forEach(key => {
|
|
1696
|
+
if(key === 'freezeDurationSinceLast' || key ==='freezeCountSinceLast') {
|
|
1697
|
+
medianStats[key] = stats.reduce((acc, cur) => acc + cur[key], 0);
|
|
1698
|
+
}
|
|
1699
|
+
// median but ignore first value of stats array
|
|
1700
|
+
else {
|
|
1701
|
+
let values = stats.map(s => s[key]);
|
|
1702
|
+
medianStats[key] = median(values)
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
});
|
|
1706
|
+
medianStats.statsLength = stats.length;
|
|
1707
|
+
return medianStats;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
_parseVideoStats(participantsStats) {
|
|
1711
|
+
participantsStats.forEach(sourceStats => {
|
|
1712
|
+
sourceStats.forEach(participantStats => {
|
|
1713
|
+
if(participantStats !== null && participantStats?.handle?.handleId !== this.handleId) {
|
|
1714
|
+
let handle = this._getHandle(participantStats.handle.handleId);
|
|
1715
|
+
if(handle) {
|
|
1716
|
+
|
|
1717
|
+
if(!handle.webrtcStuff.stats[participantStats.mid]) {
|
|
1718
|
+
handle.webrtcStuff.stats[participantStats.mid] = [];
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const stats = {
|
|
1722
|
+
framesPerSecond: null,
|
|
1723
|
+
framesDropped: null,
|
|
1724
|
+
totalFreezesDuration: null,
|
|
1725
|
+
freezeDurationSinceLast: null,
|
|
1726
|
+
freezeCount: null,
|
|
1727
|
+
jitter: null,
|
|
1728
|
+
packetsLost: null,
|
|
1729
|
+
nackCount: null,
|
|
1730
|
+
roundTripTime: null,
|
|
1731
|
+
width: null,
|
|
1732
|
+
height: null,
|
|
1733
|
+
networkType: null,
|
|
1734
|
+
powerEfficientDecoder: null,
|
|
1735
|
+
};
|
|
1736
|
+
participantStats.stats.forEach(report => {
|
|
1737
|
+
if(report.type === 'inbound-rtp' && report.kind === 'video') {
|
|
1738
|
+
stats.framesPerSecond = report.framesPerSecond || 0;
|
|
1739
|
+
stats.framesDropped = report.framesDropped || 0;
|
|
1740
|
+
stats.totalFreezesDuration = report.totalFreezesDuration || 0;
|
|
1741
|
+
stats.freezeDurationSinceLast = (report.totalFreezesDuration || 0) - (handle.webrtcStuff.stats?.[participantStats.mid]?.[handle.webrtcStuff.stats?.[participantStats.mid]?.length - 1]?.totalFreezesDuration || 0);
|
|
1742
|
+
stats.freezeCount = report.freezeCount || 0;
|
|
1743
|
+
stats.freezeCountSinceLast = (report.freezeCount || 0) - (handle.webrtcStuff.stats?.[participantStats.mid]?.[handle.webrtcStuff.stats?.[participantStats.mid]?.length - 1]?.freezeCount || 0);
|
|
1744
|
+
stats.jitter = report.jitter;
|
|
1745
|
+
stats.packetsLost = report.packetsLost;
|
|
1746
|
+
stats.nackCount = report.nackCount;
|
|
1747
|
+
stats.width = report.frameWidth;
|
|
1748
|
+
stats.height = report.frameHeight;
|
|
1749
|
+
stats.powerEfficientDecoder = report.powerEfficientDecoder;
|
|
1750
|
+
}
|
|
1751
|
+
if(report.type === 'candidate-pair') {
|
|
1752
|
+
stats.roundTripTime = report.currentRoundTripTime;
|
|
1753
|
+
}
|
|
1754
|
+
if(report.type === 'local-candidate') {
|
|
1755
|
+
stats.networkType = report.networkType;
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
// pushing stats into handle stats array but keeping only 6 last stats
|
|
1760
|
+
handle.webrtcStuff.stats[participantStats.mid].push(stats);
|
|
1761
|
+
if(handle.webrtcStuff.stats[participantStats.mid].length > this._statsMaxLength) {
|
|
1762
|
+
handle.webrtcStuff.stats[participantStats.mid].shift();
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
this.emit('rtcStats', {handleId: participantStats.handle.handleId, stats, source: participantStats.source, mid: participantsStats.mid});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
})
|
|
1770
|
+
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1439
1773
|
_getStats(type = null) {
|
|
1440
1774
|
return Promise.all(this._participants.map(participant => {
|
|
1441
|
-
let mediaTrack =
|
|
1775
|
+
let mediaTrack = [];
|
|
1442
1776
|
if (type === 'video') {
|
|
1443
|
-
mediaTrack = participant
|
|
1777
|
+
mediaTrack = participant?.webrtcStuff?.stream?.getVideoTracks() || [];
|
|
1444
1778
|
} else if (type === 'audio') {
|
|
1445
|
-
mediaTrack = participant
|
|
1779
|
+
mediaTrack = participant?.webrtcStuff?.stream?.getAudioTracks() || [];
|
|
1780
|
+
}
|
|
1781
|
+
if(type !== null ) {
|
|
1782
|
+
const transceivers = participant?.webrtcStuff?.pc?.getTransceivers();
|
|
1783
|
+
return Promise.all(mediaTrack.map(track => {
|
|
1784
|
+
const source = Object.keys(participant.webrtcStuff.streamMap).find(s => participant.webrtcStuff.streamMap[s].find(t => t === track.id));
|
|
1785
|
+
const mid = transceivers.find(t => t.receiver?.track?.id === track.id || t.sender?.track?.id === track.id)?.mid;
|
|
1786
|
+
return participant.webrtcStuff.pc.getStats(track)
|
|
1787
|
+
.then(r =>({stats: r, source, mid, handle: participant}))
|
|
1788
|
+
.catch(e => Promise.reject({stats: null, error: e, handle: participant, source, mid}))
|
|
1789
|
+
}))
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
return participant?.webrtcStuff?.pc?.getStats(null)
|
|
1793
|
+
.then(r => ({handle: participant, stats: r}))
|
|
1794
|
+
.catch(e => Promise.reject({handle: participant, error: e}))
|
|
1446
1795
|
}
|
|
1447
|
-
return participant.webrtcStuff && participant.webrtcStuff.pc && participant.webrtcStuff.pc.getStats(mediaTrack)
|
|
1448
|
-
.then(r => ({handle: participant, stats: r}))
|
|
1449
|
-
.catch(e => Promise.resolve({handle: participant, stats: e}))
|
|
1450
1796
|
}))
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
_resetStats(handleId, mid) {
|
|
1802
|
+
let handle = this._getHandle(handleId);
|
|
1803
|
+
if(handle) {
|
|
1804
|
+
let config = handle.webrtcStuff;
|
|
1805
|
+
if(!mid) {
|
|
1806
|
+
Object.keys(config.stats).forEach(mid => {
|
|
1807
|
+
config.stats[mid] = [config.stats[mid][config.stats[mid].length - 1]];
|
|
1808
|
+
})
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
// clearing stats for the new substream
|
|
1812
|
+
if(config.stats[mid]) {
|
|
1813
|
+
config.stats[mid] = [config.stats[mid][config.stats[mid].length - 1]];
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1451
1817
|
}
|
|
1452
1818
|
|
|
1453
1819
|
_sendTrickleCandidate(handleId, candidate) {
|
|
@@ -2053,7 +2419,7 @@ class RoomSession {
|
|
|
2053
2419
|
"body": {"request": "start", ...(this.roomId && {"room": this.roomId}), ...(this.pin && {pin: this.pin})},
|
|
2054
2420
|
"jsep": _jsep
|
|
2055
2421
|
});
|
|
2056
|
-
})
|
|
2422
|
+
})
|
|
2057
2423
|
}, (e) => Promise.reject({
|
|
2058
2424
|
type: 'warning',
|
|
2059
2425
|
id: 23,
|
|
@@ -2231,18 +2597,12 @@ class RoomSession {
|
|
|
2231
2597
|
config.pc.addTrack(stream.getVideoTracks()[0], config.stream);
|
|
2232
2598
|
}
|
|
2233
2599
|
else {
|
|
2234
|
-
let bitRates = this.simulcastBitrates;
|
|
2235
2600
|
if(adapter.browserDetails.browser !== 'firefox') {
|
|
2236
2601
|
// standard
|
|
2237
|
-
|
|
2238
2602
|
config.pc.addTransceiver(stream.getVideoTracks()[0], {
|
|
2239
2603
|
direction: 'sendonly',
|
|
2240
2604
|
streams: [config.stream],
|
|
2241
|
-
sendEncodings:
|
|
2242
|
-
{ rid: 'h', active: true, scalabilityMode: 'L1T2', maxBitrate: bitRates.high },
|
|
2243
|
-
{ rid: 'm', active: true, scalabilityMode: 'L1T2', maxBitrate: bitRates.medium, scaleResolutionDownBy: 2 },
|
|
2244
|
-
{ rid: 'l', active: true, scalabilityMode: 'L1T2', maxBitrate: bitRates.low, scaleResolutionDownBy: 4 }
|
|
2245
|
-
]
|
|
2605
|
+
sendEncodings: structuredClone(this.simulcastBitrates)
|
|
2246
2606
|
})
|
|
2247
2607
|
}
|
|
2248
2608
|
else {
|
|
@@ -2254,11 +2614,7 @@ class RoomSession {
|
|
|
2254
2614
|
let sender = transceiver ? transceiver.sender : null;
|
|
2255
2615
|
if(sender) {
|
|
2256
2616
|
let parameters = sender.getParameters() || {};
|
|
2257
|
-
parameters.encodings = stream.getVideoTracks()[0].sendEncodings ||
|
|
2258
|
-
{ rid: 'h', active: true, maxBitrate: bitRates.high },
|
|
2259
|
-
{ rid: 'm', active: true, maxBitrate: bitRates.medium, scaleResolutionDownBy: 2 },
|
|
2260
|
-
{ rid: 'l', active: true, maxBitrate: bitRates.low, scaleResolutionDownBy: 4 }
|
|
2261
|
-
];
|
|
2617
|
+
parameters.encodings = stream.getVideoTracks()[0].sendEncodings || structuredClone(this.simulcastBitrates);
|
|
2262
2618
|
sender.setParameters(parameters);
|
|
2263
2619
|
}
|
|
2264
2620
|
}
|
|
@@ -2572,13 +2928,99 @@ class RoomSession {
|
|
|
2572
2928
|
}
|
|
2573
2929
|
}).catch(() => null)
|
|
2574
2930
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2931
|
+
|
|
2932
|
+
_setSelectedSubstream(handleId, mid, substream) {
|
|
2933
|
+
let handle = this._getHandle(handleId);
|
|
2934
|
+
if(handle) {
|
|
2935
|
+
let config = handle.webrtcStuff;
|
|
2936
|
+
if(!mid) {
|
|
2937
|
+
Object.keys(config.selectedSubstream).forEach(mid => {
|
|
2938
|
+
config.selectedSubstream[mid] = substream;
|
|
2939
|
+
})
|
|
2940
|
+
} else {
|
|
2941
|
+
config.selectedSubstream[mid] = substream;
|
|
2580
2942
|
}
|
|
2581
|
-
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
selectSubStream(handleId, substream = 2, source, mid, manual = true) {
|
|
2947
|
+
let handle = this._getHandle(handleId);
|
|
2948
|
+
if(!handle) {
|
|
2949
|
+
return Promise.resolve();
|
|
2950
|
+
}
|
|
2951
|
+
let config = handle.webrtcStuff;
|
|
2952
|
+
return new Promise((resolve, reject) => {
|
|
2953
|
+
let messageTimeoutId;
|
|
2954
|
+
let abortResponse = () => {
|
|
2955
|
+
clearTimeout(messageTimeoutId);
|
|
2956
|
+
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
2957
|
+
this.ws.removeEventListener('message', parseResponse);
|
|
2958
|
+
reject('aborted');
|
|
2959
|
+
};
|
|
2960
|
+
let parseResponse = (event) => {
|
|
2961
|
+
let json = JSON.parse(event.data);
|
|
2962
|
+
var sender = json["sender"];
|
|
2963
|
+
if(sender === handleId) {
|
|
2964
|
+
let plugindata = json["plugindata"] || {};
|
|
2965
|
+
let msg = plugindata["data"] || {};
|
|
2966
|
+
let substream = msg["substream"];
|
|
2967
|
+
if(substream !== undefined && substream !== null && (mid !== undefined ? msg["mid"] === mid : true)) {
|
|
2968
|
+
clearTimeout(messageTimeoutId);
|
|
2969
|
+
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
2970
|
+
this.ws.removeEventListener('message', parseResponse);
|
|
2971
|
+
if(manual) {
|
|
2972
|
+
config.simulcastSubstreamManualSelect[mid] = substream;
|
|
2973
|
+
}
|
|
2974
|
+
resolve({substream, sender});
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
if(source !== undefined || mid !== undefined) {
|
|
2980
|
+
if(mid === undefined) {
|
|
2981
|
+
let transceivers = config.pc.getTransceivers();
|
|
2982
|
+
for(let trackId of config.streamMap[source]) {
|
|
2983
|
+
let transceiver = transceivers.find(transceiver => transceiver.receiver.track && transceiver.receiver.track.kind === 'video' && transceiver.receiver.track.id === trackId)
|
|
2984
|
+
if(transceiver) {
|
|
2985
|
+
mid = transceiver.mid;
|
|
2986
|
+
break;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
if(mid !== undefined) {
|
|
2991
|
+
|
|
2992
|
+
if(manual && substream === null) {
|
|
2993
|
+
config.simulcastSubstreamManualSelect[mid] = substream;
|
|
2994
|
+
resolve({substream, sender: handleId});
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
this.ws.addEventListener('message', parseResponse);
|
|
2999
|
+
this._abortController.signal.addEventListener('abort', abortResponse);
|
|
3000
|
+
messageTimeoutId = setTimeout(() => {
|
|
3001
|
+
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
3002
|
+
this.ws.removeEventListener('message', parseResponse);
|
|
3003
|
+
reject('timeout');
|
|
3004
|
+
}, 2000);
|
|
3005
|
+
|
|
3006
|
+
this.sendMessage(handleId, {
|
|
3007
|
+
"body": {
|
|
3008
|
+
"request": "configure",
|
|
3009
|
+
"streams": [
|
|
3010
|
+
{
|
|
3011
|
+
mid, substream: parseInt(substream)
|
|
3012
|
+
}
|
|
3013
|
+
]
|
|
3014
|
+
}
|
|
3015
|
+
})
|
|
3016
|
+
} else {
|
|
3017
|
+
reject('no mid found');
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
else {
|
|
3021
|
+
reject('no source or mid');
|
|
3022
|
+
}
|
|
3023
|
+
});
|
|
2582
3024
|
}
|
|
2583
3025
|
|
|
2584
3026
|
setTalkIntercomChannels(groups = ['participants']) {
|