UNPKG

photo-sphere-viewer

Version:

A JavaScript library to display Photo Sphere panoramas

472 lines (404 loc) 11.6 kB
import { SplineCurve, Vector2 } from 'three'; import { AbstractPlugin, CONSTANTS, DEFAULTS, PSVError, registerButton, utils } from '../..'; import { EVENTS } from './constants'; import { PauseOverlay } from './PauseOverlay'; import { PlayPauseButton } from './PlayPauseButton'; import { ProgressBar } from './ProgressBar'; import { TimeCaption } from './TimeCaption'; import { VolumeButton } from './VolumeButton'; import './style.scss'; /** * @typedef {Object} PSV.plugins.VideoPlugin.Keypoint * @property {PSV.ExtendedPosition} position * @property {number} time */ /** * @typedef {Object} PSV.plugins.VideoPlugin.Options * @property {boolean} [progressbar=true] - displays a progressbar on top of the navbar * @property {boolean} [bigbutton=true] - displays a big "play" button in the center of the viewer * @property {PSV.plugins.VideoPlugin.Keypoint[]} [keypoints] - defines autorotate timed keypoints */ // add video buttons DEFAULTS.lang[PlayPauseButton.id] = 'Play/Pause'; DEFAULTS.lang[VolumeButton.id] = 'Volume'; registerButton(PlayPauseButton); registerButton(VolumeButton); registerButton(TimeCaption); DEFAULTS.navbar.unshift(PlayPauseButton.groupId); export { EVENTS } from './constants'; /** * @summary Controls a video adapter * @extends PSV.plugins.AbstractPlugin * @memberof PSV.plugins */ export class VideoPlugin extends AbstractPlugin { static id = 'video'; static EVENTS = EVENTS; /** * @param {PSV.Viewer} psv * @param {PSV.plugins.VideoPlugin.Options} options */ constructor(psv, options) { super(psv); if (!this.psv.adapter.constructor.id.includes('video')) { throw new PSVError('VideoPlugin can only be used with a video adapter.'); } /** * @member {Object} * @property {THREE.SplineCurve} curve * @property {PSV.plugins.VideoPlugin.Keypoint} start * @property {PSV.plugins.VideoPlugin.Keypoint} end * @property {PSV.plugins.VideoPlugin.Keypoint[]} keypoints * @private */ this.autorotate = { curve : null, start : null, end : null, keypoints: null, }; /** * @member {PSV.plugins.VideoPlugin.Options} * @private */ this.config = { progressbar: true, bigbutton : true, ...options, }; if (this.config.progressbar) { this.progressbar = new ProgressBar(this); } if (this.config.bigbutton) { this.overlay = new PauseOverlay(this); } /** * @type {PSV.plugins.MarkersPlugin} * @private */ this.markers = null; } /** * @package */ init() { super.init(); this.markers = this.psv.getPlugin('markers'); if (this.config.keypoints) { this.setKeypoints(this.config.keypoints); delete this.config.keypoints; } this.psv.on(CONSTANTS.EVENTS.AUTOROTATE, this); this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this); this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); this.psv.on(CONSTANTS.EVENTS.KEY_PRESS, this); } /** * @package */ destroy() { this.psv.off(CONSTANTS.EVENTS.AUTOROTATE, this); this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this); this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); this.psv.off(CONSTANTS.EVENTS.KEY_PRESS, this); delete this.autorotate; delete this.progressbar; delete this.overlay; super.destroy(); } /** * @private */ handleEvent(e) { /* eslint-disable */ // @formatter:off switch (e.type) { case CONSTANTS.EVENTS.BEFORE_RENDER: this.__autorotate(); break; case CONSTANTS.EVENTS.AUTOROTATE: this.__configureAutorotate(); break; case CONSTANTS.EVENTS.PANORAMA_LOADED: this.__bindVideo(e.args[0]); this.progressbar?.show(); break; case CONSTANTS.EVENTS.KEY_PRESS: this.__onKeyPress(e, e.args[0]); break; case 'play': this.trigger(EVENTS.PLAY); break; case 'pause': this.trigger(EVENTS.PAUSE); break; case 'progress': this.trigger(EVENTS.BUFFER, this.getBufferProgress()); break; case 'volumechange': this.trigger(EVENTS.VOLUME_CHANGE, this.getVolume()); break; case 'timeupdate': this.trigger(EVENTS.PROGRESS, { time : this.getTime(), duration: this.getDuration(), progress: this.getProgress(), }); break; } // @formatter:on /* eslint-enable */ } /** * @private */ __bindVideo(textureData) { this.video = textureData.texture.image; this.video.addEventListener('play', this); this.video.addEventListener('pause', this); this.video.addEventListener('progress', this); this.video.addEventListener('volumechange', this); this.video.addEventListener('timeupdate', this); } /** * @private */ __onKeyPress(e, key) { if (key === CONSTANTS.KEY_CODES.Space) { this.playPause(); e.preventDefault(); } } /** * @summary Returns the durection of the video * @returns {number} */ getDuration() { return this.video?.duration ?? 0; } /** * @summary Returns the current time of the video * @returns {number} */ getTime() { return this.video?.currentTime ?? 0; } /** * @summary Returns the play progression of the video * @returns {number} 0-1 */ getProgress() { return this.video ? this.video.currentTime / this.video.duration : 0; } /** * @summary Returns if the video is playing * @returns {boolean} */ isPlaying() { return this.video ? !this.video.paused : false; } /** * @summary Returns the video volume * @returns {number} */ getVolume() { return this.video?.muted ? 0 : this.video?.volume ?? 0; } /** * @summary Starts or pause the video */ playPause() { if (this.video) { if (this.video.paused) { this.video.play(); } else { this.video.pause(); } } } /** * @summary Starts the video if paused */ play() { if (this.video && this.video.paused) { this.video.play(); } } /** * @summary Pauses the cideo if playing */ pause() { if (this.video && !this.video.paused) { this.video.pause(); } } /** * @summary Sets the volume of the video * @param {number} volume */ setVolume(volume) { if (this.video) { this.video.muted = false; this.video.volume = volume; } } /** * @summary (Un)mutes the video * @param {boolean} [mute] - toggle if undefined */ setMute(mute) { if (this.video) { this.video.muted = mute === undefined ? !this.video.muted : mute; if (!this.video.muted && this.video.volume === 0) { this.video.volume = 0.1; } } } /** * @summary Changes the current time of the video * @param {number} time */ setTime(time) { if (this.video) { this.video.currentTime = time; } } /** * @summary Changes the progression of the video * @param {number} progress 0-1 */ setProgress(progress) { if (this.video) { this.video.currentTime = this.video.duration * progress; } } getBufferProgress() { if (this.video) { let maxBuffer = 0; const buffer = this.video.buffered; for (let i = 0, l = buffer.length; i < l; i++) { if (buffer.start(i) <= this.video.currentTime && buffer.end(i) >= this.video.currentTime) { maxBuffer = buffer.end(i); break; } } return Math.max(this.video.currentTime, maxBuffer) / this.video.duration; } else { return 0; } } /** * @summary Changes the keypoints * @param {PSV.plugins.VideoPlugin.Keypoint[]} keypoints */ setKeypoints(keypoints) { if (keypoints && keypoints.length < 2) { throw new PSVError('At least two points are required'); } this.autorotate.keypoints = utils.clone(keypoints); if (this.autorotate.keypoints) { this.autorotate.keypoints.forEach((pt, i) => { if (pt.position) { const position = this.psv.dataHelper.cleanPosition(pt.position); pt.position = [position.longitude, position.latitude]; } else { throw new PSVError(`Keypoint #${i} is missing marker or position`); } if (utils.isNil(pt.time)) { throw new PSVError(`Keypoint #${i} is missing time`); } }); this.autorotate.keypoints.sort((a, b) => a.time - b.time); } this.__configureAutorotate(); } /** * @private */ __configureAutorotate() { delete this.autorotate.curve; delete this.autorotate.start; delete this.autorotate.end; if (this.psv.isAutorotateEnabled() && this.autorotate.keypoints) { // cancel core rotation this.psv.dynamics.position.stop(); } } /** * @private */ __autorotate() { if (!this.psv.isAutorotateEnabled() || !this.autorotate.keypoints) { return; } const currentTime = this.getTime(); const autorotate = this.autorotate; if (!autorotate.curve || currentTime < autorotate.start.time || currentTime >= autorotate.end.time) { this.__autorotateNext(currentTime); } if (autorotate.start === autorotate.end) { this.psv.rotate({ longitude: autorotate.start.position[0], latitude : autorotate.start.position[1], }); } else { const progress = (currentTime - autorotate.start.time) / (autorotate.end.time - autorotate.start.time); // only the middle segment contains the current section const pt = autorotate.curve.getPoint(1 / 3 + progress / 3); this.psv.dynamics.position.goto({ longitude: pt.x, latitude : pt.y, }); } } /** * @private */ __autorotateNext(currentTime) { let k1 = null; let k2 = null; const keypoints = this.autorotate.keypoints; const l = keypoints.length - 1; if (currentTime < keypoints[0].time) { k1 = 0; k2 = 0; } for (let i = 0; i < l; i++) { if (currentTime >= keypoints[i].time && currentTime < keypoints[i + 1].time) { k1 = i; k2 = i + 1; break; } } if (currentTime >= keypoints[l].time) { k1 = l; k2 = l; } // get the 4 points necessary to compute the current movement // one point before and two points after the current const workPoints = [ keypoints[Math.max(0, k1 - 1)].position, keypoints[k1].position, keypoints[k2].position, keypoints[Math.min(l, k2 + 1)].position, ]; // apply offsets to avoid crossing the origin const workVectors = [new Vector2(workPoints[0][0], workPoints[0][1])]; let k = 0; for (let i = 1; i <= 3; i++) { const d = workPoints[i - 1][0] - workPoints[i][0]; if (d > Math.PI) { // crossed the origin left to right k += 1; } else if (d < -Math.PI) { // crossed the origin right to left k -= 1; } if (k !== 0 && i === 1) { // do not modify first point, apply the reverse offset the the previous point instead workVectors[0].x -= k * 2 * Math.PI; k = 0; } workVectors.push(new Vector2(workPoints[i][0] + k * 2 * Math.PI, workPoints[i][1])); } this.autorotate.curve = new SplineCurve(workVectors); this.autorotate.start = keypoints[k1]; this.autorotate.end = keypoints[k2]; // debugCurve(this.markers, this.autorotate.curve.getPoints(16 * 3).map(p => ([p.x, p.y])), 16); } }