diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68a046d5f..61d8475ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,10 +39,11 @@ Configure the necessary environment variables by copying the `.env.example` file `.env.example` by default assumes you want to run both `@cap/desktop` and `@cap/web` locally. Follow the instructions in the file for how to configure the environment variables for which apps you want to run. -Run `pnpm cap-setup` to install native dependencies such as FFmpeg, -then run `pnpm install`. +Run `pnpm install`, +then run `pnpm cap-setup` to install native dependencies such as FFmpeg. On Windows, llvm, clang, and VCPKG must be installed. +On MacOS, cmake must be installed. `pnpm cap-setup` does not yet install these dependencies for you. To run both `@cap/desktop` and `@cap/web` together, use `pnpm dev`. diff --git a/Cargo.lock b/Cargo.lock index 6d62f79d8..6da168520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,18 +678,18 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bit_field" @@ -895,16 +895,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "camera-mediafoundation" -version = "0.1.0" -dependencies = [ - "inquire", - "tracing", - "windows 0.60.0", - "windows-core 0.60.1", -] - [[package]] name = "camino" version = "1.1.9" @@ -971,6 +961,29 @@ dependencies = [ "windows-core 0.60.1", ] +[[package]] +name = "cap-camera-mediafoundation" +version = "0.1.0" +dependencies = [ + "inquire", + "tracing", + "windows 0.60.0", + "windows-core 0.60.1", +] + +[[package]] +name = "cap-camera-windows" +version = "0.1.0" +dependencies = [ + "cap-camera-directshow", + "cap-camera-mediafoundation", + "ffmpeg-next", + "inquire", + "thiserror 1.0.69", + "windows 0.60.0", + "windows-core 0.60.1", +] + [[package]] name = "cap-cursor-capture" version = "0.1.0" @@ -982,7 +995,7 @@ dependencies = [ [[package]] name = "cap-desktop" -version = "0.3.58" +version = "0.3.60" dependencies = [ "anyhow", "axum", @@ -1163,7 +1176,7 @@ dependencies = [ "futures", "gif", "image 0.25.5", - "indexmap 2.5.0", + "indexmap 2.10.0", "nokhwa", "nokhwa-bindings-macos", "num-traits", @@ -1403,12 +1416,6 @@ 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 = "cfg_aliases" version = "0.2.1" @@ -1441,14 +1448,11 @@ dependencies = [ [[package]] name = "cidre" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b83f5e597a8fb4ba25eec2683aa3e89f9890dd7524622639a38e3f854c60eb" +version = "0.10.1" +source = "git+https://github.com/CapSoftware/cidre?rev=517d097ae438#517d097ae4387ebb97f36c6e133b3c94dca78e25" dependencies = [ "cidre-macros", - "half", "parking_lot", - "tokio", ] [[package]] @@ -1640,10 +1644,11 @@ dependencies = [ [[package]] name = "codespan-reporting" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ + "serde", "termcolor", "unicode-width", ] @@ -1660,37 +1665,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "com" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" -dependencies = [ - "com_macros", -] - -[[package]] -name = "com_macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" -dependencies = [ - "com_macros_support", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "com_macros_support" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "combine" version = "4.6.7" @@ -1989,18 +1963,18 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.12.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" +checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ "bitflags 2.9.0", "fontdb", "log", "rangemap", - "rayon", "rustc-hash 1.1.0", "rustybuzz", "self_cell", + "smol_str", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -2211,17 +2185,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "d3d12" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbd1f579714e3c809ebd822c81ef148b1ceaeb3d535352afc73fd0c4c6a0017" -dependencies = [ - "bitflags 2.9.0", - "libloading 0.8.5", - "winapi", -] - [[package]] name = "darling" version = "0.20.10" @@ -2456,9 +2419,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" dependencies = [ "litrs", ] @@ -2819,11 +2782,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "font-types" -version = "0.7.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" +checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7" dependencies = [ "bytemuck", ] @@ -3357,9 +3326,9 @@ dependencies = [ [[package]] name = "glow" -version = "0.13.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" dependencies = [ "js-sys", "slotmap", @@ -3378,9 +3347,9 @@ dependencies = [ [[package]] name = "glyphon" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11b1afb04c1a1be055989042258499473d0a9447f16450b433aba10bc2a46e7" +checksum = "5c6a289ad2a23656ccf4306fc818cef6776471a136d909123fd26c0f2ecb44ba" dependencies = [ "cosmic-text", "etagere", @@ -3421,15 +3390,14 @@ dependencies = [ [[package]] name = "gpu-allocator" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", "thiserror 1.0.69", - "winapi", - "windows 0.52.0", + "windows 0.58.0", ] [[package]] @@ -3522,7 +3490,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.5.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -3537,6 +3505,7 @@ checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if 1.0.0", "crunchy", + "num-traits", ] [[package]] @@ -3556,18 +3525,12 @@ dependencies = [ ] [[package]] -name = "hassle-rs" -version = "0.11.0" +name = "hashbrown" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ - "bitflags 2.9.0", - "com", - "libc", - "libloading 0.8.5", - "thiserror 1.0.69", - "widestring", - "winapi", + "foldhash", ] [[package]] @@ -3921,12 +3884,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.4", "serde", ] @@ -4170,7 +4133,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.10.0", ] [[package]] @@ -4564,9 +4527,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ "bitflags 2.9.0", "block", @@ -4710,23 +4673,27 @@ dependencies = [ [[package]] name = "naga" -version = "22.1.0" +version = "25.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" +checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", "bitflags 2.9.0", - "cfg_aliases 0.1.1", + "cfg_aliases", "codespan-reporting", + "half", + "hashbrown 0.15.4", "hexf-parse", - "indexmap 2.5.0", + "indexmap 2.10.0", "log", + "num-traits", + "once_cell", "rustc-hash 1.1.0", "spirv", - "termcolor", - "thiserror 1.0.69", - "unicode-xid", + "strum", + "thiserror 2.0.12", + "unicode-ident", ] [[package]] @@ -4851,7 +4818,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.9.0", "cfg-if 1.0.0", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] @@ -5038,6 +5005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -5418,9 +5386,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "open" @@ -5489,6 +5457,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -5846,7 +5823,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.5.0", + "indexmap 2.10.0", "quick-xml 0.32.0", "serde", "time", @@ -6330,9 +6307,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.22.7" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f" +checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d" dependencies = [ "bytemuck", "font-types", @@ -6706,7 +6683,7 @@ dependencies = [ [[package]] name = "scap" version = "0.0.8" -source = "git+https://github.com/CapSoftware/scap?rev=d69dcb2e653a#d69dcb2e653a714bf91d147333deb5fb06acb940" +source = "git+https://github.com/CapSoftware/scap?rev=4d340576772c#4d340576772c4ee53352962b40102cc1de3444d6" dependencies = [ "cidre", "cocoa 0.25.0", @@ -7079,7 +7056,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.5.0", + "indexmap 2.10.0", "serde", "serde_derive", "serde_json", @@ -7265,9 +7242,9 @@ dependencies = [ [[package]] name = "skrifa" -version = "0.22.3" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" +checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607" dependencies = [ "bytemuck", "read-fonts", @@ -7297,6 +7274,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" + [[package]] name = "socket2" version = "0.5.7" @@ -7314,7 +7297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" dependencies = [ "bytemuck", - "cfg_aliases 0.2.1", + "cfg_aliases", "core-graphics 0.23.2", "foreign-types 0.5.0", "js-sys", @@ -7470,6 +7453,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.90", +] + [[package]] name = "subtle" version = "2.6.1" @@ -7484,9 +7489,9 @@ checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" [[package]] name = "swash" -version = "0.1.19" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" +checksum = "f745de914febc7c9ab4388dfaf94bbc87e69f57bb41133a9b0c84d4be49856f3" dependencies = [ "skrifa", "yazi", @@ -8591,7 +8596,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.10.0", "toml_datetime", "winnow 0.5.40", ] @@ -8602,7 +8607,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.10.0", "toml_datetime", "winnow 0.5.40", ] @@ -8613,7 +8618,7 @@ version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", @@ -8946,12 +8951,6 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" @@ -9389,17 +9388,20 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wgpu" -version = "22.1.0" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" +checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "cfg_aliases 0.1.1", + "bitflags 2.9.0", + "cfg_aliases", "document-features", + "hashbrown 0.15.4", "js-sys", "log", "naga", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "smallvec", @@ -9414,34 +9416,67 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "22.1.0" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" +checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500" dependencies = [ "arrayvec", + "bit-set", "bit-vec", "bitflags 2.9.0", - "cfg_aliases 0.1.1", + "cfg_aliases", "document-features", - "indexmap 2.5.0", + "hashbrown 0.15.4", + "indexmap 2.10.0", "log", "naga", "once_cell", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.12", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-types", ] +[[package]] +name = "wgpu-core-deps-apple" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd488b3239b6b7b185c3b045c39ca6bf8af34467a4c5de4e0b1a564135d093d" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09ad7aceb3818e52539acc679f049d3475775586f3f4e311c30165cf2c00445" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba5fb5f7f9c98baa7c889d444f63ace25574833df56f5b817985f641af58e46" +dependencies = [ + "wgpu-hal", +] + [[package]] name = "wgpu-hal" -version = "22.0.0" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" +checksum = "f968767fe4d3d33747bbd1473ccd55bf0f6451f55d733b5597e67b5deab4ad17" dependencies = [ "android_system_properties", "arrayvec", @@ -9449,47 +9484,52 @@ dependencies = [ "bit-set", "bitflags 2.9.0", "block", - "cfg_aliases 0.1.1", + "bytemuck", + "cfg-if 1.0.0", + "cfg_aliases", "core-graphics-types 0.1.3", - "d3d12", "glow", "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hassle-rs", + "hashbrown 0.15.4", "js-sys", "khronos-egl", "libc", "libloading 0.8.5", "log", - "metal 0.29.0", + "metal 0.31.0", "naga", "ndk-sys 0.5.0+25.2.9519653", "objc", - "once_cell", + "ordered-float", "parking_lot", + "portable-atomic", "profiling", "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash 1.1.0", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.12", "wasm-bindgen", "web-sys", "wgpu-types", - "winapi", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] name = "wgpu-types" -version = "22.0.0" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" +checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ "bitflags 2.9.0", + "bytemuck", "js-sys", + "log", + "thiserror 2.0.12", "web-sys", ] @@ -9535,12 +9575,6 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "widestring" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" - [[package]] name = "winapi" version = "0.3.9" @@ -10479,9 +10513,9 @@ dependencies = [ [[package]] name = "yazi" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" [[package]] name = "zbus" @@ -10611,9 +10645,9 @@ dependencies = [ [[package]] name = "zeno" -version = "0.2.3" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" @@ -10652,7 +10686,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap 2.5.0", + "indexmap 2.10.0", "memchr", "thiserror 1.0.69", ] diff --git a/Cargo.toml b/Cargo.toml index 4770d46ea..7386f16e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,13 +25,13 @@ specta = { version = "=2.0.0-rc.20", features = [ "uuid", ] } -scap = { git = "https://github.com/CapSoftware/scap", rev = "d69dcb2e653a" } +scap = { git = "https://github.com/CapSoftware/scap", rev = "4d340576772c" } nokhwa = { git = "https://github.com/CapSoftware/nokhwa", rev = "b9c8079e82e2", features = [ "input-native", "serialize", ] } nokhwa-bindings-macos = { git = "https://github.com/CapSoftware/nokhwa", rev = "b9c8079e82e2" } -wgpu = "22.1.0" +wgpu = "25.0.2" flume = "0.11.0" thiserror = "1.0" sentry = { version = "0.34.0", features = [ @@ -41,7 +41,16 @@ sentry = { version = "0.34.0", features = [ ] } tracing = "0.1.41" -cidre = "0.9.2" +cidre = { git = "https://github.com/CapSoftware/cidre", rev = "517d097ae438", features = [ + "macos_13_0", + "cv", + "cf", + "sc", + "av", + "blocks", + "async", + "dispatch", +], default-features = false } windows = "0.58.0" windows-sys = "0.59.0" @@ -54,3 +63,4 @@ dbg_macro = "deny" [patch.crates-io] screencapturekit = { git = "https://github.com/CapSoftware/screencapturekit-rs", rev = "7ff1e103742e56c8f6c2e940b5e52684ed0bed69" } # branch = "cap-main" wry = { git = "https://github.com/CapSoftware/wry", rev = "293f510" } # branch = "cap" +cidre = { git = "https://github.com/CapSoftware/cidre", rev = "517d097ae438" } diff --git a/apps/cli/src/record.rs b/apps/cli/src/record.rs index 668fb14f9..9a1efb387 100644 --- a/apps/cli/src/record.rs +++ b/apps/cli/src/record.rs @@ -1,7 +1,6 @@ use std::{env::current_dir, path::PathBuf, sync::Arc}; use cap_media::{feeds::CameraFeed, sources::ScreenCaptureTarget}; -use cap_recording::{RecordingMode, RecordingOptions}; use clap::Args; use nokhwa::utils::{ApiBackend, CameraIndex}; use tokio::{io::AsyncBufReadExt, sync::Mutex}; diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index b6ec1383c..8b52e94aa 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cap-desktop" -version = "0.3.58" +version = "0.3.60" description = "Beautiful screen recordings, owned by you." authors = ["you"] edition = "2021" diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 0a169ba2e..2738e204f 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -51,7 +51,6 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use specta::Type; use std::collections::BTreeMap; -use std::time::Duration; use std::{ fs::File, future::Future, diff --git a/apps/desktop/src/routes/editor/Timeline/Track.tsx b/apps/desktop/src/routes/editor/Timeline/Track.tsx index 28d6dbb69..fa73d59b1 100644 --- a/apps/desktop/src/routes/editor/Timeline/Track.tsx +++ b/apps/desktop/src/routes/editor/Timeline/Track.tsx @@ -88,11 +88,13 @@ export function SegmentRoot( } export function SegmentContent(props: ComponentProps<"div">) { + const ctx = useSegmentContext(); return (
@@ -109,7 +111,7 @@ export function SegmentHandle(
{ + try { + await deleteVideo(capId); + toast.success("Cap deleted successfully"); + refresh(); + } catch (error) { + toast.error("Failed to delete cap"); + } + }; + if (count === 0) { return ; } @@ -302,7 +312,13 @@ export const Caps = ({ key={cap.id} cap={cap} analytics={analytics[cap.id] || 0} - onDelete={deleteSelectedCaps} + onDelete={async () => { + if (selectedCaps.length > 0) { + await deleteSelectedCaps(); + } else { + await deleteCap(cap.id); + } + }} userId={user?.id} customDomain={customDomain} domainVerified={domainVerified} diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx index 6c40ef6ed..87c2ec337 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -108,10 +108,15 @@ export const CapCard = ({ const confirmRemoveCap = async () => { if (!onDelete) return; - setRemoving(true); - await onDelete(); - setRemoving(false); - setConfirmOpen(false); + try { + setRemoving(true); + await onDelete(); + } catch (error) { + console.error("Error deleting cap:", error); + } finally { + setRemoving(false); + setConfirmOpen(false); + } }; const handleSharingUpdated = () => { @@ -345,7 +350,7 @@ export const CapCard = ({ + />f

Duplicate

- Google + Google Login with Google diff --git a/apps/web/app/(site)/blog/[slug]/page.tsx b/apps/web/app/(site)/blog/[slug]/page.tsx index 2500d896d..e6ce2ae53 100644 --- a/apps/web/app/(site)/blog/[slug]/page.tsx +++ b/apps/web/app/(site)/blog/[slug]/page.tsx @@ -71,7 +71,7 @@ export default async function PostPage({ params }: PostProps) { return ( <> -
+
{post.metadata.image && (
doc.slug === category); return ( -
+

{displayCategory} Documentation

{/* Show root category content if it exists */} {rootDoc && ( @@ -175,7 +175,7 @@ export default async function DocPage(props: DocProps) { } return ( -
+
{doc.metadata.image && (
+
{doc.metadata.image && (
{ } return ( -
+
{doc.metadata.image && (
{
-
+
{features.map((feature, index) => { const sizeClasses = { @@ -411,32 +409,31 @@ export const FeaturesPage = () => {
-

+

{feature.title} {feature.isPro && ( Cap Pro )} {feature.isComingSoon && ( - + SOON )}

-

+

{feature.description}

-
+
@@ -445,14 +442,14 @@ export const FeaturesPage = () => {
-
-
-

Ready to get started?

-

+

+
+

Ready to get started?

+

Join thousands of users who are already creating better recordings with Cap.

-
+
-
- )} +
+ +
- - {!isLoading && showTitleOverlay && ( + + {!isPlaying && ( +
-
-
- {ownerName && ( - - )} -
- e.stopPropagation()} - > -

- {data.name} -

-
-
- {ownerName && ( -

- {ownerName} -

- )} - {ownerName && longestDuration > 0 && ( + exit={{ opacity: 0, y: 10 }} + transition={{ duration: 0.3, delay: 0.2 }} + className="z-10 bg-black/50 backdrop-blur-md rounded-lg sm:rounded-xl px-2 py-1.5 sm:px-4 sm:py-3 border border-white/10 shadow-2xl"> +
+ {ownerName && ( + + )} +
+ e.stopPropagation()} + > +

+ {data.name} +

+
+
+ {ownerName && ( +

+ {ownerName} +

+ )} + {ownerName && longestDuration > 0 && ( + <> - )} - {longestDuration > 0 && (

{formatTime(longestDuration)}

- )} -
+ + )}
- )} - - - {currentSubtitle && currentSubtitle.text && subtitlesVisible && ( -
-
- {currentSubtitle.text - .replace("- ", "") - .replace(".", "") - .replace(",", "")} -
-
- )} - - - {isPlaying && !showTitleOverlay && !hideBranding && ( - - - - )} - -
- -
-
{ - const touch = e.touches[0]; - const rect = timelineRef.current?.getBoundingClientRect(); - if (rect && touch) { - const percentage = Math.max( - 0, - Math.min(1, (touch.clientX - rect.left) / rect.width) - ); - const seekTime = percentage * longestDuration; - if (videoRef.current) { - videoRef.current.currentTime = seekTime; - setCurrentTime(seekTime); - } - } - setIsDragging(true); - }} - onTouchMove={(e) => { - if (isDragging) { - const touch = e.touches[0]; - const rect = timelineRef.current?.getBoundingClientRect(); - if (rect && touch) { - const percentage = Math.max( - 0, - Math.min(1, (touch.clientX - rect.left) / rect.width) - ); - const seekTime = percentage * longestDuration; - if (videoRef.current) { - videoRef.current.currentTime = seekTime; - setCurrentTime(seekTime); - } - } - } - }} - onTouchEnd={() => { - setIsDragging(false); - }} - > - {!isLoading && comments && comments.length > 0 && ( -
- {comments.map((comment) => { - const commentPosition = - comment.timestamp === null - ? 0 - : (comment.timestamp / longestDuration) * 100; - - let tooltipContent = ""; - if (comment.type === "text") { - tooltipContent = - comment.authorId === "anonymous" - ? `Anonymous: ${comment.content}` - : `${comment.authorName || "User"}: ${comment.content}`; - } else { - tooltipContent = - comment.authorId === "anonymous" - ? "Anonymous" - : comment.authorName || "User"; - } - - return ( -
- - {comment.type === "text" ? ( - - ) : ( - comment.content - )} - - -
- ); - })} -
- )} - -
- {chapters.length > 0 && longestDuration > 0 ? ( -
- {chapters.map((chapter, index) => { - const nextChapter = chapters[index + 1]; - const chapterStart = chapter.start; - const chapterEnd = nextChapter - ? nextChapter.start - : longestDuration; - const chapterDuration = chapterEnd - chapterStart; - const chapterWidth = - (chapterDuration / longestDuration) * 100; - - const isCurrentChapter = - currentTime >= chapterStart && currentTime < chapterEnd; - - return ( -
{ - e.stopPropagation(); - handleChapterSeek(chapterStart); - }} - > -
- {isCurrentChapter && ( -
- )} -
- ); - })} -
- ) : ( - <> -
-
- - )} - -
-
-
-
- -
{ - setIsHoveringControls(true); - }} - onMouseLeave={() => { - setIsHoveringControls(false); - }} - > -
-
- -
- {formatTime(currentTime)} / {formatTime(longestDuration)} -
-
- -
- - - {isTranscriptionProcessing && subtitles.length === 0 && ( - - )} - - {aiProcessing && ( - - )} - - {subtitles.length > 0 && ( - - )} - - - - -
-
-
- - {user && - !isUserOnProPlan({ - subscriptionStatus: user.stripeSubscriptionStatus, - }) && ( -
-
{ e.stopPropagation(); - setUpgradeModalOpen(true); + window.open("https://cap.so", "_blank"); }} + className="hidden z-10 gap-2 items-center px-3 py-2 text-sm rounded-full border backdrop-blur-sm transition-colors duration-200 sm:flex border-white/10 w-fit text-white/80 hover:text-white bg-black/50" + aria-label="Powered by Cap" > -
-
- -
- -
-

- Remove watermark -

-
-
-
+ Powered by + +
)} - - -
+ + ); } ); -const useVideoSourceValidation = ( - data: typeof videos.$inferSelect, - videoMetadataLoaded: boolean -) => { - return useQuery({ - queryKey: ["video-source-validation", data.id, data.source.type], - queryFn: async () => { - if (data.source.type !== "desktopMP4") { - return { isMP4Source: false }; - } - - const thumbUrl = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&thumbnailTime=0`; - - try { - const response = await fetch(thumbUrl, { method: "HEAD" }); - return { isMP4Source: response.ok }; - } catch (error) { - console.error("Error checking thumbnails:", error); - return { isMP4Source: false }; - } - }, - enabled: videoMetadataLoaded && data.source.type === "desktopMP4", - staleTime: 5 * 60 * 1000, - gcTime: 10 * 60 * 1000, - }); -}; - -const useScreenSize = () => { - const [isLargeScreen, setIsLargeScreen] = useState(false); - - useEffect(() => { - const checkScreenSize = () => { - setIsLargeScreen(window.innerWidth >= 1024); - }; - - checkScreenSize(); - window.addEventListener("resize", checkScreenSize); - - return () => window.removeEventListener("resize", checkScreenSize); - }, []); - - return isLargeScreen; -}; - -const useTranscriptionProcessing = ( - data: typeof videos.$inferSelect, - transcriptContent: string | undefined, - transcriptError: any -) => { - const [isTranscriptionProcessing, setIsTranscriptionProcessing] = useState( - data.transcriptionStatus === "PROCESSING" - ); - const [subtitles, setSubtitles] = useState([]); - - useEffect(() => { - if (transcriptContent) { - const parsedSubtitles = fromVtt(transcriptContent); - setSubtitles(parsedSubtitles); - setIsTranscriptionProcessing(false); - } else if (transcriptError) { - console.error( - "[EmbedVideo] Subtitle error from React Query:", - transcriptError.message - ); - if (transcriptError.message === "TRANSCRIPT_NOT_READY") { - setIsTranscriptionProcessing(true); - } else { - setIsTranscriptionProcessing(false); - } - } else if (data.transcriptionStatus === "PROCESSING") { - setIsTranscriptionProcessing(true); - } else if (data.transcriptionStatus === "ERROR") { - setIsTranscriptionProcessing(false); - } - }, [transcriptContent, transcriptError, data.transcriptionStatus]); - - return { isTranscriptionProcessing, subtitles }; -}; diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index a90f6a768..b89e108a9 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -710,6 +710,33 @@ footer a { animation: slideUp 0.3s ease-out forwards; } +.media-player video::-webkit-media-text-track-display { + @apply !bottom-12 !top-auto !mb-0; +} + +.media-player[data-controls-visible] video::-webkit-media-text-track-display { + @apply !bottom-20; +} + +.media-player[data-state="fullscreen"][data-controls-visible] + video::-webkit-media-text-track-display { + @apply !bottom-20; +} + +/* Firefox support */ +.media-player video::cue { + position: relative !important; + bottom: 64px !important; +} + +.media-player[data-controls-visible] video::cue { + bottom: 64px !important; +} + +.media-player[data-state="fullscreen"][data-controls-visible] video::cue { + bottom: 64px !important; +} + /* Safari-specific styles using CSS hacks */ @media screen and (-webkit-min-device-pixel-ratio: 0) { _::-webkit-full-page-media, @@ -718,3 +745,68 @@ footer a { height: calc(100% - 1.7rem) !important; } } + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + --primary: 220.9 39.3% 11%; + --primary-foreground: 210 20% 98%; + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + --border: 220 13% 91%; + --input: 220 13% 91%; + --ring: 224 71.4% 4.1%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + --primary: 210 20% 98%; + --primary-foreground: 220.9 39.3% 11%; + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + --muted: 215 27.9% 16.9%; + --muted-foreground: 217.9 10.6% 64.9%; + --accent: 215 27.9% 16.9%; + --accent-foreground: 210 20% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 20% 98%; + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 216 12.2% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/web/app/lib/compose-refs.ts b/apps/web/app/lib/compose-refs.ts new file mode 100644 index 000000000..ef3c01ee9 --- /dev/null +++ b/apps/web/app/lib/compose-refs.ts @@ -0,0 +1,62 @@ +import * as React from "react"; + +type PossibleRef = React.Ref | undefined; + +/** + * Set a given ref to a given value + * This utility takes care of different types of refs: callback refs and RefObject(s) + */ +function setRef(ref: PossibleRef, value: T) { + if (typeof ref === "function") { + return ref(value); + } + + if (ref !== null && ref !== undefined) { + ref.current = value; + } +} + +/** + * A utility to compose multiple refs together + * Accepts callback refs and RefObject(s) + */ +function composeRefs(...refs: PossibleRef[]): React.RefCallback { + return (node) => { + let hasCleanup = false; + const cleanups = refs.map((ref) => { + const cleanup = setRef(ref, node); + if (!hasCleanup && typeof cleanup === "function") { + hasCleanup = true; + } + return cleanup; + }); + + // React <19 will log an error to the console if a callback ref returns a + // value. We don't use ref cleanups internally so this will only happen if a + // user's ref callback returns a value, which we only expect if they are + // using the cleanup functionality added in React 19. + if (hasCleanup) { + return () => { + for (let i = 0; i < cleanups.length; i++) { + const cleanup = cleanups[i]; + if (typeof cleanup === "function") { + cleanup(); + } else { + setRef(refs[i], null); + } + } + }; + } + }; +} + +/** + * A custom hook that composes multiple refs + * Accepts callback refs and RefObject(s) + */ +function useComposedRefs(...refs: PossibleRef[]): React.RefCallback { + // eslint-disable-next-line react-hooks/exhaustive-deps + return React.useCallback(composeRefs(...refs), refs); +} + +export { composeRefs, useComposedRefs }; diff --git a/apps/web/app/lib/utils.ts b/apps/web/app/lib/utils.ts new file mode 100644 index 000000000..bd0c391dd --- /dev/null +++ b/apps/web/app/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 7032a7d76..83e1d8cae 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -6,7 +6,6 @@ import { userSelectProps } from "@cap/database/auth/session"; import { comments as commentsSchema, videos } from "@cap/database/schema"; import { useQuery } from "@tanstack/react-query"; import { useMemo, useRef } from "react"; -import { ShareHeader } from "./_components/ShareHeader"; import { ShareVideo } from "./_components/ShareVideo"; import { Sidebar } from "./_components/Sidebar"; import { Toolbar } from "./_components/Toolbar"; @@ -67,17 +66,17 @@ const useVideoStatus = ( }, initialData: initialData ? { - transcriptionStatus: initialData.transcriptionStatus as - | "PROCESSING" - | "COMPLETE" - | "ERROR" - | null, - aiProcessing: initialData.aiData?.processing || false, - aiTitle: initialData.aiData?.title || null, - summary: initialData.aiData?.summary || null, - chapters: initialData.aiData?.chapters || null, - generationError: null, - } + transcriptionStatus: initialData.transcriptionStatus as + | "PROCESSING" + | "COMPLETE" + | "ERROR" + | null, + aiProcessing: initialData.aiData?.processing || false, + aiTitle: initialData.aiData?.title || null, + summary: initialData.aiData?.summary || null, + chapters: initialData.aiData?.chapters || null, + generationError: null, + } : undefined, refetchInterval: (query) => { const data = query.state.data; @@ -146,27 +145,13 @@ export const Share = ({ ? new Date(data.metadata.customCreatedAt) : data.createdAt; - const videoRef = useRef(null); + const playerRef = useRef(null); const { data: videoStatus } = useVideoStatus(data.id, aiGenerationEnabled, { transcriptionStatus: data.transcriptionStatus, aiData: initialAiData, }); - // const { data: viewCount } = useVideoAnalytics( - // data.id, - // initialAnalytics.views - // ); - - // const analytics = useMemo( - // () => ({ - // views: viewCount || 0, - // comments: 0, // comments.filter((c) => c.type === "text").length, - // reactions: 0, // comments.filter((c) => c.type === "emoji").length, - // }), - // [viewCount, comments] - // ); - const transcriptionStatus = videoStatus?.transcriptionStatus || data.transcriptionStatus; @@ -212,29 +197,28 @@ export const Share = ({ const aiLoading = shouldShowLoading(); const handleSeek = (time: number) => { - if (videoRef.current) { - videoRef.current.currentTime = time; + if (playerRef.current) { + playerRef.current.currentTime = time; } }; - const headerData = - aiData && aiData.title && !aiData.processing - ? { ...data, name: aiData.title, createdAt: effectiveDate } - : { ...data, createdAt: effectiveDate }; - return (
- +
+ +
@@ -263,31 +247,31 @@ export const Share = ({
-
+
{aiLoading && (transcriptionStatus === "PROCESSING" || transcriptionStatus === "COMPLETE") && ( -
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
+
{[1, 2, 3, 4].map((i) => (
-
-
+
+
))}
@@ -321,10 +305,10 @@ export const Share = ({ {aiData.chapters.map((chapter) => (
handleSeek(chapter.start)} > - + {formatTime(chapter.start)} {chapter.title} diff --git a/apps/web/app/s/[videoId]/_components/AuthOverlay.tsx b/apps/web/app/s/[videoId]/_components/AuthOverlay.tsx index 807acf164..0fd858a91 100644 --- a/apps/web/app/s/[videoId]/_components/AuthOverlay.tsx +++ b/apps/web/app/s/[videoId]/_components/AuthOverlay.tsx @@ -26,23 +26,13 @@ export const AuthOverlay: React.FC = ({ return ( - +
-

- Sign in to comment -

-

- Join the conversation. -

+

Sign in to comment

+

Join the conversation.

@@ -54,7 +44,7 @@ export const AuthOverlay: React.FC = ({ disabled={loading} > Google = ({ -
- )} - {currentSubtitle && currentSubtitle.text && subtitlesVisible && ( -
-
- {currentSubtitle.text - .replace("- ", "") - .replace(".", "") - .replace(",", "")} -
-
- )} -
- - {showPreview && !isLoading && isMP4Source && isLargeScreen && ( -
-
-
-
- {thumbnailUrl ? ( - {`Preview { - setThumbnailUrl(null); - }} - /> - ) : ( - - )} -
-
-
- {chapters.length > 0 && longestDuration > 0 && ( -
- {(() => { - const previewChapter = chapters.find((chapter, index) => { - const nextChapter = chapters[index + 1]; - const chapterStart = chapter.start; - const chapterEnd = nextChapter - ? nextChapter.start - : longestDuration; - return ( - previewTime >= chapterStart && previewTime < chapterEnd - ); - }); - return previewChapter ? previewChapter.title : ""; - })()} -
- )} -
- {formatTimeWithMilliseconds(previewTime)} -
-
-
-
- )} - -
-
{ - handleSeekMouseMove(e); - }} - > - {!isLoading && ( - - - - )} - -
- {/* Render chapter backgrounds */} - {chapters.length > 0 && longestDuration > 0 ? ( -
- {chapters.map((chapter, index) => { - const nextChapter = chapters[index + 1]; - const chapterStart = chapter.start; - const chapterEnd = nextChapter ? nextChapter.start : longestDuration; - const chapterDuration = chapterEnd - chapterStart; - const chapterWidth = (chapterDuration / longestDuration) * 100; - return ( -
{ - e.stopPropagation(); - applyTimeToVideos(chapter.start); - }} - /> - ); - })} -
- ) : ( -
- )} - {/* Render the main progress bar (white) */} -
-
- seeking - ? "scale-125 transition-transform ring-blue-300 ring-offset-2 ring-2" - : "" - )} - tabIndex={0} - /> -
-
+
+
-
{ - setIsHoveringControls(true); - }} - onMouseLeave={() => { - setIsHoveringControls(false); - }} - > -
-
- - - -
-
- {formatTime(currentTime)} - {formatTime(longestDuration)} -
- {chapters.length > 0 && longestDuration > 0 && ( -
- {(() => { - const currentChapter = chapters.find((chapter, index) => { - const nextChapter = chapters[index + 1]; - const chapterStart = chapter.start; - const chapterEnd = nextChapter - ? nextChapter.start - : longestDuration; - return ( - currentTime >= chapterStart && currentTime < chapterEnd - ); - }); - return currentChapter ? `• ${currentChapter.title}` : ""; - })()} -
- )} -
-
-
-
- - - - {isTranscriptionProcessing && subtitles.length === 0 && ( - - - - )} - {aiProcessing && ( - - - - )} - {subtitles.length > 0 && ( - - - - )} - - - - - - -
-
-
-
{user && !isUserOnProPlan({ subscriptionStatus: user.stripeSubscriptionStatus, @@ -1458,148 +168,6 @@ export const ShareVideo = forwardRef< open={upgradeModalOpen} onOpenChange={setUpgradeModalOpen} /> -
+ ); }); - -// Custom hook for video source validation using TanStack Query -const useVideoSourceValidation = ( - data: typeof videos.$inferSelect, - videoMetadataLoaded: boolean -) => { - return useQuery({ - queryKey: ["video-source-validation", data.id, data.source.type], - queryFn: async () => { - if (data.source.type !== "desktopMP4") { - return { isMP4Source: false }; - } - - const thumbUrl = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&thumbnailTime=0`; - - try { - const response = await fetch(thumbUrl, { method: "HEAD" }); - return { isMP4Source: response.ok }; - } catch (error) { - console.error("Error checking thumbnails:", error); - return { isMP4Source: false }; - } - }, - enabled: videoMetadataLoaded && data.source.type === "desktopMP4", - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - }); -}; - -// Custom hook for screen size detection -const useScreenSize = () => { - const [isLargeScreen, setIsLargeScreen] = useState(false); - - useEffect(() => { - const checkScreenSize = () => { - setIsLargeScreen(window.innerWidth >= 1024); - }; - - checkScreenSize(); - window.addEventListener("resize", checkScreenSize); - - return () => window.removeEventListener("resize", checkScreenSize); - }, []); - - return isLargeScreen; -}; - -// Custom hook for transcription processing -const useTranscriptionProcessing = ( - data: typeof videos.$inferSelect, - transcriptContent: string | undefined, - transcriptError: any -) => { - const [isTranscriptionProcessing, setIsTranscriptionProcessing] = useState( - data.transcriptionStatus === "PROCESSING" - ); - const [subtitles, setSubtitles] = useState([]); - - useEffect(() => { - if (!transcriptContent && data.transcriptionStatus === "PROCESSING") { - return setIsTranscriptionProcessing(false); - } - if (transcriptContent) { - const parsedSubtitles = fromVtt(transcriptContent); - setSubtitles(parsedSubtitles); - setIsTranscriptionProcessing(false); - } else if (transcriptError) { - console.error( - "[ShareVideo] Subtitle error from React Query:", - transcriptError.message - ); - if (transcriptError.message === "TRANSCRIPT_NOT_READY") { - setIsTranscriptionProcessing(true); - } else { - setIsTranscriptionProcessing(false); - } - } else if (data.transcriptionStatus === "PROCESSING") { - setIsTranscriptionProcessing(true); - } else if (data.transcriptionStatus === "ERROR") { - setIsTranscriptionProcessing(false); - } - }, [transcriptContent, transcriptError, data.transcriptionStatus]); - - return { isTranscriptionProcessing, subtitles }; -}; - -function CommentIndicators(props: { - className?: string; - comments: MaybePromise; - longestDuration: number; -}) { - const comments = - props.comments instanceof Promise ? use(props.comments) : props.comments; - - return ( -
- {comments.map((comment) => { - const commentPosition = - comment.timestamp === null - ? 0 - : (comment.timestamp / props.longestDuration) * 100; - - let tooltipContent = ""; - if (comment.type === "text") { - tooltipContent = - comment.authorId === "anonymous" - ? `Anonymous: ${comment.content}` - : `${comment.authorName || "User"}: ${comment.content}`; - } else { - tooltipContent = - comment.authorId === "anonymous" - ? "Anonymous" - : comment.authorName || "User"; - } - - return ( -
- - {comment.type === "text" ? ( - - ) : ( - comment.content - )} - - -
- ); - })} -
- ); -} diff --git a/apps/web/app/s/[videoId]/_components/VideoJs.tsx b/apps/web/app/s/[videoId]/_components/VideoJs.tsx new file mode 100644 index 000000000..3ed30f9f5 --- /dev/null +++ b/apps/web/app/s/[videoId]/_components/VideoJs.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { forwardRef, useRef, useEffect, MutableRefObject } from "react"; +import videojs from "video.js"; +import "video.js/dist/video-js.css"; +import Player from "video.js/dist/types/player"; + +interface Props { + onReady?: (player: Player) => void; + options: { + autoplay: boolean; + controls: boolean; + responsive: boolean; + fluid: boolean; + sources: { + src: string; + type: string; + }[]; + } +} + +export const VideoJS = forwardRef(({ options, onReady }: Props, ref) => { + const videoRef = useRef(null); + const playerRef = ref as MutableRefObject; + + useEffect(() => { + if (!videoRef.current) return; + if (!playerRef.current) { + const videoElement = document.createElement("video-js"); + + videoElement.classList.add("vjs-big-play-centered", "vjs-cap"); + videoRef.current.appendChild(videoElement); + + const player = (playerRef.current = videojs(videoElement, options, () => { + onReady && onReady(player); + })); + + } else { + const player = playerRef.current; + + player.autoplay(options.autoplay); + player.src(options.sources); + } + }, [options, videoRef]); + + useEffect(() => { + const player = playerRef.current; + + return () => { + if (player && !player.isDisposed()) { + player.dispose(); + playerRef.current = null; + } + }; + }, [playerRef]); + + useEffect(() => { + const player = playerRef.current; + + if (!player) return; + + const handleKeyDown = (event: KeyboardEvent) => { + const tag = (event.target as HTMLElement)?.tagName; + const isEditable = + tag === "INPUT" || + tag === "TEXTAREA" || + tag === "SELECT" || + tag === "BUTTON" || + (event.target as HTMLElement)?.isContentEditable; + + if (isEditable) return; + if (event.key === " ") { + event.preventDefault(); + player.paused() ? player.play() : player.pause(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [playerRef]); + + return ( +
+
+
+ ); +}); + +export default VideoJS; diff --git a/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx b/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx index 66f697715..31bfddaa2 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx @@ -20,6 +20,7 @@ interface TranscriptEntry { startTime: number; } + const parseVTT = (vttContent: string): TranscriptEntry[] => { const lines = vttContent.split("\n"); const entries: TranscriptEntry[] = []; @@ -118,8 +119,8 @@ const parseVTT = (vttContent: string): TranscriptEntry[] => { export const Transcript: React.FC = ({ data, - onSeek, user, + onSeek, }) => { const [transcriptData, setTranscriptData] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -134,6 +135,7 @@ export const Transcript: React.FC = ({ const [copyPressed, setCopyPressed] = useState(false); const [downloadPressed, setDownloadPressed] = useState(false); + const { data: transcriptContent, isLoading: isTranscriptLoading, @@ -224,9 +226,7 @@ export const Transcript: React.FC = ({ setSelectedEntry(entry.id); - if (onSeek) { - onSeek(entry.startTime); - } + onSeek?.(entry.startTime); }; const startEditing = (entry: TranscriptEntry) => { @@ -306,8 +306,8 @@ export const Transcript: React.FC = ({ return `${hours.toString().padStart(2, "0")}:${minutes .toString() .padStart(2, "0")}:${secs.toString().padStart(2, "0")}.${milliseconds - .toString() - .padStart(3, "0")}`; + .toString() + .padStart(3, "0")}`; }; return `${entry.id}\n${formatTime(startSeconds)} --> ${formatTime( @@ -448,7 +448,7 @@ export const Transcript: React.FC = ({ spinner={isCopying} > {!copyPressed ? ( - + ) : ( = ({ strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - className="w-3 h-3 mr-1 svgpathanimation" + className="mr-1 w-3 h-3 svgpathanimation" > @@ -474,7 +474,7 @@ export const Transcript: React.FC = ({ size="xs" > {!downloadPressed ? ( - + ) : ( = ({ strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - className="w-3 h-3 mr-1 svgpathanimation" + className="mr-1 w-3 h-3 svgpathanimation" > @@ -501,17 +501,16 @@ export const Transcript: React.FC = ({ {transcriptData.map((entry) => (
handleTranscriptClick(entry)} >
-
+
{entry.timestamp}
{canEdit && editingEntry !== entry.id && ( @@ -530,11 +529,11 @@ export const Transcript: React.FC = ({ {editingEntry === entry.id ? (
-
+