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