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