@osmo.inc/cli 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPLOY.md +87 -0
- package/README.md +243 -0
- package/RELEASE.md +164 -0
- package/SETUP.md +60 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/bin/osmo.mjs +12 -0
- package/dist/osmo.js +7817 -0
- package/node_modules/@esbuild/darwin-arm64/README.md +3 -0
- package/node_modules/@esbuild/darwin-arm64/bin/esbuild +0 -0
- package/node_modules/@esbuild/darwin-arm64/package.json +20 -0
- package/node_modules/esbuild/LICENSE.md +21 -0
- package/node_modules/esbuild/README.md +3 -0
- package/node_modules/esbuild/bin/esbuild +223 -0
- package/node_modules/esbuild/install.js +289 -0
- package/node_modules/esbuild/lib/main.d.ts +716 -0
- package/node_modules/esbuild/lib/main.js +2532 -0
- package/node_modules/esbuild/package.json +49 -0
- package/node_modules/fflate/LICENSE +21 -0
- package/node_modules/fflate/esm/index.mjs +2710 -0
- package/node_modules/fflate/package.json +128 -0
- package/node_modules/typescript/LICENSE.txt +55 -0
- package/node_modules/typescript/ThirdPartyNoticeText.txt +193 -0
- package/node_modules/typescript/lib/lib.d.ts +22 -0
- package/node_modules/typescript/lib/lib.decorators.d.ts +384 -0
- package/node_modules/typescript/lib/lib.decorators.legacy.d.ts +22 -0
- package/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +41 -0
- package/node_modules/typescript/lib/lib.dom.d.ts +39429 -0
- package/node_modules/typescript/lib/lib.dom.iterable.d.ts +571 -0
- package/node_modules/typescript/lib/lib.es2015.collection.d.ts +147 -0
- package/node_modules/typescript/lib/lib.es2015.core.d.ts +597 -0
- package/node_modules/typescript/lib/lib.es2015.d.ts +28 -0
- package/node_modules/typescript/lib/lib.es2015.generator.d.ts +77 -0
- package/node_modules/typescript/lib/lib.es2015.iterable.d.ts +605 -0
- package/node_modules/typescript/lib/lib.es2015.promise.d.ts +81 -0
- package/node_modules/typescript/lib/lib.es2015.proxy.d.ts +128 -0
- package/node_modules/typescript/lib/lib.es2015.reflect.d.ts +144 -0
- package/node_modules/typescript/lib/lib.es2015.symbol.d.ts +46 -0
- package/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +326 -0
- package/node_modules/typescript/lib/lib.es2016.array.include.d.ts +116 -0
- package/node_modules/typescript/lib/lib.es2016.d.ts +21 -0
- package/node_modules/typescript/lib/lib.es2016.full.d.ts +23 -0
- package/node_modules/typescript/lib/lib.es2016.intl.d.ts +31 -0
- package/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +21 -0
- package/node_modules/typescript/lib/lib.es2017.d.ts +26 -0
- package/node_modules/typescript/lib/lib.es2017.date.d.ts +31 -0
- package/node_modules/typescript/lib/lib.es2017.full.d.ts +23 -0
- package/node_modules/typescript/lib/lib.es2017.intl.d.ts +44 -0
- package/node_modules/typescript/lib/lib.es2017.object.d.ts +49 -0
- package/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +135 -0
- package/node_modules/typescript/lib/lib.es2017.string.d.ts +45 -0
- package/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +53 -0
- package/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +77 -0
- package/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +53 -0
- package/node_modules/typescript/lib/lib.es2018.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2018.full.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2018.intl.d.ts +83 -0
- package/node_modules/typescript/lib/lib.es2018.promise.d.ts +30 -0
- package/node_modules/typescript/lib/lib.es2018.regexp.d.ts +37 -0
- package/node_modules/typescript/lib/lib.es2019.array.d.ts +79 -0
- package/node_modules/typescript/lib/lib.es2019.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2019.full.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2019.intl.d.ts +23 -0
- package/node_modules/typescript/lib/lib.es2019.object.d.ts +33 -0
- package/node_modules/typescript/lib/lib.es2019.string.d.ts +37 -0
- package/node_modules/typescript/lib/lib.es2019.symbol.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2020.bigint.d.ts +765 -0
- package/node_modules/typescript/lib/lib.es2020.d.ts +27 -0
- package/node_modules/typescript/lib/lib.es2020.date.d.ts +42 -0
- package/node_modules/typescript/lib/lib.es2020.full.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2020.intl.d.ts +474 -0
- package/node_modules/typescript/lib/lib.es2020.number.d.ts +28 -0
- package/node_modules/typescript/lib/lib.es2020.promise.d.ts +47 -0
- package/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +99 -0
- package/node_modules/typescript/lib/lib.es2020.string.d.ts +44 -0
- package/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +41 -0
- package/node_modules/typescript/lib/lib.es2021.d.ts +23 -0
- package/node_modules/typescript/lib/lib.es2021.full.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2021.intl.d.ts +166 -0
- package/node_modules/typescript/lib/lib.es2021.promise.d.ts +48 -0
- package/node_modules/typescript/lib/lib.es2021.string.d.ts +33 -0
- package/node_modules/typescript/lib/lib.es2021.weakref.d.ts +78 -0
- package/node_modules/typescript/lib/lib.es2022.array.d.ts +121 -0
- package/node_modules/typescript/lib/lib.es2022.d.ts +25 -0
- package/node_modules/typescript/lib/lib.es2022.error.d.ts +75 -0
- package/node_modules/typescript/lib/lib.es2022.full.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2022.intl.d.ts +145 -0
- package/node_modules/typescript/lib/lib.es2022.object.d.ts +26 -0
- package/node_modules/typescript/lib/lib.es2022.regexp.d.ts +39 -0
- package/node_modules/typescript/lib/lib.es2022.string.d.ts +25 -0
- package/node_modules/typescript/lib/lib.es2023.array.d.ts +924 -0
- package/node_modules/typescript/lib/lib.es2023.collection.d.ts +21 -0
- package/node_modules/typescript/lib/lib.es2023.d.ts +22 -0
- package/node_modules/typescript/lib/lib.es2023.full.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2023.intl.d.ts +56 -0
- package/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +65 -0
- package/node_modules/typescript/lib/lib.es2024.collection.d.ts +29 -0
- package/node_modules/typescript/lib/lib.es2024.d.ts +26 -0
- package/node_modules/typescript/lib/lib.es2024.full.d.ts +24 -0
- package/node_modules/typescript/lib/lib.es2024.object.d.ts +29 -0
- package/node_modules/typescript/lib/lib.es2024.promise.d.ts +35 -0
- package/node_modules/typescript/lib/lib.es2024.regexp.d.ts +25 -0
- package/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +68 -0
- package/node_modules/typescript/lib/lib.es2024.string.d.ts +29 -0
- package/node_modules/typescript/lib/lib.es5.d.ts +4601 -0
- package/node_modules/typescript/lib/lib.es6.d.ts +23 -0
- package/node_modules/typescript/lib/lib.esnext.array.d.ts +35 -0
- package/node_modules/typescript/lib/lib.esnext.collection.d.ts +96 -0
- package/node_modules/typescript/lib/lib.esnext.d.ts +29 -0
- package/node_modules/typescript/lib/lib.esnext.decorators.d.ts +28 -0
- package/node_modules/typescript/lib/lib.esnext.disposable.d.ts +193 -0
- package/node_modules/typescript/lib/lib.esnext.error.d.ts +24 -0
- package/node_modules/typescript/lib/lib.esnext.float16.d.ts +445 -0
- package/node_modules/typescript/lib/lib.esnext.full.d.ts +24 -0
- package/node_modules/typescript/lib/lib.esnext.intl.d.ts +21 -0
- package/node_modules/typescript/lib/lib.esnext.iterator.d.ts +148 -0
- package/node_modules/typescript/lib/lib.esnext.promise.d.ts +34 -0
- package/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +25 -0
- package/node_modules/typescript/lib/lib.scripthost.d.ts +322 -0
- package/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +41 -0
- package/node_modules/typescript/lib/lib.webworker.d.ts +13150 -0
- package/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +23 -0
- package/node_modules/typescript/lib/lib.webworker.iterable.d.ts +340 -0
- package/node_modules/typescript/lib/typescript.js +382 -0
- package/node_modules/typescript/package.json +57 -0
- package/package.json +54 -0
- package/release.json +136 -0
- package/runtime/apps/native/tools/compile.mjs +217 -0
- package/runtime/packages/scene/scripts/validate-strict.mjs +246 -0
- package/runtime/packages/scene/src/clip.d.mts +14 -0
- package/runtime/packages/scene/src/clip.mjs +15 -0
- package/runtime/packages/scene/src/color-value.d.mts +5 -0
- package/runtime/packages/scene/src/color-value.mjs +19 -0
- package/runtime/packages/scene/src/color.ts +1 -0
- package/runtime/packages/scene/src/effects.ts +213 -0
- package/runtime/packages/scene/src/elements.ts +491 -0
- package/runtime/packages/scene/src/evaluate.ts +919 -0
- package/runtime/packages/scene/src/index.ts +69 -0
- package/runtime/packages/scene/src/jsx-runtime.ts +73 -0
- package/runtime/packages/scene/src/knob-schema.ts +115 -0
- package/runtime/packages/scene/src/knobs.ts +118 -0
- package/runtime/packages/scene/src/mask.d.mts +6 -0
- package/runtime/packages/scene/src/mask.mjs +17 -0
- package/runtime/packages/scene/src/metadata.d.mts +23 -0
- package/runtime/packages/scene/src/metadata.mjs +44 -0
- package/runtime/packages/scene/src/motion-sampling.d.mts +2 -0
- package/runtime/packages/scene/src/motion-sampling.mjs +50 -0
- package/runtime/packages/scene/src/motion.ts +121 -0
- package/runtime/packages/scene/src/shader.ts +57 -0
- package/src/resource-examples/example-image/landscape.svg +7 -0
- package/src/resource-examples/example-image/resource.json +8 -0
- package/src/resource-examples/example-model/gem.glb +0 -0
- package/src/resource-examples/example-model/preview.svg +1 -0
- package/src/resource-examples/example-model/resource.json +7 -0
- package/src/resource-examples/example-scene/resource.json +1 -0
- package/src/resource-examples/example-scene/scene.tsx +8 -0
- package/src/resource-examples/example-shader/effect.wgsl +7 -0
- package/src/resource-examples/example-shader/preview.svg +1 -0
- package/src/resource-examples/example-shader/resource.json +27 -0
- package/src/resource-examples/example-shader/tint.wgsl +7 -0
- package/src/resource-examples/example-video/ambient.mp4 +0 -0
- package/src/resource-examples/example-video/resource.json +9 -0
- package/src/starter/scene.tsx +20 -0
- package/studio/index.html +2 -0
- package/studio/public/check-circle-filled.svg +3 -0
- package/studio/public/comment-cursor/active-outer.svg +3 -0
- package/studio/public/comment-cursor/default.svg +6 -0
- package/studio/public/osmo-studio-logo.svg +12 -0
- package/studio/public/pencil-simple.svg +5 -0
- package/studio/src/active-outer-TVYUEURE.svg +3 -0
- package/studio/src/app.css +4 -0
- package/studio/src/app.js +705 -0
- package/studio/src/default-6CTRH4V5.svg +6 -0
- package/studio/src/engine_bg.wasm +0 -0
- package/studio/src/inter-cyrillic-ext-wght-normal-IYF56FF6.woff2 +0 -0
- package/studio/src/inter-cyrillic-wght-normal-JEOLYBOO.woff2 +0 -0
- package/studio/src/inter-greek-ext-wght-normal-EOVOK2B5.woff2 +0 -0
- package/studio/src/inter-greek-wght-normal-IRE366VL.woff2 +0 -0
- package/studio/src/inter-latin-ext-wght-normal-HA22NDSG.woff2 +0 -0
- package/studio/src/inter-latin-wght-normal-NRMW37G5.woff2 +0 -0
- package/studio/src/inter-vietnamese-wght-normal-CE5GGD3W.woff2 +0 -0
- package/studio/src/osmo-studio-logo-D5R2V77B.svg +12 -0
- package/third-party/cargo/adler2@2.0.1/Cargo.toml +91 -0
- package/third-party/cargo/adler2@2.0.1/LICENSE-0BSD +12 -0
- package/third-party/cargo/adler2@2.0.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/adler2@2.0.1/LICENSE-MIT +23 -0
- package/third-party/cargo/allocator-api2@0.2.21/Cargo.toml +48 -0
- package/third-party/cargo/allocator-api2@0.2.21/LICENSE-APACHE +176 -0
- package/third-party/cargo/allocator-api2@0.2.21/LICENSE-MIT +23 -0
- package/third-party/cargo/arrayref@0.3.9/Cargo.toml +23 -0
- package/third-party/cargo/arrayref@0.3.9/LICENSE +26 -0
- package/third-party/cargo/arrayvec@0.7.8/Cargo.toml +112 -0
- package/third-party/cargo/arrayvec@0.7.8/LICENSE-APACHE +201 -0
- package/third-party/cargo/arrayvec@0.7.8/LICENSE-MIT +25 -0
- package/third-party/cargo/ascii@1.1.0/Cargo.toml +41 -0
- package/third-party/cargo/ascii@1.1.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/ascii@1.1.0/LICENSE-MIT +22 -0
- package/third-party/cargo/ash@0.38.0+1.3.281/Cargo.toml +66 -0
- package/third-party/cargo/ash@0.38.0+1.3.281/LICENSE-APACHE +67 -0
- package/third-party/cargo/ash@0.38.0+1.3.281/LICENSE-MIT +25 -0
- package/third-party/cargo/autocfg@1.5.1/Cargo.toml +75 -0
- package/third-party/cargo/autocfg@1.5.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/autocfg@1.5.1/LICENSE-MIT +25 -0
- package/third-party/cargo/base64@0.13.1/Cargo.toml +53 -0
- package/third-party/cargo/base64@0.13.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/base64@0.13.1/LICENSE-MIT +21 -0
- package/third-party/cargo/base64@0.22.1/Cargo.toml +85 -0
- package/third-party/cargo/base64@0.22.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/base64@0.22.1/LICENSE-MIT +21 -0
- package/third-party/cargo/bit-set@0.10.0/Cargo.toml +108 -0
- package/third-party/cargo/bit-set@0.10.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/bit-set@0.10.0/LICENSE-MIT +25 -0
- package/third-party/cargo/bit-vec@0.9.1/Cargo.toml +94 -0
- package/third-party/cargo/bit-vec@0.9.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/bit-vec@0.9.1/LICENSE-MIT +25 -0
- package/third-party/cargo/bitflags@2.13.0/Cargo.toml +120 -0
- package/third-party/cargo/bitflags@2.13.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/bitflags@2.13.0/LICENSE-MIT +25 -0
- package/third-party/cargo/block2@0.6.2/Cargo.toml +115 -0
- package/third-party/cargo/block2@0.6.2/MIT-terms.txt +22 -0
- package/third-party/cargo/block2@0.6.2/madsmtm-objc2-b4167b582b2f-LICENSE.md +34 -0
- package/third-party/cargo/bumpalo@3.20.3/Cargo.toml +99 -0
- package/third-party/cargo/bumpalo@3.20.3/LICENSE-APACHE +201 -0
- package/third-party/cargo/bumpalo@3.20.3/LICENSE-MIT +25 -0
- package/third-party/cargo/bytemuck@1.25.1/Cargo.toml +153 -0
- package/third-party/cargo/bytemuck@1.25.1/LICENSE-APACHE +61 -0
- package/third-party/cargo/bytemuck@1.25.1/LICENSE-MIT +9 -0
- package/third-party/cargo/bytemuck@1.25.1/LICENSE-ZLIB +11 -0
- package/third-party/cargo/bytemuck_derive@1.11.0/Cargo.toml +60 -0
- package/third-party/cargo/bytemuck_derive@1.11.0/LICENSE-APACHE +61 -0
- package/third-party/cargo/bytemuck_derive@1.11.0/LICENSE-MIT +9 -0
- package/third-party/cargo/bytemuck_derive@1.11.0/LICENSE-ZLIB +11 -0
- package/third-party/cargo/byteorder-lite@0.1.0/Cargo.toml +57 -0
- package/third-party/cargo/byteorder-lite@0.1.0/LICENSE-MIT +21 -0
- package/third-party/cargo/byteorder-lite@0.1.0/UNLICENSE +24 -0
- package/third-party/cargo/byteorder@1.5.0/COPYING +3 -0
- package/third-party/cargo/byteorder@1.5.0/Cargo.toml +54 -0
- package/third-party/cargo/byteorder@1.5.0/LICENSE-MIT +21 -0
- package/third-party/cargo/byteorder@1.5.0/UNLICENSE +24 -0
- package/third-party/cargo/cfg-if@1.0.4/Cargo.toml +47 -0
- package/third-party/cargo/cfg-if@1.0.4/LICENSE-APACHE +201 -0
- package/third-party/cargo/cfg-if@1.0.4/LICENSE-MIT +25 -0
- package/third-party/cargo/cfg_aliases@0.2.1/Cargo.toml +43 -0
- package/third-party/cargo/cfg_aliases@0.2.1/LICENSE +9 -0
- package/third-party/cargo/chunked_transfer@1.5.0/Cargo.toml +27 -0
- package/third-party/cargo/chunked_transfer@1.5.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/chunked_transfer@1.5.0/LICENSE-MIT +22 -0
- package/third-party/cargo/codespan-reporting@0.13.1/Cargo.toml +115 -0
- package/third-party/cargo/codespan-reporting@0.13.1/LICENSE +201 -0
- package/third-party/cargo/color_quant@1.1.0/Cargo.toml +20 -0
- package/third-party/cargo/color_quant@1.1.0/LICENSE +21 -0
- package/third-party/cargo/console_error_panic_hook@0.1.7/Cargo.toml +27 -0
- package/third-party/cargo/console_error_panic_hook@0.1.7/LICENSE-APACHE +201 -0
- package/third-party/cargo/console_error_panic_hook@0.1.7/LICENSE-MIT +25 -0
- package/third-party/cargo/core_maths@0.1.1/Cargo.toml +37 -0
- package/third-party/cargo/core_maths@0.1.1/LICENSE +21 -0
- package/third-party/cargo/cosmic-text@0.19.0/Cargo.toml +214 -0
- package/third-party/cargo/cosmic-text@0.19.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/cosmic-text@0.19.0/LICENSE-MIT +21 -0
- package/third-party/cargo/crc32fast@1.5.0/Cargo.toml +68 -0
- package/third-party/cargo/crc32fast@1.5.0/LICENSE-APACHE +202 -0
- package/third-party/cargo/crc32fast@1.5.0/LICENSE-MIT +21 -0
- package/third-party/cargo/data-url@0.3.2/Cargo.toml +63 -0
- package/third-party/cargo/data-url@0.3.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/data-url@0.3.2/LICENSE-MIT +25 -0
- package/third-party/cargo/dispatch2@0.3.1/Cargo.toml +122 -0
- package/third-party/cargo/dispatch2@0.3.1/MIT-terms.txt +22 -0
- package/third-party/cargo/dispatch2@0.3.1/madsmtm-objc2-8852b424193c-LICENSE.md +34 -0
- package/third-party/cargo/dlib@0.5.3/Cargo.toml +43 -0
- package/third-party/cargo/dlib@0.5.3/LICENSE.txt +19 -0
- package/third-party/cargo/document-features@0.2.12/Cargo.toml +51 -0
- package/third-party/cargo/document-features@0.2.12/LICENSE-APACHE +73 -0
- package/third-party/cargo/document-features@0.2.12/LICENSE-MIT +19 -0
- package/third-party/cargo/equivalent@1.0.2/Cargo.toml +37 -0
- package/third-party/cargo/equivalent@1.0.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/equivalent@1.0.2/LICENSE-MIT +25 -0
- package/third-party/cargo/errno@0.3.14/Cargo.toml +67 -0
- package/third-party/cargo/errno@0.3.14/LICENSE-APACHE +201 -0
- package/third-party/cargo/errno@0.3.14/LICENSE-MIT +25 -0
- package/third-party/cargo/etagere@0.3.0/Cargo.toml +57 -0
- package/third-party/cargo/etagere@0.3.0/LICENSE-APACHE +13 -0
- package/third-party/cargo/etagere@0.3.0/LICENSE-MIT +20 -0
- package/third-party/cargo/euclid@0.22.14/COPYRIGHT +5 -0
- package/third-party/cargo/euclid@0.22.14/Cargo.toml +66 -0
- package/third-party/cargo/euclid@0.22.14/LICENSE-APACHE +201 -0
- package/third-party/cargo/euclid@0.22.14/LICENSE-MIT +25 -0
- package/third-party/cargo/fastrand@2.5.0/Cargo.toml +89 -0
- package/third-party/cargo/fastrand@2.5.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/fastrand@2.5.0/LICENSE-MIT +23 -0
- package/third-party/cargo/fdeflate@0.3.7/Cargo.toml +55 -0
- package/third-party/cargo/fdeflate@0.3.7/LICENSE-APACHE +177 -0
- package/third-party/cargo/fdeflate@0.3.7/LICENSE-MIT +25 -0
- package/third-party/cargo/flate2@1.1.9/Cargo.toml +274 -0
- package/third-party/cargo/flate2@1.1.9/LICENSE-APACHE +201 -0
- package/third-party/cargo/flate2@1.1.9/LICENSE-MIT +25 -0
- package/third-party/cargo/float-cmp@0.9.0/Cargo.toml +39 -0
- package/third-party/cargo/float-cmp@0.9.0/LICENSE +19 -0
- package/third-party/cargo/float_next_after@1.0.0/Cargo.toml +43 -0
- package/third-party/cargo/float_next_after@1.0.0/LICENSE +23 -0
- package/third-party/cargo/foldhash@0.2.0/Cargo.toml +80 -0
- package/third-party/cargo/foldhash@0.2.0/LICENSE +19 -0
- package/third-party/cargo/font-types@0.11.3/Cargo.toml +55 -0
- package/third-party/cargo/font-types@0.11.3/LICENSE-APACHE +67 -0
- package/third-party/cargo/font-types@0.11.3/LICENSE-MIT +25 -0
- package/third-party/cargo/fontdb@0.23.0/Cargo.toml +83 -0
- package/third-party/cargo/fontdb@0.23.0/LICENSE +21 -0
- package/third-party/cargo/futures-core@0.3.32/Cargo.toml +66 -0
- package/third-party/cargo/futures-core@0.3.32/LICENSE-APACHE +202 -0
- package/third-party/cargo/futures-core@0.3.32/LICENSE-MIT +26 -0
- package/third-party/cargo/futures-task@0.3.32/Cargo.toml +58 -0
- package/third-party/cargo/futures-task@0.3.32/LICENSE-APACHE +202 -0
- package/third-party/cargo/futures-task@0.3.32/LICENSE-MIT +26 -0
- package/third-party/cargo/futures-util@0.3.32/Cargo.toml +181 -0
- package/third-party/cargo/futures-util@0.3.32/LICENSE-APACHE +202 -0
- package/third-party/cargo/futures-util@0.3.32/LICENSE-MIT +26 -0
- package/third-party/cargo/getrandom@0.4.3/Cargo.toml +165 -0
- package/third-party/cargo/getrandom@0.4.3/LICENSE-APACHE +201 -0
- package/third-party/cargo/getrandom@0.4.3/LICENSE-MIT +26 -0
- package/third-party/cargo/gif@0.14.2/Cargo.toml +84 -0
- package/third-party/cargo/gif@0.14.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/gif@0.14.2/LICENSE-MIT +21 -0
- package/third-party/cargo/glam@0.33.2/Cargo.toml +354 -0
- package/third-party/cargo/glam@0.33.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/glam@0.33.2/LICENSE-MIT +23 -0
- package/third-party/cargo/glow@0.17.0/Cargo.toml +122 -0
- package/third-party/cargo/glow@0.17.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/glow@0.17.0/LICENSE-MIT +23 -0
- package/third-party/cargo/glow@0.17.0/LICENSE-ZLIB +15 -0
- package/third-party/cargo/gltf-derive@1.4.1/Cargo.toml +34 -0
- package/third-party/cargo/gltf-derive@1.4.1/LICENSE-APACHE +202 -0
- package/third-party/cargo/gltf-derive@1.4.1/LICENSE-MIT +26 -0
- package/third-party/cargo/gltf-json@1.4.1/Cargo.toml +50 -0
- package/third-party/cargo/gltf-json@1.4.1/LICENSE-APACHE +202 -0
- package/third-party/cargo/gltf-json@1.4.1/LICENSE-MIT +26 -0
- package/third-party/cargo/gltf@1.4.1/Cargo.toml +122 -0
- package/third-party/cargo/gltf@1.4.1/LICENSE-APACHE +202 -0
- package/third-party/cargo/gltf@1.4.1/LICENSE-MIT +26 -0
- package/third-party/cargo/glyphon@0.12.0/Cargo.toml +35 -0
- package/third-party/cargo/glyphon@0.12.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/glyphon@0.12.0/LICENSE-MIT +23 -0
- package/third-party/cargo/glyphon@0.12.0/LICENSE-ZLIB +15 -0
- package/third-party/cargo/gpu-allocator@0.28.0/Cargo.toml +180 -0
- package/third-party/cargo/gpu-allocator@0.28.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/gpu-allocator@0.28.0/LICENSE-MIT +7 -0
- package/third-party/cargo/grid@1.0.1/Cargo.toml +77 -0
- package/third-party/cargo/grid@1.0.1/LICENSE +21 -0
- package/third-party/cargo/half@2.7.1/Cargo.toml +137 -0
- package/third-party/cargo/half@2.7.1/LICENSE-APACHE +176 -0
- package/third-party/cargo/half@2.7.1/LICENSE-MIT +19 -0
- package/third-party/cargo/harfrust@0.5.2/Cargo.toml +115 -0
- package/third-party/cargo/harfrust@0.5.2/harfbuzz-harfrust-efdae3142ab7-LICENSE +22 -0
- package/third-party/cargo/hashbrown@0.16.1/Cargo.toml +180 -0
- package/third-party/cargo/hashbrown@0.16.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/hashbrown@0.16.1/LICENSE-MIT +25 -0
- package/third-party/cargo/hashbrown@0.17.1/Cargo.toml +201 -0
- package/third-party/cargo/hashbrown@0.17.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/hashbrown@0.17.1/LICENSE-MIT +25 -0
- package/third-party/cargo/httpdate@1.0.3/Cargo.toml +35 -0
- package/third-party/cargo/httpdate@1.0.3/LICENSE-APACHE +201 -0
- package/third-party/cargo/httpdate@1.0.3/LICENSE-MIT +19 -0
- package/third-party/cargo/image-webp@0.2.4/Cargo.toml +63 -0
- package/third-party/cargo/image-webp@0.2.4/LICENSE-APACHE +177 -0
- package/third-party/cargo/image-webp@0.2.4/LICENSE-MIT +25 -0
- package/third-party/cargo/image@0.25.10/Cargo.toml +248 -0
- package/third-party/cargo/image@0.25.10/LICENSE-APACHE +177 -0
- package/third-party/cargo/image@0.25.10/LICENSE-MIT +25 -0
- package/third-party/cargo/imagesize@0.14.0/Cargo.toml +104 -0
- package/third-party/cargo/imagesize@0.14.0/LICENSE +21 -0
- package/third-party/cargo/indexmap@2.14.0/Cargo.toml +167 -0
- package/third-party/cargo/indexmap@2.14.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/indexmap@2.14.0/LICENSE-MIT +25 -0
- package/third-party/cargo/inflections@1.1.1/Cargo.toml +21 -0
- package/third-party/cargo/inflections@1.1.1/LICENSE +20 -0
- package/third-party/cargo/itoa@1.0.18/Cargo.toml +66 -0
- package/third-party/cargo/itoa@1.0.18/LICENSE-APACHE +176 -0
- package/third-party/cargo/itoa@1.0.18/LICENSE-MIT +23 -0
- package/third-party/cargo/js-sys@0.3.103/Cargo.toml +98 -0
- package/third-party/cargo/js-sys@0.3.103/LICENSE-APACHE +201 -0
- package/third-party/cargo/js-sys@0.3.103/LICENSE-MIT +25 -0
- package/third-party/cargo/khronos-egl@6.0.0/Cargo.toml +82 -0
- package/third-party/cargo/khronos-egl@6.0.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/khronos-egl@6.0.0/LICENSE-MIT +23 -0
- package/third-party/cargo/kurbo@0.13.1/Cargo.toml +230 -0
- package/third-party/cargo/kurbo@0.13.1/LICENSE-APACHE +202 -0
- package/third-party/cargo/kurbo@0.13.1/LICENSE-MIT +25 -0
- package/third-party/cargo/lazy_static@1.5.0/Cargo.toml +46 -0
- package/third-party/cargo/lazy_static@1.5.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/lazy_static@1.5.0/LICENSE-MIT +25 -0
- package/third-party/cargo/libc@0.2.186/Cargo.toml +229 -0
- package/third-party/cargo/libc@0.2.186/LICENSE-APACHE +176 -0
- package/third-party/cargo/libc@0.2.186/LICENSE-MIT +25 -0
- package/third-party/cargo/libloading@0.8.9/Cargo.toml +90 -0
- package/third-party/cargo/libloading@0.8.9/LICENSE +12 -0
- package/third-party/cargo/libm@0.2.16/Cargo.toml +61 -0
- package/third-party/cargo/libm@0.2.16/LICENSE.txt +258 -0
- package/third-party/cargo/linebender_resource_handle@0.1.1/Cargo.toml +107 -0
- package/third-party/cargo/linebender_resource_handle@0.1.1/LICENSE-APACHE +176 -0
- package/third-party/cargo/linebender_resource_handle@0.1.1/LICENSE-MIT +19 -0
- package/third-party/cargo/litrs@1.0.0/Cargo.toml +63 -0
- package/third-party/cargo/litrs@1.0.0/LICENSE-APACHE +176 -0
- package/third-party/cargo/litrs@1.0.0/LICENSE-MIT +25 -0
- package/third-party/cargo/lock_api@0.4.14/Cargo.toml +68 -0
- package/third-party/cargo/lock_api@0.4.14/LICENSE-APACHE +201 -0
- package/third-party/cargo/lock_api@0.4.14/LICENSE-MIT +25 -0
- package/third-party/cargo/log@0.4.33/Cargo.toml +152 -0
- package/third-party/cargo/log@0.4.33/LICENSE-APACHE +201 -0
- package/third-party/cargo/log@0.4.33/LICENSE-MIT +25 -0
- package/third-party/cargo/lru@0.16.4/Cargo.toml +47 -0
- package/third-party/cargo/lru@0.16.4/LICENSE +21 -0
- package/third-party/cargo/lyon_geom@1.0.19/Cargo.toml +69 -0
- package/third-party/cargo/lyon_geom@1.0.19/nical-lyon-56219c95c658-LICENSE-APACHE +13 -0
- package/third-party/cargo/lyon_geom@1.0.19/nical-lyon-56219c95c658-LICENSE-MIT +20 -0
- package/third-party/cargo/lyon_path@1.0.19/Cargo.toml +58 -0
- package/third-party/cargo/lyon_path@1.0.19/nical-lyon-56219c95c658-LICENSE-APACHE +13 -0
- package/third-party/cargo/lyon_path@1.0.19/nical-lyon-56219c95c658-LICENSE-MIT +20 -0
- package/third-party/cargo/lyon_tessellation@1.0.20/Cargo.toml +71 -0
- package/third-party/cargo/lyon_tessellation@1.0.20/nical-lyon-7fa75ea8eea3-LICENSE-APACHE +13 -0
- package/third-party/cargo/lyon_tessellation@1.0.20/nical-lyon-7fa75ea8eea3-LICENSE-MIT +20 -0
- package/third-party/cargo/memchr@2.8.3/COPYING +3 -0
- package/third-party/cargo/memchr@2.8.3/Cargo.toml +89 -0
- package/third-party/cargo/memchr@2.8.3/LICENSE-MIT +21 -0
- package/third-party/cargo/memchr@2.8.3/UNLICENSE +24 -0
- package/third-party/cargo/memmap2@0.9.11/Cargo.toml +59 -0
- package/third-party/cargo/memmap2@0.9.11/LICENSE-APACHE +201 -0
- package/third-party/cargo/memmap2@0.9.11/LICENSE-MIT +26 -0
- package/third-party/cargo/miniz_oxide@0.8.9/Cargo.toml +91 -0
- package/third-party/cargo/miniz_oxide@0.8.9/LICENSE +25 -0
- package/third-party/cargo/miniz_oxide@0.8.9/LICENSE-APACHE.md +177 -0
- package/third-party/cargo/miniz_oxide@0.8.9/LICENSE-MIT.md +24 -0
- package/third-party/cargo/miniz_oxide@0.8.9/LICENSE-ZLIB.md +14 -0
- package/third-party/cargo/moxcms@0.8.1/Cargo.toml +113 -0
- package/third-party/cargo/moxcms@0.8.1/LICENSE-APACHE.md +201 -0
- package/third-party/cargo/moxcms@0.8.1/LICENSE.md +26 -0
- package/third-party/cargo/naga-types@30.0.0/Cargo.toml +94 -0
- package/third-party/cargo/naga-types@30.0.0/LICENSE.APACHE +176 -0
- package/third-party/cargo/naga-types@30.0.0/LICENSE.MIT +21 -0
- package/third-party/cargo/naga@30.0.0/Cargo.toml +246 -0
- package/third-party/cargo/naga@30.0.0/LICENSE.APACHE +176 -0
- package/third-party/cargo/naga@30.0.0/LICENSE.MIT +21 -0
- package/third-party/cargo/num-traits@0.2.19/Cargo.toml +54 -0
- package/third-party/cargo/num-traits@0.2.19/LICENSE-APACHE +201 -0
- package/third-party/cargo/num-traits@0.2.19/LICENSE-MIT +25 -0
- package/third-party/cargo/objc2-core-foundation@0.3.2/Cargo.toml +221 -0
- package/third-party/cargo/objc2-core-foundation@0.3.2/MIT-terms.txt +22 -0
- package/third-party/cargo/objc2-core-foundation@0.3.2/madsmtm-objc2-7b1abfd750a2-LICENSE.md +34 -0
- package/third-party/cargo/objc2-core-graphics@0.3.2/Cargo.toml +331 -0
- package/third-party/cargo/objc2-core-graphics@0.3.2/MIT-terms.txt +22 -0
- package/third-party/cargo/objc2-core-graphics@0.3.2/madsmtm-objc2-7b1abfd750a2-LICENSE.md +34 -0
- package/third-party/cargo/objc2-encode@4.1.0/Cargo.toml +81 -0
- package/third-party/cargo/objc2-encode@4.1.0/MIT-terms.txt +22 -0
- package/third-party/cargo/objc2-encode@4.1.0/madsmtm-objc2-8d214f547736-LICENSE.md +34 -0
- package/third-party/cargo/objc2-foundation@0.3.2/Cargo.toml +529 -0
- package/third-party/cargo/objc2-foundation@0.3.2/MIT-terms.txt +22 -0
- package/third-party/cargo/objc2-foundation@0.3.2/madsmtm-objc2-7b1abfd750a2-LICENSE.md +34 -0
- package/third-party/cargo/objc2-io-surface@0.3.2/Cargo.toml +139 -0
- package/third-party/cargo/objc2-io-surface@0.3.2/MIT-terms.txt +22 -0
- package/third-party/cargo/objc2-io-surface@0.3.2/madsmtm-objc2-7b1abfd750a2-LICENSE.md +34 -0
- package/third-party/cargo/objc2-metal@0.3.2/Cargo.toml +601 -0
- package/third-party/cargo/objc2-metal@0.3.2/MIT-terms.txt +22 -0
- package/third-party/cargo/objc2-metal@0.3.2/madsmtm-objc2-7b1abfd750a2-LICENSE.md +34 -0
- package/third-party/cargo/objc2-quartz-core@0.3.2/Cargo.toml +302 -0
- package/third-party/cargo/objc2-quartz-core@0.3.2/MIT-terms.txt +22 -0
- package/third-party/cargo/objc2-quartz-core@0.3.2/madsmtm-objc2-7b1abfd750a2-LICENSE.md +34 -0
- package/third-party/cargo/objc2@0.6.4/Cargo.toml +232 -0
- package/third-party/cargo/objc2@0.6.4/MIT-terms.txt +22 -0
- package/third-party/cargo/objc2@0.6.4/madsmtm-objc2-8852b424193c-LICENSE.md +34 -0
- package/third-party/cargo/once_cell@1.21.4/Cargo.toml +119 -0
- package/third-party/cargo/once_cell@1.21.4/LICENSE-APACHE +201 -0
- package/third-party/cargo/once_cell@1.21.4/LICENSE-MIT +23 -0
- package/third-party/cargo/ordered-float@5.3.0/Cargo.toml +162 -0
- package/third-party/cargo/ordered-float@5.3.0/LICENSE-MIT +25 -0
- package/third-party/cargo/parking_lot@0.12.5/Cargo.toml +87 -0
- package/third-party/cargo/parking_lot@0.12.5/LICENSE-APACHE +201 -0
- package/third-party/cargo/parking_lot@0.12.5/LICENSE-MIT +25 -0
- package/third-party/cargo/parking_lot_core@0.9.12/Cargo.toml +72 -0
- package/third-party/cargo/parking_lot_core@0.9.12/LICENSE-APACHE +201 -0
- package/third-party/cargo/parking_lot_core@0.9.12/LICENSE-MIT +25 -0
- package/third-party/cargo/pico-args@0.5.0/Cargo.toml +32 -0
- package/third-party/cargo/pico-args@0.5.0/LICENSE +20 -0
- package/third-party/cargo/pin-project-lite@0.2.17/Cargo.toml +198 -0
- package/third-party/cargo/pin-project-lite@0.2.17/LICENSE-APACHE +177 -0
- package/third-party/cargo/pin-project-lite@0.2.17/LICENSE-MIT +23 -0
- package/third-party/cargo/pkg-config@0.3.33/Cargo.toml +43 -0
- package/third-party/cargo/pkg-config@0.3.33/LICENSE-APACHE +201 -0
- package/third-party/cargo/pkg-config@0.3.33/LICENSE-MIT +25 -0
- package/third-party/cargo/png@0.18.1/Cargo.toml +130 -0
- package/third-party/cargo/png@0.18.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/png@0.18.1/LICENSE-MIT +25 -0
- package/third-party/cargo/pollster@0.4.0/Cargo.toml +53 -0
- package/third-party/cargo/pollster@0.4.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/pollster@0.4.0/LICENSE-MIT +25 -0
- package/third-party/cargo/polycool@0.4.0/Cargo.toml +83 -0
- package/third-party/cargo/polycool@0.4.0/LICENSE-APACHE +202 -0
- package/third-party/cargo/polycool@0.4.0/LICENSE-MIT +25 -0
- package/third-party/cargo/pp-rs@0.2.1/Cargo.toml +23 -0
- package/third-party/cargo/pp-rs@0.2.1/LICENSE +29 -0
- package/third-party/cargo/presser@0.3.1/Cargo.toml +58 -0
- package/third-party/cargo/presser@0.3.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/presser@0.3.1/LICENSE-MIT +25 -0
- package/third-party/cargo/proc-macro2@1.0.106/Cargo.toml +105 -0
- package/third-party/cargo/proc-macro2@1.0.106/LICENSE-APACHE +176 -0
- package/third-party/cargo/proc-macro2@1.0.106/LICENSE-MIT +23 -0
- package/third-party/cargo/profiling@1.0.18/Cargo.toml +96 -0
- package/third-party/cargo/profiling@1.0.18/aclysma-profiling-8271551172eb-LICENSE-APACHE +201 -0
- package/third-party/cargo/profiling@1.0.18/aclysma-profiling-8271551172eb-LICENSE-MIT +21 -0
- package/third-party/cargo/pxfm@0.1.30/Cargo.toml +53 -0
- package/third-party/cargo/pxfm@0.1.30/LICENSE-APACHE.md +201 -0
- package/third-party/cargo/pxfm@0.1.30/LICENSE.md +26 -0
- package/third-party/cargo/quick-error@2.0.1/Cargo.toml +24 -0
- package/third-party/cargo/quick-error@2.0.1/LICENSE-APACHE +202 -0
- package/third-party/cargo/quick-error@2.0.1/LICENSE-MIT +19 -0
- package/third-party/cargo/quote@1.0.46/Cargo.toml +70 -0
- package/third-party/cargo/quote@1.0.46/LICENSE-APACHE +176 -0
- package/third-party/cargo/quote@1.0.46/LICENSE-MIT +23 -0
- package/third-party/cargo/rangemap@1.7.1/Cargo.toml +91 -0
- package/third-party/cargo/rangemap@1.7.1/LICENSE-APACHE +191 -0
- package/third-party/cargo/rangemap@1.7.1/LICENSE-MIT +7 -0
- package/third-party/cargo/raw-window-handle@0.6.2/Cargo.toml +56 -0
- package/third-party/cargo/raw-window-handle@0.6.2/LICENSE-APACHE.md +177 -0
- package/third-party/cargo/raw-window-handle@0.6.2/LICENSE-MIT.md +21 -0
- package/third-party/cargo/raw-window-handle@0.6.2/LICENSE-ZLIB.md +11 -0
- package/third-party/cargo/raw-window-metal@1.1.0/Cargo.toml +111 -0
- package/third-party/cargo/raw-window-metal@1.1.0/LICENSE-APACHE +202 -0
- package/third-party/cargo/raw-window-metal@1.1.0/LICENSE-MIT +17 -0
- package/third-party/cargo/read-fonts@0.37.0/Cargo.toml +95 -0
- package/third-party/cargo/read-fonts@0.37.0/LICENSE-APACHE +67 -0
- package/third-party/cargo/read-fonts@0.37.0/LICENSE-MIT +25 -0
- package/third-party/cargo/read-fonts@0.39.2/Cargo.toml +96 -0
- package/third-party/cargo/read-fonts@0.39.2/LICENSE-APACHE +67 -0
- package/third-party/cargo/read-fonts@0.39.2/LICENSE-MIT +25 -0
- package/third-party/cargo/renderdoc-sys@1.1.0/Cargo.toml +39 -0
- package/third-party/cargo/renderdoc-sys@1.1.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/renderdoc-sys@1.1.0/LICENSE-MIT +25 -0
- package/third-party/cargo/resvg@0.47.0/Cargo.toml +112 -0
- package/third-party/cargo/resvg@0.47.0/LICENSE-APACHE +176 -0
- package/third-party/cargo/resvg@0.47.0/LICENSE-MIT +25 -0
- package/third-party/cargo/rgb@0.8.53/Cargo.toml +110 -0
- package/third-party/cargo/rgb@0.8.53/LICENSE +21 -0
- package/third-party/cargo/roxmltree@0.21.1/Cargo.toml +75 -0
- package/third-party/cargo/roxmltree@0.21.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/roxmltree@0.21.1/LICENSE-MIT +21 -0
- package/third-party/cargo/rustc-hash@1.1.0/Cargo.toml +25 -0
- package/third-party/cargo/rustc-hash@1.1.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/rustc-hash@1.1.0/LICENSE-MIT +23 -0
- package/third-party/cargo/rustc-hash@2.1.3/Cargo.toml +50 -0
- package/third-party/cargo/rustc-hash@2.1.3/LICENSE-APACHE +176 -0
- package/third-party/cargo/rustc-hash@2.1.3/LICENSE-MIT +23 -0
- package/third-party/cargo/rustix@1.1.4/COPYRIGHT +29 -0
- package/third-party/cargo/rustix@1.1.4/Cargo.toml +292 -0
- package/third-party/cargo/rustix@1.1.4/LICENSE-APACHE +201 -0
- package/third-party/cargo/rustix@1.1.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/third-party/cargo/rustix@1.1.4/LICENSE-MIT +23 -0
- package/third-party/cargo/rustversion@1.0.23/Cargo.toml +69 -0
- package/third-party/cargo/rustversion@1.0.23/LICENSE-APACHE +176 -0
- package/third-party/cargo/rustversion@1.0.23/LICENSE-MIT +23 -0
- package/third-party/cargo/rustybuzz@0.20.1/Cargo.toml +110 -0
- package/third-party/cargo/rustybuzz@0.20.1/LICENSE +22 -0
- package/third-party/cargo/scopeguard@1.2.0/Cargo.toml +43 -0
- package/third-party/cargo/scopeguard@1.2.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/scopeguard@1.2.0/LICENSE-MIT +25 -0
- package/third-party/cargo/self_cell@1.2.2/Cargo.toml +33 -0
- package/third-party/cargo/self_cell@1.2.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/self_cell@1.2.2/LICENSE-GPLv2 +339 -0
- package/third-party/cargo/self_cell@1.3.0/Cargo.toml +33 -0
- package/third-party/cargo/self_cell@1.3.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/self_cell@1.3.0/LICENSE-GPLv2 +339 -0
- package/third-party/cargo/serde@1.0.228/Cargo.toml +84 -0
- package/third-party/cargo/serde@1.0.228/LICENSE-APACHE +176 -0
- package/third-party/cargo/serde@1.0.228/LICENSE-MIT +23 -0
- package/third-party/cargo/serde_core@1.0.228/Cargo.toml +87 -0
- package/third-party/cargo/serde_core@1.0.228/LICENSE-APACHE +176 -0
- package/third-party/cargo/serde_core@1.0.228/LICENSE-MIT +23 -0
- package/third-party/cargo/serde_derive@1.0.228/Cargo.toml +87 -0
- package/third-party/cargo/serde_derive@1.0.228/LICENSE-APACHE +176 -0
- package/third-party/cargo/serde_derive@1.0.228/LICENSE-MIT +23 -0
- package/third-party/cargo/serde_json@1.0.150/Cargo.toml +162 -0
- package/third-party/cargo/serde_json@1.0.150/LICENSE-APACHE +176 -0
- package/third-party/cargo/serde_json@1.0.150/LICENSE-MIT +23 -0
- package/third-party/cargo/simd-adler32@0.3.10/Cargo.toml +68 -0
- package/third-party/cargo/simd-adler32@0.3.10/LICENSE.md +21 -0
- package/third-party/cargo/simplecss@0.2.2/Cargo.toml +146 -0
- package/third-party/cargo/simplecss@0.2.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/simplecss@0.2.2/LICENSE-MIT +21 -0
- package/third-party/cargo/siphasher@1.0.3/COPYING +7 -0
- package/third-party/cargo/siphasher@1.0.3/Cargo.toml +64 -0
- package/third-party/cargo/skrifa@0.40.0/Cargo.toml +86 -0
- package/third-party/cargo/skrifa@0.40.0/LICENSE-APACHE +67 -0
- package/third-party/cargo/skrifa@0.40.0/LICENSE-MIT +25 -0
- package/third-party/cargo/skrifa@0.42.1/Cargo.toml +86 -0
- package/third-party/cargo/skrifa@0.42.1/LICENSE-APACHE +67 -0
- package/third-party/cargo/skrifa@0.42.1/LICENSE-MIT +25 -0
- package/third-party/cargo/slab@0.4.12/Cargo.toml +67 -0
- package/third-party/cargo/slab@0.4.12/LICENSE +25 -0
- package/third-party/cargo/slotmap@1.1.1/Cargo.toml +83 -0
- package/third-party/cargo/slotmap@1.1.1/LICENSE +19 -0
- package/third-party/cargo/smallvec@1.15.2/Cargo.toml +103 -0
- package/third-party/cargo/smallvec@1.15.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/smallvec@1.15.2/LICENSE-MIT +25 -0
- package/third-party/cargo/smol_str@0.3.6/Cargo.toml +155 -0
- package/third-party/cargo/smol_str@0.3.6/LICENSE-APACHE +201 -0
- package/third-party/cargo/smol_str@0.3.6/LICENSE-MIT +23 -0
- package/third-party/cargo/spirv@0.4.0+sdk-1.4.341.0/Cargo.toml +56 -0
- package/third-party/cargo/spirv@0.4.0+sdk-1.4.341.0/gfx-rs-rspirv-8afc3d0ac8e1-LICENSE +202 -0
- package/third-party/cargo/static_assertions@1.1.0/Cargo.toml +39 -0
- package/third-party/cargo/static_assertions@1.1.0/LICENSE-APACHE +202 -0
- package/third-party/cargo/static_assertions@1.1.0/LICENSE-MIT +21 -0
- package/third-party/cargo/strict-num@0.1.1/Cargo.toml +35 -0
- package/third-party/cargo/strict-num@0.1.1/LICENSE +19 -0
- package/third-party/cargo/svgtypes@0.16.1/Cargo.toml +57 -0
- package/third-party/cargo/svgtypes@0.16.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/svgtypes@0.16.1/LICENSE-MIT +20 -0
- package/third-party/cargo/swash@0.2.9/Cargo.toml +93 -0
- package/third-party/cargo/swash@0.2.9/LICENSE-APACHE +201 -0
- package/third-party/cargo/swash@0.2.9/LICENSE-MIT +25 -0
- package/third-party/cargo/syn@2.0.118/Cargo.toml +272 -0
- package/third-party/cargo/syn@2.0.118/LICENSE-APACHE +176 -0
- package/third-party/cargo/syn@2.0.118/LICENSE-MIT +23 -0
- package/third-party/cargo/sys-locale@0.3.2/Cargo.toml +73 -0
- package/third-party/cargo/sys-locale@0.3.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/sys-locale@0.3.2/LICENSE-MIT +21 -0
- package/third-party/cargo/taffy@0.12.1/Cargo.toml +171 -0
- package/third-party/cargo/taffy@0.12.1/DioxusLabs-taffy-1c9e37a0a1cf-LICENSE.md +25 -0
- package/third-party/cargo/tempfile@3.27.0/Cargo.toml +105 -0
- package/third-party/cargo/tempfile@3.27.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/tempfile@3.27.0/LICENSE-MIT +25 -0
- package/third-party/cargo/termcolor@1.4.1/COPYING +3 -0
- package/third-party/cargo/termcolor@1.4.1/Cargo.toml +40 -0
- package/third-party/cargo/termcolor@1.4.1/LICENSE-MIT +21 -0
- package/third-party/cargo/termcolor@1.4.1/UNLICENSE +24 -0
- package/third-party/cargo/thiserror-impl@2.0.18/Cargo.toml +52 -0
- package/third-party/cargo/thiserror-impl@2.0.18/LICENSE-APACHE +176 -0
- package/third-party/cargo/thiserror-impl@2.0.18/LICENSE-MIT +23 -0
- package/third-party/cargo/thiserror@2.0.18/Cargo.toml +119 -0
- package/third-party/cargo/thiserror@2.0.18/LICENSE-APACHE +176 -0
- package/third-party/cargo/thiserror@2.0.18/LICENSE-MIT +23 -0
- package/third-party/cargo/tiny-skia-path@0.12.0/Cargo.toml +58 -0
- package/third-party/cargo/tiny-skia-path@0.12.0/LICENSE +30 -0
- package/third-party/cargo/tiny-skia@0.12.0/Cargo.toml +116 -0
- package/third-party/cargo/tiny-skia@0.12.0/LICENSE +30 -0
- package/third-party/cargo/tiny_http@0.12.0/Cargo.toml +82 -0
- package/third-party/cargo/tiny_http@0.12.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/tiny_http@0.12.0/LICENSE-MIT +25 -0
- package/third-party/cargo/tinyvec@1.12.0/Cargo.toml +184 -0
- package/third-party/cargo/tinyvec@1.12.0/LICENSE-APACHE.md +202 -0
- package/third-party/cargo/tinyvec@1.12.0/LICENSE-MIT.md +5 -0
- package/third-party/cargo/tinyvec@1.12.0/LICENSE-ZLIB.md +11 -0
- package/third-party/cargo/tinyvec_macros@0.1.1/Cargo.toml +19 -0
- package/third-party/cargo/tinyvec_macros@0.1.1/LICENSE-APACHE.md +202 -0
- package/third-party/cargo/tinyvec_macros@0.1.1/LICENSE-MIT.md +21 -0
- package/third-party/cargo/tinyvec_macros@0.1.1/LICENSE-ZLIB.md +20 -0
- package/third-party/cargo/ttf-parser@0.25.1/Cargo.toml +101 -0
- package/third-party/cargo/ttf-parser@0.25.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/ttf-parser@0.25.1/LICENSE-MIT +20 -0
- package/third-party/cargo/unicode-bidi-mirroring@0.4.0/Cargo.toml +22 -0
- package/third-party/cargo/unicode-bidi-mirroring@0.4.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-bidi-mirroring@0.4.0/LICENSE-MIT +21 -0
- package/third-party/cargo/unicode-bidi@0.3.18/COPYRIGHT +8 -0
- package/third-party/cargo/unicode-bidi@0.3.18/Cargo.toml +90 -0
- package/third-party/cargo/unicode-bidi@0.3.18/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-bidi@0.3.18/LICENSE-MIT +25 -0
- package/third-party/cargo/unicode-ccc@0.4.0/Cargo.toml +22 -0
- package/third-party/cargo/unicode-ccc@0.4.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-ccc@0.4.0/LICENSE-MIT +21 -0
- package/third-party/cargo/unicode-ident@1.0.24/Cargo.toml +84 -0
- package/third-party/cargo/unicode-ident@1.0.24/LICENSE-APACHE +176 -0
- package/third-party/cargo/unicode-ident@1.0.24/LICENSE-MIT +23 -0
- package/third-party/cargo/unicode-ident@1.0.24/LICENSE-UNICODE +39 -0
- package/third-party/cargo/unicode-linebreak@0.1.5/Cargo.toml +32 -0
- package/third-party/cargo/unicode-linebreak@0.1.5/LICENSE +201 -0
- package/third-party/cargo/unicode-properties@0.1.4/COPYRIGHT +7 -0
- package/third-party/cargo/unicode-properties@0.1.4/Cargo.toml +65 -0
- package/third-party/cargo/unicode-properties@0.1.4/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-properties@0.1.4/LICENSE-MIT +25 -0
- package/third-party/cargo/unicode-script@0.5.8/Cargo.toml +63 -0
- package/third-party/cargo/unicode-script@0.5.8/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-script@0.5.8/LICENSE-MIT +27 -0
- package/third-party/cargo/unicode-segmentation@1.13.3/COPYRIGHT +7 -0
- package/third-party/cargo/unicode-segmentation@1.13.3/Cargo.toml +91 -0
- package/third-party/cargo/unicode-segmentation@1.13.3/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-segmentation@1.13.3/LICENSE-MIT +25 -0
- package/third-party/cargo/unicode-vo@0.1.0/Cargo.toml +22 -0
- package/third-party/cargo/unicode-vo@0.1.0/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-vo@0.1.0/LICENSE-MIT +21 -0
- package/third-party/cargo/unicode-width@0.2.2/COPYRIGHT +7 -0
- package/third-party/cargo/unicode-width@0.2.2/Cargo.toml +77 -0
- package/third-party/cargo/unicode-width@0.2.2/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-width@0.2.2/LICENSE-MIT +25 -0
- package/third-party/cargo/unicode-xid@0.2.6/COPYRIGHT +7 -0
- package/third-party/cargo/unicode-xid@0.2.6/Cargo.toml +65 -0
- package/third-party/cargo/unicode-xid@0.2.6/LICENSE-APACHE +201 -0
- package/third-party/cargo/unicode-xid@0.2.6/LICENSE-MIT +25 -0
- package/third-party/cargo/urlencoding@2.1.3/Cargo.toml +41 -0
- package/third-party/cargo/urlencoding@2.1.3/LICENSE +20 -0
- package/third-party/cargo/usvg@0.47.0/Cargo.toml +138 -0
- package/third-party/cargo/usvg@0.47.0/LICENSE-APACHE +176 -0
- package/third-party/cargo/usvg@0.47.0/LICENSE-MIT +25 -0
- package/third-party/cargo/version_check@0.9.5/Cargo.toml +38 -0
- package/third-party/cargo/version_check@0.9.5/LICENSE-APACHE +201 -0
- package/third-party/cargo/version_check@0.9.5/LICENSE-MIT +19 -0
- package/third-party/cargo/wasm-bindgen-futures@0.4.76/Cargo.toml +88 -0
- package/third-party/cargo/wasm-bindgen-futures@0.4.76/LICENSE-APACHE +201 -0
- package/third-party/cargo/wasm-bindgen-futures@0.4.76/LICENSE-MIT +25 -0
- package/third-party/cargo/wasm-bindgen-macro-support@0.2.126/Cargo.toml +83 -0
- package/third-party/cargo/wasm-bindgen-macro-support@0.2.126/LICENSE-APACHE +201 -0
- package/third-party/cargo/wasm-bindgen-macro-support@0.2.126/LICENSE-MIT +25 -0
- package/third-party/cargo/wasm-bindgen-macro@0.2.126/Cargo.toml +73 -0
- package/third-party/cargo/wasm-bindgen-macro@0.2.126/LICENSE-APACHE +201 -0
- package/third-party/cargo/wasm-bindgen-macro@0.2.126/LICENSE-MIT +25 -0
- package/third-party/cargo/wasm-bindgen-shared@0.2.126/Cargo.toml +66 -0
- package/third-party/cargo/wasm-bindgen-shared@0.2.126/LICENSE-APACHE +201 -0
- package/third-party/cargo/wasm-bindgen-shared@0.2.126/LICENSE-MIT +25 -0
- package/third-party/cargo/wasm-bindgen@0.2.126/Cargo.toml +122 -0
- package/third-party/cargo/wasm-bindgen@0.2.126/LICENSE-APACHE +201 -0
- package/third-party/cargo/wasm-bindgen@0.2.126/LICENSE-MIT +25 -0
- package/third-party/cargo/wayland-sys@0.31.11/Cargo.toml +81 -0
- package/third-party/cargo/wayland-sys@0.31.11/LICENSE.txt +19 -0
- package/third-party/cargo/web-sys@0.3.103/Cargo.toml +2823 -0
- package/third-party/cargo/web-sys@0.3.103/LICENSE-APACHE +201 -0
- package/third-party/cargo/web-sys@0.3.103/LICENSE-MIT +25 -0
- package/third-party/cargo/weezl@0.1.12/Cargo.toml +120 -0
- package/third-party/cargo/weezl@0.1.12/LICENSE-APACHE +201 -0
- package/third-party/cargo/weezl@0.1.12/LICENSE-MIT +22 -0
- package/third-party/cargo/wgpu-core-deps-apple@30.0.0/Cargo.toml +53 -0
- package/third-party/cargo/wgpu-core-deps-apple@30.0.0/LICENSE.APACHE +176 -0
- package/third-party/cargo/wgpu-core-deps-apple@30.0.0/LICENSE.MIT +21 -0
- package/third-party/cargo/wgpu-core@30.0.0/Cargo.toml +237 -0
- package/third-party/cargo/wgpu-core@30.0.0/LICENSE.APACHE +176 -0
- package/third-party/cargo/wgpu-core@30.0.0/LICENSE.MIT +21 -0
- package/third-party/cargo/wgpu-hal@30.0.0/Cargo.toml +541 -0
- package/third-party/cargo/wgpu-hal@30.0.0/LICENSE.APACHE +176 -0
- package/third-party/cargo/wgpu-hal@30.0.0/LICENSE.MIT +21 -0
- package/third-party/cargo/wgpu-naga-bridge@30.0.0/Cargo.toml +46 -0
- package/third-party/cargo/wgpu-naga-bridge@30.0.0/LICENSE.APACHE +176 -0
- package/third-party/cargo/wgpu-naga-bridge@30.0.0/LICENSE.MIT +21 -0
- package/third-party/cargo/wgpu-types@30.0.0/Cargo.toml +161 -0
- package/third-party/cargo/wgpu-types@30.0.0/LICENSE.APACHE +176 -0
- package/third-party/cargo/wgpu-types@30.0.0/LICENSE.MIT +21 -0
- package/third-party/cargo/wgpu@30.0.0/Cargo.toml +278 -0
- package/third-party/cargo/wgpu@30.0.0/LICENSE.APACHE +176 -0
- package/third-party/cargo/wgpu@30.0.0/LICENSE.MIT +21 -0
- package/third-party/cargo/xmlwriter@0.1.0/Cargo.toml +23 -0
- package/third-party/cargo/xmlwriter@0.1.0/LICENSE +21 -0
- package/third-party/cargo/yazi@0.2.1/Cargo.toml +45 -0
- package/third-party/cargo/yazi@0.2.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/yazi@0.2.1/LICENSE-MIT +19 -0
- package/third-party/cargo/zeno@0.3.3/Cargo.toml +65 -0
- package/third-party/cargo/zeno@0.3.3/LICENSE-APACHE +201 -0
- package/third-party/cargo/zeno@0.3.3/LICENSE-MIT +25 -0
- package/third-party/cargo/zerocopy-derive@0.8.54/Cargo.toml +209 -0
- package/third-party/cargo/zerocopy-derive@0.8.54/LICENSE-APACHE +202 -0
- package/third-party/cargo/zerocopy-derive@0.8.54/LICENSE-BSD +24 -0
- package/third-party/cargo/zerocopy-derive@0.8.54/LICENSE-MIT +26 -0
- package/third-party/cargo/zerocopy@0.8.54/Cargo.toml +419 -0
- package/third-party/cargo/zerocopy@0.8.54/LICENSE-APACHE +202 -0
- package/third-party/cargo/zerocopy@0.8.54/LICENSE-BSD +24 -0
- package/third-party/cargo/zerocopy@0.8.54/LICENSE-MIT +26 -0
- package/third-party/cargo/zmij@1.0.23/Cargo.toml +90 -0
- package/third-party/cargo/zmij@1.0.23/LICENSE-MIT +23 -0
- package/third-party/cargo/zune-core@0.5.1/Cargo.toml +47 -0
- package/third-party/cargo/zune-core@0.5.1/LICENSE-APACHE +201 -0
- package/third-party/cargo/zune-core@0.5.1/LICENSE-MIT +21 -0
- package/third-party/cargo/zune-core@0.5.1/LICENSE-ZLIB +19 -0
- package/third-party/cargo/zune-jpeg@0.5.15/Cargo.toml +67 -0
- package/third-party/cargo/zune-jpeg@0.5.15/LICENSE-APACHE +201 -0
- package/third-party/cargo/zune-jpeg@0.5.15/LICENSE-MIT +21 -0
- package/third-party/cargo/zune-jpeg@0.5.15/LICENSE-ZLIB +19 -0
- package/third-party/inventory.json +3614 -0
- package/third-party/npm/@esbuild__darwin-arm64@0.27.4/esbuild-0.27.4-LICENSE.md +21 -0
- package/third-party/npm/@esbuild__darwin-arm64@0.27.4/go-LICENSE +27 -0
- package/third-party/npm/@esbuild__darwin-arm64@0.27.4/golang-x-sys-c0bba94af5f8-LICENSE +27 -0
- package/third-party/npm/@esbuild__darwin-arm64@0.27.4/package.json +20 -0
- package/third-party/npm/@floating-ui__core@1.7.5/LICENSE +20 -0
- package/third-party/npm/@floating-ui__core@1.7.5/package.json +63 -0
- package/third-party/npm/@floating-ui__dom@1.7.6/LICENSE +20 -0
- package/third-party/npm/@floating-ui__dom@1.7.6/package.json +71 -0
- package/third-party/npm/@floating-ui__react-dom@2.1.8/LICENSE +20 -0
- package/third-party/npm/@floating-ui__react-dom@2.1.8/package.json +76 -0
- package/third-party/npm/@floating-ui__utils@0.2.11/LICENSE +20 -0
- package/third-party/npm/@floating-ui__utils@0.2.11/package.json +70 -0
- package/third-party/npm/@radix-ui__primitive@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__primitive@1.1.7/package.json +79 -0
- package/third-party/npm/@radix-ui__react-arrow@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-arrow@1.1.15/package.json +68 -0
- package/third-party/npm/@radix-ui__react-collection@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-collection@1.1.15/package.json +71 -0
- package/third-party/npm/@radix-ui__react-compose-refs@1.1.5/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-compose-refs@1.1.5/package.json +60 -0
- package/third-party/npm/@radix-ui__react-context-menu@2.3.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-context-menu@2.3.7/package.json +73 -0
- package/third-party/npm/@radix-ui__react-context@1.2.2/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-context@1.2.2/package.json +60 -0
- package/third-party/npm/@radix-ui__react-direction@1.1.4/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-direction@1.1.4/package.json +60 -0
- package/third-party/npm/@radix-ui__react-dismissable-layer@1.1.19/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dismissable-layer@1.1.19/package.json +74 -0
- package/third-party/npm/@radix-ui__react-focus-guards@1.1.6/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-focus-guards@1.1.6/package.json +60 -0
- package/third-party/npm/@radix-ui__react-focus-scope@1.1.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-focus-scope@1.1.16/package.json +71 -0
- package/third-party/npm/@radix-ui__react-id@1.1.4/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-id@1.1.4/package.json +63 -0
- package/third-party/npm/@radix-ui__react-menu@2.1.24/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-menu@2.1.24/package.json +86 -0
- package/third-party/npm/@radix-ui__react-popper@1.3.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-popper@1.3.7/package.json +78 -0
- package/third-party/npm/@radix-ui__react-portal@1.1.17/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-portal@1.1.17/package.json +69 -0
- package/third-party/npm/@radix-ui__react-presence@1.1.10/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-presence@1.1.10/package.json +68 -0
- package/third-party/npm/@radix-ui__react-primitive@2.1.10/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-primitive@2.1.10/package.json +68 -0
- package/third-party/npm/@radix-ui__react-roving-focus@1.1.19/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-roving-focus@1.1.19/package.json +78 -0
- package/third-party/npm/@radix-ui__react-slot@1.3.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-slot@1.3.3/package.json +63 -0
- package/third-party/npm/@radix-ui__react-use-callback-ref@1.1.4/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-callback-ref@1.1.4/package.json +60 -0
- package/third-party/npm/@radix-ui__react-use-controllable-state@1.2.6/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-controllable-state@1.2.6/package.json +65 -0
- package/third-party/npm/@radix-ui__react-use-effect-event@0.0.5/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-effect-event@0.0.5/package.json +65 -0
- package/third-party/npm/@radix-ui__react-use-is-hydrated@0.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-is-hydrated@0.1.3/package.json +61 -0
- package/third-party/npm/@radix-ui__react-use-layout-effect@1.1.4/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-layout-effect@1.1.4/package.json +60 -0
- package/third-party/npm/@radix-ui__react-use-size@1.1.4/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-size@1.1.4/package.json +63 -0
- package/third-party/npm/aria-hidden@1.2.6/LICENSE +21 -0
- package/third-party/npm/aria-hidden@1.2.6/package.json +71 -0
- package/third-party/npm/detect-node-es@1.1.0/LICENSE +21 -0
- package/third-party/npm/detect-node-es@1.1.0/package.json +29 -0
- package/third-party/npm/esbuild@0.27.4/LICENSE.md +21 -0
- package/third-party/npm/esbuild@0.27.4/package.json +49 -0
- package/third-party/npm/fflate@0.8.3/LICENSE +21 -0
- package/third-party/npm/fflate@0.8.3/package.json +128 -0
- package/third-party/npm/get-nonce@1.0.1/LICENSE +21 -0
- package/third-party/npm/get-nonce@1.0.1/package.json +68 -0
- package/third-party/npm/mediabunny@1.52.2/LICENSE +373 -0
- package/third-party/npm/mediabunny@1.52.2/package.json +136 -0
- package/third-party/npm/mediabunny@1.52.2/source/LICENSE +373 -0
- package/third-party/npm/mediabunny@1.52.2/source/package.json +136 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/adts/adts-demuxer.ts +400 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/adts/adts-muxer.ts +124 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/adts/adts-reader.ts +85 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/aes.ts +307 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/codec-data.ts +3178 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/codec.ts +1154 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/conversion.ts +2042 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/custom-coder.ts +212 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/decode.ts +263 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/demuxer.ts +43 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/encode.ts +1412 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/flac/flac-demuxer.ts +813 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/flac/flac-misc.ts +165 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/flac/flac-muxer.ts +350 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/hls/hls-demuxer.ts +995 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/hls/hls-misc.ts +81 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/hls/hls-muxer.ts +1529 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/hls/hls-segmented-input.ts +772 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/id3.ts +972 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/index.ts +319 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/input-format.ts +819 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/input-track.ts +1141 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/input.ts +571 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/isobmff/isobmff-boxes.ts +1911 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/isobmff/isobmff-demuxer.ts +3953 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/isobmff/isobmff-misc.ts +90 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/isobmff/isobmff-muxer.ts +1595 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/isobmff/isobmff-reader.ts +91 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/logging.ts +120 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/matroska/ebml.ts +770 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/matroska/matroska-demuxer.ts +2598 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/matroska/matroska-misc.ts +29 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/matroska/matroska-muxer.ts +1360 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/media-sink.ts +2589 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/media-source.ts +3041 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/metadata.ts +335 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/misc.ts +1336 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/mp3/mp3-demuxer.ts +469 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/mp3/mp3-muxer.ts +169 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/mp3/mp3-reader.ts +61 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/mp3/mp3-writer.ts +121 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/mpeg-ts/mpeg-ts-demuxer.ts +2519 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/mpeg-ts/mpeg-ts-misc.ts +32 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/mpeg-ts/mpeg-ts-muxer.ts +842 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/muxer.ts +80 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/node.ts +11 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/ogg/ogg-demuxer.ts +1087 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/ogg/ogg-misc.ts +116 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/ogg/ogg-muxer.ts +527 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/ogg/ogg-reader.ts +93 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/output-format.ts +1425 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/output.ts +940 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/packet.ts +287 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/pcm.ts +112 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/reader.ts +391 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/resample.ts +287 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/sample.ts +3231 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/segmented-input.ts +575 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/source.ts +2588 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/subtitles.ts +131 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/target.ts +871 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/tsconfig.json +16 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/wave/riff-writer.ts +36 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/wave/wave-demuxer.ts +583 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/wave/wave-muxer.ts +375 -0
- package/third-party/npm/mediabunny@1.52.2/source/src/writer.ts +128 -0
- package/third-party/npm/react-dom@19.3.0/LICENSE +21 -0
- package/third-party/npm/react-dom@19.3.0/package.json +117 -0
- package/third-party/npm/react-remove-scroll-bar@2.3.8/package.json +85 -0
- package/third-party/npm/react-remove-scroll-bar@2.3.8/react-remove-scroll-bar-LICENSE +21 -0
- package/third-party/npm/react-remove-scroll@2.7.2/LICENSE +21 -0
- package/third-party/npm/react-remove-scroll@2.7.2/package.json +98 -0
- package/third-party/npm/react-style-singleton@2.2.3/LICENSE +21 -0
- package/third-party/npm/react-style-singleton@2.2.3/package.json +90 -0
- package/third-party/npm/react@19.3.0/LICENSE +21 -0
- package/third-party/npm/react@19.3.0/package.json +51 -0
- package/third-party/npm/scheduler@0.28.0/LICENSE +21 -0
- package/third-party/npm/scheduler@0.28.0/package.json +27 -0
- package/third-party/npm/tslib@2.8.1/LICENSE.txt +12 -0
- package/third-party/npm/tslib@2.8.1/package.json +47 -0
- package/third-party/npm/typescript@5.9.3/LICENSE.txt +55 -0
- package/third-party/npm/typescript@5.9.3/ThirdPartyNoticeText.txt +193 -0
- package/third-party/npm/typescript@5.9.3/package.json +120 -0
- package/third-party/npm/use-callback-ref@1.3.3/LICENSE +21 -0
- package/third-party/npm/use-callback-ref@1.3.3/package.json +85 -0
- package/third-party/npm/use-sidecar@1.1.3/LICENSE +21 -0
- package/third-party/npm/use-sidecar@1.1.3/package.json +93 -0
- package/third-party/packages/render-engine/assets/OFL.txt +93 -0
- package/third-party/packages/studio/public/fonts/OFL.txt +93 -0
- package/third-party/packages/studio/vendor/osmo-icons/LICENSE +21 -0
- package/third-party/packages/studio/vendor/shadcn/LICENSE +21 -0
- package/third-party/repository-notices.md +41 -0
- package/third-party/rust-standard-library/COPYRIGHT-library.html +5556 -0
- package/third-party/rust-standard-library/licenses/Apache-2.0.txt +73 -0
- package/third-party/rust-standard-library/licenses/BSD-2-Clause.txt +9 -0
- package/third-party/rust-standard-library/licenses/CC-BY-SA-4.0.txt +427 -0
- package/third-party/rust-standard-library/licenses/GCC-exception-3.1.txt +30 -0
- package/third-party/rust-standard-library/licenses/GPL-2.0-only.txt +133 -0
- package/third-party/rust-standard-library/licenses/GPL-3.0-or-later.txt +202 -0
- package/third-party/rust-standard-library/licenses/ISC.txt +7 -0
- package/third-party/rust-standard-library/licenses/LLVM-exception.txt +15 -0
- package/third-party/rust-standard-library/licenses/MIT.txt +9 -0
- package/third-party/rust-standard-library/licenses/NCSA.txt +29 -0
- package/third-party/rust-standard-library/licenses/OFL-1.1.txt +43 -0
- package/third-party/rust-standard-library/licenses/Unicode-3.0.txt +39 -0
- package/third-party/rust-standard-library/version.txt +1 -0
- package/vendor/osmo-lite +0 -0
- package/vendor/osmo-scene-0.1.0.tgz +0 -0
- package/vendor/platform.json +1 -0
|
@@ -0,0 +1,3041 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026-present, Vanilagy and contributors
|
|
3
|
+
*
|
|
4
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { buildAacAudioSpecificConfig, parseAacAudioSpecificConfig } from '../shared/aac-misc';
|
|
10
|
+
import {
|
|
11
|
+
AUDIO_CODECS,
|
|
12
|
+
AudioCodec,
|
|
13
|
+
MediaCodec,
|
|
14
|
+
parsePcmCodec,
|
|
15
|
+
PCM_AUDIO_CODECS,
|
|
16
|
+
PcmAudioCodec,
|
|
17
|
+
SUBTITLE_CODECS,
|
|
18
|
+
SubtitleCodec,
|
|
19
|
+
VIDEO_CODECS,
|
|
20
|
+
VideoCodec,
|
|
21
|
+
} from './codec';
|
|
22
|
+
import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output';
|
|
23
|
+
import {
|
|
24
|
+
assert,
|
|
25
|
+
assertNever,
|
|
26
|
+
binarySearchLessOrEqual,
|
|
27
|
+
CallSerializer,
|
|
28
|
+
clamp,
|
|
29
|
+
clearIntervalUnthrottled,
|
|
30
|
+
floorToDivisor,
|
|
31
|
+
last,
|
|
32
|
+
promiseWithResolvers,
|
|
33
|
+
roundToDivisor,
|
|
34
|
+
setInt24,
|
|
35
|
+
setIntervalUnthrottled,
|
|
36
|
+
setUint24,
|
|
37
|
+
toUint8Array,
|
|
38
|
+
UnthrottledTimerHandle,
|
|
39
|
+
wait,
|
|
40
|
+
} from './misc';
|
|
41
|
+
import { Muxer } from './muxer';
|
|
42
|
+
import { SubtitleParser } from './subtitles';
|
|
43
|
+
import { toAlaw, toUlaw } from './pcm';
|
|
44
|
+
import {
|
|
45
|
+
CustomVideoEncoder,
|
|
46
|
+
CustomAudioEncoder,
|
|
47
|
+
customVideoEncoders,
|
|
48
|
+
customAudioEncoders,
|
|
49
|
+
} from './custom-coder';
|
|
50
|
+
import { EncodedPacket, EncodedPacketSideData, PacketType } from './packet';
|
|
51
|
+
import {
|
|
52
|
+
AudioSample,
|
|
53
|
+
audioSampleToInterleavedFormat,
|
|
54
|
+
toInterleavedAudioFormat,
|
|
55
|
+
VideoSample,
|
|
56
|
+
VideoSamplePixelFormat,
|
|
57
|
+
} from './sample';
|
|
58
|
+
import {
|
|
59
|
+
AudioEncodingConfig,
|
|
60
|
+
buildAudioEncoderConfig,
|
|
61
|
+
buildQuantizerEncodeOptions,
|
|
62
|
+
buildVideoEncoderConfigs,
|
|
63
|
+
resolveQuality,
|
|
64
|
+
validateAudioEncodingConfig,
|
|
65
|
+
validateVideoEncodingConfig,
|
|
66
|
+
VideoEncoderConfigCandidate,
|
|
67
|
+
VideoEncodingConfig,
|
|
68
|
+
} from './encode';
|
|
69
|
+
import { AudioResampler } from './resample';
|
|
70
|
+
import { determineVideoPacketType } from './codec-data';
|
|
71
|
+
import { Logging } from './logging';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Base class for media sources. Media sources are used to add media samples to an output file.
|
|
75
|
+
* @group Media sources
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
export abstract class MediaSource {
|
|
79
|
+
/** @internal */
|
|
80
|
+
abstract readonly _codec: MediaCodec;
|
|
81
|
+
/** @internal */
|
|
82
|
+
_connectedTrack: OutputTrack | null = null;
|
|
83
|
+
/** @internal */
|
|
84
|
+
_closingPromise: Promise<void> | null = null;
|
|
85
|
+
/** @internal */
|
|
86
|
+
_closed = false;
|
|
87
|
+
|
|
88
|
+
/** @internal */
|
|
89
|
+
_ensureValidAdd() {
|
|
90
|
+
if (!this._connectedTrack) {
|
|
91
|
+
throw new Error('Source is not connected to an output track.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (this._connectedTrack.output.state === 'canceled') {
|
|
95
|
+
throw new Error('Output has been canceled.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (this._connectedTrack.output.state === 'finalizing' || this._connectedTrack.output.state === 'finalized') {
|
|
99
|
+
throw new Error('Output has been finalized.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (this._connectedTrack.output.state === 'pending') {
|
|
103
|
+
throw new Error('Output has not started.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this._closed) {
|
|
107
|
+
throw new Error('Source is closed.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @internal */
|
|
112
|
+
async _start() {}
|
|
113
|
+
/** @internal */
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
115
|
+
async _flushAndClose(forceClose: boolean) {}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Closes this source. This prevents future samples from being added and signals to the output file that no further
|
|
119
|
+
* samples will come in for this track. Calling `.close()` is optional but recommended after adding the
|
|
120
|
+
* last sample - for improved performance and reduced memory usage.
|
|
121
|
+
*/
|
|
122
|
+
close() {
|
|
123
|
+
if (this._closingPromise) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const connectedTrack = this._connectedTrack;
|
|
128
|
+
|
|
129
|
+
if (!connectedTrack) {
|
|
130
|
+
throw new Error('Cannot call close without connecting the source to an output track.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (connectedTrack.output.state === 'pending') {
|
|
134
|
+
throw new Error('Cannot call close before output has been started.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this._closingPromise = (async () => {
|
|
138
|
+
await this._flushAndClose(false);
|
|
139
|
+
|
|
140
|
+
this._closed = true;
|
|
141
|
+
|
|
142
|
+
if (connectedTrack.output.state === 'finalizing' || connectedTrack.output.state === 'finalized') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
connectedTrack.output._muxer.onTrackClose(connectedTrack);
|
|
147
|
+
})();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @internal */
|
|
151
|
+
async _flushOrWaitForOngoingClose(forceClose: boolean) {
|
|
152
|
+
return this._closingPromise ??= (async () => {
|
|
153
|
+
await this._flushAndClose(forceClose);
|
|
154
|
+
this._closed = true;
|
|
155
|
+
})();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Base class for video sources - sources for video tracks.
|
|
161
|
+
* @group Media sources
|
|
162
|
+
* @public
|
|
163
|
+
*/
|
|
164
|
+
export abstract class VideoSource extends MediaSource {
|
|
165
|
+
/** @internal */
|
|
166
|
+
override _connectedTrack: OutputVideoTrack | null = null;
|
|
167
|
+
/** @internal */
|
|
168
|
+
override readonly _codec: VideoCodec;
|
|
169
|
+
|
|
170
|
+
/** Internal constructor. */
|
|
171
|
+
constructor(codec: VideoCodec) {
|
|
172
|
+
super();
|
|
173
|
+
|
|
174
|
+
if (!VIDEO_CODECS.includes(codec)) {
|
|
175
|
+
throw new TypeError(`Invalid video codec '${codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this._codec = codec;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const maybeEnsureIsKeyPacket = (track: OutputVideoTrack, packet: EncodedPacket) => {
|
|
183
|
+
if (track.metadata.hasOnlyKeyPackets && packet.type !== 'key') {
|
|
184
|
+
throw new Error('Cannot add non-key packets to a hasOnlyKeyPackets video track.');
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The most basic video source; can be used to directly pipe encoded packets into the output file.
|
|
190
|
+
* @group Media sources
|
|
191
|
+
* @public
|
|
192
|
+
*/
|
|
193
|
+
export class EncodedVideoPacketSource extends VideoSource {
|
|
194
|
+
/** Creates a new {@link EncodedVideoPacketSource} whose packets are encoded using `codec`. */
|
|
195
|
+
constructor(codec: VideoCodec) {
|
|
196
|
+
super(codec);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Adds an encoded packet to the output video track. Packets must be added in *decode order*, while a packet's
|
|
201
|
+
* timestamp must be its *presentation timestamp*. B-frames are handled automatically.
|
|
202
|
+
*
|
|
203
|
+
* @param meta - Additional metadata from the encoder. You should pass this for the first call, including a valid
|
|
204
|
+
* decoder config.
|
|
205
|
+
*
|
|
206
|
+
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
|
|
207
|
+
* to respect writer and encoder backpressure.
|
|
208
|
+
*/
|
|
209
|
+
add(packet: EncodedPacket, meta?: EncodedVideoChunkMetadata) {
|
|
210
|
+
if (!(packet instanceof EncodedPacket)) {
|
|
211
|
+
throw new TypeError('packet must be an EncodedPacket.');
|
|
212
|
+
}
|
|
213
|
+
if (packet.isMetadataOnly) {
|
|
214
|
+
throw new TypeError('Metadata-only packets cannot be added.');
|
|
215
|
+
}
|
|
216
|
+
if (meta !== undefined && (!meta || typeof meta !== 'object')) {
|
|
217
|
+
throw new TypeError('meta, when provided, must be an object.');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this._ensureValidAdd();
|
|
221
|
+
|
|
222
|
+
maybeEnsureIsKeyPacket(this._connectedTrack!, packet);
|
|
223
|
+
return this._connectedTrack!.output._muxer.addEncodedVideoPacket(this._connectedTrack!, packet, meta);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class VideoEncoderWrapper {
|
|
228
|
+
private ensureEncoderPromise: Promise<void> | null = null;
|
|
229
|
+
private encoderInitialized = false;
|
|
230
|
+
private encoder: VideoEncoder | null = null;
|
|
231
|
+
private muxer: Muxer | null = null;
|
|
232
|
+
private lastMultipleOfKeyFrameInterval = -1;
|
|
233
|
+
private emittedEncoderPackets = 0;
|
|
234
|
+
|
|
235
|
+
// Tracks the input dimensions of the first frame
|
|
236
|
+
private codedWidth: number | null = null;
|
|
237
|
+
private codedHeight: number | null = null;
|
|
238
|
+
// Tracks the output dimensions of the first frame (used to lock dimensions for fill/contain/cover)
|
|
239
|
+
private outputWidth: number | null = null;
|
|
240
|
+
private outputHeight: number | null = null;
|
|
241
|
+
|
|
242
|
+
// Frame rate normalization state
|
|
243
|
+
private frameRateLastSample: VideoSample | null = null;
|
|
244
|
+
private frameRateLastTimestamp: number | null = null;
|
|
245
|
+
private frameRateLastEndTimestamp: number | null = null;
|
|
246
|
+
|
|
247
|
+
// VideoEncoder converts everything to microseconds, so we need to do some bookkeeping to restore the original
|
|
248
|
+
// timing information
|
|
249
|
+
private preciseTimings: {
|
|
250
|
+
microsecondTimestamp: number;
|
|
251
|
+
timestamp: number;
|
|
252
|
+
duration: number;
|
|
253
|
+
timestampIsValid: boolean;
|
|
254
|
+
durationIsValid: boolean;
|
|
255
|
+
}[] = [];
|
|
256
|
+
|
|
257
|
+
private customEncoder: CustomVideoEncoder | null = null;
|
|
258
|
+
private customEncoderCallSerializer = new CallSerializer();
|
|
259
|
+
private customEncoderQueueSize = 0;
|
|
260
|
+
|
|
261
|
+
// Set when the encoder uses quantizer-based rate control; carries the quantizer value applied to each frame
|
|
262
|
+
private defaultEncodeOptions: VideoEncoderEncodeOptions = {};
|
|
263
|
+
|
|
264
|
+
// Alpha stuff
|
|
265
|
+
private alphaEncoder: VideoEncoder | null = null;
|
|
266
|
+
private splitter: ColorAlphaSplitter | null = null;
|
|
267
|
+
private splitterCreationFailed = false;
|
|
268
|
+
private alphaFrameQueue: (VideoFrame | null)[] = [];
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Encoders typically throw their errors "out of band", meaning asynchronously in some other execution context.
|
|
272
|
+
* However, we want to surface these errors to the user within the normal control flow, so they don't go uncaught.
|
|
273
|
+
* So, we keep track of the encoder error and throw it as soon as we get the chance.
|
|
274
|
+
*/
|
|
275
|
+
private error: unknown = null;
|
|
276
|
+
private errorSet = false;
|
|
277
|
+
|
|
278
|
+
private setError(error: unknown) {
|
|
279
|
+
if (!this.errorSet) {
|
|
280
|
+
this.error = error;
|
|
281
|
+
this.errorSet = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private lastMuxerPromise: Promise<void> = Promise.resolve();
|
|
286
|
+
private closed = false;
|
|
287
|
+
|
|
288
|
+
constructor(private source: VideoSource, private encodingConfig: VideoEncodingConfig) {}
|
|
289
|
+
|
|
290
|
+
async add(videoSample: VideoSample, shouldClose: boolean, encodeOptions?: VideoEncoderEncodeOptions) {
|
|
291
|
+
const originalSample = videoSample;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
this.checkForEncoderError();
|
|
295
|
+
this.source._ensureValidAdd();
|
|
296
|
+
|
|
297
|
+
const config = this.encodingConfig;
|
|
298
|
+
const sizeChangeBehavior = config.sizeChangeBehavior ?? 'deny';
|
|
299
|
+
let isSizeChange = false;
|
|
300
|
+
|
|
301
|
+
// Ensure video sample size remains constant or handle the change
|
|
302
|
+
if (this.codedWidth !== null && this.codedHeight !== null) {
|
|
303
|
+
if (videoSample.codedWidth !== this.codedWidth || videoSample.codedHeight !== this.codedHeight) {
|
|
304
|
+
isSizeChange = true;
|
|
305
|
+
if (sizeChangeBehavior === 'deny') {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Video sample size must remain constant. Expected ${this.codedWidth}x${this.codedHeight},`
|
|
308
|
+
+ ` got ${videoSample.codedWidth}x${videoSample.codedHeight}. To allow the sample size to`
|
|
309
|
+
+ ` change over time, set \`sizeChangeBehavior\` to a value other than 'deny' in the`
|
|
310
|
+
+ ` encoding options.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
this.codedWidth = videoSample.codedWidth;
|
|
316
|
+
this.codedHeight = videoSample.codedHeight;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Determine if we need to apply transformations via canvas
|
|
320
|
+
const hasTransformConfig = config.transform?.width !== undefined
|
|
321
|
+
|| config.transform?.height !== undefined
|
|
322
|
+
|| config.transform?.rotate !== undefined
|
|
323
|
+
|| config.transform?.crop !== undefined
|
|
324
|
+
|| config.transform?.force === true;
|
|
325
|
+
const needsTransform = hasTransformConfig || (isSizeChange && sizeChangeBehavior !== 'passThrough');
|
|
326
|
+
|
|
327
|
+
if (needsTransform) {
|
|
328
|
+
let targetWidth = config.transform?.width;
|
|
329
|
+
let targetHeight = config.transform?.height;
|
|
330
|
+
let appliedFit: 'fill' | 'contain' | 'cover' = config.transform?.fit ?? 'fill';
|
|
331
|
+
|
|
332
|
+
// If the size changed and behavior is fill/contain/cover, lock to the original output dimensions
|
|
333
|
+
if (isSizeChange && sizeChangeBehavior !== 'passThrough') {
|
|
334
|
+
assert(this.outputWidth);
|
|
335
|
+
assert(this.outputHeight);
|
|
336
|
+
assert(sizeChangeBehavior !== 'deny');
|
|
337
|
+
|
|
338
|
+
targetWidth = this.outputWidth!;
|
|
339
|
+
targetHeight = this.outputHeight!;
|
|
340
|
+
appliedFit = sizeChangeBehavior;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const transformed = await videoSample.transform({
|
|
344
|
+
width: targetWidth,
|
|
345
|
+
height: targetHeight,
|
|
346
|
+
roundDimensionsTo: 2,
|
|
347
|
+
crop: config.transform?.crop,
|
|
348
|
+
rotate: config.transform?.rotate,
|
|
349
|
+
fit: appliedFit,
|
|
350
|
+
alpha: config.alpha,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Save the output dimensions of the first frame
|
|
354
|
+
if (this.outputWidth === null || this.outputHeight === null) {
|
|
355
|
+
this.outputWidth = transformed.displayWidth;
|
|
356
|
+
this.outputHeight = transformed.displayHeight;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (shouldClose) {
|
|
360
|
+
videoSample.close();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
videoSample = transformed;
|
|
364
|
+
shouldClose = true;
|
|
365
|
+
} else {
|
|
366
|
+
// If no canvas is needed, we still need to record the output dimensions for the first frame
|
|
367
|
+
if (this.outputWidth === null || this.outputHeight === null) {
|
|
368
|
+
this.outputWidth = videoSample.codedWidth;
|
|
369
|
+
this.outputHeight = videoSample.codedHeight;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const frameRate = config.transform?.frameRate;
|
|
374
|
+
if (frameRate !== undefined) {
|
|
375
|
+
// Apply frame rate normalization
|
|
376
|
+
const originalEndTimestamp = videoSample.timestamp + videoSample.duration;
|
|
377
|
+
const alignedTimestamp = floorToDivisor(videoSample.timestamp, frameRate);
|
|
378
|
+
|
|
379
|
+
if (this.frameRateLastSample !== null) {
|
|
380
|
+
if (alignedTimestamp <= this.frameRateLastTimestamp!) {
|
|
381
|
+
// Same frame rate slot, replace stored sample with the newer one
|
|
382
|
+
this.frameRateLastSample.close();
|
|
383
|
+
this.frameRateLastSample = videoSample.clone();
|
|
384
|
+
this.frameRateLastEndTimestamp = originalEndTimestamp;
|
|
385
|
+
return;
|
|
386
|
+
} else {
|
|
387
|
+
// Pad the gap by repeating the previous frame
|
|
388
|
+
await this.padFrameRate(alignedTimestamp, encodeOptions);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Clone if the sample is still the user's, to avoid mutating externally-owned data
|
|
393
|
+
if (videoSample === originalSample) {
|
|
394
|
+
videoSample = videoSample.clone();
|
|
395
|
+
shouldClose = true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
videoSample.setTimestamp(alignedTimestamp);
|
|
399
|
+
videoSample.setDuration(1 / frameRate);
|
|
400
|
+
|
|
401
|
+
this.frameRateLastSample?.close();
|
|
402
|
+
this.frameRateLastSample = videoSample.clone();
|
|
403
|
+
this.frameRateLastTimestamp = alignedTimestamp;
|
|
404
|
+
this.frameRateLastEndTimestamp = originalEndTimestamp;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await this.processAndEncode(videoSample, encodeOptions);
|
|
408
|
+
} finally {
|
|
409
|
+
if (shouldClose) {
|
|
410
|
+
videoSample.close();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Runs the process function (if any) and encodes the resulting samples.
|
|
417
|
+
*/
|
|
418
|
+
private async processAndEncode(
|
|
419
|
+
videoSample: VideoSample,
|
|
420
|
+
encodeOptions?: VideoEncoderEncodeOptions,
|
|
421
|
+
) {
|
|
422
|
+
const config = this.encodingConfig;
|
|
423
|
+
let samplesToEncode: VideoSample[];
|
|
424
|
+
|
|
425
|
+
// Apply the user-defined process function, if any
|
|
426
|
+
if (config.transform?.process) {
|
|
427
|
+
let processed = config.transform.process(videoSample);
|
|
428
|
+
if (processed instanceof Promise) {
|
|
429
|
+
processed = await processed;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (processed === null) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!Array.isArray(processed)) {
|
|
437
|
+
processed = [processed];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const mappedSamples: VideoSample[] = [];
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
for (const x of processed) {
|
|
444
|
+
if (x instanceof VideoSample) {
|
|
445
|
+
mappedSamples.push(x);
|
|
446
|
+
} else if (typeof VideoFrame !== 'undefined' && x instanceof VideoFrame) {
|
|
447
|
+
mappedSamples.push(new VideoSample(x));
|
|
448
|
+
} else {
|
|
449
|
+
// Calling the VideoSample constructor here will automatically handle input validation for us
|
|
450
|
+
// (it throws for any non-legal argument).
|
|
451
|
+
mappedSamples.push(new VideoSample(x as CanvasImageSource, {
|
|
452
|
+
timestamp: videoSample.timestamp,
|
|
453
|
+
duration: videoSample.duration,
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
// One of the returned elements was invalid; close everything closable so no resource is leaked
|
|
459
|
+
for (const sample of mappedSamples) {
|
|
460
|
+
if (sample !== videoSample) {
|
|
461
|
+
sample.close();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const x of processed) {
|
|
465
|
+
if (x instanceof VideoSample && x !== videoSample) {
|
|
466
|
+
x.close();
|
|
467
|
+
} else if (typeof VideoFrame !== 'undefined' && x instanceof VideoFrame) {
|
|
468
|
+
x.close();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
samplesToEncode = mappedSamples;
|
|
476
|
+
} else {
|
|
477
|
+
samplesToEncode = [videoSample];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
for (const sampleToEncode of samplesToEncode) {
|
|
482
|
+
if (!this.encoderInitialized) {
|
|
483
|
+
if (!this.ensureEncoderPromise) {
|
|
484
|
+
this.ensureEncoder(sampleToEncode);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// No, this "if" statement is not useless. Sometimes, the above call to
|
|
488
|
+
// `ensureEncoder` might have synchronously completed and the encoder is
|
|
489
|
+
// already initialized. In this case, we don't need to await the promise
|
|
490
|
+
// anymore. This also fixes nasty async race condition bugs when multiple
|
|
491
|
+
// code paths are calling this method: It's important that the call that
|
|
492
|
+
// initialized the encoder go through this code first.
|
|
493
|
+
if (!this.encoderInitialized) {
|
|
494
|
+
await this.ensureEncoderPromise;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
assert(this.encoderInitialized);
|
|
498
|
+
|
|
499
|
+
if (this.closed) {
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const keyFrameInterval = this.encodingConfig.keyFrameInterval ?? 2;
|
|
504
|
+
const multipleOfKeyFrameInterval = Math.floor(sampleToEncode.timestamp / keyFrameInterval);
|
|
505
|
+
|
|
506
|
+
const mergedEncodeOptions = {
|
|
507
|
+
...this.defaultEncodeOptions,
|
|
508
|
+
...sampleToEncode.encodeOptions,
|
|
509
|
+
...encodeOptions,
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const finalEncodeOptions = {
|
|
513
|
+
...mergedEncodeOptions,
|
|
514
|
+
keyFrame: mergedEncodeOptions.keyFrame !== undefined
|
|
515
|
+
? mergedEncodeOptions.keyFrame
|
|
516
|
+
// Ensure a key frame every keyFrameInterval seconds. It is important that all video tracks
|
|
517
|
+
// follow the same "key frame" rhythm, because aligned key frames are required to start new
|
|
518
|
+
// fragments in ISOBMFF or clusters in Matroska (or at least desirable).
|
|
519
|
+
: keyFrameInterval === 0
|
|
520
|
+
|| multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval,
|
|
521
|
+
};
|
|
522
|
+
this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval;
|
|
523
|
+
|
|
524
|
+
this.encodingConfig.onEncodedSample?.(sampleToEncode);
|
|
525
|
+
|
|
526
|
+
if (this.customEncoder) {
|
|
527
|
+
this.customEncoderQueueSize++;
|
|
528
|
+
|
|
529
|
+
// We clone the sample so it cannot be closed on us from the outside before it reaches the encoder
|
|
530
|
+
const clonedSample = sampleToEncode.clone();
|
|
531
|
+
|
|
532
|
+
const promise = this.customEncoderCallSerializer
|
|
533
|
+
.call(() => this.customEncoder!.encode(clonedSample, finalEncodeOptions))
|
|
534
|
+
.catch((error: unknown) => this.setError(error))
|
|
535
|
+
.finally(() => {
|
|
536
|
+
this.customEncoderQueueSize--;
|
|
537
|
+
clonedSample.close();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (this.customEncoderQueueSize >= 4) {
|
|
541
|
+
await promise;
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
assert(this.encoder);
|
|
545
|
+
|
|
546
|
+
const videoFrame = sampleToEncode.toVideoFrame();
|
|
547
|
+
|
|
548
|
+
const preciseTimingIndex = binarySearchLessOrEqual(
|
|
549
|
+
this.preciseTimings,
|
|
550
|
+
videoFrame.timestamp,
|
|
551
|
+
x => x.microsecondTimestamp,
|
|
552
|
+
);
|
|
553
|
+
const existingEntry = preciseTimingIndex !== -1
|
|
554
|
+
? this.preciseTimings[preciseTimingIndex]
|
|
555
|
+
: null;
|
|
556
|
+
if (existingEntry && existingEntry.microsecondTimestamp === videoFrame.timestamp) {
|
|
557
|
+
if (existingEntry.timestamp !== sampleToEncode.timestamp) {
|
|
558
|
+
// Mapping isn't unique, can't use the timestamp
|
|
559
|
+
existingEntry.timestampIsValid = false;
|
|
560
|
+
}
|
|
561
|
+
if (existingEntry.duration !== sampleToEncode.duration) {
|
|
562
|
+
// Mapping isn't unique, can't use the duration
|
|
563
|
+
existingEntry.durationIsValid = false;
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
this.preciseTimings.splice(preciseTimingIndex + 1, 0, {
|
|
567
|
+
microsecondTimestamp: videoFrame.timestamp,
|
|
568
|
+
timestamp: sampleToEncode.timestamp,
|
|
569
|
+
duration: sampleToEncode.duration,
|
|
570
|
+
timestampIsValid: true,
|
|
571
|
+
durationIsValid: true,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Make sure it doesn't grow indefinitely
|
|
575
|
+
if (this.preciseTimings.length > 128) {
|
|
576
|
+
this.preciseTimings.shift();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (!this.alphaEncoder) {
|
|
581
|
+
// No alpha encoder, simple case
|
|
582
|
+
try {
|
|
583
|
+
this.encoder.encode(videoFrame, finalEncodeOptions);
|
|
584
|
+
} finally {
|
|
585
|
+
videoFrame.close();
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
// We're expected to encode alpha as well
|
|
589
|
+
const frameDefinitelyHasNoAlpha = !!videoFrame.format && !videoFrame.format.includes('A');
|
|
590
|
+
|
|
591
|
+
if (frameDefinitelyHasNoAlpha || this.splitterCreationFailed) {
|
|
592
|
+
this.alphaFrameQueue.push(null);
|
|
593
|
+
try {
|
|
594
|
+
this.encoder.encode(videoFrame, finalEncodeOptions);
|
|
595
|
+
} finally {
|
|
596
|
+
videoFrame.close();
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
if (!this.splitter) {
|
|
600
|
+
this.splitter = new ColorAlphaSplitter();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// The splitter takes ownership, so no need to close the frames ourselves
|
|
604
|
+
const { colorFrame, alphaFrame } = await this.splitter.split(videoFrame);
|
|
605
|
+
|
|
606
|
+
this.alphaFrameQueue.push(alphaFrame);
|
|
607
|
+
try {
|
|
608
|
+
this.encoder.encode(colorFrame, finalEncodeOptions);
|
|
609
|
+
} finally {
|
|
610
|
+
colorFrame.close();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// We need to do this after sending the frame to the encoder as the frame otherwise might be closed
|
|
616
|
+
if (this.encoder.encodeQueueSize >= 4) {
|
|
617
|
+
await new Promise(resolve =>
|
|
618
|
+
this.encoder!.addEventListener('dequeue', resolve, { once: true }),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
await this.lastMuxerPromise; // Allow the writer to apply backpressure
|
|
624
|
+
}
|
|
625
|
+
} finally {
|
|
626
|
+
for (const sample of samplesToEncode) {
|
|
627
|
+
if (sample !== videoSample) {
|
|
628
|
+
sample.close();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Repeats the last frame rate sample to fill the gap up to the given timestamp. */
|
|
635
|
+
private async padFrameRate(until: number, encodeOptions?: VideoEncoderEncodeOptions) {
|
|
636
|
+
const frameRate = this.encodingConfig.transform!.frameRate!;
|
|
637
|
+
assert(this.frameRateLastSample);
|
|
638
|
+
|
|
639
|
+
const frameDifference = Math.round((until - this.frameRateLastTimestamp!) * frameRate);
|
|
640
|
+
|
|
641
|
+
for (let i = 1; i < frameDifference; i++) {
|
|
642
|
+
using sample = this.frameRateLastSample.clone();
|
|
643
|
+
sample.setTimestamp(this.frameRateLastTimestamp! + i / frameRate);
|
|
644
|
+
sample.setDuration(1 / frameRate);
|
|
645
|
+
await this.processAndEncode(sample, encodeOptions);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private ensureEncoder(videoSample: VideoSample) {
|
|
650
|
+
this.ensureEncoderPromise = (async () => {
|
|
651
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
652
|
+
const quality = resolveQuality(this.encodingConfig.quality, this.encodingConfig.bitrate);
|
|
653
|
+
assert(quality !== undefined);
|
|
654
|
+
|
|
655
|
+
const candidates = buildVideoEncoderConfigs({
|
|
656
|
+
...this.encodingConfig,
|
|
657
|
+
quality,
|
|
658
|
+
width: videoSample.codedWidth,
|
|
659
|
+
height: videoSample.codedHeight,
|
|
660
|
+
squarePixelWidth: videoSample.squarePixelWidth,
|
|
661
|
+
squarePixelHeight: videoSample.squarePixelHeight,
|
|
662
|
+
framerate: this.source._connectedTrack?.metadata.frameRate,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// Try the candidate configs in order of preference until we find one that is supported
|
|
666
|
+
let selected: VideoEncoderConfigCandidate | null = null;
|
|
667
|
+
let MatchingCustomEncoder: (typeof customVideoEncoders)[number] | undefined;
|
|
668
|
+
|
|
669
|
+
for (const candidate of candidates) {
|
|
670
|
+
const candidateConfig = candidate.config;
|
|
671
|
+
this.encodingConfig.onEncoderConfig?.(candidateConfig);
|
|
672
|
+
|
|
673
|
+
MatchingCustomEncoder = customVideoEncoders.find(x => x.supports(
|
|
674
|
+
this.encodingConfig.codec,
|
|
675
|
+
candidateConfig,
|
|
676
|
+
));
|
|
677
|
+
if (MatchingCustomEncoder) {
|
|
678
|
+
selected = candidate;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (typeof VideoEncoder === 'undefined') {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
candidateConfig.alpha = 'discard'; // Since we handle alpha ourselves
|
|
687
|
+
|
|
688
|
+
if (this.encodingConfig.alpha === 'keep') {
|
|
689
|
+
// Encoding alpha requires using two parallel encoders, so we need to make sure they stay in sync
|
|
690
|
+
// and that neither of them drops frames. Setting latencyMode to 'quality' achieves this, because
|
|
691
|
+
// "User Agents MUST not drop frames to achieve the target bitrate and/or framerate."
|
|
692
|
+
candidateConfig.latencyMode = 'quality';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const hasOddDimension = candidateConfig.width % 2 === 1 || candidateConfig.height % 2 === 1;
|
|
696
|
+
if (
|
|
697
|
+
hasOddDimension
|
|
698
|
+
&& (this.encodingConfig.codec === 'avc' || this.encodingConfig.codec === 'hevc')
|
|
699
|
+
) {
|
|
700
|
+
// Throw a special error for this case as it gets hit often
|
|
701
|
+
throw new Error(
|
|
702
|
+
`The dimensions ${candidateConfig.width}x${candidateConfig.height} are not supported for codec`
|
|
703
|
+
+ ` '${this.encodingConfig.codec}'; both width and height must be even numbers. Make sure to`
|
|
704
|
+
+ ` round your dimensions to the nearest even number.`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const support = await VideoEncoder.isConfigSupported(candidateConfig);
|
|
709
|
+
if (support.supported) {
|
|
710
|
+
selected = candidate;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (!selected) {
|
|
716
|
+
if (typeof VideoEncoder === 'undefined') {
|
|
717
|
+
throw new Error('VideoEncoder is not supported by this browser.');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// The candidates only differ in their rate control, so we describe them as one config with a
|
|
721
|
+
// slash-separated list of the attempted rate control methods
|
|
722
|
+
const firstConfig = candidates[0]!.config;
|
|
723
|
+
const rateControls = candidates.map(({ config, quantizer }) =>
|
|
724
|
+
quantizer !== null ? `quantizer ${quantizer}` : `${config.bitrate} bps`,
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
throw new Error(
|
|
728
|
+
`This specific encoder configuration (${firstConfig.codec}, ${rateControls.join(' / ')},`
|
|
729
|
+
+ ` ${firstConfig.width}x${firstConfig.height}, hardware acceleration:`
|
|
730
|
+
+ ` ${firstConfig.hardwareAcceleration ?? 'no-preference'}) is not supported by this browser.`
|
|
731
|
+
+ ` Consider using another codec or changing your video parameters.`,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const encoderConfig = selected.config;
|
|
736
|
+
if (selected.quantizer !== null) {
|
|
737
|
+
// The chosen config uses quantizer-based rate control, so each frame must carry the quantizer value
|
|
738
|
+
this.defaultEncodeOptions = buildQuantizerEncodeOptions(
|
|
739
|
+
this.encodingConfig.codec,
|
|
740
|
+
selected.quantizer,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (MatchingCustomEncoder) {
|
|
745
|
+
// @ts-expect-error "Can't create instance of abstract class 🤓"
|
|
746
|
+
this.customEncoder = new MatchingCustomEncoder() as CustomVideoEncoder;
|
|
747
|
+
// @ts-expect-error It's technically readonly
|
|
748
|
+
this.customEncoder.codec = this.encodingConfig.codec;
|
|
749
|
+
// @ts-expect-error It's technically readonly
|
|
750
|
+
this.customEncoder.config = encoderConfig;
|
|
751
|
+
// @ts-expect-error It's technically readonly
|
|
752
|
+
this.customEncoder.onPacket = (packet, meta) => {
|
|
753
|
+
if (!(packet instanceof EncodedPacket)) {
|
|
754
|
+
throw new TypeError('The first argument passed to onPacket must be an EncodedPacket.');
|
|
755
|
+
}
|
|
756
|
+
if (meta !== undefined && (!meta || typeof meta !== 'object')) {
|
|
757
|
+
throw new TypeError('The second argument passed to onPacket must be an object or undefined.');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
maybeEnsureIsKeyPacket(this.source._connectedTrack!, packet);
|
|
761
|
+
|
|
762
|
+
this.encodingConfig.onEncodedPacket?.(packet, meta);
|
|
763
|
+
this.lastMuxerPromise
|
|
764
|
+
= this.muxer!.addEncodedVideoPacket(this.source._connectedTrack!, packet, meta)
|
|
765
|
+
.catch((error) => {
|
|
766
|
+
this.setError(error);
|
|
767
|
+
});
|
|
768
|
+
};
|
|
769
|
+
// @ts-expect-error It's technically readonly
|
|
770
|
+
this.customEncoder.onError = (error) => {
|
|
771
|
+
this.setError(error);
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
await this.customEncoder.init();
|
|
775
|
+
} else {
|
|
776
|
+
/** Queue of color chunks waiting for their alpha counterpart. */
|
|
777
|
+
const colorChunkQueue: {
|
|
778
|
+
chunk: EncodedVideoChunk;
|
|
779
|
+
meta: EncodedVideoChunkMetadata | undefined;
|
|
780
|
+
}[] = [];
|
|
781
|
+
/** Each value is the number of encoded alpha chunks at which a null alpha chunk should be added. */
|
|
782
|
+
const nullAlphaChunkQueue: number[] = [];
|
|
783
|
+
let encodedAlphaChunkCount = 0;
|
|
784
|
+
let alphaEncoderQueue = 0;
|
|
785
|
+
|
|
786
|
+
const addPacket = (
|
|
787
|
+
colorChunk: EncodedVideoChunk,
|
|
788
|
+
alphaChunk: EncodedVideoChunk | null,
|
|
789
|
+
meta: EncodedVideoChunkMetadata | undefined,
|
|
790
|
+
) => {
|
|
791
|
+
const sideData: EncodedPacketSideData = {};
|
|
792
|
+
|
|
793
|
+
if (alphaChunk) {
|
|
794
|
+
const alphaData = new Uint8Array(alphaChunk.byteLength);
|
|
795
|
+
alphaChunk.copyTo(alphaData);
|
|
796
|
+
|
|
797
|
+
sideData.alpha = alphaData;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let packet = EncodedPacket.fromEncodedChunk(colorChunk, sideData);
|
|
801
|
+
|
|
802
|
+
// See if there's a relevant timing entry to refine the packet's timing data
|
|
803
|
+
const preciseTimingIndex = binarySearchLessOrEqual(
|
|
804
|
+
this.preciseTimings,
|
|
805
|
+
colorChunk.timestamp,
|
|
806
|
+
x => x.microsecondTimestamp,
|
|
807
|
+
);
|
|
808
|
+
const entry = preciseTimingIndex !== -1
|
|
809
|
+
? this.preciseTimings[preciseTimingIndex]
|
|
810
|
+
: null;
|
|
811
|
+
|
|
812
|
+
let actualType: PacketType | null = null;
|
|
813
|
+
if (this.emittedEncoderPackets === 0 && packet.type === 'delta' && meta?.decoderConfig) {
|
|
814
|
+
// https://github.com/Vanilagy/mediabunny/issues/365
|
|
815
|
+
// We expect the first packet to be a key packet. If it's not, let's actually verify that it's
|
|
816
|
+
// not by getting the actual type.
|
|
817
|
+
actualType = determineVideoPacketType(
|
|
818
|
+
this.encodingConfig.codec,
|
|
819
|
+
meta.decoderConfig,
|
|
820
|
+
packet.data,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Define the packet
|
|
825
|
+
if ((entry && entry.microsecondTimestamp === colorChunk.timestamp) || actualType !== null) {
|
|
826
|
+
packet = packet.clone({
|
|
827
|
+
timestamp: entry?.timestampIsValid ? entry.timestamp : undefined,
|
|
828
|
+
duration: entry?.durationIsValid ? entry.duration : undefined,
|
|
829
|
+
type: actualType ?? undefined,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
maybeEnsureIsKeyPacket(this.source._connectedTrack!, packet);
|
|
834
|
+
|
|
835
|
+
this.encodingConfig.onEncodedPacket?.(packet, meta);
|
|
836
|
+
this.lastMuxerPromise
|
|
837
|
+
= this.muxer!.addEncodedVideoPacket(this.source._connectedTrack!, packet, meta)
|
|
838
|
+
.catch((error) => {
|
|
839
|
+
this.setError(error);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
this.emittedEncoderPackets++;
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const stack = new Error('Encoding error').stack;
|
|
846
|
+
|
|
847
|
+
this.encoder = new VideoEncoder({
|
|
848
|
+
output: (chunk, meta) => {
|
|
849
|
+
if (!this.alphaEncoder) {
|
|
850
|
+
// We're done
|
|
851
|
+
addPacket(chunk, null, meta);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const alphaFrame = this.alphaFrameQueue.shift();
|
|
856
|
+
assert(alphaFrame !== undefined);
|
|
857
|
+
|
|
858
|
+
if (alphaFrame) {
|
|
859
|
+
this.alphaEncoder.encode(alphaFrame, {
|
|
860
|
+
...this.defaultEncodeOptions,
|
|
861
|
+
// Crucial: The alpha frame is forced to be a key frame whenever the color frame
|
|
862
|
+
// also is. Without this, playback can glitch and even crash in some browsers.
|
|
863
|
+
// This is the reason why the two encoders are wired in series and not in parallel.
|
|
864
|
+
keyFrame: chunk.type === 'key',
|
|
865
|
+
});
|
|
866
|
+
alphaEncoderQueue++;
|
|
867
|
+
alphaFrame.close();
|
|
868
|
+
colorChunkQueue.push({ chunk, meta });
|
|
869
|
+
} else {
|
|
870
|
+
// There was no alpha component for this frame
|
|
871
|
+
if (alphaEncoderQueue === 0) {
|
|
872
|
+
// No pending alpha encodes either, so we're done
|
|
873
|
+
addPacket(chunk, null, meta);
|
|
874
|
+
} else {
|
|
875
|
+
// There are still alpha encodes pending, so we can't add the packet immediately since
|
|
876
|
+
// we'd end up with out-of-order packets. Instead, let's queue a null alpha chunk to be
|
|
877
|
+
// added in the future, after the current encoder workload has completed:
|
|
878
|
+
nullAlphaChunkQueue.push(encodedAlphaChunkCount + alphaEncoderQueue);
|
|
879
|
+
colorChunkQueue.push({ chunk, meta });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
error: (error) => {
|
|
884
|
+
error.stack = stack; // Provide a more useful stack trace, the default one sucks
|
|
885
|
+
this.setError(error);
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
this.encoder.configure(encoderConfig);
|
|
889
|
+
|
|
890
|
+
if (this.encodingConfig.alpha === 'keep') {
|
|
891
|
+
const stack = new Error('Encoding error').stack;
|
|
892
|
+
|
|
893
|
+
// We need to encode alpha as well, which we do with a separate encoder
|
|
894
|
+
this.alphaEncoder = new VideoEncoder({
|
|
895
|
+
// We ignore the alpha chunk's metadata
|
|
896
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
897
|
+
output: (chunk, meta) => {
|
|
898
|
+
alphaEncoderQueue--;
|
|
899
|
+
|
|
900
|
+
// There has to be a color chunk because the encoders are wired in series
|
|
901
|
+
const colorChunk = colorChunkQueue.shift();
|
|
902
|
+
assert(colorChunk !== undefined);
|
|
903
|
+
|
|
904
|
+
addPacket(colorChunk.chunk, chunk, colorChunk.meta);
|
|
905
|
+
|
|
906
|
+
// See if there are any null alpha chunks queued up
|
|
907
|
+
encodedAlphaChunkCount++;
|
|
908
|
+
while (
|
|
909
|
+
nullAlphaChunkQueue.length > 0
|
|
910
|
+
&& nullAlphaChunkQueue[0] === encodedAlphaChunkCount
|
|
911
|
+
) {
|
|
912
|
+
nullAlphaChunkQueue.shift();
|
|
913
|
+
const colorChunk = colorChunkQueue.shift();
|
|
914
|
+
assert(colorChunk !== undefined);
|
|
915
|
+
|
|
916
|
+
addPacket(colorChunk.chunk, null, colorChunk.meta);
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
error: (error) => {
|
|
920
|
+
error.stack = stack; // Provide a more useful stack trace
|
|
921
|
+
this.setError(error);
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
this.alphaEncoder.configure(encoderConfig);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
assert(this.source._connectedTrack);
|
|
929
|
+
this.muxer = this.source._connectedTrack.output._muxer;
|
|
930
|
+
|
|
931
|
+
this.encoderInitialized = true;
|
|
932
|
+
})();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async flushAndClose(forceClose: boolean) {
|
|
936
|
+
try {
|
|
937
|
+
if (!forceClose) {
|
|
938
|
+
this.checkForEncoderError();
|
|
939
|
+
|
|
940
|
+
// Final frame rate padding: fill remaining frames up to the last sample's original end timestamp
|
|
941
|
+
if (this.frameRateLastSample) {
|
|
942
|
+
const frameRate = this.encodingConfig.transform!.frameRate!;
|
|
943
|
+
const alignedEnd = floorToDivisor(this.frameRateLastEndTimestamp!, frameRate);
|
|
944
|
+
await this.padFrameRate(alignedEnd);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
this.closed = true;
|
|
949
|
+
|
|
950
|
+
if (!forceClose) {
|
|
951
|
+
if (this.customEncoder) {
|
|
952
|
+
void this.customEncoderCallSerializer.call(() => this.customEncoder!.flush());
|
|
953
|
+
} else if (this.encoder) {
|
|
954
|
+
// These are wired in series, therefore they must also be flushed in series
|
|
955
|
+
await this.encoder.flush();
|
|
956
|
+
await this.alphaEncoder?.flush();
|
|
957
|
+
|
|
958
|
+
// Workaround for https://issues.chromium.org/issues/529852980 to give it time for errors to
|
|
959
|
+
// surface
|
|
960
|
+
await wait(25);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
} finally {
|
|
964
|
+
// This cleanup must also run when padding or flushing threw (e.g. due to an encoder error), otherwise
|
|
965
|
+
// samples, frames and encoders would be left dangling
|
|
966
|
+
this.closed = true;
|
|
967
|
+
|
|
968
|
+
this.frameRateLastSample?.close();
|
|
969
|
+
this.frameRateLastSample = null;
|
|
970
|
+
|
|
971
|
+
if (this.customEncoder) {
|
|
972
|
+
await this.customEncoderCallSerializer.call(() => this.customEncoder!.close())
|
|
973
|
+
.catch((error: unknown) => this.setError(error));
|
|
974
|
+
} else if (this.encoder) {
|
|
975
|
+
if (this.encoder.state !== 'closed') {
|
|
976
|
+
this.encoder.close();
|
|
977
|
+
}
|
|
978
|
+
if (this.alphaEncoder && this.alphaEncoder.state !== 'closed') {
|
|
979
|
+
this.alphaEncoder.close();
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
this.alphaFrameQueue.forEach(x => x?.close());
|
|
983
|
+
this.alphaFrameQueue.length = 0;
|
|
984
|
+
|
|
985
|
+
this.splitter?.close();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (!forceClose) {
|
|
990
|
+
this.checkForEncoderError();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
getQueueSize() {
|
|
995
|
+
if (this.customEncoder) {
|
|
996
|
+
return this.customEncoderQueueSize;
|
|
997
|
+
} else {
|
|
998
|
+
// Because the color and alpha encoders are wired in series, there's no need to also include the alpha
|
|
999
|
+
// encoder's queue size here
|
|
1000
|
+
return this.encoder?.encodeQueueSize ?? 0;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
checkForEncoderError() {
|
|
1005
|
+
if (this.errorSet) {
|
|
1006
|
+
throw this.error;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
let splitterWorkerUrl: string | null = null;
|
|
1012
|
+
|
|
1013
|
+
/** Utility class for splitting a composite frame into separate color and alpha parts on the CPU in a worker. */
|
|
1014
|
+
export class ColorAlphaSplitter {
|
|
1015
|
+
private worker: Worker | null = null;
|
|
1016
|
+
private pendingRequests = new Map<
|
|
1017
|
+
number,
|
|
1018
|
+
ReturnType<typeof promiseWithResolvers<{ colorFrame: VideoFrame; alphaFrame: VideoFrame }>>
|
|
1019
|
+
>();
|
|
1020
|
+
|
|
1021
|
+
private nextRequestId = 0;
|
|
1022
|
+
|
|
1023
|
+
split(sourceFrame: VideoFrame): Promise<{ colorFrame: VideoFrame; alphaFrame: VideoFrame }> {
|
|
1024
|
+
if (!this.worker) {
|
|
1025
|
+
if (!splitterWorkerUrl) {
|
|
1026
|
+
const blob = new Blob(
|
|
1027
|
+
[`(${colorAlphaSplitterWorkerCode.toString()})()`],
|
|
1028
|
+
{ type: 'application/javascript' },
|
|
1029
|
+
);
|
|
1030
|
+
splitterWorkerUrl = URL.createObjectURL(blob);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
this.worker = new Worker(splitterWorkerUrl);
|
|
1034
|
+
|
|
1035
|
+
this.worker.addEventListener('message', (event: MessageEvent<ColorAlphaSplitterWorkerResponse>) => {
|
|
1036
|
+
const data = event.data;
|
|
1037
|
+
const pending = this.pendingRequests.get(data.id);
|
|
1038
|
+
if (!pending) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
this.pendingRequests.delete(data.id);
|
|
1042
|
+
|
|
1043
|
+
if ('error' in data) {
|
|
1044
|
+
pending.reject(new Error(data.error));
|
|
1045
|
+
} else {
|
|
1046
|
+
pending.resolve({ colorFrame: data.colorFrame, alphaFrame: data.alphaFrame });
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
this.worker.addEventListener('error', (event) => {
|
|
1051
|
+
const error = new Error(event.message || 'Color/alpha splitter worker error.');
|
|
1052
|
+
for (const pending of this.pendingRequests.values()) {
|
|
1053
|
+
pending.reject(error);
|
|
1054
|
+
}
|
|
1055
|
+
this.pendingRequests.clear();
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const id = this.nextRequestId++;
|
|
1060
|
+
const pending = promiseWithResolvers<{ colorFrame: VideoFrame; alphaFrame: VideoFrame }>();
|
|
1061
|
+
this.pendingRequests.set(id, pending);
|
|
1062
|
+
this.worker.postMessage({ id, sourceFrame }, { transfer: [sourceFrame] });
|
|
1063
|
+
|
|
1064
|
+
return pending.promise;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
close() {
|
|
1068
|
+
this.worker?.terminate();
|
|
1069
|
+
this.worker = null;
|
|
1070
|
+
|
|
1071
|
+
const error = new Error('Color/alpha splitter closed.');
|
|
1072
|
+
for (const pending of this.pendingRequests.values()) {
|
|
1073
|
+
pending.reject(error);
|
|
1074
|
+
}
|
|
1075
|
+
this.pendingRequests.clear();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
type ColorAlphaSplitterWorkerRequest = {
|
|
1080
|
+
id: number;
|
|
1081
|
+
sourceFrame: VideoFrame;
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
type ColorAlphaSplitterWorkerResponse =
|
|
1085
|
+
| { id: number; colorFrame: VideoFrame; alphaFrame: VideoFrame }
|
|
1086
|
+
| { id: number; error: string };
|
|
1087
|
+
|
|
1088
|
+
const colorAlphaSplitterWorkerCode = () => {
|
|
1089
|
+
// Reused across frames as long as the size matches, since consecutive frames usually share dimensions.
|
|
1090
|
+
let cpuSourceBuffer: Uint8Array | null = null;
|
|
1091
|
+
|
|
1092
|
+
// Serialize execution internally so concurrent requests don't race on the shared cpuSourceBuffer.
|
|
1093
|
+
let chain: Promise<void> = Promise.resolve();
|
|
1094
|
+
self.addEventListener('message', (event: MessageEvent<ColorAlphaSplitterWorkerRequest>) => {
|
|
1095
|
+
const { id, sourceFrame } = event.data;
|
|
1096
|
+
chain = chain.then(async () => {
|
|
1097
|
+
try {
|
|
1098
|
+
const { colorFrame, alphaFrame } = await split(sourceFrame);
|
|
1099
|
+
self.postMessage({ id, colorFrame, alphaFrame }, { transfer: [colorFrame, alphaFrame] });
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
self.postMessage({ id, error: (error as Error).message });
|
|
1102
|
+
} finally {
|
|
1103
|
+
sourceFrame.close();
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
const split = async (sourceFrame: VideoFrame) => {
|
|
1109
|
+
const format = sourceFrame.format as VideoSamplePixelFormat | null;
|
|
1110
|
+
if (!format) {
|
|
1111
|
+
throw new Error('CPU color/alpha splitting requires a known VideoFrame format.');
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const sourceSize = sourceFrame.allocationSize();
|
|
1115
|
+
|
|
1116
|
+
if (!cpuSourceBuffer || cpuSourceBuffer.byteLength !== sourceSize) {
|
|
1117
|
+
cpuSourceBuffer = new Uint8Array(sourceSize);
|
|
1118
|
+
}
|
|
1119
|
+
await sourceFrame.copyTo(cpuSourceBuffer);
|
|
1120
|
+
|
|
1121
|
+
if (format === 'RGBA' || format === 'BGRA') {
|
|
1122
|
+
return splitInterleavedRgba(cpuSourceBuffer, format, sourceFrame);
|
|
1123
|
+
} else if (
|
|
1124
|
+
format === 'I420A' || format === 'I420AP10' || format === 'I420AP12'
|
|
1125
|
+
|| format === 'I422A' || format === 'I422AP10' || format === 'I422AP12'
|
|
1126
|
+
|| format === 'I444A' || format === 'I444AP10' || format === 'I444AP12'
|
|
1127
|
+
) {
|
|
1128
|
+
return splitPlanarYuvA(cpuSourceBuffer, format, sourceFrame);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
throw new Error(`CPU color/alpha splitting does not support format '${format}'.`);
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const splitInterleavedRgba = (
|
|
1135
|
+
source: Uint8Array,
|
|
1136
|
+
format: 'RGBA' | 'BGRA',
|
|
1137
|
+
sourceFrame: VideoFrame,
|
|
1138
|
+
) => {
|
|
1139
|
+
const width = sourceFrame.visibleRect?.width ?? sourceFrame.codedWidth;
|
|
1140
|
+
const height = sourceFrame.visibleRect?.height ?? sourceFrame.codedHeight;
|
|
1141
|
+
|
|
1142
|
+
const pixelCount = width * height;
|
|
1143
|
+
const chromaW = Math.ceil(width / 2);
|
|
1144
|
+
const chromaH = Math.ceil(height / 2);
|
|
1145
|
+
const alphaSize = pixelCount + chromaW * chromaH * 2;
|
|
1146
|
+
|
|
1147
|
+
// Encode alpha as I420: Y = source A bytes, UV = 128
|
|
1148
|
+
const alphaBuffer = new Uint8Array(alphaSize);
|
|
1149
|
+
for (let i = 0, j = 3; i < pixelCount; i++, j += 4) {
|
|
1150
|
+
alphaBuffer[i] = source[j]!;
|
|
1151
|
+
}
|
|
1152
|
+
alphaBuffer.fill(128, pixelCount);
|
|
1153
|
+
|
|
1154
|
+
// Hand the source buffer straight to VideoFrame as RGBX/BGRX so the A bytes are ignored
|
|
1155
|
+
const colorFrame = new VideoFrame(source, {
|
|
1156
|
+
format: format === 'RGBA' ? 'RGBX' : 'BGRX',
|
|
1157
|
+
codedWidth: width,
|
|
1158
|
+
codedHeight: height,
|
|
1159
|
+
timestamp: sourceFrame.timestamp,
|
|
1160
|
+
duration: sourceFrame.duration ?? undefined,
|
|
1161
|
+
// No transfer!
|
|
1162
|
+
});
|
|
1163
|
+
const alphaInit = {
|
|
1164
|
+
format: 'I420' as const,
|
|
1165
|
+
codedWidth: width,
|
|
1166
|
+
codedHeight: height,
|
|
1167
|
+
timestamp: sourceFrame.timestamp,
|
|
1168
|
+
duration: sourceFrame.duration ?? undefined,
|
|
1169
|
+
transfer: [alphaBuffer.buffer],
|
|
1170
|
+
};
|
|
1171
|
+
const alphaFrame = new VideoFrame(alphaBuffer, alphaInit);
|
|
1172
|
+
return { colorFrame, alphaFrame };
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const splitPlanarYuvA = (
|
|
1176
|
+
source: Uint8Array,
|
|
1177
|
+
format:
|
|
1178
|
+
| 'I420A' | 'I420AP10' | 'I420AP12'
|
|
1179
|
+
| 'I422A' | 'I422AP10' | 'I422AP12'
|
|
1180
|
+
| 'I444A' | 'I444AP10' | 'I444AP12',
|
|
1181
|
+
sourceFrame: VideoFrame,
|
|
1182
|
+
) => {
|
|
1183
|
+
const width = sourceFrame.visibleRect?.width ?? sourceFrame.codedWidth;
|
|
1184
|
+
const height = sourceFrame.visibleRect?.height ?? sourceFrame.codedHeight;
|
|
1185
|
+
|
|
1186
|
+
const is10 = format.includes('P10');
|
|
1187
|
+
const is12 = format.includes('P12');
|
|
1188
|
+
const bytesPerSample = (is10 || is12) ? 2 : 1;
|
|
1189
|
+
|
|
1190
|
+
let chromaW: number;
|
|
1191
|
+
let chromaH: number;
|
|
1192
|
+
if (format.startsWith('I420')) {
|
|
1193
|
+
chromaW = Math.ceil(width / 2);
|
|
1194
|
+
chromaH = Math.ceil(height / 2);
|
|
1195
|
+
} else if (format.startsWith('I422')) {
|
|
1196
|
+
chromaW = Math.ceil(width / 2);
|
|
1197
|
+
chromaH = height;
|
|
1198
|
+
} else {
|
|
1199
|
+
chromaW = width;
|
|
1200
|
+
chromaH = height;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const ySamples = width * height;
|
|
1204
|
+
const uvSamples = chromaW * chromaH;
|
|
1205
|
+
const yBytes = ySamples * bytesPerSample;
|
|
1206
|
+
const uvBytes = uvSamples * bytesPerSample;
|
|
1207
|
+
const aBytes = ySamples * bytesPerSample;
|
|
1208
|
+
|
|
1209
|
+
const colorBytes = yBytes + uvBytes * 2;
|
|
1210
|
+
const colorFormat = format.replace('A', '') as VideoPixelFormat;
|
|
1211
|
+
|
|
1212
|
+
const alphaChromaW = Math.ceil(width / 2);
|
|
1213
|
+
const alphaChromaH = Math.ceil(height / 2);
|
|
1214
|
+
const alphaUvSamples = alphaChromaW * alphaChromaH;
|
|
1215
|
+
const alphaUvBytes = alphaUvSamples * bytesPerSample;
|
|
1216
|
+
const alphaSize = aBytes + 2 * alphaUvBytes;
|
|
1217
|
+
const alphaBuffer = new Uint8Array(alphaSize);
|
|
1218
|
+
|
|
1219
|
+
const aPlaneStart = colorBytes;
|
|
1220
|
+
alphaBuffer.set(source.subarray(aPlaneStart, aPlaneStart + aBytes), 0);
|
|
1221
|
+
|
|
1222
|
+
// Fill UV planes with the neutral chroma value
|
|
1223
|
+
const uvOffset = aBytes;
|
|
1224
|
+
const neutralChroma = is10 ? 512 : (is12 ? 2048 : 128);
|
|
1225
|
+
|
|
1226
|
+
if (bytesPerSample === 1) {
|
|
1227
|
+
alphaBuffer.fill(neutralChroma, uvOffset);
|
|
1228
|
+
} else {
|
|
1229
|
+
const uvView = new Uint16Array(alphaBuffer.buffer, uvOffset, 2 * alphaUvSamples);
|
|
1230
|
+
uvView.fill(neutralChroma);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const alphaFormat = (is10 ? 'I420P10' : (is12 ? 'I420P12' : 'I420')) as VideoPixelFormat;
|
|
1234
|
+
|
|
1235
|
+
// Color frame is simply a prefix of the combined bytes
|
|
1236
|
+
const colorFrame = new VideoFrame(source.subarray(0, colorBytes), {
|
|
1237
|
+
format: colorFormat,
|
|
1238
|
+
codedWidth: width,
|
|
1239
|
+
codedHeight: height,
|
|
1240
|
+
timestamp: sourceFrame.timestamp,
|
|
1241
|
+
duration: sourceFrame.duration ?? undefined,
|
|
1242
|
+
});
|
|
1243
|
+
const alphaInit = {
|
|
1244
|
+
format: alphaFormat,
|
|
1245
|
+
codedWidth: width,
|
|
1246
|
+
codedHeight: height,
|
|
1247
|
+
timestamp: sourceFrame.timestamp,
|
|
1248
|
+
duration: sourceFrame.duration ?? undefined,
|
|
1249
|
+
transfer: [alphaBuffer.buffer],
|
|
1250
|
+
};
|
|
1251
|
+
const alphaFrame = new VideoFrame(alphaBuffer, alphaInit);
|
|
1252
|
+
return { colorFrame, alphaFrame };
|
|
1253
|
+
};
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* This source can be used to add raw, unencoded video samples (frames) to an output video track. These frames will
|
|
1258
|
+
* automatically be encoded and then piped into the output.
|
|
1259
|
+
* @group Media sources
|
|
1260
|
+
* @public
|
|
1261
|
+
*/
|
|
1262
|
+
export class VideoSampleSource extends VideoSource {
|
|
1263
|
+
/** @internal */
|
|
1264
|
+
private _encoder: VideoEncoderWrapper;
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Creates a new {@link VideoSampleSource} whose samples are encoded according to the specified
|
|
1268
|
+
* {@link VideoEncodingConfig}.
|
|
1269
|
+
*/
|
|
1270
|
+
constructor(encodingConfig: VideoEncodingConfig) {
|
|
1271
|
+
validateVideoEncodingConfig(encodingConfig);
|
|
1272
|
+
|
|
1273
|
+
super(encodingConfig.codec);
|
|
1274
|
+
this._encoder = new VideoEncoderWrapper(this, encodingConfig);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Encodes a video sample (frame) and then adds it to the output.
|
|
1279
|
+
*
|
|
1280
|
+
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
|
|
1281
|
+
* to respect writer and encoder backpressure.
|
|
1282
|
+
*/
|
|
1283
|
+
add(videoSample: VideoSample, encodeOptions?: VideoEncoderEncodeOptions) {
|
|
1284
|
+
if (!(videoSample instanceof VideoSample)) {
|
|
1285
|
+
throw new TypeError('videoSample must be a VideoSample.');
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return this._encoder.add(videoSample, false, encodeOptions);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** @internal */
|
|
1292
|
+
override _flushAndClose(forceClose: boolean) {
|
|
1293
|
+
return this._encoder.flushAndClose(forceClose);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* This source can be used to add video frames to the output track from a fixed canvas element. Since canvases are often
|
|
1299
|
+
* used for rendering, this source provides a convenient wrapper around {@link VideoSampleSource}.
|
|
1300
|
+
* @group Media sources
|
|
1301
|
+
* @public
|
|
1302
|
+
*/
|
|
1303
|
+
export class CanvasSource extends VideoSource {
|
|
1304
|
+
/** @internal */
|
|
1305
|
+
private _encoder: VideoEncoderWrapper;
|
|
1306
|
+
/** @internal */
|
|
1307
|
+
private _canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Creates a new {@link CanvasSource} from a canvas element or `OffscreenCanvas` whose samples are encoded
|
|
1311
|
+
* according to the specified {@link VideoEncodingConfig}.
|
|
1312
|
+
*/
|
|
1313
|
+
constructor(canvas: HTMLCanvasElement | OffscreenCanvas, encodingConfig: VideoEncodingConfig) {
|
|
1314
|
+
if (
|
|
1315
|
+
!(typeof HTMLCanvasElement !== 'undefined' && canvas instanceof HTMLCanvasElement)
|
|
1316
|
+
&& !(typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas)
|
|
1317
|
+
) {
|
|
1318
|
+
throw new TypeError('canvas must be an HTMLCanvasElement or OffscreenCanvas.');
|
|
1319
|
+
}
|
|
1320
|
+
validateVideoEncodingConfig(encodingConfig);
|
|
1321
|
+
|
|
1322
|
+
super(encodingConfig.codec);
|
|
1323
|
+
this._encoder = new VideoEncoderWrapper(this, encodingConfig);
|
|
1324
|
+
this._canvas = canvas;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Captures the current canvas state as a video sample (frame), encodes it and adds it to the output.
|
|
1329
|
+
*
|
|
1330
|
+
* @param timestamp - The timestamp of the sample, in seconds.
|
|
1331
|
+
* @param duration - The duration of the sample, in seconds.
|
|
1332
|
+
*
|
|
1333
|
+
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
|
|
1334
|
+
* to respect writer and encoder backpressure.
|
|
1335
|
+
*/
|
|
1336
|
+
add(timestamp: number, duration = 0, encodeOptions?: VideoEncoderEncodeOptions) {
|
|
1337
|
+
if (!Number.isFinite(timestamp) || timestamp < 0) {
|
|
1338
|
+
throw new TypeError('timestamp must be a non-negative number.');
|
|
1339
|
+
}
|
|
1340
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
1341
|
+
throw new TypeError('duration must be a non-negative number.');
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const sample = new VideoSample(this._canvas, { timestamp, duration });
|
|
1345
|
+
return this._encoder.add(sample, true, encodeOptions);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** @internal */
|
|
1349
|
+
override _flushAndClose(forceClose: boolean) {
|
|
1350
|
+
return this._encoder.flushAndClose(forceClose);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Options for {@link MediaStreamVideoTrackSource}.
|
|
1356
|
+
* @group Media sources
|
|
1357
|
+
* @public
|
|
1358
|
+
*/
|
|
1359
|
+
export type MediaStreamVideoTrackSourceOptions = {
|
|
1360
|
+
/**
|
|
1361
|
+
* The frame rate at which the underlying video track is sampled. Defaults to the frame rate specified in the
|
|
1362
|
+
* track's [`MediaTrackSettings`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings). Set to
|
|
1363
|
+
* `null` to only add a frame whenever the underlying track pushes one - this minimizes frame count but can
|
|
1364
|
+
* lead to wildly irregular FPS.
|
|
1365
|
+
*/
|
|
1366
|
+
frameRate?: number | null;
|
|
1367
|
+
/**
|
|
1368
|
+
* Controls the basis (zero point) for video frame timestamps.
|
|
1369
|
+
*
|
|
1370
|
+
* When set to `'synced-zero'`, timestamps will be relative to the first chunk of media from a `MediaStreamTrack`
|
|
1371
|
+
* added to the {@link Output}.
|
|
1372
|
+
*
|
|
1373
|
+
* When set to `'zero'`, timestamps will be relative to the first video frame emitted by this source.
|
|
1374
|
+
*
|
|
1375
|
+
* When set to `'unix'`, timestamps will be relative to the Unix epoch, so clearly associated with a distinct point
|
|
1376
|
+
* in time. Here, pausing via {@link MediaStreamVideoTrackSource.pause} will also create gaps in timestamps. Be sure
|
|
1377
|
+
* to pair this mode with {@link BaseTrackMetadata.isRelativeToUnixEpoch}.
|
|
1378
|
+
*
|
|
1379
|
+
* Defaults to `'synced-zero'`.
|
|
1380
|
+
*/
|
|
1381
|
+
timestampBase?: 'synced-zero' | 'zero' | 'unix';
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Video source that encodes the frames of a
|
|
1386
|
+
* [`MediaStreamVideoTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) and pipes them into the
|
|
1387
|
+
* output. This is useful for capturing live or real-time data such as webcams or screen captures. Frames will
|
|
1388
|
+
* automatically start being captured once the connected {@link Output} is started, and will keep being captured until
|
|
1389
|
+
* the {@link Output} is finalized or this source is closed.
|
|
1390
|
+
* @group Media sources
|
|
1391
|
+
* @public
|
|
1392
|
+
*/
|
|
1393
|
+
export class MediaStreamVideoTrackSource extends VideoSource {
|
|
1394
|
+
/** @internal */
|
|
1395
|
+
private _options: MediaStreamVideoTrackSourceOptions;
|
|
1396
|
+
/** @internal */
|
|
1397
|
+
private _encoder: VideoEncoderWrapper;
|
|
1398
|
+
/** @internal */
|
|
1399
|
+
private _abortController: AbortController | null = null;
|
|
1400
|
+
/** @internal */
|
|
1401
|
+
private _track: MediaStreamVideoTrack;
|
|
1402
|
+
/** @internal */
|
|
1403
|
+
private _workerTrackId: number | null = null;
|
|
1404
|
+
/** @internal */
|
|
1405
|
+
private _workerListener: ((event: MessageEvent) => void) | null = null;
|
|
1406
|
+
/** @internal */
|
|
1407
|
+
private _promiseWithResolvers = promiseWithResolvers();
|
|
1408
|
+
/** @internal */
|
|
1409
|
+
private _errorPromiseAccessed = false;
|
|
1410
|
+
/** @internal */
|
|
1411
|
+
private _paused = false;
|
|
1412
|
+
/** @internal */
|
|
1413
|
+
private _lastVideoFrame: VideoFrame | null = null;
|
|
1414
|
+
/** @internal */
|
|
1415
|
+
private _timerHandle: UnthrottledTimerHandle | null = null;
|
|
1416
|
+
/** @internal */
|
|
1417
|
+
private _videoElement: HTMLVideoElement | null = null;
|
|
1418
|
+
|
|
1419
|
+
/** A promise that rejects upon any error within this source. This promise never resolves. */
|
|
1420
|
+
get errorPromise() {
|
|
1421
|
+
this._errorPromiseAccessed = true;
|
|
1422
|
+
return this._promiseWithResolvers.promise;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/** Whether this source is currently paused as a result of calling `.pause()`. */
|
|
1426
|
+
get paused() {
|
|
1427
|
+
return this._paused;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* Creates a new {@link MediaStreamVideoTrackSource} from a
|
|
1432
|
+
* [`MediaStreamVideoTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack), which will pull
|
|
1433
|
+
* video samples from the stream in real time and encode them according to {@link VideoEncodingConfig}.
|
|
1434
|
+
*/
|
|
1435
|
+
constructor(
|
|
1436
|
+
track: MediaStreamVideoTrack,
|
|
1437
|
+
encodingConfig: VideoEncodingConfig,
|
|
1438
|
+
options: MediaStreamVideoTrackSourceOptions = {},
|
|
1439
|
+
) {
|
|
1440
|
+
if (!(track instanceof MediaStreamTrack) || track.kind !== 'video') {
|
|
1441
|
+
throw new TypeError('track must be a video MediaStreamTrack.');
|
|
1442
|
+
}
|
|
1443
|
+
validateVideoEncodingConfig(encodingConfig);
|
|
1444
|
+
if (typeof options !== 'object' || !options) {
|
|
1445
|
+
throw new TypeError('options must be an object.');
|
|
1446
|
+
}
|
|
1447
|
+
if (options.frameRate != null && (typeof options.frameRate !== 'number' || options.frameRate <= 0)) {
|
|
1448
|
+
throw new TypeError('options.frameRate, when provided, must be either a positive number or null.');
|
|
1449
|
+
}
|
|
1450
|
+
if (
|
|
1451
|
+
options.timestampBase !== undefined
|
|
1452
|
+
&& options.timestampBase !== 'synced-zero'
|
|
1453
|
+
&& options.timestampBase !== 'zero'
|
|
1454
|
+
&& options.timestampBase !== 'unix'
|
|
1455
|
+
) {
|
|
1456
|
+
throw new TypeError(
|
|
1457
|
+
'options.timestampBase, when provided, must be one of \'synced-zero\', \'zero\', or \'unix\'.',
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
encodingConfig = {
|
|
1462
|
+
...encodingConfig,
|
|
1463
|
+
latencyMode: 'realtime',
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
super(encodingConfig.codec);
|
|
1467
|
+
|
|
1468
|
+
this._options = options;
|
|
1469
|
+
this._encoder = new VideoEncoderWrapper(this, encodingConfig);
|
|
1470
|
+
this._track = track;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/** @internal */
|
|
1474
|
+
override async _start() {
|
|
1475
|
+
if (!this._errorPromiseAccessed) {
|
|
1476
|
+
Logging._warn(
|
|
1477
|
+
'Make sure not to ignore the `errorPromise` field on MediaStreamVideoTrackSource, so that any internal'
|
|
1478
|
+
+ ' errors get bubbled up properly.',
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const frameRate = this._options.frameRate !== undefined
|
|
1483
|
+
? this._options.frameRate
|
|
1484
|
+
: (this._track.getSettings().frameRate ?? null);
|
|
1485
|
+
|
|
1486
|
+
this._abortController = new AbortController();
|
|
1487
|
+
|
|
1488
|
+
let firstVideoFrameTimestamp: number | null = null;
|
|
1489
|
+
let lastFrameTime: number | null = null;
|
|
1490
|
+
let frameCount = 0;
|
|
1491
|
+
let errored = false;
|
|
1492
|
+
let lastSampleTimestamp: number | null = null;
|
|
1493
|
+
let timestampOffset = 0;
|
|
1494
|
+
|
|
1495
|
+
const tick = () => {
|
|
1496
|
+
assert(frameRate !== null);
|
|
1497
|
+
|
|
1498
|
+
if (!this._lastVideoFrame) {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
assert(lastFrameTime !== null);
|
|
1503
|
+
assert(firstVideoFrameTimestamp !== null);
|
|
1504
|
+
|
|
1505
|
+
const now = performance.now();
|
|
1506
|
+
|
|
1507
|
+
// Add as many frames as warranted by the elapsed time.
|
|
1508
|
+
// > instead of >= intentionally because tick() is called before the _lastVideoFrame is changed
|
|
1509
|
+
while (now - lastFrameTime > 1000 / frameRate) {
|
|
1510
|
+
lastFrameTime += 1000 / frameRate;
|
|
1511
|
+
const timestamp = firstVideoFrameTimestamp + frameCount / frameRate;
|
|
1512
|
+
|
|
1513
|
+
const frame = new VideoFrame(this._videoElement ?? this._lastVideoFrame, {
|
|
1514
|
+
timestamp: 1e6 * timestamp,
|
|
1515
|
+
duration: 1e6 / frameRate,
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
addVideoFrame(frame, now);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
if (frameRate !== null) {
|
|
1523
|
+
this._timerHandle = setIntervalUnthrottled(tick, 4); // Run it at 250 Hz
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const onVideoFrame = (videoFrame: VideoFrame) => {
|
|
1527
|
+
if (frameRate === null) {
|
|
1528
|
+
addVideoFrame(videoFrame);
|
|
1529
|
+
} else {
|
|
1530
|
+
const now = performance.now();
|
|
1531
|
+
|
|
1532
|
+
if (!this._lastVideoFrame) {
|
|
1533
|
+
addVideoFrame(videoFrame.clone(), now);
|
|
1534
|
+
lastFrameTime = now;
|
|
1535
|
+
this._lastVideoFrame = videoFrame;
|
|
1536
|
+
} else {
|
|
1537
|
+
tick();
|
|
1538
|
+
this._lastVideoFrame?.close();
|
|
1539
|
+
this._lastVideoFrame = videoFrame;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
const addVideoFrame = (videoFrame: VideoFrame, now = performance.now()) => {
|
|
1545
|
+
if (errored) {
|
|
1546
|
+
videoFrame.close();
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
frameCount++;
|
|
1551
|
+
const currentTimestamp = videoFrame.timestamp / 1e6;
|
|
1552
|
+
|
|
1553
|
+
if (this._paused) {
|
|
1554
|
+
const frameSeen = firstVideoFrameTimestamp !== null;
|
|
1555
|
+
if (frameSeen) {
|
|
1556
|
+
if (lastSampleTimestamp !== null && this._options.timestampBase !== 'unix') {
|
|
1557
|
+
// In addition to dropping this frame, let's also keep track of the time we have lost due to the
|
|
1558
|
+
// pause. Doing it like this instead of simply keeping track of the paused time is better since
|
|
1559
|
+
// it retains the frame rate of the underlying source.
|
|
1560
|
+
const timeDelta = currentTimestamp - lastSampleTimestamp;
|
|
1561
|
+
timestampOffset -= timeDelta;
|
|
1562
|
+
}
|
|
1563
|
+
lastSampleTimestamp = currentTimestamp;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
videoFrame.close();
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (firstVideoFrameTimestamp === null) {
|
|
1571
|
+
firstVideoFrameTimestamp = currentTimestamp;
|
|
1572
|
+
|
|
1573
|
+
let target: number;
|
|
1574
|
+
const timestampBase = this._options.timestampBase ?? 'synced-zero';
|
|
1575
|
+
if (timestampBase === 'unix') {
|
|
1576
|
+
target = Date.now() / 1000;
|
|
1577
|
+
} else if (timestampBase === 'zero') {
|
|
1578
|
+
target = 0;
|
|
1579
|
+
} else {
|
|
1580
|
+
const output = this._connectedTrack!.output;
|
|
1581
|
+
if (output._firstMediaStreamTimestamp === null) {
|
|
1582
|
+
output._firstMediaStreamTimestamp = now / 1000;
|
|
1583
|
+
target = 0;
|
|
1584
|
+
} else {
|
|
1585
|
+
target = now / 1000 - output._firstMediaStreamTimestamp;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
timestampOffset = target - firstVideoFrameTimestamp;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
lastSampleTimestamp = currentTimestamp;
|
|
1593
|
+
|
|
1594
|
+
if (this._encoder.getQueueSize() >= 8) {
|
|
1595
|
+
// Drop frames if the encoder is overloaded
|
|
1596
|
+
videoFrame.close();
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const sample = new VideoSample(videoFrame, {
|
|
1601
|
+
timestamp: currentTimestamp + timestampOffset,
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
void this._encoder.add(sample, true)
|
|
1605
|
+
.catch((error) => {
|
|
1606
|
+
errored = true;
|
|
1607
|
+
|
|
1608
|
+
this._abortController?.abort();
|
|
1609
|
+
this._promiseWithResolvers.reject(error);
|
|
1610
|
+
|
|
1611
|
+
if (this._workerTrackId !== null) {
|
|
1612
|
+
// Tell the worker to stop the track
|
|
1613
|
+
sendMessageToMediaStreamTrackProcessorWorker({
|
|
1614
|
+
type: 'stopTrack',
|
|
1615
|
+
trackId: this._workerTrackId,
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
};
|
|
1620
|
+
|
|
1621
|
+
if (typeof MediaStreamTrackProcessor !== 'undefined') {
|
|
1622
|
+
// We can do it here directly, perfect
|
|
1623
|
+
const processor = new MediaStreamTrackProcessor({ track: this._track });
|
|
1624
|
+
const consumer = new WritableStream<VideoFrame>({ write: onVideoFrame });
|
|
1625
|
+
|
|
1626
|
+
processor.readable.pipeTo(consumer, {
|
|
1627
|
+
signal: this._abortController.signal,
|
|
1628
|
+
}).catch((error) => {
|
|
1629
|
+
// Handle AbortError silently
|
|
1630
|
+
if (error instanceof DOMException && error.name === 'AbortError') return;
|
|
1631
|
+
|
|
1632
|
+
this._promiseWithResolvers.reject(error);
|
|
1633
|
+
});
|
|
1634
|
+
} else {
|
|
1635
|
+
// It might still be supported in a worker, so let's check that
|
|
1636
|
+
const supportedInWorker = await mediaStreamTrackProcessorIsSupportedInWorker();
|
|
1637
|
+
|
|
1638
|
+
if (supportedInWorker) {
|
|
1639
|
+
this._workerTrackId = nextMediaStreamTrackProcessorWorkerId++;
|
|
1640
|
+
|
|
1641
|
+
sendMessageToMediaStreamTrackProcessorWorker({
|
|
1642
|
+
type: 'videoTrack',
|
|
1643
|
+
trackId: this._workerTrackId,
|
|
1644
|
+
track: this._track,
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
this._workerListener = (event: MessageEvent) => {
|
|
1648
|
+
const message = event.data as MediaStreamTrackProcessorWorkerMessage;
|
|
1649
|
+
|
|
1650
|
+
if (message.type === 'videoFrame' && message.trackId === this._workerTrackId) {
|
|
1651
|
+
onVideoFrame(message.videoFrame);
|
|
1652
|
+
} else if (message.type === 'error' && message.trackId === this._workerTrackId) {
|
|
1653
|
+
this._promiseWithResolvers.reject(message.error);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
mediaStreamTrackProcessorWorker!.addEventListener('message', this._workerListener);
|
|
1658
|
+
} else if (frameRate !== null) {
|
|
1659
|
+
// No MediaStreamTrackProcessor support at all (e.g. Firefox), but we have a frame rate, so we can
|
|
1660
|
+
// manually sample from a hidden <video> element instead.
|
|
1661
|
+
const video = document.createElement('video');
|
|
1662
|
+
video.style.position = 'fixed';
|
|
1663
|
+
video.style.left = '-10000px';
|
|
1664
|
+
video.style.top = '-10000px';
|
|
1665
|
+
video.style.width = '1px';
|
|
1666
|
+
video.style.height = '1px';
|
|
1667
|
+
video.style.opacity = '0';
|
|
1668
|
+
video.style.pointerEvents = 'none';
|
|
1669
|
+
video.muted = true;
|
|
1670
|
+
video.srcObject = new MediaStream([this._track]);
|
|
1671
|
+
document.body.appendChild(video);
|
|
1672
|
+
this._videoElement = video;
|
|
1673
|
+
|
|
1674
|
+
video.addEventListener('loadeddata', () => {
|
|
1675
|
+
if (errored || !this._videoElement) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// This will be the first frame
|
|
1680
|
+
const frame = new VideoFrame(video, {
|
|
1681
|
+
timestamp: 1000 * performance.now(),
|
|
1682
|
+
});
|
|
1683
|
+
onVideoFrame(frame);
|
|
1684
|
+
frame.close();
|
|
1685
|
+
}, { once: true });
|
|
1686
|
+
|
|
1687
|
+
void video.play().catch((error) => {
|
|
1688
|
+
errored = true;
|
|
1689
|
+
this._promiseWithResolvers.reject(error);
|
|
1690
|
+
});
|
|
1691
|
+
} else {
|
|
1692
|
+
throw new Error(
|
|
1693
|
+
'When no explicit frame rate is set, MediaStreamTrackProcessor is required; but it\'s not supported'
|
|
1694
|
+
+ ' by this browser.',
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Pauses the capture of video frames - any video frames emitted by the underlying media stream will be ignored
|
|
1702
|
+
* while paused. This does *not* close the underlying `MediaStreamVideoTrack`, it just ignores its output.
|
|
1703
|
+
*/
|
|
1704
|
+
pause() {
|
|
1705
|
+
this._paused = true;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/** Resumes the capture of video frames after being paused. */
|
|
1709
|
+
resume() {
|
|
1710
|
+
this._paused = false;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/** @internal */
|
|
1714
|
+
override async _flushAndClose(forceClose: boolean) {
|
|
1715
|
+
if (this._abortController) {
|
|
1716
|
+
this._abortController.abort();
|
|
1717
|
+
this._abortController = null;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if (this._timerHandle) {
|
|
1721
|
+
clearIntervalUnthrottled(this._timerHandle);
|
|
1722
|
+
}
|
|
1723
|
+
this._lastVideoFrame?.close();
|
|
1724
|
+
|
|
1725
|
+
if (this._videoElement) {
|
|
1726
|
+
this._videoElement.srcObject = null;
|
|
1727
|
+
this._videoElement.remove();
|
|
1728
|
+
this._videoElement = null;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (this._workerTrackId !== null) {
|
|
1732
|
+
assert(this._workerListener);
|
|
1733
|
+
|
|
1734
|
+
sendMessageToMediaStreamTrackProcessorWorker({
|
|
1735
|
+
type: 'stopTrack',
|
|
1736
|
+
trackId: this._workerTrackId,
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
// Wait for the worker to stop the track
|
|
1740
|
+
await new Promise<void>((resolve) => {
|
|
1741
|
+
const listener = (event: MessageEvent) => {
|
|
1742
|
+
const message = event.data as MediaStreamTrackProcessorWorkerMessage;
|
|
1743
|
+
|
|
1744
|
+
if (message.type === 'trackStopped' && message.trackId === this._workerTrackId) {
|
|
1745
|
+
assert(this._workerListener);
|
|
1746
|
+
mediaStreamTrackProcessorWorker!.removeEventListener('message', this._workerListener);
|
|
1747
|
+
mediaStreamTrackProcessorWorker!.removeEventListener('message', listener);
|
|
1748
|
+
|
|
1749
|
+
resolve();
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
mediaStreamTrackProcessorWorker!.addEventListener('message', listener);
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
await this._encoder.flushAndClose(forceClose);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* Base class for audio sources - sources for audio tracks.
|
|
1763
|
+
* @group Media sources
|
|
1764
|
+
* @public
|
|
1765
|
+
*/
|
|
1766
|
+
export abstract class AudioSource extends MediaSource {
|
|
1767
|
+
/** @internal */
|
|
1768
|
+
override _connectedTrack: OutputAudioTrack | null = null;
|
|
1769
|
+
/** @internal */
|
|
1770
|
+
override readonly _codec: AudioCodec;
|
|
1771
|
+
|
|
1772
|
+
/** Internal constructor. */
|
|
1773
|
+
constructor(codec: AudioCodec) {
|
|
1774
|
+
super();
|
|
1775
|
+
|
|
1776
|
+
if (!AUDIO_CODECS.includes(codec)) {
|
|
1777
|
+
throw new TypeError(`Invalid audio codec '${codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
this._codec = codec;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* The most basic audio source; can be used to directly pipe encoded packets into the output file.
|
|
1786
|
+
* @group Media sources
|
|
1787
|
+
* @public
|
|
1788
|
+
*/
|
|
1789
|
+
export class EncodedAudioPacketSource extends AudioSource {
|
|
1790
|
+
/** Creates a new {@link EncodedAudioPacketSource} whose packets are encoded using `codec`. */
|
|
1791
|
+
constructor(codec: AudioCodec) {
|
|
1792
|
+
super(codec);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
/**
|
|
1796
|
+
* Adds an encoded packet to the output audio track. Packets must be added in *decode order*.
|
|
1797
|
+
*
|
|
1798
|
+
* @param meta - Additional metadata from the encoder. You should pass this for the first call, including a valid
|
|
1799
|
+
* decoder config.
|
|
1800
|
+
*
|
|
1801
|
+
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
|
|
1802
|
+
* to respect writer and encoder backpressure.
|
|
1803
|
+
*/
|
|
1804
|
+
add(packet: EncodedPacket, meta?: EncodedAudioChunkMetadata) {
|
|
1805
|
+
if (!(packet instanceof EncodedPacket)) {
|
|
1806
|
+
throw new TypeError('packet must be an EncodedPacket.');
|
|
1807
|
+
}
|
|
1808
|
+
if (packet.isMetadataOnly) {
|
|
1809
|
+
throw new TypeError('Metadata-only packets cannot be added.');
|
|
1810
|
+
}
|
|
1811
|
+
if (meta !== undefined && (!meta || typeof meta !== 'object')) {
|
|
1812
|
+
throw new TypeError('meta, when provided, must be an object.');
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
this._ensureValidAdd();
|
|
1816
|
+
return this._connectedTrack!.output._muxer.addEncodedAudioPacket(this._connectedTrack!, packet, meta);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
class AudioEncoderWrapper {
|
|
1821
|
+
private ensureEncoderPromise: Promise<void> | null = null;
|
|
1822
|
+
private encoderInitialized = false;
|
|
1823
|
+
private encoder: AudioEncoder | null = null;
|
|
1824
|
+
private muxer: Muxer | null = null;
|
|
1825
|
+
private lastNumberOfChannels: number | null = null;
|
|
1826
|
+
private lastSampleRate: number | null = null;
|
|
1827
|
+
|
|
1828
|
+
private isPcmEncoder = false;
|
|
1829
|
+
private outputSampleSize: number | null = null;
|
|
1830
|
+
private writeOutputValue: ((view: DataView, byteOffset: number, value: number) => void) | null = null;
|
|
1831
|
+
|
|
1832
|
+
private customEncoder: CustomAudioEncoder | null = null;
|
|
1833
|
+
private customEncoderCallSerializer = new CallSerializer();
|
|
1834
|
+
private customEncoderQueueSize = 0;
|
|
1835
|
+
|
|
1836
|
+
private lastEndSampleIndex: number | null = null;
|
|
1837
|
+
|
|
1838
|
+
private resampler: AudioResampler | null = null;
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Encoders typically throw their errors "out of band", meaning asynchronously in some other execution context.
|
|
1842
|
+
* However, we want to surface these errors to the user within the normal control flow, so they don't go uncaught.
|
|
1843
|
+
* So, we keep track of the encoder error and throw it as soon as we get the chance.
|
|
1844
|
+
*/
|
|
1845
|
+
private error: unknown = null;
|
|
1846
|
+
private errorSet = false;
|
|
1847
|
+
|
|
1848
|
+
private setError(error: unknown) {
|
|
1849
|
+
if (!this.errorSet) {
|
|
1850
|
+
this.error = error;
|
|
1851
|
+
this.errorSet = true;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
private lastMuxerPromise: Promise<void> = Promise.resolve();
|
|
1856
|
+
private closed = false;
|
|
1857
|
+
|
|
1858
|
+
constructor(private source: AudioSource, private encodingConfig: AudioEncodingConfig) {}
|
|
1859
|
+
|
|
1860
|
+
async add(audioSample: AudioSample, shouldClose: boolean) {
|
|
1861
|
+
try {
|
|
1862
|
+
this.checkForEncoderError();
|
|
1863
|
+
this.source._ensureValidAdd();
|
|
1864
|
+
|
|
1865
|
+
// Ensure audio parameters remain constant
|
|
1866
|
+
if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) {
|
|
1867
|
+
if (
|
|
1868
|
+
audioSample.numberOfChannels !== this.lastNumberOfChannels
|
|
1869
|
+
|| audioSample.sampleRate !== this.lastSampleRate
|
|
1870
|
+
) {
|
|
1871
|
+
throw new Error(
|
|
1872
|
+
`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at`
|
|
1873
|
+
+ ` ${this.lastSampleRate} Hz, got ${audioSample.numberOfChannels} channels at`
|
|
1874
|
+
+ ` ${audioSample.sampleRate} Hz.`,
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
} else {
|
|
1878
|
+
this.lastNumberOfChannels = audioSample.numberOfChannels;
|
|
1879
|
+
this.lastSampleRate = audioSample.sampleRate;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
const config = this.encodingConfig;
|
|
1883
|
+
const needsResample = config.transform?.numberOfChannels !== undefined
|
|
1884
|
+
|| config.transform?.sampleRate !== undefined;
|
|
1885
|
+
|
|
1886
|
+
if (needsResample) {
|
|
1887
|
+
if (!this.resampler) {
|
|
1888
|
+
// Initialize the resampler on first sample
|
|
1889
|
+
this.resampler = new AudioResampler({
|
|
1890
|
+
targetNumberOfChannels: config.transform!.numberOfChannels
|
|
1891
|
+
?? audioSample.numberOfChannels,
|
|
1892
|
+
targetSampleRate: config.transform!.sampleRate
|
|
1893
|
+
?? audioSample.sampleRate,
|
|
1894
|
+
onSample: async (sample) => {
|
|
1895
|
+
await this.processAndEncode(sample, true);
|
|
1896
|
+
},
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
await this.resampler.add(audioSample);
|
|
1901
|
+
} else {
|
|
1902
|
+
await this.processAndEncode(audioSample, shouldClose);
|
|
1903
|
+
}
|
|
1904
|
+
} finally {
|
|
1905
|
+
if (shouldClose) {
|
|
1906
|
+
audioSample.close();
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/**
|
|
1912
|
+
* Runs the process function (if any) and encodes the resulting samples.
|
|
1913
|
+
*/
|
|
1914
|
+
private async processAndEncode(audioSample: AudioSample, shouldClose: boolean) {
|
|
1915
|
+
const config = this.encodingConfig;
|
|
1916
|
+
|
|
1917
|
+
if (
|
|
1918
|
+
config.transform?.sampleFormat !== undefined
|
|
1919
|
+
&& toInterleavedAudioFormat(audioSample.format) !== config.transform.sampleFormat
|
|
1920
|
+
) {
|
|
1921
|
+
// Do a sample format conversion
|
|
1922
|
+
const newSample = audioSampleToInterleavedFormat(audioSample, config.transform.sampleFormat);
|
|
1923
|
+
|
|
1924
|
+
if (shouldClose) {
|
|
1925
|
+
audioSample.close();
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
audioSample = newSample;
|
|
1929
|
+
shouldClose = true;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
if (config.transform?.process) {
|
|
1933
|
+
try {
|
|
1934
|
+
let processed = config.transform.process(audioSample);
|
|
1935
|
+
if (processed instanceof Promise) {
|
|
1936
|
+
processed = await processed;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
if (processed === null) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
if (!Array.isArray(processed)) {
|
|
1944
|
+
processed = [processed];
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
try {
|
|
1948
|
+
for (const sample of processed) {
|
|
1949
|
+
if (!(sample instanceof AudioSample)) {
|
|
1950
|
+
throw new TypeError(
|
|
1951
|
+
'The audio process function must return an AudioSample, null, or an array of'
|
|
1952
|
+
+ ' AudioSamples.',
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
for (const sample of processed) {
|
|
1958
|
+
await this.encodeSample(sample, true);
|
|
1959
|
+
}
|
|
1960
|
+
} finally {
|
|
1961
|
+
// encodeSample closes the samples it was passed; this additionally covers the samples never
|
|
1962
|
+
// reached because an earlier one threw (closing is idempotent)
|
|
1963
|
+
for (const sample of processed) {
|
|
1964
|
+
if (sample instanceof AudioSample) {
|
|
1965
|
+
sample.close();
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
} finally {
|
|
1970
|
+
if (shouldClose) {
|
|
1971
|
+
audioSample.close();
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
} else {
|
|
1975
|
+
await this.encodeSample(audioSample, shouldClose);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Encodes a single audio sample, handling encoder init, gap padding, and backpressure.
|
|
1981
|
+
*/
|
|
1982
|
+
private async encodeSample(audioSample: AudioSample, shouldClose: boolean) {
|
|
1983
|
+
try {
|
|
1984
|
+
if (!this.encoderInitialized) {
|
|
1985
|
+
if (!this.ensureEncoderPromise) {
|
|
1986
|
+
this.ensureEncoder(audioSample);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// No, this "if" statement is not useless. Sometimes, the above call to `ensureEncoder` might have
|
|
1990
|
+
// synchronously completed and the encoder is already initialized. In this case, we don't need to await
|
|
1991
|
+
// the promise anymore. This also fixes nasty async race condition bugs when multiple code paths are
|
|
1992
|
+
// calling this method: It's important that the call that initialized the encoder go through this
|
|
1993
|
+
// code first.
|
|
1994
|
+
if (!this.encoderInitialized) {
|
|
1995
|
+
await this.ensureEncoderPromise;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
assert(this.encoderInitialized);
|
|
1999
|
+
|
|
2000
|
+
if (this.closed) {
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// Handle padding of gaps with silence to avoid audio drift over time, like in
|
|
2005
|
+
// https://github.com/Vanilagy/mediabunny/issues/176
|
|
2006
|
+
// TODO An open question is how encoders deal with the first AudioData having a non-zero timestamp, and with
|
|
2007
|
+
// AudioDatas that have an overlapping timestamp range.
|
|
2008
|
+
{
|
|
2009
|
+
const startSampleIndex = Math.round(
|
|
2010
|
+
audioSample.timestamp * audioSample.sampleRate,
|
|
2011
|
+
);
|
|
2012
|
+
const endSampleIndex = Math.round(
|
|
2013
|
+
(audioSample.timestamp + audioSample.duration) * audioSample.sampleRate,
|
|
2014
|
+
);
|
|
2015
|
+
|
|
2016
|
+
if (this.lastEndSampleIndex === null) {
|
|
2017
|
+
this.lastEndSampleIndex = endSampleIndex;
|
|
2018
|
+
} else {
|
|
2019
|
+
const sampleDiff = startSampleIndex - this.lastEndSampleIndex;
|
|
2020
|
+
|
|
2021
|
+
if (sampleDiff >= 64) {
|
|
2022
|
+
// The gap is big enough, let's add a correction sample
|
|
2023
|
+
const fillSample = new AudioSample({
|
|
2024
|
+
data: new Float32Array(sampleDiff * audioSample.numberOfChannels),
|
|
2025
|
+
format: 'f32-planar',
|
|
2026
|
+
sampleRate: audioSample.sampleRate,
|
|
2027
|
+
numberOfChannels: audioSample.numberOfChannels,
|
|
2028
|
+
numberOfFrames: sampleDiff,
|
|
2029
|
+
timestamp: this.lastEndSampleIndex / audioSample.sampleRate,
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
await this.encodeSample(fillSample, true); // Recursive call
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
this.lastEndSampleIndex += audioSample.numberOfFrames;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
this.encodingConfig.onEncodedSample?.(audioSample);
|
|
2040
|
+
|
|
2041
|
+
if (this.customEncoder) {
|
|
2042
|
+
this.customEncoderQueueSize++;
|
|
2043
|
+
|
|
2044
|
+
// We clone the sample so it cannot be closed on us from the outside before it reaches the encoder
|
|
2045
|
+
const clonedSample = audioSample.clone();
|
|
2046
|
+
|
|
2047
|
+
const promise = this.customEncoderCallSerializer
|
|
2048
|
+
.call(() => this.customEncoder!.encode(clonedSample))
|
|
2049
|
+
.catch((error: unknown) => this.setError(error))
|
|
2050
|
+
.finally(() => {
|
|
2051
|
+
this.customEncoderQueueSize--;
|
|
2052
|
+
clonedSample.close();
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
if (this.customEncoderQueueSize >= 4) {
|
|
2056
|
+
await promise;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
await this.lastMuxerPromise; // Allow the writer to apply backpressure
|
|
2060
|
+
} else if (this.isPcmEncoder) {
|
|
2061
|
+
await this.doPcmEncoding(audioSample, shouldClose);
|
|
2062
|
+
} else {
|
|
2063
|
+
assert(this.encoder);
|
|
2064
|
+
const audioData = audioSample.toAudioData();
|
|
2065
|
+
this.encoder.encode(audioData);
|
|
2066
|
+
audioData.close();
|
|
2067
|
+
|
|
2068
|
+
if (shouldClose) {
|
|
2069
|
+
audioSample.close();
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
if (this.encoder.encodeQueueSize >= 4) {
|
|
2073
|
+
await new Promise(resolve => this.encoder!.addEventListener('dequeue', resolve, { once: true }));
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
await this.lastMuxerPromise; // Allow the writer to apply backpressure
|
|
2077
|
+
}
|
|
2078
|
+
} finally {
|
|
2079
|
+
if (shouldClose) {
|
|
2080
|
+
audioSample.close();
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
private async doPcmEncoding(audioSample: AudioSample, shouldClose: boolean) {
|
|
2086
|
+
assert(this.outputSampleSize);
|
|
2087
|
+
assert(this.writeOutputValue);
|
|
2088
|
+
|
|
2089
|
+
// Need to extract data from the audio data before we close it
|
|
2090
|
+
const { numberOfChannels, numberOfFrames, sampleRate, timestamp } = audioSample;
|
|
2091
|
+
|
|
2092
|
+
const CHUNK_SIZE = 2048;
|
|
2093
|
+
const outputs: {
|
|
2094
|
+
frameCount: number;
|
|
2095
|
+
view: DataView;
|
|
2096
|
+
}[] = [];
|
|
2097
|
+
|
|
2098
|
+
// Prepare all of the output buffers, each being bounded by CHUNK_SIZE so we don't generate huge packets
|
|
2099
|
+
for (let frame = 0; frame < numberOfFrames; frame += CHUNK_SIZE) {
|
|
2100
|
+
const frameCount = Math.min(CHUNK_SIZE, audioSample.numberOfFrames - frame);
|
|
2101
|
+
const outputSize = frameCount * numberOfChannels * this.outputSampleSize;
|
|
2102
|
+
const outputBuffer = new ArrayBuffer(outputSize);
|
|
2103
|
+
const outputView = new DataView(outputBuffer);
|
|
2104
|
+
|
|
2105
|
+
outputs.push({ frameCount, view: outputView });
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const allocationSize = audioSample.allocationSize(({ planeIndex: 0, format: 'f32-planar' }));
|
|
2109
|
+
const floats = new Float32Array(allocationSize / Float32Array.BYTES_PER_ELEMENT);
|
|
2110
|
+
|
|
2111
|
+
for (let i = 0; i < numberOfChannels; i++) {
|
|
2112
|
+
audioSample.copyTo(floats, { planeIndex: i, format: 'f32-planar' });
|
|
2113
|
+
|
|
2114
|
+
for (let j = 0; j < outputs.length; j++) {
|
|
2115
|
+
const { frameCount, view } = outputs[j]!;
|
|
2116
|
+
|
|
2117
|
+
for (let k = 0; k < frameCount; k++) {
|
|
2118
|
+
this.writeOutputValue(
|
|
2119
|
+
view,
|
|
2120
|
+
(k * numberOfChannels + i) * this.outputSampleSize,
|
|
2121
|
+
floats[j * CHUNK_SIZE + k]!,
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
if (shouldClose) {
|
|
2128
|
+
audioSample.close();
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
const meta: EncodedAudioChunkMetadata = {
|
|
2132
|
+
decoderConfig: {
|
|
2133
|
+
codec: this.encodingConfig.codec,
|
|
2134
|
+
numberOfChannels,
|
|
2135
|
+
sampleRate,
|
|
2136
|
+
},
|
|
2137
|
+
};
|
|
2138
|
+
|
|
2139
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
2140
|
+
const { frameCount, view } = outputs[i]!;
|
|
2141
|
+
const outputBuffer = view.buffer;
|
|
2142
|
+
const startFrame = i * CHUNK_SIZE;
|
|
2143
|
+
|
|
2144
|
+
const packet = new EncodedPacket(
|
|
2145
|
+
new Uint8Array(outputBuffer),
|
|
2146
|
+
'key',
|
|
2147
|
+
timestamp + startFrame / sampleRate,
|
|
2148
|
+
frameCount / sampleRate,
|
|
2149
|
+
);
|
|
2150
|
+
|
|
2151
|
+
this.encodingConfig.onEncodedPacket?.(packet, meta);
|
|
2152
|
+
await this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta); // With backpressure
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
private ensureEncoder(audioSample: AudioSample) {
|
|
2157
|
+
this.ensureEncoderPromise = (async () => {
|
|
2158
|
+
const { numberOfChannels, sampleRate } = audioSample;
|
|
2159
|
+
|
|
2160
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
2161
|
+
const quality = resolveQuality(this.encodingConfig.quality, this.encodingConfig.bitrate);
|
|
2162
|
+
|
|
2163
|
+
const encoderConfig = buildAudioEncoderConfig({
|
|
2164
|
+
numberOfChannels,
|
|
2165
|
+
sampleRate,
|
|
2166
|
+
...this.encodingConfig,
|
|
2167
|
+
quality,
|
|
2168
|
+
});
|
|
2169
|
+
this.encodingConfig.onEncoderConfig?.(encoderConfig);
|
|
2170
|
+
|
|
2171
|
+
const MatchingCustomEncoder = customAudioEncoders.find(x => x.supports(
|
|
2172
|
+
this.encodingConfig.codec,
|
|
2173
|
+
encoderConfig,
|
|
2174
|
+
));
|
|
2175
|
+
|
|
2176
|
+
if (MatchingCustomEncoder) {
|
|
2177
|
+
// @ts-expect-error "Can't create instance of abstract class 🤓"
|
|
2178
|
+
this.customEncoder = new MatchingCustomEncoder() as CustomAudioEncoder;
|
|
2179
|
+
// @ts-expect-error It's technically readonly
|
|
2180
|
+
this.customEncoder.codec = this.encodingConfig.codec;
|
|
2181
|
+
// @ts-expect-error It's technically readonly
|
|
2182
|
+
this.customEncoder.config = encoderConfig;
|
|
2183
|
+
// @ts-expect-error It's technically readonly
|
|
2184
|
+
this.customEncoder.onPacket = (packet, meta) => {
|
|
2185
|
+
if (!(packet instanceof EncodedPacket)) {
|
|
2186
|
+
throw new TypeError('The first argument passed to onPacket must be an EncodedPacket.');
|
|
2187
|
+
}
|
|
2188
|
+
if (meta !== undefined && (!meta || typeof meta !== 'object')) {
|
|
2189
|
+
throw new TypeError('The second argument passed to onPacket must be an object or undefined.');
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
this.encodingConfig.onEncodedPacket?.(packet, meta);
|
|
2193
|
+
this.lastMuxerPromise
|
|
2194
|
+
= this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta)
|
|
2195
|
+
.catch((error) => {
|
|
2196
|
+
this.setError(error);
|
|
2197
|
+
});
|
|
2198
|
+
};
|
|
2199
|
+
// @ts-expect-error It's technically readonly
|
|
2200
|
+
this.customEncoder.onError = (error) => {
|
|
2201
|
+
this.setError(error);
|
|
2202
|
+
};
|
|
2203
|
+
|
|
2204
|
+
await this.customEncoder.init();
|
|
2205
|
+
} else if ((PCM_AUDIO_CODECS as readonly string[]).includes(this.encodingConfig.codec)) {
|
|
2206
|
+
this.initPcmEncoder();
|
|
2207
|
+
} else {
|
|
2208
|
+
if (typeof AudioEncoder === 'undefined') {
|
|
2209
|
+
throw new Error('AudioEncoder is not supported by this browser.');
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
const support = await AudioEncoder.isConfigSupported(encoderConfig);
|
|
2213
|
+
if (!support.supported) {
|
|
2214
|
+
throw new Error(
|
|
2215
|
+
`This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps,`
|
|
2216
|
+
+ ` ${encoderConfig.numberOfChannels} channels, ${encoderConfig.sampleRate} Hz) is not`
|
|
2217
|
+
+ ` supported by this browser. Consider using another codec or changing your audio parameters.`,
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const stack = new Error('Encoding error').stack;
|
|
2222
|
+
|
|
2223
|
+
this.encoder = new AudioEncoder({
|
|
2224
|
+
output: (chunk, meta) => {
|
|
2225
|
+
// WebKit emits an invalid description for AAC (https://bugs.webkit.org/show_bug.cgi?id=302253),
|
|
2226
|
+
// which we try to detect here. If detected, we'll provide our own description instead, derived
|
|
2227
|
+
// from the codec string and audio parameters.
|
|
2228
|
+
if (this.encodingConfig.codec === 'aac' && meta?.decoderConfig) {
|
|
2229
|
+
let needsDescriptionOverwrite = false;
|
|
2230
|
+
if (!meta.decoderConfig.description || meta.decoderConfig.description.byteLength < 2) {
|
|
2231
|
+
needsDescriptionOverwrite = true;
|
|
2232
|
+
} else {
|
|
2233
|
+
const audioSpecificConfig = parseAacAudioSpecificConfig(
|
|
2234
|
+
toUint8Array(meta.decoderConfig.description),
|
|
2235
|
+
);
|
|
2236
|
+
|
|
2237
|
+
needsDescriptionOverwrite = audioSpecificConfig.objectType === 0;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
if (needsDescriptionOverwrite) {
|
|
2241
|
+
const objectType = Number(last(encoderConfig.codec.split('.')));
|
|
2242
|
+
|
|
2243
|
+
meta.decoderConfig.description = buildAacAudioSpecificConfig({
|
|
2244
|
+
objectType,
|
|
2245
|
+
numberOfChannels: meta.decoderConfig.numberOfChannels,
|
|
2246
|
+
sampleRate: meta.decoderConfig.sampleRate,
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
let packet = EncodedPacket.fromEncodedChunk(chunk);
|
|
2252
|
+
|
|
2253
|
+
// Snap the timing information to the sample rate to recover information lost in microsecond
|
|
2254
|
+
// conversion
|
|
2255
|
+
packet = packet.clone({
|
|
2256
|
+
timestamp: roundToDivisor(packet.timestamp, encoderConfig.sampleRate),
|
|
2257
|
+
duration: chunk.duration != null
|
|
2258
|
+
? roundToDivisor(packet.duration, encoderConfig.sampleRate)
|
|
2259
|
+
: undefined,
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2262
|
+
this.encodingConfig.onEncodedPacket?.(packet, meta);
|
|
2263
|
+
this.lastMuxerPromise
|
|
2264
|
+
= this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta)
|
|
2265
|
+
.catch((error) => {
|
|
2266
|
+
this.setError(error);
|
|
2267
|
+
});
|
|
2268
|
+
},
|
|
2269
|
+
error: (error) => {
|
|
2270
|
+
error.stack = stack; // Provide a more useful stack trace
|
|
2271
|
+
this.setError(error);
|
|
2272
|
+
},
|
|
2273
|
+
});
|
|
2274
|
+
this.encoder.configure(encoderConfig);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
assert(this.source._connectedTrack);
|
|
2278
|
+
this.muxer = this.source._connectedTrack.output._muxer;
|
|
2279
|
+
|
|
2280
|
+
this.encoderInitialized = true;
|
|
2281
|
+
})();
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
private initPcmEncoder() {
|
|
2285
|
+
this.isPcmEncoder = true;
|
|
2286
|
+
|
|
2287
|
+
const codec = this.encodingConfig.codec as PcmAudioCodec;
|
|
2288
|
+
const { dataType, sampleSize, littleEndian } = parsePcmCodec(codec);
|
|
2289
|
+
|
|
2290
|
+
this.outputSampleSize = sampleSize;
|
|
2291
|
+
|
|
2292
|
+
// All these functions receive a float sample as input and map it into the desired format
|
|
2293
|
+
|
|
2294
|
+
switch (sampleSize) {
|
|
2295
|
+
case 1: {
|
|
2296
|
+
if (dataType === 'unsigned') {
|
|
2297
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2298
|
+
view.setUint8(byteOffset, clamp((value + 1) * 127.5, 0, 255));
|
|
2299
|
+
} else if (dataType === 'signed') {
|
|
2300
|
+
this.writeOutputValue = (view, byteOffset, value) => {
|
|
2301
|
+
view.setInt8(byteOffset, clamp(Math.round(value * 128), -128, 127));
|
|
2302
|
+
};
|
|
2303
|
+
} else if (dataType === 'ulaw') {
|
|
2304
|
+
this.writeOutputValue = (view, byteOffset, value) => {
|
|
2305
|
+
const int16 = clamp(Math.floor(value * 32767), -32768, 32767);
|
|
2306
|
+
view.setUint8(byteOffset, toUlaw(int16));
|
|
2307
|
+
};
|
|
2308
|
+
} else if (dataType === 'alaw') {
|
|
2309
|
+
this.writeOutputValue = (view, byteOffset, value) => {
|
|
2310
|
+
const int16 = clamp(Math.floor(value * 32767), -32768, 32767);
|
|
2311
|
+
view.setUint8(byteOffset, toAlaw(int16));
|
|
2312
|
+
};
|
|
2313
|
+
} else {
|
|
2314
|
+
assert(false);
|
|
2315
|
+
}
|
|
2316
|
+
}; break;
|
|
2317
|
+
case 2: {
|
|
2318
|
+
if (dataType === 'unsigned') {
|
|
2319
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2320
|
+
view.setUint16(byteOffset, clamp((value + 1) * 32767.5, 0, 65535), littleEndian);
|
|
2321
|
+
} else if (dataType === 'signed') {
|
|
2322
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2323
|
+
view.setInt16(byteOffset, clamp(Math.round(value * 32767), -32768, 32767), littleEndian);
|
|
2324
|
+
} else {
|
|
2325
|
+
assert(false);
|
|
2326
|
+
}
|
|
2327
|
+
}; break;
|
|
2328
|
+
case 3: {
|
|
2329
|
+
if (dataType === 'unsigned') {
|
|
2330
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2331
|
+
setUint24(view, byteOffset, clamp((value + 1) * 8388607.5, 0, 16777215), littleEndian);
|
|
2332
|
+
} else if (dataType === 'signed') {
|
|
2333
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2334
|
+
setInt24(
|
|
2335
|
+
view,
|
|
2336
|
+
byteOffset,
|
|
2337
|
+
clamp(Math.round(value * 8388607), -8388608, 8388607),
|
|
2338
|
+
littleEndian,
|
|
2339
|
+
);
|
|
2340
|
+
} else {
|
|
2341
|
+
assert(false);
|
|
2342
|
+
}
|
|
2343
|
+
}; break;
|
|
2344
|
+
case 4: {
|
|
2345
|
+
if (dataType === 'unsigned') {
|
|
2346
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2347
|
+
view.setUint32(byteOffset, clamp((value + 1) * 2147483647.5, 0, 4294967295), littleEndian);
|
|
2348
|
+
} else if (dataType === 'signed') {
|
|
2349
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2350
|
+
view.setInt32(
|
|
2351
|
+
byteOffset,
|
|
2352
|
+
clamp(Math.round(value * 2147483647), -2147483648, 2147483647),
|
|
2353
|
+
littleEndian,
|
|
2354
|
+
);
|
|
2355
|
+
} else if (dataType === 'float') {
|
|
2356
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2357
|
+
view.setFloat32(byteOffset, value, littleEndian);
|
|
2358
|
+
} else {
|
|
2359
|
+
assert(false);
|
|
2360
|
+
}
|
|
2361
|
+
}; break;
|
|
2362
|
+
case 8: {
|
|
2363
|
+
if (dataType === 'float') {
|
|
2364
|
+
this.writeOutputValue = (view, byteOffset, value) =>
|
|
2365
|
+
view.setFloat64(byteOffset, value, littleEndian);
|
|
2366
|
+
} else {
|
|
2367
|
+
assert(false);
|
|
2368
|
+
}
|
|
2369
|
+
}; break;
|
|
2370
|
+
default: {
|
|
2371
|
+
assertNever(sampleSize);
|
|
2372
|
+
assert(false);
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
async flushAndClose(forceClose: boolean) {
|
|
2378
|
+
try {
|
|
2379
|
+
if (!forceClose) {
|
|
2380
|
+
this.checkForEncoderError();
|
|
2381
|
+
|
|
2382
|
+
// Finalize the resampler to flush any buffered audio
|
|
2383
|
+
if (this.resampler) {
|
|
2384
|
+
await this.resampler.finalize();
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
this.closed = true;
|
|
2389
|
+
|
|
2390
|
+
if (!forceClose) {
|
|
2391
|
+
if (this.customEncoder) {
|
|
2392
|
+
void this.customEncoderCallSerializer.call(() => this.customEncoder!.flush());
|
|
2393
|
+
} else if (this.encoder) {
|
|
2394
|
+
await this.encoder.flush();
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
} finally {
|
|
2398
|
+
// This cleanup must also run when flushing threw (e.g. due to an encoder error), otherwise the encoder
|
|
2399
|
+
// would be left dangling
|
|
2400
|
+
this.closed = true;
|
|
2401
|
+
this.resampler = null;
|
|
2402
|
+
|
|
2403
|
+
if (this.customEncoder) {
|
|
2404
|
+
await this.customEncoderCallSerializer.call(() => this.customEncoder!.close())
|
|
2405
|
+
.catch((error: unknown) => this.setError(error));
|
|
2406
|
+
} else if (this.encoder && this.encoder.state !== 'closed') {
|
|
2407
|
+
this.encoder.close();
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
if (!forceClose) {
|
|
2412
|
+
this.checkForEncoderError();
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
getQueueSize() {
|
|
2417
|
+
if (this.customEncoder) {
|
|
2418
|
+
return this.customEncoderQueueSize;
|
|
2419
|
+
} else if (this.isPcmEncoder) {
|
|
2420
|
+
return 0;
|
|
2421
|
+
} else {
|
|
2422
|
+
return this.encoder?.encodeQueueSize ?? 0;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
checkForEncoderError() {
|
|
2427
|
+
if (this.errorSet) {
|
|
2428
|
+
throw this.error;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* This source can be used to add raw, unencoded audio samples to an output audio track. These samples will
|
|
2435
|
+
* automatically be encoded and then piped into the output.
|
|
2436
|
+
* @group Media sources
|
|
2437
|
+
* @public
|
|
2438
|
+
*/
|
|
2439
|
+
export class AudioSampleSource extends AudioSource {
|
|
2440
|
+
/** @internal */
|
|
2441
|
+
private _encoder: AudioEncoderWrapper;
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* Creates a new {@link AudioSampleSource} whose samples are encoded according to the specified
|
|
2445
|
+
* {@link AudioEncodingConfig}.
|
|
2446
|
+
*/
|
|
2447
|
+
constructor(encodingConfig: AudioEncodingConfig) {
|
|
2448
|
+
validateAudioEncodingConfig(encodingConfig);
|
|
2449
|
+
|
|
2450
|
+
super(encodingConfig.codec);
|
|
2451
|
+
this._encoder = new AudioEncoderWrapper(this, encodingConfig);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* Encodes an audio sample and then adds it to the output.
|
|
2456
|
+
*
|
|
2457
|
+
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
|
|
2458
|
+
* to respect writer and encoder backpressure.
|
|
2459
|
+
*/
|
|
2460
|
+
add(audioSample: AudioSample) {
|
|
2461
|
+
if (!(audioSample instanceof AudioSample)) {
|
|
2462
|
+
throw new TypeError('audioSample must be an AudioSample.');
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
return this._encoder.add(audioSample, false);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/** @internal */
|
|
2469
|
+
override _flushAndClose(forceClose: boolean) {
|
|
2470
|
+
return this._encoder.flushAndClose(forceClose);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* This source can be used to add audio data from an AudioBuffer to the output track. This is useful when working with
|
|
2476
|
+
* the Web Audio API.
|
|
2477
|
+
* @group Media sources
|
|
2478
|
+
* @public
|
|
2479
|
+
*/
|
|
2480
|
+
export class AudioBufferSource extends AudioSource {
|
|
2481
|
+
/** @internal */
|
|
2482
|
+
private _encoder: AudioEncoderWrapper;
|
|
2483
|
+
/** @internal */
|
|
2484
|
+
private _accumulatedTime = 0;
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Creates a new {@link AudioBufferSource} whose `AudioBuffer` instances are encoded according to the specified
|
|
2488
|
+
* {@link AudioEncodingConfig}.
|
|
2489
|
+
*/
|
|
2490
|
+
constructor(encodingConfig: AudioEncodingConfig) {
|
|
2491
|
+
validateAudioEncodingConfig(encodingConfig);
|
|
2492
|
+
|
|
2493
|
+
super(encodingConfig.codec);
|
|
2494
|
+
this._encoder = new AudioEncoderWrapper(this, encodingConfig);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
/**
|
|
2498
|
+
* Converts an AudioBuffer to audio samples, encodes them and adds them to the output. The first AudioBuffer will
|
|
2499
|
+
* be played at timestamp 0, and any subsequent AudioBuffer will have a timestamp equal to the total duration of
|
|
2500
|
+
* all previous AudioBuffers.
|
|
2501
|
+
*
|
|
2502
|
+
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
|
|
2503
|
+
* to respect writer and encoder backpressure.
|
|
2504
|
+
*/
|
|
2505
|
+
async add(audioBuffer: AudioBuffer) {
|
|
2506
|
+
if (!(audioBuffer instanceof AudioBuffer)) {
|
|
2507
|
+
throw new TypeError('audioBuffer must be an AudioBuffer.');
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
const iterator = AudioSample._fromAudioBuffer(audioBuffer, this._accumulatedTime);
|
|
2511
|
+
this._accumulatedTime += audioBuffer.duration;
|
|
2512
|
+
|
|
2513
|
+
for (const audioSample of iterator) {
|
|
2514
|
+
await this._encoder.add(audioSample, true);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
/** @internal */
|
|
2519
|
+
override _flushAndClose(forceClose: boolean) {
|
|
2520
|
+
return this._encoder.flushAndClose(forceClose);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* Options for {@link MediaStreamAudioTrackSource}.
|
|
2526
|
+
* @group Media sources
|
|
2527
|
+
* @public
|
|
2528
|
+
*/
|
|
2529
|
+
export type MediaStreamAudioTrackSourceOptions = {
|
|
2530
|
+
/**
|
|
2531
|
+
* Controls the basis (zero point) for audio sample timestamps.
|
|
2532
|
+
*
|
|
2533
|
+
* When set to `'synced-zero'`, timestamps will be relative to the first chunk of media from a `MediaStreamTrack`
|
|
2534
|
+
* added to the {@link Output}.
|
|
2535
|
+
*
|
|
2536
|
+
* When set to `'zero'`, timestamps will be relative to the first audio sample emitted by this source.
|
|
2537
|
+
*
|
|
2538
|
+
* When set to `'unix'`, timestamps will be relative to the Unix epoch, so clearly associated with a distinct point
|
|
2539
|
+
* in time. Here, pausing via {@link MediaStreamAudioTrackSource.pause} will also create gaps in timestamps. Be sure
|
|
2540
|
+
* to pair this mode with {@link BaseTrackMetadata.isRelativeToUnixEpoch}.
|
|
2541
|
+
*
|
|
2542
|
+
* Defaults to `'synced-zero'`.
|
|
2543
|
+
*/
|
|
2544
|
+
timestampBase?: 'synced-zero' | 'zero' | 'unix';
|
|
2545
|
+
};
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* Audio source that encodes the data of a
|
|
2549
|
+
* [`MediaStreamAudioTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) and pipes it into the
|
|
2550
|
+
* output. This is useful for capturing live or real-time audio such as microphones or audio from other media elements.
|
|
2551
|
+
* Audio will automatically start being captured once the connected {@link Output} is started, and will keep being
|
|
2552
|
+
* captured until the {@link Output} is finalized or this source is closed.
|
|
2553
|
+
* @group Media sources
|
|
2554
|
+
* @public
|
|
2555
|
+
*/
|
|
2556
|
+
export class MediaStreamAudioTrackSource extends AudioSource {
|
|
2557
|
+
/** @internal */
|
|
2558
|
+
private _options: MediaStreamAudioTrackSourceOptions;
|
|
2559
|
+
/** @internal */
|
|
2560
|
+
private _encoder: AudioEncoderWrapper;
|
|
2561
|
+
/** @internal */
|
|
2562
|
+
private _abortController: AbortController | null = null;
|
|
2563
|
+
/** @internal */
|
|
2564
|
+
private _track: MediaStreamAudioTrack;
|
|
2565
|
+
/** @internal */
|
|
2566
|
+
private _audioContext: AudioContext | null = null;
|
|
2567
|
+
/** @internal */
|
|
2568
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
2569
|
+
private _scriptProcessorNode: ScriptProcessorNode | null = null; // Deprecated but goated
|
|
2570
|
+
/** @internal */
|
|
2571
|
+
private _promiseWithResolvers = promiseWithResolvers();
|
|
2572
|
+
/** @internal */
|
|
2573
|
+
private _errorPromiseAccessed = false;
|
|
2574
|
+
/** @internal */
|
|
2575
|
+
private _paused = false;
|
|
2576
|
+
|
|
2577
|
+
/** A promise that rejects upon any error within this source. This promise never resolves. */
|
|
2578
|
+
get errorPromise() {
|
|
2579
|
+
this._errorPromiseAccessed = true;
|
|
2580
|
+
return this._promiseWithResolvers.promise;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/** Whether this source is currently paused as a result of calling `.pause()`. */
|
|
2584
|
+
get paused() {
|
|
2585
|
+
return this._paused;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
/**
|
|
2589
|
+
* Creates a new {@link MediaStreamAudioTrackSource} from a `MediaStreamAudioTrack`, which will pull audio samples
|
|
2590
|
+
* from the stream in real time and encode them according to {@link AudioEncodingConfig}.
|
|
2591
|
+
*/
|
|
2592
|
+
constructor(
|
|
2593
|
+
track: MediaStreamAudioTrack,
|
|
2594
|
+
encodingConfig: AudioEncodingConfig,
|
|
2595
|
+
options: MediaStreamAudioTrackSourceOptions = {},
|
|
2596
|
+
) {
|
|
2597
|
+
if (!(track instanceof MediaStreamTrack) || track.kind !== 'audio') {
|
|
2598
|
+
throw new TypeError('track must be an audio MediaStreamTrack.');
|
|
2599
|
+
}
|
|
2600
|
+
validateAudioEncodingConfig(encodingConfig);
|
|
2601
|
+
if (typeof options !== 'object' || !options) {
|
|
2602
|
+
throw new TypeError('options must be an object.');
|
|
2603
|
+
}
|
|
2604
|
+
if (
|
|
2605
|
+
options.timestampBase !== undefined
|
|
2606
|
+
&& options.timestampBase !== 'synced-zero'
|
|
2607
|
+
&& options.timestampBase !== 'zero'
|
|
2608
|
+
&& options.timestampBase !== 'unix'
|
|
2609
|
+
) {
|
|
2610
|
+
throw new TypeError(
|
|
2611
|
+
'options.timestampBase, when provided, must be one of \'synced-zero\', \'zero\', or \'unix\'.',
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
super(encodingConfig.codec);
|
|
2616
|
+
this._options = options;
|
|
2617
|
+
this._encoder = new AudioEncoderWrapper(this, encodingConfig);
|
|
2618
|
+
this._track = track;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
/** @internal */
|
|
2622
|
+
override async _start() {
|
|
2623
|
+
if (!this._errorPromiseAccessed) {
|
|
2624
|
+
Logging._warn(
|
|
2625
|
+
'Make sure not to ignore the `errorPromise` field on MediaStreamAudioTrackSource, so that any internal'
|
|
2626
|
+
+ ' errors get bubbled up properly.',
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
this._abortController = new AbortController();
|
|
2631
|
+
|
|
2632
|
+
let firstAudioDataTimestamp: number | null = null;
|
|
2633
|
+
let errored = false;
|
|
2634
|
+
let lastSampleTimestamp: number | null = null;
|
|
2635
|
+
let timestampOffset = 0;
|
|
2636
|
+
|
|
2637
|
+
const onAudioSample = (audioSample: AudioSample) => {
|
|
2638
|
+
if (errored) {
|
|
2639
|
+
audioSample.close();
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
const currentTimestamp = audioSample.timestamp;
|
|
2644
|
+
|
|
2645
|
+
if (this._paused) {
|
|
2646
|
+
const dataSeen = firstAudioDataTimestamp !== null;
|
|
2647
|
+
if (dataSeen) {
|
|
2648
|
+
if (lastSampleTimestamp !== null && this._options.timestampBase !== 'unix') {
|
|
2649
|
+
// In addition to dropping this sample, let's also keep track of the time we have lost due to
|
|
2650
|
+
// the pause. Doing it like this instead of simply keeping track of the paused time is better
|
|
2651
|
+
// since it retains the sample rate of the underlying source.
|
|
2652
|
+
const timeDelta = currentTimestamp - lastSampleTimestamp;
|
|
2653
|
+
timestampOffset -= timeDelta;
|
|
2654
|
+
}
|
|
2655
|
+
lastSampleTimestamp = currentTimestamp;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
audioSample.close();
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
if (firstAudioDataTimestamp === null) {
|
|
2663
|
+
firstAudioDataTimestamp = audioSample.timestamp;
|
|
2664
|
+
|
|
2665
|
+
let target: number;
|
|
2666
|
+
const timestampBase = this._options.timestampBase ?? 'synced-zero';
|
|
2667
|
+
if (timestampBase === 'unix') {
|
|
2668
|
+
target = Date.now() / 1000;
|
|
2669
|
+
} else if (timestampBase === 'zero') {
|
|
2670
|
+
target = 0;
|
|
2671
|
+
} else {
|
|
2672
|
+
const output = this._connectedTrack!.output;
|
|
2673
|
+
if (output._firstMediaStreamTimestamp === null) {
|
|
2674
|
+
output._firstMediaStreamTimestamp = performance.now() / 1000;
|
|
2675
|
+
target = 0;
|
|
2676
|
+
} else {
|
|
2677
|
+
target = performance.now() / 1000 - output._firstMediaStreamTimestamp;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
timestampOffset = target - firstAudioDataTimestamp;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
lastSampleTimestamp = currentTimestamp;
|
|
2685
|
+
|
|
2686
|
+
if (this._encoder.getQueueSize() >= 8) {
|
|
2687
|
+
// Drop data if the encoder is overloaded
|
|
2688
|
+
audioSample.close();
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
audioSample.setTimestamp(currentTimestamp + timestampOffset);
|
|
2693
|
+
|
|
2694
|
+
void this._encoder.add(audioSample, true)
|
|
2695
|
+
.catch((error) => {
|
|
2696
|
+
errored = true;
|
|
2697
|
+
|
|
2698
|
+
this._abortController?.abort();
|
|
2699
|
+
this._promiseWithResolvers.reject(error);
|
|
2700
|
+
void this._audioContext?.suspend();
|
|
2701
|
+
});
|
|
2702
|
+
};
|
|
2703
|
+
|
|
2704
|
+
if (typeof MediaStreamTrackProcessor !== 'undefined') {
|
|
2705
|
+
// Great, MediaStreamTrackProcessor is supported, this is the preferred way of doing things
|
|
2706
|
+
const processor = new MediaStreamTrackProcessor({ track: this._track });
|
|
2707
|
+
const consumer = new WritableStream<AudioData>({
|
|
2708
|
+
write: audioData => onAudioSample(new AudioSample(audioData)),
|
|
2709
|
+
});
|
|
2710
|
+
|
|
2711
|
+
processor.readable.pipeTo(consumer, {
|
|
2712
|
+
signal: this._abortController.signal,
|
|
2713
|
+
}).catch((error) => {
|
|
2714
|
+
// Handle AbortError silently
|
|
2715
|
+
if (error instanceof DOMException && error.name === 'AbortError') return;
|
|
2716
|
+
|
|
2717
|
+
this._promiseWithResolvers.reject(error);
|
|
2718
|
+
});
|
|
2719
|
+
} else {
|
|
2720
|
+
// Let's fall back to an AudioContext approach
|
|
2721
|
+
|
|
2722
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
2723
|
+
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
|
|
2724
|
+
|
|
2725
|
+
this._audioContext = new AudioContext({ sampleRate: this._track.getSettings().sampleRate });
|
|
2726
|
+
const sourceNode = this._audioContext.createMediaStreamSource(new MediaStream([this._track]));
|
|
2727
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
2728
|
+
this._scriptProcessorNode = this._audioContext.createScriptProcessor(4096);
|
|
2729
|
+
|
|
2730
|
+
if (this._audioContext.state === 'suspended') {
|
|
2731
|
+
await this._audioContext.resume();
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
sourceNode.connect(this._scriptProcessorNode);
|
|
2735
|
+
this._scriptProcessorNode.connect(this._audioContext.destination);
|
|
2736
|
+
|
|
2737
|
+
let totalDuration = 0;
|
|
2738
|
+
|
|
2739
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
2740
|
+
this._scriptProcessorNode.onaudioprocess = (event) => {
|
|
2741
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
2742
|
+
const iterator = AudioSample._fromAudioBuffer(event.inputBuffer, totalDuration);
|
|
2743
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
2744
|
+
totalDuration += event.inputBuffer.duration;
|
|
2745
|
+
|
|
2746
|
+
for (const audioSample of iterator) {
|
|
2747
|
+
onAudioSample(audioSample);
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* Pauses the capture of audio data - any audio data emitted by the underlying media stream will be ignored
|
|
2755
|
+
* while paused. This does *not* close the underlying `MediaStreamAudioTrack`, it just ignores its output.
|
|
2756
|
+
*/
|
|
2757
|
+
pause() {
|
|
2758
|
+
this._paused = true;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
/** Resumes the capture of audio data after being paused. */
|
|
2762
|
+
resume() {
|
|
2763
|
+
this._paused = false;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
/** @internal */
|
|
2767
|
+
override async _flushAndClose(forceClose: boolean) {
|
|
2768
|
+
if (this._abortController) {
|
|
2769
|
+
this._abortController.abort();
|
|
2770
|
+
this._abortController = null;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
if (this._audioContext) {
|
|
2774
|
+
assert(this._scriptProcessorNode);
|
|
2775
|
+
|
|
2776
|
+
this._scriptProcessorNode.disconnect();
|
|
2777
|
+
await this._audioContext.suspend();
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
await this._encoder.flushAndClose(forceClose);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// === MEDIA STREAM TRACK PROCESSOR WORKER ===
|
|
2785
|
+
|
|
2786
|
+
type MediaStreamTrackProcessorWorkerMessage = {
|
|
2787
|
+
type: 'support';
|
|
2788
|
+
supported: boolean;
|
|
2789
|
+
} | {
|
|
2790
|
+
type: 'videoFrame';
|
|
2791
|
+
trackId: number;
|
|
2792
|
+
videoFrame: VideoFrame;
|
|
2793
|
+
} | {
|
|
2794
|
+
type: 'trackStopped';
|
|
2795
|
+
trackId: number;
|
|
2796
|
+
} | {
|
|
2797
|
+
type: 'error';
|
|
2798
|
+
trackId: number;
|
|
2799
|
+
error: unknown;
|
|
2800
|
+
};
|
|
2801
|
+
|
|
2802
|
+
type MediaStreamTrackProcessorControllerMessage = {
|
|
2803
|
+
type: 'videoTrack';
|
|
2804
|
+
trackId: number;
|
|
2805
|
+
track: MediaStreamVideoTrack;
|
|
2806
|
+
} | {
|
|
2807
|
+
type: 'stopTrack';
|
|
2808
|
+
trackId: number;
|
|
2809
|
+
};
|
|
2810
|
+
|
|
2811
|
+
const mediaStreamTrackProcessorWorkerCode = () => {
|
|
2812
|
+
const sendMessage = (message: MediaStreamTrackProcessorWorkerMessage, transfer?: Transferable[]) => {
|
|
2813
|
+
if (transfer) {
|
|
2814
|
+
self.postMessage(message, { transfer });
|
|
2815
|
+
} else {
|
|
2816
|
+
self.postMessage(message);
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
|
|
2820
|
+
// Immediately send a message to the main thread, letting them know of the support
|
|
2821
|
+
sendMessage({
|
|
2822
|
+
type: 'support',
|
|
2823
|
+
supported: typeof MediaStreamTrackProcessor !== 'undefined',
|
|
2824
|
+
});
|
|
2825
|
+
|
|
2826
|
+
const abortControllers = new Map<number, AbortController>();
|
|
2827
|
+
const activeTracks = new Map<number, MediaStreamVideoTrack>();
|
|
2828
|
+
|
|
2829
|
+
self.addEventListener('message', (event) => {
|
|
2830
|
+
const message = event.data as MediaStreamTrackProcessorControllerMessage;
|
|
2831
|
+
|
|
2832
|
+
switch (message.type) {
|
|
2833
|
+
case 'videoTrack': {
|
|
2834
|
+
activeTracks.set(message.trackId, message.track);
|
|
2835
|
+
|
|
2836
|
+
const processor = new MediaStreamTrackProcessor({ track: message.track });
|
|
2837
|
+
const consumer = new WritableStream<VideoFrame>({
|
|
2838
|
+
write: (videoFrame) => {
|
|
2839
|
+
if (!activeTracks.has(message.trackId)) {
|
|
2840
|
+
videoFrame.close();
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// Send it to the main thread
|
|
2845
|
+
sendMessage({
|
|
2846
|
+
type: 'videoFrame',
|
|
2847
|
+
trackId: message.trackId,
|
|
2848
|
+
videoFrame,
|
|
2849
|
+
}, [videoFrame]);
|
|
2850
|
+
},
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2853
|
+
const abortController = new AbortController();
|
|
2854
|
+
abortControllers.set(message.trackId, abortController);
|
|
2855
|
+
|
|
2856
|
+
processor.readable.pipeTo(consumer, {
|
|
2857
|
+
signal: abortController.signal,
|
|
2858
|
+
}).catch((error: unknown) => {
|
|
2859
|
+
// Handle AbortError silently
|
|
2860
|
+
if (error instanceof DOMException && error.name === 'AbortError') return;
|
|
2861
|
+
|
|
2862
|
+
sendMessage({
|
|
2863
|
+
type: 'error',
|
|
2864
|
+
trackId: message.trackId,
|
|
2865
|
+
error,
|
|
2866
|
+
});
|
|
2867
|
+
});
|
|
2868
|
+
}; break;
|
|
2869
|
+
|
|
2870
|
+
case 'stopTrack': {
|
|
2871
|
+
const abortController = abortControllers.get(message.trackId);
|
|
2872
|
+
if (abortController) {
|
|
2873
|
+
abortController.abort();
|
|
2874
|
+
abortControllers.delete(message.trackId);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
const track = activeTracks.get(message.trackId);
|
|
2878
|
+
track?.stop();
|
|
2879
|
+
activeTracks.delete(message.trackId);
|
|
2880
|
+
|
|
2881
|
+
sendMessage({
|
|
2882
|
+
type: 'trackStopped',
|
|
2883
|
+
trackId: message.trackId,
|
|
2884
|
+
});
|
|
2885
|
+
}; break;
|
|
2886
|
+
|
|
2887
|
+
default: assertNever(message);
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
};
|
|
2891
|
+
|
|
2892
|
+
let nextMediaStreamTrackProcessorWorkerId = 0;
|
|
2893
|
+
let mediaStreamTrackProcessorWorker: Worker | null = null;
|
|
2894
|
+
|
|
2895
|
+
const initMediaStreamTrackProcessorWorker = () => {
|
|
2896
|
+
const blob = new Blob(
|
|
2897
|
+
[`(${mediaStreamTrackProcessorWorkerCode.toString()})()`],
|
|
2898
|
+
{ type: 'application/javascript' },
|
|
2899
|
+
);
|
|
2900
|
+
const url = URL.createObjectURL(blob);
|
|
2901
|
+
|
|
2902
|
+
mediaStreamTrackProcessorWorker = new Worker(url);
|
|
2903
|
+
};
|
|
2904
|
+
|
|
2905
|
+
let mediaStreamTrackProcessorIsSupportedInWorkerCache: boolean | null = null;
|
|
2906
|
+
const mediaStreamTrackProcessorIsSupportedInWorker = async () => {
|
|
2907
|
+
if (mediaStreamTrackProcessorIsSupportedInWorkerCache !== null) {
|
|
2908
|
+
return mediaStreamTrackProcessorIsSupportedInWorkerCache;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
if (!mediaStreamTrackProcessorWorker) {
|
|
2912
|
+
initMediaStreamTrackProcessorWorker();
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
return new Promise<boolean>((resolve) => {
|
|
2916
|
+
assert(mediaStreamTrackProcessorWorker);
|
|
2917
|
+
|
|
2918
|
+
const listener = (event: MessageEvent) => {
|
|
2919
|
+
const message = event.data as MediaStreamTrackProcessorWorkerMessage;
|
|
2920
|
+
|
|
2921
|
+
if (message.type === 'support') {
|
|
2922
|
+
mediaStreamTrackProcessorIsSupportedInWorkerCache = message.supported;
|
|
2923
|
+
mediaStreamTrackProcessorWorker!.removeEventListener('message', listener);
|
|
2924
|
+
|
|
2925
|
+
resolve(message.supported);
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
|
|
2929
|
+
mediaStreamTrackProcessorWorker.addEventListener('message', listener);
|
|
2930
|
+
});
|
|
2931
|
+
};
|
|
2932
|
+
|
|
2933
|
+
const sendMessageToMediaStreamTrackProcessorWorker = (
|
|
2934
|
+
message: MediaStreamTrackProcessorControllerMessage,
|
|
2935
|
+
transfer?: Transferable[],
|
|
2936
|
+
) => {
|
|
2937
|
+
assert(mediaStreamTrackProcessorWorker);
|
|
2938
|
+
|
|
2939
|
+
if (transfer) {
|
|
2940
|
+
mediaStreamTrackProcessorWorker.postMessage(message, transfer);
|
|
2941
|
+
} else {
|
|
2942
|
+
mediaStreamTrackProcessorWorker.postMessage(message);
|
|
2943
|
+
}
|
|
2944
|
+
};
|
|
2945
|
+
|
|
2946
|
+
/**
|
|
2947
|
+
* Base class for subtitle sources - sources for subtitle tracks.
|
|
2948
|
+
* @group Media sources
|
|
2949
|
+
* @public
|
|
2950
|
+
*/
|
|
2951
|
+
export abstract class SubtitleSource extends MediaSource {
|
|
2952
|
+
/** @internal */
|
|
2953
|
+
override _connectedTrack: OutputSubtitleTrack | null = null;
|
|
2954
|
+
/** @internal */
|
|
2955
|
+
override readonly _codec: SubtitleCodec;
|
|
2956
|
+
|
|
2957
|
+
/** Internal constructor. */
|
|
2958
|
+
constructor(codec: SubtitleCodec) {
|
|
2959
|
+
super();
|
|
2960
|
+
|
|
2961
|
+
if (!SUBTITLE_CODECS.includes(codec)) {
|
|
2962
|
+
throw new TypeError(`Invalid subtitle codec '${codec}'. Must be one of: ${SUBTITLE_CODECS.join(', ')}.`);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
this._codec = codec;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
/**
|
|
2970
|
+
* This source can be used to add subtitles from a subtitle text file.
|
|
2971
|
+
* @group Media sources
|
|
2972
|
+
* @public
|
|
2973
|
+
*/
|
|
2974
|
+
export class TextSubtitleSource extends SubtitleSource {
|
|
2975
|
+
/** @internal */
|
|
2976
|
+
private _parser: SubtitleParser;
|
|
2977
|
+
/** @internal */
|
|
2978
|
+
private _error: unknown = null;
|
|
2979
|
+
/** @internal */
|
|
2980
|
+
private _errorSet = false;
|
|
2981
|
+
/** @internal */
|
|
2982
|
+
private _lastMuxerPromise: Promise<void> = Promise.resolve();
|
|
2983
|
+
|
|
2984
|
+
/** Creates a new {@link TextSubtitleSource} where added text chunks are in the specified `codec`. */
|
|
2985
|
+
constructor(codec: SubtitleCodec) {
|
|
2986
|
+
super(codec);
|
|
2987
|
+
|
|
2988
|
+
this._parser = new SubtitleParser({
|
|
2989
|
+
codec,
|
|
2990
|
+
output: (cue, metadata) => {
|
|
2991
|
+
this._lastMuxerPromise
|
|
2992
|
+
= this._connectedTrack!.output._muxer.addSubtitleCue(this._connectedTrack!, cue, metadata)
|
|
2993
|
+
.catch((error) => {
|
|
2994
|
+
this._setError(error);
|
|
2995
|
+
});
|
|
2996
|
+
},
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
/**
|
|
3001
|
+
* Parses the subtitle text according to the specified codec and adds it to the output track. You don't have to
|
|
3002
|
+
* add the entire subtitle file at once here; you can provide it in chunks.
|
|
3003
|
+
*
|
|
3004
|
+
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
|
|
3005
|
+
* to respect writer and encoder backpressure.
|
|
3006
|
+
*/
|
|
3007
|
+
add(text: string) {
|
|
3008
|
+
if (typeof text !== 'string') {
|
|
3009
|
+
throw new TypeError('text must be a string.');
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
this._checkForError();
|
|
3013
|
+
|
|
3014
|
+
this._ensureValidAdd();
|
|
3015
|
+
this._parser.parse(text);
|
|
3016
|
+
|
|
3017
|
+
return this._lastMuxerPromise; // Allow the writer to apply backpressure
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
/** @internal */
|
|
3021
|
+
private _setError(error: unknown) {
|
|
3022
|
+
if (!this._errorSet) {
|
|
3023
|
+
this._error = error;
|
|
3024
|
+
this._errorSet = true;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
/** @internal */
|
|
3029
|
+
_checkForError() {
|
|
3030
|
+
if (this._errorSet) {
|
|
3031
|
+
throw this._error;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
/** @internal */
|
|
3036
|
+
override async _flushAndClose(forceClose: boolean) {
|
|
3037
|
+
if (!forceClose) {
|
|
3038
|
+
this._checkForError();
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
}
|