ovenlivekit 1.2.0 → 1.4.0
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 +14 -6
- package/dist/OvenLiveKit.js +3 -3
- package/dist/OvenLiveKit.min.js +1 -1
- package/dist/OvenLiveKit.min.js.map +1 -1
- package/package.json +2 -1
- package/src/OvenLiveKit.js +848 -577
- package/.github/FUNDING.yml +0 -2
- package/assets/05_OvenSpace_230214.png +0 -0
- package/index.html +0 -41
- package/webpack.dev.js +0 -17
- package/webpack.prod.js +0 -18
package/src/OvenLiveKit.js
CHANGED
|
@@ -1,906 +1,1177 @@
|
|
|
1
1
|
const OvenLiveKit = {};
|
|
2
2
|
|
|
3
|
-
const version = '1.
|
|
3
|
+
const version = '1.4.0';
|
|
4
4
|
const logHeader = 'OvenLiveKit.js :';
|
|
5
5
|
const logEventHeader = 'OvenLiveKit.js ====';
|
|
6
6
|
|
|
7
7
|
// private methods
|
|
8
8
|
function sendMessage(webSocket, message) {
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
if (webSocket) {
|
|
11
|
+
webSocket.send(JSON.stringify(message));
|
|
12
|
+
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function generateDomainFromUrl(url) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
let result = '';
|
|
17
|
+
let match;
|
|
18
|
+
if (match = url.match(/^(?:wss?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n\?\=]+)/im)) {
|
|
19
|
+
result = match[1];
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
return result;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function findIp(string) {
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
let result = '';
|
|
28
|
+
let match;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if (match = string.match(new RegExp('\\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b', 'gi'))) {
|
|
31
|
+
result = match[0];
|
|
32
|
+
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
return result;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function getFormatNumber(sdp, format) {
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const lines = sdp.split('\r\n');
|
|
40
|
+
let formatNumber = -1;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
lines[i] = lines[i].toLowerCase();
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
46
|
+
if (lines[i].indexOf('a=rtpmap') === 0 && lines[i].indexOf(format.toLowerCase()) > -1) {
|
|
47
|
+
// parsing "a=rtpmap:100 H264/90000" line
|
|
48
|
+
// a=rtpmap:<payload type> <encoding name>/<clock rate>[/<encoding parameters >]
|
|
49
|
+
formatNumber = lines[i].split(' ')[0].split(':')[1];
|
|
50
|
+
break;
|
|
52
51
|
}
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
return formatNumber;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function setPreferredVideoFormat(sdp, formatName) {
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const formatNumber = getFormatNumber(sdp, formatName);
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
if (formatNumber === -1) {
|
|
62
|
+
return sdp;
|
|
63
|
+
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
let newLines = [];
|
|
66
|
+
const lines = sdp.split('\r\n');
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
const line = lines[i];
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// m=<media> <port>/<number of ports> <transport> <fmt list>
|
|
75
|
-
const others = line.split(' ').slice(0, 3);
|
|
76
|
-
const formats = line.split(' ').slice(3);
|
|
77
|
-
formats.sort(function (x, y) { return x == formatNumber ? -1 : y == formatNumber ? 1 : 0; });
|
|
78
|
-
newLines.push(others.concat(formats).join(' '));
|
|
79
|
-
} else {
|
|
80
|
-
newLines.push(line);
|
|
81
|
-
}
|
|
72
|
+
if (line.indexOf('m=video') === 0) {
|
|
82
73
|
|
|
74
|
+
// m=<media> <port>/<number of ports> <transport> <fmt list>
|
|
75
|
+
const others = line.split(' ').slice(0, 3);
|
|
76
|
+
const formats = line.split(' ').slice(3);
|
|
77
|
+
formats.sort(function (x, y) { return x == formatNumber ? -1 : y == formatNumber ? 1 : 0; });
|
|
78
|
+
newLines.push(others.concat(formats).join(' '));
|
|
79
|
+
} else {
|
|
80
|
+
newLines.push(line);
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return newLines.join('\r\n') + '\r\n';
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function removeFormat(sdp, formatNumber) {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
let newLines = [];
|
|
90
|
+
let lines = sdp.split('\r\n');
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
for (let i = 0; i < lines.length; i++) {
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
if (lines[i].indexOf('m=video') === 0) {
|
|
95
|
+
newLines.push(lines[i].replace(' ' + formatNumber + '', ''));
|
|
96
|
+
} else if (lines[i].indexOf(formatNumber + '') > -1) {
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
98
|
+
} else {
|
|
99
|
+
newLines.push(lines[i]);
|
|
101
100
|
}
|
|
101
|
+
}
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
return newLines.join('\r\n')
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
async function getStreamForDeviceCheck(type) {
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
// High resolution video constraints makes browser to get maximum resolution of video device.
|
|
109
|
+
const constraints = {
|
|
110
|
+
};
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
if (type === 'both') {
|
|
113
|
+
constraints.audio = true;
|
|
114
|
+
constraints.video = true;
|
|
115
|
+
} else if (type === 'audio') {
|
|
116
|
+
constraints.audio = true;
|
|
117
|
+
} else if (type === 'video') {
|
|
118
|
+
constraints.video = true;
|
|
119
|
+
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
return await navigator.mediaDevices.getUserMedia(constraints);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
async function getDevices() {
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
return await navigator.mediaDevices.enumerateDevices();
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
function gotDevices(deviceInfos) {
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
let devices = {
|
|
132
|
+
'audioinput': [],
|
|
133
|
+
'audiooutput': [],
|
|
134
|
+
'videoinput': [],
|
|
135
|
+
'other': [],
|
|
136
|
+
};
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
for (let i = 0; i !== deviceInfos.length; ++i) {
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
const deviceInfo = deviceInfos[i];
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
let info = {};
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
info.deviceId = deviceInfo.deviceId;
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
if (deviceInfo.kind === 'audioinput') {
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
info.label = deviceInfo.label || `microphone ${devices.audioinput.length + 1}`;
|
|
149
|
+
devices.audioinput.push(info);
|
|
150
|
+
} else if (deviceInfo.kind === 'audiooutput') {
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
info.label = deviceInfo.label || `speaker ${devices.audiooutput.length + 1}`;
|
|
153
|
+
devices.audiooutput.push(info);
|
|
154
|
+
} else if (deviceInfo.kind === 'videoinput') {
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
info.label = deviceInfo.label || `camera ${devices.videoinput.length + 1}`;
|
|
157
|
+
devices.videoinput.push(info);
|
|
158
|
+
} else {
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
160
|
+
info.label = deviceInfo.label || `other ${devices.other.length + 1}`;
|
|
161
|
+
devices.other.push(info);
|
|
163
162
|
}
|
|
163
|
+
}
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
return devices;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
function initConfig(instance, options) {
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
instance.peerConnection = null;
|
|
173
|
-
instance.connectionConfig = {};
|
|
170
|
+
// webrtc or whip
|
|
171
|
+
instance.streamingMode = null;
|
|
174
172
|
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
instance.inputStream = null;
|
|
174
|
+
instance.webSocket = null;
|
|
175
|
+
instance.peerConnection = null;
|
|
176
|
+
instance.connectionConfig = {};
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
instance.videoElement = null;
|
|
179
|
+
instance.endpointUrl = null;
|
|
180
|
+
instance.resourceUrl = null;
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
if (options && options.callbacks) {
|
|
183
|
+
|
|
184
|
+
instance.callbacks = options.callbacks;
|
|
185
|
+
} else {
|
|
186
|
+
instance.callbacks = {};
|
|
187
|
+
}
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
function addMethod(instance) {
|
|
187
191
|
|
|
188
|
-
|
|
192
|
+
function errorHandler(error) {
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
if (instance.callbacks.error) {
|
|
191
195
|
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
instance.callbacks.error(error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function fetchWithRedirect(url, options) {
|
|
201
|
+
let fetched = await fetch(url, options);
|
|
202
|
+
|
|
203
|
+
while (fetched.redirected) {
|
|
204
|
+
url = fetched.url;
|
|
205
|
+
fetched = await fetch(url, options);
|
|
194
206
|
}
|
|
195
207
|
|
|
196
|
-
|
|
208
|
+
return fetched;
|
|
209
|
+
}
|
|
197
210
|
|
|
198
|
-
|
|
211
|
+
function getUserMedia(constraints) {
|
|
199
212
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
213
|
+
if (!constraints) {
|
|
214
|
+
|
|
215
|
+
constraints = {
|
|
216
|
+
video: {
|
|
217
|
+
deviceId: undefined
|
|
218
|
+
},
|
|
219
|
+
audio: {
|
|
220
|
+
deviceId: undefined
|
|
208
221
|
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
209
224
|
|
|
210
|
-
|
|
225
|
+
console.info(logHeader, 'Request Stream To Input Devices With Constraints', constraints);
|
|
211
226
|
|
|
212
|
-
|
|
213
|
-
|
|
227
|
+
return navigator.mediaDevices.getUserMedia(constraints)
|
|
228
|
+
.then(function (stream) {
|
|
214
229
|
|
|
215
|
-
|
|
230
|
+
console.info(logHeader, 'Received Media Stream From Input Device', stream);
|
|
216
231
|
|
|
217
|
-
|
|
232
|
+
instance.inputStream = stream;
|
|
218
233
|
|
|
219
|
-
|
|
234
|
+
let elem = instance.videoElement;
|
|
220
235
|
|
|
221
|
-
|
|
222
|
-
|
|
236
|
+
// Attach stream to video element when video element is provided.
|
|
237
|
+
if (elem) {
|
|
223
238
|
|
|
224
|
-
|
|
239
|
+
elem.srcObject = stream;
|
|
225
240
|
|
|
226
|
-
|
|
241
|
+
elem.onloadedmetadata = function (e) {
|
|
227
242
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
243
|
+
elem.play();
|
|
244
|
+
};
|
|
245
|
+
}
|
|
231
246
|
|
|
232
|
-
|
|
247
|
+
return new Promise(function (resolve) {
|
|
233
248
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
249
|
+
resolve(stream);
|
|
250
|
+
});
|
|
251
|
+
})
|
|
252
|
+
.catch(function (error) {
|
|
238
253
|
|
|
239
|
-
|
|
240
|
-
|
|
254
|
+
console.error(logHeader, 'Can\'t Get Media Stream From Input Device', error);
|
|
255
|
+
errorHandler(error);
|
|
241
256
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
257
|
+
return new Promise(function (resolve, reject) {
|
|
258
|
+
reject(error);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getDisplayMedia(constraints) {
|
|
264
|
+
|
|
265
|
+
if (!constraints) {
|
|
266
|
+
constraints = {};
|
|
246
267
|
}
|
|
247
268
|
|
|
248
|
-
|
|
269
|
+
console.info(logHeader, 'Request Stream To Display With Constraints', constraints);
|
|
270
|
+
|
|
271
|
+
return navigator.mediaDevices.getDisplayMedia(constraints)
|
|
272
|
+
.then(function (stream) {
|
|
273
|
+
|
|
274
|
+
console.info(logHeader, 'Received Media Stream From Display', stream);
|
|
275
|
+
|
|
276
|
+
instance.inputStream = stream;
|
|
277
|
+
|
|
278
|
+
let elem = instance.videoElement;
|
|
279
|
+
|
|
280
|
+
// Attach stream to video element when video element is provided.
|
|
281
|
+
if (elem) {
|
|
249
282
|
|
|
250
|
-
|
|
251
|
-
|
|
283
|
+
elem.srcObject = stream;
|
|
284
|
+
|
|
285
|
+
elem.onloadedmetadata = function (e) {
|
|
286
|
+
|
|
287
|
+
elem.play();
|
|
288
|
+
};
|
|
252
289
|
}
|
|
253
290
|
|
|
254
|
-
|
|
291
|
+
return new Promise(function (resolve) {
|
|
292
|
+
|
|
293
|
+
resolve(stream);
|
|
294
|
+
});
|
|
295
|
+
})
|
|
296
|
+
.catch(function (error) {
|
|
255
297
|
|
|
256
|
-
|
|
257
|
-
|
|
298
|
+
console.error(logHeader, 'Can\'t Get Media Stream From Display', error);
|
|
299
|
+
errorHandler(error);
|
|
258
300
|
|
|
259
|
-
|
|
301
|
+
return new Promise(function (resolve, reject) {
|
|
302
|
+
reject(error);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
260
306
|
|
|
261
|
-
|
|
307
|
+
function setMediaStream(stream) {
|
|
308
|
+
// Check if a valid stream is provided
|
|
309
|
+
if (!stream || !(stream instanceof MediaStream)) {
|
|
262
310
|
|
|
263
|
-
|
|
311
|
+
const error = new Error("Invalid MediaStream provided");
|
|
312
|
+
console.error(logHeader, 'Invalid MediaStream', error);
|
|
313
|
+
errorHandler(error);
|
|
264
314
|
|
|
265
|
-
|
|
266
|
-
|
|
315
|
+
return new Promise(function (resolve, reject) {
|
|
316
|
+
reject(error);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
267
319
|
|
|
268
|
-
|
|
320
|
+
console.info(logHeader, 'Received Media Stream', stream);
|
|
269
321
|
|
|
270
|
-
|
|
322
|
+
instance.inputStream = stream;
|
|
271
323
|
|
|
272
|
-
|
|
273
|
-
};
|
|
274
|
-
}
|
|
324
|
+
let elem = instance.videoElement;
|
|
275
325
|
|
|
276
|
-
|
|
326
|
+
// Attach stream to video element when video element is provided.
|
|
327
|
+
if (elem) {
|
|
328
|
+
elem.srcObject = stream;
|
|
277
329
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
330
|
+
elem.onloadedmetadata = function (e) {
|
|
331
|
+
elem.play();
|
|
332
|
+
};
|
|
333
|
+
}
|
|
282
334
|
|
|
283
|
-
|
|
284
|
-
|
|
335
|
+
return new Promise(function (resolve) {
|
|
336
|
+
resolve(stream);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
285
339
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
340
|
+
// From https://webrtchacks.com/limit-webrtc-bandwidth-sdp/
|
|
341
|
+
function setBitrateLimit(sdp, media, bitrate) {
|
|
342
|
+
|
|
343
|
+
let lines = sdp.split('\r\n');
|
|
344
|
+
let line = -1;
|
|
345
|
+
|
|
346
|
+
for (let i = 0; i < lines.length; i++) {
|
|
347
|
+
if (lines[i].indexOf('m=' + media) === 0) {
|
|
348
|
+
line = i;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (line === -1) {
|
|
353
|
+
// Could not find the m line for media
|
|
354
|
+
return sdp;
|
|
290
355
|
}
|
|
291
356
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return new Promise(function (resolve, reject) {
|
|
301
|
-
reject(error);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
console.info(logHeader, 'Received Media Stream', stream);
|
|
306
|
-
|
|
307
|
-
instance.inputStream = stream;
|
|
308
|
-
|
|
309
|
-
let elem = instance.videoElement;
|
|
310
|
-
|
|
311
|
-
// Attach stream to video element when video element is provided.
|
|
312
|
-
if (elem) {
|
|
313
|
-
elem.srcObject = stream;
|
|
314
|
-
|
|
315
|
-
elem.onloadedmetadata = function (e) {
|
|
316
|
-
elem.play();
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return new Promise(function (resolve) {
|
|
321
|
-
resolve(stream);
|
|
322
|
-
});
|
|
357
|
+
// Pass the m line
|
|
358
|
+
line++;
|
|
359
|
+
|
|
360
|
+
// Skip i and c lines
|
|
361
|
+
while (lines[line].indexOf('i=') === 0 || lines[line].indexOf('c=') === 0) {
|
|
362
|
+
|
|
363
|
+
line++;
|
|
323
364
|
}
|
|
324
365
|
|
|
325
|
-
//
|
|
326
|
-
|
|
366
|
+
// If we're on a b line, replace it
|
|
367
|
+
if (lines[line].indexOf('b') === 0) {
|
|
327
368
|
|
|
328
|
-
|
|
329
|
-
let line = -1;
|
|
369
|
+
lines[line] = 'b=AS:' + bitrate;
|
|
330
370
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
line = i;
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (line === -1) {
|
|
338
|
-
// Could not find the m line for media
|
|
339
|
-
return sdp;
|
|
340
|
-
}
|
|
371
|
+
return lines.join('\r\n');
|
|
372
|
+
}
|
|
341
373
|
|
|
342
|
-
|
|
343
|
-
|
|
374
|
+
// Add a new b line
|
|
375
|
+
let newLines = lines.slice(0, line)
|
|
344
376
|
|
|
345
|
-
|
|
346
|
-
|
|
377
|
+
newLines.push('b=AS:' + bitrate)
|
|
378
|
+
newLines = newLines.concat(lines.slice(line, lines.length))
|
|
347
379
|
|
|
348
|
-
|
|
349
|
-
|
|
380
|
+
return newLines.join('\r\n')
|
|
381
|
+
}
|
|
350
382
|
|
|
351
|
-
|
|
352
|
-
if (lines[line].indexOf('b') === 0) {
|
|
383
|
+
function initWebSocket(endpointUrl) {
|
|
353
384
|
|
|
354
|
-
|
|
385
|
+
if (!endpointUrl) {
|
|
386
|
+
errorHandler('endpointUrl is required');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
355
389
|
|
|
356
|
-
|
|
357
|
-
}
|
|
390
|
+
let webSocket = null;
|
|
358
391
|
|
|
359
|
-
|
|
360
|
-
let newLines = lines.slice(0, line)
|
|
392
|
+
try {
|
|
361
393
|
|
|
362
|
-
|
|
363
|
-
|
|
394
|
+
webSocket = new WebSocket(endpointUrl);
|
|
395
|
+
} catch (error) {
|
|
364
396
|
|
|
365
|
-
|
|
397
|
+
errorHandler(error);
|
|
366
398
|
}
|
|
367
399
|
|
|
368
|
-
function initWebSocket(connectionUrl) {
|
|
369
400
|
|
|
370
|
-
|
|
371
|
-
errorHandler('connectionUrl is required');
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
401
|
+
instance.webSocket = webSocket;
|
|
374
402
|
|
|
375
|
-
|
|
403
|
+
webSocket.onopen = function () {
|
|
376
404
|
|
|
377
|
-
|
|
405
|
+
// Request offer at the first time.
|
|
406
|
+
sendMessage(webSocket, {
|
|
407
|
+
command: 'request_offer'
|
|
408
|
+
});
|
|
409
|
+
};
|
|
378
410
|
|
|
379
|
-
|
|
411
|
+
webSocket.onmessage = function (e) {
|
|
380
412
|
|
|
381
|
-
|
|
382
|
-
} catch (error) {
|
|
413
|
+
let message = JSON.parse(e.data);
|
|
383
414
|
|
|
384
|
-
|
|
415
|
+
if (message.error) {
|
|
416
|
+
console.error('webSocket.onmessage', message.error);
|
|
417
|
+
errorHandler(message.error);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (message.command === 'offer') {
|
|
421
|
+
|
|
422
|
+
// OME returns offer. Start create peer connection.
|
|
423
|
+
createPeerConnection(
|
|
424
|
+
message.id,
|
|
425
|
+
message.peer_id,
|
|
426
|
+
message.sdp,
|
|
427
|
+
message.candidates,
|
|
428
|
+
message.ice_servers
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
webSocket.onerror = function (error) {
|
|
434
|
+
|
|
435
|
+
console.error('webSocket.onerror', error);
|
|
436
|
+
errorHandler(error);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
webSocket.onclose = function (e) {
|
|
440
|
+
|
|
441
|
+
if (!instance.webSocketClosedByUser) {
|
|
442
|
+
|
|
443
|
+
if (instance.callbacks.connectionClosed) {
|
|
444
|
+
instance.callbacks.connectionClosed('websocket', e);
|
|
385
445
|
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
386
448
|
|
|
449
|
+
}
|
|
387
450
|
|
|
388
|
-
|
|
451
|
+
async function startWhip(endpointUrl) {
|
|
389
452
|
|
|
390
|
-
|
|
453
|
+
if (instance.peerConnection) {
|
|
454
|
+
console.error('Connection already established');
|
|
455
|
+
errorHandler('Connection already established');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const peerConnectionConfig = {
|
|
460
|
+
bundlePolicy: "max-bundle"
|
|
461
|
+
};
|
|
391
462
|
|
|
392
|
-
|
|
393
|
-
sendMessage(webSocket, {
|
|
394
|
-
command: 'request_offer'
|
|
395
|
-
});
|
|
396
|
-
};
|
|
463
|
+
if (instance.connectionConfig.iceServers) {
|
|
397
464
|
|
|
398
|
-
|
|
465
|
+
// first priority using ice servers from local config.
|
|
466
|
+
peerConnectionConfig.iceServers = instance.connectionConfig.iceServers;
|
|
399
467
|
|
|
400
|
-
|
|
468
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
401
469
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
470
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
// last priority using default ice servers.
|
|
406
474
|
|
|
407
|
-
|
|
475
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
408
476
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
message.peer_id,
|
|
413
|
-
message.sdp,
|
|
414
|
-
message.candidates,
|
|
415
|
-
message.ice_servers
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
};
|
|
477
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
419
480
|
|
|
420
|
-
|
|
481
|
+
console.info(logHeader, 'Create Peer Connection With Config', peerConnectionConfig);
|
|
421
482
|
|
|
422
|
-
|
|
423
|
-
errorHandler(error);
|
|
424
|
-
};
|
|
483
|
+
const peerConnection = new RTCPeerConnection(peerConnectionConfig);
|
|
425
484
|
|
|
426
|
-
|
|
485
|
+
instance.peerConnection = peerConnection;
|
|
427
486
|
|
|
428
|
-
|
|
487
|
+
if (!instance.inputStream) {
|
|
488
|
+
console.error('No input stream in OvenLiveKit');
|
|
489
|
+
errorHandler('No input stream in OvenLiveKit');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
429
492
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
493
|
+
for (const track of instance.inputStream.getTracks()) {
|
|
494
|
+
console.log(logHeader, 'Adding track: ', track);
|
|
495
|
+
|
|
496
|
+
const transceiverConfig = {
|
|
497
|
+
direction: 'sendonly'
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// Add simulcast layers if configured
|
|
501
|
+
const simulcastConfig = instance.connectionConfig.simulcast;
|
|
435
502
|
|
|
503
|
+
if (track.kind === 'video' && simulcastConfig && simulcastConfig.length > 0) {
|
|
504
|
+
|
|
505
|
+
transceiverConfig.sendEncodings = [];
|
|
506
|
+
|
|
507
|
+
for (let i = 0; i < simulcastConfig.length; i++) {
|
|
508
|
+
|
|
509
|
+
const layer = {
|
|
510
|
+
rid: i,
|
|
511
|
+
active: true,
|
|
512
|
+
...simulcastConfig[i]
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
console.log(logHeader, `Adding simulcast layer to: ${track.kind}`, layer);
|
|
516
|
+
|
|
517
|
+
transceiverConfig.sendEncodings.push(layer);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
peerConnection.addTransceiver(track, transceiverConfig);
|
|
436
522
|
}
|
|
437
523
|
|
|
438
|
-
function
|
|
524
|
+
peerConnection.oniceconnectionstatechange = function (e) {
|
|
525
|
+
|
|
526
|
+
let state = peerConnection.iceConnectionState;
|
|
439
527
|
|
|
440
|
-
|
|
528
|
+
if (instance.callbacks.iceStateChange) {
|
|
441
529
|
|
|
442
|
-
|
|
443
|
-
|
|
530
|
+
console.info(logHeader, 'ICE State', '[' + state + ']');
|
|
531
|
+
instance.callbacks.iceStateChange(state);
|
|
532
|
+
}
|
|
444
533
|
|
|
445
|
-
|
|
534
|
+
if (state === 'connected') {
|
|
446
535
|
|
|
447
|
-
|
|
536
|
+
if (instance.callbacks.connected) {
|
|
537
|
+
instance.callbacks.connected(e);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
448
540
|
|
|
449
|
-
|
|
541
|
+
if (state === 'failed') {
|
|
450
542
|
|
|
451
|
-
|
|
543
|
+
if (instance.callbacks.connectionClosed) {
|
|
544
|
+
console.error(logHeader, 'Ice connection failed', e);
|
|
545
|
+
instance.callbacks.errorHandler(e);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
452
548
|
|
|
453
|
-
|
|
454
|
-
}
|
|
549
|
+
if (state === 'disconnected' || state === 'closed') {
|
|
455
550
|
|
|
456
|
-
|
|
457
|
-
|
|
551
|
+
if (instance.callbacks.connectionClosed) {
|
|
552
|
+
console.error(logHeader, 'Ice connection disconnected or closed', e);
|
|
553
|
+
instance.callbacks.connectionClosed('ice', e);
|
|
458
554
|
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
459
557
|
|
|
460
|
-
|
|
558
|
+
const offer = await peerConnection.createOffer();
|
|
559
|
+
console.log(logHeader, 'Offer SDP: ', offer.sdp);
|
|
461
560
|
|
|
462
|
-
|
|
561
|
+
if (instance.connectionConfig.maxVideoBitrate) {
|
|
463
562
|
|
|
464
|
-
|
|
563
|
+
// if bandwidth limit is set. modify sdp from ome to limit acceptable bandwidth of ome
|
|
564
|
+
offer.sdp = setBitrateLimit(offer.sdp, 'video', instance.connectionConfig.maxVideoBitrate);
|
|
565
|
+
}
|
|
465
566
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
567
|
+
if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {
|
|
568
|
+
|
|
569
|
+
offer.sdp = appendFmtp(offer.sdp);
|
|
570
|
+
}
|
|
471
571
|
|
|
472
|
-
|
|
572
|
+
if (instance.connectionConfig.preferredVideoFormat) {
|
|
573
|
+
offer.sdp = setPreferredVideoFormat(offer.sdp, instance.connectionConfig.preferredVideoFormat);
|
|
574
|
+
} else {
|
|
575
|
+
// default to H264
|
|
576
|
+
offer.sdp = setPreferredVideoFormat(offer.sdp, 'H264');
|
|
577
|
+
}
|
|
473
578
|
|
|
474
|
-
|
|
579
|
+
const headers = {
|
|
580
|
+
"Content-Type": "application/sdp"
|
|
581
|
+
};
|
|
475
582
|
|
|
476
|
-
|
|
583
|
+
if (instance.connectionConfig.httpHeaders) {
|
|
584
|
+
Object.assign(headers, instance.connectionConfig.httpHeaders);
|
|
585
|
+
}
|
|
477
586
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
587
|
+
const fetched = await fetchWithRedirect(endpointUrl, {
|
|
588
|
+
method: "POST",
|
|
589
|
+
body: offer.sdp,
|
|
590
|
+
headers
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (!fetched.ok) {
|
|
594
|
+
console.error('Failed to fetch', fetched.status);
|
|
595
|
+
errorHandler(`Failed to fetch ${fetched.status}`);
|
|
596
|
+
closePeerConnection();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (!fetched.headers.get("location")) {
|
|
601
|
+
console.error('No location header on answer response');
|
|
602
|
+
errorHandler('No location header on answer response');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// update endpointUrl
|
|
607
|
+
instance.endpointUrl = fetched.url;
|
|
608
|
+
console.log(logHeader, 'Updated endpointUrl: ', instance.endpointUrl);
|
|
609
|
+
|
|
610
|
+
const baseUrl = new URL(endpointUrl).origin;
|
|
611
|
+
instance.resourceUrl = baseUrl + fetched.headers.get("location");
|
|
612
|
+
|
|
613
|
+
const answer = await fetched.text();
|
|
614
|
+
console.log(logHeader, 'Answer SDP: ', answer);
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
await peerConnection.setLocalDescription(offer);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
console.error('peerConnection.setLocalDescription', error);
|
|
620
|
+
errorHandler(error);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
await peerConnection.setRemoteDescription({
|
|
625
|
+
type: "answer",
|
|
626
|
+
sdp: answer
|
|
627
|
+
});
|
|
628
|
+
} catch (error) {
|
|
629
|
+
console.error('peerConnection.setRemoteDescription', error);
|
|
630
|
+
errorHandler(error);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function stopWhip() {
|
|
635
|
+
|
|
636
|
+
if (!instance.peerConnection) {
|
|
637
|
+
console.error('No connection to close');
|
|
638
|
+
errorHandler('No connection to close');
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
closePeerConnection();
|
|
643
|
+
|
|
644
|
+
if (instance.resourceUrl) {
|
|
645
|
+
|
|
646
|
+
const headers = {
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
if (instance.connectionConfig.httpHeaders) {
|
|
650
|
+
Object.assign(headers, instance.connectionConfig.httpHeaders);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
await fetchWithRedirect(instance.resourceUrl, {
|
|
654
|
+
method: "DELETE",
|
|
655
|
+
headers
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function appendFmtp(sdp) {
|
|
661
|
+
|
|
662
|
+
const fmtpStr = instance.connectionConfig.sdp.appendFmtp;
|
|
663
|
+
|
|
664
|
+
const lines = sdp.split('\r\n');
|
|
665
|
+
const payloads = [];
|
|
666
|
+
|
|
667
|
+
for (let i = 0; i < lines.length; i++) {
|
|
668
|
+
|
|
669
|
+
if (lines[i].indexOf('m=video') === 0) {
|
|
670
|
+
|
|
671
|
+
let tokens = lines[i].split(' ')
|
|
672
|
+
|
|
673
|
+
for (let j = 3; j < tokens.length; j++) {
|
|
674
|
+
|
|
675
|
+
payloads.push(tokens[j]);
|
|
482
676
|
}
|
|
483
677
|
|
|
484
|
-
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
485
680
|
}
|
|
486
681
|
|
|
487
|
-
|
|
682
|
+
for (let i = 0; i < payloads.length; i++) {
|
|
488
683
|
|
|
489
|
-
|
|
490
|
-
const payloads = [];
|
|
684
|
+
let fmtpLineFound = false;
|
|
491
685
|
|
|
492
|
-
|
|
686
|
+
for (let j = 0; j < lines.length; j++) {
|
|
493
687
|
|
|
494
|
-
|
|
688
|
+
if (lines[j].indexOf('a=fmtp:' + payloads[i]) === 0) {
|
|
689
|
+
fmtpLineFound = true;
|
|
690
|
+
lines[j] += ';' + fmtpStr;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
495
693
|
|
|
496
|
-
|
|
694
|
+
if (!fmtpLineFound) {
|
|
497
695
|
|
|
498
|
-
|
|
696
|
+
for (let j = 0; j < lines.length; j++) {
|
|
499
697
|
|
|
500
|
-
|
|
501
|
-
}
|
|
698
|
+
if (lines[j].indexOf('a=rtpmap:' + payloads[i]) === 0) {
|
|
502
699
|
|
|
503
|
-
|
|
504
|
-
|
|
700
|
+
lines[j] += '\r\na=fmtp:' + payloads[i] + ' ' + fmtpStr;
|
|
701
|
+
}
|
|
505
702
|
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
506
705
|
|
|
507
|
-
|
|
706
|
+
return lines.join('\r\n')
|
|
707
|
+
}
|
|
508
708
|
|
|
509
|
-
|
|
709
|
+
function appendOrientation(sdp) {
|
|
510
710
|
|
|
511
|
-
|
|
711
|
+
const lines = sdp.split('\r\n');
|
|
712
|
+
const payloads = [];
|
|
512
713
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
714
|
+
for (let i = 0; i < lines.length; i++) {
|
|
715
|
+
|
|
716
|
+
if (lines[i].indexOf('m=video') === 0) {
|
|
717
|
+
|
|
718
|
+
let tokens = lines[i].split(' ')
|
|
719
|
+
|
|
720
|
+
for (let j = 3; j < tokens.length; j++) {
|
|
721
|
+
|
|
722
|
+
payloads.push(tokens[j]);
|
|
516
723
|
}
|
|
517
724
|
|
|
518
|
-
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
519
727
|
}
|
|
520
728
|
|
|
521
|
-
|
|
729
|
+
for (let i = 0; i < payloads.length; i++) {
|
|
522
730
|
|
|
523
|
-
|
|
731
|
+
for (let j = 0; j < lines.length; j++) {
|
|
524
732
|
|
|
525
|
-
if (
|
|
733
|
+
if (lines[j].indexOf('a=rtpmap:' + payloads[i]) === 0) {
|
|
526
734
|
|
|
527
|
-
|
|
528
|
-
|
|
735
|
+
lines[j] += '\r\na=extmap:' + payloads[i] + ' urn:3gpp:video-orientation';
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
529
739
|
|
|
530
|
-
|
|
740
|
+
return lines.join('\r\n')
|
|
741
|
+
}
|
|
531
742
|
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
} else if (iceServers) {
|
|
743
|
+
function createPeerConnection(id, peerId, offer, candidates, iceServers) {
|
|
535
744
|
|
|
536
|
-
|
|
537
|
-
peerConnectionConfig.iceServers = [];
|
|
745
|
+
let peerConnectionConfig = {};
|
|
538
746
|
|
|
539
|
-
|
|
747
|
+
if (instance.connectionConfig.iceServers) {
|
|
540
748
|
|
|
541
|
-
|
|
749
|
+
// first priority using ice servers from local config.
|
|
750
|
+
peerConnectionConfig.iceServers = instance.connectionConfig.iceServers;
|
|
542
751
|
|
|
543
|
-
|
|
752
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
544
753
|
|
|
545
|
-
|
|
754
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
755
|
+
}
|
|
756
|
+
} else if (iceServers) {
|
|
546
757
|
|
|
547
|
-
|
|
548
|
-
|
|
758
|
+
// second priority using ice servers from ome and force using TCP
|
|
759
|
+
peerConnectionConfig.iceServers = [];
|
|
549
760
|
|
|
550
|
-
|
|
761
|
+
for (let i = 0; i < iceServers.length; i++) {
|
|
551
762
|
|
|
552
|
-
|
|
763
|
+
let iceServer = iceServers[i];
|
|
553
764
|
|
|
554
|
-
|
|
555
|
-
hasWebSocketUrl = true;
|
|
556
|
-
break;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
765
|
+
let regIceServer = {};
|
|
559
766
|
|
|
560
|
-
|
|
767
|
+
regIceServer.urls = iceServer.urls;
|
|
561
768
|
|
|
562
|
-
|
|
769
|
+
let hasWebSocketUrl = false;
|
|
770
|
+
let webSocketUrl = generateDomainFromUrl(instance.endpointUrl);
|
|
563
771
|
|
|
564
|
-
|
|
565
|
-
let ip = findIp(cloneIceServer);
|
|
772
|
+
for (let j = 0; j < regIceServer.urls.length; j++) {
|
|
566
773
|
|
|
567
|
-
|
|
568
|
-
regIceServer.urls.push(cloneIceServer.replace(ip, webSocketUrl));
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
774
|
+
let serverUrl = regIceServer.urls[j];
|
|
572
775
|
|
|
573
|
-
|
|
574
|
-
|
|
776
|
+
if (serverUrl.indexOf(webSocketUrl) > -1) {
|
|
777
|
+
hasWebSocketUrl = true;
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
575
781
|
|
|
576
|
-
|
|
577
|
-
}
|
|
782
|
+
if (!hasWebSocketUrl) {
|
|
578
783
|
|
|
579
|
-
|
|
580
|
-
} else {
|
|
581
|
-
// last priority using default ice servers.
|
|
784
|
+
if (regIceServer.urls.length > 0) {
|
|
582
785
|
|
|
583
|
-
|
|
786
|
+
let cloneIceServer = regIceServer.urls[0];
|
|
787
|
+
let ip = findIp(cloneIceServer);
|
|
584
788
|
|
|
585
|
-
|
|
789
|
+
if (webSocketUrl && ip) {
|
|
790
|
+
regIceServer.urls.push(cloneIceServer.replace(ip, webSocketUrl));
|
|
586
791
|
}
|
|
792
|
+
}
|
|
587
793
|
}
|
|
588
794
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
{
|
|
592
|
-
googHighStartBitrate: {
|
|
593
|
-
exact: !0
|
|
594
|
-
}
|
|
595
|
-
},
|
|
596
|
-
{
|
|
597
|
-
googPayloadPadding: {
|
|
598
|
-
exact: !0
|
|
599
|
-
}
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
googScreencastMinBitrate: {
|
|
603
|
-
exact: 500
|
|
604
|
-
}
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
enableDscp: {
|
|
608
|
-
exact: true
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
]
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
console.info(logHeader, 'Create Peer Connection With Config', peerConnectionConfig);
|
|
615
|
-
|
|
616
|
-
let peerConnection = new RTCPeerConnection(peerConnectionConfig);
|
|
617
|
-
|
|
618
|
-
instance.peerConnection = peerConnection;
|
|
619
|
-
|
|
620
|
-
// set local stream
|
|
621
|
-
instance.inputStream.getTracks().forEach(function (track) {
|
|
622
|
-
|
|
623
|
-
console.info(logHeader, 'Add Track To Peer Connection', track);
|
|
624
|
-
peerConnection.addTrack(track, instance.inputStream);
|
|
625
|
-
});
|
|
795
|
+
regIceServer.username = iceServer.user_name;
|
|
796
|
+
regIceServer.credential = iceServer.credential;
|
|
626
797
|
|
|
627
|
-
|
|
798
|
+
peerConnectionConfig.iceServers.push(regIceServer);
|
|
799
|
+
}
|
|
628
800
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
801
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
802
|
+
|
|
803
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
804
|
+
} else {
|
|
805
|
+
peerConnectionConfig.iceTransportPolicy = 'relay';
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
// last priority using default ice servers.
|
|
632
809
|
|
|
633
|
-
|
|
810
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
634
811
|
|
|
635
|
-
|
|
636
|
-
|
|
812
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
637
815
|
|
|
638
|
-
|
|
639
|
-
|
|
816
|
+
let advancedSetting = {
|
|
817
|
+
optional: [
|
|
818
|
+
{
|
|
819
|
+
googHighStartBitrate: {
|
|
820
|
+
exact: !0
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
googPayloadPadding: {
|
|
825
|
+
exact: !0
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
googScreencastMinBitrate: {
|
|
830
|
+
exact: 500
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
enableDscp: {
|
|
835
|
+
exact: true
|
|
836
|
+
}
|
|
640
837
|
}
|
|
838
|
+
]
|
|
839
|
+
};
|
|
641
840
|
|
|
841
|
+
console.info(logHeader, 'Create Peer Connection With Config', peerConnectionConfig);
|
|
642
842
|
|
|
643
|
-
|
|
644
|
-
console.info(logHeader, 'Modified offer sdp\n\n' + offer.sdp);
|
|
843
|
+
let peerConnection = new RTCPeerConnection(peerConnectionConfig);
|
|
645
844
|
|
|
646
|
-
|
|
647
|
-
.then(function () {
|
|
845
|
+
instance.peerConnection = peerConnection;
|
|
648
846
|
|
|
649
|
-
|
|
650
|
-
|
|
847
|
+
// set local stream
|
|
848
|
+
instance.inputStream.getTracks().forEach(function (track) {
|
|
651
849
|
|
|
652
|
-
|
|
850
|
+
console.info(logHeader, 'Add Track To Peer Connection', track);
|
|
851
|
+
peerConnection.addTrack(track, instance.inputStream);
|
|
852
|
+
});
|
|
653
853
|
|
|
654
|
-
|
|
655
|
-
|
|
854
|
+
if (instance.connectionConfig.maxVideoBitrate) {
|
|
855
|
+
|
|
856
|
+
// if bandwith limit is set. modify sdp from ome to limit acceptable bandwidth of ome
|
|
857
|
+
offer.sdp = setBitrateLimit(offer.sdp, 'video', instance.connectionConfig.maxVideoBitrate);
|
|
858
|
+
}
|
|
656
859
|
|
|
657
|
-
|
|
658
|
-
answer.sdp = setPreferredVideoFormat(answer.sdp, instance.connectionConfig.preferredVideoFormat)
|
|
659
|
-
}
|
|
860
|
+
if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {
|
|
660
861
|
|
|
661
|
-
|
|
862
|
+
offer.sdp = appendFmtp(offer.sdp);
|
|
863
|
+
}
|
|
662
864
|
|
|
663
|
-
|
|
664
|
-
|
|
865
|
+
if (instance.connectionConfig.preferredVideoFormat) {
|
|
866
|
+
offer.sdp = setPreferredVideoFormat(offer.sdp, instance.connectionConfig.preferredVideoFormat)
|
|
867
|
+
}
|
|
665
868
|
|
|
666
|
-
sendMessage(instance.webSocket, {
|
|
667
|
-
id: id,
|
|
668
|
-
peer_id: peerId,
|
|
669
|
-
command: 'answer',
|
|
670
|
-
sdp: answer
|
|
671
|
-
});
|
|
672
|
-
})
|
|
673
|
-
.catch(function (error) {
|
|
674
869
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
});
|
|
678
|
-
})
|
|
679
|
-
.catch(function (error) {
|
|
870
|
+
// offer.sdp = appendOrientation(offer.sdp);
|
|
871
|
+
console.info(logHeader, 'Modified offer sdp\n\n' + offer.sdp);
|
|
680
872
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
});
|
|
684
|
-
})
|
|
685
|
-
.catch(function (error) {
|
|
873
|
+
peerConnection.setRemoteDescription(new RTCSessionDescription(offer))
|
|
874
|
+
.then(function () {
|
|
686
875
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
});
|
|
876
|
+
peerConnection.createAnswer()
|
|
877
|
+
.then(function (answer) {
|
|
690
878
|
|
|
691
|
-
|
|
879
|
+
if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {
|
|
692
880
|
|
|
693
|
-
|
|
694
|
-
|
|
881
|
+
answer.sdp = appendFmtp(answer.sdp);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (instance.connectionConfig.preferredVideoFormat) {
|
|
885
|
+
answer.sdp = setPreferredVideoFormat(answer.sdp, instance.connectionConfig.preferredVideoFormat)
|
|
886
|
+
}
|
|
695
887
|
|
|
696
|
-
|
|
888
|
+
console.info(logHeader, 'Modified answer sdp\n\n' + answer.sdp);
|
|
697
889
|
|
|
698
|
-
|
|
890
|
+
peerConnection.setLocalDescription(answer)
|
|
891
|
+
.then(function () {
|
|
699
892
|
|
|
700
893
|
sendMessage(instance.webSocket, {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
894
|
+
id: id,
|
|
895
|
+
peer_id: peerId,
|
|
896
|
+
command: 'answer',
|
|
897
|
+
sdp: answer
|
|
705
898
|
});
|
|
706
|
-
|
|
707
|
-
|
|
899
|
+
})
|
|
900
|
+
.catch(function (error) {
|
|
708
901
|
|
|
709
|
-
|
|
902
|
+
console.error('peerConnection.setLocalDescription', error);
|
|
903
|
+
errorHandler(error);
|
|
904
|
+
});
|
|
905
|
+
})
|
|
906
|
+
.catch(function (error) {
|
|
710
907
|
|
|
711
|
-
|
|
908
|
+
console.error('peerConnection.createAnswer', error);
|
|
909
|
+
errorHandler(error);
|
|
910
|
+
});
|
|
911
|
+
})
|
|
912
|
+
.catch(function (error) {
|
|
712
913
|
|
|
713
|
-
|
|
914
|
+
console.error('peerConnection.setRemoteDescription', error);
|
|
915
|
+
errorHandler(error);
|
|
916
|
+
});
|
|
714
917
|
|
|
715
|
-
|
|
716
|
-
instance.callbacks.iceStateChange(state);
|
|
717
|
-
}
|
|
918
|
+
if (candidates) {
|
|
718
919
|
|
|
719
|
-
|
|
920
|
+
addIceCandidate(peerConnection, candidates);
|
|
921
|
+
}
|
|
720
922
|
|
|
721
|
-
|
|
722
|
-
instance.callbacks.connected(e);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
923
|
+
peerConnection.onicecandidate = function (e) {
|
|
725
924
|
|
|
726
|
-
|
|
925
|
+
if (e.candidate && e.candidate.candidate) {
|
|
727
926
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
927
|
+
sendMessage(instance.webSocket, {
|
|
928
|
+
id: id,
|
|
929
|
+
peer_id: peerId,
|
|
930
|
+
command: 'candidate',
|
|
931
|
+
candidates: [e.candidate]
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
peerConnection.oniceconnectionstatechange = function (e) {
|
|
937
|
+
|
|
938
|
+
let state = peerConnection.iceConnectionState;
|
|
939
|
+
|
|
940
|
+
if (instance.callbacks.iceStateChange) {
|
|
941
|
+
|
|
942
|
+
console.info(logHeader, 'ICE State', '[' + state + ']');
|
|
943
|
+
instance.callbacks.iceStateChange(state);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (state === 'connected') {
|
|
947
|
+
|
|
948
|
+
if (instance.callbacks.connected) {
|
|
949
|
+
instance.callbacks.connected(e);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (state === 'failed' || state === 'disconnected' || state === 'closed') {
|
|
954
|
+
|
|
955
|
+
if (instance.callbacks.connectionClosed) {
|
|
956
|
+
console.error(logHeader, 'Ice connection closed', e);
|
|
957
|
+
instance.callbacks.connectionClosed('ice', e);
|
|
733
958
|
}
|
|
959
|
+
}
|
|
734
960
|
}
|
|
961
|
+
}
|
|
735
962
|
|
|
736
|
-
|
|
963
|
+
function addIceCandidate(peerConnection, candidates) {
|
|
737
964
|
|
|
738
|
-
|
|
965
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
739
966
|
|
|
740
|
-
|
|
967
|
+
if (candidates[i] && candidates[i].candidate) {
|
|
741
968
|
|
|
742
|
-
|
|
969
|
+
let basicCandidate = candidates[i];
|
|
743
970
|
|
|
744
|
-
|
|
745
|
-
|
|
971
|
+
peerConnection.addIceCandidate(new RTCIceCandidate(basicCandidate))
|
|
972
|
+
.then(function () {
|
|
746
973
|
|
|
747
|
-
|
|
748
|
-
|
|
974
|
+
})
|
|
975
|
+
.catch(function (error) {
|
|
749
976
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
977
|
+
console.error('peerConnection.addIceCandidate', error);
|
|
978
|
+
errorHandler(error);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
755
981
|
}
|
|
982
|
+
}
|
|
756
983
|
|
|
757
|
-
|
|
758
|
-
|
|
984
|
+
function closePeerConnection() {
|
|
985
|
+
if (instance.peerConnection) {
|
|
759
986
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
987
|
+
// remove tracks from peer connection
|
|
988
|
+
instance.peerConnection.getSenders().forEach(function (sender) {
|
|
989
|
+
instance.peerConnection.removeTrack(sender);
|
|
990
|
+
});
|
|
764
991
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
992
|
+
instance.peerConnection.close();
|
|
993
|
+
instance.peerConnection = null;
|
|
994
|
+
delete instance.peerConnection;
|
|
769
995
|
}
|
|
996
|
+
}
|
|
770
997
|
|
|
771
|
-
|
|
998
|
+
function closeWebSocket() {
|
|
772
999
|
|
|
773
|
-
|
|
1000
|
+
if (instance.webSocket) {
|
|
774
1001
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
1002
|
+
instance.webSocket.close();
|
|
1003
|
+
instance.webSocket = null;
|
|
1004
|
+
delete instance.webSocket;
|
|
779
1005
|
}
|
|
1006
|
+
}
|
|
780
1007
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1008
|
+
function closeInputStream() {
|
|
1009
|
+
// release video, audio stream
|
|
1010
|
+
if (instance.inputStream) {
|
|
784
1011
|
|
|
785
|
-
|
|
1012
|
+
instance.inputStream.getTracks().forEach(track => {
|
|
786
1013
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1014
|
+
track.stop();
|
|
1015
|
+
instance.inputStream.removeTrack(track);
|
|
1016
|
+
});
|
|
790
1017
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1018
|
+
if (instance.videoElement) {
|
|
1019
|
+
instance.videoElement.srcObject = null;
|
|
1020
|
+
}
|
|
794
1021
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
1022
|
+
instance.inputStream = null;
|
|
1023
|
+
delete instance.inputStream;
|
|
798
1024
|
}
|
|
1025
|
+
}
|
|
799
1026
|
|
|
800
|
-
|
|
801
|
-
|
|
1027
|
+
// instance methods
|
|
1028
|
+
instance.attachMedia = function (videoElement) {
|
|
802
1029
|
|
|
803
|
-
|
|
804
|
-
|
|
1030
|
+
instance.videoElement = videoElement;
|
|
1031
|
+
};
|
|
805
1032
|
|
|
806
|
-
|
|
1033
|
+
instance.getUserMedia = function (constraints) {
|
|
807
1034
|
|
|
808
|
-
|
|
809
|
-
|
|
1035
|
+
return getUserMedia(constraints);
|
|
1036
|
+
};
|
|
810
1037
|
|
|
811
|
-
|
|
1038
|
+
instance.getDisplayMedia = function (constraints) {
|
|
812
1039
|
|
|
813
|
-
|
|
814
|
-
|
|
1040
|
+
return getDisplayMedia(constraints);
|
|
1041
|
+
};
|
|
815
1042
|
|
|
816
|
-
|
|
1043
|
+
instance.setMediaStream = function (stream) {
|
|
817
1044
|
|
|
818
|
-
|
|
819
|
-
|
|
1045
|
+
return setMediaStream(stream);
|
|
1046
|
+
};
|
|
820
1047
|
|
|
821
|
-
|
|
1048
|
+
instance.startStreaming = function (endpointUrl, connectionConfig) {
|
|
822
1049
|
|
|
823
|
-
|
|
1050
|
+
console.info(logEventHeader, `Start Streaming to ${endpointUrl} with connectionConfig`, connectionConfig);
|
|
824
1051
|
|
|
825
|
-
|
|
1052
|
+
if (!endpointUrl) {
|
|
1053
|
+
console.error('endpointUrl is required');
|
|
1054
|
+
errorHandler('endpointUrl is required');
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
826
1057
|
|
|
827
|
-
|
|
828
|
-
}
|
|
1058
|
+
instance.endpointUrl = endpointUrl;
|
|
829
1059
|
|
|
830
|
-
|
|
831
|
-
|
|
1060
|
+
if (connectionConfig) {
|
|
1061
|
+
instance.connectionConfig = connectionConfig;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
try {
|
|
832
1065
|
|
|
833
|
-
|
|
1066
|
+
const protocol = new URL(endpointUrl).protocol;
|
|
834
1067
|
|
|
835
|
-
|
|
1068
|
+
if (protocol === 'wss:' || protocol === 'ws:') {
|
|
836
1069
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1070
|
+
instance.streamingMode = 'webrtc';
|
|
1071
|
+
initWebSocket(endpointUrl);
|
|
1072
|
+
} else if (protocol === 'https:' || protocol === 'http:') {
|
|
840
1073
|
|
|
841
|
-
|
|
1074
|
+
instance.streamingMode = 'whip';
|
|
1075
|
+
startWhip(endpointUrl);
|
|
1076
|
+
} else {
|
|
1077
|
+
console.error('Invalid protocol', error);
|
|
1078
|
+
errorHandler(error);
|
|
1079
|
+
}
|
|
842
1080
|
|
|
843
|
-
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
console.error('Cannot parse connection URL', error);
|
|
1083
|
+
errorHandler(error);
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
844
1086
|
|
|
845
|
-
|
|
846
|
-
closePeerConnection();
|
|
847
|
-
closeInputStream();
|
|
1087
|
+
instance.stopStreaming = async function () {
|
|
848
1088
|
|
|
849
|
-
|
|
1089
|
+
if (instance.streamingMode === 'webrtc') {
|
|
850
1090
|
|
|
851
|
-
|
|
1091
|
+
instance.webSocketClosedByUser = true;
|
|
1092
|
+
|
|
1093
|
+
closeWebSocket();
|
|
1094
|
+
closePeerConnection();
|
|
1095
|
+
} else if (instance.streamingMode === 'whip') {
|
|
1096
|
+
|
|
1097
|
+
await stopWhip();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (instance.callbacks.connectionClosed) {
|
|
1101
|
+
console.log(logHeader, 'Connection closed by user');
|
|
1102
|
+
instance.callbacks.connectionClosed('user', 'Connection closed by user');
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
instance.remove = function () {
|
|
1107
|
+
|
|
1108
|
+
if (instance.streamingMode === 'webrtc') {
|
|
1109
|
+
|
|
1110
|
+
instance.webSocketClosedByUser = true;
|
|
1111
|
+
|
|
1112
|
+
closeWebSocket();
|
|
1113
|
+
closePeerConnection();
|
|
1114
|
+
} else if (instance.streamingMode === 'whip') {
|
|
1115
|
+
stopWhip();
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
closeInputStream();
|
|
1119
|
+
|
|
1120
|
+
console.info(logEventHeader, 'Removed');
|
|
1121
|
+
|
|
1122
|
+
};
|
|
852
1123
|
}
|
|
853
1124
|
|
|
854
1125
|
OvenLiveKit.getVersion = function () {
|
|
855
|
-
|
|
1126
|
+
return version;
|
|
856
1127
|
}
|
|
857
1128
|
|
|
858
1129
|
// static methods
|
|
859
1130
|
OvenLiveKit.create = function (options) {
|
|
860
1131
|
|
|
861
|
-
|
|
1132
|
+
console.info(logEventHeader, 'Create WebRTC Input ' + version);
|
|
862
1133
|
|
|
863
|
-
|
|
1134
|
+
let instance = {};
|
|
864
1135
|
|
|
865
|
-
|
|
1136
|
+
instance.webSocketClosedByUser = false;
|
|
866
1137
|
|
|
867
|
-
|
|
868
|
-
|
|
1138
|
+
initConfig(instance, options);
|
|
1139
|
+
addMethod(instance);
|
|
869
1140
|
|
|
870
|
-
|
|
1141
|
+
return instance;
|
|
871
1142
|
};
|
|
872
1143
|
|
|
873
1144
|
OvenLiveKit.getDevices = async function () {
|
|
874
1145
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1146
|
+
try {
|
|
1147
|
+
// First check both audio and video sources are available.
|
|
1148
|
+
await getStreamForDeviceCheck('both');
|
|
1149
|
+
} catch (e) {
|
|
879
1150
|
|
|
880
|
-
|
|
1151
|
+
console.warn(logHeader, 'Can not find Video and Audio devices', e);
|
|
881
1152
|
|
|
882
|
-
|
|
883
|
-
|
|
1153
|
+
let videoFound = null;
|
|
1154
|
+
let audioFound = null;
|
|
884
1155
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1156
|
+
try {
|
|
1157
|
+
videoFound = await getStreamForDeviceCheck('video');
|
|
1158
|
+
} catch (e) {
|
|
1159
|
+
console.warn(logHeader, 'Can not find Video devices', e);
|
|
1160
|
+
}
|
|
890
1161
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1162
|
+
try {
|
|
1163
|
+
audioFound = await getStreamForDeviceCheck('audio');
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
console.warn(logHeader, 'Can not find Audio devices', e);
|
|
1166
|
+
}
|
|
896
1167
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
1168
|
+
if (!videoFound && !audioFound) {
|
|
1169
|
+
throw new Error('No input devices were found.');
|
|
900
1170
|
}
|
|
1171
|
+
}
|
|
901
1172
|
|
|
902
|
-
|
|
903
|
-
|
|
1173
|
+
const deviceInfos = await getDevices();
|
|
1174
|
+
return gotDevices(deviceInfos)
|
|
904
1175
|
};
|
|
905
1176
|
|
|
906
1177
|
export default OvenLiveKit;
|