Add FilePathSource for server-side use

This commit is contained in:
Vanilagy
2025-08-30 16:32:14 +02:00
parent bd4d9b966d
commit e5df6f1a63
7 changed files with 92 additions and 33 deletions
+2 -2
View File
@@ -8,11 +8,11 @@
"packages/*" "packages/*"
], ],
"main": "./dist/bundles/mediabunny.cjs", "main": "./dist/bundles/mediabunny.cjs",
"module": "./dist/modules/src/index.js", "module": "./dist/bundles/mediabunny.mjs",
"types": "./dist/modules/src/index.d.ts", "types": "./dist/modules/src/index.d.ts",
"exports": { "exports": {
"types": "./dist/modules/src/index.d.ts", "types": "./dist/modules/src/index.d.ts",
"import": "./dist/modules/src/index.js", "import": "./dist/bundles/mediabunny.mjs",
"require": "./dist/bundles/mediabunny.cjs" "require": "./dist/bundles/mediabunny.cjs"
}, },
"files": [ "files": [
+3 -3
View File
@@ -40,10 +40,10 @@ export default async function inlineWorker(scriptText) {
let Worker; let Worker;
try { try {
Worker = (await import('worker_threads')).Worker; Worker = (await import('worker_threads')).Worker;
} catch { } catch {
const workerModule = 'worker_threads'; const workerModule = 'worker_threads';
Worker = require(workerModule).Worker; Worker = require(workerModule).Worker;
} }
const worker = new Worker(scriptText, { eval: true }); const worker = new Worker(scriptText, { eval: true });
+1 -1
View File
@@ -1069,7 +1069,7 @@ export type Av1CodecInfo = {
}; };
/** Iterates over all OBUs in an AV1 packet bistream. */ /** Iterates over all OBUs in an AV1 packet bistream. */
export function* iterateAv1PacketObus(packet: Uint8Array) { export const iterateAv1PacketObus = function* (packet: Uint8Array) {
// https://aomediacodec.github.io/av1-spec/av1-spec.pdf // https://aomediacodec.github.io/av1-spec/av1-spec.pdf
const bitstream = new Bitstream(packet); const bitstream = new Bitstream(packet);
+4 -2
View File
@@ -94,12 +94,14 @@ export { Rotation, AnyIterable, SetRequired, MaybePromise } from './misc';
export { export {
Source, Source,
BufferSource, BufferSource,
StreamSource,
StreamSourceOptions,
BlobSource, BlobSource,
BlobSourceOptions, BlobSourceOptions,
UrlSource, UrlSource,
UrlSourceOptions, UrlSourceOptions,
FilePathSource,
FilePathSourceOptions,
StreamSource,
StreamSourceOptions,
} from './source'; } from './source';
export { export {
InputFormat, InputFormat,
+79 -5
View File
@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import type { FileHandle } from 'node:fs/promises';
import { import {
assert, assert,
binarySearchLessOrEqual, binarySearchLessOrEqual,
@@ -35,7 +36,7 @@ export abstract class Source {
abstract _read(start: number, end: number): MaybePromise<ReadResult>; abstract _read(start: number, end: number): MaybePromise<ReadResult>;
/** @internal */ /** @internal */
_sizePromise: Promise<number> | null = null; private _sizePromise: Promise<number> | null = null;
/** /**
* Resolves with the total size of the file in bytes. This function is memoized, meaning only the first call * Resolves with the total size of the file in bytes. This function is memoized, meaning only the first call
@@ -250,7 +251,9 @@ export class UrlSource extends Source {
super(); super();
this._url = url instanceof URL ? url : new URL(url, location.href); this._url = url instanceof URL
? url
: new URL(url, typeof location !== 'undefined' ? location.href : undefined);
this._options = options; this._options = options;
this._getRetryDelay = options.getRetryDelay ?? (previousAttempts => Math.min(2 ** (previousAttempts - 2), 8)); this._getRetryDelay = options.getRetryDelay ?? (previousAttempts => Math.min(2 ** (previousAttempts - 2), 8));
@@ -449,20 +452,91 @@ export class UrlSource extends Source {
} }
} }
/**
* Options for FilePathSource.
* @public
*/
export type FilePathSourceOptions = {
/** The maximum number of bytes the cache is allowed to hold in memory. Defaults to 8 MiB. */
maxCacheSize?: number;
};
/**
* A source backed by a path to a file. Intended for server-side usage in Node, Bun, or Deno.
* @public
*/
export class FilePathSource extends Source {
/** @internal */
_streamSource: StreamSource;
constructor(filePath: string, options: BlobSourceOptions = {}) {
if (typeof filePath !== 'string') {
throw new TypeError('filePath must be a string.');
}
if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.');
}
if (
options.maxCacheSize !== undefined
&& (!Number.isInteger(options.maxCacheSize) || options.maxCacheSize < 0)
) {
throw new TypeError('options.maxCacheSize, when provided, must be a non-negative integer.');
}
super();
let fileHandle: FileHandle | null = null;
// Let's back this source with a StreamSource, makes the implementation very simple
this._streamSource = new StreamSource({
getSize: async () => {
const FS_MODULE_NAME = 'node:fs/promises';
const fs = await import(FS_MODULE_NAME) as typeof import('node:fs/promises');
fileHandle = await fs.open(filePath, 'r');
const stats = await fileHandle.stat();
return stats.size;
},
read: async (start, end) => {
assert(fileHandle);
const buffer = Buffer.alloc(end - start);
await fileHandle.read(buffer, 0, end - start, start);
return buffer;
},
maxCacheSize: options.maxCacheSize,
prefetchProfile: 'fileSystem',
});
}
/** @internal */
_read(start: number, end: number): MaybePromise<ReadResult> {
return this._streamSource._read(start, end);
}
/** @internal */
_retrieveSize(): MaybePromise<number> {
return this._streamSource._retrieveSize();
}
}
/** /**
* Options for defining a StreamSource. * Options for defining a StreamSource.
* @public * @public
*/ */
export type StreamSourceOptions = { export type StreamSourceOptions = {
/**
* Called when the size of the entire file is requested. Must return or resolve to the size in bytes. This function
* is guaranteed to be called before `read`.
*/
getSize: () => MaybePromise<number>;
/** /**
* Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream * Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream
* that yields these bytes. * that yields these bytes.
*/ */
read: (start: number, end: number) => MaybePromise<Uint8Array | ReadableStream<Uint8Array>>; read: (start: number, end: number) => MaybePromise<Uint8Array | ReadableStream<Uint8Array>>;
/** Called when the size of the entire file is requested. Must return or resolve to the size in bytes. */
getSize: () => MaybePromise<number>;
/** The maximum number of bytes the cache is allowed to hold in memory. Defaults to 8 MiB. */ /** The maximum number of bytes the cache is allowed to hold in memory. Defaults to 8 MiB. */
maxCacheSize?: number; maxCacheSize?: number;
+2 -20
View File
@@ -1,30 +1,12 @@
import { expect, test } from 'vitest'; import { expect, test } from 'vitest';
import { ALL_FORMATS, MP4, StreamSource, EncodedPacketSink, Input } from '../src/index.js'; import { ALL_FORMATS, MP4, EncodedPacketSink, Input, FilePathSource } from '../src/index.js';
import { open } from 'node:fs/promises';
const __dirname = new URL('.', import.meta.url).pathname; const __dirname = new URL('.', import.meta.url).pathname;
const fileHandle = await open(
`${__dirname}/files/video.mp4`,
'r',
);
const source = new StreamSource({
read: async (start, end) => {
const buffer = Buffer.alloc(end - start);
await fileHandle.read(buffer, 0, end - start, start);
return buffer;
},
getSize: async () => {
const { size } = await fileHandle.stat();
return size;
},
});
test('Should be able to get packets from a .MP4 file', async () => { test('Should be able to get packets from a .MP4 file', async () => {
const input = new Input({ const input = new Input({
source: new FilePathSource(`${__dirname}/files/video.mp4`),
formats: ALL_FORMATS, formats: ALL_FORMATS,
source,
}); });
expect(await input.getFormat()).toBe(MP4); expect(await input.getFormat()).toBe(MP4);
+1
View File
@@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2021", "target": "ES2021",
"module": "esnext",
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitOverride": true, "noImplicitOverride": true,