/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.polyfill.MCapEncryptionScheme');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.device.DeviceFactory');
goog.require('shaka.polyfill');
goog.require('shaka.polyfill.EmeEncryptionSchemePolyfillMediaKeySystemAccess');
goog.require('shaka.polyfill.EncryptionSchemeUtils');
/**
* 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.
*
* @see https://wicg.github.io/encrypted-media-encryption-scheme/
* @see https://github.com/w3c/encrypted-media/pull/457
* @export
*/
shaka.polyfill.MCapEncryptionScheme = class {
/**
* 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() {
const device = shaka.device.DeviceFactory.getDevice();
if (!device.supportsEncryptionSchemePolyfill()) {
return;
}
const logPrefix = 'McEncryptionSchemePolyfill:';
if (shaka.polyfill.MCapEncryptionScheme.originalDecodingInfo_ ||
navigator.mediaCapabilitiesEncryptionSchemePolyfilled) {
shaka.log.debug(logPrefix, 'Already installed.');
return;
}
if (!navigator.mediaCapabilities) {
shaka.log.debug(logPrefix, 'MediaCapabilities not found');
// No MediaCapabilities.
return;
}
// Save the original.
shaka.polyfill.MCapEncryptionScheme.originalDecodingInfo_ =
navigator.mediaCapabilities.decodingInfo;
// Patch in a method which will check for support on the first call.
shaka.log.debug(logPrefix, 'Waiting to detect encryptionScheme support.');
navigator.mediaCapabilities.decodingInfo =
shaka.polyfill.MCapEncryptionScheme.probeDecodingInfo_;
// Mark MediaCapabilities as polyfilled. This keeps us from running into
// conflicts between multiple versions of this (compiled Shaka lib vs
// uncompiled source).
navigator.mediaCapabilitiesEncryptionSchemePolyfilled = true;
}
/**
* 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<!MediaCapabilitiesDecodingInfo>} A Promise to a result
* describing the capabilities of the browser in the request configuration.
* @private
*/
static async probeDecodingInfo_(requestedConfiguration) {
const logPrefix = 'McEncryptionSchemePolyfill:';
goog.asserts.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 =
// eslint-disable-next-line no-restricted-syntax
await shaka.polyfill.MCapEncryptionScheme.originalDecodingInfo_.call(
this, requestedConfiguration);
// If the config is not supported, we don't need to try anything else.
if (!capabilities.supported) {
return capabilities;
}
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;
const hasEncryptionScheme = shaka.polyfill.EncryptionSchemeUtils
.hasEncryptionScheme(mediaKeySystemAccess);
if (hasEncryptionScheme) {
// The browser supports the encryptionScheme field!
// No need for a patch. Revert back to the original implementation.
shaka.log.debug(logPrefix, 'Native encryptionScheme support found.');
navigator.mediaCapabilities.decodingInfo =
shaka.polyfill.MCapEncryptionScheme.originalDecodingInfo_;
// Return the results, which are completely valid.
return capabilities;
}
// If we land here, either the browser does not support the
// encryptionScheme field, or the browser does not support EME-related
// fields in MCap _at all_.
// First, install a patch to check the mediaKeySystemAccess or
// encryptionScheme field in future calls.
shaka.log.debug(logPrefix, 'No native encryptionScheme support found. '+
'Patching encryptionScheme support.');
navigator.mediaCapabilities.decodingInfo =
shaka.polyfill.MCapEncryptionScheme.polyfillDecodingInfo_;
// Second, if _none_ of the EME-related fields of MCap are supported, fill
// them in now before returning the results.
if (!mediaKeySystemAccess) {
capabilities.keySystemAccess =
await shaka.polyfill.MCapEncryptionScheme.getMediaKeySystemAccess_(
requestedConfiguration);
return capabilities;
}
// If we land here, it's only the encryption scheme field that is missing.
// The results we have may not be valid, since they didn't account for
// encryption scheme. Run the query again through our polyfill.
// eslint-disable-next-line no-restricted-syntax
return shaka.polyfill.MCapEncryptionScheme.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<!MediaCapabilitiesDecodingInfo>} A Promise to a result
* describing the capabilities of the browser in the request configuration.
* @private
*/
static async polyfillDecodingInfo_(requestedConfiguration) {
goog.asserts.assert(this == navigator.mediaCapabilities,
'bad "this" for decodingInfo');
let videoScheme = null;
let audioScheme = null;
if (requestedConfiguration.keySystemConfiguration) {
const keySystemConfig = requestedConfiguration.keySystemConfiguration;
const keySystem = keySystemConfig.keySystem;
audioScheme = keySystemConfig.audio &&
keySystemConfig.audio.encryptionScheme;
videoScheme = keySystemConfig.video &&
keySystemConfig.video.encryptionScheme;
const supportedScheme =
shaka.polyfill.EncryptionSchemeUtils.guessSupportedScheme(keySystem);
const notSupportedResult = {
powerEfficient: false,
smooth: false,
supported: false,
keySystemAccess: null,
configuration: requestedConfiguration,
};
if (!shaka.polyfill.EncryptionSchemeUtils.checkSupportedScheme(
audioScheme, supportedScheme)) {
return notSupportedResult;
}
if (!shaka.polyfill.EncryptionSchemeUtils.checkSupportedScheme(
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 =
// eslint-disable-next-line no-restricted-syntax
await shaka.polyfill.MCapEncryptionScheme.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 shaka.polyfill.EmeEncryptionSchemePolyfillMediaKeySystemAccess(
capabilities.keySystemAccess, videoScheme, audioScheme);
} else if (requestedConfiguration.keySystemConfiguration) {
// If the result is supported and the content is encrypted, we should have
// a MediaKeySystemAccess instance as part of the result. If we land
// here, the browser doesn't support the EME-related fields of MCap.
capabilities.keySystemAccess =
await shaka.polyfill.MCapEncryptionScheme.getMediaKeySystemAccess_(
requestedConfiguration);
}
return capabilities;
}
/**
* Call navigator.requestMediaKeySystemAccess to get the MediaKeySystemAccess
* information.
*
* @param {!MediaDecodingConfiguration} requestedConfiguration The requested
* decoding configuration.
* @return {!Promise<!MediaKeySystemAccess>} A Promise to a
* MediaKeySystemAccess instance.
* @private
*/
static async getMediaKeySystemAccess_(requestedConfiguration) {
const mediaKeySystemConfig =
shaka.polyfill.MCapEncryptionScheme.convertToMediaKeySystemConfig_(
requestedConfiguration);
const keySystemAccess =
await navigator.requestMediaKeySystemAccess(
requestedConfiguration.keySystemConfiguration.keySystem,
[mediaKeySystemConfig]);
return keySystemAccess;
}
/**
* Convert the MediaDecodingConfiguration object to a
* MediaKeySystemConfiguration object.
*
* @param {!MediaDecodingConfiguration} decodingConfig The decoding
* configuration.
* @return {!MediaKeySystemConfiguration} The converted MediaKeys
* configuration.
* @private
*/
static convertToMediaKeySystemConfig_(decodingConfig) {
const mediaCapKeySystemConfig = decodingConfig.keySystemConfiguration;
const audioCapabilities = [];
const videoCapabilities = [];
if (mediaCapKeySystemConfig.audio) {
const capability = {
robustness: mediaCapKeySystemConfig.audio.robustness || '',
contentType: decodingConfig.audio.contentType,
encryptionScheme: mediaCapKeySystemConfig.audio.encryptionScheme,
};
audioCapabilities.push(capability);
}
if (mediaCapKeySystemConfig.video) {
const capability = {
robustness: mediaCapKeySystemConfig.video.robustness || '',
contentType: decodingConfig.video.contentType,
encryptionScheme: mediaCapKeySystemConfig.video.encryptionScheme,
};
videoCapabilities.push(capability);
}
const initDataTypes = mediaCapKeySystemConfig.initDataType ?
[mediaCapKeySystemConfig.initDataType] : [];
/** @type {!MediaKeySystemConfiguration} */
const mediaKeySystemConfig = {
initDataTypes: initDataTypes,
distinctiveIdentifier: mediaCapKeySystemConfig.distinctiveIdentifier,
persistentState: mediaCapKeySystemConfig.persistentState,
sessionTypes: mediaCapKeySystemConfig.sessionTypes,
};
// Only add the audio video capabilities if they have valid data.
// Otherwise the query will fail.
if (audioCapabilities.length) {
mediaKeySystemConfig.audioCapabilities = audioCapabilities;
}
if (videoCapabilities.length) {
mediaKeySystemConfig.videoCapabilities = videoCapabilities;
}
return mediaKeySystemConfig;
}
};
/**
* The original decodingInfo, before we patched it.
*
* @type {
* function(this:MediaCapabilities,
* !MediaDecodingConfiguration
* ):!Promise<!MediaCapabilitiesDecodingInfo>
* }
* @private
*/
shaka.polyfill.MCapEncryptionScheme.originalDecodingInfo_;
// Install at a low priority so that other EME polyfills go first.
shaka.polyfill.register(shaka.polyfill.MCapEncryptionScheme.install, -2);