From cc97da167f4b08b98613a3296b4879f0948b79b7 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Thu, 20 Apr 2023 00:18:34 +0200 Subject: [PATCH] feat: allow reuse of persistent license sessions (#4461) Add capability to re-use persistent license sessions across sessions. DrmEngine will now always: - try to start stored persistent sessions before trying to fetch a license, as-to be able to check if all needed keys are already loaded. - ask for a new license when the persistent session doesn't have the needed keys for playback, Given the flag `persistentSessionOnlinePlayback` is true, DrmEngine: - won't remove the persistent session from the device at the end of the playback, - won't throw an error when the persistent session isn't found on the device, For now, it needs Shaka's users to persist session information by themselves (localStorage, IndexDB, ...) before giving it back for the next session. Still, it lays foundation to develop the feature to fully handling it on Shaka's side. Related to #1956 --- docs/tutorials/drm-config.md | 61 +++++++++ externs/shaka/player.js | 55 ++++++++ lib/media/drm_engine.js | 200 +++++++++++++++++++++------- lib/player.js | 10 ++ lib/util/error.js | 3 +- lib/util/player_configuration.js | 2 + test/cast/cast_utils_unit.js | 1 + test/demo/demo_unit.js | 4 +- test/media/drm_engine_unit.js | 197 +++++++++++++++++++++++++++ test/offline/storage_integration.js | 3 +- 10 files changed, 488 insertions(+), 48 deletions(-) diff --git a/docs/tutorials/drm-config.md b/docs/tutorials/drm-config.md index 1bf06de1bc..9230f64417 100644 --- a/docs/tutorials/drm-config.md +++ b/docs/tutorials/drm-config.md @@ -208,3 +208,64 @@ you should provide an empty string as robustness ##### Other key-systems Values for other key systems are not known to us at this time. + +#### Re-use persistent license DRM for online playback + +If your DRM provider configuration allows you to deliver persistent license, +you could re-use the created MediaKeys session for the next online playback. + +Configure Shaka to start DRM sessions with the `persistent-license` type +instead of the `temporary` one: + +```js +player.configure({ + drm: { + advanced: { + 'com.widevine.alpha': { + 'sessionType': 'persistent-license' + } + } + } +}); +``` + +**Using `persistent-license` might not work on every devices, use this feature +carefully.** + +When the playback starts, you can retrieve the sessions metadata: + +```js +const activeDrmSessions = this.player.getActiveSessionsMetadata(); +const persistentDrmSessions = activeDrmSessions.filter( + ({ sessionType }) => sessionType === 'persistent-license'); + +// Add your own storage mechanism here, give it an unique known identifier for +// the playing video +``` + +When starting the same video again, retrieve the metadata from the storage, and +set it back to Shaka's configuration. + +Shaka will load the given DRM persistent sessions and will only request a +license if some keys are missing for the content. + +```js +player.configure({ + drm: { + persistentSessionOnlinePlayback: true, + persistentSessionsMetadata: [{ + sessionId: 'deadbeefdeadbeefdeadbeefdeadbeef', + initData: new InitData(0), + initDataType: 'cenc' + }] + } +}); +``` + +NB: Shaka doesn't provide a out-of-the-box storage mechanism for the sessions +metadata. + +#### Continue the Tutorials + +Next, check out {@tutorial license-server-auth}. +Or check out {@tutorial fairplay}. diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 1cc0fe9156..ea0b782195 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -681,12 +681,60 @@ shaka.extern.ProducerReferenceTime; shaka.extern.AdvancedDrmConfiguration; +/** + * @typedef {{ + * sessionId: string, + * sessionType: string, + * initData: ?Uint8Array, + * initDataType: ?string + * }} + * + * @description + * DRM Session Metadata for an active session + * + * @property {string} sessionId + * Session id + * @property {string} sessionType + * Session type + * @property {?Uint8Array} initData + * Initialization data in the format indicated by initDataType. + * @property {string} initDataType + * A string to indicate what format initData is in. + * @exportDoc + */ +shaka.extern.DrmSessionMetadata; + + +/** + * @typedef {{ + * sessionId: string, + * initData: ?Uint8Array, + * initDataType: ?string + * }} + * + * @description + * DRM Session Metadata for saved persistent session + * + * @property {string} sessionId + * Session id + * @property {?Uint8Array} initData + * Initialization data in the format indicated by initDataType. + * @property {?string} initDataType + * A string to indicate what format initData is in. + * @exportDoc + */ +shaka.extern.PersistentSessionMetadata; + + /** * @typedef {{ * retryParameters: shaka.extern.RetryParameters, * servers: !Object., * clearKeys: !Object., * delayLicenseRequestUntilPlayed: boolean, + * persistentSessionOnlinePlayback: boolean, + * persistentSessionsMetadata: + * !Array., * advanced: Object., * initDataTransform: * ((function(!Uint8Array, string, ?shaka.extern.DrmInfo):!Uint8Array)| @@ -713,6 +761,13 @@ shaka.extern.AdvancedDrmConfiguration; * Defaults to false.
* True to configure drm to delay sending a license request until a user * actually starts playing content. + * @property {boolean} persistentSessionOnlinePlayback + * Defaults to false.
+ * True to configure drm to try playback with given persistent session ids + * before requesting a license. Also prevents the session removal at playback + * stop, as-to be able to re-use it later. + * @property {!Array.} persistentSessionsMetadata + * Persistent sessions metadata to load before starting playback * @property {Object.} advanced * Optional.
* A dictionary which maps key system IDs to advanced DRM configuration for diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 9873b5e5b0..e425887617 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -71,8 +71,11 @@ shaka.media.DrmEngine = class { */ this.activeSessions_ = new Map(); - /** @private {!Array.} */ - this.offlineSessionIds_ = []; + /** + * @private {!Map} + */ + this.storedPersistentSessions_ = new Map(); /** @private {!shaka.util.PublicPromise} */ this.allSessionsLoaded_ = new shaka.util.PublicPromise(); @@ -82,7 +85,10 @@ shaka.media.DrmEngine = class { /** @private {function(!shaka.util.Error)} */ this.onError_ = (err) => { - this.allSessionsLoaded_.reject(err); + if (err.severity == shaka.util.Error.Severity.CRITICAL) { + this.allSessionsLoaded_.reject(err); + } + playerInterface.onError(err); }; @@ -184,7 +190,7 @@ shaka.media.DrmEngine = class { this.currentDrmInfo_ = null; this.supportedTypes_.clear(); this.mediaKeys_ = null; - this.offlineSessionIds_ = []; + this.storedPersistentSessions_ = new Map(); this.config_ = null; this.onError_ = () => {}; this.playerInterface_ = null; @@ -228,7 +234,7 @@ shaka.media.DrmEngine = class { // 2. We are about to remove the offline sessions for this manifest - in // that case, we don't need to know about them right now either as // we will be told which ones to remove later. - this.offlineSessionIds_ = []; + this.storedPersistentSessions_ = new Map(); // What we really need to know is whether or not they are expecting to use // persistent licenses. @@ -246,8 +252,20 @@ shaka.media.DrmEngine = class { * @return {!Promise} */ initForPlayback(variants, offlineSessionIds) { - this.offlineSessionIds_ = offlineSessionIds; - this.usePersistentLicenses_ = offlineSessionIds.length > 0; + this.storedPersistentSessions_ = new Map(); + + for (const sessionId of offlineSessionIds) { + this.storedPersistentSessions_.set( + sessionId, {initData: null, initDataType: null}); + } + + for (const metadata of this.config_.persistentSessionsMetadata) { + this.storedPersistentSessions_.set( + metadata.sessionId, + {initData: metadata.initData, initDataType: metadata.initDataType}); + } + + this.usePersistentLicenses_ = this.storedPersistentSessions_.size > 0; return this.init_(variants); } @@ -301,7 +319,7 @@ shaka.media.DrmEngine = class { /** * Negotiate for a key system and set up MediaKeys. * This will assume that both |usePersistentLicences_| and - * |offlineSessionIds_| have been properly set. + * |storedPersistentSessions_| have been properly set. * * @param {!Array.} variants * The variants that we expect to operate with during the drm engine's @@ -493,18 +511,21 @@ shaka.media.DrmEngine = class { */ if (manifestInitData || this.currentDrmInfo_.keySystem !== 'com.apple.fps' || - this.offlineSessionIds_.length) { + this.storedPersistentSessions_.size) { await this.attachMediaKeys_(); } - this.createOrLoad(); + this.createOrLoad().catch(() => { + // Silence errors + // createOrLoad will run async, errors are triggered through onError_ + }); // Explicit init data for any one stream or an offline session is // sufficient to suppress 'encrypted' events for all streams. // Also suppress 'encrypted' events when parsing in-band ppsh // from media segments because that serves the same purpose as the // 'encrypted' events. - if (!manifestInitData && !this.offlineSessionIds_.length && + if (!manifestInitData && !this.storedPersistentSessions_.size && !this.config_.parseInbandPsshEnabled) { this.eventManager_.listen( this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e)); @@ -592,7 +613,8 @@ shaka.media.DrmEngine = class { goog.asserts.assert(this.mediaKeys_, 'Must call init() before removeSession'); - const session = await this.loadOfflineSession_(sessionId); + const session = await this.loadOfflineSession_( + sessionId, {initData: null, initDataType: null}); // This will be null on error, such as session not found. if (!session) { @@ -625,8 +647,30 @@ shaka.media.DrmEngine = class { * * @return {!Promise} */ - createOrLoad() { - // Create temp sessions. + async createOrLoad() { + if (this.storedPersistentSessions_.size) { + this.storedPersistentSessions_.forEach((metadata, sessionId) => { + this.loadOfflineSession_(sessionId, metadata); + }); + + await this.allSessionsLoaded_; + + const keyIds = (this.currentDrmInfo_ && this.currentDrmInfo_.keyIds) || + new Set([]); + + // All the needed keys are already loaded, we don't need another license + // Therefore we prevent starting a new session + if (keyIds.size > 0 && this.areAllKeysUsable_()) { + return this.allSessionsLoaded_; + } + + // Reset the promise for the next sessions to come if key needs aren't + // satisfied with persistent sessions + this.allSessionsLoaded_ = new shaka.util.PublicPromise(); + this.allSessionsLoaded_.catch(() => {}); + } + + // Create sessions. const initDatas = (this.currentDrmInfo_ ? this.currentDrmInfo_.initData : []) || []; for (const initDataOverride of initDatas) { @@ -634,14 +678,9 @@ shaka.media.DrmEngine = class { initDataOverride.initDataType, initDataOverride.initData); } - // Load each session. - for (const sessionId of this.offlineSessionIds_) { - this.loadOfflineSession_(sessionId); - } - // If we have no sessions, we need to resolve the promise right now or else // it will never get resolved. - if (!initDatas.length && !this.offlineSessionIds_.length) { + if (!initDatas.length) { this.allSessionsLoaded_.resolve(); } @@ -769,6 +808,28 @@ shaka.media.DrmEngine = class { return Array.from(ids); } + /** + * Returns the active sessions metadata + * + * @return {!Array.} + */ + getActiveSessionsMetadata() { + const sessions = this.activeSessions_.keys(); + + const metadata = shaka.util.Iterables.map(sessions, (session) => { + const metadata = this.activeSessions_.get(session); + + return { + sessionId: session.sessionId, + sessionType: metadata.type, + initData: metadata.initData, + initDataType: metadata.initDataType, + }; + }); + + return Array.from(metadata); + } + /** * Returns the next expiration time, or Infinity. * @return {number} @@ -1187,12 +1248,41 @@ shaka.media.DrmEngine = class { }; } + /** + * Resolves the allSessionsLoaded_ promise when all the sessions are loaded + * + * @private + */ + checkSessionsLoaded_() { + if (this.areAllSessionsLoaded_()) { + this.allSessionsLoaded_.resolve(); + } + } + + /** + * In case there are no key statuses, consider this session loaded + * after a reasonable timeout. It should definitely not take 5 + * seconds to process a license. + * @param {!shaka.media.DrmEngine.SessionMetaData} metadata + * @private + */ + setLoadSessionTimeoutTimer_(metadata) { + const timer = new shaka.util.Timer(() => { + metadata.loaded = true; + this.checkSessionsLoaded_(); + }); + + timer.tickAfter( + /* seconds= */ shaka.media.DrmEngine.SESSION_LOAD_TIMEOUT_); + } + /** * @param {string} sessionId + * @param {{initData: ?Uint8Array, initDataType: ?string}} sessionMetadata * @return {!Promise.} * @private */ - async loadOfflineSession_(sessionId) { + async loadOfflineSession_(sessionId, sessionMetadata) { let session; const sessionType = 'persistent-license'; @@ -1217,8 +1307,8 @@ shaka.media.DrmEngine = class { (event) => this.onKeyStatusesChange_(event)); const metadata = { - initData: null, - initDataType: null, + initData: sessionMetadata.initData, + initDataType: sessionMetadata.initDataType, loaded: false, oldExpiration: Infinity, updatePromise: null, @@ -1234,32 +1324,42 @@ shaka.media.DrmEngine = class { if (!present) { this.activeSessions_.delete(session); + const severity = this.config_.persistentSessionOnlinePlayback ? + shaka.util.Error.Severity.RECOVERABLE : + shaka.util.Error.Severity.CRITICAL; + this.onError_(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, + severity, shaka.util.Error.Category.DRM, shaka.util.Error.Code.OFFLINE_SESSION_REMOVED)); - return Promise.resolve(); - } - // TODO: We should get a key status change event. Remove once Chrome CDM - // is fixed. - metadata.loaded = true; - if (this.areAllSessionsLoaded_()) { - this.allSessionsLoaded_.resolve(); + metadata.loaded = true; } + this.setLoadSessionTimeoutTimer_(metadata); + this.checkSessionsLoaded_(); + return session; } catch (error) { this.destroyer_.ensureNotDestroyed(error); this.activeSessions_.delete(session); + const severity = this.config_.persistentSessionOnlinePlayback ? + shaka.util.Error.Severity.RECOVERABLE : + shaka.util.Error.Severity.CRITICAL; + this.onError_(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, + severity, shaka.util.Error.Category.DRM, shaka.util.Error.Code.FAILED_TO_CREATE_SESSION, error.message)); + + metadata.loaded = true; + + this.checkSessionsLoaded_(); } + return Promise.resolve(); } @@ -1485,18 +1585,8 @@ shaka.media.DrmEngine = class { if (metadata.updatePromise) { metadata.updatePromise.resolve(); } - // In case there are no key statuses, consider this session loaded - // after a reasonable timeout. It should definitely not take 5 - // seconds to process a license. - const timer = new shaka.util.Timer(() => { - metadata.loaded = true; - if (this.areAllSessionsLoaded_()) { - this.allSessionsLoaded_.resolve(); - } - }); - timer.tickAfter( - /* seconds= */ shaka.media.DrmEngine.SESSION_LOAD_TIMEOUT_); + this.setLoadSessionTimeoutTimer_(metadata); } } @@ -1844,8 +1934,9 @@ shaka.media.DrmEngine = class { * the playback session ends */ if (!this.initializedForStorage_ && - !this.offlineSessionIds_.includes(session.sessionId) && - metadata.type === 'persistent-license') { + !this.storedPersistentSessions_.has(session.sessionId) && + metadata.type === 'persistent-license' && + !this.config_.persistentSessionOnlinePlayback) { shaka.log.v1('Removing session', session.sessionId); await session.remove(); @@ -2019,6 +2110,25 @@ shaka.media.DrmEngine = class { return shaka.util.Iterables.every(metadatas, (data) => data.loaded); } + /** + * @return {boolean} + * @private + */ + areAllKeysUsable_() { + const keyIds = (this.currentDrmInfo_ && this.currentDrmInfo_.keyIds) || + new Set([]); + + for (const keyId of keyIds) { + const status = this.keyStatusByKeyId_.get(keyId); + + if (status !== 'usable') { + return false; + } + } + + return true; + } + /** * Replace the drm info used in each variant in |variants| to reflect each * key service in |keySystems|. diff --git a/lib/player.js b/lib/player.js index cdb0dc8e32..f9835192e5 100644 --- a/lib/player.js +++ b/lib/player.js @@ -3665,6 +3665,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return this.drmEngine_ ? this.drmEngine_.getExpiration() : Infinity; } + /** + * Returns the active sessions metadata + * + * @return {!Array.} + * @export + */ + getActiveSessionsMetadata() { + return this.drmEngine_ ? this.drmEngine_.getActiveSessionsMetadata() : []; + } + /** * Gets a map of EME key ID to the current key status. * diff --git a/lib/util/error.js b/lib/util/error.js index 0dc56976b4..09ff0ed3dc 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -858,7 +858,8 @@ shaka.util.Error.Code = { 'NO_LICENSE_SERVER_GIVEN': 6012, /** - * A required offline session was removed. The content is not playable. + * A required offline session was removed. The content might not be playable + * depending of the playback context. */ 'OFFLINE_SESSION_REMOVED': 6013, diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 04b7dac54f..3dd2ddc870 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -68,6 +68,8 @@ shaka.util.PlayerConfiguration = class { clearKeys: {}, // key is arbitrary key system ID, value must be string advanced: {}, // key is arbitrary key system ID, value is a record type delayLicenseRequestUntilPlayed: false, + persistentSessionOnlinePlayback: false, + persistentSessionsMetadata: [], initDataTransform: (initData, initDataType, drmInfo) => { const keySystem = drmInfo.keySystem; if (keySystem == 'com.apple.fps.1_0' && initDataType == 'skd') { diff --git a/test/cast/cast_utils_unit.js b/test/cast/cast_utils_unit.js index 04e1d90012..52dc0c7184 100644 --- a/test/cast/cast_utils_unit.js +++ b/test/cast/cast_utils_unit.js @@ -25,6 +25,7 @@ describe('CastUtils', () => { 'getManifest', // Too large to proxy 'getManifestParserFactory', // Would not serialize. 'setVideoContainer', + 'getActiveSessionsMetadata', // Test helper methods (not @export'd) 'createDrmEngine', diff --git a/test/demo/demo_unit.js b/test/demo/demo_unit.js index 3626b1f7f2..8bcff57286 100644 --- a/test/demo/demo_unit.js +++ b/test/demo/demo_unit.js @@ -97,7 +97,9 @@ describe('Demo', () => { .add('manifest.hls.mediaPlaylistFullMimeType') .add('manifest.mss.keySystemsBySystemId') .add('drm.keySystemsMapping') - .add('streaming.parsePrftBox'); + .add('streaming.parsePrftBox') + .add('drm.persistentSessionOnlinePlayback') + .add('drm.persistentSessionsMetadata'); /** * @param {!Object} section diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index f34c70deb1..e6320f4638 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -1095,6 +1095,178 @@ describe('DrmEngine', () => { shaka.util.Error.Code.FAILED_TO_GENERATE_LICENSE_REQUEST, message, nativeError, undefined)); }); + + it('should throw a OFFLINE_SESSION_REMOVED error', async () => { + // Given persistent session is not available + session1.load.and.returnValue(false); + + onErrorSpy.and.stub(); + + await drmEngine.initForPlayback( + manifest.variants, ['persistent-session-id']); + await drmEngine.attach(mockVideo); + + expect(drmEngine.initialized()).toBe(true); + + await Util.shortDelay(); + + expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); + expect(mockMediaKeys.createSession) + .toHaveBeenCalledWith('persistent-license'); + expect(session1.load).toHaveBeenCalledWith('persistent-session-id'); + + expect(onErrorSpy).toHaveBeenCalled(); + const error = onErrorSpy.calls.argsFor(0)[0]; + shaka.test.Util.expectToEqualError(error, new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.OFFLINE_SESSION_REMOVED)); + }); + + it('uses persistent session ids when available', async () => { + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; + + const keyId1 = makeKeyId(1); + const keyId2 = makeKeyId(2); + + /** @type {!Uint8Array} */ + const initData1 = new Uint8Array(5); + + // Key IDs in manifest + tweakDrmInfos((drmInfos) => { + drmInfos[0].keyIds = new Set([ + Uint8ArrayUtils.toHex(keyId1), Uint8ArrayUtils.toHex(keyId2), + ]); + drmInfos[0].initData = [ + {initData: initData1, initDataType: 'cenc', keyId: null}, + ]; + }); + + // Given persistent session is available + session1.load.and.returnValue(true); + + config.persistentSessionOnlinePlayback = true; + config.persistentSessionsMetadata = [{ + sessionId: 'persistent-session-id', + initData: initData1, + initDataType: 'cenc'}]; + + drmEngine.configure(config); + + await initAndAttach(); + + await Util.shortDelay(); + + session1.keyStatuses.forEach.and.callFake((callback) => { + callback(keyId1, 'usable'); + callback(keyId2, 'usable'); + }); + + session1.on['keystatuseschange']({target: session1}); + + expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); + expect(mockMediaKeys.createSession) + .toHaveBeenCalledWith('persistent-license'); + expect(session1.load).toHaveBeenCalledWith('persistent-session-id'); + + expect(session2.generateRequest).not.toHaveBeenCalled(); + }); + + it( + 'tries persistent session ids before requesting a license', + async () => { + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; + + const keyId1 = makeKeyId(1); + + /** @type {!Uint8Array} */ + const initData1 = new Uint8Array(5); + + // Key IDs in manifest + tweakDrmInfos((drmInfos) => { + drmInfos[0].keyIds = new Set([ + Uint8ArrayUtils.toHex(keyId1), + ]); + drmInfos[0].sessionType = 'temporary'; + drmInfos[0].initData = [ + {initData: initData1, initDataType: 'cenc', keyId: null}, + ]; + }); + + // Given persistent sessions aren't available + session1.load.and.returnValue(Promise.resolve(false)); + session2.load.and.returnValue( + Promise.reject(new Error('This should be a recoverable error'))); + + manifest.offlineSessionIds = ['persistent-session-id-1']; + + config.persistentSessionsMetadata = [{ + sessionId: 'persistent-session-id-2', + initData: initData1, + initDataType: 'cenc'}]; + config.persistentSessionOnlinePlayback = true; + + drmEngine.configure(config); + + onErrorSpy.and.stub(); + + await initAndAttach(); + + await Util.shortDelay(); + + shaka.test.Util.expectToEqualError( + onErrorSpy.calls.argsFor(0)[0], + new shaka.util.Error( + shaka.util.Error.Severity.RECOVERABLE, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.OFFLINE_SESSION_REMOVED)); + + shaka.test.Util.expectToEqualError( + onErrorSpy.calls.argsFor(1)[0], + new shaka.util.Error( + shaka.util.Error.Severity.RECOVERABLE, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.FAILED_TO_CREATE_SESSION, + 'This should be a recoverable error')); + + // We need to go through the whole license request / update, + // otherwise the DrmEngine will be destroyed while waiting for + // sessions to be marked as loaded, throwing an unhandled exception + const operation = shaka.util.AbortableOperation.completed( + new Uint8Array(0)); + fakeNetEngine.request.and.returnValue(operation); + + await Util.shortDelay(); + + session3.on['message']({ + target: session3, + message: new Uint8Array(0), + messageType: 'license-request'}); + + session3.keyStatuses.forEach.and.callFake((callback) => { + callback(keyId1, 'usable'); + }); + + session3.on['keystatuseschange']({target: session3}); + + await Util.shortDelay(); + + expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(3); + expect(mockMediaKeys.createSession) + .toHaveBeenCalledWith('persistent-license'); + expect(session1.load) + .toHaveBeenCalledWith('persistent-session-id-1'); + + expect(mockMediaKeys.createSession) + .toHaveBeenCalledWith('persistent-license'); + expect(session2.load) + .toHaveBeenCalledWith('persistent-session-id-2'); + + expect(mockMediaKeys.createSession) + .toHaveBeenCalledWith('temporary'); + expect(session3.generateRequest) + .toHaveBeenCalledWith('cenc', initData1); + }); }); // describe('attach') describe('events', () => { @@ -1632,6 +1804,31 @@ describe('DrmEngine', () => { expect(session2.remove).toHaveBeenCalled(); }); + it( + // eslint-disable-next-line max-len + 'tears down & does not remove active persistent sessions based on configuration flag', + async () => { + config.advanced['drm.abc'] = createAdvancedConfig(null); + config.advanced['drm.abc'].sessionType = 'persistent-license'; + config.persistentSessionOnlinePlayback = true; + + drmEngine.configure(config); + + await initAndAttach(); + await sendEncryptedEvent('cenc', new Uint8Array(2)); + + const message = new Uint8Array(0); + session1.on['message']({target: session1, message: message}); + session1.update.and.returnValue(Promise.resolve()); + + await shaka.test.Util.shortDelay(); + mockVideo.setMediaKeys.calls.reset(); + await drmEngine.destroy(); + + expect(session1.close).toHaveBeenCalled(); + expect(session1.remove).not.toHaveBeenCalled(); + }); + it('swallows errors when closing sessions', async () => { await initAndAttach(); await sendEncryptedEvent('webm'); diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index a8009407f9..1f3ab5f182 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -1751,7 +1751,8 @@ filterDescribe('Storage', storageSupport, () => { * @suppress {accessControls} */ function loadOfflineSession(drmEngine, sessionName) { - return drmEngine.loadOfflineSession_(sessionName); + return drmEngine.loadOfflineSession_( + sessionName, {initData: null, initDataType: null}); } /**