diff --git a/Cargo.lock b/Cargo.lock index 0c6af357..9ee9e151 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,7 +162,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -307,7 +307,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -342,7 +342,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -540,7 +540,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex 1.3.0", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -558,7 +558,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex 1.3.0", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -690,7 +690,7 @@ checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -825,7 +825,8 @@ dependencies = [ "tempfile", "thiserror", "tracing", - "windows 0.52.0", + "windows 0.58.0", + "windows-capture", ] [[package]] @@ -867,6 +868,7 @@ version = "0.1.0" dependencies = [ "nix 0.29.0", "tokio", + "uuid", "windows 0.58.0", ] @@ -1535,7 +1537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -1545,7 +1547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -1580,7 +1582,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -1591,7 +1593,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -1652,7 +1654,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -1665,7 +1667,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -1733,7 +1735,8 @@ dependencies = [ "tracing", "uuid", "wgpu", - "windows 0.52.0", + "windows 0.58.0", + "windows-sys 0.59.0", ] [[package]] @@ -1817,7 +1820,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -1849,7 +1852,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -2002,7 +2005,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -2265,7 +2268,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -2389,7 +2392,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -2667,7 +2670,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -2836,7 +2839,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -3264,7 +3267,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -4077,7 +4080,7 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "nokhwa" version = "0.10.3" -source = "git+https://github.com/CapSoftware/nokhwa?rev=c5c7e2298764#c5c7e2298764ddf122c2ec0206a8aa7ba80257f0" +source = "git+https://github.com/CapSoftware/nokhwa?rev=0d3d1f30a78b#0d3d1f30a78bb4616b4a4d0939a29ad0c1a8e14f" dependencies = [ "flume 0.10.14", "image 0.24.9", @@ -4093,7 +4096,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-linux" version = "0.1.0" -source = "git+https://github.com/CapSoftware/nokhwa?rev=c5c7e2298764#c5c7e2298764ddf122c2ec0206a8aa7ba80257f0" +source = "git+https://github.com/CapSoftware/nokhwa?rev=0d3d1f30a78b#0d3d1f30a78bb4616b4a4d0939a29ad0c1a8e14f" dependencies = [ "nokhwa-core", "v4l", @@ -4103,7 +4106,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-macos" version = "0.2.0" -source = "git+https://github.com/CapSoftware/nokhwa?rev=c5c7e2298764#c5c7e2298764ddf122c2ec0206a8aa7ba80257f0" +source = "git+https://github.com/CapSoftware/nokhwa?rev=0d3d1f30a78b#0d3d1f30a78bb4616b4a4d0939a29ad0c1a8e14f" dependencies = [ "block", "cocoa-foundation 0.1.2", @@ -4118,7 +4121,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-windows" version = "0.4.0" -source = "git+https://github.com/CapSoftware/nokhwa?rev=c5c7e2298764#c5c7e2298764ddf122c2ec0206a8aa7ba80257f0" +source = "git+https://github.com/CapSoftware/nokhwa?rev=0d3d1f30a78b#0d3d1f30a78bb4616b4a4d0939a29ad0c1a8e14f" dependencies = [ "nokhwa-core", "once_cell", @@ -4128,7 +4131,7 @@ dependencies = [ [[package]] name = "nokhwa-core" version = "0.1.0" -source = "git+https://github.com/CapSoftware/nokhwa?rev=c5c7e2298764#c5c7e2298764ddf122c2ec0206a8aa7ba80257f0" +source = "git+https://github.com/CapSoftware/nokhwa?rev=0d3d1f30a78b#0d3d1f30a78bb4616b4a4d0939a29ad0c1a8e14f" dependencies = [ "bytes", "image 0.24.9", @@ -4209,7 +4212,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -4270,7 +4273,7 @@ dependencies = [ "proc-macro-crate 2.0.2", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -4612,7 +4615,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -4862,7 +4865,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -4909,7 +4912,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -5118,7 +5121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -5704,7 +5707,7 @@ dependencies = [ [[package]] name = "scap" version = "0.0.7" -source = "git+https://github.com/CapSoftware/scap?rev=b1e140a3fe90#b1e140a3fe905c19b845dfea66b3b1aea02f0472" +source = "git+https://github.com/CapSoftware/scap?rev=b1e140a3fe905c19b845dfea66b3b1aea02f0472#b1e140a3fe905c19b845dfea66b3b1aea02f0472" dependencies = [ "cocoa 0.25.0", "core-graphics-helmer-fork", @@ -5753,7 +5756,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -5871,7 +5874,7 @@ checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -5882,7 +5885,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -5915,7 +5918,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -5966,7 +5969,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -6178,7 +6181,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -6349,9 +6352,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.85" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -6490,7 +6493,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -6603,7 +6606,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.85", + "syn 2.0.87", "tauri-utils", "thiserror", "time", @@ -6621,7 +6624,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", "tauri-codegen", "tauri-utils", ] @@ -6950,7 +6953,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -7060,22 +7063,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -7161,7 +7164,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -7332,7 +7335,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -7552,9 +7555,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom 0.2.15", "serde", @@ -7688,7 +7691,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -7722,7 +7725,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7891,7 +7894,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -8223,7 +8226,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -8234,7 +8237,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -8245,7 +8248,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -8256,7 +8259,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -8721,7 +8724,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0bf66bc1..9a6e2099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,10 +20,14 @@ tokio = { version = "1.39.3", features = [ ] } tauri = { version = "2.0.0" } specta = { version = "=2.0.0-rc.20" } -scap = { git = "https://github.com/CapSoftware/scap", rev = "b1e140a3fe90" } -nokhwa = { git = "https://github.com/CapSoftware/nokhwa", rev = "c5c7e2298764", features = [ +scap = { git = "https://github.com/CapSoftware/scap", rev = "b1e140a3fe905c19b845dfea66b3b1aea02f0472" } +nokhwa = { git = "https://github.com/CapSoftware/nokhwa", rev = "0d3d1f30a78b", features = [ "input-native", "serialize", ] } -nokhwa-bindings-macos = { git = "https://github.com/CapSoftware/nokhwa", rev = "c5c7e2298764" } +nokhwa-bindings-macos = { git = "https://github.com/CapSoftware/nokhwa", rev = "0d3d1f30a78b" } wgpu = "22.1.0" + +windows = "0.58.0" +windows-sys = "0.59.0" +windows-capture = "=1.3.6" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 31b11e58..3a213cff 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -90,12 +90,13 @@ swift-rs = "1.0.6" tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } [target.'cfg(target_os= "windows")'.dependencies] -windows = { version = "0.52.0", features = [ +windows = { workspace = true, features = [ "Win32_Foundation", "Win32_System", "Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", ] } +windows-sys = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { version = "0.29.0", features = ["fs"] } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index f1b132a1..4a3ca09e 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -13,6 +13,7 @@ "core:window:allow-minimize", "core:window:allow-unminimize", "core:window:allow-maximize", + "core:window:allow-unmaximize", "core:window:allow-set-size", "core:window:allow-set-focus", "core:window:allow-start-dragging", diff --git a/apps/desktop/src-tauri/src/cursor.rs b/apps/desktop/src-tauri/src/cursor.rs index 622ae8c9..3bb11434 100644 --- a/apps/desktop/src-tauri/src/cursor.rs +++ b/apps/desktop/src-tauri/src/cursor.rs @@ -185,131 +185,132 @@ fn get_cursor_image_data() -> Option> { #[cfg(windows)] fn get_cursor_image_data() -> Option> { - use windows::Win32::Foundation::{BOOL, HWND, POINT}; - use windows::Win32::Graphics::Gdi::{ - BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, CreateDIBSection, DeleteDC, - DeleteObject, GetDC, GetObjectA, ReleaseDC, SelectObject, BITMAP, BITMAPINFO, - BITMAPINFOHEADER, DIB_RGB_COLORS, SRCCOPY, - }; - use windows::Win32::UI::WindowsAndMessaging::{GetCursorInfo, CURSORINFO, CURSORINFO_FLAGS}; - use windows::Win32::UI::WindowsAndMessaging::{GetIconInfo, ICONINFO}; - - unsafe { - // Get cursor info - let mut cursor_info = CURSORINFO { - cbSize: std::mem::size_of::() as u32, - flags: CURSORINFO_FLAGS(0), - hCursor: Default::default(), - ptScreenPos: POINT::default(), - }; - - // Handle Result return type - if GetCursorInfo(&mut cursor_info).is_err() { - return None; - } - - // If no cursor, return None - if cursor_info.hCursor.is_invalid() { - return None; - } - - // Get icon info - let mut icon_info = ICONINFO::default(); - // Handle Result return type - if GetIconInfo(cursor_info.hCursor, &mut icon_info).is_err() { - return None; - } - - // Get bitmap info - let mut bitmap = BITMAP::default(); - if GetObjectA( - icon_info.hbmColor, - std::mem::size_of::() as i32, - Some(&mut bitmap as *mut _ as *mut _), - ) == 0 - { - return None; - } - - // Create compatible DC - let screen_dc = GetDC(HWND(0)); - let mem_dc = CreateCompatibleDC(screen_dc); - - // Create bitmap info header - let bi = BITMAPINFOHEADER { - biSize: std::mem::size_of::() as u32, - biWidth: bitmap.bmWidth, - biHeight: -bitmap.bmHeight, // Negative height for top-down bitmap - biPlanes: 1, - biBitCount: 32, - biCompression: 0, - biSizeImage: 0, - biXPelsPerMeter: 0, - biYPelsPerMeter: 0, - biClrUsed: 0, - biClrImportant: 0, - }; - - let bitmap_info = BITMAPINFO { - bmiHeader: bi, - bmiColors: [Default::default()], - }; - - // Create DIB section - let mut bits: *mut std::ffi::c_void = std::ptr::null_mut(); - let dib = CreateDIBSection(mem_dc, &bitmap_info, DIB_RGB_COLORS, &mut bits, None, 0); - - if dib.is_err() { - return None; - } - - let dib = dib.unwrap(); - - // Select DIB into DC - let old_bitmap = SelectObject(mem_dc, dib); - - // Copy cursor image - if BitBlt( - mem_dc, - 0, - 0, - bitmap.bmWidth, - bitmap.bmHeight, - screen_dc, - cursor_info.ptScreenPos.x, - cursor_info.ptScreenPos.y, - SRCCOPY, - ) - .is_err() - { - return None; - } - - // Get image data - let size = (bitmap.bmWidth * bitmap.bmHeight * 4) as usize; - let mut image_data = vec![0u8; size]; - std::ptr::copy_nonoverlapping(bits, image_data.as_mut_ptr() as *mut _, size); - - // Cleanup - SelectObject(mem_dc, old_bitmap); - DeleteObject(dib); - DeleteDC(mem_dc); - ReleaseDC(HWND(0), screen_dc); - DeleteObject(icon_info.hbmColor); - DeleteObject(icon_info.hbmMask); - - // Convert to PNG format - let image = - image::RgbaImage::from_raw(bitmap.bmWidth as u32, bitmap.bmHeight as u32, image_data)?; - - let mut png_data = Vec::new(); - image - .write_to( - &mut std::io::Cursor::new(&mut png_data), - image::ImageFormat::Png, - ) - .ok()?; - - Some(png_data) - } + return None; + // use windows::Win32::Foundation::{BOOL, HWND, POINT}; + // use windows::Win32::Graphics::Gdi::{ + // BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, CreateDIBSection, DeleteDC, + // DeleteObject, GetDC, GetObjectA, ReleaseDC, SelectObject, BITMAP, BITMAPINFO, + // BITMAPINFOHEADER, DIB_RGB_COLORS, SRCCOPY, + // }; + // use windows::Win32::UI::WindowsAndMessaging::{GetCursorInfo, CURSORINFO, CURSORINFO_FLAGS}; + // use windows::Win32::UI::WindowsAndMessaging::{GetIconInfo, ICONINFO}; + + // unsafe { + // // Get cursor info + // let mut cursor_info = CURSORINFO { + // cbSize: std::mem::size_of::() as u32, + // flags: CURSORINFO_FLAGS(0), + // hCursor: Default::default(), + // ptScreenPos: POINT::default(), + // }; + + // // Handle Result return type + // if GetCursorInfo(&mut cursor_info).is_err() { + // return None; + // } + + // // If no cursor, return None + // if cursor_info.hCursor.is_invalid() { + // return None; + // } + + // // Get icon info + // let mut icon_info = ICONINFO::default(); + // // Handle Result return type + // if GetIconInfo(cursor_info.hCursor, &mut icon_info).is_err() { + // return None; + // } + + // // Get bitmap info + // let mut bitmap = BITMAP::default(); + // if GetObjectA( + // icon_info.hbmColor, + // std::mem::size_of::() as i32, + // Some(&mut bitmap as *mut _ as *mut _), + // ) == 0 + // { + // return None; + // } + + // // Create compatible DC + // let screen_dc = GetDC(HWND(0)); + // let mem_dc = CreateCompatibleDC(screen_dc); + + // // Create bitmap info header + // let bi = BITMAPINFOHEADER { + // biSize: std::mem::size_of::() as u32, + // biWidth: bitmap.bmWidth, + // biHeight: -bitmap.bmHeight, // Negative height for top-down bitmap + // biPlanes: 1, + // biBitCount: 32, + // biCompression: 0, + // biSizeImage: 0, + // biXPelsPerMeter: 0, + // biYPelsPerMeter: 0, + // biClrUsed: 0, + // biClrImportant: 0, + // }; + + // let bitmap_info = BITMAPINFO { + // bmiHeader: bi, + // bmiColors: [Default::default()], + // }; + + // // Create DIB section + // let mut bits: *mut std::ffi::c_void = std::ptr::null_mut(); + // let dib = CreateDIBSection(mem_dc, &bitmap_info, DIB_RGB_COLORS, &mut bits, None, 0); + + // if dib.is_err() { + // return None; + // } + + // let dib = dib.unwrap(); + + // // Select DIB into DC + // let old_bitmap = SelectObject(mem_dc, dib); + + // // Copy cursor image + // if BitBlt( + // mem_dc, + // 0, + // 0, + // bitmap.bmWidth, + // bitmap.bmHeight, + // screen_dc, + // cursor_info.ptScreenPos.x, + // cursor_info.ptScreenPos.y, + // SRCCOPY, + // ) + // .is_err() + // { + // return None; + // } + + // // Get image data + // let size = (bitmap.bmWidth * bitmap.bmHeight * 4) as usize; + // let mut image_data = vec![0u8; size]; + // std::ptr::copy_nonoverlapping(bits, image_data.as_mut_ptr() as *mut _, size); + + // // Cleanup + // SelectObject(mem_dc, old_bitmap); + // DeleteObject(dib); + // DeleteDC(mem_dc); + // ReleaseDC(HWND(0), screen_dc); + // DeleteObject(icon_info.hbmColor); + // DeleteObject(icon_info.hbmMask); + + // // Convert to PNG format + // let image = + // image::RgbaImage::from_raw(bitmap.bmWidth as u32, bitmap.bmHeight as u32, image_data)?; + + // let mut png_data = Vec::new(); + // image + // .write_to( + // &mut std::io::Cursor::new(&mut png_data), + // image::ImageFormat::Png, + // ) + // .ok()?; + + // Some(png_data) + // } } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 4614f24b..97b68a4c 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -55,7 +55,7 @@ use tauri::{AppHandle, Manager, Runtime, State, WindowEvent}; use tauri_plugin_notification::{NotificationExt, PermissionState}; use tauri_plugin_shell::ShellExt; use tauri_specta::Event; -use tokio::task; +use tokio::io::AsyncWriteExt; use tokio::{ sync::{Mutex, RwLock}, time::sleep, @@ -987,61 +987,19 @@ async fn render_to_file_impl( let audio_dir = tempfile::tempdir().unwrap(); let video_dir = tempfile::tempdir().unwrap(); - - let video_tx = { - let pipe_path = video_dir.path().join("video.pipe"); - #[cfg(target_os = "macos")] - cap_utils::create_named_pipe(&pipe_path).unwrap(); - - ffmpeg.add_input(cap_ffmpeg_cli::FFmpegRawVideoInput { - width: output_size.0, - height: output_size.1, - fps: 30, - pix_fmt: "rgba", - input: pipe_path.clone().into_os_string(), - }); - - let (tx, mut rx) = tokio::sync::mpsc::channel::>(30); - - tokio::spawn(async move { - let mut file = std::fs::File::create(&pipe_path).unwrap(); - println!("video pipe opened"); - - while let Some(bytes) = rx.recv().await { - file.write_all(&bytes).unwrap(); - } - - println!("done writing to video pipe"); - }); - - tx - }; let mut audio = if let Some(audio_data) = audio.lock().unwrap().as_ref() { - let pipe_path = audio_dir.path().join("audio.pipe"); + let (tx, rx) = tokio::sync::mpsc::channel::>(30); - #[cfg(target_os = "macos")] - cap_utils::create_named_pipe(&pipe_path).unwrap(); + let pipe_path = + cap_utils::create_channel_named_pipe(rx, audio_dir.path().join("audio.pipe")); ffmpeg.add_input(cap_ffmpeg_cli::FFmpegRawAudioInput { - input: pipe_path.clone().into_os_string(), + input: pipe_path, sample_format: "f64le".to_string(), sample_rate: audio_data.info.sample_rate, channels: audio_data.info.channels as u16, }); - let (tx, mut rx) = tokio::sync::mpsc::channel::>(30); - - tokio::spawn(async move { - let mut file = std::fs::File::create(&pipe_path).unwrap(); - println!("audio pipe opened"); - - while let Some(bytes) = rx.recv().await { - file.write_all(&bytes).unwrap(); - } - - println!("done writing to audio pipe"); - }); - let buffer = AudioFrameBuffer::new(audio_data.clone()); Some(AudioRender { buffer, @@ -1051,6 +1009,23 @@ async fn render_to_file_impl( None }; + let video_tx = { + let (tx, rx) = tokio::sync::mpsc::channel::>(30); + + let pipe_path = + cap_utils::create_channel_named_pipe(rx, video_dir.path().join("video.pipe")); + + ffmpeg.add_input(cap_ffmpeg_cli::FFmpegRawVideoInput { + width: output_size.0, + height: output_size.1, + fps: 30, + pix_fmt: "rgba", + input: pipe_path, + }); + + tx + }; + ffmpeg .command .args(["-f", "mp4"]) @@ -1968,31 +1943,79 @@ async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Resul std::fs::create_dir_all(&recording_dir).map_err(|e| e.to_string())?; - // Take screenshot using scap with optimized settings - let options = scap::capturer::Options { - fps: 1, - output_type: scap::frame::FrameType::BGRAFrame, - show_highlight: false, - ..Default::default() - }; + // Capture the screenshot synchronously before any await points + let (width, height, bgra_data) = { + // Take screenshot using scap with optimized settings + let options = scap::capturer::Options { + fps: 1, + output_type: scap::frame::FrameType::BGRAFrame, + show_highlight: false, + ..Default::default() + }; - if let Some(window) = CapWindowId::Main.get(&app) { - window.hide().ok(); - } + // Hide main window before taking screenshot + if let Some(window) = CapWindowId::Main.get(&app) { + window.hide().ok(); + } - let mut capturer = Capturer::new(options); - capturer.start_capture(); + // Create and use capturer on the main thread + let mut capturer = Capturer::new(options); + capturer.start_capture(); + let frame = capturer + .get_next_frame() + .map_err(|e| format!("Failed to get frame: {}", e))?; + capturer.stop_capture(); + + // Show main window after taking screenshot + if let Some(window) = CapWindowId::Main.get(&app) { + window.show().ok(); + } - let frame = match capturer.get_next_frame() { - Ok(frame) => frame, - Err(e) => return Err(format!("Failed to get frame: {}", e)), - }; + match frame { + Frame::BGRA(bgra_frame) => Ok(( + bgra_frame.width as u32, + bgra_frame.height as u32, + bgra_frame.data, + )), + _ => Err("Unexpected frame type".to_string()), + } + }?; + + let now = chrono::Local::now(); + let screenshot_name = format!( + "Cap {} at {}.png", + now.format("%Y-%m-%d"), + now.format("%H.%M.%S") + ); + let screenshot_path = recording_dir.join(&screenshot_name); + + let app_handle = app.clone(); + let recording_dir = recording_dir.clone(); + tokio::task::spawn_blocking(move || -> Result<(), String> { + // Convert BGRA to RGBA + let mut rgba_data = vec![0; bgra_data.len()]; + for (bgra, rgba) in bgra_data.chunks_exact(4).zip(rgba_data.chunks_exact_mut(4)) { + rgba[0] = bgra[2]; + rgba[1] = bgra[1]; + rgba[2] = bgra[0]; + rgba[3] = bgra[3]; + } - capturer.stop_capture(); + // Create file and PNG encoder + let file = File::create(&screenshot_path).map_err(|e| e.to_string())?; + let w = &mut BufWriter::new(file); - if let Frame::BGRA(bgra_frame) = frame { - let width = bgra_frame.width as u32; - let height = bgra_frame.height as u32; + let mut encoder = Encoder::new(w, width, height); + encoder.set_color(ColorType::Rgba); + encoder.set_compression(png::Compression::Fast); + let mut writer = encoder.write_header().map_err(|e| e.to_string())?; + + // Write image data + writer + .write_image_data(&rgba_data) + .map_err(|e| e.to_string())?; + + AppSounds::Screenshot.play(); let now = chrono::Local::now(); let screenshot_name = format!( @@ -2000,81 +2023,34 @@ async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Resul now.format("%Y-%m-%d"), now.format("%H.%M.%S") ); - let screenshot_path = recording_dir.join(&screenshot_name); - - // Perform image processing and saving asynchronously - let app_handle = app.clone(); - task::spawn_blocking(move || -> Result<(), String> { - // Convert BGRA to RGBA - let mut rgba_data = vec![0; bgra_frame.data.len()]; - for (bgra, rgba) in bgra_frame - .data - .chunks_exact(4) - .zip(rgba_data.chunks_exact_mut(4)) - { - rgba[0] = bgra[2]; - rgba[1] = bgra[1]; - rgba[2] = bgra[0]; - rgba[3] = bgra[3]; - } - - // Create file and PNG encoder - let file = File::create(&screenshot_path).map_err(|e| e.to_string())?; - let w = &mut BufWriter::new(file); - - let mut encoder = Encoder::new(w, width, height); - encoder.set_color(ColorType::Rgba); - encoder.set_compression(png::Compression::Fast); - let mut writer = encoder.write_header().map_err(|e| e.to_string())?; - - // Write image data - writer - .write_image_data(&rgba_data) - .map_err(|e| e.to_string())?; - - AppSounds::Screenshot.play(); - - if let Some(window) = CapWindowId::Main.get(&app) { - window.show().ok(); - } - let now = chrono::Local::now(); - let screenshot_name = format!( - "Cap {} at {}.png", - now.format("%Y-%m-%d"), - now.format("%H.%M.%S") - ); - - use cap_project::*; - RecordingMeta { - project_path: recording_dir.clone(), - sharing: None, - pretty_name: screenshot_name, - display: Display { - path: screenshot_path.clone(), - }, - camera: None, - audio: None, - segments: vec![], - cursor: None, - } - .save_for_project(); - - NewScreenshotAdded { - path: screenshot_path, - } - .emit(&app_handle) - .ok(); + use cap_project::*; + RecordingMeta { + project_path: recording_dir.clone(), + sharing: None, + pretty_name: screenshot_name, + display: Display { + path: screenshot_path.clone(), + }, + camera: None, + audio: None, + segments: vec![], + cursor: None, + } + .save_for_project(); - Ok(()) - }) - .await - .map_err(|e| e.to_string())??; + NewScreenshotAdded { + path: screenshot_path, + } + .emit(&app_handle) + .ok(); Ok(()) - } else { - Err("Unexpected frame type".to_string()) - } + }) + .await + .map_err(|e| format!("Task join error: {}", e))??; + + Ok(()) } #[tauri::command] @@ -2447,6 +2423,7 @@ pub async fn run() { is_camera_window_open, seek_to, send_feedback_request, + windows::position_traffic_lights, ]) .events(tauri_specta::collect_events![ RecordingOptionsChanged, diff --git a/apps/desktop/src-tauri/src/platform/macos/mod.rs b/apps/desktop/src-tauri/src/platform/macos/mod.rs index 5e079c67..8934d9c8 100644 --- a/apps/desktop/src-tauri/src/platform/macos/mod.rs +++ b/apps/desktop/src-tauri/src/platform/macos/mod.rs @@ -1,11 +1,14 @@ use std::ffi::c_void; +use cocoa::{ + base::{id, nil}, + foundation::NSString, +}; use core_graphics::{ base::boolean_t, display::{CFDictionaryRef, CGRect}, - window::{kCGWindowBounds, kCGWindowOwnerPID}, }; -use objc::{msg_send, sel, sel_impl}; +use objc::{class, msg_send, sel, sel_impl}; pub mod delegates; @@ -36,176 +39,6 @@ pub fn set_window_level(window: tauri::Window, level: u32) { }); } -pub fn get_on_screen_windows() -> Vec { - use core_foundation::{ - array::CFArrayGetCount, - base::FromVoid, - dictionary::CFDictionaryGetValue, - number::{kCFNumberIntType, CFNumberGetValue, CFNumberRef}, - string::CFString, - }; - use core_graphics::{ - display::CFArrayGetValueAtIndex, - window::{ - kCGNullWindowID, kCGWindowLayer, kCGWindowListExcludeDesktopElements, - kCGWindowListOptionOnScreenOnly, kCGWindowName, kCGWindowNumber, kCGWindowOwnerName, - CGWindowListCopyWindowInfo, - }, - }; - - let mut array = vec![]; - - unsafe { - let cf_win_array = CGWindowListCopyWindowInfo( - kCGWindowListExcludeDesktopElements | kCGWindowListOptionOnScreenOnly, - kCGNullWindowID, - ); - - if cf_win_array.is_null() { - return array; - } - - let count = CFArrayGetCount(cf_win_array); - - for i in 0..count { - let window_cf_dictionary_ref = - CFArrayGetValueAtIndex(cf_win_array, i) as CFDictionaryRef; - - if window_cf_dictionary_ref.is_null() { - continue; - } - - let level = { - let level_ref = - CFDictionaryGetValue(window_cf_dictionary_ref, kCGWindowLayer as *const c_void); - if level_ref.is_null() { - continue; - } - - let mut value: u32 = 0; - let is_success = CFNumberGetValue( - level_ref as CFNumberRef, - kCFNumberIntType, - &mut value as *mut _ as *mut c_void, - ); - - if !is_success { - continue; - } - - value - }; - - let bounds = { - let value_ref = CFDictionaryGetValue( - window_cf_dictionary_ref, - kCGWindowBounds as *const c_void, - ); - if value_ref.is_null() { - continue; - } - - let rect: CGRect = { - let mut rect = std::mem::zeroed(); - CGRectMakeWithDictionaryRepresentation(value_ref.cast(), &mut rect); - rect - }; - - Bounds { - x: rect.origin.x as u32, - y: rect.origin.y as u32, - width: rect.size.width as u32, - height: rect.size.height as u32, - } - }; - - let window_number = { - let level_ref = CFDictionaryGetValue( - window_cf_dictionary_ref, - kCGWindowNumber as *const c_void, - ); - if level_ref.is_null() { - continue; - } - - let mut value: u32 = 0; - let is_success = CFNumberGetValue( - level_ref as CFNumberRef, - kCFNumberIntType, - &mut value as *mut _ as *mut c_void, - ); - - if !is_success { - continue; - } - - value - }; - - let process_id = { - let value_ref = CFDictionaryGetValue( - window_cf_dictionary_ref, - kCGWindowOwnerPID as *const c_void, - ); - if value_ref.is_null() { - continue; - } - - let mut value: u32 = 0; - let is_success = CFNumberGetValue( - value_ref as CFNumberRef, - kCFNumberIntType, - &mut value as *mut _ as *mut c_void, - ); - - if !is_success { - continue; - } - - value - }; - - let name = { - let value_ref = - CFDictionaryGetValue(window_cf_dictionary_ref, kCGWindowName as *const c_void); - if value_ref.is_null() { - String::new() - } else { - CFString::from_void(value_ref).to_string() - } - }; - - let owner_name = { - let value_ref = CFDictionaryGetValue( - window_cf_dictionary_ref, - kCGWindowOwnerName as *const c_void, - ); - if value_ref.is_null() { - String::new() - } else { - CFString::from_void(value_ref).to_string() - } - }; - - if owner_name == "Window Server" { - continue; - } - - if level == 0 && !name.is_empty() { - array.push(Window { - name, - owner_name, - process_id, - window_number, - bounds, - }); - } - } - } - - array -} - pub fn get_ns_window_number(ns_window: *mut c_void) -> isize { let ns_window = ns_window as *const objc2_app_kit::NSWindow; @@ -236,3 +69,15 @@ pub fn write_string_to_pasteboard(string: &str) { }); } } + +/// Makes the background of the WKWebView layer transparent. +/// This differs from Tauri's implementation as it does not change the window background which causes performance performance issues and artifacts when shadows are enabled on the window. +/// Use Tauri's implementation to make the window itself transparent. +pub fn make_webview_transparent(target: &tauri::WebviewWindow) -> tauri::Result<()> { + target.with_webview(|webview| unsafe { + let wkwebview = webview.inner() as id; + let no: id = msg_send![class!(NSNumber), numberWithBool:0]; + // [https://developer.apple.com/documentation/webkit/webview/1408486-drawsbackground] + let _: id = msg_send![wkwebview, setValue:no forKey: NSString::alloc(nil).init_str("drawsBackground")]; + }) +} diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 35d62777..4e6842f7 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -227,7 +227,7 @@ pub async fn start( None, ); let screen_config = screen_source.info(); - let screen_bounds = screen_source.bounds; + // let screen_bounds = screen_source.bounds; let output_config = screen_config.scaled(1920, 30); let screen_filter = VideoFilter::init("screen", screen_config, output_config)?; @@ -238,7 +238,7 @@ pub async fn start( )?; pipeline_builder = pipeline_builder .source("screen_capture", screen_source) - // .pipe("screen_capture_filter", screen_filter) + .pipe("screen_capture_filter", screen_filter) .sink("screen_capture_encoder", screen_encoder); } diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 6fda8a9e..d0c6260f 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -5,6 +5,8 @@ use tauri::{ AppHandle, LogicalPosition, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder, Wry, }; +const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition = LogicalPosition::new(12.0, 12.0); + #[derive(Clone)] pub enum CapWindow { Setup, @@ -89,11 +91,9 @@ impl CapWindowId { pub fn traffic_lights_position(&self) -> Option>> { match self { - Self::Camera - | Self::InProgressRecording - | Self::WindowCaptureOccluder - | Self::PrevRecordings => None, + Self::Camera | Self::WindowCaptureOccluder | Self::PrevRecordings => None, Self::Editor { .. } => Some(Some(LogicalPosition::new(20.0, 48.0))), + Self::InProgressRecording => Some(Some(LogicalPosition::new(-100.0, -100.0))), _ => Some(None), } } @@ -127,16 +127,10 @@ impl CapWindow { .center() .focused(true) .maximizable(false) - .transparent(true) .theme(Some(tauri::Theme::Light)) .visible(true) .shadow(true); - #[cfg(target_os = "windows")] - { - window_builder = window_builder.decorations(false).shadow(false); - } - let window = window_builder.build()?; window.set_focus().ok(); window @@ -144,18 +138,12 @@ impl CapWindow { Self::Main => { let mut window_builder = self .window_builder(app, "/") - .inner_size(300.0, 375.0) + .inner_size(300.0, 360.0) .resizable(false) .maximized(false) .maximizable(false) - .transparent(true) - .theme(Some(tauri::Theme::Light)) - .shadow(true); - - #[cfg(target_os = "windows")] - { - window_builder = window_builder.decorations(false).shadow(false); - } + .maximized(false) + .theme(Some(tauri::Theme::Light)); window_builder.build()? } @@ -167,16 +155,7 @@ impl CapWindow { ) .min_inner_size(600.0, 450.0) .resizable(true) - .maximized(false) - .transparent(true); - - #[cfg(target_os = "macos")] - { - window_builder = window_builder - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .shadow(true); - } + .maximized(false); window_builder.build()? } @@ -184,16 +163,9 @@ impl CapWindow { let mut window_builder = self .window_builder(app, format!("/editor?id={project_id}")) .inner_size(1150.0, 800.0) + .maximizable(true) .theme(Some(tauri::Theme::Light)); - #[cfg(target_os = "macos")] - { - use tauri::TitleBarStyle; - window_builder = window_builder - .hidden_title(true) - .title_bar_style(TitleBarStyle::Overlay); - } - window_builder.build()? } Self::Upgrade => { @@ -202,7 +174,6 @@ impl CapWindow { .inner_size(800.0, 850.0) .resizable(false) .maximized(false) - .shadow(true) .transparent(true); window_builder.build()? @@ -245,7 +216,6 @@ impl CapWindow { .maximized(false) .resizable(false) .fullscreen(false) - .decorations(false) .shadow(false) .always_on_top(true) .visible_on_all_workspaces(true) @@ -282,10 +252,8 @@ impl CapWindow { .maximized(false) .resizable(false) .fullscreen(false) - .decorations(false) .shadow(true) .always_on_top(true) - .transparent(true) .visible_on_all_workspaces(true) .content_protected(true) .inner_size(width, height) @@ -294,6 +262,7 @@ impl CapWindow { (monitor.size().height as f64) / monitor.scale_factor() - height - 120.0, ) .visible(false) + .theme(Some(tauri::Theme::Dark)) .build()? } Self::PrevRecordings => { @@ -365,18 +334,23 @@ impl CapWindow { let mut builder = WebviewWindow::builder(app, id.label(), WebviewUrl::App(url.into())) .title(id.title()) .visible(false) - .accept_first_mouse(true); + .accept_first_mouse(true) + .shadow(true); #[cfg(target_os = "macos")] { if id.traffic_lights_position().is_some() { builder = builder .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .shadow(true); + .title_bar_style(tauri::TitleBarStyle::Overlay); } } + #[cfg(target_os = "windows")] + { + builder = builder.decorations(false); + } + builder } @@ -406,7 +380,7 @@ fn add_traffic_lights(window: &WebviewWindow, controls_inset: Option, controls_inset: Option, controls_inset: Option) { + #[cfg(target_os = "macos")] + { + use crate::platform::delegates::{position_window_controls, UnsafeWindowHandle}; + let c_win = window.clone(); + + window + .run_on_main_thread(move || { + position_window_controls( + UnsafeWindowHandle( + c_win + .ns_window() + .expect("Failed to get native window handle"), + ), + &controls_inset + .map(LogicalPosition::from) + .unwrap_or(DEFAULT_TRAFFIC_LIGHTS_INSET), + ); + }) + .ok(); + } +} diff --git a/apps/desktop/src/components/Header.tsx b/apps/desktop/src/components/Header.tsx deleted file mode 100644 index 381b7b6b..00000000 --- a/apps/desktop/src/components/Header.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function Header() { - return ( -
- ); -} diff --git a/apps/desktop/src/components/titlebar/Titlebar.tsx b/apps/desktop/src/components/titlebar/Titlebar.tsx new file mode 100644 index 00000000..7f40448e --- /dev/null +++ b/apps/desktop/src/components/titlebar/Titlebar.tsx @@ -0,0 +1,59 @@ +// Credits: tauri-controls +import { ComponentProps, Match, splitProps, Switch } from "solid-js"; +import { type } from "@tauri-apps/plugin-os"; +import CaptionControlsWindows11 from "./controls/CaptionControlsWindows11"; +import titlebarState from "~/utils/titlebar-state"; +import { cx } from "cva"; + +export default function Titlebar() { + function left() { + if (titlebarState.order === "platform") return type() === "macos"; + return titlebarState.order === "left"; + } + + return ( +
+ {left() ? ( + <> + +
{titlebarState.items}
+ + ) : ( + <> + {titlebarState.items} + + + )} +
+ ); +} + +function WindowControls(props: ComponentProps<"div">) { + const [local, otherProps] = splitProps(props, ["class"]); + const ostype = type(); + + return ( + + + + + +
+
+
+ ); +} diff --git a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx new file mode 100644 index 00000000..7ccdd6be --- /dev/null +++ b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx @@ -0,0 +1,141 @@ +import { ComponentProps, JSX, Show, splitProps } from "solid-js"; +import { WindowControlButton as ControlButton } from "./WindowControlButton"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import titlebarState from "~/utils/titlebar-state"; +import { cx } from "cva"; + +export default function (props: ComponentProps<"div">) { + const [local, otherProps] = splitProps(props, ["class"]); + const window = getCurrentWindow(); + + return ( +
+ *]:opacity-30" + )} + > + + + + *]:opacity-30" + )} + > + {titlebarState.maximized ? ( + + ) : ( + + )} + + + *]:opacity-30" + )} + > + + +
+ ); +} + +const icons = { + minimizeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( + + + + ), + maximizeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( + + + + ), + maximizeRestoreWin: ( + props: JSX.IntrinsicAttributes & ComponentProps<"svg"> + ) => ( + + + + ), + closeWin: (props: JSX.IntrinsicAttributes & ComponentProps<"svg">) => ( + + + + ), +}; diff --git a/apps/desktop/src/components/titlebar/controls/WindowControlButton.tsx b/apps/desktop/src/components/titlebar/controls/WindowControlButton.tsx new file mode 100644 index 00000000..1809a51c --- /dev/null +++ b/apps/desktop/src/components/titlebar/controls/WindowControlButton.tsx @@ -0,0 +1,14 @@ +import { splitProps, type ComponentProps } from "solid-js"; + +export function WindowControlButton(props: ComponentProps<"button">) { + const [local, otherProps] = splitProps(props, ["class", "children"]); + + return ( + + ); +} diff --git a/apps/desktop/src/routes/(window-chrome).tsx b/apps/desktop/src/routes/(window-chrome).tsx index 954737e5..fd700f5a 100644 --- a/apps/desktop/src/routes/(window-chrome).tsx +++ b/apps/desktop/src/routes/(window-chrome).tsx @@ -1,11 +1,19 @@ import type { RouteSectionProps } from "@solidjs/router"; -import { onMount, ParentProps, Suspense } from "solid-js"; +import { + createEffect, + onCleanup, + onMount, + ParentProps, + Suspense, +} from "solid-js"; import { getCurrentWindow } from "@tauri-apps/api/window"; -import { Transition } from "solid-transition-group"; -import { Show } from "solid-js"; +import { initializeTitlebar } from "~/utils/titlebar-state"; + +import Titlebar from "~/components/titlebar/Titlebar"; -import Header from "../components/Header"; import { AbsoluteInsetLoader } from "~/components/Loader"; +import { type as ostype } from "@tauri-apps/plugin-os"; +import { commands } from "~/utils/tauri"; export const route = { info: { @@ -14,13 +22,21 @@ export const route = { }; export default function (props: RouteSectionProps) { - onMount(() => { + let unlistenResize: () => void | undefined; + + onMount(async () => { + unlistenResize = await initializeTitlebar(); + if (ostype() === "macos") commands.positionTrafficLights(null); if (location.pathname === "/") getCurrentWindow().show(); }); + onCleanup(() => { + unlistenResize?.(); + }); + return ( -
-
+
+ {/* breaks sometimes */} {/* */} + > */} }> {props.children} @@ -43,7 +59,7 @@ function Inner(props: ParentProps) { }); return ( -
+
{props.children}
); diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index 9a91bcb0..586475bf 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -47,11 +47,12 @@ import { const getAuth = cache(async () => { const value = await authStore.get(); - if (!value) return redirect("/signin"); + if (!value && !import.meta.env.TAURI_ENV_DEBUG) return redirect("/signin"); const res = await fetch(`${clientEnv.VITE_SERVER_URL}/api/desktop/plan`, { - headers: { authorization: `Bearer ${value.token}` }, + headers: { authorization: `Bearer ${value?.token}` }, }); - if (res.status !== 200) return redirect("/signin"); + if (res.status !== 200 && !import.meta.env.TAURI_ENV_DEBUG) + return redirect("/signin"); return value; }, "getAuth"); @@ -99,69 +100,70 @@ export default function () { }); } } + + setTitlebar("hideMaximize", true); + setTitlebar( + "items", +
+ + +
+ ); }); return ( - <> -
-
- -
- -
-
+
+
+ +
- -
-
- - -
- - - -
- - -
- + + + - + + Open Cap on Web + +
); } @@ -189,6 +191,8 @@ function useRequestPermission() { import * as dialog from "@tauri-apps/plugin-dialog"; import * as updater from "@tauri-apps/plugin-updater"; import { makePersisted } from "@solid-primitives/storage"; +import titlebarState, { setTitlebar } from "~/utils/titlebar-state"; +import { type as ostype } from "@tauri-apps/plugin-os"; let hasChecked = false; function createUpdateCheck() { @@ -487,6 +491,15 @@ function TargetSelect(props: { optionsEmptyText: string; placeholder: string; }) { + createEffect(() => { + const v = props.value; + if (!v) return; + + if (!props.options.some((o) => o.id === v.id)) { + props.onChange(props.options[0] ?? null); + } + }); + return ( options={props.options ?? []} @@ -511,7 +524,7 @@ function TargetSelect(props: { > as={ - props.options.length === 1 + props.options.length <= 1 ? (p) => ( ) : undefined @@ -657,11 +670,11 @@ function ChangelogButton() { return ( diff --git a/apps/desktop/src/routes/(window-chrome)/settings.tsx b/apps/desktop/src/routes/(window-chrome)/settings.tsx index df5eb169..ba824426 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings.tsx @@ -13,7 +13,7 @@ export default function Settings(props: RouteSectionProps) { const [version] = createResource(() => getVersion()); return ( -
+
-
+
{props.children}
diff --git a/apps/desktop/src/routes/(window-chrome)/setup.tsx b/apps/desktop/src/routes/(window-chrome)/setup.tsx index 2852caa0..e42ee3e3 100644 --- a/apps/desktop/src/routes/(window-chrome)/setup.tsx +++ b/apps/desktop/src/routes/(window-chrome)/setup.tsx @@ -15,6 +15,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; import { commands, OSPermission, type OSPermissionStatus } from "~/utils/tauri"; import { makePersisted } from "@solid-primitives/storage"; import { createStore } from "solid-js/store"; +import { setTitlebar } from "~/utils/titlebar-state"; function isPermitted(status?: OSPermissionStatus): boolean { return status === "granted" || status === "notNeeded"; @@ -71,71 +72,76 @@ export default function () { ); return ( -
- {showStartup() && ( - { - showStartupActions.mutate(false); - }} - /> - )} -
- -

Permissions Required

-

Cap needs permissions to run properly.

-
+ <> +
+ {showStartup() && ( + { + showStartupActions.mutate(false); + }} + /> + )} +
+ +

Permissions Required

+

Cap needs permissions to run properly.

+
-
    - - {(permission) => { - const permissionCheck = () => check()?.[permission.key]; - - return ( - -
  • -
    - - {permission.name} Permission - - {permission.description} -
    - -
  • -
    - ); +
      + + {(permission) => { + const permissionCheck = () => check()?.[permission.key]; + + return ( + +
    • +
      + + {permission.name} Permission + + + {permission.description} + +
      + +
    • +
      + ); + }} +
      +
    + +
- - -
+ > + Continue to Cap + +
+ ); } @@ -147,6 +153,7 @@ import startupAudio from "../../assets/tears-and-fireflies-adi-goldstein.mp3"; import { generalSettingsStore } from "~/store"; import { Portal } from "solid-js/web"; import { cx } from "cva"; +import { type as ostype } from "@tauri-apps/plugin-os"; function Startup(props: { onClose: () => void }) { const [audioState, setAudioState] = makePersisted( @@ -249,6 +256,32 @@ function Startup(props: { onClose: () => void }) { audio.muted = audioState.isMuted; }; + setTitlebar("transparent", true); + setTitlebar("border", false); + setTitlebar("height", "50px"); + setTitlebar( + "items", +
+ +
+ ); + + onCleanup(() => setTitlebar("items", null)); + return (
@@ -354,19 +387,6 @@ function Startup(props: { onClose: () => void }) { >
- - {/* Floating clouds */}
+ <>
-
-
- - +
+
+
+ + +
+
- +
- -
+ ); } diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index 14fbc274..5fdf572b 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -1,7 +1,14 @@ import { Button } from "@cap/ui-solid"; import { cx } from "cva"; -import { Match, Show, Switch, createResource } from "solid-js"; -import { platform } from "@tauri-apps/plugin-os"; +import { + Match, + Show, + Switch, + createResource, + onCleanup, + onMount, +} from "solid-js"; +import { type as ostype } from "@tauri-apps/plugin-os"; import { createStore, reconcile } from "solid-js/store"; import { type RenderProgress, commands } from "~/utils/tauri"; @@ -10,32 +17,47 @@ import { useEditorContext } from "./context"; import { Dialog, DialogContent } from "./ui"; export function Header() { - const [os] = createResource(() => platform()); + let unlistenTitlebar: () => void | undefined; - return ( -
{ + unlistenTitlebar = await initializeTitlebar(); + commands.positionTrafficLights([20.0, 48.0]); + }); + + onCleanup(() => { + unlistenTitlebar?.(); + }); + + setTitlebar("border", false); + setTitlebar("height", "4.5rem"); + setTitlebar( + "items", +
-
+
); + + return ; } import { Channel } from "@tauri-apps/api/core"; import { save } from "@tauri-apps/plugin-dialog"; import { DEFAULT_PROJECT_CONFIG } from "./projectConfig"; import { createMutation } from "@tanstack/solid-query"; +import Titlebar from "~/components/titlebar/Titlebar"; +import { initializeTitlebar, setTitlebar } from "~/utils/titlebar-state"; function ExportButton() { const { videoId, project, prettyName } = useEditorContext(); diff --git a/apps/desktop/src/routes/in-progress-recording.tsx b/apps/desktop/src/routes/in-progress-recording.tsx index 1ba34d7a..751d9d56 100644 --- a/apps/desktop/src/routes/in-progress-recording.tsx +++ b/apps/desktop/src/routes/in-progress-recording.tsx @@ -53,7 +53,7 @@ export default function () { return (
diff --git a/apps/desktop/src/routes/prev-recordings.tsx b/apps/desktop/src/routes/prev-recordings.tsx index 8b512d99..24a2ca35 100644 --- a/apps/desktop/src/routes/prev-recordings.tsx +++ b/apps/desktop/src/routes/prev-recordings.tsx @@ -82,7 +82,9 @@ export default function () { const allMedia = createMemo(() => [...recordings, ...screenshots]); return ( -
+
> { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } +}, +async positionTrafficLights(controlsInset: [number, number] | null) : Promise { + await TAURI_INVOKE("position_traffic_lights", { controlsInset }); } } diff --git a/apps/desktop/src/utils/titlebar-state.ts b/apps/desktop/src/utils/titlebar-state.ts new file mode 100644 index 00000000..90c04841 --- /dev/null +++ b/apps/desktop/src/utils/titlebar-state.ts @@ -0,0 +1,55 @@ +import { createStore } from "solid-js/store"; +import type { JSX } from "solid-js"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { commands } from "./tauri"; + +export interface TitlebarState { + height: string; + hideMaximize: boolean; + order: "right" | "left" | "platform"; + items?: JSX.Element; + maximized: boolean; + maximizable: boolean; + minimizable: boolean; + closable: boolean; + border: boolean; + transparent: boolean; + theme: "light" | "dark"; +} + +const [state, setState] = createStore({ + height: "36px", + hideMaximize: false, + order: "platform", + items: null, + maximized: false, + maximizable: false, + minimizable: true, + closable: true, + border: true, + transparent: false, + theme: "light", +}); + +async function initializeTitlebar() { + const currentWindow = getCurrentWindow(); + + const [maximized, maximizable, closable] = await Promise.all([ + currentWindow.isMaximized(), + currentWindow.isMaximizable(), + currentWindow.isClosable(), + ]); + + commands.positionTrafficLights(null); + + setState({ maximized, maximizable, closable }); + + return await currentWindow.onResized(() => { + currentWindow.isMaximized().then((maximized) => { + setState("maximized", maximized); + }); + }); +} + +export { setState as setTitlebar, initializeTitlebar }; +export default state; diff --git a/crates/ffmpeg-cli/src/lib.rs b/crates/ffmpeg-cli/src/lib.rs index 268d8c16..8b69dee6 100644 --- a/crates/ffmpeg-cli/src/lib.rs +++ b/crates/ffmpeg-cli/src/lib.rs @@ -181,8 +181,6 @@ impl ApplyFFmpegArgs for FFmpegRawVideoInput { command.args(["-r", &self.fps.to_string()]); } - dbg!(PathBuf::from(&self.input).exists()); - // if self.offset != 0.0 { // command.args(["-itsoffset", &self.offset.to_string()]); // } diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 046b0588..5477a10b 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -25,10 +25,6 @@ specta.workspace = true tempfile = "3.12.0" thiserror = "1.0" tracing = "0.1" -windows = { version = "0.52", features = [ - "Win32_Media_MediaFoundation", - "Win32_System_Com", -] } futures = "0.3.31" [target.'cfg(target_os = "macos")'.dependencies] @@ -43,9 +39,14 @@ cidre = { git = "https://github.com/yury/cidre", rev = "1e008bec49a0f97aeaaea613 screencapturekit = "0.2.8" [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.52.0", features = [ +windows = { workspace = true, features = [ "Win32_Foundation", "Win32_System", - "Win32_UI_WindowsAndMessaging", + "Win32_System_Threading", + "Win32_Graphics_Gdi", "Win32_Graphics_Dwm", + "Win32_UI_WindowsAndMessaging", + "Win32_UI_HiDpi", + "Win32_Media_MediaFoundation", ] } +windows-capture = { workspace = true } diff --git a/crates/media/src/data.rs b/crates/media/src/data.rs index 131ce119..0ad0222d 100644 --- a/crates/media/src/data.rs +++ b/crates/media/src/data.rs @@ -19,6 +19,7 @@ pub enum RawVideoFormat { Nv12, Gray, YUYV420, + Rgba, } pub fn ffmpeg_sample_format_for(sample_format: SampleFormat) -> Option { @@ -202,6 +203,7 @@ impl VideoInfo { RawVideoFormat::Nv12 => Pixel::NV12, RawVideoFormat::Gray => Pixel::GRAY8, RawVideoFormat::YUYV420 => Pixel::YUV420P, + RawVideoFormat::Rgba => Pixel::RGBA, }, width, height, diff --git a/crates/media/src/feeds/camera.rs b/crates/media/src/feeds/camera.rs index 12c3baf2..7bef973d 100644 --- a/crates/media/src/feeds/camera.rs +++ b/crates/media/src/feeds/camera.rs @@ -79,49 +79,14 @@ impl CameraFeed { } pub fn list_cameras() -> Vec { - #[cfg(target_os = "windows")] - { - use windows::Win32::Media::MediaFoundation::{MFStartup, MFSTARTUP_FULL}; - use windows::Win32::System::Com::{CoInitializeEx, COINIT_MULTITHREADED}; - - // On Windows, wrap the entire operation in a catch_unwind to prevent crashes - match std::panic::catch_unwind(|| { - // Initialize COM and Media Foundation - unsafe { - let _ = CoInitializeEx(None, COINIT_MULTITHREADED); - let _ = MFStartup(0x20070, MFSTARTUP_FULL); - } - - match nokhwa::query(ApiBackend::Auto) { - Ok(cameras) => cameras - .into_iter() - .map(|i| i.human_name().to_string()) - .collect::>(), - Err(e) => { - error!("Failed to query cameras: {}", e); - Vec::new() - } - } - }) { - Ok(cameras) => cameras, - Err(e) => { - error!("Camera query panicked: {:?}", e); - Vec::new() - } - } - } - - #[cfg(not(target_os = "windows"))] - { - match nokhwa::query(ApiBackend::Auto) { - Ok(cameras) => cameras - .into_iter() - .map(|i| i.human_name().to_string()) - .collect(), - Err(e) => { - error!("Failed to query cameras: {}", e); - Vec::new() - } + match nokhwa::query(ApiBackend::Auto) { + Ok(cameras) => cameras + .into_iter() + .map(|i| i.human_name().to_string()) + .collect::>(), + Err(e) => { + eprintln!("Failed to query cameras: {}", e); + Vec::new() } } } @@ -316,6 +281,19 @@ fn run_camera_feed( // Actual data capture match camera.frame() { Ok(raw_buffer) => { + let raw_buffer = if let FrameFormat::MJPEG = raw_buffer.source_frame_format() { + let rgba_buffer = raw_buffer + .decode_image::() + .unwrap(); + nokhwa::Buffer::new_from_cow( + raw_buffer.resolution(), + rgba_buffer.into_vec().into(), + FrameFormat::MJPEG, + ) + } else { + raw_buffer + }; + let converter = converter.get_or_insert_with(|| { let mut format = camera.camera_format(); format.set_format(raw_buffer.source_frame_format()); @@ -382,7 +360,7 @@ pub enum HwConverter { impl FrameConverter { fn build(camera_format: CameraFormat) -> Self { let format = match camera_format.format() { - FrameFormat::MJPEG => RawVideoFormat::Mjpeg, + FrameFormat::MJPEG => RawVideoFormat::Rgba, FrameFormat::YUYV => RawVideoFormat::Uyvy, FrameFormat::NV12 => RawVideoFormat::Nv12, FrameFormat::GRAY => RawVideoFormat::Gray, diff --git a/crates/media/src/platform/macos.rs b/crates/media/src/platform/macos.rs index 6c6e8880..36d3ac43 100644 --- a/crates/media/src/platform/macos.rs +++ b/crates/media/src/platform/macos.rs @@ -251,7 +251,7 @@ pub fn bring_window_to_focus(window_id: u32) { } } -pub fn window_names() -> HashMap { +pub fn display_names() -> HashMap { use cocoa::appkit::NSScreen; use cocoa::base::nil; use cocoa::foundation::{NSArray, NSString}; diff --git a/crates/media/src/platform/win.rs b/crates/media/src/platform/win.rs index ffbecf35..cd08f2ac 100644 --- a/crates/media/src/platform/win.rs +++ b/crates/media/src/platform/win.rs @@ -1,16 +1,20 @@ use std::collections::HashMap; -use std::ffi::OsString; +use std::ffi::{c_void, OsString}; use std::os::windows::ffi::OsStringExt; use std::path::PathBuf; use super::{Bounds, CursorShape, Window}; -use windows::core::PCWSTR; +use windows::core::{PCWSTR, PWSTR}; +use windows::Win32::Devices::Display::{ + DisplayConfigGetDeviceInfo, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, + DISPLAYCONFIG_DEVICE_INFO_HEADER, DISPLAYCONFIG_SOURCE_DEVICE_NAME, +}; use windows::Win32::Foundation::{CloseHandle, BOOL, FALSE, HWND, LPARAM, RECT, TRUE}; use windows::Win32::Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED}; use windows::Win32::Graphics::Gdi::{ EnumDisplayDevicesW, EnumDisplayMonitors, GetMonitorInfoW, DISPLAY_DEVICEW, HDC, HMONITOR, - MONITORINFO, MONITORINFOEXW, + MONITORINFOEXW, }; use windows::Win32::System::Threading::{ OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_FORMAT, PROCESS_QUERY_LIMITED_INFORMATION, @@ -26,7 +30,7 @@ use windows::Win32::UI::WindowsAndMessaging::{ #[inline] pub fn bring_window_to_focus(window_id: u32) { - let _ = unsafe { SetForegroundWindow(HWND(window_id as isize)) }; + let _ = unsafe { SetForegroundWindow(HWND(window_id as *mut c_void)) }; } pub fn get_cursor_shape(cursors: &DefaultCursors) -> CursorShape { @@ -62,28 +66,28 @@ pub fn get_cursor_shape(cursors: &DefaultCursors) -> CursorShape { /// Keeps handles to default cursor. /// Read more: [MS Doc - About Cursors](https://learn.microsoft.com/en-us/windows/win32/menurc/about-cursors) pub struct DefaultCursors { - arrow: isize, - ibeam: isize, - wait: isize, - cross: isize, - up_arrow: isize, - size_nwse: isize, - size_nesw: isize, - size_we: isize, - size_ns: isize, - size_all: isize, - no: isize, - hand: isize, - appstarting: isize, - help: isize, - pin: isize, - person: isize, + arrow: *mut c_void, + ibeam: *mut c_void, + wait: *mut c_void, + cross: *mut c_void, + up_arrow: *mut c_void, + size_nwse: *mut c_void, + size_nesw: *mut c_void, + size_we: *mut c_void, + size_ns: *mut c_void, + size_all: *mut c_void, + no: *mut c_void, + hand: *mut c_void, + appstarting: *mut c_void, + help: *mut c_void, + pin: *mut c_void, + person: *mut c_void, } impl Default for DefaultCursors { fn default() -> Self { #[inline] - fn load_cursor(lpcursorname: PCWSTR) -> isize { + fn load_cursor(lpcursorname: PCWSTR) -> *mut c_void { unsafe { LoadCursorW(None, lpcursorname) } .expect("Failed to load default system cursors") .0 @@ -112,7 +116,7 @@ impl Default for DefaultCursors { unsafe fn pid_to_exe_path(pid: u32) -> Result { let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid)?; - if handle.is_invalid() || handle.0 == 0 { + if handle.is_invalid() { tracing::error!("Invalid PID {}", pid); } let mut lpexename = [0u16; 1024]; @@ -135,7 +139,7 @@ pub fn get_on_screen_windows() -> Vec { let mut windows = Vec::::new(); unsafe extern "system" fn enum_window_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { - if hwnd.0 == 0 { + if hwnd.is_invalid() { return TRUE; } let windows = &mut *(lparam.0 as *mut Vec); @@ -229,100 +233,74 @@ pub fn get_on_screen_windows() -> Vec { } pub fn monitor_bounds(id: u32) -> Bounds { - let bounds = Bounds::default(); - let idx = 0u32; - let lparams = (id, idx, bounds); - unsafe extern "system" fn enum_monitor_proc( + let bounds = None::; + + unsafe extern "system" fn monitor_enum_proc( hmonitor: HMONITOR, _hdc: HDC, _lprc_clip: *mut RECT, lparam: LPARAM, ) -> BOOL { - let (target_id, idx, bounds) = &mut *(lparam.0 as *mut (u32, u32, Bounds)); + let (target_id, bounds) = &mut *(lparam.0 as *mut (u32, Option)); let mut minfo = MONITORINFOEXW::default(); - minfo.monitorInfo.cbSize = std::mem::size_of::() as u32; - if !GetMonitorInfoW( - hmonitor, - &mut minfo as *mut MONITORINFOEXW as *mut MONITORINFO, + minfo.monitorInfo.cbSize = std::mem::size_of::() as u32; + if !GetMonitorInfoW(hmonitor, &mut minfo as *mut MONITORINFOEXW as *mut _).as_bool() { + return TRUE; + } + + let mut display_device = DISPLAY_DEVICEW::default(); + display_device.cb = std::mem::size_of::() as u32; + + if !EnumDisplayDevicesW( + PWSTR(minfo.szDevice.as_ptr() as _), + 0, + &mut display_device, + 0, ) .as_bool() { return TRUE; - }; - - *idx += 1; - if idx != target_id { - return TRUE; } - let mi = minfo.monitorInfo; - *bounds = Bounds { - x: mi.rcMonitor.left as f64, - y: mi.rcMonitor.top as f64, - width: (mi.rcMonitor.right - mi.rcMonitor.left) as f64, - height: (mi.rcMonitor.bottom - mi.rcMonitor.top) as f64, - }; - - FALSE + let id = display_device.StateFlags as u32; + + if id == *target_id { + let rect = minfo.monitorInfo.rcMonitor; + *bounds = Some(Bounds { + x: rect.left as f64, + y: rect.top as f64, + width: (rect.right - rect.left) as f64, + height: (rect.bottom - rect.top) as f64, + }); + return FALSE; + } + TRUE } + let mut lparams = (id, bounds); let _ = unsafe { EnumDisplayMonitors( None, None, - Some(enum_monitor_proc), - LPARAM(std::ptr::addr_of!(lparams) as isize), - ); + Some(monitor_enum_proc), + LPARAM(core::ptr::addr_of_mut!(lparams) as isize), + ) }; - bounds + + bounds.unwrap_or_default() } -pub fn window_names() -> HashMap { +pub fn display_names() -> HashMap { let mut names = HashMap::new(); - unsafe extern "system" fn monitor_enum_proc( - hmonitor: HMONITOR, - _hdc: HDC, - _lprc_clip: *mut RECT, - lparam: LPARAM, - ) -> BOOL { - let monitors = &mut *(lparam.0 as *mut HashMap); - - let mut minfo = MONITORINFOEXW::default(); - minfo.monitorInfo.cbSize = std::mem::size_of::() as u32; - if !GetMonitorInfoW( - hmonitor, - &mut minfo as *mut MONITORINFOEXW as *mut MONITORINFO, - ) - .as_bool() - { - return TRUE; - }; - - let mut display_device = DISPLAY_DEVICEW::default(); - display_device.cb = std::mem::size_of::() as u32; - if !EnumDisplayDevicesW(PCWSTR(minfo.szDevice.as_ptr()), 0, &mut display_device, 0) - .as_bool() - { - return TRUE; + for window in windows_capture::monitor::Monitor::enumerate().unwrap_or_default() { + let Ok(name) = window.device_string() else { + continue; }; - let device_name = OsString::from_wide(&display_device.DeviceName) - .to_string_lossy() - .into_owned(); - let num = monitors.len() as u32; - monitors.insert(num, device_name); - TRUE + names.insert(window.as_raw_hmonitor() as u32, name); } - let _ = unsafe { - EnumDisplayMonitors( - None, - None, - Some(monitor_enum_proc), - LPARAM(core::ptr::addr_of_mut!(names) as isize), - ) - }; names } diff --git a/crates/media/src/sources/audio_input.rs b/crates/media/src/sources/audio_input.rs index 1b478f98..b40a08dd 100644 --- a/crates/media/src/sources/audio_input.rs +++ b/crates/media/src/sources/audio_input.rs @@ -59,8 +59,16 @@ impl AudioInputSource { .supported_input_configs() .map_err(|error| eprintln!("Error: {error}")) .ok() - .and_then(|mut configs| { - configs.find(|c| ffmpeg_sample_format_for(c.sample_format()).is_some()) + .and_then(|configs| { + let mut configs = configs.collect::>(); + configs.sort_by(|a, b| { + b.sample_format() + .sample_size() + .cmp(&a.sample_format().sample_size()) + }); + configs + .into_iter() + .find(|c| ffmpeg_sample_format_for(c.sample_format()).is_some()) }) .and_then(|config| { device @@ -102,6 +110,7 @@ impl AudioInputSource { let sample_format = self.config.sample_format(); let data_callback = move |data: &cpal::Data, info: &cpal::InputCallbackInfo| { + println!("data_callback"); let capture_time = info.timestamp().capture; let Ok(output) = output.try_lock() else { diff --git a/crates/media/src/sources/screen_capture.rs b/crates/media/src/sources/screen_capture.rs index 117b08ba..a29e2f81 100644 --- a/crates/media/src/sources/screen_capture.rs +++ b/crates/media/src/sources/screen_capture.rs @@ -2,7 +2,7 @@ use cap_flags::FLAGS; use flume::Sender; use scap::{ capturer::{get_output_frame_size, Area, Capturer, Options, Point, Resolution, Size}, - frame::FrameType, + frame::{Frame, FrameType}, Target, }; use serde::{Deserialize, Serialize}; @@ -58,10 +58,10 @@ impl PartialEq for ScreenCaptureTarget { } pub struct ScreenCaptureSource { - options: Options, - video_info: VideoInfo, target: ScreenCaptureTarget, - pub bounds: Bounds, + fps: u32, + resolution: Resolution, + video_info: VideoInfo, phantom: std::marker::PhantomData, } @@ -73,20 +73,39 @@ impl ScreenCaptureSource { fps: Option, resolution: Option, ) -> Self { - let fps = fps.unwrap_or(Self::DEFAULT_FPS); let output_resolution = resolution.unwrap_or(Resolution::Captured); + let fps = fps.unwrap_or(Self::DEFAULT_FPS); + + let mut this = Self { + target: capture_target.clone(), + fps, + resolution: output_resolution, + video_info: VideoInfo::from_raw(RawVideoFormat::Bgra, 0, 0, fps), + phantom: Default::default(), + }; + + let options = this.create_options(); + + let [frame_width, frame_height] = get_output_frame_size(&options); + + this.video_info = VideoInfo::from_raw(RawVideoFormat::Bgra, frame_width, frame_height, fps); + + this + } + + fn create_options(&self) -> Options { let targets = dbg!(scap::get_all_targets()); let excluded_targets: Vec = targets .iter() .filter(|target| { matches!(target, Target::Window(scap_window) - if EXCLUDED_WINDOWS.contains(&scap_window.title.as_str())) + if EXCLUDED_WINDOWS.contains(&scap_window.title.as_str())) }) .cloned() .collect(); - let (crop_area, bounds) = match capture_target { + let (crop_area, bounds) = match &self.target { ScreenCaptureTarget::Window(capture_window) => ( Some(Area { size: Size { @@ -105,7 +124,7 @@ impl ScreenCaptureSource { } }; - let target = match capture_target { + let target = match &self.target { ScreenCaptureTarget::Window(w) => None, ScreenCaptureTarget::Screen(capture_screen) => targets .iter() @@ -116,26 +135,20 @@ impl ScreenCaptureSource { .cloned(), }; - let options = Options { - fps, + Options { + fps: self.fps, show_cursor: !FLAGS.zoom, show_highlight: true, excluded_targets: Some(excluded_targets), - output_type: FrameType::YUVFrame, - output_resolution, + output_type: if cfg!(windows) { + FrameType::BGRAFrame + } else { + FrameType::YUVFrame + }, + output_resolution: self.resolution, crop_area, target, ..Default::default() - }; - - let [frame_width, frame_height] = get_output_frame_size(&options); - - Self { - options, - target: capture_target.clone(), - bounds, - video_info: VideoInfo::from_raw(RawVideoFormat::Nv12, frame_width, frame_height, fps), - phantom: Default::default(), } } @@ -145,20 +158,21 @@ impl ScreenCaptureSource { } let mut targets = vec![]; - let screens = scap::get_all_targets().into_iter().filter_map(|t| match t { - Target::Display(screen) => Some(screen), - _ => None, - }); + let screens = scap::get_all_targets() + .into_iter() + .filter_map(|t| match t { + Target::Display(screen) => Some(screen), + _ => None, + }) + .collect::>(); - let names = crate::platform::window_names(); + let names = crate::platform::display_names(); for (idx, screen) in screens.into_iter().enumerate() { - // Handle Target::Screen variant (assuming this is how it's structured in scap) - #[cfg(target_os = "macos")] targets.push(CaptureScreen { id: screen.id, name: names - .get(&screen.raw_handle.id) + .get(&screen.id) .cloned() .unwrap_or_else(|| format!("Screen {}", idx + 1)), }); @@ -218,11 +232,13 @@ impl PipelineSourceTask for ScreenCaptureSource { ) { println!("Preparing screen capture source thread..."); + let options = self.create_options(); + let maybe_capture_window_id = match &self.target { ScreenCaptureTarget::Window(window) => Some(window.id), _ => None, }; - let mut capturer = Capturer::new(dbg!(self.options.clone())); + let mut capturer = Capturer::new(dbg!(options)); let mut capturing = false; ready_signal.send(Ok(())).unwrap(); @@ -239,47 +255,66 @@ impl PipelineSourceTask for ScreenCaptureSource { println!("Screen recording started."); } - match capturer.raw().get_next_pixel_buffer() { - Ok(pixel_buffer) => { - if pixel_buffer.height() == 0 || pixel_buffer.width() == 0 { + match capturer.get_next_frame() { + Ok(Frame::BGRA(frame)) => { + if frame.height == 0 || frame.width == 0 { continue; } - let raw_timestamp = RawNanoseconds(pixel_buffer.display_time()); + let raw_timestamp = RawNanoseconds(frame.display_time); match clock.timestamp_for(raw_timestamp) { None => { eprintln!("Clock is currently stopped. Dropping frames."); } Some(timestamp) => { - let mut frame = FFVideo::new( + let mut buffer = FFVideo::new( self.video_info.pixel_format, self.video_info.width, self.video_info.height, ); - frame.set_pts(Some(timestamp)); + buffer.set_pts(Some(timestamp)); + + let bytes_per_pixel = 4; + let width_in_bytes = frame.width as usize * bytes_per_pixel; + let height = frame.height as usize; + + let src_data = &frame.data; - let planes = pixel_buffer.planes(); + let src_stride = src_data.len() / height; + let dst_stride = buffer.stride(0); + + if src_data.len() < src_stride * height { + eprintln!("Frame data size mismatch."); + continue; + } - for (i, plane) in planes.into_iter().enumerate() { - let data = plane.data(); + if src_stride < width_in_bytes { + eprintln!( + "Source stride is less than expected width in bytes." + ); + continue; + } - for y in 0..plane.height() { - let buffer_y_offset = y * plane.bytes_per_row(); - let frame_y_offset = y * frame.stride(i); + if buffer.data(0).len() < dst_stride * height { + eprintln!("Destination data size mismatch."); + continue; + } - let num_bytes = - frame.stride(i).min(plane.bytes_per_row()); + { + let dst_data = buffer.data_mut(0); - frame.data_mut(i) - [frame_y_offset..frame_y_offset + num_bytes] + for y in 0..height { + let src_offset = y * src_stride; + let dst_offset = y * dst_stride; + dst_data[dst_offset..dst_offset + width_in_bytes] .copy_from_slice( - &data[buffer_y_offset - ..buffer_y_offset + num_bytes], + &src_data + [src_offset..src_offset + width_in_bytes], ); } } - if let Err(_) = output.send(frame) { + if let Err(_) = output.send(buffer) { eprintln!( "Pipeline is unreachable. Shutting down recording." ); @@ -288,6 +323,7 @@ impl PipelineSourceTask for ScreenCaptureSource { } }; } + Ok(_) => unreachable!(), Err(error) => { eprintln!("Capture error: {error}"); break; @@ -315,7 +351,6 @@ impl PipelineSourceTask for ScreenCaptureSource { } } -#[cfg(target_os = "macos")] pub struct CMSampleBufferCapture; #[cfg(target_os = "macos")] @@ -336,7 +371,7 @@ impl PipelineSourceTask for ScreenCaptureSource { ScreenCaptureTarget::Window(window) => Some(window.id), _ => None, }; - let mut capturer = Capturer::new(dbg!(self.options.clone())); + let mut capturer = Capturer::new(dbg!(self.create_options())); let mut capturing = false; ready_signal.send(Ok(())).unwrap(); diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index fb4186db..644dab0f 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -17,3 +17,7 @@ windows = { version = "0.58.0", features = [ "Win32_System_Pipes", "Win32_System_Diagnostics_Debug", ] } + +[dependencies] +tokio = { workspace = true, features = ["net"] } +uuid = "1.11.0" diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index a209962d..42de8d9b 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,14 +1,4 @@ -#[cfg(windows)] -use tokio::net::windows::named_pipe; - -#[cfg(unix)] -pub fn create_named_pipe(path: &std::path::Path) -> Result<(), Box> { - use nix::sys::stat; - use nix::unistd; - std::fs::remove_file(path).ok(); - unistd::mkfifo(path, stat::Mode::S_IRWXU)?; - Ok(()) -} +use std::{ffi::OsString, path::PathBuf}; #[cfg(windows)] pub fn get_last_win32_error_formatted() -> String { @@ -41,8 +31,67 @@ pub fn format_error_message(error_code: u32) -> String { } } -// Windows named pipes must be in the format "\\.\pipe\name" -#[cfg(windows)] -fn named_pipe_to_path(name: &str) -> std::ffi::OsString { - format!(r"\\.\pipe\{}", name).into() +#[cfg(unix)] +fn create_named_pipe(path: &std::path::Path) -> Result<(), Box> { + use nix::sys::stat; + use nix::unistd; + std::fs::remove_file(path).ok(); + unistd::mkfifo(path, stat::Mode::S_IRWXU)?; + Ok(()) +} + +pub fn create_channel_named_pipe( + mut rx: tokio::sync::mpsc::Receiver>, + unix_path: PathBuf, +) -> OsString { + #[cfg(unix)] + { + use std::io::Write; + + create_named_pipe(&unix_path).unwrap(); + + let path = unix_path.clone(); + tokio::spawn(async move { + let mut file = std::fs::File::create(&path).unwrap(); + println!("video pipe opened"); + + while let Some(bytes) = rx.recv().await { + file.write_all(&bytes).unwrap(); + } + + println!("done writing to video pipe"); + }); + + unix_path.into_os_string() + } + + #[cfg(windows)] + { + use tokio::io::AsyncWriteExt; + use tokio::net::windows::named_pipe::ServerOptions; + + let uuid = uuid::Uuid::new_v4(); + let pipe_name = format!(r#"\\.\pipe\{uuid}"#); + + let mut server = ServerOptions::new() + .first_pipe_instance(true) + .create(&pipe_name) + .unwrap(); + + tokio::spawn({ + async move { + println!("video pipe opened"); + + server.connect().await.unwrap(); + + while let Some(bytes) = rx.recv().await { + server.write_all(&bytes).await.unwrap(); + } + + println!("done writing to video pipe"); + } + }); + + pipe_name.into() + } }