From a16e9872f06eb14a638290cb074185fcff93a4d7 Mon Sep 17 00:00:00 2001 From: Greg Fodor <gfodor@gmail.com> Date: Wed, 14 Mar 2018 17:54:25 -0700 Subject: [PATCH] Add concurrent run disconnect warning --- package.json | 5 +- src/react-components/ui-root.js | 88 ++++++++++++++++++++++++--- src/room.js | 10 ++- src/utils/concurrent-load-detector.js | 44 ++++++++++++++ yarn.lock | 18 +++--- 5 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 src/utils/concurrent-load-detector.js diff --git a/package.json b/package.json index b952d3a7e..dbf01ec7d 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 7239f2289..2485960a7 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -44,6 +44,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, @@ -54,26 +66,39 @@ 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, }; state = { entryStep: ENTRY_STEPS.start, - shareScreen: false, enterInVR: false, - micDevices: [], + + shareScreen: false, mediaStream: null, + toneInterval: null, tonePlaying: false, + micLevel: 0, + micDevices: [], micUpdateInterval: null, + + autoExitTimerStartedAt: null, + autoExitTimerInterval: null, + secondsRemainingBeforeAutoExit: Infinity, + + exited: false } componentDidMount() { this.setupTestTone(); + this.props.concurrentLoadDetector.addEventListener("concurrentload", this.onConcurrentLoad); } setupTestTone = () => { @@ -108,6 +133,42 @@ class UIRoot extends Component { this.setState({ tonePlaying: false }) } + onConcurrentLoad = () => { + 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(); @@ -274,14 +335,25 @@ 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} - </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 afe163d27..7b70b7aaa 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,11 @@ async function shareMedia(audio, video) { } } +async function exitScene() { + if (NAF.connection && NAF.connection.adapter) { + NAF.connection.disconnect(); + } +} async function enterScene(mediaStream) { const qs = queryString.parse(location.search); @@ -186,7 +194,7 @@ function onConnect() { function mountUI() { getAvailableVREntryTypes().then(availableVREntryTypes => { - ReactDOM.render(<UIRoot {...{ availableVREntryTypes, enterScene }} />, document.getElementById("ui-root")); + ReactDOM.render(<UIRoot {...{ availableVREntryTypes, enterScene, exitScene, concurrentLoadDetector }} />, 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 000000000..f05f619a3 --- /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 ca49471c8..87fa58be5 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" -- GitLab