Source: lib/polyfill/patchedmediakeys_ms.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.PatchedMediaKeysMs');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.polyfill.register');
  21. goog.require('shaka.util.ArrayUtils');
  22. goog.require('shaka.util.EventManager');
  23. goog.require('shaka.util.FakeEvent');
  24. goog.require('shaka.util.FakeEventTarget');
  25. goog.require('shaka.util.Pssh');
  26. goog.require('shaka.util.PublicPromise');
  27. goog.require('shaka.util.Uint8ArrayUtils');
  28. /**
  29. * @namespace shaka.polyfill.PatchedMediaKeysMs
  30. *
  31. * @summary A polyfill to implement
  32. * {@link http://goo.gl/blgtZZ EME draft 12 March 2015}
  33. * on top of ms-prefixed
  34. * {@link http://www.w3.org/TR/2014/WD-encrypted-media-20140218/ EME v20140218}.
  35. */
  36. /**
  37. * Installs the polyfill if needed.
  38. */
  39. shaka.polyfill.PatchedMediaKeysMs.install = function() {
  40. if (!window.HTMLVideoElement || !window.MSMediaKeys ||
  41. (navigator.requestMediaKeySystemAccess &&
  42. MediaKeySystemAccess.prototype.getConfiguration)) {
  43. return;
  44. }
  45. shaka.log.info('Using ms-prefixed EME v20140218');
  46. // Alias
  47. const PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
  48. // Construct a 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. PatchedMediaKeysMs.MediaKeyStatusMap.KEY_ID_ = (new Uint8Array([0])).buffer;
  52. // Delete mediaKeys to work around strict mode compatibility issues.
  53. delete HTMLMediaElement.prototype['mediaKeys'];
  54. // Work around read-only declaration for mediaKeys by using a string.
  55. HTMLMediaElement.prototype['mediaKeys'] = null;
  56. HTMLMediaElement.prototype.setMediaKeys = PatchedMediaKeysMs.setMediaKeys;
  57. // Install patches
  58. window.MediaKeys = PatchedMediaKeysMs.MediaKeys;
  59. window.MediaKeySystemAccess = PatchedMediaKeysMs.MediaKeySystemAccess;
  60. navigator.requestMediaKeySystemAccess =
  61. PatchedMediaKeysMs.requestMediaKeySystemAccess;
  62. };
  63. /**
  64. * An implementation of navigator.requestMediaKeySystemAccess.
  65. * Retrieves a MediaKeySystemAccess object.
  66. *
  67. * @this {!Navigator}
  68. * @param {string} keySystem
  69. * @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
  70. * @return {!Promise.<!MediaKeySystemAccess>}
  71. */
  72. shaka.polyfill.PatchedMediaKeysMs.requestMediaKeySystemAccess =
  73. function(keySystem, supportedConfigurations) {
  74. shaka.log.debug('PatchedMediaKeysMs.requestMediaKeySystemAccess');
  75. goog.asserts.assert(this == navigator,
  76. 'bad "this" for requestMediaKeySystemAccess');
  77. // Alias.
  78. const PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
  79. try {
  80. let access = new PatchedMediaKeysMs.MediaKeySystemAccess(
  81. keySystem, supportedConfigurations);
  82. return Promise.resolve(/** @type {!MediaKeySystemAccess} */ (access));
  83. } catch (exception) {
  84. return Promise.reject(exception);
  85. }
  86. };
  87. /**
  88. * An implementation of MediaKeySystemAccess.
  89. *
  90. * @constructor
  91. * @struct
  92. * @param {string} keySystem
  93. * @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
  94. * @implements {MediaKeySystemAccess}
  95. * @throws {Error} if the key system is not supported.
  96. */
  97. shaka.polyfill.PatchedMediaKeysMs.MediaKeySystemAccess =
  98. function(keySystem, supportedConfigurations) {
  99. shaka.log.debug('PatchedMediaKeysMs.MediaKeySystemAccess');
  100. /** @type {string} */
  101. this.keySystem = keySystem;
  102. /** @private {!MediaKeySystemConfiguration} */
  103. this.configuration_;
  104. let allowPersistentState = false;
  105. let success = false;
  106. for (let i = 0; i < supportedConfigurations.length; ++i) {
  107. let cfg = supportedConfigurations[i];
  108. // Create a new config object and start adding in the pieces which we
  109. // find support for. We will return this from getConfiguration() if asked.
  110. /** @type {!MediaKeySystemConfiguration} */
  111. let newCfg = {
  112. 'audioCapabilities': [],
  113. 'videoCapabilities': [],
  114. // It is technically against spec to return these as optional, but we
  115. // don't truly know their values from the prefixed API:
  116. 'persistentState': 'optional',
  117. 'distinctiveIdentifier': 'optional',
  118. // Pretend the requested init data types are supported, since we don't
  119. // really know that either:
  120. 'initDataTypes': cfg.initDataTypes,
  121. 'sessionTypes': ['temporary'],
  122. 'label': cfg.label
  123. };
  124. // PatchedMediaKeysMs tests for key system availability through
  125. // MSMediaKeys.isTypeSupported
  126. let ranAnyTests = false;
  127. if (cfg.audioCapabilities) {
  128. for (let j = 0; j < cfg.audioCapabilities.length; ++j) {
  129. let cap = cfg.audioCapabilities[j];
  130. if (cap.contentType) {
  131. ranAnyTests = true;
  132. let contentType = cap.contentType.split(';')[0];
  133. if (MSMediaKeys.isTypeSupported(this.keySystem, contentType)) {
  134. newCfg.audioCapabilities.push(cap);
  135. success = true;
  136. }
  137. }
  138. }
  139. }
  140. if (cfg.videoCapabilities) {
  141. for (let j = 0; j < cfg.videoCapabilities.length; ++j) {
  142. let cap = cfg.videoCapabilities[j];
  143. if (cap.contentType) {
  144. ranAnyTests = true;
  145. let contentType = cap.contentType.split(';')[0];
  146. if (MSMediaKeys.isTypeSupported(this.keySystem, contentType)) {
  147. newCfg.videoCapabilities.push(cap);
  148. success = true;
  149. }
  150. }
  151. }
  152. }
  153. if (!ranAnyTests) {
  154. // If no specific types were requested, we check all common types to find
  155. // out if the key system is present at all.
  156. success = MSMediaKeys.isTypeSupported(this.keySystem, 'video/mp4');
  157. }
  158. if (cfg.persistentState == 'required') {
  159. if (allowPersistentState) {
  160. newCfg.persistentState = 'required';
  161. newCfg.sessionTypes = ['persistent-license'];
  162. } else {
  163. success = false;
  164. }
  165. }
  166. if (success) {
  167. this.configuration_ = newCfg;
  168. return;
  169. }
  170. } // for each cfg in supportedConfigurations
  171. // As per the spec, this should be a DOMException, but there is not a public
  172. // constructor for this.
  173. let unsupportedKeySystemError = new Error('Unsupported keySystem');
  174. unsupportedKeySystemError.name = 'NotSupportedError';
  175. unsupportedKeySystemError.code = DOMException.NOT_SUPPORTED_ERR;
  176. throw unsupportedKeySystemError;
  177. };
  178. /** @override */
  179. shaka.polyfill.PatchedMediaKeysMs.MediaKeySystemAccess.prototype.
  180. createMediaKeys = function() {
  181. shaka.log.debug('PatchedMediaKeysMs.MediaKeySystemAccess.createMediaKeys');
  182. // Alias
  183. const PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
  184. let mediaKeys = new PatchedMediaKeysMs.MediaKeys(this.keySystem);
  185. return Promise.resolve(/** @type {!MediaKeys} */ (mediaKeys));
  186. };
  187. /** @override */
  188. shaka.polyfill.PatchedMediaKeysMs.MediaKeySystemAccess.prototype.
  189. getConfiguration = function() {
  190. shaka.log.debug('PatchedMediaKeysMs.MediaKeySystemAccess.getConfiguration');
  191. return this.configuration_;
  192. };
  193. /**
  194. * An implementation of HTMLMediaElement.prototype.setMediaKeys.
  195. * Attaches a MediaKeys object to the media element.
  196. *
  197. * @this {!HTMLMediaElement}
  198. * @param {MediaKeys} mediaKeys
  199. * @return {!Promise}
  200. */
  201. shaka.polyfill.PatchedMediaKeysMs.setMediaKeys = function(mediaKeys) {
  202. shaka.log.debug('PatchedMediaKeysMs.setMediaKeys');
  203. goog.asserts.assert(this instanceof HTMLMediaElement,
  204. 'bad "this" for setMediaKeys');
  205. // Alias
  206. const PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
  207. let newMediaKeys =
  208. /** @type {shaka.polyfill.PatchedMediaKeysMs.MediaKeys} */ (
  209. mediaKeys);
  210. let oldMediaKeys =
  211. /** @type {shaka.polyfill.PatchedMediaKeysMs.MediaKeys} */ (
  212. this.mediaKeys);
  213. if (oldMediaKeys && oldMediaKeys != newMediaKeys) {
  214. goog.asserts.assert(oldMediaKeys instanceof PatchedMediaKeysMs.MediaKeys,
  215. 'non-polyfill instance of oldMediaKeys');
  216. // Have the old MediaKeys stop listening to events on the video tag.
  217. oldMediaKeys.setMedia(null);
  218. }
  219. delete this['mediaKeys']; // in case there is an existing getter
  220. this['mediaKeys'] = mediaKeys; // work around read-only declaration
  221. if (newMediaKeys) {
  222. goog.asserts.assert(newMediaKeys instanceof PatchedMediaKeysMs.MediaKeys,
  223. 'non-polyfill instance of newMediaKeys');
  224. return newMediaKeys.setMedia(this);
  225. }
  226. return Promise.resolve();
  227. };
  228. /**
  229. * An implementation of MediaKeys.
  230. *
  231. * @constructor
  232. * @struct
  233. * @param {string} keySystem
  234. * @implements {MediaKeys}
  235. */
  236. shaka.polyfill.PatchedMediaKeysMs.MediaKeys = function(keySystem) {
  237. shaka.log.debug('PatchedMediaKeysMs.MediaKeys');
  238. /** @private {!MSMediaKeys} */
  239. this.nativeMediaKeys_ = new MSMediaKeys(keySystem);
  240. /** @private {!shaka.util.EventManager} */
  241. this.eventManager_ = new shaka.util.EventManager();
  242. };
  243. /** @override */
  244. shaka.polyfill.PatchedMediaKeysMs.MediaKeys.prototype.
  245. createSession = function(opt_sessionType) {
  246. shaka.log.debug('PatchedMediaKeysMs.MediaKeys.createSession');
  247. let sessionType = opt_sessionType || 'temporary';
  248. // For now, only the 'temporary' type is supported.
  249. if (sessionType != 'temporary') {
  250. throw new TypeError('Session type ' + opt_sessionType +
  251. ' is unsupported on this platform.');
  252. }
  253. // Alias
  254. const PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
  255. return new PatchedMediaKeysMs.MediaKeySession(
  256. this.nativeMediaKeys_, sessionType);
  257. };
  258. /** @override */
  259. shaka.polyfill.PatchedMediaKeysMs.MediaKeys.prototype.
  260. setServerCertificate = function(serverCertificate) {
  261. shaka.log.debug('PatchedMediaKeysMs.MediaKeys.setServerCertificate');
  262. // There is no equivalent in PatchedMediaKeysMs, so return failure.
  263. return Promise.resolve(false);
  264. };
  265. /**
  266. * @param {HTMLMediaElement} media
  267. * @protected
  268. * @return {!Promise}
  269. */
  270. shaka.polyfill.PatchedMediaKeysMs.MediaKeys.prototype.
  271. setMedia = function(media) {
  272. // Alias
  273. const PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
  274. // Remove any old listeners.
  275. this.eventManager_.removeAll();
  276. // It is valid for media to be null; null is used to flag that event handlers
  277. // need to be cleaned up.
  278. if (!media) {
  279. return Promise.resolve();
  280. }
  281. // Intercept and translate these prefixed EME events.
  282. this.eventManager_.listen(media, 'msneedkey',
  283. /** @type {shaka.util.EventManager.ListenerType} */
  284. (PatchedMediaKeysMs.onMsNeedKey_));
  285. let self = this;
  286. function setMediaKeysDeferred() {
  287. media.msSetMediaKeys(self.nativeMediaKeys_);
  288. media.removeEventListener('loadedmetadata', setMediaKeysDeferred);
  289. }
  290. // Wrap native HTMLMediaElement.msSetMediaKeys with a Promise.
  291. try {
  292. // IE11/Edge requires that readyState >=1 before mediaKeys can be set, so
  293. // check this and wait for loadedmetadata if we are not in the correct state
  294. if (media.readyState >= 1) {
  295. media.msSetMediaKeys(this.nativeMediaKeys_);
  296. } else {
  297. media.addEventListener('loadedmetadata', setMediaKeysDeferred);
  298. }
  299. return Promise.resolve();
  300. } catch (exception) {
  301. return Promise.reject(exception);
  302. }
  303. };
  304. /**
  305. * An implementation of MediaKeySession.
  306. *
  307. * @constructor
  308. * @struct
  309. * @param {MSMediaKeys} nativeMediaKeys
  310. * @param {string} sessionType
  311. * @implements {MediaKeySession}
  312. * @extends {shaka.util.FakeEventTarget}
  313. */
  314. shaka.polyfill.PatchedMediaKeysMs.
  315. MediaKeySession = function(nativeMediaKeys, sessionType) {
  316. shaka.log.debug('PatchedMediaKeysMs.MediaKeySession');
  317. shaka.util.FakeEventTarget.call(this);
  318. // The native MediaKeySession, which will be created in generateRequest.
  319. /** @private {MSMediaKeySession} */
  320. this.nativeMediaKeySession_ = null;
  321. /** @private {MSMediaKeys} */
  322. this.nativeMediaKeys_ = nativeMediaKeys;
  323. // Promises that are resolved later
  324. /** @private {shaka.util.PublicPromise} */
  325. this.generateRequestPromise_ = null;
  326. /** @private {shaka.util.PublicPromise} */
  327. this.updatePromise_ = null;
  328. /** @private {!shaka.util.EventManager} */
  329. this.eventManager_ = new shaka.util.EventManager();
  330. /** @type {string} */
  331. this.sessionId = '';
  332. /** @type {number} */
  333. this.expiration = NaN;
  334. /** @type {!shaka.util.PublicPromise} */
  335. this.closed = new shaka.util.PublicPromise();
  336. /** @type {!shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap} */
  337. this.keyStatuses =
  338. new shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap();
  339. };
  340. goog.inherits(shaka.polyfill.PatchedMediaKeysMs.MediaKeySession,
  341. shaka.util.FakeEventTarget);
  342. /** @override */
  343. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  344. generateRequest = function(initDataType, initData) {
  345. shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.generateRequest');
  346. this.generateRequestPromise_ = new shaka.util.PublicPromise();
  347. try {
  348. // This EME spec version requires a MIME content type as the 1st param
  349. // to createSession, but doesn't seem to matter what the value is.
  350. // NOTE: IE11 takes either Uint8Array or ArrayBuffer, but Edge 12 only
  351. // accepts Uint8Array.
  352. this.nativeMediaKeySession_ = this.nativeMediaKeys_
  353. .createSession('video/mp4', new Uint8Array(initData), null);
  354. // Attach session event handlers here.
  355. this.eventManager_.listen(this.nativeMediaKeySession_, 'mskeymessage',
  356. /** @type {shaka.util.EventManager.ListenerType} */
  357. (this.onMsKeyMessage_.bind(this)));
  358. this.eventManager_.listen(this.nativeMediaKeySession_, 'mskeyadded',
  359. /** @type {shaka.util.EventManager.ListenerType} */
  360. (this.onMsKeyAdded_.bind(this)));
  361. this.eventManager_.listen(this.nativeMediaKeySession_, 'mskeyerror',
  362. /** @type {shaka.util.EventManager.ListenerType} */
  363. (this.onMsKeyError_.bind(this)));
  364. this.updateKeyStatus_('status-pending');
  365. } catch (exception) {
  366. this.generateRequestPromise_.reject(exception);
  367. }
  368. return this.generateRequestPromise_;
  369. };
  370. /** @override */
  371. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  372. load = function() {
  373. shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.load');
  374. return Promise.reject(new Error('MediaKeySession.load not yet supported'));
  375. };
  376. /** @override */
  377. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  378. update = function(response) {
  379. shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.update');
  380. this.updatePromise_ = new shaka.util.PublicPromise();
  381. try {
  382. // Pass through to the native session.
  383. // NOTE: IE11 takes either Uint8Array or ArrayBuffer, but Edge 12 only
  384. // accepts Uint8Array.
  385. this.nativeMediaKeySession_.update(new Uint8Array(response));
  386. } catch (exception) {
  387. this.updatePromise_.reject(exception);
  388. }
  389. return this.updatePromise_;
  390. };
  391. /** @override */
  392. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  393. close = function() {
  394. shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.close');
  395. try {
  396. // Pass through to the native session.
  397. // NOTE: IE seems to have a spec discrepancy here - v2010218 should have
  398. // MediaKeySession.release, but actually uses "close". The next version
  399. // of the spec is the initial Promise based one, so it's not the target spec
  400. // either.
  401. this.nativeMediaKeySession_.close();
  402. this.closed.resolve();
  403. this.eventManager_.removeAll();
  404. } catch (exception) {
  405. this.closed.reject(exception);
  406. }
  407. return this.closed;
  408. };
  409. /** @override */
  410. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  411. remove = function() {
  412. shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.remove');
  413. return Promise.reject(new Error('MediaKeySession.remove is only ' +
  414. 'applicable for persistent licenses, which are not supported on ' +
  415. 'this platform'));
  416. };
  417. /**
  418. * Handler for the native media elements msNeedKey event.
  419. *
  420. * @this {!HTMLMediaElement}
  421. * @param {!MediaKeyEvent} event
  422. * @private
  423. */
  424. shaka.polyfill.PatchedMediaKeysMs.onMsNeedKey_ = function(event) {
  425. shaka.log.debug('PatchedMediaKeysMs.onMsNeedKey_', event);
  426. if (!event.initData) {
  427. return;
  428. }
  429. // Alias
  430. const PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
  431. // NOTE: Because "this" is a real EventTarget, on IE, the event we dispatch
  432. // here must also be a real Event.
  433. let event2 = /** @type {!CustomEvent} */(document.createEvent('CustomEvent'));
  434. event2.initCustomEvent('encrypted', false, false, null);
  435. event2.initDataType = 'cenc';
  436. event2.initData = PatchedMediaKeysMs.NormaliseInitData_(event.initData);
  437. this.dispatchEvent(event2);
  438. };
  439. /**
  440. * Normalise the initData array. This is to apply browser specific work-arounds,
  441. * e.g. removing duplicates which appears to occur intermittently when the
  442. * native msneedkey event fires (i.e. event.initData contains dupes).
  443. *
  444. * @param {?Uint8Array} initData
  445. * @private
  446. * @return {?Uint8Array}
  447. */
  448. shaka.polyfill.PatchedMediaKeysMs.
  449. NormaliseInitData_ = function(initData) {
  450. if (!initData) {
  451. return initData;
  452. }
  453. let pssh = new shaka.util.Pssh(initData);
  454. // If there is only a single pssh, return the original array.
  455. if (pssh.dataBoundaries.length <= 1) {
  456. return initData;
  457. }
  458. let unfilteredInitDatas = [];
  459. for (let i = 0; i < pssh.dataBoundaries.length; i++) {
  460. let currPssh = initData.subarray(
  461. pssh.dataBoundaries[i].start,
  462. pssh.dataBoundaries[i].end + 1); // End is exclusive, hence the +1.
  463. unfilteredInitDatas.push(currPssh);
  464. }
  465. // Dedupe psshData.
  466. let dedupedInitDatas = shaka.util.ArrayUtils.removeDuplicates(
  467. unfilteredInitDatas,
  468. shaka.polyfill.PatchedMediaKeysMs.compareInitDatas_);
  469. let targetLength = 0;
  470. for (let i = 0; i < dedupedInitDatas.length; i++) {
  471. targetLength += dedupedInitDatas[i].length;
  472. }
  473. // Flatten the array of Uint8Arrays back into a single Uint8Array.
  474. let normalisedInitData = new Uint8Array(targetLength);
  475. let offset = 0;
  476. for (let i = 0; i < dedupedInitDatas.length; i++) {
  477. normalisedInitData.set(dedupedInitDatas[i], offset);
  478. offset += dedupedInitDatas[i].length;
  479. }
  480. return normalisedInitData;
  481. };
  482. /**
  483. * @param {!Uint8Array} initDataA
  484. * @param {!Uint8Array} initDataB
  485. * @return {boolean}
  486. * @private
  487. */
  488. shaka.polyfill.PatchedMediaKeysMs.compareInitDatas_ =
  489. function(initDataA, initDataB) {
  490. return shaka.util.Uint8ArrayUtils.equal(initDataA, initDataB);
  491. };
  492. /**
  493. * Handler for the native keymessage event on MSMediaKeySession.
  494. *
  495. * @param {!MediaKeyEvent} event
  496. * @private
  497. */
  498. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  499. onMsKeyMessage_ = function(event) {
  500. shaka.log.debug('PatchedMediaKeysMs.onMsKeyMessage_', event);
  501. // We can now resolve this.generateRequestPromise, which should be non-null.
  502. goog.asserts.assert(this.generateRequestPromise_,
  503. 'generateRequestPromise_ not set in onMsKeyMessage_');
  504. if (this.generateRequestPromise_) {
  505. this.generateRequestPromise_.resolve();
  506. this.generateRequestPromise_ = null;
  507. }
  508. let isNew = this.keyStatuses.getStatus() == undefined;
  509. let event2 = new shaka.util.FakeEvent('message', {
  510. messageType: isNew ? 'license-request' : 'license-renewal',
  511. message: event.message.buffer,
  512. });
  513. this.dispatchEvent(event2);
  514. };
  515. /**
  516. * Handler for the native keyadded event on MSMediaKeySession.
  517. *
  518. * @param {!MediaKeyEvent} event
  519. * @private
  520. */
  521. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  522. onMsKeyAdded_ = function(event) {
  523. shaka.log.debug('PatchedMediaKeysMs.onMsKeyAdded_', event);
  524. // PlayReady's concept of persistent licenses makes emulation difficult here.
  525. // A license policy can say that the license persists, which causes the CDM to
  526. // store it for use in a later session. The result is that in IE11, the CDM
  527. // fires 'mskeyadded' without ever firing 'mskeymessage'.
  528. if (this.generateRequestPromise_) {
  529. shaka.log.debug('Simulating completion for a PR persistent license.');
  530. goog.asserts.assert(!this.updatePromise_,
  531. 'updatePromise_ and generateRequestPromise_ set in onMsKeyAdded_');
  532. this.updateKeyStatus_('usable');
  533. this.generateRequestPromise_.resolve();
  534. this.generateRequestPromise_ = null;
  535. return;
  536. }
  537. // We can now resolve this.updatePromise, which should be non-null.
  538. goog.asserts.assert(this.updatePromise_,
  539. 'updatePromise_ not set in onMsKeyAdded_');
  540. if (this.updatePromise_) {
  541. this.updateKeyStatus_('usable');
  542. this.updatePromise_.resolve();
  543. this.updatePromise_ = null;
  544. }
  545. };
  546. /**
  547. * Handler for the native keyerror event on MSMediaKeySession.
  548. *
  549. * @param {!MediaKeyEvent} event
  550. * @private
  551. */
  552. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  553. onMsKeyError_ = function(event) {
  554. shaka.log.debug('PatchedMediaKeysMs.onMsKeyError_', event);
  555. let error = new Error('EME PatchedMediaKeysMs key error');
  556. error.errorCode = this.nativeMediaKeySession_.error;
  557. if (this.generateRequestPromise_ != null) {
  558. this.generateRequestPromise_.reject(error);
  559. this.generateRequestPromise_ = null;
  560. } else if (this.updatePromise_ != null) {
  561. this.updatePromise_.reject(error);
  562. this.updatePromise_ = null;
  563. } else {
  564. // Unexpected error - map native codes to standardised key statuses.
  565. // Possible values of this.nativeMediaKeySession_.error.code:
  566. // MS_MEDIA_KEYERR_UNKNOWN = 1
  567. // MS_MEDIA_KEYERR_CLIENT = 2
  568. // MS_MEDIA_KEYERR_SERVICE = 3
  569. // MS_MEDIA_KEYERR_OUTPUT = 4
  570. // MS_MEDIA_KEYERR_HARDWARECHANGE = 5
  571. // MS_MEDIA_KEYERR_DOMAIN = 6
  572. switch (this.nativeMediaKeySession_.error.code) {
  573. case MSMediaKeyError.MS_MEDIA_KEYERR_OUTPUT:
  574. case MSMediaKeyError.MS_MEDIA_KEYERR_HARDWARECHANGE:
  575. this.updateKeyStatus_('output-not-allowed');
  576. break;
  577. default:
  578. this.updateKeyStatus_('internal-error');
  579. break;
  580. }
  581. }
  582. };
  583. /**
  584. * Updates key status and dispatch a 'keystatuseschange' event.
  585. *
  586. * @param {string} status
  587. * @private
  588. */
  589. shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
  590. updateKeyStatus_ = function(status) {
  591. this.keyStatuses.setStatus(status);
  592. let event = new shaka.util.FakeEvent('keystatuseschange');
  593. this.dispatchEvent(event);
  594. };
  595. /**
  596. * An implementation of MediaKeyStatusMap.
  597. * This fakes a map with a single key ID.
  598. *
  599. * @constructor
  600. * @struct
  601. * @implements {MediaKeyStatusMap}
  602. */
  603. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap = function() {
  604. /**
  605. * @type {number}
  606. */
  607. this.size = 0;
  608. /**
  609. * @private {string|undefined}
  610. */
  611. this.status_ = undefined;
  612. };
  613. /**
  614. * @const {!ArrayBuffer}
  615. * @private
  616. */
  617. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.KEY_ID_;
  618. /**
  619. * An internal method used by the session to set key status.
  620. * @param {string|undefined} status
  621. */
  622. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
  623. setStatus = function(status) {
  624. this.size = status == undefined ? 0 : 1;
  625. this.status_ = status;
  626. };
  627. /**
  628. * An internal method used by the session to get key status.
  629. * @return {string|undefined}
  630. */
  631. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
  632. getStatus = function() {
  633. return this.status_;
  634. };
  635. /** @override */
  636. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
  637. forEach = function(fn) {
  638. if (this.status_) {
  639. let fakeKeyId =
  640. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.KEY_ID_;
  641. fn(this.status_, fakeKeyId);
  642. }
  643. };
  644. /** @override */
  645. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
  646. get = function(keyId) {
  647. if (this.has(keyId)) {
  648. return this.status_;
  649. }
  650. return undefined;
  651. };
  652. /** @override */
  653. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
  654. has = function(keyId) {
  655. let fakeKeyId =
  656. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.KEY_ID_;
  657. if (this.status_ &&
  658. shaka.util.Uint8ArrayUtils.equal(
  659. new Uint8Array(keyId), new Uint8Array(fakeKeyId))) {
  660. return true;
  661. }
  662. return false;
  663. };
  664. /**
  665. * @suppress {missingReturn}
  666. * @override
  667. */
  668. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
  669. entries = function() {
  670. goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
  671. };
  672. /**
  673. * @suppress {missingReturn}
  674. * @override
  675. */
  676. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
  677. keys = function() {
  678. goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
  679. };
  680. /**
  681. * @suppress {missingReturn}
  682. * @override
  683. */
  684. shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
  685. values = function() {
  686. goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
  687. };
  688. shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysMs.install);