UNPKG

panolens

Version:
349 lines (247 loc) 8.26 kB
import * as THREE from 'three'; /** * @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: 'play' } ); } }, /** * Pause video element * @memberOf Media * @instance */ pauseVideo: function () { const { element } = this; if ( element ) { element.pause(); this.dispatchEvent( { type: '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 };