@flashphoner/sfusdk-examples 2.0.244 → 2.0.248

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.
@@ -8,11 +8,11 @@ const QUALITY_COLORS = {
8
8
  SELECTED: "blue"
9
9
  };
10
10
 
11
- const initLocalDisplay = function(localDisplayElement){
11
+ const initLocalDisplay = function (localDisplayElement) {
12
12
  const localDisplayDiv = localDisplayElement;
13
13
  const localDisplays = {};
14
14
 
15
- const removeLocalDisplay = function(id) {
15
+ const removeLocalDisplay = function (id) {
16
16
  let localDisplay = document.getElementById(localDisplays[id].id);
17
17
  let video = localDisplay.getElementsByTagName("video");
18
18
  if (video && video[0]) {
@@ -24,7 +24,7 @@ const initLocalDisplay = function(localDisplayElement){
24
24
  localDisplay.remove();
25
25
  }
26
26
 
27
- const getAudioContainer = function() {
27
+ const getAudioContainer = function () {
28
28
  for (const [key, value] of Object.entries(localDisplays)) {
29
29
  let video = value.getElementsByTagName("video");
30
30
  if (video && video[0]) {
@@ -41,24 +41,24 @@ const initLocalDisplay = function(localDisplayElement){
41
41
  }
42
42
  }
43
43
 
44
- const onMuteClick = function(button, stream, type) {
44
+ const onMuteClick = function (button, stream, type) {
45
45
  if (stream.getAudioTracks().length > 0) {
46
46
  stream.getAudioTracks()[0].enabled = !(stream.getAudioTracks()[0].enabled);
47
47
  button.innerHTML = audioStateText(stream) + " " + type;
48
48
  }
49
49
  }
50
50
 
51
- const add = function(id, name, stream, type) {
51
+ const add = function (id, name, stream, type) {
52
52
  if (stream.getAudioTracks().length > 0) {
53
53
  let videoElement = getAudioContainer();
54
54
  if (videoElement) {
55
55
  let track = stream.getAudioTracks()[0];
56
56
  videoElement.video.srcObject.addTrack(track);
57
57
  videoElement.audioStateDisplay.innerHTML = audioStateText(stream) + " " + type;
58
- videoElement.audioStateDisplay.addEventListener("click", function() {
58
+ videoElement.audioStateDisplay.addEventListener("click", function () {
59
59
  onMuteClick(videoElement.audioStateDisplay, stream, type);
60
60
  });
61
- track.addEventListener("ended", function() {
61
+ track.addEventListener("ended", function () {
62
62
  videoElement.video.srcObject.removeTrack(track);
63
63
  videoElement.audioStateDisplay.innerHTML = "No audio";
64
64
  //check video element has no tracks left
@@ -78,13 +78,14 @@ const initLocalDisplay = function(localDisplayElement){
78
78
  const publisherNameDisplay = createInfoDisplay(coreDisplay, name + " " + type);
79
79
 
80
80
  const audioStateDisplay = document.createElement("button");
81
+ audioStateDisplay.innerText = audioStateText();
81
82
  coreDisplay.appendChild(audioStateDisplay);
82
83
 
83
84
  const streamDisplay = createContainer(coreDisplay);
84
85
  streamDisplay.id = "stream-" + id;
85
86
  const video = document.createElement("video");
86
87
  video.muted = true;
87
- if(Browser().isSafariWebRTC()) {
88
+ if (Browser().isSafariWebRTC()) {
88
89
  video.setAttribute("playsinline", "");
89
90
  video.setAttribute("webkit-playsinline", "");
90
91
  }
@@ -93,8 +94,8 @@ const initLocalDisplay = function(localDisplayElement){
93
94
  video.onloadedmetadata = function (e) {
94
95
  video.play();
95
96
  };
96
- stream.getTracks().forEach(function(track){
97
- track.addEventListener("ended", function() {
97
+ stream.getTracks().forEach(function (track) {
98
+ track.addEventListener("ended", function () {
98
99
  video.srcObject.removeTrack(track);
99
100
  //check video element has no tracks left
100
101
  for (const [key, vTrack] of Object.entries(video.srcObject.getTracks())) {
@@ -116,7 +117,7 @@ const initLocalDisplay = function(localDisplayElement){
116
117
  hideItem(streamDisplay);
117
118
  // Set up mute button for audio only stream
118
119
  audioStateDisplay.innerHTML = audioStateText(stream) + " " + type;
119
- audioStateDisplay.addEventListener("click", function() {
120
+ audioStateDisplay.addEventListener("click", function () {
120
121
  onMuteClick(audioStateDisplay, stream, type);
121
122
  });
122
123
  }
@@ -132,7 +133,7 @@ const initLocalDisplay = function(localDisplayElement){
132
133
  }
133
134
 
134
135
  const audioStateText = function (stream) {
135
- if (stream.getAudioTracks().length > 0) {
136
+ if (stream && stream.getAudioTracks().length > 0) {
136
137
  if (stream.getAudioTracks()[0].enabled) {
137
138
  return "Mute";
138
139
  } else {
@@ -148,687 +149,1532 @@ const initLocalDisplay = function(localDisplayElement){
148
149
  }
149
150
  }
150
151
 
151
- const initRemoteDisplay = function(options) {
152
- const constants = SFU.constants;
153
- const remoteParticipants = {};
154
- // Validate options first
155
- if (!options.div) {
156
- throw new Error("Main div to place all the media tag is not defined");
157
- }
158
- if (!options.room) {
159
- throw new Error("Room is not defined");
160
- }
161
- if (!options.peerConnection) {
162
- throw new Error("PeerConnection is not defined");
163
- }
164
-
165
- let mainDiv = options.div;
166
- let room = options.room;
167
- let peerConnection = options.peerConnection;
168
- let displayOptions = options.displayOptions || {publisher: true, quality: true, type: true};
169
-
170
- room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e) {
171
- console.log("Received ADD_TRACKS");
172
- let participant = remoteParticipants[e.info.nickName];
173
- if (!participant) {
174
- participant = {};
175
- participant.nickName = e.info.nickName;
176
- participant.tracks = [];
177
- participant.displays = [];
178
- remoteParticipants[participant.nickName] = participant;
179
- }
180
- participant.tracks.push.apply(participant.tracks, e.info.info);
181
- for (const pTrack of e.info.info) {
182
- let createDisplay = true;
183
- for (let i = 0; i < participant.displays.length; i++) {
184
- let display = participant.displays[i];
185
- if (pTrack.type === "VIDEO") {
186
- if (display.hasVideo()) {
187
- continue;
188
- }
189
- display.videoMid = pTrack.mid;
190
- display.setTrackInfo(pTrack);
191
- createDisplay = false;
192
- break;
193
- } else if (pTrack.type === "AUDIO") {
194
- if (display.hasAudio()) {
195
- continue;
196
- }
197
- display.audioMid = pTrack.mid;
198
- display.setTrackInfo(pTrack);
199
- createDisplay = false;
200
- break;
201
- }
202
- }
203
- if (!createDisplay) {
204
- continue;
205
- }
206
- let display = createRemoteDisplay(participant.nickName, participant.nickName, mainDiv, displayOptions);
207
- participant.displays.push(display);
208
- if (pTrack.type === "VIDEO") {
209
- display.videoMid = pTrack.mid;
210
- display.setTrackInfo(pTrack);
211
- } else if (pTrack.type === "AUDIO") {
212
- display.audioMid = pTrack.mid;
213
- display.setTrackInfo(pTrack);
214
- }
215
- }
216
- }).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, function(e) {
217
- console.log("Received REMOVE_TRACKS");
218
- const participant = remoteParticipants[e.info.nickName];
219
- if (!participant) {
220
- return;
152
+ const abrManagerFactory = function (room, abrOptions) {
153
+ return {
154
+ createAbrManager: function () {
155
+ let abr = {
156
+ track: null,
157
+ interval: abrOptions.interval,
158
+ thresholds: abrOptions.thresholds,
159
+ qualities: [],
160
+ currentQualityName: null,
161
+ statTimer: null,
162
+ paused: false,
163
+ manual: false,
164
+ keepGoodTimeout: abrOptions.abrKeepOnGoodQuality,
165
+ keepGoodTimer: null,
166
+ tryUpperTimeout: abrOptions.abrTryForUpperQuality,
167
+ tryUpperTimer: null,
168
+ start: function () {
169
+ this.stop();
170
+ console.log("Start abr interval")
171
+ if (abr.interval) {
172
+ const thresholds = Thresholds();
173
+ for (const threshold of abr.thresholds) {
174
+ thresholds.add(threshold.parameter, threshold.maxLeap);
175
+ }
176
+ abr.statsTimer = setInterval(() => {
177
+ if (abr.track) {
178
+ room.getStats(abr.track.track, constants.SFU_RTC_STATS_TYPE.INBOUND, (stats) => {
179
+ if (thresholds.isReached(stats)) {
180
+ abr.shiftDown();
181
+ } else {
182
+ abr.useGoodQuality();
183
+ }
184
+ });
185
+ }
186
+ }, abr.interval);
187
+ }
188
+ },
189
+ stop: function () {
190
+ console.log("Stop abr interval")
191
+ abr.stopKeeping();
192
+ abr.stopTrying();
193
+ if (abr.statsTimer) {
194
+ clearInterval(abr.statsTimer);
195
+ abr.statsTimer = null;
196
+ }
197
+ },
198
+ isEnabled: function () {
199
+ return (abr.interval > 0);
200
+ },
201
+ pause: function () {
202
+ abr.paused = true;
203
+ },
204
+ resume: function () {
205
+ abr.paused = false;
206
+ },
207
+ setAuto: function () {
208
+ abr.manual = false;
209
+ abr.resume();
210
+ },
211
+ setManual: function () {
212
+ abr.manual = true;
213
+ abr.pause();
214
+ },
215
+ isAuto: function () {
216
+ return !abr.manual;
217
+ },
218
+ setTrack: function (track) {
219
+ abr.track = track;
220
+ },
221
+ setQualitiesList: function (qualities) {
222
+ abr.qualities = qualities;
223
+ },
224
+ clearQualityState: function () {
225
+ abr.qualities = [];
226
+ abr.currentQualityName = null;
227
+ },
228
+ addQuality: function (name) {
229
+ abr.qualities.push({name: name, available: false, good: true});
230
+ },
231
+ setQualityAvailable: function (name, available) {
232
+ for (let i = 0; i < abr.qualities.length; i++) {
233
+ if (name === abr.qualities[i].name) {
234
+ abr.qualities[i].available = available;
235
+ }
236
+ }
237
+ },
238
+ setQualityGood: function (name, good) {
239
+ if (name) {
240
+ for (let i = 0; i < abr.qualities.length; i++) {
241
+ if (name === abr.qualities[i].name) {
242
+ abr.qualities[i].good = good;
243
+ }
244
+ }
245
+ }
246
+ },
247
+ getFirstAvailableQuality: function () {
248
+ for (let i = 0; i < abr.qualities.length; i++) {
249
+ if (abr.qualities[i].available) {
250
+ return abr.qualities[i];
251
+ }
252
+ }
253
+ return null;
254
+ },
255
+ getLowerQuality: function (name) {
256
+ let quality = null;
257
+ if (!name) {
258
+ // There were no switching yet, return a first available quality
259
+ return abr.getFirstAvailableQuality();
260
+ }
261
+ let currentIndex = abr.qualities.map(item => item.name).indexOf(name);
262
+ for (let i = 0; i < currentIndex; i++) {
263
+ if (abr.qualities[i].available) {
264
+ quality = abr.qualities[i];
265
+ }
266
+ }
267
+ return quality;
268
+ },
269
+ getUpperQuality: function (name) {
270
+ let quality = null;
271
+ if (!name) {
272
+ // There were no switching yet, return a first available quality
273
+ return abr.getFirstAvailableQuality();
274
+ }
275
+ let currentIndex = abr.qualities.map(item => item.name).indexOf(name);
276
+ for (let i = currentIndex + 1; i < abr.qualities.length; i++) {
277
+ if (abr.qualities[i].available) {
278
+ quality = abr.qualities[i];
279
+ break;
280
+ }
281
+ }
282
+ return quality;
283
+ },
284
+ shiftDown: function () {
285
+ if (!abr.manual && !abr.paused) {
286
+ abr.stopKeeping();
287
+ abr.setQualityGood(abr.currentQualityName, false);
288
+ let quality = abr.getLowerQuality(abr.currentQualityName);
289
+ if (quality) {
290
+ console.log("Switching down to " + quality.name + " quality");
291
+ abr.setQuality(quality.name);
292
+ }
293
+ }
294
+ },
295
+ shiftUp: function () {
296
+ if (!abr.manual && !abr.paused) {
297
+ let quality = abr.getUpperQuality(abr.currentQualityName);
298
+ if (quality) {
299
+ if (quality.good) {
300
+ console.log("Switching up to " + quality.name + " quality");
301
+ abr.setQuality(quality.name);
302
+ } else {
303
+ abr.tryUpper();
304
+ }
305
+ }
306
+ }
307
+ },
308
+ useGoodQuality: function () {
309
+ if (!abr.manual && !abr.paused) {
310
+ if (!abr.currentQualityName) {
311
+ let quality = abr.getFirstAvailableQuality();
312
+ abr.currentQualityName = quality.name;
313
+ }
314
+ abr.setQualityGood(abr.currentQualityName, true);
315
+ abr.keepGoodQuality();
316
+ }
317
+ },
318
+ keepGoodQuality: function () {
319
+ if (abr.keepGoodTimeout && !abr.keepGoodTimer && abr.getUpperQuality(abr.currentQualityName)) {
320
+ console.log("start keepGoodTimer");
321
+ abr.keepGoodTimer = setTimeout(() => {
322
+ abr.shiftUp();
323
+ abr.stopKeeping();
324
+ }, abr.keepGoodTimeout);
325
+ }
326
+ },
327
+ stopKeeping: function () {
328
+ if (abr.keepGoodTimer) {
329
+ clearTimeout(abr.keepGoodTimer);
330
+ abr.keepGoodTimer = null;
331
+ }
332
+ },
333
+ tryUpper: function () {
334
+ let quality = abr.getUpperQuality(abr.currentQualityName);
335
+ if (abr.tryUpperTimeout && !abr.tryUpperTimer && quality) {
336
+ abr.tryUpperTimer = setTimeout(() => {
337
+ abr.setQualityGood(quality.name, true);
338
+ abr.stopTrying();
339
+ }, abr.tryUpperTimeout);
340
+ }
341
+ },
342
+ stopTrying: function () {
343
+ if (abr.tryUpperTimer) {
344
+ clearTimeout(abr.tryUpperTimer);
345
+ abr.tryUpperTimer = null;
346
+ }
347
+ },
348
+ setQuality: async function (name) {
349
+ console.log("set quality name");
350
+ // Pause switching until a new quality is received
351
+ abr.pause();
352
+ abr.currentQualityName = name;
353
+ abr.track.setPreferredQuality(abr.currentQualityName);
354
+ }
355
+ }
356
+ return abr;
221
357
  }
222
- for (const rTrack of e.info.info) {
223
- for (let i = 0; i < participant.tracks.length; i++) {
224
- if (rTrack.mid === participant.tracks[i].mid) {
225
- participant.tracks.splice(i, 1);
226
- break;
358
+ }
359
+ }
360
+
361
+
362
+ const createDefaultMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
363
+ return {
364
+ participants: new Map(),
365
+ meetingName: null,
366
+ addParticipant: function (userId, participantName) {
367
+ if (this.participants.get(userId)) {
368
+ return;
369
+ }
370
+ const [participantModel, participantView, participant] = participantFactory.createParticipant(userId, participantName, displayOptions, abrFactory);
371
+ this.participants.set(userId, participant);
372
+ meetingView.addParticipant(userId, participantName, participantView.rootDiv);
373
+ },
374
+ removeParticipant: function (userId) {
375
+ const participant = this.participants.get(userId);
376
+ if (participant) {
377
+ this.participants.delete(userId);
378
+ meetingView.removeParticipant(userId);
379
+ participant.dispose();
380
+ }
381
+ },
382
+ renameParticipant: function (userId, newNickname) {
383
+ const participant = this.participants.get(userId);
384
+ if (participant) {
385
+ participant.setNickname(newNickname);
386
+ }
387
+ },
388
+ addTracks: function (userId, tracks) {
389
+ const participant = this.participants.get(userId);
390
+ if (!participant) {
391
+ return;
392
+ }
393
+
394
+ for (const track of tracks) {
395
+ if (track.type === "VIDEO") {
396
+ participant.addVideoTrack(track);
397
+ } else if (track.type === "AUDIO") {
398
+ participant.addAudioTrack(track);
227
399
  }
228
400
  }
229
- for (let i = 0; i < participant.displays.length; i++) {
230
- let found = false;
231
- const display = participant.displays[i];
232
- if (display.audioMid === rTrack.mid) {
233
- display.setAudio(null);
234
- found = true;
235
- } else if (display.videoMid === rTrack.mid) {
236
- display.setVideo(null);
237
- found = true;
401
+ },
402
+ removeTracks: function (userId, tracks) {
403
+ const participant = this.participants.get(userId);
404
+ if (!participant) {
405
+ return;
406
+ }
407
+ for (const track of tracks) {
408
+ if (track.type === "VIDEO") {
409
+ participant.removeVideoTrack(track);
410
+ } else if (track.type === "AUDIO") {
411
+ participant.removeAudioTrack(track);
238
412
  }
239
- if (found) {
240
- if (!display.hasAudio() && !display.hasVideo()) {
241
- display.dispose();
242
- participant.displays.splice(i, 1);
413
+ }
414
+ },
415
+ updateQualityInfo: function (userId, tracksInfo) {
416
+ const participant = this.participants.get(userId);
417
+ if (!participant) {
418
+ return;
419
+ }
420
+ participant.updateQualityInfo(tracksInfo);
421
+ },
422
+ end: function () {
423
+ console.log("Meeting " + this.meetingName + " ended")
424
+ meetingView.end();
425
+ this.participants.forEach((participant, id) => {
426
+ participant.dispose();
427
+ });
428
+ this.participants.clear();
429
+ },
430
+ setMeetingName: function (id) {
431
+ this.meetingName = id;
432
+ meetingView.setMeetingName(id);
433
+ }
434
+ }
435
+ }
436
+
437
+ const createDefaultMeetingView = function (entryPoint) {
438
+ const rootDiv = document.createElement("div");
439
+ rootDiv.setAttribute("class", "grid-item");
440
+ entryPoint.appendChild(rootDiv);
441
+ const title = document.createElement("label");
442
+ title.setAttribute("style", "display:block; border: solid; border-width: 1px");
443
+ rootDiv.appendChild(title);
444
+ return {
445
+ participantViews: new Map(),
446
+ setMeetingName: function (id) {
447
+ title.innerText = "Meeting: " + id;
448
+ },
449
+ addParticipant: function (userId, participantName, cell) {
450
+ const participantDiv = createContainer(rootDiv);
451
+ participantDiv.appendChild(cell);
452
+ this.participantViews.set(userId, participantDiv);
453
+ },
454
+ removeParticipant: function (userId) {
455
+ const cell = this.participantViews.get(userId);
456
+ if (cell) {
457
+ this.participantViews.delete(userId);
458
+ cell.remove();
459
+ }
460
+ },
461
+ end: function () {
462
+ rootDiv.remove();
463
+ }
464
+ }
465
+ }
466
+ const oneToOneParticipantFactory = function (remoteTrackFactory) {
467
+ return createParticipantFactory(remoteTrackFactory, createOneToOneParticipantView, createOneToOneParticipantModel);
468
+ }
469
+ const createParticipantFactory = function (remoteTrackFactory, createParticipantView, createParticipantModel) {
470
+ return {
471
+ displayOptions: null,
472
+ abrFactory: null,
473
+ createParticipant: function (userId, nickname) {
474
+ const view = createParticipantView();
475
+ const model = createParticipantModel(userId, nickname, view, remoteTrackFactory, this.abrFactory, this.displayOptions);
476
+ const controller = createParticipantController(model);
477
+ return [model, view, controller];
478
+ }
479
+ }
480
+ }
481
+
482
+ const createParticipantController = function (model) {
483
+ return {
484
+ addVideoTrack: function (track) {
485
+ model.addVideoTrack(track);
486
+ },
487
+ removeVideoTrack: function (track) {
488
+ model.removeVideoTrack(track);
489
+ },
490
+ addAudioTrack: function (track) {
491
+ model.addAudioTrack(track);
492
+ },
493
+ removeAudioTrack: function (track) {
494
+ model.removeAudioTrack(track);
495
+ },
496
+ updateQualityInfo: function (qualityInfo) {
497
+ model.updateQualityInfo(qualityInfo);
498
+ },
499
+ setNickname: function (nickname) {
500
+ model.setNickname(nickname);
501
+ },
502
+ dispose: function () {
503
+ model.dispose();
504
+ }
505
+ }
506
+ }
507
+
508
+ const createOneToManyParticipantView = function () {
509
+
510
+ const participantDiv = createContainer(null);
511
+
512
+ const audioDisplay = createContainer(participantDiv);
513
+
514
+ const participantNicknameDisplay = createInfoDisplay(participantDiv, "Name: ")
515
+
516
+ const audioElements = new Map();
517
+ const player = createVideoPlayer(participantDiv);
518
+
519
+ return {
520
+ rootDiv: participantDiv,
521
+ currentTrack: null,
522
+ dispose: function () {
523
+ player.dispose();
524
+ for (const element of audioElements.values()) {
525
+ element.remove();
526
+ }
527
+ audioElements.clear();
528
+ },
529
+ addVideoTrack: function (track, requestVideoTrack) {
530
+ player.addVideoTrack(track, async () => {
531
+ return requestVideoTrack();
532
+ });
533
+ },
534
+ removeVideoTrack: function (track) {
535
+ player.removeVideoTrack(track);
536
+ },
537
+ addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
538
+ this.currentTrack = track;
539
+ player.setVideoSource(remoteVideoTrack, onResize, muteHandler);
540
+ },
541
+ removeVideoSource: function (track) {
542
+ if (this.currentTrack && this.currentTrack.mid === track.mid) {
543
+ player.removeVideoSource();
544
+ }
545
+ },
546
+ showVideoTrack: function (track) {
547
+ player.showVideoTrack(track);
548
+ },
549
+ addAudioTrack: function (track, audioTrack, show) {
550
+ const stream = new MediaStream();
551
+ stream.addTrack(audioTrack);
552
+ const audioElement = document.createElement("audio");
553
+ if (!show) {
554
+ hideItem(audioElement);
555
+ }
556
+ audioElement.controls = "controls";
557
+ audioElement.muted = true;
558
+ audioElement.autoplay = true;
559
+ audioElement.onloadedmetadata = function (e) {
560
+ audioElement.play().then(function () {
561
+ if (Browser().isSafariWebRTC() && Browser().isiOS()) {
562
+ console.warn("Audio track should be manually unmuted in iOS Safari");
563
+ } else {
564
+ audioElement.muted = false;
243
565
  }
244
- break;
245
- }
566
+ });
567
+ };
568
+ audioElements.set(track.mid, audioElement);
569
+ audioDisplay.appendChild(audioElement);
570
+ audioElement.srcObject = stream;
571
+ },
572
+ removeAudioTrack: function (track) {
573
+ const audioElement = audioElements.get(track.mid);
574
+ if (audioElement) {
575
+ audioElement.remove();
576
+ audioElements.delete(track.mid);
246
577
  }
578
+ },
579
+ setNickname: function (userId, nickname) {
580
+ const additionalUserId = userId ? "#" + getShortUserId(userId) : "";
581
+ participantNicknameDisplay.innerText = "Name: " + nickname + additionalUserId;
582
+ },
583
+ updateQuality: function (track, qualityName, available) {
584
+ player.updateQuality(qualityName, available);
585
+ },
586
+ addQuality: function (track, qualityName, available, onQualityPick) {
587
+ player.addQuality(qualityName, available, onQualityPick);
588
+ },
589
+ clearQualityState: function (track) {
590
+ player.clearQualityState();
591
+ },
592
+ pickQuality: function (track, qualityName) {
593
+ player.pickQuality(qualityName);
247
594
  }
248
- }).on(constants.SFU_ROOM_EVENT.LEFT, function(e) {
249
- console.log("Received LEFT");
250
- let participant = remoteParticipants[e.name];
251
- if (!participant) {
252
- return;
595
+ }
596
+ }
597
+
598
+ const createVideoPlayer = function (participantDiv) {
599
+
600
+ const streamDisplay = createContainer(participantDiv);
601
+
602
+ const resolutionLabel = createInfoDisplay(streamDisplay, "0x0");
603
+ hideItem(resolutionLabel);
604
+
605
+ const trackNameDisplay = createInfoDisplay(streamDisplay, "track not set");
606
+ hideItem(trackNameDisplay);
607
+
608
+ const videoMuteDisplay = createContainer(streamDisplay);
609
+
610
+ const qualityDisplay = createContainer(streamDisplay);
611
+
612
+ const trackDisplay = createContainer(streamDisplay);
613
+
614
+ let videoElement;
615
+
616
+ const trackButtons = new Map();
617
+ const qualityButtons = new Map();
618
+
619
+ const lock = function () {
620
+ for (const btn of trackButtons.values()) {
621
+ btn.disabled = true;
253
622
  }
254
- participant.displays.forEach(function(display){
255
- display.dispose();
256
- })
257
- delete remoteParticipants[e.name];
258
- }).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATE, function(e){
259
- console.log("Received track quality state");
260
- const participant = remoteParticipants[e.info.nickName];
261
- if (!participant) {
262
- return;
623
+ for (const state of qualityButtons.values()) {
624
+ state.btn.disabled = true;
625
+ }
626
+ }
627
+
628
+ const unlock = function () {
629
+ for (const btn of trackButtons.values()) {
630
+ btn.disabled = false;
631
+ }
632
+ for (const state of qualityButtons.values()) {
633
+ state.btn.disabled = false;
263
634
  }
635
+ }
636
+
637
+ const setWebkitEventHandlers = function (video) {
638
+ let needRestart = false;
639
+ let isFullscreen = false;
640
+ // Use webkitbeginfullscreen event to detect full screen mode in iOS Safari
641
+ video.addEventListener("webkitbeginfullscreen", function () {
642
+ isFullscreen = true;
643
+ });
644
+ video.addEventListener("pause", function () {
645
+ if (needRestart) {
646
+ console.log("Media paused after fullscreen, continue...");
647
+ video.play();
648
+ needRestart = false;
649
+ } else {
650
+ console.log("Media paused by click, continue...");
651
+ video.play();
652
+ }
653
+ });
654
+ video.addEventListener("webkitendfullscreen", function () {
655
+ video.play();
656
+ needRestart = true;
657
+ isFullscreen = false;
658
+ });
659
+ }
660
+ const setEventHandlers = function (video) {
661
+ // Ignore play/pause button
662
+ video.addEventListener("pause", function () {
663
+ console.log("Media paused by click, continue...");
664
+ video.play();
665
+ });
666
+ }
264
667
 
265
- for (const rTrack of e.info.tracks) {
266
- const mid = rTrack.mid;
267
- for (let i = 0; i < participant.displays.length; i++) {
268
- const display = participant.displays[i];
269
- if (display.videoMid === mid) {
270
- display.updateQualityInfo(rTrack.quality);
271
- break;
668
+ const repickQuality = function (qualityName) {
669
+ for (const [quality, state] of qualityButtons.entries()) {
670
+ if (quality === qualityName) {
671
+ state.btn.style.color = QUALITY_COLORS.SELECTED;
672
+ } else if (state.btn.style.color === QUALITY_COLORS.SELECTED) {
673
+ if (state.available) {
674
+ state.btn.style.color = QUALITY_COLORS.AVAILABLE;
675
+ } else {
676
+ state.btn.style.color = QUALITY_COLORS.UNAVAILABLE;
272
677
  }
273
678
  }
274
679
  }
275
- });
680
+ }
276
681
 
277
- const createRemoteDisplay = function(id, name, mainDiv, displayOptions) {
278
- const cell = document.createElement("div");
279
- cell.setAttribute("class", "text-center");
280
- cell.id = id;
281
- mainDiv.appendChild(cell);
282
- let publisherNameDisplay;
283
- let currentQualityDisplay;
284
- let videoTypeDisplay;
285
- let abrQualityCheckPeriod = ABR_QUALITY_CHECK_PERIOD;
286
- let abrKeepOnGoodQuality = ABR_KEEP_ON_QUALITY;
287
- let abrTryForUpperQuality = ABR_TRY_UPPER_QUALITY;
288
- if (displayOptions.abrQualityCheckPeriod !== undefined) {
289
- abrQualityCheckPeriod = displayOptions.abrQualityCheckPeriod;
290
- }
291
- if (displayOptions.abrKeepOnGoodQuality !== undefined) {
292
- abrKeepOnGoodQuality = displayOptions.abrKeepOnGoodQuality;
293
- }
294
- if (displayOptions.abrTryForUpperQuality !== undefined) {
295
- abrTryForUpperQuality = displayOptions.abrTryForUpperQuality;
296
- }
297
- if (!displayOptions.abr) {
298
- abrQualityCheckPeriod = 0;
299
- abrKeepOnGoodQuality = 0;
300
- abrTryForUpperQuality = 0;
301
- }
302
- if (displayOptions.publisher) {
303
- publisherNameDisplay = createInfoDisplay(cell, "Published by: " + name);
304
- }
305
- if (displayOptions.quality) {
306
- currentQualityDisplay = createInfoDisplay(cell, "");
307
- }
308
- if (displayOptions.type) {
309
- videoTypeDisplay = createInfoDisplay(cell, "");
310
- }
311
- const qualitySwitchDisplay = createInfoDisplay(cell, "");
312
-
313
- let qualityDivs = [];
314
- let contentType = "";
315
-
316
- const rootDisplay = createContainer(cell);
317
- const streamDisplay = createContainer(rootDisplay);
318
- const audioDisplay = createContainer(rootDisplay);
319
- const audioTypeDisplay = createInfoDisplay(audioDisplay);
320
- const audioTrackDisplay = createContainer(audioDisplay);
321
- const audioStateButton = AudioStateButton();
322
-
323
- hideItem(streamDisplay);
324
- hideItem(audioDisplay);
325
- hideItem(publisherNameDisplay);
326
- hideItem(currentQualityDisplay);
327
- hideItem(videoTypeDisplay);
328
- hideItem(qualitySwitchDisplay);
329
-
330
- let audio = null;
331
- let video = null;
332
-
333
- const abr = ABR(abrQualityCheckPeriod, [
334
- {parameter: "nackCount", maxLeap: 10},
335
- {parameter: "freezeCount", maxLeap: 10},
336
- {parameter: "packetsLost", maxLeap: 10}
337
- ], abrKeepOnGoodQuality, abrTryForUpperQuality);
338
-
339
- return {
340
- dispose: function() {
341
- abr.stop();
342
- cell.remove();
343
- },
344
- hide: function(value) {
345
- if (value) {
346
- cell.style.display = "none";
347
- } else {
348
- cell.style.display = "block";
682
+ return {
683
+ rootDiv: streamDisplay,
684
+ muteButton: null,
685
+ autoButton: null,
686
+ dispose: function () {
687
+ streamDisplay.remove();
688
+ },
689
+ clearQualityState: function () {
690
+ qualityButtons.forEach((state, qName) => {
691
+ state.btn.remove();
692
+ });
693
+ qualityButtons.clear();
694
+ },
695
+ addVideoTrack: function (track, asyncCallback) {
696
+ const trackButton = document.createElement("button");
697
+ trackButtons.set(track.mid, trackButton);
698
+ trackButton.innerText = "Track №" + track.mid + ": " + track.contentType;
699
+ trackButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
700
+ trackButton.style.color = QUALITY_COLORS.AVAILABLE;
701
+ const self = this;
702
+ trackButton.addEventListener('click', async function () {
703
+ console.log("Clicked on track button track.mid " + track.mid);
704
+ if (trackButton.style.color === QUALITY_COLORS.SELECTED) {
705
+ return
706
+ }
707
+
708
+ lock();
709
+ asyncCallback().then(() => {
710
+ self.showVideoTrack(track);
711
+ }).finally(() => {
712
+ unlock();
713
+ });
714
+ });
715
+ trackDisplay.appendChild(trackButton);
716
+ },
717
+ removeVideoTrack: function (track) {
718
+ const trackButton = trackButtons.get(track.mid);
719
+ if (trackButton) {
720
+ trackButton.remove();
721
+ trackButtons.delete(track.mid);
722
+ }
723
+ },
724
+ setVideoSource: function (remoteVideoTrack, onResize, onMute) {
725
+ if (!this.muteButton) {
726
+ const newVideoMuteBtn = document.createElement("button");
727
+ this.muteButton = newVideoMuteBtn;
728
+ newVideoMuteBtn.innerText = "mute";
729
+ newVideoMuteBtn.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
730
+ newVideoMuteBtn.addEventListener('click', async function () {
731
+ newVideoMuteBtn.disabled = true;
732
+ try {
733
+ if (newVideoMuteBtn.innerText === "mute") {
734
+ await onMute(true);
735
+ newVideoMuteBtn.innerText = "unmute";
736
+ } else if (newVideoMuteBtn.innerText === "unmute") {
737
+ await onMute(false);
738
+ newVideoMuteBtn.innerText = "mute";
739
+ }
740
+ } finally {
741
+ newVideoMuteBtn.disabled = false;
742
+ }
743
+ });
744
+ videoMuteDisplay.appendChild(newVideoMuteBtn);
745
+ }
746
+
747
+ if (videoElement) {
748
+ videoElement.remove();
749
+ videoElement = null;
750
+ }
751
+
752
+ if (!remoteVideoTrack) {
753
+ return;
754
+ }
755
+
756
+ videoElement = document.createElement("video");
757
+ hideItem(videoElement);
758
+ videoElement.setAttribute("style", "display:none; border: solid; border-width: 1px");
759
+
760
+ const stream = new MediaStream();
761
+
762
+ streamDisplay.appendChild(videoElement);
763
+ videoElement.srcObject = stream;
764
+ videoElement.onloadedmetadata = function (e) {
765
+ videoElement.play();
766
+ };
767
+ videoElement.addEventListener("resize", function (event) {
768
+ showItem(resolutionLabel);
769
+ if (videoElement) {
770
+ resolutionLabel.innerText = videoElement.videoWidth + "x" + videoElement.videoHeight;
771
+ resizeVideo(event.target);
772
+ onResize();
349
773
  }
350
- },
351
- setAudio: function(stream) {
352
- if (audio) {
353
- audio.remove();
774
+ });
775
+ stream.addTrack(remoteVideoTrack);
776
+ if (Browser().isSafariWebRTC()) {
777
+ videoElement.setAttribute("playsinline", "");
778
+ videoElement.setAttribute("webkit-playsinline", "");
779
+ setWebkitEventHandlers(videoElement);
780
+ } else {
781
+ setEventHandlers(videoElement);
782
+ }
783
+ },
784
+ removeVideoSource: function () {
785
+ if (videoElement) {
786
+ videoElement.remove();
787
+ videoElement = null;
788
+ }
789
+ if (this.muteButton) {
790
+ this.muteButton.remove();
791
+ this.muteButton = null;
792
+ }
793
+ hideItem(resolutionLabel);
794
+ trackNameDisplay.innerText = "track not set";
795
+ },
796
+ showVideoTrack: function (track) {
797
+ if (videoElement) {
798
+ showItem(videoElement);
799
+ }
800
+ for (const [mid, btn] of trackButtons.entries()) {
801
+ if (mid === track.mid) {
802
+ btn.style.color = QUALITY_COLORS.SELECTED;
803
+ } else if (btn.style.color === QUALITY_COLORS.SELECTED) {
804
+ btn.style.color = QUALITY_COLORS.AVAILABLE;
354
805
  }
355
- if (!stream) {
356
- audio = null;
357
- this.audioMid = undefined;
806
+ }
807
+ trackNameDisplay.innerText = "Current video track: " + track.mid;
808
+ showItem(trackNameDisplay);
809
+ },
810
+ updateQuality: function (qualityName, available) {
811
+ const value = qualityButtons.get(qualityName);
812
+ if (value) {
813
+ const qualityButton = value.btn;
814
+ value.available = available;
815
+ if (qualityButton.style.color === QUALITY_COLORS.SELECTED) {
358
816
  return;
359
817
  }
360
- showItem(audioDisplay);
361
- audio = document.createElement("audio");
362
- audio.controls = "controls";
363
- audio.muted = true;
364
- audio.autoplay = true;
365
- if (Browser().isSafariWebRTC()) {
366
- audio.setAttribute("playsinline", "");
367
- audio.setAttribute("webkit-playsinline", "");
368
- this.setWebkitEventHandlers(audio);
818
+ if (available) {
819
+ qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
369
820
  } else {
370
- this.setEventHandlers(audio);
371
- }
372
- audioTrackDisplay.appendChild(audio);
373
- audioStateButton.makeButton(audioTypeDisplay, audio);
374
- audio.srcObject = stream;
375
- audio.onloadedmetadata = function (e) {
376
- audio.play().then(function() {
377
- if (Browser().isSafariWebRTC() && Browser().isiOS()) {
378
- console.warn("Audio track should be manually unmuted in iOS Safari");
821
+ qualityButton.style.color = QUALITY_COLORS.UNAVAILABLE;
822
+ }
823
+ }
824
+ },
825
+ addQuality: function (qualityName, available, onPickQuality) {
826
+ const qualityButton = document.createElement("button");
827
+ qualityButtons.set(qualityName, {btn: qualityButton, available: available});
828
+ qualityButton.innerText = qualityName;
829
+ qualityButton.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
830
+ if (available) {
831
+ qualityButton.style.color = QUALITY_COLORS.AVAILABLE;
832
+ } else {
833
+ qualityButton.style.color = QUALITY_COLORS.UNAVAILABLE;
834
+ }
835
+ qualityDisplay.appendChild(qualityButton);
836
+ qualityButton.addEventListener('click', async function () {
837
+ console.log("Clicked on quality button " + qualityName);
838
+ if (qualityButton.style.color === QUALITY_COLORS.SELECTED || qualityButton.style.color === QUALITY_COLORS.UNAVAILABLE || !videoElement) {
839
+ return;
840
+ }
841
+ lock();
842
+ onPickQuality().finally(() => unlock());
843
+ });
844
+ },
845
+ pickQuality: function (qualityName) {
846
+ repickQuality(qualityName);
847
+ }
848
+ }
849
+ }
850
+
851
+ const createOneToOneParticipantView = function () {
852
+
853
+ const participantDiv = createContainer(null);
854
+
855
+ const audioDisplay = createContainer(participantDiv);
856
+
857
+ const participantNicknameDisplay = createInfoDisplay(participantDiv, "Name: ")
858
+
859
+ const videoPlayers = new Map();
860
+ const audioElements = new Map();
861
+
862
+ return {
863
+ rootDiv: participantDiv,
864
+ dispose: function () {
865
+ for (const player of videoPlayers.values()) {
866
+ player.dispose();
867
+ }
868
+ videoPlayers.clear();
869
+ for (const element of audioElements.values()) {
870
+ element.remove();
871
+ }
872
+ audioElements.clear();
873
+ },
874
+ addVideoTrack: function (track) {
875
+ const player = createVideoPlayer(participantDiv);
876
+ videoPlayers.set(track.mid, player);
877
+ },
878
+ removeVideoTrack: function (track) {
879
+ const player = videoPlayers.get(track.mid);
880
+ if (player) {
881
+ player.dispose();
882
+ }
883
+ },
884
+ addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
885
+ const player = videoPlayers.get(track.mid);
886
+ if (player) {
887
+ player.setVideoSource(remoteVideoTrack, onResize, muteHandler);
888
+ }
889
+ },
890
+ removeVideoSource: function (track) {
891
+ const player = videoPlayers.get(track.mid);
892
+ if (player) {
893
+ player.removeVideoSource();
894
+ }
895
+ },
896
+ showVideoTrack: function (track) {
897
+ const player = videoPlayers.get(track.mid);
898
+ if (player) {
899
+ player.showVideoTrack(track);
900
+ }
901
+ },
902
+ addAudioTrack: function (track, audioTrack, show) {
903
+ const stream = new MediaStream();
904
+ stream.addTrack(audioTrack);
905
+ const audioElement = document.createElement("audio");
906
+ if (!show) {
907
+ hideItem(audioElement);
908
+ }
909
+ audioElement.controls = "controls";
910
+ audioElement.muted = true;
911
+ audioElement.autoplay = true;
912
+ audioElement.onloadedmetadata = function (e) {
913
+ audioElement.play().then(function () {
914
+ if (Browser().isSafariWebRTC() && Browser().isiOS()) {
915
+ console.warn("Audio track should be manually unmuted in iOS Safari");
916
+ } else {
917
+ audioElement.muted = false;
918
+ }
919
+ });
920
+ };
921
+ audioElements.set(track.mid, audioElement);
922
+ audioDisplay.appendChild(audioElement);
923
+ audioElement.srcObject = stream;
924
+ },
925
+ removeAudioTrack: function (track) {
926
+ const audioElement = audioElements.get(track.mid);
927
+ if (audioElement) {
928
+ audioElement.remove();
929
+ audioElements.delete(track.mid);
930
+ }
931
+ },
932
+ setNickname: function (userId, nickname) {
933
+ const additionalUserId = userId ? "#" + getShortUserId(userId) : "";
934
+ participantNicknameDisplay.innerText = "Name: " + nickname + additionalUserId;
935
+ },
936
+ updateQuality: function (track, qualityName, available) {
937
+ const player = videoPlayers.get(track.mid);
938
+ if (player) {
939
+ player.updateQuality(qualityName, available);
940
+ }
941
+ },
942
+ addQuality: function (track, qualityName, available, onQualityPick) {
943
+ const player = videoPlayers.get(track.mid);
944
+ if (player) {
945
+ player.addQuality(qualityName, available, onQualityPick);
946
+ }
947
+ },
948
+ pickQuality: function (track, qualityName) {
949
+ const player = videoPlayers.get(track.mid);
950
+ if (player) {
951
+ player.pickQuality(qualityName);
952
+ }
953
+ }
954
+ }
955
+ }
956
+ const createOneToOneParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
957
+ const instance = {
958
+ userId: userId,
959
+ nickname: nickname,
960
+ remoteVideoTracks: new Map(),
961
+ remoteAudioTracks: new Map(),
962
+ audioTracks: new Map(),
963
+ videoTracks: new Map(),
964
+ abrManagers: new Map(),
965
+ disposed: false,
966
+ dispose: async function () {
967
+ this.disposed = true;
968
+ participantView.dispose();
969
+ this.remoteVideoTracks.forEach((track, id) => {
970
+ track.dispose();
971
+ })
972
+ this.remoteVideoTracks.clear();
973
+
974
+ this.remoteAudioTracks.forEach((track, id) => {
975
+ track.dispose();
976
+ })
977
+ this.remoteAudioTracks.clear();
978
+
979
+ this.abrManagers.forEach((abrManager, id) => {
980
+ abrManager.stop();
981
+ })
982
+ this.abrManagers.clear();
983
+
984
+ },
985
+ addVideoTrack: function (track) {
986
+ this.videoTracks.set(track.mid, track);
987
+ if (!track.quality) {
988
+ track.quality = [];
989
+ }
990
+ participantView.addVideoTrack(track);
991
+ const self = this;
992
+ remoteTrackFactory.getVideoTrack().then((remoteTrack) => {
993
+ if (remoteTrack) {
994
+ if (self.disposed || !self.videoTracks.get(track.mid)) {
995
+ remoteTrack.dispose();
996
+ return;
997
+ }
998
+
999
+ participantView.addVideoSource(remoteTrack.track, track, () => {
1000
+ const abrManager = self.abrManagers.get(track.id);
1001
+ if (!abrManager) {
1002
+ return;
1003
+ }
1004
+ if (abrManager.isAuto()) {
1005
+ abrManager.resume();
1006
+ }
1007
+ }, (mute) => {
1008
+ if (mute) {
1009
+ return self.muteVideo(track);
379
1010
  } else {
380
- audio.muted = false;
381
- audioStateButton.setButtonState();
1011
+ return self.unmuteVideo(track);
382
1012
  }
383
1013
  });
384
- };
385
- },
386
- hasAudio: function() {
387
- return audio !== null || this.audioMid !== undefined;
388
- },
389
- setVideo: function(stream) {
390
- if (video) {
391
- video.remove();
392
- }
393
-
394
- if (stream == null) {
395
- video = null;
396
- this.videoMid = undefined;
397
- qualityDivs.forEach(function(div) {
398
- div.remove();
1014
+ self.requestVideoTrack(track, remoteTrack).then(() => {
1015
+ participantView.showVideoTrack(track);
1016
+ }, (ex) => {
1017
+ participantView.removeVideoSource(track);
1018
+ remoteTrack.dispose();
399
1019
  });
400
- qualityDivs = [];
401
- return;
402
1020
  }
403
- showItem(streamDisplay);
404
- video = document.createElement("video");
405
- video.controls = "controls";
406
- video.muted = true;
407
- video.autoplay = true;
408
- if (Browser().isSafariWebRTC()) {
409
- video.setAttribute("playsinline", "");
410
- video.setAttribute("webkit-playsinline", "");
411
- this.setWebkitEventHandlers(video);
412
- } else {
413
- this.setEventHandlers(video);
414
- }
415
- streamDisplay.appendChild(video);
416
- video.srcObject = stream;
417
- this.setResizeHandler(video);
418
- abr.start();
419
- },
420
- setTrackInfo: function(trackInfo) {
421
- if (trackInfo) {
422
- if (trackInfo.quality) {
423
- showItem(qualitySwitchDisplay);
424
- if (abr.isEnabled()) {
425
- const autoDiv = createQualityButton("Auto", qualityDivs, qualitySwitchDisplay);
426
- autoDiv.style.color = QUALITY_COLORS.SELECTED;
427
- autoDiv.addEventListener('click', function() {
428
- setQualityButtonsColor(qualityDivs);
429
- autoDiv.style.color = QUALITY_COLORS.SELECTED;
430
- abr.setAuto();
431
- });
432
- }
433
- for (let i = 0; i < trackInfo.quality.length; i++) {
434
- abr.addQuality(trackInfo.quality[i]);
435
- const qualityDiv = createQualityButton(trackInfo.quality[i], qualityDivs, qualitySwitchDisplay);
436
- qualityDiv.addEventListener('click', function() {
437
- console.log("Clicked on quality " + trackInfo.quality[i] + " trackId " + trackInfo.id);
438
- if (qualityDiv.style.color === QUALITY_COLORS.UNAVAILABLE) {
439
- return;
440
- }
441
- setQualityButtonsColor(qualityDivs);
442
- qualityDiv.style.color = QUALITY_COLORS.SELECTED;
443
- abr.setManual();
444
- abr.setQuality(trackInfo.quality[i]);
445
- });
446
- }
447
- } else {
448
- hideItem(qualitySwitchDisplay);
1021
+ }, (ex) => {
1022
+ console.log("Failed to get remote track " + ex);
1023
+ });
1024
+ },
1025
+ removeVideoTrack: function (track) {
1026
+ if (this.videoTracks.delete(track.mid)) {
1027
+ const remoteTrack = this.remoteVideoTracks.get(track.mid);
1028
+ if (remoteTrack) {
1029
+ this.remoteVideoTracks.delete(track.mid);
1030
+ remoteTrack.dispose();
1031
+ }
1032
+ participantView.removeVideoTrack(track);
1033
+
1034
+ const abrManager = this.abrManagers.get(track.id);
1035
+ if (abrManager) {
1036
+ this.abrManagers.delete(track.id);
1037
+ abrManager.clearQualityState();
1038
+ abrManager.stop();
1039
+ }
1040
+ }
1041
+ },
1042
+ addAudioTrack: function (track) {
1043
+ this.audioTracks.set(track.mid, track);
1044
+ const self = this;
1045
+ remoteTrackFactory.getAudioTrack().then((remoteTrack) => {
1046
+ if (remoteTrack) {
1047
+ if (self.disposed || !self.audioTracks.get(track.mid)) {
1048
+ remoteTrack.dispose();
1049
+ return;
449
1050
  }
450
- if (trackInfo.type) {
451
- contentType = trackInfo.contentType || "";
452
- if (trackInfo.type == "VIDEO" && displayOptions.type && contentType !== "") {
453
- showItem(videoTypeDisplay);
454
- videoTypeDisplay.innerHTML = contentType;
1051
+ this.remoteAudioTracks.set(track.mid, remoteTrack);
1052
+ remoteTrack.demandTrack(track.id).then(() => {
1053
+ if (!self.audioTracks.get(track.mid)) {
1054
+ remoteTrack.dispose();
1055
+ self.remoteAudioTracks.delete(track.mid);
1056
+ return;
455
1057
  }
456
- if (trackInfo.type == "AUDIO") {
457
- audioStateButton.setContentType(contentType);
1058
+ participantView.addAudioTrack(track, remoteTrack.track, displayOptions.showAudio);
1059
+ }, (ex) => {
1060
+ console.log("Failed demand track " + ex);
1061
+ remoteTrack.dispose();
1062
+ self.remoteAudioTracks.delete(track.mid);
1063
+ });
1064
+ }
1065
+ }, (ex) => {
1066
+ console.log("Failed to get audio track " + ex);
1067
+ });
1068
+ },
1069
+ removeAudioTrack: function (track) {
1070
+ if (!this.audioTracks.delete(track.mid)) {
1071
+ return
1072
+ }
1073
+
1074
+ participantView.removeAudioTrack(track);
1075
+ const remoteTrack = this.remoteAudioTracks.get(track.mid);
1076
+ if (remoteTrack) {
1077
+ this.remoteAudioTracks.delete(track.mid);
1078
+ remoteTrack.dispose();
1079
+ }
1080
+ },
1081
+ setUserId: function (userId) {
1082
+ this.userId = userId;
1083
+ },
1084
+ setNickname: function (nickname) {
1085
+ this.nickname = nickname;
1086
+ participantView.setNickname(this.userId ? this.userId : "", nickname);
1087
+ },
1088
+ updateQualityInfo: function (remoteTracks) {
1089
+ for (const remoteTrackQuality of remoteTracks) {
1090
+ const track = this.videoTracks.get(remoteTrackQuality.mid);
1091
+ if (!track) {
1092
+ continue;
1093
+ }
1094
+ if (!this.remoteVideoTracks.get(track.mid)) {
1095
+ // update model and return, view not changed
1096
+ for (const remoteQualityInfo of remoteTrackQuality.quality) {
1097
+ const quality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
1098
+ if (quality) {
1099
+ quality.available = remoteQualityInfo.available;
1100
+ } else {
1101
+ track.quality.push(remoteQualityInfo);
458
1102
  }
459
1103
  }
1104
+ return;
460
1105
  }
461
- },
462
- updateQualityInfo: function(videoQuality) {
463
- showItem(qualitySwitchDisplay);
464
- for (const qualityInfo of videoQuality) {
465
- let qualityColor = QUALITY_COLORS.UNAVAILABLE;
466
- if (qualityInfo.available === true) {
467
- qualityColor = QUALITY_COLORS.AVAILABLE;
1106
+ let abrManager = this.abrManagers.get(track.id);
1107
+ if (abrManager && track.quality.length === 0 && remoteTrackQuality.quality.length > 0) {
1108
+ const self = this;
1109
+ participantView.addQuality(track, "Auto", true, async () => {
1110
+ const manager = self.abrManagers.get(track.id);
1111
+ if (!manager) {
1112
+ return;
1113
+ }
1114
+ manager.start();
1115
+ manager.setAuto();
1116
+ participantView.pickQuality(track, "Auto");
1117
+ })
1118
+ if (displayOptions.autoAbr) {
1119
+ abrManager.setAuto();
1120
+ abrManager.start();
1121
+ participantView.pickQuality(track, "Auto");
468
1122
  }
469
- for (const qualityDiv of qualityDivs) {
470
- if (qualityDiv.innerText === qualityInfo.quality){
471
- qualityDiv.style.color = qualityColor;
472
- break;
1123
+ }
1124
+ for (const remoteQualityInfo of remoteTrackQuality.quality) {
1125
+ const localQuality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
1126
+ if (localQuality) {
1127
+ localQuality.available = remoteQualityInfo.available;
1128
+ if (abrManager) {
1129
+ abrManager.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available);
1130
+ }
1131
+ if (displayOptions.quality) {
1132
+ participantView.updateQuality(track, localQuality.quality, localQuality.available);
1133
+ }
1134
+ } else {
1135
+ track.quality.push(remoteQualityInfo);
1136
+ if (abrManager) {
1137
+ abrManager.addQuality(remoteQualityInfo.quality);
1138
+ abrManager.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available)
1139
+ }
1140
+ if (displayOptions.quality) {
1141
+ const self = this;
1142
+ participantView.addQuality(track, remoteQualityInfo.quality, remoteQualityInfo.available, async () => {
1143
+ const manager = self.abrManagers.get(track.id);
1144
+ if (manager) {
1145
+ manager.setManual();
1146
+ manager.setQuality(remoteQualityInfo.quality);
1147
+ }
1148
+ return self.pickQuality(track, remoteQualityInfo.quality);
1149
+ });
473
1150
  }
474
1151
  }
475
- abr.setQualityAvailable(qualityInfo.quality, qualityInfo.available);
476
1152
  }
477
- },
478
- hasVideo: function() {
479
- return video !== null || this.videoMid !== undefined;
480
- },
481
- setResizeHandler: function(video) {
482
- video.addEventListener("resize", function (event) {
483
- if (displayOptions.publisher) {
484
- showItem(publisherNameDisplay);
485
- publisherNameDisplay.innerHTML = "Published by: " + name;
1153
+ }
1154
+
1155
+ },
1156
+ requestVideoTrack: async function (track, remoteTrack) {
1157
+ return new Promise((resolve, reject) => {
1158
+ if (!remoteTrack || !track) {
1159
+ reject(new Error("Remote and local track must be defined"));
1160
+ return;
1161
+ }
1162
+ const self = this;
1163
+ remoteTrack.demandTrack(track.id).then(() => {
1164
+ if (!self.videoTracks.get(track.mid)) {
1165
+ reject(new Error("Video track already removed from model"));
1166
+ return;
486
1167
  }
487
- if (displayOptions.quality) {
488
- showItem(currentQualityDisplay);
489
- currentQualityDisplay.innerHTML = video.videoWidth + "x" + video.videoHeight;
1168
+ let abrManager = self.abrManagers.get(track.id);
1169
+
1170
+ if (abrManager) {
1171
+ abrManager.clearQualityState();
1172
+ } else if (abrFactory) {
1173
+ abrManager = abrFactory.createAbrManager();
1174
+ self.abrManagers.set(track.id, abrManager);
490
1175
  }
491
- resizeVideo(event.target);
492
- // Received a new quality, resume ABR is enabled
493
- if (abr.isAuto()) {
494
- abr.resume();
1176
+
1177
+ if (abrManager) {
1178
+ abrManager.setTrack(remoteTrack);
1179
+ abrManager.stop();
1180
+ if (track.quality.length > 0) {
1181
+ participantView.addQuality(track, "Auto", true, async () => {
1182
+ const manager = self.abrManagers.get(track.id);
1183
+ if (!manager) {
1184
+ return;
1185
+ }
1186
+ manager.start();
1187
+ manager.setAuto();
1188
+ participantView.pickQuality(track, "Auto");
1189
+ });
1190
+ if (displayOptions.autoAbr) {
1191
+ abrManager.setAuto();
1192
+ abrManager.start();
1193
+ participantView.pickQuality(track, "Auto");
1194
+ }
1195
+ }
495
1196
  }
1197
+ for (const qualityDescriptor of track.quality) {
1198
+ if (abrManager) {
1199
+ abrManager.addQuality(qualityDescriptor.quality);
1200
+ abrManager.setQualityAvailable(qualityDescriptor.quality, qualityDescriptor.available);
1201
+ }
1202
+ if (displayOptions.quality) {
1203
+ participantView.addQuality(track, qualityDescriptor.quality, qualityDescriptor.available, async () => {
1204
+ const manager = self.abrManagers.get(track.id);
1205
+ if (manager) {
1206
+ manager.setManual();
1207
+ manager.setQuality(qualityDescriptor.quality);
1208
+ }
1209
+ return self.pickQuality(track, qualityDescriptor.quality);
1210
+ });
1211
+ }
1212
+ }
1213
+ self.remoteVideoTracks.delete(track.mid);
1214
+ self.remoteVideoTracks.set(track.mid, remoteTrack);
1215
+ resolve();
1216
+ }, (ex) => {
1217
+ reject(ex);
496
1218
  });
497
- },
498
- setEventHandlers: function(video) {
499
- // Ignore play/pause button
500
- video.addEventListener("pause", function () {
501
- console.log("Media paused by click, continue...");
502
- video.play();
1219
+ });
1220
+ },
1221
+ pickQuality: async function (track, qualityName) {
1222
+ let remoteVideoTrack = this.remoteVideoTracks.get(track.mid);
1223
+ if (remoteVideoTrack) {
1224
+ return remoteVideoTrack.setPreferredQuality(qualityName).then(() => {
1225
+ participantView.pickQuality(track, qualityName);
503
1226
  });
504
- },
505
- setWebkitEventHandlers: function(video) {
506
- let needRestart = false;
507
- let isFullscreen = false;
508
- // Use webkitbeginfullscreen event to detect full screen mode in iOS Safari
509
- video.addEventListener("webkitbeginfullscreen", function () {
510
- isFullscreen = true;
511
- });
512
- video.addEventListener("pause", function () {
513
- if (needRestart) {
514
- console.log("Media paused after fullscreen, continue...");
515
- video.play();
516
- needRestart = false;
517
- } else {
518
- console.log("Media paused by click, continue...");
519
- video.play();
520
- }
1227
+ }
1228
+ },
1229
+ muteVideo: async function (track) {
1230
+ const remoteTrack = this.remoteVideoTracks.get(track.mid);
1231
+ if (remoteTrack) {
1232
+ return remoteTrack.mute();
1233
+ } else {
1234
+ return new Promise((resolve, reject) => {
1235
+ reject(new Error("Remote track not defined"));
521
1236
  });
522
- video.addEventListener("webkitendfullscreen", function () {
523
- video.play();
524
- needRestart = true;
525
- isFullscreen = false;
1237
+ }
1238
+ },
1239
+ unmuteVideo: async function (track) {
1240
+ const remoteTrack = this.remoteVideoTracks.get(track.mid);
1241
+ if (remoteTrack) {
1242
+ return remoteTrack.unmute();
1243
+ } else {
1244
+ return new Promise((resolve, reject) => {
1245
+ reject(new Error("Remote track not defined"));
526
1246
  });
527
- },
528
- setVideoABRTrack: function(track) {
529
- abr.setTrack(track);
530
- },
531
- audioMid: undefined,
532
- videoMid: undefined
533
- };
534
- }
1247
+ }
1248
+ }
1249
+ };
1250
+ instance.setUserId(userId);
1251
+ instance.setNickname(nickname);
1252
+ return instance;
1253
+ }
535
1254
 
536
- const stop = function() {
537
- for (const [nickName, participant] of Object.entries(remoteParticipants)) {
538
- participant.displays.forEach(function(display){
539
- display.dispose();
540
- });
541
- delete remoteParticipants[nickName];
1255
+ const createOneToManyParticipantModel = function (userId, nickname, participantView, remoteTrackFactory, abrFactory, displayOptions) {
1256
+ // reject may received before track removed from model.
1257
+ // If a new rejection reason is added in addition to track deletion,
1258
+ // a rejection reason analysis must be added
1259
+ const repickTrack = function (model, failedTrack) {
1260
+ if (!model.remoteVideoTrack) {
1261
+ return;
1262
+ }
1263
+ const tracks = new Map(model.videoTracks);
1264
+ if (failedTrack) {
1265
+ tracks.delete(failedTrack.mid);
1266
+ participantView.removeVideoSource(failedTrack);
542
1267
  }
543
- }
544
1268
 
545
- peerConnection.ontrack = ({transceiver}) => {
546
- let rParticipant;
547
- console.log("Attach remote track " + transceiver.receiver.track.id + " kind " + transceiver.receiver.track.kind + " mid " + transceiver.mid);
548
- for (const [nickName, participant] of Object.entries(remoteParticipants)) {
549
- for (const pTrack of participant.tracks) {
550
- console.log("Participant " + participant.nickName + " track " + pTrack.id + " mid " + pTrack.mid);
551
- if (pTrack.mid === transceiver.mid) {
552
- rParticipant = participant;
553
- break;
1269
+ if (tracks.size > 0) {
1270
+ const anotherTrack = tracks.values().next().value;
1271
+ participantView.addVideoSource(model.remoteVideoTrack.track, anotherTrack, () => {
1272
+ if (!model.abr) {
1273
+ return;
1274
+ }
1275
+ if (model.abr.isAuto()) {
1276
+ model.abr.resume();
554
1277
  }
1278
+ }, (mute) => {
1279
+ if (mute) {
1280
+ return model.muteVideo(anotherTrack);
1281
+ } else {
1282
+ return model.unmuteVideo(anotherTrack);
1283
+ }
1284
+ });
1285
+ model.requestVideoTrack(anotherTrack, model.remoteVideoTrack).then(() => {
1286
+ participantView.showVideoTrack(anotherTrack)
1287
+ }, (ex) => {
1288
+ console.log("Failed to request track " + anotherTrack.mid + " " + ex);
1289
+ repickTrack(model, anotherTrack);
1290
+ });
1291
+ } else {
1292
+ if (model.abr) {
1293
+ model.abr.stop();
555
1294
  }
556
- if (rParticipant) {
557
- break;
1295
+ participantView.clearQualityState();
1296
+ if (model.remoteVideoTrack) {
1297
+ model.remoteVideoTrack.dispose();
1298
+ model.remoteVideoTrack = null;
1299
+ model.videoEnabled = false;
558
1300
  }
559
1301
  }
560
- if (rParticipant) {
561
- for (const display of rParticipant.displays) {
562
- if (transceiver.receiver.track.kind === "video") {
563
- if (display.videoMid === transceiver.mid) {
564
- let stream = new MediaStream();
565
- stream.addTrack(transceiver.receiver.track);
566
- display.setVideoABRTrack(transceiver.receiver.track);
567
- display.setVideo(stream);
568
- break;
569
- }
570
- } else if (transceiver.receiver.track.kind === "audio") {
571
- if (display.audioMid === transceiver.mid) {
572
- let stream = new MediaStream();
573
- stream.addTrack(transceiver.receiver.track);
574
- display.setAudio(stream);
575
- break;
576
- }
577
- }
578
- }
579
- } else {
580
- console.warn("Failed to find participant for track " + transceiver.receiver.track.id);
581
- }
582
- }
583
-
584
- const AudioStateButton = function() {
585
- let button = {
586
- audio: null,
587
- contentType: "",
588
- displayButton: null,
589
- makeButton: function(parent, audio) {
590
- button.setAudio(audio);
591
- button.displayButton = document.createElement("button");
592
- button.displayButton.innerHTML = button.audioState();
593
- button.displayButton.addEventListener("click", function() {
594
- button.audio.muted = !button.audio.muted;
595
- button.displayButton.innerHTML = button.audioState();
596
- });
597
- parent.appendChild(button.displayButton);
598
-
599
- },
600
- setAudio: function(audio) {
601
- button.audio = audio;
602
- },
603
- setButtonState: function() {
604
- if (button.displayButton) {
605
- button.displayButton.innerHTML = button.audioState();
606
- }
607
- },
608
- setContentType: function(type) {
609
- button.contentType = type;
610
- button.setButtonState();
611
- },
612
- audioState: function() {
613
- let state = "";
614
- if (button.audio) {
615
- if (button.audio.muted) {
616
- state = "Unmute";
617
- } else {
618
- state = "Mute";
619
- }
620
- if (button.contentType) {
621
- state = state + " " + button.contentType;
622
- }
1302
+ }
1303
+
1304
+ const requestTrackAndPick = function (model, targetTrack) {
1305
+ if (model.videoTracks.size === 0) {
1306
+ return;
1307
+ }
1308
+ if (!targetTrack) {
1309
+ targetTrack = model.videoTracks.values().next().value
1310
+ }
1311
+ if (!model.videoTracks.get(targetTrack.mid)) {
1312
+ return;
1313
+ }
1314
+ if (!model.videoEnabled) {
1315
+ model.videoEnabled = true;
1316
+ remoteTrackFactory.getVideoTrack().then((remoteTrack) => {
1317
+ if (!remoteTrack) {
1318
+ model.videoEnabled = false;
1319
+ return;
623
1320
  }
624
- return (state);
625
- }
626
- };
627
- return button;
628
- }
629
-
630
- const ABR = function(interval, thresholds, keepGoodTimeout, tryUpperTimeout) {
631
- let abr = {
632
- track: null,
633
- interval: interval,
634
- thresholds: thresholds,
635
- qualities: [],
636
- currentQualityName: null,
637
- statTimer: null,
638
- paused: false,
639
- manual: false,
640
- keepGoodTimeout: keepGoodTimeout,
641
- keepGoodTimer: null,
642
- tryUpperTimeout: tryUpperTimeout,
643
- tryUpperTimer: null,
644
- start: function() {
645
- if (abr.interval) {
646
- const thresholds = Thresholds();
647
- for (const threshold of abr.thresholds) {
648
- thresholds.add(threshold.parameter, threshold.maxLeap);
649
- }
650
- abr.statsTimer = setInterval(() => {
651
- if (abr.track) {
652
- room.getStats(abr.track, constants.SFU_RTC_STATS_TYPE.INBOUND, (stats) => {
653
- if (thresholds.isReached(stats)) {
654
- abr.shiftDown();
655
- } else {
656
- abr.useGoodQuality();
657
- }
658
- });
659
- }
660
- }, abr.interval);
661
- }
662
- },
663
- stop: function() {
664
- abr.stopKeeping();
665
- abr.stopTrying();
666
- if (abr.statsTimer) {
667
- clearInterval(abr.statsTimer);
668
- abr.statsTimer = null;
669
- }
670
- },
671
- isEnabled: function () {
672
- return (abr.interval > 0);
673
- },
674
- pause: function() {
675
- abr.paused = true;
676
- },
677
- resume: function() {
678
- abr.paused = false;
679
- },
680
- setAuto: function() {
681
- abr.manual = false;
682
- abr.resume();
683
- },
684
- setManual: function() {
685
- abr.manual = true;
686
- abr.pause();
687
- },
688
- isAuto: function() {
689
- return !abr.manual;
690
- },
691
- setTrack: function(track) {
692
- abr.track = track;
693
- },
694
- setQualitiesList: function(qualities) {
695
- abr.qualities = qualities;
696
- },
697
- addQuality: function(name) {
698
- abr.qualities.push({name: name, available: false, good: true});
699
- },
700
- setQualityAvailable: function(name, available) {
701
- for (let i = 0; i < abr.qualities.length; i++) {
702
- if (name === abr.qualities[i].name) {
703
- abr.qualities[i].available = available;
704
- }
705
- }
706
- },
707
- setQualityGood: function(name, good) {
708
- if (name) {
709
- for (let i = 0; i < abr.qualities.length; i++) {
710
- if (name === abr.qualities[i].name) {
711
- abr.qualities[i].good = good;
712
- }
713
- }
1321
+ if (model.disposed || model.videoTracks.size === 0) {
1322
+ remoteTrack.dispose();
1323
+ model.videoEnabled = false;
1324
+ return;
714
1325
  }
715
- },
716
- getFirstAvailableQuality: function() {
717
- for (let i = 0; i < abr.qualities.length; i++) {
718
- if (abr.qualities[i].available) {
719
- return abr.qualities[i];
720
- }
1326
+ model.remoteVideoTrack = remoteTrack;
1327
+ if (!model.videoTracks.get(targetTrack.mid)) {
1328
+ repickTrack(model, targetTrack);
1329
+ } else {
1330
+ repickTrack(model, null);
721
1331
  }
722
- return null;
723
- },
724
- getLowerQuality: function(name) {
725
- let quality = null;
726
- if (!name) {
727
- // There were no switching yet, return a first available quality
728
- return abr.getFirstAvailableQuality();
1332
+ }, (ex) => {
1333
+ model.videoEnabled = false;
1334
+ console.log("Failed to get remote track " + ex);
1335
+ });
1336
+ }
1337
+ }
1338
+ const instance = {
1339
+ userId: userId,
1340
+ nickname: nickname,
1341
+ videoEnabled: false,
1342
+ currentTrack: null,
1343
+ remoteVideoTrack: null,
1344
+ remoteAudioTracks: new Map(),
1345
+ audioTracks: new Map(),
1346
+ videoTracks: new Map(),
1347
+ abr: null,
1348
+ disposed: false,
1349
+ dispose: async function () {
1350
+ this.disposed = true;
1351
+ participantView.dispose();
1352
+ if (this.remoteVideoTrack) {
1353
+ const remoteTrack = this.remoteVideoTrack;
1354
+ this.remoteVideoTrack = null;
1355
+ remoteTrack.dispose();
1356
+ }
1357
+ this.remoteAudioTracks.forEach((track, id) => {
1358
+ track.dispose();
1359
+ })
1360
+ if (this.abr) {
1361
+ this.abr.stop();
1362
+ }
1363
+ this.remoteAudioTracks.clear();
1364
+ },
1365
+ addVideoTrack: function (track) {
1366
+ this.videoTracks.set(track.mid, track);
1367
+ if (!track.quality) {
1368
+ track.quality = [];
1369
+ }
1370
+ const self = this;
1371
+ participantView.addVideoTrack(track, () => {
1372
+ if (self.disposed) {
1373
+ return new Promise((resolve, reject) => {
1374
+ reject(new Error("Model disposed"));
1375
+ });
729
1376
  }
730
- let currentIndex = abr.qualities.map(item => item.name).indexOf(name);
731
- for (let i = 0; i < currentIndex; i++) {
732
- if (abr.qualities[i].available) {
733
- quality = abr.qualities[i];
734
- }
1377
+
1378
+ if (self.remoteVideoTrack) {
1379
+ return new Promise((resolve, reject) => {
1380
+ self.requestVideoTrack(track, self.remoteVideoTrack).then(() => {
1381
+ resolve();
1382
+ }, (ex) => {
1383
+ reject(ex);
1384
+ });
1385
+ });
1386
+ } else {
1387
+ return new Promise((resolve, reject) => {
1388
+ reject(new Error("Remote track is null"));
1389
+ requestTrackAndPick(self, track);
1390
+ });
735
1391
  }
736
- return quality;
737
- },
738
- getUpperQuality: function(name) {
739
- let quality = null;
740
- if (!name) {
741
- // There were no switching yet, return a first available quality
742
- return abr.getFirstAvailableQuality();
1392
+ });
1393
+ requestTrackAndPick(this, track);
1394
+ },
1395
+ removeVideoTrack: function (track) {
1396
+ this.videoTracks.delete(track.mid);
1397
+ participantView.removeVideoTrack(track);
1398
+ if (this.currentTrack && this.currentTrack.mid === track.mid) {
1399
+ repickTrack(this, track);
1400
+ }
1401
+ },
1402
+ addAudioTrack: async function (track) {
1403
+ this.audioTracks.set(track.mid, track);
1404
+ const self = this;
1405
+ remoteTrackFactory.getAudioTrack().then((remoteTrack) => {
1406
+ if (!remoteTrack) {
1407
+ return;
743
1408
  }
744
- let currentIndex = abr.qualities.map(item => item.name).indexOf(name);
745
- for (let i = currentIndex + 1; i < abr.qualities.length; i++) {
746
- if (abr.qualities[i].available) {
747
- quality = abr.qualities[i];
748
- break;
749
- }
1409
+ if (self.disposed || !self.audioTracks.get(track.mid)) {
1410
+ remoteTrack.dispose();
1411
+ return;
750
1412
  }
751
- return quality;
752
- },
753
- shiftDown: function() {
754
- if (!abr.manual && !abr.paused) {
755
- abr.stopKeeping();
756
- abr.setQualityGood(abr.currentQualityName, false);
757
- let quality = abr.getLowerQuality(abr.currentQualityName);
758
- if (quality) {
759
- console.log("Switching down to " + quality.name + " quality");
760
- abr.setQuality(quality.name);
1413
+ this.remoteAudioTracks.set(track.mid, remoteTrack);
1414
+ remoteTrack.demandTrack(track.id).then(() => {
1415
+ if (!self.audioTracks.get(track.mid)) {
1416
+ remoteTrack.dispose();
1417
+ self.remoteAudioTracks.delete(track.mid);
1418
+ return;
761
1419
  }
1420
+ participantView.addAudioTrack(track, remoteTrack.track, displayOptions.showAudio);
1421
+ }, (ex) => {
1422
+ console.log("Failed demand track " + ex);
1423
+ remoteTrack.dispose();
1424
+ self.remoteAudioTracks.delete(track.mid);
1425
+ });
1426
+ }, (ex) => {
1427
+ console.log("Failed to get audio track " + ex);
1428
+ });
1429
+
1430
+ },
1431
+ removeAudioTrack: function (track) {
1432
+ if (!this.audioTracks.delete(track.mid)) {
1433
+ return
1434
+ }
1435
+
1436
+ participantView.removeAudioTrack(track);
1437
+ const remoteTrack = this.remoteAudioTracks.get(track.mid);
1438
+ if (remoteTrack) {
1439
+ this.remoteAudioTracks.delete(track.mid);
1440
+ remoteTrack.dispose();
1441
+ }
1442
+
1443
+ },
1444
+ setUserId: function (userId) {
1445
+ this.userId = userId;
1446
+ },
1447
+ setNickname: function (nickname) {
1448
+ this.nickname = nickname;
1449
+ participantView.setNickname(this.userId ? this.userId : "", nickname);
1450
+ },
1451
+ updateQualityInfo: function (remoteTracks) {
1452
+ for (const remoteTrackQuality of remoteTracks) {
1453
+ const track = this.videoTracks.get(remoteTrackQuality.mid);
1454
+ if (!track) {
1455
+ continue;
762
1456
  }
763
- },
764
- shiftUp: function() {
765
- if (!abr.manual && !abr.paused) {
766
- let quality = abr.getUpperQuality(abr.currentQualityName);
767
- if (quality) {
768
- if (quality.good) {
769
- console.log("Switching up to " + quality.name + " quality");
770
- abr.setQuality(quality.name);
1457
+ if (!this.currentTrack || this.currentTrack.mid !== track.mid) {
1458
+ // update model and return, view not changed
1459
+ for (const remoteQualityInfo of remoteTrackQuality.quality) {
1460
+ const quality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
1461
+ if (quality) {
1462
+ quality.available = remoteQualityInfo.available;
771
1463
  } else {
772
- abr.tryUpper();
1464
+ track.quality.push(remoteQualityInfo);
773
1465
  }
774
1466
  }
1467
+ return;
775
1468
  }
776
- },
777
- useGoodQuality: function() {
778
- if (!abr.manual && !abr.paused) {
779
- if (!abr.currentQualityName) {
780
- let quality = abr.getFirstAvailableQuality();
781
- abr.currentQualityName = quality.name;
1469
+ if (this.abr && track.quality.length === 0 && remoteTrackQuality.quality.length > 0) {
1470
+ const self = this;
1471
+ participantView.addQuality(track, "Auto", true, async () => {
1472
+ if (!self.abr) {
1473
+ return;
1474
+ }
1475
+ self.abr.start();
1476
+ self.abr.setAuto();
1477
+ participantView.pickQuality(track, "Auto");
1478
+ })
1479
+ if (displayOptions.autoAbr && this.abr) {
1480
+ this.abr.setAuto();
1481
+ this.abr.start();
1482
+ participantView.pickQuality(track, "Auto");
782
1483
  }
783
- abr.setQualityGood(abr.currentQualityName, true);
784
- abr.keepGoodQuality();
785
- }
786
- },
787
- keepGoodQuality: function() {
788
- if (abr.keepGoodTimeout && !abr.keepGoodTimer && abr.getUpperQuality(abr.currentQualityName)) {
789
- abr.keepGoodTimer = setTimeout(() => {
790
- abr.shiftUp();
791
- abr.stopKeeping();
792
- }, abr.keepGoodTimeout);
793
- }
794
- },
795
- stopKeeping: function() {
796
- if (abr.keepGoodTimer) {
797
- clearTimeout(abr.keepGoodTimer);
798
- abr.keepGoodTimer = null;
799
1484
  }
800
- },
801
- tryUpper: function() {
802
- let quality = abr.getUpperQuality(abr.currentQualityName);
803
- if (abr.tryUpperTimeout && !abr.tryUpperTimer && quality) {
804
- abr.tryUpperTimer = setTimeout(() => {
805
- abr.setQualityGood(quality.name, true);
806
- abr.stopTrying();
807
- }, abr.tryUpperTimeout);
1485
+ for (const remoteQualityInfo of remoteTrackQuality.quality) {
1486
+ const localQuality = track.quality.find((q) => q.quality === remoteQualityInfo.quality);
1487
+ if (localQuality) {
1488
+ localQuality.available = remoteQualityInfo.available;
1489
+ if (this.abr) {
1490
+ this.abr.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available)
1491
+ }
1492
+ if (displayOptions.quality) {
1493
+ participantView.updateQuality(track, localQuality.quality, localQuality.available);
1494
+ }
1495
+ } else {
1496
+ track.quality.push(remoteQualityInfo);
1497
+ if (this.abr) {
1498
+ this.abr.addQuality(remoteQualityInfo.quality);
1499
+ this.abr.setQualityAvailable(remoteQualityInfo.quality, remoteQualityInfo.available)
1500
+ }
1501
+ if (displayOptions.quality) {
1502
+ const self = this;
1503
+ participantView.addQuality(track, remoteQualityInfo.quality, remoteQualityInfo.available, async () => {
1504
+ if (self.abr) {
1505
+ self.abr.setManual();
1506
+ self.abr.setQuality(remoteQualityInfo.quality);
1507
+ }
1508
+ return self.pickQuality(track, remoteQualityInfo.quality);
1509
+ });
1510
+ }
1511
+ }
808
1512
  }
809
- },
810
- stopTrying: function() {
811
- if (abr.tryUpperTimer) {
812
- clearTimeout(abr.tryUpperTimer);
813
- abr.tryUpperTimer = null;
1513
+ }
1514
+ },
1515
+ requestVideoTrack: async function (track, remoteTrack) {
1516
+ return new Promise((resolve, reject) => {
1517
+ if (!remoteTrack || !track) {
1518
+ reject(new Error("Remote and local track must be defined"));
1519
+ return;
814
1520
  }
815
- },
816
- setQuality: async function(name) {
817
- // Pause switching until a new quality is received
818
- abr.pause();
819
- abr.currentQualityName = name;
820
- await room.changeQuality(abr.track.id, abr.currentQualityName);
1521
+ const self = this;
1522
+ remoteTrack.demandTrack(track.id).then(() => {
1523
+ // channels reordering case, must be removed after channels unification
1524
+ if (!self.videoTracks.get(track.mid)) {
1525
+ reject(new Error("Video track already removed from model"));
1526
+ return;
1527
+ }
1528
+ self.currentTrack = track;
1529
+ participantView.clearQualityState(track);
1530
+ if (self.abr) {
1531
+ self.abr.stop();
1532
+ self.abr.clearQualityState();
1533
+ self.abr.setTrack(remoteTrack);
1534
+
1535
+ if (track.quality.length > 0) {
1536
+ participantView.addQuality(track, "Auto", true, async () => {
1537
+ if (!self.abr) {
1538
+ return;
1539
+ }
1540
+ self.abr.start();
1541
+ self.abr.setAuto();
1542
+ participantView.pickQuality(track, "Auto");
1543
+ })
1544
+ }
1545
+ if (displayOptions.autoAbr) {
1546
+ self.abr.setAuto();
1547
+ self.abr.start();
1548
+ participantView.pickQuality(track, "Auto");
1549
+ }
1550
+ }
1551
+ for (const qualityDescriptor of track.quality) {
1552
+ if (self.abr) {
1553
+ self.abr.addQuality(qualityDescriptor.quality);
1554
+ self.abr.setQualityAvailable(qualityDescriptor.quality, qualityDescriptor.available);
1555
+ }
1556
+ if (displayOptions.quality) {
1557
+ participantView.addQuality(track, qualityDescriptor.quality, qualityDescriptor.available, async () => {
1558
+ if (self.abr) {
1559
+ self.abr.setManual();
1560
+ self.abr.setQuality(qualityDescriptor.quality);
1561
+ }
1562
+ return self.pickQuality(track, qualityDescriptor.quality);
1563
+ });
1564
+ }
1565
+ }
1566
+ resolve();
1567
+ }, (ex) => reject(ex));
1568
+ });
1569
+ },
1570
+ pickQuality: async function (track, qualityName) {
1571
+ if (this.remoteVideoTrack) {
1572
+ return this.remoteVideoTrack.setPreferredQuality(qualityName).then(() => participantView.pickQuality(track, qualityName));
1573
+ }
1574
+ },
1575
+ muteVideo: async function (track) {
1576
+ if (this.remoteVideoTrack) {
1577
+ return this.remoteVideoTrack.mute();
1578
+ } else {
1579
+ return new Promise((resolve, reject) => {
1580
+ reject(new Error("Remote track not defined"));
1581
+ });
1582
+ }
1583
+ },
1584
+ unmuteVideo: async function (track) {
1585
+ if (this.remoteVideoTrack) {
1586
+ return this.remoteVideoTrack.unmute();
1587
+ } else {
1588
+ return new Promise((resolve, reject) => {
1589
+ reject(new Error("Remote track not defined"));
1590
+ });
821
1591
  }
822
1592
  }
823
- return abr;
1593
+ };
1594
+ instance.setUserId(userId);
1595
+ instance.setNickname(nickname);
1596
+ if (abrFactory) {
1597
+ instance.abr = abrFactory.createAbrManager();
824
1598
  }
1599
+ return instance;
1600
+ }
1601
+
1602
+
1603
+ const createDefaultMeetingController = function (room, meetingModel) {
1604
+ const constants = SFU.constants;
1605
+ room.on(constants.SFU_ROOM_EVENT.PARTICIPANT_LIST, async function (e) {
1606
+ for (const idName of e.participants) {
1607
+ meetingModel.addParticipant(idName.userId, idName.name);
1608
+ }
1609
+ }).on(constants.SFU_ROOM_EVENT.JOINED, async function (e) {
1610
+ meetingModel.addParticipant(e.userId, e.name);
1611
+ }).on(constants.SFU_ROOM_EVENT.LEFT, function (e) {
1612
+ meetingModel.removeParticipant(e.userId);
1613
+ }).on(constants.SFU_ROOM_EVENT.ADD_TRACKS, async function (e) {
1614
+ meetingModel.addTracks(e.info.userId, e.info.info);
1615
+ }).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, async function (e) {
1616
+ meetingModel.removeTracks(e.info.userId, e.info.info);
1617
+ }).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATE, async function (e) {
1618
+ meetingModel.updateQualityInfo(e.info.userId, e.info.tracks);
1619
+ }).on(constants.SFU_ROOM_EVENT.ENDED, function (e) {
1620
+ meetingModel.end();
1621
+ });
1622
+ meetingModel.setMeetingName(room.id());
1623
+
1624
+
1625
+ const stop = function () {
1626
+ meetingModel.end();
1627
+ };
825
1628
 
826
1629
  return {
827
1630
  stop: stop
828
1631
  }
829
1632
  }
830
1633
 
831
- const resizeVideo = function(video, width, height) {
1634
+ const remoteTrackProvider = function (room) {
1635
+ return {
1636
+ getVideoTrack: async function () {
1637
+ return await room.getRemoteTrack("VIDEO", false);
1638
+ },
1639
+ getAudioTrack: async function () {
1640
+ return await room.getRemoteTrack("AUDIO", true);
1641
+ }
1642
+ }
1643
+ }
1644
+ const initDefaultRemoteDisplay = function (room, div, displayOptions, abrOptions) {
1645
+ const participantFactory = createParticipantFactory(remoteTrackProvider(room), createOneToManyParticipantView, createOneToManyParticipantModel);
1646
+ return initRemoteDisplay(room, div, displayOptions, abrOptions, createDefaultMeetingController, createDefaultMeetingModel, createDefaultMeetingView, participantFactory)
1647
+ }
1648
+ /*
1649
+ display options:
1650
+ autoAbr - choose abr by default
1651
+ quality - show quality buttons
1652
+ showAudio - show audio elements
1653
+ */
1654
+ const initRemoteDisplay = function (room, div, displayOptions, abrOptions, meetingController, meetingModel, meetingView, participantFactory) {
1655
+ // Validate options first
1656
+ if (!div) {
1657
+ throw new Error("Main div to place all the media tag is not defined");
1658
+ }
1659
+ if (!room) {
1660
+ throw new Error("Room is not defined");
1661
+ }
1662
+
1663
+ const dOptions = displayOptions || {quality: true, type: true, showAudio: false};
1664
+ let abrFactory;
1665
+ if (abrOptions) {
1666
+ abrFactory = abrManagerFactory(room, abrOptions);
1667
+ }
1668
+ participantFactory.abrFactory = abrFactory;
1669
+ participantFactory.displayOptions = dOptions;
1670
+ return meetingController(room, meetingModel(meetingView(div), participantFactory));
1671
+ }
1672
+
1673
+ const resizeVideo = function (video, width, height) {
1674
+ // TODO: fix
1675
+ if (video) {
1676
+ return;
1677
+ }
832
1678
  if (!video.parentNode) {
833
1679
  return;
834
1680
  }
@@ -836,12 +1682,12 @@ const resizeVideo = function(video, width, height) {
836
1682
  video.videoWidth = video.width;
837
1683
  video.videoHeight = video.height;
838
1684
  }
839
- var display = video.parentNode;
840
- var parentSize = {
1685
+ const display = video.parentNode;
1686
+ const parentSize = {
841
1687
  w: display.parentNode.clientWidth,
842
1688
  h: display.parentNode.clientHeight
843
1689
  };
844
- var newSize;
1690
+ let newSize;
845
1691
  if (width && height) {
846
1692
  newSize = downScaleToFitSize(width, height, parentSize.w, parentSize.h);
847
1693
  } else {
@@ -851,7 +1697,7 @@ const resizeVideo = function(video, width, height) {
851
1697
  display.style.height = newSize.h + "px";
852
1698
 
853
1699
  //vertical align
854
- var margin = 0;
1700
+ let margin = 0;
855
1701
  if (parentSize.h - newSize.h > 1) {
856
1702
  margin = Math.floor((parentSize.h - newSize.h) / 2);
857
1703
  }
@@ -859,7 +1705,7 @@ const resizeVideo = function(video, width, height) {
859
1705
  console.log("Resize from " + video.videoWidth + "x" + video.videoHeight + " to " + display.offsetWidth + "x" + display.offsetHeight);
860
1706
  }
861
1707
 
862
- const downScaleToFitSize = function(videoWidth, videoHeight, dstWidth, dstHeight) {
1708
+ const downScaleToFitSize = function (videoWidth, videoHeight, dstWidth, dstHeight) {
863
1709
  var newWidth, newHeight;
864
1710
  var videoRatio = videoWidth / videoHeight;
865
1711
  var dstRatio = dstWidth / dstHeight;
@@ -876,30 +1722,30 @@ const downScaleToFitSize = function(videoWidth, videoHeight, dstWidth, dstHeight
876
1722
  };
877
1723
  }
878
1724
 
879
- const createInfoDisplay = function(parent, text) {
1725
+ const createInfoDisplay = function (parent, text) {
880
1726
  const div = document.createElement("div");
881
1727
  if (text) {
882
1728
  div.innerHTML = text;
883
1729
  }
884
- div.setAttribute("style","width:auto; height:30px;");
885
- div.setAttribute("class","text-center");
1730
+ div.setAttribute("style", "width:auto; height:30px;");
1731
+ div.setAttribute("class", "text-center");
886
1732
  if (parent) {
887
1733
  parent.appendChild(div);
888
1734
  }
889
1735
  return div;
890
1736
  }
891
1737
 
892
- const createContainer = function(parent) {
1738
+ const createContainer = function (parent) {
893
1739
  const div = document.createElement("div");
894
- div.setAttribute("style","width:auto; height:auto;");
895
- div.setAttribute("class","text-center");
1740
+ div.setAttribute("style", "width:auto; height:auto;");
1741
+ div.setAttribute("class", "text-center");
896
1742
  if (parent) {
897
1743
  parent.appendChild(div);
898
1744
  }
899
1745
  return div;
900
1746
  }
901
1747
 
902
- const createQualityButton = function(qualityName, buttonsList, parent) {
1748
+ const createQualityButton = function (qualityName, buttonsList, parent) {
903
1749
  const div = document.createElement("button");
904
1750
  div.innerText = qualityName;
905
1751
  div.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
@@ -913,7 +1759,7 @@ const createQualityButton = function(qualityName, buttonsList, parent) {
913
1759
  return div;
914
1760
  }
915
1761
 
916
- const setQualityButtonsColor = function(qualityDivs) {
1762
+ const setQualityButtonsColor = function (qualityDivs) {
917
1763
  for (let c = 0; c < qualityDivs.length; c++) {
918
1764
  if (qualityDivs[c].style.color !== QUALITY_COLORS.UNAVAILABLE) {
919
1765
  qualityDivs[c].style.color = QUALITY_COLORS.AVAILABLE;
@@ -922,13 +1768,13 @@ const setQualityButtonsColor = function(qualityDivs) {
922
1768
  }
923
1769
 
924
1770
  // Helper functions to display/hide an element
925
- const showItem = function(tag) {
1771
+ const showItem = function (tag) {
926
1772
  if (tag) {
927
1773
  tag.style.display = "block";
928
1774
  }
929
1775
  }
930
1776
 
931
- const hideItem = function(tag) {
1777
+ const hideItem = function (tag) {
932
1778
  if (tag) {
933
1779
  tag.style.display = "none";
934
1780
  }