@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.
@@ -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; //TODO: get rid of this?
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: 15,
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: 1, message: 'sendMessage failed', data: json["error"]})
884
+ return Promise.reject({type: 'warning', id: 2, message: 'sendMessage failed', data: json["error"]})
868
885
  } else {
869
- return Promise.reject({type: 'warning', id: 1, message: 'sendMessage failed', data: json});
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: 17, message: 'connection cancelled'})
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: 2, message: 'send failed', data: json, requestData});
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: 3, message: 'send timeout', data: requestData});
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: 29, message: 'No connection to WebSockets', data: requestData});
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
- this.disconnect();
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: 5,
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: 6, message: 'keepalive dead', data: e});
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: 7,
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: 8, message: 'remote participant error', data: [sender, msg]});
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: 9,
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: 10, message: 'data message parse error', data: [handleId, e]});
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
- // figure this out below
1412
- stats: {}
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
- // figure this out below
1515
- stats: {}
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
- this.emit('error', {type: 'warning', id: 16, message: 'connection already in progress'});
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: 17, message: 'Connection cancelled'});
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: 13, message: 'connection error', data: error})
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: 11, message: 'reconnection error', data: error})
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: 14, message: 'ws connection error', data: e});
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: 16, message: 'connection already in progress'});
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: 13, message: 'connection error', data: error})
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: 11, message: 'reconnection error', data: error})
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: 16, message: 'connection already in progress'});
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: 16, message: 'connection already in progress'});
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
- // sort simulcast bitrates
1830
- if(this.simulcastSettings && typeof this.simulcastSettings === 'object' && Object.keys(this.simulcastSettings).length) {
1831
- Object.keys(this.simulcastSettings).forEach(k => {
1832
- this.simulcastSettings[k].bitrates = this.simulcastSettings[k].bitrates.sort((a, b) => {
1833
- if(a.maxBitrate === b.maxBitrate) {
1834
- return a.maxFramerate - b.maxFramerate;
1835
- }
1836
- return a.maxBitrate - b.maxBitrate;
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: 15,
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: 15, message: 'id non-existent', data: [handleId, 'rtc peer']});
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: 32, message: 'rtc peer', data: [handleId, e]});
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: 22, message: 'rtc peer', data: [handleId, 'invalid jsep']});
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: 'warning', id: 28, message: 'iceRestart failed', data: e});
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: 15,
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: 24,
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: 25, message: methodName, data: [handleId, e]})
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: 15,
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: 19,
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: 23,
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.#getHandle(this.#publisherHandle.handleId);
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: 18,
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: 30,
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: 30,
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: 31,
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
- clearTimeout(dataChannelTimeoutId);
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
- this.off('dataChannel', _resolve, this);
2846
- this._abortController.signal.removeEventListener('abort', _rejectAbort);
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
- this._abortController.signal.removeEventListener('abort', _rejectAbort);
2851
- clearTimeout(dataChannelTimeoutId);
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.#getHandle(this.#publisherHandle.handleId);
2945
+ let handle = this.#publisherHandle;
2884
2946
  if (!handle) {
2885
2947
  this.emit('error', {
2886
2948
  type: 'warning',
2887
- id: 21,
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.#getHandle(this.#publisherHandle.handleId);
2982
+ let handle = this.#publisherHandle;
2921
2983
  if (!handle) {
2922
2984
  this.emit('error', {
2923
2985
  type: 'warning',
2924
- id: 21,
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
- this._log('Select substream called for id:', id, 'Source or mid:', source ? source : mid, 'Substream:', substream);
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 = null) {
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
- #adjustQualitySettings(stats) {}
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(this.#adjustQualitySettings)
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
+