Source: lib/media/drm_engine.js

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