@playkit-js/playkit-js
Version:
[](https://github.com/kaltura/playkit-js/actions/workflows/run_canary.yaml) [ • 96.3 kB
text/typescript
import Env from './utils/env';
import { EventManager } from './event/event-manager';
import PosterManager from './utils/poster-manager';
import { FakeEvent } from './event/fake-event';
import { FakeEventTarget } from './event/fake-event-target';
import { IEngine, IEngineStatic, LoggerLevels } from './types';
import { CustomEventType, EventType, Html5EventType } from './event/event-type';
import * as Utils from './utils/util';
import Locale from './utils/locale';
import getLogger, { getLogLevel, LogLevel, LogLevelType, setLogHandler, setLogLevel } from './utils/logger';
import StateManager from './state/state-manager';
import Track from './track/track';
import VideoTrack from './track/video-track';
import AudioTrack, { audioDescriptionTrackHandler, updateUnknownAudioLanguageTracks } from './track/audio-track';
import { PKTextTrack } from './track/text-track';
import TextStyle from './track/text-style';
import { processCues } from './track/text-track-display';
import { StateType } from './state/state-type';
import { TrackType, TrackTypes } from './track/track-type';
import { StreamType } from './engines/stream-type';
import { EngineType } from './engines/engine-type';
import { MediaType } from './enums/media-type';
import { AbrMode } from './track/abr-mode-type';
import { CorsType } from './engines/html5/cors-types';
import PlaybackMiddleware from './middleware/playback-middleware';
import { DefaultConfig, DefaultSources } from './player-config.js';
import './assets/style.css';
import PKError from './error/error';
import { EngineProvider } from './engines/engine-provider';
import { ExternalCaptionsHandler } from './track/external-captions-handler';
import { AdBreakType } from './ads/ad-break-type';
import { AdTagType } from './ads/ad-tag-type';
import { ResizeWatcher, getSubtitleStyleSheet, resetSubtitleStyleSheet } from './utils';
import { FullscreenController } from './fullscreen/fullscreen-controller';
import { EngineDecorator, EngineDecoratorType } from './engines/engine-decorator';
import { LabelOptions } from './track/label-options';
import { AutoPlayType } from './enums/auto-play-type';
import ImageTrack from './track/image-track';
import { ThumbnailInfo } from './thumbnail/thumbnail-info';
import { EngineDecoratorManager } from './engines/engine-decorator-manager';
import { filterTracksByRestriction } from './utils/restrictions';
import { ExternalThumbnailsHandler } from './thumbnail/external-thumbnails-handler';
import {
IEngineDecoratorProvider,
PKDimensionsConfig,
PKDrmDataObject,
PKEventTypes,
PKMediaSourceObject,
PKMetadataConfigObject,
PKPlayerDimensions,
PKPlayOptionsObject,
PKSourcesConfigObject,
PKStatsObject,
PKTextTrackDisplaySettingObject
} from './types';
import { ILogger, ILogLevel } from 'js-logger';
import { IEnv } from './types/ua-parser';
/**
* The black cover class name.
* @type {string}
* @const
*/
const BLACK_COVER_CLASS_NAME: string = 'playkit-black-cover';
/**
* The player container class name.
* @type {string}
* @const
*/
const CONTAINER_CLASS_NAME: string = 'playkit-container';
/**
/**
* The player poster class name.
* @type {string}
* @const
*/
const POSTER_CLASS_NAME: string = 'playkit-poster';
/**
* The video filter class name.
* @type {string}
* @const
*/
const VIDEO_FILTER_CLASS_NAME: string = 'playkit-video-filter';
/**
* The engine class name.
* @type {string}
* @const
*/
const ENGINE_CLASS_NAME: string = 'playkit-engine';
/**
* The subtitles class name.
* @type {string}
* @const
*/
const SUBTITLES_CLASS_NAME: string = 'playkit-subtitles';
/**
* The auto string, for captions
* @type {string}
* @const
*/
const AUTO: string = 'auto';
/**
* The off string, for captions
* @type {string}
* @const
*/
const OFF: string = 'off';
/**
* The duration offset, for seeking to duration safety.
* @type {number}
* @const
*/
const DURATION_OFFSET: number = 0.1;
/**
* The threshold in seconds from duration that we still consider it as live edge
* @type {number}
* @const
*/
const LIVE_EDGE_THRESHOLD: number = 1;
/**
* The audio description prefix string, for audio description tracks
* @type {string}
* @const
*/
const AUDIO_DESCRIPTION_PREFIX: string = 'ad-';
/**
* The HTML5 player class.
* @classdesc
*/
export default class Player extends FakeEventTarget {
/**
* The player class logger.
* @type {any}
* @static
* @private
*/
public static _logger: ILogger = getLogger('Player');
/**
* Runs the engines capabilities tests.
* @returns {void}
* @public
* @static
*/
public static runCapabilities(): void {
Player._logger.debug('Running player capabilities');
EngineProvider.getEngines().forEach((Engine) => Engine.runCapabilities());
}
/**
* Gets the engines capabilities.
* @param {?string} engineType - The engine type.
* @return {Promise<Object>} - The engines capabilities object.
* @public
* @static
*/
public static getCapabilities(engineType?: string): Promise<{ [name: string]: any }> {
Player._logger.debug('Get player capabilities', engineType);
const promises: { [name: string]: any }[] = [];
EngineProvider.getEngines().forEach((Engine) => promises.push(Engine.getCapabilities()));
return Promise.all(promises).then((arrayOfResults) => {
const playerCapabilities = {};
arrayOfResults.forEach((res) => Object.assign(playerCapabilities, res));
return engineType ? playerCapabilities[engineType] : playerCapabilities;
});
}
/**
* Sets an engine capabilities.
* @param {string} engineType - The engine type.
* @param {Object} capabilities - The engine capabilities.
* @returns {void}
* @public
* @static
*/
public static setCapabilities(engineType: string, capabilities: { [name: string]: any }): void {
Player._logger.debug('Set player capabilities', engineType, capabilities);
const selectedEngine = EngineProvider.getEngines().find((Engine) => Engine.id === engineType);
if (selectedEngine) {
selectedEngine.setCapabilities(capabilities);
}
}
/**
* The event manager of the player.
* @type {EventManager}
* @private
*/
private _eventManager!: EventManager;
/**
* The poster manager of the player.
* @type {PosterManager}
* @private
*/
private _posterManager!: PosterManager;
/**
* The runtime configuration of the player.
* @type {Object}
* @private
*/
private _config: any;
/**
* The current sources object.
* @type {PKSourcesConfigObject}
* @private
*/
private _sources: PKSourcesConfigObject = {} as PKSourcesConfigObject;
/**
* The playback engine.
* @type {IEngine}
* @private
*/
private _engine!: IEngine;
/**
* The state manager of the player.
* @type {StateManager}
* @private
*/
private _stateManager!: StateManager;
/**
* The tracks of the player.
* @type {Array<Track | TextTrack | AudioTrack | VideoTrack>}
* @private
*/
private _tracks: Array<Track | PKTextTrack | AudioTrack | VideoTrack>;
/**
* The player ready promise
* @type {Promise<*>}
* @private
*/
private _readyPromise?: Promise<void>;
/**
* Whether the play is the first or not
* @type {boolean}
* @private
*/
private _firstPlay!: boolean;
/**
* Whether the playing is the first or not
* @type {boolean}
* @private
*/
private _firstPlaying!: boolean;
/**
* Whether the playback already start
* @type {boolean}
* @private
*/
private _playbackStart!: boolean;
/**
* If quality has changed after playback ended - pend the change
* @type {boolean}
* @private
*/
private _pendingSelectedVideoTrack: VideoTrack | undefined;
/**
* The available playback rates for the player.
* @type {Array<number>}
* @private
*/
private _playbackRates!: Array<number>;
/**
* The default playback rate for the player.
* @type {number}
* @private
*/
private _defaultPlaybackRate!: number;
/**
* The player DOM element container.
* @type {HTMLDivElement}
* @private
*/
private _el!: HTMLDivElement;
/**
* The player text DOM element container.
* @type {HTMLDivElement}
* @private
*/
private _textDisplayEl!: HTMLDivElement;
/**
* The player black cover div.
* @type {HTMLDivElement}
* @private
*/
private _blackCoverEl!: HTMLDivElement;
/**
* The player DOM id.
* @type {string}
* @private
*/
private _playerId!: string;
/**
* The player last updated text cues list
* @type {Array<any>}
* @private
*/
private _activeTextCues: VTTCue[] = [];
/**
* The player text disaply settings
* @type {PKTextTrackDisplaySettingObject}
* @private
*/
private _textDisplaySettings: PKTextTrackDisplaySettingObject = {} as PKTextTrackDisplaySettingObject;
/**
* The player text style settings
* @type {TextStyle}
* @private
*/
private _textStyle!: TextStyle;
/**
* The playback middleware of the player.
* @type {PlaybackMiddleware}
* @private
*/
private _playbackMiddleware!: PlaybackMiddleware;
/**
* The environment(os,device,browser) object of the player.
* @type {Object}
* @private
*/
private _env!: IEnv;
/**
* The currently selected engine type
* @type {string}
* @private
*/
private _engineType!: string;
/**
* The currently selected stream type
* @type {string}
* @private
*/
private _streamType!: string;
/**
* The current playback attributes state
* @type {Object}
* @private
*/
private _playbackAttributesState: { [attribute: string]: any } = {
muted: undefined,
volume: undefined,
rate: undefined,
videoTrack: undefined,
audioLanguage: '',
textLanguage: '',
captionsDisplay: undefined
};
/**
* Whether a load media request has sent, the player should wait to media.
* @type {boolean}
* @private
*/
private _loadingMedia!: boolean;
/**
* Whether the player is loading a source.
* @type {boolean}
* @private
*/
private _loading!: boolean;
/**
* Reset indicator state.
* @type {boolean}
* @private
*/
private _reset!: boolean;
/**
* Destroyed indicator state.
* @type {boolean}
* @private
*/
private _destroyed!: boolean;
/**
* Fallback to muted auto play mode indicator.
* @type {boolean}
* @private
*/
private _fallbackToMutedAutoPlay!: boolean;
/**
* holds the external tracks handler controller
* @type {ExternalCaptionsHandler}
* @private
*/
private _externalCaptionsHandler!: ExternalCaptionsHandler;
/**
* holds the external tracks handler controller
* @type {ExternalCaptionsHandler}
* @private
*/
private _externalThumbnailsHandler!: ExternalThumbnailsHandler;
/**
* holds the full screen controller
* @type {FullscreenController}
* @private
*/
private _fullscreenController!: FullscreenController;
/**
* holds the resize observer. Incharge of notifying on resize changes.
* @type {?AdsController}
* @private
*/
private _resizeWatcher!: ResizeWatcher;
/**
* Whether the user interacted with the player
* @type {boolean}
* @private
*/
private _hasUserInteracted: boolean = false;
/**
* Whether the video is seeked to live edge
* @type {boolean}
* @private
*/
private _isOnLiveEdge: boolean = false;
/**
* Whether should load after attach media used
* @type {boolean}
* @private
*/
private _shouldLoadAfterAttach: boolean = false;
/**
* The aspect ratio of the player.
* @type {?string}
* @private
*/
private _aspectRatio!: string | null;
/**
* The engine decorator manager.
* @type {?EngineDecoratorManager}
* @private
*/
private _engineDecoratorManager!: EngineDecoratorManager;
private _cachedUrls: string[] = [];
private _timePercentEvent: { [time: string]: boolean } = {
PLAY_REACHED_25_PERCENT: false,
PLAY_REACHED_50_PERCENT: false,
PLAY_REACHED_75_PERCENT: false,
PLAY_REACHED_90_PERCENT: false,
PLAY_REACHED_100_PERCENT: false
}
/**
* @param {Object} config - The configuration for the player instance.
* @constructor
*/
constructor(config: any = {}) {
super();
this._setConfigLogLevel(config);
this._playerId = Utils.Generator.uniqueId(5);
this._prepareVideoElement();
Player.runCapabilities();
this._env = Env;
this._tracks = [];
this._firstPlay = true;
this._loadingMedia = false;
this._loading = false;
this._playbackStart = false;
this._firstPlaying = false;
this._reset = true;
this._destroyed = false;
this._fallbackToMutedAutoPlay = false;
this._config = Player._defaultConfig;
this._sources = Utils.Object.copyDeep(DefaultSources);
this._eventManager = new EventManager();
this._posterManager = new PosterManager();
this._stateManager = new StateManager(this);
this._resizeWatcher = new ResizeWatcher();
this._playbackMiddleware = new PlaybackMiddleware();
this._textStyle = new TextStyle();
this._createReadyPromise();
this._createPlayerContainer();
this._appendDomElements();
this._externalCaptionsHandler = new ExternalCaptionsHandler(this);
this._externalThumbnailsHandler = new ExternalThumbnailsHandler();
this._fullscreenController = new FullscreenController(this);
this.configure(config);
}
// <editor-fold desc="Public API">
// <editor-fold desc="Playback API">
/**
* Configures the player according to a given configuration.
* @param {Object} config - The configuration for the player instance.
* @returns {void}
*/
public configure(config: any = {}): void {
this._setConfigLogLevel(config);
Utils.Object.mergeDeep(this._config, config);
this._applyTextTrackConfig();
this._applyABRRestriction(config);
}
/**
* Configures the player metadata according to a given configuration.
* @param {PKMetadataConfigObject} sourcesMetadata - The sources metadata for the player instance.
* @returns {void}
*/
public setSourcesMetadata(sourcesMetadata: PKMetadataConfigObject): void {
if (this._sources) {
if (!this._sources.metadata) {
this._sources.metadata = {};
}
Utils.Object.mergeDeep(this._sources.metadata, sourcesMetadata);
}
}
/**
* Configures the player according to a given configuration.
* @param {PKSourcesConfigObject} sources - The sources for the player instance.
* @returns {void}
*/
public setSources(sources: PKSourcesConfigObject): void {
if (this._hasSources(sources)) {
this.reset();
Utils.Object.mergeDeep(this._sources, sources);
this._resizeWatcher.init(Utils.Dom.getElementById(this._playerId));
Player._logger.debug('Change source started');
this.dispatchEvent(new FakeEvent(CustomEventType.CHANGE_SOURCE_STARTED));
this._reset = false;
if (this._selectEngineByPriority()) {
this.dispatchEvent(new FakeEvent(CustomEventType.SOURCE_SELECTED, { selectedSource: this._sources[this._streamType] }));
this._attachMedia();
this._handlePlaybackOptions();
this._posterManager.setSrc(this._sources.poster);
this._handleDimensions();
this._handlePreload();
this._handleAutoPlay();
Player._logger.debug('Change source ended');
this.dispatchEvent(new FakeEvent(CustomEventType.CHANGE_SOURCE_ENDED));
} else {
Player._logger.warn('No playable engines was found to play the given sources');
this.dispatchEvent(
new FakeEvent(Html5EventType.ERROR, new PKError(PKError.Severity.CRITICAL, PKError.Category.PLAYER, PKError.Code.NO_ENGINE_FOUND_TO_PLAY_THE_SOURCE, 'No Engine Found To Play The Source'))
);
}
} else {
Utils.Object.mergeDeep(this._sources, sources);
}
}
/**
* The player readiness
* @public
* @returns {Promise<*>} - The ready promise
*/
public ready(): Promise<void> {
return this._readyPromise ? this._readyPromise : Promise.resolve();
}
/**
* Load media
* @public
* @returns {void}
*/
public load(): void {
const loadPlayer = (): void => {
if (this._engine) {
this._load();
} else {
this._eventManager.listenOnce(this, CustomEventType.SOURCE_SELECTED, () => this._load());
}
};
if (!this.src) {
this._playbackMiddleware.load(() => loadPlayer());
} else {
Player._logger.debug('The source has already been loaded. load request ignored');
}
}
/**
* Start/resume playback.
* @param {PKPlayOptionsObject} playOptions - additional options to control the play.
* @param {boolean} playOptions.programmatic - if true, the play call was not initiated by a user gesture and should be handled like auto play.
* @returns {void}
* @public
*/
public play(playOptions?: PKPlayOptionsObject): void {
if (playOptions && playOptions.programmatic) {
this._autoPlay();
return;
}
if (!this._playbackStart) {
this._playbackStart = true;
this.dispatchEvent(new FakeEvent(CustomEventType.PLAYBACK_START));
if (!this.src) {
this._prepareVideoElement();
}
this.load();
}
if (this._engine) {
this._playbackMiddleware.play(() => this._play());
} else if (this._loadingMedia) {
// load media requested but the response is delayed
this._playbackMiddleware.play(() => this._playAfterAsyncMiddleware());
} else {
this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new PKError(PKError.Severity.CRITICAL, PKError.Category.PLAYER, PKError.Code.NO_SOURCE_PROVIDED, 'No Source Provided')));
}
}
/**
* Pause playback.
* @returns {void}
* @public
*/
public pause(): void {
if (this._engine) {
this._playbackMiddleware.pause(this._pause.bind(this));
}
}
/**
* Gets the view of the player (i.e the dom container object).
* @return {HTMLElement} - The dom container.
* @public
*/
public getView(): HTMLElement {
return this._el;
}
/**
* @returns {HTMLVideoElement} - The video element.
* @public
*/
public getVideoElement(): HTMLVideoElement | undefined {
if (this._engine) {
return this._engine.getVideoElement();
}
}
/**
* Get video height.
* @returns {?number} - The intrinsic height of the video.
* @public
*/
public get videoHeight(): number | null {
if (this._engine) {
return this._engine.videoHeight;
}
return null;
}
/**
* Get video width.
* @returns {?number} - The intrinsic width of the video.
* @public
*/
public get videoWidth(): number | null {
if (this._engine) {
return this._engine.videoWidth;
}
return null;
}
/**
* Resets the necessary components before change media.
* @public
* @param {boolean} isChangeMedia - Whether or not this reset triggered due to change media
* @returns {void}
*/
public reset(isChangeMedia: boolean = false): void {
if (this._reset) return;
this.pause();
//make sure all services are reset before engine and engine attributes are reset
this._externalCaptionsHandler.reset();
this._externalThumbnailsHandler.reset();
this._posterManager.reset();
this._stateManager.reset();
this._sources = Utils.Object.copyDeep(DefaultSources);
this._activeTextCues = [];
this._updateTextDisplay([]);
this._tracks = [];
PKTextTrack.reset();
this._resetStateFlags();
this._engineType = '';
this._streamType = '';
this._pendingSelectedVideoTrack = undefined;
if (this._engine) {
this._engine.reset();
}
this.showBlackCover();
this._reset = true;
this.dispatchEvent(new FakeEvent(CustomEventType.PLAYER_RESET, { isChangeMedia }));
this._eventManager.removeAll();
this._resizeWatcher.init(Utils.Dom.getElementById(this._playerId));
this._createReadyPromise();
this._isOnLiveEdge = false;
this._shouldLoadAfterAttach = false;
this._timePercentEvent = {
PLAY_REACHED_25_PERCENT: false,
PLAY_REACHED_50_PERCENT: false,
PLAY_REACHED_75_PERCENT: false,
PLAY_REACHED_90_PERCENT: false,
PLAY_REACHED_100_PERCENT: false
};
}
/**
* Destroys the player.
* @returns {void}
* @public
*/
public destroy(): void {
if (this._destroyed) return;
//make sure all services are destroyed before engine and engine attributes are destroyed
this._externalCaptionsHandler.destroy();
this._posterManager.destroy();
this._stateManager.destroy();
this._fullscreenController.destroy();
this._activeTextCues = [];
this._textDisplaySettings = {} as PKTextTrackDisplaySettingObject;
this._config = {};
this._tracks = [];
this._engineType = '';
this._streamType = '';
this._readyPromise = undefined;
this._pendingSelectedVideoTrack = undefined;
this._resetStateFlags();
this._playbackAttributesState = {};
if (this._engine) {
this._engine.destroy();
}
if (this._engineDecoratorManager) {
this._engineDecoratorManager.destroy();
}
this._resizeWatcher.destroy();
if (this._el) {
Utils.Dom.removeChild(this._el.parentNode, this._el);
}
this._destroyed = true;
this.dispatchEvent(new FakeEvent(CustomEventType.PLAYER_DESTROY));
this._eventManager.destroy();
}
/**
* Hides the black cover div.
* @public
* @returns {void}
*/
public hideBlackCover(): void {
if (this._blackCoverEl) {
this._blackCoverEl.style.visibility = 'hidden';
}
}
/**
* Shows the black cover div.
* @public
* @returns {void}
*/
public showBlackCover(): void {
if (this._blackCoverEl) {
this._blackCoverEl.style.visibility = 'visible';
}
}
/**
* Attach the engine's media source
* @public
* @returns {void}
*/
public attachMediaSource(): void {
if (this._engine) {
this._shouldLoadAfterAttach = true;
this._engine.attachMediaSource();
this._eventManager.listenOnce(this, Html5EventType.CAN_PLAY, () => {
if (typeof this._playbackAttributesState.rate === 'number') {
this.playbackRate = this._playbackAttributesState.rate;
} else {
this.playbackRate = this.defaultPlaybackRate;
}
});
}
}
/**
* detach the engine's media source
* @public
* @returns {void}
*/
public detachMediaSource(): void {
if (this._engine) {
this.pause();
this.hideTextTrack();
this._shouldLoadAfterAttach = false;
this._createReadyPromise();
this._engine.detachMediaSource();
}
}
/**
* detach the engine's media source
* @public
* @returns {void}
* @param {IEngineDecoratorProvider} engineDecoratorProvider - function to create the decorator
*/
public registerEngineDecoratorProvider(engineDecoratorProvider: IEngineDecoratorProvider): void {
if (!this._engineDecoratorManager) {
this._engineDecoratorManager = new EngineDecoratorManager();
}
if (engineDecoratorProvider) {
this._engineDecoratorManager.register(engineDecoratorProvider);
}
}
/**
* Get the first buffered range of the engine.
* @returns {TimeRanges} - First buffered range of the engine in seconds.
* @public
*/
public get buffered(): TimeRanges | null {
if (this._engine) {
return this._engine.buffered;
}
return null;
}
public get stats(): PKStatsObject {
const statsObject: PKStatsObject = {
targetBuffer: NaN,
availableBuffer: NaN
};
if (this._engine) {
statsObject.targetBuffer = this._engine.targetBuffer;
statsObject.availableBuffer = this._engine.availableBuffer;
}
return statsObject;
}
/**
* Set the current time in seconds.
* @param {Number} to - The number to set in seconds.
* @public
*/
public set currentTime(to: number) {
this._playbackMiddleware.setCurrentTime(to, this._setCurrentTime.bind(this));
}
/**
* Get the current time in seconds.
* @returns {?Number} - The playback current time.
* @public
*/
public get currentTime(): number | null {
if (this._engine) {
return this._engine.currentTime;
}
return null;
}
/**
* Get the duration in seconds.
* @returns {?Number} - The playback duration.
* @public
*/
public get duration(): number | null {
if (this._engine) {
return this._engine.duration;
}
return null;
}
/**
* Get the live duration in seconds.
* @returns {?Number} - The live duration.
* @public
*/
public get liveDuration(): number | null {
if (this._engine) {
return this._engine.liveDuration;
}
return null;
}
/**
* Set playback volume.
* @param {Number} vol - The volume to set.
* @returns {void}
* @public
*/
public set volume(vol: number) {
if (this._engine) {
if (Utils.Number.isFloat(vol) || vol === 0 || vol === 1) {
let boundedVol = vol;
if (boundedVol < 0) {
boundedVol = 0;
}
if (boundedVol > 1) {
boundedVol = 1;
}
this._engine.volume = boundedVol;
}
}
}
/**
* Get playback volume.
* @returns {?Number} - The playback volume.
* @public
*/
public get volume(): number | null {
if (this._engine) {
return this._engine.volume;
}
return null;
}
/**
* Get paused state.
* @returns {?boolean} - Whether the video is paused or not.
* @public
*/
public get paused(): boolean | null {
if (this._engine) {
return this._engine.paused;
}
return null;
}
/**
* Get seeking state.
* @returns {?boolean} - Whether the video is seeking or not.
* @public
*/
public get seeking(): boolean | null {
if (this._engine) {
return this._engine.seeking;
}
return null;
}
/**
* Set playsinline attribute.
* Relevant for iOS 10 and up:
* Elements will now be allowed to play inline, and will not automatically enter fullscreen mode when playback begins.
* @param {boolean} playsinline - Whether the video should plays in line.
*/
public set playsinline(playsinline: boolean) {
if (this._engine) {
this._engine.playsinline = playsinline;
}
}
/**
* Get playsinline attribute.
* Relevant for iOS 10 and up:
* Elements will now be allowed to play inline, and will not automatically enter fullscreen mode when playback begins.
* @returns {boolean} - Whether the video plays in line.
*/
public get playsinline(): boolean | null {
if (this._engine) {
return this._engine.playsinline;
}
return null;
}
/**
* Set player muted state.
* @param {boolean} mute - The mute value.
* @returns {void}
* @public
*/
public set muted(mute: boolean) {
if (this._engine) {
this._engine.muted = mute;
this.dispatchEvent(new FakeEvent(CustomEventType.MUTE_CHANGE, { mute: mute }));
if (mute === false) {
this._fallbackToMutedAutoPlay = mute;
}
}
}
/**
* Get player muted state.
* @returns {?boolean} - Whether the video is muted or not.
* @public
*/
public get muted(): boolean | null {
if (this._engine) {
return this._engine.muted;
}
return null;
}
/**
* Get the player source.
* @returns {?string} - The current source of the player.
* @public
*/
public get src(): string | null {
if (this._engine) {
return this._engine.src;
}
return null;
}
/**
* Sets the dimensions of the player.
* @param {PKDimensionsConfig} dimensions - the player dimensions config.
* @returns {void}
* @public
*/
public set dimensions(dimensions: PKDimensionsConfig) {
const targetElement = this._getTargetElement();
if (!dimensions || Utils.Object.isEmptyObject(dimensions)) {
this._aspectRatio = null;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
targetElement.style.width = null;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
targetElement.style.height = null;
} else {
const { height, width } = Utils.Object.mergeDeep(this.dimensions, dimensions);
targetElement.style.width = typeof width === 'number' ? `${width}px` : width;
targetElement.style.height = typeof height === 'number' ? `${height}px` : height;
this._calcRatio(targetElement, dimensions);
}
}
private _getTargetElement(): HTMLDivElement {
return Utils.Dom.getElementById(this._config.targetId);
}
/**
* Get the dimensions of the player.
* @returns {PKPlayerDimensions} - The dimensions of the player.
* @public
*/
public get dimensions(): PKPlayerDimensions {
return {
width: this._el.clientWidth,
height: this._el.clientHeight
};
}
/**
* Get the poster source URL
* @returns {string} - the poster image URL
*/
public get poster(): string {
return this._posterManager.src;
}
/**
* Sets the playbackRate property.
* @param {number} rate - The playback speed of the video.
*/
public set playbackRate(rate: number) {
if (this._engine) {
this._engine.playbackRate = rate;
}
}
/**
* Gets the current playback speed of the video.
* @returns {number} - The current playback speed of the video.
*/
public get playbackRate(): number | null {
if (this._engine) {
return this._engine.playbackRate;
}
return null;
}
/**
* Gets the possible playback speeds of the video.
* @returns {Array<number>} - The possible playback speeds speed of the video.
*/
public get playbackRates(): Array<number> {
if (this._playbackRates) {
return this._playbackRates;
} else if (this._engine) {
return this._engine.playbackRates;
}
return [];
}
/**
* Gets the default playback speed of the video.
* @returns {number} - The default playback speed of the video.
*/
public get defaultPlaybackRate(): number {
if (this._engine) {
if (typeof this._defaultPlaybackRate === 'number' || this._defaultPlaybackRate > 0) {
return this._defaultPlaybackRate;
}
return this._engine.defaultPlaybackRate;
}
return 1;
}
/**
* get the engine type
* @returns {string} - html5
*/
public get engineType(): string {
return this._engineType;
}
/**
* get the stream type
* @returns {string} - hls|dash|progressive
*/
public get streamType(): string {
return this._streamType;
}
/**
* Getter for the environment of the player instance.
* @return {Object} - The current environment object.
* @public
*/
public get env(): IEnv {
return this._env;
}
/**
* Get the player config.
* @returns {Object} - A copy of the player configuration.
* @public
*/
public get config(): any {
return Utils.Object.mergeDeep({}, this._config);
}
/**
* Get the current player sources object.
* @returns {Object} - A copy of the player configuration.
* @public
*/
public get sources(): PKSourcesConfigObject {
return Utils.Object.mergeDeep({}, this._sources);
}
/**
* Get whether the user already interacted with the player
* @returns {boolean} - Whether the user interacted with the player
* @public
*/
public get hasUserInteracted(): boolean {
return this._hasUserInteracted;
}
/**
* Set the _loadingMedia flag to inform the player that a load media request has sent.
* @param {boolean} loading - Whether a load media request has sent.
* @returns {void}
* @public
*/
public set loadingMedia(loading: boolean) {
this._loadingMedia = loading;
}
/**
* Set crossOrigin attribute.
* @param {?string} crossOrigin - 'anonymous' or 'use-credentials'
* anonymous: CORS requests for this element will not have the credentials flag set.
* use-credentials: CORS requests for this element will have the credentials flag set; this means the request will provide credentials.
*/
public set crossOrigin(crossOrigin: string) {
if (this._engine) {
this._engine.crossOrigin = crossOrigin;
}
}
/**
* Get crossOrigin attribute.
* @returns {?string} - 'anonymous' or 'use-credentials'
**/
public get crossOrigin(): string | null {
if (this._engine) {
return this._engine.crossOrigin;
}
return null;
}
/**
* Get ended attribute state.
* @returns {?boolean} - Whether the media has been ended.
**/
public get ended(): boolean | null {
if (this._engine) {
return this._engine.ended;
}
return null;
}
/**
* Get the playback middleware.
* @returns {PlaybackMiddleware} - The playback middleware.
*/
public get playbackMiddleware(): PlaybackMiddleware {
return this._playbackMiddleware;
}
/**
* Get the poster manager.
* @returns {PlaybackMiddleware} - The poster manager.
*/
public get posterManager(): PosterManager {
return this._posterManager;
}
/**
* Checking if the current playback is live.
* @function isLive
* @returns {boolean} - Whether playback is live.
* @public
*/
public isLive(): boolean {
return !!(this._sources.type !== MediaType.VOD && (this._sources.type === MediaType.LIVE || (this._engine && this._engine.isLive())));
}
/**
* Checking if the current playback is audio only.
* @function isAudio
* @returns {boolean} - Whether playback is audio.
* @private
*/
public isAudio(): boolean {
return this._sources.type === MediaType.AUDIO;
}
/**
* Get whether the video is seeked to live edge in dvr
* @returns {boolean} - Whether the video is seeked to live edge in dvr
* @public
*/
public isOnLiveEdge(): boolean {
if (this._engine && typeof this._engine.isOnLiveEdge === 'function') {
return this._engine.isOnLiveEdge();
}
return this._isOnLiveEdge;
}
/**
* Checking if the current live playback has DVR window.
* @function isDvr
* @returns {boolean} - Whether live playback has DVR window.
* @public
*/
public isDvr(): boolean {
return this.isLive() && this._sources.dvr;
}
/**
* Seeking to live edge.
* @function seekToLiveEdge
* @returns {void}
* @public
*/
public seekToLiveEdge(): void {
if (this._engine && this.isLive()) {
this._engine.seekToLiveEdge();
this._isOnLiveEdge = true;
}
}
public shouldAddTextTrack(): boolean {
if (this._engine && typeof this._engine.shouldAddTextTrack === 'function') {
return true;
}
return false;
}
/**
* Get the start time of DVR window in live playback in seconds.
* @returns {Number} - start time of DVR window.
* @public
*/
public getStartTimeOfDvrWindow(): number {
return this._engine ? this._engine.getStartTimeOfDvrWindow() : 0;
}
// </editor-fold>
// <editor-fold desc="Tracks API">
/**
* Returns the tracks according to the filter. if no filter given returns the all tracks.
* @function getTracks
* @template {Track | AudioTrack | TextTrack | VideoTrack} T
* @param {string} [type] - a tracks filter, should be 'video', 'audio' or 'text'.
* @returns {Array<T>} - The parsed tracks.
* @public
*/
public getTracks<T extends Track | AudioTrack | PKTextTrack | VideoTrack | ImageTrack>(type?: TrackTypes): T[] {
switch (type) {
case TrackType.VIDEO:
return Utils.Object.copyDeep(this._getVideoTracks());
case TrackType.AUDIO:
return Utils.Object.copyDeep(this._getAudioTracks());
case TrackType.TEXT:
return Utils.Object.copyDeep(this._getTextTracks());
case TrackType.IMAGE:
return Utils.Object.copyDeep(this._getImageTracks());
default:
return Utils.Object.copyDeep(this._tracks);
}
}
/**
* Get an object includes the active video/audio/text tracks
* @return {{video: VideoTrack, audio: AudioTrack, text: TextTrack}} - The active tracks object
*/
public getActiveTracks(): { video: VideoTrack; audio: AudioTrack; text: PKTextTrack } {
return Utils.Object.copyDeep({
video: this._getVideoTracks().find((track) => track.active),
audio: this._getAudioTracks().find((track) => track.active),
text: this._getTextTracks().find((track) => track.active)
});
}
/**
* change quality
* @function changeQuality
* @param {?Track} track - the track to change
* @returns {void}
* @public
*/
public changeQuality(track: any | string): void {
if (track === 'auto') {
this.enableAdaptiveBitrate();
} else {
this.selectTrack(track);
}
this._markActiveTrack(track);
}
/**
* Select a track
* @function selectTrack
* @param {?Track} track - the track to select
* @returns {void}
* @public
*/
public selectTrack(track?: Track): void {
if (this._engine) {
if (track instanceof VideoTrack) {
this._playbackAttributesState.videoTrack = track;
if (this._stateManager.currentState.type === StateType.IDLE) {
this._pendingSelectedVideoTrack = track;
} else {
this._engine.selectVideoTrack(track);
}
} else if (track instanceof AudioTrack) {
this._engine.selectAudioTrack(track);
} else if (track instanceof PKTextTrack) {
this._resetTextDisplay();
if (track.language === OFF) {
this.hideTextTrack();
this._externalCaptionsHandler.hideTextTrack();
this._playbackAttributesState.captionsDisplay = false;
} else if (track.external) {
this._engine.hideTextTrack();
this._externalCaptionsHandler.selectTextTrack(track);
} else {
this._externalCaptionsHandler.hideTextTrack();
this._engine.selectTextTrack(track);
}
} else if (track instanceof ImageTrack) {
this._engine.selectImageTrack(track);
}
}
}
/**
* Hide the text track
* @function hideTextTrack
* @returns {void}
* @public
*/
public hideTextTrack(): void {
if (this._engine) {
this._engine.hideTextTrack();
this._resetTextDisplay();
const textTracks = this._getTextTracks();
const activeTextTrack = textTracks.find((track) => track.active === true);
if (activeTextTrack && activeTextTrack.external) {
this._externalCaptionsHandler.hideTextTrack();
}
textTracks.map((track) => (track.active = false));
const textTrack = textTracks.find((track) => track.language === OFF);
if (textTrack) {
textTrack.active = true;
this.dispatchEvent(new FakeEvent(CustomEventType.TEXT_TRACK_CHANGED, { selectedTextTrack: textTrack }));
}
this._playbackAttributesState.captionsDisplay = false;
}
}
/**
* Show the text track.
* @returns {void}
* @public
*/
public showTextTrack(): void {
const textTracks = this._getTextTracks();
const userPreference = this._playbackAttributesState.textLanguage;
const prevOrAutoTextLang =
userPreference ||
this._getLanguage<PKTextTrack>(
textTracks,
AUTO,
textTracks.find((textTrack) => textTrack.default)
);
this._setDefaultTrack<PKTextTrack>(textTracks, prevOrAutoTextLang);
}
/**
* Add text track
* @function addTextTrack
* @param {string} kind - Specifies the kind of text track.
* @param {?string} label - A string specifying the label for the text track.
* @param {?string} language - A two-letter language code that specifies the language of the text track.
* @returns {?TextTrack} - A TextTrack Object, which represents the new text track.
* @public
*/
public addTextTrack(kind: TextTrackKind, label?: string, language?: string): TextTrack | undefined {
if (this._engine && typeof this._engine.addTextTrack === 'function') {
return this._engine.addTextTrack(kind, label, language);
}
}
/**
* get the native text tracks
* @function getNativeTextTracks
* @returns {Array<TextTrack>} - The native TextTracks array.
* @public
*/
public getNativeTextTracks(): Array<TextTrack> {
if (this._engine && typeof this._engine.getNativeTextTracks === 'function') {
return this._engine.getNativeTextTracks();
}
return [];
}
/**
* Enables adaptive bitrate switching.
* @function enableAdaptiveBitrate
* @returns {void}
* @public
*/
public enableAdaptiveBitrate(): void {
if (this._engine) {
this._engine.enableAdaptiveBitrate();
}
this._playbackAttributesState.videoTrack = undefined;
}
/**
* Checking if adaptive bitrate switching is enabled.
* @function isAdaptiveBitrateEnabled
* @returns {boolean} - Whether adaptive bitrate is enabled.
* @public
*/
public isAdaptiveBitrateEnabled(): boolean {
if (this._engine) {
return this._engine.isAdaptiveBitrateEnabled();
}
return false;
}
/**
* update the ABR restriction config
* @function _applyABRRestriction
* @returns {void}
* @param {Object} config - new config which configure for checking if it relevant config has changed
* @private
*/
private _applyABRRestriction(config: any): void {
if (Utils.Object.hasPropertyPath(config, 'abr.restrictions') && this._engine && this._tracks?.length) {
const { restrictions } = this._config.abr;
const videoTracks: VideoTrack[] = <VideoTrack[]>this._tracks.filter((track) => track instanceof VideoTrack);
const newVideoTracks = filterTracksByRestriction(videoTracks, restrictions);
if (newVideoTracks.length) {
const currentVideoTracks = this._tracks.filter((track) => track instanceof VideoTrack && track.available);
const tracksHasChanged = !(
currentVideoTracks.length === newVideoTracks.length &&
currentVideoTracks.every((element: VideoTrack, index: number) => {
return element.bandwidth === newVideoTracks[index].bandwidth;
})
);
if (tracksHasChanged) {
this._engine.applyABRRestriction(restrictions);
this._tracks.forEach((track) => {
if (newVideoTracks.includes(<VideoTrack>track) || !(track instanceof VideoTrack)) {
track.available = true;
} else {
track.available = false;
track.active = false;
}
});
if (!this.getActiveTracks().video) {
newVideoTracks[0].active = true;
}
this.dispatchEvent(new FakeEvent(CustomEventType.TRACKS_CHANGED, { tracks: this._tracks.filter((track) => track.available) }));
}
} else {
Player._logger.warn('Invalid restriction, Nothing has changed values do not meet the restriction');
}
}
}
/**
* update the text track config from current config
* @function _applyTextTrackConfig
* @returns {void}
* @param {Object} config - new config which configure for checking if it relevant config has changed
* @private
*/
private _applyTextTrackConfig(): void {
const textTrackDisplaySetting = Utils.Object.getPropertyPath(this._config, 'text.textTrackDisplaySetting');
const useNativeTextTrack = Utils.Object.getPropertyPath(this._config, 'text.useNativeTextTrack');
const textStyle = Utils.Object.getPropertyPath(this._config, 'text.textStyle');
if (textTrackDisplaySetting && !useNativeTextTrack) {
const textDisplaySettings: any = Utils.Object.mergeDeep({}, textTrackDisplaySetting, {
// align - backward compatibility || new caption alignment API || default value
align: textTrackDisplaySetting?.align || textStyle?.textAlign || 'center'
});
// backward compatibility for `text.forceCenter`
if (Utils.Object.getPropertyPath(this._config, 'text.forceCenter')) {
textDisplaySettings.position = 'auto';
textDisplaySettings.align = 'center';
textDisplaySettings.size = '100';
}
this.setTextDisplaySettings(textDisplaySettings);
}
try {
if (textStyle) {
this.textStyle = TextStyle.fromJson(textStyle);
}
} catch (e) {
Player._logger.warn(e);
}
}
/**
* Returns in-stream thumbnail for a chosen time.
* @param {number} time - playback time.
* @public
* @return {?ThumbnailInfo} - Thumbnail info
*/
public getThumbnail(time: number): ThumbnailInfo | null {
if (this._externalThumbnailsHandler.isUsingVttThumbnails()) {
return this._externalThumbnailsHandler.getThumbnail(time);
} else if (this._engine) {
return this._engine.getThumbnail(time);
}
return null;
}
/**
* update the text display settings
* @param {PKTextTrackDisplaySettingObject} settings - text cue display settings
* @public
* @returns {void}
*/
public setTextDisplaySettings(settings: PKTextTrackDisplaySettingObject): void {
this._textDisplaySettings = Utils.Object.mergeDeep(this._textDisplaySettings, settings);
this._updateCueDisplaySettings();
for (let i = 0; i < this._activeTextCues.length; i++) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._activeTextCues[i].hasBeenReset = true;
}
this._updateTextDisplay(this._activeTextCues);
}
public get textDisplaySetting(): any {
return Utils.Object.copyDeep(this._textDisplaySettings);
}
/**
* Sets style attributes for text tracks.
* @param {TextStyle} style - text styling settings
* @returns {void}
*/
public set textStyle(style: TextStyle) {
if (!(style instanceof TextStyle)) {
throw new Error('Style must be instance of TextStyle');
}
resetSubtitleStyleSheet(this._playerId);
try {
this._textStyle = style;
if (this._config.text.useNativeTextTrack) {
this._applyCustomSubtitleStyles();
} else if (this._engine) {
this._engine.resetAllCues();
this._externalCaptionsHandler.resetAllCues();
this._updateTextDisplay(this._activeTextCues);
}
this.dispatchEvent(new FakeEvent(CustomEventType.TEXT_STYLE_CHANGED));
} catch (e) {
Player._logger.error(e.message);
}
}
/**
* Gets style attributes for text tracks.
* @returns {?TextStyle} - the current style attribute
*/
public get textStyle(): TextStyle {
return this._textStyle.clone();
}
// </editor-fold>
// <editor-fold desc="Fullscreen API">
/**
* @returns {boolean} - Whether the player is in fullscreen mode.
* @public
*/
public isFullscreen(): boolean {
return this._fullscreenController.isFullscreen();
}
/**
* Notify the player that the ui application entered to fullscreen.
* @public
* @returns {void}
*/
public notifyEnterFullscreen(): void {
if (this.isFullscreen()) {
this.dispatchEvent(new FakeEvent(CustomEventType.ENTER_FULLSCREEN));
}
}
/**
* Notify the player that the ui application exited from fullscreen.
* @public
* @returns {void}
*/
public notifyExitFullscreen(): void {
if (!this.isFullscreen()) {
this.dispatchEvent(new FakeEvent(CustomEventType.EXIT_FULLSCREEN));
}
}
/**
* Request the player to enter fullscreen.
* @public
* @param {string} elementId - element id to full screen
* @returns {void}
*/
public enterFullscreen(elementId?: string): void {
this._fullscreenController.enterFullscreen(elementId);
}
/**
* Request the player to exit fullscreen.
* @public
* @returns {void}
*/
public exitFullscreen(): void {
this._fullscreenController.exitFullscreen();
}
// </editor-fold>
// <editor-fold desc="Picture In Picture API">
/**
* Request the player to enter picture in picture mode
* @public
* @returns {void}
*/
public enterPictureInPicture(): void {
if (this.isFullscreen()) {
this.exitFullscreen();
}
if (this._engine && !this._engine.isInPictureInPicture) {
this._engine.enterPictureInPicture();
}
}
/**
* Request the player to exit picture in picture mode
* @public
* @returns {void}
*/
public exitPictureInPicture(): void {
if (this._engine && this._engine.isInPictureInPicture) {
this._engine.exitPictureInPicture();
}
}
/**
* Check if the player is in picture in picture mode
* @public
* @return {boolean} if the player is in picture in picture mode or not
*/
public isInPictureInPicture(): boolean {
if (this._engine) {
return this._engine.isInPictureInPicture;
}
return false;
}
/**
* Check if picture in picture supported in this environment
* @public
* @return {boolean} if the picture in picture feature is supported in this environment
*/
public isPictureInPictureSupported(): boolean {
return !!this._config.playback.pictureInPicture && this._engine.isPictureInPictureSupported();
}
// </editor-fold>
// <editor-fold desc="VR API">
/**
* Checking if the selected source is VR.
* @returns {boolean} - Whether is VR.
* @public
*/
public isVr(): boolean {
return !!this._sources.vr;
}
/**
* get the log level
* @param {?string} name - the logger name
* @returns {Object} - the log level
*/
public getLogLevel(name?: string): ILogLevel {
return getLogLevel(name);
}
/**
* sets the logger level
* @param {Object} level - the log level
* @param {?string} name - the logger name
* @returns {void}
*/
public setLogLevel(level: ILogLevel, name?: string): void {
setLogLevel(level, name);
}
public getDrmInfo(): PKDrmDataObject | null {
return this._engine.getDrmInfo();
}
private _applyCustomSubtitleStyles(): void {
try {
const containerId