From f2f9b2f0c31473814860c1291733d30fe620ade8 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:59:46 +0200 Subject: [PATCH] Add FilePathTarget --- docs/guide/writing-media-files.md | 29 ++++++++++++- src/index.ts | 2 + src/target.ts | 69 +++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/docs/guide/writing-media-files.md b/docs/guide/writing-media-files.md index b705476..c9ef8ed 100644 --- a/docs/guide/writing-media-files.md +++ b/docs/guide/writing-media-files.md @@ -290,7 +290,7 @@ By default, data will be emitted by the `StreamTarget` as soon as it is availabl new StreamTarget(writable, { chunked: true, chunkSize: 2 ** 20, // Optional; defaults to 16 MiB -}), +}); ``` #### Applying backpressure @@ -329,6 +329,33 @@ const output = new Output({ await output.finalize(); // Will automatically close the writable stream ``` +### `FilePathTarget` + +This target writes to a file at the specified path. It is intended for server-side usage in Node, Bun, or Deno, and offers a simpler API than `StreamTarget` when you just want to write directly to a file path. + +```ts +import { Output, FilePathTarget } from 'mediabunny'; + +const output = new Output({ + target: new FilePathTarget('/path/to/output.mp4'), + // ... +}); + +// ... + +await output.finalize(); // Will automatically close the file handle +``` + +The internally held file handle will be closed when `finalize` or `cancel` are called on the `Output`. + +Writing is chunked by default, for performance. Like `StreamTarget`, you can configure chunked mode options: +```ts +new FilePathTarget('/path/to/output.mp4', { + chunked: false, // Disable chunking (slower) + chunkSize: 2 ** 20, // Optional; defaults to 16 MiB +}); +``` + ### `NullTarget` This target simply discards all data that is passed into it. It is useful for when you need an `Output` but extract data from it differently, for example through output format-specific callbacks or encoder events. diff --git a/src/index.ts b/src/index.ts index 5925cf5..e3abe7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -92,6 +92,8 @@ export { export { Target, BufferTarget, + FilePathTarget, + FilePathTargetOptions, NullTarget, StreamTarget, StreamTargetOptions, diff --git a/src/target.ts b/src/target.ts index 3893be7..168e803 100644 --- a/src/target.ts +++ b/src/target.ts @@ -6,8 +6,15 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import type { FileHandle } from 'node:fs/promises'; import { BufferTargetWriter, NullTargetWriter, StreamTargetWriter, Writer } from './writer'; import { Output } from './output'; +import * as nodeAlias from './node'; +import { assert } from './misc'; + +const node = typeof nodeAlias !== 'undefined' + ? nodeAlias // Aliasing it prevents some bundler warnings + : undefined!; /** * Base class for targets, specifying where output files are written. @@ -121,6 +128,68 @@ export class StreamTarget extends Target { } } +/** + * Options for {@link FilePathTarget}. + * @group Output targets + * @public + */ +export type FilePathTargetOptions = StreamTargetOptions; + +/** + * A target that writes to a file at the specified path. Intended for server-side usage in Node, Bun, or Deno. + * + * Writing is chunked by default. The internally held file handle will be closed when `.finalize()` or `.cancel()` are + * called on the corresponding {@link Output}. + * @group Output targets + * @public + */ +export class FilePathTarget extends Target { + /** @internal */ + _streamTarget: StreamTarget; + /** @internal */ + _fileHandle: FileHandle | null = null; + + /** Creates a new {@link FilePathTarget} that writes to the file at the specified file path. */ + constructor(filePath: string, options: FilePathTargetOptions = {}) { + 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.'); + } + + super(); + + // Let's back this target with a StreamTarget, makes the implementation very simple + const writable = new WritableStream({ + start: async () => { + this._fileHandle = await node.fs.open(filePath, 'w'); + }, + write: async (chunk) => { + assert(this._fileHandle); + await this._fileHandle.write(chunk.data, 0, chunk.data.byteLength, chunk.position); + }, + close: async () => { + if (this._fileHandle) { + await this._fileHandle.close(); + this._fileHandle = null; + } + }, + }); + + this._streamTarget = new StreamTarget(writable, { + chunked: true, + ...options, + }); + this._streamTarget._output = this._output; + } + + /** @internal */ + _createWriter(): Writer { + return this._streamTarget._createWriter(); + } +} + /** * This target just discards all incoming data. It is useful for when you need an {@link Output} but extract data from * it differently, for example through format-specific callbacks (`onMoof`, `onMdat`, ...) or encoder events.