Source: lib/polyfill/patchedmediakeys_webkit.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.polyfill.PatchedMediaKeysWebkit');
  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.PublicPromise');
  16. goog.require('shaka.util.StringUtils');
  17. goog.require('shaka.util.Timer');
  18. goog.require('shaka.util.Uint8ArrayUtils');
  19. /**
  20. * @summary A polyfill to implement
  21. * {@link https://bit.ly/EmeMar15 EME draft 12 March 2015} on top of
  22. * webkit-prefixed {@link https://bit.ly/Eme01b EME v0.1b}.
  23. * @export
  24. */
  25. shaka.polyfill.PatchedMediaKeysWebkit = class {
  26. /**
  27. * Installs the polyfill if needed.
  28. * @export
  29. */
  30. static install() {
  31. // Alias.
  32. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  33. if (!window.HTMLVideoElement ||
  34. (navigator.requestMediaKeySystemAccess &&
  35. // eslint-disable-next-line no-restricted-syntax
  36. MediaKeySystemAccess.prototype.getConfiguration)) {
  37. return;
  38. }
  39. // eslint-disable-next-line no-restricted-syntax
  40. if (HTMLMediaElement.prototype.webkitGenerateKeyRequest) {
  41. shaka.log.info('Using webkit-prefixed EME v0.1b');
  42. PatchedMediaKeysWebkit.prefix_ = 'webkit';
  43. // eslint-disable-next-line no-restricted-syntax
  44. } else if (HTMLMediaElement.prototype.generateKeyRequest) {
  45. shaka.log.info('Using nonprefixed EME v0.1b');
  46. } else {
  47. return;
  48. }
  49. goog.asserts.assert(
  50. // eslint-disable-next-line no-restricted-syntax
  51. HTMLMediaElement.prototype[
  52. PatchedMediaKeysWebkit.prefixApi_('generateKeyRequest')],
  53. 'PatchedMediaKeysWebkit APIs not available!');
  54. // Install patches.
  55. navigator.requestMediaKeySystemAccess =
  56. PatchedMediaKeysWebkit.requestMediaKeySystemAccess;
  57. // Delete mediaKeys to work around strict mode compatibility issues.
  58. // eslint-disable-next-line no-restricted-syntax
  59. delete HTMLMediaElement.prototype['mediaKeys'];
  60. // Work around read-only declaration for mediaKeys by using a string.
  61. // eslint-disable-next-line no-restricted-syntax
  62. HTMLMediaElement.prototype['mediaKeys'] = null;
  63. // eslint-disable-next-line no-restricted-syntax
  64. HTMLMediaElement.prototype.setMediaKeys =
  65. PatchedMediaKeysWebkit.setMediaKeys;
  66. window.MediaKeys = PatchedMediaKeysWebkit.MediaKeys;
  67. window.MediaKeySystemAccess = PatchedMediaKeysWebkit.MediaKeySystemAccess;
  68. }
  69. /**
  70. * Prefix the api with the stored prefix.
  71. *
  72. * @param {string} api
  73. * @return {string}
  74. * @private
  75. */
  76. static prefixApi_(api) {
  77. const prefix = shaka.polyfill.PatchedMediaKeysWebkit.prefix_;
  78. if (prefix) {
  79. return prefix + api.charAt(0).toUpperCase() + api.slice(1);
  80. }
  81. return api;
  82. }
  83. /**
  84. * An implementation of navigator.requestMediaKeySystemAccess.
  85. * Retrieves a MediaKeySystemAccess object.
  86. *
  87. * @this {!Navigator}
  88. * @param {string} keySystem
  89. * @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
  90. * @return {!Promise.<!MediaKeySystemAccess>}
  91. */
  92. static requestMediaKeySystemAccess(keySystem, supportedConfigurations) {
  93. shaka.log.debug('PatchedMediaKeysWebkit.requestMediaKeySystemAccess');
  94. goog.asserts.assert(this == navigator,
  95. 'bad "this" for requestMediaKeySystemAccess');
  96. // Alias.
  97. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  98. try {
  99. const access = new PatchedMediaKeysWebkit.MediaKeySystemAccess(
  100. keySystem, supportedConfigurations);
  101. return Promise.resolve(/** @type {!MediaKeySystemAccess} */ (access));
  102. } catch (exception) {
  103. return Promise.reject(exception);
  104. }
  105. }
  106. /**
  107. * An implementation of HTMLMediaElement.prototype.setMediaKeys.
  108. * Attaches a MediaKeys object to the media element.
  109. *
  110. * @this {!HTMLMediaElement}
  111. * @param {MediaKeys} mediaKeys
  112. * @return {!Promise}
  113. */
  114. static setMediaKeys(mediaKeys) {
  115. shaka.log.debug('PatchedMediaKeysWebkit.setMediaKeys');
  116. goog.asserts.assert(this instanceof HTMLMediaElement,
  117. 'bad "this" for setMediaKeys');
  118. // Alias.
  119. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  120. const newMediaKeys =
  121. /** @type {shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys} */ (
  122. mediaKeys);
  123. const oldMediaKeys =
  124. /** @type {shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys} */ (
  125. this.mediaKeys);
  126. if (oldMediaKeys && oldMediaKeys != newMediaKeys) {
  127. goog.asserts.assert(
  128. oldMediaKeys instanceof PatchedMediaKeysWebkit.MediaKeys,
  129. 'non-polyfill instance of oldMediaKeys');
  130. // Have the old MediaKeys stop listening to events on the video tag.
  131. oldMediaKeys.setMedia(null);
  132. }
  133. delete this['mediaKeys']; // In case there is an existing getter.
  134. this['mediaKeys'] = mediaKeys; // Work around the read-only declaration.
  135. if (newMediaKeys) {
  136. goog.asserts.assert(
  137. newMediaKeys instanceof PatchedMediaKeysWebkit.MediaKeys,
  138. 'non-polyfill instance of newMediaKeys');
  139. newMediaKeys.setMedia(this);
  140. }
  141. return Promise.resolve();
  142. }
  143. /**
  144. * For some of this polyfill's implementation, we need to query a video
  145. * element. But for some embedded systems, it is memory-expensive to create
  146. * multiple video elements. Therefore, we check the document to see if we can
  147. * borrow one to query before we fall back to creating one temporarily.
  148. *
  149. * @return {!HTMLVideoElement}
  150. * @private
  151. */
  152. static getVideoElement_() {
  153. const videos = document.getElementsByTagName('video');
  154. const video = videos.length ? videos[0] : document.createElement('video');
  155. return /** @type {!HTMLVideoElement} */(video);
  156. }
  157. };
  158. /**
  159. * An implementation of MediaKeySystemAccess.
  160. *
  161. * @implements {MediaKeySystemAccess}
  162. */
  163. shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySystemAccess = class {
  164. /**
  165. * @param {string} keySystem
  166. * @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
  167. */
  168. constructor(keySystem, supportedConfigurations) {
  169. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySystemAccess');
  170. /** @type {string} */
  171. this.keySystem = keySystem;
  172. /** @private {string} */
  173. this.internalKeySystem_ = keySystem;
  174. /** @private {!MediaKeySystemConfiguration} */
  175. this.configuration_;
  176. // This is only a guess, since we don't really know from the prefixed API.
  177. let allowPersistentState = false;
  178. if (keySystem == 'org.w3.clearkey') {
  179. // ClearKey's string must be prefixed in v0.1b.
  180. this.internalKeySystem_ = 'webkit-org.w3.clearkey';
  181. // ClearKey doesn't support persistence.
  182. allowPersistentState = false;
  183. }
  184. let success = false;
  185. const tmpVideo = shaka.polyfill.PatchedMediaKeysWebkit.getVideoElement_();
  186. for (const cfg of supportedConfigurations) {
  187. // Create a new config object and start adding in the pieces which we
  188. // find support for. We will return this from getConfiguration() if
  189. // asked.
  190. /** @type {!MediaKeySystemConfiguration} */
  191. const newCfg = {
  192. 'audioCapabilities': [],
  193. 'videoCapabilities': [],
  194. // It is technically against spec to return these as optional, but we
  195. // don't truly know their values from the prefixed API:
  196. 'persistentState': 'optional',
  197. 'distinctiveIdentifier': 'optional',
  198. // Pretend the requested init data types are supported, since we don't
  199. // really know that either:
  200. 'initDataTypes': cfg.initDataTypes,
  201. 'sessionTypes': ['temporary'],
  202. 'label': cfg.label,
  203. };
  204. // v0.1b tests for key system availability with an extra argument on
  205. // canPlayType.
  206. let ranAnyTests = false;
  207. if (cfg.audioCapabilities) {
  208. for (const cap of cfg.audioCapabilities) {
  209. if (cap.contentType) {
  210. ranAnyTests = true;
  211. // In Chrome <= 40, if you ask about Widevine-encrypted audio
  212. // support, you get a false-negative when you specify codec
  213. // information. Work around this by stripping codec info for audio
  214. // types.
  215. const contentType = cap.contentType.split(';')[0];
  216. if (tmpVideo.canPlayType(contentType, this.internalKeySystem_)) {
  217. newCfg.audioCapabilities.push(cap);
  218. success = true;
  219. }
  220. }
  221. }
  222. }
  223. if (cfg.videoCapabilities) {
  224. for (const cap of cfg.videoCapabilities) {
  225. if (cap.contentType) {
  226. ranAnyTests = true;
  227. if (tmpVideo.canPlayType(
  228. cap.contentType, this.internalKeySystem_)) {
  229. newCfg.videoCapabilities.push(cap);
  230. success = true;
  231. }
  232. }
  233. }
  234. }
  235. if (!ranAnyTests) {
  236. // If no specific types were requested, we check all common types to
  237. // find out if the key system is present at all.
  238. success =
  239. tmpVideo.canPlayType('video/mp4', this.internalKeySystem_) ||
  240. tmpVideo.canPlayType('video/webm', this.internalKeySystem_);
  241. }
  242. if (cfg.persistentState == 'required') {
  243. if (allowPersistentState) {
  244. newCfg.persistentState = 'required';
  245. newCfg.sessionTypes = ['persistent-license'];
  246. } else {
  247. success = false;
  248. }
  249. }
  250. if (success) {
  251. this.configuration_ = newCfg;
  252. return;
  253. }
  254. } // for each cfg in supportedConfigurations
  255. let message = 'Unsupported keySystem';
  256. if (keySystem == 'org.w3.clearkey' || keySystem == 'com.widevine.alpha') {
  257. message = 'None of the requested configurations were supported.';
  258. }
  259. // According to the spec, this should be a DOMException, but there is not a
  260. // public constructor for that. So we make this look-alike instead.
  261. const unsupportedError = new Error(message);
  262. unsupportedError.name = 'NotSupportedError';
  263. unsupportedError['code'] = DOMException.NOT_SUPPORTED_ERR;
  264. throw unsupportedError;
  265. }
  266. /** @override */
  267. createMediaKeys() {
  268. shaka.log.debug(
  269. 'PatchedMediaKeysWebkit.MediaKeySystemAccess.createMediaKeys');
  270. // Alias.
  271. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  272. const mediaKeys =
  273. new PatchedMediaKeysWebkit.MediaKeys(this.internalKeySystem_);
  274. return Promise.resolve(/** @type {!MediaKeys} */ (mediaKeys));
  275. }
  276. /** @override */
  277. getConfiguration() {
  278. shaka.log.debug(
  279. 'PatchedMediaKeysWebkit.MediaKeySystemAccess.getConfiguration');
  280. return this.configuration_;
  281. }
  282. };
  283. /**
  284. * An implementation of MediaKeys.
  285. *
  286. * @implements {MediaKeys}
  287. */
  288. shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys = class {
  289. /**
  290. * @param {string} keySystem
  291. */
  292. constructor(keySystem) {
  293. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeys');
  294. /** @private {string} */
  295. this.keySystem_ = keySystem;
  296. /** @private {HTMLMediaElement} */
  297. this.media_ = null;
  298. /** @private {!shaka.util.EventManager} */
  299. this.eventManager_ = new shaka.util.EventManager();
  300. /**
  301. * @private {Array.<!shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession>}
  302. */
  303. this.newSessions_ = [];
  304. /**
  305. * @private {!Map.<string,
  306. * !shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession>}
  307. */
  308. this.sessionMap_ = new Map();
  309. }
  310. /**
  311. * @param {HTMLMediaElement} media
  312. * @protected
  313. */
  314. setMedia(media) {
  315. this.media_ = media;
  316. // Remove any old listeners.
  317. this.eventManager_.removeAll();
  318. const prefix = shaka.polyfill.PatchedMediaKeysWebkit.prefix_;
  319. if (media) {
  320. // Intercept and translate these prefixed EME events.
  321. this.eventManager_.listen(media, prefix + 'needkey',
  322. /** @type {shaka.util.EventManager.ListenerType} */ (
  323. (event) => this.onWebkitNeedKey_(event)));
  324. this.eventManager_.listen(media, prefix + 'keymessage',
  325. /** @type {shaka.util.EventManager.ListenerType} */ (
  326. (event) => this.onWebkitKeyMessage_(event)));
  327. this.eventManager_.listen(media, prefix + 'keyadded',
  328. /** @type {shaka.util.EventManager.ListenerType} */ (
  329. (event) => this.onWebkitKeyAdded_(event)));
  330. this.eventManager_.listen(media, prefix + 'keyerror',
  331. /** @type {shaka.util.EventManager.ListenerType} */ (
  332. (event) => this.onWebkitKeyError_(event)));
  333. }
  334. }
  335. /** @override */
  336. createSession(sessionType) {
  337. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeys.createSession');
  338. sessionType = sessionType || 'temporary';
  339. if (sessionType != 'temporary' && sessionType != 'persistent-license') {
  340. throw new TypeError('Session type ' + sessionType +
  341. ' is unsupported on this platform.');
  342. }
  343. // Alias.
  344. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  345. // Unprefixed EME allows for session creation without a video tag or src.
  346. // Prefixed EME requires both a valid HTMLMediaElement and a src.
  347. const media = this.media_ || /** @type {!HTMLMediaElement} */(
  348. document.createElement('video'));
  349. if (!media.src) {
  350. media.src = 'about:blank';
  351. }
  352. const session = new PatchedMediaKeysWebkit.MediaKeySession(
  353. media, this.keySystem_, sessionType);
  354. this.newSessions_.push(session);
  355. return session;
  356. }
  357. /** @override */
  358. setServerCertificate(serverCertificate) {
  359. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeys.setServerCertificate');
  360. // There is no equivalent in v0.1b, so return failure.
  361. return Promise.resolve(false);
  362. }
  363. /**
  364. * @param {!MediaKeyEvent} event
  365. * @suppress {constantProperty} We reassign what would be const on a real
  366. * MediaEncryptedEvent, but in our look-alike event.
  367. * @private
  368. */
  369. onWebkitNeedKey_(event) {
  370. shaka.log.debug('PatchedMediaKeysWebkit.onWebkitNeedKey_', event);
  371. goog.asserts.assert(this.media_, 'media_ not set in onWebkitNeedKey_');
  372. const event2 = new CustomEvent('encrypted');
  373. const encryptedEvent =
  374. /** @type {!MediaEncryptedEvent} */(/** @type {?} */(event2));
  375. // initDataType is not used by v0.1b EME, so any valid value is fine here.
  376. encryptedEvent.initDataType = 'cenc';
  377. encryptedEvent.initData = shaka.util.BufferUtils.toArrayBuffer(
  378. event.initData);
  379. this.media_.dispatchEvent(event2);
  380. }
  381. /**
  382. * @param {!MediaKeyEvent} event
  383. * @private
  384. */
  385. onWebkitKeyMessage_(event) {
  386. shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyMessage_', event);
  387. const session = this.findSession_(event.sessionId);
  388. if (!session) {
  389. shaka.log.error('Session not found', event.sessionId);
  390. return;
  391. }
  392. const isNew = session.keyStatuses.getStatus() == undefined;
  393. const data = new Map()
  394. .set('messageType', isNew ? 'licenserequest' : 'licenserenewal')
  395. .set('message', event.message);
  396. const event2 = new shaka.util.FakeEvent('message', data);
  397. session.generated();
  398. session.dispatchEvent(event2);
  399. }
  400. /**
  401. * @param {!MediaKeyEvent} event
  402. * @private
  403. */
  404. onWebkitKeyAdded_(event) {
  405. shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyAdded_', event);
  406. const session = this.findSession_(event.sessionId);
  407. goog.asserts.assert(
  408. session, 'unable to find session in onWebkitKeyAdded_');
  409. if (session) {
  410. session.ready();
  411. }
  412. }
  413. /**
  414. * @param {!MediaKeyEvent} event
  415. * @private
  416. */
  417. onWebkitKeyError_(event) {
  418. shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyError_', event);
  419. const session = this.findSession_(event.sessionId);
  420. goog.asserts.assert(
  421. session, 'unable to find session in onWebkitKeyError_');
  422. if (session) {
  423. session.handleError(event);
  424. }
  425. }
  426. /**
  427. * @param {string} sessionId
  428. * @return {shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession}
  429. * @private
  430. */
  431. findSession_(sessionId) {
  432. let session = this.sessionMap_.get(sessionId);
  433. if (session) {
  434. shaka.log.debug(
  435. 'PatchedMediaKeysWebkit.MediaKeys.findSession_', session);
  436. return session;
  437. }
  438. session = this.newSessions_.shift();
  439. if (session) {
  440. session.sessionId = sessionId;
  441. this.sessionMap_.set(sessionId, session);
  442. shaka.log.debug(
  443. 'PatchedMediaKeysWebkit.MediaKeys.findSession_', session);
  444. return session;
  445. }
  446. return null;
  447. }
  448. };
  449. /**
  450. * An implementation of MediaKeySession.
  451. *
  452. * @implements {MediaKeySession}
  453. */
  454. shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession =
  455. class extends shaka.util.FakeEventTarget {
  456. /**
  457. * @param {!HTMLMediaElement} media
  458. * @param {string} keySystem
  459. * @param {string} sessionType
  460. */
  461. constructor(media, keySystem, sessionType) {
  462. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession');
  463. super();
  464. /** @private {!HTMLMediaElement} */
  465. this.media_ = media;
  466. /** @private {boolean} */
  467. this.initialized_ = false;
  468. /** @private {shaka.util.PublicPromise} */
  469. this.generatePromise_ = null;
  470. /** @private {shaka.util.PublicPromise} */
  471. this.updatePromise_ = null;
  472. /** @private {string} */
  473. this.keySystem_ = keySystem;
  474. /** @private {string} */
  475. this.type_ = sessionType;
  476. /** @type {string} */
  477. this.sessionId = '';
  478. /** @type {number} */
  479. this.expiration = NaN;
  480. /** @type {!shaka.util.PublicPromise} */
  481. this.closed = new shaka.util.PublicPromise();
  482. /** @type {!shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap} */
  483. this.keyStatuses =
  484. new shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap();
  485. }
  486. /**
  487. * Signals that the license request has been generated. This resolves the
  488. * 'generateRequest' promise.
  489. *
  490. * @protected
  491. */
  492. generated() {
  493. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.generated');
  494. if (this.generatePromise_) {
  495. this.generatePromise_.resolve();
  496. this.generatePromise_ = null;
  497. }
  498. }
  499. /**
  500. * Signals that the session is 'ready', which is the terminology used in older
  501. * versions of EME. The new signal is to resolve the 'update' promise. This
  502. * translates between the two.
  503. *
  504. * @protected
  505. */
  506. ready() {
  507. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.ready');
  508. this.updateKeyStatus_('usable');
  509. if (this.updatePromise_) {
  510. this.updatePromise_.resolve();
  511. }
  512. this.updatePromise_ = null;
  513. }
  514. /**
  515. * Either rejects a promise, or dispatches an error event, as appropriate.
  516. *
  517. * @param {!MediaKeyEvent} event
  518. */
  519. handleError(event) {
  520. shaka.log.debug(
  521. 'PatchedMediaKeysWebkit.MediaKeySession.handleError', event);
  522. // This does not match the DOMException we get in current WD EME, but it
  523. // will at least provide some information which can be used to look into the
  524. // problem.
  525. const error = new Error('EME v0.1b key error');
  526. const errorCode = event.errorCode;
  527. errorCode.systemCode = event.systemCode;
  528. error['errorCode'] = errorCode;
  529. // The presence or absence of sessionId indicates whether this corresponds
  530. // to generateRequest() or update().
  531. if (!event.sessionId && this.generatePromise_) {
  532. if (event.systemCode == 45) {
  533. error.message = 'Unsupported session type.';
  534. }
  535. this.generatePromise_.reject(error);
  536. this.generatePromise_ = null;
  537. } else if (event.sessionId && this.updatePromise_) {
  538. this.updatePromise_.reject(error);
  539. this.updatePromise_ = null;
  540. } else {
  541. // This mapping of key statuses is imperfect at best.
  542. const code = event.errorCode.code;
  543. const systemCode = event.systemCode;
  544. if (code == MediaKeyError['MEDIA_KEYERR_OUTPUT']) {
  545. this.updateKeyStatus_('output-restricted');
  546. } else if (systemCode == 1) {
  547. this.updateKeyStatus_('expired');
  548. } else {
  549. this.updateKeyStatus_('internal-error');
  550. }
  551. }
  552. }
  553. /**
  554. * Logic which is shared between generateRequest() and load(), both of which
  555. * are ultimately implemented with webkitGenerateKeyRequest in prefixed EME.
  556. *
  557. * @param {?BufferSource} initData
  558. * @param {?string} offlineSessionId
  559. * @return {!Promise}
  560. * @private
  561. */
  562. generate_(initData, offlineSessionId) {
  563. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  564. if (this.initialized_) {
  565. const error = new Error('The session is already initialized.');
  566. return Promise.reject(error);
  567. }
  568. this.initialized_ = true;
  569. /** @type {!Uint8Array} */
  570. let mangledInitData;
  571. try {
  572. if (this.type_ == 'persistent-license') {
  573. const StringUtils = shaka.util.StringUtils;
  574. if (!offlineSessionId) {
  575. goog.asserts.assert(initData, 'expecting init data');
  576. // Persisting the initial license.
  577. // Prefix the init data with a tag to indicate persistence.
  578. const prefix = StringUtils.toUTF8('PERSISTENT|');
  579. mangledInitData = shaka.util.Uint8ArrayUtils.concat(prefix, initData);
  580. } else {
  581. // Loading a stored license.
  582. // Prefix the init data (which is really a session ID) with a tag to
  583. // indicate that we are loading a persisted session.
  584. mangledInitData = shaka.util.BufferUtils.toUint8(
  585. StringUtils.toUTF8('LOAD_SESSION|' + offlineSessionId));
  586. }
  587. } else {
  588. // Streaming.
  589. goog.asserts.assert(this.type_ == 'temporary',
  590. 'expected temporary session');
  591. goog.asserts.assert(!offlineSessionId,
  592. 'unexpected offline session ID');
  593. goog.asserts.assert(initData, 'expecting init data');
  594. mangledInitData = shaka.util.BufferUtils.toUint8(initData);
  595. }
  596. goog.asserts.assert(mangledInitData, 'init data not set!');
  597. } catch (exception) {
  598. return Promise.reject(exception);
  599. }
  600. goog.asserts.assert(this.generatePromise_ == null,
  601. 'generatePromise_ should be null');
  602. this.generatePromise_ = new shaka.util.PublicPromise();
  603. // Because we are hacking media.src in createSession to better emulate
  604. // unprefixed EME's ability to create sessions and license requests without
  605. // a video tag, we can get ourselves into trouble. It seems that sometimes,
  606. // the setting of media.src hasn't been processed by some other thread, and
  607. // GKR can throw an exception. If this occurs, wait 10 ms and try again at
  608. // most once. This situation should only occur when init data is available
  609. // ahead of the 'needkey' event.
  610. const generateKeyRequestName =
  611. PatchedMediaKeysWebkit.prefixApi_('generateKeyRequest');
  612. try {
  613. this.media_[generateKeyRequestName](this.keySystem_, mangledInitData);
  614. } catch (exception) {
  615. if (exception.name != 'InvalidStateError') {
  616. this.generatePromise_ = null;
  617. return Promise.reject(exception);
  618. }
  619. const timer = new shaka.util.Timer(() => {
  620. try {
  621. this.media_[generateKeyRequestName](this.keySystem_, mangledInitData);
  622. } catch (exception2) {
  623. this.generatePromise_.reject(exception2);
  624. this.generatePromise_ = null;
  625. }
  626. });
  627. timer.tickAfter(/* seconds= */ 0.01);
  628. }
  629. return this.generatePromise_;
  630. }
  631. /**
  632. * An internal version of update which defers new calls while old ones are in
  633. * progress.
  634. *
  635. * @param {!shaka.util.PublicPromise} promise The promise associated with
  636. * this call.
  637. * @param {BufferSource} response
  638. * @private
  639. */
  640. update_(promise, response) {
  641. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  642. if (this.updatePromise_) {
  643. // We already have an update in-progress, so defer this one until after
  644. // the old one is resolved. Execute this whether the original one
  645. // succeeds or fails.
  646. this.updatePromise_.then(() => this.update_(promise, response))
  647. .catch(() => this.update_(promise, response));
  648. return;
  649. }
  650. this.updatePromise_ = promise;
  651. let key;
  652. let keyId;
  653. if (this.keySystem_ == 'webkit-org.w3.clearkey') {
  654. // The current EME version of clearkey wants a structured JSON response.
  655. // The v0.1b version wants just a raw key. Parse the JSON response and
  656. // extract the key and key ID.
  657. const StringUtils = shaka.util.StringUtils;
  658. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  659. const licenseString = StringUtils.fromUTF8(response);
  660. const jwkSet = /** @type {JWKSet} */ (JSON.parse(licenseString));
  661. const kty = jwkSet.keys[0].kty;
  662. if (kty != 'oct') {
  663. // Reject the promise.
  664. this.updatePromise_.reject(new Error(
  665. 'Response is not a valid JSON Web Key Set.'));
  666. this.updatePromise_ = null;
  667. }
  668. key = Uint8ArrayUtils.fromBase64(jwkSet.keys[0].k);
  669. keyId = Uint8ArrayUtils.fromBase64(jwkSet.keys[0].kid);
  670. } else {
  671. // The key ID is not required.
  672. key = shaka.util.BufferUtils.toUint8(response);
  673. keyId = null;
  674. }
  675. const addKeyName = PatchedMediaKeysWebkit.prefixApi_('addKey');
  676. try {
  677. this.media_[addKeyName](this.keySystem_, key, keyId, this.sessionId);
  678. } catch (exception) {
  679. // Reject the promise.
  680. this.updatePromise_.reject(exception);
  681. this.updatePromise_ = null;
  682. }
  683. }
  684. /**
  685. * Update key status and dispatch a 'keystatuseschange' event.
  686. *
  687. * @param {string} status
  688. * @private
  689. */
  690. updateKeyStatus_(status) {
  691. this.keyStatuses.setStatus(status);
  692. const event = new shaka.util.FakeEvent('keystatuseschange');
  693. this.dispatchEvent(event);
  694. }
  695. /** @override */
  696. generateRequest(initDataType, initData) {
  697. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.generateRequest');
  698. return this.generate_(initData, null);
  699. }
  700. /** @override */
  701. load(sessionId) {
  702. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.load');
  703. if (this.type_ == 'persistent-license') {
  704. return this.generate_(null, sessionId);
  705. } else {
  706. return Promise.reject(new Error('Not a persistent session.'));
  707. }
  708. }
  709. /** @override */
  710. update(response) {
  711. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.update', response);
  712. goog.asserts.assert(this.sessionId, 'update without session ID');
  713. const nextUpdatePromise = new shaka.util.PublicPromise();
  714. this.update_(nextUpdatePromise, response);
  715. return nextUpdatePromise;
  716. }
  717. /** @override */
  718. close() {
  719. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  720. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.close');
  721. // This will remove a persistent session, but it's also the only way to free
  722. // CDM resources on v0.1b.
  723. if (this.type_ != 'persistent-license') {
  724. // sessionId may reasonably be null if no key request has been generated
  725. // yet. Unprefixed EME will return a rejected promise in this case. We
  726. // will use the same error message that Chrome 41 uses in its EME
  727. // implementation.
  728. if (!this.sessionId) {
  729. this.closed.reject(new Error('The session is not callable.'));
  730. return this.closed;
  731. }
  732. // This may throw an exception, but we ignore it because we are only using
  733. // it to clean up resources in v0.1b. We still consider the session
  734. // closed. We can't let the exception propagate because
  735. // MediaKeySession.close() should not throw.
  736. const cancelKeyRequestName =
  737. PatchedMediaKeysWebkit.prefixApi_('cancelKeyRequest');
  738. try {
  739. this.media_[cancelKeyRequestName](this.keySystem_, this.sessionId);
  740. } catch (exception) {}
  741. }
  742. // Resolve the 'closed' promise and return it.
  743. this.closed.resolve();
  744. return this.closed;
  745. }
  746. /** @override */
  747. remove() {
  748. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.remove');
  749. if (this.type_ != 'persistent-license') {
  750. return Promise.reject(new Error('Not a persistent session.'));
  751. }
  752. return this.close();
  753. }
  754. };
  755. /**
  756. * An implementation of MediaKeyStatusMap.
  757. * This fakes a map with a single key ID.
  758. *
  759. * @todo Consolidate the MediaKeyStatusMap types in these polyfills.
  760. * @implements {MediaKeyStatusMap}
  761. */
  762. shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap = class {
  763. /** */
  764. constructor() {
  765. /**
  766. * @type {number}
  767. */
  768. this.size = 0;
  769. /**
  770. * @private {string|undefined}
  771. */
  772. this.status_ = undefined;
  773. }
  774. /**
  775. * An internal method used by the session to set key status.
  776. * @param {string|undefined} status
  777. */
  778. setStatus(status) {
  779. this.size = status == undefined ? 0 : 1;
  780. this.status_ = status;
  781. }
  782. /**
  783. * An internal method used by the session to get key status.
  784. * @return {string|undefined}
  785. */
  786. getStatus() {
  787. return this.status_;
  788. }
  789. /** @override */
  790. forEach(fn) {
  791. if (this.status_) {
  792. fn(this.status_, shaka.media.DrmEngine.DUMMY_KEY_ID.value());
  793. }
  794. }
  795. /** @override */
  796. get(keyId) {
  797. if (this.has(keyId)) {
  798. return this.status_;
  799. }
  800. return undefined;
  801. }
  802. /** @override */
  803. has(keyId) {
  804. const fakeKeyId = shaka.media.DrmEngine.DUMMY_KEY_ID.value();
  805. if (this.status_ && shaka.util.BufferUtils.equal(keyId, fakeKeyId)) {
  806. return true;
  807. }
  808. return false;
  809. }
  810. /**
  811. * @suppress {missingReturn}
  812. * @override
  813. */
  814. entries() {
  815. goog.asserts.assert(false, 'Not used! Provided only for compiler.');
  816. }
  817. /**
  818. * @suppress {missingReturn}
  819. * @override
  820. */
  821. keys() {
  822. goog.asserts.assert(false, 'Not used! Provided only for compiler.');
  823. }
  824. /**
  825. * @suppress {missingReturn}
  826. * @override
  827. */
  828. values() {
  829. goog.asserts.assert(false, 'Not used! Provided only for compiler.');
  830. }
  831. };
  832. /**
  833. * Store api prefix.
  834. *
  835. * @private {string}
  836. */
  837. shaka.polyfill.PatchedMediaKeysWebkit.prefix_ = '';
  838. shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysWebkit.install);