Skip to content

Commit

Permalink
feat: Add preload system to player (#5897)
Browse files Browse the repository at this point in the history
Adds a new player method, preload. This asynchronous method creates a PreloadManager object, which
will preload data for the given manifest, and which can be passed to the load method (in place of an asset URI)
in order to apply that preloaded data. This will allow for lower load latency; if you can predict what asset will
be loaded ahead of time (say, by preloading things the user is hovering their mouse over in a menu),
you can load the manifest before the user presses the load button.
Note that PreloadManagers are only meant to be used by the player instance that created them.

Closes #880

Co-authored-by: Álvaro Velad Galván <[email protected]>
  • Loading branch information
theodab and avelad authored Feb 2, 2024
1 parent 8fc292b commit 489b11a
Show file tree
Hide file tree
Showing 18 changed files with 1,861 additions and 735 deletions.
2 changes: 2 additions & 0 deletions build/types/core
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
+../../lib/media/content_workarounds.js
+../../lib/media/drm_engine.js
+../../lib/media/gap_jumping_controller.js
+../../lib/media/manifest_filterer.js
+../../lib/media/manifest_parser.js
+../../lib/media/media_source_capabilities.js
+../../lib/media/media_source_engine.js
+../../lib/media/mp4_segment_index_parser.js
+../../lib/media/play_rate_controller.js
+../../lib/media/playhead.js
+../../lib/media/playhead_observer.js
+../../lib/media/preload_manager.js
+../../lib/media/presentation_timeline.js
+../../lib/media/quality_observer.js
+../../lib/media/region_observer.js
Expand Down
47 changes: 47 additions & 0 deletions demo/asset_card.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,53 @@ shakaDemo.AssetCard = class {
this.remakeButtonsFn_(this);
}

/** Adds basic buttons to the card ("play" and "preload"). */
addBaseButtons() {
let disableButtons = false;
this.addButton('Play', async () => {
if (disableButtons) {
return;
}
disableButtons = true;
await shakaDemoMain.loadAsset(this.asset_);
this.remakeButtons();
});
let preloadName = 'Start Preload';
if (this.asset_.preloadManager) {
preloadName = this.asset_.preloaded ? 'Preloaded!' : 'Preloading...';
} else if (this.asset_.preloadFailed) {
preloadName = 'Failed to Preload!';
}
const preloadButton = this.addButton(preloadName, async () => {
if (disableButtons) {
return;
}
disableButtons = true;
this.asset_.preloaded = false;
if (this.asset_.preloadManager) {
await this.asset_.preloadManager.destroy();
this.asset_.preloadManager = null;
this.remakeButtons();
} else {
try {
await shakaDemoMain.preloadAsset(this.asset_);
this.remakeButtons();
await this.asset_.preloadManager.waitForFinish();
this.asset_.preloaded = true;
} catch (error) {
this.asset_.preloadManager = null;
this.asset_.preloadFailed = true;
throw error;
} finally {
this.remakeButtons();
}
}
});
if (this.asset_.preloadFailed) {
preloadButton.disabled = true;
}
}

/**
* Adds a button to the bottom of the card that controls storage behavior.
* This is a separate function because it involves a significant amount of
Expand Down
12 changes: 12 additions & 0 deletions demo/common/asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ const ShakaDemoAssetInfo = class {
this.mimeType = null;


// Preload values.
/** @type {?shaka.media.PreloadManager} */
this.preloadManager;
this.preloaded = false;
this.preloadFailed = false;


// Offline storage values.
/** @type {?function()} */
this.storeCallback;
Expand Down Expand Up @@ -399,6 +406,11 @@ const ShakaDemoAssetInfo = class {
// proper formatting.
const raw = {};
for (const key in this) {
if (key.startsWith('preload') || key.startsWith('store') ||
key.endsWith('Callback')) {
// These values shouldn't be saved, as they are dynamic.
continue;
}
const value = this[key];
if (value instanceof Map) {
// The built-in JSON functions cannot convert Maps; this converts Maps
Expand Down
5 changes: 1 addition & 4 deletions demo/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -1045,10 +1045,7 @@ shakaDemo.Custom = class {
const savedList = this.savedList_;
const isFeatured = false;
return new shakaDemo.AssetCard(savedList, asset, isFeatured, (c) => {
c.addButton('Play', () => {
shakaDemoMain.loadAsset(asset);
this.updateSelected_();
});
c.addBaseButtons();
c.addButton('Edit', async () => {
if (asset.unstoreCallback) {
await asset.unstoreCallback();
Expand Down
5 changes: 1 addition & 4 deletions demo/front.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,7 @@ shakaDemo.Front = class {
if (unsupportedReason) {
c.markAsUnsupported(unsupportedReason);
} else {
c.addButton('Play', () => {
shakaDemoMain.loadAsset(asset);
this.updateSelected_();
});
c.addBaseButtons();
c.addStoreButton();
}
});
Expand Down
63 changes: 44 additions & 19 deletions demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,40 @@ shakaDemo.Main = class {
videoBar.scrollIntoView({behavior: 'smooth', block: 'start'});
}

/**
* @param {ShakaDemoAssetInfo} asset
*/
async preloadAsset(asset) {
await this.drmConfiguration_(asset);
const manifestUri = await this.getManifestUri_(asset);
asset.preloadManager = await this.player_.preload(manifestUri);
}

/**
* @param {ShakaDemoAssetInfo} asset
* @return {!Promise.<string>}
* @private
*/
async getManifestUri_(asset) {
let manifestUri = asset.manifestUri;
// If we have an offline copy, use that. If the offlineUri field is null,
// we are still downloading it.
if (asset.storedContent && asset.storedContent.offlineUri) {
manifestUri = asset.storedContent.offlineUri;
}
// If it's a server side dai asset, request ad-containing manifest
// from the ad manager.
if (asset.imaAssetKey || (asset.imaContentSrcId && asset.imaVideoId)) {
manifestUri = await this.getManifestUriFromAdManager_(asset);
}
// If it's a MediaTailor asset, request ad-containing manifest
// from the ad manager.
if (asset.mediaTailorUrl) {
manifestUri = await this.getManifestUriFromMediaTailorAdManager_(asset);
}
return manifestUri;
}

/**
* @param {ShakaDemoAssetInfo} asset
*/
Expand Down Expand Up @@ -1262,26 +1296,17 @@ shakaDemo.Main = class {
this.controls_.getCastProxy().setAppData({'asset': asset});

// Finally, the asset can be loaded.
let manifestUri = asset.manifestUri;
// If we have an offline copy, use that. If the offlineUri field is null,
// we are still downloading it.
if (asset.storedContent && asset.storedContent.offlineUri) {
manifestUri = asset.storedContent.offlineUri;
}
// If it's a server side dai asset, request ad-containing manifest
// from the ad manager.
if (asset.imaAssetKey || (asset.imaContentSrcId && asset.imaVideoId)) {
manifestUri = await this.getManifestUriFromAdManager_(asset);
}
// If it's a MediaTailor asset, request ad-containing manifest
// from the ad manager.
if (asset.mediaTailorUrl) {
manifestUri = await this.getManifestUriFromMediaTailorAdManager_(asset);
if (asset.preloadManager) {
const preloadManager = asset.preloadManager;
asset.preloadManager = null;
await this.player_.load(preloadManager);
} else {
const manifestUri = await this.getManifestUri_(asset);
await this.player_.load(
manifestUri,
/* startTime= */ null,
asset.mimeType || undefined);
}
await this.player_.load(
manifestUri,
/* startTime= */ null,
asset.mimeType || undefined);

if (this.player_.isAudioOnly() &&
this.video_.poster == shakaDemo.Main.mainPoster_) {
Expand Down
5 changes: 1 addition & 4 deletions demo/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,7 @@ shakaDemo.Search = class {
if (unsupportedReason) {
c.markAsUnsupported(unsupportedReason);
} else {
c.addButton('Play', () => {
shakaDemoMain.loadAsset(asset);
this.updateSelected_();
});
c.addBaseButtons();
c.addStoreButton();
}
});
Expand Down
9 changes: 6 additions & 3 deletions lib/media/drm_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ goog.require('shaka.util.Uint8ArrayUtils');
shaka.media.DrmEngine = class {
/**
* @param {shaka.media.DrmEngine.PlayerInterface} playerInterface
* @param {number=} updateExpirationTime
*/
constructor(playerInterface, updateExpirationTime = 1) {
constructor(playerInterface) {
/** @private {?shaka.media.DrmEngine.PlayerInterface} */
this.playerInterface_ = playerInterface;

Expand Down Expand Up @@ -125,7 +124,7 @@ shaka.media.DrmEngine = class {
/** @private {?shaka.util.Timer} */
this.expirationTimer_ = new shaka.util.Timer(() => {
this.pollExpiration_();
}).tickEvery(/* seconds= */ updateExpirationTime);
});

// Add a catch to the Promise to avoid console logs about uncaught errors.
const noop = () => {};
Expand Down Expand Up @@ -210,6 +209,10 @@ shaka.media.DrmEngine = class {
*/
configure(config) {
this.config_ = config;
if (this.expirationTimer_) {
this.expirationTimer_.tickEvery(
/* seconds= */ this.config_.updateExpirationTime);
}
}

/**
Expand Down
Loading

0 comments on commit 489b11a

Please sign in to comment.