// Copyright 2016 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import State from './states'; import EventEmitter from 'eventemitter3'; import screenfull from 'screenfull'; /** * WebVR Manager is a utility to handle VR displays */ export default class WebVRManager extends EventEmitter { /** * Construct a new WebVRManager */ constructor() { super(); this.state = State.PREPARING; // Bind vr display present change event to __onVRDisplayPresentChange this.__onVRDisplayPresentChange = this.__onVRDisplayPresentChange.bind(this); window.addEventListener('vrdisplaypresentchange', this.__onVRDisplayPresentChange); this.__onChangeFullscreen = this.__onChangeFullscreen.bind(this); if (screenfull.enabled) { document.addEventListener(screenfull.raw.fullscreenchange, this.__onChangeFullscreen); } } /** * Check if the browser is compatible with WebVR and has headsets. * @return {Promise} */ checkDisplays() { return WebVRManager.getVRDisplay() .then((display) => { this.defaultDisplay = display; this.__setState(State.READY_TO_PRESENT); return display; }) .catch((e) => { delete this.defaultDisplay; if (e.name == 'NO_DISPLAYS') { this.__setState(State.ERROR_NO_PRESENTABLE_DISPLAYS); } else if (e.name == 'WEBVR_UNSUPPORTED') { this.__setState(State.ERROR_BROWSER_NOT_SUPPORTED); } else { this.__setState(State.ERROR_UNKOWN); } }); } /** * clean up object for garbage collection */ remove() { window.removeEventListener('vrdisplaypresentchange', this.__onVRDisplayPresentChange); if (screenfull.enabled) { document.removeEventListener(screenfull.raw.fullscreenchanged, this.__onChangeFullscreen); } this.removeAllListeners(); } /** * returns promise returning list of available VR displays. * @return {Promise} */ static getVRDisplay() { return new Promise((resolve, reject) => { if (!navigator || !navigator.getVRDisplays) { let e = new Error('Browser not supporting WebVR'); e.name = 'WEBVR_UNSUPPORTED'; reject(e); return; } const rejectNoDisplay = ()=> { // No displays are found. let e = new Error('No displays found'); e.name = 'NO_DISPLAYS'; reject(e); }; navigator.getVRDisplays().then( function(displays) { // Promise succeeds, but check if there are any displays actually. for (let i = 0; i < displays.length; i++) { if (displays[i].capabilities.canPresent) { resolve(displays[i]); break; } } rejectNoDisplay(); }, rejectNoDisplay); }); } /** * Enter presentation mode with your set VR display * @param {VRDisplay} display the display to request present on * @param {HTMLCanvasElement} canvas * @return {Promise.} */ enterVR(display, canvas) { this.presentedSource = canvas; return display.requestPresent([{ source: canvas, }]) .then( ()=> {}, // this could fail if: // 1. Display `canPresent` is false // 2. Canvas is invalid // 3. not executed via user interaction ()=> this.__setState(State.ERROR_REQUEST_TO_PRESENT_REJECTED) ); } /** * Exit presentation mode on display * @param {VRDisplay} display * @return {Promise.} */ exitVR(display) { return display.exitPresent() .then( ()=> { this.presentedSource = undefined; }, // this could fail if: // 1. exit requested while not currently presenting ()=> this.__setState(State.ERROR_EXIT_PRESENT_REJECTED) ); } /** * Enter fullscreen mode * @param {HTMLCanvasElement} canvas * @return {boolean} */ enterFullscreen(canvas) { if (screenfull.enabled) { screenfull.request(canvas); } else { // iOS this.__setState(State.PRESENTING_FULLSCREEN); } return true; } /** * Exit fullscreen mode * @return {boolean} */ exitFullscreen() { if (screenfull.enabled && screenfull.isFullscreen) { screenfull.exit(); } else if (this.state == State.PRESENTING_FULLSCREEN) { this.checkDisplays(); } return true; } /** * Change the state of the manager * @param {State} state * @private */ __setState(state) { if (state != this.state) { this.emit('change', state, this.state); this.state = state; } } /** * Triggered on fullscreen change event * @param {Event} e * @private */ __onChangeFullscreen(e) { if (screenfull.isFullscreen) { this.__setState(State.PRESENTING_FULLSCREEN); } else { this.checkDisplays(); } } /** * Triggered on vr present change * @param {Event} event * @private */ __onVRDisplayPresentChange(event) { try { // Polyfill stores display under detail let display = event.display ? event.display : event.detail.display; if(display.isPresenting && display.getLayers()[0].source !== this.presentedSource) { // this means a different instance of WebVRManager has requested to present return; } const isPresenting = this.defaultDisplay && this.defaultDisplay.isPresenting; this.__setState(isPresenting ? State.PRESENTING : State.READY_TO_PRESENT); } catch(err) { // continue regardless of error } } }