Compare commits

..
10 Commits
Author SHA1 Message Date
Vanilagy b717cbf68c Bump minor 2026-02-12 12:06:25 +01:00
Vanilagy 7cc26edacb Add Mediabunny Codec Registry 2026-02-12 11:56:37 +01:00
Vanilagy de9a4bbc23 Update codec soup 2026-02-11 16:03:57 +01:00
Vanilagy 4fae45ec40 Allow negative trim ranges for conversion, add OutputFormat.supportsTimestampedMediaData 2026-02-11 15:56:51 +01:00
417c85d8ed feat(codec): add detection support for AC-3, E-AC-3 audio codecs (#275)
* ac3/eac3 detection

* fix tests

* review pass + follow codebase comments style

* Add ac3/eac3 to docs

* WIP mpeg-2

* Set registration_descriptor

* add support for ac3 system b in mpeg-ts

* Update docs with ac3 in .ts support

* fix formatting

* Remove default to fix linter

* Add eac3 to mpeg-ts

* fix formatting

* Fix failing test

* Refactor AC3/EAC3 code a little

---------

Co-authored-by: Vanilagy <[email protected]>
2026-02-11 15:23:12 +01:00
Vanilagy 612642c5ad Remove sponsor 2026-02-11 08:38:49 +01:00
Vanilagy 2298f4a8b3 Add jepcd sponsor 2026-02-10 17:07:57 +01:00
David P.andGitHub a613a870c8 Merge pull request #296 from yonathan06/patch-1
Update gling.svg
2026-02-10 16:24:26 +01:00
YonatanandGitHub b61b2b326f Update gling.svg 2026-02-10 16:45:44 +02:00
Vanilagy cacbc2a399 Fix assertion failure 2026-02-07 10:35:40 +01:00
56 changed files with 1859 additions and 102 deletions
+7 -4
View File
@@ -24,7 +24,7 @@
chunked: true,
chunkSize: 2**20
});
const outputFormat = new Mediabunny.Mp4OutputFormat({});
const outputFormat = new Mediabunny.WavOutputFormat({});
const button = document.createElement('button');
button.textContent = 'Cancel';
@@ -100,8 +100,9 @@
},
*/
video: () => ({
forceTranscode: true,
allowRotationMetadata: false,
width: 320,
//forceTranscode: true,
//allowRotationMetadata: false,
//width: 720,
//frameRate: 30,
//bitrate: Mediabunny.QUALITY_VERY_LOW,
@@ -181,7 +182,9 @@
}
},
trim: {
start: 0,
start: -2,
end: 5,
//start: 0,
//end: 4
},
});
+40
View File
@@ -42,6 +42,12 @@ export default withMermaid({
{ text: 'Examples', link: '/examples', activeMatch: '/examples' },
{ text: 'Sponsors', link: '/#sponsors', activeMatch: '/#sponsors' },
{ text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license' },
{
text: 'More',
items: [
{ text: 'Codec Registry', link: '/codec-registry/overview' },
],
},
],
sidebar: {
@@ -93,6 +99,40 @@ export default withMermaid({
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
'/api': apiRoutes as any,
'/codec-registry': [
{
text: 'Codec registry',
items: [
{ text: 'Overview', link: '/codec-registry/overview' },
],
},
{
text: 'Video',
items: [
{ text: 'AVC (H.264)', link: '/codec-registry/avc' },
{ text: 'HEVC (H.265)', link: '/codec-registry/hevc' },
{ text: 'VP8', link: '/codec-registry/vp8' },
{ text: 'VP9', link: '/codec-registry/vp9' },
{ text: 'AV1', link: '/codec-registry/av1' },
],
},
{
text: 'Audio',
items: [
{ text: 'AAC', link: '/codec-registry/aac' },
{ text: 'Opus', link: '/codec-registry/opus' },
{ text: 'MP3', link: '/codec-registry/mp3' },
{ text: 'Vorbis', link: '/codec-registry/vorbis' },
{ text: 'FLAC', link: '/codec-registry/flac' },
{ text: 'AC-3', link: '/codec-registry/ac3' },
{ text: 'E-AC-3', link: '/codec-registry/eac3' },
{ text: 'Linear PCM', link: '/codec-registry/pcm' },
{ text: 'μ-law PCM', link: '/codec-registry/ulaw' },
{ text: 'A-law PCM', link: '/codec-registry/alaw' },
],
},
],
},
socialLinks: [
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 141 KiB

+50
View File
@@ -0,0 +1,50 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# AAC codec registration
## Description
The Advanced Audio Coding (AAC) audio codec, specified in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html).
An AAC bitstream can have either of two formats:
- _AAC_ (raw), where packets contain raw AAC frames (syntax element `raw_data_block()`) as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 4.4.2.1. Here, codec metadata is provided out-of-band.
- _ADTS_, where packets contain ADTS frames as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 1.A.3.2. Here, codec metadata is provided in-band in each ADTS frame header.
All packets within the bitstream must have the same format.
## Codec ID
```ts
'aac'
```
## `EncodedPacket` data
If the bitstream is in the _AAC_ (raw) format, the packet's data must be a raw AAC frame (syntax element `raw_data_block()`) as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 4.4.2.1.
If the bitstream is in the _ADTS_ format, the packet's data must be an ADTS frame as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 1.A.3.2.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
The following fully qualified codec strings are recognized:
- `'mp4a.40.2'` — MPEG-4 AAC-LC
- `'mp4a.40.02'` — MPEG-4 AAC-LC (leading zero for Aud-OTI compatibility)
- `'mp4a.40.5'` — MPEG-4 HE-AAC v1 (AAC-LC + SBR)
- `'mp4a.40.05'` — MPEG-4 HE-AAC v1 (leading zero for Aud-OTI compatibility)
- `'mp4a.40.29'` — MPEG-4 HE-AAC v2 (AAC-LC + SBR + PS)
- `'mp4a.67'` — MPEG-2 AAC-LC
## `AudioDecoderConfig` description
If the bitstream is in the _AAC_ (raw) format, `description` must be an `AudioSpecificConfig` as defined in [ISO/IEC 14496-3](https://www.iso.org/standard/76383.html) Section 1.6.2.1.
If the bitstream is in the _ADTS_ format, `description` must be undefined.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# AC-3 codec registration
## Description
The Dolby Digital (AC-3) audio codec, specified in [ETSI TS 102 366](https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf).
## Codec ID
```ts
'ac3'
```
## `EncodedPacket` data
The packet's data must be a sync frame (syntactic element `syncframe()`) as defined in Section 4.3 of [ETSI TS 102 366](https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf), beginning with the sync word `0x0B77`.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'ac-3'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# A-law PCM codec registration
## Description
The A-law companded PCM audio codec, specified in [ITU-T G.711](https://www.itu.int/rec/T-REC-G.711) Tables 1a and 1b.
## Codec ID
```ts
'alaw'
```
## `EncodedPacket` data
The packet's data must be a sequence of bytes of arbitrary length (divisible by the channel count), where each byte is an A-law encoded PCM sample. For multichannel audio, samples from different channels are interleaved.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'alaw'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+33
View File
@@ -0,0 +1,33 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# AV1 codec registration
## Description
The AOMedia Video 1 (AV1) video codec, specified in the [AV1 Bitstream & Decoding Process Specification](https://aomediacodec.github.io/av1-spec/).
## Codec ID
```ts
'av1'
```
## `EncodedPacket` data
The packet's data must comply with the low-overhead bitstream format as defined in Section 5 of the [AV1 Bitstream & Decoding Process Specification](https://aomediacodec.github.io/av1-spec/).
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain a frame with `frame_type` of `KEY_FRAME`, as defined in Section 6.8.2 of the [AV1 Bitstream & Decoding Process Specification](https://aomediacodec.github.io/av1-spec/).
## `VideoDecoderConfig` codec string
The full codec string begins with the prefix `'av01.'`, with a variable-length suffix as specified in Section 5 of the [AV1 Codec ISO Media File Format Binding](https://aomediacodec.github.io/av1-isobmff/).
## `VideoDecoderConfig` description
`description` is not used for this codec.
+41
View File
@@ -0,0 +1,41 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# AVC (H.264) codec registration
## Description
The Advanced Video Coding (H.264) video codec, specified in [Rec. ITU-T H.264](https://www.itu.int/rec/T-REC-H.264) / [ISO/IEC 14496-10](https://www.iso.org/standard/87574.html).
An AVC bitstream can have either of two formats:
- _Canonical_ (length-prefixed), as defined in [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html) Section 5.3.2. Here, video parameter sets (SPS/PPS) are provided out-of-band.
- _Annex B_, as defined in [Rec. ITU-T H.264](https://www.itu.int/rec/T-REC-H.264) Annex B. Here, video parameter sets (SPS/PPS) must be provided in-band in the respective NALUs.
All packets within the bitstream must have the same format.
## Codec ID
```ts
'avc'
```
## `EncodedPacket` data
The packet's data must be an access unit as defined in [Rec. ITU-T H.264](https://www.itu.int/rec/T-REC-H.264) Section 7.4.1.2, in either _canonical_ or _Annex B_ format.
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain a primary coded picture from which decoding can begin. Additionally, if the bitstream's format is _Annex B_, then this packet is also expected to contain the necessary video parameter sets to initialize the decoder.
## `VideoDecoderConfig` codec string
The full codec string begins with the prefix `'avc1.'` or `'avc3.'`, with a suffix of 6 characters as described respectively in Section 3.4 of [RFC 6381](https://www.rfc-editor.org/rfc/rfc6381) and Section 5.4.1 of [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html).
## `VideoDecoderConfig` description
If the bitstream is in the _canonical_ (length-prefixed) format, `description` must be an `AVCDecoderConfigurationRecord` as defined in [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html) Section 5.3.3.1.
If the bitstream is in the _Annex B_ format, `description` must be undefined.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# E-AC-3 codec registration
## Description
The Dolby Digital Plus (E-AC-3) audio codec, specified in [ETSI TS 102 366](https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf).
## Codec ID
```ts
'eac3'
```
## `EncodedPacket` data
The packet's data must be a sync frame as defined in Section E.1.2.0 of [ETSI TS 102 366](https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf), beginning with the sync word `0x0B77`.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'ec-3'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# FLAC codec registration
## Description
The Free Lossless Audio Codec (FLAC), specified in the [FLAC Format Specification](https://xiph.org/flac/format.html).
## Codec ID
```ts
'flac'
```
## `EncodedPacket` data
The packet's data must be a FLAC frame as described in the [FLAC Format Specification](https://xiph.org/flac/format.html).
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'flac'
```
## `AudioDecoderConfig` description
`description` must contain the bytes `0x66 0x4C 0x61 0x43` (the ASCII string `'fLaC'`), followed by a `STREAMINFO` metadata block as defined in Section 7 of the [FLAC Format Specification](https://xiph.org/flac/format.html).
+41
View File
@@ -0,0 +1,41 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# HEVC (H.265) codec registration
## Description
The High Efficiency Video Coding (H.265) video codec, specified in [Rec. ITU-T H.265](https://www.itu.int/rec/T-REC-H.265) / [ISO/IEC 23008-2](https://www.iso.org/standard/75484.html).
An HEVC bitstream can have either of two formats:
- _Canonical_ (length-prefixed), as defined in [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html) Section 8.3.2. Here, video parameter sets (VPS/SPS/PPS) are provided out-of-band.
- _Annex B_, as defined in [Rec. ITU-T H.265](https://www.itu.int/rec/T-REC-H.265) Annex B. Here, video parameter sets (VPS/SPS/PPS) must be provided in-band in the respective NALUs.
All packets within the bitstream must have the same format.
## Codec ID
```ts
'hevc'
```
## `EncodedPacket` data
The packet's data must be an access unit as defined in [Rec. ITU-T H.265](https://www.itu.int/rec/T-REC-H.265) Section 7.4.2.4, containing exactly one base layer coded picture, in either _canonical_ or _Annex B_ format.
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain an IDR, CRA, or BLA picture. Additionally, if the bitstream's format is _Annex B_, then this packet is also expected to contain the necessary video parameter sets to initialize the decoder.
## `VideoDecoderConfig` codec string
The full codec string begins with the prefix `'hev1.'` or `'hvc1.'`, with a variable-length suffix of four dot-separated fields as specified in Section E.3 of [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html).
## `VideoDecoderConfig` description
If the bitstream is in the _canonical_ (length-prefixed) format, `description` must be an `HEVCDecoderConfigurationRecord` as defined in [ISO/IEC 14496-15](https://www.iso.org/standard/89118.html) Section 8.3.3.1.
If the bitstream is in the _Annex B_ format, `description` must be undefined.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# MP3 codec registration
## Description
The MP3 audio codec (MPEG-1/2 Audio Layer III), specified in [ISO/IEC 13818-3](https://www.iso.org/standard/26797.html).
## Codec ID
```ts
'mp3'
```
## `EncodedPacket` data
The packet's data must be an MP3 frame as described in Section 2.4.2.2 of [ISO/IEC 13818-3](https://www.iso.org/standard/26797.html).
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'mp3'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# Opus codec registration
## Description
The Opus audio codec, specified in [RFC 6716](https://www.rfc-editor.org/rfc/rfc6716).
## Codec ID
```ts
'opus'
```
## `EncodedPacket` data
The packet's data must be an Opus packet as described in Section 3 of [RFC 6716](https://www.rfc-editor.org/rfc/rfc6716).
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'opus'
```
## `AudioDecoderConfig` description
If present, `description` must be an Identification Header as defined in Section 5.1 of [RFC 7845](https://www.rfc-editor.org/rfc/rfc7845).
+26
View File
@@ -0,0 +1,26 @@
# Mediabunny Codec Registry
The Mediabunny Codec Registry formalizes the precise definitions of all video and audio codecs supported by Mediabunny. More specifically, for any given codec, it describes the format that `EncodedPacket`, `VideoDecoderConfig` and `AudioDecoderConfig` must adhere to. All packets coming out of or going into Mediabunny are expected to adhere to this registry.
The registry is an extension of the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). Mediabunny's registry matches that of WebCodecs for all codecs supported by both.
## Video codecs
- [AVC (H.264)](./avc)
- [HEVC (H.265)](./hevc)
- [VP8](./vp8)
- [VP9](./vp9)
- [AV1](./av1)
## Audio codecs
- [AAC](./aac)
- [Opus](./opus)
- [MP3](./mp3)
- [Vorbis](./vorbis)
- [FLAC](./flac)
- [AC-3](./ac3)
- [E-AC-3](./eac3)
- [Linear PCM](./pcm)
- [μ-law PCM](./ulaw)
- [A-law PCM](./alaw)
+44
View File
@@ -0,0 +1,44 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# Linear PCM codecs registration
## Description
A family of linear pulse-code modulation (PCM) audio codecs of various bit depths and byte orders.
## Codec IDs
| Codec ID | Description |
| --- | --- |
| `'pcm-u8'` | Unsigned 8-bit integer |
| `'pcm-s8'` | Signed 8-bit integer |
| `'pcm-s16'` | Signed 16-bit integer, little-endian |
| `'pcm-s16be'` | Signed 16-bit integer, big-endian |
| `'pcm-s24'` | Signed 24-bit integer, little-endian |
| `'pcm-s24be'` | Signed 24-bit integer, big-endian |
| `'pcm-s32'` | Signed 32-bit integer, little-endian |
| `'pcm-s32be'` | Signed 32-bit integer, big-endian |
| `'pcm-f32'` | 32-bit float, little-endian |
| `'pcm-f32be'` | 32-bit float, big-endian |
| `'pcm-f64'` | 64-bit float, little-endian |
| `'pcm-f64be'` | 64-bit float, big-endian |
## `EncodedPacket` data
The packet's data must be a sequence of bytes of arbitrary length (divisible by the size of one frame), with each sample occupying the number of bits defined by the codec ID. For multichannel audio, samples from different channels are interleaved.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
The codec string is the same as the codec ID (e.g. `'pcm-s16'`, `'pcm-f32'`).
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# μ-law PCM codec registration
## Description
The μ-law companded PCM audio codec, specified in [ITU-T G.711](https://www.itu.int/rec/T-REC-G.711) Tables 2a and 2b.
## Codec ID
```ts
'ulaw'
```
## `EncodedPacket` data
The packet's data must be a sequence of bytes of arbitrary length (divisible by the channel count), where each byte is a μ-law encoded PCM sample. For multichannel audio, samples from different channels are interleaved.
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'ulaw'
```
## `AudioDecoderConfig` description
`description` is not used for this codec.
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Audio codec" />
# Vorbis codec registration
## Description
The Vorbis audio codec, specified in the [Vorbis I Specification](https://xiph.org/vorbis/doc/Vorbis_I_spec.html).
## Codec ID
```ts
'vorbis'
```
## `EncodedPacket` data
The packet's data must be an audio packet as described in Section 4.3 of the [Vorbis I Specification](https://xiph.org/vorbis/doc/Vorbis_I_spec.html).
## `EncodedPacket` type
The packet's type is always `'key'`.
## `AudioDecoderConfig` codec string
```ts
'vorbis'
```
## `AudioDecoderConfig` description
`description` must contain Vorbis codec setup data in Xiph extradata format: the `page_segments` field, followed by the `segment_table` field, followed by the three Vorbis header packets (identification header, comments header, and setup header) as defined in Section 4.2 of the [Vorbis I Specification](https://xiph.org/vorbis/doc/Vorbis_I_spec.html).
+35
View File
@@ -0,0 +1,35 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# VP8 codec registration
## Description
The VP8 video codec, specified in [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386).
## Codec ID
```ts
'vp8'
```
## `EncodedPacket` data
The packet's data must be a frame as described in Section 4 and Annex A of [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386).
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain a frame where `key_frame` is `true`, as defined in Section 19.1 of [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386).
## `VideoDecoderConfig` codec string
```ts
'vp8'
```
## `VideoDecoderConfig` description
`description` is not used for this codec.
+33
View File
@@ -0,0 +1,33 @@
<script setup>
import { VPBadge } from 'vitepress/theme'
</script>
<VPBadge type="info" text="Video codec" />
# VP9 codec registration
## Description
The VP9 video codec, specified in the [VP9 Bitstream & Decoding Process Specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf).
## Codec ID
```ts
'vp9'
```
## `EncodedPacket` data
The packet's data must be a frame as described in Section 6 of the [VP9 Bitstream & Decoding Process Specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf).
## `EncodedPacket` type
If the packet's type is `'key'`, then the packet is expected to contain a frame with `frame_type` of `KEY_FRAME`, as defined in Section 7.2 of the [VP9 Bitstream & Decoding Process Specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf).
## `VideoDecoderConfig` codec string
The full codec string begins with the prefix `'vp09.'`, with a variable-length suffix as specified in the [VP Codec ISO Media File Format Binding](https://www.webmproject.org/vp9/mp4/).
## `VideoDecoderConfig` description
`description` is not used for this codec.
+13
View File
@@ -367,6 +367,19 @@ If only `start` is set, the clip will run until the end of the input file. If on
Note that when using the trimming defaults, the resulting media file will always begin at timestamp 0. If your input file has a start time offset (like is common with MPEG-TS files) and you want to retain that, use `trim: { start: 0 }` to ensure timestamps don't get shifted.
---
You can even use negative trimming values to offset the start of the media:
```ts
const conversion = await Conversion.init({
// ...
trim: {
start: -2, // Two seconds of no media data (freeze frame / silence) at the start
},
// ...
});
```
## Metadata tags
By default, any [descriptive metadata tags](../api/MetadataTags.md) of the input will be copied to the output. If you want to further control the metadata tags written to the output, you can use the `tags` options:
+4 -4
View File
@@ -74,7 +74,7 @@ type VideoEncodingConfig = {
- `bitrateMode`: Can be used to control constant vs. variable bitrate.
- `latencyMode`: The latency mode as specified by the WebCodecs API. Browsers default to `quality`. Media stream-driven video sources will automatically use the `realtime` setting.
- `keyFrameInterval`: The maximum interval in seconds between two adjacent key frames. Defaults to 5 seconds. More frequent key frames improve seeking behavior but increase file size. When using multiple video tracks, this value should be set to the same value for all tracks.
- `fullCodecString`: Allows you to optionally specify the full codec string used by the video encoder, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). For example, you may set it to `'avc1.42001f'` when using AVC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
- `fullCodecString`: Allows you to optionally specify the full codec string used by the video encoder, as specified in the [Mediabunny Codec Registry](/codec-registry/overview). For example, you may set it to `'avc1.42001f'` when using AVC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
- `hardwareAcceleration`: A hint that configures the hardware acceleration method of this codec. This is best left on `'no-preference'`.
- `scalabilityMode`: An encoding scalability mode identifier as defined by [WebRTC-SVC](https://w3c.github.io/webrtc-svc/#scalabilitymodes*).
- `contentHint`: An encoding video content hint as defined by [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).
@@ -104,7 +104,7 @@ type AudioEncodingConfig = {
- `codec`: The [audio codec](./supported-formats-and-codecs#audio-codecs) used for encoding. Can be omitted for uncompressed PCM codecs.
- `bitrate`: The target number of bits per second. Alternatively, this can be a [subjective quality](#subjective-qualities).
- `bitrateMode`: Can be used to control constant vs. variable bitrate.
- `fullCodecString`: Allows you to optionally specify the full codec string used by the audio encoder, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). For example, you may set it to `'mp4a.40.2'` when using AAC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
- `fullCodecString`: Allows you to optionally specify the full codec string used by the audio encoder, as specified in the [Mediabunny Codec Registry](/codec-registry/overview). For example, you may set it to `'mp4a.40.2'` when using AAC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
- `onEncodedPacket`: Called for each successfully encoded packet. Useful for determining encoding progress.
- `onEncoderConfig`: Called when the internal encoder config, as used by the WebCodecs API, is created. You can use this to introspect the full codec string.
@@ -240,7 +240,7 @@ await packetSource.add(firstPacket, {
});
```
`codec`, `codedWidth`, and `codedHeight` are required for all codecs, whereas `description` is required for some codecs. Additional fields, such as `colorSpace`, are optional. The [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/) specifies the formats of `codec` and `description` for each video codec, which you must adhere to.
`codec`, `codedWidth`, and `codedHeight` are required for all codecs, whereas `description` is required for some codecs. Additional fields, such as `colorSpace`, are optional. The [Mediabunny Codec Registry](/codec-registry/overview) specifies the formats of `codec` and `description` for each video codec, which you **must** adhere to.
#### B-frames
@@ -397,7 +397,7 @@ await packetSource.add(firstPacket, {
});
```
`codec`, `numberOfChannels`, and `sampleRate` are required for all codecs, whereas `description` is required for some codecs. The [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/) specifies the formats of `codec` and `description` for each audio codec, which you must adhere to.
`codec`, `numberOfChannels`, and `sampleRate` are required for all codecs, whereas `description` is required for some codecs. The [Mediabunny Codec Registry](/codec-registry/overview) specifies the formats of `codec` and `description` for each audio codec, which you must adhere to.
## Subtitle sources
+2
View File
@@ -94,6 +94,8 @@ constructor(
);
```
When creating a packet for a given codec, you *must* adhere to the data format specified in the [Mediabunny Codec Registry](/codec-registry/overview).
::: info
You probably won't ever need to set `sequenceNumber` or `byteLength` in the constructor.
:::
+1 -1
View File
@@ -129,7 +129,7 @@ track.codec; // => MediaCodec | null
```
This field is `null` when the track's codec couldn't be recognized or is not supported by Mediabunny. See [Codecs](./supported-formats-and-codecs#codecs) for the full list of supported codecs. When Mediabunny doesn't recognize the format, you can still use the `internalCodecId` field to figure out the codec of the track, although its format depends on the container format used and is not homogenized by Mediabunny.
You can also extract the full codec parameter string from the track, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/):
You can also extract the full codec parameter string from the track, as specified in the [Mediabunny Codec Registry](/codec-registry/overview):
```ts
await track.getCodecParameterString(); // => 'avc1.42001f'
```
+12 -4
View File
@@ -21,6 +21,8 @@ Mediabunny supports a wide range of video, audio, and subtitle codecs. More spec
The availability of the codecs provided by the WebCodecs API depends on the browser and thus cannot be guaranteed by this library. Mediabunny provides [special utility functions](#querying-codec-encodability) to check which codecs are able to be encoded. You can also specify [custom coders](#custom-coders) to provide your own encoder/decoder implementation if the browser doesn't support the codec natively.
For precise definitions of each codec including the corresponding packet format, please refer to the [Mediabunny Codec Registry](/codec-registry/overview).
::: info
Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, meaning they are always supported.
:::
@@ -40,6 +42,8 @@ Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, m
- `'mp3'` - MP3
- `'vorbis'` - Vorbis
- `'flac'` - Free Lossless Audio Codec (FLAC)
- `'ac3'` - Dolby Digital (AC-3) [^1]
- `'eac3'` - Dolby Digital Plus (E-AC-3) [^1]
- `'pcm-u8'` - 8-bit unsigned PCM
- `'pcm-s8'` - 8-bit signed PCM
- `'pcm-s16'` - 16-bit little-endian signed PCM
@@ -55,6 +59,8 @@ Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, m
- `'ulaw'` - μ-law PCM
- `'alaw'` - A-law PCM
[^1]: AC-3 and E-AC-3 are not natively supported by WebCodecs. To encode or decode these codecs, you must provide a [custom coder](#custom-coders).
### Subtitle codecs
- `'webvtt'` - WebVTT
@@ -63,7 +69,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[^1] | .ogg | .mp3 | .wav | .aac | .flac | .ts |
| | .mp4 | .mov | .mkv | .webm[^2] | .ogg | .mp3 | .wav | .aac | .flac | .ts |
|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
| `'avc'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
| `'hevc'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
@@ -75,6 +81,8 @@ Not all codecs can be used with all containers. The following table specifies th
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | | | | ✓ |
| `'vorbis'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | | |
| `'flac'` | ✓ | ✓ | ✓ | | | | | | ✓ | |
| `'ac3'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
| `'eac3'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
| `'pcm-u8'` | | ✓ | ✓ | | | | ✓ | | | |
| `'pcm-s8'` | | ✓ | | | | | | | | |
| `'pcm-s16'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
@@ -89,11 +97,11 @@ Not all codecs can be used with all containers. The following table specifies th
| `'pcm-f64be'` | ✓ | ✓ | | | | | | | | |
| `'ulaw'` | | ✓ | | | | | ✓ | | | |
| `'alaw'` | | ✓ | | | | | ✓ | | | |
| `'webvtt'`[^2] | (✓) | | (✓) | (✓) | | | | | | |
| `'webvtt'`[^3] | (✓) | | (✓) | (✓) | | | | | | |
[^1]: WebM only supports a small subset of the codecs supported by Matroska. However, this library can technically read all codecs from a WebM that are supported by Matroska.
[^2]: WebVTT can only be written, not read.
[^2]: WebM only supports a small subset of the codecs supported by Matroska. However, this library can technically read all codecs from a WebM that are supported by Matroska.
[^3]: WebVTT can only be written, not read.
## Querying codec encodability
+1 -1
View File
@@ -124,6 +124,7 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/1836701', name: 'Bean Deng', url: 'https://github.com/HADB' },
{ image: 'https://avatars.githubusercontent.com/u/255616819', name: 'cronischarles-del', url: 'https://github.com/cronischarles-del' },
{ image: 'https://avatars.githubusercontent.com/u/37973863', name: 'Gomi', url: 'https://github.com/gxy5202' },
{ image: 'https://avatars.githubusercontent.com/u/36898190', name: 'jepcd', url: 'https://github.com/jepcd' },
{ image: 'https://avatars.githubusercontent.com/u/63088713', name: 'taf2000', url: 'https://github.com/taf2000' },
{ image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
@@ -132,7 +133,6 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/3709646', name: 'Rodrigo Belfiore', url: 'https://github.com/roprgm' },
{ image: 'https://avatars.githubusercontent.com/u/31102694', name: 'Aiden Liu', url: 'https://github.com/aidenlx' },
{ image: 'https://avatars.githubusercontent.com/u/41021374', name: 'arthco', url: 'https://github.com/arthtyagi' },
{ image: 'https://avatars.githubusercontent.com/u/36898190', name: 'alakhpc', url: 'https://github.com/alakhpc' },
{ image: 'https://avatars.githubusercontent.com/u/5907357', name: 'Harvey Zhao', url: 'https://github.com/zhw2590582' },
],
};
+22 -3
View File
@@ -1,5 +1,24 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="5.33333" fill="#DAFF99"/>
<path d="M14.3273 9.29964C14.5206 8.97719 14.9878 8.97719 15.1811 9.29964L17.7213 13.5375C17.7634 13.6077 17.8221 13.6664 17.8923 13.7085L22.1301 16.2487C22.4526 16.4419 22.4526 16.9092 22.1301 17.1025L17.8923 19.6427C17.8221 19.6848 17.7634 19.7435 17.7213 19.8137L15.1811 24.0515C14.9878 24.374 14.5206 24.374 14.3273 24.0515L11.7871 19.8137C11.745 19.7435 11.6863 19.6848 11.6161 19.6427L7.37825 17.1025C7.0558 16.9092 7.0558 16.4419 7.37825 16.2487L11.6161 13.7085C11.6863 13.6664 11.745 13.6077 11.7871 13.5375L14.3273 9.29964Z" fill="#2D2D2D"/>
<path d="M22.7374 7.51156C22.7857 7.43094 22.9026 7.43094 22.9509 7.51156L23.7608 8.86276C23.7713 8.88032 23.786 8.895 23.8035 8.90552L25.1548 9.71544C25.2354 9.76376 25.2354 9.88058 25.1548 9.9289L23.8035 10.7388C23.786 10.7493 23.7713 10.764 23.7608 10.7816L22.9509 12.1328C22.9026 12.2134 22.7857 12.2134 22.7374 12.1328L21.9275 10.7816C21.917 10.764 21.9023 10.7493 21.8847 10.7388L20.5335 9.9289C20.4529 9.88058 20.4529 9.76376 20.5335 9.71544L21.8847 8.90552C21.9023 8.895 21.917 8.88032 21.9275 8.86276L22.7374 7.51156Z" fill="#2D2D2D"/>
<g clip-path="url(#clip0_3664_2482)">
<rect width="32" height="32" rx="4" fill="#DAFF99"/>
<path d="M15.736 9.07175C15.8492 8.8828 16.123 8.8828 16.2362 9.07175L18.9703 13.6355C18.9949 13.6766 19.0293 13.711 19.0705 13.7357L23.6322 16.4714C23.821 16.5846 23.821 16.8582 23.6322 16.9715L19.0705 19.7072C19.0293 19.7318 18.9949 19.7662 18.9703 19.8074L16.2362 24.3711C16.123 24.5601 15.8492 24.5601 15.736 24.3711L13.0019 19.8074C12.9772 19.7662 12.9428 19.7318 12.9017 19.7072L8.34001 16.9715C8.15121 16.8582 8.15121 16.5846 8.34001 16.4714L12.9017 13.7357C12.9428 13.711 12.9772 13.6766 13.0019 13.6355L15.736 9.07175Z" fill="#2D2D2D"/>
<g filter="url(#filter0_d_3664_2482)">
<path d="M23.8321 7.41198C23.8604 7.36474 23.9288 7.36474 23.9571 7.41198L24.815 8.84391C24.8212 8.85419 24.8298 8.86279 24.84 8.86896L26.2713 9.72731C26.3185 9.75561 26.3185 9.82401 26.2713 9.85232L24.84 10.7107C24.8298 10.7168 24.8212 10.7254 24.815 10.7357L23.9571 12.1676C23.9288 12.2149 23.8604 12.2149 23.8321 12.1676L22.9742 10.7357C22.9681 10.7254 22.9595 10.7168 22.9492 10.7107L21.5179 9.85232C21.4707 9.82401 21.4707 9.75561 21.5179 9.72731L22.9492 8.86896C22.9595 8.86279 22.9681 8.85419 22.9742 8.84391L23.8321 7.41198Z" fill="#2D2D2D"/>
</g>
</g>
<defs>
<filter id="filter0_d_3664_2482" x="-56.5175" y="-70.623" width="160.824" height="160.826" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="39"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3664_2482"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3664_2482" result="shape"/>
</filter>
<clipPath id="clip0_3664_2482">
<rect width="32" height="32" rx="4" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.32.1",
"version": "1.33.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.32.1",
"version": "1.33.0",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
@@ -7739,9 +7739,9 @@
}
},
"node_modules/mediabunny": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.32.0.tgz",
"integrity": "sha512-wMWynAQqZkmsLm+d2+fovcBal3zBQu/bot3u+yV4bDTfNxPeEDHv1wjiz6yjyKcXBuxfaQK96gImqLcKLfJG7w==",
"version": "1.32.2",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.32.2.tgz",
"integrity": "sha512-cxzGriei0JAYaYApO7xsN8ITq1npEnQPBAE43Z3vnGHhh8UVm8Cgb5o06wrufDVYlFg58I2sF6d/xWH39uXuEw==",
"license": "MPL-2.0",
"peer": true,
"workspaces": [
@@ -12065,7 +12065,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
"version": "1.32.1",
"version": "1.33.0",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.32.1",
"version": "1.33.0",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.32.1",
"version": "1.33.0",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+1 -4
View File
@@ -62,10 +62,7 @@ export class AdtsMuxer extends Muxer {
const description = meta?.decoderConfig?.description;
// From the WebCodecs Codec Registry:
// "If description is present, it is assumed to a AudioSpecificConfig as defined in [iso14496-3] section
// 1.6.2.1, Table 1.15, and the bitstream is assumed to be in aac.
// If the description is not present, the bitstream is assumed to be in adts format."
// Follows from the Mediabunny Codec Registry:
this.inputIsAdts = !description;
if (!this.inputIsAdts) {
+364
View File
@@ -2643,3 +2643,367 @@ export const createVorbisComments = (headerBytes: Uint8Array, tags: MetadataTags
return commentHeader;
};
// ============================================================================
// AC-3 / E-AC-3 Parsing
// Reference: ETSI TS 102 366 V1.4.1
// ============================================================================
/** Sample rates indexed by fscod (Table 4.1) */
export const AC3_SAMPLE_RATES = [48000, 44100, 32000] as const;
/**
* Channel counts indexed by acmod (Table 4.3).
* Does NOT include LFE - add lfeon to get total channel count.
*/
export const AC3_ACMOD_CHANNEL_COUNTS = [2, 1, 2, 3, 3, 4, 4, 5] as const;
export interface Ac3FrameInfo {
/** Sample rate code */
fscod: number;
/** Bitstream ID */
bsid: number;
/** Bitstream mode */
bsmod: number;
/** Audio coding mode */
acmod: number;
/** LFE channel on */
lfeon: number;
/** Bit rate code (0-18, maps to bitrate via Table F.4.1) */
bitRateCode: number;
}
/**
* Parse an AC-3 syncframe to extract BSI (Bit Stream Information) fields.
* Section 4.3
*/
export const parseAc3SyncFrame = (data: Uint8Array): Ac3FrameInfo | null => {
if (data.length < 7) {
return null;
}
// Check sync word (0x0B77)
if (data[0] !== 0x0B || data[1] !== 0x77) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.skipBits(16); // sync word
bitstream.skipBits(16); // crc1
const fscod = bitstream.readBits(2);
if (fscod === 3) {
return null; // Reserved, invalid
}
const frmsizecod = bitstream.readBits(6);
const bsid = bitstream.readBits(5);
// Verify this is AC-3
if (bsid > 8) {
return null;
}
const bsmod = bitstream.readBits(3);
const acmod = bitstream.readBits(3);
// Skip cmixlev (center downmix level) if three front channels are in use (L, C, R).
if ((acmod & 0x1) !== 0 && acmod !== 0x1) {
bitstream.skipBits(2);
}
// Skip surmixlev (surround downmix level) if surround channels are in use.
if ((acmod & 0x4) !== 0) {
bitstream.skipBits(2);
}
// Skip dsurmod if stereo (acmod === 2)
if (acmod === 0x2) {
bitstream.skipBits(2);
}
const lfeon = bitstream.readBits(1);
const bitRateCode = Math.floor(frmsizecod / 2);
return { fscod, bsid, bsmod, acmod, lfeon, bitRateCode };
};
/**
* AC-3 frame sizes in bytes, indexed by [3 * frmsizecod + fscod].
* fscod: 0=48kHz, 1=44.1kHz, 2=32kHz
* Values are 16-bit words * 2 (to convert to bytes).
* Table 4.13
*/
export const AC3_FRAME_SIZES = [
// frmsizecod, [48kHz, 44.1kHz, 32kHz] in bytes
64 * 2, 69 * 2, 96 * 2,
64 * 2, 70 * 2, 96 * 2,
80 * 2, 87 * 2, 120 * 2,
80 * 2, 88 * 2, 120 * 2,
96 * 2, 104 * 2, 144 * 2,
96 * 2, 105 * 2, 144 * 2,
112 * 2, 121 * 2, 168 * 2,
112 * 2, 122 * 2, 168 * 2,
128 * 2, 139 * 2, 192 * 2,
128 * 2, 140 * 2, 192 * 2,
160 * 2, 174 * 2, 240 * 2,
160 * 2, 175 * 2, 240 * 2,
192 * 2, 208 * 2, 288 * 2,
192 * 2, 209 * 2, 288 * 2,
224 * 2, 243 * 2, 336 * 2,
224 * 2, 244 * 2, 336 * 2,
256 * 2, 278 * 2, 384 * 2,
256 * 2, 279 * 2, 384 * 2,
320 * 2, 348 * 2, 480 * 2,
320 * 2, 349 * 2, 480 * 2,
384 * 2, 417 * 2, 576 * 2,
384 * 2, 418 * 2, 576 * 2,
448 * 2, 487 * 2, 672 * 2,
448 * 2, 488 * 2, 672 * 2,
512 * 2, 557 * 2, 768 * 2,
512 * 2, 558 * 2, 768 * 2,
640 * 2, 696 * 2, 960 * 2,
640 * 2, 697 * 2, 960 * 2,
768 * 2, 835 * 2, 1152 * 2,
768 * 2, 836 * 2, 1152 * 2,
896 * 2, 975 * 2, 1344 * 2,
896 * 2, 976 * 2, 1344 * 2,
1024 * 2, 1114 * 2, 1536 * 2,
1024 * 2, 1115 * 2, 1536 * 2,
1152 * 2, 1253 * 2, 1728 * 2,
1152 * 2, 1254 * 2, 1728 * 2,
1280 * 2, 1393 * 2, 1920 * 2,
1280 * 2, 1394 * 2, 1920 * 2,
];
/** Number of samples per AC-3 syncframe (always 1536) */
export const AC3_SAMPLES_PER_FRAME = 1536;
/**
* AC-3 registration_descriptor for MPEG-TS.
* Section A.2.3
*/
export const AC3_REGISTRATION_DESCRIPTOR = new Uint8Array([0x05, 0x04, 0x41, 0x43, 0x2d, 0x33]);
/** E-AC-3 registration_descriptor for MPEG-TS/ */
export const EAC3_REGISTRATION_DESCRIPTOR = new Uint8Array([0x05, 0x04, 0x45, 0x41, 0x43, 0x33]);
/** E-AC-3 reduced sample rates for fscod2 per ATSC A/52:2018 */
const EAC3_REDUCED_SAMPLE_RATES = [24000, 22050, 16000] as const;
/** Number of audio blocks per syncframe, indexed by numblkscod */
export const EAC3_NUMBLKS_TABLE = [1, 2, 3, 6] as const;
/**
* E-AC-3 independent substream info.
* Each independent substream represents a separate audio program.
*/
export interface Eac3SubstreamInfo {
/** Sample rate code */
fscod: number;
/** Sample rate code 2 (ATSC A/52:2018) */
fscod2: number | null;
/** Bitstream ID */
bsid: number;
/** Bitstream mode */
bsmod: number;
/** Audio coding mode */
acmod: number;
/** LFE channel on */
lfeon: number;
/** Number of dependent substreams */
numDepSub: number;
/** Channel locations for dependent substreams */
chanLoc: number;
}
/**
* E-AC-3 decoder configuration (dec3 box contents).
*/
export interface Eac3FrameInfo {
/** Data rate in kbps */
dataRate: number;
/** Independent substreams */
substreams: Eac3SubstreamInfo[];
}
/**
* Parse an E-AC-3 syncframe to extract BSI fields.
* Section E.1.2
*/
export const parseEac3SyncFrame = (data: Uint8Array): Eac3FrameInfo | null => {
if (data.length < 6) {
return null;
}
// Check sync word (0x0B77)
if (data[0] !== 0x0B || data[1] !== 0x77) {
return null;
}
const bitstream = new Bitstream(data);
bitstream.skipBits(16); // sync word
const strmtyp = bitstream.readBits(2);
bitstream.skipBits(3); // substreamid
// Only parse independent substreams (strmtyp 0 or 2)
if (strmtyp !== 0 && strmtyp !== 2) {
return null;
}
const frmsiz = bitstream.readBits(11);
const fscod = bitstream.readBits(2);
let fscod2 = 0;
let numblkscod: number;
if (fscod === 3) {
// fscod2 enables reduced sample rates (24/22.05/16 kHz) per ATSC A/52:2018
fscod2 = bitstream.readBits(2);
numblkscod = 3; // Implicitly 6 blocks when fscod=3
} else {
numblkscod = bitstream.readBits(2);
}
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
const bsid = bitstream.readBits(5);
// Verify this is E-AC-3
if (bsid < 11 || bsid > 16) {
return null;
}
// Calculate data rate: ((frmsiz + 1) * fs) / (numblks * 16)
const numblks = EAC3_NUMBLKS_TABLE[numblkscod]!;
let fs: number;
if (fscod < 3) {
fs = AC3_SAMPLE_RATES[fscod]! / 1000;
} else {
fs = EAC3_REDUCED_SAMPLE_RATES[fscod2]! / 1000;
}
const dataRate = Math.round(((frmsiz + 1) * fs) / (numblks * 16));
// These fields require parsing beyond the first frame.
// Defaults are correct for almost all content.
const bsmod = 0;
const numDepSub = 0;
const chanLoc = 0;
const substream: Eac3SubstreamInfo = {
fscod,
fscod2,
bsid,
bsmod,
acmod,
lfeon,
numDepSub,
chanLoc,
};
return {
dataRate,
substreams: [substream],
};
};
/**
* Parse a dec3 box to extract E-AC-3 parameters.
* Section F.6
*/
export const parseEac3Config = (data: Uint8Array): Eac3FrameInfo | null => {
if (data.length < 2) {
return null;
}
const bitstream = new Bitstream(data);
const dataRate = bitstream.readBits(13);
const numIndSub = bitstream.readBits(3);
const substreams: Eac3SubstreamInfo[] = [];
for (let i = 0; i <= numIndSub; i++) {
// Check we have enough data for this substream
// Each substream needs at least 24 bits (3 bytes) without dependent subs
if (Math.ceil(bitstream.pos / 8) + 3 > data.length) {
break;
}
const fscod = bitstream.readBits(2);
const bsid = bitstream.readBits(5);
bitstream.skipBits(1); // reserved
bitstream.skipBits(1); // asvc
const bsmod = bitstream.readBits(3);
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
bitstream.skipBits(3); // reserved
const numDepSub = bitstream.readBits(4);
let chanLoc = 0;
if (numDepSub > 0) {
chanLoc = bitstream.readBits(9);
} else {
bitstream.skipBits(1); // reserved
}
substreams.push({
fscod,
fscod2: null,
bsid,
bsmod,
acmod,
lfeon,
numDepSub,
chanLoc,
});
}
if (substreams.length === 0) {
return null;
}
return { dataRate, substreams };
};
/**
* Get sample rate from E-AC-3 config.
* See ATSC A/52:2018 for handling fscod2.
*/
export const getEac3SampleRate = (config: Eac3FrameInfo): number | null => {
const sub = config.substreams[0];
assert(sub);
if (sub.fscod < 3) {
return AC3_SAMPLE_RATES[sub.fscod]!;
} else if (sub.fscod2 !== null && sub.fscod2 < 3) {
return EAC3_REDUCED_SAMPLE_RATES[sub.fscod2]!;
}
return null;
};
/**
* Get channel count from E-AC-3 config (first independent substream only).
*/
export const getEac3ChannelCount = (config: Eac3FrameInfo): number => {
const sub = config.substreams[0];
assert(sub);
let channels = AC3_ACMOD_CHANNEL_COUNTS[sub.acmod]! + sub.lfeon;
// Add channels from dependent substreams
if (sub.numDepSub > 0) {
const CHAN_LOC_COUNTS = [2, 2, 1, 1, 2, 2, 2, 1, 1];
for (let bit = 0; bit < 9; bit++) {
if (sub.chanLoc & (1 << (8 - bit))) {
channels += CHAN_LOC_COUNTS[bit]!;
}
}
}
return channels;
};
+31 -3
View File
@@ -70,6 +70,8 @@ export const NON_PCM_AUDIO_CODECS = [
'mp3',
'vorbis',
'flac',
'ac3',
'eac3',
] as const;
/**
* List of known audio codecs, ordered by encoding preference.
@@ -528,6 +530,10 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe
return 'vorbis';
} else if (codec === 'flac') {
return 'flac';
} else if (codec === 'ac3') {
return 'ac-3';
} else if (codec === 'eac3') {
return 'ec-3';
} else if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return codec;
}
@@ -573,6 +579,10 @@ export const extractAudioCodecString = (trackInfo: {
return 'vorbis';
} else if (codec === 'flac') {
return 'flac';
} else if (codec === 'ac3') {
return 'ac-3';
} else if (codec === 'eac3') {
return 'ec-3';
} else if (codec && (PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
return codec;
}
@@ -742,6 +752,10 @@ export const inferCodecFromCodecString = (codecString: string): MediaCodec | nul
return 'vorbis';
} else if (codecString === 'flac') {
return 'flac';
} else if (codecString === 'ac-3' || codecString === 'ac3') {
return 'ac3';
} else if (codecString === 'ec-3' || codecString === 'eac3') {
return 'eac3';
} else if (codecString === 'ulaw') {
return 'ulaw';
} else if (codecString === 'alaw') {
@@ -819,7 +833,7 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in'
+ ' the WebCodecs Codec Registry.',
+ ' the Mediabunny Codec Registry.',
);
}
if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) {
@@ -933,7 +947,9 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
}
};
const VALID_AUDIO_CODEC_STRING_PREFIXES = ['mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm'];
const VALID_AUDIO_CODEC_STRING_PREFIXES = [
'mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm', 'ac-3', 'ec-3',
];
export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => {
if (!metadata) {
@@ -954,7 +970,7 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError(
'Audio chunk metadata decoder configuration codec string must be a valid audio codec string as specified in'
+ ' the WebCodecs Codec Registry.',
+ ' the Mediabunny Codec Registry.',
);
}
if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
@@ -1051,6 +1067,18 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
+ ' adhere to the format described in https://www.w3.org/TR/webcodecs-flac-codec-registration/.',
);
}
} else if (metadata.decoderConfig.codec.startsWith('ac-3') || metadata.decoderConfig.codec.startsWith('ac3')) {
// AC3-specific validation
if (metadata.decoderConfig.codec !== 'ac-3') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for AC-3 must be "ac-3".');
}
} else if (metadata.decoderConfig.codec.startsWith('ec-3') || metadata.decoderConfig.codec.startsWith('eac3')) {
// EAC3-specific validation
if (metadata.decoderConfig.codec !== 'ec-3') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for EC-3 must be "ec-3".');
}
} else if (
metadata.decoderConfig.codec.startsWith('pcm')
|| metadata.decoderConfig.codec.startsWith('ulaw')
+7 -6
View File
@@ -564,11 +564,11 @@ export class Conversion {
if (options.trim !== undefined && (!options.trim || typeof options.trim !== 'object')) {
throw new TypeError('options.trim, when provided, must be an object.');
}
if (options.trim?.start !== undefined && (!Number.isFinite(options.trim.start) || options.trim.start < 0)) {
throw new TypeError('options.trim.start, when provided, must be a non-negative number.');
if (options.trim?.start !== undefined && (!Number.isFinite(options.trim.start))) {
throw new TypeError('options.trim.start, when provided, must be a finite number.');
}
if (options.trim?.end !== undefined && (!Number.isFinite(options.trim.end) || options.trim.end < 0)) {
throw new TypeError('options.trim.end, when provided, must be a non-negative number.');
if (options.trim?.end !== undefined && (!Number.isFinite(options.trim.end))) {
throw new TypeError('options.trim.end, when provided, must be a finite number.');
}
if (
options.trim?.start !== undefined
@@ -607,7 +607,7 @@ export class Conversion {
// those out by default.
0,
);
this._endTimestamp = this._options.trim?.end ?? Infinity;
this._endTimestamp = Math.max(this._options.trim?.end ?? Infinity, this._startTimestamp);
const inputTracks = await this.input.getTracks();
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
@@ -1326,7 +1326,8 @@ export class Conversion {
let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
let needsResample = numberOfChannels !== originalNumberOfChannels
|| sampleRate !== originalSampleRate
|| firstTimestamp < this._startTimestamp;
|| firstTimestamp < this._startTimestamp
|| (firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData);
let audioCodecs = this.output.format.getSupportedAudioCodecs();
if (
+4 -2
View File
@@ -127,7 +127,7 @@ export type VideoEncodingAdditionalOptions = {
*/
latencyMode?: 'quality' | 'realtime';
/**
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
* The full codec string as specified in the Mediabunny Codec Registry. This string must match the codec
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
*/
fullCodecString?: string;
@@ -280,7 +280,7 @@ export type AudioEncodingAdditionalOptions = {
/** Configures the bitrate mode. */
bitrateMode?: 'constant' | 'variable';
/**
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
* The full codec string as specified in the Mediabunny Codec Registry. This string must match the codec
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
*/
fullCodecString?: string;
@@ -375,6 +375,8 @@ export class Quality {
opus: 64000, // 64kbps base for Opus
mp3: 160000, // 160kbps base for MP3
vorbis: 64000, // 64kbps base for Vorbis
ac3: 384000, // 384kbps base for AC-3
eac3: 192000, // 192kbps base for E-AC-3
};
const baseBitrate = baseRates[codec as keyof typeof baseRates];
+79 -1
View File
@@ -20,6 +20,7 @@ import {
UNDETERMINED_LANGUAGE,
assertNever,
keyValueIterator,
Bitstream,
} from '../misc';
import {
AudioCodec,
@@ -43,7 +44,7 @@ import {
IsobmffVideoTrackData,
Sample,
} from './isobmff-muxer';
import { parseOpusIdentificationHeader } from '../codec-data';
import { parseAc3SyncFrame, parseEac3SyncFrame, parseOpusIdentificationHeader } from '../codec-data';
import { MetadataTags, RichImageData } from '../metadata';
export class IsobmffBoxWriter {
@@ -907,6 +908,79 @@ const pcmC = (trackData: IsobmffAudioTrackData) => {
]);
};
/** AC3SpecificBox */
const dac3 = (trackData: IsobmffAudioTrackData) => {
const frameInfo = parseAc3SyncFrame(trackData.info.firstPacket.data);
if (!frameInfo) {
throw new Error(
'Couldn\'t extract AC-3 frame info from the audio packet. '
+ 'Ensure the packets contain valid AC-3 sync frames (as specified in ETSI TS 102 366).',
);
}
const bytes = new Uint8Array(3);
const bitstream = new Bitstream(bytes);
bitstream.writeBits(2, frameInfo.fscod);
bitstream.writeBits(5, frameInfo.bsid);
bitstream.writeBits(3, frameInfo.bsmod);
bitstream.writeBits(3, frameInfo.acmod);
bitstream.writeBits(1, frameInfo.lfeon);
bitstream.writeBits(5, frameInfo.bitRateCode);
bitstream.writeBits(5, 0); // reserved
return box('dac3', [...bytes]);
};
/** EC3SpecificBox */
const dec3 = (trackData: IsobmffAudioTrackData) => {
const frameInfo = parseEac3SyncFrame(trackData.info.firstPacket.data);
if (!frameInfo) {
throw new Error(
'Couldn\'t extract E-AC-3 frame info from the audio packet. '
+ 'Ensure the packets contain valid E-AC-3 sync frames (as specified in ETSI TS 102 366).',
);
}
// Calculate size
let totalBits = 16; // header: data_rate (13) + num_ind_sub (3)
for (const sub of frameInfo.substreams) {
totalBits += 23; // fixed fields per substream
if (sub.numDepSub > 0) {
totalBits += 9; // chan_loc
} else {
totalBits += 1; // reserved
}
}
const size = Math.ceil(totalBits / 8);
const bytes = new Uint8Array(size);
const bitstream = new Bitstream(bytes);
bitstream.writeBits(13, frameInfo.dataRate);
bitstream.writeBits(3, frameInfo.substreams.length - 1); // num_ind_sub
for (const sub of frameInfo.substreams) {
bitstream.writeBits(2, sub.fscod);
bitstream.writeBits(5, sub.bsid);
bitstream.writeBits(1, 0); // reserved
bitstream.writeBits(1, 0); // asvc = 0
bitstream.writeBits(3, sub.bsmod);
bitstream.writeBits(3, sub.acmod);
bitstream.writeBits(1, sub.lfeon);
bitstream.writeBits(3, 0); // reserved
bitstream.writeBits(4, sub.numDepSub);
if (sub.numDepSub > 0) {
bitstream.writeBits(9, sub.chanLoc);
} else {
bitstream.writeBits(1, 0); // reserved
}
}
return box('dec3', [...bytes]);
};
export const subtitleSampleDescription = (
compressionType: string,
trackData: IsobmffSubtitleTrackData,
@@ -1606,6 +1680,8 @@ const audioCodecToBoxName = (codec: AudioCodec, isQuickTime: boolean): string =>
case 'alaw': return 'alaw';
case 'pcm-u8': return 'raw ';
case 'pcm-s8': return 'sowt';
case 'ac3': return 'ac-3';
case 'eac3': return 'ec-3';
}
// Logic diverges here
@@ -1645,6 +1721,8 @@ const audioCodecToConfigurationBox = (codec: AudioCodec, isQuickTime: boolean) =
case 'opus': return dOps;
case 'vorbis': return esds;
case 'flac': return dfLa;
case 'ac3': return dac3;
case 'eac3': return dec3;
}
// Logic diverges here
+54
View File
@@ -27,6 +27,11 @@ import {
FlacBlockType,
HevcDecoderConfigurationRecord,
Vp9CodecInfo,
parseEac3Config,
getEac3SampleRate,
getEac3ChannelCount,
AC3_SAMPLE_RATES,
AC3_ACMOD_CHANNEL_COUNTS,
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
@@ -952,6 +957,10 @@ export class IsobmffDemuxer extends Demuxer {
track.info.codec = 'ulaw';
} else if (lowercaseBoxName === 'alaw') {
track.info.codec = 'alaw';
} else if (lowercaseBoxName === 'ac-3') {
track.info.codec = 'ac3';
} else if (lowercaseBoxName === 'ec-3') {
track.info.codec = 'eac3';
} else {
console.warn(`Unsupported audio codec (sample entry type '${sampleBoxInfo.name}').`);
}
@@ -1463,6 +1472,51 @@ export class IsobmffDemuxer extends Demuxer {
track.info.codecDescription = description;
}; break;
case 'dac3': { // AC3SpecificBox
const track = this.currentTrack;
if (!track) {
break;
}
assert(track.info?.type === 'audio');
const bytes = readBytes(slice, 3);
const bitstream = new Bitstream(bytes);
const fscod = bitstream.readBits(2);
bitstream.skipBits(5 + 3); // Skip bsid and bsmod
const acmod = bitstream.readBits(3);
const lfeon = bitstream.readBits(1);
if (fscod < 3) {
track.info.sampleRate = AC3_SAMPLE_RATES[fscod]!;
}
track.info.numberOfChannels = AC3_ACMOD_CHANNEL_COUNTS[acmod]! + lfeon;
}; break;
case 'dec3': { // EC3SpecificBox
const track = this.currentTrack;
if (!track) {
break;
}
assert(track.info?.type === 'audio');
const bytes = readBytes(slice, boxInfo.contentSize);
const config = parseEac3Config(bytes);
if (!config) {
console.warn('Invalid dec3 box contents, ignoring.');
break;
}
const sampleRate = getEac3SampleRate(config);
if (sampleRate !== null) {
track.info.sampleRate = sampleRate;
}
track.info.numberOfChannels = getEac3ChannelCount(config);
}; break;
case 'stts': {
const track = this.currentTrack;
if (!track) {
+3 -1
View File
@@ -111,6 +111,7 @@ export type IsobmffTrackData = {
* ADTS-wrapped data.
*/
requiresAdtsStripping: boolean;
firstPacket: EncodedPacket;
};
} | {
track: OutputSubtitleTrack;
@@ -429,8 +430,9 @@ export class IsobmffMuxer extends Muxer {
!this.isFragmented
&& (PCM_AUDIO_CODECS as readonly string[]).includes(track.source._codec),
requiresAdtsStripping,
firstPacket: packet,
},
timescale: meta.decoderConfig.sampleRate,
timescale: decoderConfig.sampleRate,
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
+2
View File
@@ -744,6 +744,8 @@ export const CODEC_STRING_MAP: Partial<Record<MediaCodec, string>> = {
'opus': 'A_OPUS',
'vorbis': 'A_VORBIS',
'flac': 'A_FLAC',
'ac3': 'A_AC3',
'eac3': 'A_EAC3',
'pcm-u8': 'A_PCM/INT/LIT',
'pcm-s16': 'A_PCM/INT/LIT',
'pcm-s16be': 'A_PCM/INT/BIG',
+6
View File
@@ -1084,6 +1084,12 @@ export class MatroskaDemuxer extends Demuxer {
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.flac) {
this.currentTrack.info.codec = 'flac';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.ac3) {
this.currentTrack.info.codec = 'ac3';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.eac3) {
this.currentTrack.info.codec = 'eac3';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
} else if (this.currentTrack.codecId === 'A_PCM/INT/LIT') {
if (this.currentTrack.info.bitDepth === 8) {
this.currentTrack.info.codec = 'pcm-u8';
+186 -1
View File
@@ -19,16 +19,25 @@ import {
VideoCodec,
} from '../codec';
import {
AC3_ACMOD_CHANNEL_COUNTS,
AC3_SAMPLE_RATES,
AC3_SAMPLES_PER_FRAME,
AvcDecoderConfigurationRecord,
AvcNalUnitType,
extractAvcDecoderConfigurationRecord,
extractHevcDecoderConfigurationRecord,
extractNalUnitTypeForAvc,
extractNalUnitTypeForHevc,
EAC3_NUMBLKS_TABLE,
getEac3ChannelCount,
getEac3SampleRate,
HevcDecoderConfigurationRecord,
HevcNalUnitType,
parseAc3SyncFrame,
parseAvcSps,
parseEac3SyncFrame,
parseHevcSps,
AC3_FRAME_SIZES,
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
@@ -266,7 +275,20 @@ export class MpegTsDemuxer extends Demuxer {
bitstream.skipBits(6);
const esInfoLength = bitstream.readBits(10);
bitstream.skipBits(8 * esInfoLength);
// Check ES descriptors to detect AC-3/E-AC-3 in System B
const esInfoEndPos = bitstream.pos + 8 * esInfoLength;
let hasAc3Descriptor = false;
let hasEac3Descriptor = false;
while (bitstream.pos < esInfoEndPos) {
const descriptorTag = bitstream.readBits(8);
const descriptorLength = bitstream.readBits(8);
if (descriptorTag === 0x6a) {
hasAc3Descriptor = true;
} else if (descriptorTag === 0x7a || descriptorTag === 0xcc) {
hasEac3Descriptor = true;
}
bitstream.skipBits(8 * descriptorLength);
}
let info: ElementaryStream['info'] | null = null;
@@ -306,6 +328,46 @@ export class MpegTsDemuxer extends Demuxer {
};
}; break;
case MpegTsStreamType.AC3_SYSTEM_A: {
info = {
type: 'audio',
codec: 'ac3',
aacCodecInfo: null,
numberOfChannels: -1,
sampleRate: -1,
};
}; break;
case MpegTsStreamType.EAC3_SYSTEM_A: {
info = {
type: 'audio',
codec: 'eac3',
aacCodecInfo: null,
numberOfChannels: -1,
sampleRate: -1,
};
}; break;
case MpegTsStreamType.PRIVATE_DATA: {
if (hasEac3Descriptor) {
info = {
type: 'audio',
codec: 'eac3',
aacCodecInfo: null,
numberOfChannels: -1,
sampleRate: -1,
};
} else if (hasAc3Descriptor) {
info = {
type: 'audio',
codec: 'ac3',
aacCodecInfo: null,
numberOfChannels: -1,
sampleRate: -1,
};
}
}; break;
default: {
// If we don't recognize the codec, we don't surface the track at all. This is because
// we can't determine its metadata and also have no idea how to packetize its data.
@@ -436,6 +498,44 @@ export class MpegTsDemuxer extends Demuxer {
elementaryStream.info.numberOfChannels = result.header.channel === 3 ? 1 : 2;
elementaryStream.info.sampleRate = result.header.sampleRate;
elementaryStream.initialized = true;
} else if (elementaryStream.info.codec === 'ac3') {
const frameInfo = parseAc3SyncFrame(pesPacket.data);
if (!frameInfo) {
throw new Error(
'Invalid AC-3 audio stream; could not read sync frame from first packet.',
);
}
if (frameInfo.fscod === 3) {
throw new Error(
'Invalid AC-3 audio stream; reserved sample rate code found in first packet.',
);
}
elementaryStream.info.numberOfChannels
= AC3_ACMOD_CHANNEL_COUNTS[frameInfo.acmod]! + frameInfo.lfeon;
elementaryStream.info.sampleRate = AC3_SAMPLE_RATES[frameInfo.fscod]!;
elementaryStream.initialized = true;
} else if (elementaryStream.info.codec === 'eac3') {
const frameInfo = parseEac3SyncFrame(pesPacket.data);
if (!frameInfo) {
throw new Error(
'Invalid E-AC-3 audio stream; could not read sync frame from first packet.',
);
}
const sampleRate = getEac3SampleRate(frameInfo);
if (sampleRate === null) {
throw new Error(
'Invalid E-AC-3 audio stream; reserved sample rate code found in first packet.',
);
}
elementaryStream.info.numberOfChannels = getEac3ChannelCount(frameInfo);
elementaryStream.info.sampleRate = sampleRate;
elementaryStream.initialized = true;
} else {
throw new Error('Unhandled.');
@@ -1686,6 +1786,91 @@ const markNextPacket = async (context: PacketReadingContext) => {
} else {
context.seekTo(possibleHeaderStartPos + 1);
}
} else if (codec === 'ac3') {
if (byte !== 0x0b) {
continue;
}
context.skip(-1);
const possibleSyncPos = context.currentPos;
// Need at least 5 bytes for sync word + CRC + fscod/frmsizecod
let remaining = context.ensureBuffered(5);
if (remaining instanceof Promise) remaining = await remaining;
if (remaining < 5) {
return;
}
const headerBytes = context.readBytes(5);
// Verify sync word (0x0B77)
if (headerBytes[0] !== 0x0b || headerBytes[1] !== 0x77) {
context.seekTo(possibleSyncPos + 1);
continue;
}
const fscod = headerBytes[4]! >> 6;
const frmsizecod = headerBytes[4]! & 0x3f;
if (fscod === 3 || frmsizecod > 37) {
// Invalid
context.seekTo(possibleSyncPos + 1);
continue;
}
const frameSize = AC3_FRAME_SIZES[3 * frmsizecod + fscod];
assert(frameSize !== undefined);
context.seekTo(possibleSyncPos);
remaining = context.ensureBuffered(frameSize);
if (remaining instanceof Promise) remaining = await remaining;
const duration = Math.round(
AC3_SAMPLES_PER_FRAME * TIMESCALE / elementaryStream.info.sampleRate,
);
return context.supplyPacket(remaining, duration);
} else if (codec === 'eac3') {
if (byte !== 0x0b) {
continue;
}
context.skip(-1);
const possibleSyncPos = context.currentPos;
// Need at least 5 bytes for E-AC-3 header parsing (sync word + frmsiz + fscod/numblkscod)
let remaining = context.ensureBuffered(5);
if (remaining instanceof Promise) remaining = await remaining;
if (remaining < 5) {
return;
}
const headerBytes = context.readBytes(5);
if (headerBytes[0] !== 0x0b || headerBytes[1] !== 0x77) {
context.seekTo(possibleSyncPos + 1);
continue;
}
const frmsiz = ((headerBytes[2]! & 0x07) << 8) | headerBytes[3]!;
const frameSize = (frmsiz + 1) * 2;
const fscod = headerBytes[4]! >> 6;
const numblkscod = fscod === 3 ? 3 : (headerBytes[4]! >> 4) & 0x03;
const numblks = EAC3_NUMBLKS_TABLE[numblkscod]!;
context.seekTo(possibleSyncPos);
remaining = context.ensureBuffered(frameSize);
if (remaining instanceof Promise) remaining = await remaining;
// Duration = numblks * 256 samples per block
const samplesPerFrame = numblks * 256;
const duration = Math.round(
samplesPerFrame * TIMESCALE / elementaryStream.info.sampleRate,
);
return context.supplyPacket(remaining, duration);
} else {
throw new Error('Unhandled.');
}
+3
View File
@@ -13,6 +13,9 @@ export const enum MpegTsStreamType {
MP3_MPEG1 = 0x03,
MP3_MPEG2 = 0x04,
AAC = 0x0f,
AC3_SYSTEM_A = 0x81,
EAC3_SYSTEM_A = 0x87,
PRIVATE_DATA = 0x06,
AVC = 0x1b,
HEVC = 0x24,
}
+56 -7
View File
@@ -9,11 +9,13 @@
import { parseAacAudioSpecificConfig, validateAudioChunkMetadata, validateVideoChunkMetadata } from '../codec';
import { buildAdtsHeaderTemplate, writeAdtsFrameLength } from '../adts/adts-misc';
import {
AC3_REGISTRATION_DESCRIPTOR,
AvcDecoderConfigurationRecord,
AvcNalUnitType,
concatNalUnitsInAnnexB,
deserializeAvcDecoderConfigurationRecord,
deserializeHevcDecoderConfigurationRecord,
EAC3_REGISTRATION_DESCRIPTOR,
extractNalUnitTypeForAvc,
extractNalUnitTypeForHevc,
HevcDecoderConfigurationRecord,
@@ -151,11 +153,34 @@ export class MpegTsMuxer extends Muxer {
assert(meta?.decoderConfig);
const codec = track.source._codec;
assert(codec === 'aac' || codec === 'mp3');
assert(codec === 'aac' || codec === 'mp3' || codec === 'ac3' || codec === 'eac3');
let streamType: MpegTsStreamType;
let streamId: number;
switch (codec) {
case 'aac': {
streamType = MpegTsStreamType.AAC;
streamId = AUDIO_STREAM_ID_BASE + this.audioTrackIndex++;
}; break;
case 'mp3': {
streamType = MpegTsStreamType.MP3_MPEG1;
streamId = AUDIO_STREAM_ID_BASE + this.audioTrackIndex++;
}; break;
case 'ac3': {
streamType = MpegTsStreamType.AC3_SYSTEM_A;
streamId = 0xbd;
}; break;
case 'eac3': {
streamType = MpegTsStreamType.EAC3_SYSTEM_A;
streamId = 0xbd;
}; break;
}
const streamType = codec === 'aac' ? MpegTsStreamType.AAC : MpegTsStreamType.MP3_MPEG1;
const pid = FIRST_TRACK_PID + this.trackDatas.length;
const streamId = AUDIO_STREAM_ID_BASE + this.audioTrackIndex++;
const newTrackData: MpegTsTrackData = {
track,
@@ -373,7 +398,7 @@ export class MpegTsMuxer extends Muxer {
): Uint8Array {
const codec = (trackData.track as OutputAudioTrack).source._codec;
if (codec === 'mp3') {
if (codec === 'mp3' || codec === 'ac3' || codec === 'eac3') {
// We're good
return packet.data;
}
@@ -695,7 +720,18 @@ const PAT_SECTION = new Uint8Array(16);
}
const buildPmt = (trackDatas: MpegTsTrackData[]) => {
const sectionLength = 9 + trackDatas.length * 5 + 4;
let totalEsBytes = 0;
for (const trackData of trackDatas) {
totalEsBytes += 5;
if (trackData.streamType === MpegTsStreamType.AC3_SYSTEM_A) {
totalEsBytes += AC3_REGISTRATION_DESCRIPTOR.length;
} else if (trackData.streamType === MpegTsStreamType.EAC3_SYSTEM_A) {
totalEsBytes += EAC3_REGISTRATION_DESCRIPTOR.length;
}
}
const sectionLength = 9 + totalEsBytes + 4;
const section = new Uint8Array(3 + sectionLength - 4);
const view = toDataView(section);
@@ -714,8 +750,21 @@ const buildPmt = (trackDatas: MpegTsTrackData[]) => {
section[offset++] = trackData.streamType; // stream_type
view.setUint16(offset, 0xE000 | (trackData.pid & 0x1FFF), false); // reserved=111, elementary_PID
offset += 2;
view.setUint16(offset, 0xF000, false); // reserved=1111, ES_info_length=0
offset += 2;
if (trackData.streamType === MpegTsStreamType.AC3_SYSTEM_A) {
view.setUint16(offset, 0xF000 | AC3_REGISTRATION_DESCRIPTOR.length, false);
offset += 2;
section.set(AC3_REGISTRATION_DESCRIPTOR, offset);
offset += AC3_REGISTRATION_DESCRIPTOR.length;
} else if (trackData.streamType === MpegTsStreamType.EAC3_SYSTEM_A) {
view.setUint16(offset, 0xF000 | EAC3_REGISTRATION_DESCRIPTOR.length, false);
offset += 2;
section.set(EAC3_REGISTRATION_DESCRIPTOR, offset);
offset += EAC3_REGISTRATION_DESCRIPTOR.length;
} else {
view.setUint16(offset, 0xF000, false); // reserved=1111, ES_info_length=0
offset += 2;
}
}
const crc = computeMpegTsCrc32(section);
+43 -2
View File
@@ -74,6 +74,13 @@ export abstract class OutputFormat {
abstract getSupportedTrackCounts(): TrackCountLimits;
/** Whether this output format supports video rotation metadata. */
abstract get supportsVideoRotationMetadata(): boolean;
/**
* Whether this output format's tracks store timestamped media data. When `true`, the timestamps of added packets
* will be respected, allowing things like gaps in media data or non-zero start times. When `false`, the format's
* media data implicitly starts at zero and follows an implicit sequential timing from there, using the intrinsic
* durations of the media data.
*/
abstract get supportsTimestampedMediaData(): boolean;
/** Returns a list of video codecs that this output format can contain. */
getSupportedVideoCodecs() {
@@ -258,6 +265,10 @@ export abstract class IsobmffOutputFormat extends OutputFormat {
return true;
}
get supportsTimestampedMediaData() {
return true;
}
/** @internal */
_createMuxer(output: Output) {
return new IsobmffMuxer(output, this);
@@ -292,7 +303,8 @@ export class Mp4OutputFormat extends IsobmffOutputFormat {
return [
...VIDEO_CODECS,
...NON_PCM_AUDIO_CODECS,
// These are supported via ISO/IEC 23003-5
// These are supported via ISO/IEC 23003-5:
'pcm-s16',
'pcm-s16be',
'pcm-s24',
@@ -303,6 +315,7 @@ export class Mp4OutputFormat extends IsobmffOutputFormat {
'pcm-f32be',
'pcm-f64',
'pcm-f64be',
...SUBTITLE_CODECS,
];
}
@@ -488,6 +501,10 @@ export class MkvOutputFormat extends OutputFormat {
// While it technically does support it with ProjectionPoseRoll, many players appear to ignore this value
return false;
}
get supportsTimestampedMediaData() {
return true;
}
}
/**
@@ -624,6 +641,10 @@ export class Mp3OutputFormat extends OutputFormat {
get supportsVideoRotationMetadata() {
return false;
}
get supportsTimestampedMediaData() {
return false;
}
}
/**
@@ -722,6 +743,10 @@ export class WavOutputFormat extends OutputFormat {
get supportsVideoRotationMetadata() {
return false;
}
get supportsTimestampedMediaData() {
return false;
}
}
/**
@@ -813,6 +838,10 @@ export class OggOutputFormat extends OutputFormat {
get supportsVideoRotationMetadata() {
return false;
}
get supportsTimestampedMediaData() {
return false;
}
}
/**
@@ -887,6 +916,10 @@ export class AdtsOutputFormat extends OutputFormat {
get supportsVideoRotationMetadata() {
return false;
}
get supportsTimestampedMediaData() {
return false;
}
}
/**
@@ -958,6 +991,10 @@ export class FlacOutputFormat extends OutputFormat {
get supportsVideoRotationMetadata() {
return false;
}
get supportsTimestampedMediaData() {
return false;
}
}
/**
@@ -1032,11 +1069,15 @@ export class MpegTsOutputFormat extends OutputFormat {
getSupportedCodecs(): MediaCodec[] {
return [
...VIDEO_CODECS.filter(codec => ['avc', 'hevc'].includes(codec)),
...AUDIO_CODECS.filter(codec => ['aac', 'mp3'].includes(codec)),
...AUDIO_CODECS.filter(codec => ['aac', 'mp3', 'ac3', 'eac3'].includes(codec)),
];
}
get supportsVideoRotationMetadata() {
return false;
}
get supportsTimestampedMediaData() {
return true;
}
}
+4 -1
View File
@@ -56,7 +56,10 @@ export class EncodedPacket {
/** Creates a new {@link EncodedPacket} from raw bytes and timing information. */
constructor(
/** The encoded data of this packet. */
/**
* The encoded data of this packet. For any given codec, this data must adhere to the format specified in the
* Mediabunny Codec Registry.
*/
public readonly data: Uint8Array,
/** The type of this packet. */
public readonly type: PacketType,
+4 -3
View File
@@ -425,9 +425,7 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
packetIndex: number,
options: PacketRetrievalOptions,
): Promise<EncodedPacket | null> {
if (packetIndex < 0) {
return null;
}
assert(packetIndex >= 0);
assert(this.demuxer.audioInfo);
const startOffset = packetIndex * PACKET_SIZE_IN_FRAMES * this.demuxer.audioInfo.blockSizeInBytes;
@@ -493,6 +491,9 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
timestamp * this.demuxer.audioInfo.sampleRate / PACKET_SIZE_IN_FRAMES,
(this.demuxer.dataSize - 1) / (PACKET_SIZE_IN_FRAMES * this.demuxer.audioInfo.blockSizeInBytes),
));
if (packetIndex < 0) {
return null;
}
const packet = await this.getPacketAtIndex(packetIndex, options);
if (packet) {
+1 -1
View File
@@ -16,7 +16,7 @@ test('MPEG-TS output format', async () => {
expect(format.mimeType).toBe('video/MP2T');
expect(format.fileExtension).toBe('.ts');
expect(format.supportsVideoRotationMetadata).toBe(false);
expect(format.getSupportedCodecs()).toEqual(['avc', 'hevc', 'aac', 'mp3']);
expect(format.getSupportedCodecs()).toEqual(['avc', 'hevc', 'aac', 'mp3', 'ac3', 'eac3']);
expect(format.getSupportedTrackCounts()).toEqual({
video: { min: 0, max: 16 },
audio: { min: 0, max: 32 },
+124
View File
@@ -0,0 +1,124 @@
import { expect, test } from 'vitest';
import { Input } from '../../src/input.js';
import { BufferSource, FilePathSource } from '../../src/source.js';
import path from 'node:path';
import { ALL_FORMATS } from '../../src/input-format.js';
import { Output } from '../../src/output.js';
import { MpegTsOutputFormat } from '../../src/output-format.js';
import { BufferTarget } from '../../src/target.js';
import { Conversion } from '../../src/conversion.js';
import { AC3_REGISTRATION_DESCRIPTOR, EAC3_REGISTRATION_DESCRIPTOR } from '../../src/codec-data.js';
const __dirname = new URL('.', import.meta.url).pathname;
test('reads AC-3 from MP4', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/ac3.mp4')),
formats: ALL_FORMATS,
});
const audioTrack = (await input.getPrimaryAudioTrack())!;
const decoderConfig = (await audioTrack.getDecoderConfig())!;
expect(audioTrack.codec).toBe('ac3');
expect(decoderConfig.description).toBeUndefined();
});
test('reads E-AC-3 from MP4', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/eac3.mp4')),
formats: ALL_FORMATS,
});
const audioTrack = (await input.getPrimaryAudioTrack())!;
const decoderConfig = (await audioTrack.getDecoderConfig())!;
expect(audioTrack.codec).toBe('eac3');
expect(decoderConfig.description).toBeUndefined();
});
test('reads and writes AC-3 in MPEG-TS', async () => {
using originalInput = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/ac3.ts')),
formats: ALL_FORMATS,
});
const originalAudioTrack = (await originalInput.getPrimaryAudioTrack())!;
expect(originalAudioTrack.codec).toBe('ac3');
expect(originalAudioTrack.internalCodecId).toBe(0x81);
const output = new Output({
format: new MpegTsOutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input: originalInput, output });
await conversion.execute();
using newInput = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const newAudioTrack = (await newInput.getPrimaryAudioTrack())!;
expect(newAudioTrack.codec).toBe('ac3');
expect(newAudioTrack.internalCodecId).toBe(0x81);
expect(newAudioTrack.numberOfChannels).toBe(originalAudioTrack.numberOfChannels);
expect(newAudioTrack.sampleRate).toBe(originalAudioTrack.sampleRate);
// Verify registration_descriptor is present
const buffer = new Uint8Array(output.target.buffer!);
let found = false;
for (let i = 0; i < buffer.length - AC3_REGISTRATION_DESCRIPTOR.length; i++) {
if (AC3_REGISTRATION_DESCRIPTOR.every((byte, j) => buffer[i + j] === byte)) {
found = true;
break;
}
}
expect(found).toBe(true);
});
test('reads and writes E-AC-3 in MPEG-TS', async () => {
using originalInput = new Input({
source: new FilePathSource(path.join(__dirname, '..', 'public/eac3.ts')),
formats: ALL_FORMATS,
});
const originalAudioTrack = (await originalInput.getPrimaryAudioTrack())!;
expect(originalAudioTrack.codec).toBe('eac3');
expect(originalAudioTrack.internalCodecId).toBe(0x87);
const output = new Output({
format: new MpegTsOutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input: originalInput, output });
await conversion.execute();
using newInput = new Input({
source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS,
});
const newAudioTrack = (await newInput.getPrimaryAudioTrack())!;
expect(newAudioTrack.codec).toBe('eac3');
expect(newAudioTrack.internalCodecId).toBe(0x87);
expect(newAudioTrack.numberOfChannels).toBe(originalAudioTrack.numberOfChannels);
expect(newAudioTrack.sampleRate).toBe(originalAudioTrack.sampleRate);
// Verify registration_descriptor is present
const buffer = new Uint8Array(output.target.buffer!);
let found = false;
for (let i = 0; i < buffer.length - EAC3_REGISTRATION_DESCRIPTOR.length; i++) {
if (EAC3_REGISTRATION_DESCRIPTOR.every((byte, j) => buffer[i + j] === byte)) {
found = true;
break;
}
}
expect(found).toBe(true);
});
+141
View File
@@ -9,6 +9,7 @@ import { assert } from '../../src/misc.js';
import { EncodedPacketSink } from '../../src/media-sink.js';
import { EncodedPacket } from '../../src/packet.js';
import { MpegTsDemuxer } from '../../src/mpeg-ts/mpeg-ts-demuxer.js';
import { MpegTsStreamType } from '../../src/mpeg-ts/mpeg-ts-misc.js';
const __dirname = new URL('.', import.meta.url).pathname;
@@ -612,6 +613,146 @@ test('MPEG-TS with MP3 audio', async () => {
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS with AC-3 audio (System A)', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/ac3.ts')),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
expect(audioTrack.codec).toBe('ac3');
expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.AC3_SYSTEM_A);
const audioDecoderConfig = await audioTrack.getDecoderConfig();
expect(audioDecoderConfig).toEqual({
codec: 'ac-3',
numberOfChannels: 6,
sampleRate: 48000,
});
const sink = new EncodedPacketSink(audioTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
let count = 0;
for await (const packet of sink.packets()) {
expect(packet.data[0]).toBe(0x0b);
expect(packet.data[1]).toBe(0x77);
expect(packet.type).toBe('key');
count++;
}
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS with AC-3 audio (System B)', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/ac3-system-b.ts')),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
expect(audioTrack.codec).toBe('ac3');
expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.PRIVATE_DATA);
const audioDecoderConfig = await audioTrack.getDecoderConfig();
expect(audioDecoderConfig).toEqual({
codec: 'ac-3',
numberOfChannels: 6,
sampleRate: 48000,
});
const sink = new EncodedPacketSink(audioTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
let count = 0;
for await (const packet of sink.packets()) {
expect(packet.data[0]).toBe(0x0b);
expect(packet.data[1]).toBe(0x77);
expect(packet.type).toBe('key');
count++;
}
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS with E-AC-3 audio (System A)', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/eac3.ts')),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
expect(audioTrack.codec).toBe('eac3');
expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.EAC3_SYSTEM_A);
const audioDecoderConfig = await audioTrack.getDecoderConfig();
expect(audioDecoderConfig).toEqual({
codec: 'ec-3',
numberOfChannels: 6,
sampleRate: 48000,
});
const sink = new EncodedPacketSink(audioTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
let count = 0;
for await (const packet of sink.packets()) {
expect(packet.data[0]).toBe(0x0b);
expect(packet.data[1]).toBe(0x77);
expect(packet.type).toBe('key');
count++;
}
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS with E-AC-3 audio (System B)', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/eac3-system-b.ts')),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
expect(audioTrack.codec).toBe('eac3');
expect(audioTrack.internalCodecId).toBe(MpegTsStreamType.PRIVATE_DATA);
const audioDecoderConfig = await audioTrack.getDecoderConfig();
expect(audioDecoderConfig).toEqual({
codec: 'ec-3',
numberOfChannels: 6,
sampleRate: 48000,
});
const sink = new EncodedPacketSink(audioTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
let count = 0;
for await (const packet of sink.packets()) {
expect(packet.data[0]).toBe(0x0b);
expect(packet.data[1]).toBe(0x77);
expect(packet.type).toBe('key');
count++;
}
expect(count).toBeGreaterThan(0);
});
test('MPEG-TS partial reading', async () => {
const fullPath = path.join(__dirname, '../public/193039199_mp4_h264_aac_fhd_7.ts');
const buffer = await fs.promises.readFile(fullPath);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.