Compare commits

..
155 Commits
Author SHA1 Message Date
Vanilagy 9180af799d Oops 2026-07-07 21:27:05 +02:00
Vanilagy b8d5cc5b36 Bump patch, remove Ponder :( 2026-07-07 21:17:06 +02:00
Ali TlekbaiandGitHub d5b07373b3 Fix AudioResampler crackle at exact rate ratios: 32 kHz -> 48 kHz (#432) 2026-07-07 21:13:35 +02:00
Vanilagy b99b86db0b Bump patch 2026-07-03 16:44:39 +02:00
Vanilagy b0e5f655aa Make AUD-based NALU stripping less aggressive (closes #426) 2026-07-03 16:37:06 +02:00
Vanilagy 0c2853d0aa Merge branch 'main' of https://github.com/Vanilagy/metamuxer 2026-07-03 16:24:09 +02:00
Vanilagy cc970e1f53 Fix invalid check for no available read worker stalling reads, bump patch 2026-07-03 16:24:08 +02:00
Vanilagy 5e68d9655c Make ProRes tracks return true for hasOnlyKeyPackets() 2026-07-02 22:58:51 +02:00
Vanilagy 7171cf4338 Add sync workspace deps script 2026-07-02 20:42:46 +02:00
Vanilagy baff6a67a3 Omg 2026-07-02 20:31:13 +02:00
Vanilagy d0401b42a5 Bump TurboRes to fix bug 2026-07-02 20:30:36 +02:00
Vanilagy 72427b8b39 Bump TurboRes, use desiredSize, bump patch 2026-07-02 20:18:25 +02:00
Viktor SzépeandGitHub 6812cf1643 Fix typos in source, tests, and docs (#430)
* Fix typos in source, tests, and docs

* Fix introduction guide typos
2026-07-02 09:33:43 +02:00
Zi How PohandGitHub c9a828c272 Fix duplicated entries in generated API docs (#429) 2026-07-02 07:31:09 +00:00
Vanilagy e851120bfe Add PhotoCircle bronze sponsor 2026-07-01 18:11:01 +02:00
Vanilagy 1a4fec9f6e Bump TurboRes & patch 2026-07-01 17:18:21 +02:00
Vanilagy 8c8ce8a4a5 Merge main into release AFTER the npm publish completes 2026-07-01 16:26:58 +02:00
Vanilagy 7ea8f6d751 Up timeout on test 2026-07-01 16:25:53 +02:00
Vanilagy 8fb22633fe Add frame container atom header stripping & readding for ProRes in Matroska, bump patch 2026-07-01 16:22:38 +02:00
Vanilagy 0480ecbd3a Bump TurboRes version, mention response headers in README, bump patch 2026-07-01 15:27:36 +02:00
Vanilagy 9547d36353 Add workaround for https://issues.chromium.org/issues/529852980 2026-07-01 13:50:48 +02:00
Vanilagy d8d22aa4eb Make release workflow build the docs 2026-07-01 11:40:03 +02:00
Vanilagy 50b281a9f8 Merge branch 'main' into release 2026-07-01 11:37:12 +02:00
Vanilagy 409961c02b Rename 2026-07-01 11:35:22 +02:00
Vanilagy d8a6e09a85 Up timeout 2026-07-01 11:32:45 +02:00
Vanilagy 2c7e8d85ca Create separate Build check CI 2026-07-01 11:28:52 +02:00
Vanilagy a9441aa9a9 Fix API docs generation issue 2026-07-01 11:25:24 +02:00
github-actions[bot] 403093c514 Merge main into release for tag v1.50.0 2026-06-30 20:08:10 +00:00
github-actions[bot] 54e74bfa4b Merge main into release for tag v1.49.0 2026-06-18 17:48:19 +00:00
github-actions[bot] df76bae503 Merge main into release for tag v1.48.1 2026-06-17 18:27:21 +00:00
github-actions[bot] b617c5f720 Merge main into release for tag v1.48.0 2026-06-16 18:41:51 +00:00
github-actions[bot] a5df6c7b89 Merge main into release for tag v1.47.0 2026-06-16 07:58:35 +00:00
github-actions[bot] 210d382113 Merge main into release for tag v1.46.0 2026-06-02 17:07:56 +00:00
github-actions[bot] a294d943e6 Merge main into release for tag v1.45.5 2026-06-02 11:36:44 +00:00
github-actions[bot] 9a8ccf6f6c Merge main into release for tag v1.45.4 2026-05-24 14:53:43 +00:00
github-actions[bot] f34e4cd369 Merge main into release for tag v1.45.3 2026-05-19 20:36:20 +00:00
github-actions[bot] 93e6de980d Merge main into release for tag v1.45.2 2026-05-14 12:40:17 +00:00
github-actions[bot] e6fe540cbc Merge main into release for tag v1.45.1 2026-05-14 09:47:51 +00:00
github-actions[bot] 23e6901046 Merge main into release for tag v1.45.0 2026-05-13 18:21:58 +00:00
Vanilagy 143ecea6c0 Merge branch 'main' into release 2026-05-10 17:51:36 +02:00
github-actions[bot] 067081d5eb Merge main into release for tag v1.44.2 2026-05-08 22:44:02 +00:00
github-actions[bot] f55a2b4764 Merge main into release for tag v1.44.1 2026-05-07 09:13:03 +00:00
Vanilagy 1b1c9a0299 Merge branch 'main' into release 2026-05-06 20:12:25 +02:00
github-actions[bot] 2b2e80dc24 Merge main into release for tag v1.44.0 2026-05-05 07:22:59 +00:00
Vanilagy 13d1e280e8 Merge branch 'main' into release 2026-05-04 10:53:26 +02:00
Vanilagy 62287156d6 Merge branch 'main' into release 2026-05-03 19:47:55 +02:00
github-actions[bot] 100852f266 Merge main into release for tag v1.43.1 2026-05-02 12:29:10 +00:00
github-actions[bot] f8f08a48e1 Merge main into release for tag v1.43.0 2026-04-30 08:39:05 +00:00
Vanilagy 71dd34fc3c Merge branch 'main' into release 2026-04-28 17:57:55 +02:00
github-actions[bot] 487ae3c69a Merge main into release for tag v1.42.0 2026-04-28 15:29:11 +00:00
github-actions[bot] 0d5bf18864 Merge main into release for tag v1.41.0 2026-04-24 17:46:40 +00:00
github-actions[bot] fc67e436e3 Merge main into release for tag v1.41.0 2026-04-24 17:44:40 +00:00
Vanilagy 735a6622a2 Merge branch 'main' into release 2026-04-01 17:20:33 +02:00
github-actions[bot] d51903edfe Merge main into release for tag v1.40.1 2026-03-24 10:17:07 +00:00
github-actions[bot] 6cdaa76fbb Merge main into release for tag v1.40.0 2026-03-19 21:42:33 +00:00
github-actions[bot] 8efe7e75db Merge main into release for tag v1.39.2 2026-03-13 10:05:14 +00:00
github-actions[bot] a892676453 Merge main into release for tag v1.39.1 2026-03-11 15:30:35 +00:00
github-actions[bot] 00f020921b Merge main into release for tag v1.39.0 2026-03-09 19:06:09 +00:00
github-actions[bot] 8071933f15 Merge main into release for tag v1.38.1 2026-03-07 08:40:38 +00:00
github-actions[bot] d97611453b Merge main into release for tag v1.38.0 2026-03-05 19:01:49 +00:00
github-actions[bot] 5e59ae4a1c Merge main into release for tag v1.37.0 2026-03-04 20:24:37 +00:00
github-actions[bot] 3b148f961a Merge main into release for tag v1.36.0 2026-03-04 16:13:56 +00:00
github-actions[bot] 95d60805ed Merge main into release for tag v1.36.0 2026-03-04 16:10:38 +00:00
github-actions[bot] 857956fb07 Merge main into release for tag v1.35.1 2026-02-26 20:49:45 +00:00
github-actions[bot] e9fc97e070 Merge main into release for tag v1.35.0 2026-02-25 16:24:03 +00:00
github-actions[bot] 15db8a27d4 Merge main into release for tag v1.34.5 2026-02-24 12:15:08 +00:00
Vanilagy 637d77b2cf Merge branch 'main' into release 2026-02-24 11:07:25 +01:00
github-actions[bot] b6bb382783 Merge main into release for tag v1.34.4 2026-02-17 11:37:33 +00:00
github-actions[bot] 7bb9961156 Merge main into release for tag v1.34.3 2026-02-16 20:06:09 +00:00
github-actions[bot] 776ae43f50 Merge main into release for tag v1.34.2 2026-02-12 17:55:25 +00:00
github-actions[bot] e533bab18a Merge main into release for tag v1.34.1 2026-02-12 17:50:59 +00:00
github-actions[bot] 26f3a576f3 Merge main into release for tag v1.34.0 2026-02-12 17:41:38 +00:00
github-actions[bot] 1f6457a50c Merge main into release for tag v1.34.0 2026-02-12 17:34:18 +00:00
github-actions[bot] f6aee7b414 Merge main into release for tag v1.33.0 2026-02-12 11:23:54 +00:00
github-actions[bot] 0cb4665d8e Merge main into release for tag v1.33.0 2026-02-12 11:13:21 +00:00
Vanilagy 525a03d220 Merge branch 'main' into release 2026-02-11 08:39:01 +01:00
Vanilagy 808422f4f5 Merge branch 'main' into release 2026-02-10 17:08:08 +01:00
github-actions[bot] e6110a5b9b Merge main into release for tag v1.32.2 2026-02-07 09:36:13 +00:00
github-actions[bot] cd2ded8767 Merge main into release for tag v1.32.1 2026-02-07 09:29:00 +00:00
github-actions[bot] db91d07780 Merge main into release for tag v1.32.0 2026-02-04 17:29:52 +00:00
github-actions[bot] af57c431d3 Merge main into release for tag v1.31.1 2026-02-03 14:54:52 +00:00
github-actions[bot] 049f24f2cb Merge main into release for tag v1.31.0 2026-01-27 13:39:24 +00:00
github-actions[bot] aa509cd62e Merge main into release for tag v1.30.1 2026-01-23 17:07:16 +00:00
github-actions[bot] 60789841a8 Merge main into release for tag v1.30.0 2026-01-23 12:53:16 +00:00
github-actions[bot] b6d3e3269d Merge main into release for tag v1.29.1 2026-01-20 19:13:22 +00:00
github-actions[bot] cea3a40902 Merge main into release for tag v1.29.0 2026-01-19 14:33:47 +00:00
github-actions[bot] 201a1b4066 Merge main into release for tag v1.28.0 2026-01-14 08:24:32 +00:00
github-actions[bot] 882051daf2 Merge main into release for tag v1.27.6 2026-01-12 15:48:35 +00:00
github-actions[bot] 91f96458fa Merge main into release for tag v1.27.5 2026-01-12 15:19:46 +00:00
Vanilagy 9d87a3e580 Merge branch 'main' into release 2026-01-12 10:32:50 +01:00
github-actions[bot] 040b0747be Merge main into release for tag v1.27.4 2026-01-06 12:38:15 +00:00
Vanilagy 53923e72dc Merge branch 'main' into release 2026-01-02 12:33:45 +01:00
github-actions[bot] 46b3ce39fb Merge main into release for tag v1.27.3 2025-12-30 15:22:33 +00:00
Vanilagy 14ae14b565 Merge branch 'main' into release 2025-12-30 14:11:52 +01:00
Vanilagy 70e3e98b53 Merge branch 'main' into release 2025-12-29 20:28:31 +01:00
github-actions[bot] 6b0527b691 Merge main into release for tag v1.27.2 2025-12-24 13:18:40 +00:00
Vanilagy 3e333636ec Merge branch 'main' into release 2025-12-23 20:16:55 +01:00
github-actions[bot] a583b9d168 Merge main into release for tag v1.27.1 2025-12-19 15:07:29 +00:00
github-actions[bot] 29aaaf64bc Merge main into release for tag v1.27.0 2025-12-17 14:38:30 +00:00
github-actions[bot] 5c45fd1f63 Merge main into release for tag v1.26.0 2025-12-11 19:45:19 +00:00
github-actions[bot] 7232296217 Merge main into release for tag v1.25.8 2025-12-09 13:26:37 +00:00
github-actions[bot] 30fc407c4c Merge main into release for tag v1.25.7 2025-12-05 22:03:54 +00:00
github-actions[bot] 52fd8d0e19 Merge main into release for tag v1.25.6 2025-12-04 15:58:34 +00:00
github-actions[bot] a3e43987ed Merge main into release for tag v1.25.5 2025-12-04 15:54:56 +00:00
github-actions[bot] 823ede1668 Merge main into release for tag v1.25.5 2025-12-04 15:49:12 +00:00
github-actions[bot] 1703e70a16 Merge main into release for tag v1.25.5 2025-12-04 15:45:20 +00:00
github-actions[bot] df0932d49e Merge main into release for tag v1.25.5 2025-12-04 15:41:44 +00:00
github-actions[bot] 64468b0e5b Merge main into release for tag v1.25.4 2025-12-04 09:05:03 +00:00
github-actions[bot] 903e36430f Merge main into release for tag v1.25.4 2025-12-04 09:00:14 +00:00
github-actions[bot] dbb4150718 Merge main into release for tag v1.25.3 2025-11-26 22:33:14 +00:00
github-actions[bot] 9d1f4a5f80 Merge main into release for tag v1.25.2 2025-11-25 16:28:37 +00:00
github-actions[bot] 76ac4e7aba Merge main into release for tag v1.25.1 2025-11-20 08:23:34 +00:00
github-actions[bot] f0be3ef134 Merge main into release for tag v1.25.0 2025-11-13 19:21:46 +00:00
github-actions[bot] 77fa426a9a Merge main into release for tag v1.24.7 2025-11-13 08:01:02 +00:00
github-actions[bot] b4cbf4c502 Merge main into release for tag v1.24.6 2025-11-11 21:30:53 +00:00
github-actions[bot] 27bdd33d42 Merge main into release for tag v1.24.5 2025-11-10 20:20:43 +00:00
github-actions[bot] 5972a5c906 Merge main into release for tag v1.24.4 2025-11-06 09:03:56 +00:00
Vanilagy da539cf495 Merge branch 'main' into release 2025-11-03 15:24:25 +01:00
Vanilagy 987ae467ce Merge branch 'main' into release 2025-11-03 12:12:38 +01:00
Vanilagy 2b5971d1ee Merge branch 'main' into release 2025-11-02 22:04:35 +01:00
github-actions[bot] d735f16ca5 Merge main into release for tag v1.24.3 2025-11-02 21:00:48 +00:00
github-actions[bot] 9926004401 Merge main into release for tag v1.24.2 2025-10-21 14:30:15 +00:00
github-actions[bot] 34628390b7 Merge main into release for tag v1.24.1 2025-10-20 14:14:12 +00:00
github-actions[bot] 1e46df030f Merge main into release for tag v1.24.0 2025-10-17 13:34:51 +00:00
github-actions[bot] a6fd297573 Merge main into release for tag v1.23.0 2025-10-02 14:03:04 +00:00
github-actions[bot] 447dd51e7b Merge main into release for tag v1.22.0 2025-10-02 09:34:06 +00:00
Vanilagy aebfede89a Merge branch 'main' into release 2025-10-01 17:32:31 +02:00
Vanilagy e0a1211140 Merge branch 'main' into release 2025-09-30 13:37:12 +02:00
github-actions[bot] aa8003e679 Merge main into release for tag v1.21.1 2025-09-29 19:03:27 +00:00
Vanilagy c74b6a44fb Merge branch 'main' into release 2025-09-29 09:58:12 +02:00
github-actions[bot] f21182a7b9 Merge main into release for tag v1.21.0 2025-09-26 14:39:15 +00:00
github-actions[bot] fb2dcba707 Merge main into release for tag v1.20.1 2025-09-26 08:07:45 +00:00
github-actions[bot] e25525a564 Merge main into release for tag v1.20.1 2025-09-26 08:02:50 +00:00
Vanilagy f356c05a9c Merge branch 'main' into release 2025-09-26 09:38:29 +02:00
github-actions[bot] 4e1428fcef Merge main into release for tag v1.20.0 2025-09-25 13:21:53 +00:00
github-actions[bot] ca48f31032 Merge main into release for tag v1.20.0 2025-09-25 13:18:15 +00:00
github-actions[bot] b958dd84cc Merge main into release for tag v1.19.1 2025-09-24 09:42:26 +00:00
Vanilagy f72db4dfa2 Merge branch 'main' into release 2025-09-22 22:06:18 +02:00
github-actions[bot] e9534f57c6 Merge main into release for tag v1.19.0 2025-09-22 20:03:41 +00:00
github-actions[bot] 31fdd363e3 Merge main into release for tag v1.18.1 2025-09-21 16:52:17 +00:00
github-actions[bot] 14a55fc476 Merge main into release for tag v1.18.0 2025-09-21 15:48:08 +00:00
Vanilagy 0fc3bb2b1d Merge branch 'main' into release 2025-09-19 14:32:44 +02:00
github-actions[bot] 4f670bbe06 Merge main into release for tag v1.17.3 2025-09-19 12:30:13 +00:00
github-actions[bot] 02f379d9c1 Merge main into release for tag v1.17.2 2025-09-19 12:04:29 +00:00
github-actions[bot] 588495268c Merge main into release for tag v1.17.1 2025-09-19 11:21:20 +00:00
github-actions[bot] 230778d887 Merge main into release for tag v1.17.0 2025-09-18 21:47:07 +00:00
github-actions[bot] 32267f348a Merge main into release for tag v1.16.1 2025-09-18 08:01:03 +00:00
github-actions[bot] 2aa0fd1433 Merge main into release for tag v1.16.0 2025-09-17 21:13:22 +00:00
Vanilagy bdc59306ed Merge branch 'main' into release 2025-09-17 09:12:11 +02:00
github-actions[bot] 03c1c46f9a Merge main into release for tag v1.15.2 2025-09-16 21:15:44 +00:00
github-actions[bot] b4f76fc082 Merge main into release for tag v1.15.1 2025-09-12 15:16:52 +00:00
github-actions[bot] d55431dda5 Merge main into release for tag v1.15.0 2025-09-12 15:01:03 +00:00
github-actions[bot] 6679b6b15d Merge main into release for tag v1.14.4 2025-09-10 21:04:34 +00:00
Vanilagy ad4dc659a5 Merge branch 'main' into release 2025-09-10 16:52:57 +02:00
github-actions[bot] 95fbec86e8 Merge main into release for tag v1.14.3 2025-09-09 11:11:50 +00:00
38 changed files with 294 additions and 119 deletions
+28
View File
@@ -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
+1 -4
View File
@@ -27,7 +27,4 @@ jobs:
run: npm run check run: npm run check
- name: Run ESLint - name: Run ESLint
run: npm run lint run: npm run lint
- name: Run build
run: npm run build
+12 -11
View File
@@ -25,15 +25,6 @@ jobs:
with: with:
script: "core.setFailed('Non-prerelease releases must target the main branch (got: ${{ github.event.release.target_commitish }}).')" 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 - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -60,8 +51,9 @@ jobs:
- name: Run tests - name: Run tests
run: xvfb-run npm test run: xvfb-run npm test
- name: Run build # Building the docs also builds the core library
run: npm run build - name: Build everything
run: npm run docs:build
- name: Upload build artifacts - name: Upload build artifacts
env: env:
@@ -106,3 +98,12 @@ jobs:
- name: Publish workspace packages to npm - name: Publish workspace packages to npm
run: npm publish --access public --workspaces ${{ github.event.release.prerelease && '--tag beta' || '' }} 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
+2 -2
View File
@@ -1,6 +1,6 @@
- Prefer functions declared using const, not using the function keyword - Prefer functions declared using const, not using the function keyword
- Code style is tab indent with semicolons - Code style is tab indent with semicolons
- Mediabunny core code is contained in src/, extensions are in packages/*/, website is in docs/ - 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; } - 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
+4 -8
View File
@@ -44,14 +44,6 @@ Mediabunny is a JavaScript library for reading, writing, and converting media (l
</a> </a>
</div> </div>
### Silver sponsors
<div align="center">
<a href="https://ponder.ai/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/ponder.png" width="50" height="50" alt="Ponder">
</a>
</div>
### Bronze sponsors ### Bronze sponsors
<div align="center"> <div align="center">
@@ -70,6 +62,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media (l
<a href="https://jellypod.ai/" target="_blank" rel="sponsored"> <a href="https://jellypod.ai/" target="_blank" rel="sponsored">
<img src="./docs/public/sponsors/jellypod.png" width="40" height="40" alt="Jellypod"> <img src="./docs/public/sponsors/jellypod.png" width="40" height="40" alt="Jellypod">
</a> </a>
&nbsp;&nbsp;&nbsp;&nbsp;
<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> </div>
[Sponsor Mediabunny's development](https://github.com/sponsors/Vanilagy) [Sponsor Mediabunny's development](https://github.com/sponsors/Vanilagy)
+19
View File
@@ -26,6 +26,25 @@ Alternatively, directly include them using a script tag:
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). 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 ## Usage
```ts ```ts
+3 -3
View File
@@ -60,7 +60,7 @@ To get going with Mediabunny, here are some starting points:
## Server-side usage ## 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 primary 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. 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: 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. - 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.
@@ -74,7 +74,7 @@ For more, see [the corresponding guide](./extensions/server).
## Motivation ## 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. - 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. - No help with the difficulties of navigating the WebCodecs API & related browser APIs.
- "mp4-demuxer when??" - "mp4-demuxer when??"
@@ -99,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. 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.
+2 -2
View File
@@ -342,7 +342,7 @@ Each chunk written to the `WritableStream` represents a contiguous chunk of byte
::: warning ::: 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. 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 #### Chunked mode
@@ -545,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: 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 ```ts
output.format.mimeType; // => string output.format.mimeType; // => string
``` ```
+2 -3
View File
@@ -102,14 +102,13 @@ const sponsors = {
{ image: '/sponsors/screen-studio.webp', name: 'Screen Studio', url: 'https://screen.studio/' }, { image: '/sponsors/screen-studio.webp', name: 'Screen Studio', url: 'https://screen.studio/' },
{ image: '/sponsors/tella.svg', name: 'Tella', url: 'https://www.tella.com/' }, { image: '/sponsors/tella.svg', name: 'Tella', url: 'https://www.tella.com/' },
], ],
silver: [ silver: [],
{ image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' },
],
bronze: [ bronze: [
{ image: '/sponsors/elevenlabs.png', name: 'ElevenLabs', url: 'https://elevenlabs.io/' }, { image: '/sponsors/elevenlabs.png', name: 'ElevenLabs', url: 'https://elevenlabs.io/' },
{ image: '/sponsors/rve.png', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' }, { image: '/sponsors/rve.png', name: 'React Video Editor', url: 'https://www.reactvideoeditor.com/' },
{ image: '/sponsors/mux.jpg', name: 'Mux', url: 'https://www.mux.com/' }, { image: '/sponsors/mux.jpg', name: 'Mux', url: 'https://www.mux.com/' },
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' }, { image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
{ image: '/sponsors/photocircle.png', name: 'PhotoCircle', url: 'https://www.photocircleapp.com/' },
], ],
individual: [ individual: [
{ image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' }, { image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' },
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+13 -13
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.50.0", "version": "1.50.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.50.0", "version": "1.50.7",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
".", ".",
@@ -10963,9 +10963,9 @@
"optional": true "optional": true
}, },
"node_modules/turbores": { "node_modules/turbores": {
"version": "1.1.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/turbores/-/turbores-1.1.1.tgz", "resolved": "https://registry.npmjs.org/turbores/-/turbores-1.2.1.tgz",
"integrity": "sha512-uC2bBqMgMzeE/K4/zeUJd3IwvDm9+0JtNXkiBuc7Sg1L8OaeAhk+VCgoiqDgDdwVaqF3/lztEjrSgt8k15XyYw==", "integrity": "sha512-HcnLHiUggT2YXmnTpUdT3JHnkGWvdJlW9XBPLKZBBB3X5oOEuBaHF99cW+H+CSxnhM+rth2qDTCAteIKhKwiGQ==",
"license": "MPL-2.0", "license": "MPL-2.0",
"funding": { "funding": {
"type": "individual", "type": "individual",
@@ -12894,7 +12894,7 @@
}, },
"packages/aac-encoder": { "packages/aac-encoder": {
"name": "@mediabunny/aac-encoder", "name": "@mediabunny/aac-encoder",
"version": "1.50.0", "version": "1.50.7",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12909,7 +12909,7 @@
}, },
"packages/ac3": { "packages/ac3": {
"name": "@mediabunny/ac3", "name": "@mediabunny/ac3",
"version": "1.50.0", "version": "1.50.7",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12924,7 +12924,7 @@
}, },
"packages/flac-encoder": { "packages/flac-encoder": {
"name": "@mediabunny/flac-encoder", "name": "@mediabunny/flac-encoder",
"version": "1.50.0", "version": "1.50.7",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12939,7 +12939,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.50.0", "version": "1.50.7",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
@@ -12954,10 +12954,10 @@
}, },
"packages/prores": { "packages/prores": {
"name": "@mediabunny/prores", "name": "@mediabunny/prores",
"version": "1.50.0", "version": "1.50.7",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"turbores": "^1.1.1" "turbores": "^1.2.1"
}, },
"funding": { "funding": {
"type": "individual", "type": "individual",
@@ -12969,10 +12969,10 @@
}, },
"packages/server": { "packages/server": {
"name": "@mediabunny/server", "name": "@mediabunny/server",
"version": "1.50.0", "version": "1.50.7",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@mediabunny/prores": "^1.50.0", "@mediabunny/prores": "^1.50.6",
"node-av": "^6.0.0" "node-av": "^6.0.0"
}, },
"funding": { "funding": {
+5 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.0", "version": "1.50.7",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
@@ -56,10 +56,10 @@
"examples:build": "vite build", "examples:build": "vite build",
"fix-build-import-paths": "tsx scripts/add-import-extensions.ts", "fix-build-import-paths": "tsx scripts/add-import-extensions.ts",
"append-namespace": "echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts", "append-namespace": "echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts",
"bump-patch": "npm version patch --no-git-tag-version --workspaces", "bump-patch": "npm version patch --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts",
"bump-minor": "npm version minor --no-git-tag-version --workspaces", "bump-minor": "npm version minor --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts",
"bump-major": "npm version major --no-git-tag-version --workspaces", "bump-major": "npm version major --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts",
"set-version": "npm version --no-git-tag-version --workspaces" "set-version": "npm version --no-git-tag-version --workspaces && tsx scripts/sync-workspace-deps.ts"
}, },
"license": "MPL-2.0", "license": "MPL-2.0",
"repository": { "repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/aac-encoder", "name": "@mediabunny/aac-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.0", "version": "1.50.7",
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.", "description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-aac-encoder.mjs", "main": "./dist/bundles/mediabunny-aac-encoder.mjs",
"module": "./dist/bundles/mediabunny-aac-encoder.mjs", "module": "./dist/bundles/mediabunny-aac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/ac3", "name": "@mediabunny/ac3",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.0", "version": "1.50.7",
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
"main": "./dist/bundles/mediabunny-ac3.mjs", "main": "./dist/bundles/mediabunny-ac3.mjs",
"module": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/flac-encoder", "name": "@mediabunny/flac-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.0", "version": "1.50.7",
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.", "description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
"main": "./dist/bundles/mediabunny-flac-encoder.mjs", "main": "./dist/bundles/mediabunny-flac-encoder.mjs",
"module": "./dist/bundles/mediabunny-flac-encoder.mjs", "module": "./dist/bundles/mediabunny-flac-encoder.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.0", "version": "1.50.7",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+19
View File
@@ -28,6 +28,25 @@ Alternatively, directly include them using a script tag:
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). 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 ## Usage
```ts ```ts
+2 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/prores", "name": "@mediabunny/prores",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.0", "version": "1.50.7",
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.", "description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
"main": "./dist/bundles/mediabunny-prores.mjs", "main": "./dist/bundles/mediabunny-prores.mjs",
"module": "./dist/bundles/mediabunny-prores.mjs", "module": "./dist/bundles/mediabunny-prores.mjs",
@@ -19,10 +19,6 @@
"src" "src"
], ],
"sideEffects": false, "sideEffects": false,
"browser": {
"node:worker_threads": false,
"node:os": false
},
"license": "MPL-2.0", "license": "MPL-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -38,7 +34,7 @@
"url": "https://github.com/sponsors/Vanilagy" "url": "https://github.com/sponsors/Vanilagy"
}, },
"dependencies": { "dependencies": {
"turbores": "^1.1.1" "turbores": "^1.2.1"
}, },
"peerDependencies": { "peerDependencies": {
"mediabunny": "^1.49.0" "mediabunny": "^1.49.0"
+5 -9
View File
@@ -83,16 +83,12 @@ class ProresDecoder extends CustomVideoDecoder {
async decode(packet: EncodedPacket) { async decode(packet: EncodedPacket) {
assert(this.decoder); assert(this.decoder);
if (this.decoder.useSharedMemory) { while (this.decoder.desiredSize <= 0) {
await this.runDecode(packet); await this.decoder.dequeued;
} else {
while (this.decoder.decodeQueueSize >= this.decoder.concurrency) {
await this.decoder.dequeued;
}
void this.runDecode(packet)
.catch(error => this.onError(error));
} }
void this.runDecode(packet)
.catch(error => this.onError(error));
} }
private async runDecode(packet: EncodedPacket) { private async runDecode(packet: EncodedPacket) {
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/server", "name": "@mediabunny/server",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.50.0", "version": "1.50.7",
"description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.", "description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.",
"main": "./dist/bundles/mediabunny-server.cjs", "main": "./dist/bundles/mediabunny-server.cjs",
"module": "./dist/bundles/mediabunny-server.mjs", "module": "./dist/bundles/mediabunny-server.mjs",
@@ -35,7 +35,7 @@
}, },
"dependencies": { "dependencies": {
"node-av": "^6.0.0", "node-av": "^6.0.0",
"@mediabunny/prores": "^1.50.0" "@mediabunny/prores": "^1.50.7"
}, },
"peerDependencies": { "peerDependencies": {
"mediabunny": "^1.45.0" "mediabunny": "^1.45.0"
+5 -5
View File
@@ -103,7 +103,7 @@ const mp3EncoderVariants = await createVariants(
'packages/mp3-encoder/src/index.ts', 'packages/mp3-encoder/src/index.ts',
'MediabunnyMp3Encoder', 'MediabunnyMp3Encoder',
'packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder', 'packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder',
'js', // The bundles are purely for the browser, not for Node (due to the peer dependecy) 'js', // The bundles are purely for the browser, not for Node (due to the peer dependency)
{ {
plugins: [ plugins: [
PluginExternalGlobal.externalGlobalPlugin({ PluginExternalGlobal.externalGlobalPlugin({
@@ -134,7 +134,7 @@ const ac3Variants = await createVariants(
'packages/ac3/src/index.ts', 'packages/ac3/src/index.ts',
'MediabunnyAc3', 'MediabunnyAc3',
'packages/ac3/dist/bundles/mediabunny-ac3', 'packages/ac3/dist/bundles/mediabunny-ac3',
'js', // The bundles are purely for the browser, not for Node (due to the peer dependecy) 'js', // The bundles are purely for the browser, not for Node (due to the peer dependency)
{ {
plugins: [ plugins: [
PluginExternalGlobal.externalGlobalPlugin({ PluginExternalGlobal.externalGlobalPlugin({
@@ -165,7 +165,7 @@ const aacEncoderVariants = await createVariants(
'packages/aac-encoder/src/index.ts', 'packages/aac-encoder/src/index.ts',
'MediabunnyAacEncoder', 'MediabunnyAacEncoder',
'packages/aac-encoder/dist/bundles/mediabunny-aac-encoder', 'packages/aac-encoder/dist/bundles/mediabunny-aac-encoder',
'js', // The bundles are purely for the browser, not for Node (due to the peer dependecy) 'js', // The bundles are purely for the browser, not for Node (due to the peer dependency)
{ {
plugins: [ plugins: [
PluginExternalGlobal.externalGlobalPlugin({ PluginExternalGlobal.externalGlobalPlugin({
@@ -196,7 +196,7 @@ const flacEncoderVariants = await createVariants(
'packages/flac-encoder/src/index.ts', 'packages/flac-encoder/src/index.ts',
'MediabunnyFlacEncoder', 'MediabunnyFlacEncoder',
'packages/flac-encoder/dist/bundles/mediabunny-flac-encoder', 'packages/flac-encoder/dist/bundles/mediabunny-flac-encoder',
'js', // The bundles are purely for the browser, not for Node (due to the peer dependecy) 'js', // The bundles are purely for the browser, not for Node (due to the peer dependency)
{ {
plugins: [ plugins: [
PluginExternalGlobal.externalGlobalPlugin({ PluginExternalGlobal.externalGlobalPlugin({
@@ -227,7 +227,7 @@ const proresVariants = await createVariants(
'packages/prores/src/index.ts', 'packages/prores/src/index.ts',
'MediabunnyProres', 'MediabunnyProres',
'packages/prores/dist/bundles/mediabunny-prores', 'packages/prores/dist/bundles/mediabunny-prores',
'js', // The bundles are purely for the browser, not for Node (due to the peer dependecy) 'js', // The bundles are purely for the browser, not for Node (due to the peer dependency)
{ {
plugins: [ plugins: [
PluginExternalGlobal.externalGlobalPlugin({ PluginExternalGlobal.externalGlobalPlugin({
+13 -6
View File
@@ -127,15 +127,22 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0]; const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0];
if (!declaration) return; if (!declaration) return;
// If it's a reexport, follow it recursively // If it's a reexport, resolve it to the underlying symbol (following chains of aliases,
// e.g. a re-export of a re-export) rather than recursing into the whole module it lives in
// -- `getExportsOfModule` above already gives us one entry per exported symbol, so a module
// recursion here would revisit (and duplicate) every other export of that module too.
if (exportSymbol.flags & ts.SymbolFlags.Alias) { if (exportSymbol.flags & ts.SymbolFlags.Alias) {
const aliasedSymbol = typeChecker.getAliasedSymbol(exportSymbol); let aliasedSymbol = typeChecker.getAliasedSymbol(exportSymbol);
while (aliasedSymbol.flags & ts.SymbolFlags.Alias) {
aliasedSymbol = typeChecker.getAliasedSymbol(aliasedSymbol);
}
const aliasedDeclaration = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]; const aliasedDeclaration = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0];
if (aliasedDeclaration) { if (aliasedDeclaration) {
const sourceFile = aliasedDeclaration.getSourceFile(); // Push the aliased symbol (not the alias) so downstream sees the real
const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile); // declaration and its JSDoc rather than the empty ExportSpecifier.
if (moduleSymbol) { const hasPublicTag = ts.getJSDocTags(aliasedDeclaration).some(tag => tag.tagName.text === 'public');
symbols.push(...getAllExportedSymbols(moduleSymbol, visited)); if (hasPublicTag) {
symbols.push(aliasedSymbol);
} }
} }
} }
+40
View File
@@ -0,0 +1,40 @@
import fs from 'node:fs';
import path from 'node:path';
// After `npm version --workspaces` bumps each package's version, this rewrites dependency ranges that point at sibling
// workspaces so they follow along.
const root = path.join(import.meta.dirname, '..');
const workspaceDirs = ['.', ...fs.readdirSync(path.join(root, 'packages')).map(x => `packages/${x}`)]
.filter(dir => fs.existsSync(path.join(root, dir, 'package.json')));
type Manifest = {
name: string;
version: string;
dependencies?: Record<string, string>;
};
const manifests = workspaceDirs.map((dir) => {
const filePath = path.join(root, dir, 'package.json');
return { filePath, json: JSON.parse(fs.readFileSync(filePath, 'utf8')) as Manifest };
});
const versions = new Map(manifests.map(({ json }) => [json.name, json.version]));
for (const { filePath, json } of manifests) {
let changed = false;
for (const name of Object.keys(json.dependencies ?? {})) {
const version = versions.get(name);
if (version && json.dependencies![name] !== `^${version}`) {
json.dependencies![name] = `^${version}`;
changed = true;
}
}
if (changed) {
fs.writeFileSync(filePath, JSON.stringify(json, null, 2) + '\n');
console.log(`Synced workspace dependency ranges in ${path.relative(root, filePath)}`);
}
}
+1 -1
View File
@@ -1878,7 +1878,7 @@ export type Av1CodecInfo = {
chromaSamplePosition: number; chromaSamplePosition: number;
}; };
/** Iterates over all OBUs in an AV1 packet bistream. */ /** Iterates over all OBUs in an AV1 packet bitstream. */
export const iterateAv1PacketObus = function* (packet: Uint8Array) { export const iterateAv1PacketObus = function* (packet: Uint8Array) {
// https://aomediacodec.github.io/av1-spec/av1-spec.pdf // https://aomediacodec.github.io/av1-spec/av1-spec.pdf
+7 -3
View File
@@ -838,7 +838,7 @@ export class Conversion {
if (this._options.trim?.start !== undefined) { if (this._options.trim?.start !== undefined) {
this._startTimestamp = this._options.trim.start; this._startTimestamp = this._options.trim.start;
} else { } else {
// Compute the start timestamp from the set of filtered tracks. Techncially these can still be narrowed // Compute the start timestamp from the set of filtered tracks. Technically these can still be narrowed
// down later due to discarded tracks, but we need to fix the start timestamp now due to track processing // down later due to discarded tracks, but we need to fix the start timestamp now due to track processing
// depending on it. // depending on it.
this._startTimestamp = Math.max( this._startTimestamp = Math.max(
@@ -1329,9 +1329,13 @@ export class Conversion {
firstSample.close(); firstSample.close();
await tempOutput.finalize(); await tempOutput.finalize();
} catch (error) { } catch (error) {
Logging._info('Error when probing encoder support. Falling back to rerender path.', error); Logging._warn(
needsRerender = true; 'An error occurred when probing encoder support. Falling back to rerender path.', error,
);
void tempOutput.cancel(); void tempOutput.cancel();
needsRerender = true;
encodingConfig.transform.force = true;
} }
} else { } else {
await tempOutput.cancel(); await tempOutput.cancel();
+2 -2
View File
@@ -186,7 +186,7 @@ export class FlacDemuxer extends Demuxer {
// description is required, and has to be the following: // description is required, and has to be the following:
// 1. The bytes 0x66 0x4C 0x61 0x43 ("fLaC" in ASCII) // 1. The bytes 0x66 0x4C 0x61 0x43 ("fLaC" in ASCII)
// 2. A metadata block (called the STREAMINFO block) as described in section 7 of [FLAC] // 2. A metadata block (called the STREAMINFO block) as described in section 7 of [FLAC]
// 3. Optionaly (sic) other metadata blocks, that are not used by the specification // 3. Optionally (sic) other metadata blocks, that are not used by the specification
bitstream.skipBits(16 * 8); // md5 hash bitstream.skipBits(16 * 8); // md5 hash
@@ -296,7 +296,7 @@ export class FlacDemuxer extends Demuxer {
// we expect that there are at least `minimumFrameSize` bytes left in the file // we expect that there are at least `minimumFrameSize` bytes left in the file
// Ideally we also want to validate the next header is valid // Ideally we also want to validate the next header is valid
// to throw out an accidential sync word // to throw out an accidental sync word
// The shortest valid FLAC header I can think of, based off the code // The shortest valid FLAC header I can think of, based off the code
// of readFlacFrameHeader: // of readFlacFrameHeader:
+2 -1
View File
@@ -544,7 +544,8 @@ export class InputVideoTrack extends InputTrack {
} }
async hasOnlyKeyPackets() { async hasOnlyKeyPackets() {
return (await this._backing.getHasOnlyKeyPackets?.()) ?? false; return (await this._backing.getHasOnlyKeyPackets?.())
?? (await this._backing.getCodec() === 'prores'); // Only ProRes is fully intra-frame
} }
/** Returns the width in pixels of the track's coded samples, before any transformations or rotations. */ /** Returns the width in pixels of the track's coded samples, before any transformations or rotations. */
+34
View File
@@ -46,6 +46,7 @@ import {
Rotation, Rotation,
roundIfAlmostInteger, roundIfAlmostInteger,
textDecoder, textDecoder,
toDataView,
TRANSFER_CHARACTERISTICS_MAP_INVERSE, TRANSFER_CHARACTERISTICS_MAP_INVERSE,
UNDETERMINED_LANGUAGE, UNDETERMINED_LANGUAGE,
} from '../misc'; } from '../misc';
@@ -141,6 +142,7 @@ type ClusterBlock = {
data: Uint8Array; data: Uint8Array;
lacing: BlockLacing; lacing: BlockLacing;
decoded: boolean; decoded: boolean;
postProcessed: boolean; // For codec-specific processing
mainAdditional: Uint8Array | null; mainAdditional: Uint8Array | null;
}; };
@@ -864,6 +866,7 @@ export class MatroskaDemuxer extends Demuxer {
data: frameData, data: frameData,
lacing: BlockLacing.None, lacing: BlockLacing.None,
decoded: true, decoded: true,
postProcessed: false,
mainAdditional: originalBlock.mainAdditional, mainAdditional: originalBlock.mainAdditional,
}); });
} }
@@ -1502,6 +1505,7 @@ export class MatroskaDemuxer extends Demuxer {
data: blockData, data: blockData,
lacing, lacing,
decoded: !hasDecodingInstructions, decoded: !hasDecodingInstructions,
postProcessed: false,
mainAdditional: null, mainAdditional: null,
}); });
}; break; }; break;
@@ -1538,6 +1542,7 @@ export class MatroskaDemuxer extends Demuxer {
data: blockData, data: blockData,
lacing, lacing,
decoded: !hasDecodingInstructions, decoded: !hasDecodingInstructions,
postProcessed: false,
mainAdditional: null, mainAdditional: null,
}; };
trackData.blocks.push(this.currentBlock); trackData.blocks.push(this.currentBlock);
@@ -2194,6 +2199,35 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
block.decoded = true; block.decoded = true;
} }
if (!block.postProcessed) {
if (this.internalTrack.info?.codec === 'prores') {
// For some reason, ProRes packets are stored in Matroska without the frame container atom. FFmpeg cites
// the "Matroska spec" but the actual spec says nothing about this.
const hasFrameContainer = block.data.length >= 8
&& block.data[4] === 105 // 'i'
&& block.data[5] === 99 // 'c'
&& block.data[6] === 112 // 'p'
&& block.data[7] === 102; // 'f'
if (!hasFrameContainer) {
// Wrap the frame in a frame container
const newData = new Uint8Array(block.data.length + 8);
const newDataView = toDataView(newData);
newDataView.setUint32(0, newData.length, false);
newData[4] = 105; // 'i'
newData[5] = 99; // 'c'
newData[6] = 112; // 'p'
newData[7] = 102; // 'f'
newData.set(block.data, 8);
block.data = newData;
}
}
block.postProcessed = true;
}
const data = options.metadataOnly ? PLACEHOLDER_DATA : block.data; const data = options.metadataOnly ? PLACEHOLDER_DATA : block.data;
const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor; const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor;
const duration = block.duration / this.internalTrack.segment.timestampFactor; const duration = block.duration / this.internalTrack.segment.timestampFactor;
+12 -1
View File
@@ -897,6 +897,17 @@ export class MatroskaMuxer extends Muxer {
try { try {
const trackData = this.getVideoTrackData(track, packet, meta); const trackData = this.getVideoTrackData(track, packet, meta);
let packetData = packet.data;
if (track.source._codec === 'prores') {
if (packetData.byteLength < 8) {
throw new Error('ProRes packet too small, expected at least 8 bytes.');
}
// Trim off the frame container atom header. FFmpeg does this too and cites the "Matroska spec" as the
// reason, despite the spec not saying anything about this.
packetData = packetData.subarray(8);
}
const isKeyFrame = packet.type === 'key'; const isKeyFrame = packet.type === 'key';
this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame); this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame);
@@ -913,7 +924,7 @@ export class MatroskaMuxer extends Muxer {
? packet.sideData.alpha ?? null ? packet.sideData.alpha ?? null
: null; : null;
const videoChunk = this.createInternalChunk(packet.data, timestamp, duration, packet.type, additions); const videoChunk = this.createInternalChunk(packetData, timestamp, duration, packet.type, additions);
if (track.source._codec === 'vp9') this.fixVP9ColorSpace(trackData, videoChunk); if (track.source._codec === 'vp9') this.fixVP9ColorSpace(trackData, videoChunk);
trackData.chunkQueue.push(videoChunk); trackData.chunkQueue.push(videoChunk);
+11 -4
View File
@@ -1011,15 +1011,22 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
if (this.codec === 'avc') { if (this.codec === 'avc') {
// Workaround for https://issues.chromium.org/issues/470109459 // Workaround for https://issues.chromium.org/issues/470109459
const filteredNalUnits: Uint8Array[] = []; const filteredNalUnits: Uint8Array[] = [];
let hasFrameData = false;
for (const loc of iterateAvcNalUnits(packet.data, this.decoderConfig)) { for (const loc of iterateAvcNalUnits(packet.data, this.decoderConfig)) {
const type = extractNalUnitTypeForAvc(packet.data[loc.offset]!); const type = extractNalUnitTypeForAvc(packet.data[loc.offset]!);
hasFrameData ||= type >= 1 && type <= 5;
if (type === AvcNalUnitType.AUD) { if (type === AvcNalUnitType.AUD) {
// If packets contain an AUD and have NALUs before it, this trips up Chromium's key frame if (hasFrameData) {
// detector. Clear the NALUs if an AUD is encountered. // Already has actual frame data, so treat an AUD as simply the end of the packet
// https://github.com/Vanilagy/mediabunny/issues/396 break;
filteredNalUnits.length = 0; } else {
// If packets contain an AUD and have NALUs before it, this trips up Chromium's key
// frame detector. Clear the NALUs if an AUD is encountered.
// https://github.com/Vanilagy/mediabunny/issues/396
filteredNalUnits.length = 0;
}
} }
// These trip up Chromium's key frame detection, so let's strip them // These trip up Chromium's key frame detection, so let's strip them
+4
View File
@@ -36,6 +36,7 @@ import {
setUint24, setUint24,
toUint8Array, toUint8Array,
UnthrottledTimerHandle, UnthrottledTimerHandle,
wait,
} from './misc'; } from './misc';
import { Muxer } from './muxer'; import { Muxer } from './muxer';
import { SubtitleParser } from './subtitles'; import { SubtitleParser } from './subtitles';
@@ -878,6 +879,9 @@ class VideoEncoderWrapper {
// These are wired in series, therefore they must also be flushed in series // These are wired in series, therefore they must also be flushed in series
await this.encoder.flush(); await this.encoder.flush();
await this.alphaEncoder?.flush(); await this.alphaEncoder?.flush();
// Workaround for https://issues.chromium.org/issues/529852980 to give it time for errors to surface
await wait(25);
} }
if (this.encoder.state !== 'closed') { if (this.encoder.state !== 'closed') {
+2 -2
View File
@@ -638,11 +638,11 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
const lowPages: Page[] = [lowPage]; const lowPages: Page[] = [lowPage];
// First, let's perform a binary serach (bisection search) on the file to find the approximate page where // First, let's perform a binary search (bisection search) on the file to find the approximate page where
// we'll find the packet. We want to find a page whose end packet position is less than or equal to the // we'll find the packet. We want to find a page whose end packet position is less than or equal to the
// packet position we're searching for. // packet position we're searching for.
// Outer loop: Does the binary serach // Outer loop: Does the binary search
outer: outer:
while (lowPage.headerStartPos + lowPage.totalSize < high) { while (lowPage.headerStartPos + lowPage.totalSize < high) {
const low = lowPage.headerStartPos; const low = lowPage.headerStartPos;
+2 -2
View File
@@ -172,7 +172,7 @@ export class AudioResampler {
async add(audioSample: AudioSample) { async add(audioSample: AudioSample) {
if (this.sourceSampleRate === null) { if (this.sourceSampleRate === null) {
// This is the first sample, so let's init the missing data. Initting the sample rate from the decoded // This is the first sample, so let's init the missing data. Initing the sample rate from the decoded
// sample is more reliable than using the file's metadata, because decoders are free to emit any sample rate // sample is more reliable than using the file's metadata, because decoders are free to emit any sample rate
// they see fit. // they see fit.
this.sourceSampleRate = audioSample.sampleRate; this.sourceSampleRate = audioSample.sampleRate;
@@ -199,7 +199,7 @@ export class AudioResampler {
const inputEndTime = inputStartTime + audioSample.duration; const inputEndTime = inputStartTime + audioSample.duration;
// Compute which output frames are affected by this sample // Compute which output frames are affected by this sample
const outputStartFrame = Math.floor(inputStartTime * this.targetSampleRate); const outputStartFrame = Math.floor((inputStartTime - 1 / this.sourceSampleRate) * this.targetSampleRate) + 1;
const outputEndFrame = Math.ceil(inputEndTime * this.targetSampleRate); const outputEndFrame = Math.ceil(inputEndTime * this.targetSampleRate);
for (let outputFrame = outputStartFrame; outputFrame < outputEndFrame; outputFrame++) { for (let outputFrame = outputStartFrame; outputFrame < outputEndFrame; outputFrame++) {
+7 -3
View File
@@ -2129,7 +2129,7 @@ class ReadOrchestrator {
} }
}) })
.finally(() => { .finally(() => {
if (worker.running || this.workers.length >= this.options.maxWorkerCount) { if (worker.running) {
// Rare, but can happen with multiple concurrent reads. In this case, don't do anything. // Rare, but can happen with multiple concurrent reads. In this case, don't do anything.
return; return;
} }
@@ -2144,15 +2144,19 @@ class ReadOrchestrator {
} }
const queuedRead = this.queuedReads[oldestIndex]!; const queuedRead = this.queuedReads[oldestIndex]!;
this.queuedReads.splice(oldestIndex, 1);
const newWorker = this.createWorker( const newWorker = this.createWorker(
queuedRead.hole.start, queuedRead.hole.start,
queuedRead.hole.end, queuedRead.hole.end,
queuedRead.strictTarget, queuedRead.strictTarget,
); );
assert(newWorker); // We just freed up a worker, so this should never fail if (!newWorker) {
// In high-contention cases, it could be that we've already reached max worker count, so in this
// case we don't do anything.
return;
}
this.queuedReads.splice(oldestIndex, 1);
newWorker.pendingSlices = queuedRead.pendingSlices; newWorker.pendingSlices = queuedRead.pendingSlices;
this.runWorker(newWorker); this.runWorker(newWorker);
} }
+13 -13
View File
@@ -177,8 +177,8 @@ test('HLS track assignability is kept #1', async () => {
await output.finalize(); await output.finalize();
const masterPlayist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('master.m3u8'))); const masterPlaylist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('master.m3u8')));
expect(masterPlayist.match(/\.m3u8/g)?.length).toBe(1); expect(masterPlaylist.match(/\.m3u8/g)?.length).toBe(1);
using input = new Input({ using input = new Input({
formats: ALL_FORMATS, formats: ALL_FORMATS,
@@ -208,8 +208,8 @@ test('HLS track assignability is kept #1', async () => {
const conversion = await Conversion.init({ input, output: newOutput }); const conversion = await Conversion.init({ input, output: newOutput });
await conversion.execute(); await conversion.execute();
const newMasterPlayist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('new/master.m3u8'))); const newMasterPlaylist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('new/master.m3u8')));
expect(newMasterPlayist).toBe(masterPlayist); expect(newMasterPlaylist).toBe(masterPlaylist);
}); });
test('HLS track assignability is kept #2', async () => { test('HLS track assignability is kept #2', async () => {
@@ -256,8 +256,8 @@ test('HLS track assignability is kept #2', async () => {
await output.finalize(); await output.finalize();
const masterPlayist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('master.m3u8'))); const masterPlaylist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('master.m3u8')));
expect(masterPlayist.match(/\.m3u8/g)?.length).toBe(2); expect(masterPlaylist.match(/\.m3u8/g)?.length).toBe(2);
using input = new Input({ using input = new Input({
formats: ALL_FORMATS, formats: ALL_FORMATS,
@@ -287,8 +287,8 @@ test('HLS track assignability is kept #2', async () => {
const conversion = await Conversion.init({ input, output: newOutput }); const conversion = await Conversion.init({ input, output: newOutput });
await conversion.execute(); await conversion.execute();
const newMasterPlayist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('new/master.m3u8'))); const newMasterPlaylist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('new/master.m3u8')));
expect(newMasterPlayist).toBe(masterPlayist); expect(newMasterPlaylist).toBe(masterPlaylist);
}); });
test('HLS track assignability can be overridden', async () => { test('HLS track assignability can be overridden', async () => {
@@ -335,8 +335,8 @@ test('HLS track assignability can be overridden', async () => {
await output.finalize(); await output.finalize();
const masterPlayist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('master.m3u8'))); const masterPlaylist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('master.m3u8')));
expect(masterPlayist.match(/\.m3u8/g)?.length).toBe(2); expect(masterPlaylist.match(/\.m3u8/g)?.length).toBe(2);
using input = new Input({ using input = new Input({
formats: ALL_FORMATS, formats: ALL_FORMATS,
@@ -371,9 +371,9 @@ test('HLS track assignability can be overridden', async () => {
}); });
await conversion.execute(); await conversion.execute();
const newMasterPlayist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('new/master.m3u8'))); const newMasterPlaylist = sanitizeMasterPlaylist(new TextDecoder().decode(files.get('new/master.m3u8')));
expect(newMasterPlayist).not.toBe(masterPlayist); expect(newMasterPlaylist).not.toBe(masterPlaylist);
expect(newMasterPlayist.match(/\.m3u8/g)?.length).toBe(1); expect(newMasterPlaylist.match(/\.m3u8/g)?.length).toBe(1);
}); });
test('Fractional audio sample boundary', async () => { test('Fractional audio sample boundary', async () => {
+1 -1
View File
@@ -354,7 +354,7 @@ test('MPEG-TS seeking race condition test', async () => {
} }
}); });
test('MPEG-TS video key packets', { timeout: 10_000 }, async () => { test('MPEG-TS video key packets', { timeout: 30_000 }, async () => {
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
using input = new Input({ using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny-ffmpeg.ts')), source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny-ffmpeg.ts')),
+13 -1
View File
@@ -7,7 +7,7 @@ import { Output } from '../../src/output.js';
import { MkvOutputFormat, MovOutputFormat } from '../../src/output-format.js'; import { MkvOutputFormat, MovOutputFormat } from '../../src/output-format.js';
import { BufferTarget } from '../../src/target.js'; import { BufferTarget } from '../../src/target.js';
import { Conversion } from '../../src/conversion.js'; import { Conversion } from '../../src/conversion.js';
import { VideoSampleSink } from '../../src/media-sink.js'; import { EncodedPacketSink, VideoSampleSink } from '../../src/media-sink.js';
import { assert } from '../../src/misc.js'; import { assert } from '../../src/misc.js';
const SAMPLE_URL = 'https://pub-1ee78aacb848486482b20a72b55b3121.r2.dev/turbores-sample.mov'; const SAMPLE_URL = 'https://pub-1ee78aacb848486482b20a72b55b3121.r2.dev/turbores-sample.mov';
@@ -88,6 +88,10 @@ test.concurrent('ProRes transmuxing into MKV', { timeout: 10_000 }, async () =>
}); });
await conversion.execute(); await conversion.execute();
// No 'icpf' means the frame container atom headers were successfully stripped from the ProRes packets
let str = new TextDecoder('ascii').decode(output.target.buffer!);
expect(str.includes('icpf')).toBe(false);
using newInput = new Input({ using newInput = new Input({
source: new BufferSource(output.target.buffer!), source: new BufferSource(output.target.buffer!),
formats: ALL_FORMATS, formats: ALL_FORMATS,
@@ -100,6 +104,14 @@ test.concurrent('ProRes transmuxing into MKV', { timeout: 10_000 }, async () =>
const decoderConfig = (await videoTrack.getDecoderConfig())!; const decoderConfig = (await videoTrack.getDecoderConfig())!;
expect(decoderConfig.codec).toBe('apch'); expect(decoderConfig.codec).toBe('apch');
expect(decoderConfig.description).toBeUndefined(); expect(decoderConfig.description).toBeUndefined();
const sink = new EncodedPacketSink(videoTrack);
const firstPacket = await sink.getFirstPacket();
assert(firstPacket);
// The frame container atom headers are added back when reading out the packets
str = new TextDecoder('ascii').decode(firstPacket.data);
expect(str.includes('icpf')).toBe(true);
}); });
test('Custom coder registration', { timeout: 10_000 }, async () => { test('Custom coder registration', { timeout: 10_000 }, async () => {
+2 -2
View File
@@ -801,7 +801,7 @@ describe('Video', async () => {
await conversionRoundtrip('av1'); await conversionRoundtrip('av1');
}); });
test('ProRes conversion roundtrip', { timeout: 20_000 }, async () => { test('ProRes conversion roundtrip', { timeout: 30_000 }, async () => {
await conversionRoundtrip('prores'); await conversionRoundtrip('prores');
}); });
@@ -1061,7 +1061,7 @@ describe('Video', async () => {
}); });
// https://github.com/Vanilagy/mediabunny/issues/392 // https://github.com/Vanilagy/mediabunny/issues/392
test('Memory doesn\'t leak', async () => { test('Memory doesn\'t leak', { timeout: 10_000 }, async () => {
const encoder = new NodeAvVideoEncoder(); const encoder = new NodeAvVideoEncoder();
// @ts-expect-error Readonly // @ts-expect-error Readonly
encoder.codec = 'avc'; encoder.codec = 'avc';