import Player from '../player'; import { EventManager } from '../event/event-manager'; import State from './state'; import {StateType} from './state-type'; import {CustomEventType, Html5EventType} from '../event/event-type'; import { FakeEvent } from '../event/fake-event'; import getLogger from '../utils/logger'; import Env from '../utils/env'; import Error from '../error/error'; import {StateChanged, Transition} from '../types'; /** * This class responsible to manage all the state machine of the player. * @classdesc */ export default class StateManager { /** * The logger of the class. * @member * @type {any} * @private */ private _logger: any; /** * Reference to the actual player. * @member * @type {Player} * @private */ private _player: Player; /** * The event manager of the class. * @member * @type {EventManager} * @private */ private _eventManager: EventManager; /** * Holds the current state of the player. * @member * @type {State} * @private */ private _curState: State; /** * Holds the previous state of the player. * @member * @type {State | null} * @private */ private _prevState: State | null; /** * Holds the time of the beginning of the last buffering (waiting event) * @member * @type {number | null} * @private */ private _lastWaitingTime?: number | null; /** * Holds the state history of the player. * @member * @type {Array} * @private */ private _history: Array; /** * The possible transitions from one state to another. * @type {Array} * @private */ private _transitions: Transition = { [StateType.IDLE]: { [Html5EventType.LOAD_START]: () => this._updateState(StateType.LOADING), [Html5EventType.PLAY]: () => this._updateState(StateType.BUFFERING), [Html5EventType.SEEKED]: () => this._updateState(StateType.PAUSED) }, [StateType.LOADING]: { [Html5EventType.LOADED_METADATA]: () => this._updateState(StateType.PAUSED), [Html5EventType.ERROR]: e => e.payload.severity === Error.Severity.CRITICAL && this._updateState(StateType.IDLE), [Html5EventType.SEEKED]: () => { if (this._prevState && this._prevState.type === StateType.PLAYING) { this._updateState(StateType.PLAYING); } } }, [StateType.PAUSED]: { [Html5EventType.PLAY]: () => this._updateState(StateType.PLAYING), [Html5EventType.PLAYING]: () => this._updateState(StateType.PLAYING), [Html5EventType.ENDED]: () => this._updateState(StateType.IDLE) }, [StateType.PLAYING]: { [Html5EventType.PAUSE]: () => this._updateState(StateType.PAUSED), [Html5EventType.WAITING]: () => { if (this._player.seeking) { this._updateState(StateType.LOADING); } else { this._updateState(StateType.BUFFERING); this._lastWaitingTime = this._player.currentTime; } }, [Html5EventType.ENDED]: () => this._updateState(StateType.IDLE), [Html5EventType.ERROR]: e => e.payload.severity === Error.Severity.CRITICAL && this._updateState(StateType.IDLE) }, [StateType.BUFFERING]: { [Html5EventType.PLAYING]: () => this._updateState(StateType.PLAYING), [Html5EventType.PAUSE]: () => this._updateState(StateType.PAUSED), [Html5EventType.TIME_UPDATE]: () => { if ( Env.browser.name === 'IE' && this._player.currentTime !== this._lastWaitingTime && this._prevState && this._prevState.type === StateType.PLAYING ) { this._lastWaitingTime = null; this._updateState(StateType.PLAYING); } } } }; /** * @constructor * @param {Player} player - Reference to the player. */ // constructor(player: Player) { constructor(player: any) { this._player = player; this._logger = getLogger('StateManager'); this._eventManager = new EventManager(); this._history = []; this._prevState = null; this._curState = new State(StateType.IDLE); this._attachListeners(); } /** * Register to all necessary events which impacts on the player state. * @private * @returns {void} */ private _attachListeners(): void { this._eventManager.listen(this._player, Html5EventType.ERROR, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.ENDED, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.PLAY, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.LOAD_START, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.PLAYING, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.LOADED_METADATA, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.PAUSE, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.WAITING, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.SEEKED, this._doTransition.bind(this)); this._eventManager.listen(this._player, Html5EventType.TIME_UPDATE, this._doTransition.bind(this)); } /** * Performs a state transition depends on the event which occurs in the player system. * @param {FakeEvent} event - The event occurs in the player system. * @private * @returns {void} */ private _doTransition(event: FakeEvent): void { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (event.type !== Html5EventType.TIME_UPDATE || (this._curState === StateType.BUFFERING && event.type === Html5EventType.TIME_UPDATE)) { this._logger.debug('Do transition request', event.type); // don't show most of 'timeupdate' events } const transition = this._transitions[this._curState.type]; if (typeof transition[event.type] === 'function') { transition[event.type](event); } } /** * Updates the player's state. * @param {string} type - The type of the new state. * @private * @returns {void} */ private _updateState(type: string): void { if (this._curState.type !== type) { this._curState.duration = Date.now() / 1000; this._history.push(this._curState); this._prevState = this._curState; this._curState = new State(type); this._logger.debug(`Switch player state: from ${this._prevState.type} to ${this._curState.type}`); this._dispatchEvent(); } } /** * Fires the playerStateChanged event after state has been changed. * @private * @returns {void} */ private _dispatchEvent(): void { const event = new FakeEvent( CustomEventType.PLAYER_STATE_CHANGED, ({ oldState: this._prevState, newState: this._curState } as StateChanged) ); this._player.dispatchEvent(event); } /** * Destroys the state manager. * @public * @returns {void} */ public destroy(): void { this._history = []; this._eventManager.destroy(); } /** * Resets the state manager. * @public * @returns {void} */ public reset(): void { this._history = []; } /** * Getter to the current state of the player. * @public * @returns {State} - The current state object */ public get currentState(): State { return this._curState; } /** * Getter to the previous state of the player. * @public * @returns {State|null} - The previous state object, or null if such doesn't exists */ public get previousState(): State | null { return this._prevState; } /** * Getter to the state history of the player. * @public * @returns {Array.} - The full states history objects */ public get history(): State[] { return this._history; } }