playable
Version:
Video player based on HTML5Video
182 lines (158 loc) • 4.6 kB
text/typescript
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();
}
}