@flashphoner/sfusdk-examples 2.0.185 → 2.0.188

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.185",
3
+ "version": "2.0.188",
4
4
  "description": "Official Flashphoner WebCallServer SFU SDK usage examples",
5
5
  "main": "dist/sfu.js",
6
6
  "types": "src/sfu.ts",
@@ -32,7 +32,8 @@ const getStreams = async function(tracks) {
32
32
  streams.push({
33
33
  stream: stream,
34
34
  encodings: track.encodings,
35
- source: track.source
35
+ source: track.source,
36
+ type: track.type
36
37
  });
37
38
  }
38
39
  }
@@ -29,15 +29,25 @@ const initLocalDisplay = function(localDisplayElement){
29
29
  }
30
30
  }
31
31
  }
32
- };
32
+ }
33
33
 
34
- const add = function(id, name, stream) {
34
+ const onMuteClick = function(button, stream, type) {
35
+ if (stream.getAudioTracks().length > 0) {
36
+ stream.getAudioTracks()[0].enabled = !(stream.getAudioTracks()[0].enabled);
37
+ button.innerHTML = audioStateText(stream) + " " + type;
38
+ }
39
+ }
40
+
41
+ const add = function(id, name, stream, type) {
35
42
  if (stream.getAudioTracks().length > 0) {
36
43
  let videoElement = getAudioContainer();
37
44
  if (videoElement) {
38
45
  let track = stream.getAudioTracks()[0];
39
46
  videoElement.video.srcObject.addTrack(track);
40
- videoElement.audioStateDisplay.innerHTML = audioStateText(stream);
47
+ videoElement.audioStateDisplay.innerHTML = audioStateText(stream) + " " + type;
48
+ videoElement.audioStateDisplay.addEventListener("click", function() {
49
+ onMuteClick(videoElement.audioStateDisplay, stream, type);
50
+ });
41
51
  track.addEventListener("ended", function() {
42
52
  videoElement.video.srcObject.removeTrack(track);
43
53
  videoElement.audioStateDisplay.innerHTML = "No audio";
@@ -53,31 +63,15 @@ const initLocalDisplay = function(localDisplayElement){
53
63
  }
54
64
  }
55
65
 
56
- const coreDisplay = document.createElement('div');
57
- coreDisplay.setAttribute("class","text-center");
58
- coreDisplay.setAttribute("style","width: auto; height: auto;");
66
+ const coreDisplay = createContainer(null);
59
67
  coreDisplay.id = stream.id;
60
- const publisherNameDisplay = document.createElement("div");
61
- publisherNameDisplay.innerHTML = "Name: " + name;
62
- publisherNameDisplay.setAttribute("class","text-center");
63
- publisherNameDisplay.setAttribute("style","width: auto; height: auto;");
64
- coreDisplay.appendChild(publisherNameDisplay);
68
+ const publisherNameDisplay = createInfoDisplay(coreDisplay, name + " " + type);
65
69
 
66
70
  const audioStateDisplay = document.createElement("button");
67
- audioStateDisplay.innerHTML = audioStateText(stream);
68
- audioStateDisplay.addEventListener('click', function(){
69
- if (stream.getAudioTracks().length > 0) {
70
- stream.getAudioTracks()[0].enabled = !(stream.getAudioTracks()[0].enabled);
71
- audioStateDisplay.innerHTML = audioStateText(stream);
72
- }
73
- });
74
71
  coreDisplay.appendChild(audioStateDisplay);
75
72
 
76
- const streamDisplay = document.createElement('div');
73
+ const streamDisplay = createContainer(coreDisplay);
77
74
  streamDisplay.id = "stream-" + id;
78
- streamDisplay.setAttribute("class","text-center");
79
- streamDisplay.setAttribute("style","width: auto; height: auto;");
80
- coreDisplay.appendChild(streamDisplay);
81
75
  const video = document.createElement("video");
82
76
  video.muted = true;
83
77
  if(Browser().isSafariWebRTC()) {
@@ -101,10 +95,21 @@ const initLocalDisplay = function(localDisplayElement){
101
95
  removeLocalDisplay(id);
102
96
  });
103
97
  });
104
- video.addEventListener('resize', function (event) {
105
- publisherNameDisplay.innerHTML = "Name: " + name + "<br/>Max.resolution: " + video.videoWidth + "x" + video.videoHeight;
106
- resizeVideo(event.target);
107
- });
98
+ if (stream.getVideoTracks().length > 0) {
99
+ // Resize only if video displayed
100
+ video.addEventListener('resize', function (event) {
101
+ publisherNameDisplay.innerHTML = name + " " + type + " " + video.videoWidth + "x" + video.videoHeight;
102
+ resizeVideo(event.target);
103
+ });
104
+ } else {
105
+ // Hide audio only container
106
+ hideItem(streamDisplay);
107
+ // Set up mute button for audio only stream
108
+ audioStateDisplay.innerHTML = audioStateText(stream) + " " + type;
109
+ audioStateDisplay.addEventListener("click", function() {
110
+ onMuteClick(audioStateDisplay, stream, type);
111
+ });
112
+ }
108
113
  localDisplays[id] = coreDisplay;
109
114
  localDisplayDiv.appendChild(coreDisplay);
110
115
  return coreDisplay;
@@ -150,7 +155,7 @@ const initRemoteDisplay = function(options) {
150
155
  let mainDiv = options.div;
151
156
  let room = options.room;
152
157
  let peerConnection = options.peerConnection;
153
- let displayOptions = options.displayOptions || {publisher: true, quality: true};
158
+ let displayOptions = options.displayOptions || {publisher: true, quality: true, type: true};
154
159
 
155
160
  room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e) {
156
161
  console.log("Received ADD_TRACKS");
@@ -180,6 +185,7 @@ const initRemoteDisplay = function(options) {
180
185
  continue;
181
186
  }
182
187
  display.audioMid = pTrack.mid;
188
+ display.setTrackInfo(pTrack);
183
189
  createDisplay = false;
184
190
  break;
185
191
  }
@@ -194,6 +200,7 @@ const initRemoteDisplay = function(options) {
194
200
  display.setTrackInfo(pTrack);
195
201
  } else if (pTrack.type === "AUDIO") {
196
202
  display.audioMid = pTrack.mid;
203
+ display.setTrackInfo(pTrack);
197
204
  }
198
205
  }
199
206
  }).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, function(e) {
@@ -264,35 +271,34 @@ const initRemoteDisplay = function(options) {
264
271
  mainDiv.appendChild(cell);
265
272
  let publisherNameDisplay;
266
273
  let currentQualityDisplay;
274
+ let videoTypeDisplay;
267
275
  if (displayOptions.publisher) {
268
- publisherNameDisplay = document.createElement("div");
269
- publisherNameDisplay.innerHTML = "Published by: " + name;
270
- publisherNameDisplay.setAttribute("style","width:auto; height:30px;");
271
- publisherNameDisplay.setAttribute("class","text-center");
272
- cell.appendChild(publisherNameDisplay);
276
+ publisherNameDisplay = createInfoDisplay(cell, "Published by: " + name);
273
277
  }
274
278
  if (displayOptions.quality) {
275
- currentQualityDisplay = document.createElement("div");
276
- currentQualityDisplay.innerHTML = "";
277
- currentQualityDisplay.setAttribute("style","width:auto; height:30px;");
278
- currentQualityDisplay.setAttribute("class","text-center");
279
- cell.appendChild(currentQualityDisplay);
279
+ currentQualityDisplay = createInfoDisplay(cell, "");
280
280
  }
281
- const qualitySwitchDisplay = document.createElement("div");
282
- qualitySwitchDisplay.setAttribute("style","width:auto; height:30px;");
283
- qualitySwitchDisplay.setAttribute("class","text-center");
284
- cell.appendChild(qualitySwitchDisplay);
281
+ if (displayOptions.type) {
282
+ videoTypeDisplay = createInfoDisplay(cell, "");
283
+ }
284
+ const qualitySwitchDisplay = createInfoDisplay(cell, "");
285
285
 
286
286
  let qualityDivs = [];
287
+ let contentType = "";
288
+
289
+ const rootDisplay = createContainer(cell);
290
+ const streamDisplay = createContainer(rootDisplay);
291
+ const audioDisplay = createContainer(rootDisplay);
292
+ const audioTypeDisplay = createInfoDisplay(audioDisplay);
293
+ const audioTrackDisplay = createContainer(audioDisplay);
294
+ const audioStateButton = AudioStateButton();
287
295
 
288
- const rootDisplay = document.createElement("div");
289
- rootDisplay.setAttribute("style","width:auto; height:auto;");
290
- rootDisplay.setAttribute("class","text-center");
291
- cell.appendChild(rootDisplay);
292
- const streamDisplay = document.createElement("div");
293
- streamDisplay.setAttribute("style","width:auto; height:auto;");
294
- streamDisplay.setAttribute("class","text-center");
295
- rootDisplay.appendChild(streamDisplay);
296
+ hideItem(streamDisplay);
297
+ hideItem(audioDisplay);
298
+ hideItem(publisherNameDisplay);
299
+ hideItem(currentQualityDisplay);
300
+ hideItem(videoTypeDisplay);
301
+ hideItem(qualitySwitchDisplay);
296
302
 
297
303
  let audio = null;
298
304
  let video = null;
@@ -316,6 +322,7 @@ const initRemoteDisplay = function(options) {
316
322
  this.audioMid = undefined;
317
323
  return;
318
324
  }
325
+ showItem(audioDisplay);
319
326
  audio = document.createElement("audio");
320
327
  audio.controls = "controls";
321
328
  audio.muted = true;
@@ -327,7 +334,8 @@ const initRemoteDisplay = function(options) {
327
334
  } else {
328
335
  this.setEventHandlers(audio);
329
336
  }
330
- cell.appendChild(audio);
337
+ audioTrackDisplay.appendChild(audio);
338
+ audioStateButton.makeButton(audioTypeDisplay, audio);
331
339
  audio.srcObject = stream;
332
340
  audio.onloadedmetadata = function (e) {
333
341
  audio.play().then(function() {
@@ -335,6 +343,7 @@ const initRemoteDisplay = function(options) {
335
343
  console.warn("Audio track should be manually unmuted in iOS Safari");
336
344
  } else {
337
345
  audio.muted = false;
346
+ audioStateButton.setButtonState();
338
347
  }
339
348
  });
340
349
  };
@@ -356,6 +365,7 @@ const initRemoteDisplay = function(options) {
356
365
  qualityDivs = [];
357
366
  return;
358
367
  }
368
+ showItem(streamDisplay);
359
369
  video = document.createElement("video");
360
370
  video.controls = "controls";
361
371
  video.muted = true;
@@ -372,31 +382,47 @@ const initRemoteDisplay = function(options) {
372
382
  this.setResizeHandler(video);
373
383
  },
374
384
  setTrackInfo: function(trackInfo) {
375
- if (trackInfo && trackInfo.quality) {
376
- for (let i = 0; i < trackInfo.quality.length; i++) {
377
- const qualityDiv = document.createElement("button");
378
- qualityDivs.push(qualityDiv);
379
- qualityDiv.innerText = trackInfo.quality[i];
380
- qualityDiv.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
381
- qualityDiv.style.color = "red";
382
- qualityDiv.addEventListener('click', function(){
383
- console.log("Clicked on quality " + trackInfo.quality[i] + " trackId " + trackInfo.id);
384
- if (qualityDiv.style.color === "red") {
385
- return;
386
- }
387
- for (let c = 0; c < qualityDivs.length; c++) {
388
- if (qualityDivs[c].style.color !== "red") {
389
- qualityDivs[c].style.color = "gray";
385
+ if (trackInfo) {
386
+ if (trackInfo.quality) {
387
+ showItem(qualitySwitchDisplay);
388
+ for (let i = 0; i < trackInfo.quality.length; i++) {
389
+ const qualityDiv = document.createElement("button");
390
+ qualityDivs.push(qualityDiv);
391
+ qualityDiv.innerText = trackInfo.quality[i];
392
+ qualityDiv.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
393
+ qualityDiv.style.color = "red";
394
+ qualityDiv.addEventListener('click', function(){
395
+ console.log("Clicked on quality " + trackInfo.quality[i] + " trackId " + trackInfo.id);
396
+ if (qualityDiv.style.color === "red") {
397
+ return;
390
398
  }
391
- }
392
- qualityDiv.style.color = "blue";
393
- room.changeQuality(trackInfo.id, trackInfo.quality[i]);
394
- });
395
- qualitySwitchDisplay.appendChild(qualityDiv);
399
+ for (let c = 0; c < qualityDivs.length; c++) {
400
+ if (qualityDivs[c].style.color !== "red") {
401
+ qualityDivs[c].style.color = "gray";
402
+ }
403
+ }
404
+ qualityDiv.style.color = "blue";
405
+ room.changeQuality(trackInfo.id, trackInfo.quality[i]);
406
+ });
407
+ qualitySwitchDisplay.appendChild(qualityDiv);
408
+ }
409
+ } else {
410
+ hideItem(qualitySwitchDisplay);
411
+ }
412
+ if (trackInfo.type) {
413
+ contentType = trackInfo.contentType || "";
414
+ if (trackInfo.type == "VIDEO" && displayOptions.type && contentType !== "") {
415
+ showItem(videoTypeDisplay);
416
+ videoTypeDisplay.innerHTML = contentType;
417
+ }
418
+ if (trackInfo.type == "AUDIO") {
419
+ audioStateButton.setContentType(contentType);
420
+ }
396
421
  }
397
422
  }
398
423
  },
399
424
  updateQualityInfo: function(videoQuality) {
425
+ showItem(qualitySwitchDisplay);
400
426
  for (const qualityInfo of videoQuality) {
401
427
  for (const qualityDiv of qualityDivs) {
402
428
  if (qualityDiv.innerText === qualityInfo.quality){
@@ -416,9 +442,11 @@ const initRemoteDisplay = function(options) {
416
442
  setResizeHandler: function(video) {
417
443
  video.addEventListener("resize", function (event) {
418
444
  if (displayOptions.publisher) {
445
+ showItem(publisherNameDisplay);
419
446
  publisherNameDisplay.innerHTML = "Published by: " + name;
420
447
  }
421
448
  if (displayOptions.quality) {
449
+ showItem(currentQualityDisplay);
422
450
  currentQualityDisplay.innerHTML = video.videoWidth + "x" + video.videoHeight;
423
451
  }
424
452
  resizeVideo(event.target);
@@ -506,6 +534,52 @@ const initRemoteDisplay = function(options) {
506
534
  }
507
535
  }
508
536
 
537
+ const AudioStateButton = function() {
538
+ let button = {
539
+ audio: null,
540
+ contentType: "",
541
+ displayButton: null,
542
+ makeButton: function(parent, audio) {
543
+ button.setAudio(audio);
544
+ button.displayButton = document.createElement("button");
545
+ button.displayButton.innerHTML = button.audioState();
546
+ button.displayButton.addEventListener("click", function() {
547
+ button.audio.muted = !button.audio.muted;
548
+ button.displayButton.innerHTML = button.audioState();
549
+ });
550
+ parent.appendChild(button.displayButton);
551
+
552
+ },
553
+ setAudio: function(audio) {
554
+ button.audio = audio;
555
+ },
556
+ setButtonState: function() {
557
+ if (button.displayButton) {
558
+ button.displayButton.innerHTML = button.audioState();
559
+ }
560
+ },
561
+ setContentType: function(type) {
562
+ button.contentType = type;
563
+ button.setButtonState();
564
+ },
565
+ audioState: function() {
566
+ let state = "";
567
+ if (button.audio) {
568
+ if (button.audio.muted) {
569
+ state = "Unmute";
570
+ } else {
571
+ state = "Mute";
572
+ }
573
+ if (button.contentType) {
574
+ state = state + " " + button.contentType;
575
+ }
576
+ }
577
+ return (state);
578
+ }
579
+ };
580
+ return button;
581
+ }
582
+
509
583
  return {
510
584
  stop: stop
511
585
  }
@@ -557,4 +631,40 @@ const downScaleToFitSize = function(videoWidth, videoHeight, dstWidth, dstHeight
557
631
  w: newWidth,
558
632
  h: newHeight
559
633
  };
560
- }
634
+ }
635
+
636
+ const createInfoDisplay = function(parent, text) {
637
+ const div = document.createElement("div");
638
+ if (text) {
639
+ div.innerHTML = text;
640
+ }
641
+ div.setAttribute("style","width:auto; height:30px;");
642
+ div.setAttribute("class","text-center");
643
+ if (parent) {
644
+ parent.appendChild(div);
645
+ }
646
+ return div;
647
+ }
648
+
649
+ const createContainer = function(parent) {
650
+ const div = document.createElement("div");
651
+ div.setAttribute("style","width:auto; height:auto;");
652
+ div.setAttribute("class","text-center");
653
+ if (parent) {
654
+ parent.appendChild(div);
655
+ }
656
+ return div;
657
+ }
658
+
659
+ // Helper functions to display/hide an element
660
+ const showItem = function(tag) {
661
+ if (tag) {
662
+ tag.style.display = "block";
663
+ }
664
+ }
665
+
666
+ const hideItem = function(tag) {
667
+ if (tag) {
668
+ tag.style.display = "none";
669
+ }
670
+ }
@@ -110,6 +110,7 @@ const createUUID = function (length) {
110
110
  return uuid.substring(0, length);
111
111
  }
112
112
 
113
+ // Browser detection
113
114
  const Browser = function() {
114
115
  const isIE = function () {
115
116
  return /*@cc_on!@*/false || !!document.documentMode;
@@ -176,7 +177,7 @@ const Browser = function() {
176
177
  }
177
178
  }
178
179
 
179
-
180
+ // A workaround to play audio automatically in Safari
180
181
  const playFirstSound = function (parent, preloader) {
181
182
  return new Promise(function (resolve, reject) {
182
183
  let audio = document.createElement("audio");
@@ -199,4 +200,4 @@ const playFirstSound = function (parent, preloader) {
199
200
  }
200
201
  resolve();
201
202
  });
202
- }
203
+ }
@@ -5,7 +5,9 @@ let remoteDisplay;
5
5
  let playState;
6
6
  const PLAY = "play";
7
7
  const STOP = "stop";
8
- const PRELOADER_URL="../commons/media/silence.mp3"
8
+ const PRELOADER_URL="../commons/media/silence.mp3";
9
+ const MAX_AWAIT_MS=5000;
10
+
9
11
 
10
12
  /**
11
13
  * Default publishing config
@@ -28,15 +30,31 @@ const CurrentState = function(prefix) {
28
30
  pc: null,
29
31
  session: null,
30
32
  room: null,
33
+ roomEnded: false,
34
+ timeout: null,
35
+ timer: null,
36
+ promise: null,
31
37
  set: function(pc, session, room) {
32
38
  state.pc = pc;
33
39
  state.session = session;
34
40
  state.room = room;
41
+ state.roomEnded = false;
42
+ state.timeout = null;
43
+ state.timer = null;
44
+ state.promise = null;
35
45
  },
36
46
  clear: function() {
47
+ state.stopWaiting();
37
48
  state.room = null;
38
49
  state.session = null;
39
50
  state.pc = null;
51
+ state.roomEnded = false;
52
+ state.timeout = null;
53
+ state.timer = null;
54
+ state.promise = null;
55
+ },
56
+ setRoomEnded: function() {
57
+ state.roomEnded = true;
40
58
  },
41
59
  buttonId: function() {
42
60
  return state.prefix + "Btn";
@@ -58,6 +76,52 @@ const CurrentState = function(prefix) {
58
76
  },
59
77
  is: function(value) {
60
78
  return (prefix === value);
79
+ },
80
+ isActive: function() {
81
+ return (state.room && !state.roomEnded && state.pc);
82
+ },
83
+ isConnected: function() {
84
+ return (state.session && state.session.state() == constants.SFU_STATE.CONNECTED);
85
+ },
86
+ isRoomEnded: function() {
87
+ return state.roomEnded;
88
+ },
89
+ waitFor: async function(promise, ms) {
90
+ // Create a promise that rejects in <ms> milliseconds
91
+ state.promise = promise;
92
+ state.timeout = new Promise((resolve, reject) => {
93
+ state.resolve = resolve;
94
+ state.timer = setTimeout(() => {
95
+ clearTimeout(state.timer);
96
+ state.timer = null;
97
+ state.promise = null;
98
+ state.timeout = null;
99
+ reject('Operation timed out in '+ ms + ' ms.')
100
+ }, ms)
101
+ });
102
+
103
+ // Returns a race between our timeout and the passed in promise
104
+ Promise.race([
105
+ state.promise,
106
+ state.timeout
107
+ ]).then(() => {
108
+ state.stopWaiting();
109
+ }).catch((e) => {
110
+ setStatus(state.errInfoId(), e, "red");
111
+ });
112
+ },
113
+ stopWaiting: function() {
114
+ if (state.timer) {
115
+ clearTimeout(state.timer);
116
+ state.timer = null;
117
+ }
118
+ if (state.timeout) {
119
+ state.resolve();
120
+ state.timeout = null;
121
+ }
122
+ if (state.promise) {
123
+ state.promise = null;
124
+ }
61
125
  }
62
126
  };
63
127
  return state;
@@ -120,20 +184,38 @@ const connect = function(state) {
120
184
  });
121
185
  }
122
186
 
123
- const onConnected = function(state) {
187
+ const onConnected = async function(state) {
124
188
  $("#" + state.buttonId()).text("Stop").off('click').click(function () {
125
189
  onStopClick(state);
126
- }).prop('disabled', false);
190
+ });
127
191
  $('#url').prop('disabled', true);
128
192
  $("#roomName").prop('disabled', true);
129
193
  $("#" + state.inputId()).prop('disabled', true);
130
194
  // Add errors displaying
131
195
  state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {
132
196
  setStatus(state.errInfoId(), e, "red");
197
+ state.setRoomEnded();
198
+ state.stopWaiting();
199
+ onStopClick(state);
133
200
  }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
134
201
  setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red");
202
+ state.setRoomEnded();
203
+ state.stopWaiting();
204
+ onStopClick(state);
205
+ }).on(constants.SFU_ROOM_EVENT.ENDED, function (e) {
206
+ setStatus(state.errInfoId(), "Room "+state.room.name()+" has ended", "red");
207
+ state.setRoomEnded();
208
+ state.stopWaiting();
209
+ onStopClick(state);
210
+ }).on(constants.SFU_ROOM_EVENT.DROPPED, function (e) {
211
+ setStatus(state.errInfoId(), "Dropped from the room "+state.room.name()+" due to network issues", "red");
212
+ state.setRoomEnded();
213
+ state.stopWaiting();
214
+ onStopClick(state);
135
215
  });
136
- playStreams(state);
216
+ await playStreams(state);
217
+ // Enable button after starting playback #WCS-3635
218
+ $("#" + state.buttonId()).prop('disabled', false);
137
219
  }
138
220
 
139
221
  const onDisconnected = function(state) {
@@ -158,20 +240,29 @@ const onStartClick = function(state) {
158
240
  }
159
241
  }
160
242
 
161
- const onStopClick = function(state) {
243
+ const onStopClick = async function(state) {
162
244
  $("#" + state.buttonId()).prop('disabled', true);
163
245
  stopStreams(state);
164
- state.session.disconnect();
246
+ if (state.isConnected()) {
247
+ state.waitFor(state.session.disconnect(), MAX_AWAIT_MS);
248
+ }
165
249
  }
166
250
 
167
- const playStreams = function(state) {
251
+ const playStreams = async function(state) {
168
252
  //create remote display item to show remote streams
169
- remoteDisplay = initRemoteDisplay({
170
- div: document.getElementById("remoteVideo"),
171
- room: state.room,
172
- peerConnection: state.pc
173
- });
174
- state.room.join(state.pc);
253
+ try {
254
+ remoteDisplay = initRemoteDisplay({
255
+ div: document.getElementById("remoteVideo"),
256
+ room: state.room,
257
+ peerConnection: state.pc
258
+ });
259
+ // Start WebRTC negotiation
260
+ state.waitFor(state.room.join(state.pc), MAX_AWAIT_MS);
261
+ } catch(e) {
262
+ console.error("Failed to play streams: " + e);
263
+ setStatus(state.errInfoId(), e.name, "red");
264
+ onStopClick(state);
265
+ }
175
266
  }
176
267
 
177
268
  const stopStreams = function(state) {
@@ -9,7 +9,8 @@
9
9
  "audio": {
10
10
  "tracks": [{
11
11
  "source": "mic",
12
- "channels": 2
12
+ "channels": 2,
13
+ "type": "mic1"
13
14
  }]
14
15
  },
15
16
  "video": {
@@ -26,7 +27,8 @@
26
27
  { "rid": "720p", "active": true, "maxBitrate": 900000 },
27
28
  { "rid": "360p", "active": true, "maxBitrate": 500000, "scaleResolutionDownBy": 2 },
28
29
  { "rid": "180p", "active": true, "maxBitrate": 200000, "scaleResolutionDownBy": 4 }
29
- ]
30
+ ],
31
+ "type": "cam1"
30
32
  }
31
33
  ]
32
34
  }
@@ -8,7 +8,8 @@ let playState;
8
8
  const PUBLISH = "publish";
9
9
  const PLAY = "play";
10
10
  const STOP = "stop";
11
- const PRELOADER_URL="../commons/media/silence.mp3"
11
+ const PRELOADER_URL="../commons/media/silence.mp3";
12
+ const MAX_AWAIT_MS=5000;
12
13
 
13
14
 
14
15
  /**
@@ -56,36 +57,32 @@ const CurrentState = function(prefix) {
56
57
  pc: null,
57
58
  session: null,
58
59
  room: null,
60
+ roomEnded: false,
61
+ timeout: null,
59
62
  timer: null,
63
+ promise: null,
64
+ starting: false,
60
65
  set: function(pc, session, room) {
61
66
  state.pc = pc;
62
67
  state.session = session;
63
68
  state.room = room;
69
+ state.roomEnded = false;
70
+ state.timeout = null;
71
+ state.timer = null;
72
+ state.promise = null;
64
73
  },
65
74
  clear: function() {
66
75
  state.stopWaiting();
67
76
  state.room = null;
68
77
  state.session = null;
69
78
  state.pc = null;
79
+ state.roomEnded = false;
80
+ state.timeout = null;
81
+ state.timer = null;
82
+ state.promise = null;
70
83
  },
71
- waitFor: function(div, timeout) {
72
- state.stopWaiting();
73
- state.timer = setTimeout(function () {
74
- if (div.innerHTML !== "") {
75
- // Enable stop button
76
- $("#" + state.buttonId()).prop('disabled', false);
77
- }
78
- else if (state.isConnected()) {
79
- setStatus(state.errInfoId(), "No media capturing started in " + timeout + " ms, stopping", "red");
80
- onStopClick(state);
81
- }
82
- }, timeout);
83
- },
84
- stopWaiting: function() {
85
- if (state.timer) {
86
- clearTimeout(state.timer);
87
- state.timer = null;
88
- }
84
+ setRoomEnded: function() {
85
+ state.roomEnded = true;
89
86
  },
90
87
  buttonId: function() {
91
88
  return state.prefix + "Btn";
@@ -109,10 +106,56 @@ const CurrentState = function(prefix) {
109
106
  return (prefix === value);
110
107
  },
111
108
  isActive: function() {
112
- return (state.room && state.pc);
109
+ return (state.room && !state.roomEnded && state.pc);
113
110
  },
114
111
  isConnected: function() {
115
112
  return (state.session && state.session.state() == constants.SFU_STATE.CONNECTED);
113
+ },
114
+ isRoomEnded: function() {
115
+ return state.roomEnded;
116
+ },
117
+ waitFor: async function(promise, ms) {
118
+ // Create a promise that rejects in <ms> milliseconds
119
+ state.promise = promise;
120
+ state.timeout = new Promise((resolve, reject) => {
121
+ state.resolve = resolve;
122
+ state.timer = setTimeout(() => {
123
+ clearTimeout(state.timer);
124
+ state.timer = null;
125
+ state.promise = null;
126
+ state.timeout = null;
127
+ reject('Operation timed out in '+ ms + ' ms.')
128
+ }, ms)
129
+ });
130
+
131
+ // Returns a race between our timeout and the passed in promise
132
+ Promise.race([
133
+ state.promise,
134
+ state.timeout
135
+ ]).then(() => {
136
+ state.stopWaiting();
137
+ }).catch((e) => {
138
+ setStatus(state.errInfoId(), e, "red");
139
+ });
140
+ },
141
+ stopWaiting: function() {
142
+ if (state.timer) {
143
+ clearTimeout(state.timer);
144
+ state.timer = null;
145
+ }
146
+ if (state.timeout) {
147
+ state.resolve();
148
+ state.timeout = null;
149
+ }
150
+ if (state.promise) {
151
+ state.promise = null;
152
+ }
153
+ },
154
+ setStarting: function(value) {
155
+ state.starting = value;
156
+ },
157
+ isStarting: function() {
158
+ return state.starting;
116
159
  }
117
160
  };
118
161
  return state;
@@ -154,7 +197,7 @@ const init = function() {
154
197
  */
155
198
  const connect = function(state) {
156
199
  //create peer connection
157
- pc = new RTCPeerConnection();
200
+ let pc = new RTCPeerConnection();
158
201
  //get config object for room creation
159
202
  const roomConfig = getRoomConfig(mainConfig);
160
203
  roomConfig.url = $("#url").val();
@@ -191,10 +234,24 @@ const onConnected = function(state) {
191
234
  // Add errors displaying
192
235
  state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {
193
236
  setStatus(state.errInfoId(), e, "red");
194
- stopStreaming(state);
237
+ state.setRoomEnded();
238
+ state.stopWaiting();
239
+ onStopClick(state);
195
240
  }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
196
241
  setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red");
197
- stopStreaming(state);
242
+ state.setRoomEnded();
243
+ state.stopWaiting();
244
+ onStopClick(state);
245
+ }).on(constants.SFU_ROOM_EVENT.ENDED, function () {
246
+ setStatus(state.errInfoId(), "Room "+state.room.name()+" has ended", "red");
247
+ state.setRoomEnded();
248
+ state.stopWaiting();
249
+ onStopClick(state);
250
+ }).on(constants.SFU_ROOM_EVENT.DROPPED, function () {
251
+ setStatus(state.errInfoId(), "Dropped from the room "+state.room.name()+" due to network issues", "red");
252
+ state.setRoomEnded();
253
+ state.stopWaiting();
254
+ onStopClick(state);
198
255
  });
199
256
  startStreaming(state);
200
257
  }
@@ -204,18 +261,27 @@ const onDisconnected = function(state) {
204
261
  onStartClick(state);
205
262
  }).prop('disabled', false);
206
263
  $("#" + state.inputId()).prop('disabled', false);
207
- // Check if other session is active
208
- if ((state.is(PUBLISH) && playState.session)
209
- || (state.is(PLAY) && publishState.session)) {
210
- return;
264
+ // Enable other session buttons
265
+ let otherState = getOtherState(state);
266
+ if (!otherState.session) {
267
+ $("#" + otherState.buttonId()).prop('disabled', false);
268
+ $("#" + otherState.inputId()).prop('disabled', false);
269
+ $('#url').prop('disabled', false);
270
+ $("#roomName").prop('disabled', false);
211
271
  }
212
- $('#url').prop('disabled', false);
213
- $("#roomName").prop('disabled', false);
214
272
  }
215
273
 
216
274
  const onStartClick = function(state) {
217
- if (validateForm("connectionForm") && validateForm(state.formId())) {
275
+ if (validateForm("connectionForm", state.errInfoId())
276
+ && validateForm(state.formId(), state.errInfoId())
277
+ && validateName(state, state.errInfoId())) {
278
+ state.setStarting(true);
279
+ let otherState = getOtherState(state);
218
280
  $("#" + state.buttonId()).prop('disabled', true);
281
+ // Disable other session button to prevent a simultaneous connections
282
+ if (!otherState.isStarting()) {
283
+ $("#" + otherState.buttonId()).prop('disabled', true);
284
+ }
219
285
  if (state.is(PLAY) && Browser().isSafariWebRTC()) {
220
286
  playFirstSound(document.getElementById("main"), PRELOADER_URL).then(function () {
221
287
  connect(state);
@@ -227,28 +293,35 @@ const onStartClick = function(state) {
227
293
  }
228
294
 
229
295
  const onStopClick = function(state) {
296
+ state.setStarting(false);
230
297
  $("#" + state.buttonId()).prop('disabled', true);
231
298
  stopStreaming(state);
232
- if (state.isConnected()) {
233
- state.session.disconnect();
234
- }
235
299
  }
236
300
 
237
- const startStreaming = function(state) {
301
+ const startStreaming = async function(state) {
238
302
  if (state.is(PUBLISH)) {
239
- publishStreams(state);
303
+ await publishStreams(state);
240
304
  } else if (state.is(PLAY)) {
241
- playStreams(state);
305
+ await playStreams(state);
306
+ }
307
+ state.setStarting(false);
308
+ // Enable session buttons
309
+ let otherState = getOtherState(state);
310
+ $("#" + state.buttonId()).prop('disabled', false);
311
+ if (!otherState.isStarting()) {
312
+ $("#" + otherState.buttonId()).prop('disabled', false);
242
313
  }
243
314
  }
244
315
 
245
- const stopStreaming = function(state) {
246
- state.stopWaiting();
316
+ const stopStreaming = async function(state) {
247
317
  if (state.is(PUBLISH)) {
248
318
  unPublishStreams(state);
249
319
  } else if (state.is(PLAY)) {
250
320
  stopStreams(state);
251
321
  }
322
+ if (state.isConnected()) {
323
+ state.waitFor(state.session.disconnect(), MAX_AWAIT_MS);
324
+ }
252
325
  }
253
326
 
254
327
  const publishStreams = async function(state) {
@@ -265,28 +338,23 @@ const publishStreams = async function(state) {
265
338
  let config = {};
266
339
  //add our local streams to the room (to PeerConnection)
267
340
  streams.forEach(function (s) {
341
+ let contentType = s.type || s.source;
268
342
  //add local stream to local display
269
- localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream);
343
+ localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream, contentType);
270
344
  //add each track to PeerConnection
271
345
  s.stream.getTracks().forEach((track) => {
272
- if (s.source === "screen") {
273
- config[track.id] = s.source;
274
- }
346
+ config[track.id] = contentType;
275
347
  addTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
276
348
  subscribeTrackToEndedEvent(state.room, track, state.pc);
277
349
  });
278
350
  });
279
- state.room.join(state.pc, null, config);
280
- // TODO: Use room state or promises to detect if publishing started to enable stop button
281
- state.waitFor(document.getElementById("localVideo"), 3000);
351
+ //start WebRTC negotiation
352
+ state.waitFor(state.room.join(state.pc, null, config), MAX_AWAIT_MS);
282
353
  }
283
354
  } catch(e) {
284
355
  console.error("Failed to capture streams: " + e);
285
356
  setStatus(state.errInfoId(), e.name, "red");
286
- state.stopWaiting();
287
- if (state.isConnected()) {
288
- onStopClick(state);
289
- }
357
+ onStopClick(state);
290
358
  }
291
359
  }
292
360
  }
@@ -297,17 +365,23 @@ const unPublishStreams = function(state) {
297
365
  }
298
366
  }
299
367
 
300
- const playStreams = function(state) {
368
+ const playStreams = async function(state) {
301
369
  if (state.isConnected() && state.isActive()) {
302
- //create remote display item to show remote streams
303
- remoteDisplay = initRemoteDisplay({
304
- div: document.getElementById("remoteVideo"),
305
- room: state.room,
306
- peerConnection: state.pc
307
- });
308
- state.room.join(state.pc);
370
+ try {
371
+ //create remote display item to show remote streams
372
+ remoteDisplay = initRemoteDisplay({
373
+ div: document.getElementById("remoteVideo"),
374
+ room: state.room,
375
+ peerConnection: state.pc
376
+ });
377
+ //start WebRTC negotiation
378
+ state.waitFor(state.room.join(state.pc), MAX_AWAIT_MS);
379
+ } catch(e) {
380
+ console.error("Failed to play streams: " + e);
381
+ setStatus(state.errInfoId(), e.name, "red");
382
+ onStopClick(state);
383
+ }
309
384
  }
310
- $("#" + state.buttonId()).prop('disabled', false);
311
385
  }
312
386
 
313
387
  const stopStreams = function(state) {
@@ -351,14 +425,17 @@ const setStatus = function (status, text, color) {
351
425
  field.innerText = text;
352
426
  }
353
427
 
354
- const validateForm = function (formId) {
355
- var valid = true;
428
+ const validateForm = function (formId, errorInfoId) {
429
+ let valid = true;
430
+ // Validate empty fields
356
431
  $('#' + formId + ' :text').each(function () {
357
432
  if (!$(this).val()) {
358
433
  highlightInput($(this));
359
434
  valid = false;
435
+ setStatus(errorInfoId, "Fields cannot be empty", "red");
360
436
  } else {
361
437
  removeHighlight($(this));
438
+ setStatus(errorInfoId, "");
362
439
  }
363
440
  });
364
441
  return valid;
@@ -372,7 +449,29 @@ const validateForm = function (formId) {
372
449
  }
373
450
  }
374
451
 
452
+ const validateName = function (state) {
453
+ let valid = true;
454
+ // Validate other nickname
455
+ let nameToCheck = $("#" + state.inputId()).val();
456
+ let otherState = getOtherState(state);
457
+
458
+ if (nameToCheck === $("#" + otherState.inputId()).val()) {
459
+ if (otherState.isActive() || otherState.isConnected()) {
460
+ valid = false;
461
+ setStatus(state.errInfoId(), "Cannot connect with the same name", "red");
462
+ }
463
+ }
464
+ return valid;
465
+ }
466
+
375
467
  const buttonText = function (string) {
376
468
  return string.charAt(0).toUpperCase() + string.slice(1);
377
469
  }
378
470
 
471
+ const getOtherState = function(state) {
472
+ if (state.is(PUBLISH)) {
473
+ return playState;
474
+ } else if (state.is(PLAY)) {
475
+ return publishState;
476
+ }
477
+ }
@@ -1,6 +1,8 @@
1
1
  const constants = SFU.constants;
2
2
  const sfu = SFU;
3
- const PRELOADER_URL="../commons/media/silence.mp3"
3
+ const PRELOADER_URL="../commons/media/silence.mp3";
4
+ const MAX_AWAIT_MS=5000;
5
+
4
6
 
5
7
  /**
6
8
  * Current state object
@@ -11,15 +13,40 @@ const CurrentState = function() {
11
13
  session: null,
12
14
  room: null,
13
15
  remoteDisplay: null,
16
+ roomEnded: false,
17
+ timeout: null,
18
+ timer: null,
19
+ promise: null,
14
20
  set: function(pc, session, room) {
15
21
  state.pc = pc;
16
22
  state.session = session;
17
23
  state.room = room;
24
+ state.roomEnded = false;
25
+ state.timeout = null;
26
+ state.timer = null;
27
+ state.promise = null;
18
28
  },
19
29
  clear: function() {
30
+ state.stopWaiting();
20
31
  state.room = null;
21
32
  state.session = null;
22
33
  state.pc = null;
34
+ state.roomEnded = false;
35
+ state.timeout = null;
36
+ state.timer = null;
37
+ state.promise = null;
38
+ },
39
+ setRoomEnded: function() {
40
+ state.roomEnded = true;
41
+ },
42
+ isRoomEnded: function() {
43
+ return state.roomEnded;
44
+ },
45
+ isConnected: function() {
46
+ return (state.session && state.session.state() == constants.SFU_STATE.CONNECTED);
47
+ },
48
+ isActive: function() {
49
+ return (state.room && !state.roomEnded && state.pc);
23
50
  },
24
51
  setDisplay: function(display) {
25
52
  state.remoteDisplay = display;
@@ -29,7 +56,45 @@ const CurrentState = function() {
29
56
  state.remoteDisplay.stop();
30
57
  state.remoteDisplay = null;
31
58
  }
59
+ },
60
+ waitFor: async function(promise, ms) {
61
+ // Create a promise that rejects in <ms> milliseconds
62
+ state.promise = promise;
63
+ state.timeout = new Promise((resolve, reject) => {
64
+ state.resolve = resolve;
65
+ state.timer = setTimeout(() => {
66
+ clearTimeout(state.timer);
67
+ state.timer = null;
68
+ state.promise = null;
69
+ state.timeout = null;
70
+ reject('Operation timed out in '+ ms + ' ms.')
71
+ }, ms)
72
+ });
73
+
74
+ // Returns a race between our timeout and the passed in promise
75
+ Promise.race([
76
+ state.promise,
77
+ state.timeout
78
+ ]).then(() => {
79
+ state.stopWaiting();
80
+ }).catch((e) => {
81
+ setStatus("playStatus", e, "red");
82
+ });
83
+ },
84
+ stopWaiting: function() {
85
+ if (state.timer) {
86
+ clearTimeout(state.timer);
87
+ state.timer = null;
88
+ }
89
+ if (state.timeout) {
90
+ state.resolve();
91
+ state.timeout = null;
92
+ }
93
+ if (state.promise) {
94
+ state.promise = null;
95
+ }
32
96
  }
97
+
33
98
  };
34
99
  return state;
35
100
  }
@@ -83,10 +148,10 @@ const connect = function(state) {
83
148
  });
84
149
  }
85
150
 
86
- const onConnected = function(state) {
151
+ const onConnected = async function(state) {
87
152
  $("#playBtn").text("Stop").off('click').click(function () {
88
153
  onStopClick(state);
89
- }).prop('disabled', false);
154
+ });
90
155
  $('#url').prop('disabled', true);
91
156
  $("#streamName").prop('disabled', true);
92
157
  // Add room event handling
@@ -106,12 +171,25 @@ const onConnected = function(state) {
106
171
  }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
107
172
  // Display the operation failed
108
173
  setStatus("playErrorInfo", e.operation + " failed: " + e.error, "red");
174
+ state.setRoomEnded();
175
+ state.stopWaiting();
176
+ onStopClick(state);
109
177
  }).on(constants.SFU_ROOM_EVENT.ENDED, function () {
110
178
  // Publishing is stopped, dispose playback and close connection
111
179
  setStatus("playErrorInfo", "ABR stream is stopped", "red");
180
+ state.setRoomEnded();
181
+ state.stopWaiting();
182
+ onStopClick(state);
183
+ }).on(constants.SFU_ROOM_EVENT.DROPPED, function () {
184
+ // Client dropped from the room, dispose playback and close connection
185
+ setStatus("playErrorInfo", "Playback is dropped due to network issues", "red");
186
+ state.setRoomEnded();
187
+ state.stopWaiting();
112
188
  onStopClick(state);
113
189
  });
114
- playStreams(state);
190
+ await playStreams(state);
191
+ // Enable button after starting playback #WCS-3635
192
+ $("#playBtn").prop('disabled', false);
115
193
  }
116
194
 
117
195
  const onDisconnected = function(state) {
@@ -136,13 +214,15 @@ const onStartClick = function(state) {
136
214
  }
137
215
  }
138
216
 
139
- const onStopClick = function(state) {
217
+ const onStopClick = async function(state) {
140
218
  $("#playBtn").prop('disabled', true);
141
219
  stopStreams(state);
142
- state.session.disconnect();
220
+ if (state.isConnected()) {
221
+ state.waitFor(state.session.disconnect(), MAX_AWAIT_MS);
222
+ }
143
223
  }
144
224
 
145
- const playStreams = function(state) {
225
+ const playStreams = async function(state) {
146
226
  // Create remote display item to show remote streams
147
227
  state.setDisplay(initRemoteDisplay({
148
228
  div: document.getElementById("remoteVideo"),
@@ -150,10 +230,12 @@ const playStreams = function(state) {
150
230
  peerConnection: state.pc,
151
231
  displayOptions: {
152
232
  publisher: false,
153
- quality: true
233
+ quality: true,
234
+ type: false
154
235
  }
155
236
  }));
156
- state.room.join(state.pc);
237
+ // Start WebRTC negotiation
238
+ state.waitFor(state.room.join(state.pc), MAX_AWAIT_MS);
157
239
  }
158
240
 
159
241
  const stopStreams = function(state) {