Compare commits

...
117 Commits
Author SHA1 Message Date
Vanilagy e3906a3e36 Bump patch AGAIN 2026-02-12 18:54:42 +01:00
Vanilagy 46bd70b880 Bump patch, add bunny 2026-02-12 18:50:00 +01:00
Vanilagy 31b278fbd4 Fix linting errors, fix test not creating bundle 2026-02-12 18:37:55 +01:00
Vanilagy d3b737b183 Bump minor 2026-02-12 18:32:32 +01:00
Vanilagy a97200f3f0 Add @mediabunny/ac3 extension package 2026-02-12 18:31:59 +01:00
Vanilagy e3aa7108f5 Make test 150% faster 2026-02-12 12:22:25 +01:00
Vanilagy b717cbf68c Bump minor 2026-02-12 12:06:25 +01:00
Vanilagy 7cc26edacb Add Mediabunny Codec Registry 2026-02-12 11:56:37 +01:00
Vanilagy de9a4bbc23 Update codec soup 2026-02-11 16:03:57 +01:00
Vanilagy 4fae45ec40 Allow negative trim ranges for conversion, add OutputFormat.supportsTimestampedMediaData 2026-02-11 15:56:51 +01:00
417c85d8ed feat(codec): add detection support for AC-3, E-AC-3 audio codecs (#275)
* ac3/eac3 detection

* fix tests

* review pass + follow codebase comments style

* Add ac3/eac3 to docs

* WIP mpeg-2

* Set registration_descriptor

* add support for ac3 system b in mpeg-ts

* Update docs with ac3 in .ts support

* fix formatting

* Remove default to fix linter

* Add eac3 to mpeg-ts

* fix formatting

* Fix failing test

* Refactor AC3/EAC3 code a little

---------

Co-authored-by: Vanilagy <[email protected]>
2026-02-11 15:23:12 +01:00
Vanilagy 612642c5ad Remove sponsor 2026-02-11 08:38:49 +01:00
Vanilagy 2298f4a8b3 Add jepcd sponsor 2026-02-10 17:07:57 +01:00
David P.andGitHub a613a870c8 Merge pull request #296 from yonathan06/patch-1
Update gling.svg
2026-02-10 16:24:26 +01:00
YonatanandGitHub b61b2b326f Update gling.svg 2026-02-10 16:45:44 +02:00
Vanilagy cacbc2a399 Fix assertion failure 2026-02-07 10:35:40 +01:00
Vanilagy ab0fd6a8d8 Fix WAVE demuxer out of bounds reads 2026-02-07 10:28:14 +01:00
Vanilagy 6a099b4259 Bump minor 2026-02-04 18:28:34 +01:00
Vanilagy 172b4a1fd2 Add two new sponsors 2026-02-04 16:54:45 +01:00
David P.andGitHub 43c1ea7efd Merge pull request #292 from fredrikj/parallelism
Add UrlSourceOptions.parallelism
2026-02-03 16:03:55 +01:00
Vanilagy cab92c55ec Fix a few things 2026-02-03 16:01:46 +01:00
Vanilagy 81bc9d7c44 Fix MP3 files with initial XING frame not being properly recognized (fixes #294) 2026-02-03 15:53:51 +01:00
Fredrik Johansson 3209363470 Add UrlSourceOptions.parallelism 2026-01-29 09:31:51 +01:00
Vanilagy d16b1927ad Conversion: compute duration using only the utilized tracks 2026-01-27 15:54:20 +01:00
Vanilagy f620a328e5 Add ElevenLabs sponsor, bump minor 2026-01-27 14:36:02 +01:00
Vanilagy 7aa0fa465e Add support for ID3 metadata in ADTS, make MPEG-TS demuxer more resilient 2026-01-27 14:32:16 +01:00
Vanilagy cc5f7d8e8f Fixed incorrect segment logic in MPEG-TS metadata extraction, remove unused code, make packetization not require a track backing 2026-01-23 18:05:32 +01:00
Vanilagy 7e1c93c331 Skip transparency tests for now 2026-01-23 13:50:18 +01:00
Vanilagy 5b88332cb8 Test again 2026-01-23 13:41:15 +01:00
Vanilagy 9a70ccbacd Does this fix it? 2026-01-23 13:39:14 +01:00
Vanilagy 8c67872e83 Add double loading detection and warning, improve UrlSource abort signal documentation, bump minor 2026-01-23 13:31:59 +01:00
Vanilagy 7f4093fd43 Add cronischarles-del sponsor 2026-01-23 13:18:10 +01:00
Vanilagy b63db955f6 Merge branch 'main' of https://github.com/Vanilagy/mediabunny 2026-01-23 13:16:41 +01:00
Vanilagy 92454b28f7 Use random_access_indicator for key packet detection, rewrite packet lookup algorithm to use a chunked approach to perform better over the network 2026-01-23 13:16:41 +01:00
Vanilagy db3cb143fe Sure thing ts-doc 2026-01-22 09:43:20 +01:00
Vanilagy e20f779ede Add InputTrack.number 2026-01-22 09:18:43 +01:00
Vanilagy e1e32c9505 Up the timeout 2026-01-20 20:12:17 +01:00
Vanilagy 79f19b6717 Fix MPEG-TS demuxer reading entire file in the metadata phase, optimize it a bit, bump patch 2026-01-20 20:08:17 +01:00
Vanilagy 98387258a4 Add unhandled throws 2026-01-20 08:49:49 +01:00
Vanilagy d859b39468 Bump minor 2026-01-19 15:26:13 +01:00
David P.andGitHub 2afbec4e08 Merge pull request #281 from Vanilagy/mpeg-ts
MPEG Transport Stream read/write support
2026-01-19 15:13:54 +01:00
Vanilagy ab3a514884 Cmon 2026-01-19 15:04:39 +01:00
Vanilagy 277290abe3 Fix tests and elaborate on point in docs 2026-01-19 14:11:02 +01:00
Vanilagy dbaaea255c Expand packet.clone(), make conversion start timestamp default to the input's start timestamp 2026-01-19 14:04:20 +01:00
Vanilagy d23b0369f3 Fix side data not being copied when calling EncodedPacket.clone 2026-01-19 13:32:47 +01:00
Vanilagy 6c78a0ff09 Fix HEVC not being encodable in CI 2026-01-19 10:25:57 +01:00
Vanilagy e7b3437b4b Add stuff 2026-01-19 09:29:54 +01:00
Vanilagy c3a3eb6abf Merge remote-tracking branch 'origin/main' into mpeg-ts 2026-01-16 21:04:14 +01:00
Vanilagy 46fbfc8a2c Make MPEG-TS muxing tests not rely on AAC encoder 2026-01-16 21:02:12 +01:00
Vanilagy 3113e449d4 Fix? 2026-01-16 20:55:39 +01:00
Vanilagy 58053ed00c Bruh 2026-01-16 20:53:40 +01:00
Vanilagy 20e4610da4 Document MPEG-TS 2026-01-16 20:51:27 +01:00
Vanilagy 53b04619e1 Convert ADTS to AAC in muxers that accept it 2026-01-16 17:27:50 +01:00
Vanilagy 2695921ed4 Allow .ts files to be picked in examples 2026-01-16 17:08:19 +01:00
Vanilagy 7e8f9fbbb1 It's 2026 2026-01-16 17:02:12 +01:00
Vanilagy 41e012f1e8 Fix typecheck, lint and build 2026-01-16 17:00:01 +01:00
Vanilagy 282751ec1c Set discontinuity_indicator to 1 for the first packet 2026-01-16 16:56:50 +01:00
Vanilagy e8b27768a9 Add MPEG-TS muxer, fix a demuxer bug, remove Infinity from output format track counts 2026-01-16 16:33:28 +01:00
Vanilagy ae37973878 MPEG-TS demuxer optimizations: faster Annex B segmentation, better NALU iteration 2026-01-16 13:26:58 +01:00
Vanilagy e3ee9d91a9 Add MP3 support to MPEG-TS demuxer 2026-01-16 10:28:00 +01:00
Vanilagy c0f2693cb3 Add HEVC support to MPEG-TS demuxer 2026-01-16 10:05:40 +01:00
Vanilagy d8d6ccc1f7 Update internalCodecId docs 2026-01-15 16:46:13 +01:00
Vanilagy dc25b5d7c5 No metadata for now 2026-01-15 16:44:22 +01:00
Vanilagy 54dfa75a7e Add proper packet duration extraction to MPEG-TS demuxer 2026-01-15 16:33:17 +01:00
Vanilagy 8fea9671ea Fix some demuxer bugs that occurred on files by FFmpeg, fix packet marking logic 2026-01-15 08:51:07 +01:00
Vanilagy 0719a17c85 Add mutex to packet lookup algorithm, fix incorrect next packet retrieval following packet lookup 2026-01-14 21:53:23 +01:00
Vanilagy 4a85aad012 Read only headers, no payload, when possible 2026-01-14 17:10:30 +01:00
Vanilagy 301a6adb1d Claude come on 2026-01-14 16:01:59 +01:00
Vanilagy 045df3b9a5 Make ADTS demuxer spit out ADTS packets instead of AAC, make ADTS muxer accept both AAC and ADTS input 2026-01-14 13:52:46 +01:00
Vanilagy 06ab9c8433 Add support for different TS packet strides and offsets, add proper input format detection, fix error messages, add packet lookup algorithm with no file size known 2026-01-14 13:16:40 +01:00
Vanilagy 888441232d Implement the bulk of the MPEG-TS demuxer, add a bunch of tests, and a few other things 2026-01-14 12:05:39 +01:00
Vanilagy 868578685d Shoulda bumped minor, oops 2026-01-14 09:22:43 +01:00
Vanilagy e1478f8ebd Bump patch 2026-01-14 09:21:51 +01:00
David P.andGitHub 8aac07eb61 Merge pull request #279 from samohovets/bugfix/vorbis-eos
Fix OGG Vorbis decoding error for files with empty EOS pages
2026-01-13 17:36:09 +01:00
Vanilagy 267c09abf8 Return null earlier on 2026-01-13 17:34:33 +01:00
Igor Samokhovets a89101ff40 Fix OGG Vorbis decoding error for files with empty EOS pages (VLC-encoded) 2026-01-13 13:45:43 +01:00
Vanilagy 2ab1be3834 Add maximumPageDuration Ogg muxer option (closes #277) 2026-01-13 13:37:34 +01:00
Vanilagy 571fbb3198 Fix too many tracks being removed 2026-01-12 16:47:31 +01:00
Vanilagy 32d8b1544d Bump patch 2026-01-12 16:15:35 +01:00
Vanilagy b8f104d8b4 Fix disabled Matroska tracks crashing demuxer (closes #276) 2026-01-12 16:11:23 +01:00
Vanilagy 7db0583058 Fix samples not being closed sometimes in conversion (closes #272) 2026-01-12 15:20:10 +01:00
Vanilagy 24af125415 Make Conversion.cancel() cause execute() to throw (closes #271) 2026-01-12 15:13:00 +01:00
Vanilagy 78e78e5b4f Make _flushOrWaitForOngoingClose set closingPromise (fixes #270) 2026-01-12 14:36:51 +01:00
David P.andGitHub 1d774b5fbb Merge pull request #274 from alakhpc/alakhpc/fix-out-of-bounds
fix(matroska): handle insufficient bytes when reading EBML element headers
2026-01-12 14:09:06 +01:00
Vanilagy d6b518b9fd Fix thing 2026-01-12 14:07:43 +01:00
David P.andGitHub 4f5f30bc03 Merge pull request #273 from alakhpc/alakhpc/defer-defaultduration
fix(matroska): defer DefaultDuration calculation until TimestampScale is known
2026-01-12 13:48:41 +01:00
alakhpcandGitHub f6ca3ece50 Merge branch 'main' into alakhpc/defer-defaultduration 2026-01-12 06:49:36 -05:00
Vanilagy 7b036fe8ac Fix some things 2026-01-12 11:59:37 +01:00
Vanilagy 2bb84fd35b Add new sponsors 2026-01-12 10:32:43 +01:00
Alakh Chandarana 91e7812fb2 fix 2026-01-11 16:51:28 -05:00
Alakh Chandarana d29f4f33b5 implement fix 2026-01-11 04:15:03 -05:00
Vanilagy 7580935639 Bump patch 2026-01-06 13:36:29 +01:00
Vanilagy 53824536fa Remove packet iteration upper bound for mediaSamplesInRange (fixes #269) 2026-01-06 13:35:52 +01:00
Vanilagy f363fe50e2 WIP 2026-01-05 11:41:32 +01:00
Vanilagy 8f43086e3d Make allocationSize and copyTo always throw if internal format is null (closes #267) 2026-01-05 11:07:32 +01:00
Vanilagy 9343607fe1 Add deil sponsor 2026-01-02 12:33:35 +01:00
Vanilagy f877fabc60 Bump patch 2025-12-30 16:20:52 +01:00
Vanilagy a2115b1de7 Add workaround for WebKit AudioData.copyTo bug (fixes #260) 2025-12-30 16:10:53 +01:00
Vanilagy 8d46d50c39 Document that ADTS is append-only 2025-12-30 14:11:32 +01:00
Vanilagy 19219fc1ce Add Polotno sponsor 2025-12-29 20:28:13 +01:00
Vanilagy 05070f7c88 Bump patch 2025-12-24 14:17:46 +01:00
Vanilagy 736cc50fa7 Fix aborted UrlSource not aborting request 2025-12-24 14:15:31 +01:00
Vanilagy 9ba63f5d95 Add Roman Rädle 2025-12-23 20:16:43 +01:00
Vanilagy 17c6bff1dc Bump patch 2025-12-19 16:05:46 +01:00
Vanilagy c86af052f5 Add test for Annex B to length-prefixed conversion 2025-12-19 16:04:27 +01:00
Vanilagy 831e838f74 Implement workaround for https://issues.chromium.org/issues/470109459 2025-12-19 13:06:11 +01:00
Vanilagy b730a24138 Bump minor version 2025-12-17 15:33:00 +01:00
Vanilagy ccbf14565c (Hopefully) improve error messages and stack traces a bit more (see #244) 2025-12-17 15:31:51 +01:00
Vanilagy 15bdd072e1 Add options to VideoSample allocationSize and copyTo methods, add explicit error message when format is null (closes #256) 2025-12-17 15:10:25 +01:00
4dd747a0a6 Conversion.init(hardwareAcceleration) option (#254)
* Support Conversion.init(hardwareAcceleration) option passed down to the VideoEncodingConfig.

fix #253

* Few changes

---------

Co-authored-by: Raphaël Droz <[email protected]>
Co-authored-by: Vanilagy <[email protected]>
2025-12-17 10:28:29 +00:00
Vanilagy 228721d68f Add VideoSampleColorSpace 2025-12-16 18:47:50 +01:00
Jonny BurgerandGitHub ea9ba96ae3 Add Dark Mode Remotion Logo in README (#249)
* Add Dark Mode Remotion Logo in README

* Update logo on website
2025-12-12 10:44:57 +01:00
Vanilagy f9207396d7 Bump minor 2025-12-11 20:42:31 +01:00
Vanilagy 84dec01dfd Add pause and resume methods to MediaStreamTrack sources (closes #241) 2025-12-11 15:51:18 +01:00
Vanilagy 1ff8abdaab "Extract function" ahh 2025-12-11 15:24:19 +01:00
Vanilagy cc649ba192 Add allowRotationMetadata video conversion option (see #247) 2025-12-11 14:41:00 +01:00
Vanilagy b73bf06ba7 Add sponsor 2025-12-11 14:05:01 +01:00
174 changed files with 11583 additions and 900 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ jobs:
- name: Upload build artifacts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${{ github.event.release.tag_name }} dist/bundles/mediabunny.cjs dist/bundles/mediabunny.min.cjs dist/bundles/mediabunny.mjs dist/bundles/mediabunny.min.mjs dist/mediabunny.d.ts packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.mjs packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.mjs packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
run: gh release upload ${{ github.event.release.tag_name }} dist/bundles/mediabunny.cjs dist/bundles/mediabunny.min.cjs dist/bundles/mediabunny.mjs dist/bundles/mediabunny.min.mjs dist/mediabunny.d.ts packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.mjs packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.mjs packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts packages/ac3/dist/bundles/mediabunny-ac3.js packages/ac3/dist/bundles/mediabunny-ac3.min.js packages/ac3/dist/bundles/mediabunny-ac3.mjs packages/ac3/dist/bundles/mediabunny-ac3.min.mjs packages/ac3/dist/mediabunny-ac3.d.ts
- name: Publish Mediabunny to npm
run: npm publish --access public
+2 -1
View File
@@ -6,4 +6,5 @@ node_modules
/docs/api
*.tsbuildinfo
packages/mp3-encoder/dist
packages/mp3-encoder/dist
packages/ac3/dist
+6
View File
@@ -0,0 +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.
- Avoid ifs without a {} block. So no if (cond) return;, always do if (cond) { return; }
- `type` instead of `interface` for object types
+9 -2
View File
@@ -17,7 +17,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
<div align="center">
<a href="https://remotion.dev/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/remotion.png" width="60" height="60" alt="Remotion">
<picture>
<source srcset="./docs/public/sponsors/remotion-dark.png" media="(prefers-color-scheme: dark)">
<img src="./docs/public/sponsors/remotion-light.png" width="60" height="60" alt="Remotion">
</picture>
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.gling.ai/" target="_blank" rel="sponsored">
@@ -48,6 +51,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
### Bronze sponsors
<div align="center">
<a href="https://elevenlabs.io/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/elevenlabs.png" width="40" height="40" alt="ElevenLabs">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.reactvideoeditor.com/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/rve.png" width="40" height="40" alt="React Video Editor">
</a>
@@ -67,7 +74,7 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
Core features include:
- **Wide format support**: Read and write MP4, MOV, WebM, MKV, WAVE, MP3, Ogg, ADTS, FLAC
- **Wide format support**: Read and write MP4, MOV, WebM, MKV, 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
+12 -6
View File
@@ -2,9 +2,11 @@
<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 type="module">
//MediabunnyMp3Encoder.registerMp3Encoder();
MediabunnyAc3.registerAc3Encoder();
const fileInput = document.createElement('input');
fileInput.type = 'file';
@@ -56,7 +58,8 @@
}),
output,
audio: (_, n) => ({
discard: n > 1,
codec: 'eac3',
//discard: n > 1,
//codec: 'opus',
//codec: 'opus',
/*
@@ -100,9 +103,12 @@
},
*/
video: () => ({
width: 720,
frameRate: 30,
bitrate: Mediabunny.QUALITY_VERY_LOW,
width: 320,
//forceTranscode: true,
//allowRotationMetadata: false,
//width: 720,
//frameRate: 30,
//bitrate: Mediabunny.QUALITY_VERY_LOW,
//discard: true,
/*
process: (sample) => {
@@ -179,8 +185,8 @@
}
},
trim: {
start: 0,
end: 20
//start: 0,
end: 10
},
});
console.log(conversion);
+40 -4
View File
@@ -1,8 +1,11 @@
<!DOCTYPE html>
<script src="../dist/bundles/mediabunny.cjs"></script>
<script src="../packages/ac3/dist/bundles/mediabunny-ac3.js"></script>
<script type="module">
MediabunnyAc3.registerAc3Decoder();
const fileInput = document.createElement('input');
fileInput.type = 'file';
document.body.append(fileInput);
@@ -14,13 +17,46 @@
source: new Mediabunny.BlobSource(file),
});
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
for (let i = 0; i < 1000; i++) {
const track = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.AudioSampleSink(track);
for await (const sample of sink.samples()) {
sample.close();
}
for await (const packet of sink.packets()) {
console.log(packet);
console.log(i);
}
console.log("Done")
/*
//const secondPacket = await sink.getNextPacket(firstPacket);
//const thirdPacket = await sink.getNextPacket(secondPacket);
//const fourthPacket = await sink.getNextPacket(thirdPacket);
//const fifthPacket = await sink.getNextPacket(fourthPacket);
//const sixthPacket = await sink.getNextPacket(fifthPacket);
//const seventhPacket = await sink.getNextPacket(sixthPacket);
//const eighthPacket = await sink.getNextPacket(seventhPacket);
//const ninthPacket = await sink.getNextPacket(eighthPacket);
//const tenthPacket = await sink.getNextPacket(ninthPacket);
console.log(firstPacket, secondPacket, thirdPacket, fourthPacket, fifthPacket, sixthPacket, seventhPacket, eighthPacket, ninthPacket, tenthPacket);
const packets = [firstPacket, secondPacket, thirdPacket, fourthPacket, fifthPacket, sixthPacket, seventhPacket, eighthPacket, ninthPacket, tenthPacket];
const videoDecoder = new VideoDecoder({
output: (frame) => console.log(frame.timestamp, frame),
error: console.error,
});
videoDecoder.configure(await videoTrack.getDecoderConfig());
for (const packet of packets) {
//videoDecoder.decode(packet.toEncodedVideoChunk());
}
*/
/*
const videoTrack = await input.getPrimaryVideoTrack();
+18 -6
View File
@@ -25,29 +25,41 @@
target: new Mediabunny.BufferTarget(),
format: new Mediabunny.Mp4OutputFormat(),
});
let videoSource = null;
let audioSource = null;
if (videoTrack) {
const source = new Mediabunny.MediaStreamVideoTrackSource(videoTrack, {
videoSource = new Mediabunny.MediaStreamVideoTrackSource(videoTrack, {
codec: 'avc',
bitrate: Mediabunny.QUALITY_MEDIUM
});
source.errorPromise.catch((d) => console.log("Hello?????", d));
videoSource.errorPromise.catch((d) => console.log("Hello?????", d));
output.addVideoTrack(source);
output.addVideoTrack(videoSource);
}
if (audioTrack) {
const source = new Mediabunny.MediaStreamAudioTrackSource(audioTrack, {
audioSource = new Mediabunny.MediaStreamAudioTrackSource(audioTrack, {
codec: 'mp3',
bitrate: Mediabunny.QUALITY_MEDIUM
});
source.errorPromise.catch((d) => console.log("Hello!!???", d));
audioSource.errorPromise.catch((d) => console.log("Hello!!???", d));
output.addAudioTrack(source);
output.addAudioTrack(audioSource);
}
await output.start();
setTimeout(() => {
videoSource?.pause();
audioSource?.pause();
setTimeout(() => {
videoSource?.resume();
audioSource?.resume();
}, 1000);
}, 1000);
await new Promise(resolve => setTimeout(resolve, 5000));
await output.finalize();
+45 -3
View File
@@ -42,6 +42,12 @@ export default withMermaid({
{ text: 'Examples', link: '/examples', activeMatch: '/examples' },
{ text: 'Sponsors', link: '/#sponsors', activeMatch: '/#sponsors' },
{ text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license' },
{
text: 'More',
items: [
{ text: 'Codec Registry', link: '/codec-registry/overview' },
],
},
],
sidebar: {
@@ -87,11 +93,47 @@ export default withMermaid({
text: 'Extensions',
items: [
{ text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' },
{ text: 'ac3', link: '/guide/extensions/ac3' },
],
},
],
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
'/api': apiRoutes as any,
'/codec-registry': [
{
text: 'Codec registry',
items: [
{ text: 'Overview', link: '/codec-registry/overview' },
],
},
{
text: 'Video',
items: [
{ text: 'AVC (H.264)', link: '/codec-registry/avc' },
{ text: 'HEVC (H.265)', link: '/codec-registry/hevc' },
{ text: 'VP8', link: '/codec-registry/vp8' },
{ text: 'VP9', link: '/codec-registry/vp9' },
{ text: 'AV1', link: '/codec-registry/av1' },
],
},
{
text: 'Audio',
items: [
{ text: 'AAC', link: '/codec-registry/aac' },
{ text: 'Opus', link: '/codec-registry/opus' },
{ text: 'MP3', link: '/codec-registry/mp3' },
{ text: 'Vorbis', link: '/codec-registry/vorbis' },
{ text: 'FLAC', link: '/codec-registry/flac' },
{ text: 'AC-3', link: '/codec-registry/ac3' },
{ text: 'E-AC-3', link: '/codec-registry/eac3' },
{ text: 'Linear PCM', link: '/codec-registry/pcm' },
{ text: 'μ-law PCM', link: '/codec-registry/ulaw' },
{ text: 'A-law PCM', link: '/codec-registry/alaw' },
],
},
],
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
'/api': apiRoutes,
},
socialLinks: [
@@ -111,7 +153,7 @@ export default withMermaid({
footer: {
message: 'Released under the Mozilla Public License 2.0.',
copyright: 'Copyright © 2025-present Vanilagy',
copyright: 'Copyright © 2026-present Vanilagy',
},
},
markdown: {
+2 -1
View File
@@ -19,5 +19,6 @@
"Custom coders": "API for adding custom encoders and decoders.",
"Miscellaneous": "Whatever's left.",
"@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny."
"@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny.",
"@mediabunny/ac3": "Adds AC-3/E-AC-3 decoder and encoder support to Mediabunny."
}
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 141 KiB

+50
View File
@@ -0,0 +1,50 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# AAC codec registration
## Description
The Advanced Audio Coding (AAC) audio codec, specified in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html).
An AAC bitstream can have either of two formats:
- _AAC_ (raw), where packets contain raw AAC frames (syntax element `raw_data_block()`) as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 4.4.2.1. Here, codec metadata is provided out-of-band.
- _ADTS_, where packets contain ADTS frames as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 1.A.3.2. Here, codec metadata is provided in-band in each ADTS frame header.
All packets within the bitstream must have the same format.
## Codec ID
```ts
'aac'
```
## `EncodedPacket` data
If the bitstream is in the _AAC_ (raw) format, the packet's data must be a raw AAC frame (syntax element `raw_data_block()`) as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 4.4.2.1.
If the bitstream is in the _ADTS_ format, the packet's data must be an ADTS frame as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 1.A.3.2.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
The following fully qualified codec strings are recognized:
- `'mp4a.40.2'` — MPEG-4 AAC-LC
- `'mp4a.40.02'` — MPEG-4 AAC-LC (leading zero for Aud-OTI compatibility)
- `'mp4a.40.5'` — MPEG-4 HE-AAC v1 (AAC-LC + SBR)
- `'mp4a.40.05'` — MPEG-4 HE-AAC v1 (leading zero for Aud-OTI compatibility)
- `'mp4a.40.29'` — MPEG-4 HE-AAC v2 (AAC-LC + SBR + PS)
- `'mp4a.67'` — MPEG-2 AAC-LC
## `AudioDecoderConfig` description
If the bitstream is in the _AAC_ (raw) format, `description` must be an `AudioSpecificConfig` as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 1.6.2.1.
If the bitstream is in the _ADTS_ format, `description` must be undefined.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# AC-3 codec registration
## Description
The Dolby Digital (AC-3) audio codec, specified in [ETSI TS 102 366](https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf).
## Codec ID
```ts
'ac3'
```
## `EncodedPacket` data
The packet's data must be a sync frame (syntactic element `syncframe()`) as defined in Section 4.3 of [ETSI TS 102 366](https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf), beginning with the sync word `0x0B77`.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'ac-3'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# A-law PCM codec registration
## Description
The A-law companded PCM audio codec, specified in [ITU-T G.711](https://www.itu.int/rec/T-REC-G.711) Tables 1a and 1b.
## Codec ID
```ts
'alaw'
```
## `EncodedPacket` data
The packet's data must be a sequence of bytes of arbitrary length (divisible by the channel count), where each byte is an A-law encoded PCM sample. For multichannel audio, samples from different channels are interleaved.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'alaw'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+33
View File
@@ -0,0 +1,33 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# AV1 codec registration
## Description
The AOMedia Video 1 (AV1) video codec, specified in the [AV1 Bitstream & Decoding Process Specification](https://aomediacodec.github.io/av1-spec/).
## Codec ID
```ts
'av1'
```
## `EncodedPacket` data
The packet's data must comply with the low-overhead bitstream format as defined in Section 5 of the [AV1 Bitstream & Decoding Process Specification](https://aomediacodec.github.io/av1-spec/).
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain a frame with `frame_type` of `KEY_FRAME`, as defined in Section 6.8.2 of the [AV1 Bitstream & Decoding Process Specification](https://aomediacodec.github.io/av1-spec/).
## `VideoDecoderConfig` codec string
The full codec string begins with the prefix `'av01.'`, with a variable-length suffix as specified in Section 5 of the [AV1 Codec ISO Media File Format Binding](https://aomediacodec.github.io/av1-isobmff/).
## `VideoDecoderConfig` description
`description` is not used for this codec.
+41
View File
@@ -0,0 +1,41 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# AVC (H.264) codec registration
## Description
The Advanced Video Coding (H.264) video codec, specified in [Rec. ITU-T H.264](https://www.itu.int/rec/T-REC-H.264) / [ISO/IEC 14496-10](https://www.iso.org/standard/87574.html).
An AVC bitstream can have either of two formats:
- _Canonical_ (length-prefixed), as defined in [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html) Section 5.3.2. Here, video parameter sets (SPS/PPS) are provided out-of-band.
- _Annex B_, as defined in [Rec. ITU-T H.264](https://www.itu.int/rec/T-REC-H.264) Annex B. Here, video parameter sets (SPS/PPS) must be provided in-band in the respective NALUs.
All packets within the bitstream must have the same format.
## Codec ID
```ts
'avc'
```
## `EncodedPacket` data
The packet's data must be an access unit as defined in [Rec. ITU-T H.264](https://www.itu.int/rec/T-REC-H.264) Section 7.4.1.2, in either _canonical_ or _Annex B_ format.
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain a primary coded picture from which decoding can begin. Additionally, if the bitstream's format is _Annex B_, then this packet is also expected to contain the necessary video parameter sets to initialize the decoder.
## `VideoDecoderConfig` codec string
The full codec string begins with the prefix `'avc1.'` or `'avc3.'`, with a suffix of 6 characters as described respectively in Section 3.4 of [RFC 6381](https://www.rfc-editor.org/rfc/rfc6381) and Section 5.4.1 of [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html).
## `VideoDecoderConfig` description
If the bitstream is in the _canonical_ (length-prefixed) format, `description` must be an `AVCDecoderConfigurationRecord` as defined in [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html) Section 5.3.3.1.
If the bitstream is in the _Annex B_ format, `description` must be undefined.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# E-AC-3 codec registration
## Description
The Dolby Digital Plus (E-AC-3) audio codec, specified in [ETSI TS 102 366](https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf).
## Codec ID
```ts
'eac3'
```
## `EncodedPacket` data
The packet's data must be a sync frame as defined in Section E.1.2.0 of [ETSI TS 102 366](https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf), beginning with the sync word `0x0B77`.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'ec-3'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# FLAC codec registration
## Description
The Free Lossless Audio Codec (FLAC), specified in the [FLAC Format Specification](https://xiph.org/flac/format.html).
## Codec ID
```ts
'flac'
```
## `EncodedPacket` data
The packet's data must be a FLAC frame as described in the [FLAC Format Specification](https://xiph.org/flac/format.html).
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'flac'
```
## `AudioDecoderConfig` description
`description` must contain the bytes `0x66 0x4C 0x61 0x43` (the ASCII string `'fLaC'`), followed by a `STREAMINFO` metadata block as defined in Section 7 of the [FLAC Format Specification](https://xiph.org/flac/format.html).
+41
View File
@@ -0,0 +1,41 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# HEVC (H.265) codec registration
## Description
The High Efficiency Video Coding (H.265) video codec, specified in [Rec. ITU-T H.265](https://www.itu.int/rec/T-REC-H.265) / [ISO/IEC 23008-2](https://www.iso.org/standard/75484.html).
An HEVC bitstream can have either of two formats:
- _Canonical_ (length-prefixed), as defined in [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html) Section 8.3.2. Here, video parameter sets (VPS/SPS/PPS) are provided out-of-band.
- _Annex B_, as defined in [Rec. ITU-T H.265](https://www.itu.int/rec/T-REC-H.265) Annex B. Here, video parameter sets (VPS/SPS/PPS) must be provided in-band in the respective NALUs.
All packets within the bitstream must have the same format.
## Codec ID
```ts
'hevc'
```
## `EncodedPacket` data
The packet's data must be an access unit as defined in [Rec. ITU-T H.265](https://www.itu.int/rec/T-REC-H.265) Section 7.4.2.4, containing exactly one base layer coded picture, in either _canonical_ or _Annex B_ format.
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain an IDR, CRA, or BLA picture. Additionally, if the bitstream's format is _Annex B_, then this packet is also expected to contain the necessary video parameter sets to initialize the decoder.
## `VideoDecoderConfig` codec string
The full codec string begins with the prefix `'hev1.'` or `'hvc1.'`, with a variable-length suffix of four dot-separated fields as specified in Section E.3 of [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html).
## `VideoDecoderConfig` description
If the bitstream is in the _canonical_ (length-prefixed) format, `description` must be an `HEVCDecoderConfigurationRecord` as defined in [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html) Section 8.3.3.1.
If the bitstream is in the _Annex B_ format, `description` must be undefined.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# MP3 codec registration
## Description
The MP3 audio codec (MPEG-1/2 Audio Layer III), specified in [ISO/IEC 13818-3](https://www.iso.org/standard/26797.html).
## Codec ID
```ts
'mp3'
```
## `EncodedPacket` data
The packet's data must be an MP3 frame as described in Section 2.4.2.2 of [ISO/IEC 13818-3](https://www.iso.org/standard/26797.html).
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'mp3'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# Opus codec registration
## Description
The Opus audio codec, specified in [RFC 6716](https://www.rfc-editor.org/rfc/rfc6716).
## Codec ID
```ts
'opus'
```
## `EncodedPacket` data
The packet's data must be an Opus packet as described in Section 3 of [RFC 6716](https://www.rfc-editor.org/rfc/rfc6716).
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'opus'
```
## `AudioDecoderConfig` description
If present, `description` must be an Identification Header as defined in Section 5.1 of [RFC 7845](https://www.rfc-editor.org/rfc/rfc7845).
+26
View File
@@ -0,0 +1,26 @@
# 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.
The registry is an extension of the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). Mediabunny's registry matches that of WebCodecs for all codecs supported by both.
## Video codecs
- [AVC (H.264)](./avc)
- [HEVC (H.265)](./hevc)
- [VP8](./vp8)
- [VP9](./vp9)
- [AV1](./av1)
## Audio codecs
- [AAC](./aac)
- [Opus](./opus)
- [MP3](./mp3)
- [Vorbis](./vorbis)
- [FLAC](./flac)
- [AC-3](./ac3)
- [E-AC-3](./eac3)
- [Linear PCM](./pcm)
- [μ-law PCM](./ulaw)
- [A-law PCM](./alaw)
+44
View File
@@ -0,0 +1,44 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# Linear PCM codecs registration
## Description
A family of linear pulse-code modulation (PCM) audio codecs of various bit depths and byte orders.
## Codec IDs
| Codec ID | Description |
| --- | --- |
| `'pcm-u8'` | Unsigned 8-bit integer |
| `'pcm-s8'` | Signed 8-bit integer |
| `'pcm-s16'` | Signed 16-bit integer, little-endian |
| `'pcm-s16be'` | Signed 16-bit integer, big-endian |
| `'pcm-s24'` | Signed 24-bit integer, little-endian |
| `'pcm-s24be'` | Signed 24-bit integer, big-endian |
| `'pcm-s32'` | Signed 32-bit integer, little-endian |
| `'pcm-s32be'` | Signed 32-bit integer, big-endian |
| `'pcm-f32'` | 32-bit float, little-endian |
| `'pcm-f32be'` | 32-bit float, big-endian |
| `'pcm-f64'` | 64-bit float, little-endian |
| `'pcm-f64be'` | 64-bit float, big-endian |
## `EncodedPacket` data
The packet's data must be a sequence of bytes of arbitrary length (divisible by the size of one frame), with each sample occupying the number of bits defined by the codec ID. For multichannel audio, samples from different channels are interleaved.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
The codec string is the same as the codec ID (e.g. `'pcm-s16'`, `'pcm-f32'`).
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# μ-law PCM codec registration
## Description
The μ-law companded PCM audio codec, specified in [ITU-T G.711](https://www.itu.int/rec/T-REC-G.711) Tables 2a and 2b.
## Codec ID
```ts
'ulaw'
```
## `EncodedPacket` data
The packet's data must be a sequence of bytes of arbitrary length (divisible by the channel count), where each byte is a μ-law encoded PCM sample. For multichannel audio, samples from different channels are interleaved.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'ulaw'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# Vorbis codec registration
## Description
The Vorbis audio codec, specified in the [Vorbis I Specification](https://xiph.org/vorbis/doc/Vorbis_I_spec.html).
## Codec ID
```ts
'vorbis'
```
## `EncodedPacket` data
The packet's data must be an audio packet as described in Section 4.3 of the [Vorbis I Specification](https://xiph.org/vorbis/doc/Vorbis_I_spec.html).
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'vorbis'
```
## `AudioDecoderConfig` description
`description` must contain Vorbis codec setup data in Xiph extradata format: the `page_segments` field, followed by the `segment_table` field, followed by the three Vorbis header packets (identification header, comments header, and setup header) as defined in Section 4.2 of the [Vorbis I Specification](https://xiph.org/vorbis/doc/Vorbis_I_spec.html).
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# VP8 codec registration
## Description
The VP8 video codec, specified in [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386).
## Codec ID
```ts
'vp8'
```
## `EncodedPacket` data
The packet's data must be a frame as described in Section 4 and Annex A of [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386).
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain a frame where `key_frame` is `true`, as defined in Section 19.1 of [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386).
## `VideoDecoderConfig` codec string
```ts
'vp8'
```
## `VideoDecoderConfig` description
`description` is not used for this codec.
+33
View File
@@ -0,0 +1,33 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# VP9 codec registration
## Description
The VP9 video codec, specified in the [VP9 Bitstream & Decoding Process Specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf).
## Codec ID
```ts
'vp9'
```
## `EncodedPacket` data
The packet's data must be a frame as described in Section 6 of the [VP9 Bitstream & Decoding Process Specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf).
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain a frame with `frame_type` of `KEY_FRAME`, as defined in Section 7.2 of the [VP9 Bitstream & Decoding Process Specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf).
## `VideoDecoderConfig` codec string
The full codec string begins with the prefix `'vp09.'`, with a variable-length suffix as specified in the [VP Codec ISO Media File Format Binding](https://www.webmproject.org/vp9/mp4/).
## `VideoDecoderConfig` description
`description` is not used for this codec.
+26 -6
View File
@@ -108,7 +108,7 @@ Sometimes, you may want to cancel an ongoing conversion process. For this, use t
await conversion.cancel(); // Resolves once the conversion is canceled
```
This automatically frees up all resources used by the conversion process.
This automatically frees up all resources used by the conversion process and will cause any ongoing call to `execute` to throw a `ConversionCanceledError`.
## Video options
@@ -120,11 +120,13 @@ type ConversionVideoOptions = {
height?: number;
fit?: 'fill' | 'contain' | 'cover';
rotate?: 0 | 90 | 180 | 270;
allowRotationMetadata?: boolean;
crop?: { left: number; top: number; width: number; height: number };
frameRate?: number;
codec?: VideoCodec;
bitrate?: number | Quality;
alpha?: 'discard' | 'keep'; // Defaults to 'discard'
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
keyFrameInterval?: number;
forceTranscode?: boolean;
process?: (sample: VideoSample) => MaybePromise<
@@ -175,6 +177,8 @@ In the rare case that the input video changes size over time, the `fit` field ca
`rotation` rotates the video by the specified number of degrees clockwise. This rotation is applied on top of any rotation metadata in the original input file and happens before cropping and resizing.
By default, Mediabunny will try to make use of rotation metadata in the output file to perform the rotation whenever possible. However, if you don't want this to happen, or you want to use Mediabunny to strip all rotation metadata from a file, you can set `allowRotationMetadata` to `false`.
### Cropping video
`crop` can be used to extract a rectangular region from the original video. The rectangle is specified using `left`, `top`, `width` and `height` and is clamped to the dimensions of the video. Cropping is applied after rotation but before resizing.
@@ -190,9 +194,10 @@ Use the `codec` property to control the codec of the output track. This should b
Use the `bitrate` property to control the bitrate of the output video. For example, you can use this field to compress the video track. Accepted values are the number of bits per second or a [subjective quality](./media-sources#subjective-qualities). If this property is set, transcoding will always happen. If this property is not set but transcoding is still required, `QUALITY_HIGH` will be used as the value.
Use the `keyFrameInterval` property to control the maximum interval in seconds between key frames in the output video. Setting this fields forces a transcode.
If you want to prevent direct copying of media data and force a transcoding step, use `forceTranscode: true`.
Use the `hardwareAcceleration` property to control whether hardware or software acceleration is used for video transcoding.
### Processing video
The `process` property can be used to define a custom video sample processing function, e.g. for [applying overlays](./quick-start#add-a-video-overlay), color transformations, or timestamp modifications. You are expected to perform this processing yourself, for example using the Canvas API.
@@ -301,8 +306,8 @@ const conversion = await Conversion.init({
output,
// Function gets invoked for each video track:
video: (videoTrack, n) => {
if (n > 1) {
video: (videoTrack) => {
if (videoTrack.number > 1) {
// Keep only the first video track
return { discard: true };
}
@@ -314,7 +319,7 @@ const conversion = await Conversion.init({
},
// Async functions work too:
audio: async (audioTrack, n) => {
audio: async (audioTrack) => {
if (audioTrack.languageCode !== 'rus') {
// Keep only Russian audio tracks
return { discard: true };
@@ -360,6 +365,21 @@ In this case, the output will be 15 seconds long.
If only `start` is set, the clip will run until the end of the input file. If only `end` is set, the clip will start at the beginning of the input file.
Note that when using the trimming defaults, the resulting media file will always begin at timestamp 0. If your input file has a start time offset (like is common with MPEG-TS files) and you want to retain that, use `trim: { start: 0 }` to ensure timestamps don't get shifted.
---
You can even use negative trimming values to offset the start of the media:
```ts
const conversion = await Conversion.init({
// ...
trim: {
start: -2, // Two seconds of no media data (freeze frame / silence) at the start
},
// ...
});
```
## Metadata tags
By default, any [descriptive metadata tags](../api/MetadataTags.md) of the input will be copied to the output. If you want to further control the metadata tags written to the output, you can use the `tags` options:
@@ -445,4 +465,4 @@ On the flip side, you can always query which input tracks made it into the outpu
```ts
const conversion = await Conversion.init({ input, output });
conversion.utilizedTracks; // => InputTrack[]
```
```
+33
View File
@@ -0,0 +1,33 @@
# @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.
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="https://github.com/Vanilagy/mediabunny/blob/main/packages/ac3/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/ac3
```
Alternatively, directly include them using a script tag:
```html
<script src="mediabunny.js"></script>
<script src="mediabunny-ac3.js"></script>
```
This will expose the global objects `Mediabunny` and `MediabunnyAc3`. Use `mediabunny-ac3.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).
## Usage
```ts
import { registerAc3Decoder, registerAc3Encoder } from '@mediabunny/ac3';
registerAc3Decoder();
registerAc3Encoder();
```
That's it - Mediabunny now uses the registered AC-3/E-AC-3 decoder and encoder automatically.
+2
View File
@@ -32,6 +32,7 @@ import {
OGG, // Ogg input format singleton
ADTS, // ADTS input format singleton
FLAC, // FLAC input format singleton
MPEG_TS, // MPEG-TS input format singleton
} from 'mediabunny';
```
@@ -80,6 +81,7 @@ In addition to singletons, input format classes are structured hierarchically:
- `OggInputFormat`
- `AdtsInputFormat`
- `FlacInputFormat`
- `MpegTsInputFormat`
This means you can also perform input format checks using `instanceof` instead of `===` comparisons. For example:
```ts
+1 -1
View File
@@ -12,7 +12,7 @@ Here's a long list of stuff this library does:
- Converting media files
- Hardware-accelerated decoding & encoding (via the WebCodecs API)
- Support for multiple video, audio and subtitle tracks
- Read & write support for many container formats (.mp4, .mov, .webm, .mkv, .mp3, .wav, .ogg, .aac, .flac), including variations such as MP4 with Fast Start, fragmented MP4, streamable Matroska, transparent WebM, etc.
- Read & write support for many container formats (.mp4, .mov, .webm, .mkv, .mp3, .wav, .ogg, .aac, .flac, .ts), including variations such as MP4 with Fast Start, fragmented MP4, streamable Matroska, transparent WebM, etc.
- Support for 25 different codecs
- Lazy, optimized, on-demand file reading
- Input and output streaming, arbitrary file size support
+24 -4
View File
@@ -74,7 +74,7 @@ type VideoEncodingConfig = {
- `bitrateMode`: Can be used to control constant vs. variable bitrate.
- `latencyMode`: The latency mode as specified by the WebCodecs API. Browsers default to `quality`. Media stream-driven video sources will automatically use the `realtime` setting.
- `keyFrameInterval`: The maximum interval in seconds between two adjacent key frames. Defaults to 5 seconds. More frequent key frames improve seeking behavior but increase file size. When using multiple video tracks, this value should be set to the same value for all tracks.
- `fullCodecString`: Allows you to optionally specify the full codec string used by the video encoder, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). For example, you may set it to `'avc1.42001f'` when using AVC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
- `fullCodecString`: Allows you to optionally specify the full codec string used by the video encoder, as specified in the [Mediabunny Codec Registry](/codec-registry/overview). For example, you may set it to `'avc1.42001f'` when using AVC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
- `hardwareAcceleration`: A hint that configures the hardware acceleration method of this codec. This is best left on `'no-preference'`.
- `scalabilityMode`: An encoding scalability mode identifier as defined by [WebRTC-SVC](https://w3c.github.io/webrtc-svc/#scalabilitymodes*).
- `contentHint`: An encoding video content hint as defined by [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).
@@ -104,7 +104,7 @@ type AudioEncodingConfig = {
- `codec`: The [audio codec](./supported-formats-and-codecs#audio-codecs) used for encoding. Can be omitted for uncompressed PCM codecs.
- `bitrate`: The target number of bits per second. Alternatively, this can be a [subjective quality](#subjective-qualities).
- `bitrateMode`: Can be used to control constant vs. variable bitrate.
- `fullCodecString`: Allows you to optionally specify the full codec string used by the audio encoder, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). For example, you may set it to `'mp4a.40.2'` when using AAC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
- `fullCodecString`: Allows you to optionally specify the full codec string used by the audio encoder, as specified in the [Mediabunny Codec Registry](/codec-registry/overview). For example, you may set it to `'mp4a.40.2'` when using AAC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
- `onEncodedPacket`: Called for each successfully encoded packet. Useful for determining encoding progress.
- `onEncoderConfig`: Called when the internal encoder config, as used by the WebCodecs API, is created. You can use this to introspect the full codec string.
@@ -187,6 +187,16 @@ videoTrackSource.errorPromise.catch((error) => ...);
This source requires no additional method calls; data will automatically be captured and piped to the output file as soon as `start()` is called on the `Output`. Make sure to `stop()` on `videoTrack` after finalizing the `Output` if you don't need the user's media anymore.
If you want to temporarily stop capturing video frames from this source, you can use the `pause()` and `resume()` methods:
```ts
videoTrackSource.pause();
// Later:
videoTrackSource.resume();
```
While paused, video frames emitted by the stream will be ignored. When resumed, video frames are let through again, offset in timestamp such that the result plays back continuously with no gap in playback. Note that pausing does *not* stop the underlying media stream.
::: info
If this source is the only MediaStreamTrack source in the `Output`, then the first video sample added by it starts at timestamp 0. If there are multiple, then the earliest media sample across all tracks starts at timestamp 0, and all tracks will be perfectly synchronized with each other.
:::
@@ -230,7 +240,7 @@ await packetSource.add(firstPacket, {
});
```
`codec`, `codedWidth`, and `codedHeight` are required for all codecs, whereas `description` is required for some codecs. Additional fields, such as `colorSpace`, are optional. The [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/) specifies the formats of `codec` and `description` for each video codec, which you must adhere to.
`codec`, `codedWidth`, and `codedHeight` are required for all codecs, whereas `description` is required for some codecs. Additional fields, such as `colorSpace`, are optional. The [Mediabunny Codec Registry](/codec-registry/overview) specifies the formats of `codec` and `description` for each video codec, which you **must** adhere to.
#### B-frames
@@ -344,6 +354,16 @@ audioTrackSource.errorPromise.catch((error) => ...);
This source requires no additional method calls; data will automatically be captured and piped to the output file as soon as `start()` is called on the `Output`. Make sure to `stop()` on `audioTrack` after finalizing the `Output` if you don't need the user's media anymore.
If you want to temporarily stop capturing audio data from this source, you can use the `pause()` and `resume()` methods:
```ts
audioTrackSource.pause();
// Later:
audioTrackSource.resume();
```
While paused, audio data emitted by the stream will be ignored. When resumed, audio data are let through again, offset in timestamp such that the result plays back continuously with no gap in playback. Note that pausing does *not* stop the underlying media stream.
::: info
If this source is the only MediaStreamTrack source in the `Output`, then the first audio sample added by it starts at timestamp 0. If there are multiple, then the earliest media sample across all tracks starts at timestamp 0, and all tracks will be perfectly synchronized with each other.
:::
@@ -377,7 +397,7 @@ await packetSource.add(firstPacket, {
});
```
`codec`, `numberOfChannels`, and `sampleRate` are required for all codecs, whereas `description` is required for some codecs. The [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/) specifies the formats of `codec` and `description` for each audio codec, which you must adhere to.
`codec`, `numberOfChannels`, and `sampleRate` are required for all codecs, whereas `description` is required for some codecs. The [Mediabunny Codec Registry](/codec-registry/overview) specifies the formats of `codec` and `description` for each audio codec, which you must adhere to.
## Subtitle sources
+34 -2
View File
@@ -194,9 +194,12 @@ This format ensures [append-only writing](#append-only-writing).
The following options are available:
```ts
type OggOutputFormatOptions = {
maximumPageDuration?: number;
onPage?: (data: Uint8Array, position: number, source: MediaSource) => unknown;
};
```
- `maximumPageDuration`\
The maximum duration in seconds of each Ogg page. Pages will be flushed early if adding another packet would cause the page to exceed this duration. This is useful for streaming contexts where more frequent page output is desired. By default, pages are only flushed when they exceed a certain size.
- `onPage`\
Will be called for each finalized Ogg page of the output file. The [media source](./media-sources) backing the page's track (logical bitstream) is also passed.
@@ -272,6 +275,10 @@ const output = new Output({
});
```
::: info
This format ensures [append-only writing](#append-only-writing).
:::
The following options are available:
```ts
type AdtsOutputFormatOptions = {
@@ -285,7 +292,7 @@ type AdtsOutputFormatOptions = {
This output format creates FLAC (.flac) files.
```ts
import { Output, FlacOutputFormat } from 'mediabunny';
import { Output, FlacOutputFormat } from 'mediabunny';
const output = new Output({
format: new FlacOutputFormat(options),
@@ -300,4 +307,29 @@ type FlacOutputFormatOptions = {
};
```
- `onFrame`\
Will be called for each FLAC frame that is written.
Will be called for each FLAC frame that is written.
## MPEG-TS
This output format creates MPEG Transport Stream (.ts) files.
```ts
import { Output, MpegTsOutputFormat } from 'mediabunny';
const output = new Output({
format: new MpegTsOutputFormat(options),
// ...
});
```
::: info
This format ensures [append-only writing](#append-only-writing).
:::
The following options are available:
```ts
type MpegTsOutputFormatOptions = {
onPacket?: (data: Uint8Array, position: number) => unknown;
};
```
- `onPacket`\
Will be called for each 188-byte Transport Stream packet that is written.
+5 -5
View File
@@ -94,6 +94,8 @@ constructor(
);
```
When creating a packet for a given codec, you *must* adhere to the data format specified in the [Mediabunny Codec Registry](/codec-registry/overview).
::: info
You probably won't ever need to set `sequenceNumber` or `byteLength` in the constructor.
:::
@@ -176,7 +178,7 @@ Negative sequence numbers mean the packet's ordering is undefined. When creating
### Cloning packets
Use the `clone` method to create a new packet from an existing packet. While doing so, you can change its timestamp and duration.
Use the `clone` method to create a new packet from an existing packet. While doing so, you can partially change its data.
```ts
// Creates a clone identical to the original:
packet.clone();
@@ -378,13 +380,11 @@ const bytesNeeded = videoSample.allocationSize(); // => number
Then, use `copyTo` to copy the pixel data into the destination buffer:
```ts
const bytes = new Uint8Array(bytesNeeded);
videoSample.copyTo(bytes);
const planeLayout = await videoSample.copyTo(bytes);
```
::: info
The data will always be in the pixel format specified in the `format` field.
To convert the data into a different pixel format, or to extract only a section of the frame, please use the [`allocationSize`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame/allocationSize) and [`copyTo`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame/copyTo) methods on `VideoFrame` instead. Get a `VideoFrame` by running `videoSample.toVideoFrame()`.
You can pass additional options to `allocationSize` and `copyTo` to extract data in a different pixel format.
:::
---
+6 -4
View File
@@ -556,14 +556,16 @@ const output = new Output(...);
const conversion = await Conversion.init({
input,
output,
video: {
video: track => ({
width: 480,
bitrate: QUALITY_LOW,
},
audio: {
discard: track.number > 1, // Keep only the first video track
}),
audio: track => ({
numberOfChannels: 1,
bitrate: QUALITY_LOW,
},
discard: track.number > 1, // Keep only the first audio track
}),
trim: {
// Let's keep only the first 60 seconds
start: 0,
+13 -1
View File
@@ -60,6 +60,11 @@ await input.computeDuration(); // => 1905.4615
```
More specifically, the duration is defined as the maximum end timestamp across all tracks.
Since not all media files begin at time zero, you can also retrieve the *starting timestamp* of the media file in seconds:
```ts
await input.getFirstTimestamp(); // => 0.0
```
Mediabunny also lets you read descriptive metadata tags from media files, such as title, artist, or cover art:
```ts
await input.getMetadataTags(); // => MetadataTags
@@ -93,6 +98,10 @@ Once you have an `InputTrack`, you can start extracting metadata from it.
// Get a unique ID for this track in the input file:
track.id; // => number
// Get the 1-based index of this track among all tracks of the same type
// (e.g., first video track is 1, second video track is 2, etc.):
track.number; // => number
// Check the track's type:
track.type; // => 'video' | 'audio' | 'subtitle';
@@ -120,7 +129,7 @@ track.codec; // => MediaCodec | null
```
This field is `null` when the track's codec couldn't be recognized or is not supported by Mediabunny. See [Codecs](./supported-formats-and-codecs#codecs) for the full list of supported codecs. When Mediabunny doesn't recognize the format, you can still use the `internalCodecId` field to figure out the codec of the track, although its format depends on the container format used and is not homogenized by Mediabunny.
You can also extract the full codec parameter string from the track, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/):
You can also extract the full codec parameter string from the track, as specified in the [Mediabunny Codec Registry](/codec-registry/overview):
```ts
await track.getCodecParameterString(); // => 'avc1.42001f'
```
@@ -497,6 +506,9 @@ type UrlSourceOptions = {
// in memory. Defaults to 8 MiB.
maxCacheSize?: number;
// The maximum number of parallel requests to use for fetching. Defaults to 2.
parallelism?: number;
// Used to provide a custom fetch function
fetchFn?: typeof fetch;
};
+38 -29
View File
@@ -13,6 +13,7 @@ Mediabunny supports many commonly used media container formats, all of which are
- WAVE (.wav)
- ADTS (.aac)
- FLAC (.flac)
- MPEG Transport Stream (.ts)
## Codecs
@@ -20,6 +21,8 @@ Mediabunny supports a wide range of video, audio, and subtitle codecs. More spec
The availability of the codecs provided by the WebCodecs API depends on the browser and thus cannot be guaranteed by this library. Mediabunny provides [special utility functions](#querying-codec-encodability) to check which codecs are able to be encoded. You can also specify [custom coders](#custom-coders) to provide your own encoder/decoder implementation if the browser doesn't support the codec natively.
For precise definitions of each codec including the corresponding packet format, please refer to the [Mediabunny Codec Registry](/codec-registry/overview).
::: info
Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, meaning they are always supported.
:::
@@ -39,6 +42,8 @@ Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, m
- `'mp3'` - MP3
- `'vorbis'` - Vorbis
- `'flac'` - Free Lossless Audio Codec (FLAC)
- `'ac3'` - Dolby Digital (AC-3) [^1]
- `'eac3'` - Dolby Digital Plus (E-AC-3) [^1]
- `'pcm-u8'` - 8-bit unsigned PCM
- `'pcm-s8'` - 8-bit signed PCM
- `'pcm-s16'` - 16-bit little-endian signed PCM
@@ -54,6 +59,8 @@ Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, m
- `'ulaw'` - μ-law PCM
- `'alaw'` - A-law PCM
[^1]: AC-3 and E-AC-3 are not natively supported by WebCodecs. To encode or decode these codecs, you can use the [`@mediabunny/ac3`](./extensions/ac3) extension package, or provide your own [custom coder](#custom-coders).
### Subtitle codecs
- `'webvtt'` - WebVTT
@@ -62,37 +69,39 @@ Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, m
Not all codecs can be used with all containers. The following table specifies the supported codec-container combinations:
| | .mp4 | .mov | .mkv | .webm[^1] | .ogg | .mp3 | .wav | .aac | .flac |
|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|:-----:|:-----:|
| `'avc'` | ✓ | ✓ | ✓ | | | | | | |
| `'hevc'` | ✓ | ✓ | ✓ | | | | | | |
| `'vp8'` | ✓ | ✓ | ✓ | ✓ | | | | | |
| `'vp9'` | ✓ | ✓ | ✓ | ✓ | | | | | |
| `'av1'` | ✓ | ✓ | ✓ | ✓ | | | | | |
| `'aac'` | ✓ | ✓ | ✓ | | | | | ✓ | |
| `'opus'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | |
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | | | |
| `'vorbis'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | |
| `'flac'` | ✓ | ✓ | ✓ | | | | | | ✓ |
| `'pcm-u8'` | | ✓ | ✓ | | | | ✓ | | |
| `'pcm-s8'` | | ✓ | | | | | | | |
| `'pcm-s16'` | ✓ | ✓ | ✓ | | | | ✓ | | |
| `'pcm-s16be'` | ✓ | ✓ | ✓ | | | | | | |
| `'pcm-s24'` | ✓ | ✓ | ✓ | | | | ✓ | | |
| `'pcm-s24be'` | ✓ | ✓ | ✓ | | | | | | |
| `'pcm-s32'` | ✓ | ✓ | ✓ | | | | ✓ | | |
| `'pcm-s32be'` | ✓ | ✓ | ✓ | | | | | | |
| `'pcm-f32'` | ✓ | ✓ | ✓ | | | | ✓ | | |
| `'pcm-f32be'` | ✓ | ✓ | | | | | | | |
| `'pcm-f64'` | ✓ | ✓ | ✓ | | | | | | |
| `'pcm-f64be'` | ✓ | ✓ | | | | | | | |
| `'ulaw'` | | ✓ | | | | | ✓ | | |
| `'alaw'` | | ✓ | | | | | ✓ | | |
| `'webvtt'`[^2] | (✓) | | (✓) | (✓) | | | | | |
| | .mp4 | .mov | .mkv | .webm[^2] | .ogg | .mp3 | .wav | .aac | .flac | .ts |
|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
| `'avc'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
| `'hevc'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
| `'vp8'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
| `'vp9'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
| `'av1'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
| `'aac'` | ✓ | ✓ | ✓ | | | | | ✓ | | ✓ |
| `'opus'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | | |
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | | | | ✓ |
| `'vorbis'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | | |
| `'flac'` | ✓ | ✓ | ✓ | | | | | | ✓ | |
| `'ac3'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
| `'eac3'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
| `'pcm-u8'` | | ✓ | ✓ | | | | ✓ | | | |
| `'pcm-s8'` | | ✓ | | | | | | | | |
| `'pcm-s16'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
| `'pcm-s16be'` | ✓ | ✓ | ✓ | | | | | | | |
| `'pcm-s24'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
| `'pcm-s24be'` | ✓ | ✓ | ✓ | | | | | | | |
| `'pcm-s32'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
| `'pcm-s32be'` | ✓ | ✓ | ✓ | | | | | | | |
| `'pcm-f32'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
| `'pcm-f32be'` | ✓ | ✓ | | | | | | | | |
| `'pcm-f64'` | ✓ | ✓ | ✓ | | | | | | | |
| `'pcm-f64be'` | ✓ | ✓ | | | | | | | | |
| `'ulaw'` | | ✓ | | | | | ✓ | | | |
| `'alaw'` | | ✓ | | | | | ✓ | | | |
| `'webvtt'`[^3] | (✓) | | (✓) | (✓) | | | | | | |
[^1]: WebM only supports a small subset of the codecs supported by Matroska. However, this library can technically read all codecs from a WebM that are supported by Matroska.
[^2]: WebVTT can only be written, not read.
[^2]: WebM only supports a small subset of the codecs supported by Matroska. However, this library can technically read all codecs from a WebM that are supported by Matroska.
[^3]: WebVTT can only be written, not read.
## Querying codec encodability
+11 -1
View File
@@ -91,7 +91,7 @@ const bundleSizes = [
const sponsors = {
gold: [
{ image: '/sponsors/remotion.png', name: 'Remotion', url: 'https://remotion.dev/' },
{ image: '/sponsors/remotion-light.png', name: 'Remotion', url: 'https://remotion.dev/' },
{ image: '/sponsors/gling.svg', name: 'Gling AI', url: 'https://www.gling.ai/' },
{ image: '/sponsors/diffusionstudio.png', name: 'Diffusion Studio', url: 'https://diffusion.studio/' },
{ image: '/sponsors/kino.jpg', name: 'Kino', url: 'https://kino.ai/' },
@@ -101,22 +101,30 @@ const sponsors = {
{ image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' },
],
bronze: [
{ image: '/sponsors/elevenlabs.png', name: 'ElevenLabs', url: 'https://elevenlabs.io/' },
{ image: '/sponsors/rve.png', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' },
{ image: '/sponsors/mux.jpg', name: 'Mux', url: 'https://www.mux.com/' },
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
],
individual: [
{ 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' },
{ image: 'https://avatars.githubusercontent.com/u/197597', name: 'Christopher Chedeau', url: 'https://github.com/vjeux' },
{ image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' },
{ 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/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/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' },
{ image: 'https://avatars.githubusercontent.com/u/139718', name: 'Anton Kosiakin', url: 'https://github.com/deil' },
{ image: 'https://avatars.githubusercontent.com/u/56988069', name: 'SyhabouthAlex', url: 'https://github.com/SyhabouthAlex' },
{ image: 'https://avatars.githubusercontent.com/u/38181164', name: 'wcw', url: 'https://github.com/asd55667' },
{ image: 'https://avatars.githubusercontent.com/u/1836701', name: 'Bean Deng', url: 'https://github.com/HADB' },
{ 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/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' },
@@ -124,6 +132,8 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/97225946', name: '808vita', url: 'https://github.com/808vita' },
{ image: 'https://avatars.githubusercontent.com/u/3709646', name: 'Rodrigo Belfiore', url: 'https://github.com/roprgm' },
{ image: 'https://avatars.githubusercontent.com/u/31102694', name: 'Aiden Liu', url: 'https://github.com/aidenlx' },
{ image: 'https://avatars.githubusercontent.com/u/41021374', name: 'arthco', url: 'https://github.com/arthtyagi' },
{ image: 'https://avatars.githubusercontent.com/u/5907357', name: 'Harvey Zhao', url: 'https://github.com/zhw2590582' },
],
};
</script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+22 -3
View File
@@ -1,5 +1,24 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="5.33333" fill="#DAFF99"/>
<path d="M14.3273 9.29964C14.5206 8.97719 14.9878 8.97719 15.1811 9.29964L17.7213 13.5375C17.7634 13.6077 17.8221 13.6664 17.8923 13.7085L22.1301 16.2487C22.4526 16.4419 22.4526 16.9092 22.1301 17.1025L17.8923 19.6427C17.8221 19.6848 17.7634 19.7435 17.7213 19.8137L15.1811 24.0515C14.9878 24.374 14.5206 24.374 14.3273 24.0515L11.7871 19.8137C11.745 19.7435 11.6863 19.6848 11.6161 19.6427L7.37825 17.1025C7.0558 16.9092 7.0558 16.4419 7.37825 16.2487L11.6161 13.7085C11.6863 13.6664 11.745 13.6077 11.7871 13.5375L14.3273 9.29964Z" fill="#2D2D2D"/>
<path d="M22.7374 7.51156C22.7857 7.43094 22.9026 7.43094 22.9509 7.51156L23.7608 8.86276C23.7713 8.88032 23.786 8.895 23.8035 8.90552L25.1548 9.71544C25.2354 9.76376 25.2354 9.88058 25.1548 9.9289L23.8035 10.7388C23.786 10.7493 23.7713 10.764 23.7608 10.7816L22.9509 12.1328C22.9026 12.2134 22.7857 12.2134 22.7374 12.1328L21.9275 10.7816C21.917 10.764 21.9023 10.7493 21.8847 10.7388L20.5335 9.9289C20.4529 9.88058 20.4529 9.76376 20.5335 9.71544L21.8847 8.90552C21.9023 8.895 21.917 8.88032 21.9275 8.86276L22.7374 7.51156Z" fill="#2D2D2D"/>
<g clip-path="url(#clip0_3664_2482)">
<rect width="32" height="32" rx="4" fill="#DAFF99"/>
<path d="M15.736 9.07175C15.8492 8.8828 16.123 8.8828 16.2362 9.07175L18.9703 13.6355C18.9949 13.6766 19.0293 13.711 19.0705 13.7357L23.6322 16.4714C23.821 16.5846 23.821 16.8582 23.6322 16.9715L19.0705 19.7072C19.0293 19.7318 18.9949 19.7662 18.9703 19.8074L16.2362 24.3711C16.123 24.5601 15.8492 24.5601 15.736 24.3711L13.0019 19.8074C12.9772 19.7662 12.9428 19.7318 12.9017 19.7072L8.34001 16.9715C8.15121 16.8582 8.15121 16.5846 8.34001 16.4714L12.9017 13.7357C12.9428 13.711 12.9772 13.6766 13.0019 13.6355L15.736 9.07175Z" fill="#2D2D2D"/>
<g filter="url(#filter0_d_3664_2482)">
<path d="M23.8321 7.41198C23.8604 7.36474 23.9288 7.36474 23.9571 7.41198L24.815 8.84391C24.8212 8.85419 24.8298 8.86279 24.84 8.86896L26.2713 9.72731C26.3185 9.75561 26.3185 9.82401 26.2713 9.85232L24.84 10.7107C24.8298 10.7168 24.8212 10.7254 24.815 10.7357L23.9571 12.1676C23.9288 12.2149 23.8604 12.2149 23.8321 12.1676L22.9742 10.7357C22.9681 10.7254 22.9595 10.7168 22.9492 10.7107L21.5179 9.85232C21.4707 9.82401 21.4707 9.75561 21.5179 9.72731L22.9492 8.86896C22.9595 8.86279 22.9681 8.85419 22.9742 8.84391L23.8321 7.41198Z" fill="#2D2D2D"/>
</g>
</g>
<defs>
<filter id="filter0_d_3664_2482" x="-56.5175" y="-70.623" width="160.824" height="160.826" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="39"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3664_2482"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3664_2482" result="shape"/>
</filter>
<clipPath id="clip0_3664_2482">
<rect width="32" height="32" rx="4" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

+3
View File
@@ -38,8 +38,11 @@ export default tseslint.config(
'dist-docs',
'packages/mp3-encoder/dist',
'packages/mp3-encoder/build',
'packages/ac3/dist',
'packages/ac3/build',
'eslint.config.mjs',
'docs/.vitepress/cache',
'test/public',
]
}
);
@@ -62,20 +62,22 @@ const compressFile = async (resource: File | string) => {
currentConversion = await Conversion.init({
input,
output,
video: {
video: track => ({
width: 320, // Height will be deduced automatically to retain aspect ratio
bitrate: QUALITY_VERY_LOW,
},
audio: {
discard: track.number > 1, // Keep only the first video track
}),
audio: track => ({
bitrate: 32e3,
},
discard: track.number > 1, // Keep only the first audio track
}),
});
// Keep track of progress
let progress = 0;
currentConversion.onProgress = newProgress => progress = newProgress;
const fileDuration = await input.computeDuration();
const fileDuration = (await input.computeDuration()) - (await input.getFirstTimestamp());
const startTime = performance.now();
const updateProgress = () => {
@@ -124,7 +126,7 @@ const compressFile = async (resource: File | string) => {
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,audio/*,audio/aac';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
fileInput.addEventListener('change', () => {
const file = fileInput.files?.[0];
if (!file) {
+1 -1
View File
@@ -663,7 +663,7 @@ window.addEventListener('resize', () => {
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,audio/*,audio/aac';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
fileInput.addEventListener('change', () => {
const file = fileInput.files?.[0];
if (!file) {
@@ -43,12 +43,14 @@ const extractMetadata = (resource: File | string) => {
const object = {
'Format': input.getFormat().then(format => format.name),
'Full MIME type': input.getMimeType(),
'Duration': input.computeDuration().then(duration => `${duration} seconds`),
'Starts at': input.getFirstTimestamp().then(start => `${start} seconds`),
'Ends at': input.computeDuration().then(duration => `${duration} seconds`),
'Tracks': input.getTracks().then(tracks => tracks.map(track => ({
'Type': track.type,
'Codec': track.codec,
'Full codec string': track.getCodecParameterString(),
'Duration': track.computeDuration().then(duration => `${duration} seconds`),
'Starts at': track.getFirstTimestamp().then(start => `${start} seconds`),
'Ends at': track.computeDuration().then(duration => `${duration} seconds`),
'Language code': track.languageCode,
...(track.isVideoTrack()
? {
@@ -202,7 +204,7 @@ const shortDelay = () => {
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,audio/*,audio/aac';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
fileInput.addEventListener('change', () => {
const file = fileInput.files?.[0];
if (!file) {
@@ -120,7 +120,7 @@ const generateThumbnails = async (resource: File | string) => {
selectMediaButton.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/*,video/x-matroska,audio/*,audio/aac';
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
fileInput.addEventListener('change', () => {
const file = fileInput.files?.[0];
if (!file) {
+25 -6
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.25.8",
"version": "1.34.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.25.8",
"version": "1.34.2",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
@@ -1413,6 +1413,10 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mediabunny/ac3": {
"resolved": "packages/ac3",
"link": true
},
"node_modules/@mediabunny/mp3-encoder": {
"resolved": "packages/mp3-encoder",
"link": true
@@ -7739,9 +7743,9 @@
}
},
"node_modules/mediabunny": {
"version": "1.25.7",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.25.7.tgz",
"integrity": "sha512-DL0E1h29HTDaD9bYRXLSSHiAoLbDBksrdYS+4OHWA+aNhQeN+CAGEG7EU6wlhPZ8MOpwXIeC7uv06lo4ziohQQ==",
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.34.0.tgz",
"integrity": "sha512-mjn/7QVEPbPEakuNKD8OIPDhaoe2NlgWVyyqftuLVWvsgLol7g2MQzXWCjx523CP8Y9jE2H09hbYCuTwSI7pSQ==",
"license": "MPL-2.0",
"peer": true,
"workspaces": [
@@ -12063,9 +12067,24 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"packages/ac3": {
"name": "@mediabunny/ac3",
"version": "1.34.2",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/Vanilagy"
},
"peerDependencies": {
"mediabunny": "^1.0.0"
}
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
"version": "1.25.8",
"version": "1.34.2",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
+8 -7
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.25.8",
"version": "1.34.2",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
@@ -29,18 +29,19 @@
},
"sideEffects": false,
"scripts": {
"build": "./build.sh",
"build": "./scripts/build.sh",
"watch": "tsx scripts/bundle.ts --watch",
"lint": "eslint .",
"test": "npx vitest --run",
"test-node": "npm run test node/",
"test-browser": "npm run test browser/",
"check": "rm -rf dist/modules && tsc -p src && tsc -p packages/mp3-encoder/src --noEmit && tsc -p tsconfig.vitest.json --noEmit && tsc -p scripts --noEmit && tsc -p tsconfig.vite.json --noEmit",
"test": "npm run pre-test && npx vitest --run",
"test-node": "npm run pre-test && npm run test node/",
"test-browser": "npm run pre-test && npm run test browser/",
"pre-test": "tsx scripts/bundle.ts",
"check": "./scripts/check.sh",
"check-docblocks": "tsx scripts/check-docblocks.ts dist/mediabunny.d.ts",
"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 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 docs/api-config.json",
"dev": "vite",
"examples:build": "vite build",
"fix-build-import-paths": "tsx scripts/add-import-extensions.ts",
+1
View File
@@ -0,0 +1 @@
build/* linguist-generated
+373
View File
@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+108
View File
@@ -0,0 +1,108 @@
# @mediabunny/ac3
[![](https://img.shields.io/npm/v/@mediabunny/ac3)](https://www.npmjs.com/package/@mediabunny/ac3)
[![](https://img.shields.io/bundlephobia/minzip/@mediabunny/ac3)](https://bundlephobia.com/package/@mediabunny/ac3)
[![](https://img.shields.io/npm/dm/@mediabunny/ac3)](https://www.npmjs.com/package/@mediabunny/ac3)
[![](https://img.shields.io/discord/1390044844285497344?logo=discord&label=Discord)](https://discord.gg/hmpkyYuS4U)
<div align="center">
<img src="../../docs/public/mediabunny-logo.svg" width="180" height="180">
</div>
Browsers have no support for 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](https://github.com/Vanilagy/mediabunny), allowing you to decode and encode these codecs directly in the browser. It is implemented using Mediabunny's [custom coder API](https://mediabunny.dev/guide/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.
> This package, like the rest of Mediabunny, is enabled by its [sponsors](https://mediabunny.dev/#sponsors) and their donations. If you've derived value from this package, please consider [leaving a donation](https://github.com/sponsors/Vanilagy)! 💘
## Installation
This library peer-depends on Mediabunny. Install both using npm:
```bash
npm install mediabunny @mediabunny/ac3
```
Alternatively, directly include them using a script tag:
```html
<script src="mediabunny.js"></script>
<script src="mediabunny-ac3.js"></script>
```
This will expose the global objects `Mediabunny` and `MediabunnyAc3`. Use `mediabunny-ac3.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).
## Usage
```ts
import { registerAc3Decoder, registerAc3Encoder } from '@mediabunny/ac3';
registerAc3Decoder();
registerAc3Encoder();
```
That's it - Mediabunny now uses the registered AC-3/E-AC-3 decoder and encoder automatically.
## Building and development
For simplicity, all built WASM artifacts are included in the repo, since these rarely change. However, here are the instructions for building them from scratch:
[Install Emscripten](https://emscripten.org/docs/getting_started/downloads.html) and clone [FFmpeg](https://github.com/FFmpeg/FFmpeg). Then, from the Mediabunny root and with Emscripten sourced in:
```bash
export FFMPEG_PATH=/path/to/ffmpeg
export MEDIABUNNY_ROOT=$PWD
# Build FFmpeg
cd $FFMPEG_PATH
emmake make distclean
emconfigure ./configure \
--target-os=none \
--arch=x86_32 \
--enable-cross-compile \
--disable-asm \
--disable-x86asm \
--disable-inline-asm \
--disable-programs \
--disable-doc \
--disable-debug \
--disable-all \
--disable-everything \
--disable-autodetect \
--disable-pthreads \
--disable-runtime-cpudetect \
--enable-avcodec \
--enable-decoder=ac3 \
--enable-decoder=eac3 \
--enable-encoder=ac3 \
--enable-encoder=eac3 \
--cc="emcc" \
--cxx=em++ \
--ar=emar \
--ranlib=emranlib \
--extra-cflags="-DNDEBUG -Oz -flto -msimd128" \
--extra-ldflags="-Oz -flto"
emmake make
# Compile the bridge between JavaScript and FFmpeg's API
cd $MEDIABUNNY_ROOT/packages/ac3
emcc src/bridge.c \
$FFMPEG_PATH/libavcodec/libavcodec.a \
$FFMPEG_PATH/libavutil/libavutil.a \
-I$FFMPEG_PATH \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-s SINGLE_FILE=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s ENVIRONMENT=web,worker \
-s FILESYSTEM=0 \
-s MALLOC=emmalloc \
-s SUPPORT_LONGJMP=0 \
-s EXPORTED_RUNTIME_METHODS=cwrap,HEAPU8 \
-s EXPORTED_FUNCTIONS=_malloc,_free \
-msimd128 \
-flto \
-Oz \
-o build/ac3.js
```
This generates `build/ac3.js`, which contains both the JavaScript "glue code" as well as the compiled WASM inlined.
### Building the package
Then, the complete JavaScript package can be built alongside the rest of Mediabunny by running `npm run build` in Mediabunny's root.
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "dist/modules/src/index.d.ts",
"bundledPackages": [],
"compiler": {},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "dist/mediabunny-ac3.d.ts"
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
}
}
},
"newlineKind": "lf"
}
BIN
View File
Binary file not shown.
+56
View File
@@ -0,0 +1,56 @@
{
"name": "@mediabunny/ac3",
"author": "Vanilagy",
"version": "1.34.2",
"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",
"types": "./dist/modules/src/index.d.ts",
"exports": {
"types": "./dist/modules/src/index.d.ts",
"import": "./dist/bundles/mediabunny-ac3.mjs",
"require": "./dist/bundles/mediabunny-ac3.mjs"
},
"files": [
"README.md",
"package.json",
"LICENSE",
"dist",
"src"
],
"sideEffects": false,
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/Vanilagy/mediabunny.git",
"directory": "packages/ac3"
},
"bugs": {
"url": "https://github.com/Vanilagy/mediabunny/issues"
},
"homepage": "https://mediabunny.dev/guide/extensions/ac3",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/Vanilagy"
},
"peerDependencies": {
"mediabunny": "^1.0.0"
},
"devDependencies": {
"@types/emscripten": "^1.40.1"
},
"keywords": [
"ac3",
"eac3",
"dolby",
"dolby-digital",
"encoding",
"decoding",
"codec",
"mediabunny",
"ffmpeg",
"browser",
"wasm",
"polyfill"
]
}
+289
View File
@@ -0,0 +1,289 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#include <emscripten.h>
#include <stdlib.h>
#include <string.h>
#include "libavcodec/avcodec.h"
#include "libavutil/opt.h"
#include "libavutil/channel_layout.h"
typedef struct {
AVCodecContext *codec_ctx;
AVPacket *packet;
AVFrame *frame;
} DecoderContext;
EMSCRIPTEN_KEEPALIVE
DecoderContext *init_decoder(int codec_id) {
enum AVCodecID av_codec_id = codec_id == 0 ? AV_CODEC_ID_AC3 : AV_CODEC_ID_EAC3;
const AVCodec *codec = avcodec_find_decoder(av_codec_id);
if (!codec) return NULL;
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) return NULL;
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
avcodec_free_context(&codec_ctx);
return NULL;
}
AVPacket *packet = av_packet_alloc();
if (!packet) {
avcodec_free_context(&codec_ctx);
return NULL;
}
AVFrame *frame = av_frame_alloc();
if (!frame) {
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return NULL;
}
DecoderContext *ctx = malloc(sizeof(DecoderContext));
if (!ctx) {
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return NULL;
}
ctx->codec_ctx = codec_ctx;
ctx->packet = packet;
ctx->frame = frame;
return ctx;
}
EMSCRIPTEN_KEEPALIVE
uint8_t *configure_decode_packet(DecoderContext *ctx, int size) {
if (av_new_packet(ctx->packet, size) < 0) {
return NULL;
}
return ctx->packet->data;
}
EMSCRIPTEN_KEEPALIVE
int decode_packet(DecoderContext *ctx, int pts) {
ctx->packet->pts = pts;
int ret = avcodec_send_packet(ctx->codec_ctx, ctx->packet);
av_packet_unref(ctx->packet);
if (ret < 0) return ret;
ret = avcodec_receive_frame(ctx->codec_ctx, ctx->frame);
if (ret < 0) return ret;
return 0;
}
EMSCRIPTEN_KEEPALIVE
int get_decoded_format(DecoderContext *ctx) {
return ctx->frame->format;
}
EMSCRIPTEN_KEEPALIVE
uint8_t *get_decoded_plane_ptr(DecoderContext *ctx, int plane) {
return ctx->frame->data[plane];
}
EMSCRIPTEN_KEEPALIVE
int get_decoded_channels(DecoderContext *ctx) {
return ctx->frame->ch_layout.nb_channels;
}
EMSCRIPTEN_KEEPALIVE
int get_decoded_sample_rate(DecoderContext *ctx) {
return ctx->frame->sample_rate;
}
EMSCRIPTEN_KEEPALIVE
int get_decoded_sample_count(DecoderContext *ctx) {
return ctx->frame->nb_samples;
}
EMSCRIPTEN_KEEPALIVE
int get_decoded_pts(DecoderContext *ctx) {
return (int)ctx->frame->pts;
}
EMSCRIPTEN_KEEPALIVE
void flush_decoder(DecoderContext *ctx) {
avcodec_send_packet(ctx->codec_ctx, NULL);
while (avcodec_receive_frame(ctx->codec_ctx, ctx->frame) == 0) {}
avcodec_flush_buffers(ctx->codec_ctx);
}
EMSCRIPTEN_KEEPALIVE
void close_decoder(DecoderContext *ctx) {
av_frame_free(&ctx->frame);
av_packet_free(&ctx->packet);
avcodec_free_context(&ctx->codec_ctx);
free(ctx);
}
typedef struct {
AVCodecContext *codec_ctx;
AVPacket *packet;
AVFrame *frame;
float *input_buffer;
int input_buffer_size;
int encoded_pts;
int encoded_duration;
} EncoderContext;
EMSCRIPTEN_KEEPALIVE
EncoderContext *init_encoder(int codec_id, int channels, int sample_rate, int bitrate) {
enum AVCodecID av_codec_id = codec_id == 0 ? AV_CODEC_ID_AC3 : AV_CODEC_ID_EAC3;
const AVCodec *codec = avcodec_find_encoder(av_codec_id);
if (!codec) return NULL;
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) return NULL;
codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
codec_ctx->sample_rate = sample_rate;
codec_ctx->bit_rate = bitrate;
codec_ctx->time_base = (AVRational){1, sample_rate};
AVChannelLayout layout;
av_channel_layout_default(&layout, channels);
av_channel_layout_copy(&codec_ctx->ch_layout, &layout);
av_channel_layout_uninit(&layout);
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
avcodec_free_context(&codec_ctx);
return NULL;
}
AVPacket *packet = av_packet_alloc();
if (!packet) {
avcodec_free_context(&codec_ctx);
return NULL;
}
AVFrame *frame = av_frame_alloc();
if (!frame) {
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return NULL;
}
// The frame has a fixed format, so let's create it now:
frame->format = AV_SAMPLE_FMT_FLTP;
frame->sample_rate = sample_rate;
frame->nb_samples = codec_ctx->frame_size;
av_channel_layout_copy(&frame->ch_layout, &codec_ctx->ch_layout);
if (av_frame_get_buffer(frame, 0) < 0) {
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return NULL;
}
EncoderContext *ctx = malloc(sizeof(EncoderContext));
if (!ctx) {
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return NULL;
}
ctx->codec_ctx = codec_ctx;
ctx->packet = packet;
ctx->frame = frame;
ctx->input_buffer = NULL;
ctx->input_buffer_size = 0;
ctx->encoded_pts = 0;
ctx->encoded_duration = 0;
return ctx;
}
EMSCRIPTEN_KEEPALIVE
int get_encoder_frame_size(EncoderContext *ctx) {
return ctx->codec_ctx->frame_size;
}
EMSCRIPTEN_KEEPALIVE
float *get_encode_input_ptr(EncoderContext *ctx, int size) {
if (ctx->input_buffer_size < size) {
free(ctx->input_buffer);
ctx->input_buffer = malloc(size);
if (!ctx->input_buffer) {
ctx->input_buffer_size = 0;
return NULL;
}
ctx->input_buffer_size = size;
}
return ctx->input_buffer;
}
EMSCRIPTEN_KEEPALIVE
int encode_frame(EncoderContext *ctx, int pts) {
int channels = ctx->codec_ctx->ch_layout.nb_channels;
int frame_size = ctx->frame->nb_samples;
ctx->frame->pts = pts;
// Deinterleave f32 input into the frame's f32-planar planes
float *input = ctx->input_buffer;
for (int ch = 0; ch < channels; ch++) {
float *plane = (float *)ctx->frame->data[ch];
for (int i = 0; i < frame_size; i++) {
plane[i] = input[i * channels + ch];
}
}
int ret = avcodec_send_frame(ctx->codec_ctx, ctx->frame);
if (ret < 0) return ret;
ret = avcodec_receive_packet(ctx->codec_ctx, ctx->packet);
if (ret < 0) return ret;
ctx->encoded_pts = ctx->packet->pts;
ctx->encoded_duration = ctx->packet->duration;
return ctx->packet->size;
}
EMSCRIPTEN_KEEPALIVE
void flush_encoder(EncoderContext *ctx) {
avcodec_send_frame(ctx->codec_ctx, NULL);
while (avcodec_receive_packet(ctx->codec_ctx, ctx->packet) == 0) {
av_packet_unref(ctx->packet);
}
}
EMSCRIPTEN_KEEPALIVE
uint8_t *get_encoded_data(EncoderContext *ctx) {
return ctx->packet->data;
}
EMSCRIPTEN_KEEPALIVE
int get_encoded_pts(EncoderContext *ctx) {
return ctx->encoded_pts;
}
EMSCRIPTEN_KEEPALIVE
int get_encoded_duration(EncoderContext *ctx) {
return ctx->encoded_duration;
}
EMSCRIPTEN_KEEPALIVE
void close_encoder(EncoderContext *ctx) {
free(ctx->input_buffer);
av_frame_free(&ctx->frame);
av_packet_free(&ctx->packet);
avcodec_free_context(&ctx->codec_ctx);
free(ctx);
}
+306
View File
@@ -0,0 +1,306 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import createModule from '../build/ac3';
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
type ExtendedEmscriptenModule = EmscriptenModule & {
cwrap: typeof cwrap;
};
let module: ExtendedEmscriptenModule;
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 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 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 flushEncoderFn: (ctx: number) => void;
let getEncodedData: (ctx: number) => number;
let getEncodedPts: (ctx: number) => number;
let getEncodedDuration: (ctx: number) => number;
let closeEncoderFn: (ctx: number) => void;
const codecToId = (codec: string) => codec === 'ac3' ? 0 : 1;
const ensureModule = async () => {
if (!module) {
if (modulePromise) {
// If we don't do this we can have a race condition
return modulePromise;
}
modulePromise = createModule() as Promise<ExtendedEmscriptenModule>;
module = await modulePromise;
modulePromise = null;
initDecoderFn = module.cwrap('init_decoder', 'number', ['number']);
configureDecodePacket = module.cwrap('configure_decode_packet', 'number', ['number', 'number']);
decodePacket = module.cwrap('decode_packet', 'number', ['number', 'number']);
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']);
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']);
flushEncoderFn = module.cwrap('flush_encoder', null, ['number']);
getEncodedData = module.cwrap('get_encoded_data', 'number', ['number']);
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']);
getEncodedDuration = module.cwrap('get_encoded_duration', 'number', ['number']);
closeEncoderFn = module.cwrap('close_encoder', null, ['number']);
}
};
const initDecoder = async (codec: string) => {
await ensureModule();
const ctx = initDecoderFn(codecToId(codec));
if (ctx === 0) {
throw new Error('Failed to initialize AC3 decoder.');
}
return { ctx, frameSize: 0 };
};
// Keys are AVSampleFormat enum values
const AV_FORMAT_MAP: Record<number, { format: AudioSampleFormat; bytesPerSample: number; planar: boolean }> = {
0: { format: 'u8', bytesPerSample: 1, planar: false },
1: { format: 's16', bytesPerSample: 2, planar: false },
2: { format: 's32', bytesPerSample: 4, planar: false },
3: { format: 'f32', bytesPerSample: 4, planar: false },
5: { format: 'u8-planar', bytesPerSample: 1, planar: true },
6: { format: 's16-planar', bytesPerSample: 2, planar: true },
7: { format: 's32-planar', bytesPerSample: 4, planar: true },
8: { format: 'f32-planar', bytesPerSample: 4, planar: true },
};
const decode = (ctx: number, encodedData: ArrayBuffer, timestamp: number) => {
const bytes = new Uint8Array(encodedData);
const dataPtr = configureDecodePacket(ctx, bytes.length);
if (dataPtr === 0) {
throw new Error('Failed to configure decode packet.');
}
module.HEAPU8.set(bytes, dataPtr);
const ret = decodePacket(ctx, timestamp);
if (ret < 0) {
throw new Error(`Decode failed with error code ${ret}.`);
}
const avFormat = getDecodedFormat(ctx);
const info = AV_FORMAT_MAP[avFormat];
if (!info) {
throw new Error(`Unsupported AVSampleFormat: ${avFormat}`);
}
const channels = getDecodedChannels(ctx);
const sampleRate = getDecodedSampleRate(ctx);
const sampleCount = getDecodedSampleCount(ctx);
const pts = getDecodedPts(ctx);
let pcmData: ArrayBuffer;
if (info.planar) {
const planeSize = sampleCount * info.bytesPerSample;
const buffer = new Uint8Array(planeSize * channels);
for (let ch = 0; ch < channels; ch++) {
const ptr = getDecodedPlanePtr(ctx, ch);
buffer.set(module.HEAPU8.subarray(ptr, ptr + planeSize), ch * planeSize);
}
pcmData = buffer.buffer;
} else {
const totalSize = sampleCount * channels * info.bytesPerSample;
const ptr = getDecodedPlanePtr(ctx, 0);
pcmData = module.HEAPU8.slice(ptr, ptr + totalSize).buffer;
}
return { pcmData, format: info.format, channels, sampleRate, sampleCount, pts };
};
const initEncoder = async (
codec: string,
numberOfChannels: number,
sampleRate: number,
bitrate: number,
) => {
await ensureModule();
const ctx = initEncoderFn(codecToId(codec), numberOfChannels, sampleRate, bitrate);
if (ctx === 0) {
throw new Error('Failed to initialize AC3 encoder.');
}
return { ctx, frameSize: getEncoderFrameSize(ctx) };
};
const encode = (ctx: number, audioData: ArrayBuffer, timestamp: number) => {
const audioBytes = new Uint8Array(audioData);
const inputPtr = getEncodeInputPtr(ctx, audioBytes.length);
if (inputPtr === 0) {
throw new Error('Failed to allocate encoder input buffer.');
}
module.HEAPU8.set(audioBytes, inputPtr);
const bytesWritten = encodeFrameFn(ctx, 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 duration = getEncodedDuration(ctx);
return { encodedData, pts, duration };
};
const flushEncoder = (ctx: number) => {
flushEncoderFn(ctx);
};
const onMessage = (data: { id: number; command: WorkerCommand }) => {
const { id, command } = data;
const handleCommand = async (): Promise<void> => {
try {
let result: WorkerResponseData;
const transferables: Transferable[] = [];
switch (command.type) {
case 'init-decoder': {
const { ctx, frameSize } = await initDecoder(command.data.codec);
result = { type: command.type, ctx, frameSize };
}; break;
case 'decode': {
const decoded = decode(command.data.ctx, command.data.encodedData, command.data.timestamp);
result = {
type: command.type,
pcmData: decoded.pcmData,
format: decoded.format,
channels: decoded.channels,
sampleRate: decoded.sampleRate,
sampleCount: decoded.sampleCount,
pts: decoded.pts,
};
transferables.push(decoded.pcmData);
}; break;
case 'flush-decoder': {
flushDecoderFn(command.data.ctx);
result = { type: command.type };
}; break;
case 'close-decoder': {
closeDecoderFn(command.data.ctx);
result = { type: command.type };
}; break;
case 'init-encoder': {
const { ctx, frameSize } = await initEncoder(
command.data.codec,
command.data.numberOfChannels,
command.data.sampleRate,
command.data.bitrate,
);
result = { type: command.type, ctx, frameSize };
}; break;
case 'encode': {
const encoded = encode(
command.data.ctx,
command.data.audioData,
command.data.timestamp,
);
result = {
type: command.type,
encodedData: encoded.encodedData,
pts: encoded.pts,
duration: encoded.duration,
};
transferables.push(encoded.encodedData);
}; break;
case 'flush-encoder': {
flushEncoder(command.data.ctx);
result = { type: command.type };
}; break;
case 'close-encoder': {
closeEncoderFn(command.data.ctx);
result = { type: command.type };
}; break;
}
const response: WorkerResponse = {
id,
success: true,
data: result,
};
sendMessage(response, transferables);
} catch (error: unknown) {
const response: WorkerResponse = {
id,
success: false,
error,
};
sendMessage(response);
}
};
void handleCommand();
};
const sendMessage = (data: unknown, transferables?: Transferable[]) => {
if (parentPort) {
parentPort.postMessage(data, transferables ?? []);
} else {
self.postMessage(data, { transfer: transferables ?? [] });
}
};
let parentPort: {
postMessage: (data: unknown, transferables?: Transferable[]) => void;
on: (event: string, listener: (data: never) => void) => void;
} | null = null;
if (typeof self === 'undefined') {
const workerModule = 'worker_threads';
// eslint-disable-next-line @stylistic/max-len
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access
parentPort = require(workerModule).parentPort;
}
if (parentPort) {
parentPort.on('message', onMessage);
} else {
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
}
+70
View File
@@ -0,0 +1,70 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import {
CustomAudioDecoder,
AudioCodec,
AudioSample,
EncodedPacket,
registerDecoder,
} from 'mediabunny';
import { sendCommand } from './worker-client';
class Ac3Decoder extends CustomAudioDecoder {
private ctx = 0;
static override supports(codec: AudioCodec): boolean {
return codec === 'ac3' || codec === 'eac3';
}
async init() {
const result = await sendCommand({
type: 'init-decoder',
data: { codec: this.codec },
});
this.ctx = result.ctx;
}
async decode(packet: EncodedPacket) {
const encodedData = packet.data.slice().buffer;
const timestamp = Math.round(packet.timestamp * this.config.sampleRate);
const result = await sendCommand({
type: 'decode',
data: { ctx: this.ctx, encodedData, timestamp },
}, [encodedData]);
const sample = new AudioSample({
data: result.pcmData,
format: result.format,
numberOfChannels: result.channels,
sampleRate: result.sampleRate,
timestamp: result.pts / result.sampleRate,
});
this.onSample(sample);
}
async flush() {
await sendCommand({ type: 'flush-decoder', data: { ctx: this.ctx } });
}
close() {
void sendCommand({ type: 'close-decoder', data: { ctx: this.ctx } });
}
}
/**
* 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.
*
* @group \@mediabunny/ac3
* @public
*/
export const registerAc3Decoder = () => {
registerDecoder(Ac3Decoder);
};
+191
View File
@@ -0,0 +1,191 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import {
CustomAudioEncoder,
AudioCodec,
AudioSample,
EncodedPacket,
registerEncoder,
} from 'mediabunny';
import { sendCommand } from './worker-client';
import { assert } from './shared';
import { AC3_SAMPLE_RATES, EAC3_REDUCED_SAMPLE_RATES } from '../../../shared/ac3-misc';
class Ac3Encoder extends CustomAudioEncoder {
private ctx = 0;
private encoderFrameSize = 0;
private sampleRate = 0;
private numberOfChannels = 0;
private chunkMetadata: EncodedAudioChunkMetadata = {};
// Accumulate interleaved f32 samples until we have a full frame
private pendingBuffer = new Float32Array(2 ** 16);
private pendingFrames = 0;
private nextSampleTimestampInSamples: number | null = null;
private nextPacketTimestampInSamples: number | null = null;
static override supports(codec: AudioCodec, config: AudioEncoderConfig): boolean {
const sampleRates = codec === 'eac3'
? [...AC3_SAMPLE_RATES, ...EAC3_REDUCED_SAMPLE_RATES]
: AC3_SAMPLE_RATES;
return (codec === 'ac3' || codec === 'eac3')
&& config.numberOfChannels >= 1
&& config.numberOfChannels <= 8
&& sampleRates.includes(config.sampleRate);
}
async init() {
assert(this.config.bitrate);
this.sampleRate = this.config.sampleRate;
this.numberOfChannels = this.config.numberOfChannels;
const result = await sendCommand({
type: 'init-encoder',
data: {
codec: this.codec,
numberOfChannels: this.config.numberOfChannels,
sampleRate: this.config.sampleRate,
bitrate: this.config.bitrate,
},
});
this.ctx = result.ctx;
this.encoderFrameSize = result.frameSize;
this.resetInternalState();
}
private resetInternalState() {
this.pendingFrames = 0;
this.nextSampleTimestampInSamples = null;
this.nextPacketTimestampInSamples = null;
this.chunkMetadata = {
decoderConfig: {
codec: this.codec === 'ac3' ? 'ac-3' : 'ec-3',
numberOfChannels: this.config.numberOfChannels,
sampleRate: this.config.sampleRate,
},
};
}
async encode(audioSample: AudioSample) {
if (this.nextSampleTimestampInSamples === null) {
this.nextSampleTimestampInSamples = Math.round(audioSample.timestamp * this.sampleRate);
this.nextPacketTimestampInSamples = this.nextSampleTimestampInSamples;
}
const channels = this.numberOfChannels;
const incomingFrames = audioSample.numberOfFrames;
// Extract interleaved f32 data
const totalBytes = audioSample.allocationSize({ format: 'f32', planeIndex: 0 });
const audioBytes = new Uint8Array(totalBytes);
audioSample.copyTo(audioBytes, { format: 'f32', planeIndex: 0 });
const incomingData = new Float32Array(audioBytes.buffer);
const requiredSamples = (this.pendingFrames + incomingFrames) * channels;
if (requiredSamples > this.pendingBuffer.length) {
let newSize = this.pendingBuffer.length;
while (newSize < requiredSamples) {
newSize *= 2;
}
const newBuffer = new Float32Array(newSize);
newBuffer.set(this.pendingBuffer.subarray(0, this.pendingFrames * channels));
this.pendingBuffer = newBuffer;
}
this.pendingBuffer.set(incomingData, this.pendingFrames * channels);
this.pendingFrames += incomingFrames;
while (this.pendingFrames >= this.encoderFrameSize) {
await this.encodeOneFrame();
}
}
async flush() {
// Pad remaining samples with silence to fill a full frame
if (this.pendingFrames > 0) {
const channels = this.numberOfChannels;
const frameSize = this.encoderFrameSize;
const usedSamples = this.pendingFrames * channels;
const frameSamples = frameSize * channels;
this.pendingBuffer.fill(0, usedSamples, frameSamples);
this.pendingFrames = frameSize;
await this.encodeOneFrame();
}
await sendCommand({ type: 'flush-encoder', data: { ctx: this.ctx } });
this.resetInternalState();
}
close() {
void sendCommand({ type: 'close-encoder', data: { ctx: this.ctx } });
}
private async encodeOneFrame() {
assert(this.nextSampleTimestampInSamples !== null);
assert(this.nextPacketTimestampInSamples !== null);
const channels = this.numberOfChannels;
const frameSize = this.encoderFrameSize;
const frameSamples = frameSize * channels;
const frameData = this.pendingBuffer.slice(0, frameSamples);
// Shift remaining using copyWithin
this.pendingFrames -= frameSize;
if (this.pendingFrames > 0) {
this.pendingBuffer.copyWithin(0, frameSamples, frameSamples + this.pendingFrames * channels);
}
const audioData = frameData.buffer;
const result = await sendCommand({
type: 'encode',
data: {
ctx: this.ctx,
audioData,
timestamp: this.nextSampleTimestampInSamples,
},
}, [audioData]);
this.nextSampleTimestampInSamples += frameSize;
// We always get exactly one packet because we encode the correct frame size
const packet = new EncodedPacket(
new Uint8Array(result.encodedData),
'key',
this.nextPacketTimestampInSamples / this.sampleRate,
result.duration / this.sampleRate,
);
this.nextPacketTimestampInSamples += result.duration;
this.onPacket(
packet,
this.chunkMetadata,
);
this.chunkMetadata = {};
}
}
/**
* 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.
*
* @group \@mediabunny/ac3
* @public
*/
export const registerAc3Encoder = () => {
registerEncoder(Ac3Encoder);
};
+21
View File
@@ -0,0 +1,21 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
const AC3_LOADED_SYMBOL = Symbol.for('@mediabunny/ac3 loaded');
if ((globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL]) {
console.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,'
+ ' or if something is being bundled incorrectly.',
);
}
(globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL] = true;
export { registerAc3Decoder } from './decoder';
export { registerAc3Encoder } from './encoder';
+103
View File
@@ -0,0 +1,103 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
export type WorkerCommand = {
type: 'init-decoder';
data: {
codec: string;
};
} | {
type: 'decode';
data: {
ctx: number;
encodedData: ArrayBuffer;
timestamp: number;
};
} | {
type: 'flush-decoder';
data: {
ctx: number;
};
} | {
type: 'close-decoder';
data: {
ctx: number;
};
} | {
type: 'init-encoder';
data: {
codec: string;
numberOfChannels: number;
sampleRate: number;
bitrate: number;
};
} | {
type: 'encode';
data: {
ctx: number;
audioData: ArrayBuffer;
timestamp: number;
};
} | {
type: 'flush-encoder';
data: {
ctx: number;
};
} | {
type: 'close-encoder';
data: {
ctx: number;
};
};
export type WorkerResponseData = {
type: 'init-decoder';
ctx: number;
frameSize: number;
} | {
type: 'decode';
pcmData: ArrayBuffer;
format: AudioSampleFormat;
channels: number;
sampleRate: number;
sampleCount: number;
pts: number;
} | {
type: 'flush-decoder';
} | {
type: 'close-decoder';
} | {
type: 'init-encoder';
ctx: number;
frameSize: number;
} | {
type: 'encode';
encodedData: ArrayBuffer;
pts: number;
duration: number;
} | {
type: 'flush-encoder';
} | {
type: 'close-encoder';
};
export type WorkerResponse = {
id: number;
} & ({
success: true;
data: WorkerResponseData;
} | {
success: false;
error: unknown;
});
export function assert(x: unknown): asserts x {
if (!x) {
throw new Error('Assertion failed.');
}
}
+69
View File
@@ -0,0 +1,69 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { assert, type WorkerCommand, type WorkerResponse, type WorkerResponseData } from './shared';
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
import createWorker from './codec.worker';
let workerPromise: Promise<Worker> | null;
let nextMessageId = 0;
const pendingMessages = new Map<number, {
resolve: (value: WorkerResponseData) => void;
reject: (reason?: unknown) => void;
}>();
export const sendCommand = async <T extends string>(
command: WorkerCommand & { type: T },
transferables?: Transferable[],
) => {
const worker = await ensureWorker();
return new Promise<WorkerResponseData & { type: T }>((resolve, reject) => {
const id = nextMessageId++;
pendingMessages.set(id, {
resolve: resolve as (value: WorkerResponseData) => void,
reject,
});
if (transferables) {
worker.postMessage({ id, command }, transferables);
} else {
worker.postMessage({ id, command });
}
});
};
const ensureWorker = () => {
return workerPromise ??= (async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const worker = (await createWorker()) as Worker;
const onMessage = (data: WorkerResponse) => {
const pending = pendingMessages.get(data.id);
assert(pending !== undefined);
pendingMessages.delete(data.id);
if (data.success) {
pending.resolve(data.data);
} else {
pending.reject(data.error);
}
};
if (worker.addEventListener) {
worker.addEventListener('message', event => onMessage(event.data as WorkerResponse));
} else {
const nodeWorker = worker as unknown as {
on: (event: string, listener: (data: never) => void) => void;
};
nodeWorker.on('message', onMessage);
}
return worker;
})();
};
@@ -1,7 +1,8 @@
{
"extends": "../../../tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../dist/modules",
"composite": true,
"outDir": "./dist/modules",
"declaration": true,
"declarationMap": true,
"stripInternal": true,
@@ -10,14 +11,14 @@
"module": "nodenext",
"allowJs": true,
"paths": {
"mediabunny": ["../../../src/index.ts"],
"mediabunny": ["../../src/index.ts"],
},
},
"include": [
"**/*",
"../../../shared/**/*"
"./src/**/*",
"./build/**/*",
],
"references": [
{ "path": "../../../src" }
{ "path": "../../src" }
]
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"extends": ["../../tsdoc.json"]
}
+2 -3
View File
@@ -3,6 +3,7 @@
[![](https://img.shields.io/npm/v/@mediabunny/mp3-encoder)](https://www.npmjs.com/package/@mediabunny/mp3-encoder)
[![](https://img.shields.io/bundlephobia/minzip/@mediabunny/mp3-encoder)](https://bundlephobia.com/package/@mediabunny/mp3-encoder)
[![](https://img.shields.io/npm/dm/@mediabunny/mp3-encoder)](https://www.npmjs.com/package/@mediabunny/mp3-encoder)
[![](https://img.shields.io/discord/1390044844285497344?logo=discord&label=Discord)](https://discord.gg/hmpkyYuS4U)
<div align="center">
<img src="./logo.svg" width="180" height="180">
@@ -102,8 +103,6 @@ The WASM build itself is a performance-optimized, SIMD-enabled build of LAME 3.1
## Building and development
Building this library is done using the build commands in the [Mediabunny root](https://github.com/Vanilagy/mediabunny).
For simplicity, all built WASM artifacts are included in the repo, since these rarely change. However, here are the instructions for building them from scratch:
### Prerequisites
@@ -148,7 +147,7 @@ emcc src/lame-bridge.c build/libmp3lame.a \
-o build/lame.js
```
This generates `build/lame.js`, which contains both the JavaScript "glue code" as well as the compiled WASM encoded with Base64.
This generates `build/lame.js`, which contains both the JavaScript "glue code" as well as the compiled WASM inlined.
### Building the package
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.25.8",
"version": "1.34.2",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+13 -4
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -146,7 +146,10 @@ const onMessage = (data: { id: number; command: WorkerCommand }) => {
command.data.sampleRate,
command.data.bitrate,
);
result = { success: true };
result = {
type: command.type,
success: true,
};
}; break;
case 'encode': {
@@ -154,13 +157,19 @@ const onMessage = (data: { id: number; command: WorkerCommand }) => {
command.data.audioData,
command.data.numberOfFrames,
);
result = { encodedData };
result = {
type: command.type,
encodedData,
};
transferables.push(encodedData);
}; break;
case 'flush': {
const flushedData = flush();
result = { flushedData };
result = {
type: command.type,
flushedData,
};
transferables.push(flushedData);
}; break;
}
+30 -12
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -7,11 +7,22 @@
*/
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, registerEncoder } from 'mediabunny';
import { FRAME_HEADER_SIZE, readFrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
import { FRAME_HEADER_SIZE, readMp3FrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
import createWorker from './encode.worker';
const MP3_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/mp3-encoder loaded');
if ((globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL]) {
console.error(
'[WARNING]\n@mediabunny/mp3-encoder was loaded twice.'
+ ' This will likely cause the encoder not to work correctly.'
+ ' Check if multiple dependencies are importing different versions of @mediabunny/mp3-encoder,'
+ ' or if something is being bundled incorrectly.',
);
}
(globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL] = true;
class Mp3Encoder extends CustomAudioEncoder {
private worker: Worker | null = null;
private nextMessageId = 0;
@@ -69,6 +80,12 @@ class Mp3Encoder extends CustomAudioEncoder {
},
});
this.resetInternalState();
}
private resetInternalState() {
this.currentBufferOffset = 0;
this.currentTimestamp = null;
this.chunkMetadata = {
decoderConfig: {
codec: 'mp3',
@@ -108,15 +125,15 @@ class Mp3Encoder extends CustomAudioEncoder {
},
}, [audioData]);
assert('encodedData' in result);
this.digestOutput(new Uint8Array(result.encodedData));
}
async flush() {
const result = await this.sendCommand({ type: 'flush' });
assert('flushedData' in result);
this.digestOutput(new Uint8Array(result.flushedData));
this.resetInternalState();
}
close() {
@@ -145,7 +162,7 @@ class Mp3Encoder extends CustomAudioEncoder {
let pos = 0;
while (pos <= this.currentBufferOffset - FRAME_HEADER_SIZE) {
const word = new DataView(this.buffer.buffer).getUint32(pos, false);
const header = readFrameHeader(word, null).header;
const header = readMp3FrameHeader(word, null).header;
if (!header) {
break;
}
@@ -160,9 +177,7 @@ class Mp3Encoder extends CustomAudioEncoder {
const duration = header.audioSamplesInFrame / header.sampleRate;
this.onPacket(new EncodedPacket(data, 'key', this.currentTimestamp, duration), this.chunkMetadata);
if (this.currentTimestamp === 0) {
this.chunkMetadata = {}; // Mimic WebCodecs-like behavior
}
this.chunkMetadata = {}; // Mimic WebCodecs-like behavior
this.currentTimestamp += duration;
pos += header.totalSize;
@@ -175,13 +190,16 @@ class Mp3Encoder extends CustomAudioEncoder {
}
}
private sendCommand(
command: WorkerCommand,
private sendCommand<T extends string>(
command: WorkerCommand & { type: T },
transferables?: Transferable[],
) {
return new Promise<WorkerResponseData>((resolve, reject) => {
return new Promise<WorkerResponseData & { type: T }>((resolve, reject) => {
const id = this.nextMessageId++;
this.pendingMessages.set(id, { resolve, reject });
this.pendingMessages.set(id, {
resolve: resolve as (value: WorkerResponseData) => void,
reject,
});
assert(this.worker);
+1 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
+4 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -24,10 +24,13 @@ export type WorkerCommand = {
};
export type WorkerResponseData = {
type: 'init';
success: boolean;
} | {
type: 'encode';
encodedData: ArrayBuffer;
} | {
type: 'flush';
flushedData: ArrayBuffer;
};
+25
View File
@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist/modules",
"declaration": true,
"declarationMap": true,
"stripInternal": true,
"noEmit": false,
"moduleResolution": "nodenext",
"module": "nodenext",
"allowJs": true,
"paths": {
"mediabunny": ["../../src/index.ts"],
},
},
"include": [
"./src/**/*",
"./build/**/*",
"../../shared/**/*",
],
"references": [
{ "path": "../../src" }
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"extends": ["../../tsdoc.json"]
}
+7 -2
View File
@@ -6,13 +6,15 @@ set -e
# Clear the stuff from last build
rm -rf dist
rm -rf packages/mp3-encoder/dist
rm -rf packages/ac3/dist
# Ensure license headers on all source files
tsx scripts/ensure-license-headers.ts
# Type check & generate .js and .d.ts files
tsc -p src
tsc -p packages/mp3-encoder/src
tsc -p packages/mp3-encoder
tsc -p packages/ac3
# So that the resulting files use valid ESM imports with file extension. This only runs for the core Mediabunny as only
# it ships the individual files to npm (for tree shaking, because it's large)
@@ -24,14 +26,17 @@ tsx scripts/bundle.ts
# Declaration file rollup and checks
api-extractor run
api-extractor run -c packages/mp3-encoder/api-extractor.json
api-extractor run -c packages/ac3/api-extractor.json
# Checks that all symbols are documented
tsx scripts/check-docblocks.ts dist/mediabunny.d.ts
tsx scripts/check-docblocks.ts packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
tsx scripts/check-docblocks.ts packages/ac3/dist/mediabunny-ac3.d.ts
# Checks that API docs are generatable
npm run docs:generate -- --dry
# Appends stuff to the declaration files to register the global variables these libraries expose
echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts
echo 'export as namespace MediabunnyMp3Encoder;' >> packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
echo 'export as namespace MediabunnyMp3Encoder;' >> packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
echo 'export as namespace MediabunnyAc3;' >> packages/ac3/dist/mediabunny-ac3.d.ts
+33 -1
View File
@@ -21,7 +21,7 @@ const createVariants = async (
},
banner: {
js: `/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -113,9 +113,41 @@ const mp3EncoderVariants = await createVariants(
},
);
const ac3Variants = await createVariants(
'packages/ac3/src/index.ts',
'MediabunnyAc3',
'packages/ac3/dist/bundles/mediabunny-ac3',
'js', // The bundles are purely for the browser, not for Node (due to the peer dependecy)
{
plugins: [
PluginExternalGlobal.externalGlobalPlugin({
mediabunny: 'Mediabunny',
}),
inlineWorkerPlugin({
define: {
'import.meta.url': '""',
},
legalComments: 'none',
}),
],
},
{
external: ['mediabunny'],
plugins: [
inlineWorkerPlugin({
define: {
'import.meta.url': '""',
},
legalComments: 'none',
}),
],
},
);
const contexts = [
...mediabunnyVariants,
...mp3EncoderVariants,
...ac3Variants,
];
if (process.argv[2] === '--watch') {
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -e
rm -rf dist/modules
tsc -p src --stripInternal false
tsc -p packages/mp3-encoder --noEmit
rm -rf packages/ac3/dist/modules
tsc -p packages/ac3
tsc -p tsconfig.vitest.json --noEmit
tsc -p scripts --noEmit
tsc -p tsconfig.vite.json --noEmit
+2 -1
View File
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LICENSE_HEADER = `/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -36,6 +36,7 @@ const checkDirectory = (dirPath: string) => {
checkDirectory(path.join(__dirname, '..', 'src'));
checkDirectory(path.join(__dirname, '..', 'packages', 'mp3-encoder', 'src'));
checkDirectory(path.join(__dirname, '..', 'packages', 'ac3', 'src'));
checkDirectory(path.join(__dirname, '..', 'shared'));
if (missingFiles.length > 0) {
+13
View File
@@ -0,0 +1,13 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/** Sample rates indexed by fscod (Table 4.1) */
export const AC3_SAMPLE_RATES = [48000, 44100, 32000];
/** E-AC-3 reduced sample rates for fscod2 per ATSC A/52:2018 */
export const EAC3_REDUCED_SAMPLE_RATES = [24000, 22050, 16000];
+4 -4
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -27,7 +27,7 @@ export const XING = 0x58696e67;
/** 'Info' */
export const INFO = 0x496e666f;
export type FrameHeader = {
export type Mp3FrameHeader = {
totalSize: number;
mpegVersionId: number;
layer: number;
@@ -66,8 +66,8 @@ export const getXingOffset = (mpegVersionId: number, channel: number) => {
: (channel === 3 ? 13 : 21);
};
export const readFrameHeader = (word: number, remainingBytes: number | null): {
header: FrameHeader | null;
export const readMp3FrameHeader = (word: number, remainingBytes: number | null): {
header: Mp3FrameHeader | null;
bytesAdvanced: number;
} => {
const firstByte = word >>> 24;
+83 -29
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -8,23 +8,32 @@
import { aacChannelMap, aacFrequencyTable, AudioCodec } from '../codec';
import { Demuxer } from '../demuxer';
import {
ID3_V2_HEADER_SIZE,
parseId3V2Tag,
readId3V2Header,
} from '../id3';
import { Input } from '../input';
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { PacketRetrievalOptions } from '../media-sink';
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
import {
assert,
AsyncMutex,
binarySearchExact,
binarySearchLessOrEqual,
Bitstream,
UNDETERMINED_LANGUAGE,
} from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { readBytes, Reader } from '../reader';
import { DEFAULT_TRACK_DISPOSITION } from '../metadata';
import { FrameHeader, MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts-reader';
import {
AdtsFrameHeader,
MIN_ADTS_FRAME_HEADER_SIZE,
MAX_ADTS_FRAME_HEADER_SIZE,
readAdtsFrameHeader,
} from './adts-reader';
const SAMPLES_PER_AAC_FRAME = 1024;
export const SAMPLES_PER_AAC_FRAME = 1024;
type Sample = {
timestamp: number;
@@ -37,8 +46,9 @@ export class AdtsDemuxer extends Demuxer {
reader: Reader;
metadataPromise: Promise<void> | null = null;
firstFrameHeader: FrameHeader | null = null;
firstFrameHeader: AdtsFrameHeader | null = null;
loadedSamples: Sample[] = [];
metadataTags: MetadataTags | null = null;
tracks: InputAudioTrack[] = [];
@@ -69,14 +79,38 @@ export class AdtsDemuxer extends Demuxer {
}
async advanceReader() {
let slice = this.reader.requestSliceRange(this.lastLoadedPos, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE);
if (this.lastLoadedPos === 0) {
// Skip all ID3v2 tags at the start of the file
while (true) {
let slice = this.reader.requestSlice(this.lastLoadedPos, ID3_V2_HEADER_SIZE);
if (slice instanceof Promise) slice = await slice;
if (!slice) {
this.lastSampleLoaded = true;
return;
}
const id3V2Header = readId3V2Header(slice);
if (!id3V2Header) {
break;
}
this.lastLoadedPos = slice.filePos + id3V2Header.size;
}
}
let slice = this.reader.requestSliceRange(
this.lastLoadedPos,
MIN_ADTS_FRAME_HEADER_SIZE,
MAX_ADTS_FRAME_HEADER_SIZE,
);
if (slice instanceof Promise) slice = await slice;
if (!slice) {
this.lastSampleLoaded = true;
return;
}
const header = readFrameHeader(slice);
const header = readAdtsFrameHeader(slice);
if (!header) {
this.lastSampleLoaded = true;
return;
@@ -95,13 +129,12 @@ export class AdtsDemuxer extends Demuxer {
const sampleRate = aacFrequencyTable[header.samplingFrequencyIndex];
assert(sampleRate !== undefined);
const sampleDuration = SAMPLES_PER_AAC_FRAME / sampleRate;
const headerSize = header.crcCheck ? MAX_FRAME_HEADER_SIZE : MIN_FRAME_HEADER_SIZE;
const sample: Sample = {
timestamp: this.nextTimestampInSamples / sampleRate,
duration: sampleDuration,
dataStart: header.startPos + headerSize,
dataSize: header.frameLength - headerSize,
dataStart: header.startPos,
dataSize: header.frameLength,
};
this.loadedSamples.push(sample);
@@ -128,7 +161,41 @@ export class AdtsDemuxer extends Demuxer {
}
async getMetadataTags() {
return {}; // No tags in this one
const release = await this.readingMutex.acquire();
try {
await this.readMetadata();
if (this.metadataTags) {
return this.metadataTags;
}
this.metadataTags = {};
let currentPos = 0;
while (true) {
let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
if (!headerSlice) break;
const id3V2Header = readId3V2Header(headerSlice);
if (!id3V2Header) {
break;
}
let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
if (contentSlice instanceof Promise) contentSlice = await contentSlice;
if (!contentSlice) break;
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
currentPos = headerSlice.filePos + id3V2Header.size;
}
return this.metadataTags;
} finally {
release();
}
}
}
@@ -139,6 +206,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
return 1;
}
getNumber() {
return 1;
}
async getFirstTimestamp() {
return 0;
}
@@ -198,27 +269,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
async getDecoderConfig(): Promise<AudioDecoderConfig> {
assert(this.demuxer.firstFrameHeader);
const bytes = new Uint8Array(3); // 19 bits max
const bitstream = new Bitstream(bytes);
const { objectType, samplingFrequencyIndex, channelConfiguration } = this.demuxer.firstFrameHeader;
if (objectType > 31) {
bitstream.writeBits(5, 31);
bitstream.writeBits(6, objectType - 32);
} else {
bitstream.writeBits(5, objectType);
}
bitstream.writeBits(4, samplingFrequencyIndex); // samplingFrequencyIndex === 15 is forbidden
bitstream.writeBits(4, channelConfiguration);
return {
codec: `mp4a.40.${this.demuxer.firstFrameHeader.objectType}`,
numberOfChannels: this.getNumberOfChannels(),
sampleRate: this.getSampleRate(),
description: bytes.subarray(0, Math.ceil((bitstream.pos - 1) / 8)),
};
}
+47
View File
@@ -0,0 +1,47 @@
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { AacAudioSpecificConfig } from '../codec';
import { Bitstream } from '../misc';
export type AdtsHeaderTemplate = {
header: Uint8Array;
bitstream: Bitstream;
};
export const buildAdtsHeaderTemplate = (config: AacAudioSpecificConfig): AdtsHeaderTemplate => {
const header = new Uint8Array(7);
const bitstream = new Bitstream(header);
const { objectType, frequencyIndex, channelConfiguration } = config;
const profile = objectType - 1;
bitstream.writeBits(12, 0b1111_11111111); // Syncword
bitstream.writeBits(1, 0); // MPEG Version
bitstream.writeBits(2, 0); // Layer
bitstream.writeBits(1, 1); // Protection absence
bitstream.writeBits(2, profile); // Profile
bitstream.writeBits(4, frequencyIndex); // MPEG-4 Sampling Frequency Index
bitstream.writeBits(1, 0); // Private bit
bitstream.writeBits(3, channelConfiguration); // MPEG-4 Channel Configuration
bitstream.writeBits(1, 0); // Originality
bitstream.writeBits(1, 0); // Home
bitstream.writeBits(1, 0); // Copyright ID bit
bitstream.writeBits(1, 0); // Copyright ID start
bitstream.skipBits(13); // Frame length (to be filled per packet)
bitstream.writeBits(11, 0x7ff); // Buffer fullness
bitstream.writeBits(2, 0); // Number of AAC frames minus 1
// Omit CRC check
return { header, bitstream };
};
export const writeAdtsFrameLength = (bitstream: Bitstream, frameLength: number) => {
bitstream.pos = 30;
bitstream.writeBits(13, frameLength);
};
+45 -41
View File
@@ -1,25 +1,28 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { AacAudioSpecificConfig, parseAacAudioSpecificConfig, validateAudioChunkMetadata } from '../codec';
import { parseAacAudioSpecificConfig, validateAudioChunkMetadata } from '../codec';
import { Id3V2Writer } from '../id3';
import { metadataTagsAreEmpty } from '../metadata';
import { assert, Bitstream, toUint8Array } from '../misc';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack } from '../output';
import { AdtsOutputFormat } from '../output-format';
import { EncodedPacket } from '../packet';
import { Writer } from '../writer';
import { buildAdtsHeaderTemplate, writeAdtsFrameLength } from './adts-misc';
export class AdtsMuxer extends Muxer {
private format: AdtsOutputFormat;
private writer: Writer;
private header = new Uint8Array(7);
private headerBitstream = new Bitstream(this.header);
private audioSpecificConfig: AacAudioSpecificConfig | null = null;
private header: Uint8Array | null = null;
private headerBitstream: Bitstream | null = null;
private inputIsAdts: boolean | null = null;
constructor(output: Output, format: AdtsOutputFormat) {
super(output);
@@ -29,7 +32,10 @@ export class AdtsMuxer extends Muxer {
}
async start() {
// Nothing needed here
if (!metadataTagsAreEmpty(this.output._metadataTags)) {
const id3Writer = new Id3V2Writer(this.writer);
id3Writer.writeId3V2Tag(this.output._metadataTags);
}
}
async getMimeType() {
@@ -45,56 +51,54 @@ export class AdtsMuxer extends Muxer {
packet: EncodedPacket,
meta?: EncodedAudioChunkMetadata,
) {
// https://wiki.multimedia.cx/index.php/ADTS (last visited: 2025/08/17)
const release = await this.mutex.acquire();
try {
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
if (!this.audioSpecificConfig) {
// First packet - determine input format from metadata
if (this.inputIsAdts === null) {
validateAudioChunkMetadata(meta);
const description = meta?.decoderConfig?.description;
assert(description);
this.audioSpecificConfig = parseAacAudioSpecificConfig(toUint8Array(description));
// Follows from the Mediabunny Codec Registry:
this.inputIsAdts = !description;
const { objectType, frequencyIndex, channelConfiguration } = this.audioSpecificConfig;
const profile = objectType - 1;
this.headerBitstream.writeBits(12, 0b1111_11111111); // Syncword
this.headerBitstream.writeBits(1, 0); // MPEG Version
this.headerBitstream.writeBits(2, 0); // Layer
this.headerBitstream.writeBits(1, 1); // Protection absence
this.headerBitstream.writeBits(2, profile); // Profile
this.headerBitstream.writeBits(4, frequencyIndex); // MPEG-4 Sampling Frequency Index
this.headerBitstream.writeBits(1, 0); // Private bit
this.headerBitstream.writeBits(3, channelConfiguration); // MPEG-4 Channel Configuration
this.headerBitstream.writeBits(1, 0); // Originality
this.headerBitstream.writeBits(1, 0); // Home
this.headerBitstream.writeBits(1, 0); // Copyright ID bit
this.headerBitstream.writeBits(1, 0); // Copyright ID start
this.headerBitstream.skipBits(13); // Frame length
this.headerBitstream.writeBits(11, 0x7ff); // Buffer fullness
this.headerBitstream.writeBits(2, 0); // Number of AAC frames minus 1
// Omit CRC check
if (!this.inputIsAdts) {
const config = parseAacAudioSpecificConfig(toUint8Array(description!));
const template = buildAdtsHeaderTemplate(config);
this.header = template.header;
this.headerBitstream = template.bitstream;
}
}
const frameLength = packet.data.byteLength + this.header.byteLength;
this.headerBitstream.pos = 30;
this.headerBitstream.writeBits(13, frameLength);
if (this.inputIsAdts) {
// Packets are already ADTS frames, write them directly
const startPos = this.writer.getPos();
this.writer.write(packet.data);
const startPos = this.writer.getPos();
this.writer.write(this.header);
this.writer.write(packet.data);
if (this.format._options.onFrame) {
this.format._options.onFrame(packet.data, startPos);
}
} else {
assert(this.header);
if (this.format._options.onFrame) {
const frameBytes = new Uint8Array(frameLength);
frameBytes.set(this.header, 0);
frameBytes.set(packet.data, this.header.byteLength);
// Packets are raw AAC, we gotta turn it into ADTS
const frameLength = packet.data.byteLength + this.header.byteLength;
writeAdtsFrameLength(this.headerBitstream!, frameLength);
this.format._options.onFrame(frameBytes, startPos);
const startPos = this.writer.getPos();
this.writer.write(this.header);
this.writer.write(packet.data);
if (this.format._options.onFrame) {
const frameBytes = new Uint8Array(frameLength);
frameBytes.set(this.header, 0);
frameBytes.set(packet.data, this.header.byteLength);
this.format._options.onFrame(frameBytes, startPos);
}
}
await this.writer.flush();
+5 -5
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -9,10 +9,10 @@
import { Bitstream } from '../misc';
import { FileSlice, readBytes } from '../reader';
export const MIN_FRAME_HEADER_SIZE = 7;
export const MAX_FRAME_HEADER_SIZE = 9;
export const MIN_ADTS_FRAME_HEADER_SIZE = 7;
export const MAX_ADTS_FRAME_HEADER_SIZE = 9;
export type FrameHeader = {
export type AdtsFrameHeader = {
objectType: number;
samplingFrequencyIndex: number;
channelConfiguration: number;
@@ -22,7 +22,7 @@ export type FrameHeader = {
startPos: number;
};
export const readFrameHeader = (slice: FileSlice): FrameHeader | null => {
export const readAdtsFrameHeader = (slice: FileSlice): AdtsFrameHeader | null => {
// https://wiki.multimedia.cx/index.php/ADTS (last visited: 2025/08/17)
const startPos = slice.filePos;
+1006 -205
View File
File diff suppressed because it is too large Load Diff
+65 -32
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -70,6 +70,8 @@ export const NON_PCM_AUDIO_CODECS = [
'mp3',
'vorbis',
'flac',
'ac3',
'eac3',
] as const;
/**
* List of known audio codecs, ordered by encoding preference.
@@ -116,26 +118,26 @@ export type SubtitleCodec = typeof SUBTITLE_CODECS[number];
export type MediaCodec = VideoCodec | AudioCodec | SubtitleCodec;
// https://en.wikipedia.org/wiki/Advanced_Video_Coding
const AVC_LEVEL_TABLE = [
{ maxMacroblocks: 99, maxBitrate: 64000, level: 0x0A }, // Level 1
{ maxMacroblocks: 396, maxBitrate: 192000, level: 0x0B }, // Level 1.1
{ maxMacroblocks: 396, maxBitrate: 384000, level: 0x0C }, // Level 1.2
{ maxMacroblocks: 396, maxBitrate: 768000, level: 0x0D }, // Level 1.3
{ maxMacroblocks: 396, maxBitrate: 2000000, level: 0x14 }, // Level 2
{ maxMacroblocks: 792, maxBitrate: 4000000, level: 0x15 }, // Level 2.1
{ maxMacroblocks: 1620, maxBitrate: 4000000, level: 0x16 }, // Level 2.2
{ maxMacroblocks: 1620, maxBitrate: 10000000, level: 0x1E }, // Level 3
{ maxMacroblocks: 3600, maxBitrate: 14000000, level: 0x1F }, // Level 3.1
{ maxMacroblocks: 5120, maxBitrate: 20000000, level: 0x20 }, // Level 3.2
{ maxMacroblocks: 8192, maxBitrate: 20000000, level: 0x28 }, // Level 4
{ maxMacroblocks: 8192, maxBitrate: 50000000, level: 0x29 }, // Level 4.1
{ maxMacroblocks: 8704, maxBitrate: 50000000, level: 0x2A }, // Level 4.2
{ maxMacroblocks: 22080, maxBitrate: 135000000, level: 0x32 }, // Level 5
{ maxMacroblocks: 36864, maxBitrate: 240000000, level: 0x33 }, // Level 5.1
{ maxMacroblocks: 36864, maxBitrate: 240000000, level: 0x34 }, // Level 5.2
{ maxMacroblocks: 139264, maxBitrate: 240000000, level: 0x3C }, // Level 6
{ maxMacroblocks: 139264, maxBitrate: 480000000, level: 0x3D }, // Level 6.1
{ maxMacroblocks: 139264, maxBitrate: 800000000, level: 0x3E }, // Level 6.2
export const AVC_LEVEL_TABLE = [
{ maxMacroblocks: 99, maxBitrate: 64000, maxDpbMbs: 396, level: 0x0A }, // Level 1
{ maxMacroblocks: 396, maxBitrate: 192000, maxDpbMbs: 900, level: 0x0B }, // Level 1.1
{ maxMacroblocks: 396, maxBitrate: 384000, maxDpbMbs: 2376, level: 0x0C }, // Level 1.2
{ maxMacroblocks: 396, maxBitrate: 768000, maxDpbMbs: 2376, level: 0x0D }, // Level 1.3
{ maxMacroblocks: 396, maxBitrate: 2000000, maxDpbMbs: 2376, level: 0x14 }, // Level 2
{ maxMacroblocks: 792, maxBitrate: 4000000, maxDpbMbs: 4752, level: 0x15 }, // Level 2.1
{ maxMacroblocks: 1620, maxBitrate: 4000000, maxDpbMbs: 8100, level: 0x16 }, // Level 2.2
{ maxMacroblocks: 1620, maxBitrate: 10000000, maxDpbMbs: 8100, level: 0x1E }, // Level 3
{ maxMacroblocks: 3600, maxBitrate: 14000000, maxDpbMbs: 18000, level: 0x1F }, // Level 3.1
{ maxMacroblocks: 5120, maxBitrate: 20000000, maxDpbMbs: 20480, level: 0x20 }, // Level 3.2
{ maxMacroblocks: 8192, maxBitrate: 20000000, maxDpbMbs: 32768, level: 0x28 }, // Level 4
{ maxMacroblocks: 8192, maxBitrate: 50000000, maxDpbMbs: 32768, level: 0x29 }, // Level 4.1
{ maxMacroblocks: 8704, maxBitrate: 50000000, maxDpbMbs: 34816, level: 0x2A }, // Level 4.2
{ maxMacroblocks: 22080, maxBitrate: 135000000, maxDpbMbs: 110400, level: 0x32 }, // Level 5
{ maxMacroblocks: 36864, maxBitrate: 240000000, maxDpbMbs: 184320, level: 0x33 }, // Level 5.1
{ maxMacroblocks: 36864, maxBitrate: 240000000, maxDpbMbs: 184320, level: 0x34 }, // Level 5.2
{ maxMacroblocks: 139264, maxBitrate: 240000000, maxDpbMbs: 696320, level: 0x3C }, // Level 6
{ maxMacroblocks: 139264, maxBitrate: 480000000, maxDpbMbs: 696320, level: 0x3D }, // Level 6.1
{ maxMacroblocks: 139264, maxBitrate: 800000000, maxDpbMbs: 696320, level: 0x3E }, // Level 6.2
];
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding
@@ -528,6 +530,10 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe
return 'vorbis';
} else if (codec === 'flac') {
return 'flac';
} else if (codec === 'ac3') {
return 'ac-3';
} else if (codec === 'eac3') {
return 'ec-3';
} else if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return codec;
}
@@ -537,6 +543,7 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe
export type AacCodecInfo = {
isMpeg2: boolean;
objectType: number | null;
};
export const extractAudioCodecString = (trackInfo: {
@@ -554,8 +561,15 @@ export const extractAudioCodecString = (trackInfo: {
if (aacCodecInfo.isMpeg2) {
return 'mp4a.67';
} else {
const audioSpecificConfig = parseAacAudioSpecificConfig(codecDescription);
return `mp4a.40.${audioSpecificConfig.objectType}`;
let objectType: number;
if (aacCodecInfo.objectType !== null) {
objectType = aacCodecInfo.objectType;
} else {
const audioSpecificConfig = parseAacAudioSpecificConfig(codecDescription);
objectType = audioSpecificConfig.objectType;
}
return `mp4a.40.${objectType}`;
}
} else if (codec === 'mp3') {
return 'mp3';
@@ -565,6 +579,10 @@ export const extractAudioCodecString = (trackInfo: {
return 'vorbis';
} else if (codec === 'flac') {
return 'flac';
} else if (codec === 'ac3') {
return 'ac-3';
} else if (codec === 'eac3') {
return 'ec-3';
} else if (codec && (PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return codec;
}
@@ -734,6 +752,10 @@ export const inferCodecFromCodecString = (codecString: string): MediaCodec | nul
return 'vorbis';
} else if (codecString === 'flac') {
return 'flac';
} else if (codecString === 'ac-3' || codecString === 'ac3') {
return 'ac3';
} else if (codecString === 'ec-3' || codecString === 'eac3') {
return 'eac3';
} else if (codecString === 'ulaw') {
return 'ulaw';
} else if (codecString === 'alaw') {
@@ -811,7 +833,7 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in'
+ ' the WebCodecs Codec Registry.',
+ ' the Mediabunny Codec Registry.',
);
}
if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) {
@@ -925,7 +947,9 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
}
};
const VALID_AUDIO_CODEC_STRING_PREFIXES = ['mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm'];
const VALID_AUDIO_CODEC_STRING_PREFIXES = [
'mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm', 'ac-3', 'ec-3',
];
export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => {
if (!metadata) {
@@ -946,7 +970,7 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError(
'Audio chunk metadata decoder configuration codec string must be a valid audio codec string as specified in'
+ ' the WebCodecs Codec Registry.',
+ ' the Mediabunny Codec Registry.',
);
}
if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
@@ -985,12 +1009,9 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
);
}
if (!metadata.decoderConfig.description) {
throw new TypeError(
'Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be'
+ ' an AudioSpecificConfig as specified in ISO 14496-3.',
);
}
// `description` may or may not be set, depending on if the format is AAC or ADTS, so don't perform any
// validation for it.
// https://www.w3.org/TR/webcodecs-aac-codec-registration
} else if (metadata.decoderConfig.codec.startsWith('mp3') || metadata.decoderConfig.codec.startsWith('mp4a')) {
// MP3-specific validation
@@ -1046,6 +1067,18 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
+ ' adhere to the format described in https://www.w3.org/TR/webcodecs-flac-codec-registration/.',
);
}
} else if (metadata.decoderConfig.codec.startsWith('ac-3') || metadata.decoderConfig.codec.startsWith('ac3')) {
// AC3-specific validation
if (metadata.decoderConfig.codec !== 'ac-3') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for AC-3 must be "ac-3".');
}
} else if (metadata.decoderConfig.codec.startsWith('ec-3') || metadata.decoderConfig.codec.startsWith('eac3')) {
// EAC3-specific validation
if (metadata.decoderConfig.codec !== 'ec-3') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for EC-3 must be "ec-3".');
}
} else if (
metadata.decoderConfig.codec.startsWith('pcm')
|| metadata.decoderConfig.codec.startsWith('ulaw')
+106 -35
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -66,7 +66,8 @@ export type ConversionOptions = {
* Video-specific options. When passing an object, the same options are applied to all video tracks. When passing a
* function, it will be invoked for each video track and is expected to return or resolve to the options
* for that specific track. The function is passed an instance of {@link InputVideoTrack} as well as a number `n`,
* which is the 1-based index of the track in the list of all video tracks.
* which is the 1-based index of the track in the list of all video tracks. Using `n` is deprecated, prefer the
* identical `track.number` instead.
*/
video?: ConversionVideoOptions
| ((track: InputVideoTrack, n: number) => MaybePromise<ConversionVideoOptions | undefined>);
@@ -75,7 +76,8 @@ export type ConversionOptions = {
* Audio-specific options. When passing an object, the same options are applied to all audio tracks. When passing a
* function, it will be invoked for each audio track and is expected to return or resolve to the options
* for that specific track. The function is passed an instance of {@link InputAudioTrack} as well as a number `n`,
* which is the 1-based index of the track in the list of all audio tracks.
* which is the 1-based index of the track in the list of all audio tracks. Using `n` is deprecated, prefer the
* identical `track.number` instead.
*/
audio?: ConversionAudioOptions
| ((track: InputAudioTrack, n: number) => MaybePromise<ConversionAudioOptions | undefined>);
@@ -84,7 +86,7 @@ export type ConversionOptions = {
trim?: {
/**
* The time in the input file in seconds at which the output file should start. Must be less than `end`.
* Defaults to 0 when omitted.
* When omitted, defaults to the start timestamp of the input or to 0, whichever is higher.
*/
start?: number;
/**
@@ -143,6 +145,12 @@ export type ConversionVideoOptions = {
* This rotation is _in addition to_ the natural rotation of the input video as specified in input file's metadata.
*/
rotate?: Rotation;
/**
* Defaults to `true`. When enabaled, Mediabunny will use the rotation metadata in the output file to perform video
* rotation whenever possible. Set this field to `false` if you want to ensure the output file does not make use of
* rotation metadata and that any rotation is baked into the video frames directly.
*/
allowRotationMetadata?: boolean;
/**
* 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.
@@ -180,6 +188,11 @@ export type ConversionVideoOptions = {
* Setting this fields forces a transcode.
*/
keyFrameInterval?: number;
/**
* A hint that configures the hardware acceleration method used when transcoding. This is best left on
* `'no-preference'`, the default.
*/
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
/** When `true`, video will always be re-encoded instead of directly copying over the encoded samples. */
forceTranscode?: boolean;
/**
@@ -304,6 +317,9 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
if (videoOptions?.rotate !== undefined && ![0, 90, 180, 270].includes(videoOptions.rotate)) {
throw new TypeError('options.video.rotate, when provided, must be 0, 90, 180 or 270.');
}
if (videoOptions?.allowRotationMetadata !== undefined && typeof videoOptions.allowRotationMetadata !== 'boolean') {
throw new TypeError('options.video.allowRotationMetadata, when provided, must be a boolean.');
}
if (videoOptions?.crop !== undefined) {
validateCropRectangle(videoOptions.crop, 'options.video.');
}
@@ -337,6 +353,15 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
) {
throw new TypeError('options.video.processedHeight, when provided, must be a positive integer.');
}
if (
videoOptions?.hardwareAcceleration !== undefined
&& !['no-preference', 'prefer-hardware', 'prefer-software'].includes(videoOptions.hardwareAcceleration)
) {
throw new TypeError(
'options.video.hardwareAcceleration, when provided, must be \'no-preference\', \'prefer-hardware\' or'
+ ' \'prefer-software\'.',
);
}
};
const validateAudioOptions = (audioOptions: ConversionAudioOptions | undefined) => {
@@ -439,9 +464,9 @@ export class Conversion {
/** @internal */
_options: ConversionOptions;
/** @internal */
_startTimestamp: number;
_startTimestamp!: number;
/** @internal */
_endTimestamp: number;
_endTimestamp!: number;
/** @internal */
_addedCounts: Record<TrackType, number> = {
@@ -539,11 +564,11 @@ export class Conversion {
if (options.trim !== undefined && (!options.trim || typeof options.trim !== 'object')) {
throw new TypeError('options.trim, when provided, must be an object.');
}
if (options.trim?.start !== undefined && (!Number.isFinite(options.trim.start) || options.trim.start < 0)) {
throw new TypeError('options.trim.start, when provided, must be a non-negative number.');
if (options.trim?.start !== undefined && (!Number.isFinite(options.trim.start))) {
throw new TypeError('options.trim.start, when provided, must be a finite number.');
}
if (options.trim?.end !== undefined && (!Number.isFinite(options.trim.end) || options.trim.end < 0)) {
throw new TypeError('options.trim.end, when provided, must be a non-negative number.');
if (options.trim?.end !== undefined && (!Number.isFinite(options.trim.end))) {
throw new TypeError('options.trim.end, when provided, must be a finite number.');
}
if (
options.trim?.start !== undefined
@@ -569,9 +594,6 @@ export class Conversion {
this.input = options.input;
this.output = options.output;
this._startTimestamp = options.trim?.start ?? 0;
this._endTimestamp = options.trim?.end ?? Infinity;
const { promise: started, resolve: start } = promiseWithResolvers();
this._started = started;
this._start = start;
@@ -579,6 +601,14 @@ export class Conversion {
/** @internal */
async _init() {
this._startTimestamp = this._options.trim?.start ?? Math.max(
await this.input.getFirstTimestamp(),
// Samples can also have negative timestamps, but the meaning typically is "don't present me", so let's cut
// those out by default.
0,
);
this._endTimestamp = Math.max(this._options.trim?.end ?? Infinity, this._startTimestamp);
const inputTracks = await this.input.getTracks();
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
@@ -743,6 +773,13 @@ export class Conversion {
`\nThe @mediabunny/mp3-encoder extension package provides support for encoding MP3.`,
);
}
if (codecs.includes('ac3') || codecs.includes('eac3')) {
elements.push(
'\nThe @mediabunny/ac3 extension package provides support'
+ ' for encoding and decoding AC-3/E-AC-3.',
);
}
} else {
elements.push('\nCheck the discardedTracks field for more info.');
}
@@ -771,9 +808,13 @@ export class Conversion {
this._executed = true;
if (this.onProgress) {
// Compute duration using only the utilized tracks
const durationPromises = this.utilizedTracks.map(x => x.computeDuration());
const duration = Math.max(0, ...await Promise.all(durationPromises));
this._computeProgress = true;
this._totalDuration = Math.min(
(await this.input.computeDuration()) - this._startTimestamp,
duration - this._startTimestamp,
this._endTimestamp - this._startTimestamp,
);
@@ -799,7 +840,7 @@ export class Conversion {
}
if (this._canceled) {
await new Promise(() => {}); // Never resolve
throw new ConversionCanceledError();
}
await this.output.finalize();
@@ -809,7 +850,10 @@ export class Conversion {
}
}
/** Cancels the conversion process. Does nothing if the conversion is already complete. */
/**
* Cancels the conversion process, causing any ongoing `execute` call to throw a `ConversionCanceledError`.
* Does nothing if the conversion is already complete.
*/
async cancel() {
if (this.output.state === 'finalizing' || this.output.state === 'finalized') {
return;
@@ -838,7 +882,8 @@ export class Conversion {
let videoSource: VideoSource;
const totalRotation = normalizeRotation(track.rotation + (trackOptions.rotate ?? 0));
const outputSupportsRotation = this.output.format.supportsVideoRotationMetadata;
const canUseRotationMetadata = this.output.format.supportsVideoRotationMetadata
&& (trackOptions.allowRotationMetadata ?? true);
const [rotatedWidth, rotatedHeight] = totalRotation % 180 === 0
? [track.codedWidth, track.codedHeight]
@@ -873,8 +918,7 @@ export class Conversion {
const firstTimestamp = await track.getFirstTimestamp();
const needsTranscode = !!trackOptions.forceTranscode
|| this._startTimestamp > 0
|| firstTimestamp < 0
|| firstTimestamp < this._startTimestamp
|| !!trackOptions.frameRate
|| trackOptions.keyFrameInterval !== undefined
|| trackOptions.process !== undefined;
@@ -883,7 +927,7 @@ export class Conversion {
// TODO This is suboptimal: Forcing a rerender when both rotation and process are set is not
// performance-optimal, but right now there's no other way because we can't change the track rotation
// metadata after the output has already started. Should be possible with API changes in v2, though!
|| (totalRotation !== 0 && (!outputSupportsRotation || trackOptions.process !== undefined))
|| (totalRotation !== 0 && (!canUseRotationMetadata || trackOptions.process !== undefined))
|| !!crop;
const alpha = trackOptions.alpha ?? 'discard';
@@ -916,17 +960,19 @@ export class Conversion {
return;
}
if (alpha === 'discard') {
// Feels hacky given that the rest of the packet is readonly. But, works for now.
delete packet.sideData.alpha;
delete packet.sideData.alphaByteLength;
}
const modifiedPacket = packet.clone({
timestamp: packet.timestamp - this._startTimestamp,
sideData: alpha === 'discard'
? {} // Remove alpha side data
: packet.sideData,
});
assert(modifiedPacket.timestamp >= 0);
this._reportProgress(track.id, packet.timestamp);
await source.add(packet, meta);
this._reportProgress(track.id, modifiedPacket.timestamp);
await source.add(modifiedPacket, meta);
if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
await this._synchronizer.wait(packet.timestamp);
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
await this._synchronizer.wait(modifiedPacket.timestamp);
}
}
@@ -974,6 +1020,7 @@ export class Conversion {
keyFrameInterval: trackOptions.keyFrameInterval,
sizeChangeBehavior: trackOptions.fit ?? 'passThrough',
alpha,
hardwareAcceleration: trackOptions.hardwareAcceleration,
};
const source = new VideoSampleSource(encodingConfig);
@@ -1134,6 +1181,7 @@ export class Conversion {
for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
if (this._canceled) {
sample.close();
lastSample?.close();
return;
}
@@ -1285,8 +1333,8 @@ export class Conversion {
let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
let needsResample = numberOfChannels !== originalNumberOfChannels
|| sampleRate !== originalSampleRate
|| this._startTimestamp > 0
|| firstTimestamp < 0;
|| firstTimestamp < this._startTimestamp
|| (firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData);
let audioCodecs = this.output.format.getSupportedAudioCodecs();
if (
@@ -1317,11 +1365,16 @@ export class Conversion {
return;
}
this._reportProgress(track.id, packet.timestamp);
await source.add(packet, meta);
const modifiedPacket = packet.clone({
timestamp: packet.timestamp - this._startTimestamp,
});
assert(modifiedPacket.timestamp >= 0);
if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
await this._synchronizer.wait(packet.timestamp);
this._reportProgress(track.id, modifiedPacket.timestamp);
await source.add(modifiedPacket, meta);
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
await this._synchronizer.wait(modifiedPacket.timestamp);
}
}
@@ -1416,9 +1469,13 @@ export class Conversion {
const sink = new AudioSampleSink(track);
for await (const sample of sink.samples(undefined, this._endTimestamp)) {
if (this._canceled) {
sample.close();
return;
}
// Offset the timestamp as needed
sample.setTimestamp(sample.timestamp - this._startTimestamp);
await this._registerAudioSample(track, trackOptions, source, sample);
sample.close();
}
@@ -1526,6 +1583,7 @@ export class Conversion {
for await (const sample of iterator) {
if (this._canceled) {
sample.close();
return;
}
@@ -1564,6 +1622,19 @@ export class Conversion {
}
}
/**
* Thrown when a conversion couldn't complete due to being canceled.
* @group Conversion
* @public
*/
export class ConversionCanceledError extends Error {
/** Creates a new {@link ConversionCanceledError}. */
constructor(message = 'Conversion has been canceled.') {
super(message);
this.name = 'ConversionCanceledError';
}
}
const MAX_TIMESTAMP_GAP = 5;
/**
+1 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
+1 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
+5 -3
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -127,7 +127,7 @@ export type VideoEncodingAdditionalOptions = {
*/
latencyMode?: 'quality' | 'realtime';
/**
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
* The full codec string as specified in the Mediabunny Codec Registry. This string must match the codec
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
*/
fullCodecString?: string;
@@ -280,7 +280,7 @@ export type AudioEncodingAdditionalOptions = {
/** Configures the bitrate mode. */
bitrateMode?: 'constant' | 'variable';
/**
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
* The full codec string as specified in the Mediabunny Codec Registry. This string must match the codec
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
*/
fullCodecString?: string;
@@ -375,6 +375,8 @@ export class Quality {
opus: 64000, // 64kbps base for Opus
mp3: 160000, // 160kbps base for MP3
vorbis: 64000, // 64kbps base for Vorbis
ac3: 384000, // 384kbps base for AC-3
eac3: 192000, // 192kbps base for E-AC-3
};
const baseBitrate = baseRates[codec as keyof typeof baseRates];
+5 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -529,6 +529,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
return 1;
}
getNumber() {
return 1;
}
getCodec() {
return 'flac' as const;
}
+1 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
+1 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
+1 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
+22 -3
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -9,6 +9,17 @@
/// <reference types="dom-mediacapture-transform" preserve="true" />
/// <reference types="dom-webcodecs" preserve="true" />
const MEDIABUNNY_LOADED_SYMBOL = Symbol.for('mediabunny loaded');
if ((globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL]) {
console.error(
'[WARNING]\nMediabunny was loaded twice.'
+ ' This will likely cause Mediabunny not to work correctly.'
+ ' Check if multiple dependencies are importing different versions of Mediabunny,'
+ ' or if something is being bundled incorrectly.',
);
}
(globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL] = true;
export {
Output,
OutputOptions,
@@ -31,6 +42,8 @@ export {
Mp3OutputFormat,
Mp3OutputFormatOptions,
Mp4OutputFormat,
MpegTsOutputFormat,
MpegTsOutputFormatOptions,
OggOutputFormat,
OggOutputFormatOptions,
WavOutputFormat,
@@ -126,25 +139,27 @@ export {
export {
InputFormat,
AdtsInputFormat,
FlacInputFormat,
IsobmffInputFormat,
MatroskaInputFormat,
Mp3InputFormat,
Mp4InputFormat,
MpegTsInputFormat,
OggInputFormat,
QuickTimeInputFormat,
WaveInputFormat,
WebMInputFormat,
FlacInputFormat,
ALL_FORMATS,
ADTS,
FLAC,
MATROSKA,
MP3,
MP4,
MPEG_TS,
OGG,
QTFF,
WAVE,
WEBM,
FLAC,
} from './input-format';
export {
Input,
@@ -168,7 +183,10 @@ export {
AudioSampleCopyToOptions,
VideoSample,
VideoSampleInit,
VideoSamplePixelFormat,
VideoSampleColorSpace,
CropRectangle,
VIDEO_SAMPLE_PIXEL_FORMATS,
} from './sample';
export {
AudioBufferSink,
@@ -187,6 +205,7 @@ export {
ConversionOptions,
ConversionVideoOptions,
ConversionAudioOptions,
ConversionCanceledError,
DiscardedTrack,
} from './conversion';
export {
+106 -23
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -21,15 +21,17 @@ import {
} from './matroska/ebml';
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
import { Mp3Demuxer } from './mp3/mp3-demuxer';
import { FRAME_HEADER_SIZE } from '../shared/mp3-misc';
import { FRAME_HEADER_SIZE, getXingOffset, INFO, XING } from '../shared/mp3-misc';
import { ID3_V2_HEADER_SIZE, readId3V2Header } from './id3';
import { readNextFrameHeader } from './mp3/mp3-reader';
import { readNextMp3FrameHeader } from './mp3/mp3-reader';
import { OggDemuxer } from './ogg/ogg-demuxer';
import { WaveDemuxer } from './wave/wave-demuxer';
import { MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts/adts-reader';
import { MAX_ADTS_FRAME_HEADER_SIZE, MIN_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from './adts/adts-reader';
import { AdtsDemuxer } from './adts/adts-demuxer';
import { readAscii } from './reader';
import { readAscii, readBytes, readU32Be } from './reader';
import { FlacDemuxer } from './flac/flac-demuxer';
import { MpegTsDemuxer } from './mpeg-ts/mpeg-ts-demuxer';
import { TS_PACKET_SIZE } from './mpeg-ts/mpeg-ts-misc';
/**
* Base class representing an input media file format.
@@ -155,7 +157,7 @@ export class MatroskaInputFormat extends InputFormat {
}
const dataSize = readElementSize(headerSlice);
if (dataSize === null) {
if (typeof dataSize !== 'number') {
return false; // Miss me with that shit
}
@@ -171,7 +173,7 @@ export class MatroskaInputFormat extends InputFormat {
const { id, size } = header;
const dataStartPos = dataSlice.filePos;
if (size === null) return false;
if (size === undefined) return false;
switch (id) {
case EBMLId.EBMLVersion: {
@@ -259,12 +261,7 @@ export class WebMInputFormat extends MatroskaInputFormat {
export class Mp3InputFormat extends InputFormat {
/** @internal */
async _canReadInput(input: Input) {
let slice = input._reader.requestSlice(0, 10);
if (slice instanceof Promise) slice = await slice;
if (!slice) return false;
let currentPos = 0;
let id3V2HeaderFound = false;
while (true) {
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
@@ -276,17 +273,26 @@ export class Mp3InputFormat extends InputFormat {
break;
}
id3V2HeaderFound = true;
currentPos = slice.filePos + id3V2Header.size;
}
const firstResult = await readNextFrameHeader(input._reader, currentPos, currentPos + 4096);
const firstResult = await readNextMp3FrameHeader(input._reader, currentPos, currentPos + 4096);
if (!firstResult) {
return false;
}
if (id3V2HeaderFound) {
// If there was an ID3v2 tag at the start, we can be pretty sure this is MP3 by now
const firstHeader = firstResult.header;
const xingOffset = getXingOffset(firstHeader.mpegVersionId, firstHeader.channel);
let slice = input._reader.requestSlice(firstResult.startPos + xingOffset, 4);
if (slice instanceof Promise) slice = await slice;
if (!slice) return false;
const word = readU32Be(slice);
const isXing = word === XING || word === INFO;
if (isXing) {
// Gotta be MP3
return true;
}
@@ -294,12 +300,11 @@ export class Mp3InputFormat extends InputFormat {
// Fine, we found one frame header, but we're still not entirely sure this is MP3. Let's check if we can find
// another header right after it:
const secondResult = await readNextFrameHeader(input._reader, currentPos, currentPos + FRAME_HEADER_SIZE);
const secondResult = await readNextMp3FrameHeader(input._reader, currentPos, currentPos + FRAME_HEADER_SIZE);
if (!secondResult) {
return false;
}
const firstHeader = firstResult.header;
const secondHeader = secondResult.header;
// In a well-formed MP3 file, we'd expect these two frames to share some similarities:
@@ -439,20 +444,45 @@ export class FlacInputFormat extends InputFormat {
export class AdtsInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input: Input) {
let slice = input._reader.requestSliceRange(0, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE);
let currentPos = 0;
while (true) {
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
if (slice instanceof Promise) slice = await slice;
if (!slice) break;
const id3V2Header = readId3V2Header(slice);
if (!id3V2Header) {
break;
}
currentPos = slice.filePos + id3V2Header.size;
}
let slice = input._reader.requestSliceRange(
currentPos,
MIN_ADTS_FRAME_HEADER_SIZE,
MAX_ADTS_FRAME_HEADER_SIZE,
);
if (slice instanceof Promise) slice = await slice;
if (!slice) return false;
const firstHeader = readFrameHeader(slice);
const firstHeader = readAdtsFrameHeader(slice);
if (!firstHeader) {
return false;
}
slice = input._reader.requestSliceRange(firstHeader.frameLength, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE);
currentPos += firstHeader.frameLength;
slice = input._reader.requestSliceRange(
currentPos,
MIN_ADTS_FRAME_HEADER_SIZE,
MAX_ADTS_FRAME_HEADER_SIZE,
);
if (slice instanceof Promise) slice = await slice;
if (!slice) return false;
const secondHeader = readFrameHeader(slice);
const secondHeader = readAdtsFrameHeader(slice);
if (!secondHeader) {
return false;
}
@@ -476,6 +506,52 @@ export class AdtsInputFormat extends InputFormat {
}
}
/**
* MPEG Transport Stream (MPEG-TS) file format.
*
* Do not instantiate this class; use the {@link MPEG_TS} singleton instead.
*
* @group Input formats
* @public
*/
export class MpegTsInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input: Input) {
const lengthToCheck = TS_PACKET_SIZE + 16 + 1;
let slice = input._reader.requestSlice(0, lengthToCheck);
if (slice instanceof Promise) slice = await slice;
if (!slice) return false;
const bytes = readBytes(slice, lengthToCheck);
if (bytes[0] === 0x47 && bytes[TS_PACKET_SIZE] === 0x47) {
// Regular MPEG-TS
return true;
} else if (bytes[0] === 0x47 && bytes[TS_PACKET_SIZE + 16] === 0x47) {
// MPEG-TS with Forward Error Correction
return true;
} else if (bytes[4] === 0x47 && bytes[4 + TS_PACKET_SIZE] === 0x47) {
// MPEG-2-TS (DVHS)
return true;
}
return false;
}
/** @internal */
_createDemuxer(input: Input) {
return new MpegTsDemuxer(input);
}
get name() {
return 'MPEG Transport Stream';
}
get mimeType() {
return 'video/MP2T';
}
}
/**
* MP4 input format singleton.
* @group Input formats
@@ -532,10 +608,17 @@ export const ADTS = /* #__PURE__ */ new AdtsInputFormat();
*/
export const FLAC = /* #__PURE__ */ new FlacInputFormat();
/**
* MPEG-TS input format singleton.
* @group Input formats
* @public
*/
export const MPEG_TS = /* #__PURE__ */ new MpegTsInputFormat();
/**
* List of all input format singletons. If you don't need to support all input formats, you should specify the
* formats individually for better tree shaking.
* @group Input formats
* @public
*/
export const ALL_FORMATS: InputFormat[] = [MP4, QTFF, MATROSKA, WEBM, WAVE, OGG, FLAC, MP3, ADTS];
export const ALL_FORMATS: InputFormat[] = [MP4, QTFF, MATROSKA, WEBM, WAVE, OGG, FLAC, MP3, ADTS, MPEG_TS];
+12 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -32,6 +32,7 @@ export type PacketStats = {
export interface InputTrackBacking {
getId(): number;
getNumber(): number;
getCodec(): MediaCodec | null;
getInternalCodecId(): string | number | Uint8Array | null;
getName(): string | null;
@@ -94,6 +95,15 @@ export abstract class InputTrack {
return this._backing.getId();
}
/**
* The 1-based index of this track among all tracks of the same type in the input file. For example, the first
* video track has number 1, the second video track has number 2, and so on. The index refers to the order in
* which the tracks are returned by {@link Input.getTracks}.
*/
get number() {
return this._backing.getNumber();
}
/**
* The identifier of the codec used internally by the container. It is not homogenized by Mediabunny
* and depends entirely on the container format.
@@ -104,6 +114,7 @@ export abstract class InputTrack {
* - For Matroska files, this field returns the value of the `CodecID` element.
* - For WAVE files, this field returns the value of the format tag in the `'fmt '` chunk.
* - For ADTS files, this field contains the `MPEG-4 Audio Object Type`.
* - For MPEG-TS files, this field contains the `streamType` value from the Program Map Table.
* - In all other cases, this field is `null`.
*/
get internalCodecId() {
+15 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -118,6 +118,20 @@ export class Input<S extends Source = Source> implements Disposable {
return demuxer.computeDuration();
}
/**
* Returns the timestamp at which the input file starts. More precisely, returns the smallest starting timestamp
* among all tracks.
*/
async getFirstTimestamp() {
const tracks = await this.getTracks();
if (tracks.length === 0) {
return 0;
}
const firstTimestamps = await Promise.all(tracks.map(x => x.getFirstTimestamp()));
return Math.min(...firstTimestamps);
}
/** Returns the list of all tracks of this input file. */
async getTracks() {
const demuxer = await this._getDemuxer();
+80 -2
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -20,6 +20,7 @@ import {
UNDETERMINED_LANGUAGE,
assertNever,
keyValueIterator,
Bitstream,
} from '../misc';
import {
AudioCodec,
@@ -43,7 +44,7 @@ import {
IsobmffVideoTrackData,
Sample,
} from './isobmff-muxer';
import { parseOpusIdentificationHeader } from '../codec-data';
import { parseAc3SyncFrame, parseEac3SyncFrame, parseOpusIdentificationHeader } from '../codec-data';
import { MetadataTags, RichImageData } from '../metadata';
export class IsobmffBoxWriter {
@@ -907,6 +908,79 @@ const pcmC = (trackData: IsobmffAudioTrackData) => {
]);
};
/** AC3SpecificBox */
const dac3 = (trackData: IsobmffAudioTrackData) => {
const frameInfo = parseAc3SyncFrame(trackData.info.firstPacket.data);
if (!frameInfo) {
throw new Error(
'Couldn\'t extract AC-3 frame info from the audio packet. '
+ 'Ensure the packets contain valid AC-3 sync frames (as specified in ETSI TS 102 366).',
);
}
const bytes = new Uint8Array(3);
const bitstream = new Bitstream(bytes);
bitstream.writeBits(2, frameInfo.fscod);
bitstream.writeBits(5, frameInfo.bsid);
bitstream.writeBits(3, frameInfo.bsmod);
bitstream.writeBits(3, frameInfo.acmod);
bitstream.writeBits(1, frameInfo.lfeon);
bitstream.writeBits(5, frameInfo.bitRateCode);
bitstream.writeBits(5, 0); // reserved
return box('dac3', [...bytes]);
};
/** EC3SpecificBox */
const dec3 = (trackData: IsobmffAudioTrackData) => {
const frameInfo = parseEac3SyncFrame(trackData.info.firstPacket.data);
if (!frameInfo) {
throw new Error(
'Couldn\'t extract E-AC-3 frame info from the audio packet. '
+ 'Ensure the packets contain valid E-AC-3 sync frames (as specified in ETSI TS 102 366).',
);
}
// Calculate size
let totalBits = 16; // header: data_rate (13) + num_ind_sub (3)
for (const sub of frameInfo.substreams) {
totalBits += 23; // fixed fields per substream
if (sub.numDepSub > 0) {
totalBits += 9; // chan_loc
} else {
totalBits += 1; // reserved
}
}
const size = Math.ceil(totalBits / 8);
const bytes = new Uint8Array(size);
const bitstream = new Bitstream(bytes);
bitstream.writeBits(13, frameInfo.dataRate);
bitstream.writeBits(3, frameInfo.substreams.length - 1); // num_ind_sub
for (const sub of frameInfo.substreams) {
bitstream.writeBits(2, sub.fscod);
bitstream.writeBits(5, sub.bsid);
bitstream.writeBits(1, 0); // reserved
bitstream.writeBits(1, 0); // asvc = 0
bitstream.writeBits(3, sub.bsmod);
bitstream.writeBits(3, sub.acmod);
bitstream.writeBits(1, sub.lfeon);
bitstream.writeBits(3, 0); // reserved
bitstream.writeBits(4, sub.numDepSub);
if (sub.numDepSub > 0) {
bitstream.writeBits(9, sub.chanLoc);
} else {
bitstream.writeBits(1, 0); // reserved
}
}
return box('dec3', [...bytes]);
};
export const subtitleSampleDescription = (
compressionType: string,
trackData: IsobmffSubtitleTrackData,
@@ -1606,6 +1680,8 @@ const audioCodecToBoxName = (codec: AudioCodec, isQuickTime: boolean): string =>
case 'alaw': return 'alaw';
case 'pcm-u8': return 'raw ';
case 'pcm-s8': return 'sowt';
case 'ac3': return 'ac-3';
case 'eac3': return 'ec-3';
}
// Logic diverges here
@@ -1645,6 +1721,8 @@ const audioCodecToConfigurationBox = (codec: AudioCodec, isQuickTime: boolean) =
case 'opus': return dOps;
case 'vorbis': return esds;
case 'flac': return dfLa;
case 'ac3': return dac3;
case 'eac3': return dec3;
}
// Logic diverges here
+78 -2
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -27,6 +27,10 @@ import {
FlacBlockType,
HevcDecoderConfigurationRecord,
Vp9CodecInfo,
parseEac3Config,
getEac3SampleRate,
getEac3ChannelCount,
AC3_ACMOD_CHANNEL_COUNTS,
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
@@ -87,6 +91,7 @@ import {
readAscii,
} from '../reader';
import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDisposition } from '../metadata';
import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc';
type InternalTrack = {
id: number;
@@ -952,6 +957,10 @@ export class IsobmffDemuxer extends Demuxer {
track.info.codec = 'ulaw';
} else if (lowercaseBoxName === 'alaw') {
track.info.codec = 'alaw';
} else if (lowercaseBoxName === 'ac-3') {
track.info.codec = 'ac3';
} else if (lowercaseBoxName === 'ec-3') {
track.info.codec = 'eac3';
} else {
console.warn(`Unsupported audio codec (sample entry type '${sampleBoxInfo.name}').`);
}
@@ -1235,7 +1244,10 @@ export class IsobmffDemuxer extends Demuxer {
const objectTypeIndication = readU8(slice);
if (objectTypeIndication === 0x40 || objectTypeIndication === 0x67) {
track.info.codec = 'aac';
track.info.aacCodecInfo = { isMpeg2: objectTypeIndication === 0x67 };
track.info.aacCodecInfo = {
isMpeg2: objectTypeIndication === 0x67,
objectType: null,
};
} else if (objectTypeIndication === 0x69 || objectTypeIndication === 0x6b) {
track.info.codec = 'mp3';
} else if (objectTypeIndication === 0xdd) {
@@ -1460,6 +1472,51 @@ export class IsobmffDemuxer extends Demuxer {
track.info.codecDescription = description;
}; break;
case 'dac3': { // AC3SpecificBox
const track = this.currentTrack;
if (!track) {
break;
}
assert(track.info?.type === 'audio');
const bytes = readBytes(slice, 3);
const bitstream = new Bitstream(bytes);
const fscod = bitstream.readBits(2);
bitstream.skipBits(5 + 3); // Skip bsid and bsmod
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
if (fscod < 3) {
track.info.sampleRate = AC3_SAMPLE_RATES[fscod]!;
}
track.info.numberOfChannels = AC3_ACMOD_CHANNEL_COUNTS[acmod]! + lfeon;
}; break;
case 'dec3': { // EC3SpecificBox
const track = this.currentTrack;
if (!track) {
break;
}
assert(track.info?.type === 'audio');
const bytes = readBytes(slice, boxInfo.contentSize);
const config = parseEac3Config(bytes);
if (!config) {
console.warn('Invalid dec3 box contents, ignoring.');
break;
}
const sampleRate = getEac3SampleRate(config);
if (sampleRate !== null) {
track.info.sampleRate = sampleRate;
}
track.info.numberOfChannels = getEac3ChannelCount(config);
}; break;
case 'stts': {
const track = this.currentTrack;
if (!track) {
@@ -2332,6 +2389,25 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
return this.internalTrack.id;
}
getNumber() {
const demuxer = this.internalTrack.demuxer;
const inputTrack = this.internalTrack.inputTrack!;
const trackType = inputTrack.type;
let number = 0;
for (const track of demuxer.tracks) {
if (track.inputTrack!.type === trackType) {
number++;
}
if (track === this.internalTrack) {
break;
}
}
return number;
}
getCodec(): MediaCodec | null {
throw new Error('Not implemented on base class.');
}
+1 -1
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
+72 -10
View File
@@ -1,5 +1,5 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -14,6 +14,9 @@ import { assert, computeRationalApproximation, last, promiseWithResolvers } from
import { IsobmffOutputFormatOptions, IsobmffOutputFormat, MovOutputFormat } from '../output-format';
import { inlineTimestampRegex, SubtitleConfig, SubtitleCue, SubtitleMetadata } from '../subtitles';
import {
aacChannelMap,
aacFrequencyTable,
buildAacAudioSpecificConfig,
parsePcmCodec,
PCM_AUDIO_CODECS,
PcmAudioCodec,
@@ -22,14 +25,17 @@ import {
validateSubtitleMetadata,
validateVideoChunkMetadata,
} from '../codec';
import { MAX_ADTS_FRAME_HEADER_SIZE, MIN_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from '../adts/adts-reader';
import { FileSlice } from '../reader';
import { BufferTarget } from '../target';
import { EncodedPacket, PacketType } from '../packet';
import {
concatNalUnitsInLengthPrefixed,
extractAvcDecoderConfigurationRecord,
extractHevcDecoderConfigurationRecord,
iterateNalUnitsInAnnexB,
serializeAvcDecoderConfigurationRecord,
serializeHevcDecoderConfigurationRecord,
transformAnnexBToLengthPrefixed,
} from '../codec-data';
import { buildIsobmffMimeType } from './isobmff-misc';
import { MAX_BOX_HEADER_SIZE, MIN_BOX_HEADER_SIZE } from './isobmff-reader';
@@ -100,6 +106,12 @@ export type IsobmffTrackData = {
* Some players expect this for PCM audio.
*/
requiresPcmTransformation: boolean;
/**
* The "ADTS stripping" involves removing the ADTS header from each AAC packet. SOBMFF stores raw AAC data, not
* ADTS-wrapped data.
*/
requiresAdtsStripping: boolean;
firstPacket: EncodedPacket;
};
} | {
track: OutputSubtitleTrack;
@@ -364,7 +376,7 @@ export class IsobmffMuxer extends Muxer {
return newTrackData;
}
private getAudioTrackData(track: OutputAudioTrack, meta?: EncodedAudioChunkMetadata) {
private getAudioTrackData(track: OutputAudioTrack, packet: EncodedPacket, meta?: EncodedAudioChunkMetadata) {
const existingTrackData = this.trackDatas.find(x => x.track === track);
if (existingTrackData) {
return existingTrackData as IsobmffAudioTrackData;
@@ -375,6 +387,37 @@ export class IsobmffMuxer extends Muxer {
assert(meta);
assert(meta.decoderConfig);
const decoderConfig = { ...meta.decoderConfig };
let requiresAdtsStripping = false;
if (track.source._codec === 'aac' && !decoderConfig.description) {
// ISOBMFF can only hold AAC in raw format, not ADTS, but the missing description indicates ADTS.
// Parse the first packet to extract the AudioSpecificConfig.
const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packet.data));
if (!adtsFrame) {
throw new Error(
'Couldn\'t parse ADTS header from the AAC packet. Make sure the packets are in ADTS format'
+ ' (as specified in ISO 13818-7) when not providing a description, or provide a description'
+ ' (must be an AudioSpecificConfig as specified in ISO 14496-3) and ensure the packets'
+ ' are raw AAC data.',
);
}
const sampleRate = aacFrequencyTable[adtsFrame.samplingFrequencyIndex];
const numberOfChannels = aacChannelMap[adtsFrame.channelConfiguration];
if (sampleRate === undefined || numberOfChannels === undefined) {
throw new Error('Invalid ADTS frame header.');
}
decoderConfig.description = buildAacAudioSpecificConfig({
objectType: adtsFrame.objectType,
sampleRate,
numberOfChannels,
});
requiresAdtsStripping = true;
}
const newTrackData: IsobmffAudioTrackData = {
muxer: this,
track,
@@ -382,12 +425,14 @@ export class IsobmffMuxer extends Muxer {
info: {
numberOfChannels: meta.decoderConfig.numberOfChannels,
sampleRate: meta.decoderConfig.sampleRate,
decoderConfig: meta.decoderConfig,
decoderConfig,
requiresPcmTransformation:
!this.isFragmented
&& (PCM_AUDIO_CODECS as readonly string[]).includes(track.source._codec),
requiresAdtsStripping,
firstPacket: packet,
},
timescale: meta.decoderConfig.sampleRate,
timescale: decoderConfig.sampleRate,
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
@@ -464,15 +509,19 @@ export class IsobmffMuxer extends Muxer {
let packetData = packet.data;
if (trackData.info.requiresAnnexBTransformation) {
const transformedData = transformAnnexBToLengthPrefixed(packetData);
if (!transformedData) {
const nalUnits = [...iterateNalUnitsInAnnexB(packetData)]
.map(loc => packetData.subarray(loc.offset, loc.offset + loc.length));
if (nalUnits.length === 0) {
// It's not valid Annex B data
throw new Error(
'Failed to transform packet data. Make sure all packets are provided in Annex B format, as'
+ ' specified in ITU-T-REC-H.264 and ITU-T-REC-H.265.',
);
}
packetData = transformedData;
// We don't strip things like SPS or PPS NALUs here, mainly because they can also appear in the middle
// of a stream and potentially modify the parameters of it. So, let's just leave them in to be sure.
packetData = concatNalUnitsInLengthPrefixed(nalUnits, 4);
}
const timestamp = this.validateAndNormalizeTimestamp(
@@ -498,7 +547,20 @@ export class IsobmffMuxer extends Muxer {
const release = await this.mutex.acquire();
try {
const trackData = this.getAudioTrackData(track, meta);
const trackData = this.getAudioTrackData(track, packet, meta);
let packetData = packet.data;
if (trackData.info.requiresAdtsStripping) {
const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packetData));
if (!adtsFrame) {
throw new Error('Expected ADTS frame, didn\'t get one.');
}
const headerLength = adtsFrame.crcCheck === null
? MIN_ADTS_FRAME_HEADER_SIZE
: MAX_ADTS_FRAME_HEADER_SIZE;
packetData = packetData.subarray(headerLength);
}
const timestamp = this.validateAndNormalizeTimestamp(
trackData.track,
@@ -507,7 +569,7 @@ export class IsobmffMuxer extends Muxer {
);
const internalSample = this.createSampleForTrack(
trackData,
packet.data,
packetData,
timestamp,
packet.duration,
packet.type,

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