@flashphoner/sfusdk-examples 2.0.264 → 2.0.269

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flashphoner/sfusdk-examples",
3
- "version": "2.0.264",
3
+ "version": "2.0.269",
4
4
  "description": "Official Flashphoner WebCallServer SFU SDK usage examples",
5
5
  "main": "dist/sfu.js",
6
6
  "types": "src/sfu.ts",
@@ -15,10 +15,13 @@
15
15
  "source": "camera",
16
16
  "width": 1280,
17
17
  "height": 720,
18
- "codec": "H264",
18
+ "codec": "vp9",
19
19
  "encodings": [
20
- { "rid": "m", "active": true, "maxBitrate": 300000, "scaleResolutionDownBy": 2 },
21
- { "rid": "h", "active": true, "maxBitrate": 900000 }
20
+ {
21
+ "rid": "nonsense",
22
+ "active": true,
23
+ "scalabilityMode": "L1T3"
24
+ }
22
25
  ]
23
26
  }]
24
27
  }
@@ -1,6 +1,7 @@
1
- const createControls = function(config) {
1
+ const createControls = function (config) {
2
2
 
3
- let trackCallback = function(){};
3
+ let trackCallback = function () {
4
+ };
4
5
 
5
6
  const controls = {
6
7
  entrance: {
@@ -24,16 +25,17 @@ const createControls = function(config) {
24
25
  rid: document.getElementById("addVideoTrackEncodingRid"),
25
26
  active: document.getElementById("addVideoTrackEncodingActive"),
26
27
  maxBitrate: document.getElementById("addVideoTrackEncodingMaxBitrate"),
27
- resolutionScale: document.getElementById("addVideoTrackEncodingResolutionScale")
28
+ resolutionScale: document.getElementById("addVideoTrackEncodingResolutionScale"),
29
+ scalabilityMode: document.getElementById("addVideoTrackScalabilityMode")
28
30
  },
29
31
  tables: {
30
32
  video: $('#videoTracksTable').DataTable({
31
33
  "sDom": 't',
32
34
  "columns": [
33
35
  {
34
- "className": 'details-control',
35
- "orderable": false,
36
- "data": null,
36
+ "className": 'details-control',
37
+ "orderable": false,
38
+ "data": null,
37
39
  "defaultContent": ''
38
40
  },
39
41
  {"data": "source"},
@@ -58,6 +60,7 @@ const createControls = function(config) {
58
60
  {"data": "active"},
59
61
  {"data": "maxBitrate"},
60
62
  {"data": "resolutionScale"},
63
+ {"data": "scalabilityMode"},
61
64
  {"data": "action"}
62
65
  ]
63
66
  })
@@ -70,7 +73,7 @@ const createControls = function(config) {
70
73
  controls.entrance.roomPin.value = config.room.pin;
71
74
  controls.entrance.nickName.value = config.room.nickName;
72
75
 
73
- const addAudioTrackRow = async function(track) {
76
+ const addAudioTrackRow = async function (track) {
74
77
  const stream = await getMedia([track]);
75
78
  let button = '<button id="' + stream.id + '-button" class="btn btn-primary">Delete</button>';
76
79
  const row = controls.tables.audio.row.add({
@@ -81,14 +84,14 @@ const createControls = function(config) {
81
84
  }).node();
82
85
  controls.tables.audio.draw();
83
86
 
84
- $('#' + stream.id + "-button").on('click', function(){
87
+ $('#' + stream.id + "-button").on('click', function () {
85
88
  //terminate stream
86
89
  console.log("terminate audio stream " + stream.id);
87
90
  let track = stream.getAudioTracks()[0];
88
91
  track.stop();
89
92
  track.dispatchEvent(new Event("ended"));
90
93
  }).prop('disabled', true);
91
- stream.getTracks()[0].onended = function() {
94
+ stream.getTracks()[0].onended = function () {
92
95
  controls.tables.audio.row(row).remove().draw();
93
96
  }
94
97
  trackCallback({
@@ -99,7 +102,7 @@ const createControls = function(config) {
99
102
  });
100
103
  }
101
104
 
102
- const addVideoTrackRow = async function(track) {
105
+ const addVideoTrackRow = async function (track) {
103
106
  const stream = await getMedia([track]);
104
107
  let button = '<button id="' + stream.id + '-button" class="btn btn-primary">Delete</button>';
105
108
  const row = controls.tables.video.row.add({
@@ -113,14 +116,14 @@ const createControls = function(config) {
113
116
  }).node();
114
117
  controls.tables.video.draw();
115
118
 
116
- $('#' + stream.id + "-button").on('click', function(){
119
+ $('#' + stream.id + "-button").on('click', function () {
117
120
  //terminate stream
118
121
  console.log("terminate video stream " + stream.id);
119
122
  let track = stream.getVideoTracks()[0];
120
123
  track.stop();
121
124
  track.dispatchEvent(new Event("ended"));
122
125
  }).prop('disabled', true);
123
- stream.getTracks()[0].addEventListener("ended", function() {
126
+ stream.getTracks()[0].addEventListener("ended", function () {
124
127
  controls.tables.video.row(row).remove().draw();
125
128
  });
126
129
  trackCallback({
@@ -130,40 +133,40 @@ const createControls = function(config) {
130
133
  });
131
134
  }
132
135
 
133
- const format = function(d) {
136
+ const format = function (d) {
134
137
  if (!d.encodings) {
135
138
  return;
136
139
  }
137
- let details = '<table cellpadding="5" cellspacing="0" border="0" style="padding-left:50px;">';
138
- d.encodings.forEach(function(encoding){
140
+ let details = '<table cellpadding="5" cellspacing="0" border="0" style="padding-left:50px;">';
141
+ d.encodings.forEach(function (encoding) {
139
142
  details += '<tr>';
140
143
  for (const [key, value] of Object.entries(encoding)) {
141
- details += '<td>'+ key + '</td>'+
142
- '<td>'+ value + '</td>';
144
+ details += '<td>' + key + '</td>' +
145
+ '<td>' + value + '</td>';
143
146
  }
144
147
  details += '</tr>';
145
148
  });
146
- details +='</table>';
149
+ details += '</table>';
147
150
  return details;
148
151
  }
149
152
 
150
- const muteForm = function(form) {
153
+ const muteForm = function (form) {
151
154
  for (const [key, value] of Object.entries(form)) {
152
155
  value.disabled = true;
153
156
  }
154
157
  }
155
158
 
156
- const unmuteForm = function(form) {
159
+ const unmuteForm = function (form) {
157
160
  for (const [key, value] of Object.entries(form)) {
158
161
  value.disabled = false;
159
162
  }
160
163
  }
161
164
 
162
- const muteInput = function() {
165
+ const muteInput = function () {
163
166
  muteForm(controls.entrance);
164
167
  }
165
168
 
166
- const roomConfig = function() {
169
+ const roomConfig = function () {
167
170
  let roomConfig = {
168
171
  url: controls.entrance.url.value,
169
172
  roomName: controls.entrance.roomName.value,
@@ -179,9 +182,9 @@ const createControls = function(config) {
179
182
  return roomConfig;
180
183
  }
181
184
 
182
- const getVideoStreams = function() {
185
+ const getVideoStreams = function () {
183
186
  let streams = [];
184
- controls.tables.video.rows().every(function(rowIdx, tableLoop, rowLoop) {
187
+ controls.tables.video.rows().every(function (rowIdx, tableLoop, rowLoop) {
185
188
  let data = this.data();
186
189
  streams.push({
187
190
  stream: data.stream,
@@ -192,9 +195,9 @@ const createControls = function(config) {
192
195
  });
193
196
  return streams;
194
197
  }
195
- const getAudioStreams = function() {
198
+ const getAudioStreams = function () {
196
199
  let streams = [];
197
- controls.tables.audio.rows().every(function(rowIdx, tableLoop, rowLoop) {
200
+ controls.tables.audio.rows().every(function (rowIdx, tableLoop, rowLoop) {
198
201
  let data = this.data();
199
202
  streams.push({
200
203
  stream: data.stream,
@@ -205,11 +208,11 @@ const createControls = function(config) {
205
208
  return streams;
206
209
  }
207
210
 
208
- const onTrack = function(callback) {
211
+ const onTrack = function (callback) {
209
212
  trackCallback = callback;
210
213
  }
211
214
 
212
- const displayTables = async function() {
215
+ const displayTables = async function () {
213
216
  // Add event listener for opening and closing details
214
217
  $('#videoTracksTableBody').on('click', 'td.details-control', function () {
215
218
  let tr = $(this).closest('tr');
@@ -234,15 +237,16 @@ const createControls = function(config) {
234
237
  }
235
238
 
236
239
  // Click event listener to add a new video track
237
- document.getElementById("addVideoTrack").addEventListener("click", function(e){
240
+ document.getElementById("addVideoTrack").addEventListener("click", function (e) {
238
241
  let encodings = [];
239
- controls.tables.encodings.rows().every(function() {
242
+ controls.tables.encodings.rows().every(function () {
240
243
  let encoding = this.data();
241
244
  encodings.push({
242
245
  rid: encoding.rid,
243
246
  active: encoding.active,
244
247
  maxBitrate: encoding.maxBitrate,
245
- scaleResolutionDownBy: encoding.resolutionScale
248
+ scaleResolutionDownBy: encoding.resolutionScale,
249
+ scalabilityMode: encoding.scalabilityMode
246
250
  })
247
251
  });
248
252
  let track = {
@@ -254,26 +258,27 @@ const createControls = function(config) {
254
258
  }
255
259
  addVideoTrackRow(track);
256
260
  });
257
-
261
+
258
262
  // Click event listener to remove video quality
259
- $("#videoTrackEncodingsTable").on("click", ".remove", function(){
263
+ $("#videoTrackEncodingsTable").on("click", ".remove", function () {
260
264
  controls.tables.encodings.row($(this).parents('tr')).remove().draw();
261
265
  });
262
-
266
+
263
267
  // Click event listener to add video quality
264
- document.getElementById("addVideoTrackEncoding").addEventListener("click", function(){
268
+ document.getElementById("addVideoTrackEncoding").addEventListener("click", function () {
265
269
  let button = '<button class="btn btn-primary remove">Delete</button>';
266
270
  controls.tables.encodings.row.add({
267
271
  rid: controls.addVideoEncoding.rid.value,
268
272
  active: controls.addVideoEncoding.active.value,
269
273
  maxBitrate: controls.addVideoEncoding.maxBitrate.value,
270
274
  resolutionScale: controls.addVideoEncoding.resolutionScale.value,
275
+ scalabilityMode: controls.addVideoEncoding.scalabilityMode.value,
271
276
  action: button
272
277
  }).draw();
273
278
  });
274
279
 
275
280
  // Click event listener to add a new audio track
276
- document.getElementById("addAudioTrack").addEventListener("click", function(e){
281
+ document.getElementById("addAudioTrack").addEventListener("click", function (e) {
277
282
  let encodings = [];
278
283
  let track = {
279
284
  source: controls.addAudioTrack.source.value,
@@ -282,10 +287,10 @@ const createControls = function(config) {
282
287
  }
283
288
  addAudioTrackRow(track);
284
289
  });
285
-
290
+
286
291
  }
287
292
 
288
- const cleanTables = function() {
293
+ const cleanTables = function () {
289
294
  controls.tables.video.rows().remove().draw();
290
295
  controls.tables.audio.rows().remove().draw();
291
296
  controls.tables.encodings.rows().remove().draw();
@@ -298,15 +303,16 @@ const createControls = function(config) {
298
303
  getAudioStreams: getAudioStreams,
299
304
  getVideoStreams: getVideoStreams,
300
305
  onTrack: onTrack,
301
- cleanTables: cleanTables
306
+ cleanTables: cleanTables,
307
+ controls: controls
302
308
  }
303
309
  }
304
310
 
305
- const getMedia = async function(tracks) {
311
+ const getMedia = async function (tracks) {
306
312
  //convert to constraints
307
313
  let screen = false;
308
- const constraints= {};
309
- tracks.forEach(function(track){
314
+ const constraints = {};
315
+ tracks.forEach(function (track) {
310
316
  if (track.source === "mic") {
311
317
  //audio
312
318
  constraints.audio = {};
@@ -71,6 +71,7 @@
71
71
  <select class="form-select-sm" id="addVideoTrackCodec">
72
72
  <option value="H264" selected>H264</option>
73
73
  <option value="VP8">VP8</option>
74
+ <option value="VP9">VP9</option>
74
75
  </select>
75
76
  </th>
76
77
  <th>
@@ -90,6 +91,7 @@
90
91
  <th>Active</th>
91
92
  <th>MaxBitrate</th>
92
93
  <th>ResolutionScale</th>
94
+ <th>Scalability Mode</th>
93
95
  <th>Action</th>
94
96
  </tr>
95
97
  </thead>
@@ -119,6 +121,11 @@
119
121
  <option value="6">6</option>
120
122
  </select>
121
123
  </th>
124
+ <th>
125
+ <select class="form-select-sm" id="addVideoTrackScalabilityMode">
126
+ <option selected value="">NONE</option>
127
+ </select>
128
+ </th>
122
129
  <th>
123
130
  <button class="btn btn-primary" id="addVideoTrackEncoding">Add</button>
124
131
  </th>
@@ -36,6 +36,37 @@ const defaultConfig = {
36
36
  }
37
37
  };
38
38
 
39
+ const scalabilityModes = [
40
+ 'L1T1',
41
+ 'L1T2',
42
+ 'L1T3',
43
+ 'L2T1',
44
+ 'L2T2',
45
+ 'L2T3',
46
+ 'L3T1',
47
+ 'L3T2',
48
+ 'L3T3',
49
+ 'L2T1h',
50
+ 'L2T2h',
51
+ 'L2T3h',
52
+ 'S2T1',
53
+ 'S2T2',
54
+ 'S2T3',
55
+ 'S2T1h',
56
+ 'S2T2h',
57
+ 'S2T3h',
58
+ 'S3T1',
59
+ 'S3T2',
60
+ 'S3T3',
61
+ 'S3T1h',
62
+ 'S3T2h',
63
+ 'S3T3h',
64
+ 'L2T2_KEY',
65
+ 'L2T3_KEY',
66
+ 'L3T2_KEY',
67
+ 'L3T3_KEY'
68
+ ];
69
+
39
70
  /**
40
71
  * Load track configuration and show entrance modal
41
72
  */
@@ -171,11 +202,51 @@ const publishPreconfiguredStreams = async function (room, pc, streams) {
171
202
  localDisplay.add(s.stream.id, "local", s.stream, contentType);
172
203
  });
173
204
  //join room
174
- await room.join(pc, null, config, 10);
205
+ await room.join(pc, null, config, 1);
175
206
  // Enable Delete button for each preconfigured stream #WCS-3689
176
207
  streams.forEach(function (s) {
177
208
  $('#' + s.stream.id + "-button").prop('disabled', false);
178
209
  });
210
+ cControls.controls.addVideoTrack.codec.addEventListener('change', async (event) => {
211
+ const mimeType = "video/" + event.target.value;
212
+ while (cControls.controls.addVideoEncoding.scalabilityMode.firstChild) {
213
+ cControls.controls.addVideoEncoding.scalabilityMode.firstChild.remove();
214
+ }
215
+ const option = document.createElement('option');
216
+ option.value = '';
217
+ option.innerText = 'NONE';
218
+ cControls.controls.addVideoEncoding.scalabilityMode.appendChild(option);
219
+
220
+ const capabilityPromises = [];
221
+ for (const mode of scalabilityModes) {
222
+ capabilityPromises.push(navigator.mediaCapabilities.encodingInfo({
223
+ type: 'webrtc',
224
+ video: {
225
+ contentType: mimeType,
226
+ width: 640,
227
+ height: 480,
228
+ bitrate: 10000,
229
+ framerate: 29.97,
230
+ scalabilityMode: mode
231
+ }
232
+ }));
233
+ }
234
+ const capabilityResults = await Promise.all(capabilityPromises);
235
+ for (let i = 0; i < scalabilityModes.length; ++i) {
236
+ if (capabilityResults[i].supported) {
237
+ const option = document.createElement('option');
238
+ option.value = scalabilityModes[i];
239
+ option.innerText = scalabilityModes[i];
240
+ cControls.controls.addVideoEncoding.scalabilityMode.appendChild(option);
241
+ }
242
+ }
243
+
244
+ if (cControls.controls.addVideoEncoding.scalabilityMode.childElementCount > 1) {
245
+ cControls.controls.addVideoEncoding.scalabilityMode.disabled = false;
246
+ } else {
247
+ cControls.controls.addVideoEncoding.scalabilityMode.disabled = true;
248
+ }
249
+ });
179
250
  } catch (e) {
180
251
  onOperationFailed("Failed to publish a preconfigured streams", e);
181
252
  // Enable Delete button for each preconfigured stream #WCS-3689
@@ -259,6 +330,13 @@ const subscribeTrackToEndedEvent = function (room, track, pc) {
259
330
  * @param {*} encodings
260
331
  */
261
332
  const addTrackToPeerConnection = function (pc, stream, track, encodings) {
333
+ if (encodings) {
334
+ for (const encoding of encodings) {
335
+ if (encoding.scalabilityMode === "") {
336
+ delete encoding.scalabilityMode;
337
+ }
338
+ }
339
+ }
262
340
  pc.addTransceiver(track, {
263
341
  direction: "sendonly",
264
342
  streams: [stream],
@@ -534,9 +534,9 @@ const createOneToManyParticipantView = function () {
534
534
  removeVideoTrack: function (track) {
535
535
  player.removeVideoTrack(track);
536
536
  },
537
- addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
537
+ addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler, onSidClick, onTidClick) {
538
538
  this.currentTrack = track;
539
- player.setVideoSource(remoteVideoTrack, onResize, muteHandler);
539
+ player.setVideoSource(remoteVideoTrack, onResize, muteHandler, onSidClick, onTidClick);
540
540
  },
541
541
  removeVideoSource: function (track) {
542
542
  if (this.currentTrack && this.currentTrack.mid === track.mid) {
@@ -561,11 +561,11 @@ const createOneToManyParticipantView = function () {
561
561
  const additionalUserId = userId ? "#" + getShortUserId(userId) : "";
562
562
  participantNicknameDisplay.innerText = "Name: " + nickname + additionalUserId;
563
563
  },
564
- updateQuality: function (track, qualityName, available) {
565
- player.updateQuality(qualityName, available);
564
+ updateQuality: function (track, quality) {
565
+ player.updateQuality(quality);
566
566
  },
567
- addQuality: function (track, qualityName, available, onQualityPick) {
568
- player.addQuality(qualityName, available, onQualityPick);
567
+ addQuality: function (track, quality, onQualityPick) {
568
+ player.addQuality(quality, onQualityPick);
569
569
  },
570
570
  clearQualityState: function (track) {
571
571
  player.clearQualityState();
@@ -593,25 +593,54 @@ const createVideoPlayer = function (participantDiv) {
593
593
  const trackDisplay = createContainer(streamDisplay);
594
594
 
595
595
  let videoElement;
596
-
597
- const trackButtons = new Map();
596
+ // traciId/
597
+ // {btn: button,
598
+ // qualities:Map{quality
599
+ // {name:string,
600
+ // available:boolean,
601
+ // btn: button,
602
+ // spatialLayersInfo:Map{
603
+ // available:boolean,
604
+ // resolution{width:number,height:number},
605
+ // sid:number,
606
+ // btn: button},
607
+ // temporalLayersInfo:Map{
608
+ // available:boolean,
609
+ // tid:number,
610
+ // btn: button}
611
+ // }
612
+ // }
613
+
614
+ const tracksInfo = new Map();
598
615
  const qualityButtons = new Map();
599
616
 
600
617
  const lock = function () {
601
- for (const btn of trackButtons.values()) {
618
+ for (const btn of tracksInfo.values()) {
602
619
  btn.disabled = true;
603
620
  }
604
621
  for (const state of qualityButtons.values()) {
605
622
  state.btn.disabled = true;
623
+ for (const [sid, spatialLayerButton] of state.layerButtons.spatialLayerButtons) {
624
+ spatialLayerButton.btn.disabled = true;
625
+ }
626
+ for (const [sid, temporalLayerButton] of state.layerButtons.temporalLayerButtons) {
627
+ temporalLayerButton.btn.disabled = true;
628
+ }
606
629
  }
607
630
  }
608
631
 
609
632
  const unlock = function () {
610
- for (const btn of trackButtons.values()) {
633
+ for (const btn of tracksInfo.values()) {
611
634
  btn.disabled = false;
612
635
  }
613
636
  for (const state of qualityButtons.values()) {
614
637
  state.btn.disabled = false;
638
+ for (const [sid, spatialLayerButton] of state.layerButtons.spatialLayerButtons) {
639
+ spatialLayerButton.btn.disabled = false;
640
+ }
641
+ for (const [sid, temporalLayerButton] of state.layerButtons.temporalLayerButtons) {
642
+ temporalLayerButton.btn.disabled = false;
643
+ }
615
644
  }
616
645
  }
617
646
 
@@ -648,9 +677,24 @@ const createVideoPlayer = function (participantDiv) {
648
677
 
649
678
  const repickQuality = function (qualityName) {
650
679
  for (const [quality, state] of qualityButtons.entries()) {
680
+ state.layerButtons.temporalLayerButtons.forEach((lState, __) => {
681
+ if(lState.btn.style.color === QUALITY_COLORS.SELECTED) {
682
+ lState.btn.style.color = QUALITY_COLORS.AVAILABLE;
683
+ }
684
+ });
685
+ state.layerButtons.spatialLayerButtons.forEach((lState, __) => {
686
+ if(lState.btn.style.color === QUALITY_COLORS.SELECTED) {
687
+ lState.btn.style.color = QUALITY_COLORS.AVAILABLE;
688
+ }
689
+ });
651
690
  if (quality === qualityName) {
691
+ state.layerButtons.temporalLayerButtons.forEach((lState, __) => showItem(lState.btn));
692
+ state.layerButtons.spatialLayerButtons.forEach((lState, __) => showItem(lState.btn));
693
+
652
694
  state.btn.style.color = QUALITY_COLORS.SELECTED;
653
695
  } else if (state.btn.style.color === QUALITY_COLORS.SELECTED) {
696
+ state.layerButtons.temporalLayerButtons.forEach((lState, __) => hideItem(lState.btn));
697
+ state.layerButtons.spatialLayerButtons.forEach((lState, __) => hideItem(lState.btn));
654
698
  if (state.available) {
655
699
  state.btn.style.color = QUALITY_COLORS.AVAILABLE;
656
700
  } else {
@@ -660,22 +704,52 @@ const createVideoPlayer = function (participantDiv) {
660
704
  }
661
705
  }
662
706
 
707
+ const repickSid = function (qualityName, sid) {
708
+ const qualityState = qualityButtons.get(qualityName);
709
+ for (const [__, state] of qualityState.layerButtons.spatialLayerButtons.entries()) {
710
+ if (state.layerInfo.sid === sid) {
711
+ state.btn.style.color = QUALITY_COLORS.SELECTED;
712
+ } else if (state.layerInfo.available) {
713
+ state.btn.style.color = QUALITY_COLORS.AVAILABLE;
714
+ } else {
715
+ state.btn.style.color = QUALITY_COLORS.UNAVAILABLE;
716
+ }
717
+ }
718
+ }
719
+
720
+ const repickTid = function (qualityName, tid) {
721
+ const qualityState = qualityButtons.get(qualityName);
722
+ for (const [__, state] of qualityState.layerButtons.temporalLayerButtons.entries()) {
723
+ if (state.layerInfo.tid === tid) {
724
+ state.btn.style.color = QUALITY_COLORS.SELECTED;
725
+ } else if (state.layerInfo.available) {
726
+ state.btn.style.color = QUALITY_COLORS.AVAILABLE;
727
+ } else {
728
+ state.btn.style.color = QUALITY_COLORS.UNAVAILABLE;
729
+ }
730
+ }
731
+ }
732
+
663
733
  return {
664
734
  rootDiv: streamDisplay,
665
735
  muteButton: null,
666
736
  autoButton: null,
737
+ tidListener: null,
738
+ sidListener: null,
667
739
  dispose: function () {
668
740
  streamDisplay.remove();
669
741
  },
670
742
  clearQualityState: function () {
671
743
  qualityButtons.forEach((state, qName) => {
672
744
  state.btn.remove();
745
+ state.layerButtons.temporalLayerButtons.forEach((lState, __) => lState.btn.remove());
746
+ state.layerButtons.spatialLayerButtons.forEach((lState, __) => lState.btn.remove());
673
747
  });
674
748
  qualityButtons.clear();
675
749
  },
676
750
  addVideoTrack: function (track, asyncCallback) {
677
751
  const trackButton = document.createElement("button");
678
- trackButtons.set(track.mid, trackButton);
752
+ tracksInfo.set(track.mid, trackButton);
679
753
  trackButton.innerText = "Track №" + track.mid + ": " + track.contentType;
680
754
  trackButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
681
755
  trackButton.style.color = QUALITY_COLORS.AVAILABLE;
@@ -696,13 +770,13 @@ const createVideoPlayer = function (participantDiv) {
696
770
  trackDisplay.appendChild(trackButton);
697
771
  },
698
772
  removeVideoTrack: function (track) {
699
- const trackButton = trackButtons.get(track.mid);
773
+ const trackButton = tracksInfo.get(track.mid);
700
774
  if (trackButton) {
701
775
  trackButton.remove();
702
- trackButtons.delete(track.mid);
776
+ tracksInfo.delete(track.mid);
703
777
  }
704
778
  },
705
- setVideoSource: function (remoteVideoTrack, onResize, onMute) {
779
+ setVideoSource: function (remoteVideoTrack, onResize, onMute, onSidClick, onTidClick) {
706
780
  if (!this.muteButton) {
707
781
  const newVideoMuteBtn = document.createElement("button");
708
782
  this.muteButton = newVideoMuteBtn;
@@ -724,6 +798,8 @@ const createVideoPlayer = function (participantDiv) {
724
798
  });
725
799
  videoMuteDisplay.appendChild(newVideoMuteBtn);
726
800
  }
801
+ this.sidListener = onSidClick;
802
+ this.tidListener = onTidClick;
727
803
 
728
804
  if (videoElement) {
729
805
  videoElement.remove();
@@ -778,7 +854,7 @@ const createVideoPlayer = function (participantDiv) {
778
854
  if (videoElement) {
779
855
  showItem(videoElement);
780
856
  }
781
- for (const [mid, btn] of trackButtons.entries()) {
857
+ for (const [mid, btn] of tracksInfo.entries()) {
782
858
  if (mid === track.mid) {
783
859
  btn.style.color = QUALITY_COLORS.SELECTED;
784
860
  } else if (btn.style.color === QUALITY_COLORS.SELECTED) {
@@ -788,39 +864,191 @@ const createVideoPlayer = function (participantDiv) {
788
864
  trackNameDisplay.innerText = "Current video track: " + track.mid;
789
865
  showItem(trackNameDisplay);
790
866
  },
791
- updateQuality: function (qualityName, available) {
792
- const value = qualityButtons.get(qualityName);
793
- if (value) {
794
- const qualityButton = value.btn;
795
- value.available = available;
796
- if (qualityButton.style.color === QUALITY_COLORS.SELECTED) {
797
- return;
798
- }
799
- if (available) {
800
- qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
867
+ updateQuality: function (quality) {
868
+ console.log("updateQuality" + quality.available);
869
+ const qualityInfo = qualityButtons.get(quality.quality);
870
+ if (qualityInfo) {
871
+ const qualityButton = qualityInfo.btn;
872
+ qualityInfo.available = quality.available;
873
+ const isSelectedQuality = qualityButton.style.color === QUALITY_COLORS.SELECTED;
874
+
875
+ if (quality.available) {
876
+ if(!isSelectedQuality) {
877
+ qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
878
+ }
801
879
  } else {
802
880
  qualityButton.style.color = QUALITY_COLORS.UNAVAILABLE;
803
881
  }
882
+ const self = this;
883
+ for (const spatialLayer of quality.layersInfo.spatialLayers) {
884
+ const localLayerInfo = qualityInfo.layerButtons.spatialLayerButtons.get(spatialLayer.sid);
885
+ if (localLayerInfo) {
886
+ if (spatialLayer.available) {
887
+ localLayerInfo.btn.style.color = QUALITY_COLORS.AVAILABLE;
888
+ } else {
889
+ localLayerInfo.btn.style.color = QUALITY_COLORS.UNAVAILABLE;
890
+ }
891
+ localLayerInfo.btn.innerText = "sid-" + spatialLayer.sid + " | " + spatialLayer.resolution.width + "x" + spatialLayer.resolution.height;
892
+ } else {
893
+ const layerButton = document.createElement("button");
894
+ layerButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
895
+ if (!isSelectedQuality) {
896
+ hideItem(layerButton);
897
+ }
898
+ layerButton.innerText = "sid-" + spatialLayer.sid + " | " + spatialLayer.resolution.width + "x" + spatialLayer.resolution.height;
899
+ layerButton.addEventListener('click', async function () {
900
+ console.log("Clicked on sid button " + spatialLayer.sid);
901
+ if (layerButton.style.color === QUALITY_COLORS.SELECTED || layerButton.style.color === QUALITY_COLORS.UNAVAILABLE || !videoElement) {
902
+ return;
903
+ }
904
+ if (self.sidListener) {
905
+ lock();
906
+ self.sidListener(spatialLayer.sid).finally(() => {
907
+ unlock();
908
+ repickSid(quality.quality, spatialLayer.sid);
909
+ });
910
+ }
911
+ });
912
+ if (spatialLayer.available) {
913
+ layerButton.style.color = QUALITY_COLORS.AVAILABLE;
914
+ } else {
915
+ layerButton.style.color = QUALITY_COLORS.UNAVAILABLE;
916
+ }
917
+ qualityInfo.layerButtons.spatialLayerButtons.set(spatialLayer.sid, {
918
+ btn: layerButton,
919
+ layerInfo: spatialLayer
920
+ });
921
+ qualityDisplay.appendChild(layerButton);
922
+ }
923
+ }
924
+
925
+ for (const temporalLayer of quality.layersInfo.temporalLayers) {
926
+ const localLayerInfo = qualityInfo.layerButtons.temporalLayerButtons.get(temporalLayer.tid);
927
+ if (localLayerInfo) {
928
+ if (temporalLayer.available) {
929
+ localLayerInfo.btn.style.color = QUALITY_COLORS.AVAILABLE;
930
+ } else {
931
+ localLayerInfo.btn.style.color = QUALITY_COLORS.UNAVAILABLE;
932
+ }
933
+ localLayerInfo.btn.innerText = "tid-" + temporalLayer.tid;
934
+ } else {
935
+ const layerButton = document.createElement("button");
936
+ layerButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
937
+ if (!isSelectedQuality) {
938
+ hideItem(layerButton);
939
+ }
940
+ layerButton.innerText = "tid-" + temporalLayer.tid;
941
+ layerButton.addEventListener('click', async function () {
942
+ console.log("Clicked on tid button " + temporalLayer.tid);
943
+ if (layerButton.style.color === QUALITY_COLORS.SELECTED || layerButton.style.color === QUALITY_COLORS.UNAVAILABLE || !videoElement) {
944
+ return;
945
+ }
946
+ if (self.tidListener) {
947
+ lock();
948
+ self.tidListener(temporalLayer.tid).finally(() => {
949
+ unlock();
950
+ repickTid(quality.quality, temporalLayer.tid);
951
+ });
952
+ }
953
+ });
954
+ if (temporalLayer.available) {
955
+ layerButton.style.color = QUALITY_COLORS.AVAILABLE;
956
+ } else {
957
+ layerButton.style.color = QUALITY_COLORS.UNAVAILABLE;
958
+ }
959
+ qualityInfo.layerButtons.temporalLayerButtons.set(temporalLayer.tid, {
960
+ btn: layerButton,
961
+ layerInfo: temporalLayer
962
+ })
963
+ qualityDisplay.appendChild(layerButton);
964
+ }
965
+ }
804
966
  }
805
967
  },
806
- addQuality: function (qualityName, available, onPickQuality) {
968
+ addQuality: function (quality, onQualityClick) {
969
+ console.log("addQuality" + quality.available);
970
+
807
971
  const qualityButton = document.createElement("button");
808
- qualityButtons.set(qualityName, {btn: qualityButton, available: available});
809
- qualityButton.innerText = qualityName;
972
+ qualityButton.innerText = quality.quality;
810
973
  qualityButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
811
- if (available) {
974
+ if (quality.available) {
812
975
  qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
813
976
  } else {
814
977
  qualityButton.style.color = QUALITY_COLORS.UNAVAILABLE;
815
978
  }
816
979
  qualityDisplay.appendChild(qualityButton);
980
+ const self = this;
817
981
  qualityButton.addEventListener('click', async function () {
818
- console.log("Clicked on quality button " + qualityName);
982
+ console.log("Clicked on quality button " + quality.quality);
819
983
  if (qualityButton.style.color === QUALITY_COLORS.SELECTED || qualityButton.style.color === QUALITY_COLORS.UNAVAILABLE || !videoElement) {
820
984
  return;
821
985
  }
822
986
  lock();
823
- onPickQuality().finally(() => unlock());
987
+ onQualityClick().finally(() => {
988
+ unlock();
989
+ repickQuality(quality.quality);
990
+ });
991
+ });
992
+
993
+ const spatialLayers = new Map();
994
+ for (const spatialLayer of quality.layersInfo.spatialLayers) {
995
+ const layerButton = document.createElement("button");
996
+ layerButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
997
+ hideItem(layerButton);
998
+ layerButton.innerText = "sid-" + spatialLayer.sid + " | " + spatialLayer.resolution.width + "x" + spatialLayer.resolution.height;
999
+ layerButton.addEventListener('click', async function () {
1000
+ console.log("Clicked on sid button " + spatialLayer.sid);
1001
+ if (layerButton.style.color === QUALITY_COLORS.SELECTED || layerButton.style.color === QUALITY_COLORS.UNAVAILABLE || !videoElement) {
1002
+ return;
1003
+ }
1004
+ if (self.sidListener) {
1005
+ lock();
1006
+ self.sidListener(spatialLayer.sid).finally(() => {
1007
+ unlock();
1008
+ repickSid(quality.quality, spatialLayer.sid);
1009
+ });
1010
+ }
1011
+ });
1012
+ if (spatialLayer.available) {
1013
+ layerButton.style.color = QUALITY_COLORS.AVAILABLE;
1014
+ } else {
1015
+ layerButton.style.color = QUALITY_COLORS.UNAVAILABLE;
1016
+ }
1017
+ spatialLayers.set(spatialLayer.sid, {btn: layerButton, layerInfo: spatialLayer});
1018
+ qualityDisplay.appendChild(layerButton);
1019
+ }
1020
+
1021
+ const temporalLayers = new Map();
1022
+ for (const temporalLayer of quality.layersInfo.temporalLayers) {
1023
+ const layerButton = document.createElement("button");
1024
+ layerButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
1025
+ hideItem(layerButton);
1026
+ layerButton.innerText = "tid-" + temporalLayer.tid;
1027
+ layerButton.addEventListener('click', async function () {
1028
+ console.log("Clicked on tid button " + temporalLayer.tid);
1029
+ if (layerButton.style.color === QUALITY_COLORS.SELECTED || layerButton.style.color === QUALITY_COLORS.UNAVAILABLE || !videoElement) {
1030
+ return;
1031
+ }
1032
+ if (self.tidListener) {
1033
+ lock();
1034
+ self.tidListener(temporalLayer.tid).finally(() => {
1035
+ unlock();
1036
+ repickTid(quality.quality, temporalLayer.tid);
1037
+ });
1038
+ }
1039
+ });
1040
+ if (temporalLayer.available) {
1041
+ layerButton.style.color = QUALITY_COLORS.AVAILABLE;
1042
+ } else {
1043
+ layerButton.style.color = QUALITY_COLORS.UNAVAILABLE;
1044
+ }
1045
+ temporalLayers.set(temporalLayer.tid, {btn: layerButton, layerInfo: temporalLayer})
1046
+ qualityDisplay.appendChild(layerButton);
1047
+ }
1048
+ qualityButtons.set(quality.quality, {
1049
+ btn: qualityButton,
1050
+ available: quality.available,
1051
+ layerButtons: {spatialLayerButtons: spatialLayers, temporalLayerButtons: temporalLayers}
824
1052
  });
825
1053
  },
826
1054
  pickQuality: function (qualityName) {
@@ -930,10 +1158,10 @@ const createOneToOneParticipantView = function () {
930
1158
  player.dispose();
931
1159
  }
932
1160
  },
933
- addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
1161
+ addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler, onSidClick, onTidClick) {
934
1162
  const player = videoPlayers.get(track.mid);
935
1163
  if (player) {
936
- player.setVideoSource(remoteVideoTrack, onResize, muteHandler);
1164
+ player.setVideoSource(remoteVideoTrack, onResize, muteHandler, onSidClick, onTidClick);
937
1165
  }
938
1166
  },
939
1167
  removeVideoSource: function (track) {
@@ -969,10 +1197,10 @@ const createOneToOneParticipantView = function () {
969
1197
  player.updateQuality(qualityName, available);
970
1198
  }
971
1199
  },
972
- addQuality: function (track, qualityName, available, onQualityPick) {
1200
+ addQuality: function (track, quality, onQualityPick) {
973
1201
  const player = videoPlayers.get(track.mid);
974
1202
  if (player) {
975
- player.addQuality(qualityName, available, onQualityPick);
1203
+ player.addQuality(quality, onQualityPick);
976
1204
  }
977
1205
  },
978
1206
  pickQuality: function (track, qualityName) {
@@ -1040,6 +1268,10 @@ const createOneToOneParticipantModel = function (userId, nickname, participantVi
1040
1268
  } else {
1041
1269
  return self.unmuteVideo(track);
1042
1270
  }
1271
+ }, (sid) => {
1272
+ return remoteTrack.setSid(sid)
1273
+ }, (tid) => {
1274
+ return remoteTrack.setTid(tid)
1043
1275
  });
1044
1276
  self.requestVideoTrack(track, remoteTrack).then(() => {
1045
1277
  participantView.showVideoTrack(track);
@@ -1127,6 +1359,23 @@ const createOneToOneParticipantModel = function (userId, nickname, participantVi
1127
1359
  const quality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
1128
1360
  if (quality) {
1129
1361
  quality.available = remoteQualityInfo.available;
1362
+ for(const info of remoteQualityInfo.layersInfo.temporalLayers) {
1363
+ const localTidInfo = quality.layersInfo.temporalLayers.find((t) => t.tid === info.tid);
1364
+ if(localTidInfo) {
1365
+ localTidInfo.available = info.available;
1366
+ } else {
1367
+ quality.layersInfo.temporalLayers.push(info);
1368
+ }
1369
+ }
1370
+ for(const info of remoteQualityInfo.layersInfo.spatialLayers) {
1371
+ const localSidInfo = quality.layersInfo.spatialLayers.find((s) => s.sid === info.sid);
1372
+ if(localSidInfo) {
1373
+ localSidInfo.available = info.available;
1374
+ localSidInfo.resolution = info.resolution;
1375
+ } else {
1376
+ quality.layersInfo.spatialLayers.push(info);
1377
+ }
1378
+ }
1130
1379
  } else {
1131
1380
  track.quality.push(remoteQualityInfo);
1132
1381
  }
@@ -1136,7 +1385,7 @@ const createOneToOneParticipantModel = function (userId, nickname, participantVi
1136
1385
  let abrManager = this.abrManagers.get(track.id);
1137
1386
  if (abrManager && track.quality.length === 0 && remoteTrackQuality.quality.length > 0) {
1138
1387
  const self = this;
1139
- participantView.addQuality(track, "Auto", true, async () => {
1388
+ participantView.addQuality(track, {quality:"Auto",available:true,layersInfo:{spatialLayers:[],temporalLayers:[]}}, async () => {
1140
1389
  const manager = self.abrManagers.get(track.id);
1141
1390
  if (!manager) {
1142
1391
  return;
@@ -1155,6 +1404,23 @@ const createOneToOneParticipantModel = function (userId, nickname, participantVi
1155
1404
  const localQuality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
1156
1405
  if (localQuality) {
1157
1406
  localQuality.available = remoteQualityInfo.available;
1407
+ for(const info of remoteQualityInfo.layersInfo.temporalLayers) {
1408
+ const localTidInfo = localQuality.layersInfo.temporalLayers.find((t) => t.tid === info.tid);
1409
+ if(localTidInfo) {
1410
+ localTidInfo.available = info.available;
1411
+ } else {
1412
+ localQuality.layersInfo.temporalLayers.push(info);
1413
+ }
1414
+ }
1415
+ for(const info of remoteQualityInfo.layersInfo.spatialLayers) {
1416
+ const localSidInfo = localQuality.layersInfo.spatialLayers.find((s) => s.sid === info.sid);
1417
+ if(localSidInfo) {
1418
+ localSidInfo.available = info.available;
1419
+ localSidInfo.resolution = info.resolution;
1420
+ } else {
1421
+ localQuality.layersInfo.spatialLayers.push(info);
1422
+ }
1423
+ }
1158
1424
  if (abrManager) {
1159
1425
  abrManager.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available);
1160
1426
  }
@@ -1169,13 +1435,13 @@ const createOneToOneParticipantModel = function (userId, nickname, participantVi
1169
1435
  }
1170
1436
  if (displayOptions.quality) {
1171
1437
  const self = this;
1172
- participantView.addQuality(track, remoteQualityInfo.quality, remoteQualityInfo.available, async () => {
1438
+ participantView.addQuality(track, remoteQualityInfo, async (sid, tid) => {
1173
1439
  const manager = self.abrManagers.get(track.id);
1174
1440
  if (manager) {
1175
1441
  manager.setManual();
1176
1442
  manager.setQuality(remoteQualityInfo.quality);
1177
1443
  }
1178
- return self.pickQuality(track, remoteQualityInfo.quality);
1444
+ return self.pickQuality(track, remoteQualityInfo.quality, sid, tid);
1179
1445
  });
1180
1446
  }
1181
1447
  }
@@ -1208,7 +1474,7 @@ const createOneToOneParticipantModel = function (userId, nickname, participantVi
1208
1474
  abrManager.setTrack(remoteTrack);
1209
1475
  abrManager.stop();
1210
1476
  if (track.quality.length > 0) {
1211
- participantView.addQuality(track, "Auto", true, async () => {
1477
+ participantView.addQuality(track, {quality:"Auto",available:true, layersInfo:{spatialLayers:[],temporalLayers:[]}}, async () => {
1212
1478
  const manager = self.abrManagers.get(track.id);
1213
1479
  if (!manager) {
1214
1480
  return;
@@ -1230,7 +1496,7 @@ const createOneToOneParticipantModel = function (userId, nickname, participantVi
1230
1496
  abrManager.setQualityAvailable(qualityDescriptor.quality, qualityDescriptor.available);
1231
1497
  }
1232
1498
  if (displayOptions.quality) {
1233
- participantView.addQuality(track, qualityDescriptor.quality, qualityDescriptor.available, async () => {
1499
+ participantView.addQuality(track, qualityDescriptor, async () => {
1234
1500
  const manager = self.abrManagers.get(track.id);
1235
1501
  if (manager) {
1236
1502
  manager.setManual();
@@ -1248,11 +1514,11 @@ const createOneToOneParticipantModel = function (userId, nickname, participantVi
1248
1514
  });
1249
1515
  });
1250
1516
  },
1251
- pickQuality: async function (track, qualityName) {
1517
+ pickQuality: async function (track, qualityName, tid, sid) {
1252
1518
  let remoteVideoTrack = this.remoteVideoTracks.get(track.mid);
1253
1519
  if (remoteVideoTrack) {
1254
- return remoteVideoTrack.setPreferredQuality(qualityName).then(() => {
1255
- participantView.pickQuality(track, qualityName);
1520
+ return remoteVideoTrack.setPreferredQuality(qualityName, sid, tid).then(() => {
1521
+ participantView.pickQuality(track, qualityName, sid, tid);
1256
1522
  });
1257
1523
  }
1258
1524
  },
@@ -1311,6 +1577,10 @@ const createOneToManyParticipantModel = function (userId, nickname, participantV
1311
1577
  } else {
1312
1578
  return model.unmuteVideo(anotherTrack);
1313
1579
  }
1580
+ }, (sid) => {
1581
+ return model.remoteVideoTrack.setSid(sid)
1582
+ }, (tid) => {
1583
+ return model.remoteVideoTrack.setTid(tid)
1314
1584
  });
1315
1585
  model.requestVideoTrack(anotherTrack, model.remoteVideoTrack).then(() => {
1316
1586
  participantView.showVideoTrack(anotherTrack)
@@ -1472,7 +1742,7 @@ const createOneToManyParticipantModel = function (userId, nickname, participantV
1472
1742
 
1473
1743
  },
1474
1744
  setUserId: function (userId) {
1475
- this.userId = userId;
1745
+ this.userId = userId;
1476
1746
  },
1477
1747
  setNickname: function (nickname) {
1478
1748
  this.nickname = nickname;
@@ -1490,15 +1760,32 @@ const createOneToManyParticipantModel = function (userId, nickname, participantV
1490
1760
  const quality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
1491
1761
  if (quality) {
1492
1762
  quality.available = remoteQualityInfo.available;
1763
+ for(const info of remoteQualityInfo.layersInfo.temporalLayers) {
1764
+ const localTidInfo = quality.layersInfo.temporalLayers.find((t) => t.tid === info.tid);
1765
+ if(localTidInfo) {
1766
+ localTidInfo.available = info.available;
1767
+ } else {
1768
+ quality.layersInfo.temporalLayers.push(info);
1769
+ }
1770
+ }
1771
+ for(const info of remoteQualityInfo.layersInfo.spatialLayers) {
1772
+ const localSidInfo = quality.layersInfo.spatialLayers.find((s) => s.sid === info.sid);
1773
+ if(localSidInfo) {
1774
+ localSidInfo.available = info.available;
1775
+ localSidInfo.resolution = info.resolution;
1776
+ } else {
1777
+ quality.layersInfo.spatialLayers.push(info);
1778
+ }
1779
+ }
1493
1780
  } else {
1494
1781
  track.quality.push(remoteQualityInfo);
1495
1782
  }
1496
1783
  }
1497
- return;
1784
+ continue;
1498
1785
  }
1499
1786
  if (this.abr && track.quality.length === 0 && remoteTrackQuality.quality.length > 0) {
1500
1787
  const self = this;
1501
- participantView.addQuality(track, "Auto", true, async () => {
1788
+ participantView.addQuality(track, {quality:"Auto",available:true, layersInfo:{spatialLayers:[],temporalLayers:[]}}, async () => {
1502
1789
  if (!self.abr) {
1503
1790
  return;
1504
1791
  }
@@ -1516,11 +1803,28 @@ const createOneToManyParticipantModel = function (userId, nickname, participantV
1516
1803
  const localQuality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
1517
1804
  if (localQuality) {
1518
1805
  localQuality.available = remoteQualityInfo.available;
1806
+ for(const info of remoteQualityInfo.layersInfo.temporalLayers) {
1807
+ const localTidInfo = localQuality.layersInfo.temporalLayers.find((t) => t.tid === info.tid);
1808
+ if(localTidInfo) {
1809
+ localTidInfo.available = info.available;
1810
+ } else {
1811
+ localQuality.layersInfo.temporalLayers.push(info);
1812
+ }
1813
+ }
1814
+ for(const info of remoteQualityInfo.layersInfo.spatialLayers) {
1815
+ const localSidInfo = localQuality.layersInfo.spatialLayers.find((s) => s.sid === info.sid);
1816
+ if(localSidInfo) {
1817
+ localSidInfo.available = info.available;
1818
+ localSidInfo.resolution = info.resolution;
1819
+ } else {
1820
+ localQuality.layersInfo.spatialLayers.push(info);
1821
+ }
1822
+ }
1519
1823
  if (this.abr) {
1520
1824
  this.abr.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available)
1521
1825
  }
1522
1826
  if (displayOptions.quality) {
1523
- participantView.updateQuality(track, localQuality.quality, localQuality.available);
1827
+ participantView.updateQuality(track, remoteQualityInfo);
1524
1828
  }
1525
1829
  } else {
1526
1830
  track.quality.push(remoteQualityInfo);
@@ -1530,7 +1834,7 @@ const createOneToManyParticipantModel = function (userId, nickname, participantV
1530
1834
  }
1531
1835
  if (displayOptions.quality) {
1532
1836
  const self = this;
1533
- participantView.addQuality(track, remoteQualityInfo.quality, remoteQualityInfo.available, async () => {
1837
+ participantView.addQuality(track, remoteQualityInfo, async () => {
1534
1838
  if (self.abr) {
1535
1839
  self.abr.setManual();
1536
1840
  self.abr.setQuality(remoteQualityInfo.quality);
@@ -1563,7 +1867,7 @@ const createOneToManyParticipantModel = function (userId, nickname, participantV
1563
1867
  self.abr.setTrack(remoteTrack);
1564
1868
 
1565
1869
  if (track.quality.length > 0) {
1566
- participantView.addQuality(track, "Auto", true, async () => {
1870
+ participantView.addQuality(track, {quality:"Auto",available:true,layersInfo:{spatialLayers:[],temporalLayers:[]}}, async () => {
1567
1871
  if (!self.abr) {
1568
1872
  return;
1569
1873
  }
@@ -1584,7 +1888,7 @@ const createOneToManyParticipantModel = function (userId, nickname, participantV
1584
1888
  self.abr.setQualityAvailable(qualityDescriptor.quality, qualityDescriptor.available);
1585
1889
  }
1586
1890
  if (displayOptions.quality) {
1587
- participantView.addQuality(track, qualityDescriptor.quality, qualityDescriptor.available, async () => {
1891
+ participantView.addQuality(track, qualityDescriptor, async () => {
1588
1892
  if (self.abr) {
1589
1893
  self.abr.setManual();
1590
1894
  self.abr.setQuality(qualityDescriptor.quality);
@@ -189,9 +189,11 @@ const statsToTable = function(stats) {
189
189
  pStats.push(trackToTable(track, participant.nickName, "NA"));
190
190
  tracksOut++;
191
191
  bitrateOut += track.bitrate;
192
- firOut += track.feedbackStats.receivedFIR;
193
- pliOut += track.feedbackStats.receivedPLI;
194
- nackOut += track.feedbackStats.receivedNACK;
192
+ if (track.type === 'video' && track.feedbackStats) {
193
+ firOut += track.feedbackStats.receivedFIR;
194
+ pliOut += track.feedbackStats.receivedPLI;
195
+ nackOut += track.feedbackStats.receivedNACK;
196
+ }
195
197
  }
196
198
  });
197
199
  participant.incomingTracks.forEach(function(track) {