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 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 ImageTrack from '../../track/image-track'; 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 Track from '../../track/track'; import Env from '../../../src/utils/env'; const SHORT_BUFFERING_TIMEOUT: number = 200; /** * Html5 engine for playback. * @classdesc */ export default class Html5 extends FakeEventTarget implements IEngine { /** * The video element. * @type {HTMLVideoElement} * @private */ private _el!: HTMLVideoElement; /** * The event manager of the engine. * @type {EventManager} * @private */ private _eventManager: EventManager; /** * The selected media source adapter of the engine. * @type {?IMediaSourceAdapter} * @private */ private _mediaSourceAdapter!: IMediaSourceAdapter | null; /** * The player config object. * @type {Object} * @private */ private _config: any; /** * Promise to indicate when a media source adapter can be loaded. * @type {Promise<*>} * @private */ private _canLoadMediaSourceAdapterPromise: Promise; private _droppedFramesWatcher: DroppedFramesWatcher | undefined; private _reset: boolean = false; private _cachedUrls: string[] = []; /** * The html5 class logger. * @type {any} * @static * @private */ private static _logger: any = getLogger('Html5'); /** * The html5 capabilities handlers. * @private * @static */ private static _capabilities: Array = [Html5AutoPlayCapability]; /** * @type {string} - The engine id. * @public * @static */ public static id: string = 'html5'; /** * @type {PKVideoElementStore} - Store object which maps between playerId to its video element. */ public static videoElementStore: PKVideoElementStore = {}; /** * Checks if html5 is supported. * @returns {boolean} - Whether the html5 is supported. */ public static isSupported(): boolean { try { const el = Utils.Dom.createElement('video'); el.volume = 0.5; return !!el.canPlayType; } catch (e) { return false; } } /** * Factory method to create an engine. * @param {PKMediaSourceObject} source - The selected source object. * @param {Object} config - The player configuration. * @param {string} playerId - The player id. * @returns {IEngine} - New instance of the run time engine. * @public * @static */ public static createEngine(source: PKMediaSourceObject, config: any, playerId: string): IEngine { return new this(source, config, playerId); } /** * Checks if the engine can play a given source. * @param {PKMediaSourceObject} source - The source object to check. * @param {boolean} preferNative - prefer native flag. * @param {PKDrmConfigObject} drmConfig - The drm config. * @returns {boolean} - Whether the engine can play the source. * @public * @static */ public static canPlaySource(source: PKMediaSourceObject, preferNative: boolean, drmConfig: PKDrmConfigObject): boolean { return MediaSourceProvider.canPlaySource(source, preferNative, drmConfig); } /** * Runs the html5 capabilities tests. * @returns {void} * @public * @static */ public static runCapabilities(): void { Html5._capabilities.forEach((capability) => capability.runCapability()); } /** * Gets the html5 capabilities. * @return {Promise} - The html5 capabilities object. * @public * @static */ public static getCapabilities(): Promise { const promises: CapabilityResult[] = []; 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 }; }); } /** * Sets an engine capabilities. * @param {Object} capabilities - The engine capabilities. * @returns {void} * @public * @static */ public static setCapabilities(capabilities: { [name: string]: any }): void { Html5._capabilities.forEach((capability) => capability.setCapabilities(capabilities)); } /** * For browsers which block auto play, use the user gesture to open the video element and enable playing via API. * @returns {void} * @param {string} playerId - the id to be set as the key of the video element * @private * @public */ public static prepareVideoElement(playerId: string): void { if (!Html5.videoElementStore[playerId]) { Html5._logger.debug(`Create the video element for playing ${playerId}`); const videoElement = Utils.Dom.createElement('video'); videoElement.setAttribute('aria-hidden', 'true'); Html5.videoElementStore[playerId] = videoElement; } Html5._logger.debug(`Prepare the video element for playing ${playerId}`); Html5.videoElementStore[playerId].load(); } /** * The player playback rates. * @type {Array} */ public static PLAYBACK_RATES: Array = [0.5, 1, 1.5, 2]; /** * @constructor * @param {PKMediaSourceObject} source - The selected source object. * @param {Object} config - The player configuration. * @param {string} playerId - The player id. */ constructor(source: PKMediaSourceObject, config: any, playerId: string) { super(); this._eventManager = new EventManager(); this._canLoadMediaSourceAdapterPromise = Promise.resolve(); this._createVideoElement(playerId); this._init(source, config); } /** * Restores the engine. * @param {PKMediaSourceObject} source - The selected source object. * @param {Object} config - The player configuration. * @returns {void} */ public restore(source: PKMediaSourceObject, config: any): void { this.reset(); this._init(source, config); } /** * Resets the engine. * @returns {void} */ public reset(): void { if (this._reset) return; this._reset = true; this._eventManager.removeAll(); if (this._droppedFramesWatcher) { this._droppedFramesWatcher.destroy(); this._droppedFramesWatcher = undefined; } this._canLoadMediaSourceAdapterPromise = new Promise((resolve, reject) => { const mediaSourceAdapterDestroyed = this._mediaSourceAdapter ? this._mediaSourceAdapter.destroy() : Promise.resolve(); if (this._el && this._el.src) { mediaSourceAdapterDestroyed.then(() => { Utils.Dom.setAttribute(this._el, 'src', ''); Utils.Dom.removeAttribute(this._el, 'src'); resolve(); }, reject); } else { mediaSourceAdapterDestroyed.then(resolve, reject); } }); this._mediaSourceAdapter = null; } /** * Destroys the engine. * @public * @returns {void} */ public destroy(): void { this.detach(); if (this._el) { this.pause(); Utils.Dom.removeAttribute(this._el, 'src'); Utils.Dom.removeChild(this._el.parentNode, this._el); } this._eventManager.destroy(); MediaSourceProvider.destroy(); if (this._droppedFramesWatcher) { this._droppedFramesWatcher.destroy(); this._droppedFramesWatcher = undefined; } if (this._mediaSourceAdapter) { this._mediaSourceAdapter.setCachedUrls([]); this._mediaSourceAdapter.destroy(); this._mediaSourceAdapter = null; } } /** * Get the engine mediaSourceAdapter * @public * @returns {IMediaSourceAdapter | null} */ public get mediaSourceAdapter(): IMediaSourceAdapter | null { return this._mediaSourceAdapter; } /** * Get the engine's id * @public * @returns {string} the engine's id */ public get id(): string { return Html5.id; } /** * attach media - return the media source to handle the video tag * @public * @returns {void} */ public attachMediaSource(): void { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.attachMediaSource(); } } /** * detach media - will remove the media source from handling the video * @public * @returns {void} */ public detachMediaSource(): void { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.detachMediaSource(); } } /** * Listen to the video element events and triggers them from the engine. * @public * @returns {void} */ public attach(): void { 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])); }); } }); this._eventManager.listen(this._el, Html5EventType.ERROR, () => this._handleVideoError()); this._eventManager.listen(this._el, Html5EventType.WAITING, () => this._handleWaiting()); 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 })); } }); const mediaSourceAdapter = this._mediaSourceAdapter; if (mediaSourceAdapter) { this._eventManager.listen(mediaSourceAdapter, CustomEventType.VIDEO_TRACK_CHANGED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.AUDIO_TRACK_CHANGED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.TEXT_TRACK_CHANGED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.IMAGE_TRACK_CHANGED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.ABR_MODE_CHANGED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.TEXT_CUE_CHANGED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.TRACKS_CHANGED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.FRAG_LOADED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.DRM_LICENSE_LOADED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.MANIFEST_LOADED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, Html5EventType.ERROR, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, Html5EventType.TIME_UPDATE, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, Html5EventType.PLAYING, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, Html5EventType.WAITING, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.MEDIA_RECOVERED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.TIMED_METADATA_ADDED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, 'hlsFragParsingMetadata', (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.TIMELINE_REGION_ADDED, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.TIMELINE_REGION_ENTER, (event: FakeEvent) => this.dispatchEvent(event)); this._eventManager.listen(mediaSourceAdapter, CustomEventType.TIMELINE_REGION_EXIT, (event: FakeEvent) => this.dispatchEvent(event)); if (this._droppedFramesWatcher) { this._eventManager.listen(this._droppedFramesWatcher, CustomEventType.FPS_DROP, (event: FakeEvent) => this.dispatchEvent(event)); } } } /** * Remove the listeners of the video element events. * @public * @returns {void} */ public detach(): void { Object.keys(Html5EventType).forEach((html5Event) => { this._eventManager.unlisten(this._el, Html5EventType[html5Event]); }); if (this._mediaSourceAdapter) { this._eventManager.unlisten(this._mediaSourceAdapter, CustomEventType.VIDEO_TRACK_CHANGED); this._eventManager.unlisten(this._mediaSourceAdapter, CustomEventType.AUDIO_TRACK_CHANGED); this._eventManager.unlisten(this._mediaSourceAdapter, CustomEventType.TEXT_TRACK_CHANGED); this._eventManager.unlisten(this._mediaSourceAdapter, CustomEventType.TEXT_CUE_CHANGED); } } /** * @returns {HTMLVideoElement} - The video element. * @public */ public getVideoElement(): HTMLVideoElement { return this._el; } /** * Select a new video track. * @param {VideoTrack} videoTrack - The video track object to set. * @returns {void} */ public selectVideoTrack(videoTrack: VideoTrack): void { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.selectVideoTrack(videoTrack); } } /** * Select a new audio track. * @param {AudioTrack} audioTrack - The video track object to set. * @returns {void} */ public selectAudioTrack(audioTrack: AudioTrack): void { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.selectAudioTrack(audioTrack); } } /** * Select a new text track. * @param {PKTextTrack} textTrack - The playkit text track object to set. * @returns {void} */ public selectTextTrack(textTrack: PKTextTrack): void { this._removeCueChangeListeners(); if (this._mediaSourceAdapter) { this._mediaSourceAdapter.selectTextTrack(textTrack); } this.resetAllCues(); this._addCueChangeListener(); } /** * Select a new image track. * @param {ImageTrack} imageTrack - The image track object to set. * @returns {void} */ public selectImageTrack(imageTrack: ImageTrack): void { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.selectImageTrack(imageTrack); } } /** * Hide the text track * @function hideTextTrack * @returns {void} * @public */ public hideTextTrack(): void { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.hideTextTrack(); } this._removeCueChangeListeners(); } /** * Enables adaptive bitrate switching according to the media source extension logic. * @function enableAdaptiveBitrate * @returns {void} * @public */ public enableAdaptiveBitrate(): void { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.enableAdaptiveBitrate(); } } /** * Checking if adaptive bitrate switching is enabled. * @function isAdaptiveBitrateEnabled * @returns {boolean} - Whether adaptive bitrate is enabled. * @public */ public isAdaptiveBitrateEnabled(): boolean { if (this._mediaSourceAdapter) { return this._mediaSourceAdapter.isAdaptiveBitrateEnabled(); } return false; } /** * Apply ABR restriction * @function applyABRRestriction * @param {PKABRRestrictionObject} restriction - abr restriction config * @returns {void} * @public */ public applyABRRestriction(restriction: PKABRRestrictionObject): void { if (this._mediaSourceAdapter) { return this._mediaSourceAdapter.applyABRRestriction(restriction); } } /** * Seeking to live edge. * @function seekToLiveEdge * @returns {void} * @public */ public seekToLiveEdge(): void { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.seekToLiveEdge(); } } public isOnLiveEdge(): boolean { if (this._mediaSourceAdapter) { return this._mediaSourceAdapter.isOnLiveEdge(); } 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._mediaSourceAdapter ? this._mediaSourceAdapter.getStartTimeOfDvrWindow() : 0; } /** * Checking if the current playback is live. * @function isLive * @returns {boolean} - Whether playback is live. * @public */ public isLive(): boolean { return this._mediaSourceAdapter ? this._mediaSourceAdapter.isLive() : false; } /** * Checks if the video playback has ended and resets it if necessary. * Specifically handles Safari in Picture-in-Picture mode for non-live streams. * * @private * @returns {void} */ private resetIfPlaybackEnded(): void { const currentTimeMs = Math.round(this._el.currentTime * 1000); const durationMs = Math.round(this._el.duration * 1000); if (currentTimeMs >= durationMs || this._el.ended) { this._el.currentTime = 0; } } /** * Start/resume playback. * @public * @returns {?Promise<*>} - play promise */ public play(): Promise { if (Env.isSafari && this.isInPictureInPicture && !this.isLive()) { this.resetIfPlaybackEnded(); } const playPromise = this._el.play(); if (playPromise) { playPromise.catch((err) => this.dispatchEvent(new FakeEvent(CustomEventType.PLAY_FAILED, { error: err }))); } return playPromise; } /** * Pause playback. * @public * @returns {void} */ public pause(): void { return this._el.pause(); } /** * Load media. * @param {number} startTime - Optional time to start the video from. * @public * @returns {Promise} - The loaded data */ 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[] }); }) .catch((error) => { this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, error)); return Promise.reject(error); }); } /** * Request the engine to enter picture in picture mode * @public * @returns {void} */ public enterPictureInPicture(): void { try { // Currently it's supported in chrome and in safari. So if we consider checking support before, // 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))); }); // @ts-expect-error - Property 'webkitSetPresentationMode' does not exist on type 'HTMLVideoElement' } else if (typeof this._el.webkitSetPresentationMode === 'function') { // @ts-expect-error - Property 'webkitSetPresentationMode' does not exist on type 'HTMLVideoElement' this._el.webkitSetPresentationMode('picture-in-picture'); // Safari does not fire this event but Chrome does, normalizing the behaviour 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))); } } /** * Request the engine to exit picture in picture mode * @public * @returns {void} */ public exitPictureInPicture(): void { try { // Currently it's supported in chrome and in safari. So if we consider checking support before, // 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))); }); } 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))); } } /** * Check if the engine is in picture in picture mode * @public * @return {boolean} if the engine is in picture in picture mode or not */ public isPictureInPictureSupported(): boolean { // due to a bug in shaka pip_webkit which sets pictureInPictureEnabled to true in unsupported devices like iphones we will // first rely on the response of webkitSupportsPresentationMode (if exists) and only if not on the pictureInPictureEnabled property //@ts-expect-error - Property 'webkitSupportsPresentationMode' does not exist on type 'HTMLVideoElement'. if (typeof this._el.webkitSupportsPresentationMode === 'function') { //@ts-expect-error - Property 'webkitSupportsPresentationMode' does not exist on type 'HTMLVideoElement'. return this._el.webkitSupportsPresentationMode('picture-in-picture'); } else { return !!document.pictureInPictureEnabled; } } /** * 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._mediaSourceAdapter) { return this._mediaSourceAdapter.getThumbnail(time); } return null; } /** * Set a source. * @param {string} source - Source to set. * @public * @returns {void} */ public set src(source: string) { if (this._mediaSourceAdapter) { this._mediaSourceAdapter.src = source; } } /** * Get the source url. * @returns {string} - The source url. * @public */ public get src(): string { if (this._mediaSourceAdapter) { return this._mediaSourceAdapter.src; } return ''; } /** * Get the current time in seconds. * @returns {Number} - The current playback time. * @public */ public get currentTime(): number { return this._el ? this._el.currentTime : 0; } /** * Set the current time in seconds. * @param {Number} to - The number to set in seconds. * @public * @returns {void} */ public set currentTime(to: number) { if (this._el) { this._el.currentTime = to; } } /** * Get the duration in seconds. * @returns {Number} - The playback duration. * @public */ public get duration(): number { return this._el.duration; } public get liveDuration(): number { return this._mediaSourceAdapter ? this._mediaSourceAdapter.liveDuration : -1; } /** * Set playback volume. * @param {Number} vol - The volume to set. * @public * @returns {void} */ public set volume(vol: number) { this._el.volume = vol; } /** * Get playback volume. * @returns {Number} - The volume value of the video element. * @public */ public get volume(): number { return this._el.volume; } /** * Get paused state. * @returns {boolean} - The paused value of the video element. * @public */ public get paused(): boolean { return this._el.paused; } /** * Get seeking state. * @returns {boolean} - The seeking value of the video element. * @public */ public get seeking(): boolean { return this._el.seeking; } /** * Get the first seekable range (part) of the video in seconds. * @returns {TimeRanges} - First seekable range (part) of the video in seconds. * @public */ public get seekable(): TimeRanges { return this._el.seekable; } /** * Get the first played range (part) of the video in seconds. * @returns {TimeRanges} - First played range (part) of the video in seconds. * @public */ public get played(): TimeRanges { return this._el.played; } /** * Get the first buffered range (part) of the video in seconds. * @returns {TimeRanges} - First buffered range (part) of the video in seconds. * @public */ public get buffered(): TimeRanges { return this._el.buffered; } /** * Set player muted state. * @param {boolean} mute - The new mute value. * @public * @returns {void} */ public set muted(mute: boolean) { this._el.muted = mute; } /** * Get player muted state. * @returns {boolean} - The muted value of the video element. * @public */ public get muted(): boolean { return this._el.muted; } /** * Get the default mute value. * @returns {boolean} - The defaultMuted of the video element. * @public */ public get defaultMuted(): boolean { return this._el.defaultMuted; } /** * Sets an image to be shown while the video is downloading, or until the user hits the play button. * @param {string} poster - The image url to be shown. * @returns {void} * @public */ public set poster(poster: string) { this._el.poster = poster; } /** * Gets an image to be shown while the video is downloading, or until the user hits the play button. * @returns {poster} - The image url. * @public */ public get poster(): string { return this._el.poster; } /** * Specifies if and how the author thinks that the video should be loaded when the page loads. * @param {string} preload - The preload value. * @public * @returns {void} */ public set preload(preload: 'none' | 'metadata' | 'auto' | '') { this._el.preload = preload; } /** * Gets the preload value of the video element. * @returns {string} - The preload value. * @public */ public get preload(): 'none' | 'metadata' | 'auto' | '' { return this._el.preload; } /** * Set if the video will automatically start playing as soon as it can do so without stopping. * @param {boolean} autoplay - The autoplay value. * @public * @returns {void} */ public set autoplay(autoplay: boolean) { this._el.autoplay = autoplay; } /** * Gets the autoplay value of the video element. * @returns {boolean} - The autoplay value. * @public */ public get autoplay(): boolean { return this._el.autoplay; } /** * Set to specifies that the video will start over again, every time it is finished. * @param {boolean} loop - the loop value. * @public * @returns {void} */ public set loop(loop: boolean) { this._el.loop = loop; } /** * Gets the loop value of the video element. * @returns {boolean} - The loop value. * @public */ public get loop(): boolean { return this._el.loop; } /** * Set to specifies that video controls should be displayed. * @param {boolean} controls - the controls value. * @public * @returns {void} */ public set controls(controls: boolean) { this._el.controls = controls; } /** * Gets the controls value of the video element. * @returns {boolean} - The controls value. * @public */ public get controls(): boolean { return this._el.controls; } /** * Sets the current playback speed of the audio/video. * @param {Number} playbackRate - The playback speed value. * @public * @returns {void} */ public set playbackRate(playbackRate: number) { this._el.playbackRate = playbackRate; } /** * Gets the current playback speed of the audio/video. * @returns {Number} - The current playback speed value. * @public */ public get playbackRate(): number { return this._el.playbackRate; } /** * Sets the default playback speed of the audio/video. * @param {Number} defaultPlaybackRate - The default playback speed value. * @public * @returns {void} */ public set defaultPlaybackRate(defaultPlaybackRate: number) { this._el.defaultPlaybackRate = defaultPlaybackRate; } /** * Gets the default playback speed of the audio/video. * @returns {Number} - The default playback speed value. * @public */ public get defaultPlaybackRate(): number { return this._el.defaultPlaybackRate; } /** * The ended property returns whether the playback of the audio/video has ended. * @returns {boolean} - The ended value. * @public */ public get ended(): boolean { return this._el.ended; } /** * The error property returns a MediaError object. * @returns {MediaError} - The MediaError object has a code property containing the error state of the audio/video. * @public */ public get error(): MediaError | null { return this._el.error; } /** * @returns {Number} - The current network state (activity) of the audio/video. * @public */ public get networkState(): number { return this._el.networkState; } /** * Indicates if the audio/video is ready to play or not. * @returns {Number} - The current ready state of the audio/video. * 0 = HAVE_NOTHING - no information whether or not the audio/video is ready. * 1 = HAVE_METADATA - metadata for the audio/video is ready. * 2 = HAVE_CURRENT_DATA - data for the current playback position is available, but not enough data to play next frame/millisecond. * 3 = HAVE_FUTURE_DATA - data for the current and at least the next frame is available. * 4 = HAVE_ENOUGH_DATA - enough data available to start playing. */ public get readyState(): number { return this._el.readyState; } /** * @returns {Number} - The height of the video player, in pixels. * @public */ public get videoHeight(): number { return this._el.videoHeight; } /** * @returns {Number} - The width of the video player, in pixels. * @public */ public get videoWidth(): number { return this._el.videoWidth; } /** * @param {boolean} playsinline - Whether to set on the video tag the playsinline attribute. */ public set playsinline(playsinline: boolean) { if (playsinline) { this._el.setAttribute('playsinline', ''); } else { this._el.removeAttribute('playsinline'); } } /** * @returns {boolean} - Whether the video tag has an attribute of playsinline. */ public get playsinline(): boolean { return this._el.getAttribute('playsinline') === ''; } /** * Set crossOrigin attribute. * @param {?string} crossOrigin - 'anonymous' or 'use-credentials' */ public set crossOrigin(crossOrigin: string) { if (typeof crossOrigin === 'string') { this._el.setAttribute('crossorigin', crossOrigin); } else { this._el.removeAttribute('crossorigin'); } } /** * Get crossOrigin attribute. * @returns {?string} - 'anonymous' or 'use-credentials' */ public get crossOrigin(): string | null { return this._el.getAttribute('crossorigin'); } /** * get the playback rates * @return {number[]} - playback rates */ public get playbackRates(): Array { return Html5.PLAYBACK_RATES; } /** * get if the engine's video element is the one in the PIP * @return {boolean} boolean - is in PIP */ public get isInPictureInPicture(): boolean { // Check if the engine's video element is the one in the PIP return ( (!!document.pictureInPictureElement && document.pictureInPictureElement !== null && this._el === document.pictureInPictureElement) || // @ts-expect-error - Property 'webkitPresentationMode' does not exist on type 'HTMLVideoElement' (!!this._el.webkitPresentationMode && this._el.webkitPresentationMode === 'picture-in-picture') ); } /** * Initializes the engine. * @param {PKMediaSourceObject} source - The selected source object. * @param {Object} config - The player configuration. * @private * @returns {void} */ private _init(source: PKMediaSourceObject, config: any): void { this._config = config; this._reset = false; this._loadMediaSourceAdapter(source); this.attach(); } /** * Creates a video element dom object. * @param {string} playerId - the id to be set as the key of the video element * @private * @returns {void} */ private _createVideoElement(playerId: string): void { this._el = Html5.videoElementStore[playerId] || Utils.Dom.createElement('video'); this._el.id = Utils.Generator.uniqueId(5); this._el.controls = false; } /** * Loads the appropriate media source extension adapter. * @param {PKMediaSourceObject} source - The selected source object. * @private * @returns {void} */ private _loadMediaSourceAdapter(source: PKMediaSourceObject): void { 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 = []; } } } /** * Add cuechange listener to active textTrack. * @returns {void} * @private */ private _addCueChangeListener(): void { 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)); } } /** * Remove cuechange listeners from textTracks * @returns {void} * @private */ private _removeCueChangeListeners(): void { Array.from(this._el.textTracks) .filter((track) => !PKTextTrack.isMetaDataTrack(track)) .forEach((track) => { this._eventManager.unlisten(track, 'cuechange'); }); } /** * oncuechange event handler. * @param {FakeEvent} e - The event arg. * @returns {void} * @private */ 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 })); } /** * set hasBeenReset to true for all the cues. (use case: when cues should be recalculated for display) * @returns {void} */ public resetAllCues(): void { 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' activeTextTrack.cues![i].hasBeenReset = true; } } } /** * Handles errors from the video element * @returns {void} * @private */ private _handleVideoError(): void { if (!this._el.error) return; const code = this._el.error.code; if (code === MediaError.MEDIA_ERR_ABORTED) { // Ignore this error code.js, which should only occur when navigating away or // deliberately stopping playback of HTTP content. return; } // Extra error information from MS Edge and IE11: const extended = this._getMsExtendedError(); // Extra error information from Chrome: // $FlowFixMe const message = this._el.error.message; if (this._mediaSourceAdapter && !this._mediaSourceAdapter.handleMediaError(this._el.error)) { const error = new Error(Error.Severity.CRITICAL, Error.Category.MEDIA, Error.Code.VIDEO_ERROR, { code: code, extended: extended, message: message }); this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, error)); } } private _handleWaiting(): void { let playing = false; this._eventManager.listenOnce(this._el, Html5EventType.PLAYING, () => (playing = true)); setTimeout(() => { // prevent sending waiting event for too short buffering if (!playing) { this.dispatchEvent(new FakeEvent(Html5EventType.WAITING)); } }, SHORT_BUFFERING_TIMEOUT); } /** * more info about the error * @returns {string} info about the video element error * @private */ private _getMsExtendedError(): string { //@ts-expect-error - Property 'msExtendedCode' does not exist on type 'MediaError' let extended = this._el.error.msExtendedCode; if (extended) { // Convert to unsigned: if (extended < 0) { extended += Math.pow(2, 32); } // Format as hex: extended = extended.toString(16); } return extended; } private _handleMetadataTrackEvents(): void { const listenToCueChange = (metadataTrack: any): void => { metadataTrack.mode = PKTextTrack.MODE.HIDDEN; this._eventManager.listen(metadataTrack, 'cuechange', () => { let activeCues: VTTCue[] = []; Array.from(this._el.textTracks).forEach((track: TextTrack) => { if (PKTextTrack.isMetaDataTrack(track)) { activeCues = activeCues.concat(getActiveCues(track.activeCues!)); } }); 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_CHANGE, { cues: activeCues.map((cue) => createTimedMetadata(cue)) }) ); }); }; Array.from(this._el.textTracks).forEach((track: TextTrack) => { if (PKTextTrack.isMetaDataTrack(track)) { listenToCueChange(track); } }); this._eventManager.listen(this._el.textTracks, 'addtrack', (event: any) => { if (PKTextTrack.isMetaDataTrack(event.track)) { listenToCueChange(event.track); } }); this._eventManager.listen(this._el.textTracks, 'change', () => { Array.from(this._el.textTracks).forEach((track: TextTrack) => { if (PKTextTrack.isMetaDataTrack(track) && track.mode !== PKTextTrack.MODE.HIDDEN) { track.mode = PKTextTrack.MODE.HIDDEN; } }); }); } public get targetBuffer(): number { if (this._mediaSourceAdapter) { return this._mediaSourceAdapter.targetBuffer; } return NaN; } public get availableBuffer(): number { let retVal = 0; if (this.buffered) { for (let i = 0; i < this.buffered.length; i++) { // find the relevant buffer time range containing the current time if (this.buffered.start(i) <= this._el.currentTime && this._el.currentTime <= this.buffered.end(i)) { retVal = this.buffered.end(i) - this._el.currentTime; } } } return retVal; } public addTextTrack(kind: TextTrackKind, label?: string, language?: string): TextTrack | undefined { return this._el.addTextTrack(kind, label, language); } /** * get the native text tracks * @function getNativeTextTracks * @returns {Array} - The native TextTracks array. * @public */ public getNativeTextTracks(): TextTrack[] { return Array.from(this._el.textTracks); } 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 = []; } } }