Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FEC-13946): Player core - Use shaka preload mechanism during playlist playback #780

Merged
merged 7 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading