357 lines
11 KiB
TypeScript
357 lines
11 KiB
TypeScript
import { ErrorTypes, ErrorDetails } from '../errors';
|
|
import {
|
|
LoaderStats,
|
|
LoaderResponse,
|
|
LoaderConfiguration,
|
|
LoaderCallbacks,
|
|
Loader,
|
|
KeyLoaderContext,
|
|
PlaylistLevelType,
|
|
} from '../types/loader';
|
|
import { LoadError } from './fragment-loader';
|
|
import type { HlsConfig } from '../config';
|
|
import type { Fragment } from '../loader/fragment';
|
|
import type { ComponentAPI } from '../types/component-api';
|
|
import type { KeyLoadedData } from '../types/events';
|
|
import type { LevelKey } from './level-key';
|
|
import type EMEController from '../controller/eme-controller';
|
|
import type { MediaKeySessionContext } from '../controller/eme-controller';
|
|
import type { KeySystemFormats } from '../utils/mediakeys-helper';
|
|
|
|
export interface KeyLoaderInfo {
|
|
decryptdata: LevelKey;
|
|
keyLoadPromise: Promise<KeyLoadedData> | null;
|
|
loader: Loader<KeyLoaderContext> | null;
|
|
mediaKeySessionContext: MediaKeySessionContext | null;
|
|
}
|
|
export default class KeyLoader implements ComponentAPI {
|
|
private readonly config: HlsConfig;
|
|
public keyUriToKeyInfo: { [keyuri: string]: KeyLoaderInfo } = {};
|
|
public emeController: EMEController | null = null;
|
|
|
|
constructor(config: HlsConfig) {
|
|
this.config = config;
|
|
}
|
|
|
|
abort(type?: PlaylistLevelType) {
|
|
for (const uri in this.keyUriToKeyInfo) {
|
|
const loader = this.keyUriToKeyInfo[uri].loader;
|
|
if (loader) {
|
|
if (type && type !== loader.context?.frag.type) {
|
|
return;
|
|
}
|
|
loader.abort();
|
|
}
|
|
}
|
|
}
|
|
|
|
detach() {
|
|
for (const uri in this.keyUriToKeyInfo) {
|
|
const keyInfo = this.keyUriToKeyInfo[uri];
|
|
// Remove cached EME keys on detach
|
|
if (
|
|
keyInfo.mediaKeySessionContext ||
|
|
keyInfo.decryptdata.isCommonEncryption
|
|
) {
|
|
delete this.keyUriToKeyInfo[uri];
|
|
}
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.detach();
|
|
for (const uri in this.keyUriToKeyInfo) {
|
|
const loader = this.keyUriToKeyInfo[uri].loader;
|
|
if (loader) {
|
|
loader.destroy();
|
|
}
|
|
}
|
|
this.keyUriToKeyInfo = {};
|
|
}
|
|
|
|
createKeyLoadError(
|
|
frag: Fragment,
|
|
details: ErrorDetails = ErrorDetails.KEY_LOAD_ERROR,
|
|
error: Error,
|
|
networkDetails?: any,
|
|
response?: { url: string; data: undefined; code: number; text: string },
|
|
): LoadError {
|
|
return new LoadError({
|
|
type: ErrorTypes.NETWORK_ERROR,
|
|
details,
|
|
fatal: false,
|
|
frag,
|
|
response,
|
|
error,
|
|
networkDetails,
|
|
});
|
|
}
|
|
|
|
loadClear(
|
|
loadingFrag: Fragment,
|
|
encryptedFragments: Fragment[],
|
|
): void | Promise<void> {
|
|
if (this.emeController && this.config.emeEnabled) {
|
|
// access key-system with nearest key on start (loaidng frag is unencrypted)
|
|
const { sn, cc } = loadingFrag;
|
|
for (let i = 0; i < encryptedFragments.length; i++) {
|
|
const frag = encryptedFragments[i];
|
|
if (
|
|
cc <= frag.cc &&
|
|
(sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn)
|
|
) {
|
|
this.emeController
|
|
.selectKeySystemFormat(frag)
|
|
.then((keySystemFormat) => {
|
|
frag.setKeyFormat(keySystemFormat);
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
load(frag: Fragment): Promise<KeyLoadedData> {
|
|
if (!frag.decryptdata && frag.encrypted && this.emeController) {
|
|
// Multiple keys, but none selected, resolve in eme-controller
|
|
return this.emeController
|
|
.selectKeySystemFormat(frag)
|
|
.then((keySystemFormat) => {
|
|
return this.loadInternal(frag, keySystemFormat);
|
|
});
|
|
}
|
|
|
|
return this.loadInternal(frag);
|
|
}
|
|
|
|
loadInternal(
|
|
frag: Fragment,
|
|
keySystemFormat?: KeySystemFormats,
|
|
): Promise<KeyLoadedData> {
|
|
if (keySystemFormat) {
|
|
frag.setKeyFormat(keySystemFormat);
|
|
}
|
|
const decryptdata = frag.decryptdata;
|
|
if (!decryptdata) {
|
|
const error = new Error(
|
|
keySystemFormat
|
|
? `Expected frag.decryptdata to be defined after setting format ${keySystemFormat}`
|
|
: 'Missing decryption data on fragment in onKeyLoading',
|
|
);
|
|
return Promise.reject(
|
|
this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, error),
|
|
);
|
|
}
|
|
const uri = decryptdata.uri;
|
|
if (!uri) {
|
|
return Promise.reject(
|
|
this.createKeyLoadError(
|
|
frag,
|
|
ErrorDetails.KEY_LOAD_ERROR,
|
|
new Error(`Invalid key URI: "${uri}"`),
|
|
),
|
|
);
|
|
}
|
|
let keyInfo = this.keyUriToKeyInfo[uri];
|
|
|
|
if (keyInfo?.decryptdata.key) {
|
|
decryptdata.key = keyInfo.decryptdata.key;
|
|
return Promise.resolve({ frag, keyInfo });
|
|
}
|
|
// Return key load promise as long as it does not have a mediakey session with an unusable key status
|
|
if (keyInfo?.keyLoadPromise) {
|
|
switch (keyInfo.mediaKeySessionContext?.keyStatus) {
|
|
case undefined:
|
|
case 'status-pending':
|
|
case 'usable':
|
|
case 'usable-in-future':
|
|
return keyInfo.keyLoadPromise.then((keyLoadedData) => {
|
|
// Return the correct fragment with updated decryptdata key and loaded keyInfo
|
|
decryptdata.key = keyLoadedData.keyInfo.decryptdata.key;
|
|
return { frag, keyInfo };
|
|
});
|
|
}
|
|
// If we have a key session and status and it is not pending or usable, continue
|
|
// This will go back to the eme-controller for expired keys to get a new keyLoadPromise
|
|
}
|
|
|
|
// Load the key or return the loading promise
|
|
keyInfo = this.keyUriToKeyInfo[uri] = {
|
|
decryptdata,
|
|
keyLoadPromise: null,
|
|
loader: null,
|
|
mediaKeySessionContext: null,
|
|
};
|
|
|
|
switch (decryptdata.method) {
|
|
case 'ISO-23001-7':
|
|
case 'SAMPLE-AES':
|
|
case 'SAMPLE-AES-CENC':
|
|
case 'SAMPLE-AES-CTR':
|
|
if (decryptdata.keyFormat === 'identity') {
|
|
// loadKeyHTTP handles http(s) and data URLs
|
|
return this.loadKeyHTTP(keyInfo, frag);
|
|
}
|
|
return this.loadKeyEME(keyInfo, frag);
|
|
case 'AES-128':
|
|
return this.loadKeyHTTP(keyInfo, frag);
|
|
default:
|
|
return Promise.reject(
|
|
this.createKeyLoadError(
|
|
frag,
|
|
ErrorDetails.KEY_LOAD_ERROR,
|
|
new Error(
|
|
`Key supplied with unsupported METHOD: "${decryptdata.method}"`,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
loadKeyEME(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
|
|
const keyLoadedData: KeyLoadedData = { frag, keyInfo };
|
|
if (this.emeController && this.config.emeEnabled) {
|
|
const keySessionContextPromise =
|
|
this.emeController.loadKey(keyLoadedData);
|
|
if (keySessionContextPromise) {
|
|
return (keyInfo.keyLoadPromise = keySessionContextPromise.then(
|
|
(keySessionContext) => {
|
|
keyInfo.mediaKeySessionContext = keySessionContext;
|
|
return keyLoadedData;
|
|
},
|
|
)).catch((error) => {
|
|
// Remove promise for license renewal or retry
|
|
keyInfo.keyLoadPromise = null;
|
|
throw error;
|
|
});
|
|
}
|
|
}
|
|
return Promise.resolve(keyLoadedData);
|
|
}
|
|
|
|
loadKeyHTTP(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
|
|
const config = this.config;
|
|
const Loader = config.loader;
|
|
const keyLoader = new Loader(config) as Loader<KeyLoaderContext>;
|
|
frag.keyLoader = keyInfo.loader = keyLoader;
|
|
|
|
return (keyInfo.keyLoadPromise = new Promise((resolve, reject) => {
|
|
const loaderContext: KeyLoaderContext = {
|
|
keyInfo,
|
|
frag,
|
|
responseType: 'arraybuffer',
|
|
url: keyInfo.decryptdata.uri,
|
|
};
|
|
|
|
// 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 loadPolicy = config.keyLoadPolicy.default;
|
|
const loaderConfig: LoaderConfiguration = {
|
|
loadPolicy,
|
|
timeout: loadPolicy.maxLoadTimeMs,
|
|
maxRetry: 0,
|
|
retryDelay: 0,
|
|
maxRetryDelay: 0,
|
|
};
|
|
|
|
const loaderCallbacks: LoaderCallbacks<KeyLoaderContext> = {
|
|
onSuccess: (
|
|
response: LoaderResponse,
|
|
stats: LoaderStats,
|
|
context: KeyLoaderContext,
|
|
networkDetails: any,
|
|
) => {
|
|
const { frag, keyInfo, url: uri } = context;
|
|
if (!frag.decryptdata || keyInfo !== this.keyUriToKeyInfo[uri]) {
|
|
return reject(
|
|
this.createKeyLoadError(
|
|
frag,
|
|
ErrorDetails.KEY_LOAD_ERROR,
|
|
new Error('after key load, decryptdata unset or changed'),
|
|
networkDetails,
|
|
),
|
|
);
|
|
}
|
|
|
|
keyInfo.decryptdata.key = frag.decryptdata.key = new Uint8Array(
|
|
response.data as ArrayBuffer,
|
|
);
|
|
|
|
// detach fragment key loader on load success
|
|
frag.keyLoader = null;
|
|
keyInfo.loader = null;
|
|
resolve({ frag, keyInfo });
|
|
},
|
|
|
|
onError: (
|
|
response: { code: number; text: string },
|
|
context: KeyLoaderContext,
|
|
networkDetails: any,
|
|
stats: LoaderStats,
|
|
) => {
|
|
this.resetLoader(context);
|
|
reject(
|
|
this.createKeyLoadError(
|
|
frag,
|
|
ErrorDetails.KEY_LOAD_ERROR,
|
|
new Error(
|
|
`HTTP Error ${response.code} loading key ${response.text}`,
|
|
),
|
|
networkDetails,
|
|
{ url: loaderContext.url, data: undefined, ...response },
|
|
),
|
|
);
|
|
},
|
|
|
|
onTimeout: (
|
|
stats: LoaderStats,
|
|
context: KeyLoaderContext,
|
|
networkDetails: any,
|
|
) => {
|
|
this.resetLoader(context);
|
|
reject(
|
|
this.createKeyLoadError(
|
|
frag,
|
|
ErrorDetails.KEY_LOAD_TIMEOUT,
|
|
new Error('key loading timed out'),
|
|
networkDetails,
|
|
),
|
|
);
|
|
},
|
|
|
|
onAbort: (
|
|
stats: LoaderStats,
|
|
context: KeyLoaderContext,
|
|
networkDetails: any,
|
|
) => {
|
|
this.resetLoader(context);
|
|
reject(
|
|
this.createKeyLoadError(
|
|
frag,
|
|
ErrorDetails.INTERNAL_ABORTED,
|
|
new Error('key loading aborted'),
|
|
networkDetails,
|
|
),
|
|
);
|
|
},
|
|
};
|
|
|
|
keyLoader.load(loaderContext, loaderConfig, loaderCallbacks);
|
|
}));
|
|
}
|
|
|
|
private resetLoader(context: KeyLoaderContext) {
|
|
const { frag, keyInfo, url: uri } = context;
|
|
const loader = keyInfo.loader;
|
|
if (frag.keyLoader === loader) {
|
|
frag.keyLoader = null;
|
|
keyInfo.loader = null;
|
|
}
|
|
delete this.keyUriToKeyInfo[uri];
|
|
if (loader) {
|
|
loader.destroy();
|
|
}
|
|
}
|
|
}
|