diff --git a/package.json b/package.json index b952d3a7ee378bc854b2e2adf13a28d9f3906727..dbf01ec7d1fdef208278e082ad657c386f25d948 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,11 @@ "aframe-teleport-controls": "https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin", "aframe-xr": "github:brianpeiris/aframe-xr#3162aed", "detect-browser": "^2.1.0", + "event-target-shim": "^3.0.1", "jsonschema": "^1.2.2", "material-design-lite": "^1.3.0", - "minijanus": "^0.4.0", - "naf-janus-adapter": "^0.4.0", + "minijanus": "https://github.com/mozilla/minijanus.js#master", + "naf-janus-adapter": "https://github.com/mozilla/naf-janus-adapter#feature/disconnect", "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master", "nipplejs": "^0.6.7", "query-string": "^5.0.1", diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index a9f97fa586deab5eb4c855513c14316a82a3a021..f38d9817649f4bf68ce57e44d17b3f2644046793 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -46,6 +46,18 @@ const DaydreamEntryButton = (props) => ( </button> ); +const AutoExitWarning = (props) => ( + <div> + <p> + Exit in <span>{props.secondsRemaining}</span> + </p> + + <button onClick={props.onCancel}> + Cancel + </button> + </div> +); + // This is a list of regexes that match the microphone labels of HMDs. // // If entering VR mode, and if any of these regexes match an audio device, @@ -56,28 +68,44 @@ const DaydreamEntryButton = (props) => ( // then we rely upon the user to select the proper mic. const VR_DEVICE_MIC_LABEL_REGEXES = []; +const AUTO_EXIT_TIMER_SECONDS = 10; + class UIRoot extends Component { static propTypes = { enterScene: PropTypes.func, - availableVREntryTypes: PropTypes.object + availableVREntryTypes: PropTypes.object, + concurrentLoadDetector: PropTypes.object, + disableAutoExitOnConcurrentLoad: PropTypes.bool } store = new Store() state = { entryStep: ENTRY_STEPS.start, - shareScreen: false, enterInVR: false, - micDevices: [], + + shareScreen: false, mediaStream: null, + toneInterval: null, tonePlaying: false, + micLevel: 0, + micDevices: [], micUpdateInterval: null, + + profileNamePending: "Hello", + + autoExitTimerStartedAt: null, + autoExitTimerInterval: null, + secondsRemainingBeforeAutoExit: Infinity, + + exited: false } componentDidMount() { this.setupTestTone(); + this.props.concurrentLoadDetector.addEventListener("concurrentload", this.onConcurrentLoad); } setupTestTone = () => { @@ -112,6 +140,44 @@ class UIRoot extends Component { this.setState({ tonePlaying: false }) } + onConcurrentLoad = () => { + if (this.props.disableAutoExitOnConcurrentLoad) return; + + const autoExitTimerInterval = setInterval(() => { + let secondsRemainingBeforeAutoExit = Infinity; + + if (this.state.autoExitTimerStartedAt) { + const secondsSinceStart = (new Date() - this.state.autoExitTimerStartedAt) / 1000; + secondsRemainingBeforeAutoExit = Math.max(0, Math.floor(AUTO_EXIT_TIMER_SECONDS - secondsSinceStart)); + } + + this.setState({ secondsRemainingBeforeAutoExit }); + this.checkForAutoExit(); + }, 500); + + this.setState({ autoExitTimerStartedAt: new Date(), autoExitTimerInterval }) + } + + checkForAutoExit = () => { + if (this.state.secondsRemainingBeforeAutoExit !== 0) return; + this.endAutoExitTimer(); + this.exit(); + } + + exit = () => { + this.props.exitScene(); + this.setState({ exited: true }); + } + + isWaitingForAutoExit = () => { + return this.state.secondsRemainingBeforeAutoExit <= AUTO_EXIT_TIMER_SECONDS; + } + + endAutoExitTimer = () => { + clearInterval(this.state.autoExitTimerInterval); + this.setState({ autoExitTimerStartedAt: null, autoExitTimerInterval: null, secondsRemainingBeforeAutoExit: Infinity }); + } + performDirectEntryFlow = async (enterInVR) => { this.startTestTone(); @@ -301,15 +367,26 @@ class UIRoot extends Component { </div> ) : null; - return ( - <div> - UI Here + const overlay = this.isWaitingForAutoExit() ? + (<AutoExitWarning secondsRemaining={this.state.secondsRemainingBeforeAutoExit} onCancel={this.endAutoExitTimer} />) : + (<div> {entryPanel} {micPanel} {audioSetupPanel} {nameEntryPanel} - </div> - ); + </div> + ); + + return !this.state.exited ? + ( + <div> + Base UI here + {overlay} + </div> + ) : + ( + <div>Exited</div> + ) } } diff --git a/src/room.js b/src/room.js index 2f01a1382dc9c0d5926c675eed3feb88723a5208..3dbc131746a9475ec8d54a0b0dc474f77d5a5158 100644 --- a/src/room.js +++ b/src/room.js @@ -70,6 +70,7 @@ import Store from "./storage/store"; import { generateDefaultProfile } from "./utils/identity.js"; import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js"; +import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; AFRAME.registerInputBehaviour("vive_trackpad_dpad4", vive_trackpad_dpad4); AFRAME.registerInputBehaviour("oculus_touch_joystick_dpad4", oculus_touch_joystick_dpad4); @@ -82,6 +83,8 @@ registerNetworkSchemas(); registerTelemetry(); const store = new Store(); +const concurrentLoadDetector = new ConcurrentLoadDetector(); +concurrentLoadDetector.start(); // Always layer in any new default profile bits store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) }}) @@ -112,6 +115,15 @@ async function shareMedia(audio, video) { } } +async function exitScene() { + if (NAF.connection && NAF.connection.adapter) { + NAF.connection.disconnect(); + } + + const scene = document.querySelector("a-scene"); + scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this + document.body.removeChild(scene); +} async function enterScene(mediaStream) { const qs = queryString.parse(location.search); @@ -186,10 +198,16 @@ function onConnect() { function mountUI() { getAvailableVREntryTypes().then(availableVREntryTypes => { - ReactDOM.render( - <UIRoot {...{ availableVREntryTypes, enterScene }} />, - document.getElementById("ui-root") - ); + const qs = queryString.parse(location.search); + const disableAutoExitOnConcurrentLoad = qs.allow_multi === "true" + + ReactDOM.render(<UIRoot {...{ + availableVREntryTypes, + enterScene, + exitScene, + concurrentLoadDetector, + disableAutoExitOnConcurrentLoad + }} />, document.getElementById("ui-root")); document.getElementById("loader").style.display = "none"; }); } diff --git a/src/utils/concurrent-load-detector.js b/src/utils/concurrent-load-detector.js new file mode 100644 index 0000000000000000000000000000000000000000..f05f619a3f51fab56a33acaec2eeb3b75e48ccea --- /dev/null +++ b/src/utils/concurrent-load-detector.js @@ -0,0 +1,44 @@ +// Detects if another instance of ConcurrentLoadDetector is start()'ed by in the same local storage +// context with the same instance key. Once a duplicate run is detected this will not fire any additional +// events. + +const LOCAL_STORE_KEY = "___concurrent_load_detector"; +import { EventTarget } from "event-target-shim" + +export default class ConcurrentLoadDetector extends EventTarget { + constructor(instanceKey) { + super(); + + this.interval = null; + this.startedAt = null; + this.instanceKey = instanceKey || "global"; + } + + start = () => { + this.startedAt = new Date(); + localStorage.setItem(this.localStorageKey(), JSON.stringify({ started_at: this.startedAt })); + + // Check for concurrent load every second + this.interval = setInterval(this._step, 1000); + } + + stop = () => { + if (this.interval) { + clearInterval(this.interval); + } + } + + localStorageKey = () => { + return `${LOCAL_STORE_KEY}_${this.instanceKey}`; + } + + _step = () => { + const currentState = JSON.parse(localStorage.getItem(this.localStorageKey())); + const maxStartedAt = new Date(currentState.started_at); + + if (maxStartedAt.getTime() !== this.startedAt.getTime()) { + this.dispatchEvent(new CustomEvent("concurrentload")); + this.stop(); + } + } +} diff --git a/yarn.lock b/yarn.lock index ca49471c81719770f30b8ce7a5765d9bb0490685..87fa58be5e1561fc1fc25ece01cba7cd4852132e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2558,6 +2558,10 @@ etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" +event-target-shim@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-3.0.1.tgz#a4a62f0795e5b65363e86c6780413224d1eea688" + eventemitter3@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" @@ -4396,9 +4400,9 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" -minijanus@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.4.0.tgz#4d08529da795886b1aab6714ee7c9ff122c8c802" +"minijanus@https://github.com/mozilla/minijanus.js#master": + version "0.5.0" + resolved "https://github.com/mozilla/minijanus.js#497f4dd80fdb92e247238e638daed14ae6623575" minimalistic-assert@^1.0.0: version "1.0.0" @@ -4505,12 +4509,12 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -naf-janus-adapter@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.4.0.tgz#22f14212a14d9e3d30c8d9441978704ff58392f4" +"naf-janus-adapter@https://github.com/mozilla/naf-janus-adapter#feature/disconnect": + version "0.4.1" + resolved "https://github.com/mozilla/naf-janus-adapter#4a4532014d6489403cf7e451790925ce747f8e41" dependencies: debug "^3.1.0" - minijanus "^0.4.0" + minijanus "https://github.com/mozilla/minijanus.js#master" nan@^2.3.0: version "2.9.1"