* 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]>
Subtitle cues were written as SimpleBlocks, which carry no duration. A
SimpleBlock tells the player when a cue starts but not how long to show it,
so players such as VLC and libass-based renderers display nothing for
S_TEXT/WEBVTT tracks muxed into Matroska/WebM.
Route a subtitle chunk that has a positive duration through a BlockGroup so
its BlockDuration is written, matching how additions are already handled.
Non-subtitle tracks and zero-duration cues keep using SimpleBlocks.
Co-authored-by: hikari <[email protected]>
* Fix WebVTT-in-MP4 output by starting the aux writer
The ISOBMFF muxer advertises WebVTT as a supported subtitle codec and maps
it to the wvtt sample entry, but IsobmffMuxer.start() never calls
auxWriter.start(). The aux writer builds subtitle sample boxes in memory, so
the first box write for any subtitle track hits its started === false assert
and muxing fails.
Start the aux writer alongside the main writer so WebVTT subtitle tracks can
be written to MP4/MOV.
* Move code around, add simple WebVTT muxing test
---------
Co-authored-by: hikari <[email protected]>
Co-authored-by: Vanilagy <[email protected]>
* Account for multi-frame Opus packets when computing packet duration
parseOpusTocByte only read the config field of the TOC byte and always
assumed a single frame per packet. Per RFC 6716 section 3.2, a packet may
carry 1, 2 or an arbitrary number of frames, encoded in the two low bits of
the TOC byte (plus the frame count byte for code 3), and its duration is the
frame duration times the frame count.
As a result, the Ogg muxer wrote granule positions that advanced slower than
the actual audio. Chromium's MediaRecorder packs three 20 ms frames per Opus
packet, so remuxing WebM/Opus to Ogg/Opus produced files declaring a third of
their real duration: a 5.7 s recording ended with a final granule position of
99840 (2.08 s). Decoders that trust the container then truncate the audio.
* Clean up
---------
Co-authored-by: Vanilagy <[email protected]>
* Add per-sink decoder preferences to VideoSampleSink and CanvasSink
Adds an optional VideoSinkDecoderOptions ({ hardwareAcceleration,
optimizeForLatency }) parameter to VideoSampleSink, exposed on CanvasSink
via options.decoderOptions, applied to the decoder config before the
VideoDecoderWrapper is constructed.
Motivation: applications that run many sinks concurrently (multi-track
video editors) need to manage hardware decode sessions deliberately -
the number of concurrent hardware sessions is OS-limited, undocumented,
and exceeding it fails silently on some platforms (macOS VideoToolbox
accepts configure() and decode() and simply never outputs). Such an
application places overflow sinks on 'prefer-software' explicitly.
optimizeForLatency is exposed alongside it since it is the other
WebCodecs decoder-config preference an application may want per sink.
The override composes with the existing interlaced-AVC Chromium
workaround, which runs later and can only strengthen the preference
toward software.
Validation mirrors decode.ts's validateVideoDecodingConfig.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Modify docs
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: Vanilagy <[email protected]>
* Fix orphaned queued reads when a freed worker slot is stolen concurrently
ReadOrchestrator.runWorker's finally callback dequeues the oldest queued
read and asserts that createWorker succeeds ("we just freed up a worker").
That assumption races: the callback runs on a later microtask than the
worker's stop, and concurrent read() calls in that gap can LRU-evict the
freed worker and saturate every slot. The assert then throws as an
unhandled rejection after the read was removed from the queue but before
it was attached to any worker - its pending slices' promises never settle
and the awaiting reads hang forever.
Observed in production-like load (a 4-source composition player): 25
back-to-back occurrences saturating both workers, leaving clips
permanently undecodable.
Fix: create the worker first; only dequeue the read once a slot was
actually obtained. If every slot is busy, leave the read queued - each
running worker drains the queue from this same block when it stops, so
the read is picked up by whichever worker stops next.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Update logic
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: Vanilagy <[email protected]>