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