Refactor HLS API: Get rid of specialized Manifest* classes, use Input instead. Add unhydrated InputTracks, InputTrack compatibility, InputTrack queries

This commit is contained in:
Vanilagy
2026-03-03 15:28:26 +01:00
parent b0604150c1
commit cfb5418fa0
29 changed files with 1841 additions and 904 deletions
+125
View File
@@ -0,0 +1,125 @@
import { AES_128_BLOCK_SIZE, createAesDecryptStream } from './aes';
import { ENCRYPTION_KEY_CACHE_GROUP, Input } from './input';
import { SegmentedInput } from './segmented-input';
import { arrayArgmin, assert } from './misc';
import { readBytes, Reader } from './reader';
import { ReadableStreamSource, Source } from './source';
export type SegmentEncryptionInfo = {
method: 'AES-128';
keyUri: string;
iv: Uint8Array | null;
keyFormat: string;
};
export type SegmentLocation = {
path: string;
offset: number;
length: number | null;
};
export class Segment {
readonly variant: SegmentedInput;
readonly location: SegmentLocation;
readonly relativeTimestamp: number;
readonly duration: number;
readonly title: string | null;
readonly encryption: SegmentEncryptionInfo | null;
readonly firstSegment: Segment | null;
readonly initSegment: Segment | null;
constructor(
variant: SegmentedInput,
location: SegmentLocation,
relativeTimestamp: number,
duration: number,
title: string | null,
encryption: SegmentEncryptionInfo | null,
firstSegment: Segment | null,
initSegment: Segment | null,
) {
this.variant = variant;
this.location = location;
this.relativeTimestamp = relativeTimestamp;
this.duration = duration;
this.title = title;
this.encryption = encryption;
this.firstSegment = firstSegment;
this.initSegment = initSegment;
}
toInput(): Promise<Input> {
const cacheEntry = this.variant._inputCache.find(x => x.segment === this);
if (cacheEntry) {
cacheEntry.age = this.variant._nextInputCacheAge++;
return cacheEntry.inputPromise;
}
const inputPromise = (async () => {
let initInputPromise: Promise<Input> | null = null;
if (this.initSegment || this.firstSegment) {
initInputPromise = (this.initSegment ?? this.firstSegment)!.toInput();
}
let source: Source;
const needsSlice = this.location.offset > 0 || this.location.length !== null;
if (!this.encryption) {
source = await this.variant.input._getSourceCached({ path: this.location.path });
if (needsSlice) {
source = source.slice(this.location.offset, this.location.length ?? undefined);
}
} else {
assert(this.encryption.iv);
let ciphertextSource = await this.variant.input._getSourceCached({ path: this.location.path });
if (needsSlice) {
// Slice before decrypting
ciphertextSource = ciphertextSource.slice(this.location.offset, this.location.length ?? undefined);
}
const ciphertextReader = new Reader(ciphertextSource);
const stream = createAesDecryptStream(ciphertextReader, async () => {
const keySource = await this.variant.input._getSourceCached(
{ path: this.encryption!.keyUri },
ENCRYPTION_KEY_CACHE_GROUP,
);
const keyReader = new Reader(keySource);
const keySlice = await keyReader.requestSlice(0, AES_128_BLOCK_SIZE);
if (!keySlice) {
throw new Error('Invalid AES-128 key; expected at least 16 bytes of data.');
}
const key = readBytes(keySlice, AES_128_BLOCK_SIZE);
return { key, iv: this.encryption!.iv! };
});
source = new ReadableStreamSource(stream);
}
const initInput = await initInputPromise;
return new Input({
source,
formats: this.variant.input._formats,
initInput: initInput ?? undefined,
});
})();
this.variant._inputCache.push({
segment: this,
inputPromise,
age: this.variant._nextInputCacheAge++,
});
const MAX_INPUT_CACHE_SIZE = 4;
if (this.variant._inputCache.length > MAX_INPUT_CACHE_SIZE) {
const minAgeIndex = arrayArgmin(this.variant._inputCache, x => x.age);
this.variant._inputCache.splice(minAgeIndex, 1);
}
return inputPromise;
}
}