Files
shaka-player/lib/polyfill/patchedmediakeys_apple.js
T
Joey Parrish f539147d48 fix: Correct license headers in compiled output
This fixes all the license headers in the main library, which corrects
the appearance of the main license in the compiled output.

It seems that the `!` in the header forces the compiler to keep it in
the output.  I believe older compiler releases did this purely based
on `@license`.

Issue #2638

Change-Id: I7f0e918caad10c9af689c9d07672b7fe9be7b2f3
2020-06-09 16:05:09 -07:00

737 lines
22 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.polyfill.PatchedMediaKeysApple');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.polyfill');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.MediaReadyState');
goog.require('shaka.util.PublicPromise');
/**
* @summary A polyfill to implement modern, standardized EME on top of Apple's
* prefixed EME in Safari.
*/
shaka.polyfill.PatchedMediaKeysApple = class {
/**
* Installs the polyfill if needed.
*/
static install() {
if (!window.HTMLVideoElement || !window.WebKitMediaKeys) {
// No HTML5 video or no prefixed EME.
return;
}
// TODO: Prefer unprefixed EME once we know how to use it.
// See: https://bugs.webkit.org/show_bug.cgi?id=197433
/*
if (navigator.requestMediaKeySystemAccess &&
MediaKeySystemAccess.prototype.getConfiguration) {
// Prefixed EME is preferable.
return;
}
*/
shaka.log.info('Using Apple-prefixed EME');
// Alias
const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
// Delete mediaKeys to work around strict mode compatibility issues.
// eslint-disable-next-line no-restricted-syntax
delete HTMLMediaElement.prototype['mediaKeys'];
// Work around read-only declaration for mediaKeys by using a string.
// eslint-disable-next-line no-restricted-syntax
HTMLMediaElement.prototype['mediaKeys'] = null;
// eslint-disable-next-line no-restricted-syntax
HTMLMediaElement.prototype.setMediaKeys =
PatchedMediaKeysApple.setMediaKeys;
// Install patches
window.MediaKeys = PatchedMediaKeysApple.MediaKeys;
window.MediaKeySystemAccess = PatchedMediaKeysApple.MediaKeySystemAccess;
navigator.requestMediaKeySystemAccess =
PatchedMediaKeysApple.requestMediaKeySystemAccess;
}
/**
* An implementation of navigator.requestMediaKeySystemAccess.
* Retrieves a MediaKeySystemAccess object.
*
* @this {!Navigator}
* @param {string} keySystem
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
* @return {!Promise.<!MediaKeySystemAccess>}
*/
static requestMediaKeySystemAccess(keySystem, supportedConfigurations) {
shaka.log.debug('PatchedMediaKeysApple.requestMediaKeySystemAccess');
goog.asserts.assert(this == navigator,
'bad "this" for requestMediaKeySystemAccess');
// Alias.
const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
try {
const access = new PatchedMediaKeysApple.MediaKeySystemAccess(
keySystem, supportedConfigurations);
return Promise.resolve(/** @type {!MediaKeySystemAccess} */ (access));
} catch (exception) {
return Promise.reject(exception);
}
}
/**
* An implementation of HTMLMediaElement.prototype.setMediaKeys.
* Attaches a MediaKeys object to the media element.
*
* @this {!HTMLMediaElement}
* @param {MediaKeys} mediaKeys
* @return {!Promise}
*/
static setMediaKeys(mediaKeys) {
shaka.log.debug('PatchedMediaKeysApple.setMediaKeys');
goog.asserts.assert(this instanceof HTMLMediaElement,
'bad "this" for setMediaKeys');
// Alias
const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
const newMediaKeys =
/** @type {shaka.polyfill.PatchedMediaKeysApple.MediaKeys} */ (
mediaKeys);
const oldMediaKeys =
/** @type {shaka.polyfill.PatchedMediaKeysApple.MediaKeys} */ (
this.mediaKeys);
if (oldMediaKeys && oldMediaKeys != newMediaKeys) {
goog.asserts.assert(
oldMediaKeys instanceof PatchedMediaKeysApple.MediaKeys,
'non-polyfill instance of oldMediaKeys');
// Have the old MediaKeys stop listening to events on the video tag.
oldMediaKeys.setMedia(null);
}
delete this['mediaKeys']; // in case there is an existing getter
this['mediaKeys'] = mediaKeys; // work around read-only declaration
if (newMediaKeys) {
goog.asserts.assert(
newMediaKeys instanceof PatchedMediaKeysApple.MediaKeys,
'non-polyfill instance of newMediaKeys');
return newMediaKeys.setMedia(this);
}
return Promise.resolve();
}
/**
* Handler for the native media elements webkitneedkey event.
*
* @this {!HTMLMediaElement}
* @param {!MediaKeyEvent} event
* @suppress {constantProperty} We reassign what would be const on a real
* MediaEncryptedEvent, but in our look-alike event.
* @private
*/
static onWebkitNeedKey_(event) {
shaka.log.debug('PatchedMediaKeysApple.onWebkitNeedKey_', event);
const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
const mediaKeys =
/** @type {shaka.polyfill.PatchedMediaKeysApple.MediaKeys} */(
this.mediaKeys);
goog.asserts.assert(mediaKeys instanceof PatchedMediaKeysApple.MediaKeys,
'non-polyfill instance of newMediaKeys');
goog.asserts.assert(event.initData != null, 'missing init data!');
// Convert the prefixed init data to match the native 'encrypted' event.
const uint8 = shaka.util.BufferUtils.toUint8(event.initData);
const dataview = shaka.util.BufferUtils.toDataView(uint8);
// The first part is a 4 byte little-endian int, which is the length of
// the second part.
const length = dataview.getUint32(
/* position= */ 0, /* littleEndian= */ true);
if (length + 4 != uint8.byteLength) {
throw new RangeError('Malformed FairPlay init data');
}
// The remainder is a UTF-16 skd URL. Convert this to UTF-8 and pass on.
const str = shaka.util.StringUtils.fromUTF16(
uint8.subarray(4), /* littleEndian= */ true);
const initData = shaka.util.StringUtils.toUTF8(str);
// NOTE: Because "this" is a real EventTarget, the event we dispatch here
// must also be a real Event.
const event2 = new Event('encrypted');
const encryptedEvent =
/** @type {!MediaEncryptedEvent} */(/** @type {?} */(event2));
encryptedEvent.initDataType = 'skd';
encryptedEvent.initData = shaka.util.BufferUtils.toArrayBuffer(initData);
this.dispatchEvent(event2);
}
};
/**
* An implementation of MediaKeySystemAccess.
*
* @implements {MediaKeySystemAccess}
*/
shaka.polyfill.PatchedMediaKeysApple.MediaKeySystemAccess = class {
/**
* @param {string} keySystem
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
*/
constructor(keySystem, supportedConfigurations) {
shaka.log.debug('PatchedMediaKeysApple.MediaKeySystemAccess');
/** @type {string} */
this.keySystem = keySystem;
/** @private {!MediaKeySystemConfiguration} */
this.configuration_;
// Optimization: WebKitMediaKeys.isTypeSupported delays responses by a
// significant amount of time, possibly to discourage fingerprinting.
// Since we know only FairPlay is supported here, let's skip queries for
// anything else to speed up the process.
if (keySystem.startsWith('com.apple.fps')) {
for (const cfg of supportedConfigurations) {
const newCfg = this.checkConfig_(cfg);
if (newCfg) {
this.configuration_ = newCfg;
return;
}
}
}
// According to the spec, this should be a DOMException, but there is not a
// public constructor for that. So we make this look-alike instead.
const unsupportedKeySystemError = new Error('Unsupported keySystem');
unsupportedKeySystemError.name = 'NotSupportedError';
unsupportedKeySystemError['code'] = DOMException.NOT_SUPPORTED_ERR;
throw unsupportedKeySystemError;
}
/**
* Check a single config for MediaKeySystemAccess.
*
* @param {MediaKeySystemConfiguration} cfg The requested config.
* @return {?MediaKeySystemConfiguration} A matching config we can support, or
* null if the input is not supportable.
* @private
*/
checkConfig_(cfg) {
if (cfg.persistentState == 'required') {
// Not supported by the prefixed API.
return null;
}
// Create a new config object and start adding in the pieces which we find
// support for. We will return this from getConfiguration() later if
// asked.
/** @type {!MediaKeySystemConfiguration} */
const newCfg = {
'audioCapabilities': [],
'videoCapabilities': [],
// It is technically against spec to return these as optional, but we
// don't truly know their values from the prefixed API:
'persistentState': 'optional',
'distinctiveIdentifier': 'optional',
// Pretend the requested init data types are supported, since we don't
// really know that either:
'initDataTypes': cfg.initDataTypes,
'sessionTypes': ['temporary'],
'label': cfg.label,
};
// PatchedMediaKeysApple tests for key system availability through
// WebKitMediaKeys.isTypeSupported.
let ranAnyTests = false;
let success = false;
if (cfg.audioCapabilities) {
for (const cap of cfg.audioCapabilities) {
if (cap.contentType) {
ranAnyTests = true;
const contentType = cap.contentType.split(';')[0];
if (WebKitMediaKeys.isTypeSupported(this.keySystem, contentType)) {
newCfg.audioCapabilities.push(cap);
success = true;
}
}
}
}
if (cfg.videoCapabilities) {
for (const cap of cfg.videoCapabilities) {
if (cap.contentType) {
ranAnyTests = true;
const contentType = cap.contentType.split(';')[0];
if (WebKitMediaKeys.isTypeSupported(this.keySystem, contentType)) {
newCfg.videoCapabilities.push(cap);
success = true;
}
}
}
}
if (!ranAnyTests) {
// If no specific types were requested, we check all common types to
// find out if the key system is present at all.
success = WebKitMediaKeys.isTypeSupported(this.keySystem, 'video/mp4');
}
if (success) {
return newCfg;
}
return null;
}
/** @override */
createMediaKeys() {
shaka.log.debug(
'PatchedMediaKeysApple.MediaKeySystemAccess.createMediaKeys');
// Alias
const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
const mediaKeys = new PatchedMediaKeysApple.MediaKeys(this.keySystem);
return Promise.resolve(/** @type {!MediaKeys} */ (mediaKeys));
}
/** @override */
getConfiguration() {
shaka.log.debug(
'PatchedMediaKeysApple.MediaKeySystemAccess.getConfiguration');
return this.configuration_;
}
};
/**
* An implementation of MediaKeys.
*
* @implements {MediaKeys}
*/
shaka.polyfill.PatchedMediaKeysApple.MediaKeys = class {
/** @param {string} keySystem */
constructor(keySystem) {
shaka.log.debug('PatchedMediaKeysApple.MediaKeys');
/** @private {!WebKitMediaKeys} */
this.nativeMediaKeys_ = new WebKitMediaKeys(keySystem);
/** @private {!shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
}
/** @override */
createSession(sessionType) {
shaka.log.debug('PatchedMediaKeysApple.MediaKeys.createSession');
sessionType = sessionType || 'temporary';
// For now, only the 'temporary' type is supported.
if (sessionType != 'temporary') {
throw new TypeError('Session type ' + sessionType +
' is unsupported on this platform.');
}
// Alias
const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
return new PatchedMediaKeysApple.MediaKeySession(
this.nativeMediaKeys_, sessionType);
}
/** @override */
setServerCertificate(serverCertificate) {
shaka.log.debug('PatchedMediaKeysApple.MediaKeys.setServerCertificate');
return Promise.resolve(false);
}
/**
* @param {HTMLMediaElement} media
* @protected
* @return {!Promise}
*/
setMedia(media) {
// Alias
const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
// Remove any old listeners.
this.eventManager_.removeAll();
// It is valid for media to be null; null is used to flag that event
// handlers need to be cleaned up.
if (!media) {
return Promise.resolve();
}
// Intercept and translate these prefixed EME events.
this.eventManager_.listen(media, 'webkitneedkey',
/** @type {shaka.util.EventManager.ListenerType} */
(PatchedMediaKeysApple.onWebkitNeedKey_));
// Wrap native HTMLMediaElement.webkitSetMediaKeys with a Promise.
try {
// Some browsers require that readyState >=1 before mediaKeys can be
// set, so check this and wait for loadedmetadata if we are not in the
// correct state
shaka.util.MediaReadyState.waitForReadyState(media,
HTMLMediaElement.HAVE_METADATA,
this.eventManager_, () => {
media.webkitSetMediaKeys(this.nativeMediaKeys_);
});
return Promise.resolve();
} catch (exception) {
return Promise.reject(exception);
}
}
};
/**
* An implementation of MediaKeySession.
*
* @implements {MediaKeySession}
*/
shaka.polyfill.PatchedMediaKeysApple.MediaKeySession =
class extends shaka.util.FakeEventTarget {
/**
* @param {WebKitMediaKeys} nativeMediaKeys
* @param {string} sessionType
*/
constructor(nativeMediaKeys, sessionType) {
shaka.log.debug('PatchedMediaKeysApple.MediaKeySession');
super();
/**
* The native MediaKeySession, which will be created in generateRequest.
* @private {WebKitMediaKeySession}
*/
this.nativeMediaKeySession_ = null;
/** @private {WebKitMediaKeys} */
this.nativeMediaKeys_ = nativeMediaKeys;
// Promises that are resolved later
/** @private {shaka.util.PublicPromise} */
this.generateRequestPromise_ = null;
/** @private {shaka.util.PublicPromise} */
this.updatePromise_ = null;
/** @private {!shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @type {string} */
this.sessionId = '';
/** @type {number} */
this.expiration = NaN;
/** @type {!shaka.util.PublicPromise} */
this.closed = new shaka.util.PublicPromise();
/** @type {!shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap} */
this.keyStatuses =
new shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap();
}
/** @override */
generateRequest(initDataType, initData) {
shaka.log.debug(
'PatchedMediaKeysApple.MediaKeySession.generateRequest');
this.generateRequestPromise_ = new shaka.util.PublicPromise();
try {
// This EME spec version requires a MIME content type as the 1st param to
// createSession, but doesn't seem to matter what the value is.
// It also only accepts Uint8Array, not ArrayBuffer, so explicitly make
// initData into a Uint8Array.
const session = this.nativeMediaKeys_.createSession(
'video/mp4', shaka.util.BufferUtils.toUint8(initData));
this.nativeMediaKeySession_ = session;
this.sessionId = session.sessionId || '';
// Attach session event handlers here.
this.eventManager_.listen(
this.nativeMediaKeySession_, 'webkitkeymessage',
/** @type {shaka.util.EventManager.ListenerType} */
((event) => this.onWebkitKeyMessage_(event)));
this.eventManager_.listen(session, 'webkitkeyadded',
/** @type {shaka.util.EventManager.ListenerType} */
((event) => this.onWebkitKeyAdded_(event)));
this.eventManager_.listen(session, 'webkitkeyerror',
/** @type {shaka.util.EventManager.ListenerType} */
((event) => this.onWebkitKeyError_(event)));
this.updateKeyStatus_('status-pending');
} catch (exception) {
this.generateRequestPromise_.reject(exception);
}
return this.generateRequestPromise_;
}
/** @override */
load() {
shaka.log.debug('PatchedMediaKeysApple.MediaKeySession.load');
return Promise.reject(new Error('MediaKeySession.load not yet supported'));
}
/** @override */
update(response) {
shaka.log.debug('PatchedMediaKeysApple.MediaKeySession.update');
this.updatePromise_ = new shaka.util.PublicPromise();
try {
// Pass through to the native session.
this.nativeMediaKeySession_.update(
shaka.util.BufferUtils.toUint8(response));
} catch (exception) {
this.updatePromise_.reject(exception);
}
return this.updatePromise_;
}
/** @override */
close() {
shaka.log.debug('PatchedMediaKeysApple.MediaKeySession.close');
try {
// Pass through to the native session.
this.nativeMediaKeySession_.close();
this.closed.resolve();
this.eventManager_.removeAll();
} catch (exception) {
this.closed.reject(exception);
}
return this.closed;
}
/** @override */
remove() {
shaka.log.debug('PatchedMediaKeysApple.MediaKeySession.remove');
return Promise.reject(new Error(
'MediaKeySession.remove is only applicable for persistent licenses, ' +
'which are not supported on this platform'));
}
/**
* Handler for the native keymessage event on WebKitMediaKeySession.
*
* @param {!MediaKeyEvent} event
* @private
*/
onWebkitKeyMessage_(event) {
shaka.log.debug('PatchedMediaKeysApple.onWebkitKeyMessage_', event);
// We can now resolve this.generateRequestPromise, which should be non-null.
goog.asserts.assert(this.generateRequestPromise_,
'generateRequestPromise_ should be set before now!');
if (this.generateRequestPromise_) {
this.generateRequestPromise_.resolve();
this.generateRequestPromise_ = null;
}
const isNew = this.keyStatuses.getStatus() == undefined;
const event2 = new shaka.util.FakeEvent('message', {
messageType: isNew ? 'license-request' : 'license-renewal',
message: shaka.util.BufferUtils.toArrayBuffer(event.message),
});
this.dispatchEvent(event2);
}
/**
* Handler for the native keyadded event on WebKitMediaKeySession.
*
* @param {!MediaKeyEvent} event
* @private
*/
onWebkitKeyAdded_(event) {
shaka.log.debug('PatchedMediaKeysApple.onWebkitKeyAdded_', event);
// This shouldn't fire while we're in the middle of generateRequest,
// but if it does, we will need to change the logic to account for it.
goog.asserts.assert(!this.generateRequestPromise_,
'Key added during generate!');
// We can now resolve this.updatePromise, which should be non-null.
goog.asserts.assert(this.updatePromise_,
'updatePromise_ should be set before now!');
if (this.updatePromise_) {
this.updateKeyStatus_('usable');
this.updatePromise_.resolve();
this.updatePromise_ = null;
}
}
/**
* Handler for the native keyerror event on WebKitMediaKeySession.
*
* @param {!MediaKeyEvent} event
* @private
*/
onWebkitKeyError_(event) {
shaka.log.debug('PatchedMediaKeysApple.onWebkitKeyError_', event);
const error = new Error('EME PatchedMediaKeysApple key error');
error['errorCode'] = this.nativeMediaKeySession_.error;
if (this.generateRequestPromise_ != null) {
this.generateRequestPromise_.reject(error);
this.generateRequestPromise_ = null;
} else if (this.updatePromise_ != null) {
this.updatePromise_.reject(error);
this.updatePromise_ = null;
} else {
// Unexpected error - map native codes to standardised key statuses.
// Possible values of this.nativeMediaKeySession_.error.code:
// MEDIA_KEYERR_UNKNOWN = 1
// MEDIA_KEYERR_CLIENT = 2
// MEDIA_KEYERR_SERVICE = 3
// MEDIA_KEYERR_OUTPUT = 4
// MEDIA_KEYERR_HARDWARECHANGE = 5
// MEDIA_KEYERR_DOMAIN = 6
switch (this.nativeMediaKeySession_.error.code) {
case WebKitMediaKeyError.MEDIA_KEYERR_OUTPUT:
case WebKitMediaKeyError.MEDIA_KEYERR_HARDWARECHANGE:
this.updateKeyStatus_('output-not-allowed');
break;
default:
this.updateKeyStatus_('internal-error');
break;
}
}
}
/**
* Updates key status and dispatch a 'keystatuseschange' event.
*
* @param {string} status
* @private
*/
updateKeyStatus_(status) {
this.keyStatuses.setStatus(status);
const event = new shaka.util.FakeEvent('keystatuseschange');
this.dispatchEvent(event);
}
};
/**
* @summary An implementation of MediaKeyStatusMap.
* This fakes a map with a single key ID.
*
* @todo Consolidate the MediaKeyStatusMap types in these polyfills.
* @implements {MediaKeyStatusMap}
*/
shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap = class {
constructor() {
/**
* @type {number}
*/
this.size = 0;
/**
* @private {string|undefined}
*/
this.status_ = undefined;
}
/**
* An internal method used by the session to set key status.
* @param {string|undefined} status
*/
setStatus(status) {
this.size = status == undefined ? 0 : 1;
this.status_ = status;
}
/**
* An internal method used by the session to get key status.
* @return {string|undefined}
*/
getStatus() {
return this.status_;
}
/** @override */
forEach(fn) {
if (this.status_) {
fn(this.status_, shaka.media.DrmEngine.DUMMY_KEY_ID.value());
}
}
/** @override */
get(keyId) {
if (this.has(keyId)) {
return this.status_;
}
return undefined;
}
/** @override */
has(keyId) {
const fakeKeyId = shaka.media.DrmEngine.DUMMY_KEY_ID.value();
if (this.status_ && shaka.util.BufferUtils.equal(keyId, fakeKeyId)) {
return true;
}
return false;
}
/**
* @suppress {missingReturn}
* @override
*/
entries() {
goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
}
/**
* @suppress {missingReturn}
* @override
*/
keys() {
goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
}
/**
* @suppress {missingReturn}
* @override
*/
values() {
goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
}
};
shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysApple.install);