@reactoo/watchtogether-sdk-js 2.6.68 → 2.6.69

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