From e5df6f1a632b7a39983f0d25702de34cfa31da50 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sat, 30 Aug 2025 16:32:14 +0200 Subject: [PATCH] Add FilePathSource for server-side use --- package.json | 4 +- scripts/esbuild/inlined-workers.ts | 6 +-- src/codec-data.ts | 2 +- src/index.ts | 6 ++- src/source.ts | 84 ++++++++++++++++++++++++++++-- test/read-mp4.test.ts | 22 +------- tsconfig.json | 1 + 7 files changed, 92 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index e275fc7..8a52b09 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "packages/*" ], "main": "./dist/bundles/mediabunny.cjs", - "module": "./dist/modules/src/index.js", + "module": "./dist/bundles/mediabunny.mjs", "types": "./dist/modules/src/index.d.ts", "exports": { "types": "./dist/modules/src/index.d.ts", - "import": "./dist/modules/src/index.js", + "import": "./dist/bundles/mediabunny.mjs", "require": "./dist/bundles/mediabunny.cjs" }, "files": [ diff --git a/scripts/esbuild/inlined-workers.ts b/scripts/esbuild/inlined-workers.ts index eee44ac..0f08ba9 100644 --- a/scripts/esbuild/inlined-workers.ts +++ b/scripts/esbuild/inlined-workers.ts @@ -40,10 +40,10 @@ export default async function inlineWorker(scriptText) { let Worker; try { - Worker = (await import('worker_threads')).Worker; + Worker = (await import('worker_threads')).Worker; } catch { - const workerModule = 'worker_threads'; - Worker = require(workerModule).Worker; + const workerModule = 'worker_threads'; + Worker = require(workerModule).Worker; } const worker = new Worker(scriptText, { eval: true }); diff --git a/src/codec-data.ts b/src/codec-data.ts index edcd00f..d315199 100644 --- a/src/codec-data.ts +++ b/src/codec-data.ts @@ -1069,7 +1069,7 @@ export type Av1CodecInfo = { }; /** 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 const bitstream = new Bitstream(packet); diff --git a/src/index.ts b/src/index.ts index bd1613a..1b9e5ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -94,12 +94,14 @@ export { Rotation, AnyIterable, SetRequired, MaybePromise } from './misc'; export { Source, BufferSource, - StreamSource, - StreamSourceOptions, BlobSource, BlobSourceOptions, UrlSource, UrlSourceOptions, + FilePathSource, + FilePathSourceOptions, + StreamSource, + StreamSourceOptions, } from './source'; export { InputFormat, diff --git a/src/source.ts b/src/source.ts index 568ff65..8869d51 100644 --- a/src/source.ts +++ b/src/source.ts @@ -6,6 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import type { FileHandle } from 'node:fs/promises'; import { assert, binarySearchLessOrEqual, @@ -35,7 +36,7 @@ export abstract class Source { abstract _read(start: number, end: number): MaybePromise; /** @internal */ - _sizePromise: Promise | null = null; + private _sizePromise: Promise | null = null; /** * 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(); - 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._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 { + return this._streamSource._read(start, end); + } + + /** @internal */ + _retrieveSize(): MaybePromise { + return this._streamSource._retrieveSize(); + } +} + /** * Options for defining a StreamSource. * @public */ 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; + /** * Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream * that yields these bytes. */ read: (start: number, end: number) => MaybePromise>; - /** Called when the size of the entire file is requested. Must return or resolve to the size in bytes. */ - getSize: () => MaybePromise; - /** The maximum number of bytes the cache is allowed to hold in memory. Defaults to 8 MiB. */ maxCacheSize?: number; diff --git a/test/read-mp4.test.ts b/test/read-mp4.test.ts index 31ec0f5..b5d0407 100644 --- a/test/read-mp4.test.ts +++ b/test/read-mp4.test.ts @@ -1,30 +1,12 @@ import { expect, test } from 'vitest'; -import { ALL_FORMATS, MP4, StreamSource, EncodedPacketSink, Input } from '../src/index.js'; -import { open } from 'node:fs/promises'; +import { ALL_FORMATS, MP4, EncodedPacketSink, Input, FilePathSource } from '../src/index.js'; 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 () => { const input = new Input({ + source: new FilePathSource(`${__dirname}/files/video.mp4`), formats: ALL_FORMATS, - source, }); expect(await input.getFormat()).toBe(MP4); diff --git a/tsconfig.json b/tsconfig.json index 7ecc047..9f6de94 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "ES2021", + "module": "esnext", "strict": true, "noImplicitAny": true, "noImplicitOverride": true,