diff --git a/dev/convert.html b/dev/convert.html
index 4830820..7dd4124 100644
--- a/dev/convert.html
+++ b/dev/convert.html
@@ -24,7 +24,9 @@
chunked: true,
chunkSize: 2**20
});
- const outputFormat = new Mediabunny.Mp3OutputFormat({});
+ const outputFormat = new Mediabunny.AdtsOutputFormat({
+ onFrame: console.log
+ });
const button = document.createElement('button');
button.textContent = 'Cancel';
diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md
index 512aac4..4a63c35 100644
--- a/docs/guide/introduction.md
+++ b/docs/guide/introduction.md
@@ -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), including variations such as MP4 with Fast Start, fragmented MP4, or streamable Matroska
+- Read & write support for many container formats (.mp4, .mov, .webm, .mkv, .mp3, .wav, .ogg, .aac), including variations such as MP4 with Fast Start, fragmented MP4, or streamable Matroska
- Support for 25 different codecs
- Lazy, optimized, on-demand file reading
- Input and output streaming, arbitrary file size support
diff --git a/docs/guide/output-formats.md b/docs/guide/output-formats.md
index 4317ee6..479287c 100644
--- a/docs/guide/output-formats.md
+++ b/docs/guide/output-formats.md
@@ -244,4 +244,25 @@ type WavOutputFormatOptions = {
- `large`\
When enabled, an RF64 file be written, allowing for file sizes to exceed 4 GiB, which is otherwise not possible for regular WAVE files.
- `onHeader`\
- Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the start of the data chunk (with a placeholder size of 0).
\ No newline at end of file
+ Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the start of the data chunk (with a placeholder size of 0).
+
+## ADTS
+
+This output format creates ADTS (.aac) files.
+```ts
+import { Output, AdtsOutputFormat } from 'mediabunny';
+
+const output = new Output({
+ format: new AdtsOutputFormat(options),
+ // ...
+});
+```
+
+The following options are available:
+```ts
+type AdtsOutputFormatOptions = {
+ onFrame?: (data: Uint8Array, position: number) => unknown;
+};
+```
+- `onFrame`\
+ Will be called for each ADTS frame that is written.
\ No newline at end of file
diff --git a/docs/guide/supported-formats-and-codecs.md b/docs/guide/supported-formats-and-codecs.md
index f5275a4..a945c5d 100644
--- a/docs/guide/supported-formats-and-codecs.md
+++ b/docs/guide/supported-formats-and-codecs.md
@@ -11,6 +11,7 @@ Mediabunny supports many commonly used media container formats, all of which are
- Ogg (.ogg)
- MP3 (.mp3)
- WAVE (.wav)
+- ADTS (.aac)
## Codecs
@@ -60,33 +61,33 @@ 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 |
-|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|
-| `'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[^1] | .ogg | .mp3 | .wav | .aac |
+|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|:-----:|
+| `'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] | (✓) | | (✓) | (✓) | | | | |
[^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.
diff --git a/src/adts/adts-muxer.ts b/src/adts/adts-muxer.ts
new file mode 100644
index 0000000..3fc50d3
--- /dev/null
+++ b/src/adts/adts-muxer.ts
@@ -0,0 +1,125 @@
+/*!
+ * Copyright (c) 2025-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 { 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';
+
+export class AdtsMuxer extends Muxer {
+ private format: AdtsOutputFormat;
+ private writer: Writer;
+ private header = new Uint8Array(7);
+ private audioSpecificConfig: AacAudioSpecificConfig | null = null;
+
+ constructor(output: Output, format: AdtsOutputFormat) {
+ super(output);
+
+ this.format = format;
+ this.writer = output._writer;
+ }
+
+ async start() {
+ // Nothing needed here
+ }
+
+ async getMimeType() {
+ return 'audio/aac';
+ }
+
+ async addEncodedVideoPacket() {
+ throw new Error('ADTS does not support video.');
+ }
+
+ async addEncodedAudioPacket(
+ track: OutputAudioTrack,
+ packet: EncodedPacket,
+ meta?: EncodedAudioChunkMetadata,
+ ) {
+ // https://wiki.multimedia.cx/index.php/ADTS (last visited: 2025/08/17)
+
+ const release = await this.mutex.acquire();
+
+ try {
+ if (!this.audioSpecificConfig) {
+ validateAudioChunkMetadata(meta);
+
+ const description = meta?.decoderConfig?.description;
+ assert(description);
+
+ this.audioSpecificConfig = parseAacAudioSpecificConfig(toUint8Array(description));
+ }
+
+ const syncword = 0b1111_11111111;
+ this.header[0] = (syncword >> 4);
+
+ const mpegVersion = 0;
+ const layer = 0;
+ const protectionAbsence = 1;
+ this.header[1] = (syncword << 4)
+ | (mpegVersion << 3)
+ | (layer << 1)
+ | protectionAbsence;
+
+ const privateBit = 0;
+ this.header[2] = (((this.audioSpecificConfig.objectType - 1) & 0b11) << 6)
+ | ((this.audioSpecificConfig.frequencyIndex & 0b1111) << 2)
+ | (privateBit << 1)
+ | ((this.audioSpecificConfig.channelConfiguration & 0b111) >> 2);
+
+ const originality = 0;
+ const homeUsage = 0;
+ const copyrightIdBit = 0;
+ const copyrightIdStart = 0;
+ const frameLength = (packet.data.byteLength + this.header.byteLength) & 0b11111_11111111;
+ this.header[3] = ((this.audioSpecificConfig.channelConfiguration & 0b111) << 6)
+ | (originality << 5)
+ | (homeUsage << 4)
+ | (copyrightIdBit << 3)
+ | (copyrightIdStart << 2)
+ | (frameLength >> 11);
+
+ this.header[4] = (frameLength >> 3);
+
+ const bufferFullness = 0x7ff; // Variable bitrate
+ this.header[5] = (frameLength << 5)
+ | (bufferFullness >> 6);
+
+ const numberOfAacFrames = 1;
+ this.header[6] = (bufferFullness << 2)
+ | (numberOfAacFrames - 1);
+
+ // Omit CRC check
+
+ 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();
+ } finally {
+ release();
+ }
+ }
+
+ async addSubtitleCue() {
+ throw new Error('ADTS does not support subtitles.');
+ }
+
+ async finalize() {}
+}
diff --git a/src/codec.ts b/src/codec.ts
index 4a0eb65..e144cf6 100644
--- a/src/codec.ts
+++ b/src/codec.ts
@@ -561,7 +561,15 @@ export const extractAudioCodecString = (trackInfo: {
throw new TypeError(`Unhandled codec '${codec}'.`);
};
-export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null) => {
+export type AacAudioSpecificConfig = {
+ objectType: number;
+ frequencyIndex: number;
+ sampleRate: number | null;
+ channelConfiguration: number;
+ numberOfChannels: number | null;
+};
+
+export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioSpecificConfig => {
if (!bytes || bytes.byteLength < 2) {
throw new TypeError('AAC description must be at least 2 bytes long.');
}
diff --git a/src/index.ts b/src/index.ts
index 5c28b03..fbceb47 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -35,6 +35,8 @@ export {
WavOutputFormatOptions,
OggOutputFormat,
OggOutputFormatOptions,
+ AdtsOutputFormat,
+ AdtsOutputFormatOptions,
TrackCountLimits,
InclusiveIntegerRange,
} from './output-format';
diff --git a/src/output-format.ts b/src/output-format.ts
index 5faebcb..0f5e228 100644
--- a/src/output-format.ts
+++ b/src/output-format.ts
@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
+import { AdtsMuxer } from './adts/adts-muxer';
import {
AUDIO_CODECS,
AudioCodec,
@@ -705,3 +706,74 @@ export class OggOutputFormat extends OutputFormat {
return false;
}
}
+
+/**
+ * ADTS-specific output options.
+ * @public
+ */
+export type AdtsOutputFormatOptions = {
+ /**
+ * Will be called for each ADTS frame that is written.
+ *
+ * @param data - The raw bytes.
+ * @param position - The byte offset of the data in the file.
+ */
+ onFrame?: (data: Uint8Array, position: number) => unknown;
+};
+
+/**
+ * ADTS file format.
+ * @public
+ */
+export class AdtsOutputFormat extends OutputFormat {
+ /** @internal */
+ _options: AdtsOutputFormatOptions;
+
+ constructor(options: AdtsOutputFormatOptions = {}) {
+ if (!options || typeof options !== 'object') {
+ throw new TypeError('options must be an object.');
+ }
+ if (options.onFrame !== undefined && typeof options.onFrame !== 'function') {
+ throw new TypeError('options.onFrame, when provided, must be a function.');
+ }
+
+ super();
+
+ this._options = options;
+ }
+
+ /** @internal */
+ _createMuxer(output: Output) {
+ return new AdtsMuxer(output, this);
+ }
+
+ /** @internal */
+ get _name() {
+ return 'ADTS';
+ }
+
+ getSupportedTrackCounts(): TrackCountLimits {
+ return {
+ video: { min: 0, max: 0 },
+ audio: { min: 1, max: 1 },
+ subtitle: { min: 0, max: 0 },
+ total: { min: 1, max: 1 },
+ };
+ }
+
+ get fileExtension() {
+ return '.aac';
+ }
+
+ get mimeType() {
+ return 'audio/aac';
+ }
+
+ getSupportedCodecs(): MediaCodec[] {
+ return ['aac'];
+ }
+
+ get supportsVideoRotationMetadata() {
+ return false;
+ }
+}