-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Description
What do you want to do with Hls.js?
Hey,
I wanted to build my own hlsjs loader for AES-128 key exchange (No real DRM system in use), therefor I created a piece of Wasm code I now want to utilize in a custom loader for hls.js, but for some reason the player never reaches the point of actually trying to fetch a key from my key server and I don't understand why that happens ...
CustomLoader.ts:
/* global sessionStorage, console, window, XMLHttpRequest */
/** For config passed from Hls.js to the loader constructor */
interface HlsLoaderConfig {
debug?: boolean;
}
/** Fields for the context that Hls.js passes to load(...) */
interface LoaderContext {
type: string; // e.g. "key", "manifest", "level", "audioTrack", ...
url: string; // e.g. "kms://someKeyId" or "https://..."
method?: string;
responseType?: XMLHttpRequestResponseType;
headers?: Record<string, string>;
body?: Document | BodyInit | null;
}
/** The callbacks Hls.js uses for success/failure */
interface LoaderCallbacks {
onSuccess(
response: { data: any; url: string },
stats: LoaderStats,
context: LoaderContext,
networkDetails: any
): void;
onError(
error: { code: number; text: string },
context: LoaderContext,
networkDetails: any
): void;
}
/** Stats object used by Hls.js */
interface LoaderStats {
trequest: number;
tfirst: number;
tload: number;
tparsed: number;
loaded: number;
total: number;
aborted: boolean;
retry: number;
chunkCount: number;
parsing: {
start: number;
end: number;
};
}
/**
* CustomKeyLoader:
* - If (type==='key') or URL starts with 'kms://', we call `KeyMerchant.create_media_session`
* - Otherwise do fallback XHR
*/
export class CustomKeyLoader {
private config: HlsLoaderConfig;
private keyCache: Record<string, ArrayBuffer>;
private loadTimeout: number;
constructor(config: HlsLoaderConfig) {
console.log('[CustomKeyLoader] constructor =>', config);
this.config = config;
this.keyCache = {};
this.loadTimeout = 10000;
}
public load(
context: LoaderContext,
hlsConfig: any,
callbacks: LoaderCallbacks
): void {
console.log(
'[CustomKeyLoader] load => type=%s, url=%s',
context.type,
context.url
);
// If it's a key request, handle it via create_media_session
const isKeyRequest =
context.type === 'key' || (context.url && context.url.startsWith('kms://'));
if (isKeyRequest) {
console.log('[CustomKeyLoader] => KEY REQUEST detected =>', context.url);
// Force arraybuffer for the key
context.responseType = 'arraybuffer';
this._handleKmsKey(context, callbacks);
} else {
console.log('[CustomKeyLoader] => normal request => fallback XHR =>', context.url);
this._fallbackXhr(context, callbacks);
}
}
public abort(): void {
console.log('[CustomKeyLoader] abort() => not implemented');
}
private _handleKmsKey(
context: LoaderContext,
callbacks: LoaderCallbacks
): void {
const startTime = performance.now();
const stats: LoaderStats = {
trequest: startTime,
tfirst: startTime,
tload: startTime,
tparsed: startTime,
loaded: 0,
total: 0,
aborted: false,
retry: 0,
chunkCount: 1,
parsing: {
start: startTime,
end: startTime
}
};
const url = context.url;
const keyId = url.replace('kms://', '');
console.log('[CustomKeyLoader] _handleKmsKey => keyId=', keyId);
// Check cache
if (this.keyCache[keyId]) {
console.log('[CustomKeyLoader] => key in cache =>', keyId);
const cachedKey = this.keyCache[keyId];
stats.loaded = cachedKey.byteLength;
stats.total = cachedKey.byteLength;
const now = performance.now();
stats.tload = now;
stats.tparsed = now;
stats.parsing.start = stats.trequest;
stats.parsing.end = now;
callbacks.onSuccess({ data: cachedKey, url }, stats, context, null);
return;
}
// Check token
const kmsToken = sessionStorage.getItem('kmst') || '';
if (!kmsToken) {
console.error('[CustomKeyLoader] => No KMS token => "kmst"');
callbacks.onError({ code: 401, text: 'Missing kmst token' }, context, null);
return;
}
(async () => {
try {
console.log('[CustomKeyLoader] => calling create_media_session =>', keyId);
const keyData = await window.KeyMerchant.create_media_session(kmsToken, keyId);
if (!keyData) {
throw new Error('No data from create_media_session');
}
const keyBuffer = (keyData as Uint8Array).buffer || keyData;
this.keyCache[keyId] = keyBuffer as ArrayBuffer;
console.log('[CustomKeyLoader] => key retrieved =>', keyId);
const endTime = performance.now();
stats.loaded = (keyBuffer as ArrayBuffer).byteLength;
stats.total = stats.loaded;
stats.tload = endTime;
stats.tparsed = endTime;
stats.parsing.start = startTime;
stats.parsing.end = endTime;
callbacks.onSuccess({ data: keyBuffer, url }, stats, context, null);
} catch (err: any) {
console.error('[CustomKeyLoader] => create_media_session error =>', err);
callbacks.onError(
{ code: 500, text: err.message || 'Key retrieval error' },
context,
null
);
}
})();
}
private _fallbackXhr(context: LoaderContext, callbacks: LoaderCallbacks): void {
const startTime = performance.now();
const stats: LoaderStats = {
trequest: startTime,
tfirst: startTime,
tload: startTime,
tparsed: startTime,
loaded: 0,
total: 0,
aborted: false,
retry: 0,
chunkCount: 1,
parsing: {
start: startTime,
end: startTime
}
};
console.log('[CustomKeyLoader] => _fallbackXhr =>', context.url);
const xhr = new XMLHttpRequest();
xhr.open(context.method || 'GET', context.url, true);
// For manifests, Hls.js typically sets responseType='text'
// For segments, it may set 'arraybuffer'
xhr.responseType = context.responseType || 'text';
// Attach optional CDN token
const cdnToken = sessionStorage.getItem('cdnt') || '';
if (cdnToken) {
console.log('[CustomKeyLoader] => attaching cdnt =>', cdnToken);
xhr.setRequestHeader('Authorization', cdnToken);
}
xhr.withCredentials = true;
xhr.timeout = this.loadTimeout;
if (context.headers) {
console.log('[CustomKeyLoader] => additional headers =>', context.headers);
for (const h in context.headers) {
xhr.setRequestHeader(h, context.headers[h]);
}
}
xhr.onload = () => {
const endTime = performance.now();
stats.tload = endTime;
stats.tparsed = endTime;
let loaded = 0;
if (xhr.response instanceof ArrayBuffer) {
loaded = xhr.response.byteLength;
} else if (typeof xhr.response === 'string') {
loaded = xhr.response.length;
}
stats.loaded = loaded;
stats.total = loaded;
stats.parsing.start = startTime;
stats.parsing.end = endTime;
console.log('[CustomKeyLoader] => fallback XHR success =>', context.url);
callbacks.onSuccess({ data: xhr.response, url: context.url }, stats, context, null);
};
xhr.onerror = () => {
console.error('[CustomKeyLoader] => fallback XHR error =>', xhr.status, xhr.statusText);
callbacks.onError({ code: xhr.status, text: xhr.statusText }, context, null);
};
console.log('[CustomKeyLoader] => sending fallback XHR =>', context.url);
xhr.send((context.body as Document | XMLHttpRequestBodyInit | null) || null);
}
}
// Attach to window if needed:
(window as any).CustomKeyLoader = CustomKeyLoader;The line that does not work, but which I expect to work, is this:
const isKeyRequest =
context.type === 'key' || (context.url && context.url.startsWith('kms://'));
if (isKeyRequest) { ...I never ever drop into context.type === 'key' for what ever reason. Can please somebody tell me why it's not working, how can I intercept the key fetching of the player ???
I want to accomplish the following scenario:
-> Payer sees "kms://keyId" in HLS playlist/manifest -> calls KeyMerchant.create_media_session(kmsToken, keyId) -> get's back the AES-128 key as Uint8Array
For the rest of the code, at least so far, I can confirm that it's working, e.g. "normal" media segments are getting fetched fine using the right token from my browsers' session storage (cdnt) ... But just this part with context.type === 'key' isnt working as it seems. I also double checked that enableSoftwareAES is enabled
Thanks in advance.