Source: lib/polyfill/patchedmediakeys_apple.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.polyfill.PatchedMediaKeysApple');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.media.DrmEngine');
  10. goog.require('shaka.polyfill');
  11. goog.require('shaka.util.BufferUtils');
  12. goog.require('shaka.util.EventManager');
  13. goog.require('shaka.util.FakeEvent');
  14. goog.require('shaka.util.FakeEventTarget');
  15. goog.require('shaka.util.MediaReadyState');
  16. goog.require('shaka.util.PublicPromise');
  17. goog.require('shaka.util.StringUtils');
  18. /**
  19. * @summary A polyfill to implement modern, standardized EME on top of Apple's
  20. * prefixed EME in Safari.
  21. * @export
  22. */
  23. shaka.polyfill.PatchedMediaKeysApple = class {
  24. /**
  25. * Installs the polyfill if needed.
  26. * @param {boolean=} enableUninstall enables uninstalling the polyfill
  27. * @export
  28. */
  29. static install(enableUninstall = false) {
  30. // Alias
  31. const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
  32. if (!window.HTMLVideoElement || !window.WebKitMediaKeys) {
  33. // No HTML5 video or no prefixed EME.
  34. return;
  35. }
  36. if (enableUninstall) {
  37. PatchedMediaKeysApple.enableUninstall = true;
  38. PatchedMediaKeysApple.originalHTMLMediaElementPrototypeMediaKeys =
  39. /** @type {!Object} */ (
  40. Object.getOwnPropertyDescriptor(
  41. // eslint-disable-next-line no-restricted-syntax
  42. HTMLMediaElement.prototype, 'mediaKeys',
  43. )
  44. );
  45. PatchedMediaKeysApple.originalHTMLMediaElementPrototypeSetMediaKeys =
  46. // eslint-disable-next-line no-restricted-syntax
  47. HTMLMediaElement.prototype.setMediaKeys;
  48. PatchedMediaKeysApple.originalWindowMediaKeys = window.MediaKeys;
  49. PatchedMediaKeysApple.originalWindowMediaKeySystemAccess =
  50. window.MediaKeySystemAccess;
  51. PatchedMediaKeysApple.originalNavigatorRequestMediaKeySystemAccess =
  52. navigator.requestMediaKeySystemAccess;
  53. }
  54. shaka.log.info('Using Apple-prefixed EME');
  55. // Delete mediaKeys to work around strict mode compatibility issues.
  56. // eslint-disable-next-line no-restricted-syntax
  57. delete HTMLMediaElement.prototype['mediaKeys'];
  58. // Work around read-only declaration for mediaKeys by using a string.
  59. // eslint-disable-next-line no-restricted-syntax
  60. HTMLMediaElement.prototype['mediaKeys'] = null;
  61. // eslint-disable-next-line no-restricted-syntax
  62. HTMLMediaElement.prototype.setMediaKeys =
  63. PatchedMediaKeysApple.setMediaKeys;
  64. // Install patches
  65. window.MediaKeys = PatchedMediaKeysApple.MediaKeys;
  66. window.MediaKeySystemAccess = PatchedMediaKeysApple.MediaKeySystemAccess;
  67. navigator.requestMediaKeySystemAccess =
  68. PatchedMediaKeysApple.requestMediaKeySystemAccess;
  69. window.shakaMediaKeysPolyfill = true;
  70. }
  71. /**
  72. * Uninstalls the polyfill if needed and enabled.
  73. * @export
  74. */
  75. static uninstall() {
  76. // Alias
  77. const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
  78. if (!PatchedMediaKeysApple.enableUninstall) {
  79. return;
  80. }
  81. shaka.log.info('Un-installing Apple-prefixed EME');
  82. PatchedMediaKeysApple.enableUninstall = false;
  83. Object.defineProperty(
  84. // eslint-disable-next-line no-restricted-syntax
  85. HTMLMediaElement.prototype,
  86. 'mediaKeys',
  87. PatchedMediaKeysApple.originalHTMLMediaElementPrototypeMediaKeys,
  88. );
  89. // eslint-disable-next-line no-restricted-syntax
  90. HTMLMediaElement.prototype.setMediaKeys =
  91. PatchedMediaKeysApple.originalHTMLMediaElementPrototypeSetMediaKeys;
  92. window.MediaKeys = PatchedMediaKeysApple.originalWindowMediaKeys;
  93. window.MediaKeySystemAccess =
  94. PatchedMediaKeysApple.originalWindowMediaKeySystemAccess;
  95. navigator.requestMediaKeySystemAccess =
  96. PatchedMediaKeysApple.originalNavigatorRequestMediaKeySystemAccess;
  97. PatchedMediaKeysApple.originalWindowMediaKeys = null;
  98. PatchedMediaKeysApple.originalWindowMediaKeySystemAccess = null;
  99. PatchedMediaKeysApple.originalHTMLMediaElementPrototypeSetMediaKeys = null;
  100. PatchedMediaKeysApple.originalNavigatorRequestMediaKeySystemAccess = null;
  101. PatchedMediaKeysApple.originalHTMLMediaElementPrototypeMediaKeys = null;
  102. window.shakaMediaKeysPolyfill = false;
  103. }
  104. /**
  105. * An implementation of navigator.requestMediaKeySystemAccess.
  106. * Retrieves a MediaKeySystemAccess object.
  107. *
  108. * @this {!Navigator}
  109. * @param {string} keySystem
  110. * @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
  111. * @return {!Promise.<!MediaKeySystemAccess>}
  112. */
  113. static requestMediaKeySystemAccess(keySystem, supportedConfigurations) {
  114. shaka.log.debug('PatchedMediaKeysApple.requestMediaKeySystemAccess');
  115. goog.asserts.assert(this == navigator,
  116. 'bad "this" for requestMediaKeySystemAccess');
  117. // Alias.
  118. const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
  119. try {
  120. const access = new PatchedMediaKeysApple.MediaKeySystemAccess(
  121. keySystem, supportedConfigurations);
  122. return Promise.resolve(/** @type {!MediaKeySystemAccess} */ (access));
  123. } catch (exception) {
  124. return Promise.reject(exception);
  125. }
  126. }
  127. /**
  128. * An implementation of HTMLMediaElement.prototype.setMediaKeys.
  129. * Attaches a MediaKeys object to the media element.
  130. *
  131. * @this {!HTMLMediaElement}
  132. * @param {MediaKeys} mediaKeys
  133. * @return {!Promise}
  134. */
  135. static setMediaKeys(mediaKeys) {
  136. shaka.log.debug('PatchedMediaKeysApple.setMediaKeys');
  137. goog.asserts.assert(this instanceof HTMLMediaElement,
  138. 'bad "this" for setMediaKeys');
  139. // Alias
  140. const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
  141. const newMediaKeys =
  142. /** @type {shaka.polyfill.PatchedMediaKeysApple.MediaKeys} */ (
  143. mediaKeys);
  144. const oldMediaKeys =
  145. /** @type {shaka.polyfill.PatchedMediaKeysApple.MediaKeys} */ (
  146. this.mediaKeys);
  147. if (oldMediaKeys && oldMediaKeys != newMediaKeys) {
  148. goog.asserts.assert(
  149. oldMediaKeys instanceof PatchedMediaKeysApple.MediaKeys,
  150. 'non-polyfill instance of oldMediaKeys');
  151. // Have the old MediaKeys stop listening to events on the video tag.
  152. oldMediaKeys.setMedia(null);
  153. }
  154. delete this['mediaKeys']; // in case there is an existing getter
  155. this['mediaKeys'] = mediaKeys; // work around read-only declaration
  156. if (newMediaKeys) {
  157. goog.asserts.assert(
  158. newMediaKeys instanceof PatchedMediaKeysApple.MediaKeys,
  159. 'non-polyfill instance of newMediaKeys');
  160. return newMediaKeys.setMedia(this);
  161. }
  162. return Promise.resolve();
  163. }
  164. /**
  165. * Handler for the native media elements webkitneedkey event.
  166. *
  167. * @this {!HTMLMediaElement}
  168. * @param {!MediaKeyEvent} event
  169. * @suppress {constantProperty} We reassign what would be const on a real
  170. * MediaEncryptedEvent, but in our look-alike event.
  171. * @private
  172. */
  173. static onWebkitNeedKey_(event) {
  174. shaka.log.debug('PatchedMediaKeysApple.onWebkitNeedKey_', event);
  175. const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
  176. const mediaKeys =
  177. /** @type {shaka.polyfill.PatchedMediaKeysApple.MediaKeys} */(
  178. this.mediaKeys);
  179. goog.asserts.assert(mediaKeys instanceof PatchedMediaKeysApple.MediaKeys,
  180. 'non-polyfill instance of newMediaKeys');
  181. goog.asserts.assert(event.initData != null, 'missing init data!');
  182. // Convert the prefixed init data to match the native 'encrypted' event.
  183. const uint8 = shaka.util.BufferUtils.toUint8(event.initData);
  184. const dataview = shaka.util.BufferUtils.toDataView(uint8);
  185. // The first part is a 4 byte little-endian int, which is the length of
  186. // the second part.
  187. const length = dataview.getUint32(
  188. /* position= */ 0, /* littleEndian= */ true);
  189. if (length + 4 != uint8.byteLength) {
  190. throw new RangeError('Malformed FairPlay init data');
  191. }
  192. // The remainder is a UTF-16 skd URL. Convert this to UTF-8 and pass on.
  193. const str = shaka.util.StringUtils.fromUTF16(
  194. uint8.subarray(4), /* littleEndian= */ true);
  195. const initData = shaka.util.StringUtils.toUTF8(str);
  196. // NOTE: Because "this" is a real EventTarget, the event we dispatch here
  197. // must also be a real Event.
  198. const event2 = new Event('encrypted');
  199. const encryptedEvent =
  200. /** @type {!MediaEncryptedEvent} */(/** @type {?} */(event2));
  201. encryptedEvent.initDataType = 'skd';
  202. encryptedEvent.initData = shaka.util.BufferUtils.toArrayBuffer(initData);
  203. this.dispatchEvent(event2);
  204. }
  205. };
  206. /**
  207. * An implementation of MediaKeySystemAccess.
  208. *
  209. * @implements {MediaKeySystemAccess}
  210. */
  211. shaka.polyfill.PatchedMediaKeysApple.MediaKeySystemAccess = class {
  212. /**
  213. * @param {string} keySystem
  214. * @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
  215. */
  216. constructor(keySystem, supportedConfigurations) {
  217. shaka.log.debug('PatchedMediaKeysApple.MediaKeySystemAccess');
  218. /** @type {string} */
  219. this.keySystem = keySystem;
  220. /** @private {!MediaKeySystemConfiguration} */
  221. this.configuration_;
  222. // Optimization: WebKitMediaKeys.isTypeSupported delays responses by a
  223. // significant amount of time, possibly to discourage fingerprinting.
  224. // Since we know only FairPlay is supported here, let's skip queries for
  225. // anything else to speed up the process.
  226. if (keySystem.startsWith('com.apple.fps')) {
  227. for (const cfg of supportedConfigurations) {
  228. const newCfg = this.checkConfig_(cfg);
  229. if (newCfg) {
  230. this.configuration_ = newCfg;
  231. return;
  232. }
  233. }
  234. }
  235. // According to the spec, this should be a DOMException, but there is not a
  236. // public constructor for that. So we make this look-alike instead.
  237. const unsupportedKeySystemError = new Error('Unsupported keySystem');
  238. unsupportedKeySystemError.name = 'NotSupportedError';
  239. unsupportedKeySystemError['code'] = DOMException.NOT_SUPPORTED_ERR;
  240. throw unsupportedKeySystemError;
  241. }
  242. /**
  243. * Check a single config for MediaKeySystemAccess.
  244. *
  245. * @param {MediaKeySystemConfiguration} cfg The requested config.
  246. * @return {?MediaKeySystemConfiguration} A matching config we can support, or
  247. * null if the input is not supportable.
  248. * @private
  249. */
  250. checkConfig_(cfg) {
  251. if (cfg.persistentState == 'required') {
  252. // Not supported by the prefixed API.
  253. return null;
  254. }
  255. // Create a new config object and start adding in the pieces which we find
  256. // support for. We will return this from getConfiguration() later if
  257. // asked.
  258. /** @type {!MediaKeySystemConfiguration} */
  259. const newCfg = {
  260. 'audioCapabilities': [],
  261. 'videoCapabilities': [],
  262. // It is technically against spec to return these as optional, but we
  263. // don't truly know their values from the prefixed API:
  264. 'persistentState': 'optional',
  265. 'distinctiveIdentifier': 'optional',
  266. // Pretend the requested init data types are supported, since we don't
  267. // really know that either:
  268. 'initDataTypes': cfg.initDataTypes,
  269. 'sessionTypes': ['temporary'],
  270. 'label': cfg.label,
  271. };
  272. // PatchedMediaKeysApple tests for key system availability through
  273. // WebKitMediaKeys.isTypeSupported.
  274. let ranAnyTests = false;
  275. let success = false;
  276. if (cfg.audioCapabilities) {
  277. for (const cap of cfg.audioCapabilities) {
  278. if (cap.contentType) {
  279. ranAnyTests = true;
  280. const contentType = cap.contentType.split(';')[0];
  281. if (WebKitMediaKeys.isTypeSupported(this.keySystem, contentType)) {
  282. newCfg.audioCapabilities.push(cap);
  283. success = true;
  284. }
  285. }
  286. }
  287. }
  288. if (cfg.videoCapabilities) {
  289. for (const cap of cfg.videoCapabilities) {
  290. if (cap.contentType) {
  291. ranAnyTests = true;
  292. const contentType = cap.contentType.split(';')[0];
  293. if (WebKitMediaKeys.isTypeSupported(this.keySystem, contentType)) {
  294. newCfg.videoCapabilities.push(cap);
  295. success = true;
  296. }
  297. }
  298. }
  299. }
  300. if (!ranAnyTests) {
  301. // If no specific types were requested, we check all common types to
  302. // find out if the key system is present at all.
  303. success = WebKitMediaKeys.isTypeSupported(this.keySystem, 'video/mp4');
  304. }
  305. if (success) {
  306. return newCfg;
  307. }
  308. return null;
  309. }
  310. /** @override */
  311. createMediaKeys() {
  312. shaka.log.debug(
  313. 'PatchedMediaKeysApple.MediaKeySystemAccess.createMediaKeys');
  314. // Alias
  315. const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
  316. const mediaKeys = new PatchedMediaKeysApple.MediaKeys(this.keySystem);
  317. return Promise.resolve(/** @type {!MediaKeys} */ (mediaKeys));
  318. }
  319. /** @override */
  320. getConfiguration() {
  321. shaka.log.debug(
  322. 'PatchedMediaKeysApple.MediaKeySystemAccess.getConfiguration');
  323. return this.configuration_;
  324. }
  325. };
  326. /**
  327. * An implementation of MediaKeys.
  328. *
  329. * @implements {MediaKeys}
  330. */
  331. shaka.polyfill.PatchedMediaKeysApple.MediaKeys = class {
  332. /** @param {string} keySystem */
  333. constructor(keySystem) {
  334. shaka.log.debug('PatchedMediaKeysApple.MediaKeys');
  335. /** @private {!WebKitMediaKeys} */
  336. this.nativeMediaKeys_ = new WebKitMediaKeys(keySystem);
  337. /** @private {!shaka.util.EventManager} */
  338. this.eventManager_ = new shaka.util.EventManager();
  339. }
  340. /** @override */
  341. createSession(sessionType) {
  342. shaka.log.debug('PatchedMediaKeysApple.MediaKeys.createSession');
  343. sessionType = sessionType || 'temporary';
  344. // For now, only the 'temporary' type is supported.
  345. if (sessionType != 'temporary') {
  346. throw new TypeError('Session type ' + sessionType +
  347. ' is unsupported on this platform.');
  348. }
  349. // Alias
  350. const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
  351. return new PatchedMediaKeysApple.MediaKeySession(
  352. this.nativeMediaKeys_, sessionType);
  353. }
  354. /** @override */
  355. setServerCertificate(serverCertificate) {
  356. shaka.log.debug('PatchedMediaKeysApple.MediaKeys.setServerCertificate');
  357. return Promise.resolve(false);
  358. }
  359. /**
  360. * @param {HTMLMediaElement} media
  361. * @protected
  362. * @return {!Promise}
  363. */
  364. setMedia(media) {
  365. // Alias
  366. const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
  367. // Remove any old listeners.
  368. this.eventManager_.removeAll();
  369. // It is valid for media to be null; null is used to flag that event
  370. // handlers need to be cleaned up.
  371. if (!media) {
  372. return Promise.resolve();
  373. }
  374. // Intercept and translate these prefixed EME events.
  375. this.eventManager_.listen(media, 'webkitneedkey',
  376. /** @type {shaka.util.EventManager.ListenerType} */
  377. (PatchedMediaKeysApple.onWebkitNeedKey_));
  378. // Wrap native HTMLMediaElement.webkitSetMediaKeys with a Promise.
  379. try {
  380. // Some browsers require that readyState >=1 before mediaKeys can be
  381. // set, so check this and wait for loadedmetadata if we are not in the
  382. // correct state
  383. shaka.util.MediaReadyState.waitForReadyState(media,
  384. HTMLMediaElement.HAVE_METADATA,
  385. this.eventManager_, () => {
  386. media.webkitSetMediaKeys(this.nativeMediaKeys_);
  387. });
  388. return Promise.resolve();
  389. } catch (exception) {
  390. return Promise.reject(exception);
  391. }
  392. }
  393. };
  394. /**
  395. * An implementation of MediaKeySession.
  396. *
  397. * @implements {MediaKeySession}
  398. */
  399. shaka.polyfill.PatchedMediaKeysApple.MediaKeySession =
  400. class extends shaka.util.FakeEventTarget {
  401. /**
  402. * @param {WebKitMediaKeys} nativeMediaKeys
  403. * @param {string} sessionType
  404. */
  405. constructor(nativeMediaKeys, sessionType) {
  406. shaka.log.debug('PatchedMediaKeysApple.MediaKeySession');
  407. super();
  408. /**
  409. * The native MediaKeySession, which will be created in generateRequest.
  410. * @private {WebKitMediaKeySession}
  411. */
  412. this.nativeMediaKeySession_ = null;
  413. /** @private {WebKitMediaKeys} */
  414. this.nativeMediaKeys_ = nativeMediaKeys;
  415. // Promises that are resolved later
  416. /** @private {shaka.util.PublicPromise} */
  417. this.generateRequestPromise_ = null;
  418. /** @private {shaka.util.PublicPromise} */
  419. this.updatePromise_ = null;
  420. /** @private {!shaka.util.EventManager} */
  421. this.eventManager_ = new shaka.util.EventManager();
  422. /** @type {string} */
  423. this.sessionId = '';
  424. /** @type {number} */
  425. this.expiration = NaN;
  426. /** @type {!shaka.util.PublicPromise} */
  427. this.closed = new shaka.util.PublicPromise();
  428. /** @type {!shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap} */
  429. this.keyStatuses =
  430. new shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap();
  431. }
  432. /** @override */
  433. generateRequest(initDataType, initData) {
  434. shaka.log.debug(
  435. 'PatchedMediaKeysApple.MediaKeySession.generateRequest');
  436. this.generateRequestPromise_ = new shaka.util.PublicPromise();
  437. try {
  438. // This EME spec version requires a MIME content type as the 1st param to
  439. // createSession, but doesn't seem to matter what the value is.
  440. // It also only accepts Uint8Array, not ArrayBuffer, so explicitly make
  441. // initData into a Uint8Array.
  442. const session = this.nativeMediaKeys_.createSession(
  443. 'video/mp4', shaka.util.BufferUtils.toUint8(initData));
  444. this.nativeMediaKeySession_ = session;
  445. this.sessionId = session.sessionId || '';
  446. // Attach session event handlers here.
  447. this.eventManager_.listen(
  448. this.nativeMediaKeySession_, 'webkitkeymessage',
  449. /** @type {shaka.util.EventManager.ListenerType} */
  450. ((event) => this.onWebkitKeyMessage_(event)));
  451. this.eventManager_.listen(session, 'webkitkeyadded',
  452. /** @type {shaka.util.EventManager.ListenerType} */
  453. ((event) => this.onWebkitKeyAdded_(event)));
  454. this.eventManager_.listen(session, 'webkitkeyerror',
  455. /** @type {shaka.util.EventManager.ListenerType} */
  456. ((event) => this.onWebkitKeyError_(event)));
  457. this.updateKeyStatus_('status-pending');
  458. } catch (exception) {
  459. this.generateRequestPromise_.reject(exception);
  460. }
  461. return this.generateRequestPromise_;
  462. }
  463. /** @override */
  464. load() {
  465. shaka.log.debug('PatchedMediaKeysApple.MediaKeySession.load');
  466. return Promise.reject(new Error('MediaKeySession.load not yet supported'));
  467. }
  468. /** @override */
  469. update(response) {
  470. shaka.log.debug('PatchedMediaKeysApple.MediaKeySession.update');
  471. this.updatePromise_ = new shaka.util.PublicPromise();
  472. try {
  473. // Pass through to the native session.
  474. this.nativeMediaKeySession_.update(
  475. shaka.util.BufferUtils.toUint8(response));
  476. } catch (exception) {
  477. this.updatePromise_.reject(exception);
  478. }
  479. return this.updatePromise_;
  480. }
  481. /** @override */
  482. close() {
  483. shaka.log.debug('PatchedMediaKeysApple.MediaKeySession.close');
  484. try {
  485. // Pass through to the native session.
  486. this.nativeMediaKeySession_.close();
  487. this.closed.resolve();
  488. this.eventManager_.removeAll();
  489. } catch (exception) {
  490. this.closed.reject(exception);
  491. }
  492. return this.closed;
  493. }
  494. /** @override */
  495. remove() {
  496. shaka.log.debug('PatchedMediaKeysApple.MediaKeySession.remove');
  497. return Promise.reject(new Error(
  498. 'MediaKeySession.remove is only applicable for persistent licenses, ' +
  499. 'which are not supported on this platform'));
  500. }
  501. /**
  502. * Handler for the native keymessage event on WebKitMediaKeySession.
  503. *
  504. * @param {!MediaKeyEvent} event
  505. * @private
  506. */
  507. onWebkitKeyMessage_(event) {
  508. shaka.log.debug('PatchedMediaKeysApple.onWebkitKeyMessage_', event);
  509. // We can now resolve this.generateRequestPromise, which should be non-null.
  510. goog.asserts.assert(this.generateRequestPromise_,
  511. 'generateRequestPromise_ should be set before now!');
  512. if (this.generateRequestPromise_) {
  513. this.generateRequestPromise_.resolve();
  514. this.generateRequestPromise_ = null;
  515. }
  516. const isNew = this.keyStatuses.getStatus() == undefined;
  517. const data = new Map()
  518. .set('messageType', isNew ? 'license-request' : 'license-renewal')
  519. .set('message', shaka.util.BufferUtils.toArrayBuffer(event.message));
  520. const event2 = new shaka.util.FakeEvent('message', data);
  521. this.dispatchEvent(event2);
  522. }
  523. /**
  524. * Handler for the native keyadded event on WebKitMediaKeySession.
  525. *
  526. * @param {!MediaKeyEvent} event
  527. * @private
  528. */
  529. onWebkitKeyAdded_(event) {
  530. shaka.log.debug('PatchedMediaKeysApple.onWebkitKeyAdded_', event);
  531. // This shouldn't fire while we're in the middle of generateRequest,
  532. // but if it does, we will need to change the logic to account for it.
  533. goog.asserts.assert(!this.generateRequestPromise_,
  534. 'Key added during generate!');
  535. // We can now resolve this.updatePromise, which should be non-null.
  536. goog.asserts.assert(this.updatePromise_,
  537. 'updatePromise_ should be set before now!');
  538. if (this.updatePromise_) {
  539. this.updateKeyStatus_('usable');
  540. this.updatePromise_.resolve();
  541. this.updatePromise_ = null;
  542. }
  543. }
  544. /**
  545. * Handler for the native keyerror event on WebKitMediaKeySession.
  546. *
  547. * @param {!MediaKeyEvent} event
  548. * @private
  549. */
  550. onWebkitKeyError_(event) {
  551. shaka.log.debug('PatchedMediaKeysApple.onWebkitKeyError_', event);
  552. const error = new Error('EME PatchedMediaKeysApple key error');
  553. error['errorCode'] = this.nativeMediaKeySession_.error;
  554. if (this.generateRequestPromise_ != null) {
  555. this.generateRequestPromise_.reject(error);
  556. this.generateRequestPromise_ = null;
  557. } else if (this.updatePromise_ != null) {
  558. this.updatePromise_.reject(error);
  559. this.updatePromise_ = null;
  560. } else {
  561. // Unexpected error - map native codes to standardised key statuses.
  562. // Possible values of this.nativeMediaKeySession_.error.code:
  563. // MEDIA_KEYERR_UNKNOWN = 1
  564. // MEDIA_KEYERR_CLIENT = 2
  565. // MEDIA_KEYERR_SERVICE = 3
  566. // MEDIA_KEYERR_OUTPUT = 4
  567. // MEDIA_KEYERR_HARDWARECHANGE = 5
  568. // MEDIA_KEYERR_DOMAIN = 6
  569. switch (this.nativeMediaKeySession_.error.code) {
  570. case WebKitMediaKeyError.MEDIA_KEYERR_OUTPUT:
  571. case WebKitMediaKeyError.MEDIA_KEYERR_HARDWARECHANGE:
  572. this.updateKeyStatus_('output-not-allowed');
  573. break;
  574. default:
  575. this.updateKeyStatus_('internal-error');
  576. break;
  577. }
  578. }
  579. }
  580. /**
  581. * Updates key status and dispatch a 'keystatuseschange' event.
  582. *
  583. * @param {string} status
  584. * @private
  585. */
  586. updateKeyStatus_(status) {
  587. this.keyStatuses.setStatus(status);
  588. const event = new shaka.util.FakeEvent('keystatuseschange');
  589. this.dispatchEvent(event);
  590. }
  591. };
  592. /**
  593. * @summary An implementation of MediaKeyStatusMap.
  594. * This fakes a map with a single key ID.
  595. *
  596. * @todo Consolidate the MediaKeyStatusMap types in these polyfills.
  597. * @implements {MediaKeyStatusMap}
  598. */
  599. shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap = class {
  600. /** */
  601. constructor() {
  602. /**
  603. * @type {number}
  604. */
  605. this.size = 0;
  606. /**
  607. * @private {string|undefined}
  608. */
  609. this.status_ = undefined;
  610. }
  611. /**
  612. * An internal method used by the session to set key status.
  613. * @param {string|undefined} status
  614. */
  615. setStatus(status) {
  616. this.size = status == undefined ? 0 : 1;
  617. this.status_ = status;
  618. }
  619. /**
  620. * An internal method used by the session to get key status.
  621. * @return {string|undefined}
  622. */
  623. getStatus() {
  624. return this.status_;
  625. }
  626. /** @override */
  627. forEach(fn) {
  628. if (this.status_) {
  629. fn(this.status_, shaka.media.DrmEngine.DUMMY_KEY_ID.value());
  630. }
  631. }
  632. /** @override */
  633. get(keyId) {
  634. if (this.has(keyId)) {
  635. return this.status_;
  636. }
  637. return undefined;
  638. }
  639. /** @override */
  640. has(keyId) {
  641. const fakeKeyId = shaka.media.DrmEngine.DUMMY_KEY_ID.value();
  642. if (this.status_ && shaka.util.BufferUtils.equal(keyId, fakeKeyId)) {
  643. return true;
  644. }
  645. return false;
  646. }
  647. /**
  648. * @suppress {missingReturn}
  649. * @override
  650. */
  651. entries() {
  652. goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
  653. }
  654. /**
  655. * @suppress {missingReturn}
  656. * @override
  657. */
  658. keys() {
  659. goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
  660. }
  661. /**
  662. * @suppress {missingReturn}
  663. * @override
  664. */
  665. values() {
  666. goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
  667. }
  668. };