Add SourceRef system to keep track of used Sources (fixes #332)

This commit is contained in:
Vanilagy
2026-03-20 14:46:12 +01:00
parent 587e98391e
commit c91718cdba
8 changed files with 277 additions and 136 deletions
+10 -1
View File
@@ -236,7 +236,11 @@ export class Aes128CbcContext {
} }
} }
export const createAesDecryptStream = (reader: Reader, getInit: () => MaybePromise<Aes128CbcContextInit>) => { export const createAes128CbcDecryptStream = (
reader: Reader,
getInit: () => MaybePromise<Aes128CbcContextInit>,
close: () => unknown,
) => {
let initted = false; let initted = false;
let pos = 0; let pos = 0;
const CHUNK_SIZE = 2 ** 16; const CHUNK_SIZE = 2 ** 16;
@@ -288,7 +292,12 @@ export const createAesDecryptStream = (reader: Reader, getInit: () => MaybePromi
controller.enqueue(trimmedOutput); controller.enqueue(trimmedOutput);
controller.close(); controller.close();
close();
} }
}, },
cancel: () => {
close();
},
}); });
}; };
+30 -21
View File
@@ -1,9 +1,9 @@
import { AES_128_BLOCK_SIZE, createAesDecryptStream } from '../aes'; import { AES_128_BLOCK_SIZE, createAes128CbcDecryptStream } from '../aes';
import { ENCRYPTION_KEY_CACHE_GROUP, Input } from '../input'; import { ENCRYPTION_KEY_CACHE_GROUP, Input } from '../input';
import { Segment, SegmentedInput, SegmentRetrievalOptions } from '../segmented-input'; import { Segment, SegmentedInput, SegmentRetrievalOptions } from '../segmented-input';
import { toDataView, joinPaths, last, assert, binarySearchLessOrEqual, arrayArgmin, wait } from '../misc'; import { toDataView, joinPaths, last, assert, binarySearchLessOrEqual, arrayArgmin, wait } from '../misc';
import { readAllLines, readBytes, Reader } from '../reader'; import { readAllLines, readBytes, Reader } from '../reader';
import { ReadableStreamSource, Source } from '../source'; import { ReadableStreamSource, SourceRef } from '../source';
import { HlsDemuxer } from './hls-demuxer'; import { HlsDemuxer } from './hls-demuxer';
import { AttributeList, canIgnoreLine } from './hls-misc'; import { AttributeList, canIgnoreLine } from './hls-misc';
@@ -89,18 +89,12 @@ export class HlsSegmentedInput extends SegmentedInput {
this.nextLines = null; this.nextLines = null;
if (!lines) { if (!lines) {
const source = await this.demuxer.input._getSourceUncached({ path: this.path }); using ref = await this.demuxer.input._getSourceUncached({ path: this.path });
source.ref(); const reader = new Reader(ref.source);
try {
const reader = new Reader(source);
const slice = await reader.requestEntireFile(); const slice = await reader.requestEntireFile();
assert(slice); assert(slice);
lines = readAllLines(slice, slice.length, { ignore: canIgnoreLine }); lines = readAllLines(slice, slice.length, { ignore: canIgnoreLine });
} finally {
source.unref();
}
} }
let headerRead = false; let headerRead = false;
@@ -514,34 +508,44 @@ export class HlsSegmentedInput extends SegmentedInput {
return this.input._getSourceUncached(request); return this.input._getSourceUncached(request);
} }
let source: Source; let ref: SourceRef;
const needsSlice = hlsSegment.location.offset > 0 || hlsSegment.location.length !== null; const needsSlice = hlsSegment.location.offset > 0 || hlsSegment.location.length !== null;
if (!hlsSegment.encryption) { if (!hlsSegment.encryption) {
source = await this.input._getSourceCached(request); ref = await this.input._getSourceCached(request);
if (needsSlice) { if (needsSlice) {
source = source.slice(hlsSegment.location.offset, hlsSegment.location.length ?? undefined); const slice = ref.source.slice(
hlsSegment.location.offset,
hlsSegment.location.length ?? undefined,
);
const sliceRef = slice.ref();
ref.free();
ref = sliceRef;
} }
} else { } else {
assert(hlsSegment.encryption.iv); assert(hlsSegment.encryption.iv);
let ciphertextSource = await this.input._getSourceCached(request); let ciphertextRef = await this.input._getSourceCached(request);
if (needsSlice) { if (needsSlice) {
// Slice before decrypting // Slice before decrypting
ciphertextSource = ciphertextSource.slice( const slice = ciphertextRef.source.slice(
hlsSegment.location.offset, hlsSegment.location.offset,
hlsSegment.location.length ?? undefined, hlsSegment.location.length ?? undefined,
); );
const sliceRef = slice.ref();
ciphertextRef.free();
ciphertextRef = sliceRef;
} }
const ciphertextReader = new Reader(ciphertextSource); const ciphertextReader = new Reader(ciphertextRef.source);
const stream = createAesDecryptStream(ciphertextReader, async () => { const stream = createAes128CbcDecryptStream(ciphertextReader, async () => {
const keySource = await this.input._getSourceCached( using keyRef = await this.input._getSourceCached(
{ path: hlsSegment.encryption!.keyUri }, { path: hlsSegment.encryption!.keyUri },
ENCRYPTION_KEY_CACHE_GROUP, ENCRYPTION_KEY_CACHE_GROUP,
); );
const keyReader = new Reader(keySource); const keyReader = new Reader(keyRef.source);
const keySlice = await keyReader.requestSlice(0, AES_128_BLOCK_SIZE); const keySlice = await keyReader.requestSlice(0, AES_128_BLOCK_SIZE);
if (!keySlice) { if (!keySlice) {
throw new Error('Invalid AES-128 key; expected at least 16 bytes of data.'); throw new Error('Invalid AES-128 key; expected at least 16 bytes of data.');
@@ -549,12 +553,14 @@ export class HlsSegmentedInput extends SegmentedInput {
const key = readBytes(keySlice, AES_128_BLOCK_SIZE); const key = readBytes(keySlice, AES_128_BLOCK_SIZE);
return { key, iv: hlsSegment.encryption!.iv! }; return { key, iv: hlsSegment.encryption!.iv! };
}, () => {
ciphertextRef.free();
}); });
source = new ReadableStreamSource(stream); ref = new ReadableStreamSource(stream).ref();
} }
return source; return ref!;
}, },
formats: this.input._formats, formats: this.input._formats,
initInput: initInput ?? undefined, initInput: initInput ?? undefined,
@@ -569,7 +575,10 @@ export class HlsSegmentedInput extends SegmentedInput {
const MAX_INPUT_CACHE_SIZE = 4; const MAX_INPUT_CACHE_SIZE = 4;
if (this.inputCache.length > MAX_INPUT_CACHE_SIZE) { if (this.inputCache.length > MAX_INPUT_CACHE_SIZE) {
const minAgeIndex = arrayArgmin(this.inputCache, x => x.age); const minAgeIndex = arrayArgmin(this.inputCache, x => x.age);
assert(minAgeIndex !== -1);
this.inputCache.splice(minAgeIndex, 1); this.inputCache.splice(minAgeIndex, 1);
// DON'T dispose here; the Input might still be used! The source disposal will happen with GC logic
} }
return input; return input;
+103 -73
View File
@@ -19,7 +19,7 @@ import {
import { PacketRetrievalOptions } from './media-sink'; import { PacketRetrievalOptions } from './media-sink';
import { arrayArgmin, arrayCount, assert, desc, MaybePromise, polyfillSymbolDispose, prefer, removeItem } from './misc'; import { arrayArgmin, arrayCount, assert, desc, MaybePromise, polyfillSymbolDispose, prefer, removeItem } from './misc';
import { Reader } from './reader'; import { Reader } from './reader';
import { Source } from './source'; import { Source, SourceRef } from './source';
polyfillSymbolDispose(); polyfillSymbolDispose();
@@ -34,11 +34,11 @@ const sourceRequestsAreEqual = (a: SourceRequest, b: SourceRequest) => {
return a.path === b.path; return a.path === b.path;
}; };
let inputFinalizationRegistry: FinalizationRegistry<Source[]> | null = null; let inputFinalizationRegistry: FinalizationRegistry<SourceRef[]> | null = null;
if (typeof FinalizationRegistry !== 'undefined') { if (typeof FinalizationRegistry !== 'undefined') {
inputFinalizationRegistry = new FinalizationRegistry((sources) => { inputFinalizationRegistry = new FinalizationRegistry((refs) => {
for (const source of sources) { for (const ref of refs) {
source.unref(); ref.free();
} }
}); });
} }
@@ -52,11 +52,18 @@ export type InputOptions<S extends Source = Source> = {
/** A list of supported formats. If the source file is not of one of these formats, then it cannot be read. */ /** A list of supported formats. If the source file is not of one of these formats, then it cannot be read. */
formats: InputFormat[]; formats: InputFormat[];
/** The source from which data will be read. */ /** The source from which data will be read. */
source: S | ((request: SourceRequest) => MaybePromise<S>); source: S | SourceRef<S> | ((request: SourceRequest) => MaybePromise<S | SourceRef<S>>);
entryPath?: string; entryPath?: string;
initInput?: Input; initInput?: Input;
}; };
type SourceCacheEntry<S extends Source> = {
request: SourceRequest;
sourceRef: SourceRef<S>;
age: number;
cacheGroup: number;
};
/** /**
* Represents an input media file. This is the root object from which all media read operations start. * Represents an input media file. This is the root object from which all media read operations start.
* @group Input files & tracks * @group Input files & tracks
@@ -64,7 +71,7 @@ export type InputOptions<S extends Source = Source> = {
*/ */
export class Input<S extends Source = Source> implements Disposable { export class Input<S extends Source = Source> implements Disposable {
/** @internal */ /** @internal */
_source: InputOptions<S>['source']; _source: SourceRef<S> | ((request: SourceRequest) => MaybePromise<S | SourceRef<S>>);
/** @internal */ /** @internal */
_formats: InputFormat[]; _formats: InputFormat[];
/** @internal */ /** @internal */
@@ -84,15 +91,14 @@ export class Input<S extends Source = Source> implements Disposable {
/** @internal */ /** @internal */
_nextSourceCacheAge = 0; _nextSourceCacheAge = 0;
/** @internal */ /** @internal */
// This is an array, not a set, because the same source may be reffed multiple times and therefore also needs to be _sourceRefs: SourceRef[] = [];
// unreffed multiple times.
_reffedSources: Source[] = [];
/** @internal */ /** @internal */
_sourceCache: { _sourceCache: SourceCacheEntry<S>[] = [];
/** @internal */
_sourceCachePromises: {
request: SourceRequest; request: SourceRequest;
sourcePromise: Promise<S>;
age: number;
cacheGroup: number; cacheGroup: number;
promise: Promise<SourceCacheEntry<S>>;
}[] = []; }[] = [];
/** /**
@@ -119,9 +125,6 @@ export class Input<S extends Source = Source> implements Disposable {
if (!(options.source instanceof Source) && typeof options.source !== 'function') { if (!(options.source instanceof Source) && typeof options.source !== 'function') {
throw new TypeError('options.source must be a Source or a function that returns a Source.'); throw new TypeError('options.source must be a Source or a function that returns a Source.');
} }
if (options.source instanceof Source && options.source._disposed) {
throw new TypeError('options.source must not be disposed.');
}
if (typeof options.source === 'function' && options.entryPath === undefined) { if (typeof options.source === 'function' && options.entryPath === undefined) {
throw new TypeError('options.entryPath must be provided when options.source is a function.'); throw new TypeError('options.entryPath must be provided when options.source is a function.');
} }
@@ -133,86 +136,123 @@ export class Input<S extends Source = Source> implements Disposable {
} }
this._formats = options.formats; this._formats = options.formats;
this._source = options.source;
this._initInput = options.initInput ?? null; this._initInput = options.initInput ?? null;
this._entryPath = options.entryPath ?? null; this._entryPath = options.entryPath ?? null;
inputFinalizationRegistry?.register(this, this._reffedSources, this); if (options.source instanceof Source) {
this._source = options.source.ref();
} else {
this._source = options.source;
}
if (this._source instanceof SourceRef) {
this._sourceRefs.push(this._source);
}
inputFinalizationRegistry?.register(this, this._sourceRefs, this);
} }
async _getSourceUncached(request: SourceRequest) { async _getSourceUncached(request: SourceRequest) {
assert(typeof this._source === 'function'); assert(typeof this._source === 'function');
const source = await this._source(request); const source = await this._source(request);
if (!(source instanceof Source)) { if (!(source instanceof Source || source instanceof SourceRef)) {
throw new TypeError('The source function must return a Source.'); throw new TypeError('The source function must return a Source or a SourceRef.');
} }
if (source._disposed) { if (source instanceof Source && source._disposed) {
throw new TypeError('The returned Source must not be disposed.'); throw new TypeError('The returned Source must not be disposed.');
} }
this.onSource?.(source, request); let ref: SourceRef<S>;
if (source instanceof Source) {
return source; ref = source.ref();
} else {
ref = source;
} }
_getSourceCached(request: SourceRequest, cacheGroup = DEFAULT_SOURCE_CACHE_GROUP) { this.onSource?.(ref.source, request);
return ref;
}
_getSourceCached(request: SourceRequest, cacheGroup = DEFAULT_SOURCE_CACHE_GROUP): Promise<SourceRef<S>> {
const cachedEntry = this._sourceCache.find(x => const cachedEntry = this._sourceCache.find(x =>
x.cacheGroup === cacheGroup && sourceRequestsAreEqual(x.request, request), x.cacheGroup === cacheGroup && sourceRequestsAreEqual(x.request, request),
); );
if (cachedEntry) { if (cachedEntry) {
cachedEntry.age++; cachedEntry.age++;
return cachedEntry.sourcePromise; return Promise.resolve(cachedEntry.sourceRef.source.ref());
} }
const sourcePromise = this._getSourceUncached(request); const cachedPromiseEntry = this._sourceCachePromises.find(x =>
this._sourceCache.push({ x.cacheGroup === cacheGroup && sourceRequestsAreEqual(x.request, request),
);
if (cachedPromiseEntry) {
return cachedPromiseEntry.promise.then(x => x.sourceRef.source.ref());
}
const promise = (async () => {
const sourceRef = await this._getSourceUncached(request);
const cacheEntry: SourceCacheEntry<S> = {
request, request,
sourcePromise, sourceRef,
age: this._nextSourceCacheAge++, age: this._nextSourceCacheAge++,
cacheGroup, cacheGroup,
}); };
this._sourceCache.push(cacheEntry);
const MAX_SOURCE_CACHE_SIZE = 4; const MAX_SOURCE_CACHE_SIZE = 4;
const count = arrayCount(this._sourceCache, x => x.cacheGroup === cacheGroup); const count = arrayCount(
this._sourceCache,
x => x.cacheGroup === cacheGroup && x.sourceRef.source._refCount === 1,
);
if (count > MAX_SOURCE_CACHE_SIZE) { if (count > MAX_SOURCE_CACHE_SIZE) {
const minAgeIndex = arrayArgmin(this._sourceCache, x => x.cacheGroup === cacheGroup ? x.age : Infinity); const minAgeIndex = arrayArgmin(
this._sourceCache,
x => x.cacheGroup === cacheGroup && x.sourceRef.source._refCount === 1 ? x.age : Infinity,
);
assert(minAgeIndex !== -1);
const entry = this._sourceCache[minAgeIndex]!; const entry = this._sourceCache[minAgeIndex]!;
this._sourceCache.splice(minAgeIndex, 1); this._sourceCache.splice(minAgeIndex, 1);
void entry.sourcePromise entry.sourceRef.free();
.then((source) => { removeItem(this._sourceRefs, sourceRef);
source.unref();
removeItem(this._reffedSources, source);
});
} }
void sourcePromise this._sourceRefs.push(sourceRef);
.then((source) => {
source.ref(); const promiseIndex = this._sourceCachePromises.findIndex(x => x.request === request);
this._reffedSources.push(source); assert(promiseIndex !== -1);
this._sourceCachePromises.splice(promiseIndex, 1);
return cacheEntry;
})();
this._sourceCachePromises.push({
request,
cacheGroup,
promise,
}); });
return sourcePromise; return promise.then(x => x.sourceRef.source.ref());
} }
/** @internal */ /** @internal */
_getDemuxer() { _getDemuxer() {
return this._demuxerPromise ??= (async () => { return this._demuxerPromise ??= (async () => {
let source: Source; let ref: SourceRef;
if (this._source instanceof Source) { if (this._source instanceof SourceRef) {
source = this._source; ref = this._source;
this.onSource?.(source, null); this.onSource?.(ref.source, null);
} else { } else {
assert(this._entryPath !== null); assert(this._entryPath !== null);
source = await this._getSourceUncached({ path: this._entryPath }); ref = await this._getSourceUncached({ path: this._entryPath });
this._sourceRefs.push(ref);
} }
source.ref(); this._reader = new Reader(ref.source);
this._reffedSources.push(source);
this._reader = new Reader(source);
for (const format of this._formats) { for (const format of this._formats) {
const canRead = await format._canReadInput(this); const canRead = await format._canReadInput(this);
@@ -227,29 +267,15 @@ export class Input<S extends Source = Source> implements Disposable {
} }
/** /**
* Returns a source for the given request. * @deprecated Prefer not using this getter since it is ill-defined for files driven by multiple sources. The
* * {@link Input.onSource} callback provides an alternative.
* If this input was created with a direct {@link Source}, that source is always returned. If this input was created
* with a source function, this method resolves it using the provided request or the entry path.
*/
getSource(request?: SourceRequest): MaybePromise<S> {
if (this._source instanceof Source) {
return this._source;
}
assert(this._entryPath !== null);
return this._getSourceCached(request ?? { path: this._entryPath });
}
/**
* @deprecated Use {@link getSource} instead.
* *
* Returns the source from which this input file reads data for the entry path. Throws if the source-resolving * Returns the source from which this input file reads data for the entry path. Throws if the source-resolving
* function returns a Promise. * function returns a Promise.
*/ */
get source() { get source(): S {
if (this._source instanceof Source) { if (this._source instanceof SourceRef) {
return this._source; return this._source.source;
} }
assert(this._entryPath !== null); assert(this._entryPath !== null);
@@ -262,7 +288,11 @@ export class Input<S extends Source = Source> implements Disposable {
); );
} }
if (source instanceof Source) {
return source; return source;
} else {
return source.source;
}
} }
/** /**
@@ -432,10 +462,10 @@ export class Input<S extends Source = Source> implements Disposable {
this._disposed = true; this._disposed = true;
for (const source of this._reffedSources) { for (const ref of this._sourceRefs) {
source.unref(); ref.free();
} }
this._reffedSources.length = 0; this._sourceRefs.length = 0;
inputFinalizationRegistry?.unregister(this); inputFinalizationRegistry?.unregister(this);
+6 -2
View File
@@ -2824,7 +2824,9 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
sampleInfo.sampleSize, sampleInfo.sampleSize,
); );
if (slice instanceof Promise) slice = await slice; if (slice instanceof Promise) slice = await slice;
assert(slice); if (!slice) {
return null; // Data is outside
}
data = readBytes(slice, sampleInfo.sampleSize); data = readBytes(slice, sampleInfo.sampleSize);
} }
@@ -2864,7 +2866,9 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
fragmentSample.byteSize, fragmentSample.byteSize,
); );
if (slice instanceof Promise) slice = await slice; if (slice instanceof Promise) slice = await slice;
assert(slice); if (!slice) {
return null; // Data is outside
}
data = readBytes(slice, fragmentSample.byteSize); data = readBytes(slice, fragmentSample.byteSize);
} }
+2
View File
@@ -99,6 +99,8 @@ export abstract class SegmentedInput {
entry.input.dispose(); entry.input.dispose();
} }
this.inputCache.length = 0; this.inputCache.length = 0;
this.virtualInput?.dispose();
} }
} }
+53 -22
View File
@@ -16,6 +16,7 @@ import {
isWebKit, isWebKit,
MaybePromise, MaybePromise,
mergeRequestInit, mergeRequestInit,
polyfillSymbolDispose,
promiseWithResolvers, promiseWithResolvers,
retriedFetch, retriedFetch,
toDataView, toDataView,
@@ -25,6 +26,8 @@ import {
import * as nodeAlias from './node'; import * as nodeAlias from './node';
import { InputDisposedError } from './input'; import { InputDisposedError } from './input';
polyfillSymbolDispose();
const node = typeof nodeAlias !== 'undefined' const node = typeof nodeAlias !== 'undefined'
? nodeAlias // Aliasing it prevents some bundler warnings ? nodeAlias // Aliasing it prevents some bundler warnings
: undefined!; : undefined!;
@@ -123,30 +126,56 @@ export abstract class Source {
onread: ((start: number, end: number) => unknown) | null = null; onread: ((start: number, end: number) => unknown) | null = null;
/** /**
* Increases the internal reference count of this source. Call this method when you don't want the source to be * Creates a new `SourceRef` pointing to this source. You are expected to call `.free()` on said `SourceRef` when
* disposed. * you're done with it.
*/ */
ref() { ref() {
if (this._disposed) { return new SourceRef(this);
}
}
export class SourceRef<S extends Source = Source> implements Disposable {
private _source: S | null;
freed = false;
constructor(source: S) {
if (source._disposed) {
throw new Error('Cannot ref a disposed source.'); throw new Error('Cannot ref a disposed source.');
} }
this._refCount++; source._refCount++;
this._source = source;
} }
/** get source() {
* Decreases the internal reference count of this source, signalling a lost of interest in this source. If the if (!this._source) {
* internal count reaches zero, meaning nobody is interested in the source anymore, its resources get disposed. throw new Error('Can\'t get source; ref has already been freed.');
*/ }
unref() {
if (this._refCount > 0) {
this._refCount--;
if (this._refCount === 0) { return this._source;
this._dispose();
this._disposed = true;
} }
free() {
if (this.freed) {
return;
} }
const source = this.source;
assert(source._refCount > 0);
source._refCount--;
if (source._refCount === 0) {
source._dispose();
source._disposed = true;
}
this.freed = true;
this._source = null;
}
[Symbol.dispose]() {
this.free();
} }
} }
@@ -1224,6 +1253,7 @@ export class ReadableStreamSource extends Source {
_dispose() { _dispose() {
this._pendingSlices.length = 0; this._pendingSlices.length = 0;
this._cache.length = 0; this._cache.length = 0;
void this._reader?.cancel();
} }
} }
@@ -1994,6 +2024,8 @@ export class RangedSource extends Source {
/** @internal */ /** @internal */
_baseSource: Source; _baseSource: Source;
/** @internal */ /** @internal */
_ref: SourceRef | null = null;
/** @internal */
_offset: number; _offset: number;
/** @internal */ /** @internal */
_length: number | null; _length: number | null;
@@ -2001,6 +2033,10 @@ export class RangedSource extends Source {
constructor(baseSource: Source, offset: number, length?: number) { constructor(baseSource: Source, offset: number, length?: number) {
super(); super();
if (baseSource._disposed) {
throw new Error('Cannot create a slice of a disposed source.');
}
this._baseSource = baseSource; this._baseSource = baseSource;
this._offset = offset; this._offset = offset;
this._length = length ?? null; this._length = length ?? null;
@@ -2062,16 +2098,11 @@ export class RangedSource extends Source {
} }
override _dispose(): void { override _dispose(): void {
// Nada this._ref?.free();
} }
override ref() { override ref() {
super.ref(); this._ref ??= this._baseSource.ref();
this._baseSource.ref(); return super.ref();
}
override unref() {
super.unref();
this._baseSource.unref();
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { expect, test } from 'vitest'; import { expect, test } from 'vitest';
import { Reader } from '../../src/reader.js'; import { Reader } from '../../src/reader.js';
import { BufferSource } from '../../src/source.js'; import { BufferSource } from '../../src/source.js';
import { createAesDecryptStream } from '../../src/aes.js'; import { createAes128CbcDecryptStream } from '../../src/aes.js';
// getRandomValues is length-limited, so let's just do this // getRandomValues is length-limited, so let's just do this
export const fillRandom = <T extends Uint8Array>(buffer: T) => { export const fillRandom = <T extends Uint8Array>(buffer: T) => {
@@ -27,7 +27,7 @@ test('createAesDecryptStream', async () => {
const source = new BufferSource(ciphertext); const source = new BufferSource(ciphertext);
const reader = new Reader(source); const reader = new Reader(source);
const stream = createAesDecryptStream(reader, () => ({ key, iv })); const stream = createAes128CbcDecryptStream(reader, () => ({ key, iv }));
const streamReader = stream.getReader(); const streamReader = stream.getReader();
const chunks: Uint8Array[] = []; const chunks: Uint8Array[] = [];
+56
View File
@@ -0,0 +1,56 @@
import { expect, test } from 'vitest';
import { FilePathSource } from '../../src/source.js';
import path from 'node:path';
import { Input } from '../../src/input.js';
import { ALL_FORMATS, MP4 } from '../../src/input-format.js';
const __dirname = new URL('.', import.meta.url).pathname;
test('Direct source disposal', async () => {
const filePath = path.join(__dirname, '../public/video.mp4');
const source = new FilePathSource(filePath);
expect(!source._disposed);
const ref = source.ref();
ref.free();
expect(source._disposed);
});
test('Implicit source disposal', async () => {
const filePath = path.join(__dirname, '../public/video.mp4');
const source = new FilePathSource(filePath);
const input = new Input({
source,
formats: ALL_FORMATS,
});
expect(await input.getFormat()).toBe(MP4);
expect(!source._disposed);
input.dispose();
expect(source._disposed);
});
test('Implicit source disposal, double input', async () => {
const filePath = path.join(__dirname, '../public/video.mp4');
const source = new FilePathSource(filePath);
const input1 = new Input({
source,
formats: ALL_FORMATS,
});
const input2 = new Input({
source,
formats: ALL_FORMATS,
});
expect(await input1.getFormat()).toBe(MP4);
expect(await input2.getFormat()).toBe(MP4);
expect(!source._disposed);
input1.dispose();
expect(!source._disposed);
input2.dispose();
expect(source._disposed);
});