From 9354f04b122d8dd319c8ce766a994213c22c1e83 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 22 Jul 2020 15:58:37 +0200 Subject: [PATCH 01/42] feat: use playready recommendation when using robustness or persistent state --- lib/media/drm_engine.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index ce1a371b29..9e3607aeb1 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -2038,17 +2038,23 @@ shaka.media.DrmEngine = class { } } - // Chromecast has a variant of PlayReady that uses a different key - // system ID. Since manifest parsers convert the standard PlayReady - // UUID to the standard PlayReady key system ID, here we will switch - // to the Chromecast version if we are running on that platform. - // Note that this must come after fillInDrmInfoDefaults_, since the - // player config uses the standard PlayReady ID for license server - // configuration. - if (window.cast && window.cast.__platform__) { - if (drmInfo.keySystem == 'com.microsoft.playready') { + if (drmInfo.keySystem == 'com.microsoft.playready') { + // Chromecast has a variant of PlayReady that uses a different key + // system ID. Since manifest parsers convert the standard PlayReady + // UUID to the standard PlayReady key system ID, here we will switch + // to the Chromecast version if we are running on that platform. + // Note that this must come after fillInDrmInfoDefaults_, since the + // player config uses the standard PlayReady ID for license server + // configuration. + if (window.cast && window.cast.__platform__) { drmInfo.keySystem = 'com.chromecast.playready'; } + + // com.microsoft.playready does not support robustness nor persitent license, + // we need to switch to playready recommendation for that purpose + if (drmInfo.videoRobustness || drmInfo.audioRobustness || drmInfo.persistentStateRequired) { + drmInfo.keySystem =='com.microsoft.playready.recommendation' + } } } }; From 678e005ed16c5d4bc51ed5a43bb975600e626ff3 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 22 Jul 2020 16:10:10 +0200 Subject: [PATCH 02/42] fixing lint --- lib/media/drm_engine.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 9e3607aeb1..51b0e031b8 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -2050,10 +2050,14 @@ shaka.media.DrmEngine = class { drmInfo.keySystem = 'com.chromecast.playready'; } - // com.microsoft.playready does not support robustness nor persitent license, + // com.microsoft.playready does not support robustness nor persitent, // we need to switch to playready recommendation for that purpose - if (drmInfo.videoRobustness || drmInfo.audioRobustness || drmInfo.persistentStateRequired) { - drmInfo.keySystem =='com.microsoft.playready.recommendation' + if ( + drmInfo.videoRobustness || + drmInfo.audioRobustness || + drmInfo.persistentStateRequired + ) { + drmInfo.keySystem =='com.microsoft.playready.recommendation'; } } } From 7925fb2db725fb2f3049917f711c00e150e78d1a Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 22 Jul 2020 16:10:40 +0200 Subject: [PATCH 03/42] fixup wrong assignment --- lib/media/drm_engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 51b0e031b8..d0cf645008 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -2057,7 +2057,7 @@ shaka.media.DrmEngine = class { drmInfo.audioRobustness || drmInfo.persistentStateRequired ) { - drmInfo.keySystem =='com.microsoft.playready.recommendation'; + drmInfo.keySystem = 'com.microsoft.playready.recommendation'; } } } From ed41a9dc353bf7f276abd9dcf19dd16dbca3542d Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 22 Jul 2020 16:29:34 +0200 Subject: [PATCH 04/42] unpack reco --- lib/media/drm_engine.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index d0cf645008..65d510c99c 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1247,6 +1247,8 @@ shaka.media.DrmEngine = class { // NOTE: allowCrossSiteCredentials can be set in a request filter. if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' || + this.currentDrmInfo_.keySystem == + 'com.microsoft.playready.recommendation' || this.currentDrmInfo_.keySystem == 'com.chromecast.playready') { this.unpackPlayReadyRequest_(request); } From d20cee1d07e0e0775ab0a90afda6128479827852 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Thu, 23 Jul 2020 16:18:26 +0200 Subject: [PATCH 05/42] add documentation --- docs/tutorials/drm-config.md | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/drm-config.md b/docs/tutorials/drm-config.md index acbc6bb58f..e25e32dad6 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,24 @@ 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. + +If you specify a robustness into the configuration, Shaka will try to load and start the key session with the +`com.microsoft.playready.recommendation` key system. That key system correctly implements the EME specification +and will use the given decryption robustness. + +NB: Audio Hardware DRM is not supported + +##### Other key-systems + +Values for other key systems are not known to us at this time. #### Continue the Tutorials From 021306b7b97c4b653342bebbed6c73561d6b53ce Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Thu, 23 Jul 2020 16:36:58 +0200 Subject: [PATCH 06/42] fixup type --- lib/media/drm_engine.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 65d510c99c..3a9ead39bf 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -2052,8 +2052,8 @@ shaka.media.DrmEngine = class { drmInfo.keySystem = 'com.chromecast.playready'; } - // com.microsoft.playready does not support robustness nor persitent, - // we need to switch to playready recommendation for that purpose + // com.microsoft.playready does not support robustness nor persistent + // state so we need to switch to playready recommendation for that purpose if ( drmInfo.videoRobustness || drmInfo.audioRobustness || From b011cfc26350947292821a58de9b31f746d6a402 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 24 Jul 2020 09:30:43 +0200 Subject: [PATCH 07/42] fixup typo, lines length & add more insight on robustness --- docs/tutorials/drm-config.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/drm-config.md b/docs/tutorials/drm-config.md index e25e32dad6..edb49c3d3f 100644 --- a/docs/tutorials/drm-config.md +++ b/docs/tutorials/drm-config.md @@ -164,13 +164,18 @@ Microsoft Documentation: https://docs.microsoft.com/en-us/playready/overview/sec - `3000` - `2000` -`com.microsoft.playready` key system ignores given robustness and stays at a `2000` decryption level. +`com.microsoft.playready` key system ignores given robustness and stays at a +`2000` decryption level. -If you specify a robustness into the configuration, Shaka will try to load and start the key session with the -`com.microsoft.playready.recommendation` key system. That key system correctly implements the EME specification -and will use the given decryption robustness. +If you specify a robustness into the configuration, Shaka will try to load and +start the key session with the `com.microsoft.playready.recommendation` key +system. That key system correctly implements the EME specification and will use +the given decryption robustness. -NB: Audio Hardware DRM is not supported +It's up to you to detect and set the robustness if the playing platform is able +to handle it. + +NB: Audio Hardware DRM is not supported (PlayReady limitation) ##### Other key-systems From e7e045d3941127706b87d8888eea53393f5fde45 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 24 Jul 2020 11:36:47 +0200 Subject: [PATCH 08/42] add some more rules for recommendation --- lib/media/drm_engine.js | 108 ++++++++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 32 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 3a9ead39bf..b702b24fa5 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1230,8 +1230,11 @@ shaka.media.DrmEngine = class { const metadata = this.activeSessions_.get(session); let url = this.currentDrmInfo_.licenseServerUri; - const advancedConfig = - this.config_.advanced[this.currentDrmInfo_.keySystem]; + const advancedConfig = this.config_.advanced[ + shaka.media.DrmEngine.getConfigKeySystem_( + this.currentDrmInfo_.keySystem + ) + ]; if (event.messageType == 'individualization-request' && advancedConfig && advancedConfig.individualizationServer) { url = advancedConfig.individualizationServer; @@ -1246,10 +1249,12 @@ shaka.media.DrmEngine = class { request.sessionId = session.sessionId; // NOTE: allowCrossSiteCredentials can be set in a request filter. - if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' || - this.currentDrmInfo_.keySystem == - 'com.microsoft.playready.recommendation' || - this.currentDrmInfo_.keySystem == 'com.chromecast.playready') { + if ( + shaka.media.DrmEngine.isPlayReadyKeySystem_( + this.currentDrmInfo_.keySystem + ) || + this.currentDrmInfo_.keySystem == 'com.chromecast.playready' + ) { this.unpackPlayReadyRequest_(request); } @@ -1416,31 +1421,36 @@ shaka.media.DrmEngine = class { // on Edge: 26 1d 5a 6e - 57 27 - d7 47 - 80 46 ea a5 d1 d3 4b 5a // Bug filed: https://bit.ly/2thuzXu - // NOTE that we skip this if byteLength != 16. This is used for the IE11 - // and Edge 12 EME polyfill, which uses single-byte dummy key IDs. - // However, unlike Edge and Chromecast, Tizen doesn't have this problem. - if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' && - keyId.byteLength == 16 && - (shaka.util.Platform.isIE() || shaka.util.Platform.isEdge())) { - // Read out some fields in little-endian: - const dataView = shaka.util.BufferUtils.toDataView(keyId); - const part0 = dataView.getUint32(0, /* LE= */ true); - const part1 = dataView.getUint16(4, /* LE= */ true); - const part2 = dataView.getUint16(6, /* LE= */ true); - // Write it back in big-endian: - dataView.setUint32(0, part0, /* BE= */ false); - dataView.setUint16(4, part1, /* BE= */ false); - dataView.setUint16(6, part2, /* BE= */ false); - } + if ( + shaka.media.DrmEngine.isPlayReadyKeySystem_( + this.currentDrmInfo_.keySystem + ) + ) { + // NOTE that we skip this if byteLength != 16. This is used for the + // IE11 and Edge 12 EME polyfill, which uses single-byte dummy key IDs. + // However, unlike Edge and Chromecast, Tizen doesn't have this problem. + if (keyId.byteLength == 16 && + (shaka.util.Platform.isIE() || shaka.util.Platform.isEdge()) + ) { + // Read out some fields in little-endian: + const dataView = shaka.util.BufferUtils.toDataView(keyId); + const part0 = dataView.getUint32(0, /* LE= */ true); + const part1 = dataView.getUint16(4, /* LE= */ true); + const part2 = dataView.getUint16(6, /* LE= */ true); + // Write it back in big-endian: + dataView.setUint32(0, part0, /* BE= */ false); + dataView.setUint16(4, part1, /* BE= */ false); + dataView.setUint16(6, part2, /* BE= */ false); + } - // Microsoft's implementation in IE11 seems to never set key status to - // 'usable'. It is stuck forever at 'status-pending'. In spite of this, - // the keys do seem to be usable and content plays correctly. - // Bug filed: https://bit.ly/2tpIU3n - // Microsoft has fixed the issue on Edge, but it remains in IE. - if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' && - status == 'status-pending') { - status = 'usable'; + // Microsoft's implementation in IE11 seems to never set key status to + // 'usable'. It is stuck forever at 'status-pending'. In spite of this, + // the keys do seem to be usable and content plays correctly. + // Bug filed: https://bit.ly/2tpIU3n + // Microsoft has fixed the issue on Edge, but it remains in IE. + if (status == 'status-pending') { + status = 'usable'; + } } if (status != 'status-pending') { @@ -1520,6 +1530,33 @@ shaka.media.DrmEngine = class { this.playerInterface_.onKeyStatus(shaka.util.MapUtils.asObject(publicMap)); } + /** + * + * @param {string} keySystem + * + * @private + */ + static isPlayReadyKeySystem_(keySystem) { + return keySystem === 'com.microsoft.playready.recommendation' + || keySystem === 'com.microsoft.playready'; + } + + /** + * We may change the key system internally, but we don't want developers + * to add other key systems into their configuration. + * + * @param {string} keySystem + * + * @private + */ + static getConfigKeySystem_(keySystem) { + if (keySystem === 'com.microsoft.playready.recommendation') { + return 'com.microsoft.playready'; + } + + return keySystem; + } + /** * Returns true if the browser has recent EME APIs. * @@ -1550,6 +1587,7 @@ shaka.media.DrmEngine = class { 'org.w3.clearkey', 'com.widevine.alpha', 'com.microsoft.playready', + 'com.microsoft.playready.recommendation', 'com.apple.fps.3_0', 'com.apple.fps.2_0', 'com.apple.fps.1_0', @@ -1982,6 +2020,10 @@ shaka.media.DrmEngine = class { return; } + const configKeySystem = shaka.media.DrmEngine.getConfigKeySystem_( + drmInfo.keySystem + ); + // The order of preference for drmInfo: // 1. Clear Key config, used for debugging, should override everything else. // (The application can still specify a clearkey license server.) @@ -2004,7 +2046,8 @@ shaka.media.DrmEngine = class { } else if (servers.size) { // Preference 2: If anything is configured at the application level, // override whatever was in the manifest. - const server = servers.get(drmInfo.keySystem) || ''; + const server = servers.get(configKeySystem) || ''; + drmInfo.licenseServerUri = server; } else { // Preference 3: Keep whatever we had in drmInfo.licenseServerUri, which @@ -2015,7 +2058,8 @@ shaka.media.DrmEngine = class { drmInfo.keyIds = new Set(); } - const advancedConfig = advancedConfigs.get(drmInfo.keySystem); + const advancedConfig = advancedConfigs.get(configKeySystem); + if (advancedConfig) { if (!drmInfo.distinctiveIdentifierRequired) { drmInfo.distinctiveIdentifierRequired = From a5733e133393fd900b92be2efcaeb16a4ae3f9ad Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 24 Jul 2020 17:11:58 +0200 Subject: [PATCH 09/42] add initDataTypes --- lib/media/drm_engine.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index b702b24fa5..4f9bbe2200 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1112,6 +1112,7 @@ shaka.media.DrmEngine = class { const metadata = { initData: initData, + initDataType: initDataType, loaded: false, oldExpiration: Infinity, updatePromise: null, From d524b028117ac8fe3c8a0567affba79fcc8e73dd Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 29 Jul 2020 09:56:50 +0200 Subject: [PATCH 10/42] enforce sessionTypes to persistent-license --- lib/media/drm_engine.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 4f9bbe2200..6c78817d38 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -766,8 +766,10 @@ shaka.media.DrmEngine = class { if (info.distinctiveIdentifierRequired) { config.distinctiveIdentifier = 'required'; } + if (info.persistentStateRequired) { config.persistentState = 'required'; + config.sessionTypes = ['persistent-license']; } const robustness = (stream.type == ContentType.AUDIO) ? @@ -1112,7 +1114,6 @@ shaka.media.DrmEngine = class { const metadata = { initData: initData, - initDataType: initDataType, loaded: false, oldExpiration: Infinity, updatePromise: null, From c29c6b15e18198066287edc1cda5ccea11dc6e31 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 29 Jul 2020 11:18:38 +0200 Subject: [PATCH 11/42] enforce persistent sessionType in MediaKey createSession --- demo/common/asset.js | 33 ++++++++++++++++++++++++++++++++- demo/common/assets.js | 2 ++ lib/media/drm_engine.js | 12 +++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/demo/common/asset.js b/demo/common/asset.js index e98b3199f3..dacb74cd8c 100644 --- a/demo/common/asset.js +++ b/demo/common/asset.js @@ -57,6 +57,8 @@ const ShakaDemoAssetInfo = class { this.features = [shakaAssets.Feature.VOD]; /** @type {!Map.} */ this.licenseServers = new Map(); + /** @type {!Map.} */ + this.persistentStates = new Map(); /** @type {!Map.} */ this.licenseRequestHeaders = new Map(); /** @type {?shaka.extern.RequestFilter} */ @@ -255,6 +257,17 @@ const ShakaDemoAssetInfo = class { return this; } + /** + * + * @param {string} keySystem + * @param {boolean} persistentStateRequired + * @return {!ShakaDemoAssetInfo} + */ + setPersistentStateRequired(keySystem, persistentStateRequired) { + this.persistentStates.set(keySystem, persistentStateRequired); + return this; + } + /** * @param {shakaAssets.ExtraText} extraText * @return {!ShakaDemoAssetInfo} @@ -357,13 +370,31 @@ 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.persistentStates.size) { + this.persistentStates.forEach((value, key) => { + if (!config.drm.advanced[key]) { + config.drm.advanced[key] = { + distinctiveIdentifierRequired: false, + persistentStateRequired: false, + videoRobustness: '', + audioRobustness: '', + serverCertificate: new Uint8Array(0), + individualizationServer: '', + }; + } + + config.drm.advanced[key].persistentStateRequired = value; + }); + } + if (this.clearKeys.size) { config.drm.clearKeys = {}; this.clearKeys.forEach((value, key) => { diff --git a/demo/common/assets.js b/demo/common/assets.js index cf7b289720..f7e51f0738 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -1043,6 +1043,7 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.HIGH_DEFINITION) .addLicenseServer('com.microsoft.playready', 'https://content.uplynk.com/pr') .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') + .setPersistentStateRequired('com.microsoft.playready', true) .setRequestFilter(shakaAssets.UplynkRequestFilter) .setResponseFilter(shakaAssets.UplynkResponseFilter), // Reliable Playready playback requires Edge 16+ @@ -1060,6 +1061,7 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.HIGH_DEFINITION) .addLicenseServer('com.microsoft.playready', 'https://content.uplynk.com/pr') .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') + .setPersistentStateRequired('com.microsoft.playready', true) .setRequestFilter(shakaAssets.UplynkRequestFilter) .setResponseFilter(shakaAssets.UplynkResponseFilter), new ShakaDemoAssetInfo( diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 6c78817d38..b0f3efa6f5 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -45,6 +45,9 @@ shaka.media.DrmEngine = class { /** @private {MediaKeys} */ this.mediaKeys_ = null; + /** @private {MediaKeySystemAccess} */ + this.mediaKeySystemAccess_ = null; + /** @private {HTMLMediaElement} */ this.video_ = null; @@ -929,6 +932,7 @@ shaka.media.DrmEngine = class { shaka.log.info('Created MediaKeys object for key system', this.currentDrmInfo_.keySystem); + this.mediaKeySystemAccess_ = mediaKeySystemAccess; this.mediaKeys_ = mediaKeys; this.initialized_ = true; @@ -1089,8 +1093,14 @@ shaka.media.DrmEngine = class { 'mediaKeys_ should be valid when creating temporary session.'); let session; + + const mediaKeySystemAccessConfiguration = + this.mediaKeySystemAccess_.getConfiguration(); + try { - if (this.usePersistentLicenses_) { + if (this.usePersistentLicenses_ || + mediaKeySystemAccessConfiguration.persistentState === 'required' + ) { shaka.log.v1('Creating new persistent session'); session = this.mediaKeys_.createSession('persistent-license'); } else { From 3adf032c1f4d200e0c693dd07ca5d7d62c1b6d0b Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 31 Jul 2020 14:20:44 +0200 Subject: [PATCH 12/42] set config initDataTypes from initData --- lib/media/drm_engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index b0f3efa6f5..7bd95a10ba 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -766,6 +766,7 @@ shaka.media.DrmEngine = class { info.initData.map((initData) => initData.initDataType)), ]; } + if (info.distinctiveIdentifierRequired) { config.distinctiveIdentifier = 'required'; } @@ -1613,7 +1614,6 @@ shaka.media.DrmEngine = class { ]; const basicConfig = { - initDataTypes: ['cenc'], videoCapabilities: basicVideoCapabilities, }; const offlineConfig = { From 90eae8ad8808ed3c29972d9480a58564cd6982e1 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 31 Jul 2020 15:32:13 +0200 Subject: [PATCH 13/42] prevent null access, re-order for cast priority, fixup some tests --- lib/media/drm_engine.js | 20 ++++++++++---------- test/media/drm_engine_unit.js | 6 ++++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 7bd95a10ba..d4e8e5e695 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -2097,6 +2097,16 @@ shaka.media.DrmEngine = class { } if (drmInfo.keySystem == 'com.microsoft.playready') { + // com.microsoft.playready does not support robustness nor persistent + // state so we need to switch to playready recommendation for that purpose + if ( + drmInfo.videoRobustness || + drmInfo.audioRobustness || + drmInfo.persistentStateRequired + ) { + drmInfo.keySystem = 'com.microsoft.playready.recommendation'; + } + // Chromecast has a variant of PlayReady that uses a different key // system ID. Since manifest parsers convert the standard PlayReady // UUID to the standard PlayReady key system ID, here we will switch @@ -2107,16 +2117,6 @@ shaka.media.DrmEngine = class { if (window.cast && window.cast.__platform__) { drmInfo.keySystem = 'com.chromecast.playready'; } - - // com.microsoft.playready does not support robustness nor persistent - // state so we need to switch to playready recommendation for that purpose - if ( - drmInfo.videoRobustness || - drmInfo.audioRobustness || - drmInfo.persistentStateRequired - ) { - drmInfo.keySystem = 'com.microsoft.playready.recommendation'; - } } } }; diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 07156b3d2a..63333cd4b4 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -477,7 +477,7 @@ describe('DrmEngine', () => { .toHaveBeenCalledWith('drm.def', [jasmine.objectContaining({ distinctiveIdentifier: 'optional', persistentState: 'required', - sessionTypes: ['temporary'], + sessionTypes: ['persistent-license'], })]); }); @@ -1863,6 +1863,7 @@ describe('DrmEngine', () => { // Key IDs in manifest tweakDrmInfos((drmInfos) => { drmInfos[0].keyIds = new Set(['deadbeefdeadbeefdeadbeefdeadbeef']); + drmInfos[0].initData = [{initDataType: 'niceInitDataType'}]; }); config.advanced['drm.abc'] = { @@ -1887,7 +1888,8 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, - initData: [], + initData: [{initDataType: 'niceInitDataType'}], + initDataTypes: ['niceInitDataType'], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }); }); From dc4ddaf40f2cc0bff8db77ce47e30d40b4975935 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 3 Aug 2020 17:05:31 +0200 Subject: [PATCH 14/42] add more tests --- test/media/drm_engine_unit.js | 99 ++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 63333cd4b4..d81ab6e220 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -615,6 +615,80 @@ describe('DrmEngine', () => { })]); }); + it('selects the correct configuration for PlayReady with robustness / persistentState', async () => { + // Leave only one drmInfo + manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.encrypted = true; + stream.addDrmInfo('com.microsoft.playready'); + }); + variant.addAudio(2, (stream) => { + stream.encrypted = true; + stream.addDrmInfo('com.microsoft.playready'); + }); + }); + }); + + // Add manifest-supplied license servers for both. + tweakDrmInfos((drmInfos) => { + for (const drmInfo of drmInfos) { + drmInfo.licenseServerUri = 'https://com.microsoft.playready/license'; + } + }); + + setRequestMediaKeySystemAccessSpy(['com.microsoft.playready.recommendation']); + config.servers = {'com.microsoft.playready': 'https://com.microsoft.playready/license'}; + config.advanced['com.microsoft.playready'] = { + audioRobustness: 'good', + videoRobustness: 'really_really_ridiculously_good', + serverCertificate: null, + individualizationServer: '', + distinctiveIdentifierRequired: false, + persistentStateRequired: true, + }; + + 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('com.microsoft.playready.recommendation', [jasmine.objectContaining({ + videoCapabilities: [jasmine.objectContaining({ + robustness: 'good', + })], + videoCapabilities: [jasmine.objectContaining({ + robustness: 'really_really_ridiculously_good', + })], + persistentState: 'required', + })]); + }); + + it('sets unique initDataTypes if specified from the initData', async () => { + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initDataType: 'cenc', initData: new Uint8Array(5), keyId: null}, + {initDataType: 'cenc', 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: ['cenc'], + })]); + }) + it('fails if license server is not configured', async () => { setRequestMediaKeySystemAccessSpy(['drm.abc']); @@ -773,6 +847,30 @@ describe('DrmEngine', () => { .toHaveBeenCalledWith('cenc', initData3); }); + it('creates sessions with the correct sessionType', async () => { + // Set up init data overrides in the manifest: + /** @type {!Uint8Array} */ + const initData = new Uint8Array(5); + + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: initData, initDataType: 'cenc', keyId: 'abc'}, + ]; + }); + + mockMediaKeySystemAccess.getConfiguration.and.returnValue({ + persistentState: 'required', + audioCapabilities: [{contentType: 'audio/webm'}], + videoCapabilities: [{contentType: 'video/mp4; codecs="fake"'}], + }); + + await initAndAttach(); + + // expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); + expect(mockMediaKeys.createSession).toHaveBeenCalledWith('persistent-license'); + expect(session1.generateRequest).toHaveBeenCalledWith('cenc', initData); + }); + it('ignores duplicate init data overrides', async () => { // Set up init data overrides in the manifest; // The second initData has a different keyId from the first, @@ -1889,7 +1987,6 @@ describe('DrmEngine', () => { videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, initData: [{initDataType: 'niceInitDataType'}], - initDataTypes: ['niceInitDataType'], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }); }); From 740abc9d2893fbadbf90681c04fc4cc2491f0a5d Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 3 Aug 2020 17:07:13 +0200 Subject: [PATCH 15/42] remove useless addings --- test/media/drm_engine_unit.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index d81ab6e220..854e1fbbbe 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -1961,7 +1961,6 @@ describe('DrmEngine', () => { // Key IDs in manifest tweakDrmInfos((drmInfos) => { drmInfos[0].keyIds = new Set(['deadbeefdeadbeefdeadbeefdeadbeef']); - drmInfos[0].initData = [{initDataType: 'niceInitDataType'}]; }); config.advanced['drm.abc'] = { @@ -1986,7 +1985,6 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, - initData: [{initDataType: 'niceInitDataType'}], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }); }); From 785072bcd0fb0a0033480b0fd8995ed4ebb1b7fa Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 5 Aug 2020 17:15:09 +0200 Subject: [PATCH 16/42] fixup linting --- test/media/drm_engine_unit.js | 41 +++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 854e1fbbbe..daf1c23642 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -615,7 +615,7 @@ describe('DrmEngine', () => { })]); }); - it('selects the correct configuration for PlayReady with robustness / persistentState', async () => { + it('selects the correct configuration for PlayReady', async () => { // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { @@ -633,12 +633,16 @@ describe('DrmEngine', () => { // Add manifest-supplied license servers for both. tweakDrmInfos((drmInfos) => { for (const drmInfo of drmInfos) { - drmInfo.licenseServerUri = 'https://com.microsoft.playready/license'; + drmInfo.licenseServerUri = 'https://com.microsoft.playready/license'; } }); - setRequestMediaKeySystemAccessSpy(['com.microsoft.playready.recommendation']); - config.servers = {'com.microsoft.playready': 'https://com.microsoft.playready/license'}; + setRequestMediaKeySystemAccessSpy([ + 'com.microsoft.playready.recommendation', + ]); + config.servers = { + 'com.microsoft.playready': 'https://com.microsoft.playready/license', + }; config.advanced['com.microsoft.playready'] = { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', @@ -656,15 +660,18 @@ describe('DrmEngine', () => { expect(drmEngine.initialized()).toBe(true); expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(1); expect(requestMediaKeySystemAccessSpy) - .toHaveBeenCalledWith('com.microsoft.playready.recommendation', [jasmine.objectContaining({ - videoCapabilities: [jasmine.objectContaining({ - robustness: 'good', - })], - videoCapabilities: [jasmine.objectContaining({ - robustness: 'really_really_ridiculously_good', - })], - persistentState: 'required', - })]); + .toHaveBeenCalledWith( + 'com.microsoft.playready.recommendation', + [jasmine.objectContaining({ + audioCapabilities: [jasmine.objectContaining({ + robustness: 'good', + })], + videoCapabilities: [jasmine.objectContaining({ + robustness: 'really_really_ridiculously_good', + })], + persistentState: 'required', + })] + ); }); it('sets unique initDataTypes if specified from the initData', async () => { @@ -679,7 +686,7 @@ describe('DrmEngine', () => { const variants = manifest.variants; - await drmEngine.initForPlayback(variants, manifest.offlineSessionIds) + await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(1); @@ -687,7 +694,7 @@ describe('DrmEngine', () => { .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ initDataTypes: ['cenc'], })]); - }) + }); it('fails if license server is not configured', async () => { setRequestMediaKeySystemAccessSpy(['drm.abc']); @@ -867,7 +874,9 @@ describe('DrmEngine', () => { await initAndAttach(); // expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); - expect(mockMediaKeys.createSession).toHaveBeenCalledWith('persistent-license'); + expect(mockMediaKeys.createSession).toHaveBeenCalledWith( + 'persistent-license' + ); expect(session1.generateRequest).toHaveBeenCalledWith('cenc', initData); }); From 4d164f55ef46b43c739a6e0e80b3e55b45df456e Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 10 Aug 2020 16:38:01 +0200 Subject: [PATCH 17/42] add default initDataTypes if not provided --- lib/media/drm_engine.js | 10 ++++++++++ test/media/drm_engine_unit.js | 13 ++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index d4e8e5e695..46a226a10f 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -765,6 +765,16 @@ shaka.media.DrmEngine = class { ...new Set( info.initData.map((initData) => initData.initDataType)), ]; + } else { + // In case initData is empty, request the media key system access + // with a default initDataTypes to prevent errors on some + // browser/keySystem couple when not providing one + // (example: Edge with com.microsoft.playready.recommendation) + // + // TODO: Request the MediaKeySystemAccess when receiving an + // encrypted event so we know which initDataTypes we should + // set + config.initDataTypes = ['cenc']; } if (info.distinctiveIdentifierRequired) { diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index daf1c23642..c6f3a09328 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -414,6 +414,7 @@ describe('DrmEngine', () => { distinctiveIdentifier: 'optional', persistentState: 'optional', sessionTypes: ['temporary'], + initDataTypes: ['cenc'], })]); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.def', [jasmine.objectContaining({ @@ -424,6 +425,7 @@ describe('DrmEngine', () => { distinctiveIdentifier: 'optional', persistentState: 'optional', sessionTypes: ['temporary'], + initDataTypes: ['cenc'], })]); }); @@ -442,6 +444,7 @@ describe('DrmEngine', () => { distinctiveIdentifier: 'optional', persistentState: 'required', sessionTypes: ['persistent-license'], + initDataTypes: ['cenc'], }), ]); expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledWith('drm.def', [ @@ -449,6 +452,7 @@ describe('DrmEngine', () => { distinctiveIdentifier: 'optional', persistentState: 'required', sessionTypes: ['persistent-license'], + initDataTypes: ['cenc'], }), ]); }); @@ -557,6 +561,7 @@ describe('DrmEngine', () => { })], distinctiveIdentifier: 'required', persistentState: 'required', + initDataTypes: ['cenc'], })]); }); @@ -612,6 +617,7 @@ describe('DrmEngine', () => { })], distinctiveIdentifier: 'required', persistentState: 'required', + initDataTypes: ['cenc'], })]); }); @@ -670,6 +676,7 @@ describe('DrmEngine', () => { robustness: 'really_really_ridiculously_good', })], persistentState: 'required', + initDataTypes: ['cenc'], })] ); }); @@ -677,8 +684,8 @@ describe('DrmEngine', () => { it('sets unique initDataTypes if specified from the initData', async () => { tweakDrmInfos((drmInfos) => { drmInfos[0].initData = [ - {initDataType: 'cenc', initData: new Uint8Array(5), keyId: null}, - {initDataType: 'cenc', initData: new Uint8Array(5), keyId: null}, + {initDataType: 'very_nice', initData: new Uint8Array(5), keyId: null}, + {initDataType: 'very_nice', initData: new Uint8Array(5), keyId: null}, ]; }); @@ -692,7 +699,7 @@ describe('DrmEngine', () => { expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(1); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith('drm.abc', [jasmine.objectContaining({ - initDataTypes: ['cenc'], + initDataTypes: ['very_nice'], })]); }); From f052a7e5df6a8473289c01e001e0f52506605e97 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 10 Aug 2020 16:51:40 +0200 Subject: [PATCH 18/42] fixup unit tests --- lib/media/drm_engine.js | 10 ---------- test/media/drm_engine_unit.js | 1 + 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 46a226a10f..d4e8e5e695 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -765,16 +765,6 @@ shaka.media.DrmEngine = class { ...new Set( info.initData.map((initData) => initData.initDataType)), ]; - } else { - // In case initData is empty, request the media key system access - // with a default initDataTypes to prevent errors on some - // browser/keySystem couple when not providing one - // (example: Edge with com.microsoft.playready.recommendation) - // - // TODO: Request the MediaKeySystemAccess when receiving an - // encrypted event so we know which initDataTypes we should - // set - config.initDataTypes = ['cenc']; } if (info.distinctiveIdentifierRequired) { diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index c6f3a09328..537a5514d8 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -2002,6 +2002,7 @@ describe('DrmEngine', () => { videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), + initData: [], }); }); }); // describe('getDrmInfo') From 89db3a0d6eaa3be4fedf53ecb923bb87ded7136b Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 10 Aug 2020 16:58:00 +0200 Subject: [PATCH 19/42] fix wrongly moved line --- test/media/drm_engine_unit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 537a5514d8..0d2360f164 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -2001,8 +2001,8 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, - keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), initData: [], + keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }); }); }); // describe('getDrmInfo') From 1a9cb6fd0f3ac261dcfd1508b0d046e32de6c097 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 2 Nov 2020 15:40:14 +0100 Subject: [PATCH 20/42] review --- demo/common/asset.js | 30 -------------- demo/common/assets.js | 4 +- docs/tutorials/drm-config.md | 10 ++--- lib/media/drm_engine.js | 74 ++++++++++++++++++----------------- test/media/drm_engine_unit.js | 26 ------------ 5 files changed, 43 insertions(+), 101 deletions(-) diff --git a/demo/common/asset.js b/demo/common/asset.js index dacb74cd8c..6f6265d2b3 100644 --- a/demo/common/asset.js +++ b/demo/common/asset.js @@ -57,8 +57,6 @@ const ShakaDemoAssetInfo = class { this.features = [shakaAssets.Feature.VOD]; /** @type {!Map.} */ this.licenseServers = new Map(); - /** @type {!Map.} */ - this.persistentStates = new Map(); /** @type {!Map.} */ this.licenseRequestHeaders = new Map(); /** @type {?shaka.extern.RequestFilter} */ @@ -257,17 +255,6 @@ const ShakaDemoAssetInfo = class { return this; } - /** - * - * @param {string} keySystem - * @param {boolean} persistentStateRequired - * @return {!ShakaDemoAssetInfo} - */ - setPersistentStateRequired(keySystem, persistentStateRequired) { - this.persistentStates.set(keySystem, persistentStateRequired); - return this; - } - /** * @param {shakaAssets.ExtraText} extraText * @return {!ShakaDemoAssetInfo} @@ -378,23 +365,6 @@ const ShakaDemoAssetInfo = class { }); } - if (this.persistentStates.size) { - this.persistentStates.forEach((value, key) => { - if (!config.drm.advanced[key]) { - config.drm.advanced[key] = { - distinctiveIdentifierRequired: false, - persistentStateRequired: false, - videoRobustness: '', - audioRobustness: '', - serverCertificate: new Uint8Array(0), - individualizationServer: '', - }; - } - - config.drm.advanced[key].persistentStateRequired = value; - }); - } - if (this.clearKeys.size) { config.drm.clearKeys = {}; this.clearKeys.forEach((value, key) => { diff --git a/demo/common/assets.js b/demo/common/assets.js index f7e51f0738..f5d4a6f721 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -1043,7 +1043,7 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.HIGH_DEFINITION) .addLicenseServer('com.microsoft.playready', 'https://content.uplynk.com/pr') .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') - .setPersistentStateRequired('com.microsoft.playready', true) + .setExtraConfig({drm: {advanced: {'com.microsoft.playready': {persistentStateRequired: true}}}}) .setRequestFilter(shakaAssets.UplynkRequestFilter) .setResponseFilter(shakaAssets.UplynkResponseFilter), // Reliable Playready playback requires Edge 16+ @@ -1061,7 +1061,7 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.HIGH_DEFINITION) .addLicenseServer('com.microsoft.playready', 'https://content.uplynk.com/pr') .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') - .setPersistentStateRequired('com.microsoft.playready', true) + .setExtraConfig({drm: {advanced: {'com.microsoft.playready': {persistentStateRequired: true}}}}) .setRequestFilter(shakaAssets.UplynkRequestFilter) .setResponseFilter(shakaAssets.UplynkResponseFilter), new ShakaDemoAssetInfo( diff --git a/docs/tutorials/drm-config.md b/docs/tutorials/drm-config.md index edb49c3d3f..6e987c551d 100644 --- a/docs/tutorials/drm-config.md +++ b/docs/tutorials/drm-config.md @@ -167,13 +167,9 @@ Microsoft Documentation: https://docs.microsoft.com/en-us/playready/overview/sec `com.microsoft.playready` key system ignores given robustness and stays at a `2000` decryption level. -If you specify a robustness into the configuration, Shaka will try to load and -start the key session with the `com.microsoft.playready.recommendation` key -system. That key system correctly implements the EME specification and will use -the given decryption robustness. - -It's up to you to detect and set the robustness if the playing platform is able -to handle it. +Shaka will firstly try to request a MediaKeyAccess with the recommendation keySystem +`com.microsoft.playready.recommendation` and in case it fails, silently fallback to +`com.microsoft.playready`. NB: Audio Hardware DRM is not supported (PlayReady limitation) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index d4e8e5e695..86aa9cef74 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -45,9 +45,6 @@ shaka.media.DrmEngine = class { /** @private {MediaKeys} */ this.mediaKeys_ = null; - /** @private {MediaKeySystemAccess} */ - this.mediaKeySystemAccess_ = null; - /** @private {HTMLMediaElement} */ this.video_ = null; @@ -773,7 +770,6 @@ shaka.media.DrmEngine = class { if (info.persistentStateRequired) { config.persistentState = 'required'; - config.sessionTypes = ['persistent-license']; } const robustness = (stream.type == ContentType.AUDIO) ? @@ -875,6 +871,24 @@ shaka.media.DrmEngine = class { } try { + if (keySystem == 'com.microsoft.playready') { + try { + mediaKeySystemAccess = + // eslint-disable-next-line no-await-in-loop + await navigator.requestMediaKeySystemAccess( + 'com.microsoft.playready.recommendation', + [config] + ); + + break; + } catch (error) { + shaka.log.error( + 'Requesting com.microsoft.playready.recommendation failed', + error + ); + } // Suppress errors + } + mediaKeySystemAccess = // eslint-disable-next-line no-await-in-loop await navigator.requestMediaKeySystemAccess(keySystem, [config]); break; @@ -916,9 +930,12 @@ shaka.media.DrmEngine = class { goog.asserts.assert(this.supportedTypes_.size, 'We should get at least one supported MIME type'); + const configKeySystem = shaka.media.DrmEngine.getConfigKeySystem_( + mediaKeySystemAccess.keySystem); + this.currentDrmInfo_ = shaka.media.DrmEngine.createDrmInfoFor_( mediaKeySystemAccess.keySystem, - configsByKeySystem.get(mediaKeySystemAccess.keySystem)); + configsByKeySystem.get(configKeySystem)); if (!this.currentDrmInfo_.licenseServerUri) { throw new shaka.util.Error( @@ -933,7 +950,6 @@ shaka.media.DrmEngine = class { shaka.log.info('Created MediaKeys object for key system', this.currentDrmInfo_.keySystem); - this.mediaKeySystemAccess_ = mediaKeySystemAccess; this.mediaKeys_ = mediaKeys; this.initialized_ = true; @@ -1095,13 +1111,8 @@ shaka.media.DrmEngine = class { let session; - const mediaKeySystemAccessConfiguration = - this.mediaKeySystemAccess_.getConfiguration(); - try { - if (this.usePersistentLicenses_ || - mediaKeySystemAccessConfiguration.persistentState === 'required' - ) { + if (this.usePersistentLicenses_) { shaka.log.v1('Creating new persistent session'); session = this.mediaKeys_.createSession('persistent-license'); } else { @@ -1434,14 +1445,14 @@ shaka.media.DrmEngine = class { // on Edge: 26 1d 5a 6e - 57 27 - d7 47 - 80 46 ea a5 d1 d3 4b 5a // Bug filed: https://bit.ly/2thuzXu + // NOTE that we skip this if byteLength != 16. This is used for the + // IE11 and Edge 12 EME polyfill, which uses single-byte dummy key IDs. + // However, unlike Edge and Chromecast, Tizen doesn't have this problem. if ( shaka.media.DrmEngine.isPlayReadyKeySystem_( this.currentDrmInfo_.keySystem ) ) { - // NOTE that we skip this if byteLength != 16. This is used for the - // IE11 and Edge 12 EME polyfill, which uses single-byte dummy key IDs. - // However, unlike Edge and Chromecast, Tizen doesn't have this problem. if (keyId.byteLength == 16 && (shaka.util.Platform.isIE() || shaka.util.Platform.isEdge()) ) { @@ -2096,27 +2107,18 @@ shaka.media.DrmEngine = class { } } - if (drmInfo.keySystem == 'com.microsoft.playready') { - // com.microsoft.playready does not support robustness nor persistent - // state so we need to switch to playready recommendation for that purpose - if ( - drmInfo.videoRobustness || - drmInfo.audioRobustness || - drmInfo.persistentStateRequired - ) { - drmInfo.keySystem = 'com.microsoft.playready.recommendation'; - } - - // Chromecast has a variant of PlayReady that uses a different key - // system ID. Since manifest parsers convert the standard PlayReady - // UUID to the standard PlayReady key system ID, here we will switch - // to the Chromecast version if we are running on that platform. - // Note that this must come after fillInDrmInfoDefaults_, since the - // player config uses the standard PlayReady ID for license server - // configuration. - if (window.cast && window.cast.__platform__) { - drmInfo.keySystem = 'com.chromecast.playready'; - } + // Chromecast has a variant of PlayReady that uses a different key + // system ID. Since manifest parsers convert the standard PlayReady + // UUID to the standard PlayReady key system ID, here we will switch + // to the Chromecast version if we are running on that platform. + // Note that this must come after fillInDrmInfoDefaults_, since the + // player config uses the standard PlayReady ID for license server + // configuration. + if (drmInfo.keySystem == 'com.microsoft.playready' && + window.cast && + window.cast.__platform__ + ) { + drmInfo.keySystem = 'com.chromecast.playready'; } } }; diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 0d2360f164..582433c70d 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -861,32 +861,6 @@ describe('DrmEngine', () => { .toHaveBeenCalledWith('cenc', initData3); }); - it('creates sessions with the correct sessionType', async () => { - // Set up init data overrides in the manifest: - /** @type {!Uint8Array} */ - const initData = new Uint8Array(5); - - tweakDrmInfos((drmInfos) => { - drmInfos[0].initData = [ - {initData: initData, initDataType: 'cenc', keyId: 'abc'}, - ]; - }); - - mockMediaKeySystemAccess.getConfiguration.and.returnValue({ - persistentState: 'required', - audioCapabilities: [{contentType: 'audio/webm'}], - videoCapabilities: [{contentType: 'video/mp4; codecs="fake"'}], - }); - - await initAndAttach(); - - // expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); - expect(mockMediaKeys.createSession).toHaveBeenCalledWith( - 'persistent-license' - ); - expect(session1.generateRequest).toHaveBeenCalledWith('cenc', initData); - }); - it('ignores duplicate init data overrides', async () => { // Set up init data overrides in the manifest; // The second initData has a different keyId from the first, From c0c88dda292883ed789082addedf9727e3620da3 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 2 Nov 2020 15:57:31 +0100 Subject: [PATCH 21/42] redo nitpicks --- lib/media/drm_engine.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 86aa9cef74..849442255f 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1561,8 +1561,8 @@ shaka.media.DrmEngine = class { * @private */ static isPlayReadyKeySystem_(keySystem) { - return keySystem === 'com.microsoft.playready.recommendation' - || keySystem === 'com.microsoft.playready'; + return keySystem == 'com.microsoft.playready.recommendation' || + keySystem == 'com.microsoft.playready'; } /** @@ -1574,7 +1574,7 @@ shaka.media.DrmEngine = class { * @private */ static getConfigKeySystem_(keySystem) { - if (keySystem === 'com.microsoft.playready.recommendation') { + if (keySystem == 'com.microsoft.playready.recommendation') { return 'com.microsoft.playready'; } From dfb1de50e8c091a946e29eae1f4b43205466e8d2 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 2 Nov 2020 16:34:19 +0100 Subject: [PATCH 22/42] fixup variant detection --- lib/media/drm_engine.js | 4 +++- test/media/drm_engine_unit.js | 7 ------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 849442255f..b526a80e56 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1767,7 +1767,9 @@ shaka.media.DrmEngine = class { const drmInfos = videoDrmInfos.concat(audioDrmInfos); return drmInfos.length == 0 || - drmInfos.some((drmInfo) => drmInfo.keySystem == keySystem); + drmInfos.some((drmInfo) => + drmInfo.keySystem == + shaka.media.DrmEngine.getConfigKeySystem_(keySystem)); } /** diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 582433c70d..941ca92f8f 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -636,13 +636,6 @@ describe('DrmEngine', () => { }); }); - // Add manifest-supplied license servers for both. - tweakDrmInfos((drmInfos) => { - for (const drmInfo of drmInfos) { - drmInfo.licenseServerUri = 'https://com.microsoft.playready/license'; - } - }); - setRequestMediaKeySystemAccessSpy([ 'com.microsoft.playready.recommendation', ]); From 0c38322dc0bf42738132b9d16bf62addd037feac Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 2 Nov 2020 17:30:50 +0100 Subject: [PATCH 23/42] fix nitpicks --- lib/media/drm_engine.js | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index b526a80e56..0e64d1e2d0 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -882,11 +882,10 @@ shaka.media.DrmEngine = class { break; } catch (error) { - shaka.log.error( + shaka.log.warning( 'Requesting com.microsoft.playready.recommendation failed', - error - ); - } // Suppress errors + error); + } } mediaKeySystemAccess = // eslint-disable-next-line no-await-in-loop @@ -1256,9 +1255,8 @@ shaka.media.DrmEngine = class { let url = this.currentDrmInfo_.licenseServerUri; const advancedConfig = this.config_.advanced[ shaka.media.DrmEngine.getConfigKeySystem_( - this.currentDrmInfo_.keySystem - ) - ]; + this.currentDrmInfo_.keySystem)]; + if (event.messageType == 'individualization-request' && advancedConfig && advancedConfig.individualizationServer) { url = advancedConfig.individualizationServer; @@ -1275,10 +1273,8 @@ shaka.media.DrmEngine = class { if ( shaka.media.DrmEngine.isPlayReadyKeySystem_( - this.currentDrmInfo_.keySystem - ) || - this.currentDrmInfo_.keySystem == 'com.chromecast.playready' - ) { + this.currentDrmInfo_.keySystem) || + this.currentDrmInfo_.keySystem == 'com.chromecast.playready') { this.unpackPlayReadyRequest_(request); } @@ -1451,11 +1447,9 @@ shaka.media.DrmEngine = class { if ( shaka.media.DrmEngine.isPlayReadyKeySystem_( this.currentDrmInfo_.keySystem - ) - ) { + )) { if (keyId.byteLength == 16 && - (shaka.util.Platform.isIE() || shaka.util.Platform.isEdge()) - ) { + (shaka.util.Platform.isIE() || shaka.util.Platform.isEdge())) { // Read out some fields in little-endian: const dataView = shaka.util.BufferUtils.toDataView(keyId); const part0 = dataView.getUint32(0, /* LE= */ true); @@ -2046,8 +2040,7 @@ shaka.media.DrmEngine = class { } const configKeySystem = shaka.media.DrmEngine.getConfigKeySystem_( - drmInfo.keySystem - ); + drmInfo.keySystem); // The order of preference for drmInfo: // 1. Clear Key config, used for debugging, should override everything else. @@ -2118,8 +2111,7 @@ shaka.media.DrmEngine = class { // configuration. if (drmInfo.keySystem == 'com.microsoft.playready' && window.cast && - window.cast.__platform__ - ) { + window.cast.__platform__) { drmInfo.keySystem = 'com.chromecast.playready'; } } From 1ad81c8bf2bc156e5f13f74cf04f4a4f0b799290 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 2 Nov 2020 17:38:50 +0100 Subject: [PATCH 24/42] fixup unit tests --- test/media/drm_engine_unit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 941ca92f8f..7def4d7e63 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -481,7 +481,7 @@ describe('DrmEngine', () => { .toHaveBeenCalledWith('drm.def', [jasmine.objectContaining({ distinctiveIdentifier: 'optional', persistentState: 'required', - sessionTypes: ['persistent-license'], + sessionTypes: ['temporary'], })]); }); From fbff53c51912d24d0243a77a5388581ab52e0a06 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 3 Nov 2020 08:16:04 +0100 Subject: [PATCH 25/42] enforce persistent-license session when persistentStateRequired --- lib/media/drm_engine.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 0e64d1e2d0..51edf5b14a 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1111,7 +1111,8 @@ shaka.media.DrmEngine = class { let session; try { - if (this.usePersistentLicenses_) { + if (this.usePersistentLicenses_ || + this.currentDrmInfo_.persistentStateRequired) { shaka.log.v1('Creating new persistent session'); session = this.mediaKeys_.createSession('persistent-license'); } else { From fb0897c378849c34fb82d21f3573f4fbda565ae9 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 3 Nov 2020 11:51:52 +0100 Subject: [PATCH 26/42] test recomendation on Edge only --- docs/tutorials/drm-config.md | 6 +++--- lib/media/drm_engine.js | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/drm-config.md b/docs/tutorials/drm-config.md index 6e987c551d..cbffe0bb35 100644 --- a/docs/tutorials/drm-config.md +++ b/docs/tutorials/drm-config.md @@ -167,9 +167,9 @@ Microsoft Documentation: https://docs.microsoft.com/en-us/playready/overview/sec `com.microsoft.playready` key system ignores given robustness and stays at a `2000` decryption level. -Shaka will firstly try to request a MediaKeyAccess with the recommendation keySystem -`com.microsoft.playready.recommendation` and in case it fails, silently fallback to -`com.microsoft.playready`. +On Edge, Shaka will firstly try to request a MediaKeyAccess with the +recommendation keySystem `com.microsoft.playready.recommendation` and in case +it fails, silently fallback to `com.microsoft.playready`. NB: Audio Hardware DRM is not supported (PlayReady limitation) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 51edf5b14a..0f83d2ef0a 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -871,7 +871,8 @@ shaka.media.DrmEngine = class { } try { - if (keySystem == 'com.microsoft.playready') { + if (keySystem == 'com.microsoft.playready' && + shaka.util.Platform.isEdge()) { try { mediaKeySystemAccess = // eslint-disable-next-line no-await-in-loop From 7fe9fc8f076c35ce9d64ccb73d1f41fb90672840 Mon Sep 17 00:00:00 2001 From: valotvince Date: Wed, 4 Nov 2020 15:40:56 +0100 Subject: [PATCH 27/42] add a sessionType configuration in advanced config --- demo/common/assets.js | 6 ++-- demo/main.js | 1 + externs/shaka/manifest.js | 4 +++ externs/shaka/player.js | 6 +++- lib/media/drm_engine.js | 41 +++++++++++++++++++-------- lib/util/manifest_parser_utils.js | 1 + lib/util/player_configuration.js | 1 + test/media/drm_engine_unit.js | 5 ++++ test/offline/manifest_convert_unit.js | 1 + test/offline/storage_integration.js | 1 + test/test/util/manifest_generator.js | 2 ++ 11 files changed, 54 insertions(+), 15 deletions(-) diff --git a/demo/common/assets.js b/demo/common/assets.js index f5d4a6f721..b39196c819 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -1043,7 +1043,8 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.HIGH_DEFINITION) .addLicenseServer('com.microsoft.playready', 'https://content.uplynk.com/pr') .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') - .setExtraConfig({drm: {advanced: {'com.microsoft.playready': {persistentStateRequired: true}}}}) + .setExtraConfig({drm: {advanced: { + 'com.microsoft.playready': {sessionType: 'persistent-license'}}}}) .setRequestFilter(shakaAssets.UplynkRequestFilter) .setResponseFilter(shakaAssets.UplynkResponseFilter), // Reliable Playready playback requires Edge 16+ @@ -1061,7 +1062,8 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.HIGH_DEFINITION) .addLicenseServer('com.microsoft.playready', 'https://content.uplynk.com/pr') .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') - .setExtraConfig({drm: {advanced: {'com.microsoft.playready': {persistentStateRequired: true}}}}) + .setExtraConfig({drm: {advanced: { + 'com.microsoft.playready': {sessionType: 'persistent-license'}}}}) .setRequestFilter(shakaAssets.UplynkRequestFilter) .setResponseFilter(shakaAssets.UplynkResponseFilter), new ShakaDemoAssetInfo( 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/externs/shaka/manifest.js b/externs/shaka/manifest.js index bf3c7b6f73..4cc48299b1 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -111,6 +111,7 @@ shaka.extern.InitDataOverride; * audioRobustness: string, * videoRobustness: string, * serverCertificate: Uint8Array, + * sessionTypes: Array., * 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 {Array.} sessionTypes + * 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 db88f8c366..94ae2f2dcd 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,9 @@ 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 temporary in most cases.
+ * The MediaKey Session type to create when initiating a DRM session. * * @exportDoc */ diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 0f83d2ef0a..065f3b7f1a 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -772,6 +772,10 @@ shaka.media.DrmEngine = class { config.persistentState = 'required'; } + if (info.sessionTypes && info.sessionTypes.length) { + config.sessionTypes = info.sessionTypes; + } + const robustness = (stream.type == ContentType.AUDIO) ? info.audioRobustness : info.videoRobustness; @@ -883,16 +887,21 @@ shaka.media.DrmEngine = class { break; } catch (error) { - shaka.log.warning( - 'Requesting com.microsoft.playready.recommendation failed', - error); + shaka.log.v2( + 'Requesting com.microsoft.playready.recommendation', + 'failed with config', + config, error); } } 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) { @@ -916,6 +925,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 || []; @@ -1024,6 +1038,7 @@ shaka.media.DrmEngine = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, + sessionTypes: ['temporary'], initData: initDatas, keyIds: new Set(keyIds), }; @@ -1112,14 +1127,11 @@ shaka.media.DrmEngine = class { let session; try { - if (this.usePersistentLicenses_ || - this.currentDrmInfo_.persistentStateRequired) { - 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(); - } + const sessionType = this.currentDrmInfo_.sessionTypes[0]; + + 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, @@ -1952,6 +1964,7 @@ shaka.media.DrmEngine = class { licenseServerUri: licenseServers[0], distinctiveIdentifierRequired: (distinctiveIdentifier == 'required'), persistentStateRequired: (config.persistentState == 'required'), + sessionTypes: config.sessionTypes || ['temporary'], audioRobustness: audioRobustness || '', videoRobustness: videoRobustness || '', serverCertificate: serverCerts[0], @@ -2102,6 +2115,10 @@ shaka.media.DrmEngine = class { if (!drmInfo.serverCertificate) { drmInfo.serverCertificate = advancedConfig.serverCertificate; } + + if (advancedConfig.sessionType) { + drmInfo.sessionTypes = [advancedConfig.sessionType]; + } } // Chromecast has a variant of PlayReady that uses a different key diff --git a/lib/util/manifest_parser_utils.js b/lib/util/manifest_parser_utils.js index 47d365614d..9f7a4aee50 100644 --- a/lib/util/manifest_parser_utils.js +++ b/lib/util/manifest_parser_utils.js @@ -54,6 +54,7 @@ shaka.util.ManifestParserUtils = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, + sessionTypes: [], initData: initData || [], keyIds: new Set(), }; diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index de31c80f66..9abab4d92e 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -278,6 +278,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 7def4d7e63..62979033f4 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -538,6 +538,7 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: null, + sessionType: '', individualizationServer: '', distinctiveIdentifierRequired: true, persistentStateRequired: true, @@ -594,6 +595,7 @@ describe('DrmEngine', () => { audioRobustness: 'bad', videoRobustness: 'so_bad_it_hurts', serverCertificate: null, + sessionType: '', individualizationServer: '', distinctiveIdentifierRequired: false, persistentStateRequired: false, @@ -646,6 +648,7 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: null, + sessionType: '', individualizationServer: '', distinctiveIdentifierRequired: false, persistentStateRequired: true, @@ -1951,6 +1954,7 @@ describe('DrmEngine', () => { videoRobustness: 'really_really_ridiculously_good', distinctiveIdentifierRequired: true, serverCertificate: null, + sessionType: '', individualizationServer: '', persistentStateRequired: true, }; @@ -2297,6 +2301,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..6ad60f835c 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]), + sessionTypes: [], 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..77a6520f18 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, + sessionTypes: ['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..bf25bd66fb 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 {Array.} */ + this.sessionTypes = []; /** @type {shaka.extern.DrmInfo} */ const foo = this; From 96c85ccc514957dbd7fc8c3e3fc63e7bc2af1ff4 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 4 Nov 2020 17:55:58 +0100 Subject: [PATCH 28/42] update unit tests --- test/media/drm_engine_unit.js | 42 +++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 62979033f4..fcf38475e1 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'); @@ -27,6 +28,7 @@ describe('DrmEngine', () => { const originalRequestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess; + const originalPlatformIsEdge = shaka.util.Platform.isEdge; const originalLogError = shaka.log.error; const originalBatchTime = shaka.media.DrmEngine.KEY_STATUS_BATCH_TIME; @@ -150,6 +152,7 @@ describe('DrmEngine', () => { navigator.requestMediaKeySystemAccess = originalRequestMediaKeySystemAccess; shaka.log.error = originalLogError; + shaka.util.Platform.isEdge = originalPlatformIsEdge; }); describe('supportsVariants', () => { @@ -538,7 +541,7 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: null, - sessionType: '', + sessionType: 'persistent-license', individualizationServer: '', distinctiveIdentifierRequired: true, persistentStateRequired: true, @@ -562,6 +565,7 @@ describe('DrmEngine', () => { })], distinctiveIdentifier: 'required', persistentState: 'required', + sessionTypes: ['persistent-license'], initDataTypes: ['cenc'], })]); }); @@ -623,7 +627,9 @@ describe('DrmEngine', () => { })]); }); - it('selects the correct configuration for PlayReady', async () => { + it('should silently try PlayReady recommendation keySytem', async () => { + shaka.util.Platform.isEdge = () => true; + // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { @@ -639,7 +645,7 @@ describe('DrmEngine', () => { }); setRequestMediaKeySystemAccessSpy([ - 'com.microsoft.playready.recommendation', + 'com.microsoft.playready', ]); config.servers = { 'com.microsoft.playready': 'https://com.microsoft.playready/license', @@ -659,21 +665,28 @@ describe('DrmEngine', () => { const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); + const expectedConfig = jasmine.objectContaining({ + audioCapabilities: [jasmine.objectContaining({ + robustness: 'good', + })], + videoCapabilities: [jasmine.objectContaining({ + robustness: 'really_really_ridiculously_good', + })], + persistentState: 'required', + initDataTypes: ['cenc'], + }); + expect(drmEngine.initialized()).toBe(true); - expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(1); + expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(2); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith( 'com.microsoft.playready.recommendation', - [jasmine.objectContaining({ - audioCapabilities: [jasmine.objectContaining({ - robustness: 'good', - })], - videoCapabilities: [jasmine.objectContaining({ - robustness: 'really_really_ridiculously_good', - })], - persistentState: 'required', - initDataTypes: ['cenc'], - })] + [expectedConfig] + ); + expect(requestMediaKeySystemAccessSpy) + .toHaveBeenCalledWith( + 'com.microsoft.playready', + [expectedConfig] ); }); @@ -1972,6 +1985,7 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, + sessionTypes: ['temporary'], initData: [], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }); From 594090519039176f73c470ff470ca0dbc221fac7 Mon Sep 17 00:00:00 2001 From: valotvince Date: Wed, 11 Nov 2020 10:38:36 +0100 Subject: [PATCH 29/42] review --- externs/shaka/player.js | 5 +++-- lib/media/drm_engine.js | 24 ++++++++++++------------ test/media/drm_engine_unit.js | 24 ++---------------------- 3 files changed, 17 insertions(+), 36 deletions(-) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 94ae2f2dcd..03b190b3c7 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -549,8 +549,9 @@ shaka.extern.EmsgInfo; * The server that handles an 'individualiation-request'. If the * server isn't given, it will default to the license server. * @property {string} sessionType - * Defaults temporary in most cases.
- * The MediaKey Session type to create when initiating a DRM session. + * 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 065f3b7f1a..abc3db5a90 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -562,7 +562,11 @@ shaka.media.DrmEngine = class { * @param {?shaka.extern.DrmInfo} drmInfo * @return {string} */ static keySystem(drmInfo) { - return drmInfo ? drmInfo.keySystem : ''; + if (drmInfo && drmInfo.keySystem) { + return shaka.media.DrmEngine.getConfigKeySystem_(drmInfo.keySystem); + } + + return ''; } /** @@ -1268,8 +1272,7 @@ shaka.media.DrmEngine = class { let url = this.currentDrmInfo_.licenseServerUri; const advancedConfig = this.config_.advanced[ - shaka.media.DrmEngine.getConfigKeySystem_( - this.currentDrmInfo_.keySystem)]; + shaka.media.DrmEngine.keySystem(this.currentDrmInfo_)]; if (event.messageType == 'individualization-request' && advancedConfig && advancedConfig.individualizationServer) { @@ -1775,9 +1778,7 @@ shaka.media.DrmEngine = class { const drmInfos = videoDrmInfos.concat(audioDrmInfos); return drmInfos.length == 0 || - drmInfos.some((drmInfo) => - drmInfo.keySystem == - shaka.media.DrmEngine.getConfigKeySystem_(keySystem)); + drmInfos.some((drmInfo) => drmInfo.keySystem == keySystem); } /** @@ -2054,8 +2055,7 @@ shaka.media.DrmEngine = class { return; } - const configKeySystem = shaka.media.DrmEngine.getConfigKeySystem_( - drmInfo.keySystem); + const keySystem = shaka.media.DrmEngine.keySystem(drmInfo); // The order of preference for drmInfo: // 1. Clear Key config, used for debugging, should override everything else. @@ -2072,14 +2072,14 @@ shaka.media.DrmEngine = class { // The only way to get license servers from the manifest is not to specify // any in your player config. - if (drmInfo.keySystem == 'org.w3.clearkey' && drmInfo.licenseServerUri) { + if (keySystem == 'org.w3.clearkey' && drmInfo.licenseServerUri) { // Preference 1: Clear Key with pre-configured keys will have a data URI // assigned as its license server. Don't change anything. return; } else if (servers.size) { // Preference 2: If anything is configured at the application level, // override whatever was in the manifest. - const server = servers.get(configKeySystem) || ''; + const server = servers.get(keySystem) || ''; drmInfo.licenseServerUri = server; } else { @@ -2091,7 +2091,7 @@ shaka.media.DrmEngine = class { drmInfo.keyIds = new Set(); } - const advancedConfig = advancedConfigs.get(configKeySystem); + const advancedConfig = advancedConfigs.get(keySystem); if (advancedConfig) { if (!drmInfo.distinctiveIdentifierRequired) { @@ -2128,7 +2128,7 @@ shaka.media.DrmEngine = class { // Note that this must come after fillInDrmInfoDefaults_, since the // player config uses the standard PlayReady ID for license server // configuration. - if (drmInfo.keySystem == 'com.microsoft.playready' && + if (keySystem == 'com.microsoft.playready' && window.cast && window.cast.__platform__) { drmInfo.keySystem = 'com.chromecast.playready'; diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index fcf38475e1..fa388c12fe 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -650,43 +650,23 @@ describe('DrmEngine', () => { config.servers = { 'com.microsoft.playready': 'https://com.microsoft.playready/license', }; - config.advanced['com.microsoft.playready'] = { - audioRobustness: 'good', - videoRobustness: 'really_really_ridiculously_good', - serverCertificate: null, - sessionType: '', - individualizationServer: '', - distinctiveIdentifierRequired: false, - persistentStateRequired: true, - }; drmEngine.configure(config); const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); - const expectedConfig = jasmine.objectContaining({ - audioCapabilities: [jasmine.objectContaining({ - robustness: 'good', - })], - videoCapabilities: [jasmine.objectContaining({ - robustness: 'really_really_ridiculously_good', - })], - persistentState: 'required', - initDataTypes: ['cenc'], - }); - expect(drmEngine.initialized()).toBe(true); expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(2); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith( 'com.microsoft.playready.recommendation', - [expectedConfig] + jasmine.any(Array) ); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith( 'com.microsoft.playready', - [expectedConfig] + jasmine.any(Array) ); }); From a1f3503c9944ccb59d0f40c57021e63ab94aefbd Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 18 Nov 2020 09:27:36 +0100 Subject: [PATCH 30/42] fixup code style --- lib/media/drm_engine.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index abc3db5a90..0eb5a0b3b7 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -886,8 +886,7 @@ shaka.media.DrmEngine = class { // eslint-disable-next-line no-await-in-loop await navigator.requestMediaKeySystemAccess( 'com.microsoft.playready.recommendation', - [config] - ); + [config]); break; } catch (error) { From 7d4b1ede4849c90e4a14aa546279fbc1328b5778 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 23 Feb 2021 16:37:40 +0100 Subject: [PATCH 31/42] fixup after rebase --- lib/media/drm_engine.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 0eb5a0b3b7..40fb822f9c 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1635,6 +1635,7 @@ shaka.media.DrmEngine = class { ]; const basicConfig = { + initDataTypes: ['cenc'], videoCapabilities: basicVideoCapabilities, }; const offlineConfig = { From ab6e143189751b2a79ecedd00e1263fb59ab4765 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 23 Feb 2021 18:01:40 +0100 Subject: [PATCH 32/42] remove session if they were persistent in playback --- lib/media/drm_engine.js | 40 +++++++++++++++++++++++++++++------ test/media/drm_engine_unit.js | 37 ++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 40fb822f9c..b94bbe0934 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1054,9 +1054,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, @@ -1078,6 +1081,7 @@ shaka.media.DrmEngine = class { loaded: false, oldExpiration: Infinity, updatePromise: null, + type: sessionType, }; this.activeSessions_.set(session, metadata); @@ -1129,9 +1133,9 @@ shaka.media.DrmEngine = class { let session; - try { - const sessionType = this.currentDrmInfo_.sessionTypes[0]; + const sessionType = this.currentDrmInfo_.sessionTypes[0]; + try { shaka.log.info('Creating new', sessionType, 'session'); session = this.mediaKeys_.createSession(sessionType); @@ -1155,6 +1159,7 @@ shaka.media.DrmEngine = class { loaded: false, oldExpiration: Infinity, updatePromise: null, + type: sessionType, }; this.activeSessions_.set(session, metadata); @@ -1462,7 +1467,7 @@ shaka.media.DrmEngine = class { // However, unlike Edge and Chromecast, Tizen doesn't have this problem. if ( shaka.media.DrmEngine.isPlayReadyKeySystem_( - this.currentDrmInfo_.keySystem + this.currentDrmInfo_.keySystem, )) { if (keyId.byteLength == 16 && (shaka.util.Platform.isIE() || shaka.util.Platform.isEdge())) { @@ -1728,19 +1733,37 @@ shaka.media.DrmEngine = class { async closeOpenSessions_() { // Close all open sessions. const openSessions = Array.from(this.activeSessions_.keys()); - 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); + const metadata = this.activeSessions_.get(session); 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.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); } })); + + this.activeSessions_.clear(); } /** @@ -2142,6 +2165,7 @@ shaka.media.DrmEngine = class { * loaded: boolean, * initData: Uint8Array, * oldExpiration: number, + * type: string, * updatePromise: shaka.util.PublicPromise * }} * @@ -2156,6 +2180,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/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index fa388c12fe..2f797bc385 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -661,12 +661,12 @@ describe('DrmEngine', () => { expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith( 'com.microsoft.playready.recommendation', - jasmine.any(Array) + jasmine.any(Array), ); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith( 'com.microsoft.playready', - jasmine.any(Array) + jasmine.any(Array), ); }); @@ -1559,10 +1559,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).toHaveBeenCalled(); + expect(session1.remove).toHaveBeenCalled(); + + expect(session2.close).toHaveBeenCalled(); + expect(session2.remove).toHaveBeenCalled(); + }); + it('swallows errors when closing sessions', async () => { await initAndAttach(); const initData1 = new Uint8Array(1); From 9aa90a115ad8b7f3fbf7a0ae68d6889ede8bc653 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 15 Mar 2021 17:28:19 +0100 Subject: [PATCH 33/42] add PlayReady cast inside the playready systems, remove IE check, revert activeSessions clearing --- lib/media/drm_engine.js | 28 +++++++++------------------- test/media/drm_engine_unit.js | 4 ++-- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index b94bbe0934..16ad51ab40 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1294,8 +1294,7 @@ shaka.media.DrmEngine = class { if ( shaka.media.DrmEngine.isPlayReadyKeySystem_( - this.currentDrmInfo_.keySystem) || - this.currentDrmInfo_.keySystem == 'com.chromecast.playready') { + this.currentDrmInfo_.keySystem)) { this.unpackPlayReadyRequest_(request); } @@ -1481,15 +1480,6 @@ shaka.media.DrmEngine = class { dataView.setUint16(4, part1, /* BE= */ false); dataView.setUint16(6, part2, /* BE= */ false); } - - // Microsoft's implementation in IE11 seems to never set key status to - // 'usable'. It is stuck forever at 'status-pending'. In spite of this, - // the keys do seem to be usable and content plays correctly. - // Bug filed: https://bit.ly/2tpIU3n - // Microsoft has fixed the issue on Edge, but it remains in IE. - if (status == 'status-pending') { - status = 'usable'; - } } if (status != 'status-pending') { @@ -1572,12 +1562,14 @@ shaka.media.DrmEngine = class { /** * * @param {string} keySystem + * @return {boolean} * * @private */ static isPlayReadyKeySystem_(keySystem) { return keySystem == 'com.microsoft.playready.recommendation' || - keySystem == 'com.microsoft.playready'; + keySystem == 'com.microsoft.playready' || + keySystem == 'com.chromecast.playready'; } /** @@ -1585,11 +1577,12 @@ shaka.media.DrmEngine = class { * to add other key systems into their configuration. * * @param {string} keySystem + * @return {string} * * @private */ static getConfigKeySystem_(keySystem) { - if (keySystem == 'com.microsoft.playready.recommendation') { + if (shaka.media.DrmEngine.isPlayReadyKeySystem_(keySystem)) { return 'com.microsoft.playready'; } @@ -1732,12 +1725,11 @@ 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) => { - const metadata = this.activeSessions_.get(session); - + await Promise.all(openSessions.map(async ([session, metadata]) => { try { /** * Special case when a persistent-license session has been initiated, @@ -1762,8 +1754,6 @@ shaka.media.DrmEngine = class { shaka.log.error('Failed to close or remove the session', error); } })); - - this.activeSessions_.clear(); } /** diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 2f797bc385..aca87a5b2a 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -1589,10 +1589,10 @@ describe('DrmEngine', () => { mockVideo.setMediaKeys.calls.reset(); await drmEngine.destroy(); - expect(session1.close).toHaveBeenCalled(); + expect(session1.close).not.toHaveBeenCalled(); expect(session1.remove).toHaveBeenCalled(); - expect(session2.close).toHaveBeenCalled(); + expect(session2.close).not.toHaveBeenCalled(); expect(session2.remove).toHaveBeenCalled(); }); From 9d8fcdab3610fefaa8c620e3ae9fd87c4000b0c8 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 15 Mar 2021 18:55:51 +0100 Subject: [PATCH 34/42] change DrmInfo sessionTypes to sessionType to match shaka configuration per advanced config --- externs/shaka/manifest.js | 6 +++--- lib/media/drm_engine.js | 12 ++++++------ lib/util/manifest_parser_utils.js | 2 +- test/media/drm_engine_unit.js | 2 +- test/offline/manifest_convert_unit.js | 2 +- test/offline/storage_integration.js | 2 +- test/test/util/manifest_generator.js | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index 4cc48299b1..13be37baba 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -111,7 +111,7 @@ shaka.extern.InitDataOverride; * audioRobustness: string, * videoRobustness: string, * serverCertificate: Uint8Array, - * sessionTypes: Array., + * sessionType: string, * initData: Array., * keyIds: Set. * }} @@ -133,8 +133,8 @@ 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 {Array.} sessionTypes - * Defaults to ['temporary'] if Shaka wasn't initiated for 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 diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 16ad51ab40..aa961af37f 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -776,8 +776,8 @@ shaka.media.DrmEngine = class { config.persistentState = 'required'; } - if (info.sessionTypes && info.sessionTypes.length) { - config.sessionTypes = info.sessionTypes; + if (info.sessionType && info.sessionType.length) { + config.sessionTypes = [info.sessionType]; } const robustness = (stream.type == ContentType.AUDIO) ? @@ -1041,7 +1041,7 @@ shaka.media.DrmEngine = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, - sessionTypes: ['temporary'], + sessionType: '', initData: initDatas, keyIds: new Set(keyIds), }; @@ -1133,7 +1133,7 @@ shaka.media.DrmEngine = class { let session; - const sessionType = this.currentDrmInfo_.sessionTypes[0]; + const sessionType = this.currentDrmInfo_.sessionType; try { shaka.log.info('Creating new', sessionType, 'session'); @@ -1978,7 +1978,7 @@ shaka.media.DrmEngine = class { licenseServerUri: licenseServers[0], distinctiveIdentifierRequired: (distinctiveIdentifier == 'required'), persistentStateRequired: (config.persistentState == 'required'), - sessionTypes: config.sessionTypes || ['temporary'], + sessionType: config.sessionTypes[0] || 'temporary', audioRobustness: audioRobustness || '', videoRobustness: videoRobustness || '', serverCertificate: serverCerts[0], @@ -2130,7 +2130,7 @@ shaka.media.DrmEngine = class { } if (advancedConfig.sessionType) { - drmInfo.sessionTypes = [advancedConfig.sessionType]; + drmInfo.sessionType = advancedConfig.sessionType; } } diff --git a/lib/util/manifest_parser_utils.js b/lib/util/manifest_parser_utils.js index 9f7a4aee50..b18f81ec1e 100644 --- a/lib/util/manifest_parser_utils.js +++ b/lib/util/manifest_parser_utils.js @@ -54,7 +54,7 @@ shaka.util.ManifestParserUtils = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, - sessionTypes: [], + sessionType: '', initData: initData || [], keyIds: new Set(), }; diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index aca87a5b2a..80e2eb0625 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -1998,7 +1998,7 @@ describe('DrmEngine', () => { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, - sessionTypes: ['temporary'], + sessionType: 'temporary', initData: [], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }); diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index 6ad60f835c..1dd680a9fc 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -101,7 +101,7 @@ describe('ManifestConverter', () => { audioRobustness: 'very', videoRobustness: 'kinda_sorta', serverCertificate: new Uint8Array([1, 2, 3]), - sessionTypes: [], + 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 77a6520f18..9b6a5a32df 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -1587,7 +1587,7 @@ filterDescribe('Storage', storageSupport, () => { distinctiveIdentifierRequired: false, initData: null, keyIds: null, - sessionTypes: ['temporary'], + 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 bf25bd66fb..029fd84c3f 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -405,8 +405,8 @@ shaka.test.ManifestGenerator.DrmInfo = class { this.initData = null; /** @type {Set.} */ this.keyIds = new Set(); - /** @type {Array.} */ - this.sessionTypes = []; + /** @type {string} */ + this.sessionType = ''; /** @type {shaka.extern.DrmInfo} */ const foo = this; From 02092645646e6d4ef400f8a5ed845f6de02633c8 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 15 Mar 2021 19:03:34 +0100 Subject: [PATCH 35/42] code styling fixes --- lib/media/drm_engine.js | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index aa961af37f..676e3143aa 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1292,9 +1292,8 @@ shaka.media.DrmEngine = class { request.sessionId = session.sessionId; // NOTE: allowCrossSiteCredentials can be set in a request filter. - if ( - shaka.media.DrmEngine.isPlayReadyKeySystem_( - this.currentDrmInfo_.keySystem)) { + if (shaka.media.DrmEngine.isPlayReadyKeySystem_( + this.currentDrmInfo_.keySystem)) { this.unpackPlayReadyRequest_(request); } @@ -1464,22 +1463,19 @@ shaka.media.DrmEngine = class { // NOTE that we skip this if byteLength != 16. This is used for the // IE11 and Edge 12 EME polyfill, which uses single-byte dummy key IDs. // However, unlike Edge and Chromecast, Tizen doesn't have this problem. - if ( - shaka.media.DrmEngine.isPlayReadyKeySystem_( - this.currentDrmInfo_.keySystem, - )) { - if (keyId.byteLength == 16 && - (shaka.util.Platform.isIE() || shaka.util.Platform.isEdge())) { - // Read out some fields in little-endian: - const dataView = shaka.util.BufferUtils.toDataView(keyId); - const part0 = dataView.getUint32(0, /* LE= */ true); - const part1 = dataView.getUint16(4, /* LE= */ true); - const part2 = dataView.getUint16(6, /* LE= */ true); - // Write it back in big-endian: - dataView.setUint32(0, part0, /* BE= */ false); - dataView.setUint16(4, part1, /* BE= */ false); - dataView.setUint16(6, part2, /* BE= */ false); - } + if (shaka.media.DrmEngine.isPlayReadyKeySystem_( + this.currentDrmInfo_.keySystem) && + keyId.byteLength == 16 && + shaka.util.Platform.isEdge()) { + // Read out some fields in little-endian: + const dataView = shaka.util.BufferUtils.toDataView(keyId); + const part0 = dataView.getUint32(0, /* LE= */ true); + const part1 = dataView.getUint16(4, /* LE= */ true); + const part2 = dataView.getUint16(6, /* LE= */ true); + // Write it back in big-endian: + dataView.setUint32(0, part0, /* BE= */ false); + dataView.setUint16(4, part1, /* BE= */ false); + dataView.setUint16(6, part2, /* BE= */ false); } if (status != 'status-pending') { @@ -1738,7 +1734,7 @@ shaka.media.DrmEngine = class { * the playback session ends */ if (!this.offlineSessionIds_.includes(session.sessionId) && - metadata.type === 'persistent-license') { + metadata.type === 'persistent-license') { shaka.log.v1('Removing session', session.sessionId); await session.remove(); From 0cf8d7a518e38a17bc194733cb902a6fb8523e65 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 15 Mar 2021 19:06:56 +0100 Subject: [PATCH 36/42] remove some references to IE11 --- lib/media/drm_engine.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 676e3143aa..fb231858e5 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -384,7 +384,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, @@ -1375,7 +1375,7 @@ shaka.media.DrmEngine = class { * @private */ unpackPlayReadyRequest_(request) { - // On IE and Edge, the raw license message is UTF-16-encoded XML. We need + // On Edge, the raw license message is UTF-16-encoded XML. We need // to unpack the Challenge element (base64-encoded string containing the // actual license request) and any HttpHeader elements (sent as request // headers). @@ -1401,7 +1401,7 @@ shaka.media.DrmEngine = class { const xml = shaka.util.StringUtils.fromUTF16( request.body, /* littleEndian= */ true, /* noThrow= */ true); if (!xml.includes('PlayReadyKeyMessage')) { - // This does not appear to be a wrapped message as on IE and Edge. Some + // This does not appear to be a wrapped message as on Edge. Some // clients do not need this unwrapping, so we will assume this is one of // them. Note that "xml" at this point probably looks like random // garbage, since we interpreted UTF-8 as UTF-16. @@ -1461,7 +1461,7 @@ shaka.media.DrmEngine = class { // Bug filed: https://bit.ly/2thuzXu // NOTE that we skip this if byteLength != 16. This is used for the - // IE11 and Edge 12 EME polyfill, which uses single-byte dummy key IDs. + // Edge 12 EME polyfill, which uses single-byte dummy key IDs. // However, unlike Edge and Chromecast, Tizen doesn't have this problem. if (shaka.media.DrmEngine.isPlayReadyKeySystem_( this.currentDrmInfo_.keySystem) && From e229bdee5e2bb0a46bbb5f0d0228dde7571fd091 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Tue, 16 Mar 2021 11:04:22 +0100 Subject: [PATCH 37/42] add documentation on a potential fallback fail --- docs/tutorials/drm-config.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/tutorials/drm-config.md b/docs/tutorials/drm-config.md index cbffe0bb35..a11d13e32b 100644 --- a/docs/tutorials/drm-config.md +++ b/docs/tutorials/drm-config.md @@ -173,6 +173,11 @@ it fails, silently fallback to `com.microsoft.playready`. NB: Audio Hardware DRM is not supported (PlayReady limitation) +NB: If you are using **purchase persistent licenses**, you will need to set the +`com.microsoft.playready` advancedConfig sessionType to `persistent-license`. +Not doing so will result of the license being rejected by the CDM, without the +fallback cited above. + ##### Other key-systems Values for other key systems are not known to us at this time. From a1a832a6c012a784be08843e1e4ad730efc9f713 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Wed, 17 Mar 2021 08:57:05 +0100 Subject: [PATCH 38/42] code style adjustment --- lib/media/drm_engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index fb231858e5..68b923ff4a 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -776,7 +776,7 @@ shaka.media.DrmEngine = class { config.persistentState = 'required'; } - if (info.sessionType && info.sessionType.length) { + if (info.sessionType) { config.sessionTypes = [info.sessionType]; } From 70a565212d8c631de58d60d5f9cad1486f4fcbb6 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 19 Mar 2021 08:55:58 +0100 Subject: [PATCH 39/42] hopefully fix tests on cast platform --- test/media/drm_engine_unit.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 80e2eb0625..c17b3faf6a 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -646,6 +646,9 @@ describe('DrmEngine', () => { setRequestMediaKeySystemAccessSpy([ 'com.microsoft.playready', + // Specific case for tests on the Chromecast platform since + // com.microsoft.playready is changed into the keySystem below + 'com.chromecast.playready', ]); config.servers = { 'com.microsoft.playready': 'https://com.microsoft.playready/license', @@ -665,7 +668,8 @@ describe('DrmEngine', () => { ); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith( - 'com.microsoft.playready', + jasmine.stringMatching( + /^com\.(microsoft|chromecast)\.playready$/), jasmine.any(Array), ); }); From 94bd5e2094c7a2115a19a188323971dfe423975d Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 19 Mar 2021 10:16:06 +0100 Subject: [PATCH 40/42] prevent playing Edge test on other platforms --- test/media/drm_engine_unit.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index c17b3faf6a..fd9a57bba1 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -28,7 +28,6 @@ describe('DrmEngine', () => { const originalRequestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess; - const originalPlatformIsEdge = shaka.util.Platform.isEdge; const originalLogError = shaka.log.error; const originalBatchTime = shaka.media.DrmEngine.KEY_STATUS_BATCH_TIME; @@ -152,7 +151,6 @@ describe('DrmEngine', () => { navigator.requestMediaKeySystemAccess = originalRequestMediaKeySystemAccess; shaka.log.error = originalLogError; - shaka.util.Platform.isEdge = originalPlatformIsEdge; }); describe('supportsVariants', () => { @@ -628,7 +626,10 @@ describe('DrmEngine', () => { }); it('should silently try PlayReady recommendation keySytem', async () => { - shaka.util.Platform.isEdge = () => true; + if (!shaka.util.Platform.isEdge()) { + pending('PlayReady recommendation is only available on Edge'); + return; + } // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { @@ -646,9 +647,6 @@ describe('DrmEngine', () => { setRequestMediaKeySystemAccessSpy([ 'com.microsoft.playready', - // Specific case for tests on the Chromecast platform since - // com.microsoft.playready is changed into the keySystem below - 'com.chromecast.playready', ]); config.servers = { 'com.microsoft.playready': 'https://com.microsoft.playready/license', @@ -668,8 +666,7 @@ describe('DrmEngine', () => { ); expect(requestMediaKeySystemAccessSpy) .toHaveBeenCalledWith( - jasmine.stringMatching( - /^com\.(microsoft|chromecast)\.playready$/), + 'com.microsoft.playready', jasmine.any(Array), ); }); From fe65b22640aa375abaf3c2b7b84f7aa31d821788 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 19 Mar 2021 11:00:53 +0100 Subject: [PATCH 41/42] prevent removing created sessions for storage --- lib/media/drm_engine.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 68b923ff4a..9c47285cd7 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -51,6 +51,9 @@ shaka.media.DrmEngine = class { /** @private {boolean} */ this.initialized_ = false; + /** @private {boolean} */ + this.initializedForStorage_ = false; + /** @private {number} */ this.licenseTimeSeconds_ = 0; @@ -201,6 +204,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. @@ -1733,7 +1737,8 @@ shaka.media.DrmEngine = class { * We should remove the session to prevent it from being orphaned after * the playback session ends */ - if (!this.offlineSessionIds_.includes(session.sessionId) && + if (!this.initializedForStorage_ && + !this.offlineSessionIds_.includes(session.sessionId) && metadata.type === 'persistent-license') { shaka.log.v1('Removing session', session.sessionId); From 64efd532735e82491aa56395610e1327815773f2 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Fri, 19 Mar 2021 16:01:42 +0100 Subject: [PATCH 42/42] correctly set configKeySystem when initiating DRM engine for removal --- lib/media/drm_engine.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 9c47285cd7..e6bf2597c5 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -275,7 +275,11 @@ shaka.media.DrmEngine = class { keyIds: null, }]; - configsByKeySystem.set(keySystem, config); + configsByKeySystem.set( + shaka.media.DrmEngine.getConfigKeySystem_(keySystem), + config, + ); + return this.queryMediaKeys_(configsByKeySystem); }