diff --git a/README.md b/README.md index 1ce3187dfa..7b5a1f76f5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ For details on what's coming next, see our [development roadmap](roadmap.md). ## Platform and browser support matrix -|Browser |Windows |Mac |Linux |Android |iOS >= 12 |ChromeOS|Other| +|Browser |Windows |Mac |Linux |Android |iOS >= 13 |ChromeOS|Other| |:---------:|:--------:|:-------:|:-------:|:-------:|:--------:|:------:|:---:| |Chrome¹ |**Y** |**Y** |**Y** |**Y** |**Native**|**Y** | - | |Firefox¹ |**Y** |**Y** |**Y** |untested⁵|**Native**| - | - | @@ -41,7 +41,7 @@ For details on what's coming next, see our [development roadmap](roadmap.md). |Xbox One | - | - | - | - | - | - |**Y**| NOTES: - - ¹: On macOS, only Safari 12+ is supported. On iOS, only iOS 12+ is + - ¹: On macOS, only Safari 13+ is supported. On iOS, only iOS 13+ is supported. Older versions will be rejected. - ²: The latest stable Chromecast firmware is tested. Both sender and receiver can be implemented with Shaka Player. @@ -54,7 +54,7 @@ NOTES: - Official support for LG WebOS TV: https://github.com/shaka-project/shaka-player/issues/1330 -We support iOS 12+ through Apple's native HLS player. We provide the same +We support iOS 13+ through Apple's native HLS player. We provide the same top-level API, but we just set the video's `src` element to the manifest/media. So we are dependent on the browser supporting the manifests. @@ -124,7 +124,7 @@ HLS features supported: - WebVTT and TTML - CEA-608/708 captions - Encrypted content with PlayReady and Widevine - - Encrypted content with FairPlay (Safari on macOS and iOS 12+ only) + - Encrypted content with FairPlay (Safari on macOS and iOS 13+ only) - Raw AAC, MP3, etc (without an MP4 container) HLS features **not** supported: diff --git a/build/types/polyfill b/build/types/polyfill index f25e996205..367e335fb9 100644 --- a/build/types/polyfill +++ b/build/types/polyfill @@ -7,7 +7,6 @@ +../../lib/polyfill/mediasource.js +../../lib/polyfill/media_capabilities.js +../../lib/polyfill/orientation.js -+../../lib/polyfill/patchedmediakeys_apple.js +../../lib/polyfill/patchedmediakeys_ms.js +../../lib/polyfill/patchedmediakeys_nop.js +../../lib/polyfill/patchedmediakeys_webkit.js diff --git a/docs/tutorials/fairplay.md b/docs/tutorials/fairplay.md index d223f05fc8..7c349178df 100644 --- a/docs/tutorials/fairplay.md +++ b/docs/tutorials/fairplay.md @@ -22,27 +22,6 @@ player.configure('drm.advanced.com\\.apple\\.fps\\.serverCertificateUri', 'https://example.com/cert.der'); ``` -## Content ID - -Note: This only applies when legacy Apple Media Keys is used. - -Some FairPlay content use custom signaling for the content ID. The content ID -is used by the browser to generate the license request. If you don't use the -default content ID derivation, you need to specify a custom init data transform: - -```js -player.configure('drm.initDataTransform', (initData, initDataType) => { - if (initDataType != 'skd') - return initData; - - // 'initData' is a buffer containing an 'skd://' URL as a UTF-8 string. - const skdUri = shaka.util.StringUtils.fromBytesAutoDetect(initData); - const contentId = getMyContentId(skdUri); - const cert = player.drmInfo().serverCertificate; - return shaka.util.FairPlayUtils.initDataTransform(initData, contentId, cert); -}); -``` - ## License wrapping Some FairPlay servers need to accept the license request in a different format diff --git a/externs/shaka/player.js b/externs/shaka/player.js index da49d258be..c3d6d1ed3a 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -618,9 +618,6 @@ shaka.extern.AdvancedDrmConfiguration; * clearKeys: !Object., * delayLicenseRequestUntilPlayed: boolean, * advanced: Object., - * initDataTransform: - * ((function(!Uint8Array, string, ?shaka.extern.DrmInfo):!Uint8Array)| - * undefined), * logLicenseExchange: boolean, * updateExpirationTime: number, * preferredKeySystems: !Array. @@ -644,14 +641,6 @@ shaka.extern.AdvancedDrmConfiguration; * Optional.
* A dictionary which maps key system IDs to advanced DRM configuration for * those key systems. - * @property - * {((function(!Uint8Array, string, ?shaka.extern.DrmInfo):!Uint8Array)| - * undefined)} - * initDataTransform - * Optional.
- * If given, this function is called with the init data from the - * manifest/media and should return the (possibly transformed) init data to - * pass to the browser. * @property {boolean} logLicenseExchange * Optional.
* If set to true, prints logs containing the license exchange. diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 236fcae9e3..5657e618a2 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -2454,14 +2454,6 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED); } - if (shaka.util.Platform.isMediaKeysPolyfilled()) { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code - .HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED); - } - /* * Even if we're not able to construct initData through the HLS tag, adding * a DRMInfo will allow DRM Engine to request a media key system access diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index e23aa8f7d3..594655928a 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -14,7 +14,6 @@ goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Destroyer'); goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); -goog.require('shaka.util.FairPlayUtils'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Iterables'); @@ -1254,28 +1253,6 @@ shaka.media.DrmEngine = class { }; this.activeSessions_.set(session, metadata); - /** - * initDataTransform is only necessary when using legacy protection - * APIs, so prevent doing any transform when using the EME HTML5 spec - */ - if (shaka.util.Platform.isMediaKeysPolyfilled()) { - try { - initData = this.config_.initDataTransform( - initData, initDataType, this.currentDrmInfo_); - } catch (error) { - let shakaError = error; - if (!(error instanceof shaka.util.Error)) { - shakaError = new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.DRM, - shaka.util.Error.Code.INIT_DATA_TRANSFORM_ERROR, - error); - } - this.onError_(shakaError); - return; - } - } - if (this.config_.logLicenseExchange) { const str = shaka.util.Uint8ArrayUtils.toBase64(initData); shaka.log.info('EME init data: type=', initDataType, 'data=', str); @@ -1310,23 +1287,6 @@ shaka.media.DrmEngine = class { }); } - /** - * @param {!Uint8Array} initData - * @param {string} initDataType - * @param {?shaka.extern.DrmInfo} drmInfo - * @return {!Uint8Array} - */ - static defaultInitDataTransform(initData, initDataType, drmInfo) { - if (initDataType == 'skd') { - const cert = drmInfo.serverCertificate; - const contentId = - shaka.util.FairPlayUtils.defaultGetContentId(initData); - initData = shaka.util.FairPlayUtils.initDataTransform( - initData, contentId, cert); - } - return initData; - } - /** * @param {!MediaKeyMessageEvent} event * @private diff --git a/lib/player.js b/lib/player.js index d9952af152..398191fc61 100644 --- a/lib/player.js +++ b/lib/player.js @@ -847,10 +847,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return false; } - // We do not support iOS 9, 10, or 11, nor those same versions of desktop - // Safari. + // We do not support iOS 9, 10, 11 or 12, nor those same versions of + // desktop Safari. const safariVersion = shaka.util.Platform.safariVersion(); - if (safariVersion && safariVersion < 12) { + if (safariVersion && safariVersion < 13) { return false; } diff --git a/lib/polyfill/patchedmediakeys_apple.js b/lib/polyfill/patchedmediakeys_apple.js deleted file mode 100644 index 09a309293f..0000000000 --- a/lib/polyfill/patchedmediakeys_apple.js +++ /dev/null @@ -1,742 +0,0 @@ -/*! @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'); -goog.require('shaka.util.StringUtils'); -goog.require('shaka.util.Platform'); - - -/** - * @summary A polyfill to implement modern, standardized EME on top of Apple's - * prefixed EME in Safari. - * @export - */ -shaka.polyfill.PatchedMediaKeysApple = class { - /** - * Installs the polyfill if needed. - * @export - */ - static install() { - if (!window.HTMLVideoElement || !window.WebKitMediaKeys) { - // No HTML5 video or no prefixed EME. - return; - } - - const safariVersion = shaka.util.Platform.safariVersion(); - if (navigator.requestMediaKeySystemAccess && - // eslint-disable-next-line no-restricted-syntax - MediaKeySystemAccess.prototype.getConfiguration && - safariVersion && safariVersion >= 14) { - // Unprefixed 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; - - window.shakaMediaKeysPolyfill = true; - } - - /** - * An implementation of navigator.requestMediaKeySystemAccess. - * Retrieves a MediaKeySystemAccess object. - * - * @this {!Navigator} - * @param {string} keySystem - * @param {!Array.} supportedConfigurations - * @return {!Promise.} - */ - 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.} 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 data = new Map() - .set('messageType', isNew ? 'license-request' : 'license-renewal') - .set('message', shaka.util.BufferUtils.toArrayBuffer(event.message)); - const event2 = new shaka.util.FakeEvent('message', data); - - 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); diff --git a/lib/util/error.js b/lib/util/error.js index 4830701d07..8185ced008 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -680,11 +680,7 @@ shaka.util.Error.Code = { */ 'HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED': 4040, - /** - * We do not support playing encrypted content (different than mp2t) with MSE - * and legacy Apple MediaKeys API. - */ - 'HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED': 4041, + // RETIRED: 'HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED': 4041, // RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000, diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index f0444d32ee..f9709f4d77 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -9,7 +9,6 @@ goog.provide('shaka.util.PlayerConfiguration'); goog.require('goog.asserts'); goog.require('shaka.abr.SimpleAbrManager'); goog.require('shaka.log'); -goog.require('shaka.media.DrmEngine'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.ConfigUtils'); goog.require('shaka.util.LanguageUtils'); @@ -75,7 +74,6 @@ shaka.util.PlayerConfiguration = class { clearKeys: {}, // key is arbitrary key system ID, value must be string advanced: {}, // key is arbitrary key system ID, value is a record type delayLicenseRequestUntilPlayed: false, - initDataTransform: shaka.media.DrmEngine.defaultInitDataTransform, logLicenseExchange: false, updateExpirationTime: 1, preferredKeySystems: [],