diff --git a/.eslintrc.js b/.eslintrc.js index c3c46b7b..59f859b5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -53,6 +53,9 @@ module.exports = { { // These types are browser-provided, so trust that they exist. 'definedTypes': [ + 'MediaCapabilities', + 'MediaCapabilitiesDecodingInfo', + 'MediaDecodingConfiguration', 'MediaKeySystemConfiguration', 'MediaKeySystemMediaCapability', ], diff --git a/CHANGELOG.md b/CHANGELOG.md index 376fb0c4..19a2a83d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.1.0 (2019-12-12) + +Features: + - Added support for polyfilling MediaCapabilities, too + - https://github.com/w3c/media-capabilities/issues/100 + + ## 1.0.3 (2019-12-05) Bugfixes: diff --git a/closure-compiler-check/mediacapabilities.js b/closure-compiler-check/mediacapabilities.js new file mode 100644 index 00000000..bd313bb8 --- /dev/null +++ b/closure-compiler-check/mediacapabilities.js @@ -0,0 +1,206 @@ +/* + * Copyright 2015 The Closure Compiler authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @fileoverview MediaCapabilities externs. + * Based on {@link https://w3c.github.io/media-capabilities/ MC draft 6 November + * 2019}. + * @externs + */ + +/** + * @typedef {string} + * @see https://w3c.github.io/media-capabilities/#enumdef-hdrmetadatatype + */ +var HdrMetadataType; + +/** + * @typedef {string} + * @see https://w3c.github.io/media-capabilities/#enumdef-colorgamut + */ +var ColorGamut; + +/** + * @typedef {string} + * @see https://w3c.github.io/media-capabilities/#enumdef-transferfunction + */ +var TransferFunction; + +/** + * @typedef {string} + * @see https://w3c.github.io/media-capabilities/#enumdef-mediadecodingtype + */ +var MediaDecodingType; + +/** + * @typedef {string} + * @see https://w3c.github.io/media-capabilities/#enumdef-mediaencodingtype + */ +var MediaEncodingType; + +/** + * @typedef {{ + * contentType: string, + * width: number, + * height: number, + * bitrate: number, + * framerate: number, + * hasAlphaChannel: (boolean|undefined), + * hdrMetadataType: (HdrMetadataType|undefined), + * colorGamut: (ColorGamut|undefined), + * transferFunction: (TransferFunction|undefined) + * }} + * @see https://w3c.github.io/media-capabilities/#dictdef-videoconfiguration + */ +var VideoConfiguration; + +// NOTE: channels definition below is not yet stable in the spec as of Dec 2019. +// "The channels needs to be defined as a double (2.1, 4.1, 5.1, ...), an +// unsigned short (number of channels) or as an enum value. The current +// definition is a placeholder." +/** + * @typedef {{ + * contentType: string, + * channels: (*|undefined), + * bitrate: (number|undefined), + * samplerate: (number|undefined), + * spatialRendering: (boolean|undefined) + * }} + * @see https://w3c.github.io/media-capabilities/#dictdef-audioconfiguration + */ +var AudioConfiguration; + +// NOTE: encryptionScheme is not yet in the MC spec as of Dec 2019, but has +// already landed in EME and should be in MC soon. +// https://github.com/w3c/media-capabilities/issues/100 +/** + * @typedef {{ + * robustness: (string|undefined), + * encryptionScheme: (string|undefined) + * }} + * @see https://w3c.github.io/media-capabilities/#dictdef-keysystemtrackconfiguration + */ +var KeySystemTrackConfiguration; + +/** + * @typedef {{ + * keySystem: string, + * initDataType: (string|undefined), + * distinctiveIdentifier: (string|undefined), + * persistentState: (string|undefined), + * sessionTypes: (!Array|undefined), + * audio: (KeySystemTrackConfiguration|undefined), + * video: (KeySystemTrackConfiguration|undefined) + * }} + * @see https://w3c.github.io/media-capabilities/#dictdef-mediacapabilitieskeysystemconfiguration + */ +var MediaCapabilitiesKeySystemConfiguration; + +/** + * @record + * @see https://w3c.github.io/media-capabilities/#dictdef-mediaconfiguration + */ +function MediaConfiguration() {} + +/** @type {VideoConfiguration|undefined} */ +MediaConfiguration.prototype.video; + +/** @type {AudioConfiguration|undefined} */ +MediaConfiguration.prototype.audio; + +/** + * @record + * @extends {MediaConfiguration} + * @see https://w3c.github.io/media-capabilities/#dictdef-mediadecodingconfiguration + */ +function MediaDecodingConfiguration() {} + +/** @type {MediaDecodingType} */ +MediaDecodingConfiguration.prototype.type; + +/** @type {MediaCapabilitiesKeySystemConfiguration|undefined} */ +MediaDecodingConfiguration.prototype.keySystemConfiguration; + +/** + * @record + * @extends {MediaConfiguration} + * @see https://w3c.github.io/media-capabilities/#dictdef-mediaencodingconfiguration + */ +function MediaEncodingConfiguration() {} + +/** @type {MediaEncodingType} */ +MediaEncodingConfiguration.prototype.type; + +/** + * @record + * @see https://w3c.github.io/media-capabilities/#dictdef-mediacapabilitiesinfo + */ +function MediaCapabilitiesInfo() {} + +/** @type {boolean} */ +MediaCapabilitiesInfo.prototype.supported; + +/** @type {boolean} */ +MediaCapabilitiesInfo.prototype.smooth; + +/** @type {boolean} */ +MediaCapabilitiesInfo.prototype.powerEfficient; + +/** + * @record + * @extends {MediaCapabilitiesInfo} + * @see https://w3c.github.io/media-capabilities/#dictdef-mediacapabilitiesdecodinginfo + */ +function MediaCapabilitiesDecodingInfo() {} + +/** @type {MediaKeySystemAccess} */ +MediaCapabilitiesDecodingInfo.prototype.keySystemAccess; + +/** @type {MediaDecodingConfiguration} */ +MediaCapabilitiesDecodingInfo.prototype.configuration; + +/** + * @record + * @extends {MediaCapabilitiesInfo} + * @see https://w3c.github.io/media-capabilities/#dictdef-mediacapabilitiesencodinginfo + */ +function MediaCapabilitiesEncodingInfo() {} + +/** @type {MediaEncodingConfiguration} */ +MediaCapabilitiesEncodingInfo.prototype.configuration; + +/** + * @interface + * @see https://w3c.github.io/media-capabilities/#mediacapabilities + */ +function MediaCapabilities() {} + +/** + * @param {!MediaDecodingConfiguration} configuration + * @return {!Promise} + */ +MediaCapabilities.prototype.decodingInfo = function(configuration) {}; + +/** + * @param {!MediaEncodingConfiguration} configuration + * @return {!Promise} + */ +MediaCapabilities.prototype.encodingInfo = function(configuration) {}; + +/** @const {MediaCapabilities} */ +Navigator.prototype.mediaCapabilities; + +/** @const {MediaCapabilities} */ +WorkerNavigator.prototype.mediaCapabilities; diff --git a/demo/index.html b/demo/index.html index bf020023..3c985f68 100644 --- a/demo/index.html +++ b/demo/index.html @@ -23,7 +23,10 @@ - + @@ -39,8 +42,8 @@

EME Encryption Scheme Polyfill Demo

Test query

Choose parameters like key system, encryption scheme, and media types, - then click the "run" button to see the results of the query with the - polyfill. + then click one of the "run" button to see the results of the query with + the polyfill.
@@ -85,7 +88,8 @@

Test query

- + +
@@ -93,7 +97,6 @@

Test query

-
diff --git a/demo/query.js b/demo/query.js index 38debd26..e33d6387 100644 --- a/demo/query.js +++ b/demo/query.js @@ -49,5 +49,65 @@ document.addEventListener('DOMContentLoaded', () => { } catch (error) { window.results.textContent = formatObjectToString({error: error.message}); } - }); // emeRunButton click listener + }); // emeRun click listener + + window.mcRun.addEventListener('click', async () => { + // Pull contents of query form. + const keySystem = window.keySystem.input.value; + // If encryptionScheme is blank, default to null. + const encryptionScheme = window.encryptionScheme.input.value || null; + const audio = window.audio.input.value; + const video = window.video.input.value; + + const config = { + type: 'media-source', + }; + + if (keySystem) { + config.keySystemConfiguration = { + keySystem, + }; + } + + if (audio) { + config.audio = { + contentType: audio, + }; + + if (keySystem) { + config.keySystemConfiguration.audio = { + encryptionScheme, + }; + } + } + + if (video) { + config.video = { + contentType: video, + width: 640, + height: 480, + bitrate: 1, + framerate: 24, + }; + + if (keySystem) { + config.keySystemConfiguration.video = { + encryptionScheme, + }; + } + } + + try { + const result = await navigator.mediaCapabilities.decodingInfo(config); + results.textContent = formatObjectToString(result); + + const mksa = result.keySystemAccess; + if (mksa) { + results.textContent += '\n' + + formatObjectToString(mksa.getConfiguration()); + } + } catch (error) { + results.textContent = formatObjectToString({error: error.message}); + } + }); // mcRun click listener }); // DOMContentLoaded listener diff --git a/index.js b/index.js index e6201175..a52d0ac0 100644 --- a/index.js +++ b/index.js @@ -55,38 +55,6 @@ class EmeEncryptionSchemePolyfill { EmeEncryptionSchemePolyfill.probeRMKSA_; } - /** - * Guess the supported encryption scheme for the key system. - * - * @param {string} keySystem The key system ID. - * @return {?string} A guess at the encryption scheme this key system - * supports. - * @private - */ - static guessSupportedScheme_(keySystem) { - if (keySystem.startsWith('com.widevine')) { - return 'cenc'; - } else if (keySystem.startsWith('com.microsoft')) { - return 'cenc'; - } else if (keySystem.startsWith('com.adobe')) { - return 'cenc'; - } else if (keySystem.startsWith('org.w3')) { - return 'cenc'; - } else if (keySystem.startsWith('com.apple')) { - return 'cbcs-1-9'; - } - - // We don't have this key system in our map! - - // Log a warning. The only way the request will succeed now is if the - // app doesn't specify an encryption scheme in their own configs. - // Use bracket notation to keep this from being stripped from the build. - console['warn']('EmeEncryptionSchemePolyfill: Unknown key system:', - keySystem, 'Please contribute!'); - - return null; - } - /** * A shim for navigator.requestMediaKeySystemAccess to check for * encryptionScheme support. Only used until we know if the browser has @@ -110,20 +78,7 @@ class EmeEncryptionSchemePolyfill { await EmeEncryptionSchemePolyfill.originalRMKSA_.call( this, keySystem, supportedConfigurations); - const configuration = mediaKeySystemAccess.getConfiguration(); - - // It doesn't matter which capability we look at. For this check, they - // should all produce the same result. - const firstVideoCapability = - configuration.videoCapabilities && configuration.videoCapabilities[0]; - const firstAudioCapability = - configuration.audioCapabilities && configuration.audioCapabilities[0]; - const firstCapability = firstVideoCapability || firstAudioCapability; - - // If supported by the browser, the encryptionScheme field must appear in - // the returned configuration, regardless of whether or not it was - // specified in the supportedConfigurations given by the application. - if (firstCapability['encryptionScheme'] !== undefined) { + if (hasEncryptionScheme(mediaKeySystemAccess)) { // The browser supports the encryptionScheme field! // No need for a patch. Revert back to the original implementation. console.debug('EmeEncryptionSchemePolyfill: ' + @@ -170,8 +125,7 @@ class EmeEncryptionSchemePolyfill { console.assert(this == navigator, 'bad "this" for requestMediaKeySystemAccess'); - const supportedScheme = EmeEncryptionSchemePolyfill.guessSupportedScheme_( - keySystem); + const supportedScheme = guessSupportedScheme(keySystem); // Filter the application's configurations based on our guess of what // encryption scheme is supported. @@ -256,6 +210,175 @@ class EmeEncryptionSchemePolyfill { } } +/** + * A polyfill to add support for EncryptionScheme queries in MediaCapabilities. + * + * Because this polyfill can't know what schemes the UA or CDM actually support, + * it assumes support for the historically-supported schemes of each well-known + * key system. + * + * In source form, this is compatible with the Closure Compiler, CommonJS, and + * AMD module formats. It can also be directly included via a script tag. + * + * The minified bundle is a standalone module compatible with the CommonJS and + * AMD module formats, and can also be directly included via a script tag. + * + * @see https://wicg.github.io/encrypted-media-encryption-scheme/ + * @see https://github.com/w3c/encrypted-media/pull/457 + * @export + */ +class McEncryptionSchemePolyfill { + /** + * Installs the polyfill. To avoid the possibility of extra user prompts, + * this will shim MC so long as it exists, without checking support for + * encryptionScheme upfront. The support check will happen on-demand the + * first time MC is used. + * + * @export + */ + static install() { + if (!navigator.mediaCapabilities) { + console.debug('McEncryptionSchemePolyfill: MediaCapabilities not found'); + // No MediaCapabilities. + return; + } + + // Save the original. + McEncryptionSchemePolyfill.originalDecodingInfo_ = + navigator.mediaCapabilities.decodingInfo; + + // Patch in a method which will check for support on the first call. + console.debug('McEncryptionSchemePolyfill: ' + + 'Waiting to detect encryptionScheme support.'); + navigator.mediaCapabilities.decodingInfo = + McEncryptionSchemePolyfill.probeDecodingInfo_; + } + + /** + * A shim for mediaCapabilities.decodingInfo to check for encryptionScheme + * support. Only used until we know if the browser has native support for the + * encryptionScheme field. + * + * @this {MediaCapabilities} + * @param {!MediaDecodingConfiguration} requestedConfiguration The requested + * decoding configuration. + * @return {!Promise.} A Promise to a result + * describing the capabilities of the browser in the request configuration. + * @private + */ + static async probeDecodingInfo_(requestedConfiguration) { + console.assert(this == navigator.mediaCapabilities, + 'bad "this" for decodingInfo'); + + // Call the original version. If the call succeeds, we look at the result + // to decide if the encryptionScheme field is supported or not. + const capabilities = + await McEncryptionSchemePolyfill.originalDecodingInfo_.call( + this, requestedConfiguration); + + if (!requestedConfiguration.keySystemConfiguration) { + // This was not a query regarding encrypted content. The results are + // valid, but won't tell us anything about native support for + // encryptionScheme. Just return the results. + return capabilities; + } + + const mediaKeySystemAccess = capabilities.keySystemAccess; + + if (hasEncryptionScheme(mediaKeySystemAccess)) { + // The browser supports the encryptionScheme field! + // No need for a patch. Revert back to the original implementation. + console.debug('McEncryptionSchemePolyfill: ' + + 'Native encryptionScheme support found.'); + // eslint-disable-next-line require-atomic-updates + navigator.mediaCapabilities.decodingInfo = + McEncryptionSchemePolyfill.originalDecodingInfo_; + // Return the results, which are completely valid. + return capabilities; + } + + // If we land here, the browser does _not_ support the encryptionScheme + // field. So we install another patch to check the encryptionScheme field + // in future calls. + console.debug('McEncryptionSchemePolyfill: ' + + 'No native encryptionScheme support found. '+ + 'Patching encryptionScheme support.'); + // eslint-disable-next-line require-atomic-updates + navigator.mediaCapabilities.decodingInfo = + McEncryptionSchemePolyfill.polyfillDecodingInfo_; + + // The results we have may not be valid. Run the query again through our + // polyfill. + return McEncryptionSchemePolyfill.polyfillDecodingInfo_.call( + this, requestedConfiguration); + } + + /** + * A polyfill for mediaCapabilities.decodingInfo to handle the + * encryptionScheme field in browsers that don't support it. It uses the + * user-agent string to guess what encryption schemes are supported, then + * those guesses are used to reject unsupported schemes. + * + * @this {MediaCapabilities} + * @param {!MediaDecodingConfiguration} requestedConfiguration The requested + * decoding configuration. + * @return {!Promise.} A Promise to a result + * describing the capabilities of the browser in the request configuration. + * @private + */ + static async polyfillDecodingInfo_(requestedConfiguration) { + console.assert(this == navigator.mediaCapabilities, + 'bad "this" for decodingInfo'); + + let supportedScheme = null; + + if (requestedConfiguration.keySystemConfiguration) { + const keySystemConfig = requestedConfiguration.keySystemConfiguration; + + const keySystem = keySystemConfig.keySystem; + + const audioScheme = keySystemConfig.audio && + keySystemConfig.audio.encryptionScheme; + const videoScheme = keySystemConfig.video && + keySystemConfig.video.encryptionScheme; + + supportedScheme = guessSupportedScheme(keySystem); + + const notSupportedResult = { + powerEfficient: false, + smooth: false, + supported: false, + keySystemAccess: null, + configuration: requestedConfiguration, + }; + + if (audioScheme && audioScheme != supportedScheme) { + return notSupportedResult; + } + if (videoScheme && videoScheme != supportedScheme) { + return notSupportedResult; + } + } + + // At this point, either it's unencrypted or we assume the encryption scheme + // is supported. So delegate to the original decodingInfo() method. + const capabilities = + await McEncryptionSchemePolyfill.originalDecodingInfo_.call( + this, requestedConfiguration); + + if (capabilities.keySystemAccess) { + // If the result is supported and encrypted, this will be a + // MediaKeySystemAccess instance. Wrap the MKSA object in ours to provide + // the missing field in the returned configuration. + capabilities.keySystemAccess = + new EmeEncryptionSchemePolyfillMediaKeySystemAccess( + capabilities.keySystemAccess, supportedScheme); + } + + return capabilities; + } +} + /** * A wrapper around MediaKeySystemAccess that adds encryptionScheme * fields to the configuration, to emulate what a browser with native support @@ -315,6 +438,62 @@ class EmeEncryptionSchemePolyfillMediaKeySystemAccess { } } +/** + * Guess the supported encryption scheme for the key system. + * + * @param {string} keySystem The key system ID. + * @return {?string} A guess at the encryption scheme this key system + * supports. + */ +function guessSupportedScheme(keySystem) { + if (keySystem.startsWith('com.widevine')) { + return 'cenc'; + } else if (keySystem.startsWith('com.microsoft')) { + return 'cenc'; + } else if (keySystem.startsWith('com.adobe')) { + return 'cenc'; + } else if (keySystem.startsWith('org.w3')) { + return 'cenc'; + } else if (keySystem.startsWith('com.apple')) { + return 'cbcs-1-9'; + } + + // We don't have this key system in our map! + + // Log a warning. The only way the request will succeed now is if the + // app doesn't specify an encryption scheme in their own configs. + // Use bracket notation to keep this from being stripped from the build. + console['warn']('EmeEncryptionSchemePolyfill: Unknown key system:', + keySystem, 'Please contribute!'); + + return null; +} + +/** + * @param {?MediaKeySystemAccess} mediaKeySystemAccess A native + * MediaKeySystemAccess instance from the browser. + * @return {boolean} True if browser natively supports encryptionScheme. + */ +function hasEncryptionScheme(mediaKeySystemAccess) { + const configuration = mediaKeySystemAccess.getConfiguration(); + + // It doesn't matter which capability we look at. For this check, they + // should all produce the same result. + const firstVideoCapability = + configuration.videoCapabilities && configuration.videoCapabilities[0]; + const firstAudioCapability = + configuration.audioCapabilities && configuration.audioCapabilities[0]; + const firstCapability = firstVideoCapability || firstAudioCapability; + + // If supported by the browser, the encryptionScheme field must appear in + // the returned configuration, regardless of whether or not it was + // specified in the supportedConfigurations given by the application. + if (firstCapability['encryptionScheme'] !== undefined) { + return true; + } + return false; +} + /** * The original requestMediaKeySystemAccess, before we patched it. * @@ -328,10 +507,27 @@ class EmeEncryptionSchemePolyfillMediaKeySystemAccess { */ EmeEncryptionSchemePolyfill.originalRMKSA_; +/** + * The original decodingInfo, before we patched it. + * + * @type { + * function(this:MediaCapabilities, + * !MediaDecodingConfiguration + * ):!Promise. + * } + * @private + */ +McEncryptionSchemePolyfill.originalDecodingInfo_; + // Support for CommonJS and AMD module formats. /** @suppress {undefinedVars} */ (() => { if (typeof module !== 'undefined' && module.exports) { - module.exports = EmeEncryptionSchemePolyfill; + module.exports = { + install: () => { + EmeEncryptionSchemePolyfill.install(); + McEncryptionSchemePolyfill.install(); + }, + }; } })(); diff --git a/package.json b/package.json index 517a3114..59a9e02f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "eme-encryption-scheme-polyfill", "description": "A polyfill for the encryptionScheme field in EME", - "version": "1.0.3", + "version": "1.1.0", "license": "Apache-2.0", "author": "Google", "homepage": "https://github.com/google/eme-encryption-scheme-polyfill#readme",