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.polyfill');
  10. goog.require('shaka.util.BufferUtils');
  11. goog.require('shaka.util.DrmUtils');
  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 = PatchedMediaKeysWebkit.apiName_;
  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. /** @override */
  365. getStatusForPolicy(policy) {
  366. return Promise.resolve('usable');
  367. }
  368. /**
  369. * @param {!MediaKeyEvent} event
  370. * @suppress {constantProperty} We reassign what would be const on a real
  371. * MediaEncryptedEvent, but in our look-alike event.
  372. * @private
  373. */
  374. onWebkitNeedKey_(event) {
  375. shaka.log.debug('PatchedMediaKeysWebkit.onWebkitNeedKey_', event);
  376. goog.asserts.assert(this.media_, 'media_ not set in onWebkitNeedKey_');
  377. const event2 = new CustomEvent('encrypted');
  378. const encryptedEvent =
  379. /** @type {!MediaEncryptedEvent} */(/** @type {?} */(event2));
  380. // initDataType is not used by v0.1b EME, so any valid value is fine here.
  381. encryptedEvent.initDataType = 'cenc';
  382. encryptedEvent.initData = shaka.util.BufferUtils.toArrayBuffer(
  383. event.initData);
  384. this.media_.dispatchEvent(event2);
  385. }
  386. /**
  387. * @param {!MediaKeyEvent} event
  388. * @private
  389. */
  390. onWebkitKeyMessage_(event) {
  391. shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyMessage_', event);
  392. const session = this.findSession_(event.sessionId);
  393. if (!session) {
  394. shaka.log.error('Session not found', event.sessionId);
  395. return;
  396. }
  397. const isNew = session.keyStatuses.getStatus() == undefined;
  398. const data = new Map()
  399. .set('messageType', isNew ? 'licenserequest' : 'licenserenewal')
  400. .set('message', event.message);
  401. const event2 = new shaka.util.FakeEvent('message', data);
  402. session.generated();
  403. session.dispatchEvent(event2);
  404. }
  405. /**
  406. * @param {!MediaKeyEvent} event
  407. * @private
  408. */
  409. onWebkitKeyAdded_(event) {
  410. shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyAdded_', event);
  411. const session = this.findSession_(event.sessionId);
  412. goog.asserts.assert(
  413. session, 'unable to find session in onWebkitKeyAdded_');
  414. if (session) {
  415. session.ready();
  416. }
  417. }
  418. /**
  419. * @param {!MediaKeyEvent} event
  420. * @private
  421. */
  422. onWebkitKeyError_(event) {
  423. shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyError_', event);
  424. const session = this.findSession_(event.sessionId);
  425. goog.asserts.assert(
  426. session, 'unable to find session in onWebkitKeyError_');
  427. if (session) {
  428. session.handleError(event);
  429. }
  430. }
  431. /**
  432. * @param {string} sessionId
  433. * @return {shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession}
  434. * @private
  435. */
  436. findSession_(sessionId) {
  437. let session = this.sessionMap_.get(sessionId);
  438. if (session) {
  439. shaka.log.debug(
  440. 'PatchedMediaKeysWebkit.MediaKeys.findSession_', session);
  441. return session;
  442. }
  443. session = this.newSessions_.shift();
  444. if (session) {
  445. session.sessionId = sessionId;
  446. this.sessionMap_.set(sessionId, session);
  447. shaka.log.debug(
  448. 'PatchedMediaKeysWebkit.MediaKeys.findSession_', session);
  449. return session;
  450. }
  451. return null;
  452. }
  453. };
  454. /**
  455. * An implementation of MediaKeySession.
  456. *
  457. * @implements {MediaKeySession}
  458. */
  459. shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession =
  460. class extends shaka.util.FakeEventTarget {
  461. /**
  462. * @param {!HTMLMediaElement} media
  463. * @param {string} keySystem
  464. * @param {string} sessionType
  465. */
  466. constructor(media, keySystem, sessionType) {
  467. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession');
  468. super();
  469. /** @private {!HTMLMediaElement} */
  470. this.media_ = media;
  471. /** @private {boolean} */
  472. this.initialized_ = false;
  473. /** @private {shaka.util.PublicPromise} */
  474. this.generatePromise_ = null;
  475. /** @private {shaka.util.PublicPromise} */
  476. this.updatePromise_ = null;
  477. /** @private {string} */
  478. this.keySystem_ = keySystem;
  479. /** @private {string} */
  480. this.type_ = sessionType;
  481. /** @type {string} */
  482. this.sessionId = '';
  483. /** @type {number} */
  484. this.expiration = NaN;
  485. /** @type {!shaka.util.PublicPromise} */
  486. this.closed = new shaka.util.PublicPromise();
  487. /** @type {!shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap} */
  488. this.keyStatuses =
  489. new shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap();
  490. }
  491. /**
  492. * Signals that the license request has been generated. This resolves the
  493. * 'generateRequest' promise.
  494. *
  495. * @protected
  496. */
  497. generated() {
  498. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.generated');
  499. if (this.generatePromise_) {
  500. this.generatePromise_.resolve();
  501. this.generatePromise_ = null;
  502. }
  503. }
  504. /**
  505. * Signals that the session is 'ready', which is the terminology used in older
  506. * versions of EME. The new signal is to resolve the 'update' promise. This
  507. * translates between the two.
  508. *
  509. * @protected
  510. */
  511. ready() {
  512. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.ready');
  513. this.updateKeyStatus_('usable');
  514. if (this.updatePromise_) {
  515. this.updatePromise_.resolve();
  516. }
  517. this.updatePromise_ = null;
  518. }
  519. /**
  520. * Either rejects a promise, or dispatches an error event, as appropriate.
  521. *
  522. * @param {!MediaKeyEvent} event
  523. */
  524. handleError(event) {
  525. shaka.log.debug(
  526. 'PatchedMediaKeysWebkit.MediaKeySession.handleError', event);
  527. // This does not match the DOMException we get in current WD EME, but it
  528. // will at least provide some information which can be used to look into the
  529. // problem.
  530. const error = new Error('EME v0.1b key error');
  531. const errorCode = event.errorCode;
  532. errorCode.systemCode = event.systemCode;
  533. error['errorCode'] = errorCode;
  534. // The presence or absence of sessionId indicates whether this corresponds
  535. // to generateRequest() or update().
  536. if (!event.sessionId && this.generatePromise_) {
  537. if (event.systemCode == 45) {
  538. error.message = 'Unsupported session type.';
  539. }
  540. this.generatePromise_.reject(error);
  541. this.generatePromise_ = null;
  542. } else if (event.sessionId && this.updatePromise_) {
  543. this.updatePromise_.reject(error);
  544. this.updatePromise_ = null;
  545. } else {
  546. // This mapping of key statuses is imperfect at best.
  547. const code = event.errorCode.code;
  548. const systemCode = event.systemCode;
  549. if (code == MediaKeyError['MEDIA_KEYERR_OUTPUT']) {
  550. this.updateKeyStatus_('output-restricted');
  551. } else if (systemCode == 1) {
  552. this.updateKeyStatus_('expired');
  553. } else {
  554. this.updateKeyStatus_('internal-error');
  555. }
  556. }
  557. }
  558. /**
  559. * Logic which is shared between generateRequest() and load(), both of which
  560. * are ultimately implemented with webkitGenerateKeyRequest in prefixed EME.
  561. *
  562. * @param {?BufferSource} initData
  563. * @param {?string} offlineSessionId
  564. * @return {!Promise}
  565. * @private
  566. */
  567. generate_(initData, offlineSessionId) {
  568. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  569. if (this.initialized_) {
  570. const error = new Error('The session is already initialized.');
  571. return Promise.reject(error);
  572. }
  573. this.initialized_ = true;
  574. /** @type {!Uint8Array} */
  575. let mangledInitData;
  576. try {
  577. if (this.type_ == 'persistent-license') {
  578. const StringUtils = shaka.util.StringUtils;
  579. if (!offlineSessionId) {
  580. goog.asserts.assert(initData, 'expecting init data');
  581. // Persisting the initial license.
  582. // Prefix the init data with a tag to indicate persistence.
  583. const prefix = StringUtils.toUTF8('PERSISTENT|');
  584. mangledInitData = shaka.util.Uint8ArrayUtils.concat(prefix, initData);
  585. } else {
  586. // Loading a stored license.
  587. // Prefix the init data (which is really a session ID) with a tag to
  588. // indicate that we are loading a persisted session.
  589. mangledInitData = shaka.util.BufferUtils.toUint8(
  590. StringUtils.toUTF8('LOAD_SESSION|' + offlineSessionId));
  591. }
  592. } else {
  593. // Streaming.
  594. goog.asserts.assert(this.type_ == 'temporary',
  595. 'expected temporary session');
  596. goog.asserts.assert(!offlineSessionId,
  597. 'unexpected offline session ID');
  598. goog.asserts.assert(initData, 'expecting init data');
  599. mangledInitData = shaka.util.BufferUtils.toUint8(initData);
  600. }
  601. goog.asserts.assert(mangledInitData, 'init data not set!');
  602. } catch (exception) {
  603. return Promise.reject(exception);
  604. }
  605. goog.asserts.assert(this.generatePromise_ == null,
  606. 'generatePromise_ should be null');
  607. this.generatePromise_ = new shaka.util.PublicPromise();
  608. // Because we are hacking media.src in createSession to better emulate
  609. // unprefixed EME's ability to create sessions and license requests without
  610. // a video tag, we can get ourselves into trouble. It seems that sometimes,
  611. // the setting of media.src hasn't been processed by some other thread, and
  612. // GKR can throw an exception. If this occurs, wait 10 ms and try again at
  613. // most once. This situation should only occur when init data is available
  614. // ahead of the 'needkey' event.
  615. const generateKeyRequestName =
  616. PatchedMediaKeysWebkit.prefixApi_('generateKeyRequest');
  617. try {
  618. this.media_[generateKeyRequestName](this.keySystem_, mangledInitData);
  619. } catch (exception) {
  620. if (exception.name != 'InvalidStateError') {
  621. this.generatePromise_ = null;
  622. return Promise.reject(exception);
  623. }
  624. const timer = new shaka.util.Timer(() => {
  625. try {
  626. this.media_[generateKeyRequestName](this.keySystem_, mangledInitData);
  627. } catch (exception2) {
  628. this.generatePromise_.reject(exception2);
  629. this.generatePromise_ = null;
  630. }
  631. });
  632. timer.tickAfter(/* seconds= */ 0.01);
  633. }
  634. return this.generatePromise_;
  635. }
  636. /**
  637. * An internal version of update which defers new calls while old ones are in
  638. * progress.
  639. *
  640. * @param {!shaka.util.PublicPromise} promise The promise associated with
  641. * this call.
  642. * @param {BufferSource} response
  643. * @private
  644. */
  645. update_(promise, response) {
  646. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  647. if (this.updatePromise_) {
  648. // We already have an update in-progress, so defer this one until after
  649. // the old one is resolved. Execute this whether the original one
  650. // succeeds or fails.
  651. this.updatePromise_.then(() => this.update_(promise, response))
  652. .catch(() => this.update_(promise, response));
  653. return;
  654. }
  655. this.updatePromise_ = promise;
  656. let key;
  657. let keyId;
  658. if (this.keySystem_ == 'webkit-org.w3.clearkey') {
  659. // The current EME version of clearkey wants a structured JSON response.
  660. // The v0.1b version wants just a raw key. Parse the JSON response and
  661. // extract the key and key ID.
  662. const StringUtils = shaka.util.StringUtils;
  663. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  664. const licenseString = StringUtils.fromUTF8(response);
  665. const jwkSet = /** @type {JWKSet} */ (JSON.parse(licenseString));
  666. const kty = jwkSet.keys[0].kty;
  667. if (kty != 'oct') {
  668. // Reject the promise.
  669. this.updatePromise_.reject(new Error(
  670. 'Response is not a valid JSON Web Key Set.'));
  671. this.updatePromise_ = null;
  672. }
  673. key = Uint8ArrayUtils.fromBase64(jwkSet.keys[0].k);
  674. keyId = Uint8ArrayUtils.fromBase64(jwkSet.keys[0].kid);
  675. } else {
  676. // The key ID is not required.
  677. key = shaka.util.BufferUtils.toUint8(response);
  678. keyId = null;
  679. }
  680. const addKeyName = PatchedMediaKeysWebkit.prefixApi_('addKey');
  681. try {
  682. this.media_[addKeyName](this.keySystem_, key, keyId, this.sessionId);
  683. } catch (exception) {
  684. // Reject the promise.
  685. this.updatePromise_.reject(exception);
  686. this.updatePromise_ = null;
  687. }
  688. }
  689. /**
  690. * Update key status and dispatch a 'keystatuseschange' event.
  691. *
  692. * @param {string} status
  693. * @private
  694. */
  695. updateKeyStatus_(status) {
  696. this.keyStatuses.setStatus(status);
  697. const event = new shaka.util.FakeEvent('keystatuseschange');
  698. this.dispatchEvent(event);
  699. }
  700. /** @override */
  701. generateRequest(initDataType, initData) {
  702. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.generateRequest');
  703. return this.generate_(initData, null);
  704. }
  705. /** @override */
  706. load(sessionId) {
  707. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.load');
  708. if (this.type_ == 'persistent-license') {
  709. return this.generate_(null, sessionId);
  710. } else {
  711. return Promise.reject(new Error('Not a persistent session.'));
  712. }
  713. }
  714. /** @override */
  715. update(response) {
  716. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.update', response);
  717. goog.asserts.assert(this.sessionId, 'update without session ID');
  718. const nextUpdatePromise = new shaka.util.PublicPromise();
  719. this.update_(nextUpdatePromise, response);
  720. return nextUpdatePromise;
  721. }
  722. /** @override */
  723. close() {
  724. const PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
  725. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.close');
  726. // This will remove a persistent session, but it's also the only way to free
  727. // CDM resources on v0.1b.
  728. if (this.type_ != 'persistent-license') {
  729. // sessionId may reasonably be null if no key request has been generated
  730. // yet. Unprefixed EME will return a rejected promise in this case. We
  731. // will use the same error message that Chrome 41 uses in its EME
  732. // implementation.
  733. if (!this.sessionId) {
  734. this.closed.reject(new Error('The session is not callable.'));
  735. return this.closed;
  736. }
  737. // This may throw an exception, but we ignore it because we are only using
  738. // it to clean up resources in v0.1b. We still consider the session
  739. // closed. We can't let the exception propagate because
  740. // MediaKeySession.close() should not throw.
  741. const cancelKeyRequestName =
  742. PatchedMediaKeysWebkit.prefixApi_('cancelKeyRequest');
  743. try {
  744. this.media_[cancelKeyRequestName](this.keySystem_, this.sessionId);
  745. } catch (exception) {
  746. shaka.log.debug(`${cancelKeyRequestName} exception`, exception);
  747. }
  748. }
  749. // Resolve the 'closed' promise and return it.
  750. this.closed.resolve();
  751. return this.closed;
  752. }
  753. /** @override */
  754. remove() {
  755. shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.remove');
  756. if (this.type_ != 'persistent-license') {
  757. return Promise.reject(new Error('Not a persistent session.'));
  758. }
  759. return this.close();
  760. }
  761. };
  762. /**
  763. * An implementation of MediaKeyStatusMap.
  764. * This fakes a map with a single key ID.
  765. *
  766. * @todo Consolidate the MediaKeyStatusMap types in these polyfills.
  767. * @implements {MediaKeyStatusMap}
  768. */
  769. shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap = class {
  770. /** */
  771. constructor() {
  772. /**
  773. * @type {number}
  774. */
  775. this.size = 0;
  776. /**
  777. * @private {string|undefined}
  778. */
  779. this.status_ = undefined;
  780. }
  781. /**
  782. * An internal method used by the session to set key status.
  783. * @param {string|undefined} status
  784. */
  785. setStatus(status) {
  786. this.size = status == undefined ? 0 : 1;
  787. this.status_ = status;
  788. }
  789. /**
  790. * An internal method used by the session to get key status.
  791. * @return {string|undefined}
  792. */
  793. getStatus() {
  794. return this.status_;
  795. }
  796. /** @override */
  797. forEach(fn) {
  798. if (this.status_) {
  799. fn(this.status_, shaka.util.DrmUtils.DUMMY_KEY_ID.value());
  800. }
  801. }
  802. /** @override */
  803. get(keyId) {
  804. if (this.has(keyId)) {
  805. return this.status_;
  806. }
  807. return undefined;
  808. }
  809. /** @override */
  810. has(keyId) {
  811. const fakeKeyId = shaka.util.DrmUtils.DUMMY_KEY_ID.value();
  812. if (this.status_ && shaka.util.BufferUtils.equal(keyId, fakeKeyId)) {
  813. return true;
  814. }
  815. return false;
  816. }
  817. /**
  818. * @suppress {missingReturn}
  819. * @override
  820. */
  821. entries() {
  822. goog.asserts.assert(false, 'Not used! Provided only for compiler.');
  823. }
  824. /**
  825. * @suppress {missingReturn}
  826. * @override
  827. */
  828. keys() {
  829. goog.asserts.assert(false, 'Not used! Provided only for compiler.');
  830. }
  831. /**
  832. * @suppress {missingReturn}
  833. * @override
  834. */
  835. values() {
  836. goog.asserts.assert(false, 'Not used! Provided only for compiler.');
  837. }
  838. };
  839. /**
  840. * Store api prefix.
  841. *
  842. * @private {string}
  843. */
  844. shaka.polyfill.PatchedMediaKeysWebkit.prefix_ = '';
  845. /**
  846. * API name.
  847. *
  848. * @private {string}
  849. */
  850. shaka.polyfill.PatchedMediaKeysWebkit.apiName_ = 'webkit';
  851. shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysWebkit.install);