mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 13:23:51 +02:00
Implement ADTS muxer :3
This commit is contained in:
+3
-1
@@ -24,7 +24,9 @@
|
|||||||
chunked: true,
|
chunked: true,
|
||||||
chunkSize: 2**20
|
chunkSize: 2**20
|
||||||
});
|
});
|
||||||
const outputFormat = new Mediabunny.Mp3OutputFormat({});
|
const outputFormat = new Mediabunny.AdtsOutputFormat({
|
||||||
|
onFrame: console.log
|
||||||
|
});
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.textContent = 'Cancel';
|
button.textContent = 'Cancel';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Here's a long list of stuff this library does:
|
|||||||
- Converting media files
|
- Converting media files
|
||||||
- Hardware-accelerated decoding & encoding (via the WebCodecs API)
|
- Hardware-accelerated decoding & encoding (via the WebCodecs API)
|
||||||
- Support for multiple video, audio and subtitle tracks
|
- 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
|
- Support for 25 different codecs
|
||||||
- Lazy, optimized, on-demand file reading
|
- Lazy, optimized, on-demand file reading
|
||||||
- Input and output streaming, arbitrary file size support
|
- Input and output streaming, arbitrary file size support
|
||||||
|
|||||||
@@ -244,4 +244,25 @@ type WavOutputFormatOptions = {
|
|||||||
- `large`\
|
- `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.
|
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`\
|
- `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).
|
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.
|
||||||
@@ -11,6 +11,7 @@ Mediabunny supports many commonly used media container formats, all of which are
|
|||||||
- Ogg (.ogg)
|
- Ogg (.ogg)
|
||||||
- MP3 (.mp3)
|
- MP3 (.mp3)
|
||||||
- WAVE (.wav)
|
- WAVE (.wav)
|
||||||
|
- ADTS (.aac)
|
||||||
|
|
||||||
## Codecs
|
## 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:
|
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 |
|
| | .mp4 | .mov | .mkv | .webm[^1] | .ogg | .mp3 | .wav | .aac |
|
||||||
|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|
|
|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|:-----:|
|
||||||
| `'avc'` | ✓ | ✓ | ✓ | | | | |
|
| `'avc'` | ✓ | ✓ | ✓ | | | | | |
|
||||||
| `'hevc'` | ✓ | ✓ | ✓ | | | | |
|
| `'hevc'` | ✓ | ✓ | ✓ | | | | | |
|
||||||
| `'vp8'` | ✓ | ✓ | ✓ | ✓ | | | |
|
| `'vp8'` | ✓ | ✓ | ✓ | ✓ | | | | |
|
||||||
| `'vp9'` | ✓ | ✓ | ✓ | ✓ | | | |
|
| `'vp9'` | ✓ | ✓ | ✓ | ✓ | | | | |
|
||||||
| `'av1'` | ✓ | ✓ | ✓ | ✓ | | | |
|
| `'av1'` | ✓ | ✓ | ✓ | ✓ | | | | |
|
||||||
| `'aac'` | ✓ | ✓ | ✓ | | | | |
|
| `'aac'` | ✓ | ✓ | ✓ | | | | | ✓ |
|
||||||
| `'opus'` | ✓ | ✓ | ✓ | ✓ | ✓ | | |
|
| `'opus'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | |
|
||||||
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | |
|
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | | |
|
||||||
| `'vorbis'` | ✓ | ✓ | ✓ | ✓ | ✓ | | |
|
| `'vorbis'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | |
|
||||||
| `'flac'` | ✓ | ✓ | ✓ | | | | |
|
| `'flac'` | ✓ | ✓ | ✓ | | | | | |
|
||||||
| `'pcm-u8'` | | ✓ | ✓ | | | | ✓ |
|
| `'pcm-u8'` | | ✓ | ✓ | | | | ✓ | |
|
||||||
| `'pcm-s8'` | | ✓ | | | | | |
|
| `'pcm-s8'` | | ✓ | | | | | | |
|
||||||
| `'pcm-s16'` | ✓ | ✓ | ✓ | | | | ✓ |
|
| `'pcm-s16'` | ✓ | ✓ | ✓ | | | | ✓ | |
|
||||||
| `'pcm-s16be'` | ✓ | ✓ | ✓ | | | | |
|
| `'pcm-s16be'` | ✓ | ✓ | ✓ | | | | | |
|
||||||
| `'pcm-s24'` | ✓ | ✓ | ✓ | | | | ✓ |
|
| `'pcm-s24'` | ✓ | ✓ | ✓ | | | | ✓ | |
|
||||||
| `'pcm-s24be'` | ✓ | ✓ | ✓ | | | | |
|
| `'pcm-s24be'` | ✓ | ✓ | ✓ | | | | | |
|
||||||
| `'pcm-s32'` | ✓ | ✓ | ✓ | | | | ✓ |
|
| `'pcm-s32'` | ✓ | ✓ | ✓ | | | | ✓ | |
|
||||||
| `'pcm-s32be'` | ✓ | ✓ | ✓ | | | | |
|
| `'pcm-s32be'` | ✓ | ✓ | ✓ | | | | | |
|
||||||
| `'pcm-f32'` | ✓ | ✓ | ✓ | | | | ✓ |
|
| `'pcm-f32'` | ✓ | ✓ | ✓ | | | | ✓ | |
|
||||||
| `'pcm-f32be'` | ✓ | ✓ | | | | | |
|
| `'pcm-f32be'` | ✓ | ✓ | | | | | | |
|
||||||
| `'pcm-f64'` | ✓ | ✓ | ✓ | | | | |
|
| `'pcm-f64'` | ✓ | ✓ | ✓ | | | | | |
|
||||||
| `'pcm-f64be'` | ✓ | ✓ | | | | | |
|
| `'pcm-f64be'` | ✓ | ✓ | | | | | | |
|
||||||
| `'ulaw'` | | ✓ | | | | | ✓ |
|
| `'ulaw'` | | ✓ | | | | | ✓ | |
|
||||||
| `'alaw'` | | ✓ | | | | | ✓ |
|
| `'alaw'` | | ✓ | | | | | ✓ | |
|
||||||
| `'webvtt'`[^2] | (✓) | | (✓) | (✓) | | | |
|
| `'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.
|
[^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.
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
}
|
||||||
+9
-1
@@ -561,7 +561,15 @@ export const extractAudioCodecString = (trackInfo: {
|
|||||||
throw new TypeError(`Unhandled codec '${codec}'.`);
|
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) {
|
if (!bytes || bytes.byteLength < 2) {
|
||||||
throw new TypeError('AAC description must be at least 2 bytes long.');
|
throw new TypeError('AAC description must be at least 2 bytes long.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export {
|
|||||||
WavOutputFormatOptions,
|
WavOutputFormatOptions,
|
||||||
OggOutputFormat,
|
OggOutputFormat,
|
||||||
OggOutputFormatOptions,
|
OggOutputFormatOptions,
|
||||||
|
AdtsOutputFormat,
|
||||||
|
AdtsOutputFormatOptions,
|
||||||
TrackCountLimits,
|
TrackCountLimits,
|
||||||
InclusiveIntegerRange,
|
InclusiveIntegerRange,
|
||||||
} from './output-format';
|
} from './output-format';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { AdtsMuxer } from './adts/adts-muxer';
|
||||||
import {
|
import {
|
||||||
AUDIO_CODECS,
|
AUDIO_CODECS,
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
@@ -705,3 +706,74 @@ export class OggOutputFormat extends OutputFormat {
|
|||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user