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