mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-29 03:43:50 +02:00
136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
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 {
|
|
input: SegmentedInput;
|
|
location: SegmentLocation;
|
|
timestamp: number;
|
|
relativeToUnixEpoch: boolean;
|
|
duration: number;
|
|
title: string | null;
|
|
encryption: SegmentEncryptionInfo | null;
|
|
firstSegment: Segment | null;
|
|
initSegment: Segment | null;
|
|
|
|
constructor(
|
|
input: SegmentedInput,
|
|
location: SegmentLocation,
|
|
timestamp: number,
|
|
relativeToUnixEpoch: boolean,
|
|
duration: number,
|
|
title: string | null,
|
|
encryption: SegmentEncryptionInfo | null,
|
|
firstSegment: Segment | null,
|
|
initSegment: Segment | null,
|
|
) {
|
|
this.input = input;
|
|
this.location = location;
|
|
this.timestamp = timestamp;
|
|
this.relativeToUnixEpoch = relativeToUnixEpoch;
|
|
this.duration = duration;
|
|
this.title = title;
|
|
this.encryption = encryption;
|
|
this.firstSegment = firstSegment;
|
|
this.initSegment = initSegment;
|
|
}
|
|
|
|
toInput(): Input {
|
|
const cacheEntry = this.input.inputCache.find(x => x.segment === this);
|
|
if (cacheEntry) {
|
|
cacheEntry.age = this.input.nextInputCacheAge++;
|
|
return cacheEntry.input;
|
|
}
|
|
|
|
let initInput: Input | null = null;
|
|
if (this.initSegment || this.firstSegment) {
|
|
initInput = (this.initSegment ?? this.firstSegment)!.toInput();
|
|
}
|
|
|
|
const input = new Input({
|
|
entryPath: this.location.path,
|
|
source: async (request) => {
|
|
if (request.path !== this.location.path) {
|
|
// This code technically allows for recursive .m3u8 files for example. Uncached because the added
|
|
// input adds its own layer of caching, so here we just do a passthrough.
|
|
return this.input.input._getSourceUncached(request);
|
|
}
|
|
|
|
let source: Source;
|
|
const needsSlice = this.location.offset > 0 || this.location.length !== null;
|
|
|
|
if (!this.encryption) {
|
|
source = await this.input.input._getSourceCached(request);
|
|
if (needsSlice) {
|
|
source = source.slice(this.location.offset, this.location.length ?? undefined);
|
|
}
|
|
} else {
|
|
assert(this.encryption.iv);
|
|
|
|
let ciphertextSource = await this.input.input._getSourceCached(request);
|
|
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.input.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);
|
|
}
|
|
|
|
return source;
|
|
},
|
|
formats: this.input.input._formats,
|
|
initInput: initInput ?? undefined,
|
|
});
|
|
|
|
this.input.inputCache.push({
|
|
segment: this,
|
|
input,
|
|
age: this.input.nextInputCacheAge++,
|
|
});
|
|
|
|
const MAX_INPUT_CACHE_SIZE = 4;
|
|
if (this.input.inputCache.length > MAX_INPUT_CACHE_SIZE) {
|
|
const minAgeIndex = arrayArgmin(this.input.inputCache, x => x.age);
|
|
this.input.inputCache.splice(minAgeIndex, 1);
|
|
}
|
|
|
|
return input;
|
|
}
|
|
}
|