mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Add composable conversions
* Add non-owning conversions via ConversionOptions.ownsOutput A conversion with ownsOutput: false only adds tracks to the output and drives their media data; starting, finalizing, and metadata tags remain the caller's responsibility. This lets multiple conversions and directly-added user tracks compose on a single Output (see upstream issue #436). - ownsOutput: false allows a pre-populated output (state must still be 'pending') and seeds track-capacity accounting from existing tracks - execute() requires the output to be started and never finalizes it - cancel() closes only the conversion's own sources, releasing internal synchronizer waiters, and leaves the output usable - tags cannot be combined with ownsOutput: false - isValid requires at least one contributed track instead of the format's minimum track counts Prototype for API discussion; default (owning) behavior is unchanged. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AJdfnbY9AFh9i9dKgtrj6E * Add external-audio example using a non-owning conversion Demonstrates composing a user-owned audio track (synthesized voiceover via OfflineAudioContext + AudioBufferSource) onto a picked video with Conversion.init({ ownsOutput: false }), including progress reporting and playback/download of the result. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AJdfnbY9AFh9i9dKgtrj6E * Release synchronizer waiters when canceling during output finalization A non-owning conversion's cancel() previously no-oped entirely when the output (owned by someone else) was already finalizing or finalized, leaving pump loops parked in the track synchronizer and hanging execute() forever. Now it still marks the conversion canceled and releases parked waiters in that state, without force-closing sources (finalization owns flushing them at that point). Also adds coverage: non-owning onProgress monotonicity, canceling one of two sibling conversions, capacity seeding across sequential inits, exact metadata exclusivity, and cancel-before-execute. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AJdfnbY9AFh9i9dKgtrj6E * Document non-owning conversions on the converting-media-files guide page Adds the doc section requested in #436: what ownsOutput: false does, the required choreography (add tracks -> output.start() before execute() -> run the conversion concurrently with your own sources -> finalize), the cancellation split (conversion.cancel() leaves the output alive; cancel both for a full abort and tear the output down on error paths), isValid semantics in this mode, and the tags restriction with the setMetadataTags() alternative. Also cross-links the fresh-output rule to the new section. VitePress build passes with dead-link checking on. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Clean up conversion logic, add Output.tracks and .hasEnoughTracks(), move new conversion tests around, remove external audio example * non-owning -> composable, and update docs * Update --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vanilagy <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
Vanilagy
parent
794b84884f
commit
f41eef0937
@@ -22,7 +22,7 @@ It has the following features:
|
||||
- Audio up/downmixing
|
||||
- User-defined video & audio processing
|
||||
|
||||
The conversion API was built to be simple, versatile and extremely performant.
|
||||
The conversion API was built to be simple, versatile, composable and performant.
|
||||
|
||||
## Basic usage
|
||||
|
||||
@@ -63,7 +63,7 @@ await conversion.execute();
|
||||
That's it! A `Conversion` simply takes an instance of `Input` and `Output`, then reads the data from the input and writes it to the output. If you're unfamiliar with [`Input`](./reading-media-files) and [`Output`](./writing-media-files), check out their respective guides.
|
||||
|
||||
::: info
|
||||
The `Output` passed to the `Conversion` must be *fresh*; that is, it must have no added tracks or metadata tags and be in the `'pending'` state (not started yet).
|
||||
The `Output` passed to the `Conversion` must be *fresh*; that is, it must have no added tracks or metadata tags and be in the `'pending'` state (not started yet). This requirement is relaxed for [composable conversions](#composable-conversions), which allows you to combine the conversion with other tracks.
|
||||
:::
|
||||
|
||||
Unconfigured, the conversion process handles all the details automatically, such as:
|
||||
@@ -506,6 +506,59 @@ conversion.utilizedTracks; // => InputTrack[]
|
||||
```
|
||||
A track may appear multiple times in this list when [fan-out](#track-fan-out) produces multiple output tracks from it.
|
||||
|
||||
## Composable conversions
|
||||
|
||||
By default, a `Conversion` takes full ownership of its `Output`: it requires a fresh output, then starts it, adds data, and finalizes it for you. Sometimes, however, you want a conversion to be just *one* of several contributors to a single output file - for example, to keep an input's video track while attaching your own, externally-produced audio track. For this, set `composable: true`.
|
||||
|
||||
A composable conversion only adds its own tracks to the output and pumps their media data while `execute()` runs. Everything else about the output's lifecycle is yours: you add any additional tracks, set any metadata tags, and call `start()` and `finalize()` yourself. This enables you to add additional tracks outside of the conversion, or even have multiple conversions target a single `Output`.
|
||||
|
||||
To use it, initialize everything, then start the `Output`, and then execute the conversion:
|
||||
|
||||
```ts
|
||||
import {
|
||||
Input,
|
||||
Output,
|
||||
Mp4OutputFormat,
|
||||
BufferTarget,
|
||||
Conversion,
|
||||
AudioBufferSource,
|
||||
} from 'mediabunny';
|
||||
|
||||
const input = new Input({ ... });
|
||||
const output = new Output({
|
||||
format: new Mp4OutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
// Use the conversion only to copy over the video
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
audio: { discard: true },
|
||||
composable: true,
|
||||
});
|
||||
|
||||
// Add our own audio track directly
|
||||
const audioSource = new AudioBufferSource({ codec: 'aac', bitrate: 128e3 });
|
||||
output.addAudioTrack(audioSource);
|
||||
|
||||
// Start the output
|
||||
await output.start();
|
||||
|
||||
// Run the conversion concurrently with feeding our own audio
|
||||
await Promise.all([
|
||||
conversion.execute(),
|
||||
audioSource.add(myAudioBuffer).then(() => audioSource.close()),
|
||||
]);
|
||||
|
||||
// Finalize the output
|
||||
await output.finalize();
|
||||
```
|
||||
|
||||
### Cancellation
|
||||
|
||||
[Canceling](#canceling-a-conversion) a composable conversion does *not* cancel the output, it only stops media data from being added and closes its tracks. For a full abort, you must cancel the output manually.
|
||||
|
||||
## Converting live streams
|
||||
|
||||
Live inputs, like HLS live streams, can also be used with the Conversion API. In this case, by default, the conversion will run until the live stream has ended.
|
||||
|
||||
Reference in New Issue
Block a user