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