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. import { logger } from '../utils/logger';
  9. import {
  10. getKeySystemsForConfig,
  11. getSupportedMediaKeySystemConfigurations,
  12. keySystemDomainToKeySystemFormat as keySystemToKeySystemFormat,
  13. KeySystemFormats,
  14. keySystemFormatToKeySystemDomain,
  15. KeySystemIds,
  16. keySystemIdToKeySystemDomain,
  17. } from '../utils/mediakeys-helper';
  18. import {
  19. KeySystems,
  20. requestMediaKeySystemAccess,
  21. } from '../utils/mediakeys-helper';
  22. import { strToUtf8array } from '../utils/keysystem-util';
  23. import { base64Decode } from '../utils/numeric-encoding-utils';
  24. import { DecryptData, LevelKey } from '../loader/level-key';
  25. import Hex from '../utils/hex';
  26. import { bin2str, parsePssh, parseSinf } from '../utils/mp4-tools';
  27. import EventEmitter from 'eventemitter3';
  28. import type Hls from '../hls';
  29. import type { ComponentAPI } from '../types/component-api';
  30. import type {
  31. MediaAttachedData,
  32. KeyLoadedData,
  33. ErrorData,
  34. ManifestLoadedData,
  35. } from '../types/events';
  36. import type { EMEControllerConfig } from '../config';
  37. import type { Fragment } from '../loader/fragment';
  38.  
  39. const MAX_LICENSE_REQUEST_FAILURES = 3;
  40. const LOGGER_PREFIX = '[eme]';
  41.  
  42. interface KeySystemAccessPromises {
  43. keySystemAccess: Promise<MediaKeySystemAccess>;
  44. mediaKeys?: Promise<MediaKeys>;
  45. certificate?: Promise<BufferSource | void>;
  46. }
  47.  
  48. export interface MediaKeySessionContext {
  49. keySystem: KeySystems;
  50. mediaKeys: MediaKeys;
  51. decryptdata: LevelKey;
  52. mediaKeysSession: MediaKeySession;
  53. keyStatus: MediaKeyStatus;
  54. licenseXhr?: XMLHttpRequest;
  55. }
  56.  
  57. /**
  58. * Controller to deal with encrypted media extensions (EME)
  59. * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API
  60. *
  61. * @class
  62. * @constructor
  63. */
  64. class EMEController implements ComponentAPI {
  65. public static CDMCleanupPromise: Promise<void> | void;
  66.  
  67. private readonly hls: Hls;
  68. private readonly config: EMEControllerConfig;
  69. private media: HTMLMediaElement | null = null;
  70. private keyFormatPromise: Promise<KeySystemFormats> | null = null;
  71. private keySystemAccessPromises: {
  72. [keysystem: string]: KeySystemAccessPromises;
  73. } = {};
  74. private _requestLicenseFailureCount: number = 0;
  75. private mediaKeySessions: MediaKeySessionContext[] = [];
  76. private keyIdToKeySessionPromise: {
  77. [keyId: string]: Promise<MediaKeySessionContext>;
  78. } = {};
  79. private setMediaKeysQueue: Promise<void>[] = EMEController.CDMCleanupPromise
  80. ? [EMEController.CDMCleanupPromise]
  81. : [];
  82. private onMediaEncrypted = this._onMediaEncrypted.bind(this);
  83. private onWaitingForKey = this._onWaitingForKey.bind(this);
  84.  
  85. private debug: (msg: any) => void = logger.debug.bind(logger, LOGGER_PREFIX);
  86. private log: (msg: any) => void = logger.log.bind(logger, LOGGER_PREFIX);
  87. private warn: (msg: any) => void = logger.warn.bind(logger, LOGGER_PREFIX);
  88. private error: (msg: any) => void = logger.error.bind(logger, LOGGER_PREFIX);
  89.  
  90. constructor(hls: Hls) {
  91. this.hls = hls;
  92. this.config = hls.config;
  93. this.registerListeners();
  94. }
  95.  
  96. public destroy() {
  97. this.unregisterListeners();
  98. this.onMediaDetached();
  99. // @ts-ignore
  100. this.hls =
  101. this.onMediaEncrypted =
  102. this.onWaitingForKey =
  103. this.keyIdToKeySessionPromise =
  104. null as any;
  105. }
  106.  
  107. private registerListeners() {
  108. this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  109. this.hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this);
  110. this.hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
  111. }
  112.  
  113. private unregisterListeners() {
  114. this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  115. this.hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this);
  116. this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
  117. }
  118.  
  119. private getLicenseServerUrl(keySystem: KeySystems): string | never {
  120. const { drmSystems, widevineLicenseUrl } = this.config;
  121. const keySystemConfiguration = drmSystems[keySystem];
  122.  
  123. if (keySystemConfiguration) {
  124. return keySystemConfiguration.licenseUrl;
  125. }
  126.  
  127. // For backward compatibility
  128. if (keySystem === KeySystems.WIDEVINE && widevineLicenseUrl) {
  129. return widevineLicenseUrl;
  130. }
  131.  
  132. throw new Error(
  133. `no license server URL configured for key-system "${keySystem}"`
  134. );
  135. }
  136.  
  137. private getServerCertificateUrl(keySystem: KeySystems): string | void {
  138. const { drmSystems } = this.config;
  139. const keySystemConfiguration = drmSystems[keySystem];
  140.  
  141. if (keySystemConfiguration) {
  142. return keySystemConfiguration.serverCertificateUrl;
  143. } else {
  144. this.log(`No Server Certificate in config.drmSystems["${keySystem}"]`);
  145. }
  146. }
  147.  
  148. private attemptKeySystemAccess(
  149. keySystemsToAttempt: KeySystems[]
  150. ): Promise<{ keySystem: KeySystems; mediaKeys: MediaKeys }> {
  151. const levels = this.hls.levels;
  152. const uniqueCodec = (value: string | undefined, i, a): value is string =>
  153. !!value && a.indexOf(value) === i;
  154. const audioCodecs = levels
  155. .map((level) => level.audioCodec)
  156. .filter(uniqueCodec);
  157. const videoCodecs = levels
  158. .map((level) => level.videoCodec)
  159. .filter(uniqueCodec);
  160. if (audioCodecs.length + videoCodecs.length === 0) {
  161. videoCodecs.push('avc1.42e01e');
  162. }
  163.  
  164. return new Promise(
  165. (
  166. resolve: (result: {
  167. keySystem: KeySystems;
  168. mediaKeys: MediaKeys;
  169. }) => void,
  170. reject: (Error) => void
  171. ) => {
  172. const attempt = (keySystems) => {
  173. const keySystem = keySystems.shift();
  174. this.getMediaKeysPromise(keySystem, audioCodecs, videoCodecs)
  175. .then((mediaKeys) => resolve({ keySystem, mediaKeys }))
  176. .catch((error) => {
  177. if (keySystems.length) {
  178. attempt(keySystems);
  179. } else if (error instanceof EMEKeyError) {
  180. reject(error);
  181. } else {
  182. reject(
  183. new EMEKeyError(
  184. {
  185. type: ErrorTypes.KEY_SYSTEM_ERROR,
  186. details: ErrorDetails.KEY_SYSTEM_NO_ACCESS,
  187. error,
  188. fatal: true,
  189. },
  190. error.message
  191. )
  192. );
  193. }
  194. });
  195. };
  196. attempt(keySystemsToAttempt);
  197. }
  198. );
  199. }
  200.  
  201. private requestMediaKeySystemAccess(
  202. keySystem: KeySystems,
  203. supportedConfigurations: MediaKeySystemConfiguration[]
  204. ): Promise<MediaKeySystemAccess> {
  205. const { requestMediaKeySystemAccessFunc } = this.config;
  206. if (!(typeof requestMediaKeySystemAccessFunc === 'function')) {
  207. let errMessage = `Configured requestMediaKeySystemAccess is not a function ${requestMediaKeySystemAccessFunc}`;
  208. if (
  209. requestMediaKeySystemAccess === null &&
  210. self.location.protocol === 'http:'
  211. ) {
  212. errMessage = `navigator.requestMediaKeySystemAccess is not available over insecure protocol ${location.protocol}`;
  213. }
  214. return Promise.reject(new Error(errMessage));
  215. }
  216.  
  217. return requestMediaKeySystemAccessFunc(keySystem, supportedConfigurations);
  218. }
  219.  
  220. private getMediaKeysPromise(
  221. keySystem: KeySystems,
  222. audioCodecs: string[],
  223. videoCodecs: string[]
  224. ): Promise<MediaKeys> {
  225. // This can throw, but is caught in event handler callpath
  226. const mediaKeySystemConfigs = getSupportedMediaKeySystemConfigurations(
  227. keySystem,
  228. audioCodecs,
  229. videoCodecs,
  230. this.config.drmSystemOptions
  231. );
  232. const keySystemAccessPromises: KeySystemAccessPromises =
  233. this.keySystemAccessPromises[keySystem];
  234. let keySystemAccess = keySystemAccessPromises?.keySystemAccess;
  235. if (!keySystemAccess) {
  236. this.log(
  237. `Requesting encrypted media "${keySystem}" key-system access with config: ${JSON.stringify(
  238. mediaKeySystemConfigs
  239. )}`
  240. );
  241. keySystemAccess = this.requestMediaKeySystemAccess(
  242. keySystem,
  243. mediaKeySystemConfigs
  244. );
  245. const keySystemAccessPromises: KeySystemAccessPromises =
  246. (this.keySystemAccessPromises[keySystem] = {
  247. keySystemAccess,
  248. });
  249. keySystemAccess.catch((error) => {
  250. this.log(
  251. `Failed to obtain access to key-system "${keySystem}": ${error}`
  252. );
  253. });
  254. return keySystemAccess.then((mediaKeySystemAccess) => {
  255. this.log(
  256. `Access for key-system "${mediaKeySystemAccess.keySystem}" obtained`
  257. );
  258.  
  259. const certificateRequest = this.fetchServerCertificate(keySystem);
  260.  
  261. this.log(`Create media-keys for "${keySystem}"`);
  262. keySystemAccessPromises.mediaKeys = mediaKeySystemAccess
  263. .createMediaKeys()
  264. .then((mediaKeys) => {
  265. this.log(`Media-keys created for "${keySystem}"`);
  266. return certificateRequest.then((certificate) => {
  267. if (certificate) {
  268. return this.setMediaKeysServerCertificate(
  269. mediaKeys,
  270. keySystem,
  271. certificate
  272. );
  273. }
  274. return mediaKeys;
  275. });
  276. });
  277.  
  278. keySystemAccessPromises.mediaKeys.catch((error) => {
  279. this.error(
  280. `Failed to create media-keys for "${keySystem}"}: ${error}`
  281. );
  282. });
  283.  
  284. return keySystemAccessPromises.mediaKeys;
  285. });
  286. }
  287. return keySystemAccess.then(() => keySystemAccessPromises.mediaKeys!);
  288. }
  289.  
  290. private createMediaKeySessionContext({
  291. decryptdata,
  292. keySystem,
  293. mediaKeys,
  294. }: {
  295. decryptdata: LevelKey;
  296. keySystem: KeySystems;
  297. mediaKeys: MediaKeys;
  298. }): MediaKeySessionContext {
  299. console.assert(!!mediaKeys, 'mediaKeys is defined');
  300.  
  301. this.log(
  302. `Creating key-system session "${keySystem}" keyId: ${Hex.hexDump(
  303. decryptdata.keyId! || []
  304. )}`
  305. );
  306.  
  307. const mediaKeysSession = mediaKeys.createSession();
  308.  
  309. const mediaKeySessionContext: MediaKeySessionContext = {
  310. decryptdata,
  311. keySystem,
  312. mediaKeys,
  313. mediaKeysSession,
  314. keyStatus: 'status-pending',
  315. };
  316.  
  317. this.mediaKeySessions.push(mediaKeySessionContext);
  318.  
  319. return mediaKeySessionContext;
  320. }
  321.  
  322. private renewKeySession(mediaKeySessionContext: MediaKeySessionContext) {
  323. const decryptdata = mediaKeySessionContext.decryptdata;
  324. if (decryptdata.pssh) {
  325. const keySessionContext = this.createMediaKeySessionContext(
  326. mediaKeySessionContext
  327. );
  328. const keyId = this.getKeyIdString(decryptdata);
  329. const scheme = 'cenc';
  330. this.keyIdToKeySessionPromise[keyId] =
  331. this.generateRequestWithPreferredKeySession(
  332. keySessionContext,
  333. scheme,
  334. decryptdata.pssh,
  335. 'expired'
  336. );
  337. } else {
  338. this.warn(`Could not renew expired session. Missing pssh initData.`);
  339. }
  340. this.removeSession(mediaKeySessionContext);
  341. }
  342.  
  343. private getKeyIdString(decryptdata: DecryptData | undefined): string | never {
  344. if (!decryptdata) {
  345. throw new Error('Could not read keyId of undefined decryptdata');
  346. }
  347. if (decryptdata.keyId === null) {
  348. throw new Error('keyId is null');
  349. }
  350. return Hex.hexDump(decryptdata.keyId);
  351. }
  352.  
  353. private updateKeySession(
  354. mediaKeySessionContext: MediaKeySessionContext,
  355. data: Uint8Array
  356. ): Promise<void> {
  357. const keySession = mediaKeySessionContext.mediaKeysSession;
  358. this.log(
  359. `Updating key-session "${keySession.sessionId}" for keyID ${Hex.hexDump(
  360. mediaKeySessionContext.decryptdata?.keyId! || []
  361. )}
  362. } (data length: ${data ? data.byteLength : data})`
  363. );
  364. return keySession.update(data);
  365. }
  366.  
  367. public selectKeySystemFormat(frag: Fragment): Promise<KeySystemFormats> {
  368. const keyFormats = Object.keys(frag.levelkeys || {}) as KeySystemFormats[];
  369. if (!this.keyFormatPromise) {
  370. this.log(
  371. `Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${
  372. frag.level
  373. }) key formats ${keyFormats.join(', ')}`
  374. );
  375. this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
  376. }
  377. return this.keyFormatPromise;
  378. }
  379.  
  380. private getKeyFormatPromise(
  381. keyFormats: KeySystemFormats[]
  382. ): Promise<KeySystemFormats> {
  383. return new Promise((resolve, reject) => {
  384. const keySystemsInConfig = getKeySystemsForConfig(this.config);
  385. const keySystemsToAttempt = keyFormats
  386. .map(keySystemFormatToKeySystemDomain)
  387. .filter(
  388. (value) => !!value && keySystemsInConfig.indexOf(value) !== -1
  389. ) as any as KeySystems[];
  390. return this.getKeySystemSelectionPromise(keySystemsToAttempt)
  391. .then(({ keySystem }) => {
  392. const keySystemFormat = keySystemToKeySystemFormat(keySystem);
  393. if (keySystemFormat) {
  394. resolve(keySystemFormat);
  395. } else {
  396. reject(
  397. new Error(`Unable to find format for key-system "${keySystem}"`)
  398. );
  399. }
  400. })
  401. .catch(reject);
  402. });
  403. }
  404.  
  405. public loadKey(data: KeyLoadedData): Promise<MediaKeySessionContext> {
  406. const decryptdata = data.keyInfo.decryptdata;
  407.  
  408. const keyId = this.getKeyIdString(decryptdata);
  409. const keyDetails = `(keyId: ${keyId} format: "${decryptdata.keyFormat}" method: ${decryptdata.method} uri: ${decryptdata.uri})`;
  410.  
  411. this.log(`Starting session for key ${keyDetails}`);
  412.  
  413. let keySessionContextPromise = this.keyIdToKeySessionPromise[keyId];
  414. if (!keySessionContextPromise) {
  415. keySessionContextPromise = this.keyIdToKeySessionPromise[keyId] =
  416. this.getKeySystemForKeyPromise(decryptdata).then(
  417. ({ keySystem, mediaKeys }) => {
  418. this.throwIfDestroyed();
  419. this.log(
  420. `Handle encrypted media sn: ${data.frag.sn} ${data.frag.type}: ${data.frag.level} using key ${keyDetails}`
  421. );
  422.  
  423. return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
  424. this.throwIfDestroyed();
  425. const keySessionContext = this.createMediaKeySessionContext({
  426. keySystem,
  427. mediaKeys,
  428. decryptdata,
  429. });
  430. const scheme = 'cenc';
  431. return this.generateRequestWithPreferredKeySession(
  432. keySessionContext,
  433. scheme,
  434. decryptdata.pssh,
  435. 'playlist-key'
  436. );
  437. });
  438. }
  439. );
  440.  
  441. keySessionContextPromise.catch((error) => this.handleError(error));
  442. }
  443.  
  444. return keySessionContextPromise;
  445. }
  446.  
  447. private throwIfDestroyed(message = 'Invalid state'): void | never {
  448. if (!this.hls) {
  449. throw new Error('invalid state');
  450. }
  451. }
  452.  
  453. private handleError(error: EMEKeyError | Error) {
  454. if (!this.hls) {
  455. return;
  456. }
  457. this.error(error.message);
  458. if (error instanceof EMEKeyError) {
  459. this.hls.trigger(Events.ERROR, error.data);
  460. } else {
  461. this.hls.trigger(Events.ERROR, {
  462. type: ErrorTypes.KEY_SYSTEM_ERROR,
  463. details: ErrorDetails.KEY_SYSTEM_NO_KEYS,
  464. error,
  465. fatal: true,
  466. });
  467. }
  468. }
  469.  
  470. private getKeySystemForKeyPromise(
  471. decryptdata: LevelKey
  472. ): Promise<{ keySystem: KeySystems; mediaKeys: MediaKeys }> {
  473. const keyId = this.getKeyIdString(decryptdata);
  474. const mediaKeySessionContext = this.keyIdToKeySessionPromise[keyId];
  475. if (!mediaKeySessionContext) {
  476. const keySystem = keySystemFormatToKeySystemDomain(
  477. decryptdata.keyFormat as KeySystemFormats
  478. );
  479. const keySystemsToAttempt = keySystem
  480. ? [keySystem]
  481. : getKeySystemsForConfig(this.config);
  482. return this.attemptKeySystemAccess(keySystemsToAttempt);
  483. }
  484. return mediaKeySessionContext;
  485. }
  486.  
  487. private getKeySystemSelectionPromise(
  488. keySystemsToAttempt: KeySystems[]
  489. ): Promise<{ keySystem: KeySystems; mediaKeys: MediaKeys }> | never {
  490. if (!keySystemsToAttempt.length) {
  491. keySystemsToAttempt = getKeySystemsForConfig(this.config);
  492. }
  493. if (keySystemsToAttempt.length === 0) {
  494. throw new EMEKeyError(
  495. {
  496. type: ErrorTypes.KEY_SYSTEM_ERROR,
  497. details: ErrorDetails.KEY_SYSTEM_NO_CONFIGURED_LICENSE,
  498. fatal: true,
  499. },
  500. `Missing key-system license configuration options ${JSON.stringify({
  501. drmSystems: this.config.drmSystems,
  502. })}`
  503. );
  504. }
  505. return this.attemptKeySystemAccess(keySystemsToAttempt);
  506. }
  507.  
  508. private _onMediaEncrypted(event: MediaEncryptedEvent) {
  509. const { initDataType, initData } = event;
  510. this.debug(`"${event.type}" event: init data type: "${initDataType}"`);
  511.  
  512. // Ignore event when initData is null
  513. if (initData === null) {
  514. return;
  515. }
  516.  
  517. let keyId: Uint8Array | undefined;
  518. let keySystemDomain: KeySystems | undefined;
  519.  
  520. if (
  521. initDataType === 'sinf' &&
  522. this.config.drmSystems[KeySystems.FAIRPLAY]
  523. ) {
  524. // Match sinf keyId to playlist skd://keyId=
  525. const json = bin2str(new Uint8Array(initData));
  526. try {
  527. const sinf = base64Decode(JSON.parse(json).sinf);
  528. const tenc = parseSinf(new Uint8Array(sinf));
  529. if (!tenc) {
  530. return;
  531. }
  532. keyId = tenc.subarray(8, 24);
  533. keySystemDomain = KeySystems.FAIRPLAY;
  534. } catch (error) {
  535. this.warn('Failed to parse sinf "encrypted" event message initData');
  536. return;
  537. }
  538. } else {
  539. // Support clear-lead key-session creation (otherwise depend on playlist keys)
  540. const psshInfo = parsePssh(initData);
  541. if (psshInfo === null) {
  542. return;
  543. }
  544. if (
  545. psshInfo.version === 0 &&
  546. psshInfo.systemId === KeySystemIds.WIDEVINE &&
  547. psshInfo.data
  548. ) {
  549. keyId = psshInfo.data.subarray(8, 24);
  550. }
  551. keySystemDomain = keySystemIdToKeySystemDomain(
  552. psshInfo.systemId as KeySystemIds
  553. );
  554. }
  555.  
  556. if (!keySystemDomain || !keyId) {
  557. return;
  558. }
  559.  
  560. const keyIdHex = Hex.hexDump(keyId);
  561. const { keyIdToKeySessionPromise, mediaKeySessions } = this;
  562.  
  563. let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
  564. for (let i = 0; i < mediaKeySessions.length; i++) {
  565. // Match playlist key
  566. const keyContext = mediaKeySessions[i];
  567. const decryptdata = keyContext.decryptdata;
  568. if (decryptdata.pssh || !decryptdata.keyId) {
  569. continue;
  570. }
  571. const oldKeyIdHex = Hex.hexDump(decryptdata.keyId);
  572. if (
  573. keyIdHex === oldKeyIdHex ||
  574. decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1
  575. ) {
  576. keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex];
  577. delete keyIdToKeySessionPromise[oldKeyIdHex];
  578. decryptdata.pssh = new Uint8Array(initData);
  579. decryptdata.keyId = keyId;
  580. keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
  581. keySessionContextPromise.then(() => {
  582. return this.generateRequestWithPreferredKeySession(
  583. keyContext,
  584. initDataType,
  585. initData,
  586. 'encrypted-event-key-match'
  587. );
  588. });
  589. break;
  590. }
  591. }
  592.  
  593. if (!keySessionContextPromise) {
  594. // Clear-lead key (not encountered in playlist)
  595. keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
  596. this.getKeySystemSelectionPromise([keySystemDomain]).then(
  597. ({ keySystem, mediaKeys }) => {
  598. this.throwIfDestroyed();
  599. const decryptdata = new LevelKey(
  600. 'ISO-23001-7',
  601. keyIdHex,
  602. keySystemToKeySystemFormat(keySystem) ?? ''
  603. );
  604. decryptdata.pssh = new Uint8Array(initData);
  605. decryptdata.keyId = keyId as Uint8Array;
  606. return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
  607. this.throwIfDestroyed();
  608. const keySessionContext = this.createMediaKeySessionContext({
  609. decryptdata,
  610. keySystem,
  611. mediaKeys,
  612. });
  613. return this.generateRequestWithPreferredKeySession(
  614. keySessionContext,
  615. initDataType,
  616. initData,
  617. 'encrypted-event-no-match'
  618. );
  619. });
  620. }
  621. );
  622. }
  623. keySessionContextPromise.catch((error) => this.handleError(error));
  624. }
  625.  
  626. private _onWaitingForKey(event: Event) {
  627. this.log(`"${event.type}" event`);
  628. }
  629.  
  630. private attemptSetMediaKeys(
  631. keySystem: KeySystems,
  632. mediaKeys: MediaKeys
  633. ): Promise<void> {
  634. const queue = this.setMediaKeysQueue.slice();
  635.  
  636. this.log(`Setting media-keys for "${keySystem}"`);
  637. // Only one setMediaKeys() can run at one time, and multiple setMediaKeys() operations
  638. // can be queued for execution for multiple key sessions.
  639. const setMediaKeysPromise = Promise.all(queue).then(() => {
  640. if (!this.media) {
  641. throw new Error(
  642. 'Attempted to set mediaKeys without media element attached'
  643. );
  644. }
  645. return this.media.setMediaKeys(mediaKeys);
  646. });
  647. this.setMediaKeysQueue.push(setMediaKeysPromise);
  648. return setMediaKeysPromise.then(() => {
  649. this.log(`Media-keys set for "${keySystem}"`);
  650. queue.push(setMediaKeysPromise!);
  651. this.setMediaKeysQueue = this.setMediaKeysQueue.filter(
  652. (p) => queue.indexOf(p) === -1
  653. );
  654. });
  655. }
  656.  
  657. private generateRequestWithPreferredKeySession(
  658. context: MediaKeySessionContext,
  659. initDataType: string,
  660. initData: ArrayBuffer | null,
  661. reason:
  662. | 'playlist-key'
  663. | 'encrypted-event-key-match'
  664. | 'encrypted-event-no-match'
  665. | 'expired'
  666. ): Promise<MediaKeySessionContext> | never {
  667. const generateRequestFilter =
  668. this.config.drmSystems?.[context.keySystem]?.generateRequest;
  669. if (generateRequestFilter) {
  670. try {
  671. const mappedInitData: ReturnType<typeof generateRequestFilter> =
  672. generateRequestFilter.call(this.hls, initDataType, initData, context);
  673. if (!mappedInitData) {
  674. throw new Error(
  675. 'Invalid response from configured generateRequest filter'
  676. );
  677. }
  678. initDataType = mappedInitData.initDataType;
  679. initData = context.decryptdata.pssh = mappedInitData.initData
  680. ? new Uint8Array(mappedInitData.initData)
  681. : null;
  682. } catch (error) {
  683. this.warn(error.message);
  684. if (this.hls?.config.debug) {
  685. throw error;
  686. }
  687. }
  688. }
  689.  
  690. if (initData === null) {
  691. this.log(`Skipping key-session request for "${reason}" (no initData)`);
  692. return Promise.resolve(context);
  693. }
  694.  
  695. const keyId = this.getKeyIdString(context.decryptdata);
  696. this.log(
  697. `Generating key-session request for "${reason}": ${keyId} (init data type: ${initDataType} length: ${
  698. initData ? initData.byteLength : null
  699. })`
  700. );
  701.  
  702. const licenseStatus = new EventEmitter();
  703.  
  704. context.mediaKeysSession.onmessage = (event: MediaKeyMessageEvent) => {
  705. const keySession = context.mediaKeysSession;
  706. if (!keySession) {
  707. licenseStatus.emit('error', new Error('invalid state'));
  708. return;
  709. }
  710. const { messageType, message } = event;
  711. this.log(
  712. `"${messageType}" message event for session "${keySession.sessionId}" message size: ${message.byteLength}`
  713. );
  714. if (
  715. messageType === 'license-request' ||
  716. messageType === 'license-renewal'
  717. ) {
  718. this.renewLicense(context, message).catch((error) => {
  719. this.handleError(error);
  720. licenseStatus.emit('error', error);
  721. });
  722. } else if (messageType === 'license-release') {
  723. if (context.keySystem === KeySystems.FAIRPLAY) {
  724. this.updateKeySession(context, strToUtf8array('acknowledged'));
  725. this.removeSession(context);
  726. }
  727. } else {
  728. this.warn(`unhandled media key message type "${messageType}"`);
  729. }
  730. };
  731.  
  732. context.mediaKeysSession.onkeystatuseschange = (
  733. event: MediaKeyMessageEvent
  734. ) => {
  735. const keySession = context.mediaKeysSession;
  736. if (!keySession) {
  737. licenseStatus.emit('error', new Error('invalid state'));
  738. return;
  739. }
  740. this.onKeyStatusChange(context);
  741. const keyStatus = context.keyStatus;
  742. licenseStatus.emit('keyStatus', keyStatus);
  743. if (keyStatus === 'expired') {
  744. this.warn(`${context.keySystem} expired for key ${keyId}`);
  745. this.renewKeySession(context);
  746. }
  747. };
  748.  
  749. const keyUsablePromise = new Promise(
  750. (resolve: (value?: void) => void, reject) => {
  751. licenseStatus.on('error', reject);
  752.  
  753. licenseStatus.on('keyStatus', (keyStatus) => {
  754. if (keyStatus.startsWith('usable')) {
  755. resolve();
  756. } else if (keyStatus === 'output-restricted') {
  757. reject(
  758. new EMEKeyError(
  759. {
  760. type: ErrorTypes.KEY_SYSTEM_ERROR,
  761. details: ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED,
  762. fatal: false,
  763. },
  764. 'HDCP level output restricted'
  765. )
  766. );
  767. } else if (keyStatus === 'internal-error') {
  768. reject(
  769. new EMEKeyError(
  770. {
  771. type: ErrorTypes.KEY_SYSTEM_ERROR,
  772. details: ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR,
  773. fatal: true,
  774. },
  775. `key status changed to "${keyStatus}"`
  776. )
  777. );
  778. } else if (keyStatus === 'expired') {
  779. reject(new Error('key expired while generating request'));
  780. } else {
  781. this.warn(`unhandled key status change "${keyStatus}"`);
  782. }
  783. });
  784. }
  785. );
  786.  
  787. return context.mediaKeysSession
  788. .generateRequest(initDataType, initData)
  789. .then(() => {
  790. this.log(
  791. `Request generated for key-session "${context.mediaKeysSession?.sessionId}" keyId: ${keyId}`
  792. );
  793. })
  794. .catch((error) => {
  795. throw new EMEKeyError(
  796. {
  797. type: ErrorTypes.KEY_SYSTEM_ERROR,
  798. details: ErrorDetails.KEY_SYSTEM_NO_SESSION,
  799. error,
  800. fatal: false,
  801. },
  802. `Error generating key-session request: ${error}`
  803. );
  804. })
  805. .then(() => keyUsablePromise)
  806. .catch((error) => {
  807. licenseStatus.removeAllListeners();
  808. this.removeSession(context);
  809. throw error;
  810. })
  811. .then(() => {
  812. licenseStatus.removeAllListeners();
  813. return context;
  814. });
  815. }
  816.  
  817. private onKeyStatusChange(mediaKeySessionContext: MediaKeySessionContext) {
  818. mediaKeySessionContext.mediaKeysSession.keyStatuses.forEach(
  819. (status: MediaKeyStatus, keyId: BufferSource) => {
  820. this.log(
  821. `key status change "${status}" for keyStatuses keyId: ${Hex.hexDump(
  822. 'buffer' in keyId
  823. ? new Uint8Array(keyId.buffer, keyId.byteOffset, keyId.byteLength)
  824. : new Uint8Array(keyId)
  825. )} session keyId: ${Hex.hexDump(
  826. new Uint8Array(mediaKeySessionContext.decryptdata.keyId || [])
  827. )} uri: ${mediaKeySessionContext.decryptdata.uri}`
  828. );
  829. mediaKeySessionContext.keyStatus = status;
  830. }
  831. );
  832. }
  833.  
  834. private fetchServerCertificate(
  835. keySystem: KeySystems
  836. ): Promise<BufferSource | void> {
  837. return new Promise((resolve, reject) => {
  838. const url = this.getServerCertificateUrl(keySystem);
  839. if (!url) {
  840. return resolve();
  841. }
  842. this.log(`Fetching serverCertificate for "${keySystem}"`);
  843. const xhr = new XMLHttpRequest();
  844. xhr.open('GET', url, true);
  845. xhr.responseType = 'arraybuffer';
  846. xhr.onreadystatechange = () => {
  847. if (xhr.readyState === XMLHttpRequest.DONE) {
  848. if (xhr.status === 200) {
  849. resolve(xhr.response);
  850. } else {
  851. reject(
  852. new EMEKeyError(
  853. {
  854. type: ErrorTypes.KEY_SYSTEM_ERROR,
  855. details:
  856. ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,
  857. fatal: true,
  858. networkDetails: xhr,
  859. },
  860. `"${keySystem}" certificate request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`
  861. )
  862. );
  863. }
  864. }
  865. };
  866. xhr.send();
  867. });
  868. }
  869.  
  870. private setMediaKeysServerCertificate(
  871. mediaKeys: MediaKeys,
  872. keySystem: KeySystems,
  873. cert: BufferSource
  874. ): Promise<MediaKeys> {
  875. return new Promise((resolve, reject) => {
  876. mediaKeys
  877. .setServerCertificate(cert)
  878. .then((success) => {
  879. this.log(
  880. `setServerCertificate ${
  881. success ? 'success' : 'not supported by CDM'
  882. } (${cert?.byteLength}) on "${keySystem}"`
  883. );
  884. resolve(mediaKeys);
  885. })
  886. .catch((error) => {
  887. reject(
  888. new EMEKeyError(
  889. {
  890. type: ErrorTypes.KEY_SYSTEM_ERROR,
  891. details:
  892. ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED,
  893. error,
  894. fatal: true,
  895. },
  896. error.message
  897. )
  898. );
  899. });
  900. });
  901. }
  902.  
  903. private renewLicense(
  904. context: MediaKeySessionContext,
  905. keyMessage: ArrayBuffer
  906. ): Promise<void> {
  907. return this.requestLicense(context, new Uint8Array(keyMessage)).then(
  908. (data: ArrayBuffer) => {
  909. return this.updateKeySession(context, new Uint8Array(data)).catch(
  910. (error) => {
  911. throw new EMEKeyError(
  912. {
  913. type: ErrorTypes.KEY_SYSTEM_ERROR,
  914. details: ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED,
  915. error,
  916. fatal: true,
  917. },
  918. error.message
  919. );
  920. }
  921. );
  922. }
  923. );
  924. }
  925.  
  926. private setupLicenseXHR(
  927. xhr: XMLHttpRequest,
  928. url: string,
  929. keysListItem: MediaKeySessionContext,
  930. licenseChallenge: Uint8Array
  931. ): Promise<{ xhr: XMLHttpRequest; licenseChallenge: Uint8Array }> {
  932. const licenseXhrSetup = this.config.licenseXhrSetup;
  933.  
  934. if (!licenseXhrSetup) {
  935. xhr.open('POST', url, true);
  936.  
  937. return Promise.resolve({ xhr, licenseChallenge });
  938. }
  939.  
  940. return Promise.resolve()
  941. .then(() => {
  942. if (!keysListItem.decryptdata) {
  943. throw new Error('Key removed');
  944. }
  945. return licenseXhrSetup.call(
  946. this.hls,
  947. xhr,
  948. url,
  949. keysListItem,
  950. licenseChallenge
  951. );
  952. })
  953. .catch((error: Error) => {
  954. if (!keysListItem.decryptdata) {
  955. // Key session removed. Cancel license request.
  956. throw error;
  957. }
  958. // let's try to open before running setup
  959. xhr.open('POST', url, true);
  960.  
  961. return licenseXhrSetup.call(
  962. this.hls,
  963. xhr,
  964. url,
  965. keysListItem,
  966. licenseChallenge
  967. );
  968. })
  969. .then((licenseXhrSetupResult) => {
  970. // if licenseXhrSetup did not yet call open, let's do it now
  971. if (!xhr.readyState) {
  972. xhr.open('POST', url, true);
  973. }
  974. const finalLicenseChallenge = licenseXhrSetupResult
  975. ? licenseXhrSetupResult
  976. : licenseChallenge;
  977. return { xhr, licenseChallenge: finalLicenseChallenge };
  978. });
  979. }
  980.  
  981. private requestLicense(
  982. keySessionContext: MediaKeySessionContext,
  983. licenseChallenge: Uint8Array
  984. ): Promise<ArrayBuffer> {
  985. return new Promise((resolve, reject) => {
  986. const url = this.getLicenseServerUrl(keySessionContext.keySystem);
  987. this.log(`Sending license request to URL: ${url}`);
  988. const xhr = new XMLHttpRequest();
  989. xhr.responseType = 'arraybuffer';
  990. xhr.onreadystatechange = () => {
  991. if (!this.hls || !keySessionContext.mediaKeysSession) {
  992. return reject(new Error('invalid state'));
  993. }
  994. if (xhr.readyState === 4) {
  995. if (xhr.status === 200) {
  996. this._requestLicenseFailureCount = 0;
  997. let data = xhr.response;
  998. this.log(
  999. `License received ${
  1000. data instanceof ArrayBuffer ? data.byteLength : data
  1001. }`
  1002. );
  1003. const licenseResponseCallback = this.config.licenseResponseCallback;
  1004. if (licenseResponseCallback) {
  1005. try {
  1006. data = licenseResponseCallback.call(
  1007. this.hls,
  1008. xhr,
  1009. url,
  1010. keySessionContext
  1011. );
  1012. } catch (error) {
  1013. this.error(error);
  1014. }
  1015. }
  1016. resolve(data);
  1017. } else {
  1018. this._requestLicenseFailureCount++;
  1019. if (
  1020. this._requestLicenseFailureCount > MAX_LICENSE_REQUEST_FAILURES ||
  1021. (xhr.status >= 400 && xhr.status < 500)
  1022. ) {
  1023. reject(
  1024. new EMEKeyError(
  1025. {
  1026. type: ErrorTypes.KEY_SYSTEM_ERROR,
  1027. details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED,
  1028. fatal: true,
  1029. networkDetails: xhr,
  1030. },
  1031. `License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`
  1032. )
  1033. );
  1034. } else {
  1035. const attemptsLeft =
  1036. MAX_LICENSE_REQUEST_FAILURES -
  1037. this._requestLicenseFailureCount +
  1038. 1;
  1039. this.warn(
  1040. `Retrying license request, ${attemptsLeft} attempts left`
  1041. );
  1042. this.requestLicense(keySessionContext, licenseChallenge).then(
  1043. resolve,
  1044. reject
  1045. );
  1046. }
  1047. }
  1048. }
  1049. };
  1050. if (
  1051. keySessionContext.licenseXhr &&
  1052. keySessionContext.licenseXhr.readyState !== XMLHttpRequest.DONE
  1053. ) {
  1054. keySessionContext.licenseXhr.abort();
  1055. }
  1056. keySessionContext.licenseXhr = xhr;
  1057.  
  1058. this.setupLicenseXHR(xhr, url, keySessionContext, licenseChallenge).then(
  1059. ({ xhr, licenseChallenge }) => {
  1060. xhr.send(licenseChallenge);
  1061. }
  1062. );
  1063. });
  1064. }
  1065.  
  1066. private onMediaAttached(
  1067. event: Events.MEDIA_ATTACHED,
  1068. data: MediaAttachedData
  1069. ) {
  1070. if (!this.config.emeEnabled) {
  1071. return;
  1072. }
  1073.  
  1074. const media = data.media;
  1075.  
  1076. // keep reference of media
  1077. this.media = media;
  1078.  
  1079. media.addEventListener('encrypted', this.onMediaEncrypted);
  1080. media.addEventListener('waitingforkey', this.onWaitingForKey);
  1081. }
  1082.  
  1083. private onMediaDetached() {
  1084. const media = this.media;
  1085. const mediaKeysList = this.mediaKeySessions;
  1086. if (media) {
  1087. media.removeEventListener('encrypted', this.onMediaEncrypted);
  1088. media.removeEventListener('waitingforkey', this.onWaitingForKey);
  1089. this.media = null;
  1090. }
  1091.  
  1092. this._requestLicenseFailureCount = 0;
  1093. this.setMediaKeysQueue = [];
  1094. this.mediaKeySessions = [];
  1095. this.keyIdToKeySessionPromise = {};
  1096. LevelKey.clearKeyUriToKeyIdMap();
  1097.  
  1098. // Close all sessions and remove media keys from the video element.
  1099. const keySessionCount = mediaKeysList.length;
  1100. EMEController.CDMCleanupPromise = Promise.all(
  1101. mediaKeysList
  1102. .map((mediaKeySessionContext) =>
  1103. this.removeSession(mediaKeySessionContext)
  1104. )
  1105. .concat(
  1106. media?.setMediaKeys(null).catch((error) => {
  1107. this.log(
  1108. `Could not clear media keys: ${error}. media.src: ${media?.src}`
  1109. );
  1110. })
  1111. )
  1112. )
  1113. .then(() => {
  1114. if (keySessionCount) {
  1115. this.log('finished closing key sessions and clearing media keys');
  1116. mediaKeysList.length = 0;
  1117. }
  1118. })
  1119. .catch((error) => {
  1120. this.log(
  1121. `Could not close sessions and clear media keys: ${error}. media.src: ${media?.src}`
  1122. );
  1123. });
  1124. }
  1125.  
  1126. private onManifestLoaded(
  1127. event: Events.MANIFEST_LOADED,
  1128. { sessionKeys }: ManifestLoadedData
  1129. ) {
  1130. if (!sessionKeys || !this.config.emeEnabled) {
  1131. return;
  1132. }
  1133. if (!this.keyFormatPromise) {
  1134. const keyFormats: KeySystemFormats[] = sessionKeys.reduce(
  1135. (formats: KeySystemFormats[], sessionKey: LevelKey) => {
  1136. if (
  1137. formats.indexOf(sessionKey.keyFormat as KeySystemFormats) === -1
  1138. ) {
  1139. formats.push(sessionKey.keyFormat as KeySystemFormats);
  1140. }
  1141. return formats;
  1142. },
  1143. []
  1144. );
  1145. this.log(
  1146. `Selecting key-system from session-keys ${keyFormats.join(', ')}`
  1147. );
  1148. this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
  1149. }
  1150. }
  1151.  
  1152. private removeSession(
  1153. mediaKeySessionContext: MediaKeySessionContext
  1154. ): Promise<void> | void {
  1155. const { mediaKeysSession, licenseXhr } = mediaKeySessionContext;
  1156. if (mediaKeysSession) {
  1157. this.log(
  1158. `Remove licenses and keys and close session ${mediaKeysSession.sessionId}`
  1159. );
  1160. mediaKeysSession.onmessage = null;
  1161. mediaKeysSession.onkeystatuseschange = null;
  1162. if (licenseXhr && licenseXhr.readyState !== XMLHttpRequest.DONE) {
  1163. licenseXhr.abort();
  1164. }
  1165. mediaKeySessionContext.mediaKeysSession =
  1166. mediaKeySessionContext.decryptdata =
  1167. mediaKeySessionContext.licenseXhr =
  1168. undefined!;
  1169. const index = this.mediaKeySessions.indexOf(mediaKeySessionContext);
  1170. if (index > -1) {
  1171. this.mediaKeySessions.splice(index, 1);
  1172. }
  1173. return mediaKeysSession
  1174. .remove()
  1175. .catch((error) => {
  1176. this.log(`Could not remove session: ${error}`);
  1177. })
  1178. .then(() => {
  1179. return mediaKeysSession.close();
  1180. })
  1181. .catch((error) => {
  1182. this.log(`Could not close session: ${error}`);
  1183. });
  1184. }
  1185. }
  1186. }
  1187.  
  1188. class EMEKeyError extends Error {
  1189. public readonly data: ErrorData;
  1190. constructor(data: ErrorData, message: string) {
  1191. super(message);
  1192. this.data = data;
  1193. data.err = data.error;
  1194. }
  1195. }
  1196.  
  1197. export default EMEController;