Skip to content

Commit

Permalink
fix: safari hls
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonocasey committed Jan 22, 2018
1 parent 4c0c7bb commit b73dedd
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 114 deletions.
158 changes: 59 additions & 99 deletions src/plugin.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -46,51 +33,6 @@ const errors = {
}
};

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');

Expand All @@ -115,8 +57,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;
}
Expand All @@ -132,7 +75,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';
Expand All @@ -147,7 +90,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);
}
Expand Down Expand Up @@ -303,6 +246,14 @@ class VR extends Plugin {
}
}

// 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);

Expand Down Expand Up @@ -336,15 +287,21 @@ class VR extends Plugin {
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();
}

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;
}

Expand All @@ -366,46 +323,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') {
Expand Down Expand Up @@ -583,6 +535,14 @@ class VR extends Plugin {
this.cancelAnimationFrame(this.animationFrameId_);
}

if (this.videoImage_) {
this.videoImage_ = null;
}

if (this.videoImageContext_) {
this.videoImageContext_ = null;
}

this.initialized_ = false;
}

Expand Down
5 changes: 0 additions & 5 deletions src/rgb-fragment-shader.js

This file was deleted.

5 changes: 0 additions & 5 deletions src/rgba-fragment-shader.js

This file was deleted.

99 changes: 99 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import window from 'global/window';
import document from 'global/document';
import videojs from 'video.js';

const USER_AGENT = window && window.navigator && window.navigator.userAgent || '';

// get the major/minor/patch for safari desktop an ios
// should be used in conjunction with videojs.browser
export const safariVersion = (function() {
const version = {major: 0, minor: 0, patch: 0};

if (!videojs.browser.IS_ANY_SAFARI) {
return version;
}

const matches = (/Version\/(\d+)(\.\d+)?(\.\d+)? /i).exec(USER_AGENT);

if (!matches || matches.length === 0) {
return version;
}

version.major = parseInt(matches[1], 10);

if (matches[2]) {
version.minor = parseInt(matches[2].replace('.', ''), 10);
}

if (matches[3]) {
version.patch = parseInt(matches[3].replace('.', ''), 10);
}

return version;
})();

// 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];
}
}

};
5 changes: 0 additions & 5 deletions src/vertex-shader.js

This file was deleted.

0 comments on commit b73dedd

Please sign in to comment.