Home Reference Source

src/loader/key-loader.ts

  1. import { ErrorTypes, ErrorDetails } from '../errors';
  2. import {
  3. LoaderStats,
  4. LoaderResponse,
  5. LoaderConfiguration,
  6. LoaderCallbacks,
  7. Loader,
  8. KeyLoaderContext,
  9. } from '../types/loader';
  10. import { LoadError } from './fragment-loader';
  11. import type { HlsConfig } from '../hls';
  12. import type { Fragment } from '../loader/fragment';
  13. import type { ComponentAPI } from '../types/component-api';
  14. import type { KeyLoadedData } from '../types/events';
  15. import type { LevelKey } from './level-key';
  16. import type EMEController from '../controller/eme-controller';
  17. import type { MediaKeySessionContext } from '../controller/eme-controller';
  18. import type { KeySystemFormats } from '../utils/mediakeys-helper';
  19.  
  20. export interface KeyLoaderInfo {
  21. decryptdata: LevelKey;
  22. keyLoadPromise: Promise<KeyLoadedData> | null;
  23. loader: Loader<KeyLoaderContext> | null;
  24. mediaKeySessionContext: MediaKeySessionContext | null;
  25. }
  26. export default class KeyLoader implements ComponentAPI {
  27. private readonly config: HlsConfig;
  28. public keyUriToKeyInfo: { [keyuri: string]: KeyLoaderInfo } = {};
  29. public emeController: EMEController | null = null;
  30.  
  31. constructor(config: HlsConfig) {
  32. this.config = config;
  33. }
  34.  
  35. abort() {
  36. for (const uri in this.keyUriToKeyInfo) {
  37. const loader = this.keyUriToKeyInfo[uri].loader;
  38. if (loader) {
  39. loader.abort();
  40. }
  41. }
  42. }
  43.  
  44. detach() {
  45. for (const uri in this.keyUriToKeyInfo) {
  46. const keyInfo = this.keyUriToKeyInfo[uri];
  47. // Remove cached EME keys on detach
  48. if (
  49. keyInfo.mediaKeySessionContext ||
  50. keyInfo.decryptdata.isCommonEncryption
  51. ) {
  52. delete this.keyUriToKeyInfo[uri];
  53. }
  54. }
  55. }
  56.  
  57. destroy() {
  58. this.detach();
  59. for (const uri in this.keyUriToKeyInfo) {
  60. const loader = this.keyUriToKeyInfo[uri].loader;
  61. if (loader) {
  62. loader.destroy();
  63. }
  64. }
  65. this.keyUriToKeyInfo = {};
  66. }
  67.  
  68. createKeyLoadError(
  69. frag: Fragment,
  70. details: ErrorDetails = ErrorDetails.KEY_LOAD_ERROR,
  71. networkDetails?: any,
  72. message?: string
  73. ): LoadError {
  74. return new LoadError({
  75. type: ErrorTypes.NETWORK_ERROR,
  76. details,
  77. fatal: false,
  78. frag,
  79. networkDetails,
  80. });
  81. }
  82.  
  83. loadClear(
  84. loadingFrag: Fragment,
  85. encryptedFragments: Fragment[]
  86. ): void | Promise<void> {
  87. if (this.emeController && this.config.emeEnabled) {
  88. // access key-system with nearest key on start (loaidng frag is unencrypted)
  89. const { sn, cc } = loadingFrag;
  90. for (let i = 0; i < encryptedFragments.length; i++) {
  91. const frag = encryptedFragments[i];
  92. if (cc <= frag.cc && (sn === 'initSegment' || sn < frag.sn)) {
  93. this.emeController
  94. .selectKeySystemFormat(frag)
  95. .then((keySystemFormat) => {
  96. frag.setKeyFormat(keySystemFormat);
  97. });
  98. break;
  99. }
  100. }
  101. }
  102. }
  103.  
  104. load(frag: Fragment): Promise<KeyLoadedData> {
  105. if (!frag.decryptdata && frag.encrypted && this.emeController) {
  106. // Multiple keys, but none selected, resolve in eme-controller
  107. return this.emeController
  108. .selectKeySystemFormat(frag)
  109. .then((keySystemFormat) => {
  110. return this.loadInternal(frag, keySystemFormat);
  111. });
  112. }
  113.  
  114. return this.loadInternal(frag);
  115. }
  116.  
  117. loadInternal(
  118. frag: Fragment,
  119. keySystemFormat?: KeySystemFormats
  120. ): Promise<KeyLoadedData> {
  121. if (keySystemFormat) {
  122. frag.setKeyFormat(keySystemFormat);
  123. }
  124. const decryptdata = frag.decryptdata;
  125. if (!decryptdata) {
  126. const errorMessage = keySystemFormat
  127. ? `Expected frag.decryptdata to be defined after setting format ${keySystemFormat}`
  128. : 'Missing decryption data on fragment in onKeyLoading';
  129. return Promise.reject(
  130. this.createKeyLoadError(
  131. frag,
  132. ErrorDetails.KEY_LOAD_ERROR,
  133. null,
  134. errorMessage
  135. )
  136. );
  137. }
  138. const uri = decryptdata.uri;
  139. if (!uri) {
  140. return Promise.reject(
  141. this.createKeyLoadError(
  142. frag,
  143. ErrorDetails.KEY_LOAD_ERROR,
  144. null,
  145. `Invalid key URI: "${uri}"`
  146. )
  147. );
  148. }
  149. let keyInfo = this.keyUriToKeyInfo[uri];
  150.  
  151. if (keyInfo?.decryptdata.key) {
  152. decryptdata.key = keyInfo.decryptdata.key;
  153. return Promise.resolve({ frag, keyInfo });
  154. }
  155. // Return key load promise as long as it does not have a mediakey session with an unusable key status
  156. if (keyInfo?.keyLoadPromise) {
  157. switch (keyInfo.mediaKeySessionContext?.keyStatus) {
  158. case undefined:
  159. case 'status-pending':
  160. case 'usable':
  161. case 'usable-in-future':
  162. return keyInfo.keyLoadPromise;
  163. }
  164. // If we have a key session and status and it is not pending or usable, continue
  165. // This will go back to the eme-controller for expired keys to get a new keyLoadPromise
  166. }
  167.  
  168. // Load the key or return the loading promise
  169. keyInfo = this.keyUriToKeyInfo[uri] = {
  170. decryptdata,
  171. keyLoadPromise: null,
  172. loader: null,
  173. mediaKeySessionContext: null,
  174. };
  175.  
  176. switch (decryptdata.method) {
  177. case 'ISO-23001-7':
  178. case 'SAMPLE-AES':
  179. case 'SAMPLE-AES-CENC':
  180. case 'SAMPLE-AES-CTR':
  181. if (decryptdata.keyFormat === 'identity') {
  182. // loadKeyHTTP handles http(s) and data URLs
  183. return this.loadKeyHTTP(keyInfo, frag);
  184. }
  185. return this.loadKeyEME(keyInfo, frag);
  186. case 'AES-128':
  187. return this.loadKeyHTTP(keyInfo, frag);
  188. default:
  189. return Promise.reject(
  190. this.createKeyLoadError(
  191. frag,
  192. ErrorDetails.KEY_LOAD_ERROR,
  193. null,
  194. `Key supplied with unsupported METHOD: "${decryptdata.method}"`
  195. )
  196. );
  197. }
  198. }
  199.  
  200. loadKeyEME(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
  201. const keyLoadedData: KeyLoadedData = { frag, keyInfo };
  202. if (this.emeController && this.config.emeEnabled) {
  203. const keySessionContextPromise =
  204. this.emeController.loadKey(keyLoadedData);
  205. if (keySessionContextPromise) {
  206. return (keyInfo.keyLoadPromise = keySessionContextPromise.then(
  207. (keySessionContext) => {
  208. keyInfo.mediaKeySessionContext = keySessionContext;
  209. return keyLoadedData;
  210. }
  211. )).catch((error) => {
  212. // Remove promise for license renewal or retry
  213. keyInfo.keyLoadPromise = null;
  214. throw error;
  215. });
  216. }
  217. }
  218. return Promise.resolve(keyLoadedData);
  219. }
  220.  
  221. loadKeyHTTP(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
  222. const config = this.config;
  223. const Loader = config.loader;
  224. const keyLoader = new Loader(config) as Loader<KeyLoaderContext>;
  225. frag.keyLoader = keyInfo.loader = keyLoader;
  226.  
  227. return (keyInfo.keyLoadPromise = new Promise((resolve, reject) => {
  228. const loaderContext: KeyLoaderContext = {
  229. keyInfo,
  230. frag,
  231. responseType: 'arraybuffer',
  232. url: keyInfo.decryptdata.uri,
  233. };
  234.  
  235. // maxRetry is 0 so that instead of retrying the same key on the same variant multiple times,
  236. // key-loader will trigger an error and rely on stream-controller to handle retry logic.
  237. // this will also align retry logic with fragment-loader
  238. const loaderConfig: LoaderConfiguration = {
  239. timeout: config.fragLoadingTimeOut,
  240. maxRetry: 0,
  241. retryDelay: config.fragLoadingRetryDelay,
  242. maxRetryDelay: config.fragLoadingMaxRetryTimeout,
  243. highWaterMark: 0,
  244. };
  245.  
  246. const loaderCallbacks: LoaderCallbacks<KeyLoaderContext> = {
  247. onSuccess: (
  248. response: LoaderResponse,
  249. stats: LoaderStats,
  250. context: KeyLoaderContext,
  251. networkDetails: any
  252. ) => {
  253. const { frag, keyInfo, url: uri } = context;
  254. if (!frag.decryptdata || keyInfo !== this.keyUriToKeyInfo[uri]) {
  255. return reject(
  256. this.createKeyLoadError(
  257. frag,
  258. ErrorDetails.KEY_LOAD_ERROR,
  259. networkDetails,
  260. 'after key load, decryptdata unset or changed'
  261. )
  262. );
  263. }
  264.  
  265. keyInfo.decryptdata.key = frag.decryptdata.key = new Uint8Array(
  266. response.data as ArrayBuffer
  267. );
  268.  
  269. // detach fragment key loader on load success
  270. frag.keyLoader = null;
  271. keyInfo.loader = null;
  272. resolve({ frag, keyInfo });
  273. },
  274.  
  275. onError: (
  276. error: { code: number; text: string },
  277. context: KeyLoaderContext,
  278. networkDetails: any
  279. ) => {
  280. this.resetLoader(context);
  281. reject(
  282. this.createKeyLoadError(
  283. frag,
  284. ErrorDetails.KEY_LOAD_ERROR,
  285. networkDetails
  286. )
  287. );
  288. },
  289.  
  290. onTimeout: (
  291. stats: LoaderStats,
  292. context: KeyLoaderContext,
  293. networkDetails: any
  294. ) => {
  295. this.resetLoader(context);
  296. reject(
  297. this.createKeyLoadError(
  298. frag,
  299. ErrorDetails.KEY_LOAD_TIMEOUT,
  300. networkDetails
  301. )
  302. );
  303. },
  304.  
  305. onAbort: (
  306. stats: LoaderStats,
  307. context: KeyLoaderContext,
  308. networkDetails: any
  309. ) => {
  310. this.resetLoader(context);
  311. reject(
  312. this.createKeyLoadError(
  313. frag,
  314. ErrorDetails.INTERNAL_ABORTED,
  315. networkDetails
  316. )
  317. );
  318. },
  319. };
  320.  
  321. keyLoader.load(loaderContext, loaderConfig, loaderCallbacks);
  322. }));
  323. }
  324.  
  325. private resetLoader(context: KeyLoaderContext) {
  326. const { frag, keyInfo, url: uri } = context;
  327. const loader = keyInfo.loader;
  328. if (frag.keyLoader === loader) {
  329. frag.keyLoader = null;
  330. keyInfo.loader = null;
  331. }
  332. delete this.keyUriToKeyInfo[uri];
  333. if (loader) {
  334. loader.destroy();
  335. }
  336. }
  337. }