Merge pull request #50 from Vanilagy/mp3-encoder

Create @mediabunny/mp3-encoder, restructure and extend build system
This commit is contained in:
David P.
2025-08-10 03:37:12 +02:00
committed by GitHub
44 changed files with 3244 additions and 140 deletions
+5 -2
View File
@@ -1,5 +1,8 @@
/node_modules
.vscode
node_modules
/dist
/dist-docs
.DS_Store
/docs/.vitepress/cache
/docs/.vitepress/cache
packages/mp3-encoder/dist
+7
View File
@@ -49,10 +49,17 @@ Core features include:
### 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "dist/modules/index.d.ts",
"mainEntryPointFilePath": "dist/modules/src/index.d.ts",
"bundledPackages": [],
"compiler": {},
"apiReport": {
-76
View File
@@ -1,76 +0,0 @@
import * as esbuild from 'esbuild';
import process from 'node:process';
const baseConfig = {
entryPoints: ['src/index.ts'],
bundle: true,
logLevel: 'info',
banner: {
js: `/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/`,
},
legalComments: 'none',
};
const umdConfig = {
...baseConfig,
format: 'iife',
// The following are hacks to basically make this an UMD module. No native support for that in esbuild as of today
globalName: 'Mediabunny',
footer: {
js:
`if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, Mediabunny)`,
},
};
const esmConfig = {
...baseConfig,
format: 'esm',
};
const ctxUmd = await esbuild.context({
...umdConfig,
outfile: 'dist/bundles/mediabunny.cjs',
});
const ctxEsm = await esbuild.context({
...esmConfig,
outfile: 'dist/bundles/mediabunny.mjs',
});
const ctxUmdMinified = await esbuild.context({
...umdConfig,
outfile: 'dist/bundles/mediabunny.min.cjs',
minify: true,
});
const ctxEsmMinified = await esbuild.context({
...esmConfig,
outfile: 'dist/bundles/mediabunny.min.mjs',
minify: true,
});
if (process.argv[2] === '--watch') {
await Promise.all([
ctxUmd.watch(),
ctxEsm.watch(),
ctxUmdMinified.watch(),
ctxEsmMinified.watch(),
]);
} else {
ctxUmd.rebuild();
ctxEsm.rebuild();
ctxUmdMinified.rebuild();
ctxEsmMinified.rebuild();
await Promise.all([
ctxUmd.dispose(),
ctxEsm.dispose(),
ctxUmdMinified.dispose(),
ctxEsmMinified.dispose(),
]);
}
Executable
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
set -e
# This script must be executed via `npm run build`
# Clear the stuff from last build
rm -rf dist
rm -rf packages/mp3-encoder/dist
# Ensure license headers on all source files
tsx scripts/ensure-license-headers.ts
# Type check & generate .js and .d.ts files
tsc -p src
tsc -p packages/mp3-encoder/src
# So that the resulting files use valid ESM imports with file extension. This only runs for the core Mediabunny as only
# it ships the individual files to npm (for tree shaking, because it's large)
npm run fix-build-import-paths
# Creates bundles for all packages
tsx scripts/bundle.ts
# Declaration file rollup and checks
api-extractor run
api-extractor run -c packages/mp3-encoder/api-extractor.json
# Checks that all symbols are documented
tsx scripts/check-docblocks.ts dist/mediabunny.d.ts
tsx scripts/check-docblocks.ts packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
# Appends stuff to the declaration files to register the global variables these libraries expose
echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts
echo 'export as namespace MediabunnyMp3Encoder;' >> packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts
+5 -2
View File
@@ -1,8 +1,11 @@
<!DOCTYPE html>
<script src="../dist/bundles/mediabunny.cjs"></script>
<script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script>
<script type="module">
MediabunnyMp3Encoder.registerMp3Encoder();
const fileInput = document.createElement('input');
fileInput.type = 'file';
document.body.append(fileInput);
@@ -21,7 +24,7 @@
chunked: true,
chunkSize: 2**20
});
const outputFormat = new Mediabunny.Mp4OutputFormat({});
const outputFormat = new Mediabunny.Mp3OutputFormat({});
const button = document.createElement('button');
button.textContent = 'Cancel';
@@ -70,7 +73,7 @@
},
*/
video: {
frameRate: 27.123,
//frameRate: 27.123,
//width: 320,
//forceTranscode: true,
//codec: 'av1',
+2
View File
@@ -16,6 +16,7 @@
source
});
/*
const audioTrack = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.EncodedPacketSink(audioTrack);
@@ -28,6 +29,7 @@
}
console.log("done")
*/
/*
for await (const packet of sink.packets()) {
+6
View File
@@ -75,6 +75,12 @@ export default withMermaid({
{ text: 'Supported formats & codecs', link: '/guide/supported-formats-and-codecs' },
],
},
{
text: 'Extensions',
items: [
{ text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' },
],
},
],
socialLinks: [
+90
View File
@@ -0,0 +1,90 @@
# @mediabunny/mp3-encoder
Browsers typically have no support for MP3 encoding in their WebCodecs implementations. Given the ubiquity of the format, this extension package provides an MP3 encoder for use with Mediabunny. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a highly-performant WASM build of the [LAME MP3 Encoder](https://lame.sourceforge.io/) under the hood.
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="https://github.com/Vanilagy/mediabunny/blob/main/packages/mp3-encoder/README.md">
GitHub page
<span class="vpi-arrow-right" />
</a>
## Installation
This library peer-depends on Mediabunny. Install both using npm:
```bash
npm install mediabunny @mediabunny/mp3-encoder
```
Alternatively, directly include them using a script tag:
```html
<script src="mediabunny.js"></script>
<script src="mediabunny-mp3-encoder.js"></script>
```
This will expose the global objects `Mediabunny` and `MediabunnyMp3Encoder`. Use `mediabunny-mp3-encoder.d.ts` to provide types for these globals. You can download the built distribution files from the [releases page](https://github.com/Vanilagy/mediabunny/releases).
## Usage
```ts
import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
registerMp3Encoder();
```
That's it - Mediabunny now uses the registered MP3 encoder automatically.
If you want to be more correct, check for native browser support first:
```ts
import { canEncodeAudio } from 'mediabunny';
import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
if (!(await canEncodeAudio('mp3'))) {
registerMp3Encoder();
}
```
## Example
Here, we convert an input file to an MP3:
```ts
import {
Input,
ALL_FORMATS,
BlobSource,
Output,
BufferTarget,
Mp3OutputFormat,
canEncodeAudio,
Conversion,
} from 'mediabunny';
import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
if (!(await canEncodeAudio('mp3'))) {
// Only register the custom encoder if there's no native support
registerMp3Encoder();
}
const input = new Input({
source: new BlobSource(file), // From a file picker, for example
formats: ALL_FORMATS,
});
const output = new Output({
format: new Mp3OutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({
input,
output,
});
await conversion.execute();
output.target.buffer; // => ArrayBuffer containing the MP3 file
```
## Implementation details
This library implements an MP3 encoder by registering a custom encoder class with Mediabunny. This class, when initialized, spawns a worker which then immediately loads a WASM build of the LAME MP3 encoder. Then, raw data is sent to the worker and encoded data is received from it. These encoded chunks are then concatenated in the main thread and properly split into separate MP3 frames.
Great care was put into ensuring maximum compatibility of this package; it works with bundlers, directly in the browser, as well as in Node, Deno, and Bun. All code (including worker & WASM) are bundled into a single file, eliminating the need for CDNs or WASM path arguments. This packages therefore serves as a reference implementation of WASM-based encoder extensions for Mediabunny.
The WASM build itself is a performance-optimized, SIMD-enabled build of LAME 3.100, with all unneeded features disabled. Because maximum performance was the priority, the build is slighter bigger, but ~130 kB gzipped is still very reasonable in my opinion. In my tests, it encodes 5 seconds of audio in ~90 milliseconds (55x real-time speed).
+1 -1
View File
@@ -34,6 +34,6 @@ You can also just include the library using a script tag in your HTML:
<script src="mediabunny.cjs"></script>
```
This will add a `Mediabunny` object to the global scope.
This will add a `Mediabunny` object to the global scope. You can provide types for this global using `mediabunny.d.ts`.
You can download a built distribution file from the [releases page](https://github.com/Vanilagy/mediabunny/releases). Use the `*.cjs` builds for normal script tag inclusion, or the `*.mjs` builds for script tags with `type="module"` or direct imports via ESM. Including the `mediabunny.d.ts` declaration file in your TypeScript project will declare a global `Mediabunny` namespace.
+4
View File
@@ -212,6 +212,10 @@ type Mp3OutputFormatOptions = {
- `onXingFrame`\
Will be called once the Xing metadata frame is finalized, which happens at the end of the writing process.
::: info
Most browsers don't support encoding MP3. Use the official [`@mediabunny/mp3-encoder`](./extensions/mp3-encoder) package to polyfill an encoder.
:::
## WAVE
This output format creates WAVE (.wav) files.
+3 -4
View File
@@ -34,10 +34,9 @@ export default tseslint.config(
{
ignores: [
'dist',
'build',
'api-sketch.ts',
'build.mjs',
'append-namespace.mjs',
'dist-docs',
'packages/mp3-encoder/dist',
'packages/mp3-encoder/build',
'eslint.config.mjs',
'docs/.vitepress/cache',
]
+217 -2
View File
@@ -1,13 +1,16 @@
{
"name": "mediabunny",
"version": "1.4.4",
"version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.4.4",
"version": "1.5.0",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
],
"dependencies": {
"@types/dom-mediacapture-transform": "^0.1.11",
"@types/dom-webcodecs": "0.1.13"
@@ -22,7 +25,9 @@
"@types/markdown-it-footnote": "^3.0.4",
"@types/node": "^22.13.10",
"esbuild": "^0.25.1",
"esbuild-plugin-external-global": "^1.0.1",
"eslint": "^9.22.0",
"find-cache-dir": "^5.0.0",
"markdown-it-footnote": "^4.0.0",
"markdown-it-mathjax3": "^4.3.2",
"mermaid": "^11.6.0",
@@ -1276,6 +1281,10 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mediabunny/mp3-encoder": {
"resolved": "packages/mp3-encoder",
"link": true
},
"node_modules/@mermaid-js/mermaid-mindmap": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-mindmap/-/mermaid-mindmap-9.3.0.tgz",
@@ -2448,6 +2457,13 @@
"integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==",
"license": "MIT"
},
"node_modules/@types/emscripten": {
"version": "1.40.1",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.1.tgz",
"integrity": "sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -3472,6 +3488,13 @@
"node": ">= 6"
}
},
"node_modules/common-path-prefix": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
"integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
"dev": true,
"license": "ISC"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4307,6 +4330,13 @@
"@esbuild/win32-x64": "0.25.1"
}
},
"node_modules/esbuild-plugin-external-global": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/esbuild-plugin-external-global/-/esbuild-plugin-external-global-1.0.1.tgz",
"integrity": "sha512-NDzYHRoShpvLqNcrgV8ZQh61sMIFAry5KLTQV83BPG5iTXCCu7h72SCfJ97bW0GqtuqDD/1aqLbKinI/rNgUsg==",
"dev": true,
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4678,6 +4708,23 @@
"node": ">=8"
}
},
"node_modules/find-cache-dir": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-5.0.0.tgz",
"integrity": "sha512-OuWNfjfP05JcpAP3JPgAKUhWefjMRfI5iAoSsvE24ANYWJaepAtlSgWECSVEuRgSXpyNEc9DJwG/TZpgcOqyig==",
"dev": true,
"license": "MIT",
"dependencies": {
"common-path-prefix": "^3.0.0",
"pkg-dir": "^7.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -5155,6 +5202,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -5617,6 +5673,21 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -5828,6 +5899,21 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mediabunny": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.5.0.tgz",
"integrity": "sha512-59gA8jMk8fjU1XdIwCDOI1v3M3nUE1r2Hl4jLtGZNLdxFv7sbDiliuneOPdhacCv/THheCOA6y5IdiIsF0250g==",
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"@types/dom-mediacapture-transform": "^0.1.11",
"@types/dom-webcodecs": "0.1.13"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/Vanilagy"
}
},
"node_modules/mensch": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz",
@@ -6736,6 +6822,110 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkg-dir": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz",
"integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"find-up": "^6.3.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pkg-dir/node_modules/find-up": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
"integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^7.1.0",
"path-exists": "^5.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pkg-dir/node_modules/locate-path": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
"integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^6.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pkg-dir/node_modules/p-limit": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^1.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pkg-dir/node_modules/p-locate": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
"integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pkg-dir/node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/pkg-dir/node_modules/yocto-queue": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pkg-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
@@ -6869,6 +7059,21 @@
],
"license": "MIT"
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
@@ -8806,6 +9011,16 @@
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
"version": "0.0.1",
"devDependencies": {
"@types/emscripten": "^1.40.1"
},
"peerDependencies": {
"mediabunny": "^1.0.0"
}
}
}
}
+17 -9
View File
@@ -1,15 +1,18 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.5.0",
"version": "1.6.0",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
"packages/*"
],
"main": "./dist/bundles/mediabunny.cjs",
"module": "./dist/modules/index.js",
"types": "./dist/modules/index.d.ts",
"module": "./dist/modules/src/index.js",
"types": "./dist/modules/src/index.d.ts",
"exports": {
"types": "./dist/modules/index.d.ts",
"import": "./dist/modules/index.js",
"types": "./dist/modules/src/index.d.ts",
"import": "./dist/modules/src/index.js",
"require": "./dist/bundles/mediabunny.cjs"
},
"files": [
@@ -21,10 +24,10 @@
],
"sideEffects": false,
"scripts": {
"build": "rm -rf dist && tsx scripts/ensure-license-headers.ts && tsc -p src && npm run fix-build-import-paths && node build.mjs && api-extractor run && npm run check-docblocks && npm run append-namespace",
"watch": "node build.mjs --watch",
"build": "./build.sh",
"watch": "tsx scripts/bundle.ts --watch",
"lint": "eslint .",
"check": "tsc -p src --noEmit && tsc -p scripts --noEmit && tsc -p tsconfig.vite.json --noEmit && rm tsconfig.vite.tsbuildinfo",
"check": "tsc -p src --noEmit && tsc -p packages/mp3-encoder/src --noEmit && tsc -p scripts --noEmit && tsc -p tsconfig.vite.json --noEmit && rm tsconfig.vite.tsbuildinfo",
"check-docblocks": "tsx scripts/check-docblocks.ts dist/mediabunny.d.ts",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs && npm run examples:build",
@@ -32,7 +35,10 @@
"dev": "vite",
"examples:build": "vite build",
"fix-build-import-paths": "tsx scripts/add-import-extensions.ts",
"append-namespace": "echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts"
"append-namespace": "echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts",
"bump-patch": "npm version patch --no-git-tag-version --workspaces --include-workspace-root",
"bump-minor": "npm version minor --no-git-tag-version --workspaces --include-workspace-root",
"bump-major": "npm version major --no-git-tag-version --workspaces --include-workspace-root"
},
"license": "MPL-2.0",
"repository": {
@@ -61,7 +67,9 @@
"@types/markdown-it-footnote": "^3.0.4",
"@types/node": "^22.13.10",
"esbuild": "^0.25.1",
"esbuild-plugin-external-global": "^1.0.1",
"eslint": "^9.22.0",
"find-cache-dir": "^5.0.0",
"markdown-it-footnote": "^4.0.0",
"markdown-it-mathjax3": "^4.3.2",
"mermaid": "^11.6.0",
+1
View File
@@ -0,0 +1 @@
build/* linguist-generated
+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.
+154
View File
@@ -0,0 +1,154 @@
# @mediabunny/mp3-encoder
[![](https://img.shields.io/npm/v/@mediabunny/mp3-encoder)](https://www.npmjs.com/package/@mediabunny/mp3-encoder)
[![](https://img.shields.io/bundlephobia/minzip/@mediabunny/mp3-encoder)](https://bundlephobia.com/package/@mediabunny/mp3-encoder)
[![](https://img.shields.io/npm/dm/@mediabunny/mp3-encoder)](https://www.npmjs.com/package/@mediabunny/mp3-encoder)
<div align="center">
<img src="./logo.svg" width="180" height="180">
</div>
Browsers typically have no support for MP3 encoding in their WebCodecs implementations. Given the ubiquity of the format, this extension package provides an MP3 encoder for use with [Mediabunny](https://github.com/Vanilagy/mediabunny). It is implemented using Mediabunny's [custom coder API](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) and uses a highly-performant WASM build of the [LAME MP3 Encoder](https://lame.sourceforge.io/) under the hood.
> This package, like the rest of Mediabunny, is enabled by its [sponsors](https://mediabunny.dev/#sponsors) and their donations. If you've derived value from this package, please consider leaving a donation! 💘
## Installation
This library peer-depends on Mediabunny. Install both using npm:
```bash
npm install mediabunny @mediabunny/mp3-encoder
```
Alternatively, directly include them using a script tag:
```html
<script src="mediabunny.js"></script>
<script src="mediabunny-mp3-encoder.js"></script>
```
This will expose the global objects `Mediabunny` and `MediabunnyMp3Encoder`. Use `mediabunny-mp3-encoder.d.ts` to provide types for these globals. You can download the built distribution files from the [releases page](https://github.com/Vanilagy/mediabunny/releases).
## Usage
```ts
import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
registerMp3Encoder();
```
That's it - Mediabunny now uses the registered MP3 encoder automatically.
If you want to be more correct, check for native browser support first:
```ts
import { canEncodeAudio } from 'mediabunny';
import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
if (!(await canEncodeAudio('mp3'))) {
registerMp3Encoder();
}
```
## Example
Here, we convert an input file to an MP3:
```ts
import {
Input,
ALL_FORMATS,
BlobSource,
Output,
BufferTarget,
Mp3OutputFormat,
canEncodeAudio,
Conversion,
} from 'mediabunny';
import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
if (!(await canEncodeAudio('mp3'))) {
// Only register the custom encoder if there's no native support
registerMp3Encoder();
}
const input = new Input({
source: new BlobSource(file), // From a file picker, for example
formats: ALL_FORMATS,
});
const output = new Output({
format: new Mp3OutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({
input,
output,
});
await conversion.execute();
output.target.buffer; // => ArrayBuffer containing the MP3 file
```
For more ways of using Mediabunny, refer to its [guide](https://mediabunny.dev/guide/introduction).
## License
`@mediabunny/mp3-encoder` uses [the same MPL-2.0 license](https://sourceforge.net/projects/lame/files/lame/) as Mediabunny. The LAME MP3 Encoder is licensed under LGPL. On their [license page](https://lame.sourceforge.io/license.txt), they ask that you give them a shoutout with a link.
## Implementation details
This library implements an MP3 encoder by registering a custom encoder class with Mediabunny. This class, when initialized, spawns a worker which then immediately loads a WASM build of the LAME MP3 encoder. Then, raw data is sent to the worker and encoded data is received from it. These encoded chunks are then concatenated in the main thread and properly split into separate MP3 frames.
Great care was put into ensuring maximum compatibility of this package; it works with bundlers, directly in the browser, as well as in Node, Deno, and Bun. All code (including worker & WASM) are bundled into a single file, eliminating the need for CDNs or WASM path arguments. This packages therefore serves as a reference implementation of WASM-based encoder extensions for Mediabunny.
The WASM build itself is a performance-optimized, SIMD-enabled build of LAME 3.100, with all unneeded features disabled. Because maximum performance was the priority, the build is slighter bigger, but ~130 kB gzipped is still very reasonable in my opinion. In my tests, it encodes 5 seconds of audio in ~90 milliseconds (55x real-time speed).
## Building and development
Building this library is done using the build commands in the [Mediabunny root](https://github.com/Vanilagy/mediabunny).
For simplicity, all built WASM artifacts are included in the repo, since these rarely change. However, here are the instructions for building them from scratch:
### Prerequisites
[Install Emscripten](https://emscripten.org/docs/getting_started/downloads.html). The recommended way is using the emsdk, which involves cloning a repo and running a few commands.
### Compiling LAME:
[Download the LAME source code](https://sourceforge.net/projects/lame/files/lame/) from SourceForge. Then, in the folder:
```bash
emconfigure ./configure \
CFLAGS="-DNDEBUG -DNO_STDIO -O3 -msimd128" \
--disable-dependency-tracking \
--disable-shared \
--disable-gtktest \
--disable-analyzer-hooks \
--disable-decoder \
--disable-frontend
emmake make clean
emmake make
```
This generates `libmp3lame.a`, located in `libmp3lame/.libs`.
### Compiling the LAME bridge:
Now with LAME built, we must build the C code at `src/lame-bridge.c` containing a lean bridge interface between JavaScript and LAME's API. Clone Mediabunny, and then, in `packages/mp3-encoder` and with `build/libmp3lame.a` in place, run:
```bash
emcc src/lame-bridge.c build/libmp3lame.a \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-s SINGLE_FILE=1 \
-s ENVIRONMENT=web,worker \
-s EXPORTED_RUNTIME_METHODS=cwrap,HEAPU8 \
-s EXPORTED_FUNCTIONS=_malloc,_free \
-msimd128 \
-O3 \
-o build/lame.js
```
This generates `build/lame.js`, which contains both the JavaScript "glue code" as well as the compiled WASM encoded with Base64.
### Building the package
Then, the complete JavaScript package can be built alongside the rest of Mediabunny by running `npm run build` in Mediabunny's root.
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "dist/modules/src/index.d.ts",
"bundledPackages": [],
"compiler": {},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "dist/mediabunny-mp3-encoder.d.ts"
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
}
}
},
"newlineKind": "lf"
}
File diff suppressed because one or more lines are too long
Binary file not shown.
+1342
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

+51
View File
@@ -0,0 +1,51 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.6.0",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"types": "./dist/modules/src/index.d.ts",
"exports": {
"types": "./dist/modules/src/index.d.ts",
"import": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"require": "./dist/bundles/mediabunny-mp3-encoder.mjs"
},
"files": [
"README.md",
"package.json",
"LICENSE",
"dist",
"src"
],
"sideEffects": false,
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/Vanilagy/mediabunny.git"
},
"bugs": {
"url": "https://github.com/Vanilagy/mediabunny/issues"
},
"homepage": "https://mediabunny.dev/",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/Vanilagy"
},
"peerDependencies": {
"mediabunny": "^1.0.0"
},
"devDependencies": {
"@types/emscripten": "^1.40.1"
},
"keywords": [
"mp3",
"encoding",
"codec",
"mediabunny",
"lame",
"browser",
"wasm",
"polyfill"
]
}
+212
View File
@@ -0,0 +1,212 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import createModule from '../build/lame';
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
type ExtendedEmscriptenModule = EmscriptenModule & {
cwrap: typeof cwrap;
};
let module: ExtendedEmscriptenModule;
let lamePtr: number;
let storedNumberOfChannels: number;
let storedSampleRate: number;
let storedBitrate: number;
let needsReset = false;
let initLame: (
numberOfChannels: number,
sampleRate: number,
bitrate: number
) => number;
let encodeSamples: (
lame: number,
leftPtr: number,
rightPtr: number,
sampleCount: number,
outputPtr: number,
outputSize: number
) => number;
let flushLame: (lame: number, outputPtr: number, outputSize: number) => number;
let closeLame: (lame: number) => void;
let inputSlice: Slice | null = null;
let outputSlice: Slice | null = null;
const init = async (numberOfChannels: number, sampleRate: number, bitrate: number) => {
storedNumberOfChannels = numberOfChannels;
storedSampleRate = sampleRate;
storedBitrate = bitrate;
module = (await createModule()) as ExtendedEmscriptenModule;
// Set up the functions
initLame = module.cwrap('init_lame', 'number', ['number', 'number', 'number']);
encodeSamples = module.cwrap(
'encode_samples',
'number',
['number', 'number', 'number', 'number', 'number', 'number'],
);
flushLame = module.cwrap('flush_lame', 'number', ['number', 'number', 'number']);
closeLame = module.cwrap('close_lame', null, ['number']);
lamePtr = initLame(numberOfChannels, sampleRate, bitrate);
};
const reset = () => {
closeLame(lamePtr);
lamePtr = initLame(storedNumberOfChannels, storedSampleRate, storedBitrate);
};
const encode = (audioData: ArrayBuffer, numberOfFrames: number) => {
if (needsReset) {
reset();
needsReset = false;
}
const audioBytes = new Uint8Array(audioData);
const sizePerChannel = audioBytes.length / storedNumberOfChannels;
inputSlice = maybeGrowSlice(inputSlice, audioBytes.length);
module.HEAPU8.set(audioBytes, inputSlice.ptr);
const requiredOutputSize = Math.ceil(1.25 * numberOfFrames + 7200);
outputSlice = maybeGrowSlice(outputSlice, requiredOutputSize);
const bytesWritten = encodeSamples(
lamePtr,
inputSlice.ptr,
inputSlice.ptr + (storedNumberOfChannels - 1) * sizePerChannel,
numberOfFrames,
outputSlice.ptr,
requiredOutputSize,
);
const result = module.HEAPU8.slice(outputSlice.ptr, outputSlice.ptr + bytesWritten);
return result.buffer;
};
const flush = () => {
if (needsReset) {
reset();
needsReset = false;
}
const requiredOutputSize = 7200;
outputSlice = maybeGrowSlice(outputSlice, requiredOutputSize);
const bytesWritten = flushLame(lamePtr, outputSlice.ptr, requiredOutputSize);
const result = module.HEAPU8.slice(outputSlice.ptr, outputSlice.ptr + bytesWritten);
needsReset = true; // After a flush, the encoder must be prepared to start a new encoding process
return result.buffer;
};
/** A "fat pointer" type thing. */
type Slice = {
ptr: number;
size: number;
};
/** Either returns the existing slice, or allocates a new one if there's no existing slice or it was too small. */
const maybeGrowSlice = (slice: Slice | null, requiredSize: number) => {
if (!slice || slice.size < requiredSize) {
if (slice) {
module._free(slice.ptr);
}
return {
ptr: module._malloc(requiredSize),
size: requiredSize,
};
}
return slice;
};
const onMessage = (data: { id: number; command: WorkerCommand }) => {
const { id, command } = data;
const handleCommand = async (): Promise<void> => {
try {
let result: WorkerResponseData;
const transferables: Transferable[] = [];
switch (command.type) {
case 'init': {
await init(
command.data.numberOfChannels,
command.data.sampleRate,
command.data.bitrate,
);
result = { success: true };
}; break;
case 'encode': {
const encodedData = encode(
command.data.audioData,
command.data.numberOfFrames,
);
result = { encodedData };
transferables.push(encodedData);
}; break;
case 'flush': {
const flushedData = flush();
result = { flushedData };
transferables.push(flushedData);
}; break;
}
const response: WorkerResponse = {
id,
success: true,
data: result,
};
sendMessage(response, transferables);
} catch (error) {
const response: WorkerResponse = {
id,
success: false,
error,
};
sendMessage(response);
}
};
void handleCommand();
};
const sendMessage = (data: unknown, transferables?: Transferable[]) => {
if (parentPort) {
parentPort.postMessage(data, transferables ?? []);
} else {
self.postMessage(data, { transfer: transferables ?? [] });
}
};
let parentPort: {
postMessage: (data: unknown, transferables?: Transferable[]) => void;
on: (event: string, listener: (data: never) => void) => void;
} | null = null;
if (typeof self === 'undefined') {
// We're in Node.js (or a runtime that mimics it)
const workerModule = 'worker_threads';
// eslint-disable-next-line @stylistic/max-len
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
parentPort = require(workerModule).parentPort;
}
if (parentPort) {
parentPort.on('message', onMessage);
} else {
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
}
+213
View File
@@ -0,0 +1,213 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, registerEncoder } from 'mediabunny';
import { FRAME_HEADER_SIZE, readFrameHeader } from '../../../shared/mp3-misc';
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
import createWorker from './encode.worker';
class Mp3Encoder extends CustomAudioEncoder {
private worker: Worker | null = null;
private nextMessageId = 0;
private pendingMessages = new Map<number, {
resolve: (value: WorkerResponseData) => void;
reject: (reason?: unknown) => void;
}>();
private buffer = new Uint8Array(2 ** 16);
private currentBufferOffset = 0;
private currentTimestamp = 0;
private chunkMetadata: EncodedAudioChunkMetadata = {};
static override supports(codec: AudioCodec, config: AudioDecoderConfig): boolean {
return codec === 'mp3'
&& (config.numberOfChannels === 1 || config.numberOfChannels === 2)
&& (config.sampleRate === 32000 || config.sampleRate === 44100 || config.sampleRate === 48000);
}
async init() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
this.worker = (await createWorker()) as Worker; // The actual encoding takes place in this worker
const onMessage = (data: WorkerResponse) => {
const pending = this.pendingMessages.get(data.id);
assert(pending !== undefined);
this.pendingMessages.delete(data.id);
if (data.success) {
pending.resolve(data.data);
} else {
pending.reject(data.error);
}
};
if (this.worker.addEventListener) {
this.worker.addEventListener('message', event => onMessage(event.data as WorkerResponse));
} else {
const nodeWorker = this.worker as unknown as {
on: (event: string, listener: (data: never) => void) => void;
};
nodeWorker.on('message', onMessage);
}
assert(this.config.bitrate);
await this.sendCommand({
type: 'init',
data: {
numberOfChannels: this.config.numberOfChannels,
sampleRate: this.config.sampleRate,
bitrate: this.config.bitrate,
},
});
this.chunkMetadata = {
decoderConfig: {
codec: 'mp3',
numberOfChannels: this.config.numberOfChannels,
sampleRate: this.config.sampleRate,
},
};
}
async encode(audioSample: AudioSample) {
const sizePerChannel = audioSample.allocationSize({
format: 's16-planar',
planeIndex: 0,
});
const requiredBytes = audioSample.numberOfChannels * sizePerChannel;
const audioData = new ArrayBuffer(requiredBytes);
const audioBytes = new Uint8Array(audioData);
for (let i = 0; i < audioSample.numberOfChannels; i++) {
audioSample.copyTo(audioBytes.subarray(i * sizePerChannel), {
format: 's16-planar', // LAME wants it in this format
planeIndex: i,
});
}
const result = await this.sendCommand({
type: 'encode',
data: {
audioData,
numberOfFrames: audioSample.numberOfFrames,
},
}, [audioData]);
assert('encodedData' in result);
this.digestOutput(new Uint8Array(result.encodedData));
}
async flush() {
const result = await this.sendCommand({ type: 'flush' });
assert('flushedData' in result);
this.digestOutput(new Uint8Array(result.flushedData));
}
close() {
this.worker?.terminate();
}
/**
* LAME returns data in chunks, but a chunk doesn't need to contain a full MP3 frame. Therefore, we must accumulate
* these chunks and extract the MP3 frames only when they're complete.
*/
private digestOutput(bytes: Uint8Array) {
const requiredBufferSize = this.currentBufferOffset + bytes.length;
if (requiredBufferSize > this.buffer.length) {
// Grow the buffer to the required size
const newSize = 1 << Math.ceil(Math.log2(requiredBufferSize));
const newBuffer = new Uint8Array(newSize);
newBuffer.set(this.buffer);
this.buffer = newBuffer;
}
this.buffer.set(bytes, this.currentBufferOffset);
this.currentBufferOffset = requiredBufferSize;
let pos = 0;
while (pos <= this.currentBufferOffset - FRAME_HEADER_SIZE) {
const word = new DataView(this.buffer.buffer).getUint32(pos, false);
const header = readFrameHeader(word, { pos, fileSize: null });
if (!header) {
break;
}
const fits = header.totalSize <= this.currentBufferOffset - pos;
if (!fits) {
// The frame isn't complete yet
break;
}
const data = this.buffer.slice(pos, pos + header.totalSize);
const duration = header.audioSamplesInFrame / header.sampleRate;
this.onPacket(new EncodedPacket(data, 'key', this.currentTimestamp, duration), this.chunkMetadata);
if (this.currentTimestamp === 0) {
this.chunkMetadata = {}; // Mimic WebCodecs-like behavior
}
this.currentTimestamp += duration;
pos += header.totalSize;
}
if (pos > 0) {
// Shift the data
this.buffer.set(this.buffer.subarray(pos, this.currentBufferOffset), 0);
this.currentBufferOffset -= pos;
}
}
private sendCommand(
command: WorkerCommand,
transferables?: Transferable[],
) {
return new Promise<WorkerResponseData>((resolve, reject) => {
const id = this.nextMessageId++;
this.pendingMessages.set(id, { resolve, reject });
assert(this.worker);
if (transferables) {
this.worker.postMessage({ id, command }, transferables);
} else {
this.worker.postMessage({ id, command });
}
});
}
}
/**
* Registers the LAME MP3 encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
* function before starting any encoding task.
*
* Preferably, wrap the call in a condition to avoid overriding any native MP3 encoder:
*
* ```ts
* import { canEncodeAudio } from 'mediabunny';
* import { registerMp3Encoder } from '@mediabunny/mp3-encoder';
*
* if (!(await canEncodeAudio('mp3'))) {
* registerMp3Encoder();
* }
* ```
*
* @public
*/
export const registerMp3Encoder = () => {
registerEncoder(Mp3Encoder);
};
function assert(x: unknown): asserts x {
if (!x) {
throw new Error('Assertion failed.');
}
}
+40
View File
@@ -0,0 +1,40 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#include <emscripten.h>
#include "../lib/lame.h"
EMSCRIPTEN_KEEPALIVE
lame_global_flags *init_lame(int number_of_channels, int sample_rate, int bitrate) {
lame_global_flags *gfp = lame_init();
lame_set_num_channels(gfp, number_of_channels);
lame_set_in_samplerate(gfp, sample_rate);
lame_set_out_samplerate(gfp, sample_rate);
lame_set_brate(gfp, bitrate / 1000); // MP3 wants "kilobitrate"
lame_set_bWriteVbrTag(gfp, 0);
int ret_code = lame_init_params(gfp);
return gfp;
}
EMSCRIPTEN_KEEPALIVE
int encode_samples(lame_global_flags *gfp, short int left_buf[], short int right_buf[], int sample_count, unsigned char *dest_buf, int dest_buf_size) {
return lame_encode_buffer(gfp, left_buf, right_buf, sample_count, dest_buf, dest_buf_size);
}
EMSCRIPTEN_KEEPALIVE
int flush_lame(lame_global_flags *gfp, unsigned char *dest_buf, int dest_buf_size) {
return lame_encode_flush(gfp, dest_buf, dest_buf_size);
}
EMSCRIPTEN_KEEPALIVE
void close_lame(lame_global_flags *gfp) {
lame_close(gfp);
}
+42
View File
@@ -0,0 +1,42 @@
/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
export type WorkerCommand = {
type: 'init';
data: {
numberOfChannels: number;
sampleRate: number;
bitrate: number;
};
} | {
type: 'encode';
data: {
audioData: ArrayBuffer;
numberOfFrames: number;
};
} | {
type: 'flush';
};
export type WorkerResponseData = {
success: boolean;
} | {
encodedData: ArrayBuffer;
} | {
flushedData: ArrayBuffer;
};
export type WorkerResponse = {
id: number;
} & ({
success: true;
data: WorkerResponseData;
} | {
success: false;
error: unknown;
});
+23
View File
@@ -0,0 +1,23 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../dist/modules",
"declaration": true,
"declarationMap": true,
"stripInternal": true,
"noEmit": false,
"moduleResolution": "nodenext",
"module": "nodenext",
"allowJs": true,
"paths": {
"mediabunny": ["../../../src/index.ts"],
},
},
"include": [
"**/*",
"../../../shared/**/*"
],
"references": [
{ "path": "../../../src" }
]
}
+125
View File
@@ -0,0 +1,125 @@
import * as esbuild from 'esbuild';
import process from 'node:process';
import PluginExternalGlobal from 'esbuild-plugin-external-global';
import { inlineWorkerPlugin } from './esbuild/inlined-workers.js';
/** Creates UMD and ESM variants, each unminified and minified. */
const createVariants = async (
entryPoint: string,
globalName: string,
outfileBase: string,
umdExtension: string,
specificUmdConfig: esbuild.BuildOptions = {},
specificEsmConfig: esbuild.BuildOptions = {},
) => {
const baseConfig: esbuild.BuildOptions = {
entryPoints: [entryPoint],
bundle: true,
logLevel: 'info',
banner: {
js: `/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/`,
},
legalComments: 'none',
};
const umdConfig: esbuild.BuildOptions = {
...baseConfig,
format: 'iife',
globalName,
footer: {
js:
`if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, ${globalName})`,
},
};
const esmConfig: esbuild.BuildOptions = {
...baseConfig,
format: 'esm',
};
const umdVariant = await esbuild.context({
...umdConfig,
...specificUmdConfig,
outfile: `${outfileBase}.${umdExtension}`,
});
const esmVariant = await esbuild.context({
...esmConfig,
...specificEsmConfig,
outfile: `${outfileBase}.mjs`,
});
const umdMinifiedVariant = await esbuild.context({
...umdConfig,
...specificUmdConfig,
outfile: `${outfileBase}.min.${umdExtension}`,
minify: true,
});
const esmMinifiedVariant = await esbuild.context({
...esmConfig,
...specificEsmConfig,
outfile: `${outfileBase}.min.mjs`,
minify: true,
});
return [umdVariant, esmVariant, umdMinifiedVariant, esmMinifiedVariant];
};
const mediabunnyVariants = await createVariants(
'src/index.ts',
'Mediabunny',
'dist/bundles/mediabunny',
'cjs',
);
const mp3EncoderVariants = await createVariants(
'packages/mp3-encoder/src/index.ts',
'MediabunnyMp3Encoder',
'packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder',
'js', // The bundles are purely for the browser, not for Node (due to the peer dependecy)
{
plugins: [
PluginExternalGlobal.externalGlobalPlugin({
mediabunny: 'Mediabunny',
}),
inlineWorkerPlugin({
define: {
'import.meta.url': '""',
},
legalComments: 'none',
}),
],
},
{
external: ['mediabunny'],
plugins: [
inlineWorkerPlugin({
define: {
'import.meta.url': '""',
},
legalComments: 'none',
}),
],
},
);
const contexts = [
...mediabunnyVariants,
...mp3EncoderVariants,
];
if (process.argv[2] === '--watch') {
await Promise.all(contexts.map(ctx => ctx.watch()));
} else {
for (const ctx of contexts) {
await ctx.rebuild();
await ctx.dispose();
}
}
+4 -1
View File
@@ -23,17 +23,20 @@ const checkFile = (filePath: string) => {
const checkDirectory = (dirPath: string) => {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
checkDirectory(fullPath);
} else if (entry.name.endsWith('.ts')) {
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.c')) {
checkFile(fullPath);
}
}
};
checkDirectory(path.join(__dirname, '..', 'src'));
checkDirectory(path.join(__dirname, '..', 'packages', 'mp3-encoder', 'src'));
checkDirectory(path.join(__dirname, '..', 'shared'));
if (missingFiles.length > 0) {
console.error('Files missing license header:');
+98
View File
@@ -0,0 +1,98 @@
// Adapted from https://github.com/mitschabaude/esbuild-plugin-inline-worker
import * as esbuild from 'esbuild';
import findCacheDir from 'find-cache-dir';
import fs from 'node:fs';
import path from 'node:path';
export function inlineWorkerPlugin(extraConfig: esbuild.BuildOptions): esbuild.Plugin {
return {
name: 'esbuild-plugin-inline-worker',
setup(build) {
build.onLoad(
{ filter: /\.worker\.(js|jsx|ts|tsx)$/ },
async ({ path: workerPath }) => {
const workerCode = await buildWorker(workerPath, extraConfig);
return {
contents: `import inlineWorker from '__inline-worker'
export default function Worker() {
return inlineWorker(${JSON.stringify(workerCode)});
}
`,
loader: 'js',
};
},
);
const inlineWorkerFunctionCode = `
export default async function inlineWorker(scriptText) {
if (typeof Worker !== 'undefined' && typeof Bun === 'undefined') {
// Browser, Deno
const blob = new Blob([scriptText], { type: "text/javascript" });
const url = URL.createObjectURL(blob);
const worker = new Worker(url, { type: typeof Deno !== 'undefined' ? 'module' : undefined }); // module for Deno
URL.revokeObjectURL(url);
return worker;
} else {
// Node, Bun (Bun's Worker is flaky, worker_threads works much better)
let Worker;
try {
Worker = (await import('worker_threads')).Worker;
} catch {
const workerModule = 'worker_threads';
Worker = require(workerModule).Worker;
}
const worker = new Worker(scriptText, { eval: true });
return worker;
}
}
`;
build.onResolve({ filter: /^__inline-worker$/ }, ({ path }) => {
return { path, namespace: 'inline-worker' };
});
build.onLoad({ filter: /.*/, namespace: 'inline-worker' }, () => {
return { contents: inlineWorkerFunctionCode, loader: 'js' };
});
},
};
}
const cacheDir = findCacheDir({
name: 'esbuild-plugin-inline-worker',
create: true,
});
if (cacheDir === undefined) {
throw new Error('Cache directory not found.');
}
async function buildWorker(workerPath: string, extraConfig: esbuild.BuildOptions) {
const scriptNameParts = path.basename(workerPath).split('.');
scriptNameParts.pop();
scriptNameParts.push('js');
const scriptName = scriptNameParts.join('.');
const bundlePath = path.resolve(cacheDir!, scriptName);
if (extraConfig) {
delete extraConfig.entryPoints;
delete extraConfig.outfile;
delete extraConfig.outdir;
}
await esbuild.build({
entryPoints: [workerPath],
bundle: true,
minify: true,
outfile: bundlePath,
target: 'es2017',
format: 'esm',
...extraConfig,
});
return fs.promises.readFile(bundlePath, { encoding: 'utf-8' });
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"composite": true,
"noEmit": false,
},
"include": [
"**/*"
]
}
+1 -1
View File
@@ -410,7 +410,7 @@ export class Conversion {
} catch (error) {
if (!this._canceled) {
// Make sure to cancel to stop other encoding processes and clean up resources
await this.cancel();
void this.cancel();
}
throw error;
+32 -4
View File
@@ -138,9 +138,23 @@ export const customAudioEncoders: typeof CustomAudioEncoder[] = [];
*/
export const registerDecoder = (decoder: typeof CustomVideoDecoder | typeof CustomAudioDecoder) => {
if (decoder.prototype instanceof CustomVideoDecoder) {
customVideoDecoders.push(decoder as typeof CustomVideoDecoder);
const casted = decoder as typeof CustomVideoDecoder;
if (customVideoDecoders.includes(casted)) {
console.warn('Video decoder already registered.');
return;
}
customVideoDecoders.push(casted);
} else if (decoder.prototype instanceof CustomAudioDecoder) {
customAudioDecoders.push(decoder as typeof CustomAudioDecoder);
const casted = decoder as typeof CustomAudioDecoder;
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.');
}
@@ -153,9 +167,23 @@ export const registerDecoder = (decoder: typeof CustomVideoDecoder | typeof Cust
*/
export const registerEncoder = (encoder: typeof CustomVideoEncoder | typeof CustomAudioEncoder) => {
if (encoder.prototype instanceof CustomVideoEncoder) {
customVideoEncoders.push(encoder as typeof CustomVideoEncoder);
const casted = encoder as typeof CustomVideoEncoder;
if (customVideoEncoders.includes(casted)) {
console.warn('Video encoder already registered.');
return;
}
customVideoEncoders.push(casted);
} else if (encoder.prototype instanceof CustomAudioEncoder) {
customAudioEncoders.push(encoder as typeof CustomAudioEncoder);
const casted = encoder as typeof CustomAudioEncoder;
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.');
}
+1 -1
View File
@@ -13,7 +13,7 @@ import { IsobmffReader } from './isobmff/isobmff-reader';
import { EBMLId, EBMLReader } from './matroska/ebml';
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
import { Mp3Demuxer } from './mp3/mp3-demuxer';
import { FRAME_HEADER_SIZE } from './mp3/mp3-misc';
import { FRAME_HEADER_SIZE } from '../shared/mp3-misc';
import { Mp3Reader } from './mp3/mp3-reader';
import { OggDemuxer } from './ogg/ogg-demuxer';
import { OggReader } from './ogg/ogg-reader';
+36 -25
View File
@@ -80,7 +80,8 @@ export abstract class MediaSource {
/** @internal */
async _start() {}
/** @internal */
async _flushAndClose() {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async _flushAndClose(forceClose: boolean) {}
/**
* Closes this source. This prevents future samples from being added and signals to the output file that no further
@@ -103,7 +104,7 @@ export abstract class MediaSource {
}
this._closingPromise = (async () => {
await this._flushAndClose();
await this._flushAndClose(false);
this._closed = true;
@@ -116,12 +117,12 @@ export abstract class MediaSource {
}
/** @internal */
async _flushOrWaitForClose() {
async _flushOrWaitForOngoingClose(forceClose: boolean) {
if (this._closingPromise) {
// Since closing also flushes, we don't want to do it twice
return this._closingPromise;
} else {
return this._flushAndClose();
return this._flushAndClose(forceClose);
}
}
}
@@ -451,14 +452,20 @@ class VideoEncoderWrapper {
})();
}
async flushAndClose() {
async flushAndClose(forceClose: boolean) {
this.checkForEncoderError();
if (this.customEncoder) {
void this.customEncoderCallSerializer.call(() => this.customEncoder!.flush());
if (!forceClose) {
void this.customEncoderCallSerializer.call(() => this.customEncoder!.flush());
}
await this.customEncoderCallSerializer.call(() => this.customEncoder!.close());
} else if (this.encoder) {
await this.encoder.flush();
if (!forceClose) {
await this.encoder.flush();
}
this.encoder.close();
}
@@ -512,8 +519,8 @@ export class VideoSampleSource extends VideoSource {
}
/** @internal */
override _flushAndClose() {
return this._encoder.flushAndClose();
override _flushAndClose(forceClose: boolean) {
return this._encoder.flushAndClose(forceClose);
}
}
@@ -564,8 +571,8 @@ export class CanvasSource extends VideoSource {
}
/** @internal */
override _flushAndClose() {
return this._encoder.flushAndClose();
override _flushAndClose(forceClose: boolean) {
return this._encoder.flushAndClose(forceClose);
}
}
@@ -714,7 +721,7 @@ export class MediaStreamVideoTrackSource extends VideoSource {
}
/** @internal */
override async _flushAndClose() {
override async _flushAndClose(forceClose: boolean) {
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
@@ -746,7 +753,7 @@ export class MediaStreamVideoTrackSource extends VideoSource {
});
}
await this._encoder.flushAndClose();
await this._encoder.flushAndClose(forceClose);
}
}
@@ -1226,14 +1233,20 @@ class AudioEncoderWrapper {
}
}
async flushAndClose() {
async flushAndClose(forceClose: boolean) {
this.checkForEncoderError();
if (this.customEncoder) {
void this.customEncoderCallSerializer.call(() => this.customEncoder!.flush());
if (!forceClose) {
void this.customEncoderCallSerializer.call(() => this.customEncoder!.flush());
}
await this.customEncoderCallSerializer.call(() => this.customEncoder!.close());
} else if (this.encoder) {
await this.encoder.flush();
if (!forceClose) {
await this.encoder.flush();
}
this.encoder.close();
}
@@ -1289,8 +1302,8 @@ export class AudioSampleSource extends AudioSource {
}
/** @internal */
override _flushAndClose() {
return this._encoder.flushAndClose();
override _flushAndClose(forceClose: boolean) {
return this._encoder.flushAndClose(forceClose);
}
}
@@ -1333,8 +1346,8 @@ export class AudioBufferSource extends AudioSource {
}
/** @internal */
override _flushAndClose() {
return this._encoder.flushAndClose();
override _flushAndClose(forceClose: boolean) {
return this._encoder.flushAndClose(forceClose);
}
}
@@ -1480,7 +1493,7 @@ export class MediaStreamAudioTrackSource extends AudioSource {
}
/** @internal */
override async _flushAndClose() {
override async _flushAndClose(forceClose: boolean) {
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
@@ -1493,7 +1506,7 @@ export class MediaStreamAudioTrackSource extends AudioSource {
await this._audioContext.suspend();
}
await this._encoder.flushAndClose();
await this._encoder.flushAndClose(forceClose);
}
}
@@ -1527,9 +1540,7 @@ type MediaStreamTrackProcessorControllerMessage = {
const mediaStreamTrackProcessorWorkerCode = () => {
const sendMessage = (message: MediaStreamTrackProcessorWorkerMessage, transfer?: Transferable[]) => {
if (transfer) {
// The error is bullshit, it's using the wrong postMessage
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
self.postMessage(message, transfer as any);
self.postMessage(message, { transfer });
} else {
self.postMessage(message);
}
+1 -1
View File
@@ -13,7 +13,7 @@ import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { PacketRetrievalOptions } from '../media-sink';
import { assert, binarySearchExact, binarySearchLessOrEqual, last, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { FrameHeader, getXingOffset, INFO, XING } from './mp3-misc';
import { FrameHeader, getXingOffset, INFO, XING } from '../../shared/mp3-misc';
import { Mp3Reader } from './mp3-reader';
type Sample = {
+1 -1
View File
@@ -12,7 +12,7 @@ import { Output, OutputAudioTrack } from '../output';
import { Mp3OutputFormat } from '../output-format';
import { EncodedPacket } from '../packet';
import { Writer } from '../writer';
import { getXingOffset, INFO, readFrameHeader, XING } from './mp3-misc';
import { getXingOffset, INFO, readFrameHeader, XING } from '../../shared/mp3-misc';
import { Mp3Writer, XingFrameData } from './mp3-writer';
export class Mp3Muxer extends Muxer {
+1 -1
View File
@@ -8,7 +8,7 @@
import { assert } from '../misc';
import { Reader } from '../reader';
import { FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from './mp3-misc';
import { FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from '../../shared/mp3-misc';
export class Mp3Reader {
pos = 0;
+1 -1
View File
@@ -14,7 +14,7 @@ import {
MPEG_V2_BITRATES,
SAMPLING_RATES,
XING,
} from './mp3-misc';
} from '../../shared/mp3-misc';
export type XingFrameData = {
mpegVersionId: number;
+2 -2
View File
@@ -380,7 +380,7 @@ export class Output<
const release = await this._mutex.acquire();
const promises = this._tracks.map(x => x.source._flushOrWaitForClose());
const promises = this._tracks.map(x => x.source._flushOrWaitForOngoingClose(true)); // Force close
await Promise.all(promises);
await this._writer.close();
@@ -410,7 +410,7 @@ export class Output<
const release = await this._mutex.acquire();
const promises = this._tracks.map(x => x.source._flushOrWaitForClose());
const promises = this._tracks.map(x => x.source._flushOrWaitForOngoingClose(false));
await Promise.all(promises);
await this._muxer.finalize();
+5 -2
View File
@@ -2,12 +2,15 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../dist/modules",
"rootDir": "..",
"declaration": true,
"declarationMap": true,
"stripInternal": true,
"noEmit": false
"noEmit": false,
"composite": true
},
"include": [
"**/*"
"**/*",
"../shared/**/*"
]
}
+6 -3
View File
@@ -6,12 +6,15 @@
"composite": true,
"noEmit": false,
"paths": {
"mediabunny": ["./dist/mediabunny.d.ts"],
"mediabunny": ["./src/index.ts"],
},
"types": ["vite/client"]
},
"include": [
"./vite.config.ts",
"./examples/**/*.ts",
]
"./examples/**/*.ts"
],
"references": [
{ "path": "./src" }
]
}