UNPKG

@playkit-js/playkit-js

Version:

[![Build Status](https://github.com/kaltura/playkit-js/actions/workflows/run_canary.yaml/badge.svg)](https://github.com/kaltura/playkit-js/actions/workflows/run_canary.yaml) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.

1,752 lines (1,632 loc) • 95.6 kB
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 } 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[] = []; /** * @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; } /** * 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; } } /** * 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 = this._el?.parentElement?.id || this._playerId; if (this._config.text.useNativeTextTrack && !this._config.text.useShakaTextTrackDisplay) { const sheet = getSubtitleStyleSheet(this._playerId); ExternalCaptionsHandler.applyNativeTextTrackStyles(sheet, this._textStyle, containerId, ENGINE_CLASS_NAME); } else if (this._config.text.useShakaTextTrackDisplay) { resetSubtitleStyleSheet(this._playerId); const sheet = getSubtitleStyleSheet(this._playerId); this._engine.mediaSourceAdapter?.applyTextTrackStyles?.(sheet, this._textStyle, containerId); } } catch (e) { Player._logger.error(`Failed to add custom text s