fix: two long-running HLS transcode issues (#355)

* fix: release targets from Output._targets on finalize

Long-running HLS transcodes leak memory. Every finalized BufferTarget
stays in _targets until the outer Output closes, pinning its buffer.
Writer.finalize() already does this cleanup for writer-based flows;
extend it to buffer-finalize paths via the public 'finalized' event.

* fix: ReadOrchestrator LRU eviction picks only drained workers

assert(pendingSlices.length === 0) fires under heavy concurrent reads
(e.g. multi-rendition HLS decode from BlobSource). LRU filter only
checked !running; workers with queued slices could be evicted.
Add pendingSlices.length === 0 to the filter.

* fix: export AppendOnlyStreamTarget from index

Missing from the re-export; public docs import it by name.

* fix: join HLS init segment path with root + playlist path

Init path went through _getTarget bare; segments got joined with rootPath + playlist.path. Playlist-relative URI then can't resolve when the playlist lives in a subdirectory.

* Fix targets not being cleaned up, fix paused workers with remaining pending slices, modify doc block, fixed isRoot not being changed on proxied requests

---------

Co-authored-by: Vanilagy <[email protected]>
This commit is contained in:
Ahmed Rowaihi
2026-04-23 14:37:56 +02:00
committed by GitHub
co-authored by Vanilagy
parent 7acae8dda1
commit b05cdbe7e0
10 changed files with 151 additions and 109 deletions
+21
View File
@@ -948,3 +948,24 @@ test.concurrent('Widevine encryption (SAMPLE-AES-CTR) succeeds with buffer keys'
assert(lastPacket);
expect(lastPacket.timestamp + lastPacket.duration).toBe(60);
});
test.concurrent('SourceRequest.isRoot', async () => {
using input = new Input({
source: new CustomPathedSource(
'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
({ path, isRoot }) => {
if (isRoot) {
expect(path).toBe('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
}
return new UrlSource(path);
},
),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack();
assert(videoTrack);
await videoTrack.computeDuration();
});
+34
View File
@@ -2903,3 +2903,37 @@ test('Append-only stream with monotonicity violation', async () => {
await expect(source.add(new EncodedPacket(avcPacketData, 'key', 2, 0), avcMetadata))
.rejects.toThrow('AppendOnlyStreamTarget');
});
test('Relative paths & isRoot', async () => {
const output = new Output({
format: new HlsOutputFormat({
segmentFormat: new CmafOutputFormat(),
getPlaylistPath: () => `a/folder/playlist.m3u8`,
}),
target: new PathedTarget('path/to/master.m3u8', (request) => {
if (request.isRoot) {
expect(request.path).toBe('path/to/master.m3u8');
} else {
expect(request.path.startsWith('path/to/a/folder/')).toBe(true);
}
return new BufferTarget();
}),
});
const source = videoSource();
output.addVideoTrack(source);
await output.start();
await source.add(new EncodedPacket(avcPacketData, 'key', 0, 0), avcMetadata);
await source.add(new EncodedPacket(avcPacketData, 'delta', 0.5, 0), avcMetadata);
await source.add(new EncodedPacket(avcPacketData, 'delta', 1, 0), avcMetadata);
await source.add(new EncodedPacket(avcPacketData, 'delta', 1.5, 0), avcMetadata);
await source.add(new EncodedPacket(avcPacketData, 'key', 2, 0), avcMetadata);
await source.add(new EncodedPacket(avcPacketData, 'delta', 2.5, 0), avcMetadata);
await source.add(new EncodedPacket(avcPacketData, 'delta', 3, 0), avcMetadata);
await source.add(new EncodedPacket(avcPacketData, 'delta', 3.5, 0), avcMetadata);
await output.finalize();
});