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; + } +}