diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index 3c8c454b2466925cf6e2ed8c758365e58886cd16..2915f56da8f98f3fc79bda2b147d3750179c5dbc 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -19,13 +19,13 @@ flex: 10 1 auto; justify-content: center; - &__screensharing { + &__screen-sharing { font-size: 1.4em; margin-left: 2.95em; margin-top: 0.6em; } - &__screensharing-checkbox { + &__screen-sharing-checkbox { appearance: none; -moz-appearance: none; -webkit-appearance: none; @@ -36,7 +36,7 @@ vertical-align: sub; margin: 0 0.6em } - &__screensharing-checkbox:checked { + &__screen-sharing-checkbox:checked { border: 9px double white; outline: 9px solid white; outline-offset: -18px; diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index a871f843872b557eeab2e6330e92ce225e9c2a5f..04b25e90c782c04b60860524f98be0600627fcfe 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -12,7 +12,7 @@ "entry.daydream-prefix": "Enter on ", "entry.daydream-medium": "Daydream", "entry.daydream-via-chrome": "Using Google Chrome", - "entry.enable-screensharing": "Share my desktop", + "entry.enable-screen-sharing": "Share my desktop", "profile.save": "SAVE", "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32", "profile.header": "Your identity", diff --git a/src/components/networked-video-player.js b/src/components/networked-video-player.js index 03cfba4f6d7a835ac825804724a4109d16690cdb..7d73cc9244613fdfcec43686a14046a64cc4894c 100644 --- a/src/components/networked-video-player.js +++ b/src/components/networked-video-player.js @@ -1,3 +1,5 @@ +import queryString from "query-string"; + import styles from "./networked-video-player.css"; const nafConnected = function() { @@ -9,14 +11,6 @@ const nafConnected = function() { AFRAME.registerComponent("networked-video-player", { schema: {}, async init() { - let container = document.getElementById("nvp-debug-container"); - if (!container) { - container = document.createElement("div"); - container.id = "nvp-debug-container"; - container.classList.add(styles.container); - document.body.appendChild(container); - } - await nafConnected(); const networkedEl = await NAF.utils.getNetworkedEntity(this.el); @@ -25,6 +19,22 @@ AFRAME.registerComponent("networked-video-player", { } const ownerId = networkedEl.components.networked.data.owner; + + const qs = queryString.parse(location.search); + const rejectScreenShares = qs.accept_screen_shares === undefined; + if (ownerId !== NAF.clientId && rejectScreenShares) { + this.el.setAttribute("visible", false); + return; + } + + let container = document.getElementById("nvp-debug-container"); + if (!container) { + container = document.createElement("div"); + container.id = "nvp-debug-container"; + container.classList.add(styles.container); + document.body.appendChild(container); + } + const stream = await NAF.connection.adapter.getMediaStream(ownerId, "video"); if (!stream) { return; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index da2f70bd780ecd2c216379c4308354d3b36e60d7..043960495cef9631b4fab13de59ec3d275fa3803 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -57,6 +57,7 @@ class UIRoot extends Component { concurrentLoadDetector: PropTypes.object, disableAutoExitOnConcurrentLoad: PropTypes.bool, forcedVREntryType: PropTypes.string, + enableScreenSharing: PropTypes.bool, store: PropTypes.object, scene: PropTypes.object } @@ -69,6 +70,8 @@ class UIRoot extends Component { shareScreen: false, requestedScreen: false, mediaStream: null, + videoTrack: null, + audioTrack: null, toneInterval: null, tonePlaying: false, @@ -186,9 +189,12 @@ class UIRoot extends Component { hasGrantedMicPermissions = async () => { if (this.state.requestedScreen) { - // If we've already requested the screen in this session, then we can already enumerateDevices, so we need to - // verify mic permissions by checking the mediaStream. - return this.state.mediaStream && this.state.mediaStream.getAudioTracks().length > 0; + // There is no way to tell if you've granted mic permissions in a previous session if we've + // already prompted for screen sharing permissions, so we have to assume that we've never granted permissions. + // Fortunately, if you *have* granted permissions permanently, there won't be a second browser prompt, but we + // can't determine that before hand. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1449783 for a potential solution in the future. + return false; } else { // If we haven't requested the screen in this session, check if we've granted permissions in a previous session. @@ -249,44 +255,60 @@ class UIRoot extends Component { } } - mediaVideoConstraint = () => { - return this.state.shareScreen ? { mediaSource: "screen", height: 720, frameRate: 30 } : false; - } - micDeviceChanged = async (ev) => { - const constraints = { audio: { deviceId: { exact: [ev.target.value] } }, video: this.mediaVideoConstraint() }; - await this.setupNewMediaStream(constraints); + const constraints = { audio: { deviceId: { exact: [ev.target.value] } } }; + await this.fetchAudioTrack(constraints); + await this.setupNewMediaStream(); } setMediaStreamToDefault = async () => { - await this.setupNewMediaStream({ audio: true, video: this.mediaVideoConstraint() }); + await this.fetchAudioTrack({ audio: true }); + await this.setupNewMediaStream(); } - setStateAndRequestScreen = (e) => { + setStateAndRequestScreen = async (e) => { const checked = e.target.checked; - this.setState({ requestedScreen: true, shareScreen: checked }, () => { - this.setupNewMediaStream({ video: this.mediaVideoConstraint() }); - }); + await this.setState({ requestedScreen: true, shareScreen: checked }); + if (checked) { + this.fetchVideoTrack({ video: { + mediaSource: "screen", + // Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything + // other than your current monitor that has a different aspect ratio. + width: screen.width / screen.height * 720, + height: 720, + frameRate: 30 + } }); + } + else { + this.setState({ videoTrack: null }); + } } - setupNewMediaStream = async (constraints) => { - const AudioContext = window.AudioContext || window.webkitAudioContext; - const audioContext = new AudioContext(); + fetchVideoTrack = async (constraints) => { + const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + this.setState({ videoTrack: mediaStream.getVideoTracks()[0] }); + } - if (this.state.mediaStream) { - clearInterval(this.state.micUpdateInterval); + fetchAudioTrack = async (constraints) => { + if (this.state.audioTrack) { + this.state.audioTrack.stop(); + } + const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + this.setState({ audioTrack: mediaStream.getAudioTracks()[0] }); + } - const previousStream = this.state.mediaStream; + setupNewMediaStream = async (constraints) => { + const mediaStream = new MediaStream(); - for (const tracks of [previousStream.getAudioTracks(), previousStream.getVideoTracks()]) { - for (const track of tracks) { - track.stop(); - } - } - } + // we should definitely have an audioTrack at this point. + mediaStream.addTrack(this.state.audioTrack); - const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + if (this.state.videoTrack) { + mediaStream.addTrack(this.state.videoTrack); + } + const AudioContext = window.AudioContext || window.webkitAudioContext; + const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(mediaStream); const analyzer = audioContext.createAnalyser(); const levels = new Uint8Array(analyzer.fftSize); @@ -331,7 +353,11 @@ class UIRoot extends Component { fetchMicDevices = async () => { const mediaDevices = await navigator.mediaDevices.enumerateDevices(); - this.setState({ micDevices: mediaDevices.filter(d => d.kind === "audioinput").map(d => ({ deviceId: d.deviceId, label: d.label }))}); + this.setState({ + micDevices: mediaDevices. + filter(d => d.kind === "audioinput"). + map(d => ({ deviceId: d.deviceId, label: d.label })) + }); } shouldShowHmdMicWarning = () => { @@ -410,6 +436,23 @@ class UIRoot extends Component { const daydreamMaybeSubtitle = messages["entry.daydream-via-chrome"]; + // Only show this in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and + // will attempt to share your webcam instead! + const screenSharingCheckbox = ( + this.props.enableScreenSharing && + !mobiledetect.mobile() && + /firefox/i.test(navigator.userAgent) && + ( + <label className="entry-panel__screen-sharing"> + <input className="entry-panel__screen-sharing-checkbox" type="checkbox" + value={this.state.shareScreen} + onChange={this.setStateAndRequestScreen} + /> + <FormattedMessage id="entry.enable-screen-sharing" /> + </label> + ) + ); + const entryPanel = this.state.entryStep === ENTRY_STEPS.start ? ( <div className="entry-panel"> @@ -422,14 +465,7 @@ class UIRoot extends Component { subtitle={this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : "" }/> } { this.state.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && (<div className="entry-panel__secondary" onClick={this.enterVR}><FormattedMessage id="entry.cardboard"/></div>) } - { !mobiledetect.mobile() && /firefox/i.test(navigator.userAgent) && ( - <label className="entry-panel__screensharing"> - <input className="entry-panel__screensharing-checkbox" type="checkbox" - value={this.state.shareScreen} - onChange={this.setStateAndRequestScreen} - /> - <FormattedMessage id="entry.enable-screensharing" /> - </label>) } + { screenSharingCheckbox } </div> ) : null; diff --git a/src/room.html b/src/room.html index 52fd9ddd2a9ab2cdc4b54dba0f0093c55dee0d7f..339b2ec76d4d14327619bd6d31c32d37a82ae517 100644 --- a/src/room.html +++ b/src/room.html @@ -44,7 +44,7 @@ <!-- Templates --> <template id="video-template"> - <a-entity class="video" geometry="primitive: plane;" material="side: double" networked-video-player></a-entity> + <a-entity class="video" geometry="primitive: plane;" material="side: double; shader: flat;" networked-video-player></a-entity> </template> <template id="remote-avatar-template"> diff --git a/src/room.js b/src/room.js index cac5b55a96cdd1a6d5cd9d4d85b2fb2824e644cd..be559c209526c4255b12dffe8165ec333ab9249c 100644 --- a/src/room.js +++ b/src/room.js @@ -81,6 +81,12 @@ import { generateDefaultProfile } from "./utils/identity.js"; import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; +function qsTruthy(param) { + const val = qs[param]; + // if the param exists but is not set (e.g. "?foo&bar"), its value is null. + return val === null || /1|on|true/i.test(val); +} + registerTelemetry(); AFRAME.registerInputBehaviour("vive_trackpad_dpad4", vive_trackpad_dpad4); @@ -121,18 +127,16 @@ async function enterScene(mediaStream, enterInVR) { document.querySelector("#player-camera").setAttribute("look-controls", "pointerLockEnabled: true;"); - const qs = queryString.parse(location.search); - scene.setAttribute("networked-scene", { - room: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1, + room: qs.room && !isNaN(parseInt(qs.room, 10)) ? parseInt(qs.room, 10) : 1, serverURL: process.env.JANUS_SERVER }); - if (!qs.stats || !/off|false|0/.test(qs.stats)) { + if (!qsTruthy("no_stats")) { scene.setAttribute("stats", true); } - if (isMobile || qs.mobile) { + if (isMobile || qsTruthy("mobile")) { const playerRig = document.querySelector("#player-rig"); playerRig.setAttribute("virtual-gamepad-controls", {}); } @@ -140,7 +144,7 @@ async function enterScene(mediaStream, enterInVR) { setNameTagFromStore(); store.addEventListener('statechanged', setNameTagFromStore); - const avatarScale = parseInt(qs.avatarScale, 10); + const avatarScale = parseInt(qs.avatar_scale, 10); if (avatarScale) { playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale }); @@ -168,7 +172,7 @@ async function enterScene(mediaStream, enterInVR) { screenEntity.setAttribute("visible", sharingScreen); }); - if (qs.offline) { + if (qsTruthy("offline")) { onConnect(); } else { document.body.addEventListener("connected", onConnect); @@ -201,12 +205,9 @@ function onConnect() { function mountUI(scene) { const qs = queryString.parse(location.search); - const disableAutoExitOnConcurrentLoad = qs.allow_multi === "true" - let forcedVREntryType = null; - - if (qs.vr_entry_type) { - forcedVREntryType = qs.vr_entry_type; - } + const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); + const forcedVREntryType = qs.vr_entry_type || null; + const enableScreenSharing = qsTruthy("enable_screen_sharing"); const uiRoot = ReactDOM.render(<UIRoot {...{ scene, @@ -215,6 +216,7 @@ function mountUI(scene) { concurrentLoadDetector, disableAutoExitOnConcurrentLoad, forcedVREntryType, + enableScreenSharing, store }} />, document.getElementById("ui-root"));