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