video
This commit is contained in:
356
node_modules/hls.js/src/loader/key-loader.ts
generated
vendored
Normal file
356
node_modules/hls.js/src/loader/key-loader.ts
generated
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user