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