diff --git a/demo/common/asset.js b/demo/common/asset.js index e98b3199f3..6f6265d2b3 100644 --- a/demo/common/asset.js +++ b/demo/common/asset.js @@ -357,13 +357,14 @@ const ShakaDemoAssetInfo = class { */ getConfiguration() { const config = /** @type {shaka.extern.PlayerConfiguration} */( - {drm: {}, manifest: {dash: {}}}); + {drm: {advanced: {}}, manifest: {dash: {}}}); if (this.licenseServers.size) { config.drm.servers = {}; this.licenseServers.forEach((value, key) => { config.drm.servers[key] = value; }); } + if (this.clearKeys.size) { config.drm.clearKeys = {}; this.clearKeys.forEach((value, key) => { diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index 5738b19499..34b0261ab0 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -162,6 +162,7 @@ shakaDemo.MessageIds = { DISABLE_XLINK_PROCESSING: 'DEMO_DISABLE_XLINK_PROCESSING', DRM_RETRY_SECTION_HEADER: 'DEMO_DRM_RETRY_SECTION_HEADER', DRM_SECTION_HEADER: 'DEMO_DRM_SECTION_HEADER', + DRM_SESSION_TYPE: 'DEMO_DRM_SESSION_TYPE', DURATION_BACKOFF: 'DEMO_DURATION_BACKOFF', ENABLED: 'DEMO_ENABLED', FORCE_HTTPS: 'DEMO_FORCE_HTTPS', diff --git a/demo/config.js b/demo/config.js index f377312bee..b7002423ab 100644 --- a/demo/config.js +++ b/demo/config.js @@ -133,16 +133,9 @@ shakaDemo.Config = class { /* canBeZero= */ false, /* canBeUnset= */ true); const advanced = shakaDemoMain.getConfiguration().drm.advanced || {}; - const robustnessSuggestions = [ - 'SW_SECURE_CRYPTO', - 'SW_SECURE_DECODE', - 'HW_SECURE_CRYPTO', - 'HW_SECURE_DECODE', - 'HW_SECURE_ALL', - ]; - const addRobustnessField = (name, valueName) => { - // All robustness fields of a given type are set at once. - this.addDatalistInput_(name, robustnessSuggestions, (input) => { + const addDRMAdvancedField = (name, valueName, suggestions) => { + // All advanced fields of a given type are set at once. + this.addDatalistInput_(name, suggestions, (input) => { // Add in any common drmSystem not currently in advanced. for (const drmSystem of shakaDemo.Main.commonDrmSystems) { if (!(drmSystem in advanced)) { @@ -158,12 +151,37 @@ shakaDemo.Config = class { }); const keySystem = Object.keys(advanced)[0]; if (keySystem) { - const currentRobustness = advanced[keySystem][valueName]; - this.latestInput_.input().value = currentRobustness; + const currentValue = advanced[keySystem][valueName]; + this.latestInput_.input().value = currentValue; } }; - addRobustnessField(MessageIds.VIDEO_ROBUSTNESS, 'videoRobustness'); - addRobustnessField(MessageIds.AUDIO_ROBUSTNESS, 'audioRobustness'); + + const robustnessSuggestions = [ + 'SW_SECURE_CRYPTO', + 'SW_SECURE_DECODE', + 'HW_SECURE_CRYPTO', + 'HW_SECURE_DECODE', + 'HW_SECURE_ALL', + '150', + '2000', + '3000', + ]; + + const sessionTypeSuggestions = ['temporary', 'persistent-license']; + + addDRMAdvancedField( + MessageIds.VIDEO_ROBUSTNESS, + 'videoRobustness', + robustnessSuggestions); + addDRMAdvancedField( + MessageIds.AUDIO_ROBUSTNESS, + 'audioRobustness', + robustnessSuggestions); + addDRMAdvancedField( + MessageIds.DRM_SESSION_TYPE, + 'sessionType', + sessionTypeSuggestions); + this.addRetrySection_('drm', MessageIds.DRM_RETRY_SECTION_HEADER); } diff --git a/demo/locales/en.json b/demo/locales/en.json index 7c971659e5..5e7ebecb1c 100644 --- a/demo/locales/en.json +++ b/demo/locales/en.json @@ -62,6 +62,7 @@ "DEMO_DRM_SECTION_HEADER": "DRM", "DEMO_DRM_SYSTEM": "Custom DRM System", "DEMO_DRM_TAB": "Drm", + "DEMO_DRM_SESSION_TYPE": "Session Type", "DEMO_DURATION_BACKOFF": "Duration Backoff", "DEMO_EDIT_CUSTOM": "Edit", "DEMO_ENABLED": "Enabled", diff --git a/demo/locales/source.json b/demo/locales/source.json index f72ca837db..446fc49c1f 100644 --- a/demo/locales/source.json +++ b/demo/locales/source.json @@ -243,6 +243,10 @@ "description": "The header for a section of configuration values.", "message": "[JARGON:DRM]" }, + "DEMO_DRM_SESSION_TYPE": { + "description": "The name of a configuration value.", + "message": "Session Type" + }, "DEMO_DRM_SYSTEM": { "description": "The label on a field that allows users to provide a Digital Rights Management (DRM) system identifier for a custom asset.", "message": "Custom [JARGON:DRM] System" diff --git a/demo/main.js b/demo/main.js index ecade0b8e9..1bca9194cf 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1726,6 +1726,7 @@ shakaDemo.Main = class { persistentStateRequired: false, videoRobustness: '', audioRobustness: '', + sessionType: '', serverCertificate: new Uint8Array(0), individualizationServer: '', }; diff --git a/docs/tutorials/drm-config.md b/docs/tutorials/drm-config.md index acbc6bb58f..cab9798fed 100644 --- a/docs/tutorials/drm-config.md +++ b/docs/tutorials/drm-config.md @@ -145,11 +145,11 @@ playback. Passing in a higher security level than can be supported will cause default is the empty string, which is the lowest security level supported by the key system. -Each key system has their own values for robustness. The values for Widevine -are well-known (see the [Chromium sources][]) and listed below, but -values for other key systems are not known to us at this time. +Each key system has their own values for robustness. -[Chromium sources]: https://cs.chromium.org/chromium/src/components/cdm/renderer/widevine_key_system_properties.h?q=SW_SECURE_CRYPTO&l=22 +##### Widevine + +Chromium sources: https://cs.chromium.org/chromium/src/components/cdm/renderer/widevine_key_system_properties.h?q=SW_SECURE_CRYPTO&l=22 - `SW_SECURE_CRYPTO` - `SW_SECURE_DECODE` @@ -157,6 +157,21 @@ values for other key systems are not known to us at this time. - `HW_SECURE_DECODE` - `HW_SECURE_ALL` +##### PlayReady + +Microsoft Documentation: https://docs.microsoft.com/en-us/playready/overview/security-level + +- `3000` +- `2000` + +`com.microsoft.playready` key system ignores given robustness and stays at a +`2000` decryption level. + +NB: Audio Hardware DRM is not supported (PlayReady limitation) + +##### Other key-systems + +Values for other key systems are not known to us at this time. #### Continue the Tutorials diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index bf3c7b6f73..13be37baba 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -111,6 +111,7 @@ shaka.extern.InitDataOverride; * audioRobustness: string, * videoRobustness: string, * serverCertificate: Uint8Array, + * sessionType: string, * initData: Array., * keyIds: Set. * }} @@ -132,6 +133,9 @@ shaka.extern.InitDataOverride; * Defaults to false. Can be filled in by advanced DRM config.
* True if the application requires the key system to support persistent * state, e.g., for persistent license storage. + * @property {string} sessionType + * Defaults to 'temporary' if Shaka wasn't initiated for storage. + * Can be filled in by advanced DRM config sessionType parameter.
* @property {string} audioRobustness * Defaults to '', e.g., no specific robustness required. Can be filled in * by advanced DRM config.
diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 37b872e500..3e65b7b0b5 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -516,7 +516,8 @@ shaka.extern.EmsgInfo; * videoRobustness: string, * audioRobustness: string, * serverCertificate: Uint8Array, - * individualizationServer: string + * individualizationServer: string, + * sessionType: string * }} * * @property {boolean} distinctiveIdentifierRequired @@ -547,6 +548,10 @@ shaka.extern.EmsgInfo; * @property {string} individualizationServer * The server that handles an 'individualiation-request'. If the * server isn't given, it will default to the license server. + * @property {string} sessionType + * Defaults to 'temporary' for streaming.
+ * The MediaKey session type to create streaming licenses with. This doesn't + * affect offline storage. * * @exportDoc */ diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index af07675891..c4f8760c3e 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -52,6 +52,9 @@ shaka.media.DrmEngine = class { /** @private {boolean} */ this.initialized_ = false; + /** @private {boolean} */ + this.initializedForStorage_ = false; + /** @private {number} */ this.licenseTimeSeconds_ = 0; @@ -202,6 +205,7 @@ shaka.media.DrmEngine = class { * @return {!Promise} */ initForStorage(variants, usePersistentLicenses) { + this.initializedForStorage_ = true; // There are two cases for this call: // 1. We are about to store a manifest - in that case, there are no offline // sessions and therefore no offline session ids. @@ -274,6 +278,7 @@ shaka.media.DrmEngine = class { }]; configsByKeySystem.set(keySystem, config); + return this.queryMediaKeys_(configsByKeySystem); } @@ -393,7 +398,7 @@ shaka.media.DrmEngine = class { // polyfills, since those events are only caught and translated by a // MediaKeys instance. With clear content and no polyfilled MediaKeys // instance attached, you'll never see the 'encrypted' event on those - // platforms (IE 11 & Safari). + // platforms (Safari). this.eventManager_.listenOnce(video, 'encrypted', (event) => { this.onError_(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -772,13 +777,19 @@ shaka.media.DrmEngine = class { info.initData.map((initData) => initData.initDataType)), ]; } + if (info.distinctiveIdentifierRequired) { config.distinctiveIdentifier = 'required'; } + if (info.persistentStateRequired) { config.persistentState = 'required'; } + if (info.sessionType) { + config.sessionTypes = [info.sessionType]; + } + const robustness = (stream.type == ContentType.AUDIO) ? info.audioRobustness : info.videoRobustness; @@ -881,7 +892,11 @@ shaka.media.DrmEngine = class { mediaKeySystemAccess = // eslint-disable-next-line no-await-in-loop await navigator.requestMediaKeySystemAccess(keySystem, [config]); break; - } catch (error) {} // Suppress errors. + } catch (error) { + shaka.log.v2( + 'Requesting', keySystem, 'failed with config', + config, error); + } // Suppress errors. this.destroyer_.ensureNotDestroyed(); } if (mediaKeySystemAccess) { @@ -905,6 +920,11 @@ shaka.media.DrmEngine = class { // Store the capabilities of the key system. const realConfig = mediaKeySystemAccess.getConfiguration(); + + shaka.log.v2( + 'Got MediaKeySystemAccess with configuration', + realConfig); + const audioCaps = realConfig.audioCapabilities || []; const videoCaps = realConfig.videoCapabilities || []; @@ -1010,6 +1030,7 @@ shaka.media.DrmEngine = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, + sessionType: '', initData: initDatas, keyIds: new Set(keyIds), }; @@ -1022,9 +1043,12 @@ shaka.media.DrmEngine = class { */ async loadOfflineSession_(sessionId) { let session; + + const sessionType = 'persistent-license'; + try { shaka.log.v1('Attempting to load an offline session', sessionId); - session = this.mediaKeys_.createSession('persistent-license'); + session = this.mediaKeys_.createSession(sessionType); } catch (exception) { const error = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -1046,6 +1070,7 @@ shaka.media.DrmEngine = class { loaded: false, oldExpiration: Infinity, updatePromise: null, + type: sessionType, }; this.activeSessions_.set(session, metadata); @@ -1096,14 +1121,13 @@ shaka.media.DrmEngine = class { 'mediaKeys_ should be valid when creating temporary session.'); let session; + + const sessionType = this.currentDrmInfo_.sessionType; + try { - if (this.usePersistentLicenses_) { - shaka.log.v1('Creating new persistent session'); - session = this.mediaKeys_.createSession('persistent-license'); - } else { - shaka.log.v1('Creating new temporary session'); - session = this.mediaKeys_.createSession(); - } + shaka.log.info('Creating new', sessionType, 'session'); + + session = this.mediaKeys_.createSession(sessionType); } catch (exception) { this.onError_(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -1124,6 +1148,7 @@ shaka.media.DrmEngine = class { loaded: false, oldExpiration: Infinity, updatePromise: null, + type: sessionType, }; this.activeSessions_.set(session, metadata); @@ -1241,6 +1266,7 @@ shaka.media.DrmEngine = class { let url = this.currentDrmInfo_.licenseServerUri; const advancedConfig = this.config_.advanced[this.currentDrmInfo_.keySystem]; + if (event.messageType == 'individualization-request' && advancedConfig && advancedConfig.individualizationServer) { url = advancedConfig.individualizationServer; @@ -1662,18 +1688,34 @@ shaka.media.DrmEngine = class { /** @private */ async closeOpenSessions_() { // Close all open sessions. - const openSessions = Array.from(this.activeSessions_.keys()); + const openSessions = Array.from(this.activeSessions_.entries()); this.activeSessions_.clear(); // Close all sessions before we remove media keys from the video element. - await Promise.all(openSessions.map(async (session) => { - shaka.log.v1('Closing session', session.sessionId); - + await Promise.all(openSessions.map(async ([session, metadata]) => { try { - await this.closeSession_(session); + /** + * Special case when a persistent-license session has been initiated, + * without being registered in the offline sessions at start-up. + * We should remove the session to prevent it from being orphaned after + * the playback session ends + */ + if (!this.initializedForStorage_ && + !this.offlineSessionIds_.includes(session.sessionId) && + metadata.type === 'persistent-license') { + shaka.log.v1('Removing session', session.sessionId); + + await session.remove(); + } else { + shaka.log.v1('Closing session', session.sessionId, metadata); + + await this.closeSession_(session); + } } catch (error) { // Ignore errors when closing the sessions. Closing a session that // generated no key requests will throw an error. + + shaka.log.error('Failed to close or remove the session', error); } })); } @@ -1900,6 +1942,7 @@ shaka.media.DrmEngine = class { licenseServerUri: licenseServers[0], distinctiveIdentifierRequired: (distinctiveIdentifier == 'required'), persistentStateRequired: (config.persistentState == 'required'), + sessionType: config.sessionTypes[0] || 'temporary', audioRobustness: audioRobustness || '', videoRobustness: videoRobustness || '', serverCertificate: serverCerts[0], @@ -2012,6 +2055,7 @@ shaka.media.DrmEngine = class { // Preference 2: If anything is configured at the application level, // override whatever was in the manifest. const server = servers.get(drmInfo.keySystem) || ''; + drmInfo.licenseServerUri = server; } else { // Preference 3: Keep whatever we had in drmInfo.licenseServerUri, which @@ -2023,6 +2067,7 @@ shaka.media.DrmEngine = class { } const advancedConfig = advancedConfigs.get(drmInfo.keySystem); + if (advancedConfig) { if (!drmInfo.distinctiveIdentifierRequired) { drmInfo.distinctiveIdentifierRequired = @@ -2045,6 +2090,10 @@ shaka.media.DrmEngine = class { if (!drmInfo.serverCertificate) { drmInfo.serverCertificate = advancedConfig.serverCertificate; } + + if (advancedConfig.sessionType) { + drmInfo.sessionType = advancedConfig.sessionType; + } } // Chromecast has a variant of PlayReady that uses a different key @@ -2068,6 +2117,7 @@ shaka.media.DrmEngine = class { * loaded: boolean, * initData: Uint8Array, * oldExpiration: number, + * type: string, * updatePromise: shaka.util.PublicPromise * }} * @@ -2082,6 +2132,8 @@ shaka.media.DrmEngine = class { * @property {number} oldExpiration * The expiration of the session on the last check. This is used to fire * an event when it changes. + * @property {string} type + * The session type * @property {shaka.util.PublicPromise} updatePromise * An optional Promise that will be resolved/rejected on the next update() * call. This is used to track the 'license-release' message when calling diff --git a/lib/util/manifest_parser_utils.js b/lib/util/manifest_parser_utils.js index c4aaee5bd7..4ea56282a1 100644 --- a/lib/util/manifest_parser_utils.js +++ b/lib/util/manifest_parser_utils.js @@ -55,6 +55,7 @@ shaka.util.ManifestParserUtils = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, + sessionType: '', initData: initData || [], keyIds: new Set(), }; diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 550c9c8a4b..6ab6d5b035 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -280,6 +280,7 @@ shaka.util.PlayerConfiguration = class { persistentStateRequired: false, videoRobustness: '', audioRobustness: '', + sessionType: '', serverCertificate: new Uint8Array(0), individualizationServer: '', }, diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index c97c3bcedf..b0019f38e2 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -18,6 +18,7 @@ goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.Platform'); goog.require('shaka.util.PlayerConfiguration'); +goog.require('shaka.util.Platform'); goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Uint8ArrayUtils'); @@ -421,6 +422,7 @@ describe('DrmEngine', () => { distinctiveIdentifier: 'optional', persistentState: 'optional', sessionTypes: ['temporary'], + initDataTypes: ['cenc'], })]); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.def', [jasmine.objectContaining({ @@ -431,6 +433,7 @@ describe('DrmEngine', () => { distinctiveIdentifier: 'optional', persistentState: 'optional', sessionTypes: ['temporary'], + initDataTypes: ['cenc'], })]); }); @@ -449,6 +452,7 @@ describe('DrmEngine', () => { distinctiveIdentifier: 'optional', persistentState: 'required', sessionTypes: ['persistent-license'], + initDataTypes: ['cenc'], }), ]); expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledWith('drm.def', [ @@ -456,6 +460,7 @@ describe('DrmEngine', () => { distinctiveIdentifier: 'optional', persistentState: 'required', sessionTypes: ['persistent-license'], + initDataTypes: ['cenc'], }), ]); }); @@ -541,6 +546,7 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: null, + sessionType: 'persistent-license', individualizationServer: '', distinctiveIdentifierRequired: true, persistentStateRequired: true, @@ -564,6 +570,8 @@ describe('DrmEngine', () => { })], distinctiveIdentifier: 'required', persistentState: 'required', + sessionTypes: ['persistent-license'], + initDataTypes: ['cenc'], })]); }); @@ -596,6 +604,7 @@ describe('DrmEngine', () => { audioRobustness: 'bad', videoRobustness: 'so_bad_it_hurts', serverCertificate: null, + sessionType: '', individualizationServer: '', distinctiveIdentifierRequired: false, persistentStateRequired: false, @@ -619,6 +628,29 @@ describe('DrmEngine', () => { })], distinctiveIdentifier: 'required', persistentState: 'required', + initDataTypes: ['cenc'], + })]); + }); + + it('sets unique initDataTypes if specified from the initData', async () => { + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initDataType: 'very_nice', initData: new Uint8Array(5), keyId: null}, + {initDataType: 'very_nice', initData: new Uint8Array(5), keyId: null}, + ]; + }); + + drmEngine.configure(config); + + const variants = manifest.variants; + + await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); + + expect(drmEngine.initialized()).toBe(true); + expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(1); + expect(requestMediaKeySystemAccessSpy) + .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ + initDataTypes: ['very_nice'], })]); }); @@ -1489,10 +1521,43 @@ describe('DrmEngine', () => { mockVideo.setMediaKeys.calls.reset(); await drmEngine.destroy(); expect(session1.close).toHaveBeenCalled(); + expect(session1.remove).not.toHaveBeenCalled(); expect(session2.close).toHaveBeenCalled(); + expect(session2.remove).not.toHaveBeenCalled(); expect(mockVideo.setMediaKeys).toHaveBeenCalledWith(null); }); + it('tears down & removes active persistent sessions', async () => { + config.advanced['drm.abc'] = createAdvancedConfig(null); + config.advanced['drm.abc'].sessionType = 'persistent-license'; + + drmEngine.configure(config); + + await initAndAttach(); + const initData1 = new Uint8Array(1); + const initData2 = new Uint8Array(2); + mockVideo.on['encrypted']( + {initDataType: 'webm', initData: initData1, keyId: null}); + mockVideo.on['encrypted']( + {initDataType: 'webm', initData: initData2, keyId: null}); + + const message = new Uint8Array(0); + session1.on['message']({target: session1, message: message}); + session1.update.and.returnValue(Promise.resolve()); + session2.on['message']({target: session2, message: message}); + session2.update.and.returnValue(Promise.resolve()); + + await shaka.test.Util.shortDelay(); + mockVideo.setMediaKeys.calls.reset(); + await drmEngine.destroy(); + + expect(session1.close).not.toHaveBeenCalled(); + expect(session1.remove).toHaveBeenCalled(); + + expect(session2.close).not.toHaveBeenCalled(); + expect(session2.remove).toHaveBeenCalled(); + }); + it('swallows errors when closing sessions', async () => { await initAndAttach(); const initData1 = new Uint8Array(1); @@ -1877,6 +1942,7 @@ describe('DrmEngine', () => { videoRobustness: 'really_really_ridiculously_good', distinctiveIdentifierRequired: true, serverCertificate: null, + sessionType: '', individualizationServer: '', persistentStateRequired: true, }; @@ -1894,6 +1960,7 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, + sessionType: 'temporary', initData: [], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }); @@ -2248,6 +2315,7 @@ describe('DrmEngine', () => { persistentStateRequired: false, serverCertificate: serverCert, individualizationServer: '', + sessionType: '', videoRobustness: '', }; } diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index 25d21c895f..1dd680a9fc 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -101,6 +101,7 @@ describe('ManifestConverter', () => { audioRobustness: 'very', videoRobustness: 'kinda_sorta', serverCertificate: new Uint8Array([1, 2, 3]), + sessionType: '', initData: [{ initData: new Uint8Array([4, 5, 6]), initDataType: 'cenc', diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index 8b85aac1cf..9b6a5a32df 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -1587,6 +1587,7 @@ filterDescribe('Storage', storageSupport, () => { distinctiveIdentifierRequired: false, initData: null, keyIds: null, + sessionType: 'temporary', serverCertificate: null, audioRobustness: 'HARDY', videoRobustness: 'OTHER', diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index 4f4fce239a..029fd84c3f 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -405,6 +405,8 @@ shaka.test.ManifestGenerator.DrmInfo = class { this.initData = null; /** @type {Set.} */ this.keyIds = new Set(); + /** @type {string} */ + this.sessionType = ''; /** @type {shaka.extern.DrmInfo} */ const foo = this;