From bb32f2ba26eb74e9b16f3099740ff02e0e46fc36 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:38:29 +0200 Subject: [PATCH 1/5] Like this? --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e754a7c..3165d96 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +> [!NOTE] +> I'm on vacation until 18 October, so expect slow or no replies to issues during that time! + # Mediabunny - JavaScript media toolkit [![](https://img.shields.io/npm/v/mediabunny)](https://www.npmjs.com/package/mediabunny) From 045ce43eb2a462b7d9be1f5d64c15bcb94861141 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:39:18 +0200 Subject: [PATCH 2/5] Vaycay --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3165d96..d83837c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ > [!NOTE] -> I'm on vacation until 18 October, so expect slow or no replies to issues during that time! +> I'm on vacation until 18 October, so expect slow or no replies to issues during that time. 🏖️ # Mediabunny - JavaScript media toolkit From 203c0f307ed6a175a83025a1041258767c125060 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:42:44 +0200 Subject: [PATCH 3/5] Ignore read data after source disposal --- src/source.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/source.ts b/src/source.ts index 1c048aa..2fe74d4 100644 --- a/src/source.ts +++ b/src/source.ts @@ -1224,6 +1224,7 @@ class ReadOrchestrator { workers: ReadWorker[] = []; cache: CacheEntry[] = []; currentCacheSize = 0; + disposed = false; constructor(public options: { maxCacheSize: number; @@ -1472,6 +1473,11 @@ class ReadOrchestrator { /** Called by a worker when it has read some data. */ supplyWorkerData(worker: ReadWorker, bytes: Uint8Array) { + if (this.disposed) { + // Writes may still come in after disposal, but we just ignore those + return; + } + const start = worker.currentPos; const end = start + bytes.length; @@ -1645,5 +1651,6 @@ class ReadOrchestrator { this.workers.length = 0; this.cache.length = 0; + this.disposed = true; } } 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 4/5] 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. From c4159c139123c20fa40fea06d6b341efa38bd6b6 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:00:08 +0200 Subject: [PATCH 5/5] Bump minor --- package-lock.json | 12 ++++++------ package.json | 2 +- packages/mp3-encoder/package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index be830e9..9e4af3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.22.0", + "version": "1.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.22.0", + "version": "1.23.0", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.21.1.tgz", - "integrity": "sha512-heBCNei4nBRJ2jkA08LL2zDZVyV9ADsSwFWOaRn37PEjUr6A46vlD+qlUYVTSSv5iIjIt1YTqTP7Nqo+q1hEvw==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.22.0.tgz", + "integrity": "sha512-T8zsXwrRtKEAlITRizn0RiJc1GllVMw3Zcze5ACyluTy3jndUPhAM4SrwjR2SWgqqUa+4nlJ3MmyAi2XuRxwFg==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.22.0", + "version": "1.23.0", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index ce7731a..0823b4e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.22.0", + "version": "1.23.0", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index 21fe1b9..9d1546e 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.22.0", + "version": "1.23.0", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",