From 76e3edcf4d4ee3971ed052fc636e2b782a8e8e2c Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Mon, 29 Apr 2024 15:01:00 -0700 Subject: [PATCH] feat: Add support for probing encryption scheme support This will add encryption schemes to the DRM support report generated by probeSupport() and support.html. Related to shaka-project/eme-encryption-scheme-polyfill#62, PR #6484, and issue #1419. --- externs/shaka/player.js | 7 +- lib/media/drm_engine.js | 148 ++++++++++++++++++++++++++++++---------- 2 files changed, 119 insertions(+), 36 deletions(-) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 420b2015a9..df8aa41465 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -436,11 +436,16 @@ shaka.extern.Restrictions; /** * @typedef {{ - * persistentState: boolean + * persistentState: boolean, + * encryptionSchemes: !Array * }} * * @property {boolean} persistentState * Whether this key system supports persistent state. + * @property {!Array} encryptionSchemes + * An array of encryption schemes that are reported to work, through either + * EME or MCap APIs. An empty array indicates that encryptionScheme queries + * are not supported. This should not happen if our polyfills are installed. * @exportDoc */ shaka.extern.DrmSupportType; diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index db9abf6e00..e18eda98b3 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -21,6 +21,7 @@ goog.require('shaka.util.Lazy'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MapUtils'); goog.require('shaka.util.MimeUtils'); +goog.require('shaka.util.ObjectUtils'); goog.require('shaka.util.Platform'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.PublicPromise'); @@ -1810,23 +1811,72 @@ shaka.media.DrmEngine = class { {contentType: 'video/webm; codecs="vp8"'}, ]; - const basicConfig = { - initDataTypes: ['cenc'], + const basicConfigTemplate = { videoCapabilities: basicVideoCapabilities, - }; - const offlineConfig = { - videoCapabilities: basicVideoCapabilities, - persistentState: 'required', - sessionTypes: ['persistent-license'], + initDataTypes: ['cenc', 'sinf', 'skd'], }; - // Try the offline config first, then fall back to the basic config. - const configs = [offlineConfig, basicConfig]; + const testEncryptionSchemes = [ + null, + 'cenc', + 'cbcs', + 'cbcs-1-9', + ]; /** @type {!Map.} */ const support = new Map(); - const testSystem = async (keySystem) => { + /** + * @param {string} keySystem + * @param {MediaKeySystemAccess} access + * @return {!Promise} + */ + const processMediaKeySystemAccess = async (keySystem, access) => { + try { + await access.createMediaKeys(); + } catch (error) { + // In some cases, we can get a successful access object but fail to + // create a MediaKeys instance. When this happens, don't update the + // support structure. If a previous test succeeded, we won't overwrite + // any of the results. + return; + } + + // If sessionTypes is missing, assume no support for persistent-license. + const sessionTypes = access.getConfiguration().sessionTypes; + let persistentState = sessionTypes ? + sessionTypes.includes('persistent-license') : false; + + // Tizen 3.0 doesn't support persistent licenses, but reports that it + // does. It doesn't fail until you call update() with a license + // response, which is way too late. + // This is a work-around for #894. + if (shaka.util.Platform.isTizen3()) { + persistentState = false; + } + + const videoCapabilities = access.getConfiguration().videoCapabilities; + + let supportValue = {persistentState, encryptionSchemes: []}; + if (support.has(keySystem) && support.get(keySystem)) { + // Update the existing non-null value. + supportValue = support.get(keySystem); + } else { + // Set a new one. + support.set(keySystem, supportValue); + } + + // If the returned config doesn't mention encryptionScheme, the field + // is not supported. If installed, our polyfills should make sure this + // doesn't happen. + const returnedScheme = videoCapabilities[0].encryptionScheme; + if (returnedScheme && + !supportValue.encryptionSchemes.includes(returnedScheme)) { + supportValue.encryptionSchemes.push(returnedScheme); + } + }; + + const testSystemEme = async (keySystem, encryptionScheme) => { try { // Our Polyfill will reject anything apart com.apple.fps key systems. // It seems the Safari modern EME API will allow to request a @@ -1838,37 +1888,65 @@ shaka.media.DrmEngine = class { throw new Error('Unsupported keySystem'); } + const basicConfig = + shaka.util.ObjectUtils.cloneObject(basicConfigTemplate); + for (const cap of basicConfig.videoCapabilities) { + cap.encryptionScheme = encryptionScheme; + } + + const offlineConfig = shaka.util.ObjectUtils.cloneObject(basicConfig); + offlineConfig.persistentState = 'required'; + offlineConfig.sessionTypes = ['persistent-license']; + + const configs = [offlineConfig, basicConfig]; + const access = await navigator.requestMediaKeySystemAccess( keySystem, configs); + await processMediaKeySystemAccess(keySystem, access); + } catch (error) {} // Ignore errors. + }; - // Edge doesn't return supported session types, but current versions - // do not support persistent-license. If sessionTypes is missing, - // assume no support for persistent-license. - // TODO: Polyfill Edge to return known supported session types. - // Edge bug: https://bit.ly/2IeKzho - const sessionTypes = access.getConfiguration().sessionTypes; - let persistentState = sessionTypes ? - sessionTypes.includes('persistent-license') : false; - - // Tizen 3.0 doesn't support persistent licenses, but reports that it - // does. It doesn't fail until you call update() with a license - // response, which is way too late. - // This is a work-around for #894. - if (shaka.util.Platform.isTizen3()) { - persistentState = false; - } + const testSystemMcap = async (keySystem, encryptionScheme) => { + try { + const decodingConfig = { + type: 'media-source', + video: { + contentType: 'video/mp4; codecs="avc1.42E01E"', + width: 640, + height: 480, + bitrate: 1, + framerate: 1, + }, + keySystemConfiguration: { + keySystem, + video: { + encryptionScheme, + }, + }, + }; + + const decodingInfo = + await navigator.mediaCapabilities.decodingInfo(decodingConfig); + + const access = decodingInfo.keySystemAccess; + await processMediaKeySystemAccess(keySystem, access); + } catch (error) {} // Ignore errors. + }; - support.set(keySystem, {persistentState: persistentState}); - await access.createMediaKeys(); - } catch (e) { - // Either the request failed or createMediaKeys failed. - // Either way, write null to the support object. - support.set(keySystem, null); + // Initialize the support structure for each key system. + for (const keySystem of testKeySystems) { + support.set(keySystem, null); + } + + // Test each key system and encryption scheme. + const tests = []; + for (const encryptionScheme of testEncryptionSchemes) { + for (const keySystem of testKeySystems) { + tests.push(testSystemEme(keySystem, encryptionScheme)); + tests.push(testSystemMcap(keySystem, encryptionScheme)); } - }; + } - // Test each key system. - const tests = testKeySystems.map((keySystem) => testSystem(keySystem)); await Promise.all(tests); return shaka.util.MapUtils.asObject(support); }