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