src/loader/key-loader.ts
- /*
- * Decrypt key Loader
- */
- import { ErrorTypes, ErrorDetails } from '../errors';
- import { logger } from '../utils/logger';
- import {
- LoaderStats,
- LoaderResponse,
- LoaderConfiguration,
- LoaderCallbacks,
- Loader,
- KeyLoaderContext,
- } from '../types/loader';
- import { LoadError } from './fragment-loader';
- import type { HlsConfig } from '../hls';
- import type { Fragment } from '../loader/fragment';
- import type { ComponentAPI } from '../types/component-api';
- import type { KeyLoadedData } from '../types/events';
-
- export default class KeyLoader implements ComponentAPI {
- private readonly config: HlsConfig;
- public loader: Loader<KeyLoaderContext> | null = null;
- public decryptkey: Uint8Array | null = null;
- public decrypturl: string | null = null;
-
- constructor(config: HlsConfig) {
- this.config = config;
- }
-
- abort(): void {
- this.loader?.abort();
- }
-
- destroy(): void {
- if (this.loader) {
- this.loader.destroy();
- this.loader = null;
- }
- }
-
- load(frag: Fragment): Promise<KeyLoadedData | void> | never {
- const type = frag.type;
- const loader = this.loader;
- if (!frag.decryptdata) {
- throw new Error('Missing decryption data on fragment in onKeyLoading');
- }
-
- // Load the key if the uri is different from previous one, or if the decrypt key has not yet been retrieved
- const uri = frag.decryptdata.uri;
- if (uri !== this.decrypturl || this.decryptkey === null) {
- const config = this.config;
- if (loader) {
- logger.warn(`abort previous key loader for type:${type}`);
- loader.abort();
- }
- if (!uri) {
- throw new Error('key uri is falsy');
- }
- const Loader = config.loader;
- const keyLoader =
- (frag.keyLoader =
- this.loader =
- new Loader(config) as Loader<KeyLoaderContext>);
- this.decrypturl = uri;
- this.decryptkey = null;
-
- return new Promise((resolve, reject) => {
- const loaderContext: KeyLoaderContext = {
- url: uri,
- frag: frag,
- part: null,
- responseType: 'arraybuffer',
- };
-
- // maxRetry is 0 so that instead of retrying the same key on the same variant multiple times,
- // key-loader will trigger an error and rely on stream-controller to handle retry logic.
- // this will also align retry logic with fragment-loader
- const loaderConfig: LoaderConfiguration = {
- timeout: config.fragLoadingTimeOut,
- maxRetry: 0,
- retryDelay: config.fragLoadingRetryDelay,
- maxRetryDelay: config.fragLoadingMaxRetryTimeout,
- highWaterMark: 0,
- };
-
- const loaderCallbacks: LoaderCallbacks<KeyLoaderContext> = {
- onSuccess: (
- response: LoaderResponse,
- stats: LoaderStats,
- context: KeyLoaderContext,
- networkDetails: any
- ) => {
- const frag = context.frag;
- if (!frag.decryptdata) {
- logger.error('after key load, decryptdata unset');
- return reject(
- new LoadError({
- type: ErrorTypes.NETWORK_ERROR,
- details: ErrorDetails.KEY_LOAD_ERROR,
- fatal: false,
- frag,
- networkDetails,
- })
- );
- }
- this.decryptkey = frag.decryptdata.key = new Uint8Array(
- response.data as ArrayBuffer
- );
-
- // detach fragment key loader on load success
- frag.keyLoader = null;
- this.loader = null;
- resolve({ frag });
- },
-
- onError: (
- error: { code: number; text: string },
- context: KeyLoaderContext,
- networkDetails: any
- ) => {
- this.resetLoader(context.frag, keyLoader);
- reject(
- new LoadError({
- type: ErrorTypes.NETWORK_ERROR,
- details: ErrorDetails.KEY_LOAD_ERROR,
- fatal: false,
- frag,
- networkDetails,
- })
- );
- },
-
- onTimeout: (
- stats: LoaderStats,
- context: KeyLoaderContext,
- networkDetails: any
- ) => {
- this.resetLoader(context.frag, keyLoader);
- reject(
- new LoadError({
- type: ErrorTypes.NETWORK_ERROR,
- details: ErrorDetails.KEY_LOAD_TIMEOUT,
- fatal: false,
- frag,
- networkDetails,
- })
- );
- },
-
- onAbort: (
- stats: LoaderStats,
- context: KeyLoaderContext,
- networkDetails: any
- ) => {
- this.resetLoader(context.frag, keyLoader);
- reject(
- new LoadError({
- type: ErrorTypes.NETWORK_ERROR,
- details: ErrorDetails.INTERNAL_ABORTED,
- fatal: false,
- frag,
- networkDetails,
- })
- );
- },
- };
-
- keyLoader.load(loaderContext, loaderConfig, loaderCallbacks);
- });
- } else if (this.decryptkey) {
- // Return the key if it's already been loaded
- frag.decryptdata.key = this.decryptkey;
- return Promise.resolve({ frag });
- }
- return Promise.resolve();
- }
-
- private resetLoader(frag: Fragment, loader: Loader<KeyLoaderContext>) {
- if (this.loader === loader) {
- this.loader = null;
- }
- loader.destroy();
- }
- }