Source: plugin.js

import window from 'global/window';
/* THIS CONFIGURES webvr-polyfill don't change the order */
import './webvr-config.js';
import 'webvr-polyfill/src/main';
import videojs from 'video.js';
import {version as VERSION} from '../package.json';
import * as THREE from 'three';
// previously we used
// * three/examples/js/controls/VRControls.js
// * three/examples/js/effects/VREffects.js
// but since we are using es6 now, there is no good way to make them export to us
// so the code has been copied locally to allow exporting
import VRControls from './VRControls.js';
import VREffect from './VREffect.js';
import WebVRManager from 'webvr-boilerplate';

// import controls so they get regisetered with videojs
import './cardboard-button';
import './big-vr-play-button';

window.WebVRManager = WebVRManager;

const navigator = window.navigator;
const validProjections = [
  '360',
  '360_LR',
  '360_TB',
  '360_CUBE',
  'NONE',
  'AUTO',
  'Sphere',
  'Cube',
  'equirectangular'
];
// Default options for the plugin.
const defaults = {
  projection: 'AUTO',
  debug: false
};
const errors = {
  'web-vr-no-devices-found': {
    headline: 'No 360 devices found',
    type: '360_NO_DEVICES_FOUND',
    message: 'Your browser supports 360, but no 360 displays found.'
  },
  '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'>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'>webvr.info</a> 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_CUBE';
  }

  for (let i = 0; i < validProjections.length; i++) {
    if (new RegExp('^' + validProjections[i] + '$', 'i').test(projection)) {
      return validProjections[i];
    }
  }

};
/**
 * Initializes the plugin
 */
const initPlugin = function(player, options) {
  const videoEl = player.el().getElementsByTagName('video')[0];

  if (videoEl === undefined || videoEl === null) {
    // Player is not using HTML5 tech, so don't init it.
    return;
  }

  // don't initialize twice
  if (player.vr && player.vr.currentProjection) {
    videojs.log.warn('videojs-vr is already intialized, not going to initialize again');
    return;
  }

  const settings = videojs.mergeOptions(defaults, options || {});
  const container = player.el();
  const bigPlayButtonIndex = player
    .children()
    .indexOf(player.getChild('BigPlayButton'));

  if (!getInternalProjectionName(settings.projection)) {
    videojs.log.error('videojs-vr: Please use a valid projection option: ' + validProjections.join(', '));
    return;
  }

  player.vr.defaultProjection = settings.projection;
  player.vr.currentProjection = settings.projection;

  const log = function(msg) {
    if (settings.debug) {
      videojs.log(msg);
    }
  };

  // custom videojs-errors integration boolean
  const videojsErrorsSupport = !!videojs.errors;

  if (videojsErrorsSupport) {
    player.errors({errors});
  }

  const triggerError = function(errorObj) {
    // if we have videojs-errors use it
    if (videojsErrorsSupport) {
      player.error(errorObj);
    // if we don't have videojs-errors just use a normal player error
    } else {
      player.error({code: errorObj.code, message: errors[errorObj.code].message});
    }
  };

  function isHLS() {
    const currentType = player.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));
  }

  function changeProjection(projection) {
    projection = getInternalProjectionName(projection);
    // don't change to an invalid projection
    if (validProjections.indexOf(projection) === -1) {
      projection = 'NONE';
    }

    const position = {x: 0, y: 0, z: 0 };

    if (player.vr.scene) {
      player.vr.scene.remove(player.vr.movieScreen);
    }
    if (projection === 'AUTO') {
      // 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 (player.mediainfo && player.mediainfo.projection && player.mediainfo.projection !== 'AUTO') {
        const autoProjection = getInternalProjectionName(player.mediainfo.projection);

        return changeProjection(autoProjection);
      }
      return changeProjection('NONE');
    } else if (projection === '360') {
      player.vr.movieGeometry = new THREE.SphereBufferGeometry(256, 32, 32);

      player.vr.movieScreen = new THREE.Mesh(player.vr.movieGeometry, player.vr.movieMaterial);
      player.vr.movieScreen.position.set(position.x, position.y, position.z);

      player.vr.movieScreen.scale.x = -1;
      player.vr.movieScreen.quaternion.setFromAxisAngle({x: 0, y: 1, z: 0}, -Math.PI / 2);
      player.vr.scene.add(player.vr.movieScreen);
    } else if (projection === '360_LR' || projection === '360_TB') {
      let geometry = new THREE.SphereGeometry(256, 32, 32);

      // Left eye view
      geometry.scale(-1, 1, 1);
      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;
          }
        }
      }

      player.vr.movieGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
      player.vr.movieScreen = new THREE.Mesh(player.vr.movieGeometry, player.vr.movieMaterial);
      player.vr.movieScreen.rotation.y = -Math.PI / 2;
      // display in left eye only
      player.vr.movieScreen.layers.set(1);
      player.vr.scene.add(player.vr.movieScreen);

      // Right eye view
      geometry = new THREE.SphereGeometry(256, 32, 32);
      geometry.scale(-1, 1, 1);
      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;
            uvs[ i ][ j ].y += 0.5;
          }
        }
      }

      player.vr.movieGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
      player.vr.movieScreen = new THREE.Mesh(player.vr.movieGeometry, player.vr.movieMaterial);
      player.vr.movieScreen.rotation.y = -Math.PI / 2;
      // display in right eye only
      player.vr.movieScreen.layers.set(2);
      player.vr.scene.add(player.vr.movieScreen);

    } else if (projection === '360_CUBE') {
      // Currently doesn't work - need to figure out order of cube faces
      player.vr.movieGeometry = new THREE.CubeGeometry(256, 256, 256);
      const face1 = [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 face2 = [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 face3 = [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 face4 = [new THREE.Vector2(0, 0), new THREE.Vector2(0.333, 1), new THREE.Vector2(0.333, 0.5), new THREE.Vector2(0, 0.5)];
      const face5 = [new THREE.Vector2(0.333, 1), new THREE.Vector2(0.666, 1), new THREE.Vector2(0.666, 0.5), new THREE.Vector2(0.333, 0.5)];
      const face6 = [new THREE.Vector2(0.666, 1), new THREE.Vector2(1, 0), new THREE.Vector2(1, 0.5), new THREE.Vector2(0.666, 0.5)];

      player.vr.movieGeometry.faceVertexUvs[0] = [];

      player.vr.movieGeometry.faceVertexUvs[0][0] = [ face1[0], face1[1], face1[3] ];
      player.vr.movieGeometry.faceVertexUvs[0][1] = [ face1[1], face1[2], face1[3] ];

      player.vr.movieGeometry.faceVertexUvs[0][2] = [ face2[0], face2[1], face2[3] ];
      player.vr.movieGeometry.faceVertexUvs[0][3] = [ face2[1], face2[2], face2[3] ];

      player.vr.movieGeometry.faceVertexUvs[0][4] = [ face3[0], face3[1], face3[3] ];
      player.vr.movieGeometry.faceVertexUvs[0][5] = [ face3[1], face3[2], face3[3] ];

      player.vr.movieGeometry.faceVertexUvs[0][6] = [ face4[0], face4[1], face4[3] ];
      player.vr.movieGeometry.faceVertexUvs[0][7] = [ face4[1], face4[2], face4[3] ];

      player.vr.movieGeometry.faceVertexUvs[0][8] = [ face5[0], face5[1], face5[3] ];
      player.vr.movieGeometry.faceVertexUvs[0][9] = [ face5[1], face5[2], face5[3] ];

      player.vr.movieGeometry.faceVertexUvs[0][10] = [ face6[0], face6[1], face6[3] ];
      player.vr.movieGeometry.faceVertexUvs[0][11] = [ face6[1], face6[2], face6[3] ];

      player.vr.movieScreen = new THREE.Mesh(player.vr.movieGeometry, player.vr.movieMaterial);
      player.vr.movieScreen.position.set(position.x, position.y, position.z);

      player.vr.scene.add(player.vr.movieScreen);
    }

    player.vr.currentProjection = projection;
  }

  /* reset player.vr to a default un-initialized state */
  player.vr.reset = function() {

    // re-add the big play button to player
    if (!player.getChild('BigPlayButton')) {
      player.addChild('BigPlayButton', {}, bigPlayButtonIndex);
    }

    if (player.getChild('BigVrPlayButton')) {
      player.removeChild('BigVrPlayButton');
    }

    // remove the cardboard button
    if (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
      player.controlBar.removeChild('CardboardButton');
    }

    // show the fullscreen again
    if (videojs.browser.IS_IOS) {
      player.controlBar.fullscreenToggle.show();
    }

    // reset the video element style so that it will be displayed
    videoEl.style.display = '';

    // set the current projection to the default
    player.vr.currentProjection = player.vr.defaultProjection;
    // remove the old canvas
    if (player.vr.renderedCanvas && container.contains(player.vr.renderedCanvas)) {
      container.removeChild(player.vr.renderedCanvas);
    }
  };

  player.vr.initScene = function() {
    player.vr.reset();

    // 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) {
      triggerError({code: 'web-vr-not-supported', dismiss: false});
      return;
    }

    player.vr.camera = new THREE.PerspectiveCamera(75, player.currentWidth() / player.currentHeight(), 1, 1000);
    // Store vector representing the direction in which the camera is looking, in world space.
    player.vr.cameraVector = new THREE.Vector3();

    if (player.vr.currentProjection === '360_LR' || player.vr.currentProjection === '360_TB') {
      // Render left eye when not in VR mode
      player.vr.camera.layers.enable(1);
    }

    player.vr.scene = new THREE.Scene();
    player.vr.controls3d = new VRControls(player.vr.camera);

    player.vr.videoTexture = new THREE.VideoTexture(videoEl);

    player.vr.videoTexture.generateMipmaps = false;
    player.vr.videoTexture.minFilter = THREE.LinearFilter;
    player.vr.videoTexture.magFilter = THREE.LinearFilter;

    // iOS and macOS HLS fix/hacks
    // 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) {
      log('Safari + iOS + HLS = flipY and colorspace hack');
      player.vr.videoTexture.format = THREE.RGBAFormat;
      player.vr.videoTexture.flipY = false;
    } else if (isHLS() && videojs.browser.IS_SAFARI) {
      log('Safari + HLS = flipY hack');
      player.vr.videoTexture.format = THREE.RGBFormat;
      player.vr.videoTexture.flipY = false;
    } else {
      player.vr.videoTexture.format = THREE.RGBFormat;
    }

    if (player.vr.videoTexture.format === THREE.RGBAFormat && player.vr.videoTexture.flipY === false) {
      player.vr.movieMaterial = new THREE.ShaderMaterial({
        uniforms: {
          texture: { value: player.vr.videoTexture }
        },
        vertexShader: [
          'varying vec2 vUV;',
          'void main() {',
          ' vUV = vec2( uv.x, 1.0 - uv.y );',
          ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
          '}'
        ].join('\n'),
        fragmentShader: [
          'uniform sampler2D texture;',
          'varying vec2 vUV;',
          'void main() {',
          ' gl_FragColor = texture2D( texture, vUV  ).bgra;',
          '}'
        ].join('\n')
      });
    } else if (player.vr.videoTexture.format === THREE.RGBFormat && player.vr.videoTexture.flipY === false) {
      player.vr.movieMaterial = new THREE.ShaderMaterial({
        uniforms: {
          texture: { value: player.vr.videoTexture }
        },
        vertexShader: [
          'varying vec2 vUV;',
          'void main() {',
          ' vUV = vec2( uv.x, 1.0 - uv.y );',
          ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
          '}'
        ].join('\n'),
        fragmentShader: [
          'uniform sampler2D texture;',
          'varying vec2 vUV;',
          'void main() {',
          ' gl_FragColor = texture2D( texture, vUV  );',
          '}'
        ].join('\n')
      });
    } else {
      player.vr.movieMaterial = new THREE.MeshBasicMaterial({ map: player.vr.videoTexture, overdraw: true, side: THREE.DoubleSide });
    }

    changeProjection(player.vr.currentProjection);

    if (player.vr.currentProjection === 'NONE') {
      log('Projection is NONE, dont init');
      player.vr.reset();
      return;
    }

    player.removeChild('BigPlayButton');
    player.addChild('BigVrPlayButton', {}, bigPlayButtonIndex);
    player.bigPlayButton = player.getChild('BigVrPlayButton');
    // mobile devices
    if (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
      player.controlBar.addChild('CardboardButton', {});
    }

    // if ios remove full screen toggle
    if (videojs.browser.IS_IOS) {
      player.controlBar.fullscreenToggle.hide();
    }

    player.vr.camera.position.set(0, 0, 0);

    player.vr.renderer = new THREE.WebGLRenderer({
      devicePixelRatio: window.devicePixelRatio,
      alpha: false,
      clearColor: 0xffffff,
      antialias: true
    });

    player.vr.renderer.setSize(player.currentWidth(), player.currentHeight());
    player.vr.effect = new VREffect(player.vr.renderer);

    player.vr.effect.setSize(player.currentWidth(), player.currentHeight());
    player.vr.vrDisplay = null;

    // Previous timestamps for gamepad updates
    player.vr.prevTimestamps_ = [];

    player.vr.manager = new WebVRManager(player.vr.renderer, player.vr.effect, {hideButton: true});

    player.vr.renderedCanvas = player.vr.renderer.domElement;

    player.vr.renderedCanvas.style.width = 'inherit';
    player.vr.renderedCanvas.style.height = 'inherit';

    container.insertBefore(player.vr.renderedCanvas, container.firstChild);
    videoEl.style.display = 'none';

    // Handle window resizes
    function onWindowResize(event) {
      const width = player.currentWidth();
      const height = player.currentHeight();

      player.vr.effect.setSize(width, height);
      player.vr.camera.aspect = width / height;
      player.vr.camera.updateProjectionMatrix();
    }

    player.on('fullscreenchange', onWindowResize);
    window.addEventListener('vrdisplaypresentchange', onWindowResize, true);
    window.addEventListener('resize', onWindowResize, true);

    function onVRRequestPresent() {
      player.vr.manager.enterVRMode_();
      player.vr.manager.setMode_(3);
    }

    function onVRExitPresent() {
      if (!player.vr.vrDisplay.isPresenting) {
        return;
      }
      player.vr.vrDisplay.exitPresent();
    }

    window.addEventListener('vrdisplayactivate', onVRRequestPresent, true);
    window.addEventListener('vrdisplaydeactivate', onVRExitPresent, true);

    if (navigator.getVRDisplays) {
      navigator.getVRDisplays().then(function(displays) {
        if (displays.length > 0) {
          log('WebVR supported, VRDisplays found.');
          player.vr.vrDisplay = displays[0];
          log(player.vr.vrDisplay);
        } else {
          triggerError({code: 'web-vr-no-devices-found', dismiss: false});
        }
      });
    } else if (navigator.getVRDevices) {
      triggerError({code: 'web-vr-out-of-date', dismiss: false});
    } else {
      triggerError({code: 'web-vr-not-supported', dismiss: false});
    }

    // Handle window rotate
    function onWindowRotate() {
      const screen = window.screen;

      if (window.orientation === -90 || window.orientation === 90) {
        // in iOS, width and height never changes regardless orientation
        // so when in a horizontal mode, height still greater than width
        if (screen.height > screen.width) {
          player.vr.camera.aspect = screen.height / screen.width;
        } else {
          // in Android, width and height will swap value depending on orientation
          player.vr.camera.aspect = screen.width / screen.height;
        }
      } else {
        player.vr.camera.aspect = screen.width / screen.height;
      }
      player.vr.camera.updateProjectionMatrix();
    }
    window.addEventListener('orientationchange', onWindowRotate, false);

    function togglePlay() {
      // Doesn't currently cater for case where paused due to buffering
      // and/or lack of data
      if (player.paused()) {
        player.play();
      } else {
        player.pause();
      }
    }

    (function animate() {
      if (videoEl.readyState === videoEl.HAVE_ENOUGH_DATA) {
        if (player.vr.videoTexture) {
          player.vr.videoTexture.needsUpdate = true;
        }
      }

      player.vr.controls3d.update();
      player.vr.manager.render(player.vr.scene, player.vr.camera);

      if (player.vr.vrDisplay) {
        player.vr.vrDisplay.requestAnimationFrame(animate);

        // Grab all gamepads
        if (navigator.getGamepads) {
          const gamepads = navigator.getGamepads();

          for (let i = 0; i < gamepads.length; ++i) {
            const gamepad = gamepads[i];

            // Make sure gamepad is defined
            if (gamepad) {
              // Only take input if state has changed since we checked last
              if (gamepad.timestamp && !(gamepad.timestamp === player.vr.prevTimestamps_[i])) {
                for (let j = 0; j < gamepad.buttons.length; ++j) {
                  if (gamepad.buttons[j].pressed) {
                    togglePlay();
                    player.vr.prevTimestamps_[i] = gamepad.timestamp;
                    break;
                  }
                }
              }
            }
          }
        }
      } else {
        window.requestAnimationFrame(animate);
      }

      player.vr.camera.getWorldDirection(player.vr.cameraVector);
    }());
  };

  player.on('loadedmetadata', function() {
    player.vr.initScene();
  });

  return player.vr;
};

const vr = function(options) {
  this.ready(() => initPlugin(this, options));
};

// register the plugin with video.js
videojs.registerPlugin('vr', vr);

// Include the version number
vr.VERSION = VERSION;

export default vr;