Source: lib/media/drm_engine.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.DrmEngine');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.media.Transmuxer');
  10. goog.require('shaka.net.NetworkingEngine');
  11. goog.require('shaka.util.BufferUtils');
  12. goog.require('shaka.util.Destroyer');
  13. goog.require('shaka.util.Error');
  14. goog.require('shaka.util.EventManager');
  15. goog.require('shaka.util.FairPlayUtils');
  16. goog.require('shaka.util.FakeEvent');
  17. goog.require('shaka.util.IDestroyable');
  18. goog.require('shaka.util.Iterables');
  19. goog.require('shaka.util.Lazy');
  20. goog.require('shaka.util.ManifestParserUtils');
  21. goog.require('shaka.util.MapUtils');
  22. goog.require('shaka.util.MimeUtils');
  23. goog.require('shaka.util.Platform');
  24. goog.require('shaka.util.PublicPromise');
  25. goog.require('shaka.util.StreamUtils');
  26. goog.require('shaka.util.StringUtils');
  27. goog.require('shaka.util.Timer');
  28. goog.require('shaka.util.Uint8ArrayUtils');
  29. /** @implements {shaka.util.IDestroyable} */
  30. shaka.media.DrmEngine = class {
  31. /**
  32. * @param {shaka.media.DrmEngine.PlayerInterface} playerInterface
  33. * @param {number=} updateExpirationTime
  34. */
  35. constructor(playerInterface, updateExpirationTime = 1) {
  36. /** @private {?shaka.media.DrmEngine.PlayerInterface} */
  37. this.playerInterface_ = playerInterface;
  38. /** @private {!Set.<string>} */
  39. this.supportedTypes_ = new Set();
  40. /** @private {MediaKeys} */
  41. this.mediaKeys_ = null;
  42. /** @private {HTMLMediaElement} */
  43. this.video_ = null;
  44. /** @private {boolean} */
  45. this.initialized_ = false;
  46. /** @private {boolean} */
  47. this.initializedForStorage_ = false;
  48. /** @private {number} */
  49. this.licenseTimeSeconds_ = 0;
  50. /** @private {?shaka.extern.DrmInfo} */
  51. this.currentDrmInfo_ = null;
  52. /** @private {shaka.util.EventManager} */
  53. this.eventManager_ = new shaka.util.EventManager();
  54. /**
  55. * @private {!Map.<MediaKeySession,
  56. * shaka.media.DrmEngine.SessionMetaData>}
  57. */
  58. this.activeSessions_ = new Map();
  59. /** @private {!Array.<string>} */
  60. this.offlineSessionIds_ = [];
  61. /** @private {!shaka.util.PublicPromise} */
  62. this.allSessionsLoaded_ = new shaka.util.PublicPromise();
  63. /** @private {?shaka.extern.DrmConfiguration} */
  64. this.config_ = null;
  65. /** @private {function(!shaka.util.Error)} */
  66. this.onError_ = (err) => {
  67. this.allSessionsLoaded_.reject(err);
  68. playerInterface.onError(err);
  69. };
  70. /**
  71. * The most recent key status information we have.
  72. * We may not have announced this information to the outside world yet,
  73. * which we delay to batch up changes and avoid spurious "missing key"
  74. * errors.
  75. * @private {!Map.<string, string>}
  76. */
  77. this.keyStatusByKeyId_ = new Map();
  78. /**
  79. * The key statuses most recently announced to other classes.
  80. * We may have more up-to-date information being collected in
  81. * this.keyStatusByKeyId_, which has not been batched up and released yet.
  82. * @private {!Map.<string, string>}
  83. */
  84. this.announcedKeyStatusByKeyId_ = new Map();
  85. /** @private {shaka.util.Timer} */
  86. this.keyStatusTimer_ =
  87. new shaka.util.Timer(() => this.processKeyStatusChanges_());
  88. /** @private {boolean} */
  89. this.usePersistentLicenses_ = false;
  90. /** @private {!Array.<!MediaKeyMessageEvent>} */
  91. this.mediaKeyMessageEvents_ = [];
  92. /** @private {boolean} */
  93. this.initialRequestsSent_ = false;
  94. /** @private {?shaka.util.Timer} */
  95. this.expirationTimer_ = new shaka.util.Timer(() => {
  96. this.pollExpiration_();
  97. }).tickEvery(/* seconds= */ updateExpirationTime);
  98. // Add a catch to the Promise to avoid console logs about uncaught errors.
  99. const noop = () => {};
  100. this.allSessionsLoaded_.catch(noop);
  101. /** @const {!shaka.util.Destroyer} */
  102. this.destroyer_ = new shaka.util.Destroyer(() => this.destroyNow_());
  103. /** @private {boolean} */
  104. this.srcEquals_ = false;
  105. }
  106. /** @override */
  107. destroy() {
  108. return this.destroyer_.destroy();
  109. }
  110. /**
  111. * Destroy this instance of DrmEngine. This assumes that all other checks
  112. * about "if it should" have passed.
  113. *
  114. * @private
  115. */
  116. async destroyNow_() {
  117. // |eventManager_| should only be |null| after we call |destroy|. Destroy it
  118. // first so that we will stop responding to events.
  119. this.eventManager_.release();
  120. this.eventManager_ = null;
  121. // Since we are destroying ourselves, we don't want to react to the "all
  122. // sessions loaded" event.
  123. this.allSessionsLoaded_.reject();
  124. // Stop all timers. This will ensure that they do not start any new work
  125. // while we are destroying ourselves.
  126. this.expirationTimer_.stop();
  127. this.expirationTimer_ = null;
  128. this.keyStatusTimer_.stop();
  129. this.keyStatusTimer_ = null;
  130. // Close all open sessions.
  131. await this.closeOpenSessions_();
  132. // |video_| will be |null| if we never attached to a video element.
  133. if (this.video_) {
  134. goog.asserts.assert(!this.video_.src, 'video src must be removed first!');
  135. try {
  136. await this.video_.setMediaKeys(null);
  137. } catch (error) {
  138. // Ignore any failures while removing media keys from the video element.
  139. }
  140. this.video_ = null;
  141. }
  142. // Break references to everything else we hold internally.
  143. this.currentDrmInfo_ = null;
  144. this.supportedTypes_.clear();
  145. this.mediaKeys_ = null;
  146. this.offlineSessionIds_ = [];
  147. this.config_ = null;
  148. this.onError_ = () => {};
  149. this.playerInterface_ = null;
  150. this.srcEquals_ = false;
  151. }
  152. /**
  153. * Called by the Player to provide an updated configuration any time it
  154. * changes.
  155. * Must be called at least once before init().
  156. *
  157. * @param {shaka.extern.DrmConfiguration} config
  158. */
  159. configure(config) {
  160. this.config_ = config;
  161. }
  162. /**
  163. * @param {!boolean} value
  164. */
  165. setSrcEquals(value) {
  166. this.srcEquals_ = value;
  167. }
  168. /**
  169. * Initialize the drm engine for storing and deleting stored content.
  170. *
  171. * @param {!Array.<shaka.extern.Variant>} variants
  172. * The variants that are going to be stored.
  173. * @param {boolean} usePersistentLicenses
  174. * Whether or not persistent licenses should be requested and stored for
  175. * |manifest|.
  176. * @param {boolean=} useMediaCapabilities
  177. * @return {!Promise}
  178. */
  179. initForStorage(variants, usePersistentLicenses, useMediaCapabilities) {
  180. this.initializedForStorage_ = true;
  181. // There are two cases for this call:
  182. // 1. We are about to store a manifest - in that case, there are no offline
  183. // sessions and therefore no offline session ids.
  184. // 2. We are about to remove the offline sessions for this manifest - in
  185. // that case, we don't need to know about them right now either as
  186. // we will be told which ones to remove later.
  187. this.offlineSessionIds_ = [];
  188. // What we really need to know is whether or not they are expecting to use
  189. // persistent licenses.
  190. this.usePersistentLicenses_ = usePersistentLicenses;
  191. return this.init_(variants, !!useMediaCapabilities);
  192. }
  193. /**
  194. * Initialize the drm engine for playback operations.
  195. *
  196. * @param {!Array.<shaka.extern.Variant>} variants
  197. * The variants that we want to support playing.
  198. * @param {!Array.<string>} offlineSessionIds
  199. * @param {boolean=} useMediaCapabilities
  200. * @return {!Promise}
  201. */
  202. initForPlayback(variants, offlineSessionIds, useMediaCapabilities) {
  203. this.offlineSessionIds_ = offlineSessionIds;
  204. this.usePersistentLicenses_ = offlineSessionIds.length > 0;
  205. return this.init_(variants, !!useMediaCapabilities);
  206. }
  207. /**
  208. * Initializes the drm engine for removing persistent sessions. Only the
  209. * removeSession(s) methods will work correctly, creating new sessions may not
  210. * work as desired.
  211. *
  212. * @param {string} keySystem
  213. * @param {string} licenseServerUri
  214. * @param {Uint8Array} serverCertificate
  215. * @param {!Array.<MediaKeySystemMediaCapability>} audioCapabilities
  216. * @param {!Array.<MediaKeySystemMediaCapability>} videoCapabilities
  217. * @return {!Promise}
  218. */
  219. initForRemoval(keySystem, licenseServerUri, serverCertificate,
  220. audioCapabilities, videoCapabilities) {
  221. /** @type {!Map.<string, MediaKeySystemConfiguration>} */
  222. const configsByKeySystem = new Map();
  223. /** @type {MediaKeySystemConfiguration} */
  224. const config = {
  225. audioCapabilities: audioCapabilities,
  226. videoCapabilities: videoCapabilities,
  227. distinctiveIdentifier: 'optional',
  228. persistentState: 'required',
  229. sessionTypes: ['persistent-license'],
  230. label: keySystem, // Tracked by us, ignored by EME.
  231. };
  232. // TODO: refactor, don't stick drmInfos onto MediaKeySystemConfiguration
  233. config['drmInfos'] = [{ // Non-standard attribute, ignored by EME.
  234. keySystem: keySystem,
  235. licenseServerUri: licenseServerUri,
  236. distinctiveIdentifierRequired: false,
  237. persistentStateRequired: true,
  238. audioRobustness: '', // Not required by queryMediaKeys_
  239. videoRobustness: '', // Same
  240. serverCertificate: serverCertificate,
  241. initData: null,
  242. keyIds: null,
  243. }];
  244. configsByKeySystem.set(keySystem, config);
  245. return this.queryMediaKeys_(configsByKeySystem,
  246. /* variants= */ [],
  247. /* useMediaCapabilities= */ false);
  248. }
  249. /**
  250. * Negotiate for a key system and set up MediaKeys.
  251. * This will assume that both |usePersistentLicences_| and
  252. * |offlineSessionIds_| have been properly set.
  253. *
  254. * @param {!Array.<shaka.extern.Variant>} variants
  255. * The variants that we expect to operate with during the drm engine's
  256. * lifespan of the drm engine.
  257. * @param {boolean} useMediaCapabilities
  258. * @return {!Promise} Resolved if/when a key system has been chosen.
  259. * @private
  260. */
  261. async init_(variants, useMediaCapabilities) {
  262. goog.asserts.assert(this.config_,
  263. 'DrmEngine configure() must be called before init()!');
  264. // ClearKey config overrides the manifest DrmInfo if present. The variants
  265. // are modified so that filtering in Player still works.
  266. // This comes before hadDrmInfo because it influences the value of that.
  267. /** @type {?shaka.extern.DrmInfo} */
  268. const clearKeyDrmInfo = this.configureClearKey_();
  269. if (clearKeyDrmInfo) {
  270. for (const variant of variants) {
  271. if (variant.video) {
  272. variant.video.drmInfos = [clearKeyDrmInfo];
  273. }
  274. if (variant.audio) {
  275. variant.audio.drmInfos = [clearKeyDrmInfo];
  276. }
  277. }
  278. }
  279. const hadDrmInfo = variants.some((variant) => {
  280. if (variant.video && variant.video.drmInfos.length) {
  281. return true;
  282. }
  283. if (variant.audio && variant.audio.drmInfos.length) {
  284. return true;
  285. }
  286. return false;
  287. });
  288. // When preparing to play live streams, it is possible that we won't know
  289. // about some upcoming encrypted content. If we initialize the drm engine
  290. // with no key systems, we won't be able to play when the encrypted content
  291. // comes.
  292. //
  293. // To avoid this, we will set the drm engine up to work with as many key
  294. // systems as possible so that we will be ready.
  295. if (!hadDrmInfo) {
  296. const servers = shaka.util.MapUtils.asMap(this.config_.servers);
  297. shaka.media.DrmEngine.replaceDrmInfo_(variants, servers);
  298. }
  299. // Make sure all the drm infos are valid and filled in correctly.
  300. for (const variant of variants) {
  301. const drmInfos = this.getVariantDrmInfos_(variant);
  302. for (const info of drmInfos) {
  303. shaka.media.DrmEngine.fillInDrmInfoDefaults_(
  304. info,
  305. shaka.util.MapUtils.asMap(this.config_.servers),
  306. shaka.util.MapUtils.asMap(this.config_.advanced || {}));
  307. }
  308. }
  309. /** @type {!Map.<string, MediaKeySystemConfiguration>} */
  310. let configsByKeySystem;
  311. // We should get the decodingInfo results for the variants after we filling
  312. // in the drm infos, and before queryMediaKeys_().
  313. if (useMediaCapabilities) {
  314. await shaka.util.StreamUtils.getDecodingInfosForVariants(variants,
  315. this.usePersistentLicenses_, this.srcEquals_);
  316. } else {
  317. configsByKeySystem =
  318. this.prepareMediaKeyConfigsForVariants_(variants);
  319. }
  320. const hasDrmInfo = hadDrmInfo || Object.keys(this.config_.servers).length;
  321. // An unencrypted content is initialized.
  322. if (!hasDrmInfo) {
  323. this.initialized_ = true;
  324. return Promise.resolve();
  325. }
  326. const p = this.queryMediaKeys_(configsByKeySystem, variants,
  327. useMediaCapabilities);
  328. // TODO(vaage): Look into the assertion below. If we do not have any drm
  329. // info, we create drm info so that content can play if it has drm info
  330. // later.
  331. // However it is okay if we fail to initialize? If we fail to initialize,
  332. // it means we won't be able to play the later-encrypted content, which is
  333. // not okay.
  334. // If the content did not originally have any drm info, then it doesn't
  335. // matter if we fail to initialize the drm engine, because we won't need it
  336. // anyway.
  337. return hadDrmInfo ? p : p.catch(() => {});
  338. }
  339. /**
  340. * Attach MediaKeys to the video element and start processing events.
  341. * @param {HTMLMediaElement} video
  342. * @return {!Promise}
  343. */
  344. async attach(video) {
  345. if (!this.mediaKeys_) {
  346. // Unencrypted, or so we think. We listen for encrypted events in order
  347. // to warn when the stream is encrypted, even though the manifest does
  348. // not know it.
  349. // Don't complain about this twice, so just listenOnce().
  350. // FIXME: This is ineffective when a prefixed event is translated by our
  351. // polyfills, since those events are only caught and translated by a
  352. // MediaKeys instance. With clear content and no polyfilled MediaKeys
  353. // instance attached, you'll never see the 'encrypted' event on those
  354. // platforms (Safari).
  355. this.eventManager_.listenOnce(video, 'encrypted', (event) => {
  356. this.onError_(new shaka.util.Error(
  357. shaka.util.Error.Severity.CRITICAL,
  358. shaka.util.Error.Category.DRM,
  359. shaka.util.Error.Code.ENCRYPTED_CONTENT_WITHOUT_DRM_INFO));
  360. });
  361. return;
  362. }
  363. this.video_ = video;
  364. this.eventManager_.listenOnce(this.video_, 'play', () => this.onPlay_());
  365. if ('webkitCurrentPlaybackTargetIsWireless' in this.video_) {
  366. this.eventManager_.listen(this.video_,
  367. 'webkitcurrentplaybacktargetiswirelesschanged',
  368. () => this.closeOpenSessions_());
  369. }
  370. let setMediaKeys = this.video_.setMediaKeys(this.mediaKeys_);
  371. setMediaKeys = setMediaKeys.catch((exception) => {
  372. goog.asserts.assert(exception instanceof Error, 'Wrong error type!');
  373. return Promise.reject(new shaka.util.Error(
  374. shaka.util.Error.Severity.CRITICAL,
  375. shaka.util.Error.Category.DRM,
  376. shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO,
  377. exception.message));
  378. });
  379. await setMediaKeys;
  380. this.destroyer_.ensureNotDestroyed();
  381. this.createOrLoad();
  382. if (!this.currentDrmInfo_.initData.length &&
  383. !this.offlineSessionIds_.length) {
  384. // Explicit init data for any one stream or an offline session is
  385. // sufficient to suppress 'encrypted' events for all streams.
  386. const cb = (e) => this.newInitData(
  387. e.initDataType, shaka.util.BufferUtils.toUint8(e.initData));
  388. this.eventManager_.listen(this.video_, 'encrypted', cb);
  389. }
  390. }
  391. /**
  392. * Sets the server certificate based on the current DrmInfo.
  393. *
  394. * @return {!Promise}
  395. */
  396. async setServerCertificate() {
  397. goog.asserts.assert(this.initialized_,
  398. 'Must call init() before setServerCertificate');
  399. if (this.mediaKeys_ &&
  400. this.currentDrmInfo_ &&
  401. this.currentDrmInfo_.serverCertificate &&
  402. this.currentDrmInfo_.serverCertificate.length) {
  403. try {
  404. const supported = await this.mediaKeys_.setServerCertificate(
  405. this.currentDrmInfo_.serverCertificate);
  406. if (!supported) {
  407. shaka.log.warning('Server certificates are not supported by the ' +
  408. 'key system. The server certificate has been ' +
  409. 'ignored.');
  410. }
  411. } catch (exception) {
  412. throw new shaka.util.Error(
  413. shaka.util.Error.Severity.CRITICAL,
  414. shaka.util.Error.Category.DRM,
  415. shaka.util.Error.Code.INVALID_SERVER_CERTIFICATE,
  416. exception.message);
  417. }
  418. }
  419. }
  420. /**
  421. * Remove an offline session and delete it's data. This can only be called
  422. * after a successful call to |init|. This will wait until the
  423. * 'license-release' message is handled. The returned Promise will be rejected
  424. * if there is an error releasing the license.
  425. *
  426. * @param {string} sessionId
  427. * @return {!Promise}
  428. */
  429. async removeSession(sessionId) {
  430. goog.asserts.assert(this.mediaKeys_,
  431. 'Must call init() before removeSession');
  432. const session = await this.loadOfflineSession_(sessionId);
  433. // This will be null on error, such as session not found.
  434. if (!session) {
  435. shaka.log.v2('Ignoring attempt to remove missing session', sessionId);
  436. return;
  437. }
  438. // TODO: Consider adding a timeout to get the 'message' event.
  439. // Note that the 'message' event will get raised after the remove()
  440. // promise resolves.
  441. const tasks = [];
  442. const found = this.activeSessions_.get(session);
  443. if (found) {
  444. // This will force us to wait until the 'license-release' message has been
  445. // handled.
  446. found.updatePromise = new shaka.util.PublicPromise();
  447. tasks.push(found.updatePromise);
  448. }
  449. shaka.log.v2('Attempting to remove session', sessionId);
  450. tasks.push(session.remove());
  451. await Promise.all(tasks);
  452. this.activeSessions_.delete(session);
  453. }
  454. /**
  455. * Creates the sessions for the init data and waits for them to become ready.
  456. *
  457. * @return {!Promise}
  458. */
  459. createOrLoad() {
  460. // Create temp sessions.
  461. const initDatas =
  462. (this.currentDrmInfo_ ? this.currentDrmInfo_.initData : []) || [];
  463. for (const initDataOverride of initDatas) {
  464. this.newInitData(
  465. initDataOverride.initDataType, initDataOverride.initData);
  466. }
  467. // Load each session.
  468. for (const sessionId of this.offlineSessionIds_) {
  469. this.loadOfflineSession_(sessionId);
  470. }
  471. // If we have no sessions, we need to resolve the promise right now or else
  472. // it will never get resolved.
  473. if (!initDatas.length && !this.offlineSessionIds_.length) {
  474. this.allSessionsLoaded_.resolve();
  475. }
  476. return this.allSessionsLoaded_;
  477. }
  478. /**
  479. * Called when new initialization data is encountered. If this data hasn't
  480. * been seen yet, this will create a new session for it.
  481. *
  482. * @param {string} initDataType
  483. * @param {!Uint8Array} initData
  484. */
  485. newInitData(initDataType, initData) {
  486. // Suppress duplicate init data.
  487. // Note that some init data are extremely large and can't portably be used
  488. // as keys in a dictionary.
  489. const metadatas = this.activeSessions_.values();
  490. for (const metadata of metadatas) {
  491. // Tizen 2015 and 2016 models will send multiple webkitneedkey events
  492. // with the same init data. If the duplicates are supressed, playback
  493. // will stall without errors.
  494. if (shaka.util.BufferUtils.equal(initData, metadata.initData) &&
  495. !shaka.util.Platform.isTizen2()) {
  496. shaka.log.debug('Ignoring duplicate init data.');
  497. return;
  498. }
  499. }
  500. this.createTemporarySession_(initDataType, initData);
  501. }
  502. /** @return {boolean} */
  503. initialized() {
  504. return this.initialized_;
  505. }
  506. /**
  507. * @param {?shaka.extern.DrmInfo} drmInfo
  508. * @return {string} */
  509. static keySystem(drmInfo) {
  510. return drmInfo ? drmInfo.keySystem : '';
  511. }
  512. /**
  513. * @param {?string} keySystem
  514. * @return {boolean} */
  515. static isPlayReadyKeySystem(keySystem) {
  516. if (keySystem) {
  517. return !!keySystem.match(/^com\.(microsoft|chromecast)\.playready/);
  518. }
  519. return false;
  520. }
  521. /**
  522. * Check if DrmEngine (as initialized) will likely be able to support the
  523. * given content type.
  524. *
  525. * @param {string} contentType
  526. * @return {boolean}
  527. */
  528. willSupport(contentType) {
  529. // Edge 14 does not report correct capabilities. It will only report the
  530. // first MIME type even if the others are supported. To work around this,
  531. // we say that Edge supports everything.
  532. //
  533. // See https://github.com/shaka-project/shaka-player/issues/1495 for details.
  534. if (shaka.util.Platform.isLegacyEdge()) {
  535. return true;
  536. }
  537. contentType = contentType.toLowerCase();
  538. if (shaka.util.Platform.isTizen() &&
  539. contentType.includes('codecs="ac-3"')) {
  540. // Some Tizen devices seem to misreport AC-3 support. This works around
  541. // the issue, by falling back to EC-3, which seems to be supported on the
  542. // same devices and be correctly reported in all cases we have observed.
  543. // See https://github.com/shaka-project/shaka-player/issues/2989 for
  544. // details.
  545. const fallback = contentType.replace('ac-3', 'ec-3');
  546. return this.supportedTypes_.has(contentType) ||
  547. this.supportedTypes_.has(fallback);
  548. }
  549. return this.supportedTypes_.has(contentType);
  550. }
  551. /**
  552. * Returns the ID of the sessions currently active.
  553. *
  554. * @return {!Array.<string>}
  555. */
  556. getSessionIds() {
  557. const sessions = this.activeSessions_.keys();
  558. const ids = shaka.util.Iterables.map(sessions, (s) => s.sessionId);
  559. // TODO: Make |getSessionIds| return |Iterable| instead of |Array|.
  560. return Array.from(ids);
  561. }
  562. /**
  563. * Returns the next expiration time, or Infinity.
  564. * @return {number}
  565. */
  566. getExpiration() {
  567. // This will equal Infinity if there are no entries.
  568. let min = Infinity;
  569. const sessions = this.activeSessions_.keys();
  570. for (const session of sessions) {
  571. if (!isNaN(session.expiration)) {
  572. min = Math.min(min, session.expiration);
  573. }
  574. }
  575. return min;
  576. }
  577. /**
  578. * Returns the time spent on license requests during this session, or NaN.
  579. *
  580. * @return {number}
  581. */
  582. getLicenseTime() {
  583. if (this.licenseTimeSeconds_) {
  584. return this.licenseTimeSeconds_;
  585. }
  586. return NaN;
  587. }
  588. /**
  589. * Returns the DrmInfo that was used to initialize the current key system.
  590. *
  591. * @return {?shaka.extern.DrmInfo}
  592. */
  593. getDrmInfo() {
  594. return this.currentDrmInfo_;
  595. }
  596. /**
  597. * Return the media keys created from the current mediaKeySystemAccess.
  598. * @return {MediaKeys}
  599. */
  600. getMediaKeys() {
  601. return this.mediaKeys_;
  602. }
  603. /**
  604. * Returns the current key statuses.
  605. *
  606. * @return {!Object.<string, string>}
  607. */
  608. getKeyStatuses() {
  609. return shaka.util.MapUtils.asObject(this.announcedKeyStatusByKeyId_);
  610. }
  611. /**
  612. * @param {!Array.<shaka.extern.Variant>} variants
  613. * @see https://bit.ly/EmeConfig for MediaKeySystemConfiguration spec
  614. * @return {!Map.<string, MediaKeySystemConfiguration>}
  615. * @private
  616. */
  617. prepareMediaKeyConfigsForVariants_(variants) {
  618. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  619. // Get all the drm info so that we can avoid using nested loops when we just
  620. // need the drm info.
  621. const allDrmInfo = new Set();
  622. for (const variant of variants) {
  623. const drmInfos = this.getVariantDrmInfos_(variant);
  624. for (const info of drmInfos) {
  625. allDrmInfo.add(info);
  626. }
  627. }
  628. // Make sure all the drm infos are valid and filled in correctly.
  629. for (const info of allDrmInfo) {
  630. shaka.media.DrmEngine.fillInDrmInfoDefaults_(
  631. info,
  632. shaka.util.MapUtils.asMap(this.config_.servers),
  633. shaka.util.MapUtils.asMap(this.config_.advanced || {}));
  634. }
  635. const persistentState =
  636. this.usePersistentLicenses_ ? 'required' : 'optional';
  637. const sessionTypes =
  638. this.usePersistentLicenses_ ? ['persistent-license'] : ['temporary'];
  639. const configs = new Map();
  640. // Create a config entry for each key system.
  641. for (const info of allDrmInfo) {
  642. const config = {
  643. initDataTypes: ['cenc'],
  644. audioCapabilities: [],
  645. videoCapabilities: [],
  646. distinctiveIdentifier: 'optional',
  647. persistentState: persistentState,
  648. sessionTypes: sessionTypes,
  649. label: info.keySystem, // Tracked by us, ignored by EME.
  650. };
  651. // TODO: refactor, don't stick drmInfos onto MediaKeySystemConfiguration
  652. config['drmInfos'] = []; // Non-standard attribute, ignored by EME.
  653. // Multiple calls to |set| will still respect the insertion order of the
  654. // first call to |set| for a given key.
  655. configs.set(info.keySystem, config);
  656. }
  657. // Connect each key system with each stream using it.
  658. for (const variant of variants) {
  659. /** @type {?shaka.extern.Stream} */
  660. const audio = variant.audio;
  661. /** @type {?shaka.extern.Stream} */
  662. const video = variant.video;
  663. // Add the last bit of information to each config;
  664. for (const stream of [audio, video]) {
  665. if (!stream) {
  666. continue;
  667. }
  668. const mimeType = shaka.media.DrmEngine.computeMimeType_(stream);
  669. let fallbackMimeType = null;
  670. if (stream.codecs.toLowerCase() == 'ac-3' &&
  671. shaka.util.Platform.isTizen()) {
  672. // Some Tizen devices seem to misreport AC-3 support, but correctly
  673. // report EC-3 support. So query EC-3 as a fallback for AC-3.
  674. // See https://github.com/google/shaka-player/issues/2989 for details.
  675. fallbackMimeType = shaka.media.DrmEngine.computeMimeType_(stream,
  676. 'ec-3');
  677. }
  678. for (const info of stream.drmInfos) {
  679. const config = configs.get(info.keySystem);
  680. goog.asserts.assert(
  681. config,
  682. 'Any missing configs should have be filled in before.');
  683. // TODO: refactor, don't stick drmInfos onto
  684. // MediaKeySystemConfiguration
  685. config['drmInfos'].push(info);
  686. if (info.initData && info.initData.length) {
  687. config.initDataTypes = [
  688. ...new Set(
  689. info.initData.map((initData) => initData.initDataType)),
  690. ];
  691. }
  692. if (info.distinctiveIdentifierRequired) {
  693. config.distinctiveIdentifier = 'required';
  694. }
  695. if (info.persistentStateRequired) {
  696. config.persistentState = 'required';
  697. }
  698. if (info.sessionType) {
  699. config.sessionTypes = [info.sessionType];
  700. }
  701. const robustness = (stream.type == ContentType.AUDIO) ?
  702. info.audioRobustness : info.videoRobustness;
  703. /** @type {MediaKeySystemMediaCapability} */
  704. const capability = {
  705. robustness: robustness || '',
  706. contentType: mimeType,
  707. };
  708. if (stream.type == ContentType.AUDIO) {
  709. config.audioCapabilities.push(capability);
  710. } else {
  711. config.videoCapabilities.push(capability);
  712. }
  713. // This is how we work around some misbehaving platforms by adding
  714. // synthetic capability records using a fallback MIME type.
  715. if (fallbackMimeType) {
  716. /** @type {MediaKeySystemMediaCapability} */
  717. const fallbackCapability = {
  718. robustness: robustness || '',
  719. contentType: fallbackMimeType,
  720. };
  721. if (stream.type == ContentType.AUDIO) {
  722. config.audioCapabilities.push(fallbackCapability);
  723. } else {
  724. config.videoCapabilities.push(fallbackCapability);
  725. }
  726. }
  727. } // for (const info of stream.drmInfos)
  728. } // for (const stream of [audio, video])
  729. } // for (const variant of variants)
  730. return configs;
  731. }
  732. /**
  733. * @param {shaka.extern.Stream} stream
  734. * @param {string=} codecOverride
  735. * @return {string}
  736. * @private
  737. */
  738. static computeMimeType_(stream, codecOverride) {
  739. const realMimeType = shaka.util.MimeUtils.getFullType(stream.mimeType,
  740. codecOverride || stream.codecs);
  741. if (shaka.media.Transmuxer.isSupported(realMimeType)) {
  742. // This will be handled by the Transmuxer, so use the MIME type that the
  743. // Transmuxer will produce.
  744. return shaka.media.Transmuxer.convertTsCodecs(stream.type, realMimeType);
  745. }
  746. return realMimeType;
  747. }
  748. /**
  749. * @param {!Map.<string, MediaKeySystemConfiguration>} configsByKeySystem
  750. * A dictionary of configs, indexed by key system, with an iteration order
  751. * (insertion order) that reflects the preference for the application.
  752. * @param {!Array.<shaka.extern.Variant>} variants
  753. * @param {boolean} useMediaCapabilities
  754. * @return {!Promise} Resolved if/when a key system has been chosen.
  755. * @private
  756. */
  757. async queryMediaKeys_(configsByKeySystem, variants, useMediaCapabilities) {
  758. const drmInfosByKeySystem = new Map();
  759. const mediaKeySystemAccess = useMediaCapabilities ?
  760. this.getKeySystemAccessFromVariants_(variants, drmInfosByKeySystem) :
  761. await this.getKeySystemAccessByConfigs_(configsByKeySystem);
  762. if (!mediaKeySystemAccess) {
  763. throw new shaka.util.Error(
  764. shaka.util.Error.Severity.CRITICAL,
  765. shaka.util.Error.Category.DRM,
  766. shaka.util.Error.Code.REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE);
  767. }
  768. this.destroyer_.ensureNotDestroyed();
  769. try {
  770. // Get the set of supported content types from the audio and video
  771. // capabilities. Avoid duplicates so that it is easier to read what is
  772. // supported.
  773. this.supportedTypes_.clear();
  774. // Store the capabilities of the key system.
  775. const realConfig = mediaKeySystemAccess.getConfiguration();
  776. shaka.log.v2(
  777. 'Got MediaKeySystemAccess with configuration',
  778. realConfig);
  779. const audioCaps = realConfig.audioCapabilities || [];
  780. const videoCaps = realConfig.videoCapabilities || [];
  781. for (const cap of audioCaps) {
  782. this.supportedTypes_.add(cap.contentType.toLowerCase());
  783. }
  784. for (const cap of videoCaps) {
  785. this.supportedTypes_.add(cap.contentType.toLowerCase());
  786. }
  787. goog.asserts.assert(this.supportedTypes_.size,
  788. 'We should get at least one supported MIME type');
  789. if (useMediaCapabilities) {
  790. this.currentDrmInfo_ = this.createDrmInfoByInfos_(
  791. mediaKeySystemAccess.keySystem,
  792. drmInfosByKeySystem.get(mediaKeySystemAccess.keySystem));
  793. } else {
  794. this.currentDrmInfo_ = shaka.media.DrmEngine.createDrmInfoByConfigs_(
  795. mediaKeySystemAccess.keySystem,
  796. configsByKeySystem.get(mediaKeySystemAccess.keySystem));
  797. }
  798. if (!this.currentDrmInfo_.licenseServerUri) {
  799. throw new shaka.util.Error(
  800. shaka.util.Error.Severity.CRITICAL,
  801. shaka.util.Error.Category.DRM,
  802. shaka.util.Error.Code.NO_LICENSE_SERVER_GIVEN,
  803. this.currentDrmInfo_.keySystem);
  804. }
  805. const mediaKeys = await mediaKeySystemAccess.createMediaKeys();
  806. this.destroyer_.ensureNotDestroyed();
  807. shaka.log.info('Created MediaKeys object for key system',
  808. this.currentDrmInfo_.keySystem);
  809. this.mediaKeys_ = mediaKeys;
  810. this.initialized_ = true;
  811. await this.setServerCertificate();
  812. this.destroyer_.ensureNotDestroyed();
  813. } catch (exception) {
  814. this.destroyer_.ensureNotDestroyed(exception);
  815. // Don't rewrap a shaka.util.Error from earlier in the chain:
  816. this.currentDrmInfo_ = null;
  817. this.supportedTypes_.clear();
  818. if (exception instanceof shaka.util.Error) {
  819. throw exception;
  820. }
  821. // We failed to create MediaKeys. This generally shouldn't happen.
  822. throw new shaka.util.Error(
  823. shaka.util.Error.Severity.CRITICAL,
  824. shaka.util.Error.Category.DRM,
  825. shaka.util.Error.Code.FAILED_TO_CREATE_CDM,
  826. exception.message);
  827. }
  828. }
  829. /**
  830. * Get the MediaKeySystemAccess from the decodingInfos of the variants.
  831. * @param {!Array.<shaka.extern.Variant>} variants
  832. * @param {!Map.<string, !Array.<shaka.extern.DrmInfo>>} drmInfosByKeySystem
  833. * A dictionary of drmInfos, indexed by key system.
  834. * @return {MediaKeySystemAccess}
  835. * @private
  836. */
  837. getKeySystemAccessFromVariants_(variants, drmInfosByKeySystem) {
  838. for (const variant of variants) {
  839. // Get all the key systems in the variant that shouldHaveLicenseServer.
  840. const drmInfos = this.getVariantDrmInfos_(variant);
  841. for (const info of drmInfos) {
  842. if (!drmInfosByKeySystem.has(info.keySystem)) {
  843. drmInfosByKeySystem.set(info.keySystem, []);
  844. }
  845. drmInfosByKeySystem.get(info.keySystem).push(info);
  846. }
  847. }
  848. if (drmInfosByKeySystem.size == 1 && drmInfosByKeySystem.has('')) {
  849. throw new shaka.util.Error(
  850. shaka.util.Error.Severity.CRITICAL,
  851. shaka.util.Error.Category.DRM,
  852. shaka.util.Error.Code.NO_RECOGNIZED_KEY_SYSTEMS);
  853. }
  854. // Try key systems with configured license servers first. We only have to
  855. // try key systems without configured license servers for diagnostic
  856. // reasons, so that we can differentiate between "none of these key
  857. // systems are available" and "some are available, but you did not
  858. // configure them properly." The former takes precedence.
  859. for (const shouldHaveLicenseServer of [true, false]) {
  860. for (const variant of variants) {
  861. for (const decodingInfo of variant.decodingInfos) {
  862. if (!decodingInfo.supported || !decodingInfo.keySystemAccess) {
  863. continue;
  864. }
  865. const drmInfos =
  866. drmInfosByKeySystem.get(decodingInfo.keySystemAccess.keySystem);
  867. for (const info of drmInfos) {
  868. if (!!info.licenseServerUri == shouldHaveLicenseServer) {
  869. return decodingInfo.keySystemAccess;
  870. }
  871. }
  872. }
  873. }
  874. }
  875. return null;
  876. }
  877. /**
  878. * Get the MediaKeySystemAccess by querying requestMediaKeySystemAccess.
  879. * @param {!Map.<string, MediaKeySystemConfiguration>} configsByKeySystem
  880. * A dictionary of configs, indexed by key system, with an iteration order
  881. * (insertion order) that reflects the preference for the application.
  882. * @return {!Promise.<MediaKeySystemAccess>} Resolved if/when a
  883. * mediaKeySystemAccess has been chosen.
  884. * @private
  885. */
  886. async getKeySystemAccessByConfigs_(configsByKeySystem) {
  887. /** @type {MediaKeySystemAccess} */
  888. let mediaKeySystemAccess;
  889. if (configsByKeySystem.size == 1 && configsByKeySystem.has('')) {
  890. throw new shaka.util.Error(
  891. shaka.util.Error.Severity.CRITICAL,
  892. shaka.util.Error.Category.DRM,
  893. shaka.util.Error.Code.NO_RECOGNIZED_KEY_SYSTEMS);
  894. }
  895. // If there are no tracks of a type, these should be not present.
  896. // Otherwise the query will fail.
  897. for (const config of configsByKeySystem.values()) {
  898. if (config.audioCapabilities.length == 0) {
  899. delete config.audioCapabilities;
  900. }
  901. if (config.videoCapabilities.length == 0) {
  902. delete config.videoCapabilities;
  903. }
  904. }
  905. // Try key systems with configured license servers first. We only have to
  906. // try key systems without configured license servers for diagnostic
  907. // reasons, so that we can differentiate between "none of these key
  908. // systems are available" and "some are available, but you did not
  909. // configure them properly." The former takes precedence.
  910. // TODO: once MediaCap implementation is complete, this part can be
  911. // simplified or removed.
  912. for (const shouldHaveLicenseServer of [true, false]) {
  913. for (const keySystem of configsByKeySystem.keys()) {
  914. const config = configsByKeySystem.get(keySystem);
  915. // TODO: refactor, don't stick drmInfos onto
  916. // MediaKeySystemConfiguration
  917. const hasLicenseServer = config['drmInfos'].some((info) => {
  918. return !!info.licenseServerUri;
  919. });
  920. if (hasLicenseServer != shouldHaveLicenseServer) {
  921. continue;
  922. }
  923. try {
  924. mediaKeySystemAccess = // eslint-disable-next-line no-await-in-loop
  925. await navigator.requestMediaKeySystemAccess(
  926. keySystem, [config]);
  927. return mediaKeySystemAccess;
  928. } catch (error) {
  929. shaka.log.v2(
  930. 'Requesting', keySystem, 'failed with config',
  931. config, error);
  932. } // Suppress errors.
  933. this.destroyer_.ensureNotDestroyed();
  934. }
  935. }
  936. return mediaKeySystemAccess;
  937. }
  938. /**
  939. * Create a DrmInfo using configured clear keys.
  940. * The server URI will be a data URI which decodes to a clearkey license.
  941. * @return {?shaka.extern.DrmInfo} or null if clear keys are not configured.
  942. * @private
  943. * @see https://bit.ly/2K8gOnv for the spec on the clearkey license format.
  944. */
  945. configureClearKey_() {
  946. const clearKeys = shaka.util.MapUtils.asMap(this.config_.clearKeys);
  947. if (clearKeys.size == 0) {
  948. return null;
  949. }
  950. const StringUtils = shaka.util.StringUtils;
  951. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  952. const keys = [];
  953. const keyIds = [];
  954. clearKeys.forEach((keyHex, keyIdHex) => {
  955. const keyId = Uint8ArrayUtils.fromHex(keyIdHex);
  956. const key = Uint8ArrayUtils.fromHex(keyHex);
  957. const keyObj = {
  958. kty: 'oct',
  959. kid: Uint8ArrayUtils.toBase64(keyId, false),
  960. k: Uint8ArrayUtils.toBase64(key, false),
  961. };
  962. keys.push(keyObj);
  963. keyIds.push(keyObj.kid);
  964. });
  965. const jwkSet = {keys: keys};
  966. const license = JSON.stringify(jwkSet);
  967. // Use the keyids init data since is suggested by EME.
  968. // Suggestion: https://bit.ly/2JYcNTu
  969. // Format: https://www.w3.org/TR/eme-initdata-keyids/
  970. const initDataStr = JSON.stringify({'kids': keyIds});
  971. const initData =
  972. shaka.util.BufferUtils.toUint8(StringUtils.toUTF8(initDataStr));
  973. const initDatas = [{initData: initData, initDataType: 'keyids'}];
  974. return {
  975. keySystem: 'org.w3.clearkey',
  976. licenseServerUri: 'data:application/json;base64,' + window.btoa(license),
  977. distinctiveIdentifierRequired: false,
  978. persistentStateRequired: false,
  979. audioRobustness: '',
  980. videoRobustness: '',
  981. serverCertificate: null,
  982. sessionType: '',
  983. initData: initDatas,
  984. keyIds: new Set(keyIds),
  985. };
  986. }
  987. /**
  988. * @param {string} sessionId
  989. * @return {!Promise.<MediaKeySession>}
  990. * @private
  991. */
  992. async loadOfflineSession_(sessionId) {
  993. let session;
  994. const sessionType = 'persistent-license';
  995. try {
  996. shaka.log.v1('Attempting to load an offline session', sessionId);
  997. session = this.mediaKeys_.createSession(sessionType);
  998. } catch (exception) {
  999. const error = new shaka.util.Error(
  1000. shaka.util.Error.Severity.CRITICAL,
  1001. shaka.util.Error.Category.DRM,
  1002. shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
  1003. exception.message);
  1004. this.onError_(error);
  1005. return Promise.reject(error);
  1006. }
  1007. this.eventManager_.listen(session, 'message',
  1008. /** @type {shaka.util.EventManager.ListenerType} */(
  1009. (event) => this.onSessionMessage_(event)));
  1010. this.eventManager_.listen(session, 'keystatuseschange',
  1011. (event) => this.onKeyStatusesChange_(event));
  1012. const metadata = {
  1013. initData: null,
  1014. loaded: false,
  1015. oldExpiration: Infinity,
  1016. updatePromise: null,
  1017. type: sessionType,
  1018. };
  1019. this.activeSessions_.set(session, metadata);
  1020. try {
  1021. const present = await session.load(sessionId);
  1022. this.destroyer_.ensureNotDestroyed();
  1023. shaka.log.v2('Loaded offline session', sessionId, present);
  1024. if (!present) {
  1025. this.activeSessions_.delete(session);
  1026. this.onError_(new shaka.util.Error(
  1027. shaka.util.Error.Severity.CRITICAL,
  1028. shaka.util.Error.Category.DRM,
  1029. shaka.util.Error.Code.OFFLINE_SESSION_REMOVED));
  1030. return Promise.resolve();
  1031. }
  1032. // TODO: We should get a key status change event. Remove once Chrome CDM
  1033. // is fixed.
  1034. metadata.loaded = true;
  1035. if (this.areAllSessionsLoaded_()) {
  1036. this.allSessionsLoaded_.resolve();
  1037. }
  1038. return session;
  1039. } catch (error) {
  1040. this.destroyer_.ensureNotDestroyed(error);
  1041. this.activeSessions_.delete(session);
  1042. this.onError_(new shaka.util.Error(
  1043. shaka.util.Error.Severity.CRITICAL,
  1044. shaka.util.Error.Category.DRM,
  1045. shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
  1046. error.message));
  1047. }
  1048. return Promise.resolve();
  1049. }
  1050. /**
  1051. * @param {string} initDataType
  1052. * @param {!Uint8Array} initData
  1053. * @private
  1054. */
  1055. createTemporarySession_(initDataType, initData) {
  1056. goog.asserts.assert(this.mediaKeys_,
  1057. 'mediaKeys_ should be valid when creating temporary session.');
  1058. let session;
  1059. const sessionType = this.currentDrmInfo_.sessionType;
  1060. try {
  1061. shaka.log.info('Creating new', sessionType, 'session');
  1062. session = this.mediaKeys_.createSession(sessionType);
  1063. } catch (exception) {
  1064. this.onError_(new shaka.util.Error(
  1065. shaka.util.Error.Severity.CRITICAL,
  1066. shaka.util.Error.Category.DRM,
  1067. shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
  1068. exception.message));
  1069. return;
  1070. }
  1071. this.eventManager_.listen(session, 'message',
  1072. /** @type {shaka.util.EventManager.ListenerType} */(
  1073. (event) => this.onSessionMessage_(event)));
  1074. this.eventManager_.listen(session, 'keystatuseschange',
  1075. (event) => this.onKeyStatusesChange_(event));
  1076. const metadata = {
  1077. initData: initData,
  1078. loaded: false,
  1079. oldExpiration: Infinity,
  1080. updatePromise: null,
  1081. type: sessionType,
  1082. };
  1083. this.activeSessions_.set(session, metadata);
  1084. try {
  1085. initData = this.config_.initDataTransform(
  1086. initData, initDataType, this.currentDrmInfo_);
  1087. } catch (error) {
  1088. let shakaError = error;
  1089. if (!(error instanceof shaka.util.Error)) {
  1090. shakaError = new shaka.util.Error(
  1091. shaka.util.Error.Severity.CRITICAL,
  1092. shaka.util.Error.Category.DRM,
  1093. shaka.util.Error.Code.INIT_DATA_TRANSFORM_ERROR,
  1094. error);
  1095. }
  1096. this.onError_(shakaError);
  1097. return;
  1098. }
  1099. if (this.config_.logLicenseExchange) {
  1100. const str = shaka.util.Uint8ArrayUtils.toBase64(initData);
  1101. shaka.log.info('EME init data: type=', initDataType, 'data=', str);
  1102. }
  1103. session.generateRequest(initDataType, initData).catch((error) => {
  1104. if (this.destroyer_.destroyed()) {
  1105. return;
  1106. }
  1107. goog.asserts.assert(error instanceof Error, 'Wrong error type!');
  1108. this.activeSessions_.delete(session);
  1109. // This may be supplied by some polyfills.
  1110. /** @type {MediaKeyError} */
  1111. const errorCode = error['errorCode'];
  1112. let extended;
  1113. if (errorCode && errorCode.systemCode) {
  1114. extended = errorCode.systemCode;
  1115. if (extended < 0) {
  1116. extended += Math.pow(2, 32);
  1117. }
  1118. extended = '0x' + extended.toString(16);
  1119. }
  1120. this.onError_(new shaka.util.Error(
  1121. shaka.util.Error.Severity.CRITICAL,
  1122. shaka.util.Error.Category.DRM,
  1123. shaka.util.Error.Code.FAILED_TO_GENERATE_LICENSE_REQUEST,
  1124. error.message, error, extended));
  1125. });
  1126. }
  1127. /**
  1128. * @param {!Uint8Array} initData
  1129. * @param {string} initDataType
  1130. * @param {?shaka.extern.DrmInfo} drmInfo
  1131. * @return {!Uint8Array}
  1132. */
  1133. static defaultInitDataTransform(initData, initDataType, drmInfo) {
  1134. if (initDataType == 'skd') {
  1135. const cert = drmInfo.serverCertificate;
  1136. const contentId =
  1137. shaka.util.FairPlayUtils.defaultGetContentId(initData);
  1138. initData = shaka.util.FairPlayUtils.initDataTransform(
  1139. initData, contentId, cert);
  1140. }
  1141. return initData;
  1142. }
  1143. /**
  1144. * @param {!MediaKeyMessageEvent} event
  1145. * @private
  1146. */
  1147. onSessionMessage_(event) {
  1148. if (this.delayLicenseRequest_()) {
  1149. this.mediaKeyMessageEvents_.push(event);
  1150. } else {
  1151. this.sendLicenseRequest_(event);
  1152. }
  1153. }
  1154. /**
  1155. * @return {boolean}
  1156. * @private
  1157. */
  1158. delayLicenseRequest_() {
  1159. if (!this.video_) {
  1160. // If there's no video, don't delay the license request; i.e., in the case
  1161. // of offline storage.
  1162. return false;
  1163. }
  1164. return (this.config_.delayLicenseRequestUntilPlayed &&
  1165. this.video_.paused && !this.initialRequestsSent_);
  1166. }
  1167. /**
  1168. * Sends a license request.
  1169. * @param {!MediaKeyMessageEvent} event
  1170. * @private
  1171. */
  1172. async sendLicenseRequest_(event) {
  1173. /** @type {!MediaKeySession} */
  1174. const session = event.target;
  1175. shaka.log.v1(
  1176. 'Sending license request for session', session.sessionId, 'of type',
  1177. event.messageType);
  1178. if (this.config_.logLicenseExchange) {
  1179. const str = shaka.util.Uint8ArrayUtils.toBase64(event.message);
  1180. shaka.log.info('EME license request', str);
  1181. }
  1182. const metadata = this.activeSessions_.get(session);
  1183. let url = this.currentDrmInfo_.licenseServerUri;
  1184. const advancedConfig =
  1185. this.config_.advanced[this.currentDrmInfo_.keySystem];
  1186. if (event.messageType == 'individualization-request' && advancedConfig &&
  1187. advancedConfig.individualizationServer) {
  1188. url = advancedConfig.individualizationServer;
  1189. }
  1190. const requestType = shaka.net.NetworkingEngine.RequestType.LICENSE;
  1191. const request = shaka.net.NetworkingEngine.makeRequest(
  1192. [url], this.config_.retryParameters);
  1193. request.body = event.message;
  1194. request.method = 'POST';
  1195. request.licenseRequestType = event.messageType;
  1196. request.sessionId = session.sessionId;
  1197. // NOTE: allowCrossSiteCredentials can be set in a request filter.
  1198. if (shaka.media.DrmEngine.isPlayReadyKeySystem(
  1199. this.currentDrmInfo_.keySystem)) {
  1200. this.unpackPlayReadyRequest_(request);
  1201. }
  1202. const startTimeRequest = Date.now();
  1203. let response;
  1204. try {
  1205. const req = this.playerInterface_.netEngine.request(requestType, request);
  1206. response = await req.promise;
  1207. } catch (error) {
  1208. // Request failed!
  1209. goog.asserts.assert(error instanceof shaka.util.Error,
  1210. 'Wrong NetworkingEngine error type!');
  1211. const shakaErr = new shaka.util.Error(
  1212. shaka.util.Error.Severity.CRITICAL,
  1213. shaka.util.Error.Category.DRM,
  1214. shaka.util.Error.Code.LICENSE_REQUEST_FAILED,
  1215. error);
  1216. this.onError_(shakaErr);
  1217. if (metadata && metadata.updatePromise) {
  1218. metadata.updatePromise.reject(shakaErr);
  1219. }
  1220. return;
  1221. }
  1222. if (this.destroyer_.destroyed()) {
  1223. return;
  1224. }
  1225. this.licenseTimeSeconds_ += (Date.now() - startTimeRequest) / 1000;
  1226. if (this.config_.logLicenseExchange) {
  1227. const str = shaka.util.Uint8ArrayUtils.toBase64(response.data);
  1228. shaka.log.info('EME license response', str);
  1229. }
  1230. // Request succeeded, now pass the response to the CDM.
  1231. try {
  1232. shaka.log.v1('Updating session', session.sessionId);
  1233. await session.update(response.data);
  1234. } catch (error) {
  1235. // Session update failed!
  1236. const shakaErr = new shaka.util.Error(
  1237. shaka.util.Error.Severity.CRITICAL,
  1238. shaka.util.Error.Category.DRM,
  1239. shaka.util.Error.Code.LICENSE_RESPONSE_REJECTED,
  1240. error.message);
  1241. this.onError_(shakaErr);
  1242. if (metadata && metadata.updatePromise) {
  1243. metadata.updatePromise.reject(shakaErr);
  1244. }
  1245. return;
  1246. }
  1247. const updateEvent = new shaka.util.FakeEvent('drmsessionupdate');
  1248. this.playerInterface_.onEvent(updateEvent);
  1249. if (metadata) {
  1250. if (metadata.updatePromise) {
  1251. metadata.updatePromise.resolve();
  1252. }
  1253. // In case there are no key statuses, consider this session loaded
  1254. // after a reasonable timeout. It should definitely not take 5
  1255. // seconds to process a license.
  1256. const timer = new shaka.util.Timer(() => {
  1257. metadata.loaded = true;
  1258. if (this.areAllSessionsLoaded_()) {
  1259. this.allSessionsLoaded_.resolve();
  1260. }
  1261. });
  1262. timer.tickAfter(
  1263. /* seconds= */ shaka.media.DrmEngine.SESSION_LOAD_TIMEOUT_);
  1264. }
  1265. }
  1266. /**
  1267. * Unpacks PlayReady license requests. Modifies the request object.
  1268. * @param {shaka.extern.Request} request
  1269. * @private
  1270. */
  1271. unpackPlayReadyRequest_(request) {
  1272. // On Edge, the raw license message is UTF-16-encoded XML. We need
  1273. // to unpack the Challenge element (base64-encoded string containing the
  1274. // actual license request) and any HttpHeader elements (sent as request
  1275. // headers).
  1276. // Example XML:
  1277. // <PlayReadyKeyMessage type="LicenseAcquisition">
  1278. // <LicenseAcquisition Version="1">
  1279. // <Challenge encoding="base64encoded">{Base64Data}</Challenge>
  1280. // <HttpHeaders>
  1281. // <HttpHeader>
  1282. // <name>Content-Type</name>
  1283. // <value>text/xml; charset=utf-8</value>
  1284. // </HttpHeader>
  1285. // <HttpHeader>
  1286. // <name>SOAPAction</name>
  1287. // <value>http://schemas.microsoft.com/DRM/etc/etc</value>
  1288. // </HttpHeader>
  1289. // </HttpHeaders>
  1290. // </LicenseAcquisition>
  1291. // </PlayReadyKeyMessage>
  1292. const xml = shaka.util.StringUtils.fromUTF16(
  1293. request.body, /* littleEndian= */ true, /* noThrow= */ true);
  1294. if (!xml.includes('PlayReadyKeyMessage')) {
  1295. // This does not appear to be a wrapped message as on Edge. Some
  1296. // clients do not need this unwrapping, so we will assume this is one of
  1297. // them. Note that "xml" at this point probably looks like random
  1298. // garbage, since we interpreted UTF-8 as UTF-16.
  1299. shaka.log.debug('PlayReady request is already unwrapped.');
  1300. request.headers['Content-Type'] = 'text/xml; charset=utf-8';
  1301. return;
  1302. }
  1303. shaka.log.debug('Unwrapping PlayReady request.');
  1304. const dom = new DOMParser().parseFromString(xml, 'application/xml');
  1305. // Set request headers.
  1306. const headers = dom.getElementsByTagName('HttpHeader');
  1307. for (const header of headers) {
  1308. const name = header.getElementsByTagName('name')[0];
  1309. const value = header.getElementsByTagName('value')[0];
  1310. goog.asserts.assert(name && value, 'Malformed PlayReady headers!');
  1311. request.headers[name.textContent] = value.textContent;
  1312. }
  1313. // Unpack the base64-encoded challenge.
  1314. const challenge = dom.getElementsByTagName('Challenge')[0];
  1315. goog.asserts.assert(challenge, 'Malformed PlayReady challenge!');
  1316. goog.asserts.assert(challenge.getAttribute('encoding') == 'base64encoded',
  1317. 'Unexpected PlayReady challenge encoding!');
  1318. request.body = shaka.util.Uint8ArrayUtils.fromBase64(challenge.textContent);
  1319. }
  1320. /**
  1321. * @param {!Event} event
  1322. * @private
  1323. * @suppress {invalidCasts} to swap keyId and status
  1324. */
  1325. onKeyStatusesChange_(event) {
  1326. const session = /** @type {!MediaKeySession} */(event.target);
  1327. shaka.log.v2('Key status changed for session', session.sessionId);
  1328. const found = this.activeSessions_.get(session);
  1329. const keyStatusMap = session.keyStatuses;
  1330. let hasExpiredKeys = false;
  1331. keyStatusMap.forEach((status, keyId) => {
  1332. // The spec has changed a few times on the exact order of arguments here.
  1333. // As of 2016-06-30, Edge has the order reversed compared to the current
  1334. // EME spec. Given the back and forth in the spec, it may not be the only
  1335. // one. Try to detect this and compensate:
  1336. if (typeof keyId == 'string') {
  1337. const tmp = keyId;
  1338. keyId = /** @type {!ArrayBuffer} */(status);
  1339. status = /** @type {string} */(tmp);
  1340. }
  1341. // Microsoft's implementation in Edge seems to present key IDs as
  1342. // little-endian UUIDs, rather than big-endian or just plain array of
  1343. // bytes.
  1344. // standard: 6e 5a 1d 26 - 27 57 - 47 d7 - 80 46 ea a5 d1 d3 4b 5a
  1345. // on Edge: 26 1d 5a 6e - 57 27 - d7 47 - 80 46 ea a5 d1 d3 4b 5a
  1346. // Bug filed: https://bit.ly/2thuzXu
  1347. // NOTE that we skip this if byteLength != 16. This is used for Edge
  1348. // which uses single-byte dummy key IDs.
  1349. // However, unlike Edge and Chromecast, Tizen doesn't have this problem.
  1350. if (shaka.media.DrmEngine.isPlayReadyKeySystem(
  1351. this.currentDrmInfo_.keySystem) &&
  1352. keyId.byteLength == 16 &&
  1353. shaka.util.Platform.isEdge()) {
  1354. // Read out some fields in little-endian:
  1355. const dataView = shaka.util.BufferUtils.toDataView(keyId);
  1356. const part0 = dataView.getUint32(0, /* LE= */ true);
  1357. const part1 = dataView.getUint16(4, /* LE= */ true);
  1358. const part2 = dataView.getUint16(6, /* LE= */ true);
  1359. // Write it back in big-endian:
  1360. dataView.setUint32(0, part0, /* BE= */ false);
  1361. dataView.setUint16(4, part1, /* BE= */ false);
  1362. dataView.setUint16(6, part2, /* BE= */ false);
  1363. }
  1364. if (status != 'status-pending') {
  1365. found.loaded = true;
  1366. }
  1367. if (!found) {
  1368. // We can get a key status changed for a closed session after it has
  1369. // been removed from |activeSessions_|. If it is closed, none of its
  1370. // keys should be usable.
  1371. goog.asserts.assert(
  1372. status != 'usable', 'Usable keys found in closed session');
  1373. }
  1374. if (status == 'expired') {
  1375. hasExpiredKeys = true;
  1376. }
  1377. const keyIdHex = shaka.util.Uint8ArrayUtils.toHex(keyId);
  1378. this.keyStatusByKeyId_.set(keyIdHex, status);
  1379. });
  1380. // If the session has expired, close it.
  1381. // Some CDMs do not have sub-second time resolution, so the key status may
  1382. // fire with hundreds of milliseconds left until the stated expiration time.
  1383. const msUntilExpiration = session.expiration - Date.now();
  1384. if (msUntilExpiration < 0 || (hasExpiredKeys && msUntilExpiration < 1000)) {
  1385. // If this is part of a remove(), we don't want to close the session until
  1386. // the update is complete. Otherwise, we will orphan the session.
  1387. if (found && !found.updatePromise) {
  1388. shaka.log.debug('Session has expired', session.sessionId);
  1389. this.activeSessions_.delete(session);
  1390. session.close().catch(() => {}); // Silence uncaught rejection errors
  1391. }
  1392. }
  1393. if (!this.areAllSessionsLoaded_()) {
  1394. // Don't announce key statuses or resolve the "all loaded" promise until
  1395. // everything is loaded.
  1396. return;
  1397. }
  1398. this.allSessionsLoaded_.resolve();
  1399. // Batch up key status changes before checking them or notifying Player.
  1400. // This handles cases where the statuses of multiple sessions are set
  1401. // simultaneously by the browser before dispatching key status changes for
  1402. // each of them. By batching these up, we only send one status change event
  1403. // and at most one EXPIRED error on expiration.
  1404. this.keyStatusTimer_.tickAfter(
  1405. /* seconds= */ shaka.media.DrmEngine.KEY_STATUS_BATCH_TIME);
  1406. }
  1407. /** @private */
  1408. processKeyStatusChanges_() {
  1409. const privateMap = this.keyStatusByKeyId_;
  1410. const publicMap = this.announcedKeyStatusByKeyId_;
  1411. // Copy the latest key statuses into the publicly-accessible map.
  1412. publicMap.clear();
  1413. privateMap.forEach((status, keyId) => publicMap.set(keyId, status));
  1414. // If all keys are expired, fire an error. |every| is always true for an
  1415. // empty array but we shouldn't fire an error for a lack of key status info.
  1416. const statuses = Array.from(publicMap.values());
  1417. const allExpired = statuses.length &&
  1418. statuses.every((status) => status == 'expired');
  1419. if (allExpired) {
  1420. this.onError_(new shaka.util.Error(
  1421. shaka.util.Error.Severity.CRITICAL,
  1422. shaka.util.Error.Category.DRM,
  1423. shaka.util.Error.Code.EXPIRED));
  1424. }
  1425. this.playerInterface_.onKeyStatus(shaka.util.MapUtils.asObject(publicMap));
  1426. }
  1427. /**
  1428. * Returns true if the browser has recent EME APIs.
  1429. *
  1430. * @return {boolean}
  1431. */
  1432. static isBrowserSupported() {
  1433. const basic =
  1434. !!window.MediaKeys &&
  1435. !!window.navigator &&
  1436. !!window.navigator.requestMediaKeySystemAccess &&
  1437. !!window.MediaKeySystemAccess &&
  1438. // eslint-disable-next-line no-restricted-syntax
  1439. !!window.MediaKeySystemAccess.prototype.getConfiguration;
  1440. return basic;
  1441. }
  1442. /**
  1443. * Returns a Promise to a map of EME support for well-known key systems.
  1444. *
  1445. * @return {!Promise.<!Object.<string, ?shaka.extern.DrmSupportType>>}
  1446. */
  1447. static async probeSupport() {
  1448. goog.asserts.assert(shaka.media.DrmEngine.isBrowserSupported(),
  1449. 'Must have basic EME support');
  1450. const testKeySystems = [
  1451. 'org.w3.clearkey',
  1452. 'com.widevine.alpha',
  1453. 'com.microsoft.playready',
  1454. 'com.microsoft.playready.recommendation',
  1455. 'com.apple.fps.3_0',
  1456. 'com.apple.fps.2_0',
  1457. 'com.apple.fps.1_0',
  1458. 'com.apple.fps',
  1459. 'com.adobe.primetime',
  1460. ];
  1461. const basicVideoCapabilities = [
  1462. {contentType: 'video/mp4; codecs="avc1.42E01E"'},
  1463. {contentType: 'video/webm; codecs="vp8"'},
  1464. ];
  1465. const basicConfig = {
  1466. initDataTypes: ['cenc'],
  1467. videoCapabilities: basicVideoCapabilities,
  1468. };
  1469. const offlineConfig = {
  1470. videoCapabilities: basicVideoCapabilities,
  1471. persistentState: 'required',
  1472. sessionTypes: ['persistent-license'],
  1473. };
  1474. // Try the offline config first, then fall back to the basic config.
  1475. const configs = [offlineConfig, basicConfig];
  1476. /** @type {!Map.<string, ?shaka.extern.DrmSupportType>} */
  1477. const support = new Map();
  1478. const testSystem = async (keySystem) => {
  1479. try {
  1480. const access = await navigator.requestMediaKeySystemAccess(
  1481. keySystem, configs);
  1482. // Edge doesn't return supported session types, but current versions
  1483. // do not support persistent-license. If sessionTypes is missing,
  1484. // assume no support for persistent-license.
  1485. // TODO: Polyfill Edge to return known supported session types.
  1486. // Edge bug: https://bit.ly/2IeKzho
  1487. const sessionTypes = access.getConfiguration().sessionTypes;
  1488. let persistentState = sessionTypes ?
  1489. sessionTypes.includes('persistent-license') : false;
  1490. // Tizen 3.0 doesn't support persistent licenses, but reports that it
  1491. // does. It doesn't fail until you call update() with a license
  1492. // response, which is way too late.
  1493. // This is a work-around for #894.
  1494. if (shaka.util.Platform.isTizen3()) {
  1495. persistentState = false;
  1496. }
  1497. support.set(keySystem, {persistentState: persistentState});
  1498. await access.createMediaKeys();
  1499. } catch (e) {
  1500. // Either the request failed or createMediaKeys failed.
  1501. // Either way, write null to the support object.
  1502. support.set(keySystem, null);
  1503. }
  1504. };
  1505. // Test each key system.
  1506. const tests = testKeySystems.map((keySystem) => testSystem(keySystem));
  1507. await Promise.all(tests);
  1508. return shaka.util.MapUtils.asObject(support);
  1509. }
  1510. /** @private */
  1511. onPlay_() {
  1512. for (const event of this.mediaKeyMessageEvents_) {
  1513. this.sendLicenseRequest_(event);
  1514. }
  1515. this.initialRequestsSent_ = true;
  1516. this.mediaKeyMessageEvents_ = [];
  1517. }
  1518. /**
  1519. * Close a drm session while accounting for a bug in Chrome. Sometimes the
  1520. * Promise returned by close() never resolves.
  1521. *
  1522. * See issue #2741 and http://crbug.com/1108158.
  1523. * @param {!MediaKeySession} session
  1524. * @return {!Promise}
  1525. * @private
  1526. */
  1527. async closeSession_(session) {
  1528. const DrmEngine = shaka.media.DrmEngine;
  1529. const timeout = new Promise((resolve, reject) => {
  1530. const timer = new shaka.util.Timer(reject);
  1531. timer.tickAfter(DrmEngine.CLOSE_TIMEOUT_);
  1532. });
  1533. try {
  1534. await Promise.race([
  1535. Promise.all([session.close(), session.closed]),
  1536. timeout,
  1537. ]);
  1538. } catch (e) {
  1539. shaka.log.warning('Timeout waiting for session close');
  1540. }
  1541. }
  1542. /** @private */
  1543. async closeOpenSessions_() {
  1544. // Close all open sessions.
  1545. const openSessions = Array.from(this.activeSessions_.entries());
  1546. this.activeSessions_.clear();
  1547. // Close all sessions before we remove media keys from the video element.
  1548. await Promise.all(openSessions.map(async ([session, metadata]) => {
  1549. try {
  1550. /**
  1551. * Special case when a persistent-license session has been initiated,
  1552. * without being registered in the offline sessions at start-up.
  1553. * We should remove the session to prevent it from being orphaned after
  1554. * the playback session ends
  1555. */
  1556. if (!this.initializedForStorage_ &&
  1557. !this.offlineSessionIds_.includes(session.sessionId) &&
  1558. metadata.type === 'persistent-license') {
  1559. shaka.log.v1('Removing session', session.sessionId);
  1560. await session.remove();
  1561. } else {
  1562. shaka.log.v1('Closing session', session.sessionId, metadata);
  1563. await this.closeSession_(session);
  1564. }
  1565. } catch (error) {
  1566. // Ignore errors when closing the sessions. Closing a session that
  1567. // generated no key requests will throw an error.
  1568. shaka.log.error('Failed to close or remove the session', error);
  1569. }
  1570. }));
  1571. }
  1572. /**
  1573. * Check if a variant is likely to be supported by DrmEngine. This will err on
  1574. * the side of being too accepting and may not reject a variant that it will
  1575. * later fail to play.
  1576. *
  1577. * @param {!shaka.extern.Variant} variant
  1578. * @return {boolean}
  1579. */
  1580. supportsVariant(variant) {
  1581. /** @type {?shaka.extern.Stream} */
  1582. const audio = variant.audio;
  1583. /** @type {?shaka.extern.Stream} */
  1584. const video = variant.video;
  1585. if (audio && audio.encrypted) {
  1586. const audioContentType = shaka.media.DrmEngine.computeMimeType_(audio);
  1587. if (!this.willSupport(audioContentType)) {
  1588. return false;
  1589. }
  1590. }
  1591. if (video && video.encrypted) {
  1592. const videoContentType = shaka.media.DrmEngine.computeMimeType_(video);
  1593. if (!this.willSupport(videoContentType)) {
  1594. return false;
  1595. }
  1596. }
  1597. const keySystem = shaka.media.DrmEngine.keySystem(this.currentDrmInfo_);
  1598. const drmInfos = this.getVariantDrmInfos_(variant);
  1599. return drmInfos.length == 0 ||
  1600. drmInfos.some((drmInfo) => drmInfo.keySystem == keySystem);
  1601. }
  1602. /**
  1603. * Checks if two DrmInfos can be decrypted using the same key system.
  1604. * Clear content is considered compatible with every key system.
  1605. *
  1606. * @param {!Array.<!shaka.extern.DrmInfo>} drms1
  1607. * @param {!Array.<!shaka.extern.DrmInfo>} drms2
  1608. * @return {boolean}
  1609. */
  1610. static areDrmCompatible(drms1, drms2) {
  1611. if (!drms1.length || !drms2.length) {
  1612. return true;
  1613. }
  1614. return shaka.media.DrmEngine.getCommonDrmInfos(
  1615. drms1, drms2).length > 0;
  1616. }
  1617. /**
  1618. * Returns an array of drm infos that are present in both input arrays.
  1619. * If one of the arrays is empty, returns the other one since clear
  1620. * content is considered compatible with every drm info.
  1621. *
  1622. * @param {!Array.<!shaka.extern.DrmInfo>} drms1
  1623. * @param {!Array.<!shaka.extern.DrmInfo>} drms2
  1624. * @return {!Array.<!shaka.extern.DrmInfo>}
  1625. */
  1626. static getCommonDrmInfos(drms1, drms2) {
  1627. if (!drms1.length) {
  1628. return drms2;
  1629. }
  1630. if (!drms2.length) {
  1631. return drms1;
  1632. }
  1633. const commonDrms = [];
  1634. for (const drm1 of drms1) {
  1635. for (const drm2 of drms2) {
  1636. // This method is only called to compare drmInfos of a video and an
  1637. // audio adaptations, so we shouldn't have to worry about checking
  1638. // robustness.
  1639. if (drm1.keySystem == drm2.keySystem) {
  1640. /** @type {Array<shaka.extern.InitDataOverride>} */
  1641. let initData = [];
  1642. initData = initData.concat(drm1.initData || []);
  1643. initData = initData.concat(drm2.initData || []);
  1644. initData = initData.filter((d, i) => {
  1645. return d.keyId === undefined || i === initData.findIndex((d2) => {
  1646. return d2.keyId === d.keyId;
  1647. });
  1648. });
  1649. const keyIds = drm1.keyIds && drm2.keyIds ?
  1650. new Set([...drm1.keyIds, ...drm2.keyIds]) :
  1651. drm1.keyIds || drm2.keyIds;
  1652. const mergedDrm = {
  1653. keySystem: drm1.keySystem,
  1654. licenseServerUri: drm1.licenseServerUri || drm2.licenseServerUri,
  1655. distinctiveIdentifierRequired: drm1.distinctiveIdentifierRequired ||
  1656. drm2.distinctiveIdentifierRequired,
  1657. persistentStateRequired: drm1.persistentStateRequired ||
  1658. drm2.persistentStateRequired,
  1659. videoRobustness: drm1.videoRobustness || drm2.videoRobustness,
  1660. audioRobustness: drm1.audioRobustness || drm2.audioRobustness,
  1661. serverCertificate: drm1.serverCertificate || drm2.serverCertificate,
  1662. initData,
  1663. keyIds,
  1664. };
  1665. commonDrms.push(mergedDrm);
  1666. break;
  1667. }
  1668. }
  1669. }
  1670. return commonDrms;
  1671. }
  1672. /**
  1673. * Concat the audio and video drmInfos in a variant.
  1674. * @param {shaka.extern.Variant} variant
  1675. * @return {!Array.<!shaka.extern.DrmInfo>}
  1676. * @private
  1677. */
  1678. getVariantDrmInfos_(variant) {
  1679. const videoDrmInfos = variant.video ? variant.video.drmInfos : [];
  1680. const audioDrmInfos = variant.audio ? variant.audio.drmInfos : [];
  1681. return videoDrmInfos.concat(audioDrmInfos);
  1682. }
  1683. /**
  1684. * Called in an interval timer to poll the expiration times of the sessions.
  1685. * We don't get an event from EME when the expiration updates, so we poll it
  1686. * so we can fire an event when it happens.
  1687. * @private
  1688. */
  1689. pollExpiration_() {
  1690. this.activeSessions_.forEach((metadata, session) => {
  1691. const oldTime = metadata.oldExpiration;
  1692. let newTime = session.expiration;
  1693. if (isNaN(newTime)) {
  1694. newTime = Infinity;
  1695. }
  1696. if (newTime != oldTime) {
  1697. this.playerInterface_.onExpirationUpdated(session.sessionId, newTime);
  1698. metadata.oldExpiration = newTime;
  1699. }
  1700. });
  1701. }
  1702. /**
  1703. * @return {boolean}
  1704. * @private
  1705. */
  1706. areAllSessionsLoaded_() {
  1707. const metadatas = this.activeSessions_.values();
  1708. return shaka.util.Iterables.every(metadatas, (data) => data.loaded);
  1709. }
  1710. /**
  1711. * Replace the drm info used in each variant in |variants| to reflect each
  1712. * key service in |keySystems|.
  1713. *
  1714. * @param {!Array.<shaka.extern.Variant>} variants
  1715. * @param {!Map.<string, string>} keySystems
  1716. * @private
  1717. */
  1718. static replaceDrmInfo_(variants, keySystems) {
  1719. const drmInfos = [];
  1720. keySystems.forEach((uri, keySystem) => {
  1721. drmInfos.push({
  1722. keySystem: keySystem,
  1723. licenseServerUri: uri,
  1724. distinctiveIdentifierRequired: false,
  1725. persistentStateRequired: false,
  1726. audioRobustness: '',
  1727. videoRobustness: '',
  1728. serverCertificate: null,
  1729. initData: [],
  1730. keyIds: new Set(),
  1731. });
  1732. });
  1733. for (const variant of variants) {
  1734. if (variant.video) {
  1735. variant.video.drmInfos = drmInfos;
  1736. }
  1737. if (variant.audio) {
  1738. variant.audio.drmInfos = drmInfos;
  1739. }
  1740. }
  1741. }
  1742. /**
  1743. * Creates a DrmInfo object describing the settings used to initialize the
  1744. * engine.
  1745. *
  1746. * @param {string} keySystem
  1747. * @param {!Array.<shaka.extern.DrmInfo>} drmInfos
  1748. * @return {shaka.extern.DrmInfo}
  1749. *
  1750. * @private
  1751. */
  1752. createDrmInfoByInfos_(keySystem, drmInfos) {
  1753. /** @type {!Array.<string>} */
  1754. const licenseServers = [];
  1755. /** @type {!Array.<!Uint8Array>} */
  1756. const serverCerts = [];
  1757. /** @type {!Array.<!shaka.extern.InitDataOverride>} */
  1758. const initDatas = [];
  1759. /** @type {!Set.<string>} */
  1760. const keyIds = new Set();
  1761. shaka.media.DrmEngine.processDrmInfos_(drmInfos, licenseServers,
  1762. serverCerts, initDatas, keyIds);
  1763. if (serverCerts.length > 1) {
  1764. shaka.log.warning('Multiple unique server certificates found! ' +
  1765. 'Only the first will be used.');
  1766. }
  1767. if (licenseServers.length > 1) {
  1768. shaka.log.warning('Multiple unique license server URIs found! ' +
  1769. 'Only the first will be used.');
  1770. }
  1771. const defaultSessionType =
  1772. this.usePersistentLicenses_ ? 'persistent-license' : 'temporary';
  1773. /** @type {shaka.extern.DrmInfo} */
  1774. const res = {
  1775. keySystem,
  1776. licenseServerUri: licenseServers[0],
  1777. distinctiveIdentifierRequired: drmInfos[0].distinctiveIdentifierRequired,
  1778. persistentStateRequired: drmInfos[0].persistentStateRequired,
  1779. sessionType: drmInfos[0].sessionType || defaultSessionType,
  1780. audioRobustness: drmInfos[0].audioRobustness || '',
  1781. videoRobustness: drmInfos[0].videoRobustness || '',
  1782. serverCertificate: serverCerts[0],
  1783. initData: initDatas,
  1784. keyIds,
  1785. };
  1786. for (const info of drmInfos) {
  1787. if (info.distinctiveIdentifierRequired) {
  1788. res.distinctiveIdentifierRequired = info.distinctiveIdentifierRequired;
  1789. }
  1790. if (info.persistentStateRequired) {
  1791. res.persistentStateRequired = info.persistentStateRequired;
  1792. }
  1793. }
  1794. return res;
  1795. }
  1796. /**
  1797. * Creates a DrmInfo object describing the settings used to initialize the
  1798. * engine.
  1799. *
  1800. * @param {string} keySystem
  1801. * @param {MediaKeySystemConfiguration} config
  1802. * @return {shaka.extern.DrmInfo}
  1803. *
  1804. * @private
  1805. */
  1806. static createDrmInfoByConfigs_(keySystem, config) {
  1807. /** @type {!Array.<string>} */
  1808. const licenseServers = [];
  1809. /** @type {!Array.<!Uint8Array>} */
  1810. const serverCerts = [];
  1811. /** @type {!Array.<!shaka.extern.InitDataOverride>} */
  1812. const initDatas = [];
  1813. /** @type {!Set.<string>} */
  1814. const keyIds = new Set();
  1815. // TODO: refactor, don't stick drmInfos onto MediaKeySystemConfiguration
  1816. shaka.media.DrmEngine.processDrmInfos_(
  1817. config['drmInfos'], licenseServers, serverCerts, initDatas, keyIds);
  1818. if (serverCerts.length > 1) {
  1819. shaka.log.warning('Multiple unique server certificates found! ' +
  1820. 'Only the first will be used.');
  1821. }
  1822. if (licenseServers.length > 1) {
  1823. shaka.log.warning('Multiple unique license server URIs found! ' +
  1824. 'Only the first will be used.');
  1825. }
  1826. // TODO: This only works when all DrmInfo have the same robustness.
  1827. const audioRobustness =
  1828. config.audioCapabilities ? config.audioCapabilities[0].robustness : '';
  1829. const videoRobustness =
  1830. config.videoCapabilities ? config.videoCapabilities[0].robustness : '';
  1831. const distinctiveIdentifier = config.distinctiveIdentifier;
  1832. return {
  1833. keySystem,
  1834. licenseServerUri: licenseServers[0],
  1835. distinctiveIdentifierRequired: (distinctiveIdentifier == 'required'),
  1836. persistentStateRequired: (config.persistentState == 'required'),
  1837. sessionType: config.sessionTypes[0] || 'temporary',
  1838. audioRobustness: audioRobustness || '',
  1839. videoRobustness: videoRobustness || '',
  1840. serverCertificate: serverCerts[0],
  1841. initData: initDatas,
  1842. keyIds,
  1843. };
  1844. }
  1845. /**
  1846. * Extract license server, server cert, and init data from |drmInfos|, taking
  1847. * care to eliminate duplicates.
  1848. *
  1849. * @param {!Array.<shaka.extern.DrmInfo>} drmInfos
  1850. * @param {!Array.<string>} licenseServers
  1851. * @param {!Array.<!Uint8Array>} serverCerts
  1852. * @param {!Array.<!shaka.extern.InitDataOverride>} initDatas
  1853. * @param {!Set.<string>} keyIds
  1854. * @private
  1855. */
  1856. static processDrmInfos_(
  1857. drmInfos, licenseServers, serverCerts, initDatas, keyIds) {
  1858. /** @type {function(shaka.extern.InitDataOverride,
  1859. * shaka.extern.InitDataOverride):boolean} */
  1860. const initDataOverrideEqual = (a, b) => {
  1861. if (a.keyId && a.keyId == b.keyId) {
  1862. // Two initDatas with the same keyId are considered to be the same,
  1863. // unless that "same keyId" is null.
  1864. return true;
  1865. }
  1866. return a.initDataType == b.initDataType &&
  1867. shaka.util.BufferUtils.equal(a.initData, b.initData);
  1868. };
  1869. for (const drmInfo of drmInfos) {
  1870. // Build an array of unique license servers.
  1871. if (!licenseServers.includes(drmInfo.licenseServerUri)) {
  1872. licenseServers.push(drmInfo.licenseServerUri);
  1873. }
  1874. // Build an array of unique server certs.
  1875. if (drmInfo.serverCertificate) {
  1876. const found = serverCerts.some(
  1877. (cert) => shaka.util.BufferUtils.equal(
  1878. cert, drmInfo.serverCertificate));
  1879. if (!found) {
  1880. serverCerts.push(drmInfo.serverCertificate);
  1881. }
  1882. }
  1883. // Build an array of unique init datas.
  1884. if (drmInfo.initData) {
  1885. for (const initDataOverride of drmInfo.initData) {
  1886. const found = initDatas.some(
  1887. (initData) =>
  1888. initDataOverrideEqual(initData, initDataOverride));
  1889. if (!found) {
  1890. initDatas.push(initDataOverride);
  1891. }
  1892. }
  1893. }
  1894. if (drmInfo.keyIds) {
  1895. for (const keyId of drmInfo.keyIds) {
  1896. keyIds.add(keyId);
  1897. }
  1898. }
  1899. }
  1900. }
  1901. /**
  1902. * Use |servers| and |advancedConfigs| to fill in missing values in drmInfo
  1903. * that the parser left blank. Before working with any drmInfo, it should be
  1904. * passed through here as it is uncommon for drmInfo to be complete when
  1905. * fetched from a manifest because most manifest formats do not have the
  1906. * required information.
  1907. *
  1908. * @param {shaka.extern.DrmInfo} drmInfo
  1909. * @param {!Map.<string, string>} servers
  1910. * @param {!Map.<string, shaka.extern.AdvancedDrmConfiguration>}
  1911. * advancedConfigs
  1912. * @private
  1913. */
  1914. static fillInDrmInfoDefaults_(drmInfo, servers, advancedConfigs) {
  1915. if (!drmInfo.keySystem) {
  1916. // This is a placeholder from the manifest parser for an unrecognized key
  1917. // system. Skip this entry, to avoid logging nonsensical errors.
  1918. return;
  1919. }
  1920. // The order of preference for drmInfo:
  1921. // 1. Clear Key config, used for debugging, should override everything else.
  1922. // (The application can still specify a clearkey license server.)
  1923. // 2. Application-configured servers, if any are present, should override
  1924. // anything from the manifest. Nuance: if key system A is in the
  1925. // manifest and key system B is in the player config, only B will be
  1926. // used, not A.
  1927. // 3. Manifest-provided license servers are only used if nothing else is
  1928. // specified.
  1929. // This is important because it allows the application a clear way to
  1930. // indicate which DRM systems should be used on platforms with multiple DRM
  1931. // systems.
  1932. // The only way to get license servers from the manifest is not to specify
  1933. // any in your player config.
  1934. if (drmInfo.keySystem == 'org.w3.clearkey' && drmInfo.licenseServerUri) {
  1935. // Preference 1: Clear Key with pre-configured keys will have a data URI
  1936. // assigned as its license server. Don't change anything.
  1937. return;
  1938. } else if (servers.size) {
  1939. // Preference 2: If anything is configured at the application level,
  1940. // override whatever was in the manifest.
  1941. const server = servers.get(drmInfo.keySystem) || '';
  1942. drmInfo.licenseServerUri = server;
  1943. } else {
  1944. // Preference 3: Keep whatever we had in drmInfo.licenseServerUri, which
  1945. // comes from the manifest.
  1946. }
  1947. if (!drmInfo.keyIds) {
  1948. drmInfo.keyIds = new Set();
  1949. }
  1950. const advancedConfig = advancedConfigs.get(drmInfo.keySystem);
  1951. if (advancedConfig) {
  1952. if (!drmInfo.distinctiveIdentifierRequired) {
  1953. drmInfo.distinctiveIdentifierRequired =
  1954. advancedConfig.distinctiveIdentifierRequired;
  1955. }
  1956. if (!drmInfo.persistentStateRequired) {
  1957. drmInfo.persistentStateRequired =
  1958. advancedConfig.persistentStateRequired;
  1959. }
  1960. if (!drmInfo.videoRobustness) {
  1961. drmInfo.videoRobustness = advancedConfig.videoRobustness;
  1962. }
  1963. if (!drmInfo.audioRobustness) {
  1964. drmInfo.audioRobustness = advancedConfig.audioRobustness;
  1965. }
  1966. if (!drmInfo.serverCertificate) {
  1967. drmInfo.serverCertificate = advancedConfig.serverCertificate;
  1968. }
  1969. if (advancedConfig.sessionType) {
  1970. drmInfo.sessionType = advancedConfig.sessionType;
  1971. }
  1972. }
  1973. // Chromecast has a variant of PlayReady that uses a different key
  1974. // system ID. Since manifest parsers convert the standard PlayReady
  1975. // UUID to the standard PlayReady key system ID, here we will switch
  1976. // to the Chromecast version if we are running on that platform.
  1977. // Note that this must come after fillInDrmInfoDefaults_, since the
  1978. // player config uses the standard PlayReady ID for license server
  1979. // configuration.
  1980. if (window.cast && window.cast.__platform__) {
  1981. if (drmInfo.keySystem == 'com.microsoft.playready') {
  1982. drmInfo.keySystem = 'com.chromecast.playready';
  1983. }
  1984. }
  1985. }
  1986. };
  1987. /**
  1988. * @typedef {{
  1989. * loaded: boolean,
  1990. * initData: Uint8Array,
  1991. * oldExpiration: number,
  1992. * type: string,
  1993. * updatePromise: shaka.util.PublicPromise
  1994. * }}
  1995. *
  1996. * @description A record to track sessions and suppress duplicate init data.
  1997. * @property {boolean} loaded
  1998. * True once the key status has been updated (to a non-pending state). This
  1999. * does not mean the session is 'usable'.
  2000. * @property {Uint8Array} initData
  2001. * The init data used to create the session.
  2002. * @property {!MediaKeySession} session
  2003. * The session object.
  2004. * @property {number} oldExpiration
  2005. * The expiration of the session on the last check. This is used to fire
  2006. * an event when it changes.
  2007. * @property {string} type
  2008. * The session type
  2009. * @property {shaka.util.PublicPromise} updatePromise
  2010. * An optional Promise that will be resolved/rejected on the next update()
  2011. * call. This is used to track the 'license-release' message when calling
  2012. * remove().
  2013. */
  2014. shaka.media.DrmEngine.SessionMetaData;
  2015. /**
  2016. * @typedef {{
  2017. * netEngine: !shaka.net.NetworkingEngine,
  2018. * onError: function(!shaka.util.Error),
  2019. * onKeyStatus: function(!Object.<string,string>),
  2020. * onExpirationUpdated: function(string,number),
  2021. * onEvent: function(!Event)
  2022. * }}
  2023. *
  2024. * @property {shaka.net.NetworkingEngine} netEngine
  2025. * The NetworkingEngine instance to use. The caller retains ownership.
  2026. * @property {function(!shaka.util.Error)} onError
  2027. * Called when an error occurs. If the error is recoverable (see
  2028. * {@link shaka.util.Error}) then the caller may invoke either
  2029. * StreamingEngine.switch*() or StreamingEngine.seeked() to attempt recovery.
  2030. * @property {function(!Object.<string,string>)} onKeyStatus
  2031. * Called when key status changes. The argument is a map of hex key IDs to
  2032. * statuses.
  2033. * @property {function(string,number)} onExpirationUpdated
  2034. * Called when the session expiration value changes.
  2035. * @property {function(!Event)} onEvent
  2036. * Called when an event occurs that should be sent to the app.
  2037. */
  2038. shaka.media.DrmEngine.PlayerInterface;
  2039. /**
  2040. * The amount of time, in seconds, we wait to consider a session closed.
  2041. * This allows us to work around Chrome bug https://crbug.com/1108158.
  2042. * @private {number}
  2043. */
  2044. shaka.media.DrmEngine.CLOSE_TIMEOUT_ = 1;
  2045. /**
  2046. * The amount of time, in seconds, we wait to consider session loaded even if no
  2047. * key status information is available. This allows us to support browsers/CDMs
  2048. * without key statuses.
  2049. * @private {number}
  2050. */
  2051. shaka.media.DrmEngine.SESSION_LOAD_TIMEOUT_ = 5;
  2052. /**
  2053. * The amount of time, in seconds, we wait to batch up rapid key status changes.
  2054. * This allows us to avoid multiple expiration events in most cases.
  2055. * @type {number}
  2056. */
  2057. shaka.media.DrmEngine.KEY_STATUS_BATCH_TIME = 0.5;
  2058. /**
  2059. * Contains the suggested "default" key ID used by EME polyfills that do not
  2060. * have a per-key key status. See w3c/encrypted-media#32.
  2061. * @type {!shaka.util.Lazy.<!ArrayBuffer>}
  2062. */
  2063. shaka.media.DrmEngine.DUMMY_KEY_ID = new shaka.util.Lazy(
  2064. () => shaka.util.BufferUtils.toArrayBuffer(new Uint8Array([0])));