ovenlivekit 1.2.0 → 1.3.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 +820 -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,1149 @@
|
|
|
1
1
|
const OvenLiveKit = {};
|
|
2
2
|
|
|
3
|
-
const version = '1.
|
|
3
|
+
const version = '1.3.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
|
-
|
|
60
|
-
|
|
61
|
-
if (formatNumber === -1) {
|
|
62
|
-
return sdp;
|
|
63
|
-
}
|
|
59
|
+
const formatNumber = getFormatNumber(sdp, formatName);
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
if (formatNumber === -1) {
|
|
62
|
+
return sdp;
|
|
63
|
+
}
|
|
67
64
|
|
|
68
|
-
|
|
65
|
+
let newLines = [];
|
|
66
|
+
const lines = sdp.split('\r\n');
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
const line = lines[i];
|
|
73
71
|
|
|
74
|
-
|
|
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
|
+
}
|
|
210
|
+
|
|
211
|
+
function getUserMedia(constraints) {
|
|
197
212
|
|
|
198
|
-
|
|
213
|
+
if (!constraints) {
|
|
199
214
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
};
|
|
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;
|
|
249
277
|
|
|
250
|
-
|
|
251
|
-
|
|
278
|
+
let elem = instance.videoElement;
|
|
279
|
+
|
|
280
|
+
// Attach stream to video element when video element is provided.
|
|
281
|
+
if (elem) {
|
|
282
|
+
|
|
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) {
|
|
255
292
|
|
|
256
|
-
|
|
257
|
-
|
|
293
|
+
resolve(stream);
|
|
294
|
+
});
|
|
295
|
+
})
|
|
296
|
+
.catch(function (error) {
|
|
258
297
|
|
|
259
|
-
|
|
298
|
+
console.error(logHeader, 'Can\'t Get Media Stream From Display', error);
|
|
299
|
+
errorHandler(error);
|
|
260
300
|
|
|
261
|
-
|
|
301
|
+
return new Promise(function (resolve, reject) {
|
|
302
|
+
reject(error);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
262
306
|
|
|
263
|
-
|
|
307
|
+
function setMediaStream(stream) {
|
|
308
|
+
// Check if a valid stream is provided
|
|
309
|
+
if (!stream || !(stream instanceof MediaStream)) {
|
|
264
310
|
|
|
265
|
-
|
|
266
|
-
|
|
311
|
+
const error = new Error("Invalid MediaStream provided");
|
|
312
|
+
console.error(logHeader, 'Invalid MediaStream', error);
|
|
313
|
+
errorHandler(error);
|
|
267
314
|
|
|
268
|
-
|
|
315
|
+
return new Promise(function (resolve, reject) {
|
|
316
|
+
reject(error);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
269
319
|
|
|
270
|
-
|
|
320
|
+
console.info(logHeader, 'Received Media Stream', stream);
|
|
271
321
|
|
|
272
|
-
|
|
273
|
-
};
|
|
274
|
-
}
|
|
322
|
+
instance.inputStream = stream;
|
|
275
323
|
|
|
276
|
-
|
|
324
|
+
let elem = instance.videoElement;
|
|
277
325
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
.catch(function (error) {
|
|
326
|
+
// Attach stream to video element when video element is provided.
|
|
327
|
+
if (elem) {
|
|
328
|
+
elem.srcObject = stream;
|
|
282
329
|
|
|
283
|
-
|
|
284
|
-
|
|
330
|
+
elem.onloadedmetadata = function (e) {
|
|
331
|
+
elem.play();
|
|
332
|
+
};
|
|
333
|
+
}
|
|
285
334
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
335
|
+
return new Promise(function (resolve) {
|
|
336
|
+
resolve(stream);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
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
|
+
}
|
|
391
458
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
});
|
|
396
|
-
};
|
|
459
|
+
const peerConnectionConfig = {
|
|
460
|
+
bundlePolicy: "max-bundle"
|
|
461
|
+
};
|
|
397
462
|
|
|
398
|
-
|
|
463
|
+
if (instance.connectionConfig.iceServers) {
|
|
399
464
|
|
|
400
|
-
|
|
465
|
+
// first priority using ice servers from local config.
|
|
466
|
+
peerConnectionConfig.iceServers = instance.connectionConfig.iceServers;
|
|
401
467
|
|
|
402
|
-
|
|
403
|
-
console.error('webSocket.onmessage', message.error);
|
|
404
|
-
errorHandler(message.error);
|
|
405
|
-
}
|
|
468
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
406
469
|
|
|
407
|
-
|
|
470
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
// last priority using default ice servers.
|
|
408
474
|
|
|
409
|
-
|
|
410
|
-
createPeerConnection(
|
|
411
|
-
message.id,
|
|
412
|
-
message.peer_id,
|
|
413
|
-
message.sdp,
|
|
414
|
-
message.candidates,
|
|
415
|
-
message.ice_servers
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
};
|
|
475
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
419
476
|
|
|
420
|
-
|
|
477
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
421
480
|
|
|
422
|
-
|
|
423
|
-
errorHandler(error);
|
|
424
|
-
};
|
|
481
|
+
console.info(logHeader, 'Create Peer Connection With Config', peerConnectionConfig);
|
|
425
482
|
|
|
426
|
-
|
|
483
|
+
const peerConnection = new RTCPeerConnection(peerConnectionConfig);
|
|
427
484
|
|
|
428
|
-
|
|
485
|
+
instance.peerConnection = peerConnection;
|
|
429
486
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
487
|
+
if (!instance.inputStream) {
|
|
488
|
+
console.error('No input stream in OvenLiveKit');
|
|
489
|
+
errorHandler('No input stream in OvenLiveKit');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
435
492
|
|
|
493
|
+
for (const track of instance.inputStream.getTracks()) {
|
|
494
|
+
//You could add simulcast too here
|
|
495
|
+
console.log(logHeader, 'Adding track: ', track);
|
|
496
|
+
peerConnection.addTransceiver(track, { 'direction': 'sendonly' });
|
|
436
497
|
}
|
|
437
498
|
|
|
438
|
-
function
|
|
499
|
+
peerConnection.oniceconnectionstatechange = function (e) {
|
|
439
500
|
|
|
440
|
-
|
|
501
|
+
let state = peerConnection.iceConnectionState;
|
|
441
502
|
|
|
442
|
-
|
|
443
|
-
const payloads = [];
|
|
503
|
+
if (instance.callbacks.iceStateChange) {
|
|
444
504
|
|
|
445
|
-
|
|
505
|
+
console.info(logHeader, 'ICE State', '[' + state + ']');
|
|
506
|
+
instance.callbacks.iceStateChange(state);
|
|
507
|
+
}
|
|
446
508
|
|
|
447
|
-
|
|
509
|
+
if (state === 'connected') {
|
|
448
510
|
|
|
449
|
-
|
|
511
|
+
if (instance.callbacks.connected) {
|
|
512
|
+
instance.callbacks.connected(e);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
450
515
|
|
|
451
|
-
|
|
516
|
+
if (state === 'failed') {
|
|
452
517
|
|
|
453
|
-
|
|
454
|
-
|
|
518
|
+
if (instance.callbacks.connectionClosed) {
|
|
519
|
+
console.error(logHeader, 'Ice connection failed', e);
|
|
520
|
+
instance.callbacks.errorHandler(e);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
455
523
|
|
|
456
|
-
|
|
457
|
-
|
|
524
|
+
if (state === 'disconnected' || state === 'closed') {
|
|
525
|
+
|
|
526
|
+
if (instance.callbacks.connectionClosed) {
|
|
527
|
+
console.error(logHeader, 'Ice connection disconnected or closed', e);
|
|
528
|
+
instance.callbacks.connectionClosed('ice', e);
|
|
458
529
|
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
459
532
|
|
|
460
|
-
|
|
533
|
+
const offer = await peerConnection.createOffer();
|
|
534
|
+
console.log(logHeader, 'Offer SDP: ', offer.sdp);
|
|
461
535
|
|
|
462
|
-
|
|
536
|
+
if (instance.connectionConfig.maxVideoBitrate) {
|
|
463
537
|
|
|
464
|
-
|
|
538
|
+
// if bandwith limit is set. modify sdp from ome to limit acceptable bandwidth of ome
|
|
539
|
+
offer.sdp = setBitrateLimit(offer.sdp, 'video', instance.connectionConfig.maxVideoBitrate);
|
|
540
|
+
}
|
|
465
541
|
|
|
466
|
-
|
|
467
|
-
fmtpLineFound = true;
|
|
468
|
-
lines[j] += ';' + fmtpStr;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
542
|
+
if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {
|
|
471
543
|
|
|
472
|
-
|
|
544
|
+
offer.sdp = appendFmtp(offer.sdp);
|
|
545
|
+
}
|
|
473
546
|
|
|
474
|
-
|
|
547
|
+
if (instance.connectionConfig.preferredVideoFormat) {
|
|
548
|
+
offer.sdp = setPreferredVideoFormat(offer.sdp, instance.connectionConfig.preferredVideoFormat)
|
|
549
|
+
}
|
|
475
550
|
|
|
476
|
-
|
|
551
|
+
const headers = {
|
|
552
|
+
"Content-Type": "application/sdp"
|
|
553
|
+
};
|
|
477
554
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
555
|
+
if (instance.connectionConfig.httpHeaders) {
|
|
556
|
+
Object.assign(headers, instance.connectionConfig.httpHeaders);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const fetched = await fetchWithRedirect(endpointUrl, {
|
|
560
|
+
method: "POST",
|
|
561
|
+
body: offer.sdp,
|
|
562
|
+
headers
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
if (!fetched.ok) {
|
|
566
|
+
console.error('Failed to fetch', fetched.status);
|
|
567
|
+
errorHandler(`Failed to fetch ${fetched.status}`);
|
|
568
|
+
closePeerConnection();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!fetched.headers.get("location")) {
|
|
573
|
+
console.error('No location header on answer response');
|
|
574
|
+
errorHandler('No location header on answer response');
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// update endpointUrl
|
|
579
|
+
instance.endpointUrl = fetched.url;
|
|
580
|
+
console.log(logHeader, 'Updated endpointUrl: ', instance.endpointUrl);
|
|
581
|
+
|
|
582
|
+
const baseUrl = new URL(endpointUrl).origin;
|
|
583
|
+
instance.resourceUrl = baseUrl + fetched.headers.get("location");
|
|
584
|
+
|
|
585
|
+
const answer = await fetched.text();
|
|
586
|
+
console.log(logHeader, 'Answer SDP: ', answer);
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
await peerConnection.setLocalDescription(offer);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.error('peerConnection.setLocalDescription', error);
|
|
592
|
+
errorHandler(error);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
await peerConnection.setRemoteDescription({
|
|
597
|
+
type: "answer",
|
|
598
|
+
sdp: answer
|
|
599
|
+
});
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.error('peerConnection.setRemoteDescription', error);
|
|
602
|
+
errorHandler(error);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function stopWhip() {
|
|
607
|
+
|
|
608
|
+
if (!instance.peerConnection) {
|
|
609
|
+
console.error('No connection to close');
|
|
610
|
+
errorHandler('No connection to close');
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
closePeerConnection();
|
|
615
|
+
|
|
616
|
+
if (instance.resourceUrl) {
|
|
617
|
+
|
|
618
|
+
const headers = {
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
if (instance.connectionConfig.httpHeaders) {
|
|
622
|
+
Object.assign(headers, instance.connectionConfig.httpHeaders);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
await fetchWithRedirect(instance.resourceUrl, {
|
|
626
|
+
method: "DELETE",
|
|
627
|
+
headers
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function appendFmtp(sdp) {
|
|
633
|
+
|
|
634
|
+
const fmtpStr = instance.connectionConfig.sdp.appendFmtp;
|
|
635
|
+
|
|
636
|
+
const lines = sdp.split('\r\n');
|
|
637
|
+
const payloads = [];
|
|
638
|
+
|
|
639
|
+
for (let i = 0; i < lines.length; i++) {
|
|
640
|
+
|
|
641
|
+
if (lines[i].indexOf('m=video') === 0) {
|
|
642
|
+
|
|
643
|
+
let tokens = lines[i].split(' ')
|
|
644
|
+
|
|
645
|
+
for (let j = 3; j < tokens.length; j++) {
|
|
646
|
+
|
|
647
|
+
payloads.push(tokens[j]);
|
|
482
648
|
}
|
|
483
649
|
|
|
484
|
-
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
485
652
|
}
|
|
486
653
|
|
|
487
|
-
|
|
654
|
+
for (let i = 0; i < payloads.length; i++) {
|
|
488
655
|
|
|
489
|
-
|
|
490
|
-
const payloads = [];
|
|
656
|
+
let fmtpLineFound = false;
|
|
491
657
|
|
|
492
|
-
|
|
658
|
+
for (let j = 0; j < lines.length; j++) {
|
|
493
659
|
|
|
494
|
-
|
|
660
|
+
if (lines[j].indexOf('a=fmtp:' + payloads[i]) === 0) {
|
|
661
|
+
fmtpLineFound = true;
|
|
662
|
+
lines[j] += ';' + fmtpStr;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
495
665
|
|
|
496
|
-
|
|
666
|
+
if (!fmtpLineFound) {
|
|
497
667
|
|
|
498
|
-
|
|
668
|
+
for (let j = 0; j < lines.length; j++) {
|
|
499
669
|
|
|
500
|
-
|
|
501
|
-
}
|
|
670
|
+
if (lines[j].indexOf('a=rtpmap:' + payloads[i]) === 0) {
|
|
502
671
|
|
|
503
|
-
|
|
504
|
-
|
|
672
|
+
lines[j] += '\r\na=fmtp:' + payloads[i] + ' ' + fmtpStr;
|
|
673
|
+
}
|
|
505
674
|
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
506
677
|
|
|
507
|
-
|
|
678
|
+
return lines.join('\r\n')
|
|
679
|
+
}
|
|
508
680
|
|
|
509
|
-
|
|
681
|
+
function appendOrientation(sdp) {
|
|
510
682
|
|
|
511
|
-
|
|
683
|
+
const lines = sdp.split('\r\n');
|
|
684
|
+
const payloads = [];
|
|
512
685
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
686
|
+
for (let i = 0; i < lines.length; i++) {
|
|
687
|
+
|
|
688
|
+
if (lines[i].indexOf('m=video') === 0) {
|
|
689
|
+
|
|
690
|
+
let tokens = lines[i].split(' ')
|
|
691
|
+
|
|
692
|
+
for (let j = 3; j < tokens.length; j++) {
|
|
693
|
+
|
|
694
|
+
payloads.push(tokens[j]);
|
|
516
695
|
}
|
|
517
696
|
|
|
518
|
-
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
519
699
|
}
|
|
520
700
|
|
|
521
|
-
|
|
701
|
+
for (let i = 0; i < payloads.length; i++) {
|
|
522
702
|
|
|
523
|
-
|
|
703
|
+
for (let j = 0; j < lines.length; j++) {
|
|
524
704
|
|
|
525
|
-
if (
|
|
705
|
+
if (lines[j].indexOf('a=rtpmap:' + payloads[i]) === 0) {
|
|
526
706
|
|
|
527
|
-
|
|
528
|
-
|
|
707
|
+
lines[j] += '\r\na=extmap:' + payloads[i] + ' urn:3gpp:video-orientation';
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
529
711
|
|
|
530
|
-
|
|
712
|
+
return lines.join('\r\n')
|
|
713
|
+
}
|
|
531
714
|
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
} else if (iceServers) {
|
|
715
|
+
function createPeerConnection(id, peerId, offer, candidates, iceServers) {
|
|
535
716
|
|
|
536
|
-
|
|
537
|
-
peerConnectionConfig.iceServers = [];
|
|
717
|
+
let peerConnectionConfig = {};
|
|
538
718
|
|
|
539
|
-
|
|
719
|
+
if (instance.connectionConfig.iceServers) {
|
|
540
720
|
|
|
541
|
-
|
|
721
|
+
// first priority using ice servers from local config.
|
|
722
|
+
peerConnectionConfig.iceServers = instance.connectionConfig.iceServers;
|
|
542
723
|
|
|
543
|
-
|
|
724
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
544
725
|
|
|
545
|
-
|
|
726
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
727
|
+
}
|
|
728
|
+
} else if (iceServers) {
|
|
546
729
|
|
|
547
|
-
|
|
548
|
-
|
|
730
|
+
// second priority using ice servers from ome and force using TCP
|
|
731
|
+
peerConnectionConfig.iceServers = [];
|
|
549
732
|
|
|
550
|
-
|
|
733
|
+
for (let i = 0; i < iceServers.length; i++) {
|
|
551
734
|
|
|
552
|
-
|
|
735
|
+
let iceServer = iceServers[i];
|
|
553
736
|
|
|
554
|
-
|
|
555
|
-
hasWebSocketUrl = true;
|
|
556
|
-
break;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
737
|
+
let regIceServer = {};
|
|
559
738
|
|
|
560
|
-
|
|
739
|
+
regIceServer.urls = iceServer.urls;
|
|
561
740
|
|
|
562
|
-
|
|
741
|
+
let hasWebSocketUrl = false;
|
|
742
|
+
let webSocketUrl = generateDomainFromUrl(instance.endpointUrl);
|
|
563
743
|
|
|
564
|
-
|
|
565
|
-
let ip = findIp(cloneIceServer);
|
|
744
|
+
for (let j = 0; j < regIceServer.urls.length; j++) {
|
|
566
745
|
|
|
567
|
-
|
|
568
|
-
regIceServer.urls.push(cloneIceServer.replace(ip, webSocketUrl));
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
746
|
+
let serverUrl = regIceServer.urls[j];
|
|
572
747
|
|
|
573
|
-
|
|
574
|
-
|
|
748
|
+
if (serverUrl.indexOf(webSocketUrl) > -1) {
|
|
749
|
+
hasWebSocketUrl = true;
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
575
753
|
|
|
576
|
-
|
|
577
|
-
}
|
|
754
|
+
if (!hasWebSocketUrl) {
|
|
578
755
|
|
|
579
|
-
|
|
580
|
-
} else {
|
|
581
|
-
// last priority using default ice servers.
|
|
756
|
+
if (regIceServer.urls.length > 0) {
|
|
582
757
|
|
|
583
|
-
|
|
758
|
+
let cloneIceServer = regIceServer.urls[0];
|
|
759
|
+
let ip = findIp(cloneIceServer);
|
|
584
760
|
|
|
585
|
-
|
|
761
|
+
if (webSocketUrl && ip) {
|
|
762
|
+
regIceServer.urls.push(cloneIceServer.replace(ip, webSocketUrl));
|
|
586
763
|
}
|
|
764
|
+
}
|
|
587
765
|
}
|
|
588
766
|
|
|
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
|
-
});
|
|
767
|
+
regIceServer.username = iceServer.user_name;
|
|
768
|
+
regIceServer.credential = iceServer.credential;
|
|
626
769
|
|
|
627
|
-
|
|
770
|
+
peerConnectionConfig.iceServers.push(regIceServer);
|
|
771
|
+
}
|
|
628
772
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
773
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
774
|
+
|
|
775
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
776
|
+
} else {
|
|
777
|
+
peerConnectionConfig.iceTransportPolicy = 'relay';
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
// last priority using default ice servers.
|
|
632
781
|
|
|
633
|
-
|
|
782
|
+
if (instance.connectionConfig.iceTransportPolicy) {
|
|
634
783
|
|
|
635
|
-
|
|
636
|
-
|
|
784
|
+
peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
637
787
|
|
|
638
|
-
|
|
639
|
-
|
|
788
|
+
let advancedSetting = {
|
|
789
|
+
optional: [
|
|
790
|
+
{
|
|
791
|
+
googHighStartBitrate: {
|
|
792
|
+
exact: !0
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
googPayloadPadding: {
|
|
797
|
+
exact: !0
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
googScreencastMinBitrate: {
|
|
802
|
+
exact: 500
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
enableDscp: {
|
|
807
|
+
exact: true
|
|
808
|
+
}
|
|
640
809
|
}
|
|
810
|
+
]
|
|
811
|
+
};
|
|
641
812
|
|
|
813
|
+
console.info(logHeader, 'Create Peer Connection With Config', peerConnectionConfig);
|
|
642
814
|
|
|
643
|
-
|
|
644
|
-
console.info(logHeader, 'Modified offer sdp\n\n' + offer.sdp);
|
|
815
|
+
let peerConnection = new RTCPeerConnection(peerConnectionConfig);
|
|
645
816
|
|
|
646
|
-
|
|
647
|
-
.then(function () {
|
|
817
|
+
instance.peerConnection = peerConnection;
|
|
648
818
|
|
|
649
|
-
|
|
650
|
-
|
|
819
|
+
// set local stream
|
|
820
|
+
instance.inputStream.getTracks().forEach(function (track) {
|
|
651
821
|
|
|
652
|
-
|
|
822
|
+
console.info(logHeader, 'Add Track To Peer Connection', track);
|
|
823
|
+
peerConnection.addTrack(track, instance.inputStream);
|
|
824
|
+
});
|
|
653
825
|
|
|
654
|
-
|
|
655
|
-
}
|
|
826
|
+
if (instance.connectionConfig.maxVideoBitrate) {
|
|
656
827
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
828
|
+
// if bandwith limit is set. modify sdp from ome to limit acceptable bandwidth of ome
|
|
829
|
+
offer.sdp = setBitrateLimit(offer.sdp, 'video', instance.connectionConfig.maxVideoBitrate);
|
|
830
|
+
}
|
|
660
831
|
|
|
661
|
-
|
|
832
|
+
if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {
|
|
662
833
|
|
|
663
|
-
|
|
664
|
-
|
|
834
|
+
offer.sdp = appendFmtp(offer.sdp);
|
|
835
|
+
}
|
|
665
836
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
command: 'answer',
|
|
670
|
-
sdp: answer
|
|
671
|
-
});
|
|
672
|
-
})
|
|
673
|
-
.catch(function (error) {
|
|
837
|
+
if (instance.connectionConfig.preferredVideoFormat) {
|
|
838
|
+
offer.sdp = setPreferredVideoFormat(offer.sdp, instance.connectionConfig.preferredVideoFormat)
|
|
839
|
+
}
|
|
674
840
|
|
|
675
|
-
console.error('peerConnection.setLocalDescription', error);
|
|
676
|
-
errorHandler(error);
|
|
677
|
-
});
|
|
678
|
-
})
|
|
679
|
-
.catch(function (error) {
|
|
680
841
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
});
|
|
684
|
-
})
|
|
685
|
-
.catch(function (error) {
|
|
842
|
+
// offer.sdp = appendOrientation(offer.sdp);
|
|
843
|
+
console.info(logHeader, 'Modified offer sdp\n\n' + offer.sdp);
|
|
686
844
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
});
|
|
845
|
+
peerConnection.setRemoteDescription(new RTCSessionDescription(offer))
|
|
846
|
+
.then(function () {
|
|
690
847
|
|
|
691
|
-
|
|
848
|
+
peerConnection.createAnswer()
|
|
849
|
+
.then(function (answer) {
|
|
692
850
|
|
|
693
|
-
|
|
694
|
-
|
|
851
|
+
if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {
|
|
852
|
+
|
|
853
|
+
answer.sdp = appendFmtp(answer.sdp);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (instance.connectionConfig.preferredVideoFormat) {
|
|
857
|
+
answer.sdp = setPreferredVideoFormat(answer.sdp, instance.connectionConfig.preferredVideoFormat)
|
|
858
|
+
}
|
|
695
859
|
|
|
696
|
-
|
|
860
|
+
console.info(logHeader, 'Modified answer sdp\n\n' + answer.sdp);
|
|
697
861
|
|
|
698
|
-
|
|
862
|
+
peerConnection.setLocalDescription(answer)
|
|
863
|
+
.then(function () {
|
|
699
864
|
|
|
700
865
|
sendMessage(instance.webSocket, {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
866
|
+
id: id,
|
|
867
|
+
peer_id: peerId,
|
|
868
|
+
command: 'answer',
|
|
869
|
+
sdp: answer
|
|
705
870
|
});
|
|
706
|
-
|
|
707
|
-
|
|
871
|
+
})
|
|
872
|
+
.catch(function (error) {
|
|
708
873
|
|
|
709
|
-
|
|
874
|
+
console.error('peerConnection.setLocalDescription', error);
|
|
875
|
+
errorHandler(error);
|
|
876
|
+
});
|
|
877
|
+
})
|
|
878
|
+
.catch(function (error) {
|
|
710
879
|
|
|
711
|
-
|
|
880
|
+
console.error('peerConnection.createAnswer', error);
|
|
881
|
+
errorHandler(error);
|
|
882
|
+
});
|
|
883
|
+
})
|
|
884
|
+
.catch(function (error) {
|
|
712
885
|
|
|
713
|
-
|
|
886
|
+
console.error('peerConnection.setRemoteDescription', error);
|
|
887
|
+
errorHandler(error);
|
|
888
|
+
});
|
|
714
889
|
|
|
715
|
-
|
|
716
|
-
instance.callbacks.iceStateChange(state);
|
|
717
|
-
}
|
|
890
|
+
if (candidates) {
|
|
718
891
|
|
|
719
|
-
|
|
892
|
+
addIceCandidate(peerConnection, candidates);
|
|
893
|
+
}
|
|
720
894
|
|
|
721
|
-
|
|
722
|
-
instance.callbacks.connected(e);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
895
|
+
peerConnection.onicecandidate = function (e) {
|
|
725
896
|
|
|
726
|
-
|
|
897
|
+
if (e.candidate && e.candidate.candidate) {
|
|
727
898
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
899
|
+
sendMessage(instance.webSocket, {
|
|
900
|
+
id: id,
|
|
901
|
+
peer_id: peerId,
|
|
902
|
+
command: 'candidate',
|
|
903
|
+
candidates: [e.candidate]
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
peerConnection.oniceconnectionstatechange = function (e) {
|
|
909
|
+
|
|
910
|
+
let state = peerConnection.iceConnectionState;
|
|
911
|
+
|
|
912
|
+
if (instance.callbacks.iceStateChange) {
|
|
913
|
+
|
|
914
|
+
console.info(logHeader, 'ICE State', '[' + state + ']');
|
|
915
|
+
instance.callbacks.iceStateChange(state);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (state === 'connected') {
|
|
919
|
+
|
|
920
|
+
if (instance.callbacks.connected) {
|
|
921
|
+
instance.callbacks.connected(e);
|
|
733
922
|
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (state === 'failed' || state === 'disconnected' || state === 'closed') {
|
|
926
|
+
|
|
927
|
+
if (instance.callbacks.connectionClosed) {
|
|
928
|
+
console.error(logHeader, 'Ice connection closed', e);
|
|
929
|
+
instance.callbacks.connectionClosed('ice', e);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
734
932
|
}
|
|
933
|
+
}
|
|
735
934
|
|
|
736
|
-
|
|
935
|
+
function addIceCandidate(peerConnection, candidates) {
|
|
737
936
|
|
|
738
|
-
|
|
937
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
739
938
|
|
|
740
|
-
|
|
939
|
+
if (candidates[i] && candidates[i].candidate) {
|
|
741
940
|
|
|
742
|
-
|
|
941
|
+
let basicCandidate = candidates[i];
|
|
743
942
|
|
|
744
|
-
|
|
745
|
-
|
|
943
|
+
peerConnection.addIceCandidate(new RTCIceCandidate(basicCandidate))
|
|
944
|
+
.then(function () {
|
|
746
945
|
|
|
747
|
-
|
|
748
|
-
|
|
946
|
+
})
|
|
947
|
+
.catch(function (error) {
|
|
749
948
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
949
|
+
console.error('peerConnection.addIceCandidate', error);
|
|
950
|
+
errorHandler(error);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
755
953
|
}
|
|
954
|
+
}
|
|
756
955
|
|
|
757
|
-
|
|
758
|
-
|
|
956
|
+
function closePeerConnection() {
|
|
957
|
+
if (instance.peerConnection) {
|
|
759
958
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
959
|
+
// remove tracks from peer connection
|
|
960
|
+
instance.peerConnection.getSenders().forEach(function (sender) {
|
|
961
|
+
instance.peerConnection.removeTrack(sender);
|
|
962
|
+
});
|
|
764
963
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
964
|
+
instance.peerConnection.close();
|
|
965
|
+
instance.peerConnection = null;
|
|
966
|
+
delete instance.peerConnection;
|
|
769
967
|
}
|
|
968
|
+
}
|
|
770
969
|
|
|
771
|
-
|
|
970
|
+
function closeWebSocket() {
|
|
772
971
|
|
|
773
|
-
|
|
972
|
+
if (instance.webSocket) {
|
|
774
973
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
974
|
+
instance.webSocket.close();
|
|
975
|
+
instance.webSocket = null;
|
|
976
|
+
delete instance.webSocket;
|
|
779
977
|
}
|
|
978
|
+
}
|
|
780
979
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
980
|
+
function closeInputStream() {
|
|
981
|
+
// release video, audio stream
|
|
982
|
+
if (instance.inputStream) {
|
|
784
983
|
|
|
785
|
-
|
|
984
|
+
instance.inputStream.getTracks().forEach(track => {
|
|
786
985
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
986
|
+
track.stop();
|
|
987
|
+
instance.inputStream.removeTrack(track);
|
|
988
|
+
});
|
|
790
989
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
990
|
+
if (instance.videoElement) {
|
|
991
|
+
instance.videoElement.srcObject = null;
|
|
992
|
+
}
|
|
794
993
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
994
|
+
instance.inputStream = null;
|
|
995
|
+
delete instance.inputStream;
|
|
798
996
|
}
|
|
997
|
+
}
|
|
799
998
|
|
|
800
|
-
|
|
801
|
-
|
|
999
|
+
// instance methods
|
|
1000
|
+
instance.attachMedia = function (videoElement) {
|
|
802
1001
|
|
|
803
|
-
|
|
804
|
-
|
|
1002
|
+
instance.videoElement = videoElement;
|
|
1003
|
+
};
|
|
805
1004
|
|
|
806
|
-
|
|
1005
|
+
instance.getUserMedia = function (constraints) {
|
|
807
1006
|
|
|
808
|
-
|
|
809
|
-
|
|
1007
|
+
return getUserMedia(constraints);
|
|
1008
|
+
};
|
|
810
1009
|
|
|
811
|
-
|
|
1010
|
+
instance.getDisplayMedia = function (constraints) {
|
|
812
1011
|
|
|
813
|
-
|
|
814
|
-
|
|
1012
|
+
return getDisplayMedia(constraints);
|
|
1013
|
+
};
|
|
815
1014
|
|
|
816
|
-
|
|
1015
|
+
instance.setMediaStream = function (stream) {
|
|
817
1016
|
|
|
818
|
-
|
|
819
|
-
|
|
1017
|
+
return setMediaStream(stream);
|
|
1018
|
+
};
|
|
820
1019
|
|
|
821
|
-
|
|
1020
|
+
instance.startStreaming = function (endpointUrl, connectionConfig) {
|
|
822
1021
|
|
|
823
|
-
|
|
1022
|
+
console.info(logEventHeader, `Start Streaming to ${endpointUrl} with connectionConfig`, connectionConfig);
|
|
824
1023
|
|
|
825
|
-
|
|
1024
|
+
if (!endpointUrl) {
|
|
1025
|
+
console.error('endpointUrl is required');
|
|
1026
|
+
errorHandler('endpointUrl is required');
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
826
1029
|
|
|
827
|
-
|
|
828
|
-
}
|
|
1030
|
+
instance.endpointUrl = endpointUrl;
|
|
829
1031
|
|
|
830
|
-
|
|
831
|
-
|
|
1032
|
+
if (connectionConfig) {
|
|
1033
|
+
instance.connectionConfig = connectionConfig;
|
|
1034
|
+
}
|
|
832
1035
|
|
|
833
|
-
|
|
1036
|
+
try {
|
|
834
1037
|
|
|
835
|
-
|
|
1038
|
+
const protocol = new URL(endpointUrl).protocol;
|
|
836
1039
|
|
|
837
|
-
|
|
838
|
-
closePeerConnection();
|
|
839
|
-
};
|
|
1040
|
+
if (protocol === 'wss:' || protocol === 'ws:') {
|
|
840
1041
|
|
|
841
|
-
|
|
1042
|
+
instance.streamingMode = 'webrtc';
|
|
1043
|
+
initWebSocket(endpointUrl);
|
|
1044
|
+
} else if (protocol === 'https:' || protocol === 'http:') {
|
|
842
1045
|
|
|
843
|
-
instance.
|
|
1046
|
+
instance.streamingMode = 'whip';
|
|
1047
|
+
startWhip(endpointUrl);
|
|
1048
|
+
} else {
|
|
1049
|
+
console.error('Invalid protocol', error);
|
|
1050
|
+
errorHandler(error);
|
|
1051
|
+
}
|
|
844
1052
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
console.error('Cannot parse connection URL', error);
|
|
1055
|
+
errorHandler(error);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
848
1058
|
|
|
849
|
-
|
|
1059
|
+
instance.stopStreaming = async function () {
|
|
850
1060
|
|
|
851
|
-
|
|
1061
|
+
if (instance.streamingMode === 'webrtc') {
|
|
1062
|
+
|
|
1063
|
+
instance.webSocketClosedByUser = true;
|
|
1064
|
+
|
|
1065
|
+
closeWebSocket();
|
|
1066
|
+
closePeerConnection();
|
|
1067
|
+
} else if (instance.streamingMode === 'whip') {
|
|
1068
|
+
|
|
1069
|
+
await stopWhip();
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (instance.callbacks.connectionClosed) {
|
|
1073
|
+
console.log(logHeader, 'Connection closed by user');
|
|
1074
|
+
instance.callbacks.connectionClosed('user', 'Connection closed by user');
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
instance.remove = function () {
|
|
1079
|
+
|
|
1080
|
+
if (instance.streamingMode === 'webrtc') {
|
|
1081
|
+
|
|
1082
|
+
instance.webSocketClosedByUser = true;
|
|
1083
|
+
|
|
1084
|
+
closeWebSocket();
|
|
1085
|
+
closePeerConnection();
|
|
1086
|
+
} else if (instance.streamingMode === 'whip') {
|
|
1087
|
+
stopWhip();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
closeInputStream();
|
|
1091
|
+
|
|
1092
|
+
console.info(logEventHeader, 'Removed');
|
|
1093
|
+
|
|
1094
|
+
};
|
|
852
1095
|
}
|
|
853
1096
|
|
|
854
1097
|
OvenLiveKit.getVersion = function () {
|
|
855
|
-
|
|
1098
|
+
return version;
|
|
856
1099
|
}
|
|
857
1100
|
|
|
858
1101
|
// static methods
|
|
859
1102
|
OvenLiveKit.create = function (options) {
|
|
860
1103
|
|
|
861
|
-
|
|
1104
|
+
console.info(logEventHeader, 'Create WebRTC Input ' + version);
|
|
862
1105
|
|
|
863
|
-
|
|
1106
|
+
let instance = {};
|
|
864
1107
|
|
|
865
|
-
|
|
1108
|
+
instance.webSocketClosedByUser = false;
|
|
866
1109
|
|
|
867
|
-
|
|
868
|
-
|
|
1110
|
+
initConfig(instance, options);
|
|
1111
|
+
addMethod(instance);
|
|
869
1112
|
|
|
870
|
-
|
|
1113
|
+
return instance;
|
|
871
1114
|
};
|
|
872
1115
|
|
|
873
1116
|
OvenLiveKit.getDevices = async function () {
|
|
874
1117
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1118
|
+
try {
|
|
1119
|
+
// First check both audio and video sources are available.
|
|
1120
|
+
await getStreamForDeviceCheck('both');
|
|
1121
|
+
} catch (e) {
|
|
879
1122
|
|
|
880
|
-
|
|
1123
|
+
console.warn(logHeader, 'Can not find Video and Audio devices', e);
|
|
881
1124
|
|
|
882
|
-
|
|
883
|
-
|
|
1125
|
+
let videoFound = null;
|
|
1126
|
+
let audioFound = null;
|
|
884
1127
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1128
|
+
try {
|
|
1129
|
+
videoFound = await getStreamForDeviceCheck('video');
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
console.warn(logHeader, 'Can not find Video devices', e);
|
|
1132
|
+
}
|
|
890
1133
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1134
|
+
try {
|
|
1135
|
+
audioFound = await getStreamForDeviceCheck('audio');
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
console.warn(logHeader, 'Can not find Audio devices', e);
|
|
1138
|
+
}
|
|
896
1139
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
1140
|
+
if (!videoFound && !audioFound) {
|
|
1141
|
+
throw new Error('No input devices were found.');
|
|
900
1142
|
}
|
|
1143
|
+
}
|
|
901
1144
|
|
|
902
|
-
|
|
903
|
-
|
|
1145
|
+
const deviceInfos = await getDevices();
|
|
1146
|
+
return gotDevices(deviceInfos)
|
|
904
1147
|
};
|
|
905
1148
|
|
|
906
1149
|
export default OvenLiveKit;
|