@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.
- package/README.md +43 -0
- package/package.json +32 -0
- package/src/client/chat.js +67 -0
- package/src/client/config.json +26 -0
- package/src/client/controls.js +314 -0
- package/src/client/display.js +502 -0
- package/src/client/main.css +45 -0
- package/src/client/main.html +220 -0
- package/src/client/main.js +157 -0
- package/src/client/resources/details_close.png +0 -0
- package/src/client/resources/details_open.png +0 -0
- package/src/client/util.js +67 -0
- package/src/commons/js/config.js +81 -0
- package/src/commons/js/display.js +484 -0
- package/src/commons/js/util.js +202 -0
- package/src/commons/media/silence.mp3 +0 -0
- package/src/controller/dependencies/sigma/sigma.renderers.edgeLabels.min.js +1 -0
- package/src/controller/dependencies/sigma/sigma.renderers.parallelEdges.min.js +1 -0
- package/src/controller/dependencies/sigma/sigma.require.js +12076 -0
- package/src/controller/graph-view.js +32 -0
- package/src/controller/main.css +45 -0
- package/src/controller/main.html +79 -0
- package/src/controller/main.js +65 -0
- package/src/controller/parser.js +202 -0
- package/src/controller/resources/details_close.png +0 -0
- package/src/controller/resources/details_open.png +0 -0
- package/src/controller/rest.js +56 -0
- package/src/controller/table-view.js +64 -0
- package/src/controller/test-data.js +382 -0
- package/src/player/config.json +8 -0
- package/src/player/player.css +19 -0
- package/src/player/player.html +54 -0
- package/src/player/player.js +209 -0
- package/src/sfu.ts +28 -0
- package/src/two-way-streaming/config.json +34 -0
- package/src/two-way-streaming/two-way-streaming.css +26 -0
- package/src/two-way-streaming/two-way-streaming.html +72 -0
- package/src/two-way-streaming/two-way-streaming.js +375 -0
- package/tsconfig.json +15 -0
- 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
|
+
|