Files
mediabunny/src/writer.ts
T
b05cdbe7e0 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]>
2026-04-23 14:37:56 +02:00

129 lines
3.0 KiB
TypeScript

/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { assert } from './misc';
import { Target } from './target';
export class Writer {
target: Target;
finalized = false;
started = false;
private pos = 0;
constructor(target: Target, isMonotonic: boolean) {
if (target._writerAcquired) {
throw new Error('Can\'t have multiple Writers for the same Target.');
}
this.target = target;
target._setMonotonicity(isMonotonic);
target._writerAcquired = true;
}
start() {
assert(!this.started);
this.target._start();
this.started = true;
}
/** Writes the given data to the target, at the current position. */
write(data: Uint8Array) {
assert(this.started && !this.finalized);
this.maybeTrackWrites(data);
this.target._write(data, this.pos);
this.pos += data.byteLength;
}
/** Sets the current position for future writes to a new one. */
seek(newPos: number) {
this.pos = newPos;
}
/** Returns the current position. */
getPos() {
return this.pos;
}
/** Signals to the writer that it may be time to flush. */
async flush() {
assert(this.started && !this.finalized);
return this.target._flush();
}
/** Called after muxing has finished. */
async finalize() {
assert(this.started && !this.finalized);
await this.target._finalize();
this.finalized = true;
}
private trackedWrites: Uint8Array | null = null;
private trackedStart = -1;
private trackedEnd = -1;
private maybeTrackWrites(data: Uint8Array) {
if (!this.trackedWrites) {
return;
}
// Handle negative relative write positions
let pos = this.getPos();
if (pos < this.trackedStart) {
if (pos + data.byteLength <= this.trackedStart) {
return;
}
data = data.subarray(this.trackedStart - pos);
pos = 0;
}
const neededSize = pos + data.byteLength - this.trackedStart;
let newLength = this.trackedWrites.byteLength;
while (newLength < neededSize) {
newLength *= 2;
}
// Check if we need to resize the buffer
if (newLength !== this.trackedWrites.byteLength) {
const copy = new Uint8Array(newLength);
copy.set(this.trackedWrites, 0);
this.trackedWrites = copy;
}
this.trackedWrites.set(data, pos - this.trackedStart);
this.trackedEnd = Math.max(this.trackedEnd, pos + data.byteLength);
}
startTrackingWrites() {
this.trackedWrites = new Uint8Array(2 ** 10);
this.trackedStart = this.getPos();
this.trackedEnd = this.trackedStart;
}
stopTrackingWrites() {
if (!this.trackedWrites) {
throw new Error('Internal error: Can\'t get tracked writes since nothing was tracked.');
}
const slice = this.trackedWrites.subarray(0, this.trackedEnd - this.trackedStart);
const result = {
data: slice,
start: this.trackedStart,
end: this.trackedEnd,
};
this.trackedWrites = null;
return result;
}
}