videojs-vr
Version:
A plugin to add 360 and VR video support to video.js.
872 lines (711 loc) • 30.4 kB
JavaScript
import {version as VERSION} from '../package.json';
import window from 'global/window';
import document from 'global/document';
import WebVRPolyfill from 'webvr-polyfill/src/webvr-polyfill';
import videojs from 'video.js';
import * as THREE from 'three';
import VRControls from '../vendor/three/VRControls.js';
import VREffect from '../vendor/three/VREffect.js';
import OrbitOrientationContols from './orbit-orientation-controls.js';
import * as utils from './utils';
import CanvasPlayerControls from './canvas-player-controls';
import OmnitoneController from './omnitone-controller';
// import controls so they get regisetered with videojs
import './cardboard-button';
import './big-vr-play-button';
// Default options for the plugin.
const defaults = {
debug: false,
omnitone: false,
forceCardboard: false,
omnitoneOptions: {},
projection: 'AUTO',
sphereDetail: 32,
disableTogglePlay: false
};
const errors = {
'web-vr-out-of-date': {
headline: '360 is out of date',
type: '360_OUT_OF_DATE',
message: "Your browser supports 360 but not the latest version. See <a href='http://webvr.info'>http://webvr.info</a> for more info."
},
'web-vr-not-supported': {
headline: '360 not supported on this device',
type: '360_NOT_SUPPORTED',
message: "Your browser does not support 360. See <a href='http://webvr.info'>http://webvr.info</a> for assistance."
},
'web-vr-hls-cors-not-supported': {
headline: '360 HLS video not supported on this device',
type: '360_NOT_SUPPORTED',
message: "Your browser/device does not support HLS 360 video. See <a href='http://webvr.info'>http://webvr.info</a> for assistance."
}
};
const Plugin = videojs.getPlugin('plugin');
const Component = videojs.getComponent('Component');
class VR extends Plugin {
constructor(player, options) {
const settings = videojs.mergeOptions(defaults, options);
super(player, settings);
this.options_ = settings;
this.player_ = player;
this.bigPlayButtonIndex_ = player.children().indexOf(player.getChild('BigPlayButton')) || 0;
// custom videojs-errors integration boolean
this.videojsErrorsSupport_ = !!videojs.errors;
if (this.videojsErrorsSupport_) {
player.errors({errors});
}
// IE 11 does not support enough webgl to be supported
// older safari does not support cors, so it wont work
if (videojs.browser.IE_VERSION || !utils.corsSupport) {
// if a player triggers error before 'loadstart' is fired
// video.js will reset the error overlay
this.player_.on('loadstart', () => {
this.triggerError_({code: 'web-vr-not-supported', dismiss: false});
});
return;
}
this.polyfill_ = new WebVRPolyfill({
// do not show rotate instructions
ROTATE_INSTRUCTIONS_DISABLED: true
});
this.polyfill_ = new WebVRPolyfill();
this.handleVrDisplayActivate_ = videojs.bind(this, this.handleVrDisplayActivate_);
this.handleVrDisplayDeactivate_ = videojs.bind(this, this.handleVrDisplayDeactivate_);
this.handleResize_ = videojs.bind(this, this.handleResize_);
this.animate_ = videojs.bind(this, this.animate_);
this.setProjection(this.options_.projection);
// any time the video element is recycled for ads
// we have to reset the vr state and re-init after ad
this.on(player, 'adstart', () => player.setTimeout(() => {
// if the video element was recycled for this ad
if (!player.ads || !player.ads.videoElementRecycled()) {
this.log('video element not recycled for this ad, no need to reset');
return;
}
this.log('video element recycled for this ad, reseting');
this.reset();
this.one(player, 'playing', this.init);
}), 1);
this.on(player, 'loadedmetadata', this.init);
}
changeProjection_(projection) {
projection = utils.getInternalProjectionName(projection);
// don't change to an invalid projection
if (!projection) {
projection = 'NONE';
}
const position = {x: 0, y: 0, z: 0 };
if (this.scene) {
this.scene.remove(this.movieScreen);
}
if (projection === 'AUTO') {
// mediainfo cannot be set to auto or we would infinite loop here
// each source should know whatever they are 360 or not, if using AUTO
if (this.player_.mediainfo && this.player_.mediainfo.projection && this.player_.mediainfo.projection !== 'AUTO') {
const autoProjection = utils.getInternalProjectionName(this.player_.mediainfo.projection);
return this.changeProjection_(autoProjection);
}
return this.changeProjection_('NONE');
} else if (projection === '360') {
this.movieGeometry = new THREE.SphereBufferGeometry(256, this.options_.sphereDetail, this.options_.sphereDetail);
this.movieMaterial = new THREE.MeshBasicMaterial({ map: this.videoTexture, overdraw: true, side: THREE.BackSide });
this.movieScreen = new THREE.Mesh(this.movieGeometry, this.movieMaterial);
this.movieScreen.position.set(position.x, position.y, position.z);
this.movieScreen.scale.x = -1;
this.movieScreen.quaternion.setFromAxisAngle({x: 0, y: 1, z: 0}, -Math.PI / 2);
this.scene.add(this.movieScreen);
} else if (projection === '360_LR' || projection === '360_TB') {
// Left eye view
let geometry = new THREE.SphereGeometry(
256,
this.options_.sphereDetail,
this.options_.sphereDetail
);
let uvs = geometry.faceVertexUvs[ 0 ];
for (let i = 0; i < uvs.length; i++) {
for (let j = 0; j < 3; j++) {
if (projection === '360_LR') {
uvs[ i ][ j ].x *= 0.5;
} else {
uvs[ i ][ j ].y *= 0.5;
uvs[ i ][ j ].y += 0.5;
}
}
}
this.movieGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
this.movieMaterial = new THREE.MeshBasicMaterial({ map: this.videoTexture, overdraw: true, side: THREE.BackSide });
this.movieScreen = new THREE.Mesh(this.movieGeometry, this.movieMaterial);
this.movieScreen.scale.x = -1;
this.movieScreen.quaternion.setFromAxisAngle({x: 0, y: 1, z: 0}, -Math.PI / 2);
// display in left eye only
this.movieScreen.layers.set(1);
this.scene.add(this.movieScreen);
// Right eye view
geometry = new THREE.SphereGeometry(
256,
this.options_.sphereDetail,
this.options_.sphereDetail
);
uvs = geometry.faceVertexUvs[ 0 ];
for (let i = 0; i < uvs.length; i++) {
for (let j = 0; j < 3; j++) {
if (projection === '360_LR') {
uvs[ i ][ j ].x *= 0.5;
uvs[ i ][ j ].x += 0.5;
} else {
uvs[ i ][ j ].y *= 0.5;
}
}
}
this.movieGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
this.movieMaterial = new THREE.MeshBasicMaterial({ map: this.videoTexture, overdraw: true, side: THREE.BackSide });
this.movieScreen = new THREE.Mesh(this.movieGeometry, this.movieMaterial);
this.movieScreen.scale.x = -1;
this.movieScreen.quaternion.setFromAxisAngle({x: 0, y: 1, z: 0}, -Math.PI / 2);
// display in right eye only
this.movieScreen.layers.set(2);
this.scene.add(this.movieScreen);
} else if (projection === '360_CUBE') {
this.movieGeometry = new THREE.BoxGeometry(256, 256, 256);
this.movieMaterial = new THREE.MeshBasicMaterial({ map: this.videoTexture, overdraw: true, side: THREE.BackSide });
const left = [new THREE.Vector2(0, 0.5), new THREE.Vector2(0.333, 0.5), new THREE.Vector2(0.333, 1), new THREE.Vector2(0, 1)];
const right = [new THREE.Vector2(0.333, 0.5), new THREE.Vector2(0.666, 0.5), new THREE.Vector2(0.666, 1), new THREE.Vector2(0.333, 1)];
const top = [new THREE.Vector2(0.666, 0.5), new THREE.Vector2(1, 0.5), new THREE.Vector2(1, 1), new THREE.Vector2(0.666, 1)];
const bottom = [new THREE.Vector2(0, 0), new THREE.Vector2(0.333, 0), new THREE.Vector2(0.333, 0.5), new THREE.Vector2(0, 0.5)];
const front = [new THREE.Vector2(0.333, 0), new THREE.Vector2(0.666, 0), new THREE.Vector2(0.666, 0.5), new THREE.Vector2(0.333, 0.5)];
const back = [new THREE.Vector2(0.666, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 0.5), new THREE.Vector2(0.666, 0.5)];
this.movieGeometry.faceVertexUvs[0] = [];
this.movieGeometry.faceVertexUvs[0][0] = [ right[2], right[1], right[3] ];
this.movieGeometry.faceVertexUvs[0][1] = [ right[1], right[0], right[3] ];
this.movieGeometry.faceVertexUvs[0][2] = [ left[2], left[1], left[3] ];
this.movieGeometry.faceVertexUvs[0][3] = [ left[1], left[0], left[3] ];
this.movieGeometry.faceVertexUvs[0][4] = [ top[2], top[1], top[3] ];
this.movieGeometry.faceVertexUvs[0][5] = [ top[1], top[0], top[3] ];
this.movieGeometry.faceVertexUvs[0][6] = [ bottom[2], bottom[1], bottom[3] ];
this.movieGeometry.faceVertexUvs[0][7] = [ bottom[1], bottom[0], bottom[3] ];
this.movieGeometry.faceVertexUvs[0][8] = [ front[2], front[1], front[3] ];
this.movieGeometry.faceVertexUvs[0][9] = [ front[1], front[0], front[3] ];
this.movieGeometry.faceVertexUvs[0][10] = [ back[2], back[1], back[3] ];
this.movieGeometry.faceVertexUvs[0][11] = [ back[1], back[0], back[3] ];
this.movieScreen = new THREE.Mesh(this.movieGeometry, this.movieMaterial);
this.movieScreen.position.set(position.x, position.y, position.z);
this.movieScreen.rotation.y = -Math.PI;
this.scene.add(this.movieScreen);
} else if (projection === '180' || projection === '180_LR' || projection === '180_MONO') {
let geometry = new THREE.SphereGeometry(
256,
this.options_.sphereDetail,
this.options_.sphereDetail,
Math.PI,
Math.PI
);
// Left eye view
geometry.scale(-1, 1, 1);
let uvs = geometry.faceVertexUvs[0];
if (projection !== '180_MONO') {
for (let i = 0; i < uvs.length; i++) {
for (let j = 0; j < 3; j++) {
uvs[i][j].x *= 0.5;
}
}
}
this.movieGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
this.movieMaterial = new THREE.MeshBasicMaterial({
map: this.videoTexture,
overdraw: true
});
this.movieScreen = new THREE.Mesh(this.movieGeometry, this.movieMaterial);
// display in left eye only
this.movieScreen.layers.set(1);
this.scene.add(this.movieScreen);
// Right eye view
geometry = new THREE.SphereGeometry(
256,
this.options_.sphereDetail,
this.options_.sphereDetail,
Math.PI,
Math.PI
);
geometry.scale(-1, 1, 1);
uvs = geometry.faceVertexUvs[0];
for (let i = 0; i < uvs.length; i++) {
for (let j = 0; j < 3; j++) {
uvs[i][j].x *= 0.5;
uvs[i][j].x += 0.5;
}
}
this.movieGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
this.movieMaterial = new THREE.MeshBasicMaterial({
map: this.videoTexture,
overdraw: true
});
this.movieScreen = new THREE.Mesh(this.movieGeometry, this.movieMaterial);
// display in right eye only
this.movieScreen.layers.set(2);
this.scene.add(this.movieScreen);
} else if (projection === 'EAC' || projection === 'EAC_LR') {
const makeScreen = (mapMatrix, scaleMatrix) => {
// "Continuity correction?": because of discontinuous faces and aliasing,
// we truncate the 2-pixel-wide strips on all discontinuous edges,
const contCorrect = 2;
this.movieGeometry = new THREE.BoxGeometry(256, 256, 256);
this.movieMaterial = new THREE.ShaderMaterial({
overdraw: true, side: THREE.BackSide,
uniforms: {
mapped: {value: this.videoTexture},
mapMatrix: {value: mapMatrix},
contCorrect: {value: contCorrect},
faceWH: {value: new THREE.Vector2(1 / 3, 1 / 2).applyMatrix3(scaleMatrix)},
vidWH: {value: new THREE.Vector2(this.videoTexture.image.videoWidth, this.videoTexture.image.videoHeight).applyMatrix3(scaleMatrix)}
},
vertexShader: `
varying vec2 vUv;
uniform mat3 mapMatrix;
void main() {
vUv = (mapMatrix * vec3(uv, 1.)).xy;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
}`,
fragmentShader: `
varying vec2 vUv;
uniform sampler2D mapped;
uniform vec2 faceWH;
uniform vec2 vidWH;
uniform float contCorrect;
const float PI = 3.1415926535897932384626433832795;
void main() {
vec2 corner = vUv - mod(vUv, faceWH) + vec2(0, contCorrect / vidWH.y);
vec2 faceWHadj = faceWH - vec2(0, contCorrect * 2. / vidWH.y);
vec2 p = (vUv - corner) / faceWHadj - .5;
vec2 q = 2. / PI * atan(2. * p) + .5;
vec2 eUv = corner + q * faceWHadj;
gl_FragColor = texture2D(mapped, eUv);
}`
});
const right = [new THREE.Vector2(0, 1 / 2), new THREE.Vector2(1 / 3, 1 / 2), new THREE.Vector2(1 / 3, 1), new THREE.Vector2(0, 1)];
const front = [new THREE.Vector2(1 / 3, 1 / 2), new THREE.Vector2(2 / 3, 1 / 2), new THREE.Vector2(2 / 3, 1), new THREE.Vector2(1 / 3, 1)];
const left = [new THREE.Vector2(2 / 3, 1 / 2), new THREE.Vector2(1, 1 / 2), new THREE.Vector2(1, 1), new THREE.Vector2(2 / 3, 1)];
const bottom = [new THREE.Vector2(1 / 3, 0), new THREE.Vector2(1 / 3, 1 / 2), new THREE.Vector2(0, 1 / 2), new THREE.Vector2(0, 0)];
const back = [new THREE.Vector2(1 / 3, 1 / 2), new THREE.Vector2(1 / 3, 0), new THREE.Vector2(2 / 3, 0), new THREE.Vector2(2 / 3, 1 / 2)];
const top = [new THREE.Vector2(1, 0), new THREE.Vector2(1, 1 / 2), new THREE.Vector2(2 / 3, 1 / 2), new THREE.Vector2(2 / 3, 0)];
for (const face of [right, front, left, bottom, back, top]) {
const height = this.videoTexture.image.videoHeight;
let lowY = 1;
let highY = 0;
for (const vector of face) {
if (vector.y < lowY) {
lowY = vector.y;
}
if (vector.y > highY) {
highY = vector.y;
}
}
for (const vector of face) {
if (Math.abs(vector.y - lowY) < Number.EPSILON) {
vector.y += contCorrect / height;
}
if (Math.abs(vector.y - highY) < Number.EPSILON) {
vector.y -= contCorrect / height;
}
vector.x = vector.x / height * (height - contCorrect * 2) + contCorrect / height;
}
}
this.movieGeometry.faceVertexUvs[0] = [];
this.movieGeometry.faceVertexUvs[0][0] = [ right[2], right[1], right[3] ];
this.movieGeometry.faceVertexUvs[0][1] = [ right[1], right[0], right[3] ];
this.movieGeometry.faceVertexUvs[0][2] = [ left[2], left[1], left[3] ];
this.movieGeometry.faceVertexUvs[0][3] = [ left[1], left[0], left[3] ];
this.movieGeometry.faceVertexUvs[0][4] = [ top[2], top[1], top[3] ];
this.movieGeometry.faceVertexUvs[0][5] = [ top[1], top[0], top[3] ];
this.movieGeometry.faceVertexUvs[0][6] = [ bottom[2], bottom[1], bottom[3] ];
this.movieGeometry.faceVertexUvs[0][7] = [ bottom[1], bottom[0], bottom[3] ];
this.movieGeometry.faceVertexUvs[0][8] = [ front[2], front[1], front[3] ];
this.movieGeometry.faceVertexUvs[0][9] = [ front[1], front[0], front[3] ];
this.movieGeometry.faceVertexUvs[0][10] = [ back[2], back[1], back[3] ];
this.movieGeometry.faceVertexUvs[0][11] = [ back[1], back[0], back[3] ];
this.movieScreen = new THREE.Mesh(this.movieGeometry, this.movieMaterial);
this.movieScreen.position.set(position.x, position.y, position.z);
this.movieScreen.rotation.y = -Math.PI;
return this.movieScreen;
};
if (projection === 'EAC') {
this.scene.add(makeScreen(new THREE.Matrix3(), new THREE.Matrix3()));
} else {
const scaleMatrix = new THREE.Matrix3().set(
0, 0.5, 0,
1, 0, 0,
0, 0, 1
);
makeScreen(new THREE.Matrix3().set(
0, -0.5, 0.5,
1, 0, 0,
0, 0, 1
), scaleMatrix);
// display in left eye only
this.movieScreen.layers.set(1);
this.scene.add(this.movieScreen);
makeScreen(new THREE.Matrix3().set(
0, -0.5, 1,
1, 0, 0,
0, 0, 1
), scaleMatrix);
// display in right eye only
this.movieScreen.layers.set(2);
this.scene.add(this.movieScreen);
}
}
this.currentProjection_ = projection;
}
triggerError_(errorObj) {
// if we have videojs-errors use it
if (this.videojsErrorsSupport_) {
this.player_.error(errorObj);
// if we don't have videojs-errors just use a normal player error
} else {
// strip any html content from the error message
// as it is not supported outside of videojs-errors
const div = document.createElement('div');
div.innerHTML = errors[errorObj.code].message;
const message = div.textContent || div.innerText || '';
this.player_.error({
code: errorObj.code,
message
});
}
}
log(...msgs) {
if (!this.options_.debug) {
return;
}
msgs.forEach((msg) => {
videojs.log('VR: ', msg);
});
}
handleVrDisplayActivate_() {
if (!this.vrDisplay) {
return;
}
this.vrDisplay.requestPresent([{source: this.renderedCanvas}]).then(() => {
if (!this.vrDisplay.cardboardUI_ || !videojs.browser.IS_IOS) {
return;
}
// webvr-polyfill/cardboard ui only watches for click events
// to tell that the back arrow button is pressed during cardboard vr.
// but somewhere along the line these events are silenced with preventDefault
// but only on iOS, so we translate them ourselves here
let touches = [];
const iosCardboardTouchStart_ = (e) => {
for (let i = 0; i < e.touches.length; i++) {
touches.push(e.touches[i]);
}
};
const iosCardboardTouchEnd_ = (e) => {
if (!touches.length) {
return;
}
touches.forEach((t) => {
const simulatedClick = new window.MouseEvent('click', {
screenX: t.screenX,
screenY: t.screenY,
clientX: t.clientX,
clientY: t.clientY
});
this.renderedCanvas.dispatchEvent(simulatedClick);
});
touches = [];
};
this.renderedCanvas.addEventListener('touchstart', iosCardboardTouchStart_);
this.renderedCanvas.addEventListener('touchend', iosCardboardTouchEnd_);
this.iosRevertTouchToClick_ = () => {
this.renderedCanvas.removeEventListener('touchstart', iosCardboardTouchStart_);
this.renderedCanvas.removeEventListener('touchend', iosCardboardTouchEnd_);
this.iosRevertTouchToClick_ = null;
};
});
}
handleVrDisplayDeactivate_() {
if (!this.vrDisplay || !this.vrDisplay.isPresenting) {
return;
}
if (this.iosRevertTouchToClick_) {
this.iosRevertTouchToClick_();
}
this.vrDisplay.exitPresent();
}
requestAnimationFrame(fn) {
if (this.vrDisplay) {
return this.vrDisplay.requestAnimationFrame(fn);
}
return this.player_.requestAnimationFrame(fn);
}
cancelAnimationFrame(id) {
if (this.vrDisplay) {
return this.vrDisplay.cancelAnimationFrame(id);
}
return this.player_.cancelAnimationFrame(id);
}
togglePlay_() {
if (this.player_.paused()) {
this.player_.play();
} else {
this.player_.pause();
}
}
animate_() {
if (!this.initialized_) {
return;
}
if (this.getVideoEl_().readyState === this.getVideoEl_().HAVE_ENOUGH_DATA) {
if (this.videoTexture) {
this.videoTexture.needsUpdate = true;
}
}
this.controls3d.update();
if (this.omniController) {
this.omniController.update(this.camera);
}
this.effect.render(this.scene, this.camera);
if (window.navigator.getGamepads) {
// Grab all gamepads
const gamepads = window.navigator.getGamepads();
for (let i = 0; i < gamepads.length; ++i) {
const gamepad = gamepads[i];
// Make sure gamepad is defined
// Only take input if state has changed since we checked last
if (!gamepad || !gamepad.timestamp || gamepad.timestamp === this.prevTimestamps_[i]) {
continue;
}
for (let j = 0; j < gamepad.buttons.length; ++j) {
if (gamepad.buttons[j].pressed) {
this.togglePlay_();
this.prevTimestamps_[i] = gamepad.timestamp;
break;
}
}
}
}
this.camera.getWorldDirection(this.cameraVector);
this.animationFrameId_ = this.requestAnimationFrame(this.animate_);
}
handleResize_() {
const width = this.player_.currentWidth();
const height = this.player_.currentHeight();
this.effect.setSize(width, height, false);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
}
setProjection(projection) {
if (!utils.getInternalProjectionName(projection)) {
videojs.log.error('videojs-vr: please pass a valid projection ' + utils.validProjections.join(', '));
return;
}
this.currentProjection_ = projection;
this.defaultProjection_ = projection;
}
init() {
this.reset();
this.camera = new THREE.PerspectiveCamera(75, this.player_.currentWidth() / this.player_.currentHeight(), 1, 1000);
// Store vector representing the direction in which the camera is looking, in world space.
this.cameraVector = new THREE.Vector3();
if (this.currentProjection_ === '360_LR' || this.currentProjection_ === '360_TB' || this.currentProjection_ === '180' || this.currentProjection_ === '180_LR' || this.currentProjection_ === '180_MONO' || this.currentProjection_ === 'EAC_LR') {
// Render left eye when not in VR mode
this.camera.layers.enable(1);
}
this.scene = new THREE.Scene();
this.videoTexture = new THREE.VideoTexture(this.getVideoEl_());
// shared regardless of wether VideoTexture is used or
// an image canvas is used
this.videoTexture.generateMipmaps = false;
this.videoTexture.minFilter = THREE.LinearFilter;
this.videoTexture.magFilter = THREE.LinearFilter;
this.videoTexture.format = THREE.RGBFormat;
this.changeProjection_(this.currentProjection_);
if (this.currentProjection_ === 'NONE') {
this.log('Projection is NONE, dont init');
this.reset();
return;
}
this.player_.removeChild('BigPlayButton');
this.player_.addChild('BigVrPlayButton', {}, this.bigPlayButtonIndex_);
this.player_.bigPlayButton = this.player_.getChild('BigVrPlayButton');
// mobile devices, or cardboard forced to on
if (this.options_.forceCardboard ||
videojs.browser.IS_ANDROID ||
videojs.browser.IS_IOS) {
this.addCardboardButton_();
}
// if ios remove full screen toggle
if (videojs.browser.IS_IOS && this.player_.controlBar && this.player_.controlBar.fullscreenToggle) {
this.player_.controlBar.fullscreenToggle.hide();
}
this.camera.position.set(0, 0, 0);
this.renderer = new THREE.WebGLRenderer({
devicePixelRatio: window.devicePixelRatio,
alpha: false,
clearColor: 0xffffff,
antialias: true
});
const webglContext = this.renderer.getContext('webgl');
const oldTexImage2D = webglContext.texImage2D;
/* this is a workaround since threejs uses try catch */
webglContext.texImage2D = (...args) => {
try {
return oldTexImage2D.apply(webglContext, args);
} catch (e) {
this.reset();
this.player_.pause();
this.triggerError_({code: 'web-vr-hls-cors-not-supported', dismiss: false});
throw new Error(e);
}
};
this.renderer.setSize(this.player_.currentWidth(), this.player_.currentHeight(), false);
this.effect = new VREffect(this.renderer);
this.effect.setSize(this.player_.currentWidth(), this.player_.currentHeight(), false);
this.vrDisplay = null;
// Previous timestamps for gamepad updates
this.prevTimestamps_ = [];
this.renderedCanvas = this.renderer.domElement;
this.renderedCanvas.setAttribute('style', 'width: 100%; height: 100%; position: absolute; top:0;');
const videoElStyle = this.getVideoEl_().style;
this.player_.el().insertBefore(this.renderedCanvas, this.player_.el().firstChild);
videoElStyle.zIndex = '-1';
videoElStyle.opacity = '0';
if (window.navigator.getVRDisplays) {
this.log('is supported, getting vr displays');
window.navigator.getVRDisplays().then((displays) => {
if (displays.length > 0) {
this.log('Displays found', displays);
this.vrDisplay = displays[0];
// Native WebVR Head Mounted Displays (HMDs) like the HTC Vive
// also need the cardboard button to enter fully immersive mode
// so, we want to add the button if we're not polyfilled.
if (!this.vrDisplay.isPolyfilled) {
this.log('Real HMD found using VRControls', this.vrDisplay);
this.addCardboardButton_();
// We use VRControls here since we are working with an HMD
// and we only want orientation controls.
this.controls3d = new VRControls(this.camera);
}
}
if (!this.controls3d) {
this.log('no HMD found Using Orbit & Orientation Controls');
const options = {
camera: this.camera,
canvas: this.renderedCanvas,
// check if its a half sphere view projection
halfView: this.currentProjection_.indexOf('180') === 0,
orientation: videojs.browser.IS_IOS || videojs.browser.IS_ANDROID || false
};
if (this.options_.motionControls === false) {
options.orientation = false;
}
this.controls3d = new OrbitOrientationContols(options);
this.canvasPlayerControls = new CanvasPlayerControls(this.player_, this.renderedCanvas, this.options_);
}
this.animationFrameId_ = this.requestAnimationFrame(this.animate_);
});
} else if (window.navigator.getVRDevices) {
this.triggerError_({code: 'web-vr-out-of-date', dismiss: false});
} else {
this.triggerError_({code: 'web-vr-not-supported', dismiss: false});
}
if (this.options_.omnitone) {
const audiocontext = THREE.AudioContext.getContext();
this.omniController = new OmnitoneController(
audiocontext,
this.options_.omnitone, this.getVideoEl_(), this.options_.omnitoneOptions
);
this.omniController.one('audiocontext-suspended', () => {
this.player.pause();
this.player.one('playing', () => {
audiocontext.resume();
});
});
}
this.on(this.player_, 'fullscreenchange', this.handleResize_);
window.addEventListener('vrdisplaypresentchange', this.handleResize_, true);
window.addEventListener('resize', this.handleResize_, true);
window.addEventListener('vrdisplayactivate', this.handleVrDisplayActivate_, true);
window.addEventListener('vrdisplaydeactivate', this.handleVrDisplayDeactivate_, true);
this.initialized_ = true;
this.trigger('initialized');
}
addCardboardButton_() {
if (!this.player_.controlBar.getChild('CardboardButton')) {
this.player_.controlBar.addChild('CardboardButton', {});
}
}
getVideoEl_() {
return this.player_.el().getElementsByTagName('video')[0];
}
reset() {
if (!this.initialized_) {
return;
}
if (this.omniController) {
this.omniController.off('audiocontext-suspended');
this.omniController.dispose();
this.omniController = undefined;
}
if (this.controls3d) {
this.controls3d.dispose();
this.controls3d = null;
}
if (this.canvasPlayerControls) {
this.canvasPlayerControls.dispose();
this.canvasPlayerControls = null;
}
if (this.effect) {
this.effect.dispose();
this.effect = null;
}
window.removeEventListener('resize', this.handleResize_, true);
window.removeEventListener('vrdisplaypresentchange', this.handleResize_, true);
window.removeEventListener('vrdisplayactivate', this.handleVrDisplayActivate_, true);
window.removeEventListener('vrdisplaydeactivate', this.handleVrDisplayDeactivate_, true);
// re-add the big play button to player
if (!this.player_.getChild('BigPlayButton')) {
this.player_.addChild('BigPlayButton', {}, this.bigPlayButtonIndex_);
}
if (this.player_.getChild('BigVrPlayButton')) {
this.player_.removeChild('BigVrPlayButton');
}
// remove the cardboard button
if (this.player_.getChild('CardboardButton')) {
this.player_.controlBar.removeChild('CardboardButton');
}
// show the fullscreen again
if (videojs.browser.IS_IOS && this.player_.controlBar && this.player_.controlBar.fullscreenToggle) {
this.player_.controlBar.fullscreenToggle.show();
}
// reset the video element style so that it will be displayed
const videoElStyle = this.getVideoEl_().style;
videoElStyle.zIndex = '';
videoElStyle.opacity = '';
// set the current projection to the default
this.currentProjection_ = this.defaultProjection_;
// reset the ios touch to click workaround
if (this.iosRevertTouchToClick_) {
this.iosRevertTouchToClick_();
}
// remove the old canvas
if (this.renderedCanvas) {
this.renderedCanvas.parentNode.removeChild(this.renderedCanvas);
}
if (this.animationFrameId_) {
this.cancelAnimationFrame(this.animationFrameId_);
}
this.initialized_ = false;
}
dispose() {
super.dispose();
this.reset();
}
polyfillVersion() {
return WebVRPolyfill.version;
}
}
VR.prototype.setTimeout = Component.prototype.setTimeout;
VR.prototype.clearTimeout = Component.prototype.clearTimeout;
VR.VERSION = VERSION;
videojs.registerPlugin('vr', VR);
export default VR;