Compare commits

...
313 Commits
Author SHA1 Message Date
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
Vanilagy 610dc91743 BETA TEN 2026-04-28 15:34:12 +02:00
Vanilagy e1dd95086a Add special .node.cjs variant for CJS support 2026-04-28 15:33:51 +02:00
Vanilagy a1aaa8e36a Bump to beta 9 2026-04-28 14:42:57 +02:00
Vanilagy aa4966a5f1 Update codec soup and docs 2026-04-28 14:41:20 +02:00
Vanilagy ff46a274b1 Switch back to dynamic import, leave package.json unchanged though 2026-04-28 14:36:48 +02:00
Vanilagy bf9e360587 Revert back to static import of fs, don't like to CJS bundles in package.json 2026-04-28 14:24:58 +02:00
Vanilagy 1fd99b5015 Fix Source assertion failure 2026-04-28 14:09:18 +02:00
Vanilagy 6182af7c29 Add HLS transcoding example 2026-04-28 13:54:42 +02:00
David P.andGitHub 3e70e8912b Merge pull request #291 from Vanilagy/hls
HLS read/write support
2026-04-28 10:09:24 +02:00
Vanilagy a3aa6a69ac Add use case 2026-04-28 10:04:14 +02:00
Vanilagy 105a7dda2f Move it forward 2026-04-27 21:02:59 +02:00
Vanilagy 0f6c374750 Ensure extensions work with huge timestamps, fix AC3 and AAC extension's behavior with huge timestamps 2026-04-27 20:58:05 +02:00
Vanilagy 0cc564b47a Bump to beta 8 2026-04-27 19:47:27 +02:00
Vanilagy 57b265b897 They were indeed temp 2026-04-27 19:46:06 +02:00
Vanilagy 631d207ca2 Clarify 2026-04-27 19:41:59 +02:00
Vanilagy 9a1b8b2602 Add timestampBase option for live MediaStreamTrack sources 2026-04-27 19:38:45 +02:00
Vanilagy c89e7429f7 Fix FilePathSource/FilePathTarget not working in server-side environments using CJS (closes #360) 2026-04-27 18:29:52 +02:00
Vanilagy 15beb69137 Update convert.html 2026-04-27 17:58:55 +02:00
Vanilagy dfdf7c5648 Make esbuild target es2021 2026-04-27 17:15:40 +02:00
Vanilagy cf8cea2957 Fix attribute parsing bug 2026-04-27 17:12:28 +02:00
Vanilagy 0e721daefd Bump to beta 7 2026-04-24 20:58:26 +02:00
Vanilagy 9fdf263482 Merge branch 'main' into hls 2026-04-24 20:56:59 +02:00
Vanilagy bcde2770d1 Add null terminator to TXXX 2026-04-24 20:55:34 +02:00
github-actions[bot] 0d5bf18864 Merge main into release for tag v1.41.0 2026-04-24 17:46:40 +00:00
Vanilagy b285360563 Sussy CI 2026-04-24 19:45:39 +02:00
github-actions[bot] fc67e436e3 Merge main into release for tag v1.41.0 2026-04-24 17:44:40 +00:00
Vanilagy 5e491eb0f9 Bump minor 2026-04-24 19:43:11 +02:00
Vanilagy 97f6b35cda Add special TXXX handling for ID3v2 2026-04-24 19:42:15 +02:00
Vanilagy 1ca3e48f48 Fix? 2026-04-24 19:06:45 +02:00
Vanilagy 5f8978271e Clarify 2026-04-24 19:02:35 +02:00
Vanilagy 5ee78c6480 Add pssh box parsing and expose them in resolveKey 2026-04-24 19:01:25 +02:00
Vanilagy 1d6731e4f6 Add blog, add HLS blog post 2026-04-24 17:37:49 +02:00
Vanilagy 66997ddd16 Fix file compression example, add processedTime parameter to onProgress for Conversion API 2026-04-24 14:33:19 +02:00
Vanilagy 9969b92d51 Specify AAC codec in conversion code in AAC extension readme (closes #348) 2026-04-10 16:48:29 +02: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
280 changed files with 19672 additions and 43009 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 -8
View File
@@ -70,6 +70,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 +82,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, WAVE, MP3, Ogg, ADTS, FLAC, MPEG-TS, HLS
- **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)
+34 -25
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.registerAc3Encoder();
MediabunnyAc3.registerAc3Decoder();
MediabunnyFlacEncoder.registerFlacEncoder();
const fileInput = document.createElement('input');
fileInput.type = 'file';
@@ -23,9 +25,7 @@
chunked: true,
chunkSize: 2**20
});
const outputFormat = new Mediabunny.HlsOutputFormat({
segmentFormat: new Mediabunny.MpegTsOutputFormat(),
});
const outputFormat = new Mediabunny.Mp4OutputFormat();
const p = document.createElement('p');
p.textContent = 'Capturing...';
@@ -52,7 +52,7 @@
const output = new Mediabunny.Output({
format: outputFormat,
target: new Mediabunny.PathedTarget('master.m3u8', ({ path }) => new Mediabunny.BufferTarget()),
target,
});
let input;
@@ -61,8 +61,7 @@
if (false) {
input = new Mediabunny.Input({
entryPath: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
source: ({ path }) => new Mediabunny.UrlSource(path),
source: new Mediabunny.UrlSource('http://localhost:8000/index.m3u8'),
formats: Mediabunny.ALL_FORMATS,
});
@@ -88,15 +87,23 @@
});
}
let ctx = null;
//const primaryTrack = await input.getPrimaryAudioTrack();
//const startTime = await primaryTrack.getFirstTimestamp();
//console.log(startTime)
const canvas = new OffscreenCanvas(1280, 720);
const ctx = canvas.getContext('2d');
//let ctx = null;
let conversion = await Mediabunny.Conversion.init({
input,
output,
audio: (track, n) => [{
codec: 'aac',
}, {
codec: 'aac',
}],
audio: {
//codec: 'aac',
//forceTranscode: true,
//forceTranscode: true,
//sampleFormat: 's16',
},
/*
video: {
discard: true,
@@ -121,13 +128,12 @@
bitrate: 320000
},
*/
video: [
{ height: 1080 },
{ height: 720 },
{ height: 480 },
{ height: 360 },
{ height: 240 },
],
video: {
process: (sample) => {
sample.draw(ctx, 0, 0, 1280, 720);
return new Mediabunny.VideoSample(canvas, { timestamp: sample.timestamp, duration: sample.duration });
},
},
tags: {} ?? {
title: 'Bigggy',
artist: 'Buck Bunny',
@@ -146,14 +152,17 @@
}
},
trim: {
//end: 5,
//start,
//end: start + 5,
////start: 0,
//end: 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
+4 -4
View File
@@ -19,7 +19,7 @@
button.addEventListener('click', async () => {
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
const videoTrack = stream.getVideoTracks()[0];
const audioTrack = stream.getAudioTracks()[0];
const audioTrack = null && stream.getAudioTracks()[0];
const output = new Mediabunny.Output({
target: new Mediabunny.BufferTarget(),
@@ -32,7 +32,7 @@
codec: 'vp9',
bitrate: Mediabunny.QUALITY_MEDIUM,
sizeChangeBehavior: 'passThrough',
});
}, { timestampBase: 'unix' });
videoSource.errorPromise.catch((d) => console.log("Hello?????", d));
@@ -67,9 +67,9 @@
await output.finalize();
console.log(output.target.buffer);
download(new Blob([output.target.buffer]), 'livetest' + output.format.fileExtension);
//download(new Blob([output.target.buffer]), 'livetest' + output.format.fileExtension);
videoTrack?.stop();
audioTrack?.stop();
});
</script>
</script>
+119 -13
View File
@@ -1,14 +1,19 @@
/* eslint-disable @stylistic/max-len */
import { withMermaid } from 'vitepress-plugin-mermaid';
import footnote from 'markdown-it-footnote';
import tailwindcss from '@tailwindcss/vite';
import llmstxt from 'vitepress-plugin-llms';
import { HeadConfig } from 'vitepress';
// @ts-expect-error This file gets generated once docs:generate is run
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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({
@@ -16,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',
@@ -39,11 +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: [
@@ -101,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' },
],
},
],
@@ -127,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' },
],
},
{
@@ -164,7 +182,7 @@ export default withMermaid({
footer: {
message: 'Released under the Mozilla Public License 2.0.',
copyright: 'Copyright © 2026-present Vanilagy',
copyright: `Copyright © ${new Date().getFullYear()}-present Vanilagy`,
},
},
markdown: {
@@ -184,6 +202,8 @@ export default withMermaid({
llmstxt({
ignoreFiles: [
'api/*',
'examples.md',
'llms.md',
],
}),
],
@@ -195,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."
}
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

+3
View File
@@ -0,0 +1,3 @@
import { createContentLoader } from 'vitepress';
export default createContentLoader('blog/*.md');
+29
View File
@@ -0,0 +1,29 @@
---
title: Blog posts
description: Announcements, news, and devblogs about Mediabunny.
outline: false
---
<script setup>
import { data } from './blog.data.ts';
import BlogAuthor from './components/BlogAuthor.vue';
</script>
<h1 class="!mb-8">Blog posts</h1>
<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="post.frontmatter.headerImage" class="shrink-0 sm:w-40 rounded" />
<div v-if="false" class="p-1">
<BlogAuthor :frontmatter="post.frontmatter" small />
</div>
</div>
<div class="flex-1">
<p class="!m-0 text-xs opacity-70">{{ post.frontmatter.publishedOn }}</p>
<h3 class="!m-0 group-hover:underline">{{ post.frontmatter.title }}</h3>
<p class="text-sm !m-0">{{ post.frontmatter.excerpt }}</p>
</div>
</a>
<hr v-if="i < data.length - 1" />
</template>
+262
View File
@@ -0,0 +1,262 @@
---
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.
---
<script setup>
import BlogAuthor from '../components/BlogAuthor.vue';
</script>
<img :src="$frontmatter.headerImage" class="rounded-2xl mb-2" />
<p class="!m-0 opacity-70">{{ $frontmatter.publishedOn }}</p>
<h1>{{ $frontmatter.title }}</h1>
<BlogAuthor />
Mediabunny v1.42.0 adds 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. Like the rest of Mediabunny, it has been implemented from scratch with zero dependencies and is tiny (adds about ~30 kB of additional bundle size).
If you wanna jump right into detailed guides for reading and writing HLS, check out [Reading HLS](../guide/reading-hls) and [Writing HLS](../guide/writing-hls). For the full release notes, see [v1.42.0](https://github.com/Vanilagy/mediabunny/releases/tag/v1.42.0).
## What is HTTP Live Streaming (HLS)?
For those unfamiliar, HLS is a protocol used to stream media over the internet using HTTP. At its core, it chunks a long piece of media (like a TV episode) into many short segments, each of which is individually addressable via HTTP, and then provides an index called a "playlist" to the user telling them about the available segments. The client then requests whichever segments it needs.
HLS also enables multiple variants and renditions of the same contents. Typically, this means the same video/audio content encoded with multiple bitrates, resolutions and codecs, and multiple audio tracks, one for each language.
## Supported features
One of the reasons that HLS has been in the works for so long is because I've been extremely thorough with the supported feature set.
Mediabunny supports:
- HLS reading **and** writing
- both VOD and live content
- both unencrypted and encrypted content (including DRM-protected content)
- an arbitrary number of video and audio tracks
- any configuration of variant streams and media renditions
- all segment formats (MPEG-TS, CMAF, fMP4, ADTS, MP3, WAV, ...)
- all codecs (H.264, HEVC, AV1, AAC, MP3, WAVE, AC-3, E-AC-3, ...)
- full lazy loading of track metadata and media, minimizing HTTP requests
- logarithmic seeking performance
- single-file segments via #EXT-X-BYTERANGE and HTTP range requests
- track metadata information (language, name, primary track, autoselect, ...)
- full master playlist configuration for writing
- datetime-stamped media data via #EXT-X-PROGRAM-DATE-TIME
- mid-stream discontinuities
- I-frame only video tracks via #EXT-X-I-FRAME-STREAM-INF
- more, probably
Most of the above features are implemented _symmetrically_, meaning they are available for both reading and writing operations.
---
To be fully transparent, these features are not yet supported:
- Subtitles (WebVTT, CEA-608/708, ...)
- ID3v2 metadata extraction
- Writing encrypted segments
- Low-latency HLS
- Built-in analytics, ABR, or CMCD (this is for the user to do)
## Difference to existing solutions like hls.js
The way Mediabunny enables interaction with HLS playlists is fundamentally different from how existing tools such as [hls.js](https://github.com/video-dev/hls.js) (give them a star!) do it. To put it simply, hls.js is to Mediabunny what a `<video>` element is to FFmpeg's C API: one offers a simple, playback-focused developer experience while the other provides fine-grained control over tracks, media samples, decoding, and much more.
Mediabunny is not an HLS player. Mediabunny can be used to build an HLS player, and it can do many things beyond that.
## What can it do?
The API surface added by the HLS update is vast and I obviously can't cover it in this announcement. But, here are just some cool things that Mediabunny now enables:
### Downloading an HLS playlist as an MP4
By using the Conversion API, you can just do this:
<div class="text-[13.7142857143px]">
```ts
import { ... } from 'mediabunny';
const input = new Input({
source: new UrlSource('https://example.com/playlist.m3u8'),
formats: HLS_FORMATS,
});
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input, output });
await conversion.execute();
// Done:
const mp4File = output.target.buffer!;
```
</div>
That's it. This will stream-download the entire HLS playlist, transcode it if necessary using hardware-accelerated decoding and encoding, and bundle it into a single MP4 for the user to download. All of this is fully pipelined, meaning memory usage is bounded ($O(1)$). Speed is usually limited by the client's internet connection.
### Creating an HLS playlist from a client-side video, with multiple renditions
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-[13.7142857143px]">
```ts
import { ... } from 'mediabunny';
const input = new Input({
source: new BlobSource(file), // E.g., a user-selected file
formats: ALL_FORMATS,
});
// This defines the shape and destination of the output files
const output = new Output({
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
}),
target: new PathedTarget(
'master.m3u8',
async ({ path }) => new BufferTarget({
// Upload it directly to a server
onFinalize: buffer => fetch(`/upload?file=${encodeURIComponent(path)}`, {
method: 'PUT',
body: buffer,
}),
}),
),
});
const conversion = await Conversion.init({
input,
output,
// Offer the video in 5 different resolutions:
video: [
{ codec: 'avc', height: 1080 },
{ codec: 'avc', height: 720 },
{ codec: 'avc', height: 480 },
{ codec: 'avc', height: 360 },
{ codec: 'avc', height: 240 },
],
// Offer the audio in AAC:
audio: [
{ codec: 'aac' },
],
});
await conversion.execute();
```
</div>
This code creates all renditions as fast as it can, encoding them all in parallel using WebCodecs. By the end, the entire HLS file structure will have been uploaded to the server, fully client-side generated.
No transcode server is needed here, it's all handled by the client, and the server receives a ready-to-stream HLS playlist.
### Live streaming HLS playlists from the client
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-[13.7142857143px] overflow-auto">
```ts
// Get the screen and mic
const displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const displayTrack = displayStream.getVideoTracks()[0];
const micTrack = micStream.getAudioTracks()[0];
// Define the shape of the output
const output = new Output({
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
live: true, // Live mode enabled
}),
target: new PathedTarget(
'master.m3u8',
async ({ path }) => new BufferTarget({
// Upload it directly to a server
onFinalize: buffer => fetch(`/upload?file=${encodeURIComponent(path)}`, {
method: 'PUT',
body: buffer,
}),
}),
),
});
// Full resolution video
const videoSourceFull = new MediaStreamVideoTrackSource(displayTrack, {
codec: 'avc',
bitrate: QUALITY_HIGH,
}, { timestampBase: 'unix' });
// 480p video
const videoSource480p = new MediaStreamVideoTrackSource(displayTrack, {
codec: 'avc',
bitrate: QUALITY_MEDIUM,
transform: { height: 480 },
}, { timestampBase: 'unix' });
// Audio
const audioSource = new MediaStreamAudioTrackSource(micTrack, {
codec: 'aac',
bitrate: QUALITY_HIGH,
}, { timestampBase: 'unix' });
// The "unix" stuff ensures that #EXT-X-PROGRAM-DATE-TIME gets generated
output.addVideoTrack(videoSourceFull, { isRelativeToUnixEpoch: true });
output.addVideoTrack(videoSource480p, { isRelativeToUnixEpoch: true });
output.addAudioTrack(audioSource, { isRelativeToUnixEpoch: true });
await output.start();
// Live data is now being captured.
// ...
// To end the stream:
await output.finalize();
```
</div>
All the server needs to do is store the uploaded playlists and segments; all other connected clients can then simply consume this live stream.
### Building a custom HLS player
Mediabunny's microsecond-accurate decoding and seeking means it's great for building fully-custom playback of video and audio with maximum precision and control; more than what the built-in `<video>` and `<audio>` elements can provide. The same now applies for HLS!
Mediabunny's official [Media player example](https://mediabunny.dev/examples/media-player/) supports HLS out of the gate, being able to play back both VOD and live content. It's an example of a fully custom HLS player that makes no use of the `<video>` element or Media Source Extensions.
Here's me watching German live TV in it:
<img src="../assets/german-live-tv.png" class="rounded-2xl shadow-xl">
The best thing: the player required basically no changes to be adapted for HLS. Since Mediabunny exposes the same API for HLS as it does for all other file formats, playback worked out of the box. The biggest required change was adjusting the timeline to change dynamically for live content.
### And much more!
Mediabunny's [`Input`](../guide/reading-media-files) and [`Output`](../guide/writing-media-files) APIs give you fine-grained control over basically everything, meaning you can do any arbitrary media operation on HLS playlists, such as:
- Converting/compressing them
- Downloading a section of a live stream
- Extracting video thumbnails
- Extracting specific tracks
- Extracting metadata (duration, dimensions, tracks, ...)
## Finishing up
Most of us use phones and PCs with incredibly powerful hardware with media-specific optimization. Yet, most media processing on the web today still happens server-side. This can be expensive, slow, and horrible for users with bad or no internet. Mediabunny's goal has always been to flip this around: leverage the client's full on-device resources and modern web APIs such as WebCodecs to enable fast, secure, and practically free media processing for applications. And now, with HLS support, Mediabunny takes this mission one step further.
The examples in this post likely only scratch the surface of what you can now build. That's where you come in! Try it out, play around with it, and share anything cool you've built on X, GitHub, or on the [Mediabunny Discord server](https://discord.gg/hmpkyYuS4U).
I've personally learned a ton building this, but I'm honestly also happy it's finally done and I can move onto other Mediabunny features (such as DASH support, yay).
My work on Mediabunny would've been impossible were it not for all the generous [Mediabunny sponsors](https://mediabunny.dev/#sponsors). If you've benefitted from my work or want to get in touch, please consider [sponsoring](https://github.com/sponsors/Vanilagy)!
~David
+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>
+24
View File
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { useData } from 'vitepress';
import { computed } from 'vue';
const props = defineProps<{
frontmatter?: Record<string, any>;
small?: boolean;
}>();
const data = useData();
const frontmatter = computed(() => props.frontmatter ?? data.frontmatter.value);
</script>
<template>
<a :href="frontmatter['authorLink']" class="inline-flex gap-2 items-center !no-underline !text-(--var-c-text-1) py-1">
<img :src="frontmatter['authorImage']" class="size-10 rounded-full shadow" :class="{ '!size-8': small }" />
<div class="pl-1">
<p class="!m-0 font-semibold" :class="{ 'text-sm': small }">{{ frontmatter['author'] }}</p>
<!--
<p class="!m-0 text-sm opacity-60">{{ frontmatter['authorSubtitle'] }}</p>
-->
</div>
</a>
</template>
+7
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
@@ -43,4 +44,10 @@ features:
target: _self
icon:
src: /mingcute--microphone-line.svg
- title: HLS transcoding
details: Convert one video into a full HLS manifest with five video renditions and one audio track.
link: /examples/hls-transcoding/
target: _self
icon:
src: /mingcute--live-line.svg
---
+28
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
@@ -500,3 +505,26 @@ const conversion = await Conversion.init({ input, output });
conversion.utilizedTracks; // => InputTrack[]
```
A track may appear multiple times in this list when [fan-out](#track-fan-out) produces multiple output tracks from it.
## Converting live streams
Live inputs, like HLS live streams, can also be used with the Conversion API. In this case, by default, the conversion will run until the live stream has ended.
If you only want to convert a part of the live stream instead of waiting until it has ended, you can [trim](#trimming) the conversion. For example, here we're capturing the next 60 seconds of the live stream:
```ts
// Get the live edge
const currentDuration = await input.getDurationFromMetadata(undefined, {
skipLiveWait: true,
});
const conversion = await Conversion.init({
input,
output,
trim: {
// Start at the current live edge
start: currentDuration,
// End at most 60 seconds later
end: currentDuration + 60,
},
});
```
+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:
+11 -8
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.
@@ -553,16 +557,15 @@ const output = new Output(...);
const conversion = await Conversion.init({
input,
output,
video: track => ({
width: 480,
tracks: 'primary', // Keep only the first track of each type
video: {
width: 480, // Resize to 480p
bitrate: QUALITY_LOW,
discard: track.number > 1, // Keep only the first video track
}),
audio: track => ({
numberOfChannels: 1,
},
audio: {
numberOfChannels: 1, // Resample to mono
bitrate: QUALITY_LOW,
discard: track.number > 1, // Keep only the first audio track
}),
},
trim: {
// Let's keep only the first 60 seconds
start: 0,
+39 -3
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:
@@ -352,7 +385,10 @@ using input = new Input({
formats: ALL_FORMATS,
formatOptions: {
isobmff: {
resolveKeyId: ({ keyId }) => {
resolveKeyId: ({ keyId, psshBoxes }) => {
// psshBoxes contains Protection System Specific Header boxes
// relevant to this key ID. They can be used to obtain a
// decryption key from a DRM license server.
const key = keyMap.get(keyId);
if (!key) {
throw new Error('Unknown key ID.');
@@ -367,4 +403,4 @@ using input = new Input({
## Subtitles
Reading subtitles from HLS playlists is not currently supported. Sorry!
Reading subtitles from HLS playlists is not currently supported. Sorry!
+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.
+17 -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'` | ✓ | ✓ | ✓ | | | ✓ | | | | ✓ |
@@ -99,6 +105,9 @@ Not all codecs can be used with all containers. The following table specifies th
| `'alaw'` | | ✓ | | | | | ✓ | | | |
| `'webvtt'`[^webvtt] | (✓) | | (✓) | (✓) | | | | | | |
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.
@@ -287,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
@@ -350,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
+13 -5
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]
---
@@ -89,16 +90,23 @@ console.log(writtenFiles);
```ts
const root = await navigator.storage.getDirectory();
const writePromises: Promise<void>[] = [];
const output = new Output({
target: new PathedTarget(
'master.m3u8',
async ({ path }) => {
const handle = await root.getFileHandle(path, { create: true });
const writable = await handle.createWritable();
return new StreamTarget(writable);
},
async ({ path }) => new BufferTarget({
onFinalize: (buffer) => {
writePromises.push((async () => {
const handle = await root.getFileHandle(path, { create: true });
const writable = await handle.createWritable();
await writable.write(buffer);
await writable.close();
})());
},
}),
),
onFinalize: () => Promise.all(writePromises),
// ...
});
+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
```
```
+23 -12
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
@@ -20,6 +21,9 @@ hero:
- theme: alt
text: Examples
link: /examples
- theme: alt
text: Blog
link: /blog
- theme: alt
text: Sponsors
link: "#sponsors"
@@ -106,9 +110,11 @@ const sponsors = {
{ 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' },
@@ -116,8 +122,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' },
@@ -129,6 +138,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' },
@@ -141,6 +151,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>
@@ -151,7 +162,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>
@@ -169,7 +180,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
@@ -183,7 +194,7 @@ npm install mediabunny
```ts
const input = new Input({
source: new UrlSource('./bigbuckbunny.mp4'),
formats: ALL_FORMATS, // .mp4, .webm, .wav, ...
formats: ALL_FORMATS, // .mp4, .webm, .wav, .m3u8, ...
});
const duration = await input.computeDuration();
@@ -245,7 +256,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
@@ -256,7 +267,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
@@ -292,14 +303,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
@@ -325,14 +336,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">
@@ -365,10 +376,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">
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z"/><path fill="#06b6d4" d="M16.95 2.586a1 1 0 0 1 0 1.414l-3 3H19a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h3.636L7.05 5.414A1 1 0 0 1 8.465 4l2.474 2.475a.5.5 0 0 0 .707 0l3.89-3.89a1 1 0 0 1 1.414 0M19 9H5v10h14zM8.98 11.547a1.232 1.232 0 0 1 1.72-.994a22 22 0 0 1 2.2 1.123a22 22 0 0 1 2.075 1.346c.668.494.67 1.489 0 1.984A22 22 0 0 1 12.9 16.35c-.997.576-1.785.943-2.2 1.124a1.23 1.23 0 0 1-1.72-.993a23 23 0 0 1-.128-2.467c0-1.14.078-2.014.128-2.467m1.902 1.306a23 23 0 0 0 0 2.32a23 23 0 0 0 2.008-1.16a23 23 0 0 0-2.008-1.16"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

+2
View File
@@ -5,6 +5,8 @@
"module": "NodeNext"
},
"include": [
"**/*.ts",
"**/*.vue",
".vitepress/config.mts",
".vitepress/theme/index.ts",
]
+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',
+26 -15
View File
@@ -62,31 +62,42 @@ const compressFile = async (resource: File | string) => {
currentConversion = await Conversion.init({
input,
output,
video: track => ({
tracks: 'primary', // Keep only one track per type
video: {
width: 320, // Height will be deduced automatically to retain aspect ratio
bitrate: QUALITY_VERY_LOW,
discard: track.number > 1, // Keep only the first video track
}),
audio: track => ({
bitrate: 32e3,
discard: track.number > 1, // Keep only the first audio track
}),
},
audio: {
codec: 'opus',
bitrate: QUALITY_VERY_LOW,
},
});
if (!currentConversion.isValid) {
console.info(currentConversion.discardedTracks);
throw new Error('Conversion is invalid and cannot be executed; see the console for more.');
}
// Keep track of progress
let progress = 0;
currentConversion.onProgress = newProgress => progress = newProgress;
let processedTime = 0;
let startTime: number | null = null;
const fileDuration = (await input.computeDuration()) - (await input.getFirstTimestamp());
const startTime = performance.now();
currentConversion.onProgress = (newProgress, newProcessedTime) => {
progress = newProgress;
processedTime = newProcessedTime;
startTime ??= performance.now();
};
const updateProgress = () => {
progressBar.style.width = `${progress * 100}%`;
const now = performance.now();
const elapsedSeconds = (now - startTime) / 1000;
const factor = fileDuration / (elapsedSeconds / progress);
speedometer.textContent = `Speed: ~${factor.toPrecision(3)}x real time`;
if (startTime !== null) {
const now = performance.now();
const elapsedSeconds = (now - startTime) / 1000;
const factor = processedTime / elapsedSeconds;
speedometer.textContent = `Speed: ~${factor.toPrecision(3)}x real time`;
}
};
// Update the progress indicator regularly
@@ -143,7 +154,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">
+272
View File
@@ -0,0 +1,272 @@
import {
Input,
ALL_FORMATS,
BlobSource,
UrlSource,
Output,
PathedTarget,
BufferTarget,
HlsOutputFormat,
MpegTsOutputFormat,
Conversion,
QUALITY_VERY_HIGH,
QUALITY_HIGH,
QUALITY_MEDIUM,
QUALITY_LOW,
QUALITY_VERY_LOW,
} from 'mediabunny';
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
declare global {
interface Window {
showDirectoryPicker(options: { mode: 'readwrite' }): Promise<FileSystemDirectoryHandle>;
}
}
const selectDirectoryButton = document.querySelector('#select-directory') as HTMLButtonElement;
const directoryNameElement = document.querySelector('#directory-name') as HTMLParagraphElement;
const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement;
const loadSampleButton = document.querySelector('#load-sample') as HTMLButtonElement;
const fileNameElement = document.querySelector('#file-name') as HTMLParagraphElement;
const dashboard = document.querySelector('#dashboard') as HTMLDivElement;
const statusElement = document.querySelector('#status') as HTMLParagraphElement;
const progressBar = document.querySelector('#progress-bar') as HTMLDivElement;
const percentIndicator = document.querySelector('#percent-indicator') as HTMLParagraphElement;
const speedometer = document.querySelector('#speedometer') as HTMLParagraphElement;
const bytesWrittenElement = document.querySelector('#bytes-written') as HTMLParagraphElement;
const filesCreatedElement = document.querySelector('#files-created') as HTMLParagraphElement;
const latestFileElement = document.querySelector('#latest-file') as HTMLParagraphElement;
const errorElement = document.querySelector('#error-element') as HTMLParagraphElement;
let currentConversion: Conversion | null = null;
let directoryHandle: FileSystemDirectoryHandle | null = null;
let progress = 0;
let processedTime = 0;
let startTime: number | null = null;
let bytesWritten = 0;
let filesCreated = 0;
let latestFile = '-';
let status = 'Waiting for directory';
let fileName = '';
let directoryName = '';
let errorMessage = '';
let renderIntervalId = -1;
const filePromises: Promise<void>[] = [];
const convertToHls = async (resource: File | string) => {
await currentConversion?.cancel();
resetDashboard();
fileName = resource instanceof File ? resource.name : resource;
updateFileUi();
clearInterval(renderIntervalId);
renderIntervalId = window.setInterval(render, 1000 / 60);
render();
try {
// Load the input
const source = resource instanceof File
? new BlobSource(resource)
: new UrlSource(resource);
const input = new Input({
source,
formats: ALL_FORMATS,
});
const output = new Output({
// Define the output format (HLS with MPEG-TS segments)
format: new HlsOutputFormat({
segmentFormat: new MpegTsOutputFormat(),
}),
// Describe where the files will be written
target: new PathedTarget(
'master.m3u8',
({ path }) => createFileTarget(path),
),
onFinalize: () => Promise.all(filePromises),
});
currentConversion = await Conversion.init({
input,
output,
tracks: 'primary', // Use only the primary video and audio tracks of the input
video: [
{ codec: 'avc', height: 1080, bitrate: QUALITY_VERY_HIGH },
{ codec: 'avc', height: 720, bitrate: QUALITY_HIGH },
{ codec: 'avc', height: 480, bitrate: QUALITY_MEDIUM },
{ codec: 'avc', height: 360, bitrate: QUALITY_LOW },
{ codec: 'avc', height: 240, bitrate: QUALITY_VERY_LOW },
],
audio: [
{ codec: 'aac', bitrate: QUALITY_HIGH },
],
});
if (!currentConversion.isValid) {
console.info(currentConversion.discardedTracks);
throw new Error('Conversion is invalid and cannot be executed; see the console for more.');
}
currentConversion.onProgress = (newProgress, newProcessedTime) => {
progress = newProgress;
processedTime = newProcessedTime;
startTime ??= performance.now();
};
status = 'Encoding renditions and writing HLS files';
await currentConversion.execute();
progress = 1;
status = 'HLS manifest complete';
} catch (error) {
console.error(error);
await currentConversion?.cancel();
status = 'Conversion failed';
errorMessage = String(error);
updateFileUi();
} finally {
clearInterval(renderIntervalId);
renderIntervalId = -1;
render();
}
};
const formatBytes = (bytes: number) => {
if (bytes < 1000) {
return `${bytes} B`;
}
const units = ['kB', 'MB', 'GB', 'TB'];
let size = bytes / 1000;
let unitIndex = 0;
while (size >= 1000) {
size /= 1000;
unitIndex++;
}
return `${+size.toFixed(2)} ${units[unitIndex]}`;
};
const render = () => {
const percentage = Math.floor(progress * 100);
const displayedPercentage = status === 'HLS manifest complete' ? 100 : Math.min(percentage, 99);
progressBar.style.width = `${displayedPercentage}%`;
percentIndicator.textContent = `${displayedPercentage}%`;
bytesWrittenElement.textContent = formatBytes(bytesWritten);
filesCreatedElement.textContent = filesCreated.toString();
latestFileElement.textContent = latestFile;
statusElement.textContent = status;
if (startTime !== null) {
const elapsedSeconds = (performance.now() - startTime) / 1000;
const factor = processedTime / elapsedSeconds;
speedometer.textContent = `${factor.toPrecision(3)}x`;
} else {
speedometer.textContent = '-';
}
};
const updateFileUi = () => {
directoryNameElement.textContent = directoryName;
fileNameElement.textContent = fileName;
errorElement.textContent = errorMessage;
};
const resetDashboard = () => {
progress = 0;
processedTime = 0;
startTime = null;
bytesWritten = 0;
filesCreated = 0;
latestFile = '-';
status = 'Preparing HLS output';
errorMessage = '';
filePromises.length = 0;
dashboard.classList.remove('opacity-50');
updateFileUi();
};
const createFileTarget = async (path: string) => {
const target = new BufferTarget({
onFinalize: (buffer) => {
filePromises.push((async () => {
const handle = await directoryHandle!.getFileHandle(path, { create: true });
const writable = await handle.createWritable();
await writable.write(buffer);
await writable.close();
})());
},
});
let fileBytes = 0;
filesCreated++;
latestFile = path;
target.on('write', ({ end }) => {
const newFileBytes = Math.max(fileBytes, end);
bytesWritten += newFileBytes - fileBytes;
fileBytes = newFileBytes;
});
return target;
};
// eslint-disable-next-line @typescript-eslint/no-misused-promises
selectDirectoryButton.addEventListener('click', async () => {
directoryHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
directoryName = `Selected directory: ${directoryHandle.name}`;
status = 'Waiting for source video';
selectDirectoryButton.style.display = 'none';
selectMediaButton.disabled = false;
loadSampleButton.disabled = false;
directoryNameElement.style.display = '';
directoryNameElement.textContent = directoryName;
statusElement.textContent = status;
});
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts';
fileInput.addEventListener('change', () => {
const file = fileInput.files![0];
if (file) {
void convertToHls(file);
}
});
fileInput.click();
});
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://mediabunny.dev/big-buck-bunny.mp4',
);
if (!url) {
return;
}
void convertToHls(url);
});
document.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer!.dropEffect = 'copy';
});
document.addEventListener('drop', (event) => {
event.preventDefault();
const files = event.dataTransfer!.files;
const file = files[0];
if (file) {
void convertToHls(file);
}
});
+141
View File
@@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="en-US" translate="no">
<head>
<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">
<h1 class="text-center text-3xl font-bold text-cyan-500">HLS transcoding example</h1>
<p class="max-w-2xl text-center">
Select a directory, then a video. Mediabunny will create a complete HLS VOD package with five video renditions and one audio track.
</p>
<div class="mt-5 flex flex-col items-center gap-1">
<div class="flex flex-wrap justify-center gap-2">
<button id="select-directory" class="rounded-lg bg-cyan-500 px-5 py-2 font-medium text-white hover:bg-cyan-600">
Select directory
</button>
<button id="select-file" disabled class="rounded-lg bg-zinc-200 px-5 py-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-zinc-750 dark:hover:bg-zinc-700 hover:bg-zinc-300">
Select local file
</button>
<button id="load-sample" disabled class="rounded-lg bg-zinc-200 px-5 py-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-zinc-750 dark:hover:bg-zinc-700 hover:bg-zinc-300">
Load remote URL
</button>
</div>
<a id="sample-file-download" download="big-buck-bunny-trimmed.mp4" class="text-xs opacity-50 hover:opacity-70 hover:underline">
Download sample file
</a>
</div>
<p id="directory-name" class="mt-3 max-w-[36rem] text-center text-xs opacity-60" style="display: none;"></p>
<p id="file-name" class="min-h-5 max-w-[36rem] text-center text-xs opacity-60"></p>
<p id="error-element" class="mx-auto mt-2 max-w-[36rem] text-center text-red-500"></p>
<hr class="my-4 w-full max-w-4xl border-zinc-300 dark:border-zinc-700">
<div class="grid w-full max-w-4xl gap-4 md:grid-cols-2">
<section class="rounded-xl bg-zinc-200 p-5 dark:bg-zinc-750">
<h2 class="text-lg font-bold text-cyan-500">Renditions</h2>
<div class="mt-3 grid gap-2 text-sm">
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
<p class="font-bold">1080p</p>
<p class="opacity-70">AVC video</p>
</div>
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
<p class="font-bold">720p</p>
<p class="opacity-70">AVC video</p>
</div>
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
<p class="font-bold">480p</p>
<p class="opacity-70">AVC video</p>
</div>
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
<p class="font-bold">360p</p>
<p class="opacity-70">AVC video</p>
</div>
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
<p class="font-bold">240p</p>
<p class="opacity-70">AVC video</p>
</div>
<hr class="my-1 border-zinc-300 dark:border-zinc-700">
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
<p class="font-bold">Audio</p>
<p class="opacity-70">AAC audio</p>
</div>
</div>
</section>
<section id="dashboard" class="rounded-xl bg-zinc-200 p-5 opacity-50 transition-opacity dark:bg-zinc-750">
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="text-lg font-bold text-cyan-500">Progress</h2>
<p id="status" class="mt-1 text-sm opacity-70">Waiting for directory</p>
</div>
<p id="percent-indicator" class="text-4xl font-bold tabular-nums text-cyan-500">0%</p>
</div>
<div id="progress-bar-container" class="mt-4 h-3 overflow-hidden rounded-full bg-zinc-300 dark:bg-zinc-900">
<div id="progress-bar" class="h-full w-0 bg-emerald-500 transition-[width] duration-150"></div>
</div>
<div class="mt-4 grid grid-cols-2 gap-2 text-sm">
<div class="rounded-lg bg-zinc-100 p-3 dark:bg-zinc-800">
<p class="text-xs opacity-60">Real-time speed</p>
<p id="speedometer" class="mt-1 text-lg font-bold tabular-nums">-</p>
</div>
<div class="rounded-lg bg-zinc-100 p-3 dark:bg-zinc-800">
<p class="text-xs opacity-60">Bytes written</p>
<p id="bytes-written" class="mt-1 text-lg font-bold tabular-nums">0 B</p>
</div>
<div class="rounded-lg bg-zinc-100 p-3 dark:bg-zinc-800">
<p class="text-xs opacity-60">Files created</p>
<p id="files-created" class="mt-1 text-lg font-bold tabular-nums">0</p>
</div>
<div class="rounded-lg bg-zinc-100 p-3 dark:bg-zinc-800">
<p class="text-xs opacity-60">Latest file</p>
<p id="latest-file" class="mt-1 truncate text-lg font-bold tabular-nums">-</p>
</div>
</div>
</section>
</div>
<a href="/" class="fixed left-0 top-0 flex items-center gap-2 px-5 py-2">
<img src="../../docs/public/mediabunny-logo.svg" class="size-6">
<p class="text-sm font-medium">Mediabunny</p>
</a>
<a
href="https://github.com/Vanilagy/mediabunny/tree/main/examples/hls-transcoding"
target="_blank"
class="fixed right-0 top-0 flex items-center gap-2 rounded-bl-xl bg-zinc-200 px-5 py-2 hover:bg-zinc-300 dark:bg-zinc-750 dark:hover:bg-zinc-700"
>
<img src="../../docs/assets/github-mark.svg" class="size-6 dark:invert">
<p>View source code</p>
</a>
</body>
</html>
+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">
+2 -2
View File
@@ -215,7 +215,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';
@@ -749,7 +749,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">
@@ -139,7 +139,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;
+889 -41
View File
File diff suppressed because it is too large Load Diff
+19 -9
View File
@@ -1,19 +1,29 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.42.0-beta.6",
"version": "1.50.4",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
".",
"packages/*"
],
"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.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",
"bump-minor": "npm version minor --no-git-tag-version --workspaces",
"bump-major": "npm version major --no-git-tag-version --workspaces",
"set-version": "npm version --no-git-tag-version --workspaces"
},
"license": "MPL-2.0",
"repository": {
+3
View File
@@ -81,6 +81,9 @@ const output = new Output({
const conversion = await Conversion.init({
input,
output,
audio: {
codec: 'aac',
},
});
await conversion.execute();
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/aac-encoder",
"author": "Vanilagy",
"version": "1.42.0-beta.6",
"version": "1.50.4",
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
+4 -3
View File
@@ -7,6 +7,7 @@
*/
#include <emscripten.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "libavcodec/avcodec.h"
@@ -20,7 +21,7 @@ typedef struct {
AVFrame *frame;
float *input_buffer;
int input_buffer_size;
int encoded_pts;
int64_t encoded_pts;
int encoded_duration;
} EncoderContext;
@@ -123,7 +124,7 @@ float *get_encode_input_ptr(EncoderContext *ctx, int size) {
}
EMSCRIPTEN_KEEPALIVE
int send_frame(EncoderContext *ctx, int pts) {
int send_frame(EncoderContext *ctx, int64_t pts) {
int channels = ctx->codec_ctx->ch_layout.nb_channels;
int frame_size = ctx->frame->nb_samples;
@@ -171,7 +172,7 @@ uint8_t *get_encoded_data(EncoderContext *ctx) {
}
EMSCRIPTEN_KEEPALIVE
int get_encoded_pts(EncoderContext *ctx) {
int64_t get_encoded_pts(EncoderContext *ctx) {
return ctx->encoded_pts;
}
+6 -6
View File
@@ -21,12 +21,12 @@ let getEncoderFrameSize: (ctx: number) => number;
let getEncoderExtradata: (ctx: number) => number;
let getEncoderExtradataSize: (ctx: number) => number;
let getEncodeInputPtr: (ctx: number, size: number) => number;
let sendFrameFn: (ctx: number, pts: number) => number;
let sendFrameFn: (ctx: number, pts: bigint) => number;
let receivePacketFn: (ctx: number) => number;
let flushEncoderStartFn: (ctx: number) => void;
let resetEncoderFn: (ctx: number) => void;
let getEncodedData: (ctx: number) => number;
let getEncodedPts: (ctx: number) => number;
let getEncodedPts: (ctx: number) => bigint;
let getEncodedDuration: (ctx: number) => number;
const ensureModule = async () => {
if (!module) {
@@ -43,12 +43,12 @@ const ensureModule = async () => {
getEncoderExtradata = module.cwrap('get_encoder_extradata', 'number', ['number']);
getEncoderExtradataSize = module.cwrap('get_encoder_extradata_size', 'number', ['number']);
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
sendFrameFn = module.cwrap('send_frame', 'number', ['number', 'number']);
sendFrameFn = module.cwrap('send_frame', 'number', ['number', 'number']) as unknown as typeof sendFrameFn;
receivePacketFn = module.cwrap('receive_packet', 'number', ['number']);
flushEncoderStartFn = module.cwrap('flush_encoder_start', null, ['number']);
resetEncoderFn = module.cwrap('reset_encoder', null, ['number']);
getEncodedData = module.cwrap('get_encoded_data', 'number', ['number']);
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']);
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']) as unknown as typeof getEncodedPts;
getEncodedDuration = module.cwrap('get_encoded_duration', 'number', ['number']);
}
};
@@ -81,7 +81,7 @@ const drainPackets = (ctx: number) => {
while ((size = receivePacketFn(ctx)) > 0) {
const ptr = getEncodedData(ctx);
const encodedData = module.HEAPU8.slice(ptr, ptr + size).buffer;
const pts = getEncodedPts(ctx);
const pts = Number(getEncodedPts(ctx));
const duration = getEncodedDuration(ctx);
packets.push({ encodedData, pts, duration });
}
@@ -98,7 +98,7 @@ const encode = (ctx: number, audioData: ArrayBuffer, timestamp: number) => {
}
module.HEAPU8.set(audioBytes, inputPtr);
const ret = sendFrameFn(ctx, timestamp);
const ret = sendFrameFn(ctx, BigInt(timestamp));
if (ret < 0) {
throw new Error(`Encode failed with error code ${ret}.`);
}
+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,'
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/ac3",
"author": "Vanilagy",
"version": "1.42.0-beta.6",
"version": "1.50.4",
"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",
+7 -6
View File
@@ -7,6 +7,7 @@
*/
#include <emscripten.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "libavcodec/avcodec.h"
@@ -72,7 +73,7 @@ uint8_t *configure_decode_packet(DecoderContext *ctx, int size) {
}
EMSCRIPTEN_KEEPALIVE
int decode_packet(DecoderContext *ctx, int pts) {
int decode_packet(DecoderContext *ctx, int64_t pts) {
ctx->packet->pts = pts;
int ret = avcodec_send_packet(ctx->codec_ctx, ctx->packet);
av_packet_unref(ctx->packet);
@@ -110,8 +111,8 @@ int get_decoded_sample_count(DecoderContext *ctx) {
}
EMSCRIPTEN_KEEPALIVE
int get_decoded_pts(DecoderContext *ctx) {
return (int)ctx->frame->pts;
int64_t get_decoded_pts(DecoderContext *ctx) {
return ctx->frame->pts;
}
EMSCRIPTEN_KEEPALIVE
@@ -135,7 +136,7 @@ typedef struct {
AVFrame *frame;
float *input_buffer;
int input_buffer_size;
int encoded_pts;
int64_t encoded_pts;
int encoded_duration;
} EncoderContext;
@@ -229,7 +230,7 @@ float *get_encode_input_ptr(EncoderContext *ctx, int size) {
}
EMSCRIPTEN_KEEPALIVE
int encode_frame(EncoderContext *ctx, int pts) {
int encode_frame(EncoderContext *ctx, int64_t pts) {
int channels = ctx->codec_ctx->ch_layout.nb_channels;
int frame_size = ctx->frame->nb_samples;
@@ -270,7 +271,7 @@ uint8_t *get_encoded_data(EncoderContext *ctx) {
}
EMSCRIPTEN_KEEPALIVE
int get_encoded_pts(EncoderContext *ctx) {
int64_t get_encoded_pts(EncoderContext *ctx) {
return ctx->encoded_pts;
}
+12 -12
View File
@@ -18,23 +18,23 @@ let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
let initDecoderFn: (codecId: number) => number;
let configureDecodePacket: (ctx: number, size: number) => number;
let decodePacket: (ctx: number, pts: number) => number;
let decodePacket: (ctx: number, pts: bigint) => number;
let getDecodedFormat: (ctx: number) => number;
let getDecodedPlanePtr: (ctx: number, plane: number) => number;
let getDecodedChannels: (ctx: number) => number;
let getDecodedSampleRate: (ctx: number) => number;
let getDecodedSampleCount: (ctx: number) => number;
let getDecodedPts: (ctx: number) => number;
let getDecodedPts: (ctx: number) => bigint;
let flushDecoderFn: (ctx: number) => void;
let closeDecoderFn: (ctx: number) => void;
let initEncoderFn: (codecId: number, channels: number, sampleRate: number, bitrate: number) => number;
let getEncoderFrameSize: (ctx: number) => number;
let getEncodeInputPtr: (ctx: number, size: number) => number;
let encodeFrameFn: (ctx: number, pts: number) => number;
let encodeFrameFn: (ctx: number, pts: bigint) => number;
let flushEncoderFn: (ctx: number) => void;
let getEncodedData: (ctx: number) => number;
let getEncodedPts: (ctx: number) => number;
let getEncodedPts: (ctx: number) => bigint;
let getEncodedDuration: (ctx: number) => number;
let closeEncoderFn: (ctx: number) => void;
@@ -53,23 +53,23 @@ const ensureModule = async () => {
initDecoderFn = module.cwrap('init_decoder', 'number', ['number']);
configureDecodePacket = module.cwrap('configure_decode_packet', 'number', ['number', 'number']);
decodePacket = module.cwrap('decode_packet', 'number', ['number', 'number']);
decodePacket = module.cwrap('decode_packet', 'number', ['number', 'number']) as unknown as typeof decodePacket;
getDecodedFormat = module.cwrap('get_decoded_format', 'number', ['number']);
getDecodedPlanePtr = module.cwrap('get_decoded_plane_ptr', 'number', ['number', 'number']);
getDecodedChannels = module.cwrap('get_decoded_channels', 'number', ['number']);
getDecodedSampleRate = module.cwrap('get_decoded_sample_rate', 'number', ['number']);
getDecodedSampleCount = module.cwrap('get_decoded_sample_count', 'number', ['number']);
getDecodedPts = module.cwrap('get_decoded_pts', 'number', ['number']);
getDecodedPts = module.cwrap('get_decoded_pts', 'number', ['number']) as unknown as typeof getDecodedPts;
flushDecoderFn = module.cwrap('flush_decoder', null, ['number']);
closeDecoderFn = module.cwrap('close_decoder', null, ['number']);
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number', 'number']);
getEncoderFrameSize = module.cwrap('get_encoder_frame_size', 'number', ['number']);
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
encodeFrameFn = module.cwrap('encode_frame', 'number', ['number', 'number']);
encodeFrameFn = module.cwrap('encode_frame', 'number', ['number', 'number']) as unknown as typeof encodeFrameFn;
flushEncoderFn = module.cwrap('flush_encoder', null, ['number']);
getEncodedData = module.cwrap('get_encoded_data', 'number', ['number']);
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']);
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']) as unknown as typeof getEncodedPts;
getEncodedDuration = module.cwrap('get_encoded_duration', 'number', ['number']);
closeEncoderFn = module.cwrap('close_encoder', null, ['number']);
}
@@ -108,7 +108,7 @@ const decode = (ctx: number, encodedData: ArrayBuffer, timestamp: number) => {
module.HEAPU8.set(bytes, dataPtr);
const ret = decodePacket(ctx, timestamp);
const ret = decodePacket(ctx, BigInt(timestamp));
if (ret < 0) {
throw new Error(`Decode failed with error code ${ret}.`);
}
@@ -122,7 +122,7 @@ const decode = (ctx: number, encodedData: ArrayBuffer, timestamp: number) => {
const channels = getDecodedChannels(ctx);
const sampleRate = getDecodedSampleRate(ctx);
const sampleCount = getDecodedSampleCount(ctx);
const pts = getDecodedPts(ctx);
const pts = Number(getDecodedPts(ctx));
let pcmData: ArrayBuffer;
if (info.planar) {
@@ -169,14 +169,14 @@ const encode = (ctx: number, audioData: ArrayBuffer, timestamp: number) => {
}
module.HEAPU8.set(audioBytes, inputPtr);
const bytesWritten = encodeFrameFn(ctx, timestamp);
const bytesWritten = encodeFrameFn(ctx, BigInt(timestamp));
if (bytesWritten < 0) {
throw new Error(`Encode failed with error code ${bytesWritten}.`);
}
const ptr = getEncodedData(ctx);
const encodedData = module.HEAPU8.slice(ptr, ptr + bytesWritten).buffer;
const pts = getEncodedPts(ctx);
const pts = Number(getEncodedPts(ctx));
const duration = getEncodedDuration(ctx);
return { encodedData, pts, duration };
+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,'
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/flac-encoder",
"author": "Vanilagy",
"version": "1.42.0-beta.6",
"version": "1.50.4",
"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;
}
+5 -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);
+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.6",
"version": "1.50.4",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",

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