Source: lib/polyfill/patchedmediakeys_webkit.js

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