Skip to content

Commit

Permalink
feat(Offline): Allow downloading some clearkey content without persis…
Browse files Browse the repository at this point in the history
…tent license support (#7811)

Cases:
- Configured `drm.clearKeys`
- HLS SAMPLE-AES
  • Loading branch information
avelad authored Dec 31, 2024
1 parent fe1f35b commit 50a1851
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 52 deletions.
7 changes: 7 additions & 0 deletions demo/common/asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@ const ShakaDemoAssetInfo = class {
return this;
}

/**
* @return {!Map.<string, string>}
*/
getLicenseServers() {
return this.licenseServers;
}

/**
* @param {string} keySystem
* @param {string} licenseServer
Expand Down
7 changes: 7 additions & 0 deletions demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,13 @@ shakaDemo.Main = class {
if (needOffline) {
const hasSupportedOfflineDRM = asset.drm.some((drm) => {
const identifier = shakaAssets.identifierForKeySystem(drm);
// Special case when using clear keys.
if (identifier == 'org.w3.clearkey') {
const licenseServers = asset.getLicenseServers();
if (!licenseServers.has(identifier)) {
return this.support_.drm[identifier];
}
}
return this.support_.drm[identifier] &&
this.support_.drm[identifier].persistentState;
});
Expand Down
58 changes: 26 additions & 32 deletions lib/drm/drm_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,21 +355,7 @@ shaka.drm.DrmEngine = class {
goog.asserts.assert(this.config_,
'DrmEngine configure() must be called before init()!');

// ClearKey config overrides the manifest DrmInfo if present. The variants
// are modified so that filtering in Player still works.
// This comes before hadDrmInfo because it influences the value of that.
/** @type {?shaka.extern.DrmInfo} */
const clearKeyDrmInfo = this.configureClearKey_();
if (clearKeyDrmInfo) {
for (const variant of variants) {
if (variant.video) {
variant.video.drmInfos = [clearKeyDrmInfo];
}
if (variant.audio) {
variant.audio.drmInfos = [clearKeyDrmInfo];
}
}
}
shaka.drm.DrmEngine.configureClearKey(this.config_.clearKeys, variants);

const hadDrmInfo = variants.some((variant) => {
if (variant.video && variant.video.drmInfos.length) {
Expand Down Expand Up @@ -1221,23 +1207,6 @@ shaka.drm.DrmEngine = class {
return mediaKeySystemAccess;
}

/**
* Create a DrmInfo using configured clear keys.
* The server URI will be a data URI which decodes to a clearkey license.
* @return {?shaka.extern.DrmInfo} or null if clear keys are not configured.
* @private
* @see https://bit.ly/2K8gOnv for the spec on the clearkey license format.
*/
configureClearKey_() {
const clearKeys = shaka.util.MapUtils.asMap(this.config_.clearKeys);
if (clearKeys.size == 0) {
return null;
}

const ManifestParserUtils = shaka.util.ManifestParserUtils;
return ManifestParserUtils.createDrmInfoFromClearKeys(clearKeys);
}

/**
* Resolves the allSessionsLoaded_ promise when all the sessions are loaded
*
Expand Down Expand Up @@ -2698,6 +2667,31 @@ shaka.drm.DrmEngine = class {
this.newInitData('cenc', combinedData);
return this.allSessionsLoaded_;
}

/**
* Create a DrmInfo using configured clear keys and assign it to each variant.
* Only modify variants if clear keys have been set.
* @see https://bit.ly/2K8gOnv for the spec on the clearkey license format.
*
* @param {!Object.<string, string>} configClearKeys
* @param {!Array.<shaka.extern.Variant>} variants
*/
static configureClearKey(configClearKeys, variants) {
const clearKeys = shaka.util.MapUtils.asMap(configClearKeys);
if (clearKeys.size == 0) {
return;
}
const clearKeyDrmInfo =
shaka.util.ManifestParserUtils.createDrmInfoFromClearKeys(clearKeys);
for (const variant of variants) {
if (variant.video) {
variant.video.drmInfos = [clearKeyDrmInfo];
}
if (variant.audio) {
variant.audio.drmInfos = [clearKeyDrmInfo];
}
}
}
};


Expand Down
74 changes: 54 additions & 20 deletions lib/offline/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,11 +394,38 @@ shaka.offline.Storage = class {
this.ensureNotDestroyed_();
}

shaka.drm.DrmEngine.configureClearKey(
config.drm.clearKeys, manifest.variants);

const clearKeyDataLicenseServerUri = manifest.variants.some((v) => {
if (v.audio) {
for (const drmInfo of v.audio.drmInfos) {
if (!drmInfo.licenseServerUri.startsWith('data:')) {
return true;
}
}
}
if (v.video) {
for (const drmInfo of v.video.drmInfos) {
if (!drmInfo.licenseServerUri.startsWith('data:')) {
return true;
}
}
}
return false;
});

let usePersistentLicense = config.offline.usePersistentLicense;
if (clearKeyDataLicenseServerUri) {
usePersistentLicense = false;
}

// Create the DRM engine, and load the keys in the manifest.
drmEngine = await this.createDrmEngine(
manifest,
(e) => { drmError = drmError || e; },
config);
config,
usePersistentLicense);

// We could have been asked to destroy ourselves while we were "away"
// creating the drm engine.
Expand All @@ -407,7 +434,8 @@ shaka.offline.Storage = class {
throw drmError;
}

await this.filterManifest_(manifest, drmEngine, config);
await this.filterManifest_(
manifest, drmEngine, config, usePersistentLicense);

await muxer.init();
this.ensureNotDestroyed_();
Expand All @@ -420,7 +448,8 @@ shaka.offline.Storage = class {

goog.asserts.assert(drmEngine, 'drmEngine should be non-null here.');
const {manifestDB, toDownload} = this.makeManifestDB_(
drmEngine, manifest, uri, appMetadata, config, downloader);
drmEngine, manifest, uri, appMetadata, config, downloader,
usePersistentLicense);

// Store the empty manifest, before downloading the segments.
const ids = await activeHandle.cell.addManifests([manifestDB]);
Expand All @@ -434,10 +463,12 @@ shaka.offline.Storage = class {
}

await this.downloadSegments_(toDownload, manifestId, manifestDB,
downloader, config, activeHandle.cell, manifest, drmEngine);
downloader, config, activeHandle.cell, manifest, drmEngine,
usePersistentLicense);
this.ensureNotDestroyed_();

this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
this.setManifestDrmFields_(
manifest, manifestDB, drmEngine, usePersistentLicense);
await activeHandle.cell.updateManifest(manifestId, manifestDB);
this.ensureNotDestroyed_();

Expand Down Expand Up @@ -479,12 +510,13 @@ shaka.offline.Storage = class {
* @param {shaka.extern.StorageCell} storage
* @param {shaka.extern.Manifest} manifest
* @param {!shaka.drm.DrmEngine} drmEngine
* @param {boolean} usePersistentLicense
* @return {!Promise}
* @private
*/
async downloadSegments_(
toDownload, manifestId, manifestDB, downloader, config, storage,
manifest, drmEngine) {
manifest, drmEngine, usePersistentLicense) {
let pendingManifestUpdates = {};
let pendingDataSize = 0;

Expand Down Expand Up @@ -531,7 +563,8 @@ shaka.offline.Storage = class {
// process of downloading and storing, and assignSegmentsToManifest
// does not know about the DRM engine.
this.ensureNotDestroyed_();
this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
this.setManifestDrmFields_(
manifest, manifestDB, drmEngine, usePersistentLicense);
await storage.updateManifest(manifestId, manifestDB);
}
};
Expand Down Expand Up @@ -696,10 +729,11 @@ shaka.offline.Storage = class {
* @param {shaka.extern.Manifest} manifest
* @param {!shaka.drm.DrmEngine} drmEngine
* @param {shaka.extern.PlayerConfiguration} config
* @param {boolean} usePersistentLicense
* @return {!Promise}
* @private
*/
async filterManifest_(manifest, drmEngine, config) {
async filterManifest_(manifest, drmEngine, config, usePersistentLicense) {
// Filter the manifest based on the restrictions given in the player
// configuration.
const maxHwRes = {width: Infinity, height: Infinity};
Expand All @@ -709,7 +743,7 @@ shaka.offline.Storage = class {
// Filter the manifest based on what we know MediaCapabilities will be able
// to play later (no point storing something we can't play).
await shaka.util.StreamUtils.filterManifestByMediaCapabilities(
drmEngine, manifest, config.offline.usePersistentLicense,
drmEngine, manifest, usePersistentLicense,
config.drm.preferredKeySystems, config.drm.keySystemsMapping);

// Gather all tracks.
Expand Down Expand Up @@ -815,13 +849,15 @@ shaka.offline.Storage = class {
* @param {!Object} metadata
* @param {shaka.extern.PlayerConfiguration} config
* @param {!shaka.offline.DownloadManager} downloader
* @param {boolean} usePersistentLicense
* @return {{
* manifestDB: shaka.extern.ManifestDB,
* toDownload: !Array.<!shaka.offline.DownloadInfo>
* }}
* @private
*/
makeManifestDB_(drmEngine, manifest, uri, metadata, config, downloader) {
makeManifestDB_(drmEngine, manifest, uri, metadata, config, downloader,
usePersistentLicense) {
const pendingContent = shaka.offline.StoredContentUtils.fromManifest(
uri, manifest, /* size= */ 0, metadata);
// In https://github.com/shaka-project/shaka-player/issues/2652, we found
Expand All @@ -837,7 +873,7 @@ shaka.offline.Storage = class {
progressCallback(pendingContent, progress);
};
const onInitData = (initData, systemId) => {
if (needsInitData && config.offline.usePersistentLicense &&
if (needsInitData && usePersistentLicense &&
currentSystemId == systemId) {
drmEngine.newInitData('cenc', initData);
}
Expand Down Expand Up @@ -869,7 +905,6 @@ shaka.offline.Storage = class {
downloader, estimator, drmEngine, manifest, config);

const drmInfo = drmEngine.getDrmInfo();
const usePersistentLicense = config.offline.usePersistentLicense;
if (drmInfo && usePersistentLicense) {
// Don't store init data, since we have stored sessions.
drmInfo.initData = [];
Expand Down Expand Up @@ -926,18 +961,17 @@ shaka.offline.Storage = class {
* @param {shaka.extern.Manifest} manifest
* @param {shaka.extern.ManifestDB} manifestDB
* @param {!shaka.drm.DrmEngine} drmEngine
* @param {shaka.extern.PlayerConfiguration} config
* @param {boolean} usePersistentLicense
* @private
*/
setManifestDrmFields_(manifest, manifestDB, drmEngine, config) {
setManifestDrmFields_(manifest, manifestDB, drmEngine, usePersistentLicense) {
manifestDB.expiration = drmEngine.getExpiration();

const sessions = drmEngine.getSessionIds();
manifestDB.sessionIds = config.offline.usePersistentLicense ?
sessions : [];
manifestDB.sessionIds = usePersistentLicense ? sessions : [];

if (this.getManifestIsEncrypted_(manifest) &&
config.offline.usePersistentLicense && !sessions.length) {
usePersistentLicense && !sessions.length) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
Expand Down Expand Up @@ -1498,9 +1532,10 @@ shaka.offline.Storage = class {
* @param {shaka.extern.Manifest} manifest
* @param {function(shaka.util.Error)} onError
* @param {shaka.extern.PlayerConfiguration} config
* @param {boolean} usePersistentLicense
* @return {!Promise.<!shaka.drm.DrmEngine>}
*/
async createDrmEngine(manifest, onError, config) {
async createDrmEngine(manifest, onError, config, usePersistentLicense) {
goog.asserts.assert(
this.networkingEngine_,
'Cannot call |createDrmEngine| after |destroy|');
Expand All @@ -1515,8 +1550,7 @@ shaka.offline.Storage = class {
});

drmEngine.configure(config.drm);
await drmEngine.initForStorage(
manifest.variants, config.offline.usePersistentLicense);
await drmEngine.initForStorage(manifest.variants, usePersistentLicense);
await drmEngine.createOrLoad();

return drmEngine;
Expand Down

0 comments on commit 50a1851

Please sign in to comment.