src/loader/key-loader.ts
/*
* Decrypt key Loader
*/
import { Events } from '../events';
import { ErrorTypes, ErrorDetails } from '../errors';
import { logger } from '../utils/logger';
import type Hls from '../hls';
import { Fragment } from './fragment';
import {
LoaderStats,
LoaderResponse,
LoaderContext,
LoaderConfiguration,
LoaderCallbacks,
Loader,
FragmentLoaderContext,
} from '../types/loader';
import type { ComponentAPI } from '../types/component-api';
import type { KeyLoadingData } from '../types/events';
interface KeyLoaderContext extends LoaderContext {
frag: Fragment;
}
export default class KeyLoader implements ComponentAPI {
private hls: Hls;
public loaders = {};
public decryptkey: Uint8Array | null = null;
public decrypturl: string | null = null;
constructor(hls: Hls) {
this.hls = hls;
this._registerListeners();
}
private _registerListeners() {
this.hls.on(Events.KEY_LOADING, this.onKeyLoading, this);
}
private _unregisterListeners() {
this.hls.off(Events.KEY_LOADING, this.onKeyLoading);
}
destroy(): void {
this._unregisterListeners();
for (const loaderName in this.loaders) {
const loader = this.loaders[loaderName];
if (loader) {
loader.destroy();
}
}
this.loaders = {};
}
onKeyLoading(event: Events.KEY_LOADING, data: KeyLoadingData) {
const { frag } = data;
const type = frag.type;
const loader = this.loaders[type];
if (!frag.decryptdata) {
logger.warn('Missing decryption data on fragment in onKeyLoading');
return;
}
// 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.hls.config;
if (loader) {
logger.warn(`abort previous key loader for type:${type}`);
loader.abort();
}
if (!uri) {
logger.warn('key uri is falsy');
return;
}
const Loader = config.loader;
const fragLoader = (frag.loader = this.loaders[type] = new Loader(
config
) as Loader<FragmentLoaderContext>);
this.decrypturl = uri;
this.decryptkey = null;
const loaderContext: KeyLoaderContext = {
url: uri,
frag: frag,
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: this.loadsuccess.bind(this),
onError: this.loaderror.bind(this),
onTimeout: this.loadtimeout.bind(this),
};
fragLoader.load(loaderContext, loaderConfig, loaderCallbacks);
} else if (this.decryptkey) {
// Return the key if it's already been loaded
frag.decryptdata.key = this.decryptkey;
this.hls.trigger(Events.KEY_LOADED, { frag: frag });
}
}
loadsuccess(
response: LoaderResponse,
stats: LoaderStats,
context: KeyLoaderContext
) {
const frag = context.frag;
if (!frag.decryptdata) {
logger.error('after key load, decryptdata unset');
return;
}
this.decryptkey = frag.decryptdata.key = new Uint8Array(
response.data as ArrayBuffer
);
// detach fragment loader on load success
frag.loader = null;
delete this.loaders[frag.type];
this.hls.trigger(Events.KEY_LOADED, { frag: frag });
}
loaderror(response: LoaderResponse, context: KeyLoaderContext) {
const frag = context.frag;
const loader = frag.loader;
if (loader) {
loader.abort();
}
delete this.loaders[frag.type];
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.KEY_LOAD_ERROR,
fatal: false,
frag,
response,
});
}
loadtimeout(stats: LoaderStats, context: KeyLoaderContext) {
const frag = context.frag;
const loader = frag.loader;
if (loader) {
loader.abort();
}
delete this.loaders[frag.type];
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.KEY_LOAD_TIMEOUT,
fatal: false,
frag,
});
}
}