Home Reference Source

src/controller/eme-controller.ts

  1. /**
  2. * @author Stephan Hesse <disparat@gmail.com> | <tchakabam@gmail.com>
  3. *
  4. * DRM support for Hls.js
  5. */
  6. import { Events } from '../events';
  7. import { ErrorTypes, ErrorDetails } from '../errors';
  8.  
  9. import { logger } from '../utils/logger';
  10. import type { DRMSystemOptions, EMEControllerConfig } from '../config';
  11. import type { MediaKeyFunc } from '../utils/mediakeys-helper';
  12. import { KeySystems } from '../utils/mediakeys-helper';
  13. import type Hls from '../hls';
  14. import type { ComponentAPI } from '../types/component-api';
  15. import type { MediaAttachedData, ManifestParsedData } from '../types/events';
  16.  
  17. const MAX_LICENSE_REQUEST_FAILURES = 3;
  18.  
  19. /**
  20. * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration
  21. * @param {Array<string>} audioCodecs List of required audio codecs to support
  22. * @param {Array<string>} videoCodecs List of required video codecs to support
  23. * @param {object} drmSystemOptions Optional parameters/requirements for the key-system
  24. * @returns {Array<MediaSystemConfiguration>} An array of supported configurations
  25. */
  26.  
  27. const createWidevineMediaKeySystemConfigurations = function (
  28. audioCodecs: string[],
  29. videoCodecs: string[],
  30. drmSystemOptions: DRMSystemOptions
  31. ): MediaKeySystemConfiguration[] {
  32. /* jshint ignore:line */
  33. const baseConfig: MediaKeySystemConfiguration = {
  34. // initDataTypes: ['keyids', 'mp4'],
  35. // label: "",
  36. // persistentState: "not-allowed", // or "required" ?
  37. // distinctiveIdentifier: "not-allowed", // or "required" ?
  38. // sessionTypes: ['temporary'],
  39. audioCapabilities: [], // { contentType: 'audio/mp4; codecs="mp4a.40.2"' }
  40. videoCapabilities: [], // { contentType: 'video/mp4; codecs="avc1.42E01E"' }
  41. };
  42.  
  43. audioCodecs.forEach((codec) => {
  44. baseConfig.audioCapabilities!.push({
  45. contentType: `audio/mp4; codecs="${codec}"`,
  46. robustness: drmSystemOptions.audioRobustness || '',
  47. });
  48. });
  49. videoCodecs.forEach((codec) => {
  50. baseConfig.videoCapabilities!.push({
  51. contentType: `video/mp4; codecs="${codec}"`,
  52. robustness: drmSystemOptions.videoRobustness || '',
  53. });
  54. });
  55.  
  56. return [baseConfig];
  57. };
  58.  
  59. /**
  60. * The idea here is to handle key-system (and their respective platforms) specific configuration differences
  61. * in order to work with the local requestMediaKeySystemAccess method.
  62. *
  63. * We can also rule-out platform-related key-system support at this point by throwing an error.
  64. *
  65. * @param {string} keySystem Identifier for the key-system, see `KeySystems` enum
  66. * @param {Array<string>} audioCodecs List of required audio codecs to support
  67. * @param {Array<string>} videoCodecs List of required video codecs to support
  68. * @throws will throw an error if a unknown key system is passed
  69. * @returns {Array<MediaSystemConfiguration>} A non-empty Array of MediaKeySystemConfiguration objects
  70. */
  71. const getSupportedMediaKeySystemConfigurations = function (
  72. keySystem: KeySystems,
  73. audioCodecs: string[],
  74. videoCodecs: string[],
  75. drmSystemOptions: DRMSystemOptions
  76. ): MediaKeySystemConfiguration[] {
  77. switch (keySystem) {
  78. case KeySystems.WIDEVINE:
  79. return createWidevineMediaKeySystemConfigurations(
  80. audioCodecs,
  81. videoCodecs,
  82. drmSystemOptions
  83. );
  84. default:
  85. throw new Error(`Unknown key-system: ${keySystem}`);
  86. }
  87. };
  88.  
  89. interface MediaKeysListItem {
  90. mediaKeys?: MediaKeys;
  91. mediaKeysSession?: MediaKeySession;
  92. mediaKeysSessionInitialized: boolean;
  93. mediaKeySystemAccess: MediaKeySystemAccess;
  94. mediaKeySystemDomain: KeySystems;
  95. }
  96.  
  97. /**
  98. * Controller to deal with encrypted media extensions (EME)
  99. * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API
  100. *
  101. * @class
  102. * @constructor
  103. */
  104. class EMEController implements ComponentAPI {
  105. private hls: Hls;
  106. private _widevineLicenseUrl?: string;
  107. private _licenseXhrSetup?: (xhr: XMLHttpRequest, url: string) => void;
  108. private _licenseResponseCallback?: (
  109. xhr: XMLHttpRequest,
  110. url: string
  111. ) => ArrayBuffer;
  112. private _emeEnabled: boolean;
  113. private _requestMediaKeySystemAccess: MediaKeyFunc | null;
  114. private _drmSystemOptions: DRMSystemOptions;
  115.  
  116. private _config: EMEControllerConfig;
  117. private _mediaKeysList: MediaKeysListItem[] = [];
  118. private _media: HTMLMediaElement | null = null;
  119. private _hasSetMediaKeys: boolean = false;
  120. private _requestLicenseFailureCount: number = 0;
  121.  
  122. private mediaKeysPromise: Promise<MediaKeys> | null = null;
  123. private _onMediaEncrypted = this.onMediaEncrypted.bind(this);
  124.  
  125. /**
  126. * @constructs
  127. * @param {Hls} hls Our Hls.js instance
  128. */
  129. constructor(hls: Hls) {
  130. this.hls = hls;
  131. this._config = hls.config;
  132.  
  133. this._widevineLicenseUrl = this._config.widevineLicenseUrl;
  134. this._licenseXhrSetup = this._config.licenseXhrSetup;
  135. this._licenseResponseCallback = this._config.licenseResponseCallback;
  136. this._emeEnabled = this._config.emeEnabled;
  137. this._requestMediaKeySystemAccess =
  138. this._config.requestMediaKeySystemAccessFunc;
  139. this._drmSystemOptions = this._config.drmSystemOptions;
  140.  
  141. this._registerListeners();
  142. }
  143.  
  144. public destroy() {
  145. this._unregisterListeners();
  146. // @ts-ignore
  147. this.hls = this._onMediaEncrypted = null;
  148. this._requestMediaKeySystemAccess = null;
  149. }
  150.  
  151. private _registerListeners() {
  152. this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  153. this.hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this);
  154. this.hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
  155. }
  156.  
  157. private _unregisterListeners() {
  158. this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  159. this.hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this);
  160. this.hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
  161. }
  162.  
  163. /**
  164. * @param {string} keySystem Identifier for the key-system, see `KeySystems` enum
  165. * @returns {string} License server URL for key-system (if any configured, otherwise causes error)
  166. * @throws if a unsupported keysystem is passed
  167. */
  168. getLicenseServerUrl(keySystem: KeySystems): string {
  169. switch (keySystem) {
  170. case KeySystems.WIDEVINE:
  171. if (!this._widevineLicenseUrl) {
  172. break;
  173. }
  174. return this._widevineLicenseUrl;
  175. }
  176.  
  177. throw new Error(
  178. `no license server URL configured for key-system "${keySystem}"`
  179. );
  180. }
  181.  
  182. /**
  183. * Requests access object and adds it to our list upon success
  184. * @private
  185. * @param {string} keySystem System ID (see `KeySystems`)
  186. * @param {Array<string>} audioCodecs List of required audio codecs to support
  187. * @param {Array<string>} videoCodecs List of required video codecs to support
  188. * @throws When a unsupported KeySystem is passed
  189. */
  190. private _attemptKeySystemAccess(
  191. keySystem: KeySystems,
  192. audioCodecs: string[],
  193. videoCodecs: string[]
  194. ) {
  195. // This can throw, but is caught in event handler callpath
  196. const mediaKeySystemConfigs = getSupportedMediaKeySystemConfigurations(
  197. keySystem,
  198. audioCodecs,
  199. videoCodecs,
  200. this._drmSystemOptions
  201. );
  202.  
  203. logger.log('Requesting encrypted media key-system access');
  204.  
  205. // expecting interface like window.navigator.requestMediaKeySystemAccess
  206. const keySystemAccessPromise = this.requestMediaKeySystemAccess(
  207. keySystem,
  208. mediaKeySystemConfigs
  209. );
  210.  
  211. this.mediaKeysPromise = keySystemAccessPromise.then(
  212. (mediaKeySystemAccess) =>
  213. this._onMediaKeySystemAccessObtained(keySystem, mediaKeySystemAccess)
  214. );
  215.  
  216. keySystemAccessPromise.catch((err) => {
  217. logger.error(`Failed to obtain key-system "${keySystem}" access:`, err);
  218. });
  219. }
  220.  
  221. get requestMediaKeySystemAccess() {
  222. if (!this._requestMediaKeySystemAccess) {
  223. throw new Error('No requestMediaKeySystemAccess function configured');
  224. }
  225.  
  226. return this._requestMediaKeySystemAccess;
  227. }
  228.  
  229. /**
  230. * Handles obtaining access to a key-system
  231. * @private
  232. * @param {string} keySystem
  233. * @param {MediaKeySystemAccess} mediaKeySystemAccess https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemAccess
  234. */
  235. private _onMediaKeySystemAccessObtained(
  236. keySystem: KeySystems,
  237. mediaKeySystemAccess: MediaKeySystemAccess
  238. ): Promise<MediaKeys> {
  239. logger.log(`Access for key-system "${keySystem}" obtained`);
  240.  
  241. const mediaKeysListItem: MediaKeysListItem = {
  242. mediaKeysSessionInitialized: false,
  243. mediaKeySystemAccess: mediaKeySystemAccess,
  244. mediaKeySystemDomain: keySystem,
  245. };
  246.  
  247. this._mediaKeysList.push(mediaKeysListItem);
  248.  
  249. const mediaKeysPromise = Promise.resolve()
  250. .then(() => mediaKeySystemAccess.createMediaKeys())
  251. .then((mediaKeys) => {
  252. mediaKeysListItem.mediaKeys = mediaKeys;
  253.  
  254. logger.log(`Media-keys created for key-system "${keySystem}"`);
  255.  
  256. this._onMediaKeysCreated();
  257.  
  258. return mediaKeys;
  259. });
  260.  
  261. mediaKeysPromise.catch((err) => {
  262. logger.error('Failed to create media-keys:', err);
  263. });
  264.  
  265. return mediaKeysPromise;
  266. }
  267.  
  268. /**
  269. * Handles key-creation (represents access to CDM). We are going to create key-sessions upon this
  270. * for all existing keys where no session exists yet.
  271. *
  272. * @private
  273. */
  274. private _onMediaKeysCreated() {
  275. // check for all key-list items if a session exists, otherwise, create one
  276. this._mediaKeysList.forEach((mediaKeysListItem) => {
  277. if (!mediaKeysListItem.mediaKeysSession) {
  278. // mediaKeys is definitely initialized here
  279. mediaKeysListItem.mediaKeysSession =
  280. mediaKeysListItem.mediaKeys!.createSession();
  281. this._onNewMediaKeySession(mediaKeysListItem.mediaKeysSession);
  282. }
  283. });
  284. }
  285.  
  286. /**
  287. * @private
  288. * @param {*} keySession
  289. */
  290. private _onNewMediaKeySession(keySession: MediaKeySession) {
  291. logger.log(`New key-system session ${keySession.sessionId}`);
  292.  
  293. keySession.addEventListener(
  294. 'message',
  295. (event: MediaKeyMessageEvent) => {
  296. this._onKeySessionMessage(keySession, event.message);
  297. },
  298. false
  299. );
  300. }
  301.  
  302. /**
  303. * @private
  304. * @param {MediaKeySession} keySession
  305. * @param {ArrayBuffer} message
  306. */
  307. private _onKeySessionMessage(
  308. keySession: MediaKeySession,
  309. message: ArrayBuffer
  310. ) {
  311. logger.log('Got EME message event, creating license request');
  312.  
  313. this._requestLicense(message, (data: ArrayBuffer) => {
  314. logger.log(
  315. `Received license data (length: ${
  316. data ? data.byteLength : data
  317. }), updating key-session`
  318. );
  319. keySession.update(data).catch((err) => {
  320. logger.warn(`Updating key-session failed: ${err}`);
  321. });
  322. });
  323. }
  324.  
  325. /**
  326. * @private
  327. * @param e {MediaEncryptedEvent}
  328. */
  329. private onMediaEncrypted(e: MediaEncryptedEvent) {
  330. logger.log(`Media is encrypted using "${e.initDataType}" init data type`);
  331.  
  332. if (!this.mediaKeysPromise) {
  333. logger.error(
  334. 'Fatal: Media is encrypted but no CDM access or no keys have been requested'
  335. );
  336. this.hls.trigger(Events.ERROR, {
  337. type: ErrorTypes.KEY_SYSTEM_ERROR,
  338. details: ErrorDetails.KEY_SYSTEM_NO_KEYS,
  339. fatal: true,
  340. });
  341. return;
  342. }
  343.  
  344. const finallySetKeyAndStartSession = (mediaKeys) => {
  345. if (!this._media) {
  346. return;
  347. }
  348. this._attemptSetMediaKeys(mediaKeys);
  349. this._generateRequestWithPreferredKeySession(e.initDataType, e.initData);
  350. };
  351.  
  352. // Could use `Promise.finally` but some Promise polyfills are missing it
  353. this.mediaKeysPromise
  354. .then(finallySetKeyAndStartSession)
  355. .catch(finallySetKeyAndStartSession);
  356. }
  357.  
  358. /**
  359. * @private
  360. */
  361. private _attemptSetMediaKeys(mediaKeys?: MediaKeys) {
  362. if (!this._media) {
  363. throw new Error(
  364. 'Attempted to set mediaKeys without first attaching a media element'
  365. );
  366. }
  367.  
  368. if (!this._hasSetMediaKeys) {
  369. // FIXME: see if we can/want/need-to really to deal with several potential key-sessions?
  370. const keysListItem = this._mediaKeysList[0];
  371. if (!keysListItem || !keysListItem.mediaKeys) {
  372. logger.error(
  373. 'Fatal: Media is encrypted but no CDM access or no keys have been obtained yet'
  374. );
  375. this.hls.trigger(Events.ERROR, {
  376. type: ErrorTypes.KEY_SYSTEM_ERROR,
  377. details: ErrorDetails.KEY_SYSTEM_NO_KEYS,
  378. fatal: true,
  379. });
  380. return;
  381. }
  382.  
  383. logger.log('Setting keys for encrypted media');
  384.  
  385. this._media.setMediaKeys(keysListItem.mediaKeys);
  386. this._hasSetMediaKeys = true;
  387. }
  388. }
  389.  
  390. /**
  391. * @private
  392. */
  393. private _generateRequestWithPreferredKeySession(
  394. initDataType: string,
  395. initData: ArrayBuffer | null
  396. ) {
  397. // FIXME: see if we can/want/need-to really to deal with several potential key-sessions?
  398. const keysListItem = this._mediaKeysList[0];
  399. if (!keysListItem) {
  400. logger.error(
  401. 'Fatal: Media is encrypted but not any key-system access has been obtained yet'
  402. );
  403. this.hls.trigger(Events.ERROR, {
  404. type: ErrorTypes.KEY_SYSTEM_ERROR,
  405. details: ErrorDetails.KEY_SYSTEM_NO_ACCESS,
  406. fatal: true,
  407. });
  408. return;
  409. }
  410.  
  411. if (keysListItem.mediaKeysSessionInitialized) {
  412. logger.warn('Key-Session already initialized but requested again');
  413. return;
  414. }
  415.  
  416. const keySession = keysListItem.mediaKeysSession;
  417. if (!keySession) {
  418. logger.error('Fatal: Media is encrypted but no key-session existing');
  419. this.hls.trigger(Events.ERROR, {
  420. type: ErrorTypes.KEY_SYSTEM_ERROR,
  421. details: ErrorDetails.KEY_SYSTEM_NO_SESSION,
  422. fatal: true,
  423. });
  424. return;
  425. }
  426.  
  427. // initData is null if the media is not CORS-same-origin
  428. if (!initData) {
  429. logger.warn(
  430. 'Fatal: initData required for generating a key session is null'
  431. );
  432. this.hls.trigger(Events.ERROR, {
  433. type: ErrorTypes.KEY_SYSTEM_ERROR,
  434. details: ErrorDetails.KEY_SYSTEM_NO_INIT_DATA,
  435. fatal: true,
  436. });
  437. return;
  438. }
  439.  
  440. logger.log(
  441. `Generating key-session request for "${initDataType}" init data type`
  442. );
  443. keysListItem.mediaKeysSessionInitialized = true;
  444.  
  445. keySession
  446. .generateRequest(initDataType, initData)
  447. .then(() => {
  448. logger.debug('Key-session generation succeeded');
  449. })
  450. .catch((err) => {
  451. logger.error('Error generating key-session request:', err);
  452. this.hls.trigger(Events.ERROR, {
  453. type: ErrorTypes.KEY_SYSTEM_ERROR,
  454. details: ErrorDetails.KEY_SYSTEM_NO_SESSION,
  455. fatal: false,
  456. });
  457. });
  458. }
  459.  
  460. /**
  461. * @private
  462. * @param {string} url License server URL
  463. * @param {ArrayBuffer} keyMessage Message data issued by key-system
  464. * @param {function} callback Called when XHR has succeeded
  465. * @returns {XMLHttpRequest} Unsent (but opened state) XHR object
  466. * @throws if XMLHttpRequest construction failed
  467. */
  468. private _createLicenseXhr(
  469. url: string,
  470. keyMessage: ArrayBuffer,
  471. callback: (data: ArrayBuffer) => void
  472. ): XMLHttpRequest {
  473. const xhr = new XMLHttpRequest();
  474. xhr.responseType = 'arraybuffer';
  475. xhr.onreadystatechange = this._onLicenseRequestReadyStageChange.bind(
  476. this,
  477. xhr,
  478. url,
  479. keyMessage,
  480. callback
  481. );
  482.  
  483. let licenseXhrSetup = this._licenseXhrSetup;
  484. if (licenseXhrSetup) {
  485. try {
  486. licenseXhrSetup.call(this.hls, xhr, url);
  487. licenseXhrSetup = undefined;
  488. } catch (e) {
  489. logger.error(e);
  490. }
  491. }
  492. try {
  493. // if licenseXhrSetup did not yet call open, let's do it now
  494. if (!xhr.readyState) {
  495. xhr.open('POST', url, true);
  496. }
  497. if (licenseXhrSetup) {
  498. licenseXhrSetup.call(this.hls, xhr, url);
  499. }
  500. } catch (e) {
  501. // IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS
  502. throw new Error(`issue setting up KeySystem license XHR ${e}`);
  503. }
  504.  
  505. return xhr;
  506. }
  507.  
  508. /**
  509. * @private
  510. * @param {XMLHttpRequest} xhr
  511. * @param {string} url License server URL
  512. * @param {ArrayBuffer} keyMessage Message data issued by key-system
  513. * @param {function} callback Called when XHR has succeeded
  514. */
  515. private _onLicenseRequestReadyStageChange(
  516. xhr: XMLHttpRequest,
  517. url: string,
  518. keyMessage: ArrayBuffer,
  519. callback: (data: ArrayBuffer) => void
  520. ) {
  521. switch (xhr.readyState) {
  522. case 4:
  523. if (xhr.status === 200) {
  524. this._requestLicenseFailureCount = 0;
  525. logger.log('License request succeeded');
  526. let data: ArrayBuffer = xhr.response;
  527. const licenseResponseCallback = this._licenseResponseCallback;
  528. if (licenseResponseCallback) {
  529. try {
  530. data = licenseResponseCallback.call(this.hls, xhr, url);
  531. } catch (e) {
  532. logger.error(e);
  533. }
  534. }
  535. callback(data);
  536. } else {
  537. logger.error(
  538. `License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`
  539. );
  540. this._requestLicenseFailureCount++;
  541. if (this._requestLicenseFailureCount > MAX_LICENSE_REQUEST_FAILURES) {
  542. this.hls.trigger(Events.ERROR, {
  543. type: ErrorTypes.KEY_SYSTEM_ERROR,
  544. details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED,
  545. fatal: true,
  546. });
  547. return;
  548. }
  549.  
  550. const attemptsLeft =
  551. MAX_LICENSE_REQUEST_FAILURES - this._requestLicenseFailureCount + 1;
  552. logger.warn(
  553. `Retrying license request, ${attemptsLeft} attempts left`
  554. );
  555. this._requestLicense(keyMessage, callback);
  556. }
  557. break;
  558. }
  559. }
  560.  
  561. /**
  562. * @private
  563. * @param {MediaKeysListItem} keysListItem
  564. * @param {ArrayBuffer} keyMessage
  565. * @returns {ArrayBuffer} Challenge data posted to license server
  566. * @throws if KeySystem is unsupported
  567. */
  568. private _generateLicenseRequestChallenge(
  569. keysListItem: MediaKeysListItem,
  570. keyMessage: ArrayBuffer
  571. ): ArrayBuffer {
  572. switch (keysListItem.mediaKeySystemDomain) {
  573. // case KeySystems.PLAYREADY:
  574. // from https://github.com/MicrosoftEdge/Demos/blob/master/eme/scripts/demo.js
  575. /*
  576. if (this.licenseType !== this.LICENSE_TYPE_WIDEVINE) {
  577. // For PlayReady CDMs, we need to dig the Challenge out of the XML.
  578. var keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml');
  579. if (keyMessageXml.getElementsByTagName('Challenge')[0]) {
  580. challenge = atob(keyMessageXml.getElementsByTagName('Challenge')[0].childNodes[0].nodeValue);
  581. } else {
  582. throw 'Cannot find <Challenge> in key message';
  583. }
  584. var headerNames = keyMessageXml.getElementsByTagName('name');
  585. var headerValues = keyMessageXml.getElementsByTagName('value');
  586. if (headerNames.length !== headerValues.length) {
  587. throw 'Mismatched header <name>/<value> pair in key message';
  588. }
  589. for (var i = 0; i < headerNames.length; i++) {
  590. xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue);
  591. }
  592. }
  593. break;
  594. */
  595. case KeySystems.WIDEVINE:
  596. // For Widevine CDMs, the challenge is the keyMessage.
  597. return keyMessage;
  598. }
  599.  
  600. throw new Error(
  601. `unsupported key-system: ${keysListItem.mediaKeySystemDomain}`
  602. );
  603. }
  604.  
  605. /**
  606. * @private
  607. * @param keyMessage
  608. * @param callback
  609. */
  610. private _requestLicense(
  611. keyMessage: ArrayBuffer,
  612. callback: (data: ArrayBuffer) => void
  613. ) {
  614. logger.log('Requesting content license for key-system');
  615.  
  616. const keysListItem = this._mediaKeysList[0];
  617. if (!keysListItem) {
  618. logger.error(
  619. 'Fatal error: Media is encrypted but no key-system access has been obtained yet'
  620. );
  621. this.hls.trigger(Events.ERROR, {
  622. type: ErrorTypes.KEY_SYSTEM_ERROR,
  623. details: ErrorDetails.KEY_SYSTEM_NO_ACCESS,
  624. fatal: true,
  625. });
  626. return;
  627. }
  628.  
  629. try {
  630. const url = this.getLicenseServerUrl(keysListItem.mediaKeySystemDomain);
  631. const xhr = this._createLicenseXhr(url, keyMessage, callback);
  632. logger.log(`Sending license request to URL: ${url}`);
  633. const challenge = this._generateLicenseRequestChallenge(
  634. keysListItem,
  635. keyMessage
  636. );
  637. xhr.send(challenge);
  638. } catch (e) {
  639. logger.error(`Failure requesting DRM license: ${e}`);
  640. this.hls.trigger(Events.ERROR, {
  641. type: ErrorTypes.KEY_SYSTEM_ERROR,
  642. details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED,
  643. fatal: true,
  644. });
  645. }
  646. }
  647.  
  648. onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachedData) {
  649. if (!this._emeEnabled) {
  650. return;
  651. }
  652.  
  653. const media = data.media;
  654.  
  655. // keep reference of media
  656. this._media = media;
  657.  
  658. media.addEventListener('encrypted', this._onMediaEncrypted);
  659. }
  660.  
  661. onMediaDetached() {
  662. const media = this._media;
  663. const mediaKeysList = this._mediaKeysList;
  664. if (!media) {
  665. return;
  666. }
  667. media.removeEventListener('encrypted', this._onMediaEncrypted);
  668. this._media = null;
  669. this._mediaKeysList = [];
  670. // Close all sessions and remove media keys from the video element.
  671. Promise.all(
  672. mediaKeysList.map((mediaKeysListItem) => {
  673. if (mediaKeysListItem.mediaKeysSession) {
  674. return mediaKeysListItem.mediaKeysSession.close().catch(() => {
  675. // Ignore errors when closing the sessions. Closing a session that
  676. // generated no key requests will throw an error.
  677. });
  678. }
  679. })
  680. )
  681. .then(() => {
  682. return media.setMediaKeys(null);
  683. })
  684. .catch(() => {
  685. // Ignore any failures while removing media keys from the video element.
  686. });
  687. }
  688.  
  689. onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
  690. if (!this._emeEnabled) {
  691. return;
  692. }
  693.  
  694. const audioCodecs = data.levels
  695. .map((level) => level.audioCodec)
  696. .filter(
  697. (audioCodec: string | undefined): audioCodec is string => !!audioCodec
  698. );
  699. const videoCodecs = data.levels
  700. .map((level) => level.videoCodec)
  701. .filter(
  702. (videoCodec: string | undefined): videoCodec is string => !!videoCodec
  703. );
  704.  
  705. this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs);
  706. }
  707. }
  708.  
  709. export default EMEController;