Add .gitignore to exclude all node packages and lock files

This commit is contained in:
Adolfo Reyna
2026-02-23 21:56:04 -05:00
parent faae96c9ed
commit dcc5c6c044
9747 changed files with 1555105 additions and 2 deletions
+373
View File
@@ -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.
+221
View File
@@ -0,0 +1,221 @@
# Mediabunny - JavaScript media toolkit
[![](https://img.shields.io/npm/v/mediabunny)](https://www.npmjs.com/package/mediabunny)
[![](https://img.shields.io/bundlephobia/minzip/mediabunny)](https://bundlephobia.com/package/mediabunny)
[![](https://img.shields.io/npm/dm/mediabunny)](https://www.npmjs.com/package/mediabunny)
[![](https://img.shields.io/discord/1390044844285497344?logo=discord&label=Discord)](https://discord.gg/hmpkyYuS4U)
<div align="center">
<img src="./docs/public/mediabunny-logo.svg" width="180" height="180">
</div>
Mediabunny is a JavaScript library for reading, writing, and converting media files (like MP4, WebM, MP3), directly in the browser. It aims to be a complete toolkit for high-performance media operations on the web. It's written from scratch in pure TypeScript, has zero dependencies, is very performant, and is extremely tree-shakable, meaning you only include what you use. You can think of it a bit like [FFmpeg](https://ffmpeg.org/), but built from the ground up for the web.
[Documentation](https://mediabunny.dev) | [Examples](https://mediabunny.dev/examples) | [Sponsoring](#sponsoring) | [License](#license) | [Discord](https://discord.gg/hmpkyYuS4U)
### Gold sponsors
<div align="center">
<a href="https://remotion.dev/" target="_blank" rel="sponsored">
<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>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.gling.ai/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/gling.svg" width="60" height="60" alt="Gling AI">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://diffusion.studio/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/diffusionstudio.png" width="60" height="60" alt="Diffusion Studio">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://kino.ai/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/kino.jpg" width="60" height="60" alt="Kino">
</a>
</div>
### Silver sponsors
<div align="center">
<a href="https://pqina.nl/pintura/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/pintura-labs.png" width="50" height="50" alt="Pintura Labs">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://ponder.ai/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/ponder.png" width="50" height="50" alt="Ponder">
</a>
</div>
### 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>
&nbsp;&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.mux.com/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/mux.jpg" width="40" height="40" alt="Mux">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://jellypod.ai/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/jellypod.png" width="40" height="40" alt="Jellypod">
</a>
</div>
[Sponsor Mediabunny's development](https://github.com/sponsors/Vanilagy)
## Features
Core features include:
- **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
- **Streaming I/O**: Handle reading & writing files of any size with memory-efficient streaming
- **Tree-shakable**: Only bundle what you use (as small as 5 kB gzipped)
- **Zero dependencies**: Implemented in highly performant TypeScript
- **Cross-platform**: Works in browsers and Node.js
[See full feature list](https://mediabunny.dev/guide/introduction#features)
## Quick start
### Installation
Install it via npm:
```bash
npm install mediabunny
```
Alternatively, include it directly with a script tag using one of the [builds](https://github.com/Vanilagy/mediabunny/releases). Doing so exposes a global `Mediabunny` object.
```html
<script src="mediabunny.cjs"></script>
```
Requires any JavaScript environment that can run ECMAScript 2021 or later. Mediabunny is expected to be run in modern browsers. For types, TypeScript 5.7 or later is required.
### Read file metadata
```js
import { Input, ALL_FORMATS, BlobSource } from 'mediabunny';
const input = new Input({
source: new BlobSource(file), // Reading from disk
formats: ALL_FORMATS,
});
const duration = await input.computeDuration(); // in seconds
const videoTrack = await input.getPrimaryVideoTrack();
const audioTrack = await input.getPrimaryAudioTrack();
const { displayWidth, displayHeight, rotation } = videoTrack;
const { sampleRate, numberOfChannels } = audioTrack;
const { title, artist, album } = await input.getMetadataTags();
```
### Create new media files
```js
import { Output, Mp4OutputFormat, BufferTarget, CanvasSource, QUALITY_HIGH } from 'mediabunny';
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(), // Writing to memory
});
// Add a video track backed by a canvas element
const videoSource = new CanvasSource(canvas, {
codec: 'avc',
bitrate: QUALITY_HIGH,
});
output.addVideoTrack(videoSource);
await output.start();
// Add frames...
await output.finalize();
const buffer = output.target.buffer; // Final MP4 file
```
### Convert files
```js
import { Input, Output, Conversion, ALL_FORMATS, BlobSource, WebMOutputFormat } from 'mediabunny';
const input = new Input({
source: new BlobSource(file),
formats: ALL_FORMATS,
});
const output = new Output({
format: new WebMOutputFormat(), // Convert to WebM
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input, output });
await conversion.execute();
```
[See more code snippets](https://mediabunny.dev/guide/quick-start)
## Documentation
Visit the [Docs](https://mediabunny.dev/guide/introduction) for comprehensive guides, examples and API documentation.
## Sponsoring
[See all sponsors](https://mediabunny.dev/#sponsors)
Mediabunny is an open-source project released under the <a href="https://choosealicense.com/licenses/mpl-2.0/" target="_blank">MPL-2.0</a> and is therefore free to use for any purpose, including closed-source commercial use. A permissive license is essential for a foundational library like this to truly thrive. That said, this project requires an immense amount of work and care to maintain and expand. This is made possible by the generous financial backing of the sponsors of this project.
If you have derived considerable value from this project, please consider [sponsoring it](https://github.com/sponsors/Vanilagy) or providing a one-time donation. Thank you! 🩷
## License
This project is licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/). This is a very permissive weak copyleft license, not much different from the MIT License, allowing you to:
- Use Mediabunny for any purpose, commercial or non-commercial, without royalties
- Use Mediabunny in open- and closed-source projects
- Freely distribute projects built with Mediabunny
- Inspect and modify Mediabunny's source code
However, you have the following obligation:
- If you modify Mediabunny's licensed source code (e.g. in a fork) and then distribute it, you must publicly publish your modifications under the Mozilla Public License 2.0.
This ensures that library usage remains permissive for everybody, while any improvements to Mediabunny remain in the open, benefiting everyone.
You are not allowed to:
- Remove the license and copyright headers from any Mediabunny source file
- Claim the "Mediabunny" trademark
And finally, Mediabunny - like any other library - comes with no warranty of any kind and is not liable for any direct or indirect damages.
> This is not legal advice. Refer to the full text of the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/) for the binding license agreement.
## Implementation & development
Mediabunny is implemented from scratch in pure TypeScript with zero dependencies. At its core, the library is a collection of multiplexers and demultiplexers (one for every container format), which are then connected together via abstractions around the WebCodecs API. The logic is heavily pipelined and lazy, keeping performance high and memory usage low. If this stuff interests you, refer to the [Technical overview](https://mediabunny.dev/guide/introduction#technical-overview) for more.
For development, clone this repository and install it using a modern version of Node.js and npm. The build system uses TypeScript, esbuild, API Extractor, Vite, and VitePress.
```bash
npm install # Install dependencies
npm run watch # Build bundles on watch mode
npm run build # Production build with type definitions
npm run check # Type checking
npm run lint # ESLint
npm run docs:generate # Generates API docs
npm run docs:dev # Start docs development server
npm run dev # Start examples development server, will run at http://localhost:5173/examples/[name]/
npm run docs:build # Build docs and examples
```
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
/*!
* 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 declare const AC3_SAMPLE_RATES: number[];
/** E-AC-3 reduced sample rates for fscod2 per ATSC A/52:2018 */
export declare const EAC3_REDUCED_SAMPLE_RATES: number[];
//# sourceMappingURL=ac3-misc.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"ac3-misc.d.ts","sourceRoot":"","sources":["../../../shared/ac3-misc.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,UAAwB,CAAC;AAEtD,gEAAgE;AAChE,eAAO,MAAM,yBAAyB,UAAwB,CAAC"}
@@ -0,0 +1,11 @@
/*!
* 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];
@@ -0,0 +1,37 @@
/*!
* 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 declare const FRAME_HEADER_SIZE = 4;
export declare const SAMPLING_RATES: number[];
export declare const KILOBIT_RATES: number[];
/** 'Xing' */
export declare const XING = 1483304551;
/** 'Info' */
export declare const INFO = 1231971951;
export type Mp3FrameHeader = {
totalSize: number;
mpegVersionId: number;
layer: number;
bitrate: number;
frequencyIndex: number;
sampleRate: number;
channel: number;
modeExtension: number;
copyright: number;
original: number;
emphasis: number;
audioSamplesInFrame: number;
};
export declare const computeMp3FrameSize: (lowSamplingFrequency: number, layer: number, bitrate: number, sampleRate: number, padding: number) => number;
export declare const getXingOffset: (mpegVersionId: number, channel: number) => 13 | 21 | 36;
export declare const readMp3FrameHeader: (word: number, remainingBytes: number | null) => {
header: Mp3FrameHeader | null;
bytesAdvanced: number;
};
export declare const encodeSynchsafe: (unsynchsafed: number) => number;
export declare const decodeSynchsafe: (synchsafed: number) => number;
//# sourceMappingURL=mp3-misc.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"mp3-misc.d.ts","sourceRoot":"","sources":["../../../shared/mp3-misc.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,cAAc,UAAwB,CAAC;AACpD,eAAO,MAAM,aAAa,UAYzB,CAAC;AAEF,aAAa;AACb,eAAO,MAAM,IAAI,aAAa,CAAC;AAC/B,aAAa;AACb,eAAO,MAAM,IAAI,aAAa,CAAC;AAE/B,MAAM,MAAM,cAAc,GAAG;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC/B,sBAAsB,MAAM,EAC5B,OAAO,MAAM,EACb,SAAS,MAAM,EACf,YAAY,MAAM,EAClB,SAAS,MAAM,WAWf,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,eAAe,MAAM,EAAE,SAAS,MAAM,iBAInE,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,MAAM,GAAG,IAAI,KAAG;IAChF,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;CAyFtB,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,cAAc,MAAM,WAcnD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,YAAY,MAAM,WAWjD,CAAC"}
@@ -0,0 +1,147 @@
/*!
* 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 const FRAME_HEADER_SIZE = 4;
export const SAMPLING_RATES = [44100, 48000, 32000];
export const KILOBIT_RATES = [
// lowSamplingFrequency === 0
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // layer = 0
-1, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1, // layer 1
-1, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1, // layer = 2
-1, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1, // layer = 3
// lowSamplingFrequency === 1
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // layer = 0
-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1, // layer = 1
-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1, // layer = 2
-1, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1, // layer = 3
];
/** 'Xing' */
export const XING = 0x58696e67;
/** 'Info' */
export const INFO = 0x496e666f;
export const computeMp3FrameSize = (lowSamplingFrequency, layer, bitrate, sampleRate, padding) => {
if (layer === 0) {
return 0; // Not expected that this is hit
}
else if (layer === 1) {
return Math.floor(144 * bitrate / (sampleRate << lowSamplingFrequency)) + padding;
}
else if (layer === 2) {
return Math.floor(144 * bitrate / sampleRate) + padding;
}
else { // layer === 3
return (Math.floor(12 * bitrate / sampleRate) + padding) * 4;
}
};
export const getXingOffset = (mpegVersionId, channel) => {
return mpegVersionId === 3
? (channel === 3 ? 21 : 36)
: (channel === 3 ? 13 : 21);
};
export const readMp3FrameHeader = (word, remainingBytes) => {
const firstByte = word >>> 24;
const secondByte = (word >>> 16) & 0xff;
const thirdByte = (word >>> 8) & 0xff;
const fourthByte = word & 0xff;
if (firstByte !== 0xff && secondByte !== 0xff && thirdByte !== 0xff && fourthByte !== 0xff) {
return {
header: null,
bytesAdvanced: 4,
};
}
if (firstByte !== 0xff) {
return { header: null, bytesAdvanced: 1 };
}
if ((secondByte & 0xe0) !== 0xe0) {
return { header: null, bytesAdvanced: 1 };
}
let lowSamplingFrequency = 0;
let mpeg25 = 0;
if (secondByte & (1 << 4)) {
lowSamplingFrequency = (secondByte & (1 << 3)) ? 0 : 1;
}
else {
lowSamplingFrequency = 1;
mpeg25 = 1;
}
const mpegVersionId = (secondByte >> 3) & 0x3;
const layer = (secondByte >> 1) & 0x3;
const bitrateIndex = (thirdByte >> 4) & 0xf;
const frequencyIndex = ((thirdByte >> 2) & 0x3) % 3;
const padding = (thirdByte >> 1) & 0x1;
const channel = (fourthByte >> 6) & 0x3;
const modeExtension = (fourthByte >> 4) & 0x3;
const copyright = (fourthByte >> 3) & 0x1;
const original = (fourthByte >> 2) & 0x1;
const emphasis = fourthByte & 0x3;
const kilobitRate = KILOBIT_RATES[lowSamplingFrequency * 16 * 4 + layer * 16 + bitrateIndex];
if (kilobitRate === -1) {
return { header: null, bytesAdvanced: 1 };
}
const bitrate = kilobitRate * 1000;
const sampleRate = SAMPLING_RATES[frequencyIndex] >> (lowSamplingFrequency + mpeg25);
const frameLength = computeMp3FrameSize(lowSamplingFrequency, layer, bitrate, sampleRate, padding);
if (remainingBytes !== null && remainingBytes < frameLength) {
// The frame doesn't fit into the rest of the file
return { header: null, bytesAdvanced: 1 };
}
let audioSamplesInFrame;
if (mpegVersionId === 3) {
audioSamplesInFrame = layer === 3 ? 384 : 1152;
}
else {
if (layer === 3) {
audioSamplesInFrame = 384;
}
else if (layer === 2) {
audioSamplesInFrame = 1152;
}
else {
audioSamplesInFrame = 576;
}
}
return {
header: {
totalSize: frameLength,
mpegVersionId,
layer,
bitrate,
frequencyIndex,
sampleRate,
channel,
modeExtension,
copyright,
original,
emphasis,
audioSamplesInFrame,
},
bytesAdvanced: 1,
};
};
export const encodeSynchsafe = (unsynchsafed) => {
let mask = 0x7f;
let synchsafed = 0;
let unsynchsafedRest = unsynchsafed;
while ((mask ^ 0x7fffffff) !== 0) {
synchsafed = unsynchsafedRest & ~mask;
synchsafed <<= 1;
synchsafed |= unsynchsafedRest & mask;
mask = ((mask + 1) << 8) - 1;
unsynchsafedRest = synchsafed;
}
return synchsafed;
};
export const decodeSynchsafe = (synchsafed) => {
let mask = 0x7f000000;
let unsynchsafed = 0;
while (mask !== 0) {
unsynchsafed >>= 1;
unsynchsafed |= synchsafed & mask;
mask >>= 8;
}
return unsynchsafed;
};
@@ -0,0 +1,42 @@
/*!
* 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 { Demuxer } from '../demuxer.js';
import { Input } from '../input.js';
import { InputAudioTrack } from '../input-track.js';
import { MetadataTags } from '../metadata.js';
import { AsyncMutex } from '../misc.js';
import { Reader } from '../reader.js';
import { AdtsFrameHeader } from './adts-reader.js';
export declare const SAMPLES_PER_AAC_FRAME = 1024;
type Sample = {
timestamp: number;
duration: number;
dataStart: number;
dataSize: number;
};
export declare class AdtsDemuxer extends Demuxer {
reader: Reader;
metadataPromise: Promise<void> | null;
firstFrameHeader: AdtsFrameHeader | null;
loadedSamples: Sample[];
metadataTags: MetadataTags | null;
tracks: InputAudioTrack[];
readingMutex: AsyncMutex;
lastSampleLoaded: boolean;
lastLoadedPos: number;
nextTimestampInSamples: number;
constructor(input: Input);
readMetadata(): Promise<void>;
advanceReader(): Promise<void>;
getMimeType(): Promise<string>;
getTracks(): Promise<InputAudioTrack[]>;
computeDuration(): Promise<number>;
getMetadataTags(): Promise<MetadataTags>;
}
export {};
//# sourceMappingURL=adts-demuxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"adts-demuxer.d.ts","sourceRoot":"","sources":["../../../../src/adts/adts-demuxer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAMrC,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,eAAe,EAA0B,MAAM,gBAAgB,CAAC;AAEzE,OAAO,EAA6B,YAAY,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAEN,UAAU,EAIV,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAa,MAAM,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EACN,eAAe,EAIf,MAAM,eAAe,CAAC;AAEvB,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,KAAK,MAAM,GAAG;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,WAAY,SAAQ,OAAO;IACvC,MAAM,EAAE,MAAM,CAAC;IAEf,eAAe,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAQ;IAC7C,gBAAgB,EAAE,eAAe,GAAG,IAAI,CAAQ;IAChD,aAAa,EAAE,MAAM,EAAE,CAAM;IAC7B,YAAY,EAAE,YAAY,GAAG,IAAI,CAAQ;IAEzC,MAAM,EAAE,eAAe,EAAE,CAAM;IAE/B,YAAY,aAAoB;IAChC,gBAAgB,UAAS;IACzB,aAAa,SAAK;IAClB,sBAAsB,SAAK;gBAEf,KAAK,EAAE,KAAK;IAMlB,YAAY;IAeZ,aAAa;IAgEb,WAAW;IAIX,SAAS;IAKT,eAAe;IASf,eAAe;CAqCrB"}
@@ -0,0 +1,277 @@
/*!
* 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 { aacChannelMap, aacFrequencyTable } from '../codec.js';
import { Demuxer } from '../demuxer.js';
import { ID3_V2_HEADER_SIZE, parseId3V2Tag, readId3V2Header, } from '../id3.js';
import { InputAudioTrack } from '../input-track.js';
import { DEFAULT_TRACK_DISPOSITION } from '../metadata.js';
import { assert, AsyncMutex, binarySearchExact, binarySearchLessOrEqual, UNDETERMINED_LANGUAGE, } from '../misc.js';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet.js';
import { readBytes } from '../reader.js';
import { MIN_ADTS_FRAME_HEADER_SIZE, MAX_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader, } from './adts-reader.js';
export const SAMPLES_PER_AAC_FRAME = 1024;
export class AdtsDemuxer extends Demuxer {
constructor(input) {
super(input);
this.metadataPromise = null;
this.firstFrameHeader = null;
this.loadedSamples = [];
this.metadataTags = null;
this.tracks = [];
this.readingMutex = new AsyncMutex();
this.lastSampleLoaded = false;
this.lastLoadedPos = 0;
this.nextTimestampInSamples = 0;
this.reader = input._reader;
}
async readMetadata() {
return this.metadataPromise ??= (async () => {
// Keep loading until we find the first frame header
while (!this.firstFrameHeader && !this.lastSampleLoaded) {
await this.advanceReader();
}
// There has to be a frame if this demuxer got selected
assert(this.firstFrameHeader);
// Create the single audio track
this.tracks = [new InputAudioTrack(this.input, new AdtsAudioTrackBacking(this))];
})();
}
async advanceReader() {
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 = readAdtsFrameHeader(slice);
if (!header) {
this.lastSampleLoaded = true;
return;
}
if (this.reader.fileSize !== null && header.startPos + header.frameLength > this.reader.fileSize) {
// Frame doesn't fit in the rest of the file
this.lastSampleLoaded = true;
return;
}
if (!this.firstFrameHeader) {
this.firstFrameHeader = header;
}
const sampleRate = aacFrequencyTable[header.samplingFrequencyIndex];
assert(sampleRate !== undefined);
const sampleDuration = SAMPLES_PER_AAC_FRAME / sampleRate;
const sample = {
timestamp: this.nextTimestampInSamples / sampleRate,
duration: sampleDuration,
dataStart: header.startPos,
dataSize: header.frameLength,
};
this.loadedSamples.push(sample);
this.nextTimestampInSamples += SAMPLES_PER_AAC_FRAME;
this.lastLoadedPos = header.startPos + header.frameLength;
}
async getMimeType() {
return 'audio/aac';
}
async getTracks() {
await this.readMetadata();
return this.tracks;
}
async computeDuration() {
await this.readMetadata();
const track = this.tracks[0];
assert(track);
return track.computeDuration();
}
async getMetadataTags() {
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();
}
}
}
class AdtsAudioTrackBacking {
constructor(demuxer) {
this.demuxer = demuxer;
}
getId() {
return 1;
}
getNumber() {
return 1;
}
async getFirstTimestamp() {
return 0;
}
getTimeResolution() {
const sampleRate = this.getSampleRate();
return sampleRate / SAMPLES_PER_AAC_FRAME;
}
async computeDuration() {
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
}
getName() {
return null;
}
getLanguageCode() {
return UNDETERMINED_LANGUAGE;
}
getCodec() {
return 'aac';
}
getInternalCodecId() {
assert(this.demuxer.firstFrameHeader);
return this.demuxer.firstFrameHeader.objectType;
}
getNumberOfChannels() {
assert(this.demuxer.firstFrameHeader);
const numberOfChannels = aacChannelMap[this.demuxer.firstFrameHeader.channelConfiguration];
assert(numberOfChannels !== undefined);
return numberOfChannels;
}
getSampleRate() {
assert(this.demuxer.firstFrameHeader);
const sampleRate = aacFrequencyTable[this.demuxer.firstFrameHeader.samplingFrequencyIndex];
assert(sampleRate !== undefined);
return sampleRate;
}
getDisposition() {
return {
...DEFAULT_TRACK_DISPOSITION,
};
}
async getDecoderConfig() {
assert(this.demuxer.firstFrameHeader);
return {
codec: `mp4a.40.${this.demuxer.firstFrameHeader.objectType}`,
numberOfChannels: this.getNumberOfChannels(),
sampleRate: this.getSampleRate(),
};
}
async getPacketAtIndex(sampleIndex, options) {
if (sampleIndex === -1) {
return null;
}
const rawSample = this.demuxer.loadedSamples[sampleIndex];
if (!rawSample) {
return null;
}
let data;
if (options.metadataOnly) {
data = PLACEHOLDER_DATA;
}
else {
let slice = this.demuxer.reader.requestSlice(rawSample.dataStart, rawSample.dataSize);
if (slice instanceof Promise)
slice = await slice;
if (!slice) {
return null; // Data didn't fit into the rest of the file
}
data = readBytes(slice, rawSample.dataSize);
}
return new EncodedPacket(data, 'key', rawSample.timestamp, rawSample.duration, sampleIndex, rawSample.dataSize);
}
getFirstPacket(options) {
return this.getPacketAtIndex(0, options);
}
async getNextPacket(packet, options) {
const release = await this.demuxer.readingMutex.acquire();
try {
const sampleIndex = binarySearchExact(this.demuxer.loadedSamples, packet.timestamp, x => x.timestamp);
if (sampleIndex === -1) {
throw new Error('Packet was not created from this track.');
}
const nextIndex = sampleIndex + 1;
// Ensure the next sample exists
while (nextIndex >= this.demuxer.loadedSamples.length
&& !this.demuxer.lastSampleLoaded) {
await this.demuxer.advanceReader();
}
return this.getPacketAtIndex(nextIndex, options);
}
finally {
release();
}
}
async getPacket(timestamp, options) {
const release = await this.demuxer.readingMutex.acquire();
try {
while (true) {
const index = binarySearchLessOrEqual(this.demuxer.loadedSamples, timestamp, x => x.timestamp);
if (index === -1 && this.demuxer.loadedSamples.length > 0) {
// We're before the first sample
return null;
}
if (this.demuxer.lastSampleLoaded) {
// All data is loaded, return what we found
return this.getPacketAtIndex(index, options);
}
if (index >= 0 && index + 1 < this.demuxer.loadedSamples.length) {
// The next packet also exists, we're done
return this.getPacketAtIndex(index, options);
}
// Otherwise, keep loading data
await this.demuxer.advanceReader();
}
}
finally {
release();
}
}
getKeyPacket(timestamp, options) {
return this.getPacket(timestamp, options);
}
getNextKeyPacket(packet, options) {
return this.getNextPacket(packet, options);
}
}
@@ -0,0 +1,16 @@
/*!
* 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.js';
import { Bitstream } from '../misc.js';
export type AdtsHeaderTemplate = {
header: Uint8Array;
bitstream: Bitstream;
};
export declare const buildAdtsHeaderTemplate: (config: AacAudioSpecificConfig) => AdtsHeaderTemplate;
export declare const writeAdtsFrameLength: (bitstream: Bitstream, frameLength: number) => void;
//# sourceMappingURL=adts-misc.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"adts-misc.d.ts","sourceRoot":"","sources":["../../../../src/adts/adts-misc.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,MAAM,kBAAkB,GAAG;IAChC,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,sBAAsB,KAAG,kBAyBxE,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,WAAW,SAAS,EAAE,aAAa,MAAM,SAG7E,CAAC"}
@@ -0,0 +1,35 @@
/*!
* 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 { Bitstream } from '../misc.js';
export const buildAdtsHeaderTemplate = (config) => {
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, frameLength) => {
bitstream.pos = 30;
bitstream.writeBits(13, frameLength);
};
@@ -0,0 +1,26 @@
/*!
* 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 { Muxer } from '../muxer.js';
import { Output, OutputAudioTrack } from '../output.js';
import { AdtsOutputFormat } from '../output-format.js';
import { EncodedPacket } from '../packet.js';
export declare class AdtsMuxer extends Muxer {
private format;
private writer;
private header;
private headerBitstream;
private inputIsAdts;
constructor(output: Output, format: AdtsOutputFormat);
start(): Promise<void>;
getMimeType(): Promise<string>;
addEncodedVideoPacket(): Promise<void>;
addEncodedAudioPacket(track: OutputAudioTrack, packet: EncodedPacket, meta?: EncodedAudioChunkMetadata): Promise<void>;
addSubtitleCue(): Promise<void>;
finalize(): Promise<void>;
}
//# sourceMappingURL=adts-muxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"adts-muxer.d.ts","sourceRoot":"","sources":["../../../../src/adts/adts-muxer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAI1C,qBAAa,SAAU,SAAQ,KAAK;IACnC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,eAAe,CAA0B;IACjD,OAAO,CAAC,WAAW,CAAwB;gBAE/B,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB;IAO9C,KAAK;IAOL,WAAW;IAIX,qBAAqB;IAIrB,qBAAqB,CAC1B,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,aAAa,EACrB,IAAI,CAAC,EAAE,yBAAyB;IA0D3B,cAAc;IAId,QAAQ;CACd"}
@@ -0,0 +1,85 @@
/*!
* 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 { parseAacAudioSpecificConfig, validateAudioChunkMetadata } from '../codec.js';
import { Id3V2Writer } from '../id3.js';
import { metadataTagsAreEmpty } from '../metadata.js';
import { assert, toUint8Array } from '../misc.js';
import { Muxer } from '../muxer.js';
import { buildAdtsHeaderTemplate, writeAdtsFrameLength } from './adts-misc.js';
export class AdtsMuxer extends Muxer {
constructor(output, format) {
super(output);
this.header = null;
this.headerBitstream = null;
this.inputIsAdts = null;
this.format = format;
this.writer = output._writer;
}
async start() {
if (!metadataTagsAreEmpty(this.output._metadataTags)) {
const id3Writer = new Id3V2Writer(this.writer);
id3Writer.writeId3V2Tag(this.output._metadataTags);
}
}
async getMimeType() {
return 'audio/aac';
}
async addEncodedVideoPacket() {
throw new Error('ADTS does not support video.');
}
async addEncodedAudioPacket(track, packet, meta) {
const release = await this.mutex.acquire();
try {
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
// First packet - determine input format from metadata
if (this.inputIsAdts === null) {
validateAudioChunkMetadata(meta);
const description = meta?.decoderConfig?.description;
// Follows from the Mediabunny Codec Registry:
this.inputIsAdts = !description;
if (!this.inputIsAdts) {
const config = parseAacAudioSpecificConfig(toUint8Array(description));
const template = buildAdtsHeaderTemplate(config);
this.header = template.header;
this.headerBitstream = template.bitstream;
}
}
if (this.inputIsAdts) {
// Packets are already ADTS frames, write them directly
const startPos = this.writer.getPos();
this.writer.write(packet.data);
if (this.format._options.onFrame) {
this.format._options.onFrame(packet.data, startPos);
}
}
else {
assert(this.header);
// Packets are raw AAC, we gotta turn it into ADTS
const frameLength = packet.data.byteLength + this.header.byteLength;
writeAdtsFrameLength(this.headerBitstream, frameLength);
const startPos = this.writer.getPos();
this.writer.write(this.header);
this.writer.write(packet.data);
if (this.format._options.onFrame) {
const frameBytes = new Uint8Array(frameLength);
frameBytes.set(this.header, 0);
frameBytes.set(packet.data, this.header.byteLength);
this.format._options.onFrame(frameBytes, startPos);
}
}
await this.writer.flush();
}
finally {
release();
}
}
async addSubtitleCue() {
throw new Error('ADTS does not support subtitles.');
}
async finalize() { }
}
@@ -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/.
*/
import { FileSlice } from '../reader.js';
export declare const MIN_ADTS_FRAME_HEADER_SIZE = 7;
export declare const MAX_ADTS_FRAME_HEADER_SIZE = 9;
export type AdtsFrameHeader = {
objectType: number;
samplingFrequencyIndex: number;
channelConfiguration: number;
frameLength: number;
numberOfAacFrames: number;
crcCheck: number | null;
startPos: number;
};
export declare const readAdtsFrameHeader: (slice: FileSlice) => AdtsFrameHeader | null;
//# sourceMappingURL=adts-reader.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"adts-reader.d.ts","sourceRoot":"","sources":["../../../../src/adts/adts-reader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,SAAS,EAAa,MAAM,WAAW,CAAC;AAEjD,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAC5C,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,OAAO,SAAS,KAAG,eAAe,GAAG,IA4DxE,CAAC"}
@@ -0,0 +1,63 @@
/*!
* 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 { Bitstream } from '../misc.js';
import { readBytes } from '../reader.js';
export const MIN_ADTS_FRAME_HEADER_SIZE = 7;
export const MAX_ADTS_FRAME_HEADER_SIZE = 9;
export const readAdtsFrameHeader = (slice) => {
// https://wiki.multimedia.cx/index.php/ADTS (last visited: 2025/08/17)
const startPos = slice.filePos;
const bytes = readBytes(slice, 9); // 9 with CRC, 7 without CRC
const bitstream = new Bitstream(bytes);
const syncword = bitstream.readBits(12);
if (syncword !== 0b1111_11111111) {
return null;
}
bitstream.skipBits(1); // MPEG version
const layer = bitstream.readBits(2);
if (layer !== 0) {
return null;
}
const protectionAbsence = bitstream.readBits(1);
const objectType = bitstream.readBits(2) + 1;
const samplingFrequencyIndex = bitstream.readBits(4);
if (samplingFrequencyIndex === 15) {
return null;
}
bitstream.skipBits(1); // Private bit
const channelConfiguration = bitstream.readBits(3);
if (channelConfiguration === 0) {
throw new Error('ADTS frames with channel configuration 0 are not supported.');
}
bitstream.skipBits(1); // Originality
bitstream.skipBits(1); // Home
bitstream.skipBits(1); // Copyright ID bit
bitstream.skipBits(1); // Copyright ID start
const frameLength = bitstream.readBits(13);
bitstream.skipBits(11); // Buffer fullness
const numberOfAacFrames = bitstream.readBits(2) + 1;
if (numberOfAacFrames !== 1) {
throw new Error('ADTS frames with more than one AAC frame are not supported.');
}
let crcCheck = null;
if (protectionAbsence === 1) { // No CRC
slice.filePos -= 2;
}
else { // CRC
crcCheck = bitstream.readBits(16);
}
return {
objectType,
samplingFrequencyIndex,
channelConfiguration,
frameLength,
numberOfAacFrames,
crcCheck,
startPos,
};
};
@@ -0,0 +1,287 @@
/*!
* 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 { VideoCodec } from './codec.js';
import { PacketType } from './packet.js';
import { MetadataTags } from './metadata.js';
export declare enum AvcNalUnitType {
NON_IDR_SLICE = 1,
SLICE_DPA = 2,
SLICE_DPB = 3,
SLICE_DPC = 4,
IDR = 5,
SEI = 6,
SPS = 7,
PPS = 8,
AUD = 9,
SPS_EXT = 13
}
export declare enum HevcNalUnitType {
RASL_N = 8,
RASL_R = 9,
BLA_W_LP = 16,
RSV_IRAP_VCL23 = 23,
VPS_NUT = 32,
SPS_NUT = 33,
PPS_NUT = 34,
AUD_NUT = 35,
PREFIX_SEI_NUT = 39,
SUFFIX_SEI_NUT = 40
}
export type NalUnitLocation = {
offset: number;
length: number;
};
export declare const iterateNalUnitsInAnnexB: (packetData: Uint8Array) => Generator<NalUnitLocation>;
export declare const iterateNalUnitsInLengthPrefixed: (packetData: Uint8Array, lengthSize: 1 | 2 | 3 | 4) => Generator<NalUnitLocation>;
export declare const iterateAvcNalUnits: (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => Generator<NalUnitLocation, any, any>;
export declare const extractNalUnitTypeForAvc: (byte: number) => number;
export declare const concatNalUnitsInAnnexB: (nalUnits: Uint8Array[]) => Uint8Array<ArrayBuffer>;
export declare const concatNalUnitsInLengthPrefixed: (nalUnits: Uint8Array[], lengthSize: 1 | 2 | 3 | 4) => Uint8Array<ArrayBuffer>;
export type AvcDecoderConfigurationRecord = {
configurationVersion: number;
avcProfileIndication: number;
profileCompatibility: number;
avcLevelIndication: number;
lengthSizeMinusOne: number;
sequenceParameterSets: Uint8Array[];
pictureParameterSets: Uint8Array[];
chromaFormat: number | null;
bitDepthLumaMinus8: number | null;
bitDepthChromaMinus8: number | null;
sequenceParameterSetExt: Uint8Array[] | null;
};
export declare const concatAvcNalUnits: (nalUnits: Uint8Array[], decoderConfig: VideoDecoderConfig) => Uint8Array<ArrayBuffer>;
/** Builds an AvcDecoderConfigurationRecord from an AVC packet in Annex B format. */
export declare const extractAvcDecoderConfigurationRecord: (packetData: Uint8Array) => AvcDecoderConfigurationRecord | null;
/** Serializes an AvcDecoderConfigurationRecord into the format specified in Section 5.3.3.1 of ISO 14496-15. */
export declare const serializeAvcDecoderConfigurationRecord: (record: AvcDecoderConfigurationRecord) => Uint8Array<ArrayBuffer>;
/** Deserializes an AvcDecoderConfigurationRecord from the format specified in Section 5.3.3.1 of ISO 14496-15. */
export declare const deserializeAvcDecoderConfigurationRecord: (data: Uint8Array) => AvcDecoderConfigurationRecord | null;
export type AvcSpsInfo = {
profileIdc: number;
constraintFlags: number;
levelIdc: number;
frameMbsOnlyFlag: number;
chromaFormatIdc: number;
bitDepthLumaMinus8: number;
bitDepthChromaMinus8: number;
codedWidth: number;
codedHeight: number;
displayWidth: number;
displayHeight: number;
colourPrimaries: number;
transferCharacteristics: number;
matrixCoefficients: number;
fullRangeFlag: number;
numReorderFrames: number;
maxDecFrameBuffering: number;
};
/** Parses an AVC SPS (Sequence Parameter Set) to extract basic information. */
export declare const parseAvcSps: (sps: Uint8Array) => AvcSpsInfo | null;
export type HevcDecoderConfigurationRecord = {
configurationVersion: number;
generalProfileSpace: number;
generalTierFlag: number;
generalProfileIdc: number;
generalProfileCompatibilityFlags: number;
generalConstraintIndicatorFlags: Uint8Array;
generalLevelIdc: number;
minSpatialSegmentationIdc: number;
parallelismType: number;
chromaFormatIdc: number;
bitDepthLumaMinus8: number;
bitDepthChromaMinus8: number;
avgFrameRate: number;
constantFrameRate: number;
numTemporalLayers: number;
temporalIdNested: number;
lengthSizeMinusOne: number;
arrays: {
arrayCompleteness: number;
nalUnitType: number;
nalUnits: Uint8Array[];
}[];
};
export type HevcSpsInfo = {
displayWidth: number;
displayHeight: number;
colourPrimaries: number;
transferCharacteristics: number;
matrixCoefficients: number;
fullRangeFlag: number;
maxDecFrameBuffering: number;
spsMaxSubLayersMinus1: number;
spsTemporalIdNestingFlag: number;
generalProfileSpace: number;
generalTierFlag: number;
generalProfileIdc: number;
generalProfileCompatibilityFlags: number;
generalConstraintIndicatorFlags: Uint8Array;
generalLevelIdc: number;
chromaFormatIdc: number;
bitDepthLumaMinus8: number;
bitDepthChromaMinus8: number;
minSpatialSegmentationIdc: number;
};
export declare const iterateHevcNalUnits: (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => Generator<NalUnitLocation, any, any>;
export declare const extractNalUnitTypeForHevc: (byte: number) => number;
/** Parses an HEVC SPS (Sequence Parameter Set) to extract video information. */
export declare const parseHevcSps: (sps: Uint8Array) => HevcSpsInfo | null;
/** Builds a HevcDecoderConfigurationRecord from an HEVC packet in Annex B format. */
export declare const extractHevcDecoderConfigurationRecord: (packetData: Uint8Array) => HevcDecoderConfigurationRecord | null;
/** Serializes an HevcDecoderConfigurationRecord into the format specified in Section 8.3.3.1 of ISO 14496-15. */
export declare const serializeHevcDecoderConfigurationRecord: (record: HevcDecoderConfigurationRecord) => Uint8Array<ArrayBuffer>;
/** Deserializes an HevcDecoderConfigurationRecord from the format specified in Section 8.3.3.1 of ISO 14496-15. */
export declare const deserializeHevcDecoderConfigurationRecord: (data: Uint8Array) => HevcDecoderConfigurationRecord | null;
export type Vp9CodecInfo = {
profile: number;
level: number;
bitDepth: number;
chromaSubsampling: number;
videoFullRangeFlag: number;
colourPrimaries: number;
transferCharacteristics: number;
matrixCoefficients: number;
};
export declare const extractVp9CodecInfoFromPacket: (packet: Uint8Array) => Vp9CodecInfo | null;
export type Av1CodecInfo = {
profile: number;
level: number;
tier: number;
bitDepth: number;
monochrome: number;
chromaSubsamplingX: number;
chromaSubsamplingY: number;
chromaSamplePosition: number;
};
/** Iterates over all OBUs in an AV1 packet bistream. */
export declare const iterateAv1PacketObus: (packet: Uint8Array) => Generator<{
type: number;
data: Uint8Array<ArrayBufferLike>;
}, void, unknown>;
/**
* When AV1 codec information is not provided by the container, we can still try to extract the information by digging
* into the AV1 bitstream.
*/
export declare const extractAv1CodecInfoFromPacket: (packet: Uint8Array) => Av1CodecInfo | null;
export declare const parseOpusIdentificationHeader: (bytes: Uint8Array) => {
outputChannelCount: number;
preSkip: number;
inputSampleRate: number;
outputGain: number;
channelMappingFamily: number;
channelMappingTable: Uint8Array<ArrayBufferLike> | null;
};
export declare const parseOpusTocByte: (packet: Uint8Array) => {
durationInSamples: number;
};
export declare const parseModesFromVorbisSetupPacket: (setupHeader: Uint8Array) => {
modeBlockflags: number[];
};
/** Determines a packet's type (key or delta) by digging into the packet bitstream. */
export declare const determineVideoPacketType: (codec: VideoCodec, decoderConfig: VideoDecoderConfig, packetData: Uint8Array) => PacketType | null;
export declare enum FlacBlockType {
STREAMINFO = 0,
VORBIS_COMMENT = 4,
PICTURE = 6
}
export declare const readVorbisComments: (bytes: Uint8Array, metadataTags: MetadataTags) => void;
export declare const createVorbisComments: (headerBytes: Uint8Array, tags: MetadataTags, writeImages: boolean) => Uint8Array<ArrayBuffer>;
/**
* Channel counts indexed by acmod (Table 4.3).
* Does NOT include LFE - add lfeon to get total channel count.
*/
export declare const AC3_ACMOD_CHANNEL_COUNTS: number[];
export interface Ac3FrameInfo {
/** Sample rate code */
fscod: number;
/** Bitstream ID */
bsid: number;
/** Bitstream mode */
bsmod: number;
/** Audio coding mode */
acmod: number;
/** LFE channel on */
lfeon: number;
/** Bit rate code (0-18, maps to bitrate via Table F.4.1) */
bitRateCode: number;
}
/**
* Parse an AC-3 syncframe to extract BSI (Bit Stream Information) fields.
* Section 4.3
*/
export declare const parseAc3SyncFrame: (data: Uint8Array) => Ac3FrameInfo | null;
/**
* AC-3 frame sizes in bytes, indexed by [3 * frmsizecod + fscod].
* fscod: 0=48kHz, 1=44.1kHz, 2=32kHz
* Values are 16-bit words * 2 (to convert to bytes).
* Table 4.13
*/
export declare const AC3_FRAME_SIZES: number[];
/** Number of samples per AC-3 syncframe (always 1536) */
export declare const AC3_SAMPLES_PER_FRAME = 1536;
/**
* AC-3 registration_descriptor for MPEG-TS.
* Section A.2.3
*/
export declare const AC3_REGISTRATION_DESCRIPTOR: Uint8Array<ArrayBuffer>;
/** E-AC-3 registration_descriptor for MPEG-TS/ */
export declare const EAC3_REGISTRATION_DESCRIPTOR: Uint8Array<ArrayBuffer>;
/** Number of audio blocks per syncframe, indexed by numblkscod */
export declare const EAC3_NUMBLKS_TABLE: number[];
/**
* E-AC-3 independent substream info.
* Each independent substream represents a separate audio program.
*/
export interface Eac3SubstreamInfo {
/** Sample rate code */
fscod: number;
/** Sample rate code 2 (ATSC A/52:2018) */
fscod2: number | null;
/** Bitstream ID */
bsid: number;
/** Bitstream mode */
bsmod: number;
/** Audio coding mode */
acmod: number;
/** LFE channel on */
lfeon: number;
/** Number of dependent substreams */
numDepSub: number;
/** Channel locations for dependent substreams */
chanLoc: number;
}
/**
* E-AC-3 decoder configuration (dec3 box contents).
*/
export interface Eac3FrameInfo {
/** Data rate in kbps */
dataRate: number;
/** Independent substreams */
substreams: Eac3SubstreamInfo[];
}
/**
* Parse an E-AC-3 syncframe to extract BSI fields.
* Section E.1.2
*/
export declare const parseEac3SyncFrame: (data: Uint8Array) => Eac3FrameInfo | null;
/**
* Parse a dec3 box to extract E-AC-3 parameters.
* Section F.6
*/
export declare const parseEac3Config: (data: Uint8Array) => Eac3FrameInfo | null;
/**
* Get sample rate from E-AC-3 config.
* See ATSC A/52:2018 for handling fscod2.
*/
export declare const getEac3SampleRate: (config: Eac3FrameInfo) => number | null;
/**
* Get channel count from E-AC-3 config (first independent substream only).
*/
export declare const getEac3ChannelCount: (config: Eac3FrameInfo) => number;
//# sourceMappingURL=codec-data.d.ts.map
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,165 @@
/*!
* 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 { Av1CodecInfo, AvcDecoderConfigurationRecord, HevcDecoderConfigurationRecord, Vp9CodecInfo } from './codec-data.js';
import { SubtitleMetadata } from './subtitles.js';
/**
* List of known video codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export declare const VIDEO_CODECS: readonly ["avc", "hevc", "vp9", "av1", "vp8"];
/**
* List of known PCM (uncompressed) audio codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export declare const PCM_AUDIO_CODECS: readonly ["pcm-s16", "pcm-s16be", "pcm-s24", "pcm-s24be", "pcm-s32", "pcm-s32be", "pcm-f32", "pcm-f32be", "pcm-f64", "pcm-f64be", "pcm-u8", "pcm-s8", "ulaw", "alaw"];
/**
* List of known compressed audio codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export declare const NON_PCM_AUDIO_CODECS: readonly ["aac", "opus", "mp3", "vorbis", "flac", "ac3", "eac3"];
/**
* List of known audio codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export declare const AUDIO_CODECS: readonly ["aac", "opus", "mp3", "vorbis", "flac", "ac3", "eac3", "pcm-s16", "pcm-s16be", "pcm-s24", "pcm-s24be", "pcm-s32", "pcm-s32be", "pcm-f32", "pcm-f32be", "pcm-f64", "pcm-f64be", "pcm-u8", "pcm-s8", "ulaw", "alaw"];
/**
* List of known subtitle codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export declare const SUBTITLE_CODECS: readonly ["webvtt"];
/**
* Union type of known video codecs.
* @group Codecs
* @public
*/
export type VideoCodec = typeof VIDEO_CODECS[number];
/**
* Union type of known audio codecs.
* @group Codecs
* @public
*/
export type AudioCodec = typeof AUDIO_CODECS[number];
export type PcmAudioCodec = typeof PCM_AUDIO_CODECS[number];
/**
* Union type of known subtitle codecs.
* @group Codecs
* @public
*/
export type SubtitleCodec = typeof SUBTITLE_CODECS[number];
/**
* Union type of known media codecs.
* @group Codecs
* @public
*/
export type MediaCodec = VideoCodec | AudioCodec | SubtitleCodec;
export declare const AVC_LEVEL_TABLE: {
maxMacroblocks: number;
maxBitrate: number;
maxDpbMbs: number;
level: number;
}[];
export declare const VP9_LEVEL_TABLE: {
maxPictureSize: number;
maxBitrate: number;
level: number;
}[];
export declare const buildVideoCodecString: (codec: VideoCodec, width: number, height: number, bitrate: number) => string;
export declare const generateVp9CodecConfigurationFromCodecString: (codecString: string) => number[];
export declare const generateAv1CodecConfigurationFromCodecString: (codecString: string) => number[];
export declare const extractVideoCodecString: (trackInfo: {
width: number;
height: number;
codec: VideoCodec | null;
codecDescription: Uint8Array | null;
colorSpace: VideoColorSpaceInit | null;
avcType: 1 | 3 | null;
avcCodecInfo: AvcDecoderConfigurationRecord | null;
hevcCodecInfo: HevcDecoderConfigurationRecord | null;
vp9CodecInfo: Vp9CodecInfo | null;
av1CodecInfo: Av1CodecInfo | null;
}) => string;
export declare const buildAudioCodecString: (codec: AudioCodec, numberOfChannels: number, sampleRate: number) => "pcm-s16" | "pcm-s16be" | "pcm-s24" | "pcm-s24be" | "pcm-s32" | "pcm-s32be" | "pcm-f32" | "pcm-f32be" | "pcm-f64" | "pcm-f64be" | "pcm-u8" | "pcm-s8" | "ulaw" | "alaw" | "opus" | "mp3" | "vorbis" | "flac" | "ac-3" | "ec-3" | "mp4a.40.29" | "mp4a.40.5" | "mp4a.40.2";
export type AacCodecInfo = {
isMpeg2: boolean;
objectType: number | null;
};
export declare const extractAudioCodecString: (trackInfo: {
codec: AudioCodec | null;
codecDescription: Uint8Array | null;
aacCodecInfo: AacCodecInfo | null;
}) => string;
export type AacAudioSpecificConfig = {
objectType: number;
frequencyIndex: number;
sampleRate: number | null;
channelConfiguration: number;
numberOfChannels: number | null;
};
export declare const aacFrequencyTable: number[];
export declare const aacChannelMap: number[];
export declare const parseAacAudioSpecificConfig: (bytes: Uint8Array | null) => AacAudioSpecificConfig;
export declare const buildAacAudioSpecificConfig: (config: {
objectType: number;
sampleRate: number;
numberOfChannels: number;
}) => Uint8Array<ArrayBuffer>;
export declare const OPUS_SAMPLE_RATE = 48000;
export declare const parsePcmCodec: (codec: PcmAudioCodec) => {
dataType: "ulaw";
sampleSize: 1;
littleEndian: boolean;
silentValue: number;
} | {
dataType: "alaw";
sampleSize: 1;
littleEndian: boolean;
silentValue: number;
} | {
dataType: "unsigned" | "signed" | "float";
sampleSize: 8 | 1 | 2 | 4 | 3;
littleEndian: boolean;
silentValue: number;
};
export declare const inferCodecFromCodecString: (codecString: string) => MediaCodec | null;
export declare const getVideoEncoderConfigExtension: (codec: VideoCodec) => {
avc: {
format: "avc";
};
hevc?: undefined;
} | {
hevc: {
format: "hevc";
};
avc?: undefined;
} | {
avc?: undefined;
hevc?: undefined;
};
export declare const getAudioEncoderConfigExtension: (codec: AudioCodec) => {
aac: {
format: "aac";
};
opus?: undefined;
} | {
opus: {
format: "opus";
};
aac?: undefined;
} | {
aac?: undefined;
opus?: undefined;
};
export declare const validateVideoChunkMetadata: (metadata: EncodedVideoChunkMetadata | undefined) => void;
export declare const validateAudioChunkMetadata: (metadata: EncodedAudioChunkMetadata | undefined) => void;
export declare const validateSubtitleMetadata: (metadata: SubtitleMetadata | undefined) => void;
//# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../../src/codec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,895 @@
/*!
* 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 { Bitstream, COLOR_PRIMARIES_MAP, MATRIX_COEFFICIENTS_MAP, TRANSFER_CHARACTERISTICS_MAP, assert, bytesToHexString, isAllowSharedBufferSource, last, reverseBitsU32, toDataView, } from './misc.js';
/**
* List of known video codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export const VIDEO_CODECS = [
'avc',
'hevc',
'vp9',
'av1',
'vp8',
];
/**
* List of known PCM (uncompressed) audio codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export const PCM_AUDIO_CODECS = [
'pcm-s16', // We don't prefix 'le' so we're compatible with the WebCodecs-registered PCM codec strings
'pcm-s16be',
'pcm-s24',
'pcm-s24be',
'pcm-s32',
'pcm-s32be',
'pcm-f32',
'pcm-f32be',
'pcm-f64',
'pcm-f64be',
'pcm-u8',
'pcm-s8',
'ulaw',
'alaw',
];
/**
* List of known compressed audio codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export const NON_PCM_AUDIO_CODECS = [
'aac',
'opus',
'mp3',
'vorbis',
'flac',
'ac3',
'eac3',
];
/**
* List of known audio codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export const AUDIO_CODECS = [
...NON_PCM_AUDIO_CODECS,
...PCM_AUDIO_CODECS,
];
/**
* List of known subtitle codecs, ordered by encoding preference.
* @group Codecs
* @public
*/
export const SUBTITLE_CODECS = [
'webvtt',
]; // TODO add the rest
// https://en.wikipedia.org/wiki/Advanced_Video_Coding
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
const HEVC_LEVEL_TABLE = [
{ maxPictureSize: 36864, maxBitrate: 128000, tier: 'L', level: 30 }, // Level 1 (Low Tier)
{ maxPictureSize: 122880, maxBitrate: 1500000, tier: 'L', level: 60 }, // Level 2 (Low Tier)
{ maxPictureSize: 245760, maxBitrate: 3000000, tier: 'L', level: 63 }, // Level 2.1 (Low Tier)
{ maxPictureSize: 552960, maxBitrate: 6000000, tier: 'L', level: 90 }, // Level 3 (Low Tier)
{ maxPictureSize: 983040, maxBitrate: 10000000, tier: 'L', level: 93 }, // Level 3.1 (Low Tier)
{ maxPictureSize: 2228224, maxBitrate: 12000000, tier: 'L', level: 120 }, // Level 4 (Low Tier)
{ maxPictureSize: 2228224, maxBitrate: 30000000, tier: 'H', level: 120 }, // Level 4 (High Tier)
{ maxPictureSize: 2228224, maxBitrate: 20000000, tier: 'L', level: 123 }, // Level 4.1 (Low Tier)
{ maxPictureSize: 2228224, maxBitrate: 50000000, tier: 'H', level: 123 }, // Level 4.1 (High Tier)
{ maxPictureSize: 8912896, maxBitrate: 25000000, tier: 'L', level: 150 }, // Level 5 (Low Tier)
{ maxPictureSize: 8912896, maxBitrate: 100000000, tier: 'H', level: 150 }, // Level 5 (High Tier)
{ maxPictureSize: 8912896, maxBitrate: 40000000, tier: 'L', level: 153 }, // Level 5.1 (Low Tier)
{ maxPictureSize: 8912896, maxBitrate: 160000000, tier: 'H', level: 153 }, // Level 5.1 (High Tier)
{ maxPictureSize: 8912896, maxBitrate: 60000000, tier: 'L', level: 156 }, // Level 5.2 (Low Tier)
{ maxPictureSize: 8912896, maxBitrate: 240000000, tier: 'H', level: 156 }, // Level 5.2 (High Tier)
{ maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'L', level: 180 }, // Level 6 (Low Tier)
{ maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 180 }, // Level 6 (High Tier)
{ maxPictureSize: 35651584, maxBitrate: 120000000, tier: 'L', level: 183 }, // Level 6.1 (Low Tier)
{ maxPictureSize: 35651584, maxBitrate: 480000000, tier: 'H', level: 183 }, // Level 6.1 (High Tier)
{ maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'L', level: 186 }, // Level 6.2 (Low Tier)
{ maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 186 }, // Level 6.2 (High Tier)
];
// https://en.wikipedia.org/wiki/VP9
export const VP9_LEVEL_TABLE = [
{ maxPictureSize: 36864, maxBitrate: 200000, level: 10 }, // Level 1
{ maxPictureSize: 73728, maxBitrate: 800000, level: 11 }, // Level 1.1
{ maxPictureSize: 122880, maxBitrate: 1800000, level: 20 }, // Level 2
{ maxPictureSize: 245760, maxBitrate: 3600000, level: 21 }, // Level 2.1
{ maxPictureSize: 552960, maxBitrate: 7200000, level: 30 }, // Level 3
{ maxPictureSize: 983040, maxBitrate: 12000000, level: 31 }, // Level 3.1
{ maxPictureSize: 2228224, maxBitrate: 18000000, level: 40 }, // Level 4
{ maxPictureSize: 2228224, maxBitrate: 30000000, level: 41 }, // Level 4.1
{ maxPictureSize: 8912896, maxBitrate: 60000000, level: 50 }, // Level 5
{ maxPictureSize: 8912896, maxBitrate: 120000000, level: 51 }, // Level 5.1
{ maxPictureSize: 8912896, maxBitrate: 180000000, level: 52 }, // Level 5.2
{ maxPictureSize: 35651584, maxBitrate: 180000000, level: 60 }, // Level 6
{ maxPictureSize: 35651584, maxBitrate: 240000000, level: 61 }, // Level 6.1
{ maxPictureSize: 35651584, maxBitrate: 480000000, level: 62 }, // Level 6.2
];
// https://en.wikipedia.org/wiki/AV1
const AV1_LEVEL_TABLE = [
{ maxPictureSize: 147456, maxBitrate: 1500000, tier: 'M', level: 0 }, // Level 2.0 (Main Tier)
{ maxPictureSize: 278784, maxBitrate: 3000000, tier: 'M', level: 1 }, // Level 2.1 (Main Tier)
{ maxPictureSize: 665856, maxBitrate: 6000000, tier: 'M', level: 4 }, // Level 3.0 (Main Tier)
{ maxPictureSize: 1065024, maxBitrate: 10000000, tier: 'M', level: 5 }, // Level 3.1 (Main Tier)
{ maxPictureSize: 2359296, maxBitrate: 12000000, tier: 'M', level: 8 }, // Level 4.0 (Main Tier)
{ maxPictureSize: 2359296, maxBitrate: 30000000, tier: 'H', level: 8 }, // Level 4.0 (High Tier)
{ maxPictureSize: 2359296, maxBitrate: 20000000, tier: 'M', level: 9 }, // Level 4.1 (Main Tier)
{ maxPictureSize: 2359296, maxBitrate: 50000000, tier: 'H', level: 9 }, // Level 4.1 (High Tier)
{ maxPictureSize: 8912896, maxBitrate: 30000000, tier: 'M', level: 12 }, // Level 5.0 (Main Tier)
{ maxPictureSize: 8912896, maxBitrate: 100000000, tier: 'H', level: 12 }, // Level 5.0 (High Tier)
{ maxPictureSize: 8912896, maxBitrate: 40000000, tier: 'M', level: 13 }, // Level 5.1 (Main Tier)
{ maxPictureSize: 8912896, maxBitrate: 160000000, tier: 'H', level: 13 }, // Level 5.1 (High Tier)
{ maxPictureSize: 8912896, maxBitrate: 60000000, tier: 'M', level: 14 }, // Level 5.2 (Main Tier)
{ maxPictureSize: 8912896, maxBitrate: 240000000, tier: 'H', level: 14 }, // Level 5.2 (High Tier)
{ maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'M', level: 15 }, // Level 5.3 (Main Tier)
{ maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 15 }, // Level 5.3 (High Tier)
{ maxPictureSize: 35651584, maxBitrate: 60000000, tier: 'M', level: 16 }, // Level 6.0 (Main Tier)
{ maxPictureSize: 35651584, maxBitrate: 240000000, tier: 'H', level: 16 }, // Level 6.0 (High Tier)
{ maxPictureSize: 35651584, maxBitrate: 100000000, tier: 'M', level: 17 }, // Level 6.1 (Main Tier)
{ maxPictureSize: 35651584, maxBitrate: 480000000, tier: 'H', level: 17 }, // Level 6.1 (High Tier)
{ maxPictureSize: 35651584, maxBitrate: 160000000, tier: 'M', level: 18 }, // Level 6.2 (Main Tier)
{ maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 18 }, // Level 6.2 (High Tier)
{ maxPictureSize: 35651584, maxBitrate: 160000000, tier: 'M', level: 19 }, // Level 6.3 (Main Tier)
{ maxPictureSize: 35651584, maxBitrate: 800000000, tier: 'H', level: 19 }, // Level 6.3 (High Tier)
];
const VP9_DEFAULT_SUFFIX = '.01.01.01.01.00';
const AV1_DEFAULT_SUFFIX = '.0.110.01.01.01.0';
export const buildVideoCodecString = (codec, width, height, bitrate) => {
if (codec === 'avc') {
const profileIndication = 0x64; // High Profile
const totalMacroblocks = Math.ceil(width / 16) * Math.ceil(height / 16);
// Determine the level based on the table
const levelInfo = AVC_LEVEL_TABLE.find(level => totalMacroblocks <= level.maxMacroblocks && bitrate <= level.maxBitrate) ?? last(AVC_LEVEL_TABLE);
const levelIndication = levelInfo ? levelInfo.level : 0;
const hexProfileIndication = profileIndication.toString(16).padStart(2, '0');
const hexProfileCompatibility = '00';
const hexLevelIndication = levelIndication.toString(16).padStart(2, '0');
return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`;
}
else if (codec === 'hevc') {
const profilePrefix = ''; // Profile space 0
const profileIdc = 1; // Main Profile
const compatibilityFlags = '6'; // Taken from the example in ISO 14496-15
const pictureSize = width * height;
const levelInfo = HEVC_LEVEL_TABLE.find(level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate) ?? last(HEVC_LEVEL_TABLE);
const constraintFlags = 'B0'; // Progressive source flag
return 'hev1.'
+ `${profilePrefix}${profileIdc}.`
+ `${compatibilityFlags}.`
+ `${levelInfo.tier}${levelInfo.level}.`
+ `${constraintFlags}`;
}
else if (codec === 'vp8') {
return 'vp8'; // Easy, this one
}
else if (codec === 'vp9') {
const profile = '00'; // Profile 0
const pictureSize = width * height;
const levelInfo = VP9_LEVEL_TABLE.find(level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate) ?? last(VP9_LEVEL_TABLE);
const bitDepth = '08'; // 8-bit
return `vp09.${profile}.${levelInfo.level.toString().padStart(2, '0')}.${bitDepth}`;
}
else if (codec === 'av1') {
const profile = 0; // Main Profile, single digit
const pictureSize = width * height;
const levelInfo = AV1_LEVEL_TABLE.find(level => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate) ?? last(AV1_LEVEL_TABLE);
const level = levelInfo.level.toString().padStart(2, '0');
const bitDepth = '08'; // 8-bit
return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}`;
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new TypeError(`Unhandled codec '${codec}'.`);
};
export const generateVp9CodecConfigurationFromCodecString = (codecString) => {
// Reference: https://www.webmproject.org/docs/container/#vp9-codec-feature-metadata-codecprivate
const parts = codecString.split('.'); // We can derive the required values from the codec string
const profile = Number(parts[1]);
const level = Number(parts[2]);
const bitDepth = Number(parts[3]);
const chromaSubsampling = parts[4] ? Number(parts[4]) : 1;
return [
1, 1, profile,
2, 1, level,
3, 1, bitDepth,
4, 1, chromaSubsampling,
];
};
export const generateAv1CodecConfigurationFromCodecString = (codecString) => {
// Reference: https://aomediacodec.github.io/av1-isobmff/
const parts = codecString.split('.'); // We can derive the required values from the codec string
const marker = 1;
const version = 1;
const firstByte = (marker << 7) + version;
const profile = Number(parts[1]);
const levelAndTier = parts[2];
const level = Number(levelAndTier.slice(0, -1));
const secondByte = (profile << 5) + level;
const tier = levelAndTier.slice(-1) === 'H' ? 1 : 0;
const bitDepth = Number(parts[3]);
const highBitDepth = bitDepth === 8 ? 0 : 1;
const twelveBit = 0;
const monochrome = parts[4] ? Number(parts[4]) : 0;
const chromaSubsamplingX = parts[5] ? Number(parts[5][0]) : 1;
const chromaSubsamplingY = parts[5] ? Number(parts[5][1]) : 1;
const chromaSamplePosition = parts[5] ? Number(parts[5][2]) : 0; // CSP_UNKNOWN
const thirdByte = (tier << 7)
+ (highBitDepth << 6)
+ (twelveBit << 5)
+ (monochrome << 4)
+ (chromaSubsamplingX << 3)
+ (chromaSubsamplingY << 2)
+ chromaSamplePosition;
const initialPresentationDelayPresent = 0; // Should be fine
const fourthByte = initialPresentationDelayPresent;
return [firstByte, secondByte, thirdByte, fourthByte];
};
export const extractVideoCodecString = (trackInfo) => {
const { codec, codecDescription, colorSpace, avcCodecInfo, hevcCodecInfo, vp9CodecInfo, av1CodecInfo } = trackInfo;
if (codec === 'avc') {
assert(trackInfo.avcType !== null);
if (avcCodecInfo) {
const bytes = new Uint8Array([
avcCodecInfo.avcProfileIndication,
avcCodecInfo.profileCompatibility,
avcCodecInfo.avcLevelIndication,
]);
return `avc${trackInfo.avcType}.${bytesToHexString(bytes)}`;
}
if (!codecDescription || codecDescription.byteLength < 4) {
throw new TypeError('AVC decoder description is not provided or is not at least 4 bytes long.');
}
return `avc${trackInfo.avcType}.${bytesToHexString(codecDescription.subarray(1, 4))}`;
}
else if (codec === 'hevc') {
let generalProfileSpace;
let generalProfileIdc;
let compatibilityFlags;
let generalTierFlag;
let generalLevelIdc;
let constraintFlags;
if (hevcCodecInfo) {
generalProfileSpace = hevcCodecInfo.generalProfileSpace;
generalProfileIdc = hevcCodecInfo.generalProfileIdc;
compatibilityFlags = reverseBitsU32(hevcCodecInfo.generalProfileCompatibilityFlags);
generalTierFlag = hevcCodecInfo.generalTierFlag;
generalLevelIdc = hevcCodecInfo.generalLevelIdc;
constraintFlags = [...hevcCodecInfo.generalConstraintIndicatorFlags];
}
else {
if (!codecDescription || codecDescription.byteLength < 23) {
throw new TypeError('HEVC decoder description is not provided or is not at least 23 bytes long.');
}
const view = toDataView(codecDescription);
const profileByte = view.getUint8(1);
generalProfileSpace = (profileByte >> 6) & 0x03;
generalProfileIdc = profileByte & 0x1F;
compatibilityFlags = reverseBitsU32(view.getUint32(2));
generalTierFlag = (profileByte >> 5) & 0x01;
generalLevelIdc = view.getUint8(12);
constraintFlags = [];
for (let i = 0; i < 6; i++) {
constraintFlags.push(view.getUint8(6 + i));
}
}
let codecString = 'hev1.';
codecString += ['', 'A', 'B', 'C'][generalProfileSpace] + generalProfileIdc;
codecString += '.';
codecString += compatibilityFlags.toString(16).toUpperCase();
codecString += '.';
codecString += generalTierFlag === 0 ? 'L' : 'H';
codecString += generalLevelIdc;
while (constraintFlags.length > 0 && constraintFlags[constraintFlags.length - 1] === 0) {
constraintFlags.pop();
}
if (constraintFlags.length > 0) {
codecString += '.';
codecString += constraintFlags.map(x => x.toString(16).toUpperCase()).join('.');
}
return codecString;
}
else if (codec === 'vp8') {
return 'vp8'; // Easy, this one
}
else if (codec === 'vp9') {
if (!vp9CodecInfo) {
// Calculate level based on dimensions
const pictureSize = trackInfo.width * trackInfo.height;
let level = last(VP9_LEVEL_TABLE).level; // Default to highest level
for (const entry of VP9_LEVEL_TABLE) {
if (pictureSize <= entry.maxPictureSize) {
level = entry.level;
break;
}
}
// We don't really know better, so let's return a general-purpose, common codec string and hope for the best
return `vp09.00.${level.toString().padStart(2, '0')}.08`;
}
const profile = vp9CodecInfo.profile.toString().padStart(2, '0');
const level = vp9CodecInfo.level.toString().padStart(2, '0');
const bitDepth = vp9CodecInfo.bitDepth.toString().padStart(2, '0');
const chromaSubsampling = vp9CodecInfo.chromaSubsampling.toString().padStart(2, '0');
const colourPrimaries = vp9CodecInfo.colourPrimaries.toString().padStart(2, '0');
const transferCharacteristics = vp9CodecInfo.transferCharacteristics.toString().padStart(2, '0');
const matrixCoefficients = vp9CodecInfo.matrixCoefficients.toString().padStart(2, '0');
const videoFullRangeFlag = vp9CodecInfo.videoFullRangeFlag.toString().padStart(2, '0');
let string = `vp09.${profile}.${level}.${bitDepth}.${chromaSubsampling}`;
string += `.${colourPrimaries}.${transferCharacteristics}.${matrixCoefficients}.${videoFullRangeFlag}`;
if (string.endsWith(VP9_DEFAULT_SUFFIX)) {
string = string.slice(0, -VP9_DEFAULT_SUFFIX.length);
}
return string;
}
else if (codec === 'av1') {
if (!av1CodecInfo) {
// Calculate level based on dimensions
const pictureSize = trackInfo.width * trackInfo.height;
let level = last(VP9_LEVEL_TABLE).level; // Default to highest level
for (const entry of VP9_LEVEL_TABLE) {
if (pictureSize <= entry.maxPictureSize) {
level = entry.level;
break;
}
}
// We don't really know better, so let's return a general-purpose, common codec string and hope for the best
return `av01.0.${level.toString().padStart(2, '0')}M.08`;
}
// https://aomediacodec.github.io/av1-isobmff/#codecsparam
const profile = av1CodecInfo.profile; // Single digit
const level = av1CodecInfo.level.toString().padStart(2, '0');
const tier = av1CodecInfo.tier ? 'H' : 'M';
const bitDepth = av1CodecInfo.bitDepth.toString().padStart(2, '0');
const monochrome = av1CodecInfo.monochrome ? '1' : '0';
const chromaSubsampling = 100 * av1CodecInfo.chromaSubsamplingX
+ 10 * av1CodecInfo.chromaSubsamplingY
+ 1 * (av1CodecInfo.chromaSubsamplingX && av1CodecInfo.chromaSubsamplingY
? av1CodecInfo.chromaSamplePosition
: 0);
// The defaults are 1 (ITU-R BT.709)
const colorPrimaries = colorSpace?.primaries ? COLOR_PRIMARIES_MAP[colorSpace.primaries] : 1;
const transferCharacteristics = colorSpace?.transfer ? TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer] : 1;
const matrixCoefficients = colorSpace?.matrix ? MATRIX_COEFFICIENTS_MAP[colorSpace.matrix] : 1;
const videoFullRangeFlag = colorSpace?.fullRange ? 1 : 0;
let string = `av01.${profile}.${level}${tier}.${bitDepth}`;
string += `.${monochrome}.${chromaSubsampling.toString().padStart(3, '0')}`;
string += `.${colorPrimaries.toString().padStart(2, '0')}`;
string += `.${transferCharacteristics.toString().padStart(2, '0')}`;
string += `.${matrixCoefficients.toString().padStart(2, '0')}`;
string += `.${videoFullRangeFlag}`;
if (string.endsWith(AV1_DEFAULT_SUFFIX)) {
string = string.slice(0, -AV1_DEFAULT_SUFFIX.length);
}
return string;
}
throw new TypeError(`Unhandled codec '${codec}'.`);
};
export const buildAudioCodecString = (codec, numberOfChannels, sampleRate) => {
if (codec === 'aac') {
// If stereo or higher channels and lower sample rate, likely using HE-AAC v2 with PS
if (numberOfChannels >= 2 && sampleRate <= 24000) {
return 'mp4a.40.29'; // HE-AAC v2 (AAC LC + SBR + PS)
}
// If sample rate is low, likely using HE-AAC v1 with SBR
if (sampleRate <= 24000) {
return 'mp4a.40.5'; // HE-AAC v1 (AAC LC + SBR)
}
// Default to standard AAC-LC for higher sample rates
return 'mp4a.40.2'; // AAC-LC
}
else if (codec === 'mp3') {
return 'mp3';
}
else if (codec === 'opus') {
return 'opus';
}
else if (codec === 'vorbis') {
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.includes(codec)) {
return codec;
}
throw new TypeError(`Unhandled codec '${codec}'.`);
};
export const extractAudioCodecString = (trackInfo) => {
const { codec, codecDescription, aacCodecInfo } = trackInfo;
if (codec === 'aac') {
if (!aacCodecInfo) {
throw new TypeError('AAC codec info must be provided.');
}
if (aacCodecInfo.isMpeg2) {
return 'mp4a.67';
}
else {
let objectType;
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';
}
else if (codec === 'opus') {
return 'opus';
}
else if (codec === 'vorbis') {
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.includes(codec)) {
return codec;
}
throw new TypeError(`Unhandled codec '${codec}'.`);
};
export const aacFrequencyTable = [
96000, 88200, 64000, 48000, 44100, 32000,
24000, 22050, 16000, 12000, 11025, 8000, 7350,
];
export const aacChannelMap = [-1, 1, 2, 3, 4, 5, 6, 8];
export const parseAacAudioSpecificConfig = (bytes) => {
if (!bytes || bytes.byteLength < 2) {
throw new TypeError('AAC description must be at least 2 bytes long.');
}
const bitstream = new Bitstream(bytes);
let objectType = bitstream.readBits(5);
if (objectType === 31) {
objectType = 32 + bitstream.readBits(6);
}
const frequencyIndex = bitstream.readBits(4);
let sampleRate = null;
if (frequencyIndex === 15) {
sampleRate = bitstream.readBits(24);
}
else {
if (frequencyIndex < aacFrequencyTable.length) {
sampleRate = aacFrequencyTable[frequencyIndex];
}
}
const channelConfiguration = bitstream.readBits(4);
let numberOfChannels = null;
if (channelConfiguration >= 1 && channelConfiguration <= 7) {
numberOfChannels = aacChannelMap[channelConfiguration];
}
return {
objectType,
frequencyIndex,
sampleRate,
channelConfiguration,
numberOfChannels,
};
};
export const buildAacAudioSpecificConfig = (config) => {
let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate);
let customSampleRate = null;
if (frequencyIndex === -1) {
frequencyIndex = 15;
customSampleRate = config.sampleRate;
}
const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels);
if (channelConfiguration === -1) {
throw new TypeError(`Unsupported number of channels: ${config.numberOfChannels}`);
}
let bitCount = 5 + 4 + 4;
if (config.objectType >= 32) {
bitCount += 6;
}
if (frequencyIndex === 15) {
bitCount += 24;
}
const byteCount = Math.ceil(bitCount / 8);
const bytes = new Uint8Array(byteCount);
const bitstream = new Bitstream(bytes);
if (config.objectType < 32) {
bitstream.writeBits(5, config.objectType);
}
else {
bitstream.writeBits(5, 31);
bitstream.writeBits(6, config.objectType - 32);
}
bitstream.writeBits(4, frequencyIndex);
if (frequencyIndex === 15) {
bitstream.writeBits(24, customSampleRate);
}
bitstream.writeBits(4, channelConfiguration);
return bytes;
};
export const OPUS_SAMPLE_RATE = 48_000;
const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;
export const parsePcmCodec = (codec) => {
assert(PCM_AUDIO_CODECS.includes(codec));
if (codec === 'ulaw') {
return { dataType: 'ulaw', sampleSize: 1, littleEndian: true, silentValue: 255 };
}
else if (codec === 'alaw') {
return { dataType: 'alaw', sampleSize: 1, littleEndian: true, silentValue: 213 };
}
const match = PCM_CODEC_REGEX.exec(codec);
assert(match);
let dataType;
if (match[1] === 'u') {
dataType = 'unsigned';
}
else if (match[1] === 's') {
dataType = 'signed';
}
else {
dataType = 'float';
}
const sampleSize = (Number(match[2]) / 8);
const littleEndian = match[3] !== 'be';
const silentValue = codec === 'pcm-u8' ? 2 ** 7 : 0;
return { dataType, sampleSize, littleEndian, silentValue };
};
export const inferCodecFromCodecString = (codecString) => {
// Video codecs
if (codecString.startsWith('avc1') || codecString.startsWith('avc3')) {
return 'avc';
}
else if (codecString.startsWith('hev1') || codecString.startsWith('hvc1')) {
return 'hevc';
}
else if (codecString === 'vp8') {
return 'vp8';
}
else if (codecString.startsWith('vp09')) {
return 'vp9';
}
else if (codecString.startsWith('av01')) {
return 'av1';
}
// Audio codecs
if (codecString.startsWith('mp4a.40') || codecString === 'mp4a.67') {
return 'aac';
}
else if (codecString === 'mp3'
|| codecString === 'mp4a.69'
|| codecString === 'mp4a.6B'
|| codecString === 'mp4a.6b') {
return 'mp3';
}
else if (codecString === 'opus') {
return 'opus';
}
else if (codecString === 'vorbis') {
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') {
return 'alaw';
}
else if (PCM_CODEC_REGEX.test(codecString)) {
return codecString;
}
// Subtitle codecs
if (codecString === 'webvtt') {
return 'webvtt';
}
return null;
};
export const getVideoEncoderConfigExtension = (codec) => {
if (codec === 'avc') {
return {
avc: {
format: 'avc', // Ensure the format is not Annex B
},
};
}
else if (codec === 'hevc') {
return {
hevc: {
format: 'hevc', // Ensure the format is not Annex B
},
};
}
return {};
};
export const getAudioEncoderConfigExtension = (codec) => {
if (codec === 'aac') {
return {
aac: {
format: 'aac', // Ensure the format is not ADTS
},
};
}
else if (codec === 'opus') {
return {
opus: {
format: 'opus',
},
};
}
return {};
};
const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01'];
const AVC_CODEC_STRING_REGEX = /^(avc1|avc3)\.[0-9a-fA-F]{6}$/;
const HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\.(?:[ABC]?\d+)\.[0-9a-fA-F]{1,8}\.[LH]\d+(?:\.[0-9a-fA-F]{1,2}){0,6}$/;
const VP9_CODEC_STRING_REGEX = /^vp09(?:\.\d{2}){3}(?:(?:\.\d{2}){5})?$/;
const AV1_CODEC_STRING_REGEX = /^av01\.\d\.\d{2}[MH]\.\d{2}(?:\.\d\.\d{3}\.\d{2}\.\d{2}\.\d{2}\.\d)?$/;
export const validateVideoChunkMetadata = (metadata) => {
if (!metadata) {
throw new TypeError('Video chunk metadata must be provided.');
}
if (typeof metadata !== 'object') {
throw new TypeError('Video chunk metadata must be an object.');
}
if (!metadata.decoderConfig) {
throw new TypeError('Video chunk metadata must include a decoder configuration.');
}
if (typeof metadata.decoderConfig !== 'object') {
throw new TypeError('Video chunk metadata decoder configuration must be an object.');
}
if (typeof metadata.decoderConfig.codec !== 'string') {
throw new TypeError('Video chunk metadata decoder configuration must specify a codec string.');
}
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 Mediabunny Codec Registry.');
}
if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth <= 0) {
throw new TypeError('Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).');
}
if (!Number.isInteger(metadata.decoderConfig.codedHeight) || metadata.decoderConfig.codedHeight <= 0) {
throw new TypeError('Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).');
}
if (metadata.decoderConfig.description !== undefined) {
if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
throw new TypeError('Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an'
+ ' ArrayBuffer view.');
}
}
if (metadata.decoderConfig.colorSpace !== undefined) {
const { colorSpace } = metadata.decoderConfig;
if (typeof colorSpace !== 'object') {
throw new TypeError('Video chunk metadata decoder configuration colorSpace, when provided, must be an object.');
}
const primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
if (colorSpace.primaries != null && !primariesValues.includes(colorSpace.primaries)) {
throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of`
+ ` ${primariesValues.join(', ')}.`);
}
const transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
if (colorSpace.transfer != null && !transferValues.includes(colorSpace.transfer)) {
throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of`
+ ` ${transferValues.join(', ')}.`);
}
const matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
if (colorSpace.matrix != null && !matrixValues.includes(colorSpace.matrix)) {
throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of`
+ ` ${matrixValues.join(', ')}.`);
}
if (colorSpace.fullRange != null && typeof colorSpace.fullRange !== 'boolean') {
throw new TypeError('Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.');
}
}
if (metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3')) {
// AVC-specific validation
if (!AVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError('Video chunk metadata decoder configuration codec string for AVC must be a valid AVC codec string as'
+ ' specified in Section 3.4 of RFC 6381.');
}
// `description` may or may not be set, depending on if the format is AVCC or Annex B, so don't perform any
// validation for it.
// https://www.w3.org/TR/webcodecs-avc-codec-registration
}
else if (metadata.decoderConfig.codec.startsWith('hev1') || metadata.decoderConfig.codec.startsWith('hvc1')) {
// HEVC-specific validation
if (!HEVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError('Video chunk metadata decoder configuration codec string for HEVC must be a valid HEVC codec string as'
+ ' specified in Section E.3 of ISO 14496-15.');
}
// `description` may or may not be set, depending on if the format is HEVC or Annex B, so don't perform any
// validation for it.
// https://www.w3.org/TR/webcodecs-hevc-codec-registration
}
else if (metadata.decoderConfig.codec.startsWith('vp8')) {
// VP8-specific validation
if (metadata.decoderConfig.codec !== 'vp8') {
throw new TypeError('Video chunk metadata decoder configuration codec string for VP8 must be "vp8".');
}
}
else if (metadata.decoderConfig.codec.startsWith('vp09')) {
// VP9-specific validation
if (!VP9_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError('Video chunk metadata decoder configuration codec string for VP9 must be a valid VP9 codec string as'
+ ' specified in Section "Codecs Parameter String" of https://www.webmproject.org/vp9/mp4/.');
}
}
else if (metadata.decoderConfig.codec.startsWith('av01')) {
// AV1-specific validation
if (!AV1_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError('Video chunk metadata decoder configuration codec string for AV1 must be a valid AV1 codec string as'
+ ' specified in Section "Codecs Parameter String" of https://aomediacodec.github.io/av1-isobmff/.');
}
}
};
const VALID_AUDIO_CODEC_STRING_PREFIXES = [
'mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm', 'ac-3', 'ec-3',
];
export const validateAudioChunkMetadata = (metadata) => {
if (!metadata) {
throw new TypeError('Audio chunk metadata must be provided.');
}
if (typeof metadata !== 'object') {
throw new TypeError('Audio chunk metadata must be an object.');
}
if (!metadata.decoderConfig) {
throw new TypeError('Audio chunk metadata must include a decoder configuration.');
}
if (typeof metadata.decoderConfig !== 'object') {
throw new TypeError('Audio chunk metadata decoder configuration must be an object.');
}
if (typeof metadata.decoderConfig.codec !== 'string') {
throw new TypeError('Audio chunk metadata decoder configuration must specify a codec string.');
}
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 Mediabunny Codec Registry.');
}
if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
throw new TypeError('Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).');
}
if (!Number.isInteger(metadata.decoderConfig.numberOfChannels) || metadata.decoderConfig.numberOfChannels <= 0) {
throw new TypeError('Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).');
}
if (metadata.decoderConfig.description !== undefined) {
if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
throw new TypeError('Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an'
+ ' ArrayBuffer view.');
}
}
if (metadata.decoderConfig.codec.startsWith('mp4a')
// These three refer to MP3:
&& metadata.decoderConfig.codec !== 'mp4a.69'
&& metadata.decoderConfig.codec !== 'mp4a.6B'
&& metadata.decoderConfig.codec !== 'mp4a.6b') {
// AAC-specific validation
const validStrings = ['mp4a.40.2', 'mp4a.40.02', 'mp4a.40.5', 'mp4a.40.05', 'mp4a.40.29', 'mp4a.67'];
if (!validStrings.includes(metadata.decoderConfig.codec)) {
throw new TypeError('Audio chunk metadata decoder configuration codec string for AAC must be a valid AAC codec string as'
+ ' specified in https://www.w3.org/TR/webcodecs-aac-codec-registration/.');
}
// `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
if (metadata.decoderConfig.codec !== 'mp3'
&& metadata.decoderConfig.codec !== 'mp4a.69'
&& metadata.decoderConfig.codec !== 'mp4a.6B'
&& metadata.decoderConfig.codec !== 'mp4a.6b') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for MP3 must be "mp3", "mp4a.69" or'
+ ' "mp4a.6B".');
}
}
else if (metadata.decoderConfig.codec.startsWith('opus')) {
// Opus-specific validation
if (metadata.decoderConfig.codec !== 'opus') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for Opus must be "opus".');
}
if (metadata.decoderConfig.description && metadata.decoderConfig.description.byteLength < 18) {
// Description is optional for Opus per-spec, so we shouldn't enforce it
throw new TypeError('Audio chunk metadata decoder configuration description, when specified, is expected to be an'
+ ' Identification Header as specified in Section 5.1 of RFC 7845.');
}
}
else if (metadata.decoderConfig.codec.startsWith('vorbis')) {
// Vorbis-specific validation
if (metadata.decoderConfig.codec !== 'vorbis') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for Vorbis must be "vorbis".');
}
if (!metadata.decoderConfig.description) {
throw new TypeError('Audio chunk metadata decoder configuration for Vorbis must include a description, which is expected to'
+ ' adhere to the format described in https://www.w3.org/TR/webcodecs-vorbis-codec-registration/.');
}
}
else if (metadata.decoderConfig.codec.startsWith('flac')) {
// FLAC-specific validation
if (metadata.decoderConfig.codec !== 'flac') {
throw new TypeError('Audio chunk metadata decoder configuration codec string for FLAC must be "flac".');
}
const minDescriptionSize = 4 + 4 + 34; // 'fLaC' + metadata block header + STREAMINFO block
if (!metadata.decoderConfig.description || metadata.decoderConfig.description.byteLength < minDescriptionSize) {
throw new TypeError('Audio chunk metadata decoder configuration for FLAC must include a description, which is expected to'
+ ' 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')
|| metadata.decoderConfig.codec.startsWith('alaw')) {
// PCM-specific validation
if (!PCM_AUDIO_CODECS.includes(metadata.decoderConfig.codec)) {
throw new TypeError('Audio chunk metadata decoder configuration codec string for PCM must be one of the supported PCM'
+ ` codecs (${PCM_AUDIO_CODECS.join(', ')}).`);
}
}
};
export const validateSubtitleMetadata = (metadata) => {
if (!metadata) {
throw new TypeError('Subtitle metadata must be provided.');
}
if (typeof metadata !== 'object') {
throw new TypeError('Subtitle metadata must be an object.');
}
if (!metadata.config) {
throw new TypeError('Subtitle metadata must include a config object.');
}
if (typeof metadata.config !== 'object') {
throw new TypeError('Subtitle metadata config must be an object.');
}
if (typeof metadata.config.description !== 'string') {
throw new TypeError('Subtitle metadata config description must be a string.');
}
};
@@ -0,0 +1,334 @@
/*!
* 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 { AudioCodec, VideoCodec } from './codec.js';
import { Quality } from './encode.js';
import { Input } from './input.js';
import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track.js';
import { MaybePromise, Rotation } from './misc.js';
import { Output } from './output.js';
import { AudioSample, VideoSample } from './sample.js';
import { MetadataTags } from './metadata.js';
/**
* The options for media file conversion.
* @group Conversion
* @public
*/
export type ConversionOptions = {
/** The input file. */
input: Input;
/** The output file. */
output: Output;
/**
* 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. Using `n` is deprecated, prefer the
* identical `track.number` instead.
*/
video?: ConversionVideoOptions | ((track: InputVideoTrack, n: number) => MaybePromise<ConversionVideoOptions | undefined>);
/**
* 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. Using `n` is deprecated, prefer the
* identical `track.number` instead.
*/
audio?: ConversionAudioOptions | ((track: InputAudioTrack, n: number) => MaybePromise<ConversionAudioOptions | undefined>);
/** Options to trim the input file. */
trim?: {
/**
* The time in the input file in seconds at which the output file should start. Must be less than `end`.
* When omitted, defaults to the start timestamp of the input or to 0, whichever is higher.
*/
start?: number;
/**
* The time in the input file in seconds at which the output file should end. Must be greater than `start`.
* Defaults to the duration of the input when omitted.
*/
end?: number;
};
/**
* An object or a callback that returns or resolves to an object containing the descriptive metadata tags that
* should be written to the output file. If a function is passed, it will be passed the tags of the input file as
* its first argument, allowing you to modify, augment or extend them.
*
* If no function is set, the input's metadata tags will be copied to the output.
*/
tags?: MetadataTags | ((inputTags: MetadataTags) => MaybePromise<MetadataTags>);
/**
* Whether to show potential console warnings about discarded tracks after calling `Conversion.init()`, defaults to
* `true`. Set this to `false` if you're properly handling the `discardedTracks` and `isValid` fields already and
* want to keep the console output clean.
*/
showWarnings?: boolean;
};
/**
* Video-specific options.
* @group Conversion
* @public
*/
export type ConversionVideoOptions = {
/** If `true`, all video tracks will be discarded and will not be present in the output. */
discard?: boolean;
/**
* The desired width of the output video in pixels, defaulting to the video's natural display width. If height
* is not set, it will be deduced automatically based on aspect ratio.
*/
width?: number;
/**
* The desired height of the output video in pixels, defaulting to the video's natural display height. If width
* is not set, it will be deduced automatically based on aspect ratio.
*/
height?: number;
/**
* The fitting algorithm in case both width and height are set, or if the input video changes its size over time.
*
* - `'fill'` will stretch the image to fill the entire box, potentially altering aspect ratio.
* - `'contain'` will contain the entire image within the box while preserving aspect ratio. This may lead to
* letterboxing.
* - `'cover'` will scale the image until the entire box is filled, while preserving aspect ratio.
*/
fit?: 'fill' | 'contain' | 'cover';
/**
* The angle in degrees to rotate the input video by, clockwise. Rotation is applied before cropping and resizing.
* 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.
*/
crop?: {
/** The distance in pixels from the left edge of the source frame to the left edge of the crop rectangle. */
left: number;
/** The distance in pixels from the top edge of the source frame to the top edge of the crop rectangle. */
top: number;
/** The width in pixels of the crop rectangle. */
width: number;
/** The height in pixels of the crop rectangle. */
height: number;
};
/**
* The desired frame rate of the output video, in hertz. If not specified, the original input frame rate will
* be used (which may be variable).
*/
frameRate?: number;
/** The desired output video codec. */
codec?: VideoCodec;
/** The desired bitrate of the output video. */
bitrate?: number | Quality;
/**
* Whether to discard or keep the transparency information of the input video. The default is `'discard'`. Note that
* for `'keep'` to produce a transparent video, you must use an output config that supports it, such as WebM with
* VP9.
*/
alpha?: 'discard' | 'keep';
/**
* The interval, in seconds, of how often frames are encoded as a key frame. The default is 5 seconds. Frequent key
* frames improve seeking behavior but increase file size. When using multiple video tracks, you should give them
* all the same key frame interval.
*
* 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;
/**
* Allows for custom user-defined processing of video frames, e.g. for applying overlays, color transformations, or
* timestamp modifications. Will be called for each input video sample after transformations and frame rate
* corrections.
*
* Must return a {@link VideoSample} or a `CanvasImageSource`, an array of them, or `null` for dropping the frame.
* When non-timestamped data is returned, the timestamp and duration from the source sample will be used. Rotation
* metadata of the returned sample will be ignored.
*
* This function can also be used to manually resize frames. When doing so, you should signal the post-process
* dimensions using the `processedWidth` and `processedHeight` fields, which enables the encoder to better know what
* to expect. If these fields aren't set, Mediabunny will assume you won't perform any resizing.
*/
process?: (sample: VideoSample) => MaybePromise<CanvasImageSource | VideoSample | (CanvasImageSource | VideoSample)[] | null>;
/**
* An optional hint specifying the width of video samples returned by the `process` function, for better
* encoder configuration.
*/
processedWidth?: number;
/**
* An optional hint specifying the height of video samples returned by the `process` function, for better
* encoder configuration.
*/
processedHeight?: number;
};
/**
* Audio-specific options.
* @group Conversion
* @public
*/
export type ConversionAudioOptions = {
/** If `true`, all audio tracks will be discarded and will not be present in the output. */
discard?: boolean;
/** The desired channel count of the output audio. */
numberOfChannels?: number;
/** The desired sample rate of the output audio, in hertz. */
sampleRate?: number;
/** The desired output audio codec. */
codec?: AudioCodec;
/** The desired bitrate of the output audio. */
bitrate?: number | Quality;
/** When `true`, audio will always be re-encoded instead of directly copying over the encoded samples. */
forceTranscode?: boolean;
/**
* Allows for custom user-defined processing of audio samples, e.g. for applying audio effects, transformations, or
* timestamp modifications. Will be called for each input audio sample after remixing and resampling.
*
* Must return an {@link AudioSample}, an array of them, or `null` for dropping the sample.
*
* This function can also be used to manually perform remixing or resampling. When doing so, you should signal the
* post-process parameters using the `processedNumberOfChannels` and `processedSampleRate` fields, which enables the
* encoder to better know what to expect. If these fields aren't set, Mediabunny will assume you won't perform
* remixing or resampling.
*/
process?: (sample: AudioSample) => MaybePromise<AudioSample | AudioSample[] | null>;
/**
* An optional hint specifying the channel count of audio samples returned by the `process` function, for better
* encoder configuration.
*/
processedNumberOfChannels?: number;
/**
* An optional hint specifying the sample rate of audio samples returned by the `process` function, for better
* encoder configuration.
*/
processedSampleRate?: number;
};
/**
* An input track that was discarded (excluded) from a {@link Conversion} alongside the discard reason.
* @group Conversion
* @public
*/
export type DiscardedTrack = {
/** The track that was discarded. */
track: InputTrack;
/**
* The reason for discarding the track.
*
* - `'discarded_by_user'`: You discarded this track by setting `discard: true`.
* - `'max_track_count_reached'`: The output had no more room for another track.
* - `'max_track_count_of_type_reached'`: The output had no more room for another track of this type, or the output
* doesn't support this track type at all.
* - `'unknown_source_codec'`: We don't know the codec of the input track and therefore don't know what to do
* with it.
* - `'undecodable_source_codec'`: The input track's codec is known, but we are unable to decode it.
* - `'no_encodable_target_codec'`: We can't find a codec that we are able to encode and that can be contained
* within the output format. This reason can be hit if the environment doesn't support the necessary encoders, or if
* you requested a codec that cannot be contained within the output format.
*/
reason: 'discarded_by_user' | 'max_track_count_reached' | 'max_track_count_of_type_reached' | 'unknown_source_codec' | 'undecodable_source_codec' | 'no_encodable_target_codec';
};
/**
* Represents a media file conversion process, used to convert one media file into another. In addition to conversion,
* this class can be used to resize and rotate video, resample audio, drop tracks, or trim to a specific time range.
* @group Conversion
* @public
*/
export declare class Conversion {
/** The input file. */
readonly input: Input;
/** The output file. */
readonly output: Output;
/**
* A callback that is fired whenever the conversion progresses. Returns a number between 0 and 1, indicating the
* completion of the conversion. Note that a progress of 1 doesn't necessarily mean the conversion is complete;
* the conversion is complete once `execute()` resolves.
*
* In order for progress to be computed, this property must be set before `execute` is called.
*/
onProgress?: (progress: number) => unknown;
/**
* Whether this conversion, as it has been configured, is valid and can be executed. If this field is `false`, check
* the `discardedTracks` field for reasons.
*/
isValid: boolean;
/** The list of tracks that are included in the output file. */
readonly utilizedTracks: InputTrack[];
/** The list of tracks from the input file that have been discarded, alongside the discard reason. */
readonly discardedTracks: DiscardedTrack[];
/** Initializes a new conversion process without starting the conversion. */
static init(options: ConversionOptions): Promise<Conversion>;
/** Creates a new Conversion instance (duh). */
private constructor();
/**
* Executes the conversion process. Resolves once conversion is complete.
*
* Will throw if `isValid` is `false`.
*/
execute(): Promise<void>;
/**
* Cancels the conversion process, causing any ongoing `execute` call to throw a `ConversionCanceledError`.
* Does nothing if the conversion is already complete.
*/
cancel(): Promise<void>;
}
/**
* Thrown when a conversion couldn't complete due to being canceled.
* @group Conversion
* @public
*/
export declare class ConversionCanceledError extends Error {
/** Creates a new {@link ConversionCanceledError}. */
constructor(message?: string);
}
/**
* Utility class to handle audio resampling, handling both sample rate resampling as well as channel up/downmixing.
* The advantage over doing this manually rather than using OfflineAudioContext to do it for us is the artifact-free
* handling of putting multiple resampled audio samples back to back, which produces flaky results using
* OfflineAudioContext.
*/
export declare class AudioResampler {
sourceSampleRate: number | null;
targetSampleRate: number;
sourceNumberOfChannels: number | null;
targetNumberOfChannels: number;
startTime: number;
endTime: number;
onSample: (sample: AudioSample) => Promise<void>;
bufferSizeInFrames: number;
bufferSizeInSamples: number;
outputBuffer: Float32Array;
/** Start frame of current buffer */
bufferStartFrame: number;
/** The highest index written to in the current buffer */
maxWrittenFrame: number;
channelMixer: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number;
tempSourceBuffer: Float32Array;
constructor(options: {
targetSampleRate: number;
targetNumberOfChannels: number;
startTime: number;
endTime: number;
onSample: (sample: AudioSample) => Promise<void>;
});
/**
* Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match.
*/
doChannelMixerSetup(): void;
ensureTempBufferSize(requiredSamples: number): void;
add(audioSample: AudioSample): Promise<void>;
finalizeCurrentBuffer(): Promise<void>;
finalize(): Promise<void>;
}
//# sourceMappingURL=conversion.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"conversion.d.ts","sourceRoot":"","sources":["../../../src/conversion.ts"],"names":[],"mappings":"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"}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
/*!
* 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 { AudioCodec, VideoCodec } from './codec.js';
import { MaybePromise } from './misc.js';
import { EncodedPacket } from './packet.js';
import { AudioSample, VideoSample } from './sample.js';
/**
* Base class for custom video decoders. To add your own custom video decoder, extend this class, implement the
* abstract methods and static `supports` method, and register the decoder using {@link registerDecoder}.
* @group Custom coders
* @public
*/
export declare abstract class CustomVideoDecoder {
/** The input video's codec. */
readonly codec: VideoCodec;
/** The input video's decoder config. */
readonly config: VideoDecoderConfig;
/** The callback to call when a decoded VideoSample is available. */
readonly onSample: (sample: VideoSample) => unknown;
/** Returns true if and only if the decoder can decode the given codec configuration. */
static supports(codec: VideoCodec, config: VideoDecoderConfig): boolean;
/** Called after decoder creation; can be used for custom initialization logic. */
abstract init(): MaybePromise<void>;
/** Decodes the provided encoded packet. */
abstract decode(packet: EncodedPacket): MaybePromise<void>;
/** Decodes all remaining packets and then resolves. */
abstract flush(): MaybePromise<void>;
/** Called when the decoder is no longer needed and its resources can be freed. */
abstract close(): MaybePromise<void>;
}
/**
* Base class for custom audio decoders. To add your own custom audio decoder, extend this class, implement the
* abstract methods and static `supports` method, and register the decoder using {@link registerDecoder}.
* @group Custom coders
* @public
*/
export declare abstract class CustomAudioDecoder {
/** The input audio's codec. */
readonly codec: AudioCodec;
/** The input audio's decoder config. */
readonly config: AudioDecoderConfig;
/** The callback to call when a decoded AudioSample is available. */
readonly onSample: (sample: AudioSample) => unknown;
/** Returns true if and only if the decoder can decode the given codec configuration. */
static supports(codec: AudioCodec, config: AudioDecoderConfig): boolean;
/** Called after decoder creation; can be used for custom initialization logic. */
abstract init(): MaybePromise<void>;
/** Decodes the provided encoded packet. */
abstract decode(packet: EncodedPacket): MaybePromise<void>;
/** Decodes all remaining packets and then resolves. */
abstract flush(): MaybePromise<void>;
/** Called when the decoder is no longer needed and its resources can be freed. */
abstract close(): MaybePromise<void>;
}
/**
* Base class for custom video encoders. To add your own custom video encoder, extend this class, implement the
* abstract methods and static `supports` method, and register the encoder using {@link registerEncoder}.
* @group Custom coders
* @public
*/
export declare abstract class CustomVideoEncoder {
/** The codec with which to encode the video. */
readonly codec: VideoCodec;
/** Config for the encoder. */
readonly config: VideoEncoderConfig;
/** The callback to call when an EncodedPacket is available. */
readonly onPacket: (packet: EncodedPacket, meta?: EncodedVideoChunkMetadata) => unknown;
/** Returns true if and only if the encoder can encode the given codec configuration. */
static supports(codec: VideoCodec, config: VideoEncoderConfig): boolean;
/** Called after encoder creation; can be used for custom initialization logic. */
abstract init(): MaybePromise<void>;
/** Encodes the provided video sample. */
abstract encode(videoSample: VideoSample, options: VideoEncoderEncodeOptions): MaybePromise<void>;
/** Encodes all remaining video samples and then resolves. */
abstract flush(): MaybePromise<void>;
/** Called when the encoder is no longer needed and its resources can be freed. */
abstract close(): MaybePromise<void>;
}
/**
* Base class for custom audio encoders. To add your own custom audio encoder, extend this class, implement the
* abstract methods and static `supports` method, and register the encoder using {@link registerEncoder}.
* @group Custom coders
* @public
*/
export declare abstract class CustomAudioEncoder {
/** The codec with which to encode the audio. */
readonly codec: AudioCodec;
/** Config for the encoder. */
readonly config: AudioEncoderConfig;
/** The callback to call when an EncodedPacket is available. */
readonly onPacket: (packet: EncodedPacket, meta?: EncodedAudioChunkMetadata) => unknown;
/** Returns true if and only if the encoder can encode the given codec configuration. */
static supports(codec: AudioCodec, config: AudioEncoderConfig): boolean;
/** Called after encoder creation; can be used for custom initialization logic. */
abstract init(): MaybePromise<void>;
/** Encodes the provided audio sample. */
abstract encode(audioSample: AudioSample): MaybePromise<void>;
/** Encodes all remaining audio samples and then resolves. */
abstract flush(): MaybePromise<void>;
/** Called when the encoder is no longer needed and its resources can be freed. */
abstract close(): MaybePromise<void>;
}
export declare const customVideoDecoders: typeof CustomVideoDecoder[];
export declare const customAudioDecoders: typeof CustomAudioDecoder[];
export declare const customVideoEncoders: typeof CustomVideoEncoder[];
export declare const customAudioEncoders: typeof CustomAudioEncoder[];
/**
* Registers a custom video or audio decoder. Registered decoders will automatically be used for decoding whenever
* possible.
* @group Custom coders
* @public
*/
export declare const registerDecoder: (decoder: typeof CustomVideoDecoder | typeof CustomAudioDecoder) => void;
/**
* Registers a custom video or audio encoder. Registered encoders will automatically be used for encoding whenever
* possible.
* @group Custom coders
* @public
*/
export declare const registerEncoder: (encoder: typeof CustomVideoEncoder | typeof CustomAudioEncoder) => void;
//# sourceMappingURL=custom-coder.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"custom-coder.d.ts","sourceRoot":"","sources":["../../../src/custom-coder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
/*!
* 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/.
*/
/**
* Base class for custom video decoders. To add your own custom video decoder, extend this class, implement the
* abstract methods and static `supports` method, and register the decoder using {@link registerDecoder}.
* @group Custom coders
* @public
*/
export class CustomVideoDecoder {
/** Returns true if and only if the decoder can decode the given codec configuration. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static supports(codec, config) {
return false;
}
}
/**
* Base class for custom audio decoders. To add your own custom audio decoder, extend this class, implement the
* abstract methods and static `supports` method, and register the decoder using {@link registerDecoder}.
* @group Custom coders
* @public
*/
export class CustomAudioDecoder {
/** Returns true if and only if the decoder can decode the given codec configuration. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static supports(codec, config) {
return false;
}
}
/**
* Base class for custom video encoders. To add your own custom video encoder, extend this class, implement the
* abstract methods and static `supports` method, and register the encoder using {@link registerEncoder}.
* @group Custom coders
* @public
*/
export class CustomVideoEncoder {
/** Returns true if and only if the encoder can encode the given codec configuration. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static supports(codec, config) {
return false;
}
}
/**
* Base class for custom audio encoders. To add your own custom audio encoder, extend this class, implement the
* abstract methods and static `supports` method, and register the encoder using {@link registerEncoder}.
* @group Custom coders
* @public
*/
export class CustomAudioEncoder {
/** Returns true if and only if the encoder can encode the given codec configuration. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static supports(codec, config) {
return false;
}
}
export const customVideoDecoders = [];
export const customAudioDecoders = [];
export const customVideoEncoders = [];
export const customAudioEncoders = [];
/**
* Registers a custom video or audio decoder. Registered decoders will automatically be used for decoding whenever
* possible.
* @group Custom coders
* @public
*/
export const registerDecoder = (decoder) => {
if (decoder.prototype instanceof CustomVideoDecoder) {
const casted = decoder;
if (customVideoDecoders.includes(casted)) {
console.warn('Video decoder already registered.');
return;
}
customVideoDecoders.push(casted);
}
else if (decoder.prototype instanceof CustomAudioDecoder) {
const casted = decoder;
if (customAudioDecoders.includes(casted)) {
console.warn('Audio decoder already registered.');
return;
}
customAudioDecoders.push(casted);
}
else {
throw new TypeError('Decoder must be a CustomVideoDecoder or CustomAudioDecoder.');
}
};
/**
* Registers a custom video or audio encoder. Registered encoders will automatically be used for encoding whenever
* possible.
* @group Custom coders
* @public
*/
export const registerEncoder = (encoder) => {
if (encoder.prototype instanceof CustomVideoEncoder) {
const casted = encoder;
if (customVideoEncoders.includes(casted)) {
console.warn('Video encoder already registered.');
return;
}
customVideoEncoders.push(casted);
}
else if (encoder.prototype instanceof CustomAudioEncoder) {
const casted = encoder;
if (customAudioEncoders.includes(casted)) {
console.warn('Audio encoder already registered.');
return;
}
customAudioEncoders.push(casted);
}
else {
throw new TypeError('Encoder must be a CustomVideoEncoder or CustomAudioEncoder.');
}
};
@@ -0,0 +1,19 @@
/*!
* 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 { Input } from './input.js';
import { InputTrack } from './input-track.js';
import { MetadataTags } from './metadata.js';
export declare abstract class Demuxer {
input: Input;
constructor(input: Input);
abstract computeDuration(): Promise<number>;
abstract getTracks(): Promise<InputTrack[]>;
abstract getMimeType(): Promise<string>;
abstract getMetadataTags(): Promise<MetadataTags>;
}
//# sourceMappingURL=demuxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"demuxer.d.ts","sourceRoot":"","sources":["../../../src/demuxer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,8BAAsB,OAAO;IAC5B,KAAK,EAAE,KAAK,CAAC;gBAED,KAAK,EAAE,KAAK;IAIxB,QAAQ,CAAC,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAC3C,QAAQ,CAAC,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;IACvC,QAAQ,CAAC,eAAe,IAAI,OAAO,CAAC,YAAY,CAAC;CACjD"}
@@ -0,0 +1,12 @@
/*!
* 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 class Demuxer {
constructor(input) {
this.input = input;
}
}
@@ -0,0 +1,274 @@
/*!
* 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 { AudioCodec, MediaCodec, SubtitleCodec, VideoCodec } from './codec.js';
import { EncodedPacket } from './packet.js';
/**
* Configuration object that controls video encoding. Can be used to set codec, quality, and more.
* @group Encoding
* @public
*/
export type VideoEncodingConfig = {
/** The video codec that should be used for encoding the video samples (frames). */
codec: VideoCodec;
/**
* The target bitrate for the encoded video, in bits per second. Alternatively, a subjective {@link Quality} can
* be provided.
*/
bitrate: number | Quality;
/**
* The interval, in seconds, of how often frames are encoded as a key frame. The default is 5 seconds. Frequent key
* frames improve seeking behavior but increase file size. When using multiple video tracks, you should give them
* all the same key frame interval.
*/
keyFrameInterval?: number;
/**
* Video frames may change size over time. This field controls the behavior in case this happens.
*
* - `'deny'` (default) will throw an error, requiring all frames to have the exact same dimensions.
* - `'passThrough'` will allow the change and directly pass the frame to the encoder.
* - `'fill'` will stretch the image to fill the entire original box, potentially altering aspect ratio.
* - `'contain'` will contain the entire image within the original box while preserving aspect ratio. This may lead
* to letterboxing.
* - `'cover'` will scale the image until the entire original box is filled, while preserving aspect ratio.
*
* The "original box" refers to the dimensions of the first encoded frame.
*/
sizeChangeBehavior?: 'deny' | 'passThrough' | 'fill' | 'contain' | 'cover';
/** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */
onEncodedPacket?: (packet: EncodedPacket, meta: EncodedVideoChunkMetadata | undefined) => unknown;
/**
* Called when the internal [encoder config](https://www.w3.org/TR/webcodecs/#video-encoder-config), as used by the
* WebCodecs API, is created.
*/
onEncoderConfig?: (config: VideoEncoderConfig) => unknown;
} & VideoEncodingAdditionalOptions;
export declare const validateVideoEncodingConfig: (config: VideoEncodingConfig) => void;
/**
* Additional options that control audio encoding.
* @group Encoding
* @public
*/
export type VideoEncodingAdditionalOptions = {
/**
* What to do with alpha data contained in the video samples.
*
* - `'discard'` (default): Only the samples' color data is kept; the video is opaque.
* - `'keep'`: The samples' alpha data is also encoded as side data. Make sure to pair this mode with a container
* format that supports transparency (such as WebM or Matroska).
*/
alpha?: 'discard' | 'keep';
/** Configures the bitrate mode; defaults to `'variable'`. */
bitrateMode?: 'constant' | 'variable';
/**
* The latency mode used by the encoder; controls the performance-quality tradeoff.
*
* - `'quality'` (default): The encoder prioritizes quality over latency, and no frames can be dropped.
* - `'realtime'`: The encoder prioritizes low latency over quality, and may drop frames if the encoder becomes
* overloaded to keep up with real-time requirements.
*/
latencyMode?: 'quality' | 'realtime';
/**
* 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;
/**
* A hint that configures the hardware acceleration method of this codec. This is best left on `'no-preference'`,
* the default.
*/
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
/**
* An encoding scalability mode identifier as defined by
* [WebRTC-SVC](https://w3c.github.io/webrtc-svc/#scalabilitymodes*).
*/
scalabilityMode?: string;
/**
* An encoding video content hint as defined by
* [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).
*/
contentHint?: string;
};
export declare const validateVideoEncodingAdditionalOptions: (codec: VideoCodec, options: VideoEncodingAdditionalOptions) => void;
export declare const buildVideoEncoderConfig: (options: {
codec: VideoCodec;
width: number;
height: number;
bitrate: number | Quality;
framerate: number | undefined;
} & VideoEncodingAdditionalOptions) => VideoEncoderConfig;
/**
* Configuration object that controls audio encoding. Can be used to set codec, quality, and more.
* @group Encoding
* @public
*/
export type AudioEncodingConfig = {
/** The audio codec that should be used for encoding the audio samples. */
codec: AudioCodec;
/**
* The target bitrate for the encoded audio, in bits per second. Alternatively, a subjective {@link Quality} can
* be provided. Required for compressed audio codecs, unused for PCM codecs.
*/
bitrate?: number | Quality;
/** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */
onEncodedPacket?: (packet: EncodedPacket, meta: EncodedAudioChunkMetadata | undefined) => unknown;
/**
* Called when the internal [encoder config](https://www.w3.org/TR/webcodecs/#audio-encoder-config), as used by the
* WebCodecs API, is created.
*/
onEncoderConfig?: (config: AudioEncoderConfig) => unknown;
} & AudioEncodingAdditionalOptions;
export declare const validateAudioEncodingConfig: (config: AudioEncodingConfig) => void;
/**
* Additional options that control audio encoding.
* @group Encoding
* @public
*/
export type AudioEncodingAdditionalOptions = {
/** Configures the bitrate mode. */
bitrateMode?: 'constant' | 'variable';
/**
* 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;
};
export declare const validateAudioEncodingAdditionalOptions: (codec: AudioCodec, options: AudioEncodingAdditionalOptions) => void;
export declare const buildAudioEncoderConfig: (options: {
codec: AudioCodec;
numberOfChannels: number;
sampleRate: number;
bitrate?: number | Quality;
} & AudioEncodingAdditionalOptions) => AudioEncoderConfig;
/**
* Represents a subjective media quality level.
* @group Encoding
* @public
*/
export declare class Quality {
}
/**
* Represents a very low media quality.
* @group Encoding
* @public
*/
export declare const QUALITY_VERY_LOW: Quality;
/**
* Represents a low media quality.
* @group Encoding
* @public
*/
export declare const QUALITY_LOW: Quality;
/**
* Represents a medium media quality.
* @group Encoding
* @public
*/
export declare const QUALITY_MEDIUM: Quality;
/**
* Represents a high media quality.
* @group Encoding
* @public
*/
export declare const QUALITY_HIGH: Quality;
/**
* Represents a very high media quality.
* @group Encoding
* @public
*/
export declare const QUALITY_VERY_HIGH: Quality;
/**
* Checks if the browser is able to encode the given codec.
* @group Encoding
* @public
*/
export declare const canEncode: (codec: MediaCodec) => Promise<boolean>;
/**
* Checks if the browser is able to encode the given video codec with the given parameters.
* @group Encoding
* @public
*/
export declare const canEncodeVideo: (codec: VideoCodec, options?: {
width?: number;
height?: number;
bitrate?: number | Quality;
} & VideoEncodingAdditionalOptions) => Promise<boolean>;
/**
* Checks if the browser is able to encode the given audio codec with the given parameters.
* @group Encoding
* @public
*/
export declare const canEncodeAudio: (codec: AudioCodec, options?: {
numberOfChannels?: number;
sampleRate?: number;
bitrate?: number | Quality;
} & AudioEncodingAdditionalOptions) => Promise<boolean>;
/**
* Checks if the browser is able to encode the given subtitle codec.
* @group Encoding
* @public
*/
export declare const canEncodeSubtitles: (codec: SubtitleCodec) => Promise<boolean>;
/**
* Returns the list of all media codecs that can be encoded by the browser.
* @group Encoding
* @public
*/
export declare const getEncodableCodecs: () => Promise<MediaCodec[]>;
/**
* Returns the list of all video codecs that can be encoded by the browser.
* @group Encoding
* @public
*/
export declare const getEncodableVideoCodecs: (checkedCodecs?: VideoCodec[], options?: {
width?: number;
height?: number;
bitrate?: number | Quality;
}) => Promise<VideoCodec[]>;
/**
* Returns the list of all audio codecs that can be encoded by the browser.
* @group Encoding
* @public
*/
export declare const getEncodableAudioCodecs: (checkedCodecs?: AudioCodec[], options?: {
numberOfChannels?: number;
sampleRate?: number;
bitrate?: number | Quality;
}) => Promise<AudioCodec[]>;
/**
* Returns the list of all subtitle codecs that can be encoded by the browser.
* @group Encoding
* @public
*/
export declare const getEncodableSubtitleCodecs: (checkedCodecs?: SubtitleCodec[]) => Promise<SubtitleCodec[]>;
/**
* Returns the first video codec from the given list that can be encoded by the browser.
* @group Encoding
* @public
*/
export declare const getFirstEncodableVideoCodec: (checkedCodecs: VideoCodec[], options?: {
width?: number;
height?: number;
bitrate?: number | Quality;
}) => Promise<VideoCodec | null>;
/**
* Returns the first audio codec from the given list that can be encoded by the browser.
* @group Encoding
* @public
*/
export declare const getFirstEncodableAudioCodec: (checkedCodecs: AudioCodec[], options?: {
numberOfChannels?: number;
sampleRate?: number;
bitrate?: number | Quality;
}) => Promise<AudioCodec | null>;
/**
* Returns the first subtitle codec from the given list that can be encoded by the browser.
* @group Encoding
* @public
*/
export declare const getFirstEncodableSubtitleCodec: (checkedCodecs: SubtitleCodec[]) => Promise<SubtitleCodec | null>;
//# sourceMappingURL=encode.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../../src/encode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,479 @@
/*!
* 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 { AUDIO_CODECS, buildAudioCodecString, buildVideoCodecString, getAudioEncoderConfigExtension, getVideoEncoderConfigExtension, inferCodecFromCodecString, PCM_AUDIO_CODECS, SUBTITLE_CODECS, VIDEO_CODECS, } from './codec.js';
import { customAudioEncoders, customVideoEncoders } from './custom-coder.js';
import { isFirefox } from './misc.js';
export const validateVideoEncodingConfig = (config) => {
if (!config || typeof config !== 'object') {
throw new TypeError('Encoding config must be an object.');
}
if (!VIDEO_CODECS.includes(config.codec)) {
throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`);
}
if (!(config.bitrate instanceof Quality) && (!Number.isInteger(config.bitrate) || config.bitrate <= 0)) {
throw new TypeError('config.bitrate must be a positive integer or a quality.');
}
if (config.keyFrameInterval !== undefined
&& (!Number.isFinite(config.keyFrameInterval) || config.keyFrameInterval < 0)) {
throw new TypeError('config.keyFrameInterval, when provided, must be a non-negative number.');
}
if (config.sizeChangeBehavior !== undefined
&& !['deny', 'passThrough', 'fill', 'contain', 'cover'].includes(config.sizeChangeBehavior)) {
throw new TypeError('config.sizeChangeBehavior, when provided, must be \'deny\', \'passThrough\', \'fill\', \'contain\''
+ ' or \'cover\'.');
}
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
}
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
}
validateVideoEncodingAdditionalOptions(config.codec, config);
};
export const validateVideoEncodingAdditionalOptions = (codec, options) => {
if (!options || typeof options !== 'object') {
throw new TypeError('Encoding options must be an object.');
}
if (options.alpha !== undefined && !['discard', 'keep'].includes(options.alpha)) {
throw new TypeError('options.alpha, when provided, must be \'discard\' or \'keep\'.');
}
if (options.bitrateMode !== undefined && !['constant', 'variable'].includes(options.bitrateMode)) {
throw new TypeError('bitrateMode, when provided, must be \'constant\' or \'variable\'.');
}
if (options.latencyMode !== undefined && !['quality', 'realtime'].includes(options.latencyMode)) {
throw new TypeError('latencyMode, when provided, must be \'quality\' or \'realtime\'.');
}
if (options.fullCodecString !== undefined && typeof options.fullCodecString !== 'string') {
throw new TypeError('fullCodecString, when provided, must be a string.');
}
if (options.fullCodecString !== undefined && inferCodecFromCodecString(options.fullCodecString) !== codec) {
throw new TypeError(`fullCodecString, when provided, must be a string that matches the specified codec (${codec}).`);
}
if (options.hardwareAcceleration !== undefined
&& !['no-preference', 'prefer-hardware', 'prefer-software'].includes(options.hardwareAcceleration)) {
throw new TypeError('hardwareAcceleration, when provided, must be \'no-preference\', \'prefer-hardware\' or'
+ ' \'prefer-software\'.');
}
if (options.scalabilityMode !== undefined && typeof options.scalabilityMode !== 'string') {
throw new TypeError('scalabilityMode, when provided, must be a string.');
}
if (options.contentHint !== undefined && typeof options.contentHint !== 'string') {
throw new TypeError('contentHint, when provided, must be a string.');
}
};
export const buildVideoEncoderConfig = (options) => {
const resolvedBitrate = options.bitrate instanceof Quality
? options.bitrate._toVideoBitrate(options.codec, options.width, options.height)
: options.bitrate;
return {
codec: options.fullCodecString ?? buildVideoCodecString(options.codec, options.width, options.height, resolvedBitrate),
width: options.width,
height: options.height,
bitrate: resolvedBitrate,
bitrateMode: options.bitrateMode,
alpha: options.alpha ?? 'discard',
framerate: options.framerate,
latencyMode: options.latencyMode,
hardwareAcceleration: options.hardwareAcceleration,
scalabilityMode: options.scalabilityMode,
contentHint: options.contentHint,
...getVideoEncoderConfigExtension(options.codec),
};
};
export const validateAudioEncodingConfig = (config) => {
if (!config || typeof config !== 'object') {
throw new TypeError('Encoding config must be an object.');
}
if (!AUDIO_CODECS.includes(config.codec)) {
throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`);
}
if (config.bitrate === undefined
&& (!PCM_AUDIO_CODECS.includes(config.codec) || config.codec === 'flac')) {
throw new TypeError('config.bitrate must be provided for compressed audio codecs.');
}
if (config.bitrate !== undefined
&& !(config.bitrate instanceof Quality)
&& (!Number.isInteger(config.bitrate) || config.bitrate <= 0)) {
throw new TypeError('config.bitrate, when provided, must be a positive integer or a quality.');
}
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
}
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
}
validateAudioEncodingAdditionalOptions(config.codec, config);
};
export const validateAudioEncodingAdditionalOptions = (codec, options) => {
if (!options || typeof options !== 'object') {
throw new TypeError('Encoding options must be an object.');
}
if (options.bitrateMode !== undefined && !['constant', 'variable'].includes(options.bitrateMode)) {
throw new TypeError('bitrateMode, when provided, must be \'constant\' or \'variable\'.');
}
if (options.fullCodecString !== undefined && typeof options.fullCodecString !== 'string') {
throw new TypeError('fullCodecString, when provided, must be a string.');
}
if (options.fullCodecString !== undefined && inferCodecFromCodecString(options.fullCodecString) !== codec) {
throw new TypeError(`fullCodecString, when provided, must be a string that matches the specified codec (${codec}).`);
}
};
export const buildAudioEncoderConfig = (options) => {
const resolvedBitrate = options.bitrate instanceof Quality
? options.bitrate._toAudioBitrate(options.codec)
: options.bitrate;
return {
codec: options.fullCodecString ?? buildAudioCodecString(options.codec, options.numberOfChannels, options.sampleRate),
numberOfChannels: options.numberOfChannels,
sampleRate: options.sampleRate,
bitrate: resolvedBitrate,
bitrateMode: options.bitrateMode,
...getAudioEncoderConfigExtension(options.codec),
};
};
/**
* Represents a subjective media quality level.
* @group Encoding
* @public
*/
export class Quality {
/** @internal */
constructor(factor) {
this._factor = factor;
}
/** @internal */
_toVideoBitrate(codec, width, height) {
const pixels = width * height;
const codecEfficiencyFactors = {
avc: 1.0, // H.264/AVC (baseline)
hevc: 0.6, // H.265/HEVC (~40% more efficient than AVC)
vp9: 0.6, // Similar to HEVC
av1: 0.4, // ~60% more efficient than AVC
vp8: 1.2, // Slightly less efficient than AVC
};
const referencePixels = 1920 * 1080;
const referenceBitrate = 3000000;
const scaleFactor = Math.pow(pixels / referencePixels, 0.95); // Slight non-linear scaling
const baseBitrate = referenceBitrate * scaleFactor;
const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec];
const finalBitrate = codecAdjustedBitrate * this._factor;
return Math.ceil(finalBitrate / 1000) * 1000;
}
/** @internal */
_toAudioBitrate(codec) {
if (PCM_AUDIO_CODECS.includes(codec) || codec === 'flac') {
return undefined;
}
const baseRates = {
aac: 128000, // 128kbps base for AAC
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];
if (!baseBitrate) {
throw new Error(`Unhandled codec: ${codec}`);
}
let finalBitrate = baseBitrate * this._factor;
if (codec === 'aac') {
// AAC only works with specific bitrates, let's find the closest
const validRates = [96000, 128000, 160000, 192000];
finalBitrate = validRates.reduce((prev, curr) => Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev);
}
else if (codec === 'opus' || codec === 'vorbis') {
finalBitrate = Math.max(6000, finalBitrate);
}
else if (codec === 'mp3') {
const validRates = [
8000, 16000, 24000, 32000, 40000, 48000, 64000, 80000,
96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000,
];
finalBitrate = validRates.reduce((prev, curr) => Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev);
}
return Math.round(finalBitrate / 1000) * 1000;
}
}
/**
* Represents a very low media quality.
* @group Encoding
* @public
*/
export const QUALITY_VERY_LOW = /* #__PURE__ */ new Quality(0.3);
/**
* Represents a low media quality.
* @group Encoding
* @public
*/
export const QUALITY_LOW = /* #__PURE__ */ new Quality(0.6);
/**
* Represents a medium media quality.
* @group Encoding
* @public
*/
export const QUALITY_MEDIUM = /* #__PURE__ */ new Quality(1);
/**
* Represents a high media quality.
* @group Encoding
* @public
*/
export const QUALITY_HIGH = /* #__PURE__ */ new Quality(2);
/**
* Represents a very high media quality.
* @group Encoding
* @public
*/
export const QUALITY_VERY_HIGH = /* #__PURE__ */ new Quality(4);
/**
* Checks if the browser is able to encode the given codec.
* @group Encoding
* @public
*/
export const canEncode = (codec) => {
if (VIDEO_CODECS.includes(codec)) {
return canEncodeVideo(codec);
}
else if (AUDIO_CODECS.includes(codec)) {
return canEncodeAudio(codec);
}
else if (SUBTITLE_CODECS.includes(codec)) {
return canEncodeSubtitles(codec);
}
throw new TypeError(`Unknown codec '${codec}'.`);
};
/**
* Checks if the browser is able to encode the given video codec with the given parameters.
* @group Encoding
* @public
*/
export const canEncodeVideo = async (codec, options = {}) => {
const { width = 1280, height = 720, bitrate = 1e6, ...restOptions } = options;
if (!VIDEO_CODECS.includes(codec)) {
return false;
}
if (!Number.isInteger(width) || width <= 0) {
throw new TypeError('width must be a positive integer.');
}
if (!Number.isInteger(height) || height <= 0) {
throw new TypeError('height must be a positive integer.');
}
if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
throw new TypeError('bitrate must be a positive integer or a quality.');
}
validateVideoEncodingAdditionalOptions(codec, restOptions);
let encoderConfig = null;
if (customVideoEncoders.length > 0) {
encoderConfig ??= buildVideoEncoderConfig({
codec,
width,
height,
bitrate,
framerate: undefined,
...restOptions,
});
if (customVideoEncoders.some(x => x.supports(codec, encoderConfig))) {
// There's a custom encoder
return true;
}
}
if (typeof VideoEncoder === 'undefined') {
return false;
}
const hasOddDimension = width % 2 === 1 || height % 2 === 1;
if (hasOddDimension
&& (codec === 'avc' || codec === 'hevc')) {
// Disallow odd dimensions for certain codecs
return false;
}
encoderConfig ??= buildVideoEncoderConfig({
codec,
width,
height,
bitrate,
framerate: undefined,
...restOptions,
alpha: 'discard', // Since we handle alpha ourselves
});
const support = await VideoEncoder.isConfigSupported(encoderConfig);
if (!support.supported) {
return false;
}
if (isFirefox()) {
// isConfigSupported on Firefox appears to unreliably indicate if encoding will actually succeed. Therefore, we
// just try encoding a frame to see if it actually works.
// https://github.com/Vanilagy/mediabunny/issues/222
// eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
return new Promise(async (resolve) => {
try {
const encoder = new VideoEncoder({
output: () => { },
error: () => resolve(false),
});
encoder.configure(encoderConfig);
const frameData = new Uint8Array(width * height * 4);
const frame = new VideoFrame(frameData, {
format: 'RGBA',
codedWidth: width,
codedHeight: height,
timestamp: 0,
});
encoder.encode(frame);
frame.close();
await encoder.flush();
resolve(true);
}
catch {
resolve(false);
}
});
}
else {
return true;
}
};
/**
* Checks if the browser is able to encode the given audio codec with the given parameters.
* @group Encoding
* @public
*/
export const canEncodeAudio = async (codec, options = {}) => {
const { numberOfChannels = 2, sampleRate = 48000, bitrate = 128e3, ...restOptions } = options;
if (!AUDIO_CODECS.includes(codec)) {
return false;
}
if (!Number.isInteger(numberOfChannels) || numberOfChannels <= 0) {
throw new TypeError('numberOfChannels must be a positive integer.');
}
if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
throw new TypeError('sampleRate must be a positive integer.');
}
if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
throw new TypeError('bitrate must be a positive integer.');
}
validateAudioEncodingAdditionalOptions(codec, restOptions);
let encoderConfig = null;
if (customAudioEncoders.length > 0) {
encoderConfig ??= buildAudioEncoderConfig({
codec,
numberOfChannels,
sampleRate,
bitrate,
...restOptions,
});
if (customAudioEncoders.some(x => x.supports(codec, encoderConfig))) {
// There's a custom encoder
return true;
}
}
if (PCM_AUDIO_CODECS.includes(codec)) {
return true; // Because we encode these ourselves
}
if (typeof AudioEncoder === 'undefined') {
return false;
}
encoderConfig ??= buildAudioEncoderConfig({
codec,
numberOfChannels,
sampleRate,
bitrate,
...restOptions,
});
const support = await AudioEncoder.isConfigSupported(encoderConfig);
return support.supported === true;
};
/**
* Checks if the browser is able to encode the given subtitle codec.
* @group Encoding
* @public
*/
export const canEncodeSubtitles = async (codec) => {
if (!SUBTITLE_CODECS.includes(codec)) {
return false;
}
return true;
};
/**
* Returns the list of all media codecs that can be encoded by the browser.
* @group Encoding
* @public
*/
export const getEncodableCodecs = async () => {
const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
getEncodableVideoCodecs(),
getEncodableAudioCodecs(),
getEncodableSubtitleCodecs(),
]);
return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
};
/**
* Returns the list of all video codecs that can be encoded by the browser.
* @group Encoding
* @public
*/
export const getEncodableVideoCodecs = async (checkedCodecs = VIDEO_CODECS, options) => {
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeVideo(codec, options)));
return checkedCodecs.filter((_, i) => bools[i]);
};
/**
* Returns the list of all audio codecs that can be encoded by the browser.
* @group Encoding
* @public
*/
export const getEncodableAudioCodecs = async (checkedCodecs = AUDIO_CODECS, options) => {
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeAudio(codec, options)));
return checkedCodecs.filter((_, i) => bools[i]);
};
/**
* Returns the list of all subtitle codecs that can be encoded by the browser.
* @group Encoding
* @public
*/
export const getEncodableSubtitleCodecs = async (checkedCodecs = SUBTITLE_CODECS) => {
const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles));
return checkedCodecs.filter((_, i) => bools[i]);
};
/**
* Returns the first video codec from the given list that can be encoded by the browser.
* @group Encoding
* @public
*/
export const getFirstEncodableVideoCodec = async (checkedCodecs, options) => {
for (const codec of checkedCodecs) {
if (await canEncodeVideo(codec, options)) {
return codec;
}
}
return null;
};
/**
* Returns the first audio codec from the given list that can be encoded by the browser.
* @group Encoding
* @public
*/
export const getFirstEncodableAudioCodec = async (checkedCodecs, options) => {
for (const codec of checkedCodecs) {
if (await canEncodeAudio(codec, options)) {
return codec;
}
}
return null;
};
/**
* Returns the first subtitle codec from the given list that can be encoded by the browser.
* @group Encoding
* @public
*/
export const getFirstEncodableSubtitleCodec = async (checkedCodecs) => {
for (const codec of checkedCodecs) {
if (await canEncodeSubtitles(codec)) {
return codec;
}
}
return null;
};
@@ -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 { Demuxer } from '../demuxer.js';
import { Input } from '../input.js';
import { InputAudioTrack } from '../input-track.js';
import { AsyncMutex } from '../misc.js';
import { FileSlice, Reader } from '../reader.js';
import { MetadataTags } from '../metadata.js';
type FlacAudioInfo = {
numberOfChannels: number;
sampleRate: number;
totalSamples: number;
minimumBlockSize: number;
maximumBlockSize: number;
minimumFrameSize: number;
maximumFrameSize: number;
description: Uint8Array;
};
type Sample = {
blockOffset: number;
blockSize: number;
byteOffset: number;
byteSize: number;
};
type NextFlacFrameResult = {
num: number;
blockSize: number;
sampleRate: number;
size: number;
isLastFrame: boolean;
};
export declare class FlacDemuxer extends Demuxer {
reader: Reader;
loadedSamples: Sample[];
metadataPromise: Promise<void> | null;
track: InputAudioTrack | null;
metadataTags: MetadataTags;
audioInfo: FlacAudioInfo | null;
lastLoadedPos: number | null;
blockingBit: number | null;
readingMutex: AsyncMutex;
lastSampleLoaded: boolean;
constructor(input: Input);
computeDuration(): Promise<number>;
getMetadataTags(): Promise<MetadataTags>;
getTracks(): Promise<InputAudioTrack[]>;
getMimeType(): Promise<string>;
readMetadata(): Promise<void>;
readNextFlacFrame({ startPos, isFirstPacket, }: {
startPos: number;
isFirstPacket: boolean;
}): Promise<NextFlacFrameResult | null>;
readFlacFrameHeader({ slice, isFirstPacket, }: {
slice: FileSlice;
isFirstPacket: boolean;
}): {
num: number;
blockSize: number;
sampleRate: number;
} | null;
advanceReader(): Promise<void>;
}
export {};
//# sourceMappingURL=flac-demuxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"flac-demuxer.d.ts","sourceRoot":"","sources":["../../../../src/flac/flac-demuxer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,eAAe,EAA0B,MAAM,gBAAgB,CAAC;AAEzE,OAAO,EAEN,UAAU,EAKV,MAAM,SAAS,CAAC;AAEjB,OAAO,EACN,SAAS,EAET,MAAM,EAIN,MAAM,WAAW,CAAC;AACnB,OAAO,EAA6B,YAAY,EAAE,MAAM,aAAa,CAAC;AAUtE,KAAK,aAAa,GAAG;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,UAAU,CAAC;CACxB,CAAC;AAEF,KAAK,MAAM,GAAG;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,qBAAa,WAAY,SAAQ,OAAO;IACvC,MAAM,EAAE,MAAM,CAAC;IAEf,aAAa,EAAE,MAAM,EAAE,CAAM;IAE7B,eAAe,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAQ;IAC7C,KAAK,EAAE,eAAe,GAAG,IAAI,CAAQ;IACrC,YAAY,EAAE,YAAY,CAAM;IAEhC,SAAS,EAAE,aAAa,GAAG,IAAI,CAAQ;IACvC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAQ;IACpC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAElC,YAAY,aAAoB;IAChC,gBAAgB,UAAS;gBAEb,KAAK,EAAE,KAAK;IAMT,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAMlC,eAAe,IAAI,OAAO,CAAC,YAAY,CAAC;IAKjD,SAAS;IAMT,WAAW;IAIX,YAAY;IA0JZ,iBAAiB,CAAC,EACvB,QAAQ,EACR,aAAa,GACb,EAAE;QACF,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,OAAO,CAAC;KACvB,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAiHvC,mBAAmB,CAAC,EACnB,KAAK,EACL,aAAa,GACb,EAAE;QACF,KAAK,EAAE,SAAS,CAAC;QACjB,aAAa,EAAE,OAAO,CAAC;KACvB;;;;;IAiGK,aAAa;CAqCnB"}
@@ -0,0 +1,507 @@
/*!
* 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 { FlacBlockType, readVorbisComments } from '../codec-data.js';
import { Demuxer } from '../demuxer.js';
import { InputAudioTrack } from '../input-track.js';
import { assert, AsyncMutex, binarySearchLessOrEqual, Bitstream, textDecoder, UNDETERMINED_LANGUAGE, } from '../misc.js';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet.js';
import { readBytes, readU24Be, readU32Be, readU8, } from '../reader.js';
import { DEFAULT_TRACK_DISPOSITION } from '../metadata.js';
import { calculateCrc8, readBlockSize, getBlockSizeOrUncommon, readCodedNumber, readSampleRate, getSampleRateOrUncommon, } from './flac-misc.js';
export class FlacDemuxer extends Demuxer {
constructor(input) {
super(input);
this.loadedSamples = []; // All samples from the start of the file to lastLoadedPos
this.metadataPromise = null;
this.track = null;
this.metadataTags = {};
this.audioInfo = null;
this.lastLoadedPos = null;
this.blockingBit = null;
this.readingMutex = new AsyncMutex();
this.lastSampleLoaded = false;
this.reader = input._reader;
}
async computeDuration() {
await this.readMetadata();
assert(this.track);
return this.track.computeDuration();
}
async getMetadataTags() {
await this.readMetadata();
return this.metadataTags;
}
async getTracks() {
await this.readMetadata();
assert(this.track);
return [this.track];
}
async getMimeType() {
return 'audio/flac';
}
async readMetadata() {
let currentPos = 4; // Skip 'fLaC'
return (this.metadataPromise ??= (async () => {
while (this.reader.fileSize === null
|| currentPos < this.reader.fileSize) {
let sizeSlice = this.reader.requestSlice(currentPos, 4);
if (sizeSlice instanceof Promise)
sizeSlice = await sizeSlice;
currentPos += 4;
if (sizeSlice === null) {
throw new Error(`Metadata block at position ${currentPos} is too small! Corrupted file.`);
}
assert(sizeSlice);
const byte = readU8(sizeSlice); // first bit: isLastMetadata, remaining 7 bits: metaBlockType
const size = readU24Be(sizeSlice);
const isLastMetadata = (byte & 0x80) !== 0;
const metaBlockType = byte & 0x7f;
switch (metaBlockType) {
case FlacBlockType.STREAMINFO: {
// Parse streaminfo block
// https://www.rfc-editor.org/rfc/rfc9639.html#section-8.2
let streamInfoBlock = this.reader.requestSlice(currentPos, size);
if (streamInfoBlock instanceof Promise)
streamInfoBlock = await streamInfoBlock;
assert(streamInfoBlock);
if (streamInfoBlock === null) {
throw new Error(`StreamInfo block at position ${currentPos} is too small! Corrupted file.`);
}
const streamInfoBytes = readBytes(streamInfoBlock, 34);
const bitstream = new Bitstream(streamInfoBytes);
const minimumBlockSize = bitstream.readBits(16);
const maximumBlockSize = bitstream.readBits(16);
const minimumFrameSize = bitstream.readBits(24);
const maximumFrameSize = bitstream.readBits(24);
const sampleRate = bitstream.readBits(20);
const numberOfChannels = bitstream.readBits(3) + 1;
bitstream.readBits(5); // bitsPerSample - 1
const totalSamples = bitstream.readBits(36);
// https://www.w3.org/TR/webcodecs-flac-codec-registration/#audiodecoderconfig-description
// description is required, and has to be the following:
// 1. The bytes 0x66 0x4C 0x61 0x43 ("fLaC" in ASCII)
// 2. A metadata block (called the STREAMINFO block) as described in section 7 of [FLAC]
// 3. Optionaly (sic) other metadata blocks, that are not used by the specification
bitstream.skipBits(16 * 8); // md5 hash
const description = new Uint8Array(42);
// 1. "fLaC"
description.set(new Uint8Array([0x66, 0x4c, 0x61, 0x43]), 0);
// 2. STREAMINFO block
description.set(new Uint8Array([128, 0, 0, 34]), 4);
// 3. Other metadata blocks
description.set(streamInfoBytes, 8);
this.audioInfo = {
numberOfChannels,
sampleRate,
totalSamples,
minimumBlockSize,
maximumBlockSize,
minimumFrameSize,
maximumFrameSize,
description,
};
this.track = new InputAudioTrack(this.input, new FlacAudioTrackBacking(this));
break;
}
case FlacBlockType.VORBIS_COMMENT: {
// Parse vorbis comment block
// https://www.rfc-editor.org/rfc/rfc9639.html#name-vorbis-comment
let vorbisCommentBlock = this.reader.requestSlice(currentPos, size);
if (vorbisCommentBlock instanceof Promise)
vorbisCommentBlock = await vorbisCommentBlock;
assert(vorbisCommentBlock);
readVorbisComments(readBytes(vorbisCommentBlock, size), this.metadataTags);
break;
}
case FlacBlockType.PICTURE: {
// Parse picture block
// https://www.rfc-editor.org/rfc/rfc9639.html#name-picture
let pictureBlock = this.reader.requestSlice(currentPos, size);
if (pictureBlock instanceof Promise)
pictureBlock = await pictureBlock;
assert(pictureBlock);
const pictureType = readU32Be(pictureBlock);
const mediaTypeLength = readU32Be(pictureBlock);
const mediaType = textDecoder.decode(readBytes(pictureBlock, mediaTypeLength));
const descriptionLength = readU32Be(pictureBlock);
const description = textDecoder.decode(readBytes(pictureBlock, descriptionLength));
pictureBlock.skip(4 + 4 + 4 + 4); // Skip width, height, color depth, number of indexed colors
const dataLength = readU32Be(pictureBlock);
const data = readBytes(pictureBlock, dataLength);
this.metadataTags.images ??= [];
this.metadataTags.images.push({
data,
mimeType: mediaType,
// https://www.rfc-editor.org/rfc/rfc9639.html#table13
kind: pictureType === 3
? 'coverFront'
: pictureType === 4
? 'coverBack'
: 'unknown',
description,
});
break;
}
default:
break;
}
currentPos += size;
if (isLastMetadata) {
this.lastLoadedPos = currentPos;
break;
}
}
})());
}
async readNextFlacFrame({ startPos, isFirstPacket, }) {
assert(this.audioInfo);
// we expect that there are at least `minimumFrameSize` bytes left in the file
// Ideally we also want to validate the next header is valid
// to throw out an accidential sync word
// The shortest valid FLAC header I can think of, based off the code
// of readFlacFrameHeader:
// 4 bytes used for bitstream from syncword to bit depth
// 1 byte coded number
// (uncommon values, no bytes read)
// 1 byte crc
// --> 6 bytes
const minimumHeaderLength = 6;
// If we read everything in readFlacFrameHeader, we read 16 bytes
const maximumHeaderSize = 16;
const maximumSliceLength = this.audioInfo.maximumFrameSize + maximumHeaderSize;
const slice = await this.reader.requestSliceRange(startPos, this.audioInfo.minimumFrameSize, maximumSliceLength);
if (!slice) {
return null;
}
const frameHeader = this.readFlacFrameHeader({
slice,
isFirstPacket: isFirstPacket,
});
if (!frameHeader) {
return null;
}
// We don't know exactly how long the packet is, we only know the `minimumFrameSize` and `maximumFrameSize`
// The packet is over if the next 2 bytes are the sync word followed by a valid header
// or the end of the file is reached
// The next sync word is expected at earliest when `minimumFrameSize` is reached,
// we can skip over anything before that
slice.filePos = startPos + this.audioInfo.minimumFrameSize;
while (true) {
// Reached end of the file, packet is over
if (slice.filePos > slice.end - minimumHeaderLength) {
return {
num: frameHeader.num,
blockSize: frameHeader.blockSize,
sampleRate: frameHeader.sampleRate,
size: slice.end - startPos,
isLastFrame: true,
};
}
const nextByte = readU8(slice);
if (nextByte === 0xff) {
const positionBeforeReading = slice.filePos;
const byteAfterNextByte = readU8(slice);
const expected = this.blockingBit === 1 ? 0b1111_1001 : 0b1111_1000;
if (byteAfterNextByte !== expected) {
slice.filePos = positionBeforeReading;
continue;
}
slice.skip(-2);
const lengthIfNextFlacFrameHeaderIsLegit = slice.filePos - startPos;
const nextFrameHeader = this.readFlacFrameHeader({
slice,
isFirstPacket: false,
});
if (!nextFrameHeader) {
slice.filePos = positionBeforeReading;
continue;
}
// Ensure the frameOrSampleNum is consecutive.
// https://github.com/Vanilagy/mediabunny/issues/194
if (this.blockingBit === 0) {
// Case A: If the stream is fixed block size, this is the frame number, which increments by 1
if (nextFrameHeader.num - frameHeader.num !== 1) {
slice.filePos = positionBeforeReading;
continue;
}
}
else {
// Case B: If the stream is variable block size, this is the sample number, which increments by
// amount of samples in a frame.
if (nextFrameHeader.num - frameHeader.num !== frameHeader.blockSize) {
slice.filePos = positionBeforeReading;
continue;
}
}
return {
num: frameHeader.num,
blockSize: frameHeader.blockSize,
sampleRate: frameHeader.sampleRate,
size: lengthIfNextFlacFrameHeaderIsLegit,
isLastFrame: false,
};
}
}
}
readFlacFrameHeader({ slice, isFirstPacket, }) {
// In this function, generally it is not safe to throw errors.
// We might end up here because we stumbled upon a syncword,
// but the data might not actually be a FLAC frame, it might be random bitstream
// data, in that case we should return null and continue.
const startOffset = slice.filePos;
// https://www.rfc-editor.org/rfc/rfc9639.html#section-9.1
// Each frame MUST start on a byte boundary and start with the 15-bit frame
// sync code 0b111111111111100. Following the sync code is the blocking strategy
// bit, which MUST NOT change during the audio stream.
const bytes = readBytes(slice, 4);
const bitstream = new Bitstream(bytes);
const bits = bitstream.readBits(15);
if (bits !== 0b111111111111100) {
// This cannot be a valid FLAC frame, must start with the syncword
return null;
}
if (this.blockingBit === null) {
assert(isFirstPacket);
const newBlockingBit = bitstream.readBits(1);
this.blockingBit = newBlockingBit;
}
else if (this.blockingBit === 1) {
assert(!isFirstPacket);
const newBlockingBit = bitstream.readBits(1);
if (newBlockingBit !== 1) {
// This cannot be a valid FLAC frame, expected 1 but got 0
return null;
}
}
else if (this.blockingBit === 0) {
assert(!isFirstPacket);
const newBlockingBit = bitstream.readBits(1);
if (newBlockingBit !== 0) {
// This cannot be a valid FLAC frame, expected 0 but got 1
return null;
}
}
else {
throw new Error('Invalid blocking bit');
}
const blockSizeOrUncommon = getBlockSizeOrUncommon(bitstream.readBits(4));
if (!blockSizeOrUncommon) {
// This cannot be a valid FLAC frame, the syncword was just coincidental
return null;
}
assert(this.audioInfo);
const sampleRateOrUncommon = getSampleRateOrUncommon(bitstream.readBits(4), this.audioInfo.sampleRate);
if (!sampleRateOrUncommon) {
// This cannot be a valid FLAC frame, the syncword was just coincidental
return null;
}
bitstream.readBits(4); // channel count
bitstream.readBits(3); // bit depth
const reservedZero = bitstream.readBits(1); // reserved zero
if (reservedZero !== 0) {
// This cannot be a valid FLAC frame, the syncword was just coincidental
return null;
}
const num = readCodedNumber(slice);
const blockSize = readBlockSize(slice, blockSizeOrUncommon);
const sampleRate = readSampleRate(slice, sampleRateOrUncommon);
if (sampleRate === null) {
// This cannot be a valid FLAC frame, the syncword was just coincidental
return null;
}
if (sampleRate !== this.audioInfo.sampleRate) {
// This cannot be a valid FLAC frame, the sample rate is not the same as in the stream info
return null;
}
const size = slice.filePos - startOffset;
const crc = readU8(slice);
slice.skip(-size);
slice.skip(-1);
const crcCalculated = calculateCrc8(readBytes(slice, size));
if (crc !== crcCalculated) {
// Maybe this wasn't a FLAC frame at all, the syncword was just coincidentally
// in the bitstream
return null;
}
return { num, blockSize, sampleRate };
}
async advanceReader() {
await this.readMetadata();
assert(this.lastLoadedPos !== null);
assert(this.audioInfo);
const startPos = this.lastLoadedPos;
const frame = await this.readNextFlacFrame({
startPos,
isFirstPacket: this.loadedSamples.length === 0,
});
if (!frame) {
// Unexpected case, failed to read next FLAC frame
// handling gracefully
this.lastSampleLoaded = true;
return;
}
const lastSample = this.loadedSamples[this.loadedSamples.length - 1];
const blockOffset = lastSample
? lastSample.blockOffset + lastSample.blockSize
: 0;
const sample = {
blockOffset,
blockSize: frame.blockSize,
byteOffset: startPos,
byteSize: frame.size,
};
this.lastLoadedPos = this.lastLoadedPos + frame.size;
this.loadedSamples.push(sample);
if (frame.isLastFrame) {
this.lastSampleLoaded = true;
return;
}
}
}
class FlacAudioTrackBacking {
constructor(demuxer) {
this.demuxer = demuxer;
}
getId() {
return 1;
}
getNumber() {
return 1;
}
getCodec() {
return 'flac';
}
getInternalCodecId() {
return null;
}
getNumberOfChannels() {
assert(this.demuxer.audioInfo);
return this.demuxer.audioInfo.numberOfChannels;
}
async computeDuration() {
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
}
getSampleRate() {
assert(this.demuxer.audioInfo);
return this.demuxer.audioInfo.sampleRate;
}
getName() {
return null;
}
getLanguageCode() {
return UNDETERMINED_LANGUAGE;
}
getTimeResolution() {
assert(this.demuxer.audioInfo);
return this.demuxer.audioInfo.sampleRate;
}
getDisposition() {
return {
...DEFAULT_TRACK_DISPOSITION,
};
}
async getFirstTimestamp() {
return 0;
}
async getDecoderConfig() {
assert(this.demuxer.audioInfo);
return {
codec: 'flac',
numberOfChannels: this.demuxer.audioInfo.numberOfChannels,
sampleRate: this.demuxer.audioInfo.sampleRate,
description: this.demuxer.audioInfo.description,
};
}
async getPacket(timestamp, options) {
assert(this.demuxer.audioInfo);
if (timestamp < 0) {
throw new Error('Timestamp cannot be negative');
}
const release = await this.demuxer.readingMutex.acquire();
try {
while (true) {
const packetIndex = binarySearchLessOrEqual(this.demuxer.loadedSamples, timestamp, x => x.blockOffset / this.demuxer.audioInfo.sampleRate);
if (packetIndex === -1) {
await this.demuxer.advanceReader();
continue;
}
const packet = this.demuxer.loadedSamples[packetIndex];
const sampleTimestamp = packet.blockOffset / this.demuxer.audioInfo.sampleRate;
const sampleDuration = packet.blockSize / this.demuxer.audioInfo.sampleRate;
if (sampleTimestamp + sampleDuration <= timestamp) {
if (this.demuxer.lastSampleLoaded) {
return this.getPacketAtIndex(this.demuxer.loadedSamples.length - 1, options);
}
await this.demuxer.advanceReader();
continue;
}
return this.getPacketAtIndex(packetIndex, options);
}
}
finally {
release();
}
}
async getNextPacket(packet, options) {
const release = await this.demuxer.readingMutex.acquire();
try {
const nextIndex = packet.sequenceNumber + 1;
if (this.demuxer.lastSampleLoaded
&& nextIndex >= this.demuxer.loadedSamples.length) {
return null;
}
// Ensure the next sample exists
while (nextIndex >= this.demuxer.loadedSamples.length
&& !this.demuxer.lastSampleLoaded) {
await this.demuxer.advanceReader();
}
return this.getPacketAtIndex(nextIndex, options);
}
finally {
release();
}
}
getKeyPacket(timestamp, options) {
return this.getPacket(timestamp, options);
}
getNextKeyPacket(packet, options) {
return this.getNextPacket(packet, options);
}
async getPacketAtIndex(sampleIndex, options) {
const rawSample = this.demuxer.loadedSamples[sampleIndex];
if (!rawSample) {
return null;
}
let data;
if (options.metadataOnly) {
data = PLACEHOLDER_DATA;
}
else {
let slice = this.demuxer.reader.requestSlice(rawSample.byteOffset, rawSample.byteSize);
if (slice instanceof Promise)
slice = await slice;
if (!slice) {
return null; // Data didn't fit into the rest of the file
}
data = readBytes(slice, rawSample.byteSize);
}
assert(this.demuxer.audioInfo);
const timestamp = rawSample.blockOffset / this.demuxer.audioInfo.sampleRate;
const duration = rawSample.blockSize / this.demuxer.audioInfo.sampleRate;
return new EncodedPacket(data, 'key', timestamp, duration, sampleIndex, rawSample.byteSize);
}
async getFirstPacket(options) {
// Ensure the next sample exists
while (this.demuxer.loadedSamples.length === 0
&& !this.demuxer.lastSampleLoaded) {
await this.demuxer.advanceReader();
}
return this.getPacketAtIndex(0, options);
}
}
@@ -0,0 +1,18 @@
/*!
* 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 { FileSlice } from '../reader.js';
type BlockSizeOrUncommon = number | 'uncommon-u16' | 'uncommon-u8';
type SampleRateOrUncommon = number | 'uncommon-u8' | 'uncommon-u16' | 'uncommon-u16-10';
export declare const getBlockSizeOrUncommon: (bits: number) => BlockSizeOrUncommon | null;
export declare const getSampleRateOrUncommon: (sampleRateBits: number, streamInfoSampleRate: number) => SampleRateOrUncommon | null;
export declare const readCodedNumber: (fileSlice: FileSlice) => number;
export declare const readBlockSize: (slice: FileSlice, blockSizeBits: BlockSizeOrUncommon) => number;
export declare const readSampleRate: (slice: FileSlice, sampleRateOrUncommon: SampleRateOrUncommon) => number | null;
export declare const calculateCrc8: (data: Uint8Array) => number;
export {};
//# sourceMappingURL=flac-misc.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"flac-misc.d.ts","sourceRoot":"","sources":["../../../../src/flac/flac-misc.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,SAAS,EAAgC,MAAM,WAAW,CAAC;AAEpE,KAAK,mBAAmB,GAAG,MAAM,GAAG,cAAc,GAAG,aAAa,CAAC;AACnE,KAAK,oBAAoB,GACtB,MAAM,GACN,aAAa,GACb,cAAc,GACd,iBAAiB,CAAC;AAGrB,eAAO,MAAM,sBAAsB,GAAI,MAAM,MAAM,KAAG,mBAAmB,GAAG,IAgB3E,CAAC;AAGF,eAAO,MAAM,uBAAuB,GACnC,gBAAgB,MAAM,EACtB,sBAAsB,MAAM,KAC1B,oBAAoB,GAAG,IAmBzB,CAAC;AAGF,eAAO,MAAM,eAAe,GAAI,WAAW,SAAS,KAAG,MAqCtD,CAAC;AAEF,eAAO,MAAM,aAAa,GACzB,OAAO,SAAS,EAChB,eAAe,mBAAmB,WAYlC,CAAC;AAEF,eAAO,MAAM,cAAc,GAC1B,OAAO,SAAS,EAChB,sBAAsB,oBAAoB,kBAmB1C,CAAC;AAGF,eAAO,MAAM,aAAa,GAAI,MAAM,UAAU,WAqB7C,CAAC"}
@@ -0,0 +1,135 @@
/*!
* 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, assertNever, Bitstream } from '../misc.js';
import { readBytes, readU16Be, readU8 } from '../reader.js';
// https://www.rfc-editor.org/rfc/rfc9639.html#name-block-size-bits
export const getBlockSizeOrUncommon = (bits) => {
if (bits === 0b0000) {
return null;
}
else if (bits === 0b0001) {
return 192;
}
else if (bits >= 0b0010 && bits <= 0b0101) {
return 144 * 2 ** bits;
}
else if (bits === 0b0110) {
return 'uncommon-u8';
}
else if (bits === 0b0111) {
return 'uncommon-u16';
}
else if (bits >= 0b1000 && bits <= 0b1111) {
return 2 ** bits;
}
else {
return null;
}
};
// https://www.rfc-editor.org/rfc/rfc9639.html#name-sample-rate-bits
export const getSampleRateOrUncommon = (sampleRateBits, streamInfoSampleRate) => {
switch (sampleRateBits) {
case 0b0000: return streamInfoSampleRate;
case 0b0001: return 88200;
case 0b0010: return 176400;
case 0b0011: return 192000;
case 0b0100: return 8000;
case 0b0101: return 16000;
case 0b0110: return 22050;
case 0b0111: return 24000;
case 0b1000: return 32000;
case 0b1001: return 44100;
case 0b1010: return 48000;
case 0b1011: return 96000;
case 0b1100: return 'uncommon-u8';
case 0b1101: return 'uncommon-u16';
case 0b1110: return 'uncommon-u16-10';
default: return null;
}
};
// https://www.rfc-editor.org/rfc/rfc9639.html#name-coded-number
export const readCodedNumber = (fileSlice) => {
let ones = 0;
const bitstream1 = new Bitstream(readBytes(fileSlice, 1));
while (bitstream1.readBits(1) === 1) {
ones++;
}
if (ones === 0) {
return bitstream1.readBits(7);
}
const bitArray = [];
const extraBytes = ones - 1;
const bitstream2 = new Bitstream(readBytes(fileSlice, extraBytes));
const firstByteBits = 8 - ones - 1;
for (let i = 0; i < firstByteBits; i++) {
bitArray.unshift(bitstream1.readBits(1));
}
for (let i = 0; i < extraBytes; i++) {
for (let j = 0; j < 8; j++) {
const val = bitstream2.readBits(1);
if (j < 2) {
continue;
}
bitArray.unshift(val);
}
}
const encoded = bitArray.reduce((acc, bit, index) => {
return acc | (bit << index);
}, 0);
return encoded;
};
export const readBlockSize = (slice, blockSizeBits) => {
if (blockSizeBits === 'uncommon-u16') {
return readU16Be(slice) + 1;
}
else if (blockSizeBits === 'uncommon-u8') {
return readU8(slice) + 1;
}
else if (typeof blockSizeBits === 'number') {
return blockSizeBits;
}
else {
assertNever(blockSizeBits);
assert(false);
}
};
export const readSampleRate = (slice, sampleRateOrUncommon) => {
if (sampleRateOrUncommon === 'uncommon-u16') {
return readU16Be(slice);
}
if (sampleRateOrUncommon === 'uncommon-u16-10') {
return readU16Be(slice) * 10;
}
if (sampleRateOrUncommon === 'uncommon-u8') {
return readU8(slice);
}
if (typeof sampleRateOrUncommon === 'number') {
return sampleRateOrUncommon;
}
return null;
};
// https://www.rfc-editor.org/rfc/rfc9639.html#section-9.1.1
export const calculateCrc8 = (data) => {
const polynomial = 0x07; // x^8 + x^2 + x^1 + x^0
let crc = 0x00; // Initialize CRC to 0
for (const byte of data) {
crc ^= byte; // XOR byte into least significant byte of crc
for (let i = 0; i < 8; i++) {
// For each bit in the byte
if ((crc & 0x80) !== 0) {
// If the leftmost bit (MSB) is set
crc = (crc << 1) ^ polynomial; // Shift left and XOR with polynomial
}
else {
crc <<= 1; // Just shift left
}
crc &= 0xff; // Ensure CRC remains 8-bit
}
}
return crc;
};
@@ -0,0 +1,42 @@
/*!
* 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 { Muxer } from '../muxer.js';
import { Output, OutputAudioTrack } from '../output.js';
import { FlacOutputFormat } from '../output-format.js';
import { EncodedPacket } from '../packet.js';
import { AttachedImage } from '../metadata.js';
export declare class FlacMuxer extends Muxer {
private writer;
private metadataWritten;
private blockSizes;
private frameSizes;
private sampleRate;
private channels;
private bitsPerSample;
private format;
constructor(output: Output, format: FlacOutputFormat);
start(): Promise<void>;
writeHeader({ bitsPerSample, minimumBlockSize, maximumBlockSize, minimumFrameSize, maximumFrameSize, sampleRate, channels, totalSamples, }: {
minimumBlockSize: number;
maximumBlockSize: number;
minimumFrameSize: number;
maximumFrameSize: number;
sampleRate: number;
channels: number;
bitsPerSample: number;
totalSamples: number;
}): void;
writePictureBlock(picture: AttachedImage): void;
writeVorbisCommentAndPictureBlock(): void;
getMimeType(): Promise<string>;
addEncodedVideoPacket(): Promise<void>;
addEncodedAudioPacket(track: OutputAudioTrack, packet: EncodedPacket, meta?: EncodedAudioChunkMetadata): Promise<void>;
addSubtitleCue(): Promise<void>;
finalize(): Promise<void>;
}
//# sourceMappingURL=flac-muxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"flac-muxer.d.ts","sourceRoot":"","sources":["../../../../src/flac/flac-muxer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,aAAa,EAAwB,MAAM,aAAa,CAAC;AAYlE,qBAAa,SAAU,SAAQ,KAAK;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,eAAe,CAAS;IAEhC,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,UAAU,CAAgB;IAElC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,aAAa,CAAuB;IAE5C,OAAO,CAAC,MAAM,CAAmB;gBAErB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB;IAO9C,KAAK;IAIX,WAAW,CAAC,EACX,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,YAAY,GACZ,EAAE;QACF,gBAAgB,EAAE,MAAM,CAAC;QACzB,gBAAgB,EAAE,MAAM,CAAC;QACzB,gBAAgB,EAAE,MAAM,CAAC;QACzB,gBAAgB,EAAE,MAAM,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;KACrB;IAsCD,iBAAiB,CAAC,OAAO,EAAE,aAAa;IAoDxC,iCAAiC;IA4B3B,WAAW;IAIX,qBAAqB;IAIrB,qBAAqB,CAC1B,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,aAAa,EACrB,IAAI,CAAC,EAAE,yBAAyB,GAC9B,OAAO,CAAC,IAAI,CAAC;IAmEP,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CA0C/B"}
@@ -0,0 +1,222 @@
/*!
* 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 { validateAudioChunkMetadata } from '../codec.js';
import { createVorbisComments, FlacBlockType } from '../codec-data.js';
import { assert, Bitstream, textEncoder, toDataView, toUint8Array, } from '../misc.js';
import { Muxer } from '../muxer.js';
import { FileSlice, readBytes } from '../reader.js';
import { metadataTagsAreEmpty } from '../metadata.js';
import { readBlockSize, getBlockSizeOrUncommon, readCodedNumber, } from './flac-misc.js';
const FLAC_HEADER = /* #__PURE__ */ new Uint8Array([0x66, 0x4c, 0x61, 0x43]); // 'fLaC'
const STREAMINFO_SIZE = 38;
const STREAMINFO_BLOCK_SIZE = 34;
export class FlacMuxer extends Muxer {
constructor(output, format) {
super(output);
this.metadataWritten = false;
this.blockSizes = [];
this.frameSizes = [];
this.sampleRate = null;
this.channels = null;
this.bitsPerSample = null;
this.writer = output._writer;
this.format = format;
}
async start() {
this.writer.write(FLAC_HEADER);
}
writeHeader({ bitsPerSample, minimumBlockSize, maximumBlockSize, minimumFrameSize, maximumFrameSize, sampleRate, channels, totalSamples, }) {
assert(this.writer.getPos() === 4);
const hasMetadata = !metadataTagsAreEmpty(this.output._metadataTags);
const headerBitstream = new Bitstream(new Uint8Array(4));
headerBitstream.writeBits(1, Number(!hasMetadata)); // isLastMetadata
headerBitstream.writeBits(7, FlacBlockType.STREAMINFO); // metaBlockType = streaminfo
headerBitstream.writeBits(24, STREAMINFO_BLOCK_SIZE); // size
this.writer.write(headerBitstream.bytes);
const contentBitstream = new Bitstream(new Uint8Array(18));
contentBitstream.writeBits(16, minimumBlockSize);
contentBitstream.writeBits(16, maximumBlockSize);
contentBitstream.writeBits(24, minimumFrameSize);
contentBitstream.writeBits(24, maximumFrameSize);
contentBitstream.writeBits(20, sampleRate);
contentBitstream.writeBits(3, channels - 1);
contentBitstream.writeBits(5, bitsPerSample - 1);
// Bitstream operations are only safe until 32bit, breaks when using 36 bits
// Splitting up into writing 4 0 bits and then 32 bits is safe
// This is safe for audio up to (2 ** 32 / 44100 / 3600) -> 27 hours
// Not implementing support for more than 32 bits now
if (totalSamples >= 2 ** 32) {
throw new Error('This muxer only supports writing up to 2 ** 32 samples');
}
contentBitstream.writeBits(4, 0);
contentBitstream.writeBits(32, totalSamples);
this.writer.write(contentBitstream.bytes);
// The MD5 hash is calculated from decoded audio data, but we do not have access
// to it here. We are allowed to set 0:
// "A value of 0 signifies that the value is not known."
// https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
this.writer.write(new Uint8Array(16));
}
writePictureBlock(picture) {
// Header size:
// 4 bytes: picture type
// 4 bytes: media type length
// x bytes: media type
// 4 bytes: description length
// y bytes: description
// 1 bytes: width
// 1 bytes: height
// 1 bytes: color depth
// 1 bytes: number of indexed colors
// 4 bytes: picture data length
// z bytes: picture data
// Total: 20 + x + y + z
const headerSize = 32
+ picture.mimeType.length
+ (picture.description?.length ?? 0)
+ picture.data.length;
const header = new Uint8Array(headerSize);
let offset = 0;
const dataView = toDataView(header);
dataView.setUint32(offset, picture.kind === 'coverFront' ? 3 : picture.kind === 'coverBack' ? 4 : 0);
offset += 4;
dataView.setUint32(offset, picture.mimeType.length);
offset += 4;
header.set(textEncoder.encode(picture.mimeType), 8);
offset += picture.mimeType.length;
dataView.setUint32(offset, picture.description?.length ?? 0);
offset += 4;
header.set(textEncoder.encode(picture.description ?? ''), offset);
offset += picture.description?.length ?? 0;
offset += 4 + 4 + 4 + 4; // setting width, height, color depth, number of indexed colors to 0
dataView.setUint32(offset, picture.data.length);
offset += 4;
header.set(picture.data, offset);
offset += picture.data.length;
assert(offset === headerSize);
const headerBitstream = new Bitstream(new Uint8Array(4));
headerBitstream.writeBits(1, 0); // Last metadata block -> false, will be continued by vorbis comment
headerBitstream.writeBits(7, FlacBlockType.PICTURE); // Type -> Picture
headerBitstream.writeBits(24, headerSize);
this.writer.write(headerBitstream.bytes);
this.writer.write(header);
}
writeVorbisCommentAndPictureBlock() {
this.writer.seek(STREAMINFO_SIZE + FLAC_HEADER.byteLength);
if (metadataTagsAreEmpty(this.output._metadataTags)) {
this.metadataWritten = true;
return;
}
const pictures = this.output._metadataTags.images ?? [];
for (const picture of pictures) {
this.writePictureBlock(picture);
}
const vorbisComment = createVorbisComments(new Uint8Array(0), this.output._metadataTags, false);
const headerBitstream = new Bitstream(new Uint8Array(4));
headerBitstream.writeBits(1, 1); // Last metadata block -> true
headerBitstream.writeBits(7, FlacBlockType.VORBIS_COMMENT); // Type -> Vorbis comment
headerBitstream.writeBits(24, vorbisComment.length);
this.writer.write(headerBitstream.bytes);
this.writer.write(vorbisComment);
this.metadataWritten = true;
}
async getMimeType() {
return 'audio/flac';
}
async addEncodedVideoPacket() {
throw new Error('FLAC does not support video.');
}
async addEncodedAudioPacket(track, packet, meta) {
const release = await this.mutex.acquire();
validateAudioChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.description);
try {
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
if (this.sampleRate === null) {
this.sampleRate = meta.decoderConfig.sampleRate;
}
if (this.channels === null) {
this.channels = meta.decoderConfig.numberOfChannels;
}
if (this.bitsPerSample === null) {
const descriptionBitstream = new Bitstream(toUint8Array(meta.decoderConfig.description));
// skip 'fLaC' + block size + frame size + sample rate + number of channels
// See demuxer for the exact structure
descriptionBitstream.skipBits(103 + 64);
const bitsPerSample = descriptionBitstream.readBits(5) + 1;
this.bitsPerSample = bitsPerSample;
}
if (!this.metadataWritten) {
this.writeVorbisCommentAndPictureBlock();
}
const slice = FileSlice.tempFromBytes(packet.data);
readBytes(slice, 2);
const bytes = readBytes(slice, 2);
const bitstream = new Bitstream(bytes);
const blockSizeOrUncommon = getBlockSizeOrUncommon(bitstream.readBits(4));
if (blockSizeOrUncommon === null) {
throw new Error('Invalid FLAC frame: Invalid block size.');
}
readCodedNumber(slice); // num
const blockSize = readBlockSize(slice, blockSizeOrUncommon);
this.blockSizes.push(blockSize);
this.frameSizes.push(packet.data.length);
const startPos = this.writer.getPos();
this.writer.write(packet.data);
if (this.format._options.onFrame) {
this.format._options.onFrame(packet.data, startPos);
}
await this.writer.flush();
}
finally {
release();
}
}
addSubtitleCue() {
throw new Error('FLAC does not support subtitles.');
}
async finalize() {
const release = await this.mutex.acquire();
let minimumBlockSize = Infinity;
let maximumBlockSize = 0;
let minimumFrameSize = Infinity;
let maximumFrameSize = 0;
let totalSamples = 0;
for (let i = 0; i < this.blockSizes.length; i++) {
minimumFrameSize = Math.min(minimumFrameSize, this.frameSizes[i]);
maximumFrameSize = Math.max(maximumFrameSize, this.frameSizes[i]);
maximumBlockSize = Math.max(maximumBlockSize, this.blockSizes[i]);
totalSamples += this.blockSizes[i];
// Excluding the last frame from block size calculation
// https://www.rfc-editor.org/rfc/rfc9639.html#name-streaminfo
// "The minimum block size (in samples) used in the stream, excluding the last block."
const isLastFrame = i === this.blockSizes.length - 1;
if (isLastFrame) {
continue;
}
minimumBlockSize = Math.min(minimumBlockSize, this.blockSizes[i]);
}
assert(this.sampleRate !== null);
assert(this.channels !== null);
assert(this.bitsPerSample !== null);
this.writer.seek(4);
this.writeHeader({
minimumBlockSize,
maximumBlockSize,
minimumFrameSize,
maximumFrameSize,
sampleRate: this.sampleRate,
channels: this.channels,
bitsPerSample: this.bitsPerSample,
totalSamples,
});
release();
}
}
@@ -0,0 +1,79 @@
/*!
* 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 { MetadataTags } from './metadata.js';
import { FileSlice } from './reader.js';
import { Writer } from './writer.js';
export type Id3V2Header = {
majorVersion: number;
revision: number;
flags: number;
size: number;
};
export declare enum Id3V2HeaderFlags {
Unsynchronisation = 128,
ExtendedHeader = 64,
ExperimentalIndicator = 32,
Footer = 16
}
export declare enum Id3V2TextEncoding {
ISO_8859_1 = 0,
UTF_16_WITH_BOM = 1,
UTF_16_BE_NO_BOM = 2,
UTF_8 = 3
}
export declare const ID3_V1_TAG_SIZE = 128;
export declare const ID3_V2_HEADER_SIZE = 10;
export declare const ID3_V1_GENRES: string[];
export declare const parseId3V1Tag: (slice: FileSlice, tags: MetadataTags) => void;
export declare const readId3V1String: (slice: FileSlice, length: number) => string;
export declare const readId3V2Header: (slice: FileSlice) => Id3V2Header | null;
export declare const parseId3V2Tag: (slice: FileSlice, header: Id3V2Header, tags: MetadataTags) => void;
export declare class Id3V2Reader {
header: Id3V2Header;
bytes: Uint8Array;
pos: number;
view: DataView;
constructor(header: Id3V2Header, bytes: Uint8Array);
frameHeaderSize(): 6 | 10;
ununsynchronizeAll(): void;
ununsynchronizeRegion(start: number, end: number): void;
removeFooter(): void;
readBytes(length: number): Uint8Array<ArrayBufferLike>;
readU8(): number;
readU16(): number;
readU24(): number;
readU32(): number;
readAscii(length: number): string;
readId3V2Frame(): {
id: string;
size: number;
flags: number;
} | null;
readId3V2TextEncoding(): Id3V2TextEncoding;
readId3V2Text(encoding: Id3V2TextEncoding, until: number): string;
readId3V2EncodingAndText(until: number): string;
}
export declare class Id3V2Writer {
writer: Writer;
helper: Uint8Array<ArrayBuffer>;
helperView: DataView<ArrayBufferLike>;
constructor(writer: Writer);
writeId3V2Tag(metadata: MetadataTags): number;
writeU8(value: number): void;
writeU16(value: number): void;
writeU32(value: number): void;
writeAscii(text: string): void;
writeSynchsafeU32(value: number): void;
writeIsoString(text: string): void;
writeUtf8String(text: string): void;
writeId3V2TextFrame(frameId: string, text: string): void;
writeId3V2LyricsFrame(lyrics: string): void;
writeId3V2CommentFrame(comment: string): void;
writeId3V2ApicFrame(mimeType: string, pictureType: number, description: string, imageData: Uint8Array): void;
}
//# sourceMappingURL=id3.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"id3.d.ts","sourceRoot":"","sources":["../../../src/id3.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAU1C,OAAO,EAAE,SAAS,EAA2C,MAAM,UAAU,CAAC;AAC9E,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,MAAM,MAAM,WAAW,GAAG;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,oBAAY,gBAAgB;IAC3B,iBAAiB,MAAS;IAC1B,cAAc,KAAS;IACvB,qBAAqB,KAAS;IAC9B,MAAM,KAAS;CACf;AAED,oBAAY,iBAAiB;IAC5B,UAAU,IAAA;IACV,eAAe,IAAA;IACf,gBAAgB,IAAA;IAChB,KAAK,IAAA;CACL;AAED,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,eAAO,MAAM,aAAa,UA4BzB,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO,SAAS,EAAE,MAAM,YAAY,SA8CjE,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,SAAS,EAAE,QAAQ,MAAM,WAa/D,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,SAAS,KAAG,WAAW,GAAG,IAiBhE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO,SAAS,EAAE,QAAQ,WAAW,EAAE,MAAM,YAAY,SA+OtF,CAAC;AAGF,qBAAa,WAAW;IAIJ,MAAM,EAAE,WAAW;IAAS,KAAK,EAAE,UAAU;IAHhE,GAAG,SAAK;IACR,IAAI,EAAE,QAAQ,CAAC;gBAEI,MAAM,EAAE,WAAW,EAAS,KAAK,EAAE,UAAU;IAIhE,eAAe;IAIf,kBAAkB;IAmBlB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;IA0BhD,YAAY;IAKZ,SAAS,CAAC,MAAM,EAAE,MAAM;IAMxB,MAAM;IAMN,OAAO;IAMP,OAAO;IAOP,OAAO;IAMP,SAAS,CAAC,MAAM,EAAE,MAAM;IASxB,cAAc;;;;;IA6Dd,qBAAqB,IAAI,iBAAiB;IAQ1C,aAAa,CAAC,QAAQ,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IA+DjE,wBAAwB,CAAC,KAAK,EAAE,MAAM;CAQtC;AAED,qBAAa,WAAW;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,0BAAqB;IAC3B,UAAU,4BAA2B;gBAEzB,MAAM,EAAE,MAAM;IAI1B,aAAa,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM;IA2I7C,OAAO,CAAC,KAAK,EAAE,MAAM;IAKrB,QAAQ,CAAC,KAAK,EAAE,MAAM;IAKtB,QAAQ,CAAC,KAAK,EAAE,MAAM;IAKtB,UAAU,CAAC,IAAI,EAAE,MAAM;IAOvB,iBAAiB,CAAC,KAAK,EAAE,MAAM;IAI/B,cAAc,CAAC,IAAI,EAAE,MAAM;IAS3B,eAAe,CAAC,IAAI,EAAE,MAAM;IAM5B,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAiBjD,qBAAqB,CAAC,MAAM,EAAE,MAAM;IAqBpC,sBAAsB,CAAC,OAAO,EAAE,MAAM;IAwBtC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU;CA6BrG"}
@@ -0,0 +1,848 @@
/*!
* 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 { decodeSynchsafe, encodeSynchsafe } from '../shared/mp3-misc.js';
import { coalesceIndex, textDecoder, textEncoder, isIso88591Compatible, assertNever, keyValueIterator, toDataView, } from './misc.js';
import { readAscii, readBytes, readU32Be, readU8 } from './reader.js';
export var Id3V2HeaderFlags;
(function (Id3V2HeaderFlags) {
Id3V2HeaderFlags[Id3V2HeaderFlags["Unsynchronisation"] = 128] = "Unsynchronisation";
Id3V2HeaderFlags[Id3V2HeaderFlags["ExtendedHeader"] = 64] = "ExtendedHeader";
Id3V2HeaderFlags[Id3V2HeaderFlags["ExperimentalIndicator"] = 32] = "ExperimentalIndicator";
Id3V2HeaderFlags[Id3V2HeaderFlags["Footer"] = 16] = "Footer";
})(Id3V2HeaderFlags || (Id3V2HeaderFlags = {}));
export var Id3V2TextEncoding;
(function (Id3V2TextEncoding) {
Id3V2TextEncoding[Id3V2TextEncoding["ISO_8859_1"] = 0] = "ISO_8859_1";
Id3V2TextEncoding[Id3V2TextEncoding["UTF_16_WITH_BOM"] = 1] = "UTF_16_WITH_BOM";
Id3V2TextEncoding[Id3V2TextEncoding["UTF_16_BE_NO_BOM"] = 2] = "UTF_16_BE_NO_BOM";
Id3V2TextEncoding[Id3V2TextEncoding["UTF_8"] = 3] = "UTF_8";
})(Id3V2TextEncoding || (Id3V2TextEncoding = {}));
export const ID3_V1_TAG_SIZE = 128;
export const ID3_V2_HEADER_SIZE = 10;
export const ID3_V1_GENRES = [
'Blues', 'Classic rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge', 'Hip-hop', 'Jazz',
'Metal', 'New age', 'Oldies', 'Other', 'Pop', 'Rhythm and blues', 'Rap', 'Reggae', 'Rock',
'Techno', 'Industrial', 'Alternative', 'Ska', 'Death metal', 'Pranks', 'Soundtrack',
'Euro-techno', 'Ambient', 'Trip-hop', 'Vocal', 'Jazz & funk', 'Fusion', 'Trance', 'Classical',
'Instrumental', 'Acid', 'House', 'Game', 'Sound clip', 'Gospel', 'Noise', 'Alternative rock',
'Bass', 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental pop', 'Instrumental rock',
'Ethnic', 'Gothic', 'Darkwave', 'Techno-industrial', 'Electronic', 'Pop-folk', 'Eurodance',
'Dream', 'Southern rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40', 'Christian rap', 'Pop/funk',
'Jungle music', 'Native US', 'Cabaret', 'New wave', 'Psychedelic', 'Rave', 'Showtunes',
'Trailer', 'Lo-fi', 'Tribal', 'Acid punk', 'Acid jazz', 'Polka', 'Retro', 'Musical',
'Rock \'n\' roll', 'Hard rock', 'Folk', 'Folk rock', 'National folk', 'Swing', 'Fast fusion',
'Bebop', 'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic rock',
'Progressive rock', 'Psychedelic rock', 'Symphonic rock', 'Slow rock', 'Big band', 'Chorus',
'Easy listening', 'Acoustic', 'Humour', 'Speech', 'Chanson', 'Opera', 'Chamber music',
'Sonata', 'Symphony', 'Booty bass', 'Primus', 'Porn groove', 'Satire', 'Slow jam', 'Club',
'Tango', 'Samba', 'Folklore', 'Ballad', 'Power ballad', 'Rhythmic Soul', 'Freestyle', 'Duet',
'Punk rock', 'Drum solo', 'A cappella', 'Euro-house', 'Dance hall', 'Goa music', 'Drum & bass',
'Club-house', 'Hardcore techno', 'Terror', 'Indie', 'Britpop', 'Negerpunk', 'Polsk punk',
'Beat', 'Christian gangsta rap', 'Heavy metal', 'Black metal', 'Crossover',
'Contemporary Christian', 'Christian rock', 'Merengue', 'Salsa', 'Thrash metal', 'Anime',
'Jpop', 'Synthpop', 'Christmas', 'Art rock', 'Baroque', 'Bhangra', 'Big beat', 'Breakbeat',
'Chillout', 'Downtempo', 'Dub', 'EBM', 'Eclectic', 'Electro', 'Electroclash', 'Emo',
'Experimental', 'Garage', 'Global', 'IDM', 'Illbient', 'Industro-Goth', 'Jam Band',
'Krautrock', 'Leftfield', 'Lounge', 'Math rock', 'New romantic', 'Nu-breakz', 'Post-punk',
'Post-rock', 'Psytrance', 'Shoegaze', 'Space rock', 'Trop rock', 'World music', 'Neoclassical',
'Audiobook', 'Audio theatre', 'Neue Deutsche Welle', 'Podcast', 'Indie rock', 'G-Funk',
'Dubstep', 'Garage rock', 'Psybient',
];
export const parseId3V1Tag = (slice, tags) => {
const startPos = slice.filePos;
tags.raw ??= {};
tags.raw['TAG'] ??= readBytes(slice, ID3_V1_TAG_SIZE - 3); // Dump the whole tag into the raw metadata
slice.filePos = startPos;
const title = readId3V1String(slice, 30);
if (title)
tags.title ??= title;
const artist = readId3V1String(slice, 30);
if (artist)
tags.artist ??= artist;
const album = readId3V1String(slice, 30);
if (album)
tags.album ??= album;
const yearText = readId3V1String(slice, 4);
const year = Number.parseInt(yearText, 10);
if (Number.isInteger(year) && year > 0) {
tags.date ??= new Date(year, 0, 1);
}
const commentBytes = readBytes(slice, 30);
let comment;
// Check for the ID3v1.1 track number format:
// The 29th byte (index 28) is a null terminator, and the 30th byte is the track number.
if (commentBytes[28] === 0 && commentBytes[29] !== 0) {
const trackNum = commentBytes[29];
if (trackNum > 0) {
tags.trackNumber ??= trackNum;
}
slice.skip(-30);
comment = readId3V1String(slice, 28);
slice.skip(2);
}
else {
slice.skip(-30);
comment = readId3V1String(slice, 30);
}
if (comment)
tags.comment ??= comment;
const genreIndex = readU8(slice);
if (genreIndex < ID3_V1_GENRES.length) {
tags.genre ??= ID3_V1_GENRES[genreIndex];
}
};
export const readId3V1String = (slice, length) => {
const bytes = readBytes(slice, length);
const endIndex = coalesceIndex(bytes.indexOf(0), bytes.length);
const relevantBytes = bytes.subarray(0, endIndex);
// Decode as ISO-8859-1
let str = '';
for (let i = 0; i < relevantBytes.length; i++) {
str += String.fromCharCode(relevantBytes[i]);
}
return str.trimEnd(); // String also may be padded with spaces
};
export const readId3V2Header = (slice) => {
const startPos = slice.filePos;
const tag = readAscii(slice, 3);
const majorVersion = readU8(slice);
const revision = readU8(slice);
const flags = readU8(slice);
const sizeRaw = readU32Be(slice);
if (tag !== 'ID3' || majorVersion === 0xff || revision === 0xff || (sizeRaw & 0x80808080) !== 0) {
slice.filePos = startPos;
return null;
}
const size = decodeSynchsafe(sizeRaw);
return { majorVersion, revision, flags, size };
};
export const parseId3V2Tag = (slice, header, tags) => {
// https://id3.org/id3v2.3.0
if (![2, 3, 4].includes(header.majorVersion)) {
console.warn(`Unsupported ID3v2 major version: ${header.majorVersion}`);
return;
}
const bytes = readBytes(slice, header.size);
const reader = new Id3V2Reader(header, bytes);
if (header.flags & Id3V2HeaderFlags.Footer) {
reader.removeFooter();
}
if ((header.flags & Id3V2HeaderFlags.Unsynchronisation) && header.majorVersion === 3) {
reader.ununsynchronizeAll();
}
if (header.flags & Id3V2HeaderFlags.ExtendedHeader) {
const extendedHeaderSize = reader.readU32();
if (header.majorVersion === 3) {
reader.pos += extendedHeaderSize; // The extended header size excludes itself
}
else {
reader.pos += extendedHeaderSize - 4; // The extended header size includes itself
}
}
while (reader.pos <= reader.bytes.length - reader.frameHeaderSize()) {
const frame = reader.readId3V2Frame();
if (!frame) {
break;
}
const frameStartPos = reader.pos;
const frameEndPos = reader.pos + frame.size;
let frameEncrypted = false;
let frameCompressed = false;
let frameUnsynchronized = false;
if (header.majorVersion === 3) {
frameEncrypted = !!(frame.flags & (1 << 6));
frameCompressed = !!(frame.flags & (1 << 7));
}
else if (header.majorVersion === 4) {
frameEncrypted = !!(frame.flags & (1 << 2));
frameCompressed = !!(frame.flags & (1 << 3));
frameUnsynchronized = !!(frame.flags & (1 << 1))
|| !!(header.flags & Id3V2HeaderFlags.Unsynchronisation);
}
if (frameEncrypted) {
console.warn(`Skipping encrypted ID3v2 frame ${frame.id}`);
reader.pos = frameEndPos;
continue;
}
if (frameCompressed) {
console.warn(`Skipping compressed ID3v2 frame ${frame.id}`); // Maybe someday? Idk
reader.pos = frameEndPos;
continue;
}
if (frameUnsynchronized) {
reader.ununsynchronizeRegion(reader.pos, frameEndPos);
}
tags.raw ??= {};
if (frame.id[0] === 'T') {
// It's a text frame, let's decode as text
tags.raw[frame.id] ??= reader.readId3V2EncodingAndText(frameEndPos);
}
else {
// For the others, let's just get the bytes
tags.raw[frame.id] ??= reader.readBytes(frame.size);
}
reader.pos = frameStartPos;
switch (frame.id) {
case 'TIT2':
case 'TT2':
{
tags.title ??= reader.readId3V2EncodingAndText(frameEndPos);
}
;
break;
case 'TIT3':
case 'TT3':
{
tags.description ??= reader.readId3V2EncodingAndText(frameEndPos);
}
;
break;
case 'TPE1':
case 'TP1':
{
tags.artist ??= reader.readId3V2EncodingAndText(frameEndPos);
}
;
break;
case 'TALB':
case 'TAL':
{
tags.album ??= reader.readId3V2EncodingAndText(frameEndPos);
}
;
break;
case 'TPE2':
case 'TP2':
{
tags.albumArtist ??= reader.readId3V2EncodingAndText(frameEndPos);
}
;
break;
case 'TRCK':
case 'TRK':
{
const trackText = reader.readId3V2EncodingAndText(frameEndPos);
const parts = trackText.split('/');
const trackNum = Number.parseInt(parts[0], 10);
const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
tags.trackNumber ??= trackNum;
}
if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
tags.tracksTotal ??= tracksTotal;
}
}
;
break;
case 'TPOS':
case 'TPA':
{
const discText = reader.readId3V2EncodingAndText(frameEndPos);
const parts = discText.split('/');
const discNum = Number.parseInt(parts[0], 10);
const discsTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(discNum) && discNum > 0) {
tags.discNumber ??= discNum;
}
if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
tags.discsTotal ??= discsTotal;
}
}
;
break;
case 'TCON':
case 'TCO':
{
const genreText = reader.readId3V2EncodingAndText(frameEndPos);
let match = /^\((\d+)\)/.exec(genreText);
if (match) {
const genreNumber = Number.parseInt(match[1]);
if (ID3_V1_GENRES[genreNumber] !== undefined) {
tags.genre ??= ID3_V1_GENRES[genreNumber];
break;
}
}
match = /^\d+$/.exec(genreText);
if (match) {
const genreNumber = Number.parseInt(match[0]);
if (ID3_V1_GENRES[genreNumber] !== undefined) {
tags.genre ??= ID3_V1_GENRES[genreNumber];
break;
}
}
tags.genre ??= genreText;
}
;
break;
case 'TDRC':
case 'TDAT':
{
const dateText = reader.readId3V2EncodingAndText(frameEndPos);
const date = new Date(dateText);
if (!Number.isNaN(date.getTime())) {
tags.date ??= date;
}
}
;
break;
case 'TYER':
case 'TYE':
{
const yearText = reader.readId3V2EncodingAndText(frameEndPos);
const year = Number.parseInt(yearText, 10);
if (Number.isInteger(year)) {
tags.date ??= new Date(year, 0, 1);
}
}
;
break;
case 'USLT':
case 'ULT':
{
const encoding = reader.readU8();
reader.pos += 3; // Skip language
reader.readId3V2Text(encoding, frameEndPos); // Short content description
tags.lyrics ??= reader.readId3V2Text(encoding, frameEndPos);
}
;
break;
case 'COMM':
case 'COM':
{
const encoding = reader.readU8();
reader.pos += 3; // Skip language
reader.readId3V2Text(encoding, frameEndPos); // Short content description
tags.comment ??= reader.readId3V2Text(encoding, frameEndPos);
}
;
break;
case 'APIC':
case 'PIC':
{
const encoding = reader.readId3V2TextEncoding();
let mimeType;
if (header.majorVersion === 2) {
const imageFormat = reader.readAscii(3);
mimeType = imageFormat === 'PNG'
? 'image/png'
: imageFormat === 'JPG'
? 'image/jpeg'
: 'image/*';
}
else {
mimeType = reader.readId3V2Text(encoding, frameEndPos);
}
const pictureType = reader.readU8();
const description = reader.readId3V2Text(encoding, frameEndPos).trimEnd(); // Trim ending spaces
const imageDataSize = frameEndPos - reader.pos;
if (imageDataSize >= 0) {
const imageData = reader.readBytes(imageDataSize);
if (!tags.images)
tags.images = [];
tags.images.push({
data: imageData,
mimeType,
kind: pictureType === 3
? 'coverFront'
: pictureType === 4
? 'coverBack'
: 'unknown',
description,
});
}
}
;
break;
default:
{
reader.pos += frame.size;
}
;
break;
}
reader.pos = frameEndPos;
}
};
// https://id3.org/id3v2.3.0
export class Id3V2Reader {
constructor(header, bytes) {
this.header = header;
this.bytes = bytes;
this.pos = 0;
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}
frameHeaderSize() {
return this.header.majorVersion === 2 ? 6 : 10;
}
ununsynchronizeAll() {
const newBytes = [];
for (let i = 0; i < this.bytes.length; i++) {
const value1 = this.bytes[i];
newBytes.push(value1);
if (value1 === 0xff && i !== this.bytes.length - 1) {
const value2 = this.bytes[i];
if (value2 === 0x00) {
i++;
}
}
}
this.bytes = new Uint8Array(newBytes);
this.view = new DataView(this.bytes.buffer);
}
ununsynchronizeRegion(start, end) {
const newBytes = [];
for (let i = start; i < end; i++) {
const value1 = this.bytes[i];
newBytes.push(value1);
if (value1 === 0xff && i !== end - 1) {
const value2 = this.bytes[i + 1];
if (value2 === 0x00) {
i++;
}
}
}
const before = this.bytes.subarray(0, start);
const after = this.bytes.subarray(end);
this.bytes = new Uint8Array(before.length + newBytes.length + after.length);
this.bytes.set(before, 0);
this.bytes.set(newBytes, before.length);
this.bytes.set(after, before.length + newBytes.length);
this.view = new DataView(this.bytes.buffer);
}
removeFooter() {
this.bytes = this.bytes.subarray(0, this.bytes.length - ID3_V2_HEADER_SIZE);
this.view = new DataView(this.bytes.buffer);
}
readBytes(length) {
const slice = this.bytes.subarray(this.pos, this.pos + length);
this.pos += length;
return slice;
}
readU8() {
const value = this.view.getUint8(this.pos);
this.pos += 1;
return value;
}
readU16() {
const value = this.view.getUint16(this.pos, false);
this.pos += 2;
return value;
}
readU24() {
const high = this.view.getUint16(this.pos, false);
const low = this.view.getUint8(this.pos + 1);
this.pos += 3;
return high * 0x100 + low;
}
readU32() {
const value = this.view.getUint32(this.pos, false);
this.pos += 4;
return value;
}
readAscii(length) {
let str = '';
for (let i = 0; i < length; i++) {
str += String.fromCharCode(this.view.getUint8(this.pos + i));
}
this.pos += length;
return str;
}
readId3V2Frame() {
if (this.header.majorVersion === 2) {
const id = this.readAscii(3);
if (id === '\x00\x00\x00') {
return null;
}
const size = this.readU24();
return { id, size, flags: 0 };
}
else {
const id = this.readAscii(4);
if (id === '\x00\x00\x00\x00') {
// We've landed in the padding section
return null;
}
const sizeRaw = this.readU32();
let size = this.header.majorVersion === 4
? decodeSynchsafe(sizeRaw)
: sizeRaw;
const flags = this.readU16();
const headerEndPos = this.pos;
// Some files may have incorrectly synchsafed/unsynchsafed sizes. To validate which interpretation is valid,
// we validate a size by skipping ahead and seeing if we land at a valid frame header (or at the end of the
// tag.
const isSizeValid = (size) => {
const nextPos = this.pos + size;
if (nextPos > this.bytes.length) {
return false;
}
if (nextPos <= this.bytes.length - this.frameHeaderSize()) {
this.pos += size;
const nextId = this.readAscii(4);
if (nextId !== '\x00\x00\x00\x00' && !/[0-9A-Z]{4}/.test(nextId)) {
return false;
}
}
return true;
};
if (!isSizeValid(size)) {
// Flip the synchsafing, and try if this one makes more sense
const otherSize = this.header.majorVersion === 4
? sizeRaw
: decodeSynchsafe(sizeRaw);
if (isSizeValid(otherSize)) {
size = otherSize;
}
}
this.pos = headerEndPos;
return { id, size, flags };
}
}
readId3V2TextEncoding() {
const number = this.readU8();
if (number > 3) {
throw new Error(`Unsupported text encoding: ${number}`);
}
return number;
}
readId3V2Text(encoding, until) {
const startPos = this.pos;
const data = this.readBytes(until - this.pos);
switch (encoding) {
case Id3V2TextEncoding.ISO_8859_1: {
let str = '';
for (let i = 0; i < data.length; i++) {
const value = data[i];
if (value === 0) {
this.pos = startPos + i + 1;
break;
}
str += String.fromCharCode(value);
}
return str;
}
case Id3V2TextEncoding.UTF_16_WITH_BOM: {
if (data[0] === 0xff && data[1] === 0xfe) {
const decoder = new TextDecoder('utf-16le');
const endIndex = coalesceIndex(data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0), data.length);
this.pos = startPos + Math.min(endIndex + 2, data.length);
return decoder.decode(data.subarray(2, endIndex));
}
else if (data[0] === 0xfe && data[1] === 0xff) {
const decoder = new TextDecoder('utf-16be');
const endIndex = coalesceIndex(data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0), data.length);
this.pos = startPos + Math.min(endIndex + 2, data.length);
return decoder.decode(data.subarray(2, endIndex));
}
else {
// Treat it like UTF-8, some files do this
const endIndex = coalesceIndex(data.findIndex(x => x === 0), data.length);
this.pos = startPos + Math.min(endIndex + 1, data.length);
return textDecoder.decode(data.subarray(0, endIndex));
}
}
case Id3V2TextEncoding.UTF_16_BE_NO_BOM: {
const decoder = new TextDecoder('utf-16be');
const endIndex = coalesceIndex(data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0), data.length);
this.pos = startPos + Math.min(endIndex + 2, data.length);
return decoder.decode(data.subarray(0, endIndex));
}
case Id3V2TextEncoding.UTF_8: {
const endIndex = coalesceIndex(data.findIndex(x => x === 0), data.length);
this.pos = startPos + Math.min(endIndex + 1, data.length);
return textDecoder.decode(data.subarray(0, endIndex));
}
}
}
readId3V2EncodingAndText(until) {
if (this.pos >= until) {
return '';
}
const encoding = this.readId3V2TextEncoding();
return this.readId3V2Text(encoding, until);
}
}
export class Id3V2Writer {
constructor(writer) {
this.helper = new Uint8Array(8);
this.helperView = toDataView(this.helper);
this.writer = writer;
}
writeId3V2Tag(metadata) {
const tagStartPos = this.writer.getPos();
// Write ID3v2.4 header
this.writeAscii('ID3');
this.writeU8(0x04); // Version 2.4
this.writeU8(0x00); // Revision 0
this.writeU8(0x00); // Flags
this.writeSynchsafeU32(0); // Size placeholder
const framesStartPos = this.writer.getPos();
const writtenTags = new Set();
// Write all metadata frames
for (const { key, value } of keyValueIterator(metadata)) {
switch (key) {
case 'title':
{
this.writeId3V2TextFrame('TIT2', value);
writtenTags.add('TIT2');
}
;
break;
case 'description':
{
this.writeId3V2TextFrame('TIT3', value);
writtenTags.add('TIT3');
}
;
break;
case 'artist':
{
this.writeId3V2TextFrame('TPE1', value);
writtenTags.add('TPE1');
}
;
break;
case 'album':
{
this.writeId3V2TextFrame('TALB', value);
writtenTags.add('TALB');
}
;
break;
case 'albumArtist':
{
this.writeId3V2TextFrame('TPE2', value);
writtenTags.add('TPE2');
}
;
break;
case 'trackNumber':
{
const string = metadata.tracksTotal !== undefined
? `${value}/${metadata.tracksTotal}`
: value.toString();
this.writeId3V2TextFrame('TRCK', string);
writtenTags.add('TRCK');
}
;
break;
case 'discNumber':
{
const string = metadata.discsTotal !== undefined
? `${value}/${metadata.discsTotal}`
: value.toString();
this.writeId3V2TextFrame('TPOS', string);
writtenTags.add('TPOS');
}
;
break;
case 'genre':
{
this.writeId3V2TextFrame('TCON', value);
writtenTags.add('TCON');
}
;
break;
case 'date':
{
this.writeId3V2TextFrame('TDRC', value.toISOString().slice(0, 10));
writtenTags.add('TDRC');
}
;
break;
case 'lyrics':
{
this.writeId3V2LyricsFrame(value);
writtenTags.add('USLT');
}
;
break;
case 'comment':
{
this.writeId3V2CommentFrame(value);
writtenTags.add('COMM');
}
;
break;
case 'images':
{
const pictureTypeMap = { coverFront: 0x03, coverBack: 0x04, unknown: 0x00 };
for (const image of value) {
const pictureType = pictureTypeMap[image.kind] ?? 0x00;
const description = image.description ?? '';
this.writeId3V2ApicFrame(image.mimeType, pictureType, description, image.data);
}
}
;
break;
case 'tracksTotal':
case 'discsTotal':
{
// Handled with trackNumber and discNumber respectively
}
;
break;
case 'raw':
{
// Handled later
}
;
break;
default: {
assertNever(key);
}
}
}
if (metadata.raw) {
for (const key in metadata.raw) {
const value = metadata.raw[key];
if (value == null || key.length !== 4 || writtenTags.has(key)) {
continue;
}
let bytes;
if (typeof value === 'string') {
const encoded = textEncoder.encode(value);
bytes = new Uint8Array(encoded.byteLength + 2);
bytes[0] = Id3V2TextEncoding.UTF_8;
bytes.set(encoded, 1);
// Last byte is the null terminator
}
else if (value instanceof Uint8Array) {
bytes = value;
}
else {
continue;
}
this.writeAscii(key);
this.writeSynchsafeU32(bytes.byteLength);
this.writeU16(0x0000);
this.writer.write(bytes);
}
}
const framesEndPos = this.writer.getPos();
const framesSize = framesEndPos - framesStartPos;
// Update the size field in the header (synchsafe)
this.writer.seek(tagStartPos + 6); // Skip 'ID3' + version + revision + flags
this.writeSynchsafeU32(framesSize);
this.writer.seek(framesEndPos);
return framesSize + 10; // +10 for the header size
}
writeU8(value) {
this.helper[0] = value;
this.writer.write(this.helper.subarray(0, 1));
}
writeU16(value) {
this.helperView.setUint16(0, value, false);
this.writer.write(this.helper.subarray(0, 2));
}
writeU32(value) {
this.helperView.setUint32(0, value, false);
this.writer.write(this.helper.subarray(0, 4));
}
writeAscii(text) {
for (let i = 0; i < text.length; i++) {
this.helper[i] = text.charCodeAt(i);
}
this.writer.write(this.helper.subarray(0, text.length));
}
writeSynchsafeU32(value) {
this.writeU32(encodeSynchsafe(value));
}
writeIsoString(text) {
const bytes = new Uint8Array(text.length + 1);
for (let i = 0; i < text.length; i++) {
bytes[i] = text.charCodeAt(i);
}
bytes[text.length] = 0x00;
this.writer.write(bytes);
}
writeUtf8String(text) {
const utf8Data = textEncoder.encode(text);
this.writer.write(utf8Data);
this.writeU8(0x00);
}
writeId3V2TextFrame(frameId, text) {
const useIso88591 = isIso88591Compatible(text);
const textDataLength = useIso88591 ? text.length : textEncoder.encode(text).byteLength;
const frameSize = 1 + textDataLength + 1;
this.writeAscii(frameId);
this.writeSynchsafeU32(frameSize);
this.writeU16(0x0000);
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
if (useIso88591) {
this.writeIsoString(text);
}
else {
this.writeUtf8String(text);
}
}
writeId3V2LyricsFrame(lyrics) {
const useIso88591 = isIso88591Compatible(lyrics);
const shortDescription = '';
const frameSize = 1 + 3 + shortDescription.length + 1 + lyrics.length + 1;
this.writeAscii('USLT');
this.writeSynchsafeU32(frameSize);
this.writeU16(0x0000);
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
this.writeAscii('und');
if (useIso88591) {
this.writeIsoString(shortDescription);
this.writeIsoString(lyrics);
}
else {
this.writeUtf8String(shortDescription);
this.writeUtf8String(lyrics);
}
}
writeId3V2CommentFrame(comment) {
const useIso88591 = isIso88591Compatible(comment);
const textDataLength = useIso88591 ? comment.length : textEncoder.encode(comment).byteLength;
const shortDescription = '';
const frameSize = 1 + 3 + shortDescription.length + 1 + textDataLength + 1;
this.writeAscii('COMM');
this.writeSynchsafeU32(frameSize);
this.writeU16(0x0000);
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
this.writeU8(0x75); // 'u'
this.writeU8(0x6E); // 'n'
this.writeU8(0x64); // 'd'
if (useIso88591) {
this.writeIsoString(shortDescription);
this.writeIsoString(comment);
}
else {
this.writeUtf8String(shortDescription);
this.writeUtf8String(comment);
}
}
writeId3V2ApicFrame(mimeType, pictureType, description, imageData) {
const useIso88591 = isIso88591Compatible(mimeType) && isIso88591Compatible(description);
const descriptionDataLength = useIso88591
? description.length
: textEncoder.encode(description).byteLength;
const frameSize = 1 + mimeType.length + 1 + 1 + descriptionDataLength + 1 + imageData.byteLength;
this.writeAscii('APIC');
this.writeSynchsafeU32(frameSize);
this.writeU16(0x0000);
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
if (useIso88591) {
this.writeIsoString(mimeType);
}
else {
this.writeUtf8String(mimeType);
}
this.writeU8(pictureType);
if (useIso88591) {
this.writeIsoString(description);
}
else {
this.writeUtf8String(description);
}
this.writer.write(imageData);
}
}
@@ -0,0 +1,28 @@
/*!
* 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/.
*/
/// <reference types="dom-mediacapture-transform" preserve="true" />
/// <reference types="dom-webcodecs" preserve="true" />
export { Output, OutputOptions, BaseTrackMetadata, VideoTrackMetadata, AudioTrackMetadata, SubtitleTrackMetadata, } from './output.js';
export { OutputFormat, AdtsOutputFormat, AdtsOutputFormatOptions, FlacOutputFormat, FlacOutputFormatOptions, IsobmffOutputFormat, IsobmffOutputFormatOptions, MkvOutputFormat, MkvOutputFormatOptions, MovOutputFormat, Mp3OutputFormat, Mp3OutputFormatOptions, Mp4OutputFormat, MpegTsOutputFormat, MpegTsOutputFormatOptions, OggOutputFormat, OggOutputFormatOptions, WavOutputFormat, WavOutputFormatOptions, WebMOutputFormat, WebMOutputFormatOptions, InclusiveIntegerRange, TrackCountLimits, } from './output-format.js';
export { MediaSource, VideoSource, AudioSource, SubtitleSource, AudioBufferSource, AudioSampleSource, CanvasSource, EncodedAudioPacketSource, EncodedVideoPacketSource, MediaStreamAudioTrackSource, MediaStreamVideoTrackSource, TextSubtitleSource, VideoSampleSource, } from './media-source.js';
export { MediaCodec, VideoCodec, AudioCodec, SubtitleCodec, VIDEO_CODECS, AUDIO_CODECS, PCM_AUDIO_CODECS, NON_PCM_AUDIO_CODECS, SUBTITLE_CODECS, } from './codec.js';
export { VideoEncodingConfig, VideoEncodingAdditionalOptions, AudioEncodingConfig, AudioEncodingAdditionalOptions, canEncode, canEncodeVideo, canEncodeAudio, canEncodeSubtitles, getEncodableCodecs, getEncodableVideoCodecs, getEncodableAudioCodecs, getEncodableSubtitleCodecs, getFirstEncodableVideoCodec, getFirstEncodableAudioCodec, getFirstEncodableSubtitleCodec, Quality, QUALITY_VERY_LOW, QUALITY_LOW, QUALITY_MEDIUM, QUALITY_HIGH, QUALITY_VERY_HIGH, } from './encode.js';
export { Target, BufferTarget, FilePathTarget, FilePathTargetOptions, NullTarget, StreamTarget, StreamTargetOptions, StreamTargetChunk, } from './target.js';
export { AnyIterable, MaybePromise, Rotation, SetRequired, } from './misc.js';
export { TrackType, ALL_TRACK_TYPES, } from './output.js';
export { Source, BlobSource, BlobSourceOptions, BufferSource, FilePathSource, FilePathSourceOptions, StreamSource, StreamSourceOptions, ReadableStreamSource, ReadableStreamSourceOptions, UrlSource, UrlSourceOptions, } from './source.js';
export { InputFormat, AdtsInputFormat, FlacInputFormat, IsobmffInputFormat, MatroskaInputFormat, Mp3InputFormat, Mp4InputFormat, MpegTsInputFormat, OggInputFormat, QuickTimeInputFormat, WaveInputFormat, WebMInputFormat, ALL_FORMATS, ADTS, FLAC, MATROSKA, MP3, MP4, MPEG_TS, OGG, QTFF, WAVE, WEBM, } from './input-format.js';
export { Input, InputOptions, InputDisposedError, } from './input.js';
export { InputTrack, InputVideoTrack, InputAudioTrack, PacketStats, } from './input-track.js';
export { EncodedPacket, EncodedPacketSideData, PacketType, } from './packet.js';
export { AudioSample, AudioSampleInit, AudioSampleCopyToOptions, VideoSample, VideoSampleInit, VideoSamplePixelFormat, VideoSampleColorSpace, CropRectangle, VIDEO_SAMPLE_PIXEL_FORMATS, } from './sample.js';
export { AudioBufferSink, AudioSampleSink, BaseMediaSampleSink, CanvasSink, CanvasSinkOptions, EncodedPacketSink, PacketRetrievalOptions, VideoSampleSink, WrappedAudioBuffer, WrappedCanvas, } from './media-sink.js';
export { Conversion, ConversionOptions, ConversionVideoOptions, ConversionAudioOptions, ConversionCanceledError, DiscardedTrack, } from './conversion.js';
export { CustomVideoDecoder, CustomVideoEncoder, CustomAudioDecoder, CustomAudioEncoder, registerDecoder, registerEncoder, } from './custom-coder.js';
export { MetadataTags, AttachedImage, RichImageData, AttachedFile, TrackDisposition, } from './metadata.js';
//# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
/*!
* 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/.
*/
/// <reference types="dom-mediacapture-transform" preserve="true" />
/// <reference types="dom-webcodecs" preserve="true" />
const MEDIABUNNY_LOADED_SYMBOL = Symbol.for('mediabunny loaded');
if (globalThis[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[MEDIABUNNY_LOADED_SYMBOL] = true;
export { Output, } from './output.js';
export { OutputFormat, AdtsOutputFormat, FlacOutputFormat, IsobmffOutputFormat, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, MpegTsOutputFormat, OggOutputFormat, WavOutputFormat, WebMOutputFormat, } from './output-format.js';
export { MediaSource, VideoSource, AudioSource, SubtitleSource, AudioBufferSource, AudioSampleSource, CanvasSource, EncodedAudioPacketSource, EncodedVideoPacketSource, MediaStreamAudioTrackSource, MediaStreamVideoTrackSource, TextSubtitleSource, VideoSampleSource, } from './media-source.js';
export { VIDEO_CODECS, AUDIO_CODECS, PCM_AUDIO_CODECS, NON_PCM_AUDIO_CODECS, SUBTITLE_CODECS, } from './codec.js';
export { canEncode, canEncodeVideo, canEncodeAudio, canEncodeSubtitles, getEncodableCodecs, getEncodableVideoCodecs, getEncodableAudioCodecs, getEncodableSubtitleCodecs, getFirstEncodableVideoCodec, getFirstEncodableAudioCodec, getFirstEncodableSubtitleCodec, Quality, QUALITY_VERY_LOW, QUALITY_LOW, QUALITY_MEDIUM, QUALITY_HIGH, QUALITY_VERY_HIGH, } from './encode.js';
export { Target, BufferTarget, FilePathTarget, NullTarget, StreamTarget, } from './target.js';
export { ALL_TRACK_TYPES, } from './output.js';
export { Source, BlobSource, BufferSource, FilePathSource, StreamSource, ReadableStreamSource, UrlSource, } from './source.js';
export { InputFormat, AdtsInputFormat, FlacInputFormat, IsobmffInputFormat, MatroskaInputFormat, Mp3InputFormat, Mp4InputFormat, MpegTsInputFormat, OggInputFormat, QuickTimeInputFormat, WaveInputFormat, WebMInputFormat, ALL_FORMATS, ADTS, FLAC, MATROSKA, MP3, MP4, MPEG_TS, OGG, QTFF, WAVE, WEBM, } from './input-format.js';
export { Input, InputDisposedError, } from './input.js';
export { InputTrack, InputVideoTrack, InputAudioTrack, } from './input-track.js';
export { EncodedPacket, } from './packet.js';
export { AudioSample, VideoSample, VideoSampleColorSpace, VIDEO_SAMPLE_PIXEL_FORMATS, } from './sample.js';
export { AudioBufferSink, AudioSampleSink, BaseMediaSampleSink, CanvasSink, EncodedPacketSink, VideoSampleSink, } from './media-sink.js';
export { Conversion, ConversionCanceledError, } from './conversion.js';
export { CustomVideoDecoder, CustomVideoEncoder, CustomAudioDecoder, CustomAudioEncoder, registerDecoder, registerEncoder, } from './custom-coder.js';
export { RichImageData, AttachedFile, } from './metadata.js';
// 🐡🦔
@@ -0,0 +1,213 @@
/*!
* 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/.
*/
/**
* Base class representing an input media file format.
* @group Input formats
* @public
*/
export declare abstract class InputFormat {
/** Returns the name of the input format. */
abstract get name(): string;
/** Returns the typical base MIME type of the input format. */
abstract get mimeType(): string;
}
/**
* Format representing files compatible with the ISO base media file format (ISOBMFF), like MP4 or MOV files.
* @group Input formats
* @public
*/
export declare abstract class IsobmffInputFormat extends InputFormat {
}
/**
* MPEG-4 Part 14 (MP4) file format.
*
* Do not instantiate this class; use the {@link MP4} singleton instead.
*
* @group Input formats
* @public
*/
export declare class Mp4InputFormat extends IsobmffInputFormat {
get name(): string;
get mimeType(): string;
}
/**
* QuickTime File Format (QTFF), often called MOV.
*
* Do not instantiate this class; use the {@link QTFF} singleton instead.
*
* @group Input formats
* @public
*/
export declare class QuickTimeInputFormat extends IsobmffInputFormat {
get name(): string;
get mimeType(): string;
}
/**
* Matroska file format.
*
* Do not instantiate this class; use the {@link MATROSKA} singleton instead.
*
* @group Input formats
* @public
*/
export declare class MatroskaInputFormat extends InputFormat {
get name(): string;
get mimeType(): string;
}
/**
* WebM file format, based on Matroska.
*
* Do not instantiate this class; use the {@link WEBM} singleton instead.
*
* @group Input formats
* @public
*/
export declare class WebMInputFormat extends MatroskaInputFormat {
get name(): string;
get mimeType(): string;
}
/**
* MP3 file format.
*
* Do not instantiate this class; use the {@link MP3} singleton instead.
*
* @group Input formats
* @public
*/
export declare class Mp3InputFormat extends InputFormat {
get name(): string;
get mimeType(): string;
}
/**
* WAVE file format, based on RIFF.
*
* Do not instantiate this class; use the {@link WAVE} singleton instead.
*
* @group Input formats
* @public
*/
export declare class WaveInputFormat extends InputFormat {
get name(): string;
get mimeType(): string;
}
/**
* Ogg file format.
*
* Do not instantiate this class; use the {@link OGG} singleton instead.
*
* @group Input formats
* @public
*/
export declare class OggInputFormat extends InputFormat {
get name(): string;
get mimeType(): string;
}
/**
* FLAC file format.
*
* Do not instantiate this class; use the {@link FLAC} singleton instead.
*
* @group Input formats
* @public
*/
export declare class FlacInputFormat extends InputFormat {
get name(): string;
get mimeType(): string;
}
/**
* ADTS file format.
*
* Do not instantiate this class; use the {@link ADTS} singleton instead.
*
* @group Input formats
* @public
*/
export declare class AdtsInputFormat extends InputFormat {
get name(): string;
get mimeType(): string;
}
/**
* MPEG Transport Stream (MPEG-TS) file format.
*
* Do not instantiate this class; use the {@link MPEG_TS} singleton instead.
*
* @group Input formats
* @public
*/
export declare class MpegTsInputFormat extends InputFormat {
get name(): string;
get mimeType(): string;
}
/**
* MP4 input format singleton.
* @group Input formats
* @public
*/
export declare const MP4: Mp4InputFormat;
/**
* QuickTime File Format input format singleton.
* @group Input formats
* @public
*/
export declare const QTFF: QuickTimeInputFormat;
/**
* Matroska input format singleton.
* @group Input formats
* @public
*/
export declare const MATROSKA: MatroskaInputFormat;
/**
* WebM input format singleton.
* @group Input formats
* @public
*/
export declare const WEBM: WebMInputFormat;
/**
* MP3 input format singleton.
* @group Input formats
* @public
*/
export declare const MP3: Mp3InputFormat;
/**
* WAVE input format singleton.
* @group Input formats
* @public
*/
export declare const WAVE: WaveInputFormat;
/**
* Ogg input format singleton.
* @group Input formats
* @public
*/
export declare const OGG: OggInputFormat;
/**
* ADTS input format singleton.
* @group Input formats
* @public
*/
export declare const ADTS: AdtsInputFormat;
/**
* FLAC input format singleton.
* @group Input formats
* @public
*/
export declare const FLAC: FlacInputFormat;
/**
* MPEG-TS input format singleton.
* @group Input formats
* @public
*/
export declare const MPEG_TS: 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 declare const ALL_FORMATS: InputFormat[];
//# sourceMappingURL=input-format.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"input-format.d.ts","sourceRoot":"","sources":["../../../src/input-format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA6BH;;;;GAIG;AACH,8BAAsB,WAAW;IAOhC,4CAA4C;IAC5C,QAAQ,KAAK,IAAI,IAAI,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,8BAAsB,kBAAmB,SAAQ,WAAW;CAqB3D;AAED;;;;;;;GAOG;AACH,qBAAa,cAAe,SAAQ,kBAAkB;IAOrD,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CACD;AAED;;;;;;;GAOG;AACH,qBAAa,oBAAqB,SAAQ,kBAAkB;IAO3D,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CACD;AAED;;;;;;;GAOG;AACH,qBAAa,mBAAoB,SAAQ,WAAW;IAmFnD,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CACD;AAED;;;;;;;GAOG;AACH,qBAAa,eAAgB,SAAQ,mBAAmB;IAMvD,IAAa,IAAI,WAEhB;IAED,IAAa,QAAQ,WAEpB;CACD;AAED;;;;;;;GAOG;AACH,qBAAa,cAAe,SAAQ,WAAW;IA+D9C,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CACD;AAED;;;;;;;GAOG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAuB/C,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CACD;AAED;;;;;;;GAOG;AACH,qBAAa,cAAe,SAAQ,WAAW;IAe9C,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CACD;AACD;;;;;;;GAOG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAU/C,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CAMD;AAED;;;;;;;GAOG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAwD/C,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CACD;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IA6BjD,IAAI,IAAI,WAEP;IAED,IAAI,QAAQ,WAEX;CACD;AAED;;;;GAIG;AACH,eAAO,MAAM,GAAG,gBAAuC,CAAC;AACxD;;;;GAIG;AACH,eAAO,MAAM,IAAI,sBAA6C,CAAC;AAC/D;;;;GAIG;AACH,eAAO,MAAM,QAAQ,qBAA4C,CAAC;AAClE;;;;GAIG;AACH,eAAO,MAAM,IAAI,iBAAwC,CAAC;AAC1D;;;;GAIG;AACH,eAAO,MAAM,GAAG,gBAAuC,CAAC;AACxD;;;;GAIG;AACH,eAAO,MAAM,IAAI,iBAAwC,CAAC;AAC1D;;;;GAIG;AACH,eAAO,MAAM,GAAG,gBAAuC,CAAC;AACxD;;;;GAIG;AACH,eAAO,MAAM,IAAI,iBAAwC,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,IAAI,iBAAwC,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,OAAO,mBAA0C,CAAC;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,WAAW,EAAqE,CAAC"}
@@ -0,0 +1,546 @@
/*!
* 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 { IsobmffDemuxer } from './isobmff/isobmff-demuxer.js';
import { EBMLId, MAX_HEADER_SIZE, MIN_HEADER_SIZE, readAsciiString, readElementHeader, readElementSize, readUnsignedInt, readVarIntSize, } from './matroska/ebml.js';
import { MatroskaDemuxer } from './matroska/matroska-demuxer.js';
import { Mp3Demuxer } from './mp3/mp3-demuxer.js';
import { FRAME_HEADER_SIZE, getXingOffset, INFO, XING } from '../shared/mp3-misc.js';
import { ID3_V2_HEADER_SIZE, readId3V2Header } from './id3.js';
import { readNextMp3FrameHeader } from './mp3/mp3-reader.js';
import { OggDemuxer } from './ogg/ogg-demuxer.js';
import { WaveDemuxer } from './wave/wave-demuxer.js';
import { MAX_ADTS_FRAME_HEADER_SIZE, MIN_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from './adts/adts-reader.js';
import { AdtsDemuxer } from './adts/adts-demuxer.js';
import { readAscii, readBytes, readU32Be } from './reader.js';
import { FlacDemuxer } from './flac/flac-demuxer.js';
import { MpegTsDemuxer } from './mpeg-ts/mpeg-ts-demuxer.js';
import { TS_PACKET_SIZE } from './mpeg-ts/mpeg-ts-misc.js';
/**
* Base class representing an input media file format.
* @group Input formats
* @public
*/
export class InputFormat {
}
/**
* Format representing files compatible with the ISO base media file format (ISOBMFF), like MP4 or MOV files.
* @group Input formats
* @public
*/
export class IsobmffInputFormat extends InputFormat {
/** @internal */
async _getMajorBrand(input) {
let slice = input._reader.requestSlice(0, 12);
if (slice instanceof Promise)
slice = await slice;
if (!slice)
return null;
slice.skip(4);
const fourCc = readAscii(slice, 4);
if (fourCc !== 'ftyp') {
return null;
}
return readAscii(slice, 4);
}
/** @internal */
_createDemuxer(input) {
return new IsobmffDemuxer(input);
}
}
/**
* MPEG-4 Part 14 (MP4) file format.
*
* Do not instantiate this class; use the {@link MP4} singleton instead.
*
* @group Input formats
* @public
*/
export class Mp4InputFormat extends IsobmffInputFormat {
/** @internal */
async _canReadInput(input) {
const majorBrand = await this._getMajorBrand(input);
return !!majorBrand && majorBrand !== 'qt ';
}
get name() {
return 'MP4';
}
get mimeType() {
return 'video/mp4';
}
}
/**
* QuickTime File Format (QTFF), often called MOV.
*
* Do not instantiate this class; use the {@link QTFF} singleton instead.
*
* @group Input formats
* @public
*/
export class QuickTimeInputFormat extends IsobmffInputFormat {
/** @internal */
async _canReadInput(input) {
const majorBrand = await this._getMajorBrand(input);
return majorBrand === 'qt ';
}
get name() {
return 'QuickTime File Format';
}
get mimeType() {
return 'video/quicktime';
}
}
/**
* Matroska file format.
*
* Do not instantiate this class; use the {@link MATROSKA} singleton instead.
*
* @group Input formats
* @public
*/
export class MatroskaInputFormat extends InputFormat {
/** @internal */
async isSupportedEBMLOfDocType(input, desiredDocType) {
let headerSlice = input._reader.requestSlice(0, MAX_HEADER_SIZE);
if (headerSlice instanceof Promise)
headerSlice = await headerSlice;
if (!headerSlice)
return false;
const varIntSize = readVarIntSize(headerSlice);
if (varIntSize === null) {
return false;
}
if (varIntSize < 1 || varIntSize > 8) {
return false;
}
const id = readUnsignedInt(headerSlice, varIntSize);
if (id !== EBMLId.EBML) {
return false;
}
const dataSize = readElementSize(headerSlice);
if (typeof dataSize !== 'number') {
return false; // Miss me with that shit
}
let dataSlice = input._reader.requestSlice(headerSlice.filePos, dataSize);
if (dataSlice instanceof Promise)
dataSlice = await dataSlice;
if (!dataSlice)
return false;
const startPos = headerSlice.filePos;
while (dataSlice.filePos <= startPos + dataSize - MIN_HEADER_SIZE) {
const header = readElementHeader(dataSlice);
if (!header)
break;
const { id, size } = header;
const dataStartPos = dataSlice.filePos;
if (size === undefined)
return false;
switch (id) {
case EBMLId.EBMLVersion:
{
const ebmlVersion = readUnsignedInt(dataSlice, size);
if (ebmlVersion !== 1) {
return false;
}
}
;
break;
case EBMLId.EBMLReadVersion:
{
const ebmlReadVersion = readUnsignedInt(dataSlice, size);
if (ebmlReadVersion !== 1) {
return false;
}
}
;
break;
case EBMLId.DocType:
{
const docType = readAsciiString(dataSlice, size);
if (docType !== desiredDocType) {
return false;
}
}
;
break;
case EBMLId.DocTypeVersion:
{
const docTypeVersion = readUnsignedInt(dataSlice, size);
if (docTypeVersion > 4) { // Support up to Matroska v4
return false;
}
}
;
break;
}
dataSlice.filePos = dataStartPos + size;
}
return true;
}
/** @internal */
_canReadInput(input) {
return this.isSupportedEBMLOfDocType(input, 'matroska');
}
/** @internal */
_createDemuxer(input) {
return new MatroskaDemuxer(input);
}
get name() {
return 'Matroska';
}
get mimeType() {
return 'video/x-matroska';
}
}
/**
* WebM file format, based on Matroska.
*
* Do not instantiate this class; use the {@link WEBM} singleton instead.
*
* @group Input formats
* @public
*/
export class WebMInputFormat extends MatroskaInputFormat {
/** @internal */
_canReadInput(input) {
return this.isSupportedEBMLOfDocType(input, 'webm');
}
get name() {
return 'WebM';
}
get mimeType() {
return 'video/webm';
}
}
/**
* MP3 file format.
*
* Do not instantiate this class; use the {@link MP3} singleton instead.
*
* @group Input formats
* @public
*/
export class Mp3InputFormat extends InputFormat {
/** @internal */
async _canReadInput(input) {
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;
}
const firstResult = await readNextMp3FrameHeader(input._reader, currentPos, currentPos + 4096);
if (!firstResult) {
return false;
}
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;
}
currentPos = firstResult.startPos + firstResult.header.totalSize;
// 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 readNextMp3FrameHeader(input._reader, currentPos, currentPos + FRAME_HEADER_SIZE);
if (!secondResult) {
return false;
}
const secondHeader = secondResult.header;
// In a well-formed MP3 file, we'd expect these two frames to share some similarities:
if (firstHeader.channel !== secondHeader.channel || firstHeader.sampleRate !== secondHeader.sampleRate) {
return false;
}
// We have found two matching consecutive MP3 frames, a strong indicator that this is an MP3 file
return true;
}
/** @internal */
_createDemuxer(input) {
return new Mp3Demuxer(input);
}
get name() {
return 'MP3';
}
get mimeType() {
return 'audio/mpeg';
}
}
/**
* WAVE file format, based on RIFF.
*
* Do not instantiate this class; use the {@link WAVE} singleton instead.
*
* @group Input formats
* @public
*/
export class WaveInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input) {
let slice = input._reader.requestSlice(0, 12);
if (slice instanceof Promise)
slice = await slice;
if (!slice)
return false;
const riffType = readAscii(slice, 4);
if (riffType !== 'RIFF' && riffType !== 'RIFX' && riffType !== 'RF64') {
return false;
}
slice.skip(4);
const format = readAscii(slice, 4);
return format === 'WAVE';
}
/** @internal */
_createDemuxer(input) {
return new WaveDemuxer(input);
}
get name() {
return 'WAVE';
}
get mimeType() {
return 'audio/wav';
}
}
/**
* Ogg file format.
*
* Do not instantiate this class; use the {@link OGG} singleton instead.
*
* @group Input formats
* @public
*/
export class OggInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input) {
let slice = input._reader.requestSlice(0, 4);
if (slice instanceof Promise)
slice = await slice;
if (!slice)
return false;
return readAscii(slice, 4) === 'OggS';
}
/** @internal */
_createDemuxer(input) {
return new OggDemuxer(input);
}
get name() {
return 'Ogg';
}
get mimeType() {
return 'application/ogg';
}
}
/**
* FLAC file format.
*
* Do not instantiate this class; use the {@link FLAC} singleton instead.
*
* @group Input formats
* @public
*/
export class FlacInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input) {
let slice = input._reader.requestSlice(0, 4);
if (slice instanceof Promise)
slice = await slice;
if (!slice)
return false;
return readAscii(slice, 4) === 'fLaC';
}
get name() {
return 'FLAC';
}
get mimeType() {
return 'audio/flac';
}
/** @internal */
_createDemuxer(input) {
return new FlacDemuxer(input);
}
}
/**
* ADTS file format.
*
* Do not instantiate this class; use the {@link ADTS} singleton instead.
*
* @group Input formats
* @public
*/
export class AdtsInputFormat extends InputFormat {
/** @internal */
async _canReadInput(input) {
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 = readAdtsFrameHeader(slice);
if (!firstHeader) {
return false;
}
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 = readAdtsFrameHeader(slice);
if (!secondHeader) {
return false;
}
return firstHeader.objectType === secondHeader.objectType
&& firstHeader.samplingFrequencyIndex === secondHeader.samplingFrequencyIndex
&& firstHeader.channelConfiguration === secondHeader.channelConfiguration;
}
/** @internal */
_createDemuxer(input) {
return new AdtsDemuxer(input);
}
get name() {
return 'ADTS';
}
get mimeType() {
return 'audio/aac';
}
}
/**
* 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) {
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) {
return new MpegTsDemuxer(input);
}
get name() {
return 'MPEG Transport Stream';
}
get mimeType() {
return 'video/MP2T';
}
}
/**
* MP4 input format singleton.
* @group Input formats
* @public
*/
export const MP4 = /* #__PURE__ */ new Mp4InputFormat();
/**
* QuickTime File Format input format singleton.
* @group Input formats
* @public
*/
export const QTFF = /* #__PURE__ */ new QuickTimeInputFormat();
/**
* Matroska input format singleton.
* @group Input formats
* @public
*/
export const MATROSKA = /* #__PURE__ */ new MatroskaInputFormat();
/**
* WebM input format singleton.
* @group Input formats
* @public
*/
export const WEBM = /* #__PURE__ */ new WebMInputFormat();
/**
* MP3 input format singleton.
* @group Input formats
* @public
*/
export const MP3 = /* #__PURE__ */ new Mp3InputFormat();
/**
* WAVE input format singleton.
* @group Input formats
* @public
*/
export const WAVE = /* #__PURE__ */ new WaveInputFormat();
/**
* Ogg input format singleton.
* @group Input formats
* @public
*/
export const OGG = /* #__PURE__ */ new OggInputFormat();
/**
* ADTS input format singleton.
* @group Input formats
* @public
*/
export const ADTS = /* #__PURE__ */ new AdtsInputFormat();
/**
* FLAC input format singleton.
* @group Input formats
* @public
*/
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 = [MP4, QTFF, MATROSKA, WEBM, WAVE, OGG, FLAC, MP3, ADTS, MPEG_TS];
@@ -0,0 +1,194 @@
/*!
* 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 { AudioCodec, MediaCodec, VideoCodec } from './codec.js';
import { Input } from './input.js';
import { PacketRetrievalOptions } from './media-sink.js';
import { Rotation } from './misc.js';
import { TrackType } from './output.js';
import { EncodedPacket, PacketType } from './packet.js';
import { TrackDisposition } from './metadata.js';
/**
* Contains aggregate statistics about the encoded packets of a track.
* @group Input files & tracks
* @public
*/
export type PacketStats = {
/** The total number of packets. */
packetCount: number;
/** The average number of packets per second. For video tracks, this will equal the average frame rate (FPS). */
averagePacketRate: number;
/** The average number of bits per second. */
averageBitrate: number;
};
export interface InputTrackBacking {
getId(): number;
getNumber(): number;
getCodec(): MediaCodec | null;
getInternalCodecId(): string | number | Uint8Array | null;
getName(): string | null;
getLanguageCode(): string;
getTimeResolution(): number;
getDisposition(): TrackDisposition;
getFirstTimestamp(): Promise<number>;
computeDuration(): Promise<number>;
getFirstPacket(options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
}
/**
* Represents a media track in an input file.
* @group Input files & tracks
* @public
*/
export declare abstract class InputTrack {
/** The input file this track belongs to. */
readonly input: Input;
/** The type of the track. */
abstract get type(): TrackType;
/** The codec of the track's packets. */
abstract get codec(): MediaCodec | null;
/** Returns the full codec parameter string for this track. */
abstract getCodecParameterString(): Promise<string | null>;
/** Checks if this track's packets can be decoded by the browser. */
abstract canDecode(): Promise<boolean>;
/**
* For a given packet of this track, this method determines the actual type of this packet (key/delta) by looking
* into its bitstream. Returns null if the type couldn't be determined.
*/
abstract determinePacketType(packet: EncodedPacket): Promise<PacketType | null>;
/** Returns true if and only if this track is a video track. */
isVideoTrack(): this is InputVideoTrack;
/** Returns true if and only if this track is an audio track. */
isAudioTrack(): this is InputAudioTrack;
/** The unique ID of this track in the input file. */
get id(): number;
/**
* 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(): number;
/**
* The identifier of the codec used internally by the container. It is not homogenized by Mediabunny
* and depends entirely on the container format.
*
* This field can be used to determine the codec of a track in case Mediabunny doesn't know that codec.
*
* - For ISOBMFF files, this field returns the name of the Sample Description Box (e.g. `'avc1'`).
* - 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(): string | number | Uint8Array<ArrayBufferLike> | null;
/**
* The ISO 639-2/T language code for this track. If the language is unknown, this field is `'und'` (undetermined).
*/
get languageCode(): string;
/** A user-defined name for this track. */
get name(): string | null;
/**
* A positive number x such that all timestamps and durations of all packets of this track are
* integer multiples of 1/x.
*/
get timeResolution(): number;
/** The track's disposition, i.e. information about its intended usage. */
get disposition(): TrackDisposition;
/**
* Returns the start timestamp of the first packet of this track, in seconds. While often near zero, this value
* may be positive or even negative. A negative starting timestamp means the track's timing has been offset. Samples
* with a negative timestamp should not be presented.
*/
getFirstTimestamp(): Promise<number>;
/** Returns the end timestamp of the last packet of this track, in seconds. */
computeDuration(): Promise<number>;
/**
* Computes aggregate packet statistics for this track, such as average packet rate or bitrate.
*
* @param targetPacketCount - This optional parameter sets a target for how many packets this method must have
* looked at before it can return early; this means, you can use it to aggregate only a subset (prefix) of all
* packets. This is very useful for getting a great estimate of video frame rate without having to scan through the
* entire file.
*/
computePacketStats(targetPacketCount?: number): Promise<PacketStats>;
}
export interface InputVideoTrackBacking extends InputTrackBacking {
getCodec(): VideoCodec | null;
getCodedWidth(): number;
getCodedHeight(): number;
getRotation(): Rotation;
getColorSpace(): Promise<VideoColorSpaceInit>;
canBeTransparent(): Promise<boolean>;
getDecoderConfig(): Promise<VideoDecoderConfig | null>;
}
/**
* Represents a video track in an input file.
* @group Input files & tracks
* @public
*/
export declare class InputVideoTrack extends InputTrack {
get type(): TrackType;
get codec(): VideoCodec | null;
/** The width in pixels of the track's coded samples, before any transformations or rotations. */
get codedWidth(): number;
/** The height in pixels of the track's coded samples, before any transformations or rotations. */
get codedHeight(): number;
/** The angle in degrees by which the track's frames should be rotated (clockwise). */
get rotation(): Rotation;
/** The width in pixels of the track's frames after rotation. */
get displayWidth(): number;
/** The height in pixels of the track's frames after rotation. */
get displayHeight(): number;
/** Returns the color space of the track's samples. */
getColorSpace(): Promise<VideoColorSpaceInit>;
/** If this method returns true, the track's samples use a high dynamic range (HDR). */
hasHighDynamicRange(): Promise<boolean>;
/** Checks if this track may contain transparent samples with alpha data. */
canBeTransparent(): Promise<boolean>;
/**
* Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#video-decoder-config) for decoding the
* track's packets using a [`VideoDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/VideoDecoder). Returns
* null if the track's codec is unknown.
*/
getDecoderConfig(): Promise<VideoDecoderConfig | null>;
getCodecParameterString(): Promise<string | null>;
canDecode(): Promise<boolean>;
determinePacketType(packet: EncodedPacket): Promise<PacketType | null>;
}
export interface InputAudioTrackBacking extends InputTrackBacking {
getCodec(): AudioCodec | null;
getNumberOfChannels(): number;
getSampleRate(): number;
getDecoderConfig(): Promise<AudioDecoderConfig | null>;
}
/**
* Represents an audio track in an input file.
* @group Input files & tracks
* @public
*/
export declare class InputAudioTrack extends InputTrack {
get type(): TrackType;
get codec(): AudioCodec | null;
/** The number of audio channels in the track. */
get numberOfChannels(): number;
/** The track's audio sample rate in hertz. */
get sampleRate(): number;
/**
* Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#audio-decoder-config) for decoding the
* track's packets using an [`AudioDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/AudioDecoder). Returns
* null if the track's codec is unknown.
*/
getDecoderConfig(): Promise<AudioDecoderConfig | null>;
getCodecParameterString(): Promise<string | null>;
canDecode(): Promise<boolean>;
determinePacketType(packet: EncodedPacket): Promise<PacketType | null>;
}
//# sourceMappingURL=input-track.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"input-track.d.ts","sourceRoot":"","sources":["../../../src/input-track.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,305 @@
/*!
* 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 { determineVideoPacketType } from './codec-data.js';
import { customAudioDecoders, customVideoDecoders } from './custom-coder.js';
import { EncodedPacketSink } from './media-sink.js';
import { assert } from './misc.js';
import { EncodedPacket } from './packet.js';
/**
* Represents a media track in an input file.
* @group Input files & tracks
* @public
*/
export class InputTrack {
/** @internal */
constructor(input, backing) {
this.input = input;
this._backing = backing;
}
/** Returns true if and only if this track is a video track. */
isVideoTrack() {
return this instanceof InputVideoTrack;
}
/** Returns true if and only if this track is an audio track. */
isAudioTrack() {
return this instanceof InputAudioTrack;
}
/** The unique ID of this track in the input file. */
get id() {
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.
*
* This field can be used to determine the codec of a track in case Mediabunny doesn't know that codec.
*
* - For ISOBMFF files, this field returns the name of the Sample Description Box (e.g. `'avc1'`).
* - 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() {
return this._backing.getInternalCodecId();
}
/**
* The ISO 639-2/T language code for this track. If the language is unknown, this field is `'und'` (undetermined).
*/
get languageCode() {
return this._backing.getLanguageCode();
}
/** A user-defined name for this track. */
get name() {
return this._backing.getName();
}
/**
* A positive number x such that all timestamps and durations of all packets of this track are
* integer multiples of 1/x.
*/
get timeResolution() {
return this._backing.getTimeResolution();
}
/** The track's disposition, i.e. information about its intended usage. */
get disposition() {
return this._backing.getDisposition();
}
/**
* Returns the start timestamp of the first packet of this track, in seconds. While often near zero, this value
* may be positive or even negative. A negative starting timestamp means the track's timing has been offset. Samples
* with a negative timestamp should not be presented.
*/
getFirstTimestamp() {
return this._backing.getFirstTimestamp();
}
/** Returns the end timestamp of the last packet of this track, in seconds. */
computeDuration() {
return this._backing.computeDuration();
}
/**
* Computes aggregate packet statistics for this track, such as average packet rate or bitrate.
*
* @param targetPacketCount - This optional parameter sets a target for how many packets this method must have
* looked at before it can return early; this means, you can use it to aggregate only a subset (prefix) of all
* packets. This is very useful for getting a great estimate of video frame rate without having to scan through the
* entire file.
*/
async computePacketStats(targetPacketCount = Infinity) {
const sink = new EncodedPacketSink(this);
let startTimestamp = Infinity;
let endTimestamp = -Infinity;
let packetCount = 0;
let totalPacketBytes = 0;
for await (const packet of sink.packets(undefined, undefined, { metadataOnly: true })) {
if (packetCount >= targetPacketCount
// This additional condition is needed to produce correct results with out-of-presentation-order packets
&& packet.timestamp >= endTimestamp) {
break;
}
startTimestamp = Math.min(startTimestamp, packet.timestamp);
endTimestamp = Math.max(endTimestamp, packet.timestamp + packet.duration);
packetCount++;
totalPacketBytes += packet.byteLength;
}
return {
packetCount,
averagePacketRate: packetCount
? Number((packetCount / (endTimestamp - startTimestamp)).toPrecision(16))
: 0,
averageBitrate: packetCount
? Number((8 * totalPacketBytes / (endTimestamp - startTimestamp)).toPrecision(16))
: 0,
};
}
}
/**
* Represents a video track in an input file.
* @group Input files & tracks
* @public
*/
export class InputVideoTrack extends InputTrack {
/** @internal */
constructor(input, backing) {
super(input, backing);
this._backing = backing;
}
get type() {
return 'video';
}
get codec() {
return this._backing.getCodec();
}
/** The width in pixels of the track's coded samples, before any transformations or rotations. */
get codedWidth() {
return this._backing.getCodedWidth();
}
/** The height in pixels of the track's coded samples, before any transformations or rotations. */
get codedHeight() {
return this._backing.getCodedHeight();
}
/** The angle in degrees by which the track's frames should be rotated (clockwise). */
get rotation() {
return this._backing.getRotation();
}
/** The width in pixels of the track's frames after rotation. */
get displayWidth() {
const rotation = this._backing.getRotation();
return rotation % 180 === 0 ? this._backing.getCodedWidth() : this._backing.getCodedHeight();
}
/** The height in pixels of the track's frames after rotation. */
get displayHeight() {
const rotation = this._backing.getRotation();
return rotation % 180 === 0 ? this._backing.getCodedHeight() : this._backing.getCodedWidth();
}
/** Returns the color space of the track's samples. */
getColorSpace() {
return this._backing.getColorSpace();
}
/** If this method returns true, the track's samples use a high dynamic range (HDR). */
async hasHighDynamicRange() {
const colorSpace = await this._backing.getColorSpace();
return colorSpace.primaries === 'bt2020' || colorSpace.primaries === 'smpte432'
|| colorSpace.transfer === 'pg' || colorSpace.transfer === 'hlg'
|| colorSpace.matrix === 'bt2020-ncl';
}
/** Checks if this track may contain transparent samples with alpha data. */
canBeTransparent() {
return this._backing.canBeTransparent();
}
/**
* Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#video-decoder-config) for decoding the
* track's packets using a [`VideoDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/VideoDecoder). Returns
* null if the track's codec is unknown.
*/
getDecoderConfig() {
return this._backing.getDecoderConfig();
}
async getCodecParameterString() {
const decoderConfig = await this._backing.getDecoderConfig();
return decoderConfig?.codec ?? null;
}
async canDecode() {
try {
const decoderConfig = await this._backing.getDecoderConfig();
if (!decoderConfig) {
return false;
}
const codec = this._backing.getCodec();
assert(codec !== null);
if (customVideoDecoders.some(x => x.supports(codec, decoderConfig))) {
return true;
}
if (typeof VideoDecoder === 'undefined') {
return false;
}
const support = await VideoDecoder.isConfigSupported(decoderConfig);
return support.supported === true;
}
catch (error) {
console.error('Error during decodability check:', error);
return false;
}
}
async determinePacketType(packet) {
if (!(packet instanceof EncodedPacket)) {
throw new TypeError('packet must be an EncodedPacket.');
}
if (packet.isMetadataOnly) {
throw new TypeError('packet must not be metadata-only to determine its type.');
}
if (this.codec === null) {
return null;
}
const decoderConfig = await this.getDecoderConfig();
assert(decoderConfig);
return determineVideoPacketType(this.codec, decoderConfig, packet.data);
}
}
/**
* Represents an audio track in an input file.
* @group Input files & tracks
* @public
*/
export class InputAudioTrack extends InputTrack {
/** @internal */
constructor(input, backing) {
super(input, backing);
this._backing = backing;
}
get type() {
return 'audio';
}
get codec() {
return this._backing.getCodec();
}
/** The number of audio channels in the track. */
get numberOfChannels() {
return this._backing.getNumberOfChannels();
}
/** The track's audio sample rate in hertz. */
get sampleRate() {
return this._backing.getSampleRate();
}
/**
* Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#audio-decoder-config) for decoding the
* track's packets using an [`AudioDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/AudioDecoder). Returns
* null if the track's codec is unknown.
*/
getDecoderConfig() {
return this._backing.getDecoderConfig();
}
async getCodecParameterString() {
const decoderConfig = await this._backing.getDecoderConfig();
return decoderConfig?.codec ?? null;
}
async canDecode() {
try {
const decoderConfig = await this._backing.getDecoderConfig();
if (!decoderConfig) {
return false;
}
const codec = this._backing.getCodec();
assert(codec !== null);
if (customAudioDecoders.some(x => x.supports(codec, decoderConfig))) {
return true;
}
if (decoderConfig.codec.startsWith('pcm-')) {
return true; // Since we decode it ourselves
}
else {
if (typeof AudioDecoder === 'undefined') {
return false;
}
const support = await AudioDecoder.isConfigSupported(decoderConfig);
return support.supported === true;
}
}
catch (error) {
console.error('Error during decodability check:', error);
return false;
}
}
async determinePacketType(packet) {
if (!(packet instanceof EncodedPacket)) {
throw new TypeError('packet must be an EncodedPacket.');
}
if (this.codec === null) {
return null;
}
return 'key'; // No audio codec with delta packets
}
}
@@ -0,0 +1,96 @@
/*!
* 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 { InputFormat } from './input-format.js';
import { Source } from './source.js';
/**
* The options for creating an Input object.
* @group Input files & tracks
* @public
*/
export type InputOptions<S extends Source = Source> = {
/** A list of supported formats. If the source file is not of one of these formats, then it cannot be read. */
formats: InputFormat[];
/** The source from which data will be read. */
source: S;
};
/**
* Represents an input media file. This is the root object from which all media read operations start.
* @group Input files & tracks
* @public
*/
export declare class Input<S extends Source = Source> implements Disposable {
/** True if the input has been disposed. */
get disposed(): boolean;
/**
* Creates a new input file from the specified options. No reading operations will be performed until methods are
* called on this instance.
*/
constructor(options: InputOptions<S>);
/**
* Returns the source from which this input file reads its data. This is the same source that was passed to the
* constructor.
*/
get source(): S;
/**
* Returns the format of the input file. You can compare this result directly to the {@link InputFormat} singletons
* or use `instanceof` checks for subset-aware logic (for example, `format instanceof MatroskaInputFormat` is true
* for both MKV and WebM).
*/
getFormat(): Promise<InputFormat>;
/**
* Computes the duration of the input file, in seconds. More precisely, returns the largest end timestamp among
* all tracks.
*/
computeDuration(): Promise<number>;
/**
* Returns the timestamp at which the input file starts. More precisely, returns the smallest starting timestamp
* among all tracks.
*/
getFirstTimestamp(): Promise<number>;
/** Returns the list of all tracks of this input file. */
getTracks(): Promise<import("./input-track.js").InputTrack[]>;
/** Returns the list of all video tracks of this input file. */
getVideoTracks(): Promise<import("./input-track.js").InputVideoTrack[]>;
/** Returns the list of all audio tracks of this input file. */
getAudioTracks(): Promise<import("./input-track.js").InputAudioTrack[]>;
/** Returns the primary video track of this input file, or null if there are no video tracks. */
getPrimaryVideoTrack(): Promise<import("./input-track.js").InputVideoTrack | null>;
/** Returns the primary audio track of this input file, or null if there are no audio tracks. */
getPrimaryAudioTrack(): Promise<import("./input-track.js").InputAudioTrack | null>;
/** Returns the full MIME type of this input file, including track codecs. */
getMimeType(): Promise<string>;
/**
* Returns descriptive metadata tags about the media file, such as title, author, date, cover art, or other
* attached files.
*/
getMetadataTags(): Promise<import("./metadata.js").MetadataTags>;
/**
* Disposes this input and frees connected resources. When an input is disposed, ongoing read operations will be
* canceled, all future read operations will fail, any open decoders will be closed, and all ongoing media sink
* operations will be canceled. Disallowed and canceled operations will throw an {@link InputDisposedError}.
*
* You are expected not to use an input after disposing it. While some operations may still work, it is not
* specified and may change in any future update.
*/
dispose(): void;
/**
* Calls `.dispose()` on the input, implementing the `Disposable` interface for use with
* JavaScript Explicit Resource Management features.
*/
[Symbol.dispose](): void;
}
/**
* Thrown when an operation was prevented because the corresponding {@link Input} has been disposed.
* @group Input files & tracks
* @public
*/
export declare class InputDisposedError extends Error {
/** Creates a new {@link InputDisposedError}. */
constructor(message?: string);
}
//# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../../src/input.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAIlC;;;;GAIG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACrD,8GAA8G;IAC9G,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,+CAA+C;IAC/C,MAAM,EAAE,CAAC,CAAC;CACV,CAAC;AAEF;;;;GAIG;AACH,qBAAa,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,YAAW,UAAU;IAclE,2CAA2C;IAC3C,IAAI,QAAQ,YAEX;IAED;;;OAGG;gBACS,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;IAoCpC;;;OAGG;IACH,IAAI,MAAM,MAET;IAED;;;;OAIG;IACG,SAAS;IAMf;;;OAGG;IACG,eAAe;IAKrB;;;OAGG;IACG,iBAAiB;IAUvB,yDAAyD;IACnD,SAAS;IAKf,+DAA+D;IACzD,cAAc;IAKpB,+DAA+D;IACzD,cAAc;IAKpB,gGAAgG;IAC1F,oBAAoB;IAK1B,gGAAgG;IAC1F,oBAAoB;IAK1B,6EAA6E;IACvE,WAAW;IAKjB;;;OAGG;IACG,eAAe;IAKrB;;;;;;;OAOG;IACH,OAAO;IAWP;;;OAGG;IACH,CAAC,MAAM,CAAC,OAAO,CAAC;CAGhB;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,gDAAgD;gBACpC,OAAO,SAA6B;CAIhD"}
@@ -0,0 +1,174 @@
/*!
* 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 { InputFormat } from './input-format.js';
import { assert, polyfillSymbolDispose } from './misc.js';
import { Reader } from './reader.js';
import { Source } from './source.js';
polyfillSymbolDispose();
/**
* Represents an input media file. This is the root object from which all media read operations start.
* @group Input files & tracks
* @public
*/
export class Input {
/** True if the input has been disposed. */
get disposed() {
return this._disposed;
}
/**
* Creates a new input file from the specified options. No reading operations will be performed until methods are
* called on this instance.
*/
constructor(options) {
/** @internal */
this._demuxerPromise = null;
/** @internal */
this._format = null;
/** @internal */
this._disposed = false;
if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.');
}
if (!Array.isArray(options.formats) || options.formats.some(x => !(x instanceof InputFormat))) {
throw new TypeError('options.formats must be an array of InputFormat.');
}
if (!(options.source instanceof Source)) {
throw new TypeError('options.source must be a Source.');
}
if (options.source._disposed) {
throw new Error('options.source must not be disposed.');
}
this._formats = options.formats;
this._source = options.source;
this._reader = new Reader(options.source);
}
/** @internal */
_getDemuxer() {
return this._demuxerPromise ??= (async () => {
this._reader.fileSize = await this._source.getSizeOrNull();
for (const format of this._formats) {
const canRead = await format._canReadInput(this);
if (canRead) {
this._format = format;
return format._createDemuxer(this);
}
}
throw new Error('Input has an unsupported or unrecognizable format.');
})();
}
/**
* Returns the source from which this input file reads its data. This is the same source that was passed to the
* constructor.
*/
get source() {
return this._source;
}
/**
* Returns the format of the input file. You can compare this result directly to the {@link InputFormat} singletons
* or use `instanceof` checks for subset-aware logic (for example, `format instanceof MatroskaInputFormat` is true
* for both MKV and WebM).
*/
async getFormat() {
await this._getDemuxer();
assert(this._format);
return this._format;
}
/**
* Computes the duration of the input file, in seconds. More precisely, returns the largest end timestamp among
* all tracks.
*/
async computeDuration() {
const demuxer = await this._getDemuxer();
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();
return demuxer.getTracks();
}
/** Returns the list of all video tracks of this input file. */
async getVideoTracks() {
const tracks = await this.getTracks();
return tracks.filter(x => x.isVideoTrack());
}
/** Returns the list of all audio tracks of this input file. */
async getAudioTracks() {
const tracks = await this.getTracks();
return tracks.filter(x => x.isAudioTrack());
}
/** Returns the primary video track of this input file, or null if there are no video tracks. */
async getPrimaryVideoTrack() {
const tracks = await this.getTracks();
return tracks.find(x => x.isVideoTrack()) ?? null;
}
/** Returns the primary audio track of this input file, or null if there are no audio tracks. */
async getPrimaryAudioTrack() {
const tracks = await this.getTracks();
return tracks.find(x => x.isAudioTrack()) ?? null;
}
/** Returns the full MIME type of this input file, including track codecs. */
async getMimeType() {
const demuxer = await this._getDemuxer();
return demuxer.getMimeType();
}
/**
* Returns descriptive metadata tags about the media file, such as title, author, date, cover art, or other
* attached files.
*/
async getMetadataTags() {
const demuxer = await this._getDemuxer();
return demuxer.getMetadataTags();
}
/**
* Disposes this input and frees connected resources. When an input is disposed, ongoing read operations will be
* canceled, all future read operations will fail, any open decoders will be closed, and all ongoing media sink
* operations will be canceled. Disallowed and canceled operations will throw an {@link InputDisposedError}.
*
* You are expected not to use an input after disposing it. While some operations may still work, it is not
* specified and may change in any future update.
*/
dispose() {
if (this._disposed) {
return;
}
this._disposed = true;
this._source._disposed = true;
this._source._dispose();
}
/**
* Calls `.dispose()` on the input, implementing the `Disposable` interface for use with
* JavaScript Explicit Resource Management features.
*/
[Symbol.dispose]() {
this.dispose();
}
}
/**
* Thrown when an operation was prevented because the corresponding {@link Input} has been disposed.
* @group Input files & tracks
* @public
*/
export class InputDisposedError extends Error {
/** Creates a new {@link InputDisposedError}. */
constructor(message = 'Input has been disposed.') {
super(message);
this.name = 'InputDisposedError';
}
}
@@ -0,0 +1,204 @@
/*!
* 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 { Writer } from '../writer.js';
import { IsobmffAudioTrackData, IsobmffMuxer, IsobmffSubtitleTrackData, IsobmffTrackData, IsobmffVideoTrackData } from './isobmff-muxer.js';
export declare class IsobmffBoxWriter {
private writer;
private helper;
private helperView;
/**
* Stores the position from the start of the file to where boxes elements have been written. This is used to
* rewrite/edit elements that were already added before, and to measure sizes of things.
*/
offsets: WeakMap<Box, number>;
constructor(writer: Writer);
writeU32(value: number): void;
writeU64(value: number): void;
writeAscii(text: string): void;
writeBox(box: Box): void;
writeBoxHeader(box: Box, size: number): void;
measureBoxHeader(box: Box): number;
patchBox(box: Box): void;
measureBox(box: Box): number;
}
export interface Box {
type: string;
contents?: Uint8Array;
children?: (Box | null)[];
size?: number;
largeSize?: boolean;
}
type NestedNumberArray = (number | NestedNumberArray)[];
export declare const box: (type: string, contents?: NestedNumberArray, children?: (Box | null)[]) => Box;
/** A FullBox always starts with a version byte, followed by three flag bytes. */
export declare const fullBox: (type: string, version: number, flags: number, contents?: NestedNumberArray, children?: Box[]) => Box;
/**
* File Type Compatibility Box: Allows the reader to determine whether this is a type of file that the
* reader understands.
*/
export declare const ftyp: (details: {
isQuickTime: boolean;
holdsAvc: boolean;
fragmented: boolean;
}) => Box;
/** Movie Sample Data Box. Contains the actual frames/samples of the media. */
export declare const mdat: (reserveLargeSize: boolean) => Box;
/** Free Space Box: A box that designates unused space in the movie data file. */
export declare const free: (size: number) => Box;
/**
* Movie Box: Used to specify the information that defines a movie - that is, the information that allows
* an application to interpret the sample data that is stored elsewhere.
*/
export declare const moov: (muxer: IsobmffMuxer) => Box;
/** Movie Header Box: Used to specify the characteristics of the entire movie, such as timescale and duration. */
export declare const mvhd: (creationTime: number, trackDatas: IsobmffTrackData[]) => Box;
/**
* Track Box: Defines a single track of a movie. A movie may consist of one or more tracks. Each track is
* independent of the other tracks in the movie and carries its own temporal and spatial information. Each Track Box
* contains its associated Media Box.
*/
export declare const trak: (trackData: IsobmffTrackData, creationTime: number) => Box;
/** Track Header Box: Specifies the characteristics of a single track within a movie. */
export declare const tkhd: (trackData: IsobmffTrackData, creationTime: number) => Box;
/** Media Box: Describes and define a track's media type and sample data. */
export declare const mdia: (trackData: IsobmffTrackData, creationTime: number) => Box;
/** Media Header Box: Specifies the characteristics of a media, including timescale and duration. */
export declare const mdhd: (trackData: IsobmffTrackData, creationTime: number) => Box;
/** Handler Reference Box. */
export declare const hdlr: (hasComponentType: boolean, handlerType: string, name: string, manufacturer?: string) => Box;
/**
* Media Information Box: Stores handler-specific information for a track's media data. The media handler uses this
* information to map from media time to media data and to process the media data.
*/
export declare const minf: (trackData: IsobmffTrackData) => Box;
/** Video Media Information Header Box: Defines specific color and graphics mode information. */
export declare const vmhd: () => Box;
/** Sound Media Information Header Box: Stores the sound media's control information, such as balance. */
export declare const smhd: () => Box;
/** Null Media Header Box. */
export declare const nmhd: () => Box;
/**
* Data Information Box: Contains information specifying the data handler component that provides access to the
* media data. The data handler component uses the Data Information Box to interpret the media's data.
*/
export declare const dinf: () => Box;
/**
* Data Reference Box: Contains tabular data that instructs the data handler component how to access the media's data.
*/
export declare const dref: () => Box;
export declare const url: () => Box;
/**
* Sample Table Box: Contains information for converting from media time to sample number to sample location. This box
* also indicates how to interpret the sample (for example, whether to decompress the video data and, if so, how).
*/
export declare const stbl: (trackData: IsobmffTrackData) => Box;
/**
* Sample Description Box: Stores information that allows you to decode samples in the media. The data stored in the
* sample description varies, depending on the media type.
*/
export declare const stsd: (trackData: IsobmffTrackData) => Box;
/** Video Sample Description Box: Contains information that defines how to interpret video media data. */
export declare const videoSampleDescription: (compressionType: string, trackData: IsobmffVideoTrackData) => Box;
/** Colour Information Box: Specifies the color space of the video. */
export declare const colr: (trackData: IsobmffVideoTrackData) => Box;
/** AVC Configuration Box: Provides additional information to the decoder. */
export declare const avcC: (trackData: IsobmffVideoTrackData) => Box;
/** HEVC Configuration Box: Provides additional information to the decoder. */
export declare const hvcC: (trackData: IsobmffVideoTrackData) => Box;
/** VP Configuration Box: Provides additional information to the decoder. */
export declare const vpcC: (trackData: IsobmffVideoTrackData) => Box | null;
/** AV1 Configuration Box: Provides additional information to the decoder. */
export declare const av1C: (trackData: IsobmffVideoTrackData) => Box;
/** Sound Sample Description Box: Contains information that defines how to interpret sound media data. */
export declare const soundSampleDescription: (compressionType: string, trackData: IsobmffAudioTrackData) => Box;
/** MPEG-4 Elementary Stream Descriptor Box. */
export declare const esds: (trackData: IsobmffAudioTrackData) => Box;
export declare const wave: (trackData: IsobmffAudioTrackData) => Box;
export declare const frma: (trackData: IsobmffAudioTrackData) => Box;
export declare const enda: (trackData: IsobmffAudioTrackData) => Box;
/** Opus Specific Box. */
export declare const dOps: (trackData: IsobmffAudioTrackData) => Box;
/** FLAC specific box. */
export declare const dfLa: (trackData: IsobmffAudioTrackData) => Box;
export declare const subtitleSampleDescription: (compressionType: string, trackData: IsobmffSubtitleTrackData) => Box;
export declare const vttC: (trackData: IsobmffSubtitleTrackData) => Box;
export declare const txtC: (textConfig: Uint8Array) => Box;
/**
* Time-To-Sample Box: Stores duration information for a media's samples, providing a mapping from a time in a media
* to the corresponding data sample. The table is compact, meaning that consecutive samples with the same time delta
* will be grouped.
*/
export declare const stts: (trackData: IsobmffTrackData) => Box;
/** Sync Sample Box: Identifies the key frames in the media, marking the random access points within a stream. */
export declare const stss: (trackData: IsobmffTrackData) => Box | null;
/**
* Sample-To-Chunk Box: As samples are added to a media, they are collected into chunks that allow optimized data
* access. A chunk contains one or more samples. Chunks in a media may have different sizes, and the samples within a
* chunk may have different sizes. The Sample-To-Chunk Box stores chunk information for the samples in a media, stored
* in a compactly-coded fashion.
*/
export declare const stsc: (trackData: IsobmffTrackData) => Box;
/** Sample Size Box: Specifies the byte size of each sample in the media. */
export declare const stsz: (trackData: IsobmffTrackData) => Box;
/** Chunk Offset Box: Identifies the location of each chunk of data in the media's data stream, relative to the file. */
export declare const stco: (trackData: IsobmffTrackData) => Box;
/**
* Composition Time to Sample Box: Stores composition time offset information (PTS-DTS) for a
* media's samples. The table is compact, meaning that consecutive samples with the same time
* composition time offset will be grouped.
*/
export declare const ctts: (trackData: IsobmffTrackData) => Box;
/**
* Composition to Decode Box: Stores information about the composition and display times of the media samples.
*/
export declare const cslg: (trackData: IsobmffTrackData) => Box | null;
/**
* Movie Extends Box: This box signals to readers that the file is fragmented. Contains a single Track Extends Box
* for each track in the movie.
*/
export declare const mvex: (trackDatas: IsobmffTrackData[]) => Box;
/** Track Extends Box: Contains the default values used by the movie fragments. */
export declare const trex: (trackData: IsobmffTrackData) => Box;
/**
* Movie Fragment Box: The movie fragments extend the presentation in time. They provide the information that would
* previously have been in the Movie Box.
*/
export declare const moof: (sequenceNumber: number, trackDatas: IsobmffTrackData[]) => Box;
/** Movie Fragment Header Box: Contains a sequence number as a safety check. */
export declare const mfhd: (sequenceNumber: number) => Box;
/** Track Fragment Box */
export declare const traf: (trackData: IsobmffTrackData) => Box;
/** Track Fragment Header Box: Provides a reference to the extended track, and flags. */
export declare const tfhd: (trackData: IsobmffTrackData) => Box;
/**
* Track Fragment Decode Time Box: Provides the absolute decode time of the first sample of the fragment. This is
* useful for performing random access on the media file.
*/
export declare const tfdt: (trackData: IsobmffTrackData) => Box;
/** Track Run Box: Specifies a run of contiguous samples for a given track. */
export declare const trun: (trackData: IsobmffTrackData) => Box;
/**
* Movie Fragment Random Access Box: For each track, provides pointers to sync samples within the file
* for random access.
*/
export declare const mfra: (trackDatas: IsobmffTrackData[]) => Box;
/** Track Fragment Random Access Box: Provides pointers to sync samples within the file for random access. */
export declare const tfra: (trackData: IsobmffTrackData, trackIndex: number) => Box;
/**
* Movie Fragment Random Access Offset Box: Provides the size of the enclosing mfra box. This box can be used by readers
* to quickly locate the mfra box by searching from the end of the file.
*/
export declare const mfro: () => Box;
/** VTT Empty Cue Box */
export declare const vtte: () => Box;
/** VTT Cue Box */
export declare const vttc: (payload: string, timestamp: number | null, identifier: string | null, settings: string | null, sourceId: number | null) => Box;
/** VTT Additional Text Box */
export declare const vtta: (notes: string) => Box;
export {};
//# sourceMappingURL=isobmff-boxes.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"isobmff-boxes.d.ts","sourceRoot":"","sources":["../../../../src/isobmff/isobmff-boxes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA4BH,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAIN,qBAAqB,EACrB,YAAY,EACZ,wBAAwB,EACxB,gBAAgB,EAChB,qBAAqB,EAErB,MAAM,iBAAiB,CAAC;AAIzB,qBAAa,gBAAgB;IAUhB,OAAO,CAAC,MAAM;IAT1B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAoC;IAEtD;;;OAGG;IACH,OAAO,uBAA8B;gBAEjB,MAAM,EAAE,MAAM;IAElC,QAAQ,CAAC,KAAK,EAAE,MAAM;IAKtB,QAAQ,CAAC,KAAK,EAAE,MAAM;IAMtB,UAAU,CAAC,IAAI,EAAE,MAAM;IAWvB,QAAQ,CAAC,GAAG,EAAE,GAAG;IAqBjB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM;IAMrC,gBAAgB,CAAC,GAAG,EAAE,GAAG;IAIzB,QAAQ,CAAC,GAAG,EAAE,GAAG;IAUjB,UAAU,CAAC,GAAG,EAAE,GAAG;CAYnB;AAwHD,MAAM,WAAW,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,KAAK,iBAAiB,GAAG,CAAC,MAAM,GAAG,iBAAiB,CAAC,EAAE,CAAC;AAExD,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,EAAE,WAAW,iBAAiB,EAAE,WAAW,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,KAAG,GAI1F,CAAC;AAEH,iFAAiF;AACjF,eAAO,MAAM,OAAO,GACnB,MAAM,MAAM,EACZ,SAAS,MAAM,EACf,OAAO,MAAM,EACb,WAAW,iBAAiB,EAC5B,WAAW,GAAG,EAAE,QAKhB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,SAAS;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;CACpB,QAmCA,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,IAAI,GAAI,kBAAkB,OAAO,KAAG,GAAsD,CAAC;AAExG,iFAAiF;AACjF,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,GAA+B,CAAC;AAEpE;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,YAAY,QAKtC,CAAC;AAEH,iHAAiH;AACjH,eAAO,MAAM,IAAI,GAChB,cAAc,MAAM,EACpB,YAAY,gBAAgB,EAAE,QA6B9B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,EAAE,cAAc,MAAM,QAcrE,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,IAAI,GAChB,WAAW,gBAAgB,EAC3B,cAAc,MAAM,QAuCpB,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,EAAE,cAAc,MAAM,QAIpE,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,IAAI,GAChB,WAAW,gBAAgB,EAC3B,cAAc,MAAM,QAmBpB,CAAC;AAcF,6BAA6B;AAC7B,eAAO,MAAM,IAAI,GAChB,kBAAkB,OAAO,EACzB,aAAa,MAAM,EACnB,MAAM,MAAM,EACZ,qBAAyB,QAQxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAI9C,CAAC;AAEH,gGAAgG;AAChG,eAAO,MAAM,IAAI,WAKf,CAAC;AAEH,yGAAyG;AACzG,eAAO,MAAM,IAAI,WAGf,CAAC;AAEH,6BAA6B;AAC7B,eAAO,MAAM,IAAI,WAA8B,CAAC;AAQhD;;;GAGG;AACH,eAAO,MAAM,IAAI,WAEf,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,IAAI,WAIf,CAAC;AAEH,eAAO,MAAM,GAAG,WAA8B,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAc/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QA8B/C,CAAC;AAEF,yGAAyG;AACzG,eAAO,MAAM,sBAAsB,GAClC,iBAAiB,MAAM,EACvB,WAAW,qBAAqB,QAmB/B,CAAC;AAEH,sEAAsE;AACtE,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAMnD,CAAC;AAEH,6EAA6E;AAC7E,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAGnD,CAAC;AAEH,8EAA8E;AAC9E,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAGnD,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,eA2CpD,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAEpD,CAAC;AAEF,yGAAyG;AACzG,eAAO,MAAM,sBAAsB,GAClC,iBAAiB,MAAM,EACvB,WAAW,qBAAqB,QAqDhC,CAAC;AAEF,+CAA+C;AAC/C,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAqDpD,CAAC;AAEF,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAMpD,CAAC;AAEF,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAIpD,CAAC;AAGF,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAMpD,CAAC;AAEF,yBAAyB;AACzB,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QAuCpD,CAAC;AAEF,yBAAyB;AACzB,eAAO,MAAM,IAAI,GAAI,WAAW,qBAAqB,QASpD,CAAC;AAsFF,eAAO,MAAM,yBAAyB,GACrC,iBAAiB,MAAM,EACvB,WAAW,wBAAwB,QAMlC,CAAC;AAEH,eAAO,MAAM,IAAI,GAAI,WAAW,wBAAwB,QAEtD,CAAC;AAEH,eAAO,MAAM,IAAI,GAAI,YAAY,UAAU,QAEzC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAQ/C,CAAC;AAEF,iHAAiH;AACjH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,eAQ/C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAS/C,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAgB/C,CAAC;AAEF,wHAAwH;AACxH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAa/C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAQ/C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,eA2C/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,YAAY,gBAAgB,EAAE,QAElD,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAQ/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,gBAAgB,MAAM,EAAE,YAAY,gBAAgB,EAAE,QAK1E,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,IAAI,GAAI,gBAAgB,MAAM,QAI1C,CAAC;AAqBF,yBAAyB;AACzB,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAM/C,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAuB/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAM/C,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,QAyC/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,YAAY,gBAAgB,EAAE,QAKlD,CAAC;AAEF,6GAA6G;AAC7G,eAAO,MAAM,IAAI,GAAI,WAAW,gBAAgB,EAAE,YAAY,MAAM,QAenE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,IAAI,WAMhB,CAAC;AAEF,wBAAwB;AACxB,eAAO,MAAM,IAAI,WAAoB,CAAC;AAEtC,kBAAkB;AAClB,eAAO,MAAM,IAAI,GAChB,SAAS,MAAM,EACf,WAAW,MAAM,GAAG,IAAI,EACxB,YAAY,MAAM,GAAG,IAAI,EACzB,UAAU,MAAM,GAAG,IAAI,EACvB,UAAU,MAAM,GAAG,IAAI,QAOtB,CAAC;AAEH,8BAA8B;AAC9B,eAAO,MAAM,IAAI,GAAI,OAAO,MAAM,QAAgD,CAAC"}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,189 @@
/*!
* 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 { AacCodecInfo, AudioCodec, VideoCodec } from '../codec.js';
import { Av1CodecInfo, AvcDecoderConfigurationRecord, HevcDecoderConfigurationRecord, Vp9CodecInfo } from '../codec-data.js';
import { Demuxer } from '../demuxer.js';
import { Input } from '../input.js';
import { InputTrack } from '../input-track.js';
import { Rotation } from '../misc.js';
import { FileSlice, Reader } from '../reader.js';
import { MetadataTags, TrackDisposition } from '../metadata.js';
type InternalTrack = {
id: number;
demuxer: IsobmffDemuxer;
inputTrack: InputTrack | null;
disposition: TrackDisposition;
timescale: number;
durationInMovieTimescale: number;
durationInMediaTimescale: number;
rotation: Rotation;
internalCodecId: string | null;
name: string | null;
languageCode: string;
sampleTableByteOffset: number;
sampleTable: SampleTable | null;
fragmentLookupTable: FragmentLookupTableEntry[];
currentFragmentState: FragmentTrackState | null;
/**
* List of all encountered fragment offsets alongside their timestamps. This list never gets truncated, but memory
* consumption should be negligible.
*/
fragmentPositionCache: {
moofOffset: number;
startTimestamp: number;
endTimestamp: number;
}[];
/** The segment durations of all edit list entries leading up to the main one (from which the offset is taken.) */
editListPreviousSegmentDurations: number;
/** The media time offset of the main edit list entry (with media time !== -1) */
editListOffset: number;
} & ({
info: null;
} | {
info: {
type: 'video';
width: number;
height: number;
codec: VideoCodec | null;
codecDescription: Uint8Array | null;
colorSpace: VideoColorSpaceInit | null;
avcType: 1 | 3 | null;
avcCodecInfo: AvcDecoderConfigurationRecord | null;
hevcCodecInfo: HevcDecoderConfigurationRecord | null;
vp9CodecInfo: Vp9CodecInfo | null;
av1CodecInfo: Av1CodecInfo | null;
};
} | {
info: {
type: 'audio';
numberOfChannels: number;
sampleRate: number;
codec: AudioCodec | null;
codecDescription: Uint8Array | null;
aacCodecInfo: AacCodecInfo | null;
};
});
type SampleTable = {
sampleTimingEntries: SampleTimingEntry[];
sampleCompositionTimeOffsets: SampleCompositionTimeOffsetEntry[];
sampleSizes: number[];
keySampleIndices: number[] | null;
chunkOffsets: number[];
sampleToChunk: SampleToChunkEntry[];
presentationTimestamps: {
presentationTimestamp: number;
sampleIndex: number;
}[] | null;
/**
* Provides a fast map from sample index to index in the sorted presentation timestamps array - so, a fast map from
* decode order to presentation order.
*/
presentationTimestampIndexMap: number[] | null;
};
type SampleTimingEntry = {
startIndex: number;
startDecodeTimestamp: number;
count: number;
delta: number;
};
type SampleCompositionTimeOffsetEntry = {
startIndex: number;
count: number;
offset: number;
};
type SampleToChunkEntry = {
startSampleIndex: number;
startChunkIndex: number;
samplesPerChunk: number;
sampleDescriptionIndex: number;
};
type FragmentTrackDefaults = {
trackId: number;
defaultSampleDescriptionIndex: number;
defaultSampleDuration: number;
defaultSampleSize: number;
defaultSampleFlags: number;
};
type FragmentLookupTableEntry = {
timestamp: number;
moofOffset: number;
};
type FragmentTrackState = {
baseDataOffset: number;
sampleDescriptionIndex: number | null;
defaultSampleDuration: number | null;
defaultSampleSize: number | null;
defaultSampleFlags: number | null;
startTimestamp: number | null;
};
type FragmentTrackData = {
track: InternalTrack;
startTimestamp: number;
endTimestamp: number;
firstKeyFrameTimestamp: number | null;
samples: FragmentTrackSample[];
presentationTimestamps: {
presentationTimestamp: number;
sampleIndex: number;
}[];
startTimestampIsFinal: boolean;
};
type FragmentTrackSample = {
presentationTimestamp: number;
duration: number;
byteOffset: number;
byteSize: number;
isKeyFrame: boolean;
};
type Fragment = {
moofOffset: number;
moofSize: number;
implicitBaseDataOffset: number;
trackData: Map<InternalTrack['id'], FragmentTrackData>;
};
export declare class IsobmffDemuxer extends Demuxer {
reader: Reader;
moovSlice: FileSlice | null;
currentTrack: InternalTrack | null;
tracks: InternalTrack[];
metadataPromise: Promise<void> | null;
movieTimescale: number;
movieDurationInTimescale: number;
isQuickTime: boolean;
metadataTags: MetadataTags;
currentMetadataKeys: Map<number, string> | null;
isFragmented: boolean;
fragmentTrackDefaults: FragmentTrackDefaults[];
currentFragment: Fragment | null;
/**
* Caches the last fragment that was read. Based on the assumption that there will be multiple reads to the
* same fragment in quick succession.
*/
lastReadFragment: Fragment | null;
constructor(input: Input);
computeDuration(): Promise<number>;
getTracks(): Promise<InputTrack[]>;
getMimeType(): Promise<string>;
getMetadataTags(): Promise<MetadataTags>;
readMetadata(): Promise<void>;
getSampleTableForTrack(internalTrack: InternalTrack): SampleTable;
readFragment(startPos: number): Promise<Fragment>;
readContiguousBoxes(slice: FileSlice): void;
iterateContiguousBoxes(slice: FileSlice): Generator<{
boxInfo: {
name: string;
totalSize: number;
headerSize: number;
contentSize: number;
};
slice: FileSlice;
}, void, unknown>;
traverseBox(slice: FileSlice): boolean;
}
export {};
//# sourceMappingURL=isobmff-demuxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"isobmff-demuxer.d.ts","sourceRoot":"","sources":["../../../../src/isobmff/isobmff-demuxer.ts"],"names":[],"mappings":"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"}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
/*!
* 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 declare const buildIsobmffMimeType: (info: {
isQuickTime: boolean;
hasVideo: boolean;
hasAudio: boolean;
codecStrings: string[];
}) => string;
//# sourceMappingURL=isobmff-misc.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"isobmff-misc.d.ts","sourceRoot":"","sources":["../../../../src/isobmff/isobmff-misc.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,oBAAoB,GAAI,MAAM;IAC1C,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;CACvB,WAeA,CAAC"}
@@ -0,0 +1,20 @@
/*!
* 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 const buildIsobmffMimeType = (info) => {
const base = info.hasVideo
? 'video/'
: info.hasAudio
? 'audio/'
: 'application/';
let string = base + (info.isQuickTime ? 'quicktime' : 'mp4');
if (info.codecStrings.length > 0) {
const uniqueCodecMimeTypes = [...new Set(info.codecStrings)];
string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`;
}
return string;
};
@@ -0,0 +1,155 @@
/*!
* 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 { Muxer } from '../muxer.js';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output.js';
import { IsobmffOutputFormat } from '../output-format.js';
import { SubtitleConfig, SubtitleCue, SubtitleMetadata } from '../subtitles.js';
import { EncodedPacket, PacketType } from '../packet.js';
export declare const GLOBAL_TIMESCALE = 1000;
export type Sample = {
timestamp: number;
decodeTimestamp: number;
duration: number;
data: Uint8Array | null;
size: number;
type: PacketType;
timescaleUnitsToNextSample: number;
};
type Chunk = {
/** The lowest presentation timestamp in this chunk */
startTimestamp: number;
samples: Sample[];
offset: number | null;
moofOffset: number | null;
};
export type IsobmffTrackData = {
muxer: IsobmffMuxer;
timescale: number;
samples: Sample[];
sampleQueue: Sample[];
timestampProcessingQueue: Sample[];
timeToSampleTable: {
sampleCount: number;
sampleDelta: number;
}[];
compositionTimeOffsetTable: {
sampleCount: number;
sampleCompositionTimeOffset: number;
}[];
lastTimescaleUnits: number | null;
lastSample: Sample | null;
finalizedChunks: Chunk[];
currentChunk: Chunk | null;
compactlyCodedChunkTable: {
firstChunk: number;
samplesPerChunk: number;
}[];
} & ({
track: OutputVideoTrack;
type: 'video';
info: {
width: number;
height: number;
decoderConfig: VideoDecoderConfig;
/**
* The "Annex B transformation" involves converting the raw packet data from Annex B to
* "MP4" (length-prefixed) format.
* https://stackoverflow.com/questions/24884827
*/
requiresAnnexBTransformation: boolean;
};
} | {
track: OutputAudioTrack;
type: 'audio';
info: {
numberOfChannels: number;
sampleRate: number;
decoderConfig: AudioDecoderConfig;
/**
* The "PCM transformation" is making every sample in the sample table be exactly one PCM audio sample long.
* 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;
type: 'subtitle';
info: {
config: SubtitleConfig;
};
lastCueEndTimestamp: number;
cueQueue: SubtitleCue[];
nextSourceId: number;
cueToSourceId: WeakMap<SubtitleCue, number>;
});
export type IsobmffVideoTrackData = IsobmffTrackData & {
type: 'video';
};
export type IsobmffAudioTrackData = IsobmffTrackData & {
type: 'audio';
};
export type IsobmffSubtitleTrackData = IsobmffTrackData & {
type: 'subtitle';
};
export type IsobmffMetadata = {
name?: string;
};
export declare const getTrackMetadata: (trackData: IsobmffTrackData) => IsobmffMetadata;
export declare const intoTimescale: (timeInSeconds: number, timescale: number, round?: boolean) => number;
export declare class IsobmffMuxer extends Muxer {
format: IsobmffOutputFormat;
private writer;
private boxWriter;
private fastStart;
isFragmented: boolean;
isQuickTime: boolean;
private auxTarget;
private auxWriter;
private auxBoxWriter;
private mdat;
private ftypSize;
trackDatas: IsobmffTrackData[];
private allTracksKnown;
creationTime: number;
private finalizedChunks;
private nextFragmentNumber;
private maxWrittenTimestamp;
private minimumFragmentDuration;
constructor(output: Output, format: IsobmffOutputFormat);
start(): Promise<void>;
private allTracksAreKnown;
getMimeType(): Promise<string>;
private getVideoTrackData;
private getAudioTrackData;
private getSubtitleTrackData;
addEncodedVideoPacket(track: OutputVideoTrack, packet: EncodedPacket, meta?: EncodedVideoChunkMetadata): Promise<void>;
addEncodedAudioPacket(track: OutputAudioTrack, packet: EncodedPacket, meta?: EncodedAudioChunkMetadata): Promise<void>;
private maybePadWithSilence;
addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata): Promise<void>;
private processWebVTTCues;
private createSampleForTrack;
private processTimestamps;
private registerSample;
private addSampleToTrack;
private finalizeCurrentChunk;
private interleaveSamples;
private finalizeFragment;
private registerSampleFastStartReserve;
private computeSampleTableSizeUpperBound;
onTrackClose(track: OutputTrack): Promise<void>;
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
finalize(): Promise<void>;
}
export {};
//# sourceMappingURL=isobmff-muxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"isobmff-muxer.d.ts","sourceRoot":"","sources":["../../../../src/isobmff/isobmff-muxer.ts"],"names":[],"mappings":"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"}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
/*!
* 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 { RichImageData } from '../metadata.js';
import { FileSlice } from '../reader.js';
export declare const MIN_BOX_HEADER_SIZE = 8;
export declare const MAX_BOX_HEADER_SIZE = 16;
export declare const readBoxHeader: (slice: FileSlice) => {
name: string;
totalSize: number;
headerSize: number;
contentSize: number;
} | null;
export declare const readFixed_16_16: (slice: FileSlice) => number;
export declare const readFixed_2_30: (slice: FileSlice) => number;
export declare const readIsomVariableInteger: (slice: FileSlice) => number;
export declare const readMetadataStringShort: (slice: FileSlice) => string;
export declare const readDataBox: (slice: FileSlice) => string | Uint8Array<ArrayBufferLike> | RichImageData | null;
//# sourceMappingURL=isobmff-reader.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"isobmff-reader.d.ts","sourceRoot":"","sources":["../../../../src/isobmff/isobmff-reader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,SAAS,EAA4E,MAAM,WAAW,CAAC;AAEhH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,eAAO,MAAM,aAAa,GAAI,OAAO,SAAS;;;;;QAiB7C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,SAAS,WAE/C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,OAAO,SAAS,WAE9C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,OAAO,SAAS,WAcvD,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,OAAO,SAAS,WAMvD,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,SAAS,gEAwB3C,CAAC"}
@@ -0,0 +1,72 @@
/*!
* 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 { RichImageData } from '../metadata.js';
import { textDecoder } from '../misc.js';
import { readAscii, readBytes, readI32Be, readU16Be, readU32Be, readU64Be, readU8 } from '../reader.js';
export const MIN_BOX_HEADER_SIZE = 8;
export const MAX_BOX_HEADER_SIZE = 16;
export const readBoxHeader = (slice) => {
let totalSize = readU32Be(slice);
const name = readAscii(slice, 4);
let headerSize = 8;
const hasLargeSize = totalSize === 1;
if (hasLargeSize) {
totalSize = readU64Be(slice);
headerSize = 16;
}
const contentSize = totalSize - headerSize;
if (contentSize < 0) {
return null; // Hardly a box is it
}
return { name, totalSize, headerSize, contentSize };
};
export const readFixed_16_16 = (slice) => {
return readI32Be(slice) / 0x10000;
};
export const readFixed_2_30 = (slice) => {
return readI32Be(slice) / 0x40000000;
};
export const readIsomVariableInteger = (slice) => {
let result = 0;
for (let i = 0; i < 4; i++) {
result <<= 7;
const nextByte = readU8(slice);
result |= nextByte & 0x7f;
if ((nextByte & 0x80) === 0) {
break;
}
}
return result;
};
export const readMetadataStringShort = (slice) => {
let stringLength = readU16Be(slice);
slice.skip(2); // Language
stringLength = Math.min(stringLength, slice.remainingLength);
return textDecoder.decode(readBytes(slice, stringLength));
};
export const readDataBox = (slice) => {
const header = readBoxHeader(slice);
if (!header || header.name !== 'data') {
return null;
}
if (slice.remainingLength < 8) {
// Box is too small
return null;
}
const typeIndicator = readU32Be(slice);
slice.skip(4); // Locale indicator
const data = readBytes(slice, header.contentSize - 8);
switch (typeIndicator) {
case 1: return textDecoder.decode(data); // UTF-8
case 2: return new TextDecoder('utf-16be').decode(data); // UTF-16-BE
case 13: return new RichImageData(data, 'image/jpeg');
case 14: return new RichImageData(data, 'image/png');
case 27: return new RichImageData(data, 'image/bmp');
default: return data;
}
};
@@ -0,0 +1,198 @@
/*!
* 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 { MediaCodec } from '../codec.js';
import { FileSlice, Reader } from '../reader.js';
import { Writer } from '../writer.js';
export interface EBMLElement {
id: number;
size?: number;
data: number | bigint | string | Uint8Array | EBMLFloat32 | EBMLFloat64 | EBMLSignedInt | EBMLUnicodeString | (EBML | null)[];
}
export type EBML = EBMLElement | Uint8Array | (EBML | null)[];
/** Wrapper around a number to be able to differentiate it in the writer. */
export declare class EBMLFloat32 {
value: number;
constructor(value: number);
}
/** Wrapper around a number to be able to differentiate it in the writer. */
export declare class EBMLFloat64 {
value: number;
constructor(value: number);
}
/** Wrapper around a number to be able to differentiate it in the writer. */
export declare class EBMLSignedInt {
value: number;
constructor(value: number);
}
export declare class EBMLUnicodeString {
value: string;
constructor(value: string);
}
/** Defines some of the EBML IDs used by Matroska files. */
export declare enum EBMLId {
EBML = 440786851,
EBMLVersion = 17030,
EBMLReadVersion = 17143,
EBMLMaxIDLength = 17138,
EBMLMaxSizeLength = 17139,
DocType = 17026,
DocTypeVersion = 17031,
DocTypeReadVersion = 17029,
Void = 236,
Segment = 408125543,
SeekHead = 290298740,
Seek = 19899,
SeekID = 21419,
SeekPosition = 21420,
Duration = 17545,
Info = 357149030,
TimestampScale = 2807729,
MuxingApp = 19840,
WritingApp = 22337,
Tracks = 374648427,
TrackEntry = 174,
TrackNumber = 215,
TrackUID = 29637,
TrackType = 131,
FlagEnabled = 185,
FlagDefault = 136,
FlagForced = 21930,
FlagOriginal = 21934,
FlagHearingImpaired = 21931,
FlagVisualImpaired = 21932,
FlagCommentary = 21935,
FlagLacing = 156,
Name = 21358,
Language = 2274716,
LanguageBCP47 = 2274717,
CodecID = 134,
CodecPrivate = 25506,
CodecDelay = 22186,
SeekPreRoll = 22203,
DefaultDuration = 2352003,
Video = 224,
PixelWidth = 176,
PixelHeight = 186,
AlphaMode = 21440,
Audio = 225,
SamplingFrequency = 181,
Channels = 159,
BitDepth = 25188,
SimpleBlock = 163,
BlockGroup = 160,
Block = 161,
BlockAdditions = 30113,
BlockMore = 166,
BlockAdditional = 165,
BlockAddID = 238,
BlockDuration = 155,
ReferenceBlock = 251,
Cluster = 524531317,
Timestamp = 231,
Cues = 475249515,
CuePoint = 187,
CueTime = 179,
CueTrackPositions = 183,
CueTrack = 247,
CueClusterPosition = 241,
Colour = 21936,
MatrixCoefficients = 21937,
TransferCharacteristics = 21946,
Primaries = 21947,
Range = 21945,
Projection = 30320,
ProjectionType = 30321,
ProjectionPoseRoll = 30325,
Attachments = 423732329,
AttachedFile = 24999,
FileDescription = 18046,
FileName = 18030,
FileMediaType = 18016,
FileData = 18012,
FileUID = 18094,
Chapters = 272869232,
Tags = 307544935,
Tag = 29555,
Targets = 25536,
TargetTypeValue = 26826,
TargetType = 25546,
TagTrackUID = 25541,
TagEditionUID = 25545,
TagChapterUID = 25540,
TagAttachmentUID = 25542,
SimpleTag = 26568,
TagName = 17827,
TagLanguage = 17530,
TagString = 17543,
TagBinary = 17541,
ContentEncodings = 28032,
ContentEncoding = 25152,
ContentEncodingOrder = 20529,
ContentEncodingScope = 20530,
ContentCompression = 20532,
ContentCompAlgo = 16980,
ContentCompSettings = 16981,
ContentEncryption = 20533
}
export declare const LEVEL_0_EBML_IDS: EBMLId[];
export declare const LEVEL_1_EBML_IDS: EBMLId[];
export declare const LEVEL_0_AND_1_EBML_IDS: EBMLId[];
export declare const measureUnsignedInt: (value: number) => 1 | 5 | 6 | 2 | 4 | 3;
export declare const measureUnsignedBigInt: (value: bigint) => 8 | 7 | 1 | 5 | 6 | 2 | 4 | 3;
export declare const measureSignedInt: (value: number) => 1 | 5 | 6 | 2 | 4 | 3;
export declare const measureVarInt: (value: number) => 1 | 5 | 6 | 2 | 4 | 3;
export declare class EBMLWriter {
private writer;
helper: Uint8Array<ArrayBuffer>;
helperView: DataView<ArrayBuffer>;
/**
* Stores the position from the start of the file to where EBML elements have been written. This is used to
* rewrite/edit elements that were already added before, and to measure sizes of things.
*/
offsets: WeakMap<EBML, number>;
/** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
dataOffsets: WeakMap<EBML, number>;
constructor(writer: Writer);
writeByte(value: number): void;
writeFloat32(value: number): void;
writeFloat64(value: number): void;
writeUnsignedInt(value: number, width?: number): void;
writeUnsignedBigInt(value: bigint, width?: number): void;
writeSignedInt(value: number, width?: number): void;
writeVarInt(value: number, width?: number): void;
writeAsciiString(str: string): void;
writeEBML(data: EBML | null): void;
}
export declare const MAX_VAR_INT_SIZE = 8;
export declare const MIN_HEADER_SIZE = 2;
export declare const MAX_HEADER_SIZE: number;
export declare const readVarIntSize: (slice: FileSlice) => number | null;
export declare const readVarInt: (slice: FileSlice) => number | null;
export declare const readUnsignedInt: (slice: FileSlice, width: number) => number;
export declare const readUnsignedBigInt: (slice: FileSlice, width: number) => bigint;
export declare const readSignedInt: (slice: FileSlice, width: number) => number;
export declare const readElementId: (slice: FileSlice) => number | null;
/** Returns `undefined` to indicate the EBML undefined size. Returns `null` if the size couldn't be read. */
export declare const readElementSize: (slice: FileSlice) => number | undefined | null;
export declare const readElementHeader: (slice: FileSlice) => {
id: number;
size: number | undefined;
} | null;
export declare const readAsciiString: (slice: FileSlice, length: number) => string;
export declare const readUnicodeString: (slice: FileSlice, length: number) => string;
export declare const readFloat: (slice: FileSlice, width: number) => number;
/** Returns the byte offset in the file of the next element with a matching ID. */
export declare const searchForNextElementId: (reader: Reader, startPos: number, ids: EBMLId[], until: number | null) => Promise<{
pos: number;
found: boolean;
}>;
/** Searches for the next occurrence of an element ID using a naive byte-wise search. */
export declare const resync: (reader: Reader, startPos: number, ids: EBMLId[], until: number) => Promise<number | null>;
export declare const CODEC_STRING_MAP: Partial<Record<MediaCodec, string>>;
export declare function assertDefinedSize(size: number | undefined): asserts size is number;
//# sourceMappingURL=ebml.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"ebml.d.ts","sourceRoot":"","sources":["../../../../src/matroska/ebml.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,679 @@
/*!
* 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, assertNever, textDecoder, textEncoder } from '../misc.js';
import { readBytes, readF32Be, readF64Be, readU8 } from '../reader.js';
/** Wrapper around a number to be able to differentiate it in the writer. */
export class EBMLFloat32 {
constructor(value) {
this.value = value;
}
}
/** Wrapper around a number to be able to differentiate it in the writer. */
export class EBMLFloat64 {
constructor(value) {
this.value = value;
}
}
/** Wrapper around a number to be able to differentiate it in the writer. */
export class EBMLSignedInt {
constructor(value) {
this.value = value;
}
}
export class EBMLUnicodeString {
constructor(value) {
this.value = value;
}
}
/** Defines some of the EBML IDs used by Matroska files. */
export var EBMLId;
(function (EBMLId) {
EBMLId[EBMLId["EBML"] = 440786851] = "EBML";
EBMLId[EBMLId["EBMLVersion"] = 17030] = "EBMLVersion";
EBMLId[EBMLId["EBMLReadVersion"] = 17143] = "EBMLReadVersion";
EBMLId[EBMLId["EBMLMaxIDLength"] = 17138] = "EBMLMaxIDLength";
EBMLId[EBMLId["EBMLMaxSizeLength"] = 17139] = "EBMLMaxSizeLength";
EBMLId[EBMLId["DocType"] = 17026] = "DocType";
EBMLId[EBMLId["DocTypeVersion"] = 17031] = "DocTypeVersion";
EBMLId[EBMLId["DocTypeReadVersion"] = 17029] = "DocTypeReadVersion";
EBMLId[EBMLId["Void"] = 236] = "Void";
EBMLId[EBMLId["Segment"] = 408125543] = "Segment";
EBMLId[EBMLId["SeekHead"] = 290298740] = "SeekHead";
EBMLId[EBMLId["Seek"] = 19899] = "Seek";
EBMLId[EBMLId["SeekID"] = 21419] = "SeekID";
EBMLId[EBMLId["SeekPosition"] = 21420] = "SeekPosition";
EBMLId[EBMLId["Duration"] = 17545] = "Duration";
EBMLId[EBMLId["Info"] = 357149030] = "Info";
EBMLId[EBMLId["TimestampScale"] = 2807729] = "TimestampScale";
EBMLId[EBMLId["MuxingApp"] = 19840] = "MuxingApp";
EBMLId[EBMLId["WritingApp"] = 22337] = "WritingApp";
EBMLId[EBMLId["Tracks"] = 374648427] = "Tracks";
EBMLId[EBMLId["TrackEntry"] = 174] = "TrackEntry";
EBMLId[EBMLId["TrackNumber"] = 215] = "TrackNumber";
EBMLId[EBMLId["TrackUID"] = 29637] = "TrackUID";
EBMLId[EBMLId["TrackType"] = 131] = "TrackType";
EBMLId[EBMLId["FlagEnabled"] = 185] = "FlagEnabled";
EBMLId[EBMLId["FlagDefault"] = 136] = "FlagDefault";
EBMLId[EBMLId["FlagForced"] = 21930] = "FlagForced";
EBMLId[EBMLId["FlagOriginal"] = 21934] = "FlagOriginal";
EBMLId[EBMLId["FlagHearingImpaired"] = 21931] = "FlagHearingImpaired";
EBMLId[EBMLId["FlagVisualImpaired"] = 21932] = "FlagVisualImpaired";
EBMLId[EBMLId["FlagCommentary"] = 21935] = "FlagCommentary";
EBMLId[EBMLId["FlagLacing"] = 156] = "FlagLacing";
EBMLId[EBMLId["Name"] = 21358] = "Name";
EBMLId[EBMLId["Language"] = 2274716] = "Language";
EBMLId[EBMLId["LanguageBCP47"] = 2274717] = "LanguageBCP47";
EBMLId[EBMLId["CodecID"] = 134] = "CodecID";
EBMLId[EBMLId["CodecPrivate"] = 25506] = "CodecPrivate";
EBMLId[EBMLId["CodecDelay"] = 22186] = "CodecDelay";
EBMLId[EBMLId["SeekPreRoll"] = 22203] = "SeekPreRoll";
EBMLId[EBMLId["DefaultDuration"] = 2352003] = "DefaultDuration";
EBMLId[EBMLId["Video"] = 224] = "Video";
EBMLId[EBMLId["PixelWidth"] = 176] = "PixelWidth";
EBMLId[EBMLId["PixelHeight"] = 186] = "PixelHeight";
EBMLId[EBMLId["AlphaMode"] = 21440] = "AlphaMode";
EBMLId[EBMLId["Audio"] = 225] = "Audio";
EBMLId[EBMLId["SamplingFrequency"] = 181] = "SamplingFrequency";
EBMLId[EBMLId["Channels"] = 159] = "Channels";
EBMLId[EBMLId["BitDepth"] = 25188] = "BitDepth";
EBMLId[EBMLId["SimpleBlock"] = 163] = "SimpleBlock";
EBMLId[EBMLId["BlockGroup"] = 160] = "BlockGroup";
EBMLId[EBMLId["Block"] = 161] = "Block";
EBMLId[EBMLId["BlockAdditions"] = 30113] = "BlockAdditions";
EBMLId[EBMLId["BlockMore"] = 166] = "BlockMore";
EBMLId[EBMLId["BlockAdditional"] = 165] = "BlockAdditional";
EBMLId[EBMLId["BlockAddID"] = 238] = "BlockAddID";
EBMLId[EBMLId["BlockDuration"] = 155] = "BlockDuration";
EBMLId[EBMLId["ReferenceBlock"] = 251] = "ReferenceBlock";
EBMLId[EBMLId["Cluster"] = 524531317] = "Cluster";
EBMLId[EBMLId["Timestamp"] = 231] = "Timestamp";
EBMLId[EBMLId["Cues"] = 475249515] = "Cues";
EBMLId[EBMLId["CuePoint"] = 187] = "CuePoint";
EBMLId[EBMLId["CueTime"] = 179] = "CueTime";
EBMLId[EBMLId["CueTrackPositions"] = 183] = "CueTrackPositions";
EBMLId[EBMLId["CueTrack"] = 247] = "CueTrack";
EBMLId[EBMLId["CueClusterPosition"] = 241] = "CueClusterPosition";
EBMLId[EBMLId["Colour"] = 21936] = "Colour";
EBMLId[EBMLId["MatrixCoefficients"] = 21937] = "MatrixCoefficients";
EBMLId[EBMLId["TransferCharacteristics"] = 21946] = "TransferCharacteristics";
EBMLId[EBMLId["Primaries"] = 21947] = "Primaries";
EBMLId[EBMLId["Range"] = 21945] = "Range";
EBMLId[EBMLId["Projection"] = 30320] = "Projection";
EBMLId[EBMLId["ProjectionType"] = 30321] = "ProjectionType";
EBMLId[EBMLId["ProjectionPoseRoll"] = 30325] = "ProjectionPoseRoll";
EBMLId[EBMLId["Attachments"] = 423732329] = "Attachments";
EBMLId[EBMLId["AttachedFile"] = 24999] = "AttachedFile";
EBMLId[EBMLId["FileDescription"] = 18046] = "FileDescription";
EBMLId[EBMLId["FileName"] = 18030] = "FileName";
EBMLId[EBMLId["FileMediaType"] = 18016] = "FileMediaType";
EBMLId[EBMLId["FileData"] = 18012] = "FileData";
EBMLId[EBMLId["FileUID"] = 18094] = "FileUID";
EBMLId[EBMLId["Chapters"] = 272869232] = "Chapters";
EBMLId[EBMLId["Tags"] = 307544935] = "Tags";
EBMLId[EBMLId["Tag"] = 29555] = "Tag";
EBMLId[EBMLId["Targets"] = 25536] = "Targets";
EBMLId[EBMLId["TargetTypeValue"] = 26826] = "TargetTypeValue";
EBMLId[EBMLId["TargetType"] = 25546] = "TargetType";
EBMLId[EBMLId["TagTrackUID"] = 25541] = "TagTrackUID";
EBMLId[EBMLId["TagEditionUID"] = 25545] = "TagEditionUID";
EBMLId[EBMLId["TagChapterUID"] = 25540] = "TagChapterUID";
EBMLId[EBMLId["TagAttachmentUID"] = 25542] = "TagAttachmentUID";
EBMLId[EBMLId["SimpleTag"] = 26568] = "SimpleTag";
EBMLId[EBMLId["TagName"] = 17827] = "TagName";
EBMLId[EBMLId["TagLanguage"] = 17530] = "TagLanguage";
EBMLId[EBMLId["TagString"] = 17543] = "TagString";
EBMLId[EBMLId["TagBinary"] = 17541] = "TagBinary";
EBMLId[EBMLId["ContentEncodings"] = 28032] = "ContentEncodings";
EBMLId[EBMLId["ContentEncoding"] = 25152] = "ContentEncoding";
EBMLId[EBMLId["ContentEncodingOrder"] = 20529] = "ContentEncodingOrder";
EBMLId[EBMLId["ContentEncodingScope"] = 20530] = "ContentEncodingScope";
EBMLId[EBMLId["ContentCompression"] = 20532] = "ContentCompression";
EBMLId[EBMLId["ContentCompAlgo"] = 16980] = "ContentCompAlgo";
EBMLId[EBMLId["ContentCompSettings"] = 16981] = "ContentCompSettings";
EBMLId[EBMLId["ContentEncryption"] = 20533] = "ContentEncryption";
})(EBMLId || (EBMLId = {}));
export const LEVEL_0_EBML_IDS = [
EBMLId.EBML,
EBMLId.Segment,
];
// All the stuff that can appear in a segment, basically
export const LEVEL_1_EBML_IDS = [
EBMLId.SeekHead,
EBMLId.Info,
EBMLId.Cluster,
EBMLId.Tracks,
EBMLId.Cues,
EBMLId.Attachments,
EBMLId.Chapters,
EBMLId.Tags,
];
export const LEVEL_0_AND_1_EBML_IDS = [
...LEVEL_0_EBML_IDS,
...LEVEL_1_EBML_IDS,
];
export const measureUnsignedInt = (value) => {
if (value < (1 << 8)) {
return 1;
}
else if (value < (1 << 16)) {
return 2;
}
else if (value < (1 << 24)) {
return 3;
}
else if (value < 2 ** 32) {
return 4;
}
else if (value < 2 ** 40) {
return 5;
}
else {
return 6;
}
};
export const measureUnsignedBigInt = (value) => {
if (value < (1n << 8n)) {
return 1;
}
else if (value < (1n << 16n)) {
return 2;
}
else if (value < (1n << 24n)) {
return 3;
}
else if (value < (1n << 32n)) {
return 4;
}
else if (value < (1n << 40n)) {
return 5;
}
else if (value < (1n << 48n)) {
return 6;
}
else if (value < (1n << 56n)) {
return 7;
}
else {
return 8;
}
};
export const measureSignedInt = (value) => {
if (value >= -(1 << 6) && value < (1 << 6)) {
return 1;
}
else if (value >= -(1 << 13) && value < (1 << 13)) {
return 2;
}
else if (value >= -(1 << 20) && value < (1 << 20)) {
return 3;
}
else if (value >= -(1 << 27) && value < (1 << 27)) {
return 4;
}
else if (value >= -(2 ** 34) && value < 2 ** 34) {
return 5;
}
else {
return 6;
}
};
export const measureVarInt = (value) => {
if (value < (1 << 7) - 1) {
/** Top bit is set, leaving 7 bits to hold the integer, but we can't store
* 127 because "all bits set to one" is a reserved value. Same thing for the
* other cases below:
*/
return 1;
}
else if (value < (1 << 14) - 1) {
return 2;
}
else if (value < (1 << 21) - 1) {
return 3;
}
else if (value < (1 << 28) - 1) {
return 4;
}
else if (value < 2 ** 35 - 1) {
return 5;
}
else if (value < 2 ** 42 - 1) {
return 6;
}
else {
throw new Error('EBML varint size not supported ' + value);
}
};
export class EBMLWriter {
constructor(writer) {
this.writer = writer;
this.helper = new Uint8Array(8);
this.helperView = new DataView(this.helper.buffer);
/**
* Stores the position from the start of the file to where EBML elements have been written. This is used to
* rewrite/edit elements that were already added before, and to measure sizes of things.
*/
this.offsets = new WeakMap();
/** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
this.dataOffsets = new WeakMap();
}
writeByte(value) {
this.helperView.setUint8(0, value);
this.writer.write(this.helper.subarray(0, 1));
}
writeFloat32(value) {
this.helperView.setFloat32(0, value, false);
this.writer.write(this.helper.subarray(0, 4));
}
writeFloat64(value) {
this.helperView.setFloat64(0, value, false);
this.writer.write(this.helper);
}
writeUnsignedInt(value, width = measureUnsignedInt(value)) {
let pos = 0;
// Each case falls through:
switch (width) {
case 6:
// Need to use division to access >32 bits of floating point var
this.helperView.setUint8(pos++, (value / 2 ** 40) | 0);
// eslint-disable-next-line no-fallthrough
case 5:
this.helperView.setUint8(pos++, (value / 2 ** 32) | 0);
// eslint-disable-next-line no-fallthrough
case 4:
this.helperView.setUint8(pos++, value >> 24);
// eslint-disable-next-line no-fallthrough
case 3:
this.helperView.setUint8(pos++, value >> 16);
// eslint-disable-next-line no-fallthrough
case 2:
this.helperView.setUint8(pos++, value >> 8);
// eslint-disable-next-line no-fallthrough
case 1:
this.helperView.setUint8(pos++, value);
break;
default:
throw new Error('Bad unsigned int size ' + width);
}
this.writer.write(this.helper.subarray(0, pos));
}
writeUnsignedBigInt(value, width = measureUnsignedBigInt(value)) {
let pos = 0;
for (let i = width - 1; i >= 0; i--) {
this.helperView.setUint8(pos++, Number((value >> BigInt(i * 8)) & 0xffn));
}
this.writer.write(this.helper.subarray(0, pos));
}
writeSignedInt(value, width = measureSignedInt(value)) {
if (value < 0) {
// Two's complement stuff
value += 2 ** (width * 8);
}
this.writeUnsignedInt(value, width);
}
writeVarInt(value, width = measureVarInt(value)) {
let pos = 0;
switch (width) {
case 1:
this.helperView.setUint8(pos++, (1 << 7) | value);
break;
case 2:
this.helperView.setUint8(pos++, (1 << 6) | (value >> 8));
this.helperView.setUint8(pos++, value);
break;
case 3:
this.helperView.setUint8(pos++, (1 << 5) | (value >> 16));
this.helperView.setUint8(pos++, value >> 8);
this.helperView.setUint8(pos++, value);
break;
case 4:
this.helperView.setUint8(pos++, (1 << 4) | (value >> 24));
this.helperView.setUint8(pos++, value >> 16);
this.helperView.setUint8(pos++, value >> 8);
this.helperView.setUint8(pos++, value);
break;
case 5:
/**
* JavaScript converts its doubles to 32-bit integers for bitwise
* operations, so we need to do a division by 2^32 instead of a
* right-shift of 32 to retain those top 3 bits
*/
this.helperView.setUint8(pos++, (1 << 3) | ((value / 2 ** 32) & 0x7));
this.helperView.setUint8(pos++, value >> 24);
this.helperView.setUint8(pos++, value >> 16);
this.helperView.setUint8(pos++, value >> 8);
this.helperView.setUint8(pos++, value);
break;
case 6:
this.helperView.setUint8(pos++, (1 << 2) | ((value / 2 ** 40) & 0x3));
this.helperView.setUint8(pos++, (value / 2 ** 32) | 0);
this.helperView.setUint8(pos++, value >> 24);
this.helperView.setUint8(pos++, value >> 16);
this.helperView.setUint8(pos++, value >> 8);
this.helperView.setUint8(pos++, value);
break;
default:
throw new Error('Bad EBML varint size ' + width);
}
this.writer.write(this.helper.subarray(0, pos));
}
writeAsciiString(str) {
this.writer.write(new Uint8Array(str.split('').map(x => x.charCodeAt(0))));
}
writeEBML(data) {
if (data === null)
return;
if (data instanceof Uint8Array) {
this.writer.write(data);
}
else if (Array.isArray(data)) {
for (const elem of data) {
this.writeEBML(elem);
}
}
else {
this.offsets.set(data, this.writer.getPos());
this.writeUnsignedInt(data.id); // ID field
if (Array.isArray(data.data)) {
const sizePos = this.writer.getPos();
const sizeSize = data.size === -1 ? 1 : (data.size ?? 4);
if (data.size === -1) {
// Write the reserved all-one-bits marker for unknown/unbounded size.
this.writeByte(0xff);
}
else {
this.writer.seek(this.writer.getPos() + sizeSize);
}
const startPos = this.writer.getPos();
this.dataOffsets.set(data, startPos);
this.writeEBML(data.data);
if (data.size !== -1) {
const size = this.writer.getPos() - startPos;
const endPos = this.writer.getPos();
this.writer.seek(sizePos);
this.writeVarInt(size, sizeSize);
this.writer.seek(endPos);
}
}
else if (typeof data.data === 'number') {
const size = data.size ?? measureUnsignedInt(data.data);
this.writeVarInt(size);
this.writeUnsignedInt(data.data, size);
}
else if (typeof data.data === 'bigint') {
const size = data.size ?? measureUnsignedBigInt(data.data);
this.writeVarInt(size);
this.writeUnsignedBigInt(data.data, size);
}
else if (typeof data.data === 'string') {
this.writeVarInt(data.data.length);
this.writeAsciiString(data.data);
}
else if (data.data instanceof Uint8Array) {
this.writeVarInt(data.data.byteLength, data.size);
this.writer.write(data.data);
}
else if (data.data instanceof EBMLFloat32) {
this.writeVarInt(4);
this.writeFloat32(data.data.value);
}
else if (data.data instanceof EBMLFloat64) {
this.writeVarInt(8);
this.writeFloat64(data.data.value);
}
else if (data.data instanceof EBMLSignedInt) {
const size = data.size ?? measureSignedInt(data.data.value);
this.writeVarInt(size);
this.writeSignedInt(data.data.value, size);
}
else if (data.data instanceof EBMLUnicodeString) {
const bytes = textEncoder.encode(data.data.value);
this.writeVarInt(bytes.length);
this.writer.write(bytes);
}
else {
assertNever(data.data);
}
}
}
}
export const MAX_VAR_INT_SIZE = 8;
export const MIN_HEADER_SIZE = 2; // 1-byte ID and 1-byte size
export const MAX_HEADER_SIZE = 2 * MAX_VAR_INT_SIZE; // 8-byte ID and 8-byte size
export const readVarIntSize = (slice) => {
if (slice.remainingLength < 1) {
return null;
}
const firstByte = readU8(slice);
slice.skip(-1);
if (firstByte === 0) {
return null; // Invalid VINT
}
let width = 1;
let mask = 0x80;
while ((firstByte & mask) === 0) {
width++;
mask >>= 1;
}
// Check if we have enough bytes to read the full varint
if (slice.remainingLength < width) {
return null;
}
return width;
};
export const readVarInt = (slice) => {
if (slice.remainingLength < 1) {
return null;
}
// Read the first byte to determine the width of the variable-length integer
const firstByte = readU8(slice);
if (firstByte === 0) {
return null; // Invalid VINT
}
// Find the position of VINT_MARKER, which determines the width
let width = 1;
let mask = 1 << 7;
while ((firstByte & mask) === 0) {
width++;
mask >>= 1;
}
if (slice.remainingLength < width - 1) {
// Not enough bytes
return null;
}
// First byte's value needs the marker bit cleared
let value = firstByte & (mask - 1);
// Read remaining bytes
for (let i = 1; i < width; i++) {
value *= 1 << 8;
value += readU8(slice);
}
return value;
};
export const readUnsignedInt = (slice, width) => {
if (width < 1 || width > 8) {
throw new Error('Bad unsigned int size ' + width);
}
let value = 0;
// Read bytes from most significant to least significant
for (let i = 0; i < width; i++) {
value *= 1 << 8;
value += readU8(slice);
}
return value;
};
export const readUnsignedBigInt = (slice, width) => {
if (width < 1) {
throw new Error('Bad unsigned int size ' + width);
}
let value = 0n;
for (let i = 0; i < width; i++) {
value <<= 8n;
value += BigInt(readU8(slice));
}
return value;
};
export const readSignedInt = (slice, width) => {
let value = readUnsignedInt(slice, width);
// If the highest bit is set, convert from two's complement
if (value & (1 << (width * 8 - 1))) {
value -= 2 ** (width * 8);
}
return value;
};
export const readElementId = (slice) => {
const size = readVarIntSize(slice);
if (size === null) {
return null;
}
if (slice.remainingLength < size) {
return null; // It don't fit
}
const id = readUnsignedInt(slice, size);
return id;
};
/** Returns `undefined` to indicate the EBML undefined size. Returns `null` if the size couldn't be read. */
export const readElementSize = (slice) => {
// Need at least 1 byte to read the size
if (slice.remainingLength < 1) {
return null;
}
const firstByte = readU8(slice);
if (firstByte === 0xff) {
return undefined;
}
slice.skip(-1);
const size = readVarInt(slice);
if (size === null) {
return null;
}
// In some (livestreamed) files, this is the value of the size field. While this technically is just a very
// large number, it is intended to behave like the reserved size 0xFF, meaning the size is undefined. We
// catch the number here. Note that it cannot be perfectly represented as a double, but the comparison works
// nonetheless.
// eslint-disable-next-line no-loss-of-precision
if (size === 0x00ffffffffffffff) {
return undefined;
}
return size;
};
export const readElementHeader = (slice) => {
assert(slice.remainingLength >= MIN_HEADER_SIZE);
const id = readElementId(slice);
if (id === null) {
return null;
}
const size = readElementSize(slice);
if (size === null) {
return null;
}
return { id, size };
};
export const readAsciiString = (slice, length) => {
const bytes = readBytes(slice, length);
// Actual string length might be shorter due to null terminators
let strLength = 0;
while (strLength < length && bytes[strLength] !== 0) {
strLength += 1;
}
return String.fromCharCode(...bytes.subarray(0, strLength));
};
export const readUnicodeString = (slice, length) => {
const bytes = readBytes(slice, length);
// Actual string length might be shorter due to null terminators
let strLength = 0;
while (strLength < length && bytes[strLength] !== 0) {
strLength += 1;
}
return textDecoder.decode(bytes.subarray(0, strLength));
};
export const readFloat = (slice, width) => {
if (width === 0) {
return 0;
}
if (width !== 4 && width !== 8) {
throw new Error('Bad float size ' + width);
}
return width === 4 ? readF32Be(slice) : readF64Be(slice);
};
/** Returns the byte offset in the file of the next element with a matching ID. */
export const searchForNextElementId = async (reader, startPos, ids, until) => {
const idsSet = new Set(ids);
let currentPos = startPos;
while (until === null || currentPos < until) {
let slice = reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE);
if (slice instanceof Promise)
slice = await slice;
if (!slice)
break;
const elementHeader = readElementHeader(slice);
if (!elementHeader) {
break;
}
if (idsSet.has(elementHeader.id)) {
return { pos: currentPos, found: true };
}
assertDefinedSize(elementHeader.size);
currentPos = slice.filePos + elementHeader.size;
}
return { pos: (until !== null && until > currentPos) ? until : currentPos, found: false };
};
/** Searches for the next occurrence of an element ID using a naive byte-wise search. */
export const resync = async (reader, startPos, ids, until) => {
const CHUNK_SIZE = 2 ** 16; // So we don't need to grab thousands of slices
const idsSet = new Set(ids);
let currentPos = startPos;
while (currentPos < until) {
let slice = reader.requestSliceRange(currentPos, 0, Math.min(CHUNK_SIZE, until - currentPos));
if (slice instanceof Promise)
slice = await slice;
if (!slice)
break;
if (slice.length < MAX_VAR_INT_SIZE)
break;
for (let i = 0; i < slice.length - MAX_VAR_INT_SIZE; i++) {
slice.filePos = currentPos;
const elementId = readElementId(slice);
if (elementId !== null && idsSet.has(elementId)) {
return currentPos;
}
currentPos++;
}
}
return null;
};
export const CODEC_STRING_MAP = {
'avc': 'V_MPEG4/ISO/AVC',
'hevc': 'V_MPEGH/ISO/HEVC',
'vp8': 'V_VP8',
'vp9': 'V_VP9',
'av1': 'V_AV1',
'aac': 'A_AAC',
'mp3': 'A_MPEG/L3',
'opus': 'A_OPUS',
'vorbis': 'A_VORBIS',
'flac': 'A_FLAC',
'ac3': 'A_AC3',
'eac3': 'A_EAC3',
'pcm-u8': 'A_PCM/INT/LIT',
'pcm-s16': 'A_PCM/INT/LIT',
'pcm-s16be': 'A_PCM/INT/BIG',
'pcm-s24': 'A_PCM/INT/LIT',
'pcm-s24be': 'A_PCM/INT/BIG',
'pcm-s32': 'A_PCM/INT/LIT',
'pcm-s32be': 'A_PCM/INT/BIG',
'pcm-f32': 'A_PCM/FLOAT/IEEE',
'pcm-f64': 'A_PCM/FLOAT/IEEE',
'webvtt': 'S_TEXT/WEBVTT',
};
export function assertDefinedSize(size) {
if (size === undefined) {
throw new Error('Undefined element size is used in a place where it is not supported.');
}
}
;
@@ -0,0 +1,186 @@
/*!
* 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 { AacCodecInfo, AudioCodec, VideoCodec } from '../codec.js';
import { Demuxer } from '../demuxer.js';
import { Input } from '../input.js';
import { InputTrack } from '../input-track.js';
import { MetadataTags, TrackDisposition } from '../metadata.js';
import { Rotation } from '../misc.js';
import { FileSlice, Reader } from '../reader.js';
type Segment = {
seekHeadSeen: boolean;
infoSeen: boolean;
tracksSeen: boolean;
cuesSeen: boolean;
attachmentsSeen: boolean;
tagsSeen: boolean;
timestampScale: number;
timestampFactor: number;
duration: number;
seekEntries: SeekEntry[];
tracks: InternalTrack[];
cuePoints: CuePoint[];
dataStartPos: number;
elementEndPos: number | null;
clusterSeekStartPos: number;
/**
* Caches the last cluster that was read. Based on the assumption that there will be multiple reads to the
* same cluster in quick succession.
*/
lastReadCluster: Cluster | null;
metadataTags: MetadataTags;
metadataTagsCollected: boolean;
};
type SeekEntry = {
id: number;
segmentPosition: number;
};
type Cluster = {
segment: Segment;
elementStartPos: number;
elementEndPos: number;
dataStartPos: number;
timestamp: number;
trackData: Map<number, ClusterTrackData>;
};
type ClusterTrackData = {
track: InternalTrack;
startTimestamp: number;
endTimestamp: number;
firstKeyFrameTimestamp: number | null;
blocks: ClusterBlock[];
presentationTimestamps: {
timestamp: number;
blockIndex: number;
}[];
};
declare enum BlockLacing {
None = 0,
Xiph = 1,
FixedSize = 2,
Ebml = 3
}
type ClusterBlock = {
timestamp: number;
duration: number;
isKeyFrame: boolean;
data: Uint8Array;
lacing: BlockLacing;
decoded: boolean;
mainAdditional: Uint8Array | null;
};
type CuePoint = {
time: number;
trackId: number;
clusterPosition: number;
};
declare enum ContentEncodingScope {
Block = 1,
Private = 2,
Next = 4
}
declare enum ContentCompAlgo {
Zlib = 0,
Bzlib = 1,
lzo1x = 2,
HeaderStripping = 3
}
type DecodingInstruction = {
order: number;
scope: ContentEncodingScope;
data: {
type: 'decompress';
algorithm: ContentCompAlgo | null;
settings: Uint8Array | null;
} | {
type: 'decrypt';
} | null;
};
type InternalTrack = {
id: number;
demuxer: MatroskaDemuxer;
segment: Segment;
/**
* List of all encountered cluster offsets alongside their timestamps. This list never gets truncated, but memory
* consumption should be negligible.
*/
clusterPositionCache: {
elementStartPos: number;
startTimestamp: number;
}[];
cuePoints: CuePoint[];
disposition: TrackDisposition;
inputTrack: InputTrack | null;
codecId: string | null;
codecPrivate: Uint8Array | null;
defaultDuration: number | null;
defaultDurationNs: number | null;
name: string | null;
languageCode: string;
decodingInstructions: DecodingInstruction[];
info: null | {
type: 'video';
width: number;
height: number;
rotation: Rotation;
codec: VideoCodec | null;
codecDescription: Uint8Array | null;
colorSpace: VideoColorSpaceInit | null;
alphaMode: boolean;
} | {
type: 'audio';
numberOfChannels: number;
sampleRate: number;
bitDepth: number;
codec: AudioCodec | null;
codecDescription: Uint8Array | null;
aacCodecInfo: AacCodecInfo | null;
};
};
export declare class MatroskaDemuxer extends Demuxer {
reader: Reader;
readMetadataPromise: Promise<void> | null;
segments: Segment[];
currentSegment: Segment | null;
currentTrack: InternalTrack | null;
currentCluster: Cluster | null;
currentBlock: ClusterBlock | null;
currentBlockAdditional: {
addId: number;
data: Uint8Array | null;
} | null;
currentCueTime: number | null;
currentDecodingInstruction: DecodingInstruction | null;
currentTagTargetIsMovie: boolean;
currentSimpleTagName: string | null;
currentAttachedFile: {
fileUid: bigint | null;
fileName: string | null;
fileMediaType: string | null;
fileData: Uint8Array | null;
fileDescription: string | null;
} | null;
isWebM: boolean;
constructor(input: Input);
computeDuration(): Promise<number>;
getTracks(): Promise<InputTrack[]>;
getMimeType(): Promise<string>;
getMetadataTags(): Promise<MetadataTags>;
readMetadata(): Promise<void>;
readSegment(segmentDataStart: number, dataSize: number | undefined): Promise<void>;
readCluster(startPos: number, segment: Segment): Promise<Cluster>;
getTrackDataInCluster(cluster: Cluster, trackNumber: number): ClusterTrackData | null;
expandLacedBlocks(blocks: ClusterBlock[], track: InternalTrack): void;
loadSegmentMetadata(segment: Segment): Promise<void>;
readContiguousElements(slice: FileSlice, stopIds?: number[]): number;
traverseElement(slice: FileSlice, stopIds?: number[]): boolean;
decodeBlockData(track: InternalTrack, rawData: Uint8Array): Uint8Array<ArrayBufferLike>;
processTagValue(name: string, value: string | Uint8Array): void;
}
export {};
//# sourceMappingURL=matroska-demuxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"matroska-demuxer.d.ts","sourceRoot":"","sources":["../../../../src/matroska/matroska-demuxer.ts"],"names":[],"mappings":"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"}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
/*!
* 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 declare const buildMatroskaMimeType: (info: {
isWebM: boolean;
hasVideo: boolean;
hasAudio: boolean;
codecStrings: string[];
}) => string;
//# sourceMappingURL=matroska-misc.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"matroska-misc.d.ts","sourceRoot":"","sources":["../../../../src/matroska/matroska-misc.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,qBAAqB,GAAI,MAAM;IAC3C,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;CACvB,WAeA,CAAC"}
@@ -0,0 +1,20 @@
/*!
* 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 const buildMatroskaMimeType = (info) => {
const base = info.hasVideo
? 'video/'
: info.hasAudio
? 'audio/'
: 'application/';
let string = base + (info.isWebM ? 'webm' : 'x-matroska');
if (info.codecStrings.length > 0) {
const uniqueCodecMimeTypes = [...new Set(info.codecStrings.filter(Boolean))];
string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`;
}
return string;
};
@@ -0,0 +1,75 @@
/*!
* 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 { MkvOutputFormat } from '../output-format.js';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputVideoTrack } from '../output.js';
import { SubtitleCue, SubtitleMetadata } from '../subtitles.js';
import { Muxer } from '../muxer.js';
import { EncodedPacket } from '../packet.js';
export declare class MatroskaMuxer extends Muxer {
private writer;
private ebmlWriter;
private format;
private trackDatas;
private allTracksKnown;
private segment;
private segmentInfo;
private seekHead;
private tracksElement;
private tagsElement;
private attachmentsElement;
private segmentDuration;
private cues;
private currentCluster;
private currentClusterStartMsTimestamp;
private currentClusterMaxMsTimestamp;
private trackDatasInCurrentCluster;
private duration;
constructor(output: Output, format: MkvOutputFormat);
start(): Promise<void>;
private writeEBMLHeader;
/**
* Creates a SeekHead element which is positioned near the start of the file and allows the media player to seek to
* relevant sections more easily. Since we don't know the positions of those sections yet, we'll set them later.
*/
private maybeCreateSeekHead;
private createSegmentInfo;
private createTracks;
private videoSpecificTrackInfo;
private audioSpecificTrackInfo;
private subtitleSpecificTrackInfo;
private maybeCreateTags;
private maybeCreateAttachments;
private createSegment;
private createCues;
private get segmentDataOffset();
private allTracksAreKnown;
getMimeType(): Promise<string>;
private getVideoTrackData;
private getAudioTrackData;
private getSubtitleTrackData;
addEncodedVideoPacket(track: OutputVideoTrack, packet: EncodedPacket, meta?: EncodedVideoChunkMetadata): Promise<void>;
addEncodedAudioPacket(track: OutputAudioTrack, packet: EncodedPacket, meta?: EncodedAudioChunkMetadata): Promise<void>;
addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata): Promise<void>;
private interleaveChunks;
/**
* Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often
* lack color space information. This method patches in that information.
*/
private fixVP9ColorSpace;
/** Converts a read-only external chunk into an internal one for easier use. */
private createInternalChunk;
/** Writes a block containing media data to the file. */
private writeBlock;
/** Creates a new Cluster element to contain media chunks. */
private createNewCluster;
private finalizeCurrentCluster;
onTrackClose(): Promise<void>;
/** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
finalize(): Promise<void>;
}
//# sourceMappingURL=matroska-muxer.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"matroska-muxer.d.ts","sourceRoot":"","sources":["../../../../src/matroska/matroska-muxer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAiCH,OAAO,EAAE,eAAe,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,mBAAmB,EAAe,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACzG,OAAO,EAEN,WAAW,EACX,gBAAgB,EAIhB,MAAM,cAAc,CAAC;AAkBtB,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AA6D1C,qBAAa,aAAc,SAAQ,KAAK;IACvC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,MAAM,CAAqC;IAEnD,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,cAAc,CAA0B;IAEhD,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,IAAI,CAA4B;IAExC,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,8BAA8B,CAAuB;IAC7D,OAAO,CAAC,4BAA4B,CAAuB;IAC3D,OAAO,CAAC,0BAA0B,CAE7B;IAEL,OAAO,CAAC,QAAQ,CAAK;gBAET,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe;IAa7C,KAAK;IAaX,OAAO,CAAC,eAAe;IAsBvB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAsE3B,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,YAAY;IA2DpB,OAAO,CAAC,sBAAsB;IAuE9B,OAAO,CAAC,sBAAsB;IAoB9B,OAAO,CAAC,yBAAyB;IAMjC,OAAO,CAAC,eAAe;IA2HvB,OAAO,CAAC,sBAAsB;IAkF9B,OAAO,CAAC,aAAa;IAiCrB,OAAO,CAAC,UAAU;IAIlB,OAAO,KAAK,iBAAiB,GAG5B;IAED,OAAO,CAAC,iBAAiB;IAUnB,WAAW;IAwBjB,OAAO,CAAC,iBAAiB;IAyDzB,OAAO,CAAC,iBAAiB;IAiEzB,OAAO,CAAC,oBAAoB;IA+BtB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,yBAAyB;IA8BtG,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,yBAAyB;IA8BtG,cAAc,CAAC,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,gBAAgB;YAsC5E,gBAAgB;IAkC9B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA4CxB,+EAA+E;IAC/E,OAAO,CAAC,mBAAmB;IAkB3B,wDAAwD;IACxD,OAAO,CAAC,UAAU;IA6GlB,6DAA6D;IAC7D,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,sBAAsB;IAmDf,YAAY;IAa3B,0GAA0G;IACpG,QAAQ;CA2Cd"}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,310 @@
/*!
* 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 { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track.js';
import { AnyIterable, Rotation } from './misc.js';
import { EncodedPacket } from './packet.js';
import { AudioSample, CropRectangle, VideoSample } from './sample.js';
/**
* Additional options for controlling packet retrieval.
* @group Media sinks
* @public
*/
export type PacketRetrievalOptions = {
/**
* When set to `true`, only packet metadata (like timestamp) will be retrieved - the actual packet data will not
* be loaded.
*/
metadataOnly?: boolean;
/**
* When set to true, key packets will be verified upon retrieval by looking into the packet's bitstream.
* If not enabled, the packet types will be determined solely by what's stored in the containing file and may be
* incorrect, potentially leading to decoder errors. Since determining a packet's actual type requires looking into
* its data, this option cannot be enabled together with `metadataOnly`.
*/
verifyKeyPackets?: boolean;
};
/**
* Sink for retrieving encoded packets from an input track.
* @group Media sinks
* @public
*/
export declare class EncodedPacketSink {
/** Creates a new {@link EncodedPacketSink} for the given {@link InputTrack}. */
constructor(track: InputTrack);
/**
* Retrieves the track's first packet (in decode order), or null if it has no packets. The first packet is very
* likely to be a key packet.
*/
getFirstPacket(options?: PacketRetrievalOptions): Promise<EncodedPacket | null>;
/**
* Retrieves the packet corresponding to the given timestamp, in seconds. More specifically, returns the last packet
* (in presentation order) with a start timestamp less than or equal to the given timestamp. This method can be
* used to retrieve a track's last packet using `getPacket(Infinity)`. The method returns null if the timestamp
* is before the first packet in the track.
*
* @param timestamp - The timestamp used for retrieval, in seconds.
*/
getPacket(timestamp: number, options?: PacketRetrievalOptions): Promise<EncodedPacket | null>;
/**
* Retrieves the packet following the given packet (in decode order), or null if the given packet is the
* last packet.
*/
getNextPacket(packet: EncodedPacket, options?: PacketRetrievalOptions): Promise<EncodedPacket | null>;
/**
* Retrieves the key packet corresponding to the given timestamp, in seconds. More specifically, returns the last
* key packet (in presentation order) with a start timestamp less than or equal to the given timestamp. A key packet
* is a packet that doesn't require previous packets to be decoded. This method can be used to retrieve a track's
* last key packet using `getKeyPacket(Infinity)`. The method returns null if the timestamp is before the first
* key packet in the track.
*
* To ensure that the returned packet is guaranteed to be a real key frame, enable `options.verifyKeyPackets`.
*
* @param timestamp - The timestamp used for retrieval, in seconds.
*/
getKeyPacket(timestamp: number, options?: PacketRetrievalOptions): Promise<EncodedPacket | null>;
/**
* Retrieves the key packet following the given packet (in decode order), or null if the given packet is the last
* key packet.
*
* To ensure that the returned packet is guaranteed to be a real key frame, enable `options.verifyKeyPackets`.
*/
getNextKeyPacket(packet: EncodedPacket, options?: PacketRetrievalOptions): Promise<EncodedPacket | null>;
/**
* Creates an async iterator that yields the packets in this track in decode order. To enable fast iteration, this
* method will intelligently preload packets based on the speed of the consumer.
*
* @param startPacket - (optional) The packet from which iteration should begin. This packet will also be yielded.
* @param endTimestamp - (optional) The timestamp at which iteration should end. This packet will _not_ be yielded.
*/
packets(startPacket?: EncodedPacket, endPacket?: EncodedPacket, options?: PacketRetrievalOptions): AsyncGenerator<EncodedPacket, void, unknown>;
}
/**
* Base class for decoded media sample sinks.
* @group Media sinks
* @public
*/
export declare abstract class BaseMediaSampleSink<MediaSample extends VideoSample | AudioSample> {
}
/**
* A sink that retrieves decoded video samples (video frames) from a video track.
* @group Media sinks
* @public
*/
export declare class VideoSampleSink extends BaseMediaSampleSink<VideoSample> {
/** Creates a new {@link VideoSampleSink} for the given {@link InputVideoTrack}. */
constructor(videoTrack: InputVideoTrack);
/**
* Retrieves the video sample (frame) corresponding to the given timestamp, in seconds. More specifically, returns
* the last video sample (in presentation order) with a start timestamp less than or equal to the given timestamp.
* Returns null if the timestamp is before the track's first timestamp.
*
* @param timestamp - The timestamp used for retrieval, in seconds.
*/
getSample(timestamp: number): Promise<VideoSample | null>;
/**
* Creates an async iterator that yields the video samples (frames) of this track in presentation order. This method
* will intelligently pre-decode a few frames ahead to enable fast iteration.
*
* @param startTimestamp - The timestamp in seconds at which to start yielding samples (inclusive).
* @param endTimestamp - The timestamp in seconds at which to stop yielding samples (exclusive).
*/
samples(startTimestamp?: number, endTimestamp?: number): AsyncGenerator<VideoSample, void, unknown>;
/**
* Creates an async iterator that yields a video sample (frame) for each timestamp in the argument. This method
* uses an optimized decoding pipeline if these timestamps are monotonically sorted, decoding each packet at most
* once, and is therefore more efficient than manually getting the sample for every timestamp. The iterator may
* yield null if no frame is available for a given timestamp.
*
* @param timestamps - An iterable or async iterable of timestamps in seconds.
*/
samplesAtTimestamps(timestamps: AnyIterable<number>): AsyncGenerator<VideoSample | null, void, unknown>;
}
/**
* A canvas with additional timing information (timestamp & duration).
* @group Media sinks
* @public
*/
export type WrappedCanvas = {
/** A canvas element or offscreen canvas. */
canvas: HTMLCanvasElement | OffscreenCanvas;
/** The timestamp of the corresponding video sample, in seconds. */
timestamp: number;
/** The duration of the corresponding video sample, in seconds. */
duration: number;
};
/**
* Options for constructing a CanvasSink.
* @group Media sinks
* @public
*/
export type CanvasSinkOptions = {
/**
* Whether the output canvases should have transparency instead of a black background. Defaults to `false`. Set
* this to `true` when using this sink to read transparent videos.
*/
alpha?: boolean;
/**
* The width of the output canvas in pixels, defaulting to the display width of the video track. If height is not
* set, it will be deduced automatically based on aspect ratio.
*/
width?: number;
/**
* The height of the output canvas in pixels, defaulting to the display height of the video track. If width is not
* set, it will be deduced automatically based on aspect ratio.
*/
height?: number;
/**
* The fitting algorithm in case both width and height are set.
*
* - `'fill'` will stretch the image to fill the entire box, potentially altering aspect ratio.
* - `'contain'` will contain the entire image within the box while preserving aspect ratio. This may lead to
* letterboxing.
* - `'cover'` will scale the image until the entire box is filled, while preserving aspect ratio.
*/
fit?: 'fill' | 'contain' | 'cover';
/**
* The clockwise rotation by which to rotate the raw video frame. Defaults to the rotation set in the file metadata.
* Rotation is applied before resizing.
*/
rotation?: Rotation;
/**
* 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.
*/
crop?: CropRectangle;
/**
* When set, specifies the number of canvases in the pool. These canvases will be reused in a ring buffer /
* round-robin type fashion. This keeps the amount of allocated VRAM constant and relieves the browser from
* constantly allocating/deallocating canvases. A pool size of 0 or `undefined` disables the pool and means a new
* canvas is created each time.
*/
poolSize?: number;
};
/**
* A sink that renders video samples (frames) of the given video track to canvases. This is often more useful than
* directly retrieving frames, as it comes with common preprocessing steps such as resizing or applying rotation
* metadata.
*
* This sink will yield `HTMLCanvasElement`s when in a DOM context, and `OffscreenCanvas`es otherwise.
*
* @group Media sinks
* @public
*/
export declare class CanvasSink {
/** Creates a new {@link CanvasSink} for the given {@link InputVideoTrack}. */
constructor(videoTrack: InputVideoTrack, options?: CanvasSinkOptions);
/**
* Retrieves a canvas with the video frame corresponding to the given timestamp, in seconds. More specifically,
* returns the last video frame (in presentation order) with a start timestamp less than or equal to the given
* timestamp. Returns null if the timestamp is before the track's first timestamp.
*
* @param timestamp - The timestamp used for retrieval, in seconds.
*/
getCanvas(timestamp: number): Promise<WrappedCanvas | null>;
/**
* Creates an async iterator that yields canvases with the video frames of this track in presentation order. This
* method will intelligently pre-decode a few frames ahead to enable fast iteration.
*
* @param startTimestamp - The timestamp in seconds at which to start yielding canvases (inclusive).
* @param endTimestamp - The timestamp in seconds at which to stop yielding canvases (exclusive).
*/
canvases(startTimestamp?: number, endTimestamp?: number): AsyncGenerator<WrappedCanvas, void, unknown>;
/**
* Creates an async iterator that yields a canvas for each timestamp in the argument. This method uses an optimized
* decoding pipeline if these timestamps are monotonically sorted, decoding each packet at most once, and is
* therefore more efficient than manually getting the canvas for every timestamp. The iterator may yield null if
* no frame is available for a given timestamp.
*
* @param timestamps - An iterable or async iterable of timestamps in seconds.
*/
canvasesAtTimestamps(timestamps: AnyIterable<number>): AsyncGenerator<WrappedCanvas | null, void, unknown>;
}
/**
* Sink for retrieving decoded audio samples from an audio track.
* @group Media sinks
* @public
*/
export declare class AudioSampleSink extends BaseMediaSampleSink<AudioSample> {
/** Creates a new {@link AudioSampleSink} for the given {@link InputAudioTrack}. */
constructor(audioTrack: InputAudioTrack);
/**
* Retrieves the audio sample corresponding to the given timestamp, in seconds. More specifically, returns
* the last audio sample (in presentation order) with a start timestamp less than or equal to the given timestamp.
* Returns null if the timestamp is before the track's first timestamp.
*
* @param timestamp - The timestamp used for retrieval, in seconds.
*/
getSample(timestamp: number): Promise<AudioSample | null>;
/**
* Creates an async iterator that yields the audio samples of this track in presentation order. This method
* will intelligently pre-decode a few samples ahead to enable fast iteration.
*
* @param startTimestamp - The timestamp in seconds at which to start yielding samples (inclusive).
* @param endTimestamp - The timestamp in seconds at which to stop yielding samples (exclusive).
*/
samples(startTimestamp?: number, endTimestamp?: number): AsyncGenerator<AudioSample, void, unknown>;
/**
* Creates an async iterator that yields an audio sample for each timestamp in the argument. This method
* uses an optimized decoding pipeline if these timestamps are monotonically sorted, decoding each packet at most
* once, and is therefore more efficient than manually getting the sample for every timestamp. The iterator may
* yield null if no sample is available for a given timestamp.
*
* @param timestamps - An iterable or async iterable of timestamps in seconds.
*/
samplesAtTimestamps(timestamps: AnyIterable<number>): AsyncGenerator<AudioSample | null, void, unknown>;
}
/**
* An AudioBuffer with additional timing information (timestamp & duration).
* @group Media sinks
* @public
*/
export type WrappedAudioBuffer = {
/** An AudioBuffer. */
buffer: AudioBuffer;
/** The timestamp of the corresponding audio sample, in seconds. */
timestamp: number;
/** The duration of the corresponding audio sample, in seconds. */
duration: number;
};
/**
* A sink that retrieves decoded audio samples from an audio track and converts them to `AudioBuffer` instances. This is
* often more useful than directly retrieving audio samples, as audio buffers can be directly used with the
* Web Audio API.
* @group Media sinks
* @public
*/
export declare class AudioBufferSink {
/** Creates a new {@link AudioBufferSink} for the given {@link InputAudioTrack}. */
constructor(audioTrack: InputAudioTrack);
/**
* Retrieves the audio buffer corresponding to the given timestamp, in seconds. More specifically, returns
* the last audio buffer (in presentation order) with a start timestamp less than or equal to the given timestamp.
* Returns null if the timestamp is before the track's first timestamp.
*
* @param timestamp - The timestamp used for retrieval, in seconds.
*/
getBuffer(timestamp: number): Promise<WrappedAudioBuffer | null>;
/**
* Creates an async iterator that yields audio buffers of this track in presentation order. This method
* will intelligently pre-decode a few buffers ahead to enable fast iteration.
*
* @param startTimestamp - The timestamp in seconds at which to start yielding buffers (inclusive).
* @param endTimestamp - The timestamp in seconds at which to stop yielding buffers (exclusive).
*/
buffers(startTimestamp?: number, endTimestamp?: number): AsyncGenerator<WrappedAudioBuffer, void, unknown>;
/**
* Creates an async iterator that yields an audio buffer for each timestamp in the argument. This method
* uses an optimized decoding pipeline if these timestamps are monotonically sorted, decoding each packet at most
* once, and is therefore more efficient than manually getting the buffer for every timestamp. The iterator may
* yield null if no buffer is available for a given timestamp.
*
* @param timestamps - An iterable or async iterable of timestamps in seconds.
*/
buffersAtTimestamps(timestamps: AnyIterable<number>): AsyncGenerator<WrappedAudioBuffer | null, void, unknown>;
}
//# sourceMappingURL=media-sink.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"media-sink.d.ts","sourceRoot":"","sources":["../../../src/media-sink.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAgBH,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EACN,WAAW,EAcX,QAAQ,EAKR,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,WAAW,EAAsB,aAAa,EAAyB,WAAW,EAAE,MAAM,UAAU,CAAC;AAE9G;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AA+CF;;;;GAIG;AACH,qBAAa,iBAAiB;IAI7B,gFAAgF;gBACpE,KAAK,EAAE,UAAU;IAQ7B;;;OAGG;IACH,cAAc,CAAC,OAAO,GAAE,sBAA2B;IAUnD;;;;;;;OAOG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B;IAWjE;;;OAGG;IACH,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,sBAA2B;IAazE;;;;;;;;;;OAUG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IA2B1G;;;;;OAKG;IACG,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IA6BlH;;;;;;OAMG;IACH,OAAO,CACN,WAAW,CAAC,EAAE,aAAa,EAC3B,SAAS,CAAC,EAAE,aAAa,EACzB,OAAO,GAAE,sBAA2B,GAClC,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC;CA6G/C;AAgBD;;;;GAIG;AACH,8BAAsB,mBAAmB,CACxC,WAAW,SAAS,WAAW,GAAG,WAAW;CAyY7C;AAqjBD;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,mBAAmB,CAAC,WAAW,CAAC;IAIpE,mFAAmF;gBACvE,UAAU,EAAE,eAAe;IAoCvC;;;;;;OAMG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM;IASjC;;;;;;OAMG;IACH,OAAO,CAAC,cAAc,SAAI,EAAE,YAAY,SAAW;IAInD;;;;;;;OAOG;IACH,mBAAmB,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC;CAGnD;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC3B,4CAA4C;IAC5C,MAAM,EAAE,iBAAiB,GAAG,eAAe,CAAC;IAC5C,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC/B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;;;GASG;AACH,qBAAa,UAAU;IAsBtB,8EAA8E;gBAClE,UAAU,EAAE,eAAe,EAAE,OAAO,GAAE,iBAAsB;IA0IxE;;;;;;OAMG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM;IAOjC;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,SAAI,EAAE,YAAY,SAAW;IAOpD;;;;;;;OAOG;IACH,oBAAoB,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC;CAMpD;AAsTD;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,mBAAmB,CAAC,WAAW,CAAC;IAIpE,mFAAmF;gBACvE,UAAU,EAAE,eAAe;IAsCvC;;;;;;OAMG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM;IASjC;;;;;;OAMG;IACH,OAAO,CAAC,cAAc,SAAI,EAAE,YAAY,SAAW;IAInD;;;;;;;OAOG;IACH,mBAAmB,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC;CAGnD;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAChC,sBAAsB;IACtB,MAAM,EAAE,WAAW,CAAC;IACpB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,eAAe;IAI3B,mFAAmF;gBACvE,UAAU,EAAE,eAAe;IAoBvC;;;;;;OAMG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM;IAOjC;;;;;;OAMG;IACH,OAAO,CAAC,cAAc,SAAI,EAAE,YAAY,SAAW;IAOnD;;;;;;;OAOG;IACH,mBAAmB,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC;CAMnD"}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,248 @@
/*!
* 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 { AudioCodec, SubtitleCodec, VideoCodec } from './codec.js';
import { EncodedPacket } from './packet.js';
import { AudioSample, VideoSample } from './sample.js';
import { AudioEncodingConfig, VideoEncodingConfig } from './encode.js';
/**
* Base class for media sources. Media sources are used to add media samples to an output file.
* @group Media sources
* @public
*/
export declare abstract class MediaSource {
/**
* Closes this source. This prevents future samples from being added and signals to the output file that no further
* samples will come in for this track. Calling `.close()` is optional but recommended after adding the
* last sample - for improved performance and reduced memory usage.
*/
close(): void;
}
/**
* Base class for video sources - sources for video tracks.
* @group Media sources
* @public
*/
export declare abstract class VideoSource extends MediaSource {
/** Internal constructor. */
constructor(codec: VideoCodec);
}
/**
* The most basic video source; can be used to directly pipe encoded packets into the output file.
* @group Media sources
* @public
*/
export declare class EncodedVideoPacketSource extends VideoSource {
/** Creates a new {@link EncodedVideoPacketSource} whose packets are encoded using `codec`. */
constructor(codec: VideoCodec);
/**
* Adds an encoded packet to the output video track. Packets must be added in *decode order*, while a packet's
* timestamp must be its *presentation timestamp*. B-frames are handled automatically.
*
* @param meta - Additional metadata from the encoder. You should pass this for the first call, including a valid
* decoder config.
*
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
* to respect writer and encoder backpressure.
*/
add(packet: EncodedPacket, meta?: EncodedVideoChunkMetadata): Promise<void>;
}
/**
* This source can be used to add raw, unencoded video samples (frames) to an output video track. These frames will
* automatically be encoded and then piped into the output.
* @group Media sources
* @public
*/
export declare class VideoSampleSource extends VideoSource {
/**
* Creates a new {@link VideoSampleSource} whose samples are encoded according to the specified
* {@link VideoEncodingConfig}.
*/
constructor(encodingConfig: VideoEncodingConfig);
/**
* Encodes a video sample (frame) and then adds it to the output.
*
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
* to respect writer and encoder backpressure.
*/
add(videoSample: VideoSample, encodeOptions?: VideoEncoderEncodeOptions): Promise<void>;
}
/**
* This source can be used to add video frames to the output track from a fixed canvas element. Since canvases are often
* used for rendering, this source provides a convenient wrapper around {@link VideoSampleSource}.
* @group Media sources
* @public
*/
export declare class CanvasSource extends VideoSource {
/**
* Creates a new {@link CanvasSource} from a canvas element or `OffscreenCanvas` whose samples are encoded
* according to the specified {@link VideoEncodingConfig}.
*/
constructor(canvas: HTMLCanvasElement | OffscreenCanvas, encodingConfig: VideoEncodingConfig);
/**
* Captures the current canvas state as a video sample (frame), encodes it and adds it to the output.
*
* @param timestamp - The timestamp of the sample, in seconds.
* @param duration - The duration of the sample, in seconds.
*
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
* to respect writer and encoder backpressure.
*/
add(timestamp: number, duration?: number, encodeOptions?: VideoEncoderEncodeOptions): Promise<void>;
}
/**
* Video source that encodes the frames of a
* [`MediaStreamVideoTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) and pipes them into the
* output. This is useful for capturing live or real-time data such as webcams or screen captures. Frames will
* automatically start being captured once the connected {@link Output} is started, and will keep being captured until
* the {@link Output} is finalized or this source is closed.
* @group Media sources
* @public
*/
export declare class MediaStreamVideoTrackSource extends VideoSource {
/** A promise that rejects upon any error within this source. This promise never resolves. */
get errorPromise(): Promise<void>;
/** Whether this source is currently paused as a result of calling `.pause()`. */
get paused(): boolean;
/**
* Creates a new {@link MediaStreamVideoTrackSource} from a
* [`MediaStreamVideoTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack), which will pull
* video samples from the stream in real time and encode them according to {@link VideoEncodingConfig}.
*/
constructor(track: MediaStreamVideoTrack, encodingConfig: VideoEncodingConfig);
/**
* Pauses the capture of video frames - any video frames emitted by the underlying media stream will be ignored
* while paused. This does *not* close the underlying `MediaStreamVideoTrack`, it just ignores its output.
*/
pause(): void;
/** Resumes the capture of video frames after being paused. */
resume(): void;
}
/**
* Base class for audio sources - sources for audio tracks.
* @group Media sources
* @public
*/
export declare abstract class AudioSource extends MediaSource {
/** Internal constructor. */
constructor(codec: AudioCodec);
}
/**
* The most basic audio source; can be used to directly pipe encoded packets into the output file.
* @group Media sources
* @public
*/
export declare class EncodedAudioPacketSource extends AudioSource {
/** Creates a new {@link EncodedAudioPacketSource} whose packets are encoded using `codec`. */
constructor(codec: AudioCodec);
/**
* Adds an encoded packet to the output audio track. Packets must be added in *decode order*.
*
* @param meta - Additional metadata from the encoder. You should pass this for the first call, including a valid
* decoder config.
*
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
* to respect writer and encoder backpressure.
*/
add(packet: EncodedPacket, meta?: EncodedAudioChunkMetadata): Promise<void>;
}
/**
* This source can be used to add raw, unencoded audio samples to an output audio track. These samples will
* automatically be encoded and then piped into the output.
* @group Media sources
* @public
*/
export declare class AudioSampleSource extends AudioSource {
/**
* Creates a new {@link AudioSampleSource} whose samples are encoded according to the specified
* {@link AudioEncodingConfig}.
*/
constructor(encodingConfig: AudioEncodingConfig);
/**
* Encodes an audio sample and then adds it to the output.
*
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
* to respect writer and encoder backpressure.
*/
add(audioSample: AudioSample): Promise<void>;
}
/**
* This source can be used to add audio data from an AudioBuffer to the output track. This is useful when working with
* the Web Audio API.
* @group Media sources
* @public
*/
export declare class AudioBufferSource extends AudioSource {
/**
* Creates a new {@link AudioBufferSource} whose `AudioBuffer` instances are encoded according to the specified
* {@link AudioEncodingConfig}.
*/
constructor(encodingConfig: AudioEncodingConfig);
/**
* Converts an AudioBuffer to audio samples, encodes them and adds them to the output. The first AudioBuffer will
* be played at timestamp 0, and any subsequent AudioBuffer will have a timestamp equal to the total duration of
* all previous AudioBuffers.
*
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
* to respect writer and encoder backpressure.
*/
add(audioBuffer: AudioBuffer): Promise<void>;
}
/**
* Audio source that encodes the data of a
* [`MediaStreamAudioTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) and pipes it into the
* output. This is useful for capturing live or real-time audio such as microphones or audio from other media elements.
* Audio will automatically start being captured once the connected {@link Output} is started, and will keep being
* captured until the {@link Output} is finalized or this source is closed.
* @group Media sources
* @public
*/
export declare class MediaStreamAudioTrackSource extends AudioSource {
/** A promise that rejects upon any error within this source. This promise never resolves. */
get errorPromise(): Promise<void>;
/** Whether this source is currently paused as a result of calling `.pause()`. */
get paused(): boolean;
/**
* Creates a new {@link MediaStreamAudioTrackSource} from a `MediaStreamAudioTrack`, which will pull audio samples
* from the stream in real time and encode them according to {@link AudioEncodingConfig}.
*/
constructor(track: MediaStreamAudioTrack, encodingConfig: AudioEncodingConfig);
/**
* Pauses the capture of audio data - any audio data emitted by the underlying media stream will be ignored
* while paused. This does *not* close the underlying `MediaStreamAudioTrack`, it just ignores its output.
*/
pause(): void;
/** Resumes the capture of audio data after being paused. */
resume(): void;
}
/**
* Base class for subtitle sources - sources for subtitle tracks.
* @group Media sources
* @public
*/
export declare abstract class SubtitleSource extends MediaSource {
/** Internal constructor. */
constructor(codec: SubtitleCodec);
}
/**
* This source can be used to add subtitles from a subtitle text file.
* @group Media sources
* @public
*/
export declare class TextSubtitleSource extends SubtitleSource {
/** Creates a new {@link TextSubtitleSource} where added text chunks are in the specified `codec`. */
constructor(codec: SubtitleCodec);
/**
* Parses the subtitle text according to the specified codec and adds it to the output track. You don't have to
* add the entire subtitle file at once here; you can provide it in chunks.
*
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
* to respect writer and encoder backpressure.
*/
add(text: string): Promise<void>;
}
//# sourceMappingURL=media-source.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"media-source.d.ts","sourceRoot":"","sources":["../../../src/media-source.ts"],"names":[],"mappings":"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"}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More