@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.
@@ -0,0 +1,646 @@
1
+ const constants = SFU.constants;
2
+ const sfu = SFU;
3
+ let mainConfig;
4
+ let publishState;
5
+ let test1State;
6
+ let test2State;
7
+ let test3State;
8
+
9
+ const PUBLISH = "publish";
10
+ const TEST1 = "test1";
11
+ const TEST2 = "test2";
12
+ const TEST3 = "test3";
13
+ const PRELOADER_URL = "../commons/media/silence.mp3";
14
+
15
+ const trackCount = 30;
16
+ /**
17
+ * Default publishing config
18
+ */
19
+ const defaultConfig = {
20
+ room: {
21
+ url: "wss://127.0.0.1:8888",
22
+ name: "ROOM1",
23
+ pin: "1234",
24
+ nickName: "User1",
25
+ failedProbesThreshold: 5,
26
+ pingInterval: 5000
27
+ },
28
+ media: {
29
+ video: {
30
+ tracks: Array(trackCount).fill({
31
+ source: "camera",
32
+ width: 1280,
33
+ height: 720,
34
+ codec: "H264",
35
+ constraints: {
36
+ frameRate: 25
37
+ },
38
+ encodings: [
39
+ {rid: "180p", active: true, maxBitrate: 2000000, scaleResolutionDownBy: 4}
40
+ ],
41
+ type: "cam1"
42
+ })
43
+ }
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Current state object
49
+ */
50
+ const CurrentState = function (prefix) {
51
+ let state = {
52
+ prefix: prefix,
53
+ pc: null,
54
+ session: null,
55
+ room: null,
56
+ display: null,
57
+ roomEnded: false,
58
+ starting: false,
59
+ set: function (pc, session, room) {
60
+ state.pc = pc;
61
+ state.session = session;
62
+ state.room = room;
63
+ state.roomEnded = false;
64
+ },
65
+ clear: function () {
66
+ state.room = null;
67
+ state.session = null;
68
+ state.pc = null;
69
+ state.roomEnded = false;
70
+ },
71
+ setRoomEnded: function () {
72
+ state.roomEnded = true;
73
+ },
74
+ buttonId: function () {
75
+ return state.prefix + "Btn";
76
+ },
77
+ buttonText: function () {
78
+ return (state.prefix.charAt(0).toUpperCase() + state.prefix.slice(1));
79
+ },
80
+ inputId: function () {
81
+ return state.prefix + "Name";
82
+ },
83
+ statusId: function () {
84
+ return state.prefix + "Status";
85
+ },
86
+ formId: function () {
87
+ return state.prefix + "Form";
88
+ },
89
+ errInfoId: function () {
90
+ return state.prefix + "ErrorInfo";
91
+ },
92
+ is: function (value) {
93
+ return (prefix === value);
94
+ },
95
+ isActive: function () {
96
+ return (state.room && !state.roomEnded && state.pc);
97
+ },
98
+ isConnected: function () {
99
+ return (state.session && state.session.state() === constants.SFU_STATE.CONNECTED);
100
+ },
101
+ isRoomEnded: function () {
102
+ return state.roomEnded;
103
+ },
104
+ setStarting: function (value) {
105
+ state.starting = value;
106
+ },
107
+ isStarting: function () {
108
+ return state.starting;
109
+ },
110
+ setDisplay: function (display) {
111
+ state.display = display;
112
+ },
113
+ disposeDisplay: function () {
114
+ if (state.display) {
115
+ state.display.stop();
116
+ state.display = null;
117
+ }
118
+ }
119
+ };
120
+ return state;
121
+ }
122
+
123
+ /**
124
+ * load config and set default values
125
+ */
126
+ const init = function () {
127
+ $("#publishBtn").prop('disabled', true);
128
+ $("#test1Btn").prop('disabled', true);
129
+ $("#test2Btn").prop('disabled', true);
130
+ $("#test3Btn").prop('disabled', true);
131
+ $("#url").prop('disabled', true);
132
+ $("#roomName").prop('disabled', true);
133
+ $("#publishName").prop('disabled', true);
134
+ publishState = CurrentState(PUBLISH);
135
+ test1State = CurrentState(TEST1);
136
+ test2State = CurrentState(TEST2);
137
+ test3State = CurrentState(TEST3);
138
+ mainConfig = defaultConfig;
139
+ onDisconnected(publishState);
140
+ onDisconnected(test1State);
141
+ onDisconnected(test2State);
142
+ onDisconnected(test3State);
143
+ $("#url").val(setURL());
144
+ $("#roomName").val("ROOM1-" + createUUID(4));
145
+ $("#publishName").val("Publisher1-" + createUUID(4));
146
+ }
147
+
148
+ /**
149
+ * connect to server
150
+ */
151
+ const connect = async function (state) {
152
+ //create peer connection
153
+ let pc = new RTCPeerConnection();
154
+ //get config object for room creation
155
+ const roomConfig = getRoomConfig(mainConfig);
156
+ roomConfig.url = $("#url").val();
157
+ roomConfig.roomName = $("#roomName").val();
158
+ roomConfig.nickname = createUUID(5);
159
+ // clean state display items
160
+ setStatus(state.statusId(), "");
161
+ setStatus(state.errInfoId(), "");
162
+ // connect to server and create a room if not
163
+ try {
164
+ const session = await sfu.createRoom(roomConfig);
165
+ // Set up session ending events
166
+ session.on(constants.SFU_EVENT.DISCONNECTED, function () {
167
+ onStopClick(state);
168
+ onDisconnected(state);
169
+ setStatus(state.statusId(), "DISCONNECTED", "green");
170
+ }).on(constants.SFU_EVENT.FAILED, function (e) {
171
+ onStopClick(state);
172
+ onDisconnected(state);
173
+ setStatus(state.statusId(), "FAILED", "red");
174
+ if (e.status && e.statusText) {
175
+ setStatus(state.errInfoId(), e.status + " " + e.statusText, "red");
176
+ } else if (e.type && e.info) {
177
+ setStatus(state.errInfoId(), e.type + ": " + e.info, "red");
178
+ }
179
+ });
180
+ // Connected successfully
181
+ onConnected(state, pc, session);
182
+ setStatus(state.statusId(), "ESTABLISHED", "green");
183
+ } catch (e) {
184
+ onDisconnected(state);
185
+ setStatus(state.statusId(), "FAILED", "red");
186
+ setStatus(state.errInfoId(), e, "red");
187
+ }
188
+ }
189
+
190
+ const onConnected = function (state, pc, session) {
191
+ state.set(pc, session, session.room());
192
+ $("#" + state.buttonId()).text("Stop").off('click').click(function () {
193
+ onStopClick(state);
194
+ });
195
+ $('#url').prop('disabled', true);
196
+ $("#roomName").prop('disabled', true);
197
+ $("#" + state.inputId()).prop('disabled', true);
198
+ // Add errors displaying
199
+ state.room.on(constants.SFU_ROOM_EVENT.FAILED, function (e) {
200
+ setStatus(state.errInfoId(), e, "red");
201
+ state.setRoomEnded();
202
+ onStopClick(state);
203
+ }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
204
+ onOperationFailed(state, e);
205
+ }).on(constants.SFU_ROOM_EVENT.ENDED, function () {
206
+ setStatus(state.errInfoId(), "Room " + state.room.name() + " has ended", "red");
207
+ state.setRoomEnded();
208
+ onStopClick(state);
209
+ }).on(constants.SFU_ROOM_EVENT.DROPPED, function () {
210
+ setStatus(state.errInfoId(), "Dropped from the room " + state.room.name() + " due to network issues", "red");
211
+ state.setRoomEnded();
212
+ onStopClick(state);
213
+ });
214
+ startStreaming(state);
215
+ }
216
+
217
+ const onDisconnected = function (state) {
218
+ state.clear();
219
+ $("#" + state.buttonId()).text(state.buttonText()).off('click').click(function () {
220
+ onStartClick(state);
221
+ }).prop('disabled', false);
222
+ }
223
+
224
+ const onStartClick = function (state) {
225
+ if (validateForm("connectionForm", state.errInfoId())
226
+ && validateForm(state.formId(), state.errInfoId())) {
227
+ state.setStarting(true);
228
+ if (!state.is(PUBLISH) && Browser().isSafariWebRTC()) {
229
+ playFirstSound(document.getElementById("main"), PRELOADER_URL).then(function () {
230
+ connect(state);
231
+ });
232
+ } else {
233
+ connect(state);
234
+ }
235
+ }
236
+ }
237
+
238
+ const onOperationFailed = function (state, event) {
239
+ if (event.operation && event.error) {
240
+ setStatus(state.errInfoId(), event.operation + " failed: " + event.error, "red");
241
+ } else {
242
+ setStatus(state.errInfoId(), event, "red");
243
+ }
244
+ state.setRoomEnded();
245
+ onStopClick(state);
246
+ }
247
+
248
+ const onStopClick = async function (state) {
249
+ state.setStarting(false);
250
+ disposeStateDisplay(state);
251
+ if (state.isConnected()) {
252
+ $("#" + state.buttonId()).prop('disabled', true);
253
+ await state.session.disconnect();
254
+ onDisconnected(state);
255
+ }
256
+ }
257
+
258
+ const startStreaming = async function (state) {
259
+ if (state.is(PUBLISH)) {
260
+ await publishStreams(state);
261
+ } else {
262
+ await playStreams(state);
263
+ }
264
+ state.setStarting(false);
265
+ }
266
+
267
+ const publishStreams = async function (state) {
268
+ if (state.isConnected()) {
269
+ //create local display item to show local streams
270
+ const localDisplay = initLocalDisplay(document.getElementById("localVideo"));
271
+ state.setDisplay(localDisplay);
272
+ try {
273
+ //get configured local video streams
274
+ let streams = await getVideoStreams(mainConfig);
275
+ let audioStreams = await getAudioStreams(mainConfig);
276
+ if (state.isConnected() && state.isActive()) {
277
+ //combine local video streams with audio streams
278
+ streams.push.apply(streams, audioStreams);
279
+ let config = {};
280
+ //add our local streams to the room (to PeerConnection)
281
+ streams.forEach(function (s) {
282
+ let contentType = s.type || s.source;
283
+ //add local stream to local display
284
+ localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream, contentType);
285
+ //add each track to PeerConnection
286
+ s.stream.getTracks().forEach((track) => {
287
+ config[track.id] = contentType;
288
+ addTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
289
+ subscribeTrackToEndedEvent(state.room, track, state.pc);
290
+ });
291
+ });
292
+ //start WebRTC negotiation
293
+ await state.room.join(state.pc, null, config);
294
+ }
295
+ } catch (e) {
296
+ if (e.type === constants.SFU_ROOM_EVENT.OPERATION_FAILED) {
297
+ onOperationFailed(state, e);
298
+ } else {
299
+ console.error("Failed to capture streams: " + e);
300
+ setStatus(state.errInfoId(), e.name, "red");
301
+ onStopClick(state);
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ const playStreams = async function (state) {
308
+ if (state.isConnected() && state.isActive()) {
309
+ try {
310
+ if (state.is(TEST1)) {
311
+ const display = initRemoteDisplay(state.room, document.getElementById("remoteVideo"), null, null,
312
+ createDefaultMeetingController,
313
+ createDelayedMeetingModel,
314
+ createDefaultMeetingView,
315
+ oneToOneParticipantFactory(remoteTrackProvider(state.room)));
316
+ state.setDisplay(display);
317
+ } else if (state.is(TEST2)) {
318
+ const display = initRemoteDisplay(state.room, document.getElementById("remoteVideo"), null, null,
319
+ createDefaultMeetingController,
320
+ createDefaultMeetingModel,
321
+ createDefaultMeetingView,
322
+ createParticipantFactory(remoteTrackProvider(state.room), createAutoMuteParticipantView, createOneToManyParticipantModel));
323
+ state.setDisplay(display);
324
+ } else if (state.is(TEST3)) {
325
+ const display = initRemoteDisplay(state.room, document.getElementById("remoteVideo"), null, null,
326
+ createDefaultMeetingController,
327
+ createTest3MeetingModel,
328
+ createDefaultMeetingView,
329
+ oneToOneParticipantFactory(remoteTrackProvider(state.room)));
330
+ state.setDisplay(display);
331
+ }
332
+ //start WebRTC negotiation
333
+ await state.room.join(state.pc, null, null, 10);
334
+ } catch (e) {
335
+ if (e.type === constants.SFU_ROOM_EVENT.OPERATION_FAILED) {
336
+ onOperationFailed(state, e);
337
+ } else {
338
+ console.error("Failed to play streams: " + e);
339
+ setStatus(state.errInfoId(), e.name, "red");
340
+ onStopClick(state);
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+
347
+ const disposeStateDisplay = function (state) {
348
+ state.disposeDisplay();
349
+ }
350
+
351
+ const subscribeTrackToEndedEvent = function (room, track, pc) {
352
+ track.addEventListener("ended", async function () {
353
+ //track ended, see if we need to cleanup
354
+ let negotiate = false;
355
+ for (const sender of pc.getSenders()) {
356
+ if (sender.track === track) {
357
+ pc.removeTrack(sender);
358
+ //track found, set renegotiation flag
359
+ negotiate = true;
360
+ break;
361
+ }
362
+ }
363
+ if (negotiate) {
364
+ //kickoff renegotiation
365
+ await room.updateState();
366
+ }
367
+ });
368
+ };
369
+
370
+ const addTrackToPeerConnection = function (pc, stream, track, encodings) {
371
+ pc.addTransceiver(track, {
372
+ direction: "sendonly",
373
+ streams: [stream],
374
+ sendEncodings: encodings ? encodings : [] //passing encoding types for video simulcast tracks
375
+ });
376
+ }
377
+
378
+ const setStatus = function (status, text, color) {
379
+ const field = document.getElementById(status);
380
+ if (color) {
381
+ field.style.color = color;
382
+ }
383
+ field.innerText = text;
384
+ }
385
+
386
+ const validateForm = function (formId, errorInfoId) {
387
+ let valid = true;
388
+ // Validate empty fields
389
+ $('#' + formId + ' :text').each(function () {
390
+ if (!$(this).val()) {
391
+ highlightInput($(this));
392
+ valid = false;
393
+ setStatus(errorInfoId, "Fields cannot be empty", "red");
394
+ } else {
395
+ removeHighlight($(this));
396
+ setStatus(errorInfoId, "");
397
+ }
398
+ });
399
+ return valid;
400
+
401
+ function highlightInput(input) {
402
+ input.closest('.input-group').addClass("has-error");
403
+ }
404
+
405
+ function removeHighlight(input) {
406
+ input.closest('.input-group').removeClass("has-error");
407
+ }
408
+ }
409
+
410
+ function timeout(ms) {
411
+ return new Promise(resolve => setTimeout(resolve, ms));
412
+ }
413
+
414
+ const createDelayedMeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
415
+ return {
416
+ participants: new Map(),
417
+ meetingName: null,
418
+ ended: false,
419
+ addParticipant: function (userId, participantName) {
420
+ if (this.participants.get(userId)) {
421
+ return;
422
+ }
423
+ const [participantModel, participantView, participant] = participantFactory.createParticipant(userId, participantName, displayOptions, abrFactory);
424
+ this.participants.set(userId, participant);
425
+ meetingView.addParticipant(userId, participantName, participantView.rootDiv);
426
+ },
427
+ removeParticipant: function (userId) {
428
+ const participant = this.participants.get(userId);
429
+ if (participant) {
430
+ this.participants.delete(userId);
431
+ meetingView.removeParticipant(userId);
432
+ participant.dispose();
433
+ }
434
+ },
435
+ addTracks: async function (userId, tracks) {
436
+ const participant = this.participants.get(userId);
437
+ if (!participant) {
438
+ return;
439
+ }
440
+
441
+ for (let i = 0; i < 10 && i < tracks.length; i++) {
442
+ if (this.ended) {
443
+ return;
444
+ }
445
+ if (tracks[i].type === "VIDEO") {
446
+ participant.addVideoTrack(tracks[i]);
447
+ await timeout(1000);
448
+ }
449
+ }
450
+ },
451
+ removeTracks: function (userId, tracks) {
452
+ const participant = this.participants.get(userId);
453
+ if (!participant) {
454
+ return;
455
+ }
456
+ for (const track of tracks) {
457
+ if (track.type === "VIDEO") {
458
+ participant.removeVideoTrack(track);
459
+ } else if (track.type === "AUDIO") {
460
+ participant.removeAudioTrack(track);
461
+ }
462
+ }
463
+ },
464
+ updateQualityInfo: function (userId, tracksInfo) {
465
+ const participant = this.participants.get(userId);
466
+ if (!participant) {
467
+ return;
468
+ }
469
+ participant.updateQualityInfo(tracksInfo);
470
+ },
471
+ end: function () {
472
+ console.log("Meeting " + this.meetingName + " ended")
473
+ meetingView.end();
474
+ this.participants.forEach((participant, id) => {
475
+ participant.dispose();
476
+ });
477
+ this.participants.clear();
478
+ },
479
+ setMeetingName: function (id) {
480
+ this.meetingName = id;
481
+ meetingView.setMeetingName(id);
482
+ }
483
+ }
484
+ }
485
+
486
+ const createAutoMuteParticipantView = function () {
487
+
488
+ const participantDiv = createContainer(null);
489
+
490
+ const participantNicknameDisplay = createInfoDisplay(participantDiv, "Name: ");
491
+ const muteStatus = createInfoDisplay(participantDiv, "unMuted");
492
+
493
+
494
+ const player = createVideoPlayer(participantDiv);
495
+ const timer = setInterval(() => {
496
+ if (player.muteButton) {
497
+ player.muteButton.click();
498
+ }
499
+ }, 10000);
500
+
501
+ return {
502
+ rootDiv: participantDiv,
503
+ dispose: function () {
504
+ player.dispose();
505
+ if (timer) {
506
+ clearInterval(timer);
507
+ }
508
+ },
509
+ addVideoTrack: function (track, requestVideoTrack) {
510
+
511
+ },
512
+ removeVideoTrack: function (track) {
513
+
514
+ },
515
+ addVideoSource: function (remoteVideoTrack, track, onResize, muteHandler) {
516
+ player.setVideoSource(remoteVideoTrack, onResize, async (mute) => {
517
+ const startDate = Date.now();
518
+ if (mute) {
519
+ muteStatus.innerText = "muting";
520
+ return muteHandler(mute).then(() => {
521
+ const delay = Date.now() - startDate;
522
+ muteStatus.innerText = "muted";
523
+ muteStatus.innerHTML = muteStatus.innerHTML + " in " + delay + "ms";
524
+
525
+ });
526
+ } else {
527
+ muteStatus.innerText = "unMuting";
528
+ return muteHandler(mute).then(() => {
529
+ const delay = Date.now() - startDate;
530
+ muteStatus.innerText = "unMuted";
531
+ muteStatus.innerHTML = muteStatus.innerHTML + " in " + delay + "ms";
532
+
533
+ });
534
+ }});
535
+ if (player.muteButton) {
536
+ hideItem(player.muteButton);
537
+ }
538
+ },
539
+ removeVideoSource: function (track) {
540
+ player.removeVideoSource(track);
541
+ },
542
+ showVideoTrack: function (track) {
543
+ player.showVideoTrack(track);
544
+ },
545
+ addAudioTrack: function (track, audioTrack, show) {
546
+
547
+ },
548
+ removeAudioTrack: function (track) {
549
+
550
+ },
551
+ setNickname: function (nickname) {
552
+ participantNicknameDisplay.innerText = "Name: " + nickname;
553
+ },
554
+ updateQuality: function (track, qualityName, available) {
555
+ player.updateQuality(qualityName, available);
556
+ },
557
+ addQuality: function (track, qualityName, available, onQualityPick) {
558
+ player.addQuality(qualityName, available, onQualityPick);
559
+ },
560
+ clearQualityState: function (track) {
561
+ player.clearQualityState();
562
+ },
563
+ pickQuality: function (track, qualityName) {
564
+ player.pickQuality(qualityName);
565
+ }
566
+ }
567
+ }
568
+
569
+ const createTest3MeetingModel = function (meetingView, participantFactory, displayOptions, abrFactory) {
570
+ return {
571
+ participants: new Map(),
572
+ meetingName: null,
573
+ ended: false,
574
+ addParticipant: function (userId, participantName) {
575
+ if (this.participants.get(userId)) {
576
+ return;
577
+ }
578
+ const [participantModel, participantView, participant] = participantFactory.createParticipant(userId, participantName, displayOptions, abrFactory);
579
+ this.participants.set(userId, participant);
580
+ meetingView.addParticipant(userId, participantName, participantView.rootDiv);
581
+ },
582
+ removeParticipant: function (userId) {
583
+ const participant = this.participants.get(userId);
584
+ if (participant) {
585
+ this.participants.delete(userId);
586
+ meetingView.removeParticipant(userId);
587
+ participant.dispose();
588
+ }
589
+ },
590
+ addTracks: async function (userId, tracks) {
591
+ const participant = this.participants.get(userId);
592
+ if (!participant) {
593
+ return;
594
+ }
595
+ const videoTracks = tracks.filter((t) => t.type === "VIDEO");
596
+ for (let i = 0; i < videoTracks.length;i+=5) {
597
+ console.log("add 5 tracks");
598
+ for(let j = i; j< i+5; j++) {
599
+ participant.addVideoTrack(videoTracks[j]);
600
+ }
601
+ await timeout(5000);
602
+ if (this.ended) {
603
+ return;
604
+ }
605
+ const tracksToRemove = []
606
+ console.log("remove 5 tracks");
607
+ for(let j = i; j< i+5; j++) {
608
+ tracksToRemove.push(videoTracks[j]);
609
+ }
610
+ this.removeTracks(userId, tracksToRemove);
611
+ }
612
+ },
613
+ removeTracks: function (userId, tracks) {
614
+ const participant = this.participants.get(userId);
615
+ if (!participant) {
616
+ return;
617
+ }
618
+ for (const track of tracks) {
619
+ if (track.type === "VIDEO") {
620
+ participant.removeVideoTrack(track);
621
+ } else if (track.type === "AUDIO") {
622
+ participant.removeAudioTrack(track);
623
+ }
624
+ }
625
+ },
626
+ updateQualityInfo: function (userId, tracksInfo) {
627
+ const participant = this.participants.get(userId);
628
+ if (!participant) {
629
+ return;
630
+ }
631
+ participant.updateQualityInfo(tracksInfo);
632
+ },
633
+ end: function () {
634
+ console.log("Meeting " + this.meetingName + " ended")
635
+ meetingView.end();
636
+ this.participants.forEach((participant, id) => {
637
+ participant.dispose();
638
+ });
639
+ this.participants.clear();
640
+ },
641
+ setMeetingName: function (id) {
642
+ this.meetingName = id;
643
+ meetingView.setMeetingName(id);
644
+ }
645
+ }
646
+ }
@@ -26,12 +26,12 @@
26
26
  "frameRate": 25
27
27
  },
28
28
  "encodings": [
29
- { "rid": "720p", "active": true, "maxBitrate": 900000 },
29
+ { "rid": "180p", "active": true, "maxBitrate": 200000, "scaleResolutionDownBy": 4 },
30
30
  { "rid": "360p", "active": true, "maxBitrate": 500000, "scaleResolutionDownBy": 2 },
31
- { "rid": "180p", "active": true, "maxBitrate": 200000, "scaleResolutionDownBy": 4 }
31
+ { "rid": "720p", "active": true, "maxBitrate": 900000 }
32
32
  ],
33
33
  "type": "cam1"
34
- }
34
+ }
35
35
  ]
36
36
  }
37
37
  }