@reactoo/watchtogether-sdk-js 2.7.98 → 2.8.0-beta.2
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 +3538 -3448
- package/dist/watchtogether-sdk.js.map +1 -1
- package/dist/watchtogether-sdk.min.js +2 -2
- package/example/index.html +5 -5
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/models/room-session.js +1 -8
- package/src/modules/wt-room-sfu.js +592 -94
|
@@ -201,6 +201,23 @@ class RoomSession {
|
|
|
201
201
|
#subscriberHandle = null;
|
|
202
202
|
#subscriberJoinPromise = null;
|
|
203
203
|
|
|
204
|
+
// Configuration for simulcast quality selection
|
|
205
|
+
#rtcStatsConfig = {
|
|
206
|
+
// Network thresholds
|
|
207
|
+
packetLossThreshold: 3.0, // Percentage of packet loss to trigger downgrade
|
|
208
|
+
jitterBufferThreshold: 100, // ms of jitter buffer delay to trigger downgrade
|
|
209
|
+
roundTripTimeThreshold: 250, // ms of RTT to trigger downgrade
|
|
210
|
+
frameDropRateThreshold: 5, // Percentage of frame drops to trigger downgrade
|
|
211
|
+
freezeLengthThreshold: 0.7, // 70% of freeze time for current monitored period
|
|
212
|
+
// Hysteresis to prevent rapid switching (ms)
|
|
213
|
+
switchCooldown: 15000, // Min time between switches
|
|
214
|
+
switchCooldownForFreeze: 3000,
|
|
215
|
+
historySize: 60000, // Ms of history to keep
|
|
216
|
+
badBandwidthThresholdMultiplier: 0.8, // 80% of required bitrate
|
|
217
|
+
// Stability thresholds
|
|
218
|
+
stableNetworkTime: 10000, // Time in ms to consider network stable
|
|
219
|
+
};
|
|
220
|
+
|
|
204
221
|
|
|
205
222
|
constructor(constructId = null, type = 'reactooroom', options = {}) {
|
|
206
223
|
|
|
@@ -262,7 +279,7 @@ class RoomSession {
|
|
|
262
279
|
|
|
263
280
|
this.isSupposeToBeConnected = false;
|
|
264
281
|
this.isConnecting = false;
|
|
265
|
-
this.isEstablishingConnection = false;
|
|
282
|
+
this.isEstablishingConnection = false;
|
|
266
283
|
this.isDisconnecting = false;
|
|
267
284
|
this.isConnected = false;
|
|
268
285
|
this.isPublished = false;
|
|
@@ -278,7 +295,6 @@ class RoomSession {
|
|
|
278
295
|
this._longPollTimeout = 60000;
|
|
279
296
|
this._maxev = 10;
|
|
280
297
|
this._keepAliveId = null;
|
|
281
|
-
this._participants = [];
|
|
282
298
|
this._restrictSubscribeToUserIds = []; // all if empty
|
|
283
299
|
this._talkIntercomChannels = ['participants'];
|
|
284
300
|
this._listenIntercomChannels = ['participants'];
|
|
@@ -295,6 +311,7 @@ class RoomSession {
|
|
|
295
311
|
if (this.options.debug) {
|
|
296
312
|
this.#enableDebug();
|
|
297
313
|
}
|
|
314
|
+
|
|
298
315
|
}
|
|
299
316
|
|
|
300
317
|
#httpAPICall = function(url, options) {
|
|
@@ -772,7 +789,7 @@ class RoomSession {
|
|
|
772
789
|
if (!handle) {
|
|
773
790
|
this.emit('error', {
|
|
774
791
|
type: 'warning',
|
|
775
|
-
id:
|
|
792
|
+
id: 1,
|
|
776
793
|
message: 'id non-existent',
|
|
777
794
|
data: [handleId, 'updateTransceiverMap']
|
|
778
795
|
});
|
|
@@ -864,9 +881,9 @@ class RoomSession {
|
|
|
864
881
|
})
|
|
865
882
|
.catch(json => {
|
|
866
883
|
if (json && json["error"]) {
|
|
867
|
-
return Promise.reject({type: 'warning', id:
|
|
884
|
+
return Promise.reject({type: 'warning', id: 2, message: 'sendMessage failed', data: json["error"]})
|
|
868
885
|
} else {
|
|
869
|
-
return Promise.reject({type: 'warning', id:
|
|
886
|
+
return Promise.reject({type: 'warning', id: 3, message: 'sendMessage failed', data: json});
|
|
870
887
|
}
|
|
871
888
|
})
|
|
872
889
|
}
|
|
@@ -907,7 +924,7 @@ class RoomSession {
|
|
|
907
924
|
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
908
925
|
clearTimeout(messageTimeoutId);
|
|
909
926
|
this.ws.removeEventListener('message', parseResponse);
|
|
910
|
-
reject({type: 'warning', id:
|
|
927
|
+
reject({type: 'warning', id: 4, message: 'connection cancelled'})
|
|
911
928
|
};
|
|
912
929
|
|
|
913
930
|
let parseResponse = (event) => {
|
|
@@ -921,7 +938,7 @@ class RoomSession {
|
|
|
921
938
|
if (json?.error?.code == 403) {
|
|
922
939
|
this.disconnect();
|
|
923
940
|
}
|
|
924
|
-
reject({type: 'error', id:
|
|
941
|
+
reject({type: 'error', id: 5, message: 'send failed', data: json, requestData});
|
|
925
942
|
} else {
|
|
926
943
|
resolve(json);
|
|
927
944
|
}
|
|
@@ -939,12 +956,12 @@ class RoomSession {
|
|
|
939
956
|
messageTimeoutId = setTimeout(() => {
|
|
940
957
|
this.ws.removeEventListener('message', parseResponse);
|
|
941
958
|
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
942
|
-
reject({type: 'warning', id:
|
|
959
|
+
reject({type: 'warning', id: 6, message: 'send timeout', data: requestData});
|
|
943
960
|
}, this._sendMessageTimeout);
|
|
944
961
|
this._abortController.signal.addEventListener('abort', abortResponse);
|
|
945
962
|
this.ws.send(JSON.stringify(requestData));
|
|
946
963
|
} else {
|
|
947
|
-
reject({type: 'warning', id:
|
|
964
|
+
reject({type: 'warning', id: 7, message: 'No connection to WebSockets', data: requestData});
|
|
948
965
|
}
|
|
949
966
|
}
|
|
950
967
|
})
|
|
@@ -1006,7 +1023,9 @@ class RoomSession {
|
|
|
1006
1023
|
|
|
1007
1024
|
this.#reconnect()
|
|
1008
1025
|
.catch(e => {
|
|
1009
|
-
|
|
1026
|
+
if(e.type !== 'warning') {
|
|
1027
|
+
this.disconnect();
|
|
1028
|
+
}
|
|
1010
1029
|
this.emit('error', e)
|
|
1011
1030
|
});
|
|
1012
1031
|
}
|
|
@@ -1024,14 +1043,14 @@ class RoomSession {
|
|
|
1024
1043
|
if (json["janus"] !== 'ack') {
|
|
1025
1044
|
this.emit('error', {
|
|
1026
1045
|
type: 'warning',
|
|
1027
|
-
id:
|
|
1046
|
+
id: 8,
|
|
1028
1047
|
message: 'keepalive response suspicious',
|
|
1029
1048
|
data: json["janus"]
|
|
1030
1049
|
});
|
|
1031
1050
|
}
|
|
1032
1051
|
})
|
|
1033
1052
|
.catch((e) => {
|
|
1034
|
-
this.emit('error', {type: 'warning', id:
|
|
1053
|
+
this.emit('error', {type: 'warning', id: 9, message: 'keepalive dead', data: e});
|
|
1035
1054
|
this.#connectionClosed();
|
|
1036
1055
|
});
|
|
1037
1056
|
|
|
@@ -1077,10 +1096,15 @@ class RoomSession {
|
|
|
1077
1096
|
let candidate = json["candidate"];
|
|
1078
1097
|
let config = handle.webrtcStuff;
|
|
1079
1098
|
if (config.pc && config.remoteSdp) {
|
|
1099
|
+
|
|
1080
1100
|
if (!candidate || candidate.completed === true) {
|
|
1081
|
-
config.pc.addIceCandidate(null)
|
|
1101
|
+
config.pc.addIceCandidate(null).catch((e) => {
|
|
1102
|
+
this._log('Error adding null candidate', e);
|
|
1103
|
+
});
|
|
1082
1104
|
} else {
|
|
1083
|
-
config.pc.addIceCandidate(candidate)
|
|
1105
|
+
config.pc.addIceCandidate(candidate).catch((e) => {
|
|
1106
|
+
this._log('Error adding candidate', e);
|
|
1107
|
+
});
|
|
1084
1108
|
}
|
|
1085
1109
|
} else {
|
|
1086
1110
|
if (!config.candidates) {
|
|
@@ -1131,7 +1155,8 @@ class RoomSession {
|
|
|
1131
1155
|
let leaving = msg["leaving"];
|
|
1132
1156
|
let kicked = msg["kicked"];
|
|
1133
1157
|
let substream = msg["substream"];
|
|
1134
|
-
let temporal = msg["temporal"];
|
|
1158
|
+
let temporal = msg["temporal"] ?? msg["temporal_layer"];
|
|
1159
|
+
let spatial = msg["spatial_layer"];
|
|
1135
1160
|
let joining = msg["joining"];
|
|
1136
1161
|
let unpublished = msg["unpublished"];
|
|
1137
1162
|
let error = msg["error"];
|
|
@@ -1143,7 +1168,7 @@ class RoomSession {
|
|
|
1143
1168
|
this.isConnected = true;
|
|
1144
1169
|
|
|
1145
1170
|
this._log('We have successfully joined Room',msg);
|
|
1146
|
-
|
|
1171
|
+
this.emit('joined', true, this.constructId);
|
|
1147
1172
|
// initial events
|
|
1148
1173
|
|
|
1149
1174
|
attendees.forEach((attendee) => {
|
|
@@ -1157,8 +1182,6 @@ class RoomSession {
|
|
|
1157
1182
|
});
|
|
1158
1183
|
|
|
1159
1184
|
// end of initial events
|
|
1160
|
-
|
|
1161
|
-
this.emit('joined', true, this.constructId);
|
|
1162
1185
|
this.#emitLocalFeedUpdate();
|
|
1163
1186
|
this.#updateAvailablePublishersTrackData(list);
|
|
1164
1187
|
this.#updateSubscriptions()
|
|
@@ -1243,7 +1266,7 @@ class RoomSession {
|
|
|
1243
1266
|
if (error) {
|
|
1244
1267
|
this.emit('error', {
|
|
1245
1268
|
type: 'error',
|
|
1246
|
-
id:
|
|
1269
|
+
id: 10,
|
|
1247
1270
|
message: 'local participant error',
|
|
1248
1271
|
data: [sender, msg]
|
|
1249
1272
|
});
|
|
@@ -1285,8 +1308,9 @@ class RoomSession {
|
|
|
1285
1308
|
let event = msg["videoroom"];
|
|
1286
1309
|
let error = msg["error"];
|
|
1287
1310
|
let substream = msg["substream"];
|
|
1311
|
+
let temporal = msg["temporal"] ?? msg["temporal_layer"];
|
|
1312
|
+
let spatial = msg["spatial_layer"];
|
|
1288
1313
|
let mid = msg["mid"];
|
|
1289
|
-
let temporal = msg["temporal"];
|
|
1290
1314
|
|
|
1291
1315
|
if(substream !== undefined && substream !== null) {
|
|
1292
1316
|
this._log('Substream: ', sender, mid, substream);
|
|
@@ -1316,7 +1340,7 @@ class RoomSession {
|
|
|
1316
1340
|
}
|
|
1317
1341
|
|
|
1318
1342
|
if (error) {
|
|
1319
|
-
this.emit('error', {type: 'warning', id:
|
|
1343
|
+
this.emit('error', {type: 'warning', id: 11, message: 'remote participant error', data: [sender, msg]});
|
|
1320
1344
|
}
|
|
1321
1345
|
|
|
1322
1346
|
if (jsep) {
|
|
@@ -1349,7 +1373,7 @@ class RoomSession {
|
|
|
1349
1373
|
|
|
1350
1374
|
this.emit('error', {
|
|
1351
1375
|
type: 'warning',
|
|
1352
|
-
id:
|
|
1376
|
+
id: 12,
|
|
1353
1377
|
message: 'data event warning',
|
|
1354
1378
|
data: [handleId, data]
|
|
1355
1379
|
});
|
|
@@ -1373,7 +1397,7 @@ class RoomSession {
|
|
|
1373
1397
|
try {
|
|
1374
1398
|
d = JSON.parse(data)
|
|
1375
1399
|
} catch (e) {
|
|
1376
|
-
this.emit('error', {type: 'warning', id:
|
|
1400
|
+
this.emit('error', {type: 'warning', id: 13, message: 'data message parse error', data: [handleId, e]});
|
|
1377
1401
|
return;
|
|
1378
1402
|
}
|
|
1379
1403
|
this.emit('data', d);
|
|
@@ -1407,9 +1431,10 @@ class RoomSession {
|
|
|
1407
1431
|
streamMap: {}, // id to sources to mids?
|
|
1408
1432
|
availablePublishers: [],
|
|
1409
1433
|
subscribeMap: [], // subscribed to [id][mid]
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1434
|
+
stats: {},
|
|
1435
|
+
currentLayers : new Map(),
|
|
1436
|
+
qualityHistory : new Map(),
|
|
1437
|
+
lastSwitchTime : new Map(),
|
|
1413
1438
|
}
|
|
1414
1439
|
};
|
|
1415
1440
|
})
|
|
@@ -1511,8 +1536,10 @@ class RoomSession {
|
|
|
1511
1536
|
streamMap: {}, // id to sources to mids?
|
|
1512
1537
|
availablePublishers: [],
|
|
1513
1538
|
subscribeMap: [], // subscribed to [id][mid]
|
|
1514
|
-
|
|
1515
|
-
|
|
1539
|
+
stats: {},
|
|
1540
|
+
currentLayers : new Map(),
|
|
1541
|
+
qualityHistory : new Map(),
|
|
1542
|
+
lastSwitchTime : new Map(),
|
|
1516
1543
|
}
|
|
1517
1544
|
|
|
1518
1545
|
if(this.#publisherHandle && handleId === this.#publisherHandle.handleId) {
|
|
@@ -1626,8 +1653,7 @@ class RoomSession {
|
|
|
1626
1653
|
async #webSocketConnection(reclaim = false) {
|
|
1627
1654
|
|
|
1628
1655
|
if(this.isEstablishingConnection) {
|
|
1629
|
-
|
|
1630
|
-
return;
|
|
1656
|
+
return Promise.reject({type: 'warning', id: 14, message: 'connection already in progress'});
|
|
1631
1657
|
}
|
|
1632
1658
|
|
|
1633
1659
|
this.isEstablishingConnection = true;
|
|
@@ -1652,7 +1678,7 @@ class RoomSession {
|
|
|
1652
1678
|
this.ws.removeEventListener('message', this.__handleWsEventsBoundFn);
|
|
1653
1679
|
this.ws.onopen = null;
|
|
1654
1680
|
this.ws.onerror = null;
|
|
1655
|
-
reject({type: 'warning', id:
|
|
1681
|
+
reject({type: 'warning', id: 15, message: 'Connection cancelled'});
|
|
1656
1682
|
};
|
|
1657
1683
|
|
|
1658
1684
|
this.ws = new WebSocket(this.server, 'janus-protocol');
|
|
@@ -1669,7 +1695,7 @@ class RoomSession {
|
|
|
1669
1695
|
resolve(this);
|
|
1670
1696
|
})
|
|
1671
1697
|
.catch(error => {
|
|
1672
|
-
reject({type: error?.type === 'warning' ? 'warning' : 'error', id:
|
|
1698
|
+
reject({type: error?.type === 'warning' ? 'warning' : 'error', id: 16, message: 'connection error', data: error})
|
|
1673
1699
|
})
|
|
1674
1700
|
.finally(() => {
|
|
1675
1701
|
this.isEstablishingConnection = false;
|
|
@@ -1683,7 +1709,7 @@ class RoomSession {
|
|
|
1683
1709
|
resolve(json);
|
|
1684
1710
|
})
|
|
1685
1711
|
.catch(error => {
|
|
1686
|
-
reject({type: 'error', id:
|
|
1712
|
+
reject({type: 'error', id: 17, message: 'reconnection error', data: error})
|
|
1687
1713
|
})
|
|
1688
1714
|
.finally(() => {
|
|
1689
1715
|
this.isEstablishingConnection = false;
|
|
@@ -1693,7 +1719,7 @@ class RoomSession {
|
|
|
1693
1719
|
|
|
1694
1720
|
this.ws.onerror = (e) => {
|
|
1695
1721
|
this._abortController.signal.removeEventListener('abort', abortConnect);
|
|
1696
|
-
reject({type: 'error', id:
|
|
1722
|
+
reject({type: 'error', id: 18, message: 'ws connection error', data: e});
|
|
1697
1723
|
}
|
|
1698
1724
|
|
|
1699
1725
|
this._abortController.signal.addEventListener('abort', abortConnect);
|
|
@@ -1704,7 +1730,7 @@ class RoomSession {
|
|
|
1704
1730
|
async #httpConnection(reclaim = false) {
|
|
1705
1731
|
|
|
1706
1732
|
if(this.isEstablishingConnection) {
|
|
1707
|
-
this.emit('error', {type: 'warning', id:
|
|
1733
|
+
this.emit('error', {type: 'warning', id: 19, message: 'connection already in progress'});
|
|
1708
1734
|
return;
|
|
1709
1735
|
}
|
|
1710
1736
|
|
|
@@ -1729,7 +1755,7 @@ class RoomSession {
|
|
|
1729
1755
|
return this;
|
|
1730
1756
|
})
|
|
1731
1757
|
.catch(error => {
|
|
1732
|
-
return Promise.reject({type: error?.type === 'warning' ? 'warning' : 'error', id:
|
|
1758
|
+
return Promise.reject({type: error?.type === 'warning' ? 'warning' : 'error', id: 20, message: 'connection error', data: error})
|
|
1733
1759
|
})
|
|
1734
1760
|
.finally(() => {
|
|
1735
1761
|
this.isEstablishingConnection = false;
|
|
@@ -1744,7 +1770,7 @@ class RoomSession {
|
|
|
1744
1770
|
return this;
|
|
1745
1771
|
})
|
|
1746
1772
|
.catch(error => {
|
|
1747
|
-
return Promise.reject({type: 'error', id:
|
|
1773
|
+
return Promise.reject({type: 'error', id: 21, message: 'reconnection error', data: error})
|
|
1748
1774
|
})
|
|
1749
1775
|
.finally(() => {
|
|
1750
1776
|
this.isEstablishingConnection = false;
|
|
@@ -1752,10 +1778,39 @@ class RoomSession {
|
|
|
1752
1778
|
}
|
|
1753
1779
|
}
|
|
1754
1780
|
|
|
1781
|
+
async #waitForConnectEvent() {
|
|
1782
|
+
return new Promise((resolve, reject) => {
|
|
1783
|
+
let timeoutId = null;
|
|
1784
|
+
if(this.isConnected) {
|
|
1785
|
+
return resolve();
|
|
1786
|
+
}
|
|
1787
|
+
const cleanup = () => {
|
|
1788
|
+
clearTimeout(timeoutId);
|
|
1789
|
+
this.off('joined', _resolve);
|
|
1790
|
+
this._abortController.signal.removeEventListener('abort', _rejectAbort);
|
|
1791
|
+
}
|
|
1792
|
+
let _resolve = () => {
|
|
1793
|
+
cleanup();
|
|
1794
|
+
resolve();
|
|
1795
|
+
}
|
|
1796
|
+
let _rejectAbort = () => {
|
|
1797
|
+
cleanup();
|
|
1798
|
+
reject({type: 'warning', id: 43, message: 'Connection cancelled'})
|
|
1799
|
+
}
|
|
1800
|
+
let _rejectTimeout = () => {
|
|
1801
|
+
cleanup();
|
|
1802
|
+
reject({type: 'warning', id: 44, message: 'Connection timeout'})
|
|
1803
|
+
}
|
|
1804
|
+
this.once('joined', _resolve);
|
|
1805
|
+
timeoutId = setTimeout(_rejectTimeout, 10000)
|
|
1806
|
+
this._abortController.signal.addEventListener('abort', _rejectAbort);
|
|
1807
|
+
})
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1755
1810
|
async #reconnect() {
|
|
1756
1811
|
|
|
1757
1812
|
if (this.isConnecting) {
|
|
1758
|
-
return Promise.reject({type: 'warning', id:
|
|
1813
|
+
return Promise.reject({type: 'warning', id: 22, message: 'connection already in progress'});
|
|
1759
1814
|
}
|
|
1760
1815
|
|
|
1761
1816
|
this.isConnecting = true;
|
|
@@ -1798,7 +1853,7 @@ class RoomSession {
|
|
|
1798
1853
|
this.isSupposeToBeConnected = true;
|
|
1799
1854
|
|
|
1800
1855
|
if (this.isConnecting) {
|
|
1801
|
-
this.emit('error', {type: 'warning', id:
|
|
1856
|
+
this.emit('error', {type: 'warning', id: 23, message: 'connection already in progress'});
|
|
1802
1857
|
return
|
|
1803
1858
|
}
|
|
1804
1859
|
|
|
@@ -1826,17 +1881,14 @@ class RoomSession {
|
|
|
1826
1881
|
this.simulcastSettings = structuredClone(simulcastSettings);
|
|
1827
1882
|
this.useWebsockets = this.protocol === 'ws' || this.protocol === 'wss';
|
|
1828
1883
|
|
|
1829
|
-
//
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
});
|
|
1838
|
-
});
|
|
1839
|
-
}
|
|
1884
|
+
// fixing wrong order
|
|
1885
|
+
Object.keys(this.simulcastSettings).forEach(source => {
|
|
1886
|
+
const wrongOrder = this.simulcastSettings[source].bitrates[0].rid === 'l';
|
|
1887
|
+
if(wrongOrder) {
|
|
1888
|
+
this.simulcastSettings[source].bitrates = this.simulcastSettings[source].bitrates.reverse();
|
|
1889
|
+
this.simulcastSettings[source].defaultSubstream = 2 - this.simulcastSettings[source].defaultSubstream
|
|
1890
|
+
}
|
|
1891
|
+
})
|
|
1840
1892
|
|
|
1841
1893
|
this.isConnecting = true;
|
|
1842
1894
|
this.emit('joining', true);
|
|
@@ -1856,6 +1908,7 @@ class RoomSession {
|
|
|
1856
1908
|
this.#subscriberHandle = await this.#createHandle();
|
|
1857
1909
|
|
|
1858
1910
|
await this.#joinAsPublisher(this.roomId, this.pin, this.userId, this.display);
|
|
1911
|
+
await this.#waitForConnectEvent();
|
|
1859
1912
|
|
|
1860
1913
|
} catch (error) {
|
|
1861
1914
|
this.emit('error', error);
|
|
@@ -1965,7 +2018,7 @@ class RoomSession {
|
|
|
1965
2018
|
if (!handle) {
|
|
1966
2019
|
this.emit('error', {
|
|
1967
2020
|
type: 'error',
|
|
1968
|
-
id:
|
|
2021
|
+
id: 24,
|
|
1969
2022
|
message: 'id non-existent',
|
|
1970
2023
|
data: [handleId, 'create rtc connection']
|
|
1971
2024
|
});
|
|
@@ -2151,9 +2204,8 @@ class RoomSession {
|
|
|
2151
2204
|
event.track.onended = (ev) => {
|
|
2152
2205
|
this._log('Remote track ended', ev);
|
|
2153
2206
|
|
|
2154
|
-
// TODO: check this
|
|
2155
|
-
|
|
2156
2207
|
const trackIndex = config?.tracks?.findIndex(t => t.id === ev.target.id);
|
|
2208
|
+
|
|
2157
2209
|
if(trackIndex > -1) {
|
|
2158
2210
|
config.tracks.splice(trackIndex, 1);
|
|
2159
2211
|
}
|
|
@@ -2253,7 +2305,7 @@ class RoomSession {
|
|
|
2253
2305
|
|
|
2254
2306
|
let handle = this.#getHandle(handleId);
|
|
2255
2307
|
if (!handle) {
|
|
2256
|
-
return Promise.reject({type: 'warning', id:
|
|
2308
|
+
return Promise.reject({type: 'warning', id: 25, message: 'id non-existent', data: [handleId, 'rtc peer']});
|
|
2257
2309
|
}
|
|
2258
2310
|
|
|
2259
2311
|
var config = handle.webrtcStuff;
|
|
@@ -2272,9 +2324,13 @@ class RoomSession {
|
|
|
2272
2324
|
for (var i = 0; i < config.candidates.length; i++) {
|
|
2273
2325
|
var candidate = config.candidates[i];
|
|
2274
2326
|
if (!candidate || candidate.completed === true) {
|
|
2275
|
-
config.pc.addIceCandidate(null)
|
|
2327
|
+
config.pc.addIceCandidate(null).catch((e) => {
|
|
2328
|
+
this._log('Error adding null candidate', e);
|
|
2329
|
+
});;
|
|
2276
2330
|
} else {
|
|
2277
|
-
config.pc.addIceCandidate(candidate)
|
|
2331
|
+
config.pc.addIceCandidate(candidate).catch((e) => {
|
|
2332
|
+
this._log('Error adding candidate', e);
|
|
2333
|
+
});
|
|
2278
2334
|
}
|
|
2279
2335
|
}
|
|
2280
2336
|
config.candidates = [];
|
|
@@ -2283,10 +2339,10 @@ class RoomSession {
|
|
|
2283
2339
|
return true;
|
|
2284
2340
|
})
|
|
2285
2341
|
.catch((e) => {
|
|
2286
|
-
return Promise.reject({type: 'warning', id:
|
|
2342
|
+
return Promise.reject({type: 'warning', id: 26, message: 'rtc peer', data: [handleId, e]});
|
|
2287
2343
|
});
|
|
2288
2344
|
} else {
|
|
2289
|
-
return Promise.reject({type: 'warning', id:
|
|
2345
|
+
return Promise.reject({type: 'warning', id: 27, message: 'rtc peer', data: [handleId, 'invalid jsep']});
|
|
2290
2346
|
}
|
|
2291
2347
|
}
|
|
2292
2348
|
|
|
@@ -2305,6 +2361,9 @@ class RoomSession {
|
|
|
2305
2361
|
|
|
2306
2362
|
config.isIceRestarting = true;
|
|
2307
2363
|
|
|
2364
|
+
// removing this so we can cache ice candidates again
|
|
2365
|
+
config.remoteSdp = null;
|
|
2366
|
+
|
|
2308
2367
|
if (this.#publisherHandle.handleId === handleId) {
|
|
2309
2368
|
this._log('Performing local ICE restart');
|
|
2310
2369
|
let hasAudio = !!(config.stream && config.stream.getAudioTracks().length > 0);
|
|
@@ -2325,7 +2384,7 @@ class RoomSession {
|
|
|
2325
2384
|
})
|
|
2326
2385
|
.catch((e) => {
|
|
2327
2386
|
config.isIceRestarting = false;
|
|
2328
|
-
this.emit('error', {type: '
|
|
2387
|
+
this.emit('error', {type: 'error', id: 28, message: 'iceRestart failed', data: e});
|
|
2329
2388
|
});
|
|
2330
2389
|
} else {
|
|
2331
2390
|
this._log('Performing remote ICE restart', handleId);
|
|
@@ -2430,7 +2489,7 @@ class RoomSession {
|
|
|
2430
2489
|
if (!handle) {
|
|
2431
2490
|
return Promise.reject({
|
|
2432
2491
|
type: 'warning',
|
|
2433
|
-
id:
|
|
2492
|
+
id: 29,
|
|
2434
2493
|
message: 'id non-existent',
|
|
2435
2494
|
data: [handleId, 'createAO', type]
|
|
2436
2495
|
});
|
|
@@ -2464,7 +2523,7 @@ class RoomSession {
|
|
|
2464
2523
|
.catch((e) => {
|
|
2465
2524
|
return Promise.reject({
|
|
2466
2525
|
type: 'warning',
|
|
2467
|
-
id:
|
|
2526
|
+
id: 30,
|
|
2468
2527
|
message: 'setLocalDescription',
|
|
2469
2528
|
data: [handleId, e]
|
|
2470
2529
|
})
|
|
@@ -2491,7 +2550,7 @@ class RoomSession {
|
|
|
2491
2550
|
|
|
2492
2551
|
return _p.then(() => jsep)
|
|
2493
2552
|
}, (e) => {
|
|
2494
|
-
return Promise.reject({type: 'warning', id:
|
|
2553
|
+
return Promise.reject({type: 'warning', id: 31, message: methodName, data: [handleId, e]})
|
|
2495
2554
|
});
|
|
2496
2555
|
|
|
2497
2556
|
}
|
|
@@ -2501,7 +2560,7 @@ class RoomSession {
|
|
|
2501
2560
|
if (!handle) {
|
|
2502
2561
|
return Promise.reject({
|
|
2503
2562
|
type: 'warning',
|
|
2504
|
-
id:
|
|
2563
|
+
id: 32,
|
|
2505
2564
|
message: 'id non-existent',
|
|
2506
2565
|
data: [handleId, 'publish remote participant']
|
|
2507
2566
|
})
|
|
@@ -2512,7 +2571,6 @@ class RoomSession {
|
|
|
2512
2571
|
let config = handle.webrtcStuff;
|
|
2513
2572
|
|
|
2514
2573
|
if (jsep) {
|
|
2515
|
-
|
|
2516
2574
|
return config.pc.setRemoteDescription(jsep)
|
|
2517
2575
|
.then(() => {
|
|
2518
2576
|
config.remoteSdp = jsep.sdp;
|
|
@@ -2522,10 +2580,14 @@ class RoomSession {
|
|
|
2522
2580
|
var candidate = config.candidates[i];
|
|
2523
2581
|
if (!candidate || candidate.completed === true) {
|
|
2524
2582
|
// end-of-candidates
|
|
2525
|
-
config.pc.addIceCandidate(null)
|
|
2583
|
+
config.pc.addIceCandidate(null).catch(e => {
|
|
2584
|
+
this._log('Error adding null candidate', e);
|
|
2585
|
+
});
|
|
2526
2586
|
} else {
|
|
2527
2587
|
// New candidate
|
|
2528
|
-
config.pc.addIceCandidate(candidate)
|
|
2588
|
+
config.pc.addIceCandidate(candidate).catch(e => {
|
|
2589
|
+
this._log('Error adding candidate', e);
|
|
2590
|
+
});
|
|
2529
2591
|
}
|
|
2530
2592
|
}
|
|
2531
2593
|
config.candidates = [];
|
|
@@ -2539,7 +2601,7 @@ class RoomSession {
|
|
|
2539
2601
|
if (!_jsep) {
|
|
2540
2602
|
this.emit('error', {
|
|
2541
2603
|
type: 'warning',
|
|
2542
|
-
id:
|
|
2604
|
+
id: 33,
|
|
2543
2605
|
message: 'publish remote participant',
|
|
2544
2606
|
data: [handleId, 'no jsep']
|
|
2545
2607
|
});
|
|
@@ -2552,7 +2614,7 @@ class RoomSession {
|
|
|
2552
2614
|
})
|
|
2553
2615
|
}, (e) => Promise.reject({
|
|
2554
2616
|
type: 'warning',
|
|
2555
|
-
id:
|
|
2617
|
+
id: 34,
|
|
2556
2618
|
message: 'setRemoteDescription',
|
|
2557
2619
|
data: [handleId, e]
|
|
2558
2620
|
}));
|
|
@@ -2564,7 +2626,7 @@ class RoomSession {
|
|
|
2564
2626
|
}
|
|
2565
2627
|
|
|
2566
2628
|
#republishOnTrackEnded(source) {
|
|
2567
|
-
let handle = this.#
|
|
2629
|
+
let handle = this.#publisherHandle;
|
|
2568
2630
|
if (!handle) {
|
|
2569
2631
|
return;
|
|
2570
2632
|
}
|
|
@@ -2592,10 +2654,10 @@ class RoomSession {
|
|
|
2592
2654
|
|
|
2593
2655
|
async publishLocal(stream = null, source = 'camera0') {
|
|
2594
2656
|
|
|
2595
|
-
if(!this.isConnected) {
|
|
2657
|
+
if(!this.isConnected || this.isDisconnecting) {
|
|
2596
2658
|
return {
|
|
2597
2659
|
type: 'warning',
|
|
2598
|
-
id:
|
|
2660
|
+
id: 35,
|
|
2599
2661
|
message: 'Either not connected or disconnecting',
|
|
2600
2662
|
}
|
|
2601
2663
|
}
|
|
@@ -2603,7 +2665,7 @@ class RoomSession {
|
|
|
2603
2665
|
if(stream?.getVideoTracks()?.length > 1) {
|
|
2604
2666
|
return {
|
|
2605
2667
|
type: 'warning',
|
|
2606
|
-
id:
|
|
2668
|
+
id: 36,
|
|
2607
2669
|
message: 'multiple video tracks not supported',
|
|
2608
2670
|
data: null
|
|
2609
2671
|
}
|
|
@@ -2612,7 +2674,7 @@ class RoomSession {
|
|
|
2612
2674
|
if(stream?.getAudioTracks()?.length > 1) {
|
|
2613
2675
|
return {
|
|
2614
2676
|
type: 'warning',
|
|
2615
|
-
id:
|
|
2677
|
+
id: 37,
|
|
2616
2678
|
message: 'multiple audio tracks not supported',
|
|
2617
2679
|
data: null
|
|
2618
2680
|
}
|
|
@@ -2622,7 +2684,7 @@ class RoomSession {
|
|
|
2622
2684
|
if (!handle) {
|
|
2623
2685
|
return {
|
|
2624
2686
|
type: 'error',
|
|
2625
|
-
id:
|
|
2687
|
+
id: 38,
|
|
2626
2688
|
message: 'no local handle, connect before publishing',
|
|
2627
2689
|
data: null
|
|
2628
2690
|
}
|
|
@@ -2833,24 +2895,24 @@ class RoomSession {
|
|
|
2833
2895
|
if(this._isDataChannelOpen !== true) {
|
|
2834
2896
|
await new Promise((resolve, reject) => {
|
|
2835
2897
|
let dataChannelTimeoutId = null;
|
|
2898
|
+
let _cleanup = () => {
|
|
2899
|
+
clearTimeout(dataChannelTimeoutId);
|
|
2900
|
+
this._abortController.signal.removeEventListener('abort', _rejectAbort);
|
|
2901
|
+
this.off('dataChannel', _resolve, this);
|
|
2902
|
+
}
|
|
2836
2903
|
let _resolve = (val) => {
|
|
2837
2904
|
if (val) {
|
|
2838
|
-
|
|
2839
|
-
this._abortController.signal.removeEventListener('abort', _rejectAbort);
|
|
2840
|
-
this.off('dataChannel', _resolve, this);
|
|
2905
|
+
_cleanup();
|
|
2841
2906
|
resolve(this);
|
|
2842
2907
|
}
|
|
2843
2908
|
};
|
|
2844
2909
|
let _rejectTimeout = () => {
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
reject({type: 'error', id: 27, message: 'Data channel did not open', data: null});
|
|
2910
|
+
_cleanup();
|
|
2911
|
+
reject({type: 'error', id: 39, message: 'Data channel did not open', data: null});
|
|
2848
2912
|
}
|
|
2849
2913
|
let _rejectAbort = () => {
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
this.off('dataChannel', _resolve, this);
|
|
2853
|
-
reject({type: 'warning', id: 17, message: 'Connection cancelled'})
|
|
2914
|
+
_cleanup();
|
|
2915
|
+
reject({type: 'warning', id: 40, message: 'Connection cancelled'})
|
|
2854
2916
|
}
|
|
2855
2917
|
dataChannelTimeoutId = setTimeout(_rejectTimeout, 10000);
|
|
2856
2918
|
this._abortController.signal.addEventListener('abort', _rejectAbort);
|
|
@@ -2880,11 +2942,11 @@ class RoomSession {
|
|
|
2880
2942
|
}
|
|
2881
2943
|
|
|
2882
2944
|
toggleAudio(value = null, source = 'camera0', mid) {
|
|
2883
|
-
let handle = this.#
|
|
2945
|
+
let handle = this.#publisherHandle;
|
|
2884
2946
|
if (!handle) {
|
|
2885
2947
|
this.emit('error', {
|
|
2886
2948
|
type: 'warning',
|
|
2887
|
-
id:
|
|
2949
|
+
id: 41,
|
|
2888
2950
|
message: 'no local id, connect first', data: null
|
|
2889
2951
|
});
|
|
2890
2952
|
return;
|
|
@@ -2917,11 +2979,11 @@ class RoomSession {
|
|
|
2917
2979
|
}
|
|
2918
2980
|
|
|
2919
2981
|
toggleVideo(value = null, source = 'camera0', mid) {
|
|
2920
|
-
let handle = this.#
|
|
2982
|
+
let handle = this.#publisherHandle;
|
|
2921
2983
|
if (!handle) {
|
|
2922
2984
|
this.emit('error', {
|
|
2923
2985
|
type: 'warning',
|
|
2924
|
-
id:
|
|
2986
|
+
id: 42,
|
|
2925
2987
|
message: 'no local id, connect first', data: null
|
|
2926
2988
|
});
|
|
2927
2989
|
return;
|
|
@@ -2969,10 +3031,38 @@ class RoomSession {
|
|
|
2969
3031
|
|
|
2970
3032
|
#setSelectedSubstream(sender, mid, substream) {
|
|
2971
3033
|
|
|
3034
|
+
const handle = this.#getHandle(sender);
|
|
3035
|
+
|
|
3036
|
+
if(!handle) {
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
handle.webrtcStuff.lastSwitchTime.set(mid, Date.now());
|
|
3041
|
+
|
|
3042
|
+
if(!handle.webrtcStuff.currentLayers.has(mid)) {
|
|
3043
|
+
handle.webrtcStuff.currentLayers.set(mid, {mid, substream: substream, temporal: -1});
|
|
3044
|
+
}
|
|
3045
|
+
else {
|
|
3046
|
+
handle.webrtcStuff.currentLayers.get(mid).substream = substream;
|
|
3047
|
+
}
|
|
2972
3048
|
}
|
|
2973
3049
|
|
|
2974
3050
|
#setSelectedTemporal(sender, mid, temporal) {
|
|
2975
3051
|
|
|
3052
|
+
const handle = this.#getHandle(sender);
|
|
3053
|
+
|
|
3054
|
+
if(!handle) {
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
handle.webrtcStuff.lastSwitchTime.set(mid, Date.now());
|
|
3059
|
+
|
|
3060
|
+
if(!handle.webrtcStuff.currentLayers.has(mid)) {
|
|
3061
|
+
handle.webrtcStuff.currentLayers.set(mid, {mid, substream: -1, temporal: temporal});
|
|
3062
|
+
}
|
|
3063
|
+
else {
|
|
3064
|
+
handle.webrtcStuff.currentLayers.get(mid).temporal = temporal;
|
|
3065
|
+
}
|
|
2976
3066
|
}
|
|
2977
3067
|
|
|
2978
3068
|
#shouldEmitFeedUpdate(parsedDisplay = {}) {
|
|
@@ -3084,8 +3174,9 @@ class RoomSession {
|
|
|
3084
3174
|
return await this.#updateSubscriptions();
|
|
3085
3175
|
}
|
|
3086
3176
|
|
|
3087
|
-
async selectSubStream(id, substream = 2, source, mid) {
|
|
3088
|
-
|
|
3177
|
+
async selectSubStream(id, substream = 2, temporal = 2, source, mid) {
|
|
3178
|
+
|
|
3179
|
+
this._log('Select substream called for id:', id, 'Source, mid:', source, mid, 'Substream:', substream, 'Temporal:', temporal);
|
|
3089
3180
|
|
|
3090
3181
|
let config = this.#subscriberHandle.webrtcStuff;
|
|
3091
3182
|
return new Promise((resolve, reject) => {
|
|
@@ -3150,7 +3241,7 @@ class RoomSession {
|
|
|
3150
3241
|
"request": "configure",
|
|
3151
3242
|
"streams": [
|
|
3152
3243
|
{
|
|
3153
|
-
mid, substream: parseInt(substream)
|
|
3244
|
+
mid, substream: parseInt(substream), temporal: parseInt(temporal)
|
|
3154
3245
|
}
|
|
3155
3246
|
]
|
|
3156
3247
|
}
|
|
@@ -3165,11 +3256,406 @@ class RoomSession {
|
|
|
3165
3256
|
});
|
|
3166
3257
|
}
|
|
3167
3258
|
|
|
3168
|
-
#getStats(type =
|
|
3259
|
+
async #getStats(type = 'video') {
|
|
3260
|
+
|
|
3261
|
+
let handle = this.#subscriberHandle;
|
|
3262
|
+
if(handle) {
|
|
3263
|
+
|
|
3264
|
+
const tracks = handle?.webrtcStuff?.tracks.filter(track => track.kind === type);
|
|
3265
|
+
const transceivers = handle?.webrtcStuff?.pc?.getTransceivers();
|
|
3266
|
+
return await tracks.reduce(async (prevPromise, track) => {
|
|
3267
|
+
|
|
3268
|
+
const results = await prevPromise;
|
|
3269
|
+
const mid = transceivers.find(t =>
|
|
3270
|
+
t.receiver?.track?.id === track.id || t.sender?.track?.id === track.id
|
|
3271
|
+
)?.mid;
|
|
3272
|
+
const transceiverData = this.#getTransceiverDataByMid(mid);
|
|
3273
|
+
|
|
3274
|
+
const id = transceiverData?.feed_id ?? null;
|
|
3275
|
+
const display = transceiverData?.feed_display ?? null;
|
|
3276
|
+
const description = transceiverData.feed_description ?? null;
|
|
3277
|
+
|
|
3278
|
+
return handle.webrtcStuff.pc.getStats(track)
|
|
3279
|
+
.then(stats => {
|
|
3280
|
+
|
|
3281
|
+
const parsedStats = {
|
|
3282
|
+
inboundRtpStats: null,
|
|
3283
|
+
remoteOutboundRtpStats: null,
|
|
3284
|
+
candidatePairStats: null,
|
|
3285
|
+
packetLoss: 0,
|
|
3286
|
+
jitter: 0,
|
|
3287
|
+
rtt: 0,
|
|
3288
|
+
currentBandwidth: 0,
|
|
3289
|
+
framesDecoded: 0,
|
|
3290
|
+
framesDropped: 0,
|
|
3291
|
+
framesReceived: 0,
|
|
3292
|
+
frameRate: 0,
|
|
3293
|
+
freezeCount: 0,
|
|
3294
|
+
totalFreezesDuration: 0,
|
|
3295
|
+
};
|
|
3296
|
+
|
|
3297
|
+
let selectedCandidatePairId = null;
|
|
3298
|
+
|
|
3299
|
+
// First pass to identify the selected candidate pair ID from transport
|
|
3300
|
+
stats.forEach(stat => {
|
|
3301
|
+
if (stat.type === 'transport' && stat.selectedCandidatePairId) {
|
|
3302
|
+
selectedCandidatePairId = stat.selectedCandidatePairId;
|
|
3303
|
+
}
|
|
3304
|
+
});
|
|
3305
|
+
|
|
3306
|
+
// Second pass to process all stats
|
|
3307
|
+
stats.forEach(stat => {
|
|
3308
|
+
// Inbound RTP stats processing
|
|
3309
|
+
if (stat.type === 'inbound-rtp' && !stat.isRemote) {
|
|
3310
|
+
parsedStats.inboundRtpStats = stat;
|
|
3311
|
+
|
|
3312
|
+
// Consistent unit conversion - convert seconds to milliseconds for jitter
|
|
3313
|
+
parsedStats.jitter = stat.jitter ? Math.round(stat.jitter * 1000) : 0;
|
|
3314
|
+
|
|
3315
|
+
parsedStats.framesDecoded = stat.framesDecoded || 0;
|
|
3316
|
+
parsedStats.framesDropped = stat.framesDropped || 0;
|
|
3317
|
+
parsedStats.framesReceived = stat.framesReceived || 0;
|
|
3318
|
+
parsedStats.frameRate = stat.framesPerSecond || 0;
|
|
3319
|
+
parsedStats.freezeCount = stat.freezeCount || 0;
|
|
3320
|
+
parsedStats.totalFreezesDuration = stat.totalFreezesDuration || 0;
|
|
3321
|
+
|
|
3322
|
+
// Robust packet loss calculation
|
|
3323
|
+
if (stat.packetsLost !== undefined && stat.packetsReceived !== undefined) {
|
|
3324
|
+
const totalPackets = stat.packetsLost + stat.packetsReceived;
|
|
3325
|
+
if (totalPackets > 0) {
|
|
3326
|
+
// Calculate as percentage and round to 2 decimal places for accuracy
|
|
3327
|
+
parsedStats.packetLoss = Number(((stat.packetsLost / totalPackets) * 100).toFixed(2));
|
|
3328
|
+
} else {
|
|
3329
|
+
parsedStats.packetLoss = 0;
|
|
3330
|
+
}
|
|
3331
|
+
} else {
|
|
3332
|
+
parsedStats.packetLoss = 0;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
if (stat.type === 'remote-outbound-rtp') {
|
|
3337
|
+
parsedStats.remoteOutboundRtpStats = stat;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// Find active candidate pair based on the selectedCandidatePairId
|
|
3341
|
+
if (stat.type === 'candidate-pair' &&
|
|
3342
|
+
(stat.selected || stat.id === selectedCandidatePairId)) {
|
|
3343
|
+
parsedStats.candidatePairStats = stat;
|
|
3344
|
+
|
|
3345
|
+
// RTT calculation from candidate pair
|
|
3346
|
+
if (stat.currentRoundTripTime) {
|
|
3347
|
+
parsedStats.rtt = Math.round(stat.currentRoundTripTime * 1000); // Convert to ms
|
|
3348
|
+
} else if (stat.totalRoundTripTime && stat.responsesReceived > 0) {
|
|
3349
|
+
parsedStats.rtt = Math.round((stat.totalRoundTripTime / stat.responsesReceived) * 1000);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
// Additional fallback for bandwidth estimation from inboundRtp
|
|
3355
|
+
if (parsedStats.currentBandwidth === 0 && parsedStats.inboundRtpStats) {
|
|
3356
|
+
const stat = parsedStats.inboundRtpStats;
|
|
3357
|
+
// Simple estimation based on received bytes over time
|
|
3358
|
+
if (stat.bytesReceived && stat.timestamp && handle.webrtcStuff.stats &&
|
|
3359
|
+
handle.webrtcStuff.stats[stat.id]) {
|
|
3360
|
+
const prevStat = handle.webrtcStuff.stats[stat.id];
|
|
3361
|
+
const timeDiff = stat.timestamp - prevStat.timestamp;
|
|
3362
|
+
if (timeDiff > 0) {
|
|
3363
|
+
const bitrateBps = 8000 * (stat.bytesReceived - prevStat.bytesReceived) / timeDiff;
|
|
3364
|
+
parsedStats.currentBandwidth = Math.floor(bitrateBps / 1000);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
// Store current stats for next calculation
|
|
3368
|
+
if (!handle.webrtcStuff.stats) {
|
|
3369
|
+
handle.webrtcStuff.stats = {};
|
|
3370
|
+
}
|
|
3371
|
+
handle.webrtcStuff.stats[stat.id] = { ...stat };
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
return [...results, {
|
|
3375
|
+
stats: Object.fromEntries(stats), // Convert MapLike object to regular object
|
|
3376
|
+
parsedStats,
|
|
3377
|
+
id,
|
|
3378
|
+
display,
|
|
3379
|
+
description,
|
|
3380
|
+
mid
|
|
3381
|
+
}];
|
|
3382
|
+
});
|
|
3383
|
+
}, Promise.resolve([]))
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
return [];
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
#adjustVideoQualitySettings(stats) {
|
|
3390
|
+
|
|
3391
|
+
// Only proceed if we have stats to work with and simulcast is enabled
|
|
3392
|
+
if (!stats?.length || !this.simulcast) {
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
const handle = this.#subscriberHandle;
|
|
3397
|
+
const webrtcStuff = handle?.webrtcStuff;
|
|
3398
|
+
|
|
3399
|
+
if(!handle) {
|
|
3400
|
+
return
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
const now = Date.now();
|
|
3404
|
+
|
|
3405
|
+
// Process each video stream's stats
|
|
3406
|
+
for (const stat of stats) {
|
|
3407
|
+
// Skip if no parsedStats or missing essential data
|
|
3408
|
+
if (!stat.parsedStats || !stat.mid || !stat.id || !stat.description) {
|
|
3409
|
+
continue;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
const mid = stat.mid;
|
|
3413
|
+
const feedId = stat.id;
|
|
3414
|
+
|
|
3415
|
+
// Parse description to get simulcast settings
|
|
3416
|
+
let simulcastConfig = null;
|
|
3417
|
+
try {
|
|
3418
|
+
const description = typeof stat.description === 'string' ?
|
|
3419
|
+
JSON.parse(stat.description) : stat.description;
|
|
3420
|
+
|
|
3421
|
+
if (description?.simulcastBitrates) {
|
|
3422
|
+
simulcastConfig = {
|
|
3423
|
+
bitrates: description.simulcastBitrates,
|
|
3424
|
+
defaultSubstream: 0 // Default to highest quality initially
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
} catch (e) {
|
|
3428
|
+
this._log('Error parsing simulcast config:', e);
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
// Skip if no simulcast config or we don't have layer tracking for this mid
|
|
3433
|
+
if (!simulcastConfig || !webrtcStuff?.currentLayers?.has(mid)) {
|
|
3434
|
+
continue;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
const currentLayer = webrtcStuff?.currentLayers?.get(mid);
|
|
3438
|
+
const currentSubstream = currentLayer.substream;
|
|
3439
|
+
const currentTemporal = currentLayer.temporal === -1 ? 2 : currentLayer.temporal;
|
|
3440
|
+
|
|
3441
|
+
// Initialize quality history for this mid if needed
|
|
3442
|
+
if (!webrtcStuff.qualityHistory.has(mid)) {
|
|
3443
|
+
webrtcStuff.qualityHistory.set(mid, []);
|
|
3444
|
+
webrtcStuff.lastSwitchTime.set(mid, 0);
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
// Extract network stats
|
|
3448
|
+
const {
|
|
3449
|
+
packetLoss,
|
|
3450
|
+
jitter,
|
|
3451
|
+
rtt,
|
|
3452
|
+
currentBandwidth,
|
|
3453
|
+
framesDropped,
|
|
3454
|
+
framesReceived,
|
|
3455
|
+
freezeCount,
|
|
3456
|
+
totalFreezesDuration
|
|
3457
|
+
} = stat.parsedStats;
|
|
3458
|
+
|
|
3459
|
+
// Calculated stats (like frame drop rate)
|
|
3460
|
+
const frameDropRate = framesReceived ? (framesDropped / framesReceived * 100) : 0;
|
|
3461
|
+
|
|
3462
|
+
// Add current quality measurement to history
|
|
3463
|
+
const qualityMeasure = {
|
|
3464
|
+
timestamp: now,
|
|
3465
|
+
packetLoss,
|
|
3466
|
+
jitter,
|
|
3467
|
+
rtt,
|
|
3468
|
+
currentBandwidth,
|
|
3469
|
+
frameDropRate,
|
|
3470
|
+
freezeCount,
|
|
3471
|
+
totalFreezesDuration
|
|
3472
|
+
};
|
|
3473
|
+
|
|
3474
|
+
// Determine whether we need to switch qualities
|
|
3475
|
+
let targetSubstream = currentSubstream;
|
|
3476
|
+
|
|
3477
|
+
const history = webrtcStuff.qualityHistory.get(mid);
|
|
3478
|
+
history.push(qualityMeasure);
|
|
3479
|
+
|
|
3480
|
+
// Keep only recent history
|
|
3481
|
+
const recentHistory = history.filter(item => now - item.timestamp < this.#rtcStatsConfig.historySize);
|
|
3482
|
+
webrtcStuff.qualityHistory.set(mid, recentHistory);
|
|
3483
|
+
|
|
3484
|
+
let totalFreezesDurationTillLast = null;
|
|
3485
|
+
|
|
3486
|
+
// if we can calculate the freeze duration for last measured segment
|
|
3487
|
+
|
|
3488
|
+
if(recentHistory.length - 2 > -1) {
|
|
3169
3489
|
|
|
3490
|
+
totalFreezesDurationTillLast = totalFreezesDuration - (recentHistory?.[recentHistory.length - 2]?.totalFreezesDuration ?? 0);
|
|
3491
|
+
if(totalFreezesDurationTillLast * 1000 >= this.#rtcStatsConfig.freezeLengthThreshold * this._statsInterval) {
|
|
3492
|
+
this._log(`Freezes detected for ${mid} - totalFreezesDuration: ${totalFreezesDuration}, totalFreezesDurationTillLast: ${totalFreezesDurationTillLast}`);
|
|
3493
|
+
if (currentSubstream > 0) {
|
|
3494
|
+
targetSubstream = currentSubstream - 1;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// Apply the layer change if needed
|
|
3498
|
+
// Don't change layers if we haven't waited enough since last switch
|
|
3499
|
+
|
|
3500
|
+
if (targetSubstream !== currentSubstream && now - webrtcStuff.lastSwitchTime.get(mid) >= this.#rtcStatsConfig.switchCooldownForFreeze) {
|
|
3501
|
+
webrtcStuff.lastSwitchTime.set(mid, now);
|
|
3502
|
+
this.selectSubStream(feedId, targetSubstream, currentTemporal, null, mid)
|
|
3503
|
+
.then(() => {
|
|
3504
|
+
this._log(`Successfully switched substream for ${mid} to ${targetSubstream}`);
|
|
3505
|
+
})
|
|
3506
|
+
.catch(err => {
|
|
3507
|
+
this._log(`Failed to switch substream for ${mid}:`, err);
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
continue;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// Don't change layers if we haven't waited enough since last switch
|
|
3515
|
+
if (now - webrtcStuff.lastSwitchTime.get(mid) < this.#rtcStatsConfig.switchCooldown) {
|
|
3516
|
+
continue;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// Get median values from recent history to avoid making decisions on outliers
|
|
3520
|
+
const medianPacketLoss = median(recentHistory.map(item => item.packetLoss));
|
|
3521
|
+
const medianJitter = median(recentHistory.map(item => item.jitter));
|
|
3522
|
+
const medianRtt = median(recentHistory.map(item => item.rtt));
|
|
3523
|
+
const medianBandwidth = median(recentHistory.map(item => item.currentBandwidth));
|
|
3524
|
+
const medianFrameDropRate = median(recentHistory.map(item => item.frameDropRate));
|
|
3525
|
+
|
|
3526
|
+
// Check for network issues that would require downgrading
|
|
3527
|
+
const hasHighPacketLoss = medianPacketLoss > this.#rtcStatsConfig.packetLossThreshold;
|
|
3528
|
+
const hasHighJitter = medianJitter > this.#rtcStatsConfig.jitterBufferThreshold;
|
|
3529
|
+
const hasHighRtt = medianRtt > this.#rtcStatsConfig.roundTripTimeThreshold;
|
|
3530
|
+
// Check if we have high frame drop rate
|
|
3531
|
+
const hasHighFrameDropRate = medianFrameDropRate > this.#rtcStatsConfig.frameDropRateThreshold; // 5% frame drop threshold
|
|
3532
|
+
|
|
3533
|
+
// Determine required bandwidth based on simulcast config
|
|
3534
|
+
let currentLayerBitrate = 0;
|
|
3535
|
+
let nextHigherLayerBitrate = 0;
|
|
3536
|
+
|
|
3537
|
+
// Bitrates are ordered from high to low (h, m, l)
|
|
3538
|
+
if (simulcastConfig?.bitrates?.length > currentSubstream) {
|
|
3539
|
+
currentLayerBitrate = simulcastConfig.bitrates[2 - currentSubstream].maxBitrate / 1000; // Convert to kbps
|
|
3540
|
+
|
|
3541
|
+
if (currentSubstream < simulcastConfig?.bitrates?.length - 1) {
|
|
3542
|
+
nextHigherLayerBitrate = simulcastConfig.bitrates[(2 - currentSubstream) - 1].maxBitrate / 1000;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
// !!! WE CAN'T USE THIS AS IT REPORTS medianBandwidth of 1500 for 2000 stream and the stream works just fine
|
|
3547
|
+
// Check if we have enough bandwidth for current layer
|
|
3548
|
+
const hasLowBandwidthForCurrentLayer = medianBandwidth < (currentLayerBitrate * this.#rtcStatsConfig.badBandwidthThresholdMultiplier);
|
|
3549
|
+
|
|
3550
|
+
// Log stats for debugging
|
|
3551
|
+
this._log(`Stream ${mid} stats:`, {
|
|
3552
|
+
packetLoss: medianPacketLoss,
|
|
3553
|
+
jitter: medianJitter,
|
|
3554
|
+
rtt: medianRtt,
|
|
3555
|
+
bandwidth: medianBandwidth,
|
|
3556
|
+
frameDropRate: medianFrameDropRate,
|
|
3557
|
+
currentLayerBitrate,
|
|
3558
|
+
currentSubstream,
|
|
3559
|
+
totalFreezesDurationTillLast
|
|
3560
|
+
});
|
|
3561
|
+
|
|
3562
|
+
// Network issues detected - downgrade if possible
|
|
3563
|
+
if (hasHighPacketLoss || hasHighJitter || hasHighRtt || hasHighFrameDropRate) {
|
|
3564
|
+
// Can't downgrade if already at lowest quality
|
|
3565
|
+
if (currentSubstream > 0) {
|
|
3566
|
+
targetSubstream = currentSubstream - 1;
|
|
3567
|
+
this._log(`Downgrading stream quality for ${mid} due to network issues:`, {
|
|
3568
|
+
packetLoss: medianPacketLoss,
|
|
3569
|
+
jitter: medianJitter,
|
|
3570
|
+
rtt: medianRtt,
|
|
3571
|
+
bandwidth: medianBandwidth,
|
|
3572
|
+
frameDropRate: medianFrameDropRate,
|
|
3573
|
+
from: currentSubstream,
|
|
3574
|
+
to: targetSubstream
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
// Check if we can upgrade - network is good
|
|
3579
|
+
else {
|
|
3580
|
+
|
|
3581
|
+
const stableHistory = recentHistory.filter(item => now - item.timestamp < this.#rtcStatsConfig.stableNetworkTime)
|
|
3582
|
+
const hasStableNetwork = stableHistory.every(item => item.packetLoss < this.#rtcStatsConfig.packetLossThreshold &&
|
|
3583
|
+
item.jitter < this.#rtcStatsConfig.jitterBufferThreshold &&
|
|
3584
|
+
item.rtt < this.#rtcStatsConfig.roundTripTimeThreshold &&
|
|
3585
|
+
item.frameDropRate < this.#rtcStatsConfig.frameDropRateThreshold
|
|
3586
|
+
);
|
|
3587
|
+
|
|
3588
|
+
if (hasStableNetwork) {
|
|
3589
|
+
if(currentSubstream < 2) {
|
|
3590
|
+
targetSubstream = currentSubstream + 1;
|
|
3591
|
+
this._log(`Upgrading stream quality for ${mid} due to good network conditions:`, {
|
|
3592
|
+
packetLoss: medianPacketLoss,
|
|
3593
|
+
jitter: medianJitter,
|
|
3594
|
+
rtt: medianRtt,
|
|
3595
|
+
bandwidth: medianBandwidth,
|
|
3596
|
+
from: currentSubstream,
|
|
3597
|
+
to: targetSubstream
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
// Apply the layer change if needed
|
|
3604
|
+
if (targetSubstream !== currentSubstream) {
|
|
3605
|
+
webrtcStuff.lastSwitchTime.set(mid, now);
|
|
3606
|
+
this.selectSubStream(feedId, targetSubstream, currentTemporal, null, mid)
|
|
3607
|
+
.then(() => {
|
|
3608
|
+
this._log(`Successfully switched substream for ${mid} to ${targetSubstream}`);
|
|
3609
|
+
})
|
|
3610
|
+
.catch(err => {
|
|
3611
|
+
this._log(`Failed to switch substream for ${mid}:`, err);
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3170
3615
|
}
|
|
3171
3616
|
|
|
3172
|
-
#
|
|
3617
|
+
#emitRtcStats(stats) {
|
|
3618
|
+
if (stats?.length > 0) {
|
|
3619
|
+
const s = stats.map(stat => {
|
|
3620
|
+
const {
|
|
3621
|
+
currentBandwidth,
|
|
3622
|
+
frameRate,
|
|
3623
|
+
framesDecoded,
|
|
3624
|
+
framesDropped,
|
|
3625
|
+
framesReceived,
|
|
3626
|
+
freezeCount,
|
|
3627
|
+
jitter,
|
|
3628
|
+
packetLoss,
|
|
3629
|
+
rtt,
|
|
3630
|
+
totalFreezesDuration
|
|
3631
|
+
} = stat.parsedStats;
|
|
3632
|
+
|
|
3633
|
+
let description = {};
|
|
3634
|
+
try {
|
|
3635
|
+
description = JSON.parse(stat.description);
|
|
3636
|
+
} catch {}
|
|
3637
|
+
return {
|
|
3638
|
+
id: stat.id,
|
|
3639
|
+
mid: stat.mid,
|
|
3640
|
+
userId: decodeJanusDisplay(stat.display)?.userId,
|
|
3641
|
+
source: description.source,
|
|
3642
|
+
stats: {
|
|
3643
|
+
currentBandwidth,
|
|
3644
|
+
frameRate,
|
|
3645
|
+
framesDecoded,
|
|
3646
|
+
framesDropped,
|
|
3647
|
+
framesReceived,
|
|
3648
|
+
freezeCount,
|
|
3649
|
+
jitter,
|
|
3650
|
+
packetLoss,
|
|
3651
|
+
rtt,
|
|
3652
|
+
totalFreezesDuration
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
});
|
|
3656
|
+
this.emit('rtcStats', s);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3173
3659
|
|
|
3174
3660
|
#disableStatsWatch() {
|
|
3175
3661
|
if (this._statsIntervalId) {
|
|
@@ -3177,13 +3663,21 @@ class RoomSession {
|
|
|
3177
3663
|
this._statsIntervalId = null;
|
|
3178
3664
|
}
|
|
3179
3665
|
}
|
|
3666
|
+
|
|
3180
3667
|
#enableStatsWatch() {
|
|
3181
3668
|
|
|
3182
3669
|
this.#disableStatsWatch();
|
|
3183
3670
|
|
|
3184
3671
|
const loop = () => {
|
|
3185
3672
|
this.#getStats('video')
|
|
3186
|
-
.then(
|
|
3673
|
+
.then(stats => {
|
|
3674
|
+
if (stats && stats.length > 0) {
|
|
3675
|
+
this.#emitRtcStats(stats)
|
|
3676
|
+
if(this.options.enableVideoQualityAdjustment) {
|
|
3677
|
+
this.#adjustVideoQualitySettings(stats);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
})
|
|
3187
3681
|
};
|
|
3188
3682
|
|
|
3189
3683
|
this._statsIntervalId = setInterval(loop, this._statsInterval);
|
|
@@ -3191,4 +3685,8 @@ class RoomSession {
|
|
|
3191
3685
|
|
|
3192
3686
|
}
|
|
3193
3687
|
|
|
3688
|
+
|
|
3194
3689
|
export default Room;
|
|
3690
|
+
|
|
3691
|
+
|
|
3692
|
+
|