@flashphoner/sfusdk-examples 2.0.56

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.
Files changed (40) hide show
  1. package/README.md +43 -0
  2. package/package.json +32 -0
  3. package/src/client/chat.js +67 -0
  4. package/src/client/config.json +26 -0
  5. package/src/client/controls.js +314 -0
  6. package/src/client/display.js +502 -0
  7. package/src/client/main.css +45 -0
  8. package/src/client/main.html +220 -0
  9. package/src/client/main.js +157 -0
  10. package/src/client/resources/details_close.png +0 -0
  11. package/src/client/resources/details_open.png +0 -0
  12. package/src/client/util.js +67 -0
  13. package/src/commons/js/config.js +81 -0
  14. package/src/commons/js/display.js +484 -0
  15. package/src/commons/js/util.js +202 -0
  16. package/src/commons/media/silence.mp3 +0 -0
  17. package/src/controller/dependencies/sigma/sigma.renderers.edgeLabels.min.js +1 -0
  18. package/src/controller/dependencies/sigma/sigma.renderers.parallelEdges.min.js +1 -0
  19. package/src/controller/dependencies/sigma/sigma.require.js +12076 -0
  20. package/src/controller/graph-view.js +32 -0
  21. package/src/controller/main.css +45 -0
  22. package/src/controller/main.html +79 -0
  23. package/src/controller/main.js +65 -0
  24. package/src/controller/parser.js +202 -0
  25. package/src/controller/resources/details_close.png +0 -0
  26. package/src/controller/resources/details_open.png +0 -0
  27. package/src/controller/rest.js +56 -0
  28. package/src/controller/table-view.js +64 -0
  29. package/src/controller/test-data.js +382 -0
  30. package/src/player/config.json +8 -0
  31. package/src/player/player.css +19 -0
  32. package/src/player/player.html +54 -0
  33. package/src/player/player.js +209 -0
  34. package/src/sfu.ts +28 -0
  35. package/src/two-way-streaming/config.json +34 -0
  36. package/src/two-way-streaming/two-way-streaming.css +26 -0
  37. package/src/two-way-streaming/two-way-streaming.html +72 -0
  38. package/src/two-way-streaming/two-way-streaming.js +375 -0
  39. package/tsconfig.json +15 -0
  40. package/webpack.config.js +40 -0
package/src/sfu.ts ADDED
@@ -0,0 +1,28 @@
1
+ import {Sfu, RoomEvent, SfuEvent, State} from "@flashphoner/sfusdk";
2
+
3
+ export function createRoom(options: {
4
+ url: string,
5
+ roomName: string,
6
+ pin: string,
7
+ nickname: string,
8
+ pc: RTCPeerConnection
9
+ }) {
10
+ const sfu = new Sfu();
11
+ sfu.connect({
12
+ url: options.url,
13
+ nickname: options.nickname,
14
+ logGroup: options.roomName
15
+ });
16
+ const room = sfu.createRoom({
17
+ name: options.roomName,
18
+ pin: options.pin,
19
+ pc: options.pc
20
+ });
21
+ return sfu;
22
+ }
23
+
24
+ export const constants = {
25
+ SFU_EVENT: SfuEvent,
26
+ SFU_ROOM_EVENT: RoomEvent,
27
+ SFU_STATE: State
28
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "room": {
3
+ "url": "ws://127.0.0.1:8080",
4
+ "name": "ROOM1",
5
+ "pin": "1234",
6
+ "nickName": "User1"
7
+ },
8
+ "media": {
9
+ "audio": {
10
+ "tracks": [{
11
+ "source": "mic",
12
+ "channels": 2
13
+ }]
14
+ },
15
+ "video": {
16
+ "tracks": [
17
+ {
18
+ "source": "camera",
19
+ "width": 1280,
20
+ "height": 720,
21
+ "codec": "H264",
22
+ "constraints": {
23
+ "frameRate": 25
24
+ },
25
+ "encodings": [
26
+ { "rid": "720p", "active": true, "maxBitrate": 900000 },
27
+ { "rid": "360p", "active": true, "maxBitrate": 500000, "scaleResolutionDownBy": 2 },
28
+ { "rid": "180p", "active": true, "maxBitrate": 200000, "scaleResolutionDownBy": 4 }
29
+ ]
30
+ }
31
+ ]
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,26 @@
1
+ video, object {
2
+ width: 100%;
3
+ height: 100%;
4
+ }
5
+
6
+ .local-video-display {
7
+ border: 1px solid rgba(0, 0, 0, 0.8);
8
+ text-align: center;
9
+ width: 200px;
10
+ height: auto;
11
+ }
12
+
13
+ .display {
14
+ width: 100%;
15
+ height: 100%;
16
+ display: inline-block;
17
+ }
18
+
19
+ .display > video, object {
20
+ width: 100%;
21
+ height: 100%;
22
+ }
23
+
24
+ video:-webkit-full-screen {
25
+ border-radius: 1px;
26
+ }
@@ -0,0 +1,72 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>SFU Two Way Streaming</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet"
8
+ integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
9
+ <!-- JavaScript Bundle with Popper -->
10
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
11
+ integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
12
+ crossorigin="anonymous"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
14
+ <link rel="stylesheet" href="two-way-streaming.css">
15
+ <script type="text/javascript" src="../sfu.js"></script>
16
+ <script type="text/javascript" src="../commons/js/util.js"></script>
17
+ <script type="text/javascript" src="../commons/js/config.js"></script>
18
+ <script type="text/javascript" src="../commons/js/display.js"></script>
19
+ <script type="text/javascript" src="two-way-streaming.js"></script>
20
+ </head>
21
+ <body onload="init()">
22
+ <div class="container" id="main">
23
+ <div class="col-sm-12">
24
+ <h2 class="text-center">SFU Two-way Streaming</h2>
25
+
26
+ <div class="row col-sm-12 justify-content-center">
27
+ <div id="connectionForm" class="col-sm-6 text-center">
28
+ <label for="url" class="control-label">Server url</label>
29
+ <input class="form-control" id="url" type="text">
30
+ <label for="roomName" class="control-label">Room name</label>
31
+ <input class="form-control" id="roomName" type="text">
32
+ </div>
33
+ </div>
34
+ <div class="row col-sm-12 justify-content-center" style="margin-top: 10px;">
35
+ <div class="col-sm-6 text-center">
36
+ <div class="text-center text-muted">Publisher</div>
37
+ <div id="publishForm" class="input-group col-sm-5" style="margin-top: 10px;">
38
+ <input class="form-control" id="publishName" type="text" style="height: 30px;" placeholder="Publisher name">
39
+ <div class="input-group-btn">
40
+ <button id="publishBtn" type="button" style="height: 30px; width: auto;">Publish</button>
41
+ </div>
42
+ </div>
43
+ <div class="text-center" style="margin-top: 20px">
44
+ <div id="publishStatus"></div>
45
+ <div id="publishErrorInfo"></div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <div class="row col-sm-12 justify-content-center" style="margin-top: 10px">
50
+ <div id="localVideo" class="col-sm-6 justify-content-center"></div>
51
+ </div>
52
+ <div class="row col-sm-12 justify-content-center" style="margin-top: 10px;">
53
+ <div class="col-sm-6 text-center">
54
+ <div class="text-center text-muted">Player</div>
55
+ <div id="playForm" class="input-group col-sm-5" style="margin-top: 10px;">
56
+ <input class="form-control" id="playName" type="text" style="height: 30px;" placeholder="Player name">
57
+ <div class="input-group-btn">
58
+ <button id="playBtn" type="button" style="height: 30px;; width: auto;">Play</button>
59
+ </div>
60
+ </div>
61
+ <div class="text-center" style="margin-top: 20px">
62
+ <div id="playStatus"></div>
63
+ <div id="playErrorInfo"></div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ <div class="row col-sm-12 justify-content-center" style="margin-top: 10px">
68
+ <div id="remoteVideo" class="col-sm-6 justify-content-center"></div>
69
+ </div>
70
+ </div>
71
+ </body>
72
+ </html>
@@ -0,0 +1,375 @@
1
+ const constants = SFU.constants;
2
+ const sfu = SFU;
3
+ let mainConfig;
4
+ let localDisplay;
5
+ let remoteDisplay;
6
+ let publishState;
7
+ let playState;
8
+ const PUBLISH = "publish";
9
+ const PLAY = "play";
10
+ const STOP = "stop";
11
+ const PRELOADER_URL="../commons/media/silence.mp3"
12
+
13
+
14
+ /**
15
+ * Default publishing config
16
+ */
17
+ const defaultConfig = {
18
+ room: {
19
+ url: "wss://127.0.0.1:8888",
20
+ name: "ROOM1",
21
+ pin: "1234",
22
+ nickName: "User1"
23
+ },
24
+ media: {
25
+ audio: {
26
+ tracks: [
27
+ {
28
+ source: "mic",
29
+ channels: 1
30
+ }
31
+ ]
32
+ },
33
+ video: {
34
+ tracks: [
35
+ {
36
+ source: "camera",
37
+ width: 640,
38
+ height: 360,
39
+ codec: "H264",
40
+ encodings: [
41
+ { rid: "360p", active: true, maxBitrate: 500000 },
42
+ { rid: "180p", active: true, maxBitrate: 200000, scaleResolutionDownBy: 2 }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Current state object
52
+ */
53
+ const CurrentState = function(prefix) {
54
+ let state = {
55
+ prefix: prefix,
56
+ pc: null,
57
+ session: null,
58
+ room: null,
59
+ timer: null,
60
+ set: function(pc, session, room) {
61
+ state.pc = pc;
62
+ state.session = session;
63
+ state.room = room;
64
+ },
65
+ clear: function() {
66
+ state.stopWaiting();
67
+ state.room = null;
68
+ state.session = null;
69
+ state.pc = null;
70
+ },
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
+ }
89
+ },
90
+ buttonId: function() {
91
+ return state.prefix + "Btn";
92
+ },
93
+ buttonText: function() {
94
+ return (state.prefix.charAt(0).toUpperCase() + state.prefix.slice(1));
95
+ },
96
+ inputId: function() {
97
+ return state.prefix + "Name";
98
+ },
99
+ statusId: function() {
100
+ return state.prefix + "Status";
101
+ },
102
+ formId: function() {
103
+ return state.prefix + "Form";
104
+ },
105
+ errInfoId: function() {
106
+ return state.prefix + "ErrorInfo";
107
+ },
108
+ is: function(value) {
109
+ return (prefix === value);
110
+ },
111
+ isActive: function() {
112
+ return (state.room && state.pc);
113
+ },
114
+ isConnected: function() {
115
+ return (state.session && state.session.state() == constants.SFU_STATE.CONNECTED);
116
+ }
117
+ };
118
+ return state;
119
+ }
120
+
121
+ /**
122
+ * load config and set default values
123
+ */
124
+ const init = function() {
125
+ let configName = getUrlParam("config") || "./config.json";
126
+ $("#publishBtn").prop('disabled', true);
127
+ $("#playBtn").prop('disabled', true);
128
+ $("#url").prop('disabled', true);
129
+ $("#roomName").prop('disabled', true);
130
+ $("#publishName").prop('disabled', true);
131
+ $("#playName").prop('disabled', true);
132
+ publishState = CurrentState(PUBLISH);
133
+ playState = CurrentState(PLAY);
134
+ $.getJSON(configName, function(cfg){
135
+ mainConfig = cfg;
136
+ onDisconnected(publishState);
137
+ onDisconnected(playState);
138
+ }).fail(function(e){
139
+ //use default config
140
+ console.error("Error reading configuration file " + configName + ": " + e.status + " " + e.statusText)
141
+ console.log("Default config will be used");
142
+ mainConfig = defaultConfig;
143
+ onDisconnected(publishState);
144
+ onDisconnected(playState);
145
+ });
146
+ $("#url").val(setURL());
147
+ $("#roomName").val("ROOM1-"+createUUID(4));
148
+ $("#publishName").val("Publisher1-"+createUUID(4));
149
+ $("#playName").val("Player1-"+createUUID(4));
150
+ }
151
+
152
+ /**
153
+ * connect to server
154
+ */
155
+ const connect = function(state) {
156
+ //create peer connection
157
+ pc = new RTCPeerConnection();
158
+ //get config object for room creation
159
+ const roomConfig = getRoomConfig(mainConfig);
160
+ roomConfig.pc = pc;
161
+ roomConfig.url = $("#url").val();
162
+ roomConfig.roomName = $("#roomName").val();
163
+ roomConfig.nickname = $("#" + state.inputId()).val();
164
+ // clean state display items
165
+ setStatus(state.statusId(), "");
166
+ setStatus(state.errInfoId(), "");
167
+ // connect to server and create a room if not
168
+ const session = sfu.createRoom(roomConfig);
169
+ session.on(constants.SFU_EVENT.CONNECTED, function() {
170
+ state.set(pc, session, session.room());
171
+ onConnected(state);
172
+ setStatus(state.statusId(), "ESTABLISHED", "green");
173
+ }).on(constants.SFU_EVENT.DISCONNECTED, function() {
174
+ state.clear();
175
+ onDisconnected(state);
176
+ setStatus(state.statusId(), "DISCONNECTED", "green");
177
+ }).on(constants.SFU_EVENT.FAILED, function(e) {
178
+ state.clear();
179
+ onDisconnected(state);
180
+ setStatus(state.statusId(), "FAILED", "red");
181
+ setStatus(state.errInfoId(), e.status + " " + e.statusText, "red");
182
+ });
183
+ }
184
+
185
+ const onConnected = function(state) {
186
+ $("#" + state.buttonId()).text("Stop").off('click').click(function () {
187
+ onStopClick(state);
188
+ });
189
+ $('#url').prop('disabled', true);
190
+ $("#roomName").prop('disabled', true);
191
+ $("#" + state.inputId()).prop('disabled', true);
192
+ // Add errors displaying
193
+ state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {
194
+ setStatus(state.errInfoId(), e, "red");
195
+ stopStreaming(state);
196
+ }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
197
+ setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red");
198
+ stopStreaming(state);
199
+ });
200
+ startStreaming(state);
201
+ }
202
+
203
+ const onDisconnected = function(state) {
204
+ $("#" + state.buttonId()).text(state.buttonText()).off('click').click(function () {
205
+ onStartClick(state);
206
+ }).prop('disabled', false);
207
+ $("#" + state.inputId()).prop('disabled', false);
208
+ // Check if other session is active
209
+ if ((state.is(PUBLISH) && playState.session)
210
+ || (state.is(PLAY) && publishState.session)) {
211
+ return;
212
+ }
213
+ $('#url').prop('disabled', false);
214
+ $("#roomName").prop('disabled', false);
215
+ }
216
+
217
+ const onStartClick = function(state) {
218
+ if (validateForm("connectionForm") && validateForm(state.formId())) {
219
+ $("#" + state.buttonId()).prop('disabled', true);
220
+ if (state.is(PLAY) && Browser().isSafariWebRTC()) {
221
+ playFirstSound(document.getElementById("main"), PRELOADER_URL).then(function () {
222
+ connect(state);
223
+ });
224
+ } else {
225
+ connect(state);
226
+ }
227
+ }
228
+ }
229
+
230
+ const onStopClick = function(state) {
231
+ $("#" + state.buttonId()).prop('disabled', true);
232
+ stopStreaming(state);
233
+ if (state.isConnected()) {
234
+ state.session.disconnect();
235
+ }
236
+ }
237
+
238
+ const startStreaming = function(state) {
239
+ if (state.is(PUBLISH)) {
240
+ publishStreams(state);
241
+ } else if (state.is(PLAY)) {
242
+ playStreams(state);
243
+ }
244
+ }
245
+
246
+ const stopStreaming = function(state) {
247
+ state.stopWaiting();
248
+ if (state.is(PUBLISH)) {
249
+ unPublishStreams(state);
250
+ } else if (state.is(PLAY)) {
251
+ stopStreams(state);
252
+ }
253
+ }
254
+
255
+ const publishStreams = async function(state) {
256
+ if (state.isConnected()) {
257
+ //create local display item to show local streams
258
+ localDisplay = initLocalDisplay(document.getElementById("localVideo"));
259
+ try {
260
+ //get configured local video streams
261
+ let streams = await getVideoStreams(mainConfig);
262
+ let audioStreams = await getAudioStreams(mainConfig);
263
+ if (state.isConnected() && state.isActive()) {
264
+ //combine local video streams with audio streams
265
+ streams.push.apply(streams, audioStreams);
266
+ let config = {};
267
+ //add our local streams to the room (to PeerConnection)
268
+ streams.forEach(function (s) {
269
+ //add local stream to local display
270
+ localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream);
271
+ //add each track to PeerConnection
272
+ s.stream.getTracks().forEach((track) => {
273
+ if (s.source === "screen") {
274
+ config[track.id] = s.source;
275
+ }
276
+ addTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
277
+ subscribeTrackToEndedEvent(state.room, track, state.pc);
278
+ });
279
+ });
280
+ state.room.join(config);
281
+ // TODO: Use room state or promises to detect if publishing started to enable stop button
282
+ state.waitFor(document.getElementById("localVideo"), 3000);
283
+ }
284
+ } catch(e) {
285
+ console.error("Failed to capture streams: " + e);
286
+ setStatus(state.errInfoId(), e.name, "red");
287
+ state.stopWaiting();
288
+ if (state.isConnected()) {
289
+ onStopClick(state);
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ const unPublishStreams = function(state) {
296
+ if (localDisplay) {
297
+ localDisplay.stop();
298
+ }
299
+ }
300
+
301
+ const playStreams = function(state) {
302
+ if (state.isConnected() && state.isActive()) {
303
+ //create remote display item to show remote streams
304
+ remoteDisplay = initRemoteDisplay(document.getElementById("remoteVideo"), state.room, state.pc);
305
+ state.room.join();
306
+ }
307
+ $("#" + state.buttonId()).prop('disabled', false);
308
+ }
309
+
310
+ const stopStreams = function(state) {
311
+ if (remoteDisplay) {
312
+ remoteDisplay.stop();
313
+ }
314
+ }
315
+
316
+ const subscribeTrackToEndedEvent = function(room, track, pc) {
317
+ track.addEventListener("ended", function() {
318
+ //track ended, see if we need to cleanup
319
+ let negotiate = false;
320
+ for (const sender of pc.getSenders()) {
321
+ if (sender.track === track) {
322
+ pc.removeTrack(sender);
323
+ //track found, set renegotiation flag
324
+ negotiate = true;
325
+ break;
326
+ }
327
+ }
328
+ if (negotiate) {
329
+ //kickoff renegotiation
330
+ room.updateState();
331
+ }
332
+ });
333
+ };
334
+
335
+ const addTrackToPeerConnection = function(pc, stream, track, encodings) {
336
+ pc.addTransceiver(track, {
337
+ direction: "sendonly",
338
+ streams: [stream],
339
+ sendEncodings: encodings ? encodings : [] //passing encoding types for video simulcast tracks
340
+ });
341
+ }
342
+
343
+ const setStatus = function (status, text, color) {
344
+ const field = document.getElementById(status);
345
+ if (color) {
346
+ field.style.color = color;
347
+ }
348
+ field.innerText = text;
349
+ }
350
+
351
+ const validateForm = function (formId) {
352
+ var valid = true;
353
+ $('#' + formId + ' :text').each(function () {
354
+ if (!$(this).val()) {
355
+ highlightInput($(this));
356
+ valid = false;
357
+ } else {
358
+ removeHighlight($(this));
359
+ }
360
+ });
361
+ return valid;
362
+
363
+ function highlightInput(input) {
364
+ input.closest('.input-group').addClass("has-error");
365
+ }
366
+
367
+ function removeHighlight(input) {
368
+ input.closest('.input-group').removeClass("has-error");
369
+ }
370
+ }
371
+
372
+ const buttonText = function (string) {
373
+ return string.charAt(0).toUpperCase() + string.slice(1);
374
+ }
375
+
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es2015",
5
+ "lib": [ "es2015", "dom" ],
6
+ "esModuleInterop": true,
7
+ "moduleResolution": "node",
8
+ "sourceMap": true,
9
+ "declaration": true,
10
+ "outDir": "./dist"
11
+ },
12
+ "include": [
13
+ "./src/**/*"
14
+ ]
15
+ }
@@ -0,0 +1,40 @@
1
+ const path = require('path');
2
+ const CopyWebpackPlugin = require('copy-webpack-plugin');
3
+
4
+ module.exports = {
5
+ entry: path.resolve(__dirname, 'src/sfu.ts'),
6
+ module: {
7
+ rules: [
8
+ {
9
+ test: /\.ts?$/,
10
+ loader: 'ts-loader'
11
+ }
12
+ ]
13
+ },
14
+ resolve: {
15
+ extensions: ['.ts', '.js'],
16
+ },
17
+ output: {
18
+ filename: 'sfu.js',
19
+ library: "SFU",
20
+ libraryTarget: "var",
21
+ globalObject: "global",
22
+ path: path.resolve(__dirname, 'dist'),
23
+ },
24
+ plugins: [
25
+ new CopyWebpackPlugin({
26
+ patterns: [
27
+ { from: path.resolve(__dirname, "src/**/*"),
28
+ to({ context, absoluteFilename }) {
29
+ return `${path.relative(context, absoluteFilename).replace(/^src\//g, "")}`;
30
+ },
31
+ info: { minimized: false },
32
+ globOptions: {
33
+ ignore: [ "**/*.ts" ]
34
+ }
35
+ }
36
+ ],
37
+ })
38
+ ]
39
+ };
40
+