Add receive-only transceivers to remove Apple compat mode

* WebRTC functionality is now 100% working as intended for Safari and
  iPad browsers!
* The legacy WebRTC API had properties like offerToReceiveVideo
  available on createOffer(), to set up a receive-only channel, but the
  modern WebRTC API had removed these and Safari only supports the
  modern API.
* The modern solution for the same feature is to add a recvonly
  transceiver to the connection in place of offering a local video/audio
  stream to share.
This commit is contained in:
Noah 2024-05-07 21:18:20 -07:00
parent f094213a34
commit f36c83dbcc
2 changed files with 26 additions and 81 deletions

View File

@ -18,7 +18,6 @@ import LocalStorage from './lib/LocalStorage';
import VideoFlag from './lib/VideoFlag'; import VideoFlag from './lib/VideoFlag';
import StatusMessage from './lib/StatusMessage'; import StatusMessage from './lib/StatusMessage';
import { SoundEffects, DefaultSounds } from './lib/sounds'; import { SoundEffects, DefaultSounds } from './lib/sounds';
import { isAppleWebkit } from './lib/browsers';
// WebRTC configuration. // WebRTC configuration.
const configuration = { const configuration = {
@ -156,7 +155,6 @@ export default {
closeDMs: false, // ignore unsolicited DMs closeDMs: false, // ignore unsolicited DMs
muteSounds: false, // mute all sound effects muteSounds: false, // mute all sound effects
theme: "auto", // auto, light, dark theme theme: "auto", // auto, light, dark theme
appleCompat: isAppleWebkit(), // Apple browser compatibility mode
debug: false, // enable debugging features debug: false, // enable debugging features
}, },
@ -522,9 +520,6 @@ export default {
// Tell ChatServer if we have gone to/from DND. // Tell ChatServer if we have gone to/from DND.
this.sendMe(); this.sendMe();
}, },
"prefs.appleCompat": function () {
LocalStorage.set('appleCompat', this.prefs.appleCompat);
},
"prefs.debug": function () { "prefs.debug": function () {
LocalStorage.set('debug', this.prefs.debug); LocalStorage.set('debug', this.prefs.debug);
}, },
@ -819,11 +814,6 @@ export default {
// Default ordering from ChatServer = a-z // Default ordering from ChatServer = a-z
return result; return result;
}, },
isAppleWebkit() {
// Return whether we are detected to be an iPad or iPhone,
// or if the appleCompat seting is enabled.
return isAppleWebkit() || this.prefs.appleCompat;
},
}, },
methods: { methods: {
// Load user prefs from localStorage, called on startup // Load user prefs from localStorage, called on startup
@ -902,9 +892,6 @@ export default {
if (settings.closeDMs != undefined) { if (settings.closeDMs != undefined) {
this.prefs.closeDMs = settings.closeDMs === true; this.prefs.closeDMs = settings.closeDMs === true;
} }
if (this.prefs.appleCompat != undefined) {
this.prefs.appleCompat = settings.appleCompat === true;
}
if (this.prefs.debug != undefined) { if (this.prefs.debug != undefined) {
this.prefs.debug = settings.debug === true; this.prefs.debug = settings.debug === true;
} }
@ -1021,19 +1008,6 @@ export default {
return; return;
} }
// 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.<br><br>" +
`* Auto-detection: ${this.isAppleWebkit}<br>` +
`* Manual setting: ${this.prefs.appleCompat}`);
} else {
this.ChatClient("I have detected that you <strong>are not</strong> an iPad or iPhone browser.");
}
this.message = "";
return;
}
// DEBUGGING: print WebRTC statistics // DEBUGGING: print WebRTC statistics
if (this.message.toLowerCase().indexOf("/debug-webrtc") === 0) { if (this.message.toLowerCase().indexOf("/debug-webrtc") === 0) {
let lines = [ let lines = [
@ -1660,11 +1634,6 @@ export default {
// OFFERER: If we were already broadcasting our own video, and the answerer // OFFERER: If we were already broadcasting our own video, and the answerer
// has the "auto-open your video" setting enabled, attach our video to the initial // has the "auto-open your video" setting enabled, attach our video to the initial
// offer right now. // offer right now.
//
// NOTE: this will force open our video on the answerer's side, and this workflow
// is also the only way that iPads/iPhones/Safari browsers can make a call
// (two-way video is the only option for them; send-only/receive-only channels seem
// not to work in Safari).
if (isOfferer) { if (isOfferer) {
let shouldOfferVideo = ( let shouldOfferVideo = (
(this.whoMap[username].video & this.VideoFlag.MutualOpen) // They auto-open us (this.whoMap[username].video & this.VideoFlag.MutualOpen) // They auto-open us
@ -1679,14 +1648,21 @@ export default {
// Attach our video on the outgoing offer, so that on the answerer's side our // Attach our video on the outgoing offer, so that on the answerer's side our
// local video pops up on their screen. // local video pops up on their screen.
// NOTE: on Apple devices, always send your video to satisfy the two-way video call if (shouldOfferVideo) {
// 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}`) this.DebugChannel(`[WebRTC] Offerer: I am attaching my video to the connection with: ${username}`)
let stream = this.webcam.stream; let stream = this.webcam.stream;
stream.getTracks().forEach(track => { stream.getTracks().forEach(track => {
pc.addTrack(track, stream) pc.addTrack(track, stream)
}); });
} else {
// We aren't offering video, but still want to receive audio/video. Add a receive-only
// transceiver to this offer. NOTE: in the legacy WebRTC API we could put offerToReceiveVideo
// and offerToReceiveAudio in the createOffer() call later, but the modern WebRTC has removed
// those options and Safari only supports the modern way. Adding a receive-only transceiver
// here is the modern way to do it that Safari will be happy with.
this.DebugChannel(`[WebRTC] Offer: I am attaching a receive-only video/audio transceiver to the connection with: ${username}`);
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
} }
} }
@ -2299,20 +2275,9 @@ export default {
delete (this.WebRTC.openTimeouts[user.username]); delete (this.WebRTC.openTimeouts[user.username]);
} }
this.WebRTC.openTimeouts[user.username] = setTimeout(() => { this.WebRTC.openTimeouts[user.username] = setTimeout(() => {
// It timed out. If they are on an iPad, offer additional hints on
// how to have better luck connecting their cameras.
if (this.isAppleWebkit) {
this.ChatClient(
`There was an error opening <strong>${user.username}</strong>'s camera.<br><br>` +
"<strong>Advice:</strong> You appear to be on an iPad-style browser. Webcam sharing " +
"may be limited and only work if:<br>A) You are sharing your own camera first, and<br>B) " +
"The person you view has the setting to auto-open your camera in return.<br>Best of luck!",
);
} else {
this.ChatClient( this.ChatClient(
`There was an error opening <strong>${user.username}</strong>'s camera.`, `There was an error opening <strong>${user.username}</strong>'s camera.`,
); );
}
delete (this.WebRTC.openTimeouts[user.username]); delete (this.WebRTC.openTimeouts[user.username]);
}, 10000); }, 10000);
@ -2500,15 +2465,6 @@ export default {
return 'fa-eye'; return 'fa-eye';
} }
// iPad test: they will have very limited luck opening videos unless
// A) the iPad camera is already on, and
// B) the person they want to watch has mutual auto-open enabled.
if (this.isAppleWebkit) {
if (!this.webcam.active) {
return 'fa-video-slash'; // can not open any cam w/o local video on
}
}
if (this.isVideoNotAllowed(user)) return 'fa-video-slash'; if (this.isVideoNotAllowed(user)) return 'fa-video-slash';
return 'fa-video'; return 'fa-video';
}, },
@ -3641,12 +3597,20 @@ export default {
<!-- Sound settings --> <!-- Sound settings -->
<div v-else-if="settingsModal.tab === 'sounds'"> <div v-else-if="settingsModal.tab === 'sounds'">
<div class="mb-4"> <div class="columns mb-4">
<div class="column">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="prefs.muteSounds" :value="true"> <input type="checkbox" v-model="prefs.muteSounds" :value="true">
Mute all sound effects Mute all sound effects
</label> </label>
</div> </div>
<div class="column">
<label class="checkbox">
<input type="checkbox" v-model="webcam.autoMuteWebcams" :value="true">
Automatically mute webcams
</label>
</div>
</div>
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-2 pr-1"> <div class="column is-2 pr-1">
@ -3940,24 +3904,6 @@ export default {
</p> </p>
</div> </div>
<div class="field">
<label class="label mb-0">
Apple compatibility mode
</label>
<label class="checkbox">
<input type="checkbox"
v-model="prefs.appleCompat"
:value="true">
Check this box if you are on an iPad, iPhone, or Safari browser
</label>
<p class="help">
If you experience difficulty opening cameras and you are on an Apple device (iPad,
iPhone, or the Safari browser on macOS) try enabling this option and see if it will
help. <strong>Note:</strong> You will need to share your webcam first before you can
open successfully open others', due to limitations in Apple's WebRTC implementation.
</p>
</div>
<div class="field" v-if="isOp || prefs.debug"> <div class="field" v-if="isOp || prefs.debug">
<label class="label mb-0"> <label class="label mb-0">
Stats for nerds Stats for nerds

View File

@ -26,7 +26,6 @@ const keys = {
'watchNotif': Boolean, 'watchNotif': Boolean,
'muteSounds': Boolean, 'muteSounds': Boolean,
'closeDMs': Boolean, // close unsolicited DMs 'closeDMs': Boolean, // close unsolicited DMs
'appleCompat': Boolean, // Apple browser compatibility mode
'debug': Boolean, // Debug views enabled (admin only) 'debug': Boolean, // Debug views enabled (admin only)
// Don't Show Again on NSFW modals. // Don't Show Again on NSFW modals.