UNPKG

playable

Version:

Video player based on HTML5Video

182 lines (158 loc) 4.6 kB
import { IEventEmitter } from '../../../event-emitter/types'; import { isSafari } from '../../../../utils/device-detection'; import { VideoEvent, EngineState } from '../../../../constants'; export const NATIVE_VIDEO_EVENTS_TO_STATE = [ 'loadstart', 'loadedmetadata', 'canplay', 'play', 'playing', 'pause', 'ended', 'waiting', 'seeking', 'seeked', ]; export default class StateEngine { private _eventEmitter: IEventEmitter; private _video: HTMLVideoElement; private _currentState: EngineState; private _statesTimestamps: { [state: string]: number }; private _initialTimeStamp: number; private _isMetadataLoaded: boolean; constructor(eventEmitter: IEventEmitter, video: HTMLVideoElement) { this._eventEmitter = eventEmitter; this._video = video; this._currentState = null; this._isMetadataLoaded = false; this._statesTimestamps = {}; this._bindCallbacks(); this._bindEvents(); } private _bindCallbacks() { this._processEventFromVideo = this._processEventFromVideo.bind(this); } private _bindEvents() { NATIVE_VIDEO_EVENTS_TO_STATE.forEach(event => { this._video.addEventListener(event, this._processEventFromVideo); }); } private _unbindEvents() { NATIVE_VIDEO_EVENTS_TO_STATE.forEach(event => this._video.removeEventListener(event, this._processEventFromVideo), ); } clearTimestamps() { this._statesTimestamps = {}; } private _setInitialTimeStamp() { this._initialTimeStamp = Date.now(); } private _setStateTimestamp(state: EngineState) { if (!this._statesTimestamps[state]) { this._statesTimestamps[state] = Date.now() - this._initialTimeStamp; this._setInitialTimeStamp(); } } get stateTimestamps() { return this._statesTimestamps; } private _processEventFromVideo(event: any = {}) { const videoElement = this._video; switch (event.type) { case 'loadstart': { this._setInitialTimeStamp(); this.setState(EngineState.LOAD_STARTED); break; } case 'loadedmetadata': { this._setStateTimestamp(EngineState.METADATA_LOADED); this.setState(EngineState.METADATA_LOADED); this._isMetadataLoaded = true; break; } case 'canplay': { if (this._currentState === EngineState.METADATA_LOADED) { this._setStateTimestamp(EngineState.READY_TO_PLAY); this.setState(EngineState.READY_TO_PLAY); } break; } case 'play': { this.setState(EngineState.PLAY_REQUESTED); break; } case 'playing': { // Safari triggers event 'playing' even when play request aborted by browser. So we need to check if video is actualy playing if (isSafari()) { if (!videoElement.paused) { this.setState(EngineState.PLAYING); } } else { this.setState(EngineState.PLAYING); } break; } case 'waiting': { this.setState(EngineState.WAITING); break; } case 'pause': { // Safari triggers event 'pause' even when playing was aborted buy autoplay policies, emit pause event even if there wasn't any real playback if (isSafari()) { if (videoElement.played.length) { this.setState(EngineState.PAUSED); } } else { this.setState(EngineState.PAUSED); } break; } case 'ended': { this.setState(EngineState.ENDED); break; } case 'seeking': { this.setState(EngineState.SEEK_IN_PROGRESS); break; } case 'seeked': { this.setState( videoElement.paused ? EngineState.PAUSED : EngineState.PLAYING, ); break; } default: break; } } setState(state: EngineState) { if (state === this._currentState) { return; } //This case is happens only with dash.js sometimes when manifest got some problems if (this._currentState === EngineState.METADATA_LOADED) { if ( state === EngineState.SEEK_IN_PROGRESS || state === EngineState.PAUSED ) { return; } } this._eventEmitter.emitAsync(VideoEvent.STATE_CHANGED, { prevState: this._currentState, nextState: state, }); this._eventEmitter.emitAsync(state); this._currentState = state; } get isMetadataLoaded() { return this._isMetadataLoaded; } get state() { return this._currentState; } destroy() { this._unbindEvents(); } }