diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 31b076b..441a113 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -59,7 +59,28 @@ 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 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
+ 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
+ packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.js
+ packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.min.js
+ packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.mjs
+ packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.min.mjs
+ packages/aac-encoder/dist/mediabunny-aac-encoder.d.ts
- name: Publish Mediabunny to npm
run: npm publish --access public
diff --git a/.gitignore b/.gitignore
index 289f126..e4b72d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@ node_modules
*.tsbuildinfo
packages/mp3-encoder/dist
-packages/ac3/dist
\ No newline at end of file
+packages/ac3/dist
+packages/aac-encoder/dist
\ No newline at end of file
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 7452915..a050b49 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -93,6 +93,7 @@ export default withMermaid({
text: 'Extensions',
items: [
{ text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' },
+ { text: 'aac-encoder', link: '/guide/extensions/aac-encoder' },
{ text: 'ac3', link: '/guide/extensions/ac3' },
],
},
diff --git a/docs/api-config.json b/docs/api-config.json
index 202768f..53ba487 100644
--- a/docs/api-config.json
+++ b/docs/api-config.json
@@ -20,5 +20,6 @@
"Miscellaneous": "Whatever's left.",
"@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny.",
- "@mediabunny/ac3": "Adds AC-3/E-AC-3 decoder and encoder support to Mediabunny."
+ "@mediabunny/ac3": "Adds AC-3/E-AC-3 decoder and encoder support to Mediabunny.",
+ "@mediabunny/aac-encoder": "Adds AAC encoder support to Mediabunny."
}
diff --git a/docs/guide/extensions/aac-encoder.md b/docs/guide/extensions/aac-encoder.md
new file mode 100644
index 0000000..a8b3488
--- /dev/null
+++ b/docs/guide/extensions/aac-encoder.md
@@ -0,0 +1,82 @@
+# @mediabunny/aac-encoder
+
+Some browsers lack support for AAC encoding in their WebCodecs implementations. This extension package provides a reliable AAC-LC encoder for use with Mediabunny. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [FFmpeg](https://ffmpeg.org/)'s AAC encoder under the hood.
+
+
+ GitHub page
+
+
+
+## Installation
+
+This library peer-depends on Mediabunny. Install both using npm:
+```bash
+npm install mediabunny @mediabunny/aac-encoder
+```
+
+Alternatively, directly include them using a script tag:
+```html
+
+
+```
+
+This will expose the global objects `Mediabunny` and `MediabunnyAacEncoder`. Use `mediabunny-aac-encoder.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 { registerAacEncoder } from '@mediabunny/aac-encoder';
+
+registerAacEncoder();
+```
+That's it - Mediabunny now uses the registered AAC encoder automatically.
+
+If you want to be more correct, check for native browser support first:
+```ts
+import { canEncodeAudio } from 'mediabunny';
+import { registerAacEncoder } from '@mediabunny/aac-encoder';
+
+if (!(await canEncodeAudio('aac'))) {
+ registerAacEncoder();
+}
+```
+
+## Example
+
+Here, we convert an input file to an MP4 with AAC audio:
+
+```ts
+import {
+ Input,
+ ALL_FORMATS,
+ BlobSource,
+ Output,
+ BufferTarget,
+ Mp4OutputFormat,
+ canEncodeAudio,
+ Conversion,
+} from 'mediabunny';
+import { registerAacEncoder } from '@mediabunny/aac-encoder';
+
+if (!(await canEncodeAudio('aac'))) {
+ // Only register the custom encoder if there's no native support
+ registerAacEncoder();
+}
+
+const input = new Input({
+ source: new BlobSource(file), // From a file picker, for example
+ formats: ALL_FORMATS,
+});
+const output = new Output({
+ format: new Mp4OutputFormat(),
+ target: new BufferTarget(),
+});
+
+const conversion = await Conversion.init({
+ input,
+ output,
+});
+await conversion.execute();
+
+output.target.buffer; // => ArrayBuffer containing the MP4 file
+```
diff --git a/docs/guide/supported-formats-and-codecs.md b/docs/guide/supported-formats-and-codecs.md
index dbaf533..1e2f5c3 100644
--- a/docs/guide/supported-formats-and-codecs.md
+++ b/docs/guide/supported-formats-and-codecs.md
@@ -37,13 +37,13 @@ Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, m
### Audio codecs
-- `'aac'` - Advanced Audio Coding (AAC)
+- `'aac'` - Advanced Audio Coding (AAC) [^1]
- `'opus'` - Opus
-- `'mp3'` - MP3
+- `'mp3'` - MP3 [^2]
- `'vorbis'` - Vorbis
- `'flac'` - Free Lossless Audio Codec (FLAC)
-- `'ac3'` - Dolby Digital (AC-3) [^1]
-- `'eac3'` - Dolby Digital Plus (E-AC-3) [^1]
+- `'ac3'` - Dolby Digital (AC-3) [^3]
+- `'eac3'` - Dolby Digital Plus (E-AC-3) [^3]
- `'pcm-u8'` - 8-bit unsigned PCM
- `'pcm-s8'` - 8-bit signed PCM
- `'pcm-s16'` - 16-bit little-endian signed PCM
@@ -59,8 +59,6 @@ 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
@@ -69,7 +67,7 @@ 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[^2] | .ogg | .mp3 | .wav | .aac | .flac | .ts |
+| | .mp4 | .mov | .mkv | .webm[^4] | .ogg | .mp3 | .wav | .aac | .flac | .ts |
|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
| `'avc'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
| `'hevc'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
@@ -97,11 +95,13 @@ Not all codecs can be used with all containers. The following table specifies th
| `'pcm-f64be'` | ✓ | ✓ | | | | | | | | |
| `'ulaw'` | | ✓ | | | | | ✓ | | | |
| `'alaw'` | | ✓ | | | | | ✓ | | | |
-| `'webvtt'`[^3] | (✓) | | (✓) | (✓) | | | | | | |
+| `'webvtt'`[^5] | (✓) | | (✓) | (✓) | | | | | | |
-
-[^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.
+[^1]: In some browsers, AAC encoding is not supported by WebCodecs. You can polyfill it with the [`@mediabunny/aac-encoder`](./extensions/aac-encoder) extension package, or provide your own [custom coder](#custom-coders).
+[^2]: MP3 encoding is not supported by WebCodecs. You can polyfill it with the [`@mediabunny/mp3-encoder`](./extensions/mp3-encoder) extension package, or provide your own [custom coder](#custom-coders).
+[^3]: 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).
+[^4]: 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.
+[^5]: WebVTT can only be written, not read.
## Querying codec encodability
diff --git a/eslint.config.mjs b/eslint.config.mjs
index fa924e2..95f80b1 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -40,6 +40,8 @@ export default tseslint.config(
'packages/mp3-encoder/build',
'packages/ac3/dist',
'packages/ac3/build',
+ 'packages/aac-encoder/dist',
+ 'packages/aac-encoder/build',
'eslint.config.mjs',
'docs/.vitepress/cache',
'test/public',
diff --git a/package-lock.json b/package-lock.json
index 7fb6a76..4cdbc8f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1413,6 +1413,10 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mediabunny/aac-encoder": {
+ "resolved": "packages/aac-encoder",
+ "link": true
+ },
"node_modules/@mediabunny/ac3": {
"resolved": "packages/ac3",
"link": true
@@ -12067,6 +12071,21 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "packages/aac-encoder": {
+ "name": "@mediabunny/aac-encoder",
+ "version": "1.35.0",
+ "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/ac3": {
"name": "@mediabunny/ac3",
"version": "1.35.1",
diff --git a/package.json b/package.json
index 15fe549..536af7e 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"docs:dev": "vitepress dev docs",
"docs:build": "npm run build && npm run docs:generate && vitepress build docs && npm run examples:build && cp dist/mediabunny.d.ts dist-docs/",
"docs:preview": "vitepress preview docs",
- "docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts docs/api-config.json",
+ "docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts packages/aac-encoder/src/index.ts docs/api-config.json",
"dev": "vite",
"examples:build": "vite build",
"fix-build-import-paths": "tsx scripts/add-import-extensions.ts",
diff --git a/packages/aac-encoder/.gitattributes b/packages/aac-encoder/.gitattributes
new file mode 100644
index 0000000..72ef19a
--- /dev/null
+++ b/packages/aac-encoder/.gitattributes
@@ -0,0 +1 @@
+build/* linguist-generated
diff --git a/packages/aac-encoder/LICENSE b/packages/aac-encoder/LICENSE
new file mode 100644
index 0000000..d0a1fa1
--- /dev/null
+++ b/packages/aac-encoder/LICENSE
@@ -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.
diff --git a/packages/aac-encoder/README.md b/packages/aac-encoder/README.md
new file mode 100644
index 0000000..69062c2
--- /dev/null
+++ b/packages/aac-encoder/README.md
@@ -0,0 +1,156 @@
+# @mediabunny/aac-encoder
+
+[](https://www.npmjs.com/package/@mediabunny/aac-encoder)
+[](https://bundlephobia.com/package/@mediabunny/aac-encoder)
+[](https://www.npmjs.com/package/@mediabunny/aac-encoder)
+[](https://discord.gg/hmpkyYuS4U)
+
+
+

+
+
+Some browsers lack support for AAC encoding in their WebCodecs implementations. This extension package provides a reliable AAC-LC encoder for use with [Mediabunny](https://github.com/Vanilagy/mediabunny). 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 AAC encoder 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/aac-encoder
+```
+
+Alternatively, directly include them using a script tag:
+```html
+
+
+```
+
+This will expose the global objects `Mediabunny` and `MediabunnyAacEncoder`. Use `mediabunny-aac-encoder.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 { registerAacEncoder } from '@mediabunny/aac-encoder';
+
+registerAacEncoder();
+```
+That's it - Mediabunny now uses the registered AAC encoder automatically.
+
+If you want to be more correct, check for native browser support first:
+```ts
+import { canEncodeAudio } from 'mediabunny';
+import { registerAacEncoder } from '@mediabunny/aac-encoder';
+
+if (!(await canEncodeAudio('aac'))) {
+ registerAacEncoder();
+}
+```
+
+## Example
+
+Here, we convert an input file to an MP4 with AAC audio:
+
+```ts
+import {
+ Input,
+ ALL_FORMATS,
+ BlobSource,
+ Output,
+ BufferTarget,
+ Mp4OutputFormat,
+ canEncodeAudio,
+ Conversion,
+} from 'mediabunny';
+import { registerAacEncoder } from '@mediabunny/aac-encoder';
+
+if (!(await canEncodeAudio('aac'))) {
+ // Only register the custom encoder if there's no native support
+ registerAacEncoder();
+}
+
+const input = new Input({
+ source: new BlobSource(file), // From a file picker, for example
+ formats: ALL_FORMATS,
+});
+const output = new Output({
+ format: new Mp4OutputFormat(),
+ target: new BufferTarget(),
+});
+
+const conversion = await Conversion.init({
+ input,
+ output,
+});
+await conversion.execute();
+
+output.target.buffer; // => ArrayBuffer containing the MP4 file
+```
+
+For more ways of using Mediabunny, refer to its [guide](https://mediabunny.dev/guide/introduction).
+
+## 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-encoder=aac \
+ --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/aac-encoder
+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/aac.js
+```
+
+This generates `build/aac.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.
diff --git a/packages/aac-encoder/api-extractor.json b/packages/aac-encoder/api-extractor.json
new file mode 100644
index 0000000..6908bb7
--- /dev/null
+++ b/packages/aac-encoder/api-extractor.json
@@ -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-aac-encoder.d.ts"
+ },
+ "tsdocMetadata": {
+ "enabled": false
+ },
+ "messages": {
+ "compilerMessageReporting": {
+ "default": {
+ "logLevel": "warning"
+ }
+ },
+ "extractorMessageReporting": {
+ "default": {
+ "logLevel": "warning"
+ }
+ },
+ "tsdocMessageReporting": {
+ "default": {
+ "logLevel": "warning"
+ }
+ }
+ },
+ "newlineKind": "lf"
+}
diff --git a/packages/aac-encoder/build/aac.js b/packages/aac-encoder/build/aac.js
new file mode 100644
index 0000000..9bd7d8d
Binary files /dev/null and b/packages/aac-encoder/build/aac.js differ
diff --git a/packages/aac-encoder/package.json b/packages/aac-encoder/package.json
new file mode 100644
index 0000000..5f7560f
--- /dev/null
+++ b/packages/aac-encoder/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@mediabunny/aac-encoder",
+ "author": "Vanilagy",
+ "version": "1.35.1",
+ "description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
+ "main": "./dist/bundles/mediabunny-aac-encoder.mjs",
+ "module": "./dist/bundles/mediabunny-aac-encoder.mjs",
+ "types": "./dist/modules/src/index.d.ts",
+ "exports": {
+ "types": "./dist/modules/src/index.d.ts",
+ "import": "./dist/bundles/mediabunny-aac-encoder.mjs",
+ "require": "./dist/bundles/mediabunny-aac-encoder.mjs"
+ },
+ "files": [
+ "README.md",
+ "package.json",
+ "LICENSE",
+ "dist",
+ "src"
+ ],
+ "browser": {
+ "worker_threads": false
+ },
+ "sideEffects": false,
+ "license": "MPL-2.0",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Vanilagy/mediabunny.git",
+ "directory": "packages/aac-encoder"
+ },
+ "bugs": {
+ "url": "https://github.com/Vanilagy/mediabunny/issues"
+ },
+ "homepage": "https://mediabunny.dev/guide/extensions/aac-encoder",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/Vanilagy"
+ },
+ "peerDependencies": {
+ "mediabunny": "^1.0.0"
+ },
+ "devDependencies": {
+ "@types/emscripten": "^1.40.1"
+ },
+ "keywords": [
+ "aac",
+ "encoding",
+ "codec",
+ "mediabunny",
+ "ffmpeg",
+ "browser",
+ "wasm",
+ "polyfill"
+ ]
+}
diff --git a/packages/aac-encoder/src/bridge.c b/packages/aac-encoder/src/bridge.c
new file mode 100644
index 0000000..0306e38
--- /dev/null
+++ b/packages/aac-encoder/src/bridge.c
@@ -0,0 +1,190 @@
+/*!
+ * 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
+#include
+#include
+#include "libavcodec/avcodec.h"
+#include "libavutil/opt.h"
+#include "libavutil/channel_layout.h"
+#include "libavutil/log.h"
+
+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 channels, int sample_rate, int bitrate) {
+ av_log_set_level(AV_LOG_ERROR);
+
+ const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
+ 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;
+ }
+
+ 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
+uint8_t *get_encoder_extradata(EncoderContext *ctx) {
+ return ctx->codec_ctx->extradata;
+}
+
+EMSCRIPTEN_KEEPALIVE
+int get_encoder_extradata_size(EncoderContext *ctx) {
+ return ctx->codec_ctx->extradata_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 send_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];
+ }
+ }
+
+ return avcodec_send_frame(ctx->codec_ctx, ctx->frame);
+}
+
+EMSCRIPTEN_KEEPALIVE
+int receive_packet(EncoderContext *ctx) {
+ int ret = avcodec_receive_packet(ctx->codec_ctx, ctx->packet);
+ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
+ return 0;
+ }
+ 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_start(EncoderContext *ctx) {
+ avcodec_send_frame(ctx->codec_ctx, NULL);
+}
+
+EMSCRIPTEN_KEEPALIVE
+void reset_encoder(EncoderContext *ctx) {
+ avcodec_flush_buffers(ctx->codec_ctx);
+}
+
+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);
+}
diff --git a/packages/aac-encoder/src/encode.worker.ts b/packages/aac-encoder/src/encode.worker.ts
new file mode 100644
index 0000000..d109920
--- /dev/null
+++ b/packages/aac-encoder/src/encode.worker.ts
@@ -0,0 +1,204 @@
+/*!
+ * 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/aac';
+import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
+
+type ExtendedEmscriptenModule = EmscriptenModule & {
+ cwrap: typeof cwrap;
+};
+
+let module: ExtendedEmscriptenModule;
+let modulePromise: Promise | null = null;
+
+let initEncoderFn: (channels: number, sampleRate: number, bitrate: number) => number;
+let getEncoderFrameSize: (ctx: number) => number;
+let getEncoderExtradata: (ctx: number) => number;
+let getEncoderExtradataSize: (ctx: number) => number;
+let getEncodeInputPtr: (ctx: number, size: number) => number;
+let sendFrameFn: (ctx: number, pts: number) => number;
+let receivePacketFn: (ctx: number) => number;
+let flushEncoderStartFn: (ctx: number) => void;
+let resetEncoderFn: (ctx: number) => void;
+let getEncodedData: (ctx: number) => number;
+let getEncodedPts: (ctx: number) => number;
+let getEncodedDuration: (ctx: number) => number;
+let closeEncoderFn: (ctx: number) => void;
+
+const ensureModule = async () => {
+ if (!module) {
+ if (modulePromise) {
+ return modulePromise;
+ }
+
+ modulePromise = createModule() as Promise;
+ module = await modulePromise;
+ modulePromise = null;
+
+ initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number']);
+ getEncoderFrameSize = module.cwrap('get_encoder_frame_size', 'number', ['number']);
+ getEncoderExtradata = module.cwrap('get_encoder_extradata', 'number', ['number']);
+ getEncoderExtradataSize = module.cwrap('get_encoder_extradata_size', 'number', ['number']);
+ getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
+ sendFrameFn = module.cwrap('send_frame', 'number', ['number', 'number']);
+ receivePacketFn = module.cwrap('receive_packet', 'number', ['number']);
+ flushEncoderStartFn = module.cwrap('flush_encoder_start', null, ['number']);
+ resetEncoderFn = module.cwrap('reset_encoder', null, ['number']);
+ getEncodedData = module.cwrap('get_encoded_data', 'number', ['number']);
+ getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']);
+ getEncodedDuration = module.cwrap('get_encoded_duration', 'number', ['number']);
+ closeEncoderFn = module.cwrap('close_encoder', null, ['number']);
+ }
+};
+
+const initEncoder = async (
+ numberOfChannels: number,
+ sampleRate: number,
+ bitrate: number,
+) => {
+ await ensureModule();
+
+ const ctx = initEncoderFn(numberOfChannels, sampleRate, bitrate);
+ if (ctx === 0) {
+ throw new Error('Failed to initialize AAC encoder.');
+ }
+
+ const frameSize = getEncoderFrameSize(ctx);
+
+ const extradataPtr = getEncoderExtradata(ctx);
+ const extradataSize = getEncoderExtradataSize(ctx);
+ const extradata = module.HEAPU8.slice(extradataPtr, extradataPtr + extradataSize).buffer;
+
+ return { ctx, frameSize, extradata };
+};
+
+type PacketInfo = { encodedData: ArrayBuffer; pts: number; duration: number };
+
+const drainPackets = (ctx: number) => {
+ const packets: PacketInfo[] = [];
+
+ let size: number;
+ while ((size = receivePacketFn(ctx)) > 0) {
+ const ptr = getEncodedData(ctx);
+ const encodedData = module.HEAPU8.slice(ptr, ptr + size).buffer;
+ const pts = getEncodedPts(ctx);
+ const duration = getEncodedDuration(ctx);
+ packets.push({ encodedData, pts, duration });
+ }
+
+ return packets;
+};
+
+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 ret = sendFrameFn(ctx, timestamp);
+ if (ret < 0) {
+ throw new Error(`Encode failed with error code ${ret}.`);
+ }
+
+ return drainPackets(ctx);
+};
+
+const onMessage = (data: { id: number; command: WorkerCommand }) => {
+ const { id, command } = data;
+
+ const handleCommand = async (): Promise => {
+ try {
+ let result: WorkerResponseData;
+ const transferables: Transferable[] = [];
+
+ switch (command.type) {
+ case 'init': {
+ const { ctx, frameSize, extradata } = await initEncoder(
+ command.data.numberOfChannels,
+ command.data.sampleRate,
+ command.data.bitrate,
+ );
+ result = { type: command.type, ctx, frameSize, extradata };
+ transferables.push(extradata);
+ }; break;
+
+ case 'encode': {
+ const packets = encode(
+ command.data.ctx,
+ command.data.audioData,
+ command.data.timestamp,
+ );
+ for (const p of packets) {
+ transferables.push(p.encodedData);
+ }
+ result = { type: command.type, packets };
+ }; break;
+
+ case 'flush': {
+ flushEncoderStartFn(command.data.ctx);
+ const packets = drainPackets(command.data.ctx);
+ for (const p of packets) {
+ transferables.push(p.encodedData);
+ }
+ resetEncoderFn(command.data.ctx);
+ result = { type: command.type, packets };
+ }; break;
+
+ case 'close': {
+ 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 }));
+}
diff --git a/packages/aac-encoder/src/encoder.ts b/packages/aac-encoder/src/encoder.ts
new file mode 100644
index 0000000..6f2d681
--- /dev/null
+++ b/packages/aac-encoder/src/encoder.ts
@@ -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 {
+ type AdtsHeaderTemplate,
+ buildAdtsHeaderTemplate,
+ parseAacAudioSpecificConfig,
+ writeAdtsFrameLength,
+} from '../../../shared/aac-misc';
+import {
+ CustomAudioEncoder,
+ AudioCodec,
+ AudioSample,
+ EncodedPacket,
+ registerEncoder,
+} from 'mediabunny';
+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 AAC_SAMPLE_RATES = [
+ 96000, 88200, 64000, 48000, 44100, 32000,
+ 24000, 22050, 16000, 12000, 11025, 8000, 7350,
+];
+
+class AacEncoder extends CustomAudioEncoder {
+ private worker: Worker | null = null;
+ private nextMessageId = 0;
+ private pendingMessages = new Map void;
+ reject: (reason?: unknown) => void;
+ }>();
+
+ private ctx = 0;
+ private encoderFrameSize = 0;
+ private sampleRate = 0;
+ private numberOfChannels = 0;
+ private chunkMetadata: EncodedAudioChunkMetadata = {};
+ private useAdts = false;
+ private adtsHeaderTemplate: AdtsHeaderTemplate | null = null;
+ private description: Uint8Array | null = null;
+
+ // 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 {
+ return codec === 'aac'
+ && config.numberOfChannels >= 1
+ && config.numberOfChannels <= 8
+ && AAC_SAMPLE_RATES.includes(config.sampleRate)
+ && config.bitrate !== undefined;
+ }
+
+ async init() {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ this.worker = (await createWorker()) as Worker;
+
+ const onMessage = (data: WorkerResponse) => {
+ const pending = this.pendingMessages.get(data.id);
+ assert(pending !== undefined);
+
+ this.pendingMessages.delete(data.id);
+ if (data.success) {
+ pending.resolve(data.data);
+ } else {
+ pending.reject(data.error);
+ }
+ };
+
+ if (this.worker.addEventListener) {
+ this.worker.addEventListener('message', event => onMessage(event.data as WorkerResponse));
+ } else {
+ const nodeWorker = this.worker as unknown as {
+ on: (event: string, listener: (data: never) => void) => void;
+ };
+ nodeWorker.on('message', onMessage);
+ }
+
+ assert(this.config.bitrate !== undefined);
+ this.sampleRate = this.config.sampleRate;
+ this.numberOfChannels = this.config.numberOfChannels;
+
+ const result = await this.sendCommand({
+ type: 'init',
+ data: {
+ numberOfChannels: this.config.numberOfChannels,
+ sampleRate: this.config.sampleRate,
+ bitrate: this.config.bitrate,
+ },
+ });
+
+ this.ctx = result.ctx;
+ this.encoderFrameSize = result.frameSize;
+
+ // The ffmpeg encoder provides an AudioSpecificConfig as extradata after init
+ const description = new Uint8Array(result.extradata);
+
+ const aacConfig = (this.config as { aac?: { format?: 'aac' | 'adts' } }).aac;
+ this.useAdts = aacConfig?.format === 'adts';
+
+ if (this.useAdts) {
+ const audioSpecificConfig = parseAacAudioSpecificConfig(description);
+ this.adtsHeaderTemplate = buildAdtsHeaderTemplate(audioSpecificConfig);
+ }
+
+ this.description = this.useAdts ? null : description;
+ this.resetInternalState();
+ }
+
+ private resetInternalState() {
+ this.pendingFrames = 0;
+ this.nextSampleTimestampInSamples = null;
+ this.nextPacketTimestampInSamples = null;
+
+ this.chunkMetadata = {
+ decoderConfig: {
+ codec: 'mp4a.40.2',
+ numberOfChannels: this.config.numberOfChannels,
+ sampleRate: this.config.sampleRate,
+ ...(this.description ? { description: this.description } : {}),
+ },
+ };
+ }
+
+ 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();
+ }
+
+ const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } });
+ this.emitPackets(result.packets);
+
+ this.resetInternalState();
+ }
+
+ close() {
+ void this.sendCommand({ type: 'close', data: { ctx: this.ctx } });
+ this.worker?.terminate();
+ }
+
+ 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 this.sendCommand({
+ type: 'encode',
+ data: {
+ ctx: this.ctx,
+ audioData,
+ timestamp: this.nextSampleTimestampInSamples,
+ },
+ }, [audioData]);
+
+ this.nextSampleTimestampInSamples += frameSize;
+
+ this.emitPackets(result.packets);
+ }
+
+ private emitPackets(packets: Array<{ encodedData: ArrayBuffer; pts: number; duration: number }>) {
+ assert(this.nextPacketTimestampInSamples !== null);
+
+ for (const p of packets) {
+ let data = new Uint8Array(p.encodedData);
+
+ if (this.useAdts) {
+ assert(this.adtsHeaderTemplate !== null);
+ const { header, bitstream } = this.adtsHeaderTemplate;
+ const frameLength = header.byteLength + data.byteLength;
+ writeAdtsFrameLength(bitstream, frameLength);
+
+ const adtsFrame = new Uint8Array(frameLength);
+ adtsFrame.set(header, 0);
+ adtsFrame.set(data, header.byteLength);
+ data = adtsFrame;
+ }
+
+ const packet = new EncodedPacket(
+ data,
+ 'key',
+ this.nextPacketTimestampInSamples / this.sampleRate,
+ p.duration / this.sampleRate,
+ );
+
+ this.nextPacketTimestampInSamples += p.duration;
+
+ this.onPacket(
+ packet,
+ this.chunkMetadata,
+ );
+
+ this.chunkMetadata = {};
+ }
+ }
+
+ private sendCommand(
+ command: WorkerCommand & { type: T },
+ transferables?: Transferable[],
+ ) {
+ return new Promise((resolve, reject) => {
+ const id = this.nextMessageId++;
+ this.pendingMessages.set(id, {
+ resolve: resolve as (value: WorkerResponseData) => void,
+ reject,
+ });
+
+ assert(this.worker);
+
+ if (transferables) {
+ this.worker.postMessage({ id, command }, transferables);
+ } else {
+ this.worker.postMessage({ id, command });
+ }
+ });
+ }
+}
+
+/**
+ * Registers the AAC encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
+ * function before starting any encoding task.
+ *
+ * Preferably, wrap the call in a condition to avoid overriding any native AAC encoder:
+ *
+ * ```ts
+ * import { canEncodeAudio } from 'mediabunny';
+ * import { registerAacEncoder } from '@mediabunny/aac-encoder';
+ *
+ * if (!(await canEncodeAudio('aac'))) {
+ * registerAacEncoder();
+ * }
+ * ```
+ *
+ * @group \@mediabunny/aac-encoder
+ * @public
+ */
+export const registerAacEncoder = () => {
+ registerEncoder(AacEncoder);
+};
+
+function assert(x: unknown): asserts x {
+ if (!x) {
+ throw new Error('Assertion failed.');
+ }
+}
diff --git a/packages/aac-encoder/src/index.ts b/packages/aac-encoder/src/index.ts
new file mode 100644
index 0000000..d0bed47
--- /dev/null
+++ b/packages/aac-encoder/src/index.ts
@@ -0,0 +1,20 @@
+/*!
+ * 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 AAC_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/aac-encoder loaded');
+if ((globalThis as Record)[AAC_ENCODER_LOADED_SYMBOL]) {
+ console.error(
+ '[WARNING]\n@mediabunny/aac-encoder was loaded twice.'
+ + ' This will likely cause the encoder not to work correctly.'
+ + ' Check if multiple dependencies are importing different versions of @mediabunny/aac-encoder,'
+ + ' or if something is being bundled incorrectly.',
+ );
+}
+(globalThis as Record)[AAC_ENCODER_LOADED_SYMBOL] = true;
+
+export { registerAacEncoder } from './encoder';
diff --git a/packages/aac-encoder/src/shared.ts b/packages/aac-encoder/src/shared.ts
new file mode 100644
index 0000000..ce5513d
--- /dev/null
+++ b/packages/aac-encoder/src/shared.ts
@@ -0,0 +1,66 @@
+/*!
+ * 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';
+ data: {
+ numberOfChannels: number;
+ sampleRate: number;
+ bitrate: number;
+ };
+} | {
+ type: 'encode';
+ data: {
+ ctx: number;
+ audioData: ArrayBuffer;
+ timestamp: number;
+ };
+} | {
+ type: 'flush';
+ data: {
+ ctx: number;
+ };
+} | {
+ type: 'close';
+ data: {
+ ctx: number;
+ };
+};
+
+export type WorkerResponseData = {
+ type: 'init';
+ ctx: number;
+ frameSize: number;
+ extradata: ArrayBuffer;
+} | {
+ type: 'encode';
+ packets: Array<{
+ encodedData: ArrayBuffer;
+ pts: number;
+ duration: number;
+ }>;
+} | {
+ type: 'flush';
+ packets: Array<{
+ encodedData: ArrayBuffer;
+ pts: number;
+ duration: number;
+ }>;
+} | {
+ type: 'close';
+};
+
+export type WorkerResponse = {
+ id: number;
+} & ({
+ success: true;
+ data: WorkerResponseData;
+} | {
+ success: false;
+ error: unknown;
+});
diff --git a/packages/aac-encoder/tsconfig.json b/packages/aac-encoder/tsconfig.json
new file mode 100644
index 0000000..27a7ef1
--- /dev/null
+++ b/packages/aac-encoder/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "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/**/*",
+ ],
+ "references": [
+ { "path": "../../src" }
+ ]
+}
diff --git a/packages/aac-encoder/tsdoc.json b/packages/aac-encoder/tsdoc.json
new file mode 100644
index 0000000..51d29d3
--- /dev/null
+++ b/packages/aac-encoder/tsdoc.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
+ "extends": ["../../tsdoc.json"]
+}
diff --git a/packages/mp3-encoder/tsconfig.json b/packages/mp3-encoder/tsconfig.json
index bf2e94f..27a7ef1 100644
--- a/packages/mp3-encoder/tsconfig.json
+++ b/packages/mp3-encoder/tsconfig.json
@@ -17,7 +17,6 @@
"include": [
"./src/**/*",
"./build/**/*",
- "../../shared/**/*",
],
"references": [
{ "path": "../../src" }
diff --git a/scripts/build.sh b/scripts/build.sh
index c0595fe..e448138 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -7,6 +7,7 @@ set -e
rm -rf dist
rm -rf packages/mp3-encoder/dist
rm -rf packages/ac3/dist
+rm -rf packages/aac-encoder/dist
# Ensure license headers on all source files
tsx scripts/ensure-license-headers.ts
@@ -15,6 +16,7 @@ tsx scripts/ensure-license-headers.ts
tsc -p src
tsc -p packages/mp3-encoder
tsc -p packages/ac3
+tsc -p packages/aac-encoder
# 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)
@@ -27,11 +29,13 @@ tsx scripts/bundle.ts
api-extractor run
api-extractor run -c packages/mp3-encoder/api-extractor.json
api-extractor run -c packages/ac3/api-extractor.json
+api-extractor run -c packages/aac-encoder/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
+tsx scripts/check-docblocks.ts packages/aac-encoder/dist/mediabunny-aac-encoder.d.ts
# Checks that API docs are generatable
npm run docs:generate -- --dry
@@ -39,4 +43,5 @@ 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 MediabunnyAc3;' >> packages/ac3/dist/mediabunny-ac3.d.ts
\ No newline at end of file
+echo 'export as namespace MediabunnyAc3;' >> packages/ac3/dist/mediabunny-ac3.d.ts
+echo 'export as namespace MediabunnyAacEncoder;' >> packages/aac-encoder/dist/mediabunny-aac-encoder.d.ts
\ No newline at end of file
diff --git a/scripts/bundle.ts b/scripts/bundle.ts
index c1a23d2..b94106f 100644
--- a/scripts/bundle.ts
+++ b/scripts/bundle.ts
@@ -144,10 +144,42 @@ const ac3Variants = await createVariants(
},
);
+const aacEncoderVariants = await createVariants(
+ 'packages/aac-encoder/src/index.ts',
+ 'MediabunnyAacEncoder',
+ 'packages/aac-encoder/dist/bundles/mediabunny-aac-encoder',
+ '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,
+ ...aacEncoderVariants,
];
if (process.argv[2] === '--watch') {
diff --git a/scripts/check.sh b/scripts/check.sh
index 41aaef3..6b1c4db 100755
--- a/scripts/check.sh
+++ b/scripts/check.sh
@@ -4,11 +4,15 @@ set -e
rm -rf dist/modules
tsc -p src --stripInternal false
-tsc -p packages/mp3-encoder --noEmit
+rm -rf packages/mp3-encoder/dist/modules
+tsc -p packages/mp3-encoder
rm -rf packages/ac3/dist/modules
tsc -p packages/ac3
+rm -rf packages/aac-encoder/dist/modules
+tsc -p packages/aac-encoder
+
tsc -p tsconfig.vitest.json --noEmit
tsc -p scripts --noEmit
diff --git a/shared/aac-misc.ts b/shared/aac-misc.ts
new file mode 100644
index 0000000..5c0fc71
--- /dev/null
+++ b/shared/aac-misc.ts
@@ -0,0 +1,146 @@
+/*!
+ * 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 { Bitstream } from './bitstream';
+
+export type AacAudioSpecificConfig = {
+ objectType: number;
+ frequencyIndex: number;
+ sampleRate: number | null;
+ channelConfiguration: number;
+ numberOfChannels: number | null;
+};
+
+export const aacFrequencyTable = [
+ 96000, 88200, 64000, 48000, 44100, 32000,
+ 24000, 22050, 16000, 12000, 11025, 8000, 7350,
+];
+
+export const aacChannelMap = [-1, 1, 2, 3, 4, 5, 6, 8];
+
+export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioSpecificConfig => {
+ if (!bytes || bytes.byteLength < 2) {
+ throw new TypeError('AAC description must be at least 2 bytes long.');
+ }
+
+ const bitstream = new Bitstream(bytes);
+
+ let objectType = bitstream.readBits(5);
+ if (objectType === 31) {
+ objectType = 32 + bitstream.readBits(6);
+ }
+
+ const frequencyIndex = bitstream.readBits(4);
+ let sampleRate: number | null = null;
+ if (frequencyIndex === 15) {
+ sampleRate = bitstream.readBits(24);
+ } else {
+ if (frequencyIndex < aacFrequencyTable.length) {
+ sampleRate = aacFrequencyTable[frequencyIndex]!;
+ }
+ }
+
+ const channelConfiguration = bitstream.readBits(4);
+ let numberOfChannels: number | null = null;
+ if (channelConfiguration >= 1 && channelConfiguration <= 7) {
+ numberOfChannels = aacChannelMap[channelConfiguration]!;
+ }
+
+ return {
+ objectType,
+ frequencyIndex,
+ sampleRate,
+ channelConfiguration,
+ numberOfChannels,
+ };
+};
+
+export const buildAacAudioSpecificConfig = (config: {
+ objectType: number;
+ sampleRate: number;
+ numberOfChannels: number;
+}) => {
+ let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate);
+ let customSampleRate: number | null = null;
+
+ if (frequencyIndex === -1) {
+ frequencyIndex = 15;
+ customSampleRate = config.sampleRate;
+ }
+
+ const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels);
+ if (channelConfiguration === -1) {
+ throw new TypeError(`Unsupported number of channels: ${config.numberOfChannels}`);
+ }
+
+ let bitCount = 5 + 4 + 4;
+ if (config.objectType >= 32) {
+ bitCount += 6;
+ }
+ if (frequencyIndex === 15) {
+ bitCount += 24;
+ }
+
+ const byteCount = Math.ceil(bitCount / 8);
+ const bytes = new Uint8Array(byteCount);
+ const bitstream = new Bitstream(bytes);
+
+ if (config.objectType < 32) {
+ bitstream.writeBits(5, config.objectType);
+ } else {
+ bitstream.writeBits(5, 31);
+ bitstream.writeBits(6, config.objectType - 32);
+ }
+
+ bitstream.writeBits(4, frequencyIndex);
+
+ if (frequencyIndex === 15) {
+ bitstream.writeBits(24, customSampleRate!);
+ }
+
+ bitstream.writeBits(4, channelConfiguration);
+
+ return bytes;
+};
+
+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);
+};
diff --git a/shared/bitstream.ts b/shared/bitstream.ts
new file mode 100644
index 0000000..f2d2590
--- /dev/null
+++ b/shared/bitstream.ts
@@ -0,0 +1,85 @@
+/*!
+ * 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 class Bitstream {
+ /** Current offset in bits. */
+ pos = 0;
+
+ constructor(public bytes: Uint8Array) {}
+
+ seekToByte(byteOffset: number) {
+ this.pos = 8 * byteOffset;
+ }
+
+ private readBit() {
+ const byteIndex = Math.floor(this.pos / 8);
+ const byte = this.bytes[byteIndex] ?? 0;
+ const bitIndex = 0b111 - (this.pos & 0b111);
+ const bit = (byte & (1 << bitIndex)) >> bitIndex;
+
+ this.pos++;
+ return bit;
+ }
+
+ readBits(n: number) {
+ if (n === 1) {
+ return this.readBit();
+ }
+
+ let result = 0;
+
+ for (let i = 0; i < n; i++) {
+ result <<= 1;
+ result |= this.readBit();
+ }
+
+ return result;
+ }
+
+ writeBits(n: number, value: number) {
+ const end = this.pos + n;
+
+ for (let i = this.pos; i < end; i++) {
+ const byteIndex = Math.floor(i / 8);
+ let byte = this.bytes[byteIndex]!;
+ const bitIndex = 0b111 - (i & 0b111);
+
+ byte &= ~(1 << bitIndex);
+ byte |= ((value & (1 << (end - i - 1))) >> (end - i - 1)) << bitIndex;
+ this.bytes[byteIndex] = byte;
+ }
+
+ this.pos = end;
+ };
+
+ readAlignedByte() {
+ if (this.pos % 8 !== 0) {
+ throw new Error('Bitstream is not byte-aligned.');
+ }
+
+ const byteIndex = this.pos / 8;
+ const byte = this.bytes[byteIndex] ?? 0;
+
+ this.pos += 8;
+ return byte;
+ }
+
+ skipBits(n: number) {
+ this.pos += n;
+ }
+
+ getBitsLeft() {
+ return this.bytes.length * 8 - this.pos;
+ }
+
+ clone() {
+ const clone = new Bitstream(this.bytes);
+ clone.pos = this.pos;
+ return clone;
+ }
+}
diff --git a/src/adts/adts-demuxer.ts b/src/adts/adts-demuxer.ts
index 18af576..0539e86 100644
--- a/src/adts/adts-demuxer.ts
+++ b/src/adts/adts-demuxer.ts
@@ -6,7 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { aacChannelMap, aacFrequencyTable, AudioCodec } from '../codec';
+import { aacChannelMap, aacFrequencyTable } from '../../shared/aac-misc';
+import { AudioCodec } from '../codec';
import { Demuxer } from '../demuxer';
import {
ID3_V2_HEADER_SIZE,
diff --git a/src/adts/adts-misc.ts b/src/adts/adts-misc.ts
deleted file mode 100644
index c736703..0000000
--- a/src/adts/adts-misc.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/*!
- * 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);
-};
diff --git a/src/adts/adts-muxer.ts b/src/adts/adts-muxer.ts
index f004ae3..bc0c55b 100644
--- a/src/adts/adts-muxer.ts
+++ b/src/adts/adts-muxer.ts
@@ -6,16 +6,17 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { parseAacAudioSpecificConfig, validateAudioChunkMetadata } from '../codec';
+import { buildAdtsHeaderTemplate, parseAacAudioSpecificConfig, writeAdtsFrameLength } from '../../shared/aac-misc';
+import { Bitstream } from '../../shared/bitstream';
+import { validateAudioChunkMetadata } from '../codec';
import { Id3V2Writer } from '../id3';
import { metadataTagsAreEmpty } from '../metadata';
-import { assert, Bitstream, toUint8Array } from '../misc';
+import { assert, 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;
diff --git a/src/adts/adts-reader.ts b/src/adts/adts-reader.ts
index c6803e9..672e727 100644
--- a/src/adts/adts-reader.ts
+++ b/src/adts/adts-reader.ts
@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { Bitstream } from '../misc';
+import { Bitstream } from '../../shared/bitstream';
import { FileSlice, readBytes } from '../reader';
export const MIN_ADTS_FRAME_HEADER_SIZE = 7;
diff --git a/src/codec-data.ts b/src/codec-data.ts
index 0cb5135..235c080 100644
--- a/src/codec-data.ts
+++ b/src/codec-data.ts
@@ -11,7 +11,6 @@ import {
assert,
assertNever,
base64ToBytes,
- Bitstream,
bytesToBase64,
keyValueIterator,
getUint24,
@@ -30,6 +29,7 @@ import {
import { PacketType } from './packet';
import { MetadataTags } from './metadata';
import { AC3_SAMPLE_RATES, EAC3_REDUCED_SAMPLE_RATES } from '../shared/ac3-misc';
+import { Bitstream } from '../shared/bitstream';
// References for AVC/HEVC code:
// ISO 14496-15
diff --git a/src/codec.ts b/src/codec.ts
index b7086f6..4c7d1ff 100644
--- a/src/codec.ts
+++ b/src/codec.ts
@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
+import { parseAacAudioSpecificConfig } from '../shared/aac-misc';
import {
Av1CodecInfo,
AvcDecoderConfigurationRecord,
@@ -13,7 +14,6 @@ import {
Vp9CodecInfo,
} from './codec-data';
import {
- Bitstream,
COLOR_PRIMARIES_MAP,
MATRIX_COEFFICIENTS_MAP,
TRANSFER_CHARACTERISTICS_MAP,
@@ -590,106 +590,6 @@ export const extractAudioCodecString = (trackInfo: {
throw new TypeError(`Unhandled codec '${codec}'.`);
};
-export type AacAudioSpecificConfig = {
- objectType: number;
- frequencyIndex: number;
- sampleRate: number | null;
- channelConfiguration: number;
- numberOfChannels: number | null;
-};
-
-export const aacFrequencyTable = [
- 96000, 88200, 64000, 48000, 44100, 32000,
- 24000, 22050, 16000, 12000, 11025, 8000, 7350,
-];
-
-export const aacChannelMap = [-1, 1, 2, 3, 4, 5, 6, 8];
-
-export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioSpecificConfig => {
- if (!bytes || bytes.byteLength < 2) {
- throw new TypeError('AAC description must be at least 2 bytes long.');
- }
-
- const bitstream = new Bitstream(bytes);
-
- let objectType = bitstream.readBits(5);
- if (objectType === 31) {
- objectType = 32 + bitstream.readBits(6);
- }
-
- const frequencyIndex = bitstream.readBits(4);
- let sampleRate: number | null = null;
- if (frequencyIndex === 15) {
- sampleRate = bitstream.readBits(24);
- } else {
- if (frequencyIndex < aacFrequencyTable.length) {
- sampleRate = aacFrequencyTable[frequencyIndex]!;
- }
- }
-
- const channelConfiguration = bitstream.readBits(4);
- let numberOfChannels: number | null = null;
- if (channelConfiguration >= 1 && channelConfiguration <= 7) {
- numberOfChannels = aacChannelMap[channelConfiguration]!;
- }
-
- return {
- objectType,
- frequencyIndex,
- sampleRate,
- channelConfiguration,
- numberOfChannels,
- };
-};
-
-export const buildAacAudioSpecificConfig = (config: {
- objectType: number;
- sampleRate: number;
- numberOfChannels: number;
-}) => {
- let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate);
- let customSampleRate: number | null = null;
-
- if (frequencyIndex === -1) {
- frequencyIndex = 15;
- customSampleRate = config.sampleRate;
- }
-
- const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels);
- if (channelConfiguration === -1) {
- throw new TypeError(`Unsupported number of channels: ${config.numberOfChannels}`);
- }
-
- let bitCount = 5 + 4 + 4;
- if (config.objectType >= 32) {
- bitCount += 6;
- }
- if (frequencyIndex === 15) {
- bitCount += 24;
- }
-
- const byteCount = Math.ceil(bitCount / 8);
- const bytes = new Uint8Array(byteCount);
- const bitstream = new Bitstream(bytes);
-
- if (config.objectType < 32) {
- bitstream.writeBits(5, config.objectType);
- } else {
- bitstream.writeBits(5, 31);
- bitstream.writeBits(6, config.objectType - 32);
- }
-
- bitstream.writeBits(4, frequencyIndex);
-
- if (frequencyIndex === 15) {
- bitstream.writeBits(24, customSampleRate!);
- }
-
- bitstream.writeBits(4, channelConfiguration);
-
- return bytes;
-};
-
export const OPUS_SAMPLE_RATE = 48_000;
const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;
diff --git a/src/conversion.ts b/src/conversion.ts
index 14bc516..cda03c8 100644
--- a/src/conversion.ts
+++ b/src/conversion.ts
@@ -774,6 +774,12 @@ export class Conversion {
);
}
+ if (codecs.includes('aac')) {
+ elements.push(
+ '\nThe @mediabunny/aac-encoder extension package provides support for encoding AAC.',
+ );
+ }
+
if (codecs.includes('ac3') || codecs.includes('eac3')) {
elements.push(
'\nThe @mediabunny/ac3 extension package provides support'
diff --git a/src/flac/flac-demuxer.ts b/src/flac/flac-demuxer.ts
index 4547c4b..adffadf 100644
--- a/src/flac/flac-demuxer.ts
+++ b/src/flac/flac-demuxer.ts
@@ -15,7 +15,6 @@ import {
assert,
AsyncMutex,
binarySearchLessOrEqual,
- Bitstream,
textDecoder,
UNDETERMINED_LANGUAGE,
} from '../misc';
@@ -37,6 +36,7 @@ import {
readSampleRate,
getSampleRateOrUncommon,
} from './flac-misc';
+import { Bitstream } from '../../shared/bitstream';
type FlacAudioInfo = {
numberOfChannels: number;
diff --git a/src/flac/flac-misc.ts b/src/flac/flac-misc.ts
index 1cc78b2..f440d3d 100644
--- a/src/flac/flac-misc.ts
+++ b/src/flac/flac-misc.ts
@@ -6,7 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { assert, assertNever, Bitstream } from '../misc';
+import { Bitstream } from '../../shared/bitstream';
+import { assert, assertNever } from '../misc';
import { FileSlice, readBytes, readU16Be, readU8 } from '../reader';
type BlockSizeOrUncommon = number | 'uncommon-u16' | 'uncommon-u8';
diff --git a/src/flac/flac-muxer.ts b/src/flac/flac-muxer.ts
index 01bbd7c..dad7a61 100644
--- a/src/flac/flac-muxer.ts
+++ b/src/flac/flac-muxer.ts
@@ -10,7 +10,6 @@ import { validateAudioChunkMetadata } from '../codec';
import { createVorbisComments, FlacBlockType } from '../codec-data';
import {
assert,
- Bitstream,
textEncoder,
toDataView,
toUint8Array,
@@ -27,6 +26,7 @@ import {
getBlockSizeOrUncommon,
readCodedNumber,
} from './flac-misc';
+import { Bitstream } from '../../shared/bitstream';
const FLAC_HEADER = /* #__PURE__ */ new Uint8Array([0x66, 0x4c, 0x61, 0x43]); // 'fLaC'
const STREAMINFO_SIZE = 38;
diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts
index 040f00b..7115b1c 100644
--- a/src/isobmff/isobmff-boxes.ts
+++ b/src/isobmff/isobmff-boxes.ts
@@ -20,7 +20,6 @@ import {
UNDETERMINED_LANGUAGE,
assertNever,
keyValueIterator,
- Bitstream,
} from '../misc';
import {
AudioCodec,
@@ -46,6 +45,7 @@ import {
} from './isobmff-muxer';
import { parseAc3SyncFrame, parseEac3SyncFrame, parseOpusIdentificationHeader } from '../codec-data';
import { MetadataTags, RichImageData } from '../metadata';
+import { Bitstream } from '../../shared/bitstream';
export class IsobmffBoxWriter {
private helper = new Uint8Array(8);
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index df68ff9..e247193 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
+import { parseAacAudioSpecificConfig } from '../../shared/aac-misc';
import {
AacCodecInfo,
AudioCodec,
@@ -13,7 +14,6 @@ import {
extractVideoCodecString,
MediaCodec,
OPUS_SAMPLE_RATE,
- parseAacAudioSpecificConfig,
parsePcmCodec,
PCM_AUDIO_CODECS,
PcmAudioCodec,
@@ -47,7 +47,6 @@ import {
assert,
binarySearchExact,
binarySearchLessOrEqual,
- Bitstream,
COLOR_PRIMARIES_MAP_INVERSE,
findLastIndex,
isIso639Dash2LanguageCode,
@@ -92,6 +91,7 @@ import {
} from '../reader';
import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDisposition } from '../metadata';
import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc';
+import { Bitstream } from '../../shared/bitstream';
type InternalTrack = {
id: number;
diff --git a/src/isobmff/isobmff-muxer.ts b/src/isobmff/isobmff-muxer.ts
index 7c56b7c..4256f53 100644
--- a/src/isobmff/isobmff-muxer.ts
+++ b/src/isobmff/isobmff-muxer.ts
@@ -13,10 +13,8 @@ import { BufferTargetWriter, Writer } from '../writer';
import { assert, computeRationalApproximation, last, promiseWithResolvers, Rational, simplifyRational } from '../misc';
import { IsobmffOutputFormatOptions, IsobmffOutputFormat, MovOutputFormat } from '../output-format';
import { inlineTimestampRegex, SubtitleConfig, SubtitleCue, SubtitleMetadata } from '../subtitles';
+import { aacChannelMap, aacFrequencyTable, buildAacAudioSpecificConfig } from '../../shared/aac-misc';
import {
- aacChannelMap,
- aacFrequencyTable,
- buildAacAudioSpecificConfig,
parsePcmCodec,
PCM_AUDIO_CODECS,
PcmAudioCodec,
diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts
index 6554f20..fb1e6b1 100644
--- a/src/matroska/matroska-muxer.ts
+++ b/src/matroska/matroska-muxer.ts
@@ -6,8 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
+import { Bitstream } from '../../shared/bitstream';
import {
- Bitstream,
COLOR_PRIMARIES_MAP,
MATRIX_COEFFICIENTS_MAP,
TRANSFER_CHARACTERISTICS_MAP,
@@ -49,10 +49,8 @@ import {
inlineTimestampRegex,
parseSubtitleTimestamp,
} from '../subtitles';
+import { aacChannelMap, aacFrequencyTable, buildAacAudioSpecificConfig } from '../../shared/aac-misc';
import {
- aacChannelMap,
- aacFrequencyTable,
- buildAacAudioSpecificConfig,
OPUS_SAMPLE_RATE,
PCM_AUDIO_CODECS,
PcmAudioCodec,
diff --git a/src/media-source.ts b/src/media-source.ts
index faf6243..3580a14 100644
--- a/src/media-source.ts
+++ b/src/media-source.ts
@@ -6,11 +6,10 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
+import { buildAacAudioSpecificConfig, parseAacAudioSpecificConfig } from '../shared/aac-misc';
import {
AUDIO_CODECS,
AudioCodec,
- buildAacAudioSpecificConfig,
- parseAacAudioSpecificConfig,
parsePcmCodec,
PCM_AUDIO_CODECS,
PcmAudioCodec,
diff --git a/src/misc.ts b/src/misc.ts
index 72b33df..b963ae7 100644
--- a/src/misc.ts
+++ b/src/misc.ts
@@ -6,6 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
+import { Bitstream } from '../shared/bitstream';
+
export function assert(x: unknown): asserts x {
if (!x) {
throw new Error('Assertion failed.');
@@ -39,85 +41,6 @@ export const isU32 = (value: number) => {
return value >= 0 && value < 2 ** 32;
};
-export class Bitstream {
- /** Current offset in bits. */
- pos = 0;
-
- constructor(public bytes: Uint8Array) {}
-
- seekToByte(byteOffset: number) {
- this.pos = 8 * byteOffset;
- }
-
- private readBit() {
- const byteIndex = Math.floor(this.pos / 8);
- const byte = this.bytes[byteIndex] ?? 0;
- const bitIndex = 0b111 - (this.pos & 0b111);
- const bit = (byte & (1 << bitIndex)) >> bitIndex;
-
- this.pos++;
- return bit;
- }
-
- readBits(n: number) {
- if (n === 1) {
- return this.readBit();
- }
-
- let result = 0;
-
- for (let i = 0; i < n; i++) {
- result <<= 1;
- result |= this.readBit();
- }
-
- return result;
- }
-
- writeBits(n: number, value: number) {
- const end = this.pos + n;
-
- for (let i = this.pos; i < end; i++) {
- const byteIndex = Math.floor(i / 8);
- let byte = this.bytes[byteIndex]!;
- const bitIndex = 0b111 - (i & 0b111);
-
- byte &= ~(1 << bitIndex);
- byte |= ((value & (1 << (end - i - 1))) >> (end - i - 1)) << bitIndex;
- this.bytes[byteIndex] = byte;
- }
-
- this.pos = end;
- };
-
- readAlignedByte() {
- // Ensure we're byte-aligned
- if (this.pos % 8 !== 0) {
- throw new Error('Bitstream is not byte-aligned.');
- }
-
- const byteIndex = this.pos / 8;
- const byte = this.bytes[byteIndex] ?? 0;
-
- this.pos += 8;
- return byte;
- }
-
- skipBits(n: number) {
- this.pos += n;
- }
-
- getBitsLeft() {
- return this.bytes.length * 8 - this.pos;
- }
-
- clone() {
- const clone = new Bitstream(this.bytes);
- clone.pos = this.pos;
- return clone;
- }
-}
-
/** Reads an exponential-Golomb universal code from a Bitstream. */
export const readExpGolomb = (bitstream: Bitstream) => {
let leadingZeroBits = 0;
diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts
index 562ee45..253e8de 100644
--- a/src/mpeg-ts/mpeg-ts-demuxer.ts
+++ b/src/mpeg-ts/mpeg-ts-demuxer.ts
@@ -8,10 +8,9 @@
import { SAMPLES_PER_AAC_FRAME } from '../adts/adts-demuxer';
import { MAX_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from '../adts/adts-reader';
+import { aacChannelMap, aacFrequencyTable } from '../../shared/aac-misc';
import {
- aacChannelMap,
AacCodecInfo,
- aacFrequencyTable,
AudioCodec,
extractAudioCodecString,
extractVideoCodecString,
@@ -54,7 +53,6 @@ import {
assert,
binarySearchExact,
binarySearchLessOrEqual,
- Bitstream,
COLOR_PRIMARIES_MAP_INVERSE,
findLastIndex,
floorToMultiple,
@@ -71,6 +69,7 @@ import { EncodedPacket, PacketType, PLACEHOLDER_DATA } from '../packet';
import { FileSlice, readBytes, Reader, readU16Be, readU32Be, readU8 } from '../reader';
import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc';
import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc';
+import { Bitstream } from '../../shared/bitstream';
// Resources:
// ISO/IEC 13818-1
diff --git a/src/mpeg-ts/mpeg-ts-muxer.ts b/src/mpeg-ts/mpeg-ts-muxer.ts
index e4977cc..102843a 100644
--- a/src/mpeg-ts/mpeg-ts-muxer.ts
+++ b/src/mpeg-ts/mpeg-ts-muxer.ts
@@ -6,8 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { parseAacAudioSpecificConfig, validateAudioChunkMetadata, validateVideoChunkMetadata } from '../codec';
-import { buildAdtsHeaderTemplate, writeAdtsFrameLength } from '../adts/adts-misc';
+import { buildAdtsHeaderTemplate, parseAacAudioSpecificConfig, writeAdtsFrameLength } from '../../shared/aac-misc';
+import { validateAudioChunkMetadata, validateVideoChunkMetadata } from '../codec';
import {
AC3_REGISTRATION_DESCRIPTOR,
AvcDecoderConfigurationRecord,
@@ -23,7 +23,8 @@ import {
iterateNalUnitsInAnnexB,
iterateNalUnitsInLengthPrefixed,
} from '../codec-data';
-import { assert, Bitstream, promiseWithResolvers, setUint24, toDataView, toUint8Array } from '../misc';
+import { Bitstream } from '../../shared/bitstream';
+import { assert, promiseWithResolvers, setUint24, toDataView, toUint8Array } from '../misc';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from '../output';
import { MpegTsOutputFormat } from '../output-format';
diff --git a/test/node/aac-encoder-extension.test.ts b/test/node/aac-encoder-extension.test.ts
new file mode 100644
index 0000000..9a1bb0c
--- /dev/null
+++ b/test/node/aac-encoder-extension.test.ts
@@ -0,0 +1,82 @@
+import { expect, test } from 'vitest';
+import { Input } from '../../src/input.js';
+import { BufferSource } from '../../src/source.js';
+import { ALL_FORMATS } from '../../src/input-format.js';
+import { Output } from '../../src/output.js';
+import { BufferTarget } from '../../src/target.js';
+import { canEncode } from '../../src/encode.js';
+import { AudioSampleSource } from '../../src/media-source.js';
+import { EncodedPacketSink } from '../../src/media-sink.js';
+import { Mp4OutputFormat } from '../../src/output-format.js';
+import { AudioSample } from '../../src/sample.js';
+import { registerAacEncoder } from '@mediabunny/aac-encoder';
+
+const createSineWave = (sampleRate: number, channels: number, durationSeconds: number) => {
+ const totalFrames = sampleRate * durationSeconds;
+ const data = new Float32Array(totalFrames * channels);
+
+ for (let i = 0; i < totalFrames; i++) {
+ const value = Math.sin(2 * Math.PI * 440 * i / sampleRate);
+ for (let ch = 0; ch < channels; ch++) {
+ data[i * channels + ch] = value;
+ }
+ }
+
+ return data;
+};
+
+test('Custom coder registration', async () => {
+ expect(await canEncode('aac')).toBe(false);
+
+ registerAacEncoder();
+
+ expect(await canEncode('aac')).toBe(true);
+});
+
+test('AAC encoding', async () => {
+ registerAacEncoder();
+
+ const sampleRate = 48000;
+ const channels = 2;
+ const durationSeconds = 2;
+ const data = createSineWave(sampleRate, channels, durationSeconds);
+
+ const output = new Output({
+ format: new Mp4OutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ const audioSource = new AudioSampleSource({ codec: 'aac', bitrate: 128000 });
+ output.addAudioTrack(audioSource);
+
+ await output.start();
+ await audioSource.add(new AudioSample({
+ data,
+ format: 'f32',
+ numberOfChannels: channels,
+ sampleRate,
+ timestamp: 0,
+ }));
+ audioSource.close();
+ await output.finalize();
+
+ using input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const track = (await input.getPrimaryAudioTrack())!;
+ expect(track.codec).toBe('aac');
+ expect(track.sampleRate).toBe(sampleRate);
+ expect(track.numberOfChannels).toBe(channels);
+
+ const sink = new EncodedPacketSink(track);
+ let packetCount = 0;
+ for await (const packet of sink.packets()) {
+ expect(packet.type).toBe('key');
+ packetCount++;
+ }
+ expect(packetCount).toBeGreaterThan(durationSeconds * sampleRate / 1024);
+
+ expect(await track.computeDuration()).toBeCloseTo(2, 1);
+});
diff --git a/tsconfig.vite.json b/tsconfig.vite.json
index 18c8e14..07743b0 100644
--- a/tsconfig.vite.json
+++ b/tsconfig.vite.json
@@ -8,6 +8,7 @@
"paths": {
"mediabunny": ["./src/index.ts"],
"@mediabunny/ac3": ["./packages/ac3/src/index.ts"],
+ "@mediabunny/aac-encoder": ["./packages/aac-encoder/src/index.ts"],
},
"types": ["vite/client"]
},
diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json
index 7101888..e4ac060 100644
--- a/tsconfig.vitest.json
+++ b/tsconfig.vitest.json
@@ -8,9 +8,10 @@
"paths": {
"mediabunny": ["./src/index.ts"],
"@mediabunny/ac3": ["./packages/ac3/src/index.ts"],
+ "@mediabunny/aac-encoder": ["./packages/aac-encoder/src/index.ts"],
},
},
"include": ["vitest.config.ts", "./test/**/*"],
"exclude": ["./test/public/**/*"],
- "references": [{ "path": "./src" }, { "path": "./packages/ac3" }]
+ "references": [{ "path": "./src" }, { "path": "./packages/ac3" }, { "path": "./packages/aac-encoder" }]
}
diff --git a/vite.config.ts b/vite.config.ts
index 6b460b0..c376f06 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -20,7 +20,9 @@ const rollupInput = Object.fromEntries(
export default defineConfig({
resolve: {
alias: {
- mediabunny: path.resolve(__dirname, './dist/bundles/mediabunny.mjs'),
+ 'mediabunny': path.resolve(__dirname, './dist/bundles/mediabunny.mjs'),
+ '@mediabunny/aac-encoder':
+ path.resolve(__dirname, './packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.mjs'),
},
},
plugins: [
diff --git a/vitest.config.ts b/vitest.config.ts
index 3623ed8..d73adc9 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -8,6 +8,8 @@ export default defineConfig({
alias: {
'mediabunny': path.resolve(__dirname, './src/index.ts'),
'@mediabunny/ac3': path.resolve(__dirname, './packages/ac3/dist/bundles/mediabunny-ac3.mjs'),
+ '@mediabunny/aac-encoder':
+ path.resolve(__dirname, './packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.mjs'),
},
},
test: {