mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 08:43:49 +02:00
Compare commits
303
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
794b84884f | ||
|
|
45c34f9dcd | ||
|
|
3ec4061e03 | ||
|
|
bf592a87ca | ||
|
|
454476ab26 | ||
|
|
d2aea552d9 | ||
|
|
94d42667f1 | ||
|
|
0554696505 | ||
|
|
bfbd90e1d8 | ||
|
|
1183b542ee | ||
|
|
db75747533 | ||
|
|
3fcb93712d | ||
|
|
5b1dbf5558 | ||
|
|
9180af799d | ||
|
|
b8d5cc5b36 | ||
|
|
d5b07373b3 | ||
|
|
b99b86db0b | ||
|
|
b0e5f655aa | ||
|
|
0c2853d0aa | ||
|
|
cc970e1f53 | ||
|
|
5e68d9655c | ||
|
|
7171cf4338 | ||
|
|
baff6a67a3 | ||
|
|
d0401b42a5 | ||
|
|
72427b8b39 | ||
|
|
6812cf1643 | ||
|
|
c9a828c272 | ||
|
|
e851120bfe | ||
|
|
1a4fec9f6e | ||
|
|
8c8ce8a4a5 | ||
|
|
7ea8f6d751 | ||
|
|
8fb22633fe | ||
|
|
0480ecbd3a | ||
|
|
9547d36353 | ||
|
|
d8d22aa4eb | ||
|
|
50b281a9f8 | ||
|
|
409961c02b | ||
|
|
d8a6e09a85 | ||
|
|
2c7e8d85ca | ||
|
|
a9441aa9a9 | ||
|
|
403093c514 | ||
|
|
6c6687dfaf | ||
|
|
7d89c8de2d | ||
|
|
b4ef20cd36 | ||
|
|
a4884184e9 | ||
|
|
6f4a40ff4e | ||
|
|
7453336f70 | ||
|
|
bfbcea69b4 | ||
|
|
40e35d6f14 | ||
|
|
8716ddda14 | ||
|
|
8ae0f9d09e | ||
|
|
d5c94a23ab | ||
|
|
af618f014a | ||
|
|
54e74bfa4b | ||
|
|
3e686659d9 | ||
|
|
cdc2ef63dd | ||
|
|
92a384f418 | ||
|
|
5b890efc29 | ||
|
|
df76bae503 | ||
|
|
7f963367b5 | ||
|
|
bfd4059c22 | ||
|
|
b617c5f720 | ||
|
|
53833f6e94 | ||
|
|
794c97b91a | ||
|
|
5b4a5db1a5 | ||
|
|
2f0c040fcd | ||
|
|
b4a6757739 | ||
|
|
4affad934e | ||
|
|
324fae5153 | ||
|
|
08e3a85370 | ||
|
|
a5df6c7b89 | ||
|
|
ad167c77e7 | ||
|
|
8b9a1acf16 | ||
|
|
0da1107858 | ||
|
|
00599e556e | ||
|
|
1ddeae2e80 | ||
|
|
e1c6ab1866 | ||
|
|
bc533c71f7 | ||
|
|
879fd75547 | ||
|
|
210d382113 | ||
|
|
b4e51077aa | ||
|
|
c423d62fd9 | ||
|
|
0cdf0063e2 | ||
|
|
bb2f5d505d | ||
|
|
ebfb1e6d75 | ||
|
|
a294d943e6 | ||
|
|
8fbd31849b | ||
|
|
d92df801bf | ||
|
|
6832153fef | ||
|
|
82690ef327 | ||
|
|
c0d93f45a5 | ||
|
|
05f8ecd908 | ||
|
|
e0ced9901e | ||
|
|
96d0d9fb22 | ||
|
|
c6c768d93e | ||
|
|
5799cdf630 | ||
|
|
831d395696 | ||
|
|
288372ee71 | ||
|
|
4825818f9b | ||
|
|
b82ccc2c6a | ||
|
|
eb8b5e2534 | ||
|
|
9a8ccf6f6c | ||
|
|
1c12f2b66d | ||
|
|
441eea38b9 | ||
|
|
ef27d73405 | ||
|
|
41a331b0dc | ||
|
|
85a80972a0 | ||
|
|
f34e4cd369 | ||
|
|
149a2b4571 | ||
|
|
0c455236da | ||
|
|
41fb9657cb | ||
|
|
93e6de980d | ||
|
|
d77ddf033b | ||
|
|
b9f48ca9ef | ||
|
|
aa38385cae | ||
|
|
e6fe540cbc | ||
|
|
8bfedb0a88 | ||
|
|
d731037f6f | ||
|
|
f869358f93 | ||
|
|
f2bd003728 | ||
|
|
23e6901046 | ||
|
|
6c9d9a56b5 | ||
|
|
1c6c8c5217 | ||
|
|
f151ce85e8 | ||
|
|
045f82584c | ||
|
|
a0574fa09a | ||
|
|
4c7b6d939b | ||
|
|
deb9423ddb | ||
|
|
09a8ff0166 | ||
|
|
4047038113 | ||
|
|
b06bb65a0f | ||
|
|
699cfb25e6 | ||
|
|
14c534ed00 | ||
|
|
292189e3d4 | ||
|
|
d2d485196f | ||
|
|
af43f67835 | ||
|
|
633d1bc235 | ||
|
|
c0d28fa086 | ||
|
|
62ec685591 | ||
|
|
060ceb8ed3 | ||
|
|
5599e52a1a | ||
|
|
4e5f894d7a | ||
|
|
30b8754411 | ||
|
|
42b279e9b7 | ||
|
|
81531cc510 | ||
|
|
d8c8606341 | ||
|
|
1bf200894c | ||
|
|
a89d715524 | ||
|
|
b48eb75565 | ||
|
|
143ecea6c0 | ||
|
|
89ee88bc7c | ||
|
|
3ae73a7c5e | ||
|
|
1e200edc83 | ||
|
|
067081d5eb | ||
|
|
fff69619ec | ||
|
|
fc2f98825b | ||
|
|
3cde68e0ae | ||
|
|
ab9958234a | ||
|
|
f55a2b4764 | ||
|
|
9e3db7cf91 | ||
|
|
fff09411e6 | ||
|
|
1b1c9a0299 | ||
|
|
daf42181c9 | ||
|
|
f5bd540863 | ||
|
|
2b2e80dc24 | ||
|
|
e790f5ff97 | ||
|
|
6d9fb30c29 | ||
|
|
2e933ee5fc | ||
|
|
47ad5c6806 | ||
|
|
13d1e280e8 | ||
|
|
8955ba70c2 | ||
|
|
62287156d6 | ||
|
|
1680bd02ad | ||
|
|
100852f266 | ||
|
|
4810c13c29 | ||
|
|
9ed21bf9c6 | ||
|
|
f8f08a48e1 | ||
|
|
62dfc5dd1c | ||
|
|
782d3e134b | ||
|
|
9224fb886c | ||
|
|
a61631a299 | ||
|
|
f3dec587fd | ||
|
|
2d49122277 | ||
|
|
71dd34fc3c | ||
|
|
06a89ed085 | ||
|
|
487ae3c69a | ||
|
|
2207c7d996 | ||
|
|
7bd97b33cd | ||
|
|
e5dbf8a783 | ||
|
|
0d5bf18864 | ||
|
|
fc67e436e3 | ||
|
|
735a6622a2 | ||
|
|
d51903edfe | ||
|
|
6cdaa76fbb | ||
|
|
8efe7e75db | ||
|
|
a892676453 | ||
|
|
00f020921b | ||
|
|
8071933f15 | ||
|
|
d97611453b | ||
|
|
5e59ae4a1c | ||
|
|
3b148f961a | ||
|
|
95d60805ed | ||
|
|
857956fb07 | ||
|
|
e9fc97e070 | ||
|
|
15db8a27d4 | ||
|
|
637d77b2cf | ||
|
|
b6bb382783 | ||
|
|
7bb9961156 | ||
|
|
776ae43f50 | ||
|
|
e533bab18a | ||
|
|
26f3a576f3 | ||
|
|
1f6457a50c | ||
|
|
b2d00f84d8 | ||
|
|
f6aee7b414 | ||
|
|
0cb4665d8e | ||
|
|
525a03d220 | ||
|
|
808422f4f5 | ||
|
|
e6110a5b9b | ||
|
|
cd2ded8767 | ||
|
|
db91d07780 | ||
|
|
af57c431d3 | ||
|
|
049f24f2cb | ||
|
|
aa509cd62e | ||
|
|
60789841a8 | ||
|
|
b6d3e3269d | ||
|
|
cea3a40902 | ||
|
|
201a1b4066 | ||
|
|
882051daf2 | ||
|
|
91f96458fa | ||
|
|
9d87a3e580 | ||
|
|
040b0747be | ||
|
|
53923e72dc | ||
|
|
700a95ac35 | ||
|
|
46b3ce39fb | ||
|
|
14ae14b565 | ||
|
|
70e3e98b53 | ||
|
|
6b0527b691 | ||
|
|
3e333636ec | ||
|
|
a583b9d168 | ||
|
|
7d436c8f1f | ||
|
|
a56f9bd961 | ||
|
|
12b8a1ac5a | ||
|
|
29aaaf64bc | ||
|
|
6a772a329b | ||
|
|
81d3d1e6a4 | ||
|
|
5c45fd1f63 | ||
|
|
18d64ffb33 | ||
|
|
0dd53ff1f7 | ||
|
|
7232296217 | ||
|
|
30fc407c4c | ||
|
|
52fd8d0e19 | ||
|
|
a3e43987ed | ||
|
|
823ede1668 | ||
|
|
1703e70a16 | ||
|
|
df0932d49e | ||
|
|
64468b0e5b | ||
|
|
903e36430f | ||
|
|
dbb4150718 | ||
|
|
9d1f4a5f80 | ||
|
|
76ac4e7aba | ||
|
|
f0be3ef134 | ||
|
|
77fa426a9a | ||
|
|
b4cbf4c502 | ||
|
|
27bdd33d42 | ||
|
|
5972a5c906 | ||
|
|
da539cf495 | ||
|
|
987ae467ce | ||
|
|
2b5971d1ee | ||
|
|
d735f16ca5 | ||
|
|
9926004401 | ||
|
|
34628390b7 | ||
|
|
1e46df030f | ||
|
|
a6fd297573 | ||
|
|
447dd51e7b | ||
|
|
aebfede89a | ||
|
|
e0a1211140 | ||
|
|
aa8003e679 | ||
|
|
c74b6a44fb | ||
|
|
f21182a7b9 | ||
|
|
fb2dcba707 | ||
|
|
e25525a564 | ||
|
|
f356c05a9c | ||
|
|
4e1428fcef | ||
|
|
ca48f31032 | ||
|
|
b958dd84cc | ||
|
|
f72db4dfa2 | ||
|
|
e9534f57c6 | ||
|
|
31fdd363e3 | ||
|
|
14a55fc476 | ||
|
|
0fc3bb2b1d | ||
|
|
4f670bbe06 | ||
|
|
02f379d9c1 | ||
|
|
588495268c | ||
|
|
230778d887 | ||
|
|
32267f348a | ||
|
|
2aa0fd1433 | ||
|
|
bdc59306ed | ||
|
|
03c1c46f9a | ||
|
|
b4f76fc082 | ||
|
|
d55431dda5 | ||
|
|
6679b6b15d | ||
|
|
ad4dc659a5 | ||
|
|
95fbec86e8 |
@@ -0,0 +1,28 @@
|
||||
name: Build check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# Building the docs also means building the core library
|
||||
- name: Build everything
|
||||
run: npm run docs:build
|
||||
@@ -27,7 +27,4 @@ jobs:
|
||||
run: npm run check
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
run: npm run lint
|
||||
@@ -25,15 +25,6 @@ jobs:
|
||||
with:
|
||||
script: "core.setFailed('Non-prerelease releases must target the main branch (got: ${{ github.event.release.target_commitish }}).')"
|
||||
|
||||
- name: Merge main into release branch
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout release
|
||||
git merge origin/main --no-ff -m "Merge main into release for tag ${{ github.event.release.tag_name }}"
|
||||
git push origin release
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -60,8 +51,9 @@ jobs:
|
||||
- name: Run tests
|
||||
run: xvfb-run npm test
|
||||
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
# Building the docs also builds the core library
|
||||
- name: Build everything
|
||||
run: npm run docs:build
|
||||
|
||||
- name: Upload build artifacts
|
||||
env:
|
||||
@@ -93,9 +85,25 @@ jobs:
|
||||
packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.mjs
|
||||
packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.min.mjs
|
||||
packages/flac-encoder/dist/mediabunny-flac-encoder.d.ts
|
||||
|
||||
- name: Publish Mediabunny to npm
|
||||
run: npm publish --access public ${{ github.event.release.prerelease && '--tag beta' || '' }}
|
||||
packages/prores/dist/bundles/mediabunny-prores.js
|
||||
packages/prores/dist/bundles/mediabunny-prores.min.js
|
||||
packages/prores/dist/bundles/mediabunny-prores.mjs
|
||||
packages/prores/dist/bundles/mediabunny-prores.min.mjs
|
||||
packages/prores/dist/mediabunny-prores.d.ts
|
||||
packages/server/dist/bundles/mediabunny-server.cjs
|
||||
packages/server/dist/bundles/mediabunny-server.min.cjs
|
||||
packages/server/dist/bundles/mediabunny-server.mjs
|
||||
packages/server/dist/bundles/mediabunny-server.min.mjs
|
||||
packages/server/dist/mediabunny-server.d.ts
|
||||
|
||||
- name: Publish workspace packages to npm
|
||||
run: npm publish --access public --workspaces ${{ github.event.release.prerelease && '--tag beta' || '' }}
|
||||
|
||||
- name: Merge main into release branch
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout release
|
||||
git merge origin/main --no-ff -m "Merge main into release for tag ${{ github.event.release.tag_name }}"
|
||||
git push origin release
|
||||
|
||||
+3
-1
@@ -9,4 +9,6 @@ node_modules
|
||||
packages/mp3-encoder/dist
|
||||
packages/ac3/dist
|
||||
packages/aac-encoder/dist
|
||||
packages/flac-encoder/dist
|
||||
packages/flac-encoder/dist
|
||||
packages/server/dist
|
||||
packages/prores/dist
|
||||
Vendored
+2
-1
@@ -3,5 +3,6 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"js/ts.tsdk.path": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- Prefer functions declared using const, not using the function keyword
|
||||
- Code style is tab indent with semicolons
|
||||
- Mediabunny core code is contained in src/, extensions are in packages/*/, website is in docs/
|
||||
- Tests: Prefer fewer, longer test files over many small ones. Test files should be named after the general catergory of thing that is being tested, not after any individual single test.
|
||||
- Tests: Prefer fewer, longer test files over many small ones. Test files should be named after the general category of thing that is being tested, not after any individual single test.
|
||||
- Avoid ifs without a {} block. So no if (cond) return;, always do if (cond) { return; }
|
||||
- `type` instead of `interface` for object types
|
||||
- `type` instead of `interface` for object types
|
||||
|
||||
@@ -44,14 +44,6 @@ Mediabunny is a JavaScript library for reading, writing, and converting media (l
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Silver sponsors
|
||||
|
||||
<div align="center">
|
||||
<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">
|
||||
@@ -70,6 +62,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media (l
|
||||
<a href="https://jellypod.ai/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/jellypod.png" width="40" height="40" alt="Jellypod">
|
||||
</a>
|
||||
|
||||
<a href="https://www.photocircleapp.com/" target="_blank" rel="sponsored">
|
||||
<img src="./docs/public/sponsors/photocircle.png" width="40" height="40" alt="PhotoCircle">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
[Sponsor Mediabunny's development](https://github.com/sponsors/Vanilagy)
|
||||
@@ -78,14 +74,14 @@ Mediabunny is a JavaScript library for reading, writing, and converting media (l
|
||||
|
||||
Core features include:
|
||||
|
||||
- **Wide format support**: Read **and** write MP4, MOV, WebM, MKV, HLS, 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
|
||||
- Wide format support: Read _and_ write MP4, MOV, WebM, MKV, HLS, 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
|
||||
- Cross-platform: Works in all browsers as well as in Node, Bun, and Deno using [`@mediabunny/server`](https://github.com/Vanilagy/mediabunny/blob/main/packages/server/README.md)
|
||||
- Tree-shakable: Only bundle what you use (as small as 5 kB gzipped)
|
||||
- Zero dependencies: Implemented in highly performant TypeScript
|
||||
|
||||
[See full feature list](https://mediabunny.dev/guide/introduction#features)
|
||||
|
||||
|
||||
+29
-10
@@ -3,10 +3,12 @@
|
||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||
<script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script>
|
||||
<script src="../packages/ac3/dist/bundles/mediabunny-ac3.js"></script>
|
||||
<script src="../packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.js"></script>
|
||||
|
||||
<script type="module">
|
||||
//MediabunnyMp3Encoder.registerMp3Encoder();
|
||||
MediabunnyAc3.registerAc3Decoder();
|
||||
MediabunnyFlacEncoder.registerFlacEncoder();
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
@@ -23,7 +25,7 @@
|
||||
chunked: true,
|
||||
chunkSize: 2**20
|
||||
});
|
||||
const outputFormat = new Mediabunny.WavOutputFormat();
|
||||
const outputFormat = new Mediabunny.Mp4OutputFormat();
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'Capturing...';
|
||||
@@ -57,7 +59,7 @@
|
||||
const tracks = [];
|
||||
let start = 0;
|
||||
|
||||
if (true) {
|
||||
if (false) {
|
||||
input = new Mediabunny.Input({
|
||||
source: new Mediabunny.UrlSource('http://localhost:8000/index.m3u8'),
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
@@ -85,15 +87,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
const primaryTrack = await input.getPrimaryAudioTrack();
|
||||
const startTime = await primaryTrack.getFirstTimestamp();
|
||||
console.log(startTime)
|
||||
//const primaryTrack = await input.getPrimaryAudioTrack();
|
||||
//const startTime = await primaryTrack.getFirstTimestamp();
|
||||
//console.log(startTime)
|
||||
|
||||
let ctx = null;
|
||||
const canvas = new OffscreenCanvas(1280, 720);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
//let ctx = null;
|
||||
let conversion = await Mediabunny.Conversion.init({
|
||||
input,
|
||||
output,
|
||||
audio: (track) => ({ discard: track.number !== primaryTrack.number }),
|
||||
audio: {
|
||||
//codec: 'aac',
|
||||
//forceTranscode: true,
|
||||
//forceTranscode: true,
|
||||
//sampleFormat: 's16',
|
||||
},
|
||||
/*
|
||||
video: {
|
||||
discard: true,
|
||||
@@ -119,7 +129,10 @@
|
||||
},
|
||||
*/
|
||||
video: {
|
||||
discard: true,
|
||||
process: (sample) => {
|
||||
sample.draw(ctx, 0, 0, 1280, 720);
|
||||
return new Mediabunny.VideoSample(canvas, { timestamp: sample.timestamp, duration: sample.duration });
|
||||
},
|
||||
},
|
||||
tags: {} ?? {
|
||||
title: 'Bigggy',
|
||||
@@ -139,11 +152,17 @@
|
||||
}
|
||||
},
|
||||
trim: {
|
||||
start: startTime,
|
||||
end: startTime + 2,
|
||||
start: -2,
|
||||
end: 10,
|
||||
//start: 300.14984567374756 - 100,
|
||||
//end: 310.1548298151939 - 100,
|
||||
//end: 10,
|
||||
//start: startTime,
|
||||
//end: startTime + 2,
|
||||
},
|
||||
});
|
||||
//console.log(conversion);
|
||||
console.log(conversion.discardedTracks);
|
||||
|
||||
let progress = 0;
|
||||
conversion.onProgress = newProgress => progress = newProgress;
|
||||
|
||||
+70
-8
@@ -11,23 +11,84 @@
|
||||
document.body.append(fileInput);
|
||||
|
||||
fileInput.addEventListener('change', async () => {
|
||||
/*
|
||||
const file = fileInput.files[0];
|
||||
const input = new Mediabunny.Input({
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
source: new Mediabunny.BlobSource(file),
|
||||
});
|
||||
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
const sink3 = new Mediabunny.EncodedPacketSink(track);
|
||||
console.log((await sink3.getFirstPacket()).data.join(', '));
|
||||
return;
|
||||
console.log(await track.getDurationFromMetadata(), await track.computeDuration());
|
||||
const track = await input.getPrimaryVideoTrack();
|
||||
const packetSink = new Mediabunny.EncodedPacketSink(track);
|
||||
|
||||
//console.log(await input.getDurationFromMetadata(), await input.computeDuration());
|
||||
return;
|
||||
const first = await packetSink.getFirstPacket({ verifyKeyPackets: true });
|
||||
const second = await packetSink.getNextPacket(first, { verifyKeyPackets: true });
|
||||
const third = await packetSink.getNextPacket(second, { verifyKeyPackets: true });
|
||||
const fourth = await packetSink.getNextPacket(third, { verifyKeyPackets: true });
|
||||
console.log(first, second, third, fourth);
|
||||
|
||||
/*
|
||||
|
||||
for await (const packet of packetSink.packets()) {
|
||||
//console.log(packet);
|
||||
break;
|
||||
}
|
||||
|
||||
const sink = new Mediabunny.AudioSampleSink(track);
|
||||
|
||||
let count = 0;
|
||||
for await (const sample of sink.samples()) {
|
||||
const buf = new Float32Array(new ArrayBuffer(sample.allocationSize({ format: 'f32-planar', planeIndex: 0 })));
|
||||
sample.copyTo(buf, { format: 'f32-planar', planeIndex: 0 });
|
||||
let max = Math.max(...buf.map(x => Math.abs(x)));
|
||||
|
||||
console.log(sample, buf, max)
|
||||
|
||||
if (++count === 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
const sink = new Mediabunny.EncodedPacketSink(track);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet);
|
||||
}
|
||||
|
||||
const output = new Mediabunny.Output({
|
||||
format: new Mediabunny.Mp4OutputFormat(),
|
||||
target: new Mediabunny.BufferTarget(),
|
||||
});
|
||||
|
||||
const conversion = await Mediabunny.Conversion.init({ input, output });
|
||||
await conversion.execute();
|
||||
|
||||
return;
|
||||
|
||||
// Download it now
|
||||
const blob = new Blob([output.target.buffer]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = file.name.replace(/\.\w+$/, '.mp4');
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
*/
|
||||
|
||||
/*
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
const sink = new Mediabunny.EncodedPacketSink(track);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet);
|
||||
}
|
||||
|
||||
console.log("Done")
|
||||
*/
|
||||
|
||||
/*
|
||||
const input = new Mediabunny.Input({
|
||||
source: new Mediabunny.UrlSource('https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8'),
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
@@ -36,6 +97,7 @@
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||
console.log(await sink.getFirstPacket());
|
||||
*/
|
||||
|
||||
/*
|
||||
return
|
||||
|
||||
+115
-11
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @stylistic/max-len */
|
||||
import { withMermaid } from 'vitepress-plugin-mermaid';
|
||||
import footnote from 'markdown-it-footnote';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
@@ -7,9 +8,12 @@ import { HeadConfig } from 'vitepress';
|
||||
// @ts-ignore This file gets generated once docs:generate is run
|
||||
import apiRoutes from '../api/index.json';
|
||||
import m3u8Grammar from './m3u8-grammar.json' with { type: 'json' };
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,'
|
||||
+ ' and faster than anybunny else.';
|
||||
const ORIGIN = 'https://mediabunny.dev';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default withMermaid({
|
||||
@@ -17,22 +21,31 @@ export default withMermaid({
|
||||
description: DESCRIPTION,
|
||||
cleanUrls: true,
|
||||
sitemap: {
|
||||
hostname: 'https://mediabunny.dev',
|
||||
hostname: ORIGIN,
|
||||
transformItems: async (items) => {
|
||||
const entries = await fs.readdir('./examples');
|
||||
for (const entry of entries) {
|
||||
const isDirectory = await fs.stat(path.join('./examples', entry)).then(stat => stat.isDirectory());
|
||||
if (isDirectory) {
|
||||
items.push({
|
||||
url: `/examples/${entry}/`, // With trailing slash
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
},
|
||||
lastUpdated: true,
|
||||
// lastUpdated: true,
|
||||
head: [
|
||||
['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }],
|
||||
['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }],
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:site_name', content: 'Mediabunny' }],
|
||||
['meta', { property: 'og:url', content: 'https://mediabunny.dev/' }],
|
||||
['meta', { property: 'og:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }],
|
||||
['meta', { property: 'og:image', content: `${ORIGIN}/mediabunny-og-image.png` }],
|
||||
['meta', { property: 'og:locale', content: 'en-US' }],
|
||||
['meta', { property: 'og:description', content: DESCRIPTION }],
|
||||
['meta', { name: 'twitter:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }],
|
||||
['meta', { name: 'twitter:image', content: `${ORIGIN}/mediabunny-og-image.png` }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
['meta', { name: 'twitter:site', content: '@vanilagy' }],
|
||||
['meta', { name: 'twitter:description', content: DESCRIPTION }],
|
||||
],
|
||||
themeConfig: {
|
||||
logo: '/mediabunny-logo.svg',
|
||||
@@ -40,12 +53,12 @@ export default withMermaid({
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Guide', link: '/guide/introduction', activeMatch: '/guide' },
|
||||
{ text: 'API', link: '/api', activeMatch: '/api' },
|
||||
{ text: 'API', link: '/api/', activeMatch: '/api/' }, // Trailing slash because it's index.html from there
|
||||
{ text: 'LLMs', link: '/llms', activeMatch: '/llms' },
|
||||
{ text: 'Examples', link: '/examples', activeMatch: '/examples' },
|
||||
{ text: 'Blog', link: '/blog', activeMatch: '/blog' },
|
||||
{ text: 'Sponsors', link: '/#sponsors', activeMatch: '/#sponsors' },
|
||||
{ text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license' },
|
||||
{ text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license', rel: 'noopener' },
|
||||
{
|
||||
text: 'More',
|
||||
items: [
|
||||
@@ -103,10 +116,12 @@ export default withMermaid({
|
||||
{
|
||||
text: 'Extensions',
|
||||
items: [
|
||||
{ text: 'server', link: '/guide/extensions/server' },
|
||||
{ text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' },
|
||||
{ text: 'aac-encoder', link: '/guide/extensions/aac-encoder' },
|
||||
{ text: 'ac3', link: '/guide/extensions/ac3' },
|
||||
{ text: 'flac-encoder', link: '/guide/extensions/flac-encoder' },
|
||||
{ text: 'prores', link: '/guide/extensions/prores' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -129,6 +144,7 @@ export default withMermaid({
|
||||
{ text: 'VP8', link: '/codec-registry/vp8' },
|
||||
{ text: 'VP9', link: '/codec-registry/vp9' },
|
||||
{ text: 'AV1', link: '/codec-registry/av1' },
|
||||
{ text: 'ProRes', link: '/codec-registry/prores' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -186,6 +202,8 @@ export default withMermaid({
|
||||
llmstxt({
|
||||
ignoreFiles: [
|
||||
'api/*',
|
||||
'examples.md',
|
||||
'llms.md',
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -197,9 +215,95 @@ export default withMermaid({
|
||||
title += ' | Mediabunny';
|
||||
}
|
||||
|
||||
const canonicalUrl = `${ORIGIN}/${pageData.relativePath}`
|
||||
.replace(/index\.md$/, '')
|
||||
.replace(/\.md$/, '');
|
||||
const isBlogPost = canonicalUrl.includes('/blog/');
|
||||
|
||||
const breadcrumbs: object[] = [];
|
||||
|
||||
if (canonicalUrl.includes('/guide/')) {
|
||||
breadcrumbs.push({
|
||||
'@type': 'ListItem',
|
||||
'position': 1,
|
||||
'name': 'Guide',
|
||||
'item': `${ORIGIN}/guide`,
|
||||
}, {
|
||||
'@type': 'ListItem',
|
||||
'position': 2,
|
||||
'name': pageData.title,
|
||||
});
|
||||
}
|
||||
|
||||
if (canonicalUrl.includes('/api/')) {
|
||||
breadcrumbs.push({
|
||||
'@type': 'ListItem',
|
||||
'position': 1,
|
||||
'name': 'API docs',
|
||||
'item': `${ORIGIN}/api/`,
|
||||
}, {
|
||||
'@type': 'ListItem',
|
||||
'position': 2,
|
||||
'name': pageData.title,
|
||||
});
|
||||
}
|
||||
|
||||
if (canonicalUrl.includes('/codec-registry/')) {
|
||||
breadcrumbs.push({
|
||||
'@type': 'ListItem',
|
||||
'position': 1,
|
||||
'name': 'Codec registry',
|
||||
'item': `${ORIGIN}/codec-registry/overview`,
|
||||
}, {
|
||||
'@type': 'ListItem',
|
||||
'position': 2,
|
||||
'name': pageData.title,
|
||||
});
|
||||
}
|
||||
|
||||
((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push(
|
||||
['meta', { property: 'og:type', content: isBlogPost ? 'article' : 'website' }],
|
||||
['meta', { property: 'og:title', content: title }],
|
||||
['meta', { property: 'twitter:title', content: title }],
|
||||
['meta', { property: 'og:description', content: pageData.description || DESCRIPTION }],
|
||||
['meta', { property: 'og:url', content: canonicalUrl }],
|
||||
['meta', { name: 'twitter:title', content: title }],
|
||||
['meta', { name: 'twitter:description', content: pageData.description || DESCRIPTION }],
|
||||
['link', { rel: 'canonical', href: canonicalUrl }],
|
||||
);
|
||||
|
||||
if (isBlogPost) {
|
||||
((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push(
|
||||
['meta', { property: 'article:published_time', content: String(pageData.frontmatter['publishedOnIso']) }],
|
||||
['meta', { property: 'article:author', content: String(pageData.frontmatter['author']) }],
|
||||
);
|
||||
|
||||
breadcrumbs.push({
|
||||
'@type': 'ListItem',
|
||||
'position': 1,
|
||||
'name': 'Blog posts',
|
||||
'item': `${ORIGIN}/blog`,
|
||||
}, {
|
||||
'@type': 'ListItem',
|
||||
'position': 2,
|
||||
'name': pageData.title,
|
||||
});
|
||||
}
|
||||
|
||||
if (breadcrumbs.length > 0) {
|
||||
((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push(
|
||||
['script', { type: 'application/ld+json' }, JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
'itemListElement': breadcrumbs,
|
||||
})],
|
||||
);
|
||||
}
|
||||
},
|
||||
buildEnd: async () => {
|
||||
const files = await fs.readdir('./docs/api');
|
||||
|
||||
for (const file of files) {
|
||||
await fs.copyFile('./docs/api/' + file, './dist-docs/api/' + file);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"heading": "Mediabunny API reference",
|
||||
"intro": "Here you can find detailed documentation for all classes, functions, constants and types exposed by Mediabunny's public API.",
|
||||
"description": "Detailed documentation for all classes, functions, constants and types exposed by Mediabunny's public API.",
|
||||
|
||||
"Samples": "Raw, unencoded chunks of media data, such as video frames or sections of audio.",
|
||||
"Packets": "Chunks of encoded media data.",
|
||||
@@ -18,10 +19,13 @@
|
||||
"Encoding": "Encoder configuration and encodability checks.",
|
||||
"Decoding": "Decoder configuration and decodability checks.",
|
||||
"Custom coders": "API for adding custom encoders and decoders.",
|
||||
"Logging": "Control over what Mediabunny logs to the console.",
|
||||
"Miscellaneous": "Whatever's left.",
|
||||
|
||||
"@mediabunny/server": "Adds full video/audio decoder and encoder support to Mediabunny running in server-side environments such as Node, Bun, or Deno.",
|
||||
"@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny.",
|
||||
"@mediabunny/ac3": "Adds AC-3/E-AC-3 decoder and encoder support to Mediabunny.",
|
||||
"@mediabunny/aac-encoder": "Polyfills AAC encoder support to Mediabunny.",
|
||||
"@mediabunny/flac-encoder": "Adds FLAC encoder support to Mediabunny."
|
||||
"@mediabunny/flac-encoder": "Adds FLAC encoder support to Mediabunny.",
|
||||
"@mediabunny/prores": "Adds Apple ProRes decoder support to Mediabunny."
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
---
|
||||
title: Blog posts
|
||||
description: Announcements, news, and devblogs about Mediabunny.
|
||||
outline: false
|
||||
---
|
||||
|
||||
@@ -12,7 +14,7 @@ import BlogAuthor from './components/BlogAuthor.vue';
|
||||
<template v-for="(post, i) in data">
|
||||
<a :href="post.url" class="flex flex-col sm:flex-row items-start sm:gap-8 !text-inherit !no-underline ![font-weight:inherit] group">
|
||||
<div>
|
||||
<img src="./assets/on-air.png" class="shrink-0 sm:w-40 rounded" />
|
||||
<img :src="post.frontmatter.headerImage" class="shrink-0 sm:w-40 rounded" />
|
||||
<div v-if="false" class="p-1">
|
||||
<BlogAuthor :frontmatter="post.frontmatter" small />
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
---
|
||||
title: Mediabunny now supports HLS!
|
||||
description: Mediabunny v1.42.0 adds read/write support for HTTP Live Streaming (HLS), pushing the frontier of fully client-side media processing.
|
||||
publishedOn: April 24, 2026
|
||||
publishedOnIso: "2026-04-24"
|
||||
author: Vanilagy
|
||||
authorImage: /vani.png
|
||||
authorLink: https://github.com/Vanilagy
|
||||
authorSubtitle: Creator of Mediabunny
|
||||
headerImage: /on-air.png
|
||||
excerpt: Mediabunny v1.42.0 adds read/write support for HTTP Live Streaming (HLS). This has been in the making for the last ~4 months and is, by far, the biggest addition to Mediabunny since its original release.
|
||||
---
|
||||
|
||||
@@ -12,7 +15,7 @@ excerpt: Mediabunny v1.42.0 adds read/write support for HTTP Live Streaming (HLS
|
||||
import BlogAuthor from '../components/BlogAuthor.vue';
|
||||
</script>
|
||||
|
||||
<img src="../assets/on-air.png" class="rounded-2xl mb-2">
|
||||
<img :src="$frontmatter.headerImage" class="rounded-2xl mb-2" />
|
||||
|
||||
<p class="!m-0 opacity-70">{{ $frontmatter.publishedOn }}</p>
|
||||
|
||||
@@ -77,7 +80,7 @@ The API surface added by the HLS update is vast and I obviously can't cover it i
|
||||
|
||||
By using the Conversion API, you can just do this:
|
||||
|
||||
<div class="text-xs">
|
||||
<div class="text-[13.7142857143px]">
|
||||
|
||||
```ts
|
||||
import { ... } from 'mediabunny';
|
||||
@@ -107,7 +110,7 @@ That's it. This will stream-download the entire HLS playlist, transcode it if ne
|
||||
|
||||
This is basically the inverse of the previous example. Just like we're able to read HLS and turn it into an MP4, we're able to read any input file and turn it into a full HLS playlist including master playlist, media playlists and segments:
|
||||
|
||||
<div class="text-xs">
|
||||
<div class="text-[13.7142857143px]">
|
||||
|
||||
```ts
|
||||
import { ... } from 'mediabunny';
|
||||
@@ -163,7 +166,7 @@ No transcode server is needed here, it's all handled by the client, and the serv
|
||||
|
||||
You could build an OBS-like broadcasting system where a user records their screen, facecam or microphone, encodes multiple variants locally, and then broadcasts finished HLS segments directly to the server, meaning no transcoding is needed.
|
||||
|
||||
<div class="text-xs overflow-auto">
|
||||
<div class="text-[13.7142857143px] overflow-auto">
|
||||
|
||||
```ts
|
||||
// Get the screen and mic
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Advanced Audio Coding (AAC) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Dolby Digital (AC-3) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: A-law companded PCM audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: AOMedia Video 1 (AV1) video codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Advanced Video Coding (H.264) video codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Dolby Digital Plus (E-AC-3) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Free Lossless Audio Codec (FLAC) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: High Efficiency Video Coding (H.265) video codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: MP3 (MPEG-1/2 Audio Layer III) audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Opus audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: The Mediabunny Codec Registry formalizes the precise definitions of all video and audio codecs supported by Mediabunny, like expected codec string and packet data format.
|
||||
---
|
||||
|
||||
# Mediabunny Codec Registry
|
||||
|
||||
The Mediabunny Codec Registry formalizes the precise definitions of all video and audio codecs supported by Mediabunny. More specifically, for any given codec, it describes the format that `EncodedPacket`, `VideoDecoderConfig` and `AudioDecoderConfig` must adhere to. All packets coming out of or going into Mediabunny are expected to adhere to this registry.
|
||||
@@ -11,6 +15,7 @@ The registry is an extension of the [WebCodecs Codec Registry](https://www.w3.or
|
||||
- [VP8](./vp8)
|
||||
- [VP9](./vp9)
|
||||
- [AV1](./av1)
|
||||
- [ProRes](./prores)
|
||||
|
||||
## Audio codecs
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: PCM audio codec definitions, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
description: Apple ProRes video codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
<VPBadge type="info" text="Video codec" />
|
||||
|
||||
# ProRes codec registration
|
||||
|
||||
## Description
|
||||
|
||||
The Apple ProRes video codec, as specified in [SMPTE RDD 36](https://pub.smpte.org/doc/rdd36/).
|
||||
|
||||
## Codec ID
|
||||
|
||||
```ts
|
||||
'prores'
|
||||
```
|
||||
|
||||
## `EncodedPacket` data
|
||||
|
||||
The packet's data must be a `frame()` as defined in [SMPTE RDD 36](https://pub.smpte.org/doc/rdd36/).
|
||||
|
||||
## `EncodedPacket` type
|
||||
|
||||
Since ProRes is intra-frame-only, every packet is a key frame and its type is therefore always `'key'`.
|
||||
|
||||
## `VideoDecoderConfig` codec string
|
||||
|
||||
The codec string must be one of the six four-character codes identifying the ProRes profile:
|
||||
|
||||
- `'ap4x'` - ProRes 4444 XQ
|
||||
- `'ap4h'` - ProRes 4444
|
||||
- `'apch'` - ProRes 422 High Quality
|
||||
- `'apcn'` - ProRes 422 Standard Definition
|
||||
- `'apcs'` - ProRes 422 LT
|
||||
- `'apco'` - ProRes 422 Proxy
|
||||
|
||||
## `VideoDecoderConfig` description
|
||||
|
||||
`description` is not used for this codec.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: μ-law companded PCM audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Vorbis audio codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: VP8 video codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: VP9 video codec definition, defining legal codec strings, decoder configs, and packet data formats.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { VPBadge } from 'vitepress/theme'
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
description: "Demos showcasing the various features of Mediabunny: metadata extraction, thumbnail generation, compression, media playback, live streaming, and more."
|
||||
layout: home
|
||||
title: Examples
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Efficiently convert any media file to any format, directly in the browser. Optionally apply trimming, video resizing, rotation, custom overlays, and more.
|
||||
---
|
||||
|
||||
# Converting media files
|
||||
|
||||
The [reading](./reading-media-files) and [writing](./writing-media-files) primitives in Mediabunny provide everything you need to convert media files. However, since this is such a common operation and the details can be tricky, Mediabunny ships with a built-in file conversion abstraction.
|
||||
@@ -242,6 +246,7 @@ type ConversionAudioOptions = {
|
||||
bitrate?: number | Quality;
|
||||
numberOfChannels?: number;
|
||||
sampleRate?: number;
|
||||
sampleFormat?: 'u8' | 's16' | 's32' | 'f32';
|
||||
forceTranscode?: boolean;
|
||||
process?: (sample: AudioSample) => MaybePromise<
|
||||
AudioSample | AudioSample[] | null
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: The @mediabunny/aac-encoder extension provides a fast AAC encoder polyfill for use in the browser and on the server.
|
||||
---
|
||||
|
||||
# @mediabunny/aac-encoder
|
||||
|
||||
Some browsers lack support for AAC encoding in their WebCodecs implementations. This extension package provides a reliable AAC-LC encoder for use with Mediabunny. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [FFmpeg](https://ffmpeg.org/)'s AAC encoder under the hood.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: The @mediabunny/ac3 extension provides fast AC-3 and E-AC-3 decoders and encoders for both browser and server environments.
|
||||
---
|
||||
|
||||
# @mediabunny/ac3
|
||||
|
||||
Browsers have no support for AC-3 (Dolby Digital) or E-AC-3 (Dolby Digital Plus) in their WebCodecs implementations. This extension package provides both a decoder and encoder for use with Mediabunny, allowing you to decode and encode these codecs directly in the browser. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [FFmpeg](https://ffmpeg.org/)'s AC-3 and E-AC-3 coders under the hood.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: The @mediabunny/flac-encoder extension provides a highly-performant FLAC encoder polyfill for use in the browser and on the server.
|
||||
---
|
||||
|
||||
# @mediabunny/flac-encoder
|
||||
|
||||
No browser currently supports FLAC encoding in their WebCodecs implementations. This extension package provides a reliable FLAC encoder for use with Mediabunny. It is implemented using Mediabunny's [custom coder API](../supported-formats-and-codecs#custom-coders) and uses a fast, size-optimized WASM build of [libFLAC](https://github.com/xiph/flac) under the hood.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: The @mediabunny/mp3-encoder extension provides a highly-performant MP3 encoder polyfill for use in the browser and on the server.
|
||||
---
|
||||
|
||||
# @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.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: The @mediabunny/prores extension provides an extremely fast Apple ProRes decoder for the browser.
|
||||
---
|
||||
|
||||
# @mediabunny/prores
|
||||
|
||||
Browsers have no support for Apple ProRes in their WebCodecs implementations. This extension package provides a decoder for use with Mediabunny, allowing you to decode ProRes directly in the browser at unprecedented speed. It is implemented using Mediabunny's [custom coder API](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) and uses [TurboRes](https://github.com/Vanilagy/turbores), an extremely fast WASM-based ProRes decoder, 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/prores/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/prores
|
||||
```
|
||||
|
||||
Alternatively, directly include them using a script tag:
|
||||
```html
|
||||
<script src="mediabunny.js"></script>
|
||||
<script src="mediabunny-prores.js"></script>
|
||||
```
|
||||
|
||||
This will expose the global objects `Mediabunny` and `MediabunnyProres`. Use `mediabunny-prores.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).
|
||||
|
||||
## Setup
|
||||
|
||||
`@mediabunny/prores` can make use of shared-memory multithreading to achieve maximum performance. To enable this in browsers, your website must be cross-origin isolated by setting the following response headers:
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
Alternatively, you can use:
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: credentialless
|
||||
```
|
||||
which is generally more permissive but is not supported in Safari (of course).
|
||||
|
||||
---
|
||||
|
||||
If you cannot enable cross-origin isolation, `@mediabunny/prores` will fall back to a slower multithreading algorithm.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { registerProresDecoder } from '@mediabunny/prores';
|
||||
|
||||
registerProresDecoder();
|
||||
```
|
||||
That's it - Mediabunny now uses the registered ProRes decoder automatically.
|
||||
@@ -0,0 +1,318 @@
|
||||
---
|
||||
description: Add full video and audio decoder and encoder support for use in server-side environments, like Node, Bun, or Deno.
|
||||
---
|
||||
|
||||
# @mediabunny/server
|
||||
|
||||
By default, Mediabunny requires a browser environment for full access to decoders, encoders, and video processing features. `@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) to polyfill this functionality for server-side environments such as Node, Bun, or Deno, enabling the usage of all Mediabunny features on the server. The result is a server-side media processing API that integrates naturally with TypeScript as opposed to the awkwardness and inefficiencies of calling out to the FFmpeg CLI.
|
||||
|
||||
Features added by this package include:
|
||||
- Video decoders and encoders for AVC (H.264), HEVC (H.265), VP8, VP9, AV1, and ProRes. Supports both length-prefixed and Annex B AVC/HEVC as well as transparent video via VP9 and ProRes.
|
||||
- Audio decoders and encoders for AAC, MP3, Vorbis, Opus, FLAC, AC-3 and E-AC-3. Supports AAC in both AAC and ADTS formats.
|
||||
- Video frame transformation support (resize, rotate, crop)
|
||||
- Automatic hardware acceleration on all platforms (macOS, Linux, Windows)
|
||||
- Built-in multithreading
|
||||
- Zero-copy decode and encode paths
|
||||
|
||||
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="https://github.com/Vanilagy/mediabunny/blob/main/packages/server/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/server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { registerMediabunnyServer } from '@mediabunny/server';
|
||||
registerMediabunnyServer();
|
||||
```
|
||||
|
||||
That's it - you now have access to the full Mediabunny feature set on the server.
|
||||
|
||||
---
|
||||
|
||||
An optional `options` parameter is available for further configuration:
|
||||
```ts
|
||||
import { registerMediabunnyServer } from '@mediabunny/server';
|
||||
import * as NodeAv from 'node-av';
|
||||
|
||||
registerMediabunnyServer({
|
||||
// Use a specific hardware rendering device:
|
||||
hardwareContext: NodeAv.HardwareContext.create(
|
||||
NodeAv.AV_HWDEVICE_TYPE_VAAPI,
|
||||
'/dev/dri/renderD128',
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
## Upload media compression example
|
||||
|
||||
Here, we set up a simple media compression server in Node.js. The client's request body is streamed to Mediabunny, the media gets processed, and the output is streamed directly to the disk. Memory usage is O(1) due to pipelining, and an overly fast uploader is automatically slowed down due to stream backpressure.
|
||||
|
||||
```ts
|
||||
import { ALL_FORMATS, Conversion, FilePathTarget, Input, Mp4OutputFormat, Output, QUALITY_MEDIUM, ReadableStreamSource } from "mediabunny";
|
||||
import { registerMediabunnyServer } from "@mediabunny/server";
|
||||
import { Readable } from "node:stream";
|
||||
import http from "node:http";
|
||||
|
||||
registerMediabunnyServer();
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// Read the request body as a stream
|
||||
const stream = Readable.toWeb(req) as ReadableStream<Uint8Array>;
|
||||
const input = new Input({
|
||||
source: new ReadableStreamSource(stream),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
// Stream the output directly to the disk, could also stream to S3 etc.
|
||||
const output = new Output({
|
||||
format: new Mp4OutputFormat(),
|
||||
target: new FilePathTarget(`./converted-${crypto.randomUUID()}.mp4`),
|
||||
});
|
||||
|
||||
try {
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: async track => ({
|
||||
codec: 'avc',
|
||||
height: Math.min(720, await track.getDisplayHeight()),
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
}),
|
||||
});
|
||||
await conversion.execute();
|
||||
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
} catch (error) {
|
||||
res.statusCode = 500;
|
||||
res.end();
|
||||
|
||||
console.error("Error processing media:", error);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
For all the other ways to use Mediabunny, refer to its [guide](https://mediabunny.dev/guide/introduction).
|
||||
|
||||
## Performance
|
||||
|
||||
`@mediabunny/server` is extremely performant as it is a thin wrapper around [NodeAV](https://github.com/seydx/node-av), which itself is a thin wrapper around the FFmpeg C API. All decoders and encoders automatically run on separate threads, keeping the main thread unblocked. Hardware acceleration is automatically detected and utilized on all operating systems whenever available (unless explicitly disabled using `hardwareAcceleration: 'prefer-software'`). Video frame and audio sample data is never copied from FFmpeg unless explicitly requested via `VideoSample.copyTo()` and `AudioSample.copyTo()`, and zero-copy GPU decode -> encode paths are used automatically whenever possible.
|
||||
|
||||
## Advanced usage
|
||||
|
||||
### Usage with NodeAV
|
||||
|
||||
`@mediabunny/server` provides `AvFrameVideoSampleResource` and `AvFrameAudioSampleResource` as a means to create `VideoSample` and `AudioSample` instances that are directly backed by data residing in NodeAV's [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html) (and therefore FFmpeg's `AVFrame`) without ever having to copy data to or from JavaScript. Reading NodeAV's documentation can help you make full use of this integration.
|
||||
|
||||
To convert between Mediabunny and NodeAV (FFmpeg) worlds, you can do this:
|
||||
```ts
|
||||
import { VideoSample, AudioSample } from 'mediabunny';
|
||||
import { AvFrameVideoSampleResource, AvFrameAudioSampleResource, toAvFrame } from '@mediabunny/server';
|
||||
|
||||
// Frame -> VideoSample
|
||||
new VideoSample(new AvFrameVideoSampleResource(frame), { timestamp });
|
||||
|
||||
// Frame -> AudioSample
|
||||
new AudioSample(new AvFrameAudioSampleResource(frame));
|
||||
// (uses the timestamp in the frame)
|
||||
|
||||
// VideoSample -> Frame
|
||||
await toAvFrame(videoSample, frame);
|
||||
|
||||
// AudioSample -> Frame
|
||||
await toAvFrame(audioSample, frame);
|
||||
```
|
||||
|
||||
#### Electron example
|
||||
|
||||
For example, when using Electron, we may want to capture the app's contents without moving video data from the GPU to the CPU:
|
||||
```ts
|
||||
import { VideoSample } from 'mediabunny';
|
||||
import { AvFrameVideoSampleResource } from '@mediabunny/server';
|
||||
import { HardwareContext, SharedTexture, AV_HWDEVICE_TYPE_VIDEOTOOLBOX } from 'node-av';
|
||||
|
||||
// Create hardware context (platform-specific)
|
||||
const hw = HardwareContext.create(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
|
||||
using sharedTexture = SharedTexture.create(hw);
|
||||
|
||||
// In Electron paint event with offscreen rendering
|
||||
offscreen.webContents.on('paint', (event) => {
|
||||
const texture = event.texture;
|
||||
if (!texture?.textureInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Import as hardware frame (zero-copy)
|
||||
const frame = sharedTexture.importTexture(texture.textureInfo, { pts: 0n });
|
||||
const sample = new VideoSample(new AvFrameVideoSampleResource(frame), {
|
||||
timestamp: 0,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
texture.release();
|
||||
});
|
||||
```
|
||||
|
||||
#### Microphone recording example
|
||||
|
||||
Here, we're using NodeAV's Device API to access the user's microphone:
|
||||
```ts
|
||||
import { AudioSample } from 'mediabunny';
|
||||
import { AvFrameAudioSampleResource } from '@mediabunny/server';
|
||||
import { DeviceAPI, Decoder } from 'node-av';
|
||||
|
||||
await using mic = await DeviceAPI.openMicrophone();
|
||||
const audioStream = mic.audio()!;
|
||||
using decoder = await Decoder.create(audioStream);
|
||||
|
||||
let firstTimestamp: number | null = null;
|
||||
for await (const frame of decoder.frames(mic.packets(audioStream.index))) {
|
||||
if (!frame) {
|
||||
break;
|
||||
}
|
||||
|
||||
const sample = new AudioSample(new AvFrameAudioSampleResource(frame));
|
||||
|
||||
if (firstTimestamp === null) {
|
||||
firstTimestamp = sample.timestamp;
|
||||
}
|
||||
|
||||
// Offset timestamps so they start at 0
|
||||
sample.setTimestamp(sample.timestamp - firstTimestamp);
|
||||
|
||||
// Do something with the sample now, like passing it to an AudioSampleSource
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Video and audio processing
|
||||
|
||||
Browser environments ship with many API goodies such as the Canvas 2D API which are a naturally great fit for doing video frame processing, and they integrate well with Mediabunny. On the server, these APIs don't exist, so other approaches must be used:
|
||||
|
||||
#### VideoSample.transform()
|
||||
|
||||
This method allows for simple transformations on `VideoSample` instances and works when `@mediabunny/server` has been registered:
|
||||
```ts
|
||||
const transformed = await sample.transform({
|
||||
width: 640,
|
||||
height: 360,
|
||||
fit: 'cover',
|
||||
});
|
||||
```
|
||||
|
||||
#### NodeAV filter graphs
|
||||
|
||||
FFmpeg's `libavfilter` is an incredibly powerful and generic media processing library, and all of it is directly accessible via NodeAV. It works for both video as well as audio data.
|
||||
|
||||
For example, here we combine Mediabunny's Conversion API with a filter graph to grayscale a video:
|
||||
```ts
|
||||
import { Conversion } from 'mediabunny';
|
||||
import { AvFrameVideoSampleResource, toAvFrame } from '@mediabunny/server';
|
||||
import { Frame, FilterAPI } from 'node-av';
|
||||
|
||||
async function* one(f: Frame) { yield f; }
|
||||
|
||||
const conversion = await Conversion.init({
|
||||
// ...
|
||||
video: {
|
||||
process: async (sample) => {
|
||||
// VideoSample -> Frame
|
||||
using inFrame = new Frame();
|
||||
inFrame.alloc();
|
||||
await toAvFrame(sample, inFrame);
|
||||
|
||||
// Frame -> filter -> AvFrameVideoSampleResource
|
||||
using filter = FilterAPI.create('format=gray');
|
||||
for await (const outFrame of filter.frames(one(inFrame))) {
|
||||
return outFrame && new AvFrameVideoSampleResource(outFrame);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
// ...
|
||||
});
|
||||
await conversion.execute();
|
||||
```
|
||||
|
||||
#### Canvas API polyfills
|
||||
|
||||
Libraries like [Skia Canvas](https://github.com/samizdatco/skia-canvas) provide GPU-enabled polyfills for the Canvas 2D API. Using it with Mediabunny is simply a matter of converting from and to the Canvas API:
|
||||
```ts
|
||||
const width = videoSample.displayWidth;
|
||||
const height = videoSample.displayHeight;
|
||||
|
||||
const canvas = new Canvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Copy data from VideoSample
|
||||
const imageData = ctx.createImageData(width, height);
|
||||
await videoSample.copyTo(imageData.data, { format: 'RGBA' });
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Issue draw commands
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(20, 20, 100, 60);
|
||||
|
||||
// Convert to VideoSample again
|
||||
const pixels = ctx.getImageData(0, 0, width, height).data;
|
||||
return new VideoSample(pixels, {
|
||||
format: 'RGBA',
|
||||
codedWidth: width,
|
||||
codedHeight: height,
|
||||
timestamp: videoSample.timestamp,
|
||||
duration: videoSample.duration,
|
||||
});
|
||||
```
|
||||
|
||||
## Controlling logging
|
||||
|
||||
Especially in command-line applications you usually don't want Mediabunny interfering with your stdout and stderr output. Mediabunny provides ways to control its console output:
|
||||
```ts
|
||||
import { Logging, LogLevel } from 'mediabunny';
|
||||
|
||||
// The default: Mediabunny can log errors, warnings, and information messages.
|
||||
Logging.level = LogLevel.Info;
|
||||
|
||||
// Only log warnings and errors.
|
||||
Logging.level = LogLevel.Warnings;
|
||||
|
||||
// Only log errors.
|
||||
Logging.level = LogLevel.Errors;
|
||||
|
||||
// Don't log anything at all.
|
||||
Logging.level = LogLevel.Silent;
|
||||
```
|
||||
|
||||
You can also hook into log events:
|
||||
```ts
|
||||
Logging.on('error', (args: unknown[]) => {
|
||||
// Handle error message
|
||||
});
|
||||
|
||||
Logging.on('warn', (args: unknown[]) => {
|
||||
// Handle warning message
|
||||
});
|
||||
|
||||
Logging.on('info', (args: unknown[]) => {
|
||||
// Handle info message
|
||||
});
|
||||
```
|
||||
|
||||
## Implementation details
|
||||
|
||||
`@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) under the hood which provides N-API C bindings to FFmpeg's C API. Using NodeAV, this package implements [custom decoders and encoders](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) by directly using the APIs provided by `libavcodec`.
|
||||
|
||||
For encoding, video frames and audio samples are transferred to FFmpeg by converting them to an `AVFrame` and are then passed to the correct encoder. The resulting packets are then normalized into the format expected by WebCodecs and the [Mediabunny Codec Registry](https://mediabunny.dev/codec-registry/overview). For decoding, the above process is inverted: packets and decoder metadata are passed to the correct decoder, and the resulting `AVFrame` instances are wrapped in `VideoSample` or `AudioSample` instances. Video frame transformations (resize, rotate, crop) are implemented using the `libavfilter` API.
|
||||
|
||||
Whenever possible, `AVFrame`s are never copied over to JavaScript unless explicitly needed. This enables zero-copy decode -> transformation -> encode paths.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Mediabunny supports a wide range of media input formats, such as MP4, WebM, MP3, HLS, and many more.
|
||||
---
|
||||
|
||||
# Input formats
|
||||
|
||||
Mediabunny supports a wide variety of commonly used container formats for reading input files. These *input formats* are used in two ways:
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Install Mediabunny using npm (or your favorite package manager), or include it directly as a <script> tag.
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
Install Mediabunny using your favorite package manager:
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Mediabunny is a zero-dependency, tree-shakable TypeScript library for reading, writing and converting media files in the browser. Like FFmpeg, but for the web.
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
Mediabunny is a JavaScript library for reading, writing, and converting media files (like MP4 or WebM), 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, 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 for the web's needs.
|
||||
@@ -27,7 +31,7 @@ Here's a long list of stuff this library does:
|
||||
- Custom encoder & decoder support for polyfilling
|
||||
- Low- & high-level abstractions for different use cases
|
||||
- Performant everything
|
||||
- Node.js support
|
||||
- Server-side support (Node, Bun, Deno) via `@mediabunny/server`
|
||||
|
||||
...and there's probably more.
|
||||
|
||||
@@ -54,9 +58,23 @@ To get going with Mediabunny, here are some starting points:
|
||||
- Start with [Converting media files](./converting-media-files) if you care about file conversions.
|
||||
- Dive into [Packets & samples](./packets-and-samples) for a deeper understanding of the concepts underlying this library.
|
||||
|
||||
## Server-side usage
|
||||
|
||||
Mediabunny's simple yet flexible API provides a modern alternative to traditional server-side media processing pipelines which may involve calling out to FFmpeg's CLI manually. Mediabunny was primarily built for client-side environments, but when combined with the [`@mediabunny/server`](./extensions/server) extension, the full Mediabunny feature set is available in server-side environments such as Node, Bun, and Deno.
|
||||
|
||||
The extension enables:
|
||||
- Video decoders and encoders for AVC (H.264), HEVC (H.265), VP8, VP9, and AV1. Supports both length-prefixed and Annex B AVC/HEVC as well as transparent video via VP9.
|
||||
- Audio decoders and encoders for AAC, MP3, Vorbis, Opus, FLAC, AC-3 and E-AC-3. Supports AAC in both AAC and ADTS formats.
|
||||
- Video frame transformation support (resize, rotate, crop)
|
||||
- Automatic hardware acceleration on all platforms (macOS, Linux, Windows)
|
||||
- Built-in multithreading
|
||||
- Zero-copy decode and encode paths
|
||||
|
||||
For more, see [the corresponding guide](./extensions/server).
|
||||
|
||||
## Motivation
|
||||
|
||||
Mediabunny is the evolution of my previous libraries, [mp4-muxer](https://github.com/Vanilagy/mp4-muxer) and [webm-muxer](https://github.com/Vanilagy/webm-muxer), which were both created due to the advent of the WebCodecs API. While they fulfilled their job just fine, I saw a few painpoints:
|
||||
Mediabunny is the evolution of my previous libraries, [mp4-muxer](https://github.com/Vanilagy/mp4-muxer) and [webm-muxer](https://github.com/Vanilagy/webm-muxer), which were both created due to the advent of the WebCodecs API. While they fulfilled their job just fine, I saw a few pain points:
|
||||
- Lots of duplicated code between the two libraries, otherwise very similar API.
|
||||
- No help with the difficulties of navigating the WebCodecs API & related browser APIs.
|
||||
- "mp4-demuxer when??"
|
||||
@@ -81,4 +99,4 @@ At its core, Mediabunny is a collection of multiplexers and demultiplexers, one
|
||||
|
||||
Mediabunny then provides several wrappers around the WebCodecs API to simplify usage: for reading, it creates decoders with the correct codec configuration and efficiently decodes media data in a pipelined way. For writing, it figures out the necessary codec configuration and sets up encoders which are then used to encode raw media data, while respecting the backpressure applied by the encoder. Extracting the right decoder configuration from a media file can be tricky and sometimes involves diving into encoded media packet bitstreams.
|
||||
|
||||
The conversion abstraction is built on top of Mediabunny's reading and writing primitives and combines them both in a heavily-pipelined way, making sure reading and writing happen in lockstep. It also consists of a lot of conditional logic probing output track compatibility, decoding support, and finding encodable codec configurations. It makes use of the Canvas API for video processing operations, and uses a custom implementation for audio resampling and up/downmixing.
|
||||
The conversion abstraction is built on top of Mediabunny's reading and writing primitives and combines them both in a heavily-pipelined way, making sure reading and writing happen in lockstep. It also consists of a lot of conditional logic probing output track compatibility, decoding support, and finding encodable codec configurations. It makes use of the Canvas API for video processing operations, and uses a custom implementation for audio resampling and up/downmixing.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Media sinks enable different ways to extract video and audio data from files, from built-in decoding to manual per-packet control.
|
||||
---
|
||||
|
||||
# Media sinks
|
||||
|
||||
## Introduction
|
||||
@@ -221,6 +225,11 @@ Create the sink like so:
|
||||
import { VideoSampleSink } from 'mediabunny';
|
||||
|
||||
const sink = new VideoSampleSink(videoTrack);
|
||||
|
||||
// Optionally, configure the decoder:
|
||||
const sink = new VideoSampleSink(videoTrack, {
|
||||
hardwareAcceleration: 'prefer-software',
|
||||
});
|
||||
```
|
||||
|
||||
#### Single retrieval
|
||||
@@ -359,6 +368,8 @@ type CanvasSinkOptions = {
|
||||
rotation?: 0 | 90 | 180 | 270;
|
||||
crop?: { left: number; top: number; width: number; height: number };
|
||||
poolSize?: number;
|
||||
alpha?: boolean;
|
||||
decoderOptions?: VideoSinkDecoderOptions;
|
||||
};
|
||||
```
|
||||
- `width`\
|
||||
@@ -376,6 +387,10 @@ type CanvasSinkOptions = {
|
||||
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. The crop region is in the _display pixel space_ of the underlying video data.
|
||||
- `poolSize`\
|
||||
See [Canvas pool](#canvas-pool).
|
||||
- `alpha`\
|
||||
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.
|
||||
- `decoderOptions`\
|
||||
Additional preferences for the underlying video decoder.
|
||||
|
||||
Some examples:
|
||||
```ts
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Media sources enable multiple ways to add media data to new media files, from built-in encoding to manual per-packet control.
|
||||
---
|
||||
|
||||
# Media sources
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Mediabunny can write a wide range of media output formats, including MP4, WebM, MP3, HLS, and many more.
|
||||
---
|
||||
|
||||
# Output formats
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Learn about how packets carry encoded media data, samples carry raw decoded media data, and the operations you can perform on them.
|
||||
---
|
||||
|
||||
# Packets & samples
|
||||
|
||||
## Introduction
|
||||
@@ -270,6 +274,26 @@ const sample = new VideoSample(buffer, {
|
||||
|
||||
See [`VideoPixelFormat`](https://w3c.github.io/webcodecs/#enumdef-videopixelformat) for a list of pixel formats supported by WebCodecs.
|
||||
|
||||
#### Custom resource constructor
|
||||
|
||||
For advanced use cases (custom decoders, GPU-backed frames, etc.), you can back a `VideoSample` with your own implementation of [`VideoSampleResource`](../api/VideoSampleResource):
|
||||
|
||||
```ts
|
||||
import { VideoSample, VideoSampleResource } from 'mediabunny';
|
||||
|
||||
class MyResource extends VideoSampleResource {
|
||||
// Implement getFormat(), getCodedWidth(), getCodedHeight(),
|
||||
// getSquarePixelWidth(), getSquarePixelHeight(), getColorSpace(),
|
||||
// getDataPlanes(), toRgbSample(), and close().
|
||||
}
|
||||
|
||||
const sample = new VideoSample(new MyResource(), {
|
||||
timestamp: 0,
|
||||
});
|
||||
```
|
||||
|
||||
This allows you to back a `VideoSample` with your own data without having to copy it.
|
||||
|
||||
### Inspecting video samples
|
||||
|
||||
A `VideoSample` has several read-only properties:
|
||||
@@ -306,9 +330,12 @@ videoSample.microsecondDuration; // => Duration in microseconds
|
||||
videoSample.colorSpace; // => VideoColorSpace
|
||||
|
||||
videoSample.visibleRect; // Rectangle
|
||||
|
||||
// Encode options used when this sample is passed to an encoder
|
||||
videoSample.encodeOptions; // => VideoEncoderEncodeOptions (defaults to {})
|
||||
```
|
||||
|
||||
While all of these properties are read-only, you can use the `setTimestamp`, `setDuration` and `setRotation` methods to modify some of the metadata of the video sample.
|
||||
While all of these properties are read-only, you can use the `setTimestamp`, `setDuration`, `setRotation` and `setEncodeOptions` methods to modify some of the metadata of the video sample.
|
||||
|
||||
::: warning
|
||||
Timestamps can be [negative](#negative-timestamps).
|
||||
@@ -398,6 +425,31 @@ You can pass additional options to `allocationSize` and `copyTo` to extract data
|
||||
|
||||
---
|
||||
|
||||
You can transform a `VideoSample` to resize, rotate, and/or crop it, producing a new `VideoSample`:
|
||||
```ts
|
||||
const transformed = await videoSample.transform({
|
||||
width: 640,
|
||||
height: 360,
|
||||
fit: 'cover',
|
||||
rotate: 90,
|
||||
crop: { left: 0, top: 0, width: 1920, height: 1000 },
|
||||
alpha: 'discard',
|
||||
});
|
||||
```
|
||||
|
||||
In browser environments, the transform is performed using a canvas. In non-browser environments without `OffscreenCanvas` or `HTMLCanvasElement`, this method throws unless you register a custom transformer:
|
||||
```ts
|
||||
import { registerVideoSampleTransformer } from 'mediabunny';
|
||||
|
||||
registerVideoSampleTransformer((sample, description) => {
|
||||
// Return a transformed VideoSample, or null to defer to the next transformer.
|
||||
});
|
||||
```
|
||||
|
||||
The [`@mediabunny/server`](./extensions/server) extension registers such a transformer.
|
||||
|
||||
---
|
||||
|
||||
You can also clone a `VideoSample`:
|
||||
```ts
|
||||
const clonedSample = videoSample.clone(); // => VideoSample
|
||||
@@ -459,6 +511,23 @@ Planar formats store each channel's data contiguously, while interleaved formats
|
||||
|
||||

|
||||
|
||||
#### Custom resource constructor
|
||||
|
||||
For advanced use cases (custom decoders, etc.), you can back an `AudioSample` with your own implementation of [`AudioSampleResource`](../api/AudioSampleResource):
|
||||
|
||||
```ts
|
||||
import { AudioSample, AudioSampleResource } from 'mediabunny';
|
||||
|
||||
class MyResource extends AudioSampleResource {
|
||||
// Implement getFormat(), getSampleRate(), getNumberOfFrames(),
|
||||
// getNumberOfChannels(), getTimestamp(), getDataPlane(), and close().
|
||||
}
|
||||
|
||||
const sample = new AudioSample(new MyResource());
|
||||
```
|
||||
|
||||
This allows you to back an `AudioSample` with your own data without having to copy it.
|
||||
|
||||
### Inspecting audio samples
|
||||
|
||||
An `AudioSample` has several read-only properties:
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: A collection of short code snippets that showcase the most common operations that Mediabunny can do.
|
||||
---
|
||||
|
||||
# Quick start
|
||||
|
||||
This page is a collection of short code snippets that showcase the most common operations you may use this library for.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Use Mediabunny to read HLS (.m3u8) playlists, both VOD and live. Extract tracks, metadata, and media data efficiently and with precise control.
|
||||
---
|
||||
|
||||
# Reading HLS
|
||||
|
||||
Mediabunny has full support for reading HLS (.m3u8) playlists, both VOD and live. This page will go into HLS-specific behavior and advice. For general information about how to read any media file (including HLS) in Mediabunny, refer to [Reading media files](./reading-media-files).
|
||||
@@ -8,7 +12,7 @@ Mediabunny exposes HLS playlists as if they were a single giant input file. Like
|
||||
|
||||
## HLS inputs
|
||||
|
||||
HLS playlists (master & media) are read through the same `Input` interface as all other media files in Mediabunny. HLS must read multiple files, meaning any [`PathedSource`](../api/PathedSource) is required:
|
||||
HLS playlists (master & media) are read through the same `Input` interface as all other media files in Mediabunny. HLS must read multiple files, meaning any [`PathedSource`](../api/PathedSource) (such as `UrlSource`) is required:
|
||||
```ts
|
||||
import { Input, UrlSource, HLS_FORMATS } from 'mediabunny';
|
||||
|
||||
@@ -229,6 +233,35 @@ This makes cross-track synchronization trivial. To know if a track's timestamps
|
||||
await track.isRelativeToUnixEpoch(); // => boolean
|
||||
```
|
||||
|
||||
### Disabling Unix offsets
|
||||
|
||||
If you don't want Mediabunny to offset packet timestamps to be in Unix time, you can set `offsetTimestampsByDateTime` to `false` in the input format options:
|
||||
```ts
|
||||
const input = new Input({
|
||||
// ...
|
||||
formatOptions: {
|
||||
hls: {
|
||||
offsetTimestampsByDateTime: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This way, track and packet timestamps behave as if no `#EXT-X-PROGRAM-DATE-TIME` tags existed. This also means that any date time gaps are completely collapsed.
|
||||
|
||||
You will still be able to query the Unix time metadata via a mapping function on the `InputTrack`:
|
||||
```ts
|
||||
const firstTimestamp = await inputTrack.getFirstTimestamp(); // => 0
|
||||
await inputTrack.getUnixTimeForTimestamp(firstTimestamp); // => 1704067200 (Unix timestamp for 2024-01-01T00:00:00Z)
|
||||
```
|
||||
|
||||
This function performs a piecewise-continuous mapping of timestamp space into Unix time space.
|
||||
|
||||
If no wall-clock time information is available, `getUnixTimeForTimestamp()` will return `null`. You can check the presence of Unix time metadata via:
|
||||
```ts
|
||||
await inputTrack.hasUnixTimeMapping(); // boolean
|
||||
```
|
||||
|
||||
## Live HLS
|
||||
|
||||
HLS playlists may be live. You can check that a track is live via:
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Efficiently extract metadata, video frames, and audio samples from any media file, such as MP4 or WebM.
|
||||
---
|
||||
|
||||
# Reading media files
|
||||
|
||||
Mediabunny allows you to read media files with great control and efficiency. You can use it to extract metadata (such as duration or resolution), as well as to read actual media data from video and audio tracks with frame-accurate timing. Many commonly used [input file formats](./input-formats) are supported. Using [input sources](#input-sources), data can be read from multiple sources, such as directly from memory, from the user's disk, or even over the network.
|
||||
@@ -639,6 +643,10 @@ const source = new UrlSource('https://example.com/bigbuckbunny.mp4', {
|
||||
});
|
||||
```
|
||||
|
||||
::: info
|
||||
All `requestInit` fields are respected except for `signal`, which is overridden by Mediabunny. The same applies to the `signal` value of a `Request` passed as the first constructor argument. To cancel ongoing requests, [dispose of the input](#disposing-inputs).
|
||||
:::
|
||||
|
||||
`getRetryDelay` can be used to control the retry logic used should a request fail. When a request fails, `getRetryDelay` should return the time to wait in seconds before the request will be retried. Returning `null` prevents further retries.
|
||||
```ts
|
||||
// UrlSource using retry logic with exponential backoff:
|
||||
@@ -692,18 +700,18 @@ type FilePathSourceOptions = {
|
||||
When using this source, make sure to manually [dispose of the Input](#disposing-inputs) when you are done with it to properly close the internal file handle held by this source.
|
||||
:::
|
||||
|
||||
### `StreamSource`
|
||||
### `CustomSource`
|
||||
|
||||
This is a general-purpose input source you can use to read data from anywhere.
|
||||
|
||||
For example, here we're reading a file from disk using the Node.js file system (although you should use [`FilePathSource`](#filepathsource) for that):
|
||||
For example, here we're reading a file from disk using the Node.js file system (although you should use the existing [`FilePathSource`](#filepathsource) for that):
|
||||
```ts
|
||||
import { StreamSource } from 'mediabunny';
|
||||
import { CustomSource } from 'mediabunny';
|
||||
import { open } from 'node:fs/promises';
|
||||
|
||||
const fileHandle = await open('bigbuckbunny.mp4', 'r');
|
||||
|
||||
const source = new StreamSource({
|
||||
const source = new CustomSource({
|
||||
read: async (start, end) => {
|
||||
const buffer = Buffer.alloc(end - start);
|
||||
await fileHandle.read(buffer, 0, end - start, start);
|
||||
@@ -716,9 +724,9 @@ const source = new StreamSource({
|
||||
});
|
||||
```
|
||||
|
||||
The options of `StreamSource` have the following type:
|
||||
The options of `CustomSource` have the following type:
|
||||
```ts
|
||||
type StreamSourceOptions = {
|
||||
type CustomSourceOptions = {
|
||||
getSize: () => MaybePromise<number>;
|
||||
read: (start: number, end: number) => MaybePromise<Uint8Array | ReadableStream<Uint8Array>>;
|
||||
dispose?: () => unknown;
|
||||
@@ -732,7 +740,7 @@ type MaybePromise<T> = T | Promise<T>;
|
||||
- `getSize`\
|
||||
Called when the size of the entire file is requested. Must return or resolve to the size in bytes. This function is guaranteed to be called before `read`.
|
||||
- `read`\
|
||||
Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream that yields these bytes.
|
||||
Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream that yields these bytes. You are guaranteed that `0 <= start < end < fileSize`.
|
||||
- `dispose`\
|
||||
Called when the `Input` driven by this source is disposed.
|
||||
- `maxCacheSize`\
|
||||
@@ -743,6 +751,10 @@ type MaybePromise<T> = T | Promise<T>;
|
||||
- `'fileSystem'`: File system-optimized prefetching: a small amount of data is prefetched bidirectionally, aligned with page boundaries.
|
||||
- `'network'`: Network-optimized prefetching, or more generally, prefetching optimized for any high-latency environment: tries to minimize the amount of read calls and aggressively prefetches data when sequential access patterns are detected.
|
||||
|
||||
::: info
|
||||
`CustomSource` was previously known as `StreamSource` and is still available under that alias, but usage of `StreamSource` is deprecated.
|
||||
:::
|
||||
|
||||
### `ReadableStreamSource`
|
||||
|
||||
This is a source backed by a `ReadableStream` of `Uint8Array`, representing an append-only byte stream of unknown length. This is the source to use for incrementally streaming in input files that are still being constructed and whose size we don't yet know. You could also use it to stream in existing files, but other sources (such as [`BlobSource`](#blobsource) or [`FilePathSource`](#filepathsource)) are recommended instead because they offer random access.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Mediabunny supports a wide range of media container formats (.mp4, .webm, .mp3, .wav, .m3u8, ...) and video/audio codecs (H.264, HEVC, VP9, AV1, AAC, Opus, FLAC, ...).
|
||||
---
|
||||
|
||||
# Supported formats & codecs
|
||||
|
||||
## Container formats
|
||||
@@ -36,6 +40,7 @@ Mediabunny ships with built-in decoders and encoders for all audio PCM codecs, m
|
||||
- `'vp8'` - VP8
|
||||
- `'vp9'` - VP9
|
||||
- `'av1'` - AOMedia Video 1 (AV1)
|
||||
- `'prores'` - Apple ProRes [^prores]
|
||||
|
||||
### Audio codecs
|
||||
|
||||
@@ -76,6 +81,7 @@ Not all codecs can be used with all containers. The following table specifies th
|
||||
| `'vp8'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'vp9'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'av1'` | ✓ | ✓ | ✓ | ✓ | | | | | | |
|
||||
| `'prores'` | ✓ | ✓ | ✓ | | | | | | | |
|
||||
| `'aac'` | ✓ | ✓ | ✓ | | | | | ✓ | | ✓ |
|
||||
| `'opus'` | ✓ | ✓ | ✓ | ✓ | ✓ | | | | | |
|
||||
| `'mp3'` | ✓ | ✓ | ✓ | | | ✓ | | | | ✓ |
|
||||
@@ -101,6 +107,7 @@ Not all codecs can be used with all containers. The following table specifies th
|
||||
|
||||
For HLS, the supported codecs depend on the segment format chosen.
|
||||
|
||||
[^prores]: ProRes is not supported by WebCodecs. To decode it, use the [`@mediabunny/prores`](./extensions/prores) extension package. The [`@mediabunny/server`](./extensions/server) extension package provides both decoding and encoding support for server-side environments.
|
||||
[^aac]: In some browsers, AAC encoding is not supported by WebCodecs. You can polyfill it with the [`@mediabunny/aac-encoder`](./extensions/aac-encoder) extension package.
|
||||
[^mp3]: MP3 encoding is not supported by WebCodecs. You can polyfill it with the [`@mediabunny/mp3-encoder`](./extensions/mp3-encoder) extension package.
|
||||
[^flac]: FLAC encoding is not supported by WebCodecs. You can polyfill it with the [`@mediabunny/flac-encoder`](./extensions/flac-encoder) extension package.
|
||||
@@ -289,10 +296,13 @@ class {
|
||||
codec: AudioCodec;
|
||||
config: AudioEncoderConfig;
|
||||
onPacket: (packet: EncodedPacket, meta?: EncodedAudioChunkMetadata) => unknown;
|
||||
|
||||
// For both:
|
||||
onError: (error: unknown) => void;
|
||||
}
|
||||
```
|
||||
|
||||
`codec` and `config` specify the concrete codec configuration to use, and `onPacket` is a method that your code **must** call for each encoded packet it creates.
|
||||
`codec` and `config` specify the concrete codec configuration to use, and `onPacket` is a method that your code **must** call for each encoded packet it creates. `onError` is a method you can call to surface any out-of-band errors that occur outside of the regular method calls (such as from an asynchronous background task); these errors would otherwise go uncaught.
|
||||
|
||||
You **must** implement the following methods in your custom encoder class:
|
||||
```ts
|
||||
@@ -352,10 +362,13 @@ class {
|
||||
codec: AudioCodec;
|
||||
config: AudioDecoderConfig;
|
||||
onSample: (sample: AudioSample) => unknown;
|
||||
|
||||
// For both:
|
||||
onError: (error: unknown) => void;
|
||||
}
|
||||
```
|
||||
|
||||
`codec` and `config` specify the concrete codec configuration to use, and `onSample` is a method that your code **must** call for each video/audio sample it creates.
|
||||
`codec` and `config` specify the concrete codec configuration to use, and `onSample` is a method that your code **must** call for each video/audio sample it creates. `onError` is a method you can call to surface any out-of-band errors that occur outside of the regular method calls (such as from an asynchronous background task); these errors would otherwise go uncaught.
|
||||
|
||||
You **must** implement the following methods in your custom decoder class:
|
||||
```ts
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
description: Mediabunny can create new HLS (.m3u8) playlists directly in the browser, both VOD and live. Define tracks, add media data, and upload directly to a server.
|
||||
outline: [2, 4]
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Use Mediabunny to create new video and audio files of any size directly in the browser, and with full control over frame timing, tracks, and more.
|
||||
---
|
||||
|
||||
# Writing media files
|
||||
|
||||
Mediabunny enables you to create media files with very fine levels of control. You can add an arbitrary number of video, audio and subtitle tracks to a media file, and precisely control the timing of media data. This library supports [many output file formats](./output-formats). Using [output targets](#output-targets), you can decide if you want to build up the entire file in memory or stream it out in chunks as it's being created - allowing you to create very large files.
|
||||
@@ -338,7 +342,7 @@ Each chunk written to the `WritableStream` represents a contiguous chunk of byte
|
||||
::: warning
|
||||
Note that some byte regions in the output file may be written to multiple times. It is therefore **incorrect** to construct the final file by simply concatenating all `Uint8Array`s together - you **must** write each chunk of data at the specified byte offset position _in the order_ in which the chunks arrived. If you don't do this, your output file will likely be invalid or corrupted.
|
||||
|
||||
Some [output formats](./output-formats) have *append-only* writing modes in which the byte offset of a written chunk will always be equal to the total number of bytes in all previously written chunks. In other words, when writing is append-only, simply concatening all `Uint8Array`s yields the correct result. Some APIs (like `appendBuffer` of Media Source Extensions) require this, so make sure to configure your output format accordingly for those cases.
|
||||
Some [output formats](./output-formats) have *append-only* writing modes in which the byte offset of a written chunk will always be equal to the total number of bytes in all previously written chunks. In other words, when writing is append-only, simply concatenating all `Uint8Array`s yields the correct result. Some APIs (like `appendBuffer` of Media Source Extensions) require this, so make sure to configure your output format accordingly for those cases.
|
||||
:::
|
||||
|
||||
#### Chunked mode
|
||||
@@ -541,4 +545,4 @@ The promise returned by `getMimeType` only resolves once the precise codec strin
|
||||
If you don't care about specific track codecs, you can instead use the simpler [`mimeType`](./output-formats#output-format-properties) property on the `Output`'s format:
|
||||
```ts
|
||||
output.format.mimeType; // => string
|
||||
```
|
||||
```
|
||||
|
||||
+20
-14
@@ -1,7 +1,8 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
title: Mediabunny
|
||||
title: Mediabunny — A complete JavaScript media toolkit for the browser
|
||||
titleTemplate: false
|
||||
|
||||
hero:
|
||||
name: Mediabunny
|
||||
@@ -101,17 +102,17 @@ const sponsors = {
|
||||
{ image: '/sponsors/screen-studio.webp', name: 'Screen Studio', url: 'https://screen.studio/' },
|
||||
{ image: '/sponsors/tella.svg', name: 'Tella', url: 'https://www.tella.com/' },
|
||||
],
|
||||
silver: [
|
||||
{ image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' },
|
||||
],
|
||||
silver: [],
|
||||
bronze: [
|
||||
{ image: '/sponsors/elevenlabs.png', name: 'ElevenLabs', url: 'https://elevenlabs.io/' },
|
||||
{ image: '/sponsors/rve.png', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' },
|
||||
{ image: '/sponsors/mux.jpg', name: 'Mux', url: 'https://www.mux.com/' },
|
||||
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
|
||||
{ image: '/sponsors/photocircle.png', name: 'PhotoCircle', url: 'https://www.photocircleapp.com/' },
|
||||
],
|
||||
individual: [
|
||||
{ image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/189556080', name: 'Cursorful', url: 'https://github.com/cursorful' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/11537072', name: 'Threema', url: 'https://threema.com/en' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' },
|
||||
@@ -119,8 +120,11 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/18653821', name: 'ZerGo0', url: 'https://github.com/ZerGo0' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5913254', name: 'Brandon McConnell', url: 'https://github.com/brandonmcconnell' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/9549394', name: 'studnitz', url: 'https://github.com/studnitz' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/67356781', name: 'Ahmed Rowaihi', url: 'https://github.com/ahmedrowaihi' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/4714175', name: 'Phoomparin Mano', url: 'https://github.com/heypoom' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/504909', name: 'Hirbod', url: 'https://github.com/hirbod' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/2565549', name: 'MatthewNorton', url: 'https://github.com/MatthewNorton' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/272247752', name: 'Motionik', url: 'https://github.com/Motionik' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
||||
@@ -132,6 +136,7 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/255616819', name: 'cronischarles-del', url: 'https://github.com/cronischarles-del' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/37973863', name: 'Gomi', url: 'https://github.com/gxy5202' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/36898190', name: 'jepcd', url: 'https://github.com/jepcd' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/695166', name: 'Takenori Nakagawa', url: 'https://github.com/ww24' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/63088713', name: 'taf2000', url: 'https://github.com/taf2000' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
|
||||
@@ -144,6 +149,7 @@ const sponsors = {
|
||||
{ image: 'https://avatars.githubusercontent.com/u/13042781', name: 'Anshul Bansal', url: 'https://github.com/akbansa' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/43105172', name: 'zhmou', url: 'https://github.com/zhmou' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/695166', name: 'Takenori Nakagawa', url: 'https://github.com/ww24' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/43607012', name: 'Sturlen', url: 'https://github.com/Sturlen' },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
@@ -154,7 +160,7 @@ const sponsors = {
|
||||
<p class="!my-0 flex-1 text-sm !leading-6">{{ quote.quote }}</p>
|
||||
<div class="shrink-0 flex gap-2 items-center mt-1.5">
|
||||
<a :href="quote.url" target="_blank">
|
||||
<img :src="quote.image" class="size-8 rounded-full shrink-0" alt="Author image">
|
||||
<img :src="quote.image" class="size-8 rounded-full shrink-0" :alt="quote.author">
|
||||
</a>
|
||||
<p class="!my-0 flex-1 text-xs font-medium">{{ quote.author }}</p>
|
||||
</div>
|
||||
@@ -172,7 +178,7 @@ npm install mediabunny
|
||||
<div class="space-y-16 mt-18">
|
||||
<div class="flex flex-col lg:flex-row lg:gap-20 lg:items-center">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Read any media file, efficiently</h1>
|
||||
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Read any media file, efficiently</h2>
|
||||
<p class="text-lg">Mediabunny allows you efficiently read data from any video or audio file, no matter the size: duration, resolution, rotation, tracks, codecs and other metadata, as well as raw or decoded media data from anywhere in the file. Load only what you need.</p>
|
||||
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/reading-media-files">
|
||||
Docs
|
||||
@@ -248,7 +254,7 @@ const { buffer } = output.target; // Contains the final file
|
||||
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Create new media files, programmatically</h1>
|
||||
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Create new media files, programmatically</h2>
|
||||
<p class="text-lg">Generate new media files (such as MP4 or WebM) directly on the client, and as fast as the hardware allows. Add multiple video, audio, or subtitle tracks, and have precise control down to the microsecond.</p>
|
||||
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/writing-media-files">
|
||||
Docs
|
||||
@@ -259,7 +265,7 @@ const { buffer } = output.target; // Contains the final file
|
||||
|
||||
<div class="flex flex-col lg:flex-row lg:gap-20 lg:items-center">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Hopping-fast file conversion</h1>
|
||||
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Hopping-fast file conversion</h2>
|
||||
<p class="text-lg">Use the Conversion API to transform any media file into any other media file: perform transmuxing, transcoding, resizing, rotation, audio resampling, trimming, and more.</p>
|
||||
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/converting-media-files">
|
||||
Docs
|
||||
@@ -295,14 +301,14 @@ await conversion.execute();
|
||||
<img class="relative" src="./assets/inspiring-io.svg">
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Universal I/O</h1>
|
||||
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Universal I/O</h2>
|
||||
<p class="text-lg">Read and write files from and to memory, disk, or the network. Create files for offline use, or live-stream them as they're being created. Inject media data from a canvas, webcam, screen, microphone, audio buffer, your own encoding stack, or whatever. It's all up to you.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-4 lg:gap-20 lg:items-center">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Wide container & codec support</h1>
|
||||
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Wide container & codec support</h2>
|
||||
<p class="text-lg">Mediabunny supports many commonly used container formats and a wide variety of video and audio codecs. And all of them bidirectionally, too! (that means for reading and writing)</p>
|
||||
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/supported-formats-and-codecs">
|
||||
See full list
|
||||
@@ -328,14 +334,14 @@ await conversion.execute();
|
||||
<p class="!my-0 text-center text-[10px] opacity-50 !leading-4">Reading BigBuckBunny1080pH264.mov (691 MiB) from disk<br>Ryzen 7600X, RTX 4070, NVMe SSD, measured 2025-06-22</p>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">High performance</h1>
|
||||
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">High performance</h2>
|
||||
<p class="text-lg">By reading only what you need, writing progressively, utilizing hardware-accelerated encoding and decoding via the WebCodecs API, and using a pipelined design, Mediabunny is able to get the job done fast.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-4 lg:gap-20 lg:items-center">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Built from scratch, for the web</h1>
|
||||
<h2 class="inline-block !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Built from scratch, for the web</h2>
|
||||
<p class="text-lg">Mediabunny is 100% implemented in TypeScript and has zero dependencies. Its API was designed to be highly tree-shakable, meaning you only include what you use.</p>
|
||||
</div>
|
||||
<div class="space-y-2 flex-1 min-w-0">
|
||||
@@ -368,10 +374,10 @@ await conversion.execute();
|
||||
<hr class="!my-16" />
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<h1 id="sponsors" class="flex items-center gap-4" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">
|
||||
<h2 id="sponsors" class="flex items-center gap-4 !text-[32px] !border-0 !m-0 !p-0 !leading-[40px]" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">
|
||||
Made possible by you
|
||||
<img class="size-8" src="./assets/fluent-emoji--heart-suit.svg">
|
||||
</h1>
|
||||
</h2>
|
||||
<p class="max-w-2xl">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. This is made possible by the generous financial backing of these awesome sponsors:</p>
|
||||
<template v-if="sponsors.gold.length > 0">
|
||||
<h3 class="!text-3xl">Gold sponsors</h3>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Teach Claude Code, Codex, Cursor, Copilot or any other AI coding agent how to make full use of Mediabunny.
|
||||
---
|
||||
|
||||
# Mediabunny and LLMs
|
||||
|
||||
<div class="grid place-items-center my-14">
|
||||
|
||||
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 304 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -32,6 +32,7 @@ export default tseslint.config(
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
||||
'@typescript-eslint/no-unsafe-unary-minus': 'off',
|
||||
'@typescript-eslint/no-deprecated': 'error',
|
||||
'@typescript-eslint/consistent-type-exports': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -46,6 +47,8 @@ export default tseslint.config(
|
||||
'packages/aac-encoder/build',
|
||||
'packages/flac-encoder/dist',
|
||||
'packages/flac-encoder/build',
|
||||
'packages/prores/dist',
|
||||
'packages/server/dist',
|
||||
'eslint.config.mjs',
|
||||
'docs/.vitepress/cache',
|
||||
'test/public',
|
||||
|
||||
@@ -9,8 +9,15 @@ import {
|
||||
Conversion,
|
||||
QUALITY_VERY_LOW,
|
||||
} from 'mediabunny';
|
||||
import { registerAc3Decoder } from '@mediabunny/ac3';
|
||||
import { registerProresDecoder } from '@mediabunny/prores';
|
||||
|
||||
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
|
||||
|
||||
// Enable codecs that aren't natively supported by WebCodecs.
|
||||
registerAc3Decoder();
|
||||
registerProresDecoder();
|
||||
|
||||
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
|
||||
|
||||
const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement;
|
||||
@@ -68,6 +75,7 @@ const compressFile = async (resource: File | string) => {
|
||||
bitrate: QUALITY_VERY_LOW,
|
||||
},
|
||||
audio: {
|
||||
codec: 'opus',
|
||||
bitrate: QUALITY_VERY_LOW,
|
||||
},
|
||||
});
|
||||
@@ -136,7 +144,7 @@ const compressFile = async (resource: File | string) => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
@@ -153,7 +161,7 @@ loadUrlButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
'https://mediabunny.dev/big-buck-bunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<html lang="en-US" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>File compression example | Mediabunny</title>
|
||||
<meta name="description" content="Select or drop a media file, and Mediabunny will convert it to a heavily-compressed MP4 file.">
|
||||
<script type="module" src="../base.ts"></script>
|
||||
<script type="module" src="./file-compression.ts"></script>
|
||||
<link rel="stylesheet" href="../base.css">
|
||||
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
|
||||
<link rel="canonical" href="https://mediabunny.dev/examples/file-compression/">
|
||||
<meta property="og:site_name" content="Mediabunny">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="File compression example | Mediabunny">
|
||||
<meta property="og:description" content="Select or drop a media file, and Mediabunny will convert it to a heavily-compressed MP4 file.">
|
||||
<meta property="og:url" content="https://mediabunny.dev/examples/file-compression/">
|
||||
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<meta property="og:locale" content="en-US">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@vanilagy">
|
||||
<meta name="twitter:title" content="File compression example | Mediabunny">
|
||||
<meta name="twitter:description" content="Select or drop a media file, and Mediabunny will convert it to a heavily-compressed MP4 file.">
|
||||
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"File compression"}]}</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
|
||||
|
||||
@@ -15,8 +15,15 @@ import {
|
||||
QUALITY_LOW,
|
||||
QUALITY_VERY_LOW,
|
||||
} from 'mediabunny';
|
||||
import { registerAc3Decoder } from '@mediabunny/ac3';
|
||||
import { registerProresDecoder } from '@mediabunny/prores';
|
||||
|
||||
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
|
||||
|
||||
// Enable codecs that aren't natively supported by WebCodecs.
|
||||
registerAc3Decoder();
|
||||
registerProresDecoder();
|
||||
|
||||
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
|
||||
|
||||
declare global {
|
||||
@@ -233,7 +240,7 @@ selectDirectoryButton.addEventListener('click', async () => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files![0];
|
||||
if (file) {
|
||||
@@ -248,7 +255,7 @@ loadSampleButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
'https://mediabunny.dev/big-buck-bunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<html lang="en-US" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HLS transcoding example | Mediabunny</title>
|
||||
<meta name="description" content="Select a directory, then a video. Mediabunny will create a complete HLS VOD package with five video renditions and one audio track.">
|
||||
<script type="module" src="../base.ts"></script>
|
||||
<script type="module" src="./hls-transcoding.ts"></script>
|
||||
<link rel="stylesheet" href="../base.css">
|
||||
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
|
||||
<link rel="canonical" href="https://mediabunny.dev/examples/hls-transcoding/">
|
||||
<meta property="og:site_name" content="Mediabunny">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="HLS transcoding example | Mediabunny">
|
||||
<meta property="og:description" content="Select a directory, then a video. Mediabunny will create a complete HLS VOD package with five video renditions and one audio track.">
|
||||
<meta property="og:url" content="https://mediabunny.dev/examples/hls-transcoding/">
|
||||
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<meta property="og:locale" content="en-US">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@vanilagy">
|
||||
<meta name="twitter:title" content="HLS transcoding example | Mediabunny">
|
||||
<meta name="twitter:description" content="Select a directory, then a video. Mediabunny will create a complete HLS VOD package with five video renditions and one audio track.">
|
||||
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"HLS transcoding"}]}</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center bg-zinc-50 px-2 py-10 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200">
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<html lang="en-US" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Live recording & streaming example | Mediabunny</title>
|
||||
<meta name="description" content="The live canvas state and your microphone input will be written into a fragmented MP4 file and live-streamed to a <video> element.">
|
||||
<script type="module" src="../base.ts"></script>
|
||||
<script type="module" src="./live-recording.ts"></script>
|
||||
<link rel="stylesheet" href="../base.css">
|
||||
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
|
||||
<link rel="canonical" href="https://mediabunny.dev/examples/live-recording/">
|
||||
<meta property="og:site_name" content="Mediabunny">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Live recording & streaming example | Mediabunny">
|
||||
<meta property="og:description" content="The live canvas state and your microphone input will be written into a fragmented MP4 file and live-streamed to a <video> element.">
|
||||
<meta property="og:url" content="https://mediabunny.dev/examples/live-recording/">
|
||||
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<meta property="og:locale" content="en-US">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@vanilagy">
|
||||
<meta name="twitter:title" content="Live recording & streaming example | Mediabunny">
|
||||
<meta name="twitter:description" content="The live canvas state and your microphone input will be written into a fragmented MP4 file and live-streamed to a <video> element.">
|
||||
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Live recording & streaming"}]}</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<html lang="en-US" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Media player example | Mediabunny</title>
|
||||
<meta name="description" content="Select or drop a media file, and a fully custom, Mediabunny-powered player will appear.">
|
||||
<script type="module" src="../base.ts"></script>
|
||||
<script type="module" src="./media-player.ts"></script>
|
||||
<link rel="stylesheet" href="../base.css">
|
||||
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
|
||||
<link rel="canonical" href="https://mediabunny.dev/examples/media-player/">
|
||||
<meta property="og:site_name" content="Mediabunny">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Media player example | Mediabunny">
|
||||
<meta property="og:description" content="Select or drop a media file, and a fully custom, Mediabunny-powered player will appear.">
|
||||
<meta property="og:url" content="https://mediabunny.dev/examples/media-player/">
|
||||
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<meta property="og:locale" content="en-US">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@vanilagy">
|
||||
<meta name="twitter:title" content="Media player example | Mediabunny">
|
||||
<meta name="twitter:description" content="Select or drop a media file, and a fully custom, Mediabunny-powered player will appear.">
|
||||
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Media player"}]}</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2 h-svh">
|
||||
|
||||
@@ -8,8 +8,15 @@ import {
|
||||
WrappedAudioBuffer,
|
||||
WrappedCanvas,
|
||||
} from 'mediabunny';
|
||||
import { registerAc3Decoder } from '@mediabunny/ac3';
|
||||
import { registerProresDecoder } from '@mediabunny/prores';
|
||||
|
||||
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
|
||||
|
||||
// Enable codecs that aren't natively supported by WebCodecs.
|
||||
registerAc3Decoder();
|
||||
registerProresDecoder();
|
||||
|
||||
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
|
||||
|
||||
const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement;
|
||||
@@ -215,7 +222,7 @@ const initMediaPlayer = async (resource: File | string) => {
|
||||
|
||||
if (audioContext.state === 'running') {
|
||||
// Start playback automatically if the audio context permits
|
||||
await play();
|
||||
// await play();
|
||||
}
|
||||
|
||||
loadingElement.style.display = 'none';
|
||||
@@ -732,7 +739,7 @@ window.addEventListener('resize', () => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
@@ -749,7 +756,7 @@ loadUrlButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
'https://mediabunny.dev/big-buck-bunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<html lang="en-US" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Metadata extraction example | Mediabunny</title>
|
||||
<meta name="description" content="Select or drop a media file, and Mediabunny will start extracting various metadata about that file.">
|
||||
<script type="module" src="../base.ts"></script>
|
||||
<script type="module" src="./metadata-extraction.ts"></script>
|
||||
<link rel="stylesheet" href="../base.css">
|
||||
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
|
||||
<link rel="canonical" href="https://mediabunny.dev/examples/metadata-extraction/">
|
||||
<meta property="og:site_name" content="Mediabunny">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Metadata extraction example | Mediabunny">
|
||||
<meta property="og:description" content="Select or drop a media file, and Mediabunny will start extracting various metadata about that file.">
|
||||
<meta property="og:url" content="https://mediabunny.dev/examples/metadata-extraction/">
|
||||
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<meta property="og:locale" content="en-US">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@vanilagy">
|
||||
<meta name="twitter:title" content="Metadata extraction example | Mediabunny">
|
||||
<meta name="twitter:description" content="Select or drop a media file, and Mediabunny will start extracting various metadata about that file.">
|
||||
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Metadata extraction"}]}</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
|
||||
|
||||
@@ -238,7 +238,7 @@ loadUrlButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
'https://mediabunny.dev/big-buck-bunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<html lang="en-US" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Procedural Generation example | Mediabunny</title>
|
||||
<meta name="description" content="Using Mediabunny, this page will procedurally generate a video of musical bouncing balls as fast as possible.">
|
||||
<script type="module" src="../base.ts"></script>
|
||||
<script type="module" src="./procedural-generation.ts"></script>
|
||||
<link rel="stylesheet" href="../base.css">
|
||||
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
|
||||
<link rel="canonical" href="https://mediabunny.dev/examples/procedural-generation/">
|
||||
<meta property="og:site_name" content="Mediabunny">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Procedural Generation example | Mediabunny">
|
||||
<meta property="og:description" content="Using Mediabunny, this page will procedurally generate a video of musical bouncing balls as fast as possible.">
|
||||
<meta property="og:url" content="https://mediabunny.dev/examples/procedural-generation/">
|
||||
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<meta property="og:locale" content="en-US">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@vanilagy">
|
||||
<meta name="twitter:title" content="Procedural Generation example | Mediabunny">
|
||||
<meta name="twitter:description" content="Using Mediabunny, this page will procedurally generate a video of musical bouncing balls as fast as possible.">
|
||||
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Procedural generation"}]}</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<html lang="en-US" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Thumbnail generation example | Mediabunny</title>
|
||||
<meta name="description" content="Select or drop a media file, and Mediabunny will extract video thumbnails for it.">
|
||||
<script type="module" src="./../base.ts"></script>
|
||||
<script type="module" src="./thumbnail-generation.ts"></script>
|
||||
<link rel="stylesheet" href="../base.css">
|
||||
<link rel="icon" href="../../docs/public/mediabunny-logo.svg">
|
||||
<link rel="canonical" href="https://mediabunny.dev/examples/thumbnail-generation/">
|
||||
<meta property="og:site_name" content="Mediabunny">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Thumbnail generation example | Mediabunny">
|
||||
<meta property="og:description" content="Select or drop a media file, and Mediabunny will extract video thumbnails for it.">
|
||||
<meta property="og:url" content="https://mediabunny.dev/examples/thumbnail-generation/">
|
||||
<meta property="og:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<meta property="og:locale" content="en-US">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@vanilagy">
|
||||
<meta name="twitter:title" content="Thumbnail generation example | Mediabunny">
|
||||
<meta name="twitter:description" content="Select or drop a media file, and Mediabunny will extract video thumbnails for it.">
|
||||
<meta name="twitter:image" content="https://mediabunny.dev/mediabunny-og-image.png">
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Examples","item":"https://mediabunny.dev/examples"},{"@type":"ListItem","position":2,"name":"Thumbnail generation"}]}</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-gray-50 text-gray-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Input, ALL_FORMATS, BlobSource, UrlSource, CanvasSink } from 'mediabunny';
|
||||
import { registerAc3Decoder } from '@mediabunny/ac3';
|
||||
import { registerProresDecoder } from '@mediabunny/prores';
|
||||
|
||||
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
|
||||
|
||||
// Enable codecs that aren't natively supported by WebCodecs.
|
||||
registerAc3Decoder();
|
||||
registerProresDecoder();
|
||||
|
||||
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
|
||||
|
||||
const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement;
|
||||
@@ -122,7 +129,7 @@ const generateThumbnails = async (resource: File | string) => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts,audio/*,audio/aac';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.mkv,.ts,audio/*,audio/aac,.aac';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
@@ -139,7 +146,7 @@ loadUrlButton.addEventListener('click', () => {
|
||||
const url = prompt(
|
||||
'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the'
|
||||
+ ' right CORS headers set.',
|
||||
'https://remotion.media/BigBuckBunny.mp4',
|
||||
'https://mediabunny.dev/big-buck-bunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
|
||||
Generated
+949
-41
File diff suppressed because it is too large
Load Diff
+20
-10
@@ -1,19 +1,29 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.42.0-beta.10",
|
||||
"version": "1.50.9",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
".",
|
||||
"packages/*"
|
||||
],
|
||||
"main": "./dist/bundles/mediabunny.node.cjs",
|
||||
"main": "./dist/bundles/mediabunny.cjs",
|
||||
"module": "./dist/modules/src/index.js",
|
||||
"types": "./dist/modules/src/index.d.ts",
|
||||
"exports": {
|
||||
"types": "./dist/modules/src/index.d.ts",
|
||||
"import": "./dist/modules/src/index.js",
|
||||
"require": "./dist/bundles/mediabunny.node.cjs"
|
||||
".": {
|
||||
"types": "./dist/modules/src/index.d.ts",
|
||||
"browser": {
|
||||
"import": "./dist/modules/src/index.js",
|
||||
"require": "./dist/bundles/mediabunny.cjs"
|
||||
},
|
||||
"node": {
|
||||
"import": "./dist/modules/src/index.js",
|
||||
"require": "./dist/bundles/mediabunny.node.cjs"
|
||||
},
|
||||
"default": "./dist/modules/src/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
@@ -41,15 +51,15 @@
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "npm run build && npm run docs:generate && vitepress build docs && npm run examples:build && cp dist/mediabunny.d.ts dist-docs/",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts packages/aac-encoder/src/index.ts packages/flac-encoder/src/index.ts docs/api-config.json",
|
||||
"docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts packages/aac-encoder/src/index.ts packages/flac-encoder/src/index.ts packages/prores/src/index.ts packages/server/src/index.ts docs/api-config.json",
|
||||
"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",
|
||||
"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",
|
||||
"set-version": "npm version --no-git-tag-version --workspaces --include-workspace-root"
|
||||
"bump-patch": "npm version patch --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only",
|
||||
"bump-minor": "npm version minor --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only",
|
||||
"bump-major": "npm version major --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only",
|
||||
"set-version": "npm version --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts && npm i --package-lock-only"
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/aac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.42.0-beta.10",
|
||||
"version": "1.50.9",
|
||||
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
|
||||
@@ -192,3 +192,7 @@ if (parentPort) {
|
||||
} else {
|
||||
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
|
||||
}
|
||||
|
||||
// Prevents the worker for being randomly closed by Firefox
|
||||
// https://github.com/Vanilagy/mediabunny/issues/435
|
||||
setInterval(() => {}, 1000);
|
||||
|
||||
@@ -276,6 +276,8 @@ class AacEncoder extends CustomAudioEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
/**
|
||||
* Registers the AAC encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
|
||||
* function before starting any encoding task.
|
||||
@@ -295,6 +297,11 @@ class AacEncoder extends CustomAudioEncoder {
|
||||
* @public
|
||||
*/
|
||||
export const registerAacEncoder = () => {
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
registered = true;
|
||||
|
||||
registerEncoder(AacEncoder);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Logging } from 'mediabunny';
|
||||
|
||||
const AAC_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/aac-encoder loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[AAC_ENCODER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/aac-encoder was loaded twice.'
|
||||
+ ' This will likely cause the encoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/aac-encoder,'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/ac3",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.42.0-beta.10",
|
||||
"version": "1.50.9",
|
||||
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
"module": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
|
||||
@@ -304,3 +304,7 @@ if (parentPort) {
|
||||
} else {
|
||||
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
|
||||
}
|
||||
|
||||
// Prevents the worker for being randomly closed by Firefox
|
||||
// https://github.com/Vanilagy/mediabunny/issues/435
|
||||
setInterval(() => {}, 1000);
|
||||
|
||||
@@ -61,6 +61,8 @@ class Ac3Decoder extends CustomAudioDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
/**
|
||||
* Registers AC-3 and E-AC-3 decoders, which Mediabunny will then use automatically when applicable. Make sure to call
|
||||
* this function before starting any decoding task.
|
||||
@@ -69,5 +71,10 @@ class Ac3Decoder extends CustomAudioDecoder {
|
||||
* @public
|
||||
*/
|
||||
export const registerAc3Decoder = () => {
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
registered = true;
|
||||
|
||||
registerDecoder(Ac3Decoder);
|
||||
};
|
||||
|
||||
@@ -183,6 +183,8 @@ class Ac3Encoder extends CustomAudioEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
/**
|
||||
* Registers AC-3 and E-AC-3 encoders, which Mediabunny will then use automatically when applicable. Make sure to call
|
||||
* this function before starting any encoding task.
|
||||
@@ -191,5 +193,10 @@ class Ac3Encoder extends CustomAudioEncoder {
|
||||
* @public
|
||||
*/
|
||||
export const registerAc3Encoder = () => {
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
registered = true;
|
||||
|
||||
registerEncoder(Ac3Encoder);
|
||||
};
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Logging } from 'mediabunny';
|
||||
|
||||
const AC3_LOADED_SYMBOL = Symbol.for('@mediabunny/ac3 loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/ac3 was loaded twice.'
|
||||
+ ' This will likely cause the encoder/decoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/ac3,'
|
||||
|
||||
@@ -46,6 +46,7 @@ export const unrefWorker = async () => {
|
||||
if (worker) {
|
||||
if (worker.unref) {
|
||||
worker.unref(); // If we don't do this, then the Node process never terminates by itself
|
||||
// Keep the worker around tho
|
||||
} else if (typeof window === 'undefined') {
|
||||
// Non-browser environment without unref - terminate instead
|
||||
worker.terminate();
|
||||
|
||||
Generated
BIN
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.42.0-beta.10",
|
||||
"version": "1.50.9",
|
||||
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
|
||||
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define BITS_PER_SAMPLE 16
|
||||
#define COMPRESSION_LEVEL 5
|
||||
|
||||
typedef struct {
|
||||
@@ -23,14 +22,10 @@ typedef struct {
|
||||
typedef struct {
|
||||
FLAC__StreamEncoder *encoder;
|
||||
|
||||
// Input buffer for interleaved int16 samples from JS
|
||||
int16_t *input_buffer;
|
||||
// Input buffer for interleaved int32 samples from JS
|
||||
FLAC__int32 *input_buffer;
|
||||
int input_buffer_size;
|
||||
|
||||
// Widened to int32 for libFLAC
|
||||
FLAC__int32 *int32_buffer;
|
||||
int int32_buffer_size;
|
||||
|
||||
// Contiguous output buffer for encoded frame data
|
||||
uint8_t *output_buffer;
|
||||
int output_size;
|
||||
@@ -48,6 +43,7 @@ typedef struct {
|
||||
bool header_done;
|
||||
|
||||
int channels;
|
||||
int bits_per_sample;
|
||||
} EncoderContext;
|
||||
|
||||
static void ensure_output_capacity(EncoderContext *ctx, int needed) {
|
||||
@@ -120,13 +116,14 @@ static void reset_output(EncoderContext *ctx) {
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int init_encoder(int channels, int sample_rate) {
|
||||
int init_encoder(int channels, int sample_rate, int bits_per_sample) {
|
||||
EncoderContext *ctx = calloc(1, sizeof(EncoderContext));
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx->channels = channels;
|
||||
ctx->bits_per_sample = bits_per_sample;
|
||||
|
||||
ctx->encoder = FLAC__stream_encoder_new();
|
||||
if (!ctx->encoder) {
|
||||
@@ -136,7 +133,7 @@ int init_encoder(int channels, int sample_rate) {
|
||||
|
||||
FLAC__stream_encoder_set_channels(ctx->encoder, channels);
|
||||
FLAC__stream_encoder_set_sample_rate(ctx->encoder, sample_rate);
|
||||
FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, BITS_PER_SAMPLE);
|
||||
FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, bits_per_sample);
|
||||
FLAC__stream_encoder_set_compression_level(ctx->encoder, COMPRESSION_LEVEL);
|
||||
FLAC__stream_encoder_set_verify(ctx->encoder, false);
|
||||
|
||||
@@ -174,19 +171,15 @@ EMSCRIPTEN_KEEPALIVE
|
||||
int send_samples(int ctx_ptr, int num_samples) {
|
||||
EncoderContext *ctx = (EncoderContext *)ctx_ptr;
|
||||
|
||||
// Widen int16 to int32 for libFLAC
|
||||
int total = num_samples * ctx->channels;
|
||||
if (total > ctx->int32_buffer_size) {
|
||||
ctx->int32_buffer = realloc(ctx->int32_buffer, total * sizeof(FLAC__int32));
|
||||
ctx->int32_buffer_size = total;
|
||||
}
|
||||
int shift = 32 - ctx->bits_per_sample;
|
||||
for (int i = 0; i < total; i++) {
|
||||
ctx->int32_buffer[i] = ctx->input_buffer[i];
|
||||
ctx->input_buffer[i] >>= shift;
|
||||
}
|
||||
|
||||
reset_output(ctx);
|
||||
|
||||
FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->int32_buffer, num_samples);
|
||||
FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->input_buffer, num_samples);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ type ExtendedEmscriptenModule = EmscriptenModule & {
|
||||
let module: ExtendedEmscriptenModule;
|
||||
let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
|
||||
|
||||
let initEncoderFn: (channels: number, sampleRate: number) => number;
|
||||
let initEncoderFn: (channels: number, sampleRate: number, bitsPerSample: number) => number;
|
||||
let getEncodeInputPtr: (ctx: number, size: number) => number;
|
||||
let sendSamplesFn: (ctx: number, numSamples: number) => number;
|
||||
let getOutputData: (ctx: number) => number;
|
||||
@@ -37,7 +37,7 @@ const ensureModule = async () => {
|
||||
module = await modulePromise;
|
||||
modulePromise = null;
|
||||
|
||||
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number']);
|
||||
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number']);
|
||||
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
|
||||
sendSamplesFn = module.cwrap('send_samples', 'number', ['number', 'number']);
|
||||
getOutputData = module.cwrap('get_output_data', 'number', ['number']);
|
||||
@@ -50,10 +50,10 @@ const ensureModule = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const initEncoder = async (numberOfChannels: number, sampleRate: number) => {
|
||||
const initEncoder = async (numberOfChannels: number, sampleRate: number, bitsPerSample: 16 | 24) => {
|
||||
await ensureModule();
|
||||
|
||||
const ctx = initEncoderFn(numberOfChannels, sampleRate);
|
||||
const ctx = initEncoderFn(numberOfChannels, sampleRate, bitsPerSample);
|
||||
if (ctx === 0) {
|
||||
throw new Error('Failed to initialize FLAC encoder.');
|
||||
}
|
||||
@@ -121,6 +121,7 @@ const onMessage = (data: { id: number; command: WorkerCommand }) => {
|
||||
const { ctx, header } = await initEncoder(
|
||||
command.data.numberOfChannels,
|
||||
command.data.sampleRate,
|
||||
command.data.bitsPerSample,
|
||||
);
|
||||
result = { type: command.type, ctx, header };
|
||||
transferables.push(header);
|
||||
@@ -191,3 +192,7 @@ if (parentPort) {
|
||||
} else {
|
||||
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
|
||||
}
|
||||
|
||||
// Prevents the worker for being randomly closed by Firefox
|
||||
// https://github.com/Vanilagy/mediabunny/issues/435
|
||||
setInterval(() => {}, 1000);
|
||||
|
||||
@@ -29,7 +29,7 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
reject: (reason?: unknown) => void;
|
||||
}>();
|
||||
|
||||
private ctx = 0;
|
||||
private ctx: number | null = null;
|
||||
private chunkMetadata: EncodedAudioChunkMetadata = {};
|
||||
private description: Uint8Array | null = null;
|
||||
private nextTimestampInSamples: number | null = null;
|
||||
@@ -65,19 +65,6 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
};
|
||||
nodeWorker.on('message', onMessage);
|
||||
}
|
||||
|
||||
const result = await this.sendCommand({
|
||||
type: 'init',
|
||||
data: {
|
||||
numberOfChannels: this.config.numberOfChannels,
|
||||
sampleRate: this.config.sampleRate,
|
||||
},
|
||||
});
|
||||
|
||||
this.ctx = result.ctx;
|
||||
|
||||
this.description = new Uint8Array(result.header);
|
||||
this.resetInternalState();
|
||||
}
|
||||
|
||||
private resetInternalState() {
|
||||
@@ -94,15 +81,50 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
}
|
||||
|
||||
async encode(audioSample: AudioSample) {
|
||||
if (this.ctx === null) {
|
||||
// This is the first sample, let's do some init
|
||||
|
||||
let bitsPerSample: 16 | 24;
|
||||
switch (audioSample.format) {
|
||||
case 'u8':
|
||||
case 'u8-planar':
|
||||
case 's16':
|
||||
case 's16-planar':
|
||||
bitsPerSample = 16;
|
||||
break;
|
||||
case 's32':
|
||||
case 's32-planar':
|
||||
case 'f32':
|
||||
case 'f32-planar':
|
||||
bitsPerSample = 24;
|
||||
break;
|
||||
default:
|
||||
assertNever(audioSample.format);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
const result = await this.sendCommand({
|
||||
type: 'init',
|
||||
data: {
|
||||
numberOfChannels: this.config.numberOfChannels,
|
||||
sampleRate: this.config.sampleRate,
|
||||
bitsPerSample,
|
||||
},
|
||||
});
|
||||
|
||||
this.ctx = result.ctx;
|
||||
this.description = new Uint8Array(result.header);
|
||||
this.resetInternalState();
|
||||
}
|
||||
|
||||
if (this.nextTimestampInSamples === null) {
|
||||
this.nextTimestampInSamples = Math.round(audioSample.timestamp * this.config.sampleRate);
|
||||
}
|
||||
|
||||
const totalBytes = audioSample.allocationSize({ format: 's16', planeIndex: 0 });
|
||||
const audioBytes = new Uint8Array(totalBytes);
|
||||
audioSample.copyTo(audioBytes, { format: 's16', planeIndex: 0 });
|
||||
const totalBytes = audioSample.allocationSize({ format: 's32', planeIndex: 0 });
|
||||
const audioData = new ArrayBuffer(totalBytes);
|
||||
audioSample.copyTo(audioData, { format: 's32', planeIndex: 0 });
|
||||
|
||||
const audioData = audioBytes.buffer;
|
||||
const result = await this.sendCommand({
|
||||
type: 'encode',
|
||||
data: {
|
||||
@@ -116,6 +138,10 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
}
|
||||
|
||||
async flush() {
|
||||
if (this.ctx === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } });
|
||||
this.emitPackets(result.packets);
|
||||
|
||||
@@ -171,9 +197,12 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
/**
|
||||
* Registers the FLAC encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
|
||||
* function before starting any encoding task.
|
||||
* function before starting any encoding task. The FLAC encoder will automatically determine the output bit depth
|
||||
* (16 or 24) based on the sample format of incoming `AudioSample` instances.
|
||||
*
|
||||
* Preferably, wrap the call in a condition to avoid overriding any native FLAC encoder:
|
||||
*
|
||||
@@ -190,6 +219,11 @@ class FlacEncoder extends CustomAudioEncoder {
|
||||
* @public
|
||||
*/
|
||||
export const registerFlacEncoder = () => {
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
registered = true;
|
||||
|
||||
registerEncoder(FlacEncoder);
|
||||
};
|
||||
|
||||
@@ -198,3 +232,8 @@ function assert(x: unknown): asserts x {
|
||||
throw new Error('Assertion failed.');
|
||||
}
|
||||
}
|
||||
|
||||
export const assertNever = (x: never) => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unexpected value: ${x}`);
|
||||
};
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Logging } from 'mediabunny';
|
||||
|
||||
const FLAC_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/flac-encoder loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[FLAC_ENCODER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/flac-encoder was loaded twice.'
|
||||
+ ' This will likely cause the encoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/flac-encoder,'
|
||||
|
||||
@@ -16,6 +16,7 @@ export type WorkerCommand = {
|
||||
data: {
|
||||
numberOfChannels: number;
|
||||
sampleRate: number;
|
||||
bitsPerSample: 16 | 24;
|
||||
};
|
||||
} | {
|
||||
type: 'encode';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.42.0-beta.10",
|
||||
"version": "1.50.9",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -219,3 +219,7 @@ if (parentPort) {
|
||||
} else {
|
||||
self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
|
||||
}
|
||||
|
||||
// Prevents the worker for being randomly closed by Firefox
|
||||
// https://github.com/Vanilagy/mediabunny/issues/435
|
||||
setInterval(() => {}, 1000);
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, registerEncoder } from 'mediabunny';
|
||||
import { FRAME_HEADER_SIZE, readMp3FrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
|
||||
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, Logging, registerEncoder } from 'mediabunny';
|
||||
import { MP3_FRAME_HEADER_SIZE, readMp3FrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
|
||||
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
|
||||
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
|
||||
import createWorker from './encode.worker';
|
||||
|
||||
const MP3_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/mp3-encoder loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/mp3-encoder was loaded twice.'
|
||||
+ ' This will likely cause the encoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/mp3-encoder,'
|
||||
@@ -160,7 +160,7 @@ class Mp3Encoder extends CustomAudioEncoder {
|
||||
this.currentBufferOffset = requiredBufferSize;
|
||||
|
||||
let pos = 0;
|
||||
while (pos <= this.currentBufferOffset - FRAME_HEADER_SIZE) {
|
||||
while (pos <= this.currentBufferOffset - MP3_FRAME_HEADER_SIZE) {
|
||||
const word = new DataView(this.buffer.buffer).getUint32(pos, false);
|
||||
const header = readMp3FrameHeader(word, null).header;
|
||||
if (!header) {
|
||||
@@ -212,6 +212,8 @@ class Mp3Encoder extends CustomAudioEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
/**
|
||||
* Registers the LAME MP3 encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
|
||||
* function before starting any encoding task.
|
||||
@@ -231,6 +233,11 @@ class Mp3Encoder extends CustomAudioEncoder {
|
||||
* @public
|
||||
*/
|
||||
export const registerMp3Encoder = () => {
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
registered = true;
|
||||
|
||||
registerEncoder(Mp3Encoder);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -0,0 +1,63 @@
|
||||
# @mediabunny/prores
|
||||
|
||||
[](https://www.npmjs.com/package/@mediabunny/prores)
|
||||
[](https://bundlephobia.com/package/@mediabunny/prores)
|
||||
[](https://www.npmjs.com/package/@mediabunny/prores)
|
||||
[](https://discord.gg/hmpkyYuS4U)
|
||||
|
||||
<div align="center">
|
||||
<img src="../../docs/public/mediabunny-logo.svg" width="180" height="180">
|
||||
</div>
|
||||
|
||||
Browsers have no support for Apple ProRes in their WebCodecs implementations. This extension package provides a decoder for use with [Mediabunny](https://github.com/Vanilagy/mediabunny), allowing you to decode ProRes directly in the browser at unprecedented speed. It is implemented using Mediabunny's [custom coder API](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) and uses [TurboRes](https://github.com/Vanilagy/turbores), an extremely fast WASM-based ProRes decoder, under the hood.
|
||||
|
||||
> This package, like the rest of Mediabunny, is enabled by its [sponsors](https://mediabunny.dev/#sponsors) and their donations. If you've derived value from this package, please consider [leaving a donation](https://github.com/sponsors/Vanilagy)! 💘
|
||||
|
||||
## Installation
|
||||
|
||||
This library peer-depends on Mediabunny. Install both using npm:
|
||||
```bash
|
||||
npm install mediabunny @mediabunny/prores
|
||||
```
|
||||
|
||||
Alternatively, directly include them using a script tag:
|
||||
```html
|
||||
<script src="mediabunny.js"></script>
|
||||
<script src="mediabunny-prores.js"></script>
|
||||
```
|
||||
|
||||
This will expose the global objects `Mediabunny` and `MediabunnyProres`. Use `mediabunny-prores.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).
|
||||
|
||||
## Setup
|
||||
|
||||
`@mediabunny/prores` can make use of shared-memory multithreading to achieve maximum performance. To enable this in browsers, your website must be cross-origin isolated by setting the following response headers:
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
Alternatively, you can use:
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: credentialless
|
||||
```
|
||||
which is generally more permissive but is not supported in Safari (of course).
|
||||
|
||||
---
|
||||
|
||||
If you cannot enable cross-origin isolation, `@mediabunny/prores` will fall back to a slower multithreading algorithm.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { registerProresDecoder } from '@mediabunny/prores';
|
||||
|
||||
registerProresDecoder();
|
||||
```
|
||||
That's it - Mediabunny now uses the registered ProRes decoder automatically.
|
||||
|
||||
For all the ways of using Mediabunny, refer to its [guide](https://mediabunny.dev/guide/introduction).
|
||||
|
||||
## Building and development
|
||||
|
||||
The complete JavaScript package can be built alongside the rest of Mediabunny by running `npm run build` in Mediabunny's root.
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
||||
"mainEntryPointFilePath": "dist/modules/src/index.d.ts",
|
||||
"bundledPackages": [],
|
||||
"compiler": {},
|
||||
"apiReport": {
|
||||
"enabled": false
|
||||
},
|
||||
"docModel": {
|
||||
"enabled": false
|
||||
},
|
||||
"dtsRollup": {
|
||||
"enabled": true,
|
||||
"untrimmedFilePath": "dist/mediabunny-prores.d.ts"
|
||||
},
|
||||
"tsdocMetadata": {
|
||||
"enabled": false
|
||||
},
|
||||
"messages": {
|
||||
"compilerMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
},
|
||||
"extractorMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
},
|
||||
"tsdocMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newlineKind": "lf"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@mediabunny/prores",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.50.9",
|
||||
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
|
||||
"main": "./dist/bundles/mediabunny-prores.mjs",
|
||||
"module": "./dist/bundles/mediabunny-prores.mjs",
|
||||
"types": "./dist/modules/src/index.d.ts",
|
||||
"exports": {
|
||||
"types": "./dist/modules/src/index.d.ts",
|
||||
"import": "./dist/bundles/mediabunny-prores.mjs",
|
||||
"require": "./dist/bundles/mediabunny-prores.mjs"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"package.json",
|
||||
"LICENSE",
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"license": "MPL-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Vanilagy/mediabunny.git",
|
||||
"directory": "packages/prores"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Vanilagy/mediabunny/issues"
|
||||
},
|
||||
"homepage": "https://mediabunny.dev/guide/extensions/prores",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/Vanilagy"
|
||||
},
|
||||
"dependencies": {
|
||||
"turbores": "^1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mediabunny": "^1.49.0"
|
||||
},
|
||||
"keywords": [
|
||||
"prores",
|
||||
"apple-prores",
|
||||
"decoding",
|
||||
"codec",
|
||||
"mediabunny",
|
||||
"turbores",
|
||||
"video",
|
||||
"browser",
|
||||
"wasm",
|
||||
"polyfill"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*!
|
||||
* 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 { CustomVideoDecoder, EncodedPacket, Logging, registerDecoder, VideoCodec, VideoSample } from 'mediabunny';
|
||||
import { Decoder, Frame, PixelFormat, PIXEL_FORMATS, FilledFrame } from 'turbores';
|
||||
import {
|
||||
assert,
|
||||
isWebKit,
|
||||
} from '../../../src/misc';
|
||||
import { type ProresFourCc } from '../../../src/codec';
|
||||
|
||||
const PRORES_LOADED_SYMBOL = Symbol.for('@mediabunny/prores loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[PRORES_LOADED_SYMBOL]) {
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/prores was loaded twice.'
|
||||
+ ' This will likely cause the decoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/prores,'
|
||||
+ ' or if something is being bundled incorrectly.',
|
||||
);
|
||||
}
|
||||
(globalThis as Record<symbol, unknown>)[PRORES_LOADED_SYMBOL] = true;
|
||||
|
||||
class ProresDecoder extends CustomVideoDecoder {
|
||||
private decoder: Decoder | null = null;
|
||||
private framePool: Frame[] = [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
static override supports(codec: VideoCodec, config: VideoDecoderConfig): boolean {
|
||||
return codec === 'prores';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
static _supportedVideoFrameFormats: PixelFormat[] | null = null;
|
||||
|
||||
/** @internal */
|
||||
static _determineSupportedVideoFrameFormats() {
|
||||
const result: PixelFormat[] = [];
|
||||
const data = new Uint8Array(32);
|
||||
|
||||
for (const format of PIXEL_FORMATS) {
|
||||
try {
|
||||
const frame = new VideoFrame(data, {
|
||||
format: format as VideoPixelFormat,
|
||||
codedWidth: 2,
|
||||
codedHeight: 2,
|
||||
timestamp: 0,
|
||||
duration: 0,
|
||||
});
|
||||
frame.close();
|
||||
|
||||
result.push(format);
|
||||
} catch {
|
||||
// Format is not supported
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (typeof VideoFrame !== 'undefined') {
|
||||
// Not all VideoFrame implementations support all pixel formats, therefore let's determine the supported set
|
||||
ProresDecoder._supportedVideoFrameFormats ??= ProresDecoder._determineSupportedVideoFrameFormats();
|
||||
}
|
||||
|
||||
const decoder = await Decoder.create({
|
||||
proresFourCc: this.config.codec as ProresFourCc,
|
||||
useSharedMemory: Decoder.canUseSharedMemory(),
|
||||
allowedOutputFormats: ProresDecoder._supportedVideoFrameFormats ?? undefined,
|
||||
});
|
||||
if (decoder instanceof Error) {
|
||||
throw decoder;
|
||||
}
|
||||
|
||||
this.decoder = decoder;
|
||||
}
|
||||
|
||||
async decode(packet: EncodedPacket) {
|
||||
assert(this.decoder);
|
||||
|
||||
while (this.decoder.desiredSize <= 0) {
|
||||
await this.decoder.dequeued;
|
||||
}
|
||||
|
||||
void this.runDecode(packet)
|
||||
.catch(error => this.onError(error));
|
||||
}
|
||||
|
||||
private async runDecode(packet: EncodedPacket) {
|
||||
assert(this.decoder);
|
||||
|
||||
let frame: Frame;
|
||||
if (this.framePool.length > 0) {
|
||||
frame = this.framePool.shift()!;
|
||||
} else {
|
||||
frame = new Frame();
|
||||
}
|
||||
|
||||
const result = await this.decoder.decode(packet.data, frame);
|
||||
this.framePool.push(frame);
|
||||
|
||||
if (result instanceof Error) {
|
||||
throw result;
|
||||
}
|
||||
|
||||
if (result.visibleHeight < result.codedHeight && isWebKit()) {
|
||||
// WebKit has (had) a bug with displaying height-trimmed YUV frames, so we must compact the frame data a
|
||||
// little https://bugs.webkit.org/show_bug.cgi?id=317524
|
||||
this.trimCodedHeightToVisibleHeight(result);
|
||||
}
|
||||
|
||||
const colorSpaceInit = {
|
||||
primaries: result.colorPrimariesString as VideoColorPrimaries | undefined,
|
||||
matrix: result.colorMatrixString as VideoMatrixCoefficients | undefined,
|
||||
transfer: result.colorTransferString as VideoTransferCharacteristics | undefined,
|
||||
fullRange: result.colorRangeFull,
|
||||
};
|
||||
|
||||
let displayWidth = result.visibleWidth;
|
||||
let displayHeight = result.visibleHeight;
|
||||
|
||||
if (result.pixelAspectRatio.num > result.pixelAspectRatio.den) {
|
||||
displayWidth = Math.round(
|
||||
result.visibleWidth * result.pixelAspectRatio.num / result.pixelAspectRatio.den,
|
||||
);
|
||||
} else {
|
||||
displayHeight = Math.round(
|
||||
result.visibleHeight * result.pixelAspectRatio.den / result.pixelAspectRatio.num,
|
||||
);
|
||||
}
|
||||
|
||||
let sample: VideoSample;
|
||||
if (typeof VideoFrame !== 'undefined') {
|
||||
// Create a VideoFrame directly; this avoids the frame data being copied twice
|
||||
const frame = new VideoFrame(result.frameData, {
|
||||
format: result.pixelFormat as VideoPixelFormat,
|
||||
codedWidth: result.codedWidth,
|
||||
codedHeight: result.codedHeight,
|
||||
displayWidth,
|
||||
displayHeight,
|
||||
visibleRect: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: result.visibleWidth,
|
||||
height: result.visibleHeight,
|
||||
},
|
||||
timestamp: packet.microsecondTimestamp,
|
||||
duration: packet.microsecondDuration,
|
||||
colorSpace: colorSpaceInit,
|
||||
});
|
||||
|
||||
sample = new VideoSample(frame, {
|
||||
timestamp: packet.timestamp,
|
||||
duration: packet.duration,
|
||||
});
|
||||
} else {
|
||||
sample = new VideoSample(result.frameData, {
|
||||
format: result.pixelFormat,
|
||||
codedWidth: result.codedWidth,
|
||||
codedHeight: result.codedHeight,
|
||||
displayWidth,
|
||||
displayHeight,
|
||||
visibleRect: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: result.visibleWidth,
|
||||
height: result.visibleHeight,
|
||||
},
|
||||
timestamp: packet.timestamp,
|
||||
duration: packet.duration,
|
||||
colorSpace: colorSpaceInit,
|
||||
});
|
||||
}
|
||||
|
||||
this.onSample(sample);
|
||||
}
|
||||
|
||||
private trimCodedHeightToVisibleHeight(result: FilledFrame) {
|
||||
const bytesPerSample = result.pixelFormat.includes('P') ? 2 : 1;
|
||||
const subWidth = result.pixelFormat.includes('444') ? 1 : 2;
|
||||
const subHeight = result.pixelFormat.includes('420') ? 2 : 1;
|
||||
|
||||
const chromaCodedWidth = result.codedWidth / subWidth;
|
||||
const chromaCodedHeight = result.codedHeight / subHeight;
|
||||
const chromaVisibleHeight = Math.ceil(result.visibleHeight / subHeight);
|
||||
|
||||
const lumaCodedPixels = result.codedWidth * result.codedHeight;
|
||||
const lumaVisiblePixels = result.codedWidth * result.visibleHeight;
|
||||
const chromaCodedPixels = chromaCodedWidth * chromaCodedHeight;
|
||||
const chromaVisiblePixels = chromaCodedWidth * chromaVisibleHeight;
|
||||
|
||||
// U
|
||||
result.frameData.set(
|
||||
result.frameData.subarray(
|
||||
bytesPerSample * lumaCodedPixels,
|
||||
bytesPerSample * (lumaCodedPixels + chromaCodedPixels),
|
||||
),
|
||||
bytesPerSample * lumaVisiblePixels,
|
||||
);
|
||||
// V
|
||||
result.frameData.set(
|
||||
result.frameData.subarray(
|
||||
bytesPerSample * (lumaCodedPixels + chromaCodedPixels),
|
||||
bytesPerSample * (lumaCodedPixels + 2 * chromaCodedPixels),
|
||||
),
|
||||
bytesPerSample * (lumaVisiblePixels + chromaVisiblePixels),
|
||||
);
|
||||
|
||||
result.codedHeight = result.visibleHeight;
|
||||
}
|
||||
|
||||
async flush() {
|
||||
assert(this.decoder);
|
||||
|
||||
while (this.decoder.decodeQueueSize > 0) {
|
||||
await this.decoder.dequeued;
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
assert(this.decoder);
|
||||
|
||||
await this.decoder.close();
|
||||
|
||||
for (const frame of this.framePool) {
|
||||
frame.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
/**
|
||||
* Registers an Apple ProRes decoder which Mediabunny will then use automatically when applicable. Make sure to call
|
||||
* this function before starting any decoding task.
|
||||
*
|
||||
* @group \@mediabunny/prores
|
||||
* @public
|
||||
*/
|
||||
export const registerProresDecoder = () => {
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
registered = true;
|
||||
|
||||
registerDecoder(ProresDecoder);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/modules",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"stripInternal": true,
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"moduleResolution": "nodenext",
|
||||
"module": "nodenext",
|
||||
"allowJs": true,
|
||||
"paths": {
|
||||
"mediabunny": ["../../src/index.ts"],
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../src" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["../../tsdoc.json"]
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -0,0 +1,328 @@
|
||||
# @mediabunny/server
|
||||
|
||||
[](https://www.npmjs.com/package/@mediabunny/server)
|
||||
[](https://bundlephobia.com/package/@mediabunny/server)
|
||||
[](https://www.npmjs.com/package/@mediabunny/server)
|
||||
[](https://discord.gg/hmpkyYuS4U)
|
||||
|
||||
<div align="center">
|
||||
<img src="./logo.svg" width="180" height="180">
|
||||
</div>
|
||||
|
||||
By default, Mediabunny requires a browser environment for full access to decoders, encoders, and video processing features. `@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) to polyfill this functionality for server-side environments such as Node, Bun, or Deno, enabling the usage of all Mediabunny features on the server. The result is a server-side media processing API that integrates naturally with TypeScript as opposed to the awkwardness and inefficiencies of calling out to the FFmpeg CLI.
|
||||
|
||||
Features added by this package include:
|
||||
- Video decoders and encoders for AVC (H.264), HEVC (H.265), VP8, VP9, AV1, and ProRes. Supports both length-prefixed and Annex B AVC/HEVC as well as transparent video via VP9 and ProRes.
|
||||
- Audio decoders and encoders for AAC, MP3, Vorbis, Opus, FLAC, AC-3 and E-AC-3. Supports AAC in both AAC and ADTS formats.
|
||||
- Video frame transformation support (resize, rotate, crop)
|
||||
- Automatic hardware acceleration on all platforms (macOS, Linux, Windows)
|
||||
- Built-in multithreading
|
||||
- Zero-copy decode and encode paths
|
||||
|
||||
> This package, like the rest of Mediabunny, is enabled by its [sponsors](https://mediabunny.dev/#sponsors) and their donations. If you've derived value from this package, please consider [leaving a donation](https://github.com/sponsors/Vanilagy)! 💘
|
||||
|
||||
> This package was made possible in large part due to seydx's amazing work on [NodeAV](https://github.com/seydx/node-av). The library is truly modern, a joy to work with, and incredibly powerful. Give them a star!
|
||||
|
||||
## Installation
|
||||
|
||||
This library peer-depends on Mediabunny. Install both using npm:
|
||||
```bash
|
||||
npm install mediabunny @mediabunny/server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { registerMediabunnyServer } from '@mediabunny/server';
|
||||
registerMediabunnyServer();
|
||||
```
|
||||
|
||||
That's it - you now have access to the full Mediabunny feature set on the server.
|
||||
|
||||
---
|
||||
|
||||
An optional `options` parameter is available for further configuration:
|
||||
```ts
|
||||
import { registerMediabunnyServer } from '@mediabunny/server';
|
||||
import * as NodeAv from 'node-av';
|
||||
|
||||
registerMediabunnyServer({
|
||||
// Use a specific hardware rendering device:
|
||||
hardwareContext: NodeAv.HardwareContext.create(
|
||||
NodeAv.AV_HWDEVICE_TYPE_VAAPI,
|
||||
'/dev/dri/renderD128',
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
## Upload media compression example
|
||||
|
||||
Here, we set up a simple media compression server in Node.js. The client's request body is streamed to Mediabunny, the media gets processed, and the output is streamed directly to the disk. Memory usage is O(1) due to pipelining, and an overly fast uploader is automatically slowed down due to stream backpressure.
|
||||
|
||||
```ts
|
||||
import { ALL_FORMATS, Conversion, FilePathTarget, Input, Mp4OutputFormat, Output, QUALITY_MEDIUM, ReadableStreamSource } from "mediabunny";
|
||||
import { registerMediabunnyServer } from "@mediabunny/server";
|
||||
import { Readable } from "node:stream";
|
||||
import http from "node:http";
|
||||
|
||||
registerMediabunnyServer();
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// Read the request body as a stream
|
||||
const stream = Readable.toWeb(req) as ReadableStream<Uint8Array>;
|
||||
const input = new Input({
|
||||
source: new ReadableStreamSource(stream),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
// Stream the output directly to the disk, could also stream to S3 etc.
|
||||
const output = new Output({
|
||||
format: new Mp4OutputFormat(),
|
||||
target: new FilePathTarget(`./converted-${crypto.randomUUID()}.mp4`),
|
||||
});
|
||||
|
||||
try {
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: async track => ({
|
||||
codec: 'avc',
|
||||
height: Math.min(720, await track.getDisplayHeight()),
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
}),
|
||||
});
|
||||
await conversion.execute();
|
||||
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
} catch (error) {
|
||||
res.statusCode = 500;
|
||||
res.end();
|
||||
|
||||
console.error("Error processing media:", error);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
For all the other ways to use Mediabunny, refer to its [guide](https://mediabunny.dev/guide/introduction).
|
||||
|
||||
## Performance
|
||||
|
||||
`@mediabunny/server` is extremely performant as it is a thin wrapper around [NodeAV](https://github.com/seydx/node-av), which itself is a thin wrapper around the FFmpeg C API. All decoders and encoders automatically run on separate threads, keeping the main thread unblocked. Hardware acceleration is automatically detected and utilized on all operating systems whenever available (unless explicitly disabled using `hardwareAcceleration: 'prefer-software'`). Video frame and audio sample data is never copied from FFmpeg unless explicitly requested via `VideoSample.copyTo()` and `AudioSample.copyTo()`, and zero-copy GPU decode -> encode paths are used automatically whenever possible.
|
||||
|
||||
## Advanced usage
|
||||
|
||||
### Usage with NodeAV
|
||||
|
||||
`@mediabunny/server` provides `AvFrameVideoSampleResource` and `AvFrameAudioSampleResource` as a means to create `VideoSample` and `AudioSample` instances that are directly backed by data residing in NodeAV's [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html) (and therefore FFmpeg's `AVFrame`) without ever having to copy data to or from JavaScript. Reading NodeAV's documentation can help you make full use of this integration.
|
||||
|
||||
To convert between Mediabunny and NodeAV (FFmpeg) worlds, you can do this:
|
||||
```ts
|
||||
import { VideoSample, AudioSample } from 'mediabunny';
|
||||
import { AvFrameVideoSampleResource, AvFrameAudioSampleResource, toAvFrame } from '@mediabunny/server';
|
||||
|
||||
// Frame -> VideoSample
|
||||
new VideoSample(new AvFrameVideoSampleResource(frame), { timestamp });
|
||||
|
||||
// Frame -> AudioSample
|
||||
new AudioSample(new AvFrameAudioSampleResource(frame));
|
||||
// (uses the timestamp in the frame)
|
||||
|
||||
// VideoSample -> Frame
|
||||
await toAvFrame(videoSample, frame);
|
||||
|
||||
// AudioSample -> Frame
|
||||
await toAvFrame(audioSample, frame);
|
||||
```
|
||||
|
||||
#### Electron example
|
||||
|
||||
For example, when using Electron, we may want to capture the app's contents without moving video data from the GPU to the CPU:
|
||||
```ts
|
||||
import { VideoSample } from 'mediabunny';
|
||||
import { AvFrameVideoSampleResource } from '@mediabunny/server';
|
||||
import { HardwareContext, SharedTexture, AV_HWDEVICE_TYPE_VIDEOTOOLBOX } from 'node-av';
|
||||
|
||||
// Create hardware context (platform-specific)
|
||||
const hw = HardwareContext.create(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
|
||||
using sharedTexture = SharedTexture.create(hw);
|
||||
|
||||
// In Electron paint event with offscreen rendering
|
||||
offscreen.webContents.on('paint', (event) => {
|
||||
const texture = event.texture;
|
||||
if (!texture?.textureInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Import as hardware frame (zero-copy)
|
||||
const frame = sharedTexture.importTexture(texture.textureInfo, { pts: 0n });
|
||||
const sample = new VideoSample(new AvFrameVideoSampleResource(frame), {
|
||||
timestamp: 0,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
texture.release();
|
||||
});
|
||||
```
|
||||
|
||||
#### Microphone recording example
|
||||
|
||||
Here, we're using NodeAV's Device API to access the user's microphone:
|
||||
```ts
|
||||
import { AudioSample } from 'mediabunny';
|
||||
import { AvFrameAudioSampleResource } from '@mediabunny/server';
|
||||
import { DeviceAPI, Decoder } from 'node-av';
|
||||
|
||||
await using mic = await DeviceAPI.openMicrophone();
|
||||
const audioStream = mic.audio()!;
|
||||
using decoder = await Decoder.create(audioStream);
|
||||
|
||||
let firstTimestamp: number | null = null;
|
||||
for await (const frame of decoder.frames(mic.packets(audioStream.index))) {
|
||||
if (!frame) {
|
||||
break;
|
||||
}
|
||||
|
||||
const sample = new AudioSample(new AvFrameAudioSampleResource(frame));
|
||||
|
||||
if (firstTimestamp === null) {
|
||||
firstTimestamp = sample.timestamp;
|
||||
}
|
||||
|
||||
// Offset timestamps so they start at 0
|
||||
sample.setTimestamp(sample.timestamp - firstTimestamp);
|
||||
|
||||
// Do something with the sample now, like passing it to an AudioSampleSource
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Video and audio processing
|
||||
|
||||
Browser environments ship with many API goodies such as the Canvas 2D API which are a naturally great fit for doing video frame processing, and they integrate well with Mediabunny. On the server, these APIs don't exist, so other approaches must be used:
|
||||
|
||||
#### VideoSample.transform()
|
||||
|
||||
This method allows for simple transformations on `VideoSample` instances and works when `@mediabunny/server` has been registered:
|
||||
```ts
|
||||
const transformed = await sample.transform({
|
||||
width: 640,
|
||||
height: 360,
|
||||
fit: 'cover',
|
||||
});
|
||||
```
|
||||
|
||||
#### NodeAV filter graphs
|
||||
|
||||
FFmpeg's `libavfilter` is an incredibly powerful and generic media processing library, and all of it is directly accessible via NodeAV. It works for both video as well as audio data.
|
||||
|
||||
For example, here we combine Mediabunny's Conversion API with a filter graph to grayscale a video:
|
||||
```ts
|
||||
import { Conversion } from 'mediabunny';
|
||||
import { AvFrameVideoSampleResource, toAvFrame } from '@mediabunny/server';
|
||||
import { Frame, FilterAPI } from 'node-av';
|
||||
|
||||
async function* one(f: Frame) { yield f; }
|
||||
|
||||
const conversion = await Conversion.init({
|
||||
// ...
|
||||
video: {
|
||||
process: async (sample) => {
|
||||
// VideoSample -> Frame
|
||||
using inFrame = new Frame();
|
||||
inFrame.alloc();
|
||||
await toAvFrame(sample, inFrame);
|
||||
|
||||
// Frame -> filter -> AvFrameVideoSampleResource
|
||||
using filter = FilterAPI.create('format=gray');
|
||||
for await (const outFrame of filter.frames(one(inFrame))) {
|
||||
return outFrame && new AvFrameVideoSampleResource(outFrame);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
// ...
|
||||
});
|
||||
await conversion.execute();
|
||||
```
|
||||
|
||||
#### Canvas API polyfills
|
||||
|
||||
Libraries like [Skia Canvas](https://github.com/samizdatco/skia-canvas) provide GPU-enabled polyfills for the Canvas 2D API. Using it with Mediabunny is simply a matter of converting from and to the Canvas API:
|
||||
```ts
|
||||
const width = videoSample.displayWidth;
|
||||
const height = videoSample.displayHeight;
|
||||
|
||||
const canvas = new Canvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Copy data from VideoSample
|
||||
const imageData = ctx.createImageData(width, height);
|
||||
await videoSample.copyTo(imageData.data, { format: 'RGBA' });
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Issue draw commands
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(20, 20, 100, 60);
|
||||
|
||||
// Convert to VideoSample again
|
||||
const pixels = ctx.getImageData(0, 0, width, height).data;
|
||||
return new VideoSample(pixels, {
|
||||
format: 'RGBA',
|
||||
codedWidth: width,
|
||||
codedHeight: height,
|
||||
timestamp: videoSample.timestamp,
|
||||
duration: videoSample.duration,
|
||||
});
|
||||
```
|
||||
|
||||
## Controlling logging
|
||||
|
||||
Especially in command-line applications you usually don't want Mediabunny interfering with your stdout and stderr output. Mediabunny provides ways to control its console output:
|
||||
```ts
|
||||
import { Logging, LogLevel } from 'mediabunny';
|
||||
|
||||
// The default: Mediabunny can log errors, warnings, and information messages.
|
||||
Logging.level = LogLevel.Info;
|
||||
|
||||
// Only log warnings and errors.
|
||||
Logging.level = LogLevel.Warnings;
|
||||
|
||||
// Only log errors.
|
||||
Logging.level = LogLevel.Errors;
|
||||
|
||||
// Don't log anything at all.
|
||||
Logging.level = LogLevel.None;
|
||||
```
|
||||
|
||||
You can also hook into log events:
|
||||
```ts
|
||||
Logging.on('error', (args: unknown[]) => {
|
||||
// Handle error message
|
||||
});
|
||||
|
||||
Logging.on('warn', (args: unknown[]) => {
|
||||
// Handle warning message
|
||||
});
|
||||
|
||||
Logging.on('info', (args: unknown[]) => {
|
||||
// Handle info message
|
||||
});
|
||||
```
|
||||
|
||||
## Implementation details
|
||||
|
||||
`@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) under the hood which provides N-API C bindings to FFmpeg's C API. Using NodeAV, this package implements [custom decoders and encoders](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) by directly using the APIs provided by `libavcodec`.
|
||||
|
||||
For encoding, video frames and audio samples are transferred to FFmpeg by converting them to an `AVFrame` and are then passed to the correct encoder. The resulting packets are then normalized into the format expected by WebCodecs and the [Mediabunny Codec Registry](https://mediabunny.dev/codec-registry/overview). For decoding, the above process is inverted: packets and decoder metadata are passed to the correct decoder, and the resulting `AVFrame` instances are wrapped in `VideoSample` or `AudioSample` instances. Video frame transformations (resize, rotate, crop) are implemented using the `libavfilter` API.
|
||||
|
||||
Whenever possible, `AVFrame`s are never copied over to JavaScript unless explicitly needed. This enables zero-copy decode -> transformation -> encode paths.
|
||||
|
||||
ProRes decoding support is provided by [TurboRes](https://github.com/Vanilagy/turbores), as it is often faster than FFmpeg.
|
||||
|
||||
## License
|
||||
|
||||
`@mediabunny/server` uses the same MPL-2.0 license as Mediabunny.
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
||||
"mainEntryPointFilePath": "dist/modules/src/index.d.ts",
|
||||
"bundledPackages": [],
|
||||
"compiler": {
|
||||
"tsconfigFilePath": "<projectFolder>/tsconfig.api-extractor.json"
|
||||
},
|
||||
"apiReport": {
|
||||
"enabled": false
|
||||
},
|
||||
"docModel": {
|
||||
"enabled": false
|
||||
},
|
||||
"dtsRollup": {
|
||||
"enabled": true,
|
||||
"untrimmedFilePath": "dist/mediabunny-server.d.ts"
|
||||
},
|
||||
"tsdocMetadata": {
|
||||
"enabled": false
|
||||
},
|
||||
"messages": {
|
||||
"compilerMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
},
|
||||
"extractorMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
},
|
||||
"tsdocMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newlineKind": "lf"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user