Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
610dc91743 | ||
|
|
e1dd95086a | ||
|
|
a1aaa8e36a | ||
|
|
aa4966a5f1 | ||
|
|
ff46a274b1 | ||
|
|
bf9e360587 | ||
|
|
1fd99b5015 | ||
|
|
6182af7c29 | ||
|
|
3e70e8912b | ||
|
|
a3aa6a69ac | ||
|
|
105a7dda2f | ||
|
|
0f6c374750 | ||
|
|
0cc564b47a | ||
|
|
57b265b897 | ||
|
|
631d207ca2 | ||
|
|
9a1b8b2602 | ||
|
|
c89e7429f7 | ||
|
|
15beb69137 | ||
|
|
dfdf7c5648 | ||
|
|
cf8cea2957 | ||
|
|
0e721daefd | ||
|
|
9fdf263482 | ||
|
|
bcde2770d1 | ||
|
|
0d5bf18864 | ||
|
|
b285360563 | ||
|
|
fc67e436e3 | ||
|
|
5e491eb0f9 | ||
|
|
97f6b35cda | ||
|
|
1ca3e48f48 | ||
|
|
5f8978271e | ||
|
|
5ee78c6480 | ||
|
|
1d6731e4f6 | ||
|
|
66997ddd16 | ||
|
|
9969b92d51 | ||
|
|
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -70,6 +70,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 +82,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, WAVE, MP3, Ogg, ADTS, FLAC, MPEG-TS, HLS
|
||||
- **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)
|
||||
|
||||
|
||||
@@ -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.registerAc3Encoder();
|
||||
MediabunnyAc3.registerAc3Decoder();
|
||||
MediabunnyFlacEncoder.registerFlacEncoder();
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
@@ -23,9 +25,7 @@
|
||||
chunked: true,
|
||||
chunkSize: 2**20
|
||||
});
|
||||
const outputFormat = new Mediabunny.HlsOutputFormat({
|
||||
segmentFormat: new Mediabunny.MpegTsOutputFormat(),
|
||||
});
|
||||
const outputFormat = new Mediabunny.Mp4OutputFormat();
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'Capturing...';
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
const output = new Mediabunny.Output({
|
||||
format: outputFormat,
|
||||
target: new Mediabunny.PathedTarget('master.m3u8', ({ path }) => new Mediabunny.BufferTarget()),
|
||||
target,
|
||||
});
|
||||
|
||||
let input;
|
||||
@@ -61,8 +61,7 @@
|
||||
|
||||
if (false) {
|
||||
input = new Mediabunny.Input({
|
||||
entryPath: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
|
||||
source: ({ path }) => new Mediabunny.UrlSource(path),
|
||||
source: new Mediabunny.UrlSource('http://localhost:8000/index.m3u8'),
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
});
|
||||
|
||||
@@ -88,15 +87,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
let ctx = null;
|
||||
//const primaryTrack = await input.getPrimaryAudioTrack();
|
||||
//const startTime = await primaryTrack.getFirstTimestamp();
|
||||
//console.log(startTime)
|
||||
|
||||
const canvas = new OffscreenCanvas(1280, 720);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
//let ctx = null;
|
||||
let conversion = await Mediabunny.Conversion.init({
|
||||
input,
|
||||
output,
|
||||
audio: (track, n) => [{
|
||||
codec: 'aac',
|
||||
}, {
|
||||
codec: 'aac',
|
||||
}],
|
||||
audio: {
|
||||
//codec: 'aac',
|
||||
//forceTranscode: true,
|
||||
//forceTranscode: true,
|
||||
//sampleFormat: 's16',
|
||||
},
|
||||
/*
|
||||
video: {
|
||||
discard: true,
|
||||
@@ -121,13 +128,12 @@
|
||||
bitrate: 320000
|
||||
},
|
||||
*/
|
||||
video: [
|
||||
{ height: 1080 },
|
||||
{ height: 720 },
|
||||
{ height: 480 },
|
||||
{ height: 360 },
|
||||
{ height: 240 },
|
||||
],
|
||||
video: {
|
||||
process: (sample) => {
|
||||
sample.draw(ctx, 0, 0, 1280, 720);
|
||||
return new Mediabunny.VideoSample(canvas, { timestamp: sample.timestamp, duration: sample.duration });
|
||||
},
|
||||
},
|
||||
tags: {} ?? {
|
||||
title: 'Bigggy',
|
||||
artist: 'Buck Bunny',
|
||||
@@ -146,14 +152,17 @@
|
||||
}
|
||||
},
|
||||
trim: {
|
||||
//end: 5,
|
||||
//start,
|
||||
//end: start + 5,
|
||||
////start: 0,
|
||||
//end: 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
button.addEventListener('click', async () => {
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
const audioTrack = stream.getAudioTracks()[0];
|
||||
const audioTrack = null && stream.getAudioTracks()[0];
|
||||
|
||||
const output = new Mediabunny.Output({
|
||||
target: new Mediabunny.BufferTarget(),
|
||||
@@ -32,7 +32,7 @@
|
||||
codec: 'vp9',
|
||||
bitrate: Mediabunny.QUALITY_MEDIUM,
|
||||
sizeChangeBehavior: 'passThrough',
|
||||
});
|
||||
}, { timestampBase: 'unix' });
|
||||
|
||||
videoSource.errorPromise.catch((d) => console.log("Hello?????", d));
|
||||
|
||||
@@ -67,9 +67,9 @@
|
||||
await output.finalize();
|
||||
|
||||
console.log(output.target.buffer);
|
||||
download(new Blob([output.target.buffer]), 'livetest' + output.format.fileExtension);
|
||||
//download(new Blob([output.target.buffer]), 'livetest' + output.format.fileExtension);
|
||||
|
||||
videoTrack?.stop();
|
||||
audioTrack?.stop();
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
/* eslint-disable @stylistic/max-len */
|
||||
import { withMermaid } from 'vitepress-plugin-mermaid';
|
||||
import footnote from 'markdown-it-footnote';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
import { HeadConfig } from 'vitepress';
|
||||
// @ts-expect-error This file gets generated once docs:generate is run
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @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({
|
||||
@@ -16,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',
|
||||
@@ -39,11 +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: [
|
||||
@@ -101,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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -127,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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -164,7 +182,7 @@ export default withMermaid({
|
||||
|
||||
footer: {
|
||||
message: 'Released under the Mozilla Public License 2.0.',
|
||||
copyright: 'Copyright © 2026-present Vanilagy',
|
||||
copyright: `Copyright © ${new Date().getFullYear()}-present Vanilagy`,
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
@@ -184,6 +202,8 @@ export default withMermaid({
|
||||
llmstxt({
|
||||
ignoreFiles: [
|
||||
'api/*',
|
||||
'examples.md',
|
||||
'llms.md',
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -195,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."
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
@@ -0,0 +1,3 @@
|
||||
import { createContentLoader } from 'vitepress';
|
||||
|
||||
export default createContentLoader('blog/*.md');
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Blog posts
|
||||
description: Announcements, news, and devblogs about Mediabunny.
|
||||
outline: false
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import { data } from './blog.data.ts';
|
||||
import BlogAuthor from './components/BlogAuthor.vue';
|
||||
</script>
|
||||
|
||||
<h1 class="!mb-8">Blog posts</h1>
|
||||
|
||||
<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="post.frontmatter.headerImage" class="shrink-0 sm:w-40 rounded" />
|
||||
<div v-if="false" class="p-1">
|
||||
<BlogAuthor :frontmatter="post.frontmatter" small />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="!m-0 text-xs opacity-70">{{ post.frontmatter.publishedOn }}</p>
|
||||
<h3 class="!m-0 group-hover:underline">{{ post.frontmatter.title }}</h3>
|
||||
<p class="text-sm !m-0">{{ post.frontmatter.excerpt }}</p>
|
||||
</div>
|
||||
</a>
|
||||
<hr v-if="i < data.length - 1" />
|
||||
</template>
|
||||
@@ -0,0 +1,262 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import BlogAuthor from '../components/BlogAuthor.vue';
|
||||
</script>
|
||||
|
||||
<img :src="$frontmatter.headerImage" class="rounded-2xl mb-2" />
|
||||
|
||||
<p class="!m-0 opacity-70">{{ $frontmatter.publishedOn }}</p>
|
||||
|
||||
<h1>{{ $frontmatter.title }}</h1>
|
||||
|
||||
<BlogAuthor />
|
||||
|
||||
Mediabunny v1.42.0 adds 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. Like the rest of Mediabunny, it has been implemented from scratch with zero dependencies and is tiny (adds about ~30 kB of additional bundle size).
|
||||
|
||||
If you wanna jump right into detailed guides for reading and writing HLS, check out [Reading HLS](../guide/reading-hls) and [Writing HLS](../guide/writing-hls). For the full release notes, see [v1.42.0](https://github.com/Vanilagy/mediabunny/releases/tag/v1.42.0).
|
||||
|
||||
## What is HTTP Live Streaming (HLS)?
|
||||
|
||||
For those unfamiliar, HLS is a protocol used to stream media over the internet using HTTP. At its core, it chunks a long piece of media (like a TV episode) into many short segments, each of which is individually addressable via HTTP, and then provides an index called a "playlist" to the user telling them about the available segments. The client then requests whichever segments it needs.
|
||||
|
||||
HLS also enables multiple variants and renditions of the same contents. Typically, this means the same video/audio content encoded with multiple bitrates, resolutions and codecs, and multiple audio tracks, one for each language.
|
||||
|
||||
## Supported features
|
||||
|
||||
One of the reasons that HLS has been in the works for so long is because I've been extremely thorough with the supported feature set.
|
||||
|
||||
Mediabunny supports:
|
||||
- HLS reading **and** writing
|
||||
- both VOD and live content
|
||||
- both unencrypted and encrypted content (including DRM-protected content)
|
||||
- an arbitrary number of video and audio tracks
|
||||
- any configuration of variant streams and media renditions
|
||||
- all segment formats (MPEG-TS, CMAF, fMP4, ADTS, MP3, WAV, ...)
|
||||
- all codecs (H.264, HEVC, AV1, AAC, MP3, WAVE, AC-3, E-AC-3, ...)
|
||||
- full lazy loading of track metadata and media, minimizing HTTP requests
|
||||
- logarithmic seeking performance
|
||||
- single-file segments via #EXT-X-BYTERANGE and HTTP range requests
|
||||
- track metadata information (language, name, primary track, autoselect, ...)
|
||||
- full master playlist configuration for writing
|
||||
- datetime-stamped media data via #EXT-X-PROGRAM-DATE-TIME
|
||||
- mid-stream discontinuities
|
||||
- I-frame only video tracks via #EXT-X-I-FRAME-STREAM-INF
|
||||
- more, probably
|
||||
|
||||
Most of the above features are implemented _symmetrically_, meaning they are available for both reading and writing operations.
|
||||
|
||||
---
|
||||
|
||||
To be fully transparent, these features are not yet supported:
|
||||
- Subtitles (WebVTT, CEA-608/708, ...)
|
||||
- ID3v2 metadata extraction
|
||||
- Writing encrypted segments
|
||||
- Low-latency HLS
|
||||
- Built-in analytics, ABR, or CMCD (this is for the user to do)
|
||||
|
||||
## Difference to existing solutions like hls.js
|
||||
|
||||
The way Mediabunny enables interaction with HLS playlists is fundamentally different from how existing tools such as [hls.js](https://github.com/video-dev/hls.js) (give them a star!) do it. To put it simply, hls.js is to Mediabunny what a `<video>` element is to FFmpeg's C API: one offers a simple, playback-focused developer experience while the other provides fine-grained control over tracks, media samples, decoding, and much more.
|
||||
|
||||
Mediabunny is not an HLS player. Mediabunny can be used to build an HLS player, and it can do many things beyond that.
|
||||
|
||||
## What can it do?
|
||||
|
||||
The API surface added by the HLS update is vast and I obviously can't cover it in this announcement. But, here are just some cool things that Mediabunny now enables:
|
||||
|
||||
### Downloading an HLS playlist as an MP4
|
||||
|
||||
By using the Conversion API, you can just do this:
|
||||
|
||||
<div class="text-[13.7142857143px]">
|
||||
|
||||
```ts
|
||||
import { ... } from 'mediabunny';
|
||||
|
||||
const input = new Input({
|
||||
source: new UrlSource('https://example.com/playlist.m3u8'),
|
||||
formats: HLS_FORMATS,
|
||||
});
|
||||
|
||||
const output = new Output({
|
||||
format: new Mp4OutputFormat(),
|
||||
target: new BufferTarget(),
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({ input, output });
|
||||
await conversion.execute();
|
||||
|
||||
// Done:
|
||||
const mp4File = output.target.buffer!;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
That's it. This will stream-download the entire HLS playlist, transcode it if necessary using hardware-accelerated decoding and encoding, and bundle it into a single MP4 for the user to download. All of this is fully pipelined, meaning memory usage is bounded ($O(1)$). Speed is usually limited by the client's internet connection.
|
||||
|
||||
### Creating an HLS playlist from a client-side video, with multiple renditions
|
||||
|
||||
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-[13.7142857143px]">
|
||||
|
||||
```ts
|
||||
import { ... } from 'mediabunny';
|
||||
|
||||
const input = new Input({
|
||||
source: new BlobSource(file), // E.g., a user-selected file
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
// This defines the shape and destination of the output files
|
||||
const output = new Output({
|
||||
format: new HlsOutputFormat({
|
||||
segmentFormat: new MpegTsOutputFormat(),
|
||||
}),
|
||||
target: new PathedTarget(
|
||||
'master.m3u8',
|
||||
async ({ path }) => new BufferTarget({
|
||||
// Upload it directly to a server
|
||||
onFinalize: buffer => fetch(`/upload?file=${encodeURIComponent(path)}`, {
|
||||
method: 'PUT',
|
||||
body: buffer,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
// Offer the video in 5 different resolutions:
|
||||
video: [
|
||||
{ codec: 'avc', height: 1080 },
|
||||
{ codec: 'avc', height: 720 },
|
||||
{ codec: 'avc', height: 480 },
|
||||
{ codec: 'avc', height: 360 },
|
||||
{ codec: 'avc', height: 240 },
|
||||
],
|
||||
// Offer the audio in AAC:
|
||||
audio: [
|
||||
{ codec: 'aac' },
|
||||
],
|
||||
});
|
||||
await conversion.execute();
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
This code creates all renditions as fast as it can, encoding them all in parallel using WebCodecs. By the end, the entire HLS file structure will have been uploaded to the server, fully client-side generated.
|
||||
|
||||
No transcode server is needed here, it's all handled by the client, and the server receives a ready-to-stream HLS playlist.
|
||||
|
||||
### Live streaming HLS playlists from the client
|
||||
|
||||
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-[13.7142857143px] overflow-auto">
|
||||
|
||||
```ts
|
||||
// Get the screen and mic
|
||||
const displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
||||
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const displayTrack = displayStream.getVideoTracks()[0];
|
||||
const micTrack = micStream.getAudioTracks()[0];
|
||||
|
||||
// Define the shape of the output
|
||||
const output = new Output({
|
||||
format: new HlsOutputFormat({
|
||||
segmentFormat: new MpegTsOutputFormat(),
|
||||
live: true, // Live mode enabled
|
||||
}),
|
||||
target: new PathedTarget(
|
||||
'master.m3u8',
|
||||
async ({ path }) => new BufferTarget({
|
||||
// Upload it directly to a server
|
||||
onFinalize: buffer => fetch(`/upload?file=${encodeURIComponent(path)}`, {
|
||||
method: 'PUT',
|
||||
body: buffer,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Full resolution video
|
||||
const videoSourceFull = new MediaStreamVideoTrackSource(displayTrack, {
|
||||
codec: 'avc',
|
||||
bitrate: QUALITY_HIGH,
|
||||
}, { timestampBase: 'unix' });
|
||||
// 480p video
|
||||
const videoSource480p = new MediaStreamVideoTrackSource(displayTrack, {
|
||||
codec: 'avc',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
transform: { height: 480 },
|
||||
}, { timestampBase: 'unix' });
|
||||
// Audio
|
||||
const audioSource = new MediaStreamAudioTrackSource(micTrack, {
|
||||
codec: 'aac',
|
||||
bitrate: QUALITY_HIGH,
|
||||
}, { timestampBase: 'unix' });
|
||||
|
||||
// The "unix" stuff ensures that #EXT-X-PROGRAM-DATE-TIME gets generated
|
||||
output.addVideoTrack(videoSourceFull, { isRelativeToUnixEpoch: true });
|
||||
output.addVideoTrack(videoSource480p, { isRelativeToUnixEpoch: true });
|
||||
output.addAudioTrack(audioSource, { isRelativeToUnixEpoch: true });
|
||||
|
||||
await output.start();
|
||||
|
||||
// Live data is now being captured.
|
||||
// ...
|
||||
|
||||
// To end the stream:
|
||||
await output.finalize();
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
All the server needs to do is store the uploaded playlists and segments; all other connected clients can then simply consume this live stream.
|
||||
|
||||
### Building a custom HLS player
|
||||
|
||||
Mediabunny's microsecond-accurate decoding and seeking means it's great for building fully-custom playback of video and audio with maximum precision and control; more than what the built-in `<video>` and `<audio>` elements can provide. The same now applies for HLS!
|
||||
|
||||
Mediabunny's official [Media player example](https://mediabunny.dev/examples/media-player/) supports HLS out of the gate, being able to play back both VOD and live content. It's an example of a fully custom HLS player that makes no use of the `<video>` element or Media Source Extensions.
|
||||
|
||||
Here's me watching German live TV in it:
|
||||
|
||||
<img src="../assets/german-live-tv.png" class="rounded-2xl shadow-xl">
|
||||
|
||||
The best thing: the player required basically no changes to be adapted for HLS. Since Mediabunny exposes the same API for HLS as it does for all other file formats, playback worked out of the box. The biggest required change was adjusting the timeline to change dynamically for live content.
|
||||
|
||||
### And much more!
|
||||
|
||||
Mediabunny's [`Input`](../guide/reading-media-files) and [`Output`](../guide/writing-media-files) APIs give you fine-grained control over basically everything, meaning you can do any arbitrary media operation on HLS playlists, such as:
|
||||
- Converting/compressing them
|
||||
- Downloading a section of a live stream
|
||||
- Extracting video thumbnails
|
||||
- Extracting specific tracks
|
||||
- Extracting metadata (duration, dimensions, tracks, ...)
|
||||
|
||||
## Finishing up
|
||||
|
||||
Most of us use phones and PCs with incredibly powerful hardware with media-specific optimization. Yet, most media processing on the web today still happens server-side. This can be expensive, slow, and horrible for users with bad or no internet. Mediabunny's goal has always been to flip this around: leverage the client's full on-device resources and modern web APIs such as WebCodecs to enable fast, secure, and practically free media processing for applications. And now, with HLS support, Mediabunny takes this mission one step further.
|
||||
|
||||
The examples in this post likely only scratch the surface of what you can now build. That's where you come in! Try it out, play around with it, and share anything cool you've built on X, GitHub, or on the [Mediabunny Discord server](https://discord.gg/hmpkyYuS4U).
|
||||
|
||||
I've personally learned a ton building this, but I'm honestly also happy it's finally done and I can move onto other Mediabunny features (such as DASH support, yay).
|
||||
|
||||
My work on Mediabunny would've been impossible were it not for all the generous [Mediabunny sponsors](https://mediabunny.dev/#sponsors). If you've benefitted from my work or want to get in touch, please consider [sponsoring](https://github.com/sponsors/Vanilagy)!
|
||||
|
||||
~David
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
frontmatter?: Record<string, any>;
|
||||
small?: boolean;
|
||||
}>();
|
||||
|
||||
const data = useData();
|
||||
const frontmatter = computed(() => props.frontmatter ?? data.frontmatter.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :href="frontmatter['authorLink']" class="inline-flex gap-2 items-center !no-underline !text-(--var-c-text-1) py-1">
|
||||
<img :src="frontmatter['authorImage']" class="size-10 rounded-full shadow" :class="{ '!size-8': small }" />
|
||||
<div class="pl-1">
|
||||
<p class="!m-0 font-semibold" :class="{ 'text-sm': small }">{{ frontmatter['author'] }}</p>
|
||||
<!--
|
||||
<p class="!m-0 text-sm opacity-60">{{ frontmatter['authorSubtitle'] }}</p>
|
||||
-->
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
@@ -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
|
||||
|
||||
@@ -43,4 +44,10 @@ features:
|
||||
target: _self
|
||||
icon:
|
||||
src: /mingcute--microphone-line.svg
|
||||
- title: HLS transcoding
|
||||
details: Convert one video into a full HLS manifest with five video renditions and one audio track.
|
||||
link: /examples/hls-transcoding/
|
||||
target: _self
|
||||
icon:
|
||||
src: /mingcute--live-line.svg
|
||||
---
|
||||
|
||||
@@ -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
|
||||
@@ -500,3 +505,26 @@ const conversion = await Conversion.init({ input, output });
|
||||
conversion.utilizedTracks; // => InputTrack[]
|
||||
```
|
||||
A track may appear multiple times in this list when [fan-out](#track-fan-out) produces multiple output tracks from it.
|
||||
|
||||
## Converting live streams
|
||||
|
||||
Live inputs, like HLS live streams, can also be used with the Conversion API. In this case, by default, the conversion will run until the live stream has ended.
|
||||
|
||||
If you only want to convert a part of the live stream instead of waiting until it has ended, you can [trim](#trimming) the conversion. For example, here we're capturing the next 60 seconds of the live stream:
|
||||
```ts
|
||||
// Get the live edge
|
||||
const currentDuration = await input.getDurationFromMetadata(undefined, {
|
||||
skipLiveWait: true,
|
||||
});
|
||||
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
trim: {
|
||||
// Start at the current live edge
|
||||
start: currentDuration,
|
||||
// End at most 60 seconds later
|
||||
end: currentDuration + 60,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
@@ -553,16 +557,15 @@ const output = new Output(...);
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: track => ({
|
||||
width: 480,
|
||||
tracks: 'primary', // Keep only the first track of each type
|
||||
video: {
|
||||
width: 480, // Resize to 480p
|
||||
bitrate: QUALITY_LOW,
|
||||
discard: track.number > 1, // Keep only the first video track
|
||||
}),
|
||||
audio: track => ({
|
||||
numberOfChannels: 1,
|
||||
},
|
||||
audio: {
|
||||
numberOfChannels: 1, // Resample to mono
|
||||
bitrate: QUALITY_LOW,
|
||||
discard: track.number > 1, // Keep only the first audio track
|
||||
}),
|
||||
},
|
||||
trim: {
|
||||
// Let's keep only the first 60 seconds
|
||||
start: 0,
|
||||
|
||||
@@ -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:
|
||||
@@ -352,7 +385,10 @@ using input = new Input({
|
||||
formats: ALL_FORMATS,
|
||||
formatOptions: {
|
||||
isobmff: {
|
||||
resolveKeyId: ({ keyId }) => {
|
||||
resolveKeyId: ({ keyId, psshBoxes }) => {
|
||||
// psshBoxes contains Protection System Specific Header boxes
|
||||
// relevant to this key ID. They can be used to obtain a
|
||||
// decryption key from a DRM license server.
|
||||
const key = keyMap.get(keyId);
|
||||
if (!key) {
|
||||
throw new Error('Unknown key ID.');
|
||||
@@ -367,4 +403,4 @@ using input = new Input({
|
||||
|
||||
## Subtitles
|
||||
|
||||
Reading subtitles from HLS playlists is not currently supported. Sorry!
|
||||
Reading subtitles from HLS playlists is not currently supported. Sorry!
|
||||
|
||||
@@ -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'` | ✓ | ✓ | ✓ | | | ✓ | | | | ✓ |
|
||||
@@ -99,6 +105,9 @@ Not all codecs can be used with all containers. The following table specifies th
|
||||
| `'alaw'` | | ✓ | | | | | ✓ | | | |
|
||||
| `'webvtt'`[^webvtt] | (✓) | | (✓) | (✓) | | | | | | |
|
||||
|
||||
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.
|
||||
@@ -287,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
|
||||
@@ -350,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]
|
||||
---
|
||||
|
||||
@@ -89,16 +90,23 @@ console.log(writtenFiles);
|
||||
|
||||
```ts
|
||||
const root = await navigator.storage.getDirectory();
|
||||
const writePromises: Promise<void>[] = [];
|
||||
|
||||
const output = new Output({
|
||||
target: new PathedTarget(
|
||||
'master.m3u8',
|
||||
async ({ path }) => {
|
||||
const handle = await root.getFileHandle(path, { create: true });
|
||||
const writable = await handle.createWritable();
|
||||
return new StreamTarget(writable);
|
||||
},
|
||||
async ({ path }) => new BufferTarget({
|
||||
onFinalize: (buffer) => {
|
||||
writePromises.push((async () => {
|
||||
const handle = await root.getFileHandle(path, { create: true });
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(buffer);
|
||||
await writable.close();
|
||||
})());
|
||||
},
|
||||
}),
|
||||
),
|
||||
onFinalize: () => Promise.all(writePromises),
|
||||
// ...
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
title: Mediabunny
|
||||
title: Mediabunny — A complete JavaScript media toolkit for the browser
|
||||
titleTemplate: false
|
||||
|
||||
hero:
|
||||
name: Mediabunny
|
||||
@@ -20,6 +21,9 @@ hero:
|
||||
- theme: alt
|
||||
text: Examples
|
||||
link: /examples
|
||||
- theme: alt
|
||||
text: Blog
|
||||
link: /blog
|
||||
- theme: alt
|
||||
text: Sponsors
|
||||
link: "#sponsors"
|
||||
@@ -106,9 +110,11 @@ const sponsors = {
|
||||
{ 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' },
|
||||
@@ -116,8 +122,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' },
|
||||
@@ -129,6 +138,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' },
|
||||
@@ -141,6 +151,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>
|
||||
@@ -151,7 +162,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>
|
||||
@@ -169,7 +180,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
|
||||
@@ -183,7 +194,7 @@ npm install mediabunny
|
||||
```ts
|
||||
const input = new Input({
|
||||
source: new UrlSource('./bigbuckbunny.mp4'),
|
||||
formats: ALL_FORMATS, // .mp4, .webm, .wav, ...
|
||||
formats: ALL_FORMATS, // .mp4, .webm, .wav, .m3u8, ...
|
||||
});
|
||||
|
||||
const duration = await input.computeDuration();
|
||||
@@ -245,7 +256,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
|
||||
@@ -256,7 +267,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
|
||||
@@ -292,14 +303,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
|
||||
@@ -325,14 +336,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">
|
||||
@@ -365,10 +376,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">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z"/><path fill="#06b6d4" d="M16.95 2.586a1 1 0 0 1 0 1.414l-3 3H19a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h3.636L7.05 5.414A1 1 0 0 1 8.465 4l2.474 2.475a.5.5 0 0 0 .707 0l3.89-3.89a1 1 0 0 1 1.414 0M19 9H5v10h14zM8.98 11.547a1.232 1.232 0 0 1 1.72-.994a22 22 0 0 1 2.2 1.123a22 22 0 0 1 2.075 1.346c.668.494.67 1.489 0 1.984A22 22 0 0 1 12.9 16.35c-.997.576-1.785.943-2.2 1.124a1.23 1.23 0 0 1-1.72-.993a23 23 0 0 1-.128-2.467c0-1.14.078-2.014.128-2.467m1.902 1.306a23 23 0 0 0 0 2.32a23 23 0 0 0 2.008-1.16a23 23 0 0 0-2.008-1.16"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 260 KiB |
@@ -5,6 +5,8 @@
|
||||
"module": "NodeNext"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.vue",
|
||||
".vitepress/config.mts",
|
||||
".vitepress/theme/index.ts",
|
||||
]
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -62,31 +62,42 @@ const compressFile = async (resource: File | string) => {
|
||||
currentConversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
video: track => ({
|
||||
tracks: 'primary', // Keep only one track per type
|
||||
video: {
|
||||
width: 320, // Height will be deduced automatically to retain aspect ratio
|
||||
bitrate: QUALITY_VERY_LOW,
|
||||
discard: track.number > 1, // Keep only the first video track
|
||||
}),
|
||||
audio: track => ({
|
||||
bitrate: 32e3,
|
||||
discard: track.number > 1, // Keep only the first audio track
|
||||
}),
|
||||
},
|
||||
audio: {
|
||||
codec: 'opus',
|
||||
bitrate: QUALITY_VERY_LOW,
|
||||
},
|
||||
});
|
||||
|
||||
if (!currentConversion.isValid) {
|
||||
console.info(currentConversion.discardedTracks);
|
||||
throw new Error('Conversion is invalid and cannot be executed; see the console for more.');
|
||||
}
|
||||
|
||||
// Keep track of progress
|
||||
let progress = 0;
|
||||
currentConversion.onProgress = newProgress => progress = newProgress;
|
||||
let processedTime = 0;
|
||||
let startTime: number | null = null;
|
||||
|
||||
const fileDuration = (await input.computeDuration()) - (await input.getFirstTimestamp());
|
||||
const startTime = performance.now();
|
||||
currentConversion.onProgress = (newProgress, newProcessedTime) => {
|
||||
progress = newProgress;
|
||||
processedTime = newProcessedTime;
|
||||
startTime ??= performance.now();
|
||||
};
|
||||
|
||||
const updateProgress = () => {
|
||||
progressBar.style.width = `${progress * 100}%`;
|
||||
|
||||
const now = performance.now();
|
||||
const elapsedSeconds = (now - startTime) / 1000;
|
||||
const factor = fileDuration / (elapsedSeconds / progress);
|
||||
speedometer.textContent = `Speed: ~${factor.toPrecision(3)}x real time`;
|
||||
if (startTime !== null) {
|
||||
const now = performance.now();
|
||||
const elapsedSeconds = (now - startTime) / 1000;
|
||||
const factor = processedTime / elapsedSeconds;
|
||||
speedometer.textContent = `Speed: ~${factor.toPrecision(3)}x real time`;
|
||||
}
|
||||
};
|
||||
|
||||
// Update the progress indicator regularly
|
||||
@@ -143,7 +154,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">
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import {
|
||||
Input,
|
||||
ALL_FORMATS,
|
||||
BlobSource,
|
||||
UrlSource,
|
||||
Output,
|
||||
PathedTarget,
|
||||
BufferTarget,
|
||||
HlsOutputFormat,
|
||||
MpegTsOutputFormat,
|
||||
Conversion,
|
||||
QUALITY_VERY_HIGH,
|
||||
QUALITY_HIGH,
|
||||
QUALITY_MEDIUM,
|
||||
QUALITY_LOW,
|
||||
QUALITY_VERY_LOW,
|
||||
} from 'mediabunny';
|
||||
|
||||
import SampleFileUrl from '../../docs/assets/big-buck-bunny-trimmed.mp4';
|
||||
(document.querySelector('#sample-file-download') as HTMLAnchorElement).href = SampleFileUrl;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
showDirectoryPicker(options: { mode: 'readwrite' }): Promise<FileSystemDirectoryHandle>;
|
||||
}
|
||||
}
|
||||
|
||||
const selectDirectoryButton = document.querySelector('#select-directory') as HTMLButtonElement;
|
||||
const directoryNameElement = document.querySelector('#directory-name') as HTMLParagraphElement;
|
||||
const selectMediaButton = document.querySelector('#select-file') as HTMLButtonElement;
|
||||
const loadSampleButton = document.querySelector('#load-sample') as HTMLButtonElement;
|
||||
const fileNameElement = document.querySelector('#file-name') as HTMLParagraphElement;
|
||||
const dashboard = document.querySelector('#dashboard') as HTMLDivElement;
|
||||
const statusElement = document.querySelector('#status') as HTMLParagraphElement;
|
||||
const progressBar = document.querySelector('#progress-bar') as HTMLDivElement;
|
||||
const percentIndicator = document.querySelector('#percent-indicator') as HTMLParagraphElement;
|
||||
const speedometer = document.querySelector('#speedometer') as HTMLParagraphElement;
|
||||
const bytesWrittenElement = document.querySelector('#bytes-written') as HTMLParagraphElement;
|
||||
const filesCreatedElement = document.querySelector('#files-created') as HTMLParagraphElement;
|
||||
const latestFileElement = document.querySelector('#latest-file') as HTMLParagraphElement;
|
||||
const errorElement = document.querySelector('#error-element') as HTMLParagraphElement;
|
||||
|
||||
let currentConversion: Conversion | null = null;
|
||||
let directoryHandle: FileSystemDirectoryHandle | null = null;
|
||||
let progress = 0;
|
||||
let processedTime = 0;
|
||||
let startTime: number | null = null;
|
||||
let bytesWritten = 0;
|
||||
let filesCreated = 0;
|
||||
let latestFile = '-';
|
||||
let status = 'Waiting for directory';
|
||||
let fileName = '';
|
||||
let directoryName = '';
|
||||
let errorMessage = '';
|
||||
let renderIntervalId = -1;
|
||||
const filePromises: Promise<void>[] = [];
|
||||
|
||||
const convertToHls = async (resource: File | string) => {
|
||||
await currentConversion?.cancel();
|
||||
|
||||
resetDashboard();
|
||||
fileName = resource instanceof File ? resource.name : resource;
|
||||
updateFileUi();
|
||||
|
||||
clearInterval(renderIntervalId);
|
||||
renderIntervalId = window.setInterval(render, 1000 / 60);
|
||||
render();
|
||||
|
||||
try {
|
||||
// Load the input
|
||||
const source = resource instanceof File
|
||||
? new BlobSource(resource)
|
||||
: new UrlSource(resource);
|
||||
const input = new Input({
|
||||
source,
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const output = new Output({
|
||||
// Define the output format (HLS with MPEG-TS segments)
|
||||
format: new HlsOutputFormat({
|
||||
segmentFormat: new MpegTsOutputFormat(),
|
||||
}),
|
||||
// Describe where the files will be written
|
||||
target: new PathedTarget(
|
||||
'master.m3u8',
|
||||
({ path }) => createFileTarget(path),
|
||||
),
|
||||
onFinalize: () => Promise.all(filePromises),
|
||||
});
|
||||
|
||||
currentConversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
tracks: 'primary', // Use only the primary video and audio tracks of the input
|
||||
video: [
|
||||
{ codec: 'avc', height: 1080, bitrate: QUALITY_VERY_HIGH },
|
||||
{ codec: 'avc', height: 720, bitrate: QUALITY_HIGH },
|
||||
{ codec: 'avc', height: 480, bitrate: QUALITY_MEDIUM },
|
||||
{ codec: 'avc', height: 360, bitrate: QUALITY_LOW },
|
||||
{ codec: 'avc', height: 240, bitrate: QUALITY_VERY_LOW },
|
||||
],
|
||||
audio: [
|
||||
{ codec: 'aac', bitrate: QUALITY_HIGH },
|
||||
],
|
||||
});
|
||||
|
||||
if (!currentConversion.isValid) {
|
||||
console.info(currentConversion.discardedTracks);
|
||||
throw new Error('Conversion is invalid and cannot be executed; see the console for more.');
|
||||
}
|
||||
|
||||
currentConversion.onProgress = (newProgress, newProcessedTime) => {
|
||||
progress = newProgress;
|
||||
processedTime = newProcessedTime;
|
||||
startTime ??= performance.now();
|
||||
};
|
||||
|
||||
status = 'Encoding renditions and writing HLS files';
|
||||
|
||||
await currentConversion.execute();
|
||||
|
||||
progress = 1;
|
||||
status = 'HLS manifest complete';
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
await currentConversion?.cancel();
|
||||
status = 'Conversion failed';
|
||||
errorMessage = String(error);
|
||||
updateFileUi();
|
||||
} finally {
|
||||
clearInterval(renderIntervalId);
|
||||
renderIntervalId = -1;
|
||||
render();
|
||||
}
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes < 1000) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
const units = ['kB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes / 1000;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1000) {
|
||||
size /= 1000;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${+size.toFixed(2)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
const percentage = Math.floor(progress * 100);
|
||||
const displayedPercentage = status === 'HLS manifest complete' ? 100 : Math.min(percentage, 99);
|
||||
progressBar.style.width = `${displayedPercentage}%`;
|
||||
percentIndicator.textContent = `${displayedPercentage}%`;
|
||||
bytesWrittenElement.textContent = formatBytes(bytesWritten);
|
||||
filesCreatedElement.textContent = filesCreated.toString();
|
||||
latestFileElement.textContent = latestFile;
|
||||
statusElement.textContent = status;
|
||||
|
||||
if (startTime !== null) {
|
||||
const elapsedSeconds = (performance.now() - startTime) / 1000;
|
||||
const factor = processedTime / elapsedSeconds;
|
||||
speedometer.textContent = `${factor.toPrecision(3)}x`;
|
||||
} else {
|
||||
speedometer.textContent = '-';
|
||||
}
|
||||
};
|
||||
|
||||
const updateFileUi = () => {
|
||||
directoryNameElement.textContent = directoryName;
|
||||
fileNameElement.textContent = fileName;
|
||||
errorElement.textContent = errorMessage;
|
||||
};
|
||||
|
||||
const resetDashboard = () => {
|
||||
progress = 0;
|
||||
processedTime = 0;
|
||||
startTime = null;
|
||||
bytesWritten = 0;
|
||||
filesCreated = 0;
|
||||
latestFile = '-';
|
||||
status = 'Preparing HLS output';
|
||||
errorMessage = '';
|
||||
filePromises.length = 0;
|
||||
dashboard.classList.remove('opacity-50');
|
||||
updateFileUi();
|
||||
};
|
||||
|
||||
const createFileTarget = async (path: string) => {
|
||||
const target = new BufferTarget({
|
||||
onFinalize: (buffer) => {
|
||||
filePromises.push((async () => {
|
||||
const handle = await directoryHandle!.getFileHandle(path, { create: true });
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(buffer);
|
||||
await writable.close();
|
||||
})());
|
||||
},
|
||||
});
|
||||
|
||||
let fileBytes = 0;
|
||||
|
||||
filesCreated++;
|
||||
latestFile = path;
|
||||
|
||||
target.on('write', ({ end }) => {
|
||||
const newFileBytes = Math.max(fileBytes, end);
|
||||
bytesWritten += newFileBytes - fileBytes;
|
||||
fileBytes = newFileBytes;
|
||||
});
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
selectDirectoryButton.addEventListener('click', async () => {
|
||||
directoryHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||
directoryName = `Selected directory: ${directoryHandle.name}`;
|
||||
status = 'Waiting for source video';
|
||||
selectDirectoryButton.style.display = 'none';
|
||||
selectMediaButton.disabled = false;
|
||||
loadSampleButton.disabled = false;
|
||||
directoryNameElement.style.display = '';
|
||||
directoryNameElement.textContent = directoryName;
|
||||
statusElement.textContent = status;
|
||||
});
|
||||
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,video/mp2t,.ts';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files![0];
|
||||
if (file) {
|
||||
void convertToHls(file);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
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://mediabunny.dev/big-buck-bunny.mp4',
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
void convertToHls(url);
|
||||
});
|
||||
|
||||
document.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer!.dropEffect = 'copy';
|
||||
});
|
||||
|
||||
document.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
const files = event.dataTransfer!.files;
|
||||
const file = files[0];
|
||||
if (file) {
|
||||
void convertToHls(file);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US" translate="no">
|
||||
<head>
|
||||
<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">
|
||||
<h1 class="text-center text-3xl font-bold text-cyan-500">HLS transcoding example</h1>
|
||||
<p class="max-w-2xl text-center">
|
||||
Select a directory, then a video. Mediabunny will create a complete HLS VOD package with five video renditions and one audio track.
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex flex-col items-center gap-1">
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<button id="select-directory" class="rounded-lg bg-cyan-500 px-5 py-2 font-medium text-white hover:bg-cyan-600">
|
||||
Select directory
|
||||
</button>
|
||||
|
||||
<button id="select-file" disabled class="rounded-lg bg-zinc-200 px-5 py-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-zinc-750 dark:hover:bg-zinc-700 hover:bg-zinc-300">
|
||||
Select local file
|
||||
</button>
|
||||
|
||||
<button id="load-sample" disabled class="rounded-lg bg-zinc-200 px-5 py-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-zinc-750 dark:hover:bg-zinc-700 hover:bg-zinc-300">
|
||||
Load remote URL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<a id="sample-file-download" download="big-buck-bunny-trimmed.mp4" class="text-xs opacity-50 hover:opacity-70 hover:underline">
|
||||
Download sample file
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p id="directory-name" class="mt-3 max-w-[36rem] text-center text-xs opacity-60" style="display: none;"></p>
|
||||
<p id="file-name" class="min-h-5 max-w-[36rem] text-center text-xs opacity-60"></p>
|
||||
<p id="error-element" class="mx-auto mt-2 max-w-[36rem] text-center text-red-500"></p>
|
||||
|
||||
<hr class="my-4 w-full max-w-4xl border-zinc-300 dark:border-zinc-700">
|
||||
|
||||
<div class="grid w-full max-w-4xl gap-4 md:grid-cols-2">
|
||||
<section class="rounded-xl bg-zinc-200 p-5 dark:bg-zinc-750">
|
||||
<h2 class="text-lg font-bold text-cyan-500">Renditions</h2>
|
||||
<div class="mt-3 grid gap-2 text-sm">
|
||||
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
|
||||
<p class="font-bold">1080p</p>
|
||||
<p class="opacity-70">AVC video</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
|
||||
<p class="font-bold">720p</p>
|
||||
<p class="opacity-70">AVC video</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
|
||||
<p class="font-bold">480p</p>
|
||||
<p class="opacity-70">AVC video</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
|
||||
<p class="font-bold">360p</p>
|
||||
<p class="opacity-70">AVC video</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
|
||||
<p class="font-bold">240p</p>
|
||||
<p class="opacity-70">AVC video</p>
|
||||
</div>
|
||||
<hr class="my-1 border-zinc-300 dark:border-zinc-700">
|
||||
<div class="grid grid-cols-[5rem_1fr] rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
|
||||
<p class="font-bold">Audio</p>
|
||||
<p class="opacity-70">AAC audio</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="dashboard" class="rounded-xl bg-zinc-200 p-5 opacity-50 transition-opacity dark:bg-zinc-750">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-cyan-500">Progress</h2>
|
||||
<p id="status" class="mt-1 text-sm opacity-70">Waiting for directory</p>
|
||||
</div>
|
||||
<p id="percent-indicator" class="text-4xl font-bold tabular-nums text-cyan-500">0%</p>
|
||||
</div>
|
||||
|
||||
<div id="progress-bar-container" class="mt-4 h-3 overflow-hidden rounded-full bg-zinc-300 dark:bg-zinc-900">
|
||||
<div id="progress-bar" class="h-full w-0 bg-emerald-500 transition-[width] duration-150"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-2 text-sm">
|
||||
<div class="rounded-lg bg-zinc-100 p-3 dark:bg-zinc-800">
|
||||
<p class="text-xs opacity-60">Real-time speed</p>
|
||||
<p id="speedometer" class="mt-1 text-lg font-bold tabular-nums">-</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-100 p-3 dark:bg-zinc-800">
|
||||
<p class="text-xs opacity-60">Bytes written</p>
|
||||
<p id="bytes-written" class="mt-1 text-lg font-bold tabular-nums">0 B</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-100 p-3 dark:bg-zinc-800">
|
||||
<p class="text-xs opacity-60">Files created</p>
|
||||
<p id="files-created" class="mt-1 text-lg font-bold tabular-nums">0</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-100 p-3 dark:bg-zinc-800">
|
||||
<p class="text-xs opacity-60">Latest file</p>
|
||||
<p id="latest-file" class="mt-1 truncate text-lg font-bold tabular-nums">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<a href="/" class="fixed left-0 top-0 flex items-center gap-2 px-5 py-2">
|
||||
<img src="../../docs/public/mediabunny-logo.svg" class="size-6">
|
||||
<p class="text-sm font-medium">Mediabunny</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/Vanilagy/mediabunny/tree/main/examples/hls-transcoding"
|
||||
target="_blank"
|
||||
class="fixed right-0 top-0 flex items-center gap-2 rounded-bl-xl bg-zinc-200 px-5 py-2 hover:bg-zinc-300 dark:bg-zinc-750 dark:hover:bg-zinc-700"
|
||||
>
|
||||
<img src="../../docs/assets/github-mark.svg" class="size-6 dark:invert">
|
||||
<p>View source code</p>
|
||||
</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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">
|
||||
|
||||
@@ -215,7 +215,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';
|
||||
@@ -749,7 +749,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">
|
||||
|
||||
@@ -139,7 +139,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,19 +1,29 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.42.0-beta.6",
|
||||
"version": "1.50.4",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
".",
|
||||
"packages/*"
|
||||
],
|
||||
"main": "./dist/bundles/mediabunny.cjs",
|
||||
"module": "./dist/modules/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.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",
|
||||
"bump-minor": "npm version minor --no-git-tag-version --workspaces",
|
||||
"bump-major": "npm version major --no-git-tag-version --workspaces",
|
||||
"set-version": "npm version --no-git-tag-version --workspaces"
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -81,6 +81,9 @@ const output = new Output({
|
||||
const conversion = await Conversion.init({
|
||||
input,
|
||||
output,
|
||||
audio: {
|
||||
codec: 'aac',
|
||||
},
|
||||
});
|
||||
await conversion.execute();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/aac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.42.0-beta.6",
|
||||
"version": "1.50.4",
|
||||
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "libavcodec/avcodec.h"
|
||||
@@ -20,7 +21,7 @@ typedef struct {
|
||||
AVFrame *frame;
|
||||
float *input_buffer;
|
||||
int input_buffer_size;
|
||||
int encoded_pts;
|
||||
int64_t encoded_pts;
|
||||
int encoded_duration;
|
||||
} EncoderContext;
|
||||
|
||||
@@ -123,7 +124,7 @@ float *get_encode_input_ptr(EncoderContext *ctx, int size) {
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int send_frame(EncoderContext *ctx, int pts) {
|
||||
int send_frame(EncoderContext *ctx, int64_t pts) {
|
||||
int channels = ctx->codec_ctx->ch_layout.nb_channels;
|
||||
int frame_size = ctx->frame->nb_samples;
|
||||
|
||||
@@ -171,7 +172,7 @@ uint8_t *get_encoded_data(EncoderContext *ctx) {
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_encoded_pts(EncoderContext *ctx) {
|
||||
int64_t get_encoded_pts(EncoderContext *ctx) {
|
||||
return ctx->encoded_pts;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ let getEncoderFrameSize: (ctx: number) => number;
|
||||
let getEncoderExtradata: (ctx: number) => number;
|
||||
let getEncoderExtradataSize: (ctx: number) => number;
|
||||
let getEncodeInputPtr: (ctx: number, size: number) => number;
|
||||
let sendFrameFn: (ctx: number, pts: number) => number;
|
||||
let sendFrameFn: (ctx: number, pts: bigint) => number;
|
||||
let receivePacketFn: (ctx: number) => number;
|
||||
let flushEncoderStartFn: (ctx: number) => void;
|
||||
let resetEncoderFn: (ctx: number) => void;
|
||||
let getEncodedData: (ctx: number) => number;
|
||||
let getEncodedPts: (ctx: number) => number;
|
||||
let getEncodedPts: (ctx: number) => bigint;
|
||||
let getEncodedDuration: (ctx: number) => number;
|
||||
const ensureModule = async () => {
|
||||
if (!module) {
|
||||
@@ -43,12 +43,12 @@ const ensureModule = async () => {
|
||||
getEncoderExtradata = module.cwrap('get_encoder_extradata', 'number', ['number']);
|
||||
getEncoderExtradataSize = module.cwrap('get_encoder_extradata_size', 'number', ['number']);
|
||||
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
|
||||
sendFrameFn = module.cwrap('send_frame', 'number', ['number', 'number']);
|
||||
sendFrameFn = module.cwrap('send_frame', 'number', ['number', 'number']) as unknown as typeof sendFrameFn;
|
||||
receivePacketFn = module.cwrap('receive_packet', 'number', ['number']);
|
||||
flushEncoderStartFn = module.cwrap('flush_encoder_start', null, ['number']);
|
||||
resetEncoderFn = module.cwrap('reset_encoder', null, ['number']);
|
||||
getEncodedData = module.cwrap('get_encoded_data', 'number', ['number']);
|
||||
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']);
|
||||
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']) as unknown as typeof getEncodedPts;
|
||||
getEncodedDuration = module.cwrap('get_encoded_duration', 'number', ['number']);
|
||||
}
|
||||
};
|
||||
@@ -81,7 +81,7 @@ const drainPackets = (ctx: number) => {
|
||||
while ((size = receivePacketFn(ctx)) > 0) {
|
||||
const ptr = getEncodedData(ctx);
|
||||
const encodedData = module.HEAPU8.slice(ptr, ptr + size).buffer;
|
||||
const pts = getEncodedPts(ctx);
|
||||
const pts = Number(getEncodedPts(ctx));
|
||||
const duration = getEncodedDuration(ctx);
|
||||
packets.push({ encodedData, pts, duration });
|
||||
}
|
||||
@@ -98,7 +98,7 @@ const encode = (ctx: number, audioData: ArrayBuffer, timestamp: number) => {
|
||||
}
|
||||
module.HEAPU8.set(audioBytes, inputPtr);
|
||||
|
||||
const ret = sendFrameFn(ctx, timestamp);
|
||||
const ret = sendFrameFn(ctx, BigInt(timestamp));
|
||||
if (ret < 0) {
|
||||
throw new Error(`Encode failed with error code ${ret}.`);
|
||||
}
|
||||
|
||||
@@ -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.6",
|
||||
"version": "1.50.4",
|
||||
"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",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "libavcodec/avcodec.h"
|
||||
@@ -72,7 +73,7 @@ uint8_t *configure_decode_packet(DecoderContext *ctx, int size) {
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int decode_packet(DecoderContext *ctx, int pts) {
|
||||
int decode_packet(DecoderContext *ctx, int64_t pts) {
|
||||
ctx->packet->pts = pts;
|
||||
int ret = avcodec_send_packet(ctx->codec_ctx, ctx->packet);
|
||||
av_packet_unref(ctx->packet);
|
||||
@@ -110,8 +111,8 @@ int get_decoded_sample_count(DecoderContext *ctx) {
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_decoded_pts(DecoderContext *ctx) {
|
||||
return (int)ctx->frame->pts;
|
||||
int64_t get_decoded_pts(DecoderContext *ctx) {
|
||||
return ctx->frame->pts;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
@@ -135,7 +136,7 @@ typedef struct {
|
||||
AVFrame *frame;
|
||||
float *input_buffer;
|
||||
int input_buffer_size;
|
||||
int encoded_pts;
|
||||
int64_t encoded_pts;
|
||||
int encoded_duration;
|
||||
} EncoderContext;
|
||||
|
||||
@@ -229,7 +230,7 @@ float *get_encode_input_ptr(EncoderContext *ctx, int size) {
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int encode_frame(EncoderContext *ctx, int pts) {
|
||||
int encode_frame(EncoderContext *ctx, int64_t pts) {
|
||||
int channels = ctx->codec_ctx->ch_layout.nb_channels;
|
||||
int frame_size = ctx->frame->nb_samples;
|
||||
|
||||
@@ -270,7 +271,7 @@ uint8_t *get_encoded_data(EncoderContext *ctx) {
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int get_encoded_pts(EncoderContext *ctx) {
|
||||
int64_t get_encoded_pts(EncoderContext *ctx) {
|
||||
return ctx->encoded_pts;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,23 +18,23 @@ let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
|
||||
|
||||
let initDecoderFn: (codecId: number) => number;
|
||||
let configureDecodePacket: (ctx: number, size: number) => number;
|
||||
let decodePacket: (ctx: number, pts: number) => number;
|
||||
let decodePacket: (ctx: number, pts: bigint) => number;
|
||||
let getDecodedFormat: (ctx: number) => number;
|
||||
let getDecodedPlanePtr: (ctx: number, plane: number) => number;
|
||||
let getDecodedChannels: (ctx: number) => number;
|
||||
let getDecodedSampleRate: (ctx: number) => number;
|
||||
let getDecodedSampleCount: (ctx: number) => number;
|
||||
let getDecodedPts: (ctx: number) => number;
|
||||
let getDecodedPts: (ctx: number) => bigint;
|
||||
let flushDecoderFn: (ctx: number) => void;
|
||||
let closeDecoderFn: (ctx: number) => void;
|
||||
|
||||
let initEncoderFn: (codecId: number, channels: number, sampleRate: number, bitrate: number) => number;
|
||||
let getEncoderFrameSize: (ctx: number) => number;
|
||||
let getEncodeInputPtr: (ctx: number, size: number) => number;
|
||||
let encodeFrameFn: (ctx: number, pts: number) => number;
|
||||
let encodeFrameFn: (ctx: number, pts: bigint) => number;
|
||||
let flushEncoderFn: (ctx: number) => void;
|
||||
let getEncodedData: (ctx: number) => number;
|
||||
let getEncodedPts: (ctx: number) => number;
|
||||
let getEncodedPts: (ctx: number) => bigint;
|
||||
let getEncodedDuration: (ctx: number) => number;
|
||||
let closeEncoderFn: (ctx: number) => void;
|
||||
|
||||
@@ -53,23 +53,23 @@ const ensureModule = async () => {
|
||||
|
||||
initDecoderFn = module.cwrap('init_decoder', 'number', ['number']);
|
||||
configureDecodePacket = module.cwrap('configure_decode_packet', 'number', ['number', 'number']);
|
||||
decodePacket = module.cwrap('decode_packet', 'number', ['number', 'number']);
|
||||
decodePacket = module.cwrap('decode_packet', 'number', ['number', 'number']) as unknown as typeof decodePacket;
|
||||
getDecodedFormat = module.cwrap('get_decoded_format', 'number', ['number']);
|
||||
getDecodedPlanePtr = module.cwrap('get_decoded_plane_ptr', 'number', ['number', 'number']);
|
||||
getDecodedChannels = module.cwrap('get_decoded_channels', 'number', ['number']);
|
||||
getDecodedSampleRate = module.cwrap('get_decoded_sample_rate', 'number', ['number']);
|
||||
getDecodedSampleCount = module.cwrap('get_decoded_sample_count', 'number', ['number']);
|
||||
getDecodedPts = module.cwrap('get_decoded_pts', 'number', ['number']);
|
||||
getDecodedPts = module.cwrap('get_decoded_pts', 'number', ['number']) as unknown as typeof getDecodedPts;
|
||||
flushDecoderFn = module.cwrap('flush_decoder', null, ['number']);
|
||||
closeDecoderFn = module.cwrap('close_decoder', null, ['number']);
|
||||
|
||||
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number', 'number']);
|
||||
getEncoderFrameSize = module.cwrap('get_encoder_frame_size', 'number', ['number']);
|
||||
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
|
||||
encodeFrameFn = module.cwrap('encode_frame', 'number', ['number', 'number']);
|
||||
encodeFrameFn = module.cwrap('encode_frame', 'number', ['number', 'number']) as unknown as typeof encodeFrameFn;
|
||||
flushEncoderFn = module.cwrap('flush_encoder', null, ['number']);
|
||||
getEncodedData = module.cwrap('get_encoded_data', 'number', ['number']);
|
||||
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']);
|
||||
getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']) as unknown as typeof getEncodedPts;
|
||||
getEncodedDuration = module.cwrap('get_encoded_duration', 'number', ['number']);
|
||||
closeEncoderFn = module.cwrap('close_encoder', null, ['number']);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ const decode = (ctx: number, encodedData: ArrayBuffer, timestamp: number) => {
|
||||
|
||||
module.HEAPU8.set(bytes, dataPtr);
|
||||
|
||||
const ret = decodePacket(ctx, timestamp);
|
||||
const ret = decodePacket(ctx, BigInt(timestamp));
|
||||
if (ret < 0) {
|
||||
throw new Error(`Decode failed with error code ${ret}.`);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ const decode = (ctx: number, encodedData: ArrayBuffer, timestamp: number) => {
|
||||
const channels = getDecodedChannels(ctx);
|
||||
const sampleRate = getDecodedSampleRate(ctx);
|
||||
const sampleCount = getDecodedSampleCount(ctx);
|
||||
const pts = getDecodedPts(ctx);
|
||||
const pts = Number(getDecodedPts(ctx));
|
||||
|
||||
let pcmData: ArrayBuffer;
|
||||
if (info.planar) {
|
||||
@@ -169,14 +169,14 @@ const encode = (ctx: number, audioData: ArrayBuffer, timestamp: number) => {
|
||||
}
|
||||
module.HEAPU8.set(audioBytes, inputPtr);
|
||||
|
||||
const bytesWritten = encodeFrameFn(ctx, timestamp);
|
||||
const bytesWritten = encodeFrameFn(ctx, BigInt(timestamp));
|
||||
if (bytesWritten < 0) {
|
||||
throw new Error(`Encode failed with error code ${bytesWritten}.`);
|
||||
}
|
||||
|
||||
const ptr = getEncodedData(ctx);
|
||||
const encodedData = module.HEAPU8.slice(ptr, ptr + bytesWritten).buffer;
|
||||
const pts = getEncodedPts(ctx);
|
||||
const pts = Number(getEncodedPts(ctx));
|
||||
const duration = getEncodedDuration(ctx);
|
||||
|
||||
return { encodedData, pts, duration };
|
||||
|
||||
@@ -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,'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.42.0-beta.6",
|
||||
"version": "1.50.4",
|
||||
"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);
|
||||
|
||||
@@ -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.6",
|
||||
"version": "1.50.4",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||