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