Compare commits

..
303 Commits
Author SHA1 Message Date
Vanilagy 794b84884f Make sure that samples are always closed in the Conversion API, even on error cases 2026-07-18 22:54:24 +02:00
Vanilagy 45c34f9dcd Bump patch 2026-07-18 16:15:53 +02:00
Vanilagy 3ec4061e03 Clean up 2026-07-18 15:49:57 +02:00
bf592a87ca Give Matroska subtitle cues a BlockDuration (#442)
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]>
2026-07-18 15:48:55 +02:00
454476ab26 Fix WebVTT-in-MP4 output by starting the aux writer (#441)
* 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]>
2026-07-18 13:45:23 +00:00
Vanilagy d2aea552d9 Add spacing 2026-07-18 15:27:06 +02:00
Igor SamokhovetsandGitHub 94d42667f1 Add extension decoders to media player example (#433)
* Add extension decoders to media player example

* Enable extension decoders in input examples

* Simplify media player file filter

* Align input file filters across examples
2026-07-18 15:26:31 +02:00
Vanilagy 0554696505 Fix nested worker randomly shutting off in Firefox (closes #435) 2026-07-18 14:44:57 +02:00
bfbd90e1d8 Account for multi-frame Opus packets when computing packet duration (#439)
* 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]>
2026-07-18 12:05:31 +00:00
Vanilagy 1183b542ee Make WAVE demuxer throw for format tags that it doesn't support (fixes #440), add support for reading & writing float64 PCM in WAVE 2026-07-14 17:30:50 +02:00
Vanilagy db75747533 Bump patch 2026-07-10 00:15:25 +02:00
Vanilagy 3fcb93712d @mediabunny/server: Properly force IDR frames when encoding AVC/HEVC on NVENC 2026-07-08 16:50:58 +02:00
Vanilagy 5b1dbf5558 Fix npm i not being called after sync workspace deps 2026-07-08 16:07:41 +02:00
Vanilagy 9180af799d Oops 2026-07-07 21:27:05 +02:00
Vanilagy b8d5cc5b36 Bump patch, remove Ponder :( 2026-07-07 21:17:06 +02:00
Ali TlekbaiandGitHub d5b07373b3 Fix AudioResampler crackle at exact rate ratios: 32 kHz -> 48 kHz (#432) 2026-07-07 21:13:35 +02:00
Vanilagy b99b86db0b Bump patch 2026-07-03 16:44:39 +02:00
Vanilagy b0e5f655aa Make AUD-based NALU stripping less aggressive (closes #426) 2026-07-03 16:37:06 +02:00
Vanilagy 0c2853d0aa Merge branch 'main' of https://github.com/Vanilagy/metamuxer 2026-07-03 16:24:09 +02:00
Vanilagy cc970e1f53 Fix invalid check for no available read worker stalling reads, bump patch 2026-07-03 16:24:08 +02:00
Vanilagy 5e68d9655c Make ProRes tracks return true for hasOnlyKeyPackets() 2026-07-02 22:58:51 +02:00
Vanilagy 7171cf4338 Add sync workspace deps script 2026-07-02 20:42:46 +02:00
Vanilagy baff6a67a3 Omg 2026-07-02 20:31:13 +02:00
Vanilagy d0401b42a5 Bump TurboRes to fix bug 2026-07-02 20:30:36 +02:00
Vanilagy 72427b8b39 Bump TurboRes, use desiredSize, bump patch 2026-07-02 20:18:25 +02:00
Viktor SzépeandGitHub 6812cf1643 Fix typos in source, tests, and docs (#430)
* Fix typos in source, tests, and docs

* Fix introduction guide typos
2026-07-02 09:33:43 +02:00
Zi How PohandGitHub c9a828c272 Fix duplicated entries in generated API docs (#429) 2026-07-02 07:31:09 +00:00
Vanilagy e851120bfe Add PhotoCircle bronze sponsor 2026-07-01 18:11:01 +02:00
Vanilagy 1a4fec9f6e Bump TurboRes & patch 2026-07-01 17:18:21 +02:00
Vanilagy 8c8ce8a4a5 Merge main into release AFTER the npm publish completes 2026-07-01 16:26:58 +02:00
Vanilagy 7ea8f6d751 Up timeout on test 2026-07-01 16:25:53 +02:00
Vanilagy 8fb22633fe Add frame container atom header stripping & readding for ProRes in Matroska, bump patch 2026-07-01 16:22:38 +02:00
Vanilagy 0480ecbd3a Bump TurboRes version, mention response headers in README, bump patch 2026-07-01 15:27:36 +02:00
Vanilagy 9547d36353 Add workaround for https://issues.chromium.org/issues/529852980 2026-07-01 13:50:48 +02:00
Vanilagy d8d22aa4eb Make release workflow build the docs 2026-07-01 11:40:03 +02:00
Vanilagy 50b281a9f8 Merge branch 'main' into release 2026-07-01 11:37:12 +02:00
Vanilagy 409961c02b Rename 2026-07-01 11:35:22 +02:00
Vanilagy d8a6e09a85 Up timeout 2026-07-01 11:32:45 +02:00
Vanilagy 2c7e8d85ca Create separate Build check CI 2026-07-01 11:28:52 +02:00
Vanilagy a9441aa9a9 Fix API docs generation issue 2026-07-01 11:25:24 +02:00
github-actions[bot] 403093c514 Merge main into release for tag v1.50.0 2026-06-30 20:08:10 +00:00
Vanilagy 6c6687dfaf Bump TurboRes 2026-06-30 21:59:05 +02:00
Vanilagy 7d89c8de2d Add ProRes encode and decode support to @mediabunny/server 2026-06-30 21:40:58 +02:00
Vanilagy b4ef20cd36 Bump minor 2026-06-30 18:26:54 +02:00
Vanilagy a4884184e9 Remove GPU path for alpha merger/splitter, add worker pool for merger, allow concurrent merging for better performance 2026-06-30 18:23:59 +02:00
6f4a40ff4e Fix VP9 alpha merge for padded decoded frames (#422)
* Fix VP9 alpha merge for padded decoded frames

* Clean up alpha merging and splitting logic

---------

Co-authored-by: Vanilagy <[email protected]>
2026-06-30 17:09:16 +02:00
David P.andGitHub 7453336f70 Merge pull request #425 from Vanilagy/prores
Add ProRes support and @mediabunny/prores extension
2026-06-30 16:09:06 +02:00
Vanilagy bfbcea69b4 Fix AAC error in Matroska muxer, remove special VideoFrame path for VideoSample due to Chromium bug 2026-06-30 16:04:30 +02:00
Vanilagy 40e35d6f14 Update lockfile 2026-06-29 19:18:24 +02:00
Vanilagy 8716ddda14 Merge remote-tracking branch 'origin/main' into prores 2026-06-29 17:15:24 +02:00
Vanilagy 8ae0f9d09e Implement @mediabunny/prores extension, add onError callbacks to custom coders, fix visibleRect bug in VideoSample 2026-06-29 17:15:10 +02:00
Vanilagy d5c94a23ab Conversion API: emit padding sample in the same format as what the decoder is emitting 2026-06-19 14:07:25 +02:00
Vanilagy af618f014a Merge branch 'main' into prores 2026-06-19 13:24:33 +02:00
github-actions[bot] 54e74bfa4b Merge main into release for tag v1.49.0 2026-06-18 17:48:19 +00:00
Vanilagy 3e686659d9 Bump minor 2026-06-18 19:46:44 +02:00
Vanilagy cdc2ef63dd Add Logging singleton to manually control Mediabunny's console output (fixes #415) 2026-06-18 19:32:18 +02:00
92a384f418 Added ID3v2 support for FLAC files. (Fixes #417) (#418)
* Added ID3v2 support for FLAC files.  (Fixes #417)

* Fix ID3v2 header size calculation, adjust ID3v2 logic for FLAC files

---------

Co-authored-by: Vanilagy <[email protected]>
2026-06-18 16:40:43 +00:00
Vanilagy 5b890efc29 Dumb oversight 2026-06-18 10:06:16 +02:00
github-actions[bot] df76bae503 Merge main into release for tag v1.48.1 2026-06-17 18:27:21 +00:00
Vanilagy 7f963367b5 Bump patch 2026-06-17 20:25:32 +02:00
Vanilagy bfd4059c22 Add advanced access unit delimination logic to MPEG-TS demuxer (fixes #414), ignore irrelevant PIDs in MPEG-TS metadata reading 2026-06-17 17:16:39 +02:00
github-actions[bot] b617c5f720 Merge main into release for tag v1.48.0 2026-06-16 18:41:51 +00:00
Vanilagy 53833f6e94 Add missing validation for HlsInputFormatOptions 2026-06-16 20:36:54 +02:00
Vanilagy 794c97b91a Bump minor 2026-06-16 20:35:34 +02:00
Vanilagy 5b4a5db1a5 Add HlsInputFormatOptions.offsetTimestampsByDateTime option, add InputTrack.getUnixTimeForTimestamp() 2026-06-16 16:32:53 +02:00
Vanilagy 2f0c040fcd Properly handle subrequests for redirected URLs 2026-06-16 14:53:27 +02:00
Vanilagy b4a6757739 Fix field typo 2026-06-16 13:43:03 +02:00
4affad934e Add per-sink decoder preferences (hardwareAcceleration, optimizeForLatency) (#406)
* 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]>
2026-06-16 11:42:24 +00:00
Vanilagy 324fae5153 Adjusted error management in ReadOrchestrator (closes #405) 2026-06-16 11:42:02 +02:00
08e3a85370 Fix orphaned queued reads when a freed worker slot is stolen concurrently (#404)
* 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]>
2026-06-16 09:26:40 +00:00
github-actions[bot] a5df6c7b89 Merge main into release for tag v1.47.0 2026-06-16 07:58:35 +00:00
Vanilagy ad167c77e7 Bump minor 2026-06-16 09:55:40 +02:00
Vanilagy 8b9a1acf16 Fix Conversion API process callbacks being called *before* other transformations (fixes #403), restructure Conversion API to make full use of in-source transformations, add AudioSample.trim() 2026-06-15 18:30:16 +02:00
Jonny BurgerandGitHub 0da1107858 [AI-generated] Fix unhandled rejection when disposing invalid input (#413)
* Fix input dispose rejection handling

* Move input disposal regression test
2026-06-15 16:25:02 +02:00
Vanilagy 00599e556e Properly handle zero-sample track fragments again (fixes #411) 2026-06-15 16:23:53 +02:00
Vanilagy 1ddeae2e80 Merge branch 'main' into prores 2026-06-12 15:41:58 +02:00
Vanilagy e1c6ab1866 Docs clarification 2026-06-12 15:36:50 +02:00
Vanilagy bc533c71f7 Fix indentation 2026-06-11 21:50:27 +02:00
Vanilagy 879fd75547 Update to NodeAv v6 (#392) 2026-06-04 10:16:42 +02:00
github-actions[bot] 210d382113 Merge main into release for tag v1.46.0 2026-06-02 17:07:56 +00:00
Vanilagy b4e51077aa Bump minor 2026-06-02 19:04:05 +02:00
Vanilagy c423d62fd9 Change keyFrame parameter priority 2026-06-02 18:18:53 +02:00
Vanilagy 0cdf0063e2 Add VideoSample.encodeOptions, optimized ArrayBuffer-backed VideoSample cloning 2026-06-02 18:01:22 +02:00
Vanilagy bb2f5d505d Clarify and better error messages 2026-06-02 16:39:04 +02:00
Vanilagy ebfb1e6d75 Add MediabunnyServerOptions and configurable hardware context (closes #389) 2026-06-02 16:28:47 +02:00
github-actions[bot] a294d943e6 Merge main into release for tag v1.45.5 2026-06-02 11:36:44 +00:00
Vanilagy 8fbd31849b Bump patch 2026-06-02 13:31:16 +02:00
Vanilagy d92df801bf Change requestEntireFile() slice offset logic (#390) 2026-06-02 13:28:37 +02:00
Vanilagy 6832153fef Merge branch 'main' of https://github.com/Vanilagy/metamuxer 2026-06-02 13:06:42 +02:00
Vanilagy 82690ef327 Clean up AVC packet NALUs with AUD in them for Chromium key frame detection (fixes #396) 2026-06-02 13:06:41 +02:00
c0d93f45a5 Support QuickTime nclc color information (#397)
* Support QuickTime nclc color information

* Tighten QuickTime nclc color test fixture

* Make QuickTime muxer write nclc color box

---------

Co-authored-by: Vanilagy <[email protected]>
2026-06-02 09:48:06 +00:00
Vanilagy 05f8ecd908 Respect Range header for UrlSource (#387) 2026-06-02 11:05:46 +02:00
Vanilagy e0ced9901e Remove NodeAv FinaliziationRegistry logic (#392) 2026-06-02 10:24:33 +02:00
96d0d9fb22 Fix tkhd issue for Apple ecosystem compatibility (#391)
* fix: ensure at least one track per type is enabled for Apple ecosystem compatibility

* Add ensureOneEnabledTrack() to ISOBMFF muxer

* Add test case

---------

Co-authored-by: Vanilagy <[email protected]>
2026-06-01 22:55:02 +02:00
Vanilagy c6c768d93e Omit 'signal', clarify docs for reading ranged resources (#387) 2026-06-01 22:00:39 +02:00
David P.andGitHub 5799cdf630 Merge pull request #387 from samohovets/docs/urlsource-requestinit-overrides
Document UrlSource RequestInit overrides
2026-06-01 21:55:47 +02:00
Vanilagy 831d395696 More stable test? 2026-06-01 21:52:34 +02:00
Vanilagy 288372ee71 Fix NodeAv memory leak (fixes #392) 2026-06-01 21:45:49 +02:00
Vanilagy 4825818f9b Add sponsors 2026-06-01 18:42:33 +02:00
Igor Samokhovets b82ccc2c6a Document UrlSource RequestInit overrides 2026-05-25 11:12:41 +02:00
Vanilagy eb8b5e2534 Add headerImage frontmatter 2026-05-24 17:59:49 +02:00
github-actions[bot] 9a8ccf6f6c Merge main into release for tag v1.45.4 2026-05-24 14:53:43 +00:00
Vanilagy 1c12f2b66d Bump patch 2026-05-24 16:50:13 +02:00
Vanilagy 441eea38b9 CustomSource -> StreamSource 2026-05-24 15:48:01 +02:00
Vanilagy ef27d73405 Fix FinalizationRegistry being able to prevent Inputs from being GCd in some cases (fixes #367) 2026-05-24 14:45:13 +02:00
Vanilagy 41a331b0dc Cảm ơn 2026-05-22 11:46:33 +02:00
Vanilagy 85a80972a0 Make MP3 next frame location more strict for better recovery (fixes #382), fix Date timezone for ID3 tags 2026-05-22 11:31:26 +02:00
github-actions[bot] f34e4cd369 Merge main into release for tag v1.45.3 2026-05-19 20:36:20 +00:00
Vanilagy 149a2b4571 Bump patch 2026-05-19 22:34:52 +02:00
Vanilagy 0c455236da Add first encoded packet key frame check (fixes #365) 2026-05-19 16:15:17 +02:00
Vanilagy 41fb9657cb Add sponsor 2026-05-19 15:20:03 +02:00
github-actions[bot] 93e6de980d Merge main into release for tag v1.45.2 2026-05-14 12:40:17 +00:00
Vanilagy d77ddf033b Bump patch 2026-05-14 14:39:16 +02:00
David P.andGitHub b9f48ca9ef Merge pull request #381 from Sturlen/fix/id3-metadata-parser-offset
Fix ID3v2 metadata parser
2026-05-14 14:03:44 +02:00
Sturlen aa38385cae fix(id3v2reader): use correct header size in readU24 2026-05-14 13:54:51 +02:00
github-actions[bot] e6fe540cbc Merge main into release for tag v1.45.1 2026-05-14 09:47:51 +00:00
Vanilagy 8bfedb0a88 Bump patch 2026-05-14 11:46:07 +02:00
Vanilagy d731037f6f Simplify PCM regex (closes #380) 2026-05-14 11:44:13 +02:00
Vanilagy f869358f93 Bump node-av to ^5.2.4 2026-05-14 11:39:55 +02:00
Vanilagy f2bd003728 Change peer dependency 2026-05-13 20:36:01 +02:00
github-actions[bot] 23e6901046 Merge main into release for tag v1.45.0 2026-05-13 18:21:58 +00:00
Vanilagy 6c9d9a56b5 Bump minor 2026-05-13 20:04:25 +02:00
Vanilagy 1c6c8c5217 Add missing stuff in docs 2026-05-13 20:03:46 +02:00
Vanilagy f151ce85e8 Add eccentric timestamp tests for server extension 2026-05-13 19:55:22 +02:00
Vanilagy 045f82584c Yes it can! 2026-05-13 18:59:58 +02:00
Vanilagy a0574fa09a Fix decoded audio always starting at 0, fix startTimestamp=0 for ranged media iterators (fixes #370) 2026-05-13 18:54:45 +02:00
Vanilagy 4c7b6d939b Yes 2026-05-13 17:31:52 +02:00
Vanilagy deb9423ddb The test is self-explanatory 2026-05-13 17:30:17 +02:00
David P.andGitHub 09a8ff0166 Merge pull request #369 from Theredbt/fix/recognize-sidx-mp4-segment
fix(input-format): recognise CMAF segments that start with sidx
2026-05-13 17:29:28 +02:00
Vanilagy 4047038113 Add doc block clarification (fixes #368) 2026-05-13 17:27:36 +02:00
Vanilagy b06bb65a0f Fix tab indentation 2026-05-13 15:59:53 +02:00
David P.andGitHub 699cfb25e6 Merge pull request #374 from Vanilagy/server
@mediabunny/server
2026-05-13 15:48:09 +02:00
Vanilagy 14c534ed00 Polish docs 2026-05-13 15:44:09 +02:00
Vanilagy 292189e3d4 Add toAvFrame, NodeAv* -> Av*, add server README, fix track synchronizer in conversion, increase default cache size for ReadableStreamSource 2026-05-13 15:26:13 +02:00
Vanilagy d2d485196f package.json pass 2026-05-12 15:08:32 +02:00
Vanilagy af43f67835 Add license headers, add @mediabunny/server to build process, add doc blocks, small fixes 2026-05-12 15:04:32 +02:00
Vanilagy 633d1bc235 Increase test timeout 🙄 2026-05-12 13:18:11 +02:00
Vanilagy c0d28fa086 Fix workspace structure, fix AAC channel layout, fix incorrect import, enable HW-accelerated video decode by default 2026-05-12 13:01:42 +02:00
Vanilagy 62ec685591 Fix missing register 2026-05-12 00:46:37 +02:00
Vanilagy 060ceb8ed3 Fix tests some more 2026-05-12 00:43:08 +02:00
Vanilagy 5599e52a1a Fix async race condition in transparency code, reduce HEVC conversion duration 2026-05-12 00:24:03 +02:00
Vanilagy 4e5f894d7a Adjust tests for CI 2026-05-12 00:06:48 +02:00
Vanilagy 30b8754411 Fix faulty imports 2026-05-11 23:55:51 +02:00
Vanilagy 42b279e9b7 More fixes 2026-05-11 23:51:50 +02:00
Vanilagy 81531cc510 Fix some bugs & polish edge cases 2026-05-11 23:50:06 +02:00
Vanilagy d8c8606341 Merge branch 'main' into server 2026-05-11 21:36:38 +02:00
Vanilagy 1bf200894c Add CPU path for transparent encoding and decoding, add transparency support to FFmpeg-backed VP9 decoder and encoder 2026-05-11 21:33:29 +02:00
Vanilagy a89d715524 Add VideoSample.transform(), add custom transformer functions, ditch CanvasSink path in Conversion API, add libavfilter-based VideoSample transformation 2026-05-11 15:57:51 +02:00
Theredbt b48eb75565 fix(input-format): recognise CMAF segments that start with sidx
Sibling case to #308: when a fragmented MP4 segment begins with a sidx
box (no ftyp/styp/moof at the file root), Mp4InputFormat._canReadInput
returned false, causing Input.getTracks() to throw
UnsupportedInputFormatError.

This shape is standard in CMAF when the DASH on-demand profile is used
or when HLS playlists are derived from one (Vimeo's vod-adaptive-ak CDN,
for example). The segment is still a valid ISOBMFF fragment — the sidx
just sits before the moof.

Adds 'sidx' alongside the existing 'moof' check so the format probe
accepts these segments, and a focused regression test that fails on
the previous behaviour.
2026-05-11 04:22:46 +01:00
Vanilagy 143ecea6c0 Merge branch 'main' into release 2026-05-10 17:51:36 +02:00
Vanilagy 89ee88bc7c Add Cursorful sponsor 2026-05-10 17:51:21 +02:00
Vanilagy 3ae73a7c5e Loosen constraint on format returned by toRgbSample, fix X->A conversio, fix tests 2026-05-10 17:31:56 +02:00
Vanilagy 1e200edc83 Implement audio decoder & encoder 2026-05-10 16:37:25 +02:00
github-actions[bot] 067081d5eb Merge main into release for tag v1.44.2 2026-05-08 22:44:02 +00:00
Vanilagy fff69619ec Lint 2026-05-09 00:42:37 +02:00
Vanilagy fc2f98825b Bump patch 2026-05-09 00:41:08 +02:00
Vanilagy 3cde68e0ae Fix AudioResampler emitting timestamps before startTimestamp (fixes #366) 2026-05-09 00:40:25 +02:00
Vanilagy ab9958234a Change VideoSampleResource interface, validate return values, add more proper VideoSample.copyTo() logic, fix decoder/encoder color space issues 2026-05-08 19:37:41 +02:00
github-actions[bot] f55a2b4764 Merge main into release for tag v1.44.1 2026-05-07 09:13:03 +00:00
Vanilagy 9e3db7cf91 Bump patch 2026-05-07 11:12:14 +02:00
Vanilagy fff09411e6 Fixed negative timestamps occurring with live media (fixes #364) 2026-05-07 11:11:37 +02:00
Vanilagy 1b1c9a0299 Merge branch 'main' into release 2026-05-06 20:12:25 +02:00
Vanilagy daf42181c9 Add sponsor 2026-05-06 20:12:14 +02:00
Vanilagy f5bd540863 Add the video portion of @mediabunny/server 2026-05-05 23:38:14 +02:00
github-actions[bot] 2b2e80dc24 Merge main into release for tag v1.44.0 2026-05-05 07:22:59 +00:00
Vanilagy e790f5ff97 Bump minor 2026-05-05 09:21:33 +02:00
Vanilagy 6d9fb30c29 Fix things that broke in the merge 2026-05-04 17:58:05 +02:00
Vanilagy 2e933ee5fc Merge branch 'main' into sample-resources 2026-05-04 17:23:45 +02:00
Vanilagy 47ad5c6806 Add BlobSourceOptions.useStreamReader (#179) 2026-05-04 14:20:21 +02:00
Vanilagy 13d1e280e8 Merge branch 'main' into release 2026-05-04 10:53:26 +02:00
Vanilagy 8955ba70c2 Sorry Jonny <3 2026-05-04 10:53:09 +02:00
Vanilagy 62287156d6 Merge branch 'main' into release 2026-05-03 19:47:55 +02:00
Vanilagy 1680bd02ad Holistic technical website cleanup 2026-05-03 19:47:39 +02:00
github-actions[bot] 100852f266 Merge main into release for tag v1.43.1 2026-05-02 12:29:10 +00:00
Vanilagy 4810c13c29 Bump patch 2026-05-02 14:27:47 +02:00
Vanilagy 9ed21bf9c6 Fix behavior with files with invalid pasp box (with num & den === 0) (fixes #362) 2026-05-02 14:27:25 +02:00
github-actions[bot] f8f08a48e1 Merge main into release for tag v1.43.0 2026-04-30 08:39:05 +00:00
Vanilagy 62dfc5dd1c Bump minor 2026-04-30 10:34:36 +02:00
Vanilagy 782d3e134b Use edit lists to model non-zero start timestamps for regular MP4 (fixes #336); fix MP4 PCM logic breaking down with approximate packet timestamps 2026-04-30 10:32:42 +02:00
Vanilagy 9224fb886c Add link rel="canonical" 2026-04-29 17:00:59 +02:00
Vanilagy a61631a299 Clarify in readme 2026-04-29 16:30:10 +02:00
Vanilagy f3dec587fd Make FLAC encoder bit depth depend on input sample format, add sampleFormat audio transformation option, fix incorrect bitrate validation for FLAC (closes #357) 2026-04-29 16:27:20 +02:00
Vanilagy 2d49122277 Sanitize initial HEVC packet to avoid Chromium's validation logic from tripping up (#314) 2026-04-29 11:37:56 +02:00
Vanilagy 71dd34fc3c Merge branch 'main' into release 2026-04-28 17:57:55 +02:00
Vanilagy 06a89ed085 Make code 12px 2026-04-28 17:57:44 +02:00
github-actions[bot] 487ae3c69a Merge main into release for tag v1.42.0 2026-04-28 15:29:11 +00:00
Vanilagy 2207c7d996 Bump minor 2026-04-28 17:27:15 +02:00
Vanilagy 7bd97b33cd Adjust package.json again + beta 11 2026-04-28 15:51:39 +02:00
Vanilagy e5dbf8a783 Add sponsor 2026-04-28 15:36:31 +02:00
github-actions[bot] 0d5bf18864 Merge main into release for tag v1.41.0 2026-04-24 17:46:40 +00:00
github-actions[bot] fc67e436e3 Merge main into release for tag v1.41.0 2026-04-24 17:44:40 +00:00
Vanilagy 735a6622a2 Merge branch 'main' into release 2026-04-01 17:20:33 +02:00
github-actions[bot] d51903edfe Merge main into release for tag v1.40.1 2026-03-24 10:17:07 +00:00
github-actions[bot] 6cdaa76fbb Merge main into release for tag v1.40.0 2026-03-19 21:42:33 +00:00
github-actions[bot] 8efe7e75db Merge main into release for tag v1.39.2 2026-03-13 10:05:14 +00:00
github-actions[bot] a892676453 Merge main into release for tag v1.39.1 2026-03-11 15:30:35 +00:00
github-actions[bot] 00f020921b Merge main into release for tag v1.39.0 2026-03-09 19:06:09 +00:00
github-actions[bot] 8071933f15 Merge main into release for tag v1.38.1 2026-03-07 08:40:38 +00:00
github-actions[bot] d97611453b Merge main into release for tag v1.38.0 2026-03-05 19:01:49 +00:00
github-actions[bot] 5e59ae4a1c Merge main into release for tag v1.37.0 2026-03-04 20:24:37 +00:00
github-actions[bot] 3b148f961a Merge main into release for tag v1.36.0 2026-03-04 16:13:56 +00:00
github-actions[bot] 95d60805ed Merge main into release for tag v1.36.0 2026-03-04 16:10:38 +00:00
github-actions[bot] 857956fb07 Merge main into release for tag v1.35.1 2026-02-26 20:49:45 +00:00
github-actions[bot] e9fc97e070 Merge main into release for tag v1.35.0 2026-02-25 16:24:03 +00:00
github-actions[bot] 15db8a27d4 Merge main into release for tag v1.34.5 2026-02-24 12:15:08 +00:00
Vanilagy 637d77b2cf Merge branch 'main' into release 2026-02-24 11:07:25 +01:00
github-actions[bot] b6bb382783 Merge main into release for tag v1.34.4 2026-02-17 11:37:33 +00:00
github-actions[bot] 7bb9961156 Merge main into release for tag v1.34.3 2026-02-16 20:06:09 +00:00
github-actions[bot] 776ae43f50 Merge main into release for tag v1.34.2 2026-02-12 17:55:25 +00:00
github-actions[bot] e533bab18a Merge main into release for tag v1.34.1 2026-02-12 17:50:59 +00:00
github-actions[bot] 26f3a576f3 Merge main into release for tag v1.34.0 2026-02-12 17:41:38 +00:00
github-actions[bot] 1f6457a50c Merge main into release for tag v1.34.0 2026-02-12 17:34:18 +00:00
Vanilagy b2d00f84d8 More work 2026-02-12 12:47:52 +01:00
github-actions[bot] f6aee7b414 Merge main into release for tag v1.33.0 2026-02-12 11:23:54 +00:00
github-actions[bot] 0cb4665d8e Merge main into release for tag v1.33.0 2026-02-12 11:13:21 +00:00
Vanilagy 525a03d220 Merge branch 'main' into release 2026-02-11 08:39:01 +01:00
Vanilagy 808422f4f5 Merge branch 'main' into release 2026-02-10 17:08:08 +01:00
github-actions[bot] e6110a5b9b Merge main into release for tag v1.32.2 2026-02-07 09:36:13 +00:00
github-actions[bot] cd2ded8767 Merge main into release for tag v1.32.1 2026-02-07 09:29:00 +00:00
github-actions[bot] db91d07780 Merge main into release for tag v1.32.0 2026-02-04 17:29:52 +00:00
github-actions[bot] af57c431d3 Merge main into release for tag v1.31.1 2026-02-03 14:54:52 +00:00
github-actions[bot] 049f24f2cb Merge main into release for tag v1.31.0 2026-01-27 13:39:24 +00:00
github-actions[bot] aa509cd62e Merge main into release for tag v1.30.1 2026-01-23 17:07:16 +00:00
github-actions[bot] 60789841a8 Merge main into release for tag v1.30.0 2026-01-23 12:53:16 +00:00
github-actions[bot] b6d3e3269d Merge main into release for tag v1.29.1 2026-01-20 19:13:22 +00:00
github-actions[bot] cea3a40902 Merge main into release for tag v1.29.0 2026-01-19 14:33:47 +00:00
github-actions[bot] 201a1b4066 Merge main into release for tag v1.28.0 2026-01-14 08:24:32 +00:00
github-actions[bot] 882051daf2 Merge main into release for tag v1.27.6 2026-01-12 15:48:35 +00:00
github-actions[bot] 91f96458fa Merge main into release for tag v1.27.5 2026-01-12 15:19:46 +00:00
Vanilagy 9d87a3e580 Merge branch 'main' into release 2026-01-12 10:32:50 +01:00
github-actions[bot] 040b0747be Merge main into release for tag v1.27.4 2026-01-06 12:38:15 +00:00
Vanilagy 53923e72dc Merge branch 'main' into release 2026-01-02 12:33:45 +01:00
Vanilagy 700a95ac35 Merge branch 'main' into sample-resources 2025-12-30 17:22:40 +01:00
github-actions[bot] 46b3ce39fb Merge main into release for tag v1.27.3 2025-12-30 15:22:33 +00:00
Vanilagy 14ae14b565 Merge branch 'main' into release 2025-12-30 14:11:52 +01:00
Vanilagy 70e3e98b53 Merge branch 'main' into release 2025-12-29 20:28:31 +01:00
github-actions[bot] 6b0527b691 Merge main into release for tag v1.27.2 2025-12-24 13:18:40 +00:00
Vanilagy 3e333636ec Merge branch 'main' into release 2025-12-23 20:16:55 +01:00
github-actions[bot] a583b9d168 Merge main into release for tag v1.27.1 2025-12-19 15:07:29 +00:00
Vanilagy 7d436c8f1f Ah of course 2025-12-17 15:44:24 +01:00
Vanilagy a56f9bd961 Rename 2025-12-17 15:41:52 +01:00
Vanilagy 12b8a1ac5a Merge branch 'main' into sample-resources 2025-12-17 15:41:36 +01:00
github-actions[bot] 29aaaf64bc Merge main into release for tag v1.27.0 2025-12-17 14:38:30 +00:00
Vanilagy 6a772a329b Add missing exports 2025-12-17 11:20:40 +01:00
Vanilagy 81d3d1e6a4 Add VideoSampleResource and AudioSampleResource 2025-12-17 11:14:12 +01:00
github-actions[bot] 5c45fd1f63 Merge main into release for tag v1.26.0 2025-12-11 19:45:19 +00:00
Vanilagy 18d64ffb33 Change ProRes codec string, remove ProRes RAW variants for now 2025-12-10 10:18:53 +01:00
Vanilagy 0dd53ff1f7 Add ProRes codec and allow demuxing & muxing 2025-12-10 09:52:54 +01:00
github-actions[bot] 7232296217 Merge main into release for tag v1.25.8 2025-12-09 13:26:37 +00:00
github-actions[bot] 30fc407c4c Merge main into release for tag v1.25.7 2025-12-05 22:03:54 +00:00
github-actions[bot] 52fd8d0e19 Merge main into release for tag v1.25.6 2025-12-04 15:58:34 +00:00
github-actions[bot] a3e43987ed Merge main into release for tag v1.25.5 2025-12-04 15:54:56 +00:00
github-actions[bot] 823ede1668 Merge main into release for tag v1.25.5 2025-12-04 15:49:12 +00:00
github-actions[bot] 1703e70a16 Merge main into release for tag v1.25.5 2025-12-04 15:45:20 +00:00
github-actions[bot] df0932d49e Merge main into release for tag v1.25.5 2025-12-04 15:41:44 +00:00
github-actions[bot] 64468b0e5b Merge main into release for tag v1.25.4 2025-12-04 09:05:03 +00:00
github-actions[bot] 903e36430f Merge main into release for tag v1.25.4 2025-12-04 09:00:14 +00:00
github-actions[bot] dbb4150718 Merge main into release for tag v1.25.3 2025-11-26 22:33:14 +00:00
github-actions[bot] 9d1f4a5f80 Merge main into release for tag v1.25.2 2025-11-25 16:28:37 +00:00
github-actions[bot] 76ac4e7aba Merge main into release for tag v1.25.1 2025-11-20 08:23:34 +00:00
github-actions[bot] f0be3ef134 Merge main into release for tag v1.25.0 2025-11-13 19:21:46 +00:00
github-actions[bot] 77fa426a9a Merge main into release for tag v1.24.7 2025-11-13 08:01:02 +00:00
github-actions[bot] b4cbf4c502 Merge main into release for tag v1.24.6 2025-11-11 21:30:53 +00:00
github-actions[bot] 27bdd33d42 Merge main into release for tag v1.24.5 2025-11-10 20:20:43 +00:00
github-actions[bot] 5972a5c906 Merge main into release for tag v1.24.4 2025-11-06 09:03:56 +00:00
Vanilagy da539cf495 Merge branch 'main' into release 2025-11-03 15:24:25 +01:00
Vanilagy 987ae467ce Merge branch 'main' into release 2025-11-03 12:12:38 +01:00
Vanilagy 2b5971d1ee Merge branch 'main' into release 2025-11-02 22:04:35 +01:00
github-actions[bot] d735f16ca5 Merge main into release for tag v1.24.3 2025-11-02 21:00:48 +00:00
github-actions[bot] 9926004401 Merge main into release for tag v1.24.2 2025-10-21 14:30:15 +00:00
github-actions[bot] 34628390b7 Merge main into release for tag v1.24.1 2025-10-20 14:14:12 +00:00
github-actions[bot] 1e46df030f Merge main into release for tag v1.24.0 2025-10-17 13:34:51 +00:00
github-actions[bot] a6fd297573 Merge main into release for tag v1.23.0 2025-10-02 14:03:04 +00:00
github-actions[bot] 447dd51e7b Merge main into release for tag v1.22.0 2025-10-02 09:34:06 +00:00
Vanilagy aebfede89a Merge branch 'main' into release 2025-10-01 17:32:31 +02:00
Vanilagy e0a1211140 Merge branch 'main' into release 2025-09-30 13:37:12 +02:00
github-actions[bot] aa8003e679 Merge main into release for tag v1.21.1 2025-09-29 19:03:27 +00:00
Vanilagy c74b6a44fb Merge branch 'main' into release 2025-09-29 09:58:12 +02:00
github-actions[bot] f21182a7b9 Merge main into release for tag v1.21.0 2025-09-26 14:39:15 +00:00
github-actions[bot] fb2dcba707 Merge main into release for tag v1.20.1 2025-09-26 08:07:45 +00:00
github-actions[bot] e25525a564 Merge main into release for tag v1.20.1 2025-09-26 08:02:50 +00:00
Vanilagy f356c05a9c Merge branch 'main' into release 2025-09-26 09:38:29 +02:00
github-actions[bot] 4e1428fcef Merge main into release for tag v1.20.0 2025-09-25 13:21:53 +00:00
github-actions[bot] ca48f31032 Merge main into release for tag v1.20.0 2025-09-25 13:18:15 +00:00
github-actions[bot] b958dd84cc Merge main into release for tag v1.19.1 2025-09-24 09:42:26 +00:00
Vanilagy f72db4dfa2 Merge branch 'main' into release 2025-09-22 22:06:18 +02:00
github-actions[bot] e9534f57c6 Merge main into release for tag v1.19.0 2025-09-22 20:03:41 +00:00
github-actions[bot] 31fdd363e3 Merge main into release for tag v1.18.1 2025-09-21 16:52:17 +00:00
github-actions[bot] 14a55fc476 Merge main into release for tag v1.18.0 2025-09-21 15:48:08 +00:00
Vanilagy 0fc3bb2b1d Merge branch 'main' into release 2025-09-19 14:32:44 +02:00
github-actions[bot] 4f670bbe06 Merge main into release for tag v1.17.3 2025-09-19 12:30:13 +00:00
github-actions[bot] 02f379d9c1 Merge main into release for tag v1.17.2 2025-09-19 12:04:29 +00:00
github-actions[bot] 588495268c Merge main into release for tag v1.17.1 2025-09-19 11:21:20 +00:00
github-actions[bot] 230778d887 Merge main into release for tag v1.17.0 2025-09-18 21:47:07 +00:00
github-actions[bot] 32267f348a Merge main into release for tag v1.16.1 2025-09-18 08:01:03 +00:00
github-actions[bot] 2aa0fd1433 Merge main into release for tag v1.16.0 2025-09-17 21:13:22 +00:00
Vanilagy bdc59306ed Merge branch 'main' into release 2025-09-17 09:12:11 +02:00
github-actions[bot] 03c1c46f9a Merge main into release for tag v1.15.2 2025-09-16 21:15:44 +00:00
github-actions[bot] b4f76fc082 Merge main into release for tag v1.15.1 2025-09-12 15:16:52 +00:00
github-actions[bot] d55431dda5 Merge main into release for tag v1.15.0 2025-09-12 15:01:03 +00:00
github-actions[bot] 6679b6b15d Merge main into release for tag v1.14.4 2025-09-10 21:04:34 +00:00
Vanilagy ad4dc659a5 Merge branch 'main' into release 2025-09-10 16:52:57 +02:00
github-actions[bot] 95fbec86e8 Merge main into release for tag v1.14.3 2025-09-09 11:11:50 +00:00
201 changed files with 18509 additions and 2086 deletions
+28
View File
@@ -0,0 +1,28 @@
name: Build check
on:
push:
branches:
- main
pull_request:
jobs:
build-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
# Building the docs also means building the core library
- name: Build everything
run: npm run docs:build
+1 -4
View File
@@ -27,7 +27,4 @@ jobs:
run: npm run check
- name: Run ESLint
run: npm run lint
- name: Run build
run: npm run build
run: npm run lint
+22 -14
View File
@@ -25,15 +25,6 @@ jobs:
with:
script: "core.setFailed('Non-prerelease releases must target the main branch (got: ${{ github.event.release.target_commitish }}).')"
- name: Merge main into release branch
if: ${{ !github.event.release.prerelease }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout release
git merge origin/main --no-ff -m "Merge main into release for tag ${{ github.event.release.tag_name }}"
git push origin release
- name: Set up Node.js
uses: actions/setup-node@v4
with:
@@ -60,8 +51,9 @@ jobs:
- name: Run tests
run: xvfb-run npm test
- name: Run build
run: npm run build
# Building the docs also builds the core library
- name: Build everything
run: npm run docs:build
- name: Upload build artifacts
env:
@@ -93,9 +85,25 @@ jobs:
packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.mjs
packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.min.mjs
packages/flac-encoder/dist/mediabunny-flac-encoder.d.ts
- name: Publish Mediabunny to npm
run: npm publish --access public ${{ github.event.release.prerelease && '--tag beta' || '' }}
packages/prores/dist/bundles/mediabunny-prores.js
packages/prores/dist/bundles/mediabunny-prores.min.js
packages/prores/dist/bundles/mediabunny-prores.mjs
packages/prores/dist/bundles/mediabunny-prores.min.mjs
packages/prores/dist/mediabunny-prores.d.ts
packages/server/dist/bundles/mediabunny-server.cjs
packages/server/dist/bundles/mediabunny-server.min.cjs
packages/server/dist/bundles/mediabunny-server.mjs
packages/server/dist/bundles/mediabunny-server.min.mjs
packages/server/dist/mediabunny-server.d.ts
- name: Publish workspace packages to npm
run: npm publish --access public --workspaces ${{ github.event.release.prerelease && '--tag beta' || '' }}
- name: Merge main into release branch
if: ${{ !github.event.release.prerelease }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout release
git merge origin/main --no-ff -m "Merge main into release for tag ${{ github.event.release.tag_name }}"
git push origin release
+3 -1
View File
@@ -9,4 +9,6 @@ node_modules
packages/mp3-encoder/dist
packages/ac3/dist
packages/aac-encoder/dist
packages/flac-encoder/dist
packages/flac-encoder/dist
packages/server/dist
packages/prores/dist
+2 -1
View File
@@ -3,5 +3,6 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"js/ts.tsdk.path": "node_modules/typescript/lib"
}
+2 -2
View File
@@ -1,6 +1,6 @@
- Prefer functions declared using const, not using the function keyword
- Code style is tab indent with semicolons
- Mediabunny core code is contained in src/, extensions are in packages/*/, website is in docs/
- Tests: Prefer fewer, longer test files over many small ones. Test files should be named after the general catergory of thing that is being tested, not after any individual single test.
- Tests: Prefer fewer, longer test files over many small ones. Test files should be named after the general category of thing that is being tested, not after any individual single test.
- Avoid ifs without a {} block. So no if (cond) return;, always do if (cond) { return; }
- `type` instead of `interface` for object types
- `type` instead of `interface` for object types
+12 -16
View File
@@ -44,14 +44,6 @@ Mediabunny is a JavaScript library for reading, writing, and converting media (l
</a>
</div>
### Silver sponsors
<div align="center">
<a href="https://ponder.ai/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/ponder.png" width="50" height="50" alt="Ponder">
</a>
</div>
### Bronze sponsors
<div align="center">
@@ -70,6 +62,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media (l
<a href="https://jellypod.ai/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/jellypod.png" width="40" height="40" alt="Jellypod">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.photocircleapp.com/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/photocircle.png" width="40" height="40" alt="PhotoCircle">
</a>
</div>
[Sponsor Mediabunny's development](https://github.com/sponsors/Vanilagy)
@@ -78,14 +74,14 @@ Mediabunny is a JavaScript library for reading, writing, and converting media (l
Core features include:
- **Wide format support**: Read **and** write MP4, MOV, WebM, MKV, HLS, WAVE, MP3, Ogg, ADTS, FLAC, MPEG-TS
- **Built-in encoding & decoding**: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API
- **High precision**: Fine-grained, microsecond-accurate reading and writing operations
- **Conversion API**: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, cropping, resampling, trimming, and more
- **Streaming I/O**: Handle reading & writing files of any size with memory-efficient streaming
- **Tree-shakable**: Only bundle what you use (as small as 5 kB gzipped)
- **Zero dependencies**: Implemented in highly performant TypeScript
- **Cross-platform**: Works in browsers and Node.js
- Wide format support: Read _and_ write MP4, MOV, WebM, MKV, HLS, WAVE, MP3, Ogg, ADTS, FLAC, MPEG-TS
- Built-in encoding & decoding: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API
- High precision: Fine-grained, microsecond-accurate reading and writing operations
- Conversion API: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, cropping, resampling, trimming, and more
- Streaming I/O: Handle reading & writing files of any size with memory-efficient streaming
- Cross-platform: Works in all browsers as well as in Node, Bun, and Deno using [`@mediabunny/server`](https://github.com/Vanilagy/mediabunny/blob/main/packages/server/README.md)
- Tree-shakable: Only bundle what you use (as small as 5 kB gzipped)
- Zero dependencies: Implemented in highly performant TypeScript
[See full feature list](https://mediabunny.dev/guide/introduction#features)
+29 -10
View File
@@ -3,10 +3,12 @@
<script src="../dist/bundles/mediabunny.cjs"></script>
<script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script>
<script src="../packages/ac3/dist/bundles/mediabunny-ac3.js"></script>
<script src="../packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.js"></script>
<script type="module">
//MediabunnyMp3Encoder.registerMp3Encoder();
MediabunnyAc3.registerAc3Decoder();
MediabunnyFlacEncoder.registerFlacEncoder();
const fileInput = document.createElement('input');
fileInput.type = 'file';
@@ -23,7 +25,7 @@
chunked: true,
chunkSize: 2**20
});
const outputFormat = new Mediabunny.WavOutputFormat();
const outputFormat = new Mediabunny.Mp4OutputFormat();
const p = document.createElement('p');
p.textContent = 'Capturing...';
@@ -57,7 +59,7 @@
const tracks = [];
let start = 0;
if (true) {
if (false) {
input = new Mediabunny.Input({
source: new Mediabunny.UrlSource('http://localhost:8000/index.m3u8'),
formats: Mediabunny.ALL_FORMATS,
@@ -85,15 +87,23 @@
});
}
const primaryTrack = await input.getPrimaryAudioTrack();
const startTime = await primaryTrack.getFirstTimestamp();
console.log(startTime)
//const primaryTrack = await input.getPrimaryAudioTrack();
//const startTime = await primaryTrack.getFirstTimestamp();
//console.log(startTime)
let ctx = null;
const canvas = new OffscreenCanvas(1280, 720);
const ctx = canvas.getContext('2d');
//let ctx = null;
let conversion = await Mediabunny.Conversion.init({
input,
output,
audio: (track) => ({ discard: track.number !== primaryTrack.number }),
audio: {
//codec: 'aac',
//forceTranscode: true,
//forceTranscode: true,
//sampleFormat: 's16',
},
/*
video: {
discard: true,
@@ -119,7 +129,10 @@
},
*/
video: {
discard: true,
process: (sample) => {
sample.draw(ctx, 0, 0, 1280, 720);
return new Mediabunny.VideoSample(canvas, { timestamp: sample.timestamp, duration: sample.duration });
},
},
tags: {} ?? {
title: 'Bigggy',
@@ -139,11 +152,17 @@
}
},
trim: {
start: startTime,
end: startTime + 2,
start: -2,
end: 10,
//start: 300.14984567374756 - 100,
//end: 310.1548298151939 - 100,
//end: 10,
//start: startTime,
//end: startTime + 2,
},
});
//console.log(conversion);
console.log(conversion.discardedTracks);
let progress = 0;
conversion.onProgress = newProgress => progress = newProgress;
+70 -8
View File
@@ -11,23 +11,84 @@
document.body.append(fileInput);
fileInput.addEventListener('change', async () => {
/*
const file = fileInput.files[0];
const input = new Mediabunny.Input({
formats: Mediabunny.ALL_FORMATS,
source: new Mediabunny.BlobSource(file),
});
const track = await input.getPrimaryAudioTrack();
const sink3 = new Mediabunny.EncodedPacketSink(track);
console.log((await sink3.getFirstPacket()).data.join(', '));
return;
console.log(await track.getDurationFromMetadata(), await track.computeDuration());
const track = await input.getPrimaryVideoTrack();
const packetSink = new Mediabunny.EncodedPacketSink(track);
//console.log(await input.getDurationFromMetadata(), await input.computeDuration());
return;
const first = await packetSink.getFirstPacket({ verifyKeyPackets: true });
const second = await packetSink.getNextPacket(first, { verifyKeyPackets: true });
const third = await packetSink.getNextPacket(second, { verifyKeyPackets: true });
const fourth = await packetSink.getNextPacket(third, { verifyKeyPackets: true });
console.log(first, second, third, fourth);
/*
for await (const packet of packetSink.packets()) {
//console.log(packet);
break;
}
const sink = new Mediabunny.AudioSampleSink(track);
let count = 0;
for await (const sample of sink.samples()) {
const buf = new Float32Array(new ArrayBuffer(sample.allocationSize({ format: 'f32-planar', planeIndex: 0 })));
sample.copyTo(buf, { format: 'f32-planar', planeIndex: 0 });
let max = Math.max(...buf.map(x => Math.abs(x)));
console.log(sample, buf, max)
if (++count === 3) {
break;
}
}
*/
/*
const track = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.EncodedPacketSink(track);
for await (const packet of sink.packets()) {
console.log(packet);
}
const output = new Mediabunny.Output({
format: new Mediabunny.Mp4OutputFormat(),
target: new Mediabunny.BufferTarget(),
});
const conversion = await Mediabunny.Conversion.init({ input, output });
await conversion.execute();
return;
// Download it now
const blob = new Blob([output.target.buffer]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name.replace(/\.\w+$/, '.mp4');
a.click();
URL.revokeObjectURL(url);
*/
/*
const track = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.EncodedPacketSink(track);
for await (const packet of sink.packets()) {
console.log(packet);
}
console.log("Done")
*/
/*
const input = new Mediabunny.Input({
source: new Mediabunny.UrlSource('https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8'),
formats: Mediabunny.ALL_FORMATS,
@@ -36,6 +97,7 @@
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
console.log(await sink.getFirstPacket());
*/
/*
return
+115 -11
View File
@@ -1,3 +1,4 @@
/* eslint-disable @stylistic/max-len */
import { withMermaid } from 'vitepress-plugin-mermaid';
import footnote from 'markdown-it-footnote';
import tailwindcss from '@tailwindcss/vite';
@@ -7,9 +8,12 @@ import { HeadConfig } from 'vitepress';
// @ts-ignore This file gets generated once docs:generate is run
import apiRoutes from '../api/index.json';
import m3u8Grammar from './m3u8-grammar.json' with { type: 'json' };
import fs from 'node:fs/promises';
import path from 'node:path';
const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,'
+ ' and faster than anybunny else.';
const ORIGIN = 'https://mediabunny.dev';
// https://vitepress.dev/reference/site-config
export default withMermaid({
@@ -17,22 +21,31 @@ export default withMermaid({
description: DESCRIPTION,
cleanUrls: true,
sitemap: {
hostname: 'https://mediabunny.dev',
hostname: ORIGIN,
transformItems: async (items) => {
const entries = await fs.readdir('./examples');
for (const entry of entries) {
const isDirectory = await fs.stat(path.join('./examples', entry)).then(stat => stat.isDirectory());
if (isDirectory) {
items.push({
url: `/examples/${entry}/`, // With trailing slash
});
}
}
return items;
},
},
lastUpdated: true,
// lastUpdated: true,
head: [
['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }],
['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'Mediabunny' }],
['meta', { property: 'og:url', content: 'https://mediabunny.dev/' }],
['meta', { property: 'og:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }],
['meta', { property: 'og:image', content: `${ORIGIN}/mediabunny-og-image.png` }],
['meta', { property: 'og:locale', content: 'en-US' }],
['meta', { property: 'og:description', content: DESCRIPTION }],
['meta', { name: 'twitter:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }],
['meta', { name: 'twitter:image', content: `${ORIGIN}/mediabunny-og-image.png` }],
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { name: 'twitter:site', content: '@vanilagy' }],
['meta', { name: 'twitter:description', content: DESCRIPTION }],
],
themeConfig: {
logo: '/mediabunny-logo.svg',
@@ -40,12 +53,12 @@ export default withMermaid({
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Guide', link: '/guide/introduction', activeMatch: '/guide' },
{ text: 'API', link: '/api', activeMatch: '/api' },
{ text: 'API', link: '/api/', activeMatch: '/api/' }, // Trailing slash because it's index.html from there
{ text: 'LLMs', link: '/llms', activeMatch: '/llms' },
{ text: 'Examples', link: '/examples', activeMatch: '/examples' },
{ text: 'Blog', link: '/blog', activeMatch: '/blog' },
{ text: 'Sponsors', link: '/#sponsors', activeMatch: '/#sponsors' },
{ text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license' },
{ text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license', rel: 'noopener' },
{
text: 'More',
items: [
@@ -103,10 +116,12 @@ export default withMermaid({
{
text: 'Extensions',
items: [
{ text: 'server', link: '/guide/extensions/server' },
{ text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' },
{ text: 'aac-encoder', link: '/guide/extensions/aac-encoder' },
{ text: 'ac3', link: '/guide/extensions/ac3' },
{ text: 'flac-encoder', link: '/guide/extensions/flac-encoder' },
{ text: 'prores', link: '/guide/extensions/prores' },
],
},
],
@@ -129,6 +144,7 @@ export default withMermaid({
{ text: 'VP8', link: '/codec-registry/vp8' },
{ text: 'VP9', link: '/codec-registry/vp9' },
{ text: 'AV1', link: '/codec-registry/av1' },
{ text: 'ProRes', link: '/codec-registry/prores' },
],
},
{
@@ -186,6 +202,8 @@ export default withMermaid({
llmstxt({
ignoreFiles: [
'api/*',
'examples.md',
'llms.md',
],
}),
],
@@ -197,9 +215,95 @@ export default withMermaid({
title += ' | Mediabunny';
}
const canonicalUrl = `${ORIGIN}/${pageData.relativePath}`
.replace(/index\.md$/, '')
.replace(/\.md$/, '');
const isBlogPost = canonicalUrl.includes('/blog/');
const breadcrumbs: object[] = [];
if (canonicalUrl.includes('/guide/')) {
breadcrumbs.push({
'@type': 'ListItem',
'position': 1,
'name': 'Guide',
'item': `${ORIGIN}/guide`,
}, {
'@type': 'ListItem',
'position': 2,
'name': pageData.title,
});
}
if (canonicalUrl.includes('/api/')) {
breadcrumbs.push({
'@type': 'ListItem',
'position': 1,
'name': 'API docs',
'item': `${ORIGIN}/api/`,
}, {
'@type': 'ListItem',
'position': 2,
'name': pageData.title,
});
}
if (canonicalUrl.includes('/codec-registry/')) {
breadcrumbs.push({
'@type': 'ListItem',
'position': 1,
'name': 'Codec registry',
'item': `${ORIGIN}/codec-registry/overview`,
}, {
'@type': 'ListItem',
'position': 2,
'name': pageData.title,
});
}
((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push(
['meta', { property: 'og:type', content: isBlogPost ? 'article' : 'website' }],
['meta', { property: 'og:title', content: title }],
['meta', { property: 'twitter:title', content: title }],
['meta', { property: 'og:description', content: pageData.description || DESCRIPTION }],
['meta', { property: 'og:url', content: canonicalUrl }],
['meta', { name: 'twitter:title', content: title }],
['meta', { name: 'twitter:description', content: pageData.description || DESCRIPTION }],
['link', { rel: 'canonical', href: canonicalUrl }],
);
if (isBlogPost) {
((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push(
['meta', { property: 'article:published_time', content: String(pageData.frontmatter['publishedOnIso']) }],
['meta', { property: 'article:author', content: String(pageData.frontmatter['author']) }],
);
breadcrumbs.push({
'@type': 'ListItem',
'position': 1,
'name': 'Blog posts',
'item': `${ORIGIN}/blog`,
}, {
'@type': 'ListItem',
'position': 2,
'name': pageData.title,
});
}
if (breadcrumbs.length > 0) {
((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push(
['script', { type: 'application/ld+json' }, JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
'itemListElement': breadcrumbs,
})],
);
}
},
buildEnd: async () => {
const files = await fs.readdir('./docs/api');
for (const file of files) {
await fs.copyFile('./docs/api/' + file, './dist-docs/api/' + file);
}
},
});
+5 -1
View File
@@ -1,6 +1,7 @@
{
"heading": "Mediabunny API reference",
"intro": "Here you can find detailed documentation for all classes, functions, constants and types exposed by Mediabunny's public API.",
"description": "Detailed documentation for all classes, functions, constants and types exposed by Mediabunny's public API.",
"Samples": "Raw, unencoded chunks of media data, such as video frames or sections of audio.",
"Packets": "Chunks of encoded media data.",
@@ -18,10 +19,13 @@
"Encoding": "Encoder configuration and encodability checks.",
"Decoding": "Decoder configuration and decodability checks.",
"Custom coders": "API for adding custom encoders and decoders.",
"Logging": "Control over what Mediabunny logs to the console.",
"Miscellaneous": "Whatever's left.",
"@mediabunny/server": "Adds full video/audio decoder and encoder support to Mediabunny running in server-side environments such as Node, Bun, or Deno.",
"@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny.",
"@mediabunny/ac3": "Adds AC-3/E-AC-3 decoder and encoder support to Mediabunny.",
"@mediabunny/aac-encoder": "Polyfills AAC encoder support to Mediabunny.",
"@mediabunny/flac-encoder": "Adds FLAC encoder support to Mediabunny."
"@mediabunny/flac-encoder": "Adds FLAC encoder support to Mediabunny.",
"@mediabunny/prores": "Adds Apple ProRes decoder support to Mediabunny."
}
+3 -1
View File
@@ -1,4 +1,6 @@
---
title: Blog posts
description: Announcements, news, and devblogs about Mediabunny.
outline: false
---
@@ -12,7 +14,7 @@ import BlogAuthor from './components/BlogAuthor.vue';
<template v-for="(post, i) in data">
<a :href="post.url" class="flex flex-col sm:flex-row items-start sm:gap-8 !text-inherit !no-underline ![font-weight:inherit] group">
<div>
<img src="./assets/on-air.png" class="shrink-0 sm:w-40 rounded" />
<img :src="post.frontmatter.headerImage" class="shrink-0 sm:w-40 rounded" />
<div v-if="false" class="p-1">
<BlogAuthor :frontmatter="post.frontmatter" small />
</div>
+7 -4
View File
@@ -1,10 +1,13 @@
---
title: Mediabunny now supports HLS!
description: Mediabunny v1.42.0 adds read/write support for HTTP Live Streaming (HLS), pushing the frontier of fully client-side media processing.
publishedOn: April 24, 2026
publishedOnIso: "2026-04-24"
author: Vanilagy
authorImage: /vani.png
authorLink: https://github.com/Vanilagy
authorSubtitle: Creator of Mediabunny
headerImage: /on-air.png
excerpt: Mediabunny v1.42.0 adds read/write support for HTTP Live Streaming (HLS). This has been in the making for the last ~4 months and is, by far, the biggest addition to Mediabunny since its original release.
---
@@ -12,7 +15,7 @@ excerpt: Mediabunny v1.42.0 adds read/write support for HTTP Live Streaming (HLS
import BlogAuthor from '../components/BlogAuthor.vue';
</script>
<img src="../assets/on-air.png" class="rounded-2xl mb-2">
<img :src="$frontmatter.headerImage" class="rounded-2xl mb-2" />
<p class="!m-0 opacity-70">{{ $frontmatter.publishedOn }}</p>
@@ -77,7 +80,7 @@ The API surface added by the HLS update is vast and I obviously can't cover it i
By using the Conversion API, you can just do this:
<div class="text-xs">
<div class="text-[13.7142857143px]">
```ts
import { ... } from 'mediabunny';
@@ -107,7 +110,7 @@ That's it. This will stream-download the entire HLS playlist, transcode it if ne
This is basically the inverse of the previous example. Just like we're able to read HLS and turn it into an MP4, we're able to read any input file and turn it into a full HLS playlist including master playlist, media playlists and segments:
<div class="text-xs">
<div class="text-[13.7142857143px]">
```ts
import { ... } from 'mediabunny';
@@ -163,7 +166,7 @@ No transcode server is needed here, it's all handled by the client, and the serv
You could build an OBS-like broadcasting system where a user records their screen, facecam or microphone, encodes multiple variants locally, and then broadcasts finished HLS segments directly to the server, meaning no transcoding is needed.
<div class="text-xs overflow-auto">
<div class="text-[13.7142857143px] overflow-auto">
```ts
// Get the screen and mic
+4
View File
@@ -1,3 +1,7 @@
---
description: Advanced Audio Coding (AAC) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: Dolby Digital (AC-3) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: A-law companded PCM audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: AOMedia Video 1 (AV1) video codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: Advanced Video Coding (H.264) video codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: Dolby Digital Plus (E-AC-3) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: Free Lossless Audio Codec (FLAC) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: High Efficiency Video Coding (H.265) video codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: MP3 (MPEG-1/2 Audio Layer III) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: Opus audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+5
View File
@@ -1,3 +1,7 @@
---
description: The Mediabunny Codec Registry formalizes the precise definitions of all video and audio codecs supported by Mediabunny, like expected codec string and packet data format.
---
# Mediabunny Codec Registry
The Mediabunny Codec Registry formalizes the precise definitions of all video and audio codecs supported by Mediabunny. More specifically, for any given codec, it describes the format that `EncodedPacket`, `VideoDecoderConfig` and `AudioDecoderConfig` must adhere to. All packets coming out of or going into Mediabunny are expected to adhere to this registry.
@@ -11,6 +15,7 @@ The registry is an extension of the [WebCodecs Codec Registry](https://www.w3.or
- [VP8](./vp8)
- [VP9](./vp9)
- [AV1](./av1)
- [ProRes](./prores)
## Audio codecs
+4
View File
@@ -1,3 +1,7 @@
---
description: PCM audio codec definitions, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+44
View File
@@ -0,0 +1,44 @@
---
description: Apple ProRes video codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# ProRes codec registration
## Description
The Apple ProRes video codec, as specified in [SMPTE RDD 36](https://pub.smpte.org/doc/rdd36/).
## Codec ID
```ts
'prores'
```
## `EncodedPacket` data
The packet's data must be a `frame()` as defined in [SMPTE RDD 36](https://pub.smpte.org/doc/rdd36/).
## `EncodedPacket` type
Since ProRes is intra-frame-only, every packet is a key frame and its type is therefore always `'key'`.
## `VideoDecoderConfig` codec string
The codec string must be one of the six four-character codes identifying the ProRes profile:
- `'ap4x'` - ProRes 4444 XQ
- `'ap4h'` - ProRes 4444
- `'apch'` - ProRes 422 High Quality
- `'apcn'` - ProRes 422 Standard Definition
- `'apcs'` - ProRes 422 LT
- `'apco'` - ProRes 422 Proxy
## `VideoDecoderConfig` description
`description` is not used for this codec.
+4
View File
@@ -1,3 +1,7 @@
---
description: μ-law companded PCM audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: Vorbis audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: VP8 video codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+4
View File
@@ -1,3 +1,7 @@
---
description: VP9 video codec definition, defining legal codec strings, decoder configs, and packet data formats.
---
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
+1
View File
@@ -1,4 +1,5 @@
---
description: "Demos showcasing the various features of Mediabunny: metadata extraction, thumbnail generation, compression, media playback, live streaming, and more."
layout: home
title: Examples
+5
View File
@@ -1,3 +1,7 @@
---
description: Efficiently convert any media file to any format, directly in the browser. Optionally apply trimming, video resizing, rotation, custom overlays, and more.
---
# Converting media files
The [reading](./reading-media-files) and [writing](./writing-media-files) primitives in Mediabunny provide everything you need to convert media files. However, since this is such a common operation and the details can be tricky, Mediabunny ships with a built-in file conversion abstraction.
@@ -242,6 +246,7 @@ type ConversionAudioOptions = {
bitrate?: number | Quality;
numberOfChannels?: number;
sampleRate?: number;
sampleFormat?: 'u8' | 's16' | 's32' | 'f32';
forceTranscode?: boolean;
process?: (sample: AudioSample) => MaybePromise<
AudioSample | AudioSample[] | null
+4
View File
@@ -1,3 +1,7 @@
---
description: The @mediabunny/aac-encoder extension provides a fast AAC encoder polyfill for use in the browser and on the server.
---
# @mediabunny/aac-encoder
Some browsers lack support for AAC encoding in their WebCodecs implementations. This extension package provides a reliable AAC-LC encoder for use with Mediabunny. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [FFmpeg](https://ffmpeg.org/)'s AAC encoder under the hood.
+4
View File
@@ -1,3 +1,7 @@
---
description: The @mediabunny/ac3 extension provides fast AC-3 and E-AC-3 decoders and encoders for both browser and server environments.
---
# @mediabunny/ac3
Browsers have no support for AC-3 (Dolby Digital) or E-AC-3 (Dolby Digital Plus) in their WebCodecs implementations. This extension package provides both a decoder and encoder for use with Mediabunny, allowing you to decode and encode these codecs directly in the browser. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [FFmpeg](https://ffmpeg.org/)'s AC-3 and E-AC-3 coders under the hood.
+4
View File
@@ -1,3 +1,7 @@
---
description: The @mediabunny/flac-encoder extension provides a highly-performant FLAC encoder polyfill for use in the browser and on the server.
---
# @mediabunny/flac-encoder
No browser currently supports FLAC encoding in their WebCodecs implementations. This extension package provides a reliable FLAC encoder for use with Mediabunny. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [libFLAC](https://github.com/xiph/flac) under the hood.
+4
View File
@@ -1,3 +1,7 @@
---
description: The @mediabunny/mp3-encoder extension provides a highly-performant MP3 encoder polyfill for use in the browser and on the server.
---
# @mediabunny/mp3-encoder
Browsers typically have no support for MP3 encoding in their WebCodecs implementations. Given the ubiquity of the format, this extension package provides an MP3 encoder for use with Mediabunny. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a highly-performant WASM build of the [LAME MP3 Encoder](https://lame.sourceforge.io/) under the hood.
+55
View File
@@ -0,0 +1,55 @@
---
description: The @mediabunny/prores extension provides an extremely fast Apple ProRes decoder for the browser.
---
# @mediabunny/prores
Browsers have no support for Apple ProRes in their WebCodecs implementations. This extension package provides a decoder for use with Mediabunny, allowing you to decode ProRes directly in the browser at unprecedented speed. It is implemented using Mediabunny's [custom coder API](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) and uses [TurboRes](https://github.com/Vanilagy/turbores), an extremely fast WASM-based ProRes decoder, under the hood.
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="https://github.com/Vanilagy/mediabunny/blob/main/packages/prores/README.md">
GitHub page
<span class="vpi-arrow-right" />
</a>
## Installation
This library peer-depends on Mediabunny. Install both using npm:
```bash
npm install mediabunny @mediabunny/prores
```
Alternatively, directly include them using a script tag:
```html
<script src="mediabunny.js"></script>
<script src="mediabunny-prores.js"></script>
```
This will expose the global objects `Mediabunny` and `MediabunnyProres`. Use `mediabunny-prores.d.ts` to provide types for these globals. You can download the built distribution files from the [releases page](https://github.com/Vanilagy/mediabunny/releases).
## Setup
`@mediabunny/prores` can make use of shared-memory multithreading to achieve maximum performance. To enable this in browsers, your website must be cross-origin isolated by setting the following response headers:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
Alternatively, you can use:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
```
which is generally more permissive but is not supported in Safari (of course).
---
If you cannot enable cross-origin isolation, `@mediabunny/prores` will fall back to a slower multithreading algorithm.
## Usage
```ts
import { registerProresDecoder } from '@mediabunny/prores';
registerProresDecoder();
```
That's it - Mediabunny now uses the registered ProRes decoder automatically.
+318
View File
@@ -0,0 +1,318 @@
---
description: Add full video and audio decoder and encoder support for use in server-side environments, like Node, Bun, or Deno.
---
# @mediabunny/server
By default, Mediabunny requires a browser environment for full access to decoders, encoders, and video processing features. `@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) to polyfill this functionality for server-side environments such as Node, Bun, or Deno, enabling the usage of all Mediabunny features on the server. The result is a server-side media processing API that integrates naturally with TypeScript as opposed to the awkwardness and inefficiencies of calling out to the FFmpeg CLI.
Features added by this package include:
- Video decoders and encoders for AVC (H.264), HEVC (H.265), VP8, VP9, AV1, and ProRes. Supports both length-prefixed and Annex B AVC/HEVC as well as transparent video via VP9 and ProRes.
- Audio decoders and encoders for AAC, MP3, Vorbis, Opus, FLAC, AC-3 and E-AC-3. Supports AAC in both AAC and ADTS formats.
- Video frame transformation support (resize, rotate, crop)
- Automatic hardware acceleration on all platforms (macOS, Linux, Windows)
- Built-in multithreading
- Zero-copy decode and encode paths
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="https://github.com/Vanilagy/mediabunny/blob/main/packages/server/README.md">
GitHub page
<span class="vpi-arrow-right" />
</a>
## Installation
This library peer-depends on Mediabunny. Install both using npm:
```bash
npm install mediabunny @mediabunny/server
```
## Usage
```ts
import { registerMediabunnyServer } from '@mediabunny/server';
registerMediabunnyServer();
```
That's it - you now have access to the full Mediabunny feature set on the server.
---
An optional `options` parameter is available for further configuration:
```ts
import { registerMediabunnyServer } from '@mediabunny/server';
import * as NodeAv from 'node-av';
registerMediabunnyServer({
// Use a specific hardware rendering device:
hardwareContext: NodeAv.HardwareContext.create(
NodeAv.AV_HWDEVICE_TYPE_VAAPI,
'/dev/dri/renderD128',
),
});
```
## Upload media compression example
Here, we set up a simple media compression server in Node.js. The client's request body is streamed to Mediabunny, the media gets processed, and the output is streamed directly to the disk. Memory usage is O(1) due to pipelining, and an overly fast uploader is automatically slowed down due to stream backpressure.
```ts
import { ALL_FORMATS, Conversion, FilePathTarget, Input, Mp4OutputFormat, Output, QUALITY_MEDIUM, ReadableStreamSource } from "mediabunny";
import { registerMediabunnyServer } from "@mediabunny/server";
import { Readable } from "node:stream";
import http from "node:http";
registerMediabunnyServer();
const server = http.createServer(async (req, res) => {
// Read the request body as a stream
const stream = Readable.toWeb(req) as ReadableStream<Uint8Array>;
const input = new Input({
source: new ReadableStreamSource(stream),
formats: ALL_FORMATS,
});
// Stream the output directly to the disk, could also stream to S3 etc.
const output = new Output({
format: new Mp4OutputFormat(),
target: new FilePathTarget(`./converted-${crypto.randomUUID()}.mp4`),
});
try {
const conversion = await Conversion.init({
input,
output,
video: async track => ({
codec: 'avc',
height: Math.min(720, await track.getDisplayHeight()),
bitrate: QUALITY_MEDIUM,
}),
});
await conversion.execute();
res.statusCode = 204;
res.end();
} catch (error) {
res.statusCode = 500;
res.end();
console.error("Error processing media:", error);
}
});
server.listen(3000);
```
For all the other ways to use Mediabunny, refer to its [guide](https://mediabunny.dev/guide/introduction).
## Performance
`@mediabunny/server` is extremely performant as it is a thin wrapper around [NodeAV](https://github.com/seydx/node-av), which itself is a thin wrapper around the FFmpeg C API. All decoders and encoders automatically run on separate threads, keeping the main thread unblocked. Hardware acceleration is automatically detected and utilized on all operating systems whenever available (unless explicitly disabled using `hardwareAcceleration: 'prefer-software'`). Video frame and audio sample data is never copied from FFmpeg unless explicitly requested via `VideoSample.copyTo()` and `AudioSample.copyTo()`, and zero-copy GPU decode -> encode paths are used automatically whenever possible.
## Advanced usage
### Usage with NodeAV
`@mediabunny/server` provides `AvFrameVideoSampleResource` and `AvFrameAudioSampleResource` as a means to create `VideoSample` and `AudioSample` instances that are directly backed by data residing in NodeAV's [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html) (and therefore FFmpeg's `AVFrame`) without ever having to copy data to or from JavaScript. Reading NodeAV's documentation can help you make full use of this integration.
To convert between Mediabunny and NodeAV (FFmpeg) worlds, you can do this:
```ts
import { VideoSample, AudioSample } from 'mediabunny';
import { AvFrameVideoSampleResource, AvFrameAudioSampleResource, toAvFrame } from '@mediabunny/server';
// Frame -> VideoSample
new VideoSample(new AvFrameVideoSampleResource(frame), { timestamp });
// Frame -> AudioSample
new AudioSample(new AvFrameAudioSampleResource(frame));
// (uses the timestamp in the frame)
// VideoSample -> Frame
await toAvFrame(videoSample, frame);
// AudioSample -> Frame
await toAvFrame(audioSample, frame);
```
#### Electron example
For example, when using Electron, we may want to capture the app's contents without moving video data from the GPU to the CPU:
```ts
import { VideoSample } from 'mediabunny';
import { AvFrameVideoSampleResource } from '@mediabunny/server';
import { HardwareContext, SharedTexture, AV_HWDEVICE_TYPE_VIDEOTOOLBOX } from 'node-av';
// Create hardware context (platform-specific)
const hw = HardwareContext.create(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
using sharedTexture = SharedTexture.create(hw);
// In Electron paint event with offscreen rendering
offscreen.webContents.on('paint', (event) => {
const texture = event.texture;
if (!texture?.textureInfo) {
return;
}
// Import as hardware frame (zero-copy)
const frame = sharedTexture.importTexture(texture.textureInfo, { pts: 0n });
const sample = new VideoSample(new AvFrameVideoSampleResource(frame), {
timestamp: 0,
duration: 0,
});
texture.release();
});
```
#### Microphone recording example
Here, we're using NodeAV's Device API to access the user's microphone:
```ts
import { AudioSample } from 'mediabunny';
import { AvFrameAudioSampleResource } from '@mediabunny/server';
import { DeviceAPI, Decoder } from 'node-av';
await using mic = await DeviceAPI.openMicrophone();
const audioStream = mic.audio()!;
using decoder = await Decoder.create(audioStream);
let firstTimestamp: number | null = null;
for await (const frame of decoder.frames(mic.packets(audioStream.index))) {
if (!frame) {
break;
}
const sample = new AudioSample(new AvFrameAudioSampleResource(frame));
if (firstTimestamp === null) {
firstTimestamp = sample.timestamp;
}
// Offset timestamps so they start at 0
sample.setTimestamp(sample.timestamp - firstTimestamp);
// Do something with the sample now, like passing it to an AudioSampleSource
// ...
}
```
### Video and audio processing
Browser environments ship with many API goodies such as the Canvas 2D API which are a naturally great fit for doing video frame processing, and they integrate well with Mediabunny. On the server, these APIs don't exist, so other approaches must be used:
#### VideoSample.transform()
This method allows for simple transformations on `VideoSample` instances and works when `@mediabunny/server` has been registered:
```ts
const transformed = await sample.transform({
width: 640,
height: 360,
fit: 'cover',
});
```
#### NodeAV filter graphs
FFmpeg's `libavfilter` is an incredibly powerful and generic media processing library, and all of it is directly accessible via NodeAV. It works for both video as well as audio data.
For example, here we combine Mediabunny's Conversion API with a filter graph to grayscale a video:
```ts
import { Conversion } from 'mediabunny';
import { AvFrameVideoSampleResource, toAvFrame } from '@mediabunny/server';
import { Frame, FilterAPI } from 'node-av';
async function* one(f: Frame) { yield f; }
const conversion = await Conversion.init({
// ...
video: {
process: async (sample) => {
// VideoSample -> Frame
using inFrame = new Frame();
inFrame.alloc();
await toAvFrame(sample, inFrame);
// Frame -> filter -> AvFrameVideoSampleResource
using filter = FilterAPI.create('format=gray');
for await (const outFrame of filter.frames(one(inFrame))) {
return outFrame && new AvFrameVideoSampleResource(outFrame);
}
return null;
},
},
// ...
});
await conversion.execute();
```
#### Canvas API polyfills
Libraries like [Skia Canvas](https://github.com/samizdatco/skia-canvas) provide GPU-enabled polyfills for the Canvas 2D API. Using it with Mediabunny is simply a matter of converting from and to the Canvas API:
```ts
const width = videoSample.displayWidth;
const height = videoSample.displayHeight;
const canvas = new Canvas(width, height);
const ctx = canvas.getContext('2d');
// Copy data from VideoSample
const imageData = ctx.createImageData(width, height);
await videoSample.copyTo(imageData.data, { format: 'RGBA' });
ctx.putImageData(imageData, 0, 0);
// Issue draw commands
ctx.fillStyle = 'red';
ctx.fillRect(20, 20, 100, 60);
// Convert to VideoSample again
const pixels = ctx.getImageData(0, 0, width, height).data;
return new VideoSample(pixels, {
format: 'RGBA',
codedWidth: width,
codedHeight: height,
timestamp: videoSample.timestamp,
duration: videoSample.duration,
});
```
## Controlling logging
Especially in command-line applications you usually don't want Mediabunny interfering with your stdout and stderr output. Mediabunny provides ways to control its console output:
```ts
import { Logging, LogLevel } from 'mediabunny';
// The default: Mediabunny can log errors, warnings, and information messages.
Logging.level = LogLevel.Info;
// Only log warnings and errors.
Logging.level = LogLevel.Warnings;
// Only log errors.
Logging.level = LogLevel.Errors;
// Don't log anything at all.
Logging.level = LogLevel.Silent;
```
You can also hook into log events:
```ts
Logging.on('error', (args: unknown[]) => {
// Handle error message
});
Logging.on('warn', (args: unknown[]) => {
// Handle warning message
});
Logging.on('info', (args: unknown[]) => {
// Handle info message
});
```
## Implementation details
`@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) under the hood which provides N-API C bindings to FFmpeg's C API. Using NodeAV, this package implements [custom decoders and encoders](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) by directly using the APIs provided by `libavcodec`.
For encoding, video frames and audio samples are transferred to FFmpeg by converting them to an `AVFrame` and are then passed to the correct encoder. The resulting packets are then normalized into the format expected by WebCodecs and the [Mediabunny Codec Registry](https://mediabunny.dev/codec-registry/overview). For decoding, the above process is inverted: packets and decoder metadata are passed to the correct decoder, and the resulting `AVFrame` instances are wrapped in `VideoSample` or `AudioSample` instances. Video frame transformations (resize, rotate, crop) are implemented using the `libavfilter` API.
Whenever possible, `AVFrame`s are never copied over to JavaScript unless explicitly needed. This enables zero-copy decode -> transformation -> encode paths.
+4
View File
@@ -1,3 +1,7 @@
---
description: Mediabunny supports a wide range of media input formats, such as MP4, WebM, MP3, HLS, and many more.
---
# Input formats
Mediabunny supports a wide variety of commonly used container formats for reading input files. These *input formats* are used in two ways:
+4
View File
@@ -1,3 +1,7 @@
---
description: Install Mediabunny using npm (or your favorite package manager), or include it directly as a <script> tag.
---
# Installation
Install Mediabunny using your favorite package manager:
+21 -3
View File
@@ -1,3 +1,7 @@
---
description: Mediabunny is a zero-dependency, tree-shakable TypeScript library for reading, writing and converting media files in the browser. Like FFmpeg, but for the web.
---
# Introduction
Mediabunny is a JavaScript library for reading, writing, and converting media files (like MP4 or WebM), directly in the browser. It aims to be a complete toolkit for high-performance media operations on the web. It's written from scratch in pure TypeScript, has zero dependencies, and is extremely tree-shakable, meaning you only include what you use. You can think of it a bit like [FFmpeg](https://ffmpeg.org/), but built for the web's needs.
@@ -27,7 +31,7 @@ Here's a long list of stuff this library does:
- Custom encoder & decoder support for polyfilling
- Low- & high-level abstractions for different use cases
- Performant everything
- Node.js support
- Server-side support (Node, Bun, Deno) via `@mediabunny/server`
...and there's probably more.
@@ -54,9 +58,23 @@ To get going with Mediabunny, here are some starting points:
- Start with [Converting media files](./converting-media-files) if you care about file conversions.
- Dive into [Packets & samples](./packets-and-samples) for a deeper understanding of the concepts underlying this library.
## Server-side usage
Mediabunny's simple yet flexible API provides a modern alternative to traditional server-side media processing pipelines which may involve calling out to FFmpeg's CLI manually. Mediabunny was primarily built for client-side environments, but when combined with the [`@mediabunny/server`](./extensions/server) extension, the full Mediabunny feature set is available in server-side environments such as Node, Bun, and Deno.
The extension enables:
- Video decoders and encoders for AVC (H.264), HEVC (H.265), VP8, VP9, and AV1. Supports both length-prefixed and Annex B AVC/HEVC as well as transparent video via VP9.
- Audio decoders and encoders for AAC, MP3, Vorbis, Opus, FLAC, AC-3 and E-AC-3. Supports AAC in both AAC and ADTS formats.
- Video frame transformation support (resize, rotate, crop)
- Automatic hardware acceleration on all platforms (macOS, Linux, Windows)
- Built-in multithreading
- Zero-copy decode and encode paths
For more, see [the corresponding guide](./extensions/server).
## Motivation
Mediabunny is the evolution of my previous libraries, [mp4-muxer](https://github.com/Vanilagy/mp4-muxer) and [webm-muxer](https://github.com/Vanilagy/webm-muxer), which were both created due to the advent of the WebCodecs API. While they fulfilled their job just fine, I saw a few painpoints:
Mediabunny is the evolution of my previous libraries, [mp4-muxer](https://github.com/Vanilagy/mp4-muxer) and [webm-muxer](https://github.com/Vanilagy/webm-muxer), which were both created due to the advent of the WebCodecs API. While they fulfilled their job just fine, I saw a few pain points:
- Lots of duplicated code between the two libraries, otherwise very similar API.
- No help with the difficulties of navigating the WebCodecs API & related browser APIs.
- "mp4-demuxer when??"
@@ -81,4 +99,4 @@ At its core, Mediabunny is a collection of multiplexers and demultiplexers, one
Mediabunny then provides several wrappers around the WebCodecs API to simplify usage: for reading, it creates decoders with the correct codec configuration and efficiently decodes media data in a pipelined way. For writing, it figures out the necessary codec configuration and sets up encoders which are then used to encode raw media data, while respecting the backpressure applied by the encoder. Extracting the right decoder configuration from a media file can be tricky and sometimes involves diving into encoded media packet bitstreams.
The conversion abstraction is built on top of Mediabunny's reading and writing primitives and combines them both in a heavily-pipelined way, making sure reading and writing happen in lockstep. It also consists of a lot of conditional logic probing output track compatibility, decoding support, and finding encodable codec configurations. It makes use of the Canvas API for video processing operations, and uses a custom implementation for audio resampling and up/downmixing.
The conversion abstraction is built on top of Mediabunny's reading and writing primitives and combines them both in a heavily-pipelined way, making sure reading and writing happen in lockstep. It also consists of a lot of conditional logic probing output track compatibility, decoding support, and finding encodable codec configurations. It makes use of the Canvas API for video processing operations, and uses a custom implementation for audio resampling and up/downmixing.
+15
View File
@@ -1,3 +1,7 @@
---
description: Media sinks enable different ways to extract video and audio data from files, from built-in decoding to manual per-packet control.
---
# Media sinks
## Introduction
@@ -221,6 +225,11 @@ Create the sink like so:
import { VideoSampleSink } from 'mediabunny';
const sink = new VideoSampleSink(videoTrack);
// Optionally, configure the decoder:
const sink = new VideoSampleSink(videoTrack, {
hardwareAcceleration: 'prefer-software',
});
```
#### Single retrieval
@@ -359,6 +368,8 @@ type CanvasSinkOptions = {
rotation?: 0 | 90 | 180 | 270;
crop?: { left: number; top: number; width: number; height: number };
poolSize?: number;
alpha?: boolean;
decoderOptions?: VideoSinkDecoderOptions;
};
```
- `width`\
@@ -376,6 +387,10 @@ type CanvasSinkOptions = {
Specifies the rectangular region of the input video to crop to. The crop region will automatically be clamped to the dimensions of the input video track. Cropping is performed after rotation but before resizing. The crop region is in the _display pixel space_ of the underlying video data.
- `poolSize`\
See [Canvas pool](#canvas-pool).
- `alpha`\
Whether the output canvases should have transparency instead of a black background. Defaults to `false`. Set this to `true` when using this sink to read transparent videos.
- `decoderOptions`\
Additional preferences for the underlying video decoder.
Some examples:
```ts
+4
View File
@@ -1,3 +1,7 @@
---
description: Media sources enable multiple ways to add media data to new media files, from built-in encoding to manual per-packet control.
---
# Media sources
## Introduction
+4
View File
@@ -1,3 +1,7 @@
---
description: Mediabunny can write a wide range of media output formats, including MP4, WebM, MP3, HLS, and many more.
---
# Output formats
## Introduction
+70 -1
View File
@@ -1,3 +1,7 @@
---
description: Learn about how packets carry encoded media data, samples carry raw decoded media data, and the operations you can perform on them.
---
# Packets & samples
## Introduction
@@ -270,6 +274,26 @@ const sample = new VideoSample(buffer, {
See [`VideoPixelFormat`](https://w3c.github.io/webcodecs/#enumdef-videopixelformat) for a list of pixel formats supported by WebCodecs.
#### Custom resource constructor
For advanced use cases (custom decoders, GPU-backed frames, etc.), you can back a `VideoSample` with your own implementation of [`VideoSampleResource`](../api/VideoSampleResource):
```ts
import { VideoSample, VideoSampleResource } from 'mediabunny';
class MyResource extends VideoSampleResource {
// Implement getFormat(), getCodedWidth(), getCodedHeight(),
// getSquarePixelWidth(), getSquarePixelHeight(), getColorSpace(),
// getDataPlanes(), toRgbSample(), and close().
}
const sample = new VideoSample(new MyResource(), {
timestamp: 0,
});
```
This allows you to back a `VideoSample` with your own data without having to copy it.
### Inspecting video samples
A `VideoSample` has several read-only properties:
@@ -306,9 +330,12 @@ videoSample.microsecondDuration; // => Duration in microseconds
videoSample.colorSpace; // => VideoColorSpace
videoSample.visibleRect; // Rectangle
// Encode options used when this sample is passed to an encoder
videoSample.encodeOptions; // => VideoEncoderEncodeOptions (defaults to {})
```
While all of these properties are read-only, you can use the `setTimestamp`, `setDuration` and `setRotation` methods to modify some of the metadata of the video sample.
While all of these properties are read-only, you can use the `setTimestamp`, `setDuration`, `setRotation` and `setEncodeOptions` methods to modify some of the metadata of the video sample.
::: warning
Timestamps can be [negative](#negative-timestamps).
@@ -398,6 +425,31 @@ You can pass additional options to `allocationSize` and `copyTo` to extract data
---
You can transform a `VideoSample` to resize, rotate, and/or crop it, producing a new `VideoSample`:
```ts
const transformed = await videoSample.transform({
width: 640,
height: 360,
fit: 'cover',
rotate: 90,
crop: { left: 0, top: 0, width: 1920, height: 1000 },
alpha: 'discard',
});
```
In browser environments, the transform is performed using a canvas. In non-browser environments without `OffscreenCanvas` or `HTMLCanvasElement`, this method throws unless you register a custom transformer:
```ts
import { registerVideoSampleTransformer } from 'mediabunny';
registerVideoSampleTransformer((sample, description) => {
// Return a transformed VideoSample, or null to defer to the next transformer.
});
```
The [`@mediabunny/server`](./extensions/server) extension registers such a transformer.
---
You can also clone a `VideoSample`:
```ts
const clonedSample = videoSample.clone(); // => VideoSample
@@ -459,6 +511,23 @@ Planar formats store each channel's data contiguously, while interleaved formats
![Planar vs. interleaved formats](../assets/planar_interleaved.svg)
#### Custom resource constructor
For advanced use cases (custom decoders, etc.), you can back an `AudioSample` with your own implementation of [`AudioSampleResource`](../api/AudioSampleResource):
```ts
import { AudioSample, AudioSampleResource } from 'mediabunny';
class MyResource extends AudioSampleResource {
// Implement getFormat(), getSampleRate(), getNumberOfFrames(),
// getNumberOfChannels(), getTimestamp(), getDataPlane(), and close().
}
const sample = new AudioSample(new MyResource());
```
This allows you to back an `AudioSample` with your own data without having to copy it.
### Inspecting audio samples
An `AudioSample` has several read-only properties:
+4
View File
@@ -1,3 +1,7 @@
---
description: A collection of short code snippets that showcase the most common operations that Mediabunny can do.
---
# Quick start
This page is a collection of short code snippets that showcase the most common operations you may use this library for.
+34 -1
View File
@@ -1,3 +1,7 @@
---
description: Use Mediabunny to read HLS (.m3u8) playlists, both VOD and live. Extract tracks, metadata, and media data efficiently and with precise control.
---
# Reading HLS
Mediabunny has full support for reading HLS (.m3u8) playlists, both VOD and live. This page will go into HLS-specific behavior and advice. For general information about how to read any media file (including HLS) in Mediabunny, refer to [Reading media files](./reading-media-files).
@@ -8,7 +12,7 @@ Mediabunny exposes HLS playlists as if they were a single giant input file. Like
## HLS inputs
HLS playlists (master & media) are read through the same `Input` interface as all other media files in Mediabunny. HLS must read multiple files, meaning any [`PathedSource`](../api/PathedSource) is required:
HLS playlists (master & media) are read through the same `Input` interface as all other media files in Mediabunny. HLS must read multiple files, meaning any [`PathedSource`](../api/PathedSource) (such as `UrlSource`) is required:
```ts
import { Input, UrlSource, HLS_FORMATS } from 'mediabunny';
@@ -229,6 +233,35 @@ This makes cross-track synchronization trivial. To know if a track's timestamps
await track.isRelativeToUnixEpoch(); // => boolean
```
### Disabling Unix offsets
If you don't want Mediabunny to offset packet timestamps to be in Unix time, you can set `offsetTimestampsByDateTime` to `false` in the input format options:
```ts
const input = new Input({
// ...
formatOptions: {
hls: {
offsetTimestampsByDateTime: false,
},
},
});
```
This way, track and packet timestamps behave as if no `#EXT-X-PROGRAM-DATE-TIME` tags existed. This also means that any date time gaps are completely collapsed.
You will still be able to query the Unix time metadata via a mapping function on the `InputTrack`:
```ts
const firstTimestamp = await inputTrack.getFirstTimestamp(); // => 0
await inputTrack.getUnixTimeForTimestamp(firstTimestamp); // => 1704067200 (Unix timestamp for 2024-01-01T00:00:00Z)
```
This function performs a piecewise-continuous mapping of timestamp space into Unix time space.
If no wall-clock time information is available, `getUnixTimeForTimestamp()` will return `null`. You can check the presence of Unix time metadata via:
```ts
await inputTrack.hasUnixTimeMapping(); // boolean
```
## Live HLS
HLS playlists may be live. You can check that a track is live via:
+19 -7
View File
@@ -1,3 +1,7 @@
---
description: Efficiently extract metadata, video frames, and audio samples from any media file, such as MP4 or WebM.
---
# Reading media files
Mediabunny allows you to read media files with great control and efficiency. You can use it to extract metadata (such as duration or resolution), as well as to read actual media data from video and audio tracks with frame-accurate timing. Many commonly used [input file formats](./input-formats) are supported. Using [input sources](#input-sources), data can be read from multiple sources, such as directly from memory, from the user's disk, or even over the network.
@@ -639,6 +643,10 @@ const source = new UrlSource('https://example.com/bigbuckbunny.mp4', {
});
```
::: info
All `requestInit` fields are respected except for `signal`, which is overridden by Mediabunny. The same applies to the `signal` value of a `Request` passed as the first constructor argument. To cancel ongoing requests, [dispose of the input](#disposing-inputs).
:::
`getRetryDelay` can be used to control the retry logic used should a request fail. When a request fails, `getRetryDelay` should return the time to wait in seconds before the request will be retried. Returning `null` prevents further retries.
```ts
// UrlSource using retry logic with exponential backoff:
@@ -692,18 +700,18 @@ type FilePathSourceOptions = {
When using this source, make sure to manually [dispose of the Input](#disposing-inputs) when you are done with it to properly close the internal file handle held by this source.
:::
### `StreamSource`
### `CustomSource`
This is a general-purpose input source you can use to read data from anywhere.
For example, here we're reading a file from disk using the Node.js file system (although you should use [`FilePathSource`](#filepathsource) for that):
For example, here we're reading a file from disk using the Node.js file system (although you should use the existing [`FilePathSource`](#filepathsource) for that):
```ts
import { StreamSource } from 'mediabunny';
import { CustomSource } from 'mediabunny';
import { open } from 'node:fs/promises';
const fileHandle = await open('bigbuckbunny.mp4', 'r');
const source = new StreamSource({
const source = new CustomSource({
read: async (start, end) => {
const buffer = Buffer.alloc(end - start);
await fileHandle.read(buffer, 0, end - start, start);
@@ -716,9 +724,9 @@ const source = new StreamSource({
});
```
The options of `StreamSource` have the following type:
The options of `CustomSource` have the following type:
```ts
type StreamSourceOptions = {
type CustomSourceOptions = {
getSize: () => MaybePromise<number>;
read: (start: number, end: number) => MaybePromise<Uint8Array | ReadableStream<Uint8Array>>;
dispose?: () => unknown;
@@ -732,7 +740,7 @@ type MaybePromise<T> = T | Promise<T>;
- `getSize`\
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`.
- `read`\
Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream that yields these bytes.
Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream that yields these bytes. You are guaranteed that `0 <= start < end < fileSize`.
- `dispose`\
Called when the `Input` driven by this source is disposed.
- `maxCacheSize`\
@@ -743,6 +751,10 @@ type MaybePromise<T> = T | Promise<T>;
- `'fileSystem'`: File system-optimized prefetching: a small amount of data is prefetched bidirectionally, aligned with page boundaries.
- `'network'`: Network-optimized prefetching, or more generally, prefetching optimized for any high-latency environment: tries to minimize the amount of read calls and aggressively prefetches data when sequential access patterns are detected.
::: info
`CustomSource` was previously known as `StreamSource` and is still available under that alias, but usage of `StreamSource` is deprecated.
:::
### `ReadableStreamSource`
This is a source backed by a `ReadableStream` of `Uint8Array`, representing an append-only byte stream of unknown length. This is the source to use for incrementally streaming in input files that are still being constructed and whose size we don't yet know. You could also use it to stream in existing files, but other sources (such as [`BlobSource`](#blobsource) or [`FilePathSource`](#filepathsource)) are recommended instead because they offer random access.
+15 -2
View File
@@ -1,3 +1,7 @@
---
description: Mediabunny supports a wide range of media container formats (.mp4, .webm, .mp3, .wav, .m3u8, ...) and video/audio codecs (H.264, HEVC, VP9, AV1, AAC, Opus, FLAC, ...).
---
# Supported formats & codecs
## Container formats
@@ -36,6 +40,7 @@ Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, m
- `'vp8'` - VP8
- `'vp9'` - VP9
- `'av1'` - AOMedia Video 1 (AV1)
- `'prores'` - Apple ProRes [^prores]
### Audio codecs
@@ -76,6 +81,7 @@ Not all codecs can be used with all containers. The following table specifies th
| `'vp8'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
| `'vp9'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
| `'av1'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
| `'prores'` | ✓ | ✓ | ✓ | | | | | | | |
| `'aac'` | ✓ | ✓ | ✓ | | | | | ✓ | | ✓ |
| `'opus'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | | |
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | | | | ✓ |
@@ -101,6 +107,7 @@ Not all codecs can be used with all containers. The following table specifies th
For HLS, the supported codecs depend on the segment format chosen.
[^prores]: ProRes is not supported by WebCodecs. To decode it, use the [`@mediabunny/prores`](./extensions/prores) extension package. The [`@mediabunny/server`](./extensions/server) extension package provides both decoding and encoding support for server-side environments.
[^aac]: In some browsers, AAC encoding is not supported by WebCodecs. You can polyfill it with the [`@mediabunny/aac-encoder`](./extensions/aac-encoder) extension package.
[^mp3]: MP3 encoding is not supported by WebCodecs. You can polyfill it with the [`@mediabunny/mp3-encoder`](./extensions/mp3-encoder) extension package.
[^flac]: FLAC encoding is not supported by WebCodecs. You can polyfill it with the [`@mediabunny/flac-encoder`](./extensions/flac-encoder) extension package.
@@ -289,10 +296,13 @@ class {
codec: AudioCodec;
config: AudioEncoderConfig;
onPacket: (packet: EncodedPacket, meta?: EncodedAudioChunkMetadata) => unknown;
// For both:
onError: (error: unknown) => void;
}
```
`codec` and `config` specify the concrete codec configuration to use, and `onPacket` is a method that your code **must** call for each encoded packet it creates.
`codec` and `config` specify the concrete codec configuration to use, and `onPacket` is a method that your code **must** call for each encoded packet it creates. `onError` is a method you can call to surface any out-of-band errors that occur outside of the regular method calls (such as from an asynchronous background task); these errors would otherwise go uncaught.
You **must** implement the following methods in your custom encoder class:
```ts
@@ -352,10 +362,13 @@ class {
codec: AudioCodec;
config: AudioDecoderConfig;
onSample: (sample: AudioSample) => unknown;
// For both:
onError: (error: unknown) => void;
}
```
`codec` and `config` specify the concrete codec configuration to use, and `onSample` is a method that your code **must** call for each video/audio sample it creates.
`codec` and `config` specify the concrete codec configuration to use, and `onSample` is a method that your code **must** call for each video/audio sample it creates. `onError` is a method you can call to surface any out-of-band errors that occur outside of the regular method calls (such as from an asynchronous background task); these errors would otherwise go uncaught.
You **must** implement the following methods in your custom decoder class:
```ts
+1
View File
@@ -1,4 +1,5 @@
---
description: Mediabunny can create new HLS (.m3u8) playlists directly in the browser, both VOD and live. Define tracks, add media data, and upload directly to a server.
outline: [2, 4]
---
+6 -2
View File
@@ -1,3 +1,7 @@
---
description: Use Mediabunny to create new video and audio files of any size directly in the browser, and with full control over frame timing, tracks, and more.
---
# Writing media files
Mediabunny enables you to create media files with very fine levels of control. You can add an arbitrary number of video, audio and subtitle tracks to a media file, and precisely control the timing of media data. This library supports [many output file formats](./output-formats). Using [output targets](#output-targets), you can decide if you want to build up the entire file in memory or stream it out in chunks as it's being created - allowing you to create very large files.
@@ -338,7 +342,7 @@ Each chunk written to the `WritableStream` represents a contiguous chunk of byte
::: warning
Note that some byte regions in the output file may be written to multiple times. It is therefore **incorrect** to construct the final file by simply concatenating all `Uint8Array`s together - you **must** write each chunk of data at the specified byte offset position _in the order_ in which the chunks arrived. If you don't do this, your output file will likely be invalid or corrupted.
Some [output formats](./output-formats) have *append-only* writing modes in which the byte offset of a written chunk will always be equal to the total number of bytes in all previously written chunks. In other words, when writing is append-only, simply concatening all `Uint8Array`s yields the correct result. Some APIs (like `appendBuffer` of Media Source Extensions) require this, so make sure to configure your output format accordingly for those cases.
Some [output formats](./output-formats) have *append-only* writing modes in which the byte offset of a written chunk will always be equal to the total number of bytes in all previously written chunks. In other words, when writing is append-only, simply concatenating all `Uint8Array`s yields the correct result. Some APIs (like `appendBuffer` of Media Source Extensions) require this, so make sure to configure your output format accordingly for those cases.
:::
#### Chunked mode
@@ -541,4 +545,4 @@ The promise returned by `getMimeType` only resolves once the precise codec strin
If you don't care about specific track codecs, you can instead use the simpler [`mimeType`](./output-formats#output-format-properties) property on the `Output`'s format:
```ts
output.format.mimeType; // => string
```
```
+20 -14
View File
@@ -1,7 +1,8 @@
---
layout: home
title: Mediabunny
title: Mediabunny — A complete JavaScript media toolkit for the browser
titleTemplate: false
hero:
name: Mediabunny
@@ -101,17 +102,17 @@ const sponsors = {
{ image: '/sponsors/screen-studio.webp', name: 'Screen Studio', url: 'https://screen.studio/' },
{ image: '/sponsors/tella.svg', name: 'Tella', url: 'https://www.tella.com/' },
],
silver: [
{ image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' },
],
silver: [],
bronze: [
{ image: '/sponsors/elevenlabs.png', name: 'ElevenLabs', url: 'https://elevenlabs.io/' },
{ image: '/sponsors/rve.png', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' },
{ image: '/sponsors/mux.jpg', name: 'Mux', url: 'https://www.mux.com/' },
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
{ image: '/sponsors/photocircle.png', name: 'PhotoCircle', url: 'https://www.photocircleapp.com/' },
],
individual: [
{ image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' },
{ image: 'https://avatars.githubusercontent.com/u/189556080', name: 'Cursorful', url: 'https://github.com/cursorful' },
{ image: 'https://avatars.githubusercontent.com/u/11537072', name: 'Threema', url: 'https://threema.com/en' },
{ image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' },
{ image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' },
@@ -119,8 +120,11 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/18653821', name: 'ZerGo0', url: 'https://github.com/ZerGo0' },
{ image: 'https://avatars.githubusercontent.com/u/5913254', name: 'Brandon McConnell', url: 'https://github.com/brandonmcconnell' },
{ image: 'https://avatars.githubusercontent.com/u/9549394', name: 'studnitz', url: 'https://github.com/studnitz' },
{ image: 'https://avatars.githubusercontent.com/u/67356781', name: 'Ahmed Rowaihi', url: 'https://github.com/ahmedrowaihi' },
{ image: 'https://avatars.githubusercontent.com/u/4714175', name: 'Phoomparin Mano', url: 'https://github.com/heypoom' },
{ image: 'https://avatars.githubusercontent.com/u/504909', name: 'Hirbod', url: 'https://github.com/hirbod' },
{ image: 'https://avatars.githubusercontent.com/u/2565549', name: 'MatthewNorton', url: 'https://github.com/MatthewNorton' },
{ image: 'https://avatars.githubusercontent.com/u/272247752', name: 'Motionik', url: 'https://github.com/Motionik' },
{ image: 'https://avatars.githubusercontent.com/u/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
@@ -132,6 +136,7 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/255616819', name: 'cronischarles-del', url: 'https://github.com/cronischarles-del' },
{ image: 'https://avatars.githubusercontent.com/u/37973863', name: 'Gomi', url: 'https://github.com/gxy5202' },
{ image: 'https://avatars.githubusercontent.com/u/36898190', name: 'jepcd', url: 'https://github.com/jepcd' },
{ image: 'https://avatars.githubusercontent.com/u/695166', name: 'Takenori Nakagawa', url: 'https://github.com/ww24' },
{ image: 'https://avatars.githubusercontent.com/u/63088713', name: 'taf2000', url: 'https://github.com/taf2000' },
{ image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
@@ -144,6 +149,7 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/13042781', name: 'Anshul Bansal', url: 'https://github.com/akbansa' },
{ image: 'https://avatars.githubusercontent.com/u/43105172', name: 'zhmou', url: 'https://github.com/zhmou' },
{ image: 'https://avatars.githubusercontent.com/u/695166', name: 'Takenori Nakagawa', url: 'https://github.com/ww24' },
{ image: 'https://avatars.githubusercontent.com/u/43607012', name: 'Sturlen', url: 'https://github.com/Sturlen' },
],
};
</script>
@@ -154,7 +160,7 @@ const sponsors = {
<p class="!my-0 flex-1 text-sm !leading-6">{{ quote.quote }}</p>
<div class="shrink-0 flex gap-2 items-center mt-1.5">
<a :href="quote.url" target="_blank">
<img :src="quote.image" class="size-8 rounded-full shrink-0" alt="Author image">
<img :src="quote.image" class="size-8 rounded-full shrink-0" :alt="quote.author">
</a>
<p class="!my-0 flex-1 text-xs font-medium">{{ quote.author }}</p>
</div>
@@ -172,7 +178,7 @@ npm install mediabunny
<div class="space-y-16 mt-18">
<div class="flex flex-col lg:flex-row lg:gap-20 lg:items-center">
<div class="flex-1 min-w-0">
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Read any media file, efficiently</h1>
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Read any media file, efficiently</h2>
<p class="text-lg">Mediabunny allows you efficiently read data from any video or audio file, no matter the size: duration, resolution, rotation, tracks, codecs and other metadata, as well as raw or decoded media data from anywhere in the file. Load only what you need.</p>
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/reading-media-files">
Docs
@@ -248,7 +254,7 @@ const { buffer } = output.target; // Contains the final file
</div>
<div class="flex-1 min-w-0">
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Create new media files, programmatically</h1>
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Create new media files, programmatically</h2>
<p class="text-lg">Generate new media files (such as MP4 or WebM) directly on the client, and as fast as the hardware allows. Add multiple video, audio, or subtitle tracks, and have precise control down to the microsecond.</p>
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/writing-media-files">
Docs
@@ -259,7 +265,7 @@ const { buffer } = output.target; // Contains the final file
<div class="flex flex-col lg:flex-row lg:gap-20 lg:items-center">
<div class="flex-1 min-w-0">
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Hopping-fast file conversion</h1>
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Hopping-fast file conversion</h2>
<p class="text-lg">Use the Conversion API to transform any media file into any other media file: perform transmuxing, transcoding, resizing, rotation, audio resampling, trimming, and more.</p>
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/converting-media-files">
Docs
@@ -295,14 +301,14 @@ await conversion.execute();
<img class="relative" src="./assets/inspiring-io.svg">
</div>
<div class="flex-1 min-w-0">
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Universal I/O</h1>
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Universal I/O</h2>
<p class="text-lg">Read and write files from and to memory, disk, or the network. Create files for offline use, or live-stream them as they're being created. Inject media data from a canvas, webcam, screen, microphone, audio buffer, your own encoding stack, or whatever. It's all up to you.</p>
</div>
</div>
<div class="flex flex-col lg:flex-row gap-4 lg:gap-20 lg:items-center">
<div class="flex-1 min-w-0">
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Wide container & codec support</h1>
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Wide container & codec support</h2>
<p class="text-lg">Mediabunny supports many commonly used container formats and a wide variety of video and audio codecs. And all of them bidirectionally, too! (that means for reading and writing)</p>
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/supported-formats-and-codecs">
See full list
@@ -328,14 +334,14 @@ await conversion.execute();
<p class="!my-0 text-center text-[10px] opacity-50 !leading-4">Reading BigBuckBunny1080pH264.mov (691 MiB) from disk<br>Ryzen 7600X, RTX 4070, NVMe SSD, measured 2025-06-22</p>
</div>
<div class="flex-1 min-w-0">
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">High performance</h1>
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">High performance</h2>
<p class="text-lg">By reading only what you need, writing progressively, utilizing hardware-accelerated encoding and decoding via the WebCodecs API, and using a pipelined design, Mediabunny is able to get the job done fast.</p>
</div>
</div>
<div class="flex flex-col lg:flex-row gap-4 lg:gap-20 lg:items-center">
<div class="flex-1 min-w-0">
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Built from scratch, for the web</h1>
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Built from scratch, for the web</h2>
<p class="text-lg">Mediabunny is 100% implemented in TypeScript and has zero dependencies. Its API was designed to be highly tree-shakable, meaning you only include what you use.</p>
</div>
<div class="space-y-2 flex-1 min-w-0">
@@ -368,10 +374,10 @@ await conversion.execute();
<hr class="!my-16" />
<div class="flex flex-col items-center">
<h1 id="sponsors" class="flex items-center gap-4" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">
<h2 id="sponsors" class="flex items-center gap-4 !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">
Made possible by you
<img class="size-8" src="./assets/fluent-emoji--heart-suit.svg">
</h1>
</h2>
<p class="max-w-2xl">Mediabunny is an open-source project released under the <a href="https://choosealicense.com/licenses/mpl-2.0/" target="_blank">MPL-2.0</a> and is therefore free to use for any purpose, including closed-source commercial use. A permissive license is essential for a foundational library like this to truly thrive. That said, this project requires an immense amount of work and care. This is made possible by the generous financial backing of these awesome sponsors:</p>
<template v-if="sponsors.gold.length > 0">
<h3 class="!text-3xl">Gold sponsors</h3>
+4
View File
@@ -1,3 +1,7 @@
---
description: Teach Claude Code, Codex, Cursor, Copilot or any other AI coding agent how to make full use of Mediabunny.
---
# Mediabunny and LLMs
<div class="grid place-items-center my-14">

Before

Width:  |  Height:  |  Size: 304 KiB

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+3
View File
@@ -32,6 +32,7 @@ export default tseslint.config(
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/no-unsafe-unary-minus': 'off',
'@typescript-eslint/no-deprecated': 'error',
'@typescript-eslint/consistent-type-exports': 'error',
},
},
{
@@ -46,6 +47,8 @@ export default tseslint.config(
'packages/aac-encoder/build',
'packages/flac-encoder/dist',
'packages/flac-encoder/build',
'packages/prores/dist',
'packages/server/dist',
'eslint.config.mjs',
'docs/.vitepress/cache',
'test/public',
+10 -2
View File
@@ -9,8 +9,15 @@ import {
Conversion,
QUALITY_VERY_LOW,
} from 'mediabunny';
import { registerAc3Decoder } from '@mediabunny/ac3';
import { registerProresDecoder } from '@mediabunny/prores';
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
// Enable codecs that aren't natively supported by WebCodecs.
registerAc3Decoder();
registerProresDecoder();
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement;
@@ -68,6 +75,7 @@ const compressFile = async (resource: File | string) => {
bitrate: QUALITY_VERY_LOW,
},
audio: {
codec: 'opus',
bitrate: QUALITY_VERY_LOW,
},
});
@@ -136,7 +144,7 @@ const compressFile = async (resource: File | string) => {
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac';
fileInput.addEventListener('change', () => {
const file = fileInput.files?.[0];
if (!file) {
@@ -153,7 +161,7 @@ loadUrlButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://remotion.media/BigBuckBunny.mp4',
'https://mediabunny.dev/big-buck-bunny.mp4',
);
if (!url) {
return;
+17 -2
View File
@@ -1,14 +1,29 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<html lang="en-US" translate="no">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File compression example | Mediabunny</title>
<meta name="description" content="Select or drop a media file, and Mediabunny will convert it to a heavily-compressed MP4 file.">
<script type="module" src="../base.ts"></script>
<script type="module" src="./file-compression.ts"></script>
<link rel="stylesheet" href="../base.css">
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
<link rel="canonical" href="https://mediabunny.dev/examples/file-compression/">
<meta property="og:site_name" content="Mediabunny">
<meta property="og:type" content="website">
<meta property="og:title" content="File compression example | Mediabunny">
<meta property="og:description" content="Select or drop a media file, and Mediabunny will convert it to a heavily-compressed MP4 file.">
<meta property="og:url" content="https://mediabunny.dev/examples/file-compression/">
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<meta property="og:locale" content="en-US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@vanilagy">
<meta name="twitter:title" content="File compression example | Mediabunny">
<meta name="twitter:description" content="Select or drop a media file, and Mediabunny will convert it to a heavily-compressed MP4 file.">
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"File compression"}]}</script>
</head>
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
+9 -2
View File
@@ -15,8 +15,15 @@ import {
QUALITY_LOW,
QUALITY_VERY_LOW,
} from 'mediabunny';
import { registerAc3Decoder } from '@mediabunny/ac3';
import { registerProresDecoder } from '@mediabunny/prores';
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
// Enable codecs that aren't natively supported by WebCodecs.
registerAc3Decoder();
registerProresDecoder();
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
declare global {
@@ -233,7 +240,7 @@ selectDirectoryButton.addEventListener('click', async () => {
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts';
fileInput.addEventListener('change', () => {
const file = fileInput.files![0];
if (file) {
@@ -248,7 +255,7 @@ loadSampleButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://remotion.media/BigBuckBunny.mp4',
'https://mediabunny.dev/big-buck-bunny.mp4',
);
if (!url) {
return;
+17 -2
View File
@@ -1,14 +1,29 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<html lang="en-US" translate="no">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HLS transcoding example | Mediabunny</title>
<meta name="description" content="Select a directory, then a video. Mediabunny will create a complete HLS VOD package with five video renditions and one audio track.">
<script type="module" src="../base.ts"></script>
<script type="module" src="./hls-transcoding.ts"></script>
<link rel="stylesheet" href="../base.css">
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
<link rel="canonical" href="https://mediabunny.dev/examples/hls-transcoding/">
<meta property="og:site_name" content="Mediabunny">
<meta property="og:type" content="website">
<meta property="og:title" content="HLS transcoding example | Mediabunny">
<meta property="og:description" content="Select a directory, then a video. Mediabunny will create a complete HLS VOD package with five video renditions and one audio track.">
<meta property="og:url" content="https://mediabunny.dev/examples/hls-transcoding/">
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<meta property="og:locale" content="en-US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@vanilagy">
<meta name="twitter:title" content="HLS transcoding example | Mediabunny">
<meta name="twitter:description" content="Select a directory, then a video. Mediabunny will create a complete HLS VOD package with five video renditions and one audio track.">
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"HLS transcoding"}]}</script>
</head>
<body class="flex flex-col items-center bg-zinc-50 px-2 py-10 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200">
+17 -2
View File
@@ -1,14 +1,29 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<html lang="en-US" translate="no">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live recording & streaming example | Mediabunny</title>
<meta name="description" content="The live canvas state and your microphone input will be written into a fragmented MP4 file and live-streamed to a &lt;video&gt; element.">
<script type="module" src="../base.ts"></script>
<script type="module" src="./live-recording.ts"></script>
<link rel="stylesheet" href="../base.css">
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
<link rel="canonical" href="https://mediabunny.dev/examples/live-recording/">
<meta property="og:site_name" content="Mediabunny">
<meta property="og:type" content="website">
<meta property="og:title" content="Live recording & streaming example | Mediabunny">
<meta property="og:description" content="The live canvas state and your microphone input will be written into a fragmented MP4 file and live-streamed to a &lt;video&gt; element.">
<meta property="og:url" content="https://mediabunny.dev/examples/live-recording/">
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<meta property="og:locale" content="en-US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@vanilagy">
<meta name="twitter:title" content="Live recording & streaming example | Mediabunny">
<meta name="twitter:description" content="The live canvas state and your microphone input will be written into a fragmented MP4 file and live-streamed to a &lt;video&gt; element.">
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Live recording & streaming"}]}</script>
</head>
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
+17 -2
View File
@@ -1,14 +1,29 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<html lang="en-US" translate="no">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Media player example | Mediabunny</title>
<meta name="description" content="Select or drop a media file, and a fully custom, Mediabunny-powered player will appear.">
<script type="module" src="../base.ts"></script>
<script type="module" src="./media-player.ts"></script>
<link rel="stylesheet" href="../base.css">
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
<link rel="canonical" href="https://mediabunny.dev/examples/media-player/">
<meta property="og:site_name" content="Mediabunny">
<meta property="og:type" content="website">
<meta property="og:title" content="Media player example | Mediabunny">
<meta property="og:description" content="Select or drop a media file, and a fully custom, Mediabunny-powered player will appear.">
<meta property="og:url" content="https://mediabunny.dev/examples/media-player/">
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<meta property="og:locale" content="en-US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@vanilagy">
<meta name="twitter:title" content="Media player example | Mediabunny">
<meta name="twitter:description" content="Select or drop a media file, and a fully custom, Mediabunny-powered player will appear.">
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Media player"}]}</script>
</head>
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2 h-svh">
+10 -3
View File
@@ -8,8 +8,15 @@ import {
WrappedAudioBuffer,
WrappedCanvas,
} from 'mediabunny';
import { registerAc3Decoder } from '@mediabunny/ac3';
import { registerProresDecoder } from '@mediabunny/prores';
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
// Enable codecs that aren't natively supported by WebCodecs.
registerAc3Decoder();
registerProresDecoder();
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement;
@@ -215,7 +222,7 @@ const initMediaPlayer = async (resource: File | string) => {
if (audioContext.state === 'running') {
// Start playback automatically if the audio context permits
await play();
// await play();
}
loadingElement.style.display = 'none';
@@ -732,7 +739,7 @@ window.addEventListener('resize', () => {
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac';
fileInput.addEventListener('change', () => {
const file = fileInput.files?.[0];
if (!file) {
@@ -749,7 +756,7 @@ loadUrlButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://remotion.media/BigBuckBunny.mp4',
'https://mediabunny.dev/big-buck-bunny.mp4',
);
if (!url) {
return;
+17 -2
View File
@@ -1,14 +1,29 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<html lang="en-US" translate="no">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Metadata extraction example | Mediabunny</title>
<meta name="description" content="Select or drop a media file, and Mediabunny will start extracting various metadata about that file.">
<script type="module" src="../base.ts"></script>
<script type="module" src="./metadata-extraction.ts"></script>
<link rel="stylesheet" href="../base.css">
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
<link rel="canonical" href="https://mediabunny.dev/examples/metadata-extraction/">
<meta property="og:site_name" content="Mediabunny">
<meta property="og:type" content="website">
<meta property="og:title" content="Metadata extraction example | Mediabunny">
<meta property="og:description" content="Select or drop a media file, and Mediabunny will start extracting various metadata about that file.">
<meta property="og:url" content="https://mediabunny.dev/examples/metadata-extraction/">
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<meta property="og:locale" content="en-US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@vanilagy">
<meta name="twitter:title" content="Metadata extraction example | Mediabunny">
<meta name="twitter:description" content="Select or drop a media file, and Mediabunny will start extracting various metadata about that file.">
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Metadata extraction"}]}</script>
</head>
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
@@ -238,7 +238,7 @@ loadUrlButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://remotion.media/BigBuckBunny.mp4',
'https://mediabunny.dev/big-buck-bunny.mp4',
);
if (!url) {
return;
+17 -2
View File
@@ -1,14 +1,29 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<html lang="en-US" translate="no">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procedural Generation example | Mediabunny</title>
<meta name="description" content="Using Mediabunny, this page will procedurally generate a video of musical bouncing balls as fast as possible.">
<script type="module" src="../base.ts"></script>
<script type="module" src="./procedural-generation.ts"></script>
<link rel="stylesheet" href="../base.css">
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
<link rel="canonical" href="https://mediabunny.dev/examples/procedural-generation/">
<meta property="og:site_name" content="Mediabunny">
<meta property="og:type" content="website">
<meta property="og:title" content="Procedural Generation example | Mediabunny">
<meta property="og:description" content="Using Mediabunny, this page will procedurally generate a video of musical bouncing balls as fast as possible.">
<meta property="og:url" content="https://mediabunny.dev/examples/procedural-generation/">
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<meta property="og:locale" content="en-US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@vanilagy">
<meta name="twitter:title" content="Procedural Generation example | Mediabunny">
<meta name="twitter:description" content="Using Mediabunny, this page will procedurally generate a video of musical bouncing balls as fast as possible.">
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Procedural generation"}]}</script>
</head>
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
+17 -2
View File
@@ -1,14 +1,29 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<html lang="en-US" translate="no">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thumbnail generation example | Mediabunny</title>
<meta name="description" content="Select or drop a media file, and Mediabunny will extract video thumbnails for it.">
<script type="module" src="./../base.ts"></script>
<script type="module" src="./thumbnail-generation.ts"></script>
<link rel="stylesheet" href="../base.css">
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
<link rel="canonical" href="https://mediabunny.dev/examples/thumbnail-generation/">
<meta property="og:site_name" content="Mediabunny">
<meta property="og:type" content="website">
<meta property="og:title" content="Thumbnail generation example | Mediabunny">
<meta property="og:description" content="Select or drop a media file, and Mediabunny will extract video thumbnails for it.">
<meta property="og:url" content="https://mediabunny.dev/examples/thumbnail-generation/">
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<meta property="og:locale" content="en-US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@vanilagy">
<meta name="twitter:title" content="Thumbnail generation example | Mediabunny">
<meta name="twitter:description" content="Select or drop a media file, and Mediabunny will extract video thumbnails for it.">
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Thumbnail generation"}]}</script>
</head>
<body class="flex flex-col items-center py-10 bg-gray-50 text-gray-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
@@ -1,6 +1,13 @@
import { Input, ALL_FORMATS, BlobSource, UrlSource, CanvasSink } from 'mediabunny';
import { registerAc3Decoder } from '@mediabunny/ac3';
import { registerProresDecoder } from '@mediabunny/prores';
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
// Enable codecs that aren't natively supported by WebCodecs.
registerAc3Decoder();
registerProresDecoder();
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement;
@@ -122,7 +129,7 @@ const generateThumbnails = async (resource: File | string) => {
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac';
fileInput.addEventListener('change', () => {
const file = fileInput.files?.[0];
if (!file) {
@@ -139,7 +146,7 @@ loadUrlButton.addEventListener('click', () => {
const url = prompt(
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
+ ' right CORS headers set.',
'https://remotion.media/BigBuckBunny.mp4',
'https://mediabunny.dev/big-buck-bunny.mp4',
);
if (!url) {
return;
+949 -41
View File
File diff suppressed because it is too large Load Diff
+20 -10
View File
@@ -1,19 +1,29 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.42.0-beta.10",
"version": "1.50.9",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
".",
"packages/*"
],
"main": "./dist/bundles/mediabunny.node.cjs",
"main": "./dist/bundles/mediabunny.cjs",
"module": "./dist/modules/src/index.js",
"types": "./dist/modules/src/index.d.ts",
"exports": {
"types": "./dist/modules/src/index.d.ts",
"import": "./dist/modules/src/index.js",
"require": "./dist/bundles/mediabunny.node.cjs"
".": {
"types": "./dist/modules/src/index.d.ts",
"browser": {
"import": "./dist/modules/src/index.js",
"require": "./dist/bundles/mediabunny.cjs"
},
"node": {
"import": "./dist/modules/src/index.js",
"require": "./dist/bundles/mediabunny.node.cjs"
},
"default": "./dist/modules/src/index.js"
}
},
"files": [
"README.md",
@@ -41,15 +51,15 @@
"docs:dev": "vitepress dev docs",
"docs:build": "npm run build && npm run docs:generate && vitepress build docs && npm run examples:build && cp dist/mediabunny.d.ts dist-docs/",
"docs:preview": "vitepress preview docs",
"docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts packages/aac-encoder/src/index.ts packages/flac-encoder/src/index.ts docs/api-config.json",
"docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts packages/aac-encoder/src/index.ts packages/flac-encoder/src/index.ts packages/prores/src/index.ts packages/server/src/index.ts docs/api-config.json",
"dev": "vite",
"examples:build": "vite build",
"fix-build-import-paths": "tsx scripts/add-import-extensions.ts",
"append-namespace": "echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts",
"bump-patch": "npm version patch --no-git-tag-version --workspaces --include-workspace-root",
"bump-minor": "npm version minor --no-git-tag-version --workspaces --include-workspace-root",
"bump-major": "npm version major --no-git-tag-version --workspaces --include-workspace-root",
"set-version": "npm version --no-git-tag-version --workspaces --include-workspace-root"
"bump-patch": "npm version patch --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only",
"bump-minor": "npm version minor --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only",
"bump-major": "npm version major --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only",
"set-version": "npm version --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only"
},
"license": "MPL-2.0",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/aac-encoder",
"author": "Vanilagy",
"version": "1.42.0-beta.10",
"version": "1.50.9",
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
@@ -192,3 +192,7 @@ if (parentPort) {
} else {
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
}
// Prevents the worker for being randomly closed by Firefox
// https://github.com/Vanilagy/mediabunny/issues/435
setInterval(() => {}, 1000);
+7
View File
@@ -276,6 +276,8 @@ class AacEncoder extends CustomAudioEncoder {
}
}
let registered = false;
/**
* Registers the AAC encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
* function before starting any encoding task.
@@ -295,6 +297,11 @@ class AacEncoder extends CustomAudioEncoder {
* @public
*/
export const registerAacEncoder = () => {
if (registered) {
return;
}
registered = true;
registerEncoder(AacEncoder);
};
+3 -1
View File
@@ -6,9 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Logging } from 'mediabunny';
const AAC_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/aac-encoder loaded');
if ((globalThis as Record<symbol, unknown>)[AAC_ENCODER_LOADED_SYMBOL]) {
console.error(
Logging._error(
'[WARNING]\n@mediabunny/aac-encoder was loaded twice.'
+ ' This will likely cause the encoder not to work correctly.'
+ ' Check if multiple dependencies are importing different versions of @mediabunny/aac-encoder,'
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/ac3",
"author": "Vanilagy",
"version": "1.42.0-beta.10",
"version": "1.50.9",
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-ac3.mjs",
"module": "./dist/bundles/mediabunny-ac3.mjs",
+4
View File
@@ -304,3 +304,7 @@ if (parentPort) {
} else {
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
}
// Prevents the worker for being randomly closed by Firefox
// https://github.com/Vanilagy/mediabunny/issues/435
setInterval(() => {}, 1000);
+7
View File
@@ -61,6 +61,8 @@ class Ac3Decoder extends CustomAudioDecoder {
}
}
let registered = false;
/**
* Registers AC-3 and E-AC-3 decoders, which Mediabunny will then use automatically when applicable. Make sure to call
* this function before starting any decoding task.
@@ -69,5 +71,10 @@ class Ac3Decoder extends CustomAudioDecoder {
* @public
*/
export const registerAc3Decoder = () => {
if (registered) {
return;
}
registered = true;
registerDecoder(Ac3Decoder);
};
+7
View File
@@ -183,6 +183,8 @@ class Ac3Encoder extends CustomAudioEncoder {
}
}
let registered = false;
/**
* Registers AC-3 and E-AC-3 encoders, which Mediabunny will then use automatically when applicable. Make sure to call
* this function before starting any encoding task.
@@ -191,5 +193,10 @@ class Ac3Encoder extends CustomAudioEncoder {
* @public
*/
export const registerAc3Encoder = () => {
if (registered) {
return;
}
registered = true;
registerEncoder(Ac3Encoder);
};
+3 -1
View File
@@ -6,9 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Logging } from 'mediabunny';
const AC3_LOADED_SYMBOL = Symbol.for('@mediabunny/ac3 loaded');
if ((globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL]) {
console.error(
Logging._error(
'[WARNING]\n@mediabunny/ac3 was loaded twice.'
+ ' This will likely cause the encoder/decoder not to work correctly.'
+ ' Check if multiple dependencies are importing different versions of @mediabunny/ac3,'
+1
View File
@@ -46,6 +46,7 @@ export const unrefWorker = async () => {
if (worker) {
if (worker.unref) {
worker.unref(); // If we don't do this, then the Node process never terminates by itself
// Keep the worker around tho
} else if (typeof window === 'undefined') {
// Non-browser environment without unref - terminate instead
worker.terminate();
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/flac-encoder",
"author": "Vanilagy",
"version": "1.42.0-beta.10",
"version": "1.50.9",
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
+9 -16
View File
@@ -12,7 +12,6 @@
#include <stdlib.h>
#include <string.h>
#define BITS_PER_SAMPLE 16
#define COMPRESSION_LEVEL 5
typedef struct {
@@ -23,14 +22,10 @@ typedef struct {
typedef struct {
FLAC__StreamEncoder *encoder;
// Input buffer for interleaved int16 samples from JS
int16_t *input_buffer;
// Input buffer for interleaved int32 samples from JS
FLAC__int32 *input_buffer;
int input_buffer_size;
// Widened to int32 for libFLAC
FLAC__int32 *int32_buffer;
int int32_buffer_size;
// Contiguous output buffer for encoded frame data
uint8_t *output_buffer;
int output_size;
@@ -48,6 +43,7 @@ typedef struct {
bool header_done;
int channels;
int bits_per_sample;
} EncoderContext;
static void ensure_output_capacity(EncoderContext *ctx, int needed) {
@@ -120,13 +116,14 @@ static void reset_output(EncoderContext *ctx) {
}
EMSCRIPTEN_KEEPALIVE
int init_encoder(int channels, int sample_rate) {
int init_encoder(int channels, int sample_rate, int bits_per_sample) {
EncoderContext *ctx = calloc(1, sizeof(EncoderContext));
if (!ctx) {
return 0;
}
ctx->channels = channels;
ctx->bits_per_sample = bits_per_sample;
ctx->encoder = FLAC__stream_encoder_new();
if (!ctx->encoder) {
@@ -136,7 +133,7 @@ int init_encoder(int channels, int sample_rate) {
FLAC__stream_encoder_set_channels(ctx->encoder, channels);
FLAC__stream_encoder_set_sample_rate(ctx->encoder, sample_rate);
FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, BITS_PER_SAMPLE);
FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, bits_per_sample);
FLAC__stream_encoder_set_compression_level(ctx->encoder, COMPRESSION_LEVEL);
FLAC__stream_encoder_set_verify(ctx->encoder, false);
@@ -174,19 +171,15 @@ EMSCRIPTEN_KEEPALIVE
int send_samples(int ctx_ptr, int num_samples) {
EncoderContext *ctx = (EncoderContext *)ctx_ptr;
// Widen int16 to int32 for libFLAC
int total = num_samples * ctx->channels;
if (total > ctx->int32_buffer_size) {
ctx->int32_buffer = realloc(ctx->int32_buffer, total * sizeof(FLAC__int32));
ctx->int32_buffer_size = total;
}
int shift = 32 - ctx->bits_per_sample;
for (int i = 0; i < total; i++) {
ctx->int32_buffer[i] = ctx->input_buffer[i];
ctx->input_buffer[i] >>= shift;
}
reset_output(ctx);
FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->int32_buffer, num_samples);
FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->input_buffer, num_samples);
return ok ? 0 : -1;
}
+9 -4
View File
@@ -16,7 +16,7 @@ type ExtendedEmscriptenModule = EmscriptenModule & {
let module: ExtendedEmscriptenModule;
let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
let initEncoderFn: (channels: number, sampleRate: number) => number;
let initEncoderFn: (channels: number, sampleRate: number, bitsPerSample: number) => number;
let getEncodeInputPtr: (ctx: number, size: number) => number;
let sendSamplesFn: (ctx: number, numSamples: number) => number;
let getOutputData: (ctx: number) => number;
@@ -37,7 +37,7 @@ const ensureModule = async () => {
module = await modulePromise;
modulePromise = null;
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number']);
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number']);
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
sendSamplesFn = module.cwrap('send_samples', 'number', ['number', 'number']);
getOutputData = module.cwrap('get_output_data', 'number', ['number']);
@@ -50,10 +50,10 @@ const ensureModule = async () => {
}
};
const initEncoder = async (numberOfChannels: number, sampleRate: number) => {
const initEncoder = async (numberOfChannels: number, sampleRate: number, bitsPerSample: 16 | 24) => {
await ensureModule();
const ctx = initEncoderFn(numberOfChannels, sampleRate);
const ctx = initEncoderFn(numberOfChannels, sampleRate, bitsPerSample);
if (ctx === 0) {
throw new Error('Failed to initialize FLAC encoder.');
}
@@ -121,6 +121,7 @@ const onMessage = (data: { id: number; command: WorkerCommand }) => {
const { ctx, header } = await initEncoder(
command.data.numberOfChannels,
command.data.sampleRate,
command.data.bitsPerSample,
);
result = { type: command.type, ctx, header };
transferables.push(header);
@@ -191,3 +192,7 @@ if (parentPort) {
} else {
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
}
// Prevents the worker for being randomly closed by Firefox
// https://github.com/Vanilagy/mediabunny/issues/435
setInterval(() => {}, 1000);
+58 -19
View File
@@ -29,7 +29,7 @@ class FlacEncoder extends CustomAudioEncoder {
reject: (reason?: unknown) => void;
}>();
private ctx = 0;
private ctx: number | null = null;
private chunkMetadata: EncodedAudioChunkMetadata = {};
private description: Uint8Array | null = null;
private nextTimestampInSamples: number | null = null;
@@ -65,19 +65,6 @@ class FlacEncoder extends CustomAudioEncoder {
};
nodeWorker.on('message', onMessage);
}
const result = await this.sendCommand({
type: 'init',
data: {
numberOfChannels: this.config.numberOfChannels,
sampleRate: this.config.sampleRate,
},
});
this.ctx = result.ctx;
this.description = new Uint8Array(result.header);
this.resetInternalState();
}
private resetInternalState() {
@@ -94,15 +81,50 @@ class FlacEncoder extends CustomAudioEncoder {
}
async encode(audioSample: AudioSample) {
if (this.ctx === null) {
// This is the first sample, let's do some init
let bitsPerSample: 16 | 24;
switch (audioSample.format) {
case 'u8':
case 'u8-planar':
case 's16':
case 's16-planar':
bitsPerSample = 16;
break;
case 's32':
case 's32-planar':
case 'f32':
case 'f32-planar':
bitsPerSample = 24;
break;
default:
assertNever(audioSample.format);
assert(false);
}
const result = await this.sendCommand({
type: 'init',
data: {
numberOfChannels: this.config.numberOfChannels,
sampleRate: this.config.sampleRate,
bitsPerSample,
},
});
this.ctx = result.ctx;
this.description = new Uint8Array(result.header);
this.resetInternalState();
}
if (this.nextTimestampInSamples === null) {
this.nextTimestampInSamples = Math.round(audioSample.timestamp * this.config.sampleRate);
}
const totalBytes = audioSample.allocationSize({ format: 's16', planeIndex: 0 });
const audioBytes = new Uint8Array(totalBytes);
audioSample.copyTo(audioBytes, { format: 's16', planeIndex: 0 });
const totalBytes = audioSample.allocationSize({ format: 's32', planeIndex: 0 });
const audioData = new ArrayBuffer(totalBytes);
audioSample.copyTo(audioData, { format: 's32', planeIndex: 0 });
const audioData = audioBytes.buffer;
const result = await this.sendCommand({
type: 'encode',
data: {
@@ -116,6 +138,10 @@ class FlacEncoder extends CustomAudioEncoder {
}
async flush() {
if (this.ctx === null) {
return;
}
const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } });
this.emitPackets(result.packets);
@@ -171,9 +197,12 @@ class FlacEncoder extends CustomAudioEncoder {
}
}
let registered = false;
/**
* Registers the FLAC encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
* function before starting any encoding task.
* function before starting any encoding task. The FLAC encoder will automatically determine the output bit depth
* (16 or 24) based on the sample format of incoming `AudioSample` instances.
*
* Preferably, wrap the call in a condition to avoid overriding any native FLAC encoder:
*
@@ -190,6 +219,11 @@ class FlacEncoder extends CustomAudioEncoder {
* @public
*/
export const registerFlacEncoder = () => {
if (registered) {
return;
}
registered = true;
registerEncoder(FlacEncoder);
};
@@ -198,3 +232,8 @@ function assert(x: unknown): asserts x {
throw new Error('Assertion failed.');
}
}
export const assertNever = (x: never) => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unexpected value: ${x}`);
};
+3 -1
View File
@@ -6,9 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Logging } from 'mediabunny';
const FLAC_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/flac-encoder loaded');
if ((globalThis as Record<symbol, unknown>)[FLAC_ENCODER_LOADED_SYMBOL]) {
console.error(
Logging._error(
'[WARNING]\n@mediabunny/flac-encoder was loaded twice.'
+ ' This will likely cause the encoder not to work correctly.'
+ ' Check if multiple dependencies are importing different versions of @mediabunny/flac-encoder,'
+1
View File
@@ -16,6 +16,7 @@ export type WorkerCommand = {
data: {
numberOfChannels: number;
sampleRate: number;
bitsPerSample: 16 | 24;
};
} | {
type: 'encode';
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.42.0-beta.10",
"version": "1.50.9",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
@@ -219,3 +219,7 @@ if (parentPort) {
} else {
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
}
// Prevents the worker for being randomly closed by Firefox
// https://github.com/Vanilagy/mediabunny/issues/435
setInterval(() => {}, 1000);
+11 -4
View File
@@ -6,15 +6,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, registerEncoder } from 'mediabunny';
import { FRAME_HEADER_SIZE, readMp3FrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, Logging, registerEncoder } from 'mediabunny';
import { MP3_FRAME_HEADER_SIZE, readMp3FrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
import createWorker from './encode.worker';
const MP3_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/mp3-encoder loaded');
if ((globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL]) {
console.error(
Logging._error(
'[WARNING]\n@mediabunny/mp3-encoder was loaded twice.'
+ ' This will likely cause the encoder not to work correctly.'
+ ' Check if multiple dependencies are importing different versions of @mediabunny/mp3-encoder,'
@@ -160,7 +160,7 @@ class Mp3Encoder extends CustomAudioEncoder {
this.currentBufferOffset = requiredBufferSize;
let pos = 0;
while (pos <= this.currentBufferOffset - FRAME_HEADER_SIZE) {
while (pos <= this.currentBufferOffset - MP3_FRAME_HEADER_SIZE) {
const word = new DataView(this.buffer.buffer).getUint32(pos, false);
const header = readMp3FrameHeader(word, null).header;
if (!header) {
@@ -212,6 +212,8 @@ class Mp3Encoder extends CustomAudioEncoder {
}
}
let registered = false;
/**
* Registers the LAME MP3 encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
* function before starting any encoding task.
@@ -231,6 +233,11 @@ class Mp3Encoder extends CustomAudioEncoder {
* @public
*/
export const registerMp3Encoder = () => {
if (registered) {
return;
}
registered = true;
registerEncoder(Mp3Encoder);
};
+373
View File
@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+63
View File
@@ -0,0 +1,63 @@
# @mediabunny/prores
[![](https://img.shields.io/npm/v/@mediabunny/prores)](https://www.npmjs.com/package/@mediabunny/prores)
[![](https://img.shields.io/bundlephobia/minzip/@mediabunny/prores)](https://bundlephobia.com/package/@mediabunny/prores)
[![](https://img.shields.io/npm/dm/@mediabunny/prores)](https://www.npmjs.com/package/@mediabunny/prores)
[![](https://img.shields.io/discord/1390044844285497344?logo=discord&label=Discord)](https://discord.gg/hmpkyYuS4U)
<div align="center">
<img src="../../docs/public/mediabunny-logo.svg" width="180" height="180">
</div>
Browsers have no support for Apple ProRes in their WebCodecs implementations. This extension package provides a decoder for use with [Mediabunny](https://github.com/Vanilagy/mediabunny), allowing you to decode ProRes directly in the browser at unprecedented speed. It is implemented using Mediabunny's [custom coder API](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) and uses [TurboRes](https://github.com/Vanilagy/turbores), an extremely fast WASM-based ProRes decoder, under the hood.
> This package, like the rest of Mediabunny, is enabled by its [sponsors](https://mediabunny.dev/#sponsors) and their donations. If you've derived value from this package, please consider [leaving a donation](https://github.com/sponsors/Vanilagy)! 💘
## Installation
This library peer-depends on Mediabunny. Install both using npm:
```bash
npm install mediabunny @mediabunny/prores
```
Alternatively, directly include them using a script tag:
```html
<script src="mediabunny.js"></script>
<script src="mediabunny-prores.js"></script>
```
This will expose the global objects `Mediabunny` and `MediabunnyProres`. Use `mediabunny-prores.d.ts` to provide types for these globals. You can download the built distribution files from the [releases page](https://github.com/Vanilagy/mediabunny/releases).
## Setup
`@mediabunny/prores` can make use of shared-memory multithreading to achieve maximum performance. To enable this in browsers, your website must be cross-origin isolated by setting the following response headers:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
Alternatively, you can use:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
```
which is generally more permissive but is not supported in Safari (of course).
---
If you cannot enable cross-origin isolation, `@mediabunny/prores` will fall back to a slower multithreading algorithm.
## Usage
```ts
import { registerProresDecoder } from '@mediabunny/prores';
registerProresDecoder();
```
That's it - Mediabunny now uses the registered ProRes decoder automatically.
For all the ways of using Mediabunny, refer to its [guide](https://mediabunny.dev/guide/introduction).
## Building and development
The complete JavaScript package can be built alongside the rest of Mediabunny by running `npm run build` in Mediabunny's root.
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "dist/modules/src/index.d.ts",
"bundledPackages": [],
"compiler": {},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "dist/mediabunny-prores.d.ts"
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
}
}
},
"newlineKind": "lf"
}
+54
View File
@@ -0,0 +1,54 @@
{
"name": "@mediabunny/prores",
"author": "Vanilagy",
"version": "1.50.9",
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
"main": "./dist/bundles/mediabunny-prores.mjs",
"module": "./dist/bundles/mediabunny-prores.mjs",
"types": "./dist/modules/src/index.d.ts",
"exports": {
"types": "./dist/modules/src/index.d.ts",
"import": "./dist/bundles/mediabunny-prores.mjs",
"require": "./dist/bundles/mediabunny-prores.mjs"
},
"files": [
"README.md",
"package.json",
"LICENSE",
"dist",
"src"
],
"sideEffects": false,
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/Vanilagy/mediabunny.git",
"directory": "packages/prores"
},
"bugs": {
"url": "https://github.com/Vanilagy/mediabunny/issues"
},
"homepage": "https://mediabunny.dev/guide/extensions/prores",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/Vanilagy"
},
"dependencies": {
"turbores": "^1.2.2"
},
"peerDependencies": {
"mediabunny": "^1.49.0"
},
"keywords": [
"prores",
"apple-prores",
"decoding",
"codec",
"mediabunny",
"turbores",
"video",
"browser",
"wasm",
"polyfill"
]
}
+252
View File
@@ -0,0 +1,252 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { CustomVideoDecoder, EncodedPacket, Logging, registerDecoder, VideoCodec, VideoSample } from 'mediabunny';
import { Decoder, Frame, PixelFormat, PIXEL_FORMATS, FilledFrame } from 'turbores';
import {
assert,
isWebKit,
} from '../../../src/misc';
import { type ProresFourCc } from '../../../src/codec';
const PRORES_LOADED_SYMBOL = Symbol.for('@mediabunny/prores loaded');
if ((globalThis as Record<symbol, unknown>)[PRORES_LOADED_SYMBOL]) {
Logging._error(
'[WARNING]\n@mediabunny/prores was loaded twice.'
+ ' This will likely cause the decoder not to work correctly.'
+ ' Check if multiple dependencies are importing different versions of @mediabunny/prores,'
+ ' or if something is being bundled incorrectly.',
);
}
(globalThis as Record<symbol, unknown>)[PRORES_LOADED_SYMBOL] = true;
class ProresDecoder extends CustomVideoDecoder {
private decoder: Decoder | null = null;
private framePool: Frame[] = [];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static override supports(codec: VideoCodec, config: VideoDecoderConfig): boolean {
return codec === 'prores';
}
/** @internal */
static _supportedVideoFrameFormats: PixelFormat[] | null = null;
/** @internal */
static _determineSupportedVideoFrameFormats() {
const result: PixelFormat[] = [];
const data = new Uint8Array(32);
for (const format of PIXEL_FORMATS) {
try {
const frame = new VideoFrame(data, {
format: format as VideoPixelFormat,
codedWidth: 2,
codedHeight: 2,
timestamp: 0,
duration: 0,
});
frame.close();
result.push(format);
} catch {
// Format is not supported
}
}
return result;
}
async init() {
if (typeof VideoFrame !== 'undefined') {
// Not all VideoFrame implementations support all pixel formats, therefore let's determine the supported set
ProresDecoder._supportedVideoFrameFormats ??= ProresDecoder._determineSupportedVideoFrameFormats();
}
const decoder = await Decoder.create({
proresFourCc: this.config.codec as ProresFourCc,
useSharedMemory: Decoder.canUseSharedMemory(),
allowedOutputFormats: ProresDecoder._supportedVideoFrameFormats ?? undefined,
});
if (decoder instanceof Error) {
throw decoder;
}
this.decoder = decoder;
}
async decode(packet: EncodedPacket) {
assert(this.decoder);
while (this.decoder.desiredSize <= 0) {
await this.decoder.dequeued;
}
void this.runDecode(packet)
.catch(error => this.onError(error));
}
private async runDecode(packet: EncodedPacket) {
assert(this.decoder);
let frame: Frame;
if (this.framePool.length > 0) {
frame = this.framePool.shift()!;
} else {
frame = new Frame();
}
const result = await this.decoder.decode(packet.data, frame);
this.framePool.push(frame);
if (result instanceof Error) {
throw result;
}
if (result.visibleHeight < result.codedHeight && isWebKit()) {
// WebKit has (had) a bug with displaying height-trimmed YUV frames, so we must compact the frame data a
// little https://bugs.webkit.org/show_bug.cgi?id=317524
this.trimCodedHeightToVisibleHeight(result);
}
const colorSpaceInit = {
primaries: result.colorPrimariesString as VideoColorPrimaries | undefined,
matrix: result.colorMatrixString as VideoMatrixCoefficients | undefined,
transfer: result.colorTransferString as VideoTransferCharacteristics | undefined,
fullRange: result.colorRangeFull,
};
let displayWidth = result.visibleWidth;
let displayHeight = result.visibleHeight;
if (result.pixelAspectRatio.num > result.pixelAspectRatio.den) {
displayWidth = Math.round(
result.visibleWidth * result.pixelAspectRatio.num / result.pixelAspectRatio.den,
);
} else {
displayHeight = Math.round(
result.visibleHeight * result.pixelAspectRatio.den / result.pixelAspectRatio.num,
);
}
let sample: VideoSample;
if (typeof VideoFrame !== 'undefined') {
// Create a VideoFrame directly; this avoids the frame data being copied twice
const frame = new VideoFrame(result.frameData, {
format: result.pixelFormat as VideoPixelFormat,
codedWidth: result.codedWidth,
codedHeight: result.codedHeight,
displayWidth,
displayHeight,
visibleRect: {
x: 0,
y: 0,
width: result.visibleWidth,
height: result.visibleHeight,
},
timestamp: packet.microsecondTimestamp,
duration: packet.microsecondDuration,
colorSpace: colorSpaceInit,
});
sample = new VideoSample(frame, {
timestamp: packet.timestamp,
duration: packet.duration,
});
} else {
sample = new VideoSample(result.frameData, {
format: result.pixelFormat,
codedWidth: result.codedWidth,
codedHeight: result.codedHeight,
displayWidth,
displayHeight,
visibleRect: {
left: 0,
top: 0,
width: result.visibleWidth,
height: result.visibleHeight,
},
timestamp: packet.timestamp,
duration: packet.duration,
colorSpace: colorSpaceInit,
});
}
this.onSample(sample);
}
private trimCodedHeightToVisibleHeight(result: FilledFrame) {
const bytesPerSample = result.pixelFormat.includes('P') ? 2 : 1;
const subWidth = result.pixelFormat.includes('444') ? 1 : 2;
const subHeight = result.pixelFormat.includes('420') ? 2 : 1;
const chromaCodedWidth = result.codedWidth / subWidth;
const chromaCodedHeight = result.codedHeight / subHeight;
const chromaVisibleHeight = Math.ceil(result.visibleHeight / subHeight);
const lumaCodedPixels = result.codedWidth * result.codedHeight;
const lumaVisiblePixels = result.codedWidth * result.visibleHeight;
const chromaCodedPixels = chromaCodedWidth * chromaCodedHeight;
const chromaVisiblePixels = chromaCodedWidth * chromaVisibleHeight;
// U
result.frameData.set(
result.frameData.subarray(
bytesPerSample * lumaCodedPixels,
bytesPerSample * (lumaCodedPixels + chromaCodedPixels),
),
bytesPerSample * lumaVisiblePixels,
);
// V
result.frameData.set(
result.frameData.subarray(
bytesPerSample * (lumaCodedPixels + chromaCodedPixels),
bytesPerSample * (lumaCodedPixels + 2 * chromaCodedPixels),
),
bytesPerSample * (lumaVisiblePixels + chromaVisiblePixels),
);
result.codedHeight = result.visibleHeight;
}
async flush() {
assert(this.decoder);
while (this.decoder.decodeQueueSize > 0) {
await this.decoder.dequeued;
}
}
async close() {
assert(this.decoder);
await this.decoder.close();
for (const frame of this.framePool) {
frame.clear();
}
}
}
let registered = false;
/**
* Registers an Apple ProRes decoder which Mediabunny will then use automatically when applicable. Make sure to call
* this function before starting any decoding task.
*
* @group \@mediabunny/prores
* @public
*/
export const registerProresDecoder = () => {
if (registered) {
return;
}
registered = true;
registerDecoder(ProresDecoder);
};
+23
View File
@@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist/modules",
"declaration": true,
"declarationMap": true,
"stripInternal": true,
"composite": true,
"noEmit": false,
"moduleResolution": "nodenext",
"module": "nodenext",
"allowJs": true,
"paths": {
"mediabunny": ["../../src/index.ts"],
},
},
"include": [
"./src/**/*",
],
"references": [
{ "path": "../../src" }
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"extends": ["../../tsdoc.json"]
}
+373
View File
@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+328
View File
@@ -0,0 +1,328 @@
# @mediabunny/server
[![](https://img.shields.io/npm/v/@mediabunny/server)](https://www.npmjs.com/package/@mediabunny/server)
[![](https://img.shields.io/bundlephobia/minzip/@mediabunny/server)](https://bundlephobia.com/package/@mediabunny/server)
[![](https://img.shields.io/npm/dm/@mediabunny/server)](https://www.npmjs.com/package/@mediabunny/server)
[![](https://img.shields.io/discord/1390044844285497344?logo=discord&label=Discord)](https://discord.gg/hmpkyYuS4U)
<div align="center">
<img src="./logo.svg" width="180" height="180">
</div>
By default, Mediabunny requires a browser environment for full access to decoders, encoders, and video processing features. `@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) to polyfill this functionality for server-side environments such as Node, Bun, or Deno, enabling the usage of all Mediabunny features on the server. The result is a server-side media processing API that integrates naturally with TypeScript as opposed to the awkwardness and inefficiencies of calling out to the FFmpeg CLI.
Features added by this package include:
- Video decoders and encoders for AVC (H.264), HEVC (H.265), VP8, VP9, AV1, and ProRes. Supports both length-prefixed and Annex B AVC/HEVC as well as transparent video via VP9 and ProRes.
- Audio decoders and encoders for AAC, MP3, Vorbis, Opus, FLAC, AC-3 and E-AC-3. Supports AAC in both AAC and ADTS formats.
- Video frame transformation support (resize, rotate, crop)
- Automatic hardware acceleration on all platforms (macOS, Linux, Windows)
- Built-in multithreading
- Zero-copy decode and encode paths
> This package, like the rest of Mediabunny, is enabled by its [sponsors](https://mediabunny.dev/#sponsors) and their donations. If you've derived value from this package, please consider [leaving a donation](https://github.com/sponsors/Vanilagy)! 💘
> This package was made possible in large part due to seydx's amazing work on [NodeAV](https://github.com/seydx/node-av). The library is truly modern, a joy to work with, and incredibly powerful. Give them a star!
## Installation
This library peer-depends on Mediabunny. Install both using npm:
```bash
npm install mediabunny @mediabunny/server
```
## Usage
```ts
import { registerMediabunnyServer } from '@mediabunny/server';
registerMediabunnyServer();
```
That's it - you now have access to the full Mediabunny feature set on the server.
---
An optional `options` parameter is available for further configuration:
```ts
import { registerMediabunnyServer } from '@mediabunny/server';
import * as NodeAv from 'node-av';
registerMediabunnyServer({
// Use a specific hardware rendering device:
hardwareContext: NodeAv.HardwareContext.create(
NodeAv.AV_HWDEVICE_TYPE_VAAPI,
'/dev/dri/renderD128',
),
});
```
## Upload media compression example
Here, we set up a simple media compression server in Node.js. The client's request body is streamed to Mediabunny, the media gets processed, and the output is streamed directly to the disk. Memory usage is O(1) due to pipelining, and an overly fast uploader is automatically slowed down due to stream backpressure.
```ts
import { ALL_FORMATS, Conversion, FilePathTarget, Input, Mp4OutputFormat, Output, QUALITY_MEDIUM, ReadableStreamSource } from "mediabunny";
import { registerMediabunnyServer } from "@mediabunny/server";
import { Readable } from "node:stream";
import http from "node:http";
registerMediabunnyServer();
const server = http.createServer(async (req, res) => {
// Read the request body as a stream
const stream = Readable.toWeb(req) as ReadableStream<Uint8Array>;
const input = new Input({
source: new ReadableStreamSource(stream),
formats: ALL_FORMATS,
});
// Stream the output directly to the disk, could also stream to S3 etc.
const output = new Output({
format: new Mp4OutputFormat(),
target: new FilePathTarget(`./converted-${crypto.randomUUID()}.mp4`),
});
try {
const conversion = await Conversion.init({
input,
output,
video: async track => ({
codec: 'avc',
height: Math.min(720, await track.getDisplayHeight()),
bitrate: QUALITY_MEDIUM,
}),
});
await conversion.execute();
res.statusCode = 204;
res.end();
} catch (error) {
res.statusCode = 500;
res.end();
console.error("Error processing media:", error);
}
});
server.listen(3000);
```
For all the other ways to use Mediabunny, refer to its [guide](https://mediabunny.dev/guide/introduction).
## Performance
`@mediabunny/server` is extremely performant as it is a thin wrapper around [NodeAV](https://github.com/seydx/node-av), which itself is a thin wrapper around the FFmpeg C API. All decoders and encoders automatically run on separate threads, keeping the main thread unblocked. Hardware acceleration is automatically detected and utilized on all operating systems whenever available (unless explicitly disabled using `hardwareAcceleration: 'prefer-software'`). Video frame and audio sample data is never copied from FFmpeg unless explicitly requested via `VideoSample.copyTo()` and `AudioSample.copyTo()`, and zero-copy GPU decode -> encode paths are used automatically whenever possible.
## Advanced usage
### Usage with NodeAV
`@mediabunny/server` provides `AvFrameVideoSampleResource` and `AvFrameAudioSampleResource` as a means to create `VideoSample` and `AudioSample` instances that are directly backed by data residing in NodeAV's [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html) (and therefore FFmpeg's `AVFrame`) without ever having to copy data to or from JavaScript. Reading NodeAV's documentation can help you make full use of this integration.
To convert between Mediabunny and NodeAV (FFmpeg) worlds, you can do this:
```ts
import { VideoSample, AudioSample } from 'mediabunny';
import { AvFrameVideoSampleResource, AvFrameAudioSampleResource, toAvFrame } from '@mediabunny/server';
// Frame -> VideoSample
new VideoSample(new AvFrameVideoSampleResource(frame), { timestamp });
// Frame -> AudioSample
new AudioSample(new AvFrameAudioSampleResource(frame));
// (uses the timestamp in the frame)
// VideoSample -> Frame
await toAvFrame(videoSample, frame);
// AudioSample -> Frame
await toAvFrame(audioSample, frame);
```
#### Electron example
For example, when using Electron, we may want to capture the app's contents without moving video data from the GPU to the CPU:
```ts
import { VideoSample } from 'mediabunny';
import { AvFrameVideoSampleResource } from '@mediabunny/server';
import { HardwareContext, SharedTexture, AV_HWDEVICE_TYPE_VIDEOTOOLBOX } from 'node-av';
// Create hardware context (platform-specific)
const hw = HardwareContext.create(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
using sharedTexture = SharedTexture.create(hw);
// In Electron paint event with offscreen rendering
offscreen.webContents.on('paint', (event) => {
const texture = event.texture;
if (!texture?.textureInfo) {
return;
}
// Import as hardware frame (zero-copy)
const frame = sharedTexture.importTexture(texture.textureInfo, { pts: 0n });
const sample = new VideoSample(new AvFrameVideoSampleResource(frame), {
timestamp: 0,
duration: 0,
});
texture.release();
});
```
#### Microphone recording example
Here, we're using NodeAV's Device API to access the user's microphone:
```ts
import { AudioSample } from 'mediabunny';
import { AvFrameAudioSampleResource } from '@mediabunny/server';
import { DeviceAPI, Decoder } from 'node-av';
await using mic = await DeviceAPI.openMicrophone();
const audioStream = mic.audio()!;
using decoder = await Decoder.create(audioStream);
let firstTimestamp: number | null = null;
for await (const frame of decoder.frames(mic.packets(audioStream.index))) {
if (!frame) {
break;
}
const sample = new AudioSample(new AvFrameAudioSampleResource(frame));
if (firstTimestamp === null) {
firstTimestamp = sample.timestamp;
}
// Offset timestamps so they start at 0
sample.setTimestamp(sample.timestamp - firstTimestamp);
// Do something with the sample now, like passing it to an AudioSampleSource
// ...
}
```
### Video and audio processing
Browser environments ship with many API goodies such as the Canvas 2D API which are a naturally great fit for doing video frame processing, and they integrate well with Mediabunny. On the server, these APIs don't exist, so other approaches must be used:
#### VideoSample.transform()
This method allows for simple transformations on `VideoSample` instances and works when `@mediabunny/server` has been registered:
```ts
const transformed = await sample.transform({
width: 640,
height: 360,
fit: 'cover',
});
```
#### NodeAV filter graphs
FFmpeg's `libavfilter` is an incredibly powerful and generic media processing library, and all of it is directly accessible via NodeAV. It works for both video as well as audio data.
For example, here we combine Mediabunny's Conversion API with a filter graph to grayscale a video:
```ts
import { Conversion } from 'mediabunny';
import { AvFrameVideoSampleResource, toAvFrame } from '@mediabunny/server';
import { Frame, FilterAPI } from 'node-av';
async function* one(f: Frame) { yield f; }
const conversion = await Conversion.init({
// ...
video: {
process: async (sample) => {
// VideoSample -> Frame
using inFrame = new Frame();
inFrame.alloc();
await toAvFrame(sample, inFrame);
// Frame -> filter -> AvFrameVideoSampleResource
using filter = FilterAPI.create('format=gray');
for await (const outFrame of filter.frames(one(inFrame))) {
return outFrame && new AvFrameVideoSampleResource(outFrame);
}
return null;
},
},
// ...
});
await conversion.execute();
```
#### Canvas API polyfills
Libraries like [Skia Canvas](https://github.com/samizdatco/skia-canvas) provide GPU-enabled polyfills for the Canvas 2D API. Using it with Mediabunny is simply a matter of converting from and to the Canvas API:
```ts
const width = videoSample.displayWidth;
const height = videoSample.displayHeight;
const canvas = new Canvas(width, height);
const ctx = canvas.getContext('2d');
// Copy data from VideoSample
const imageData = ctx.createImageData(width, height);
await videoSample.copyTo(imageData.data, { format: 'RGBA' });
ctx.putImageData(imageData, 0, 0);
// Issue draw commands
ctx.fillStyle = 'red';
ctx.fillRect(20, 20, 100, 60);
// Convert to VideoSample again
const pixels = ctx.getImageData(0, 0, width, height).data;
return new VideoSample(pixels, {
format: 'RGBA',
codedWidth: width,
codedHeight: height,
timestamp: videoSample.timestamp,
duration: videoSample.duration,
});
```
## Controlling logging
Especially in command-line applications you usually don't want Mediabunny interfering with your stdout and stderr output. Mediabunny provides ways to control its console output:
```ts
import { Logging, LogLevel } from 'mediabunny';
// The default: Mediabunny can log errors, warnings, and information messages.
Logging.level = LogLevel.Info;
// Only log warnings and errors.
Logging.level = LogLevel.Warnings;
// Only log errors.
Logging.level = LogLevel.Errors;
// Don't log anything at all.
Logging.level = LogLevel.None;
```
You can also hook into log events:
```ts
Logging.on('error', (args: unknown[]) => {
// Handle error message
});
Logging.on('warn', (args: unknown[]) => {
// Handle warning message
});
Logging.on('info', (args: unknown[]) => {
// Handle info message
});
```
## Implementation details
`@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) under the hood which provides N-API C bindings to FFmpeg's C API. Using NodeAV, this package implements [custom decoders and encoders](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) by directly using the APIs provided by `libavcodec`.
For encoding, video frames and audio samples are transferred to FFmpeg by converting them to an `AVFrame` and are then passed to the correct encoder. The resulting packets are then normalized into the format expected by WebCodecs and the [Mediabunny Codec Registry](https://mediabunny.dev/codec-registry/overview). For decoding, the above process is inverted: packets and decoder metadata are passed to the correct decoder, and the resulting `AVFrame` instances are wrapped in `VideoSample` or `AudioSample` instances. Video frame transformations (resize, rotate, crop) are implemented using the `libavfilter` API.
Whenever possible, `AVFrame`s are never copied over to JavaScript unless explicitly needed. This enables zero-copy decode -> transformation -> encode paths.
ProRes decoding support is provided by [TurboRes](https://github.com/Vanilagy/turbores), as it is often faster than FFmpeg.
## License
`@mediabunny/server` uses the same MPL-2.0 license as Mediabunny.
+39
View File
@@ -0,0 +1,39 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "dist/modules/src/index.d.ts",
"bundledPackages": [],
"compiler": {
"tsconfigFilePath": "<projectFolder>/tsconfig.api-extractor.json"
},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "dist/mediabunny-server.d.ts"
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
}
}
},
"newlineKind": "lf"
}

Some files were not shown because too many files have changed in this diff Show More