Source: lib/media/drm_engine.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.media.DrmEngine');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.net.NetworkingEngine');
  21. goog.require('shaka.util.ArrayUtils');
  22. goog.require('shaka.util.Error');
  23. goog.require('shaka.util.EventManager');
  24. goog.require('shaka.util.FakeEvent');
  25. goog.require('shaka.util.IDestroyable');
  26. goog.require('shaka.util.ManifestParserUtils');
  27. goog.require('shaka.util.MapUtils');
  28. goog.require('shaka.util.MimeUtils');
  29. goog.require('shaka.util.PublicPromise');
  30. goog.require('shaka.util.StringUtils');
  31. goog.require('shaka.util.Timer');
  32. goog.require('shaka.util.Uint8ArrayUtils');
  33. /**
  34. * @param {shaka.media.DrmEngine.PlayerInterface} playerInterface
  35. *
  36. * @constructor
  37. * @struct
  38. * @implements {shaka.util.IDestroyable}
  39. */
  40. shaka.media.DrmEngine = function(playerInterface) {
  41. /** @private {?shaka.media.DrmEngine.PlayerInterface} */
  42. this.playerInterface_ = playerInterface;
  43. /** @private {Array.<string>} */
  44. this.supportedTypes_ = null;
  45. /** @private {MediaKeys} */
  46. this.mediaKeys_ = null;
  47. /** @private {HTMLMediaElement} */
  48. this.video_ = null;
  49. /** @private {boolean} */
  50. this.initialized_ = false;
  51. /** @private {?shakaExtern.DrmInfo} */
  52. this.currentDrmInfo_ = null;
  53. /** @private {shaka.util.EventManager} */
  54. this.eventManager_ = new shaka.util.EventManager();
  55. /** @private {!Array.<shaka.media.DrmEngine.ActiveSession>} */
  56. this.activeSessions_ = [];
  57. /** @private {!Array.<string>} */
  58. this.offlineSessionIds_ = [];
  59. /** @private {!shaka.util.PublicPromise} */
  60. this.allSessionsLoaded_ = new shaka.util.PublicPromise();
  61. /** @private {?shakaExtern.DrmConfiguration} */
  62. this.config_ = null;
  63. /** @private {?function(!shaka.util.Error)} */
  64. this.onError_ = (function(err) {
  65. this.allSessionsLoaded_.reject(err);
  66. playerInterface.onError(err);
  67. }.bind(this));
  68. /**
  69. * The most recent key status information we have.
  70. * We may not have announced this information to the outside world yet,
  71. * which we delay to batch up changes and avoid spurious "missing key" errors.
  72. * @private {!Object.<string, string>}
  73. */
  74. this.keyStatusByKeyId_ = {};
  75. /**
  76. * The key statuses most recently announced to other classes.
  77. * We may have more up-to-date information being collected in
  78. * this.keyStatusByKeyId_, which has not been batched up and released yet.
  79. * @private {!Object.<string, string>}
  80. */
  81. this.announcedKeyStatusByKeyId_ = {};
  82. /** @private {shaka.util.Timer} */
  83. this.keyStatusTimer_ = new shaka.util.Timer(
  84. this.processKeyStatusChanges_.bind(this));
  85. /**
  86. * A flag to signal when have started destroying ourselves. This will:
  87. * 1. Stop later calls to |destroy| from trying to destroy the already
  88. * destroyed (or currently destroying) DrmEngine.
  89. * 2. Stop in-progress async operations from continuing.
  90. *
  91. * @private {boolean}
  92. */
  93. this.isDestroying_ = false;
  94. /**
  95. * A promise that will only resolve once we have finished destroying
  96. * ourselves, this is used to ensure that subsequent calls to |destroy| don't
  97. * resolve before the first call to |destroy|.
  98. *
  99. * @private {!shaka.util.PublicPromise}
  100. */
  101. this.finishedDestroyingPromise_ = new shaka.util.PublicPromise();
  102. /** @private {boolean} */
  103. this.isOffline_ = false;
  104. /** @private {!Array.<!MediaKeyMessageEvent>} */
  105. this.mediaKeyMessageEvents_ = [];
  106. /** @private {boolean} */
  107. this.initialRequestsSent_ = false;
  108. /** @private {?shaka.util.Timer} */
  109. this.expirationTimer_ = new shaka.util.Timer(this.pollExpiration_.bind(this));
  110. this.expirationTimer_.scheduleRepeated(1);
  111. // Add a catch to the Promise to avoid console logs about uncaught errors.
  112. this.allSessionsLoaded_.catch(function() {});
  113. };
  114. /**
  115. * @typedef {{
  116. * loaded: boolean,
  117. * initData: Uint8Array,
  118. * session: !MediaKeySession,
  119. * oldExpiration: number,
  120. * updatePromise: shaka.util.PublicPromise
  121. * }}
  122. *
  123. * @description A record to track sessions and suppress duplicate init data.
  124. * @property {boolean} loaded
  125. * True once the key status has been updated (to a non-pending state). This
  126. * does not mean the session is 'usable'.
  127. * @property {Uint8Array} initData
  128. * The init data used to create the session.
  129. * @property {!MediaKeySession} session
  130. * The session object.
  131. * @property {number} oldExpiration
  132. * The expiration of the session on the last check. This is used to fire
  133. * an event when it changes.
  134. * @property {shaka.util.PublicPromise} updatePromise
  135. * An optional Promise that will be resolved/rejected on the next update()
  136. * call. This is used to track the 'license-release' message when calling
  137. * remove().
  138. */
  139. shaka.media.DrmEngine.ActiveSession;
  140. /**
  141. * @typedef {{
  142. * netEngine: !shaka.net.NetworkingEngine,
  143. * onError: function(!shaka.util.Error),
  144. * onKeyStatus: function(!Object.<string,string>),
  145. * onExpirationUpdated: function(string,number),
  146. * onEvent: function(!Event)
  147. * }}
  148. *
  149. * @property {shaka.net.NetworkingEngine} netEngine
  150. * The NetworkingEngine instance to use. The caller retains ownership.
  151. * @property {function(!shaka.util.Error)} onError
  152. * Called when an error occurs. If the error is recoverable (see
  153. * {@link shaka.util.Error}) then the caller may invoke either
  154. * StreamingEngine.switch*() or StreamingEngine.seeked() to attempt recovery.
  155. * @property {function(!Object.<string,string>)} onKeyStatus
  156. * Called when key status changes. The argument is a map of hex key IDs to
  157. * statuses.
  158. * @property {function(string,number)} onExpirationUpdated
  159. * Called when the session expiration value changes.
  160. * @property {function(!Event)} onEvent
  161. * Called when an event occurs that should be sent to the app.
  162. */
  163. shaka.media.DrmEngine.PlayerInterface;
  164. /** @override */
  165. shaka.media.DrmEngine.prototype.destroy = async function() {
  166. // If we have started destroying ourselves, wait for the common "I am finished
  167. // being destroyed" promise to be resolved.
  168. if (this.isDestroying_) {
  169. await this.finishedDestroyingPromise_;
  170. } else {
  171. this.isDestroying_ = true;
  172. await this.destroyNow_();
  173. this.finishedDestroyingPromise_.resolve();
  174. }
  175. };
  176. /**
  177. * Destroy this instance of DrmEngine. This assumes that all other checks about
  178. * "if it should" have passed.
  179. *
  180. * @private
  181. */
  182. shaka.media.DrmEngine.prototype.destroyNow_ = async function() {
  183. // |eventManager_| should only be |null| after we call |destroy|. Destroy it
  184. // first so that we will stop responding to events.
  185. await this.eventManager_.destroy();
  186. this.eventManager_ = null;
  187. // Since we are destroying ourselves, we don't want to react to the "all
  188. // sessions loaded" event.
  189. this.allSessionsLoaded_.reject();
  190. // Stop all timers. This will ensure that they do not start any new work while
  191. // we are destroying ourselves.
  192. this.expirationTimer_.cancel();
  193. this.expirationTimer_ = null;
  194. this.keyStatusTimer_.cancel();
  195. this.keyStatusTimer_ = null;
  196. // Close all open sessions.
  197. const openSessions = this.activeSessions_;
  198. this.activeSessions_ = [];
  199. // Close all sessions before we remove media keys from the video element.
  200. await Promise.all(openSessions.map((session) => {
  201. return Promise.resolve().then(async () => {
  202. shaka.log.v1('Closing session', session.sessionId);
  203. try {
  204. await shaka.media.DrmEngine.closeSession_(session.session);
  205. } catch (error) {
  206. // Ignore errors when closing the sessions. Closing a session that
  207. // generated no key requests will throw an error.
  208. }
  209. });
  210. }));
  211. // |video_| will be |null| if we never attached to a video element.
  212. if (this.video_) {
  213. goog.asserts.assert(!this.video_.src, 'video src must be removed first!');
  214. try {
  215. await this.video_.setMediaKeys(null);
  216. } catch (error) {
  217. // Ignore any failures while removing media keys from the video element.
  218. }
  219. this.video_ = null;
  220. }
  221. // Break references to everything else we hold internally.
  222. this.currentDrmInfo_ = null;
  223. this.supportedTypes_ = null;
  224. this.mediaKeys_ = null;
  225. this.offlineSessionIds_ = [];
  226. this.config_ = null;
  227. this.onError_ = null;
  228. this.playerInterface_ = null;
  229. };
  230. /**
  231. * Called by the Player to provide an updated configuration any time it changes.
  232. * Must be called at least once before init().
  233. *
  234. * @param {shakaExtern.DrmConfiguration} config
  235. */
  236. shaka.media.DrmEngine.prototype.configure = function(config) {
  237. this.config_ = config;
  238. };
  239. /**
  240. * Negotiate for a key system and set up MediaKeys.
  241. * @param {!shakaExtern.Manifest} manifest The manifest is read for MIME type
  242. * and DRM information to query EME. If the 'clearKeys' configuration is
  243. * used, the manifest will be modified to force the use of Clear Key.
  244. * @param {boolean} offline True if we are storing or loading offline content.
  245. * @return {!Promise} Resolved if/when a key system has been chosen.
  246. */
  247. shaka.media.DrmEngine.prototype.init = function(manifest, offline) {
  248. goog.asserts.assert(this.config_,
  249. 'DrmEngine configure() must be called before init()!');
  250. /** @type {!Object.<string, MediaKeySystemConfiguration>} */
  251. let configsByKeySystem = {};
  252. /** @type {!Array.<string>} */
  253. let keySystemsInOrder = [];
  254. // If initial manifest contains unencrypted content, drm configuration
  255. // overrides DrmInfo so drmEngine can be activated. Thus, the player can play
  256. // encrypted content if live stream switches from unencrypted content to
  257. // encrypted content during live streams.
  258. let isEncryptedContent = manifest.periods.some((period) => {
  259. return period.variants.some((variant) => variant.drmInfos.length);
  260. });
  261. // |isOffline_| determines what kind of session to create. The argument to
  262. // |prepareMediaKeyConfigs_| determines the kind of CDM to query for. So
  263. // we still need persistent state when we are loading offline sessions.
  264. this.isOffline_ = offline;
  265. this.offlineSessionIds_ = manifest.offlineSessionIds;
  266. this.prepareMediaKeyConfigs_(
  267. manifest, offline || manifest.offlineSessionIds.length > 0,
  268. configsByKeySystem, keySystemsInOrder);
  269. if (!keySystemsInOrder.length) {
  270. // Unencrypted.
  271. this.initialized_ = true;
  272. return Promise.resolve();
  273. }
  274. return this.queryMediaKeys_(
  275. configsByKeySystem, keySystemsInOrder, isEncryptedContent);
  276. };
  277. /**
  278. * Attach MediaKeys to the video element and start processing events.
  279. * @param {HTMLMediaElement} video
  280. * @return {!Promise}
  281. */
  282. shaka.media.DrmEngine.prototype.attach = function(video) {
  283. if (!this.mediaKeys_) {
  284. // Unencrypted, or so we think. We listen for encrypted events in order to
  285. // warn when the stream is encrypted, even though the manifest does not know
  286. // it.
  287. // Don't complain about this twice, so just listenOnce().
  288. this.eventManager_.listenOnce(video, 'encrypted', function(event) {
  289. this.onError_(new shaka.util.Error(
  290. shaka.util.Error.Severity.CRITICAL,
  291. shaka.util.Error.Category.DRM,
  292. shaka.util.Error.Code.ENCRYPTED_CONTENT_WITHOUT_DRM_INFO));
  293. }.bind(this));
  294. return Promise.resolve();
  295. }
  296. this.video_ = video;
  297. this.eventManager_.listenOnce(this.video_, 'play', this.onPlay_.bind(this));
  298. let setMediaKeys = this.video_.setMediaKeys(this.mediaKeys_);
  299. setMediaKeys = setMediaKeys.catch(function(exception) {
  300. return Promise.reject(new shaka.util.Error(
  301. shaka.util.Error.Severity.CRITICAL,
  302. shaka.util.Error.Category.DRM,
  303. shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO,
  304. exception.message));
  305. });
  306. let setServerCertificate = null;
  307. if (this.currentDrmInfo_.serverCertificate &&
  308. this.currentDrmInfo_.serverCertificate.length) {
  309. setServerCertificate = this.mediaKeys_.setServerCertificate(
  310. this.currentDrmInfo_.serverCertificate).then(function(supported) {
  311. if (!supported) {
  312. shaka.log.warning('Server certificates are not supported by the key' +
  313. ' system. The server certificate has been ignored.');
  314. }
  315. }).catch(function(exception) {
  316. return Promise.reject(new shaka.util.Error(
  317. shaka.util.Error.Severity.CRITICAL,
  318. shaka.util.Error.Category.DRM,
  319. shaka.util.Error.Code.INVALID_SERVER_CERTIFICATE,
  320. exception.message));
  321. });
  322. }
  323. return Promise.all([setMediaKeys, setServerCertificate]).then(() => {
  324. if (this.isDestroying_) { return Promise.reject(); }
  325. this.createOrLoad();
  326. if (!this.currentDrmInfo_.initData.length &&
  327. !this.offlineSessionIds_.length) {
  328. // Explicit init data for any one stream or an offline session is
  329. // sufficient to suppress 'encrypted' events for all streams.
  330. const cb = (e) =>
  331. this.newInitData(e.initDataType, new Uint8Array(e.initData));
  332. this.eventManager_.listen(this.video_, 'encrypted', cb);
  333. }
  334. }).catch((error) => {
  335. if (this.isDestroying_) { return; }
  336. return Promise.reject(error);
  337. });
  338. };
  339. /**
  340. * Removes the given offline sessions and deletes their data. Must call init()
  341. * before this. This will wait until the 'license-release' message is handled
  342. * and the resulting Promise will be rejected if there is an error with that.
  343. *
  344. * @param {!Array.<string>} sessions
  345. * @return {!Promise}
  346. */
  347. shaka.media.DrmEngine.prototype.removeSessions = function(sessions) {
  348. goog.asserts.assert(this.mediaKeys_ || !sessions.length,
  349. 'Must call init() before removeSessions');
  350. return Promise.all(sessions.map(function(sessionId) {
  351. return this.loadOfflineSession_(sessionId).then(function(session) {
  352. // This will be null on error, such as session not found.
  353. if (session) {
  354. let p = new shaka.util.PublicPromise();
  355. // TODO: Consider adding a timeout to get the 'message' event.
  356. // Note that the 'message' event will get raised after the remove()
  357. // promise resolves.
  358. for (let i = 0; i < this.activeSessions_.length; i++) {
  359. if (this.activeSessions_[i].session == session) {
  360. this.activeSessions_[i].updatePromise = p;
  361. break;
  362. }
  363. }
  364. return Promise.all([session.remove(), p]);
  365. }
  366. }.bind(this));
  367. }.bind(this)));
  368. };
  369. /**
  370. * Creates the sessions for the init data and waits for them to become ready.
  371. *
  372. * @return {!Promise}
  373. */
  374. shaka.media.DrmEngine.prototype.createOrLoad = function() {
  375. let initDatas = this.currentDrmInfo_ ? this.currentDrmInfo_.initData : [];
  376. initDatas.forEach(function(initDataOverride) {
  377. this.createTemporarySession_(
  378. initDataOverride.initDataType, initDataOverride.initData);
  379. }.bind(this));
  380. this.offlineSessionIds_.forEach(function(sessionId) {
  381. this.loadOfflineSession_(sessionId);
  382. }.bind(this));
  383. if (!initDatas.length && !this.offlineSessionIds_.length) {
  384. this.allSessionsLoaded_.resolve();
  385. }
  386. return this.allSessionsLoaded_;
  387. };
  388. /**
  389. * Called when new initialization data is encountered. If this data hasn't
  390. * been seen yet, this will create a new session for it.
  391. *
  392. * @param {string} initDataType
  393. * @param {!Uint8Array} initData
  394. */
  395. shaka.media.DrmEngine.prototype.newInitData = function(initDataType, initData) {
  396. // Aliases:
  397. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  398. // Suppress duplicate init data.
  399. // Note that some init data are extremely large and can't portably be used as
  400. // keys in a dictionary.
  401. for (const session of this.activeSessions_) {
  402. if (Uint8ArrayUtils.equal(initData, session.initData)) {
  403. shaka.log.debug('Ignoring duplicate init data.');
  404. return;
  405. }
  406. }
  407. this.createTemporarySession_(initDataType, initData);
  408. };
  409. /** @return {boolean} */
  410. shaka.media.DrmEngine.prototype.initialized = function() {
  411. return this.initialized_;
  412. };
  413. /** @return {string} */
  414. shaka.media.DrmEngine.prototype.keySystem = function() {
  415. return this.currentDrmInfo_ ? this.currentDrmInfo_.keySystem : '';
  416. };
  417. /**
  418. * Returns an array of the media types supported by the current key system.
  419. * These will be full mime types (e.g. 'video/webm; codecs="vp8"').
  420. *
  421. * @return {Array.<string>}
  422. */
  423. shaka.media.DrmEngine.prototype.getSupportedTypes = function() {
  424. return this.supportedTypes_;
  425. };
  426. /**
  427. * Returns the ID of the sessions currently active.
  428. *
  429. * @return {!Array.<string>}
  430. */
  431. shaka.media.DrmEngine.prototype.getSessionIds = function() {
  432. return this.activeSessions_.map(function(session) {
  433. return session.session.sessionId;
  434. });
  435. };
  436. /**
  437. * Returns the next expiration time, or Infinity.
  438. * @return {number}
  439. */
  440. shaka.media.DrmEngine.prototype.getExpiration = function() {
  441. let expirations = this.activeSessions_.map(function(session) {
  442. let expiration = session.session.expiration;
  443. return isNaN(expiration) ? Infinity : expiration;
  444. });
  445. // This will equal Infinity if there are no entries.
  446. return Math.min.apply(Math, expirations);
  447. };
  448. /**
  449. * Returns the DrmInfo that was used to initialize the current key system.
  450. *
  451. * @return {?shakaExtern.DrmInfo}
  452. */
  453. shaka.media.DrmEngine.prototype.getDrmInfo = function() {
  454. return this.currentDrmInfo_;
  455. };
  456. /**
  457. * Returns the current key statuses.
  458. *
  459. * @return {!Object.<string, string>}
  460. */
  461. shaka.media.DrmEngine.prototype.getKeyStatuses = function() {
  462. return this.announcedKeyStatusByKeyId_;
  463. };
  464. /**
  465. * @param {!shakaExtern.Manifest} manifest
  466. * @param {boolean} offline True if we are storing or loading offline content.
  467. * @param {!Object.<string, MediaKeySystemConfiguration>} configsByKeySystem
  468. * (Output parameter.) A dictionary of configs, indexed by key system.
  469. * @param {!Array.<string>} keySystemsInOrder
  470. * (Output parameter.) A list of key systems in the order in which we
  471. * encounter them.
  472. * @see https://goo.gl/nwdYnY for MediaKeySystemConfiguration spec
  473. * @private
  474. */
  475. shaka.media.DrmEngine.prototype.prepareMediaKeyConfigs_ =
  476. function(manifest, offline, configsByKeySystem, keySystemsInOrder) {
  477. let clearKeyDrmInfo = this.configureClearKey_();
  478. let configDrmInfos = this.getDrmInfosByConfig_(manifest);
  479. manifest.periods.forEach(function(period) {
  480. period.variants.forEach(function(variant) {
  481. // clearKey config overrides manifest DrmInfo if present.
  482. // The manifest is modified so that filtering in Player still works.
  483. if (clearKeyDrmInfo) {
  484. variant.drmInfos = [clearKeyDrmInfo];
  485. }
  486. // If initial manifest contains unencrypted content,
  487. // drm configuration overrides DrmInfo so drmEngine can be activated.
  488. // Thus, the player can play encrypted content if live stream switches
  489. // from unencrypted content to encrypted content during live streams.
  490. if (configDrmInfos) {
  491. variant.drmInfos = configDrmInfos;
  492. }
  493. variant.drmInfos.forEach(function(drmInfo) {
  494. this.fillInDrmInfoDefaults_(drmInfo);
  495. // Chromecast has a variant of PlayReady that uses a different key
  496. // system ID. Since manifest parsers convert the standard PlayReady
  497. // UUID to the standard PlayReady key system ID, here we will switch
  498. // to the Chromecast version if we are running on that platform.
  499. // Note that this must come after fillInDrmInfoDefaults_, since the
  500. // player config uses the standard PlayReady ID for license server
  501. // configuration.
  502. if (window.cast && window.cast.__platform__) {
  503. if (drmInfo.keySystem == 'com.microsoft.playready') {
  504. drmInfo.keySystem = 'com.chromecast.playready';
  505. }
  506. }
  507. let config = configsByKeySystem[drmInfo.keySystem];
  508. if (!config) {
  509. config = {
  510. // Ignore initDataTypes.
  511. audioCapabilities: [],
  512. videoCapabilities: [],
  513. distinctiveIdentifier: 'optional',
  514. persistentState: offline ? 'required' : 'optional',
  515. sessionTypes: [offline ? 'persistent-license' : 'temporary'],
  516. label: drmInfo.keySystem,
  517. drmInfos: [] // Tracked by us, ignored by EME.
  518. };
  519. configsByKeySystem[drmInfo.keySystem] = config;
  520. keySystemsInOrder.push(drmInfo.keySystem);
  521. }
  522. config.drmInfos.push(drmInfo);
  523. if (drmInfo.distinctiveIdentifierRequired) {
  524. config.distinctiveIdentifier = 'required';
  525. }
  526. if (drmInfo.persistentStateRequired) {
  527. config.persistentState = 'required';
  528. }
  529. let streams = [];
  530. if (variant.video) streams.push(variant.video);
  531. if (variant.audio) streams.push(variant.audio);
  532. streams.forEach(function(stream) {
  533. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  534. /** @type {!Array.<!MediaKeySystemMediaCapability>} */
  535. let capabilities = (stream.type == ContentType.VIDEO) ?
  536. config.videoCapabilities : config.audioCapabilities;
  537. /** @type {string} */
  538. let robustness = ((stream.type == ContentType.VIDEO) ?
  539. drmInfo.videoRobustness : drmInfo.audioRobustness) || '';
  540. let fullMimeType = shaka.util.MimeUtils.getFullType(
  541. stream.mimeType, stream.codecs);
  542. capabilities.push({
  543. robustness: robustness,
  544. contentType: fullMimeType
  545. });
  546. }.bind(this)); // streams.forEach (variant.video, variant.audio)
  547. }.bind(this)); // variant.drmInfos.forEach
  548. }.bind(this)); // periods.variants.forEach
  549. }.bind(this)); // manifest.perios.forEach
  550. };
  551. /**
  552. * @param {!Object.<string, MediaKeySystemConfiguration>} configsByKeySystem
  553. * A dictionary of configs, indexed by key system.
  554. * @param {!Array.<string>} keySystemsInOrder
  555. * A list of key systems in the order in which we should query them.
  556. * On a browser which supports multiple key systems, the order may indicate
  557. * a real preference for the application.
  558. * @param {boolean} isEncryptedContent
  559. * True if the content is encrypted, false otherwise.
  560. * @return {!Promise} Resolved if/when a key system has been chosen.
  561. * @private
  562. */
  563. shaka.media.DrmEngine.prototype.queryMediaKeys_ =
  564. function(configsByKeySystem, keySystemsInOrder, isEncryptedContent) {
  565. if (keySystemsInOrder.length == 1 && keySystemsInOrder[0] == '') {
  566. return Promise.reject(new shaka.util.Error(
  567. shaka.util.Error.Severity.CRITICAL,
  568. shaka.util.Error.Category.DRM,
  569. shaka.util.Error.Code.NO_RECOGNIZED_KEY_SYSTEMS));
  570. }
  571. // Wait to reject this initial Promise until we have built the entire chain.
  572. let instigator = new shaka.util.PublicPromise();
  573. let p = instigator;
  574. // Try key systems with configured license servers first. We only have to try
  575. // key systems without configured license servers for diagnostic reasons, so
  576. // that we can differentiate between "none of these key systems are available"
  577. // and "some are available, but you did not configure them properly." The
  578. // former takes precedence.
  579. [true, false].forEach(function(shouldHaveLicenseServer) {
  580. keySystemsInOrder.forEach(function(keySystem) {
  581. let config = configsByKeySystem[keySystem];
  582. let hasLicenseServer = config.drmInfos.some(function(info) {
  583. return !!info.licenseServerUri;
  584. });
  585. if (hasLicenseServer != shouldHaveLicenseServer) return;
  586. // If there are no tracks of a type, these should be not present.
  587. // Otherwise the query will fail.
  588. if (config.audioCapabilities.length == 0) {
  589. delete config.audioCapabilities;
  590. }
  591. if (config.videoCapabilities.length == 0) {
  592. delete config.videoCapabilities;
  593. }
  594. p = p.catch(function() {
  595. if (this.isDestroying_) { return; }
  596. return navigator.requestMediaKeySystemAccess(keySystem, [config]);
  597. }.bind(this));
  598. }.bind(this));
  599. }.bind(this));
  600. p = p.catch(function() {
  601. return Promise.reject(new shaka.util.Error(
  602. shaka.util.Error.Severity.CRITICAL,
  603. shaka.util.Error.Category.DRM,
  604. shaka.util.Error.Code.REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE));
  605. });
  606. p = p.then(function(mediaKeySystemAccess) {
  607. if (this.isDestroying_) { return Promise.reject(); }
  608. // TODO: Remove once Edge has released a fix for https://goo.gl/qMeV7v
  609. let isEdge = navigator.userAgent.indexOf('Edge/') >= 0;
  610. // Store the capabilities of the key system.
  611. let realConfig = mediaKeySystemAccess.getConfiguration();
  612. let audioCaps = realConfig.audioCapabilities || [];
  613. let videoCaps = realConfig.videoCapabilities || [];
  614. let caps = audioCaps.concat(videoCaps);
  615. this.supportedTypes_ = caps.map(function(c) { return c.contentType; });
  616. if (isEdge) {
  617. // Edge 14 does not report correct capabilities. It will only report the
  618. // first MIME type even if the others are supported. To work around this,
  619. // set the supported types to null, which Player will use as a signal that
  620. // the information is not available.
  621. // See: https://goo.gl/qMeV7v
  622. this.supportedTypes_ = null;
  623. }
  624. goog.asserts.assert(!this.supportedTypes_ || this.supportedTypes_.length,
  625. 'We should get at least one supported MIME type');
  626. let originalConfig = configsByKeySystem[mediaKeySystemAccess.keySystem];
  627. this.createCurrentDrmInfo_(
  628. mediaKeySystemAccess.keySystem, originalConfig,
  629. originalConfig.drmInfos);
  630. if (!this.currentDrmInfo_.licenseServerUri) {
  631. return Promise.reject(new shaka.util.Error(
  632. shaka.util.Error.Severity.CRITICAL,
  633. shaka.util.Error.Category.DRM,
  634. shaka.util.Error.Code.NO_LICENSE_SERVER_GIVEN));
  635. }
  636. return mediaKeySystemAccess.createMediaKeys();
  637. }.bind(this)).then(function(mediaKeys) {
  638. if (this.isDestroying_) { return Promise.reject(); }
  639. shaka.log.info('Created MediaKeys object for key system',
  640. this.currentDrmInfo_.keySystem);
  641. this.mediaKeys_ = mediaKeys;
  642. this.initialized_ = true;
  643. }.bind(this)).catch(function(exception) {
  644. if (this.isDestroying_) { return; }
  645. // Don't rewrap a shaka.util.Error from earlier in the chain:
  646. this.currentDrmInfo_ = null;
  647. this.supportedTypes_ = null;
  648. if (exception instanceof shaka.util.Error) {
  649. return Promise.reject(exception);
  650. }
  651. // We failed to create MediaKeys. This generally shouldn't happen.
  652. return Promise.reject(new shaka.util.Error(
  653. shaka.util.Error.Severity.CRITICAL,
  654. shaka.util.Error.Category.DRM,
  655. shaka.util.Error.Code.FAILED_TO_CREATE_CDM,
  656. exception.message));
  657. }.bind(this));
  658. if (!isEncryptedContent) {
  659. // It doesn't matter if we fail to initialize the drm engine, if we won't
  660. // actually need it anyway.
  661. p = p.catch(() => {});
  662. }
  663. instigator.reject();
  664. return p;
  665. };
  666. /**
  667. * Use this.config_ to fill in missing values in drmInfo.
  668. * @param {shakaExtern.DrmInfo} drmInfo
  669. * @private
  670. */
  671. shaka.media.DrmEngine.prototype.fillInDrmInfoDefaults_ = function(drmInfo) {
  672. let keySystem = drmInfo.keySystem;
  673. if (!keySystem) {
  674. // This is a placeholder from the manifest parser for an unrecognized key
  675. // system. Skip this entry, to avoid logging nonsensical errors.
  676. return;
  677. }
  678. if (!drmInfo.licenseServerUri) {
  679. let server = this.config_.servers[keySystem];
  680. if (server) {
  681. drmInfo.licenseServerUri = server;
  682. }
  683. }
  684. if (!drmInfo.keyIds) {
  685. drmInfo.keyIds = [];
  686. }
  687. let advanced = this.config_.advanced[keySystem];
  688. if (advanced) {
  689. if (!drmInfo.distinctiveIdentifierRequired) {
  690. drmInfo.distinctiveIdentifierRequired =
  691. advanced.distinctiveIdentifierRequired;
  692. }
  693. if (!drmInfo.persistentStateRequired) {
  694. drmInfo.persistentStateRequired = advanced.persistentStateRequired;
  695. }
  696. if (!drmInfo.videoRobustness) {
  697. drmInfo.videoRobustness = advanced.videoRobustness;
  698. }
  699. if (!drmInfo.audioRobustness) {
  700. drmInfo.audioRobustness = advanced.audioRobustness;
  701. }
  702. if (!drmInfo.serverCertificate) {
  703. drmInfo.serverCertificate = advanced.serverCertificate;
  704. }
  705. }
  706. };
  707. /**
  708. * Create a DrmInfo using configured clear keys.
  709. * The server URI will be a data URI which decodes to a clearkey license.
  710. * @return {?shakaExtern.DrmInfo} or null if clear keys are not configured.
  711. * @private
  712. * @see https://goo.gl/6nPdhF for the spec on the clearkey license format.
  713. */
  714. shaka.media.DrmEngine.prototype.configureClearKey_ = function() {
  715. let hasClearKeys = !shaka.util.MapUtils.empty(this.config_.clearKeys);
  716. if (!hasClearKeys) return null;
  717. const StringUtils = shaka.util.StringUtils;
  718. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  719. let keys = [];
  720. let keyIds = [];
  721. for (let keyIdHex in this.config_.clearKeys) {
  722. let keyHex = this.config_.clearKeys[keyIdHex];
  723. let keyId = Uint8ArrayUtils.fromHex(keyIdHex);
  724. let key = Uint8ArrayUtils.fromHex(keyHex);
  725. let keyObj = {
  726. kty: 'oct',
  727. kid: Uint8ArrayUtils.toBase64(keyId, false),
  728. k: Uint8ArrayUtils.toBase64(key, false)
  729. };
  730. keys.push(keyObj);
  731. keyIds.push(keyObj.kid);
  732. }
  733. let jwkSet = {keys: keys};
  734. let license = JSON.stringify(jwkSet);
  735. // Use the keyids init data since is suggested by EME.
  736. // Suggestion: https://goo.gl/R72xp4
  737. // Format: https://www.w3.org/TR/eme-initdata-keyids/
  738. let initDataStr = JSON.stringify({'kids': keyIds});
  739. let initData = new Uint8Array(StringUtils.toUTF8(initDataStr));
  740. let initDatas = [{initData: initData, initDataType: 'keyids'}];
  741. return {
  742. keySystem: 'org.w3.clearkey',
  743. licenseServerUri: 'data:application/json;base64,' + window.btoa(license),
  744. distinctiveIdentifierRequired: false,
  745. persistentStateRequired: false,
  746. audioRobustness: '',
  747. videoRobustness: '',
  748. serverCertificate: null,
  749. initData: initDatas,
  750. keyIds: []
  751. };
  752. };
  753. /**
  754. * Returns the DrmInfo that is generated by drm configation.
  755. * It activates DrmEngine if drm configs have keySystems.
  756. * @param {!shakaExtern.Manifest} manifest
  757. * @return {Array.<{shakaExtern.DrmInfo}>}
  758. * @private
  759. */
  760. shaka.media.DrmEngine.prototype.getDrmInfosByConfig_ = function(manifest) {
  761. let config = this.config_;
  762. let serverKeys = Object.keys(config.servers);
  763. if (!serverKeys.length) {
  764. return null;
  765. }
  766. let isEncryptedContent = manifest.periods.some(function(period) {
  767. return period.variants.some(function(variant) {
  768. return variant.drmInfos.length;
  769. });
  770. });
  771. // We should only create fake DrmInfos when none are provided by the manifest.
  772. if (isEncryptedContent) {
  773. return null;
  774. }
  775. return serverKeys.map(function(keySystem) {
  776. return {
  777. keySystem: keySystem,
  778. licenseServerUri: config.servers[keySystem],
  779. distinctiveIdentifierRequired: false,
  780. persistentStateRequired: false,
  781. audioRobustness: '',
  782. videoRobustness: '',
  783. serverCertificate: null,
  784. initData: [],
  785. keyIds: []
  786. };
  787. });
  788. };
  789. /**
  790. * Creates a DrmInfo object describing the settings used to initialize the
  791. * engine.
  792. *
  793. * @param {string} keySystem
  794. * @param {MediaKeySystemConfiguration} config
  795. * @param {!Array.<shakaExtern.DrmInfo>} drmInfos
  796. * @private
  797. */
  798. shaka.media.DrmEngine.prototype.createCurrentDrmInfo_ = function(
  799. keySystem, config, drmInfos) {
  800. /** @type {!Array.<string>} */
  801. let licenseServers = [];
  802. /** @type {!Array.<!Uint8Array>} */
  803. let serverCerts = [];
  804. /** @type {!Array.<!shakaExtern.InitDataOverride>} */
  805. let initDatas = [];
  806. /** @type {!Array.<string>} */
  807. let keyIds = [];
  808. this.processDrmInfos_(drmInfos, licenseServers, serverCerts, initDatas,
  809. keyIds);
  810. if (serverCerts.length > 1) {
  811. shaka.log.warning('Multiple unique server certificates found! ' +
  812. 'Only the first will be used.');
  813. }
  814. if (licenseServers.length > 1) {
  815. shaka.log.warning('Multiple unique license server URIs found! ' +
  816. 'Only the first will be used.');
  817. }
  818. // TODO: This only works when all DrmInfo have the same robustness.
  819. let audioRobustness =
  820. config.audioCapabilities ? config.audioCapabilities[0].robustness : '';
  821. let videoRobustness =
  822. config.videoCapabilities ? config.videoCapabilities[0].robustness : '';
  823. this.currentDrmInfo_ = {
  824. keySystem: keySystem,
  825. licenseServerUri: licenseServers[0],
  826. distinctiveIdentifierRequired: (config.distinctiveIdentifier == 'required'),
  827. persistentStateRequired: (config.persistentState == 'required'),
  828. audioRobustness: audioRobustness,
  829. videoRobustness: videoRobustness,
  830. serverCertificate: serverCerts[0],
  831. initData: initDatas,
  832. keyIds: keyIds
  833. };
  834. };
  835. /**
  836. * Extract license server, server cert, and init data from DrmInfos, taking
  837. * care to eliminate duplicates.
  838. *
  839. * @param {!Array.<shakaExtern.DrmInfo>} drmInfos
  840. * @param {!Array.<string>} licenseServers
  841. * @param {!Array.<!Uint8Array>} serverCerts
  842. * @param {!Array.<!shakaExtern.InitDataOverride>} initDatas
  843. * @param {!Array.<string>} keyIds
  844. * @private
  845. */
  846. shaka.media.DrmEngine.prototype.processDrmInfos_ =
  847. function(drmInfos, licenseServers, serverCerts, initDatas, keyIds) {
  848. /**
  849. * @param {shakaExtern.InitDataOverride} a
  850. * @param {shakaExtern.InitDataOverride} b
  851. * @return {boolean}
  852. */
  853. function initDataOverrideEqual(a, b) {
  854. if (a.keyId && a.keyId == b.keyId) {
  855. // Two initDatas with the same keyId are considered to be the same,
  856. // unless that "same keyId" is null.
  857. return true;
  858. }
  859. return a.initDataType == b.initDataType &&
  860. shaka.util.Uint8ArrayUtils.equal(a.initData, b.initData);
  861. }
  862. drmInfos.forEach(function(drmInfo) {
  863. // Aliases:
  864. const ArrayUtils = shaka.util.ArrayUtils;
  865. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  866. // Build an array of unique license servers.
  867. if (licenseServers.indexOf(drmInfo.licenseServerUri) == -1) {
  868. licenseServers.push(drmInfo.licenseServerUri);
  869. }
  870. // Build an array of unique server certs.
  871. if (drmInfo.serverCertificate) {
  872. if (ArrayUtils.indexOf(serverCerts, drmInfo.serverCertificate,
  873. Uint8ArrayUtils.equal) == -1) {
  874. serverCerts.push(drmInfo.serverCertificate);
  875. }
  876. }
  877. // Build an array of unique init datas.
  878. if (drmInfo.initData) {
  879. drmInfo.initData.forEach(function(initDataOverride) {
  880. if (ArrayUtils.indexOf(initDatas, initDataOverride,
  881. initDataOverrideEqual) == -1) {
  882. initDatas.push(initDataOverride);
  883. }
  884. });
  885. }
  886. if (drmInfo.keyIds) {
  887. for (let i = 0; i < drmInfo.keyIds.length; ++i) {
  888. if (keyIds.indexOf(drmInfo.keyIds[i]) == -1) {
  889. keyIds.push(drmInfo.keyIds[i]);
  890. }
  891. }
  892. }
  893. });
  894. };
  895. /**
  896. * @param {string} sessionId
  897. * @return {!Promise.<MediaKeySession>}
  898. * @private
  899. */
  900. shaka.media.DrmEngine.prototype.loadOfflineSession_ = function(sessionId) {
  901. let session;
  902. try {
  903. session = this.mediaKeys_.createSession('persistent-license');
  904. } catch (exception) {
  905. let error = new shaka.util.Error(
  906. shaka.util.Error.Severity.CRITICAL,
  907. shaka.util.Error.Category.DRM,
  908. shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
  909. exception.message);
  910. this.onError_(error);
  911. return Promise.reject(error);
  912. }
  913. this.eventManager_.listen(session, 'message',
  914. /** @type {shaka.util.EventManager.ListenerType} */(
  915. this.onSessionMessage_.bind(this)));
  916. this.eventManager_.listen(session, 'keystatuseschange',
  917. this.onKeyStatusesChange_.bind(this));
  918. let activeSession = {
  919. initData: null,
  920. session: session,
  921. loaded: false,
  922. oldExpiration: Infinity,
  923. updatePromise: null
  924. };
  925. this.activeSessions_.push(activeSession);
  926. return session.load(sessionId).then(function(present) {
  927. if (this.isDestroying_) { return Promise.reject(); }
  928. shaka.log.v2('Loaded offline session', sessionId, present);
  929. if (!present) {
  930. let i = this.activeSessions_.indexOf(activeSession);
  931. goog.asserts.assert(i >= 0, 'Session must be in the array');
  932. this.activeSessions_.splice(i, 1);
  933. this.onError_(new shaka.util.Error(
  934. shaka.util.Error.Severity.CRITICAL,
  935. shaka.util.Error.Category.DRM,
  936. shaka.util.Error.Code.OFFLINE_SESSION_REMOVED));
  937. return;
  938. }
  939. // TODO: We should get a key status change event. Remove once Chrome CDM
  940. // is fixed.
  941. activeSession.loaded = true;
  942. if (this.activeSessions_.every((s) => s.loaded)) {
  943. this.allSessionsLoaded_.resolve();
  944. }
  945. return session;
  946. }.bind(this), function(error) {
  947. if (this.isDestroying_) { return; }
  948. let i = this.activeSessions_.indexOf(activeSession);
  949. goog.asserts.assert(i >= 0, 'Session must be in the array');
  950. this.activeSessions_.splice(i, 1);
  951. this.onError_(new shaka.util.Error(
  952. shaka.util.Error.Severity.CRITICAL,
  953. shaka.util.Error.Category.DRM,
  954. shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
  955. error.message));
  956. }.bind(this));
  957. };
  958. /**
  959. * @param {string} initDataType
  960. * @param {!Uint8Array} initData
  961. * @private
  962. */
  963. shaka.media.DrmEngine.prototype.createTemporarySession_ =
  964. function(initDataType, initData) {
  965. let session;
  966. try {
  967. if (this.isOffline_) {
  968. session = this.mediaKeys_.createSession('persistent-license');
  969. } else {
  970. session = this.mediaKeys_.createSession();
  971. }
  972. } catch (exception) {
  973. this.onError_(new shaka.util.Error(
  974. shaka.util.Error.Severity.CRITICAL,
  975. shaka.util.Error.Category.DRM,
  976. shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
  977. exception.message));
  978. return;
  979. }
  980. this.eventManager_.listen(session, 'message',
  981. /** @type {shaka.util.EventManager.ListenerType} */(
  982. this.onSessionMessage_.bind(this)));
  983. this.eventManager_.listen(session, 'keystatuseschange',
  984. this.onKeyStatusesChange_.bind(this));
  985. this.activeSessions_.push({
  986. initData: initData,
  987. session: session,
  988. loaded: false,
  989. oldExpiration: Infinity,
  990. updatePromise: null
  991. });
  992. session.generateRequest(initDataType, initData.buffer).catch((error) => {
  993. if (this.isDestroying_) { return; }
  994. for (let i = 0; i < this.activeSessions_.length; ++i) {
  995. if (this.activeSessions_[i].session == session) {
  996. this.activeSessions_.splice(i, 1);
  997. break;
  998. }
  999. }
  1000. let extended;
  1001. if (error.errorCode && error.errorCode.systemCode) {
  1002. extended = error.errorCode.systemCode;
  1003. if (extended < 0) {
  1004. extended += Math.pow(2, 32);
  1005. }
  1006. extended = '0x' + extended.toString(16);
  1007. }
  1008. this.onError_(new shaka.util.Error(
  1009. shaka.util.Error.Severity.CRITICAL,
  1010. shaka.util.Error.Category.DRM,
  1011. shaka.util.Error.Code.FAILED_TO_GENERATE_LICENSE_REQUEST,
  1012. error.message, error, extended));
  1013. });
  1014. };
  1015. /**
  1016. * @param {!MediaKeyMessageEvent} event
  1017. * @private
  1018. */
  1019. shaka.media.DrmEngine.prototype.onSessionMessage_ = function(event) {
  1020. if (this.delayLicenseRequest_()) {
  1021. this.mediaKeyMessageEvents_.push(event);
  1022. } else {
  1023. this.sendLicenseRequest_(event);
  1024. }
  1025. };
  1026. /**
  1027. * @return {boolean}
  1028. * @private
  1029. */
  1030. shaka.media.DrmEngine.prototype.delayLicenseRequest_ = function() {
  1031. return (this.config_.delayLicenseRequestUntilPlayed &&
  1032. this.video_.paused && !this.initialRequestsSent_);
  1033. };
  1034. /**
  1035. * Sends a license request.
  1036. * @param {!MediaKeyMessageEvent} event
  1037. * @private
  1038. */
  1039. shaka.media.DrmEngine.prototype.sendLicenseRequest_ = function(event) {
  1040. /** @type {!MediaKeySession} */
  1041. let session = event.target;
  1042. let activeSession;
  1043. for (let i = 0; i < this.activeSessions_.length; i++) {
  1044. if (this.activeSessions_[i].session == session) {
  1045. activeSession = this.activeSessions_[i];
  1046. break;
  1047. }
  1048. }
  1049. const requestType = shaka.net.NetworkingEngine.RequestType.LICENSE;
  1050. let request = shaka.net.NetworkingEngine.makeRequest(
  1051. [this.currentDrmInfo_.licenseServerUri], this.config_.retryParameters);
  1052. request.body = event.message;
  1053. request.method = 'POST';
  1054. // NOTE: allowCrossSiteCredentials can be set in a request filter.
  1055. if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' ||
  1056. this.currentDrmInfo_.keySystem == 'com.chromecast.playready') {
  1057. this.unpackPlayReadyRequest_(request);
  1058. }
  1059. this.playerInterface_.netEngine.request(requestType, request).promise
  1060. .then(function(response) {
  1061. if (this.isDestroying_) { return Promise.reject(); }
  1062. // Request succeeded, now pass the response to the CDM.
  1063. return session.update(response.data).then(function() {
  1064. let event = new shaka.util.FakeEvent('drmsessionupdate');
  1065. this.playerInterface_.onEvent(event);
  1066. if (activeSession) {
  1067. if (activeSession.updatePromise) {
  1068. activeSession.updatePromise.resolve();
  1069. }
  1070. // In case there are no key statuses, consider this session loaded
  1071. // after a reasonable timeout. It should definitely not take 5
  1072. // seconds to process a license.
  1073. setTimeout(function() {
  1074. activeSession.loaded = true;
  1075. if (this.activeSessions_.every((s) => s.loaded)) {
  1076. this.allSessionsLoaded_.resolve();
  1077. }
  1078. }.bind(this), shaka.media.DrmEngine.SESSION_LOAD_TIMEOUT_ * 1000);
  1079. }
  1080. }.bind(this));
  1081. }.bind(this), function(error) {
  1082. // Ignore destruction errors
  1083. if (this.isDestroying_) { return; }
  1084. // Request failed!
  1085. goog.asserts.assert(error instanceof shaka.util.Error,
  1086. 'Wrong NetworkingEngine error type!');
  1087. let shakaErr = new shaka.util.Error(
  1088. shaka.util.Error.Severity.CRITICAL,
  1089. shaka.util.Error.Category.DRM,
  1090. shaka.util.Error.Code.LICENSE_REQUEST_FAILED,
  1091. error);
  1092. this.onError_(shakaErr);
  1093. if (activeSession && activeSession.updatePromise) {
  1094. activeSession.updatePromise.reject(shakaErr);
  1095. }
  1096. }.bind(this)).catch(function(error) {
  1097. // Ignore destruction errors
  1098. if (this.isDestroying_) { return; }
  1099. // Session update failed!
  1100. let shakaErr = new shaka.util.Error(
  1101. shaka.util.Error.Severity.CRITICAL,
  1102. shaka.util.Error.Category.DRM,
  1103. shaka.util.Error.Code.LICENSE_RESPONSE_REJECTED,
  1104. error.message);
  1105. this.onError_(shakaErr);
  1106. if (activeSession && activeSession.updatePromise) {
  1107. activeSession.updatePromise.reject(shakaErr);
  1108. }
  1109. }.bind(this));
  1110. };
  1111. /**
  1112. * Unpacks PlayReady license requests. Modifies the request object.
  1113. * @param {shakaExtern.Request} request
  1114. * @private
  1115. */
  1116. shaka.media.DrmEngine.prototype.unpackPlayReadyRequest_ = function(request) {
  1117. // On IE and Edge, the raw license message is UTF-16-encoded XML. We need to
  1118. // unpack the Challenge element (base64-encoded string containing the actual
  1119. // license request) and any HttpHeader elements (sent as request headers).
  1120. // Example XML:
  1121. // <PlayReadyKeyMessage type="LicenseAcquisition">
  1122. // <LicenseAcquisition Version="1">
  1123. // <Challenge encoding="base64encoded">{Base64Data}</Challenge>
  1124. // <HttpHeaders>
  1125. // <HttpHeader>
  1126. // <name>Content-Type</name>
  1127. // <value>text/xml; charset=utf-8</value>
  1128. // </HttpHeader>
  1129. // <HttpHeader>
  1130. // <name>SOAPAction</name>
  1131. // <value>http://schemas.microsoft.com/DRM/etc/etc</value>
  1132. // </HttpHeader>
  1133. // </HttpHeaders>
  1134. // </LicenseAcquisition>
  1135. // </PlayReadyKeyMessage>
  1136. let xml = shaka.util.StringUtils.fromUTF16(
  1137. request.body, true /* littleEndian */, true /* noThrow */);
  1138. if (xml.indexOf('PlayReadyKeyMessage') == -1) {
  1139. // This does not appear to be a wrapped message as on IE and Edge. Some
  1140. // clients do not need this unwrapping, so we will assume this is one of
  1141. // them. Note that "xml" at this point probably looks like random garbage,
  1142. // since we interpreted UTF-8 as UTF-16.
  1143. shaka.log.debug('PlayReady request is already unwrapped.');
  1144. request.headers['Content-Type'] = 'text/xml; charset=utf-8';
  1145. return;
  1146. }
  1147. shaka.log.debug('Unwrapping PlayReady request.');
  1148. let dom = new DOMParser().parseFromString(xml, 'application/xml');
  1149. // Set request headers.
  1150. let headers = dom.getElementsByTagName('HttpHeader');
  1151. for (let i = 0; i < headers.length; ++i) {
  1152. let name = headers[i].querySelector('name');
  1153. let value = headers[i].querySelector('value');
  1154. goog.asserts.assert(name && value, 'Malformed PlayReady headers!');
  1155. request.headers[name.textContent] = value.textContent;
  1156. }
  1157. // Unpack the base64-encoded challenge.
  1158. let challenge = dom.querySelector('Challenge');
  1159. goog.asserts.assert(challenge, 'Malformed PlayReady challenge!');
  1160. goog.asserts.assert(challenge.getAttribute('encoding') == 'base64encoded',
  1161. 'Unexpected PlayReady challenge encoding!');
  1162. request.body =
  1163. shaka.util.Uint8ArrayUtils.fromBase64(challenge.textContent).buffer;
  1164. };
  1165. /**
  1166. * @param {!Event} event
  1167. * @private
  1168. * @suppress {invalidCasts} to swap keyId and status
  1169. */
  1170. shaka.media.DrmEngine.prototype.onKeyStatusesChange_ = function(event) {
  1171. let session = /** @type {!MediaKeySession} */(event.target);
  1172. // Locate the session in the active sessions list.
  1173. let i;
  1174. for (i = 0; i < this.activeSessions_.length; ++i) {
  1175. if (this.activeSessions_[i].session == session) {
  1176. break;
  1177. }
  1178. }
  1179. const found = i < this.activeSessions_.length;
  1180. let keyStatusMap = session.keyStatuses;
  1181. let hasExpiredKeys = false;
  1182. keyStatusMap.forEach(function(status, keyId) {
  1183. // The spec has changed a few times on the exact order of arguments here.
  1184. // As of 2016-06-30, Edge has the order reversed compared to the current
  1185. // EME spec. Given the back and forth in the spec, it may not be the only
  1186. // one. Try to detect this and compensate:
  1187. if (typeof keyId == 'string') {
  1188. let tmp = keyId;
  1189. keyId = /** @type {ArrayBuffer} */(status);
  1190. status = /** @type {string} */(tmp);
  1191. }
  1192. // Microsoft's implementation in Edge seems to present key IDs as
  1193. // little-endian UUIDs, rather than big-endian or just plain array of bytes.
  1194. // standard: 6e 5a 1d 26 - 27 57 - 47 d7 - 80 46 ea a5 d1 d3 4b 5a
  1195. // on Edge: 26 1d 5a 6e - 57 27 - d7 47 - 80 46 ea a5 d1 d3 4b 5a
  1196. // Bug filed: https://goo.gl/gnRSkJ
  1197. // NOTE that we skip this if byteLength != 16. This is used for the IE11
  1198. // and Edge 12 EME polyfill, which uses single-byte dummy key IDs.
  1199. // However, unlike Edge and Chromecast, Tizen doesn't have this problem.
  1200. if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' &&
  1201. keyId.byteLength == 16 && !/Tizen/.exec(navigator.userAgent)) {
  1202. // Read out some fields in little-endian:
  1203. let dataView = new DataView(keyId);
  1204. let part0 = dataView.getUint32(0, true /* LE */);
  1205. let part1 = dataView.getUint16(4, true /* LE */);
  1206. let part2 = dataView.getUint16(6, true /* LE */);
  1207. // Write it back in big-endian:
  1208. dataView.setUint32(0, part0, false /* BE */);
  1209. dataView.setUint16(4, part1, false /* BE */);
  1210. dataView.setUint16(6, part2, false /* BE */);
  1211. }
  1212. // Microsoft's implementation in IE11 seems to never set key status to
  1213. // 'usable'. It is stuck forever at 'status-pending'. In spite of this,
  1214. // the keys do seem to be usable and content plays correctly.
  1215. // Bug filed: https://goo.gl/fcXEy1
  1216. // Microsoft has fixed the issue on Edge, but it remains in IE.
  1217. if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' &&
  1218. status == 'status-pending') {
  1219. status = 'usable';
  1220. }
  1221. if (status != 'status-pending') {
  1222. this.activeSessions_[i].loaded = true;
  1223. }
  1224. if (!found) {
  1225. // We can get a key status changed for a closed session after it has been
  1226. // removed from |activeSessions_|. If it is closed, none of its keys
  1227. // should be usable.
  1228. goog.asserts.assert(
  1229. status != 'usable', 'Usable keys found in closed session');
  1230. }
  1231. if (status == 'expired') {
  1232. hasExpiredKeys = true;
  1233. }
  1234. let keyIdHex = shaka.util.Uint8ArrayUtils.toHex(new Uint8Array(keyId));
  1235. this.keyStatusByKeyId_[keyIdHex] = status;
  1236. }.bind(this));
  1237. // If the session has expired, close it.
  1238. // Some CDMs do not have sub-second time resolution, so the key status may
  1239. // fire with hundreds of milliseconds left until the stated expiration time.
  1240. let msUntilExpiration = session.expiration - Date.now();
  1241. if (msUntilExpiration < 0 || (hasExpiredKeys && msUntilExpiration < 1000)) {
  1242. // If this is part of a remove(), we don't want to close the session until
  1243. // the update is complete. Otherwise, we will orphan the session.
  1244. if (found && !this.activeSessions_[i].updatePromise) {
  1245. shaka.log.debug('Session has expired', session);
  1246. this.activeSessions_.splice(i, 1);
  1247. session.close().catch(() => {}); // Silence uncaught rejection errors
  1248. }
  1249. }
  1250. const allSessionsLoaded = this.activeSessions_.every((s) => s.loaded);
  1251. if (!allSessionsLoaded) {
  1252. // Don't announce key statuses or resolve the "all loaded" promise until
  1253. // everything is loaded.
  1254. return;
  1255. }
  1256. this.allSessionsLoaded_.resolve();
  1257. // Batch up key status changes before checking them or notifying Player.
  1258. // This handles cases where the statuses of multiple sessions are set
  1259. // simultaneously by the browser before dispatching key status changes for
  1260. // each of them. By batching these up, we only send one status change event
  1261. // and at most one EXPIRED error on expiration.
  1262. this.keyStatusTimer_.schedule(shaka.media.DrmEngine.KEY_STATUS_BATCH_TIME_);
  1263. };
  1264. /**
  1265. * @private
  1266. */
  1267. shaka.media.DrmEngine.prototype.processKeyStatusChanges_ = function() {
  1268. // Copy the latest key statuses into the publicly-accessible map.
  1269. this.announcedKeyStatusByKeyId_ = {};
  1270. for (let keyId in this.keyStatusByKeyId_) {
  1271. this.announcedKeyStatusByKeyId_[keyId] = this.keyStatusByKeyId_[keyId];
  1272. }
  1273. // If all keys are expired, fire an error.
  1274. function isExpired(keyId, status) {
  1275. return status == 'expired';
  1276. }
  1277. const MapUtils = shaka.util.MapUtils;
  1278. // Note that every() is always true for an empty map,
  1279. // but we shouldn't fire an error for a lack of key status info.
  1280. let allExpired = !MapUtils.empty(this.announcedKeyStatusByKeyId_) &&
  1281. MapUtils.every(this.announcedKeyStatusByKeyId_, isExpired);
  1282. if (allExpired) {
  1283. this.onError_(new shaka.util.Error(
  1284. shaka.util.Error.Severity.CRITICAL,
  1285. shaka.util.Error.Category.DRM,
  1286. shaka.util.Error.Code.EXPIRED));
  1287. }
  1288. this.playerInterface_.onKeyStatus(this.announcedKeyStatusByKeyId_);
  1289. };
  1290. /**
  1291. * Returns true if the browser has recent EME APIs.
  1292. *
  1293. * @return {boolean}
  1294. */
  1295. shaka.media.DrmEngine.isBrowserSupported = function() {
  1296. let basic =
  1297. !!window.MediaKeys &&
  1298. !!window.navigator &&
  1299. !!window.navigator.requestMediaKeySystemAccess &&
  1300. !!window.MediaKeySystemAccess &&
  1301. !!window.MediaKeySystemAccess.prototype.getConfiguration;
  1302. return basic;
  1303. };
  1304. /**
  1305. * Returns a Promise to a map of EME support for well-known key systems.
  1306. *
  1307. * @return {!Promise.<!Object.<string, ?shakaExtern.DrmSupportType>>}
  1308. */
  1309. shaka.media.DrmEngine.probeSupport = function() {
  1310. goog.asserts.assert(shaka.media.DrmEngine.isBrowserSupported(),
  1311. 'Must have basic EME support');
  1312. let tests = [];
  1313. let testKeySystems = [
  1314. 'org.w3.clearkey',
  1315. 'com.widevine.alpha',
  1316. 'com.microsoft.playready',
  1317. 'com.apple.fps.2_0',
  1318. 'com.apple.fps.1_0',
  1319. 'com.apple.fps',
  1320. 'com.adobe.primetime'
  1321. ];
  1322. let basicVideoCapabilities = [
  1323. {contentType: 'video/mp4; codecs="avc1.42E01E"'},
  1324. {contentType: 'video/webm; codecs="vp8"'}
  1325. ];
  1326. let basicConfig = {
  1327. videoCapabilities: basicVideoCapabilities
  1328. };
  1329. let offlineConfig = {
  1330. videoCapabilities: basicVideoCapabilities,
  1331. persistentState: 'required',
  1332. sessionTypes: ['persistent-license']
  1333. };
  1334. // Try the offline config first, then fall back to the basic config.
  1335. let configs = [offlineConfig, basicConfig];
  1336. let support = {};
  1337. testKeySystems.forEach(function(keySystem) {
  1338. let p = navigator.requestMediaKeySystemAccess(keySystem, configs)
  1339. .then(function(access) {
  1340. // Edge doesn't return supported session types, but current versions
  1341. // do not support persistent-license. If sessionTypes is missing,
  1342. // assume no support for persistent-license.
  1343. // TODO: Polyfill Edge to return known supported session types.
  1344. // Edge bug: https://goo.gl/z0URJ0
  1345. let sessionTypes = access.getConfiguration().sessionTypes;
  1346. let persistentState = sessionTypes ?
  1347. sessionTypes.indexOf('persistent-license') >= 0 : false;
  1348. // Tizen 3.0 doesn't support persistent licenses, but reports that it
  1349. // does. It doesn't fail until you call update() with a license
  1350. // response, which is way too late.
  1351. // This is a work-around for #894.
  1352. if (navigator.userAgent.indexOf('Tizen 3') >= 0) {
  1353. persistentState = false;
  1354. }
  1355. support[keySystem] = {persistentState: persistentState};
  1356. return access.createMediaKeys();
  1357. }).catch(function() {
  1358. // Either the request failed or createMediaKeys failed.
  1359. // Either way, write null to the support object.
  1360. support[keySystem] = null;
  1361. });
  1362. tests.push(p);
  1363. });
  1364. return Promise.all(tests).then(function() {
  1365. return support;
  1366. });
  1367. };
  1368. /**
  1369. * @private
  1370. */
  1371. shaka.media.DrmEngine.prototype.onPlay_ = function() {
  1372. for (let i = 0; i < this.mediaKeyMessageEvents_.length; i++) {
  1373. this.sendLicenseRequest_(this.mediaKeyMessageEvents_[i]);
  1374. }
  1375. this.initialRequestsSent_ = true;
  1376. this.mediaKeyMessageEvents_ = [];
  1377. };
  1378. /**
  1379. * Checks if a variant is compatible with the key system.
  1380. * @param {!shakaExtern.Variant} variant
  1381. * @return {boolean}
  1382. **/
  1383. shaka.media.DrmEngine.prototype.isSupportedByKeySystem = function(variant) {
  1384. let keySystem = this.keySystem();
  1385. return variant.drmInfos.length == 0 ||
  1386. variant.drmInfos.some(function(drmInfo) {
  1387. return drmInfo.keySystem == keySystem;
  1388. });
  1389. };
  1390. /**
  1391. * Checks if two DrmInfos can be decrypted using the same key system.
  1392. * Clear content is considered compatible with every key system.
  1393. *
  1394. * @param {!Array.<!shakaExtern.DrmInfo>} drms1
  1395. * @param {!Array.<!shakaExtern.DrmInfo>} drms2
  1396. * @return {boolean}
  1397. */
  1398. shaka.media.DrmEngine.areDrmCompatible = function(drms1, drms2) {
  1399. if (!drms1.length || !drms2.length) return true;
  1400. return shaka.media.DrmEngine.getCommonDrmInfos(
  1401. drms1, drms2).length > 0;
  1402. };
  1403. /**
  1404. * Returns an array of drm infos that are present in both input arrays.
  1405. * If one of the arrays is empty, returns the other one since clear
  1406. * content is considered compatible with every drm info.
  1407. *
  1408. * @param {!Array.<!shakaExtern.DrmInfo>} drms1
  1409. * @param {!Array.<!shakaExtern.DrmInfo>} drms2
  1410. * @return {!Array.<!shakaExtern.DrmInfo>}
  1411. */
  1412. shaka.media.DrmEngine.getCommonDrmInfos = function(drms1, drms2) {
  1413. if (!drms1.length) return drms2;
  1414. if (!drms2.length) return drms1;
  1415. let commonDrms = [];
  1416. for (let i = 0; i < drms1.length; i++) {
  1417. for (let j = 0; j < drms2.length; j++) {
  1418. // This method is only called to compare drmInfos of a video and an audio
  1419. // adaptations, so we shouldn't have to worry about checking robustness.
  1420. if (drms1[i].keySystem == drms2[j].keySystem) {
  1421. let drm1 = drms1[i];
  1422. let drm2 = drms2[j];
  1423. let initData = [];
  1424. initData = initData.concat(drm1.initData || []);
  1425. initData = initData.concat(drm2.initData || []);
  1426. let keyIds = [];
  1427. keyIds = keyIds.concat(drm1.keyIds);
  1428. keyIds = keyIds.concat(drm2.keyIds);
  1429. let mergedDrm = {
  1430. keySystem: drm1.keySystem,
  1431. licenseServerUri: drm1.licenseServerUri || drm2.licenseServerUri,
  1432. distinctiveIdentifierRequired: drm1.distinctiveIdentifierRequired ||
  1433. drm2.distinctiveIdentifierRequired,
  1434. persistentStateRequired: drm1.persistentStateRequired ||
  1435. drm2.persistentStateRequired,
  1436. videoRobustness: drm1.videoRobustness || drm2.videoRobustness,
  1437. audioRobustness: drm1.audioRobustness || drm2.audioRobustness,
  1438. serverCertificate: drm1.serverCertificate || drm2.serverCertificate,
  1439. initData: initData,
  1440. keyIds: keyIds
  1441. };
  1442. commonDrms.push(mergedDrm);
  1443. break;
  1444. }
  1445. }
  1446. }
  1447. return commonDrms;
  1448. };
  1449. /**
  1450. * Called in an interval timer to poll the expiration times of the sessions. We
  1451. * don't get an event from EME when the expiration updates, so we poll it so we
  1452. * can fire an event when it happens.
  1453. * @private
  1454. */
  1455. shaka.media.DrmEngine.prototype.pollExpiration_ = function() {
  1456. this.activeSessions_.forEach(function(session) {
  1457. let old = session.oldExpiration;
  1458. let new_ = session.session.expiration;
  1459. if (isNaN(new_)) {
  1460. new_ = Infinity;
  1461. }
  1462. if (new_ != old) {
  1463. this.playerInterface_.onExpirationUpdated(
  1464. session.session.sessionId, new_);
  1465. session.oldExpiration = new_;
  1466. }
  1467. }.bind(this));
  1468. };
  1469. /**
  1470. * Close a drm session while accounting for a bug in Chrome. Sometimes the
  1471. * Promise returned by close() never resolves.
  1472. *
  1473. * See issue #1093 and https://crbug.com/690583.
  1474. *
  1475. * @param {!MediaKeySession} session
  1476. * @return {!Promise}
  1477. * @private
  1478. */
  1479. shaka.media.DrmEngine.closeSession_ = async function(session) {
  1480. /** @type {!Promise.<boolean>} */
  1481. const close = session.close().then(() => true);
  1482. /** @type {!Promise.<boolean>} */
  1483. const timeout = new Promise((resolve) => {
  1484. setTimeout(() => { resolve(false); },
  1485. shaka.media.DrmEngine.CLOSE_TIMEOUT_ * 1000);
  1486. });
  1487. /** @type {boolean} */
  1488. const wasSessionClosed = await Promise.race([close, timeout]);
  1489. if (!wasSessionClosed) {
  1490. shaka.log.warning('Timeout waiting for session close');
  1491. }
  1492. };
  1493. /**
  1494. * The amount of time, in seconds, we wait to consider a session closed.
  1495. * This allows us to work around Chrome bug https://crbug.com/690583.
  1496. * @private {number}
  1497. */
  1498. shaka.media.DrmEngine.CLOSE_TIMEOUT_ = 1;
  1499. /**
  1500. * The amount of time, in seconds, we wait to consider session loaded even if no
  1501. * key status information is available. This allows us to support browsers/CDMs
  1502. * without key statuses.
  1503. * @private {number}
  1504. */
  1505. shaka.media.DrmEngine.SESSION_LOAD_TIMEOUT_ = 5;
  1506. /**
  1507. * The amount of time, in seconds, we wait to batch up rapid key status changes.
  1508. * This allows us to avoid multiple expiration events in most cases.
  1509. * @private {number}
  1510. */
  1511. shaka.media.DrmEngine.KEY_STATUS_BATCH_TIME_ = 0.5;