Add missing doc blocks for everything, add PathedTarget and PathedSource, few other changes & fixes

This commit is contained in:
Vanilagy
2026-04-09 15:22:45 +02:00
parent 7d053ae50d
commit 0813bcab28
16 changed files with 942 additions and 412 deletions
+9 -7
View File
@@ -23,6 +23,7 @@ import { EncodedPacket } from '../packet';
import { readAllLines } from '../reader';
import { AttributeList, canIgnoreLine } from './hls-misc';
import { HlsSegmentedInput } from './hls-segmented-input';
import { PathedSource } from '../source';
type InternalTrack = {
id: number;
@@ -66,7 +67,8 @@ export class HlsDemuxer extends Demuxer {
readMetadata() {
return this.metadataPromise ??= (async () => {
assert(this.input._entryPath !== null);
assert(this.input._source instanceof PathedSource);
const source = this.input._source;
const slice = await this.input._reader.requestEntireFile();
assert(slice);
@@ -96,7 +98,7 @@ export class HlsDemuxer extends Demuxer {
throw new Error('Incorrect M3U8 file; a line must follow the #EXT-X-STREAM-INF tag.');
}
const fullPath = joinPaths(this.input._entryPath, playlistPath);
const fullPath = joinPaths(source.rootPath, playlistPath);
const attributes = new AttributeList(line.slice(18));
const bandwidth = attributes.getAsNumber('bandwidth');
@@ -123,7 +125,7 @@ export class HlsDemuxer extends Demuxer {
);
}
const fullPath = joinPaths(this.input._entryPath, playlistPath);
const fullPath = joinPaths(source.rootPath, playlistPath);
variantStreams.push({
fullPath,
@@ -151,7 +153,7 @@ export class HlsDemuxer extends Demuxer {
let fullPath: string | null = null;
const uri = attributes.get('uri');
if (uri !== null) {
fullPath = joinPaths(this.input._entryPath, uri);
fullPath = joinPaths(source.rootPath, uri);
}
mediaTags.push({ fullPath, attributes, lineNumber: i });
@@ -159,7 +161,7 @@ export class HlsDemuxer extends Demuxer {
// iFramesOnlyTagFound = true;
} else if (line.startsWith('#EXTINF:')) {
// This is a media playlist, not a master playlist
const segmentedInput = new HlsSegmentedInput(this, this.input._entryPath, lines);
const segmentedInput = new HlsSegmentedInput(this, source.rootPath, lines);
this.segmentedInputs = [segmentedInput];
this.hasMasterPlaylist = false;
@@ -236,7 +238,7 @@ export class HlsDemuxer extends Demuxer {
return null;
}
const fullPath = joinPaths(this.input._entryPath!, uri);
const fullPath = joinPaths(source.rootPath, uri);
const segmentedInput = this.getSegmentedInputForPath(fullPath);
const input = segmentedInput.toInput();
const videoTrack = await input.getPrimaryVideoTrack();
@@ -277,7 +279,7 @@ export class HlsDemuxer extends Demuxer {
return null;
}
const fullPath = joinPaths(this.input._entryPath!, uri);
const fullPath = joinPaths(source.rootPath, uri);
const segmentedInput = this.getSegmentedInputForPath(fullPath);
const input = segmentedInput.toInput();
const audioTrack = await input.getPrimaryAudioTrack();
+34 -29
View File
@@ -15,7 +15,6 @@ import {
OutputAudioTrack,
OutputSubtitleTrack,
OutputTrack,
outputTracksArePairable,
OutputVideoTrack,
TrackType,
} from '../output';
@@ -29,7 +28,7 @@ import {
import { Writer } from '../writer';
import { EncodedPacket } from '../packet';
import { SubtitleCue, SubtitleMetadata } from '../subtitles';
import { NullTarget, Target } from '../target';
import { NullTarget, PathedTarget, Target, TargetRequest } from '../target';
type HlsTrackData = {
track: OutputTrack;
@@ -105,8 +104,8 @@ export class HlsMuxer extends Muxer {
playlistDeclarations: PlaylistDeclaration[] = [];
constructor(output: Output, format: HlsOutputFormat) {
if (!output._targetIsFunction()) {
throw new TypeError('HLS outputs require `OutputOptions.target` to be a function.');
if (!(output._target instanceof PathedTarget)) {
throw new TypeError('HLS outputs require `OutputOptions.target` to be a PathedTarget.');
}
super(output);
@@ -173,7 +172,7 @@ export class HlsMuxer extends Muxer {
continue;
}
if (!outputTracksArePairable(track, otherTrack)) {
if (!track.canBePairedWith(otherTrack)) {
continue;
}
@@ -807,7 +806,8 @@ export class HlsMuxer extends Muxer {
let relativeSegmentPath: string;
let fullSegmentPath: string;
assert(this.output._rootPath !== null);
assert(this.output._target instanceof PathedTarget);
const pathedTarget = this.output._target;
if (this.singleFilePerPlaylist) {
if (playlist.singleFile === null) {
@@ -822,7 +822,7 @@ export class HlsMuxer extends Muxer {
validateSegmentPath(relativeSegmentPath);
fullSegmentPath = joinPaths(
joinPaths(this.output._rootPath, playlist.path),
joinPaths(pathedTarget.rootPath, playlist.path),
relativeSegmentPath,
);
@@ -838,7 +838,7 @@ export class HlsMuxer extends Muxer {
} else {
relativeSegmentPath = playlist.singleFile.path;
fullSegmentPath = joinPaths(
joinPaths(this.output._rootPath, playlist.path),
joinPaths(pathedTarget.rootPath, playlist.path),
relativeSegmentPath,
);
}
@@ -853,8 +853,7 @@ export class HlsMuxer extends Muxer {
relativeSegmentPath = await this.getSegmentPath(segmentInfo);
validateSegmentPath(relativeSegmentPath);
assert(this.output._rootPath !== null);
fullSegmentPath = joinPaths(joinPaths(this.output._rootPath, playlist.path), relativeSegmentPath);
fullSegmentPath = joinPaths(joinPaths(pathedTarget.rootPath, playlist.path), relativeSegmentPath);
playlist.nextSegmentId++;
}
@@ -863,25 +862,27 @@ export class HlsMuxer extends Muxer {
const output = new Output({
format: playlist.segmentFormat,
rootPath: fullSegmentPath,
target: async (request) => {
if (request.isRoot) {
if (playlist.singleFile) {
const slice = playlist.singleFile.target.slice(playlist.singleFile.nextOffset);
slice.on('write', ({ end }) => segmentSize = Math.max(segmentSize, end));
target: new PathedTarget(
fullSegmentPath,
async (request: TargetRequest) => {
if (request.isRoot) {
if (playlist.singleFile) {
const slice = playlist.singleFile.target.slice(playlist.singleFile.nextOffset);
slice.on('write', ({ end }) => segmentSize = Math.max(segmentSize, end));
return slice;
} else {
const target = await this.output._getTarget(request);
outputTarget = target;
target.on('write', ({ end }) => segmentSize = Math.max(segmentSize, end));
return slice;
} else {
const target = await this.output._getTarget(request);
outputTarget = target;
target.on('write', ({ end }) => segmentSize = Math.max(segmentSize, end));
return target;
return target;
}
}
}
return this.output._getTarget(request);
},
return this.output._getTarget(request);
},
),
initTarget: async () => {
if (playlist.initSegment) {
// We already have an init segment from a previous segment
@@ -1113,7 +1114,8 @@ export class HlsMuxer extends Muxer {
}
private async writePlaylist(playlist: Playlist) {
assert(this.output._rootPath !== null);
assert(this.output._target instanceof PathedTarget);
const pathedTarget = this.output._target;
this.updatePlaylistBitrates(playlist);
@@ -1140,7 +1142,7 @@ export class HlsMuxer extends Muxer {
// In live mode, target duration is not allowed to change, so we use the nominal value
const targetDuration = this.isLive ? this.targetSegmentDuration : this.globalTargetDuration;
const playlistPath = joinPaths(this.output._rootPath, playlist.path);
const playlistPath = joinPaths(pathedTarget.rootPath, playlist.path);
const playlistText = '#EXTM3U\n'
+ `#EXT-X-VERSION:${version}\n`
+ (!this.isLive ? '#EXT-X-PLAYLIST-TYPE:VOD\n' : '')
@@ -1184,6 +1186,9 @@ export class HlsMuxer extends Muxer {
}
private async writeMasterPlaylist() {
assert(this.output._target instanceof PathedTarget);
const pathedTarget = this.output._target;
let masterPlaylistText = '#EXTM3U\n';
let firstVariantWritten = false;
@@ -1366,7 +1371,7 @@ export class HlsMuxer extends Muxer {
this.format._options.onMaster?.(masterPlaylistText);
const target = await this.output._getTarget({ path: this.output._rootPath!, isRoot: true });
const target = await this.output._getTarget({ path: pathedTarget.rootPath, isRoot: true });
const writer = new Writer(target);
writer.start();
@@ -1390,7 +1395,7 @@ export class HlsMuxer extends Muxer {
}
async finalize() {
assert(this.output._rootPath !== null);
assert(this.output._target instanceof PathedTarget);
const release = await this.mutex.acquire();
for (const trackData of this.trackDatas) {
+58 -56
View File
@@ -11,7 +11,7 @@ import { ENCRYPTION_KEY_CACHE_GROUP, Input } from '../input';
import { Segment, SegmentedInput, SegmentRetrievalOptions } from '../segmented-input';
import { toDataView, joinPaths, last, assert, binarySearchLessOrEqual, arrayArgmin, wait } from '../misc';
import { readAllLines, readBytes, Reader } from '../reader';
import { ReadableStreamSource, SourceRef } from '../source';
import { PathedSource, ReadableStreamSource, SourceRef } from '../source';
import { HlsDemuxer } from './hls-demuxer';
import { AttributeList, canIgnoreLine } from './hls-misc';
@@ -525,68 +525,70 @@ export class HlsSegmentedInput extends SegmentedInput {
}
const input = new Input({
entryPath: hlsSegment.location.path,
source: async (request) => {
if (request.path !== hlsSegment.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._getSourceUncached(request);
}
let ref: SourceRef;
const needsSlice = hlsSegment.location.offset > 0 || hlsSegment.location.length !== null;
if (!hlsSegment.encryption) {
ref = await this.input._getSourceCached(request);
if (needsSlice) {
const slice = ref.source.slice(
hlsSegment.location.offset,
hlsSegment.location.length ?? undefined,
);
const sliceRef = slice.ref();
ref.free();
ref = sliceRef;
}
} else {
assert(hlsSegment.encryption.iv);
let ciphertextRef = await this.input._getSourceCached(request);
if (needsSlice) {
// Slice before decrypting
const slice = ciphertextRef.source.slice(
hlsSegment.location.offset,
hlsSegment.location.length ?? undefined,
);
const sliceRef = slice.ref();
ciphertextRef.free();
ciphertextRef = sliceRef;
source: new PathedSource(
hlsSegment.location.path,
async (request) => {
if (request.path !== hlsSegment.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._getSourceUncached(request);
}
const ciphertextReader = new Reader(ciphertextRef.source);
let ref: SourceRef;
const needsSlice = hlsSegment.location.offset > 0 || hlsSegment.location.length !== null;
const stream = createAes128CbcDecryptStream(ciphertextReader, async () => {
using keyRef = await this.input._getSourceCached(
{ path: hlsSegment.encryption!.keyUri, isRoot: false },
ENCRYPTION_KEY_CACHE_GROUP,
);
const keyReader = new Reader(keyRef.source);
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.');
if (!hlsSegment.encryption) {
ref = await this.input._getSourceCached(request);
if (needsSlice) {
const slice = ref.source.slice(
hlsSegment.location.offset,
hlsSegment.location.length ?? undefined,
);
const sliceRef = slice.ref();
ref.free();
ref = sliceRef;
}
const key = readBytes(keySlice, AES_128_BLOCK_SIZE);
} else {
assert(hlsSegment.encryption.iv);
return { key, iv: hlsSegment.encryption!.iv! };
}, () => {
ciphertextRef.free();
});
let ciphertextRef = await this.input._getSourceCached(request);
if (needsSlice) {
// Slice before decrypting
const slice = ciphertextRef.source.slice(
hlsSegment.location.offset,
hlsSegment.location.length ?? undefined,
);
const sliceRef = slice.ref();
ciphertextRef.free();
ciphertextRef = sliceRef;
}
ref = new ReadableStreamSource(stream).ref();
}
const ciphertextReader = new Reader(ciphertextRef.source);
return ref!;
},
const stream = createAes128CbcDecryptStream(ciphertextReader, async () => {
using keyRef = await this.input._getSourceCached(
{ path: hlsSegment.encryption!.keyUri, isRoot: false },
ENCRYPTION_KEY_CACHE_GROUP,
);
const keyReader = new Reader(keyRef.source);
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: hlsSegment.encryption!.iv! };
}, () => {
ciphertextRef.free();
});
ref = new ReadableStreamSource(stream).ref();
}
return ref!;
},
),
formats: this.input._formats,
initInput: initInput ?? undefined,
});