From 57595b641571e4e132fd820ef652b00100ba759c Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 01:46:13 +0800 Subject: [PATCH 01/28] use rust workspace with crates/ folder --- .gitignore | 3 +- Cargo.lock | 6522 +++++++++++++++++ Cargo.toml | 4 + apps/desktop/src-tauri/Cargo.toml | 33 +- .../src => crates}/capture/Cargo.lock | 0 .../src => crates}/capture/Cargo.toml | 0 .../src => crates}/capture/README.md | 0 .../src-tauri/src => crates}/capture/build.rs | 0 .../src => crates}/capture/src/common/dxgi.rs | 0 .../src => crates}/capture/src/common/mod.rs | 0 .../capture/src/common/quartz.rs | 0 .../src => crates}/capture/src/common/x11.rs | 0 .../src => crates}/capture/src/dxgi/ffi.rs | 0 .../src => crates}/capture/src/dxgi/mod.rs | 0 .../src => crates}/capture/src/lib.rs | 0 .../capture/src/quartz/capturer.rs | 0 .../capture/src/quartz/config.rs | 0 .../capture/src/quartz/display.rs | 0 .../src => crates}/capture/src/quartz/ffi.rs | 0 .../capture/src/quartz/frame.rs | 0 .../src => crates}/capture/src/quartz/mod.rs | 0 .../capture/src/x11/capturer.rs | 0 .../src => crates}/capture/src/x11/display.rs | 0 .../src => crates}/capture/src/x11/ffi.rs | 0 .../src => crates}/capture/src/x11/iter.rs | 0 .../src => crates}/capture/src/x11/mod.rs | 0 .../src => crates}/capture/src/x11/server.rs | 0 27 files changed, 6559 insertions(+), 3 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml rename {apps/desktop/src-tauri/src => crates}/capture/Cargo.lock (100%) rename {apps/desktop/src-tauri/src => crates}/capture/Cargo.toml (100%) rename {apps/desktop/src-tauri/src => crates}/capture/README.md (100%) rename {apps/desktop/src-tauri/src => crates}/capture/build.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/common/dxgi.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/common/mod.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/common/quartz.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/common/x11.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/dxgi/ffi.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/dxgi/mod.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/lib.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/capturer.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/config.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/display.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/ffi.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/frame.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/mod.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/capturer.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/display.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/ffi.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/iter.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/mod.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/server.rs (100%) diff --git a/.gitignore b/.gitignore index 8d6ada89..6d194240 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ dist-ssr *.sln *.sw? .turbo -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..cac09136 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6522 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "ab_glyph" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e53b0a3d5760cd2ba9b787ae0c6440ad18ee294ff71b05e3381c900a7d16cfd" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "alsa" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" +dependencies = [ + "alsa-sys", + "bitflags 2.5.0", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-activity" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289" +dependencies = [ + "android-properties", + "bitflags 2.5.0", + "cc", + "cesu8", + "jni 0.21.1", + "jni-sys", + "libc", + "log", + "ndk 0.8.0", + "ndk-context", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum 0.7.2", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.5.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.66", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" +dependencies = [ + "block-sys", + "objc2", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "calloop" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.5.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cap" +version = "0.0.0" +dependencies = [ + "bytemuck", + "byteorder", + "bytes", + "capture", + "chrono", + "cpal", + "dotenv_codegen", + "ffmpeg-sidecar", + "fix-path-env", + "futures", + "image", + "jpeg-encoder", + "nix", + "regex", + "reqwest 0.11.27", + "sentry", + "serde", + "serde_json", + "specta", + "tauri", + "tauri-build", + "tauri-plugin-context-menu", + "tauri-plugin-deep-link", + "tauri-plugin-oauth", + "tauri-plugin-positioner", + "tauri-specta", + "tokio", + "tokio-util", + "urlencoding", + "which", + "window-shadows", + "window-vibrancy", + "winit", +] + +[[package]] +name = "capture" +version = "0.0.1" +dependencies = [ + "block", + "cfg-if", + "core-foundation", + "libc", + "winapi", +] + +[[package]] +name = "cargo_toml" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.5", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.3", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni 0.21.1", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.66", +] + +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn 2.0.66", +] + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "darling" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.66", +] + +[[package]] +name = "darling_macro" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.3", +] + +[[package]] +name = "document-features" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenv_codegen" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56966279c10e4f8ee8c22123a15ed74e7c8150b658b26c619c53f4a56eb4a8aa" +dependencies = [ + "dotenv_codegen_implementation", + "proc-macro-hack", +] + +[[package]] +name = "dotenv_codegen_implementation" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e737a3522cd45f6adc19b644ce43ef53e1e9045f2d2de425c1f468abd4cf33" +dependencies = [ + "dotenv", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "either" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" + +[[package]] +name = "embed-resource" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.14", + "vswhom", + "winreg 0.52.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "ffmpeg-sidecar" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b56b7872b0cbc828e83043a82268b111e3648dab25f29079fce448648e572a" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "fix-path-env" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/fix-path-env-rs#8481725b7ebfc56cdb052d522517421242eac36b" +dependencies = [ + "strip-ansi-escapes", + "thiserror", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps 6.2.2", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags 1.3.2", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +dependencies = [ + "anyhow", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows 0.52.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa 1.0.11", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "itoa 1.0.11", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.29", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icrate" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" +dependencies = [ + "block2", + "dispatch", + "objc2", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.7", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "infer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a" +dependencies = [ + "cfb", +] + +[[package]] +name = "infer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +dependencies = [ + "cfb", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "cfg-if", + "libc", + "rustc_version", + "to_method", + "winapi", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "javascriptcore-rs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + +[[package]] +name = "jpeg-encoder" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fefe5a4fb12fa836172dc53cc36c37af693f6197ae702f931faad8774caf926" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libappindicator" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + +[[package]] +name = "libredox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +dependencies = [ + "bitflags 2.5.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + +[[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "minisign-verify" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" + +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys 0.3.0", + "num_enum 0.5.11", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.5.0", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum 0.7.2", + "raw-window-handle 0.6.2", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive 0.7.2", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni 0.21.1", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys 0.42.0", +] + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +dependencies = [ + "libredox 0.0.2", +] + +[[package]] +name = "os_info" +version = "3.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +dependencies = [ + "log", + "serde", + "windows-sys 0.52.0", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b41438d2fc63c46c74a2203bf5ccd82c41ba04347b2fcf5754f230b167067d5" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags 1.3.2", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.1", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64 0.21.7", + "indexmap 2.2.6", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom 0.2.15", + "libredox 0.1.3", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.29", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.1.2", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.52.0", +] + +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle 0.5.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b2eaf3a5b264a521b988b2e73042e742df700c4f962cde845d1541adb46550" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags 2.5.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "sentry" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00421ed8fa0c995f07cde48ba6c89e80f2b312f74ff637326f392fbfd23abe02" +dependencies = [ + "httpdate", + "native-tls", + "reqwest 0.12.4", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-debug-images", + "sentry-panic", + "sentry-tracing", + "tokio", + "ureq", +] + +[[package]] +name = "sentry-backtrace" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a79194074f34b0cbe5dd33896e5928bbc6ab63a889bd9df2264af5acb186921e" +dependencies = [ + "backtrace", + "once_cell", + "regex", + "sentry-core", +] + +[[package]] +name = "sentry-contexts" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba8870c5dba2bfd9db25c75574a11429f6b95957b0a78ac02e2970dd7a5249a" +dependencies = [ + "hostname", + "libc", + "os_info", + "rustc_version", + "sentry-core", + "uname", +] + +[[package]] +name = "sentry-core" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a75011ea1c0d5c46e9e57df03ce81f5c7f0a9e199086334a1f9c0a541e0826" +dependencies = [ + "once_cell", + "rand 0.8.5", + "sentry-types", + "serde", + "serde_json", +] + +[[package]] +name = "sentry-debug-images" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec2a486336559414ab66548da610da5e9626863c3c4ffca07d88f7dc71c8de8" +dependencies = [ + "findshlibs", + "once_cell", + "sentry-core", +] + +[[package]] +name = "sentry-panic" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eaa3ecfa3c8750c78dcfd4637cfa2598b95b52897ed184b4dc77fcf7d95060d" +dependencies = [ + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-tracing" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f715932bf369a61b7256687c6f0554141b7ce097287e30e3f7ed6e9de82498fe" +dependencies = [ + "sentry-backtrace", + "sentry-core", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sentry-types" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4519c900ce734f7a0eb7aba0869dfb225a7af8820634a7dd51449e3b093cfb7c" +dependencies = [ + "debugid", + "hex", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror", + "time", + "url", + "uuid", +] + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "indexmap 2.2.6", + "itoa 1.0.11", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.5.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags 1.3.2", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags 1.3.2", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "specta" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2240c3aa020aa61d2c569087d213baafbb212f4ceb9de9dd162376ea6aa0fe3" +dependencies = [ + "document-features", + "indoc 1.0.9", + "once_cell", + "paste", + "serde", + "serde_json", + "specta-macros", + "tauri", + "thiserror", +] + +[[package]] +name = "specta-macros" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4605306321c356e03873b8ee71d7592a5e7c508add325c3ed0677c16fdf1bcfb" +dependencies = [ + "Inflector", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", + "termcolor", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "sys-locale" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee" +dependencies = [ + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.11", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr 0.15.8", + "heck 0.5.0", + "pkg-config", + "toml 0.8.14", + "version-compare 0.2.0", +] + +[[package]] +name = "tao" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575c856fc21e551074869dcfaad8f706412bd5b803dfa0fbf6881c4ff4bfafab" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "cc", + "cocoa 0.24.1", + "core-foundation", + "core-graphics 0.22.3", + "crossbeam-channel", + "dirs-next", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib", + "glib-sys", + "gtk", + "image", + "instant", + "jni 0.20.0", + "lazy_static", + "libappindicator", + "libc", + "log", + "ndk 0.6.0", + "ndk-context", + "ndk-sys 0.3.0", + "objc", + "once_cell", + "parking_lot", + "png", + "raw-window-handle 0.5.2", + "scopeguard", + "serde", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.39.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tar" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + +[[package]] +name = "tauri" +version = "1.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77567d2b3b74de4588d544147142d02297f3eaa171a25a065252141d8597a516" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bytes", + "cocoa 0.24.1", + "dirs-next", + "dunce", + "embed_plist", + "encoding_rs", + "flate2", + "futures-util", + "getrandom 0.2.15", + "glib", + "glob", + "gtk", + "heck 0.5.0", + "http 0.2.12", + "ignore", + "indexmap 1.9.3", + "infer 0.9.0", + "minisign-verify", + "objc", + "once_cell", + "open", + "os_info", + "percent-encoding", + "png", + "rand 0.8.5", + "raw-window-handle 0.5.2", + "regex", + "reqwest 0.11.27", + "rfd", + "semver", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "sys-locale", + "tar", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "tempfile", + "thiserror", + "time", + "tokio", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "zip", +] + +[[package]] +name = "tauri-build" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab30cba12974d0f9b09794f61e72cad6da2142d3ceb81e519321bab86ce53312" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "heck 0.5.0", + "json-patch", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a1d90db526a8cdfd54444ad3f34d8d4d58fa5c536463915942393743bd06f8" +dependencies = [ + "base64 0.21.7", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "regex", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils", + "thiserror", + "time", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a582d75414250122e4a597b9dd7d3c910a2c77906648fc2ac9353845ff0feec" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin-context-menu" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8afd632ae9da2f48cc172890de2fa0b9a3dbeed514d04d5e42df839d81835a" +dependencies = [ + "cocoa 0.24.1", + "dispatch", + "gdk", + "glib", + "gtk", + "image", + "lazy_static", + "libc", + "objc", + "serde", + "tauri", + "time", + "winapi", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "0.1.2" +source = "git+https://github.com/FabianLars/tauri-plugin-deep-link?branch=main#1aa5c3d01dfadfe6495673bb8767a913198da279" +dependencies = [ + "dirs", + "interprocess", + "log", + "objc2", + "once_cell", + "tauri-utils", + "windows-sys 0.52.0", + "winreg 0.52.0", +] + +[[package]] +name = "tauri-plugin-oauth" +version = "0.0.0-alpha.0" +source = "git+https://github.com/FabianLars/tauri-plugin-oauth?branch=main#50dadbf4a81cba51f625587b7722892f0b4316a6" +dependencies = [ + "httparse", + "log", + "serde", + "tauri", + "url", +] + +[[package]] +name = "tauri-plugin-positioner" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c491af3aaa053d6e85d95b516bdaa20ae7c6ebb36cbdae2988dea68f57f27e" +dependencies = [ + "log", + "serde", + "serde_json", + "serde_repr", + "tauri", + "thiserror", +] + +[[package]] +name = "tauri-runtime" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7ffddf36d450791018e63a3ddf54979b9581d9644c584a5fb5611e6b5f20b4" +dependencies = [ + "gtk", + "http 0.2.12", + "http-range", + "rand 0.8.5", + "raw-window-handle 0.5.2", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "uuid", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1989b3b4d611f5428b3414a4abae6fa6df30c7eb8ed33250ca90a5f7e5bb3655" +dependencies = [ + "cocoa 0.24.1", + "gtk", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle 0.5.2", + "tauri-runtime", + "tauri-utils", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "wry", +] + +[[package]] +name = "tauri-specta" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa034c38b7bdfeccc606eca0b030a1e67a20b78e7642edef09816b7e1ff9a9de" +dependencies = [ + "heck 0.4.1", + "indoc 2.0.5", + "serde", + "serde_json", + "specta", + "tauri", + "thiserror", +] + +[[package]] +name = "tauri-utils" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "450b17a7102e5d46d4bdabae0d1590fd27953e704e691fc081f06c06d2253b35" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck 0.5.0", + "html5ever", + "infer 0.13.0", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows-version", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.8", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa 1.0.11", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.2.6", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.13", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "ureq" +version = "2.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" +dependencies = [ + "base64 0.22.1", + "log", + "native-tls", + "once_cell", + "url", +] + +[[package]] +name = "url" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.66", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +dependencies = [ + "bitflags 2.5.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.5.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a206e8b2b53b1d3fcb9428fec72bc278ce539e2fa81fe2bfc1ab27703d5187b9" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup2", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup2-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "webview2-com" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "webview2-com-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +dependencies = [ + "regex", + "serde", + "serde_json", + "thiserror", + "windows 0.39.0", + "windows-bindgen", + "windows-metadata", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-shadows" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ff424735b1ac21293b0492b069394b0a189c8a463fb015a16dea7c2e221c08" +dependencies = [ + "cocoa 0.25.0", + "objc", + "raw-window-handle 0.5.2", + "windows-sys 0.48.0", +] + +[[package]] +name = "window-vibrancy" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af6abc2b9c56bd95887825a1ce56cde49a2a97c07e28db465d541f5098a2656c" +dependencies = [ + "cocoa 0.25.0", + "objc", + "raw-window-handle 0.5.2", + "windows-sys 0.52.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-implement" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +dependencies = [ + "syn 1.0.109", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows-version" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winit" +version = "0.29.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d59ad965a635657faf09c8f062badd885748428933dad8e8bdd64064d92e5ca" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.5.0", + "bytemuck", + "calloop", + "cfg_aliases", + "core-foundation", + "core-graphics 0.23.2", + "cursor-icon", + "icrate", + "js-sys", + "libc", + "log", + "memmap2", + "ndk 0.8.0", + "ndk-sys 0.5.0+25.2.9519653", + "objc2", + "once_cell", + "orbclient", + "percent-encoding", + "raw-window-handle 0.6.2", + "redox_syscall 0.3.5", + "rustix", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.48.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wry" +version = "0.24.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00711278ed357350d44c749c286786ecac644e044e4da410d466212152383b45" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa 0.24.1", + "core-graphics 0.22.3", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever", + "http 0.2.12", + "kuchikiki", + "libc", + "log", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "sha2", + "soup2", + "tao", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.3", + "once_cell", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "xcursor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.5.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..02610840 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ + +[workspace] +resolver = "2" +members = ["apps/desktop/src-tauri", "crates/*"] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index e2d2f4ba..8378b63f 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -14,7 +14,36 @@ tauri-build = { version = "1.5.1", features = [] } ffmpeg-sidecar = "0.5.1" [dependencies] -tauri = { version = "1.6.1", features = [ "system-tray", "updater", "macos-private-api", "window-set-position", "fs-write-file", "fs-remove-file", "fs-read-file", "fs-rename-file", "fs-exists", "fs-remove-dir", "fs-read-dir", "fs-copy-file", "fs-create-dir", "window-set-ignore-cursor-events", "window-unminimize", "window-minimize", "window-close", "window-show", "window-start-dragging", "window-hide", "window-unmaximize", "window-maximize", "window-set-always-on-top", "shell-open", "devtools", "os-all", "http-all", "icon-png"] } +tauri = { version = "1.6.1", features = [ + "system-tray", + "updater", + "macos-private-api", + "window-set-position", + "fs-write-file", + "fs-remove-file", + "fs-read-file", + "fs-rename-file", + "fs-exists", + "fs-remove-dir", + "fs-read-dir", + "fs-copy-file", + "fs-create-dir", + "window-set-ignore-cursor-events", + "window-unminimize", + "window-minimize", + "window-close", + "window-show", + "window-start-dragging", + "window-hide", + "window-unmaximize", + "window-maximize", + "window-set-always-on-top", + "shell-open", + "devtools", + "os-all", + "http-all", + "icon-png", +] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tauri-plugin-context-menu = "0.7.0" @@ -35,7 +64,7 @@ dotenv_codegen = "0.15.0" byteorder = "1.4.3" bytemuck = "1.14.3" regex = "1" -capture = { path = "./src/capture" } +capture = { path = "../../../crates/capture" } image = "0.24.9" sentry = "0.32.2" fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } diff --git a/apps/desktop/src-tauri/src/capture/Cargo.lock b/crates/capture/Cargo.lock similarity index 100% rename from apps/desktop/src-tauri/src/capture/Cargo.lock rename to crates/capture/Cargo.lock diff --git a/apps/desktop/src-tauri/src/capture/Cargo.toml b/crates/capture/Cargo.toml similarity index 100% rename from apps/desktop/src-tauri/src/capture/Cargo.toml rename to crates/capture/Cargo.toml diff --git a/apps/desktop/src-tauri/src/capture/README.md b/crates/capture/README.md similarity index 100% rename from apps/desktop/src-tauri/src/capture/README.md rename to crates/capture/README.md diff --git a/apps/desktop/src-tauri/src/capture/build.rs b/crates/capture/build.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/build.rs rename to crates/capture/build.rs diff --git a/apps/desktop/src-tauri/src/capture/src/common/dxgi.rs b/crates/capture/src/common/dxgi.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/common/dxgi.rs rename to crates/capture/src/common/dxgi.rs diff --git a/apps/desktop/src-tauri/src/capture/src/common/mod.rs b/crates/capture/src/common/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/common/mod.rs rename to crates/capture/src/common/mod.rs diff --git a/apps/desktop/src-tauri/src/capture/src/common/quartz.rs b/crates/capture/src/common/quartz.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/common/quartz.rs rename to crates/capture/src/common/quartz.rs diff --git a/apps/desktop/src-tauri/src/capture/src/common/x11.rs b/crates/capture/src/common/x11.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/common/x11.rs rename to crates/capture/src/common/x11.rs diff --git a/apps/desktop/src-tauri/src/capture/src/dxgi/ffi.rs b/crates/capture/src/dxgi/ffi.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/dxgi/ffi.rs rename to crates/capture/src/dxgi/ffi.rs diff --git a/apps/desktop/src-tauri/src/capture/src/dxgi/mod.rs b/crates/capture/src/dxgi/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/dxgi/mod.rs rename to crates/capture/src/dxgi/mod.rs diff --git a/apps/desktop/src-tauri/src/capture/src/lib.rs b/crates/capture/src/lib.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/lib.rs rename to crates/capture/src/lib.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/capturer.rs b/crates/capture/src/quartz/capturer.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/capturer.rs rename to crates/capture/src/quartz/capturer.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/config.rs b/crates/capture/src/quartz/config.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/config.rs rename to crates/capture/src/quartz/config.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/display.rs b/crates/capture/src/quartz/display.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/display.rs rename to crates/capture/src/quartz/display.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/ffi.rs b/crates/capture/src/quartz/ffi.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/ffi.rs rename to crates/capture/src/quartz/ffi.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/frame.rs b/crates/capture/src/quartz/frame.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/frame.rs rename to crates/capture/src/quartz/frame.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/mod.rs b/crates/capture/src/quartz/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/mod.rs rename to crates/capture/src/quartz/mod.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/capturer.rs b/crates/capture/src/x11/capturer.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/capturer.rs rename to crates/capture/src/x11/capturer.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/display.rs b/crates/capture/src/x11/display.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/display.rs rename to crates/capture/src/x11/display.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/ffi.rs b/crates/capture/src/x11/ffi.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/ffi.rs rename to crates/capture/src/x11/ffi.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/iter.rs b/crates/capture/src/x11/iter.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/iter.rs rename to crates/capture/src/x11/iter.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/mod.rs b/crates/capture/src/x11/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/mod.rs rename to crates/capture/src/x11/mod.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/server.rs b/crates/capture/src/x11/server.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/server.rs rename to crates/capture/src/x11/server.rs From faf0a4f59eabc380b03514b9ee41766f92ce6a1b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 16:23:34 +0800 Subject: [PATCH 02/28] combine video and audio into one ffmpeg process --- apps/desktop/src-tauri/src/main.rs | 31 +- apps/desktop/src-tauri/src/media.rs | 751 ++++++++++---------- apps/desktop/src-tauri/src/recording.rs | 265 ++++--- apps/desktop/src-tauri/src/upload.rs | 145 ++-- apps/desktop/src-tauri/src/utils.rs | 5 - apps/web/app/api/upload/mux/create/route.ts | 51 +- 6 files changed, 661 insertions(+), 587 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 23df038f..007608ca 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,27 +1,29 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::collections::LinkedList; -use std::sync::{Arc}; -use std::path::PathBuf; use cpal::Devices; use regex::Regex; +use std::collections::LinkedList; +use std::path::PathBuf; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::vec; +use tauri::{ + CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTraySubmenu, Window, +}; +use tauri_plugin_oauth::start; +use tauri_plugin_positioner::{Position, WindowExt}; use tokio::sync::Mutex; -use std::sync::atomic::{AtomicBool}; -use std::{vec}; -use tauri::{command, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTraySubmenu, Window}; -use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial}; use window_shadows::set_shadow; -use tauri_plugin_positioner::{WindowExt, Position}; -use tauri_plugin_oauth::start; +use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial}; +mod media; mod recording; mod upload; mod utils; -mod media; -use recording::{RecordingState, start_dual_recording, stop_all_recordings}; -use media::{enumerate_audio_devices}; -use utils::{has_screen_capture_access}; +use media::enumerate_audio_devices; +use recording::{start_dual_recording, stop_all_recordings, RecordingState}; +use utils::has_screen_capture_access; use ffmpeg_sidecar::{ command::ffmpeg_is_installed, @@ -213,8 +215,7 @@ fn main() { media_process: None, recording_options: None, shutdown_flag: Arc::new(AtomicBool::new(false)), - video_uploading_finished: Arc::new(AtomicBool::new(false)), - audio_uploading_finished: Arc::new(AtomicBool::new(false)), + uploading_finished: Arc::new(AtomicBool::new(false)), data_dir: Some(data_directory), max_screen_width: max_width as usize, max_screen_height: max_height as usize, diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index 3b55744e..4b0da1a4 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -1,84 +1,89 @@ +use byteorder::{ByteOrder, LittleEndian}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::SampleFormat; -use std::process::{Stdio}; -use byteorder::{ByteOrder, LittleEndian}; -use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; -use std::io::{ErrorKind::WouldBlock, Error}; -use std::time::{Instant, Duration}; -use std::path::Path; -use image::{ImageBuffer, Rgba, ImageFormat}; use image::codecs::jpeg::JpegEncoder; - -use tokio::io::{AsyncWriteExt}; -use tokio::process::{Command, Child, ChildStdin}; +use image::{ImageBuffer, ImageFormat, Rgba}; +use std::io::{Error, ErrorKind::WouldBlock}; +use std::path::Path; +use std::process::Stdio; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::time::{Duration, Instant}; +use tokio::fs::File; + +use tokio::io::AsyncWriteExt; +use tokio::process::{Child, ChildStdin, Command}; use tokio::sync::{mpsc, Mutex}; use tokio::try_join; use crate::recording::RecordingOptions; -use crate::utils::{ffmpeg_path_as_str}; -use crate::upload::upload_file; +use crate::upload::{self, upload_file}; +use crate::utils::{create_named_pipe, ffmpeg_path_as_str}; use capture::{Capturer, Display}; const FRAME_RATE: u64 = 30; pub struct MediaRecorder { pub options: Option, - ffmpeg_audio_process: Option, - ffmpeg_video_process: Option, - ffmpeg_audio_stdin: Option>>>, - ffmpeg_video_stdin: Option>>>, + ffmpeg_process: Option, + ffmpeg_stdin: Option>>>, device_name: Option, - stream: Option, + audio_stream: Option, audio_channel_sender: Option>>, audio_channel_receiver: Option>>, video_channel_sender: Option>>, video_channel_receiver: Option>>, should_stop: Arc, start_time: Option, - audio_file_path: Option, - video_file_path: Option, + file_path: Option, } impl MediaRecorder { - pub fn new() -> Self { MediaRecorder { options: None, - ffmpeg_audio_process: None, - ffmpeg_video_process: None, - ffmpeg_audio_stdin: None, - ffmpeg_video_stdin: None, + ffmpeg_process: None, + ffmpeg_stdin: None, device_name: None, - stream: None, + audio_stream: None, audio_channel_sender: None, audio_channel_receiver: None, video_channel_sender: None, video_channel_receiver: None, should_stop: Arc::new(AtomicBool::new(false)), start_time: None, - audio_file_path: None, - video_file_path: None, + file_path: None, } } - pub async fn start_media_recording(&mut self, options: RecordingOptions, audio_file_path: &str, video_file_path: &str, screenshot_file_path: &str, custom_device: Option<&str>, max_screen_width: usize, max_screen_height: usize) -> Result<(), String> { + pub async fn start_media_recording( + &mut self, + options: RecordingOptions, + chunks_file_path: &str, + screenshot_file_path: &str, + custom_device: Option<&str>, + max_screen_width: usize, + max_screen_height: usize, + ) -> Result<(), String> { self.options = Some(options.clone()); println!("Custom device: {:?}", custom_device); - + let host = cpal::default_host(); let devices = host.devices().expect("Failed to get devices"); let _display = Display::primary().expect("Failed to find primary display"); let w = max_screen_width; let h = max_screen_height; - + let adjusted_width = w & !2; let adjusted_height = h & !2; let capture_size = adjusted_width * adjusted_height * 4; let (audio_tx, audio_rx) = tokio::sync::mpsc::channel::>(2048); let (video_tx, video_rx) = tokio::sync::mpsc::channel::>(2048); let calculated_stride = (adjusted_width * 4) as usize; - + println!("Display width: {}", w); println!("Display height: {}", h); println!("Adjusted width: {}", adjusted_width); @@ -93,8 +98,7 @@ impl MediaRecorder { self.audio_channel_receiver = Some(audio_rx); self.video_channel_sender = Some(video_tx); self.video_channel_receiver = Some(video_rx); - self.ffmpeg_audio_stdin = Some(Arc::new(Mutex::new(None))); - self.ffmpeg_video_stdin = Some(Arc::new(Mutex::new(None))); + self.ffmpeg_stdin = Some(Arc::new(Mutex::new(None))); let audio_channel_sender = self.audio_channel_sender.clone(); let video_channel_sender = self.video_channel_sender.clone(); @@ -103,7 +107,7 @@ impl MediaRecorder { let video_channel_receiver = Arc::new(Mutex::new(self.video_channel_receiver.take())); let should_stop = Arc::clone(&self.should_stop); - + let mut input_devices = devices.filter_map(|device| { let supported_input_configs = device.supported_input_configs(); if supported_input_configs.is_ok() && supported_input_configs.unwrap().count() > 0 { @@ -115,20 +119,41 @@ impl MediaRecorder { let device = if let Some(custom_device_name) = custom_device { input_devices - .find(|d| d.name().map(|name| name == custom_device_name).unwrap_or(false)) - .unwrap_or_else(|| host.default_input_device().expect("No default input device available")) + .find(|d| { + d.name() + .map(|name| name == custom_device_name) + .unwrap_or(false) + }) + .unwrap_or_else(|| { + host.default_input_device() + .expect("No default input device available") + }) } else { - host.default_input_device().expect("No default input device available") + host.default_input_device() + .expect("No default input device available") }; - println!("Using audio device: {}", device.name().expect("Failed to get device name")); + println!( + "Using audio device: {}", + device.name().expect("Failed to get device name") + ); - let config = device.supported_input_configs() + let config = device + .supported_input_configs() .expect("Failed to get supported input configs") - .find(|c| c.sample_format() == SampleFormat::F32 || c.sample_format() == SampleFormat::I16 || c.sample_format() == SampleFormat::I8 || c.sample_format() == SampleFormat::I32) - .unwrap_or_else(|| - device.supported_input_configs().expect("Failed to get supported input configs").next().expect("No supported input config") - ) + .find(|c| { + c.sample_format() == SampleFormat::F32 + || c.sample_format() == SampleFormat::I16 + || c.sample_format() == SampleFormat::I8 + || c.sample_format() == SampleFormat::I32 + }) + .unwrap_or_else(|| { + device + .supported_input_configs() + .expect("Failed to get supported input configs") + .next() + .expect("No supported input config") + }) .with_max_sample_rate(); let sample_rate = config.sample_rate().0; @@ -144,146 +169,145 @@ impl MediaRecorder { println!("Sample rate: {}", sample_rate); println!("Channels: {}", channels); println!("Sample format: {}", sample_format); - + let ffmpeg_binary_path_str = ffmpeg_path_as_str().unwrap().to_owned(); println!("FFmpeg binary path: {}", ffmpeg_binary_path_str); - - let audio_file_path_owned = audio_file_path.to_owned(); - let video_file_path_owned = video_file_path.to_owned(); + let sample_rate_str = sample_rate.to_string(); let channels_str = channels.to_string(); - - let ffmpeg_audio_stdin = self.ffmpeg_audio_stdin.clone(); - let ffmpeg_video_stdin = self.ffmpeg_video_stdin.clone(); let err_fn = move |err| { eprintln!("an error occurred on stream: {}", err); }; - - if custom_device != Some("None") { + + let needs_audio = custom_device != Some("None"); + + if needs_audio { println!("Building input stream..."); - let stream_result: Result = match config.sample_format() { - SampleFormat::I8 => device.build_input_stream( - &config.into(), - { - let audio_start_time = Arc::clone(&audio_start_time); - move |data: &[i8], _: &_| { - let mut first_frame_time_guard = audio_start_time.try_lock(); - - let bytes = data.iter().map(|&sample| sample as u8).collect::>(); - if let Some(sender) = &audio_channel_sender { - if sender.try_send(bytes).is_err() { - eprintln!("Channel send error. Dropping data."); + let stream_result: Result = + match config.sample_format() { + SampleFormat::I8 => device.build_input_stream( + &config.into(), + { + let audio_start_time = Arc::clone(&audio_start_time); + move |data: &[i8], _: &_| { + let mut first_frame_time_guard = audio_start_time.try_lock(); + + let bytes = + data.iter().map(|&sample| sample as u8).collect::>(); + if let Some(sender) = &audio_channel_sender { + if sender.try_send(bytes).is_err() { + eprintln!("Channel send error. Dropping data."); + } + } + + if let Ok(ref mut start_time_option) = first_frame_time_guard { + if start_time_option.is_none() { + **start_time_option = Some(Instant::now()); + + println!("Audio start time captured"); + } + } } - } - - if let Ok(ref mut start_time_option) = first_frame_time_guard { - if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); - - println!("Audio start time captured"); - } - } - } - }, - err_fn, - None, - ), - SampleFormat::I16 => device.build_input_stream( - &config.into(), - { - let audio_start_time = Arc::clone(&audio_start_time); - move |data: &[i16], _: &_| { - let mut first_frame_time_guard = audio_start_time.try_lock(); - - let mut bytes = vec![0; data.len() * 2]; - LittleEndian::write_i16_into(data, &mut bytes); - if let Some(sender) = &audio_channel_sender { - if sender.try_send(bytes).is_err() { - eprintln!("Channel send error. Dropping data."); - } - } - - if let Ok(ref mut start_time_option) = first_frame_time_guard { - if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); - - println!("Audio start time captured"); - } - } - } - }, - err_fn, - None, - ), - SampleFormat::I32 => device.build_input_stream( - &config.into(), - { - let audio_start_time = Arc::clone(&audio_start_time); - move |data: &[i32], _: &_| { - let mut first_frame_time_guard = audio_start_time.try_lock(); - - let mut bytes = vec![0; data.len() * 2]; - LittleEndian::write_i32_into(data, &mut bytes); - if let Some(sender) = &audio_channel_sender { - if sender.try_send(bytes).is_err() { - eprintln!("Channel send error. Dropping data."); - } - } - - if let Ok(ref mut start_time_option) = first_frame_time_guard { - if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); - - println!("Audio start time captured"); - } - } - } - }, - err_fn, - None, - ), - SampleFormat::F32 => device.build_input_stream( - &config.into(), - { - let audio_start_time = Arc::clone(&audio_start_time); - move |data: &[f32], _: &_| { - let mut first_frame_time_guard = audio_start_time.try_lock(); - - let mut bytes = vec![0; data.len() * 4]; - LittleEndian::write_f32_into(data, &mut bytes); - if let Some(sender) = &audio_channel_sender { - if sender.try_send(bytes).is_err() { - eprintln!("Channel send error. Dropping data."); - } - } - - if let Ok(ref mut start_time_option) = first_frame_time_guard { - if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); - - println!("Audio start time captured"); - } - } - } - }, - err_fn, - None, - ), - _sample_format => Err(cpal::BuildStreamError::DeviceNotAvailable), - }; - + }, + err_fn, + None, + ), + SampleFormat::I16 => device.build_input_stream( + &config.into(), + { + let audio_start_time = Arc::clone(&audio_start_time); + move |data: &[i16], _: &_| { + let mut first_frame_time_guard = audio_start_time.try_lock(); + + let mut bytes = vec![0; data.len() * 2]; + LittleEndian::write_i16_into(data, &mut bytes); + if let Some(sender) = &audio_channel_sender { + if sender.try_send(bytes).is_err() { + eprintln!("Channel send error. Dropping data."); + } + } + + if let Ok(ref mut start_time_option) = first_frame_time_guard { + if start_time_option.is_none() { + **start_time_option = Some(Instant::now()); + + println!("Audio start time captured"); + } + } + } + }, + err_fn, + None, + ), + SampleFormat::I32 => device.build_input_stream( + &config.into(), + { + let audio_start_time = Arc::clone(&audio_start_time); + move |data: &[i32], _: &_| { + let mut first_frame_time_guard = audio_start_time.try_lock(); + + let mut bytes = vec![0; data.len() * 2]; + LittleEndian::write_i32_into(data, &mut bytes); + if let Some(sender) = &audio_channel_sender { + if sender.try_send(bytes).is_err() { + eprintln!("Channel send error. Dropping data."); + } + } + + if let Ok(ref mut start_time_option) = first_frame_time_guard { + if start_time_option.is_none() { + **start_time_option = Some(Instant::now()); + + println!("Audio start time captured"); + } + } + } + }, + err_fn, + None, + ), + SampleFormat::F32 => device.build_input_stream( + &config.into(), + { + let audio_start_time = Arc::clone(&audio_start_time); + move |data: &[f32], _: &_| { + let mut first_frame_time_guard = audio_start_time.try_lock(); + + let mut bytes = vec![0; data.len() * 4]; + LittleEndian::write_f32_into(data, &mut bytes); + if let Some(sender) = &audio_channel_sender { + if sender.try_send(bytes).is_err() { + eprintln!("Channel send error. Dropping data."); + } + } + + if let Ok(ref mut start_time_option) = first_frame_time_guard { + if start_time_option.is_none() { + **start_time_option = Some(Instant::now()); + + println!("Audio start time captured"); + } + } + } + }, + err_fn, + None, + ), + _sample_format => Err(cpal::BuildStreamError::DeviceNotAvailable), + }; + let stream = stream_result.map_err(|_| "Failed to build input stream")?; - self.stream = Some(stream); + self.audio_stream = Some(stream); self.trigger_play()?; } - let video_start_time_clone = Arc::clone(&video_start_time); - let screenshot_file_path_owned = format!("{}/screen-capture.jpg", screenshot_file_path); + let video_start_time_clone = Arc::clone(&video_start_time); + let screenshot_file_path_owned = format!("{screenshot_file_path}/screen-capture.jpg"); let capture_frame_at = Duration::from_secs(3); - + std::thread::spawn(move || { println!("Starting video recording capture thread..."); @@ -292,7 +316,12 @@ impl MediaRecorder { _ => false, }; - let mut capturer = Capturer::new(Display::primary().expect("Failed to find primary display"), w.try_into().unwrap(), h.try_into().unwrap()).expect("Failed to start capture"); + let mut capturer = Capturer::new( + Display::primary().expect("Failed to find primary display"), + w.try_into().unwrap(), + h.try_into().unwrap(), + ) + .expect("Failed to start capture"); let fps = FRAME_RATE; let spf = Duration::from_nanos(1_000_000_000 / fps); @@ -301,7 +330,7 @@ impl MediaRecorder { let start_time = Instant::now(); let mut time_next = Instant::now() + spf; let mut screenshot_captured: bool = false; - + while !should_stop.load(Ordering::SeqCst) { let options_clone = options.clone(); let now = Instant::now(); @@ -309,10 +338,12 @@ impl MediaRecorder { if now >= time_next { match capturer.frame() { Ok(frame) => { - let mut frame_data = Vec::with_capacity(capture_size.try_into().unwrap()); + let mut frame_data = + Vec::with_capacity(capture_size.try_into().unwrap()); for row in 0..adjusted_height { - let padded_stride = frame.stride_override().unwrap_or(calculated_stride); + let padded_stride = + frame.stride_override().unwrap_or(calculated_stride); assert!(padded_stride >= calculated_stride, "Image stride with padding should not be smaller than calculated bytes per row"); // Each row should skip the padding of the previous row let start = row * padded_stride; @@ -323,7 +354,8 @@ impl MediaRecorder { if now - start_time >= capture_frame_at && !screenshot_captured { screenshot_captured = true; - let screenshot_file_path_owned_cloned = screenshot_file_path_owned.clone(); + let screenshot_file_path_owned_cloned = + screenshot_file_path_owned.clone(); let mut frame_data_clone = frame_data.clone(); std::thread::spawn(move || { @@ -332,31 +364,46 @@ impl MediaRecorder { } let path = Path::new(&screenshot_file_path_owned_cloned); - let image: ImageBuffer, Vec> = ImageBuffer::from_raw( - adjusted_width.try_into().unwrap(), - adjusted_height.try_into().unwrap(), - frame_data_clone - ).expect("Failed to create image buffer"); - - let mut output_file = std::fs::File::create(&path).expect("Failed to create output file"); - let mut encoder = JpegEncoder::new_with_quality(&mut output_file, 20); + let image: ImageBuffer, Vec> = + ImageBuffer::from_raw( + adjusted_width.try_into().unwrap(), + adjusted_height.try_into().unwrap(), + frame_data_clone, + ) + .expect("Failed to create image buffer"); + + let mut output_file = std::fs::File::create(&path) + .expect("Failed to create output file"); + let mut encoder = + JpegEncoder::new_with_quality(&mut output_file, 20); if let Err(e) = encoder.encode_image(&image) { eprintln!("Failed to save screenshot: {}", e); } else { if !is_local_mode { let rt = tokio::runtime::Runtime::new().unwrap(); - let screenshot_file_path_owned_cloned_copy = screenshot_file_path_owned_cloned.clone(); + let screenshot_file_path_owned_cloned_copy = + screenshot_file_path_owned_cloned.clone(); rt.block_on(async { - let upload_task = tokio::spawn(upload_file(Some(options_clone), screenshot_file_path_owned_cloned_copy.clone(), "screenshot".to_string())); + let upload_task = tokio::spawn(upload_file( + Some(options_clone), + screenshot_file_path_owned_cloned_copy.clone(), + upload::FileType::Screenshot, + )); match upload_task.await { - Ok(result) => { - match result { - Ok(_) => println!("Screenshot captured and saved to {:?}", path), - Err(e) => eprintln!("Failed to upload file: {}", e), - } + Ok(result) => match result { + Ok(_) => println!( + "Screenshot captured and saved to {:?}", + path + ), + Err(e) => eprintln!( + "Failed to upload file: {}", + e + ), }, - Err(e) => eprintln!("Failed to join task: {}", e), + Err(e) => { + eprintln!("Failed to join task: {}", e) + } } }); } @@ -375,22 +422,22 @@ impl MediaRecorder { if let Ok(ref mut start_time_option) = first_frame_time_guard { if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); + **start_time_option = Some(Instant::now()); println!("Video start time captured"); } } frame_count += 1; - }, + } Err(error) if error.kind() == WouldBlock => { std::thread::sleep(Duration::from_millis(1)); continue; - }, + } Err(error) => { eprintln!("Capture error: {}", error); break; - }, + } } time_next += spf; @@ -409,11 +456,9 @@ impl MediaRecorder { }); println!("Starting audio recording and processing..."); - let audio_output_chunk_pattern = format!("{}/audio_recording_%03d.aac", audio_file_path_owned); - let audio_segment_list_filename = format!("{}/segment_list.txt", audio_file_path_owned); - let video_output_chunk_pattern = format!("{}/video_recording_%03d.ts", video_file_path_owned); - let video_segment_list_filename = format!("{}/segment_list.txt", video_file_path_owned); - + let video_output_chunk_pattern = format!("{chunks_file_path}/video_recording_%03d.ts"); + let video_segment_list_filename = format!("{chunks_file_path}/segment_list.txt"); + let mut audio_filters = Vec::new(); if channels > 2 { @@ -422,132 +467,149 @@ impl MediaRecorder { audio_filters.push("loudnorm"); - let mut ffmpeg_audio_command: Vec = vec![ - "-f", sample_format, - "-ar", &sample_rate_str, - "-ac", &channels_str, - "-thread_queue_size", "4096", - "-i", "pipe:0", - "-af", "aresample=async=1:min_hard_comp=0.100000:first_pts=0", - "-c:a", "aac", - "-b:a", "128k", - "-async", "1", - "-f", "segment", - "-segment_time", "3", - "-segment_time_delta", "0.01", - "-segment_list", &audio_segment_list_filename, - "-reset_timestamps", "1", - &audio_output_chunk_pattern, - ].into_iter().map(|s| s.to_string()).collect(); - - let mut ffmpeg_video_command: Vec = vec![ - "-f", "rawvideo", - "-pix_fmt", "bgra", - "-s", &format!("{}x{}", adjusted_width, adjusted_height), - "-r", "30", - "-thread_queue_size", "4096", - "-i", "pipe:0", - "-vf", "fps=30,scale=in_range=full:out_range=limited", - "-c:v", "libx264", - "-preset", "ultrafast", - "-pix_fmt", "yuv420p", - "-tune", "zerolatency", - "-vsync", "1", - "-force_key_frames", "expr:gte(t,n_forced*3)", - "-f", "segment", - "-segment_time", "3", - "-segment_time_delta", "0.01", - "-segment_list", &video_segment_list_filename, - "-segment_format", "ts", - "-movflags", "frag_keyframe+empty_moov", - "-reset_timestamps", "1", - &video_output_chunk_pattern, - ].into_iter().map(|s| s.to_string()).collect(); - - if custom_device != Some("None") { + std::fs::create_dir_all(format!("{chunks_file_path}/pipes")).map_err(|e| e.to_string())?; + + let video_pipe_path = format!("{chunks_file_path}/pipes/video.pipe"); + + std::fs::remove_file(&video_pipe_path).ok(); + create_named_pipe(&video_pipe_path).map_err(|e| e.to_string())?; + + let audio_pipe_path = format!("{chunks_file_path}/pipes/audio.pipe"); + + std::fs::remove_file(&audio_pipe_path).ok(); + create_named_pipe(&audio_pipe_path).map_err(|e| e.to_string())?; + + let size = format!("{}x{}", adjusted_width, adjusted_height); + let mut ffmpeg_command = vec![ + ["-f", "rawvideo"], + ["-pix_fmt", "bgra"], + ["-s", &size], + ["-r", "30"], + ["-thread_queue_size", "4096"], + ["-i", &video_pipe_path], + ]; + + if needs_audio { + ffmpeg_command.extend([ + // in + ["-f", sample_format], + ["-ar", &sample_rate_str], + ["-ac", &channels_str], + ["-thread_queue_size", "4096"], + ["-i", &audio_pipe_path], + // out + [ + "-af", + "aresample=async=1:min_hard_comp=0.100000:first_pts=0", + ], + ["-c:a", "aac"], + ["-b:a", "128k"], + ["-async", "1"], + ]); + }; + + ffmpeg_command.extend([ + ["-vf", "fps=30,scale=in_range=full:out_range=limited"], + ["-c:v", "libx264"], + ["-preset", "ultrafast"], + ["-pix_fmt", "yuv420p"], + ["-tune", "zerolatency"], + ["-vsync", "1"], + ["-force_key_frames", "expr:gte(t,n_forced*3)"], + ["-f", "segment"], + ["-segment_time", "3"], + ["-segment_time_delta", "0.01"], + ["-segment_list", &video_segment_list_filename], + ["-segment_format", "ts"], + ["-movflags", "frag_keyframe+empty_moov"], + ["-reset_timestamps", "1"], + ]); + + let mut ffmpeg_command = ffmpeg_command + .into_iter() + .flatten() + .map(|s| s.to_string()) + .chain([video_output_chunk_pattern]) + .collect(); + + if needs_audio { println!("Adjusting FFmpeg commands based on start times..."); adjust_ffmpeg_commands_based_on_start_times( Arc::clone(&audio_start_time), Arc::clone(&video_start_time), - &mut ffmpeg_audio_command, - &mut ffmpeg_video_command, - ).await; + &mut ffmpeg_command, + ) + .await; } - println!("Starting FFmpeg audio and video processes..."); - - let mut audio_stdin: Option = None; - let mut audio_child: Option = None; - - if custom_device != Some("None") { - let (child, stdin) = self.start_audio_ffmpeg_processes(&ffmpeg_binary_path_str, &ffmpeg_audio_command).await.map_err(|e| e.to_string())?; - audio_child = Some(child); - audio_stdin = Some(stdin); - println!("Audio process started"); - } + println!("Starting FFmpeg process..."); - let (video_child, video_stdin) = self.start_video_ffmpeg_processes(&ffmpeg_binary_path_str, &ffmpeg_video_command).await.map_err(|e| e.to_string())?; - println!("Video process started"); - - if let Some(ffmpeg_audio_stdin) = &self.ffmpeg_audio_stdin { - let mut audio_stdin_lock = ffmpeg_audio_stdin.lock().await; - *audio_stdin_lock = audio_stdin; - drop(audio_stdin_lock); - println!("Audio stdin set"); - } + let (ffmpeg_child, ffmpeg_stdin) = self + .start_ffmpeg_process(&ffmpeg_binary_path_str, &ffmpeg_command) + .await + .map_err(|e| e.to_string())?; + println!("Ffmpeg process started"); - if let Some(ffmpeg_video_stdin) = &self.ffmpeg_video_stdin { + if let Some(ffmpeg_video_stdin) = &self.ffmpeg_stdin { let mut video_stdin_lock = ffmpeg_video_stdin.lock().await; - *video_stdin_lock = Some(video_stdin); + *video_stdin_lock = Some(ffmpeg_stdin); drop(video_stdin_lock); - println!("Video stdin set"); + println!("Ffmpeg stdin set"); } - if custom_device != Some("None") { + if needs_audio { println!("Starting audio channel senders..."); + tokio::spawn(async move { - while let Some(bytes) = &audio_channel_receiver.lock().await.as_mut().unwrap().recv().await { - if let Some(audio_stdin_arc) = &ffmpeg_audio_stdin{ - let mut audio_stdin_guard = audio_stdin_arc.lock().await; - if let Some(ref mut stdin) = *audio_stdin_guard { - stdin.write_all(&bytes).await.expect("Failed to write audio data to FFmpeg stdin"); - } - drop(audio_stdin_guard); - } + let mut audio_pipe = File::create(audio_pipe_path).await.unwrap(); + + while let Some(bytes) = &audio_channel_receiver + .lock() + .await + .as_mut() + .unwrap() + .recv() + .await + { + audio_pipe + .write_all(&bytes) + .await + .expect("Failed to write audio data to FFmpeg stdin"); } }); } println!("Starting video channel senders..."); tokio::spawn(async move { - while let Some(bytes) = &video_channel_receiver.lock().await.as_mut().unwrap().recv().await { - if let Some(video_stdin_arc) = &ffmpeg_video_stdin { - let mut video_stdin_guard = video_stdin_arc.lock().await; - if let Some(ref mut stdin) = *video_stdin_guard { - stdin.write_all(&bytes).await.expect("Failed to write video data to FFmpeg stdin"); - } - drop(video_stdin_guard); - } + let mut video_pipe = File::create(video_pipe_path).await.unwrap(); + + while let Some(bytes) = &video_channel_receiver + .lock() + .await + .as_mut() + .unwrap() + .recv() + .await + { + video_pipe + .write_all(&bytes) + .await + .expect("Failed to write video data to FFmpeg stdin"); } }); - - if custom_device != Some("None") { - self.ffmpeg_audio_process = audio_child; - } self.start_time = Some(Instant::now()); - self.audio_file_path = Some(audio_file_path_owned); - self.video_file_path = Some(video_file_path_owned); - self.ffmpeg_video_process = Some(video_child); + self.file_path = Some(chunks_file_path.to_string()); + self.ffmpeg_process = Some(ffmpeg_child); self.device_name = Some(device.name().expect("Failed to get device name")); - + println!("End of the start_audio_recording function"); - + Ok(()) } - pub fn trigger_play (&mut self) -> Result<(), &'static str> { - if let Some(ref mut stream) = self.stream { + pub fn trigger_play(&mut self) -> Result<(), &'static str> { + if let Some(ref mut stream) = self.audio_stream { stream.play().map_err(|_| "Failed to play stream")?; println!("Audio recording playing."); } else { @@ -562,19 +624,16 @@ impl MediaRecorder { let segment_duration = Duration::from_secs(3); let recording_duration = start_time.elapsed(); let expected_segments = recording_duration.as_secs() / segment_duration.as_secs(); - let audio_file_path = self.audio_file_path.as_ref().ok_or("Audio file path not set")?; - let video_file_path = self.video_file_path.as_ref().ok_or("Video file path not set")?; - let audio_segment_list_filename = format!("{}/segment_list.txt", audio_file_path); + let video_file_path = self.file_path.as_ref().ok_or("Video file path not set")?; let video_segment_list_filename = format!("{}/segment_list.txt", video_file_path); loop { - let audio_segments = std::fs::read_to_string(&audio_segment_list_filename).unwrap_or_default(); - let video_segments = std::fs::read_to_string(&video_segment_list_filename).unwrap_or_default(); + let video_segments = + std::fs::read_to_string(&video_segment_list_filename).unwrap_or_default(); - let audio_segment_count = audio_segments.lines().count(); let video_segment_count = video_segments.lines().count(); - if audio_segment_count >= expected_segments as usize && video_segment_count >= expected_segments as usize { + if video_segment_count >= expected_segments as usize { println!("All segments generated"); break; } @@ -583,17 +642,7 @@ impl MediaRecorder { } } - if let Some(ref ffmpeg_audio_stdin) = self.ffmpeg_audio_stdin { - let mut audio_stdin_guard = ffmpeg_audio_stdin.lock().await; - if let Some(mut audio_stdin) = audio_stdin_guard.take() { - if let Err(e) = audio_stdin.write_all(b"q\n").await { - eprintln!("Failed to send 'q' to audio FFmpeg process: {}", e); - } - let _ = audio_stdin.shutdown().await.map_err(|e| e.to_string()); - } - } - - if let Some(ref ffmpeg_video_stdin) = self.ffmpeg_video_stdin { + if let Some(ref ffmpeg_video_stdin) = self.ffmpeg_stdin { let mut video_stdin_guard = ffmpeg_video_stdin.lock().await; if let Some(mut video_stdin) = video_stdin_guard.take() { if let Err(e) = video_stdin.write_all(b"q\n").await { @@ -613,18 +662,14 @@ impl MediaRecorder { drop(sender); } - if let Some(ref mut stream) = self.stream { + if let Some(ref mut stream) = self.audio_stream { stream.pause().map_err(|_| "Failed to pause stream")?; println!("Audio recording paused."); } else { return Err("Original recording was not started".to_string()); } - if let Some(process) = &mut self.ffmpeg_audio_process { - let _ = process.kill().await.map_err(|e| e.to_string()); - } - - if let Some(process) = &mut self.ffmpeg_video_process { + if let Some(process) = &mut self.ffmpeg_process { let _ = process.kill().await.map_err(|e| e.to_string()); } @@ -632,33 +677,17 @@ impl MediaRecorder { Ok(()) } - async fn start_audio_ffmpeg_processes( - &self, - ffmpeg_binary_path: &str, - audio_ffmpeg_command: &[String], - ) -> Result<(Child, ChildStdin), Error> { - let mut audio_process = start_recording_process(ffmpeg_binary_path, audio_ffmpeg_command).await.map_err(|e| { - eprintln!("Failed to start audio recording process: {}", e); - std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) - })?; - - let audio_stdin = audio_process.stdin.take().ok_or_else(|| { - eprintln!("Failed to take audio stdin"); - std::io::Error::new(std::io::ErrorKind::Other, "Failed to take audio stdin") - })?; - - Ok((audio_process, audio_stdin)) - } - - async fn start_video_ffmpeg_processes( + async fn start_ffmpeg_process( &self, ffmpeg_binary_path: &str, video_ffmpeg_command: &[String], ) -> Result<(Child, ChildStdin), Error> { - let mut video_process = start_recording_process(ffmpeg_binary_path, video_ffmpeg_command).await.map_err(|e| { - eprintln!("Failed to start video recording process: {}", e); - std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) - })?; + let mut video_process = start_recording_process(ffmpeg_binary_path, video_ffmpeg_command) + .await + .map_err(|e| { + eprintln!("Failed to start video recording process: {}", e); + std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) + })?; let video_stdin = video_process.stdin.take().ok_or_else(|| { eprintln!("Failed to take video stdin"); @@ -667,14 +696,17 @@ impl MediaRecorder { Ok((video_process, video_stdin)) } - } #[tauri::command] pub fn enumerate_audio_devices() -> Vec { let host = cpal::default_host(); - let default_device = host.default_input_device().expect("No default input device available"); - let default_device_name = default_device.name().expect("Failed to get default device name"); + let default_device = host + .default_input_device() + .expect("No default input device available"); + let default_device_name = default_device + .name() + .expect("Failed to get default device name"); let devices = host.devices().expect("Failed to get devices"); let mut input_device_names: Vec = devices @@ -694,11 +726,11 @@ pub fn enumerate_audio_devices() -> Vec { input_device_names } -use tokio::io::{BufReader, AsyncBufReadExt}; +use tokio::io::{AsyncBufReadExt, BufReader}; async fn start_recording_process( - ffmpeg_binary_path_str: &str, - args: &[String], + ffmpeg_binary_path_str: &str, + args: &[String], ) -> Result { let mut process = Command::new(ffmpeg_binary_path_str) .args(args) @@ -707,7 +739,7 @@ async fn start_recording_process( .spawn()?; if let Some(process_stderr) = process.stderr.take() { - tokio::spawn(async move { + tokio::spawn(async move { let mut process_reader = BufReader::new(process_stderr).lines(); while let Ok(Some(line)) = process_reader.next_line().await { eprintln!("FFmpeg process STDERR: {}", line); @@ -725,7 +757,7 @@ async fn wait_for_start_times( loop { let audio_start_locked = audio_start_time.lock().await; let video_start_locked = video_start_time.lock().await; - + if audio_start_locked.is_some() && video_start_locked.is_some() { let audio_start = *audio_start_locked.as_ref().unwrap(); let video_start = *video_start_locked.as_ref().unwrap(); @@ -740,7 +772,7 @@ async fn wait_for_start_times( async fn adjust_ffmpeg_commands_based_on_start_times( audio_start_time: Arc>>, video_start_time: Arc>>, - ffmpeg_audio_command: &mut Vec, + // ffmpeg_audio_command: &mut Vec, ffmpeg_video_command: &mut Vec, ) { let (audio_start, video_start) = wait_for_start_times(audio_start_time, video_start_time).await; @@ -755,22 +787,23 @@ async fn adjust_ffmpeg_commands_based_on_start_times( println!("Video start: {:?}", video_start); // Convert the duration difference to a float representing seconds - let offset_seconds = duration_difference.as_secs() as f64 - + duration_difference.subsec_nanos() as f64 * 1e-9; + let offset_seconds = + duration_difference.as_secs() as f64 + duration_difference.subsec_nanos() as f64 * 1e-9; // Depending on which started first, adjust the relevant FFmpeg command if audio_start > video_start { // Offset the video start time - ffmpeg_video_command.splice(0..0, vec![ - "-itsoffset".to_string(), format!("{:.3}", offset_seconds) - ]); + ffmpeg_video_command.splice( + 0..0, + vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], + ); println!("Applying -itsoffset {:.3} to video", offset_seconds); } else if video_start > audio_start { // Offset the audio start time - ffmpeg_audio_command.splice(0..0, vec![ - "-itsoffset".to_string(), format!("{:.3}", offset_seconds) - ]); + // ffmpeg_audio_command.splice( + // 0..0, + // vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], + // ); println!("Applying -itsoffset {:.3} to audio", offset_seconds); } - -} \ No newline at end of file +} diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 73b26010..2cd3b788 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -1,28 +1,30 @@ -use std::path::{Path, PathBuf}; +use futures::future::join_all; +use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use std::io::{self, BufReader, BufRead, ErrorKind}; use std::fs::File; -use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; -use tokio::sync:: {Mutex}; -use tokio::task::JoinHandle; -use tokio::time::{Duration}; -use serde::{Serialize, Deserialize}; +use std::io::{self, BufRead, BufReader, ErrorKind}; +use std::path::{Path, PathBuf}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use tauri::State; -use futures::future::join_all; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +use tokio::time::Duration; -use crate::upload::{upload_file}; +use crate::upload::{self, upload_file}; use crate::media::MediaRecorder; pub struct RecordingState { - pub media_process: Option, - pub recording_options: Option, - pub shutdown_flag: Arc, - pub video_uploading_finished: Arc, - pub audio_uploading_finished: Arc, - pub data_dir: Option, - pub max_screen_width: usize, - pub max_screen_height: usize, + pub media_process: Option, + pub recording_options: Option, + pub shutdown_flag: Arc, + pub uploading_finished: Arc, + pub data_dir: Option, + pub max_screen_width: usize, + pub max_screen_height: usize, } unsafe impl Send for RecordingState {} @@ -32,83 +34,99 @@ unsafe impl Sync for MediaRecorder {} #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RecordingOptions { - pub user_id: String, - pub video_id: String, - pub screen_index: String, - pub video_index: String, - pub audio_name: String, - pub aws_region: String, - pub aws_bucket: String, + pub user_id: String, + pub video_id: String, + pub screen_index: String, + pub video_index: String, + pub audio_name: String, + pub aws_region: String, + pub aws_bucket: String, } #[tauri::command] pub async fn start_dual_recording( - state: State<'_, Arc>>, - options: RecordingOptions, + state: State<'_, Arc>>, + options: RecordingOptions, ) -> Result<(), String> { - println!("Starting screen recording..."); - let mut state_guard = state.lock().await; - - let shutdown_flag = Arc::new(AtomicBool::new(false)); - - let data_dir = state_guard.data_dir.as_ref() - .ok_or("Data directory is not set in the recording state".to_string())?.clone(); - - println!("data_dir: {:?}", data_dir); - - let audio_chunks_dir = data_dir.join("chunks/audio"); - let video_chunks_dir = data_dir.join("chunks/video"); - let screenshot_dir = data_dir.join("screenshots"); - - clean_and_create_dir(&audio_chunks_dir)?; - clean_and_create_dir(&video_chunks_dir)?; - clean_and_create_dir(&screenshot_dir)?; - - let audio_name = if options.audio_name.is_empty() { - None - } else { - Some(options.audio_name.clone()) - }; - - let media_recording_preparation = prepare_media_recording(&options, &audio_chunks_dir, &video_chunks_dir, &screenshot_dir, audio_name, state_guard.max_screen_width, state_guard.max_screen_height); - let media_recording_result = media_recording_preparation.await.map_err(|e| e.to_string())?; - - state_guard.media_process = Some(media_recording_result); - state_guard.recording_options = Some(options.clone()); - state_guard.shutdown_flag = shutdown_flag.clone(); - state_guard.video_uploading_finished = Arc::new(AtomicBool::new(false)); - state_guard.audio_uploading_finished = Arc::new(AtomicBool::new(false)); - - let is_local_mode = match dotenv_codegen::dotenv!("NEXT_PUBLIC_LOCAL_MODE") { - "true" => true, - _ => false, - }; - - if !is_local_mode { - let screen_upload = start_upload_loop(video_chunks_dir.clone(), options.clone(), "video".to_string(), shutdown_flag.clone(), state_guard.video_uploading_finished.clone()); - let audio_upload = start_upload_loop(audio_chunks_dir, options.clone(), "audio".to_string(), shutdown_flag.clone(), state_guard.audio_uploading_finished.clone()); - - drop(state_guard); - - println!("Starting upload loops..."); - - match tokio::try_join!(screen_upload, audio_upload) { - Ok(_) => { - println!("Both upload loops completed successfully."); - }, - Err(e) => { - eprintln!("An error occurred: {}", e); - }, - } - } else { - println!("Skipping upload loops due to NEXT_PUBLIC_LOCAL_MODE being set to 'true'."); - } - - Ok(()) + println!("Starting screen recording..."); + let mut state_guard = state.lock().await; + + let shutdown_flag = Arc::new(AtomicBool::new(false)); + + let data_dir = state_guard + .data_dir + .as_ref() + .ok_or("Data directory is not set in the recording state".to_string())? + .clone(); + + println!("data_dir: {:?}", data_dir); + + let screenshot_dir = data_dir.join("screenshots"); + let chunks_dir = data_dir.join("chunks"); + + clean_and_create_dir(&chunks_dir)?; + clean_and_create_dir(&screenshot_dir)?; + + let audio_name = if options.audio_name.is_empty() { + None + } else { + Some(options.audio_name.clone()) + }; + + let media_recording_preparation = prepare_media_recording( + &options, + &chunks_dir, + &screenshot_dir, + audio_name, + state_guard.max_screen_width, + state_guard.max_screen_height, + ); + let media_recording_result = media_recording_preparation + .await + .map_err(|e| e.to_string())?; + + state_guard.media_process = Some(media_recording_result); + state_guard.recording_options = Some(options.clone()); + state_guard.shutdown_flag = shutdown_flag.clone(); + state_guard.uploading_finished = Arc::new(AtomicBool::new(false)); + + let is_local_mode = match dotenv_codegen::dotenv!("NEXT_PUBLIC_LOCAL_MODE") { + "true" => true, + _ => false, + }; + + if !is_local_mode { + let upload = start_upload_loop( + chunks_dir.clone(), + options.clone(), + shutdown_flag.clone(), + state_guard.uploading_finished.clone(), + ); + + drop(state_guard); + + println!("Starting upload loops..."); + + match upload.await { + Ok(_) => { + println!("Both upload loops completed successfully."); + } + Err(e) => { + eprintln!("An error occurred: {}", e); + } + } + } else { + println!("Skipping upload loops due to NEXT_PUBLIC_LOCAL_MODE being set to 'true'."); + } + + Ok(()) } #[tauri::command] -pub async fn stop_all_recordings(state: State<'_, Arc>>) -> Result<(), String> { +#[specta::specta] +pub async fn stop_all_recordings( + state: State<'_, Arc>>, +) -> Result<(), String> { let mut guard = state.lock().await; println!("Stopping media recording..."); @@ -117,7 +135,10 @@ pub async fn stop_all_recordings(state: State<'_, Arc>>) - if let Some(mut media_process) = guard.media_process.take() { println!("Stopping media recording..."); - media_process.stop_media_recording().await.expect("Failed to stop media recording"); + media_process + .stop_media_recording() + .await + .expect("Failed to stop media recording"); } let is_local_mode = match dotenv_codegen::dotenv!("NEXT_PUBLIC_LOCAL_MODE") { @@ -126,8 +147,7 @@ pub async fn stop_all_recordings(state: State<'_, Arc>>) - }; if !is_local_mode { - while !guard.video_uploading_finished.load(Ordering::SeqCst) - || !guard.audio_uploading_finished.load(Ordering::SeqCst) { + while !guard.uploading_finished.load(Ordering::SeqCst) { println!("Waiting for uploads to finish..."); tokio::time::sleep(Duration::from_millis(50)).await; } @@ -146,24 +166,23 @@ fn clean_and_create_dir(dir: &Path) -> Result<(), String> { std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; if !dir.to_string_lossy().contains("screenshots") { - let segment_list_path = dir.join("segment_list.txt"); - match File::open(&segment_list_path) { - Ok(_) => Ok(()), - Err(ref e) if e.kind() == ErrorKind::NotFound => { - File::create(&segment_list_path).map_err(|e| e.to_string())?; - Ok(()) - }, - Err(e) => Err(e.to_string()), - } + let segment_list_path = dir.join("segment_list.txt"); + match File::open(&segment_list_path) { + Ok(_) => Ok(()), + Err(ref e) if e.kind() == ErrorKind::NotFound => { + File::create(&segment_list_path).map_err(|e| e.to_string())?; + Ok(()) + } + Err(e) => Err(e.to_string()), + } } else { - Ok(()) + Ok(()) } } async fn start_upload_loop( chunks_dir: PathBuf, options: RecordingOptions, - video_type: String, shutdown_flag: Arc, uploading_finished: Arc, ) -> Result<(), String> { @@ -189,12 +208,17 @@ async fn start_upload_loop( let segment_path = chunks_dir.join(segment_filename); if segment_path.is_file() { let options_clone = options.clone(); - let video_type_clone = video_type.clone(); let segment_path_clone = segment_path.clone(); upload_tasks.push(tokio::spawn(async move { let filepath_str = segment_path_clone.to_str().unwrap_or_default().to_owned(); - println!("Uploading video for {}: {}", video_type_clone, filepath_str); - upload_file(Some(options_clone), filepath_str, video_type_clone).await.map(|_| ()) + println!("Uploading video: {}", filepath_str); + upload_file( + Some(options_clone), + filepath_str, + upload::FileType::VideoWithAudio, + ) + .await + .map(|_| ()) })); } watched_segments.insert(segment_filename.clone()); @@ -226,18 +250,25 @@ fn load_segment_list(segment_list_path: &Path) -> io::Result> { } async fn prepare_media_recording( - options: &RecordingOptions, - audio_chunks_dir: &Path, - screenshot_dir: &Path, - video_chunks_dir: &Path, - audio_name: Option, - max_screen_width: usize, - max_screen_height: usize, + options: &RecordingOptions, + screenshot_dir: &Path, + video_chunks_dir: &Path, + audio_name: Option, + max_screen_width: usize, + max_screen_height: usize, ) -> Result { - let mut media_recorder = MediaRecorder::new(); - let audio_file_path = audio_chunks_dir.to_str().unwrap(); - let video_file_path = video_chunks_dir.to_str().unwrap(); - let screenshot_dir_path = screenshot_dir.to_str().unwrap(); - media_recorder.start_media_recording(options.clone(), audio_file_path, screenshot_dir_path, video_file_path, audio_name.as_ref().map(String::as_str), max_screen_width, max_screen_height).await?; - Ok(media_recorder) -} \ No newline at end of file + let mut media_recorder = MediaRecorder::new(); + let video_file_path = video_chunks_dir.to_str().unwrap(); + let screenshot_dir_path = screenshot_dir.to_str().unwrap(); + media_recorder + .start_media_recording( + options.clone(), + screenshot_dir_path, + video_file_path, + audio_name.as_ref().map(String::as_str), + max_screen_width, + max_screen_height, + ) + .await?; + Ok(media_recorder) +} diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 2c339f0f..73dde999 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -1,25 +1,37 @@ +use regex::Regex; use reqwest; -use std::fs::File; -use std::io::Read; +use serde_json::Value as JsonValue; use std::path::Path; use std::process::{Command, Output}; use std::str; -use std::fs; -use regex::Regex; -use serde_json::Value as JsonValue; use crate::recording::RecordingOptions; -use crate::utils::{ffmpeg_path_as_str}; +use crate::utils::ffmpeg_path_as_str; + +pub enum FileType { + VideoWithAudio, + Screenshot, +} + +impl std::fmt::Display for FileType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + FileType::VideoWithAudio => write!(f, "video-with-audio"), + FileType::Screenshot => write!(f, "screenshot"), + } + } +} pub async fn upload_file( options: Option, file_path: String, - file_type: String, + file_type: FileType, ) -> Result { if let Some(ref options) = options { println!("Uploading video..."); - let duration = get_video_duration(&file_path).map_err(|e| format!("Failed to get video duration: {}", e))?; + let duration = get_video_duration(&file_path) + .map_err(|e| format!("Failed to get video duration: {}", e))?; let duration_str = duration.to_string(); let file_name = Path::new(&file_path) @@ -28,40 +40,48 @@ pub async fn upload_file( .ok_or("Invalid file path")? .to_string(); - let file_key = format!("{}/{}/{}/{}", options.user_id, options.video_id, file_type, file_name); + let file_key = format!( + "{}/{}/{}/{}", + options.user_id, + options.video_id, + file_type.to_string(), + file_name + ); let server_url_base: &'static str = dotenv_codegen::dotenv!("NEXT_PUBLIC_URL"); let server_url = format!("{}/api/upload/signed", server_url_base); - - let body: serde_json::Value; - - if file_type == "video" { - let (codec_name, width, height, frame_rate, bit_rate) = log_video_info(&file_path).map_err(|e| format!("Failed to log video info: {}", e))?; - - body = serde_json::json!({ - "userId": options.user_id, - "fileKey": file_key, - "awsBucket": options.aws_bucket, - "awsRegion": options.aws_region, - "duration": duration_str, - "resolution": format!("{}x{}", width, height), - "framerate": frame_rate, - "bandwidth": bit_rate, - "videoCodec": codec_name, - }); - } else { - body = serde_json::json!({ - "userId": options.user_id, - "fileKey": file_key, - "awsBucket": options.aws_bucket, - "awsRegion": options.aws_region, - "duration": duration_str, - }); - } + let body = match file_type { + FileType::VideoWithAudio => { + let (codec_name, width, height, frame_rate, bit_rate) = log_video_info(&file_path) + .map_err(|e| format!("Failed to log video info: {}", e))?; + + serde_json::json!({ + "userId": options.user_id, + "fileKey": file_key, + "awsBucket": options.aws_bucket, + "awsRegion": options.aws_region, + "duration": duration_str, + "resolution": format!("{}x{}", width, height), + "framerate": frame_rate, + "bandwidth": bit_rate, + "videoCodec": codec_name, + }) + } + FileType::Screenshot => { + serde_json::json!({ + "userId": options.user_id, + "fileKey": file_key, + "awsBucket": options.aws_bucket, + "awsRegion": options.aws_region, + "duration": duration_str, + }) + } + }; let client = reqwest::Client::new(); - let server_response = client.post(server_url) + let server_response = client + .post(server_url) .json(&body) .send() .await @@ -72,34 +92,37 @@ pub async fn upload_file( println!("Server response: {}", server_response); - // Deserialize the server response let presigned_post_data: JsonValue = serde_json::from_str(&server_response) .map_err(|e| format!("Failed to deserialize server response: {}", e))?; // Construct the multipart form for the file upload - let fields = presigned_post_data["presignedPostData"]["fields"].as_object() + let fields = presigned_post_data["presignedPostData"]["fields"] + .as_object() .ok_or("Fields object is missing or not an object")?; - + let mut form = reqwest::multipart::Form::new(); - + for (key, value) in fields.iter() { - let value_str = value.as_str() + let value_str = value + .as_str() .ok_or(format!("Value for key '{}' is not a string", key))?; form = form.text(key.to_string(), value_str.to_owned()); } println!("Uploading file: {}", file_path); - + let mime_type = if file_path.to_lowercase().ends_with(".aac") { "audio/aac" - } else if file_path.to_lowercase().ends_with(".webm") { - "audio/webm" + } else if file_path.to_lowercase().ends_with(".webm") { + "audio/webm" } else { "video/mp2t" }; - let file_bytes = tokio::fs::read(&file_path).await.map_err(|e| format!("Failed to read file: {}", e))?; + let file_bytes = tokio::fs::read(&file_path) + .await + .map_err(|e| format!("Failed to read file: {}", e))?; let file_part = reqwest::multipart::Part::bytes(file_bytes) .file_name(file_name.clone()) .mime_str(mime_type) @@ -107,15 +130,13 @@ pub async fn upload_file( form = form.part("file", file_part); - let post_url = presigned_post_data["presignedPostData"]["url"].as_str() + let post_url = presigned_post_data["presignedPostData"]["url"] + .as_str() .ok_or("URL is missing or not a string")?; println!("Uploading file to: {}", post_url); - let response = client.post(post_url) - .multipart(form) - .send() - .await; + let response = client.post(post_url).multipart(form).send().await; match response { Ok(response) if response.status().is_success() => { @@ -123,9 +144,18 @@ pub async fn upload_file( } Ok(response) => { let status = response.status(); - let error_body = response.text().await.unwrap_or_else(|_| "".to_string()); - eprintln!("Failed to upload file. Status: {}. Body: {}", status, error_body); - return Err(format!("Failed to upload file. Status: {}. Body: {}", status, error_body)); + let error_body = response + .text() + .await + .unwrap_or_else(|_| "".to_string()); + eprintln!( + "Failed to upload file. Status: {}. Body: {}", + status, error_body + ); + return Err(format!( + "Failed to upload file. Status: {}. Body: {}", + status, error_body + )); } Err(e) => { return Err(format!("Failed to send upload file request: {}", e)); @@ -191,13 +221,14 @@ fn log_video_info(file_path: &str) -> Result<(String, String, String, String, St let codec_name = info_parts[0].to_string(); let width: String = info_parts[1].to_string(); let height: String = info_parts[2].to_string(); - + // Parse frame rate as a fraction and convert to float let frame_rate_parts: Vec<&str> = info_parts[3].split('/').collect(); - let frame_rate: f64 = frame_rate_parts[0].parse::().unwrap() / frame_rate_parts[1].parse::().unwrap(); + let frame_rate: f64 = + frame_rate_parts[0].parse::().unwrap() / frame_rate_parts[1].parse::().unwrap(); let frame_rate: String = frame_rate.to_string(); - + let bit_rate: String = info_parts[4].to_string(); Ok((codec_name, width, height, frame_rate, bit_rate)) -} \ No newline at end of file +} diff --git a/apps/desktop/src-tauri/src/utils.rs b/apps/desktop/src-tauri/src/utils.rs index 135b276d..d879ffff 100644 --- a/apps/desktop/src-tauri/src/utils.rs +++ b/apps/desktop/src-tauri/src/utils.rs @@ -111,8 +111,3 @@ pub fn create_named_pipe(path: &str) -> Result<(), nix::Error> { unistd::mkfifo(path, stat::Mode::S_IRWXU)?; Ok(()) } - -pub fn remove_named_pipe(path: &str) -> Result<(), std::io::Error> { - std::fs::remove_file(path)?; - Ok(()) -} \ No newline at end of file diff --git a/apps/web/app/api/upload/mux/create/route.ts b/apps/web/app/api/upload/mux/create/route.ts index bea163b5..6122648d 100644 --- a/apps/web/app/api/upload/mux/create/route.ts +++ b/apps/web/app/api/upload/mux/create/route.ts @@ -97,8 +97,7 @@ export async function GET(request: NextRequest) { } const bucket = process.env.CAP_AWS_BUCKET || ""; - const videoPrefix = `${userId}/${videoId}/video/`; - const audioPrefix = `${userId}/${videoId}/audio/`; + const videoPrefix = `${userId}/${videoId}/video-with-audio/`; try { const s3Client = new S3Client({ @@ -114,13 +113,7 @@ export async function GET(request: NextRequest) { Prefix: videoPrefix, }); - const audioSegmentCommand = new ListObjectsV2Command({ - Bucket: bucket, - Prefix: audioPrefix, - }); - const videoSegments = await s3Client.send(videoSegmentCommand); - const audioSegments = await s3Client.send(audioSegmentCommand); const videoSegmentKeys = (videoSegments.Contents || []).map( (object) => `s3://${bucket}/${object.Key}` @@ -148,10 +141,6 @@ export async function GET(request: NextRequest) { ); } - const audioSegmentKeys = (audioSegments.Contents || []).map( - (object) => `s3://${bucket}/${object.Key}` - ); - const mediaConvertClient = new MediaConvertClient({ region: process.env.CAP_AWS_REGION || "", credentials: { @@ -165,18 +154,14 @@ export async function GET(request: NextRequest) { const createJobCommand = new CreateJobCommand({ Role: process.env.CAP_AWS_MEDIACONVERT_ROLE_ARN || "", Settings: { - Inputs: videoSegmentKeys.map((videoSegmentKey, index) => { - const audioSegmentKey = audioSegmentKeys[index]; + Inputs: videoSegmentKeys.map((videoSegmentKey) => { return { FileInput: videoSegmentKey, - ...(audioSegmentKey && { - AudioSelectors: { - "Audio Selector 1": { - DefaultSelection: "DEFAULT", - ExternalAudioFileInput: audioSegmentKey, - }, + AudioSelectors: { + "Audio Selector 1": { + DefaultSelection: "DEFAULT", }, - }), + }, VideoSelector: {}, TimecodeSource: "ZEROBASED", }; @@ -218,21 +203,19 @@ export async function GET(request: NextRequest) { }, }, }, - ...(audioSegmentKeys.length > 0 && { - AudioDescriptions: [ - { - AudioSourceName: "Audio Selector 1", - CodecSettings: { - Codec: "AAC", - AacSettings: { - Bitrate: 128000, - CodingMode: "CODING_MODE_2_0", - SampleRate: 48000, - }, + AudioDescriptions: [ + { + AudioSourceName: "Audio Selector 1", + CodecSettings: { + Codec: "AAC", + AacSettings: { + Bitrate: 128000, + CodingMode: "CODING_MODE_2_0", + SampleRate: 48000, }, }, - ], - }), + }, + ], }, ], }, From c5e81b81b8c7d44b153af1cc05992b29de00a694 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 16:49:01 +0800 Subject: [PATCH 03/28] cleanup --- apps/desktop/src-tauri/src/media.rs | 43 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index 4b0da1a4..af1371df 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -456,8 +456,8 @@ impl MediaRecorder { }); println!("Starting audio recording and processing..."); - let video_output_chunk_pattern = format!("{chunks_file_path}/video_recording_%03d.ts"); - let video_segment_list_filename = format!("{chunks_file_path}/segment_list.txt"); + let output_chunk_pattern = format!("{chunks_file_path}/recording_%03d.ts"); + let segment_list_filename = format!("{chunks_file_path}/segment_list.txt"); let mut audio_filters = Vec::new(); @@ -519,7 +519,7 @@ impl MediaRecorder { ["-f", "segment"], ["-segment_time", "3"], ["-segment_time_delta", "0.01"], - ["-segment_list", &video_segment_list_filename], + ["-segment_list", &segment_list_filename], ["-segment_format", "ts"], ["-movflags", "frag_keyframe+empty_moov"], ["-reset_timestamps", "1"], @@ -529,7 +529,7 @@ impl MediaRecorder { .into_iter() .flatten() .map(|s| s.to_string()) - .chain([video_output_chunk_pattern]) + .chain([output_chunk_pattern]) .collect(); if needs_audio { @@ -550,10 +550,10 @@ impl MediaRecorder { .map_err(|e| e.to_string())?; println!("Ffmpeg process started"); - if let Some(ffmpeg_video_stdin) = &self.ffmpeg_stdin { - let mut video_stdin_lock = ffmpeg_video_stdin.lock().await; - *video_stdin_lock = Some(ffmpeg_stdin); - drop(video_stdin_lock); + if let Some(ffmpeg_stdin) = &self.ffmpeg_stdin { + let mut stdin_lock = ffmpeg_stdin.lock().await; + *stdin_lock = Some(ffmpeg_stdin); + drop(stdin_lock); println!("Ffmpeg stdin set"); } @@ -581,7 +581,7 @@ impl MediaRecorder { println!("Starting video channel senders..."); tokio::spawn(async move { - let mut video_pipe = File::create(video_pipe_path).await.unwrap(); + let mut pipe = File::create(video_pipe_path).await.unwrap(); while let Some(bytes) = &video_channel_receiver .lock() @@ -591,8 +591,7 @@ impl MediaRecorder { .recv() .await { - video_pipe - .write_all(&bytes) + pipe.write_all(&bytes) .await .expect("Failed to write video data to FFmpeg stdin"); } @@ -624,16 +623,15 @@ impl MediaRecorder { let segment_duration = Duration::from_secs(3); let recording_duration = start_time.elapsed(); let expected_segments = recording_duration.as_secs() / segment_duration.as_secs(); - let video_file_path = self.file_path.as_ref().ok_or("Video file path not set")?; - let video_segment_list_filename = format!("{}/segment_list.txt", video_file_path); + let file_path = self.file_path.as_ref().ok_or("File path not set")?; + let segment_list_filename = format!("{}/segment_list.txt", file_path); loop { - let video_segments = - std::fs::read_to_string(&video_segment_list_filename).unwrap_or_default(); + let segments = std::fs::read_to_string(&segment_list_filename).unwrap_or_default(); - let video_segment_count = video_segments.lines().count(); + let segment_count = segments.lines().count(); - if video_segment_count >= expected_segments as usize { + if segment_count >= expected_segments as usize { println!("All segments generated"); break; } @@ -642,13 +640,13 @@ impl MediaRecorder { } } - if let Some(ref ffmpeg_video_stdin) = self.ffmpeg_stdin { - let mut video_stdin_guard = ffmpeg_video_stdin.lock().await; - if let Some(mut video_stdin) = video_stdin_guard.take() { - if let Err(e) = video_stdin.write_all(b"q\n").await { + if let Some(ref ffmpeg_stdin) = self.ffmpeg_stdin { + let mut stdin_guard = ffmpeg_stdin.lock().await; + if let Some(mut stdin) = stdin_guard.take() { + if let Err(e) = stdin.write_all(b"q\n").await { eprintln!("Failed to send 'q' to video FFmpeg process: {}", e); } - let _ = video_stdin.shutdown().await.map_err(|e| e.to_string()); + let _ = stdin.shutdown().await.map_err(|e| e.to_string()); } } @@ -772,7 +770,6 @@ async fn wait_for_start_times( async fn adjust_ffmpeg_commands_based_on_start_times( audio_start_time: Arc>>, video_start_time: Arc>>, - // ffmpeg_audio_command: &mut Vec, ffmpeg_video_command: &mut Vec, ) { let (audio_start, video_start) = wait_for_start_times(audio_start_time, video_start_time).await; From 2deae8e004f9d508a442ecfcaaaa0907cd8e356a Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 17:22:23 +0800 Subject: [PATCH 04/28] cleanup --- apps/desktop/src-tauri/src/main.rs | 42 +++++---- apps/desktop/src-tauri/src/media.rs | 120 +++++++++++++----------- apps/desktop/src-tauri/src/recording.rs | 9 +- 3 files changed, 94 insertions(+), 77 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 007608ca..da239710 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -35,10 +35,9 @@ use ffmpeg_sidecar::{ use winit::monitor::{MonitorHandle, VideoMode}; - -fn main() { +fn main() { let _ = fix_path_env::fix(); - + std::panic::set_hook(Box::new(|info| { eprintln!("Thread panicked: {:?}", info); })); @@ -73,7 +72,7 @@ fn main() { handle_ffmpeg_installation().expect("Failed to install FFmpeg"); - #[command] + #[tauri::command] async fn start_server(window: Window) -> Result { start(move |url| { let _ = window.emit("redirect_uri", url); @@ -147,10 +146,14 @@ fn main() { })); let event_loop = winit::event_loop::EventLoop::new().expect("Failed to create event loop"); - let monitor: MonitorHandle = event_loop.primary_monitor().expect("No primary monitor found"); + let monitor: MonitorHandle = event_loop + .primary_monitor() + .expect("No primary monitor found"); let video_modes: Vec = monitor.video_modes().collect(); - let max_mode = video_modes.iter().max_by_key(|mode| mode.size().width * mode.size().height); + let max_mode = video_modes + .iter() + .max_by_key(|mode| mode.size().width * mode.size().height); let (max_width, max_height) = if let Some(max_mode) = max_mode { println!("Maximum resolution: {:?}", max_mode.size()); @@ -162,10 +165,10 @@ fn main() { #[derive(serde::Deserialize, PartialEq)] enum DeviceKind { - #[serde(alias="videoinput")] + #[serde(alias = "videoinput")] Video, - #[serde(alias="audioinput")] - Audio + #[serde(alias = "audioinput")] + Audio, } #[derive(serde::Deserialize)] @@ -173,7 +176,7 @@ fn main() { struct MediaDevice { id: String, kind: DeviceKind, - label: String + label: String, } fn create_tray_menu(submenus: Option>) -> SystemTrayMenu { @@ -191,7 +194,10 @@ fn main() { .add_item(CustomMenuItem::new("quit".to_string(), "Quit").accelerator("CmdOrControl+Q")) } - let tray = SystemTray::new().with_menu(create_tray_menu(None)).with_menu_on_left_click(false).with_title("Cap"); + let tray = SystemTray::new() + .with_menu(create_tray_menu(None)) + .with_menu_on_left_click(false) + .with_title("Cap"); tauri::Builder::default() .plugin(tauri_plugin_oauth::init()) @@ -199,14 +205,14 @@ fn main() { .setup(move |app| { let handle = app.handle(); - if let Some(options_window) = app.get_window("main") { + if let Some(options_window) = app.get_window("main") { let _ = options_window.move_window(Position::Center); #[cfg(target_os = "macos")] apply_vibrancy(&options_window, NSVisualEffectMaterial::MediumLight, None, Some(16.0)).expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS"); #[cfg(target_os = "windows")] apply_blur(&options_window, Some((255, 255, 255, 255))).expect("Unsupported platform! 'apply_blur' is only supported on Windows"); - + set_shadow(&options_window, true).expect("Unsupported platform!"); } @@ -250,7 +256,7 @@ fn main() { } } }); - + let tray_handle = app.tray_handle(); app.listen_global("media-devices-set", move|event| { #[derive(serde::Deserialize)] @@ -266,7 +272,7 @@ fn main() { let id_prefix = if kind == DeviceKind::Video { "video" } else { - "audio" + "audio" }; let mut none_item = CustomMenuItem::new(format!("in_{}_none", id_prefix), "None"); if selected_device.is_none() { @@ -278,13 +284,13 @@ fn main() { .filter(|device| device.kind == kind) .fold(initial, |tray_items, device| { let mut menu_item = CustomMenuItem::new(format!("in_{}_{}", id_prefix, device.id), &device.label); - + if let Some(selected) = selected_device { if selected.label == device.label { menu_item = menu_item.selected(); } } - + tray_items.add_item(menu_item) }) } @@ -349,7 +355,7 @@ fn main() { let kind = if item_id.contains("video") { "videoinput" } else { "audioinput" }; app.emit_all("tray-set-device-id", SetDevicePayload { - device_type: kind.to_string(), + device_type: kind.to_string(), id: if device_id == "none" { None } else { Some(device_id) } }).expect("Failed to emit tray set media device event to windows"); } diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index af1371df..e837bd56 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -479,36 +479,51 @@ impl MediaRecorder { std::fs::remove_file(&audio_pipe_path).ok(); create_named_pipe(&audio_pipe_path).map_err(|e| e.to_string())?; + let time_offset = if needs_audio { + println!("Adjusting FFmpeg commands based on start times..."); + create_time_offset_args(&audio_start_time, &video_start_time).await + } else { + None + }; + let size = format!("{}x{}", adjusted_width, adjusted_height); - let mut ffmpeg_command = vec![ + let mut ffmpeg_command = flatten_str_args([ ["-f", "rawvideo"], ["-pix_fmt", "bgra"], ["-s", &size], ["-r", "30"], ["-thread_queue_size", "4096"], ["-i", &video_pipe_path], - ]; + ]); - if needs_audio { - ffmpeg_command.extend([ - // in - ["-f", sample_format], - ["-ar", &sample_rate_str], - ["-ac", &channels_str], - ["-thread_queue_size", "4096"], - ["-i", &audio_pipe_path], - // out - [ - "-af", - "aresample=async=1:min_hard_comp=0.100000:first_pts=0", - ], - ["-c:a", "aac"], - ["-b:a", "128k"], - ["-async", "1"], - ]); + let mut audio_args = flatten_str_args([ + // in + ["-f", sample_format], + ["-ar", &sample_rate_str], + ["-ac", &channels_str], + ["-thread_queue_size", "4096"], + ["-i", &audio_pipe_path], + // out + [ + "-af", + "aresample=async=1:min_hard_comp=0.100000:first_pts=0", + ], + ["-c:a", "aac"], + ["-b:a", "128k"], + ["-async", "1"], + ]); + + match time_offset { + Some((TimeOffsetTarget::Video, args)) => ffmpeg_command.splice(0..0, args.clone()), + Some((TimeOffsetTarget::Audio, args)) => audio_args.splice(0..0, args.clone()), + None => {} }; - ffmpeg_command.extend([ + if needs_audio { + ffmpeg_command.extend(audio_args); + } + + ffmpeg_command.extend(flatten_str_args([ ["-vf", "fps=30,scale=in_range=full:out_range=limited"], ["-c:v", "libx264"], ["-preset", "ultrafast"], @@ -523,24 +538,9 @@ impl MediaRecorder { ["-segment_format", "ts"], ["-movflags", "frag_keyframe+empty_moov"], ["-reset_timestamps", "1"], - ]); + ])); - let mut ffmpeg_command = ffmpeg_command - .into_iter() - .flatten() - .map(|s| s.to_string()) - .chain([output_chunk_pattern]) - .collect(); - - if needs_audio { - println!("Adjusting FFmpeg commands based on start times..."); - adjust_ffmpeg_commands_based_on_start_times( - Arc::clone(&audio_start_time), - Arc::clone(&video_start_time), - &mut ffmpeg_command, - ) - .await; - } + ffmpeg_command.push(output_chunk_pattern); println!("Starting FFmpeg process..."); @@ -550,8 +550,8 @@ impl MediaRecorder { .map_err(|e| e.to_string())?; println!("Ffmpeg process started"); - if let Some(ffmpeg_stdin) = &self.ffmpeg_stdin { - let mut stdin_lock = ffmpeg_stdin.lock().await; + if let Some(ffmpeg_stdin_mutex) = &self.ffmpeg_stdin { + let mut stdin_lock = ffmpeg_stdin_mutex.lock().await; *stdin_lock = Some(ffmpeg_stdin); drop(stdin_lock); println!("Ffmpeg stdin set"); @@ -749,8 +749,8 @@ async fn start_recording_process( } async fn wait_for_start_times( - audio_start_time: Arc>>, - video_start_time: Arc>>, + audio_start_time: &Mutex>, + video_start_time: &Mutex>, ) -> (Instant, Instant) { loop { let audio_start_locked = audio_start_time.lock().await; @@ -767,11 +767,15 @@ async fn wait_for_start_times( } } -async fn adjust_ffmpeg_commands_based_on_start_times( - audio_start_time: Arc>>, - video_start_time: Arc>>, - ffmpeg_video_command: &mut Vec, -) { +pub enum TimeOffsetTarget { + Audio, + Video, +} + +async fn create_time_offset_args( + audio_start_time: &Mutex>, + video_start_time: &Mutex>, +) -> Option<(TimeOffsetTarget, Vec)> { let (audio_start, video_start) = wait_for_start_times(audio_start_time, video_start_time).await; let duration_difference = if audio_start > video_start { audio_start.duration_since(video_start) @@ -790,17 +794,25 @@ async fn adjust_ffmpeg_commands_based_on_start_times( // Depending on which started first, adjust the relevant FFmpeg command if audio_start > video_start { // Offset the video start time - ffmpeg_video_command.splice( - 0..0, - vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], - ); println!("Applying -itsoffset {:.3} to video", offset_seconds); + + Some(( + TimeOffsetTarget::Video, + vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], + )) } else if video_start > audio_start { // Offset the audio start time - // ffmpeg_audio_command.splice( - // 0..0, - // vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], - // ); println!("Applying -itsoffset {:.3} to audio", offset_seconds); + + Some(( + TimeOffsetTarget::Audio, + vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], + )) + } else { + None } } + +fn flatten_str_args<'a>(args: impl IntoIterator) -> Vec { + args.into_iter().flatten().map(|s| s.to_string()).collect() +} diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 2cd3b788..37fd38a0 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -123,14 +123,13 @@ pub async fn start_dual_recording( } #[tauri::command] -#[specta::specta] pub async fn stop_all_recordings( state: State<'_, Arc>>, ) -> Result<(), String> { let mut guard = state.lock().await; - + println!("Stopping media recording..."); - + guard.shutdown_flag.store(true, Ordering::SeqCst); if let Some(mut media_process) = guard.media_process.take() { @@ -152,7 +151,7 @@ pub async fn stop_all_recordings( tokio::time::sleep(Duration::from_millis(50)).await; } } - + println!("All recordings and uploads stopped."); Ok(()) @@ -227,7 +226,7 @@ async fn start_upload_loop( if !upload_tasks.is_empty() { let _ = join_all(upload_tasks).await; } - + tokio::time::sleep(Duration::from_millis(50)).await; } uploading_finished.store(true, Ordering::SeqCst); From dfb0d1c1f3c52f1af0fe4af431acea25435b40bf Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 17:24:58 +0800 Subject: [PATCH 05/28] segment --- apps/desktop/src-tauri/src/recording.rs | 10 +++------- apps/desktop/src-tauri/src/upload.rs | 6 +++--- apps/web/app/api/upload/mux/create/route.ts | 14 +++++++------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 37fd38a0..fe881ed0 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -211,13 +211,9 @@ async fn start_upload_loop( upload_tasks.push(tokio::spawn(async move { let filepath_str = segment_path_clone.to_str().unwrap_or_default().to_owned(); println!("Uploading video: {}", filepath_str); - upload_file( - Some(options_clone), - filepath_str, - upload::FileType::VideoWithAudio, - ) - .await - .map(|_| ()) + upload_file(Some(options_clone), filepath_str, upload::FileType::Segment) + .await + .map(|_| ()) })); } watched_segments.insert(segment_filename.clone()); diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 73dde999..57089ce8 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -9,14 +9,14 @@ use crate::recording::RecordingOptions; use crate::utils::ffmpeg_path_as_str; pub enum FileType { - VideoWithAudio, + Segment, Screenshot, } impl std::fmt::Display for FileType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - FileType::VideoWithAudio => write!(f, "video-with-audio"), + FileType::Segment => write!(f, "segment"), FileType::Screenshot => write!(f, "screenshot"), } } @@ -52,7 +52,7 @@ pub async fn upload_file( let server_url = format!("{}/api/upload/signed", server_url_base); let body = match file_type { - FileType::VideoWithAudio => { + FileType::Segment => { let (codec_name, width, height, frame_rate, bit_rate) = log_video_info(&file_path) .map_err(|e| format!("Failed to log video info: {}", e))?; diff --git a/apps/web/app/api/upload/mux/create/route.ts b/apps/web/app/api/upload/mux/create/route.ts index 6122648d..4de99e9e 100644 --- a/apps/web/app/api/upload/mux/create/route.ts +++ b/apps/web/app/api/upload/mux/create/route.ts @@ -97,7 +97,7 @@ export async function GET(request: NextRequest) { } const bucket = process.env.CAP_AWS_BUCKET || ""; - const videoPrefix = `${userId}/${videoId}/video-with-audio/`; + const segmentsPrefix = `${userId}/${videoId}/segment/`; try { const s3Client = new S3Client({ @@ -108,18 +108,18 @@ export async function GET(request: NextRequest) { }, }); - const videoSegmentCommand = new ListObjectsV2Command({ + const segmentsCommand = new ListObjectsV2Command({ Bucket: bucket, - Prefix: videoPrefix, + Prefix: segmentsPrefix, }); - const videoSegments = await s3Client.send(videoSegmentCommand); + const segments = await s3Client.send(segmentsCommand); - const videoSegmentKeys = (videoSegments.Contents || []).map( + const segmentKeys = (segments.Contents || []).map( (object) => `s3://${bucket}/${object.Key}` ); - if (videoSegmentKeys.length > 149) { + if (segmentKeys.length > 149) { await db .update(videos) .set({ skipProcessing: true }) @@ -154,7 +154,7 @@ export async function GET(request: NextRequest) { const createJobCommand = new CreateJobCommand({ Role: process.env.CAP_AWS_MEDIACONVERT_ROLE_ARN || "", Settings: { - Inputs: videoSegmentKeys.map((videoSegmentKey) => { + Inputs: segmentKeys.map((videoSegmentKey) => { return { FileInput: videoSegmentKey, AudioSelectors: { From 8047c2b91154f0d5ef360b0120190367aa8fcbb4 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 17:52:46 +0800 Subject: [PATCH 06/28] fix splice --- apps/desktop/src-tauri/src/media.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index e837bd56..669474da 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -514,9 +514,15 @@ impl MediaRecorder { ]); match time_offset { - Some((TimeOffsetTarget::Video, args)) => ffmpeg_command.splice(0..0, args.clone()), - Some((TimeOffsetTarget::Audio, args)) => audio_args.splice(0..0, args.clone()), - None => {} + Some((TimeOffsetTarget::Video, args)) => { + ffmpeg_command.splice(0..0, args.clone()).collect() + } + Some((TimeOffsetTarget::Audio, args)) => { + audio_args.splice(0..0, args.clone()).collect() + } + None => { + vec![] + } }; if needs_audio { From a1afe4428c7f9ae532a45f9bfe3230fb1fdc67f0 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 10 Jul 2024 12:15:23 +0800 Subject: [PATCH 07/28] use virtual workspace --- .gitignore | 3 +- .../src-tauri/Cargo.lock => Cargo.lock | 0 Cargo.toml | 3 ++ apps/desktop/src-tauri/Cargo.toml | 33 +++++++++++++++++-- .../src => crates}/capture/Cargo.lock | 0 .../src => crates}/capture/Cargo.toml | 0 .../src => crates}/capture/README.md | 0 .../src-tauri/src => crates}/capture/build.rs | 0 .../src => crates}/capture/src/common/dxgi.rs | 0 .../src => crates}/capture/src/common/mod.rs | 0 .../capture/src/common/quartz.rs | 0 .../src => crates}/capture/src/common/x11.rs | 0 .../src => crates}/capture/src/dxgi/ffi.rs | 0 .../src => crates}/capture/src/dxgi/mod.rs | 0 .../src => crates}/capture/src/lib.rs | 0 .../capture/src/quartz/capturer.rs | 0 .../capture/src/quartz/config.rs | 0 .../capture/src/quartz/display.rs | 0 .../src => crates}/capture/src/quartz/ffi.rs | 0 .../capture/src/quartz/frame.rs | 0 .../src => crates}/capture/src/quartz/mod.rs | 0 .../capture/src/x11/capturer.rs | 0 .../src => crates}/capture/src/x11/display.rs | 0 .../src => crates}/capture/src/x11/ffi.rs | 0 .../src => crates}/capture/src/x11/iter.rs | 0 .../src => crates}/capture/src/x11/mod.rs | 0 .../src => crates}/capture/src/x11/server.rs | 0 27 files changed, 36 insertions(+), 3 deletions(-) rename apps/desktop/src-tauri/Cargo.lock => Cargo.lock (100%) create mode 100644 Cargo.toml rename {apps/desktop/src-tauri/src => crates}/capture/Cargo.lock (100%) rename {apps/desktop/src-tauri/src => crates}/capture/Cargo.toml (100%) rename {apps/desktop/src-tauri/src => crates}/capture/README.md (100%) rename {apps/desktop/src-tauri/src => crates}/capture/build.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/common/dxgi.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/common/mod.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/common/quartz.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/common/x11.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/dxgi/ffi.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/dxgi/mod.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/lib.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/capturer.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/config.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/display.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/ffi.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/frame.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/quartz/mod.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/capturer.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/display.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/ffi.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/iter.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/mod.rs (100%) rename {apps/desktop/src-tauri/src => crates}/capture/src/x11/server.rs (100%) diff --git a/.gitignore b/.gitignore index 8d6ada89..b70ab983 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ dist-ssr .env .env.production .env.test +target # Editor directories and files .vscode/* @@ -26,4 +27,4 @@ dist-ssr *.sln *.sw? .turbo -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml diff --git a/apps/desktop/src-tauri/Cargo.lock b/Cargo.lock similarity index 100% rename from apps/desktop/src-tauri/Cargo.lock rename to Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..91e408b4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["apps/desktop/src-tauri", "crates/*"] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index e2d2f4ba..8378b63f 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -14,7 +14,36 @@ tauri-build = { version = "1.5.1", features = [] } ffmpeg-sidecar = "0.5.1" [dependencies] -tauri = { version = "1.6.1", features = [ "system-tray", "updater", "macos-private-api", "window-set-position", "fs-write-file", "fs-remove-file", "fs-read-file", "fs-rename-file", "fs-exists", "fs-remove-dir", "fs-read-dir", "fs-copy-file", "fs-create-dir", "window-set-ignore-cursor-events", "window-unminimize", "window-minimize", "window-close", "window-show", "window-start-dragging", "window-hide", "window-unmaximize", "window-maximize", "window-set-always-on-top", "shell-open", "devtools", "os-all", "http-all", "icon-png"] } +tauri = { version = "1.6.1", features = [ + "system-tray", + "updater", + "macos-private-api", + "window-set-position", + "fs-write-file", + "fs-remove-file", + "fs-read-file", + "fs-rename-file", + "fs-exists", + "fs-remove-dir", + "fs-read-dir", + "fs-copy-file", + "fs-create-dir", + "window-set-ignore-cursor-events", + "window-unminimize", + "window-minimize", + "window-close", + "window-show", + "window-start-dragging", + "window-hide", + "window-unmaximize", + "window-maximize", + "window-set-always-on-top", + "shell-open", + "devtools", + "os-all", + "http-all", + "icon-png", +] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tauri-plugin-context-menu = "0.7.0" @@ -35,7 +64,7 @@ dotenv_codegen = "0.15.0" byteorder = "1.4.3" bytemuck = "1.14.3" regex = "1" -capture = { path = "./src/capture" } +capture = { path = "../../../crates/capture" } image = "0.24.9" sentry = "0.32.2" fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } diff --git a/apps/desktop/src-tauri/src/capture/Cargo.lock b/crates/capture/Cargo.lock similarity index 100% rename from apps/desktop/src-tauri/src/capture/Cargo.lock rename to crates/capture/Cargo.lock diff --git a/apps/desktop/src-tauri/src/capture/Cargo.toml b/crates/capture/Cargo.toml similarity index 100% rename from apps/desktop/src-tauri/src/capture/Cargo.toml rename to crates/capture/Cargo.toml diff --git a/apps/desktop/src-tauri/src/capture/README.md b/crates/capture/README.md similarity index 100% rename from apps/desktop/src-tauri/src/capture/README.md rename to crates/capture/README.md diff --git a/apps/desktop/src-tauri/src/capture/build.rs b/crates/capture/build.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/build.rs rename to crates/capture/build.rs diff --git a/apps/desktop/src-tauri/src/capture/src/common/dxgi.rs b/crates/capture/src/common/dxgi.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/common/dxgi.rs rename to crates/capture/src/common/dxgi.rs diff --git a/apps/desktop/src-tauri/src/capture/src/common/mod.rs b/crates/capture/src/common/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/common/mod.rs rename to crates/capture/src/common/mod.rs diff --git a/apps/desktop/src-tauri/src/capture/src/common/quartz.rs b/crates/capture/src/common/quartz.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/common/quartz.rs rename to crates/capture/src/common/quartz.rs diff --git a/apps/desktop/src-tauri/src/capture/src/common/x11.rs b/crates/capture/src/common/x11.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/common/x11.rs rename to crates/capture/src/common/x11.rs diff --git a/apps/desktop/src-tauri/src/capture/src/dxgi/ffi.rs b/crates/capture/src/dxgi/ffi.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/dxgi/ffi.rs rename to crates/capture/src/dxgi/ffi.rs diff --git a/apps/desktop/src-tauri/src/capture/src/dxgi/mod.rs b/crates/capture/src/dxgi/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/dxgi/mod.rs rename to crates/capture/src/dxgi/mod.rs diff --git a/apps/desktop/src-tauri/src/capture/src/lib.rs b/crates/capture/src/lib.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/lib.rs rename to crates/capture/src/lib.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/capturer.rs b/crates/capture/src/quartz/capturer.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/capturer.rs rename to crates/capture/src/quartz/capturer.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/config.rs b/crates/capture/src/quartz/config.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/config.rs rename to crates/capture/src/quartz/config.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/display.rs b/crates/capture/src/quartz/display.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/display.rs rename to crates/capture/src/quartz/display.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/ffi.rs b/crates/capture/src/quartz/ffi.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/ffi.rs rename to crates/capture/src/quartz/ffi.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/frame.rs b/crates/capture/src/quartz/frame.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/frame.rs rename to crates/capture/src/quartz/frame.rs diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/mod.rs b/crates/capture/src/quartz/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/quartz/mod.rs rename to crates/capture/src/quartz/mod.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/capturer.rs b/crates/capture/src/x11/capturer.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/capturer.rs rename to crates/capture/src/x11/capturer.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/display.rs b/crates/capture/src/x11/display.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/display.rs rename to crates/capture/src/x11/display.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/ffi.rs b/crates/capture/src/x11/ffi.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/ffi.rs rename to crates/capture/src/x11/ffi.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/iter.rs b/crates/capture/src/x11/iter.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/iter.rs rename to crates/capture/src/x11/iter.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/mod.rs b/crates/capture/src/x11/mod.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/mod.rs rename to crates/capture/src/x11/mod.rs diff --git a/apps/desktop/src-tauri/src/capture/src/x11/server.rs b/crates/capture/src/x11/server.rs similarity index 100% rename from apps/desktop/src-tauri/src/capture/src/x11/server.rs rename to crates/capture/src/x11/server.rs From 5d2f2ae3d4d7e6c7c845cce7333e9cf9f43c86e4 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 01:46:13 +0800 Subject: [PATCH 08/28] use rust workspace with crates/ folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b70ab983..d18e2c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ target *.sw? .turbo pnpm-lock.yaml +target From e24b78355552cece0485bcebf95baa3e0f04b647 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 16:23:34 +0800 Subject: [PATCH 09/28] combine video and audio into one ffmpeg process --- apps/desktop/src-tauri/src/main.rs | 31 +- apps/desktop/src-tauri/src/media.rs | 714 ++++++++++---------- apps/desktop/src-tauri/src/recording.rs | 236 ++++--- apps/desktop/src-tauri/src/upload.rs | 141 ++-- apps/desktop/src-tauri/src/utils.rs | 5 - apps/web/app/api/upload/mux/create/route.ts | 51 +- 6 files changed, 626 insertions(+), 552 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index f199f5d6..2eed91cb 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,27 +1,29 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::collections::LinkedList; -use std::sync::{Arc}; -use std::path::PathBuf; use cpal::Devices; use regex::Regex; +use std::collections::LinkedList; +use std::path::PathBuf; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::vec; +use tauri::{ + CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTraySubmenu, Window, +}; +use tauri_plugin_oauth::start; +use tauri_plugin_positioner::{Position, WindowExt}; use tokio::sync::Mutex; -use std::sync::atomic::{AtomicBool}; -use std::{vec}; -use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTraySubmenu, Window}; -use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial}; use window_shadows::set_shadow; -use tauri_plugin_positioner::{WindowExt, Position}; -use tauri_plugin_oauth::start; +use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial}; +mod media; mod recording; mod upload; mod utils; -mod media; -use recording::{RecordingState, start_dual_recording, stop_all_recordings}; -use media::{enumerate_audio_devices}; -use utils::{has_screen_capture_access}; +use media::enumerate_audio_devices; +use recording::{start_dual_recording, stop_all_recordings, RecordingState}; +use utils::has_screen_capture_access; use ffmpeg_sidecar::{ command::ffmpeg_is_installed, @@ -238,8 +240,7 @@ fn main() { media_process: None, recording_options: None, shutdown_flag: Arc::new(AtomicBool::new(false)), - video_uploading_finished: Arc::new(AtomicBool::new(false)), - audio_uploading_finished: Arc::new(AtomicBool::new(false)), + uploading_finished: Arc::new(AtomicBool::new(false)), data_dir: Some(data_directory), max_screen_width: max_width as usize, max_screen_height: max_height as usize, diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index 436eaa0c..e7b8d02a 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -1,67 +1,72 @@ +use byteorder::{ByteOrder, LittleEndian}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::SampleFormat; -use std::process::{Stdio}; -use byteorder::{ByteOrder, LittleEndian}; -use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; -use std::io::{ErrorKind::WouldBlock, Error}; -use std::time::{Instant, Duration}; -use std::path::Path; -use image::{ImageBuffer, Rgba, ImageFormat}; use image::codecs::jpeg::JpegEncoder; - -use tokio::io::{AsyncWriteExt}; -use tokio::process::{Command, Child, ChildStdin}; +use image::{ImageBuffer, ImageFormat, Rgba}; +use std::io::{Error, ErrorKind::WouldBlock}; +use std::path::Path; +use std::process::Stdio; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::time::{Duration, Instant}; +use tokio::fs::File; + +use tokio::io::AsyncWriteExt; +use tokio::process::{Child, ChildStdin, Command}; use tokio::sync::{mpsc, Mutex}; use tokio::try_join; use crate::recording::RecordingOptions; -use crate::utils::{ffmpeg_path_as_str}; -use crate::upload::upload_file; +use crate::upload::{self, upload_file}; +use crate::utils::{create_named_pipe, ffmpeg_path_as_str}; use capture::{Capturer, Display}; const FRAME_RATE: u64 = 30; pub struct MediaRecorder { pub options: Option, - ffmpeg_audio_process: Option, - ffmpeg_video_process: Option, - ffmpeg_audio_stdin: Option>>>, - ffmpeg_video_stdin: Option>>>, + ffmpeg_process: Option, + ffmpeg_stdin: Option>>>, device_name: Option, - stream: Option, + audio_stream: Option, audio_channel_sender: Option>>, audio_channel_receiver: Option>>, video_channel_sender: Option>>, video_channel_receiver: Option>>, should_stop: Arc, start_time: Option, - audio_file_path: Option, - video_file_path: Option, + file_path: Option, } impl MediaRecorder { - pub fn new() -> Self { MediaRecorder { options: None, - ffmpeg_audio_process: None, - ffmpeg_video_process: None, - ffmpeg_audio_stdin: None, - ffmpeg_video_stdin: None, + ffmpeg_process: None, + ffmpeg_stdin: None, device_name: None, - stream: None, + audio_stream: None, audio_channel_sender: None, audio_channel_receiver: None, video_channel_sender: None, video_channel_receiver: None, should_stop: Arc::new(AtomicBool::new(false)), start_time: None, - audio_file_path: None, - video_file_path: None, + file_path: None, } } - pub async fn start_media_recording(&mut self, options: RecordingOptions, audio_file_path: &str, video_file_path: &str, screenshot_file_path: &str, custom_device: Option<&str>, max_screen_width: usize, max_screen_height: usize) -> Result<(), String> { + pub async fn start_media_recording( + &mut self, + options: RecordingOptions, + chunks_file_path: &str, + screenshot_file_path: &str, + custom_device: Option<&str>, + max_screen_width: usize, + max_screen_height: usize, + ) -> Result<(), String> { self.options = Some(options.clone()); println!("Custom device: {:?}", custom_device); @@ -93,8 +98,7 @@ impl MediaRecorder { self.audio_channel_receiver = Some(audio_rx); self.video_channel_sender = Some(video_tx); self.video_channel_receiver = Some(video_rx); - self.ffmpeg_audio_stdin = Some(Arc::new(Mutex::new(None))); - self.ffmpeg_video_stdin = Some(Arc::new(Mutex::new(None))); + self.ffmpeg_stdin = Some(Arc::new(Mutex::new(None))); let audio_channel_sender = self.audio_channel_sender.clone(); let video_channel_sender = self.video_channel_sender.clone(); @@ -115,20 +119,41 @@ impl MediaRecorder { let device = if let Some(custom_device_name) = custom_device { input_devices - .find(|d| d.name().map(|name| name == custom_device_name).unwrap_or(false)) - .unwrap_or_else(|| host.default_input_device().expect("No default input device available")) + .find(|d| { + d.name() + .map(|name| name == custom_device_name) + .unwrap_or(false) + }) + .unwrap_or_else(|| { + host.default_input_device() + .expect("No default input device available") + }) } else { - host.default_input_device().expect("No default input device available") + host.default_input_device() + .expect("No default input device available") }; - println!("Using audio device: {}", device.name().expect("Failed to get device name")); + println!( + "Using audio device: {}", + device.name().expect("Failed to get device name") + ); - let config = device.supported_input_configs() + let config = device + .supported_input_configs() .expect("Failed to get supported input configs") - .find(|c| c.sample_format() == SampleFormat::F32 || c.sample_format() == SampleFormat::I16 || c.sample_format() == SampleFormat::I8 || c.sample_format() == SampleFormat::I32) - .unwrap_or_else(|| - device.supported_input_configs().expect("Failed to get supported input configs").next().expect("No supported input config") - ) + .find(|c| { + c.sample_format() == SampleFormat::F32 + || c.sample_format() == SampleFormat::I16 + || c.sample_format() == SampleFormat::I8 + || c.sample_format() == SampleFormat::I32 + }) + .unwrap_or_else(|| { + device + .supported_input_configs() + .expect("Failed to get supported input configs") + .next() + .expect("No supported input config") + }) .with_max_sample_rate(); let sample_rate = config.sample_rate().0; @@ -149,139 +174,139 @@ impl MediaRecorder { println!("FFmpeg binary path: {}", ffmpeg_binary_path_str); - let audio_file_path_owned = audio_file_path.to_owned(); - let video_file_path_owned = video_file_path.to_owned(); let sample_rate_str = sample_rate.to_string(); let channels_str = channels.to_string(); - let ffmpeg_audio_stdin = self.ffmpeg_audio_stdin.clone(); - let ffmpeg_video_stdin = self.ffmpeg_video_stdin.clone(); - let err_fn = move |err| { eprintln!("an error occurred on stream: {}", err); }; - if custom_device != Some("None") { + let needs_audio = custom_device != Some("None"); + + if needs_audio { println!("Building input stream..."); - let stream_result: Result = match config.sample_format() { - SampleFormat::I8 => device.build_input_stream( - &config.into(), - { - let audio_start_time = Arc::clone(&audio_start_time); - move |data: &[i8], _: &_| { - let mut first_frame_time_guard = audio_start_time.try_lock(); - - let bytes = data.iter().map(|&sample| sample as u8).collect::>(); - if let Some(sender) = &audio_channel_sender { - if sender.try_send(bytes).is_err() { - eprintln!("Channel send error. Dropping data."); + let stream_result: Result = + match config.sample_format() { + SampleFormat::I8 => device.build_input_stream( + &config.into(), + { + let audio_start_time = Arc::clone(&audio_start_time); + move |data: &[i8], _: &_| { + let mut first_frame_time_guard = audio_start_time.try_lock(); + + let bytes = + data.iter().map(|&sample| sample as u8).collect::>(); + if let Some(sender) = &audio_channel_sender { + if sender.try_send(bytes).is_err() { + eprintln!("Channel send error. Dropping data."); + } + } + + if let Ok(ref mut start_time_option) = first_frame_time_guard { + if start_time_option.is_none() { + **start_time_option = Some(Instant::now()); + + println!("Audio start time captured"); + } + } } - } - - if let Ok(ref mut start_time_option) = first_frame_time_guard { - if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); - - println!("Audio start time captured"); - } - } - } - }, - err_fn, - None, - ), - SampleFormat::I16 => device.build_input_stream( - &config.into(), - { - let audio_start_time = Arc::clone(&audio_start_time); - move |data: &[i16], _: &_| { - let mut first_frame_time_guard = audio_start_time.try_lock(); - - let mut bytes = vec![0; data.len() * 2]; - LittleEndian::write_i16_into(data, &mut bytes); - if let Some(sender) = &audio_channel_sender { - if sender.try_send(bytes).is_err() { - eprintln!("Channel send error. Dropping data."); - } - } - - if let Ok(ref mut start_time_option) = first_frame_time_guard { - if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); - - println!("Audio start time captured"); - } - } - } - }, - err_fn, - None, - ), - SampleFormat::I32 => device.build_input_stream( - &config.into(), - { - let audio_start_time = Arc::clone(&audio_start_time); - move |data: &[i32], _: &_| { - let mut first_frame_time_guard = audio_start_time.try_lock(); - - let mut bytes = vec![0; data.len() * 2]; - LittleEndian::write_i32_into(data, &mut bytes); - if let Some(sender) = &audio_channel_sender { - if sender.try_send(bytes).is_err() { - eprintln!("Channel send error. Dropping data."); - } - } - - if let Ok(ref mut start_time_option) = first_frame_time_guard { - if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); - - println!("Audio start time captured"); - } - } - } - }, - err_fn, - None, - ), - SampleFormat::F32 => device.build_input_stream( - &config.into(), - { - let audio_start_time = Arc::clone(&audio_start_time); - move |data: &[f32], _: &_| { - let mut first_frame_time_guard = audio_start_time.try_lock(); - - let mut bytes = vec![0; data.len() * 4]; - LittleEndian::write_f32_into(data, &mut bytes); - if let Some(sender) = &audio_channel_sender { - if sender.try_send(bytes).is_err() { - eprintln!("Channel send error. Dropping data."); - } - } - - if let Ok(ref mut start_time_option) = first_frame_time_guard { - if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); - - println!("Audio start time captured"); - } - } - } - }, - err_fn, - None, - ), - _sample_format => Err(cpal::BuildStreamError::DeviceNotAvailable), - }; + }, + err_fn, + None, + ), + SampleFormat::I16 => device.build_input_stream( + &config.into(), + { + let audio_start_time = Arc::clone(&audio_start_time); + move |data: &[i16], _: &_| { + let mut first_frame_time_guard = audio_start_time.try_lock(); + + let mut bytes = vec![0; data.len() * 2]; + LittleEndian::write_i16_into(data, &mut bytes); + if let Some(sender) = &audio_channel_sender { + if sender.try_send(bytes).is_err() { + eprintln!("Channel send error. Dropping data."); + } + } + + if let Ok(ref mut start_time_option) = first_frame_time_guard { + if start_time_option.is_none() { + **start_time_option = Some(Instant::now()); + + println!("Audio start time captured"); + } + } + } + }, + err_fn, + None, + ), + SampleFormat::I32 => device.build_input_stream( + &config.into(), + { + let audio_start_time = Arc::clone(&audio_start_time); + move |data: &[i32], _: &_| { + let mut first_frame_time_guard = audio_start_time.try_lock(); + + let mut bytes = vec![0; data.len() * 2]; + LittleEndian::write_i32_into(data, &mut bytes); + if let Some(sender) = &audio_channel_sender { + if sender.try_send(bytes).is_err() { + eprintln!("Channel send error. Dropping data."); + } + } + + if let Ok(ref mut start_time_option) = first_frame_time_guard { + if start_time_option.is_none() { + **start_time_option = Some(Instant::now()); + + println!("Audio start time captured"); + } + } + } + }, + err_fn, + None, + ), + SampleFormat::F32 => device.build_input_stream( + &config.into(), + { + let audio_start_time = Arc::clone(&audio_start_time); + move |data: &[f32], _: &_| { + let mut first_frame_time_guard = audio_start_time.try_lock(); + + let mut bytes = vec![0; data.len() * 4]; + LittleEndian::write_f32_into(data, &mut bytes); + if let Some(sender) = &audio_channel_sender { + if sender.try_send(bytes).is_err() { + eprintln!("Channel send error. Dropping data."); + } + } + + if let Ok(ref mut start_time_option) = first_frame_time_guard { + if start_time_option.is_none() { + **start_time_option = Some(Instant::now()); + + println!("Audio start time captured"); + } + } + } + }, + err_fn, + None, + ), + _sample_format => Err(cpal::BuildStreamError::DeviceNotAvailable), + }; let stream = stream_result.map_err(|_| "Failed to build input stream")?; - self.stream = Some(stream); + self.audio_stream = Some(stream); self.trigger_play()?; } let video_start_time_clone = Arc::clone(&video_start_time); - let screenshot_file_path_owned = format!("{}/screen-capture.jpg", screenshot_file_path); + let screenshot_file_path_owned = format!("{screenshot_file_path}/screen-capture.jpg"); + let capture_frame_at = Duration::from_secs(3); std::thread::spawn(move || { @@ -292,7 +317,12 @@ impl MediaRecorder { _ => false, }; - let mut capturer = Capturer::new(Display::primary().expect("Failed to find primary display"), w.try_into().unwrap(), h.try_into().unwrap()).expect("Failed to start capture"); + let mut capturer = Capturer::new( + Display::primary().expect("Failed to find primary display"), + w.try_into().unwrap(), + h.try_into().unwrap(), + ) + .expect("Failed to start capture"); let fps = FRAME_RATE; let spf = Duration::from_nanos(1_000_000_000 / fps); @@ -309,10 +339,12 @@ impl MediaRecorder { if now >= time_next { match capturer.frame() { Ok(frame) => { - let mut frame_data = Vec::with_capacity(capture_size.try_into().unwrap()); + let mut frame_data = + Vec::with_capacity(capture_size.try_into().unwrap()); for row in 0..adjusted_height { - let padded_stride = frame.stride_override().unwrap_or(calculated_stride); + let padded_stride = + frame.stride_override().unwrap_or(calculated_stride); assert!(padded_stride >= calculated_stride, "Image stride with padding should not be smaller than calculated bytes per row"); // Each row should skip the padding of the previous row let start = row * padded_stride; @@ -323,7 +355,8 @@ impl MediaRecorder { if now - start_time >= capture_frame_at && !screenshot_captured { screenshot_captured = true; - let screenshot_file_path_owned_cloned = screenshot_file_path_owned.clone(); + let screenshot_file_path_owned_cloned = + screenshot_file_path_owned.clone(); let mut frame_data_clone = frame_data.clone(); std::thread::spawn(move || { @@ -332,31 +365,46 @@ impl MediaRecorder { } let path = Path::new(&screenshot_file_path_owned_cloned); - let image: ImageBuffer, Vec> = ImageBuffer::from_raw( - adjusted_width.try_into().unwrap(), - adjusted_height.try_into().unwrap(), - frame_data_clone - ).expect("Failed to create image buffer"); - - let mut output_file = std::fs::File::create(&path).expect("Failed to create output file"); - let mut encoder = JpegEncoder::new_with_quality(&mut output_file, 20); + let image: ImageBuffer, Vec> = + ImageBuffer::from_raw( + adjusted_width.try_into().unwrap(), + adjusted_height.try_into().unwrap(), + frame_data_clone, + ) + .expect("Failed to create image buffer"); + + let mut output_file = std::fs::File::create(&path) + .expect("Failed to create output file"); + let mut encoder = + JpegEncoder::new_with_quality(&mut output_file, 20); if let Err(e) = encoder.encode_image(&image) { eprintln!("Failed to save screenshot: {}", e); } else { if !is_local_mode { let rt = tokio::runtime::Runtime::new().unwrap(); - let screenshot_file_path_owned_cloned_copy = screenshot_file_path_owned_cloned.clone(); + let screenshot_file_path_owned_cloned_copy = + screenshot_file_path_owned_cloned.clone(); rt.block_on(async { - let upload_task = tokio::spawn(upload_file(Some(options_clone), screenshot_file_path_owned_cloned_copy.clone(), "screenshot".to_string())); + let upload_task = tokio::spawn(upload_file( + Some(options_clone), + screenshot_file_path_owned_cloned_copy.clone(), + upload::FileType::Screenshot, + )); match upload_task.await { - Ok(result) => { - match result { - Ok(_) => println!("Screenshot captured and saved to {:?}", path), - Err(e) => eprintln!("Failed to upload file: {}", e), - } + Ok(result) => match result { + Ok(_) => println!( + "Screenshot captured and saved to {:?}", + path + ), + Err(e) => eprintln!( + "Failed to upload file: {}", + e + ), }, - Err(e) => eprintln!("Failed to join task: {}", e), + Err(e) => { + eprintln!("Failed to join task: {}", e) + } } }); } @@ -382,15 +430,15 @@ impl MediaRecorder { } frame_count += 1; - }, + } Err(error) if error.kind() == WouldBlock => { std::thread::sleep(Duration::from_millis(1)); continue; - }, + } Err(error) => { eprintln!("Capture error: {}", error); break; - }, + } } time_next += spf; @@ -409,10 +457,8 @@ impl MediaRecorder { }); println!("Starting audio recording and processing..."); - let audio_output_chunk_pattern = format!("{}/audio_recording_%03d.aac", audio_file_path_owned); - let audio_segment_list_filename = format!("{}/segment_list.txt", audio_file_path_owned); - let video_output_chunk_pattern = format!("{}/video_recording_%03d.ts", video_file_path_owned); - let video_segment_list_filename = format!("{}/segment_list.txt", video_file_path_owned); + let video_output_chunk_pattern = format!("{chunks_file_path}/video_recording_%03d.ts"); + let video_segment_list_filename = format!("{chunks_file_path}/segment_list.txt"); let mut audio_filters = Vec::new(); @@ -422,123 +468,140 @@ impl MediaRecorder { audio_filters.push("loudnorm"); - let mut ffmpeg_audio_command: Vec = vec![ - "-f", sample_format, - "-ar", &sample_rate_str, - "-ac", &channels_str, - "-thread_queue_size", "4096", - "-i", "pipe:0", - "-af", "aresample=async=1:min_hard_comp=0.100000:first_pts=0", - "-c:a", "aac", - "-b:a", "128k", - "-async", "1", - "-f", "segment", - "-segment_time", "3", - "-segment_time_delta", "0.01", - "-segment_list", &audio_segment_list_filename, - "-reset_timestamps", "1", - &audio_output_chunk_pattern, - ].into_iter().map(|s| s.to_string()).collect(); - - let mut ffmpeg_video_command: Vec = vec![ - "-f", "rawvideo", - "-pix_fmt", "bgra", - "-s", &format!("{}x{}", adjusted_width, adjusted_height), - "-r", "30", - "-thread_queue_size", "4096", - "-i", "pipe:0", - "-vf", "fps=30,scale=in_range=full:out_range=limited", - "-c:v", "libx264", - "-preset", "ultrafast", - "-pix_fmt", "yuv420p", - "-tune", "zerolatency", - "-vsync", "1", - "-force_key_frames", "expr:gte(t,n_forced*3)", - "-f", "segment", - "-segment_time", "3", - "-segment_time_delta", "0.01", - "-segment_list", &video_segment_list_filename, - "-segment_format", "ts", - "-movflags", "frag_keyframe+empty_moov", - "-reset_timestamps", "1", - &video_output_chunk_pattern, - ].into_iter().map(|s| s.to_string()).collect(); - - if custom_device != Some("None") { + std::fs::create_dir_all(format!("{chunks_file_path}/pipes")).map_err(|e| e.to_string())?; + + let video_pipe_path = format!("{chunks_file_path}/pipes/video.pipe"); + + std::fs::remove_file(&video_pipe_path).ok(); + create_named_pipe(&video_pipe_path).map_err(|e| e.to_string())?; + + let audio_pipe_path = format!("{chunks_file_path}/pipes/audio.pipe"); + + std::fs::remove_file(&audio_pipe_path).ok(); + create_named_pipe(&audio_pipe_path).map_err(|e| e.to_string())?; + + let size = format!("{}x{}", adjusted_width, adjusted_height); + let mut ffmpeg_command = vec![ + ["-f", "rawvideo"], + ["-pix_fmt", "bgra"], + ["-s", &size], + ["-r", "30"], + ["-thread_queue_size", "4096"], + ["-i", &video_pipe_path], + ]; + + if needs_audio { + ffmpeg_command.extend([ + // in + ["-f", sample_format], + ["-ar", &sample_rate_str], + ["-ac", &channels_str], + ["-thread_queue_size", "4096"], + ["-i", &audio_pipe_path], + // out + [ + "-af", + "aresample=async=1:min_hard_comp=0.100000:first_pts=0", + ], + ["-c:a", "aac"], + ["-b:a", "128k"], + ["-async", "1"], + ]); + }; + + ffmpeg_command.extend([ + ["-vf", "fps=30,scale=in_range=full:out_range=limited"], + ["-c:v", "libx264"], + ["-preset", "ultrafast"], + ["-pix_fmt", "yuv420p"], + ["-tune", "zerolatency"], + ["-vsync", "1"], + ["-force_key_frames", "expr:gte(t,n_forced*3)"], + ["-f", "segment"], + ["-segment_time", "3"], + ["-segment_time_delta", "0.01"], + ["-segment_list", &video_segment_list_filename], + ["-segment_format", "ts"], + ["-movflags", "frag_keyframe+empty_moov"], + ["-reset_timestamps", "1"], + ]); + + let mut ffmpeg_command = ffmpeg_command + .into_iter() + .flatten() + .map(|s| s.to_string()) + .chain([video_output_chunk_pattern]) + .collect(); + + if needs_audio { println!("Adjusting FFmpeg commands based on start times..."); adjust_ffmpeg_commands_based_on_start_times( Arc::clone(&audio_start_time), Arc::clone(&video_start_time), - &mut ffmpeg_audio_command, - &mut ffmpeg_video_command, - ).await; - } - - println!("Starting FFmpeg audio and video processes..."); - - let mut audio_stdin: Option = None; - let mut audio_child: Option = None; - - if custom_device != Some("None") { - let (child, stdin) = self.start_audio_ffmpeg_processes(&ffmpeg_binary_path_str, &ffmpeg_audio_command).await.map_err(|e| e.to_string())?; - audio_child = Some(child); - audio_stdin = Some(stdin); - println!("Audio process started"); + &mut ffmpeg_command, + ) + .await; } - let (video_child, video_stdin) = self.start_video_ffmpeg_processes(&ffmpeg_binary_path_str, &ffmpeg_video_command).await.map_err(|e| e.to_string())?; - println!("Video process started"); + println!("Starting FFmpeg process..."); - if let Some(ffmpeg_audio_stdin) = &self.ffmpeg_audio_stdin { - let mut audio_stdin_lock = ffmpeg_audio_stdin.lock().await; - *audio_stdin_lock = audio_stdin; - drop(audio_stdin_lock); - println!("Audio stdin set"); - } + let (ffmpeg_child, ffmpeg_stdin) = self + .start_ffmpeg_process(&ffmpeg_binary_path_str, &ffmpeg_command) + .await + .map_err(|e| e.to_string())?; + println!("Ffmpeg process started"); - if let Some(ffmpeg_video_stdin) = &self.ffmpeg_video_stdin { + if let Some(ffmpeg_video_stdin) = &self.ffmpeg_stdin { let mut video_stdin_lock = ffmpeg_video_stdin.lock().await; - *video_stdin_lock = Some(video_stdin); + *video_stdin_lock = Some(ffmpeg_stdin); drop(video_stdin_lock); - println!("Video stdin set"); + println!("Ffmpeg stdin set"); } - if custom_device != Some("None") { + if needs_audio { println!("Starting audio channel senders..."); + tokio::spawn(async move { - while let Some(bytes) = &audio_channel_receiver.lock().await.as_mut().unwrap().recv().await { - if let Some(audio_stdin_arc) = &ffmpeg_audio_stdin{ - let mut audio_stdin_guard = audio_stdin_arc.lock().await; - if let Some(ref mut stdin) = *audio_stdin_guard { - stdin.write_all(&bytes).await.expect("Failed to write audio data to FFmpeg stdin"); - } - drop(audio_stdin_guard); - } + let mut audio_pipe = File::create(audio_pipe_path).await.unwrap(); + + while let Some(bytes) = &audio_channel_receiver + .lock() + .await + .as_mut() + .unwrap() + .recv() + .await + { + audio_pipe + .write_all(&bytes) + .await + .expect("Failed to write audio data to FFmpeg stdin"); } }); } println!("Starting video channel senders..."); tokio::spawn(async move { - while let Some(bytes) = &video_channel_receiver.lock().await.as_mut().unwrap().recv().await { - if let Some(video_stdin_arc) = &ffmpeg_video_stdin { - let mut video_stdin_guard = video_stdin_arc.lock().await; - if let Some(ref mut stdin) = *video_stdin_guard { - stdin.write_all(&bytes).await.expect("Failed to write video data to FFmpeg stdin"); - } - drop(video_stdin_guard); - } + let mut video_pipe = File::create(video_pipe_path).await.unwrap(); + + while let Some(bytes) = &video_channel_receiver + .lock() + .await + .as_mut() + .unwrap() + .recv() + .await + { + video_pipe + .write_all(&bytes) + .await + .expect("Failed to write video data to FFmpeg stdin"); } }); - if custom_device != Some("None") { - self.ffmpeg_audio_process = audio_child; - } - self.start_time = Some(Instant::now()); - self.audio_file_path = Some(audio_file_path_owned); - self.video_file_path = Some(video_file_path_owned); - self.ffmpeg_video_process = Some(video_child); + self.file_path = Some(chunks_file_path.to_string()); + self.ffmpeg_process = Some(ffmpeg_child); self.device_name = Some(device.name().expect("Failed to get device name")); println!("End of the start_audio_recording function"); @@ -546,8 +609,8 @@ impl MediaRecorder { Ok(()) } - pub fn trigger_play (&mut self) -> Result<(), &'static str> { - if let Some(ref mut stream) = self.stream { + pub fn trigger_play(&mut self) -> Result<(), &'static str> { + if let Some(ref mut stream) = self.audio_stream { stream.play().map_err(|_| "Failed to play stream")?; println!("Audio recording playing."); } else { @@ -562,19 +625,16 @@ impl MediaRecorder { let segment_duration = Duration::from_secs(3); let recording_duration = start_time.elapsed(); let expected_segments = recording_duration.as_secs() / segment_duration.as_secs(); - let audio_file_path = self.audio_file_path.as_ref().ok_or("Audio file path not set")?; - let video_file_path = self.video_file_path.as_ref().ok_or("Video file path not set")?; - let audio_segment_list_filename = format!("{}/segment_list.txt", audio_file_path); + let video_file_path = self.file_path.as_ref().ok_or("Video file path not set")?; let video_segment_list_filename = format!("{}/segment_list.txt", video_file_path); loop { - let audio_segments = std::fs::read_to_string(&audio_segment_list_filename).unwrap_or_default(); - let video_segments = std::fs::read_to_string(&video_segment_list_filename).unwrap_or_default(); + let video_segments = + std::fs::read_to_string(&video_segment_list_filename).unwrap_or_default(); - let audio_segment_count = audio_segments.lines().count(); let video_segment_count = video_segments.lines().count(); - if audio_segment_count >= expected_segments as usize && video_segment_count >= expected_segments as usize { + if video_segment_count >= expected_segments as usize { println!("All segments generated"); break; } @@ -583,17 +643,7 @@ impl MediaRecorder { } } - if let Some(ref ffmpeg_audio_stdin) = self.ffmpeg_audio_stdin { - let mut audio_stdin_guard = ffmpeg_audio_stdin.lock().await; - if let Some(mut audio_stdin) = audio_stdin_guard.take() { - if let Err(e) = audio_stdin.write_all(b"q\n").await { - eprintln!("Failed to send 'q' to audio FFmpeg process: {}", e); - } - let _ = audio_stdin.shutdown().await.map_err(|e| e.to_string()); - } - } - - if let Some(ref ffmpeg_video_stdin) = self.ffmpeg_video_stdin { + if let Some(ref ffmpeg_video_stdin) = self.ffmpeg_stdin { let mut video_stdin_guard = ffmpeg_video_stdin.lock().await; if let Some(mut video_stdin) = video_stdin_guard.take() { if let Err(e) = video_stdin.write_all(b"q\n").await { @@ -613,18 +663,14 @@ impl MediaRecorder { drop(sender); } - if let Some(ref mut stream) = self.stream { + if let Some(ref mut stream) = self.audio_stream { stream.pause().map_err(|_| "Failed to pause stream")?; println!("Audio recording paused."); } else { return Err("Original recording was not started".to_string()); } - if let Some(process) = &mut self.ffmpeg_audio_process { - let _ = process.kill().await.map_err(|e| e.to_string()); - } - - if let Some(process) = &mut self.ffmpeg_video_process { + if let Some(process) = &mut self.ffmpeg_process { let _ = process.kill().await.map_err(|e| e.to_string()); } @@ -632,33 +678,17 @@ impl MediaRecorder { Ok(()) } - async fn start_audio_ffmpeg_processes( - &self, - ffmpeg_binary_path: &str, - audio_ffmpeg_command: &[String], - ) -> Result<(Child, ChildStdin), Error> { - let mut audio_process = start_recording_process(ffmpeg_binary_path, audio_ffmpeg_command).await.map_err(|e| { - eprintln!("Failed to start audio recording process: {}", e); - std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) - })?; - - let audio_stdin = audio_process.stdin.take().ok_or_else(|| { - eprintln!("Failed to take audio stdin"); - std::io::Error::new(std::io::ErrorKind::Other, "Failed to take audio stdin") - })?; - - Ok((audio_process, audio_stdin)) - } - - async fn start_video_ffmpeg_processes( + async fn start_ffmpeg_process( &self, ffmpeg_binary_path: &str, video_ffmpeg_command: &[String], ) -> Result<(Child, ChildStdin), Error> { - let mut video_process = start_recording_process(ffmpeg_binary_path, video_ffmpeg_command).await.map_err(|e| { - eprintln!("Failed to start video recording process: {}", e); - std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) - })?; + let mut video_process = start_recording_process(ffmpeg_binary_path, video_ffmpeg_command) + .await + .map_err(|e| { + eprintln!("Failed to start video recording process: {}", e); + std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) + })?; let video_stdin = video_process.stdin.take().ok_or_else(|| { eprintln!("Failed to take video stdin"); @@ -667,15 +697,18 @@ impl MediaRecorder { Ok((video_process, video_stdin)) } - } #[tauri::command] #[specta::specta] pub fn enumerate_audio_devices() -> Vec { let host = cpal::default_host(); - let default_device = host.default_input_device().expect("No default input device available"); - let default_device_name = default_device.name().expect("Failed to get default device name"); + let default_device = host + .default_input_device() + .expect("No default input device available"); + let default_device_name = default_device + .name() + .expect("Failed to get default device name"); let devices = host.devices().expect("Failed to get devices"); let mut input_device_names: Vec = devices @@ -695,7 +728,7 @@ pub fn enumerate_audio_devices() -> Vec { input_device_names } -use tokio::io::{BufReader, AsyncBufReadExt}; +use tokio::io::{AsyncBufReadExt, BufReader}; async fn start_recording_process( ffmpeg_binary_path_str: &str, @@ -708,7 +741,7 @@ async fn start_recording_process( .spawn()?; if let Some(process_stderr) = process.stderr.take() { - tokio::spawn(async move { + tokio::spawn(async move { let mut process_reader = BufReader::new(process_stderr).lines(); while let Ok(Some(line)) = process_reader.next_line().await { eprintln!("FFmpeg process STDERR: {}", line); @@ -741,7 +774,7 @@ async fn wait_for_start_times( async fn adjust_ffmpeg_commands_based_on_start_times( audio_start_time: Arc>>, video_start_time: Arc>>, - ffmpeg_audio_command: &mut Vec, + // ffmpeg_audio_command: &mut Vec, ffmpeg_video_command: &mut Vec, ) { let (audio_start, video_start) = wait_for_start_times(audio_start_time, video_start_time).await; @@ -756,22 +789,23 @@ async fn adjust_ffmpeg_commands_based_on_start_times( println!("Video start: {:?}", video_start); // Convert the duration difference to a float representing seconds - let offset_seconds = duration_difference.as_secs() as f64 - + duration_difference.subsec_nanos() as f64 * 1e-9; + let offset_seconds = + duration_difference.as_secs() as f64 + duration_difference.subsec_nanos() as f64 * 1e-9; // Depending on which started first, adjust the relevant FFmpeg command if audio_start > video_start { // Offset the video start time - ffmpeg_video_command.splice(0..0, vec![ - "-itsoffset".to_string(), format!("{:.3}", offset_seconds) - ]); + ffmpeg_video_command.splice( + 0..0, + vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], + ); println!("Applying -itsoffset {:.3} to video", offset_seconds); } else if video_start > audio_start { // Offset the audio start time - ffmpeg_audio_command.splice(0..0, vec![ - "-itsoffset".to_string(), format!("{:.3}", offset_seconds) - ]); + // ffmpeg_audio_command.splice( + // 0..0, + // vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], + // ); println!("Applying -itsoffset {:.3} to audio", offset_seconds); } - } diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index d7bc2492..67f3b401 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -1,28 +1,30 @@ -use std::path::{Path, PathBuf}; +use futures::future::join_all; +use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use std::io::{self, BufReader, BufRead, ErrorKind}; use std::fs::File; -use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; -use tokio::sync:: {Mutex}; -use tokio::task::JoinHandle; -use tokio::time::{Duration}; -use serde::{Serialize, Deserialize}; +use std::io::{self, BufRead, BufReader, ErrorKind}; +use std::path::{Path, PathBuf}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use tauri::State; -use futures::future::join_all; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +use tokio::time::Duration; -use crate::upload::{upload_file}; +use crate::upload::{self, upload_file}; use crate::media::MediaRecorder; pub struct RecordingState { - pub media_process: Option, - pub recording_options: Option, - pub shutdown_flag: Arc, - pub video_uploading_finished: Arc, - pub audio_uploading_finished: Arc, - pub data_dir: Option, - pub max_screen_width: usize, - pub max_screen_height: usize, + pub media_process: Option, + pub recording_options: Option, + pub shutdown_flag: Arc, + pub uploading_finished: Arc, + pub data_dir: Option, + pub max_screen_width: usize, + pub max_screen_height: usize, } unsafe impl Send for RecordingState {} @@ -32,85 +34,100 @@ unsafe impl Sync for MediaRecorder {} #[derive(Debug, Serialize, Deserialize, Clone, specta::Type)] pub struct RecordingOptions { - pub user_id: String, - pub video_id: String, - pub screen_index: String, - pub video_index: String, - pub audio_name: String, - pub aws_region: String, - pub aws_bucket: String, + pub user_id: String, + pub video_id: String, + pub screen_index: String, + pub video_index: String, + pub audio_name: String, + pub aws_region: String, + pub aws_bucket: String, } #[tauri::command] #[specta::specta] pub async fn start_dual_recording( - state: State<'_, Arc>>, - options: RecordingOptions, + state: State<'_, Arc>>, + options: RecordingOptions, ) -> Result<(), String> { - println!("Starting screen recording..."); - let mut state_guard = state.lock().await; - - let shutdown_flag = Arc::new(AtomicBool::new(false)); + println!("Starting screen recording..."); + let mut state_guard = state.lock().await; - let data_dir = state_guard.data_dir.as_ref() - .ok_or("Data directory is not set in the recording state".to_string())?.clone(); + let shutdown_flag = Arc::new(AtomicBool::new(false)); - println!("data_dir: {:?}", data_dir); + let data_dir = state_guard + .data_dir + .as_ref() + .ok_or("Data directory is not set in the recording state".to_string())? + .clone(); - let audio_chunks_dir = data_dir.join("chunks/audio"); - let video_chunks_dir = data_dir.join("chunks/video"); - let screenshot_dir = data_dir.join("screenshots"); + println!("data_dir: {:?}", data_dir); - clean_and_create_dir(&audio_chunks_dir)?; - clean_and_create_dir(&video_chunks_dir)?; - clean_and_create_dir(&screenshot_dir)?; + let screenshot_dir = data_dir.join("screenshots"); + let chunks_dir = data_dir.join("chunks"); - let audio_name = if options.audio_name.is_empty() { - None - } else { - Some(options.audio_name.clone()) - }; + clean_and_create_dir(&chunks_dir)?; + clean_and_create_dir(&screenshot_dir)?; - let media_recording_preparation = prepare_media_recording(&options, &audio_chunks_dir, &video_chunks_dir, &screenshot_dir, audio_name, state_guard.max_screen_width, state_guard.max_screen_height); - let media_recording_result = media_recording_preparation.await.map_err(|e| e.to_string())?; + let audio_name = if options.audio_name.is_empty() { + None + } else { + Some(options.audio_name.clone()) + }; - state_guard.media_process = Some(media_recording_result); - state_guard.recording_options = Some(options.clone()); - state_guard.shutdown_flag = shutdown_flag.clone(); - state_guard.video_uploading_finished = Arc::new(AtomicBool::new(false)); - state_guard.audio_uploading_finished = Arc::new(AtomicBool::new(false)); + let media_recording_preparation = prepare_media_recording( + &options, + &chunks_dir, + &screenshot_dir, + audio_name, + state_guard.max_screen_width, + state_guard.max_screen_height, + ); + let media_recording_result = media_recording_preparation + .await + .map_err(|e| e.to_string())?; + + state_guard.media_process = Some(media_recording_result); + state_guard.recording_options = Some(options.clone()); + state_guard.shutdown_flag = shutdown_flag.clone(); + state_guard.uploading_finished = Arc::new(AtomicBool::new(false)); - let is_local_mode = match dotenv_codegen::dotenv!("NEXT_PUBLIC_LOCAL_MODE") { - "true" => true, - _ => false, - }; + let is_local_mode = match dotenv_codegen::dotenv!("NEXT_PUBLIC_LOCAL_MODE") { + "true" => true, + _ => false, + }; - if !is_local_mode { - let screen_upload = start_upload_loop(video_chunks_dir.clone(), options.clone(), "video".to_string(), shutdown_flag.clone(), state_guard.video_uploading_finished.clone()); - let audio_upload = start_upload_loop(audio_chunks_dir, options.clone(), "audio".to_string(), shutdown_flag.clone(), state_guard.audio_uploading_finished.clone()); + if !is_local_mode { + let upload = start_upload_loop( + chunks_dir.clone(), + options.clone(), + shutdown_flag.clone(), + state_guard.uploading_finished.clone(), + ); - drop(state_guard); + drop(state_guard); - println!("Starting upload loops..."); + println!("Starting upload loops..."); - match tokio::try_join!(screen_upload, audio_upload) { - Ok(_) => { - println!("Both upload loops completed successfully."); - }, - Err(e) => { - eprintln!("An error occurred: {}", e); - }, - } - } else { - println!("Skipping upload loops due to NEXT_PUBLIC_LOCAL_MODE being set to 'true'."); - } + match upload.await { + Ok(_) => { + println!("Both upload loops completed successfully."); + } + Err(e) => { + eprintln!("An error occurred: {}", e); + } + } + } else { + println!("Skipping upload loops due to NEXT_PUBLIC_LOCAL_MODE being set to 'true'."); + } - Ok(()) + Ok(()) } #[tauri::command] #[specta::specta] -pub async fn stop_all_recordings(state: State<'_, Arc>>) -> Result<(), String> { +pub async fn stop_all_recordings( + state: State<'_, Arc>>, +) -> Result<(), String> { let mut guard = state.lock().await; println!("Stopping media recording..."); @@ -119,7 +136,10 @@ pub async fn stop_all_recordings(state: State<'_, Arc>>) - if let Some(mut media_process) = guard.media_process.take() { println!("Stopping media recording..."); - media_process.stop_media_recording().await.expect("Failed to stop media recording"); + media_process + .stop_media_recording() + .await + .expect("Failed to stop media recording"); } let is_local_mode = match dotenv_codegen::dotenv!("NEXT_PUBLIC_LOCAL_MODE") { @@ -128,8 +148,7 @@ pub async fn stop_all_recordings(state: State<'_, Arc>>) - }; if !is_local_mode { - while !guard.video_uploading_finished.load(Ordering::SeqCst) - || !guard.audio_uploading_finished.load(Ordering::SeqCst) { + while !guard.uploading_finished.load(Ordering::SeqCst) { println!("Waiting for uploads to finish..."); tokio::time::sleep(Duration::from_millis(50)).await; } @@ -148,24 +167,23 @@ fn clean_and_create_dir(dir: &Path) -> Result<(), String> { std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; if !dir.to_string_lossy().contains("screenshots") { - let segment_list_path = dir.join("segment_list.txt"); - match File::open(&segment_list_path) { - Ok(_) => Ok(()), - Err(ref e) if e.kind() == ErrorKind::NotFound => { - File::create(&segment_list_path).map_err(|e| e.to_string())?; - Ok(()) - }, - Err(e) => Err(e.to_string()), - } + let segment_list_path = dir.join("segment_list.txt"); + match File::open(&segment_list_path) { + Ok(_) => Ok(()), + Err(ref e) if e.kind() == ErrorKind::NotFound => { + File::create(&segment_list_path).map_err(|e| e.to_string())?; + Ok(()) + } + Err(e) => Err(e.to_string()), + } } else { - Ok(()) + Ok(()) } } async fn start_upload_loop( chunks_dir: PathBuf, options: RecordingOptions, - video_type: String, shutdown_flag: Arc, uploading_finished: Arc, ) -> Result<(), String> { @@ -191,12 +209,17 @@ async fn start_upload_loop( let segment_path = chunks_dir.join(segment_filename); if segment_path.is_file() { let options_clone = options.clone(); - let video_type_clone = video_type.clone(); let segment_path_clone = segment_path.clone(); upload_tasks.push(tokio::spawn(async move { let filepath_str = segment_path_clone.to_str().unwrap_or_default().to_owned(); - println!("Uploading video for {}: {}", video_type_clone, filepath_str); - upload_file(Some(options_clone), filepath_str, video_type_clone).await.map(|_| ()) + println!("Uploading video: {}", filepath_str); + upload_file( + Some(options_clone), + filepath_str, + upload::FileType::VideoWithAudio, + ) + .await + .map(|_| ()) })); } watched_segments.insert(segment_filename.clone()); @@ -228,18 +251,25 @@ fn load_segment_list(segment_list_path: &Path) -> io::Result> { } async fn prepare_media_recording( - options: &RecordingOptions, - audio_chunks_dir: &Path, - screenshot_dir: &Path, - video_chunks_dir: &Path, - audio_name: Option, - max_screen_width: usize, - max_screen_height: usize, + options: &RecordingOptions, + screenshot_dir: &Path, + video_chunks_dir: &Path, + audio_name: Option, + max_screen_width: usize, + max_screen_height: usize, ) -> Result { - let mut media_recorder = MediaRecorder::new(); - let audio_file_path = audio_chunks_dir.to_str().unwrap(); - let video_file_path = video_chunks_dir.to_str().unwrap(); - let screenshot_dir_path = screenshot_dir.to_str().unwrap(); - media_recorder.start_media_recording(options.clone(), audio_file_path, screenshot_dir_path, video_file_path, audio_name.as_ref().map(String::as_str), max_screen_width, max_screen_height).await?; - Ok(media_recorder) + let mut media_recorder = MediaRecorder::new(); + let video_file_path = video_chunks_dir.to_str().unwrap(); + let screenshot_dir_path = screenshot_dir.to_str().unwrap(); + media_recorder + .start_media_recording( + options.clone(), + screenshot_dir_path, + video_file_path, + audio_name.as_ref().map(String::as_str), + max_screen_width, + max_screen_height, + ) + .await?; + Ok(media_recorder) } diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 55cf324d..d41543cc 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -1,25 +1,37 @@ +use regex::Regex; use reqwest; -use std::fs::File; -use std::io::Read; +use serde_json::Value as JsonValue; use std::path::Path; use std::process::{Command, Output}; use std::str; -use std::fs; -use regex::Regex; -use serde_json::Value as JsonValue; use crate::recording::RecordingOptions; -use crate::utils::{ffmpeg_path_as_str}; +use crate::utils::ffmpeg_path_as_str; + +pub enum FileType { + VideoWithAudio, + Screenshot, +} + +impl std::fmt::Display for FileType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + FileType::VideoWithAudio => write!(f, "video-with-audio"), + FileType::Screenshot => write!(f, "screenshot"), + } + } +} pub async fn upload_file( options: Option, file_path: String, - file_type: String, + file_type: FileType, ) -> Result { if let Some(ref options) = options { println!("Uploading video..."); - let duration = get_video_duration(&file_path).map_err(|e| format!("Failed to get video duration: {}", e))?; + let duration = get_video_duration(&file_path) + .map_err(|e| format!("Failed to get video duration: {}", e))?; let duration_str = duration.to_string(); let file_name = Path::new(&file_path) @@ -28,40 +40,48 @@ pub async fn upload_file( .ok_or("Invalid file path")? .to_string(); - let file_key = format!("{}/{}/{}/{}", options.user_id, options.video_id, file_type, file_name); + let file_key = format!( + "{}/{}/{}/{}", + options.user_id, + options.video_id, + file_type.to_string(), + file_name + ); let server_url_base: &'static str = dotenv_codegen::dotenv!("NEXT_PUBLIC_URL"); let server_url = format!("{}/api/upload/signed", server_url_base); - - let body: serde_json::Value; - - if file_type == "video" { - let (codec_name, width, height, frame_rate, bit_rate) = log_video_info(&file_path).map_err(|e| format!("Failed to log video info: {}", e))?; - - body = serde_json::json!({ - "userId": options.user_id, - "fileKey": file_key, - "awsBucket": options.aws_bucket, - "awsRegion": options.aws_region, - "duration": duration_str, - "resolution": format!("{}x{}", width, height), - "framerate": frame_rate, - "bandwidth": bit_rate, - "videoCodec": codec_name, - }); - } else { - body = serde_json::json!({ - "userId": options.user_id, - "fileKey": file_key, - "awsBucket": options.aws_bucket, - "awsRegion": options.aws_region, - "duration": duration_str, - }); - } + let body = match file_type { + FileType::VideoWithAudio => { + let (codec_name, width, height, frame_rate, bit_rate) = log_video_info(&file_path) + .map_err(|e| format!("Failed to log video info: {}", e))?; + + serde_json::json!({ + "userId": options.user_id, + "fileKey": file_key, + "awsBucket": options.aws_bucket, + "awsRegion": options.aws_region, + "duration": duration_str, + "resolution": format!("{}x{}", width, height), + "framerate": frame_rate, + "bandwidth": bit_rate, + "videoCodec": codec_name, + }) + } + FileType::Screenshot => { + serde_json::json!({ + "userId": options.user_id, + "fileKey": file_key, + "awsBucket": options.aws_bucket, + "awsRegion": options.aws_region, + "duration": duration_str, + }) + } + }; let client = reqwest::Client::new(); - let server_response = client.post(server_url) + let server_response = client + .post(server_url) .json(&body) .send() .await @@ -72,25 +92,26 @@ pub async fn upload_file( println!("Server response: {}", server_response); - // Deserialize the server response let presigned_post_data: JsonValue = serde_json::from_str(&server_response) .map_err(|e| format!("Failed to deserialize server response: {}", e))?; // Construct the multipart form for the file upload - let fields = presigned_post_data["presignedPostData"]["fields"].as_object() + let fields = presigned_post_data["presignedPostData"]["fields"] + .as_object() .ok_or("Fields object is missing or not an object")?; - + let mut form = reqwest::multipart::Form::new(); - + for (key, value) in fields.iter() { - let value_str = value.as_str() + let value_str = value + .as_str() .ok_or(format!("Value for key '{}' is not a string", key))?; form = form.text(key.to_string(), value_str.to_owned()); } println!("Uploading file: {}", file_path); - + let mime_type = if file_path.to_lowercase().ends_with(".aac") { "audio/aac" } else if file_path.to_lowercase().ends_with(".webm") { @@ -101,7 +122,9 @@ pub async fn upload_file( "video/mp2t" }; - let file_bytes = tokio::fs::read(&file_path).await.map_err(|e| format!("Failed to read file: {}", e))?; + let file_bytes = tokio::fs::read(&file_path) + .await + .map_err(|e| format!("Failed to read file: {}", e))?; let file_part = reqwest::multipart::Part::bytes(file_bytes) .file_name(file_name.clone()) .mime_str(mime_type) @@ -109,15 +132,13 @@ pub async fn upload_file( form = form.part("file", file_part); - let post_url = presigned_post_data["presignedPostData"]["url"].as_str() + let post_url = presigned_post_data["presignedPostData"]["url"] + .as_str() .ok_or("URL is missing or not a string")?; println!("Uploading file to: {}", post_url); - let response = client.post(post_url) - .multipart(form) - .send() - .await; + let response = client.post(post_url).multipart(form).send().await; match response { Ok(response) if response.status().is_success() => { @@ -125,9 +146,18 @@ pub async fn upload_file( } Ok(response) => { let status = response.status(); - let error_body = response.text().await.unwrap_or_else(|_| "".to_string()); - eprintln!("Failed to upload file. Status: {}. Body: {}", status, error_body); - return Err(format!("Failed to upload file. Status: {}. Body: {}", status, error_body)); + let error_body = response + .text() + .await + .unwrap_or_else(|_| "".to_string()); + eprintln!( + "Failed to upload file. Status: {}. Body: {}", + status, error_body + ); + return Err(format!( + "Failed to upload file. Status: {}. Body: {}", + status, error_body + )); } Err(e) => { return Err(format!("Failed to send upload file request: {}", e)); @@ -193,13 +223,14 @@ fn log_video_info(file_path: &str) -> Result<(String, String, String, String, St let codec_name = info_parts[0].to_string(); let width: String = info_parts[1].to_string(); let height: String = info_parts[2].to_string(); - + // Parse frame rate as a fraction and convert to float let frame_rate_parts: Vec<&str> = info_parts[3].split('/').collect(); - let frame_rate: f64 = frame_rate_parts[0].parse::().unwrap() / frame_rate_parts[1].parse::().unwrap(); + let frame_rate: f64 = + frame_rate_parts[0].parse::().unwrap() / frame_rate_parts[1].parse::().unwrap(); let frame_rate: String = frame_rate.to_string(); - + let bit_rate: String = info_parts[4].to_string(); Ok((codec_name, width, height, frame_rate, bit_rate)) -} \ No newline at end of file +} diff --git a/apps/desktop/src-tauri/src/utils.rs b/apps/desktop/src-tauri/src/utils.rs index 34b4213f..00a77071 100644 --- a/apps/desktop/src-tauri/src/utils.rs +++ b/apps/desktop/src-tauri/src/utils.rs @@ -110,8 +110,3 @@ pub fn create_named_pipe(path: &str) -> Result<(), nix::Error> { unistd::mkfifo(path, stat::Mode::S_IRWXU)?; Ok(()) } - -pub fn remove_named_pipe(path: &str) -> Result<(), std::io::Error> { - std::fs::remove_file(path)?; - Ok(()) -} diff --git a/apps/web/app/api/upload/mux/create/route.ts b/apps/web/app/api/upload/mux/create/route.ts index bea163b5..6122648d 100644 --- a/apps/web/app/api/upload/mux/create/route.ts +++ b/apps/web/app/api/upload/mux/create/route.ts @@ -97,8 +97,7 @@ export async function GET(request: NextRequest) { } const bucket = process.env.CAP_AWS_BUCKET || ""; - const videoPrefix = `${userId}/${videoId}/video/`; - const audioPrefix = `${userId}/${videoId}/audio/`; + const videoPrefix = `${userId}/${videoId}/video-with-audio/`; try { const s3Client = new S3Client({ @@ -114,13 +113,7 @@ export async function GET(request: NextRequest) { Prefix: videoPrefix, }); - const audioSegmentCommand = new ListObjectsV2Command({ - Bucket: bucket, - Prefix: audioPrefix, - }); - const videoSegments = await s3Client.send(videoSegmentCommand); - const audioSegments = await s3Client.send(audioSegmentCommand); const videoSegmentKeys = (videoSegments.Contents || []).map( (object) => `s3://${bucket}/${object.Key}` @@ -148,10 +141,6 @@ export async function GET(request: NextRequest) { ); } - const audioSegmentKeys = (audioSegments.Contents || []).map( - (object) => `s3://${bucket}/${object.Key}` - ); - const mediaConvertClient = new MediaConvertClient({ region: process.env.CAP_AWS_REGION || "", credentials: { @@ -165,18 +154,14 @@ export async function GET(request: NextRequest) { const createJobCommand = new CreateJobCommand({ Role: process.env.CAP_AWS_MEDIACONVERT_ROLE_ARN || "", Settings: { - Inputs: videoSegmentKeys.map((videoSegmentKey, index) => { - const audioSegmentKey = audioSegmentKeys[index]; + Inputs: videoSegmentKeys.map((videoSegmentKey) => { return { FileInput: videoSegmentKey, - ...(audioSegmentKey && { - AudioSelectors: { - "Audio Selector 1": { - DefaultSelection: "DEFAULT", - ExternalAudioFileInput: audioSegmentKey, - }, + AudioSelectors: { + "Audio Selector 1": { + DefaultSelection: "DEFAULT", }, - }), + }, VideoSelector: {}, TimecodeSource: "ZEROBASED", }; @@ -218,21 +203,19 @@ export async function GET(request: NextRequest) { }, }, }, - ...(audioSegmentKeys.length > 0 && { - AudioDescriptions: [ - { - AudioSourceName: "Audio Selector 1", - CodecSettings: { - Codec: "AAC", - AacSettings: { - Bitrate: 128000, - CodingMode: "CODING_MODE_2_0", - SampleRate: 48000, - }, + AudioDescriptions: [ + { + AudioSourceName: "Audio Selector 1", + CodecSettings: { + Codec: "AAC", + AacSettings: { + Bitrate: 128000, + CodingMode: "CODING_MODE_2_0", + SampleRate: 48000, }, }, - ], - }), + }, + ], }, ], }, From 861a0be09c017e7fd74efc96859fad423174ff3b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 16:49:01 +0800 Subject: [PATCH 10/28] cleanup --- apps/desktop/src-tauri/src/media.rs | 43 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index e7b8d02a..d037a34e 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -457,8 +457,8 @@ impl MediaRecorder { }); println!("Starting audio recording and processing..."); - let video_output_chunk_pattern = format!("{chunks_file_path}/video_recording_%03d.ts"); - let video_segment_list_filename = format!("{chunks_file_path}/segment_list.txt"); + let output_chunk_pattern = format!("{chunks_file_path}/recording_%03d.ts"); + let segment_list_filename = format!("{chunks_file_path}/segment_list.txt"); let mut audio_filters = Vec::new(); @@ -520,7 +520,7 @@ impl MediaRecorder { ["-f", "segment"], ["-segment_time", "3"], ["-segment_time_delta", "0.01"], - ["-segment_list", &video_segment_list_filename], + ["-segment_list", &segment_list_filename], ["-segment_format", "ts"], ["-movflags", "frag_keyframe+empty_moov"], ["-reset_timestamps", "1"], @@ -530,7 +530,7 @@ impl MediaRecorder { .into_iter() .flatten() .map(|s| s.to_string()) - .chain([video_output_chunk_pattern]) + .chain([output_chunk_pattern]) .collect(); if needs_audio { @@ -551,10 +551,10 @@ impl MediaRecorder { .map_err(|e| e.to_string())?; println!("Ffmpeg process started"); - if let Some(ffmpeg_video_stdin) = &self.ffmpeg_stdin { - let mut video_stdin_lock = ffmpeg_video_stdin.lock().await; - *video_stdin_lock = Some(ffmpeg_stdin); - drop(video_stdin_lock); + if let Some(ffmpeg_stdin) = &self.ffmpeg_stdin { + let mut stdin_lock = ffmpeg_stdin.lock().await; + *stdin_lock = Some(ffmpeg_stdin); + drop(stdin_lock); println!("Ffmpeg stdin set"); } @@ -582,7 +582,7 @@ impl MediaRecorder { println!("Starting video channel senders..."); tokio::spawn(async move { - let mut video_pipe = File::create(video_pipe_path).await.unwrap(); + let mut pipe = File::create(video_pipe_path).await.unwrap(); while let Some(bytes) = &video_channel_receiver .lock() @@ -592,8 +592,7 @@ impl MediaRecorder { .recv() .await { - video_pipe - .write_all(&bytes) + pipe.write_all(&bytes) .await .expect("Failed to write video data to FFmpeg stdin"); } @@ -625,16 +624,15 @@ impl MediaRecorder { let segment_duration = Duration::from_secs(3); let recording_duration = start_time.elapsed(); let expected_segments = recording_duration.as_secs() / segment_duration.as_secs(); - let video_file_path = self.file_path.as_ref().ok_or("Video file path not set")?; - let video_segment_list_filename = format!("{}/segment_list.txt", video_file_path); + let file_path = self.file_path.as_ref().ok_or("File path not set")?; + let segment_list_filename = format!("{}/segment_list.txt", file_path); loop { - let video_segments = - std::fs::read_to_string(&video_segment_list_filename).unwrap_or_default(); + let segments = std::fs::read_to_string(&segment_list_filename).unwrap_or_default(); - let video_segment_count = video_segments.lines().count(); + let segment_count = segments.lines().count(); - if video_segment_count >= expected_segments as usize { + if segment_count >= expected_segments as usize { println!("All segments generated"); break; } @@ -643,13 +641,13 @@ impl MediaRecorder { } } - if let Some(ref ffmpeg_video_stdin) = self.ffmpeg_stdin { - let mut video_stdin_guard = ffmpeg_video_stdin.lock().await; - if let Some(mut video_stdin) = video_stdin_guard.take() { - if let Err(e) = video_stdin.write_all(b"q\n").await { + if let Some(ref ffmpeg_stdin) = self.ffmpeg_stdin { + let mut stdin_guard = ffmpeg_stdin.lock().await; + if let Some(mut stdin) = stdin_guard.take() { + if let Err(e) = stdin.write_all(b"q\n").await { eprintln!("Failed to send 'q' to video FFmpeg process: {}", e); } - let _ = video_stdin.shutdown().await.map_err(|e| e.to_string()); + let _ = stdin.shutdown().await.map_err(|e| e.to_string()); } } @@ -774,7 +772,6 @@ async fn wait_for_start_times( async fn adjust_ffmpeg_commands_based_on_start_times( audio_start_time: Arc>>, video_start_time: Arc>>, - // ffmpeg_audio_command: &mut Vec, ffmpeg_video_command: &mut Vec, ) { let (audio_start, video_start) = wait_for_start_times(audio_start_time, video_start_time).await; From 933dadaf7519600e0a457ab088c8b3cb7767e173 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 17:22:23 +0800 Subject: [PATCH 11/28] cleanup --- apps/desktop/src-tauri/src/media.rs | 120 +++++++++++++----------- apps/desktop/src-tauri/src/recording.rs | 1 - 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index d037a34e..61b87373 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -480,36 +480,51 @@ impl MediaRecorder { std::fs::remove_file(&audio_pipe_path).ok(); create_named_pipe(&audio_pipe_path).map_err(|e| e.to_string())?; + let time_offset = if needs_audio { + println!("Adjusting FFmpeg commands based on start times..."); + create_time_offset_args(&audio_start_time, &video_start_time).await + } else { + None + }; + let size = format!("{}x{}", adjusted_width, adjusted_height); - let mut ffmpeg_command = vec![ + let mut ffmpeg_command = flatten_str_args([ ["-f", "rawvideo"], ["-pix_fmt", "bgra"], ["-s", &size], ["-r", "30"], ["-thread_queue_size", "4096"], ["-i", &video_pipe_path], - ]; + ]); - if needs_audio { - ffmpeg_command.extend([ - // in - ["-f", sample_format], - ["-ar", &sample_rate_str], - ["-ac", &channels_str], - ["-thread_queue_size", "4096"], - ["-i", &audio_pipe_path], - // out - [ - "-af", - "aresample=async=1:min_hard_comp=0.100000:first_pts=0", - ], - ["-c:a", "aac"], - ["-b:a", "128k"], - ["-async", "1"], - ]); + let mut audio_args = flatten_str_args([ + // in + ["-f", sample_format], + ["-ar", &sample_rate_str], + ["-ac", &channels_str], + ["-thread_queue_size", "4096"], + ["-i", &audio_pipe_path], + // out + [ + "-af", + "aresample=async=1:min_hard_comp=0.100000:first_pts=0", + ], + ["-c:a", "aac"], + ["-b:a", "128k"], + ["-async", "1"], + ]); + + match time_offset { + Some((TimeOffsetTarget::Video, args)) => ffmpeg_command.splice(0..0, args.clone()), + Some((TimeOffsetTarget::Audio, args)) => audio_args.splice(0..0, args.clone()), + None => {} }; - ffmpeg_command.extend([ + if needs_audio { + ffmpeg_command.extend(audio_args); + } + + ffmpeg_command.extend(flatten_str_args([ ["-vf", "fps=30,scale=in_range=full:out_range=limited"], ["-c:v", "libx264"], ["-preset", "ultrafast"], @@ -524,24 +539,9 @@ impl MediaRecorder { ["-segment_format", "ts"], ["-movflags", "frag_keyframe+empty_moov"], ["-reset_timestamps", "1"], - ]); + ])); - let mut ffmpeg_command = ffmpeg_command - .into_iter() - .flatten() - .map(|s| s.to_string()) - .chain([output_chunk_pattern]) - .collect(); - - if needs_audio { - println!("Adjusting FFmpeg commands based on start times..."); - adjust_ffmpeg_commands_based_on_start_times( - Arc::clone(&audio_start_time), - Arc::clone(&video_start_time), - &mut ffmpeg_command, - ) - .await; - } + ffmpeg_command.push(output_chunk_pattern); println!("Starting FFmpeg process..."); @@ -551,8 +551,8 @@ impl MediaRecorder { .map_err(|e| e.to_string())?; println!("Ffmpeg process started"); - if let Some(ffmpeg_stdin) = &self.ffmpeg_stdin { - let mut stdin_lock = ffmpeg_stdin.lock().await; + if let Some(ffmpeg_stdin_mutex) = &self.ffmpeg_stdin { + let mut stdin_lock = ffmpeg_stdin_mutex.lock().await; *stdin_lock = Some(ffmpeg_stdin); drop(stdin_lock); println!("Ffmpeg stdin set"); @@ -751,8 +751,8 @@ async fn start_recording_process( } async fn wait_for_start_times( - audio_start_time: Arc>>, - video_start_time: Arc>>, + audio_start_time: &Mutex>, + video_start_time: &Mutex>, ) -> (Instant, Instant) { loop { let audio_start_locked = audio_start_time.lock().await; @@ -769,11 +769,15 @@ async fn wait_for_start_times( } } -async fn adjust_ffmpeg_commands_based_on_start_times( - audio_start_time: Arc>>, - video_start_time: Arc>>, - ffmpeg_video_command: &mut Vec, -) { +pub enum TimeOffsetTarget { + Audio, + Video, +} + +async fn create_time_offset_args( + audio_start_time: &Mutex>, + video_start_time: &Mutex>, +) -> Option<(TimeOffsetTarget, Vec)> { let (audio_start, video_start) = wait_for_start_times(audio_start_time, video_start_time).await; let duration_difference = if audio_start > video_start { audio_start.duration_since(video_start) @@ -792,17 +796,25 @@ async fn adjust_ffmpeg_commands_based_on_start_times( // Depending on which started first, adjust the relevant FFmpeg command if audio_start > video_start { // Offset the video start time - ffmpeg_video_command.splice( - 0..0, - vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], - ); println!("Applying -itsoffset {:.3} to video", offset_seconds); + + Some(( + TimeOffsetTarget::Video, + vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], + )) } else if video_start > audio_start { // Offset the audio start time - // ffmpeg_audio_command.splice( - // 0..0, - // vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], - // ); println!("Applying -itsoffset {:.3} to audio", offset_seconds); + + Some(( + TimeOffsetTarget::Audio, + vec!["-itsoffset".to_string(), format!("{:.3}", offset_seconds)], + )) + } else { + None } } + +fn flatten_str_args<'a>(args: impl IntoIterator) -> Vec { + args.into_iter().flatten().map(|s| s.to_string()).collect() +} diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 67f3b401..db0c6ae2 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -124,7 +124,6 @@ pub async fn start_dual_recording( } #[tauri::command] -#[specta::specta] pub async fn stop_all_recordings( state: State<'_, Arc>>, ) -> Result<(), String> { From f9b3a787c16a3573b017f0b398f318afa12e7df3 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 17:24:58 +0800 Subject: [PATCH 12/28] segment --- apps/desktop/src-tauri/src/recording.rs | 10 +++------- apps/desktop/src-tauri/src/upload.rs | 6 +++--- apps/web/app/api/upload/mux/create/route.ts | 14 +++++++------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index db0c6ae2..e68aff73 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -212,13 +212,9 @@ async fn start_upload_loop( upload_tasks.push(tokio::spawn(async move { let filepath_str = segment_path_clone.to_str().unwrap_or_default().to_owned(); println!("Uploading video: {}", filepath_str); - upload_file( - Some(options_clone), - filepath_str, - upload::FileType::VideoWithAudio, - ) - .await - .map(|_| ()) + upload_file(Some(options_clone), filepath_str, upload::FileType::Segment) + .await + .map(|_| ()) })); } watched_segments.insert(segment_filename.clone()); diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index d41543cc..d24c3d58 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -9,14 +9,14 @@ use crate::recording::RecordingOptions; use crate::utils::ffmpeg_path_as_str; pub enum FileType { - VideoWithAudio, + Segment, Screenshot, } impl std::fmt::Display for FileType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - FileType::VideoWithAudio => write!(f, "video-with-audio"), + FileType::Segment => write!(f, "segment"), FileType::Screenshot => write!(f, "screenshot"), } } @@ -52,7 +52,7 @@ pub async fn upload_file( let server_url = format!("{}/api/upload/signed", server_url_base); let body = match file_type { - FileType::VideoWithAudio => { + FileType::Segment => { let (codec_name, width, height, frame_rate, bit_rate) = log_video_info(&file_path) .map_err(|e| format!("Failed to log video info: {}", e))?; diff --git a/apps/web/app/api/upload/mux/create/route.ts b/apps/web/app/api/upload/mux/create/route.ts index 6122648d..4de99e9e 100644 --- a/apps/web/app/api/upload/mux/create/route.ts +++ b/apps/web/app/api/upload/mux/create/route.ts @@ -97,7 +97,7 @@ export async function GET(request: NextRequest) { } const bucket = process.env.CAP_AWS_BUCKET || ""; - const videoPrefix = `${userId}/${videoId}/video-with-audio/`; + const segmentsPrefix = `${userId}/${videoId}/segment/`; try { const s3Client = new S3Client({ @@ -108,18 +108,18 @@ export async function GET(request: NextRequest) { }, }); - const videoSegmentCommand = new ListObjectsV2Command({ + const segmentsCommand = new ListObjectsV2Command({ Bucket: bucket, - Prefix: videoPrefix, + Prefix: segmentsPrefix, }); - const videoSegments = await s3Client.send(videoSegmentCommand); + const segments = await s3Client.send(segmentsCommand); - const videoSegmentKeys = (videoSegments.Contents || []).map( + const segmentKeys = (segments.Contents || []).map( (object) => `s3://${bucket}/${object.Key}` ); - if (videoSegmentKeys.length > 149) { + if (segmentKeys.length > 149) { await db .update(videos) .set({ skipProcessing: true }) @@ -154,7 +154,7 @@ export async function GET(request: NextRequest) { const createJobCommand = new CreateJobCommand({ Role: process.env.CAP_AWS_MEDIACONVERT_ROLE_ARN || "", Settings: { - Inputs: videoSegmentKeys.map((videoSegmentKey) => { + Inputs: segmentKeys.map((videoSegmentKey) => { return { FileInput: videoSegmentKey, AudioSelectors: { From b35b4ef3468c10ba6ef79ba6bb48e35c58d1898f Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 17:52:46 +0800 Subject: [PATCH 13/28] fix splice --- apps/desktop/src-tauri/src/media.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index 61b87373..c0366588 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -515,9 +515,15 @@ impl MediaRecorder { ]); match time_offset { - Some((TimeOffsetTarget::Video, args)) => ffmpeg_command.splice(0..0, args.clone()), - Some((TimeOffsetTarget::Audio, args)) => audio_args.splice(0..0, args.clone()), - None => {} + Some((TimeOffsetTarget::Video, args)) => { + ffmpeg_command.splice(0..0, args.clone()).collect() + } + Some((TimeOffsetTarget::Audio, args)) => { + audio_args.splice(0..0, args.clone()).collect() + } + None => { + vec![] + } }; if needs_audio { From 61368f9e7799853d64be40823a03d5b1b63931e4 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 14 Jun 2024 00:13:55 +0800 Subject: [PATCH 14/28] fix --- apps/desktop/src-tauri/src/recording.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index e68aff73..aa927e9a 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -124,6 +124,7 @@ pub async fn start_dual_recording( } #[tauri::command] +#[specta::specta] pub async fn stop_all_recordings( state: State<'_, Arc>>, ) -> Result<(), String> { From 423c72e4f1e8e38d56cd7a01b62be3471ef2dec9 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 10 Jul 2024 12:20:58 +0800 Subject: [PATCH 15/28] cleanup gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index d18e2c7d..b70ab983 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,3 @@ target *.sw? .turbo pnpm-lock.yaml -target From 8bb9b8358c6b3fb2946a6a4c3dfa8f17e2f6a066 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sun, 14 Jul 2024 20:02:51 -0700 Subject: [PATCH 16/28] Refactor to use `dub` TypeScript SDK --- .../web/app/api/desktop/video/create/route.ts | 23 +++++-------------- apps/web/app/api/video/analytics/route.ts | 18 +++++---------- apps/web/package.json | 1 + apps/web/utils/dub.ts | 5 ++++ pnpm-lock.yaml | 13 ++++++++++- 5 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 apps/web/utils/dub.ts diff --git a/apps/web/app/api/desktop/video/create/route.ts b/apps/web/app/api/desktop/video/create/route.ts index c7bd9ac4..fa65d979 100644 --- a/apps/web/app/api/desktop/video/create/route.ts +++ b/apps/web/app/api/desktop/video/create/route.ts @@ -4,6 +4,7 @@ import { videos } from "@cap/database/schema"; import { getCurrentUser } from "@cap/database/auth/session"; import { nanoId } from "@cap/database/helpers"; import { cookies } from "next/headers"; +import { dub } from "@/utils/dub"; const allowedOrigins = [ process.env.NEXT_PUBLIC_URL, @@ -91,23 +92,11 @@ export async function GET(req: NextRequest) { process.env.NEXT_PUBLIC_IS_CAP && process.env.NEXT_PUBLIC_ENVIRONMENT === "production" ) { - const dubOptions = { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.DUB_API_KEY}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - url: process.env.NEXT_PUBLIC_URL + "/s/" + id, - key: id, - domain: "cap.link", - }), - }; - - await fetch("https://api.dub.co/links?projectSlug=cap", dubOptions) - .then((response) => response.json()) - .then((response) => console.log(response)) - .catch((err) => console.error(err)); + await dub.links.create({ + url: process.env.NEXT_PUBLIC_URL + "/s/" + id, + domain: "cap.link", + key: id, + }); } return new Response( diff --git a/apps/web/app/api/video/analytics/route.ts b/apps/web/app/api/video/analytics/route.ts index 699832e9..ac861fe7 100644 --- a/apps/web/app/api/video/analytics/route.ts +++ b/apps/web/app/api/video/analytics/route.ts @@ -1,3 +1,5 @@ +import { dub } from "@/utils/dub"; +import { ClicksCount } from "dub/models/components"; import { NextRequest } from "next/server"; export const revalidate = 300; @@ -15,18 +17,10 @@ export async function GET(request: NextRequest) { }); } - const dubOptions = { - method: "GET", - headers: { - Authorization: `Bearer ${process.env.DUB_API_KEY}`, - "Content-Type": "application/json", - }, - }; - - const analytics = await fetch( - `https://api.dub.co/analytics/clicks?projectSlug=cap&domain=cap.link&key=${videoId}`, - dubOptions - ).then((response) => response.json()); + const { clicks: analytics } = (await dub.analytics.retrieve({ + domain: "cap.link", + key: videoId, + })) as ClicksCount; if (typeof analytics !== "number") { return new Response(JSON.stringify({ error: true }), { diff --git a/apps/web/package.json b/apps/web/package.json index eb3b5661..7121c8d4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,6 +33,7 @@ "date-fns": "^3.2.0", "dotenv": "^16.3.1", "drizzle-orm": "0.30.9", + "dub": "^0.33.1", "hls.js": "^1.5.3", "lodash": "^4.17.21", "lucide-react": "^0.294.0", diff --git a/apps/web/utils/dub.ts b/apps/web/utils/dub.ts new file mode 100644 index 00000000..bd35ce65 --- /dev/null +++ b/apps/web/utils/dub.ts @@ -0,0 +1,5 @@ +import { Dub } from "dub"; + +export const dub = new Dub({ + token: process.env.DUB_WORKSPACE_API_KEY, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 292cd541..4331d5ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -368,6 +368,9 @@ importers: drizzle-orm: specifier: 0.30.9 version: 0.30.9(@planetscale/database@1.13.0)(@types/react@18.2.48)(mysql2@3.9.7)(react@18.2.0) + dub: + specifier: ^0.33.1 + version: 0.33.1(zod@3.22.4) hls.js: specifier: ^1.5.3 version: 1.5.3 @@ -9291,6 +9294,14 @@ packages: react: 18.2.0 dev: false + /dub@0.33.1(zod@3.22.4): + resolution: {integrity: sha512-R0X0z9+nPCLs7J62kH2K3tX5L6fNigsPsw/Ch6zvfMy3hgSuSrA6idGQ474e/kJQdXXSi8T4hZsAPy1UzTZHAA==} + peerDependencies: + zod: '>= 3' + dependencies: + zod: 3.22.4 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -10007,7 +10018,7 @@ packages: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 From 8cf6a4216976f8e6919a0690b33756dcf84bf343 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sun, 14 Jul 2024 20:05:20 -0700 Subject: [PATCH 17/28] Update dub.ts --- apps/web/utils/dub.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/utils/dub.ts b/apps/web/utils/dub.ts index bd35ce65..59acf19b 100644 --- a/apps/web/utils/dub.ts +++ b/apps/web/utils/dub.ts @@ -1,5 +1,5 @@ import { Dub } from "dub"; export const dub = new Dub({ - token: process.env.DUB_WORKSPACE_API_KEY, + token: process.env.DUB_API_KEY, }); From 0ff0aafc15344aee900d452549fbbb4cd7d79c62 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sun, 14 Jul 2024 20:08:08 -0700 Subject: [PATCH 18/28] Update dub.ts --- apps/web/utils/dub.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/utils/dub.ts b/apps/web/utils/dub.ts index 59acf19b..7f329035 100644 --- a/apps/web/utils/dub.ts +++ b/apps/web/utils/dub.ts @@ -1,5 +1,3 @@ import { Dub } from "dub"; -export const dub = new Dub({ - token: process.env.DUB_API_KEY, -}); +export const dub = new Dub(); From 13423ffc875be4fe5ac9de0b9b6ca51bf2e168b3 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:53:14 +0100 Subject: [PATCH 19/28] feat: update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8d6ada89..dc442713 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ dist-ssr *.sln *.sw? .turbo -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml +/target \ No newline at end of file From e2f02fbaec18c3ce0a03be892cd6d3bbb73ed6da Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:49:30 +0100 Subject: [PATCH 20/28] feat: add tooltips to Caps dashboard --- apps/desktop/package.json | 2 +- apps/web/app/api/video/analytics/route.ts | 58 ++++++++++++++----- apps/web/app/dashboard/caps/Caps.tsx | 52 ++++++++++++++--- .../s/[videoId]/_components/ShareHeader.tsx | 2 - .../s/[videoId]/_components/ShareVideo.tsx | 2 - apps/web/utils/dub.ts | 4 +- 6 files changed, 91 insertions(+), 29 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f50e766b..d89a1472 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.2.7", "scripts": { - "dev": "tauri dev", + "dev": "RUST_BACKTRACE=1 tauri dev", "build": "tsc && next build", "tauri": "tauri" }, diff --git a/apps/web/app/api/video/analytics/route.ts b/apps/web/app/api/video/analytics/route.ts index ac861fe7..125c7e18 100644 --- a/apps/web/app/api/video/analytics/route.ts +++ b/apps/web/app/api/video/analytics/route.ts @@ -2,7 +2,7 @@ import { dub } from "@/utils/dub"; import { ClicksCount } from "dub/models/components"; import { NextRequest } from "next/server"; -export const revalidate = 300; +export const revalidate = 180; export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; @@ -17,24 +17,54 @@ export async function GET(request: NextRequest) { }); } - const { clicks: analytics } = (await dub.analytics.retrieve({ - domain: "cap.link", - key: videoId, - })) as ClicksCount; + try { + const response = await dub.analytics.retrieve({ + domain: "cap.link", + key: videoId, + }); + console.log(response); + const { clicks: analytics } = response as ClicksCount; + + if (typeof analytics !== "number" || analytics === null) { + console.log("analytics error"); + return new Response(JSON.stringify({ error: true }), { + status: 401, + headers: { + "Content-Type": "application/json", + }, + }); + } - if (typeof analytics !== "number") { + return new Response(JSON.stringify({ count: analytics }), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); + } catch (error: any) { + // Handle specific 'not_found' error + if (error.code === "not_found") { + return new Response( + JSON.stringify({ + error: true, + message: "Video link not found.", + docUrl: error.docUrl, + }), + { + status: 404, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + // Handle other unexpected errors return new Response(JSON.stringify({ error: true }), { - status: 401, + status: 500, headers: { "Content-Type": "application/json", }, }); } - - return new Response(JSON.stringify({ count: analytics }), { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); } diff --git a/apps/web/app/dashboard/caps/Caps.tsx b/apps/web/app/dashboard/caps/Caps.tsx index 9c4a68c7..4eea4e7d 100644 --- a/apps/web/app/dashboard/caps/Caps.tsx +++ b/apps/web/app/dashboard/caps/Caps.tsx @@ -26,6 +26,7 @@ import { } from "@cap/ui"; import { debounce } from "lodash"; import { playlistToMp4 } from "@/utils/video/ffmpeg/helpers"; +import { Tooltip } from "react-tooltip"; type videoData = { id: string; @@ -144,7 +145,7 @@ export const Caps = ({ data, count }: { data: videoData; count: number }) => { } ) .finally(() => { - setIsDownloading(null); // Reset downloading state after completion or failure + setIsDownloading(null); }); }; @@ -211,7 +212,7 @@ export const Caps = ({ data, count }: { data: videoData; count: number }) => { return (
{ {titles[cap.id] || cap.name}

)} -

- {moment(cap.createdAt).fromNow()} +

+ + {moment(cap.createdAt).fromNow()} + +

-
-
+
+
{videoAnalytics ?? "-"} +
-
+
{cap.totalComments} +
-
+
{cap.totalReactions} +
diff --git a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx index 267c24dc..d5361b03 100644 --- a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx @@ -6,7 +6,6 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { toast } from "react-hot-toast"; import { LinkIcon } from "lucide-react"; -import { Tooltip } from "react-tooltip"; export const ShareHeader = ({ data, @@ -53,7 +52,6 @@ export const ShareHeader = ({ return ( <> -
diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index bfcf4e18..756b9486 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -11,7 +11,6 @@ import { } from "lucide-react"; import { LogoSpinner } from "@cap/ui"; import { userSelectProps } from "@cap/database/auth/session"; -import { Tooltip } from "react-tooltip"; import { fromVtt, Subtitle } from "subtitles-parser-vtt"; import { is } from "drizzle-orm"; import toast from "react-hot-toast"; @@ -410,7 +409,6 @@ export const ShareVideo = ({ : "User" }`} > - {comment.type === "text" ? ( Date: Mon, 15 Jul 2024 17:56:44 +0100 Subject: [PATCH 21/28] feat: revert analytics refresh to 5 mins --- apps/web/app/api/video/analytics/route.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/web/app/api/video/analytics/route.ts b/apps/web/app/api/video/analytics/route.ts index 125c7e18..6a7f2922 100644 --- a/apps/web/app/api/video/analytics/route.ts +++ b/apps/web/app/api/video/analytics/route.ts @@ -2,7 +2,7 @@ import { dub } from "@/utils/dub"; import { ClicksCount } from "dub/models/components"; import { NextRequest } from "next/server"; -export const revalidate = 180; +export const revalidate = 300; export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; @@ -22,11 +22,9 @@ export async function GET(request: NextRequest) { domain: "cap.link", key: videoId, }); - console.log(response); const { clicks: analytics } = response as ClicksCount; if (typeof analytics !== "number" || analytics === null) { - console.log("analytics error"); return new Response(JSON.stringify({ error: true }), { status: 401, headers: { @@ -42,7 +40,6 @@ export async function GET(request: NextRequest) { }, }); } catch (error: any) { - // Handle specific 'not_found' error if (error.code === "not_found") { return new Response( JSON.stringify({ @@ -58,8 +55,6 @@ export async function GET(request: NextRequest) { } ); } - - // Handle other unexpected errors return new Response(JSON.stringify({ error: true }), { status: 500, headers: { From 9155b83e5b1648669dcbf975c8be77238fb7b99d Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:52:24 +0100 Subject: [PATCH 22/28] feat: Sign out function + improved login notification flow --- .../_components/DynamicSharedLayout.tsx | 7 ++-- apps/web/app/login/form.tsx | 32 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/web/app/dashboard/_components/DynamicSharedLayout.tsx b/apps/web/app/dashboard/_components/DynamicSharedLayout.tsx index 3d25ae09..8b46896f 100644 --- a/apps/web/app/dashboard/_components/DynamicSharedLayout.tsx +++ b/apps/web/app/dashboard/_components/DynamicSharedLayout.tsx @@ -6,15 +6,14 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, - Button, } from "@cap/ui"; import { UsageButton } from "@/components/UsageButton"; import { users, spaces } from "@cap/database/schema"; import Link from "next/link"; import { isUserOnProPlan } from "@cap/utils"; +import { signOut } from "next-auth/react"; type SharedContext = { spaceData: (typeof spaces.$inferSelect)[] | null; @@ -119,9 +118,9 @@ export default function DynamicSharedLayout({ - + diff --git a/apps/web/app/login/form.tsx b/apps/web/app/login/form.tsx index 90e8c8b7..c78c551a 100644 --- a/apps/web/app/login/form.tsx +++ b/apps/web/app/login/form.tsx @@ -11,6 +11,7 @@ export function LoginForm() { const next = searchParams?.get("next"); const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); + const [emailSent, setEmailSent] = useState(false); useEffect(() => { const error = searchParams?.get("error"); @@ -31,21 +32,17 @@ export function LoginForm() { ...(next && next.length > 0 ? { callbackUrl: next } : {}), }) .then((res) => { - console.log("res"); - console.log(res); setLoading(false); if (res?.ok && !res?.error) { setEmail(""); - toast.success("Email sent - check your inbox!", { - duration: 20000, - }); + setEmailSent(true); + toast.success("Email sent - check your inbox!"); } else { toast.error("Error sending email - try again?"); } }) .catch((err) => { - console.log("err"); - console.log(err); + setEmailSent(false); setLoading(false); toast.error("Error sending email - try again?"); }); @@ -58,10 +55,11 @@ export function LoginForm() { name="email" autoFocus type="email" - placeholder="tim@apple.com" + placeholder={emailSent ? "" : "tim@apple.com"} autoComplete="email" required value={email} + disabled={emailSent} onChange={(e) => { setEmail(e.target.value); }} @@ -73,9 +71,9 @@ export function LoginForm() { size="lg" className="h-12 text-lg" type="submit" - disabled={loading} + disabled={loading || emailSent} > - Continue with Email + {emailSent ? "Email was sent to your inbox" : "Continue with Email"}

By typing your email and clicking continue, you acknowledge that you @@ -98,6 +96,20 @@ export function LoginForm() { .

+ {emailSent && ( +
+ +
+ )} ); } From 3026084761118687dbf5eb844ea6bfdb4ab0cf18 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 17 Jul 2024 01:49:49 +0800 Subject: [PATCH 23/28] undo some merge conflict problems --- apps/desktop/src-tauri/src/main.rs | 26 +++- apps/desktop/src-tauri/src/media.rs | 179 +++++++++++------------- apps/desktop/src-tauri/src/recording.rs | 48 +++++-- apps/desktop/src-tauri/src/upload.rs | 44 +++--- apps/desktop/src-tauri/src/utils.rs | 2 +- 5 files changed, 164 insertions(+), 135 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index da239710..6cac8159 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -35,6 +35,18 @@ use ffmpeg_sidecar::{ use winit::monitor::{MonitorHandle, VideoMode}; +macro_rules! generate_handler { + ($($command:ident),*) => {{ + #[cfg(debug_assertions)] + tauri_specta::ts::export( + specta::collect_types![$($command),*], + "../src/utils/commands.ts" + ).unwrap(); + + tauri::generate_handler![$($command),*] + }} +} + fn main() { let _ = fix_path_env::fix(); @@ -73,6 +85,7 @@ fn main() { handle_ffmpeg_installation().expect("Failed to install FFmpeg"); #[tauri::command] + #[specta::specta] async fn start_server(window: Window) -> Result { start(move |url| { let _ = window.emit("redirect_uri", url); @@ -81,6 +94,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn open_screen_capture_preferences() { #[cfg(target_os = "macos")] std::process::Command::new("open") @@ -90,6 +104,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn open_mic_preferences() { #[cfg(target_os = "macos")] std::process::Command::new("open") @@ -99,6 +114,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn open_camera_preferences() { #[cfg(target_os = "macos")] std::process::Command::new("open") @@ -108,6 +124,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn reset_screen_permissions() { #[cfg(target_os = "macos")] std::process::Command::new("tccutil") @@ -119,6 +136,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn reset_microphone_permissions() { #[cfg(target_os = "macos")] std::process::Command::new("tccutil") @@ -130,6 +148,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn reset_camera_permissions() { #[cfg(target_os = "macos")] std::process::Command::new("tccutil") @@ -221,7 +240,8 @@ fn main() { media_process: None, recording_options: None, shutdown_flag: Arc::new(AtomicBool::new(false)), - uploading_finished: Arc::new(AtomicBool::new(false)), + video_uploading_finished: Arc::new(AtomicBool::new(false)), + audio_uploading_finished: Arc::new(AtomicBool::new(false)), data_dir: Some(data_directory), max_screen_width: max_width as usize, max_screen_height: max_height as usize, @@ -307,7 +327,7 @@ fn main() { Ok(()) }) - .invoke_handler(tauri::generate_handler![ + .invoke_handler(generate_handler![ start_dual_recording, stop_all_recordings, enumerate_audio_devices, @@ -318,7 +338,7 @@ fn main() { has_screen_capture_access, reset_screen_permissions, reset_microphone_permissions, - reset_camera_permissions, + reset_camera_permissions ]) .plugin(tauri_plugin_context_menu::init()) .system_tray(tray) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index 669474da..baa90ae7 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -4,7 +4,7 @@ use cpal::SampleFormat; use image::codecs::jpeg::JpegEncoder; use image::{ImageBuffer, ImageFormat, Rgba}; use std::io::{Error, ErrorKind::WouldBlock}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::{ atomic::{AtomicBool, Ordering}, @@ -37,7 +37,7 @@ pub struct MediaRecorder { video_channel_receiver: Option>>, should_stop: Arc, start_time: Option, - file_path: Option, + file_path: Option, } impl MediaRecorder { @@ -61,8 +61,9 @@ impl MediaRecorder { pub async fn start_media_recording( &mut self, options: RecordingOptions, - chunks_file_path: &str, - screenshot_file_path: &str, + audio_chunks_dir: &Path, + video_chunks_dir: &Path, + screenshot_folder: &Path, custom_device: Option<&str>, max_screen_width: usize, max_screen_height: usize, @@ -305,7 +306,7 @@ impl MediaRecorder { } let video_start_time_clone = Arc::clone(&video_start_time); - let screenshot_file_path_owned = format!("{screenshot_file_path}/screen-capture.jpg"); + let screenshot_file_path = screenshot_folder.join("screen-capture.jpg"); let capture_frame_at = Duration::from_secs(3); std::thread::spawn(move || { @@ -354,16 +355,14 @@ impl MediaRecorder { if now - start_time >= capture_frame_at && !screenshot_captured { screenshot_captured = true; - let screenshot_file_path_owned_cloned = - screenshot_file_path_owned.clone(); let mut frame_data_clone = frame_data.clone(); + let screenshot_file_path = screenshot_file_path.clone(); std::thread::spawn(move || { for chunk in frame_data_clone.chunks_mut(4) { chunk.swap(0, 2); } - let path = Path::new(&screenshot_file_path_owned_cloned); let image: ImageBuffer, Vec> = ImageBuffer::from_raw( adjusted_width.try_into().unwrap(), @@ -372,29 +371,33 @@ impl MediaRecorder { ) .expect("Failed to create image buffer"); - let mut output_file = std::fs::File::create(&path) - .expect("Failed to create output file"); + let mut output_file = + std::fs::File::create(&screenshot_file_path) + .expect("Failed to create output file"); let mut encoder = JpegEncoder::new_with_quality(&mut output_file, 20); if let Err(e) = encoder.encode_image(&image) { eprintln!("Failed to save screenshot: {}", e); } else { + println!( + "Screenshot captured and saved to {:?}", + screenshot_file_path + ); + if !is_local_mode { let rt = tokio::runtime::Runtime::new().unwrap(); - let screenshot_file_path_owned_cloned_copy = - screenshot_file_path_owned_cloned.clone(); - rt.block_on(async { + rt.block_on(async move { let upload_task = tokio::spawn(upload_file( Some(options_clone), - screenshot_file_path_owned_cloned_copy.clone(), + screenshot_file_path.clone(), upload::FileType::Screenshot, )); match upload_task.await { Ok(result) => match result { Ok(_) => println!( "Screenshot captured and saved to {:?}", - path + screenshot_file_path ), Err(e) => eprintln!( "Failed to upload file: {}", @@ -407,7 +410,6 @@ impl MediaRecorder { } }); } - println!("Screenshot captured and saved to {:?}", path); } }); } @@ -456,8 +458,10 @@ impl MediaRecorder { }); println!("Starting audio recording and processing..."); - let output_chunk_pattern = format!("{chunks_file_path}/recording_%03d.ts"); - let segment_list_filename = format!("{chunks_file_path}/segment_list.txt"); + let audio_chunk_pattern = chunks_file_path.join("audio/recording_%03d.aac"); + let video_chunk_pattern = chunks_file_path.join("video/recording_%03d.ts"); + let audio_segment_list_filename = chunks_file_path.join("audio/segment_list.txt"); + let video_segment_list_filename = chunks_file_path.join("video/segment_list.txt"); let mut audio_filters = Vec::new(); @@ -467,14 +471,14 @@ impl MediaRecorder { audio_filters.push("loudnorm"); - std::fs::create_dir_all(format!("{chunks_file_path}/pipes")).map_err(|e| e.to_string())?; + std::fs::create_dir_all(chunks_file_path.join("pipes")).map_err(|e| e.to_string())?; - let video_pipe_path = format!("{chunks_file_path}/pipes/video.pipe"); + let video_pipe_path = chunks_file_path.join("pipes/video.pipe"); std::fs::remove_file(&video_pipe_path).ok(); create_named_pipe(&video_pipe_path).map_err(|e| e.to_string())?; - let audio_pipe_path = format!("{chunks_file_path}/pipes/audio.pipe"); + let audio_pipe_path = chunks_file_path.join("pipes/audio.pipe"); std::fs::remove_file(&audio_pipe_path).ok(); create_named_pipe(&audio_pipe_path).map_err(|e| e.to_string())?; @@ -487,71 +491,57 @@ impl MediaRecorder { }; let size = format!("{}x{}", adjusted_width, adjusted_height); - let mut ffmpeg_command = flatten_str_args([ - ["-f", "rawvideo"], - ["-pix_fmt", "bgra"], - ["-s", &size], - ["-r", "30"], - ["-thread_queue_size", "4096"], - ["-i", &video_pipe_path], - ]); - - let mut audio_args = flatten_str_args([ - // in - ["-f", sample_format], - ["-ar", &sample_rate_str], - ["-ac", &channels_str], - ["-thread_queue_size", "4096"], - ["-i", &audio_pipe_path], - // out - [ - "-af", - "aresample=async=1:min_hard_comp=0.100000:first_pts=0", - ], - ["-c:a", "aac"], - ["-b:a", "128k"], - ["-async", "1"], - ]); - - match time_offset { - Some((TimeOffsetTarget::Video, args)) => { - ffmpeg_command.splice(0..0, args.clone()).collect() - } - Some((TimeOffsetTarget::Audio, args)) => { - audio_args.splice(0..0, args.clone()).collect() - } - None => { - vec![] - } - }; - if needs_audio { - ffmpeg_command.extend(audio_args); + let mut ffmpeg_command = Command::new(ffmpeg_binary_path_str); + + if let Some((TimeOffsetTarget::Video, args)) = &time_offset { + ffmpeg_command.args(args); } - ffmpeg_command.extend(flatten_str_args([ - ["-vf", "fps=30,scale=in_range=full:out_range=limited"], - ["-c:v", "libx264"], - ["-preset", "ultrafast"], - ["-pix_fmt", "yuv420p"], - ["-tune", "zerolatency"], - ["-vsync", "1"], - ["-force_key_frames", "expr:gte(t,n_forced*3)"], - ["-f", "segment"], - ["-segment_time", "3"], - ["-segment_time_delta", "0.01"], - ["-segment_list", &segment_list_filename], - ["-segment_format", "ts"], - ["-movflags", "frag_keyframe+empty_moov"], - ["-reset_timestamps", "1"], - ])); - - ffmpeg_command.push(output_chunk_pattern); + ffmpeg_command + // video in + .args(["-f", "rawvideo", "-pix_fmt", "bgra"]) + .args(["-s", &size, "-r", "30"]) + .args(["-thread_queue_size", "4096", "-i"]) + .arg(&video_pipe_path) + // video out + .args(["-vf", "fps=30,scale=in_range=full:out_range=limited"]) + .args(["-codec:v", "libx264", "-preset", "ultrafast"]) + .args(["-pix_fmt", "yuv420p", "-tune", "zerolatency"]) + .args(["-vsync", "1", "-force_key_frames", "expr:gte(t,n_forced*3)"]) + .args(["-f", "segment", "-movflags", "frag_keyframe+empty_moov"]) + .args(["-reset_timestamps", "1", "-an"]) + .args(["-segment_time", "3"]) + .args(["-segment_format", "ts"]) + .args(["-segment_time_delta", "0.01", "-segment_list"]) + .args([&video_segment_list_filename, &video_chunk_pattern]); + + if needs_audio { + if let Some((TimeOffsetTarget::Audio, args)) = &time_offset { + ffmpeg_command.args(args); + } + + ffmpeg_command + // audio in + .args(["-f", sample_format, "-ar", &sample_rate_str]) + .args(["-ac", &channels_str, "-thread_queue_size", "4096", "-i"]) + .arg(&audio_pipe_path) + // out + .args([ + "-af", + "aresample=async=1:min_hard_comp=0.100000:first_pts=0", + ]) + .args(["-codec:a", "aac", "-b:a", "128k"]) + .args(["-async", "1", "-f", "segment"]) + .args(["-segment_time", "3", "-segment_time_delta", "0.01"]) + .args(["-reset_timestamps", "1", "-vn", "-segment_list"]) + .args([&audio_segment_list_filename, &audio_chunk_pattern]); + } println!("Starting FFmpeg process..."); let (ffmpeg_child, ffmpeg_stdin) = self - .start_ffmpeg_process(&ffmpeg_binary_path_str, &ffmpeg_command) + .start_ffmpeg_process(ffmpeg_command) .await .map_err(|e| e.to_string())?; println!("Ffmpeg process started"); @@ -604,7 +594,7 @@ impl MediaRecorder { }); self.start_time = Some(Instant::now()); - self.file_path = Some(chunks_file_path.to_string()); + self.file_path = Some(chunks_file_path.to_path_buf()); self.ffmpeg_process = Some(ffmpeg_child); self.device_name = Some(device.name().expect("Failed to get device name")); @@ -630,7 +620,7 @@ impl MediaRecorder { let recording_duration = start_time.elapsed(); let expected_segments = recording_duration.as_secs() / segment_duration.as_secs(); let file_path = self.file_path.as_ref().ok_or("File path not set")?; - let segment_list_filename = format!("{}/segment_list.txt", file_path); + let segment_list_filename = file_path.join("segment_list.txt"); loop { let segments = std::fs::read_to_string(&segment_list_filename).unwrap_or_default(); @@ -683,15 +673,14 @@ impl MediaRecorder { async fn start_ffmpeg_process( &self, - ffmpeg_binary_path: &str, - video_ffmpeg_command: &[String], + cmd: Command, + // ffmpeg_binary_path: &str, + // video_ffmpeg_command: &[String], ) -> Result<(Child, ChildStdin), Error> { - let mut video_process = start_recording_process(ffmpeg_binary_path, video_ffmpeg_command) - .await - .map_err(|e| { - eprintln!("Failed to start video recording process: {}", e); - std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) - })?; + let mut video_process = start_recording_process(cmd).await.map_err(|e| { + eprintln!("Failed to start video recording process: {}", e); + std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) + })?; let video_stdin = video_process.stdin.take().ok_or_else(|| { eprintln!("Failed to take video stdin"); @@ -703,6 +692,7 @@ impl MediaRecorder { } #[tauri::command] +#[specta::specta] pub fn enumerate_audio_devices() -> Vec { let host = cpal::default_host(); let default_device = host @@ -733,14 +723,11 @@ pub fn enumerate_audio_devices() -> Vec { use tokio::io::{AsyncBufReadExt, BufReader}; async fn start_recording_process( - ffmpeg_binary_path_str: &str, - args: &[String], + mut cmd: Command, + // ffmpeg_binary_path_str: &str, + // args: &[String], ) -> Result { - let mut process = Command::new(ffmpeg_binary_path_str) - .args(args) - .stdin(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; + let mut process = cmd.stdin(Stdio::piped()).stderr(Stdio::piped()).spawn()?; if let Some(process_stderr) = process.stderr.take() { tokio::spawn(async move { diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index fe881ed0..f52d02e7 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -21,7 +21,8 @@ pub struct RecordingState { pub media_process: Option, pub recording_options: Option, pub shutdown_flag: Arc, - pub uploading_finished: Arc, + pub video_uploading_finished: Arc, + pub audio_uploading_finished: Arc, pub data_dir: Option, pub max_screen_width: usize, pub max_screen_height: usize, @@ -32,7 +33,7 @@ unsafe impl Sync for RecordingState {} unsafe impl Send for MediaRecorder {} unsafe impl Sync for MediaRecorder {} -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, specta::Type)] pub struct RecordingOptions { pub user_id: String, pub video_id: String, @@ -44,6 +45,7 @@ pub struct RecordingOptions { } #[tauri::command] +#[specta::specta] pub async fn start_dual_recording( state: State<'_, Arc>>, options: RecordingOptions, @@ -62,9 +64,11 @@ pub async fn start_dual_recording( println!("data_dir: {:?}", data_dir); let screenshot_dir = data_dir.join("screenshots"); - let chunks_dir = data_dir.join("chunks"); + let audio_chunks_dir = data_dir.join("chunks/audio"); + let video_chunks_dir = data_dir.join("chunks/video"); - clean_and_create_dir(&chunks_dir)?; + clean_and_create_dir(&audio_chunks_dir)?; + clean_and_create_dir(&video_chunks_dir)?; clean_and_create_dir(&screenshot_dir)?; let audio_name = if options.audio_name.is_empty() { @@ -75,7 +79,8 @@ pub async fn start_dual_recording( let media_recording_preparation = prepare_media_recording( &options, - &chunks_dir, + &audio_chunks_dir, + &video_chunks_dir, &screenshot_dir, audio_name, state_guard.max_screen_width, @@ -88,7 +93,8 @@ pub async fn start_dual_recording( state_guard.media_process = Some(media_recording_result); state_guard.recording_options = Some(options.clone()); state_guard.shutdown_flag = shutdown_flag.clone(); - state_guard.uploading_finished = Arc::new(AtomicBool::new(false)); + state_guard.video_uploading_finished = Arc::new(AtomicBool::new(false)); + state_guard.audio_uploading_finished = Arc::new(AtomicBool::new(false)); let is_local_mode = match dotenv_codegen::dotenv!("NEXT_PUBLIC_LOCAL_MODE") { "true" => true, @@ -96,6 +102,20 @@ pub async fn start_dual_recording( }; if !is_local_mode { + let screen_upload = start_upload_loop( + video_chunks_dir.clone(), + options.clone(), + "video".to_string(), + shutdown_flag.clone(), + state_guard.video_uploading_finished.clone(), + ); + let audio_upload = start_upload_loop( + audio_chunks_dir, + options.clone(), + "audio".to_string(), + shutdown_flag.clone(), + state_guard.audio_uploading_finished.clone(), + ); let upload = start_upload_loop( chunks_dir.clone(), options.clone(), @@ -123,6 +143,7 @@ pub async fn start_dual_recording( } #[tauri::command] +#[specta::specta] pub async fn stop_all_recordings( state: State<'_, Arc>>, ) -> Result<(), String> { @@ -182,6 +203,7 @@ fn clean_and_create_dir(dir: &Path) -> Result<(), String> { async fn start_upload_loop( chunks_dir: PathBuf, options: RecordingOptions, + file_type: upload::FileType, shutdown_flag: Arc, uploading_finished: Arc, ) -> Result<(), String> { @@ -207,11 +229,9 @@ async fn start_upload_loop( let segment_path = chunks_dir.join(segment_filename); if segment_path.is_file() { let options_clone = options.clone(); - let segment_path_clone = segment_path.clone(); upload_tasks.push(tokio::spawn(async move { - let filepath_str = segment_path_clone.to_str().unwrap_or_default().to_owned(); - println!("Uploading video: {}", filepath_str); - upload_file(Some(options_clone), filepath_str, upload::FileType::Segment) + println!("Uploading video for {file_type}: {segment_path:?}"); + upload_file(Some(options_clone), segment_path, file_type) .await .map(|_| ()) })); @@ -247,19 +267,19 @@ fn load_segment_list(segment_list_path: &Path) -> io::Result> { async fn prepare_media_recording( options: &RecordingOptions, screenshot_dir: &Path, + audio_chunks_dir: &Path, video_chunks_dir: &Path, audio_name: Option, max_screen_width: usize, max_screen_height: usize, ) -> Result { let mut media_recorder = MediaRecorder::new(); - let video_file_path = video_chunks_dir.to_str().unwrap(); - let screenshot_dir_path = screenshot_dir.to_str().unwrap(); media_recorder .start_media_recording( options.clone(), - screenshot_dir_path, - video_file_path, + screenshot_dir, + audio_chunks_dir, + video_chunks_dir, audio_name.as_ref().map(String::as_str), max_screen_width, max_screen_height, diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 57089ce8..4909a6b5 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -1,22 +1,25 @@ use regex::Regex; use reqwest; use serde_json::Value as JsonValue; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::str; use crate::recording::RecordingOptions; use crate::utils::ffmpeg_path_as_str; +#[derive(Clone, Copy)] pub enum FileType { - Segment, + Video, + Audio, Screenshot, } impl std::fmt::Display for FileType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - FileType::Segment => write!(f, "segment"), + FileType::Video => write!(f, "video"), + FileType::Audio => write!(f, "audio"), FileType::Screenshot => write!(f, "screenshot"), } } @@ -24,7 +27,7 @@ impl std::fmt::Display for FileType { pub async fn upload_file( options: Option, - file_path: String, + file_path: PathBuf, file_type: FileType, ) -> Result { if let Some(ref options) = options { @@ -41,18 +44,15 @@ pub async fn upload_file( .to_string(); let file_key = format!( - "{}/{}/{}/{}", - options.user_id, - options.video_id, - file_type.to_string(), - file_name + "{}/{}/{file_type}/{file_name}", + options.user_id, options.video_id, ); let server_url_base: &'static str = dotenv_codegen::dotenv!("NEXT_PUBLIC_URL"); let server_url = format!("{}/api/upload/signed", server_url_base); let body = match file_type { - FileType::Segment => { + FileType::Video => { let (codec_name, width, height, frame_rate, bit_rate) = log_video_info(&file_path) .map_err(|e| format!("Failed to log video info: {}", e))?; @@ -68,7 +68,7 @@ pub async fn upload_file( "videoCodec": codec_name, }) } - FileType::Screenshot => { + FileType::Audio | FileType::Screenshot => { serde_json::json!({ "userId": options.user_id, "fileKey": file_key, @@ -110,11 +110,13 @@ pub async fn upload_file( form = form.text(key.to_string(), value_str.to_owned()); } - println!("Uploading file: {}", file_path); + println!("Uploading file: {file_path:?}"); - let mime_type = if file_path.to_lowercase().ends_with(".aac") { + let mime_type = if file_path.extension().unwrap() == ".aac" { "audio/aac" - } else if file_path.to_lowercase().ends_with(".webm") { + } else if file_path.extension().unwrap() == ".mp3" { + "audio/mpeg" + } else if file_path.extension().unwrap() == ".webm" { "audio/webm" } else { "video/mp2t" @@ -162,7 +164,7 @@ pub async fn upload_file( } } - println!("Removing file after upload: {}", file_path); + println!("Removing file after upload: {file_path:?}"); let remove_result = tokio::fs::remove_file(&file_path).await; match &remove_result { Ok(_) => println!("File removed successfully"), @@ -176,7 +178,7 @@ pub async fn upload_file( } } -pub fn get_video_duration(file_path: &str) -> Result { +pub fn get_video_duration(file_path: &Path) -> Result { let ffmpeg_binary_path_str = ffmpeg_path_as_str().unwrap().to_owned(); let output = Command::new(ffmpeg_binary_path_str) @@ -185,19 +187,19 @@ pub fn get_video_duration(file_path: &str) -> Result { .output()?; let output_str = str::from_utf8(&output.stderr).unwrap(); - let duration_regex = Regex::new(r"Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}").unwrap(); + let duration_regex = Regex::new(r"Duration: (\d{2}):(\d{2}):(\d{2})\.(\d{2})").unwrap(); let caps = duration_regex.captures(output_str).unwrap(); let hours: f64 = caps.get(1).unwrap().as_str().parse().unwrap(); let minutes: f64 = caps.get(2).unwrap().as_str().parse().unwrap(); - let seconds: f64 = caps.get(3).unwrap().as_str().parse().unwrap(); - - let duration = hours * 3600.0 + minutes * 60.0 + seconds; + let seconds: f64 = caps.get(3).unwrap().as_str().parse::().unwrap(); + let milliseconds: f64 = caps.get(4).unwrap().as_str().parse::().unwrap() / 100.0; + let duration = hours * 3600.0 + minutes * 60.0 + seconds + milliseconds; Ok(duration) } -fn log_video_info(file_path: &str) -> Result<(String, String, String, String, String), String> { +fn log_video_info(file_path: &Path) -> Result<(String, String, String, String, String), String> { let output: Output = Command::new("ffprobe") .arg("-v") .arg("error") diff --git a/apps/desktop/src-tauri/src/utils.rs b/apps/desktop/src-tauri/src/utils.rs index 00a77071..4413b6ca 100644 --- a/apps/desktop/src-tauri/src/utils.rs +++ b/apps/desktop/src-tauri/src/utils.rs @@ -104,7 +104,7 @@ pub fn ffmpeg_path_as_str() -> Result { } } -pub fn create_named_pipe(path: &str) -> Result<(), nix::Error> { +pub fn create_named_pipe(path: &Path) -> Result<(), nix::Error> { use nix::sys::stat; use nix::unistd; unistd::mkfifo(path, stat::Mode::S_IRWXU)?; From f5bcab2fb828dc491ac4db9db7a0d94bde6fab22 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 17 Jul 2024 01:58:03 +0800 Subject: [PATCH 24/28] go back to uploading audio and video spearately --- apps/desktop/src-tauri/src/media.rs | 4 +- apps/desktop/src-tauri/src/recording.rs | 16 ++---- apps/web/app/api/upload/mux/create/route.ts | 61 +++++++++++++-------- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index baa90ae7..de6837fa 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -61,9 +61,9 @@ impl MediaRecorder { pub async fn start_media_recording( &mut self, options: RecordingOptions, + screenshot_dir: &Path, audio_chunks_dir: &Path, video_chunks_dir: &Path, - screenshot_folder: &Path, custom_device: Option<&str>, max_screen_width: usize, max_screen_height: usize, @@ -306,7 +306,7 @@ impl MediaRecorder { } let video_start_time_clone = Arc::clone(&video_start_time); - let screenshot_file_path = screenshot_folder.join("screen-capture.jpg"); + let screenshot_file_path = screenshot_dir.join("screen-capture.jpg"); let capture_frame_at = Duration::from_secs(3); std::thread::spawn(move || { diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index f52d02e7..414f596f 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -79,9 +79,9 @@ pub async fn start_dual_recording( let media_recording_preparation = prepare_media_recording( &options, + &screenshot_dir, &audio_chunks_dir, &video_chunks_dir, - &screenshot_dir, audio_name, state_guard.max_screen_width, state_guard.max_screen_height, @@ -102,32 +102,26 @@ pub async fn start_dual_recording( }; if !is_local_mode { - let screen_upload = start_upload_loop( + let video_upload = start_upload_loop( video_chunks_dir.clone(), options.clone(), - "video".to_string(), + upload::FileType::Video, shutdown_flag.clone(), state_guard.video_uploading_finished.clone(), ); let audio_upload = start_upload_loop( audio_chunks_dir, options.clone(), - "audio".to_string(), + upload::FileType::Audio, shutdown_flag.clone(), state_guard.audio_uploading_finished.clone(), ); - let upload = start_upload_loop( - chunks_dir.clone(), - options.clone(), - shutdown_flag.clone(), - state_guard.uploading_finished.clone(), - ); drop(state_guard); println!("Starting upload loops..."); - match upload.await { + match tokio::try_join!(video_upload, audio_upload) { Ok(_) => { println!("Both upload loops completed successfully."); } diff --git a/apps/web/app/api/upload/mux/create/route.ts b/apps/web/app/api/upload/mux/create/route.ts index 4de99e9e..bea163b5 100644 --- a/apps/web/app/api/upload/mux/create/route.ts +++ b/apps/web/app/api/upload/mux/create/route.ts @@ -97,7 +97,8 @@ export async function GET(request: NextRequest) { } const bucket = process.env.CAP_AWS_BUCKET || ""; - const segmentsPrefix = `${userId}/${videoId}/segment/`; + const videoPrefix = `${userId}/${videoId}/video/`; + const audioPrefix = `${userId}/${videoId}/audio/`; try { const s3Client = new S3Client({ @@ -108,18 +109,24 @@ export async function GET(request: NextRequest) { }, }); - const segmentsCommand = new ListObjectsV2Command({ + const videoSegmentCommand = new ListObjectsV2Command({ Bucket: bucket, - Prefix: segmentsPrefix, + Prefix: videoPrefix, }); - const segments = await s3Client.send(segmentsCommand); + const audioSegmentCommand = new ListObjectsV2Command({ + Bucket: bucket, + Prefix: audioPrefix, + }); + + const videoSegments = await s3Client.send(videoSegmentCommand); + const audioSegments = await s3Client.send(audioSegmentCommand); - const segmentKeys = (segments.Contents || []).map( + const videoSegmentKeys = (videoSegments.Contents || []).map( (object) => `s3://${bucket}/${object.Key}` ); - if (segmentKeys.length > 149) { + if (videoSegmentKeys.length > 149) { await db .update(videos) .set({ skipProcessing: true }) @@ -141,6 +148,10 @@ export async function GET(request: NextRequest) { ); } + const audioSegmentKeys = (audioSegments.Contents || []).map( + (object) => `s3://${bucket}/${object.Key}` + ); + const mediaConvertClient = new MediaConvertClient({ region: process.env.CAP_AWS_REGION || "", credentials: { @@ -154,14 +165,18 @@ export async function GET(request: NextRequest) { const createJobCommand = new CreateJobCommand({ Role: process.env.CAP_AWS_MEDIACONVERT_ROLE_ARN || "", Settings: { - Inputs: segmentKeys.map((videoSegmentKey) => { + Inputs: videoSegmentKeys.map((videoSegmentKey, index) => { + const audioSegmentKey = audioSegmentKeys[index]; return { FileInput: videoSegmentKey, - AudioSelectors: { - "Audio Selector 1": { - DefaultSelection: "DEFAULT", + ...(audioSegmentKey && { + AudioSelectors: { + "Audio Selector 1": { + DefaultSelection: "DEFAULT", + ExternalAudioFileInput: audioSegmentKey, + }, }, - }, + }), VideoSelector: {}, TimecodeSource: "ZEROBASED", }; @@ -203,19 +218,21 @@ export async function GET(request: NextRequest) { }, }, }, - AudioDescriptions: [ - { - AudioSourceName: "Audio Selector 1", - CodecSettings: { - Codec: "AAC", - AacSettings: { - Bitrate: 128000, - CodingMode: "CODING_MODE_2_0", - SampleRate: 48000, + ...(audioSegmentKeys.length > 0 && { + AudioDescriptions: [ + { + AudioSourceName: "Audio Selector 1", + CodecSettings: { + Codec: "AAC", + AacSettings: { + Bitrate: 128000, + CodingMode: "CODING_MODE_2_0", + SampleRate: 48000, + }, }, }, - }, - ], + ], + }), }, ], }, From 0bc3152a7b999d6964c109fd4d7d0342d6f8a1df Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 17 Jul 2024 02:01:35 +0800 Subject: [PATCH 25/28] use match on extensions --- apps/desktop/src-tauri/src/upload.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 4909a6b5..89ed6beb 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -112,14 +112,11 @@ pub async fn upload_file( println!("Uploading file: {file_path:?}"); - let mime_type = if file_path.extension().unwrap() == ".aac" { - "audio/aac" - } else if file_path.extension().unwrap() == ".mp3" { - "audio/mpeg" - } else if file_path.extension().unwrap() == ".webm" { - "audio/webm" - } else { - "video/mp2t" + let mime_type = match file_path.extension() { + Some(ext) if ext == "aac" => "audio/aac", + Some(ext) if ext == "mp3" => "audio/mpeg", + Some(ext) if ext == "webm" => "audio/webm", + _ => "video/mp2t", }; let file_bytes = tokio::fs::read(&file_path) From cf5bac345b5d990e4f63d493d0459af4eabf9a35 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 17 Jul 2024 02:03:51 +0800 Subject: [PATCH 26/28] wait for audio and video to finish uploading --- apps/desktop/src-tauri/src/recording.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 414f596f..5240ffa6 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -161,7 +161,9 @@ pub async fn stop_all_recordings( }; if !is_local_mode { - while !guard.uploading_finished.load(Ordering::SeqCst) { + while !guard.video_uploading_finished.load(Ordering::SeqCst) + || !guard.audio_uploading_finished.load(Ordering::SeqCst) + { println!("Waiting for uploads to finish..."); tokio::time::sleep(Duration::from_millis(50)).await; } From 61440e783f3307597e4ab87d5a6f6bc3d98c9bf2 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 17 Jul 2024 02:14:22 +0800 Subject: [PATCH 27/28] cleanup --- apps/desktop/src-tauri/src/media.rs | 60 +++++++++++++------------ apps/desktop/src-tauri/src/recording.rs | 2 +- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index de6837fa..f9cc378e 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -37,7 +37,8 @@ pub struct MediaRecorder { video_channel_receiver: Option>>, should_stop: Arc, start_time: Option, - file_path: Option, + audio_file_path: Option, + video_file_path: Option, } impl MediaRecorder { @@ -54,7 +55,8 @@ impl MediaRecorder { video_channel_receiver: None, should_stop: Arc::new(AtomicBool::new(false)), start_time: None, - file_path: None, + audio_file_path: None, + video_file_path: None, } } @@ -458,10 +460,10 @@ impl MediaRecorder { }); println!("Starting audio recording and processing..."); - let audio_chunk_pattern = chunks_file_path.join("audio/recording_%03d.aac"); - let video_chunk_pattern = chunks_file_path.join("video/recording_%03d.ts"); - let audio_segment_list_filename = chunks_file_path.join("audio/segment_list.txt"); - let video_segment_list_filename = chunks_file_path.join("video/segment_list.txt"); + let audio_chunk_pattern = audio_chunks_dir.join("recording_%03d.aac"); + let video_chunk_pattern = video_chunks_dir.join("recording_%03d.ts"); + let audio_segment_list_filename = audio_chunks_dir.join("segment_list.txt"); + let video_segment_list_filename = video_chunks_dir.join("segment_list.txt"); let mut audio_filters = Vec::new(); @@ -471,14 +473,12 @@ impl MediaRecorder { audio_filters.push("loudnorm"); - std::fs::create_dir_all(chunks_file_path.join("pipes")).map_err(|e| e.to_string())?; - - let video_pipe_path = chunks_file_path.join("pipes/video.pipe"); + let video_pipe_path = video_chunks_dir.join("pipe.pipe"); std::fs::remove_file(&video_pipe_path).ok(); create_named_pipe(&video_pipe_path).map_err(|e| e.to_string())?; - let audio_pipe_path = chunks_file_path.join("pipes/audio.pipe"); + let audio_pipe_path = audio_chunks_dir.join("pipe.pipe"); std::fs::remove_file(&audio_pipe_path).ok(); create_named_pipe(&audio_pipe_path).map_err(|e| e.to_string())?; @@ -594,7 +594,8 @@ impl MediaRecorder { }); self.start_time = Some(Instant::now()); - self.file_path = Some(chunks_file_path.to_path_buf()); + self.audio_file_path = Some(audio_chunks_dir.to_path_buf()); + self.video_file_path = Some(video_chunks_dir.to_path_buf()); self.ffmpeg_process = Some(ffmpeg_child); self.device_name = Some(device.name().expect("Failed to get device name")); @@ -619,15 +620,29 @@ impl MediaRecorder { let segment_duration = Duration::from_secs(3); let recording_duration = start_time.elapsed(); let expected_segments = recording_duration.as_secs() / segment_duration.as_secs(); - let file_path = self.file_path.as_ref().ok_or("File path not set")?; - let segment_list_filename = file_path.join("segment_list.txt"); + let audio_file_path = self + .audio_file_path + .as_ref() + .ok_or("Audio file path not set")?; + let video_file_path = self + .video_file_path + .as_ref() + .ok_or("Video file path not set")?; + let audio_segment_list_filename = audio_file_path.join("segment_list.txt"); + let video_segment_list_filename = video_file_path.join("segment_list.txt"); loop { - let segments = std::fs::read_to_string(&segment_list_filename).unwrap_or_default(); + let audio_segments = + std::fs::read_to_string(&audio_segment_list_filename).unwrap_or_default(); + let video_segments = + std::fs::read_to_string(&video_segment_list_filename).unwrap_or_default(); - let segment_count = segments.lines().count(); + let audio_segment_count = audio_segments.lines().count(); + let video_segment_count = video_segments.lines().count(); - if segment_count >= expected_segments as usize { + if audio_segment_count >= expected_segments as usize + && video_segment_count >= expected_segments as usize + { println!("All segments generated"); break; } @@ -671,12 +686,7 @@ impl MediaRecorder { Ok(()) } - async fn start_ffmpeg_process( - &self, - cmd: Command, - // ffmpeg_binary_path: &str, - // video_ffmpeg_command: &[String], - ) -> Result<(Child, ChildStdin), Error> { + async fn start_ffmpeg_process(&self, cmd: Command) -> Result<(Child, ChildStdin), Error> { let mut video_process = start_recording_process(cmd).await.map_err(|e| { eprintln!("Failed to start video recording process: {}", e); std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) @@ -724,8 +734,6 @@ use tokio::io::{AsyncBufReadExt, BufReader}; async fn start_recording_process( mut cmd: Command, - // ffmpeg_binary_path_str: &str, - // args: &[String], ) -> Result { let mut process = cmd.stdin(Stdio::piped()).stderr(Stdio::piped()).spawn()?; @@ -805,7 +813,3 @@ async fn create_time_offset_args( None } } - -fn flatten_str_args<'a>(args: impl IntoIterator) -> Vec { - args.into_iter().flatten().map(|s| s.to_string()).collect() -} diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 5240ffa6..488dcf08 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -67,9 +67,9 @@ pub async fn start_dual_recording( let audio_chunks_dir = data_dir.join("chunks/audio"); let video_chunks_dir = data_dir.join("chunks/video"); + clean_and_create_dir(&screenshot_dir)?; clean_and_create_dir(&audio_chunks_dir)?; clean_and_create_dir(&video_chunks_dir)?; - clean_and_create_dir(&screenshot_dir)?; let audio_name = if options.audio_name.is_empty() { None From 84ba2edf4f341cfb84171149a77f1a3e8028bd12 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 16 Jul 2024 20:00:56 +0100 Subject: [PATCH 28/28] chore: rename video/audio output files --- apps/desktop/src-tauri/src/media.rs | 256 ++++++++++++++-------------- 1 file changed, 124 insertions(+), 132 deletions(-) diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index f9cc378e..0f1ff1ce 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -1,27 +1,24 @@ -use byteorder::{ByteOrder, LittleEndian}; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use byteorder::{ ByteOrder, LittleEndian }; +use cpal::traits::{ DeviceTrait, HostTrait, StreamTrait }; use cpal::SampleFormat; use image::codecs::jpeg::JpegEncoder; -use image::{ImageBuffer, ImageFormat, Rgba}; -use std::io::{Error, ErrorKind::WouldBlock}; -use std::path::{Path, PathBuf}; +use image::{ ImageBuffer, ImageFormat, Rgba }; +use std::io::{ Error, ErrorKind::WouldBlock }; +use std::path::{ Path, PathBuf }; use std::process::Stdio; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; -use std::time::{Duration, Instant}; +use std::sync::{ atomic::{ AtomicBool, Ordering }, Arc }; +use std::time::{ Duration, Instant }; use tokio::fs::File; use tokio::io::AsyncWriteExt; -use tokio::process::{Child, ChildStdin, Command}; -use tokio::sync::{mpsc, Mutex}; +use tokio::process::{ Child, ChildStdin, Command }; +use tokio::sync::{ mpsc, Mutex }; use tokio::try_join; use crate::recording::RecordingOptions; -use crate::upload::{self, upload_file}; -use crate::utils::{create_named_pipe, ffmpeg_path_as_str}; -use capture::{Capturer, Display}; +use crate::upload::{ self, upload_file }; +use crate::utils::{ create_named_pipe, ffmpeg_path_as_str }; +use capture::{ Capturer, Display }; const FRAME_RATE: u64 = 30; @@ -68,7 +65,7 @@ impl MediaRecorder { video_chunks_dir: &Path, custom_device: Option<&str>, max_screen_width: usize, - max_screen_height: usize, + max_screen_height: usize ) -> Result<(), String> { self.options = Some(options.clone()); @@ -128,27 +125,22 @@ impl MediaRecorder { .unwrap_or(false) }) .unwrap_or_else(|| { - host.default_input_device() - .expect("No default input device available") + host.default_input_device().expect("No default input device available") }) } else { - host.default_input_device() - .expect("No default input device available") + host.default_input_device().expect("No default input device available") }; - println!( - "Using audio device: {}", - device.name().expect("Failed to get device name") - ); + println!("Using audio device: {}", device.name().expect("Failed to get device name")); let config = device .supported_input_configs() .expect("Failed to get supported input configs") .find(|c| { - c.sample_format() == SampleFormat::F32 - || c.sample_format() == SampleFormat::I16 - || c.sample_format() == SampleFormat::I8 - || c.sample_format() == SampleFormat::I32 + c.sample_format() == SampleFormat::F32 || + c.sample_format() == SampleFormat::I16 || + c.sample_format() == SampleFormat::I8 || + c.sample_format() == SampleFormat::I32 }) .unwrap_or_else(|| { device @@ -189,17 +181,21 @@ impl MediaRecorder { if needs_audio { println!("Building input stream..."); - let stream_result: Result = - match config.sample_format() { - SampleFormat::I8 => device.build_input_stream( + let stream_result: Result = match + config.sample_format() + { + SampleFormat::I8 => + device.build_input_stream( &config.into(), { let audio_start_time = Arc::clone(&audio_start_time); move |data: &[i8], _: &_| { let mut first_frame_time_guard = audio_start_time.try_lock(); - let bytes = - data.iter().map(|&sample| sample as u8).collect::>(); + let bytes = data + .iter() + .map(|&sample| sample as u8) + .collect::>(); if let Some(sender) = &audio_channel_sender { if sender.try_send(bytes).is_err() { eprintln!("Channel send error. Dropping data."); @@ -216,9 +212,10 @@ impl MediaRecorder { } }, err_fn, - None, + None ), - SampleFormat::I16 => device.build_input_stream( + SampleFormat::I16 => + device.build_input_stream( &config.into(), { let audio_start_time = Arc::clone(&audio_start_time); @@ -243,9 +240,10 @@ impl MediaRecorder { } }, err_fn, - None, + None ), - SampleFormat::I32 => device.build_input_stream( + SampleFormat::I32 => + device.build_input_stream( &config.into(), { let audio_start_time = Arc::clone(&audio_start_time); @@ -270,9 +268,10 @@ impl MediaRecorder { } }, err_fn, - None, + None ), - SampleFormat::F32 => device.build_input_stream( + SampleFormat::F32 => + device.build_input_stream( &config.into(), { let audio_start_time = Arc::clone(&audio_start_time); @@ -297,10 +296,10 @@ impl MediaRecorder { } }, err_fn, - None, + None ), - _sample_format => Err(cpal::BuildStreamError::DeviceNotAvailable), - }; + _sample_format => Err(cpal::BuildStreamError::DeviceNotAvailable), + }; let stream = stream_result.map_err(|_| "Failed to build input stream")?; self.audio_stream = Some(stream); @@ -322,9 +321,8 @@ impl MediaRecorder { let mut capturer = Capturer::new( Display::primary().expect("Failed to find primary display"), w.try_into().unwrap(), - h.try_into().unwrap(), - ) - .expect("Failed to start capture"); + h.try_into().unwrap() + ).expect("Failed to start capture"); let fps = FRAME_RATE; let spf = Duration::from_nanos(1_000_000_000 / fps); @@ -341,13 +339,18 @@ impl MediaRecorder { if now >= time_next { match capturer.frame() { Ok(frame) => { - let mut frame_data = - Vec::with_capacity(capture_size.try_into().unwrap()); + let mut frame_data = Vec::with_capacity( + capture_size.try_into().unwrap() + ); for row in 0..adjusted_height { - let padded_stride = - frame.stride_override().unwrap_or(calculated_stride); - assert!(padded_stride >= calculated_stride, "Image stride with padding should not be smaller than calculated bytes per row"); + let padded_stride = frame + .stride_override() + .unwrap_or(calculated_stride); + assert!( + padded_stride >= calculated_stride, + "Image stride with padding should not be smaller than calculated bytes per row" + ); // Each row should skip the padding of the previous row let start = row * padded_stride; // Each row should stop before/trim off its padding, for compatibility with software that doesn't follow arbitrary padding. @@ -365,19 +368,22 @@ impl MediaRecorder { chunk.swap(0, 2); } - let image: ImageBuffer, Vec> = - ImageBuffer::from_raw( - adjusted_width.try_into().unwrap(), - adjusted_height.try_into().unwrap(), - frame_data_clone, - ) - .expect("Failed to create image buffer"); - - let mut output_file = - std::fs::File::create(&screenshot_file_path) - .expect("Failed to create output file"); - let mut encoder = - JpegEncoder::new_with_quality(&mut output_file, 20); + let image: ImageBuffer< + Rgba, + Vec + > = ImageBuffer::from_raw( + adjusted_width.try_into().unwrap(), + adjusted_height.try_into().unwrap(), + frame_data_clone + ).expect("Failed to create image buffer"); + + let mut output_file = std::fs::File + ::create(&screenshot_file_path) + .expect("Failed to create output file"); + let mut encoder = JpegEncoder::new_with_quality( + &mut output_file, + 20 + ); if let Err(e) = encoder.encode_image(&image) { eprintln!("Failed to save screenshot: {}", e); @@ -390,22 +396,24 @@ impl MediaRecorder { if !is_local_mode { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { - let upload_task = tokio::spawn(upload_file( - Some(options_clone), - screenshot_file_path.clone(), - upload::FileType::Screenshot, - )); + let upload_task = tokio::spawn( + upload_file( + Some(options_clone), + screenshot_file_path.clone(), + upload::FileType::Screenshot + ) + ); match upload_task.await { - Ok(result) => match result { - Ok(_) => println!( - "Screenshot captured and saved to {:?}", - screenshot_file_path - ), - Err(e) => eprintln!( - "Failed to upload file: {}", - e - ), - }, + Ok(result) => + match result { + Ok(_) => + println!( + "Screenshot captured and saved to {:?}", + screenshot_file_path + ), + Err(e) => + eprintln!("Failed to upload file: {}", e), + } Err(e) => { eprintln!("Failed to join task: {}", e) } @@ -455,13 +463,13 @@ impl MediaRecorder { } let elapsed_total_time = start_time.elapsed(); - let fps = frame_count as f64 / elapsed_total_time.as_secs_f64(); + let fps = (frame_count as f64) / elapsed_total_time.as_secs_f64(); println!("Current FPS: {}", fps); }); println!("Starting audio recording and processing..."); - let audio_chunk_pattern = audio_chunks_dir.join("recording_%03d.aac"); - let video_chunk_pattern = video_chunks_dir.join("recording_%03d.ts"); + let audio_chunk_pattern = audio_chunks_dir.join("audio_recording_%03d.aac"); + let video_chunk_pattern = video_chunks_dir.join("video_recording_%03d.ts"); let audio_segment_list_filename = audio_chunks_dir.join("segment_list.txt"); let video_segment_list_filename = video_chunks_dir.join("segment_list.txt"); @@ -506,7 +514,7 @@ impl MediaRecorder { .arg(&video_pipe_path) // video out .args(["-vf", "fps=30,scale=in_range=full:out_range=limited"]) - .args(["-codec:v", "libx264", "-preset", "ultrafast"]) + .args(["-c:v", "libx264", "-preset", "ultrafast"]) .args(["-pix_fmt", "yuv420p", "-tune", "zerolatency"]) .args(["-vsync", "1", "-force_key_frames", "expr:gte(t,n_forced*3)"]) .args(["-f", "segment", "-movflags", "frag_keyframe+empty_moov"]) @@ -527,10 +535,7 @@ impl MediaRecorder { .args(["-ac", &channels_str, "-thread_queue_size", "4096", "-i"]) .arg(&audio_pipe_path) // out - .args([ - "-af", - "aresample=async=1:min_hard_comp=0.100000:first_pts=0", - ]) + .args(["-af", "aresample=async=1:min_hard_comp=0.100000:first_pts=0"]) .args(["-codec:a", "aac", "-b:a", "128k"]) .args(["-async", "1", "-f", "segment"]) .args(["-segment_time", "3", "-segment_time_delta", "0.01"]) @@ -541,8 +546,7 @@ impl MediaRecorder { println!("Starting FFmpeg process..."); let (ffmpeg_child, ffmpeg_stdin) = self - .start_ffmpeg_process(ffmpeg_command) - .await + .start_ffmpeg_process(ffmpeg_command).await .map_err(|e| e.to_string())?; println!("Ffmpeg process started"); @@ -559,17 +563,15 @@ impl MediaRecorder { tokio::spawn(async move { let mut audio_pipe = File::create(audio_pipe_path).await.unwrap(); - while let Some(bytes) = &audio_channel_receiver - .lock() - .await - .as_mut() - .unwrap() - .recv() - .await + while + let Some(bytes) = &audio_channel_receiver + .lock().await + .as_mut() + .unwrap() + .recv().await { audio_pipe - .write_all(&bytes) - .await + .write_all(&bytes).await .expect("Failed to write audio data to FFmpeg stdin"); } }); @@ -579,17 +581,14 @@ impl MediaRecorder { tokio::spawn(async move { let mut pipe = File::create(video_pipe_path).await.unwrap(); - while let Some(bytes) = &video_channel_receiver - .lock() - .await - .as_mut() - .unwrap() - .recv() - .await + while + let Some(bytes) = &video_channel_receiver + .lock().await + .as_mut() + .unwrap() + .recv().await { - pipe.write_all(&bytes) - .await - .expect("Failed to write video data to FFmpeg stdin"); + pipe.write_all(&bytes).await.expect("Failed to write video data to FFmpeg stdin"); } }); @@ -620,28 +619,25 @@ impl MediaRecorder { let segment_duration = Duration::from_secs(3); let recording_duration = start_time.elapsed(); let expected_segments = recording_duration.as_secs() / segment_duration.as_secs(); - let audio_file_path = self - .audio_file_path - .as_ref() - .ok_or("Audio file path not set")?; - let video_file_path = self - .video_file_path - .as_ref() - .ok_or("Video file path not set")?; + let audio_file_path = self.audio_file_path.as_ref().ok_or("Audio file path not set")?; + let video_file_path = self.video_file_path.as_ref().ok_or("Video file path not set")?; let audio_segment_list_filename = audio_file_path.join("segment_list.txt"); let video_segment_list_filename = video_file_path.join("segment_list.txt"); loop { - let audio_segments = - std::fs::read_to_string(&audio_segment_list_filename).unwrap_or_default(); - let video_segments = - std::fs::read_to_string(&video_segment_list_filename).unwrap_or_default(); + let audio_segments = std::fs + ::read_to_string(&audio_segment_list_filename) + .unwrap_or_default(); + let video_segments = std::fs + ::read_to_string(&video_segment_list_filename) + .unwrap_or_default(); let audio_segment_count = audio_segments.lines().count(); let video_segment_count = video_segments.lines().count(); - if audio_segment_count >= expected_segments as usize - && video_segment_count >= expected_segments as usize + if + audio_segment_count >= (expected_segments as usize) && + video_segment_count >= (expected_segments as usize) { println!("All segments generated"); break; @@ -705,12 +701,8 @@ impl MediaRecorder { #[specta::specta] pub fn enumerate_audio_devices() -> Vec { let host = cpal::default_host(); - let default_device = host - .default_input_device() - .expect("No default input device available"); - let default_device_name = default_device - .name() - .expect("Failed to get default device name"); + let default_device = host.default_input_device().expect("No default input device available"); + let default_device_name = default_device.name().expect("Failed to get default device name"); let devices = host.devices().expect("Failed to get devices"); let mut input_device_names: Vec = devices @@ -730,10 +722,10 @@ pub fn enumerate_audio_devices() -> Vec { input_device_names } -use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::io::{ AsyncBufReadExt, BufReader }; async fn start_recording_process( - mut cmd: Command, + mut cmd: Command ) -> Result { let mut process = cmd.stdin(Stdio::piped()).stderr(Stdio::piped()).spawn()?; @@ -751,7 +743,7 @@ async fn start_recording_process( async fn wait_for_start_times( audio_start_time: &Mutex>, - video_start_time: &Mutex>, + video_start_time: &Mutex> ) -> (Instant, Instant) { loop { let audio_start_locked = audio_start_time.lock().await; @@ -775,7 +767,7 @@ pub enum TimeOffsetTarget { async fn create_time_offset_args( audio_start_time: &Mutex>, - video_start_time: &Mutex>, + video_start_time: &Mutex> ) -> Option<(TimeOffsetTarget, Vec)> { let (audio_start, video_start) = wait_for_start_times(audio_start_time, video_start_time).await; let duration_difference = if audio_start > video_start { @@ -790,7 +782,7 @@ async fn create_time_offset_args( // Convert the duration difference to a float representing seconds let offset_seconds = - duration_difference.as_secs() as f64 + duration_difference.subsec_nanos() as f64 * 1e-9; + (duration_difference.as_secs() as f64) + (duration_difference.subsec_nanos() as f64) * 1e-9; // Depending on which started first, adjust the relevant FFmpeg command if audio_start > video_start {