@playkit-js/playkit-js
Version:
[](https://github.com/kaltura/playkit-js/actions/workflows/run_canary.yaml) [ • 7.93 kB
text/typescript
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<State>}
* @private
*/
private _history: Array<State>;
/**
* The possible transitions from one state to another.
* @type {Array<Transition>}
* @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.<State>} - The full states history objects
*/
public get history(): State[] {
return this._history;
}
}