Skip to content

Commit

Permalink
feat(FEC-13946): Player core - Use shaka preload mechanism during pla…
Browse files Browse the repository at this point in the history
…ylist playback (#780)

### Description of the Changes

- Add setCachedUrls API
- Pass the arguments to the adapter setCachedUrls API once an adapter is
available

Related PRs:
kaltura/playkit-js-dash#262
kaltura/kaltura-player-js#792

Resolves FEC-13946
  • Loading branch information
SivanA-Kaltura authored Jun 23, 2024
1 parent 16e2d47 commit 9e16ef3
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 214 deletions.
118 changes: 57 additions & 61 deletions src/engines/html5/html5.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -59,6 +59,9 @@ export default class Html5 extends FakeEventTarget implements IEngine {
private _canLoadMediaSourceAdapterPromise: Promise<void>;
private _droppedFramesWatcher: DroppedFramesWatcher | undefined;
private _reset: boolean = false;

private _cachedUrls: string[] = [];

/**
* The html5 class logger.
* @type {any}
Expand Down Expand Up @@ -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());
}

/**
Expand All @@ -142,13 +145,13 @@ export default class Html5 extends FakeEventTarget implements IEngine {
* @public
* @static
*/
public static getCapabilities(): Promise<any> {
public static getCapabilities(): Promise<any> {
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 };
});
}

Expand All @@ -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));
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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]));
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -517,7 +521,7 @@ export default class Html5 extends FakeEventTarget implements IEngine {
public play(): Promise<void> {
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;
}
Expand All @@ -537,13 +541,13 @@ export default class Html5 extends FakeEventTarget implements IEngine {
* @public
* @returns {Promise<Object>} - 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);
});
Expand All @@ -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') {
Expand All @@ -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)));
}
}

Expand All @@ -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)));
}
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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 = [];
}
}
}

Expand All @@ -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));
}
Expand All @@ -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');
});
}
Expand All @@ -1118,17 +1107,15 @@ 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 }));
}

/**
* 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
);
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'
Expand Down Expand Up @@ -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))
})
);
});
Expand Down Expand Up @@ -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 = [];
}
}
}
4 changes: 3 additions & 1 deletion src/engines/html5/media-source/base-media-source-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -344,4 +344,6 @@ export default class BaseMediaSourceAdapter extends FakeEventTarget implements I
public getDrmInfo(): PKDrmDataObject | null {
return null;
}

public setCachedUrls(): void {}
}
Loading

0 comments on commit 9e16ef3

Please sign in to comment.