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