diff --git a/src/plugin.js b/src/plugin.js
index bdd26dff..d8ae6640 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -1,31 +1,18 @@
import {version as VERSION} from '../package.json';
import window from 'global/window';
+import document from 'global/document';
import WebVRPolyfill from 'webvr-polyfill';
import videojs from 'video.js';
import * as THREE from 'three';
import VRControls from 'three/examples/js/controls/VRControls.js';
import VREffect from 'three/examples/js/effects/VREffect.js';
import OrbitControls from 'three/examples/js/controls/OrbitControls.js';
-import rgbFragmentShader from './rgb-fragment-shader';
-import rgbaFragmentShader from './rgba-fragment-shader';
-import vertexShader from './vertex-shader';
+import * as utils from './utils';
// import controls so they get regisetered with videojs
import './cardboard-button';
import './big-vr-play-button';
-const validProjections = [
- '360',
- '360_LR',
- '360_TB',
- '360_CUBE',
- 'NONE',
- 'AUTO',
- 'Sphere',
- 'Cube',
- 'equirectangular'
-];
-
// Default options for the plugin.
const defaults = {
projection: 'AUTO',
@@ -43,54 +30,14 @@ const errors = {
headline: '360 not supported on this device',
type: '360_NOT_SUPPORTED',
message: "Your browser does not support 360. See webvr.info 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 webvr.info for assistance."
}
};
-const getInternalProjectionName = function(projection) {
- if (!projection) {
- return;
- }
-
- projection = projection.toString().trim();
-
- if ((/sphere/i).test(projection)) {
- return '360';
- }
-
- if ((/cube/i).test(projection)) {
- return '360_CUBE';
- }
-
- if ((/equirectangular/i).test(projection)) {
- return '360';
- }
-
- for (let i = 0; i < validProjections.length; i++) {
- if (new RegExp('^' + validProjections[i] + '$', 'i').test(projection)) {
- return validProjections[i];
- }
- }
-
-};
-
-const isHLS = function(currentType) {
- // hls video types
- const hlsTypes = [
- // Apple santioned
- 'application/vnd.apple.mpegurl',
- // Very common
- 'application/x-mpegurl',
- // Included for completeness
- 'video/x-mpegurl',
- 'video/mpegurl',
- 'application/mpegurl'
- ];
-
- // if the current type has a case insensitivie match from the list above
- // this is hls
- return hlsTypes.some((type) => (new RegExp(`^${type}$`, 'i')).test(currentType));
-};
-
const Plugin = videojs.getPlugin('plugin');
const Component = videojs.getComponent('Component');
@@ -115,8 +62,9 @@ class VR extends Plugin {
player.errors({errors});
}
- // we need this as IE 11 reports that it has a VR display, but isnt compatible with Video as a Texture. for example
- if (videojs.browser.IE_VERSION) {
+ // 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) {
this.triggerError_({code: 'web-vr-not-supported', dismiss: false});
return;
}
@@ -132,7 +80,7 @@ class VR extends Plugin {
}
changeProjection_(projection) {
- projection = getInternalProjectionName(projection);
+ projection = utils.getInternalProjectionName(projection);
// don't change to an invalid projection
if (!projection) {
projection = 'NONE';
@@ -147,7 +95,7 @@ class VR extends Plugin {
// mediainfo cannot be set to auto or we would infinite loop here
// each source should know wether they are 360 or not, if using AUTO
if (this.player_.mediainfo && this.player_.mediainfo.projection && this.player_.mediainfo.projection !== 'AUTO') {
- const autoProjection = getInternalProjectionName(this.player_.mediainfo.projection);
+ const autoProjection = utils.getInternalProjectionName(this.player_.mediainfo.projection);
return this.changeProjection_(autoProjection);
}
@@ -285,7 +233,15 @@ class VR extends Plugin {
return this.vrDisplay.requestAnimationFrame(fn);
}
- return super.requestAnimationFrame(fn);
+ return Component.prototype.requestAnimationFrame.call(this, fn);
+ }
+
+ cancelAnimationFrame(id) {
+ if (this.vrDisplay) {
+ return this.vrDisplay.cancelAnimationFrame(id);
+ }
+
+ return Component.prototype.cancelAnimationFrame.call(this, id);
}
togglePlay_() {
@@ -297,17 +253,26 @@ class VR extends Plugin {
}
animate_() {
+ if (!this.initialized_) {
+ return;
+ }
if (this.getVideoEl_().readyState === this.getVideoEl_().HAVE_ENOUGH_DATA) {
if (this.videoTexture) {
this.videoTexture.needsUpdate = true;
}
}
+ // This draws the current video data as an image to a canvas every render. That canvas is used
+ // as a texture by webgl. Normally the video is used directly and we don't have to do this, but
+ // HLS video textures on iOS >= 11 is currently broken, so we have to support those browser
+ // in a roundabout way.
+ if (this.videoImageContext_) {
+ this.videoImageContext_.drawImage(this.getVideoEl_(), 0, 0, this.videoImage_.width, this.videoImage_.height);
+ }
+
this.controls3d.update();
this.effect.render(this.scene, this.camera);
- this.animationFrameId_ = this.requestAnimationFrame(this.animate_);
-
if (window.navigator.getGamepads) {
// Grab all gamepads
const gamepads = window.navigator.getGamepads();
@@ -330,12 +295,20 @@ class VR extends Plugin {
}
}
this.camera.getWorldDirection(this.cameraVector);
+
+ this.animationFrameId_ = this.requestAnimationFrame(this.animate_);
}
handleResize_() {
const width = this.player_.currentWidth();
const height = this.player_.currentHeight();
+ // when dealing with a non video
+ if (this.videoImage_) {
+ this.videoImage_.width = width;
+ this.videoImage_.height = height;
+ }
+
this.effect.setSize(width, height, false);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
@@ -343,8 +316,8 @@ class VR extends Plugin {
setProjection(projection) {
- if (!getInternalProjectionName(projection)) {
- videojs.log.error('videojs-vr: please pass a valid projection: ' + validProjections.join(', '));
+ if (!utils.getInternalProjectionName(projection)) {
+ videojs.log.error('videojs-vr: please pass a valid projection ' + utils.validProjections.join(', '));
return;
}
@@ -366,46 +339,41 @@ class VR extends Plugin {
this.scene = new THREE.Scene();
- this.videoTexture = new THREE.VideoTexture(this.getVideoEl_());
-
- this.videoTexture.generateMipmaps = false;
- this.videoTexture.minFilter = THREE.LinearFilter;
- this.videoTexture.magFilter = THREE.LinearFilter;
-
- // iOS and macOS HLS fix/hacks
+ // We opted to stop using a video texture on safari due to
+ // various bugs that exist when using it. This gives us worse performance
+ // but it will actually work on all recent version of safari. See
+ // the following issues for more info on this:
+ //
// https://bugs.webkit.org/show_bug.cgi?id=163866#c3
- // https://github.com/mrdoob/three.js/issues/9754
- // On iOS with HLS, color space is wrong and texture is flipped on Y axis
- // On macOS, just need to flip texture Y axis
-
- if (isHLS() && videojs.browser.IS_ANY_SAFARI) {
- this.log('Safari + iOS + HLS = flipY and colorspace hack');
- this.videoTexture.format = THREE.RGBAFormat;
- this.videoTexture.flipY = false;
- } else if (isHLS() && videojs.browser.IS_SAFARI) {
- this.log('Safari + HLS = flipY hack');
- this.videoTexture.format = THREE.RGBFormat;
- this.videoTexture.flipY = false;
- } else {
- this.videoTexture.format = THREE.RGBFormat;
- }
+ // https://bugs.webkit.org/show_bug.cgi?id=179417
+ if (videojs.browser.IS_ANY_SAFARI && utils.isHLS(this.player_.currentSource().type)) {
+ this.log('Video texture is not supported using image canvas hack');
+ this.videoImage_ = document.createElement('canvas');
+ this.videoImage_.width = this.player_.currentWidth();
+ this.videoImage_.height = this.player_.currentHeight();
- if ((this.videoTexture.format === THREE.RGBAFormat || this.videoTexture.format === THREE.RGBFormat) && this.videoTexture.flipY === false) {
- let fragmentShader = rgbFragmentShader;
+ this.videoImageContext_ = this.videoImage_.getContext('2d');
+ this.videoImageContext_.fillStyle = '#000000';
- if (this.videoTexture.format === THREE.RGBAFormat) {
- fragmentShader = rgbaFragmentShader;
- }
+ this.videoTexture = new THREE.Texture(this.videoImage_);
- this.movieMaterial = new THREE.ShaderMaterial({
- uniforms: {texture: {value: this.videoTexture}},
- vertexShader,
- fragmentShader
- });
+ this.videoTexture.wrapS = THREE.ClampToEdgeWrapping;
+ this.videoTexture.wrapT = THREE.ClampToEdgeWrapping;
+ this.videoTexture.flipY = true;
} else {
- this.movieMaterial = new THREE.MeshBasicMaterial({ map: this.videoTexture, overdraw: true, side: THREE.DoubleSide });
+ this.log('Video texture is supported using that');
+ 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.movieMaterial = new THREE.MeshBasicMaterial({ map: this.videoTexture, overdraw: true, side: THREE.DoubleSide });
+
this.changeProjection_(this.currentProjection_);
if (this.currentProjection_ === 'NONE') {
@@ -438,6 +406,20 @@ class VR extends Plugin {
antialias: true
});
+ const webglContext = this.renderer.getContext('webgl');
+ const oldTexImage2D = webglContext.texImage2D;
+
+ 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);
@@ -501,7 +483,7 @@ class VR extends Plugin {
this.controls3d = new OrbitControls(this.camera, this.renderedCanvas);
this.controls3d.target.set(0, 0, -1);
}
- this.requestAnimationFrame(this.animate_);
+ this.animationFrameId_ = this.requestAnimationFrame(this.animate_);
});
} else if (window.navigator.getVRDevices) {
this.triggerError_({code: 'web-vr-out-of-date', dismiss: false});
@@ -583,6 +565,14 @@ class VR extends Plugin {
this.cancelAnimationFrame(this.animationFrameId_);
}
+ if (this.videoImage_) {
+ this.videoImage_ = null;
+ }
+
+ if (this.videoImageContext_) {
+ this.videoImageContext_ = null;
+ }
+
this.initialized_ = false;
}
@@ -592,8 +582,6 @@ class VR extends Plugin {
}
}
-VR.prototype.requestAnimationFrame = Component.prototype.requestAnimationFrame;
-VR.prototype.cancelAnimationFrame = Component.prototype.cancelAnimationFrame;
VR.prototype.setTimeout = Component.prototype.setTimeout;
VR.prototype.clearTimeout = Component.prototype.clearTimeout;
diff --git a/src/rgb-fragment-shader.js b/src/rgb-fragment-shader.js
deleted file mode 100644
index 708384a1..00000000
--- a/src/rgb-fragment-shader.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = `uniform sampler2D texture;
-varying vec2 vUV;
-void main() {
- gl_FragColor = texture2D(texture, vUV);
-}`;
diff --git a/src/rgba-fragment-shader.js b/src/rgba-fragment-shader.js
deleted file mode 100644
index 05de9b0d..00000000
--- a/src/rgba-fragment-shader.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = `uniform sampler2D texture;
-varying vec2 vUV;
-void main() {
- gl_FragColor = texture2D(texture, vUV).bgra;
-}`;
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 00000000..1e833909
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,67 @@
+import document from 'global/document';
+
+// check if the browser supports cors
+export const corsSupport = (function() {
+ const video = document.createElement('video');
+
+ video.crossOrigin = 'anonymous';
+
+ return video.hasAttribute('crossorigin');
+})();
+
+export const isHLS = function(currentType) {
+ // hls video types
+ const hlsTypes = [
+ // Apple santioned
+ 'application/vnd.apple.mpegurl',
+ // Very common
+ 'application/x-mpegurl',
+ // Included for completeness
+ 'video/x-mpegurl',
+ 'video/mpegurl',
+ 'application/mpegurl'
+ ];
+
+ // if the current type has a case insensitivie match from the list above
+ // this is hls
+ return hlsTypes.some((type) => (RegExp(`^${type}$`, 'i')).test(currentType));
+};
+
+export const validProjections = [
+ '360',
+ '360_LR',
+ '360_TB',
+ '360_CUBE',
+ 'NONE',
+ 'AUTO',
+ 'Sphere',
+ 'Cube',
+ 'equirectangular'
+];
+
+export const getInternalProjectionName = function(projection) {
+ if (!projection) {
+ return;
+ }
+
+ projection = projection.toString().trim();
+
+ if ((/sphere/i).test(projection)) {
+ return '360';
+ }
+
+ if ((/cube/i).test(projection)) {
+ return '360_CUBE';
+ }
+
+ if ((/equirectangular/i).test(projection)) {
+ return '360';
+ }
+
+ for (let i = 0; i < validProjections.length; i++) {
+ if (new RegExp('^' + validProjections[i] + '$', 'i').test(projection)) {
+ return validProjections[i];
+ }
+ }
+
+};
diff --git a/src/vertex-shader.js b/src/vertex-shader.js
deleted file mode 100644
index 9aa41971..00000000
--- a/src/vertex-shader.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = `varying vec2 vUV;
-void main() {
- vUV = vec2( uv.x, 1.0 - uv.y );
- gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
-}`;