import * as THREE from 'three';
import { EVENTS } from '../Constants';
/**
* @classdesc User Media
* @constructor
* @param {object} [constraints={ video: { width: { ideal: 1920 }, height: { ideal: 1080 }, facingMode: { exact: 'environment' } }, audio: false }]
*/
function Media ( constraints ) {
const defaultConstraints = { video: { width: { ideal: 1920 }, height: { ideal: 1080 }, facingMode: { exact: 'environment' } }, audio: false };
this.constraints = Object.assign( defaultConstraints, constraints );
this.container = null;
this.scene = null;
this.element = null;
this.devices = [];
this.stream = null;
this.ratioScalar = 1;
this.videoDeviceIndex = 0;
};
Media.prototype = Object.assign( Object.create( THREE.EventDispatcher.prototype ), {
setContainer: function ( container ) {
this.container = container;
},
setScene: function ( scene ) {
this.scene = scene;
},
/**
* Enumerate devices
* @memberOf Media
* @instance
* @returns {Promise}
*/
enumerateDevices: function () {
const devices = this.devices;
const resolvedPromise = new Promise( resolve => { resolve( devices ); } );
return devices.length > 0 ? resolvedPromise : window.navigator.mediaDevices.enumerateDevices();
},
/**
* Switch to next available video device
* @memberOf Media
* @instance
*/
switchNextVideoDevice: function () {
const stop = this.stop.bind( this );
const start = this.start.bind( this );
const setVideDeviceIndex = this.setVideDeviceIndex.bind( this );
let index = this.videoDeviceIndex;
this.getDevices( 'video' )
.then( devices => {
stop();
index++;
if ( index >= devices.length ) {
setVideDeviceIndex( 0 );
index--;
} else {
setVideDeviceIndex( index );
}
start( devices[ index ] );
} );
},
/**
* Get devices
* @param {string} type - type keyword to match device.kind
* @memberOf Media
* @instance
*/
getDevices: function ( type = 'video' ) {
const devices = this.devices;
const validate = _devices => {
return _devices.map( device => {
if ( !devices.includes( device ) ) { devices.push( device ); }
return device;
} );
};
const filter = _devices => {
const reg = new RegExp( type, 'i' );
return _devices.filter( device => reg.test( device.kind ) );
};
return this.enumerateDevices()
.then( validate )
.then( filter );
},
/**
* Get user media
* @param {MediaStreamConstraints} constraints
* @memberOf Media
* @instance
*/
getUserMedia: function ( constraints ) {
const setMediaStream = this.setMediaStream.bind( this );
const playVideo = this.playVideo.bind( this );
const onCatchError = error => { console.warn( `PANOLENS.Media: ${error}` ); };
return window.navigator.mediaDevices.getUserMedia( constraints )
.then( setMediaStream )
.then( playVideo )
.catch( onCatchError );
},
/**
* Set video device index
* @param {number} index
* @memberOf Media
* @instance
*/
setVideDeviceIndex: function ( index ) {
this.videoDeviceIndex = index;
},
/**
* Start streaming
* @param {MediaDeviceInfo} [targetDevice]
* @memberOf Media
* @instance
*/
start: function( targetDevice ) {
const constraints = this.constraints;
const getUserMedia = this.getUserMedia.bind( this );
const onVideoDevices = devices => {
if ( !devices || devices.length === 0 ) {
throw Error( 'no video device found' );
}
const device = targetDevice || devices[ 0 ];
constraints.video.deviceId = device.deviceId;
return getUserMedia( constraints );
};
this.element = this.createVideoElement();
return this.getDevices().then( onVideoDevices );
},
/**
* Stop streaming
* @memberOf Media
* @instance
*/
stop: function () {
const stream = this.stream;
if ( stream && stream.active ) {
const track = stream.getTracks()[ 0 ];
track.stop();
window.removeEventListener( 'resize', this.onWindowResize.bind( this ) );
this.element = null;
this.stream = null;
}
},
/**
* Set media stream
* @param {MediaStream} stream
* @memberOf Media
* @instance
*/
setMediaStream: function ( stream ) {
this.stream = stream;
this.element.srcObject = stream;
if ( this.scene ) {
this.scene.background = this.createVideoTexture();
}
window.addEventListener( 'resize', this.onWindowResize.bind( this ) );
},
/**
* Play video element
* @memberOf Media
* @instance
*/
playVideo: function () {
const { element } = this;
if ( element ) {
element.play();
this.dispatchEvent( { type: EVENTS.MEDIA.PLAY } );
}
},
/**
* Pause video element
* @memberOf Media
* @instance
*/
pauseVideo: function () {
const { element } = this;
if ( element ) {
element.pause();
this.dispatchEvent( { type: EVENTS.MEDIA.PAUSE } );
}
},
/**
* Create video texture
* @memberOf Media
* @instance
* @returns {THREE.VideoTexture}
*/
createVideoTexture: function () {
const video = this.element;
const texture = new THREE.VideoTexture( video );
texture.generateMipmaps = false;
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.format = THREE.RGBFormat;
texture.center.set( 0.5, 0.5 );
video.addEventListener( 'canplay', this.onWindowResize.bind( this ) );
return texture;
},
/**
* Create video element
* @memberOf Media
* @instance
* @returns {HTMLVideoElement}
* @fires Media#canplay
*/
createVideoElement: function() {
const dispatchEvent = this.dispatchEvent.bind( this );
const video = document.createElement( 'video' );
/**
* Video can play event
* @type {object}
* @event Media#canplay
*/
const canPlay = () => dispatchEvent( { type: 'canplay' } );
video.setAttribute( 'autoplay', '' );
video.setAttribute( 'muted', '' );
video.setAttribute( 'playsinline', '' );
video.style.position = 'absolute';
video.style.top = '0';
video.style.left = '0';
video.style.width = '100%';
video.style.height = '100%';
video.style.objectPosition = 'center';
video.style.objectFit = 'cover';
video.style.display = this.scene ? 'none' : '';
video.addEventListener( 'canplay', canPlay );
return video;
},
/**
* On window resize event
* @param {Event} event
* @memberOf Media
* @instance
*/
onWindowResize: function () {
if ( this.element && this.element.videoWidth && this.element.videoHeight && this.scene ) {
const { clientWidth: width, clientHeight: height } = this.container;
const texture = this.scene.background;
const { videoWidth, videoHeight } = this.element;
const cameraRatio = videoHeight / videoWidth;
const viewportRatio = this.container ? width / height : 1.0;
const ratio = cameraRatio * viewportRatio * this.ratioScalar;
if ( width > height ) {
texture.repeat.set( ratio, 1 );
} else {
texture.repeat.set( 1, 1 / ratio );
}
}
}
} );
export { Media };