From f094213a343c118eb4cea4a6dc9268314a7adf3c Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Tue, 7 May 2024 20:54:13 -0700 Subject: [PATCH] Improve WebRTC connection for Safari browsers --- src/App.vue | 94 ++++++++++++++++++++++++++++++++++------- src/lib/LocalStorage.js | 1 + src/lib/browsers.js | 12 +++++- 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/src/App.vue b/src/App.vue index 198af52..d66bca4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -37,6 +37,7 @@ const configuration = { }; const FileUploadMaxSize = 1024 * 1024 * 8; // 8 MB +const DebugChannelID = "barertc-debug"; export default { name: 'BareRTC', @@ -156,6 +157,7 @@ export default { muteSounds: false, // mute all sound effects theme: "auto", // auto, light, dark theme appleCompat: isAppleWebkit(), // Apple browser compatibility mode + debug: false, // enable debugging features }, // My video feed. @@ -523,6 +525,9 @@ export default { "prefs.appleCompat": function () { LocalStorage.set('appleCompat', this.prefs.appleCompat); }, + "prefs.debug": function () { + LocalStorage.set('debug', this.prefs.debug); + }, "prefs.theme": function() { LocalStorage.set('theme', this.prefs.theme); }, @@ -900,6 +905,9 @@ export default { if (this.prefs.appleCompat != undefined) { this.prefs.appleCompat = settings.appleCompat === true; } + if (this.prefs.debug != undefined) { + this.prefs.debug = settings.debug === true; + } if (settings.whoSort != undefined) { this.whoSort = settings.whoSort; } @@ -993,6 +1001,16 @@ export default { return; } + // DEBUGGING: enable the 'debug' pref and see the debug channel. + if (this.message.toLowerCase().indexOf("/toggle-debug-settings") === 0) { + this.prefs.debug = !this.prefs.debug; + this.ChatClient( + `Debug tools have been turned: ${this.prefs.debug ? 'on' : 'off'}.`, + ); + this.message = ""; + return; + } + // DEBUGGING: fake set the freeze indicator. let match = this.message.match(/^\/freeze (.+?)$/i); if (match) { @@ -1006,7 +1024,9 @@ export default { // DEBUGGING: test whether the page thinks you're Apple Webkit. if (this.message.toLowerCase().indexOf("/ipad") === 0) { if (this.isAppleWebkit) { - this.ChatClient("I have detected that you are probably an iPad or iPhone browser."); + this.ChatClient("I have detected that you are probably an iPad or iPhone browser.

" + + `* Auto-detection: ${this.isAppleWebkit}
` + + `* Manual setting: ${this.prefs.appleCompat}`); } else { this.ChatClient("I have detected that you are not an iPad or iPhone browser."); } @@ -1322,6 +1342,7 @@ export default { // Send a video request to access a user's camera. sendOpen(username) { + this.DebugChannel(`[WebRTC] Sending "open" message to ask to connect to: ${username}`); this.client.send({ action: "open", username: username, @@ -1341,10 +1362,12 @@ export default { }, onOpen(msg) { // Response for the opener to begin WebRTC connection. + this.DebugChannel(`[WebRTC] Received "open" echo from chat server to connect to: ${msg.username}`); this.startWebRTC(msg.username, true); }, onRing(msg) { // Request from a viewer to see our broadcast. + this.DebugChannel(`[WebRTC] Received "ring" message from chat server to share my video with: ${msg.username}`); this.startWebRTC(msg.username, false); }, onUserExited(msg) { @@ -1518,17 +1541,11 @@ export default { this.WebRTC.pc[username].answerer = pc; } + this.DebugChannel(`[WebRTC] Starting WebRTC with: ${username} (I am the: ${isOfferer ? 'offerer' : 'answerer'})`); + // Keep a pointer to the current channel being established (for candidate/SDP). this.WebRTC.pc[username].connecting = pc; - // Create a data channel so we have something to connect over even if - // the local user is not broadcasting their own camera. - // TODO: adding a dummy data channel might allow iPad to open single directional video - let dataChannel = pc.createDataChannel("data"); - dataChannel.addEventListener("open", event => { - // beginTransmission(dataChannel); - }) - // 'onicecandidate' notifies us whenever an ICE agent needs to deliver a // message to the other peer through the signaling server. pc.onicecandidate = event => { @@ -1554,7 +1571,7 @@ export default { // cam and sending their video on the offer, but we don't want to auto-open their // video, so don't use it) if (!isOfferer && !this.webcam.mutualOpen) { - console.log(`The offerer ${username} gave us a video, but we don't auto-open their video.`); + this.DebugChannel(`[WebRTC] The offerer ${username} gave us a video, but we don't auto-open their video.`); return; } @@ -1606,7 +1623,7 @@ export default { // Set a mute video handler to detect freezes. stream.getVideoTracks().forEach(videoTrack => { let freezeDetected = () => { - console.log("FREEZE DETECTED:", username); + this.DebugChannel("[WebRTC] A video freeze was detected from:", username); // Wait some seconds to see if the stream has recovered on its own setTimeout(() => { // Flag it as likely frozen. @@ -1616,7 +1633,6 @@ export default { }, 7500); // 7.5s }; - console.log("Apply onmute handler for", username); videoTrack.onmute = freezeDetected; // Double check for frozen streams on an interval. @@ -1634,6 +1650,7 @@ export default { // ANSWERER: add our video to the connection so that the offerer (the one who // clicked on our video icon to watch us) can receive it. if (!isOfferer && this.webcam.active) { + this.DebugChannel(`[WebRTC] Answerer: attaching my video to the connection with: ${username}`); let stream = this.webcam.stream; stream.getTracks().forEach(track => { pc.addTrack(track, stream) @@ -1665,6 +1682,7 @@ export default { // NOTE: on Apple devices, always send your video to satisfy the two-way video call // constraint imposed by Safari's WebRTC implementation. if (shouldOfferVideo || this.isAppleWebkit) { + this.DebugChannel(`[WebRTC] Offerer: I am attaching my video to the connection with: ${username}`) let stream = this.webcam.stream; stream.getTracks().forEach(track => { pc.addTrack(track, stream) @@ -1674,6 +1692,7 @@ export default { // If we are the offerer, begin the connection. if (isOfferer) { + this.DebugChannel(`[WebRTC] Offerer: create the offer and send it to ${username}`); pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true, @@ -1685,6 +1704,7 @@ export default { localDescCreated(pc, username) { return (desc) => { pc.setLocalDescription(desc).then(() => { + this.DebugChannel(`[WebRTC] Local description created; sending SDP message to ${username}:

${JSON.stringify(pc.localDescription)}`); this.client.send({ action: "sdp", username: username, @@ -1709,6 +1729,8 @@ export default { // about at all). Re-parse the JSON stringified object here. let candidate = JSON.parse(msg.candidate); + this.DebugChannel(`[WebRTC] ICE candidate from ${msg.username}:

${msg.candidate}`); + // Add the new ICE candidate. pc.addIceCandidate(candidate).catch(e => { console.error(`addIceCandidate: ${e}`); @@ -1720,6 +1742,8 @@ export default { } let pc = this.WebRTC.pc[msg.username].connecting; + this.DebugChannel(`[WebRTC] Received SDP message from ${msg.username}:

${msg.description}`); + // XX: WebRTC candidate/SDP messages JSON stringify their inner payload so that the // Go back-end server won't re-order their json keys (Safari on Mac OS is very sensitive // to the keys being re-ordered during the handshake, in ways that NO OTHER BROWSER cares @@ -1727,13 +1751,16 @@ export default { let message = JSON.parse(msg.description); // Add the new ICE candidate. - // this.ChatClient(`Received a Remote Description from ${msg.username}: ${JSON.stringify(msg.description)}.`); - pc.setRemoteDescription(new RTCSessionDescription(message), () => { + pc.setRemoteDescription(new RTCSessionDescription(message)).then(() => { + this.DebugChannel(`[WebRTC] setRemoteDescription called back OK!
Our pc.remoteDescription.type is: ${pc.remoteDescription.type}`); // When receiving an offer let's answer it. if (pc.remoteDescription.type === 'offer') { + this.DebugChannel(`[WebRTC] Answerer: create SDP answer message for ${msg.username}`); pc.createAnswer().then(this.localDescCreated(pc, msg.username)).catch(this.ChatClient); + } else { + this.DebugChannel(`[WebRTC] pc.remoteDescription.type was not 'offer', we do not need to create an SDP Answer message.`); } - }, console.error); + }).catch(this.DebugChannel); }, onWatch(msg) { // The user has our video feed open now. @@ -1783,7 +1810,9 @@ export default { setChannel(channel) { this.channel = typeof (channel) === "string" ? channel : channel.ID; this.scrollHistory(this.channel, true); - this.channels[this.channel].unread = 0; + if (this.channels[this.channel]) { + this.channels[this.channel].unread = 0; + } // Responsive CSS: switch back to chat panel upon selecting a channel. this.openChatPanel(); @@ -1977,6 +2006,14 @@ export default { } result.push(data); } + + // Is the debug channel enabled? + if (this.prefs.debug) { + result.push({ + ID: DebugChannelID, + Name: "Debug Log", + }); + } return result; }, @@ -2242,6 +2279,7 @@ export default { // Camera is already open? Then disconnect the connection. if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.pc[user.username].offerer != undefined) { + this.DebugChannel(`OpenVideo(${user.username}): already had a connection open, closing it first.`); this.closeVideo(user.username, "offerer"); } @@ -2914,6 +2952,14 @@ export default { isChatClient: true, }); }, + DebugChannel(message) { + this.pushHistory({ + channel: DebugChannelID, + username: "ChatClient", + message: message, + isChatClient: true, + }); + }, // CSS classes for the profile button (color coded genders) profileButtonClass(user) { @@ -3912,6 +3958,22 @@ export default {

+
+ + + Enable the "Debug Log" channel. +

+ This enables a channel where under-the-hood debug messages may be posted, + e.g. to debug WebRTC connection problems. +

+
+ diff --git a/src/lib/LocalStorage.js b/src/lib/LocalStorage.js index 53788c1..c6a8c41 100644 --- a/src/lib/LocalStorage.js +++ b/src/lib/LocalStorage.js @@ -27,6 +27,7 @@ const keys = { 'muteSounds': Boolean, 'closeDMs': Boolean, // close unsolicited DMs 'appleCompat': Boolean, // Apple browser compatibility mode + 'debug': Boolean, // Debug views enabled (admin only) // Don't Show Again on NSFW modals. 'skip-nsfw-modal': Boolean, diff --git a/src/lib/browsers.js b/src/lib/browsers.js index 3b800e1..4df9ee6 100644 --- a/src/lib/browsers.js +++ b/src/lib/browsers.js @@ -2,8 +2,16 @@ // special nuances in their WebRTC video sharing support. This is intended to // detect: iPads, iPhones, and Safari on macOS. function isAppleWebkit() { - // By User-Agent. - if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { + const ua = navigator.userAgent; + + // By User-Agent: Apple mobiles. + if (/iPad|iPhone|iPod/.test(ua)) { + return true; + } + + // Safari browser: claims to be Safari but not Chrome + // (Google Chrome claims to be both) + if (/Safari/i.test(ua) && !/Chrome/i.test(ua)) { return true; }