diff --git a/src/engines/html5/html5.ts b/src/engines/html5/html5.ts index 58a8e71c..96eefae3 100644 --- a/src/engines/html5/html5.ts +++ b/src/engines/html5/html5.ts @@ -1,23 +1,23 @@ import { FakeEventTarget } from '../../event/fake-event-target'; import { FakeEvent } from '../../event/fake-event'; import { EventManager } from '../../event/event-manager'; -import {CustomEventType, Html5EventType} from '../../event/event-type'; +import { CustomEventType, Html5EventType } from '../../event/event-type'; import MediaSourceProvider from './media-source/media-source-provider'; import VideoTrack from '../../track/video-track'; import AudioTrack from '../../track/audio-track'; -import {PKTextTrack, getActiveCues} from '../../track/text-track'; +import { PKTextTrack, getActiveCues } from '../../track/text-track'; import ImageTrack from '../../track/image-track'; -import {createTimedMetadata} from '../../track/timed-metadata'; +import { createTimedMetadata } from '../../track/timed-metadata'; import * as Utils from '../../utils/util'; import Html5AutoPlayCapability from './capabilities/html5-autoplay'; import Error from '../../error/error'; import getLogger from '../../utils/logger'; -import {DroppedFramesWatcher} from '../dropped-frames-watcher'; -import {ThumbnailInfo} from '../../thumbnail/thumbnail-info'; -import {IMediaSourceAdapter} from '../../types'; -import {CapabilityResult, ICapability} from '../../types'; -import {PKABRRestrictionObject, PKDrmConfigObject, PKDrmDataObject, PKMediaSourceObject, PKVideoElementStore} from '../../types'; -import {IEngine} from '../../types'; +import { DroppedFramesWatcher } from '../dropped-frames-watcher'; +import { ThumbnailInfo } from '../../thumbnail/thumbnail-info'; +import { IMediaSourceAdapter } from '../../types'; +import { CapabilityResult, ICapability } from '../../types'; +import { PKABRRestrictionObject, PKDrmConfigObject, PKDrmDataObject, PKMediaSourceObject, PKVideoElementStore } from '../../types'; +import { IEngine } from '../../types'; import Track from '../../track/track'; const SHORT_BUFFERING_TIMEOUT: number = 200; @@ -59,6 +59,9 @@ export default class Html5 extends FakeEventTarget implements IEngine { private _canLoadMediaSourceAdapterPromise: Promise; private _droppedFramesWatcher: DroppedFramesWatcher | undefined; private _reset: boolean = false; + + private _cachedUrls: string[] = []; + /** * The html5 class logger. * @type {any} @@ -133,7 +136,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @static */ public static runCapabilities(): void { - Html5._capabilities.forEach(capability => capability.runCapability()); + Html5._capabilities.forEach((capability) => capability.runCapability()); } /** @@ -142,13 +145,13 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @public * @static */ - public static getCapabilities(): Promise { + public static getCapabilities(): Promise { const promises: CapabilityResult[] = []; - Html5._capabilities.forEach(capability => promises.push(capability.getCapability())); - return Promise.all(promises).then(arrayOfResults => { + Html5._capabilities.forEach((capability) => promises.push(capability.getCapability())); + return Promise.all(promises).then((arrayOfResults) => { const mergedResults: CapabilityResult = {}; - arrayOfResults.forEach(res => Object.assign(mergedResults, res)); - return {[Html5.id]: mergedResults}; + arrayOfResults.forEach((res) => Object.assign(mergedResults, res)); + return { [Html5.id]: mergedResults }; }); } @@ -159,8 +162,8 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @public * @static */ - public static setCapabilities(capabilities: {[name: string]: any}): void { - Html5._capabilities.forEach(capability => capability.setCapabilities(capabilities)); + public static setCapabilities(capabilities: { [name: string]: any }): void { + Html5._capabilities.forEach((capability) => capability.setCapabilities(capabilities)); } /** @@ -257,6 +260,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { this._droppedFramesWatcher = undefined; } if (this._mediaSourceAdapter) { + this._mediaSourceAdapter.setCachedUrls([]); this._mediaSourceAdapter.destroy(); this._mediaSourceAdapter = null; } @@ -308,7 +312,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @returns {void} */ public attach(): void { - Object.keys(Html5EventType).forEach(html5Event => { + Object.keys(Html5EventType).forEach((html5Event) => { if (![Html5EventType.ERROR, Html5EventType.WAITING].includes(Html5EventType[html5Event])) { this._eventManager.listen(this._el, Html5EventType[html5Event], () => { return this.dispatchEvent(new FakeEvent(Html5EventType[html5Event])); @@ -320,7 +324,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { this._handleMetadataTrackEvents(); this._eventManager.listen(this._el.textTracks, 'addtrack', (event: any) => { if (PKTextTrack.isNativeTextTrack(event.track)) { - this.dispatchEvent(new FakeEvent(CustomEventType.TEXT_TRACK_ADDED, {track: event.track})); + this.dispatchEvent(new FakeEvent(CustomEventType.TEXT_TRACK_ADDED, { track: event.track })); } }); const mediaSourceAdapter = this._mediaSourceAdapter; @@ -354,7 +358,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @returns {void} */ public detach(): void { - Object.keys(Html5EventType).forEach(html5Event => { + Object.keys(Html5EventType).forEach((html5Event) => { this._eventManager.unlisten(this._el, Html5EventType[html5Event]); }); if (this._mediaSourceAdapter) { @@ -517,7 +521,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { public play(): Promise { const playPromise = this._el.play(); if (playPromise) { - playPromise.catch(err => this.dispatchEvent(new FakeEvent(CustomEventType.PLAY_FAILED, {error: err}))); + playPromise.catch((err) => this.dispatchEvent(new FakeEvent(CustomEventType.PLAY_FAILED, { error: err }))); } return playPromise; } @@ -537,13 +541,13 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @public * @returns {Promise} - The loaded data */ - public load(startTime?: number): Promise<{tracks: Track[]}> { + public load(startTime?: number): Promise<{ tracks: Track[] }> { this._el.load(); return this._canLoadMediaSourceAdapterPromise .then(() => { - return this._mediaSourceAdapter ? this._mediaSourceAdapter.load(startTime) : Promise.resolve({} as {tracks: Track[]}); + return this._mediaSourceAdapter ? this._mediaSourceAdapter.load(startTime) : Promise.resolve({} as { tracks: Track[] }); }) - .catch(error => { + .catch((error) => { this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, error)); return Promise.reject(error); }); @@ -560,13 +564,8 @@ export default class Html5 extends FakeEventTarget implements IEngine { // we can use this flag to distinguish between the two. In the future we might need a different method. // Second condition is because flow does not support this API yet if (document.pictureInPictureEnabled && typeof this._el.requestPictureInPicture === 'function' && !this._el.disablePictureInPicture) { - this._el.requestPictureInPicture().catch(error => { - this.dispatchEvent( - new FakeEvent( - Html5EventType.ERROR, - new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.ENTER_PICTURE_IN_PICTURE_FAILED, error) - ) - ); + this._el.requestPictureInPicture().catch((error) => { + this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.ENTER_PICTURE_IN_PICTURE_FAILED, error))); }); // @ts-expect-error - Property 'webkitSetPresentationMode' does not exist on type 'HTMLVideoElement' } else if (typeof this._el.webkitSetPresentationMode === 'function') { @@ -576,12 +575,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { setTimeout(() => this.dispatchEvent(new FakeEvent(Html5EventType.ENTER_PICTURE_IN_PICTURE)), 0); } } catch (error) { - this.dispatchEvent( - new FakeEvent( - Html5EventType.ERROR, - new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.ENTER_PICTURE_IN_PICTURE_FAILED, error) - ) - ); + this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.ENTER_PICTURE_IN_PICTURE_FAILED, error))); } } @@ -596,25 +590,15 @@ export default class Html5 extends FakeEventTarget implements IEngine { // we can use this flag to distinguish between the two. In the future we might need a different method. // Second condition is because flow does not support this API yet if (document.pictureInPictureEnabled && typeof document.exitPictureInPicture === 'function' && this._el === document.pictureInPictureElement) { - document.exitPictureInPicture().catch(error => { - this.dispatchEvent( - new FakeEvent( - Html5EventType.ERROR, - new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.EXIT_PICTURE_IN_PICTURE_FAILED, error) - ) - ); + document.exitPictureInPicture().catch((error) => { + this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.EXIT_PICTURE_IN_PICTURE_FAILED, error))); }); } else if (typeof this._el['webkitSetPresentationMode'] === 'function') { //@ts-expect-error - Property 'webkitSetPresentationMode' does not exist on type 'HTMLVideoElement'. this._el.webkitSetPresentationMode('inline'); } } catch (error) { - this.dispatchEvent( - new FakeEvent( - Html5EventType.ERROR, - new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.EXIT_PICTURE_IN_PICTURE_FAILED, error) - ) - ); + this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.EXIT_PICTURE_IN_PICTURE_FAILED, error))); } } @@ -823,7 +807,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @public * @returns {void} */ - public set preload(preload: 'none' | 'metadata' | 'auto' | '') { + public set preload(preload: 'none' | 'metadata' | 'auto' | '') { this._el.preload = preload; } @@ -832,7 +816,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @returns {string} - The preload value. * @public */ - public get preload(): 'none' | 'metadata' | 'auto' | '' { + public get preload(): 'none' | 'metadata' | 'auto' | '' { return this._el.preload; } @@ -1081,6 +1065,11 @@ export default class Html5 extends FakeEventTarget implements IEngine { this._mediaSourceAdapter = MediaSourceProvider.getMediaSourceAdapter(this.getVideoElement(), source, this._config); if (this._mediaSourceAdapter) { this._droppedFramesWatcher = new DroppedFramesWatcher(this._mediaSourceAdapter, this._config.abr, this._el); + + if (this._cachedUrls.length) { + this._mediaSourceAdapter.setCachedUrls(this._cachedUrls); + this._cachedUrls = []; + } } } @@ -1090,7 +1079,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @private */ private _addCueChangeListener(): void { - const textTrackEl = Array.from(this._el.textTracks).find(track => PKTextTrack.isNativeTextTrack(track) && track.mode !== PKTextTrack.MODE.DISABLED); + const textTrackEl = Array.from(this._el.textTracks).find((track) => PKTextTrack.isNativeTextTrack(track) && track.mode !== PKTextTrack.MODE.DISABLED); if (textTrackEl) { this._eventManager.listen(textTrackEl, 'cuechange', (e: FakeEvent) => this._onCueChange(e)); } @@ -1103,8 +1092,8 @@ export default class Html5 extends FakeEventTarget implements IEngine { */ private _removeCueChangeListeners(): void { Array.from(this._el.textTracks) - .filter(track => !PKTextTrack.isMetaDataTrack(track)) - .forEach(track => { + .filter((track) => !PKTextTrack.isMetaDataTrack(track)) + .forEach((track) => { this._eventManager.unlisten(track, 'cuechange'); }); } @@ -1118,7 +1107,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { private _onCueChange(e: FakeEvent): void { const activeCues: TextTrackCueList = e.currentTarget.activeCues; const normalizedActiveCues = getActiveCues(activeCues); - this.dispatchEvent(new FakeEvent(CustomEventType.TEXT_CUE_CHANGED, {cues: normalizedActiveCues})); + this.dispatchEvent(new FakeEvent(CustomEventType.TEXT_CUE_CHANGED, { cues: normalizedActiveCues })); } /** @@ -1126,9 +1115,7 @@ export default class Html5 extends FakeEventTarget implements IEngine { * @returns {void} */ public resetAllCues(): void { - const activeTextTrack = Array.from(this._el.textTracks).find( - track => PKTextTrack.isNativeTextTrack(track) && track.mode !== PKTextTrack.MODE.DISABLED - ); + const activeTextTrack = Array.from(this._el.textTracks).find((track) => PKTextTrack.isNativeTextTrack(track) && track.mode !== PKTextTrack.MODE.DISABLED); if (activeTextTrack) { for (let i = 0; i < activeTextTrack.cues!.length; i++) { // @ts-expect-error - Property 'hasBeenReset' does not exist on type 'TextTrackCue' @@ -1210,10 +1197,10 @@ export default class Html5 extends FakeEventTarget implements IEngine { activeCues = activeCues.sort((a: VTTCue, b: VTTCue) => { return a.startTime - b.startTime; }); - this.dispatchEvent(new FakeEvent(CustomEventType.TIMED_METADATA, {cues: activeCues})); + this.dispatchEvent(new FakeEvent(CustomEventType.TIMED_METADATA, { cues: activeCues })); this.dispatchEvent( new FakeEvent(CustomEventType.TIMED_METADATA_CHANGE, { - cues: activeCues.map(cue => createTimedMetadata(cue)) + cues: activeCues.map((cue) => createTimedMetadata(cue)) }) ); }); @@ -1277,4 +1264,13 @@ export default class Html5 extends FakeEventTarget implements IEngine { public getDrmInfo(): PKDrmDataObject | null { return this._mediaSourceAdapter ? this._mediaSourceAdapter.getDrmInfo() : null; } + + public setCachedUrls(cachedUrls: string[]): void { + this._cachedUrls = cachedUrls; + + if (this._mediaSourceAdapter) { + this._mediaSourceAdapter.setCachedUrls(cachedUrls); + this._cachedUrls = []; + } + } } diff --git a/src/engines/html5/media-source/base-media-source-adapter.ts b/src/engines/html5/media-source/base-media-source-adapter.ts index 4daa98b5..bef9b906 100644 --- a/src/engines/html5/media-source/base-media-source-adapter.ts +++ b/src/engines/html5/media-source/base-media-source-adapter.ts @@ -230,7 +230,7 @@ export default class BaseMediaSourceAdapter extends FakeEventTarget implements I } public isOnLiveEdge(): boolean { - if(this.getSegmentDuration()===0){ + if (this.getSegmentDuration() === 0) { //If no segment duration, we cannot estimate live edge return true; } @@ -344,4 +344,6 @@ export default class BaseMediaSourceAdapter extends FakeEventTarget implements I public getDrmInfo(): PKDrmDataObject | null { return null; } + + public setCachedUrls(): void {} } diff --git a/src/player.ts b/src/player.ts index 0375a5c9..b9fecd9f 100644 --- a/src/player.ts +++ b/src/player.ts @@ -2,53 +2,59 @@ 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 { FakeEventTarget } from './event/fake-event-target'; import { IEngine, IEngineStatic, LoggerLevels } from './types'; -import {CustomEventType, EventType, Html5EventType} from './event/event-type'; +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 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 from './track/audio-track'; -import {PKTextTrack} from './track/text-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 { 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 { 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 { 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 { 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, + 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'; +import { ILogger, ILogLevel } from 'js-logger'; +import { IEnv } from './types/ua-parser'; /** * The black cover class name. @@ -64,11 +70,11 @@ const BLACK_COVER_CLASS_NAME: string = 'playkit-black-cover'; const CONTAINER_CLASS_NAME: string = 'playkit-container'; /** - /** - * The player poster class name. - * @type {string} - * @const - */ +/** +* The player poster class name. +* @type {string} +* @const +*/ const POSTER_CLASS_NAME: string = 'playkit-poster'; /** @@ -134,7 +140,7 @@ export default class Player extends FakeEventTarget { */ public static runCapabilities(): void { Player._logger.debug('Running player capabilities'); - EngineProvider.getEngines().forEach(Engine => Engine.runCapabilities()); + EngineProvider.getEngines().forEach((Engine) => Engine.runCapabilities()); } /** @@ -144,13 +150,13 @@ export default class Player extends FakeEventTarget { * @public * @static */ - public static getCapabilities(engineType?: string): Promise<{[name: string]: any}> { + 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 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)); + arrayOfResults.forEach((res) => Object.assign(playerCapabilities, res)); return engineType ? playerCapabilities[engineType] : playerCapabilities; }); } @@ -163,9 +169,9 @@ export default class Player extends FakeEventTarget { * @public * @static */ - public static setCapabilities(engineType: string, capabilities: {[name: string]: any}): void { + 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); + const selectedEngine = EngineProvider.getEngines().find((Engine) => Engine.id === engineType); if (selectedEngine) { selectedEngine.setCapabilities(capabilities); } @@ -321,7 +327,7 @@ export default class Player extends FakeEventTarget { * @type {Object} * @private */ - private _playbackAttributesState: {[attribute: string]: any} = { + private _playbackAttributesState: { [attribute: string]: any } = { muted: undefined, volume: undefined, rate: undefined, @@ -415,6 +421,8 @@ export default class Player extends FakeEventTarget { */ private _engineDecoratorManager!: EngineDecoratorManager; + private _cachedUrls: string[] = []; + /** * @param {Object} config - The configuration for the player instance. * @constructor @@ -496,7 +504,7 @@ export default class Player extends FakeEventTarget { 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.dispatchEvent(new FakeEvent(CustomEventType.SOURCE_SELECTED, { selectedSource: this._sources[this._streamType] })); this._attachMedia(); this._handlePlaybackOptions(); this._posterManager.setSrc(this._sources.poster); @@ -508,15 +516,7 @@ export default class Player extends FakeEventTarget { } 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' - ) - ) + 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 { @@ -580,12 +580,7 @@ export default class Player extends FakeEventTarget { // 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') - ) - ); + this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new PKError(PKError.Severity.CRITICAL, PKError.Category.PLAYER, PKError.Code.NO_SOURCE_PROVIDED, 'No Source Provided'))); } } @@ -671,7 +666,7 @@ export default class Player extends FakeEventTarget { } this.showBlackCover(); this._reset = true; - this.dispatchEvent(new FakeEvent(CustomEventType.PLAYER_RESET, {isChangeMedia})); + this.dispatchEvent(new FakeEvent(CustomEventType.PLAYER_RESET, { isChangeMedia })); this._eventManager.removeAll(); this._resizeWatcher.init(Utils.Dom.getElementById(this._playerId)); this._createReadyPromise(); @@ -946,7 +941,7 @@ export default class Player extends FakeEventTarget { public set muted(mute: boolean) { if (this._engine) { this._engine.muted = mute; - this.dispatchEvent(new FakeEvent(CustomEventType.MUTE_CHANGE, {mute: mute})); + this.dispatchEvent(new FakeEvent(CustomEventType.MUTE_CHANGE, { mute: mute })); if (mute === false) { this._fallbackToMutedAutoPlay = mute; } @@ -994,7 +989,7 @@ export default class Player extends FakeEventTarget { // @ts-ignore targetElement.style.height = null; } else { - const {height, width} = Utils.Object.mergeDeep(this.dimensions, dimensions); + 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); @@ -1277,11 +1272,11 @@ export default class Player extends FakeEventTarget { * 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} { + 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) + video: this._getVideoTracks().find((track) => track.active), + audio: this._getAudioTracks().find((track) => track.active), + text: this._getTextTracks().find((track) => track.active) }); } @@ -1333,15 +1328,15 @@ export default class Player extends FakeEventTarget { this._engine.hideTextTrack(); this._resetTextDisplay(); const textTracks = this._getTextTracks(); - const activeTextTrack = textTracks.find(track => track.active === true); + 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); + 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.dispatchEvent(new FakeEvent(CustomEventType.TEXT_TRACK_CHANGED, { selectedTextTrack: textTrack })); } this._playbackAttributesState.captionsDisplay = false; } @@ -1360,7 +1355,7 @@ export default class Player extends FakeEventTarget { this._getLanguage( textTracks, AUTO, - textTracks.find(textTrack => textTrack.default) + textTracks.find((textTrack) => textTrack.default) ); this._setDefaultTrack(textTracks, prevOrAutoTextLang); } @@ -1428,11 +1423,11 @@ export default class Player extends FakeEventTarget { */ 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[] = this._tracks.filter(track => track instanceof VideoTrack); + const { restrictions } = this._config.abr; + const videoTracks: 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 currentVideoTracks = this._tracks.filter((track) => track instanceof VideoTrack && track.available); const tracksHasChanged = !( currentVideoTracks.length === newVideoTracks.length && currentVideoTracks.every((element: VideoTrack, index: number) => { @@ -1441,7 +1436,7 @@ export default class Player extends FakeEventTarget { ); if (tracksHasChanged) { this._engine.applyABRRestriction(restrictions); - this._tracks.forEach(track => { + this._tracks.forEach((track) => { if (newVideoTracks.includes(track) || !(track instanceof VideoTrack)) { track.available = true; } else { @@ -1452,7 +1447,7 @@ export default class Player extends FakeEventTarget { if (!this.getActiveTracks().video) { newVideoTracks[0].active = true; } - this.dispatchEvent(new FakeEvent(CustomEventType.TRACKS_CHANGED, {tracks: this._tracks.filter(track => track.available)})); + 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'); @@ -1760,7 +1755,7 @@ export default class Player extends FakeEventTarget { */ private _hasSources(sources: PKSourcesConfigObject): boolean { if (sources) { - return !!Object.values(StreamType).find(type => sources[type] && sources[type].length > 0); + return !!Object.values(StreamType).find((type) => sources[type] && sources[type].length > 0); } return false; } @@ -1871,7 +1866,7 @@ export default class Player extends FakeEventTarget { for (const priority of streamPriority) { const engineId = typeof priority.engine === 'string' ? priority.engine.toLowerCase() : ''; const format = typeof priority.format === 'string' ? priority.format.toLowerCase() : ''; - const Engine = EngineProvider.getEngines().find(Engine => Engine.id === engineId); + const Engine = EngineProvider.getEngines().find((Engine) => Engine.id === engineId); if (Engine) { const formatSources = sources[format]; if (formatSources && formatSources.length > 0) { @@ -1902,7 +1897,7 @@ export default class Player extends FakeEventTarget { this._appendEngineEl(); } else { if (this._engine.id === Engine.id) { - this._engine.restore(source, {...this._config, sources: this._sources}); + this._engine.restore(source, { ...this._config, sources: this._sources }); } else { this._engine.destroy(); this._createEngine(Engine, source); @@ -1919,8 +1914,13 @@ export default class Player extends FakeEventTarget { * @private */ private _createEngine(Engine: IEngineStatic, source: PKMediaSourceObject): void { - const engine = Engine.createEngine(source, {...this._config, sources: this._sources}, this._playerId); - this._engine = this._engineDecoratorManager ? new EngineDecorator(engine, this._engineDecoratorManager) as EngineDecoratorType : engine; + const engine = Engine.createEngine(source, { ...this._config, sources: this._sources }, this._playerId); + this._engine = this._engineDecoratorManager ? (new EngineDecorator(engine, this._engineDecoratorManager) as EngineDecoratorType) : engine; + + if (this._cachedUrls.length) { + this._engine.setCachedUrls(this._cachedUrls); + this._cachedUrls = []; + } } /** @@ -1930,7 +1930,7 @@ export default class Player extends FakeEventTarget { */ private _attachMedia(): void { if (this._engine) { - Object.keys(Html5EventType).forEach(html5Event => { + Object.keys(Html5EventType).forEach((html5Event) => { this._eventManager.listen(this._engine, Html5EventType[html5Event], (event: FakeEvent) => { return this.dispatchEvent(event); }); @@ -1988,9 +1988,7 @@ export default class Player extends FakeEventTarget { this.dispatchEvent(event); }); this._eventManager.listen(this._externalCaptionsHandler, CustomEventType.TEXT_CUE_CHANGED, (event: FakeEvent) => this._onCueChange(event)); - this._eventManager.listen(this._externalCaptionsHandler, CustomEventType.TEXT_TRACK_CHANGED, (event: FakeEvent) => - this._onTextTrackChanged(event) - ); + this._eventManager.listen(this._externalCaptionsHandler, CustomEventType.TEXT_TRACK_CHANGED, (event: FakeEvent) => this._onTextTrackChanged(event)); this._eventManager.listen(this._externalCaptionsHandler, Html5EventType.ERROR, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(this._externalThumbnailsHandler, Html5EventType.ERROR, (event: FakeEvent) => this.dispatchEvent(event)); const rootElement = Utils.Dom.getElementBySelector(`#${this.config.targetId}`); @@ -2002,7 +2000,7 @@ export default class Player extends FakeEventTarget { this._hasUserInteracted = true; this.dispatchEvent(new FakeEvent(CustomEventType.USER_GESTURE)); }, - {capture: true} + { capture: true } ); } } @@ -2073,9 +2071,7 @@ export default class Player extends FakeEventTarget { this.crossOrigin = this._config.playback.crossOrigin; } if (Array.isArray(this._config.playback.playbackRates)) { - const validPlaybackRates = this._config.playback.playbackRates - .filter((number, index, self) => number > 0 && number <= 16 && self.indexOf(number) === index) - .sort((a, b) => a - b); + const validPlaybackRates = this._config.playback.playbackRates.filter((number, index, self) => number > 0 && number <= 16 && self.indexOf(number) === index).sort((a, b) => a - b); if (validPlaybackRates) { this._playbackRates = validPlaybackRates; } @@ -2095,7 +2091,7 @@ export default class Player extends FakeEventTarget { private _autoPlay(): void { const allowMutedAutoPlay = this._config.playback.allowMutedAutoPlay; - Player.getCapabilities(this.engineType).then(capabilities => { + Player.getCapabilities(this.engineType).then((capabilities) => { if (capabilities.autoplay) { onAutoPlay(); } else { @@ -2158,11 +2154,11 @@ export default class Player extends FakeEventTarget { } /** - } - * Checks auto play configuration and handles initialization accordingly. - * @returns {void} - * @private - */ + } + * Checks auto play configuration and handles initialization accordingly. + * @returns {void} + * @private + */ private _handleAutoPlay(): void { if (this.isAudio() || this._config.playback.autoplay !== AutoPlayType.TRUE) { this._posterManager.show(); @@ -2195,13 +2191,13 @@ export default class Player extends FakeEventTarget { const startTime = this._sources.startTime; this._engine .load(startTime) - .then(data => { + .then((data) => { if (this.isLive() && (startTime === -1 || Number(startTime) >= Number(this.duration))) { this._isOnLiveEdge = true; } this._updateTracks(data.tracks); - this.dispatchEvent(new FakeEvent(CustomEventType.TRACKS_CHANGED, {tracks: this._tracks})); - if(this.sources.thumbnails) this._externalThumbnailsHandler.load(this.sources.thumbnails); + this.dispatchEvent(new FakeEvent(CustomEventType.TRACKS_CHANGED, { tracks: this._tracks })); + if (this.sources.thumbnails) this._externalThumbnailsHandler.load(this.sources.thumbnails); }) .finally(() => { resetFlags(); @@ -2215,7 +2211,7 @@ export default class Player extends FakeEventTarget { * @returns {void} */ private _handleDimensions(): void { - const {dimensions} = this.config; + const { dimensions } = this.config; if (Utils.Object.isObject(dimensions) && !Utils.Object.isEmptyObject(dimensions)) { this.dimensions = dimensions; } @@ -2250,10 +2246,13 @@ export default class Player extends FakeEventTarget { if (!this._firstPlay) { return outOfDvr; } else { - return !!this.src && !this.isOnLiveEdge() - // Live video can be set with explicit start time,(e.g. startOver) - // in that case we don't want to move to the liveEdge - && this._sources.startTime === undefined; + return ( + !!this.src && + !this.isOnLiveEdge() && + // Live video can be set with explicit start time,(e.g. startOver) + // in that case we don't want to move to the liveEdge + this._sources.startTime === undefined + ); } } return false; @@ -2384,7 +2383,7 @@ export default class Player extends FakeEventTarget { this._aspectRatio = dimensions.ratio; } if (this._aspectRatio) { - const [ratioWidth, ratioHeight] = this._aspectRatio.split(':').map(r => Number(r)); + const [ratioWidth, ratioHeight] = this._aspectRatio.split(':').map((r) => Number(r)); if (dimensions.width || (!dimensions.width && !dimensions.height)) { const height = (ratioHeight / ratioWidth) * targetElement.clientWidth; targetElement.style.height = `${height}px`; @@ -2442,7 +2441,7 @@ export default class Player extends FakeEventTarget { * @returns {Array} - The parsed tracks. * @private */ - private _getTracksByType(type: { new(...args: any[]): T }): T[] { + private _getTracksByType(type: { new (...args: any[]): T }): T[] { return this._tracks.reduce((arr: T[], track) => { if (track instanceof type && track.available) { arr.push(track); @@ -2599,15 +2598,12 @@ export default class Player extends FakeEventTarget { */ private _setDefaultTracks(): void { const activeTracks = this.getActiveTracks(); - const defaultStreamTrack = this._getTextTracks().find(track => track.default); + const defaultStreamTrack = this._getTextTracks().find((track) => track.default); const playbackConfig = this.config.playback; - const offTextTrack: Track = this._getTextTracks().find(track => PKTextTrack.langComparer(OFF, track.language))! ; + const offTextTrack: Track = this._getTextTracks().find((track) => PKTextTrack.langComparer(OFF, track.language))!; const defaultLanguage = this._getLanguage(this._getTextTracks(), playbackConfig.textLanguage, defaultStreamTrack); - const currentOrConfiguredTextLang = - !this._playbackAttributesState.textLanguage || this.config.disableUserCache ? defaultLanguage : this._playbackAttributesState.textLanguage; - const currentOrConfiguredAudioLang = - this._playbackAttributesState.audioLanguage || - this._getLanguage(this._getAudioTracks(), playbackConfig.audioLanguage, activeTracks.audio); + const currentOrConfiguredTextLang = !this._playbackAttributesState.textLanguage || this.config.disableUserCache ? defaultLanguage : this._playbackAttributesState.textLanguage; + const currentOrConfiguredAudioLang = this._playbackAttributesState.audioLanguage || this._getLanguage(this._getAudioTracks(), playbackConfig.audioLanguage, activeTracks.audio); if (!playbackConfig.captionsDisplay) { this._playbackAttributesState.textLanguage = defaultLanguage; this._setDefaultTrack(this._getTextTracks(), OFF, offTextTrack); @@ -2619,12 +2615,7 @@ export default class Player extends FakeEventTarget { } } if (currentOrConfiguredAudioLang === playbackConfig.audioLanguage) { - this._setDefaultTrack( - this._getAudioTracks(), - currentOrConfiguredAudioLang, - activeTracks.audio, - playbackConfig.additionalAudioLanguage - ); + this._setDefaultTrack(this._getAudioTracks(), currentOrConfiguredAudioLang, activeTracks.audio, playbackConfig.additionalAudioLanguage); } else { this._setDefaultTrack(this._getAudioTracks(), currentOrConfiguredAudioLang, activeTracks.audio); } @@ -2642,7 +2633,7 @@ export default class Player extends FakeEventTarget { private _getLanguage(tracks: T[], configuredLanguage: string, defaultTrack?: T): string { let language = configuredLanguage; if (language === AUTO) { - const localeTrack: T | undefined = tracks.find(track => Track.langComparer(Locale.language, track.language)); + const localeTrack: T | undefined = tracks.find((track) => Track.langComparer(Locale.language, track.language)); if (localeTrack) { language = localeTrack.language; } else if (defaultTrack && defaultTrack.language !== OFF) { @@ -2664,21 +2655,16 @@ export default class Player extends FakeEventTarget { * @returns {void} * @private */ - private _setDefaultTrack( - tracks: T[], - language: string, - defaultTrack?: Track | null, - additionalLanguage?: string | string [] - ): void { + private _setDefaultTrack(tracks: T[], language: string, defaultTrack?: Track | null, additionalLanguage?: string | string[]): void { const updateTrack = (track: T): void => { this.selectTrack(track); this._markActiveTrack(track); }; - const sameTrack: T | undefined = tracks.find(track => Track.langComparer(language, track.language, additionalLanguage, true)); + const sameTrack: T | undefined = tracks.find((track) => Track.langComparer(language, track.language, additionalLanguage, true)); if (sameTrack) { updateTrack(sameTrack); } else { - const track: T | undefined = tracks.find(track => Track.langComparer(language, track.language, additionalLanguage, false)); + const track: T | undefined = tracks.find((track) => Track.langComparer(language, track.language, additionalLanguage, false)); if (track) { updateTrack(track); } else if (defaultTrack && !defaultTrack.active) { @@ -2694,13 +2680,9 @@ export default class Player extends FakeEventTarget { */ private _setDefaultVideoTrack(): void { const sortedVideoTracks = this._getVideoTracks().sort((track1: VideoTrack, track2: VideoTrack) => track2.bandwidth - track1.bandwidth); - let selectedVideoTrack = sortedVideoTracks.find( - (track: VideoTrack) => track.label && track.label === this._playbackAttributesState.videoTrack?.label - ); + let selectedVideoTrack = sortedVideoTracks.find((track: VideoTrack) => track.label && track.label === this._playbackAttributesState.videoTrack?.label); if (!selectedVideoTrack) { - selectedVideoTrack = sortedVideoTracks.find( - (track: VideoTrack) => track.height && track.height === this._playbackAttributesState.videoTrack?.height - ); + selectedVideoTrack = sortedVideoTracks.find((track: VideoTrack) => track.height && track.height === this._playbackAttributesState.videoTrack?.height); } if (selectedVideoTrack) { this.selectTrack(selectedVideoTrack); @@ -2744,7 +2726,7 @@ export default class Player extends FakeEventTarget { * @returns {void} */ private _setTracksCustomLabels(tracks: T[], callback: (track: T) => string): void { - tracks.forEach(track => { + tracks.forEach((track) => { const result = callback(Utils.Object.copyDeep(track)); if (result) { track.label = result; @@ -2865,7 +2847,7 @@ export default class Player extends FakeEventTarget { * @returns {PKAdTagTypes} - The ad tag types of the player. * @public */ - public get AdTagType(): typeof AdTagType{ + public get AdTagType(): typeof AdTagType { return AdTagType; } @@ -2877,4 +2859,13 @@ export default class Player extends FakeEventTarget { public get Error(): typeof PKError { return PKError; } + + public setCachedUrls(cachedUrls: string[]): void { + this._cachedUrls = cachedUrls; + + if (this._engine) { + this._engine.setCachedUrls(cachedUrls); + this._cachedUrls = []; + } + } } diff --git a/src/types/interfaces/engine.ts b/src/types/interfaces/engine.ts index ef1ac4b2..1317e985 100644 --- a/src/types/interfaces/engine.ts +++ b/src/types/interfaces/engine.ts @@ -1,16 +1,15 @@ - import VideoTrack from '../../track/video-track'; import AudioTrack from '../../track/audio-track'; import { FakeEventTarget } from '../../event/fake-event-target'; -import {ThumbnailInfo} from '../../thumbnail/thumbnail-info'; +import { ThumbnailInfo } from '../../thumbnail/thumbnail-info'; import ImageTrack from '../../track/image-track'; -import {PKMediaSourceObject} from '../media-source'; -import {PKDrmConfigObject} from '../drm-config'; -import {PKDrmDataObject} from '../drm-data'; -import {PKABRRestrictionObject} from '../restrictions-types'; +import { PKMediaSourceObject } from '../media-source'; +import { PKDrmConfigObject } from '../drm-config'; +import { PKDrmDataObject } from '../drm-data'; +import { PKABRRestrictionObject } from '../restrictions-types'; import Track from '../../track/track'; -import {PKTextTrack} from '../../track/text-track'; -import {IMediaSourceAdapter} from '../../types'; +import { PKTextTrack } from '../../track/text-track'; +import { IMediaSourceAdapter } from '../../types'; export interface IEngineStatic { id: string; @@ -18,7 +17,7 @@ export interface IEngineStatic { canPlaySource(source: PKMediaSourceObject, preferNative: boolean, drmConfig: PKDrmConfigObject): boolean; runCapabilities(): void; getCapabilities(): Promise; - setCapabilities(capabilities: {[name: string]: any}): void; + setCapabilities(capabilities: { [name: string]: any }): void; prepareVideoElement(playerId: string): void; isSupported(): boolean; } @@ -30,7 +29,7 @@ export interface IEngine extends FakeEventTarget { detach(): void; play(): Promise | undefined; pause(): void; - load(startTime?: number): Promise<{tracks: Track[]}>; + load(startTime?: number): Promise<{ tracks: Track[] }>; reset(): void; selectVideoTrack(videoTrack: VideoTrack): void; selectAudioTrack(audioTrack: AudioTrack): void; @@ -50,11 +49,12 @@ export interface IEngine extends FakeEventTarget { resetAllCues(): void; attachMediaSource(): void; detachMediaSource(): void; - getThumbnail(time: number): ThumbnailInfo | null + getThumbnail(time: number): ThumbnailInfo | null; isOnLiveEdge(): boolean; - addTextTrack(kind: TextTrackKind, label?: string, language?: string): TextTrack | undefined ; + addTextTrack(kind: TextTrackKind, label?: string, language?: string): TextTrack | undefined; getNativeTextTracks(): TextTrack[]; getDrmInfo(): PKDrmDataObject | null; + setCachedUrls(cachedUrls: string[]); id: string; currentTime: number; duration: number; @@ -70,7 +70,7 @@ export interface IEngine extends FakeEventTarget { defaultMuted: boolean; src: string; poster: string; - preload: "none" | "metadata" | "auto" | ""; + preload: 'none' | 'metadata' | 'auto' | ''; autoplay: boolean; controls: boolean; loop: boolean; @@ -84,7 +84,7 @@ export interface IEngine extends FakeEventTarget { networkState: number; readyState: number; playsinline: boolean; - crossOrigin: string | null + crossOrigin: string | null; targetBuffer: number; availableBuffer: number; mediaSourceAdapter: IMediaSourceAdapter | null; diff --git a/src/types/interfaces/media-source-adapter.ts b/src/types/interfaces/media-source-adapter.ts index ced420fa..b7d7c7d5 100644 --- a/src/types/interfaces/media-source-adapter.ts +++ b/src/types/interfaces/media-source-adapter.ts @@ -49,4 +49,5 @@ export interface IMediaSourceAdapter extends FakeEventTarget { getDrmInfo(): PKDrmDataObject | null; applyABRRestriction(restriction: PKABRRestrictionObject): void; applyTextTrackStyles(sheet: CSSStyleSheet, styles: TextStyle, containerId: string, engineClassName?: string): void; + setCachedUrls(cachedUrls: string[]); }