Commit 4a9611cd authored by nextime's avatar nextime

Initial commit

parents
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"rules": {
"no-console": "off"
}
};
\ No newline at end of file
# OS
Thumbs.db
ehthumbs.db
Desktop.ini
.DS_Store
._*
# Editors
*~
*.swp
*.tmproj
*.tmproject
*.sublime-*
.idea/
.project/
.settings/
.vscode/
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
bower_components/
node_modules/
# Build-related directories
docs/api/
test/dist/
.eslintcache
.yo-rc.json
# Intentionally left blank, so that npm does not ignore anything by default,
# but relies on the package.json "files" array to explicitly define what ends
# up in the package.
dist: xenial
language: node_js
# node version is specified using the .nvmrc file
cache: npm
before_install:
- npm install -g greenkeeper-lockfile@1
before_script:
- greenkeeper-lockfile-update
after_script:
- greenkeeper-lockfile-upload
addons:
firefox: latest
chrome: stable
services:
- xvfb
<a name="0.1.0"></a>
# [0.1.0](https://github.com/thomasdeppisch/videojs-xr/compare/v0.0.5...v0.1.0) (2021-03-09)
<a name="0.0.5"></a>
## [0.0.5](https://github.com/thomasdeppisch/videojs-xr/compare/v0.0.4...v0.0.5) (2021-03-09)
<a name="0.0.4"></a>
## [0.0.4](https://github.com/thomasdeppisch/videojs-xr/compare/v0.0.3...v0.0.4) (2021-03-02)
<a name="0.0.3"></a>
## [0.0.3](https://github.com/thomasdeppisch/videojs-xr/compare/v0.0.2...v0.0.3) (2020-05-11)
<a name="0.0.2"></a>
## 0.0.2 (2020-05-11)
# CONTRIBUTING
We welcome contributions from everyone!
## Getting Started
Make sure you have Node.js 8 or higher and npm installed.
1. Fork this repository and clone your fork
1. Install dependencies: `npm install`
1. Run a development server: `npm start`
### Making Changes
Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship.
When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository.
### Running Tests
Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma].
- In all available and supported browsers: `npm test`
- In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc.
- While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local]
[karma]: http://karma-runner.github.io/
[local]: http://localhost:9999/test/
[conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md
Original codebase: Copyright (c) Thomas Deppisch
180_LR implementation: Copyright (C) Franco Lanza
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# videojs-xr
This is a plugin for using WebXR with [videojs](https://github.com/videojs), based on [videojs-vr](https://github.com/videojs/videojs-vr) and forked from [videojs-xr](https://github.com/thomasdeppisch/videojs-xr). Videojs-xr supports the Oculus browser and Firefox Reality on Oculus Quest. For a jump start how to use it with videojs see the [index.html](https://github.com/thomasdeppisch/videojs-xr/blob/master/index.html).
You can see it in action [here (NSFW! it's PORN!)](https://www.sexhack.me/v/vr180-3d-blowcum/).
Currently the only supported video format is stereo 180° LR side by side equirectangular.
## Installation
```sh
npm install --save videojs-xr
```
## Usage
To include videojs-xr on your website or web application, use any of the following methods.
### `<script>` Tag
This is the simplest case. Get the script in whatever way you prefer and include the plugin _after_ you include [video.js][videojs], so that the `videojs` global is available.
```html
<script src="//path/to/video.min.js"></script>
<script src="//path/to/videojs-xr.min.js"></script>
<script>
var player = videojs('my-video');
player.xr();
</script>
```
### Browserify/CommonJS
When using with Browserify, install videojs-xr via npm and `require` the plugin as you would any other module.
```js
var videojs = require('video.js');
// The actual plugin function is exported by this module, but it is also
// attached to the `Player.prototype`; so, there is no need to assign it
// to a variable.
require('videojs-xr');
var player = videojs('my-video');
player.xr();
```
### RequireJS/AMD
When using with RequireJS (or another AMD library), get the script in whatever way you prefer and `require` the plugin as you normally would:
```js
require(['video.js', 'videojs-xr'], function(videojs) {
var player = videojs('my-video');
player.xr();
});
```
## License
MIT. Copyright (c) Thomas Deppisch
[videojs]: http://videojs.com/
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>videojs-xr Demo</title>
<!-- <link href="node_modules/video.js/dist/video-js.css" rel="stylesheet"> -->
<link href="https://vjs.zencdn.net/7.10.2/video-js.css" rel="stylesheet" />
<link href="./dist/videojs-xr.css" rel="stylesheet">
</head>
<body>
<video id="videojs-xr-player" class="video-js vjs-default-skin" controls>
<!-- <source src="//vjs.zencdn.net/v/oceans.mp4" type='video/mp4'>
<source src="//vjs.zencdn.net/v/oceans.webm" type='video/webm'> -->
<source src="./media/hoast_analyzer.mp4" type='video/mp4'>
</video>
<ul>
<li><a href="/test/debug.html">Run unit tests in browser.</a></li>
</ul>
<script src="https://vjs.zencdn.net/7.10.2/video.min.js"></script>
<script src="./dist/videojs-xr.js"></script>
<script>
(function(window, videojs) {
var examplePlayer = window.examplePlayer = videojs('videojs-xr-player');
var xr = window.xr = examplePlayer.xr();
}(window, window.videojs));
</script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "videojs-xr",
"version": "0.1.0",
"description": "Plugin for using WebXR with videojs, based on videojs-vr",
"main": "dist/videojs-xr.cjs.js",
"module": "dist/videojs-xr.es.js",
"repository": "https://github.com/thomasdeppisch/videojs-xr",
"generator-videojs-plugin": {
"version": "7.6.3"
},
"browserslist": [
"defaults",
"ie 11"
],
"scripts": {
"prebuild": "npm run clean",
"build": "npm-run-all -p build:*",
"build:css": "postcss -o dist/videojs-xr.css --config scripts/postcss.config.js src/plugin.css",
"build:js": "rollup -c scripts/rollup.config.js",
"clean": "shx rm -rf ./dist ./test/dist",
"postclean": "shx mkdir -p ./dist ./test/dist",
"lint": "vjsstandard",
"server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch",
"start": "npm-run-all -p server watch",
"pretest": "npm-run-all lint build",
"test": "npm-run-all test:*",
"posttest": "shx cat test/dist/coverage/text.txt",
"test:unit": "karma start scripts/karma.conf.js",
"test:verify": "vjsverify --skip-es-check --verbose",
"update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s",
"preversion": "npm test",
"version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md",
"watch": "npm-run-all -p watch:*",
"watch:css": "npm run build:css -- -w",
"watch:js": "npm run build:js -- -w"
},
"engines": {
"node": ">=8",
"npm": ">=5"
},
"keywords": [
"videojs",
"videojs-plugin"
],
"author": "Thomas Deppisch",
"license": "MIT",
"vjsstandard": {
"ignore": [
"dist",
"docs",
"test/dist"
]
},
"files": [
"CONTRIBUTING.md",
"dist/",
"docs/",
"index.html",
"scripts/",
"src/",
"test/"
],
"husky": {
"hooks": {}
},
"dependencies": {
"global": "^4.4.0",
"shx": "^0.3.4",
"three": "^0.125.2",
"video.js": "^7.7.6",
"webxr-polyfill": "^2.0.2"
},
"devDependencies": {
"@videojs/generator-helpers": "~1.0.0",
"babel-polyfill": "^6.26.0",
"eslint": "^6.8.0",
"karma": "^4.4.1",
"postcss-cli": "^6.0.0",
"rollup": "^1.32.1",
"sinon": "^7.5.0",
"videojs-generate-karma-config": "^5.3.1",
"videojs-generate-postcss-config": "~2.1.0",
"videojs-generate-rollup-config": "^3.2.1",
"videojs-generator-verify": "~1.2.0",
"videojs-standard": "^8.0.4"
}
}
const generate = require('videojs-generate-karma-config');
module.exports = function(config) {
// see https://github.com/videojs/videojs-generate-karma-config
// for options
const options = {};
config = generate(config, options);
// any other custom stuff not supported by options here!
};
const generate = require('videojs-generate-postcss-config');
module.exports = function(context) {
const result = generate({}, context);
// do custom stuff here
return result;
};
const generate = require('videojs-generate-rollup-config');
// see https://github.com/videojs/videojs-generate-rollup-config
// for options
const options = {};
const config = generate(options);
// Add additonal builds/customization here!
// export the builds to rollup
export default Object.values(config.builds);
/**
* @author richt / http://richt.me
* @author WestLangley / http://github.com/WestLangley
*
* W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html)
*/
import {
Euler,
MathUtils,
Quaternion,
Vector3
} from "three";
var DeviceOrientationControls = function ( object ) {
var scope = this;
this.object = object;
this.object.rotation.reorder( 'YXZ' );
this.enabled = true;
this.deviceOrientation = {};
this.screenOrientation = 0;
this.alphaOffset = 0; // radians
var onDeviceOrientationChangeEvent = function ( event ) {
scope.deviceOrientation = event;
};
var onScreenOrientationChangeEvent = function () {
scope.screenOrientation = window.orientation || 0;
};
// The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y''
var setObjectQuaternion = function () {
var zee = new Vector3( 0, 0, 1 );
var euler = new Euler();
var q0 = new Quaternion();
var q1 = new Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis
return function ( quaternion, alpha, beta, gamma, orient ) {
euler.set( beta, alpha, - gamma, 'YXZ' ); // 'ZXY' for the device, but 'YXZ' for us
quaternion.setFromEuler( euler ); // orient the device
quaternion.multiply( q1 ); // camera looks out the back of the device, not the top
quaternion.multiply( q0.setFromAxisAngle( zee, - orient ) ); // adjust for screen orientation
};
}();
this.connect = function () {
onScreenOrientationChangeEvent(); // run once on load
// iOS 13+
if ( window.DeviceOrientationEvent !== undefined && typeof window.DeviceOrientationEvent.requestPermission === 'function' ) {
window.DeviceOrientationEvent.requestPermission().then( function ( response ) {
if ( response == 'granted' ) {
window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
}
} ).catch( function ( error ) {
console.error( 'THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:', error );
} );
} else {
window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
}
scope.enabled = true;
};
this.disconnect = function () {
window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
scope.enabled = false;
};
this.update = function () {
if ( scope.enabled === false ) return;
var device = scope.deviceOrientation;
if ( device ) {
var alpha = device.alpha ? MathUtils.degToRad( device.alpha ) + scope.alphaOffset : 0; // Z
var beta = device.beta ? MathUtils.degToRad( device.beta ) : 0; // X'
var gamma = device.gamma ? MathUtils.degToRad( device.gamma ) : 0; // Y''
var orient = scope.screenOrientation ? MathUtils.degToRad( scope.screenOrientation ) : 0; // O
setObjectQuaternion( scope.object.quaternion, alpha, beta, gamma, orient );
}
};
this.dispose = function () {
scope.disconnect();
};
this.connect();
};
export { DeviceOrientationControls };
/**
* @author qiao / https://github.com/qiao
* @author mrdoob / http://mrdoob.com
* @author alteredq / http://alteredqualia.com/
* @author WestLangley / http://github.com/WestLangley
* @author erich666 / http://erichaines.com
* @author ScieCode / http://github.com/sciecode
*/
import {
EventDispatcher,
MOUSE,
Quaternion,
Spherical,
TOUCH,
Vector2,
Vector3
} from "three";
// This set of controls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
//
// Orbit - left mouse / touch: one-finger move
// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
var OrbitControls = function ( object, domElement ) {
if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' );
if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );
this.object = object;
this.domElement = domElement;
// Set to false to disable this control
this.enabled = true;
// "target" sets the location of focus, where the object orbits around
this.target = new Vector3();
// How far you can dolly in and out ( PerspectiveCamera only )
this.currentDistance = 1;
this.minDistance = -700;
this.maxDistance = 200;
// How far you can zoom in and out ( OrthographicCamera only )
this.minZoom = 0;
this.maxZoom = Infinity;
// How far you can orbit vertically, upper and lower limits.
// Range is 0 to Math.PI radians.
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
// How far you can orbit horizontally, upper and lower limits.
// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
this.minAzimuthAngle = - Infinity; // radians
this.maxAzimuthAngle = Infinity; // radians
// Set to true to enable damping (inertia)
// If damping is enabled, you must call controls.update() in your animation loop
this.enableDamping = false;
this.dampingFactor = 0.05;
// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
// Set to false to disable zooming
this.enableZoom = true;
this.zoomSpeed = 1.0;
// Set to false to disable rotating
this.enableRotate = true;
this.rotateSpeed = 1.0;
// Set to false to disable panning
this.enablePan = true;
this.panSpeed = 1.0;
this.screenSpacePanning = false; // if true, pan in screen-space
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
// Set to true to automatically rotate around the target
// If auto-rotate is enabled, you must call controls.update() in your animation loop
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
// Set to false to disable use of the keys
this.enableKeys = true;
// The four arrow keys
this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
// Mouse buttons
this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
// Touch fingers
this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.zoom0 = this.object.zoom;
//
// public methods
//
this.getPolarAngle = function () {
return spherical.phi;
};
this.getAzimuthalAngle = function () {
return spherical.theta;
};
this.saveState = function () {
scope.target0.copy( scope.target );
scope.position0.copy( scope.object.position );
scope.zoom0 = scope.object.zoom;
};
this.reset = function () {
scope.target.copy( scope.target0 );
scope.object.position.copy( scope.position0 );
scope.object.zoom = scope.zoom0;
scope.object.updateProjectionMatrix();
scope.dispatchEvent( changeEvent );
scope.update();
state = STATE.NONE;
};
// this method is exposed, but perhaps it would be better if we can make it private...
this.update = function () {
var offset = new Vector3();
// so camera.up is the orbit axis
var quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
var quatInverse = quat.clone().inverse();
var lastPosition = new Vector3();
var lastQuaternion = new Quaternion();
return function update() {
var position = scope.object.position;
offset.copy( position ).sub( scope.target );
// rotate offset to "y-axis-is-up" space
offset.applyQuaternion( quat );
// angle from z-axis around y-axis
spherical.setFromVector3( offset );
if ( scope.autoRotate && state === STATE.NONE ) {
rotateLeft( getAutoRotationAngle() );
}
if ( scope.enableDamping ) {
spherical.theta += sphericalDelta.theta * scope.dampingFactor;
spherical.phi += sphericalDelta.phi * scope.dampingFactor;
} else {
spherical.theta += sphericalDelta.theta;
spherical.phi += sphericalDelta.phi;
}
// restrict theta to be between desired limits
spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) );
// restrict phi to be between desired limits
spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
spherical.makeSafe();
if (scope.currentDistance >= 0)
{
var newRad = scope.currentDistance;
if (spherical.radius != newRad)
{
spherical.radius = scope.currentDistance;
zoomChanged = true;
}
}
else // simulate very close zoom by changing fov
{
var newFov = 75 + scope.currentDistance * 0.06;
if (scope.object.fov != newFov)
{
scope.object.fov = newFov;
scope.object.updateProjectionMatrix();
zoomChanged = true;
}
}
// restrict radius to be between desired limits
spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
// move target to panned location
if ( scope.enableDamping === true ) {
scope.target.addScaledVector( panOffset, scope.dampingFactor );
} else {
scope.target.add( panOffset );
}
offset.setFromSpherical( spherical );
// rotate offset back to "camera-up-vector-is-up" space
offset.applyQuaternion( quatInverse );
position.copy( scope.target ).add( offset );
scope.object.lookAt( scope.target );
if ( scope.enableDamping === true ) {
sphericalDelta.theta *= ( 1 - scope.dampingFactor );
sphericalDelta.phi *= ( 1 - scope.dampingFactor );
panOffset.multiplyScalar( 1 - scope.dampingFactor );
} else {
sphericalDelta.set( 0, 0, 0 );
panOffset.set( 0, 0, 0 );
}
// update condition is:
// min(camera displacement, camera rotation in radians)^2 > EPS
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
if ( zoomChanged ||
lastPosition.distanceToSquared( scope.object.position ) > EPS ||
8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
if (zoomChanged)
scope.dispatchEvent( zoomEvent );
else
scope.dispatchEvent( changeEvent );
lastPosition.copy( scope.object.position );
lastQuaternion.copy( scope.object.quaternion );
zoomChanged = false;
return true;
}
return false;
};
}();
this.dispose = function () {
scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );
scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
scope.domElement.removeEventListener( 'keydown', onKeyDown, false );
//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
};
//
// internals
//
var scope = this;
var changeEvent = { type: 'change' };
var zoomEvent = { type: 'zoom' };
var startEvent = { type: 'start' };
var endEvent = { type: 'end' };
var STATE = {
NONE: - 1,
ROTATE: 0,
DOLLY: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_PAN: 4,
TOUCH_DOLLY_PAN: 5,
TOUCH_DOLLY_ROTATE: 6
};
var state = STATE.NONE;
var EPS = 0.000001;
// current position in spherical coordinates
var spherical = new Spherical();
var sphericalDelta = new Spherical();
var panOffset = new Vector3();
var zoomChanged = false;
var rotateStart = new Vector2();
var rotateEnd = new Vector2();
var rotateDelta = new Vector2();
var panStart = new Vector2();
var panEnd = new Vector2();
var panDelta = new Vector2();
var dollyStart = new Vector2();
var dollyEnd = new Vector2();
var dollyDelta = new Vector2();
function getAutoRotationAngle() {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
}
function getZoomScale() {
return Math.pow( 0.95, scope.zoomSpeed );
}
function rotateLeft( angle ) {
sphericalDelta.theta -= angle;
}
function rotateUp( angle ) {
sphericalDelta.phi -= angle;
}
var panLeft = function () {
var v = new Vector3();
return function panLeft( distance, objectMatrix ) {
v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
v.multiplyScalar( - distance );
panOffset.add( v );
};
}();
var panUp = function () {
var v = new Vector3();
return function panUp( distance, objectMatrix ) {
if ( scope.screenSpacePanning === true ) {
v.setFromMatrixColumn( objectMatrix, 1 );
} else {
v.setFromMatrixColumn( objectMatrix, 0 );
v.crossVectors( scope.object.up, v );
}
v.multiplyScalar( distance );
panOffset.add( v );
};
}();
// deltaX and deltaY are in pixels; right and down are positive
var pan = function () {
var offset = new Vector3();
return function pan( deltaX, deltaY ) {
var element = scope.domElement;
if ( scope.object.isPerspectiveCamera ) {
// perspective
var position = scope.object.position;
offset.copy( position ).sub( scope.target );
var targetDistance = offset.length();
// half of the fov is center to top of screen
targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
// we use only clientHeight here so aspect ratio does not distort speed
panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
} else if ( scope.object.isOrthographicCamera ) {
// orthographic
panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
} else {
// camera neither orthographic nor perspective
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
scope.enablePan = false;
}
};
}();
function dollyIn( dollyScale ) {
if ( scope.object.isOrthographicCamera ) {
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
scope.object.updateProjectionMatrix();
zoomChanged = true;
} else {
if ( !scope.object.isPerspectiveCamera ) { // dolly for perspective camera not implemented
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
}
function dollyOut( dollyScale ) {
if ( scope.object.isOrthographicCamera ) {
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
scope.object.updateProjectionMatrix();
zoomChanged = true;
} else {
if ( !scope.object.isPerspectiveCamera ) { // dolly for perspective camera not implemented
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
scope.enableZoom = false;
}
}
}
//
// event callbacks - update the object state
//
function handleMouseDownRotate( event ) {
rotateStart.set( event.clientX, event.clientY );
}
function handleMouseDownDolly( event ) {
dollyStart.set( event.clientX, event.clientY );
}
function handleMouseDownPan( event ) {
panStart.set( event.clientX, event.clientY );
}
function handleMouseMoveRotate( event ) {
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
var element = scope.domElement;
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
rotateStart.copy( rotateEnd );
scope.update();
}
function handleMouseMoveDolly( event ) {
dollyEnd.set( event.clientX, event.clientY );
dollyDelta.subVectors( dollyEnd, dollyStart );
if ( dollyDelta.y > 0 ) {
dollyIn( getZoomScale() );
} else if ( dollyDelta.y < 0 ) {
dollyOut( getZoomScale() );
}
dollyStart.copy( dollyEnd );
scope.update();
}
function handleMouseMovePan( event ) {
panEnd.set( event.clientX, event.clientY );
panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
scope.update();
}
function handleMouseUp( /*event*/ ) {
// no-op
}
function handleMouseWheel( event ) {
scope.currentDistance += Math.sign(event.deltaY) * 15;
// make sure scope.currentDistance never gets 0!
scope.currentDistance = Math.clamp( scope.currentDistance != 0 ? scope.currentDistance : Math.sign(event.deltaY) + 0.1, scope.minDistance, scope.maxDistance );
if ( event.deltaY < 0 ) {
dollyOut( getZoomScale() );
} else if ( event.deltaY > 0 ) {
dollyIn( getZoomScale() );
}
scope.update();
}
function handleKeyDown( event ) {
var needsUpdate = false;
switch ( event.keyCode ) {
case scope.keys.UP:
pan( 0, scope.keyPanSpeed );
needsUpdate = true;
break;
case scope.keys.BOTTOM:
pan( 0, - scope.keyPanSpeed );
needsUpdate = true;
break;
case scope.keys.LEFT:
pan( scope.keyPanSpeed, 0 );
needsUpdate = true;
break;
case scope.keys.RIGHT:
pan( - scope.keyPanSpeed, 0 );
needsUpdate = true;
break;
}
if ( needsUpdate ) {
// prevent the browser from scrolling on cursor keys
event.preventDefault();
scope.update();
}
}
function handleTouchStartRotate( event ) {
if ( event.touches.length == 1 ) {
rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
} else {
var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
rotateStart.set( x, y );
}
}
function handleTouchStartPan( event ) {
if ( event.touches.length == 1 ) {
panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
} else {
var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
panStart.set( x, y );
}
}
function handleTouchStartDolly( event ) {
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
var distance = Math.sqrt( dx * dx + dy * dy );
dollyStart.set( 0, distance );
}
function handleTouchStartDollyPan( event ) {
if ( scope.enableZoom ) handleTouchStartDolly( event );
if ( scope.enablePan ) handleTouchStartPan( event );
}
function handleTouchStartDollyRotate( event ) {
if ( scope.enableZoom ) handleTouchStartDolly( event );
if ( scope.enableRotate ) handleTouchStartRotate( event );
}
function handleTouchMoveRotate( event ) {
if ( event.touches.length == 1 ) {
rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
} else {
var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
rotateEnd.set( x, y );
}
rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
var element = scope.domElement;
rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
rotateStart.copy( rotateEnd );
}
function handleTouchMovePan( event ) {
if ( event.touches.length == 1 ) {
panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
} else {
var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
panEnd.set( x, y );
}
panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
pan( panDelta.x, panDelta.y );
panStart.copy( panEnd );
}
function handleTouchMoveDolly( event ) {
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
var distance = Math.sqrt( dx * dx + dy * dy );
dollyEnd.set( 0, distance );
dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
dollyIn( dollyDelta.y );
dollyStart.copy( dollyEnd );
}
function handleTouchMoveDollyPan( event ) {
if ( scope.enableZoom ) handleTouchMoveDolly( event );
if ( scope.enablePan ) handleTouchMovePan( event );
}
function handleTouchMoveDollyRotate( event ) {
if ( scope.enableZoom ) handleTouchMoveDolly( event );
if ( scope.enableRotate ) handleTouchMoveRotate( event );
}
function handleTouchEnd( /*event*/ ) {
// no-op
}
//
// event handlers - FSM: listen for events and reset state
//
function onMouseDown( event ) {
if ( scope.enabled === false ) return;
// Prevent the browser from scrolling.
event.preventDefault();
// Manually set the focus since calling preventDefault above
// prevents the browser from setting it automatically.
scope.domElement.focus ? scope.domElement.focus() : window.focus();
var mouseAction;
switch ( event.button ) {
case 0:
mouseAction = scope.mouseButtons.LEFT;
break;
case 1:
mouseAction = scope.mouseButtons.MIDDLE;
break;
case 2:
mouseAction = scope.mouseButtons.RIGHT;
break;
default:
mouseAction = - 1;
}
switch ( mouseAction ) {
case MOUSE.DOLLY:
if ( scope.enableZoom === false ) return;
handleMouseDownDolly( event );
state = STATE.DOLLY;
break;
case MOUSE.ROTATE:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
if ( scope.enablePan === false ) return;
handleMouseDownPan( event );
state = STATE.PAN;
} else {
if ( scope.enableRotate === false ) return;
handleMouseDownRotate( event );
state = STATE.ROTATE;
}
break;
case MOUSE.PAN:
if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
if ( scope.enableRotate === false ) return;
handleMouseDownRotate( event );
state = STATE.ROTATE;
} else {
if ( scope.enablePan === false ) return;
handleMouseDownPan( event );
state = STATE.PAN;
}
break;
default:
state = STATE.NONE;
}
if ( state !== STATE.NONE ) {
document.addEventListener( 'mousemove', onMouseMove, false );
document.addEventListener( 'mouseup', onMouseUp, false );
scope.dispatchEvent( startEvent );
}
}
function onMouseMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
switch ( state ) {
case STATE.ROTATE:
if ( scope.enableRotate === false ) return;
handleMouseMoveRotate( event );
break;
case STATE.DOLLY:
if ( scope.enableZoom === false ) return;
handleMouseMoveDolly( event );
break;
case STATE.PAN:
if ( scope.enablePan === false ) return;
handleMouseMovePan( event );
break;
}
}
function onMouseUp( event ) {
if ( scope.enabled === false ) return;
handleMouseUp( event );
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
scope.dispatchEvent( endEvent );
state = STATE.NONE;
}
function onMouseWheel( event ) {
if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
event.preventDefault();
event.stopPropagation();
scope.dispatchEvent( startEvent );
handleMouseWheel( event );
scope.dispatchEvent( endEvent );
}
function onKeyDown( event ) {
if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;
handleKeyDown( event );
}
function onTouchStart( event ) {
if ( scope.enabled === false ) return;
event.preventDefault(); // prevent scrolling
switch ( event.touches.length ) {
case 1:
switch ( scope.touches.ONE ) {
case TOUCH.ROTATE:
if ( scope.enableRotate === false ) return;
handleTouchStartRotate( event );
state = STATE.TOUCH_ROTATE;
break;
case TOUCH.PAN:
if ( scope.enablePan === false ) return;
handleTouchStartPan( event );
state = STATE.TOUCH_PAN;
break;
default:
state = STATE.NONE;
}
break;
case 2:
switch ( scope.touches.TWO ) {
case TOUCH.DOLLY_PAN:
if ( scope.enableZoom === false && scope.enablePan === false ) return;
handleTouchStartDollyPan( event );
state = STATE.TOUCH_DOLLY_PAN;
break;
case TOUCH.DOLLY_ROTATE:
if ( scope.enableZoom === false && scope.enableRotate === false ) return;
handleTouchStartDollyRotate( event );
state = STATE.TOUCH_DOLLY_ROTATE;
break;
default:
state = STATE.NONE;
}
break;
default:
state = STATE.NONE;
}
if ( state !== STATE.NONE ) {
scope.dispatchEvent( startEvent );
}
}
function onTouchMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault(); // prevent scrolling
event.stopPropagation();
switch ( state ) {
case STATE.TOUCH_ROTATE:
if ( scope.enableRotate === false ) return;
handleTouchMoveRotate( event );
scope.update();
break;
case STATE.TOUCH_PAN:
if ( scope.enablePan === false ) return;
handleTouchMovePan( event );
scope.update();
break;
case STATE.TOUCH_DOLLY_PAN:
if ( scope.enableZoom === false && scope.enablePan === false ) return;
handleTouchMoveDollyPan( event );
scope.update();
break;
case STATE.TOUCH_DOLLY_ROTATE:
if ( scope.enableZoom === false && scope.enableRotate === false ) return;
handleTouchMoveDollyRotate( event );
scope.update();
break;
default:
state = STATE.NONE;
}
}
function onTouchEnd( event ) {
if ( scope.enabled === false ) return;
handleTouchEnd( event );
scope.dispatchEvent( endEvent );
state = STATE.NONE;
}
function onContextMenu( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
}
//
scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );
scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
scope.domElement.addEventListener( 'wheel', onMouseWheel, false );
scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
scope.domElement.addEventListener( 'touchmove', onTouchMove, false );
scope.domElement.addEventListener( 'keydown', onKeyDown, false );
// make sure element can receive keys.
if ( scope.domElement.tabIndex === - 1 ) {
scope.domElement.tabIndex = 0;
}
// force an update at start
this.update();
};
OrbitControls.prototype = Object.create( EventDispatcher.prototype );
OrbitControls.prototype.constructor = OrbitControls;
// This set of controls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
// This is very similar to OrbitControls, another set of touch behavior
//
// Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
// Pan - left mouse, or arrow keys / touch: one-finger move
var MapControls = function ( object, domElement ) {
OrbitControls.call( this, object, domElement );
this.mouseButtons.LEFT = MOUSE.PAN;
this.mouseButtons.RIGHT = MOUSE.ROTATE;
this.touches.ONE = TOUCH.PAN;
this.touches.TWO = TOUCH.DOLLY_ROTATE;
};
MapControls.prototype = Object.create( EventDispatcher.prototype );
MapControls.prototype.constructor = MapControls;
export { OrbitControls, MapControls };
import videojs from 'video.js';
const BigPlayButton = videojs.getComponent('BigPlayButton');
class BigVrPlayButton extends BigPlayButton {
buildCSSClass() {
return `vjs-big-vr-play-button ${super.buildCSSClass()}`;
}
}
videojs.registerComponent('BigVrPlayButton', BigVrPlayButton);
export default BigVrPlayButton;
import videojs from 'video.js';
/**
* This class reacts to interactions with the canvas and
* triggers appropriate functionality on the player. Right now
* it does two things:
*
* 1. A `mousedown`/`touchstart` followed by `touchend`/`mouseup` without any
* `touchmove` or `mousemove` toggles play/pause on the player
* 2. Only moving on/clicking the control bar or toggling play/pause should
* show the control bar. Moving around the scene in the canvas should not.
*/
class CanvasPlayerControls extends videojs.EventTarget {
constructor(player, canvas) {
super();
this.player = player;
this.canvas = canvas;
this.onMoveEnd = videojs.bind(this, this.onMoveEnd);
this.onMoveStart = videojs.bind(this, this.onMoveStart);
this.onMove = videojs.bind(this, this.onMove);
this.onControlBarMove = videojs.bind(this, this.onControlBarMove);
this.player.controlBar.on([
'mousedown',
'mousemove',
'mouseup',
'touchstart',
'touchmove',
'touchend'
], this.onControlBarMove);
// we have to override these here because
// video.js listens for user activity on the video element
// and makes the user active when the mouse moves.
// We don't want that for 3d videos
this.oldReportUserActivity = this.player.reportUserActivity;
this.player.reportUserActivity = () => {};
// canvas movements
this.canvas.addEventListener('mousedown', this.onMoveStart);
this.canvas.addEventListener('touchstart', this.onMoveStart);
this.canvas.addEventListener('mousemove', this.onMove);
this.canvas.addEventListener('touchmove', this.onMove);
this.canvas.addEventListener('mouseup', this.onMoveEnd);
this.canvas.addEventListener('touchend', this.onMoveEnd);
this.shouldTogglePlay = false;
}
togglePlay() {
this.trigger('vrtoggleplay');
}
onMoveStart(e) {
// if the player does not have a controlbar or
// the move was a mouse click but not left click do not
// toggle play.
if (!this.player.controls() || (e.type === 'mousedown' && !videojs.dom.isSingleLeftClick(e))) {
this.shouldTogglePlay = false;
return;
}
this.shouldTogglePlay = true;
this.touchMoveCount_ = 0;
}
onMoveEnd(e) {
// We want to have the same behavior in VR360 Player and standar player.
// in touchend we want to know if was a touch click, for a click we show the bar,
// otherwise continue with the mouse logic.
//
// Maximum movement allowed during a touch event to still be considered a tap
// Other popular libs use anywhere from 2 (hammer.js) to 15,
// so 10 seems like a nice, round number.
if (e.type === 'touchend' && this.touchMoveCount_ < 10) {
if (this.player.userActive() === false) {
this.player.userActive(true);
return;
}
this.player.userActive(false);
return;
}
if (!this.shouldTogglePlay) {
return;
}
// We want the same behavior in Desktop for VR360 and standar player
if (e.type === 'mouseup') {
this.togglePlay();
}
}
onMove(e) {
// Increase touchMoveCount_ since Android detects 1 - 6 touches when user click normaly
this.touchMoveCount_++;
this.shouldTogglePlay = false;
}
onControlBarMove(e) {
this.player.userActive(true);
}
dispose() {
this.canvas.removeEventListener('mousedown', this.onMoveStart);
this.canvas.removeEventListener('touchstart', this.onMoveStart);
this.canvas.removeEventListener('mousemove', this.onMove);
this.canvas.removeEventListener('touchmove', this.onMove);
this.canvas.removeEventListener('mouseup', this.onMoveEnd);
this.canvas.removeEventListener('touchend', this.onMoveEnd);
this.player.controlBar.off([
'mousedown',
'mousemove',
'mouseup',
'touchstart',
'touchmove',
'touchend'
], this.onControlBarMove);
this.player.reportUserActivity = this.oldReportUserActivity;
}
}
export default CanvasPlayerControls;
import window from 'global/window';
import videojs from 'video.js';
// Add Cardboard button
const Button = videojs.getComponent('Button');
class CardboardButton extends Button {
constructor(player, options) {
super(player, options);
// this.handleVrDisplayActivate_ = videojs.bind(this, this.handleVrDisplayActivate_);
// this.handleVrDisplayDeactivate_ = videojs.bind(this, this.handleVrDisplayDeactivate_);
// this.handleVrDisplayPresentChange_ = videojs.bind(this, this.handleVrDisplayPresentChange_);
// this.handleOrientationChange_ = videojs.bind(this, this.handleOrientationChange_);
// window.addEventListener('orientationchange', this.handleOrientationChange_);
// window.addEventListener('vrdisplayactivate', this.handleVrDisplayActivate_);
// window.addEventListener('vrdisplaydeactivate', this.handleVrDisplayDeactivate_);
// vrdisplaypresentchange does not fire activate or deactivate
// and happens when hitting the back button during cardboard mode
// so we need to make sure we stay in the correct state by
// listening to it and checking if we are presenting it or not
// window.addEventListener('vrdisplaypresentchange', this.handleVrDisplayPresentChange_);
// we cannot show the cardboard button in fullscreen on
// android as it breaks the controls, and makes it impossible
// to exit cardboard mode
// if (videojs.browser.IS_ANDROID) {
// this.on(player, 'fullscreenchange', () => {
// if (player.isFullscreen()) {
// this.hide();
// } else {
// this.show();
// }
// });
// }
}
buildCSSClass() {
return `vjs-button-vr ${super.buildCSSClass()}`;
}
// handleVrDisplayPresentChange_() {
// if (!this.player_.vr().vrDisplay.isPresenting && this.active_) {
// this.handleVrDisplayDeactivate_();
// }
// if (this.player_.vr().vrDisplay.isPresenting && !this.active_) {
// this.handleVrDisplayActivate_();
// }
// }
// handleOrientationChange_() {
// if (this.active_ && videojs.browser.IS_IOS) {
// this.changeSize_();
// }
// }
// changeSize_() {
// this.player_.width(window.innerWidth);
// this.player_.height(window.innerHeight);
// window.dispatchEvent(new window.Event('resize'));
// }
// handleVrDisplayActivate_() {
// // we mimic fullscreen on IOS
// if (videojs.browser.IS_IOS) {
// this.oldWidth_ = this.player_.currentWidth();
// this.oldHeight_ = this.player_.currentHeight();
// this.player_.enterFullWindow();
// this.changeSize_();
// }
// this.active_ = true;
// }
// handleVrDisplayDeactivate_() {
// // un-mimic fullscreen on iOS
// if (videojs.browser.IS_IOS) {
// if (this.oldWidth_) {
// this.player_.width(this.oldWidth_);
// }
// if (this.oldHeight_) {
// this.player_.height(this.oldHeight_);
// }
// this.player_.exitFullWindow();
// }
// this.active_ = false;
// }
handleClick(event) {
// if cardboard mode display is not active, activate it
// otherwise deactivate it
if (!this.active_) {
// This starts playback mode when the cardboard button
// is clicked on Andriod. We need to do this as the controls
// disappear
// if (!this.player_.hasStarted() && videojs.browser.IS_ANDROID) {
// this.player_.play();
// }
window.dispatchEvent(new window.Event('vrdisplayactivate'));
this.active_ = true;
} else {
window.dispatchEvent(new window.Event('vrdisplaydeactivate'));
this.active_ = false;
}
}
dispose() {
super.dispose();
// window.removeEventListener('vrdisplayactivate', this.handleVrDisplayActivate_);
// window.removeEventListener('vrdisplaydeactivate', this.handleVrDisplayDeactivate_);
// window.removeEventListener('vrdisplaypresentchange', this.handleVrDisplayPresentChange_);
}
}
videojs.registerComponent('CardboardButton', CardboardButton);
export default CardboardButton;
import * as THREE from 'three';
import { OrbitControls } from './OrbitControls.js';
import { DeviceOrientationControls } from './DeviceOrientationControls.js';
/**
* Convert a quaternion to an angle
*
* Taken from https://stackoverflow.com/a/35448946
* Thanks P. Ellul
*/
function Quat2Angle(x, y, z, w) {
const test = x * y + z * w;
// singularity at north pole
if (test > 0.499) {
const yaw = 2 * Math.atan2(x, w);
const pitch = Math.PI / 2;
const roll = 0;
return new THREE.Vector3(pitch, roll, yaw);
}
// singularity at south pole
if (test < -0.499) {
const yaw = -2 * Math.atan2(x, w);
const pitch = -Math.PI / 2;
const roll = 0;
return new THREE.Vector3(pitch, roll, yaw);
}
const sqx = x * x;
const sqy = y * y;
const sqz = z * z;
const yaw = Math.atan2(2 * y * w - 2 * x * z, 1 - 2 * sqy - 2 * sqz);
const pitch = Math.asin(2 * test);
const roll = Math.atan2(2 * x * w - 2 * y * z, 1 - 2 * sqx - 2 * sqz);
return new THREE.Vector3(pitch, roll, yaw);
}
class OrbitOrientationControls {
constructor(options) {
this.object = options.camera;
this.domElement = options.canvas;
this.orbit = new OrbitControls(this.object, this.domElement);
this.speed = 0.5;
this.orbit.target.set(0, 1, -1);
this.orbit.enableZoom = true;
this.orbit.enablePan = false;
this.orbit.rotateSpeed = -this.speed;
// if orientation is supported
if (options.orientation) {
this.orientation = new DeviceOrientationControls(this.object);
}
// if projection is not full view
// limit the rotation angle in order to not display back half view
if (options.halfView) {
this.orbit.minAzimuthAngle = -Math.PI / 4;
this.orbit.maxAzimuthAngle = Math.PI / 4;
}
}
update() {
// orientation updates the camera using quaternions and
// orbit updates the camera using angles. They are incompatible
// and one update overrides the other. So before
// orbit overrides orientation we convert our quaternion changes to
// an angle change. Then save the angle into orbit so that
// it will take those into account when it updates the camera and overrides
// our changes
if (this.orientation) {
this.orientation.update();
const quat = this.orientation.object.quaternion;
const currentAngle = Quat2Angle(quat.x, quat.y, quat.z, quat.w);
// we also have to store the last angle since quaternions are b
if (typeof this.lastAngle_ === 'undefined') {
this.lastAngle_ = currentAngle;
}
this.orbit.rotateLeft((this.lastAngle_.z - currentAngle.z) * (1 + this.speed));
this.orbit.rotateUp((this.lastAngle_.y - currentAngle.y) * (1 + this.speed));
this.lastAngle_ = currentAngle;
}
this.orbit.update();
}
dispose() {
this.orbit.dispose();
if (this.orientation) {
this.orientation.dispose();
}
}
enable() {
this.orbit.enabled = true;
if (this.orientation)
this.orientation.enabled = true;
}
disable() {
this.orbit.enabled = false;
if (this.orientation)
this.orientation.enabled = false;
}
}
export default OrbitOrientationControls;
.video-js .vjs-big-vr-play-button {
width: 100px;
height: 100px;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='360' viewBox='0 0 360 360'%3E%3Cpath fill='%23FFF' d='M334.883 275.78l-6.374-36.198-6.375-36.2-28.16 23.62-28.164 23.62 25.837 9.41C266.247 296.544 224 320.5 176.25 320.5c-77.47 0-140.5-63.03-140.5-140.5 0-77.472 63.03-140.5 140.5-140.5 53.428 0 99.98 29.978 123.733 73.993l13.304-6.923C287.025 57.76 235.45 24.5 176.25 24.5c-85.743 0-155.5 69.757-155.5 155.5 0 85.742 69.757 155.5 155.5 155.5 54.253 0 102.09-27.94 129.922-70.177l28.71 10.457z'/%3E%3Cpath fill='%23FFF' d='M314.492 175.167c-12.98 0-23.54-10.56-23.54-23.54s10.56-23.54 23.54-23.54c12.98 0 23.54 10.56 23.54 23.54s-10.56 23.54-23.54 23.54zm0-38.08c-8.018 0-14.54 6.522-14.54 14.54s6.522 14.54 14.54 14.54c8.017 0 14.54-6.522 14.54-14.54s-6.523-14.54-14.54-14.54z'/%3E%3Cg fill='%23FFF'%3E%3Cpath d='M88.76 173.102h9.395c4.74-.042 8.495-1.27 11.268-3.682 2.77-2.412 4.157-5.903 4.157-10.474 0-4.4-1.153-7.817-3.46-10.25-2.307-2.434-5.83-3.65-10.568-3.65-4.147 0-7.554 1.195-10.22 3.585-2.666 2.392-4 5.514-4 9.364H69.908c0-4.74 1.26-9.055 3.776-12.95 2.518-3.892 6.03-6.928 10.537-9.108 4.508-2.18 9.554-3.27 15.14-3.27 9.225 0 16.472 2.318 21.74 6.952 5.27 4.634 7.903 11.077 7.903 19.33 0 4.147-1.323 8.05-3.967 11.71-2.646 3.66-6.062 6.422-10.252 8.284 5.078 1.736 8.94 4.465 11.584 8.19s3.968 8.166 3.968 13.33c0 8.294-2.847 14.895-8.538 19.804s-13.17 7.363-22.438 7.363c-8.887 0-16.166-2.37-21.836-7.11-5.67-4.74-8.506-11.045-8.506-18.916h15.425c0 4.062 1.365 7.363 4.094 9.902 2.73 2.54 6.4 3.81 11.014 3.81 4.782 0 8.55-1.27 11.3-3.81s4.126-6.22 4.126-11.045c0-4.865-1.44-8.61-4.316-11.235-2.878-2.623-7.152-3.936-12.822-3.936H88.76V173.1zM187.598 133.493v12.76h-1.904c-8.633.126-15.53 2.497-20.693 7.108-5.162 4.614-8.23 11.152-9.203 19.615 4.95-5.205 11.277-7.808 18.98-7.808 8.166 0 14.608 2.878 19.328 8.633 4.718 5.755 7.077 13.182 7.077 22.28 0 9.395-2.76 17.002-8.284 22.82-5.52 5.818-12.77 8.73-21.74 8.73-9.226 0-16.705-3.407-22.44-10.222-5.733-6.812-8.6-15.742-8.6-26.787v-5.267c0-16.208 3.945-28.903 11.84-38.086 7.89-9.182 19.242-13.774 34.054-13.774h1.586zM171.03 177.61c-3.386 0-6.485.95-9.3 2.855-2.814 1.904-4.877 4.443-6.188 7.617v4.697c0 6.854 1.438 12.304 4.316 16.345 2.877 4.04 6.602 6.062 11.172 6.062s8.188-1.715 10.854-5.143 4-7.934 4-13.52-1.355-10.135-4.063-13.648c-2.708-3.51-6.304-5.267-10.79-5.267zM271.136 187.447c0 13.29-2.486 23.307-7.46 30.057s-12.535 10.125-22.69 10.125c-9.988 0-17.51-3.292-22.566-9.872-5.058-6.58-7.65-16.323-7.776-29.23V172.53c0-13.287 2.485-23.252 7.458-29.896 4.973-6.643 12.558-9.966 22.757-9.966 10.112 0 17.655 3.237 22.63 9.712 4.97 6.475 7.52 16.166 7.647 29.072v15.995zm-15.425-17.265c0-8.674-1.185-15.033-3.554-19.075-2.37-4.04-6.137-6.062-11.3-6.062-5.035 0-8.738 1.915-11.107 5.745-2.37 3.83-3.62 9.807-3.746 17.932v20.948c0 8.633 1.206 15.064 3.618 19.297s6.2 6.348 11.362 6.348c4.95 0 8.61-1.957 10.98-5.87 2.37-3.915 3.62-10.04 3.746-18.378v-20.885z'/%3E%3C/g%3E%3C/svg%3E");
background-size: contain;
background-color: rgba(0, 0, 0, 0.5)
}
.video-js .vjs-big-vr-play-button .vjs-icon-placeholder {
display: none
}
:hover.video-js .vjs-big-vr-play-button {
-webkit-transition: border-color 0.4s, outline 0.4s, background-color 0.4s;
-moz-transition: border-color 0.4s, outline 0.4s, background-color 0.4s;
-ms-transition: border-color 0.4s, outline 0.4s, background-color 0.4s;
-o-transition: border-color 0.4s, outline 0.4s, background-color 0.4s;
transition: border-color 0.4s, outline 0.4s, background-color 0.4s
}
.video-js .vjs-big-vr-play-button::before {
content: ''
}
.video-js canvas {
cursor: move
}
.video-js .vjs-button-vr .vjs-icon-placeholder {
height: 30px;
width: 30px;
display: inline-block;
background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI0ZGRkZGRiI+CiAgICA8cGF0aCBkPSJNMjAuNzQgNkgzLjIxQzIuNTUgNiAyIDYuNTcgMiA3LjI4djEwLjQ0YzAgLjcuNTUgMS4yOCAxLjIzIDEuMjhoNC43OWMuNTIgMCAuOTYtLjMzIDEuMTQtLjc5bDEuNC0zLjQ4Yy4yMy0uNTkuNzktMS4wMSAxLjQ0LTEuMDFzMS4yMS40MiAxLjQ1IDEuMDFsMS4zOSAzLjQ4Yy4xOS40Ni42My43OSAxLjExLjc5aDQuNzljLjcxIDAgMS4yNi0uNTcgMS4yNi0xLjI4VjcuMjhjMC0uNy0uNTUtMS4yOC0xLjI2LTEuMjh6TTcuNSAxNC42MmMtMS4xNyAwLTIuMTMtLjk1LTIuMTMtMi4xMiAwLTEuMTcuOTYtMi4xMyAyLjEzLTIuMTMgMS4xOCAwIDIuMTIuOTYgMi4xMiAyLjEzcy0uOTUgMi4xMi0yLjEyIDIuMTJ6bTkgMGMtMS4xNyAwLTIuMTMtLjk1LTIuMTMtMi4xMiAwLTEuMTcuOTYtMi4xMyAyLjEzLTIuMTNzMi4xMi45NiAyLjEyIDIuMTMtLjk1IDIuMTItMi4xMiAyLjEyeiIvPgogICAgPHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgyNHYyNEgwVjB6Ii8+Cjwvc3ZnPgo=) no-repeat left center
}
\ No newline at end of file
import videojs from 'video.js';
import { version as VERSION } from '../package.json';
import 'babel-polyfill';
import WebXRPolyfill from 'webxr-polyfill';
import * as THREE from 'three';
import OrbitOrientationControls from './orbit-orientation-controls.js';
import CanvasPlayerControls from './canvas-player-controls';
import './big-vr-play-button';
import './cardboard-button';
const Plugin = videojs.getPlugin('plugin');
// Default options for the plugin.
const defaults = {};
class Xr extends Plugin {
/**
* Create a Xr plugin instance.
*
* @param {Player} player
* A Video.js Player instance.
*
* @param {Object} [options]
* An optional options object.
*
* While not a core part of the Video.js plugin architecture, a
* second argument of options is a convenient way to accept inputs
* from your plugin's caller.
*/
constructor(player, options) {
// the parent class will add player under this.player
super(player);
this.options = videojs.mergeOptions(defaults, options);
this.bigPlayButtonIndex_ = player.children().indexOf(player.getChild('BigPlayButton')) || 0;
if (!navigator.xr)
this.polyfill_ = new WebXRPolyfill();
this.handleVrDisplayActivate_ = videojs.bind(this, this.handleVrDisplayActivate_);
this.handleVrDisplayDeactivate_ = videojs.bind(this, this.handleVrDisplayDeactivate_);
this.onXRSessionEnd_ = videojs.bind(this, this.onXRSessionEnd_);
this.handleResize_ = videojs.bind(this, this.handleResize_);
this.animate_ = videojs.bind(this, this.animate_);
this.currentSession = null;
this.on(player, 'loadedmetadata', this.init);
this.player.ready(() => {
this.player.addClass('vjs-xr');
});
}
handleVrDisplayActivate_() {
if (!this.xrSupported)
return;
var self = this;
var sessionInit = { optionalFeatures: ['local-floor'] };
navigator.xr.requestSession('immersive-vr', sessionInit).then(function (session) {
self.renderer.xr.setSession(session);
session.addEventListener('end', self.onXRSessionEnd_);
self.xrActive = true;
self.currentSession = session;
session.requestReferenceSpace('local')
.then((referenceSpace) => {
self.xrReferenceSpace = referenceSpace;
})
self.controls3d.disable();
self.trigger('xrSessionActivated');
self.animationFrameId_ = self.requestAnimationFrame(self.animate_);
});
}
handleVrDisplayDeactivate_() {
this.currentSession.end();
}
onXRSessionEnd_() {
if (this.animationFrameId_) {
this.currentSession.cancelAnimationFrame(this.animationFrameId_);
this.animationFrameId_ = 0;
}
this.currentSession = null;
this.xrActive = false;
this.controls3d.enable();
this.trigger('xrSessionDeactivated');
this.animationFrameId_ = this.requestAnimationFrame(this.animate_);
}
requestAnimationFrame(fn) {
if (this.xrActive)
return this.currentSession.requestAnimationFrame(fn);
else
return this.player.requestAnimationFrame(fn);
}
cancelAnimationFrame(id) {
return this.player.cancelAnimationFrame(id);
}
togglePlay_() {
if (this.player.paused()) {
this.player.play();
} else {
this.player.pause();
}
}
animate_(xrTimestamp, xrFrame) {
if (!this.initialized_) {
return;
}
if (this.getVideoEl_().readyState === this.getVideoEl_().HAVE_ENOUGH_DATA) {
if (this.videoTexture) {
this.videoTexture.needsUpdate = true;
}
}
if (!this.xrActive)
this.controls3d.update();
if (this.xrActive && xrFrame) {
this.xrPose = xrFrame.getViewerPose(this.xrReferenceSpace);
this.trigger('xrCameraUpdate');
}
this.camera.getWorldDirection(this.cameraVector);
this.animationFrameId_ = this.requestAnimationFrame(this.animate_);
this.renderer.render(this.scene, this.camera);
}
handleResize_() {
const width = this.player.currentWidth();
const height = this.player.currentHeight();
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
}
init() {
this.reset();
this.xrSupported = false;
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();
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;
const position = { x: 0, y: 0, z: 0 };
if (this.scene) {
this.scene.remove(this.movieScreen);
}
// 180_LR projection
// Left eye
let geometryL = new THREE.SphereBufferGeometry(256, 32, 32, Math.PI, Math.PI);
geometryL.scale(-1, 1, 1);
let uvAttributeL = geometryL.getAttribute("uv");
let uvArrayL = uvAttributeL.array;
for (let i = 0; i < uvAttributeL.length; i += 2) {
uvArrayL[i + 0] *= 0.5;
}
/*
for (let i = 0; i < uvArrayL.length; i++) {
for (let j = 0; j < 3; j++) {
uvArrayL[i][j].x *= 0.5;
u
}
} */
uvAttributeL.needsUpdate = true;
//this.movieGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
this.movieGeometry = geometryL;
//this.movieGeometry = new THREE.SphereBufferGeometry(256, 32, 32, Math.PI/2, Math.PI*2, 0, Math.PI);
this.movieMaterial = new THREE.MeshBasicMaterial({
map: this.videoTexture,
});
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
let geometryR = new THREE.SphereBufferGeometry(256, 32, 32, Math.PI, Math.PI);
geometryR.scale(-1, 1, 1);
let uvAttributeR = geometryR.getAttribute("uv");
let uvArrayR = uvAttributeR.array;
for (let i = 0; i < uvAttributeR.length; i += 2) {
uvArrayR[i + 0] *= 0.5;
uvArrayR[i + 0] += 0.5;
}
/*
for (let _i = 0; _i < uvArrayR.length; _i++) {
for (let _j = 0; _j < 3; _j++) {
uvArrayR[_i][_j].x *= 0.5;
uvArrayR[_i][_j].x += 0.5;
}
} */
uvAttributeR.needsUpdate = true;
//this.movieGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
this.movieGeometry = geometryR;
//this.movieGeometry = new THREE.SphereBufferGeometry(256, 32, 32, Math.PI/2, Math.PI*2, 0, Math.PI);
this.movieMaterial = new THREE.MeshBasicMaterial({
map: this.videoTexture,
});
this.movieScreen = new THREE.Mesh(this.movieGeometry, this.movieMaterial); // display in right eye only
this.movieScreen.layers.set(2);
this.scene.add(this.movieScreen);
// 360 equirectangular projection
/*this.movieGeometry = new THREE.SphereBufferGeometry(256, 32, 32);
this.movieMaterial = new THREE.MeshBasicMaterial({ map: this.videoTexture, 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);
*/
this.player.removeChild('BigPlayButton');
this.player.addChild('BigVrPlayButton', {}, this.bigPlayButtonIndex_);
this.player.bigPlayButton = this.player.getChild('BigVrPlayButton');
this.camera.position.set(0, 0, 0);
this.renderer = new THREE.WebGLRenderer({
devicePixelRatio: window.devicePixelRatio,
alpha: false,
clearColor: 0xffffff,
antialias: true
});
this.renderer.setSize(this.player.currentWidth(), this.player.currentHeight());
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';
this.xrActive = false;
if (!this.controls3d) {
// self.controls3d = new OrbitControls(self.camera, self.renderedCanvas);
const options = {
camera: this.camera,
canvas: this.renderedCanvas,
// check if its a half sphere view projection
halfView: false,
orientation: false
};
this.controls3d = new OrbitOrientationControls(options);
this.canvasPlayerControls = new CanvasPlayerControls(this.player, this.renderedCanvas);
}
if (window.navigator.xr) {
this.renderer.xr.enabled = true;
// this.renderer.xr.setReferenceSpaceType('local');
var self = this;
navigator.xr.isSessionSupported('immersive-vr').then(function (supported) {
self.xrSupported = supported;
if (supported) {
self.addCardboardButton_();
console.log('webxr session supported');
} else {
console.log('web xr device not found, using orbit controls');
}
});
} else {
console.log('web xr not available');
}
self.completeInitialization(); // wait until controls are initialized
this.animationFrameId_ = this.requestAnimationFrame(this.animate_);
this.on(this.player, 'fullscreenchange', this.handleResize_);
window.addEventListener('vrdisplaypresentchange', this.handleResize_, true);
window.addEventListener('resize', this.handleResize_, true);
// these are triggered by the carboard button:
window.addEventListener('vrdisplayactivate', this.handleVrDisplayActivate_, true);
window.addEventListener('vrdisplaydeactivate', this.handleVrDisplayDeactivate_, true);
}
completeInitialization() {
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.controls3d) {
this.controls3d.dispose();
this.controls3d = null;
}
if (this.canvasPlayerControls) {
this.canvasPlayerControls.dispose();
this.canvasPlayerControls = 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');
}
// reset the video element style so that it will be displayed
const videoElStyle = this.getVideoEl_().style;
videoElStyle.zIndex = '';
videoElStyle.opacity = '';
// remove the old canvas
if (this.renderedCanvas) {
this.renderedCanvas.parentNode.removeChild(this.renderedCanvas);
}
if (this.animationFrameId_) {
this.cancelAnimationFrame(this.animationFrameId_);
}
this.initialized_ = false;
}
dispose() {
this.reset();
super.dispose();
}
polyfillVersion() {
return WebXRPolyfill.version;
}
}
// Define default values for the plugin's `state` object here.
Xr.defaultState = {};
// Include the version number.
Xr.VERSION = VERSION;
// Register the plugin with video.js.
videojs.registerPlugin('xr', Xr);
export default Xr;
import document from 'global/document';
import QUnit from 'qunit';
import sinon from 'sinon';
import videojs from 'video.js';
import plugin from '../src/plugin';
const Player = videojs.getComponent('Player');
QUnit.test('the environment is sane', function(assert) {
assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists');
assert.strictEqual(typeof sinon, 'object', 'sinon exists');
assert.strictEqual(typeof videojs, 'function', 'videojs exists');
assert.strictEqual(typeof plugin, 'function', 'plugin is a function');
});
QUnit.module('videojs-xr', {
beforeEach() {
// Mock the environment's timers because certain things - particularly
// player readiness - are asynchronous in video.js 5. This MUST come
// before any player is created; otherwise, timers could get created
// with the actual timer methods!
this.clock = sinon.useFakeTimers();
this.fixture = document.getElementById('qunit-fixture');
this.video = document.createElement('video');
this.fixture.appendChild(this.video);
this.player = videojs(this.video);
},
afterEach() {
this.player.dispose();
this.clock.restore();
}
});
QUnit.test('registers itself with video.js', function(assert) {
assert.expect(2);
assert.strictEqual(
typeof Player.prototype.xr,
'function',
'videojs-xr plugin was registered'
);
this.player.xr();
// Tick the clock forward enough to trigger the player to be "ready".
this.clock.tick(1);
assert.ok(
this.player.hasClass('vjs-xr'),
'the plugin adds a class to the player'
);
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment