mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Add SourceRef system to keep track of used Sources (fixes #332)
This commit is contained in:
+10
-1
@@ -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();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user