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