Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3906a3e36 | ||
|
|
46bd70b880 | ||
|
|
31b278fbd4 | ||
|
|
d3b737b183 | ||
|
|
a97200f3f0 | ||
|
|
e3aa7108f5 | ||
|
|
b717cbf68c | ||
|
|
7cc26edacb | ||
|
|
de9a4bbc23 | ||
|
|
4fae45ec40 | ||
|
|
417c85d8ed | ||
|
|
612642c5ad | ||
|
|
2298f4a8b3 | ||
|
|
a613a870c8 | ||
|
|
b61b2b326f | ||
|
|
cacbc2a399 | ||
|
|
ab0fd6a8d8 | ||
|
|
6a099b4259 | ||
|
|
172b4a1fd2 | ||
|
|
43c1ea7efd | ||
|
|
cab92c55ec | ||
|
|
81bc9d7c44 | ||
|
|
3209363470 | ||
|
|
d16b1927ad | ||
|
|
f620a328e5 | ||
|
|
7aa0fa465e | ||
|
|
cc5f7d8e8f | ||
|
|
7e1c93c331 | ||
|
|
5b88332cb8 | ||
|
|
9a70ccbacd | ||
|
|
8c67872e83 | ||
|
|
7f4093fd43 | ||
|
|
b63db955f6 | ||
|
|
92454b28f7 | ||
|
|
db3cb143fe | ||
|
|
e20f779ede | ||
|
|
e1e32c9505 | ||
|
|
79f19b6717 | ||
|
|
98387258a4 | ||
|
|
d859b39468 | ||
|
|
2afbec4e08 | ||
|
|
ab3a514884 | ||
|
|
277290abe3 | ||
|
|
dbaaea255c | ||
|
|
d23b0369f3 | ||
|
|
6c78a0ff09 | ||
|
|
e7b3437b4b | ||
|
|
c3a3eb6abf | ||
|
|
46fbfc8a2c | ||
|
|
3113e449d4 | ||
|
|
58053ed00c | ||
|
|
20e4610da4 | ||
|
|
53b04619e1 | ||
|
|
2695921ed4 | ||
|
|
7e8f9fbbb1 | ||
|
|
41e012f1e8 | ||
|
|
282751ec1c | ||
|
|
e8b27768a9 | ||
|
|
ae37973878 | ||
|
|
e3ee9d91a9 | ||
|
|
c0f2693cb3 | ||
|
|
d8d6ccc1f7 | ||
|
|
dc25b5d7c5 | ||
|
|
54dfa75a7e | ||
|
|
8fea9671ea | ||
|
|
0719a17c85 | ||
|
|
4a85aad012 | ||
|
|
301a6adb1d | ||
|
|
045df3b9a5 | ||
|
|
06ab9c8433 | ||
|
|
888441232d | ||
|
|
868578685d | ||
|
|
e1478f8ebd | ||
|
|
8aac07eb61 | ||
|
|
267c09abf8 | ||
|
|
a89101ff40 | ||
|
|
2ab1be3834 | ||
|
|
571fbb3198 | ||
|
|
32d8b1544d | ||
|
|
b8f104d8b4 | ||
|
|
7db0583058 | ||
|
|
24af125415 | ||
|
|
78e78e5b4f | ||
|
|
1d774b5fbb | ||
|
|
d6b518b9fd | ||
|
|
4f5f30bc03 | ||
|
|
f6ca3ece50 | ||
|
|
7b036fe8ac | ||
|
|
2bb84fd35b | ||
|
|
91e7812fb2 | ||
|
|
d29f4f33b5 | ||
|
|
7580935639 | ||
|
|
53824536fa | ||
|
|
f363fe50e2 | ||
|
|
8f43086e3d | ||
|
|
9343607fe1 | ||
|
|
f877fabc60 | ||
|
|
a2115b1de7 | ||
|
|
8d46d50c39 | ||
|
|
19219fc1ce | ||
|
|
05070f7c88 | ||
|
|
736cc50fa7 | ||
|
|
9ba63f5d95 | ||
|
|
17c6bff1dc | ||
|
|
c86af052f5 | ||
|
|
831e838f74 | ||
|
|
b730a24138 | ||
|
|
ccbf14565c | ||
|
|
15bdd072e1 | ||
|
|
4dd747a0a6 | ||
|
|
228721d68f | ||
|
|
ea9ba96ae3 | ||
|
|
f9207396d7 | ||
|
|
84dec01dfd | ||
|
|
1ff8abdaab | ||
|
|
cc649ba192 | ||
|
|
b73bf06ba7 |
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: Upload build artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release upload ${{ github.event.release.tag_name }} dist/bundles/mediabunny.cjs dist/bundles/mediabunny.min.cjs dist/bundles/mediabunny.mjs dist/bundles/mediabunny.min.mjs dist/mediabunny.d.ts packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.mjs packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.mjs packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
|
||||
run: gh release upload ${{ github.event.release.tag_name }} dist/bundles/mediabunny.cjs dist/bundles/mediabunny.min.cjs dist/bundles/mediabunny.mjs dist/bundles/mediabunny.min.mjs dist/mediabunny.d.ts packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.mjs packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.mjs packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts packages/ac3/dist/bundles/mediabunny-ac3.js packages/ac3/dist/bundles/mediabunny-ac3.min.js packages/ac3/dist/bundles/mediabunny-ac3.mjs packages/ac3/dist/bundles/mediabunny-ac3.min.mjs packages/ac3/dist/mediabunny-ac3.d.ts
|
||||
|
||||
- name: Publish Mediabunny to npm
|
||||
run: npm publish --access public
|
||||
|
||||
@@ -6,4 +6,5 @@ node_modules
|
||||
/docs/api
|
||||
*.tsbuildinfo
|
||||
|
||||
packages/mp3-encoder/dist
|
||||
packages/mp3-encoder/dist
|
||||
packages/ac3/dist
|
||||
@@ -0,0 +1,6 @@
|
||||
- Prefer functions declared using const, not using the function keyword
|
||||
- Code style is tab indent with semicolons
|
||||
- Mediabunny core code is contained in src/, extensions are in packages/*/, website is in docs/
|
||||
- Tests: Prefer fewer, longer test files over many small ones. Test files should be named after the general catergory of thing that is being tested, not after any individual single test.
|
||||
- Avoid ifs without a {} block. So no if (cond) return;, always do if (cond) { return; }
|
||||
- `type` instead of `interface` for object types
|
||||
@@ -17,7 +17,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
|
||||
|
||||
<div align="center">
|
||||
<a href="https://remotion.dev/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/remotion.png" width="60" height="60" alt="Remotion">
|
||||
<picture>
|
||||
<source srcset="./docs/public/sponsors/remotion-dark.png" media="(prefers-color-scheme: dark)">
|
||||
<img src="./docs/public/sponsors/remotion-light.png" width="60" height="60" alt="Remotion">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<a href="https://www.gling.ai/" target="_blank" rel="sponsored">
|
||||
@@ -48,6 +51,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
|
||||
### Bronze sponsors
|
||||
|
||||
<div align="center">
|
||||
<a href="https://elevenlabs.io/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/elevenlabs.png" width="40" height="40" alt="ElevenLabs">
|
||||
</a>
|
||||
|
||||
<a href="https://www.reactvideoeditor.com/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/rve.png" width="40" height="40" alt="React Video Editor">
|
||||
</a>
|
||||
@@ -67,7 +74,7 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
|
||||
|
||||
Core features include:
|
||||
|
||||
- **Wide format support**: Read and write MP4, MOV, WebM, MKV, WAVE, MP3, Ogg, ADTS, FLAC
|
||||
- **Wide format support**: Read and write MP4, MOV, WebM, MKV, WAVE, MP3, Ogg, ADTS, FLAC, MPEG-TS
|
||||
- **Built-in encoding & decoding**: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API
|
||||
- **High precision**: Fine-grained, microsecond-accurate reading and writing operations
|
||||
- **Conversion API**: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, cropping, resampling, trimming, and more
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||
<script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script>
|
||||
<script src="../packages/ac3/dist/bundles/mediabunny-ac3.js"></script>
|
||||
|
||||
<script type="module">
|
||||
//MediabunnyMp3Encoder.registerMp3Encoder();
|
||||
MediabunnyAc3.registerAc3Encoder();
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
@@ -56,7 +58,8 @@
|
||||
}),
|
||||
output,
|
||||
audio: (_, n) => ({
|
||||
discard: n > 1,
|
||||
codec: 'eac3',
|
||||
//discard: n > 1,
|
||||
//codec: 'opus',
|
||||
//codec: 'opus',
|
||||
/*
|
||||
@@ -100,9 +103,12 @@
|
||||
},
|
||||
*/
|
||||
video: () => ({
|
||||
width: 720,
|
||||
frameRate: 30,
|
||||
bitrate: Mediabunny.QUALITY_VERY_LOW,
|
||||
width: 320,
|
||||
//forceTranscode: true,
|
||||
//allowRotationMetadata: false,
|
||||
//width: 720,
|
||||
//frameRate: 30,
|
||||
//bitrate: Mediabunny.QUALITY_VERY_LOW,
|
||||
//discard: true,
|
||||
/*
|
||||
process: (sample) => {
|
||||
@@ -179,8 +185,8 @@
|
||||
}
|
||||
},
|
||||
trim: {
|
||||
start: 0,
|
||||
end: 20
|
||||
//start: 0,
|
||||
end: 10
|
||||
},
|
||||
});
|
||||
console.log(conversion);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||
<script src="../packages/ac3/dist/bundles/mediabunny-ac3.js"></script>
|
||||
|
||||
<script type="module">
|
||||
MediabunnyAc3.registerAc3Decoder();
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
document.body.append(fileInput);
|
||||
@@ -14,13 +17,46 @@
|
||||
source: new Mediabunny.BlobSource(file),
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
const sink = new Mediabunny.AudioSampleSink(track);
|
||||
|
||||
for await (const sample of sink.samples()) {
|
||||
sample.close();
|
||||
}
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet);
|
||||
console.log(i);
|
||||
}
|
||||
|
||||
console.log("Done")
|
||||
|
||||
/*
|
||||
|
||||
//const secondPacket = await sink.getNextPacket(firstPacket);
|
||||
//const thirdPacket = await sink.getNextPacket(secondPacket);
|
||||
//const fourthPacket = await sink.getNextPacket(thirdPacket);
|
||||
//const fifthPacket = await sink.getNextPacket(fourthPacket);
|
||||
//const sixthPacket = await sink.getNextPacket(fifthPacket);
|
||||
//const seventhPacket = await sink.getNextPacket(sixthPacket);
|
||||
//const eighthPacket = await sink.getNextPacket(seventhPacket);
|
||||
//const ninthPacket = await sink.getNextPacket(eighthPacket);
|
||||
//const tenthPacket = await sink.getNextPacket(ninthPacket);
|
||||
|
||||
console.log(firstPacket, secondPacket, thirdPacket, fourthPacket, fifthPacket, sixthPacket, seventhPacket, eighthPacket, ninthPacket, tenthPacket);
|
||||
|
||||
const packets = [firstPacket, secondPacket, thirdPacket, fourthPacket, fifthPacket, sixthPacket, seventhPacket, eighthPacket, ninthPacket, tenthPacket];
|
||||
|
||||
const videoDecoder = new VideoDecoder({
|
||||
output: (frame) => console.log(frame.timestamp, frame),
|
||||
error: console.error,
|
||||
});
|
||||
videoDecoder.configure(await videoTrack.getDecoderConfig());
|
||||
|
||||
for (const packet of packets) {
|
||||
//videoDecoder.decode(packet.toEncodedVideoChunk());
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
|
||||
@@ -25,29 +25,41 @@
|
||||
target: new Mediabunny.BufferTarget(),
|
||||
format: new Mediabunny.Mp4OutputFormat(),
|
||||
});
|
||||
let videoSource = null;
|
||||
let audioSource = null;
|
||||
if (videoTrack) {
|
||||
const source = new Mediabunny.MediaStreamVideoTrackSource(videoTrack, {
|
||||
videoSource = new Mediabunny.MediaStreamVideoTrackSource(videoTrack, {
|
||||
codec: 'avc',
|
||||
bitrate: Mediabunny.QUALITY_MEDIUM
|
||||
});
|
||||
|
||||
source.errorPromise.catch((d) => console.log("Hello?????", d));
|
||||
videoSource.errorPromise.catch((d) => console.log("Hello?????", d));
|
||||
|
||||
output.addVideoTrack(source);
|
||||
output.addVideoTrack(videoSource);
|
||||
}
|
||||
if (audioTrack) {
|
||||
const source = new Mediabunny.MediaStreamAudioTrackSource(audioTrack, {
|
||||
audioSource = new Mediabunny.MediaStreamAudioTrackSource(audioTrack, {
|
||||
codec: 'mp3',
|
||||
bitrate: Mediabunny.QUALITY_MEDIUM
|
||||
});
|
||||
|
||||
source.errorPromise.catch((d) => console.log("Hello!!???", d));
|
||||
audioSource.errorPromise.catch((d) => console.log("Hello!!???", d));
|
||||
|
||||
output.addAudioTrack(source);
|
||||
output.addAudioTrack(audioSource);
|
||||
}
|
||||
|
||||
await output.start();
|
||||
|
||||
setTimeout(() => {
|
||||
videoSource?.pause();
|
||||
audioSource?.pause();
|
||||
|
||||
setTimeout(() => {
|
||||
videoSource?.resume();
|
||||
audioSource?.resume();
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
await output.finalize();
|
||||
|
||||
@@ -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: {
|
||||
@@ -87,11 +93,47 @@ export default withMermaid({
|
||||
text: 'Extensions',
|
||||
items: [
|
||||
{ text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' },
|
||||
{ text: 'ac3', link: '/guide/extensions/ac3' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// 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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
'/api': apiRoutes,
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
@@ -111,7 +153,7 @@ export default withMermaid({
|
||||
|
||||
footer: {
|
||||
message: 'Released under the Mozilla Public License 2.0.',
|
||||
copyright: 'Copyright © 2025-present Vanilagy',
|
||||
copyright: 'Copyright © 2026-present Vanilagy',
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
"Custom coders": "API for adding custom encoders and decoders.",
|
||||
"Miscellaneous": "Whatever's left.",
|
||||
|
||||
"@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny."
|
||||
"@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny.",
|
||||
"@mediabunny/ac3": "Adds AC-3/E-AC-3 decoder and encoder support to Mediabunny."
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 141 KiB |
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -108,7 +108,7 @@ Sometimes, you may want to cancel an ongoing conversion process. For this, use t
|
||||
await conversion.cancel(); // Resolves once the conversion is canceled
|
||||
```
|
||||
|
||||
This automatically frees up all resources used by the conversion process.
|
||||
This automatically frees up all resources used by the conversion process and will cause any ongoing call to `execute` to throw a `ConversionCanceledError`.
|
||||
|
||||
## Video options
|
||||
|
||||
@@ -120,11 +120,13 @@ type ConversionVideoOptions = {
|
||||
height?: number;
|
||||
fit?: 'fill' | 'contain' | 'cover';
|
||||
rotate?: 0 | 90 | 180 | 270;
|
||||
allowRotationMetadata?: boolean;
|
||||
crop?: { left: number; top: number; width: number; height: number };
|
||||
frameRate?: number;
|
||||
codec?: VideoCodec;
|
||||
bitrate?: number | Quality;
|
||||
alpha?: 'discard' | 'keep'; // Defaults to 'discard'
|
||||
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
|
||||
keyFrameInterval?: number;
|
||||
forceTranscode?: boolean;
|
||||
process?: (sample: VideoSample) => MaybePromise<
|
||||
@@ -175,6 +177,8 @@ In the rare case that the input video changes size over time, the `fit` field ca
|
||||
|
||||
`rotation` rotates the video by the specified number of degrees clockwise. This rotation is applied on top of any rotation metadata in the original input file and happens before cropping and resizing.
|
||||
|
||||
By default, Mediabunny will try to make use of rotation metadata in the output file to perform the rotation whenever possible. However, if you don't want this to happen, or you want to use Mediabunny to strip all rotation metadata from a file, you can set `allowRotationMetadata` to `false`.
|
||||
|
||||
### Cropping video
|
||||
|
||||
`crop` can be used to extract a rectangular region from the original video. The rectangle is specified using `left`, `top`, `width` and `height` and is clamped to the dimensions of the video. Cropping is applied after rotation but before resizing.
|
||||
@@ -190,9 +194,10 @@ Use the `codec` property to control the codec of the output track. This should b
|
||||
Use the `bitrate` property to control the bitrate of the output video. For example, you can use this field to compress the video track. Accepted values are the number of bits per second or a [subjective quality](./media-sources#subjective-qualities). If this property is set, transcoding will always happen. If this property is not set but transcoding is still required, `QUALITY_HIGH` will be used as the value.
|
||||
|
||||
Use the `keyFrameInterval` property to control the maximum interval in seconds between key frames in the output video. Setting this fields forces a transcode.
|
||||
|
||||
If you want to prevent direct copying of media data and force a transcoding step, use `forceTranscode: true`.
|
||||
|
||||
Use the `hardwareAcceleration` property to control whether hardware or software acceleration is used for video transcoding.
|
||||
|
||||
### Processing video
|
||||
|
||||
The `process` property can be used to define a custom video sample processing function, e.g. for [applying overlays](./quick-start#add-a-video-overlay), color transformations, or timestamp modifications. You are expected to perform this processing yourself, for example using the Canvas API.
|
||||
@@ -301,8 +306,8 @@ const conversion = await Conversion.init({
|
||||
output,
|
||||
|
||||
// Function gets invoked for each video track:
|
||||
video: (videoTrack, n) => {
|
||||
if (n > 1) {
|
||||
video: (videoTrack) => {
|
||||
if (videoTrack.number > 1) {
|
||||
// Keep only the first video track
|
||||
return { discard: true };
|
||||
}
|
||||
@@ -314,7 +319,7 @@ const conversion = await Conversion.init({
|
||||
},
|
||||
|
||||
// Async functions work too:
|
||||
audio: async (audioTrack, n) => {
|
||||
audio: async (audioTrack) => {
|
||||
if (audioTrack.languageCode !== 'rus') {
|
||||
// Keep only Russian audio tracks
|
||||
return { discard: true };
|
||||
@@ -360,6 +365,21 @@ In this case, the output will be 15 seconds long.
|
||||
|
||||
If only `start` is set, the clip will run until the end of the input file. If only `end` is set, the clip will start at the beginning of the input file.
|
||||
|
||||
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:
|
||||
@@ -445,4 +465,4 @@ On the flip side, you can always query which input tracks made it into the outpu
|
||||
```ts
|
||||
const conversion = await Conversion.init({ input, output });
|
||||
conversion.utilizedTracks; // => InputTrack[]
|
||||
```
|
||||
```
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# @mediabunny/ac3
|
||||
|
||||
Browsers have no support for AC-3 (Dolby Digital) or E-AC-3 (Dolby Digital Plus) in their WebCodecs implementations. This extension package provides both a decoder and encoder for use with Mediabunny, allowing you to decode and encode these codecs directly in the browser. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [FFmpeg](https://ffmpeg.org/)'s AC-3 and E-AC-3 coders under the hood.
|
||||
|
||||
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="https://github.com/Vanilagy/mediabunny/blob/main/packages/ac3/README.md">
|
||||
GitHub page
|
||||
<span class="vpi-arrow-right" />
|
||||
</a>
|
||||
|
||||
## Installation
|
||||
|
||||
This library peer-depends on Mediabunny. Install both using npm:
|
||||
```bash
|
||||
npm install mediabunny @mediabunny/ac3
|
||||
```
|
||||
|
||||
Alternatively, directly include them using a script tag:
|
||||
```html
|
||||
<script src="mediabunny.js"></script>
|
||||
<script src="mediabunny-ac3.js"></script>
|
||||
```
|
||||
|
||||
This will expose the global objects `Mediabunny` and `MediabunnyAc3`. Use `mediabunny-ac3.d.ts` to provide types for these globals. You can download the built distribution files from the [releases page](https://github.com/Vanilagy/mediabunny/releases).
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { registerAc3Decoder, registerAc3Encoder } from '@mediabunny/ac3';
|
||||
|
||||
registerAc3Decoder();
|
||||
registerAc3Encoder();
|
||||
```
|
||||
That's it - Mediabunny now uses the registered AC-3/E-AC-3 decoder and encoder automatically.
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
OGG, // Ogg input format singleton
|
||||
ADTS, // ADTS input format singleton
|
||||
FLAC, // FLAC input format singleton
|
||||
MPEG_TS, // MPEG-TS input format singleton
|
||||
} from 'mediabunny';
|
||||
```
|
||||
|
||||
@@ -80,6 +81,7 @@ In addition to singletons, input format classes are structured hierarchically:
|
||||
- `OggInputFormat`
|
||||
- `AdtsInputFormat`
|
||||
- `FlacInputFormat`
|
||||
- `MpegTsInputFormat`
|
||||
|
||||
This means you can also perform input format checks using `instanceof` instead of `===` comparisons. For example:
|
||||
```ts
|
||||
|
||||
@@ -12,7 +12,7 @@ Here's a long list of stuff this library does:
|
||||
- Converting media files
|
||||
- Hardware-accelerated decoding & encoding (via the WebCodecs API)
|
||||
- Support for multiple video, audio and subtitle tracks
|
||||
- Read & write support for many container formats (.mp4, .mov, .webm, .mkv, .mp3, .wav, .ogg, .aac, .flac), including variations such as MP4 with Fast Start, fragmented MP4, streamable Matroska, transparent WebM, etc.
|
||||
- Read & write support for many container formats (.mp4, .mov, .webm, .mkv, .mp3, .wav, .ogg, .aac, .flac, .ts), including variations such as MP4 with Fast Start, fragmented MP4, streamable Matroska, transparent WebM, etc.
|
||||
- Support for 25 different codecs
|
||||
- Lazy, optimized, on-demand file reading
|
||||
- Input and output streaming, arbitrary file size support
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -187,6 +187,16 @@ videoTrackSource.errorPromise.catch((error) => ...);
|
||||
|
||||
This source requires no additional method calls; data will automatically be captured and piped to the output file as soon as `start()` is called on the `Output`. Make sure to `stop()` on `videoTrack` after finalizing the `Output` if you don't need the user's media anymore.
|
||||
|
||||
If you want to temporarily stop capturing video frames from this source, you can use the `pause()` and `resume()` methods:
|
||||
```ts
|
||||
videoTrackSource.pause();
|
||||
|
||||
// Later:
|
||||
videoTrackSource.resume();
|
||||
```
|
||||
|
||||
While paused, video frames emitted by the stream will be ignored. When resumed, video frames are let through again, offset in timestamp such that the result plays back continuously with no gap in playback. Note that pausing does *not* stop the underlying media stream.
|
||||
|
||||
::: info
|
||||
If this source is the only MediaStreamTrack source in the `Output`, then the first video sample added by it starts at timestamp 0. If there are multiple, then the earliest media sample across all tracks starts at timestamp 0, and all tracks will be perfectly synchronized with each other.
|
||||
:::
|
||||
@@ -230,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
|
||||
|
||||
@@ -344,6 +354,16 @@ audioTrackSource.errorPromise.catch((error) => ...);
|
||||
|
||||
This source requires no additional method calls; data will automatically be captured and piped to the output file as soon as `start()` is called on the `Output`. Make sure to `stop()` on `audioTrack` after finalizing the `Output` if you don't need the user's media anymore.
|
||||
|
||||
If you want to temporarily stop capturing audio data from this source, you can use the `pause()` and `resume()` methods:
|
||||
```ts
|
||||
audioTrackSource.pause();
|
||||
|
||||
// Later:
|
||||
audioTrackSource.resume();
|
||||
```
|
||||
|
||||
While paused, audio data emitted by the stream will be ignored. When resumed, audio data are let through again, offset in timestamp such that the result plays back continuously with no gap in playback. Note that pausing does *not* stop the underlying media stream.
|
||||
|
||||
::: info
|
||||
If this source is the only MediaStreamTrack source in the `Output`, then the first audio sample added by it starts at timestamp 0. If there are multiple, then the earliest media sample across all tracks starts at timestamp 0, and all tracks will be perfectly synchronized with each other.
|
||||
:::
|
||||
@@ -377,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
|
||||
|
||||
|
||||
@@ -194,9 +194,12 @@ This format ensures [append-only writing](#append-only-writing).
|
||||
The following options are available:
|
||||
```ts
|
||||
type OggOutputFormatOptions = {
|
||||
maximumPageDuration?: number;
|
||||
onPage?: (data: Uint8Array, position: number, source: MediaSource) => unknown;
|
||||
};
|
||||
```
|
||||
- `maximumPageDuration`\
|
||||
The maximum duration in seconds of each Ogg page. Pages will be flushed early if adding another packet would cause the page to exceed this duration. This is useful for streaming contexts where more frequent page output is desired. By default, pages are only flushed when they exceed a certain size.
|
||||
- `onPage`\
|
||||
Will be called for each finalized Ogg page of the output file. The [media source](./media-sources) backing the page's track (logical bitstream) is also passed.
|
||||
|
||||
@@ -272,6 +275,10 @@ const output = new Output({
|
||||
});
|
||||
```
|
||||
|
||||
::: info
|
||||
This format ensures [append-only writing](#append-only-writing).
|
||||
:::
|
||||
|
||||
The following options are available:
|
||||
```ts
|
||||
type AdtsOutputFormatOptions = {
|
||||
@@ -285,7 +292,7 @@ type AdtsOutputFormatOptions = {
|
||||
|
||||
This output format creates FLAC (.flac) files.
|
||||
```ts
|
||||
import { Output, FlacOutputFormat } from 'mediabunny';
|
||||
import { Output, FlacOutputFormat } from 'mediabunny';
|
||||
|
||||
const output = new Output({
|
||||
format: new FlacOutputFormat(options),
|
||||
@@ -300,4 +307,29 @@ type FlacOutputFormatOptions = {
|
||||
};
|
||||
```
|
||||
- `onFrame`\
|
||||
Will be called for each FLAC frame that is written.
|
||||
Will be called for each FLAC frame that is written.
|
||||
|
||||
## MPEG-TS
|
||||
|
||||
This output format creates MPEG Transport Stream (.ts) files.
|
||||
```ts
|
||||
import { Output, MpegTsOutputFormat } from 'mediabunny';
|
||||
|
||||
const output = new Output({
|
||||
format: new MpegTsOutputFormat(options),
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
::: info
|
||||
This format ensures [append-only writing](#append-only-writing).
|
||||
:::
|
||||
|
||||
The following options are available:
|
||||
```ts
|
||||
type MpegTsOutputFormatOptions = {
|
||||
onPacket?: (data: Uint8Array, position: number) => unknown;
|
||||
};
|
||||
```
|
||||
- `onPacket`\
|
||||
Will be called for each 188-byte Transport Stream packet that is written.
|
||||
@@ -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.
|
||||
:::
|
||||
@@ -176,7 +178,7 @@ Negative sequence numbers mean the packet's ordering is undefined. When creating
|
||||
|
||||
### Cloning packets
|
||||
|
||||
Use the `clone` method to create a new packet from an existing packet. While doing so, you can change its timestamp and duration.
|
||||
Use the `clone` method to create a new packet from an existing packet. While doing so, you can partially change its data.
|
||||
```ts
|
||||
// Creates a clone identical to the original:
|
||||
packet.clone();
|
||||
@@ -378,13 +380,11 @@ const bytesNeeded = videoSample.allocationSize(); // => number
|
||||
Then, use `copyTo` to copy the pixel data into the destination buffer:
|
||||
```ts
|
||||
const bytes = new Uint8Array(bytesNeeded);
|
||||
videoSample.copyTo(bytes);
|
||||
const planeLayout = await videoSample.copyTo(bytes);
|
||||
```
|
||||
|
||||
::: info
|
||||
The data will always be in the pixel format specified in the `format` field.
|
||||
|
||||
To convert the data into a different pixel format, or to extract only a section of the frame, please use the [`allocationSize`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame/allocationSize) and [`copyTo`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame/copyTo) methods on `VideoFrame` instead. Get a `VideoFrame` by running `videoSample.toVideoFrame()`.
|
||||
You can pass additional options to `allocationSize` and `copyTo` to extract data in a different pixel format.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
@@ -556,14 +556,16 @@ const output = new Output(...);
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: {
|
||||
video: track => ({
|
||||
width: 480,
|
||||
bitrate: QUALITY_LOW,
|
||||
},
|
||||
audio: {
|
||||
discard: track.number > 1, // Keep only the first video track
|
||||
}),
|
||||
audio: track => ({
|
||||
numberOfChannels: 1,
|
||||
bitrate: QUALITY_LOW,
|
||||
},
|
||||
discard: track.number > 1, // Keep only the first audio track
|
||||
}),
|
||||
trim: {
|
||||
// Let's keep only the first 60 seconds
|
||||
start: 0,
|
||||
|
||||
@@ -60,6 +60,11 @@ await input.computeDuration(); // => 1905.4615
|
||||
```
|
||||
More specifically, the duration is defined as the maximum end timestamp across all tracks.
|
||||
|
||||
Since not all media files begin at time zero, you can also retrieve the *starting timestamp* of the media file in seconds:
|
||||
```ts
|
||||
await input.getFirstTimestamp(); // => 0.0
|
||||
```
|
||||
|
||||
Mediabunny also lets you read descriptive metadata tags from media files, such as title, artist, or cover art:
|
||||
```ts
|
||||
await input.getMetadataTags(); // => MetadataTags
|
||||
@@ -93,6 +98,10 @@ Once you have an `InputTrack`, you can start extracting metadata from it.
|
||||
// Get a unique ID for this track in the input file:
|
||||
track.id; // => number
|
||||
|
||||
// Get the 1-based index of this track among all tracks of the same type
|
||||
// (e.g., first video track is 1, second video track is 2, etc.):
|
||||
track.number; // => number
|
||||
|
||||
// Check the track's type:
|
||||
track.type; // => 'video' | 'audio' | 'subtitle';
|
||||
|
||||
@@ -120,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'
|
||||
```
|
||||
@@ -497,6 +506,9 @@ type UrlSourceOptions = {
|
||||
// in memory. Defaults to 8 MiB.
|
||||
maxCacheSize?: number;
|
||||
|
||||
// The maximum number of parallel requests to use for fetching. Defaults to 2.
|
||||
parallelism?: number;
|
||||
|
||||
// Used to provide a custom fetch function
|
||||
fetchFn?: typeof fetch;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ Mediabunny supports many commonly used media container formats, all of which are
|
||||
- WAVE (.wav)
|
||||
- ADTS (.aac)
|
||||
- FLAC (.flac)
|
||||
- MPEG Transport Stream (.ts)
|
||||
|
||||
## Codecs
|
||||
|
||||
@@ -20,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.
|
||||
:::
|
||||
@@ -39,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
|
||||
@@ -54,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 can use the [`@mediabunny/ac3`](./extensions/ac3) extension package, or provide your own [custom coder](#custom-coders).
|
||||
|
||||
### Subtitle codecs
|
||||
|
||||
- `'webvtt'` - WebVTT
|
||||
@@ -62,37 +69,39 @@ 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 |
|
||||
|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||
| `'avc'` | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'hevc'` | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'vp8'` | ✓ | ✓ | ✓ | ✓ | | | | | |
|
||||
| `'vp9'` | ✓ | ✓ | ✓ | ✓ | | | | | |
|
||||
| `'av1'` | ✓ | ✓ | ✓ | ✓ | | | | | |
|
||||
| `'aac'` | ✓ | ✓ | ✓ | | | | | ✓ | |
|
||||
| `'opus'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | |
|
||||
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | | | |
|
||||
| `'vorbis'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | |
|
||||
| `'flac'` | ✓ | ✓ | ✓ | | | | | | ✓ |
|
||||
| `'pcm-u8'` | | ✓ | ✓ | | | | ✓ | | |
|
||||
| `'pcm-s8'` | | ✓ | | | | | | | |
|
||||
| `'pcm-s16'` | ✓ | ✓ | ✓ | | | | ✓ | | |
|
||||
| `'pcm-s16be'` | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'pcm-s24'` | ✓ | ✓ | ✓ | | | | ✓ | | |
|
||||
| `'pcm-s24be'` | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'pcm-s32'` | ✓ | ✓ | ✓ | | | | ✓ | | |
|
||||
| `'pcm-s32be'` | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'pcm-f32'` | ✓ | ✓ | ✓ | | | | ✓ | | |
|
||||
| `'pcm-f32be'` | ✓ | ✓ | | | | | | | |
|
||||
| `'pcm-f64'` | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'pcm-f64be'` | ✓ | ✓ | | | | | | | |
|
||||
| `'ulaw'` | | ✓ | | | | | ✓ | | |
|
||||
| `'alaw'` | | ✓ | | | | | ✓ | | |
|
||||
| `'webvtt'`[^2] | (✓) | | (✓) | (✓) | | | | | |
|
||||
| | .mp4 | .mov | .mkv | .webm[^2] | .ogg | .mp3 | .wav | .aac | .flac | .ts |
|
||||
|:--------------:|:--------:|:-----:|:-----:|:---------:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||
| `'avc'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
|
||||
| `'hevc'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
|
||||
| `'vp8'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'vp9'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'av1'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'aac'` | ✓ | ✓ | ✓ | | | | | ✓ | | ✓ |
|
||||
| `'opus'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | | |
|
||||
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | | | | ✓ |
|
||||
| `'vorbis'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | | |
|
||||
| `'flac'` | ✓ | ✓ | ✓ | | | | | | ✓ | |
|
||||
| `'ac3'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
|
||||
| `'eac3'` | ✓ | ✓ | ✓ | | | | | | | ✓ |
|
||||
| `'pcm-u8'` | | ✓ | ✓ | | | | ✓ | | | |
|
||||
| `'pcm-s8'` | | ✓ | | | | | | | | |
|
||||
| `'pcm-s16'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
|
||||
| `'pcm-s16be'` | ✓ | ✓ | ✓ | | | | | | | |
|
||||
| `'pcm-s24'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
|
||||
| `'pcm-s24be'` | ✓ | ✓ | ✓ | | | | | | | |
|
||||
| `'pcm-s32'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
|
||||
| `'pcm-s32be'` | ✓ | ✓ | ✓ | | | | | | | |
|
||||
| `'pcm-f32'` | ✓ | ✓ | ✓ | | | | ✓ | | | |
|
||||
| `'pcm-f32be'` | ✓ | ✓ | | | | | | | | |
|
||||
| `'pcm-f64'` | ✓ | ✓ | ✓ | | | | | | | |
|
||||
| `'pcm-f64be'` | ✓ | ✓ | | | | | | | | |
|
||||
| `'ulaw'` | | ✓ | | | | | ✓ | | | |
|
||||
| `'alaw'` | | ✓ | | | | | ✓ | | | |
|
||||
| `'webvtt'`[^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
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ const bundleSizes = [
|
||||
|
||||
const sponsors = {
|
||||
gold: [
|
||||
{ image: '/sponsors/remotion.png', name: 'Remotion', url: 'https://remotion.dev/' },
|
||||
{ image: '/sponsors/remotion-light.png', name: 'Remotion', url: 'https://remotion.dev/' },
|
||||
{ image: '/sponsors/gling.svg', name: 'Gling AI', url: 'https://www.gling.ai/' },
|
||||
{ image: '/sponsors/diffusionstudio.png', name: 'Diffusion Studio', url: 'https://diffusion.studio/' },
|
||||
{ image: '/sponsors/kino.jpg', name: 'Kino', url: 'https://kino.ai/' },
|
||||
@@ -101,22 +101,30 @@ const sponsors = {
|
||||
{ image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' },
|
||||
],
|
||||
bronze: [
|
||||
{ image: '/sponsors/elevenlabs.png', name: 'ElevenLabs', url: 'https://elevenlabs.io/' },
|
||||
{ image: '/sponsors/rve.png', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' },
|
||||
{ image: '/sponsors/mux.jpg', name: 'Mux', url: 'https://www.mux.com/' },
|
||||
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
|
||||
],
|
||||
individual: [
|
||||
{ image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/197597', name: 'Christopher Chedeau', url: 'https://github.com/vjeux' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5913254', name: 'Brandon McConnell', url: 'https://github.com/brandonmcconnell' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/9549394', name: 'studnitz', url: 'https://github.com/studnitz' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/4714175', name: 'Phoomparin Mano', url: 'https://github.com/heypoom' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/504909', name: 'Hirbod', url: 'https://github.com/hirbod' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/139718', name: 'Anton Kosiakin', url: 'https://github.com/deil' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/56988069', name: 'SyhabouthAlex', url: 'https://github.com/SyhabouthAlex' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/38181164', name: 'wcw', url: 'https://github.com/asd55667' },
|
||||
{ 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' },
|
||||
@@ -124,6 +132,8 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/97225946', name: '808vita', url: 'https://github.com/808vita' },
|
||||
{ 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/5907357', name: 'Harvey Zhao', url: 'https://github.com/zhw2590582' },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -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 |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -38,8 +38,11 @@ export default tseslint.config(
|
||||
'dist-docs',
|
||||
'packages/mp3-encoder/dist',
|
||||
'packages/mp3-encoder/build',
|
||||
'packages/ac3/dist',
|
||||
'packages/ac3/build',
|
||||
'eslint.config.mjs',
|
||||
'docs/.vitepress/cache',
|
||||
'test/public',
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
@@ -62,20 +62,22 @@ const compressFile = async (resource: File | string) => {
|
||||
currentConversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: {
|
||||
video: track => ({
|
||||
width: 320, // Height will be deduced automatically to retain aspect ratio
|
||||
bitrate: QUALITY_VERY_LOW,
|
||||
},
|
||||
audio: {
|
||||
discard: track.number > 1, // Keep only the first video track
|
||||
}),
|
||||
audio: track => ({
|
||||
bitrate: 32e3,
|
||||
},
|
||||
discard: track.number > 1, // Keep only the first audio track
|
||||
}),
|
||||
});
|
||||
|
||||
// Keep track of progress
|
||||
let progress = 0;
|
||||
currentConversion.onProgress = newProgress => progress = newProgress;
|
||||
|
||||
const fileDuration = await input.computeDuration();
|
||||
const fileDuration = (await input.computeDuration()) - (await input.getFirstTimestamp());
|
||||
const startTime = performance.now();
|
||||
|
||||
const updateProgress = () => {
|
||||
@@ -124,7 +126,7 @@ const compressFile = async (resource: File | string) => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,audio/*,audio/aac';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
|
||||
@@ -663,7 +663,7 @@ window.addEventListener('resize', () => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,audio/*,audio/aac';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
|
||||
@@ -43,12 +43,14 @@ const extractMetadata = (resource: File | string) => {
|
||||
const object = {
|
||||
'Format': input.getFormat().then(format => format.name),
|
||||
'Full MIME type': input.getMimeType(),
|
||||
'Duration': input.computeDuration().then(duration => `${duration} seconds`),
|
||||
'Starts at': input.getFirstTimestamp().then(start => `${start} seconds`),
|
||||
'Ends at': input.computeDuration().then(duration => `${duration} seconds`),
|
||||
'Tracks': input.getTracks().then(tracks => tracks.map(track => ({
|
||||
'Type': track.type,
|
||||
'Codec': track.codec,
|
||||
'Full codec string': track.getCodecParameterString(),
|
||||
'Duration': track.computeDuration().then(duration => `${duration} seconds`),
|
||||
'Starts at': track.getFirstTimestamp().then(start => `${start} seconds`),
|
||||
'Ends at': track.computeDuration().then(duration => `${duration} seconds`),
|
||||
'Language code': track.languageCode,
|
||||
...(track.isVideoTrack()
|
||||
? {
|
||||
@@ -202,7 +204,7 @@ const shortDelay = () => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,audio/*,audio/aac';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
|
||||
@@ -120,7 +120,7 @@ const generateThumbnails = async (resource: File | string) => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,audio/*,audio/aac';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.25.8",
|
||||
"version": "1.34.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.25.8",
|
||||
"version": "1.34.2",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -1413,6 +1413,10 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mediabunny/ac3": {
|
||||
"resolved": "packages/ac3",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@mediabunny/mp3-encoder": {
|
||||
"resolved": "packages/mp3-encoder",
|
||||
"link": true
|
||||
@@ -7739,9 +7743,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.25.7",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.25.7.tgz",
|
||||
"integrity": "sha512-DL0E1h29HTDaD9bYRXLSSHiAoLbDBksrdYS+4OHWA+aNhQeN+CAGEG7EU6wlhPZ8MOpwXIeC7uv06lo4ziohQQ==",
|
||||
"version": "1.34.0",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.34.0.tgz",
|
||||
"integrity": "sha512-mjn/7QVEPbPEakuNKD8OIPDhaoe2NlgWVyyqftuLVWvsgLol7g2MQzXWCjx523CP8Y9jE2H09hbYCuTwSI7pSQ==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
@@ -12063,9 +12067,24 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"packages/ac3": {
|
||||
"name": "@mediabunny/ac3",
|
||||
"version": "1.34.2",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/Vanilagy"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mediabunny": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.25.8",
|
||||
"version": "1.34.2",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.25.8",
|
||||
"version": "1.34.2",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
@@ -29,18 +29,19 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "./build.sh",
|
||||
"build": "./scripts/build.sh",
|
||||
"watch": "tsx scripts/bundle.ts --watch",
|
||||
"lint": "eslint .",
|
||||
"test": "npx vitest --run",
|
||||
"test-node": "npm run test node/",
|
||||
"test-browser": "npm run test browser/",
|
||||
"check": "rm -rf dist/modules && tsc -p src && tsc -p packages/mp3-encoder/src --noEmit && tsc -p tsconfig.vitest.json --noEmit && tsc -p scripts --noEmit && tsc -p tsconfig.vite.json --noEmit",
|
||||
"test": "npm run pre-test && npx vitest --run",
|
||||
"test-node": "npm run pre-test && npm run test node/",
|
||||
"test-browser": "npm run pre-test && npm run test browser/",
|
||||
"pre-test": "tsx scripts/bundle.ts",
|
||||
"check": "./scripts/check.sh",
|
||||
"check-docblocks": "tsx scripts/check-docblocks.ts dist/mediabunny.d.ts",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "npm run build && npm run docs:generate && vitepress build docs && npm run examples:build && cp dist/mediabunny.d.ts dist-docs/",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts docs/api-config.json",
|
||||
"docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts docs/api-config.json",
|
||||
"dev": "vite",
|
||||
"examples:build": "vite build",
|
||||
"fix-build-import-paths": "tsx scripts/add-import-extensions.ts",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
build/* linguist-generated
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -0,0 +1,108 @@
|
||||
# @mediabunny/ac3
|
||||
|
||||
[](https://www.npmjs.com/package/@mediabunny/ac3)
|
||||
[](https://bundlephobia.com/package/@mediabunny/ac3)
|
||||
[](https://www.npmjs.com/package/@mediabunny/ac3)
|
||||
[](https://discord.gg/hmpkyYuS4U)
|
||||
|
||||
<div align="center">
|
||||
<img src="../../docs/public/mediabunny-logo.svg" width="180" height="180">
|
||||
</div>
|
||||
|
||||
Browsers have no support for AC-3 (Dolby Digital) or E-AC-3 (Dolby Digital Plus) in their WebCodecs implementations. This extension package provides both a decoder and encoder for use with [Mediabunny](https://github.com/Vanilagy/mediabunny), allowing you to decode and encode these codecs directly in the browser. It is implemented using Mediabunny's [custom coder API](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [FFmpeg](https://ffmpeg.org/)'s AC-3 and E-AC-3 coders under the hood.
|
||||
|
||||
> This package, like the rest of Mediabunny, is enabled by its [sponsors](https://mediabunny.dev/#sponsors) and their donations. If you've derived value from this package, please consider [leaving a donation](https://github.com/sponsors/Vanilagy)! 💘
|
||||
|
||||
## Installation
|
||||
|
||||
This library peer-depends on Mediabunny. Install both using npm:
|
||||
```bash
|
||||
npm install mediabunny @mediabunny/ac3
|
||||
```
|
||||
|
||||
Alternatively, directly include them using a script tag:
|
||||
```html
|
||||
<script src="mediabunny.js"></script>
|
||||
<script src="mediabunny-ac3.js"></script>
|
||||
```
|
||||
|
||||
This will expose the global objects `Mediabunny` and `MediabunnyAc3`. Use `mediabunny-ac3.d.ts` to provide types for these globals. You can download the built distribution files from the [releases page](https://github.com/Vanilagy/mediabunny/releases).
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { registerAc3Decoder, registerAc3Encoder } from '@mediabunny/ac3';
|
||||
|
||||
registerAc3Decoder();
|
||||
registerAc3Encoder();
|
||||
```
|
||||
That's it - Mediabunny now uses the registered AC-3/E-AC-3 decoder and encoder automatically.
|
||||
|
||||
## Building and development
|
||||
|
||||
For simplicity, all built WASM artifacts are included in the repo, since these rarely change. However, here are the instructions for building them from scratch:
|
||||
|
||||
[Install Emscripten](https://emscripten.org/docs/getting_started/downloads.html) and clone [FFmpeg](https://github.com/FFmpeg/FFmpeg). Then, from the Mediabunny root and with Emscripten sourced in:
|
||||
|
||||
```bash
|
||||
export FFMPEG_PATH=/path/to/ffmpeg
|
||||
export MEDIABUNNY_ROOT=$PWD
|
||||
|
||||
# Build FFmpeg
|
||||
cd $FFMPEG_PATH
|
||||
emmake make distclean
|
||||
emconfigure ./configure \
|
||||
--target-os=none \
|
||||
--arch=x86_32 \
|
||||
--enable-cross-compile \
|
||||
--disable-asm \
|
||||
--disable-x86asm \
|
||||
--disable-inline-asm \
|
||||
--disable-programs \
|
||||
--disable-doc \
|
||||
--disable-debug \
|
||||
--disable-all \
|
||||
--disable-everything \
|
||||
--disable-autodetect \
|
||||
--disable-pthreads \
|
||||
--disable-runtime-cpudetect \
|
||||
--enable-avcodec \
|
||||
--enable-decoder=ac3 \
|
||||
--enable-decoder=eac3 \
|
||||
--enable-encoder=ac3 \
|
||||
--enable-encoder=eac3 \
|
||||
--cc="emcc" \
|
||||
--cxx=em++ \
|
||||
--ar=emar \
|
||||
--ranlib=emranlib \
|
||||
--extra-cflags="-DNDEBUG -Oz -flto -msimd128" \
|
||||
--extra-ldflags="-Oz -flto"
|
||||
emmake make
|
||||
|
||||
# Compile the bridge between JavaScript and FFmpeg's API
|
||||
cd $MEDIABUNNY_ROOT/packages/ac3
|
||||
emcc src/bridge.c \
|
||||
$FFMPEG_PATH/libavcodec/libavcodec.a \
|
||||
$FFMPEG_PATH/libavutil/libavutil.a \
|
||||
-I$FFMPEG_PATH \
|
||||
-s MODULARIZE=1 \
|
||||
-s EXPORT_ES6=1 \
|
||||
-s SINGLE_FILE=1 \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s ENVIRONMENT=web,worker \
|
||||
-s FILESYSTEM=0 \
|
||||
-s MALLOC=emmalloc \
|
||||
-s SUPPORT_LONGJMP=0 \
|
||||
-s EXPORTED_RUNTIME_METHODS=cwrap,HEAPU8 \
|
||||
-s EXPORTED_FUNCTIONS=_malloc,_free \
|
||||
-msimd128 \
|
||||
-flto \
|
||||
-Oz \
|
||||
-o build/ac3.js
|
||||
```
|
||||
|
||||
This generates `build/ac3.js`, which contains both the JavaScript "glue code" as well as the compiled WASM inlined.
|
||||
|
||||
### Building the package
|
||||
|
||||
Then, the complete JavaScript package can be built alongside the rest of Mediabunny by running `npm run build` in Mediabunny's root.
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
||||
"mainEntryPointFilePath": "dist/modules/src/index.d.ts",
|
||||
"bundledPackages": [],
|
||||
"compiler": {},
|
||||
"apiReport": {
|
||||
"enabled": false
|
||||
},
|
||||
"docModel": {
|
||||
"enabled": false
|
||||
},
|
||||
"dtsRollup": {
|
||||
"enabled": true,
|
||||
"untrimmedFilePath": "dist/mediabunny-ac3.d.ts"
|
||||
},
|
||||
"tsdocMetadata": {
|
||||
"enabled": false
|
||||
},
|
||||
"messages": {
|
||||
"compilerMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
},
|
||||
"extractorMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
},
|
||||
"tsdocMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newlineKind": "lf"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@mediabunny/ac3",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.34.2",
|
||||
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
"module": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
"types": "./dist/modules/src/index.d.ts",
|
||||
"exports": {
|
||||
"types": "./dist/modules/src/index.d.ts",
|
||||
"import": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
"require": "./dist/bundles/mediabunny-ac3.mjs"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"package.json",
|
||||
"LICENSE",
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"license": "MPL-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Vanilagy/mediabunny.git",
|
||||
"directory": "packages/ac3"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Vanilagy/mediabunny/issues"
|
||||
},
|
||||
"homepage": "https://mediabunny.dev/guide/extensions/ac3",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/Vanilagy"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mediabunny": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
},
|
||||
"keywords": [
|
||||
"ac3",
|
||||
"eac3",
|
||||
"dolby",
|
||||
"dolby-digital",
|
||||
"encoding",
|
||||
"decoding",
|
||||
"codec",
|
||||
"mediabunny",
|
||||
"ffmpeg",
|
||||
"browser",
|
||||
"wasm",
|
||||
"polyfill"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "libavcodec/avcodec.h"
|
||||
#include "libavutil/opt.h"
|
||||
#include "libavutil/channel_layout.h"
|
||||
|
||||
typedef struct {
|
||||
AVCodecContext *codec_ctx;
|
||||
AVPacket *packet;
|
||||
AVFrame *frame;
|
||||
} DecoderContext;
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
DecoderContext *init_decoder(int codec_id) {
|
||||
enum AVCodecID av_codec_id = codec_id == 0 ? AV_CODEC_ID_AC3 : AV_CODEC_ID_EAC3;
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(av_codec_id);
|
||||
if (!codec) return NULL;
|
||||
|
||||
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!codec_ctx) return NULL;
|
||||
|
||||
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
if (!packet) {
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
if (!frame) {
|
||||
av_packet_free(&packet);
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DecoderContext *ctx = malloc(sizeof(DecoderContext));
|
||||
if (!ctx) {
|
||||
av_frame_free(&frame);
|
||||
av_packet_free(&packet);
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ctx->codec_ctx = codec_ctx;
|
||||
ctx->packet = packet;
|
||||
ctx->frame = frame;
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
uint8_t *configure_decode_packet(DecoderContext *ctx, int size) {
|
||||
if (av_new_packet(ctx->packet, size) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return ctx->packet->data;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int decode_packet(DecoderContext *ctx, int pts) {
|
||||
ctx->packet->pts = pts;
|
||||
int ret = avcodec_send_packet(ctx->codec_ctx, ctx->packet);
|
||||
av_packet_unref(ctx->packet);
|
||||
if (ret < 0) return ret;
|
||||
|
||||
ret = avcodec_receive_frame(ctx->codec_ctx, ctx->frame);
|
||||
if (ret < 0) return ret;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_decoded_format(DecoderContext *ctx) {
|
||||
return ctx->frame->format;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
uint8_t *get_decoded_plane_ptr(DecoderContext *ctx, int plane) {
|
||||
return ctx->frame->data[plane];
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_decoded_channels(DecoderContext *ctx) {
|
||||
return ctx->frame->ch_layout.nb_channels;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_decoded_sample_rate(DecoderContext *ctx) {
|
||||
return ctx->frame->sample_rate;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_decoded_sample_count(DecoderContext *ctx) {
|
||||
return ctx->frame->nb_samples;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_decoded_pts(DecoderContext *ctx) {
|
||||
return (int)ctx->frame->pts;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void flush_decoder(DecoderContext *ctx) {
|
||||
avcodec_send_packet(ctx->codec_ctx, NULL);
|
||||
while (avcodec_receive_frame(ctx->codec_ctx, ctx->frame) == 0) {}
|
||||
avcodec_flush_buffers(ctx->codec_ctx);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void close_decoder(DecoderContext *ctx) {
|
||||
av_frame_free(&ctx->frame);
|
||||
av_packet_free(&ctx->packet);
|
||||
avcodec_free_context(&ctx->codec_ctx);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
AVCodecContext *codec_ctx;
|
||||
AVPacket *packet;
|
||||
AVFrame *frame;
|
||||
float *input_buffer;
|
||||
int input_buffer_size;
|
||||
int encoded_pts;
|
||||
int encoded_duration;
|
||||
} EncoderContext;
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
EncoderContext *init_encoder(int codec_id, int channels, int sample_rate, int bitrate) {
|
||||
enum AVCodecID av_codec_id = codec_id == 0 ? AV_CODEC_ID_AC3 : AV_CODEC_ID_EAC3;
|
||||
|
||||
const AVCodec *codec = avcodec_find_encoder(av_codec_id);
|
||||
if (!codec) return NULL;
|
||||
|
||||
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!codec_ctx) return NULL;
|
||||
|
||||
codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
|
||||
codec_ctx->sample_rate = sample_rate;
|
||||
codec_ctx->bit_rate = bitrate;
|
||||
codec_ctx->time_base = (AVRational){1, sample_rate};
|
||||
|
||||
AVChannelLayout layout;
|
||||
av_channel_layout_default(&layout, channels);
|
||||
av_channel_layout_copy(&codec_ctx->ch_layout, &layout);
|
||||
av_channel_layout_uninit(&layout);
|
||||
|
||||
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
if (!packet) {
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
if (!frame) {
|
||||
av_packet_free(&packet);
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The frame has a fixed format, so let's create it now:
|
||||
frame->format = AV_SAMPLE_FMT_FLTP;
|
||||
frame->sample_rate = sample_rate;
|
||||
frame->nb_samples = codec_ctx->frame_size;
|
||||
av_channel_layout_copy(&frame->ch_layout, &codec_ctx->ch_layout);
|
||||
|
||||
if (av_frame_get_buffer(frame, 0) < 0) {
|
||||
av_frame_free(&frame);
|
||||
av_packet_free(&packet);
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
EncoderContext *ctx = malloc(sizeof(EncoderContext));
|
||||
if (!ctx) {
|
||||
av_frame_free(&frame);
|
||||
av_packet_free(&packet);
|
||||
avcodec_free_context(&codec_ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ctx->codec_ctx = codec_ctx;
|
||||
ctx->packet = packet;
|
||||
ctx->frame = frame;
|
||||
ctx->input_buffer = NULL;
|
||||
ctx->input_buffer_size = 0;
|
||||
ctx->encoded_pts = 0;
|
||||
ctx->encoded_duration = 0;
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_encoder_frame_size(EncoderContext *ctx) {
|
||||
return ctx->codec_ctx->frame_size;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
float *get_encode_input_ptr(EncoderContext *ctx, int size) {
|
||||
if (ctx->input_buffer_size < size) {
|
||||
free(ctx->input_buffer);
|
||||
ctx->input_buffer = malloc(size);
|
||||
if (!ctx->input_buffer) {
|
||||
ctx->input_buffer_size = 0;
|
||||
return NULL;
|
||||
}
|
||||
ctx->input_buffer_size = size;
|
||||
}
|
||||
return ctx->input_buffer;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int encode_frame(EncoderContext *ctx, int pts) {
|
||||
int channels = ctx->codec_ctx->ch_layout.nb_channels;
|
||||
int frame_size = ctx->frame->nb_samples;
|
||||
|
||||
ctx->frame->pts = pts;
|
||||
|
||||
// Deinterleave f32 input into the frame's f32-planar planes
|
||||
float *input = ctx->input_buffer;
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
float *plane = (float *)ctx->frame->data[ch];
|
||||
for (int i = 0; i < frame_size; i++) {
|
||||
plane[i] = input[i * channels + ch];
|
||||
}
|
||||
}
|
||||
|
||||
int ret = avcodec_send_frame(ctx->codec_ctx, ctx->frame);
|
||||
if (ret < 0) return ret;
|
||||
|
||||
ret = avcodec_receive_packet(ctx->codec_ctx, ctx->packet);
|
||||
if (ret < 0) return ret;
|
||||
|
||||
ctx->encoded_pts = ctx->packet->pts;
|
||||
ctx->encoded_duration = ctx->packet->duration;
|
||||
|
||||
return ctx->packet->size;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void flush_encoder(EncoderContext *ctx) {
|
||||
avcodec_send_frame(ctx->codec_ctx, NULL);
|
||||
while (avcodec_receive_packet(ctx->codec_ctx, ctx->packet) == 0) {
|
||||
av_packet_unref(ctx->packet);
|
||||
}
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
uint8_t *get_encoded_data(EncoderContext *ctx) {
|
||||
return ctx->packet->data;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_encoded_pts(EncoderContext *ctx) {
|
||||
return ctx->encoded_pts;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_encoded_duration(EncoderContext *ctx) {
|
||||
return ctx->encoded_duration;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void close_encoder(EncoderContext *ctx) {
|
||||
free(ctx->input_buffer);
|
||||
av_frame_free(&ctx->frame);
|
||||
av_packet_free(&ctx->packet);
|
||||
avcodec_free_context(&ctx->codec_ctx);
|
||||
free(ctx);
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import createModule from '../build/ac3';
|
||||
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
|
||||
|
||||
type ExtendedEmscriptenModule = EmscriptenModule & {
|
||||
cwrap: typeof cwrap;
|
||||
};
|
||||
|
||||
let module: ExtendedEmscriptenModule;
|
||||
let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
|
||||
|
||||
let initDecoderFn: (codecId: number) => number;
|
||||
let configureDecodePacket: (ctx: number, size: number) => number;
|
||||
let decodePacket: (ctx: number, pts: number) => number;
|
||||
let getDecodedFormat: (ctx: number) => number;
|
||||
let getDecodedPlanePtr: (ctx: number, plane: number) => number;
|
||||
let getDecodedChannels: (ctx: number) => number;
|
||||
let getDecodedSampleRate: (ctx: number) => number;
|
||||
let getDecodedSampleCount: (ctx: number) => number;
|
||||
let getDecodedPts: (ctx: number) => number;
|
||||
let flushDecoderFn: (ctx: number) => void;
|
||||
let closeDecoderFn: (ctx: number) => void;
|
||||
|
||||
let initEncoderFn: (codecId: number, channels: number, sampleRate: number, bitrate: number) => number;
|
||||
let getEncoderFrameSize: (ctx: number) => number;
|
||||
let getEncodeInputPtr: (ctx: number, size: number) => number;
|
||||
let encodeFrameFn: (ctx: number, pts: number) => number;
|
||||
let flushEncoderFn: (ctx: number) => void;
|
||||
let getEncodedData: (ctx: number) => number;
|
||||
let getEncodedPts: (ctx: number) => number;
|
||||
let getEncodedDuration: (ctx: number) => number;
|
||||
let closeEncoderFn: (ctx: number) => void;
|
||||
|
||||
const codecToId = (codec: string) => codec === 'ac3' ? 0 : 1;
|
||||
|
||||
const ensureModule = async () => {
|
||||
if (!module) {
|
||||
if (modulePromise) {
|
||||
// If we don't do this we can have a race condition
|
||||
return modulePromise;
|
||||
}
|
||||
|
||||
modulePromise = createModule() as Promise<ExtendedEmscriptenModule>;
|
||||
module = await modulePromise;
|
||||
modulePromise = null;
|
||||
|
||||
initDecoderFn = module.cwrap('init_decoder', 'number', ['number']);
|
||||
configureDecodePacket = module.cwrap('configure_decode_packet', 'number', ['number', 'number']);
|
||||
decodePacket = module.cwrap('decode_packet', 'number', ['number', 'number']);
|
||||
getDecodedFormat = module.cwrap('get_decoded_format', 'number', ['number']);
|
||||
getDecodedPlanePtr = module.cwrap('get_decoded_plane_ptr', 'number', ['number', 'number']);
|
||||
getDecodedChannels = module.cwrap('get_decoded_channels', 'number', ['number']);
|
||||
getDecodedSampleRate = module.cwrap('get_decoded_sample_rate', 'number', ['number']);
|
||||
getDecodedSampleCount = module.cwrap('get_decoded_sample_count', 'number', ['number']);
|
||||
getDecodedPts = module.cwrap('get_decoded_pts', 'number', ['number']);
|
||||
flushDecoderFn = module.cwrap('flush_decoder', null, ['number']);
|
||||
closeDecoderFn = module.cwrap('close_decoder', null, ['number']);
|
||||
|
||||
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number', 'number']);
|
||||
getEncoderFrameSize = module.cwrap('get_encoder_frame_size', 'number', ['number']);
|
||||
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
|
||||
encodeFrameFn = module.cwrap('encode_frame', 'number', ['number', 'number']);
|
||||
flushEncoderFn = module.cwrap('flush_encoder', null, ['number']);
|
||||
getEncodedData = module.cwrap('get_encoded_data', 'number', ['number']);
|
||||
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']);
|
||||
getEncodedDuration = module.cwrap('get_encoded_duration', 'number', ['number']);
|
||||
closeEncoderFn = module.cwrap('close_encoder', null, ['number']);
|
||||
}
|
||||
};
|
||||
|
||||
const initDecoder = async (codec: string) => {
|
||||
await ensureModule();
|
||||
|
||||
const ctx = initDecoderFn(codecToId(codec));
|
||||
if (ctx === 0) {
|
||||
throw new Error('Failed to initialize AC3 decoder.');
|
||||
}
|
||||
|
||||
return { ctx, frameSize: 0 };
|
||||
};
|
||||
|
||||
// Keys are AVSampleFormat enum values
|
||||
const AV_FORMAT_MAP: Record<number, { format: AudioSampleFormat; bytesPerSample: number; planar: boolean }> = {
|
||||
0: { format: 'u8', bytesPerSample: 1, planar: false },
|
||||
1: { format: 's16', bytesPerSample: 2, planar: false },
|
||||
2: { format: 's32', bytesPerSample: 4, planar: false },
|
||||
3: { format: 'f32', bytesPerSample: 4, planar: false },
|
||||
5: { format: 'u8-planar', bytesPerSample: 1, planar: true },
|
||||
6: { format: 's16-planar', bytesPerSample: 2, planar: true },
|
||||
7: { format: 's32-planar', bytesPerSample: 4, planar: true },
|
||||
8: { format: 'f32-planar', bytesPerSample: 4, planar: true },
|
||||
};
|
||||
|
||||
const decode = (ctx: number, encodedData: ArrayBuffer, timestamp: number) => {
|
||||
const bytes = new Uint8Array(encodedData);
|
||||
|
||||
const dataPtr = configureDecodePacket(ctx, bytes.length);
|
||||
if (dataPtr === 0) {
|
||||
throw new Error('Failed to configure decode packet.');
|
||||
}
|
||||
|
||||
module.HEAPU8.set(bytes, dataPtr);
|
||||
|
||||
const ret = decodePacket(ctx, timestamp);
|
||||
if (ret < 0) {
|
||||
throw new Error(`Decode failed with error code ${ret}.`);
|
||||
}
|
||||
|
||||
const avFormat = getDecodedFormat(ctx);
|
||||
const info = AV_FORMAT_MAP[avFormat];
|
||||
if (!info) {
|
||||
throw new Error(`Unsupported AVSampleFormat: ${avFormat}`);
|
||||
}
|
||||
|
||||
const channels = getDecodedChannels(ctx);
|
||||
const sampleRate = getDecodedSampleRate(ctx);
|
||||
const sampleCount = getDecodedSampleCount(ctx);
|
||||
const pts = getDecodedPts(ctx);
|
||||
|
||||
let pcmData: ArrayBuffer;
|
||||
if (info.planar) {
|
||||
const planeSize = sampleCount * info.bytesPerSample;
|
||||
const buffer = new Uint8Array(planeSize * channels);
|
||||
|
||||
for (let ch = 0; ch < channels; ch++) {
|
||||
const ptr = getDecodedPlanePtr(ctx, ch);
|
||||
buffer.set(module.HEAPU8.subarray(ptr, ptr + planeSize), ch * planeSize);
|
||||
}
|
||||
|
||||
pcmData = buffer.buffer;
|
||||
} else {
|
||||
const totalSize = sampleCount * channels * info.bytesPerSample;
|
||||
const ptr = getDecodedPlanePtr(ctx, 0);
|
||||
pcmData = module.HEAPU8.slice(ptr, ptr + totalSize).buffer;
|
||||
}
|
||||
|
||||
return { pcmData, format: info.format, channels, sampleRate, sampleCount, pts };
|
||||
};
|
||||
|
||||
const initEncoder = async (
|
||||
codec: string,
|
||||
numberOfChannels: number,
|
||||
sampleRate: number,
|
||||
bitrate: number,
|
||||
) => {
|
||||
await ensureModule();
|
||||
|
||||
const ctx = initEncoderFn(codecToId(codec), numberOfChannels, sampleRate, bitrate);
|
||||
if (ctx === 0) {
|
||||
throw new Error('Failed to initialize AC3 encoder.');
|
||||
}
|
||||
|
||||
return { ctx, frameSize: getEncoderFrameSize(ctx) };
|
||||
};
|
||||
|
||||
const encode = (ctx: number, audioData: ArrayBuffer, timestamp: number) => {
|
||||
const audioBytes = new Uint8Array(audioData);
|
||||
|
||||
const inputPtr = getEncodeInputPtr(ctx, audioBytes.length);
|
||||
if (inputPtr === 0) {
|
||||
throw new Error('Failed to allocate encoder input buffer.');
|
||||
}
|
||||
module.HEAPU8.set(audioBytes, inputPtr);
|
||||
|
||||
const bytesWritten = encodeFrameFn(ctx, timestamp);
|
||||
if (bytesWritten < 0) {
|
||||
throw new Error(`Encode failed with error code ${bytesWritten}.`);
|
||||
}
|
||||
|
||||
const ptr = getEncodedData(ctx);
|
||||
const encodedData = module.HEAPU8.slice(ptr, ptr + bytesWritten).buffer;
|
||||
const pts = getEncodedPts(ctx);
|
||||
const duration = getEncodedDuration(ctx);
|
||||
|
||||
return { encodedData, pts, duration };
|
||||
};
|
||||
|
||||
const flushEncoder = (ctx: number) => {
|
||||
flushEncoderFn(ctx);
|
||||
};
|
||||
|
||||
const onMessage = (data: { id: number; command: WorkerCommand }) => {
|
||||
const { id, command } = data;
|
||||
|
||||
const handleCommand = async (): Promise<void> => {
|
||||
try {
|
||||
let result: WorkerResponseData;
|
||||
const transferables: Transferable[] = [];
|
||||
|
||||
switch (command.type) {
|
||||
case 'init-decoder': {
|
||||
const { ctx, frameSize } = await initDecoder(command.data.codec);
|
||||
result = { type: command.type, ctx, frameSize };
|
||||
}; break;
|
||||
|
||||
case 'decode': {
|
||||
const decoded = decode(command.data.ctx, command.data.encodedData, command.data.timestamp);
|
||||
result = {
|
||||
type: command.type,
|
||||
pcmData: decoded.pcmData,
|
||||
format: decoded.format,
|
||||
channels: decoded.channels,
|
||||
sampleRate: decoded.sampleRate,
|
||||
sampleCount: decoded.sampleCount,
|
||||
pts: decoded.pts,
|
||||
};
|
||||
transferables.push(decoded.pcmData);
|
||||
}; break;
|
||||
|
||||
case 'flush-decoder': {
|
||||
flushDecoderFn(command.data.ctx);
|
||||
result = { type: command.type };
|
||||
}; break;
|
||||
|
||||
case 'close-decoder': {
|
||||
closeDecoderFn(command.data.ctx);
|
||||
result = { type: command.type };
|
||||
}; break;
|
||||
|
||||
case 'init-encoder': {
|
||||
const { ctx, frameSize } = await initEncoder(
|
||||
command.data.codec,
|
||||
command.data.numberOfChannels,
|
||||
command.data.sampleRate,
|
||||
command.data.bitrate,
|
||||
);
|
||||
result = { type: command.type, ctx, frameSize };
|
||||
}; break;
|
||||
|
||||
case 'encode': {
|
||||
const encoded = encode(
|
||||
command.data.ctx,
|
||||
command.data.audioData,
|
||||
command.data.timestamp,
|
||||
);
|
||||
result = {
|
||||
type: command.type,
|
||||
encodedData: encoded.encodedData,
|
||||
pts: encoded.pts,
|
||||
duration: encoded.duration,
|
||||
};
|
||||
transferables.push(encoded.encodedData);
|
||||
}; break;
|
||||
|
||||
case 'flush-encoder': {
|
||||
flushEncoder(command.data.ctx);
|
||||
result = { type: command.type };
|
||||
}; break;
|
||||
|
||||
case 'close-encoder': {
|
||||
closeEncoderFn(command.data.ctx);
|
||||
result = { type: command.type };
|
||||
}; break;
|
||||
}
|
||||
|
||||
const response: WorkerResponse = {
|
||||
id,
|
||||
success: true,
|
||||
data: result,
|
||||
};
|
||||
sendMessage(response, transferables);
|
||||
} catch (error: unknown) {
|
||||
const response: WorkerResponse = {
|
||||
id,
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
sendMessage(response);
|
||||
}
|
||||
};
|
||||
|
||||
void handleCommand();
|
||||
};
|
||||
|
||||
const sendMessage = (data: unknown, transferables?: Transferable[]) => {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(data, transferables ?? []);
|
||||
} else {
|
||||
self.postMessage(data, { transfer: transferables ?? [] });
|
||||
}
|
||||
};
|
||||
|
||||
let parentPort: {
|
||||
postMessage: (data: unknown, transferables?: Transferable[]) => void;
|
||||
on: (event: string, listener: (data: never) => void) => void;
|
||||
} | null = null;
|
||||
|
||||
if (typeof self === 'undefined') {
|
||||
const workerModule = 'worker_threads';
|
||||
// eslint-disable-next-line @stylistic/max-len
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access
|
||||
parentPort = require(workerModule).parentPort;
|
||||
}
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.on('message', onMessage);
|
||||
} else {
|
||||
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import {
|
||||
CustomAudioDecoder,
|
||||
AudioCodec,
|
||||
AudioSample,
|
||||
EncodedPacket,
|
||||
registerDecoder,
|
||||
} from 'mediabunny';
|
||||
import { sendCommand } from './worker-client';
|
||||
|
||||
class Ac3Decoder extends CustomAudioDecoder {
|
||||
private ctx = 0;
|
||||
|
||||
static override supports(codec: AudioCodec): boolean {
|
||||
return codec === 'ac3' || codec === 'eac3';
|
||||
}
|
||||
|
||||
async init() {
|
||||
const result = await sendCommand({
|
||||
type: 'init-decoder',
|
||||
data: { codec: this.codec },
|
||||
});
|
||||
this.ctx = result.ctx;
|
||||
}
|
||||
|
||||
async decode(packet: EncodedPacket) {
|
||||
const encodedData = packet.data.slice().buffer;
|
||||
const timestamp = Math.round(packet.timestamp * this.config.sampleRate);
|
||||
|
||||
const result = await sendCommand({
|
||||
type: 'decode',
|
||||
data: { ctx: this.ctx, encodedData, timestamp },
|
||||
}, [encodedData]);
|
||||
|
||||
const sample = new AudioSample({
|
||||
data: result.pcmData,
|
||||
format: result.format,
|
||||
numberOfChannels: result.channels,
|
||||
sampleRate: result.sampleRate,
|
||||
timestamp: result.pts / result.sampleRate,
|
||||
});
|
||||
this.onSample(sample);
|
||||
}
|
||||
|
||||
async flush() {
|
||||
await sendCommand({ type: 'flush-decoder', data: { ctx: this.ctx } });
|
||||
}
|
||||
|
||||
close() {
|
||||
void sendCommand({ type: 'close-decoder', data: { ctx: this.ctx } });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers AC-3 and E-AC-3 decoders, which Mediabunny will then use automatically when applicable. Make sure to call
|
||||
* this function before starting any decoding task.
|
||||
*
|
||||
* @group \@mediabunny/ac3
|
||||
* @public
|
||||
*/
|
||||
export const registerAc3Decoder = () => {
|
||||
registerDecoder(Ac3Decoder);
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import {
|
||||
CustomAudioEncoder,
|
||||
AudioCodec,
|
||||
AudioSample,
|
||||
EncodedPacket,
|
||||
registerEncoder,
|
||||
} from 'mediabunny';
|
||||
import { sendCommand } from './worker-client';
|
||||
import { assert } from './shared';
|
||||
import { AC3_SAMPLE_RATES, EAC3_REDUCED_SAMPLE_RATES } from '../../../shared/ac3-misc';
|
||||
|
||||
class Ac3Encoder extends CustomAudioEncoder {
|
||||
private ctx = 0;
|
||||
private encoderFrameSize = 0;
|
||||
private sampleRate = 0;
|
||||
private numberOfChannels = 0;
|
||||
private chunkMetadata: EncodedAudioChunkMetadata = {};
|
||||
|
||||
// Accumulate interleaved f32 samples until we have a full frame
|
||||
private pendingBuffer = new Float32Array(2 ** 16);
|
||||
private pendingFrames = 0;
|
||||
private nextSampleTimestampInSamples: number | null = null;
|
||||
private nextPacketTimestampInSamples: number | null = null;
|
||||
|
||||
static override supports(codec: AudioCodec, config: AudioEncoderConfig): boolean {
|
||||
const sampleRates = codec === 'eac3'
|
||||
? [...AC3_SAMPLE_RATES, ...EAC3_REDUCED_SAMPLE_RATES]
|
||||
: AC3_SAMPLE_RATES;
|
||||
|
||||
return (codec === 'ac3' || codec === 'eac3')
|
||||
&& config.numberOfChannels >= 1
|
||||
&& config.numberOfChannels <= 8
|
||||
&& sampleRates.includes(config.sampleRate);
|
||||
}
|
||||
|
||||
async init() {
|
||||
assert(this.config.bitrate);
|
||||
this.sampleRate = this.config.sampleRate;
|
||||
this.numberOfChannels = this.config.numberOfChannels;
|
||||
|
||||
const result = await sendCommand({
|
||||
type: 'init-encoder',
|
||||
data: {
|
||||
codec: this.codec,
|
||||
numberOfChannels: this.config.numberOfChannels,
|
||||
sampleRate: this.config.sampleRate,
|
||||
bitrate: this.config.bitrate,
|
||||
},
|
||||
});
|
||||
|
||||
this.ctx = result.ctx;
|
||||
this.encoderFrameSize = result.frameSize;
|
||||
|
||||
this.resetInternalState();
|
||||
}
|
||||
|
||||
private resetInternalState() {
|
||||
this.pendingFrames = 0;
|
||||
this.nextSampleTimestampInSamples = null;
|
||||
this.nextPacketTimestampInSamples = null;
|
||||
|
||||
this.chunkMetadata = {
|
||||
decoderConfig: {
|
||||
codec: this.codec === 'ac3' ? 'ac-3' : 'ec-3',
|
||||
numberOfChannels: this.config.numberOfChannels,
|
||||
sampleRate: this.config.sampleRate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async encode(audioSample: AudioSample) {
|
||||
if (this.nextSampleTimestampInSamples === null) {
|
||||
this.nextSampleTimestampInSamples = Math.round(audioSample.timestamp * this.sampleRate);
|
||||
this.nextPacketTimestampInSamples = this.nextSampleTimestampInSamples;
|
||||
}
|
||||
|
||||
const channels = this.numberOfChannels;
|
||||
const incomingFrames = audioSample.numberOfFrames;
|
||||
|
||||
// Extract interleaved f32 data
|
||||
const totalBytes = audioSample.allocationSize({ format: 'f32', planeIndex: 0 });
|
||||
const audioBytes = new Uint8Array(totalBytes);
|
||||
audioSample.copyTo(audioBytes, { format: 'f32', planeIndex: 0 });
|
||||
const incomingData = new Float32Array(audioBytes.buffer);
|
||||
|
||||
const requiredSamples = (this.pendingFrames + incomingFrames) * channels;
|
||||
if (requiredSamples > this.pendingBuffer.length) {
|
||||
let newSize = this.pendingBuffer.length;
|
||||
while (newSize < requiredSamples) {
|
||||
newSize *= 2;
|
||||
}
|
||||
const newBuffer = new Float32Array(newSize);
|
||||
newBuffer.set(this.pendingBuffer.subarray(0, this.pendingFrames * channels));
|
||||
this.pendingBuffer = newBuffer;
|
||||
}
|
||||
this.pendingBuffer.set(incomingData, this.pendingFrames * channels);
|
||||
this.pendingFrames += incomingFrames;
|
||||
|
||||
while (this.pendingFrames >= this.encoderFrameSize) {
|
||||
await this.encodeOneFrame();
|
||||
}
|
||||
}
|
||||
|
||||
async flush() {
|
||||
// Pad remaining samples with silence to fill a full frame
|
||||
if (this.pendingFrames > 0) {
|
||||
const channels = this.numberOfChannels;
|
||||
const frameSize = this.encoderFrameSize;
|
||||
const usedSamples = this.pendingFrames * channels;
|
||||
const frameSamples = frameSize * channels;
|
||||
|
||||
this.pendingBuffer.fill(0, usedSamples, frameSamples);
|
||||
this.pendingFrames = frameSize;
|
||||
|
||||
await this.encodeOneFrame();
|
||||
}
|
||||
|
||||
await sendCommand({ type: 'flush-encoder', data: { ctx: this.ctx } });
|
||||
|
||||
this.resetInternalState();
|
||||
}
|
||||
|
||||
close() {
|
||||
void sendCommand({ type: 'close-encoder', data: { ctx: this.ctx } });
|
||||
}
|
||||
|
||||
private async encodeOneFrame() {
|
||||
assert(this.nextSampleTimestampInSamples !== null);
|
||||
assert(this.nextPacketTimestampInSamples !== null);
|
||||
|
||||
const channels = this.numberOfChannels;
|
||||
const frameSize = this.encoderFrameSize;
|
||||
const frameSamples = frameSize * channels;
|
||||
|
||||
const frameData = this.pendingBuffer.slice(0, frameSamples);
|
||||
|
||||
// Shift remaining using copyWithin
|
||||
this.pendingFrames -= frameSize;
|
||||
if (this.pendingFrames > 0) {
|
||||
this.pendingBuffer.copyWithin(0, frameSamples, frameSamples + this.pendingFrames * channels);
|
||||
}
|
||||
|
||||
const audioData = frameData.buffer;
|
||||
const result = await sendCommand({
|
||||
type: 'encode',
|
||||
data: {
|
||||
ctx: this.ctx,
|
||||
audioData,
|
||||
timestamp: this.nextSampleTimestampInSamples,
|
||||
},
|
||||
}, [audioData]);
|
||||
|
||||
this.nextSampleTimestampInSamples += frameSize;
|
||||
|
||||
// We always get exactly one packet because we encode the correct frame size
|
||||
const packet = new EncodedPacket(
|
||||
new Uint8Array(result.encodedData),
|
||||
'key',
|
||||
this.nextPacketTimestampInSamples / this.sampleRate,
|
||||
result.duration / this.sampleRate,
|
||||
);
|
||||
|
||||
this.nextPacketTimestampInSamples += result.duration;
|
||||
|
||||
this.onPacket(
|
||||
packet,
|
||||
this.chunkMetadata,
|
||||
);
|
||||
|
||||
this.chunkMetadata = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers AC-3 and E-AC-3 encoders, which Mediabunny will then use automatically when applicable. Make sure to call
|
||||
* this function before starting any encoding task.
|
||||
*
|
||||
* @group \@mediabunny/ac3
|
||||
* @public
|
||||
*/
|
||||
export const registerAc3Encoder = () => {
|
||||
registerEncoder(Ac3Encoder);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
const AC3_LOADED_SYMBOL = Symbol.for('@mediabunny/ac3 loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
'[WARNING]\n@mediabunny/ac3 was loaded twice.'
|
||||
+ ' This will likely cause the encoder/decoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/ac3,'
|
||||
+ ' or if something is being bundled incorrectly.',
|
||||
);
|
||||
}
|
||||
(globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL] = true;
|
||||
|
||||
export { registerAc3Decoder } from './decoder';
|
||||
export { registerAc3Encoder } from './encoder';
|
||||
@@ -0,0 +1,103 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
export type WorkerCommand = {
|
||||
type: 'init-decoder';
|
||||
data: {
|
||||
codec: string;
|
||||
};
|
||||
} | {
|
||||
type: 'decode';
|
||||
data: {
|
||||
ctx: number;
|
||||
encodedData: ArrayBuffer;
|
||||
timestamp: number;
|
||||
};
|
||||
} | {
|
||||
type: 'flush-decoder';
|
||||
data: {
|
||||
ctx: number;
|
||||
};
|
||||
} | {
|
||||
type: 'close-decoder';
|
||||
data: {
|
||||
ctx: number;
|
||||
};
|
||||
} | {
|
||||
type: 'init-encoder';
|
||||
data: {
|
||||
codec: string;
|
||||
numberOfChannels: number;
|
||||
sampleRate: number;
|
||||
bitrate: number;
|
||||
};
|
||||
} | {
|
||||
type: 'encode';
|
||||
data: {
|
||||
ctx: number;
|
||||
audioData: ArrayBuffer;
|
||||
timestamp: number;
|
||||
};
|
||||
} | {
|
||||
type: 'flush-encoder';
|
||||
data: {
|
||||
ctx: number;
|
||||
};
|
||||
} | {
|
||||
type: 'close-encoder';
|
||||
data: {
|
||||
ctx: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkerResponseData = {
|
||||
type: 'init-decoder';
|
||||
ctx: number;
|
||||
frameSize: number;
|
||||
} | {
|
||||
type: 'decode';
|
||||
pcmData: ArrayBuffer;
|
||||
format: AudioSampleFormat;
|
||||
channels: number;
|
||||
sampleRate: number;
|
||||
sampleCount: number;
|
||||
pts: number;
|
||||
} | {
|
||||
type: 'flush-decoder';
|
||||
} | {
|
||||
type: 'close-decoder';
|
||||
} | {
|
||||
type: 'init-encoder';
|
||||
ctx: number;
|
||||
frameSize: number;
|
||||
} | {
|
||||
type: 'encode';
|
||||
encodedData: ArrayBuffer;
|
||||
pts: number;
|
||||
duration: number;
|
||||
} | {
|
||||
type: 'flush-encoder';
|
||||
} | {
|
||||
type: 'close-encoder';
|
||||
};
|
||||
|
||||
export type WorkerResponse = {
|
||||
id: number;
|
||||
} & ({
|
||||
success: true;
|
||||
data: WorkerResponseData;
|
||||
} | {
|
||||
success: false;
|
||||
error: unknown;
|
||||
});
|
||||
|
||||
export function assert(x: unknown): asserts x {
|
||||
if (!x) {
|
||||
throw new Error('Assertion failed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { assert, type WorkerCommand, type WorkerResponse, type WorkerResponseData } from './shared';
|
||||
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
|
||||
import createWorker from './codec.worker';
|
||||
|
||||
let workerPromise: Promise<Worker> | null;
|
||||
let nextMessageId = 0;
|
||||
const pendingMessages = new Map<number, {
|
||||
resolve: (value: WorkerResponseData) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}>();
|
||||
|
||||
export const sendCommand = async <T extends string>(
|
||||
command: WorkerCommand & { type: T },
|
||||
transferables?: Transferable[],
|
||||
) => {
|
||||
const worker = await ensureWorker();
|
||||
|
||||
return new Promise<WorkerResponseData & { type: T }>((resolve, reject) => {
|
||||
const id = nextMessageId++;
|
||||
pendingMessages.set(id, {
|
||||
resolve: resolve as (value: WorkerResponseData) => void,
|
||||
reject,
|
||||
});
|
||||
|
||||
if (transferables) {
|
||||
worker.postMessage({ id, command }, transferables);
|
||||
} else {
|
||||
worker.postMessage({ id, command });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const ensureWorker = () => {
|
||||
return workerPromise ??= (async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const worker = (await createWorker()) as Worker;
|
||||
|
||||
const onMessage = (data: WorkerResponse) => {
|
||||
const pending = pendingMessages.get(data.id);
|
||||
assert(pending !== undefined);
|
||||
|
||||
pendingMessages.delete(data.id);
|
||||
if (data.success) {
|
||||
pending.resolve(data.data);
|
||||
} else {
|
||||
pending.reject(data.error);
|
||||
}
|
||||
};
|
||||
|
||||
if (worker.addEventListener) {
|
||||
worker.addEventListener('message', event => onMessage(event.data as WorkerResponse));
|
||||
} else {
|
||||
const nodeWorker = worker as unknown as {
|
||||
on: (event: string, listener: (data: never) => void) => void;
|
||||
};
|
||||
nodeWorker.on('message', onMessage);
|
||||
}
|
||||
|
||||
return worker;
|
||||
})();
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/modules",
|
||||
"composite": true,
|
||||
"outDir": "./dist/modules",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"stripInternal": true,
|
||||
@@ -10,14 +11,14 @@
|
||||
"module": "nodenext",
|
||||
"allowJs": true,
|
||||
"paths": {
|
||||
"mediabunny": ["../../../src/index.ts"],
|
||||
"mediabunny": ["../../src/index.ts"],
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"**/*",
|
||||
"../../../shared/**/*"
|
||||
"./src/**/*",
|
||||
"./build/**/*",
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src" }
|
||||
{ "path": "../../src" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://www.npmjs.com/package/@mediabunny/mp3-encoder)
|
||||
[](https://bundlephobia.com/package/@mediabunny/mp3-encoder)
|
||||
[](https://www.npmjs.com/package/@mediabunny/mp3-encoder)
|
||||
[](https://discord.gg/hmpkyYuS4U)
|
||||
|
||||
<div align="center">
|
||||
<img src="./logo.svg" width="180" height="180">
|
||||
@@ -102,8 +103,6 @@ The WASM build itself is a performance-optimized, SIMD-enabled build of LAME 3.1
|
||||
|
||||
## Building and development
|
||||
|
||||
Building this library is done using the build commands in the [Mediabunny root](https://github.com/Vanilagy/mediabunny).
|
||||
|
||||
For simplicity, all built WASM artifacts are included in the repo, since these rarely change. However, here are the instructions for building them from scratch:
|
||||
|
||||
### Prerequisites
|
||||
@@ -148,7 +147,7 @@ emcc src/lame-bridge.c build/libmp3lame.a \
|
||||
-o build/lame.js
|
||||
```
|
||||
|
||||
This generates `build/lame.js`, which contains both the JavaScript "glue code" as well as the compiled WASM encoded with Base64.
|
||||
This generates `build/lame.js`, which contains both the JavaScript "glue code" as well as the compiled WASM inlined.
|
||||
|
||||
### Building the package
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.25.8",
|
||||
"version": "1.34.2",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -146,7 +146,10 @@ const onMessage = (data: { id: number; command: WorkerCommand }) => {
|
||||
command.data.sampleRate,
|
||||
command.data.bitrate,
|
||||
);
|
||||
result = { success: true };
|
||||
result = {
|
||||
type: command.type,
|
||||
success: true,
|
||||
};
|
||||
}; break;
|
||||
|
||||
case 'encode': {
|
||||
@@ -154,13 +157,19 @@ const onMessage = (data: { id: number; command: WorkerCommand }) => {
|
||||
command.data.audioData,
|
||||
command.data.numberOfFrames,
|
||||
);
|
||||
result = { encodedData };
|
||||
result = {
|
||||
type: command.type,
|
||||
encodedData,
|
||||
};
|
||||
transferables.push(encodedData);
|
||||
}; break;
|
||||
|
||||
case 'flush': {
|
||||
const flushedData = flush();
|
||||
result = { flushedData };
|
||||
result = {
|
||||
type: command.type,
|
||||
flushedData,
|
||||
};
|
||||
transferables.push(flushedData);
|
||||
}; break;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -7,11 +7,22 @@
|
||||
*/
|
||||
|
||||
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, registerEncoder } from 'mediabunny';
|
||||
import { FRAME_HEADER_SIZE, readFrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
|
||||
import { FRAME_HEADER_SIZE, readMp3FrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
|
||||
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
|
||||
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
|
||||
import createWorker from './encode.worker';
|
||||
|
||||
const MP3_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/mp3-encoder loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
'[WARNING]\n@mediabunny/mp3-encoder was loaded twice.'
|
||||
+ ' This will likely cause the encoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/mp3-encoder,'
|
||||
+ ' or if something is being bundled incorrectly.',
|
||||
);
|
||||
}
|
||||
(globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL] = true;
|
||||
|
||||
class Mp3Encoder extends CustomAudioEncoder {
|
||||
private worker: Worker | null = null;
|
||||
private nextMessageId = 0;
|
||||
@@ -69,6 +80,12 @@ class Mp3Encoder extends CustomAudioEncoder {
|
||||
},
|
||||
});
|
||||
|
||||
this.resetInternalState();
|
||||
}
|
||||
|
||||
private resetInternalState() {
|
||||
this.currentBufferOffset = 0;
|
||||
this.currentTimestamp = null;
|
||||
this.chunkMetadata = {
|
||||
decoderConfig: {
|
||||
codec: 'mp3',
|
||||
@@ -108,15 +125,15 @@ class Mp3Encoder extends CustomAudioEncoder {
|
||||
},
|
||||
}, [audioData]);
|
||||
|
||||
assert('encodedData' in result);
|
||||
this.digestOutput(new Uint8Array(result.encodedData));
|
||||
}
|
||||
|
||||
async flush() {
|
||||
const result = await this.sendCommand({ type: 'flush' });
|
||||
|
||||
assert('flushedData' in result);
|
||||
this.digestOutput(new Uint8Array(result.flushedData));
|
||||
|
||||
this.resetInternalState();
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -145,7 +162,7 @@ class Mp3Encoder extends CustomAudioEncoder {
|
||||
let pos = 0;
|
||||
while (pos <= this.currentBufferOffset - FRAME_HEADER_SIZE) {
|
||||
const word = new DataView(this.buffer.buffer).getUint32(pos, false);
|
||||
const header = readFrameHeader(word, null).header;
|
||||
const header = readMp3FrameHeader(word, null).header;
|
||||
if (!header) {
|
||||
break;
|
||||
}
|
||||
@@ -160,9 +177,7 @@ class Mp3Encoder extends CustomAudioEncoder {
|
||||
const duration = header.audioSamplesInFrame / header.sampleRate;
|
||||
this.onPacket(new EncodedPacket(data, 'key', this.currentTimestamp, duration), this.chunkMetadata);
|
||||
|
||||
if (this.currentTimestamp === 0) {
|
||||
this.chunkMetadata = {}; // Mimic WebCodecs-like behavior
|
||||
}
|
||||
this.chunkMetadata = {}; // Mimic WebCodecs-like behavior
|
||||
|
||||
this.currentTimestamp += duration;
|
||||
pos += header.totalSize;
|
||||
@@ -175,13 +190,16 @@ class Mp3Encoder extends CustomAudioEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
private sendCommand(
|
||||
command: WorkerCommand,
|
||||
private sendCommand<T extends string>(
|
||||
command: WorkerCommand & { type: T },
|
||||
transferables?: Transferable[],
|
||||
) {
|
||||
return new Promise<WorkerResponseData>((resolve, reject) => {
|
||||
return new Promise<WorkerResponseData & { type: T }>((resolve, reject) => {
|
||||
const id = this.nextMessageId++;
|
||||
this.pendingMessages.set(id, { resolve, reject });
|
||||
this.pendingMessages.set(id, {
|
||||
resolve: resolve as (value: WorkerResponseData) => void,
|
||||
reject,
|
||||
});
|
||||
|
||||
assert(this.worker);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -24,10 +24,13 @@ export type WorkerCommand = {
|
||||
};
|
||||
|
||||
export type WorkerResponseData = {
|
||||
type: 'init';
|
||||
success: boolean;
|
||||
} | {
|
||||
type: 'encode';
|
||||
encodedData: ArrayBuffer;
|
||||
} | {
|
||||
type: 'flush';
|
||||
flushedData: ArrayBuffer;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist/modules",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"stripInternal": true,
|
||||
"noEmit": false,
|
||||
"moduleResolution": "nodenext",
|
||||
"module": "nodenext",
|
||||
"allowJs": true,
|
||||
"paths": {
|
||||
"mediabunny": ["../../src/index.ts"],
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"./build/**/*",
|
||||
"../../shared/**/*",
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../src" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
@@ -6,13 +6,15 @@ set -e
|
||||
# Clear the stuff from last build
|
||||
rm -rf dist
|
||||
rm -rf packages/mp3-encoder/dist
|
||||
rm -rf packages/ac3/dist
|
||||
|
||||
# Ensure license headers on all source files
|
||||
tsx scripts/ensure-license-headers.ts
|
||||
|
||||
# Type check & generate .js and .d.ts files
|
||||
tsc -p src
|
||||
tsc -p packages/mp3-encoder/src
|
||||
tsc -p packages/mp3-encoder
|
||||
tsc -p packages/ac3
|
||||
|
||||
# So that the resulting files use valid ESM imports with file extension. This only runs for the core Mediabunny as only
|
||||
# it ships the individual files to npm (for tree shaking, because it's large)
|
||||
@@ -24,14 +26,17 @@ tsx scripts/bundle.ts
|
||||
# Declaration file rollup and checks
|
||||
api-extractor run
|
||||
api-extractor run -c packages/mp3-encoder/api-extractor.json
|
||||
api-extractor run -c packages/ac3/api-extractor.json
|
||||
|
||||
# Checks that all symbols are documented
|
||||
tsx scripts/check-docblocks.ts dist/mediabunny.d.ts
|
||||
tsx scripts/check-docblocks.ts packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
|
||||
tsx scripts/check-docblocks.ts packages/ac3/dist/mediabunny-ac3.d.ts
|
||||
|
||||
# Checks that API docs are generatable
|
||||
npm run docs:generate -- --dry
|
||||
|
||||
# Appends stuff to the declaration files to register the global variables these libraries expose
|
||||
echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts
|
||||
echo 'export as namespace MediabunnyMp3Encoder;' >> packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
|
||||
echo 'export as namespace MediabunnyMp3Encoder;' >> packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
|
||||
echo 'export as namespace MediabunnyAc3;' >> packages/ac3/dist/mediabunny-ac3.d.ts
|
||||
@@ -21,7 +21,7 @@ const createVariants = async (
|
||||
},
|
||||
banner: {
|
||||
js: `/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -113,9 +113,41 @@ const mp3EncoderVariants = await createVariants(
|
||||
},
|
||||
);
|
||||
|
||||
const ac3Variants = await createVariants(
|
||||
'packages/ac3/src/index.ts',
|
||||
'MediabunnyAc3',
|
||||
'packages/ac3/dist/bundles/mediabunny-ac3',
|
||||
'js', // The bundles are purely for the browser, not for Node (due to the peer dependecy)
|
||||
{
|
||||
plugins: [
|
||||
PluginExternalGlobal.externalGlobalPlugin({
|
||||
mediabunny: 'Mediabunny',
|
||||
}),
|
||||
inlineWorkerPlugin({
|
||||
define: {
|
||||
'import.meta.url': '""',
|
||||
},
|
||||
legalComments: 'none',
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
external: ['mediabunny'],
|
||||
plugins: [
|
||||
inlineWorkerPlugin({
|
||||
define: {
|
||||
'import.meta.url': '""',
|
||||
},
|
||||
legalComments: 'none',
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const contexts = [
|
||||
...mediabunnyVariants,
|
||||
...mp3EncoderVariants,
|
||||
...ac3Variants,
|
||||
];
|
||||
|
||||
if (process.argv[2] === '--watch') {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
rm -rf dist/modules
|
||||
tsc -p src --stripInternal false
|
||||
|
||||
tsc -p packages/mp3-encoder --noEmit
|
||||
|
||||
rm -rf packages/ac3/dist/modules
|
||||
tsc -p packages/ac3
|
||||
|
||||
tsc -p tsconfig.vitest.json --noEmit
|
||||
|
||||
tsc -p scripts --noEmit
|
||||
|
||||
tsc -p tsconfig.vite.json --noEmit
|
||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const LICENSE_HEADER = `/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -36,6 +36,7 @@ const checkDirectory = (dirPath: string) => {
|
||||
|
||||
checkDirectory(path.join(__dirname, '..', 'src'));
|
||||
checkDirectory(path.join(__dirname, '..', 'packages', 'mp3-encoder', 'src'));
|
||||
checkDirectory(path.join(__dirname, '..', 'packages', 'ac3', 'src'));
|
||||
checkDirectory(path.join(__dirname, '..', 'shared'));
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/** Sample rates indexed by fscod (Table 4.1) */
|
||||
export const AC3_SAMPLE_RATES = [48000, 44100, 32000];
|
||||
|
||||
/** E-AC-3 reduced sample rates for fscod2 per ATSC A/52:2018 */
|
||||
export const EAC3_REDUCED_SAMPLE_RATES = [24000, 22050, 16000];
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -27,7 +27,7 @@ export const XING = 0x58696e67;
|
||||
/** 'Info' */
|
||||
export const INFO = 0x496e666f;
|
||||
|
||||
export type FrameHeader = {
|
||||
export type Mp3FrameHeader = {
|
||||
totalSize: number;
|
||||
mpegVersionId: number;
|
||||
layer: number;
|
||||
@@ -66,8 +66,8 @@ export const getXingOffset = (mpegVersionId: number, channel: number) => {
|
||||
: (channel === 3 ? 13 : 21);
|
||||
};
|
||||
|
||||
export const readFrameHeader = (word: number, remainingBytes: number | null): {
|
||||
header: FrameHeader | null;
|
||||
export const readMp3FrameHeader = (word: number, remainingBytes: number | null): {
|
||||
header: Mp3FrameHeader | null;
|
||||
bytesAdvanced: number;
|
||||
} => {
|
||||
const firstByte = word >>> 24;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -8,23 +8,32 @@
|
||||
|
||||
import { aacChannelMap, aacFrequencyTable, AudioCodec } from '../codec';
|
||||
import { Demuxer } from '../demuxer';
|
||||
import {
|
||||
ID3_V2_HEADER_SIZE,
|
||||
parseId3V2Tag,
|
||||
readId3V2Header,
|
||||
} from '../id3';
|
||||
import { Input } from '../input';
|
||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||
import { PacketRetrievalOptions } from '../media-sink';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||
import {
|
||||
assert,
|
||||
AsyncMutex,
|
||||
binarySearchExact,
|
||||
binarySearchLessOrEqual,
|
||||
Bitstream,
|
||||
UNDETERMINED_LANGUAGE,
|
||||
} from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { readBytes, Reader } from '../reader';
|
||||
import { DEFAULT_TRACK_DISPOSITION } from '../metadata';
|
||||
import { FrameHeader, MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts-reader';
|
||||
import {
|
||||
AdtsFrameHeader,
|
||||
MIN_ADTS_FRAME_HEADER_SIZE,
|
||||
MAX_ADTS_FRAME_HEADER_SIZE,
|
||||
readAdtsFrameHeader,
|
||||
} from './adts-reader';
|
||||
|
||||
const SAMPLES_PER_AAC_FRAME = 1024;
|
||||
export const SAMPLES_PER_AAC_FRAME = 1024;
|
||||
|
||||
type Sample = {
|
||||
timestamp: number;
|
||||
@@ -37,8 +46,9 @@ export class AdtsDemuxer extends Demuxer {
|
||||
reader: Reader;
|
||||
|
||||
metadataPromise: Promise<void> | null = null;
|
||||
firstFrameHeader: FrameHeader | null = null;
|
||||
firstFrameHeader: AdtsFrameHeader | null = null;
|
||||
loadedSamples: Sample[] = [];
|
||||
metadataTags: MetadataTags | null = null;
|
||||
|
||||
tracks: InputAudioTrack[] = [];
|
||||
|
||||
@@ -69,14 +79,38 @@ export class AdtsDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
async advanceReader() {
|
||||
let slice = this.reader.requestSliceRange(this.lastLoadedPos, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE);
|
||||
if (this.lastLoadedPos === 0) {
|
||||
// Skip all ID3v2 tags at the start of the file
|
||||
while (true) {
|
||||
let slice = this.reader.requestSlice(this.lastLoadedPos, ID3_V2_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
|
||||
if (!slice) {
|
||||
this.lastSampleLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const id3V2Header = readId3V2Header(slice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.lastLoadedPos = slice.filePos + id3V2Header.size;
|
||||
}
|
||||
}
|
||||
|
||||
let slice = this.reader.requestSliceRange(
|
||||
this.lastLoadedPos,
|
||||
MIN_ADTS_FRAME_HEADER_SIZE,
|
||||
MAX_ADTS_FRAME_HEADER_SIZE,
|
||||
);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) {
|
||||
this.lastSampleLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const header = readFrameHeader(slice);
|
||||
const header = readAdtsFrameHeader(slice);
|
||||
if (!header) {
|
||||
this.lastSampleLoaded = true;
|
||||
return;
|
||||
@@ -95,13 +129,12 @@ export class AdtsDemuxer extends Demuxer {
|
||||
const sampleRate = aacFrequencyTable[header.samplingFrequencyIndex];
|
||||
assert(sampleRate !== undefined);
|
||||
const sampleDuration = SAMPLES_PER_AAC_FRAME / sampleRate;
|
||||
const headerSize = header.crcCheck ? MAX_FRAME_HEADER_SIZE : MIN_FRAME_HEADER_SIZE;
|
||||
|
||||
const sample: Sample = {
|
||||
timestamp: this.nextTimestampInSamples / sampleRate,
|
||||
duration: sampleDuration,
|
||||
dataStart: header.startPos + headerSize,
|
||||
dataSize: header.frameLength - headerSize,
|
||||
dataStart: header.startPos,
|
||||
dataSize: header.frameLength,
|
||||
};
|
||||
|
||||
this.loadedSamples.push(sample);
|
||||
@@ -128,7 +161,41 @@ export class AdtsDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
async getMetadataTags() {
|
||||
return {}; // No tags in this one
|
||||
const release = await this.readingMutex.acquire();
|
||||
|
||||
try {
|
||||
await this.readMetadata();
|
||||
|
||||
if (this.metadataTags) {
|
||||
return this.metadataTags;
|
||||
}
|
||||
|
||||
this.metadataTags = {};
|
||||
let currentPos = 0;
|
||||
|
||||
while (true) {
|
||||
let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
||||
if (!headerSlice) break;
|
||||
|
||||
const id3V2Header = readId3V2Header(headerSlice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
|
||||
if (contentSlice instanceof Promise) contentSlice = await contentSlice;
|
||||
if (!contentSlice) break;
|
||||
|
||||
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
|
||||
|
||||
currentPos = headerSlice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
return this.metadataTags;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +206,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
async getFirstTimestamp() {
|
||||
return 0;
|
||||
}
|
||||
@@ -198,27 +269,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
|
||||
async getDecoderConfig(): Promise<AudioDecoderConfig> {
|
||||
assert(this.demuxer.firstFrameHeader);
|
||||
|
||||
const bytes = new Uint8Array(3); // 19 bits max
|
||||
const bitstream = new Bitstream(bytes);
|
||||
|
||||
const { objectType, samplingFrequencyIndex, channelConfiguration } = this.demuxer.firstFrameHeader;
|
||||
|
||||
if (objectType > 31) {
|
||||
bitstream.writeBits(5, 31);
|
||||
bitstream.writeBits(6, objectType - 32);
|
||||
} else {
|
||||
bitstream.writeBits(5, objectType);
|
||||
}
|
||||
|
||||
bitstream.writeBits(4, samplingFrequencyIndex); // samplingFrequencyIndex === 15 is forbidden
|
||||
|
||||
bitstream.writeBits(4, channelConfiguration);
|
||||
|
||||
return {
|
||||
codec: `mp4a.40.${this.demuxer.firstFrameHeader.objectType}`,
|
||||
numberOfChannels: this.getNumberOfChannels(),
|
||||
sampleRate: this.getSampleRate(),
|
||||
description: bytes.subarray(0, Math.ceil((bitstream.pos - 1) / 8)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { AacAudioSpecificConfig } from '../codec';
|
||||
import { Bitstream } from '../misc';
|
||||
|
||||
export type AdtsHeaderTemplate = {
|
||||
header: Uint8Array;
|
||||
bitstream: Bitstream;
|
||||
};
|
||||
|
||||
export const buildAdtsHeaderTemplate = (config: AacAudioSpecificConfig): AdtsHeaderTemplate => {
|
||||
const header = new Uint8Array(7);
|
||||
const bitstream = new Bitstream(header);
|
||||
|
||||
const { objectType, frequencyIndex, channelConfiguration } = config;
|
||||
const profile = objectType - 1;
|
||||
|
||||
bitstream.writeBits(12, 0b1111_11111111); // Syncword
|
||||
bitstream.writeBits(1, 0); // MPEG Version
|
||||
bitstream.writeBits(2, 0); // Layer
|
||||
bitstream.writeBits(1, 1); // Protection absence
|
||||
bitstream.writeBits(2, profile); // Profile
|
||||
bitstream.writeBits(4, frequencyIndex); // MPEG-4 Sampling Frequency Index
|
||||
bitstream.writeBits(1, 0); // Private bit
|
||||
bitstream.writeBits(3, channelConfiguration); // MPEG-4 Channel Configuration
|
||||
bitstream.writeBits(1, 0); // Originality
|
||||
bitstream.writeBits(1, 0); // Home
|
||||
bitstream.writeBits(1, 0); // Copyright ID bit
|
||||
bitstream.writeBits(1, 0); // Copyright ID start
|
||||
bitstream.skipBits(13); // Frame length (to be filled per packet)
|
||||
bitstream.writeBits(11, 0x7ff); // Buffer fullness
|
||||
bitstream.writeBits(2, 0); // Number of AAC frames minus 1
|
||||
// Omit CRC check
|
||||
|
||||
return { header, bitstream };
|
||||
};
|
||||
|
||||
export const writeAdtsFrameLength = (bitstream: Bitstream, frameLength: number) => {
|
||||
bitstream.pos = 30;
|
||||
bitstream.writeBits(13, frameLength);
|
||||
};
|
||||
@@ -1,25 +1,28 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { AacAudioSpecificConfig, parseAacAudioSpecificConfig, validateAudioChunkMetadata } from '../codec';
|
||||
import { parseAacAudioSpecificConfig, validateAudioChunkMetadata } from '../codec';
|
||||
import { Id3V2Writer } from '../id3';
|
||||
import { metadataTagsAreEmpty } from '../metadata';
|
||||
import { assert, Bitstream, toUint8Array } from '../misc';
|
||||
import { Muxer } from '../muxer';
|
||||
import { Output, OutputAudioTrack } from '../output';
|
||||
import { AdtsOutputFormat } from '../output-format';
|
||||
import { EncodedPacket } from '../packet';
|
||||
import { Writer } from '../writer';
|
||||
import { buildAdtsHeaderTemplate, writeAdtsFrameLength } from './adts-misc';
|
||||
|
||||
export class AdtsMuxer extends Muxer {
|
||||
private format: AdtsOutputFormat;
|
||||
private writer: Writer;
|
||||
private header = new Uint8Array(7);
|
||||
private headerBitstream = new Bitstream(this.header);
|
||||
private audioSpecificConfig: AacAudioSpecificConfig | null = null;
|
||||
private header: Uint8Array | null = null;
|
||||
private headerBitstream: Bitstream | null = null;
|
||||
private inputIsAdts: boolean | null = null;
|
||||
|
||||
constructor(output: Output, format: AdtsOutputFormat) {
|
||||
super(output);
|
||||
@@ -29,7 +32,10 @@ export class AdtsMuxer extends Muxer {
|
||||
}
|
||||
|
||||
async start() {
|
||||
// Nothing needed here
|
||||
if (!metadataTagsAreEmpty(this.output._metadataTags)) {
|
||||
const id3Writer = new Id3V2Writer(this.writer);
|
||||
id3Writer.writeId3V2Tag(this.output._metadataTags);
|
||||
}
|
||||
}
|
||||
|
||||
async getMimeType() {
|
||||
@@ -45,56 +51,54 @@ export class AdtsMuxer extends Muxer {
|
||||
packet: EncodedPacket,
|
||||
meta?: EncodedAudioChunkMetadata,
|
||||
) {
|
||||
// https://wiki.multimedia.cx/index.php/ADTS (last visited: 2025/08/17)
|
||||
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
try {
|
||||
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
|
||||
|
||||
if (!this.audioSpecificConfig) {
|
||||
// First packet - determine input format from metadata
|
||||
if (this.inputIsAdts === null) {
|
||||
validateAudioChunkMetadata(meta);
|
||||
|
||||
const description = meta?.decoderConfig?.description;
|
||||
assert(description);
|
||||
|
||||
this.audioSpecificConfig = parseAacAudioSpecificConfig(toUint8Array(description));
|
||||
// Follows from the Mediabunny Codec Registry:
|
||||
this.inputIsAdts = !description;
|
||||
|
||||
const { objectType, frequencyIndex, channelConfiguration } = this.audioSpecificConfig;
|
||||
const profile = objectType - 1;
|
||||
|
||||
this.headerBitstream.writeBits(12, 0b1111_11111111); // Syncword
|
||||
this.headerBitstream.writeBits(1, 0); // MPEG Version
|
||||
this.headerBitstream.writeBits(2, 0); // Layer
|
||||
this.headerBitstream.writeBits(1, 1); // Protection absence
|
||||
this.headerBitstream.writeBits(2, profile); // Profile
|
||||
this.headerBitstream.writeBits(4, frequencyIndex); // MPEG-4 Sampling Frequency Index
|
||||
this.headerBitstream.writeBits(1, 0); // Private bit
|
||||
this.headerBitstream.writeBits(3, channelConfiguration); // MPEG-4 Channel Configuration
|
||||
this.headerBitstream.writeBits(1, 0); // Originality
|
||||
this.headerBitstream.writeBits(1, 0); // Home
|
||||
this.headerBitstream.writeBits(1, 0); // Copyright ID bit
|
||||
this.headerBitstream.writeBits(1, 0); // Copyright ID start
|
||||
this.headerBitstream.skipBits(13); // Frame length
|
||||
this.headerBitstream.writeBits(11, 0x7ff); // Buffer fullness
|
||||
this.headerBitstream.writeBits(2, 0); // Number of AAC frames minus 1
|
||||
// Omit CRC check
|
||||
if (!this.inputIsAdts) {
|
||||
const config = parseAacAudioSpecificConfig(toUint8Array(description!));
|
||||
const template = buildAdtsHeaderTemplate(config);
|
||||
this.header = template.header;
|
||||
this.headerBitstream = template.bitstream;
|
||||
}
|
||||
}
|
||||
|
||||
const frameLength = packet.data.byteLength + this.header.byteLength;
|
||||
this.headerBitstream.pos = 30;
|
||||
this.headerBitstream.writeBits(13, frameLength);
|
||||
if (this.inputIsAdts) {
|
||||
// Packets are already ADTS frames, write them directly
|
||||
const startPos = this.writer.getPos();
|
||||
this.writer.write(packet.data);
|
||||
|
||||
const startPos = this.writer.getPos();
|
||||
this.writer.write(this.header);
|
||||
this.writer.write(packet.data);
|
||||
if (this.format._options.onFrame) {
|
||||
this.format._options.onFrame(packet.data, startPos);
|
||||
}
|
||||
} else {
|
||||
assert(this.header);
|
||||
|
||||
if (this.format._options.onFrame) {
|
||||
const frameBytes = new Uint8Array(frameLength);
|
||||
frameBytes.set(this.header, 0);
|
||||
frameBytes.set(packet.data, this.header.byteLength);
|
||||
// Packets are raw AAC, we gotta turn it into ADTS
|
||||
const frameLength = packet.data.byteLength + this.header.byteLength;
|
||||
writeAdtsFrameLength(this.headerBitstream!, frameLength);
|
||||
|
||||
this.format._options.onFrame(frameBytes, startPos);
|
||||
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();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -9,10 +9,10 @@
|
||||
import { Bitstream } from '../misc';
|
||||
import { FileSlice, readBytes } from '../reader';
|
||||
|
||||
export const MIN_FRAME_HEADER_SIZE = 7;
|
||||
export const MAX_FRAME_HEADER_SIZE = 9;
|
||||
export const MIN_ADTS_FRAME_HEADER_SIZE = 7;
|
||||
export const MAX_ADTS_FRAME_HEADER_SIZE = 9;
|
||||
|
||||
export type FrameHeader = {
|
||||
export type AdtsFrameHeader = {
|
||||
objectType: number;
|
||||
samplingFrequencyIndex: number;
|
||||
channelConfiguration: number;
|
||||
@@ -22,7 +22,7 @@ export type FrameHeader = {
|
||||
startPos: number;
|
||||
};
|
||||
|
||||
export const readFrameHeader = (slice: FileSlice): FrameHeader | null => {
|
||||
export const readAdtsFrameHeader = (slice: FileSlice): AdtsFrameHeader | null => {
|
||||
// https://wiki.multimedia.cx/index.php/ADTS (last visited: 2025/08/17)
|
||||
|
||||
const startPos = slice.filePos;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -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.
|
||||
@@ -116,26 +118,26 @@ export type SubtitleCodec = typeof SUBTITLE_CODECS[number];
|
||||
export type MediaCodec = VideoCodec | AudioCodec | SubtitleCodec;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Advanced_Video_Coding
|
||||
const AVC_LEVEL_TABLE = [
|
||||
{ maxMacroblocks: 99, maxBitrate: 64000, level: 0x0A }, // Level 1
|
||||
{ maxMacroblocks: 396, maxBitrate: 192000, level: 0x0B }, // Level 1.1
|
||||
{ maxMacroblocks: 396, maxBitrate: 384000, level: 0x0C }, // Level 1.2
|
||||
{ maxMacroblocks: 396, maxBitrate: 768000, level: 0x0D }, // Level 1.3
|
||||
{ maxMacroblocks: 396, maxBitrate: 2000000, level: 0x14 }, // Level 2
|
||||
{ maxMacroblocks: 792, maxBitrate: 4000000, level: 0x15 }, // Level 2.1
|
||||
{ maxMacroblocks: 1620, maxBitrate: 4000000, level: 0x16 }, // Level 2.2
|
||||
{ maxMacroblocks: 1620, maxBitrate: 10000000, level: 0x1E }, // Level 3
|
||||
{ maxMacroblocks: 3600, maxBitrate: 14000000, level: 0x1F }, // Level 3.1
|
||||
{ maxMacroblocks: 5120, maxBitrate: 20000000, level: 0x20 }, // Level 3.2
|
||||
{ maxMacroblocks: 8192, maxBitrate: 20000000, level: 0x28 }, // Level 4
|
||||
{ maxMacroblocks: 8192, maxBitrate: 50000000, level: 0x29 }, // Level 4.1
|
||||
{ maxMacroblocks: 8704, maxBitrate: 50000000, level: 0x2A }, // Level 4.2
|
||||
{ maxMacroblocks: 22080, maxBitrate: 135000000, level: 0x32 }, // Level 5
|
||||
{ maxMacroblocks: 36864, maxBitrate: 240000000, level: 0x33 }, // Level 5.1
|
||||
{ maxMacroblocks: 36864, maxBitrate: 240000000, level: 0x34 }, // Level 5.2
|
||||
{ maxMacroblocks: 139264, maxBitrate: 240000000, level: 0x3C }, // Level 6
|
||||
{ maxMacroblocks: 139264, maxBitrate: 480000000, level: 0x3D }, // Level 6.1
|
||||
{ maxMacroblocks: 139264, maxBitrate: 800000000, level: 0x3E }, // Level 6.2
|
||||
export const AVC_LEVEL_TABLE = [
|
||||
{ maxMacroblocks: 99, maxBitrate: 64000, maxDpbMbs: 396, level: 0x0A }, // Level 1
|
||||
{ maxMacroblocks: 396, maxBitrate: 192000, maxDpbMbs: 900, level: 0x0B }, // Level 1.1
|
||||
{ maxMacroblocks: 396, maxBitrate: 384000, maxDpbMbs: 2376, level: 0x0C }, // Level 1.2
|
||||
{ maxMacroblocks: 396, maxBitrate: 768000, maxDpbMbs: 2376, level: 0x0D }, // Level 1.3
|
||||
{ maxMacroblocks: 396, maxBitrate: 2000000, maxDpbMbs: 2376, level: 0x14 }, // Level 2
|
||||
{ maxMacroblocks: 792, maxBitrate: 4000000, maxDpbMbs: 4752, level: 0x15 }, // Level 2.1
|
||||
{ maxMacroblocks: 1620, maxBitrate: 4000000, maxDpbMbs: 8100, level: 0x16 }, // Level 2.2
|
||||
{ maxMacroblocks: 1620, maxBitrate: 10000000, maxDpbMbs: 8100, level: 0x1E }, // Level 3
|
||||
{ maxMacroblocks: 3600, maxBitrate: 14000000, maxDpbMbs: 18000, level: 0x1F }, // Level 3.1
|
||||
{ maxMacroblocks: 5120, maxBitrate: 20000000, maxDpbMbs: 20480, level: 0x20 }, // Level 3.2
|
||||
{ maxMacroblocks: 8192, maxBitrate: 20000000, maxDpbMbs: 32768, level: 0x28 }, // Level 4
|
||||
{ maxMacroblocks: 8192, maxBitrate: 50000000, maxDpbMbs: 32768, level: 0x29 }, // Level 4.1
|
||||
{ maxMacroblocks: 8704, maxBitrate: 50000000, maxDpbMbs: 34816, level: 0x2A }, // Level 4.2
|
||||
{ maxMacroblocks: 22080, maxBitrate: 135000000, maxDpbMbs: 110400, level: 0x32 }, // Level 5
|
||||
{ maxMacroblocks: 36864, maxBitrate: 240000000, maxDpbMbs: 184320, level: 0x33 }, // Level 5.1
|
||||
{ maxMacroblocks: 36864, maxBitrate: 240000000, maxDpbMbs: 184320, level: 0x34 }, // Level 5.2
|
||||
{ maxMacroblocks: 139264, maxBitrate: 240000000, maxDpbMbs: 696320, level: 0x3C }, // Level 6
|
||||
{ maxMacroblocks: 139264, maxBitrate: 480000000, maxDpbMbs: 696320, level: 0x3D }, // Level 6.1
|
||||
{ maxMacroblocks: 139264, maxBitrate: 800000000, maxDpbMbs: 696320, level: 0x3E }, // Level 6.2
|
||||
];
|
||||
|
||||
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding
|
||||
@@ -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;
|
||||
}
|
||||
@@ -537,6 +543,7 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe
|
||||
|
||||
export type AacCodecInfo = {
|
||||
isMpeg2: boolean;
|
||||
objectType: number | null;
|
||||
};
|
||||
|
||||
export const extractAudioCodecString = (trackInfo: {
|
||||
@@ -554,8 +561,15 @@ export const extractAudioCodecString = (trackInfo: {
|
||||
if (aacCodecInfo.isMpeg2) {
|
||||
return 'mp4a.67';
|
||||
} else {
|
||||
const audioSpecificConfig = parseAacAudioSpecificConfig(codecDescription);
|
||||
return `mp4a.40.${audioSpecificConfig.objectType}`;
|
||||
let objectType: number;
|
||||
if (aacCodecInfo.objectType !== null) {
|
||||
objectType = aacCodecInfo.objectType;
|
||||
} else {
|
||||
const audioSpecificConfig = parseAacAudioSpecificConfig(codecDescription);
|
||||
objectType = audioSpecificConfig.objectType;
|
||||
}
|
||||
|
||||
return `mp4a.40.${objectType}`;
|
||||
}
|
||||
} else if (codec === 'mp3') {
|
||||
return 'mp3';
|
||||
@@ -565,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;
|
||||
}
|
||||
@@ -734,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') {
|
||||
@@ -811,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) {
|
||||
@@ -925,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) {
|
||||
@@ -946,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) {
|
||||
@@ -985,12 +1009,9 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
|
||||
);
|
||||
}
|
||||
|
||||
if (!metadata.decoderConfig.description) {
|
||||
throw new TypeError(
|
||||
'Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be'
|
||||
+ ' an AudioSpecificConfig as specified in ISO 14496-3.',
|
||||
);
|
||||
}
|
||||
// `description` may or may not be set, depending on if the format is AAC or ADTS, so don't perform any
|
||||
// validation for it.
|
||||
// https://www.w3.org/TR/webcodecs-aac-codec-registration
|
||||
} else if (metadata.decoderConfig.codec.startsWith('mp3') || metadata.decoderConfig.codec.startsWith('mp4a')) {
|
||||
// MP3-specific validation
|
||||
|
||||
@@ -1046,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')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -66,7 +66,8 @@ export type ConversionOptions = {
|
||||
* Video-specific options. When passing an object, the same options are applied to all video tracks. When passing a
|
||||
* function, it will be invoked for each video track and is expected to return or resolve to the options
|
||||
* for that specific track. The function is passed an instance of {@link InputVideoTrack} as well as a number `n`,
|
||||
* which is the 1-based index of the track in the list of all video tracks.
|
||||
* which is the 1-based index of the track in the list of all video tracks. Using `n` is deprecated, prefer the
|
||||
* identical `track.number` instead.
|
||||
*/
|
||||
video?: ConversionVideoOptions
|
||||
| ((track: InputVideoTrack, n: number) => MaybePromise<ConversionVideoOptions | undefined>);
|
||||
@@ -75,7 +76,8 @@ export type ConversionOptions = {
|
||||
* Audio-specific options. When passing an object, the same options are applied to all audio tracks. When passing a
|
||||
* function, it will be invoked for each audio track and is expected to return or resolve to the options
|
||||
* for that specific track. The function is passed an instance of {@link InputAudioTrack} as well as a number `n`,
|
||||
* which is the 1-based index of the track in the list of all audio tracks.
|
||||
* which is the 1-based index of the track in the list of all audio tracks. Using `n` is deprecated, prefer the
|
||||
* identical `track.number` instead.
|
||||
*/
|
||||
audio?: ConversionAudioOptions
|
||||
| ((track: InputAudioTrack, n: number) => MaybePromise<ConversionAudioOptions | undefined>);
|
||||
@@ -84,7 +86,7 @@ export type ConversionOptions = {
|
||||
trim?: {
|
||||
/**
|
||||
* The time in the input file in seconds at which the output file should start. Must be less than `end`.
|
||||
* Defaults to 0 when omitted.
|
||||
* When omitted, defaults to the start timestamp of the input or to 0, whichever is higher.
|
||||
*/
|
||||
start?: number;
|
||||
/**
|
||||
@@ -143,6 +145,12 @@ export type ConversionVideoOptions = {
|
||||
* This rotation is _in addition to_ the natural rotation of the input video as specified in input file's metadata.
|
||||
*/
|
||||
rotate?: Rotation;
|
||||
/**
|
||||
* Defaults to `true`. When enabaled, Mediabunny will use the rotation metadata in the output file to perform video
|
||||
* rotation whenever possible. Set this field to `false` if you want to ensure the output file does not make use of
|
||||
* rotation metadata and that any rotation is baked into the video frames directly.
|
||||
*/
|
||||
allowRotationMetadata?: boolean;
|
||||
/**
|
||||
* Specifies the rectangular region of the input video to crop to. The crop region will automatically be clamped to
|
||||
* the dimensions of the input video track. Cropping is performed after rotation but before resizing.
|
||||
@@ -180,6 +188,11 @@ export type ConversionVideoOptions = {
|
||||
* Setting this fields forces a transcode.
|
||||
*/
|
||||
keyFrameInterval?: number;
|
||||
/**
|
||||
* A hint that configures the hardware acceleration method used when transcoding. This is best left on
|
||||
* `'no-preference'`, the default.
|
||||
*/
|
||||
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
|
||||
/** When `true`, video will always be re-encoded instead of directly copying over the encoded samples. */
|
||||
forceTranscode?: boolean;
|
||||
/**
|
||||
@@ -304,6 +317,9 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
|
||||
if (videoOptions?.rotate !== undefined && ![0, 90, 180, 270].includes(videoOptions.rotate)) {
|
||||
throw new TypeError('options.video.rotate, when provided, must be 0, 90, 180 or 270.');
|
||||
}
|
||||
if (videoOptions?.allowRotationMetadata !== undefined && typeof videoOptions.allowRotationMetadata !== 'boolean') {
|
||||
throw new TypeError('options.video.allowRotationMetadata, when provided, must be a boolean.');
|
||||
}
|
||||
if (videoOptions?.crop !== undefined) {
|
||||
validateCropRectangle(videoOptions.crop, 'options.video.');
|
||||
}
|
||||
@@ -337,6 +353,15 @@ const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined)
|
||||
) {
|
||||
throw new TypeError('options.video.processedHeight, when provided, must be a positive integer.');
|
||||
}
|
||||
if (
|
||||
videoOptions?.hardwareAcceleration !== undefined
|
||||
&& !['no-preference', 'prefer-hardware', 'prefer-software'].includes(videoOptions.hardwareAcceleration)
|
||||
) {
|
||||
throw new TypeError(
|
||||
'options.video.hardwareAcceleration, when provided, must be \'no-preference\', \'prefer-hardware\' or'
|
||||
+ ' \'prefer-software\'.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const validateAudioOptions = (audioOptions: ConversionAudioOptions | undefined) => {
|
||||
@@ -439,9 +464,9 @@ export class Conversion {
|
||||
/** @internal */
|
||||
_options: ConversionOptions;
|
||||
/** @internal */
|
||||
_startTimestamp: number;
|
||||
_startTimestamp!: number;
|
||||
/** @internal */
|
||||
_endTimestamp: number;
|
||||
_endTimestamp!: number;
|
||||
|
||||
/** @internal */
|
||||
_addedCounts: Record<TrackType, number> = {
|
||||
@@ -539,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
|
||||
@@ -569,9 +594,6 @@ export class Conversion {
|
||||
this.input = options.input;
|
||||
this.output = options.output;
|
||||
|
||||
this._startTimestamp = options.trim?.start ?? 0;
|
||||
this._endTimestamp = options.trim?.end ?? Infinity;
|
||||
|
||||
const { promise: started, resolve: start } = promiseWithResolvers();
|
||||
this._started = started;
|
||||
this._start = start;
|
||||
@@ -579,6 +601,14 @@ export class Conversion {
|
||||
|
||||
/** @internal */
|
||||
async _init() {
|
||||
this._startTimestamp = this._options.trim?.start ?? Math.max(
|
||||
await this.input.getFirstTimestamp(),
|
||||
// Samples can also have negative timestamps, but the meaning typically is "don't present me", so let's cut
|
||||
// those out by default.
|
||||
0,
|
||||
);
|
||||
this._endTimestamp = Math.max(this._options.trim?.end ?? Infinity, this._startTimestamp);
|
||||
|
||||
const inputTracks = await this.input.getTracks();
|
||||
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
|
||||
|
||||
@@ -743,6 +773,13 @@ export class Conversion {
|
||||
`\nThe @mediabunny/mp3-encoder extension package provides support for encoding MP3.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (codecs.includes('ac3') || codecs.includes('eac3')) {
|
||||
elements.push(
|
||||
'\nThe @mediabunny/ac3 extension package provides support'
|
||||
+ ' for encoding and decoding AC-3/E-AC-3.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
elements.push('\nCheck the discardedTracks field for more info.');
|
||||
}
|
||||
@@ -771,9 +808,13 @@ export class Conversion {
|
||||
this._executed = true;
|
||||
|
||||
if (this.onProgress) {
|
||||
// Compute duration using only the utilized tracks
|
||||
const durationPromises = this.utilizedTracks.map(x => x.computeDuration());
|
||||
const duration = Math.max(0, ...await Promise.all(durationPromises));
|
||||
|
||||
this._computeProgress = true;
|
||||
this._totalDuration = Math.min(
|
||||
(await this.input.computeDuration()) - this._startTimestamp,
|
||||
duration - this._startTimestamp,
|
||||
this._endTimestamp - this._startTimestamp,
|
||||
);
|
||||
|
||||
@@ -799,7 +840,7 @@ export class Conversion {
|
||||
}
|
||||
|
||||
if (this._canceled) {
|
||||
await new Promise(() => {}); // Never resolve
|
||||
throw new ConversionCanceledError();
|
||||
}
|
||||
|
||||
await this.output.finalize();
|
||||
@@ -809,7 +850,10 @@ export class Conversion {
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancels the conversion process. Does nothing if the conversion is already complete. */
|
||||
/**
|
||||
* Cancels the conversion process, causing any ongoing `execute` call to throw a `ConversionCanceledError`.
|
||||
* Does nothing if the conversion is already complete.
|
||||
*/
|
||||
async cancel() {
|
||||
if (this.output.state === 'finalizing' || this.output.state === 'finalized') {
|
||||
return;
|
||||
@@ -838,7 +882,8 @@ export class Conversion {
|
||||
let videoSource: VideoSource;
|
||||
|
||||
const totalRotation = normalizeRotation(track.rotation + (trackOptions.rotate ?? 0));
|
||||
const outputSupportsRotation = this.output.format.supportsVideoRotationMetadata;
|
||||
const canUseRotationMetadata = this.output.format.supportsVideoRotationMetadata
|
||||
&& (trackOptions.allowRotationMetadata ?? true);
|
||||
|
||||
const [rotatedWidth, rotatedHeight] = totalRotation % 180 === 0
|
||||
? [track.codedWidth, track.codedHeight]
|
||||
@@ -873,8 +918,7 @@ export class Conversion {
|
||||
|
||||
const firstTimestamp = await track.getFirstTimestamp();
|
||||
const needsTranscode = !!trackOptions.forceTranscode
|
||||
|| this._startTimestamp > 0
|
||||
|| firstTimestamp < 0
|
||||
|| firstTimestamp < this._startTimestamp
|
||||
|| !!trackOptions.frameRate
|
||||
|| trackOptions.keyFrameInterval !== undefined
|
||||
|| trackOptions.process !== undefined;
|
||||
@@ -883,7 +927,7 @@ export class Conversion {
|
||||
// TODO This is suboptimal: Forcing a rerender when both rotation and process are set is not
|
||||
// performance-optimal, but right now there's no other way because we can't change the track rotation
|
||||
// metadata after the output has already started. Should be possible with API changes in v2, though!
|
||||
|| (totalRotation !== 0 && (!outputSupportsRotation || trackOptions.process !== undefined))
|
||||
|| (totalRotation !== 0 && (!canUseRotationMetadata || trackOptions.process !== undefined))
|
||||
|| !!crop;
|
||||
|
||||
const alpha = trackOptions.alpha ?? 'discard';
|
||||
@@ -916,17 +960,19 @@ export class Conversion {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alpha === 'discard') {
|
||||
// Feels hacky given that the rest of the packet is readonly. But, works for now.
|
||||
delete packet.sideData.alpha;
|
||||
delete packet.sideData.alphaByteLength;
|
||||
}
|
||||
const modifiedPacket = packet.clone({
|
||||
timestamp: packet.timestamp - this._startTimestamp,
|
||||
sideData: alpha === 'discard'
|
||||
? {} // Remove alpha side data
|
||||
: packet.sideData,
|
||||
});
|
||||
assert(modifiedPacket.timestamp >= 0);
|
||||
|
||||
this._reportProgress(track.id, packet.timestamp);
|
||||
await source.add(packet, meta);
|
||||
this._reportProgress(track.id, modifiedPacket.timestamp);
|
||||
await source.add(modifiedPacket, meta);
|
||||
|
||||
if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
|
||||
await this._synchronizer.wait(packet.timestamp);
|
||||
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
|
||||
await this._synchronizer.wait(modifiedPacket.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,6 +1020,7 @@ export class Conversion {
|
||||
keyFrameInterval: trackOptions.keyFrameInterval,
|
||||
sizeChangeBehavior: trackOptions.fit ?? 'passThrough',
|
||||
alpha,
|
||||
hardwareAcceleration: trackOptions.hardwareAcceleration,
|
||||
};
|
||||
|
||||
const source = new VideoSampleSource(encodingConfig);
|
||||
@@ -1134,6 +1181,7 @@ export class Conversion {
|
||||
|
||||
for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
|
||||
if (this._canceled) {
|
||||
sample.close();
|
||||
lastSample?.close();
|
||||
return;
|
||||
}
|
||||
@@ -1285,8 +1333,8 @@ export class Conversion {
|
||||
let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
|
||||
let needsResample = numberOfChannels !== originalNumberOfChannels
|
||||
|| sampleRate !== originalSampleRate
|
||||
|| this._startTimestamp > 0
|
||||
|| firstTimestamp < 0;
|
||||
|| firstTimestamp < this._startTimestamp
|
||||
|| (firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData);
|
||||
|
||||
let audioCodecs = this.output.format.getSupportedAudioCodecs();
|
||||
if (
|
||||
@@ -1317,11 +1365,16 @@ export class Conversion {
|
||||
return;
|
||||
}
|
||||
|
||||
this._reportProgress(track.id, packet.timestamp);
|
||||
await source.add(packet, meta);
|
||||
const modifiedPacket = packet.clone({
|
||||
timestamp: packet.timestamp - this._startTimestamp,
|
||||
});
|
||||
assert(modifiedPacket.timestamp >= 0);
|
||||
|
||||
if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
|
||||
await this._synchronizer.wait(packet.timestamp);
|
||||
this._reportProgress(track.id, modifiedPacket.timestamp);
|
||||
await source.add(modifiedPacket, meta);
|
||||
|
||||
if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) {
|
||||
await this._synchronizer.wait(modifiedPacket.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1416,9 +1469,13 @@ export class Conversion {
|
||||
const sink = new AudioSampleSink(track);
|
||||
for await (const sample of sink.samples(undefined, this._endTimestamp)) {
|
||||
if (this._canceled) {
|
||||
sample.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Offset the timestamp as needed
|
||||
sample.setTimestamp(sample.timestamp - this._startTimestamp);
|
||||
|
||||
await this._registerAudioSample(track, trackOptions, source, sample);
|
||||
sample.close();
|
||||
}
|
||||
@@ -1526,6 +1583,7 @@ export class Conversion {
|
||||
|
||||
for await (const sample of iterator) {
|
||||
if (this._canceled) {
|
||||
sample.close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1564,6 +1622,19 @@ export class Conversion {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a conversion couldn't complete due to being canceled.
|
||||
* @group Conversion
|
||||
* @public
|
||||
*/
|
||||
export class ConversionCanceledError extends Error {
|
||||
/** Creates a new {@link ConversionCanceledError}. */
|
||||
constructor(message = 'Conversion has been canceled.') {
|
||||
super(message);
|
||||
this.name = 'ConversionCanceledError';
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_TIMESTAMP_GAP = 5;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -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];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -529,6 +529,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
getCodec() {
|
||||
return 'flac' as const;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -9,6 +9,17 @@
|
||||
/// <reference types="dom-mediacapture-transform" preserve="true" />
|
||||
/// <reference types="dom-webcodecs" preserve="true" />
|
||||
|
||||
const MEDIABUNNY_LOADED_SYMBOL = Symbol.for('mediabunny loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
'[WARNING]\nMediabunny was loaded twice.'
|
||||
+ ' This will likely cause Mediabunny not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of Mediabunny,'
|
||||
+ ' or if something is being bundled incorrectly.',
|
||||
);
|
||||
}
|
||||
(globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL] = true;
|
||||
|
||||
export {
|
||||
Output,
|
||||
OutputOptions,
|
||||
@@ -31,6 +42,8 @@ export {
|
||||
Mp3OutputFormat,
|
||||
Mp3OutputFormatOptions,
|
||||
Mp4OutputFormat,
|
||||
MpegTsOutputFormat,
|
||||
MpegTsOutputFormatOptions,
|
||||
OggOutputFormat,
|
||||
OggOutputFormatOptions,
|
||||
WavOutputFormat,
|
||||
@@ -126,25 +139,27 @@ export {
|
||||
export {
|
||||
InputFormat,
|
||||
AdtsInputFormat,
|
||||
FlacInputFormat,
|
||||
IsobmffInputFormat,
|
||||
MatroskaInputFormat,
|
||||
Mp3InputFormat,
|
||||
Mp4InputFormat,
|
||||
MpegTsInputFormat,
|
||||
OggInputFormat,
|
||||
QuickTimeInputFormat,
|
||||
WaveInputFormat,
|
||||
WebMInputFormat,
|
||||
FlacInputFormat,
|
||||
ALL_FORMATS,
|
||||
ADTS,
|
||||
FLAC,
|
||||
MATROSKA,
|
||||
MP3,
|
||||
MP4,
|
||||
MPEG_TS,
|
||||
OGG,
|
||||
QTFF,
|
||||
WAVE,
|
||||
WEBM,
|
||||
FLAC,
|
||||
} from './input-format';
|
||||
export {
|
||||
Input,
|
||||
@@ -168,7 +183,10 @@ export {
|
||||
AudioSampleCopyToOptions,
|
||||
VideoSample,
|
||||
VideoSampleInit,
|
||||
VideoSamplePixelFormat,
|
||||
VideoSampleColorSpace,
|
||||
CropRectangle,
|
||||
VIDEO_SAMPLE_PIXEL_FORMATS,
|
||||
} from './sample';
|
||||
export {
|
||||
AudioBufferSink,
|
||||
@@ -187,6 +205,7 @@ export {
|
||||
ConversionOptions,
|
||||
ConversionVideoOptions,
|
||||
ConversionAudioOptions,
|
||||
ConversionCanceledError,
|
||||
DiscardedTrack,
|
||||
} from './conversion';
|
||||
export {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -21,15 +21,17 @@ import {
|
||||
} from './matroska/ebml';
|
||||
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
|
||||
import { Mp3Demuxer } from './mp3/mp3-demuxer';
|
||||
import { FRAME_HEADER_SIZE } from '../shared/mp3-misc';
|
||||
import { FRAME_HEADER_SIZE, getXingOffset, INFO, XING } from '../shared/mp3-misc';
|
||||
import { ID3_V2_HEADER_SIZE, readId3V2Header } from './id3';
|
||||
import { readNextFrameHeader } from './mp3/mp3-reader';
|
||||
import { readNextMp3FrameHeader } from './mp3/mp3-reader';
|
||||
import { OggDemuxer } from './ogg/ogg-demuxer';
|
||||
import { WaveDemuxer } from './wave/wave-demuxer';
|
||||
import { MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts/adts-reader';
|
||||
import { MAX_ADTS_FRAME_HEADER_SIZE, MIN_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from './adts/adts-reader';
|
||||
import { AdtsDemuxer } from './adts/adts-demuxer';
|
||||
import { readAscii } from './reader';
|
||||
import { readAscii, readBytes, readU32Be } from './reader';
|
||||
import { FlacDemuxer } from './flac/flac-demuxer';
|
||||
import { MpegTsDemuxer } from './mpeg-ts/mpeg-ts-demuxer';
|
||||
import { TS_PACKET_SIZE } from './mpeg-ts/mpeg-ts-misc';
|
||||
|
||||
/**
|
||||
* Base class representing an input media file format.
|
||||
@@ -155,7 +157,7 @@ export class MatroskaInputFormat extends InputFormat {
|
||||
}
|
||||
|
||||
const dataSize = readElementSize(headerSlice);
|
||||
if (dataSize === null) {
|
||||
if (typeof dataSize !== 'number') {
|
||||
return false; // Miss me with that shit
|
||||
}
|
||||
|
||||
@@ -171,7 +173,7 @@ export class MatroskaInputFormat extends InputFormat {
|
||||
|
||||
const { id, size } = header;
|
||||
const dataStartPos = dataSlice.filePos;
|
||||
if (size === null) return false;
|
||||
if (size === undefined) return false;
|
||||
|
||||
switch (id) {
|
||||
case EBMLId.EBMLVersion: {
|
||||
@@ -259,12 +261,7 @@ export class WebMInputFormat extends MatroskaInputFormat {
|
||||
export class Mp3InputFormat extends InputFormat {
|
||||
/** @internal */
|
||||
async _canReadInput(input: Input) {
|
||||
let slice = input._reader.requestSlice(0, 10);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
let currentPos = 0;
|
||||
let id3V2HeaderFound = false;
|
||||
|
||||
while (true) {
|
||||
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
@@ -276,17 +273,26 @@ export class Mp3InputFormat extends InputFormat {
|
||||
break;
|
||||
}
|
||||
|
||||
id3V2HeaderFound = true;
|
||||
currentPos = slice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
const firstResult = await readNextFrameHeader(input._reader, currentPos, currentPos + 4096);
|
||||
const firstResult = await readNextMp3FrameHeader(input._reader, currentPos, currentPos + 4096);
|
||||
if (!firstResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id3V2HeaderFound) {
|
||||
// If there was an ID3v2 tag at the start, we can be pretty sure this is MP3 by now
|
||||
const firstHeader = firstResult.header;
|
||||
const xingOffset = getXingOffset(firstHeader.mpegVersionId, firstHeader.channel);
|
||||
|
||||
let slice = input._reader.requestSlice(firstResult.startPos + xingOffset, 4);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
const word = readU32Be(slice);
|
||||
const isXing = word === XING || word === INFO;
|
||||
|
||||
if (isXing) {
|
||||
// Gotta be MP3
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -294,12 +300,11 @@ export class Mp3InputFormat extends InputFormat {
|
||||
|
||||
// Fine, we found one frame header, but we're still not entirely sure this is MP3. Let's check if we can find
|
||||
// another header right after it:
|
||||
const secondResult = await readNextFrameHeader(input._reader, currentPos, currentPos + FRAME_HEADER_SIZE);
|
||||
const secondResult = await readNextMp3FrameHeader(input._reader, currentPos, currentPos + FRAME_HEADER_SIZE);
|
||||
if (!secondResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstHeader = firstResult.header;
|
||||
const secondHeader = secondResult.header;
|
||||
|
||||
// In a well-formed MP3 file, we'd expect these two frames to share some similarities:
|
||||
@@ -439,20 +444,45 @@ export class FlacInputFormat extends InputFormat {
|
||||
export class AdtsInputFormat extends InputFormat {
|
||||
/** @internal */
|
||||
async _canReadInput(input: Input) {
|
||||
let slice = input._reader.requestSliceRange(0, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE);
|
||||
let currentPos = 0;
|
||||
|
||||
while (true) {
|
||||
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const id3V2Header = readId3V2Header(slice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentPos = slice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
let slice = input._reader.requestSliceRange(
|
||||
currentPos,
|
||||
MIN_ADTS_FRAME_HEADER_SIZE,
|
||||
MAX_ADTS_FRAME_HEADER_SIZE,
|
||||
);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
const firstHeader = readFrameHeader(slice);
|
||||
const firstHeader = readAdtsFrameHeader(slice);
|
||||
if (!firstHeader) {
|
||||
return false;
|
||||
}
|
||||
|
||||
slice = input._reader.requestSliceRange(firstHeader.frameLength, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE);
|
||||
currentPos += firstHeader.frameLength;
|
||||
|
||||
slice = input._reader.requestSliceRange(
|
||||
currentPos,
|
||||
MIN_ADTS_FRAME_HEADER_SIZE,
|
||||
MAX_ADTS_FRAME_HEADER_SIZE,
|
||||
);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
const secondHeader = readFrameHeader(slice);
|
||||
const secondHeader = readAdtsFrameHeader(slice);
|
||||
if (!secondHeader) {
|
||||
return false;
|
||||
}
|
||||
@@ -476,6 +506,52 @@ export class AdtsInputFormat extends InputFormat {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MPEG Transport Stream (MPEG-TS) file format.
|
||||
*
|
||||
* Do not instantiate this class; use the {@link MPEG_TS} singleton instead.
|
||||
*
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export class MpegTsInputFormat extends InputFormat {
|
||||
/** @internal */
|
||||
async _canReadInput(input: Input) {
|
||||
const lengthToCheck = TS_PACKET_SIZE + 16 + 1;
|
||||
let slice = input._reader.requestSlice(0, lengthToCheck);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
const bytes = readBytes(slice, lengthToCheck);
|
||||
|
||||
if (bytes[0] === 0x47 && bytes[TS_PACKET_SIZE] === 0x47) {
|
||||
// Regular MPEG-TS
|
||||
return true;
|
||||
} else if (bytes[0] === 0x47 && bytes[TS_PACKET_SIZE + 16] === 0x47) {
|
||||
// MPEG-TS with Forward Error Correction
|
||||
return true;
|
||||
} else if (bytes[4] === 0x47 && bytes[4 + TS_PACKET_SIZE] === 0x47) {
|
||||
// MPEG-2-TS (DVHS)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_createDemuxer(input: Input) {
|
||||
return new MpegTsDemuxer(input);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'MPEG Transport Stream';
|
||||
}
|
||||
|
||||
get mimeType() {
|
||||
return 'video/MP2T';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MP4 input format singleton.
|
||||
* @group Input formats
|
||||
@@ -532,10 +608,17 @@ export const ADTS = /* #__PURE__ */ new AdtsInputFormat();
|
||||
*/
|
||||
export const FLAC = /* #__PURE__ */ new FlacInputFormat();
|
||||
|
||||
/**
|
||||
* MPEG-TS input format singleton.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const MPEG_TS = /* #__PURE__ */ new MpegTsInputFormat();
|
||||
|
||||
/**
|
||||
* List of all input format singletons. If you don't need to support all input formats, you should specify the
|
||||
* formats individually for better tree shaking.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export const ALL_FORMATS: InputFormat[] = [MP4, QTFF, MATROSKA, WEBM, WAVE, OGG, FLAC, MP3, ADTS];
|
||||
export const ALL_FORMATS: InputFormat[] = [MP4, QTFF, MATROSKA, WEBM, WAVE, OGG, FLAC, MP3, ADTS, MPEG_TS];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -32,6 +32,7 @@ export type PacketStats = {
|
||||
|
||||
export interface InputTrackBacking {
|
||||
getId(): number;
|
||||
getNumber(): number;
|
||||
getCodec(): MediaCodec | null;
|
||||
getInternalCodecId(): string | number | Uint8Array | null;
|
||||
getName(): string | null;
|
||||
@@ -94,6 +95,15 @@ export abstract class InputTrack {
|
||||
return this._backing.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* The 1-based index of this track among all tracks of the same type in the input file. For example, the first
|
||||
* video track has number 1, the second video track has number 2, and so on. The index refers to the order in
|
||||
* which the tracks are returned by {@link Input.getTracks}.
|
||||
*/
|
||||
get number() {
|
||||
return this._backing.getNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* The identifier of the codec used internally by the container. It is not homogenized by Mediabunny
|
||||
* and depends entirely on the container format.
|
||||
@@ -104,6 +114,7 @@ export abstract class InputTrack {
|
||||
* - For Matroska files, this field returns the value of the `CodecID` element.
|
||||
* - For WAVE files, this field returns the value of the format tag in the `'fmt '` chunk.
|
||||
* - For ADTS files, this field contains the `MPEG-4 Audio Object Type`.
|
||||
* - For MPEG-TS files, this field contains the `streamType` value from the Program Map Table.
|
||||
* - In all other cases, this field is `null`.
|
||||
*/
|
||||
get internalCodecId() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -118,6 +118,20 @@ export class Input<S extends Source = Source> implements Disposable {
|
||||
return demuxer.computeDuration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp at which the input file starts. More precisely, returns the smallest starting timestamp
|
||||
* among all tracks.
|
||||
*/
|
||||
async getFirstTimestamp() {
|
||||
const tracks = await this.getTracks();
|
||||
if (tracks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const firstTimestamps = await Promise.all(tracks.map(x => x.getFirstTimestamp()));
|
||||
return Math.min(...firstTimestamps);
|
||||
}
|
||||
|
||||
/** Returns the list of all tracks of this input file. */
|
||||
async getTracks() {
|
||||
const demuxer = await this._getDemuxer();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
FlacBlockType,
|
||||
HevcDecoderConfigurationRecord,
|
||||
Vp9CodecInfo,
|
||||
parseEac3Config,
|
||||
getEac3SampleRate,
|
||||
getEac3ChannelCount,
|
||||
AC3_ACMOD_CHANNEL_COUNTS,
|
||||
} from '../codec-data';
|
||||
import { Demuxer } from '../demuxer';
|
||||
import { Input } from '../input';
|
||||
@@ -87,6 +91,7 @@ import {
|
||||
readAscii,
|
||||
} from '../reader';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDisposition } from '../metadata';
|
||||
import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc';
|
||||
|
||||
type InternalTrack = {
|
||||
id: number;
|
||||
@@ -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}').`);
|
||||
}
|
||||
@@ -1235,7 +1244,10 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
const objectTypeIndication = readU8(slice);
|
||||
if (objectTypeIndication === 0x40 || objectTypeIndication === 0x67) {
|
||||
track.info.codec = 'aac';
|
||||
track.info.aacCodecInfo = { isMpeg2: objectTypeIndication === 0x67 };
|
||||
track.info.aacCodecInfo = {
|
||||
isMpeg2: objectTypeIndication === 0x67,
|
||||
objectType: null,
|
||||
};
|
||||
} else if (objectTypeIndication === 0x69 || objectTypeIndication === 0x6b) {
|
||||
track.info.codec = 'mp3';
|
||||
} else if (objectTypeIndication === 0xdd) {
|
||||
@@ -1460,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) {
|
||||
@@ -2332,6 +2389,25 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
return this.internalTrack.id;
|
||||
}
|
||||
|
||||
getNumber() {
|
||||
const demuxer = this.internalTrack.demuxer;
|
||||
const inputTrack = this.internalTrack.inputTrack!;
|
||||
const trackType = inputTrack.type;
|
||||
|
||||
let number = 0;
|
||||
for (const track of demuxer.tracks) {
|
||||
if (track.inputTrack!.type === trackType) {
|
||||
number++;
|
||||
}
|
||||
|
||||
if (track === this.internalTrack) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
getCodec(): MediaCodec | null {
|
||||
throw new Error('Not implemented on base class.');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -14,6 +14,9 @@ import { assert, computeRationalApproximation, last, promiseWithResolvers } from
|
||||
import { IsobmffOutputFormatOptions, IsobmffOutputFormat, MovOutputFormat } from '../output-format';
|
||||
import { inlineTimestampRegex, SubtitleConfig, SubtitleCue, SubtitleMetadata } from '../subtitles';
|
||||
import {
|
||||
aacChannelMap,
|
||||
aacFrequencyTable,
|
||||
buildAacAudioSpecificConfig,
|
||||
parsePcmCodec,
|
||||
PCM_AUDIO_CODECS,
|
||||
PcmAudioCodec,
|
||||
@@ -22,14 +25,17 @@ import {
|
||||
validateSubtitleMetadata,
|
||||
validateVideoChunkMetadata,
|
||||
} from '../codec';
|
||||
import { MAX_ADTS_FRAME_HEADER_SIZE, MIN_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from '../adts/adts-reader';
|
||||
import { FileSlice } from '../reader';
|
||||
import { BufferTarget } from '../target';
|
||||
import { EncodedPacket, PacketType } from '../packet';
|
||||
import {
|
||||
concatNalUnitsInLengthPrefixed,
|
||||
extractAvcDecoderConfigurationRecord,
|
||||
extractHevcDecoderConfigurationRecord,
|
||||
iterateNalUnitsInAnnexB,
|
||||
serializeAvcDecoderConfigurationRecord,
|
||||
serializeHevcDecoderConfigurationRecord,
|
||||
transformAnnexBToLengthPrefixed,
|
||||
} from '../codec-data';
|
||||
import { buildIsobmffMimeType } from './isobmff-misc';
|
||||
import { MAX_BOX_HEADER_SIZE, MIN_BOX_HEADER_SIZE } from './isobmff-reader';
|
||||
@@ -100,6 +106,12 @@ export type IsobmffTrackData = {
|
||||
* Some players expect this for PCM audio.
|
||||
*/
|
||||
requiresPcmTransformation: boolean;
|
||||
/**
|
||||
* The "ADTS stripping" involves removing the ADTS header from each AAC packet. SOBMFF stores raw AAC data, not
|
||||
* ADTS-wrapped data.
|
||||
*/
|
||||
requiresAdtsStripping: boolean;
|
||||
firstPacket: EncodedPacket;
|
||||
};
|
||||
} | {
|
||||
track: OutputSubtitleTrack;
|
||||
@@ -364,7 +376,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
return newTrackData;
|
||||
}
|
||||
|
||||
private getAudioTrackData(track: OutputAudioTrack, meta?: EncodedAudioChunkMetadata) {
|
||||
private getAudioTrackData(track: OutputAudioTrack, packet: EncodedPacket, meta?: EncodedAudioChunkMetadata) {
|
||||
const existingTrackData = this.trackDatas.find(x => x.track === track);
|
||||
if (existingTrackData) {
|
||||
return existingTrackData as IsobmffAudioTrackData;
|
||||
@@ -375,6 +387,37 @@ export class IsobmffMuxer extends Muxer {
|
||||
assert(meta);
|
||||
assert(meta.decoderConfig);
|
||||
|
||||
const decoderConfig = { ...meta.decoderConfig };
|
||||
let requiresAdtsStripping = false;
|
||||
|
||||
if (track.source._codec === 'aac' && !decoderConfig.description) {
|
||||
// ISOBMFF can only hold AAC in raw format, not ADTS, but the missing description indicates ADTS.
|
||||
// Parse the first packet to extract the AudioSpecificConfig.
|
||||
const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packet.data));
|
||||
if (!adtsFrame) {
|
||||
throw new Error(
|
||||
'Couldn\'t parse ADTS header from the AAC packet. Make sure the packets are in ADTS format'
|
||||
+ ' (as specified in ISO 13818-7) when not providing a description, or provide a description'
|
||||
+ ' (must be an AudioSpecificConfig as specified in ISO 14496-3) and ensure the packets'
|
||||
+ ' are raw AAC data.',
|
||||
);
|
||||
}
|
||||
|
||||
const sampleRate = aacFrequencyTable[adtsFrame.samplingFrequencyIndex];
|
||||
const numberOfChannels = aacChannelMap[adtsFrame.channelConfiguration];
|
||||
|
||||
if (sampleRate === undefined || numberOfChannels === undefined) {
|
||||
throw new Error('Invalid ADTS frame header.');
|
||||
}
|
||||
|
||||
decoderConfig.description = buildAacAudioSpecificConfig({
|
||||
objectType: adtsFrame.objectType,
|
||||
sampleRate,
|
||||
numberOfChannels,
|
||||
});
|
||||
requiresAdtsStripping = true;
|
||||
}
|
||||
|
||||
const newTrackData: IsobmffAudioTrackData = {
|
||||
muxer: this,
|
||||
track,
|
||||
@@ -382,12 +425,14 @@ export class IsobmffMuxer extends Muxer {
|
||||
info: {
|
||||
numberOfChannels: meta.decoderConfig.numberOfChannels,
|
||||
sampleRate: meta.decoderConfig.sampleRate,
|
||||
decoderConfig: meta.decoderConfig,
|
||||
decoderConfig,
|
||||
requiresPcmTransformation:
|
||||
!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: [],
|
||||
@@ -464,15 +509,19 @@ export class IsobmffMuxer extends Muxer {
|
||||
|
||||
let packetData = packet.data;
|
||||
if (trackData.info.requiresAnnexBTransformation) {
|
||||
const transformedData = transformAnnexBToLengthPrefixed(packetData);
|
||||
if (!transformedData) {
|
||||
const nalUnits = [...iterateNalUnitsInAnnexB(packetData)]
|
||||
.map(loc => packetData.subarray(loc.offset, loc.offset + loc.length));
|
||||
if (nalUnits.length === 0) {
|
||||
// It's not valid Annex B data
|
||||
throw new Error(
|
||||
'Failed to transform packet data. Make sure all packets are provided in Annex B format, as'
|
||||
+ ' specified in ITU-T-REC-H.264 and ITU-T-REC-H.265.',
|
||||
);
|
||||
}
|
||||
|
||||
packetData = transformedData;
|
||||
// We don't strip things like SPS or PPS NALUs here, mainly because they can also appear in the middle
|
||||
// of a stream and potentially modify the parameters of it. So, let's just leave them in to be sure.
|
||||
packetData = concatNalUnitsInLengthPrefixed(nalUnits, 4);
|
||||
}
|
||||
|
||||
const timestamp = this.validateAndNormalizeTimestamp(
|
||||
@@ -498,7 +547,20 @@ export class IsobmffMuxer extends Muxer {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
try {
|
||||
const trackData = this.getAudioTrackData(track, meta);
|
||||
const trackData = this.getAudioTrackData(track, packet, meta);
|
||||
|
||||
let packetData = packet.data;
|
||||
if (trackData.info.requiresAdtsStripping) {
|
||||
const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packetData));
|
||||
if (!adtsFrame) {
|
||||
throw new Error('Expected ADTS frame, didn\'t get one.');
|
||||
}
|
||||
|
||||
const headerLength = adtsFrame.crcCheck === null
|
||||
? MIN_ADTS_FRAME_HEADER_SIZE
|
||||
: MAX_ADTS_FRAME_HEADER_SIZE;
|
||||
packetData = packetData.subarray(headerLength);
|
||||
}
|
||||
|
||||
const timestamp = this.validateAndNormalizeTimestamp(
|
||||
trackData.track,
|
||||
@@ -507,7 +569,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
);
|
||||
const internalSample = this.createSampleForTrack(
|
||||
trackData,
|
||||
packet.data,
|
||||
packetData,
|
||||
timestamp,
|
||||
packet.duration,
|
||||
packet.type,
|
||||
|
||||