diff --git a/demo/offline_section.js b/demo/offline_section.js index cb1d67eec5..52df1c5fcd 100644 --- a/demo/offline_section.js +++ b/demo/offline_section.js @@ -202,23 +202,33 @@ shakaDemo.storeDeleteAsset_ = function() { return shakaDemo.refreshAssetList_(); }); } else { + let configureCertificate = Promise.resolve(); + let asset = shakaDemo.preparePlayer_(option.asset); - let nameField = document.getElementById('offlineName').value; - let assetName = asset.name ? '[OFFLINE] ' + asset.name : null; - let metadata = {name: assetName || nameField || asset.manifestUri}; - p = storage.store(asset.manifestUri, metadata).then(function() { - if (option.asset) { - option.isStored = true; - } - return shakaDemo.refreshAssetList_().then(function() { - // Auto-select offline copy of asset after storing. - let group = shakaDemo.offlineOptGroup_; - for (let i = 0; i < group.childNodes.length; i++) { - let option = group.childNodes[i]; - if (option.textContent == assetName) { - assetList.selectedIndex = option.index; - } + + if (asset.certificateUri) { + configureCertificate = shakaDemo.requestCertificate_(asset.certificateUri) + .then(shakaDemo.configureCertificate_); + } + + p = configureCertificate.then(function() { + let nameField = document.getElementById('offlineName').value; + let assetName = asset.name ? '[OFFLINE] ' + asset.name : null; + let metadata = {name: assetName || nameField || asset.manifestUri}; + return storage.store(asset.manifestUri, metadata).then(function() { + if (option.asset) { + option.isStored = true; } + return shakaDemo.refreshAssetList_().then(function() { + // Auto-select offline copy of asset after storing. + let group = shakaDemo.offlineOptGroup_; + for (let i = 0; i < group.childNodes.length; i++) { + let option = group.childNodes[i]; + if (option.textContent == assetName) { + assetList.selectedIndex = option.index; + } + } + }); }); }); } diff --git a/externs/shaka/offline.js b/externs/shaka/offline.js index ef56c1e111..27857496ab 100644 --- a/externs/shaka/offline.js +++ b/externs/shaka/offline.js @@ -212,6 +212,7 @@ shaka.extern.SegmentDataDB; * sessionId: string, * keySystem: string, * licenseUri: string, + * serverCertificate: Uint8Array, * audioCapabilities: !Array., * videoCapabilities: !Array. * }} @@ -222,6 +223,10 @@ shaka.extern.SegmentDataDB; * The EME key system string the session belongs to. * @property {string} licenseUri * The URI for the license server. + * @property {Uint8Array} serverCertificate + * A key-system-specific server certificate used to encrypt license requests. + * Its use is optional and is meant as an optimization to avoid a round-trip + * to request a certificate. * @property {!Array.} audioCapabilities * The EME audio capabilities used to create the session. * @property {!Array.} videoCapabilities diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index fd4d995da1..607a296e51 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -307,12 +307,14 @@ shaka.media.DrmEngine.prototype.initForPlayback = function( * * @param {string} keySystem * @param {string} licenseServerUri + * @param {Uint8Array} serverCertificate * @param {!Array.} audioCapabilities * @param {!Array.} videoCapabilities * @return {!Promise} */ shaka.media.DrmEngine.prototype.initForRemoval = function( - keySystem, licenseServerUri, audioCapabilities, videoCapabilities) { + keySystem, licenseServerUri, serverCertificate, + audioCapabilities, videoCapabilities) { /** @type {!Map.} */ const configsByKeySystem = new Map(); configsByKeySystem.set(keySystem, { @@ -329,7 +331,7 @@ shaka.media.DrmEngine.prototype.initForRemoval = function( persistentStateRequired: true, audioRobustness: '', // Not required by queryMediaKeys_ videoRobustness: '', // Same - serverCertificate: null, + serverCertificate: serverCertificate, initData: null, keyIds: null, }], // Tracked by us, ignored by EME. @@ -439,23 +441,7 @@ shaka.media.DrmEngine.prototype.attach = function(video) { exception.message)); }); - let setServerCertificate = null; - if (this.currentDrmInfo_.serverCertificate && - this.currentDrmInfo_.serverCertificate.length) { - setServerCertificate = this.mediaKeys_.setServerCertificate( - this.currentDrmInfo_.serverCertificate).then(function(supported) { - if (!supported) { - shaka.log.warning('Server certificates are not supported by the key' + - ' system. The server certificate has been ignored.'); - } - }).catch(function(exception) { - return Promise.reject(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.DRM, - shaka.util.Error.Code.INVALID_SERVER_CERTIFICATE, - exception.message)); - }); - } + let setServerCertificate = this.setServerCertificate(); return Promise.all([setMediaKeys, setServerCertificate]).then(() => { if (this.destroyed_) return Promise.reject(); @@ -476,6 +462,37 @@ shaka.media.DrmEngine.prototype.attach = function(video) { }; +/** + * Sets the server certificate based on the current DrmInfo. + * + * @return {!Promise} + */ +shaka.media.DrmEngine.prototype.setServerCertificate = async function() { + goog.asserts.assert(this.initialized_, + 'Must call init() before setServerCertificate'); + + if (this.mediaKeys_ && + this.currentDrmInfo_ && + this.currentDrmInfo_.serverCertificate && + this.currentDrmInfo_.serverCertificate.length) { + try { + const supported = await this.mediaKeys_.setServerCertificate( + this.currentDrmInfo_.serverCertificate); + if (!supported) { + shaka.log.warning('Server certificates are not supported by the key' + + ' system. The server certificate has been ignored.'); + } + } catch (exception) { + return Promise.reject(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.INVALID_SERVER_CERTIFICATE, + exception.message)); + } + } +}; + + /** * Remove an offline session and delete it's data. This can only be called * after a successful call to |init|. This will wait until the 'license-release' diff --git a/lib/offline/session_deleter.js b/lib/offline/session_deleter.js index 7d5c87fa87..8eee134bbe 100644 --- a/lib/offline/session_deleter.js +++ b/lib/offline/session_deleter.js @@ -70,6 +70,7 @@ shaka.offline.SessionDeleter = class { drmEngine.configure(config); await drmEngine.initForRemoval( bucket.info.keySystem, bucket.info.licenseUri, + bucket.info.serverCertificate, bucket.info.audioCapabilities, bucket.info.videoCapabilities); } catch (e) { shaka.log.warning('Error initializing EME', e); @@ -77,6 +78,14 @@ shaka.offline.SessionDeleter = class { return []; } + try { + await drmEngine.setServerCertificate(); + } catch (e) { + shaka.log.warning('Error setting server certificate', e); + await drmEngine.destroy(); + return []; + } + /** @type {!Array.} */ const sessionIds = []; await Promise.all(bucket.sessionIds.map(async (sessionId) => { diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 2031dfcf5d..a099e00da7 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -809,6 +809,7 @@ shaka.offline.Storage.prototype.createDrmEngine = async function( const config = this.config_; drmEngine.configure(config.drm); await drmEngine.initForStorage(variants, config.offline.usePersistentLicense); + await drmEngine.setServerCertificate(); await drmEngine.createOrLoad(); return drmEngine; @@ -1189,6 +1190,7 @@ shaka.offline.Storage.deleteLicenseFor_ = async function( sessionId: sessionId, keySystem: manifestDb.drmInfo.keySystem, licenseUri: manifestDb.drmInfo.licenseServerUri, + serverCertificate: manifestDb.drmInfo.serverCertificate, audioCapabilities: shaka.offline.Storage.getCapabilities_( manifestDb, /* isVideo */ false),