ep_webrtc 0.1.77 → 0.1.81
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 +2 -1
- package/locales/en.json +2 -0
- package/package.json +1 -1
- package/static/css/styles.css +6 -0
- package/static/js/index.js +144 -67
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# ep_webrtc
|
|
4
4
|
|
|
5
|
-
WebRTC-based audio/video chat with other users visiting the
|
|
5
|
+
WebRTC-based audio/video chat and screen sharing with other users visiting the
|
|
6
|
+
same pad.
|
|
6
7
|
|
|
7
8
|
The audio and video streams are peer-to-peer: every user sends a copy of their
|
|
8
9
|
audio/video streams directly to every other user visiting the same pad. Because
|
package/locales/en.json
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
"ep_webrtc_error_permission_cam": "Failed to get permission to access your camera.",
|
|
6
6
|
"ep_webrtc_error_permission_mic": "Failed to get permission to access your microphone.",
|
|
7
7
|
"ep_webrtc_error_permission_cammic": "Failed to get permission to access your camera and microphone.",
|
|
8
|
+
"ep_webrtc_error_permission_screen": "Failed to get permission to access your screen.",
|
|
8
9
|
"ep_webrtc_error_notFound_cam": "Failed to access your camera.",
|
|
9
10
|
"ep_webrtc_error_notFound_mic": "Failed to access your microphone.",
|
|
10
11
|
"ep_webrtc_error_notFound_cammic": "Failed to access your camera and microphone.",
|
|
12
|
+
"ep_webrtc_error_notFound_screen": "Failed to access your screen.",
|
|
11
13
|
"ep_webrtc_error_notReadable": "Sorry, a hardware error occurred that prevented access to your camera and/or microphone:",
|
|
12
14
|
"ep_webrtc_error_otherCantAccess": "Sorry, the browser doesn't have access to your camera and/or microphone. Please check permissions in your browser's settings. This is most likely not a hardware error:",
|
|
13
15
|
|
package/package.json
CHANGED
package/static/css/styles.css
CHANGED
|
@@ -102,6 +102,12 @@
|
|
|
102
102
|
#rtcbox .interface-btn.video-btn:before { content: '\e83b'; }
|
|
103
103
|
#rtcbox .interface-btn.video-btn.off:before { content: '\e83c'; }
|
|
104
104
|
|
|
105
|
+
#rtcbox .interface-btn.screenshare-btn:before {
|
|
106
|
+
content: "\f108";
|
|
107
|
+
font-family: "fontawesome-ep_webrtc";
|
|
108
|
+
}
|
|
109
|
+
#rtcbox .interface-btn.screenshare-btn.off:before { content: "\f109"; }
|
|
110
|
+
|
|
105
111
|
#rtcbox .interface-btn.enlarge-btn:before { content: '\e840'; }
|
|
106
112
|
#rtcbox .interface-btn.enlarge-btn.large:before { content: '\e83f'; }
|
|
107
113
|
|
package/static/js/index.js
CHANGED
|
@@ -44,6 +44,30 @@ const sessionId = Date.now();
|
|
|
44
44
|
// Incremented each time a new RTCPeerConnection is created.
|
|
45
45
|
let nextInstanceId = 0;
|
|
46
46
|
|
|
47
|
+
const logErrorToServer = async (err, delay = 10000) => {
|
|
48
|
+
// Sleep to avoid logging benign errors caused by the user leaving the page (e.g., audio/video
|
|
49
|
+
// stream ended unexpectedly). If the user navigates away during this sleep the error will not
|
|
50
|
+
// be logged.
|
|
51
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
52
|
+
// Mimick Etherpad core's global exception handler in pad_utils.js.
|
|
53
|
+
const {message = 'unknown', fileName = 'unknown', lineNumber = -1} = err;
|
|
54
|
+
let msg = message;
|
|
55
|
+
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
|
|
56
|
+
msg = `${err.name}: ${msg}`;
|
|
57
|
+
}
|
|
58
|
+
await $.post('../jserror', {
|
|
59
|
+
errorInfo: JSON.stringify({
|
|
60
|
+
type: 'Plugin ep_webrtc',
|
|
61
|
+
msg,
|
|
62
|
+
url: window.location.href,
|
|
63
|
+
source: fileName,
|
|
64
|
+
linenumber: lineNumber,
|
|
65
|
+
userAgent: navigator.userAgent,
|
|
66
|
+
stack: err.stack,
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
47
71
|
class Mutex {
|
|
48
72
|
async lock() {
|
|
49
73
|
while (this._locked != null) await this._locked;
|
|
@@ -60,6 +84,7 @@ class LocalTracks extends EventTargetPolyfill {
|
|
|
60
84
|
constructor() {
|
|
61
85
|
super();
|
|
62
86
|
Object.defineProperty(this, 'stream', {value: new MediaStream(), writeable: false});
|
|
87
|
+
this.videoIsScreenshare = false;
|
|
63
88
|
}
|
|
64
89
|
|
|
65
90
|
_getTracks(kind) {
|
|
@@ -158,6 +183,7 @@ class PeerState extends EventTargetPolyfill {
|
|
|
158
183
|
return await sender.replaceTrack(newTrack);
|
|
159
184
|
} catch (err) {
|
|
160
185
|
this._debug('renegotiation is required');
|
|
186
|
+
logErrorToServer(err);
|
|
161
187
|
}
|
|
162
188
|
}
|
|
163
189
|
this._pc.removeTrack(sender);
|
|
@@ -184,7 +210,11 @@ class PeerState extends EventTargetPolyfill {
|
|
|
184
210
|
}
|
|
185
211
|
}
|
|
186
212
|
|
|
187
|
-
_resetConnection(peerIds = null) {
|
|
213
|
+
_resetConnection(err = null, peerIds = null) {
|
|
214
|
+
if (err != null) {
|
|
215
|
+
this._debug('resetting connection due to error:', err);
|
|
216
|
+
logErrorToServer(err);
|
|
217
|
+
}
|
|
188
218
|
if (this._closed) {
|
|
189
219
|
this._debug('ignoring _resetConnection() on closed PeerState');
|
|
190
220
|
return;
|
|
@@ -217,7 +247,7 @@ class PeerState extends EventTargetPolyfill {
|
|
|
217
247
|
} catch (err) {
|
|
218
248
|
console.error('Error setting local description:', err);
|
|
219
249
|
if (++this._failedSLDAttempts > 10) throw err; // Avoid an infinite loop.
|
|
220
|
-
this._resetConnection();
|
|
250
|
+
this._resetConnection(err);
|
|
221
251
|
return;
|
|
222
252
|
} finally {
|
|
223
253
|
negotiationState.makingOffer = false;
|
|
@@ -226,7 +256,9 @@ class PeerState extends EventTargetPolyfill {
|
|
|
226
256
|
pc.addEventListener('connectionstatechange', () => {
|
|
227
257
|
this._debug(`connection state changed to ${pc.connectionState}`);
|
|
228
258
|
switch (pc.connectionState) {
|
|
229
|
-
case 'closed':
|
|
259
|
+
case 'closed':
|
|
260
|
+
this._resetConnection(new Error('connectionState changed to closed'));
|
|
261
|
+
break;
|
|
230
262
|
case 'connected':
|
|
231
263
|
if (this._remoteStream == null) this._setRemoteStream(this._disconnectedRemoteStream);
|
|
232
264
|
break;
|
|
@@ -240,7 +272,7 @@ class PeerState extends EventTargetPolyfill {
|
|
|
240
272
|
// seems that on at least Chrome 90 the 'failed' state is terminal (it can never go back to
|
|
241
273
|
// working) so a new RTCPeerConnection must be made.
|
|
242
274
|
case 'failed':
|
|
243
|
-
this._resetConnection();
|
|
275
|
+
this._resetConnection(new Error('connectionState changed to failed'));
|
|
244
276
|
break;
|
|
245
277
|
}
|
|
246
278
|
});
|
|
@@ -325,26 +357,26 @@ class PeerState extends EventTargetPolyfill {
|
|
|
325
357
|
return;
|
|
326
358
|
}
|
|
327
359
|
for (const idType of ['session', 'instance']) {
|
|
328
|
-
const newId = ids.from[idType];
|
|
360
|
+
const newId = (ids.from || {})[idType];
|
|
329
361
|
const currentId = (this._ids.to || {})[idType];
|
|
330
362
|
if (currentId == null || newId === currentId) continue;
|
|
331
363
|
if (newId == null || newId < currentId) return;
|
|
332
364
|
// The remote peer reloaded the page or experienced an error. Destroy and recreate the local
|
|
333
365
|
// RTCPeerConnection to avoid browser quirks caused by state mismatches.
|
|
334
|
-
this.
|
|
335
|
-
|
|
336
|
-
|
|
366
|
+
this._resetConnection(new Error(
|
|
367
|
+
`remote peer forced WebRTC renegotiation via new ${idType} ID ` +
|
|
368
|
+
`(old ID ${currentId}, new ID ${newId})`), ids.from);
|
|
337
369
|
break;
|
|
338
370
|
}
|
|
339
371
|
this._ids.to = ids.from;
|
|
340
372
|
}
|
|
341
|
-
if (this._pc == null) this._resetConnection(this._ids.to);
|
|
373
|
+
if (this._pc == null) this._resetConnection(null, this._ids.to);
|
|
342
374
|
try {
|
|
343
375
|
if (description != null) await this._setRemoteDescription(description);
|
|
344
376
|
if (candidate != null) await this._addIceCandidate(candidate);
|
|
345
377
|
} catch (err) {
|
|
346
378
|
console.error('Error processing message from peer:', err);
|
|
347
|
-
this._resetConnection();
|
|
379
|
+
this._resetConnection(err);
|
|
348
380
|
return;
|
|
349
381
|
}
|
|
350
382
|
}
|
|
@@ -409,16 +441,17 @@ exports.rtc = new class {
|
|
|
409
441
|
const {kind} = oldTrack || newTrack;
|
|
410
442
|
$videoContainer.find(`.${kind}ended-error-btn`)
|
|
411
443
|
.css('display', newTrack == null ? '' : 'none');
|
|
412
|
-
if (newTrack == null) {
|
|
413
|
-
this.logErrorToServer(new Error(`Local ${kind} track ended unexpectedly`));
|
|
414
|
-
}
|
|
444
|
+
if (newTrack == null) logErrorToServer(new Error(`Local ${kind} track ended unexpectedly`));
|
|
415
445
|
($videoContainer.data('updateMinSize') || (() => {}))();
|
|
416
446
|
|
|
417
447
|
// Update the audio/video buttons to reflect the new state.
|
|
418
448
|
if (newTrack != null) return;
|
|
419
449
|
switch (oldTrack.kind) {
|
|
420
450
|
case 'audio': this._selfViewButtons.audio.enabled = false; break;
|
|
421
|
-
case 'video':
|
|
451
|
+
case 'video':
|
|
452
|
+
this._selfViewButtons.video.enabled = false;
|
|
453
|
+
this._selfViewButtons.screenshare.enabled = false;
|
|
454
|
+
break;
|
|
422
455
|
}
|
|
423
456
|
});
|
|
424
457
|
this._pad = null;
|
|
@@ -541,30 +574,6 @@ exports.rtc = new class {
|
|
|
541
574
|
($videoContainer.data('updateMinSize') || (() => {}))();
|
|
542
575
|
}
|
|
543
576
|
|
|
544
|
-
async logErrorToServer(err, delay = 10000) {
|
|
545
|
-
// Sleep to avoid logging benign errors caused by the user leaving the page (e.g., audio/video
|
|
546
|
-
// stream ended unexpectedly). If the user navigates away during this sleep the error will not
|
|
547
|
-
// be logged.
|
|
548
|
-
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
549
|
-
// Mimick Etherpad core's global exception handler in pad_utils.js.
|
|
550
|
-
const {message = 'unknown', fileName = 'unknown', lineNumber = -1} = err;
|
|
551
|
-
let msg = message;
|
|
552
|
-
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
|
|
553
|
-
msg = `${err.name}: ${msg}`;
|
|
554
|
-
}
|
|
555
|
-
await $.post('../jserror', {
|
|
556
|
-
errorInfo: JSON.stringify({
|
|
557
|
-
type: 'Plugin ep_webrtc',
|
|
558
|
-
msg,
|
|
559
|
-
url: window.location.href,
|
|
560
|
-
source: fileName,
|
|
561
|
-
linenumber: lineNumber,
|
|
562
|
-
userAgent: navigator.userAgent,
|
|
563
|
-
stack: err.stack,
|
|
564
|
-
}),
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
|
|
568
577
|
showUserMediaError(err) { // show an error returned from getUserMedia
|
|
569
578
|
err.devices.sort();
|
|
570
579
|
const devices = err.devices.join('');
|
|
@@ -625,7 +634,7 @@ exports.rtc = new class {
|
|
|
625
634
|
sticky: true,
|
|
626
635
|
class_name: 'error',
|
|
627
636
|
});
|
|
628
|
-
|
|
637
|
+
logErrorToServer(err);
|
|
629
638
|
}
|
|
630
639
|
|
|
631
640
|
// Performs the following steps for the local audio and/or video tracks:
|
|
@@ -646,27 +655,52 @@ exports.rtc = new class {
|
|
|
646
655
|
(t) => t !== this._disabledSilence && t.readyState === 'live');
|
|
647
656
|
if (addAudioTrack) devices.push('mic');
|
|
648
657
|
const addVideoTrack = updateVideo && this._selfViewButtons.video.enabled &&
|
|
649
|
-
!this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live')
|
|
658
|
+
(!this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live') ||
|
|
659
|
+
this._localTracks.videoIsScreenshare);
|
|
650
660
|
if (addVideoTrack) devices.push('cam');
|
|
651
|
-
|
|
661
|
+
const addScreenshareTrack = updateVideo && this._selfViewButtons.screenshare.enabled &&
|
|
662
|
+
!this._selfViewButtons.video.enabled && // Video button overrides screenshare button.
|
|
663
|
+
(!this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live') ||
|
|
664
|
+
!this._localTracks.videoIsScreenshare);
|
|
665
|
+
const getUserMedia = async () => {
|
|
666
|
+
if (!addAudioTrack && !addVideoTrack) return new MediaStream();
|
|
652
667
|
debug(`requesting permission to access ${devices.join(' and ')}`);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
668
|
+
const stream = await window.navigator.mediaDevices.getUserMedia({
|
|
669
|
+
audio: addAudioTrack,
|
|
670
|
+
video: addVideoTrack && {width: {ideal: 320}, height: {ideal: 240}},
|
|
671
|
+
});
|
|
672
|
+
debug('successfully accessed device(s)');
|
|
673
|
+
return stream;
|
|
674
|
+
};
|
|
675
|
+
const getDisplayMedia = async () => {
|
|
676
|
+
if (!addScreenshareTrack) return new MediaStream();
|
|
677
|
+
debug('requesting permission to access screen');
|
|
678
|
+
const stream = await window.navigator.mediaDevices.getDisplayMedia({
|
|
679
|
+
video: {cursor: 'always'},
|
|
680
|
+
});
|
|
681
|
+
debug('successfully accessed screen');
|
|
682
|
+
return new MediaStream(stream.getVideoTracks());
|
|
683
|
+
};
|
|
684
|
+
await Promise.all([[getUserMedia, devices], [getDisplayMedia, ['screen']]].map(
|
|
685
|
+
async ([getMedia, devices]) => {
|
|
686
|
+
let stream;
|
|
687
|
+
try {
|
|
688
|
+
stream = await getMedia();
|
|
689
|
+
} catch (err) {
|
|
690
|
+
// Display but otherwise ignore the error. The button(s) will be toggled back to
|
|
691
|
+
// disabled below if we failed to access the microphone/camera. The user can re-click
|
|
692
|
+
// the button to try again.
|
|
693
|
+
err.devices = devices;
|
|
694
|
+
(async () => this.showUserMediaError(err))();
|
|
695
|
+
stream = new MediaStream();
|
|
696
|
+
}
|
|
697
|
+
for (const track of stream.getTracks()) {
|
|
698
|
+
if (track.kind === 'video') {
|
|
699
|
+
this._localTracks.videoIsScreenshare = devices.includes('screen');
|
|
700
|
+
}
|
|
701
|
+
this._localTracks.setTrack(track.kind, track);
|
|
702
|
+
}
|
|
703
|
+
}));
|
|
670
704
|
if (updateAudio) {
|
|
671
705
|
for (const track of this._localTracks.stream.getAudioTracks()) {
|
|
672
706
|
// Re-check the state of the button because the user might have clicked it while
|
|
@@ -680,12 +714,15 @@ exports.rtc = new class {
|
|
|
680
714
|
if (updateVideo) {
|
|
681
715
|
for (const track of this._localTracks.stream.getVideoTracks()) {
|
|
682
716
|
// Re-check the state of the button because the user might have clicked it while
|
|
683
|
-
// getUserMedia() was running.
|
|
684
|
-
track.enabled = this.
|
|
717
|
+
// getUserMedia() or getDisplayMedia() was running.
|
|
718
|
+
track.enabled = this._localTracks.videoIsScreenshare
|
|
719
|
+
? this._selfViewButtons.screenshare.enabled : this._selfViewButtons.video.enabled;
|
|
685
720
|
}
|
|
686
721
|
const hasVideo = this._localTracks.stream.getVideoTracks().some(
|
|
687
722
|
(t) => t.enabled && t.readyState === 'live');
|
|
688
|
-
this._selfViewButtons.video.enabled = hasVideo;
|
|
723
|
+
this._selfViewButtons.video.enabled = hasVideo && !this._localTracks.videoIsScreenshare;
|
|
724
|
+
this._selfViewButtons.screenshare.enabled =
|
|
725
|
+
hasVideo && this._localTracks.videoIsScreenshare;
|
|
689
726
|
}
|
|
690
727
|
} finally {
|
|
691
728
|
if (updateVideo) this._trackLocks.video.unlock();
|
|
@@ -739,8 +776,15 @@ exports.rtc = new class {
|
|
|
739
776
|
const $rtcbox = $('#rtcbox');
|
|
740
777
|
$rtcbox.empty(); // In case any peer videos didn't get cleaned up for some reason.
|
|
741
778
|
$rtcbox.hide();
|
|
742
|
-
|
|
743
|
-
|
|
779
|
+
await this._trackLocks.audio.lock();
|
|
780
|
+
await this._trackLocks.video.lock();
|
|
781
|
+
try {
|
|
782
|
+
for (const track of this._localTracks.stream.getTracks()) {
|
|
783
|
+
this._localTracks.setTrack(track.kind, null);
|
|
784
|
+
}
|
|
785
|
+
} finally {
|
|
786
|
+
this._trackLocks.video.unlock();
|
|
787
|
+
this._trackLocks.audio.unlock();
|
|
744
788
|
}
|
|
745
789
|
} finally {
|
|
746
790
|
$checkbox.prop('disabled', false);
|
|
@@ -796,7 +840,7 @@ exports.rtc = new class {
|
|
|
796
840
|
$video.data('automuted', true);
|
|
797
841
|
return await this.playVideo($video);
|
|
798
842
|
}
|
|
799
|
-
|
|
843
|
+
logErrorToServer(err);
|
|
800
844
|
$playErrorBtn.css({display: ''});
|
|
801
845
|
}
|
|
802
846
|
($videoContainer.data('updateMinSize') || (() => {}))();
|
|
@@ -931,10 +975,11 @@ exports.rtc = new class {
|
|
|
931
975
|
});
|
|
932
976
|
|
|
933
977
|
// /////
|
|
934
|
-
//
|
|
978
|
+
// Video and Screen Sharing Buttons
|
|
935
979
|
// /////
|
|
936
980
|
|
|
937
981
|
let $videoBtn;
|
|
982
|
+
let $screenshareBtn;
|
|
938
983
|
if (isLocal) {
|
|
939
984
|
$videoBtn = $('<span>').addClass('interface-btn video-btn buttonicon').appendTo($interface);
|
|
940
985
|
this._selfViewButtons.video = {
|
|
@@ -951,11 +996,43 @@ exports.rtc = new class {
|
|
|
951
996
|
if (videoHardDisabled) {
|
|
952
997
|
$videoBtn.attr('title', 'Video disallowed by admin').addClass('disallowed');
|
|
953
998
|
}
|
|
999
|
+
const {navigator: {mediaDevices: {getDisplayMedia} = {}} = {}} = window;
|
|
1000
|
+
$screenshareBtn = $('<span>')
|
|
1001
|
+
.addClass('interface-btn screenshare-btn buttonicon')
|
|
1002
|
+
.css('display', typeof getDisplayMedia === 'function' ? '' : 'none')
|
|
1003
|
+
.appendTo($interface);
|
|
1004
|
+
this._selfViewButtons.screenshare = {
|
|
1005
|
+
get enabled() { return !$screenshareBtn.hasClass('off'); },
|
|
1006
|
+
set enabled(val) {
|
|
1007
|
+
$screenshareBtn
|
|
1008
|
+
.toggleClass('off', !val)
|
|
1009
|
+
.attr('title', val ? 'Stop screen share' : 'Start screen share');
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
this._selfViewButtons.screenshare.enabled = false;
|
|
954
1013
|
addAsyncEventHandlers($videoBtn, videoHardDisabled ? {} : {
|
|
955
1014
|
click: async () => {
|
|
956
1015
|
const videoEnabled = !this._selfViewButtons.video.enabled;
|
|
957
1016
|
_debug(`video button clicked to ${videoEnabled ? 'en' : 'dis'}able video`);
|
|
958
1017
|
this._selfViewButtons.video.enabled = videoEnabled;
|
|
1018
|
+
// Unconditionally disable screen sharing. Either the camera was previously disabled in
|
|
1019
|
+
// which case the user now wants to share camera video, or the camera was previously
|
|
1020
|
+
// enabled in which case the user now wants to shut off all video.
|
|
1021
|
+
this._selfViewButtons.screenshare.enabled = false;
|
|
1022
|
+
await this.updateLocalTracks({updateVideo: true});
|
|
1023
|
+
// Don't use `await` here -- see the comment for the audio button click handler above.
|
|
1024
|
+
this.unmuteAndPlayAll();
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
addAsyncEventHandlers($screenshareBtn, {
|
|
1028
|
+
click: async () => {
|
|
1029
|
+
const screenshareEnabled = !this._selfViewButtons.screenshare.enabled;
|
|
1030
|
+
_debug(`button clicked to ${screenshareEnabled ? 'en' : 'dis'}able screen sharing`);
|
|
1031
|
+
// Unconditionally disable the camera. Either screen sharing was previously disabled in
|
|
1032
|
+
// which case the user now wants to share the screen, or screen sharing was previously
|
|
1033
|
+
// enabled in which case the user wants to shut off all video.
|
|
1034
|
+
this._selfViewButtons.video.enabled = false;
|
|
1035
|
+
this._selfViewButtons.screenshare.enabled = screenshareEnabled;
|
|
959
1036
|
await this.updateLocalTracks({updateVideo: true});
|
|
960
1037
|
// Don't use `await` here -- see the comment for the audio button click handler above.
|
|
961
1038
|
this.unmuteAndPlayAll();
|
|
@@ -1042,7 +1119,7 @@ exports.rtc = new class {
|
|
|
1042
1119
|
{
|
|
1043
1120
|
cls: 'videoended-error-btn',
|
|
1044
1121
|
title: 'Video stopped unexpectedly. Click to retry.',
|
|
1045
|
-
click: () => $videoBtn.click(),
|
|
1122
|
+
click: () => (this._localTracks.videoIsScreenshare ? $screenshareBtn : $videoBtn).click(),
|
|
1046
1123
|
},
|
|
1047
1124
|
] : []),
|
|
1048
1125
|
];
|
|
@@ -1224,7 +1301,7 @@ exports.rtc = new class {
|
|
|
1224
1301
|
// The userLeave hook isn't called until it has been 8s since the peer left. Wait a bit
|
|
1225
1302
|
// longer than that before logging the disconnect to the server.
|
|
1226
1303
|
logDisconnectErrorTimeout =
|
|
1227
|
-
setTimeout(() =>
|
|
1304
|
+
setTimeout(() => logErrorToServer(new Error('RTC connection lost'), 0), 10000);
|
|
1228
1305
|
($videoContainer.data('updateMinSize') || (() => {}))();
|
|
1229
1306
|
await this.setStream(userId, this._blankStream);
|
|
1230
1307
|
});
|