From 7352ca0c3dedc7d791da947f94e635cd6d7b70b0 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 14 Aug 2025 11:13:32 +0800 Subject: [PATCH 01/47] integrate cap-displays a bit --- Cargo.lock | 20 +- crates/displays/Cargo.toml | 27 +-- crates/displays/src/lib.rs | 4 + crates/displays/src/platform/macos.rs | 30 +++ crates/displays/src/platform/mod.rs | 12 +- crates/displays/src/platform/win.rs | 22 +- crates/scap-direct3d/Cargo.toml | 6 +- crates/scap-direct3d/examples/cli.rs | 4 +- crates/scap-direct3d/src/lib.rs | 17 +- crates/scap-screencapturekit/Cargo.toml | 1 + crates/scap-screencapturekit/examples/cli.rs | 37 ++-- crates/scap-screencapturekit/src/lib.rs | 2 - crates/scap-screencapturekit/src/targets.rs | 200 ------------------- crates/scap/Cargo.toml | 16 ++ crates/scap/src/lib.rs | 1 + 15 files changed, 138 insertions(+), 261 deletions(-) delete mode 100644 crates/scap-screencapturekit/src/targets.rs create mode 100644 crates/scap/Cargo.toml create mode 100644 crates/scap/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index bc354bfc8d..6a26d3e364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -852,7 +852,7 @@ dependencies = [ "clap", "ffmpeg-next", "flume", - "scap", + "scap 0.1.0-beta.1", "serde", "serde_json", "tokio", @@ -1019,7 +1019,7 @@ dependencies = [ "relative-path", "reqwest", "rodio", - "scap", + "scap 0.1.0-beta.1", "sentry", "serde", "serde_json", @@ -1065,6 +1065,7 @@ dependencies = [ name = "cap-displays" version = "0.1.0" dependencies = [ + "cidre", "cocoa 0.26.1", "core-foundation 0.10.1", "core-graphics 0.24.0", @@ -1180,7 +1181,7 @@ dependencies = [ "objc-foundation", "objc2-foundation 0.2.2", "ringbuf", - "scap", + "scap 0.1.0-beta.1", "screencapturekit", "serde", "specta", @@ -1259,7 +1260,7 @@ dependencies = [ "objc", "objc2-app-kit", "relative-path", - "scap", + "scap 0.1.0-beta.1", "screencapturekit", "serde", "serde_json", @@ -6789,10 +6790,20 @@ dependencies = [ "windows-capture", ] +[[package]] +name = "scap" +version = "0.1.0" +dependencies = [ + "cap-displays", + "scap-direct3d", + "scap-screencapturekit", +] + [[package]] name = "scap-direct3d" version = "0.1.0" dependencies = [ + "cap-displays", "windows 0.60.0", ] @@ -6812,6 +6823,7 @@ dependencies = [ name = "scap-screencapturekit" version = "0.1.0" dependencies = [ + "cap-displays", "cidre", "clap", "futures", diff --git a/crates/displays/Cargo.toml b/crates/displays/Cargo.toml index 8f4ffd5a8a..c04ae22962 100644 --- a/crates/displays/Cargo.toml +++ b/crates/displays/Cargo.toml @@ -12,24 +12,25 @@ specta.workspace = true image = "0.24" [target.'cfg(target_os = "macos")'.dependencies] +cidre = { workspace = true, default-features = false, features = ["sc"] } core-graphics = "0.24.0" core-foundation = "0.10.0" cocoa = "0.26.0" objc = "0.2.7" -[target.'cfg(target_os= "windows")'.dependencies] +# [target.'cfg(target_os= "windows")'.dependencies] windows = { workspace = true, features = [ - "Win32_Foundation", - "Win32_System", - "Win32_System_Threading", - "Win32_System_Registry", - "Win32_System_Com", - "Win32_System_Wmi", - "Win32_UI_WindowsAndMessaging", - "Win32_UI_Shell", - "Win32_UI_HiDpi", - "Win32_Graphics_Gdi", - "Win32_Storage_FileSystem", - "Win32_Devices_Display", + "Win32_Foundation", + "Win32_System", + "Win32_System_Threading", + "Win32_System_Registry", + "Win32_System_Com", + "Win32_System_Wmi", + "Win32_UI_WindowsAndMessaging", + "Win32_UI_Shell", + "Win32_UI_HiDpi", + "Win32_Graphics_Gdi", + "Win32_Storage_FileSystem", + "Win32_Devices_Display", ] } windows-sys = { workspace = true } diff --git a/crates/displays/src/lib.rs b/crates/displays/src/lib.rs index 9ad6b69d26..927c98c3fe 100644 --- a/crates/displays/src/lib.rs +++ b/crates/displays/src/lib.rs @@ -16,6 +16,10 @@ impl Display { DisplayImpl::list().into_iter().map(Self).collect() } + pub fn primary() -> Self { + Self(DisplayImpl::primary()) + } + pub fn raw_handle(&self) -> &DisplayImpl { &self.0 } diff --git a/crates/displays/src/platform/macos.rs b/crates/displays/src/platform/macos.rs index 324036ef1a..3093e7414f 100644 --- a/crates/displays/src/platform/macos.rs +++ b/crates/displays/src/platform/macos.rs @@ -1,5 +1,6 @@ use std::{ffi::c_void, str::FromStr}; +use cidre::{arc, ns, sc}; use core_foundation::{base::FromVoid, number::CFNumber, string::CFString}; use core_graphics::{ display::{ @@ -142,6 +143,35 @@ impl DisplayImpl { } } +impl DisplayImpl { + pub async fn as_sc(&self) -> Option> { + sc::ShareableContent::current() + .await + .ok()? + .displays() + .iter() + .find(|d| d.display_id().0 == self.0.id) + .map(|v| v.retained()) + } + + pub async fn as_content_filter(&self) -> Option> { + self.as_content_filter_excluding_windows(vec![]).await + } + + pub async fn as_content_filter_excluding_windows( + &self, + windows: Vec>, + ) -> Option> { + let excluded_windows = + ns::Array::from_slice_retained(windows.into_iter().collect::>().as_slice()); + + Some(sc::ContentFilter::with_display_excluding_windows( + self.as_sc().await?.as_ref(), + &excluded_windows, + )) + } +} + fn get_cursor_position() -> Option { let event_source = core_graphics::event_source::CGEventSource::new( core_graphics::event_source::CGEventSourceStateID::Private, diff --git a/crates/displays/src/platform/mod.rs b/crates/displays/src/platform/mod.rs index bfe03249bb..c4afca73ed 100644 --- a/crates/displays/src/platform/mod.rs +++ b/crates/displays/src/platform/mod.rs @@ -1,9 +1,9 @@ -#[cfg(target_os = "macos")] -mod macos; -#[cfg(target_os = "macos")] -pub use macos::*; +// #[cfg(target_os = "macos")] +// mod macos; +// #[cfg(target_os = "macos")] +// pub use macos::*; -#[cfg(windows)] +// #[cfg(windows)] mod win; -#[cfg(windows)] +// #[cfg(windows)] pub use win::*; diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs index a09ea60a39..0515721603 100644 --- a/crates/displays/src/platform/win.rs +++ b/crates/displays/src/platform/win.rs @@ -11,13 +11,16 @@ use windows::{ QueryDisplayConfig, }, Foundation::{CloseHandle, HWND, LPARAM, POINT, RECT, TRUE, WIN32_ERROR, WPARAM}, - Graphics::Gdi::{ - BI_RGB, BITMAP, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, - CreateCompatibleDC, CreateSolidBrush, DEVMODEW, DIB_RGB_COLORS, DeleteDC, DeleteObject, - ENUM_CURRENT_SETTINGS, EnumDisplayMonitors, EnumDisplaySettingsW, FillRect, GetDC, - GetDIBits, GetMonitorInfoW, GetObjectA, HBRUSH, HDC, HGDIOBJ, HMONITOR, - MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTONULL, MONITORINFOEXW, MonitorFromPoint, - ReleaseDC, SelectObject, + Graphics::{ + Capture::GraphicsCaptureItem, + Gdi::{ + BI_RGB, BITMAP, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, + CreateCompatibleDC, CreateSolidBrush, DEVMODEW, DIB_RGB_COLORS, DeleteDC, + DeleteObject, ENUM_CURRENT_SETTINGS, EnumDisplayMonitors, EnumDisplaySettingsW, + FillRect, GetDC, GetDIBits, GetMonitorInfoW, GetObjectA, HBRUSH, HDC, HGDIOBJ, + HMONITOR, MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTONULL, MONITORINFOEXW, + MonitorFromPoint, ReleaseDC, SelectObject, + }, }, Storage::FileSystem::{GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW}, System::{ @@ -649,6 +652,11 @@ impl DisplayImpl { } None } + + fn try_as_capture_item(&self) -> windows::core::Result { + let interop = windows::core::factory::()?; + unsafe { interop.CreateForMonitor(self.inner) } + } } fn get_cursor_position() -> Option { diff --git a/crates/scap-direct3d/Cargo.toml b/crates/scap-direct3d/Cargo.toml index c043816f24..4414b0cc1f 100644 --- a/crates/scap-direct3d/Cargo.toml +++ b/crates/scap-direct3d/Cargo.toml @@ -3,7 +3,8 @@ name = "scap-direct3d" version = "0.1.0" edition = "2024" -[target.'cfg(windows)'.dependencies] +# [target.'cfg(windows)'.dependencies] +[dependencies] windows = { workspace = true, features = [ "System", "Graphics_Capture", @@ -19,5 +20,8 @@ windows = { workspace = true, features = [ "Win32_UI_WindowsAndMessaging", ] } +[dev-dependencies] +cap-displays = { path = "../displays" } + [lints] workspace = true diff --git a/crates/scap-direct3d/examples/cli.rs b/crates/scap-direct3d/examples/cli.rs index d7ad710afe..2e8b6233d6 100644 --- a/crates/scap-direct3d/examples/cli.rs +++ b/crates/scap-direct3d/examples/cli.rs @@ -1,8 +1,10 @@ -use scap_direct3d::{Capturer, Display, PixelFormat, Settings}; +use cap_displays::*; +use scap_direct3d::{Capturer, PixelFormat, Settings}; use std::time::Duration; fn main() { let display = Display::primary().unwrap(); + let display = display.raw_handle(); let capturer = Capturer::new( display.try_as_capture_item().unwrap(), diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index 5638fbda36..46d460c038 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -1,6 +1,6 @@ // a whole bunch of credit to https://github.com/NiiightmareXD/windows-capture -#![cfg(windows)] +// #![cfg(windows)] use std::{ os::windows::io::AsRawHandle, @@ -64,11 +64,6 @@ impl PixelFormat { } } -#[derive(Clone)] -pub struct CaptureItem { - inner: GraphicsCaptureItem, -} - pub struct Display { inner: HMONITOR, } @@ -98,12 +93,12 @@ pub struct Settings { } pub struct Capturer { - item: CaptureItem, + item: GraphicsCaptureItem, settings: Settings, } impl Capturer { - pub fn new(item: CaptureItem, settings: Settings) -> Self { + pub fn new(item: GraphicsCaptureItem, settings: Settings) -> Self { Self { item, settings } } @@ -288,7 +283,7 @@ impl<'a> FrameBuffer<'a> { } fn run( - item: CaptureItem, + item: GraphicsCaptureItem, settings: Settings, mut callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, stop_flag: Arc, @@ -340,12 +335,12 @@ fn run( &direct3d_device, PixelFormat::R8G8B8A8Unorm.as_directx(), 1, - item.inner.Size().map_err(|_| "Item size")?, + item.Size().map_err(|_| "Item size")?, ) .map_err(|_| "Failed to create frame pool")?; let session = frame_pool - .CreateCaptureSession(&item.inner) + .CreateCaptureSession(&item) .map_err(|_| "Failed to create capture session")?; if let Some(border_required) = settings.is_border_required { diff --git a/crates/scap-screencapturekit/Cargo.toml b/crates/scap-screencapturekit/Cargo.toml index 87c5ff1199..a202808533 100644 --- a/crates/scap-screencapturekit/Cargo.toml +++ b/crates/scap-screencapturekit/Cargo.toml @@ -20,3 +20,4 @@ workspace = true [dev-dependencies] clap = { version = "4.5.40", features = ["derive"] } inquire = "0.7.5" +cap-displays = { path = "../displays" } diff --git a/crates/scap-screencapturekit/examples/cli.rs b/crates/scap-screencapturekit/examples/cli.rs index 84d2b390c1..c1bd13416d 100644 --- a/crates/scap-screencapturekit/examples/cli.rs +++ b/crates/scap-screencapturekit/examples/cli.rs @@ -1,10 +1,12 @@ +use cap_displays::Display; use std::time::Duration; use futures::executor::block_on; -use scap_screencapturekit::{Capturer, Display, StreamCfgBuilder, Window}; +use scap_screencapturekit::{Capturer, StreamCfgBuilder}; fn main() { - let display = block_on(Display::primary()).expect("Primary display not found"); + let display = Display::primary(); + let display = display.raw_handle(); // let windows = block_on(Window::list()).expect("Failed to list windows"); // let window = windows @@ -14,22 +16,25 @@ fn main() { let config = StreamCfgBuilder::default() .with_fps(60.0) - .with_width(display.width()) - .with_height(display.height()) + .with_width(display.physical_size().width() as usize) + .with_height(display.physical_size().height() as usize) .build(); - let capturer = Capturer::builder(display.as_content_filter(), config) - .with_output_sample_buf_cb(|frame| { - dbg!(frame.output_type()); - // if let Some(image_buf) = buf.image_buf() { - // image_buf.show(); - // } - }) - .with_stop_with_err_cb(|stream, error| { - dbg!(stream, error); - }) - .build() - .expect("Failed to build capturer"); + let capturer = Capturer::builder( + block_on(display.as_content_filter()).expect("Failed to get display as content filter"), + config, + ) + .with_output_sample_buf_cb(|frame| { + dbg!(frame.output_type()); + // if let Some(image_buf) = buf.image_buf() { + // image_buf.show(); + // } + }) + .with_stop_with_err_cb(|stream, error| { + dbg!(stream, error); + }) + .build() + .expect("Failed to build capturer"); block_on(capturer.start()).expect("Failed to start capturing"); diff --git a/crates/scap-screencapturekit/src/lib.rs b/crates/scap-screencapturekit/src/lib.rs index 7db9fac845..8bf42a8849 100644 --- a/crates/scap-screencapturekit/src/lib.rs +++ b/crates/scap-screencapturekit/src/lib.rs @@ -2,8 +2,6 @@ mod capture; mod config; -mod targets; pub use capture::{AudioFrame, Capturer, CapturerBuilder, Frame, VideoFrame}; pub use config::StreamCfgBuilder; -pub use targets::{Display, Window}; diff --git a/crates/scap-screencapturekit/src/targets.rs b/crates/scap-screencapturekit/src/targets.rs deleted file mode 100644 index 39225e5f1d..0000000000 --- a/crates/scap-screencapturekit/src/targets.rs +++ /dev/null @@ -1,200 +0,0 @@ -use cidre::{arc, cg, ns, sc}; -use objc2::{MainThreadMarker, rc::Retained}; -use objc2_app_kit::NSScreen; -use objc2_foundation::{NSArray, NSNumber, ns_string}; - -#[derive(Clone, Debug)] -pub struct Display { - inner: arc::R, - name: String, -} - -impl Display { - pub fn name(&self) -> &String { - &self.name - } - - pub fn inner(&self) -> &sc::Display { - self.inner.as_ref() - } - - /// Logical width of the display in pixels - pub fn width(&self) -> usize { - self.inner().width() as usize - } - - /// Logical height of the display in pixels - pub fn height(&self) -> usize { - self.inner().height() as usize - } - - pub async fn list() -> Result, arc::R> { - let content = sc::ShareableContent::current().await?; - - // SAFETY: NSScreen::screens is callable from any thread - let ns_screens = NSScreen::screens(unsafe { MainThreadMarker::new_unchecked() }); - - let displays = content - .displays() - .iter() - .filter_map(|display| { - let id = display.display_id(); - - let ns_screen = ns_screens.iter().find(|ns_screen| { - ns_screen - .deviceDescription() - .objectForKey(ns_string!("NSScreenNumber")) - .and_then(|v| v.downcast_ref::().map(|v| v.as_u32())) - .map(|v| v == id.0) - .unwrap_or(false) - })?; - - let name = unsafe { ns_screen.localizedName() }.to_string(); - - Some(Self { - inner: display.retained(), - name, - }) - }) - .collect::>(); - - Ok(displays) - } - - pub async fn primary() -> Option { - let id = cg::DirectDisplayId::main(); - - let content = sc::ShareableContent::current().await.ok()?; - - let inner = content - .displays() - .iter() - .find(|d| d.display_id() == id)? - .retained(); - - Some(Self { - inner, - name: get_display_name(id, None)?, - }) - } - - pub async fn from_id(id: cg::DirectDisplayId) -> Option { - let content = sc::ShareableContent::current().await.ok()?; - - let inner = content - .displays() - .iter() - .find(|d| d.display_id() == id)? - .retained(); - - Some(Self { - inner, - name: get_display_name(id, None)?, - }) - } - - pub fn as_content_filter(&self) -> arc::R { - self.as_content_filter_excluding_windows(vec![]) - } - - pub fn as_content_filter_excluding_windows( - &self, - windows: Vec, - ) -> arc::R { - let excluded_windows = ns::Array::from_slice_retained( - windows - .into_iter() - .map(|win| win.inner) - .collect::>() - .as_slice(), - ); - - sc::ContentFilter::with_display_excluding_windows(&self.inner, &excluded_windows) - } -} - -fn get_display_name( - id: cg::DirectDisplayId, - screens: Option>>, -) -> Option { - // SAFETY: NSScreen::screens is callable from any thread - let screens = - screens.unwrap_or_else(|| NSScreen::screens(unsafe { MainThreadMarker::new_unchecked() })); - - let ns_screen = screens.iter().find(|ns_screen| { - ns_screen - .deviceDescription() - .objectForKey(ns_string!("NSScreenNumber")) - .and_then(|v| v.downcast_ref::().map(|v| v.as_u32())) - .map(|v| v == id.0) - .unwrap_or(false) - })?; - - Some(unsafe { ns_screen.localizedName() }.to_string()) -} - -#[derive(Clone, Debug)] -pub struct Window { - inner: arc::R, - title: Option, -} - -impl Window { - pub fn title(&self) -> Option<&String> { - self.title.as_ref() - } - - pub fn inner(&self) -> &sc::Window { - self.inner.as_ref() - } - - /// Logical width of the window in pixels. - pub fn width(&self) -> usize { - self.inner.frame().size.width as usize - } - - /// Logical height of the window in pixels. - pub fn height(&self) -> usize { - self.inner.frame().size.height as usize - } - - fn from_sc(window: &sc::Window) -> Self { - Self { - inner: window.retained(), - title: window.title().map(|s| s.to_string()), - } - } - - pub async fn list() -> Result, arc::R> { - let content = sc::ShareableContent::current().await?; - - let windows = content - .windows() - .iter() - .filter_map(|window| Some(Self::from_sc(window))) - .collect::>(); - - Ok(windows) - } - - pub fn as_content_filter(&self) -> arc::R { - sc::ContentFilter::with_desktop_independent_window(self.inner.as_ref()) - } -} - -#[cfg(debug_assertions)] -mod test { - use super::*; - - fn assert_send() {} - fn assert_send_val(_: T) {} - - #[allow(dead_code)] - fn ensure_send() { - assert_send::(); - assert_send_val(Display::list()); - - assert_send::(); - assert_send_val(Window::list()); - } -} diff --git a/crates/scap/Cargo.toml b/crates/scap/Cargo.toml new file mode 100644 index 0000000000..8ccd783cef --- /dev/null +++ b/crates/scap/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "scap" +version = "0.1.0" +edition = "2024" + +[dependencies] +cap-displays = { path = "../displays" } + +[target.'cfg(windows)'.dependencies] +scap-direct3d = { path = "../scap-direct3d" } + +[target.'cfg(target_os = "macos")'.dependencies] +scap-screencapturekit = { path = "../scap-screencapturekit" } + +[lints] +workspace = true diff --git a/crates/scap/src/lib.rs b/crates/scap/src/lib.rs new file mode 100644 index 0000000000..e1f82b5051 --- /dev/null +++ b/crates/scap/src/lib.rs @@ -0,0 +1 @@ +pub struct Capturer {} From 0dde2c1b33f1d88887d40d6df13b7e981b034fc8 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 14 Aug 2025 14:34:01 +0800 Subject: [PATCH 02/47] start removing scap --- Cargo.lock | 67 +-- Cargo.toml | 4 +- apps/cli/Cargo.toml | 1 - apps/cli/src/record.rs | 2 +- apps/desktop/src-tauri/Cargo.toml | 1 - .../desktop/src-tauri/src/deeplink_actions.rs | 2 +- apps/desktop/src-tauri/src/lib.rs | 4 +- apps/desktop/src-tauri/src/recording.rs | 4 +- crates/camera-avfoundation/examples/cli.rs | 4 +- crates/camera-directshow/Cargo.toml | 12 - crates/camera-windows/Cargo.toml | 6 - crates/camera/src/macos.rs | 4 +- crates/displays/src/platform/mod.rs | 12 +- crates/displays/src/platform/win.rs | 2 +- crates/media/Cargo.toml | 9 +- crates/media/src/pipeline/builder.rs | 3 +- crates/media/src/pipeline/task.rs | 1 - crates/media/src/sources/audio_input.rs | 1 - crates/media/src/sources/audio_mixer.rs | 1 - crates/media/src/sources/camera.rs | 1 - crates/media/src/sources/screen_capture.rs | 394 +++++++++++++++--- crates/recording/Cargo.toml | 1 - crates/recording/src/capture_pipeline.rs | 1 - crates/recording/src/studio_recording.rs | 4 +- crates/scap-direct3d/examples/cli.rs | 11 +- crates/scap-direct3d/src/lib.rs | 14 +- crates/scap-ffmpeg/src/lib.rs | 4 +- crates/scap-screencapturekit/src/capture.rs | 86 ++-- crates/scap/Cargo.toml | 16 - crates/scap/src/lib.rs | 1 - 30 files changed, 456 insertions(+), 217 deletions(-) delete mode 100644 crates/scap/Cargo.toml delete mode 100644 crates/scap/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6a26d3e364..20fb2dc02a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -852,7 +852,6 @@ dependencies = [ "clap", "ffmpeg-next", "flume", - "scap 0.1.0-beta.1", "serde", "serde_json", "tokio", @@ -865,7 +864,7 @@ dependencies = [ name = "cap-audio" version = "0.1.0" dependencies = [ - "cidre", + "cidre 0.11.0", "cpal 0.15.3 (git+https://github.com/RustAudio/cpal?rev=f43d36e55494993bbbde3299af0c53e5cdf4d4cf)", "ffmpeg-next", "tokio", @@ -877,7 +876,7 @@ version = "0.1.0" dependencies = [ "cap-camera-avfoundation", "cap-camera-windows", - "cidre", + "cidre 0.11.0", "inquire", "objc2-av-foundation", "serde", @@ -891,7 +890,7 @@ dependencies = [ name = "cap-camera-avfoundation" version = "0.1.0" dependencies = [ - "cidre", + "cidre 0.11.0", "clap", "inquire", "tracing", @@ -916,7 +915,7 @@ dependencies = [ "cap-camera", "cap-camera-avfoundation", "cap-camera-windows", - "cidre", + "cidre 0.11.0", "ffmpeg-next", "inquire", "thiserror 1.0.69", @@ -991,7 +990,7 @@ dependencies = [ "cap-rendering", "cap-utils", "chrono", - "cidre", + "cidre 0.11.0", "clipboard-rs", "cocoa 0.26.1", "core-foundation 0.10.1", @@ -1019,7 +1018,6 @@ dependencies = [ "relative-path", "reqwest", "rodio", - "scap 0.1.0-beta.1", "sentry", "serde", "serde_json", @@ -1065,7 +1063,7 @@ dependencies = [ name = "cap-displays" version = "0.1.0" dependencies = [ - "cidre", + "cidre 0.11.0", "cocoa 0.26.1", "core-foundation 0.10.1", "core-graphics 0.24.0", @@ -1157,12 +1155,13 @@ dependencies = [ "cap-camera", "cap-camera-ffmpeg", "cap-camera-windows", + "cap-displays", "cap-fail", "cap-flags", "cap-media-encoders", "cap-media-info", "cap-project", - "cidre", + "cidre 0.11.0", "cocoa 0.26.1", "core-foundation 0.10.1", "core-graphics 0.24.0", @@ -1181,10 +1180,14 @@ dependencies = [ "objc-foundation", "objc2-foundation 0.2.2", "ringbuf", - "scap 0.1.0-beta.1", + "scap", + "scap-direct3d", + "scap-ffmpeg", + "scap-screencapturekit", "screencapturekit", "serde", "specta", + "sync_wrapper", "tempfile", "thiserror 1.0.69", "tokio", @@ -1204,7 +1207,7 @@ dependencies = [ "cap-flags", "cap-media-info", "cap-project", - "cidre", + "cidre 0.11.0", "ffmpeg-next", "gif", "thiserror 1.0.69", @@ -1248,7 +1251,7 @@ dependencies = [ "cap-project", "cap-utils", "chrono", - "cidre", + "cidre 0.11.0", "cocoa 0.26.1", "device_query", "either", @@ -1260,7 +1263,6 @@ dependencies = [ "objc", "objc2-app-kit", "relative-path", - "scap 0.1.0-beta.1", "screencapturekit", "serde", "serde_json", @@ -1287,7 +1289,7 @@ dependencies = [ "cap-flags", "cap-project", "cap-video-decode", - "cidre", + "cidre 0.11.0", "ffmpeg-hw-device", "ffmpeg-next", "ffmpeg-sys-next", @@ -1343,7 +1345,7 @@ dependencies = [ name = "cap-video-decode" version = "0.1.0" dependencies = [ - "cidre", + "cidre 0.11.0", "ffmpeg-hw-device", "ffmpeg-next", "ffmpeg-sys-next", @@ -1470,9 +1472,19 @@ dependencies = [ [[package]] name = "cidre" version = "0.10.1" -source = "git+https://github.com/CapSoftware/cidre?rev=517d097ae438#517d097ae4387ebb97f36c6e133b3c94dca78e25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a105a9a8dd9e625e1e5938aa8442f63551990bfb2ab4bd606b1b30ccbfd8cf" dependencies = [ - "cidre-macros", + "cidre-macros 0.2.0", + "parking_lot", +] + +[[package]] +name = "cidre" +version = "0.11.0" +source = "git+https://github.com/CapSoftware/cidre?rev=bf84b67079a8#bf84b67079a89d6eaf01c1dab073baaba1ea8f77" +dependencies = [ + "cidre-macros 0.3.0", "parking_lot", ] @@ -1482,6 +1494,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f07a383a232853874a9b0b3e80e6eefe9bea73495b8ab4bdd960bc7c1db4c6d" +[[package]] +name = "cidre-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82bc2f84c0baaa09299da3a03864491549685912c1e338a54211e00589dc1e4c" + [[package]] name = "clang-sys" version = "1.8.1" @@ -6775,7 +6793,7 @@ name = "scap" version = "0.1.0-beta.1" source = "git+https://github.com/CapSoftware/scap?rev=3cefe71561ff#3cefe71561ff735efe5c7bee69bcf32cf9ed3da7" dependencies = [ - "cidre", + "cidre 0.10.1", "cocoa 0.25.0", "core-graphics-helmer-fork", "cpal 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -6790,15 +6808,6 @@ dependencies = [ "windows-capture", ] -[[package]] -name = "scap" -version = "0.1.0" -dependencies = [ - "cap-displays", - "scap-direct3d", - "scap-screencapturekit", -] - [[package]] name = "scap-direct3d" version = "0.1.0" @@ -6811,7 +6820,7 @@ dependencies = [ name = "scap-ffmpeg" version = "0.1.0" dependencies = [ - "cidre", + "cidre 0.11.0", "ffmpeg-next", "futures", "scap-direct3d", @@ -6824,7 +6833,7 @@ name = "scap-screencapturekit" version = "0.1.0" dependencies = [ "cap-displays", - "cidre", + "cidre 0.11.0", "clap", "futures", "inquire", diff --git a/Cargo.toml b/Cargo.toml index 3959865e21..84b3c7b657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ sentry = { version = "0.34.0", features = [ tracing = "0.1.41" futures = "0.3.31" -cidre = { git = "https://github.com/CapSoftware/cidre", rev = "517d097ae438", features = [ +cidre = { git = "https://github.com/CapSoftware/cidre", rev = "bf84b67079a8", features = [ "macos_13_0", "cv", "cf", @@ -79,6 +79,6 @@ debug = true [patch.crates-io] screencapturekit = { git = "https://github.com/CapSoftware/screencapturekit-rs", rev = "7ff1e103742e56c8f6c2e940b5e52684ed0bed69" } # branch = "cap-main" -cidre = { git = "https://github.com/CapSoftware/cidre", rev = "517d097ae438" } +cidre = { git = "https://github.com/CapSoftware/cidre", rev = "bf84b67079a8" } # https://github.com/gfx-rs/wgpu/pull/7550 # wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "cd41a6e32a6239b65d1cecbeccde6a43a100914a" } diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index 05105bf0d7..4ca5d5aaf8 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -17,7 +17,6 @@ cap-camera = { path = "../../crates/camera" } serde = { workspace = true } serde_json = "1.0.133" tokio.workspace = true -scap.workspace = true uuid = { version = "1.11.1", features = ["v4"] } ffmpeg = { workspace = true } tracing.workspace = true diff --git a/apps/cli/src/record.rs b/apps/cli/src/record.rs index 92d9b8bb11..34e5fe05b6 100644 --- a/apps/cli/src/record.rs +++ b/apps/cli/src/record.rs @@ -33,7 +33,7 @@ impl RecordStart { (Some(id), _) => cap_media::sources::list_screens() .into_iter() .find(|s| s.0.id == id) - .map(|(s, t)| (ScreenCaptureTarget::Screen { id: s.id }, t)) + .map(|(s, t)| (ScreenCaptureTarget::Display { id: s.id }, t)) .ok_or(format!("Screen with id '{id}' not found")), (_, Some(id)) => cap_media::sources::list_windows() .into_iter() diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index af38d91fb8..376d4bffd3 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -51,7 +51,6 @@ specta.workspace = true specta-typescript = "0.0.7" tokio.workspace = true uuid = { version = "1.10.0", features = ["v4"] } -scap.workspace = true image = "0.25.2" mp4 = "0.14.0" futures-intrusive = "0.5.0" diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index c71e4d1c27..e555c43522 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -126,7 +126,7 @@ impl DeepLinkAction { CaptureMode::Screen(name) => cap_media::sources::list_screens() .into_iter() .find(|(s, _)| s.name == name) - .map(|(s, _)| ScreenCaptureTarget::Screen { id: s.id }) + .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) .ok_or(format!("No screen with name \"{}\"", &name))?, CaptureMode::Window(name) => cap_media::sources::list_windows() .into_iter() diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 833728eb73..15e55cde75 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -436,12 +436,12 @@ async fn get_current_recording( let bounds = r.bounds(); let target = match r.capture_target() { - ScreenCaptureTarget::Screen { id } => CurrentRecordingTarget::Screen { id: *id }, + ScreenCaptureTarget::Display { id } => CurrentRecordingTarget::Screen { id: *id }, ScreenCaptureTarget::Window { id } => CurrentRecordingTarget::Window { id: *id, bounds: *bounds, }, - ScreenCaptureTarget::Area { screen, bounds } => CurrentRecordingTarget::Area { + ScreenCaptureTarget::Area { display_id, bounds } => CurrentRecordingTarget::Area { screen: *screen, bounds: *bounds, }, diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index a6842300dd..23a5cba896 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -253,7 +253,7 @@ pub async fn start_recording( .map(|v| v.owner_name.to_string()) .unwrap_or_else(|| "Window".to_string()) } - ScreenCaptureTarget::Screen { .. } => title.unwrap_or_else(|| "Screen".to_string()), + ScreenCaptureTarget::Display { .. } => title.unwrap_or_else(|| "Screen".to_string()), } }; @@ -320,7 +320,7 @@ pub async fn start_recording( .show(&app) .await; } - ScreenCaptureTarget::Area { screen, .. } => { + ScreenCaptureTarget::Area { display_id, .. } => { let _ = ShowCapWindow::WindowCaptureOccluder { screen_id: *screen } .show(&app) .await; diff --git a/crates/camera-avfoundation/examples/cli.rs b/crates/camera-avfoundation/examples/cli.rs index 4d9aab2af6..95014b5a22 100644 --- a/crates/camera-avfoundation/examples/cli.rs +++ b/crates/camera-avfoundation/examples/cli.rs @@ -53,8 +53,8 @@ pub fn main() { for fr_range in fr_ranges.iter() { _formats.push(Format { index: i, - width: desc.dimensions().width, - height: desc.dimensions().height, + width: desc.dims().width, + height: desc.dims().height, fourcc: desc.media_sub_type(), color_space, max_frame_rate: ( diff --git a/crates/camera-directshow/Cargo.toml b/crates/camera-directshow/Cargo.toml index f4f668855f..e41b1b1b78 100644 --- a/crates/camera-directshow/Cargo.toml +++ b/crates/camera-directshow/Cargo.toml @@ -26,15 +26,3 @@ workspace = true inquire = "0.7.5" tracing.workspace = true tracing-subscriber = "0.3.19" - -[target.'cfg(windows)'.dev-dependencies] -windows-core = { workspace = true } -windows = { workspace = true, features = [ - "Win32_System_Com", - "Win32_Media_MediaFoundation", - "Win32_Media_DirectShow", - "Win32_System_Com_StructuredStorage", - "Win32_System_Ole", - "Win32_System_Variant", - "Win32_Media_KernelStreaming", -] } diff --git a/crates/camera-windows/Cargo.toml b/crates/camera-windows/Cargo.toml index befc53dbe2..3602afd368 100644 --- a/crates/camera-windows/Cargo.toml +++ b/crates/camera-windows/Cargo.toml @@ -19,9 +19,3 @@ workspace = true [dev-dependencies] inquire = "0.7.5" - -cap-camera-mediafoundation = { path = "../camera-mediafoundation" } -cap-camera-directshow = { path = "../camera-directshow" } - -windows = { workspace = true } -windows-core = { workspace = true } diff --git a/crates/camera/src/macos.rs b/crates/camera/src/macos.rs index 389c3b02f6..715a8f5784 100644 --- a/crates/camera/src/macos.rs +++ b/crates/camera/src/macos.rs @@ -25,8 +25,8 @@ impl CameraInfo { for format in device.formats().iter() { let desc = format.format_desc(); - let width = desc.dimensions().width as u32; - let height = desc.dimensions().height as u32; + let width = desc.dims().width as u32; + let height = desc.dims().height as u32; for fr_range in format.video_supported_frame_rate_ranges().iter() { // SAFETY: trust me bro it crashes on intel mac otherwise diff --git a/crates/displays/src/platform/mod.rs b/crates/displays/src/platform/mod.rs index c4afca73ed..bfe03249bb 100644 --- a/crates/displays/src/platform/mod.rs +++ b/crates/displays/src/platform/mod.rs @@ -1,9 +1,9 @@ -// #[cfg(target_os = "macos")] -// mod macos; -// #[cfg(target_os = "macos")] -// pub use macos::*; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub use macos::*; -// #[cfg(windows)] +#[cfg(windows)] mod win; -// #[cfg(windows)] +#[cfg(windows)] pub use win::*; diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs index 0515721603..36d9c3677f 100644 --- a/crates/displays/src/platform/win.rs +++ b/crates/displays/src/platform/win.rs @@ -653,7 +653,7 @@ impl DisplayImpl { None } - fn try_as_capture_item(&self) -> windows::core::Result { + pub fn try_as_capture_item(&self) -> windows::core::Result { let interop = windows::core::factory::()?; unsafe { interop.CreateForMonitor(self.inner) } } diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 1f0dcf0236..09bc67630b 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -17,6 +17,7 @@ cap-flags = { path = "../flags" } cap-audio = { path = "../audio" } cap-camera = { path = "../camera", features = ["specta", "serde"] } cap-camera-ffmpeg = { path = "../camera-ffmpeg" } +cap-displays = { path = "../displays" } cap-fail = { path = "../fail" } cap-media-encoders = { path = "../media-encoders" } cap-media-info = { path = "../media-info" } @@ -27,7 +28,6 @@ flume.workspace = true indexmap = "2.5.0" num-traits = "0.2.19" ringbuf = "0.4.7" -scap.workspace = true serde = { workspace = true } specta.workspace = true tempfile = "3.12.0" @@ -40,6 +40,8 @@ image = { version = "0.25.2", features = ["gif"] } gif = "0.13.1" tokio-util = "0.7.15" kameo = "0.17.2" +scap = { workspace = true } +sync_wrapper = "1.0.2" [target.'cfg(target_os = "macos")'.dependencies] cidre = { workspace = true } @@ -50,8 +52,9 @@ objc = "0.2.7" objc-foundation = "0.1.1" objc2-foundation = { version = "0.2.2", features = ["NSValue"] } screencapturekit = "0.3.5" +scap-screencapturekit = { path = "../scap-screencapturekit" } -[target.'cfg(target_os = "windows")'.dependencies] +# [target.'cfg(target_os = "windows")'.dependencies] windows = { workspace = true, features = [ "Win32_Foundation", "Win32_System", @@ -64,6 +67,8 @@ windows = { workspace = true, features = [ ] } windows-capture = { workspace = true } cap-camera-windows = { path = "../camera-windows" } +scap-direct3d = { path = "../scap-direct3d" } +scap-ffmpeg = { path = "../scap-ffmpeg" } [dev-dependencies] inquire = "0.7.5" diff --git a/crates/media/src/pipeline/builder.rs b/crates/media/src/pipeline/builder.rs index d878ff96c5..5520ab4ac2 100644 --- a/crates/media/src/pipeline/builder.rs +++ b/crates/media/src/pipeline/builder.rs @@ -41,11 +41,10 @@ impl PipelineBuilder { mut task: impl PipelineSourceTask + 'static, ) { let name = name.into(); - let clock = C::clone_from(&self.clock); let control_signal = self.control.add_listener(name.clone()); self.spawn_task(name, move |ready_signal| { - task.run(clock, ready_signal, control_signal) + task.run(ready_signal, control_signal) }); } diff --git a/crates/media/src/pipeline/task.rs b/crates/media/src/pipeline/task.rs index b669d52104..817977f54a 100644 --- a/crates/media/src/pipeline/task.rs +++ b/crates/media/src/pipeline/task.rs @@ -11,7 +11,6 @@ pub trait PipelineSourceTask: Send { fn run( &mut self, - clock: Self::Clock, ready_signal: PipelineReadySignal, control_signal: PipelineControlSignal, ) -> Result<(), String>; diff --git a/crates/media/src/sources/audio_input.rs b/crates/media/src/sources/audio_input.rs index 9cfec73fd4..7541684ee1 100644 --- a/crates/media/src/sources/audio_input.rs +++ b/crates/media/src/sources/audio_input.rs @@ -107,7 +107,6 @@ impl PipelineSourceTask for AudioInputSource { fn run( &mut self, - _: Self::Clock, ready_signal: crate::pipeline::task::PipelineReadySignal, mut control_signal: crate::pipeline::control::PipelineControlSignal, ) -> Result<(), String> { diff --git a/crates/media/src/sources/audio_mixer.rs b/crates/media/src/sources/audio_mixer.rs index 52d26ecbf5..f21254f183 100644 --- a/crates/media/src/sources/audio_mixer.rs +++ b/crates/media/src/sources/audio_mixer.rs @@ -166,7 +166,6 @@ impl PipelineSourceTask for AudioMixer { fn run( &mut self, - _clock: Self::Clock, ready_signal: crate::pipeline::task::PipelineReadySignal, mut control_signal: crate::pipeline::control::PipelineControlSignal, ) -> Result<(), String> { diff --git a/crates/media/src/sources/camera.rs b/crates/media/src/sources/camera.rs index 12c5d404be..9069847c5b 100644 --- a/crates/media/src/sources/camera.rs +++ b/crates/media/src/sources/camera.rs @@ -97,7 +97,6 @@ impl PipelineSourceTask for CameraSource { // #[tracing::instrument(skip_all)] fn run( &mut self, - _: Self::Clock, ready_signal: crate::pipeline::task::PipelineReadySignal, mut control_signal: crate::pipeline::control::PipelineControlSignal, ) -> Result<(), String> { diff --git a/crates/media/src/sources/screen_capture.rs b/crates/media/src/sources/screen_capture.rs index b3284906e7..4262fc0289 100644 --- a/crates/media/src/sources/screen_capture.rs +++ b/crates/media/src/sources/screen_capture.rs @@ -453,7 +453,6 @@ impl PipelineSourceTask for ScreenCaptureSource { // #[instrument(skip_all)] fn run( &mut self, - _clock: Self::Clock, ready_signal: crate::pipeline::task::PipelineReadySignal, control_signal: crate::pipeline::control::PipelineControlSignal, ) -> Result<(), String> { @@ -740,7 +739,6 @@ impl PipelineSourceTask for ScreenCaptureSource { fn run( &mut self, - _clock: Self::Clock, ready_signal: crate::pipeline::task::PipelineReadySignal, control_signal: crate::pipeline::control::PipelineControlSignal, ) -> Result<(), String> { @@ -780,69 +778,69 @@ impl PipelineSourceTask for ScreenCaptureSource { let relative_time = unix_timestamp - start_time_f64; match typ { - sc::stream::OutputType::Screen => { - let Some(pixel_buffer) = sample_buffer.image_buf() else { - return ControlFlow::Continue(()); - }; - - if pixel_buffer.height() == 0 || pixel_buffer.width() == 0 { - return ControlFlow::Continue(()); - } - - let check_skip_send = || { - cap_fail::fail_err!( - "media::sources::screen_capture::skip_send", - () - ); - - Ok::<(), ()>(()) - }; - - if check_skip_send().is_ok() - && video_tx.send((sample_buffer, relative_time)).is_err() - { - error!("Pipeline is unreachable. Shutting down recording."); - return ControlFlow::Continue(()); - } - } - sc::stream::OutputType::Audio => { - use ffmpeg::ChannelLayout; - - let res = || { - cap_fail::fail_err!("screen_capture audio skip", ()); - Ok::<(), ()>(()) - }; - if res().is_err() { - return ControlFlow::Continue(()); - } - - let Some(audio_tx) = &audio_tx else { - return ControlFlow::Continue(()); - }; - - let buf_list = sample_buffer.audio_buf_list::<2>().unwrap(); - let slice = buf_list.block().as_slice().unwrap(); - - let mut frame = ffmpeg::frame::Audio::new( - ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), - sample_buffer.num_samples() as usize, - ChannelLayout::STEREO, - ); - frame.set_rate(48_000); - let data_bytes_size = buf_list.list().buffers[0].data_bytes_size; - for i in 0..frame.planes() { - use cap_media_info::PlanarData; - - frame.plane_data_mut(i).copy_from_slice( - &slice[i * data_bytes_size as usize - ..(i + 1) * data_bytes_size as usize], - ); - } - - frame.set_pts(Some((relative_time * AV_TIME_BASE_Q.den as f64) as i64)); - - let _ = audio_tx.send((frame, relative_time)); - } + // sc::stream::OutputType::Screen => { + // let Some(pixel_buffer) = sample_buffer.image_buf() else { + // return ControlFlow::Continue(()); + // }; + + // if pixel_buffer.height() == 0 || pixel_buffer.width() == 0 { + // return ControlFlow::Continue(()); + // } + + // let check_skip_send = || { + // cap_fail::fail_err!( + // "media::sources::screen_capture::skip_send", + // () + // ); + + // Ok::<(), ()>(()) + // }; + + // if check_skip_send().is_ok() + // && video_tx.send((sample_buffer, relative_time)).is_err() + // { + // error!("Pipeline is unreachable. Shutting down recording."); + // return ControlFlow::Continue(()); + // } + // } + // sc::stream::OutputType::Audio => { + // use ffmpeg::ChannelLayout; + + // let res = || { + // cap_fail::fail_err!("screen_capture audio skip", ()); + // Ok::<(), ()>(()) + // }; + // if res().is_err() { + // return ControlFlow::Continue(()); + // } + + // let Some(audio_tx) = &audio_tx else { + // return ControlFlow::Continue(()); + // }; + + // let buf_list = sample_buffer.audio_buf_list::<2>().unwrap(); + // let slice = buf_list.block().as_slice().unwrap(); + + // let mut frame = ffmpeg::frame::Audio::new( + // ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), + // sample_buffer.num_samples() as usize, + // ChannelLayout::STEREO, + // ); + // frame.set_rate(48_000); + // let data_bytes_size = buf_list.list().buffers[0].data_bytes_size; + // for i in 0..frame.planes() { + // use cap_media_info::PlanarData; + + // frame.plane_data_mut(i).copy_from_slice( + // &slice[i * data_bytes_size as usize + // ..(i + 1) * data_bytes_size as usize], + // ); + // } + + // frame.set_pts(Some((relative_time * AV_TIME_BASE_Q.den as f64) as i64)); + + // let _ = audio_tx.send((frame, relative_time)); + // } _ => {} } @@ -1018,3 +1016,269 @@ fn scap_audio_to_ffmpeg(scap_frame: scap::frame::AudioFrame) -> ffmpeg::frame::A ffmpeg_frame } + +mod new_stuff { + use kameo::prelude::*; + + mod macos { + use super::*; + use cidre::*; + + #[derive(Actor)] + pub struct MacOSScreenCapture { + capturer: Option, + } + + // Public + + pub struct StartCapturing { + target: arc::R, + frame_handler: Recipient, + error_handler: Option>, + } + + // External + + pub struct NewFrame(pub scap_screencapturekit::Frame); + + // Internal + + pub struct CaptureError(arc::R); + + #[derive(Debug, Clone)] + pub enum StartCapturingError { + AlreadyCapturing, + CapturerBuild(arc::R), + Start(arc::R), + } + + impl Message for MacOSScreenCapture { + type Reply = Result<(), StartCapturingError>; + + async fn handle( + &mut self, + msg: StartCapturing, + _: &mut Context, + ) -> Self::Reply { + if self.capturer.is_some() { + return Err(StartCapturingError::AlreadyCapturing); + } + + let capturer = { + let mut capturer_builder = scap_screencapturekit::Capturer::builder( + msg.target.clone(), + sc::StreamCfg::new(), + ) + .with_output_sample_buf_cb(move |frame| { + let _ = msg.frame_handler.tell(NewFrame(frame)).try_send(); + }); + + if let Some(error_handler) = msg.error_handler { + capturer_builder = capturer_builder.with_stop_with_err_cb(move |_, err| { + let _ = error_handler + .tell(CaptureError(err.retained())) + .blocking_send(); + }); + } + + capturer_builder + .build() + .map_err(StartCapturingError::CapturerBuild)? + }; + + capturer.start().await.map_err(StartCapturingError::Start)?; + + self.capturer = Some(capturer); + + Ok(()) + } + } + + impl Message for MacOSScreenCapture { + type Reply = (); + + async fn handle( + &mut self, + msg: CaptureError, + ctx: &mut Context, + ) -> Self::Reply { + dbg!(msg.0); + if let Some(capturer) = self.capturer.as_mut() { + let _ = capturer.stop().await; + ctx.actor_ref().kill(); + } + } + } + + #[tokio::test] + async fn kameo_test() { + use std::time::Duration; + + #[derive(Actor)] + struct FrameHandler; + + impl Message for FrameHandler { + type Reply = (); + + async fn handle( + &mut self, + msg: NewFrame, + _: &mut Context, + ) -> Self::Reply { + dbg!(msg.0.output_type()); + } + } + + let actor = MacOSScreenCapture::spawn(MacOSScreenCapture { capturer: None }); + + let frame_handler = FrameHandler::spawn(FrameHandler); + + actor + .ask(StartCapturing { + target: cap_displays::Display::primary() + .raw_handle() + .as_content_filter() + .await + .unwrap(), + frame_handler: frame_handler.clone().recipient(), + error_handler: None, + }) + .await + .inspect_err(|e| { + dbg!(e); + }) + .ok(); + + actor + .ask(StartCapturing { + target: cap_displays::Display::primary() + .raw_handle() + .as_content_filter() + .await + .unwrap(), + frame_handler: frame_handler.recipient(), + error_handler: None, + }) + .await + .inspect_err(|e| { + dbg!(e); + }) + .ok(); + + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + + mod windows { + use super::*; + use ::windows::Graphics::Capture::GraphicsCaptureItem; + use scap_ffmpeg::AsFFmpeg; + + #[derive(Actor)] + pub struct WindowsScreenCapture { + capturer: Option, + } + + pub struct StartCapturing { + target: GraphicsCaptureItem, + settings: scap_direct3d::Settings, + frame_handler: Recipient, + error_handler: Option>, + } + + struct NewFrame { + ff_frame: ffmpeg::frame::Video, + } + + impl Message for WindowsScreenCapture { + type Reply = (); + + async fn handle( + &mut self, + msg: StartCapturing, + ctx: &mut Context, + ) -> Self::Reply { + let capturer = scap_direct3d::Capturer::new(msg.target, msg.settings); + + let capture_handle = capturer.start( + |frame| { + let ff_frame = frame.as_ffmpeg().unwrap(); + + let _ = msg.frame_handler.tell(NewFrame { ff_frame }).try_send(); + + Ok(()) + }, + || { + Ok(()); + }, + ); + } + } + + #[tokio::test] + async fn kameo_test() { + use std::time::Duration; + + #[derive(Actor)] + struct FrameHandler; + + impl Message for FrameHandler { + type Reply = (); + + async fn handle( + &mut self, + msg: NewFrame, + _: &mut Context, + ) -> Self::Reply { + dbg!( + msg.ff_frame.width(), + msg.ff_frame.height(), + msg.ff_frame.format() + ); + } + } + + let actor = WindowsScreenCapture::spawn(WindowsScreenCapture { capturer: None }); + + let frame_handler = FrameHandler::spawn(FrameHandler); + + actor + .ask(StartCapturing { + target: cap_displays::Display::primary() + .raw_handle() + .try_as_capture_item() + .unwrap(), + settings: scap_direct3d::Settings { + is_border_required: Some(false), + is_cursor_capture_enabled: Some(false), + pixel_format: scap_direct3d::PixelFormat::R8G8B8A8Unorm, + }, + frame_handler: frame_handler.clone().recipient(), + error_handler: None, + }) + .await + .inspect_err(|e| { + dbg!(e); + }) + .ok(); + + // actor + // .ask(StartCapturing { + // target: cap_displays::Display::primary() + // .raw_handle() + // .as_content_filter() + // .await + // .unwrap(), + // frame_handler: frame_handler.recipient(), + // error_handler: None, + // }) + // .await + // .inspect_err(|e| { + // dbg!(e); + // }) + // .ok(); + + tokio::time::sleep(Duration::from_millis(100)).await; + } + } +} diff --git a/crates/recording/Cargo.toml b/crates/recording/Cargo.toml index 9ab1170c60..a1d4034092 100644 --- a/crates/recording/Cargo.toml +++ b/crates/recording/Cargo.toml @@ -22,7 +22,6 @@ tokio.workspace = true flume.workspace = true thiserror.workspace = true ffmpeg = { workspace = true } -scap.workspace = true serde = { workspace = true } serde_json = "1" diff --git a/crates/recording/src/capture_pipeline.rs b/crates/recording/src/capture_pipeline.rs index fc7a31f646..b2041facd6 100644 --- a/crates/recording/src/capture_pipeline.rs +++ b/crates/recording/src/capture_pipeline.rs @@ -377,7 +377,6 @@ pub async fn create_screen_capture( ScreenCaptureSource::::init( capture_target, - None, show_camera, force_show_cursor, max_fps, diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs index 149d8bba05..5840217c1a 100644 --- a/crates/recording/src/studio_recording.rs +++ b/crates/recording/src/studio_recording.rs @@ -867,8 +867,8 @@ async fn create_segment_pipeline( cap_displays::Display::list() .into_iter() .find(|m| match &capture_target { - ScreenCaptureTarget::Screen { id } - | ScreenCaptureTarget::Area { screen: id, .. } => { + ScreenCaptureTarget::Display { id } + | ScreenCaptureTarget::Area { display_id: id, .. } => { m.raw_handle().inner().id == *id } ScreenCaptureTarget::Window { id } => { diff --git a/crates/scap-direct3d/examples/cli.rs b/crates/scap-direct3d/examples/cli.rs index 2e8b6233d6..09d14c0162 100644 --- a/crates/scap-direct3d/examples/cli.rs +++ b/crates/scap-direct3d/examples/cli.rs @@ -15,11 +15,14 @@ fn main() { }, ); - let capture_handle = capturer.start(|frame| { - dbg!(frame); + let capture_handle = capturer.start( + |frame| { + dbg!(frame); - Ok(()) - }); + Ok(()) + }, + || Ok(()), + ); std::thread::sleep(Duration::from_secs(3)); diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index 46d460c038..af41984249 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -105,12 +105,19 @@ impl Capturer { pub fn start( self, callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, + closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static, ) -> CaptureHandle { let stop_flag = Arc::new(AtomicBool::new(false)); let thread_handle = std::thread::spawn({ let stop_flag = stop_flag.clone(); move || { - let _ = dbg!(run(self.item, self.settings, callback, stop_flag)); + run( + self.item, + self.settings, + callback, + closed_callback, + stop_flag, + ); } }); @@ -286,6 +293,7 @@ fn run( item: GraphicsCaptureItem, settings: Settings, mut callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, + mut closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static, stop_flag: Arc, ) -> Result<(), &'static str> { if let Err(e) = unsafe { RoInitialize(RO_INIT_MULTITHREADED) } @@ -388,6 +396,10 @@ fn run( ) .map_err(|_| "Failed to register frame arrived handler")?; + item.Closed(item.Closed( + &TypedEventHandler::::new({ |_, _| closed_callback() }), + )); + session .StartCapture() .map_err(|_| "Failed to start capture")?; diff --git a/crates/scap-ffmpeg/src/lib.rs b/crates/scap-ffmpeg/src/lib.rs index 6a8423a279..f2b455dc13 100644 --- a/crates/scap-ffmpeg/src/lib.rs +++ b/crates/scap-ffmpeg/src/lib.rs @@ -3,9 +3,9 @@ mod screencapturekit; #[cfg(target_os = "macos")] pub use screencapturekit::*; -#[cfg(windows)] +// #[cfg(windows)] mod direct3d; -#[cfg(windows)] +// #[cfg(windows)] pub use direct3d::*; pub trait AsFFmpeg { diff --git a/crates/scap-screencapturekit/src/capture.rs b/crates/scap-screencapturekit/src/capture.rs index fd1e6e49b8..0dbc364e5e 100644 --- a/crates/scap-screencapturekit/src/capture.rs +++ b/crates/scap-screencapturekit/src/capture.rs @@ -8,6 +8,9 @@ define_obj_type!( pub CapturerCallbacks + StreamOutputImpl + StreamDelegateImpl, CapturerCallbacksInner, CAPTURER ); +unsafe impl Send for CapturerCallbacks {} +unsafe impl Sync for CapturerCallbacks {} + impl sc::stream::Output for CapturerCallbacks {} #[objc::add_methods] @@ -15,11 +18,12 @@ impl sc::stream::OutputImpl for CapturerCallbacks { extern "C" fn impl_stream_did_output_sample_buf( &mut self, _cmd: Option<&objc::Sel>, - stream: &sc::Stream, + _: &sc::Stream, sample_buf: &mut cm::SampleBuf, kind: sc::OutputType, ) { if let Some(cb) = &mut self.inner_mut().did_output_sample_buf_cb { + let sample_buf = sample_buf.retained(); let frame = match kind { sc::OutputType::Screen => { let Some(image_buf) = sample_buf.image_buf().map(|v| v.retained()) else { @@ -28,13 +32,12 @@ impl sc::stream::OutputImpl for CapturerCallbacks { }; Frame::Screen(VideoFrame { - stream, sample_buf, image_buf, }) } - sc::OutputType::Audio => Frame::Audio(AudioFrame { stream, sample_buf }), - sc::OutputType::Mic => Frame::Mic(AudioFrame { stream, sample_buf }), + sc::OutputType::Audio => Frame::Audio(AudioFrame { sample_buf }), + sc::OutputType::Mic => Frame::Mic(AudioFrame { sample_buf }), }; (cb)(frame); } @@ -75,10 +78,9 @@ impl Default for CapturerCallbacksInner { } pub struct Capturer { - target: arc::R, - config: arc::R, _queue: arc::R, stream: arc::R, + // READING THIS IS NOT THREAD SAFE, IT JUST HAS TO EXIST _callbacks: arc::R, } @@ -94,14 +96,6 @@ impl Capturer { } } - pub fn config(&self) -> &sc::StreamCfg { - &self.config - } - - pub fn target(&self) -> &sc::ContentFilter { - &self.target - } - pub async fn start(&self) -> Result<(), arc::R> { self.stream.start().await } @@ -111,67 +105,65 @@ impl Capturer { } } -pub struct VideoFrame<'a> { - stream: &'a sc::Stream, - sample_buf: &'a mut cm::SampleBuf, +pub struct VideoFrame { + sample_buf: arc::R, image_buf: arc::R, } -impl<'a> VideoFrame<'a> { - pub fn stream(&self) -> &sc::Stream { - self.stream - } +unsafe impl Send for VideoFrame {} +impl VideoFrame { pub fn sample_buf(&self) -> &cm::SampleBuf { - self.sample_buf + self.sample_buf.as_ref() } pub fn sample_buf_mut(&mut self) -> &mut cm::SampleBuf { - self.sample_buf + self.sample_buf.as_mut() } pub fn image_buf(&self) -> &cv::ImageBuf { - &self.image_buf + self.image_buf.as_ref() } pub fn image_buf_mut(&mut self) -> &mut cv::ImageBuf { - &mut self.image_buf + self.image_buf.as_mut() } } -pub struct AudioFrame<'a> { - stream: &'a sc::Stream, - sample_buf: &'a mut cm::SampleBuf, +pub struct AudioFrame { + sample_buf: arc::R, } -pub enum Frame<'a> { - Screen(VideoFrame<'a>), - Audio(AudioFrame<'a>), - Mic(AudioFrame<'a>), -} +impl AudioFrame { + pub fn sample_buf(&self) -> &cm::SampleBuf { + self.sample_buf.as_ref() + } -impl<'a> Frame<'a> { - pub fn stream(&self) -> &sc::Stream { - match self { - Frame::Screen(frame) => frame.stream, - Frame::Audio(frame) => frame.stream, - Frame::Mic(frame) => frame.stream, - } + pub fn sample_buf_mut(&mut self) -> &mut cm::SampleBuf { + self.sample_buf.as_mut() } +} + +pub enum Frame { + Screen(VideoFrame), + Audio(AudioFrame), + Mic(AudioFrame), +} +impl Frame { pub fn sample_buf(&self) -> &cm::SampleBuf { match self { - Frame::Screen(frame) => frame.sample_buf, - Frame::Audio(frame) => frame.sample_buf, - Frame::Mic(frame) => frame.sample_buf, + Frame::Screen(frame) => frame.sample_buf(), + Frame::Audio(frame) => frame.sample_buf(), + Frame::Mic(frame) => frame.sample_buf(), } } pub fn sample_buf_mut(&mut self) -> &mut cm::SampleBuf { match self { - Frame::Screen(frame) => frame.sample_buf, - Frame::Audio(frame) => frame.sample_buf, - Frame::Mic(frame) => frame.sample_buf, + Frame::Screen(frame) => frame.sample_buf_mut(), + Frame::Audio(frame) => frame.sample_buf_mut(), + Frame::Mic(frame) => frame.sample_buf_mut(), } } @@ -222,8 +214,6 @@ impl CapturerBuilder { .map_err(|e| e.retained())?; Ok(Capturer { - target: self.target, - config: self.config, _queue: queue, stream, _callbacks: callbacks, diff --git a/crates/scap/Cargo.toml b/crates/scap/Cargo.toml deleted file mode 100644 index 8ccd783cef..0000000000 --- a/crates/scap/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "scap" -version = "0.1.0" -edition = "2024" - -[dependencies] -cap-displays = { path = "../displays" } - -[target.'cfg(windows)'.dependencies] -scap-direct3d = { path = "../scap-direct3d" } - -[target.'cfg(target_os = "macos")'.dependencies] -scap-screencapturekit = { path = "../scap-screencapturekit" } - -[lints] -workspace = true diff --git a/crates/scap/src/lib.rs b/crates/scap/src/lib.rs deleted file mode 100644 index e1f82b5051..0000000000 --- a/crates/scap/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct Capturer {} From b71dee46e892907699468a1a5a9c1f499cf203a1 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 14 Aug 2025 15:08:20 +0800 Subject: [PATCH 03/47] windows actor --- crates/displays/Cargo.toml | 4 +- crates/displays/src/platform/win.rs | 49 +++++++-------- crates/media/Cargo.toml | 2 +- crates/media/src/sources/screen_capture.rs | 72 +++++++++++++--------- crates/scap-direct3d/Cargo.toml | 3 +- crates/scap-direct3d/src/lib.rs | 32 +++------- 6 files changed, 81 insertions(+), 81 deletions(-) diff --git a/crates/displays/Cargo.toml b/crates/displays/Cargo.toml index c04ae22962..863f56fd58 100644 --- a/crates/displays/Cargo.toml +++ b/crates/displays/Cargo.toml @@ -18,7 +18,7 @@ core-foundation = "0.10.0" cocoa = "0.26.0" objc = "0.2.7" -# [target.'cfg(target_os= "windows")'.dependencies] +[target.'cfg(target_os= "windows")'.dependencies] windows = { workspace = true, features = [ "Win32_Foundation", "Win32_System", @@ -32,5 +32,7 @@ windows = { workspace = true, features = [ "Win32_Graphics_Gdi", "Win32_Storage_FileSystem", "Win32_Devices_Display", + "Graphics_Capture", + "Win32_System_WinRT_Graphics_Capture", ] } windows-sys = { workspace = true } diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs index 36d9c3677f..21e927bc92 100644 --- a/crates/displays/src/platform/win.rs +++ b/crates/displays/src/platform/win.rs @@ -1,51 +1,52 @@ use std::{mem, str::FromStr}; use windows::{ + core::{BOOL, PCWSTR, PWSTR}, + Graphics::Capture::GraphicsCaptureItem, Win32::{ Devices::Display::{ + DisplayConfigGetDeviceInfo, GetDisplayConfigBufferSizes, QueryDisplayConfig, DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, DISPLAYCONFIG_DEVICE_INFO_HEADER, DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_SOURCE_DEVICE_NAME, DISPLAYCONFIG_TARGET_DEVICE_NAME, DISPLAYCONFIG_TARGET_DEVICE_NAME_FLAGS, DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, - DisplayConfigGetDeviceInfo, GetDisplayConfigBufferSizes, QDC_ONLY_ACTIVE_PATHS, - QueryDisplayConfig, + QDC_ONLY_ACTIVE_PATHS, }, Foundation::{CloseHandle, HWND, LPARAM, POINT, RECT, TRUE, WIN32_ERROR, WPARAM}, - Graphics::{ - Capture::GraphicsCaptureItem, + Graphics:: Gdi::{ - BI_RGB, BITMAP, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, - CreateCompatibleDC, CreateSolidBrush, DEVMODEW, DIB_RGB_COLORS, DeleteDC, - DeleteObject, ENUM_CURRENT_SETTINGS, EnumDisplayMonitors, EnumDisplaySettingsW, - FillRect, GetDC, GetDIBits, GetMonitorInfoW, GetObjectA, HBRUSH, HDC, HGDIOBJ, - HMONITOR, MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTONULL, MONITORINFOEXW, - MonitorFromPoint, ReleaseDC, SelectObject, + CreateCompatibleBitmap, CreateCompatibleDC, CreateSolidBrush, DeleteDC, + DeleteObject, EnumDisplayMonitors, EnumDisplaySettingsW, FillRect, GetDC, + GetDIBits, GetMonitorInfoW, GetObjectA, MonitorFromPoint, ReleaseDC, SelectObject, + BITMAP, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DEVMODEW, DIB_RGB_COLORS, + ENUM_CURRENT_SETTINGS, HBRUSH, HDC, HGDIOBJ, HMONITOR, MONITORINFOEXW, + MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTONULL, }, - }, + Storage::FileSystem::{GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW}, System::{ Registry::{ - HKEY, HKEY_LOCAL_MACHINE, KEY_READ, REG_BINARY, REG_SZ, RegCloseKey, RegEnumKeyExW, - RegOpenKeyExW, RegQueryValueExW, + RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, RegQueryValueExW, HKEY, + HKEY_LOCAL_MACHINE, KEY_READ, REG_BINARY, REG_SZ, }, Threading::{ - GetCurrentProcessId, OpenProcess, PROCESS_NAME_FORMAT, - PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW, + GetCurrentProcessId, OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_FORMAT, + PROCESS_QUERY_LIMITED_INFORMATION, }, + WinRT::Graphics::Capture::IGraphicsCaptureItemInterop, }, UI::{ HiDpi::GetDpiForWindow, Shell::ExtractIconExW, WindowsAndMessaging::{ - DI_FLAGS, DestroyIcon, DrawIconEx, EnumWindows, GCLP_HICON, GW_HWNDNEXT, - GWL_EXSTYLE, GetClassLongPtrW, GetClassNameW, GetCursorPos, GetIconInfo, - GetLayeredWindowAttributes, GetWindow, GetWindowLongW, GetWindowRect, - GetWindowThreadProcessId, HICON, ICONINFO, IsIconic, IsWindowVisible, SendMessageW, - WM_GETICON, WS_EX_LAYERED, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WindowFromPoint, + DestroyIcon, DrawIconEx, EnumWindows, GetClassLongPtrW, GetClassNameW, + GetCursorPos, GetIconInfo, GetLayeredWindowAttributes, GetWindow, GetWindowLongW, + GetWindowRect, GetWindowThreadProcessId, IsIconic, IsWindowVisible, SendMessageW, + WindowFromPoint, DI_FLAGS, GCLP_HICON, GWL_EXSTYLE, GW_HWNDNEXT, HICON, ICONINFO, + WM_GETICON, WS_EX_LAYERED, WS_EX_TOPMOST, WS_EX_TRANSPARENT, }, }, }, - core::{BOOL, PCWSTR, PWSTR}, }; use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize}; @@ -107,7 +108,7 @@ impl DisplayImpl { } pub fn raw_id(&self) -> DisplayIdImpl { - DisplayIdImpl(self.0.0 as u64) + DisplayIdImpl(self.0 .0 as u64) } pub fn from_id(id: String) -> Option { @@ -655,7 +656,7 @@ impl DisplayImpl { pub fn try_as_capture_item(&self) -> windows::core::Result { let interop = windows::core::factory::()?; - unsafe { interop.CreateForMonitor(self.inner) } + unsafe { interop.CreateForMonitor(self.0) } } } @@ -814,7 +815,7 @@ impl WindowImpl { } pub fn id(&self) -> WindowIdImpl { - WindowIdImpl(self.0.0 as u64) + WindowIdImpl(self.0 .0 as u64) } pub fn level(&self) -> Option { diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 09bc67630b..f862019207 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -54,7 +54,7 @@ objc2-foundation = { version = "0.2.2", features = ["NSValue"] } screencapturekit = "0.3.5" scap-screencapturekit = { path = "../scap-screencapturekit" } -# [target.'cfg(target_os = "windows")'.dependencies] +[target.'cfg(target_os = "windows")'.dependencies] windows = { workspace = true, features = [ "Win32_Foundation", "Win32_System", diff --git a/crates/media/src/sources/screen_capture.rs b/crates/media/src/sources/screen_capture.rs index 4262fc0289..bebe114487 100644 --- a/crates/media/src/sources/screen_capture.rs +++ b/crates/media/src/sources/screen_capture.rs @@ -1020,6 +1020,7 @@ fn scap_audio_to_ffmpeg(scap_frame: scap::frame::AudioFrame) -> ffmpeg::frame::A mod new_stuff { use kameo::prelude::*; + #[cfg(target_os = "macos")] mod macos { use super::*; use cidre::*; @@ -1176,14 +1177,19 @@ mod new_stuff { #[derive(Actor)] pub struct WindowsScreenCapture { - capturer: Option, + capture_handle: Option, } pub struct StartCapturing { target: GraphicsCaptureItem, settings: scap_direct3d::Settings, frame_handler: Recipient, - error_handler: Option>, + // error_handler: Option>, + } + + #[derive(Debug)] + pub enum StartCapturingError { + AlreadyCapturing, } struct NewFrame { @@ -1191,27 +1197,33 @@ mod new_stuff { } impl Message for WindowsScreenCapture { - type Reply = (); + type Reply = Result<(), StartCapturingError>; async fn handle( &mut self, msg: StartCapturing, ctx: &mut Context, ) -> Self::Reply { + if self.capture_handle.is_some() { + return Err(StartCapturingError::AlreadyCapturing); + } + let capturer = scap_direct3d::Capturer::new(msg.target, msg.settings); let capture_handle = capturer.start( - |frame| { + move |frame| { let ff_frame = frame.as_ffmpeg().unwrap(); let _ = msg.frame_handler.tell(NewFrame { ff_frame }).try_send(); Ok(()) }, - || { - Ok(()); - }, + || Ok(()), ); + + self.capture_handle = Some(capture_handle); + + Ok(()) } } @@ -1238,11 +1250,13 @@ mod new_stuff { } } - let actor = WindowsScreenCapture::spawn(WindowsScreenCapture { capturer: None }); + let actor = WindowsScreenCapture::spawn(WindowsScreenCapture { + capture_handle: None, + }); let frame_handler = FrameHandler::spawn(FrameHandler); - actor + let _ = actor .ask(StartCapturing { target: cap_displays::Display::primary() .raw_handle() @@ -1254,31 +1268,33 @@ mod new_stuff { pixel_format: scap_direct3d::PixelFormat::R8G8B8A8Unorm, }, frame_handler: frame_handler.clone().recipient(), - error_handler: None, + // error_handler: None, }) .await .inspect_err(|e| { dbg!(e); - }) - .ok(); + }); - // actor - // .ask(StartCapturing { - // target: cap_displays::Display::primary() - // .raw_handle() - // .as_content_filter() - // .await - // .unwrap(), - // frame_handler: frame_handler.recipient(), - // error_handler: None, - // }) - // .await - // .inspect_err(|e| { - // dbg!(e); - // }) - // .ok(); + let _ = actor + .ask(StartCapturing { + target: cap_displays::Display::primary() + .raw_handle() + .try_as_capture_item() + .unwrap(), + settings: scap_direct3d::Settings { + is_border_required: Some(false), + is_cursor_capture_enabled: Some(false), + pixel_format: scap_direct3d::PixelFormat::R8G8B8A8Unorm, + }, + frame_handler: frame_handler.clone().recipient(), + // error_handler: None, + }) + .await + .inspect_err(|e| { + dbg!(e); + }); - tokio::time::sleep(Duration::from_millis(100)).await; + tokio::time::sleep(Duration::from_millis(300)).await; } } } diff --git a/crates/scap-direct3d/Cargo.toml b/crates/scap-direct3d/Cargo.toml index 4414b0cc1f..693a40ea22 100644 --- a/crates/scap-direct3d/Cargo.toml +++ b/crates/scap-direct3d/Cargo.toml @@ -3,8 +3,7 @@ name = "scap-direct3d" version = "0.1.0" edition = "2024" -# [target.'cfg(windows)'.dependencies] -[dependencies] +[target.'cfg(windows)'.dependencies] windows = { workspace = true, features = [ "System", "Graphics_Capture", diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index af41984249..bec4095949 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -1,6 +1,6 @@ // a whole bunch of credit to https://github.com/NiiightmareXD/windows-capture -// #![cfg(windows)] +#![cfg(windows)] use std::{ os::windows::io::AsRawHandle, @@ -64,27 +64,6 @@ impl PixelFormat { } } -pub struct Display { - inner: HMONITOR, -} - -impl Display { - pub fn primary() -> Option { - let monitor = unsafe { MonitorFromPoint(POINT { x: 0, y: 0 }, MONITOR_DEFAULTTONULL) }; - if monitor.is_invalid() { - return None; - } - Some(Self { inner: monitor }) - } - - pub fn try_as_capture_item(&self) -> windows::core::Result { - let interop = windows::core::factory::()?; - let inner = unsafe { interop.CreateForMonitor(self.inner) }?; - - Ok(CaptureItem { inner }) - } -} - #[derive(Default)] pub struct Settings { pub is_border_required: Option, @@ -396,9 +375,12 @@ fn run( ) .map_err(|_| "Failed to register frame arrived handler")?; - item.Closed(item.Closed( - &TypedEventHandler::::new({ |_, _| closed_callback() }), - )); + let _ = item.Closed( + &TypedEventHandler::::new(move |_, _| { + closed_callback(); + Ok(()) + }), + ); session .StartCapture() From f7d6067534d2e62b983efc574e7c4e7587729d14 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 15 Aug 2025 12:49:36 +0800 Subject: [PATCH 04/47] actors --- Cargo.lock | 1 + apps/cli/src/record.rs | 2 +- apps/desktop/src-tauri/Cargo.toml | 1 + .../desktop/src-tauri/src/deeplink_actions.rs | 44 +- apps/desktop/src-tauri/src/lib.rs | 30 +- apps/desktop/src-tauri/src/recording.rs | 119 +- .../src/routes/(window-chrome)/(main).tsx | 22 +- apps/desktop/src/utils/tauri.ts | 1202 ++++++----------- crates/displays/src/bounds.rs | 39 +- crates/displays/src/lib.rs | 4 +- crates/displays/src/platform/macos.rs | 4 +- crates/media/Cargo.toml | 2 +- crates/media/src/pipeline/control.rs | 2 +- crates/media/src/sources/screen_capture.rs | 1162 ++++++++++------ crates/recording/src/capture_pipeline.rs | 1 + crates/recording/src/instant_recording.rs | 4 +- crates/recording/src/studio_recording.rs | 76 +- packages/ui-solid/src/auto-imports.d.ts | 168 +-- 18 files changed, 1456 insertions(+), 1427 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20fb2dc02a..c17afa7d83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1018,6 +1018,7 @@ dependencies = [ "relative-path", "reqwest", "rodio", + "scap", "sentry", "serde", "serde_json", diff --git a/apps/cli/src/record.rs b/apps/cli/src/record.rs index 34e5fe05b6..92d9b8bb11 100644 --- a/apps/cli/src/record.rs +++ b/apps/cli/src/record.rs @@ -33,7 +33,7 @@ impl RecordStart { (Some(id), _) => cap_media::sources::list_screens() .into_iter() .find(|s| s.0.id == id) - .map(|(s, t)| (ScreenCaptureTarget::Display { id: s.id }, t)) + .map(|(s, t)| (ScreenCaptureTarget::Screen { id: s.id }, t)) .ok_or(format!("Screen with id '{id}' not found")), (_, Some(id)) => cap_media::sources::list_windows() .into_iter() diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 376d4bffd3..4d13c21bf4 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -45,6 +45,7 @@ tauri-plugin-deep-link = "2.2.0" tauri-plugin-clipboard-manager = "2.2.1" tauri-plugin-opener = "2.2.6" +scap = { workspace = true } serde = { workspace = true } serde_json = "1.0.111" specta.workspace = true diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index e555c43522..d3a598a821 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -121,27 +121,29 @@ impl DeepLinkAction { crate::set_camera_input(app.clone(), state.clone(), camera_preview, camera).await?; crate::set_mic_input(state.clone(), mic_label).await?; - use cap_media::sources::ScreenCaptureTarget; - let capture_target: ScreenCaptureTarget = match capture_mode { - CaptureMode::Screen(name) => cap_media::sources::list_screens() - .into_iter() - .find(|(s, _)| s.name == name) - .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) - .ok_or(format!("No screen with name \"{}\"", &name))?, - CaptureMode::Window(name) => cap_media::sources::list_windows() - .into_iter() - .find(|(w, _)| w.name == name) - .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) - .ok_or(format!("No window with name \"{}\"", &name))?, - }; - - let inputs = StartRecordingInputs { - capture_target, - capture_system_audio, - mode, - }; - - crate::recording::start_recording(app.clone(), state, inputs).await + todo!(); + + // use cap_media::sources::ScreenCaptureTarget; + // let capture_target: ScreenCaptureTarget = match capture_mode { + // CaptureMode::Screen(name) => cap_media::sources::list_screens() + // .into_iter() + // .find(|(s, _)| s.name == name) + // .map(|(s, _)| ScreenCaptureTarget::Screen { id: s.id }) + // .ok_or(format!("No screen with name \"{}\"", &name))?, + // CaptureMode::Window(name) => cap_media::sources::list_windows() + // .into_iter() + // .find(|(w, _)| w.name == name) + // .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) + // .ok_or(format!("No window with name \"{}\"", &name))?, + // }; + + // let inputs = StartRecordingInputs { + // capture_target, + // capture_system_audio, + // mode, + // }; + + // crate::recording::start_recording(app.clone(), state, inputs).await } DeepLinkAction::StopRecording => { crate::recording::stop_recording(app.clone(), app.state()).await diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 15e55cde75..c30110ac02 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -433,17 +433,19 @@ async fn get_current_recording( ) -> Result>, ()> { let state = state.read().await; Ok(JsonValue::new(&state.current_recording().map(|r| { - let bounds = r.bounds(); + // let bounds = r.bounds(); let target = match r.capture_target() { - ScreenCaptureTarget::Display { id } => CurrentRecordingTarget::Screen { id: *id }, + ScreenCaptureTarget::Screen { id } => { + CurrentRecordingTarget::Screen { id: 0, /* *id */ } + } ScreenCaptureTarget::Window { id } => CurrentRecordingTarget::Window { - id: *id, - bounds: *bounds, + id: 0, // *id, + bounds: Bounds::default(), // *bounds, }, - ScreenCaptureTarget::Area { display_id, bounds } => CurrentRecordingTarget::Area { - screen: *screen, - bounds: *bounds, + ScreenCaptureTarget::Area { screen, bounds } => CurrentRecordingTarget::Area { + screen: 0, // *screen, + bounds: Default::default(), // *bounds, }, }; @@ -1988,13 +1990,13 @@ pub async fn run(recording_logging_handle: LoggingHandle) { .typ::() .typ::(); - // #[cfg(debug_assertions)] - // specta_builder - // .export( - // specta_typescript::Typescript::default(), - // "../src/utils/tauri.ts", - // ) - // .expect("Failed to export typescript bindings"); + #[cfg(debug_assertions)] + specta_builder + .export( + specta_typescript::Typescript::default(), + "../src/utils/tauri.ts", + ) + .expect("Failed to export typescript bindings"); let (camera_tx, camera_ws_port, _shutdown) = camera_legacy::create_camera_preview_ws().await; diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 23a5cba896..3efa746033 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -125,12 +125,12 @@ impl InProgressRecording { } } - pub fn bounds(&self) -> &Bounds { - match self { - Self::Instant { handle, .. } => &handle.bounds, - Self::Studio { handle, .. } => &handle.bounds, - } - } + // pub fn bounds(&self) -> &Bounds { + // match self { + // Self::Instant { handle, .. } => &handle.bounds, + // Self::Studio { handle, .. } => &handle.bounds, + // } + // } } pub enum CompletedRecording { @@ -172,19 +172,46 @@ impl CompletedRecording { #[tauri::command(async)] #[specta::specta] pub async fn list_capture_screens() -> Vec { - cap_media::sources::list_screens() + cap_displays::Display::list() .into_iter() - .map(|(v, _)| v) + .enumerate() + .map(|(i, display)| { + let id = display.id(); + let name = format!("Display {}", i); + let refresh_rate = display.raw_handle().refresh_rate(); + + CaptureScreen { + id, + name, + refresh_rate: refresh_rate as u32, + } + }) .collect() + + // cap_media::sources::list_screens() + // .into_iter() + // .map(|(v, _)| v) + // .collect() } #[tauri::command(async)] #[specta::specta] pub async fn list_capture_windows() -> Vec { - cap_media::sources::list_windows() + cap_displays::Window::list() .into_iter() - .map(|(v, _)| v) + .enumerate() + .map(|(i, v)| CaptureWindow { + id: v.id(), + owner_name: v.owner_name().unwrap_or_default(), + name: format!("Window {i}"), + bounds: v.bounds().unwrap(), + refresh_rate: 60, + }) .collect() + // cap_media::sources::list_windows() + // .into_iter() + // .map(|(v, _)| v) + // .collect() } #[tauri::command(async)] @@ -237,9 +264,9 @@ pub async fn start_recording( .await?; let target_name = { - let title = inputs.capture_target.get_title(); + let title = "TODO Title".to_string(); // inputs.capture_target.get_title(); - match inputs.capture_target { + match inputs.capture_target.clone() { ScreenCaptureTarget::Area { .. } => "Area".to_string(), ScreenCaptureTarget::Window { id, .. } => { let platform_windows: HashMap = @@ -248,12 +275,13 @@ pub async fn start_recording( .map(|window| (window.window_id, window)) .collect(); - platform_windows - .get(&id) - .map(|v| v.owner_name.to_string()) - .unwrap_or_else(|| "Window".to_string()) + "Window".to_string() + // platform_windows + // .get(&id) + // .map(|v| v.owner_name.to_string()) + // .unwrap_or_else(|| "Window".to_string()) } - ScreenCaptureTarget::Display { .. } => title.unwrap_or_else(|| "Screen".to_string()), + ScreenCaptureTarget::Screen { .. } => title, // .unwrap_or_else(|| "Screen".to_string()), } }; @@ -300,33 +328,33 @@ pub async fn start_recording( RecordingMode::Studio => None, }; - match &inputs.capture_target { - ScreenCaptureTarget::Window { id: _id } => { - #[cfg(target_os = "macos")] - let display = display_for_window(*_id).unwrap().id; - - #[cfg(windows)] - let display = { - let scap::Target::Window(target) = inputs.capture_target.get_target().unwrap() - else { - unreachable!(); - }; - display_for_window(windows::Win32::Foundation::HWND(target.raw_handle.0)) - .unwrap() - .0 as u32 - }; - - let _ = ShowCapWindow::WindowCaptureOccluder { screen_id: display } - .show(&app) - .await; - } - ScreenCaptureTarget::Area { display_id, .. } => { - let _ = ShowCapWindow::WindowCaptureOccluder { screen_id: *screen } - .show(&app) - .await; - } - _ => {} - } + // match &inputs.capture_target { + // ScreenCaptureTarget::Window { id: _id } => { + // #[cfg(target_os = "macos")] + // let display = display_for_window(*_id).unwrap().id; + + // #[cfg(windows)] + // let display = { + // let scap::Target::Window(target) = inputs.capture_target.get_target().unwrap() + // else { + // unreachable!(); + // }; + // display_for_window(windows::Win32::Foundation::HWND(target.raw_handle.0)) + // .unwrap() + // .0 as u32 + // }; + + // let _ = ShowCapWindow::WindowCaptureOccluder { screen_id: display } + // .show(&app) + // .await; + // } + // ScreenCaptureTarget::Area { screen, .. } => { + // let _ = ShowCapWindow::WindowCaptureOccluder { screen_id: *screen } + // .show(&app) + // .await; + // } + // _ => {} + // } // Set pending state BEFORE closing main window and starting countdown { @@ -376,12 +404,13 @@ pub async fn start_recording( let actor_done_rx = spawn_actor({ let state_mtx = Arc::clone(&state_mtx); let general_settings = general_settings.cloned(); + let capture_target = inputs.capture_target.clone(); async move { fail!("recording::spawn_actor"); let mut state = state_mtx.write().await; let base_inputs = cap_recording::RecordingBaseInputs { - capture_target: inputs.capture_target, + capture_target, capture_system_audio: inputs.capture_system_audio, mic_feed: &state.mic_feed, }; diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index 188a93f7e6..009c6eacfe 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -143,7 +143,7 @@ function Page() { // allowing us to define fallbacks if the selected options aren't actually available const options = { screen: () => { - let screen; + let screen: CaptureScreen | undefined; if (rawOptions.captureTarget.variant === "screen") { const screenId = rawOptions.captureTarget.id; @@ -156,7 +156,7 @@ function Page() { return screen; }, window: () => { - let win; + let win: CaptureWindow | undefined; if (rawOptions.captureTarget.variant === "window") { const windowId = rawOptions.captureTarget.id; @@ -170,12 +170,16 @@ function Page() { const { cameraID } = rawOptions; if (!cameraID) return; if ("ModelID" in cameraID && c.model_id === cameraID.ModelID) return c; - if ("DeviceID" in cameraID && c.device_id == cameraID.DeviceID) + if ("DeviceID" in cameraID && c.device_id === cameraID.DeviceID) return c; }), micName: () => mics.data?.find((name) => name === rawOptions.micName), }; + createEffect(() => { + console.log(_windows()); + }); + // if target is window and no windows are available, switch to screen capture createEffect(() => { const screen = _screens()?.[0]; @@ -333,6 +337,8 @@ function Page() { }> + {/** biome-ignore lint/a11y/useKeyWithClickEvents: */} + {/** biome-ignore lint/a11y/noStaticElementInteractions: */} { if (license.data?.type !== "pro") { @@ -366,12 +372,14 @@ function Page() { : rawOptions.captureTarget.variant } onChange={(area) => { + const screen = options.screen(); + if (!screen) return; if (!area) setOptions( "captureTarget", reconcile({ variant: "screen", - id: options.screen()?.id ?? -1, + id: screen.id, }), ); }} @@ -750,6 +758,7 @@ function CameraSelect(props: { return (
diff --git a/apps/desktop/src/routes/window-capture-occluder.tsx b/apps/desktop/src/routes/window-capture-occluder.tsx index 7b31f931fc..a3ae4a02a9 100644 --- a/apps/desktop/src/routes/window-capture-occluder.tsx +++ b/apps/desktop/src/routes/window-capture-occluder.tsx @@ -25,10 +25,6 @@ export default function () { } }; - const [scale] = createResource(() => getCurrentWindow().scaleFactor(), { - initialValue: 0, - }); - return ( @@ -42,16 +38,7 @@ export default function () { return ( ); diff --git a/apps/desktop/src/utils/queries.ts b/apps/desktop/src/utils/queries.ts index 69dce92dd3..2d305b0670 100644 --- a/apps/desktop/src/utils/queries.ts +++ b/apps/desktop/src/utils/queries.ts @@ -85,7 +85,7 @@ export const getPermissions = queryOptions({ }); export function createOptionsQuery() { - const PERSIST_KEY = "recording-options-query"; + const PERSIST_KEY = "recording-options-query-2"; const [_state, _setState] = createStore<{ captureTarget: ScreenCaptureTarget; micName: string | null; @@ -96,7 +96,7 @@ export function createOptionsQuery() { /** @deprecated */ cameraLabel: string | null; }>({ - captureTarget: { variant: "screen", id: 0 }, + captureTarget: { variant: "screen", id: "0" }, micName: null, cameraLabel: null, mode: "studio", diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 6f5dfba989..94c2cc0030 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -363,7 +363,16 @@ export type FileType = "recording" | "screenshot" export type Flags = { captions: boolean } export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" } export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; customCursorCapture?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour } -export type GifExportSettings = { fps: number; resolution_base: XY } +export type GifExportSettings = { fps: number; resolution_base: XY; quality: GifQuality | null } +export type GifQuality = { +/** + * Encoding quality from 1-100 (default: 90) + */ +quality: number | null; +/** + * Whether to prioritize speed over quality (default: false) + */ +fast: boolean | null } export type HapticPattern = "Alignment" | "LevelChange" | "Generic" export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted" export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } diff --git a/crates/displays/src/bounds.rs b/crates/displays/src/bounds.rs index 49c60cfacd..50d667fa76 100644 --- a/crates/displays/src/bounds.rs +++ b/crates/displays/src/bounds.rs @@ -1,5 +1,3 @@ -use std::any::Any; - use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/crates/recording/src/capture_pipeline.rs b/crates/recording/src/capture_pipeline.rs index 1f92ea1b28..3e1e8ea8ba 100644 --- a/crates/recording/src/capture_pipeline.rs +++ b/crates/recording/src/capture_pipeline.rs @@ -1,5 +1,5 @@ use cap_media::MediaError; -use cap_media_encoders::*; + use cap_media_info::AudioInfo; use flume::{Receiver, Sender}; use std::{ @@ -384,5 +384,5 @@ pub async fn create_screen_capture( ) .await .map(|v| (v, video_rx)) - .map_err(|e| RecordingError::Media(MediaError::TaskLaunch(e))) + .map_err(|e| RecordingError::Media(MediaError::TaskLaunch(e.to_string()))) } diff --git a/crates/recording/src/sources/screen_capture.rs b/crates/recording/src/sources/screen_capture.rs index 89c683464c..fa0ac09bd0 100644 --- a/crates/recording/src/sources/screen_capture.rs +++ b/crates/recording/src/sources/screen_capture.rs @@ -2,14 +2,12 @@ use cap_displays::{ Display, DisplayId, Window, WindowId, bounds::{LogicalBounds, PhysicalSize}, }; -use cap_media_info::{AudioInfo, RawVideoFormat, VideoInfo}; -use cpal::traits::{DeviceTrait, HostTrait}; -use ffmpeg::{format::Sample, sys::AV_TIME_BASE_Q}; +use cap_media_info::{AudioInfo, VideoInfo}; +use ffmpeg::sys::AV_TIME_BASE_Q; use flume::Sender; use serde::{Deserialize, Serialize}; use specta::Type; use std::time::SystemTime; -use tracing::info; use tracing::{error, warn}; use crate::pipeline::{control::Control, task::PipelineSourceTask}; @@ -180,6 +178,14 @@ struct Config { show_cursor: bool, } +#[derive(Debug, Clone, thiserror::Error)] +pub enum ScreenCaptureInitError { + #[error("NoDisplay")] + NoDisplay, + #[error("PhysicalSize")] + PhysicalSize, +} + impl ScreenCaptureSource { #[allow(clippy::too_many_arguments)] pub async fn init( @@ -190,13 +196,16 @@ impl ScreenCaptureSource { audio_tx: Option>, start_time: SystemTime, tokio_handle: tokio::runtime::Handle, - ) -> Result { + ) -> Result { cap_fail::fail!("media::screen_capture::init"); - let fps = max_fps.min(target.display().unwrap().refresh_rate() as u32); + let display = target.display().ok_or(ScreenCaptureInitError::NoDisplay)?; + + let fps = max_fps.min(display.refresh_rate() as u32); - let output_size = target.physical_size().unwrap(); - let display = target.display().unwrap(); + let output_size = target + .physical_size() + .ok_or(ScreenCaptureInitError::PhysicalSize)?; Ok(Self { config: Config { @@ -209,7 +218,7 @@ impl ScreenCaptureSource { TCaptureFormat::pixel_format(), output_size.width() as u32, output_size.height() as u32, - 120, + fps, ), video_tx, audio_tx, @@ -571,7 +580,9 @@ mod windows { .await; if let Some(audio_tx) = audio_tx { - let audio_capture = WindowsAudioCapture::spawn(WindowsAudioCapture::new(audio_tx, start_time).unwrap()); + let audio_capture = WindowsAudioCapture::spawn( + WindowsAudioCapture::new(audio_tx, start_time).unwrap(), + ); let _ = audio_capture.ask(audio::StartCapturing).send().await; } @@ -685,9 +696,9 @@ mod windows { use audio::WindowsAudioCapture; pub mod audio { use super::*; + use cpal::traits::StreamTrait; use scap_cpal::*; use scap_ffmpeg::*; - use cpal::traits::StreamTrait; #[derive(Actor)] pub struct WindowsAudioCapture { @@ -695,32 +706,34 @@ mod windows { } impl WindowsAudioCapture { - pub fn new(audio_tx: Sender<(ffmpeg::frame::Audio, f64)>, start_time: SystemTime) -> Result { - let mut i = 0; - let capturer = scap_cpal::create_capturer( - move |data, _: &cpal::InputCallbackInfo, config| { - use scap_ffmpeg::*; - - let timestamp = SystemTime::now(); - let mut ff_frame = data.as_ffmpeg(config); + pub fn new( + audio_tx: Sender<(ffmpeg::frame::Audio, f64)>, + start_time: SystemTime, + ) -> Result { + let mut i = 0; + let capturer = scap_cpal::create_capturer( + move |data, _: &cpal::InputCallbackInfo, config| { + use scap_ffmpeg::*; - let Ok(elapsed) = timestamp.duration_since(start_time) else { - warn!("Skipping audio frame {i} as elapsed time is invalid"); - return; - }; + let timestamp = SystemTime::now(); + let mut ff_frame = data.as_ffmpeg(config); - ff_frame.set_pts(Some( - (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64 - )); + let Ok(elapsed) = timestamp.duration_since(start_time) else { + warn!("Skipping audio frame {i} as elapsed time is invalid"); + return; + }; - let _ = audio_tx.send((ff_frame, elapsed.as_secs_f64())); - i += 1; - }, - move |e| { - dbg!(e); - }, - )?; + ff_frame.set_pts(Some( + (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64, + )); + let _ = audio_tx.send((ff_frame, elapsed.as_secs_f64())); + i += 1; + }, + move |e| { + dbg!(e); + }, + )?; Ok(Self { capturer }) } @@ -829,8 +842,7 @@ mod macos { .send((sample_buffer.retained(), relative_time)) .is_err() { - // error!("Pipeline is unreachable. Shutting down recording."); - // return ControlFlow::Continue(()); + warn!("Pipeline is unreachable"); } } scap_screencapturekit::Frame::Audio(_) => { @@ -904,7 +916,11 @@ mod macos { start_time_f64, }); - let content_filter = display.raw_handle().as_content_filter().await.unwrap(); + let content_filter = display + .raw_handle() + .as_content_filter() + .await + .ok_or_else(|| "Failed to get content filter".to_string())?; let size = config.target.physical_size().unwrap(); let mut settings = scap_screencapturekit::StreamCfgBuilder::default() diff --git a/crates/scap-cpal/Cargo.toml b/crates/scap-cpal/Cargo.toml index 12a79562d3..7d20c1f7fc 100644 --- a/crates/scap-cpal/Cargo.toml +++ b/crates/scap-cpal/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] cpal.workspace = true +thiserror.workspace = true [lints] workspace = true diff --git a/crates/scap-cpal/src/lib.rs b/crates/scap-cpal/src/lib.rs index d6970eb35e..14dafcc5c1 100644 --- a/crates/scap-cpal/src/lib.rs +++ b/crates/scap-cpal/src/lib.rs @@ -1,32 +1,42 @@ use cpal::{ - InputCallbackInfo, PlayStreamError, Stream, StreamConfig, StreamError, traits::StreamTrait, + BuildStreamError, DefaultStreamConfigError, InputCallbackInfo, PlayStreamError, Stream, + StreamConfig, StreamError, traits::StreamTrait, }; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CapturerError { + #[error("NoDevice")] + NoDevice, + #[error("DefaultConfig: {0}")] + DefaultConfig(#[from] DefaultStreamConfigError), + #[error("BuildStream: {0}")] + BuildStream(#[from] BuildStreamError), +} pub fn create_capturer( mut data_callback: impl FnMut(&cpal::Data, &InputCallbackInfo, &StreamConfig) + Send + 'static, error_callback: impl FnMut(StreamError) + Send + 'static, -) -> Result { +) -> Result { use cpal::traits::{DeviceTrait, HostTrait}; let host = cpal::default_host(); - let output_device = host.default_output_device().ok_or("Device not available")?; - let supported_config = output_device - .default_output_config() - .map_err(|_| "Failed to get default output config")?; + let output_device = host + .default_output_device() + .ok_or(CapturerError::NoDevice)?; + let supported_config = output_device.default_output_config()?; let config = supported_config.clone().into(); - let stream = output_device - .build_input_stream_raw( - &config, - supported_config.sample_format(), - { - let config = config.clone(); - move |data, info: &InputCallbackInfo| data_callback(data, info, &config) - }, - error_callback, - None, - ) - .map_err(|_| "failed to build input stream")?; + let stream = output_device.build_input_stream_raw( + &config, + supported_config.sample_format(), + { + let config = config.clone(); + move |data, info: &InputCallbackInfo| data_callback(data, info, &config) + }, + error_callback, + None, + )?; Ok(Capturer { stream, config }) } diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 4962d1ea82..dadf6414ae 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -6,13 +6,13 @@ // biome-ignore lint: disable export {} declare global { - const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"] + const IconCapArrows: typeof import('~icons/cap/arrows.jsx')['default'] const IconCapAudioOn: typeof import('~icons/cap/audio-on.jsx')['default'] const IconCapBgBlur: typeof import('~icons/cap/bg-blur.jsx')['default'] const IconCapCamera: typeof import('~icons/cap/camera.jsx')['default'] const IconCapCaptions: typeof import('~icons/cap/captions.jsx')['default'] const IconCapChevronDown: typeof import('~icons/cap/chevron-down.jsx')['default'] - const IconCapCircle: typeof import("~icons/cap/circle.jsx")["default"] + const IconCapCircle: typeof import('~icons/cap/circle.jsx')['default'] const IconCapCircleCheck: typeof import('~icons/cap/circle-check.jsx')['default'] const IconCapCirclePlus: typeof import('~icons/cap/circle-plus.jsx')['default'] const IconCapCircleX: typeof import('~icons/cap/circle-x.jsx')['default'] @@ -50,7 +50,7 @@ declare global { const IconCapScissors: typeof import('~icons/cap/scissors.jsx')['default'] const IconCapSettings: typeof import('~icons/cap/settings.jsx')['default'] const IconCapShadow: typeof import('~icons/cap/shadow.jsx')['default'] - const IconCapSquare: typeof import("~icons/cap/square.jsx")["default"] + const IconCapSquare: typeof import('~icons/cap/square.jsx')['default'] const IconCapStopCircle: typeof import('~icons/cap/stop-circle.jsx')['default'] const IconCapTrash: typeof import('~icons/cap/trash.jsx')['default'] const IconCapUndo: typeof import('~icons/cap/undo.jsx')['default'] @@ -76,7 +76,7 @@ declare global { const IconLucideMessageSquarePlus: typeof import('~icons/lucide/message-square-plus.jsx')['default'] const IconLucideMicOff: typeof import('~icons/lucide/mic-off.jsx')['default'] const IconLucideMonitor: typeof import('~icons/lucide/monitor.jsx')['default'] - const IconLucideRectangleHorizontal: typeof import("~icons/lucide/rectangle-horizontal.jsx")["default"] + const IconLucideRectangleHorizontal: typeof import('~icons/lucide/rectangle-horizontal.jsx')['default'] const IconLucideRotateCcw: typeof import('~icons/lucide/rotate-ccw.jsx')['default'] const IconLucideSearch: typeof import('~icons/lucide/search.jsx')['default'] const IconLucideSquarePlay: typeof import('~icons/lucide/square-play.jsx')['default'] From 9568eac90a5c087397d221aa86321d068ed66619 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 20 Aug 2025 23:08:06 +0800 Subject: [PATCH 21/47] cursor bounds all good on macos --- apps/desktop/src-tauri/src/recording.rs | 8 ++-- crates/recording/src/cursor.rs | 10 ++++- crates/recording/src/studio_recording.rs | 50 +++++++++++++++++++----- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 1712decd6d..db98b95bed 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -76,10 +76,10 @@ impl InProgressRecording { } } - pub async fn resume(&self) -> Result<(), RecordingError> { + pub async fn resume(&self) -> Result<(), String> { match self { - Self::Instant { handle, .. } => handle.resume().await, - Self::Studio { handle, .. } => handle.resume().await, + Self::Instant { handle, .. } => handle.resume().await.map_err(|e| e.to_string()), + Self::Studio { handle, .. } => handle.resume().await.map_err(|e| e.to_string()), } } @@ -468,7 +468,7 @@ pub async fn start_recording( } // Actor hasn't errored, it's just finished v => { - dbg!(v); + info!("recording actor ended: {v:?}"); } } } diff --git a/crates/recording/src/cursor.rs b/crates/recording/src/cursor.rs index 88c9b9f8e8..50425b5c37 100644 --- a/crates/recording/src/cursor.rs +++ b/crates/recording/src/cursor.rs @@ -36,7 +36,7 @@ impl CursorActor { #[tracing::instrument(name = "cursor", skip_all)] pub fn spawn_cursor_recorder( - screen_bounds: LogicalBounds, + crop_bounds: LogicalBounds, display: cap_displays::Display, cursors_dir: PathBuf, prev_cursors: Cursors, @@ -131,13 +131,19 @@ pub fn spawn_cursor_recorder( let position = cap_cursor_capture::RawCursorPosition::get(); + dbg!(&position); + let position = (position != last_position).then(|| { last_position = position; + dbg!(&crop_bounds); + let cropped_norm_pos = position .relative_to_display(display) .normalize() - .with_crop(screen_bounds.position(), screen_bounds.size()); + .with_crop(crop_bounds.position(), crop_bounds.size()); + + dbg!(&cropped_norm_pos); (cropped_norm_pos.x(), cropped_norm_pos.y()) }); diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs index e9462f5e09..aae0eaf295 100644 --- a/crates/recording/src/studio_recording.rs +++ b/crates/recording/src/studio_recording.rs @@ -41,7 +41,7 @@ enum StudioRecordingActorState { pub enum StudioRecordingActorControlMessage { Pause(oneshot::Sender>), - Resume(oneshot::Sender>), + Resume(oneshot::Sender>), Stop(oneshot::Sender>), Cancel(oneshot::Sender>), } @@ -111,7 +111,7 @@ impl StudioRecordingHandle { send_message!(self.ctrl_tx, StudioRecordingActorControlMessage::Pause) } - pub async fn resume(&self) -> Result<(), RecordingError> { + pub async fn resume(&self) -> Result<(), CreateSegmentPipelineError> { send_message!(self.ctrl_tx, StudioRecordingActorControlMessage::Resume) } @@ -120,13 +120,22 @@ impl StudioRecordingHandle { } } +#[derive(Debug, thiserror::Error)] +pub enum SpawnStudioRecordingError { + #[error("{0}")] + Media(#[from] MediaError), + #[error("{0}")] + PipelineCreationError(#[from] CreateSegmentPipelineError), +} + pub async fn spawn_studio_recording_actor<'a>( id: String, recording_dir: PathBuf, base_inputs: RecordingBaseInputs<'a>, camera_feed: Option>>, custom_cursor_capture: bool, -) -> Result<(StudioRecordingHandle, oneshot::Receiver>), RecordingError> { +) -> Result<(StudioRecordingHandle, oneshot::Receiver>), SpawnStudioRecordingError> +{ ensure_dir(&recording_dir)?; let (done_tx, done_rx) = oneshot::channel(); @@ -608,7 +617,7 @@ impl SegmentPipelineFactory { StudioRecordingPipeline, oneshot::Receiver>, ), - RecordingError, + CreateSegmentPipelineError, > { let result = create_segment_pipeline( &self.segments_dir, @@ -632,6 +641,20 @@ impl SegmentPipelineFactory { } } +#[derive(Debug, thiserror::Error)] +pub enum CreateSegmentPipelineError { + #[error("NoDisplay")] + NoDisplay, + #[error("NoBounds")] + NoBounds, + #[error("Actor/{0}")] + Actor(#[from] ActorError), + #[error("{0}")] + Recording(#[from] RecordingError), + #[error("{0}")] + Media(#[from] MediaError), +} + #[tracing::instrument(skip_all, name = "segment", fields(index = index))] #[allow(clippy::too_many_arguments)] async fn create_segment_pipeline( @@ -652,7 +675,7 @@ async fn create_segment_pipeline( StudioRecordingPipeline, oneshot::Receiver>, ), - RecordingError, + CreateSegmentPipelineError, > { let system_audio = if capture_system_audio { let (tx, rx) = flume::bounded(64); @@ -661,8 +684,12 @@ async fn create_segment_pipeline( (None, None) }; - let display = capture_target.display().unwrap(); // TODO: fix - let display_bounds = display.logical_bounds(); + let display = capture_target + .display() + .ok_or(CreateSegmentPipelineError::NoDisplay)?; + let crop_bounds = capture_target + .logical_bounds() + .ok_or(CreateSegmentPipelineError::NoBounds)?; let (screen_source, screen_rx) = create_screen_capture( &capture_target, @@ -861,7 +888,7 @@ async fn create_segment_pipeline( let cursor = custom_cursor_capture.then(move || { let cursor = spawn_cursor_recorder( - display_bounds, + crop_bounds, display, cursors_dir.to_path_buf(), prev_cursors, @@ -875,9 +902,12 @@ async fn create_segment_pipeline( } }); - let (mut pipeline, pipeline_done_rx) = pipeline_builder.build().await?; + let (mut pipeline, pipeline_done_rx) = pipeline_builder + .build() + .await + .map_err(RecordingError::from)?; - pipeline.play().await?; + pipeline.play().await.map_err(RecordingError::from)?; info!("pipeline playing"); From 59aa5be2051b07a03be52524737d2d6ba1f395ec Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 20 Aug 2025 23:48:25 +0800 Subject: [PATCH 22/47] can_* functions --- crates/scap-direct3d/src/lib.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index 244ce7de99..30f409e436 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -12,7 +12,7 @@ use std::{ }; use windows::{ - Foundation::TypedEventHandler, + Foundation::{TypedEventHandler, Metadata::ApiInformation}, Graphics::{ Capture::{Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem}, DirectX::{Direct3D11::IDirect3DDevice, DirectXPixelFormat}, @@ -48,7 +48,7 @@ use windows::{ DispatchMessageW, GetMessageW, MSG, PostThreadMessageW, TranslateMessage, WM_QUIT, }, }, - core::{IInspectable, Interface}, + core::{IInspectable, Interface, HSTRING}, }; #[derive(Default, Clone, Copy, Debug)] @@ -76,11 +76,34 @@ impl PixelFormat { pub struct Settings { pub is_border_required: Option, pub is_cursor_capture_enabled: Option, - pub pixel_format: PixelFormat, pub min_update_interval: Option, + pub pixel_format: PixelFormat, pub crop: Option, } +impl Settings { + pub fn can_is_border_required(&self) -> windows::core::Result { + Ok(ApiInformation::IsPropertyPresent( + &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), + &HSTRING::from("IsCursorCaptureEnabled"), + )) + } + + pub fn can_is_cursor_capture_enabled(&self) -> windows::core::Result { + Ok(ApiInformation::IsPropertyPresent( + &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), + &HSTRING::from("IsBorderRequired"), + )) + } + + pub fn can_min_update_interval(&self) -> windows::core::Result { + Ok(ApiInformation::IsPropertyPresent( + &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), + &HSTRING::from("MinUpdateInterval"), + )) + } +} + pub struct Capturer { item: GraphicsCaptureItem, settings: Settings, From 8e397acfe4ba68ceb3ca4c38fa0071bf3181e576 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 21 Aug 2025 00:06:36 +0800 Subject: [PATCH 23/47] capability check fns --- crates/scap-direct3d/Cargo.toml | 1 + crates/scap-direct3d/src/lib.rs | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/scap-direct3d/Cargo.toml b/crates/scap-direct3d/Cargo.toml index a04d362296..7d90a9ea52 100644 --- a/crates/scap-direct3d/Cargo.toml +++ b/crates/scap-direct3d/Cargo.toml @@ -8,6 +8,7 @@ windows = { workspace = true, features = [ "System", "Graphics_Capture", "Graphics_DirectX_Direct3D11", + "Foundation_Metadata", "Win32_Graphics_Gdi", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index 30f409e436..76e0d82695 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -14,7 +14,7 @@ use std::{ use windows::{ Foundation::{TypedEventHandler, Metadata::ApiInformation}, Graphics::{ - Capture::{Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem}, + Capture::{Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem, GraphicsCaptureSession}, DirectX::{Direct3D11::IDirect3DDevice, DirectXPixelFormat}, }, Win32::{ @@ -72,6 +72,13 @@ impl PixelFormat { } } +pub fn is_supported() -> windows::core::Result { + Ok(ApiInformation::IsApiContractPresentByMajor( + &HSTRING::from("Windows.Foundation.UniversalApiContract"), + 8 + )? && GraphicsCaptureSession::IsSupported()?) +} + #[derive(Default, Debug)] pub struct Settings { pub is_border_required: Option, @@ -83,24 +90,24 @@ pub struct Settings { impl Settings { pub fn can_is_border_required(&self) -> windows::core::Result { - Ok(ApiInformation::IsPropertyPresent( + ApiInformation::IsPropertyPresent( &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), &HSTRING::from("IsCursorCaptureEnabled"), - )) + ) } pub fn can_is_cursor_capture_enabled(&self) -> windows::core::Result { - Ok(ApiInformation::IsPropertyPresent( + ApiInformation::IsPropertyPresent( &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), &HSTRING::from("IsBorderRequired"), - )) + ) } pub fn can_min_update_interval(&self) -> windows::core::Result { - Ok(ApiInformation::IsPropertyPresent( + ApiInformation::IsPropertyPresent( &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), &HSTRING::from("MinUpdateInterval"), - )) + ) } } From d5e330ca44d953c0aaec21ae328da7476b43a4d1 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 21 Aug 2025 15:16:19 +0800 Subject: [PATCH 24/47] make it work on windows --- Cargo.lock | 2 + apps/desktop/src-tauri/src/lib.rs | 2 +- .../src-tauri/src/target_select_overlay.rs | 16 +- apps/desktop/src-tauri/src/windows.rs | 25 +- .../src/routes/(window-chrome)/(main).tsx | 2 +- .../src/routes/target-select-overlay.tsx | 4 +- apps/desktop/src/utils/createPresets.ts | 2 +- apps/desktop/src/utils/tauri.ts | 1228 +++++++++++------ crates/camera-directshow/examples/cli.rs | 2 +- crates/camera-mediafoundation/examples/cli.rs | 2 +- crates/camera-windows/examples/cli.rs | 4 +- crates/cursor-capture/src/position.rs | 40 +- crates/displays/Cargo.toml | 2 + crates/displays/src/lib.rs | 66 +- crates/displays/src/main.rs | 284 ++-- crates/displays/src/platform/win.rs | 239 ++-- crates/recording/examples/recording-cli.rs | 23 +- crates/recording/src/capture_pipeline.rs | 4 + crates/recording/src/cursor.rs | 18 +- .../recording/src/sources/screen_capture.rs | 247 ++-- crates/recording/src/studio_recording.rs | 4 +- crates/scap-direct3d/Cargo.toml | 3 + crates/scap-direct3d/src/lib.rs | 493 ++++--- crates/scap-ffmpeg/examples/cli.rs | 14 +- packages/ui-solid/src/auto-imports.d.ts | 147 +- 25 files changed, 1715 insertions(+), 1158 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5609bd195b..a8a47d465f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1073,6 +1073,7 @@ dependencies = [ "objc", "serde", "specta", + "tracing", "windows 0.60.0", "windows-sys 0.59.0", ] @@ -7003,6 +7004,7 @@ version = "0.1.0" dependencies = [ "cap-displays", "scap-ffmpeg", + "thiserror 1.0.69", "windows 0.60.0", ] diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index d065663065..42a5a3430e 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -475,7 +475,7 @@ async fn get_current_recording( id: id.clone(), bounds: cap_displays::Window::from_id(id) .ok_or(())? - .logical_bounds() + .display_relative_logical_bounds() .ok_or(())?, }, ScreenCaptureTarget::Area { screen, bounds } => CurrentRecordingTarget::Area { diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index 513b4629aa..ee0f1964dc 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -10,7 +10,9 @@ use base64::prelude::*; use crate::windows::{CapWindowId, ShowCapWindow}; use cap_displays::{ DisplayId, WindowId, - bounds::{LogicalBounds, PhysicalSize}, + bounds::{ + LogicalBounds, LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, + }, }; use serde::Serialize; use specta::Type; @@ -70,17 +72,19 @@ pub async fn open_target_select_overlays( window: window.and_then(|w| { Some(WindowUnderCursor { id: w.id(), - bounds: w.logical_bounds()?, + bounds: w.display_relative_logical_bounds()?, app_name: w.owner_name()?, icon: w.app_icon().map(|bytes| { format!("data:image/png;base64,{}", BASE64_STANDARD.encode(&bytes)) }), }) }), - screen: display.map(|d| ScreenUnderCursor { - name: d.name().unwrap_or_default(), - physical_size: d.physical_size(), - refresh_rate: d.refresh_rate().to_string(), + screen: display.and_then(|d| { + Some(ScreenUnderCursor { + name: d.name().unwrap_or_default(), + physical_size: d.physical_size()?, + refresh_rate: d.refresh_rate().to_string(), + }) }), } .emit(&app); diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index ad23f85868..60801add61 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -255,8 +255,8 @@ impl ShowCapWindow { return Err(tauri::Error::WindowNotFound); }; - let size = display.raw_handle().logical_size(); - let position = display.raw_handle().logical_position(); + let size = display.physical_size().unwrap(); + let position = display.physical_position().unwrap(); let mut window_builder = self .window_builder( @@ -398,7 +398,8 @@ impl ShowCapWindow { return Err(tauri::Error::WindowNotFound); }; - let bounds = display.logical_bounds(); + let pos = display.physical_position().unwrap(); + let bounds = display.physical_size().unwrap(); let mut window_builder = self .window_builder(app, "/window-capture-occluder") @@ -410,8 +411,8 @@ impl ShowCapWindow { .visible_on_all_workspaces(true) .content_protected(true) .skip_taskbar(true) - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()) + .inner_size(bounds.width(), bounds.height()) + .position(pos.x(), pos.y()) .transparent(true); let window = window_builder.build()?; @@ -442,11 +443,11 @@ impl ShowCapWindow { return Err(tauri::Error::WindowNotFound); }; - let bounds = display.logical_bounds(); - - window_builder = window_builder - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()); + if let Some(bounds) = display.physical_bounds() { + window_builder = window_builder + .inner_size(bounds.size().width(), bounds.size().height()) + .position(bounds.position().x(), bounds.position().y()); + } let window = window_builder.build()?; @@ -715,7 +716,9 @@ trait MonitorExt { impl MonitorExt for Display { fn intersects(&self, position: PhysicalPosition, size: PhysicalSize) -> bool { - let bounds = self.logical_bounds(); + let Some(bounds) = self.physical_bounds() else { + return false; + }; let left = bounds.position().x() as i32; let right = left + bounds.size().width() as i32; diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index a715424f6e..afc4c7aa2d 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -36,7 +36,7 @@ import { } from "~/utils/queries"; import { type CameraInfo, - CaptureDisplay, + type CaptureDisplay, type CaptureWindow, commands, events, diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx index 7173996d5c..c277d0637c 100644 --- a/apps/desktop/src/routes/target-select-overlay.tsx +++ b/apps/desktop/src/routes/target-select-overlay.tsx @@ -21,9 +21,9 @@ import { createStore, reconcile } from "solid-js/store"; import { createOptionsQuery } from "~/utils/queries"; import { commands, - DisplayId, + type DisplayId, events, - LogicalBounds, + type LogicalBounds, type ScreenCaptureTarget, type TargetUnderCursor, } from "~/utils/tauri"; diff --git a/apps/desktop/src/utils/createPresets.ts b/apps/desktop/src/utils/createPresets.ts index 75942c04e7..2217a75262 100644 --- a/apps/desktop/src/utils/createPresets.ts +++ b/apps/desktop/src/utils/createPresets.ts @@ -26,7 +26,7 @@ export function createPresets() { query, createPreset: async (preset: CreatePreset) => { const config = { ...preset.config }; - // @ts-ignore we reeeally don't want the timeline in the preset + // @ts-expect-error we reeeally don't want the timeline in the preset config.timeline = undefined; await updatePresets((store) => { diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 94c2cc0030..b4645d7441 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1,463 +1,810 @@ - // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. /** user-defined commands **/ - export const commands = { -async setMicInput(label: string | null) : Promise { - return await TAURI_INVOKE("set_mic_input", { label }); -}, -async setCameraInput(id: DeviceOrModelID | null) : Promise { - return await TAURI_INVOKE("set_camera_input", { id }); -}, -async startRecording(inputs: StartRecordingInputs) : Promise { - return await TAURI_INVOKE("start_recording", { inputs }); -}, -async stopRecording() : Promise { - return await TAURI_INVOKE("stop_recording"); -}, -async pauseRecording() : Promise { - return await TAURI_INVOKE("pause_recording"); -}, -async resumeRecording() : Promise { - return await TAURI_INVOKE("resume_recording"); -}, -async restartRecording() : Promise { - return await TAURI_INVOKE("restart_recording"); -}, -async deleteRecording() : Promise { - return await TAURI_INVOKE("delete_recording"); -}, -async listCameras() : Promise { - return await TAURI_INVOKE("list_cameras"); -}, -async listCaptureWindows() : Promise { - return await TAURI_INVOKE("list_capture_windows"); -}, -async listCaptureDisplays() : Promise { - return await TAURI_INVOKE("list_capture_displays"); -}, -async takeScreenshot() : Promise { - return await TAURI_INVOKE("take_screenshot"); -}, -async listAudioDevices() : Promise { - return await TAURI_INVOKE("list_audio_devices"); -}, -async closeRecordingsOverlayWindow() : Promise { - await TAURI_INVOKE("close_recordings_overlay_window"); -}, -async setFakeWindowBounds(name: string, bounds: LogicalBounds) : Promise { - return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); -}, -async removeFakeWindow(name: string) : Promise { - return await TAURI_INVOKE("remove_fake_window", { name }); -}, -async focusCapturesPanel() : Promise { - await TAURI_INVOKE("focus_captures_panel"); -}, -async getCurrentRecording() : Promise> { - return await TAURI_INVOKE("get_current_recording"); -}, -async exportVideo(projectPath: string, progress: TAURI_CHANNEL, settings: ExportSettings) : Promise { - return await TAURI_INVOKE("export_video", { projectPath, progress, settings }); -}, -async getExportEstimates(path: string, resolution: XY, fps: number) : Promise { - return await TAURI_INVOKE("get_export_estimates", { path, resolution, fps }); -}, -async copyFileToPath(src: string, dst: string) : Promise { - return await TAURI_INVOKE("copy_file_to_path", { src, dst }); -}, -async copyVideoToClipboard(path: string) : Promise { - return await TAURI_INVOKE("copy_video_to_clipboard", { path }); -}, -async copyScreenshotToClipboard(path: string) : Promise { - return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); -}, -async openFilePath(path: string) : Promise { - return await TAURI_INVOKE("open_file_path", { path }); -}, -async getVideoMetadata(path: string) : Promise { - return await TAURI_INVOKE("get_video_metadata", { path }); -}, -async createEditorInstance() : Promise { - return await TAURI_INVOKE("create_editor_instance"); -}, -async getMicWaveforms() : Promise { - return await TAURI_INVOKE("get_mic_waveforms"); -}, -async getSystemAudioWaveforms() : Promise { - return await TAURI_INVOKE("get_system_audio_waveforms"); -}, -async startPlayback(fps: number, resolutionBase: XY) : Promise { - return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); -}, -async stopPlayback() : Promise { - return await TAURI_INVOKE("stop_playback"); -}, -async setPlayheadPosition(frameNumber: number) : Promise { - return await TAURI_INVOKE("set_playhead_position", { frameNumber }); -}, -async setProjectConfig(config: ProjectConfiguration) : Promise { - return await TAURI_INVOKE("set_project_config", { config }); -}, -async generateZoomSegmentsFromClicks() : Promise { - return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); -}, -async openPermissionSettings(permission: OSPermission) : Promise { - await TAURI_INVOKE("open_permission_settings", { permission }); -}, -async doPermissionsCheck(initialCheck: boolean) : Promise { - return await TAURI_INVOKE("do_permissions_check", { initialCheck }); -}, -async requestPermission(permission: OSPermission) : Promise { - await TAURI_INVOKE("request_permission", { permission }); -}, -async uploadExportedVideo(path: string, mode: UploadMode) : Promise { - return await TAURI_INVOKE("upload_exported_video", { path, mode }); -}, -async uploadScreenshot(screenshotPath: string) : Promise { - return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); -}, -async getRecordingMeta(path: string, fileType: FileType) : Promise { - return await TAURI_INVOKE("get_recording_meta", { path, fileType }); -}, -async saveFileDialog(fileName: string, fileType: string) : Promise { - return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); -}, -async listRecordings() : Promise<([string, RecordingMetaWithType])[]> { - return await TAURI_INVOKE("list_recordings"); -}, -async listScreenshots() : Promise<([string, RecordingMeta])[]> { - return await TAURI_INVOKE("list_screenshots"); -}, -async checkUpgradedAndUpdate() : Promise { - return await TAURI_INVOKE("check_upgraded_and_update"); -}, -async openExternalLink(url: string) : Promise { - return await TAURI_INVOKE("open_external_link", { url }); -}, -async setHotkey(action: HotkeyAction, hotkey: Hotkey | null) : Promise { - return await TAURI_INVOKE("set_hotkey", { action, hotkey }); -}, -async resetCameraPermissions() : Promise { - return await TAURI_INVOKE("reset_camera_permissions"); -}, -async resetMicrophonePermissions() : Promise { - return await TAURI_INVOKE("reset_microphone_permissions"); -}, -async isCameraWindowOpen() : Promise { - return await TAURI_INVOKE("is_camera_window_open"); -}, -async seekTo(frameNumber: number) : Promise { - return await TAURI_INVOKE("seek_to", { frameNumber }); -}, -async positionTrafficLights(controlsInset: [number, number] | null) : Promise { - await TAURI_INVOKE("position_traffic_lights", { controlsInset }); -}, -async setTheme(theme: AppTheme) : Promise { - await TAURI_INVOKE("set_theme", { theme }); -}, -async globalMessageDialog(message: string) : Promise { - await TAURI_INVOKE("global_message_dialog", { message }); -}, -async showWindow(window: ShowCapWindow) : Promise { - return await TAURI_INVOKE("show_window", { window }); -}, -async writeClipboardString(text: string) : Promise { - return await TAURI_INVOKE("write_clipboard_string", { text }); -}, -async performHapticFeedback(pattern: HapticPattern | null, time: HapticPerformanceTime | null) : Promise { - return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); -}, -async listFails() : Promise<{ [key in string]: boolean }> { - return await TAURI_INVOKE("list_fails"); -}, -async setFail(name: string, value: boolean) : Promise { - await TAURI_INVOKE("set_fail", { name, value }); -}, -async updateAuthPlan() : Promise { - await TAURI_INVOKE("update_auth_plan"); -}, -async setWindowTransparent(value: boolean) : Promise { - await TAURI_INVOKE("set_window_transparent", { value }); -}, -async getEditorMeta() : Promise { - return await TAURI_INVOKE("get_editor_meta"); -}, -async setPrettyName(prettyName: string) : Promise { - return await TAURI_INVOKE("set_pretty_name", { prettyName }); -}, -async setServerUrl(serverUrl: string) : Promise { - return await TAURI_INVOKE("set_server_url", { serverUrl }); -}, -async setCameraPreviewState(state: CameraWindowState) : Promise { - return await TAURI_INVOKE("set_camera_preview_state", { state }); -}, -async awaitCameraPreviewReady() : Promise { - return await TAURI_INVOKE("await_camera_preview_ready"); -}, -/** - * Function to handle creating directories for the model - */ -async createDir(path: string, recursive: boolean) : Promise { - return await TAURI_INVOKE("create_dir", { path, recursive }); -}, -/** - * Function to save the model file - */ -async saveModelFile(path: string, data: number[]) : Promise { - return await TAURI_INVOKE("save_model_file", { path, data }); -}, -/** - * Function to transcribe audio from a video file using Whisper - */ -async transcribeAudio(videoPath: string, modelPath: string, language: string) : Promise { - return await TAURI_INVOKE("transcribe_audio", { videoPath, modelPath, language }); -}, -/** - * Function to save caption data to a file - */ -async saveCaptions(videoId: string, captions: CaptionData) : Promise { - return await TAURI_INVOKE("save_captions", { videoId, captions }); -}, -/** - * Function to load caption data from a file - */ -async loadCaptions(videoId: string) : Promise { - return await TAURI_INVOKE("load_captions", { videoId }); -}, -/** - * Helper function to download a Whisper model from Hugging Face Hub - */ -async downloadWhisperModel(modelName: string, outputPath: string) : Promise { - return await TAURI_INVOKE("download_whisper_model", { modelName, outputPath }); -}, -/** - * Function to check if a model file exists - */ -async checkModelExists(modelPath: string) : Promise { - return await TAURI_INVOKE("check_model_exists", { modelPath }); -}, -/** - * Function to delete a downloaded model - */ -async deleteWhisperModel(modelPath: string) : Promise { - return await TAURI_INVOKE("delete_whisper_model", { modelPath }); -}, -/** - * Export captions to an SRT file - */ -async exportCaptionsSrt(videoId: string) : Promise { - return await TAURI_INVOKE("export_captions_srt", { videoId }); -}, -async openTargetSelectOverlays() : Promise { - return await TAURI_INVOKE("open_target_select_overlays"); -}, -async closeTargetSelectOverlays() : Promise { - return await TAURI_INVOKE("close_target_select_overlays"); -} -} + async setMicInput(label: string | null): Promise { + return await TAURI_INVOKE("set_mic_input", { label }); + }, + async setCameraInput(id: DeviceOrModelID | null): Promise { + return await TAURI_INVOKE("set_camera_input", { id }); + }, + async startRecording(inputs: StartRecordingInputs): Promise { + return await TAURI_INVOKE("start_recording", { inputs }); + }, + async stopRecording(): Promise { + return await TAURI_INVOKE("stop_recording"); + }, + async pauseRecording(): Promise { + return await TAURI_INVOKE("pause_recording"); + }, + async resumeRecording(): Promise { + return await TAURI_INVOKE("resume_recording"); + }, + async restartRecording(): Promise { + return await TAURI_INVOKE("restart_recording"); + }, + async deleteRecording(): Promise { + return await TAURI_INVOKE("delete_recording"); + }, + async listCameras(): Promise { + return await TAURI_INVOKE("list_cameras"); + }, + async listCaptureWindows(): Promise { + return await TAURI_INVOKE("list_capture_windows"); + }, + async listCaptureDisplays(): Promise { + return await TAURI_INVOKE("list_capture_displays"); + }, + async takeScreenshot(): Promise { + return await TAURI_INVOKE("take_screenshot"); + }, + async listAudioDevices(): Promise { + return await TAURI_INVOKE("list_audio_devices"); + }, + async closeRecordingsOverlayWindow(): Promise { + await TAURI_INVOKE("close_recordings_overlay_window"); + }, + async setFakeWindowBounds( + name: string, + bounds: LogicalBounds, + ): Promise { + return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); + }, + async removeFakeWindow(name: string): Promise { + return await TAURI_INVOKE("remove_fake_window", { name }); + }, + async focusCapturesPanel(): Promise { + await TAURI_INVOKE("focus_captures_panel"); + }, + async getCurrentRecording(): Promise> { + return await TAURI_INVOKE("get_current_recording"); + }, + async exportVideo( + projectPath: string, + progress: TAURI_CHANNEL, + settings: ExportSettings, + ): Promise { + return await TAURI_INVOKE("export_video", { + projectPath, + progress, + settings, + }); + }, + async getExportEstimates( + path: string, + resolution: XY, + fps: number, + ): Promise { + return await TAURI_INVOKE("get_export_estimates", { + path, + resolution, + fps, + }); + }, + async copyFileToPath(src: string, dst: string): Promise { + return await TAURI_INVOKE("copy_file_to_path", { src, dst }); + }, + async copyVideoToClipboard(path: string): Promise { + return await TAURI_INVOKE("copy_video_to_clipboard", { path }); + }, + async copyScreenshotToClipboard(path: string): Promise { + return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); + }, + async openFilePath(path: string): Promise { + return await TAURI_INVOKE("open_file_path", { path }); + }, + async getVideoMetadata(path: string): Promise { + return await TAURI_INVOKE("get_video_metadata", { path }); + }, + async createEditorInstance(): Promise { + return await TAURI_INVOKE("create_editor_instance"); + }, + async getMicWaveforms(): Promise { + return await TAURI_INVOKE("get_mic_waveforms"); + }, + async getSystemAudioWaveforms(): Promise { + return await TAURI_INVOKE("get_system_audio_waveforms"); + }, + async startPlayback(fps: number, resolutionBase: XY): Promise { + return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); + }, + async stopPlayback(): Promise { + return await TAURI_INVOKE("stop_playback"); + }, + async setPlayheadPosition(frameNumber: number): Promise { + return await TAURI_INVOKE("set_playhead_position", { frameNumber }); + }, + async setProjectConfig(config: ProjectConfiguration): Promise { + return await TAURI_INVOKE("set_project_config", { config }); + }, + async generateZoomSegmentsFromClicks(): Promise { + return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); + }, + async openPermissionSettings(permission: OSPermission): Promise { + await TAURI_INVOKE("open_permission_settings", { permission }); + }, + async doPermissionsCheck(initialCheck: boolean): Promise { + return await TAURI_INVOKE("do_permissions_check", { initialCheck }); + }, + async requestPermission(permission: OSPermission): Promise { + await TAURI_INVOKE("request_permission", { permission }); + }, + async uploadExportedVideo( + path: string, + mode: UploadMode, + ): Promise { + return await TAURI_INVOKE("upload_exported_video", { path, mode }); + }, + async uploadScreenshot(screenshotPath: string): Promise { + return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); + }, + async getRecordingMeta( + path: string, + fileType: FileType, + ): Promise { + return await TAURI_INVOKE("get_recording_meta", { path, fileType }); + }, + async saveFileDialog( + fileName: string, + fileType: string, + ): Promise { + return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); + }, + async listRecordings(): Promise<[string, RecordingMetaWithType][]> { + return await TAURI_INVOKE("list_recordings"); + }, + async listScreenshots(): Promise<[string, RecordingMeta][]> { + return await TAURI_INVOKE("list_screenshots"); + }, + async checkUpgradedAndUpdate(): Promise { + return await TAURI_INVOKE("check_upgraded_and_update"); + }, + async openExternalLink(url: string): Promise { + return await TAURI_INVOKE("open_external_link", { url }); + }, + async setHotkey(action: HotkeyAction, hotkey: Hotkey | null): Promise { + return await TAURI_INVOKE("set_hotkey", { action, hotkey }); + }, + async resetCameraPermissions(): Promise { + return await TAURI_INVOKE("reset_camera_permissions"); + }, + async resetMicrophonePermissions(): Promise { + return await TAURI_INVOKE("reset_microphone_permissions"); + }, + async isCameraWindowOpen(): Promise { + return await TAURI_INVOKE("is_camera_window_open"); + }, + async seekTo(frameNumber: number): Promise { + return await TAURI_INVOKE("seek_to", { frameNumber }); + }, + async positionTrafficLights( + controlsInset: [number, number] | null, + ): Promise { + await TAURI_INVOKE("position_traffic_lights", { controlsInset }); + }, + async setTheme(theme: AppTheme): Promise { + await TAURI_INVOKE("set_theme", { theme }); + }, + async globalMessageDialog(message: string): Promise { + await TAURI_INVOKE("global_message_dialog", { message }); + }, + async showWindow(window: ShowCapWindow): Promise { + return await TAURI_INVOKE("show_window", { window }); + }, + async writeClipboardString(text: string): Promise { + return await TAURI_INVOKE("write_clipboard_string", { text }); + }, + async performHapticFeedback( + pattern: HapticPattern | null, + time: HapticPerformanceTime | null, + ): Promise { + return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); + }, + async listFails(): Promise<{ [key in string]: boolean }> { + return await TAURI_INVOKE("list_fails"); + }, + async setFail(name: string, value: boolean): Promise { + await TAURI_INVOKE("set_fail", { name, value }); + }, + async updateAuthPlan(): Promise { + await TAURI_INVOKE("update_auth_plan"); + }, + async setWindowTransparent(value: boolean): Promise { + await TAURI_INVOKE("set_window_transparent", { value }); + }, + async getEditorMeta(): Promise { + return await TAURI_INVOKE("get_editor_meta"); + }, + async setPrettyName(prettyName: string): Promise { + return await TAURI_INVOKE("set_pretty_name", { prettyName }); + }, + async setServerUrl(serverUrl: string): Promise { + return await TAURI_INVOKE("set_server_url", { serverUrl }); + }, + async setCameraPreviewState(state: CameraWindowState): Promise { + return await TAURI_INVOKE("set_camera_preview_state", { state }); + }, + async awaitCameraPreviewReady(): Promise { + return await TAURI_INVOKE("await_camera_preview_ready"); + }, + /** + * Function to handle creating directories for the model + */ + async createDir(path: string, recursive: boolean): Promise { + return await TAURI_INVOKE("create_dir", { path, recursive }); + }, + /** + * Function to save the model file + */ + async saveModelFile(path: string, data: number[]): Promise { + return await TAURI_INVOKE("save_model_file", { path, data }); + }, + /** + * Function to transcribe audio from a video file using Whisper + */ + async transcribeAudio( + videoPath: string, + modelPath: string, + language: string, + ): Promise { + return await TAURI_INVOKE("transcribe_audio", { + videoPath, + modelPath, + language, + }); + }, + /** + * Function to save caption data to a file + */ + async saveCaptions(videoId: string, captions: CaptionData): Promise { + return await TAURI_INVOKE("save_captions", { videoId, captions }); + }, + /** + * Function to load caption data from a file + */ + async loadCaptions(videoId: string): Promise { + return await TAURI_INVOKE("load_captions", { videoId }); + }, + /** + * Helper function to download a Whisper model from Hugging Face Hub + */ + async downloadWhisperModel( + modelName: string, + outputPath: string, + ): Promise { + return await TAURI_INVOKE("download_whisper_model", { + modelName, + outputPath, + }); + }, + /** + * Function to check if a model file exists + */ + async checkModelExists(modelPath: string): Promise { + return await TAURI_INVOKE("check_model_exists", { modelPath }); + }, + /** + * Function to delete a downloaded model + */ + async deleteWhisperModel(modelPath: string): Promise { + return await TAURI_INVOKE("delete_whisper_model", { modelPath }); + }, + /** + * Export captions to an SRT file + */ + async exportCaptionsSrt(videoId: string): Promise { + return await TAURI_INVOKE("export_captions_srt", { videoId }); + }, + async openTargetSelectOverlays(): Promise { + return await TAURI_INVOKE("open_target_select_overlays"); + }, + async closeTargetSelectOverlays(): Promise { + return await TAURI_INVOKE("close_target_select_overlays"); + }, +}; /** user-defined events **/ - export const events = __makeEvents__<{ -audioInputLevelChange: AudioInputLevelChange, -authenticationInvalid: AuthenticationInvalid, -currentRecordingChanged: CurrentRecordingChanged, -downloadProgress: DownloadProgress, -editorStateChanged: EditorStateChanged, -newNotification: NewNotification, -newScreenshotAdded: NewScreenshotAdded, -newStudioRecordingAdded: NewStudioRecordingAdded, -onEscapePress: OnEscapePress, -recordingDeleted: RecordingDeleted, -recordingEvent: RecordingEvent, -recordingOptionsChanged: RecordingOptionsChanged, -recordingStarted: RecordingStarted, -recordingStopped: RecordingStopped, -renderFrameEvent: RenderFrameEvent, -requestNewScreenshot: RequestNewScreenshot, -requestOpenSettings: RequestOpenSettings, -requestStartRecording: RequestStartRecording, -targetUnderCursor: TargetUnderCursor, -uploadProgress: UploadProgress + audioInputLevelChange: AudioInputLevelChange; + authenticationInvalid: AuthenticationInvalid; + currentRecordingChanged: CurrentRecordingChanged; + downloadProgress: DownloadProgress; + editorStateChanged: EditorStateChanged; + newNotification: NewNotification; + newScreenshotAdded: NewScreenshotAdded; + newStudioRecordingAdded: NewStudioRecordingAdded; + onEscapePress: OnEscapePress; + recordingDeleted: RecordingDeleted; + recordingEvent: RecordingEvent; + recordingOptionsChanged: RecordingOptionsChanged; + recordingStarted: RecordingStarted; + recordingStopped: RecordingStopped; + renderFrameEvent: RenderFrameEvent; + requestNewScreenshot: RequestNewScreenshot; + requestOpenSettings: RequestOpenSettings; + requestStartRecording: RequestStartRecording; + targetUnderCursor: TargetUnderCursor; + uploadProgress: UploadProgress; }>({ -audioInputLevelChange: "audio-input-level-change", -authenticationInvalid: "authentication-invalid", -currentRecordingChanged: "current-recording-changed", -downloadProgress: "download-progress", -editorStateChanged: "editor-state-changed", -newNotification: "new-notification", -newScreenshotAdded: "new-screenshot-added", -newStudioRecordingAdded: "new-studio-recording-added", -onEscapePress: "on-escape-press", -recordingDeleted: "recording-deleted", -recordingEvent: "recording-event", -recordingOptionsChanged: "recording-options-changed", -recordingStarted: "recording-started", -recordingStopped: "recording-stopped", -renderFrameEvent: "render-frame-event", -requestNewScreenshot: "request-new-screenshot", -requestOpenSettings: "request-open-settings", -requestStartRecording: "request-start-recording", -targetUnderCursor: "target-under-cursor", -uploadProgress: "upload-progress" -}) + audioInputLevelChange: "audio-input-level-change", + authenticationInvalid: "authentication-invalid", + currentRecordingChanged: "current-recording-changed", + downloadProgress: "download-progress", + editorStateChanged: "editor-state-changed", + newNotification: "new-notification", + newScreenshotAdded: "new-screenshot-added", + newStudioRecordingAdded: "new-studio-recording-added", + onEscapePress: "on-escape-press", + recordingDeleted: "recording-deleted", + recordingEvent: "recording-event", + recordingOptionsChanged: "recording-options-changed", + recordingStarted: "recording-started", + recordingStopped: "recording-stopped", + renderFrameEvent: "render-frame-event", + requestNewScreenshot: "request-new-screenshot", + requestOpenSettings: "request-open-settings", + requestStartRecording: "request-start-recording", + targetUnderCursor: "target-under-cursor", + uploadProgress: "upload-progress", +}); /** user-defined constants **/ - - /** user-defined types **/ -export type AppTheme = "system" | "light" | "dark" -export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall" -export type Audio = { duration: number; sample_rate: number; channels: number; start_time: number } -export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb?: number; micStereoMode?: StereoMode; systemVolumeDb?: number } -export type AudioInputLevelChange = number -export type AudioMeta = { path: string; -/** - * unix time of the first frame - */ -start_time?: number | null } -export type AuthSecret = { api_key: string } | { token: string; expires: number } -export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; intercom_hash: string | null } -export type AuthenticationInvalid = null -export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; inset: number; crop: Crop | null; shadow?: number; advancedShadow?: ShadowConfiguration | null } -export type BackgroundSource = { type: "wallpaper"; path: string | null } | { type: "image"; path: string | null } | { type: "color"; value: [number, number, number] } | { type: "gradient"; from: [number, number, number]; to: [number, number, number]; angle?: number } -export type Camera = { hide: boolean; mirror: boolean; position: CameraPosition; size: number; zoom_size: number | null; rounding?: number; shadow?: number; advanced_shadow?: ShadowConfiguration | null; shape?: CameraShape } -export type CameraInfo = { device_id: string; model_id: ModelIDType | null; display_name: string } -export type CameraPosition = { x: CameraXPosition; y: CameraYPosition } -export type CameraPreviewShape = "round" | "square" | "full" -export type CameraPreviewSize = "sm" | "lg" -export type CameraShape = "square" | "source" -export type CameraWindowState = { size: CameraPreviewSize; shape: CameraPreviewShape; mirrored: boolean } -export type CameraXPosition = "left" | "center" | "right" -export type CameraYPosition = "top" | "bottom" -export type CaptionData = { segments: CaptionSegment[]; settings: CaptionSettings | null } -export type CaptionSegment = { id: string; start: number; end: number; text: string } -export type CaptionSettings = { enabled: boolean; font: string; size: number; color: string; backgroundColor: string; backgroundOpacity: number; position: string; bold: boolean; italic: boolean; outline: boolean; outlineColor: string; exportWithSubtitles: boolean } -export type CaptionsData = { segments: CaptionSegment[]; settings: CaptionSettings } -export type CaptureDisplay = { id: DisplayId; name: string; refresh_rate: number } -export type CaptureWindow = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number } -export type CommercialLicense = { licenseKey: string; expiryDate: number | null; refresh: number; activatedOn: number } -export type Crop = { position: XY; size: XY } -export type CurrentRecording = { target: CurrentRecordingTarget; type: RecordingType } -export type CurrentRecordingChanged = null -export type CurrentRecordingTarget = { window: { id: WindowId; bounds: LogicalBounds } } | { screen: { id: DisplayId } } | { area: { screen: DisplayId; bounds: LogicalBounds } } -export type CursorAnimationStyle = "regular" | "slow" | "fast" -export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean; motionBlur?: number; useSvg?: boolean } -export type CursorMeta = { imagePath: string; hotspot: XY; shape?: string | null } -export type CursorType = "pointer" | "circle" -export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta } -export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType } -export type DisplayId = string -export type DownloadProgress = { progress: number; message: string } -export type EditorStateChanged = { playhead_position: number } -export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato" -export type ExportEstimates = { duration_seconds: number; estimated_time_seconds: number; estimated_size_mb: number } -export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format: "Gif" } & GifExportSettings) -export type FileType = "recording" | "screenshot" -export type Flags = { captions: boolean } -export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" } -export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; customCursorCapture?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour } -export type GifExportSettings = { fps: number; resolution_base: XY; quality: GifQuality | null } -export type GifQuality = { -/** - * Encoding quality from 1-100 (default: 90) - */ -quality: number | null; -/** - * Whether to prioritize speed over quality (default: false) - */ -fast: boolean | null } -export type HapticPattern = "Alignment" | "LevelChange" | "Generic" -export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted" -export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } -export type HotkeyAction = "startRecording" | "stopRecording" | "restartRecording" -export type HotkeysConfiguration = { show: boolean } -export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } } -export type InstantRecordingMeta = { fps: number; sample_rate: number | null } -export type JsonValue = [T] -export type LogicalBounds = { position: LogicalPosition; size: LogicalSize } -export type LogicalPosition = { x: number; y: number } -export type LogicalSize = { width: number; height: number } -export type MainWindowRecordingStartBehaviour = "close" | "minimise" -export type ModelIDType = string -export type Mp4ExportSettings = { fps: number; resolution_base: XY; compression: ExportCompression } -export type MultipleSegment = { display: VideoMeta; camera?: VideoMeta | null; mic?: AudioMeta | null; system_audio?: AudioMeta | null; cursor?: string | null } -export type MultipleSegments = { segments: MultipleSegment[]; cursors: Cursors } -export type NewNotification = { title: string; body: string; is_error: boolean } -export type NewScreenshotAdded = { path: string } -export type NewStudioRecordingAdded = { path: string } -export type OSPermission = "screenRecording" | "camera" | "microphone" | "accessibility" -export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied" -export type OSPermissionsCheck = { screenRecording: OSPermissionStatus; microphone: OSPermissionStatus; camera: OSPermissionStatus; accessibility: OSPermissionStatus } -export type OnEscapePress = null -export type PhysicalSize = { width: number; height: number } -export type Plan = { upgraded: boolean; manual: boolean; last_checked: number } -export type Platform = "MacOS" | "Windows" -export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow" -export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay" -export type Preset = { name: string; config: ProjectConfiguration } -export type PresetsStore = { presets: Preset[]; default: number | null } -export type ProjectConfiguration = { aspectRatio: AspectRatio | null; background: BackgroundConfiguration; camera: Camera; audio: AudioConfiguration; cursor: CursorConfiguration; hotkeys: HotkeysConfiguration; timeline?: TimelineConfiguration | null; captions?: CaptionsData | null } -export type ProjectRecordingsMeta = { segments: SegmentRecordings[] } -export type RecordingDeleted = { path: string } -export type RecordingEvent = { variant: "Countdown"; value: number } | { variant: "Started" } | { variant: "Stopped" } | { variant: "Failed"; error: string } -export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null } -export type RecordingMetaWithType = ((StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null }) & { type: RecordingType } -export type RecordingMode = "studio" | "instant" -export type RecordingOptionsChanged = null -export type RecordingStarted = null -export type RecordingStopped = null -export type RecordingType = "studio" | "instant" -export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY } -export type RequestNewScreenshot = null -export type RequestOpenSettings = { page: string } -export type RequestStartRecording = null -export type S3UploadMeta = { id: string } -export type ScreenCaptureTarget = { variant: "window"; id: WindowId } | { variant: "screen"; id: DisplayId } | { variant: "area"; screen: DisplayId; bounds: LogicalBounds } -export type ScreenUnderCursor = { name: string; physical_size: PhysicalSize; refresh_rate: string } -export type SegmentRecordings = { display: Video; camera: Video | null; mic: Audio | null; system_audio: Audio | null } -export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordingsMeta; path: string } -export type ShadowConfiguration = { size: number; opacity: number; blur: number } -export type SharingMeta = { id: string; link: string } -export type ShowCapWindow = "Setup" | "Main" | { Settings: { page: string | null } } | { Editor: { project_path: string } } | "RecordingsOverlay" | { WindowCaptureOccluder: { screen_id: DisplayId } } | { TargetSelectOverlay: { display_id: DisplayId } } | { CaptureArea: { screen_id: DisplayId } } | "Camera" | { InProgressRecording: { countdown: number | null } } | "Upgrade" | "ModeSelect" -export type SingleSegment = { display: VideoMeta; camera?: VideoMeta | null; audio?: AudioMeta | null; cursor?: string | null } -export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; capture_system_audio?: boolean; mode: RecordingMode } -export type StereoMode = "stereo" | "monoL" | "monoR" -export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } -export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null; screen: ScreenUnderCursor | null } -export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[] } -export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } -export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" -export type UploadProgress = { progress: number } -export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired" -export type Video = { duration: number; width: number; height: number; fps: number; start_time: number } -export type VideoMeta = { path: string; fps?: number; -/** - * unix time of the first frame - */ -start_time?: number | null } -export type VideoRecordingMetadata = { duration: number; size: number } -export type VideoUploadInfo = { id: string; link: string; config: S3UploadMeta } -export type WindowId = string -export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds; icon: string | null } -export type XY = { x: T; y: T } -export type ZoomMode = "auto" | { manual: { x: number; y: number } } -export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode } +export type AppTheme = "system" | "light" | "dark"; +export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"; +export type Audio = { + duration: number; + sample_rate: number; + channels: number; + start_time: number; +}; +export type AudioConfiguration = { + mute: boolean; + improve: boolean; + micVolumeDb?: number; + micStereoMode?: StereoMode; + systemVolumeDb?: number; +}; +export type AudioInputLevelChange = number; +export type AudioMeta = { + path: string; + /** + * unix time of the first frame + */ + start_time?: number | null; +}; +export type AuthSecret = + | { api_key: string } + | { token: string; expires: number }; +export type AuthStore = { + secret: AuthSecret; + user_id: string | null; + plan: Plan | null; + intercom_hash: string | null; +}; +export type AuthenticationInvalid = null; +export type BackgroundConfiguration = { + source: BackgroundSource; + blur: number; + padding: number; + rounding: number; + inset: number; + crop: Crop | null; + shadow?: number; + advancedShadow?: ShadowConfiguration | null; +}; +export type BackgroundSource = + | { type: "wallpaper"; path: string | null } + | { type: "image"; path: string | null } + | { type: "color"; value: [number, number, number] } + | { + type: "gradient"; + from: [number, number, number]; + to: [number, number, number]; + angle?: number; + }; +export type Camera = { + hide: boolean; + mirror: boolean; + position: CameraPosition; + size: number; + zoom_size: number | null; + rounding?: number; + shadow?: number; + advanced_shadow?: ShadowConfiguration | null; + shape?: CameraShape; +}; +export type CameraInfo = { + device_id: string; + model_id: ModelIDType | null; + display_name: string; +}; +export type CameraPosition = { x: CameraXPosition; y: CameraYPosition }; +export type CameraPreviewShape = "round" | "square" | "full"; +export type CameraPreviewSize = "sm" | "lg"; +export type CameraShape = "square" | "source"; +export type CameraWindowState = { + size: CameraPreviewSize; + shape: CameraPreviewShape; + mirrored: boolean; +}; +export type CameraXPosition = "left" | "center" | "right"; +export type CameraYPosition = "top" | "bottom"; +export type CaptionData = { + segments: CaptionSegment[]; + settings: CaptionSettings | null; +}; +export type CaptionSegment = { + id: string; + start: number; + end: number; + text: string; +}; +export type CaptionSettings = { + enabled: boolean; + font: string; + size: number; + color: string; + backgroundColor: string; + backgroundOpacity: number; + position: string; + bold: boolean; + italic: boolean; + outline: boolean; + outlineColor: string; + exportWithSubtitles: boolean; +}; +export type CaptionsData = { + segments: CaptionSegment[]; + settings: CaptionSettings; +}; +export type CaptureDisplay = { + id: DisplayId; + name: string; + refresh_rate: number; +}; +export type CaptureWindow = { + id: WindowId; + owner_name: string; + name: string; + bounds: LogicalBounds; + refresh_rate: number; +}; +export type CommercialLicense = { + licenseKey: string; + expiryDate: number | null; + refresh: number; + activatedOn: number; +}; +export type Crop = { position: XY; size: XY }; +export type CurrentRecording = { + target: CurrentRecordingTarget; + type: RecordingType; +}; +export type CurrentRecordingChanged = null; +export type CurrentRecordingTarget = + | { window: { id: WindowId; bounds: LogicalBounds } } + | { screen: { id: DisplayId } } + | { area: { screen: DisplayId; bounds: LogicalBounds } }; +export type CursorAnimationStyle = "regular" | "slow" | "fast"; +export type CursorConfiguration = { + hide?: boolean; + hideWhenIdle: boolean; + size: number; + type: CursorType; + animationStyle: CursorAnimationStyle; + tension: number; + mass: number; + friction: number; + raw?: boolean; + motionBlur?: number; + useSvg?: boolean; +}; +export type CursorMeta = { + imagePath: string; + hotspot: XY; + shape?: string | null; +}; +export type CursorType = "pointer" | "circle"; +export type Cursors = + | { [key in string]: string } + | { [key in string]: CursorMeta }; +export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType }; +export type DisplayId = string; +export type DownloadProgress = { progress: number; message: string }; +export type EditorStateChanged = { playhead_position: number }; +export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato"; +export type ExportEstimates = { + duration_seconds: number; + estimated_time_seconds: number; + estimated_size_mb: number; +}; +export type ExportSettings = + | ({ format: "Mp4" } & Mp4ExportSettings) + | ({ format: "Gif" } & GifExportSettings); +export type FileType = "recording" | "screenshot"; +export type Flags = { captions: boolean }; +export type FramesRendered = { + renderedCount: number; + totalFrames: number; + type: "FramesRendered"; +}; +export type GeneralSettingsStore = { + instanceId?: string; + uploadIndividualFiles?: boolean; + hideDockIcon?: boolean; + hapticsEnabled?: boolean; + autoCreateShareableLink?: boolean; + enableNotifications?: boolean; + disableAutoOpenLinks?: boolean; + hasCompletedStartup?: boolean; + theme?: AppTheme; + commercialLicense?: CommercialLicense | null; + lastVersion?: string | null; + windowTransparency?: boolean; + postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; + mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; + customCursorCapture?: boolean; + serverUrl?: string; + recordingCountdown?: number | null; + enableNativeCameraPreview: boolean; + autoZoomOnClicks?: boolean; + enableNewRecordingFlow: boolean; + postDeletionBehaviour?: PostDeletionBehaviour; +}; +export type GifExportSettings = { + fps: number; + resolution_base: XY; + quality: GifQuality | null; +}; +export type GifQuality = { + /** + * Encoding quality from 1-100 (default: 90) + */ + quality: number | null; + /** + * Whether to prioritize speed over quality (default: false) + */ + fast: boolean | null; +}; +export type HapticPattern = "Alignment" | "LevelChange" | "Generic"; +export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted"; +export type Hotkey = { + code: string; + meta: boolean; + ctrl: boolean; + alt: boolean; + shift: boolean; +}; +export type HotkeyAction = + | "startRecording" + | "stopRecording" + | "restartRecording"; +export type HotkeysConfiguration = { show: boolean }; +export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }; +export type InstantRecordingMeta = { fps: number; sample_rate: number | null }; +export type JsonValue = [T]; +export type LogicalBounds = { position: LogicalPosition; size: LogicalSize }; +export type LogicalPosition = { x: number; y: number }; +export type LogicalSize = { width: number; height: number }; +export type MainWindowRecordingStartBehaviour = "close" | "minimise"; +export type ModelIDType = string; +export type Mp4ExportSettings = { + fps: number; + resolution_base: XY; + compression: ExportCompression; +}; +export type MultipleSegment = { + display: VideoMeta; + camera?: VideoMeta | null; + mic?: AudioMeta | null; + system_audio?: AudioMeta | null; + cursor?: string | null; +}; +export type MultipleSegments = { + segments: MultipleSegment[]; + cursors: Cursors; +}; +export type NewNotification = { + title: string; + body: string; + is_error: boolean; +}; +export type NewScreenshotAdded = { path: string }; +export type NewStudioRecordingAdded = { path: string }; +export type OSPermission = + | "screenRecording" + | "camera" + | "microphone" + | "accessibility"; +export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied"; +export type OSPermissionsCheck = { + screenRecording: OSPermissionStatus; + microphone: OSPermissionStatus; + camera: OSPermissionStatus; + accessibility: OSPermissionStatus; +}; +export type OnEscapePress = null; +export type PhysicalSize = { width: number; height: number }; +export type Plan = { upgraded: boolean; manual: boolean; last_checked: number }; +export type Platform = "MacOS" | "Windows"; +export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow"; +export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay"; +export type Preset = { name: string; config: ProjectConfiguration }; +export type PresetsStore = { presets: Preset[]; default: number | null }; +export type ProjectConfiguration = { + aspectRatio: AspectRatio | null; + background: BackgroundConfiguration; + camera: Camera; + audio: AudioConfiguration; + cursor: CursorConfiguration; + hotkeys: HotkeysConfiguration; + timeline?: TimelineConfiguration | null; + captions?: CaptionsData | null; +}; +export type ProjectRecordingsMeta = { segments: SegmentRecordings[] }; +export type RecordingDeleted = { path: string }; +export type RecordingEvent = + | { variant: "Countdown"; value: number } + | { variant: "Started" } + | { variant: "Stopped" } + | { variant: "Failed"; error: string }; +export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { + platform?: Platform | null; + pretty_name: string; + sharing?: SharingMeta | null; +}; +export type RecordingMetaWithType = (( + | StudioRecordingMeta + | InstantRecordingMeta +) & { + platform?: Platform | null; + pretty_name: string; + sharing?: SharingMeta | null; +}) & { type: RecordingType }; +export type RecordingMode = "studio" | "instant"; +export type RecordingOptionsChanged = null; +export type RecordingStarted = null; +export type RecordingStopped = null; +export type RecordingType = "studio" | "instant"; +export type RenderFrameEvent = { + frame_number: number; + fps: number; + resolution_base: XY; +}; +export type RequestNewScreenshot = null; +export type RequestOpenSettings = { page: string }; +export type RequestStartRecording = null; +export type S3UploadMeta = { id: string }; +export type ScreenCaptureTarget = + | { variant: "window"; id: WindowId } + | { variant: "screen"; id: DisplayId } + | { variant: "area"; screen: DisplayId; bounds: LogicalBounds }; +export type ScreenUnderCursor = { + name: string; + physical_size: PhysicalSize; + refresh_rate: string; +}; +export type SegmentRecordings = { + display: Video; + camera: Video | null; + mic: Audio | null; + system_audio: Audio | null; +}; +export type SerializedEditorInstance = { + framesSocketUrl: string; + recordingDuration: number; + savedProjectConfig: ProjectConfiguration; + recordings: ProjectRecordingsMeta; + path: string; +}; +export type ShadowConfiguration = { + size: number; + opacity: number; + blur: number; +}; +export type SharingMeta = { id: string; link: string }; +export type ShowCapWindow = + | "Setup" + | "Main" + | { Settings: { page: string | null } } + | { Editor: { project_path: string } } + | "RecordingsOverlay" + | { WindowCaptureOccluder: { screen_id: DisplayId } } + | { TargetSelectOverlay: { display_id: DisplayId } } + | { CaptureArea: { screen_id: DisplayId } } + | "Camera" + | { InProgressRecording: { countdown: number | null } } + | "Upgrade" + | "ModeSelect"; +export type SingleSegment = { + display: VideoMeta; + camera?: VideoMeta | null; + audio?: AudioMeta | null; + cursor?: string | null; +}; +export type StartRecordingInputs = { + capture_target: ScreenCaptureTarget; + capture_system_audio?: boolean; + mode: RecordingMode; +}; +export type StereoMode = "stereo" | "monoL" | "monoR"; +export type StudioRecordingMeta = + | { segment: SingleSegment } + | { inner: MultipleSegments }; +export type TargetUnderCursor = { + display_id: DisplayId | null; + window: WindowUnderCursor | null; + screen: ScreenUnderCursor | null; +}; +export type TimelineConfiguration = { + segments: TimelineSegment[]; + zoomSegments: ZoomSegment[]; +}; +export type TimelineSegment = { + recordingSegment?: number; + timescale: number; + start: number; + end: number; +}; +export type UploadMode = + | { Initial: { pre_created_video: VideoUploadInfo | null } } + | "Reupload"; +export type UploadProgress = { progress: number }; +export type UploadResult = + | { Success: string } + | "NotAuthenticated" + | "PlanCheckFailed" + | "UpgradeRequired"; +export type Video = { + duration: number; + width: number; + height: number; + fps: number; + start_time: number; +}; +export type VideoMeta = { + path: string; + fps?: number; + /** + * unix time of the first frame + */ + start_time?: number | null; +}; +export type VideoRecordingMetadata = { duration: number; size: number }; +export type VideoUploadInfo = { + id: string; + link: string; + config: S3UploadMeta; +}; +export type WindowId = string; +export type WindowUnderCursor = { + id: WindowId; + app_name: string; + bounds: LogicalBounds; + icon: string | null; +}; +export type XY = { x: T; y: T }; +export type ZoomMode = "auto" | { manual: { x: number; y: number } }; +export type ZoomSegment = { + start: number; + end: number; + amount: number; + mode: ZoomMode; +}; /** tauri-specta globals **/ import { + type Channel as TAURI_CHANNEL, invoke as TAURI_INVOKE, - Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; +import type { WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; type __EventObj__ = { listen: ( @@ -480,9 +827,8 @@ function __makeEvents__>( ) { return new Proxy( {} as unknown as { - [K in keyof T]: __EventObj__ & { - (handle: __WebviewWindow__): __EventObj__; - }; + [K in keyof T]: __EventObj__ & + ((handle: __WebviewWindow__) => __EventObj__); }, { get: (_, event) => { diff --git a/crates/camera-directshow/examples/cli.rs b/crates/camera-directshow/examples/cli.rs index af19899f03..fd4ceed2cc 100644 --- a/crates/camera-directshow/examples/cli.rs +++ b/crates/camera-directshow/examples/cli.rs @@ -1,6 +1,6 @@ fn main() { #[cfg(windows)] - windows::run(); + windows::main(); #[cfg(not(windows))] panic!("This example is only available on Windows"); } diff --git a/crates/camera-mediafoundation/examples/cli.rs b/crates/camera-mediafoundation/examples/cli.rs index 2916d4eb94..317e15f65c 100644 --- a/crates/camera-mediafoundation/examples/cli.rs +++ b/crates/camera-mediafoundation/examples/cli.rs @@ -1,6 +1,6 @@ fn main() { #[cfg(windows)] - windows::run(); + windows::main(); #[cfg(not(windows))] panic!("This example is only available on Windows"); } diff --git a/crates/camera-windows/examples/cli.rs b/crates/camera-windows/examples/cli.rs index 7c4c598a41..cf73abca5c 100644 --- a/crates/camera-windows/examples/cli.rs +++ b/crates/camera-windows/examples/cli.rs @@ -1,6 +1,6 @@ fn main() { #[cfg(windows)] - windows::run(); + windows::main(); #[cfg(not(windows))] panic!("This example is only available on Windows"); } @@ -11,7 +11,7 @@ mod windows { use cap_camera_windows::*; - fn run() { + pub fn main() { let devices = get_devices() .unwrap() .into_iter() diff --git a/crates/cursor-capture/src/position.rs b/crates/cursor-capture/src/position.rs index 68f0c2bef8..db031db9ed 100644 --- a/crates/cursor-capture/src/position.rs +++ b/crates/cursor-capture/src/position.rs @@ -1,11 +1,12 @@ use cap_displays::{ Display, - bounds::{LogicalPosition, LogicalSize}, + bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, }; use device_query::{DeviceQuery, DeviceState}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct RawCursorPosition { + // inner: PhysicalPosition, pub x: i32, pub y: i32, } @@ -21,7 +22,7 @@ impl RawCursorPosition { } } - pub fn relative_to_display(&self, display: Display) -> RelativeCursorPosition { + pub fn relative_to_display(&self, display: Display) -> Option { RelativeCursorPosition::from_raw(*self, display) } } @@ -35,14 +36,14 @@ pub struct RelativeCursorPosition { } impl RelativeCursorPosition { - pub fn from_raw(raw: RawCursorPosition, display: Display) -> Self { - let logical_bounds = display.logical_bounds(); + pub fn from_raw(raw: RawCursorPosition, display: Display) -> Option { + let physical_bounds = display.physical_bounds()?; - Self { - x: raw.x - logical_bounds.position().x() as i32, - y: raw.y - logical_bounds.position().y() as i32, + Some(Self { + x: raw.x - physical_bounds.position().x() as i32, + y: raw.y - physical_bounds.position().y() as i32, display, - } + }) } pub fn x(&self) -> i32 { @@ -57,18 +58,17 @@ impl RelativeCursorPosition { &self.display } - pub fn normalize(&self) -> NormalizedCursorPosition { - let bounds = self.display().logical_bounds(); + pub fn normalize(&self) -> Option { + let bounds = self.display().physical_bounds()?; let size = bounds.size(); - let position = bounds.position(); - NormalizedCursorPosition { + Some(NormalizedCursorPosition { x: self.x as f64 / size.width(), y: self.y as f64 / size.height(), - crop_position: LogicalPosition::new(position.x(), position.y()), - crop_size: LogicalSize::new(size.width(), size.height()), + crop_position: bounds.position(), + crop_size: size, display: self.display, - } + }) } } @@ -84,8 +84,8 @@ impl std::fmt::Debug for RelativeCursorPosition { pub struct NormalizedCursorPosition { pub(crate) x: f64, pub(crate) y: f64, - pub(crate) crop_position: LogicalPosition, - pub(crate) crop_size: LogicalSize, + pub(crate) crop_position: PhysicalPosition, + pub(crate) crop_size: PhysicalSize, pub(crate) display: Display, } @@ -102,15 +102,15 @@ impl NormalizedCursorPosition { &self.display } - pub fn crop_position(&self) -> LogicalPosition { + pub fn crop_position(&self) -> PhysicalPosition { self.crop_position } - pub fn crop_size(&self) -> LogicalSize { + pub fn crop_size(&self) -> PhysicalSize { self.crop_size } - pub fn with_crop(&self, position: LogicalPosition, size: LogicalSize) -> Self { + pub fn with_crop(&self, position: PhysicalPosition, size: PhysicalSize) -> Self { let raw_px = ( self.x * self.crop_size.width() + self.crop_position.x(), self.y * self.crop_size.height() + self.crop_position.y(), diff --git a/crates/displays/Cargo.toml b/crates/displays/Cargo.toml index 863f56fd58..5cd8174e4f 100644 --- a/crates/displays/Cargo.toml +++ b/crates/displays/Cargo.toml @@ -10,6 +10,7 @@ workspace = true serde = { version = "1.0.219", features = ["derive"] } specta.workspace = true image = "0.24" +tracing.workspace = true [target.'cfg(target_os = "macos")'.dependencies] cidre = { workspace = true, default-features = false, features = ["sc"] } @@ -29,6 +30,7 @@ windows = { workspace = true, features = [ "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_UI_HiDpi", + "Win32_Graphics_Dwm", "Win32_Graphics_Gdi", "Win32_Storage_FileSystem", "Win32_Devices_Display", diff --git a/crates/displays/src/lib.rs b/crates/displays/src/lib.rs index dc65417bfc..403b3b893b 100644 --- a/crates/displays/src/lib.rs +++ b/crates/displays/src/lib.rs @@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize}; use specta::Type; use std::str::FromStr; +use crate::bounds::{LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition}; + #[derive(Clone, Copy)] pub struct Display(DisplayImpl); @@ -39,21 +41,25 @@ impl Display { self.0.name() } - pub fn physical_size(&self) -> PhysicalSize { + pub fn physical_size(&self) -> Option { self.0.physical_size() } - pub fn refresh_rate(&self) -> f64 { - self.0.refresh_rate() + pub fn physical_position(&self) -> Option { + self.0.physical_position() } - pub fn logical_bounds(&self) -> LogicalBounds { - self.0.logical_bounds() + pub fn physical_bounds(&self) -> Option { + self.0.physical_bounds() } - // pub fn physical_bounds(&self) -> Option { - // self.0.physical_bounds() - // } + pub fn logical_size(&self) -> Option { + self.0.logical_size() + } + + pub fn refresh_rate(&self) -> f64 { + self.0.refresh_rate() + } } #[derive(Serialize, Deserialize, Type, Clone, PartialEq, Debug)] @@ -125,17 +131,21 @@ impl Window { Self::list().into_iter().find(|d| &d.id() == id) } - pub fn logical_bounds(&self) -> Option { - self.0.logical_bounds() + pub fn physical_size(&self) -> Option { + self.0.physical_size() } - pub fn physical_size(&self) -> Option { + pub fn physical_position(&self) -> Option { self.0.physical_size() } - // pub fn physical_bounds(&self) -> Option { - // self.0.physical_bounds() - // } + pub fn physical_bounds(&self) -> Option { + self.0.physical_bounds() + } + + pub fn logical_size(&self) -> Option { + self.0.logical_size() + } pub fn owner_name(&self) -> Option { self.0.owner_name() @@ -156,6 +166,34 @@ impl Window { pub fn name(&self) -> Option { self.0.name() } + + pub fn display_relative_logical_bounds(&self) -> Option { + let display = self.display()?; + let display_physical_bounds = display.physical_bounds()?; + let display_logical_size = display.logical_size()?; + let window_physical_bounds = self.physical_bounds()?; + + let scale = display_logical_size.width() / display_physical_bounds.size().width; + + let display_relative_physical_bounds = PhysicalBounds::new( + PhysicalPosition::new( + window_physical_bounds.position().x - display_physical_bounds.position().x, + window_physical_bounds.position().y - display_physical_bounds.position().y, + ), + window_physical_bounds.size(), + ); + + Some(LogicalBounds::new( + LogicalPosition::new( + display_relative_physical_bounds.position().x() * scale, + display_relative_physical_bounds.position().y() * scale, + ), + LogicalSize::new( + display_relative_physical_bounds.size().width() * scale, + display_relative_physical_bounds.size().height() * scale, + ), + )) + } } #[derive(Serialize, Deserialize, Type, Clone, PartialEq, Debug)] diff --git a/crates/displays/src/main.rs b/crates/displays/src/main.rs index 037c06549d..ede56a5e5a 100644 --- a/crates/displays/src/main.rs +++ b/crates/displays/src/main.rs @@ -1,135 +1,173 @@ use std::time::Duration; +use cap_displays::{Display, Window}; +use windows::Win32::UI::HiDpi::{ + PROCESS_DPI_UNAWARE, PROCESS_PER_MONITOR_DPI_AWARE, PROCESS_SYSTEM_DPI_AWARE, + SetProcessDpiAwareness, +}; + fn main() { - // Test display functionality - println!("=== Display Information ==="); - for (index, display) in cap_displays::Display::list().iter().enumerate() { - println!("Display {}: {}", index + 1, display.name().unwrap()); - println!(" ID: {}", display.id()); - - let logical_size = display.raw_handle().logical_size(); - let physical_size = display.physical_size(); - let refresh_rate = display.refresh_rate(); - - println!( - " Logical Resolution: {}x{}", - logical_size.width(), - logical_size.height() - ); - println!( - " Physical Resolution: {}x{}", - physical_size.width(), - physical_size.height() - ); - - if refresh_rate > 0.0 { - println!(" Refresh Rate: {} Hz", refresh_rate); - } else { - println!(" Refresh Rate: Unknown"); - } + unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE).unwrap() }; - // Check if this is the main display - let main_display_id = cap_displays::Display::list().first().map(|d| d.id()); - - if let Some(main_id) = main_display_id { - if display.id() == main_id { - println!(" Type: Primary Display"); - } else { - println!(" Type: Secondary Display"); - } - } else { - println!(" Type: Unknown"); - } + for display in Display::list() { + dbg!(display.name()); - println!(); - } + let display = display.raw_handle(); - if let Some(cursor_display) = cap_displays::Display::get_containing_cursor() { - println!( - "🖱️ Cursor is currently on: {}", - cursor_display.name().unwrap() - ); - println!(); + dbg!(display.physical_bounds()); + dbg!(display.logical_size()); } - // Test window functionality - println!("=== Windows Under Cursor ==="); - let windows = cap_displays::Window::list_containing_cursor(); - - if windows.is_empty() { - println!("No windows found under cursor"); - } else { - println!("Found {} window(s) under cursor:", windows.len()); - for (index, window) in windows.iter().take(5).enumerate() { - // Limit to first 5 windows - println!("\nWindow {}: {}", index + 1, window.id()); - - if let Some(bounds) = window.logical_bounds() { - println!( - " Bounds: {}x{} at ({}, {})", - bounds.size().width(), - bounds.size().height(), - bounds.position().x(), - bounds.position().y() - ); - } - - if let Some(owner) = window.owner_name() { - println!(" Application: {}", owner); - } else { - println!(" Application: Unknown"); - } - - // Test icon functionality - match window.app_icon() { - Some(icon_data) => { - println!(" Icon (Standard): {} bytes", icon_data.len()); - println!(" Format: PNG (Raw bytes)"); - println!(" Size: {} bytes", icon_data.len()); - } - None => println!(" Icon (Standard): Not available"), - } - } - } - - println!("\n=== Topmost Window Icon Test ==="); - if let Some(topmost) = cap_displays::Window::get_topmost_at_cursor() - && let Some(owner) = topmost.owner_name() - { - println!("Testing icon extraction for: {}", owner); - - match topmost.app_icon() { - Some(icon_data) => { - println!(" ✅ Icon found: {} bytes", icon_data.len()); - println!(" Format: PNG (Raw bytes)"); - println!(" Size: {} bytes", icon_data.len()); - } - None => println!(" ❌ No icon found"), - } - } - - println!("\n=== Live Monitoring (Press Ctrl+C to exit) ==="); - println!("Monitoring window levels under cursor...\n"); - - loop { - let mut relevant_windows = cap_displays::WindowImpl::list_containing_cursor() - .into_iter() - .filter_map(|window| { - let level = window.level()?; - level.lt(&5).then_some((window, level)) - }) - .collect::>(); - - relevant_windows.sort_by(|a, b| b.1.cmp(&a.1)); - - // Print current topmost window info - if let Some((topmost_window, level)) = relevant_windows.first() - && let Some(owner) = topmost_window.owner_name() + for win in cap_displays::Window::list() { + if let Some(name) = win.name() + && name.contains("Firefox") { - print!("\rTopmost: {} (level: {}) ", owner, level); - std::io::Write::flush(&mut std::io::stdout()).unwrap(); + dbg!(win.physical_bounds()); + dbg!(win.logical_size()); } - - std::thread::sleep(Duration::from_millis(100)); } + + return; + + // loop { + // for win in cap_displays::Window::list() { + // if let Some(name) = win.name() + // && name.contains("Firefox") + // { + // dbg!(win.logical_bounds()); + // // dbg!(win.physical_size()); + // } + // } + // } + // Test display functionality + // println!("=== Display Information ==="); + // for (index, display) in cap_displays::Display::list().iter().enumerate() { + // println!("Display {}: {}", index + 1, display.name().unwrap()); + // println!(" ID: {}", display.id()); + + // let logical_size = display.raw_handle().logical_size(); + // let physical_size = display.physical_size(); + // let refresh_rate = display.refresh_rate(); + + // println!( + // " Logical Resolution: {}x{}", + // logical_size.width(), + // logical_size.height() + // ); + // println!( + // " Physical Resolution: {}x{}", + // physical_size.width(), + // physical_size.height() + // ); + + // if refresh_rate > 0.0 { + // println!(" Refresh Rate: {} Hz", refresh_rate); + // } else { + // println!(" Refresh Rate: Unknown"); + // } + + // // Check if this is the main display + // let main_display_id = cap_displays::Display::list().first().map(|d| d.id()); + + // if let Some(main_id) = main_display_id { + // if display.id() == main_id { + // println!(" Type: Primary Display"); + // } else { + // println!(" Type: Secondary Display"); + // } + // } else { + // println!(" Type: Unknown"); + // } + + // println!(); + // } + + // if let Some(cursor_display) = cap_displays::Display::get_containing_cursor() { + // println!( + // "🖱️ Cursor is currently on: {}", + // cursor_display.name().unwrap() + // ); + // println!(); + // } + + // // Test window functionality + // println!("=== Windows Under Cursor ==="); + // let windows = cap_displays::Window::list_containing_cursor(); + + // if windows.is_empty() { + // println!("No windows found under cursor"); + // } else { + // println!("Found {} window(s) under cursor:", windows.len()); + // for (index, window) in windows.iter().take(5).enumerate() { + // // Limit to first 5 windows + // println!("\nWindow {}: {}", index + 1, window.id()); + + // if let Some(bounds) = window.logical_bounds() { + // println!( + // " Bounds: {}x{} at ({}, {})", + // bounds.size().width(), + // bounds.size().height(), + // bounds.position().x(), + // bounds.position().y() + // ); + // } + + // if let Some(owner) = window.owner_name() { + // println!(" Application: {}", owner); + // } else { + // println!(" Application: Unknown"); + // } + + // // Test icon functionality + // match window.app_icon() { + // Some(icon_data) => { + // println!(" Icon (Standard): {} bytes", icon_data.len()); + // println!(" Format: PNG (Raw bytes)"); + // println!(" Size: {} bytes", icon_data.len()); + // } + // None => println!(" Icon (Standard): Not available"), + // } + // } + // } + + // println!("\n=== Topmost Window Icon Test ==="); + // if let Some(topmost) = cap_displays::Window::get_topmost_at_cursor() + // && let Some(owner) = topmost.owner_name() + // { + // println!("Testing icon extraction for: {}", owner); + + // match topmost.app_icon() { + // Some(icon_data) => { + // println!(" ✅ Icon found: {} bytes", icon_data.len()); + // println!(" Format: PNG (Raw bytes)"); + // println!(" Size: {} bytes", icon_data.len()); + // } + // None => println!(" ❌ No icon found"), + // } + // } + + // println!("\n=== Live Monitoring (Press Ctrl+C to exit) ==="); + // println!("Monitoring window levels under cursor...\n"); + + // loop { + // let mut relevant_windows = cap_displays::WindowImpl::list_containing_cursor() + // .into_iter() + // .filter_map(|window| { + // let level = window.level()?; + // level.lt(&5).then_some((window, level)) + // }) + // .collect::>(); + + // relevant_windows.sort_by(|a, b| b.1.cmp(&a.1)); + + // // Print current topmost window info + // if let Some((topmost_window, level)) = relevant_windows.first() + // && let Some(owner) = topmost_window.owner_name() + // { + // print!("\rTopmost: {} (level: {}) ", owner, level); + // std::io::Write::flush(&mut std::io::stdout()).unwrap(); + // } + + // std::thread::sleep(Duration::from_millis(100)); + // } } diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs index 8c2297f041..9802e4b43c 100644 --- a/crates/displays/src/platform/win.rs +++ b/crates/displays/src/platform/win.rs @@ -1,5 +1,5 @@ use std::{mem, str::FromStr}; - +use tracing::error; use windows::{ Graphics::Capture::GraphicsCaptureItem, Win32::{ @@ -12,14 +12,17 @@ use windows::{ QueryDisplayConfig, }, Foundation::{CloseHandle, HWND, LPARAM, POINT, RECT, TRUE, WIN32_ERROR, WPARAM}, - Graphics::Gdi::{ - BI_RGB, BITMAP, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, - CreateCompatibleDC, CreateSolidBrush, DEVMODEW, DIB_RGB_COLORS, - DISPLAY_DEVICE_STATE_FLAGS, DISPLAY_DEVICEW, DeleteDC, DeleteObject, - ENUM_CURRENT_SETTINGS, EnumDisplayDevicesW, EnumDisplayMonitors, EnumDisplaySettingsW, - FillRect, GetDC, GetDIBits, GetMonitorInfoW, GetObjectA, HBRUSH, HDC, HGDIOBJ, - HMONITOR, MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTONULL, MONITORINFOEXW, - MonitorFromPoint, MonitorFromWindow, ReleaseDC, SelectObject, + Graphics::{ + Dwm::{DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute}, + Gdi::{ + BI_RGB, BITMAP, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, + CreateCompatibleDC, CreateSolidBrush, DEVMODEW, DIB_RGB_COLORS, + DISPLAY_DEVICE_STATE_FLAGS, DISPLAY_DEVICEW, DeleteDC, DeleteObject, + ENUM_CURRENT_SETTINGS, EnumDisplayDevicesW, EnumDisplayMonitors, + EnumDisplaySettingsW, FillRect, GetDC, GetDIBits, GetMonitorInfoW, GetObjectA, + HBRUSH, HDC, HGDIOBJ, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTONULL, + MONITORINFOEXW, MonitorFromPoint, MonitorFromWindow, ReleaseDC, SelectObject, + }, }, Storage::FileSystem::{GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW}, System::{ @@ -34,7 +37,11 @@ use windows::{ WinRT::Graphics::Capture::IGraphicsCaptureItemInterop, }, UI::{ - HiDpi::GetDpiForWindow, + HiDpi::{ + DPI_AWARENESS_UNAWARE, GetDpiForMonitor, GetDpiForWindow, GetProcessDpiAwareness, + MDT_DEFAULT, MDT_EFFECTIVE_DPI, MDT_RAW_DPI, PROCESS_DPI_UNAWARE, + PROCESS_PER_MONITOR_DPI_AWARE, + }, Shell::ExtractIconExW, WindowsAndMessaging::{ DI_FLAGS, DestroyIcon, DrawIconEx, EnumWindows, GCLP_HICON, GW_HWNDNEXT, @@ -53,6 +60,11 @@ use crate::bounds::{ LogicalBounds, LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, }; +// All of this assumes PROCESS_PER_MONITOR_DPI_AWARE +// +// On Windows it's nigh impossible to get the logical position of a display +// or window, since there's no simple API that accounts for each monitor having different DPI. + #[derive(Clone, Copy)] pub struct DisplayImpl(HMONITOR); @@ -120,48 +132,21 @@ impl DisplayImpl { Self::list().into_iter().find(|d| d.raw_id().0 == parsed_id) } - pub fn logical_size(&self) -> LogicalSize { - let mut info = MONITORINFOEXW::default(); - info.monitorInfo.cbSize = mem::size_of::() as u32; + pub fn logical_size(&self) -> Option { + let physical_size = self.physical_size()?; - unsafe { - if GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _).as_bool() { - let rect = info.monitorInfo.rcMonitor; - LogicalSize { - width: (rect.right - rect.left) as f64, - height: (rect.bottom - rect.top) as f64, - } - } else { - LogicalSize { - width: 0.0, - height: 0.0, - } - } - } - } - - pub fn logical_position(&self) -> LogicalPosition { - let mut info = MONITORINFOEXW::default(); - info.monitorInfo.cbSize = mem::size_of::() as u32; + let dpi = unsafe { + let mut dpi_x = 0; + GetDpiForMonitor(self.0, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut 0).ok()?; + dpi_x + }; - unsafe { - if GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _).as_bool() { - let rect = info.monitorInfo.rcMonitor; - LogicalPosition { - x: rect.left as f64, - y: rect.top as f64, - } - } else { - LogicalPosition { x: 0.0, y: 0.0 } - } - } - } + let scale = dpi as f64 / 96.0; - pub fn logical_bounds(&self) -> LogicalBounds { - LogicalBounds { - size: self.logical_size(), - position: self.logical_position(), - } + Some(LogicalSize::new( + physical_size.width() / scale, + physical_size.height() / scale, + )) } pub fn get_containing_cursor() -> Option { @@ -179,40 +164,30 @@ impl DisplayImpl { } } - pub fn physical_size(&self) -> PhysicalSize { + pub fn physical_bounds(&self) -> Option { let mut info = MONITORINFOEXW::default(); info.monitorInfo.cbSize = mem::size_of::() as u32; - unsafe { - if GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _).as_bool() { - let device_name = info.szDevice; - let mut devmode = DEVMODEW::default(); - devmode.dmSize = mem::size_of::() as u16; - - if EnumDisplaySettingsW( - PCWSTR(device_name.as_ptr()), - ENUM_CURRENT_SETTINGS, - &mut devmode, + unsafe { GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _) } + .as_bool() + .then(|| { + let rect = info.monitorInfo.rcMonitor; + PhysicalBounds::new( + PhysicalPosition::new(rect.left as f64, rect.top as f64), + PhysicalSize::new( + rect.right as f64 - rect.left as f64, + rect.bottom as f64 - rect.top as f64, + ), ) - .as_bool() - { - PhysicalSize { - width: devmode.dmPelsWidth as f64, - height: devmode.dmPelsHeight as f64, - } - } else { - PhysicalSize { - width: 0.0, - height: 0.0, - } - } - } else { - PhysicalSize { - width: 0.0, - height: 0.0, - } - } - } + }) + } + + pub fn physical_position(&self) -> Option { + Some(self.physical_bounds()?.position()) + } + + pub fn physical_size(&self) -> Option { + Some(self.physical_bounds()?.size()) } pub fn refresh_rate(&self) -> f64 { @@ -666,11 +641,11 @@ impl DisplayImpl { } } -fn get_cursor_position() -> Option { +fn get_cursor_position() -> Option { let mut point = POINT { x: 0, y: 0 }; unsafe { if GetCursorPos(&mut point).is_ok() { - Some(LogicalPosition { + Some(PhysicalPosition { x: point.x as f64, y: point.y as f64, }) @@ -814,7 +789,7 @@ impl WindowImpl { Self::list() .into_iter() .filter_map(|window| { - let bounds = window.logical_bounds()?; + let bounds = window.physical_bounds()?; bounds.contains_point(cursor).then_some(window) }) .collect() @@ -1232,51 +1207,77 @@ impl WindowImpl { } } - pub fn logical_bounds(&self) -> Option { + pub fn logical_size(&self) -> Option { let mut rect = RECT::default(); + unsafe { - if GetWindowRect(self.0, &mut rect).is_ok() { - // Get DPI scaling factor to convert physical to logical coordinates - const BASE_DPI: f64 = 96.0; - let dpi = match GetDpiForWindow(self.0) { - 0 => BASE_DPI as u32, - dpi => dpi, - } as f64; - let scale_factor = dpi / BASE_DPI; - - Some(LogicalBounds { - position: LogicalPosition { - x: rect.left as f64 / scale_factor, - y: rect.top as f64 / scale_factor, - }, - size: LogicalSize { - width: (rect.right - rect.left) as f64 / scale_factor, - height: (rect.bottom - rect.top) as f64 / scale_factor, - }, - }) - } else { - None + match GetProcessDpiAwareness(None) { + Ok(PROCESS_PER_MONITOR_DPI_AWARE) => {} + Err(e) => { + error!("Failed to get process DPI awareness: {e}"); + return None; + } + Ok(v) => { + error!("Unsupported DPI awareness {v:?}"); + return None; + } } + + DwmGetWindowAttribute( + self.0, + DWMWA_EXTENDED_FRAME_BOUNDS, + (&raw mut rect).cast(), + size_of::() as u32, + ) + .ok()?; + + const BASE_DPI: f64 = 96.0; + let dpi = match GetDpiForWindow(self.0) { + 0 => BASE_DPI as u32, + dpi => dpi, + } as f64; + let scale_factor = dpi / BASE_DPI; + + Some(LogicalSize { + width: (rect.right - rect.left) as f64 / scale_factor, + height: (rect.bottom - rect.top) as f64 / scale_factor, + }) } } pub fn physical_bounds(&self) -> Option { let mut rect = RECT::default(); unsafe { - if GetWindowRect(self.0, &mut rect).is_ok() { - Some(PhysicalBounds { - position: PhysicalPosition { - x: rect.left as f64, - y: rect.top as f64, - }, - size: PhysicalSize { - width: (rect.right - rect.left) as f64, - height: (rect.bottom - rect.top) as f64, - }, - }) - } else { - None + match GetProcessDpiAwareness(None) { + Ok(PROCESS_PER_MONITOR_DPI_AWARE) => {} + Err(e) => { + error!("Failed to get process DPI awareness: {e}"); + return None; + } + Ok(v) => { + error!("Unsupported DPI awareness {v:?}"); + return None; + } } + + DwmGetWindowAttribute( + self.0, + DWMWA_EXTENDED_FRAME_BOUNDS, + (&raw mut rect).cast(), + size_of::() as u32, + ) + .ok()?; + + Some(PhysicalBounds { + position: PhysicalPosition { + x: rect.left as f64, + y: rect.top as f64, + }, + size: PhysicalSize { + width: (rect.right - rect.left) as f64, + height: (rect.bottom - rect.top) as f64, + }, + }) } } @@ -1284,6 +1285,10 @@ impl WindowImpl { Some(self.physical_bounds()?.size()) } + pub fn physical_position(&self) -> Option { + Some(self.physical_bounds()?.position()) + } + pub fn display(&self) -> Option { let hwmonitor = unsafe { MonitorFromWindow(self.0, MONITOR_DEFAULTTONULL) }; if hwmonitor.is_invalid() { diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index a22662adfa..8e766fb7f2 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -1,10 +1,17 @@ use std::time::Duration; -use cap_displays::{Display, bounds::{LogicalBounds, LogicalSize, LogicalPosition}}; +use cap_displays::Window; use cap_recording::{RecordingBaseInputs, screen_capture::ScreenCaptureTarget}; #[tokio::main] pub async fn main() { + #[cfg(windows)] + { + use windows::Win32::UI::HiDpi::{PROCESS_PER_MONITOR_DPI_AWARE, SetProcessDpiAwareness}; + + unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE).unwrap() }; + } + tracing_subscriber::fmt::init(); let _ = std::fs::remove_dir_all("/tmp/bruh"); @@ -18,18 +25,18 @@ pub async fn main() { "test".to_string(), dir.path().into(), RecordingBaseInputs { - capture_target: ScreenCaptureTarget::Area { - screen: Display::primary().id(), - bounds: LogicalBounds::new( - LogicalPosition::new(0.0, 0.0), - LogicalSize::new(450.0, 400.0) - ) + capture_target: ScreenCaptureTarget::Window { + id: Window::list() + .into_iter() + .find(|w| w.name().unwrap_or_default().contains("Firefox")) + .unwrap() + .id(), }, capture_system_audio: true, mic_feed: &None, }, None, - false, + true, ) .await .unwrap(); diff --git a/crates/recording/src/capture_pipeline.rs b/crates/recording/src/capture_pipeline.rs index 3e1e8ea8ba..908f14303d 100644 --- a/crates/recording/src/capture_pipeline.rs +++ b/crates/recording/src/capture_pipeline.rs @@ -235,6 +235,8 @@ impl MakeCapturePipeline for screen_capture::AVFrameCapture { where Self: Sized, { + use cap_media_encoders::{MP4File, H264Encoder}; + let screen_config = source.0.info(); let mut screen_encoder = MP4File::init( "screen", @@ -280,6 +282,8 @@ impl MakeCapturePipeline for screen_capture::AVFrameCapture { where Self: Sized, { + use cap_media_encoders::{MP4File, H264Encoder, AACEncoder, AudioEncoder}; + let (audio_tx, audio_rx) = flume::bounded(64); let mut audio_mixer = AudioMixer::new(audio_tx); diff --git a/crates/recording/src/cursor.rs b/crates/recording/src/cursor.rs index 50425b5c37..a944042fd5 100644 --- a/crates/recording/src/cursor.rs +++ b/crates/recording/src/cursor.rs @@ -1,5 +1,5 @@ use cap_cursor_info::CursorShape; -use cap_displays::bounds::LogicalBounds; +use cap_displays::bounds::{LogicalBounds, PhysicalBounds}; use cap_project::{CursorClickEvent, CursorMoveEvent, XY}; use std::{collections::HashMap, path::PathBuf, time::SystemTime}; use tokio::sync::oneshot; @@ -36,7 +36,7 @@ impl CursorActor { #[tracing::instrument(name = "cursor", skip_all)] pub fn spawn_cursor_recorder( - crop_bounds: LogicalBounds, + crop_bounds: PhysicalBounds, display: cap_displays::Display, cursors_dir: PathBuf, prev_cursors: Cursors, @@ -131,24 +131,18 @@ pub fn spawn_cursor_recorder( let position = cap_cursor_capture::RawCursorPosition::get(); - dbg!(&position); - let position = (position != last_position).then(|| { last_position = position; - dbg!(&crop_bounds); - let cropped_norm_pos = position - .relative_to_display(display) - .normalize() + .relative_to_display(display)? + .normalize()? .with_crop(crop_bounds.position(), crop_bounds.size()); - dbg!(&cropped_norm_pos); - - (cropped_norm_pos.x(), cropped_norm_pos.y()) + Some((cropped_norm_pos.x(), cropped_norm_pos.y())) }); - if let Some((x, y)) = position { + if let Some((x, y)) = position.flatten() { let mouse_event = CursorMoveEvent { active_modifiers: vec![], cursor_id: cursor_id.clone(), diff --git a/crates/recording/src/sources/screen_capture.rs b/crates/recording/src/sources/screen_capture.rs index fa0ac09bd0..2375d8ac6a 100644 --- a/crates/recording/src/sources/screen_capture.rs +++ b/crates/recording/src/sources/screen_capture.rs @@ -1,6 +1,8 @@ use cap_displays::{ Display, DisplayId, Window, WindowId, - bounds::{LogicalBounds, PhysicalSize}, + bounds::{ + LogicalBounds, LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, + }, }; use cap_media_info::{AudioInfo, VideoInfo}; use ffmpeg::sys::AV_TIME_BASE_Q; @@ -8,7 +10,7 @@ use flume::Sender; use serde::{Deserialize, Serialize}; use specta::Type; use std::time::SystemTime; -use tracing::{error, warn}; +use tracing::{error, info, warn}; use crate::pipeline::{control::Control, task::PipelineSourceTask}; @@ -66,20 +68,62 @@ impl ScreenCaptureTarget { pub fn logical_bounds(&self) -> Option { match self { - Self::Screen { id } => Display::from_id(id).map(|d| d.logical_bounds()), - Self::Window { id } => Window::from_id(id).and_then(|w| w.logical_bounds()), + Self::Screen { id } => todo!(), // Display::from_id(id).map(|d| d.logical_bounds()), + Self::Window { id } => Some(LogicalBounds::new( + LogicalPosition::new(0.0, 0.0), + Window::from_id(id)?.raw_handle().logical_size()?, + )), Self::Area { bounds, .. } => Some(*bounds), } } + pub fn display_relative_physical_bounds(&self) -> Option { + match self { + Self::Screen { .. } => Some(PhysicalBounds::new( + PhysicalPosition::new(0.0, 0.0), + self.physical_size()?, + )), + Self::Window { id } => { + let window = Window::from_id(id)?; + let display_bounds = self.display()?.physical_bounds()?; + let window_bounds = window.physical_bounds()?; + + Some(PhysicalBounds::new( + PhysicalPosition::new( + window_bounds.position().x() - display_bounds.position().x(), + window_bounds.position().y() - display_bounds.position().y(), + ), + PhysicalSize::new(window_bounds.size().width(), window_bounds.size().height()), + )) + } + Self::Area { bounds, .. } => { + let display = self.display()?; + let display_bounds = display.physical_bounds()?; + let display_logical_size = display.logical_size()?; + + let scale = display_bounds.size().width() / display_logical_size.width(); + + Some(PhysicalBounds::new( + PhysicalPosition::new( + bounds.position().x() * scale, + bounds.position().y() * scale, + ), + PhysicalSize::new( + bounds.size().width() * scale, + bounds.size().height() * scale, + ), + )) + } + } + } + pub fn physical_size(&self) -> Option { match self { - Self::Screen { id } => Display::from_id(id).map(|d| d.physical_size()), + Self::Screen { id } => Display::from_id(id).and_then(|d| d.physical_size()), Self::Window { id } => Window::from_id(id).and_then(|w| w.physical_size()), Self::Area { bounds, .. } => { let display = self.display()?; - let scale = - display.physical_size().width() / display.logical_bounds().size().width(); + let scale = display.physical_size()?.width() / display.logical_size()?.width(); let size = bounds.size(); Some(PhysicalSize::new( @@ -97,30 +141,10 @@ impl ScreenCaptureTarget { Self::Area { screen, .. } => Display::from_id(screen).and_then(|d| d.name()), } } - - // pub fn get_title(&self) -> Option { - // let target = self.get_target(); - - // match target { - // None => None, - // Some(scap::Target::Window(window)) => Some(window.title.clone()), - // Some(scap::Target::Display(screen)) => { - // let names = crate::platform::display_names(); - - // Some( - // names - // .get(&screen.id) - // .cloned() - // .unwrap_or_else(|| screen.title.clone()), - // ) - // } - // } - // } } pub struct ScreenCaptureSource { config: Config, - display: Display, video_info: VideoInfo, tokio_handle: tokio::runtime::Handle, video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>, @@ -132,7 +156,6 @@ pub struct ScreenCaptureSource { impl std::fmt::Debug for ScreenCaptureSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ScreenCaptureSource") - .field("target", &self.config.target) // .field("bounds", &self.bounds) // .field("output_resolution", &self.output_resolution) .field("fps", &self.config.fps) @@ -160,7 +183,6 @@ impl Clone for ScreenCaptureSource Self { Self { config: self.config.clone(), - display: self.display.clone(), video_info: self.video_info, video_tx: self.video_tx.clone(), audio_tx: self.audio_tx.clone(), @@ -173,7 +195,8 @@ impl Clone for ScreenCaptureSource, fps: u32, show_cursor: bool, } @@ -203,17 +226,58 @@ impl ScreenCaptureSource { let fps = max_fps.min(display.refresh_rate() as u32); - let output_size = target - .physical_size() - .ok_or(ScreenCaptureInitError::PhysicalSize)?; + let crop_bounds = match target { + ScreenCaptureTarget::Screen { .. } => None, + ScreenCaptureTarget::Window { id } => { + let window = Window::from_id(&id).unwrap(); + + let raw_display_position = display.physical_position().unwrap(); + let raw_window_bounds = window.physical_bounds().unwrap(); + + Some(PhysicalBounds::new( + PhysicalPosition::new( + raw_window_bounds.position().x() - raw_display_position.x(), + raw_window_bounds.position().y() - raw_display_position.y(), + ), + raw_window_bounds.size(), + )) + } + ScreenCaptureTarget::Area { + bounds: relative_bounds, + .. + } => { + let raw_display_size = display.physical_size().unwrap(); + let logical_display_size = display.logical_size().unwrap(); + + Some(PhysicalBounds::new( + PhysicalPosition::new( + (relative_bounds.position().x() / logical_display_size.width()) + * raw_display_size.width(), + (relative_bounds.position().y() / logical_display_size.height()) + * raw_display_size.height(), + ), + PhysicalSize::new( + (relative_bounds.size().width() / logical_display_size.width()) + * raw_display_size.width(), + (relative_bounds.size().height() / logical_display_size.height()) + * raw_display_size.height(), + ), + )) + } + }; + + let output_size = crop_bounds + .map(|b| b.size()) + .or_else(|| display.physical_size()) + .unwrap(); Ok(Self { config: Config { - target: target.clone(), + display: display.id(), + crop_bounds, fps, show_cursor, }, - display, video_info: VideoInfo::from_raw_ffmpeg( TCaptureFormat::pixel_format(), output_size.width() as u32, @@ -240,15 +304,15 @@ impl ScreenCaptureSource { pub fn list_displays() -> Vec<(CaptureDisplay, Display)> { cap_displays::Display::list() .into_iter() - .map(|display| { - ( + .filter_map(|display| { + Some(( CaptureDisplay { id: display.id(), - name: display.name().unwrap(), + name: display.name()?, refresh_rate: display.raw_handle().refresh_rate() as u32, }, display, - ) + )) }) .collect() } @@ -257,6 +321,12 @@ pub fn list_windows() -> Vec<(CaptureWindow, Window)> { cap_displays::Window::list() .into_iter() .flat_map(|v| { + let name = v.name()?; + + if name.is_empty() { + return None; + } + #[cfg(target_os = "macos")] { if v.raw_handle().level() != Some(0) @@ -269,9 +339,9 @@ pub fn list_windows() -> Vec<(CaptureWindow, Window)> { Some(( CaptureWindow { id: v.id(), + name, owner_name: v.owner_name()?, - bounds: v.logical_bounds()?, - name: v.raw_handle().name()?, + bounds: v.display_relative_logical_bounds()?, refresh_rate: v.display()?.raw_handle().refresh_rate() as u32, }, v, @@ -295,7 +365,7 @@ mod windows { use ::windows::{ Graphics::Capture::GraphicsCaptureItem, Win32::Graphics::Direct3D11::D3D11_BOX, }; - use cap_displays::bounds::{PhysicalBounds, PhysicalPosition}; + use cpal::traits::{DeviceTrait, HostTrait}; use scap_ffmpeg::*; #[derive(Debug)] @@ -319,7 +389,7 @@ mod windows { let mut info = AudioInfo::from_stream_config(&supported_config); - info.sample_format = Sample::F32(ffmpeg::format::sample::Type::Packed); + info.sample_format = ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed); info } @@ -520,54 +590,26 @@ mod windows { let mut settings = scap_direct3d::Settings { is_border_required: Some(false), pixel_format: AVFrameCapture::PIXEL_FORMAT, - ..Default::default() - }; - - let capture_item = match &config.target { - ScreenCaptureTarget::Screen { id } => { - let display = Display::from_id(&id).unwrap(); - let display = display.raw_handle(); - - display.try_as_capture_item().unwrap() - } - ScreenCaptureTarget::Window { id } => { - let window = Window::from_id(&id).unwrap(); - let display = window.display().unwrap(); - let display = display.raw_handle(); - - display.try_as_capture_item().unwrap() - } - ScreenCaptureTarget::Area { screen, bounds } => { - let display = Display::from_id(&screen).unwrap(); - let display = display.raw_handle(); - - // this will always be >= 1 - let scale = - display.physical_size().width() / display.logical_size().width(); - - let size = PhysicalSize::new( - ((bounds.size().width() * scale) / 2.0).round() * 2.0, - ((bounds.size().height() * scale) / 2.0).round() * 2.0, - ); + crop: config.crop_bounds.map(|b| { + let position = b.position(); + let size = b.size(); - let position = PhysicalPosition::new( - bounds.position().x() * scale, - bounds.position().y() * scale, - ); - - settings.crop = Some(D3D11_BOX { + D3D11_BOX { left: position.x() as u32, top: position.y() as u32, right: (position.x() + size.width()) as u32, bottom: (position.y() + size.height()) as u32, front: 0, back: 1, - }); - - display.try_as_capture_item().unwrap() - } + } + }), + ..Default::default() }; + let display = Display::from_id(&config.display).unwrap(); + + let capture_item = display.raw_handle().try_as_capture_item().unwrap(); + settings.is_cursor_capture_enabled = Some(config.show_cursor); let _ = capturer @@ -624,6 +666,7 @@ mod windows { #[derive(Debug)] pub enum StartCapturingError { AlreadyCapturing, + Inner(scap_direct3d::StartCapturerError), } pub struct NewFrame { @@ -637,33 +680,35 @@ mod windows { async fn handle( &mut self, msg: StartCapturing, - ctx: &mut Context, + _: &mut Context, ) -> Self::Reply { - println!("bruh"); - if self.capture_handle.is_some() { return Err(StartCapturingError::AlreadyCapturing); } let capturer = scap_direct3d::Capturer::new(msg.target, msg.settings); - let capture_handle = capturer.start( - move |frame| { - let display_time = SystemTime::now(); - let ff_frame = frame.as_ffmpeg().unwrap(); + let capture_handle = capturer + .start( + move |frame| { + let display_time = SystemTime::now(); + let ff_frame = frame.as_ffmpeg().unwrap(); - let _ = msg - .frame_handler - .tell(NewFrame { - ff_frame, - display_time, - }) - .try_send(); + // dbg!(ff_frame.width(), ff_frame.height()); - Ok(()) - }, - || Ok(()), - ); + let _ = msg + .frame_handler + .tell(NewFrame { + ff_frame, + display_time, + }) + .try_send(); + + Ok(()) + }, + || Ok(()), + ) + .map_err(StartCapturingError::Inner)?; self.capture_handle = Some(capture_handle); @@ -709,7 +754,7 @@ mod windows { pub fn new( audio_tx: Sender<(ffmpeg::frame::Audio, f64)>, start_time: SystemTime, - ) -> Result { + ) -> Result { let mut i = 0; let capturer = scap_cpal::create_capturer( move |data, _: &cpal::InputCallbackInfo, config| { diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs index aae0eaf295..7fe6715a67 100644 --- a/crates/recording/src/studio_recording.rs +++ b/crates/recording/src/studio_recording.rs @@ -409,8 +409,6 @@ async fn run_actor_iteration( ) .await; - println!("Bruh"); - match shutdown(pipeline, &mut actor, segment_start_time).await { Ok((cursors, _)) => stop_recording(actor, cursors).await, Err(e) => Err(e), @@ -688,7 +686,7 @@ async fn create_segment_pipeline( .display() .ok_or(CreateSegmentPipelineError::NoDisplay)?; let crop_bounds = capture_target - .logical_bounds() + .display_relative_physical_bounds() .ok_or(CreateSegmentPipelineError::NoBounds)?; let (screen_source, screen_rx) = create_screen_capture( diff --git a/crates/scap-direct3d/Cargo.toml b/crates/scap-direct3d/Cargo.toml index 7d90a9ea52..2b88b9e907 100644 --- a/crates/scap-direct3d/Cargo.toml +++ b/crates/scap-direct3d/Cargo.toml @@ -26,3 +26,6 @@ scap-ffmpeg = { path = "../scap-ffmpeg" } [lints] workspace = true + +[dependencies] +thiserror.workspace = true diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index 76e0d82695..94686c7458 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -7,14 +7,18 @@ use std::{ sync::{ Arc, atomic::{AtomicBool, Ordering}, + mpsc::RecvError, }, time::Duration, }; use windows::{ - Foundation::{TypedEventHandler, Metadata::ApiInformation}, + Foundation::{Metadata::ApiInformation, TypedEventHandler}, Graphics::{ - Capture::{Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem, GraphicsCaptureSession}, + Capture::{ + Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem, + GraphicsCaptureSession, + }, DirectX::{Direct3D11::IDirect3DDevice, DirectXPixelFormat}, }, Win32::{ @@ -48,7 +52,7 @@ use windows::{ DispatchMessageW, GetMessageW, MSG, PostThreadMessageW, TranslateMessage, WM_QUIT, }, }, - core::{IInspectable, Interface, HSTRING}, + core::{HSTRING, IInspectable, Interface}, }; #[derive(Default, Clone, Copy, Debug)] @@ -73,10 +77,10 @@ impl PixelFormat { } pub fn is_supported() -> windows::core::Result { - Ok(ApiInformation::IsApiContractPresentByMajor( - &HSTRING::from("Windows.Foundation.UniversalApiContract"), - 8 - )? && GraphicsCaptureSession::IsSupported()?) + Ok(ApiInformation::IsApiContractPresentByMajor( + &HSTRING::from("Windows.Foundation.UniversalApiContract"), + 8, + )? && GraphicsCaptureSession::IsSupported()?) } #[derive(Default, Debug)] @@ -89,26 +93,44 @@ pub struct Settings { } impl Settings { - pub fn can_is_border_required(&self) -> windows::core::Result { - ApiInformation::IsPropertyPresent( - &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), - &HSTRING::from("IsCursorCaptureEnabled"), - ) - } - - pub fn can_is_cursor_capture_enabled(&self) -> windows::core::Result { - ApiInformation::IsPropertyPresent( - &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), - &HSTRING::from("IsBorderRequired"), - ) - } - - pub fn can_min_update_interval(&self) -> windows::core::Result { - ApiInformation::IsPropertyPresent( - &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), - &HSTRING::from("MinUpdateInterval"), - ) - } + pub fn can_is_border_required() -> windows::core::Result { + ApiInformation::IsPropertyPresent( + &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), + &HSTRING::from("IsCursorCaptureEnabled"), + ) + } + + pub fn can_is_cursor_capture_enabled() -> windows::core::Result { + ApiInformation::IsPropertyPresent( + &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), + &HSTRING::from("IsBorderRequired"), + ) + } + + pub fn can_min_update_interval() -> windows::core::Result { + ApiInformation::IsPropertyPresent( + &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), + &HSTRING::from("MinUpdateInterval"), + ) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum StartCapturerError { + #[error("NotSupported")] + NotSupported, + #[error("BorderNotSupported")] + BorderNotSupported, + #[error("CursorNotSupported")] + CursorNotSupported, + #[error("UpdateIntervalNotSupported")] + UpdateIntervalNotSupported, + #[error("CreateRunner: {0}")] + CreateRunner(#[from] CreateRunnerError), + #[error("RecvTimeout")] + RecvTimeout(#[from] RecvError), + #[error("Other: {0}")] + Other(#[from] windows::core::Error), } pub struct Capturer { @@ -125,25 +147,59 @@ impl Capturer { self, callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static, - ) -> CaptureHandle { + ) -> Result { + if !is_supported()? { + return Err(StartCapturerError::NotSupported); + } + + if self.settings.is_border_required.is_some() && !Settings::can_is_border_required()? { + return Err(StartCapturerError::BorderNotSupported); + } + + if self.settings.is_cursor_capture_enabled.is_some() + && !Settings::can_is_cursor_capture_enabled()? + { + return Err(StartCapturerError::CursorNotSupported); + } + + if self.settings.min_update_interval.is_some() && !Settings::can_min_update_interval()? { + return Err(StartCapturerError::UpdateIntervalNotSupported); + } + let stop_flag = Arc::new(AtomicBool::new(false)); + let (started_tx, started_rx) = std::sync::mpsc::channel(); let thread_handle = std::thread::spawn({ let stop_flag = stop_flag.clone(); move || { - run( + let runner = Runner::start( self.item, self.settings, callback, closed_callback, stop_flag, ); + + let runner = match runner { + Ok(runner) => { + started_tx.send(Ok(())); + runner + } + Err(e) => { + started_tx.send(Err(e)); + return; + } + }; + + runner.run(); } }); - CaptureHandle { + started_rx.recv()??; + + Ok(CaptureHandle { stop_flag, thread_handle, - } + }) } } @@ -309,190 +365,221 @@ impl<'a> FrameBuffer<'a> { } } -fn run( - item: GraphicsCaptureItem, - settings: Settings, - mut callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, - mut closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static, - stop_flag: Arc, -) -> Result<(), &'static str> { - if let Err(e) = unsafe { RoInitialize(RO_INIT_MULTITHREADED) } - && e.code() != S_FALSE - { - return Err("Failed to initialise WinRT"); - } +#[derive(Debug, thiserror::Error)] +pub enum CreateRunnerError { + #[error("Failed to initialize WinRT")] + FailedToInitializeWinRT, + #[error("DispatchQueue: {0}")] + DispatchQueue(windows::core::Error), + #[error("D3DDevice: {0}")] + D3DDevice(windows::core::Error), + #[error("Direct3DDevice: {0}")] + Direct3DDevice(windows::core::Error), + #[error("FramePool: {0}")] + FramePool(windows::core::Error), + #[error("CaptureSession: {0}")] + CaptureSession(windows::core::Error), + #[error("CropTexture: {0}")] + CropTexture(windows::core::Error), + #[error("RegisterFrameArrived: {0}")] + RegisterFrameArrived(windows::core::Error), + #[error("RegisterClosed: {0}")] + RegisterClosed(windows::core::Error), + #[error("StartCapture: {0}")] + StartCapture(windows::core::Error), + #[error("Other: {0}")] + Other(#[from] windows::core::Error), +} - let queue_options = DispatcherQueueOptions { - dwSize: std::mem::size_of::() as u32, - threadType: DQTYPE_THREAD_CURRENT, - apartmentType: DQTAT_COM_NONE, - }; - - let _controller = unsafe { CreateDispatcherQueueController(queue_options) } - .map_err(|_| "Failed to create dispatcher queue controller")?; - - let mut d3d_device = None; - let mut d3d_context = None; - - unsafe { - D3D11CreateDevice( - None, - D3D_DRIVER_TYPE_HARDWARE, - HMODULE::default(), - Default::default(), - None, - D3D11_SDK_VERSION, - Some(&mut d3d_device), - None, - Some(&mut d3d_context), +struct Runner { + _session: GraphicsCaptureSession, + _frame_pool: Direct3D11CaptureFramePool, +} + +impl Runner { + fn start( + item: GraphicsCaptureItem, + settings: Settings, + mut callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, + mut closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static, + stop_flag: Arc, + ) -> Result { + if let Err(e) = unsafe { RoInitialize(RO_INIT_MULTITHREADED) } + && e.code() != S_FALSE + { + return Err(CreateRunnerError::FailedToInitializeWinRT); + } + + let queue_options = DispatcherQueueOptions { + dwSize: std::mem::size_of::() as u32, + threadType: DQTYPE_THREAD_CURRENT, + apartmentType: DQTAT_COM_NONE, + }; + + let _controller = unsafe { CreateDispatcherQueueController(queue_options) } + .map_err(CreateRunnerError::DispatchQueue)?; + + let mut d3d_device = None; + let mut d3d_context = None; + + unsafe { + D3D11CreateDevice( + None, + D3D_DRIVER_TYPE_HARDWARE, + HMODULE::default(), + Default::default(), + None, + D3D11_SDK_VERSION, + Some(&mut d3d_device), + None, + Some(&mut d3d_context), + ) + } + .map_err(CreateRunnerError::D3DDevice)?; + + let d3d_device = d3d_device.unwrap(); + let d3d_context = d3d_context.unwrap(); + + let direct3d_device = (|| { + let dxgi_device = d3d_device.cast::()?; + let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }?; + inspectable.cast::() + })() + .map_err(CreateRunnerError::Direct3DDevice)?; + + let frame_pool = Direct3D11CaptureFramePool::Create( + &direct3d_device, + PixelFormat::R8G8B8A8Unorm.as_directx(), + 1, + item.Size()?, ) - } - .map_err(|_| "Failed to create d3d11 device")?; - - let d3d_device = d3d_device.unwrap(); - let d3d_context = d3d_context.unwrap(); - - let direct3d_device = (|| { - let dxgi_device = d3d_device.cast::()?; - let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }?; - inspectable.cast::() - })() - .map_err(|_| "Failed to create direct3d device")?; - - let frame_pool = Direct3D11CaptureFramePool::Create( - &direct3d_device, - PixelFormat::R8G8B8A8Unorm.as_directx(), - 1, - item.Size().map_err(|_| "Item size")?, - ) - .map_err(|_| "Failed to create frame pool")?; - - let session = frame_pool - .CreateCaptureSession(&item) - .map_err(|_| "Failed to create capture session")?; - - if let Some(border_required) = settings.is_border_required { - session - .SetIsBorderRequired(border_required) - .map_err(|_| "Failed to set border required")?; - } + .map_err(CreateRunnerError::FramePool)?; - if let Some(cursor_capture_enabled) = settings.is_cursor_capture_enabled { - session - .SetIsCursorCaptureEnabled(cursor_capture_enabled) - .map_err(|_| "Failed to set cursor capture enabled")?; - } + let session = frame_pool + .CreateCaptureSession(&item) + .map_err(CreateRunnerError::CaptureSession)?; - if let Some(min_update_interval) = settings.min_update_interval { - session - .SetMinUpdateInterval(min_update_interval.into()) - .map_err(|_| "Failed to set min update interval")?; - } + if let Some(border_required) = settings.is_border_required { + session.SetIsBorderRequired(border_required)?; + } - let crop_data = settings - .crop - .map(|crop| { - let desc = D3D11_TEXTURE2D_DESC { - Width: (crop.right - crop.left), - Height: (crop.bottom - crop.top), - MipLevels: 1, - ArraySize: 1, - Format: settings.pixel_format.as_dxgi(), - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, - CPUAccessFlags: 0, - MiscFlags: 0, - }; - - let mut texture = None; - unsafe { d3d_device.CreateTexture2D(&desc, None, Some(&mut texture)) }?; - - Ok::<_, windows::core::Error>(( - texture.ok_or(windows::core::Error::from_hresult(S_FALSE))?, - crop, - )) - }) - .transpose() - .map_err(|_| "Failed to create crop texture")?; - - frame_pool - .FrameArrived( - &TypedEventHandler::::new( - move |frame_pool, _| { - if stop_flag.load(Ordering::Relaxed) { - return Ok(()); - } + if let Some(cursor_capture_enabled) = settings.is_cursor_capture_enabled { + session.SetIsCursorCaptureEnabled(cursor_capture_enabled)?; + } - let frame = frame_pool - .as_ref() - .expect("FrameArrived parameter was None") - .TryGetNextFrame()?; - - let size = frame.ContentSize()?; - - let surface = frame.Surface()?; - let dxgi_interface = surface.cast::()?; - let texture = unsafe { dxgi_interface.GetInterface::() }?; - - let frame = if let Some((cropped_texture, crop)) = crop_data.clone() { - unsafe { - d3d_context.CopySubresourceRegion( - &cropped_texture, - 0, - 0, - 0, - 0, - &texture, - 0, - Some(&crop), - ); - } + if let Some(min_update_interval) = settings.min_update_interval { + session.SetMinUpdateInterval(min_update_interval.into())?; + } - Frame { - width: crop.right - crop.left, - height: crop.bottom - crop.top, - pixel_format: settings.pixel_format, - inner: frame, - texture: cropped_texture, - d3d_context: &d3d_context, - d3d_device: &d3d_device, + let crop_data = settings + .crop + .map(|crop| { + let desc = D3D11_TEXTURE2D_DESC { + Width: (crop.right - crop.left), + Height: (crop.bottom - crop.top), + MipLevels: 1, + ArraySize: 1, + Format: settings.pixel_format.as_dxgi(), + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + + let mut texture = None; + unsafe { d3d_device.CreateTexture2D(&desc, None, Some(&mut texture)) } + .map_err(CreateRunnerError::CropTexture)?; + + Ok::<_, CreateRunnerError>((texture.unwrap(), crop)) + }) + .transpose()?; + + frame_pool + .FrameArrived( + &TypedEventHandler::::new( + move |frame_pool, _| { + if stop_flag.load(Ordering::Relaxed) { + return Ok(()); } - } else { - Frame { - width: size.Width as u32, - height: size.Height as u32, - pixel_format: settings.pixel_format, - inner: frame, - texture, - d3d_context: &d3d_context, - d3d_device: &d3d_device, - } - }; - (callback)(frame) - }, - ), - ) - .map_err(|_| "Failed to register frame arrived handler")?; + let frame = frame_pool + .as_ref() + .expect("FrameArrived parameter was None") + .TryGetNextFrame()?; + + let size = frame.ContentSize()?; + + let surface = frame.Surface()?; + let dxgi_interface = surface.cast::()?; + let texture = unsafe { dxgi_interface.GetInterface::() }?; + + let frame = if let Some((cropped_texture, crop)) = crop_data.clone() { + unsafe { + d3d_context.CopySubresourceRegion( + &cropped_texture, + 0, + 0, + 0, + 0, + &texture, + 0, + Some(&crop), + ); + } + + Frame { + width: crop.right - crop.left, + height: crop.bottom - crop.top, + pixel_format: settings.pixel_format, + inner: frame, + texture: cropped_texture, + d3d_context: &d3d_context, + d3d_device: &d3d_device, + } + } else { + Frame { + width: size.Width as u32, + height: size.Height as u32, + pixel_format: settings.pixel_format, + inner: frame, + texture, + d3d_context: &d3d_context, + d3d_device: &d3d_device, + } + }; + + (callback)(frame) + }, + ), + ) + .map_err(CreateRunnerError::RegisterFrameArrived)?; - item.Closed( - &TypedEventHandler::::new(move |_, _| closed_callback()), - ) - .map_err(|_| "Failed to register closed handler")?; + item.Closed( + &TypedEventHandler::::new(move |_, _| { + closed_callback() + }), + ) + .map_err(CreateRunnerError::RegisterClosed)?; - session - .StartCapture() - .map_err(|_| "Failed to start capture")?; + session + .StartCapture() + .map_err(CreateRunnerError::StartCapture)?; - let mut message = MSG::default(); - while unsafe { GetMessageW(&mut message, None, 0, 0) }.as_bool() { - let _ = unsafe { TranslateMessage(&message) }; - unsafe { DispatchMessageW(&message) }; + Ok(Self { + _session: session, + _frame_pool: frame_pool, + }) } - Ok(()) + fn run(self) { + let mut message = MSG::default(); + while unsafe { GetMessageW(&mut message, None, 0, 0) }.as_bool() { + let _ = unsafe { TranslateMessage(&message) }; + unsafe { DispatchMessageW(&message) }; + } + } } diff --git a/crates/scap-ffmpeg/examples/cli.rs b/crates/scap-ffmpeg/examples/cli.rs index 7a2372695c..878abf3421 100644 --- a/crates/scap-ffmpeg/examples/cli.rs +++ b/crates/scap-ffmpeg/examples/cli.rs @@ -19,15 +19,17 @@ pub async fn main() { }, ); - let capture_handle = capturer.start(|frame| { - use scap_ffmpeg::AsFFmpeg; + let capture_handle = capturer + .start(|frame| { + use scap_ffmpeg::AsFFmpeg; - let ff_frame = frame.as_ffmpeg()?; + let ff_frame = frame.as_ffmpeg()?; - dbg!(ff_frame.width(), ff_frame.height(), ff_frame.format()); + dbg!(ff_frame.width(), ff_frame.height(), ff_frame.format()); - Ok(()) - }); + Ok(()) + }) + .unwrap(); std::thread::sleep(Duration::from_secs(3)); diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index dadf6414ae..7ab73f4c84 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -4,89 +4,68 @@ // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import // biome-ignore lint: disable -export {} +export {}; declare global { - const IconCapArrows: typeof import('~icons/cap/arrows.jsx')['default'] - const IconCapAudioOn: typeof import('~icons/cap/audio-on.jsx')['default'] - const IconCapBgBlur: typeof import('~icons/cap/bg-blur.jsx')['default'] - const IconCapCamera: typeof import('~icons/cap/camera.jsx')['default'] - const IconCapCaptions: typeof import('~icons/cap/captions.jsx')['default'] - const IconCapChevronDown: typeof import('~icons/cap/chevron-down.jsx')['default'] - const IconCapCircle: typeof import('~icons/cap/circle.jsx')['default'] - const IconCapCircleCheck: typeof import('~icons/cap/circle-check.jsx')['default'] - const IconCapCirclePlus: typeof import('~icons/cap/circle-plus.jsx')['default'] - const IconCapCircleX: typeof import('~icons/cap/circle-x.jsx')['default'] - const IconCapCopy: typeof import('~icons/cap/copy.jsx')['default'] - const IconCapCorners: typeof import('~icons/cap/corners.jsx')['default'] - const IconCapCrop: typeof import('~icons/cap/crop.jsx')['default'] - const IconCapCursor: typeof import('~icons/cap/cursor.jsx')['default'] - const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"] - const IconCapEnlarge: typeof import('~icons/cap/enlarge.jsx')['default'] - const IconCapFile: typeof import('~icons/cap/file.jsx')['default'] - const IconCapFilmCut: typeof import('~icons/cap/film-cut.jsx')['default'] - const IconCapGauge: typeof import('~icons/cap/gauge.jsx')['default'] - const IconCapHotkeys: typeof import('~icons/cap/hotkeys.jsx')['default'] - const IconCapImage: typeof import('~icons/cap/image.jsx')['default'] - const IconCapInfo: typeof import('~icons/cap/info.jsx')['default'] - const IconCapInstant: typeof import('~icons/cap/instant.jsx')['default'] - const IconCapLayout: typeof import('~icons/cap/layout.jsx')['default'] - const IconCapLink: typeof import('~icons/cap/link.jsx')['default'] - const IconCapLogo: typeof import('~icons/cap/logo.jsx')['default'] - const IconCapLogoFull: typeof import('~icons/cap/logo-full.jsx')['default'] - const IconCapLogoFullDark: typeof import('~icons/cap/logo-full-dark.jsx')['default'] - const IconCapMessageBubble: typeof import('~icons/cap/message-bubble.jsx')['default'] - const IconCapMicrophone: typeof import('~icons/cap/microphone.jsx')['default'] - const IconCapMoreVertical: typeof import('~icons/cap/more-vertical.jsx')['default'] - const IconCapNext: typeof import('~icons/cap/next.jsx')['default'] - const IconCapPadding: typeof import('~icons/cap/padding.jsx')['default'] - const IconCapPause: typeof import('~icons/cap/pause.jsx')['default'] - const IconCapPauseCircle: typeof import('~icons/cap/pause-circle.jsx')['default'] - const IconCapPlay: typeof import('~icons/cap/play.jsx')['default'] - const IconCapPlayCircle: typeof import('~icons/cap/play-circle.jsx')['default'] - const IconCapPresets: typeof import('~icons/cap/presets.jsx')['default'] - const IconCapPrev: typeof import('~icons/cap/prev.jsx')['default'] - const IconCapRedo: typeof import('~icons/cap/redo.jsx')['default'] - const IconCapRestart: typeof import('~icons/cap/restart.jsx')['default'] - const IconCapScissors: typeof import('~icons/cap/scissors.jsx')['default'] - const IconCapSettings: typeof import('~icons/cap/settings.jsx')['default'] - const IconCapShadow: typeof import('~icons/cap/shadow.jsx')['default'] - const IconCapSquare: typeof import('~icons/cap/square.jsx')['default'] - const IconCapStopCircle: typeof import('~icons/cap/stop-circle.jsx')['default'] - const IconCapTrash: typeof import('~icons/cap/trash.jsx')['default'] - const IconCapUndo: typeof import('~icons/cap/undo.jsx')['default'] - const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"] - const IconCapZoomIn: typeof import('~icons/cap/zoom-in.jsx')['default'] - const IconCapZoomOut: typeof import('~icons/cap/zoom-out.jsx')['default'] - const IconFa6SolidDisplay: typeof import("~icons/fa6-solid/display.jsx")["default"] - const IconHugeiconsEaseCurveControlPoints: typeof import('~icons/hugeicons/ease-curve-control-points.jsx')['default'] - const IconIcBaselineMonitor: typeof import("~icons/ic/baseline-monitor.jsx")["default"] - const IconIcRoundSearch: typeof import("~icons/ic/round-search.jsx")["default"] - const IconLucideAppWindowMac: typeof import("~icons/lucide/app-window-mac.jsx")["default"] - const IconLucideBell: typeof import('~icons/lucide/bell.jsx')['default'] - const IconLucideBug: typeof import('~icons/lucide/bug.jsx')['default'] - const IconLucideCheck: typeof import('~icons/lucide/check.jsx')['default'] - const IconLucideClock: typeof import('~icons/lucide/clock.jsx')['default'] - const IconLucideDatabase: typeof import('~icons/lucide/database.jsx')['default'] - const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"] - const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"] - const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] - const IconLucideGift: typeof import('~icons/lucide/gift.jsx')['default'] - const IconLucideHardDrive: typeof import('~icons/lucide/hard-drive.jsx')['default'] - const IconLucideLoaderCircle: typeof import('~icons/lucide/loader-circle.jsx')['default'] - const IconLucideMessageSquarePlus: typeof import('~icons/lucide/message-square-plus.jsx')['default'] - const IconLucideMicOff: typeof import('~icons/lucide/mic-off.jsx')['default'] - const IconLucideMonitor: typeof import('~icons/lucide/monitor.jsx')['default'] - const IconLucideRectangleHorizontal: typeof import('~icons/lucide/rectangle-horizontal.jsx')['default'] - const IconLucideRotateCcw: typeof import('~icons/lucide/rotate-ccw.jsx')['default'] - const IconLucideSearch: typeof import('~icons/lucide/search.jsx')['default'] - const IconLucideSquarePlay: typeof import('~icons/lucide/square-play.jsx')['default'] - const IconLucideUnplug: typeof import('~icons/lucide/unplug.jsx')['default'] - const IconLucideVolume2: typeof import('~icons/lucide/volume2.jsx')['default'] - const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"] - const IconLucideX: typeof import("~icons/lucide/x.jsx")["default"] - const IconMaterialSymbolsLightScreenshotFrame2: typeof import("~icons/material-symbols-light/screenshot-frame2.jsx")["default"] - const IconMaterialSymbolsLightScreenshotFrame2MaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols-light/screenshot-frame2-material-symbols-screenshot-frame2-rounded.jsx")["default"] - const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"] - const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"] - const IconPhMonitorBold: typeof import('~icons/ph/monitor-bold.jsx')['default'] + const IconCapAudioOn: typeof import("~icons/cap/audio-on.jsx")["default"]; + const IconCapBgBlur: typeof import("~icons/cap/bg-blur.jsx")["default"]; + const IconCapCamera: typeof import("~icons/cap/camera.jsx")["default"]; + const IconCapCaptions: typeof import("~icons/cap/captions.jsx")["default"]; + const IconCapChevronDown: typeof import("~icons/cap/chevron-down.jsx")["default"]; + const IconCapCircleCheck: typeof import("~icons/cap/circle-check.jsx")["default"]; + const IconCapCirclePlus: typeof import("~icons/cap/circle-plus.jsx")["default"]; + const IconCapCircleX: typeof import("~icons/cap/circle-x.jsx")["default"]; + const IconCapCopy: typeof import("~icons/cap/copy.jsx")["default"]; + const IconCapCorners: typeof import("~icons/cap/corners.jsx")["default"]; + const IconCapCrop: typeof import("~icons/cap/crop.jsx")["default"]; + const IconCapCursor: typeof import("~icons/cap/cursor.jsx")["default"]; + const IconCapEnlarge: typeof import("~icons/cap/enlarge.jsx")["default"]; + const IconCapFile: typeof import("~icons/cap/file.jsx")["default"]; + const IconCapFilmCut: typeof import("~icons/cap/film-cut.jsx")["default"]; + const IconCapGauge: typeof import("~icons/cap/gauge.jsx")["default"]; + const IconCapHotkeys: typeof import("~icons/cap/hotkeys.jsx")["default"]; + const IconCapImage: typeof import("~icons/cap/image.jsx")["default"]; + const IconCapInfo: typeof import("~icons/cap/info.jsx")["default"]; + const IconCapInstant: typeof import("~icons/cap/instant.jsx")["default"]; + const IconCapLayout: typeof import("~icons/cap/layout.jsx")["default"]; + const IconCapLink: typeof import("~icons/cap/link.jsx")["default"]; + const IconCapLogo: typeof import("~icons/cap/logo.jsx")["default"]; + const IconCapLogoFull: typeof import("~icons/cap/logo-full.jsx")["default"]; + const IconCapLogoFullDark: typeof import("~icons/cap/logo-full-dark.jsx")["default"]; + const IconCapMessageBubble: typeof import("~icons/cap/message-bubble.jsx")["default"]; + const IconCapMicrophone: typeof import("~icons/cap/microphone.jsx")["default"]; + const IconCapMoreVertical: typeof import("~icons/cap/more-vertical.jsx")["default"]; + const IconCapNext: typeof import("~icons/cap/next.jsx")["default"]; + const IconCapPadding: typeof import("~icons/cap/padding.jsx")["default"]; + const IconCapPause: typeof import("~icons/cap/pause.jsx")["default"]; + const IconCapPauseCircle: typeof import("~icons/cap/pause-circle.jsx")["default"]; + const IconCapPlay: typeof import("~icons/cap/play.jsx")["default"]; + const IconCapPlayCircle: typeof import("~icons/cap/play-circle.jsx")["default"]; + const IconCapPresets: typeof import("~icons/cap/presets.jsx")["default"]; + const IconCapPrev: typeof import("~icons/cap/prev.jsx")["default"]; + const IconCapRedo: typeof import("~icons/cap/redo.jsx")["default"]; + const IconCapRestart: typeof import("~icons/cap/restart.jsx")["default"]; + const IconCapScissors: typeof import("~icons/cap/scissors.jsx")["default"]; + const IconCapSettings: typeof import("~icons/cap/settings.jsx")["default"]; + const IconCapShadow: typeof import("~icons/cap/shadow.jsx")["default"]; + const IconCapStopCircle: typeof import("~icons/cap/stop-circle.jsx")["default"]; + const IconCapTrash: typeof import("~icons/cap/trash.jsx")["default"]; + const IconCapUndo: typeof import("~icons/cap/undo.jsx")["default"]; + const IconCapZoomIn: typeof import("~icons/cap/zoom-in.jsx")["default"]; + const IconCapZoomOut: typeof import("~icons/cap/zoom-out.jsx")["default"]; + const IconHugeiconsEaseCurveControlPoints: typeof import("~icons/hugeicons/ease-curve-control-points.jsx")["default"]; + const IconLucideBell: typeof import("~icons/lucide/bell.jsx")["default"]; + const IconLucideBug: typeof import("~icons/lucide/bug.jsx")["default"]; + const IconLucideCheck: typeof import("~icons/lucide/check.jsx")["default"]; + const IconLucideClock: typeof import("~icons/lucide/clock.jsx")["default"]; + const IconLucideFolder: typeof import("~icons/lucide/folder.jsx")["default"]; + const IconLucideGift: typeof import("~icons/lucide/gift.jsx")["default"]; + const IconLucideHardDrive: typeof import("~icons/lucide/hard-drive.jsx")["default"]; + const IconLucideLoaderCircle: typeof import("~icons/lucide/loader-circle.jsx")["default"]; + const IconLucideMicOff: typeof import("~icons/lucide/mic-off.jsx")["default"]; + const IconLucideMonitor: typeof import("~icons/lucide/monitor.jsx")["default"]; + const IconLucideRotateCcw: typeof import("~icons/lucide/rotate-ccw.jsx")["default"]; + const IconLucideSearch: typeof import("~icons/lucide/search.jsx")["default"]; + const IconLucideSquarePlay: typeof import("~icons/lucide/square-play.jsx")["default"]; + const IconLucideVolume2: typeof import("~icons/lucide/volume2.jsx")["default"]; + const IconPhMonitorBold: typeof import("~icons/ph/monitor-bold.jsx")["default"]; } From a1a59654f09bc2b1fe5ed4b047492be9514595c2 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 21 Aug 2025 18:41:47 +0800 Subject: [PATCH 25/47] bounds work properly on macos --- Cargo.lock | 34 +- apps/desktop/src-tauri/src/windows.rs | 68 +- apps/desktop/src/utils/tauri.ts | 1228 ++++++----------- crates/cursor-capture/src/main.rs | 12 +- crates/cursor-capture/src/position.rs | 155 ++- crates/displays/src/lib.rs | 81 +- crates/displays/src/main.rs | 15 +- crates/displays/src/platform/macos.rs | 77 +- crates/recording/Cargo.toml | 2 +- crates/recording/examples/recording-cli.rs | 8 +- crates/recording/src/capture_pipeline.rs | 6 +- crates/recording/src/cursor.rs | 5 +- .../recording/src/sources/screen_capture.rs | 238 +++- crates/recording/src/studio_recording.rs | 2 +- crates/scap-screencapturekit/src/capture.rs | 1 - 15 files changed, 884 insertions(+), 1048 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8a47d465f..2c5e98db86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4637,15 +4637,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "matches" version = "0.1.10" @@ -6607,17 +6598,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -6628,15 +6610,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -9243,14 +9219,10 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ - "matchers", "nu-ansi-term", - "once_cell", - "regex", "sharded-slab", "smallvec", "thread_local", - "tracing", "tracing-core", "tracing-log", ] diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 60801add61..0c91f7f65c 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -256,7 +256,12 @@ impl ShowCapWindow { }; let size = display.physical_size().unwrap(); - let position = display.physical_position().unwrap(); + + #[cfg(target_os = "macos")] + let position = display.raw_handle().logical_position(); + + #[cfg(windows)] + let position = display.raw_handle().physical_position().unwrap(); let mut window_builder = self .window_builder( @@ -398,7 +403,12 @@ impl ShowCapWindow { return Err(tauri::Error::WindowNotFound); }; - let pos = display.physical_position().unwrap(); + #[cfg(target_os = "macos")] + let position = display.raw_handle().logical_position(); + + #[cfg(windows)] + let position = display.raw_handle().physical_position().unwrap(); + let bounds = display.physical_size().unwrap(); let mut window_builder = self @@ -412,7 +422,7 @@ impl ShowCapWindow { .content_protected(true) .skip_taskbar(true) .inner_size(bounds.width(), bounds.height()) - .position(pos.x(), pos.y()) + .position(position.x(), position.y()) .transparent(true); let window = window_builder.build()?; @@ -443,7 +453,14 @@ impl ShowCapWindow { return Err(tauri::Error::WindowNotFound); }; - if let Some(bounds) = display.physical_bounds() { + if let Some(bounds) = display.raw_handle().logical_bounds() { + window_builder = window_builder + .inner_size(bounds.size().width(), bounds.size().height()) + .position(bounds.position().x(), bounds.position().y()); + } + + #[cfg(windows)] + if let Some(bounds) = display.raw_handle().physical_bounds() { window_builder = window_builder .inner_size(bounds.size().width(), bounds.size().height()) .position(bounds.position().x(), bounds.position().y()); @@ -716,26 +733,31 @@ trait MonitorExt { impl MonitorExt for Display { fn intersects(&self, position: PhysicalPosition, size: PhysicalSize) -> bool { - let Some(bounds) = self.physical_bounds() else { - return false; - }; + return false; - let left = bounds.position().x() as i32; - let right = left + bounds.size().width() as i32; - let top = bounds.position().y() as i32; - let bottom = top + bounds.size().height() as i32; - - [ - (position.x, position.y), - (position.x + size.width as i32, position.y), - (position.x, position.y + size.height as i32), - ( - position.x + size.width as i32, - position.y + size.height as i32, - ), - ] - .into_iter() - .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) + #[cfg(windows)] + { + let Some(bounds) = self.raw_handle().physical_bounds() else { + return false; + }; + + let left = bounds.position().x() as i32; + let right = left + bounds.size().width() as i32; + let top = bounds.position().y() as i32; + let bottom = top + bounds.size().height() as i32; + + [ + (position.x, position.y), + (position.x + size.width as i32, position.y), + (position.x, position.y + size.height as i32), + ( + position.x + size.width as i32, + position.y + size.height as i32, + ), + ] + .into_iter() + .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) + } } } diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index b4645d7441..94c2cc0030 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1,810 +1,463 @@ + // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. /** user-defined commands **/ + export const commands = { - async setMicInput(label: string | null): Promise { - return await TAURI_INVOKE("set_mic_input", { label }); - }, - async setCameraInput(id: DeviceOrModelID | null): Promise { - return await TAURI_INVOKE("set_camera_input", { id }); - }, - async startRecording(inputs: StartRecordingInputs): Promise { - return await TAURI_INVOKE("start_recording", { inputs }); - }, - async stopRecording(): Promise { - return await TAURI_INVOKE("stop_recording"); - }, - async pauseRecording(): Promise { - return await TAURI_INVOKE("pause_recording"); - }, - async resumeRecording(): Promise { - return await TAURI_INVOKE("resume_recording"); - }, - async restartRecording(): Promise { - return await TAURI_INVOKE("restart_recording"); - }, - async deleteRecording(): Promise { - return await TAURI_INVOKE("delete_recording"); - }, - async listCameras(): Promise { - return await TAURI_INVOKE("list_cameras"); - }, - async listCaptureWindows(): Promise { - return await TAURI_INVOKE("list_capture_windows"); - }, - async listCaptureDisplays(): Promise { - return await TAURI_INVOKE("list_capture_displays"); - }, - async takeScreenshot(): Promise { - return await TAURI_INVOKE("take_screenshot"); - }, - async listAudioDevices(): Promise { - return await TAURI_INVOKE("list_audio_devices"); - }, - async closeRecordingsOverlayWindow(): Promise { - await TAURI_INVOKE("close_recordings_overlay_window"); - }, - async setFakeWindowBounds( - name: string, - bounds: LogicalBounds, - ): Promise { - return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); - }, - async removeFakeWindow(name: string): Promise { - return await TAURI_INVOKE("remove_fake_window", { name }); - }, - async focusCapturesPanel(): Promise { - await TAURI_INVOKE("focus_captures_panel"); - }, - async getCurrentRecording(): Promise> { - return await TAURI_INVOKE("get_current_recording"); - }, - async exportVideo( - projectPath: string, - progress: TAURI_CHANNEL, - settings: ExportSettings, - ): Promise { - return await TAURI_INVOKE("export_video", { - projectPath, - progress, - settings, - }); - }, - async getExportEstimates( - path: string, - resolution: XY, - fps: number, - ): Promise { - return await TAURI_INVOKE("get_export_estimates", { - path, - resolution, - fps, - }); - }, - async copyFileToPath(src: string, dst: string): Promise { - return await TAURI_INVOKE("copy_file_to_path", { src, dst }); - }, - async copyVideoToClipboard(path: string): Promise { - return await TAURI_INVOKE("copy_video_to_clipboard", { path }); - }, - async copyScreenshotToClipboard(path: string): Promise { - return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); - }, - async openFilePath(path: string): Promise { - return await TAURI_INVOKE("open_file_path", { path }); - }, - async getVideoMetadata(path: string): Promise { - return await TAURI_INVOKE("get_video_metadata", { path }); - }, - async createEditorInstance(): Promise { - return await TAURI_INVOKE("create_editor_instance"); - }, - async getMicWaveforms(): Promise { - return await TAURI_INVOKE("get_mic_waveforms"); - }, - async getSystemAudioWaveforms(): Promise { - return await TAURI_INVOKE("get_system_audio_waveforms"); - }, - async startPlayback(fps: number, resolutionBase: XY): Promise { - return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); - }, - async stopPlayback(): Promise { - return await TAURI_INVOKE("stop_playback"); - }, - async setPlayheadPosition(frameNumber: number): Promise { - return await TAURI_INVOKE("set_playhead_position", { frameNumber }); - }, - async setProjectConfig(config: ProjectConfiguration): Promise { - return await TAURI_INVOKE("set_project_config", { config }); - }, - async generateZoomSegmentsFromClicks(): Promise { - return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); - }, - async openPermissionSettings(permission: OSPermission): Promise { - await TAURI_INVOKE("open_permission_settings", { permission }); - }, - async doPermissionsCheck(initialCheck: boolean): Promise { - return await TAURI_INVOKE("do_permissions_check", { initialCheck }); - }, - async requestPermission(permission: OSPermission): Promise { - await TAURI_INVOKE("request_permission", { permission }); - }, - async uploadExportedVideo( - path: string, - mode: UploadMode, - ): Promise { - return await TAURI_INVOKE("upload_exported_video", { path, mode }); - }, - async uploadScreenshot(screenshotPath: string): Promise { - return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); - }, - async getRecordingMeta( - path: string, - fileType: FileType, - ): Promise { - return await TAURI_INVOKE("get_recording_meta", { path, fileType }); - }, - async saveFileDialog( - fileName: string, - fileType: string, - ): Promise { - return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); - }, - async listRecordings(): Promise<[string, RecordingMetaWithType][]> { - return await TAURI_INVOKE("list_recordings"); - }, - async listScreenshots(): Promise<[string, RecordingMeta][]> { - return await TAURI_INVOKE("list_screenshots"); - }, - async checkUpgradedAndUpdate(): Promise { - return await TAURI_INVOKE("check_upgraded_and_update"); - }, - async openExternalLink(url: string): Promise { - return await TAURI_INVOKE("open_external_link", { url }); - }, - async setHotkey(action: HotkeyAction, hotkey: Hotkey | null): Promise { - return await TAURI_INVOKE("set_hotkey", { action, hotkey }); - }, - async resetCameraPermissions(): Promise { - return await TAURI_INVOKE("reset_camera_permissions"); - }, - async resetMicrophonePermissions(): Promise { - return await TAURI_INVOKE("reset_microphone_permissions"); - }, - async isCameraWindowOpen(): Promise { - return await TAURI_INVOKE("is_camera_window_open"); - }, - async seekTo(frameNumber: number): Promise { - return await TAURI_INVOKE("seek_to", { frameNumber }); - }, - async positionTrafficLights( - controlsInset: [number, number] | null, - ): Promise { - await TAURI_INVOKE("position_traffic_lights", { controlsInset }); - }, - async setTheme(theme: AppTheme): Promise { - await TAURI_INVOKE("set_theme", { theme }); - }, - async globalMessageDialog(message: string): Promise { - await TAURI_INVOKE("global_message_dialog", { message }); - }, - async showWindow(window: ShowCapWindow): Promise { - return await TAURI_INVOKE("show_window", { window }); - }, - async writeClipboardString(text: string): Promise { - return await TAURI_INVOKE("write_clipboard_string", { text }); - }, - async performHapticFeedback( - pattern: HapticPattern | null, - time: HapticPerformanceTime | null, - ): Promise { - return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); - }, - async listFails(): Promise<{ [key in string]: boolean }> { - return await TAURI_INVOKE("list_fails"); - }, - async setFail(name: string, value: boolean): Promise { - await TAURI_INVOKE("set_fail", { name, value }); - }, - async updateAuthPlan(): Promise { - await TAURI_INVOKE("update_auth_plan"); - }, - async setWindowTransparent(value: boolean): Promise { - await TAURI_INVOKE("set_window_transparent", { value }); - }, - async getEditorMeta(): Promise { - return await TAURI_INVOKE("get_editor_meta"); - }, - async setPrettyName(prettyName: string): Promise { - return await TAURI_INVOKE("set_pretty_name", { prettyName }); - }, - async setServerUrl(serverUrl: string): Promise { - return await TAURI_INVOKE("set_server_url", { serverUrl }); - }, - async setCameraPreviewState(state: CameraWindowState): Promise { - return await TAURI_INVOKE("set_camera_preview_state", { state }); - }, - async awaitCameraPreviewReady(): Promise { - return await TAURI_INVOKE("await_camera_preview_ready"); - }, - /** - * Function to handle creating directories for the model - */ - async createDir(path: string, recursive: boolean): Promise { - return await TAURI_INVOKE("create_dir", { path, recursive }); - }, - /** - * Function to save the model file - */ - async saveModelFile(path: string, data: number[]): Promise { - return await TAURI_INVOKE("save_model_file", { path, data }); - }, - /** - * Function to transcribe audio from a video file using Whisper - */ - async transcribeAudio( - videoPath: string, - modelPath: string, - language: string, - ): Promise { - return await TAURI_INVOKE("transcribe_audio", { - videoPath, - modelPath, - language, - }); - }, - /** - * Function to save caption data to a file - */ - async saveCaptions(videoId: string, captions: CaptionData): Promise { - return await TAURI_INVOKE("save_captions", { videoId, captions }); - }, - /** - * Function to load caption data from a file - */ - async loadCaptions(videoId: string): Promise { - return await TAURI_INVOKE("load_captions", { videoId }); - }, - /** - * Helper function to download a Whisper model from Hugging Face Hub - */ - async downloadWhisperModel( - modelName: string, - outputPath: string, - ): Promise { - return await TAURI_INVOKE("download_whisper_model", { - modelName, - outputPath, - }); - }, - /** - * Function to check if a model file exists - */ - async checkModelExists(modelPath: string): Promise { - return await TAURI_INVOKE("check_model_exists", { modelPath }); - }, - /** - * Function to delete a downloaded model - */ - async deleteWhisperModel(modelPath: string): Promise { - return await TAURI_INVOKE("delete_whisper_model", { modelPath }); - }, - /** - * Export captions to an SRT file - */ - async exportCaptionsSrt(videoId: string): Promise { - return await TAURI_INVOKE("export_captions_srt", { videoId }); - }, - async openTargetSelectOverlays(): Promise { - return await TAURI_INVOKE("open_target_select_overlays"); - }, - async closeTargetSelectOverlays(): Promise { - return await TAURI_INVOKE("close_target_select_overlays"); - }, -}; +async setMicInput(label: string | null) : Promise { + return await TAURI_INVOKE("set_mic_input", { label }); +}, +async setCameraInput(id: DeviceOrModelID | null) : Promise { + return await TAURI_INVOKE("set_camera_input", { id }); +}, +async startRecording(inputs: StartRecordingInputs) : Promise { + return await TAURI_INVOKE("start_recording", { inputs }); +}, +async stopRecording() : Promise { + return await TAURI_INVOKE("stop_recording"); +}, +async pauseRecording() : Promise { + return await TAURI_INVOKE("pause_recording"); +}, +async resumeRecording() : Promise { + return await TAURI_INVOKE("resume_recording"); +}, +async restartRecording() : Promise { + return await TAURI_INVOKE("restart_recording"); +}, +async deleteRecording() : Promise { + return await TAURI_INVOKE("delete_recording"); +}, +async listCameras() : Promise { + return await TAURI_INVOKE("list_cameras"); +}, +async listCaptureWindows() : Promise { + return await TAURI_INVOKE("list_capture_windows"); +}, +async listCaptureDisplays() : Promise { + return await TAURI_INVOKE("list_capture_displays"); +}, +async takeScreenshot() : Promise { + return await TAURI_INVOKE("take_screenshot"); +}, +async listAudioDevices() : Promise { + return await TAURI_INVOKE("list_audio_devices"); +}, +async closeRecordingsOverlayWindow() : Promise { + await TAURI_INVOKE("close_recordings_overlay_window"); +}, +async setFakeWindowBounds(name: string, bounds: LogicalBounds) : Promise { + return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); +}, +async removeFakeWindow(name: string) : Promise { + return await TAURI_INVOKE("remove_fake_window", { name }); +}, +async focusCapturesPanel() : Promise { + await TAURI_INVOKE("focus_captures_panel"); +}, +async getCurrentRecording() : Promise> { + return await TAURI_INVOKE("get_current_recording"); +}, +async exportVideo(projectPath: string, progress: TAURI_CHANNEL, settings: ExportSettings) : Promise { + return await TAURI_INVOKE("export_video", { projectPath, progress, settings }); +}, +async getExportEstimates(path: string, resolution: XY, fps: number) : Promise { + return await TAURI_INVOKE("get_export_estimates", { path, resolution, fps }); +}, +async copyFileToPath(src: string, dst: string) : Promise { + return await TAURI_INVOKE("copy_file_to_path", { src, dst }); +}, +async copyVideoToClipboard(path: string) : Promise { + return await TAURI_INVOKE("copy_video_to_clipboard", { path }); +}, +async copyScreenshotToClipboard(path: string) : Promise { + return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); +}, +async openFilePath(path: string) : Promise { + return await TAURI_INVOKE("open_file_path", { path }); +}, +async getVideoMetadata(path: string) : Promise { + return await TAURI_INVOKE("get_video_metadata", { path }); +}, +async createEditorInstance() : Promise { + return await TAURI_INVOKE("create_editor_instance"); +}, +async getMicWaveforms() : Promise { + return await TAURI_INVOKE("get_mic_waveforms"); +}, +async getSystemAudioWaveforms() : Promise { + return await TAURI_INVOKE("get_system_audio_waveforms"); +}, +async startPlayback(fps: number, resolutionBase: XY) : Promise { + return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); +}, +async stopPlayback() : Promise { + return await TAURI_INVOKE("stop_playback"); +}, +async setPlayheadPosition(frameNumber: number) : Promise { + return await TAURI_INVOKE("set_playhead_position", { frameNumber }); +}, +async setProjectConfig(config: ProjectConfiguration) : Promise { + return await TAURI_INVOKE("set_project_config", { config }); +}, +async generateZoomSegmentsFromClicks() : Promise { + return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); +}, +async openPermissionSettings(permission: OSPermission) : Promise { + await TAURI_INVOKE("open_permission_settings", { permission }); +}, +async doPermissionsCheck(initialCheck: boolean) : Promise { + return await TAURI_INVOKE("do_permissions_check", { initialCheck }); +}, +async requestPermission(permission: OSPermission) : Promise { + await TAURI_INVOKE("request_permission", { permission }); +}, +async uploadExportedVideo(path: string, mode: UploadMode) : Promise { + return await TAURI_INVOKE("upload_exported_video", { path, mode }); +}, +async uploadScreenshot(screenshotPath: string) : Promise { + return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); +}, +async getRecordingMeta(path: string, fileType: FileType) : Promise { + return await TAURI_INVOKE("get_recording_meta", { path, fileType }); +}, +async saveFileDialog(fileName: string, fileType: string) : Promise { + return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); +}, +async listRecordings() : Promise<([string, RecordingMetaWithType])[]> { + return await TAURI_INVOKE("list_recordings"); +}, +async listScreenshots() : Promise<([string, RecordingMeta])[]> { + return await TAURI_INVOKE("list_screenshots"); +}, +async checkUpgradedAndUpdate() : Promise { + return await TAURI_INVOKE("check_upgraded_and_update"); +}, +async openExternalLink(url: string) : Promise { + return await TAURI_INVOKE("open_external_link", { url }); +}, +async setHotkey(action: HotkeyAction, hotkey: Hotkey | null) : Promise { + return await TAURI_INVOKE("set_hotkey", { action, hotkey }); +}, +async resetCameraPermissions() : Promise { + return await TAURI_INVOKE("reset_camera_permissions"); +}, +async resetMicrophonePermissions() : Promise { + return await TAURI_INVOKE("reset_microphone_permissions"); +}, +async isCameraWindowOpen() : Promise { + return await TAURI_INVOKE("is_camera_window_open"); +}, +async seekTo(frameNumber: number) : Promise { + return await TAURI_INVOKE("seek_to", { frameNumber }); +}, +async positionTrafficLights(controlsInset: [number, number] | null) : Promise { + await TAURI_INVOKE("position_traffic_lights", { controlsInset }); +}, +async setTheme(theme: AppTheme) : Promise { + await TAURI_INVOKE("set_theme", { theme }); +}, +async globalMessageDialog(message: string) : Promise { + await TAURI_INVOKE("global_message_dialog", { message }); +}, +async showWindow(window: ShowCapWindow) : Promise { + return await TAURI_INVOKE("show_window", { window }); +}, +async writeClipboardString(text: string) : Promise { + return await TAURI_INVOKE("write_clipboard_string", { text }); +}, +async performHapticFeedback(pattern: HapticPattern | null, time: HapticPerformanceTime | null) : Promise { + return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); +}, +async listFails() : Promise<{ [key in string]: boolean }> { + return await TAURI_INVOKE("list_fails"); +}, +async setFail(name: string, value: boolean) : Promise { + await TAURI_INVOKE("set_fail", { name, value }); +}, +async updateAuthPlan() : Promise { + await TAURI_INVOKE("update_auth_plan"); +}, +async setWindowTransparent(value: boolean) : Promise { + await TAURI_INVOKE("set_window_transparent", { value }); +}, +async getEditorMeta() : Promise { + return await TAURI_INVOKE("get_editor_meta"); +}, +async setPrettyName(prettyName: string) : Promise { + return await TAURI_INVOKE("set_pretty_name", { prettyName }); +}, +async setServerUrl(serverUrl: string) : Promise { + return await TAURI_INVOKE("set_server_url", { serverUrl }); +}, +async setCameraPreviewState(state: CameraWindowState) : Promise { + return await TAURI_INVOKE("set_camera_preview_state", { state }); +}, +async awaitCameraPreviewReady() : Promise { + return await TAURI_INVOKE("await_camera_preview_ready"); +}, +/** + * Function to handle creating directories for the model + */ +async createDir(path: string, recursive: boolean) : Promise { + return await TAURI_INVOKE("create_dir", { path, recursive }); +}, +/** + * Function to save the model file + */ +async saveModelFile(path: string, data: number[]) : Promise { + return await TAURI_INVOKE("save_model_file", { path, data }); +}, +/** + * Function to transcribe audio from a video file using Whisper + */ +async transcribeAudio(videoPath: string, modelPath: string, language: string) : Promise { + return await TAURI_INVOKE("transcribe_audio", { videoPath, modelPath, language }); +}, +/** + * Function to save caption data to a file + */ +async saveCaptions(videoId: string, captions: CaptionData) : Promise { + return await TAURI_INVOKE("save_captions", { videoId, captions }); +}, +/** + * Function to load caption data from a file + */ +async loadCaptions(videoId: string) : Promise { + return await TAURI_INVOKE("load_captions", { videoId }); +}, +/** + * Helper function to download a Whisper model from Hugging Face Hub + */ +async downloadWhisperModel(modelName: string, outputPath: string) : Promise { + return await TAURI_INVOKE("download_whisper_model", { modelName, outputPath }); +}, +/** + * Function to check if a model file exists + */ +async checkModelExists(modelPath: string) : Promise { + return await TAURI_INVOKE("check_model_exists", { modelPath }); +}, +/** + * Function to delete a downloaded model + */ +async deleteWhisperModel(modelPath: string) : Promise { + return await TAURI_INVOKE("delete_whisper_model", { modelPath }); +}, +/** + * Export captions to an SRT file + */ +async exportCaptionsSrt(videoId: string) : Promise { + return await TAURI_INVOKE("export_captions_srt", { videoId }); +}, +async openTargetSelectOverlays() : Promise { + return await TAURI_INVOKE("open_target_select_overlays"); +}, +async closeTargetSelectOverlays() : Promise { + return await TAURI_INVOKE("close_target_select_overlays"); +} +} /** user-defined events **/ + export const events = __makeEvents__<{ - audioInputLevelChange: AudioInputLevelChange; - authenticationInvalid: AuthenticationInvalid; - currentRecordingChanged: CurrentRecordingChanged; - downloadProgress: DownloadProgress; - editorStateChanged: EditorStateChanged; - newNotification: NewNotification; - newScreenshotAdded: NewScreenshotAdded; - newStudioRecordingAdded: NewStudioRecordingAdded; - onEscapePress: OnEscapePress; - recordingDeleted: RecordingDeleted; - recordingEvent: RecordingEvent; - recordingOptionsChanged: RecordingOptionsChanged; - recordingStarted: RecordingStarted; - recordingStopped: RecordingStopped; - renderFrameEvent: RenderFrameEvent; - requestNewScreenshot: RequestNewScreenshot; - requestOpenSettings: RequestOpenSettings; - requestStartRecording: RequestStartRecording; - targetUnderCursor: TargetUnderCursor; - uploadProgress: UploadProgress; +audioInputLevelChange: AudioInputLevelChange, +authenticationInvalid: AuthenticationInvalid, +currentRecordingChanged: CurrentRecordingChanged, +downloadProgress: DownloadProgress, +editorStateChanged: EditorStateChanged, +newNotification: NewNotification, +newScreenshotAdded: NewScreenshotAdded, +newStudioRecordingAdded: NewStudioRecordingAdded, +onEscapePress: OnEscapePress, +recordingDeleted: RecordingDeleted, +recordingEvent: RecordingEvent, +recordingOptionsChanged: RecordingOptionsChanged, +recordingStarted: RecordingStarted, +recordingStopped: RecordingStopped, +renderFrameEvent: RenderFrameEvent, +requestNewScreenshot: RequestNewScreenshot, +requestOpenSettings: RequestOpenSettings, +requestStartRecording: RequestStartRecording, +targetUnderCursor: TargetUnderCursor, +uploadProgress: UploadProgress }>({ - audioInputLevelChange: "audio-input-level-change", - authenticationInvalid: "authentication-invalid", - currentRecordingChanged: "current-recording-changed", - downloadProgress: "download-progress", - editorStateChanged: "editor-state-changed", - newNotification: "new-notification", - newScreenshotAdded: "new-screenshot-added", - newStudioRecordingAdded: "new-studio-recording-added", - onEscapePress: "on-escape-press", - recordingDeleted: "recording-deleted", - recordingEvent: "recording-event", - recordingOptionsChanged: "recording-options-changed", - recordingStarted: "recording-started", - recordingStopped: "recording-stopped", - renderFrameEvent: "render-frame-event", - requestNewScreenshot: "request-new-screenshot", - requestOpenSettings: "request-open-settings", - requestStartRecording: "request-start-recording", - targetUnderCursor: "target-under-cursor", - uploadProgress: "upload-progress", -}); +audioInputLevelChange: "audio-input-level-change", +authenticationInvalid: "authentication-invalid", +currentRecordingChanged: "current-recording-changed", +downloadProgress: "download-progress", +editorStateChanged: "editor-state-changed", +newNotification: "new-notification", +newScreenshotAdded: "new-screenshot-added", +newStudioRecordingAdded: "new-studio-recording-added", +onEscapePress: "on-escape-press", +recordingDeleted: "recording-deleted", +recordingEvent: "recording-event", +recordingOptionsChanged: "recording-options-changed", +recordingStarted: "recording-started", +recordingStopped: "recording-stopped", +renderFrameEvent: "render-frame-event", +requestNewScreenshot: "request-new-screenshot", +requestOpenSettings: "request-open-settings", +requestStartRecording: "request-start-recording", +targetUnderCursor: "target-under-cursor", +uploadProgress: "upload-progress" +}) /** user-defined constants **/ + + /** user-defined types **/ -export type AppTheme = "system" | "light" | "dark"; -export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"; -export type Audio = { - duration: number; - sample_rate: number; - channels: number; - start_time: number; -}; -export type AudioConfiguration = { - mute: boolean; - improve: boolean; - micVolumeDb?: number; - micStereoMode?: StereoMode; - systemVolumeDb?: number; -}; -export type AudioInputLevelChange = number; -export type AudioMeta = { - path: string; - /** - * unix time of the first frame - */ - start_time?: number | null; -}; -export type AuthSecret = - | { api_key: string } - | { token: string; expires: number }; -export type AuthStore = { - secret: AuthSecret; - user_id: string | null; - plan: Plan | null; - intercom_hash: string | null; -}; -export type AuthenticationInvalid = null; -export type BackgroundConfiguration = { - source: BackgroundSource; - blur: number; - padding: number; - rounding: number; - inset: number; - crop: Crop | null; - shadow?: number; - advancedShadow?: ShadowConfiguration | null; -}; -export type BackgroundSource = - | { type: "wallpaper"; path: string | null } - | { type: "image"; path: string | null } - | { type: "color"; value: [number, number, number] } - | { - type: "gradient"; - from: [number, number, number]; - to: [number, number, number]; - angle?: number; - }; -export type Camera = { - hide: boolean; - mirror: boolean; - position: CameraPosition; - size: number; - zoom_size: number | null; - rounding?: number; - shadow?: number; - advanced_shadow?: ShadowConfiguration | null; - shape?: CameraShape; -}; -export type CameraInfo = { - device_id: string; - model_id: ModelIDType | null; - display_name: string; -}; -export type CameraPosition = { x: CameraXPosition; y: CameraYPosition }; -export type CameraPreviewShape = "round" | "square" | "full"; -export type CameraPreviewSize = "sm" | "lg"; -export type CameraShape = "square" | "source"; -export type CameraWindowState = { - size: CameraPreviewSize; - shape: CameraPreviewShape; - mirrored: boolean; -}; -export type CameraXPosition = "left" | "center" | "right"; -export type CameraYPosition = "top" | "bottom"; -export type CaptionData = { - segments: CaptionSegment[]; - settings: CaptionSettings | null; -}; -export type CaptionSegment = { - id: string; - start: number; - end: number; - text: string; -}; -export type CaptionSettings = { - enabled: boolean; - font: string; - size: number; - color: string; - backgroundColor: string; - backgroundOpacity: number; - position: string; - bold: boolean; - italic: boolean; - outline: boolean; - outlineColor: string; - exportWithSubtitles: boolean; -}; -export type CaptionsData = { - segments: CaptionSegment[]; - settings: CaptionSettings; -}; -export type CaptureDisplay = { - id: DisplayId; - name: string; - refresh_rate: number; -}; -export type CaptureWindow = { - id: WindowId; - owner_name: string; - name: string; - bounds: LogicalBounds; - refresh_rate: number; -}; -export type CommercialLicense = { - licenseKey: string; - expiryDate: number | null; - refresh: number; - activatedOn: number; -}; -export type Crop = { position: XY; size: XY }; -export type CurrentRecording = { - target: CurrentRecordingTarget; - type: RecordingType; -}; -export type CurrentRecordingChanged = null; -export type CurrentRecordingTarget = - | { window: { id: WindowId; bounds: LogicalBounds } } - | { screen: { id: DisplayId } } - | { area: { screen: DisplayId; bounds: LogicalBounds } }; -export type CursorAnimationStyle = "regular" | "slow" | "fast"; -export type CursorConfiguration = { - hide?: boolean; - hideWhenIdle: boolean; - size: number; - type: CursorType; - animationStyle: CursorAnimationStyle; - tension: number; - mass: number; - friction: number; - raw?: boolean; - motionBlur?: number; - useSvg?: boolean; -}; -export type CursorMeta = { - imagePath: string; - hotspot: XY; - shape?: string | null; -}; -export type CursorType = "pointer" | "circle"; -export type Cursors = - | { [key in string]: string } - | { [key in string]: CursorMeta }; -export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType }; -export type DisplayId = string; -export type DownloadProgress = { progress: number; message: string }; -export type EditorStateChanged = { playhead_position: number }; -export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato"; -export type ExportEstimates = { - duration_seconds: number; - estimated_time_seconds: number; - estimated_size_mb: number; -}; -export type ExportSettings = - | ({ format: "Mp4" } & Mp4ExportSettings) - | ({ format: "Gif" } & GifExportSettings); -export type FileType = "recording" | "screenshot"; -export type Flags = { captions: boolean }; -export type FramesRendered = { - renderedCount: number; - totalFrames: number; - type: "FramesRendered"; -}; -export type GeneralSettingsStore = { - instanceId?: string; - uploadIndividualFiles?: boolean; - hideDockIcon?: boolean; - hapticsEnabled?: boolean; - autoCreateShareableLink?: boolean; - enableNotifications?: boolean; - disableAutoOpenLinks?: boolean; - hasCompletedStartup?: boolean; - theme?: AppTheme; - commercialLicense?: CommercialLicense | null; - lastVersion?: string | null; - windowTransparency?: boolean; - postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; - mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; - customCursorCapture?: boolean; - serverUrl?: string; - recordingCountdown?: number | null; - enableNativeCameraPreview: boolean; - autoZoomOnClicks?: boolean; - enableNewRecordingFlow: boolean; - postDeletionBehaviour?: PostDeletionBehaviour; -}; -export type GifExportSettings = { - fps: number; - resolution_base: XY; - quality: GifQuality | null; -}; -export type GifQuality = { - /** - * Encoding quality from 1-100 (default: 90) - */ - quality: number | null; - /** - * Whether to prioritize speed over quality (default: false) - */ - fast: boolean | null; -}; -export type HapticPattern = "Alignment" | "LevelChange" | "Generic"; -export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted"; -export type Hotkey = { - code: string; - meta: boolean; - ctrl: boolean; - alt: boolean; - shift: boolean; -}; -export type HotkeyAction = - | "startRecording" - | "stopRecording" - | "restartRecording"; -export type HotkeysConfiguration = { show: boolean }; -export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }; -export type InstantRecordingMeta = { fps: number; sample_rate: number | null }; -export type JsonValue = [T]; -export type LogicalBounds = { position: LogicalPosition; size: LogicalSize }; -export type LogicalPosition = { x: number; y: number }; -export type LogicalSize = { width: number; height: number }; -export type MainWindowRecordingStartBehaviour = "close" | "minimise"; -export type ModelIDType = string; -export type Mp4ExportSettings = { - fps: number; - resolution_base: XY; - compression: ExportCompression; -}; -export type MultipleSegment = { - display: VideoMeta; - camera?: VideoMeta | null; - mic?: AudioMeta | null; - system_audio?: AudioMeta | null; - cursor?: string | null; -}; -export type MultipleSegments = { - segments: MultipleSegment[]; - cursors: Cursors; -}; -export type NewNotification = { - title: string; - body: string; - is_error: boolean; -}; -export type NewScreenshotAdded = { path: string }; -export type NewStudioRecordingAdded = { path: string }; -export type OSPermission = - | "screenRecording" - | "camera" - | "microphone" - | "accessibility"; -export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied"; -export type OSPermissionsCheck = { - screenRecording: OSPermissionStatus; - microphone: OSPermissionStatus; - camera: OSPermissionStatus; - accessibility: OSPermissionStatus; -}; -export type OnEscapePress = null; -export type PhysicalSize = { width: number; height: number }; -export type Plan = { upgraded: boolean; manual: boolean; last_checked: number }; -export type Platform = "MacOS" | "Windows"; -export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow"; -export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay"; -export type Preset = { name: string; config: ProjectConfiguration }; -export type PresetsStore = { presets: Preset[]; default: number | null }; -export type ProjectConfiguration = { - aspectRatio: AspectRatio | null; - background: BackgroundConfiguration; - camera: Camera; - audio: AudioConfiguration; - cursor: CursorConfiguration; - hotkeys: HotkeysConfiguration; - timeline?: TimelineConfiguration | null; - captions?: CaptionsData | null; -}; -export type ProjectRecordingsMeta = { segments: SegmentRecordings[] }; -export type RecordingDeleted = { path: string }; -export type RecordingEvent = - | { variant: "Countdown"; value: number } - | { variant: "Started" } - | { variant: "Stopped" } - | { variant: "Failed"; error: string }; -export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { - platform?: Platform | null; - pretty_name: string; - sharing?: SharingMeta | null; -}; -export type RecordingMetaWithType = (( - | StudioRecordingMeta - | InstantRecordingMeta -) & { - platform?: Platform | null; - pretty_name: string; - sharing?: SharingMeta | null; -}) & { type: RecordingType }; -export type RecordingMode = "studio" | "instant"; -export type RecordingOptionsChanged = null; -export type RecordingStarted = null; -export type RecordingStopped = null; -export type RecordingType = "studio" | "instant"; -export type RenderFrameEvent = { - frame_number: number; - fps: number; - resolution_base: XY; -}; -export type RequestNewScreenshot = null; -export type RequestOpenSettings = { page: string }; -export type RequestStartRecording = null; -export type S3UploadMeta = { id: string }; -export type ScreenCaptureTarget = - | { variant: "window"; id: WindowId } - | { variant: "screen"; id: DisplayId } - | { variant: "area"; screen: DisplayId; bounds: LogicalBounds }; -export type ScreenUnderCursor = { - name: string; - physical_size: PhysicalSize; - refresh_rate: string; -}; -export type SegmentRecordings = { - display: Video; - camera: Video | null; - mic: Audio | null; - system_audio: Audio | null; -}; -export type SerializedEditorInstance = { - framesSocketUrl: string; - recordingDuration: number; - savedProjectConfig: ProjectConfiguration; - recordings: ProjectRecordingsMeta; - path: string; -}; -export type ShadowConfiguration = { - size: number; - opacity: number; - blur: number; -}; -export type SharingMeta = { id: string; link: string }; -export type ShowCapWindow = - | "Setup" - | "Main" - | { Settings: { page: string | null } } - | { Editor: { project_path: string } } - | "RecordingsOverlay" - | { WindowCaptureOccluder: { screen_id: DisplayId } } - | { TargetSelectOverlay: { display_id: DisplayId } } - | { CaptureArea: { screen_id: DisplayId } } - | "Camera" - | { InProgressRecording: { countdown: number | null } } - | "Upgrade" - | "ModeSelect"; -export type SingleSegment = { - display: VideoMeta; - camera?: VideoMeta | null; - audio?: AudioMeta | null; - cursor?: string | null; -}; -export type StartRecordingInputs = { - capture_target: ScreenCaptureTarget; - capture_system_audio?: boolean; - mode: RecordingMode; -}; -export type StereoMode = "stereo" | "monoL" | "monoR"; -export type StudioRecordingMeta = - | { segment: SingleSegment } - | { inner: MultipleSegments }; -export type TargetUnderCursor = { - display_id: DisplayId | null; - window: WindowUnderCursor | null; - screen: ScreenUnderCursor | null; -}; -export type TimelineConfiguration = { - segments: TimelineSegment[]; - zoomSegments: ZoomSegment[]; -}; -export type TimelineSegment = { - recordingSegment?: number; - timescale: number; - start: number; - end: number; -}; -export type UploadMode = - | { Initial: { pre_created_video: VideoUploadInfo | null } } - | "Reupload"; -export type UploadProgress = { progress: number }; -export type UploadResult = - | { Success: string } - | "NotAuthenticated" - | "PlanCheckFailed" - | "UpgradeRequired"; -export type Video = { - duration: number; - width: number; - height: number; - fps: number; - start_time: number; -}; -export type VideoMeta = { - path: string; - fps?: number; - /** - * unix time of the first frame - */ - start_time?: number | null; -}; -export type VideoRecordingMetadata = { duration: number; size: number }; -export type VideoUploadInfo = { - id: string; - link: string; - config: S3UploadMeta; -}; -export type WindowId = string; -export type WindowUnderCursor = { - id: WindowId; - app_name: string; - bounds: LogicalBounds; - icon: string | null; -}; -export type XY = { x: T; y: T }; -export type ZoomMode = "auto" | { manual: { x: number; y: number } }; -export type ZoomSegment = { - start: number; - end: number; - amount: number; - mode: ZoomMode; -}; +export type AppTheme = "system" | "light" | "dark" +export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall" +export type Audio = { duration: number; sample_rate: number; channels: number; start_time: number } +export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb?: number; micStereoMode?: StereoMode; systemVolumeDb?: number } +export type AudioInputLevelChange = number +export type AudioMeta = { path: string; +/** + * unix time of the first frame + */ +start_time?: number | null } +export type AuthSecret = { api_key: string } | { token: string; expires: number } +export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; intercom_hash: string | null } +export type AuthenticationInvalid = null +export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; inset: number; crop: Crop | null; shadow?: number; advancedShadow?: ShadowConfiguration | null } +export type BackgroundSource = { type: "wallpaper"; path: string | null } | { type: "image"; path: string | null } | { type: "color"; value: [number, number, number] } | { type: "gradient"; from: [number, number, number]; to: [number, number, number]; angle?: number } +export type Camera = { hide: boolean; mirror: boolean; position: CameraPosition; size: number; zoom_size: number | null; rounding?: number; shadow?: number; advanced_shadow?: ShadowConfiguration | null; shape?: CameraShape } +export type CameraInfo = { device_id: string; model_id: ModelIDType | null; display_name: string } +export type CameraPosition = { x: CameraXPosition; y: CameraYPosition } +export type CameraPreviewShape = "round" | "square" | "full" +export type CameraPreviewSize = "sm" | "lg" +export type CameraShape = "square" | "source" +export type CameraWindowState = { size: CameraPreviewSize; shape: CameraPreviewShape; mirrored: boolean } +export type CameraXPosition = "left" | "center" | "right" +export type CameraYPosition = "top" | "bottom" +export type CaptionData = { segments: CaptionSegment[]; settings: CaptionSettings | null } +export type CaptionSegment = { id: string; start: number; end: number; text: string } +export type CaptionSettings = { enabled: boolean; font: string; size: number; color: string; backgroundColor: string; backgroundOpacity: number; position: string; bold: boolean; italic: boolean; outline: boolean; outlineColor: string; exportWithSubtitles: boolean } +export type CaptionsData = { segments: CaptionSegment[]; settings: CaptionSettings } +export type CaptureDisplay = { id: DisplayId; name: string; refresh_rate: number } +export type CaptureWindow = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number } +export type CommercialLicense = { licenseKey: string; expiryDate: number | null; refresh: number; activatedOn: number } +export type Crop = { position: XY; size: XY } +export type CurrentRecording = { target: CurrentRecordingTarget; type: RecordingType } +export type CurrentRecordingChanged = null +export type CurrentRecordingTarget = { window: { id: WindowId; bounds: LogicalBounds } } | { screen: { id: DisplayId } } | { area: { screen: DisplayId; bounds: LogicalBounds } } +export type CursorAnimationStyle = "regular" | "slow" | "fast" +export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean; motionBlur?: number; useSvg?: boolean } +export type CursorMeta = { imagePath: string; hotspot: XY; shape?: string | null } +export type CursorType = "pointer" | "circle" +export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta } +export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType } +export type DisplayId = string +export type DownloadProgress = { progress: number; message: string } +export type EditorStateChanged = { playhead_position: number } +export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato" +export type ExportEstimates = { duration_seconds: number; estimated_time_seconds: number; estimated_size_mb: number } +export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format: "Gif" } & GifExportSettings) +export type FileType = "recording" | "screenshot" +export type Flags = { captions: boolean } +export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" } +export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; customCursorCapture?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour } +export type GifExportSettings = { fps: number; resolution_base: XY; quality: GifQuality | null } +export type GifQuality = { +/** + * Encoding quality from 1-100 (default: 90) + */ +quality: number | null; +/** + * Whether to prioritize speed over quality (default: false) + */ +fast: boolean | null } +export type HapticPattern = "Alignment" | "LevelChange" | "Generic" +export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted" +export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } +export type HotkeyAction = "startRecording" | "stopRecording" | "restartRecording" +export type HotkeysConfiguration = { show: boolean } +export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } } +export type InstantRecordingMeta = { fps: number; sample_rate: number | null } +export type JsonValue = [T] +export type LogicalBounds = { position: LogicalPosition; size: LogicalSize } +export type LogicalPosition = { x: number; y: number } +export type LogicalSize = { width: number; height: number } +export type MainWindowRecordingStartBehaviour = "close" | "minimise" +export type ModelIDType = string +export type Mp4ExportSettings = { fps: number; resolution_base: XY; compression: ExportCompression } +export type MultipleSegment = { display: VideoMeta; camera?: VideoMeta | null; mic?: AudioMeta | null; system_audio?: AudioMeta | null; cursor?: string | null } +export type MultipleSegments = { segments: MultipleSegment[]; cursors: Cursors } +export type NewNotification = { title: string; body: string; is_error: boolean } +export type NewScreenshotAdded = { path: string } +export type NewStudioRecordingAdded = { path: string } +export type OSPermission = "screenRecording" | "camera" | "microphone" | "accessibility" +export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied" +export type OSPermissionsCheck = { screenRecording: OSPermissionStatus; microphone: OSPermissionStatus; camera: OSPermissionStatus; accessibility: OSPermissionStatus } +export type OnEscapePress = null +export type PhysicalSize = { width: number; height: number } +export type Plan = { upgraded: boolean; manual: boolean; last_checked: number } +export type Platform = "MacOS" | "Windows" +export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow" +export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay" +export type Preset = { name: string; config: ProjectConfiguration } +export type PresetsStore = { presets: Preset[]; default: number | null } +export type ProjectConfiguration = { aspectRatio: AspectRatio | null; background: BackgroundConfiguration; camera: Camera; audio: AudioConfiguration; cursor: CursorConfiguration; hotkeys: HotkeysConfiguration; timeline?: TimelineConfiguration | null; captions?: CaptionsData | null } +export type ProjectRecordingsMeta = { segments: SegmentRecordings[] } +export type RecordingDeleted = { path: string } +export type RecordingEvent = { variant: "Countdown"; value: number } | { variant: "Started" } | { variant: "Stopped" } | { variant: "Failed"; error: string } +export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null } +export type RecordingMetaWithType = ((StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null }) & { type: RecordingType } +export type RecordingMode = "studio" | "instant" +export type RecordingOptionsChanged = null +export type RecordingStarted = null +export type RecordingStopped = null +export type RecordingType = "studio" | "instant" +export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY } +export type RequestNewScreenshot = null +export type RequestOpenSettings = { page: string } +export type RequestStartRecording = null +export type S3UploadMeta = { id: string } +export type ScreenCaptureTarget = { variant: "window"; id: WindowId } | { variant: "screen"; id: DisplayId } | { variant: "area"; screen: DisplayId; bounds: LogicalBounds } +export type ScreenUnderCursor = { name: string; physical_size: PhysicalSize; refresh_rate: string } +export type SegmentRecordings = { display: Video; camera: Video | null; mic: Audio | null; system_audio: Audio | null } +export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordingsMeta; path: string } +export type ShadowConfiguration = { size: number; opacity: number; blur: number } +export type SharingMeta = { id: string; link: string } +export type ShowCapWindow = "Setup" | "Main" | { Settings: { page: string | null } } | { Editor: { project_path: string } } | "RecordingsOverlay" | { WindowCaptureOccluder: { screen_id: DisplayId } } | { TargetSelectOverlay: { display_id: DisplayId } } | { CaptureArea: { screen_id: DisplayId } } | "Camera" | { InProgressRecording: { countdown: number | null } } | "Upgrade" | "ModeSelect" +export type SingleSegment = { display: VideoMeta; camera?: VideoMeta | null; audio?: AudioMeta | null; cursor?: string | null } +export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; capture_system_audio?: boolean; mode: RecordingMode } +export type StereoMode = "stereo" | "monoL" | "monoR" +export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } +export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null; screen: ScreenUnderCursor | null } +export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[] } +export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } +export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" +export type UploadProgress = { progress: number } +export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired" +export type Video = { duration: number; width: number; height: number; fps: number; start_time: number } +export type VideoMeta = { path: string; fps?: number; +/** + * unix time of the first frame + */ +start_time?: number | null } +export type VideoRecordingMetadata = { duration: number; size: number } +export type VideoUploadInfo = { id: string; link: string; config: S3UploadMeta } +export type WindowId = string +export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds; icon: string | null } +export type XY = { x: T; y: T } +export type ZoomMode = "auto" | { manual: { x: number; y: number } } +export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode } /** tauri-specta globals **/ import { - type Channel as TAURI_CHANNEL, invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import type { WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; type __EventObj__ = { listen: ( @@ -827,8 +480,9 @@ function __makeEvents__>( ) { return new Proxy( {} as unknown as { - [K in keyof T]: __EventObj__ & - ((handle: __WebviewWindow__) => __EventObj__); + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; }, { get: (_, event) => { diff --git a/crates/cursor-capture/src/main.rs b/crates/cursor-capture/src/main.rs index cb2d00370d..e6dc8c6d98 100644 --- a/crates/cursor-capture/src/main.rs +++ b/crates/cursor-capture/src/main.rs @@ -1,15 +1,13 @@ use cap_cursor_capture::RawCursorPosition; -use cap_displays::{ - Display, - bounds::{LogicalPosition, LogicalSize}, -}; +use cap_displays::Display; fn main() { loop { let position = RawCursorPosition::get() - .relative_to_display(Display::list()[0]) - .normalize() - .with_crop(LogicalPosition::new(0.0, 0.0), LogicalSize::new(1.0, 1.0)); + .relative_to_display(Display::list()[1]) + .unwrap() + .normalize(); + // .with_crop(LogicalPosition::new(0.0, 0.0), LogicalSize::new(1.0, 1.0)); println!("{position:?}"); } diff --git a/crates/cursor-capture/src/position.rs b/crates/cursor-capture/src/position.rs index db031db9ed..9332c7724f 100644 --- a/crates/cursor-capture/src/position.rs +++ b/crates/cursor-capture/src/position.rs @@ -1,14 +1,14 @@ use cap_displays::{ Display, - bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + bounds::{LogicalBounds, PhysicalPosition, PhysicalSize}, }; use device_query::{DeviceQuery, DeviceState}; +// Physical on Windows, Logical on macOS #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct RawCursorPosition { - // inner: PhysicalPosition, - pub x: i32, - pub y: i32, + x: i32, + y: i32, } impl RawCursorPosition { @@ -30,28 +30,34 @@ impl RawCursorPosition { // relative to display using top-left origin #[derive(Clone, Copy)] pub struct RelativeCursorPosition { - pub(crate) x: i32, - pub(crate) y: i32, - pub(crate) display: Display, + x: i32, + y: i32, + display: Display, } impl RelativeCursorPosition { pub fn from_raw(raw: RawCursorPosition, display: Display) -> Option { - let physical_bounds = display.physical_bounds()?; - - Some(Self { - x: raw.x - physical_bounds.position().x() as i32, - y: raw.y - physical_bounds.position().y() as i32, - display, - }) - } + #[cfg(windows)] + { + let physical_bounds = display.physical_bounds()?; + + return Some(Self { + x: raw.x - physical_bounds.position().x() as i32, + y: raw.y - physical_bounds.position().y() as i32, + display, + }); + } - pub fn x(&self) -> i32 { - self.x - } + #[cfg(target_os = "macos")] + { + let logical_bounds = display.raw_handle().logical_bounds()?; - pub fn y(&self) -> i32 { - self.y + return Some(Self { + x: raw.x - logical_bounds.position().x() as i32, + y: raw.y - logical_bounds.position().y() as i32, + display, + }); + } } pub fn display(&self) -> &Display { @@ -59,16 +65,37 @@ impl RelativeCursorPosition { } pub fn normalize(&self) -> Option { - let bounds = self.display().physical_bounds()?; - let size = bounds.size(); - - Some(NormalizedCursorPosition { - x: self.x as f64 / size.width(), - y: self.y as f64 / size.height(), - crop_position: bounds.position(), - crop_size: size, - display: self.display, - }) + #[cfg(windows)] + { + let bounds = self.display().physical_bounds()?; + let size = bounds.size(); + + Some(NormalizedCursorPosition { + x: self.x as f64 / size.width(), + y: self.y as f64 / size.height(), + crop_position: bounds.position(), + crop_size: size, + display: self.display, + }) + } + + #[cfg(target_os = "macos")] + { + let bounds = self.display().raw_handle().logical_bounds()?; + let size = bounds.size(); + + Some(NormalizedCursorPosition { + x: self.x as f64 / size.width(), + y: self.y as f64 / size.height(), + crop: CursorCropBounds { + x: 0.0, + y: 0.0, + width: size.width(), + height: size.height(), + }, + display: self.display, + }) + } } } @@ -81,12 +108,49 @@ impl std::fmt::Debug for RelativeCursorPosition { } } +#[derive(Clone, Copy, Debug)] +/// Needs to be logical coordinates on macOS and physical on Windows +/// This type is opqaue on purpose as the logical/physical invariants need to hold +pub struct CursorCropBounds { + x: f64, + y: f64, + width: f64, + height: f64, +} + +impl CursorCropBounds { + #[cfg(target_os = "macos")] + pub fn new_macos(bounds: LogicalBounds) -> Self { + Self { + x: bounds.position().x(), + y: bounds.position().y(), + width: bounds.size().width(), + height: bounds.size().height(), + } + } + + pub fn x(&self) -> f64 { + self.x + } + + pub fn y(&self) -> f64 { + self.y + } + + pub fn width(&self) -> f64 { + self.width + } + + pub fn height(&self) -> f64 { + self.height + } +} + pub struct NormalizedCursorPosition { - pub(crate) x: f64, - pub(crate) y: f64, - pub(crate) crop_position: PhysicalPosition, - pub(crate) crop_size: PhysicalSize, - pub(crate) display: Display, + x: f64, + y: f64, + crop: CursorCropBounds, + display: Display, } impl NormalizedCursorPosition { @@ -102,25 +166,22 @@ impl NormalizedCursorPosition { &self.display } - pub fn crop_position(&self) -> PhysicalPosition { - self.crop_position + pub fn crop(&self) -> CursorCropBounds { + self.crop } - pub fn crop_size(&self) -> PhysicalSize { - self.crop_size - } + pub fn with_crop(&self, crop: CursorCropBounds) -> Self { + dbg!(self.x, self.y, self.crop, crop); - pub fn with_crop(&self, position: PhysicalPosition, size: PhysicalSize) -> Self { let raw_px = ( - self.x * self.crop_size.width() + self.crop_position.x(), - self.y * self.crop_size.height() + self.crop_position.y(), + self.x * self.crop.width + self.crop.x, + self.y * self.crop.height + self.crop.y, ); Self { - x: (raw_px.0 - position.x()) / size.width(), - y: (raw_px.1 - position.y()) / size.height(), - crop_position: position, - crop_size: size, + x: (raw_px.0 - crop.x) / crop.width, + y: (raw_px.1 - crop.y) / crop.height, + crop, display: self.display, } } diff --git a/crates/displays/src/lib.rs b/crates/displays/src/lib.rs index 403b3b893b..d804c9e303 100644 --- a/crates/displays/src/lib.rs +++ b/crates/displays/src/lib.rs @@ -45,14 +45,6 @@ impl Display { self.0.physical_size() } - pub fn physical_position(&self) -> Option { - self.0.physical_position() - } - - pub fn physical_bounds(&self) -> Option { - self.0.physical_bounds() - } - pub fn logical_size(&self) -> Option { self.0.logical_size() } @@ -135,14 +127,6 @@ impl Window { self.0.physical_size() } - pub fn physical_position(&self) -> Option { - self.0.physical_size() - } - - pub fn physical_bounds(&self) -> Option { - self.0.physical_bounds() - } - pub fn logical_size(&self) -> Option { self.0.logical_size() } @@ -169,30 +153,47 @@ impl Window { pub fn display_relative_logical_bounds(&self) -> Option { let display = self.display()?; - let display_physical_bounds = display.physical_bounds()?; - let display_logical_size = display.logical_size()?; - let window_physical_bounds = self.physical_bounds()?; - - let scale = display_logical_size.width() / display_physical_bounds.size().width; - - let display_relative_physical_bounds = PhysicalBounds::new( - PhysicalPosition::new( - window_physical_bounds.position().x - display_physical_bounds.position().x, - window_physical_bounds.position().y - display_physical_bounds.position().y, - ), - window_physical_bounds.size(), - ); - - Some(LogicalBounds::new( - LogicalPosition::new( - display_relative_physical_bounds.position().x() * scale, - display_relative_physical_bounds.position().y() * scale, - ), - LogicalSize::new( - display_relative_physical_bounds.size().width() * scale, - display_relative_physical_bounds.size().height() * scale, - ), - )) + + #[cfg(target_os = "macos")] + { + let display_logical_bounds = display.raw_handle().logical_bounds()?; + let window_logical_bounds = self.raw_handle().logical_bounds()?; + + Some(LogicalBounds::new( + LogicalPosition::new( + window_logical_bounds.position().x() - display_logical_bounds.position().x(), + window_logical_bounds.position().y() - display_logical_bounds.position().y(), + ), + window_logical_bounds.size(), + )) + } + + #[cfg(windows)] + { + let display_physical_bounds = display.raw_handle().physical_bounds()?; + let window_physical_bounds: PhysicalBounds = self.physical_bounds().raw_handle()?; + + let scale = display_logical_size.width() / display_physical_bounds.size().width; + + let display_relative_physical_bounds = PhysicalBounds::new( + PhysicalPosition::new( + window_physical_bounds.position().x - display_physical_bounds.position().x, + window_physical_bounds.position().y - display_physical_bounds.position().y, + ), + window_physical_bounds.size(), + ); + + Some(LogicalBounds::new( + LogicalPosition::new( + display_relative_physical_bounds.position().x() * scale, + display_relative_physical_bounds.position().y() * scale, + ), + LogicalSize::new( + display_relative_physical_bounds.size().width() * scale, + display_relative_physical_bounds.size().height() * scale, + ), + )) + } } } diff --git a/crates/displays/src/main.rs b/crates/displays/src/main.rs index ede56a5e5a..4f7110d251 100644 --- a/crates/displays/src/main.rs +++ b/crates/displays/src/main.rs @@ -1,13 +1,17 @@ use std::time::Duration; use cap_displays::{Display, Window}; -use windows::Win32::UI::HiDpi::{ - PROCESS_DPI_UNAWARE, PROCESS_PER_MONITOR_DPI_AWARE, PROCESS_SYSTEM_DPI_AWARE, - SetProcessDpiAwareness, -}; fn main() { - unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE).unwrap() }; + #[cfg(windows)] + { + use windows::Win32::UI::HiDpi::{ + PROCESS_DPI_UNAWARE, PROCESS_PER_MONITOR_DPI_AWARE, PROCESS_SYSTEM_DPI_AWARE, + SetProcessDpiAwareness, + }; + + unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE).unwrap() }; + } for display in Display::list() { dbg!(display.name()); @@ -15,6 +19,7 @@ fn main() { let display = display.raw_handle(); dbg!(display.physical_bounds()); + dbg!(display.physical_size()); dbg!(display.logical_size()); } diff --git a/crates/displays/src/platform/macos.rs b/crates/displays/src/platform/macos.rs index 854d59cc98..1c59612d6c 100644 --- a/crates/displays/src/platform/macos.rs +++ b/crates/displays/src/platform/macos.rs @@ -1,6 +1,7 @@ use std::{ffi::c_void, str::FromStr}; use cidre::{arc, ns, sc}; +use cocoa::appkit::NSScreen; use core_foundation::{array::CFArray, base::FromVoid, number::CFNumber, string::CFString}; use core_graphics::{ display::{ @@ -13,7 +14,9 @@ use core_graphics::{ }, }; -use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize}; +use crate::bounds::{ + LogicalBounds, LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, +}; #[derive(Clone, Copy)] pub struct DisplayImpl(CGDisplay); @@ -44,13 +47,13 @@ impl DisplayImpl { Self::list().into_iter().find(|d| d.0.id == parsed_id) } - pub fn logical_size(&self) -> LogicalSize { + pub fn logical_size(&self) -> Option { let rect = unsafe { CGDisplayBounds(self.0.id) }; - LogicalSize { + Some(LogicalSize { width: rect.size.width, height: rect.size.height, - } + }) } pub fn logical_position(&self) -> LogicalPosition { @@ -62,32 +65,33 @@ impl DisplayImpl { } } - pub fn logical_bounds(&self) -> LogicalBounds { - LogicalBounds { + pub fn logical_bounds(&self) -> Option { + Some(LogicalBounds { position: self.logical_position(), - size: self.logical_size(), - } + size: self.logical_size()?, + }) } pub fn get_containing_cursor() -> Option { let cursor = get_cursor_position()?; Self::list().into_iter().find(|display| { + let Some(logical_size) = display.logical_size() else { + return false; + }; + let bounds = LogicalBounds { position: display.logical_position(), - size: display.logical_size(), + size: logical_size, }; bounds.contains_point(cursor) }) } - pub fn physical_size(&self) -> PhysicalSize { + pub fn physical_size(&self) -> Option { let mode = unsafe { CGDisplayCopyDisplayMode(self.0.id) }; if mode.is_null() { - return PhysicalSize { - width: 0.0, - height: 0.0, - }; + return None; } let width = unsafe { core_graphics::display::CGDisplayModeGetPixelWidth(mode) }; @@ -95,10 +99,14 @@ impl DisplayImpl { unsafe { core_graphics::display::CGDisplayModeRelease(mode) }; - PhysicalSize { + Some(PhysicalSize { width: width as f64, height: height as f64, - } + }) + } + + pub fn scale(&self) -> Option { + Some(unsafe { NSScreen::backingScaleFactor(self.as_ns_screen()?) }) } pub fn refresh_rate(&self) -> f64 { @@ -115,11 +123,32 @@ impl DisplayImpl { } pub fn name(&self) -> Option { + use cocoa::appkit::NSScreen; + use cocoa::base::id; + use cocoa::foundation::NSString; + use objc::{msg_send, *}; + use std::ffi::CStr; + + unsafe { + if let Some(ns_screen) = self.as_ns_screen() { + let name: id = msg_send![ns_screen, localizedName]; + if !name.is_null() { + let name = CStr::from_ptr(NSString::UTF8String(name)) + .to_string_lossy() + .to_string(); + return Some(name); + } + } + } + + None + } + + fn as_ns_screen(&self) -> Option<*mut objc::runtime::Object> { use cocoa::appkit::NSScreen; use cocoa::base::{id, nil}; use cocoa::foundation::{NSArray, NSDictionary, NSString}; use objc::{msg_send, *}; - use std::ffi::CStr; unsafe { let screens = NSScreen::screens(nil); @@ -137,13 +166,7 @@ impl DisplayImpl { let num_value: u32 = msg_send![num, unsignedIntValue]; if num_value == self.0.id { - let name: id = msg_send![screen, localizedName]; - if !name.is_null() { - let name = CStr::from_ptr(NSString::UTF8String(name)) - .to_string_lossy() - .to_string(); - return Some(name); - } + return Some(screen); } } } @@ -335,11 +358,15 @@ impl WindowImpl { }) } + pub fn logical_size(&self) -> Option { + Some(self.logical_bounds()?.size()) + } + pub fn physical_size(&self) -> Option { let logical_bounds = self.logical_bounds()?; let display = self.display()?; - let scale = display.physical_size().width() / display.logical_size().width(); + let scale = display.physical_size()?.width() / display.logical_size()?.width(); Some(PhysicalSize { width: logical_bounds.size().width() * scale, diff --git a/crates/recording/Cargo.toml b/crates/recording/Cargo.toml index 74c31634d7..25b5cf7e1f 100644 --- a/crates/recording/Cargo.toml +++ b/crates/recording/Cargo.toml @@ -34,7 +34,7 @@ tracing.workspace = true device_query = "4.0.1" image = "0.25.2" either = "1.13.0" -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.19" } relative-path = "1.9.3" futures = { workspace = true } tokio-util = "0.7.15" diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index 8e766fb7f2..85dfa9168b 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use cap_displays::Window; +use cap_displays::{Display, Window}; use cap_recording::{RecordingBaseInputs, screen_capture::ScreenCaptureTarget}; #[tokio::main] @@ -21,6 +21,10 @@ pub async fn main() { println!("Recording to directory '{}'", dir.path().display()); + for display in Display::list() { + display.name(); + } + let (handle, _ready_rx) = cap_recording::spawn_studio_recording_actor( "test".to_string(), dir.path().into(), @@ -28,7 +32,7 @@ pub async fn main() { capture_target: ScreenCaptureTarget::Window { id: Window::list() .into_iter() - .find(|w| w.name().unwrap_or_default().contains("Firefox")) + .find(|w| w.owner_name().unwrap_or_default().contains("Brave")) .unwrap() .id(), }, diff --git a/crates/recording/src/capture_pipeline.rs b/crates/recording/src/capture_pipeline.rs index 908f14303d..ec09554fd2 100644 --- a/crates/recording/src/capture_pipeline.rs +++ b/crates/recording/src/capture_pipeline.rs @@ -57,6 +57,8 @@ impl MakeCapturePipeline for screen_capture::CMSampleBufferCapture { output_path: PathBuf, ) -> Result<(PipelineBuilder, flume::Receiver), MediaError> { let screen_config = source.0.info(); + tracing::info!("screen config: {:?}", screen_config); + let mut screen_encoder = cap_media_encoders::MP4AVAssetWriterEncoder::init( "screen", screen_config, @@ -235,7 +237,7 @@ impl MakeCapturePipeline for screen_capture::AVFrameCapture { where Self: Sized, { - use cap_media_encoders::{MP4File, H264Encoder}; + use cap_media_encoders::{H264Encoder, MP4File}; let screen_config = source.0.info(); let mut screen_encoder = MP4File::init( @@ -282,7 +284,7 @@ impl MakeCapturePipeline for screen_capture::AVFrameCapture { where Self: Sized, { - use cap_media_encoders::{MP4File, H264Encoder, AACEncoder, AudioEncoder}; + use cap_media_encoders::{AACEncoder, AudioEncoder, H264Encoder, MP4File}; let (audio_tx, audio_rx) = flume::bounded(64); let mut audio_mixer = AudioMixer::new(audio_tx); diff --git a/crates/recording/src/cursor.rs b/crates/recording/src/cursor.rs index a944042fd5..bcc053b1f5 100644 --- a/crates/recording/src/cursor.rs +++ b/crates/recording/src/cursor.rs @@ -1,3 +1,4 @@ +use cap_cursor_capture::CursorCropBounds; use cap_cursor_info::CursorShape; use cap_displays::bounds::{LogicalBounds, PhysicalBounds}; use cap_project::{CursorClickEvent, CursorMoveEvent, XY}; @@ -36,7 +37,7 @@ impl CursorActor { #[tracing::instrument(name = "cursor", skip_all)] pub fn spawn_cursor_recorder( - crop_bounds: PhysicalBounds, + crop_bounds: CursorCropBounds, display: cap_displays::Display, cursors_dir: PathBuf, prev_cursors: Cursors, @@ -137,7 +138,7 @@ pub fn spawn_cursor_recorder( let cropped_norm_pos = position .relative_to_display(display)? .normalize()? - .with_crop(crop_bounds.position(), crop_bounds.size()); + .with_crop(crop_bounds); Some((cropped_norm_pos.x(), cropped_norm_pos.y())) }); diff --git a/crates/recording/src/sources/screen_capture.rs b/crates/recording/src/sources/screen_capture.rs index 2375d8ac6a..6d1aa1bbbd 100644 --- a/crates/recording/src/sources/screen_capture.rs +++ b/crates/recording/src/sources/screen_capture.rs @@ -1,3 +1,4 @@ +use cap_cursor_capture::CursorCropBounds; use cap_displays::{ Display, DisplayId, Window, WindowId, bounds::{ @@ -77,42 +78,86 @@ impl ScreenCaptureTarget { } } - pub fn display_relative_physical_bounds(&self) -> Option { + pub fn cursor_crop(&self) -> Option { match self { - Self::Screen { .. } => Some(PhysicalBounds::new( - PhysicalPosition::new(0.0, 0.0), - self.physical_size()?, - )), + Self::Screen { .. } => { + #[cfg(target_os = "macos")] + { + let display = self.display()?; + return Some(CursorCropBounds::new_macos(LogicalBounds::new( + LogicalPosition::new(0.0, 0.0), + display.raw_handle().logical_size()?, + ))); + } + + #[cfg(windows)] + { + return Some(PhysicalBounds::new( + PhysicalPosition::new(0.0, 0.0), + self.physical_size()?, + )); + } + } Self::Window { id } => { let window = Window::from_id(id)?; - let display_bounds = self.display()?.physical_bounds()?; - let window_bounds = window.physical_bounds()?; - - Some(PhysicalBounds::new( - PhysicalPosition::new( - window_bounds.position().x() - display_bounds.position().x(), - window_bounds.position().y() - display_bounds.position().y(), - ), - PhysicalSize::new(window_bounds.size().width(), window_bounds.size().height()), - )) + let display = self.display()?; + + #[cfg(target_os = "macos")] + { + let display_position = display.raw_handle().logical_position(); + let window_bounds = window.raw_handle().logical_bounds()?; + + return Some(CursorCropBounds::new_macos(LogicalBounds::new( + LogicalPosition::new( + window_bounds.position().x() - display_position.x(), + window_bounds.position().y() - display_position.y(), + ), + window_bounds.size(), + ))); + } + + #[cfg(windows)] + { + let display_bounds = self.display()?.physical_bounds()?; + let window_bounds = window.physical_bounds()?; + + return Some(PhysicalBounds::new( + PhysicalPosition::new( + window_bounds.position().x() - display_bounds.position().x(), + window_bounds.position().y() - display_bounds.position().y(), + ), + PhysicalSize::new( + window_bounds.size().width(), + window_bounds.size().height(), + ), + )); + } } Self::Area { bounds, .. } => { - let display = self.display()?; - let display_bounds = display.physical_bounds()?; - let display_logical_size = display.logical_size()?; - - let scale = display_bounds.size().width() / display_logical_size.width(); - - Some(PhysicalBounds::new( - PhysicalPosition::new( - bounds.position().x() * scale, - bounds.position().y() * scale, - ), - PhysicalSize::new( - bounds.size().width() * scale, - bounds.size().height() * scale, - ), - )) + #[cfg(target_os = "macos")] + { + return Some(CursorCropBounds::new_macos(*bounds)); + } + + #[cfg(windows)] + { + let display = self.display()?; + let display_bounds = display.physical_bounds()?; + let display_logical_size = display.logical_size()?; + + let scale = display_bounds.size().width() / display_logical_size.width(); + + return Some(PhysicalBounds::new( + PhysicalPosition::new( + bounds.position().x() * scale, + bounds.position().y() * scale, + ), + PhysicalSize::new( + bounds.size().width() * scale, + bounds.size().height() * scale, + ), + )); + } } } } @@ -193,10 +238,13 @@ impl Clone for ScreenCaptureSource, + #[cfg(target_os = "macos")] + crop_bounds: Option, fps: u32, show_cursor: bool, } @@ -231,43 +279,81 @@ impl ScreenCaptureSource { ScreenCaptureTarget::Window { id } => { let window = Window::from_id(&id).unwrap(); - let raw_display_position = display.physical_position().unwrap(); - let raw_window_bounds = window.physical_bounds().unwrap(); + #[cfg(target_os = "macos")] + { + let raw_display_bounds = display.raw_handle().logical_bounds().unwrap(); + let raw_window_bounds = window.raw_handle().logical_bounds().unwrap(); + + Some(LogicalBounds::new( + LogicalPosition::new( + raw_window_bounds.position().x() - raw_display_bounds.position().x(), + raw_window_bounds.position().y() - raw_display_bounds.position().y(), + ), + raw_window_bounds.size(), + )) + } - Some(PhysicalBounds::new( - PhysicalPosition::new( - raw_window_bounds.position().x() - raw_display_position.x(), - raw_window_bounds.position().y() - raw_display_position.y(), - ), - raw_window_bounds.size(), - )) + #[cfg(windows)] + { + let raw_display_position = display.raw_handle().physical_position().unwrap(); + let raw_window_bounds = window.physical_bounds().unwrap(); + + Some(PhysicalBounds::new( + PhysicalPosition::new( + raw_window_bounds.position().x() - raw_display_position.x(), + raw_window_bounds.position().y() - raw_display_position.y(), + ), + raw_window_bounds.size(), + )) + } } ScreenCaptureTarget::Area { bounds: relative_bounds, .. } => { - let raw_display_size = display.physical_size().unwrap(); - let logical_display_size = display.logical_size().unwrap(); - - Some(PhysicalBounds::new( - PhysicalPosition::new( - (relative_bounds.position().x() / logical_display_size.width()) - * raw_display_size.width(), - (relative_bounds.position().y() / logical_display_size.height()) - * raw_display_size.height(), - ), - PhysicalSize::new( - (relative_bounds.size().width() / logical_display_size.width()) - * raw_display_size.width(), - (relative_bounds.size().height() / logical_display_size.height()) - * raw_display_size.height(), - ), - )) + #[cfg(target_os = "macos")] + { + Some(*relative_bounds) + } + + #[cfg(windows)] + { + let raw_display_size = display.physical_size().unwrap(); + let logical_display_size = display.logical_size().unwrap(); + + Some(PhysicalBounds::new( + PhysicalPosition::new( + (relative_bounds.position().x() / logical_display_size.width()) + * raw_display_size.width(), + (relative_bounds.position().y() / logical_display_size.height()) + * raw_display_size.height(), + ), + PhysicalSize::new( + (relative_bounds.size().width() / logical_display_size.width()) + * raw_display_size.width(), + (relative_bounds.size().height() / logical_display_size.height()) + * raw_display_size.height(), + ), + )) + } } }; let output_size = crop_bounds - .map(|b| b.size()) + .and_then(|b| { + #[cfg(target_os = "macos")] + { + let logical_size = b.size(); + let scale = display.raw_handle().scale()?; + Some(PhysicalSize::new( + logical_size.width() * scale, + logical_size.height() * scale, + )) + } + + #[cfg(windows)] + Some(b.size()) + }) .or_else(|| display.physical_size()) .unwrap(); @@ -950,7 +1036,6 @@ mod macos { let video_tx = self.video_tx.clone(); let audio_tx = self.audio_tx.clone(); let config = self.config.clone(); - let display = self.display.clone(); self.tokio_handle.block_on(async move { let frame_handler = FrameHandler::spawn(FrameHandler { @@ -961,13 +1046,29 @@ mod macos { start_time_f64, }); + let display = Display::from_id(&config.display).unwrap(); + let content_filter = display .raw_handle() .as_content_filter() .await .ok_or_else(|| "Failed to get content filter".to_string())?; - let size = config.target.physical_size().unwrap(); + let size = { + let logical_size = config + .crop_bounds + .map(|bounds| bounds.size()) + .or_else(|| display.logical_size()) + .unwrap(); + + let scale = display.physical_size().unwrap().width() + / display.logical_size().unwrap().width(); + + PhysicalSize::new(logical_size.width() * scale, logical_size.height() * scale) + }; + + tracing::info!("size: {:?}", size); + let mut settings = scap_screencapturekit::StreamCfgBuilder::default() .with_width(size.width() as usize) .with_height(size.height() as usize) @@ -977,19 +1078,8 @@ mod macos { settings.set_pixel_format(cv::PixelFormat::_32_BGRA); - let crop_bounds = match &config.target { - ScreenCaptureTarget::Window { id } => Some( - Window::from_id(&id) - .unwrap() - .raw_handle() - .logical_bounds() - .unwrap(), - ), - ScreenCaptureTarget::Area { bounds, .. } => Some(bounds.clone()), - _ => None, - }; - - if let Some(crop_bounds) = crop_bounds { + if let Some(crop_bounds) = config.crop_bounds { + tracing::info!("crop bounds: {:?}", crop_bounds); settings.set_src_rect(cg::Rect::new( crop_bounds.position().x(), crop_bounds.position().y(), diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs index 7fe6715a67..13fa7a5d80 100644 --- a/crates/recording/src/studio_recording.rs +++ b/crates/recording/src/studio_recording.rs @@ -686,7 +686,7 @@ async fn create_segment_pipeline( .display() .ok_or(CreateSegmentPipelineError::NoDisplay)?; let crop_bounds = capture_target - .display_relative_physical_bounds() + .cursor_crop() .ok_or(CreateSegmentPipelineError::NoBounds)?; let (screen_source, screen_rx) = create_screen_capture( diff --git a/crates/scap-screencapturekit/src/capture.rs b/crates/scap-screencapturekit/src/capture.rs index a8d029063d..0c171319db 100644 --- a/crates/scap-screencapturekit/src/capture.rs +++ b/crates/scap-screencapturekit/src/capture.rs @@ -27,7 +27,6 @@ impl sc::stream::OutputImpl for CapturerCallbacks { let frame = match kind { sc::OutputType::Screen => { let Some(image_buf) = sample_buf.image_buf().map(|v| v.retained()) else { - warn!("Screen sample buffer has no image buffer"); return; }; From bb9c6cc98d6d87b82a89646ed1154cb6083736d8 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 21 Aug 2025 19:24:22 +0800 Subject: [PATCH 26/47] fixup windows --- apps/desktop/src-tauri/src/windows.rs | 1 + crates/cursor-capture/src/position.rs | 24 +++++-- crates/displays/src/lib.rs | 3 +- crates/displays/src/platform/win.rs | 67 ++++++++++++++++++- crates/recording/examples/recording-cli.rs | 17 +++-- .../recording/src/sources/screen_capture.rs | 32 +++++---- 6 files changed, 121 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 0c91f7f65c..4ca4248c85 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -453,6 +453,7 @@ impl ShowCapWindow { return Err(tauri::Error::WindowNotFound); }; + #[cfg(target_os = "macos")] if let Some(bounds) = display.raw_handle().logical_bounds() { window_builder = window_builder .inner_size(bounds.size().width(), bounds.size().height()) diff --git a/crates/cursor-capture/src/position.rs b/crates/cursor-capture/src/position.rs index 9332c7724f..03067cbc1b 100644 --- a/crates/cursor-capture/src/position.rs +++ b/crates/cursor-capture/src/position.rs @@ -1,3 +1,5 @@ +#[cfg(target_os = "windows")] +use cap_displays::bounds::PhysicalBounds; use cap_displays::{ Display, bounds::{LogicalBounds, PhysicalPosition, PhysicalSize}, @@ -39,7 +41,7 @@ impl RelativeCursorPosition { pub fn from_raw(raw: RawCursorPosition, display: Display) -> Option { #[cfg(windows)] { - let physical_bounds = display.physical_bounds()?; + let physical_bounds = display.raw_handle().physical_bounds()?; return Some(Self { x: raw.x - physical_bounds.position().x() as i32, @@ -67,14 +69,18 @@ impl RelativeCursorPosition { pub fn normalize(&self) -> Option { #[cfg(windows)] { - let bounds = self.display().physical_bounds()?; + let bounds = self.display().raw_handle().physical_bounds()?; let size = bounds.size(); Some(NormalizedCursorPosition { x: self.x as f64 / size.width(), y: self.y as f64 / size.height(), - crop_position: bounds.position(), - crop_size: size, + crop: CursorCropBounds { + x: 0.0, + y: 0.0, + width: size.width(), + height: size.height(), + }, display: self.display, }) } @@ -129,6 +135,16 @@ impl CursorCropBounds { } } + #[cfg(target_os = "windows")] + pub fn new_windows(bounds: PhysicalBounds) -> Self { + Self { + x: bounds.position().x(), + y: bounds.position().y(), + width: bounds.size().width(), + height: bounds.size().height(), + } + } + pub fn x(&self) -> f64 { self.x } diff --git a/crates/displays/src/lib.rs b/crates/displays/src/lib.rs index d804c9e303..2c9bba51e8 100644 --- a/crates/displays/src/lib.rs +++ b/crates/displays/src/lib.rs @@ -171,7 +171,8 @@ impl Window { #[cfg(windows)] { let display_physical_bounds = display.raw_handle().physical_bounds()?; - let window_physical_bounds: PhysicalBounds = self.physical_bounds().raw_handle()?; + let display_logical_size = display.logical_size()?; + let window_physical_bounds: PhysicalBounds = self.raw_handle().physical_bounds()?; let scale = display_logical_size.width() / display_physical_bounds.size().width; diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs index 9802e4b43c..121e8e89ca 100644 --- a/crates/displays/src/platform/win.rs +++ b/crates/displays/src/platform/win.rs @@ -1,4 +1,4 @@ -use std::{mem, str::FromStr}; +use std::{ffi::OsString, mem, os::windows::ffi::OsStringExt, path::PathBuf, str::FromStr}; use tracing::error; use windows::{ Graphics::Capture::GraphicsCaptureItem, @@ -13,7 +13,7 @@ use windows::{ }, Foundation::{CloseHandle, HWND, LPARAM, POINT, RECT, TRUE, WIN32_ERROR, WPARAM}, Graphics::{ - Dwm::{DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute}, + Dwm::{DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute}, Gdi::{ BI_RGB, BITMAP, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, CreateCompatibleDC, CreateSolidBrush, DEVMODEW, DIB_RGB_COLORS, @@ -690,6 +690,10 @@ impl WindowImpl { context.list } + pub fn inner(&self) -> HWND { + self.0 + } + pub fn get_topmost_at_cursor() -> Option { let cursor = get_cursor_position()?; let point = POINT { @@ -1319,6 +1323,42 @@ impl WindowImpl { ) .ok() } + + pub fn is_on_screen(&self) -> bool { + use ::windows::Win32::UI::WindowsAndMessaging::IsWindowVisible; + if !unsafe { IsWindowVisible(self.0) }.as_bool() { + return false; + } + + let mut pvattribute_cloaked = 0u32; + unsafe { + DwmGetWindowAttribute( + self.0, + DWMWA_CLOAKED, + &mut pvattribute_cloaked as *mut _ as *mut std::ffi::c_void, + std::mem::size_of::() as u32, + ) + } + .ok(); + + if pvattribute_cloaked != 0 { + return false; + } + + let mut process_id = 0; + unsafe { GetWindowThreadProcessId(self.0, Some(&mut process_id)) }; + + let owner_process_path = match unsafe { pid_to_exe_path(process_id) } { + Ok(path) => path, + Err(_) => return false, + }; + + if owner_process_path.starts_with("C:\\Windows\\SystemApps") { + return false; + } + + true + } } fn is_window_valid_for_enumeration(hwnd: HWND, current_process_id: u32) -> bool { @@ -1450,3 +1490,26 @@ impl FromStr for WindowIdImpl { .map_err(|_| "Invalid window ID".to_string()) } } + +unsafe fn pid_to_exe_path(pid: u32) -> Result { + let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }?; + if handle.is_invalid() { + tracing::error!("Invalid PID {}", pid); + } + let mut lpexename = [0u16; 1024]; + let mut lpdwsize = lpexename.len() as u32; + + let query = unsafe { + QueryFullProcessImageNameW( + handle, + PROCESS_NAME_FORMAT::default(), + windows::core::PWSTR(lpexename.as_mut_ptr()), + &mut lpdwsize, + ) + }; + unsafe { CloseHandle(handle) }.ok(); + query?; + + let os_str = &OsString::from_wide(&lpexename[..lpdwsize as usize]); + Ok(PathBuf::from(os_str)) +} diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index 85dfa9168b..bbaaa98fd8 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -1,7 +1,11 @@ use std::time::Duration; use cap_displays::{Display, Window}; -use cap_recording::{RecordingBaseInputs, screen_capture::ScreenCaptureTarget}; +use cap_recording::{ + RecordingBaseInputs, + screen_capture::ScreenCaptureTarget, + sources::{ScreenCaptureSource, list_displays, list_windows}, +}; #[tokio::main] pub async fn main() { @@ -21,9 +25,14 @@ pub async fn main() { println!("Recording to directory '{}'", dir.path().display()); - for display in Display::list() { - display.name(); - } + dbg!( + list_windows() + .into_iter() + .map(|(v, _)| v) + .collect::>() + ); + + return; let (handle, _ready_rx) = cap_recording::spawn_studio_recording_actor( "test".to_string(), diff --git a/crates/recording/src/sources/screen_capture.rs b/crates/recording/src/sources/screen_capture.rs index 6d1aa1bbbd..295bdcbdd9 100644 --- a/crates/recording/src/sources/screen_capture.rs +++ b/crates/recording/src/sources/screen_capture.rs @@ -92,18 +92,19 @@ impl ScreenCaptureTarget { #[cfg(windows)] { - return Some(PhysicalBounds::new( + let display = self.display()?; + return Some(CursorCropBounds::new_windows(PhysicalBounds::new( PhysicalPosition::new(0.0, 0.0), - self.physical_size()?, - )); + display.raw_handle().physical_size()?, + ))); } } Self::Window { id } => { let window = Window::from_id(id)?; - let display = self.display()?; #[cfg(target_os = "macos")] { + let display = self.display()?; let display_position = display.raw_handle().logical_position(); let window_bounds = window.raw_handle().logical_bounds()?; @@ -118,10 +119,10 @@ impl ScreenCaptureTarget { #[cfg(windows)] { - let display_bounds = self.display()?.physical_bounds()?; - let window_bounds = window.physical_bounds()?; + let display_bounds = self.display()?.raw_handle().physical_bounds()?; + let window_bounds = window.raw_handle().physical_bounds()?; - return Some(PhysicalBounds::new( + return Some(CursorCropBounds::new_windows(PhysicalBounds::new( PhysicalPosition::new( window_bounds.position().x() - display_bounds.position().x(), window_bounds.position().y() - display_bounds.position().y(), @@ -130,7 +131,7 @@ impl ScreenCaptureTarget { window_bounds.size().width(), window_bounds.size().height(), ), - )); + ))); } } Self::Area { bounds, .. } => { @@ -142,12 +143,12 @@ impl ScreenCaptureTarget { #[cfg(windows)] { let display = self.display()?; - let display_bounds = display.physical_bounds()?; + let display_bounds = display.raw_handle().physical_bounds()?; let display_logical_size = display.logical_size()?; let scale = display_bounds.size().width() / display_logical_size.width(); - return Some(PhysicalBounds::new( + return Some(CursorCropBounds::new_windows(PhysicalBounds::new( PhysicalPosition::new( bounds.position().x() * scale, bounds.position().y() * scale, @@ -156,7 +157,7 @@ impl ScreenCaptureTarget { bounds.size().width() * scale, bounds.size().height() * scale, ), - )); + ))); } } } @@ -296,7 +297,7 @@ impl ScreenCaptureSource { #[cfg(windows)] { let raw_display_position = display.raw_handle().physical_position().unwrap(); - let raw_window_bounds = window.physical_bounds().unwrap(); + let raw_window_bounds = window.raw_handle().physical_bounds().unwrap(); Some(PhysicalBounds::new( PhysicalPosition::new( @@ -422,6 +423,13 @@ pub fn list_windows() -> Vec<(CaptureWindow, Window)> { } } + #[cfg(windows)] + { + if !v.raw_handle().is_on_screen() { + return None; + } + } + Some(( CaptureWindow { id: v.id(), From a88491697e3f345cd015a09e1c89732933745241 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 21 Aug 2025 20:54:52 +0800 Subject: [PATCH 27/47] macos audio capture --- apps/desktop/src/utils/tauri.ts | 1228 +++++++++++------ crates/cursor-capture/src/position.rs | 2 - crates/recording/examples/recording-cli.rs | 16 +- .../recording/src/sources/screen_capture.rs | 2 + crates/scap-screencapturekit/src/capture.rs | 1 - crates/scap-screencapturekit/src/config.rs | 9 + packages/ui-solid/src/auto-imports.d.ts | 4 + 7 files changed, 810 insertions(+), 452 deletions(-) diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 94c2cc0030..b4645d7441 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1,463 +1,810 @@ - // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. /** user-defined commands **/ - export const commands = { -async setMicInput(label: string | null) : Promise { - return await TAURI_INVOKE("set_mic_input", { label }); -}, -async setCameraInput(id: DeviceOrModelID | null) : Promise { - return await TAURI_INVOKE("set_camera_input", { id }); -}, -async startRecording(inputs: StartRecordingInputs) : Promise { - return await TAURI_INVOKE("start_recording", { inputs }); -}, -async stopRecording() : Promise { - return await TAURI_INVOKE("stop_recording"); -}, -async pauseRecording() : Promise { - return await TAURI_INVOKE("pause_recording"); -}, -async resumeRecording() : Promise { - return await TAURI_INVOKE("resume_recording"); -}, -async restartRecording() : Promise { - return await TAURI_INVOKE("restart_recording"); -}, -async deleteRecording() : Promise { - return await TAURI_INVOKE("delete_recording"); -}, -async listCameras() : Promise { - return await TAURI_INVOKE("list_cameras"); -}, -async listCaptureWindows() : Promise { - return await TAURI_INVOKE("list_capture_windows"); -}, -async listCaptureDisplays() : Promise { - return await TAURI_INVOKE("list_capture_displays"); -}, -async takeScreenshot() : Promise { - return await TAURI_INVOKE("take_screenshot"); -}, -async listAudioDevices() : Promise { - return await TAURI_INVOKE("list_audio_devices"); -}, -async closeRecordingsOverlayWindow() : Promise { - await TAURI_INVOKE("close_recordings_overlay_window"); -}, -async setFakeWindowBounds(name: string, bounds: LogicalBounds) : Promise { - return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); -}, -async removeFakeWindow(name: string) : Promise { - return await TAURI_INVOKE("remove_fake_window", { name }); -}, -async focusCapturesPanel() : Promise { - await TAURI_INVOKE("focus_captures_panel"); -}, -async getCurrentRecording() : Promise> { - return await TAURI_INVOKE("get_current_recording"); -}, -async exportVideo(projectPath: string, progress: TAURI_CHANNEL, settings: ExportSettings) : Promise { - return await TAURI_INVOKE("export_video", { projectPath, progress, settings }); -}, -async getExportEstimates(path: string, resolution: XY, fps: number) : Promise { - return await TAURI_INVOKE("get_export_estimates", { path, resolution, fps }); -}, -async copyFileToPath(src: string, dst: string) : Promise { - return await TAURI_INVOKE("copy_file_to_path", { src, dst }); -}, -async copyVideoToClipboard(path: string) : Promise { - return await TAURI_INVOKE("copy_video_to_clipboard", { path }); -}, -async copyScreenshotToClipboard(path: string) : Promise { - return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); -}, -async openFilePath(path: string) : Promise { - return await TAURI_INVOKE("open_file_path", { path }); -}, -async getVideoMetadata(path: string) : Promise { - return await TAURI_INVOKE("get_video_metadata", { path }); -}, -async createEditorInstance() : Promise { - return await TAURI_INVOKE("create_editor_instance"); -}, -async getMicWaveforms() : Promise { - return await TAURI_INVOKE("get_mic_waveforms"); -}, -async getSystemAudioWaveforms() : Promise { - return await TAURI_INVOKE("get_system_audio_waveforms"); -}, -async startPlayback(fps: number, resolutionBase: XY) : Promise { - return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); -}, -async stopPlayback() : Promise { - return await TAURI_INVOKE("stop_playback"); -}, -async setPlayheadPosition(frameNumber: number) : Promise { - return await TAURI_INVOKE("set_playhead_position", { frameNumber }); -}, -async setProjectConfig(config: ProjectConfiguration) : Promise { - return await TAURI_INVOKE("set_project_config", { config }); -}, -async generateZoomSegmentsFromClicks() : Promise { - return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); -}, -async openPermissionSettings(permission: OSPermission) : Promise { - await TAURI_INVOKE("open_permission_settings", { permission }); -}, -async doPermissionsCheck(initialCheck: boolean) : Promise { - return await TAURI_INVOKE("do_permissions_check", { initialCheck }); -}, -async requestPermission(permission: OSPermission) : Promise { - await TAURI_INVOKE("request_permission", { permission }); -}, -async uploadExportedVideo(path: string, mode: UploadMode) : Promise { - return await TAURI_INVOKE("upload_exported_video", { path, mode }); -}, -async uploadScreenshot(screenshotPath: string) : Promise { - return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); -}, -async getRecordingMeta(path: string, fileType: FileType) : Promise { - return await TAURI_INVOKE("get_recording_meta", { path, fileType }); -}, -async saveFileDialog(fileName: string, fileType: string) : Promise { - return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); -}, -async listRecordings() : Promise<([string, RecordingMetaWithType])[]> { - return await TAURI_INVOKE("list_recordings"); -}, -async listScreenshots() : Promise<([string, RecordingMeta])[]> { - return await TAURI_INVOKE("list_screenshots"); -}, -async checkUpgradedAndUpdate() : Promise { - return await TAURI_INVOKE("check_upgraded_and_update"); -}, -async openExternalLink(url: string) : Promise { - return await TAURI_INVOKE("open_external_link", { url }); -}, -async setHotkey(action: HotkeyAction, hotkey: Hotkey | null) : Promise { - return await TAURI_INVOKE("set_hotkey", { action, hotkey }); -}, -async resetCameraPermissions() : Promise { - return await TAURI_INVOKE("reset_camera_permissions"); -}, -async resetMicrophonePermissions() : Promise { - return await TAURI_INVOKE("reset_microphone_permissions"); -}, -async isCameraWindowOpen() : Promise { - return await TAURI_INVOKE("is_camera_window_open"); -}, -async seekTo(frameNumber: number) : Promise { - return await TAURI_INVOKE("seek_to", { frameNumber }); -}, -async positionTrafficLights(controlsInset: [number, number] | null) : Promise { - await TAURI_INVOKE("position_traffic_lights", { controlsInset }); -}, -async setTheme(theme: AppTheme) : Promise { - await TAURI_INVOKE("set_theme", { theme }); -}, -async globalMessageDialog(message: string) : Promise { - await TAURI_INVOKE("global_message_dialog", { message }); -}, -async showWindow(window: ShowCapWindow) : Promise { - return await TAURI_INVOKE("show_window", { window }); -}, -async writeClipboardString(text: string) : Promise { - return await TAURI_INVOKE("write_clipboard_string", { text }); -}, -async performHapticFeedback(pattern: HapticPattern | null, time: HapticPerformanceTime | null) : Promise { - return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); -}, -async listFails() : Promise<{ [key in string]: boolean }> { - return await TAURI_INVOKE("list_fails"); -}, -async setFail(name: string, value: boolean) : Promise { - await TAURI_INVOKE("set_fail", { name, value }); -}, -async updateAuthPlan() : Promise { - await TAURI_INVOKE("update_auth_plan"); -}, -async setWindowTransparent(value: boolean) : Promise { - await TAURI_INVOKE("set_window_transparent", { value }); -}, -async getEditorMeta() : Promise { - return await TAURI_INVOKE("get_editor_meta"); -}, -async setPrettyName(prettyName: string) : Promise { - return await TAURI_INVOKE("set_pretty_name", { prettyName }); -}, -async setServerUrl(serverUrl: string) : Promise { - return await TAURI_INVOKE("set_server_url", { serverUrl }); -}, -async setCameraPreviewState(state: CameraWindowState) : Promise { - return await TAURI_INVOKE("set_camera_preview_state", { state }); -}, -async awaitCameraPreviewReady() : Promise { - return await TAURI_INVOKE("await_camera_preview_ready"); -}, -/** - * Function to handle creating directories for the model - */ -async createDir(path: string, recursive: boolean) : Promise { - return await TAURI_INVOKE("create_dir", { path, recursive }); -}, -/** - * Function to save the model file - */ -async saveModelFile(path: string, data: number[]) : Promise { - return await TAURI_INVOKE("save_model_file", { path, data }); -}, -/** - * Function to transcribe audio from a video file using Whisper - */ -async transcribeAudio(videoPath: string, modelPath: string, language: string) : Promise { - return await TAURI_INVOKE("transcribe_audio", { videoPath, modelPath, language }); -}, -/** - * Function to save caption data to a file - */ -async saveCaptions(videoId: string, captions: CaptionData) : Promise { - return await TAURI_INVOKE("save_captions", { videoId, captions }); -}, -/** - * Function to load caption data from a file - */ -async loadCaptions(videoId: string) : Promise { - return await TAURI_INVOKE("load_captions", { videoId }); -}, -/** - * Helper function to download a Whisper model from Hugging Face Hub - */ -async downloadWhisperModel(modelName: string, outputPath: string) : Promise { - return await TAURI_INVOKE("download_whisper_model", { modelName, outputPath }); -}, -/** - * Function to check if a model file exists - */ -async checkModelExists(modelPath: string) : Promise { - return await TAURI_INVOKE("check_model_exists", { modelPath }); -}, -/** - * Function to delete a downloaded model - */ -async deleteWhisperModel(modelPath: string) : Promise { - return await TAURI_INVOKE("delete_whisper_model", { modelPath }); -}, -/** - * Export captions to an SRT file - */ -async exportCaptionsSrt(videoId: string) : Promise { - return await TAURI_INVOKE("export_captions_srt", { videoId }); -}, -async openTargetSelectOverlays() : Promise { - return await TAURI_INVOKE("open_target_select_overlays"); -}, -async closeTargetSelectOverlays() : Promise { - return await TAURI_INVOKE("close_target_select_overlays"); -} -} + async setMicInput(label: string | null): Promise { + return await TAURI_INVOKE("set_mic_input", { label }); + }, + async setCameraInput(id: DeviceOrModelID | null): Promise { + return await TAURI_INVOKE("set_camera_input", { id }); + }, + async startRecording(inputs: StartRecordingInputs): Promise { + return await TAURI_INVOKE("start_recording", { inputs }); + }, + async stopRecording(): Promise { + return await TAURI_INVOKE("stop_recording"); + }, + async pauseRecording(): Promise { + return await TAURI_INVOKE("pause_recording"); + }, + async resumeRecording(): Promise { + return await TAURI_INVOKE("resume_recording"); + }, + async restartRecording(): Promise { + return await TAURI_INVOKE("restart_recording"); + }, + async deleteRecording(): Promise { + return await TAURI_INVOKE("delete_recording"); + }, + async listCameras(): Promise { + return await TAURI_INVOKE("list_cameras"); + }, + async listCaptureWindows(): Promise { + return await TAURI_INVOKE("list_capture_windows"); + }, + async listCaptureDisplays(): Promise { + return await TAURI_INVOKE("list_capture_displays"); + }, + async takeScreenshot(): Promise { + return await TAURI_INVOKE("take_screenshot"); + }, + async listAudioDevices(): Promise { + return await TAURI_INVOKE("list_audio_devices"); + }, + async closeRecordingsOverlayWindow(): Promise { + await TAURI_INVOKE("close_recordings_overlay_window"); + }, + async setFakeWindowBounds( + name: string, + bounds: LogicalBounds, + ): Promise { + return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); + }, + async removeFakeWindow(name: string): Promise { + return await TAURI_INVOKE("remove_fake_window", { name }); + }, + async focusCapturesPanel(): Promise { + await TAURI_INVOKE("focus_captures_panel"); + }, + async getCurrentRecording(): Promise> { + return await TAURI_INVOKE("get_current_recording"); + }, + async exportVideo( + projectPath: string, + progress: TAURI_CHANNEL, + settings: ExportSettings, + ): Promise { + return await TAURI_INVOKE("export_video", { + projectPath, + progress, + settings, + }); + }, + async getExportEstimates( + path: string, + resolution: XY, + fps: number, + ): Promise { + return await TAURI_INVOKE("get_export_estimates", { + path, + resolution, + fps, + }); + }, + async copyFileToPath(src: string, dst: string): Promise { + return await TAURI_INVOKE("copy_file_to_path", { src, dst }); + }, + async copyVideoToClipboard(path: string): Promise { + return await TAURI_INVOKE("copy_video_to_clipboard", { path }); + }, + async copyScreenshotToClipboard(path: string): Promise { + return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); + }, + async openFilePath(path: string): Promise { + return await TAURI_INVOKE("open_file_path", { path }); + }, + async getVideoMetadata(path: string): Promise { + return await TAURI_INVOKE("get_video_metadata", { path }); + }, + async createEditorInstance(): Promise { + return await TAURI_INVOKE("create_editor_instance"); + }, + async getMicWaveforms(): Promise { + return await TAURI_INVOKE("get_mic_waveforms"); + }, + async getSystemAudioWaveforms(): Promise { + return await TAURI_INVOKE("get_system_audio_waveforms"); + }, + async startPlayback(fps: number, resolutionBase: XY): Promise { + return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); + }, + async stopPlayback(): Promise { + return await TAURI_INVOKE("stop_playback"); + }, + async setPlayheadPosition(frameNumber: number): Promise { + return await TAURI_INVOKE("set_playhead_position", { frameNumber }); + }, + async setProjectConfig(config: ProjectConfiguration): Promise { + return await TAURI_INVOKE("set_project_config", { config }); + }, + async generateZoomSegmentsFromClicks(): Promise { + return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); + }, + async openPermissionSettings(permission: OSPermission): Promise { + await TAURI_INVOKE("open_permission_settings", { permission }); + }, + async doPermissionsCheck(initialCheck: boolean): Promise { + return await TAURI_INVOKE("do_permissions_check", { initialCheck }); + }, + async requestPermission(permission: OSPermission): Promise { + await TAURI_INVOKE("request_permission", { permission }); + }, + async uploadExportedVideo( + path: string, + mode: UploadMode, + ): Promise { + return await TAURI_INVOKE("upload_exported_video", { path, mode }); + }, + async uploadScreenshot(screenshotPath: string): Promise { + return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); + }, + async getRecordingMeta( + path: string, + fileType: FileType, + ): Promise { + return await TAURI_INVOKE("get_recording_meta", { path, fileType }); + }, + async saveFileDialog( + fileName: string, + fileType: string, + ): Promise { + return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); + }, + async listRecordings(): Promise<[string, RecordingMetaWithType][]> { + return await TAURI_INVOKE("list_recordings"); + }, + async listScreenshots(): Promise<[string, RecordingMeta][]> { + return await TAURI_INVOKE("list_screenshots"); + }, + async checkUpgradedAndUpdate(): Promise { + return await TAURI_INVOKE("check_upgraded_and_update"); + }, + async openExternalLink(url: string): Promise { + return await TAURI_INVOKE("open_external_link", { url }); + }, + async setHotkey(action: HotkeyAction, hotkey: Hotkey | null): Promise { + return await TAURI_INVOKE("set_hotkey", { action, hotkey }); + }, + async resetCameraPermissions(): Promise { + return await TAURI_INVOKE("reset_camera_permissions"); + }, + async resetMicrophonePermissions(): Promise { + return await TAURI_INVOKE("reset_microphone_permissions"); + }, + async isCameraWindowOpen(): Promise { + return await TAURI_INVOKE("is_camera_window_open"); + }, + async seekTo(frameNumber: number): Promise { + return await TAURI_INVOKE("seek_to", { frameNumber }); + }, + async positionTrafficLights( + controlsInset: [number, number] | null, + ): Promise { + await TAURI_INVOKE("position_traffic_lights", { controlsInset }); + }, + async setTheme(theme: AppTheme): Promise { + await TAURI_INVOKE("set_theme", { theme }); + }, + async globalMessageDialog(message: string): Promise { + await TAURI_INVOKE("global_message_dialog", { message }); + }, + async showWindow(window: ShowCapWindow): Promise { + return await TAURI_INVOKE("show_window", { window }); + }, + async writeClipboardString(text: string): Promise { + return await TAURI_INVOKE("write_clipboard_string", { text }); + }, + async performHapticFeedback( + pattern: HapticPattern | null, + time: HapticPerformanceTime | null, + ): Promise { + return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); + }, + async listFails(): Promise<{ [key in string]: boolean }> { + return await TAURI_INVOKE("list_fails"); + }, + async setFail(name: string, value: boolean): Promise { + await TAURI_INVOKE("set_fail", { name, value }); + }, + async updateAuthPlan(): Promise { + await TAURI_INVOKE("update_auth_plan"); + }, + async setWindowTransparent(value: boolean): Promise { + await TAURI_INVOKE("set_window_transparent", { value }); + }, + async getEditorMeta(): Promise { + return await TAURI_INVOKE("get_editor_meta"); + }, + async setPrettyName(prettyName: string): Promise { + return await TAURI_INVOKE("set_pretty_name", { prettyName }); + }, + async setServerUrl(serverUrl: string): Promise { + return await TAURI_INVOKE("set_server_url", { serverUrl }); + }, + async setCameraPreviewState(state: CameraWindowState): Promise { + return await TAURI_INVOKE("set_camera_preview_state", { state }); + }, + async awaitCameraPreviewReady(): Promise { + return await TAURI_INVOKE("await_camera_preview_ready"); + }, + /** + * Function to handle creating directories for the model + */ + async createDir(path: string, recursive: boolean): Promise { + return await TAURI_INVOKE("create_dir", { path, recursive }); + }, + /** + * Function to save the model file + */ + async saveModelFile(path: string, data: number[]): Promise { + return await TAURI_INVOKE("save_model_file", { path, data }); + }, + /** + * Function to transcribe audio from a video file using Whisper + */ + async transcribeAudio( + videoPath: string, + modelPath: string, + language: string, + ): Promise { + return await TAURI_INVOKE("transcribe_audio", { + videoPath, + modelPath, + language, + }); + }, + /** + * Function to save caption data to a file + */ + async saveCaptions(videoId: string, captions: CaptionData): Promise { + return await TAURI_INVOKE("save_captions", { videoId, captions }); + }, + /** + * Function to load caption data from a file + */ + async loadCaptions(videoId: string): Promise { + return await TAURI_INVOKE("load_captions", { videoId }); + }, + /** + * Helper function to download a Whisper model from Hugging Face Hub + */ + async downloadWhisperModel( + modelName: string, + outputPath: string, + ): Promise { + return await TAURI_INVOKE("download_whisper_model", { + modelName, + outputPath, + }); + }, + /** + * Function to check if a model file exists + */ + async checkModelExists(modelPath: string): Promise { + return await TAURI_INVOKE("check_model_exists", { modelPath }); + }, + /** + * Function to delete a downloaded model + */ + async deleteWhisperModel(modelPath: string): Promise { + return await TAURI_INVOKE("delete_whisper_model", { modelPath }); + }, + /** + * Export captions to an SRT file + */ + async exportCaptionsSrt(videoId: string): Promise { + return await TAURI_INVOKE("export_captions_srt", { videoId }); + }, + async openTargetSelectOverlays(): Promise { + return await TAURI_INVOKE("open_target_select_overlays"); + }, + async closeTargetSelectOverlays(): Promise { + return await TAURI_INVOKE("close_target_select_overlays"); + }, +}; /** user-defined events **/ - export const events = __makeEvents__<{ -audioInputLevelChange: AudioInputLevelChange, -authenticationInvalid: AuthenticationInvalid, -currentRecordingChanged: CurrentRecordingChanged, -downloadProgress: DownloadProgress, -editorStateChanged: EditorStateChanged, -newNotification: NewNotification, -newScreenshotAdded: NewScreenshotAdded, -newStudioRecordingAdded: NewStudioRecordingAdded, -onEscapePress: OnEscapePress, -recordingDeleted: RecordingDeleted, -recordingEvent: RecordingEvent, -recordingOptionsChanged: RecordingOptionsChanged, -recordingStarted: RecordingStarted, -recordingStopped: RecordingStopped, -renderFrameEvent: RenderFrameEvent, -requestNewScreenshot: RequestNewScreenshot, -requestOpenSettings: RequestOpenSettings, -requestStartRecording: RequestStartRecording, -targetUnderCursor: TargetUnderCursor, -uploadProgress: UploadProgress + audioInputLevelChange: AudioInputLevelChange; + authenticationInvalid: AuthenticationInvalid; + currentRecordingChanged: CurrentRecordingChanged; + downloadProgress: DownloadProgress; + editorStateChanged: EditorStateChanged; + newNotification: NewNotification; + newScreenshotAdded: NewScreenshotAdded; + newStudioRecordingAdded: NewStudioRecordingAdded; + onEscapePress: OnEscapePress; + recordingDeleted: RecordingDeleted; + recordingEvent: RecordingEvent; + recordingOptionsChanged: RecordingOptionsChanged; + recordingStarted: RecordingStarted; + recordingStopped: RecordingStopped; + renderFrameEvent: RenderFrameEvent; + requestNewScreenshot: RequestNewScreenshot; + requestOpenSettings: RequestOpenSettings; + requestStartRecording: RequestStartRecording; + targetUnderCursor: TargetUnderCursor; + uploadProgress: UploadProgress; }>({ -audioInputLevelChange: "audio-input-level-change", -authenticationInvalid: "authentication-invalid", -currentRecordingChanged: "current-recording-changed", -downloadProgress: "download-progress", -editorStateChanged: "editor-state-changed", -newNotification: "new-notification", -newScreenshotAdded: "new-screenshot-added", -newStudioRecordingAdded: "new-studio-recording-added", -onEscapePress: "on-escape-press", -recordingDeleted: "recording-deleted", -recordingEvent: "recording-event", -recordingOptionsChanged: "recording-options-changed", -recordingStarted: "recording-started", -recordingStopped: "recording-stopped", -renderFrameEvent: "render-frame-event", -requestNewScreenshot: "request-new-screenshot", -requestOpenSettings: "request-open-settings", -requestStartRecording: "request-start-recording", -targetUnderCursor: "target-under-cursor", -uploadProgress: "upload-progress" -}) + audioInputLevelChange: "audio-input-level-change", + authenticationInvalid: "authentication-invalid", + currentRecordingChanged: "current-recording-changed", + downloadProgress: "download-progress", + editorStateChanged: "editor-state-changed", + newNotification: "new-notification", + newScreenshotAdded: "new-screenshot-added", + newStudioRecordingAdded: "new-studio-recording-added", + onEscapePress: "on-escape-press", + recordingDeleted: "recording-deleted", + recordingEvent: "recording-event", + recordingOptionsChanged: "recording-options-changed", + recordingStarted: "recording-started", + recordingStopped: "recording-stopped", + renderFrameEvent: "render-frame-event", + requestNewScreenshot: "request-new-screenshot", + requestOpenSettings: "request-open-settings", + requestStartRecording: "request-start-recording", + targetUnderCursor: "target-under-cursor", + uploadProgress: "upload-progress", +}); /** user-defined constants **/ - - /** user-defined types **/ -export type AppTheme = "system" | "light" | "dark" -export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall" -export type Audio = { duration: number; sample_rate: number; channels: number; start_time: number } -export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb?: number; micStereoMode?: StereoMode; systemVolumeDb?: number } -export type AudioInputLevelChange = number -export type AudioMeta = { path: string; -/** - * unix time of the first frame - */ -start_time?: number | null } -export type AuthSecret = { api_key: string } | { token: string; expires: number } -export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; intercom_hash: string | null } -export type AuthenticationInvalid = null -export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; inset: number; crop: Crop | null; shadow?: number; advancedShadow?: ShadowConfiguration | null } -export type BackgroundSource = { type: "wallpaper"; path: string | null } | { type: "image"; path: string | null } | { type: "color"; value: [number, number, number] } | { type: "gradient"; from: [number, number, number]; to: [number, number, number]; angle?: number } -export type Camera = { hide: boolean; mirror: boolean; position: CameraPosition; size: number; zoom_size: number | null; rounding?: number; shadow?: number; advanced_shadow?: ShadowConfiguration | null; shape?: CameraShape } -export type CameraInfo = { device_id: string; model_id: ModelIDType | null; display_name: string } -export type CameraPosition = { x: CameraXPosition; y: CameraYPosition } -export type CameraPreviewShape = "round" | "square" | "full" -export type CameraPreviewSize = "sm" | "lg" -export type CameraShape = "square" | "source" -export type CameraWindowState = { size: CameraPreviewSize; shape: CameraPreviewShape; mirrored: boolean } -export type CameraXPosition = "left" | "center" | "right" -export type CameraYPosition = "top" | "bottom" -export type CaptionData = { segments: CaptionSegment[]; settings: CaptionSettings | null } -export type CaptionSegment = { id: string; start: number; end: number; text: string } -export type CaptionSettings = { enabled: boolean; font: string; size: number; color: string; backgroundColor: string; backgroundOpacity: number; position: string; bold: boolean; italic: boolean; outline: boolean; outlineColor: string; exportWithSubtitles: boolean } -export type CaptionsData = { segments: CaptionSegment[]; settings: CaptionSettings } -export type CaptureDisplay = { id: DisplayId; name: string; refresh_rate: number } -export type CaptureWindow = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number } -export type CommercialLicense = { licenseKey: string; expiryDate: number | null; refresh: number; activatedOn: number } -export type Crop = { position: XY; size: XY } -export type CurrentRecording = { target: CurrentRecordingTarget; type: RecordingType } -export type CurrentRecordingChanged = null -export type CurrentRecordingTarget = { window: { id: WindowId; bounds: LogicalBounds } } | { screen: { id: DisplayId } } | { area: { screen: DisplayId; bounds: LogicalBounds } } -export type CursorAnimationStyle = "regular" | "slow" | "fast" -export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean; motionBlur?: number; useSvg?: boolean } -export type CursorMeta = { imagePath: string; hotspot: XY; shape?: string | null } -export type CursorType = "pointer" | "circle" -export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta } -export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType } -export type DisplayId = string -export type DownloadProgress = { progress: number; message: string } -export type EditorStateChanged = { playhead_position: number } -export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato" -export type ExportEstimates = { duration_seconds: number; estimated_time_seconds: number; estimated_size_mb: number } -export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format: "Gif" } & GifExportSettings) -export type FileType = "recording" | "screenshot" -export type Flags = { captions: boolean } -export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" } -export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; customCursorCapture?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour } -export type GifExportSettings = { fps: number; resolution_base: XY; quality: GifQuality | null } -export type GifQuality = { -/** - * Encoding quality from 1-100 (default: 90) - */ -quality: number | null; -/** - * Whether to prioritize speed over quality (default: false) - */ -fast: boolean | null } -export type HapticPattern = "Alignment" | "LevelChange" | "Generic" -export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted" -export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } -export type HotkeyAction = "startRecording" | "stopRecording" | "restartRecording" -export type HotkeysConfiguration = { show: boolean } -export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } } -export type InstantRecordingMeta = { fps: number; sample_rate: number | null } -export type JsonValue = [T] -export type LogicalBounds = { position: LogicalPosition; size: LogicalSize } -export type LogicalPosition = { x: number; y: number } -export type LogicalSize = { width: number; height: number } -export type MainWindowRecordingStartBehaviour = "close" | "minimise" -export type ModelIDType = string -export type Mp4ExportSettings = { fps: number; resolution_base: XY; compression: ExportCompression } -export type MultipleSegment = { display: VideoMeta; camera?: VideoMeta | null; mic?: AudioMeta | null; system_audio?: AudioMeta | null; cursor?: string | null } -export type MultipleSegments = { segments: MultipleSegment[]; cursors: Cursors } -export type NewNotification = { title: string; body: string; is_error: boolean } -export type NewScreenshotAdded = { path: string } -export type NewStudioRecordingAdded = { path: string } -export type OSPermission = "screenRecording" | "camera" | "microphone" | "accessibility" -export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied" -export type OSPermissionsCheck = { screenRecording: OSPermissionStatus; microphone: OSPermissionStatus; camera: OSPermissionStatus; accessibility: OSPermissionStatus } -export type OnEscapePress = null -export type PhysicalSize = { width: number; height: number } -export type Plan = { upgraded: boolean; manual: boolean; last_checked: number } -export type Platform = "MacOS" | "Windows" -export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow" -export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay" -export type Preset = { name: string; config: ProjectConfiguration } -export type PresetsStore = { presets: Preset[]; default: number | null } -export type ProjectConfiguration = { aspectRatio: AspectRatio | null; background: BackgroundConfiguration; camera: Camera; audio: AudioConfiguration; cursor: CursorConfiguration; hotkeys: HotkeysConfiguration; timeline?: TimelineConfiguration | null; captions?: CaptionsData | null } -export type ProjectRecordingsMeta = { segments: SegmentRecordings[] } -export type RecordingDeleted = { path: string } -export type RecordingEvent = { variant: "Countdown"; value: number } | { variant: "Started" } | { variant: "Stopped" } | { variant: "Failed"; error: string } -export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null } -export type RecordingMetaWithType = ((StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null }) & { type: RecordingType } -export type RecordingMode = "studio" | "instant" -export type RecordingOptionsChanged = null -export type RecordingStarted = null -export type RecordingStopped = null -export type RecordingType = "studio" | "instant" -export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY } -export type RequestNewScreenshot = null -export type RequestOpenSettings = { page: string } -export type RequestStartRecording = null -export type S3UploadMeta = { id: string } -export type ScreenCaptureTarget = { variant: "window"; id: WindowId } | { variant: "screen"; id: DisplayId } | { variant: "area"; screen: DisplayId; bounds: LogicalBounds } -export type ScreenUnderCursor = { name: string; physical_size: PhysicalSize; refresh_rate: string } -export type SegmentRecordings = { display: Video; camera: Video | null; mic: Audio | null; system_audio: Audio | null } -export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordingsMeta; path: string } -export type ShadowConfiguration = { size: number; opacity: number; blur: number } -export type SharingMeta = { id: string; link: string } -export type ShowCapWindow = "Setup" | "Main" | { Settings: { page: string | null } } | { Editor: { project_path: string } } | "RecordingsOverlay" | { WindowCaptureOccluder: { screen_id: DisplayId } } | { TargetSelectOverlay: { display_id: DisplayId } } | { CaptureArea: { screen_id: DisplayId } } | "Camera" | { InProgressRecording: { countdown: number | null } } | "Upgrade" | "ModeSelect" -export type SingleSegment = { display: VideoMeta; camera?: VideoMeta | null; audio?: AudioMeta | null; cursor?: string | null } -export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; capture_system_audio?: boolean; mode: RecordingMode } -export type StereoMode = "stereo" | "monoL" | "monoR" -export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } -export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null; screen: ScreenUnderCursor | null } -export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[] } -export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } -export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" -export type UploadProgress = { progress: number } -export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired" -export type Video = { duration: number; width: number; height: number; fps: number; start_time: number } -export type VideoMeta = { path: string; fps?: number; -/** - * unix time of the first frame - */ -start_time?: number | null } -export type VideoRecordingMetadata = { duration: number; size: number } -export type VideoUploadInfo = { id: string; link: string; config: S3UploadMeta } -export type WindowId = string -export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds; icon: string | null } -export type XY = { x: T; y: T } -export type ZoomMode = "auto" | { manual: { x: number; y: number } } -export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode } +export type AppTheme = "system" | "light" | "dark"; +export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"; +export type Audio = { + duration: number; + sample_rate: number; + channels: number; + start_time: number; +}; +export type AudioConfiguration = { + mute: boolean; + improve: boolean; + micVolumeDb?: number; + micStereoMode?: StereoMode; + systemVolumeDb?: number; +}; +export type AudioInputLevelChange = number; +export type AudioMeta = { + path: string; + /** + * unix time of the first frame + */ + start_time?: number | null; +}; +export type AuthSecret = + | { api_key: string } + | { token: string; expires: number }; +export type AuthStore = { + secret: AuthSecret; + user_id: string | null; + plan: Plan | null; + intercom_hash: string | null; +}; +export type AuthenticationInvalid = null; +export type BackgroundConfiguration = { + source: BackgroundSource; + blur: number; + padding: number; + rounding: number; + inset: number; + crop: Crop | null; + shadow?: number; + advancedShadow?: ShadowConfiguration | null; +}; +export type BackgroundSource = + | { type: "wallpaper"; path: string | null } + | { type: "image"; path: string | null } + | { type: "color"; value: [number, number, number] } + | { + type: "gradient"; + from: [number, number, number]; + to: [number, number, number]; + angle?: number; + }; +export type Camera = { + hide: boolean; + mirror: boolean; + position: CameraPosition; + size: number; + zoom_size: number | null; + rounding?: number; + shadow?: number; + advanced_shadow?: ShadowConfiguration | null; + shape?: CameraShape; +}; +export type CameraInfo = { + device_id: string; + model_id: ModelIDType | null; + display_name: string; +}; +export type CameraPosition = { x: CameraXPosition; y: CameraYPosition }; +export type CameraPreviewShape = "round" | "square" | "full"; +export type CameraPreviewSize = "sm" | "lg"; +export type CameraShape = "square" | "source"; +export type CameraWindowState = { + size: CameraPreviewSize; + shape: CameraPreviewShape; + mirrored: boolean; +}; +export type CameraXPosition = "left" | "center" | "right"; +export type CameraYPosition = "top" | "bottom"; +export type CaptionData = { + segments: CaptionSegment[]; + settings: CaptionSettings | null; +}; +export type CaptionSegment = { + id: string; + start: number; + end: number; + text: string; +}; +export type CaptionSettings = { + enabled: boolean; + font: string; + size: number; + color: string; + backgroundColor: string; + backgroundOpacity: number; + position: string; + bold: boolean; + italic: boolean; + outline: boolean; + outlineColor: string; + exportWithSubtitles: boolean; +}; +export type CaptionsData = { + segments: CaptionSegment[]; + settings: CaptionSettings; +}; +export type CaptureDisplay = { + id: DisplayId; + name: string; + refresh_rate: number; +}; +export type CaptureWindow = { + id: WindowId; + owner_name: string; + name: string; + bounds: LogicalBounds; + refresh_rate: number; +}; +export type CommercialLicense = { + licenseKey: string; + expiryDate: number | null; + refresh: number; + activatedOn: number; +}; +export type Crop = { position: XY; size: XY }; +export type CurrentRecording = { + target: CurrentRecordingTarget; + type: RecordingType; +}; +export type CurrentRecordingChanged = null; +export type CurrentRecordingTarget = + | { window: { id: WindowId; bounds: LogicalBounds } } + | { screen: { id: DisplayId } } + | { area: { screen: DisplayId; bounds: LogicalBounds } }; +export type CursorAnimationStyle = "regular" | "slow" | "fast"; +export type CursorConfiguration = { + hide?: boolean; + hideWhenIdle: boolean; + size: number; + type: CursorType; + animationStyle: CursorAnimationStyle; + tension: number; + mass: number; + friction: number; + raw?: boolean; + motionBlur?: number; + useSvg?: boolean; +}; +export type CursorMeta = { + imagePath: string; + hotspot: XY; + shape?: string | null; +}; +export type CursorType = "pointer" | "circle"; +export type Cursors = + | { [key in string]: string } + | { [key in string]: CursorMeta }; +export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType }; +export type DisplayId = string; +export type DownloadProgress = { progress: number; message: string }; +export type EditorStateChanged = { playhead_position: number }; +export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato"; +export type ExportEstimates = { + duration_seconds: number; + estimated_time_seconds: number; + estimated_size_mb: number; +}; +export type ExportSettings = + | ({ format: "Mp4" } & Mp4ExportSettings) + | ({ format: "Gif" } & GifExportSettings); +export type FileType = "recording" | "screenshot"; +export type Flags = { captions: boolean }; +export type FramesRendered = { + renderedCount: number; + totalFrames: number; + type: "FramesRendered"; +}; +export type GeneralSettingsStore = { + instanceId?: string; + uploadIndividualFiles?: boolean; + hideDockIcon?: boolean; + hapticsEnabled?: boolean; + autoCreateShareableLink?: boolean; + enableNotifications?: boolean; + disableAutoOpenLinks?: boolean; + hasCompletedStartup?: boolean; + theme?: AppTheme; + commercialLicense?: CommercialLicense | null; + lastVersion?: string | null; + windowTransparency?: boolean; + postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; + mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; + customCursorCapture?: boolean; + serverUrl?: string; + recordingCountdown?: number | null; + enableNativeCameraPreview: boolean; + autoZoomOnClicks?: boolean; + enableNewRecordingFlow: boolean; + postDeletionBehaviour?: PostDeletionBehaviour; +}; +export type GifExportSettings = { + fps: number; + resolution_base: XY; + quality: GifQuality | null; +}; +export type GifQuality = { + /** + * Encoding quality from 1-100 (default: 90) + */ + quality: number | null; + /** + * Whether to prioritize speed over quality (default: false) + */ + fast: boolean | null; +}; +export type HapticPattern = "Alignment" | "LevelChange" | "Generic"; +export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted"; +export type Hotkey = { + code: string; + meta: boolean; + ctrl: boolean; + alt: boolean; + shift: boolean; +}; +export type HotkeyAction = + | "startRecording" + | "stopRecording" + | "restartRecording"; +export type HotkeysConfiguration = { show: boolean }; +export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }; +export type InstantRecordingMeta = { fps: number; sample_rate: number | null }; +export type JsonValue = [T]; +export type LogicalBounds = { position: LogicalPosition; size: LogicalSize }; +export type LogicalPosition = { x: number; y: number }; +export type LogicalSize = { width: number; height: number }; +export type MainWindowRecordingStartBehaviour = "close" | "minimise"; +export type ModelIDType = string; +export type Mp4ExportSettings = { + fps: number; + resolution_base: XY; + compression: ExportCompression; +}; +export type MultipleSegment = { + display: VideoMeta; + camera?: VideoMeta | null; + mic?: AudioMeta | null; + system_audio?: AudioMeta | null; + cursor?: string | null; +}; +export type MultipleSegments = { + segments: MultipleSegment[]; + cursors: Cursors; +}; +export type NewNotification = { + title: string; + body: string; + is_error: boolean; +}; +export type NewScreenshotAdded = { path: string }; +export type NewStudioRecordingAdded = { path: string }; +export type OSPermission = + | "screenRecording" + | "camera" + | "microphone" + | "accessibility"; +export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied"; +export type OSPermissionsCheck = { + screenRecording: OSPermissionStatus; + microphone: OSPermissionStatus; + camera: OSPermissionStatus; + accessibility: OSPermissionStatus; +}; +export type OnEscapePress = null; +export type PhysicalSize = { width: number; height: number }; +export type Plan = { upgraded: boolean; manual: boolean; last_checked: number }; +export type Platform = "MacOS" | "Windows"; +export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow"; +export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay"; +export type Preset = { name: string; config: ProjectConfiguration }; +export type PresetsStore = { presets: Preset[]; default: number | null }; +export type ProjectConfiguration = { + aspectRatio: AspectRatio | null; + background: BackgroundConfiguration; + camera: Camera; + audio: AudioConfiguration; + cursor: CursorConfiguration; + hotkeys: HotkeysConfiguration; + timeline?: TimelineConfiguration | null; + captions?: CaptionsData | null; +}; +export type ProjectRecordingsMeta = { segments: SegmentRecordings[] }; +export type RecordingDeleted = { path: string }; +export type RecordingEvent = + | { variant: "Countdown"; value: number } + | { variant: "Started" } + | { variant: "Stopped" } + | { variant: "Failed"; error: string }; +export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { + platform?: Platform | null; + pretty_name: string; + sharing?: SharingMeta | null; +}; +export type RecordingMetaWithType = (( + | StudioRecordingMeta + | InstantRecordingMeta +) & { + platform?: Platform | null; + pretty_name: string; + sharing?: SharingMeta | null; +}) & { type: RecordingType }; +export type RecordingMode = "studio" | "instant"; +export type RecordingOptionsChanged = null; +export type RecordingStarted = null; +export type RecordingStopped = null; +export type RecordingType = "studio" | "instant"; +export type RenderFrameEvent = { + frame_number: number; + fps: number; + resolution_base: XY; +}; +export type RequestNewScreenshot = null; +export type RequestOpenSettings = { page: string }; +export type RequestStartRecording = null; +export type S3UploadMeta = { id: string }; +export type ScreenCaptureTarget = + | { variant: "window"; id: WindowId } + | { variant: "screen"; id: DisplayId } + | { variant: "area"; screen: DisplayId; bounds: LogicalBounds }; +export type ScreenUnderCursor = { + name: string; + physical_size: PhysicalSize; + refresh_rate: string; +}; +export type SegmentRecordings = { + display: Video; + camera: Video | null; + mic: Audio | null; + system_audio: Audio | null; +}; +export type SerializedEditorInstance = { + framesSocketUrl: string; + recordingDuration: number; + savedProjectConfig: ProjectConfiguration; + recordings: ProjectRecordingsMeta; + path: string; +}; +export type ShadowConfiguration = { + size: number; + opacity: number; + blur: number; +}; +export type SharingMeta = { id: string; link: string }; +export type ShowCapWindow = + | "Setup" + | "Main" + | { Settings: { page: string | null } } + | { Editor: { project_path: string } } + | "RecordingsOverlay" + | { WindowCaptureOccluder: { screen_id: DisplayId } } + | { TargetSelectOverlay: { display_id: DisplayId } } + | { CaptureArea: { screen_id: DisplayId } } + | "Camera" + | { InProgressRecording: { countdown: number | null } } + | "Upgrade" + | "ModeSelect"; +export type SingleSegment = { + display: VideoMeta; + camera?: VideoMeta | null; + audio?: AudioMeta | null; + cursor?: string | null; +}; +export type StartRecordingInputs = { + capture_target: ScreenCaptureTarget; + capture_system_audio?: boolean; + mode: RecordingMode; +}; +export type StereoMode = "stereo" | "monoL" | "monoR"; +export type StudioRecordingMeta = + | { segment: SingleSegment } + | { inner: MultipleSegments }; +export type TargetUnderCursor = { + display_id: DisplayId | null; + window: WindowUnderCursor | null; + screen: ScreenUnderCursor | null; +}; +export type TimelineConfiguration = { + segments: TimelineSegment[]; + zoomSegments: ZoomSegment[]; +}; +export type TimelineSegment = { + recordingSegment?: number; + timescale: number; + start: number; + end: number; +}; +export type UploadMode = + | { Initial: { pre_created_video: VideoUploadInfo | null } } + | "Reupload"; +export type UploadProgress = { progress: number }; +export type UploadResult = + | { Success: string } + | "NotAuthenticated" + | "PlanCheckFailed" + | "UpgradeRequired"; +export type Video = { + duration: number; + width: number; + height: number; + fps: number; + start_time: number; +}; +export type VideoMeta = { + path: string; + fps?: number; + /** + * unix time of the first frame + */ + start_time?: number | null; +}; +export type VideoRecordingMetadata = { duration: number; size: number }; +export type VideoUploadInfo = { + id: string; + link: string; + config: S3UploadMeta; +}; +export type WindowId = string; +export type WindowUnderCursor = { + id: WindowId; + app_name: string; + bounds: LogicalBounds; + icon: string | null; +}; +export type XY = { x: T; y: T }; +export type ZoomMode = "auto" | { manual: { x: number; y: number } }; +export type ZoomSegment = { + start: number; + end: number; + amount: number; + mode: ZoomMode; +}; /** tauri-specta globals **/ import { + type Channel as TAURI_CHANNEL, invoke as TAURI_INVOKE, - Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; +import type { WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; type __EventObj__ = { listen: ( @@ -480,9 +827,8 @@ function __makeEvents__>( ) { return new Proxy( {} as unknown as { - [K in keyof T]: __EventObj__ & { - (handle: __WebviewWindow__): __EventObj__; - }; + [K in keyof T]: __EventObj__ & + ((handle: __WebviewWindow__) => __EventObj__); }, { get: (_, event) => { diff --git a/crates/cursor-capture/src/position.rs b/crates/cursor-capture/src/position.rs index 03067cbc1b..8d9f75a92d 100644 --- a/crates/cursor-capture/src/position.rs +++ b/crates/cursor-capture/src/position.rs @@ -187,8 +187,6 @@ impl NormalizedCursorPosition { } pub fn with_crop(&self, crop: CursorCropBounds) -> Self { - dbg!(self.x, self.y, self.crop, crop); - let raw_px = ( self.x * self.crop.width + self.crop.x, self.y * self.crop.height + self.crop.y, diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index bbaaa98fd8..d864e07055 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -25,14 +25,14 @@ pub async fn main() { println!("Recording to directory '{}'", dir.path().display()); - dbg!( - list_windows() - .into_iter() - .map(|(v, _)| v) - .collect::>() - ); - - return; + // dbg!( + // list_windows() + // .into_iter() + // .map(|(v, _)| v) + // .collect::>() + // ); + + // return; let (handle, _ready_rx) = cap_recording::spawn_studio_recording_actor( "test".to_string(), diff --git a/crates/recording/src/sources/screen_capture.rs b/crates/recording/src/sources/screen_capture.rs index 295bdcbdd9..9b3aadc436 100644 --- a/crates/recording/src/sources/screen_capture.rs +++ b/crates/recording/src/sources/screen_capture.rs @@ -1046,6 +1046,7 @@ mod macos { let config = self.config.clone(); self.tokio_handle.block_on(async move { + let captures_audio = audio_tx.is_some(); let frame_handler = FrameHandler::spawn(FrameHandler { video_tx, audio_tx, @@ -1082,6 +1083,7 @@ mod macos { .with_height(size.height() as usize) .with_fps(config.fps as f32) .with_shows_cursor(config.show_cursor) + .with_captures_audio(captures_audio) .build(); settings.set_pixel_format(cv::PixelFormat::_32_BGRA); diff --git a/crates/scap-screencapturekit/src/capture.rs b/crates/scap-screencapturekit/src/capture.rs index 0c171319db..18a1a0bb62 100644 --- a/crates/scap-screencapturekit/src/capture.rs +++ b/crates/scap-screencapturekit/src/capture.rs @@ -2,7 +2,6 @@ use cidre::{ arc, cm, cv, define_obj_type, dispatch, ns, objc, sc::{self, StreamDelegate, StreamDelegateImpl, StreamOutput, StreamOutputImpl}, }; -use tracing::warn; define_obj_type!( pub CapturerCallbacks + StreamOutputImpl + StreamDelegateImpl, CapturerCallbacksInner, CAPTURER diff --git a/crates/scap-screencapturekit/src/config.rs b/crates/scap-screencapturekit/src/config.rs index aaebd3bca5..5787959c70 100644 --- a/crates/scap-screencapturekit/src/config.rs +++ b/crates/scap-screencapturekit/src/config.rs @@ -31,6 +31,10 @@ impl StreamCfgBuilder { }); } + pub fn set_captures_audio(&mut self, captures_audio: bool) { + self.0.set_captures_audio(captures_audio); + } + /// Logical width of the capture area pub fn with_width(mut self, width: usize) -> Self { self.set_width(width); @@ -54,6 +58,11 @@ impl StreamCfgBuilder { self } + pub fn with_captures_audio(mut self, captures_audio: bool) -> Self { + self.set_captures_audio(captures_audio); + self + } + pub fn with_fps(mut self, fps: f32) -> Self { self.set_fps(fps); self diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 7ab73f4c84..fc5c05c352 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -57,15 +57,19 @@ declare global { const IconLucideBug: typeof import("~icons/lucide/bug.jsx")["default"]; const IconLucideCheck: typeof import("~icons/lucide/check.jsx")["default"]; const IconLucideClock: typeof import("~icons/lucide/clock.jsx")["default"]; + const IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"]; + const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"]; const IconLucideFolder: typeof import("~icons/lucide/folder.jsx")["default"]; const IconLucideGift: typeof import("~icons/lucide/gift.jsx")["default"]; const IconLucideHardDrive: typeof import("~icons/lucide/hard-drive.jsx")["default"]; const IconLucideLoaderCircle: typeof import("~icons/lucide/loader-circle.jsx")["default"]; + const IconLucideMessageSquarePlus: typeof import("~icons/lucide/message-square-plus.jsx")["default"]; const IconLucideMicOff: typeof import("~icons/lucide/mic-off.jsx")["default"]; const IconLucideMonitor: typeof import("~icons/lucide/monitor.jsx")["default"]; const IconLucideRotateCcw: typeof import("~icons/lucide/rotate-ccw.jsx")["default"]; const IconLucideSearch: typeof import("~icons/lucide/search.jsx")["default"]; const IconLucideSquarePlay: typeof import("~icons/lucide/square-play.jsx")["default"]; + const IconLucideUnplug: typeof import("~icons/lucide/unplug.jsx")["default"]; const IconLucideVolume2: typeof import("~icons/lucide/volume2.jsx")["default"]; const IconPhMonitorBold: typeof import("~icons/ph/monitor-bold.jsx")["default"]; } From c189c816ac608800b9a2f4d39703066a2b06e20f Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 21 Aug 2025 22:49:17 +0800 Subject: [PATCH 28/47] don't drop audio capturer after starting recording -_- --- crates/recording/examples/recording-cli.rs | 15 ++++++---- .../recording/src/sources/screen_capture.rs | 29 +++++++++++++++++-- crates/scap-cpal/src/lib.rs | 19 ++++++++++-- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index d864e07055..cbbd341c40 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -38,13 +38,16 @@ pub async fn main() { "test".to_string(), dir.path().into(), RecordingBaseInputs { - capture_target: ScreenCaptureTarget::Window { - id: Window::list() - .into_iter() - .find(|w| w.owner_name().unwrap_or_default().contains("Brave")) - .unwrap() - .id(), + capture_target: ScreenCaptureTarget::Screen { + id: Display::list()[1].id(), }, + // ScreenCaptureTarget::Window { + // id: Window::list() + // .into_iter() + // .find(|w| w.owner_name().unwrap_or_default().contains("Brave")) + // .unwrap() + // .id(), + // }, capture_system_audio: true, mic_feed: &None, }, diff --git a/crates/recording/src/sources/screen_capture.rs b/crates/recording/src/sources/screen_capture.rs index 9b3aadc436..277e985549 100644 --- a/crates/recording/src/sources/screen_capture.rs +++ b/crates/recording/src/sources/screen_capture.rs @@ -715,19 +715,28 @@ mod windows { .send() .await; - if let Some(audio_tx) = audio_tx { + let audio_capture = if let Some(audio_tx) = audio_tx { let audio_capture = WindowsAudioCapture::spawn( WindowsAudioCapture::new(audio_tx, start_time).unwrap(), ); - let _ = audio_capture.ask(audio::StartCapturing).send().await; - } + let _ = dbg!(audio_capture.ask(audio::StartCapturing).send().await); + + Some(audio_capture) + } else { + None + }; let _ = ready_signal.send(Ok(())); while let Ok(msg) = control_signal.receiver.recv_async().await { if let Control::Shutdown = msg { let _ = stop_recipient.ask(StopCapturing).await; + + if let Some(audio_capture) = audio_capture { + let _ = audio_capture.ask(StopCapturing).await; + } + break; } } @@ -893,6 +902,20 @@ mod windows { Ok(()) } } + + impl Message for WindowsAudioCapture { + type Reply = Result<(), &'static str>; + + async fn handle( + &mut self, + msg: StopCapturing, + ctx: &mut Context, + ) -> Self::Reply { + self.capturer.pause().map_err(|_| "failed to stop stream")?; + + Ok(()) + } + } } } diff --git a/crates/scap-cpal/src/lib.rs b/crates/scap-cpal/src/lib.rs index 14dafcc5c1..b4b71f08fd 100644 --- a/crates/scap-cpal/src/lib.rs +++ b/crates/scap-cpal/src/lib.rs @@ -1,6 +1,6 @@ use cpal::{ - BuildStreamError, DefaultStreamConfigError, InputCallbackInfo, PlayStreamError, Stream, - StreamConfig, StreamError, traits::StreamTrait, + BuildStreamError, DefaultStreamConfigError, InputCallbackInfo, PauseStreamError, + PlayStreamError, Stream, StreamConfig, StreamError, traits::StreamTrait, }; use thiserror::Error; @@ -38,12 +38,21 @@ pub fn create_capturer( None, )?; - Ok(Capturer { stream, config }) + Ok(Capturer { + stream, + config, + _output_device: output_device, + _host: host, + _supported_config: supported_config, + }) } pub struct Capturer { stream: Stream, config: StreamConfig, + _output_device: cpal::Device, + _host: cpal::Host, + _supported_config: cpal::SupportedStreamConfig, } impl Capturer { @@ -51,6 +60,10 @@ impl Capturer { self.stream.play() } + pub fn pause(&self) -> Result<(), PauseStreamError> { + self.stream.pause() + } + pub fn config(&self) -> &StreamConfig { &self.config } From 90b62b79420e31298018eabbd5951a1dfb7a42e9 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 21 Aug 2025 23:53:20 +0800 Subject: [PATCH 29/47] move screen capture source to multiple files --- .../recording/src/sources/screen_capture.rs | 1289 ----------------- .../src/sources/screen_capture/macos.rs | 356 +++++ .../src/sources/screen_capture/mod.rs | 460 ++++++ .../src/sources/screen_capture/windows.rs | 461 ++++++ 4 files changed, 1277 insertions(+), 1289 deletions(-) delete mode 100644 crates/recording/src/sources/screen_capture.rs create mode 100644 crates/recording/src/sources/screen_capture/macos.rs create mode 100644 crates/recording/src/sources/screen_capture/mod.rs create mode 100644 crates/recording/src/sources/screen_capture/windows.rs diff --git a/crates/recording/src/sources/screen_capture.rs b/crates/recording/src/sources/screen_capture.rs deleted file mode 100644 index 277e985549..0000000000 --- a/crates/recording/src/sources/screen_capture.rs +++ /dev/null @@ -1,1289 +0,0 @@ -use cap_cursor_capture::CursorCropBounds; -use cap_displays::{ - Display, DisplayId, Window, WindowId, - bounds::{ - LogicalBounds, LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, - }, -}; -use cap_media_info::{AudioInfo, VideoInfo}; -use ffmpeg::sys::AV_TIME_BASE_Q; -use flume::Sender; -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::time::SystemTime; -use tracing::{error, info, warn}; - -use crate::pipeline::{control::Control, task::PipelineSourceTask}; - -static EXCLUDED_WINDOWS: &[&str] = &[ - "Cap Camera", - "Cap Recordings Overlay", - "Cap In Progress Recording", -]; - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub struct CaptureWindow { - pub id: WindowId, - pub owner_name: String, - pub name: String, - pub bounds: LogicalBounds, - pub refresh_rate: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub struct CaptureDisplay { - pub id: DisplayId, - pub name: String, - pub refresh_rate: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub struct CaptureArea { - pub screen: CaptureDisplay, - pub bounds: LogicalBounds, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase", tag = "variant")] -pub enum ScreenCaptureTarget { - Window { - id: WindowId, - }, - Screen { - id: DisplayId, - }, - Area { - screen: DisplayId, - bounds: LogicalBounds, - }, -} - -impl ScreenCaptureTarget { - pub fn display(&self) -> Option { - match self { - Self::Screen { id } => Display::from_id(id), - Self::Window { id } => Window::from_id(id).and_then(|w| w.display()), - Self::Area { screen, .. } => Display::from_id(screen), - } - } - - pub fn logical_bounds(&self) -> Option { - match self { - Self::Screen { id } => todo!(), // Display::from_id(id).map(|d| d.logical_bounds()), - Self::Window { id } => Some(LogicalBounds::new( - LogicalPosition::new(0.0, 0.0), - Window::from_id(id)?.raw_handle().logical_size()?, - )), - Self::Area { bounds, .. } => Some(*bounds), - } - } - - pub fn cursor_crop(&self) -> Option { - match self { - Self::Screen { .. } => { - #[cfg(target_os = "macos")] - { - let display = self.display()?; - return Some(CursorCropBounds::new_macos(LogicalBounds::new( - LogicalPosition::new(0.0, 0.0), - display.raw_handle().logical_size()?, - ))); - } - - #[cfg(windows)] - { - let display = self.display()?; - return Some(CursorCropBounds::new_windows(PhysicalBounds::new( - PhysicalPosition::new(0.0, 0.0), - display.raw_handle().physical_size()?, - ))); - } - } - Self::Window { id } => { - let window = Window::from_id(id)?; - - #[cfg(target_os = "macos")] - { - let display = self.display()?; - let display_position = display.raw_handle().logical_position(); - let window_bounds = window.raw_handle().logical_bounds()?; - - return Some(CursorCropBounds::new_macos(LogicalBounds::new( - LogicalPosition::new( - window_bounds.position().x() - display_position.x(), - window_bounds.position().y() - display_position.y(), - ), - window_bounds.size(), - ))); - } - - #[cfg(windows)] - { - let display_bounds = self.display()?.raw_handle().physical_bounds()?; - let window_bounds = window.raw_handle().physical_bounds()?; - - return Some(CursorCropBounds::new_windows(PhysicalBounds::new( - PhysicalPosition::new( - window_bounds.position().x() - display_bounds.position().x(), - window_bounds.position().y() - display_bounds.position().y(), - ), - PhysicalSize::new( - window_bounds.size().width(), - window_bounds.size().height(), - ), - ))); - } - } - Self::Area { bounds, .. } => { - #[cfg(target_os = "macos")] - { - return Some(CursorCropBounds::new_macos(*bounds)); - } - - #[cfg(windows)] - { - let display = self.display()?; - let display_bounds = display.raw_handle().physical_bounds()?; - let display_logical_size = display.logical_size()?; - - let scale = display_bounds.size().width() / display_logical_size.width(); - - return Some(CursorCropBounds::new_windows(PhysicalBounds::new( - PhysicalPosition::new( - bounds.position().x() * scale, - bounds.position().y() * scale, - ), - PhysicalSize::new( - bounds.size().width() * scale, - bounds.size().height() * scale, - ), - ))); - } - } - } - } - - pub fn physical_size(&self) -> Option { - match self { - Self::Screen { id } => Display::from_id(id).and_then(|d| d.physical_size()), - Self::Window { id } => Window::from_id(id).and_then(|w| w.physical_size()), - Self::Area { bounds, .. } => { - let display = self.display()?; - let scale = display.physical_size()?.width() / display.logical_size()?.width(); - let size = bounds.size(); - - Some(PhysicalSize::new( - size.width() * scale, - size.height() * scale, - )) - } - } - } - - pub fn title(&self) -> Option { - match self { - Self::Screen { id } => Display::from_id(id).and_then(|d| d.name()), - Self::Window { id } => Window::from_id(id).and_then(|w| w.name()), - Self::Area { screen, .. } => Display::from_id(screen).and_then(|d| d.name()), - } - } -} - -pub struct ScreenCaptureSource { - config: Config, - video_info: VideoInfo, - tokio_handle: tokio::runtime::Handle, - video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>, - audio_tx: Option>, - start_time: SystemTime, - _phantom: std::marker::PhantomData, -} - -impl std::fmt::Debug for ScreenCaptureSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ScreenCaptureSource") - // .field("bounds", &self.bounds) - // .field("output_resolution", &self.output_resolution) - .field("fps", &self.config.fps) - .field("video_info", &self.video_info) - .field( - "audio_info", - &self.audio_tx.as_ref().map(|_| self.audio_info()), - ) - .finish() - } -} - -unsafe impl Send for ScreenCaptureSource {} -unsafe impl Sync for ScreenCaptureSource {} - -pub trait ScreenCaptureFormat { - type VideoFormat; - - fn pixel_format() -> ffmpeg::format::Pixel; - - fn audio_info() -> AudioInfo; -} - -impl Clone for ScreenCaptureSource { - fn clone(&self) -> Self { - Self { - config: self.config.clone(), - video_info: self.video_info, - video_tx: self.video_tx.clone(), - audio_tx: self.audio_tx.clone(), - tokio_handle: self.tokio_handle.clone(), - start_time: self.start_time, - _phantom: std::marker::PhantomData, - } - } -} - -#[derive(Clone, Debug)] -struct Config { - display: DisplayId, - #[cfg(windows)] - crop_bounds: Option, - #[cfg(target_os = "macos")] - crop_bounds: Option, - fps: u32, - show_cursor: bool, -} - -#[derive(Debug, Clone, thiserror::Error)] -pub enum ScreenCaptureInitError { - #[error("NoDisplay")] - NoDisplay, - #[error("PhysicalSize")] - PhysicalSize, -} - -impl ScreenCaptureSource { - #[allow(clippy::too_many_arguments)] - pub async fn init( - target: &ScreenCaptureTarget, - show_cursor: bool, - max_fps: u32, - video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>, - audio_tx: Option>, - start_time: SystemTime, - tokio_handle: tokio::runtime::Handle, - ) -> Result { - cap_fail::fail!("media::screen_capture::init"); - - let display = target.display().ok_or(ScreenCaptureInitError::NoDisplay)?; - - let fps = max_fps.min(display.refresh_rate() as u32); - - let crop_bounds = match target { - ScreenCaptureTarget::Screen { .. } => None, - ScreenCaptureTarget::Window { id } => { - let window = Window::from_id(&id).unwrap(); - - #[cfg(target_os = "macos")] - { - let raw_display_bounds = display.raw_handle().logical_bounds().unwrap(); - let raw_window_bounds = window.raw_handle().logical_bounds().unwrap(); - - Some(LogicalBounds::new( - LogicalPosition::new( - raw_window_bounds.position().x() - raw_display_bounds.position().x(), - raw_window_bounds.position().y() - raw_display_bounds.position().y(), - ), - raw_window_bounds.size(), - )) - } - - #[cfg(windows)] - { - let raw_display_position = display.raw_handle().physical_position().unwrap(); - let raw_window_bounds = window.raw_handle().physical_bounds().unwrap(); - - Some(PhysicalBounds::new( - PhysicalPosition::new( - raw_window_bounds.position().x() - raw_display_position.x(), - raw_window_bounds.position().y() - raw_display_position.y(), - ), - raw_window_bounds.size(), - )) - } - } - ScreenCaptureTarget::Area { - bounds: relative_bounds, - .. - } => { - #[cfg(target_os = "macos")] - { - Some(*relative_bounds) - } - - #[cfg(windows)] - { - let raw_display_size = display.physical_size().unwrap(); - let logical_display_size = display.logical_size().unwrap(); - - Some(PhysicalBounds::new( - PhysicalPosition::new( - (relative_bounds.position().x() / logical_display_size.width()) - * raw_display_size.width(), - (relative_bounds.position().y() / logical_display_size.height()) - * raw_display_size.height(), - ), - PhysicalSize::new( - (relative_bounds.size().width() / logical_display_size.width()) - * raw_display_size.width(), - (relative_bounds.size().height() / logical_display_size.height()) - * raw_display_size.height(), - ), - )) - } - } - }; - - let output_size = crop_bounds - .and_then(|b| { - #[cfg(target_os = "macos")] - { - let logical_size = b.size(); - let scale = display.raw_handle().scale()?; - Some(PhysicalSize::new( - logical_size.width() * scale, - logical_size.height() * scale, - )) - } - - #[cfg(windows)] - Some(b.size()) - }) - .or_else(|| display.physical_size()) - .unwrap(); - - Ok(Self { - config: Config { - display: display.id(), - crop_bounds, - fps, - show_cursor, - }, - video_info: VideoInfo::from_raw_ffmpeg( - TCaptureFormat::pixel_format(), - output_size.width() as u32, - output_size.height() as u32, - fps, - ), - video_tx, - audio_tx, - tokio_handle, - start_time, - _phantom: std::marker::PhantomData, - }) - } - - pub fn info(&self) -> VideoInfo { - self.video_info - } - - pub fn audio_info(&self) -> AudioInfo { - TCaptureFormat::audio_info() - } -} - -pub fn list_displays() -> Vec<(CaptureDisplay, Display)> { - cap_displays::Display::list() - .into_iter() - .filter_map(|display| { - Some(( - CaptureDisplay { - id: display.id(), - name: display.name()?, - refresh_rate: display.raw_handle().refresh_rate() as u32, - }, - display, - )) - }) - .collect() -} - -pub fn list_windows() -> Vec<(CaptureWindow, Window)> { - cap_displays::Window::list() - .into_iter() - .flat_map(|v| { - let name = v.name()?; - - if name.is_empty() { - return None; - } - - #[cfg(target_os = "macos")] - { - if v.raw_handle().level() != Some(0) - || v.owner_name().filter(|v| v == "Window Server").is_some() - { - return None; - } - } - - #[cfg(windows)] - { - if !v.raw_handle().is_on_screen() { - return None; - } - } - - Some(( - CaptureWindow { - id: v.id(), - name, - owner_name: v.owner_name()?, - bounds: v.display_relative_logical_bounds()?, - refresh_rate: v.display()?.raw_handle().refresh_rate() as u32, - }, - v, - )) - }) - .collect() -} - -use kameo::prelude::*; - -pub struct StopCapturing; - -#[derive(Debug, Clone)] -pub enum StopCapturingError { - NotCapturing, -} - -#[cfg(windows)] -mod windows { - use super::*; - use ::windows::{ - Graphics::Capture::GraphicsCaptureItem, Win32::Graphics::Direct3D11::D3D11_BOX, - }; - use cpal::traits::{DeviceTrait, HostTrait}; - use scap_ffmpeg::*; - - #[derive(Debug)] - pub struct AVFrameCapture; - - impl AVFrameCapture { - const PIXEL_FORMAT: scap_direct3d::PixelFormat = scap_direct3d::PixelFormat::R8G8B8A8Unorm; - } - - impl ScreenCaptureFormat for AVFrameCapture { - type VideoFormat = ffmpeg::frame::Video; - - fn pixel_format() -> ffmpeg::format::Pixel { - scap_direct3d::PixelFormat::R8G8B8A8Unorm.as_ffmpeg() - } - - fn audio_info() -> AudioInfo { - let host = cpal::default_host(); - let output_device = host.default_output_device().unwrap(); - let supported_config = output_device.default_output_config().unwrap(); - - let mut info = AudioInfo::from_stream_config(&supported_config); - - info.sample_format = ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed); - - info - } - } - - impl PipelineSourceTask for ScreenCaptureSource { - // #[instrument(skip_all)] - fn run( - &mut self, - ready_signal: crate::pipeline::task::PipelineReadySignal, - control_signal: crate::pipeline::control::PipelineControlSignal, - ) -> Result<(), String> { - use kameo::prelude::*; - - const WINDOW_DURATION: Duration = Duration::from_secs(3); - const LOG_INTERVAL: Duration = Duration::from_secs(5); - const MAX_DROP_RATE_THRESHOLD: f64 = 0.25; - - let video_info = self.video_info; - let video_tx = self.video_tx.clone(); - let audio_tx = self.audio_tx.clone(); - - let start_time = self.start_time; - - let mut video_i = 0; - let mut audio_i = 0; - - let mut frames_dropped = 0; - - // Frame drop rate tracking state - use std::collections::VecDeque; - use std::time::{Duration, Instant}; - - struct FrameHandler { - capturer: WeakActorRef, - start_time: SystemTime, - frames_dropped: u32, - last_cleanup: Instant, - last_log: Instant, - frame_events: VecDeque<(Instant, bool)>, - video_tx: Sender<(ffmpeg::frame::Video, f64)>, - } - - impl Actor for FrameHandler { - type Args = Self; - type Error = (); - - async fn on_start( - args: Self::Args, - self_actor: ActorRef, - ) -> Result { - if let Some(capturer) = args.capturer.upgrade() { - self_actor.link(&capturer).await; - } - - Ok(args) - } - - async fn on_link_died( - &mut self, - actor_ref: WeakActorRef, - id: ActorID, - _: ActorStopReason, - ) -> Result, Self::Error> { - if self.capturer.id() == id - && let Some(self_actor) = actor_ref.upgrade() - { - let _ = self_actor.stop_gracefully().await; - - return Ok(std::ops::ControlFlow::Break(ActorStopReason::Normal)); - } - - Ok(std::ops::ControlFlow::Continue(())) - } - } - - impl FrameHandler { - // Helper function to clean up old frame events - fn cleanup_old_events(&mut self, now: Instant) { - let cutoff = now - WINDOW_DURATION; - while let Some(&(timestamp, _)) = self.frame_events.front() { - if timestamp < cutoff { - self.frame_events.pop_front(); - } else { - break; - } - } - } - - // Helper function to calculate current drop rate - fn calculate_drop_rate(&mut self) -> (f64, usize, usize) { - let now = Instant::now(); - self.cleanup_old_events(now); - - if self.frame_events.is_empty() { - return (0.0, 0, 0); - } - - let total_frames = self.frame_events.len(); - let dropped_frames = self - .frame_events - .iter() - .filter(|(_, dropped)| *dropped) - .count(); - let drop_rate = dropped_frames as f64 / total_frames as f64; - - (drop_rate, dropped_frames, total_frames) - } - } - - impl Message for FrameHandler { - type Reply = (); - - async fn handle( - &mut self, - mut msg: NewFrame, - ctx: &mut kameo::prelude::Context, - ) -> Self::Reply { - let Ok(elapsed) = msg.display_time.duration_since(self.start_time) else { - return; - }; - - msg.ff_frame.set_pts(Some( - (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64, - )); - - let now = Instant::now(); - let frame_dropped = match self - .video_tx - .try_send((msg.ff_frame, elapsed.as_secs_f64())) - { - Err(flume::TrySendError::Disconnected(_)) => { - warn!("Pipeline disconnected"); - let _ = ctx.actor_ref().stop_gracefully().await; - return; - } - Err(flume::TrySendError::Full(_)) => { - warn!("Screen capture sender is full, dropping frame"); - self.frames_dropped += 1; - true - } - _ => false, - }; - - self.frame_events.push_back((now, frame_dropped)); - - if now.duration_since(self.last_cleanup) > Duration::from_millis(100) { - self.cleanup_old_events(now); - self.last_cleanup = now; - } - - // Check drop rate and potentially exit - let (drop_rate, dropped_count, total_count) = self.calculate_drop_rate(); - - if drop_rate > MAX_DROP_RATE_THRESHOLD && total_count >= 10 { - error!( - "High frame drop rate detected: {:.1}% ({}/{} frames in last {}s). Exiting capture.", - drop_rate * 100.0, - dropped_count, - total_count, - WINDOW_DURATION.as_secs() - ); - return; - // return ControlFlow::Break(Err("Recording can't keep up with screen capture. Try reducing your display's resolution or refresh rate.".to_string())); - } - - // Periodic logging of drop rate - if now.duration_since(self.last_log) > LOG_INTERVAL && total_count > 0 { - info!( - "Frame drop rate: {:.1}% ({}/{} frames, total dropped: {})", - drop_rate * 100.0, - dropped_count, - total_count, - self.frames_dropped - ); - self.last_log = now; - } - } - } - - let config = self.config.clone(); - - let _ = self.tokio_handle.block_on(async move { - let capturer = WindowsScreenCapture::spawn(WindowsScreenCapture::new()); - - let stop_recipient = capturer.clone().reply_recipient::(); - - let frame_handler = FrameHandler::spawn(FrameHandler { - capturer: capturer.downgrade(), - video_tx, - start_time, - frame_events: Default::default(), - frames_dropped: Default::default(), - last_cleanup: Instant::now(), - last_log: Instant::now(), - }); - - let mut settings = scap_direct3d::Settings { - is_border_required: Some(false), - pixel_format: AVFrameCapture::PIXEL_FORMAT, - crop: config.crop_bounds.map(|b| { - let position = b.position(); - let size = b.size(); - - D3D11_BOX { - left: position.x() as u32, - top: position.y() as u32, - right: (position.x() + size.width()) as u32, - bottom: (position.y() + size.height()) as u32, - front: 0, - back: 1, - } - }), - ..Default::default() - }; - - let display = Display::from_id(&config.display).unwrap(); - - let capture_item = display.raw_handle().try_as_capture_item().unwrap(); - - settings.is_cursor_capture_enabled = Some(config.show_cursor); - - let _ = capturer - .ask(StartCapturing { - target: capture_item, - settings, - frame_handler: frame_handler.clone().recipient(), - }) - .send() - .await; - - let audio_capture = if let Some(audio_tx) = audio_tx { - let audio_capture = WindowsAudioCapture::spawn( - WindowsAudioCapture::new(audio_tx, start_time).unwrap(), - ); - - let _ = dbg!(audio_capture.ask(audio::StartCapturing).send().await); - - Some(audio_capture) - } else { - None - }; - - let _ = ready_signal.send(Ok(())); - - while let Ok(msg) = control_signal.receiver.recv_async().await { - if let Control::Shutdown = msg { - let _ = stop_recipient.ask(StopCapturing).await; - - if let Some(audio_capture) = audio_capture { - let _ = audio_capture.ask(StopCapturing).await; - } - - break; - } - } - }); - - Ok(()) - } - } - - #[derive(Actor)] - pub struct WindowsScreenCapture { - capture_handle: Option, - } - - impl WindowsScreenCapture { - pub fn new() -> Self { - Self { - capture_handle: None, - } - } - } - - pub struct StartCapturing { - pub target: GraphicsCaptureItem, - pub settings: scap_direct3d::Settings, - pub frame_handler: Recipient, - // error_handler: Option>, - } - - #[derive(Debug)] - pub enum StartCapturingError { - AlreadyCapturing, - Inner(scap_direct3d::StartCapturerError), - } - - pub struct NewFrame { - pub ff_frame: ffmpeg::frame::Video, - pub display_time: SystemTime, - } - - impl Message for WindowsScreenCapture { - type Reply = Result<(), StartCapturingError>; - - async fn handle( - &mut self, - msg: StartCapturing, - _: &mut Context, - ) -> Self::Reply { - if self.capture_handle.is_some() { - return Err(StartCapturingError::AlreadyCapturing); - } - - let capturer = scap_direct3d::Capturer::new(msg.target, msg.settings); - - let capture_handle = capturer - .start( - move |frame| { - let display_time = SystemTime::now(); - let ff_frame = frame.as_ffmpeg().unwrap(); - - // dbg!(ff_frame.width(), ff_frame.height()); - - let _ = msg - .frame_handler - .tell(NewFrame { - ff_frame, - display_time, - }) - .try_send(); - - Ok(()) - }, - || Ok(()), - ) - .map_err(StartCapturingError::Inner)?; - - self.capture_handle = Some(capture_handle); - - Ok(()) - } - } - - impl Message for WindowsScreenCapture { - type Reply = Result<(), StopCapturingError>; - - async fn handle( - &mut self, - msg: StopCapturing, - ctx: &mut Context, - ) -> Self::Reply { - let Some(capturer) = self.capture_handle.take() else { - return Err(StopCapturingError::NotCapturing); - }; - - println!("stopping windows capturer"); - if let Err(e) = capturer.stop() { - error!("Silently failed to stop Windows capturer: {}", e); - } - println!("stopped windows capturer"); - - Ok(()) - } - } - - use audio::WindowsAudioCapture; - pub mod audio { - use super::*; - use cpal::traits::StreamTrait; - use scap_cpal::*; - use scap_ffmpeg::*; - - #[derive(Actor)] - pub struct WindowsAudioCapture { - capturer: scap_cpal::Capturer, - } - - impl WindowsAudioCapture { - pub fn new( - audio_tx: Sender<(ffmpeg::frame::Audio, f64)>, - start_time: SystemTime, - ) -> Result { - let mut i = 0; - let capturer = scap_cpal::create_capturer( - move |data, _: &cpal::InputCallbackInfo, config| { - use scap_ffmpeg::*; - - let timestamp = SystemTime::now(); - let mut ff_frame = data.as_ffmpeg(config); - - let Ok(elapsed) = timestamp.duration_since(start_time) else { - warn!("Skipping audio frame {i} as elapsed time is invalid"); - return; - }; - - ff_frame.set_pts(Some( - (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64, - )); - - let _ = audio_tx.send((ff_frame, elapsed.as_secs_f64())); - i += 1; - }, - move |e| { - dbg!(e); - }, - )?; - - Ok(Self { capturer }) - } - } - - pub struct StartCapturing; - - impl Message for WindowsAudioCapture { - type Reply = Result<(), &'static str>; - - async fn handle( - &mut self, - msg: StartCapturing, - ctx: &mut Context, - ) -> Self::Reply { - self.capturer.play().map_err(|_| "failed to start stream")?; - - Ok(()) - } - } - - impl Message for WindowsAudioCapture { - type Reply = Result<(), &'static str>; - - async fn handle( - &mut self, - msg: StopCapturing, - ctx: &mut Context, - ) -> Self::Reply { - self.capturer.pause().map_err(|_| "failed to stop stream")?; - - Ok(()) - } - } - } -} - -#[cfg(windows)] -pub use windows::*; - -#[cfg(target_os = "macos")] -mod macos { - use super::*; - use cidre::*; - - #[derive(Debug)] - pub struct CMSampleBufferCapture; - - #[cfg(target_os = "macos")] - impl ScreenCaptureFormat for CMSampleBufferCapture { - type VideoFormat = cidre::arc::R; - - fn pixel_format() -> ffmpeg::format::Pixel { - ffmpeg::format::Pixel::BGRA - } - - fn audio_info() -> AudioInfo { - AudioInfo::new( - ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), - 48_000, - 2, - ) - .unwrap() - } - } - - #[cfg(target_os = "macos")] - impl PipelineSourceTask for ScreenCaptureSource { - fn run( - &mut self, - ready_signal: crate::pipeline::task::PipelineReadySignal, - control_signal: crate::pipeline::control::PipelineControlSignal, - ) -> Result<(), String> { - use cidre::{arc, cg, cm, cv}; - use kameo::prelude::*; - - #[derive(Actor)] - struct FrameHandler { - start_time_unix: f64, - start_cmtime: f64, - start_time_f64: f64, - video_tx: Sender<(arc::R, f64)>, - audio_tx: Option>, - } - - impl Message for FrameHandler { - type Reply = (); - - async fn handle( - &mut self, - msg: NewFrame, - _: &mut kameo::prelude::Context, - ) -> Self::Reply { - let frame = msg.0; - let sample_buffer = frame.sample_buf(); - - let frame_time = - sample_buffer.pts().value as f64 / sample_buffer.pts().scale as f64; - let unix_timestamp = self.start_time_unix + frame_time - self.start_cmtime; - let relative_time = unix_timestamp - self.start_time_f64; - - match &frame { - scap_screencapturekit::Frame::Screen(frame) => { - if frame.image_buf().height() == 0 || frame.image_buf().width() == 0 { - return; - } - - let check_skip_send = || { - cap_fail::fail_err!( - "media::sources::screen_capture::skip_send", - () - ); - - Ok::<(), ()>(()) - }; - - if check_skip_send().is_ok() - && self - .video_tx - .send((sample_buffer.retained(), relative_time)) - .is_err() - { - warn!("Pipeline is unreachable"); - } - } - scap_screencapturekit::Frame::Audio(_) => { - use ffmpeg::ChannelLayout; - - let res = || { - cap_fail::fail_err!("screen_capture audio skip", ()); - Ok::<(), ()>(()) - }; - if res().is_err() { - return; - } - - let Some(audio_tx) = &self.audio_tx else { - return; - }; - - let buf_list = sample_buffer.audio_buf_list::<2>().unwrap(); - let slice = buf_list.block().as_slice().unwrap(); - - let mut frame = ffmpeg::frame::Audio::new( - ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), - sample_buffer.num_samples() as usize, - ChannelLayout::STEREO, - ); - frame.set_rate(48_000); - let data_bytes_size = buf_list.list().buffers[0].data_bytes_size; - for i in 0..frame.planes() { - use cap_media_info::PlanarData; - - frame.plane_data_mut(i).copy_from_slice( - &slice[i * data_bytes_size as usize - ..(i + 1) * data_bytes_size as usize], - ); - } - - frame.set_pts(Some((relative_time * AV_TIME_BASE_Q.den as f64) as i64)); - - let _ = audio_tx.send((frame, relative_time)); - } - _ => {} - } - } - } - - let start = std::time::SystemTime::now(); - let start_time_unix = start - .duration_since(std::time::UNIX_EPOCH) - .expect("Time went backwards") - .as_secs_f64(); - let start_cmtime = cidre::cm::Clock::host_time_clock().time(); - let start_cmtime = start_cmtime.value as f64 / start_cmtime.scale as f64; - - let start_time_f64 = self - .start_time - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs_f64(); - - let video_tx = self.video_tx.clone(); - let audio_tx = self.audio_tx.clone(); - let config = self.config.clone(); - - self.tokio_handle.block_on(async move { - let captures_audio = audio_tx.is_some(); - let frame_handler = FrameHandler::spawn(FrameHandler { - video_tx, - audio_tx, - start_time_unix, - start_cmtime, - start_time_f64, - }); - - let display = Display::from_id(&config.display).unwrap(); - - let content_filter = display - .raw_handle() - .as_content_filter() - .await - .ok_or_else(|| "Failed to get content filter".to_string())?; - - let size = { - let logical_size = config - .crop_bounds - .map(|bounds| bounds.size()) - .or_else(|| display.logical_size()) - .unwrap(); - - let scale = display.physical_size().unwrap().width() - / display.logical_size().unwrap().width(); - - PhysicalSize::new(logical_size.width() * scale, logical_size.height() * scale) - }; - - tracing::info!("size: {:?}", size); - - let mut settings = scap_screencapturekit::StreamCfgBuilder::default() - .with_width(size.width() as usize) - .with_height(size.height() as usize) - .with_fps(config.fps as f32) - .with_shows_cursor(config.show_cursor) - .with_captures_audio(captures_audio) - .build(); - - settings.set_pixel_format(cv::PixelFormat::_32_BGRA); - - if let Some(crop_bounds) = config.crop_bounds { - tracing::info!("crop bounds: {:?}", crop_bounds); - settings.set_src_rect(cg::Rect::new( - crop_bounds.position().x(), - crop_bounds.position().y(), - crop_bounds.size().width(), - crop_bounds.size().height(), - )); - } - - let (error_tx, error_rx) = flume::bounded(1); - - let capturer = ScreenCaptureActor::spawn( - ScreenCaptureActor::new( - content_filter, - settings, - frame_handler.recipient(), - error_tx.clone(), - ) - .unwrap(), - ); - - let stop_recipient = capturer.clone().reply_recipient::(); - - let _ = capturer.ask(StartCapturing).send().await.unwrap(); - - let _ = ready_signal.send(Ok(())); - - loop { - use futures::future::Either; - - let check_err = || { - use cidre::ns; - - Result::<_, arc::R>::Ok(cap_fail::fail_err!( - "macos screen capture startup error", - ns::Error::with_domain(ns::ErrorDomain::os_status(), 1, None) - )) - }; - if let Err(e) = check_err() { - let _ = error_tx.send(e); - } - - match futures::future::select( - error_rx.recv_async(), - control_signal.receiver.recv_async(), - ) - .await - { - Either::Left((Ok(error), _)) => { - error!("Error capturing screen: {}", error); - let _ = stop_recipient.ask(StopCapturing).await; - return Err(error.to_string()); - } - Either::Right((Ok(ctrl), _)) => { - if let Control::Shutdown = ctrl { - let _ = stop_recipient.ask(StopCapturing).await; - return Ok(()); - } - } - _ => { - warn!("Screen capture recv channels shutdown, exiting."); - - let _ = stop_recipient.ask(StopCapturing).await; - - return Ok(()); - } - } - } - }) - } - } - - #[derive(Actor)] - pub struct ScreenCaptureActor { - capturer: scap_screencapturekit::Capturer, - capturing: bool, - } - - impl ScreenCaptureActor { - pub fn new( - target: arc::R, - settings: arc::R, - frame_handler: Recipient, - error_tx: Sender>, - ) -> Result> { - let _error_tx = error_tx.clone(); - let capturer_builder = scap_screencapturekit::Capturer::builder(target, settings) - .with_output_sample_buf_cb(move |frame| { - let check_err = || { - Result::<_, arc::R>::Ok(cap_fail::fail_err!( - "macos screen capture frame error", - ns::Error::with_domain(ns::ErrorDomain::os_status(), 1, None) - )) - }; - if let Err(e) = check_err() { - let _ = _error_tx.send(e); - } - - let _ = frame_handler.tell(NewFrame(frame)).try_send(); - }) - .with_stop_with_err_cb(move |_, err| { - let _ = error_tx.send(err.retained()); - }); - - Ok(ScreenCaptureActor { - capturer: capturer_builder.build()?, - capturing: false, - }) - } - } - - // Public - - pub struct StartCapturing; - - // External - - pub struct NewFrame(pub scap_screencapturekit::Frame); - - // Internal - - pub struct CaptureError(pub arc::R); - - #[derive(Debug, Clone)] - pub enum StartCapturingError { - AlreadyCapturing, - Start(arc::R), - } - - impl Message for ScreenCaptureActor { - type Reply = Result<(), StartCapturingError>; - - async fn handle( - &mut self, - _: StartCapturing, - _: &mut Context, - ) -> Self::Reply { - if self.capturing { - return Err(StartCapturingError::AlreadyCapturing); - } - - self.capturer - .start() - .await - .map_err(StartCapturingError::Start)?; - - self.capturing = true; - - Ok(()) - } - } - - impl Message for ScreenCaptureActor { - type Reply = Result<(), StopCapturingError>; - - async fn handle( - &mut self, - _: StopCapturing, - _: &mut Context, - ) -> Self::Reply { - if !self.capturing { - return Err(StopCapturingError::NotCapturing); - }; - - if let Err(e) = self.capturer.stop().await { - error!("Silently failed to stop macOS capturer: {}", e); - } - - Ok(()) - } - } -} - -#[cfg(target_os = "macos")] -pub use macos::*; diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs new file mode 100644 index 0000000000..81f9a5b216 --- /dev/null +++ b/crates/recording/src/sources/screen_capture/macos.rs @@ -0,0 +1,356 @@ +use super::*; +use cidre::*; + +#[derive(Debug)] +pub struct CMSampleBufferCapture; + +#[cfg(target_os = "macos")] +impl ScreenCaptureFormat for CMSampleBufferCapture { + type VideoFormat = cidre::arc::R; + + fn pixel_format() -> ffmpeg::format::Pixel { + ffmpeg::format::Pixel::BGRA + } + + fn audio_info() -> AudioInfo { + AudioInfo::new( + ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), + 48_000, + 2, + ) + .unwrap() + } +} + +#[cfg(target_os = "macos")] +impl PipelineSourceTask for ScreenCaptureSource { + fn run( + &mut self, + ready_signal: crate::pipeline::task::PipelineReadySignal, + control_signal: crate::pipeline::control::PipelineControlSignal, + ) -> Result<(), String> { + use cidre::{arc, cg, cm, cv}; + use kameo::prelude::*; + + #[derive(Actor)] + struct FrameHandler { + start_time_unix: f64, + start_cmtime: f64, + start_time_f64: f64, + video_tx: Sender<(arc::R, f64)>, + audio_tx: Option>, + } + + impl Message for FrameHandler { + type Reply = (); + + async fn handle( + &mut self, + msg: NewFrame, + _: &mut kameo::prelude::Context, + ) -> Self::Reply { + let frame = msg.0; + let sample_buffer = frame.sample_buf(); + + let frame_time = + sample_buffer.pts().value as f64 / sample_buffer.pts().scale as f64; + let unix_timestamp = self.start_time_unix + frame_time - self.start_cmtime; + let relative_time = unix_timestamp - self.start_time_f64; + + match &frame { + scap_screencapturekit::Frame::Screen(frame) => { + if frame.image_buf().height() == 0 || frame.image_buf().width() == 0 { + return; + } + + let check_skip_send = || { + cap_fail::fail_err!("media::sources::screen_capture::skip_send", ()); + + Ok::<(), ()>(()) + }; + + if check_skip_send().is_ok() + && self + .video_tx + .send((sample_buffer.retained(), relative_time)) + .is_err() + { + warn!("Pipeline is unreachable"); + } + } + scap_screencapturekit::Frame::Audio(_) => { + use ffmpeg::ChannelLayout; + + let res = || { + cap_fail::fail_err!("screen_capture audio skip", ()); + Ok::<(), ()>(()) + }; + if res().is_err() { + return; + } + + let Some(audio_tx) = &self.audio_tx else { + return; + }; + + let buf_list = sample_buffer.audio_buf_list::<2>().unwrap(); + let slice = buf_list.block().as_slice().unwrap(); + + let mut frame = ffmpeg::frame::Audio::new( + ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), + sample_buffer.num_samples() as usize, + ChannelLayout::STEREO, + ); + frame.set_rate(48_000); + let data_bytes_size = buf_list.list().buffers[0].data_bytes_size; + for i in 0..frame.planes() { + use cap_media_info::PlanarData; + + frame.plane_data_mut(i).copy_from_slice( + &slice[i * data_bytes_size as usize + ..(i + 1) * data_bytes_size as usize], + ); + } + + frame.set_pts(Some((relative_time * AV_TIME_BASE_Q.den as f64) as i64)); + + let _ = audio_tx.send((frame, relative_time)); + } + _ => {} + } + } + } + + let start = std::time::SystemTime::now(); + let start_time_unix = start + .duration_since(std::time::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs_f64(); + let start_cmtime = cidre::cm::Clock::host_time_clock().time(); + let start_cmtime = start_cmtime.value as f64 / start_cmtime.scale as f64; + + let start_time_f64 = self + .start_time + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs_f64(); + + let video_tx = self.video_tx.clone(); + let audio_tx = self.audio_tx.clone(); + let config = self.config.clone(); + + self.tokio_handle.block_on(async move { + let captures_audio = audio_tx.is_some(); + let frame_handler = FrameHandler::spawn(FrameHandler { + video_tx, + audio_tx, + start_time_unix, + start_cmtime, + start_time_f64, + }); + + let display = Display::from_id(&config.display).unwrap(); + + let content_filter = display + .raw_handle() + .as_content_filter() + .await + .ok_or_else(|| "Failed to get content filter".to_string())?; + + let size = { + let logical_size = config + .crop_bounds + .map(|bounds| bounds.size()) + .or_else(|| display.logical_size()) + .unwrap(); + + let scale = display.physical_size().unwrap().width() + / display.logical_size().unwrap().width(); + + PhysicalSize::new(logical_size.width() * scale, logical_size.height() * scale) + }; + + tracing::info!("size: {:?}", size); + + let mut settings = scap_screencapturekit::StreamCfgBuilder::default() + .with_width(size.width() as usize) + .with_height(size.height() as usize) + .with_fps(config.fps as f32) + .with_shows_cursor(config.show_cursor) + .with_captures_audio(captures_audio) + .build(); + + settings.set_pixel_format(cv::PixelFormat::_32_BGRA); + + if let Some(crop_bounds) = config.crop_bounds { + tracing::info!("crop bounds: {:?}", crop_bounds); + settings.set_src_rect(cg::Rect::new( + crop_bounds.position().x(), + crop_bounds.position().y(), + crop_bounds.size().width(), + crop_bounds.size().height(), + )); + } + + let (error_tx, error_rx) = flume::bounded(1); + + let capturer = ScreenCaptureActor::spawn( + ScreenCaptureActor::new( + content_filter, + settings, + frame_handler.recipient(), + error_tx.clone(), + ) + .unwrap(), + ); + + let stop_recipient = capturer.clone().reply_recipient::(); + + let _ = capturer.ask(StartCapturing).send().await.unwrap(); + + let _ = ready_signal.send(Ok(())); + + loop { + use futures::future::Either; + + let check_err = || { + use cidre::ns; + + Result::<_, arc::R>::Ok(cap_fail::fail_err!( + "macos screen capture startup error", + ns::Error::with_domain(ns::ErrorDomain::os_status(), 1, None) + )) + }; + if let Err(e) = check_err() { + let _ = error_tx.send(e); + } + + match futures::future::select( + error_rx.recv_async(), + control_signal.receiver.recv_async(), + ) + .await + { + Either::Left((Ok(error), _)) => { + error!("Error capturing screen: {}", error); + let _ = stop_recipient.ask(StopCapturing).await; + return Err(error.to_string()); + } + Either::Right((Ok(ctrl), _)) => { + if let Control::Shutdown = ctrl { + let _ = stop_recipient.ask(StopCapturing).await; + return Ok(()); + } + } + _ => { + warn!("Screen capture recv channels shutdown, exiting."); + + let _ = stop_recipient.ask(StopCapturing).await; + + return Ok(()); + } + } + } + }) + } +} + +#[derive(Actor)] +pub struct ScreenCaptureActor { + capturer: scap_screencapturekit::Capturer, + capturing: bool, +} + +impl ScreenCaptureActor { + pub fn new( + target: arc::R, + settings: arc::R, + frame_handler: Recipient, + error_tx: Sender>, + ) -> Result> { + let _error_tx = error_tx.clone(); + let capturer_builder = scap_screencapturekit::Capturer::builder(target, settings) + .with_output_sample_buf_cb(move |frame| { + let check_err = || { + Result::<_, arc::R>::Ok(cap_fail::fail_err!( + "macos screen capture frame error", + ns::Error::with_domain(ns::ErrorDomain::os_status(), 1, None) + )) + }; + if let Err(e) = check_err() { + let _ = _error_tx.send(e); + } + + let _ = frame_handler.tell(NewFrame(frame)).try_send(); + }) + .with_stop_with_err_cb(move |_, err| { + let _ = error_tx.send(err.retained()); + }); + + Ok(ScreenCaptureActor { + capturer: capturer_builder.build()?, + capturing: false, + }) + } +} + +// Public + +pub struct StartCapturing; + +// External + +pub struct NewFrame(pub scap_screencapturekit::Frame); + +// Internal + +pub struct CaptureError(pub arc::R); + +#[derive(Debug, Clone)] +pub enum StartCapturingError { + AlreadyCapturing, + Start(arc::R), +} + +impl Message for ScreenCaptureActor { + type Reply = Result<(), StartCapturingError>; + + async fn handle( + &mut self, + _: StartCapturing, + _: &mut Context, + ) -> Self::Reply { + if self.capturing { + return Err(StartCapturingError::AlreadyCapturing); + } + + self.capturer + .start() + .await + .map_err(StartCapturingError::Start)?; + + self.capturing = true; + + Ok(()) + } +} + +impl Message for ScreenCaptureActor { + type Reply = Result<(), StopCapturingError>; + + async fn handle( + &mut self, + _: StopCapturing, + _: &mut Context, + ) -> Self::Reply { + if !self.capturing { + return Err(StopCapturingError::NotCapturing); + }; + + if let Err(e) = self.capturer.stop().await { + error!("Silently failed to stop macOS capturer: {}", e); + } + + Ok(()) + } +} diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs new file mode 100644 index 0000000000..aa17db85f5 --- /dev/null +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -0,0 +1,460 @@ +use cap_cursor_capture::CursorCropBounds; +use cap_displays::{ + Display, DisplayId, Window, WindowId, + bounds::{LogicalBounds, LogicalPosition, PhysicalBounds, PhysicalPosition, PhysicalSize}, +}; +use cap_media_info::{AudioInfo, VideoInfo}; +use ffmpeg::sys::AV_TIME_BASE_Q; +use flume::Sender; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::time::SystemTime; +use tracing::{error, info, warn}; + +use crate::pipeline::{control::Control, task::PipelineSourceTask}; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use windows::*; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub use macos::*; + +pub struct StopCapturing; + +#[derive(Debug, Clone)] +pub enum StopCapturingError { + NotCapturing, +} + +static EXCLUDED_WINDOWS: &[&str] = &[ + "Cap Camera", + "Cap Recordings Overlay", + "Cap In Progress Recording", +]; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct CaptureWindow { + pub id: WindowId, + pub owner_name: String, + pub name: String, + pub bounds: LogicalBounds, + pub refresh_rate: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct CaptureDisplay { + pub id: DisplayId, + pub name: String, + pub refresh_rate: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct CaptureArea { + pub screen: CaptureDisplay, + pub bounds: LogicalBounds, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase", tag = "variant")] +pub enum ScreenCaptureTarget { + Window { + id: WindowId, + }, + Screen { + id: DisplayId, + }, + Area { + screen: DisplayId, + bounds: LogicalBounds, + }, +} + +impl ScreenCaptureTarget { + pub fn display(&self) -> Option { + match self { + Self::Screen { id } => Display::from_id(id), + Self::Window { id } => Window::from_id(id).and_then(|w| w.display()), + Self::Area { screen, .. } => Display::from_id(screen), + } + } + + pub fn logical_bounds(&self) -> Option { + match self { + Self::Screen { id } => todo!(), // Display::from_id(id).map(|d| d.logical_bounds()), + Self::Window { id } => Some(LogicalBounds::new( + LogicalPosition::new(0.0, 0.0), + Window::from_id(id)?.raw_handle().logical_size()?, + )), + Self::Area { bounds, .. } => Some(*bounds), + } + } + + pub fn cursor_crop(&self) -> Option { + match self { + Self::Screen { .. } => { + #[cfg(target_os = "macos")] + { + let display = self.display()?; + return Some(CursorCropBounds::new_macos(LogicalBounds::new( + LogicalPosition::new(0.0, 0.0), + display.raw_handle().logical_size()?, + ))); + } + + #[cfg(windows)] + { + let display = self.display()?; + return Some(CursorCropBounds::new_windows(PhysicalBounds::new( + PhysicalPosition::new(0.0, 0.0), + display.raw_handle().physical_size()?, + ))); + } + } + Self::Window { id } => { + let window = Window::from_id(id)?; + + #[cfg(target_os = "macos")] + { + let display = self.display()?; + let display_position = display.raw_handle().logical_position(); + let window_bounds = window.raw_handle().logical_bounds()?; + + return Some(CursorCropBounds::new_macos(LogicalBounds::new( + LogicalPosition::new( + window_bounds.position().x() - display_position.x(), + window_bounds.position().y() - display_position.y(), + ), + window_bounds.size(), + ))); + } + + #[cfg(windows)] + { + let display_bounds = self.display()?.raw_handle().physical_bounds()?; + let window_bounds = window.raw_handle().physical_bounds()?; + + return Some(CursorCropBounds::new_windows(PhysicalBounds::new( + PhysicalPosition::new( + window_bounds.position().x() - display_bounds.position().x(), + window_bounds.position().y() - display_bounds.position().y(), + ), + PhysicalSize::new( + window_bounds.size().width(), + window_bounds.size().height(), + ), + ))); + } + } + Self::Area { bounds, .. } => { + #[cfg(target_os = "macos")] + { + return Some(CursorCropBounds::new_macos(*bounds)); + } + + #[cfg(windows)] + { + let display = self.display()?; + let display_bounds = display.raw_handle().physical_bounds()?; + let display_logical_size = display.logical_size()?; + + let scale = display_bounds.size().width() / display_logical_size.width(); + + return Some(CursorCropBounds::new_windows(PhysicalBounds::new( + PhysicalPosition::new( + bounds.position().x() * scale, + bounds.position().y() * scale, + ), + PhysicalSize::new( + bounds.size().width() * scale, + bounds.size().height() * scale, + ), + ))); + } + } + } + } + + pub fn physical_size(&self) -> Option { + match self { + Self::Screen { id } => Display::from_id(id).and_then(|d| d.physical_size()), + Self::Window { id } => Window::from_id(id).and_then(|w| w.physical_size()), + Self::Area { bounds, .. } => { + let display = self.display()?; + let scale = display.physical_size()?.width() / display.logical_size()?.width(); + let size = bounds.size(); + + Some(PhysicalSize::new( + size.width() * scale, + size.height() * scale, + )) + } + } + } + + pub fn title(&self) -> Option { + match self { + Self::Screen { id } => Display::from_id(id).and_then(|d| d.name()), + Self::Window { id } => Window::from_id(id).and_then(|w| w.name()), + Self::Area { screen, .. } => Display::from_id(screen).and_then(|d| d.name()), + } + } +} + +pub struct ScreenCaptureSource { + config: Config, + video_info: VideoInfo, + tokio_handle: tokio::runtime::Handle, + video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>, + audio_tx: Option>, + start_time: SystemTime, + _phantom: std::marker::PhantomData, +} + +impl std::fmt::Debug for ScreenCaptureSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ScreenCaptureSource") + // .field("bounds", &self.bounds) + // .field("output_resolution", &self.output_resolution) + .field("fps", &self.config.fps) + .field("video_info", &self.video_info) + .field( + "audio_info", + &self.audio_tx.as_ref().map(|_| self.audio_info()), + ) + .finish() + } +} + +unsafe impl Send for ScreenCaptureSource {} +unsafe impl Sync for ScreenCaptureSource {} + +pub trait ScreenCaptureFormat { + type VideoFormat; + + fn pixel_format() -> ffmpeg::format::Pixel; + + fn audio_info() -> AudioInfo; +} + +impl Clone for ScreenCaptureSource { + fn clone(&self) -> Self { + Self { + config: self.config.clone(), + video_info: self.video_info, + video_tx: self.video_tx.clone(), + audio_tx: self.audio_tx.clone(), + tokio_handle: self.tokio_handle.clone(), + start_time: self.start_time, + _phantom: std::marker::PhantomData, + } + } +} + +#[derive(Clone, Debug)] +struct Config { + display: DisplayId, + #[cfg(windows)] + crop_bounds: Option, + #[cfg(target_os = "macos")] + crop_bounds: Option, + fps: u32, + show_cursor: bool, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ScreenCaptureInitError { + #[error("NoDisplay")] + NoDisplay, + #[error("PhysicalSize")] + PhysicalSize, +} + +impl ScreenCaptureSource { + #[allow(clippy::too_many_arguments)] + pub async fn init( + target: &ScreenCaptureTarget, + show_cursor: bool, + max_fps: u32, + video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>, + audio_tx: Option>, + start_time: SystemTime, + tokio_handle: tokio::runtime::Handle, + ) -> Result { + cap_fail::fail!("media::screen_capture::init"); + + let display = target.display().ok_or(ScreenCaptureInitError::NoDisplay)?; + + let fps = max_fps.min(display.refresh_rate() as u32); + + let crop_bounds = match target { + ScreenCaptureTarget::Screen { .. } => None, + ScreenCaptureTarget::Window { id } => { + let window = Window::from_id(&id).unwrap(); + + #[cfg(target_os = "macos")] + { + let raw_display_bounds = display.raw_handle().logical_bounds().unwrap(); + let raw_window_bounds = window.raw_handle().logical_bounds().unwrap(); + + Some(LogicalBounds::new( + LogicalPosition::new( + raw_window_bounds.position().x() - raw_display_bounds.position().x(), + raw_window_bounds.position().y() - raw_display_bounds.position().y(), + ), + raw_window_bounds.size(), + )) + } + + #[cfg(windows)] + { + let raw_display_position = display.raw_handle().physical_position().unwrap(); + let raw_window_bounds = window.raw_handle().physical_bounds().unwrap(); + + Some(PhysicalBounds::new( + PhysicalPosition::new( + raw_window_bounds.position().x() - raw_display_position.x(), + raw_window_bounds.position().y() - raw_display_position.y(), + ), + raw_window_bounds.size(), + )) + } + } + ScreenCaptureTarget::Area { + bounds: relative_bounds, + .. + } => { + #[cfg(target_os = "macos")] + { + Some(*relative_bounds) + } + + #[cfg(windows)] + { + let raw_display_size = display.physical_size().unwrap(); + let logical_display_size = display.logical_size().unwrap(); + + Some(PhysicalBounds::new( + PhysicalPosition::new( + (relative_bounds.position().x() / logical_display_size.width()) + * raw_display_size.width(), + (relative_bounds.position().y() / logical_display_size.height()) + * raw_display_size.height(), + ), + PhysicalSize::new( + (relative_bounds.size().width() / logical_display_size.width()) + * raw_display_size.width(), + (relative_bounds.size().height() / logical_display_size.height()) + * raw_display_size.height(), + ), + )) + } + } + }; + + let output_size = crop_bounds + .and_then(|b| { + #[cfg(target_os = "macos")] + { + let logical_size = b.size(); + let scale = display.raw_handle().scale()?; + Some(PhysicalSize::new( + logical_size.width() * scale, + logical_size.height() * scale, + )) + } + + #[cfg(windows)] + Some(b.size()) + }) + .or_else(|| display.physical_size()) + .unwrap(); + + Ok(Self { + config: Config { + display: display.id(), + crop_bounds, + fps, + show_cursor, + }, + video_info: VideoInfo::from_raw_ffmpeg( + TCaptureFormat::pixel_format(), + output_size.width() as u32, + output_size.height() as u32, + fps, + ), + video_tx, + audio_tx, + tokio_handle, + start_time, + _phantom: std::marker::PhantomData, + }) + } + + pub fn info(&self) -> VideoInfo { + self.video_info + } + + pub fn audio_info(&self) -> AudioInfo { + TCaptureFormat::audio_info() + } +} + +pub fn list_displays() -> Vec<(CaptureDisplay, Display)> { + cap_displays::Display::list() + .into_iter() + .filter_map(|display| { + Some(( + CaptureDisplay { + id: display.id(), + name: display.name()?, + refresh_rate: display.raw_handle().refresh_rate() as u32, + }, + display, + )) + }) + .collect() +} + +pub fn list_windows() -> Vec<(CaptureWindow, Window)> { + cap_displays::Window::list() + .into_iter() + .flat_map(|v| { + let name = v.name()?; + + if name.is_empty() { + return None; + } + + #[cfg(target_os = "macos")] + { + if v.raw_handle().level() != Some(0) + || v.owner_name().filter(|v| v == "Window Server").is_some() + { + return None; + } + } + + #[cfg(windows)] + { + if !v.raw_handle().is_on_screen() { + return None; + } + } + + Some(( + CaptureWindow { + id: v.id(), + name, + owner_name: v.owner_name()?, + bounds: v.display_relative_logical_bounds()?, + refresh_rate: v.display()?.raw_handle().refresh_rate() as u32, + }, + v, + )) + }) + .collect() +} diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs new file mode 100644 index 0000000000..45eb208d59 --- /dev/null +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -0,0 +1,461 @@ +use super::*; +use ::windows::{Graphics::Capture::GraphicsCaptureItem, Win32::Graphics::Direct3D11::D3D11_BOX}; +use cpal::traits::{DeviceTrait, HostTrait}; +use kameo::prelude::*; +use scap_ffmpeg::*; + +#[derive(Debug)] +pub struct AVFrameCapture; + +impl AVFrameCapture { + const PIXEL_FORMAT: scap_direct3d::PixelFormat = scap_direct3d::PixelFormat::R8G8B8A8Unorm; +} + +impl ScreenCaptureFormat for AVFrameCapture { + type VideoFormat = ffmpeg::frame::Video; + + fn pixel_format() -> ffmpeg::format::Pixel { + scap_direct3d::PixelFormat::R8G8B8A8Unorm.as_ffmpeg() + } + + fn audio_info() -> AudioInfo { + let host = cpal::default_host(); + let output_device = host.default_output_device().unwrap(); + let supported_config = output_device.default_output_config().unwrap(); + + let mut info = AudioInfo::from_stream_config(&supported_config); + + info.sample_format = ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed); + + info + } +} + +impl PipelineSourceTask for ScreenCaptureSource { + // #[instrument(skip_all)] + fn run( + &mut self, + ready_signal: crate::pipeline::task::PipelineReadySignal, + control_signal: crate::pipeline::control::PipelineControlSignal, + ) -> Result<(), String> { + use kameo::prelude::*; + + const WINDOW_DURATION: Duration = Duration::from_secs(3); + const LOG_INTERVAL: Duration = Duration::from_secs(5); + const MAX_DROP_RATE_THRESHOLD: f64 = 0.25; + + let video_info = self.video_info; + let video_tx = self.video_tx.clone(); + let audio_tx = self.audio_tx.clone(); + + let start_time = self.start_time; + + let mut video_i = 0; + let mut audio_i = 0; + + let mut frames_dropped = 0; + + // Frame drop rate tracking state + use std::collections::VecDeque; + use std::time::{Duration, Instant}; + + struct FrameHandler { + capturer: WeakActorRef, + start_time: SystemTime, + frames_dropped: u32, + last_cleanup: Instant, + last_log: Instant, + frame_events: VecDeque<(Instant, bool)>, + video_tx: Sender<(ffmpeg::frame::Video, f64)>, + } + + impl Actor for FrameHandler { + type Args = Self; + type Error = (); + + async fn on_start( + args: Self::Args, + self_actor: ActorRef, + ) -> Result { + if let Some(capturer) = args.capturer.upgrade() { + self_actor.link(&capturer).await; + } + + Ok(args) + } + + async fn on_link_died( + &mut self, + actor_ref: WeakActorRef, + id: ActorID, + _: ActorStopReason, + ) -> Result, Self::Error> { + if self.capturer.id() == id + && let Some(self_actor) = actor_ref.upgrade() + { + let _ = self_actor.stop_gracefully().await; + + return Ok(std::ops::ControlFlow::Break(ActorStopReason::Normal)); + } + + Ok(std::ops::ControlFlow::Continue(())) + } + } + + impl FrameHandler { + // Helper function to clean up old frame events + fn cleanup_old_events(&mut self, now: Instant) { + let cutoff = now - WINDOW_DURATION; + while let Some(&(timestamp, _)) = self.frame_events.front() { + if timestamp < cutoff { + self.frame_events.pop_front(); + } else { + break; + } + } + } + + // Helper function to calculate current drop rate + fn calculate_drop_rate(&mut self) -> (f64, usize, usize) { + let now = Instant::now(); + self.cleanup_old_events(now); + + if self.frame_events.is_empty() { + return (0.0, 0, 0); + } + + let total_frames = self.frame_events.len(); + let dropped_frames = self + .frame_events + .iter() + .filter(|(_, dropped)| *dropped) + .count(); + let drop_rate = dropped_frames as f64 / total_frames as f64; + + (drop_rate, dropped_frames, total_frames) + } + } + + impl Message for FrameHandler { + type Reply = (); + + async fn handle( + &mut self, + mut msg: NewFrame, + ctx: &mut kameo::prelude::Context, + ) -> Self::Reply { + let Ok(elapsed) = msg.display_time.duration_since(self.start_time) else { + return; + }; + + msg.ff_frame.set_pts(Some( + (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64, + )); + + let now = Instant::now(); + let frame_dropped = match self + .video_tx + .try_send((msg.ff_frame, elapsed.as_secs_f64())) + { + Err(flume::TrySendError::Disconnected(_)) => { + warn!("Pipeline disconnected"); + let _ = ctx.actor_ref().stop_gracefully().await; + return; + } + Err(flume::TrySendError::Full(_)) => { + warn!("Screen capture sender is full, dropping frame"); + self.frames_dropped += 1; + true + } + _ => false, + }; + + self.frame_events.push_back((now, frame_dropped)); + + if now.duration_since(self.last_cleanup) > Duration::from_millis(100) { + self.cleanup_old_events(now); + self.last_cleanup = now; + } + + // Check drop rate and potentially exit + let (drop_rate, dropped_count, total_count) = self.calculate_drop_rate(); + + if drop_rate > MAX_DROP_RATE_THRESHOLD && total_count >= 10 { + error!( + "High frame drop rate detected: {:.1}% ({}/{} frames in last {}s). Exiting capture.", + drop_rate * 100.0, + dropped_count, + total_count, + WINDOW_DURATION.as_secs() + ); + return; + // return ControlFlow::Break(Err("Recording can't keep up with screen capture. Try reducing your display's resolution or refresh rate.".to_string())); + } + + // Periodic logging of drop rate + if now.duration_since(self.last_log) > LOG_INTERVAL && total_count > 0 { + info!( + "Frame drop rate: {:.1}% ({}/{} frames, total dropped: {})", + drop_rate * 100.0, + dropped_count, + total_count, + self.frames_dropped + ); + self.last_log = now; + } + } + } + + let config = self.config.clone(); + + let _ = self.tokio_handle.block_on(async move { + let capturer = WindowsScreenCapture::spawn(WindowsScreenCapture::new()); + + let stop_recipient = capturer.clone().reply_recipient::(); + + let frame_handler = FrameHandler::spawn(FrameHandler { + capturer: capturer.downgrade(), + video_tx, + start_time, + frame_events: Default::default(), + frames_dropped: Default::default(), + last_cleanup: Instant::now(), + last_log: Instant::now(), + }); + + let mut settings = scap_direct3d::Settings { + is_border_required: Some(false), + pixel_format: AVFrameCapture::PIXEL_FORMAT, + crop: config.crop_bounds.map(|b| { + let position = b.position(); + let size = b.size(); + + D3D11_BOX { + left: position.x() as u32, + top: position.y() as u32, + right: (position.x() + size.width()) as u32, + bottom: (position.y() + size.height()) as u32, + front: 0, + back: 1, + } + }), + ..Default::default() + }; + + let display = Display::from_id(&config.display).unwrap(); + + let capture_item = display.raw_handle().try_as_capture_item().unwrap(); + + settings.is_cursor_capture_enabled = Some(config.show_cursor); + + let _ = capturer + .ask(StartCapturing { + target: capture_item, + settings, + frame_handler: frame_handler.clone().recipient(), + }) + .send() + .await; + + let audio_capture = if let Some(audio_tx) = audio_tx { + let audio_capture = WindowsAudioCapture::spawn( + WindowsAudioCapture::new(audio_tx, start_time).unwrap(), + ); + + let _ = dbg!(audio_capture.ask(audio::StartCapturing).send().await); + + Some(audio_capture) + } else { + None + }; + + let _ = ready_signal.send(Ok(())); + + while let Ok(msg) = control_signal.receiver.recv_async().await { + if let Control::Shutdown = msg { + let _ = stop_recipient.ask(StopCapturing).await; + + if let Some(audio_capture) = audio_capture { + let _ = audio_capture.ask(StopCapturing).await; + } + + break; + } + } + }); + + Ok(()) + } +} + +#[derive(Actor)] +pub struct WindowsScreenCapture { + capture_handle: Option, +} + +impl WindowsScreenCapture { + pub fn new() -> Self { + Self { + capture_handle: None, + } + } +} + +pub struct StartCapturing { + pub target: GraphicsCaptureItem, + pub settings: scap_direct3d::Settings, + pub frame_handler: Recipient, + // error_handler: Option>, +} + +#[derive(Debug)] +pub enum StartCapturingError { + AlreadyCapturing, + Inner(scap_direct3d::StartCapturerError), +} + +pub struct NewFrame { + pub ff_frame: ffmpeg::frame::Video, + pub display_time: SystemTime, +} + +impl Message for WindowsScreenCapture { + type Reply = Result<(), StartCapturingError>; + + async fn handle( + &mut self, + msg: StartCapturing, + _: &mut Context, + ) -> Self::Reply { + if self.capture_handle.is_some() { + return Err(StartCapturingError::AlreadyCapturing); + } + + let capturer = scap_direct3d::Capturer::new(msg.target, msg.settings); + + let capture_handle = capturer + .start( + move |frame| { + let display_time = SystemTime::now(); + let ff_frame = frame.as_ffmpeg().unwrap(); + + // dbg!(ff_frame.width(), ff_frame.height()); + + let _ = msg + .frame_handler + .tell(NewFrame { + ff_frame, + display_time, + }) + .try_send(); + + Ok(()) + }, + || Ok(()), + ) + .map_err(StartCapturingError::Inner)?; + + self.capture_handle = Some(capture_handle); + + Ok(()) + } +} + +impl Message for WindowsScreenCapture { + type Reply = Result<(), StopCapturingError>; + + async fn handle( + &mut self, + msg: StopCapturing, + ctx: &mut Context, + ) -> Self::Reply { + let Some(capturer) = self.capture_handle.take() else { + return Err(StopCapturingError::NotCapturing); + }; + + println!("stopping windows capturer"); + if let Err(e) = capturer.stop() { + error!("Silently failed to stop Windows capturer: {}", e); + } + println!("stopped windows capturer"); + + Ok(()) + } +} + +use audio::WindowsAudioCapture; +pub mod audio { + use super::*; + use cpal::traits::StreamTrait; + use scap_cpal::*; + use scap_ffmpeg::*; + + #[derive(Actor)] + pub struct WindowsAudioCapture { + capturer: scap_cpal::Capturer, + } + + impl WindowsAudioCapture { + pub fn new( + audio_tx: Sender<(ffmpeg::frame::Audio, f64)>, + start_time: SystemTime, + ) -> Result { + let mut i = 0; + let capturer = scap_cpal::create_capturer( + move |data, _: &cpal::InputCallbackInfo, config| { + use scap_ffmpeg::*; + + let timestamp = SystemTime::now(); + let mut ff_frame = data.as_ffmpeg(config); + + let Ok(elapsed) = timestamp.duration_since(start_time) else { + warn!("Skipping audio frame {i} as elapsed time is invalid"); + return; + }; + + ff_frame.set_pts(Some( + (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64, + )); + + let _ = audio_tx.send((ff_frame, elapsed.as_secs_f64())); + i += 1; + }, + move |e| { + dbg!(e); + }, + )?; + + Ok(Self { capturer }) + } + } + + pub struct StartCapturing; + + impl Message for WindowsAudioCapture { + type Reply = Result<(), &'static str>; + + async fn handle( + &mut self, + msg: StartCapturing, + ctx: &mut Context, + ) -> Self::Reply { + self.capturer.play().map_err(|_| "failed to start stream")?; + + Ok(()) + } + } + + impl Message for WindowsAudioCapture { + type Reply = Result<(), &'static str>; + + async fn handle( + &mut self, + msg: StopCapturing, + ctx: &mut Context, + ) -> Self::Reply { + self.capturer.pause().map_err(|_| "failed to stop stream")?; + + Ok(()) + } + } +} From 98297d2cee0578a20a08aa61c495cbf6804df7a0 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 21 Aug 2025 23:58:18 +0800 Subject: [PATCH 30/47] fix macos --- crates/recording/src/sources/screen_capture/macos.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs index 81f9a5b216..3085732b51 100644 --- a/crates/recording/src/sources/screen_capture/macos.rs +++ b/crates/recording/src/sources/screen_capture/macos.rs @@ -1,10 +1,10 @@ use super::*; use cidre::*; +use kameo::prelude::*; #[derive(Debug)] pub struct CMSampleBufferCapture; -#[cfg(target_os = "macos")] impl ScreenCaptureFormat for CMSampleBufferCapture { type VideoFormat = cidre::arc::R; @@ -22,7 +22,6 @@ impl ScreenCaptureFormat for CMSampleBufferCapture { } } -#[cfg(target_os = "macos")] impl PipelineSourceTask for ScreenCaptureSource { fn run( &mut self, From 88a237fba4643bba57289a8b31787dd5d901894d Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 00:01:08 +0800 Subject: [PATCH 31/47] cargo fix --- apps/desktop/src-tauri/src/target_select_overlay.rs | 2 +- crates/cursor-capture/src/position.rs | 5 +---- crates/displays/src/lib.rs | 2 +- crates/displays/src/platform/macos.rs | 4 ++-- crates/recording/examples/recording-cli.rs | 3 +-- crates/recording/src/cursor.rs | 1 - crates/recording/src/sources/screen_capture/mod.rs | 4 ++-- 7 files changed, 8 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index ee0f1964dc..b355a4ea34 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -11,7 +11,7 @@ use crate::windows::{CapWindowId, ShowCapWindow}; use cap_displays::{ DisplayId, WindowId, bounds::{ - LogicalBounds, LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, + LogicalBounds, PhysicalSize, }, }; use serde::Serialize; diff --git a/crates/cursor-capture/src/position.rs b/crates/cursor-capture/src/position.rs index 8d9f75a92d..c5aab813fc 100644 --- a/crates/cursor-capture/src/position.rs +++ b/crates/cursor-capture/src/position.rs @@ -1,9 +1,6 @@ #[cfg(target_os = "windows")] use cap_displays::bounds::PhysicalBounds; -use cap_displays::{ - Display, - bounds::{LogicalBounds, PhysicalPosition, PhysicalSize}, -}; +use cap_displays::{Display, bounds::LogicalBounds}; use device_query::{DeviceQuery, DeviceState}; // Physical on Windows, Logical on macOS diff --git a/crates/displays/src/lib.rs b/crates/displays/src/lib.rs index 2c9bba51e8..cd1cc5fc93 100644 --- a/crates/displays/src/lib.rs +++ b/crates/displays/src/lib.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use specta::Type; use std::str::FromStr; -use crate::bounds::{LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition}; +use crate::bounds::{LogicalPosition, LogicalSize}; #[derive(Clone, Copy)] pub struct Display(DisplayImpl); diff --git a/crates/displays/src/platform/macos.rs b/crates/displays/src/platform/macos.rs index 1c59612d6c..901abec9cb 100644 --- a/crates/displays/src/platform/macos.rs +++ b/crates/displays/src/platform/macos.rs @@ -15,7 +15,7 @@ use core_graphics::{ }; use crate::bounds::{ - LogicalBounds, LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, + LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize, }; #[derive(Clone, Copy)] @@ -123,7 +123,7 @@ impl DisplayImpl { } pub fn name(&self) -> Option { - use cocoa::appkit::NSScreen; + use cocoa::base::id; use cocoa::foundation::NSString; use objc::{msg_send, *}; diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index cbbd341c40..d33854791f 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -1,10 +1,9 @@ use std::time::Duration; -use cap_displays::{Display, Window}; +use cap_displays::Display; use cap_recording::{ RecordingBaseInputs, screen_capture::ScreenCaptureTarget, - sources::{ScreenCaptureSource, list_displays, list_windows}, }; #[tokio::main] diff --git a/crates/recording/src/cursor.rs b/crates/recording/src/cursor.rs index bcc053b1f5..4bc817c5ed 100644 --- a/crates/recording/src/cursor.rs +++ b/crates/recording/src/cursor.rs @@ -1,6 +1,5 @@ use cap_cursor_capture::CursorCropBounds; use cap_cursor_info::CursorShape; -use cap_displays::bounds::{LogicalBounds, PhysicalBounds}; use cap_project::{CursorClickEvent, CursorMoveEvent, XY}; use std::{collections::HashMap, path::PathBuf, time::SystemTime}; use tokio::sync::oneshot; diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index aa17db85f5..13b2e82a0e 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -1,7 +1,7 @@ use cap_cursor_capture::CursorCropBounds; use cap_displays::{ Display, DisplayId, Window, WindowId, - bounds::{LogicalBounds, LogicalPosition, PhysicalBounds, PhysicalPosition, PhysicalSize}, + bounds::{LogicalBounds, LogicalPosition, PhysicalSize}, }; use cap_media_info::{AudioInfo, VideoInfo}; use ffmpeg::sys::AV_TIME_BASE_Q; @@ -9,7 +9,7 @@ use flume::Sender; use serde::{Deserialize, Serialize}; use specta::Type; use std::time::SystemTime; -use tracing::{error, info, warn}; +use tracing::{error, warn}; use crate::pipeline::{control::Control, task::PipelineSourceTask}; From bd5427d6d7ca0ec930d503542b7f4cef2458d084 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 03:02:42 +0800 Subject: [PATCH 32/47] rename Screen to Display --- apps/cli/src/record.rs | 2 +- .../desktop/src-tauri/src/deeplink_actions.rs | 2 +- apps/desktop/src-tauri/src/lib.rs | 2 +- apps/desktop/src-tauri/src/recording.rs | 9 +- .../src/routes/(window-chrome)/(main).tsx | 20 +- .../src/routes/(window-chrome)/new-main.tsx | 16 +- apps/desktop/src/routes/capture-area.tsx | 2 +- .../src/routes/target-select-overlay.tsx | 7 +- apps/desktop/src/utils/queries.ts | 4 +- apps/desktop/src/utils/tauri.ts | 2 +- crates/cursor-capture/src/position.rs | 4 +- crates/displays/src/platform/macos.rs | 5 +- crates/recording/examples/recording-cli.rs | 7 +- .../src/sources/screen_capture/macos.rs | 374 ++++++++------- .../src/sources/screen_capture/mod.rs | 23 +- .../src/sources/screen_capture/windows.rs | 449 +++++++++--------- packages/ui-solid/src/auto-imports.d.ts | 11 + 17 files changed, 470 insertions(+), 469 deletions(-) diff --git a/apps/cli/src/record.rs b/apps/cli/src/record.rs index d6fcb76d4e..b8f3952aea 100644 --- a/apps/cli/src/record.rs +++ b/apps/cli/src/record.rs @@ -33,7 +33,7 @@ impl RecordStart { (Some(id), _) => cap_recording::screen_capture::list_displays() .into_iter() .find(|s| s.0.id == id) - .map(|(s, _)| ScreenCaptureTarget::Screen { id: s.id }) + .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) .ok_or(format!("Screen with id '{id}' not found")), (_, Some(id)) => cap_recording::screen_capture::list_windows() .into_iter() diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 98dd8c0d70..c6e2f9296b 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -124,7 +124,7 @@ impl DeepLinkAction { CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() .into_iter() .find(|(s, _)| s.name == name) - .map(|(s, _)| ScreenCaptureTarget::Screen { id: s.id }) + .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) .ok_or(format!("No screen with name \"{}\"", &name))?, CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() .into_iter() diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 42a5a3430e..30c669fd47 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -468,7 +468,7 @@ async fn get_current_recording( .current_recording() .map(|r| { let target = match r.capture_target() { - ScreenCaptureTarget::Screen { id } => { + ScreenCaptureTarget::Display { id } => { CurrentRecordingTarget::Screen { id: id.clone() } } ScreenCaptureTarget::Window { id } => CurrentRecordingTarget::Window { diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index db98b95bed..21886eb6f7 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -121,13 +121,6 @@ impl InProgressRecording { Self::Studio { handle, .. } => handle.cancel().await, } } - - // pub fn bounds(&self) -> &Bounds { - // match self { - // Self::Instant { handle, .. } => &handle.bounds, - // Self::Studio { handle, .. } => &handle.bounds, - // } - // } } pub enum CompletedRecording { @@ -239,7 +232,7 @@ pub async fn start_recording( match inputs.capture_target.clone() { ScreenCaptureTarget::Area { .. } => title.unwrap_or_else(|| "Area".to_string()), ScreenCaptureTarget::Window { .. } => title.unwrap_or_else(|| "Window".to_string()), - ScreenCaptureTarget::Screen { .. } => title.unwrap_or_else(|| "Screen".to_string()), + ScreenCaptureTarget::Display { .. } => title.unwrap_or_else(|| "Screen".to_string()), } }; diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index afc4c7aa2d..a3133d9458 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -145,7 +145,7 @@ function Page() { screen: () => { let screen: CaptureDisplay | undefined; - if (rawOptions.captureTarget.variant === "screen") { + if (rawOptions.captureTarget.variant === "display") { const screenId = rawOptions.captureTarget.id; screen = _screens()?.find((s) => s.id === screenId) ?? _screens()?.[0]; } else if (rawOptions.captureTarget.variant === "area") { @@ -191,7 +191,7 @@ function Page() { ) { setOptions( "captureTarget", - reconcile({ variant: "screen", id: screen.id }), + reconcile({ variant: "display", id: screen.id }), ); } }); @@ -201,7 +201,7 @@ function Page() { if (!isRecording()) { const capture_target = ((): ScreenCaptureTarget => { switch (rawOptions.captureTarget.variant) { - case "screen": { + case "display": { const screen = options.screen(); if (!screen) throw new Error( @@ -209,7 +209,7 @@ function Page() { _screens()?.length }`, ); - return { variant: "screen", id: screen.id }; + return { variant: "display", id: screen.id }; } case "window": { const win = options.window(); @@ -378,7 +378,7 @@ function Page() { setOptions( "captureTarget", reconcile({ - variant: "screen", + variant: "display", id: screen.id, }), ); @@ -387,7 +387,7 @@ function Page() {
@@ -593,7 +593,7 @@ function createUpdateCheck() { } function AreaSelectButton(props: { - targetVariant: "screen" | "area" | "other"; + targetVariant: "display" | "area" | "other"; screen: CaptureDisplay | undefined; onChange(area?: number): void; }) { diff --git a/apps/desktop/src/routes/(window-chrome)/new-main.tsx b/apps/desktop/src/routes/(window-chrome)/new-main.tsx index 0fabc19f91..7e226b97af 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main.tsx @@ -146,7 +146,7 @@ function Page() { screen: () => { let screen; - if (rawOptions.captureTarget.variant === "screen") { + if (rawOptions.captureTarget.variant === "display") { const screenId = rawOptions.captureTarget.id; screen = screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0]; @@ -175,10 +175,10 @@ function Page() { micName: () => mics.data?.find((name) => name === rawOptions.micName), target: (): ScreenCaptureTarget | undefined => { switch (rawOptions.captureTarget.variant) { - case "screen": { + case "display": { const screen = options.screen(); if (!screen) return; - return { variant: "screen", id: screen.id }; + return { variant: "display", id: screen.id }; } case "window": { const window = options.window(); @@ -208,7 +208,7 @@ function Page() { if (target.variant === "window" && windows.data?.length === 0) { setOptions( "captureTarget", - reconcile({ variant: "screen", id: screen.id }), + reconcile({ variant: "display", id: screen.id }), ); } }); @@ -293,12 +293,14 @@ function Page() {
- setOptions("targetMode", (v) => (v === "screen" ? null : "screen")) + setOptions("targetMode", (v) => + v === "display" ? null : "display", + ) } - name="Screen" + name="display" /> - + {(_) => ( {(screenUnderCursor) => ( @@ -96,10 +96,7 @@ export default function () {
)} diff --git a/apps/desktop/src/utils/queries.ts b/apps/desktop/src/utils/queries.ts index 2d305b0670..2e0fa67f19 100644 --- a/apps/desktop/src/utils/queries.ts +++ b/apps/desktop/src/utils/queries.ts @@ -91,12 +91,12 @@ export function createOptionsQuery() { micName: string | null; mode: RecordingMode; captureSystemAudio?: boolean; - targetMode?: "screen" | "window" | "area" | null; + targetMode?: "display" | "window" | "area" | null; cameraID?: DeviceOrModelID | null; /** @deprecated */ cameraLabel: string | null; }>({ - captureTarget: { variant: "screen", id: "0" }, + captureTarget: { variant: "display", id: "0" }, micName: null, cameraLabel: null, mode: "studio", diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index b4645d7441..64f9d8003e 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -682,7 +682,7 @@ export type RequestStartRecording = null; export type S3UploadMeta = { id: string }; export type ScreenCaptureTarget = | { variant: "window"; id: WindowId } - | { variant: "screen"; id: DisplayId } + | { variant: "display"; id: DisplayId } | { variant: "area"; screen: DisplayId; bounds: LogicalBounds }; export type ScreenUnderCursor = { name: string; diff --git a/crates/cursor-capture/src/position.rs b/crates/cursor-capture/src/position.rs index c5aab813fc..b41248784c 100644 --- a/crates/cursor-capture/src/position.rs +++ b/crates/cursor-capture/src/position.rs @@ -1,6 +1,4 @@ -#[cfg(target_os = "windows")] -use cap_displays::bounds::PhysicalBounds; -use cap_displays::{Display, bounds::LogicalBounds}; +use cap_displays::{Display, bounds::*}; use device_query::{DeviceQuery, DeviceState}; // Physical on Windows, Logical on macOS diff --git a/crates/displays/src/platform/macos.rs b/crates/displays/src/platform/macos.rs index 901abec9cb..8aee9123a4 100644 --- a/crates/displays/src/platform/macos.rs +++ b/crates/displays/src/platform/macos.rs @@ -14,9 +14,7 @@ use core_graphics::{ }, }; -use crate::bounds::{ - LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize, -}; +use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize}; #[derive(Clone, Copy)] pub struct DisplayImpl(CGDisplay); @@ -123,7 +121,6 @@ impl DisplayImpl { } pub fn name(&self) -> Option { - use cocoa::base::id; use cocoa::foundation::NSString; use objc::{msg_send, *}; diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index d33854791f..138a451712 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -1,10 +1,7 @@ use std::time::Duration; use cap_displays::Display; -use cap_recording::{ - RecordingBaseInputs, - screen_capture::ScreenCaptureTarget, -}; +use cap_recording::{RecordingBaseInputs, screen_capture::ScreenCaptureTarget}; #[tokio::main] pub async fn main() { @@ -37,7 +34,7 @@ pub async fn main() { "test".to_string(), dir.path().into(), RecordingBaseInputs { - capture_target: ScreenCaptureTarget::Screen { + capture_target: ScreenCaptureTarget::Display { id: Display::list()[1].id(), }, // ScreenCaptureTarget::Window { diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs index 3085732b51..133bbfff9c 100644 --- a/crates/recording/src/sources/screen_capture/macos.rs +++ b/crates/recording/src/sources/screen_capture/macos.rs @@ -22,104 +22,109 @@ impl ScreenCaptureFormat for CMSampleBufferCapture { } } -impl PipelineSourceTask for ScreenCaptureSource { - fn run( +#[derive(Actor)] +struct FrameHandler { + start_time_unix: f64, + start_cmtime: f64, + start_time_f64: f64, + video_tx: Sender<(arc::R, f64)>, + audio_tx: Option>, +} + +impl Message for FrameHandler { + type Reply = (); + + async fn handle( &mut self, - ready_signal: crate::pipeline::task::PipelineReadySignal, - control_signal: crate::pipeline::control::PipelineControlSignal, - ) -> Result<(), String> { - use cidre::{arc, cg, cm, cv}; - use kameo::prelude::*; - - #[derive(Actor)] - struct FrameHandler { - start_time_unix: f64, - start_cmtime: f64, - start_time_f64: f64, - video_tx: Sender<(arc::R, f64)>, - audio_tx: Option>, - } + msg: NewFrame, + _: &mut kameo::prelude::Context, + ) -> Self::Reply { + let frame = msg.0; + let sample_buffer = frame.sample_buf(); - impl Message for FrameHandler { - type Reply = (); - - async fn handle( - &mut self, - msg: NewFrame, - _: &mut kameo::prelude::Context, - ) -> Self::Reply { - let frame = msg.0; - let sample_buffer = frame.sample_buf(); - - let frame_time = - sample_buffer.pts().value as f64 / sample_buffer.pts().scale as f64; - let unix_timestamp = self.start_time_unix + frame_time - self.start_cmtime; - let relative_time = unix_timestamp - self.start_time_f64; - - match &frame { - scap_screencapturekit::Frame::Screen(frame) => { - if frame.image_buf().height() == 0 || frame.image_buf().width() == 0 { - return; - } + let frame_time = sample_buffer.pts().value as f64 / sample_buffer.pts().scale as f64; + let unix_timestamp = self.start_time_unix + frame_time - self.start_cmtime; + let relative_time = unix_timestamp - self.start_time_f64; - let check_skip_send = || { - cap_fail::fail_err!("media::sources::screen_capture::skip_send", ()); + match &frame { + scap_screencapturekit::Frame::Screen(frame) => { + if frame.image_buf().height() == 0 || frame.image_buf().width() == 0 { + return; + } - Ok::<(), ()>(()) - }; + let check_skip_send = || { + cap_fail::fail_err!("media::sources::screen_capture::skip_send", ()); - if check_skip_send().is_ok() - && self - .video_tx - .send((sample_buffer.retained(), relative_time)) - .is_err() - { - warn!("Pipeline is unreachable"); - } - } - scap_screencapturekit::Frame::Audio(_) => { - use ffmpeg::ChannelLayout; - - let res = || { - cap_fail::fail_err!("screen_capture audio skip", ()); - Ok::<(), ()>(()) - }; - if res().is_err() { - return; - } + Ok::<(), ()>(()) + }; - let Some(audio_tx) = &self.audio_tx else { - return; - }; - - let buf_list = sample_buffer.audio_buf_list::<2>().unwrap(); - let slice = buf_list.block().as_slice().unwrap(); - - let mut frame = ffmpeg::frame::Audio::new( - ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), - sample_buffer.num_samples() as usize, - ChannelLayout::STEREO, - ); - frame.set_rate(48_000); - let data_bytes_size = buf_list.list().buffers[0].data_bytes_size; - for i in 0..frame.planes() { - use cap_media_info::PlanarData; - - frame.plane_data_mut(i).copy_from_slice( - &slice[i * data_bytes_size as usize - ..(i + 1) * data_bytes_size as usize], - ); - } + if check_skip_send().is_ok() + && self + .video_tx + .send((sample_buffer.retained(), relative_time)) + .is_err() + { + warn!("Pipeline is unreachable"); + } + } + scap_screencapturekit::Frame::Audio(_) => { + use ffmpeg::ChannelLayout; - frame.set_pts(Some((relative_time * AV_TIME_BASE_Q.den as f64) as i64)); + let res = || { + cap_fail::fail_err!("screen_capture audio skip", ()); + Ok::<(), ()>(()) + }; + if res().is_err() { + return; + } - let _ = audio_tx.send((frame, relative_time)); - } - _ => {} + let Some(audio_tx) = &self.audio_tx else { + return; + }; + + let buf_list = sample_buffer.audio_buf_list::<2>().unwrap(); + let slice = buf_list.block().as_slice().unwrap(); + + let mut frame = ffmpeg::frame::Audio::new( + ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), + sample_buffer.num_samples() as usize, + ChannelLayout::STEREO, + ); + frame.set_rate(48_000); + let data_bytes_size = buf_list.list().buffers[0].data_bytes_size; + for i in 0..frame.planes() { + use cap_media_info::PlanarData; + + frame.plane_data_mut(i).copy_from_slice( + &slice[i * data_bytes_size as usize..(i + 1) * data_bytes_size as usize], + ); } + + frame.set_pts(Some((relative_time * AV_TIME_BASE_Q.den as f64) as i64)); + + let _ = audio_tx.send((frame, relative_time)); } + _ => {} } + } +} +#[derive(Debug, thiserror::Error)] +enum SourceError { + #[error("NoDisplay: Id '{0}'")] + NoDisplay(DisplayId), + #[error("AsContentFilter")] + AsContentFilter, + #[error("DidStopWithError: {0}")] + DidStopWithError(arc::R), +} + +impl PipelineSourceTask for ScreenCaptureSource { + fn run( + &mut self, + ready_signal: crate::pipeline::task::PipelineReadySignal, + control_signal: crate::pipeline::control::PipelineControlSignal, + ) -> Result<(), String> { let start = std::time::SystemTime::now(); let start_time_unix = start .duration_since(std::time::UNIX_EPOCH) @@ -138,119 +143,122 @@ impl PipelineSourceTask for ScreenCaptureSource { let audio_tx = self.audio_tx.clone(); let config = self.config.clone(); - self.tokio_handle.block_on(async move { - let captures_audio = audio_tx.is_some(); - let frame_handler = FrameHandler::spawn(FrameHandler { - video_tx, - audio_tx, - start_time_unix, - start_cmtime, - start_time_f64, - }); + self.tokio_handle + .block_on(async move { + let captures_audio = audio_tx.is_some(); + let frame_handler = FrameHandler::spawn(FrameHandler { + video_tx, + audio_tx, + start_time_unix, + start_cmtime, + start_time_f64, + }); + + let display = Display::from_id(&config.display) + .ok_or_else(|| SourceError::NoDisplay(config.display))?; + + let content_filter = display + .raw_handle() + .as_content_filter() + .await + .ok_or_else(|| SourceError::AsContentFilter)?; + + let size = { + let logical_size = config + .crop_bounds + .map(|bounds| bounds.size()) + .or_else(|| display.logical_size()) + .unwrap(); + + let scale = display.physical_size().unwrap().width() + / display.logical_size().unwrap().width(); + + PhysicalSize::new(logical_size.width() * scale, logical_size.height() * scale) + }; - let display = Display::from_id(&config.display).unwrap(); - - let content_filter = display - .raw_handle() - .as_content_filter() - .await - .ok_or_else(|| "Failed to get content filter".to_string())?; - - let size = { - let logical_size = config - .crop_bounds - .map(|bounds| bounds.size()) - .or_else(|| display.logical_size()) - .unwrap(); - - let scale = display.physical_size().unwrap().width() - / display.logical_size().unwrap().width(); - - PhysicalSize::new(logical_size.width() * scale, logical_size.height() * scale) - }; - - tracing::info!("size: {:?}", size); - - let mut settings = scap_screencapturekit::StreamCfgBuilder::default() - .with_width(size.width() as usize) - .with_height(size.height() as usize) - .with_fps(config.fps as f32) - .with_shows_cursor(config.show_cursor) - .with_captures_audio(captures_audio) - .build(); - - settings.set_pixel_format(cv::PixelFormat::_32_BGRA); - - if let Some(crop_bounds) = config.crop_bounds { - tracing::info!("crop bounds: {:?}", crop_bounds); - settings.set_src_rect(cg::Rect::new( - crop_bounds.position().x(), - crop_bounds.position().y(), - crop_bounds.size().width(), - crop_bounds.size().height(), - )); - } + tracing::info!("size: {:?}", size); + + let mut settings = scap_screencapturekit::StreamCfgBuilder::default() + .with_width(size.width() as usize) + .with_height(size.height() as usize) + .with_fps(config.fps as f32) + .with_shows_cursor(config.show_cursor) + .with_captures_audio(captures_audio) + .build(); + + settings.set_pixel_format(cv::PixelFormat::_32_BGRA); + + if let Some(crop_bounds) = config.crop_bounds { + tracing::info!("crop bounds: {:?}", crop_bounds); + settings.set_src_rect(cg::Rect::new( + crop_bounds.position().x(), + crop_bounds.position().y(), + crop_bounds.size().width(), + crop_bounds.size().height(), + )); + } - let (error_tx, error_rx) = flume::bounded(1); + let (error_tx, error_rx) = flume::bounded(1); - let capturer = ScreenCaptureActor::spawn( - ScreenCaptureActor::new( - content_filter, - settings, - frame_handler.recipient(), - error_tx.clone(), - ) - .unwrap(), - ); + let capturer = ScreenCaptureActor::spawn( + ScreenCaptureActor::new( + content_filter, + settings, + frame_handler.recipient(), + error_tx.clone(), + ) + .unwrap(), + ); - let stop_recipient = capturer.clone().reply_recipient::(); + let stop_recipient = capturer.clone().reply_recipient::(); - let _ = capturer.ask(StartCapturing).send().await.unwrap(); + let _ = capturer.ask(StartCapturing).send().await.unwrap(); - let _ = ready_signal.send(Ok(())); + let _ = ready_signal.send(Ok(())); - loop { - use futures::future::Either; + loop { + use futures::future::Either; - let check_err = || { - use cidre::ns; + let check_err = || { + use cidre::ns; - Result::<_, arc::R>::Ok(cap_fail::fail_err!( - "macos screen capture startup error", - ns::Error::with_domain(ns::ErrorDomain::os_status(), 1, None) - )) - }; - if let Err(e) = check_err() { - let _ = error_tx.send(e); - } - - match futures::future::select( - error_rx.recv_async(), - control_signal.receiver.recv_async(), - ) - .await - { - Either::Left((Ok(error), _)) => { - error!("Error capturing screen: {}", error); - let _ = stop_recipient.ask(StopCapturing).await; - return Err(error.to_string()); + Result::<_, arc::R>::Ok(cap_fail::fail_err!( + "macos screen capture startup error", + ns::Error::with_domain(ns::ErrorDomain::os_status(), 1, None) + )) + }; + if let Err(e) = check_err() { + let _ = error_tx.send(e); } - Either::Right((Ok(ctrl), _)) => { - if let Control::Shutdown = ctrl { + + match futures::future::select( + error_rx.recv_async(), + control_signal.receiver.recv_async(), + ) + .await + { + Either::Left((Ok(error), _)) => { + error!("Error capturing screen: {}", error); let _ = stop_recipient.ask(StopCapturing).await; - return Ok(()); + return Err(SourceError::DidStopWithError(error)); } - } - _ => { - warn!("Screen capture recv channels shutdown, exiting."); + Either::Right((Ok(ctrl), _)) => { + if let Control::Shutdown = ctrl { + let _ = stop_recipient.ask(StopCapturing).await; + return Ok(()); + } + } + _ => { + warn!("Screen capture recv channels shutdown, exiting."); - let _ = stop_recipient.ask(StopCapturing).await; + let _ = stop_recipient.ask(StopCapturing).await; - return Ok(()); + return Ok(()); + } } } - } - }) + }) + .map_err(|e: SourceError| e.to_string()) } } diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index 13b2e82a0e..eb0db07af3 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -64,7 +64,7 @@ pub enum ScreenCaptureTarget { Window { id: WindowId, }, - Screen { + Display { id: DisplayId, }, Area { @@ -76,26 +76,15 @@ pub enum ScreenCaptureTarget { impl ScreenCaptureTarget { pub fn display(&self) -> Option { match self { - Self::Screen { id } => Display::from_id(id), + Self::Display { id } => Display::from_id(id), Self::Window { id } => Window::from_id(id).and_then(|w| w.display()), Self::Area { screen, .. } => Display::from_id(screen), } } - pub fn logical_bounds(&self) -> Option { - match self { - Self::Screen { id } => todo!(), // Display::from_id(id).map(|d| d.logical_bounds()), - Self::Window { id } => Some(LogicalBounds::new( - LogicalPosition::new(0.0, 0.0), - Window::from_id(id)?.raw_handle().logical_size()?, - )), - Self::Area { bounds, .. } => Some(*bounds), - } - } - pub fn cursor_crop(&self) -> Option { match self { - Self::Screen { .. } => { + Self::Display { .. } => { #[cfg(target_os = "macos")] { let display = self.display()?; @@ -180,7 +169,7 @@ impl ScreenCaptureTarget { pub fn physical_size(&self) -> Option { match self { - Self::Screen { id } => Display::from_id(id).and_then(|d| d.physical_size()), + Self::Display { id } => Display::from_id(id).and_then(|d| d.physical_size()), Self::Window { id } => Window::from_id(id).and_then(|w| w.physical_size()), Self::Area { bounds, .. } => { let display = self.display()?; @@ -197,7 +186,7 @@ impl ScreenCaptureTarget { pub fn title(&self) -> Option { match self { - Self::Screen { id } => Display::from_id(id).and_then(|d| d.name()), + Self::Display { id } => Display::from_id(id).and_then(|d| d.name()), Self::Window { id } => Window::from_id(id).and_then(|w| w.name()), Self::Area { screen, .. } => Display::from_id(screen).and_then(|d| d.name()), } @@ -291,7 +280,7 @@ impl ScreenCaptureSource { let fps = max_fps.min(display.refresh_rate() as u32); let crop_bounds = match target { - ScreenCaptureTarget::Screen { .. } => None, + ScreenCaptureTarget::Display { .. } => None, ScreenCaptureTarget::Window { id } => { let window = Window::from_id(&id).unwrap(); diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index 45eb208d59..a45921acbf 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -3,6 +3,11 @@ use ::windows::{Graphics::Capture::GraphicsCaptureItem, Win32::Graphics::Direct3 use cpal::traits::{DeviceTrait, HostTrait}; use kameo::prelude::*; use scap_ffmpeg::*; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; +use tracing::info; #[derive(Debug)] pub struct AVFrameCapture; @@ -31,260 +36,266 @@ impl ScreenCaptureFormat for AVFrameCapture { } } -impl PipelineSourceTask for ScreenCaptureSource { - // #[instrument(skip_all)] - fn run( - &mut self, - ready_signal: crate::pipeline::task::PipelineReadySignal, - control_signal: crate::pipeline::control::PipelineControlSignal, - ) -> Result<(), String> { - use kameo::prelude::*; - - const WINDOW_DURATION: Duration = Duration::from_secs(3); - const LOG_INTERVAL: Duration = Duration::from_secs(5); - const MAX_DROP_RATE_THRESHOLD: f64 = 0.25; - - let video_info = self.video_info; - let video_tx = self.video_tx.clone(); - let audio_tx = self.audio_tx.clone(); - - let start_time = self.start_time; +struct FrameHandler { + capturer: WeakActorRef, + start_time: SystemTime, + frames_dropped: u32, + last_cleanup: Instant, + last_log: Instant, + frame_events: VecDeque<(Instant, bool)>, + video_tx: Sender<(ffmpeg::frame::Video, f64)>, +} - let mut video_i = 0; - let mut audio_i = 0; +impl Actor for FrameHandler { + type Args = Self; + type Error = (); - let mut frames_dropped = 0; + async fn on_start(args: Self::Args, self_actor: ActorRef) -> Result { + if let Some(capturer) = args.capturer.upgrade() { + self_actor.link(&capturer).await; + } - // Frame drop rate tracking state - use std::collections::VecDeque; - use std::time::{Duration, Instant}; + Ok(args) + } - struct FrameHandler { - capturer: WeakActorRef, - start_time: SystemTime, - frames_dropped: u32, - last_cleanup: Instant, - last_log: Instant, - frame_events: VecDeque<(Instant, bool)>, - video_tx: Sender<(ffmpeg::frame::Video, f64)>, + async fn on_link_died( + &mut self, + actor_ref: WeakActorRef, + id: ActorID, + _: ActorStopReason, + ) -> Result, Self::Error> { + if self.capturer.id() == id + && let Some(self_actor) = actor_ref.upgrade() + { + let _ = self_actor.stop_gracefully().await; + + return Ok(std::ops::ControlFlow::Break(ActorStopReason::Normal)); } - impl Actor for FrameHandler { - type Args = Self; - type Error = (); - - async fn on_start( - args: Self::Args, - self_actor: ActorRef, - ) -> Result { - if let Some(capturer) = args.capturer.upgrade() { - self_actor.link(&capturer).await; - } + Ok(std::ops::ControlFlow::Continue(())) + } +} - Ok(args) +impl FrameHandler { + // Helper function to clean up old frame events + fn cleanup_old_events(&mut self, now: Instant) { + let cutoff = now - WINDOW_DURATION; + while let Some(&(timestamp, _)) = self.frame_events.front() { + if timestamp < cutoff { + self.frame_events.pop_front(); + } else { + break; } + } + } - async fn on_link_died( - &mut self, - actor_ref: WeakActorRef, - id: ActorID, - _: ActorStopReason, - ) -> Result, Self::Error> { - if self.capturer.id() == id - && let Some(self_actor) = actor_ref.upgrade() - { - let _ = self_actor.stop_gracefully().await; - - return Ok(std::ops::ControlFlow::Break(ActorStopReason::Normal)); - } + // Helper function to calculate current drop rate + fn calculate_drop_rate(&mut self) -> (f64, usize, usize) { + let now = Instant::now(); + self.cleanup_old_events(now); - Ok(std::ops::ControlFlow::Continue(())) - } + if self.frame_events.is_empty() { + return (0.0, 0, 0); } - impl FrameHandler { - // Helper function to clean up old frame events - fn cleanup_old_events(&mut self, now: Instant) { - let cutoff = now - WINDOW_DURATION; - while let Some(&(timestamp, _)) = self.frame_events.front() { - if timestamp < cutoff { - self.frame_events.pop_front(); - } else { - break; - } - } - } + let total_frames = self.frame_events.len(); + let dropped_frames = self + .frame_events + .iter() + .filter(|(_, dropped)| *dropped) + .count(); + let drop_rate = dropped_frames as f64 / total_frames as f64; - // Helper function to calculate current drop rate - fn calculate_drop_rate(&mut self) -> (f64, usize, usize) { - let now = Instant::now(); - self.cleanup_old_events(now); + (drop_rate, dropped_frames, total_frames) + } +} - if self.frame_events.is_empty() { - return (0.0, 0, 0); - } +impl Message for FrameHandler { + type Reply = (); - let total_frames = self.frame_events.len(); - let dropped_frames = self - .frame_events - .iter() - .filter(|(_, dropped)| *dropped) - .count(); - let drop_rate = dropped_frames as f64 / total_frames as f64; + async fn handle( + &mut self, + mut msg: NewFrame, + ctx: &mut kameo::prelude::Context, + ) -> Self::Reply { + let Ok(elapsed) = msg.display_time.duration_since(self.start_time) else { + return; + }; - (drop_rate, dropped_frames, total_frames) + msg.ff_frame.set_pts(Some( + (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64, + )); + + let now = Instant::now(); + let frame_dropped = match self + .video_tx + .try_send((msg.ff_frame, elapsed.as_secs_f64())) + { + Err(flume::TrySendError::Disconnected(_)) => { + warn!("Pipeline disconnected"); + let _ = ctx.actor_ref().stop_gracefully().await; + return; } + Err(flume::TrySendError::Full(_)) => { + warn!("Screen capture sender is full, dropping frame"); + self.frames_dropped += 1; + true + } + _ => false, + }; + + self.frame_events.push_back((now, frame_dropped)); + + if now.duration_since(self.last_cleanup) > Duration::from_millis(100) { + self.cleanup_old_events(now); + self.last_cleanup = now; } - impl Message for FrameHandler { - type Reply = (); + // Check drop rate and potentially exit + let (drop_rate, dropped_count, total_count) = self.calculate_drop_rate(); + + if drop_rate > MAX_DROP_RATE_THRESHOLD && total_count >= 10 { + error!( + "High frame drop rate detected: {:.1}% ({}/{} frames in last {}s). Exiting capture.", + drop_rate * 100.0, + dropped_count, + total_count, + WINDOW_DURATION.as_secs() + ); + return; + // return ControlFlow::Break(Err("Recording can't keep up with screen capture. Try reducing your display's resolution or refresh rate.".to_string())); + } - async fn handle( - &mut self, - mut msg: NewFrame, - ctx: &mut kameo::prelude::Context, - ) -> Self::Reply { - let Ok(elapsed) = msg.display_time.duration_since(self.start_time) else { - return; - }; + // Periodic logging of drop rate + if now.duration_since(self.last_log) > LOG_INTERVAL && total_count > 0 { + info!( + "Frame drop rate: {:.1}% ({}/{} frames, total dropped: {})", + drop_rate * 100.0, + dropped_count, + total_count, + self.frames_dropped + ); + self.last_log = now; + } + } +} - msg.ff_frame.set_pts(Some( - (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64, - )); - - let now = Instant::now(); - let frame_dropped = match self - .video_tx - .try_send((msg.ff_frame, elapsed.as_secs_f64())) - { - Err(flume::TrySendError::Disconnected(_)) => { - warn!("Pipeline disconnected"); - let _ = ctx.actor_ref().stop_gracefully().await; - return; - } - Err(flume::TrySendError::Full(_)) => { - warn!("Screen capture sender is full, dropping frame"); - self.frames_dropped += 1; - true - } - _ => false, - }; +#[derive(Clone, Debug, thiserror::Error)] +enum SourceError { + #[error("NoDisplay: Id '{0}'")] + NoDisplay(DisplayId), + #[error("AsCaptureItem: {0}")] + AsCaptureItem(windows::core::Error), +} - self.frame_events.push_back((now, frame_dropped)); +impl PipelineSourceTask for ScreenCaptureSource { + // #[instrument(skip_all)] + fn run( + &mut self, + ready_signal: crate::pipeline::task::PipelineReadySignal, + control_signal: crate::pipeline::control::PipelineControlSignal, + ) -> Result<(), String> { + const WINDOW_DURATION: Duration = Duration::from_secs(3); + const LOG_INTERVAL: Duration = Duration::from_secs(5); + const MAX_DROP_RATE_THRESHOLD: f64 = 0.25; - if now.duration_since(self.last_cleanup) > Duration::from_millis(100) { - self.cleanup_old_events(now); - self.last_cleanup = now; - } + let video_info = self.video_info; + let video_tx = self.video_tx.clone(); + let audio_tx = self.audio_tx.clone(); - // Check drop rate and potentially exit - let (drop_rate, dropped_count, total_count) = self.calculate_drop_rate(); + let start_time = self.start_time; - if drop_rate > MAX_DROP_RATE_THRESHOLD && total_count >= 10 { - error!( - "High frame drop rate detected: {:.1}% ({}/{} frames in last {}s). Exiting capture.", - drop_rate * 100.0, - dropped_count, - total_count, - WINDOW_DURATION.as_secs() - ); - return; - // return ControlFlow::Break(Err("Recording can't keep up with screen capture. Try reducing your display's resolution or refresh rate.".to_string())); - } + let mut video_i = 0; + let mut audio_i = 0; - // Periodic logging of drop rate - if now.duration_since(self.last_log) > LOG_INTERVAL && total_count > 0 { - info!( - "Frame drop rate: {:.1}% ({}/{} frames, total dropped: {})", - drop_rate * 100.0, - dropped_count, - total_count, - self.frames_dropped - ); - self.last_log = now; - } - } - } + let mut frames_dropped = 0; + // Frame drop rate tracking state let config = self.config.clone(); - let _ = self.tokio_handle.block_on(async move { - let capturer = WindowsScreenCapture::spawn(WindowsScreenCapture::new()); - - let stop_recipient = capturer.clone().reply_recipient::(); - - let frame_handler = FrameHandler::spawn(FrameHandler { - capturer: capturer.downgrade(), - video_tx, - start_time, - frame_events: Default::default(), - frames_dropped: Default::default(), - last_cleanup: Instant::now(), - last_log: Instant::now(), - }); - - let mut settings = scap_direct3d::Settings { - is_border_required: Some(false), - pixel_format: AVFrameCapture::PIXEL_FORMAT, - crop: config.crop_bounds.map(|b| { - let position = b.position(); - let size = b.size(); - - D3D11_BOX { - left: position.x() as u32, - top: position.y() as u32, - right: (position.x() + size.width()) as u32, - bottom: (position.y() + size.height()) as u32, - front: 0, - back: 1, - } - }), - ..Default::default() - }; + self.tokio_handle + .block_on(async move { + let capturer = WindowsScreenCapture::spawn(WindowsScreenCapture::new()); + + let stop_recipient = capturer.clone().reply_recipient::(); + + let frame_handler = FrameHandler::spawn(FrameHandler { + capturer: capturer.downgrade(), + video_tx, + start_time, + frame_events: Default::default(), + frames_dropped: Default::default(), + last_cleanup: Instant::now(), + last_log: Instant::now(), + }); + + let mut settings = scap_direct3d::Settings { + is_border_required: Some(false), + pixel_format: AVFrameCapture::PIXEL_FORMAT, + crop: config.crop_bounds.map(|b| { + let position = b.position(); + let size = b.size(); + + D3D11_BOX { + left: position.x() as u32, + top: position.y() as u32, + right: (position.x() + size.width()) as u32, + bottom: (position.y() + size.height()) as u32, + front: 0, + back: 1, + } + }), + ..Default::default() + }; - let display = Display::from_id(&config.display).unwrap(); + let display = Display::from_id(&config.display) + .ok_or_else(|| SourceError::NoDisplay(config.display))?; - let capture_item = display.raw_handle().try_as_capture_item().unwrap(); + let capture_item = display + .raw_handle() + .try_as_capture_item() + .map_err(SourceError::AsCaptureItem); - settings.is_cursor_capture_enabled = Some(config.show_cursor); + settings.is_cursor_capture_enabled = Some(config.show_cursor); - let _ = capturer - .ask(StartCapturing { - target: capture_item, - settings, - frame_handler: frame_handler.clone().recipient(), - }) - .send() - .await; + let _ = capturer + .ask(StartCapturing { + target: capture_item, + settings, + frame_handler: frame_handler.clone().recipient(), + }) + .send() + .await; - let audio_capture = if let Some(audio_tx) = audio_tx { - let audio_capture = WindowsAudioCapture::spawn( - WindowsAudioCapture::new(audio_tx, start_time).unwrap(), - ); + let audio_capture = if let Some(audio_tx) = audio_tx { + let audio_capture = WindowsAudioCapture::spawn( + WindowsAudioCapture::new(audio_tx, start_time).unwrap(), + ); - let _ = dbg!(audio_capture.ask(audio::StartCapturing).send().await); + let _ = audio_capture.ask(audio::StartCapturing).send().await; - Some(audio_capture) - } else { - None - }; + Some(audio_capture) + } else { + None + }; - let _ = ready_signal.send(Ok(())); + let _ = ready_signal.send(Ok(())); - while let Ok(msg) = control_signal.receiver.recv_async().await { - if let Control::Shutdown = msg { - let _ = stop_recipient.ask(StopCapturing).await; + while let Ok(msg) = control_signal.receiver.recv_async().await { + if let Control::Shutdown = msg { + let _ = stop_recipient.ask(StopCapturing).await; - if let Some(audio_capture) = audio_capture { - let _ = audio_capture.ask(StopCapturing).await; - } + if let Some(audio_capture) = audio_capture { + let _ = audio_capture.ask(StopCapturing).await; + } - break; + break; + } } - } - }); - Ok(()) + Ok::<_, SourceError>(()) + }) + .map_err(|e| e.to_string()) } } @@ -339,8 +350,6 @@ impl Message for WindowsScreenCapture { let display_time = SystemTime::now(); let ff_frame = frame.as_ffmpeg().unwrap(); - // dbg!(ff_frame.width(), ff_frame.height()); - let _ = msg .frame_handler .tell(NewFrame { @@ -373,11 +382,11 @@ impl Message for WindowsScreenCapture { return Err(StopCapturingError::NotCapturing); }; - println!("stopping windows capturer"); if let Err(e) = capturer.stop() { error!("Silently failed to stop Windows capturer: {}", e); } - println!("stopped windows capturer"); + + info!("stopped windows capturer"); Ok(()) } @@ -432,28 +441,28 @@ pub mod audio { pub struct StartCapturing; impl Message for WindowsAudioCapture { - type Reply = Result<(), &'static str>; + type Reply = Result<(), PlayStreamError>; async fn handle( &mut self, msg: StartCapturing, ctx: &mut Context, ) -> Self::Reply { - self.capturer.play().map_err(|_| "failed to start stream")?; + self.capturer.play()?; Ok(()) } } impl Message for WindowsAudioCapture { - type Reply = Result<(), &'static str>; + type Reply = Result<(), PauseStreamError>; async fn handle( &mut self, msg: StopCapturing, ctx: &mut Context, ) -> Self::Reply { - self.capturer.pause().map_err(|_| "failed to stop stream")?; + self.capturer.pause()?; Ok(()) } diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index fc5c05c352..7f6519957b 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -6,11 +6,13 @@ // biome-ignore lint: disable export {}; declare global { + const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"]; const IconCapAudioOn: typeof import("~icons/cap/audio-on.jsx")["default"]; const IconCapBgBlur: typeof import("~icons/cap/bg-blur.jsx")["default"]; const IconCapCamera: typeof import("~icons/cap/camera.jsx")["default"]; const IconCapCaptions: typeof import("~icons/cap/captions.jsx")["default"]; const IconCapChevronDown: typeof import("~icons/cap/chevron-down.jsx")["default"]; + const IconCapCircle: typeof import("~icons/cap/circle.jsx")["default"]; const IconCapCircleCheck: typeof import("~icons/cap/circle-check.jsx")["default"]; const IconCapCirclePlus: typeof import("~icons/cap/circle-plus.jsx")["default"]; const IconCapCircleX: typeof import("~icons/cap/circle-x.jsx")["default"]; @@ -18,6 +20,7 @@ declare global { const IconCapCorners: typeof import("~icons/cap/corners.jsx")["default"]; const IconCapCrop: typeof import("~icons/cap/crop.jsx")["default"]; const IconCapCursor: typeof import("~icons/cap/cursor.jsx")["default"]; + const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"]; const IconCapEnlarge: typeof import("~icons/cap/enlarge.jsx")["default"]; const IconCapFile: typeof import("~icons/cap/file.jsx")["default"]; const IconCapFilmCut: typeof import("~icons/cap/film-cut.jsx")["default"]; @@ -47,18 +50,22 @@ declare global { const IconCapScissors: typeof import("~icons/cap/scissors.jsx")["default"]; const IconCapSettings: typeof import("~icons/cap/settings.jsx")["default"]; const IconCapShadow: typeof import("~icons/cap/shadow.jsx")["default"]; + const IconCapSquare: typeof import("~icons/cap/square.jsx")["default"]; const IconCapStopCircle: typeof import("~icons/cap/stop-circle.jsx")["default"]; const IconCapTrash: typeof import("~icons/cap/trash.jsx")["default"]; const IconCapUndo: typeof import("~icons/cap/undo.jsx")["default"]; + const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"]; const IconCapZoomIn: typeof import("~icons/cap/zoom-in.jsx")["default"]; const IconCapZoomOut: typeof import("~icons/cap/zoom-out.jsx")["default"]; const IconHugeiconsEaseCurveControlPoints: typeof import("~icons/hugeicons/ease-curve-control-points.jsx")["default"]; + const IconLucideAppWindowMac: typeof import("~icons/lucide/app-window-mac.jsx")["default"]; const IconLucideBell: typeof import("~icons/lucide/bell.jsx")["default"]; const IconLucideBug: typeof import("~icons/lucide/bug.jsx")["default"]; const IconLucideCheck: typeof import("~icons/lucide/check.jsx")["default"]; const IconLucideClock: typeof import("~icons/lucide/clock.jsx")["default"]; const IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"]; const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"]; + const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"]; const IconLucideFolder: typeof import("~icons/lucide/folder.jsx")["default"]; const IconLucideGift: typeof import("~icons/lucide/gift.jsx")["default"]; const IconLucideHardDrive: typeof import("~icons/lucide/hard-drive.jsx")["default"]; @@ -66,10 +73,14 @@ declare global { const IconLucideMessageSquarePlus: typeof import("~icons/lucide/message-square-plus.jsx")["default"]; const IconLucideMicOff: typeof import("~icons/lucide/mic-off.jsx")["default"]; const IconLucideMonitor: typeof import("~icons/lucide/monitor.jsx")["default"]; + const IconLucideRectangleHorizontal: typeof import("~icons/lucide/rectangle-horizontal.jsx")["default"]; const IconLucideRotateCcw: typeof import("~icons/lucide/rotate-ccw.jsx")["default"]; const IconLucideSearch: typeof import("~icons/lucide/search.jsx")["default"]; const IconLucideSquarePlay: typeof import("~icons/lucide/square-play.jsx")["default"]; const IconLucideUnplug: typeof import("~icons/lucide/unplug.jsx")["default"]; const IconLucideVolume2: typeof import("~icons/lucide/volume2.jsx")["default"]; + const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"]; + const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"]; + const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"]; const IconPhMonitorBold: typeof import("~icons/ph/monitor-bold.jsx")["default"]; } From 2ba98fa6a56d2900eff26eaa923afcfa12b84a88 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 03:17:47 +0800 Subject: [PATCH 33/47] update tauri opener --- Cargo.lock | 6 +- apps/desktop/package.json | 2 +- apps/desktop/src/routes/editor/Header.tsx | 5 +- apps/desktop/src/utils/tauri.ts | 1228 ++++++----------- crates/displays/src/main.rs | 282 ++-- .../src/sources/screen_capture/mod.rs | 6 - packages/ui-solid/src/auto-imports.d.ts | 156 +-- pnpm-lock.yaml | 146 +- 8 files changed, 770 insertions(+), 1061 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c5e98db86..1fea080414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3774,7 +3774,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.58.0", ] [[package]] @@ -4436,7 +4436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-targets 0.48.5", ] [[package]] @@ -10147,7 +10147,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 126dd75d4b..ec1aeeefbe 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -45,7 +45,7 @@ "@tauri-apps/plugin-fs": "^2.4.1", "@tauri-apps/plugin-http": "^2.5.1", "@tauri-apps/plugin-notification": "^2.3.0", - "@tauri-apps/plugin-opener": "^2.4.0", + "@tauri-apps/plugin-opener": "^2.5.0", "@tauri-apps/plugin-os": "^2.3.0", "@tauri-apps/plugin-process": "2.3.0", "@tauri-apps/plugin-shell": "^2.3.0", diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index ffd30fe0fe..65a85a12bb 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -86,7 +86,10 @@ export function Header() { leftIcon={} /> revealItemInDir(`${editorInstance.path}/`)} + onClick={() => { + console.log({ path: `${editorInstance.path}/` }); + revealItemInDir(`${editorInstance.path}/`); + }} tooltipText="Open recording bundle" leftIcon={} /> diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 64f9d8003e..637a72250f 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1,810 +1,463 @@ + // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. /** user-defined commands **/ + export const commands = { - async setMicInput(label: string | null): Promise { - return await TAURI_INVOKE("set_mic_input", { label }); - }, - async setCameraInput(id: DeviceOrModelID | null): Promise { - return await TAURI_INVOKE("set_camera_input", { id }); - }, - async startRecording(inputs: StartRecordingInputs): Promise { - return await TAURI_INVOKE("start_recording", { inputs }); - }, - async stopRecording(): Promise { - return await TAURI_INVOKE("stop_recording"); - }, - async pauseRecording(): Promise { - return await TAURI_INVOKE("pause_recording"); - }, - async resumeRecording(): Promise { - return await TAURI_INVOKE("resume_recording"); - }, - async restartRecording(): Promise { - return await TAURI_INVOKE("restart_recording"); - }, - async deleteRecording(): Promise { - return await TAURI_INVOKE("delete_recording"); - }, - async listCameras(): Promise { - return await TAURI_INVOKE("list_cameras"); - }, - async listCaptureWindows(): Promise { - return await TAURI_INVOKE("list_capture_windows"); - }, - async listCaptureDisplays(): Promise { - return await TAURI_INVOKE("list_capture_displays"); - }, - async takeScreenshot(): Promise { - return await TAURI_INVOKE("take_screenshot"); - }, - async listAudioDevices(): Promise { - return await TAURI_INVOKE("list_audio_devices"); - }, - async closeRecordingsOverlayWindow(): Promise { - await TAURI_INVOKE("close_recordings_overlay_window"); - }, - async setFakeWindowBounds( - name: string, - bounds: LogicalBounds, - ): Promise { - return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); - }, - async removeFakeWindow(name: string): Promise { - return await TAURI_INVOKE("remove_fake_window", { name }); - }, - async focusCapturesPanel(): Promise { - await TAURI_INVOKE("focus_captures_panel"); - }, - async getCurrentRecording(): Promise> { - return await TAURI_INVOKE("get_current_recording"); - }, - async exportVideo( - projectPath: string, - progress: TAURI_CHANNEL, - settings: ExportSettings, - ): Promise { - return await TAURI_INVOKE("export_video", { - projectPath, - progress, - settings, - }); - }, - async getExportEstimates( - path: string, - resolution: XY, - fps: number, - ): Promise { - return await TAURI_INVOKE("get_export_estimates", { - path, - resolution, - fps, - }); - }, - async copyFileToPath(src: string, dst: string): Promise { - return await TAURI_INVOKE("copy_file_to_path", { src, dst }); - }, - async copyVideoToClipboard(path: string): Promise { - return await TAURI_INVOKE("copy_video_to_clipboard", { path }); - }, - async copyScreenshotToClipboard(path: string): Promise { - return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); - }, - async openFilePath(path: string): Promise { - return await TAURI_INVOKE("open_file_path", { path }); - }, - async getVideoMetadata(path: string): Promise { - return await TAURI_INVOKE("get_video_metadata", { path }); - }, - async createEditorInstance(): Promise { - return await TAURI_INVOKE("create_editor_instance"); - }, - async getMicWaveforms(): Promise { - return await TAURI_INVOKE("get_mic_waveforms"); - }, - async getSystemAudioWaveforms(): Promise { - return await TAURI_INVOKE("get_system_audio_waveforms"); - }, - async startPlayback(fps: number, resolutionBase: XY): Promise { - return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); - }, - async stopPlayback(): Promise { - return await TAURI_INVOKE("stop_playback"); - }, - async setPlayheadPosition(frameNumber: number): Promise { - return await TAURI_INVOKE("set_playhead_position", { frameNumber }); - }, - async setProjectConfig(config: ProjectConfiguration): Promise { - return await TAURI_INVOKE("set_project_config", { config }); - }, - async generateZoomSegmentsFromClicks(): Promise { - return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); - }, - async openPermissionSettings(permission: OSPermission): Promise { - await TAURI_INVOKE("open_permission_settings", { permission }); - }, - async doPermissionsCheck(initialCheck: boolean): Promise { - return await TAURI_INVOKE("do_permissions_check", { initialCheck }); - }, - async requestPermission(permission: OSPermission): Promise { - await TAURI_INVOKE("request_permission", { permission }); - }, - async uploadExportedVideo( - path: string, - mode: UploadMode, - ): Promise { - return await TAURI_INVOKE("upload_exported_video", { path, mode }); - }, - async uploadScreenshot(screenshotPath: string): Promise { - return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); - }, - async getRecordingMeta( - path: string, - fileType: FileType, - ): Promise { - return await TAURI_INVOKE("get_recording_meta", { path, fileType }); - }, - async saveFileDialog( - fileName: string, - fileType: string, - ): Promise { - return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); - }, - async listRecordings(): Promise<[string, RecordingMetaWithType][]> { - return await TAURI_INVOKE("list_recordings"); - }, - async listScreenshots(): Promise<[string, RecordingMeta][]> { - return await TAURI_INVOKE("list_screenshots"); - }, - async checkUpgradedAndUpdate(): Promise { - return await TAURI_INVOKE("check_upgraded_and_update"); - }, - async openExternalLink(url: string): Promise { - return await TAURI_INVOKE("open_external_link", { url }); - }, - async setHotkey(action: HotkeyAction, hotkey: Hotkey | null): Promise { - return await TAURI_INVOKE("set_hotkey", { action, hotkey }); - }, - async resetCameraPermissions(): Promise { - return await TAURI_INVOKE("reset_camera_permissions"); - }, - async resetMicrophonePermissions(): Promise { - return await TAURI_INVOKE("reset_microphone_permissions"); - }, - async isCameraWindowOpen(): Promise { - return await TAURI_INVOKE("is_camera_window_open"); - }, - async seekTo(frameNumber: number): Promise { - return await TAURI_INVOKE("seek_to", { frameNumber }); - }, - async positionTrafficLights( - controlsInset: [number, number] | null, - ): Promise { - await TAURI_INVOKE("position_traffic_lights", { controlsInset }); - }, - async setTheme(theme: AppTheme): Promise { - await TAURI_INVOKE("set_theme", { theme }); - }, - async globalMessageDialog(message: string): Promise { - await TAURI_INVOKE("global_message_dialog", { message }); - }, - async showWindow(window: ShowCapWindow): Promise { - return await TAURI_INVOKE("show_window", { window }); - }, - async writeClipboardString(text: string): Promise { - return await TAURI_INVOKE("write_clipboard_string", { text }); - }, - async performHapticFeedback( - pattern: HapticPattern | null, - time: HapticPerformanceTime | null, - ): Promise { - return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); - }, - async listFails(): Promise<{ [key in string]: boolean }> { - return await TAURI_INVOKE("list_fails"); - }, - async setFail(name: string, value: boolean): Promise { - await TAURI_INVOKE("set_fail", { name, value }); - }, - async updateAuthPlan(): Promise { - await TAURI_INVOKE("update_auth_plan"); - }, - async setWindowTransparent(value: boolean): Promise { - await TAURI_INVOKE("set_window_transparent", { value }); - }, - async getEditorMeta(): Promise { - return await TAURI_INVOKE("get_editor_meta"); - }, - async setPrettyName(prettyName: string): Promise { - return await TAURI_INVOKE("set_pretty_name", { prettyName }); - }, - async setServerUrl(serverUrl: string): Promise { - return await TAURI_INVOKE("set_server_url", { serverUrl }); - }, - async setCameraPreviewState(state: CameraWindowState): Promise { - return await TAURI_INVOKE("set_camera_preview_state", { state }); - }, - async awaitCameraPreviewReady(): Promise { - return await TAURI_INVOKE("await_camera_preview_ready"); - }, - /** - * Function to handle creating directories for the model - */ - async createDir(path: string, recursive: boolean): Promise { - return await TAURI_INVOKE("create_dir", { path, recursive }); - }, - /** - * Function to save the model file - */ - async saveModelFile(path: string, data: number[]): Promise { - return await TAURI_INVOKE("save_model_file", { path, data }); - }, - /** - * Function to transcribe audio from a video file using Whisper - */ - async transcribeAudio( - videoPath: string, - modelPath: string, - language: string, - ): Promise { - return await TAURI_INVOKE("transcribe_audio", { - videoPath, - modelPath, - language, - }); - }, - /** - * Function to save caption data to a file - */ - async saveCaptions(videoId: string, captions: CaptionData): Promise { - return await TAURI_INVOKE("save_captions", { videoId, captions }); - }, - /** - * Function to load caption data from a file - */ - async loadCaptions(videoId: string): Promise { - return await TAURI_INVOKE("load_captions", { videoId }); - }, - /** - * Helper function to download a Whisper model from Hugging Face Hub - */ - async downloadWhisperModel( - modelName: string, - outputPath: string, - ): Promise { - return await TAURI_INVOKE("download_whisper_model", { - modelName, - outputPath, - }); - }, - /** - * Function to check if a model file exists - */ - async checkModelExists(modelPath: string): Promise { - return await TAURI_INVOKE("check_model_exists", { modelPath }); - }, - /** - * Function to delete a downloaded model - */ - async deleteWhisperModel(modelPath: string): Promise { - return await TAURI_INVOKE("delete_whisper_model", { modelPath }); - }, - /** - * Export captions to an SRT file - */ - async exportCaptionsSrt(videoId: string): Promise { - return await TAURI_INVOKE("export_captions_srt", { videoId }); - }, - async openTargetSelectOverlays(): Promise { - return await TAURI_INVOKE("open_target_select_overlays"); - }, - async closeTargetSelectOverlays(): Promise { - return await TAURI_INVOKE("close_target_select_overlays"); - }, -}; +async setMicInput(label: string | null) : Promise { + return await TAURI_INVOKE("set_mic_input", { label }); +}, +async setCameraInput(id: DeviceOrModelID | null) : Promise { + return await TAURI_INVOKE("set_camera_input", { id }); +}, +async startRecording(inputs: StartRecordingInputs) : Promise { + return await TAURI_INVOKE("start_recording", { inputs }); +}, +async stopRecording() : Promise { + return await TAURI_INVOKE("stop_recording"); +}, +async pauseRecording() : Promise { + return await TAURI_INVOKE("pause_recording"); +}, +async resumeRecording() : Promise { + return await TAURI_INVOKE("resume_recording"); +}, +async restartRecording() : Promise { + return await TAURI_INVOKE("restart_recording"); +}, +async deleteRecording() : Promise { + return await TAURI_INVOKE("delete_recording"); +}, +async listCameras() : Promise { + return await TAURI_INVOKE("list_cameras"); +}, +async listCaptureWindows() : Promise { + return await TAURI_INVOKE("list_capture_windows"); +}, +async listCaptureDisplays() : Promise { + return await TAURI_INVOKE("list_capture_displays"); +}, +async takeScreenshot() : Promise { + return await TAURI_INVOKE("take_screenshot"); +}, +async listAudioDevices() : Promise { + return await TAURI_INVOKE("list_audio_devices"); +}, +async closeRecordingsOverlayWindow() : Promise { + await TAURI_INVOKE("close_recordings_overlay_window"); +}, +async setFakeWindowBounds(name: string, bounds: LogicalBounds) : Promise { + return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); +}, +async removeFakeWindow(name: string) : Promise { + return await TAURI_INVOKE("remove_fake_window", { name }); +}, +async focusCapturesPanel() : Promise { + await TAURI_INVOKE("focus_captures_panel"); +}, +async getCurrentRecording() : Promise> { + return await TAURI_INVOKE("get_current_recording"); +}, +async exportVideo(projectPath: string, progress: TAURI_CHANNEL, settings: ExportSettings) : Promise { + return await TAURI_INVOKE("export_video", { projectPath, progress, settings }); +}, +async getExportEstimates(path: string, resolution: XY, fps: number) : Promise { + return await TAURI_INVOKE("get_export_estimates", { path, resolution, fps }); +}, +async copyFileToPath(src: string, dst: string) : Promise { + return await TAURI_INVOKE("copy_file_to_path", { src, dst }); +}, +async copyVideoToClipboard(path: string) : Promise { + return await TAURI_INVOKE("copy_video_to_clipboard", { path }); +}, +async copyScreenshotToClipboard(path: string) : Promise { + return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); +}, +async openFilePath(path: string) : Promise { + return await TAURI_INVOKE("open_file_path", { path }); +}, +async getVideoMetadata(path: string) : Promise { + return await TAURI_INVOKE("get_video_metadata", { path }); +}, +async createEditorInstance() : Promise { + return await TAURI_INVOKE("create_editor_instance"); +}, +async getMicWaveforms() : Promise { + return await TAURI_INVOKE("get_mic_waveforms"); +}, +async getSystemAudioWaveforms() : Promise { + return await TAURI_INVOKE("get_system_audio_waveforms"); +}, +async startPlayback(fps: number, resolutionBase: XY) : Promise { + return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); +}, +async stopPlayback() : Promise { + return await TAURI_INVOKE("stop_playback"); +}, +async setPlayheadPosition(frameNumber: number) : Promise { + return await TAURI_INVOKE("set_playhead_position", { frameNumber }); +}, +async setProjectConfig(config: ProjectConfiguration) : Promise { + return await TAURI_INVOKE("set_project_config", { config }); +}, +async generateZoomSegmentsFromClicks() : Promise { + return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); +}, +async openPermissionSettings(permission: OSPermission) : Promise { + await TAURI_INVOKE("open_permission_settings", { permission }); +}, +async doPermissionsCheck(initialCheck: boolean) : Promise { + return await TAURI_INVOKE("do_permissions_check", { initialCheck }); +}, +async requestPermission(permission: OSPermission) : Promise { + await TAURI_INVOKE("request_permission", { permission }); +}, +async uploadExportedVideo(path: string, mode: UploadMode) : Promise { + return await TAURI_INVOKE("upload_exported_video", { path, mode }); +}, +async uploadScreenshot(screenshotPath: string) : Promise { + return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); +}, +async getRecordingMeta(path: string, fileType: FileType) : Promise { + return await TAURI_INVOKE("get_recording_meta", { path, fileType }); +}, +async saveFileDialog(fileName: string, fileType: string) : Promise { + return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); +}, +async listRecordings() : Promise<([string, RecordingMetaWithType])[]> { + return await TAURI_INVOKE("list_recordings"); +}, +async listScreenshots() : Promise<([string, RecordingMeta])[]> { + return await TAURI_INVOKE("list_screenshots"); +}, +async checkUpgradedAndUpdate() : Promise { + return await TAURI_INVOKE("check_upgraded_and_update"); +}, +async openExternalLink(url: string) : Promise { + return await TAURI_INVOKE("open_external_link", { url }); +}, +async setHotkey(action: HotkeyAction, hotkey: Hotkey | null) : Promise { + return await TAURI_INVOKE("set_hotkey", { action, hotkey }); +}, +async resetCameraPermissions() : Promise { + return await TAURI_INVOKE("reset_camera_permissions"); +}, +async resetMicrophonePermissions() : Promise { + return await TAURI_INVOKE("reset_microphone_permissions"); +}, +async isCameraWindowOpen() : Promise { + return await TAURI_INVOKE("is_camera_window_open"); +}, +async seekTo(frameNumber: number) : Promise { + return await TAURI_INVOKE("seek_to", { frameNumber }); +}, +async positionTrafficLights(controlsInset: [number, number] | null) : Promise { + await TAURI_INVOKE("position_traffic_lights", { controlsInset }); +}, +async setTheme(theme: AppTheme) : Promise { + await TAURI_INVOKE("set_theme", { theme }); +}, +async globalMessageDialog(message: string) : Promise { + await TAURI_INVOKE("global_message_dialog", { message }); +}, +async showWindow(window: ShowCapWindow) : Promise { + return await TAURI_INVOKE("show_window", { window }); +}, +async writeClipboardString(text: string) : Promise { + return await TAURI_INVOKE("write_clipboard_string", { text }); +}, +async performHapticFeedback(pattern: HapticPattern | null, time: HapticPerformanceTime | null) : Promise { + return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); +}, +async listFails() : Promise<{ [key in string]: boolean }> { + return await TAURI_INVOKE("list_fails"); +}, +async setFail(name: string, value: boolean) : Promise { + await TAURI_INVOKE("set_fail", { name, value }); +}, +async updateAuthPlan() : Promise { + await TAURI_INVOKE("update_auth_plan"); +}, +async setWindowTransparent(value: boolean) : Promise { + await TAURI_INVOKE("set_window_transparent", { value }); +}, +async getEditorMeta() : Promise { + return await TAURI_INVOKE("get_editor_meta"); +}, +async setPrettyName(prettyName: string) : Promise { + return await TAURI_INVOKE("set_pretty_name", { prettyName }); +}, +async setServerUrl(serverUrl: string) : Promise { + return await TAURI_INVOKE("set_server_url", { serverUrl }); +}, +async setCameraPreviewState(state: CameraWindowState) : Promise { + return await TAURI_INVOKE("set_camera_preview_state", { state }); +}, +async awaitCameraPreviewReady() : Promise { + return await TAURI_INVOKE("await_camera_preview_ready"); +}, +/** + * Function to handle creating directories for the model + */ +async createDir(path: string, recursive: boolean) : Promise { + return await TAURI_INVOKE("create_dir", { path, recursive }); +}, +/** + * Function to save the model file + */ +async saveModelFile(path: string, data: number[]) : Promise { + return await TAURI_INVOKE("save_model_file", { path, data }); +}, +/** + * Function to transcribe audio from a video file using Whisper + */ +async transcribeAudio(videoPath: string, modelPath: string, language: string) : Promise { + return await TAURI_INVOKE("transcribe_audio", { videoPath, modelPath, language }); +}, +/** + * Function to save caption data to a file + */ +async saveCaptions(videoId: string, captions: CaptionData) : Promise { + return await TAURI_INVOKE("save_captions", { videoId, captions }); +}, +/** + * Function to load caption data from a file + */ +async loadCaptions(videoId: string) : Promise { + return await TAURI_INVOKE("load_captions", { videoId }); +}, +/** + * Helper function to download a Whisper model from Hugging Face Hub + */ +async downloadWhisperModel(modelName: string, outputPath: string) : Promise { + return await TAURI_INVOKE("download_whisper_model", { modelName, outputPath }); +}, +/** + * Function to check if a model file exists + */ +async checkModelExists(modelPath: string) : Promise { + return await TAURI_INVOKE("check_model_exists", { modelPath }); +}, +/** + * Function to delete a downloaded model + */ +async deleteWhisperModel(modelPath: string) : Promise { + return await TAURI_INVOKE("delete_whisper_model", { modelPath }); +}, +/** + * Export captions to an SRT file + */ +async exportCaptionsSrt(videoId: string) : Promise { + return await TAURI_INVOKE("export_captions_srt", { videoId }); +}, +async openTargetSelectOverlays() : Promise { + return await TAURI_INVOKE("open_target_select_overlays"); +}, +async closeTargetSelectOverlays() : Promise { + return await TAURI_INVOKE("close_target_select_overlays"); +} +} /** user-defined events **/ + export const events = __makeEvents__<{ - audioInputLevelChange: AudioInputLevelChange; - authenticationInvalid: AuthenticationInvalid; - currentRecordingChanged: CurrentRecordingChanged; - downloadProgress: DownloadProgress; - editorStateChanged: EditorStateChanged; - newNotification: NewNotification; - newScreenshotAdded: NewScreenshotAdded; - newStudioRecordingAdded: NewStudioRecordingAdded; - onEscapePress: OnEscapePress; - recordingDeleted: RecordingDeleted; - recordingEvent: RecordingEvent; - recordingOptionsChanged: RecordingOptionsChanged; - recordingStarted: RecordingStarted; - recordingStopped: RecordingStopped; - renderFrameEvent: RenderFrameEvent; - requestNewScreenshot: RequestNewScreenshot; - requestOpenSettings: RequestOpenSettings; - requestStartRecording: RequestStartRecording; - targetUnderCursor: TargetUnderCursor; - uploadProgress: UploadProgress; +audioInputLevelChange: AudioInputLevelChange, +authenticationInvalid: AuthenticationInvalid, +currentRecordingChanged: CurrentRecordingChanged, +downloadProgress: DownloadProgress, +editorStateChanged: EditorStateChanged, +newNotification: NewNotification, +newScreenshotAdded: NewScreenshotAdded, +newStudioRecordingAdded: NewStudioRecordingAdded, +onEscapePress: OnEscapePress, +recordingDeleted: RecordingDeleted, +recordingEvent: RecordingEvent, +recordingOptionsChanged: RecordingOptionsChanged, +recordingStarted: RecordingStarted, +recordingStopped: RecordingStopped, +renderFrameEvent: RenderFrameEvent, +requestNewScreenshot: RequestNewScreenshot, +requestOpenSettings: RequestOpenSettings, +requestStartRecording: RequestStartRecording, +targetUnderCursor: TargetUnderCursor, +uploadProgress: UploadProgress }>({ - audioInputLevelChange: "audio-input-level-change", - authenticationInvalid: "authentication-invalid", - currentRecordingChanged: "current-recording-changed", - downloadProgress: "download-progress", - editorStateChanged: "editor-state-changed", - newNotification: "new-notification", - newScreenshotAdded: "new-screenshot-added", - newStudioRecordingAdded: "new-studio-recording-added", - onEscapePress: "on-escape-press", - recordingDeleted: "recording-deleted", - recordingEvent: "recording-event", - recordingOptionsChanged: "recording-options-changed", - recordingStarted: "recording-started", - recordingStopped: "recording-stopped", - renderFrameEvent: "render-frame-event", - requestNewScreenshot: "request-new-screenshot", - requestOpenSettings: "request-open-settings", - requestStartRecording: "request-start-recording", - targetUnderCursor: "target-under-cursor", - uploadProgress: "upload-progress", -}); +audioInputLevelChange: "audio-input-level-change", +authenticationInvalid: "authentication-invalid", +currentRecordingChanged: "current-recording-changed", +downloadProgress: "download-progress", +editorStateChanged: "editor-state-changed", +newNotification: "new-notification", +newScreenshotAdded: "new-screenshot-added", +newStudioRecordingAdded: "new-studio-recording-added", +onEscapePress: "on-escape-press", +recordingDeleted: "recording-deleted", +recordingEvent: "recording-event", +recordingOptionsChanged: "recording-options-changed", +recordingStarted: "recording-started", +recordingStopped: "recording-stopped", +renderFrameEvent: "render-frame-event", +requestNewScreenshot: "request-new-screenshot", +requestOpenSettings: "request-open-settings", +requestStartRecording: "request-start-recording", +targetUnderCursor: "target-under-cursor", +uploadProgress: "upload-progress" +}) /** user-defined constants **/ + + /** user-defined types **/ -export type AppTheme = "system" | "light" | "dark"; -export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"; -export type Audio = { - duration: number; - sample_rate: number; - channels: number; - start_time: number; -}; -export type AudioConfiguration = { - mute: boolean; - improve: boolean; - micVolumeDb?: number; - micStereoMode?: StereoMode; - systemVolumeDb?: number; -}; -export type AudioInputLevelChange = number; -export type AudioMeta = { - path: string; - /** - * unix time of the first frame - */ - start_time?: number | null; -}; -export type AuthSecret = - | { api_key: string } - | { token: string; expires: number }; -export type AuthStore = { - secret: AuthSecret; - user_id: string | null; - plan: Plan | null; - intercom_hash: string | null; -}; -export type AuthenticationInvalid = null; -export type BackgroundConfiguration = { - source: BackgroundSource; - blur: number; - padding: number; - rounding: number; - inset: number; - crop: Crop | null; - shadow?: number; - advancedShadow?: ShadowConfiguration | null; -}; -export type BackgroundSource = - | { type: "wallpaper"; path: string | null } - | { type: "image"; path: string | null } - | { type: "color"; value: [number, number, number] } - | { - type: "gradient"; - from: [number, number, number]; - to: [number, number, number]; - angle?: number; - }; -export type Camera = { - hide: boolean; - mirror: boolean; - position: CameraPosition; - size: number; - zoom_size: number | null; - rounding?: number; - shadow?: number; - advanced_shadow?: ShadowConfiguration | null; - shape?: CameraShape; -}; -export type CameraInfo = { - device_id: string; - model_id: ModelIDType | null; - display_name: string; -}; -export type CameraPosition = { x: CameraXPosition; y: CameraYPosition }; -export type CameraPreviewShape = "round" | "square" | "full"; -export type CameraPreviewSize = "sm" | "lg"; -export type CameraShape = "square" | "source"; -export type CameraWindowState = { - size: CameraPreviewSize; - shape: CameraPreviewShape; - mirrored: boolean; -}; -export type CameraXPosition = "left" | "center" | "right"; -export type CameraYPosition = "top" | "bottom"; -export type CaptionData = { - segments: CaptionSegment[]; - settings: CaptionSettings | null; -}; -export type CaptionSegment = { - id: string; - start: number; - end: number; - text: string; -}; -export type CaptionSettings = { - enabled: boolean; - font: string; - size: number; - color: string; - backgroundColor: string; - backgroundOpacity: number; - position: string; - bold: boolean; - italic: boolean; - outline: boolean; - outlineColor: string; - exportWithSubtitles: boolean; -}; -export type CaptionsData = { - segments: CaptionSegment[]; - settings: CaptionSettings; -}; -export type CaptureDisplay = { - id: DisplayId; - name: string; - refresh_rate: number; -}; -export type CaptureWindow = { - id: WindowId; - owner_name: string; - name: string; - bounds: LogicalBounds; - refresh_rate: number; -}; -export type CommercialLicense = { - licenseKey: string; - expiryDate: number | null; - refresh: number; - activatedOn: number; -}; -export type Crop = { position: XY; size: XY }; -export type CurrentRecording = { - target: CurrentRecordingTarget; - type: RecordingType; -}; -export type CurrentRecordingChanged = null; -export type CurrentRecordingTarget = - | { window: { id: WindowId; bounds: LogicalBounds } } - | { screen: { id: DisplayId } } - | { area: { screen: DisplayId; bounds: LogicalBounds } }; -export type CursorAnimationStyle = "regular" | "slow" | "fast"; -export type CursorConfiguration = { - hide?: boolean; - hideWhenIdle: boolean; - size: number; - type: CursorType; - animationStyle: CursorAnimationStyle; - tension: number; - mass: number; - friction: number; - raw?: boolean; - motionBlur?: number; - useSvg?: boolean; -}; -export type CursorMeta = { - imagePath: string; - hotspot: XY; - shape?: string | null; -}; -export type CursorType = "pointer" | "circle"; -export type Cursors = - | { [key in string]: string } - | { [key in string]: CursorMeta }; -export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType }; -export type DisplayId = string; -export type DownloadProgress = { progress: number; message: string }; -export type EditorStateChanged = { playhead_position: number }; -export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato"; -export type ExportEstimates = { - duration_seconds: number; - estimated_time_seconds: number; - estimated_size_mb: number; -}; -export type ExportSettings = - | ({ format: "Mp4" } & Mp4ExportSettings) - | ({ format: "Gif" } & GifExportSettings); -export type FileType = "recording" | "screenshot"; -export type Flags = { captions: boolean }; -export type FramesRendered = { - renderedCount: number; - totalFrames: number; - type: "FramesRendered"; -}; -export type GeneralSettingsStore = { - instanceId?: string; - uploadIndividualFiles?: boolean; - hideDockIcon?: boolean; - hapticsEnabled?: boolean; - autoCreateShareableLink?: boolean; - enableNotifications?: boolean; - disableAutoOpenLinks?: boolean; - hasCompletedStartup?: boolean; - theme?: AppTheme; - commercialLicense?: CommercialLicense | null; - lastVersion?: string | null; - windowTransparency?: boolean; - postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; - mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; - customCursorCapture?: boolean; - serverUrl?: string; - recordingCountdown?: number | null; - enableNativeCameraPreview: boolean; - autoZoomOnClicks?: boolean; - enableNewRecordingFlow: boolean; - postDeletionBehaviour?: PostDeletionBehaviour; -}; -export type GifExportSettings = { - fps: number; - resolution_base: XY; - quality: GifQuality | null; -}; -export type GifQuality = { - /** - * Encoding quality from 1-100 (default: 90) - */ - quality: number | null; - /** - * Whether to prioritize speed over quality (default: false) - */ - fast: boolean | null; -}; -export type HapticPattern = "Alignment" | "LevelChange" | "Generic"; -export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted"; -export type Hotkey = { - code: string; - meta: boolean; - ctrl: boolean; - alt: boolean; - shift: boolean; -}; -export type HotkeyAction = - | "startRecording" - | "stopRecording" - | "restartRecording"; -export type HotkeysConfiguration = { show: boolean }; -export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }; -export type InstantRecordingMeta = { fps: number; sample_rate: number | null }; -export type JsonValue = [T]; -export type LogicalBounds = { position: LogicalPosition; size: LogicalSize }; -export type LogicalPosition = { x: number; y: number }; -export type LogicalSize = { width: number; height: number }; -export type MainWindowRecordingStartBehaviour = "close" | "minimise"; -export type ModelIDType = string; -export type Mp4ExportSettings = { - fps: number; - resolution_base: XY; - compression: ExportCompression; -}; -export type MultipleSegment = { - display: VideoMeta; - camera?: VideoMeta | null; - mic?: AudioMeta | null; - system_audio?: AudioMeta | null; - cursor?: string | null; -}; -export type MultipleSegments = { - segments: MultipleSegment[]; - cursors: Cursors; -}; -export type NewNotification = { - title: string; - body: string; - is_error: boolean; -}; -export type NewScreenshotAdded = { path: string }; -export type NewStudioRecordingAdded = { path: string }; -export type OSPermission = - | "screenRecording" - | "camera" - | "microphone" - | "accessibility"; -export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied"; -export type OSPermissionsCheck = { - screenRecording: OSPermissionStatus; - microphone: OSPermissionStatus; - camera: OSPermissionStatus; - accessibility: OSPermissionStatus; -}; -export type OnEscapePress = null; -export type PhysicalSize = { width: number; height: number }; -export type Plan = { upgraded: boolean; manual: boolean; last_checked: number }; -export type Platform = "MacOS" | "Windows"; -export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow"; -export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay"; -export type Preset = { name: string; config: ProjectConfiguration }; -export type PresetsStore = { presets: Preset[]; default: number | null }; -export type ProjectConfiguration = { - aspectRatio: AspectRatio | null; - background: BackgroundConfiguration; - camera: Camera; - audio: AudioConfiguration; - cursor: CursorConfiguration; - hotkeys: HotkeysConfiguration; - timeline?: TimelineConfiguration | null; - captions?: CaptionsData | null; -}; -export type ProjectRecordingsMeta = { segments: SegmentRecordings[] }; -export type RecordingDeleted = { path: string }; -export type RecordingEvent = - | { variant: "Countdown"; value: number } - | { variant: "Started" } - | { variant: "Stopped" } - | { variant: "Failed"; error: string }; -export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { - platform?: Platform | null; - pretty_name: string; - sharing?: SharingMeta | null; -}; -export type RecordingMetaWithType = (( - | StudioRecordingMeta - | InstantRecordingMeta -) & { - platform?: Platform | null; - pretty_name: string; - sharing?: SharingMeta | null; -}) & { type: RecordingType }; -export type RecordingMode = "studio" | "instant"; -export type RecordingOptionsChanged = null; -export type RecordingStarted = null; -export type RecordingStopped = null; -export type RecordingType = "studio" | "instant"; -export type RenderFrameEvent = { - frame_number: number; - fps: number; - resolution_base: XY; -}; -export type RequestNewScreenshot = null; -export type RequestOpenSettings = { page: string }; -export type RequestStartRecording = null; -export type S3UploadMeta = { id: string }; -export type ScreenCaptureTarget = - | { variant: "window"; id: WindowId } - | { variant: "display"; id: DisplayId } - | { variant: "area"; screen: DisplayId; bounds: LogicalBounds }; -export type ScreenUnderCursor = { - name: string; - physical_size: PhysicalSize; - refresh_rate: string; -}; -export type SegmentRecordings = { - display: Video; - camera: Video | null; - mic: Audio | null; - system_audio: Audio | null; -}; -export type SerializedEditorInstance = { - framesSocketUrl: string; - recordingDuration: number; - savedProjectConfig: ProjectConfiguration; - recordings: ProjectRecordingsMeta; - path: string; -}; -export type ShadowConfiguration = { - size: number; - opacity: number; - blur: number; -}; -export type SharingMeta = { id: string; link: string }; -export type ShowCapWindow = - | "Setup" - | "Main" - | { Settings: { page: string | null } } - | { Editor: { project_path: string } } - | "RecordingsOverlay" - | { WindowCaptureOccluder: { screen_id: DisplayId } } - | { TargetSelectOverlay: { display_id: DisplayId } } - | { CaptureArea: { screen_id: DisplayId } } - | "Camera" - | { InProgressRecording: { countdown: number | null } } - | "Upgrade" - | "ModeSelect"; -export type SingleSegment = { - display: VideoMeta; - camera?: VideoMeta | null; - audio?: AudioMeta | null; - cursor?: string | null; -}; -export type StartRecordingInputs = { - capture_target: ScreenCaptureTarget; - capture_system_audio?: boolean; - mode: RecordingMode; -}; -export type StereoMode = "stereo" | "monoL" | "monoR"; -export type StudioRecordingMeta = - | { segment: SingleSegment } - | { inner: MultipleSegments }; -export type TargetUnderCursor = { - display_id: DisplayId | null; - window: WindowUnderCursor | null; - screen: ScreenUnderCursor | null; -}; -export type TimelineConfiguration = { - segments: TimelineSegment[]; - zoomSegments: ZoomSegment[]; -}; -export type TimelineSegment = { - recordingSegment?: number; - timescale: number; - start: number; - end: number; -}; -export type UploadMode = - | { Initial: { pre_created_video: VideoUploadInfo | null } } - | "Reupload"; -export type UploadProgress = { progress: number }; -export type UploadResult = - | { Success: string } - | "NotAuthenticated" - | "PlanCheckFailed" - | "UpgradeRequired"; -export type Video = { - duration: number; - width: number; - height: number; - fps: number; - start_time: number; -}; -export type VideoMeta = { - path: string; - fps?: number; - /** - * unix time of the first frame - */ - start_time?: number | null; -}; -export type VideoRecordingMetadata = { duration: number; size: number }; -export type VideoUploadInfo = { - id: string; - link: string; - config: S3UploadMeta; -}; -export type WindowId = string; -export type WindowUnderCursor = { - id: WindowId; - app_name: string; - bounds: LogicalBounds; - icon: string | null; -}; -export type XY = { x: T; y: T }; -export type ZoomMode = "auto" | { manual: { x: number; y: number } }; -export type ZoomSegment = { - start: number; - end: number; - amount: number; - mode: ZoomMode; -}; +export type AppTheme = "system" | "light" | "dark" +export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall" +export type Audio = { duration: number; sample_rate: number; channels: number; start_time: number } +export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb?: number; micStereoMode?: StereoMode; systemVolumeDb?: number } +export type AudioInputLevelChange = number +export type AudioMeta = { path: string; +/** + * unix time of the first frame + */ +start_time?: number | null } +export type AuthSecret = { api_key: string } | { token: string; expires: number } +export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; intercom_hash: string | null } +export type AuthenticationInvalid = null +export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; inset: number; crop: Crop | null; shadow?: number; advancedShadow?: ShadowConfiguration | null } +export type BackgroundSource = { type: "wallpaper"; path: string | null } | { type: "image"; path: string | null } | { type: "color"; value: [number, number, number] } | { type: "gradient"; from: [number, number, number]; to: [number, number, number]; angle?: number } +export type Camera = { hide: boolean; mirror: boolean; position: CameraPosition; size: number; zoom_size: number | null; rounding?: number; shadow?: number; advanced_shadow?: ShadowConfiguration | null; shape?: CameraShape } +export type CameraInfo = { device_id: string; model_id: ModelIDType | null; display_name: string } +export type CameraPosition = { x: CameraXPosition; y: CameraYPosition } +export type CameraPreviewShape = "round" | "square" | "full" +export type CameraPreviewSize = "sm" | "lg" +export type CameraShape = "square" | "source" +export type CameraWindowState = { size: CameraPreviewSize; shape: CameraPreviewShape; mirrored: boolean } +export type CameraXPosition = "left" | "center" | "right" +export type CameraYPosition = "top" | "bottom" +export type CaptionData = { segments: CaptionSegment[]; settings: CaptionSettings | null } +export type CaptionSegment = { id: string; start: number; end: number; text: string } +export type CaptionSettings = { enabled: boolean; font: string; size: number; color: string; backgroundColor: string; backgroundOpacity: number; position: string; bold: boolean; italic: boolean; outline: boolean; outlineColor: string; exportWithSubtitles: boolean } +export type CaptionsData = { segments: CaptionSegment[]; settings: CaptionSettings } +export type CaptureDisplay = { id: DisplayId; name: string; refresh_rate: number } +export type CaptureWindow = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number } +export type CommercialLicense = { licenseKey: string; expiryDate: number | null; refresh: number; activatedOn: number } +export type Crop = { position: XY; size: XY } +export type CurrentRecording = { target: CurrentRecordingTarget; type: RecordingType } +export type CurrentRecordingChanged = null +export type CurrentRecordingTarget = { window: { id: WindowId; bounds: LogicalBounds } } | { screen: { id: DisplayId } } | { area: { screen: DisplayId; bounds: LogicalBounds } } +export type CursorAnimationStyle = "regular" | "slow" | "fast" +export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean; motionBlur?: number; useSvg?: boolean } +export type CursorMeta = { imagePath: string; hotspot: XY; shape?: string | null } +export type CursorType = "pointer" | "circle" +export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta } +export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType } +export type DisplayId = string +export type DownloadProgress = { progress: number; message: string } +export type EditorStateChanged = { playhead_position: number } +export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato" +export type ExportEstimates = { duration_seconds: number; estimated_time_seconds: number; estimated_size_mb: number } +export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format: "Gif" } & GifExportSettings) +export type FileType = "recording" | "screenshot" +export type Flags = { captions: boolean } +export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" } +export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; customCursorCapture?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour } +export type GifExportSettings = { fps: number; resolution_base: XY; quality: GifQuality | null } +export type GifQuality = { +/** + * Encoding quality from 1-100 (default: 90) + */ +quality: number | null; +/** + * Whether to prioritize speed over quality (default: false) + */ +fast: boolean | null } +export type HapticPattern = "Alignment" | "LevelChange" | "Generic" +export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted" +export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } +export type HotkeyAction = "startRecording" | "stopRecording" | "restartRecording" +export type HotkeysConfiguration = { show: boolean } +export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } } +export type InstantRecordingMeta = { fps: number; sample_rate: number | null } +export type JsonValue = [T] +export type LogicalBounds = { position: LogicalPosition; size: LogicalSize } +export type LogicalPosition = { x: number; y: number } +export type LogicalSize = { width: number; height: number } +export type MainWindowRecordingStartBehaviour = "close" | "minimise" +export type ModelIDType = string +export type Mp4ExportSettings = { fps: number; resolution_base: XY; compression: ExportCompression } +export type MultipleSegment = { display: VideoMeta; camera?: VideoMeta | null; mic?: AudioMeta | null; system_audio?: AudioMeta | null; cursor?: string | null } +export type MultipleSegments = { segments: MultipleSegment[]; cursors: Cursors } +export type NewNotification = { title: string; body: string; is_error: boolean } +export type NewScreenshotAdded = { path: string } +export type NewStudioRecordingAdded = { path: string } +export type OSPermission = "screenRecording" | "camera" | "microphone" | "accessibility" +export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied" +export type OSPermissionsCheck = { screenRecording: OSPermissionStatus; microphone: OSPermissionStatus; camera: OSPermissionStatus; accessibility: OSPermissionStatus } +export type OnEscapePress = null +export type PhysicalSize = { width: number; height: number } +export type Plan = { upgraded: boolean; manual: boolean; last_checked: number } +export type Platform = "MacOS" | "Windows" +export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow" +export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay" +export type Preset = { name: string; config: ProjectConfiguration } +export type PresetsStore = { presets: Preset[]; default: number | null } +export type ProjectConfiguration = { aspectRatio: AspectRatio | null; background: BackgroundConfiguration; camera: Camera; audio: AudioConfiguration; cursor: CursorConfiguration; hotkeys: HotkeysConfiguration; timeline?: TimelineConfiguration | null; captions?: CaptionsData | null } +export type ProjectRecordingsMeta = { segments: SegmentRecordings[] } +export type RecordingDeleted = { path: string } +export type RecordingEvent = { variant: "Countdown"; value: number } | { variant: "Started" } | { variant: "Stopped" } | { variant: "Failed"; error: string } +export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null } +export type RecordingMetaWithType = ((StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null }) & { type: RecordingType } +export type RecordingMode = "studio" | "instant" +export type RecordingOptionsChanged = null +export type RecordingStarted = null +export type RecordingStopped = null +export type RecordingType = "studio" | "instant" +export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY } +export type RequestNewScreenshot = null +export type RequestOpenSettings = { page: string } +export type RequestStartRecording = null +export type S3UploadMeta = { id: string } +export type ScreenCaptureTarget = { variant: "window"; id: WindowId } | { variant: "display"; id: DisplayId } | { variant: "area"; screen: DisplayId; bounds: LogicalBounds } +export type ScreenUnderCursor = { name: string; physical_size: PhysicalSize; refresh_rate: string } +export type SegmentRecordings = { display: Video; camera: Video | null; mic: Audio | null; system_audio: Audio | null } +export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordingsMeta; path: string } +export type ShadowConfiguration = { size: number; opacity: number; blur: number } +export type SharingMeta = { id: string; link: string } +export type ShowCapWindow = "Setup" | "Main" | { Settings: { page: string | null } } | { Editor: { project_path: string } } | "RecordingsOverlay" | { WindowCaptureOccluder: { screen_id: DisplayId } } | { TargetSelectOverlay: { display_id: DisplayId } } | { CaptureArea: { screen_id: DisplayId } } | "Camera" | { InProgressRecording: { countdown: number | null } } | "Upgrade" | "ModeSelect" +export type SingleSegment = { display: VideoMeta; camera?: VideoMeta | null; audio?: AudioMeta | null; cursor?: string | null } +export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; capture_system_audio?: boolean; mode: RecordingMode } +export type StereoMode = "stereo" | "monoL" | "monoR" +export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } +export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null; screen: ScreenUnderCursor | null } +export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[] } +export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } +export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" +export type UploadProgress = { progress: number } +export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired" +export type Video = { duration: number; width: number; height: number; fps: number; start_time: number } +export type VideoMeta = { path: string; fps?: number; +/** + * unix time of the first frame + */ +start_time?: number | null } +export type VideoRecordingMetadata = { duration: number; size: number } +export type VideoUploadInfo = { id: string; link: string; config: S3UploadMeta } +export type WindowId = string +export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds; icon: string | null } +export type XY = { x: T; y: T } +export type ZoomMode = "auto" | { manual: { x: number; y: number } } +export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode } /** tauri-specta globals **/ import { - type Channel as TAURI_CHANNEL, invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import type { WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; type __EventObj__ = { listen: ( @@ -827,8 +480,9 @@ function __makeEvents__>( ) { return new Proxy( {} as unknown as { - [K in keyof T]: __EventObj__ & - ((handle: __WebviewWindow__) => __EventObj__); + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; }, { get: (_, event) => { diff --git a/crates/displays/src/main.rs b/crates/displays/src/main.rs index 4f7110d251..e4b9cae41a 100644 --- a/crates/displays/src/main.rs +++ b/crates/displays/src/main.rs @@ -1,7 +1,5 @@ use std::time::Duration; -use cap_displays::{Display, Window}; - fn main() { #[cfg(windows)] { @@ -13,166 +11,136 @@ fn main() { unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE).unwrap() }; } - for display in Display::list() { - dbg!(display.name()); + // Test display functionality + println!("=== Display Information ==="); + for (index, display) in cap_displays::Display::list().iter().enumerate() { + println!("Display {}: {}", index + 1, display.name().unwrap()); + println!(" ID: {}", display.id()); + + let logical_size = display.raw_handle().logical_size().unwrap(); + let physical_size = display.physical_size().unwrap(); + let refresh_rate = display.refresh_rate(); + + println!( + " Logical Resolution: {}x{}", + logical_size.width(), + logical_size.height() + ); + println!( + " Physical Resolution: {}x{}", + physical_size.width(), + physical_size.height() + ); + + if refresh_rate > 0.0 { + println!(" Refresh Rate: {} Hz", refresh_rate); + } else { + println!(" Refresh Rate: Unknown"); + } - let display = display.raw_handle(); + // Check if this is the main display + let main_display_id = cap_displays::Display::list().first().map(|d| d.id()); + + if let Some(main_id) = main_display_id { + if display.id() == main_id { + println!(" Type: Primary Display"); + } else { + println!(" Type: Secondary Display"); + } + } else { + println!(" Type: Unknown"); + } - dbg!(display.physical_bounds()); - dbg!(display.physical_size()); - dbg!(display.logical_size()); + println!(); } - for win in cap_displays::Window::list() { - if let Some(name) = win.name() - && name.contains("Firefox") - { - dbg!(win.physical_bounds()); - dbg!(win.logical_size()); + if let Some(cursor_display) = cap_displays::Display::get_containing_cursor() { + println!( + "🖱️ Cursor is currently on: {}", + cursor_display.name().unwrap() + ); + println!(); + } + + // Test window functionality + println!("=== Windows Under Cursor ==="); + let windows = cap_displays::Window::list_containing_cursor(); + + if windows.is_empty() { + println!("No windows found under cursor"); + } else { + println!("Found {} window(s) under cursor:", windows.len()); + for (index, window) in windows.iter().take(5).enumerate() { + // Limit to first 5 windows + println!("\nWindow {}: {}", index + 1, window.id()); + + #[cfg(target_os = "macos")] + if let Some(bounds) = window.raw_handle().logical_bounds() { + println!( + " Bounds: {}x{} at ({}, {})", + bounds.size().width(), + bounds.size().height(), + bounds.position().x(), + bounds.position().y() + ); + } + + if let Some(owner) = window.owner_name() { + println!(" Application: {}", owner); + } else { + println!(" Application: Unknown"); + } + + // Test icon functionality + match window.app_icon() { + Some(icon_data) => { + println!(" Icon (Standard): {} bytes", icon_data.len()); + println!(" Format: PNG (Raw bytes)"); + println!(" Size: {} bytes", icon_data.len()); + } + None => println!(" Icon (Standard): Not available"), + } } } - return; - - // loop { - // for win in cap_displays::Window::list() { - // if let Some(name) = win.name() - // && name.contains("Firefox") - // { - // dbg!(win.logical_bounds()); - // // dbg!(win.physical_size()); - // } - // } - // } - // Test display functionality - // println!("=== Display Information ==="); - // for (index, display) in cap_displays::Display::list().iter().enumerate() { - // println!("Display {}: {}", index + 1, display.name().unwrap()); - // println!(" ID: {}", display.id()); - - // let logical_size = display.raw_handle().logical_size(); - // let physical_size = display.physical_size(); - // let refresh_rate = display.refresh_rate(); - - // println!( - // " Logical Resolution: {}x{}", - // logical_size.width(), - // logical_size.height() - // ); - // println!( - // " Physical Resolution: {}x{}", - // physical_size.width(), - // physical_size.height() - // ); - - // if refresh_rate > 0.0 { - // println!(" Refresh Rate: {} Hz", refresh_rate); - // } else { - // println!(" Refresh Rate: Unknown"); - // } - - // // Check if this is the main display - // let main_display_id = cap_displays::Display::list().first().map(|d| d.id()); - - // if let Some(main_id) = main_display_id { - // if display.id() == main_id { - // println!(" Type: Primary Display"); - // } else { - // println!(" Type: Secondary Display"); - // } - // } else { - // println!(" Type: Unknown"); - // } - - // println!(); - // } - - // if let Some(cursor_display) = cap_displays::Display::get_containing_cursor() { - // println!( - // "🖱️ Cursor is currently on: {}", - // cursor_display.name().unwrap() - // ); - // println!(); - // } - - // // Test window functionality - // println!("=== Windows Under Cursor ==="); - // let windows = cap_displays::Window::list_containing_cursor(); - - // if windows.is_empty() { - // println!("No windows found under cursor"); - // } else { - // println!("Found {} window(s) under cursor:", windows.len()); - // for (index, window) in windows.iter().take(5).enumerate() { - // // Limit to first 5 windows - // println!("\nWindow {}: {}", index + 1, window.id()); - - // if let Some(bounds) = window.logical_bounds() { - // println!( - // " Bounds: {}x{} at ({}, {})", - // bounds.size().width(), - // bounds.size().height(), - // bounds.position().x(), - // bounds.position().y() - // ); - // } - - // if let Some(owner) = window.owner_name() { - // println!(" Application: {}", owner); - // } else { - // println!(" Application: Unknown"); - // } - - // // Test icon functionality - // match window.app_icon() { - // Some(icon_data) => { - // println!(" Icon (Standard): {} bytes", icon_data.len()); - // println!(" Format: PNG (Raw bytes)"); - // println!(" Size: {} bytes", icon_data.len()); - // } - // None => println!(" Icon (Standard): Not available"), - // } - // } - // } - - // println!("\n=== Topmost Window Icon Test ==="); - // if let Some(topmost) = cap_displays::Window::get_topmost_at_cursor() - // && let Some(owner) = topmost.owner_name() - // { - // println!("Testing icon extraction for: {}", owner); - - // match topmost.app_icon() { - // Some(icon_data) => { - // println!(" ✅ Icon found: {} bytes", icon_data.len()); - // println!(" Format: PNG (Raw bytes)"); - // println!(" Size: {} bytes", icon_data.len()); - // } - // None => println!(" ❌ No icon found"), - // } - // } - - // println!("\n=== Live Monitoring (Press Ctrl+C to exit) ==="); - // println!("Monitoring window levels under cursor...\n"); - - // loop { - // let mut relevant_windows = cap_displays::WindowImpl::list_containing_cursor() - // .into_iter() - // .filter_map(|window| { - // let level = window.level()?; - // level.lt(&5).then_some((window, level)) - // }) - // .collect::>(); - - // relevant_windows.sort_by(|a, b| b.1.cmp(&a.1)); - - // // Print current topmost window info - // if let Some((topmost_window, level)) = relevant_windows.first() - // && let Some(owner) = topmost_window.owner_name() - // { - // print!("\rTopmost: {} (level: {}) ", owner, level); - // std::io::Write::flush(&mut std::io::stdout()).unwrap(); - // } - - // std::thread::sleep(Duration::from_millis(100)); - // } + println!("\n=== Topmost Window Icon Test ==="); + if let Some(topmost) = cap_displays::Window::get_topmost_at_cursor() + && let Some(owner) = topmost.owner_name() + { + println!("Testing icon extraction for: {}", owner); + + match topmost.app_icon() { + Some(icon_data) => { + println!(" ✅ Icon found: {} bytes", icon_data.len()); + println!(" Format: PNG (Raw bytes)"); + println!(" Size: {} bytes", icon_data.len()); + } + None => println!(" ❌ No icon found"), + } + } + + println!("\n=== Live Monitoring (Press Ctrl+C to exit) ==="); + println!("Monitoring window levels under cursor...\n"); + + loop { + let mut relevant_windows = cap_displays::WindowImpl::list_containing_cursor() + .into_iter() + .filter_map(|window| { + let level = window.level()?; + level.lt(&5).then_some((window, level)) + }) + .collect::>(); + + relevant_windows.sort_by(|a, b| b.1.cmp(&a.1)); + + // Print current topmost window info + if let Some((topmost_window, level)) = relevant_windows.first() + && let Some(owner) = topmost_window.owner_name() + { + print!("\rTopmost: {} (level: {}) ", owner, level); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + } + + std::thread::sleep(Duration::from_millis(100)); + } } diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index eb0db07af3..334691c20e 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -30,12 +30,6 @@ pub enum StopCapturingError { NotCapturing, } -static EXCLUDED_WINDOWS: &[&str] = &[ - "Cap Camera", - "Cap Recordings Overlay", - "Cap In Progress Recording", -]; - #[derive(Debug, Clone, Serialize, Deserialize, Type)] pub struct CaptureWindow { pub id: WindowId, diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 7f6519957b..f12f24f593 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -4,83 +4,83 @@ // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import // biome-ignore lint: disable -export {}; +export {} declare global { - const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"]; - const IconCapAudioOn: typeof import("~icons/cap/audio-on.jsx")["default"]; - const IconCapBgBlur: typeof import("~icons/cap/bg-blur.jsx")["default"]; - const IconCapCamera: typeof import("~icons/cap/camera.jsx")["default"]; - const IconCapCaptions: typeof import("~icons/cap/captions.jsx")["default"]; - const IconCapChevronDown: typeof import("~icons/cap/chevron-down.jsx")["default"]; - const IconCapCircle: typeof import("~icons/cap/circle.jsx")["default"]; - const IconCapCircleCheck: typeof import("~icons/cap/circle-check.jsx")["default"]; - const IconCapCirclePlus: typeof import("~icons/cap/circle-plus.jsx")["default"]; - const IconCapCircleX: typeof import("~icons/cap/circle-x.jsx")["default"]; - const IconCapCopy: typeof import("~icons/cap/copy.jsx")["default"]; - const IconCapCorners: typeof import("~icons/cap/corners.jsx")["default"]; - const IconCapCrop: typeof import("~icons/cap/crop.jsx")["default"]; - const IconCapCursor: typeof import("~icons/cap/cursor.jsx")["default"]; - const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"]; - const IconCapEnlarge: typeof import("~icons/cap/enlarge.jsx")["default"]; - const IconCapFile: typeof import("~icons/cap/file.jsx")["default"]; - const IconCapFilmCut: typeof import("~icons/cap/film-cut.jsx")["default"]; - const IconCapGauge: typeof import("~icons/cap/gauge.jsx")["default"]; - const IconCapHotkeys: typeof import("~icons/cap/hotkeys.jsx")["default"]; - const IconCapImage: typeof import("~icons/cap/image.jsx")["default"]; - const IconCapInfo: typeof import("~icons/cap/info.jsx")["default"]; - const IconCapInstant: typeof import("~icons/cap/instant.jsx")["default"]; - const IconCapLayout: typeof import("~icons/cap/layout.jsx")["default"]; - const IconCapLink: typeof import("~icons/cap/link.jsx")["default"]; - const IconCapLogo: typeof import("~icons/cap/logo.jsx")["default"]; - const IconCapLogoFull: typeof import("~icons/cap/logo-full.jsx")["default"]; - const IconCapLogoFullDark: typeof import("~icons/cap/logo-full-dark.jsx")["default"]; - const IconCapMessageBubble: typeof import("~icons/cap/message-bubble.jsx")["default"]; - const IconCapMicrophone: typeof import("~icons/cap/microphone.jsx")["default"]; - const IconCapMoreVertical: typeof import("~icons/cap/more-vertical.jsx")["default"]; - const IconCapNext: typeof import("~icons/cap/next.jsx")["default"]; - const IconCapPadding: typeof import("~icons/cap/padding.jsx")["default"]; - const IconCapPause: typeof import("~icons/cap/pause.jsx")["default"]; - const IconCapPauseCircle: typeof import("~icons/cap/pause-circle.jsx")["default"]; - const IconCapPlay: typeof import("~icons/cap/play.jsx")["default"]; - const IconCapPlayCircle: typeof import("~icons/cap/play-circle.jsx")["default"]; - const IconCapPresets: typeof import("~icons/cap/presets.jsx")["default"]; - const IconCapPrev: typeof import("~icons/cap/prev.jsx")["default"]; - const IconCapRedo: typeof import("~icons/cap/redo.jsx")["default"]; - const IconCapRestart: typeof import("~icons/cap/restart.jsx")["default"]; - const IconCapScissors: typeof import("~icons/cap/scissors.jsx")["default"]; - const IconCapSettings: typeof import("~icons/cap/settings.jsx")["default"]; - const IconCapShadow: typeof import("~icons/cap/shadow.jsx")["default"]; - const IconCapSquare: typeof import("~icons/cap/square.jsx")["default"]; - const IconCapStopCircle: typeof import("~icons/cap/stop-circle.jsx")["default"]; - const IconCapTrash: typeof import("~icons/cap/trash.jsx")["default"]; - const IconCapUndo: typeof import("~icons/cap/undo.jsx")["default"]; - const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"]; - const IconCapZoomIn: typeof import("~icons/cap/zoom-in.jsx")["default"]; - const IconCapZoomOut: typeof import("~icons/cap/zoom-out.jsx")["default"]; - const IconHugeiconsEaseCurveControlPoints: typeof import("~icons/hugeicons/ease-curve-control-points.jsx")["default"]; - const IconLucideAppWindowMac: typeof import("~icons/lucide/app-window-mac.jsx")["default"]; - const IconLucideBell: typeof import("~icons/lucide/bell.jsx")["default"]; - const IconLucideBug: typeof import("~icons/lucide/bug.jsx")["default"]; - const IconLucideCheck: typeof import("~icons/lucide/check.jsx")["default"]; - const IconLucideClock: typeof import("~icons/lucide/clock.jsx")["default"]; - const IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"]; - const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"]; - const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"]; - const IconLucideFolder: typeof import("~icons/lucide/folder.jsx")["default"]; - const IconLucideGift: typeof import("~icons/lucide/gift.jsx")["default"]; - const IconLucideHardDrive: typeof import("~icons/lucide/hard-drive.jsx")["default"]; - const IconLucideLoaderCircle: typeof import("~icons/lucide/loader-circle.jsx")["default"]; - const IconLucideMessageSquarePlus: typeof import("~icons/lucide/message-square-plus.jsx")["default"]; - const IconLucideMicOff: typeof import("~icons/lucide/mic-off.jsx")["default"]; - const IconLucideMonitor: typeof import("~icons/lucide/monitor.jsx")["default"]; - const IconLucideRectangleHorizontal: typeof import("~icons/lucide/rectangle-horizontal.jsx")["default"]; - const IconLucideRotateCcw: typeof import("~icons/lucide/rotate-ccw.jsx")["default"]; - const IconLucideSearch: typeof import("~icons/lucide/search.jsx")["default"]; - const IconLucideSquarePlay: typeof import("~icons/lucide/square-play.jsx")["default"]; - const IconLucideUnplug: typeof import("~icons/lucide/unplug.jsx")["default"]; - const IconLucideVolume2: typeof import("~icons/lucide/volume2.jsx")["default"]; - const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"]; - const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"]; - const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"]; - const IconPhMonitorBold: typeof import("~icons/ph/monitor-bold.jsx")["default"]; + const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"] + const IconCapAudioOn: typeof import('~icons/cap/audio-on.jsx')['default'] + const IconCapBgBlur: typeof import('~icons/cap/bg-blur.jsx')['default'] + const IconCapCamera: typeof import('~icons/cap/camera.jsx')['default'] + const IconCapCaptions: typeof import('~icons/cap/captions.jsx')['default'] + const IconCapChevronDown: typeof import('~icons/cap/chevron-down.jsx')['default'] + const IconCapCircle: typeof import("~icons/cap/circle.jsx")["default"] + const IconCapCircleCheck: typeof import('~icons/cap/circle-check.jsx')['default'] + const IconCapCirclePlus: typeof import('~icons/cap/circle-plus.jsx')['default'] + const IconCapCircleX: typeof import('~icons/cap/circle-x.jsx')['default'] + const IconCapCopy: typeof import('~icons/cap/copy.jsx')['default'] + const IconCapCorners: typeof import('~icons/cap/corners.jsx')['default'] + const IconCapCrop: typeof import('~icons/cap/crop.jsx')['default'] + const IconCapCursor: typeof import('~icons/cap/cursor.jsx')['default'] + const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"] + const IconCapEnlarge: typeof import('~icons/cap/enlarge.jsx')['default'] + const IconCapFile: typeof import('~icons/cap/file.jsx')['default'] + const IconCapFilmCut: typeof import('~icons/cap/film-cut.jsx')['default'] + const IconCapGauge: typeof import('~icons/cap/gauge.jsx')['default'] + const IconCapHotkeys: typeof import('~icons/cap/hotkeys.jsx')['default'] + const IconCapImage: typeof import('~icons/cap/image.jsx')['default'] + const IconCapInfo: typeof import('~icons/cap/info.jsx')['default'] + const IconCapInstant: typeof import('~icons/cap/instant.jsx')['default'] + const IconCapLayout: typeof import('~icons/cap/layout.jsx')['default'] + const IconCapLink: typeof import('~icons/cap/link.jsx')['default'] + const IconCapLogo: typeof import('~icons/cap/logo.jsx')['default'] + const IconCapLogoFull: typeof import('~icons/cap/logo-full.jsx')['default'] + const IconCapLogoFullDark: typeof import('~icons/cap/logo-full-dark.jsx')['default'] + const IconCapMessageBubble: typeof import('~icons/cap/message-bubble.jsx')['default'] + const IconCapMicrophone: typeof import('~icons/cap/microphone.jsx')['default'] + const IconCapMoreVertical: typeof import('~icons/cap/more-vertical.jsx')['default'] + const IconCapNext: typeof import('~icons/cap/next.jsx')['default'] + const IconCapPadding: typeof import('~icons/cap/padding.jsx')['default'] + const IconCapPause: typeof import('~icons/cap/pause.jsx')['default'] + const IconCapPauseCircle: typeof import('~icons/cap/pause-circle.jsx')['default'] + const IconCapPlay: typeof import('~icons/cap/play.jsx')['default'] + const IconCapPlayCircle: typeof import('~icons/cap/play-circle.jsx')['default'] + const IconCapPresets: typeof import('~icons/cap/presets.jsx')['default'] + const IconCapPrev: typeof import('~icons/cap/prev.jsx')['default'] + const IconCapRedo: typeof import('~icons/cap/redo.jsx')['default'] + const IconCapRestart: typeof import('~icons/cap/restart.jsx')['default'] + const IconCapScissors: typeof import('~icons/cap/scissors.jsx')['default'] + const IconCapSettings: typeof import('~icons/cap/settings.jsx')['default'] + const IconCapShadow: typeof import('~icons/cap/shadow.jsx')['default'] + const IconCapSquare: typeof import("~icons/cap/square.jsx")["default"] + const IconCapStopCircle: typeof import('~icons/cap/stop-circle.jsx')['default'] + const IconCapTrash: typeof import('~icons/cap/trash.jsx')['default'] + const IconCapUndo: typeof import('~icons/cap/undo.jsx')['default'] + const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"] + const IconCapZoomIn: typeof import('~icons/cap/zoom-in.jsx')['default'] + const IconCapZoomOut: typeof import('~icons/cap/zoom-out.jsx')['default'] + const IconHugeiconsEaseCurveControlPoints: typeof import('~icons/hugeicons/ease-curve-control-points.jsx')['default'] + const IconLucideAppWindowMac: typeof import("~icons/lucide/app-window-mac.jsx")["default"] + const IconLucideBell: typeof import('~icons/lucide/bell.jsx')['default'] + const IconLucideBug: typeof import('~icons/lucide/bug.jsx')['default'] + const IconLucideCheck: typeof import('~icons/lucide/check.jsx')['default'] + const IconLucideClock: typeof import('~icons/lucide/clock.jsx')['default'] + const IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"] + const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default'] + const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"] + const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] + const IconLucideGift: typeof import('~icons/lucide/gift.jsx')['default'] + const IconLucideHardDrive: typeof import('~icons/lucide/hard-drive.jsx')['default'] + const IconLucideLoaderCircle: typeof import('~icons/lucide/loader-circle.jsx')['default'] + const IconLucideMessageSquarePlus: typeof import('~icons/lucide/message-square-plus.jsx')['default'] + const IconLucideMicOff: typeof import('~icons/lucide/mic-off.jsx')['default'] + const IconLucideMonitor: typeof import('~icons/lucide/monitor.jsx')['default'] + const IconLucideRectangleHorizontal: typeof import("~icons/lucide/rectangle-horizontal.jsx")["default"] + const IconLucideRotateCcw: typeof import('~icons/lucide/rotate-ccw.jsx')['default'] + const IconLucideSearch: typeof import('~icons/lucide/search.jsx')['default'] + const IconLucideSquarePlay: typeof import('~icons/lucide/square-play.jsx')['default'] + const IconLucideUnplug: typeof import('~icons/lucide/unplug.jsx')['default'] + const IconLucideVolume2: typeof import('~icons/lucide/volume2.jsx')['default'] + const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"] + const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"] + const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"] + const IconPhMonitorBold: typeof import('~icons/ph/monitor-bold.jsx')['default'] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44e8dc1708..2f26ffb15d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,8 +135,8 @@ importers: specifier: ^2.3.0 version: 2.3.0 '@tauri-apps/plugin-opener': - specifier: ^2.4.0 - version: 2.4.0 + specifier: ^2.5.0 + version: 2.5.0 '@tauri-apps/plugin-os': specifier: ^2.3.0 version: 2.3.0 @@ -325,7 +325,7 @@ importers: version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3)) storybook-solidjs-vite: specifier: ^1.0.0-beta.2 - version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) + version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) typescript: specifier: ^5.8.3 version: 5.8.3 @@ -3383,10 +3383,16 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -3401,9 +3407,15 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -5819,10 +5831,10 @@ packages: react-dom: optional: true - '@storybook/builder-vite@9.2.0-alpha.3': - resolution: {integrity: sha512-QkZA/9pOmcbES9COYQLPzgqPBPjEeoFNnwoz1uAYa9k8rqCGHDKi2nnGDYFbikvpJs3BougV0vrd/vY37CmL5Q==} + '@storybook/builder-vite@10.0.0-beta.0': + resolution: {integrity: sha512-IGd75RO4YR7ofOZWqND2dYHrKLjwN05+mM1xfrucb2SoQzjbNXvFNVVxtOCBmqsarjmrMKLKHuRgFzU41vs43g==} peerDependencies: - storybook: ^9.2.0-alpha.3 + storybook: ^10.0.0-beta.0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 '@storybook/core@8.6.12': @@ -5833,16 +5845,29 @@ packages: prettier: optional: true + '@storybook/csf-plugin@10.0.0-beta.0': + resolution: {integrity: sha512-2vFHbbnp/yGWt4p53OO5swDVZzmOp3HkJEbhc1zE9BW3YcX0mG8Gh2sd7K0+Io04uoupwJpawTz8VDdKfYkJUA==} + peerDependencies: + esbuild: '*' + rollup: '*' + storybook: ^10.0.0-beta.0 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + '@storybook/csf-plugin@8.6.12': resolution: {integrity: sha512-6s8CnP1aoKPb3XtC0jRLUp8M5vTA8RhGAwQDKUsFpCC7g89JR9CaKs9FY2ZSzsNbjR15uASi7b3K8BzeYumYQg==} peerDependencies: storybook: ^8.6.12 - '@storybook/csf-plugin@9.2.0-alpha.3': - resolution: {integrity: sha512-pAoSleGEUkl2Hx2l17GkA+DZMRdrUAn/oiTrig5dGJaVRlEvTsd4c/e/wkiEPBxLnTXsJTdcEANxTMBByhybCA==} - peerDependencies: - storybook: ^9.2.0-alpha.3 - '@storybook/docs-tools@8.6.12': resolution: {integrity: sha512-bOmTNJE4FM6+3/1hPQVgha8cLxX2Nyi/4Z+tYq0pCDEyQqGRTD2KXre9XsSCnziRaGxiW80OrKkmqWY8Otuu4A==} peerDependencies: @@ -6005,6 +6030,9 @@ packages: '@tauri-apps/api@2.7.0': resolution: {integrity: sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==} + '@tauri-apps/api@2.8.0': + resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==} + '@tauri-apps/cli-darwin-arm64@1.6.3': resolution: {integrity: sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==} engines: {node: '>= 10'} @@ -6159,8 +6187,8 @@ packages: '@tauri-apps/plugin-notification@2.3.0': resolution: {integrity: sha512-QDwXo9VzAlH97c0veuf19TZI6cRBPfJDl2O6hNEDvI66j60lOO9z+PL6MJrj8A6Y+t55r7mGhe3rQWLmOre2HA==} - '@tauri-apps/plugin-opener@2.4.0': - resolution: {integrity: sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==} + '@tauri-apps/plugin-opener@2.5.0': + resolution: {integrity: sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA==} '@tauri-apps/plugin-os@2.3.0': resolution: {integrity: sha512-dm3bDsMuUngpIQdJ1jaMkMfyQpHyDcaTIKTFaAMHoKeUd+Is3UHO2uzhElr6ZZkfytIIyQtSVnCWdW2Kc58f3g==} @@ -11316,6 +11344,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -13149,6 +13181,10 @@ packages: resolution: {integrity: sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w==} engines: {node: '>=18.12.0'} + unplugin@2.3.8: + resolution: {integrity: sha512-lkaSIlxceytPyt9yfb1h7L9jDFqwMqvUZeGsKB7Z8QrvAO3xZv2S+xMQQYzxk0AGJHcQhbcvhKEstrMy99jnuQ==} + engines: {node: '>=18.12.0'} + unrs-resolver@1.7.2: resolution: {integrity: sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==} @@ -16115,12 +16151,22 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} @@ -16132,11 +16178,18 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -19218,12 +19271,16 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@storybook/builder-vite@9.2.0-alpha.3(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))': + '@storybook/builder-vite@10.0.0-beta.0(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))': dependencies: - '@storybook/csf-plugin': 9.2.0-alpha.3(storybook@8.6.12(prettier@3.5.3)) + '@storybook/csf-plugin': 10.0.0-beta.0(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) storybook: 8.6.12(prettier@3.5.3) ts-dedent: 2.2.0 vite: 6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1) + transitivePeerDependencies: + - esbuild + - rollup + - webpack '@storybook/core@8.6.12(prettier@3.5.3)(storybook@8.6.12(prettier@3.5.3))': dependencies: @@ -19246,12 +19303,16 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/csf-plugin@10.0.0-beta.0(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))': dependencies: storybook: 8.6.12(prettier@3.5.3) - unplugin: 1.16.1 + unplugin: 2.3.8 + optionalDependencies: + esbuild: 0.25.4 + rollup: 4.40.2 + vite: 6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1) - '@storybook/csf-plugin@9.2.0-alpha.3(storybook@8.6.12(prettier@3.5.3))': + '@storybook/csf-plugin@8.6.12(storybook@8.6.12(prettier@3.5.3))': dependencies: storybook: 8.6.12(prettier@3.5.3) unplugin: 1.16.1 @@ -19462,6 +19523,8 @@ snapshots: '@tauri-apps/api@2.7.0': {} + '@tauri-apps/api@2.8.0': {} + '@tauri-apps/cli-darwin-arm64@1.6.3': optional: true @@ -19578,9 +19641,9 @@ snapshots: dependencies: '@tauri-apps/api': 2.7.0 - '@tauri-apps/plugin-opener@2.4.0': + '@tauri-apps/plugin-opener@2.5.0': dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 '@tauri-apps/plugin-os@2.3.0': dependencies: @@ -22215,8 +22278,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -22263,6 +22326,21 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.0(supports-color@5.5.0) + eslint: 8.57.1 + get-tsconfig: 4.10.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.13 + unrs-resolver: 1.7.2 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 @@ -22293,14 +22371,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -22326,7 +22404,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -22337,7 +22415,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -26128,6 +26206,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pify@2.3.0: {} pirates@4.0.7: {} @@ -27432,9 +27512,9 @@ snapshots: stoppable@1.1.0: {} - storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)): + storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)): dependencies: - '@storybook/builder-vite': 9.2.0-alpha.3(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) + '@storybook/builder-vite': 10.0.0-beta.0(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) '@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3)) magic-string: 0.30.17 solid-js: 1.9.6 @@ -27444,6 +27524,9 @@ snapshots: vite-plugin-solid: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)) transitivePeerDependencies: - '@storybook/test' + - esbuild + - rollup + - webpack storybook-solidjs@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3)): dependencies: @@ -28338,6 +28421,13 @@ snapshots: picomatch: 4.0.2 webpack-virtual-modules: 0.6.2 + unplugin@2.3.8: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + unrs-resolver@1.7.2: dependencies: napi-postinstall: 0.2.3 From f111ec8b9e503d7634045f9811082bdeee8705e4 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 03:18:15 +0800 Subject: [PATCH 34/47] formatting --- apps/desktop/src/utils/tauri.ts | 1228 +++++++++++++++-------- packages/ui-solid/src/auto-imports.d.ts | 156 +-- 2 files changed, 865 insertions(+), 519 deletions(-) diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 637a72250f..64f9d8003e 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1,463 +1,810 @@ - // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. /** user-defined commands **/ - export const commands = { -async setMicInput(label: string | null) : Promise { - return await TAURI_INVOKE("set_mic_input", { label }); -}, -async setCameraInput(id: DeviceOrModelID | null) : Promise { - return await TAURI_INVOKE("set_camera_input", { id }); -}, -async startRecording(inputs: StartRecordingInputs) : Promise { - return await TAURI_INVOKE("start_recording", { inputs }); -}, -async stopRecording() : Promise { - return await TAURI_INVOKE("stop_recording"); -}, -async pauseRecording() : Promise { - return await TAURI_INVOKE("pause_recording"); -}, -async resumeRecording() : Promise { - return await TAURI_INVOKE("resume_recording"); -}, -async restartRecording() : Promise { - return await TAURI_INVOKE("restart_recording"); -}, -async deleteRecording() : Promise { - return await TAURI_INVOKE("delete_recording"); -}, -async listCameras() : Promise { - return await TAURI_INVOKE("list_cameras"); -}, -async listCaptureWindows() : Promise { - return await TAURI_INVOKE("list_capture_windows"); -}, -async listCaptureDisplays() : Promise { - return await TAURI_INVOKE("list_capture_displays"); -}, -async takeScreenshot() : Promise { - return await TAURI_INVOKE("take_screenshot"); -}, -async listAudioDevices() : Promise { - return await TAURI_INVOKE("list_audio_devices"); -}, -async closeRecordingsOverlayWindow() : Promise { - await TAURI_INVOKE("close_recordings_overlay_window"); -}, -async setFakeWindowBounds(name: string, bounds: LogicalBounds) : Promise { - return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); -}, -async removeFakeWindow(name: string) : Promise { - return await TAURI_INVOKE("remove_fake_window", { name }); -}, -async focusCapturesPanel() : Promise { - await TAURI_INVOKE("focus_captures_panel"); -}, -async getCurrentRecording() : Promise> { - return await TAURI_INVOKE("get_current_recording"); -}, -async exportVideo(projectPath: string, progress: TAURI_CHANNEL, settings: ExportSettings) : Promise { - return await TAURI_INVOKE("export_video", { projectPath, progress, settings }); -}, -async getExportEstimates(path: string, resolution: XY, fps: number) : Promise { - return await TAURI_INVOKE("get_export_estimates", { path, resolution, fps }); -}, -async copyFileToPath(src: string, dst: string) : Promise { - return await TAURI_INVOKE("copy_file_to_path", { src, dst }); -}, -async copyVideoToClipboard(path: string) : Promise { - return await TAURI_INVOKE("copy_video_to_clipboard", { path }); -}, -async copyScreenshotToClipboard(path: string) : Promise { - return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); -}, -async openFilePath(path: string) : Promise { - return await TAURI_INVOKE("open_file_path", { path }); -}, -async getVideoMetadata(path: string) : Promise { - return await TAURI_INVOKE("get_video_metadata", { path }); -}, -async createEditorInstance() : Promise { - return await TAURI_INVOKE("create_editor_instance"); -}, -async getMicWaveforms() : Promise { - return await TAURI_INVOKE("get_mic_waveforms"); -}, -async getSystemAudioWaveforms() : Promise { - return await TAURI_INVOKE("get_system_audio_waveforms"); -}, -async startPlayback(fps: number, resolutionBase: XY) : Promise { - return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); -}, -async stopPlayback() : Promise { - return await TAURI_INVOKE("stop_playback"); -}, -async setPlayheadPosition(frameNumber: number) : Promise { - return await TAURI_INVOKE("set_playhead_position", { frameNumber }); -}, -async setProjectConfig(config: ProjectConfiguration) : Promise { - return await TAURI_INVOKE("set_project_config", { config }); -}, -async generateZoomSegmentsFromClicks() : Promise { - return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); -}, -async openPermissionSettings(permission: OSPermission) : Promise { - await TAURI_INVOKE("open_permission_settings", { permission }); -}, -async doPermissionsCheck(initialCheck: boolean) : Promise { - return await TAURI_INVOKE("do_permissions_check", { initialCheck }); -}, -async requestPermission(permission: OSPermission) : Promise { - await TAURI_INVOKE("request_permission", { permission }); -}, -async uploadExportedVideo(path: string, mode: UploadMode) : Promise { - return await TAURI_INVOKE("upload_exported_video", { path, mode }); -}, -async uploadScreenshot(screenshotPath: string) : Promise { - return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); -}, -async getRecordingMeta(path: string, fileType: FileType) : Promise { - return await TAURI_INVOKE("get_recording_meta", { path, fileType }); -}, -async saveFileDialog(fileName: string, fileType: string) : Promise { - return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); -}, -async listRecordings() : Promise<([string, RecordingMetaWithType])[]> { - return await TAURI_INVOKE("list_recordings"); -}, -async listScreenshots() : Promise<([string, RecordingMeta])[]> { - return await TAURI_INVOKE("list_screenshots"); -}, -async checkUpgradedAndUpdate() : Promise { - return await TAURI_INVOKE("check_upgraded_and_update"); -}, -async openExternalLink(url: string) : Promise { - return await TAURI_INVOKE("open_external_link", { url }); -}, -async setHotkey(action: HotkeyAction, hotkey: Hotkey | null) : Promise { - return await TAURI_INVOKE("set_hotkey", { action, hotkey }); -}, -async resetCameraPermissions() : Promise { - return await TAURI_INVOKE("reset_camera_permissions"); -}, -async resetMicrophonePermissions() : Promise { - return await TAURI_INVOKE("reset_microphone_permissions"); -}, -async isCameraWindowOpen() : Promise { - return await TAURI_INVOKE("is_camera_window_open"); -}, -async seekTo(frameNumber: number) : Promise { - return await TAURI_INVOKE("seek_to", { frameNumber }); -}, -async positionTrafficLights(controlsInset: [number, number] | null) : Promise { - await TAURI_INVOKE("position_traffic_lights", { controlsInset }); -}, -async setTheme(theme: AppTheme) : Promise { - await TAURI_INVOKE("set_theme", { theme }); -}, -async globalMessageDialog(message: string) : Promise { - await TAURI_INVOKE("global_message_dialog", { message }); -}, -async showWindow(window: ShowCapWindow) : Promise { - return await TAURI_INVOKE("show_window", { window }); -}, -async writeClipboardString(text: string) : Promise { - return await TAURI_INVOKE("write_clipboard_string", { text }); -}, -async performHapticFeedback(pattern: HapticPattern | null, time: HapticPerformanceTime | null) : Promise { - return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); -}, -async listFails() : Promise<{ [key in string]: boolean }> { - return await TAURI_INVOKE("list_fails"); -}, -async setFail(name: string, value: boolean) : Promise { - await TAURI_INVOKE("set_fail", { name, value }); -}, -async updateAuthPlan() : Promise { - await TAURI_INVOKE("update_auth_plan"); -}, -async setWindowTransparent(value: boolean) : Promise { - await TAURI_INVOKE("set_window_transparent", { value }); -}, -async getEditorMeta() : Promise { - return await TAURI_INVOKE("get_editor_meta"); -}, -async setPrettyName(prettyName: string) : Promise { - return await TAURI_INVOKE("set_pretty_name", { prettyName }); -}, -async setServerUrl(serverUrl: string) : Promise { - return await TAURI_INVOKE("set_server_url", { serverUrl }); -}, -async setCameraPreviewState(state: CameraWindowState) : Promise { - return await TAURI_INVOKE("set_camera_preview_state", { state }); -}, -async awaitCameraPreviewReady() : Promise { - return await TAURI_INVOKE("await_camera_preview_ready"); -}, -/** - * Function to handle creating directories for the model - */ -async createDir(path: string, recursive: boolean) : Promise { - return await TAURI_INVOKE("create_dir", { path, recursive }); -}, -/** - * Function to save the model file - */ -async saveModelFile(path: string, data: number[]) : Promise { - return await TAURI_INVOKE("save_model_file", { path, data }); -}, -/** - * Function to transcribe audio from a video file using Whisper - */ -async transcribeAudio(videoPath: string, modelPath: string, language: string) : Promise { - return await TAURI_INVOKE("transcribe_audio", { videoPath, modelPath, language }); -}, -/** - * Function to save caption data to a file - */ -async saveCaptions(videoId: string, captions: CaptionData) : Promise { - return await TAURI_INVOKE("save_captions", { videoId, captions }); -}, -/** - * Function to load caption data from a file - */ -async loadCaptions(videoId: string) : Promise { - return await TAURI_INVOKE("load_captions", { videoId }); -}, -/** - * Helper function to download a Whisper model from Hugging Face Hub - */ -async downloadWhisperModel(modelName: string, outputPath: string) : Promise { - return await TAURI_INVOKE("download_whisper_model", { modelName, outputPath }); -}, -/** - * Function to check if a model file exists - */ -async checkModelExists(modelPath: string) : Promise { - return await TAURI_INVOKE("check_model_exists", { modelPath }); -}, -/** - * Function to delete a downloaded model - */ -async deleteWhisperModel(modelPath: string) : Promise { - return await TAURI_INVOKE("delete_whisper_model", { modelPath }); -}, -/** - * Export captions to an SRT file - */ -async exportCaptionsSrt(videoId: string) : Promise { - return await TAURI_INVOKE("export_captions_srt", { videoId }); -}, -async openTargetSelectOverlays() : Promise { - return await TAURI_INVOKE("open_target_select_overlays"); -}, -async closeTargetSelectOverlays() : Promise { - return await TAURI_INVOKE("close_target_select_overlays"); -} -} + async setMicInput(label: string | null): Promise { + return await TAURI_INVOKE("set_mic_input", { label }); + }, + async setCameraInput(id: DeviceOrModelID | null): Promise { + return await TAURI_INVOKE("set_camera_input", { id }); + }, + async startRecording(inputs: StartRecordingInputs): Promise { + return await TAURI_INVOKE("start_recording", { inputs }); + }, + async stopRecording(): Promise { + return await TAURI_INVOKE("stop_recording"); + }, + async pauseRecording(): Promise { + return await TAURI_INVOKE("pause_recording"); + }, + async resumeRecording(): Promise { + return await TAURI_INVOKE("resume_recording"); + }, + async restartRecording(): Promise { + return await TAURI_INVOKE("restart_recording"); + }, + async deleteRecording(): Promise { + return await TAURI_INVOKE("delete_recording"); + }, + async listCameras(): Promise { + return await TAURI_INVOKE("list_cameras"); + }, + async listCaptureWindows(): Promise { + return await TAURI_INVOKE("list_capture_windows"); + }, + async listCaptureDisplays(): Promise { + return await TAURI_INVOKE("list_capture_displays"); + }, + async takeScreenshot(): Promise { + return await TAURI_INVOKE("take_screenshot"); + }, + async listAudioDevices(): Promise { + return await TAURI_INVOKE("list_audio_devices"); + }, + async closeRecordingsOverlayWindow(): Promise { + await TAURI_INVOKE("close_recordings_overlay_window"); + }, + async setFakeWindowBounds( + name: string, + bounds: LogicalBounds, + ): Promise { + return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }); + }, + async removeFakeWindow(name: string): Promise { + return await TAURI_INVOKE("remove_fake_window", { name }); + }, + async focusCapturesPanel(): Promise { + await TAURI_INVOKE("focus_captures_panel"); + }, + async getCurrentRecording(): Promise> { + return await TAURI_INVOKE("get_current_recording"); + }, + async exportVideo( + projectPath: string, + progress: TAURI_CHANNEL, + settings: ExportSettings, + ): Promise { + return await TAURI_INVOKE("export_video", { + projectPath, + progress, + settings, + }); + }, + async getExportEstimates( + path: string, + resolution: XY, + fps: number, + ): Promise { + return await TAURI_INVOKE("get_export_estimates", { + path, + resolution, + fps, + }); + }, + async copyFileToPath(src: string, dst: string): Promise { + return await TAURI_INVOKE("copy_file_to_path", { src, dst }); + }, + async copyVideoToClipboard(path: string): Promise { + return await TAURI_INVOKE("copy_video_to_clipboard", { path }); + }, + async copyScreenshotToClipboard(path: string): Promise { + return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }); + }, + async openFilePath(path: string): Promise { + return await TAURI_INVOKE("open_file_path", { path }); + }, + async getVideoMetadata(path: string): Promise { + return await TAURI_INVOKE("get_video_metadata", { path }); + }, + async createEditorInstance(): Promise { + return await TAURI_INVOKE("create_editor_instance"); + }, + async getMicWaveforms(): Promise { + return await TAURI_INVOKE("get_mic_waveforms"); + }, + async getSystemAudioWaveforms(): Promise { + return await TAURI_INVOKE("get_system_audio_waveforms"); + }, + async startPlayback(fps: number, resolutionBase: XY): Promise { + return await TAURI_INVOKE("start_playback", { fps, resolutionBase }); + }, + async stopPlayback(): Promise { + return await TAURI_INVOKE("stop_playback"); + }, + async setPlayheadPosition(frameNumber: number): Promise { + return await TAURI_INVOKE("set_playhead_position", { frameNumber }); + }, + async setProjectConfig(config: ProjectConfiguration): Promise { + return await TAURI_INVOKE("set_project_config", { config }); + }, + async generateZoomSegmentsFromClicks(): Promise { + return await TAURI_INVOKE("generate_zoom_segments_from_clicks"); + }, + async openPermissionSettings(permission: OSPermission): Promise { + await TAURI_INVOKE("open_permission_settings", { permission }); + }, + async doPermissionsCheck(initialCheck: boolean): Promise { + return await TAURI_INVOKE("do_permissions_check", { initialCheck }); + }, + async requestPermission(permission: OSPermission): Promise { + await TAURI_INVOKE("request_permission", { permission }); + }, + async uploadExportedVideo( + path: string, + mode: UploadMode, + ): Promise { + return await TAURI_INVOKE("upload_exported_video", { path, mode }); + }, + async uploadScreenshot(screenshotPath: string): Promise { + return await TAURI_INVOKE("upload_screenshot", { screenshotPath }); + }, + async getRecordingMeta( + path: string, + fileType: FileType, + ): Promise { + return await TAURI_INVOKE("get_recording_meta", { path, fileType }); + }, + async saveFileDialog( + fileName: string, + fileType: string, + ): Promise { + return await TAURI_INVOKE("save_file_dialog", { fileName, fileType }); + }, + async listRecordings(): Promise<[string, RecordingMetaWithType][]> { + return await TAURI_INVOKE("list_recordings"); + }, + async listScreenshots(): Promise<[string, RecordingMeta][]> { + return await TAURI_INVOKE("list_screenshots"); + }, + async checkUpgradedAndUpdate(): Promise { + return await TAURI_INVOKE("check_upgraded_and_update"); + }, + async openExternalLink(url: string): Promise { + return await TAURI_INVOKE("open_external_link", { url }); + }, + async setHotkey(action: HotkeyAction, hotkey: Hotkey | null): Promise { + return await TAURI_INVOKE("set_hotkey", { action, hotkey }); + }, + async resetCameraPermissions(): Promise { + return await TAURI_INVOKE("reset_camera_permissions"); + }, + async resetMicrophonePermissions(): Promise { + return await TAURI_INVOKE("reset_microphone_permissions"); + }, + async isCameraWindowOpen(): Promise { + return await TAURI_INVOKE("is_camera_window_open"); + }, + async seekTo(frameNumber: number): Promise { + return await TAURI_INVOKE("seek_to", { frameNumber }); + }, + async positionTrafficLights( + controlsInset: [number, number] | null, + ): Promise { + await TAURI_INVOKE("position_traffic_lights", { controlsInset }); + }, + async setTheme(theme: AppTheme): Promise { + await TAURI_INVOKE("set_theme", { theme }); + }, + async globalMessageDialog(message: string): Promise { + await TAURI_INVOKE("global_message_dialog", { message }); + }, + async showWindow(window: ShowCapWindow): Promise { + return await TAURI_INVOKE("show_window", { window }); + }, + async writeClipboardString(text: string): Promise { + return await TAURI_INVOKE("write_clipboard_string", { text }); + }, + async performHapticFeedback( + pattern: HapticPattern | null, + time: HapticPerformanceTime | null, + ): Promise { + return await TAURI_INVOKE("perform_haptic_feedback", { pattern, time }); + }, + async listFails(): Promise<{ [key in string]: boolean }> { + return await TAURI_INVOKE("list_fails"); + }, + async setFail(name: string, value: boolean): Promise { + await TAURI_INVOKE("set_fail", { name, value }); + }, + async updateAuthPlan(): Promise { + await TAURI_INVOKE("update_auth_plan"); + }, + async setWindowTransparent(value: boolean): Promise { + await TAURI_INVOKE("set_window_transparent", { value }); + }, + async getEditorMeta(): Promise { + return await TAURI_INVOKE("get_editor_meta"); + }, + async setPrettyName(prettyName: string): Promise { + return await TAURI_INVOKE("set_pretty_name", { prettyName }); + }, + async setServerUrl(serverUrl: string): Promise { + return await TAURI_INVOKE("set_server_url", { serverUrl }); + }, + async setCameraPreviewState(state: CameraWindowState): Promise { + return await TAURI_INVOKE("set_camera_preview_state", { state }); + }, + async awaitCameraPreviewReady(): Promise { + return await TAURI_INVOKE("await_camera_preview_ready"); + }, + /** + * Function to handle creating directories for the model + */ + async createDir(path: string, recursive: boolean): Promise { + return await TAURI_INVOKE("create_dir", { path, recursive }); + }, + /** + * Function to save the model file + */ + async saveModelFile(path: string, data: number[]): Promise { + return await TAURI_INVOKE("save_model_file", { path, data }); + }, + /** + * Function to transcribe audio from a video file using Whisper + */ + async transcribeAudio( + videoPath: string, + modelPath: string, + language: string, + ): Promise { + return await TAURI_INVOKE("transcribe_audio", { + videoPath, + modelPath, + language, + }); + }, + /** + * Function to save caption data to a file + */ + async saveCaptions(videoId: string, captions: CaptionData): Promise { + return await TAURI_INVOKE("save_captions", { videoId, captions }); + }, + /** + * Function to load caption data from a file + */ + async loadCaptions(videoId: string): Promise { + return await TAURI_INVOKE("load_captions", { videoId }); + }, + /** + * Helper function to download a Whisper model from Hugging Face Hub + */ + async downloadWhisperModel( + modelName: string, + outputPath: string, + ): Promise { + return await TAURI_INVOKE("download_whisper_model", { + modelName, + outputPath, + }); + }, + /** + * Function to check if a model file exists + */ + async checkModelExists(modelPath: string): Promise { + return await TAURI_INVOKE("check_model_exists", { modelPath }); + }, + /** + * Function to delete a downloaded model + */ + async deleteWhisperModel(modelPath: string): Promise { + return await TAURI_INVOKE("delete_whisper_model", { modelPath }); + }, + /** + * Export captions to an SRT file + */ + async exportCaptionsSrt(videoId: string): Promise { + return await TAURI_INVOKE("export_captions_srt", { videoId }); + }, + async openTargetSelectOverlays(): Promise { + return await TAURI_INVOKE("open_target_select_overlays"); + }, + async closeTargetSelectOverlays(): Promise { + return await TAURI_INVOKE("close_target_select_overlays"); + }, +}; /** user-defined events **/ - export const events = __makeEvents__<{ -audioInputLevelChange: AudioInputLevelChange, -authenticationInvalid: AuthenticationInvalid, -currentRecordingChanged: CurrentRecordingChanged, -downloadProgress: DownloadProgress, -editorStateChanged: EditorStateChanged, -newNotification: NewNotification, -newScreenshotAdded: NewScreenshotAdded, -newStudioRecordingAdded: NewStudioRecordingAdded, -onEscapePress: OnEscapePress, -recordingDeleted: RecordingDeleted, -recordingEvent: RecordingEvent, -recordingOptionsChanged: RecordingOptionsChanged, -recordingStarted: RecordingStarted, -recordingStopped: RecordingStopped, -renderFrameEvent: RenderFrameEvent, -requestNewScreenshot: RequestNewScreenshot, -requestOpenSettings: RequestOpenSettings, -requestStartRecording: RequestStartRecording, -targetUnderCursor: TargetUnderCursor, -uploadProgress: UploadProgress + audioInputLevelChange: AudioInputLevelChange; + authenticationInvalid: AuthenticationInvalid; + currentRecordingChanged: CurrentRecordingChanged; + downloadProgress: DownloadProgress; + editorStateChanged: EditorStateChanged; + newNotification: NewNotification; + newScreenshotAdded: NewScreenshotAdded; + newStudioRecordingAdded: NewStudioRecordingAdded; + onEscapePress: OnEscapePress; + recordingDeleted: RecordingDeleted; + recordingEvent: RecordingEvent; + recordingOptionsChanged: RecordingOptionsChanged; + recordingStarted: RecordingStarted; + recordingStopped: RecordingStopped; + renderFrameEvent: RenderFrameEvent; + requestNewScreenshot: RequestNewScreenshot; + requestOpenSettings: RequestOpenSettings; + requestStartRecording: RequestStartRecording; + targetUnderCursor: TargetUnderCursor; + uploadProgress: UploadProgress; }>({ -audioInputLevelChange: "audio-input-level-change", -authenticationInvalid: "authentication-invalid", -currentRecordingChanged: "current-recording-changed", -downloadProgress: "download-progress", -editorStateChanged: "editor-state-changed", -newNotification: "new-notification", -newScreenshotAdded: "new-screenshot-added", -newStudioRecordingAdded: "new-studio-recording-added", -onEscapePress: "on-escape-press", -recordingDeleted: "recording-deleted", -recordingEvent: "recording-event", -recordingOptionsChanged: "recording-options-changed", -recordingStarted: "recording-started", -recordingStopped: "recording-stopped", -renderFrameEvent: "render-frame-event", -requestNewScreenshot: "request-new-screenshot", -requestOpenSettings: "request-open-settings", -requestStartRecording: "request-start-recording", -targetUnderCursor: "target-under-cursor", -uploadProgress: "upload-progress" -}) + audioInputLevelChange: "audio-input-level-change", + authenticationInvalid: "authentication-invalid", + currentRecordingChanged: "current-recording-changed", + downloadProgress: "download-progress", + editorStateChanged: "editor-state-changed", + newNotification: "new-notification", + newScreenshotAdded: "new-screenshot-added", + newStudioRecordingAdded: "new-studio-recording-added", + onEscapePress: "on-escape-press", + recordingDeleted: "recording-deleted", + recordingEvent: "recording-event", + recordingOptionsChanged: "recording-options-changed", + recordingStarted: "recording-started", + recordingStopped: "recording-stopped", + renderFrameEvent: "render-frame-event", + requestNewScreenshot: "request-new-screenshot", + requestOpenSettings: "request-open-settings", + requestStartRecording: "request-start-recording", + targetUnderCursor: "target-under-cursor", + uploadProgress: "upload-progress", +}); /** user-defined constants **/ - - /** user-defined types **/ -export type AppTheme = "system" | "light" | "dark" -export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall" -export type Audio = { duration: number; sample_rate: number; channels: number; start_time: number } -export type AudioConfiguration = { mute: boolean; improve: boolean; micVolumeDb?: number; micStereoMode?: StereoMode; systemVolumeDb?: number } -export type AudioInputLevelChange = number -export type AudioMeta = { path: string; -/** - * unix time of the first frame - */ -start_time?: number | null } -export type AuthSecret = { api_key: string } | { token: string; expires: number } -export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; intercom_hash: string | null } -export type AuthenticationInvalid = null -export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; inset: number; crop: Crop | null; shadow?: number; advancedShadow?: ShadowConfiguration | null } -export type BackgroundSource = { type: "wallpaper"; path: string | null } | { type: "image"; path: string | null } | { type: "color"; value: [number, number, number] } | { type: "gradient"; from: [number, number, number]; to: [number, number, number]; angle?: number } -export type Camera = { hide: boolean; mirror: boolean; position: CameraPosition; size: number; zoom_size: number | null; rounding?: number; shadow?: number; advanced_shadow?: ShadowConfiguration | null; shape?: CameraShape } -export type CameraInfo = { device_id: string; model_id: ModelIDType | null; display_name: string } -export type CameraPosition = { x: CameraXPosition; y: CameraYPosition } -export type CameraPreviewShape = "round" | "square" | "full" -export type CameraPreviewSize = "sm" | "lg" -export type CameraShape = "square" | "source" -export type CameraWindowState = { size: CameraPreviewSize; shape: CameraPreviewShape; mirrored: boolean } -export type CameraXPosition = "left" | "center" | "right" -export type CameraYPosition = "top" | "bottom" -export type CaptionData = { segments: CaptionSegment[]; settings: CaptionSettings | null } -export type CaptionSegment = { id: string; start: number; end: number; text: string } -export type CaptionSettings = { enabled: boolean; font: string; size: number; color: string; backgroundColor: string; backgroundOpacity: number; position: string; bold: boolean; italic: boolean; outline: boolean; outlineColor: string; exportWithSubtitles: boolean } -export type CaptionsData = { segments: CaptionSegment[]; settings: CaptionSettings } -export type CaptureDisplay = { id: DisplayId; name: string; refresh_rate: number } -export type CaptureWindow = { id: WindowId; owner_name: string; name: string; bounds: LogicalBounds; refresh_rate: number } -export type CommercialLicense = { licenseKey: string; expiryDate: number | null; refresh: number; activatedOn: number } -export type Crop = { position: XY; size: XY } -export type CurrentRecording = { target: CurrentRecordingTarget; type: RecordingType } -export type CurrentRecordingChanged = null -export type CurrentRecordingTarget = { window: { id: WindowId; bounds: LogicalBounds } } | { screen: { id: DisplayId } } | { area: { screen: DisplayId; bounds: LogicalBounds } } -export type CursorAnimationStyle = "regular" | "slow" | "fast" -export type CursorConfiguration = { hide?: boolean; hideWhenIdle: boolean; size: number; type: CursorType; animationStyle: CursorAnimationStyle; tension: number; mass: number; friction: number; raw?: boolean; motionBlur?: number; useSvg?: boolean } -export type CursorMeta = { imagePath: string; hotspot: XY; shape?: string | null } -export type CursorType = "pointer" | "circle" -export type Cursors = { [key in string]: string } | { [key in string]: CursorMeta } -export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType } -export type DisplayId = string -export type DownloadProgress = { progress: number; message: string } -export type EditorStateChanged = { playhead_position: number } -export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato" -export type ExportEstimates = { duration_seconds: number; estimated_time_seconds: number; estimated_size_mb: number } -export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format: "Gif" } & GifExportSettings) -export type FileType = "recording" | "screenshot" -export type Flags = { captions: boolean } -export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" } -export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; customCursorCapture?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour } -export type GifExportSettings = { fps: number; resolution_base: XY; quality: GifQuality | null } -export type GifQuality = { -/** - * Encoding quality from 1-100 (default: 90) - */ -quality: number | null; -/** - * Whether to prioritize speed over quality (default: false) - */ -fast: boolean | null } -export type HapticPattern = "Alignment" | "LevelChange" | "Generic" -export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted" -export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } -export type HotkeyAction = "startRecording" | "stopRecording" | "restartRecording" -export type HotkeysConfiguration = { show: boolean } -export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } } -export type InstantRecordingMeta = { fps: number; sample_rate: number | null } -export type JsonValue = [T] -export type LogicalBounds = { position: LogicalPosition; size: LogicalSize } -export type LogicalPosition = { x: number; y: number } -export type LogicalSize = { width: number; height: number } -export type MainWindowRecordingStartBehaviour = "close" | "minimise" -export type ModelIDType = string -export type Mp4ExportSettings = { fps: number; resolution_base: XY; compression: ExportCompression } -export type MultipleSegment = { display: VideoMeta; camera?: VideoMeta | null; mic?: AudioMeta | null; system_audio?: AudioMeta | null; cursor?: string | null } -export type MultipleSegments = { segments: MultipleSegment[]; cursors: Cursors } -export type NewNotification = { title: string; body: string; is_error: boolean } -export type NewScreenshotAdded = { path: string } -export type NewStudioRecordingAdded = { path: string } -export type OSPermission = "screenRecording" | "camera" | "microphone" | "accessibility" -export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied" -export type OSPermissionsCheck = { screenRecording: OSPermissionStatus; microphone: OSPermissionStatus; camera: OSPermissionStatus; accessibility: OSPermissionStatus } -export type OnEscapePress = null -export type PhysicalSize = { width: number; height: number } -export type Plan = { upgraded: boolean; manual: boolean; last_checked: number } -export type Platform = "MacOS" | "Windows" -export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow" -export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay" -export type Preset = { name: string; config: ProjectConfiguration } -export type PresetsStore = { presets: Preset[]; default: number | null } -export type ProjectConfiguration = { aspectRatio: AspectRatio | null; background: BackgroundConfiguration; camera: Camera; audio: AudioConfiguration; cursor: CursorConfiguration; hotkeys: HotkeysConfiguration; timeline?: TimelineConfiguration | null; captions?: CaptionsData | null } -export type ProjectRecordingsMeta = { segments: SegmentRecordings[] } -export type RecordingDeleted = { path: string } -export type RecordingEvent = { variant: "Countdown"; value: number } | { variant: "Started" } | { variant: "Stopped" } | { variant: "Failed"; error: string } -export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null } -export type RecordingMetaWithType = ((StudioRecordingMeta | InstantRecordingMeta) & { platform?: Platform | null; pretty_name: string; sharing?: SharingMeta | null }) & { type: RecordingType } -export type RecordingMode = "studio" | "instant" -export type RecordingOptionsChanged = null -export type RecordingStarted = null -export type RecordingStopped = null -export type RecordingType = "studio" | "instant" -export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY } -export type RequestNewScreenshot = null -export type RequestOpenSettings = { page: string } -export type RequestStartRecording = null -export type S3UploadMeta = { id: string } -export type ScreenCaptureTarget = { variant: "window"; id: WindowId } | { variant: "display"; id: DisplayId } | { variant: "area"; screen: DisplayId; bounds: LogicalBounds } -export type ScreenUnderCursor = { name: string; physical_size: PhysicalSize; refresh_rate: string } -export type SegmentRecordings = { display: Video; camera: Video | null; mic: Audio | null; system_audio: Audio | null } -export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordingsMeta; path: string } -export type ShadowConfiguration = { size: number; opacity: number; blur: number } -export type SharingMeta = { id: string; link: string } -export type ShowCapWindow = "Setup" | "Main" | { Settings: { page: string | null } } | { Editor: { project_path: string } } | "RecordingsOverlay" | { WindowCaptureOccluder: { screen_id: DisplayId } } | { TargetSelectOverlay: { display_id: DisplayId } } | { CaptureArea: { screen_id: DisplayId } } | "Camera" | { InProgressRecording: { countdown: number | null } } | "Upgrade" | "ModeSelect" -export type SingleSegment = { display: VideoMeta; camera?: VideoMeta | null; audio?: AudioMeta | null; cursor?: string | null } -export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; capture_system_audio?: boolean; mode: RecordingMode } -export type StereoMode = "stereo" | "monoL" | "monoR" -export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } -export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null; screen: ScreenUnderCursor | null } -export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[] } -export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } -export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" -export type UploadProgress = { progress: number } -export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired" -export type Video = { duration: number; width: number; height: number; fps: number; start_time: number } -export type VideoMeta = { path: string; fps?: number; -/** - * unix time of the first frame - */ -start_time?: number | null } -export type VideoRecordingMetadata = { duration: number; size: number } -export type VideoUploadInfo = { id: string; link: string; config: S3UploadMeta } -export type WindowId = string -export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds; icon: string | null } -export type XY = { x: T; y: T } -export type ZoomMode = "auto" | { manual: { x: number; y: number } } -export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode } +export type AppTheme = "system" | "light" | "dark"; +export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"; +export type Audio = { + duration: number; + sample_rate: number; + channels: number; + start_time: number; +}; +export type AudioConfiguration = { + mute: boolean; + improve: boolean; + micVolumeDb?: number; + micStereoMode?: StereoMode; + systemVolumeDb?: number; +}; +export type AudioInputLevelChange = number; +export type AudioMeta = { + path: string; + /** + * unix time of the first frame + */ + start_time?: number | null; +}; +export type AuthSecret = + | { api_key: string } + | { token: string; expires: number }; +export type AuthStore = { + secret: AuthSecret; + user_id: string | null; + plan: Plan | null; + intercom_hash: string | null; +}; +export type AuthenticationInvalid = null; +export type BackgroundConfiguration = { + source: BackgroundSource; + blur: number; + padding: number; + rounding: number; + inset: number; + crop: Crop | null; + shadow?: number; + advancedShadow?: ShadowConfiguration | null; +}; +export type BackgroundSource = + | { type: "wallpaper"; path: string | null } + | { type: "image"; path: string | null } + | { type: "color"; value: [number, number, number] } + | { + type: "gradient"; + from: [number, number, number]; + to: [number, number, number]; + angle?: number; + }; +export type Camera = { + hide: boolean; + mirror: boolean; + position: CameraPosition; + size: number; + zoom_size: number | null; + rounding?: number; + shadow?: number; + advanced_shadow?: ShadowConfiguration | null; + shape?: CameraShape; +}; +export type CameraInfo = { + device_id: string; + model_id: ModelIDType | null; + display_name: string; +}; +export type CameraPosition = { x: CameraXPosition; y: CameraYPosition }; +export type CameraPreviewShape = "round" | "square" | "full"; +export type CameraPreviewSize = "sm" | "lg"; +export type CameraShape = "square" | "source"; +export type CameraWindowState = { + size: CameraPreviewSize; + shape: CameraPreviewShape; + mirrored: boolean; +}; +export type CameraXPosition = "left" | "center" | "right"; +export type CameraYPosition = "top" | "bottom"; +export type CaptionData = { + segments: CaptionSegment[]; + settings: CaptionSettings | null; +}; +export type CaptionSegment = { + id: string; + start: number; + end: number; + text: string; +}; +export type CaptionSettings = { + enabled: boolean; + font: string; + size: number; + color: string; + backgroundColor: string; + backgroundOpacity: number; + position: string; + bold: boolean; + italic: boolean; + outline: boolean; + outlineColor: string; + exportWithSubtitles: boolean; +}; +export type CaptionsData = { + segments: CaptionSegment[]; + settings: CaptionSettings; +}; +export type CaptureDisplay = { + id: DisplayId; + name: string; + refresh_rate: number; +}; +export type CaptureWindow = { + id: WindowId; + owner_name: string; + name: string; + bounds: LogicalBounds; + refresh_rate: number; +}; +export type CommercialLicense = { + licenseKey: string; + expiryDate: number | null; + refresh: number; + activatedOn: number; +}; +export type Crop = { position: XY; size: XY }; +export type CurrentRecording = { + target: CurrentRecordingTarget; + type: RecordingType; +}; +export type CurrentRecordingChanged = null; +export type CurrentRecordingTarget = + | { window: { id: WindowId; bounds: LogicalBounds } } + | { screen: { id: DisplayId } } + | { area: { screen: DisplayId; bounds: LogicalBounds } }; +export type CursorAnimationStyle = "regular" | "slow" | "fast"; +export type CursorConfiguration = { + hide?: boolean; + hideWhenIdle: boolean; + size: number; + type: CursorType; + animationStyle: CursorAnimationStyle; + tension: number; + mass: number; + friction: number; + raw?: boolean; + motionBlur?: number; + useSvg?: boolean; +}; +export type CursorMeta = { + imagePath: string; + hotspot: XY; + shape?: string | null; +}; +export type CursorType = "pointer" | "circle"; +export type Cursors = + | { [key in string]: string } + | { [key in string]: CursorMeta }; +export type DeviceOrModelID = { DeviceID: string } | { ModelID: ModelIDType }; +export type DisplayId = string; +export type DownloadProgress = { progress: number; message: string }; +export type EditorStateChanged = { playhead_position: number }; +export type ExportCompression = "Minimal" | "Social" | "Web" | "Potato"; +export type ExportEstimates = { + duration_seconds: number; + estimated_time_seconds: number; + estimated_size_mb: number; +}; +export type ExportSettings = + | ({ format: "Mp4" } & Mp4ExportSettings) + | ({ format: "Gif" } & GifExportSettings); +export type FileType = "recording" | "screenshot"; +export type Flags = { captions: boolean }; +export type FramesRendered = { + renderedCount: number; + totalFrames: number; + type: "FramesRendered"; +}; +export type GeneralSettingsStore = { + instanceId?: string; + uploadIndividualFiles?: boolean; + hideDockIcon?: boolean; + hapticsEnabled?: boolean; + autoCreateShareableLink?: boolean; + enableNotifications?: boolean; + disableAutoOpenLinks?: boolean; + hasCompletedStartup?: boolean; + theme?: AppTheme; + commercialLicense?: CommercialLicense | null; + lastVersion?: string | null; + windowTransparency?: boolean; + postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; + mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; + customCursorCapture?: boolean; + serverUrl?: string; + recordingCountdown?: number | null; + enableNativeCameraPreview: boolean; + autoZoomOnClicks?: boolean; + enableNewRecordingFlow: boolean; + postDeletionBehaviour?: PostDeletionBehaviour; +}; +export type GifExportSettings = { + fps: number; + resolution_base: XY; + quality: GifQuality | null; +}; +export type GifQuality = { + /** + * Encoding quality from 1-100 (default: 90) + */ + quality: number | null; + /** + * Whether to prioritize speed over quality (default: false) + */ + fast: boolean | null; +}; +export type HapticPattern = "Alignment" | "LevelChange" | "Generic"; +export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted"; +export type Hotkey = { + code: string; + meta: boolean; + ctrl: boolean; + alt: boolean; + shift: boolean; +}; +export type HotkeyAction = + | "startRecording" + | "stopRecording" + | "restartRecording"; +export type HotkeysConfiguration = { show: boolean }; +export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }; +export type InstantRecordingMeta = { fps: number; sample_rate: number | null }; +export type JsonValue = [T]; +export type LogicalBounds = { position: LogicalPosition; size: LogicalSize }; +export type LogicalPosition = { x: number; y: number }; +export type LogicalSize = { width: number; height: number }; +export type MainWindowRecordingStartBehaviour = "close" | "minimise"; +export type ModelIDType = string; +export type Mp4ExportSettings = { + fps: number; + resolution_base: XY; + compression: ExportCompression; +}; +export type MultipleSegment = { + display: VideoMeta; + camera?: VideoMeta | null; + mic?: AudioMeta | null; + system_audio?: AudioMeta | null; + cursor?: string | null; +}; +export type MultipleSegments = { + segments: MultipleSegment[]; + cursors: Cursors; +}; +export type NewNotification = { + title: string; + body: string; + is_error: boolean; +}; +export type NewScreenshotAdded = { path: string }; +export type NewStudioRecordingAdded = { path: string }; +export type OSPermission = + | "screenRecording" + | "camera" + | "microphone" + | "accessibility"; +export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied"; +export type OSPermissionsCheck = { + screenRecording: OSPermissionStatus; + microphone: OSPermissionStatus; + camera: OSPermissionStatus; + accessibility: OSPermissionStatus; +}; +export type OnEscapePress = null; +export type PhysicalSize = { width: number; height: number }; +export type Plan = { upgraded: boolean; manual: boolean; last_checked: number }; +export type Platform = "MacOS" | "Windows"; +export type PostDeletionBehaviour = "doNothing" | "reopenRecordingWindow"; +export type PostStudioRecordingBehaviour = "openEditor" | "showOverlay"; +export type Preset = { name: string; config: ProjectConfiguration }; +export type PresetsStore = { presets: Preset[]; default: number | null }; +export type ProjectConfiguration = { + aspectRatio: AspectRatio | null; + background: BackgroundConfiguration; + camera: Camera; + audio: AudioConfiguration; + cursor: CursorConfiguration; + hotkeys: HotkeysConfiguration; + timeline?: TimelineConfiguration | null; + captions?: CaptionsData | null; +}; +export type ProjectRecordingsMeta = { segments: SegmentRecordings[] }; +export type RecordingDeleted = { path: string }; +export type RecordingEvent = + | { variant: "Countdown"; value: number } + | { variant: "Started" } + | { variant: "Stopped" } + | { variant: "Failed"; error: string }; +export type RecordingMeta = (StudioRecordingMeta | InstantRecordingMeta) & { + platform?: Platform | null; + pretty_name: string; + sharing?: SharingMeta | null; +}; +export type RecordingMetaWithType = (( + | StudioRecordingMeta + | InstantRecordingMeta +) & { + platform?: Platform | null; + pretty_name: string; + sharing?: SharingMeta | null; +}) & { type: RecordingType }; +export type RecordingMode = "studio" | "instant"; +export type RecordingOptionsChanged = null; +export type RecordingStarted = null; +export type RecordingStopped = null; +export type RecordingType = "studio" | "instant"; +export type RenderFrameEvent = { + frame_number: number; + fps: number; + resolution_base: XY; +}; +export type RequestNewScreenshot = null; +export type RequestOpenSettings = { page: string }; +export type RequestStartRecording = null; +export type S3UploadMeta = { id: string }; +export type ScreenCaptureTarget = + | { variant: "window"; id: WindowId } + | { variant: "display"; id: DisplayId } + | { variant: "area"; screen: DisplayId; bounds: LogicalBounds }; +export type ScreenUnderCursor = { + name: string; + physical_size: PhysicalSize; + refresh_rate: string; +}; +export type SegmentRecordings = { + display: Video; + camera: Video | null; + mic: Audio | null; + system_audio: Audio | null; +}; +export type SerializedEditorInstance = { + framesSocketUrl: string; + recordingDuration: number; + savedProjectConfig: ProjectConfiguration; + recordings: ProjectRecordingsMeta; + path: string; +}; +export type ShadowConfiguration = { + size: number; + opacity: number; + blur: number; +}; +export type SharingMeta = { id: string; link: string }; +export type ShowCapWindow = + | "Setup" + | "Main" + | { Settings: { page: string | null } } + | { Editor: { project_path: string } } + | "RecordingsOverlay" + | { WindowCaptureOccluder: { screen_id: DisplayId } } + | { TargetSelectOverlay: { display_id: DisplayId } } + | { CaptureArea: { screen_id: DisplayId } } + | "Camera" + | { InProgressRecording: { countdown: number | null } } + | "Upgrade" + | "ModeSelect"; +export type SingleSegment = { + display: VideoMeta; + camera?: VideoMeta | null; + audio?: AudioMeta | null; + cursor?: string | null; +}; +export type StartRecordingInputs = { + capture_target: ScreenCaptureTarget; + capture_system_audio?: boolean; + mode: RecordingMode; +}; +export type StereoMode = "stereo" | "monoL" | "monoR"; +export type StudioRecordingMeta = + | { segment: SingleSegment } + | { inner: MultipleSegments }; +export type TargetUnderCursor = { + display_id: DisplayId | null; + window: WindowUnderCursor | null; + screen: ScreenUnderCursor | null; +}; +export type TimelineConfiguration = { + segments: TimelineSegment[]; + zoomSegments: ZoomSegment[]; +}; +export type TimelineSegment = { + recordingSegment?: number; + timescale: number; + start: number; + end: number; +}; +export type UploadMode = + | { Initial: { pre_created_video: VideoUploadInfo | null } } + | "Reupload"; +export type UploadProgress = { progress: number }; +export type UploadResult = + | { Success: string } + | "NotAuthenticated" + | "PlanCheckFailed" + | "UpgradeRequired"; +export type Video = { + duration: number; + width: number; + height: number; + fps: number; + start_time: number; +}; +export type VideoMeta = { + path: string; + fps?: number; + /** + * unix time of the first frame + */ + start_time?: number | null; +}; +export type VideoRecordingMetadata = { duration: number; size: number }; +export type VideoUploadInfo = { + id: string; + link: string; + config: S3UploadMeta; +}; +export type WindowId = string; +export type WindowUnderCursor = { + id: WindowId; + app_name: string; + bounds: LogicalBounds; + icon: string | null; +}; +export type XY = { x: T; y: T }; +export type ZoomMode = "auto" | { manual: { x: number; y: number } }; +export type ZoomSegment = { + start: number; + end: number; + amount: number; + mode: ZoomMode; +}; /** tauri-specta globals **/ import { + type Channel as TAURI_CHANNEL, invoke as TAURI_INVOKE, - Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; +import type { WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; type __EventObj__ = { listen: ( @@ -480,9 +827,8 @@ function __makeEvents__>( ) { return new Proxy( {} as unknown as { - [K in keyof T]: __EventObj__ & { - (handle: __WebviewWindow__): __EventObj__; - }; + [K in keyof T]: __EventObj__ & + ((handle: __WebviewWindow__) => __EventObj__); }, { get: (_, event) => { diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index f12f24f593..7f6519957b 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -4,83 +4,83 @@ // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import // biome-ignore lint: disable -export {} +export {}; declare global { - const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"] - const IconCapAudioOn: typeof import('~icons/cap/audio-on.jsx')['default'] - const IconCapBgBlur: typeof import('~icons/cap/bg-blur.jsx')['default'] - const IconCapCamera: typeof import('~icons/cap/camera.jsx')['default'] - const IconCapCaptions: typeof import('~icons/cap/captions.jsx')['default'] - const IconCapChevronDown: typeof import('~icons/cap/chevron-down.jsx')['default'] - const IconCapCircle: typeof import("~icons/cap/circle.jsx")["default"] - const IconCapCircleCheck: typeof import('~icons/cap/circle-check.jsx')['default'] - const IconCapCirclePlus: typeof import('~icons/cap/circle-plus.jsx')['default'] - const IconCapCircleX: typeof import('~icons/cap/circle-x.jsx')['default'] - const IconCapCopy: typeof import('~icons/cap/copy.jsx')['default'] - const IconCapCorners: typeof import('~icons/cap/corners.jsx')['default'] - const IconCapCrop: typeof import('~icons/cap/crop.jsx')['default'] - const IconCapCursor: typeof import('~icons/cap/cursor.jsx')['default'] - const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"] - const IconCapEnlarge: typeof import('~icons/cap/enlarge.jsx')['default'] - const IconCapFile: typeof import('~icons/cap/file.jsx')['default'] - const IconCapFilmCut: typeof import('~icons/cap/film-cut.jsx')['default'] - const IconCapGauge: typeof import('~icons/cap/gauge.jsx')['default'] - const IconCapHotkeys: typeof import('~icons/cap/hotkeys.jsx')['default'] - const IconCapImage: typeof import('~icons/cap/image.jsx')['default'] - const IconCapInfo: typeof import('~icons/cap/info.jsx')['default'] - const IconCapInstant: typeof import('~icons/cap/instant.jsx')['default'] - const IconCapLayout: typeof import('~icons/cap/layout.jsx')['default'] - const IconCapLink: typeof import('~icons/cap/link.jsx')['default'] - const IconCapLogo: typeof import('~icons/cap/logo.jsx')['default'] - const IconCapLogoFull: typeof import('~icons/cap/logo-full.jsx')['default'] - const IconCapLogoFullDark: typeof import('~icons/cap/logo-full-dark.jsx')['default'] - const IconCapMessageBubble: typeof import('~icons/cap/message-bubble.jsx')['default'] - const IconCapMicrophone: typeof import('~icons/cap/microphone.jsx')['default'] - const IconCapMoreVertical: typeof import('~icons/cap/more-vertical.jsx')['default'] - const IconCapNext: typeof import('~icons/cap/next.jsx')['default'] - const IconCapPadding: typeof import('~icons/cap/padding.jsx')['default'] - const IconCapPause: typeof import('~icons/cap/pause.jsx')['default'] - const IconCapPauseCircle: typeof import('~icons/cap/pause-circle.jsx')['default'] - const IconCapPlay: typeof import('~icons/cap/play.jsx')['default'] - const IconCapPlayCircle: typeof import('~icons/cap/play-circle.jsx')['default'] - const IconCapPresets: typeof import('~icons/cap/presets.jsx')['default'] - const IconCapPrev: typeof import('~icons/cap/prev.jsx')['default'] - const IconCapRedo: typeof import('~icons/cap/redo.jsx')['default'] - const IconCapRestart: typeof import('~icons/cap/restart.jsx')['default'] - const IconCapScissors: typeof import('~icons/cap/scissors.jsx')['default'] - const IconCapSettings: typeof import('~icons/cap/settings.jsx')['default'] - const IconCapShadow: typeof import('~icons/cap/shadow.jsx')['default'] - const IconCapSquare: typeof import("~icons/cap/square.jsx")["default"] - const IconCapStopCircle: typeof import('~icons/cap/stop-circle.jsx')['default'] - const IconCapTrash: typeof import('~icons/cap/trash.jsx')['default'] - const IconCapUndo: typeof import('~icons/cap/undo.jsx')['default'] - const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"] - const IconCapZoomIn: typeof import('~icons/cap/zoom-in.jsx')['default'] - const IconCapZoomOut: typeof import('~icons/cap/zoom-out.jsx')['default'] - const IconHugeiconsEaseCurveControlPoints: typeof import('~icons/hugeicons/ease-curve-control-points.jsx')['default'] - const IconLucideAppWindowMac: typeof import("~icons/lucide/app-window-mac.jsx")["default"] - const IconLucideBell: typeof import('~icons/lucide/bell.jsx')['default'] - const IconLucideBug: typeof import('~icons/lucide/bug.jsx')['default'] - const IconLucideCheck: typeof import('~icons/lucide/check.jsx')['default'] - const IconLucideClock: typeof import('~icons/lucide/clock.jsx')['default'] - const IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"] - const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default'] - const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"] - const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] - const IconLucideGift: typeof import('~icons/lucide/gift.jsx')['default'] - const IconLucideHardDrive: typeof import('~icons/lucide/hard-drive.jsx')['default'] - const IconLucideLoaderCircle: typeof import('~icons/lucide/loader-circle.jsx')['default'] - const IconLucideMessageSquarePlus: typeof import('~icons/lucide/message-square-plus.jsx')['default'] - const IconLucideMicOff: typeof import('~icons/lucide/mic-off.jsx')['default'] - const IconLucideMonitor: typeof import('~icons/lucide/monitor.jsx')['default'] - const IconLucideRectangleHorizontal: typeof import("~icons/lucide/rectangle-horizontal.jsx")["default"] - const IconLucideRotateCcw: typeof import('~icons/lucide/rotate-ccw.jsx')['default'] - const IconLucideSearch: typeof import('~icons/lucide/search.jsx')['default'] - const IconLucideSquarePlay: typeof import('~icons/lucide/square-play.jsx')['default'] - const IconLucideUnplug: typeof import('~icons/lucide/unplug.jsx')['default'] - const IconLucideVolume2: typeof import('~icons/lucide/volume2.jsx')['default'] - const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"] - const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"] - const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"] - const IconPhMonitorBold: typeof import('~icons/ph/monitor-bold.jsx')['default'] + const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"]; + const IconCapAudioOn: typeof import("~icons/cap/audio-on.jsx")["default"]; + const IconCapBgBlur: typeof import("~icons/cap/bg-blur.jsx")["default"]; + const IconCapCamera: typeof import("~icons/cap/camera.jsx")["default"]; + const IconCapCaptions: typeof import("~icons/cap/captions.jsx")["default"]; + const IconCapChevronDown: typeof import("~icons/cap/chevron-down.jsx")["default"]; + const IconCapCircle: typeof import("~icons/cap/circle.jsx")["default"]; + const IconCapCircleCheck: typeof import("~icons/cap/circle-check.jsx")["default"]; + const IconCapCirclePlus: typeof import("~icons/cap/circle-plus.jsx")["default"]; + const IconCapCircleX: typeof import("~icons/cap/circle-x.jsx")["default"]; + const IconCapCopy: typeof import("~icons/cap/copy.jsx")["default"]; + const IconCapCorners: typeof import("~icons/cap/corners.jsx")["default"]; + const IconCapCrop: typeof import("~icons/cap/crop.jsx")["default"]; + const IconCapCursor: typeof import("~icons/cap/cursor.jsx")["default"]; + const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"]; + const IconCapEnlarge: typeof import("~icons/cap/enlarge.jsx")["default"]; + const IconCapFile: typeof import("~icons/cap/file.jsx")["default"]; + const IconCapFilmCut: typeof import("~icons/cap/film-cut.jsx")["default"]; + const IconCapGauge: typeof import("~icons/cap/gauge.jsx")["default"]; + const IconCapHotkeys: typeof import("~icons/cap/hotkeys.jsx")["default"]; + const IconCapImage: typeof import("~icons/cap/image.jsx")["default"]; + const IconCapInfo: typeof import("~icons/cap/info.jsx")["default"]; + const IconCapInstant: typeof import("~icons/cap/instant.jsx")["default"]; + const IconCapLayout: typeof import("~icons/cap/layout.jsx")["default"]; + const IconCapLink: typeof import("~icons/cap/link.jsx")["default"]; + const IconCapLogo: typeof import("~icons/cap/logo.jsx")["default"]; + const IconCapLogoFull: typeof import("~icons/cap/logo-full.jsx")["default"]; + const IconCapLogoFullDark: typeof import("~icons/cap/logo-full-dark.jsx")["default"]; + const IconCapMessageBubble: typeof import("~icons/cap/message-bubble.jsx")["default"]; + const IconCapMicrophone: typeof import("~icons/cap/microphone.jsx")["default"]; + const IconCapMoreVertical: typeof import("~icons/cap/more-vertical.jsx")["default"]; + const IconCapNext: typeof import("~icons/cap/next.jsx")["default"]; + const IconCapPadding: typeof import("~icons/cap/padding.jsx")["default"]; + const IconCapPause: typeof import("~icons/cap/pause.jsx")["default"]; + const IconCapPauseCircle: typeof import("~icons/cap/pause-circle.jsx")["default"]; + const IconCapPlay: typeof import("~icons/cap/play.jsx")["default"]; + const IconCapPlayCircle: typeof import("~icons/cap/play-circle.jsx")["default"]; + const IconCapPresets: typeof import("~icons/cap/presets.jsx")["default"]; + const IconCapPrev: typeof import("~icons/cap/prev.jsx")["default"]; + const IconCapRedo: typeof import("~icons/cap/redo.jsx")["default"]; + const IconCapRestart: typeof import("~icons/cap/restart.jsx")["default"]; + const IconCapScissors: typeof import("~icons/cap/scissors.jsx")["default"]; + const IconCapSettings: typeof import("~icons/cap/settings.jsx")["default"]; + const IconCapShadow: typeof import("~icons/cap/shadow.jsx")["default"]; + const IconCapSquare: typeof import("~icons/cap/square.jsx")["default"]; + const IconCapStopCircle: typeof import("~icons/cap/stop-circle.jsx")["default"]; + const IconCapTrash: typeof import("~icons/cap/trash.jsx")["default"]; + const IconCapUndo: typeof import("~icons/cap/undo.jsx")["default"]; + const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"]; + const IconCapZoomIn: typeof import("~icons/cap/zoom-in.jsx")["default"]; + const IconCapZoomOut: typeof import("~icons/cap/zoom-out.jsx")["default"]; + const IconHugeiconsEaseCurveControlPoints: typeof import("~icons/hugeicons/ease-curve-control-points.jsx")["default"]; + const IconLucideAppWindowMac: typeof import("~icons/lucide/app-window-mac.jsx")["default"]; + const IconLucideBell: typeof import("~icons/lucide/bell.jsx")["default"]; + const IconLucideBug: typeof import("~icons/lucide/bug.jsx")["default"]; + const IconLucideCheck: typeof import("~icons/lucide/check.jsx")["default"]; + const IconLucideClock: typeof import("~icons/lucide/clock.jsx")["default"]; + const IconLucideDatabase: typeof import("~icons/lucide/database.jsx")["default"]; + const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"]; + const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"]; + const IconLucideFolder: typeof import("~icons/lucide/folder.jsx")["default"]; + const IconLucideGift: typeof import("~icons/lucide/gift.jsx")["default"]; + const IconLucideHardDrive: typeof import("~icons/lucide/hard-drive.jsx")["default"]; + const IconLucideLoaderCircle: typeof import("~icons/lucide/loader-circle.jsx")["default"]; + const IconLucideMessageSquarePlus: typeof import("~icons/lucide/message-square-plus.jsx")["default"]; + const IconLucideMicOff: typeof import("~icons/lucide/mic-off.jsx")["default"]; + const IconLucideMonitor: typeof import("~icons/lucide/monitor.jsx")["default"]; + const IconLucideRectangleHorizontal: typeof import("~icons/lucide/rectangle-horizontal.jsx")["default"]; + const IconLucideRotateCcw: typeof import("~icons/lucide/rotate-ccw.jsx")["default"]; + const IconLucideSearch: typeof import("~icons/lucide/search.jsx")["default"]; + const IconLucideSquarePlay: typeof import("~icons/lucide/square-play.jsx")["default"]; + const IconLucideUnplug: typeof import("~icons/lucide/unplug.jsx")["default"]; + const IconLucideVolume2: typeof import("~icons/lucide/volume2.jsx")["default"]; + const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"]; + const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"]; + const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"]; + const IconPhMonitorBold: typeof import("~icons/ph/monitor-bold.jsx")["default"]; } From cc58bcde3b7f109731de90e5f998457c38b1d8ad Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 03:19:10 +0800 Subject: [PATCH 35/47] formatting --- apps/desktop/src-tauri/src/target_select_overlay.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index b355a4ea34..75165c955b 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -10,9 +10,7 @@ use base64::prelude::*; use crate::windows::{CapWindowId, ShowCapWindow}; use cap_displays::{ DisplayId, WindowId, - bounds::{ - LogicalBounds, PhysicalSize, - }, + bounds::{LogicalBounds, PhysicalSize}, }; use serde::Serialize; use specta::Type; From 15ede687fae26410c349ed568714007965dde350 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 03:33:18 +0800 Subject: [PATCH 36/47] types --- apps/desktop/package.json | 2 +- .../desktop/src/routes/editor/ExportDialog.tsx | 4 +++- pnpm-lock.yaml | 18 +++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ec1aeeefbe..f7d299f51f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -49,7 +49,7 @@ "@tauri-apps/plugin-os": "^2.3.0", "@tauri-apps/plugin-process": "2.3.0", "@tauri-apps/plugin-shell": "^2.3.0", - "@tauri-apps/plugin-store": "^2.3.0", + "@tauri-apps/plugin-store": "^2.4.0", "@tauri-apps/plugin-updater": "^2.9.0", "@ts-rest/core": "^3.52.1", "@types/react-tooltip": "^4.2.4", diff --git a/apps/desktop/src/routes/editor/ExportDialog.tsx b/apps/desktop/src/routes/editor/ExportDialog.tsx index fae79de5cc..9928356958 100644 --- a/apps/desktop/src/routes/editor/ExportDialog.tsx +++ b/apps/desktop/src/routes/editor/ExportDialog.tsx @@ -33,6 +33,8 @@ import { type ExportSettings, events, type FramesRendered, + Mp4ExportSettings, + GifExportSettings, } from "~/utils/tauri"; import { type RenderState, useEditorContext } from "./context"; import { RESOLUTION_OPTIONS } from "./Header"; @@ -140,7 +142,7 @@ export function ExportDialog() { y: settings.resolution.height, }, compression: settings.compression, - }, + } as (Mp4ExportSettings & GifExportSettings) & { format: "Mp4" | "Gif" }, onProgress, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f26ffb15d..7811110fbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,7 +97,7 @@ importers: version: 1.5.1(solid-js@1.9.6) '@solid-primitives/storage': specifier: ^4.0.0 - version: 4.3.2(@tauri-apps/plugin-store@2.3.0)(solid-js@1.9.6) + version: 4.3.2(@tauri-apps/plugin-store@2.4.0)(solid-js@1.9.6) '@solid-primitives/timer': specifier: ^1.3.9 version: 1.4.1(solid-js@1.9.6) @@ -147,8 +147,8 @@ importers: specifier: ^2.3.0 version: 2.3.0 '@tauri-apps/plugin-store': - specifier: ^2.3.0 - version: 2.3.0 + specifier: ^2.4.0 + version: 2.4.0 '@tauri-apps/plugin-updater': specifier: ^2.9.0 version: 2.9.0 @@ -6199,8 +6199,8 @@ packages: '@tauri-apps/plugin-shell@2.3.0': resolution: {integrity: sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA==} - '@tauri-apps/plugin-store@2.3.0': - resolution: {integrity: sha512-mre8er0nXPhyEWQzWCpUd+UnEoBQYcoA5JYlwpwOV9wcxKqlXTGfminpKsE37ic8NUb2BIZqf0QQ9/U3ib2+/A==} + '@tauri-apps/plugin-store@2.4.0': + resolution: {integrity: sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A==} '@tauri-apps/plugin-updater@2.9.0': resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==} @@ -19092,12 +19092,12 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.6) solid-js: 1.9.6 - '@solid-primitives/storage@4.3.2(@tauri-apps/plugin-store@2.3.0)(solid-js@1.9.6)': + '@solid-primitives/storage@4.3.2(@tauri-apps/plugin-store@2.4.0)(solid-js@1.9.6)': dependencies: '@solid-primitives/utils': 6.3.1(solid-js@1.9.6) solid-js: 1.9.6 optionalDependencies: - '@tauri-apps/plugin-store': 2.3.0 + '@tauri-apps/plugin-store': 2.4.0 '@solid-primitives/timer@1.4.1(solid-js@1.9.6)': dependencies: @@ -19657,9 +19657,9 @@ snapshots: dependencies: '@tauri-apps/api': 2.7.0 - '@tauri-apps/plugin-store@2.3.0': + '@tauri-apps/plugin-store@2.4.0': dependencies: - '@tauri-apps/api': 2.7.0 + '@tauri-apps/api': 2.8.0 '@tauri-apps/plugin-updater@2.9.0': dependencies: From 22fe8dfdf87a708e0aeec2191a35588eb245ec6b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 03:33:29 +0800 Subject: [PATCH 37/47] format --- apps/desktop/src/routes/editor/ExportDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/routes/editor/ExportDialog.tsx b/apps/desktop/src/routes/editor/ExportDialog.tsx index 9928356958..c9d33956be 100644 --- a/apps/desktop/src/routes/editor/ExportDialog.tsx +++ b/apps/desktop/src/routes/editor/ExportDialog.tsx @@ -33,8 +33,8 @@ import { type ExportSettings, events, type FramesRendered, - Mp4ExportSettings, - GifExportSettings, + type GifExportSettings, + type Mp4ExportSettings, } from "~/utils/tauri"; import { type RenderState, useEditorContext } from "./context"; import { RESOLUTION_OPTIONS } from "./Header"; From 7358adba903cff578b4636aab7c80971df1870c9 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 12:16:01 +0800 Subject: [PATCH 38/47] round recording crop to nearest 2 on windows --- crates/displays/src/bounds.rs | 7 +++++++ crates/recording/src/sources/screen_capture/mod.rs | 2 +- crates/recording/src/sources/screen_capture/windows.rs | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/displays/src/bounds.rs b/crates/displays/src/bounds.rs index 50d667fa76..cc34ad47c3 100644 --- a/crates/displays/src/bounds.rs +++ b/crates/displays/src/bounds.rs @@ -93,6 +93,13 @@ impl PhysicalSize { pub fn height(&self) -> f64 { self.height } + + pub fn map(&self, f: impl Fn(f64) -> f64) -> Self { + Self { + width: f(self.width), + height: f(self.height), + } + } } #[derive(Clone, Copy, Debug, Type, Serialize, Deserialize, PartialEq)] diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index 334691c20e..4e168156b2 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -351,7 +351,7 @@ impl ScreenCaptureSource { } #[cfg(windows)] - Some(b.size()) + Some(b.size().map(|v| (v / 2.0).floor() * 2.0)) }) .or_else(|| display.physical_size()) .unwrap(); diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index a45921acbf..882de9e567 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -234,7 +234,7 @@ impl PipelineSourceTask for ScreenCaptureSource { pixel_format: AVFrameCapture::PIXEL_FORMAT, crop: config.crop_bounds.map(|b| { let position = b.position(); - let size = b.size(); + let size = b.size().map(|v| (v / 2.0).floor() * 2.0); D3D11_BOX { left: position.x() as u32, From 49d2c2ef6119dc89d6cfa45f0da0186abf652643 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 12:44:30 +0800 Subject: [PATCH 39/47] fix on windows --- crates/displays/src/lib.rs | 2 +- .../src/sources/screen_capture/mod.rs | 5 +-- .../src/sources/screen_capture/windows.rs | 34 +++++++------------ 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/crates/displays/src/lib.rs b/crates/displays/src/lib.rs index cd1cc5fc93..be57e60736 100644 --- a/crates/displays/src/lib.rs +++ b/crates/displays/src/lib.rs @@ -1,7 +1,7 @@ pub mod bounds; pub mod platform; -use bounds::{LogicalBounds, PhysicalSize}; +use bounds::*; pub use platform::{DisplayIdImpl, DisplayImpl, WindowIdImpl, WindowImpl}; use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index 4e168156b2..06182abc2f 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -1,8 +1,5 @@ use cap_cursor_capture::CursorCropBounds; -use cap_displays::{ - Display, DisplayId, Window, WindowId, - bounds::{LogicalBounds, LogicalPosition, PhysicalSize}, -}; +use cap_displays::{Display, DisplayId, Window, WindowId, bounds::*}; use cap_media_info::{AudioInfo, VideoInfo}; use ffmpeg::sys::AV_TIME_BASE_Q; use flume::Sender; diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index 882de9e567..bd129590a9 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -9,6 +9,10 @@ use std::{ }; use tracing::info; +const WINDOW_DURATION: Duration = Duration::from_secs(3); +const LOG_INTERVAL: Duration = Duration::from_secs(5); +const MAX_DROP_RATE_THRESHOLD: f64 = 0.25; + #[derive(Debug)] pub struct AVFrameCapture; @@ -185,7 +189,7 @@ enum SourceError { #[error("NoDisplay: Id '{0}'")] NoDisplay(DisplayId), #[error("AsCaptureItem: {0}")] - AsCaptureItem(windows::core::Error), + AsCaptureItem(::windows::core::Error), } impl PipelineSourceTask for ScreenCaptureSource { @@ -195,21 +199,11 @@ impl PipelineSourceTask for ScreenCaptureSource { ready_signal: crate::pipeline::task::PipelineReadySignal, control_signal: crate::pipeline::control::PipelineControlSignal, ) -> Result<(), String> { - const WINDOW_DURATION: Duration = Duration::from_secs(3); - const LOG_INTERVAL: Duration = Duration::from_secs(5); - const MAX_DROP_RATE_THRESHOLD: f64 = 0.25; - - let video_info = self.video_info; let video_tx = self.video_tx.clone(); let audio_tx = self.audio_tx.clone(); let start_time = self.start_time; - let mut video_i = 0; - let mut audio_i = 0; - - let mut frames_dropped = 0; - // Frame drop rate tracking state let config = self.config.clone(); @@ -254,7 +248,7 @@ impl PipelineSourceTask for ScreenCaptureSource { let capture_item = display .raw_handle() .try_as_capture_item() - .map_err(SourceError::AsCaptureItem); + .map_err(SourceError::AsCaptureItem)?; settings.is_cursor_capture_enabled = Some(config.show_cursor); @@ -375,8 +369,8 @@ impl Message for WindowsScreenCapture { async fn handle( &mut self, - msg: StopCapturing, - ctx: &mut Context, + _: StopCapturing, + _: &mut Context, ) -> Self::Reply { let Some(capturer) = self.capture_handle.take() else { return Err(StopCapturingError::NotCapturing); @@ -395,9 +389,7 @@ impl Message for WindowsScreenCapture { use audio::WindowsAudioCapture; pub mod audio { use super::*; - use cpal::traits::StreamTrait; - use scap_cpal::*; - use scap_ffmpeg::*; + use cpal::{PauseStreamError, PlayStreamError}; #[derive(Actor)] pub struct WindowsAudioCapture { @@ -445,8 +437,8 @@ pub mod audio { async fn handle( &mut self, - msg: StartCapturing, - ctx: &mut Context, + _: StartCapturing, + _: &mut Context, ) -> Self::Reply { self.capturer.play()?; @@ -459,8 +451,8 @@ pub mod audio { async fn handle( &mut self, - msg: StopCapturing, - ctx: &mut Context, + _: StopCapturing, + _: &mut Context, ) -> Self::Reply { self.capturer.pause()?; From 305880410086d848b1699475041e4e0448831dd3 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 13:20:42 +0800 Subject: [PATCH 40/47] use is_valid from windows-capture --- Cargo.lock | 1 - crates/displays/Cargo.toml | 1 - crates/displays/src/platform/win.rs | 47 +++++++++++++++---- crates/recording/examples/recording-cli.rs | 18 +++---- .../src/sources/screen_capture/mod.rs | 2 +- 5 files changed, 50 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fea080414..89f2cc83d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1075,7 +1075,6 @@ dependencies = [ "specta", "tracing", "windows 0.60.0", - "windows-sys 0.59.0", ] [[package]] diff --git a/crates/displays/Cargo.toml b/crates/displays/Cargo.toml index 5cd8174e4f..18070abb9e 100644 --- a/crates/displays/Cargo.toml +++ b/crates/displays/Cargo.toml @@ -37,4 +37,3 @@ windows = { workspace = true, features = [ "Graphics_Capture", "Win32_System_WinRT_Graphics_Capture", ] } -windows-sys = { workspace = true } diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs index 121e8e89ca..aba383557c 100644 --- a/crates/displays/src/platform/win.rs +++ b/crates/displays/src/platform/win.rs @@ -44,12 +44,13 @@ use windows::{ }, Shell::ExtractIconExW, WindowsAndMessaging::{ - DI_FLAGS, DestroyIcon, DrawIconEx, EnumWindows, GCLP_HICON, GW_HWNDNEXT, - GWL_EXSTYLE, GetClassLongPtrW, GetClassNameW, GetCursorPos, GetIconInfo, - GetLayeredWindowAttributes, GetWindow, GetWindowLongW, GetWindowRect, - GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, HICON, ICONINFO, - IsIconic, IsWindowVisible, SendMessageW, WM_GETICON, WS_EX_LAYERED, WS_EX_TOPMOST, - WS_EX_TRANSPARENT, WindowFromPoint, + DI_FLAGS, DestroyIcon, DrawIconEx, EnumChildWindows, EnumWindows, GCLP_HICON, + GW_HWNDNEXT, GWL_EXSTYLE, GWL_STYLE, GetClassLongPtrW, GetClassNameW, + GetClientRect, GetCursorPos, GetDesktopWindow, GetIconInfo, + GetLayeredWindowAttributes, GetWindow, GetWindowLongPtrW, GetWindowLongW, + GetWindowRect, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, + HICON, ICONINFO, IsIconic, IsWindowVisible, SendMessageW, WM_GETICON, WS_CHILD, + WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WindowFromPoint, }, }, }, @@ -681,7 +682,8 @@ impl WindowImpl { }; unsafe { - let _ = EnumWindows( + let _ = EnumChildWindows( + Some(GetDesktopWindow()), Some(enum_windows_proc), LPARAM(std::ptr::addr_of_mut!(context) as isize), ); @@ -1325,7 +1327,6 @@ impl WindowImpl { } pub fn is_on_screen(&self) -> bool { - use ::windows::Win32::UI::WindowsAndMessaging::IsWindowVisible; if !unsafe { IsWindowVisible(self.0) }.as_bool() { return false; } @@ -1359,6 +1360,36 @@ impl WindowImpl { true } + + pub fn is_valid(&self) -> bool { + if !unsafe { IsWindowVisible(self.0).as_bool() } { + return false; + } + + let mut id = 0; + unsafe { GetWindowThreadProcessId(self.0, Some(&mut id)) }; + if id == unsafe { GetCurrentProcessId() } { + return false; + } + + let mut rect = RECT::default(); + let result = unsafe { GetClientRect(self.0, &mut rect) }; + if result.is_ok() { + let styles = unsafe { GetWindowLongPtrW(self.0, GWL_STYLE) }; + let ex_styles = unsafe { GetWindowLongPtrW(self.0, GWL_EXSTYLE) }; + + if (ex_styles & isize::try_from(WS_EX_TOOLWINDOW.0).unwrap()) != 0 { + return false; + } + if (styles & isize::try_from(WS_CHILD.0).unwrap()) != 0 { + return false; + } + } else { + return false; + } + + true + } } fn is_window_valid_for_enumeration(hwnd: HWND, current_process_id: u32) -> bool { diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index 138a451712..e05a1de987 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -1,7 +1,9 @@ use std::time::Duration; use cap_displays::Display; -use cap_recording::{RecordingBaseInputs, screen_capture::ScreenCaptureTarget}; +use cap_recording::{ + RecordingBaseInputs, screen_capture::ScreenCaptureTarget, sources::list_windows, +}; #[tokio::main] pub async fn main() { @@ -21,14 +23,14 @@ pub async fn main() { println!("Recording to directory '{}'", dir.path().display()); - // dbg!( - // list_windows() - // .into_iter() - // .map(|(v, _)| v) - // .collect::>() - // ); + dbg!( + list_windows() + .into_iter() + .map(|(v, _)| v) + .collect::>() + ); - // return; + return; let (handle, _ready_rx) = cap_recording::spawn_studio_recording_actor( "test".to_string(), diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index 06182abc2f..43f74a6ce2 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -420,7 +420,7 @@ pub fn list_windows() -> Vec<(CaptureWindow, Window)> { #[cfg(windows)] { - if !v.raw_handle().is_on_screen() { + if !v.raw_handle().is_valid() || !v.raw_handle().is_on_screen() { return None; } } From 8b773dcf25124b58c50c8f46fb45abfe00a9e847 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 13:25:57 +0800 Subject: [PATCH 41/47] license scap and camera crates as MIT --- LICENSE | 8 +++++++- LICENSE-MIT | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 LICENSE-MIT diff --git a/LICENSE b/LICENSE index ce1326157c..2986532ecd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,11 @@ Copyright (c) 2023-present Cap Software, Inc. - + +Portions of this software are licensed as follows: + +- All code residing in the `cap-camera*` and `scap-* family of crates is licensed under the MIT license as defined in the LICENSE-MIT file +- All third party components are licensed under the original license provided by the owner of the applicable component +- All other content not mentioned above is available under the AGPLv3 license as defined below + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000000..53982646c3 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Cap Software, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 71f1d1be3623a3648733fd5f3e562196b8eb1edf Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 13:50:47 +0800 Subject: [PATCH 42/47] fix intersects for macos and fix Display label --- apps/desktop/src-tauri/src/windows.rs | 47 ++++++++++++++++--- .../src/routes/(window-chrome)/(main).tsx | 2 +- .../src/routes/(window-chrome)/new-main.tsx | 2 +- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 4ca4248c85..9477c1e974 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -255,13 +255,15 @@ impl ShowCapWindow { return Err(tauri::Error::WindowNotFound); }; - let size = display.physical_size().unwrap(); - #[cfg(target_os = "macos")] let position = display.raw_handle().logical_position(); + #[cfg(target_os = "macos")] + let size = display.logical_size().unwrap(); #[cfg(windows)] let position = display.raw_handle().physical_position().unwrap(); + #[cfg(windows)] + let size = display.physical_size().unwrap(); let mut window_builder = self .window_builder( @@ -479,7 +481,8 @@ impl ShowCapWindow { if let Some(main_window) = CapWindowId::Main.get(app) && let (Ok(outer_pos), Ok(outer_size)) = (main_window.outer_position(), main_window.outer_size()) - && display.intersects(outer_pos, outer_size) + && let Ok(scale_factor) = main_window.scale_factor() + && display.intersects(outer_pos, outer_size, scale_factor) { let _ = main_window.minimize(); }; @@ -729,12 +732,44 @@ fn position_traffic_lights_impl( // Credits: tauri-plugin-window-state trait MonitorExt { - fn intersects(&self, position: PhysicalPosition, size: PhysicalSize) -> bool; + fn intersects( + &self, + position: PhysicalPosition, + size: PhysicalSize, + scale: f64, + ) -> bool; } impl MonitorExt for Display { - fn intersects(&self, position: PhysicalPosition, size: PhysicalSize) -> bool { - return false; + fn intersects( + &self, + position: PhysicalPosition, + size: PhysicalSize, + _scale: f64, + ) -> bool { + #[cfg(target_os = "macos")] + { + let Some(bounds) = self.raw_handle().logical_bounds() else { + return false; + }; + + let left = (bounds.position().x() * _scale) as i32; + let right = left + (bounds.size().width() * _scale) as i32; + let top = (bounds.position().y() * _scale) as i32; + let bottom = top + (bounds.size().height() * _scale) as i32; + + [ + (position.x, position.y), + (position.x + size.width as i32, position.y), + (position.x, position.y + size.height as i32), + ( + position.x + size.width as i32, + position.y + size.height as i32, + ), + ] + .into_iter() + .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) + } #[cfg(windows)] { diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index a3133d9458..ba7259f9f3 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -424,7 +424,7 @@ function Page() { ); }} value={options.screen() ?? null} - placeholder="display" + placeholder="Display" optionsEmptyText="No screens found" selected={ rawOptions.captureTarget.variant === "display" || diff --git a/apps/desktop/src/routes/(window-chrome)/new-main.tsx b/apps/desktop/src/routes/(window-chrome)/new-main.tsx index 7e226b97af..b1ac198683 100644 --- a/apps/desktop/src/routes/(window-chrome)/new-main.tsx +++ b/apps/desktop/src/routes/(window-chrome)/new-main.tsx @@ -300,7 +300,7 @@ function Page() { v === "display" ? null : "display", ) } - name="display" + name="Display" /> Date: Fri, 22 Aug 2025 14:33:05 +0800 Subject: [PATCH 43/47] disable a11y lints for desktop --- apps/desktop/src/routes/(window-chrome)/(main).tsx | 2 -- biome.json | 10 +++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index ba7259f9f3..30bf74ca9c 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -337,8 +337,6 @@ function Page() { }> - {/** biome-ignore lint/a11y/useKeyWithClickEvents: */} - {/** biome-ignore lint/a11y/noStaticElementInteractions: */} { if (license.data?.type !== "pro") { diff --git a/biome.json b/biome.json index 6ef5792715..550497650c 100644 --- a/biome.json +++ b/biome.json @@ -30,5 +30,13 @@ "organizeImports": "on" } } - } + }, + "overrides": [ + { + "includes": ["apps/desktop/**/*"], + "linter": { + "rules": { "a11y": "off" } + } + } + ] } From eb9d3bb2650e0a4091bf8fa9a32ee282c107bb6a Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 15:06:24 +0800 Subject: [PATCH 44/47] address coderabbit feedback --- LICENSE | 2 +- crates/camera-avfoundation/Cargo.toml | 1 + crates/camera-directshow/Cargo.toml | 1 + crates/camera-directshow/examples/cli.rs | 2 +- crates/camera-ffmpeg/Cargo.toml | 1 + crates/camera-mediafoundation/Cargo.toml | 1 + crates/camera-windows/Cargo.toml | 1 + crates/camera/Cargo.toml | 3 +- crates/cursor-capture/src/main.rs | 1 - crates/recording/examples/recording-cli.rs | 11 +----- crates/recording/src/pipeline/control.rs | 12 +++++- .../src/sources/screen_capture/macos.rs | 2 +- .../src/sources/screen_capture/mod.rs | 38 ++++++++++++++----- .../src/sources/screen_capture/windows.rs | 1 + crates/scap-cpal/Cargo.toml | 1 + crates/scap-direct3d/Cargo.toml | 1 + crates/scap-direct3d/src/lib.rs | 4 +- crates/scap-ffmpeg/Cargo.toml | 7 ++-- crates/scap-ffmpeg/src/cpal.rs | 6 +-- crates/scap-ffmpeg/src/lib.rs | 2 - crates/scap-screencapturekit/Cargo.toml | 2 +- 21 files changed, 62 insertions(+), 38 deletions(-) diff --git a/LICENSE b/LICENSE index 2986532ecd..1e91201891 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ Copyright (c) 2023-present Cap Software, Inc. Portions of this software are licensed as follows: -- All code residing in the `cap-camera*` and `scap-* family of crates is licensed under the MIT license as defined in the LICENSE-MIT file +- All code residing in the `cap-camera*` and `scap-*` families of crates is licensed under the MIT License (see LICENSE-MIT). - All third party components are licensed under the original license provided by the owner of the applicable component - All other content not mentioned above is available under the AGPLv3 license as defined below diff --git a/crates/camera-avfoundation/Cargo.toml b/crates/camera-avfoundation/Cargo.toml index ec24935a40..4eba06649e 100644 --- a/crates/camera-avfoundation/Cargo.toml +++ b/crates/camera-avfoundation/Cargo.toml @@ -2,6 +2,7 @@ name = "cap-camera-avfoundation" version = "0.1.0" edition = "2024" +license = "MIT" [target.'cfg(target_os = "macos")'.dependencies] cidre = { workspace = true } diff --git a/crates/camera-directshow/Cargo.toml b/crates/camera-directshow/Cargo.toml index e41b1b1b78..b9d780b070 100644 --- a/crates/camera-directshow/Cargo.toml +++ b/crates/camera-directshow/Cargo.toml @@ -2,6 +2,7 @@ name = "cap-camera-directshow" version = "0.1.0" edition = "2024" +license = "MIT" [dependencies] tracing.workspace = true diff --git a/crates/camera-directshow/examples/cli.rs b/crates/camera-directshow/examples/cli.rs index fd4ceed2cc..bad9b5761b 100644 --- a/crates/camera-directshow/examples/cli.rs +++ b/crates/camera-directshow/examples/cli.rs @@ -19,7 +19,7 @@ mod windows { core::Interface, }; - fn main() { + pub fn main() { tracing_subscriber::fmt::init(); unsafe { diff --git a/crates/camera-ffmpeg/Cargo.toml b/crates/camera-ffmpeg/Cargo.toml index 88abdbfb05..c924c9ffb9 100644 --- a/crates/camera-ffmpeg/Cargo.toml +++ b/crates/camera-ffmpeg/Cargo.toml @@ -2,6 +2,7 @@ name = "cap-camera-ffmpeg" version = "0.1.0" edition = "2024" +license = "MIT" [dependencies] ffmpeg = { workspace = true } diff --git a/crates/camera-mediafoundation/Cargo.toml b/crates/camera-mediafoundation/Cargo.toml index b8281db9ac..3bc52bb58b 100644 --- a/crates/camera-mediafoundation/Cargo.toml +++ b/crates/camera-mediafoundation/Cargo.toml @@ -2,6 +2,7 @@ name = "cap-camera-mediafoundation" version = "0.1.0" edition = "2024" +license = "MIT" [dependencies] tracing.workspace = true diff --git a/crates/camera-windows/Cargo.toml b/crates/camera-windows/Cargo.toml index 3602afd368..4960cfcab4 100644 --- a/crates/camera-windows/Cargo.toml +++ b/crates/camera-windows/Cargo.toml @@ -2,6 +2,7 @@ name = "cap-camera-windows" version = "0.1.0" edition = "2024" +license = "MIT" [dependencies] thiserror.workspace = true diff --git a/crates/camera/Cargo.toml b/crates/camera/Cargo.toml index aa63af062a..ff64fcd156 100644 --- a/crates/camera/Cargo.toml +++ b/crates/camera/Cargo.toml @@ -2,9 +2,10 @@ name = "cap-camera" version = "0.1.0" edition = "2024" +license = "MIT" [dependencies] -thiserror.workspace = true +thiserror = { workspace = true } serde = { workspace = true, optional = true } specta = { workspace = true, optional = true } diff --git a/crates/cursor-capture/src/main.rs b/crates/cursor-capture/src/main.rs index e6dc8c6d98..9131818717 100644 --- a/crates/cursor-capture/src/main.rs +++ b/crates/cursor-capture/src/main.rs @@ -7,7 +7,6 @@ fn main() { .relative_to_display(Display::list()[1]) .unwrap() .normalize(); - // .with_crop(LogicalPosition::new(0.0, 0.0), LogicalSize::new(1.0, 1.0)); println!("{position:?}"); } diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index e05a1de987..6ce98cfa00 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -23,21 +23,12 @@ pub async fn main() { println!("Recording to directory '{}'", dir.path().display()); - dbg!( - list_windows() - .into_iter() - .map(|(v, _)| v) - .collect::>() - ); - - return; - let (handle, _ready_rx) = cap_recording::spawn_studio_recording_actor( "test".to_string(), dir.path().into(), RecordingBaseInputs { capture_target: ScreenCaptureTarget::Display { - id: Display::list()[1].id(), + id: Display::primary().id(), }, // ScreenCaptureTarget::Window { // id: Window::list() diff --git a/crates/recording/src/pipeline/control.rs b/crates/recording/src/pipeline/control.rs index 84e8125355..99a8ecb8d3 100644 --- a/crates/recording/src/pipeline/control.rs +++ b/crates/recording/src/pipeline/control.rs @@ -9,11 +9,19 @@ pub enum Control { } pub struct PipelineControlSignal { - pub last_value: Option, - pub receiver: Receiver, + last_value: Option, + receiver: Receiver, } impl PipelineControlSignal { + pub fn receiver(&self) -> &Receiver { + &self.receiver + } + + pub fn last_cached(&self) -> Option { + self.last_value + } + pub fn last(&mut self) -> Option { self.blocking_last_if(false) } diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs index 133bbfff9c..3520cdce7a 100644 --- a/crates/recording/src/sources/screen_capture/macos.rs +++ b/crates/recording/src/sources/screen_capture/macos.rs @@ -233,7 +233,7 @@ impl PipelineSourceTask for ScreenCaptureSource { match futures::future::select( error_rx.recv_async(), - control_signal.receiver.recv_async(), + control_signal.receiver().recv_async(), ) .await { diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index 43f74a6ce2..92183b1733 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -249,8 +249,10 @@ struct Config { pub enum ScreenCaptureInitError { #[error("NoDisplay")] NoDisplay, - #[error("PhysicalSize")] - PhysicalSize, + #[error("NoWindow")] + NoWindow, + #[error("Bounds")] + NoBounds, } impl ScreenCaptureSource { @@ -273,12 +275,18 @@ impl ScreenCaptureSource { let crop_bounds = match target { ScreenCaptureTarget::Display { .. } => None, ScreenCaptureTarget::Window { id } => { - let window = Window::from_id(&id).unwrap(); + let window = Window::from_id(&id).ok_or(ScreenCaptureInitError::NoWindow)?; #[cfg(target_os = "macos")] { - let raw_display_bounds = display.raw_handle().logical_bounds().unwrap(); - let raw_window_bounds = window.raw_handle().logical_bounds().unwrap(); + let raw_display_bounds = display + .raw_handle() + .logical_bounds() + .ok_or(ScreenCaptureInitError::NoBounds)?; + let raw_window_bounds = window + .raw_handle() + .logical_bounds() + .ok_or(ScreenCaptureInitError::NoBounds)?; Some(LogicalBounds::new( LogicalPosition::new( @@ -291,8 +299,14 @@ impl ScreenCaptureSource { #[cfg(windows)] { - let raw_display_position = display.raw_handle().physical_position().unwrap(); - let raw_window_bounds = window.raw_handle().physical_bounds().unwrap(); + let raw_display_position = display + .raw_handle() + .physical_position() + .ok_or(ScreenCaptureInitError::NoBounds)?; + let raw_window_bounds = window + .raw_handle() + .physical_bounds() + .ok_or(ScreenCaptureInitError::NoBounds)?; Some(PhysicalBounds::new( PhysicalPosition::new( @@ -314,8 +328,12 @@ impl ScreenCaptureSource { #[cfg(windows)] { - let raw_display_size = display.physical_size().unwrap(); - let logical_display_size = display.logical_size().unwrap(); + let raw_display_size = display + .physical_size() + .ok_or(ScreenCaptureInitError::NoBounds)?; + let logical_display_size = display + .logical_size() + .ok_or(ScreenCaptureInitError::NoBounds)?; Some(PhysicalBounds::new( PhysicalPosition::new( @@ -351,7 +369,7 @@ impl ScreenCaptureSource { Some(b.size().map(|v| (v / 2.0).floor() * 2.0)) }) .or_else(|| display.physical_size()) - .unwrap(); + .ok_or(ScreenCaptureInitError::NoBounds)?; Ok(Self { config: Config { diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index bd129590a9..1a0a463f55 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -166,6 +166,7 @@ impl Message for FrameHandler { total_count, WINDOW_DURATION.as_secs() ); + let _ = ctx.actor_ref().stop_gracefully().await; return; // return ControlFlow::Break(Err("Recording can't keep up with screen capture. Try reducing your display's resolution or refresh rate.".to_string())); } diff --git a/crates/scap-cpal/Cargo.toml b/crates/scap-cpal/Cargo.toml index 7d20c1f7fc..dae7575133 100644 --- a/crates/scap-cpal/Cargo.toml +++ b/crates/scap-cpal/Cargo.toml @@ -2,6 +2,7 @@ name = "scap-cpal" version = "0.1.0" edition = "2024" +license = "MIT" [dependencies] cpal.workspace = true diff --git a/crates/scap-direct3d/Cargo.toml b/crates/scap-direct3d/Cargo.toml index 2b88b9e907..1684ecab6f 100644 --- a/crates/scap-direct3d/Cargo.toml +++ b/crates/scap-direct3d/Cargo.toml @@ -2,6 +2,7 @@ name = "scap-direct3d" version = "0.1.0" edition = "2024" +license = "MIT" [target.'cfg(windows)'.dependencies] windows = { workspace = true, features = [ diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index 94686c7458..e40bdf5c20 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -96,14 +96,14 @@ impl Settings { pub fn can_is_border_required() -> windows::core::Result { ApiInformation::IsPropertyPresent( &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), - &HSTRING::from("IsCursorCaptureEnabled"), + &HSTRING::from("IsBorderRequired"), ) } pub fn can_is_cursor_capture_enabled() -> windows::core::Result { ApiInformation::IsPropertyPresent( &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"), - &HSTRING::from("IsBorderRequired"), + &HSTRING::from("IsCursorCaptureEnabled"), ) } diff --git a/crates/scap-ffmpeg/Cargo.toml b/crates/scap-ffmpeg/Cargo.toml index a86a8db10d..a318010cde 100644 --- a/crates/scap-ffmpeg/Cargo.toml +++ b/crates/scap-ffmpeg/Cargo.toml @@ -2,17 +2,18 @@ name = "scap-ffmpeg" version = "0.1.0" edition = "2024" +license = "MIT" [dependencies] ffmpeg = { workspace = true } +scap-cpal = { optional = true, path = "../scap-cpal" } +cpal = { workspace = true } + [target.'cfg(windows)'.dependencies] scap-direct3d = { path = "../scap-direct3d" } windows = { workspace = true, features = [] } -scap-cpal = { optional = true, path = "../scap-cpal" } -cpal = { workspace = true } - [target.'cfg(target_os = "macos")'.dependencies] scap-screencapturekit = { path = "../scap-screencapturekit" } cidre = { workspace = true } diff --git a/crates/scap-ffmpeg/src/cpal.rs b/crates/scap-ffmpeg/src/cpal.rs index 0074e82591..c3afb79ff6 100644 --- a/crates/scap-ffmpeg/src/cpal.rs +++ b/crates/scap-ffmpeg/src/cpal.rs @@ -14,11 +14,11 @@ impl DataExt for ::cpal::Data { let mut ffmpeg_frame = ffmpeg::frame::Audio::new( match self.sample_format() { - SampleFormat::F32 => Sample::F32(format_typ), - SampleFormat::F64 => Sample::F64(format_typ), + SampleFormat::U8 => Sample::U8(format_typ), SampleFormat::I16 => Sample::I16(format_typ), SampleFormat::I32 => Sample::I32(format_typ), - SampleFormat::U8 => Sample::U8(format_typ), + SampleFormat::F32 => Sample::F32(format_typ), + SampleFormat::F64 => Sample::F64(format_typ), _ => panic!("Unsupported sample format"), }, sample_count, diff --git a/crates/scap-ffmpeg/src/lib.rs b/crates/scap-ffmpeg/src/lib.rs index 261967d308..93beea3086 100644 --- a/crates/scap-ffmpeg/src/lib.rs +++ b/crates/scap-ffmpeg/src/lib.rs @@ -8,9 +8,7 @@ mod direct3d; #[cfg(windows)] pub use direct3d::*; -#[cfg(windows)] mod cpal; -#[cfg(windows)] pub use cpal::*; pub trait AsFFmpeg { diff --git a/crates/scap-screencapturekit/Cargo.toml b/crates/scap-screencapturekit/Cargo.toml index a202808533..32d74b4611 100644 --- a/crates/scap-screencapturekit/Cargo.toml +++ b/crates/scap-screencapturekit/Cargo.toml @@ -2,7 +2,7 @@ name = "scap-screencapturekit" version = "0.1.0" edition = "2024" - +license = "MIT" [dependencies] futures = { workspace = true } From d4aa70908c82c9a09ef30425863450e433788546 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 17:44:54 +0800 Subject: [PATCH 45/47] macos fail points --- apps/desktop/src-tauri/src/recording.rs | 174 ++++++++++-------- apps/desktop/src/utils/tauri.ts | 8 +- crates/fail/src/lib.rs | 18 +- crates/recording/examples/recording-cli.rs | 4 +- crates/recording/src/pipeline/builder.rs | 18 +- crates/recording/src/pipeline/control.rs | 1 + .../src/sources/screen_capture/macos.rs | 37 ++-- .../src/sources/screen_capture/mod.rs | 2 +- .../src/sources/screen_capture/windows.rs | 20 +- crates/recording/src/studio_recording.rs | 21 ++- 10 files changed, 180 insertions(+), 123 deletions(-) diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 07abfedea6..918d156bd1 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -305,13 +305,6 @@ pub async fn start_recording( state.set_pending_recording(); } - if let Some(window) = CapWindowId::Main.get(&app) { - let _ = general_settings - .map(|v| v.main_window_recording_start_behaviour) - .unwrap_or_default() - .perform(&window); - } - let countdown = general_settings.and_then(|v| v.recording_countdown); let _ = ShowCapWindow::InProgressRecording { countdown } .show(&app) @@ -343,58 +336,39 @@ pub async fn start_recording( println!("spawning actor"); - // done in spawn to catch panics just in case - let actor_done_rx = spawn_actor({ - let state_mtx = Arc::clone(&state_mtx); - let general_settings = general_settings.cloned(); - let capture_target = inputs.capture_target.clone(); - async move { - fail!("recording::spawn_actor"); - let mut state = state_mtx.write().await; - - let base_inputs = cap_recording::RecordingBaseInputs { - capture_target, - capture_system_audio: inputs.capture_system_audio, - mic_feed: &state.mic_feed, - }; - - let (actor, actor_done_rx) = match inputs.mode { - RecordingMode::Studio => { - let (handle, actor_done_rx) = cap_recording::spawn_studio_recording_actor( - id.clone(), - recording_dir.clone(), - base_inputs, - state.camera_feed.clone(), - general_settings - .map(|s| s.custom_cursor_capture) - .unwrap_or_default(), - ) - .await - .map_err(|e| { - error!("Failed to spawn studio recording actor: {e}"); - e.to_string() - })?; - - ( - InProgressRecording::Studio { - handle, - target_name, - inputs, - recording_dir: recording_dir.clone(), - }, - actor_done_rx, - ) - } - RecordingMode::Instant => { - let Some(video_upload_info) = video_upload_info.clone() else { - return Err("Video upload info not found".to_string()); - }; + if let Some(window) = CapWindowId::Main.get(&app) { + let _ = general_settings + .map(|v| v.main_window_recording_start_behaviour) + .unwrap_or_default() + .perform(&window); + } - let (handle, actor_done_rx) = - cap_recording::instant_recording::spawn_instant_recording_actor( + // done in spawn to catch panics just in case + let spawn_actor_res = async { + spawn_actor({ + let state_mtx = Arc::clone(&state_mtx); + let general_settings = general_settings.cloned(); + let capture_target = inputs.capture_target.clone(); + async move { + fail!("recording::spawn_actor"); + let mut state = state_mtx.write().await; + + let base_inputs = cap_recording::RecordingBaseInputs { + capture_target, + capture_system_audio: inputs.capture_system_audio, + mic_feed: &state.mic_feed, + }; + + let (actor, actor_done_rx) = match inputs.mode { + RecordingMode::Studio => { + let (handle, actor_done_rx) = cap_recording::spawn_studio_recording_actor( id.clone(), recording_dir.clone(), base_inputs, + state.camera_feed.clone(), + general_settings + .map(|s| s.custom_cursor_capture) + .unwrap_or_default(), ) .await .map_err(|e| { @@ -402,27 +376,81 @@ pub async fn start_recording( e.to_string() })?; - ( - InProgressRecording::Instant { - handle, - progressive_upload, - video_upload_info, - target_name, - inputs, - recording_dir: recording_dir.clone(), - }, - actor_done_rx, - ) - } - }; + ( + InProgressRecording::Studio { + handle, + target_name, + inputs, + recording_dir: recording_dir.clone(), + }, + actor_done_rx, + ) + } + RecordingMode::Instant => { + let Some(video_upload_info) = video_upload_info.clone() else { + return Err("Video upload info not found".to_string()); + }; - state.set_current_recording(actor); + let (handle, actor_done_rx) = + cap_recording::instant_recording::spawn_instant_recording_actor( + id.clone(), + recording_dir.clone(), + base_inputs, + ) + .await + .map_err(|e| { + error!("Failed to spawn studio recording actor: {e}"); + e.to_string() + })?; + + ( + InProgressRecording::Instant { + handle, + progressive_upload, + video_upload_info, + target_name, + inputs, + recording_dir: recording_dir.clone(), + }, + actor_done_rx, + ) + } + }; - Ok::<_, String>(actor_done_rx) + state.set_current_recording(actor); + + Ok::<_, String>(actor_done_rx) + } + }) + .await + .map_err(|e| format!("Failed to spawn recording actor: {e}"))? + } + .await; + + let actor_done_rx = match spawn_actor_res { + Ok(rx) => rx, + Err(e) => { + let _ = RecordingEvent::Failed { error: e.clone() }.emit(&app); + + let mut dialog = MessageDialogBuilder::new( + app.dialog().clone(), + "An error occurred".to_string(), + e.clone(), + ) + .kind(tauri_plugin_dialog::MessageDialogKind::Error); + + if let Some(window) = CapWindowId::InProgressRecording.get(&app) { + dialog = dialog.parent(&window); + } + + dialog.blocking_show(); + + let mut state = state_mtx.write().await; + let _ = handle_recording_end(app, None, &mut state).await; + + return Err(e); } - }) - .await - .map_err(|e| format!("Failed to spawn recording actor: {e}"))??; + }; let _ = RecordingEvent::Started.emit(&app); diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 9b12a6ecb0..41170046ff 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -588,6 +588,8 @@ export type HotkeysConfiguration = { show: boolean }; export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }; export type InstantRecordingMeta = { fps: number; sample_rate: number | null }; export type JsonValue = [T]; +export type LayoutMode = "default" | "cameraOnly" | "hideCamera"; +export type LayoutSegment = { start: number; end: number; mode?: LayoutMode }; export type LogicalBounds = { position: LogicalPosition; size: LogicalSize }; export type LogicalPosition = { x: number; y: number }; export type LogicalSize = { width: number; height: number }; @@ -741,12 +743,6 @@ export type TargetUnderCursor = { window: WindowUnderCursor | null; screen: ScreenUnderCursor | null; }; -export type LayoutSegment = { - start: number; - end: number; - mode?: "default" | "cameraOnly" | "hideCamera"; -}; - export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; diff --git a/crates/fail/src/lib.rs b/crates/fail/src/lib.rs index 8c3efc7f9d..de8f2038ad 100644 --- a/crates/fail/src/lib.rs +++ b/crates/fail/src/lib.rs @@ -22,15 +22,16 @@ macro_rules! fail { ($name:literal) => { #[cfg(debug_assertions)] { + const NAME: &'static str = concat!(env!("CARGO_PKG_NAME"), "::", $name); + $crate::private::inventory::submit! { - $crate::Fail { name: $name } + $crate::Fail { name: NAME } } - let name: &str = $name; - let should_fail = $crate::private::should_fail(name); + let should_fail = $crate::private::should_fail(NAME); if should_fail { - panic!("Purposely panicked at '{name}'") + panic!("Purposely panicked at '{NAME}'") } } }; @@ -41,15 +42,16 @@ macro_rules! fail_err { ($name:literal, $value:expr) => { #[cfg(debug_assertions)] { + const NAME: &'static str = concat!(env!("CARGO_PKG_NAME"), "::", $name); + $crate::private::inventory::submit! { - $crate::Fail { name: $name } + $crate::Fail { name: NAME } } - let name: &str = $name; - let should_fail = $crate::private::should_fail(name); + let should_fail = $crate::private::should_fail(NAME); if should_fail { - eprintln!("Purposely Err'd at '{name}'"); + eprintln!("Purposely Err'd at '{NAME}'"); Err($value)?; } } diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs index 6ce98cfa00..7cd7dff9e2 100644 --- a/crates/recording/examples/recording-cli.rs +++ b/crates/recording/examples/recording-cli.rs @@ -1,9 +1,7 @@ use std::time::Duration; use cap_displays::Display; -use cap_recording::{ - RecordingBaseInputs, screen_capture::ScreenCaptureTarget, sources::list_windows, -}; +use cap_recording::{RecordingBaseInputs, screen_capture::ScreenCaptureTarget}; #[tokio::main] pub async fn main() { diff --git a/crates/recording/src/pipeline/builder.rs b/crates/recording/src/pipeline/builder.rs index e1e8acba08..6bcba23daf 100644 --- a/crates/recording/src/pipeline/builder.rs +++ b/crates/recording/src/pipeline/builder.rs @@ -40,8 +40,16 @@ impl PipelineBuilder { let name = name.into(); let control_signal = self.control.add_listener(name.clone()); - self.spawn_task(name, move |ready_signal| { - task.run(ready_signal, control_signal) + self.spawn_task(name.clone(), move |ready_signal| { + let res = task.run(ready_signal.clone(), control_signal); + + if let Err(e) = &res + && !ready_signal.is_disconnected() + { + let _ = ready_signal.send(Err(MediaError::Any(format!("Task/{name}/{e}").into()))); + } + + res }); } @@ -123,7 +131,7 @@ impl PipelineBuilder { tokio::time::timeout(Duration::from_secs(5), task.ready_signal.recv_async()) .await .map_err(|_| MediaError::TaskLaunch(format!("task timed out: '{name}'")))? - .map_err(|e| MediaError::TaskLaunch(format!("{name} build / {e}")))??; + .map_err(|e| MediaError::TaskLaunch(format!("'{name}' build / {e}")))??; task_handles.insert(name.clone(), task.join_handle); stop_rx.push(task.done_rx); @@ -139,8 +147,8 @@ impl PipelineBuilder { let task_name = &task_names[index]; let result = match result { - Ok(Err(error)) => Err(format!("Task '{task_name}' failed: {error}")), - Err(_) => Err(format!("Task '{task_name}' failed for unknown reason")), + Ok(Err(error)) => Err(format!("Task/{task_name}/{error}")), + Err(_) => Err(format!("Task/{task_name}/Unknown")), _ => Ok(()), }; diff --git a/crates/recording/src/pipeline/control.rs b/crates/recording/src/pipeline/control.rs index 99a8ecb8d3..8fabc57d44 100644 --- a/crates/recording/src/pipeline/control.rs +++ b/crates/recording/src/pipeline/control.rs @@ -8,6 +8,7 @@ pub enum Control { Shutdown, } +#[derive(Clone)] pub struct PipelineControlSignal { last_value: Option, receiver: Receiver, diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs index 3520cdce7a..57dbb5d4e2 100644 --- a/crates/recording/src/sources/screen_capture/macos.rs +++ b/crates/recording/src/sources/screen_capture/macos.rs @@ -115,6 +115,10 @@ enum SourceError { NoDisplay(DisplayId), #[error("AsContentFilter")] AsContentFilter, + #[error("CreateActor: {0}")] + CreateActor(arc::R), + #[error("StartCapturing/{0}")] + StartCapturing(SendError), #[error("DidStopWithError: {0}")] DidStopWithError(arc::R), } @@ -207,30 +211,22 @@ impl PipelineSourceTask for ScreenCaptureSource { frame_handler.recipient(), error_tx.clone(), ) - .unwrap(), + .map_err(SourceError::CreateActor)?, ); let stop_recipient = capturer.clone().reply_recipient::(); - let _ = capturer.ask(StartCapturing).send().await.unwrap(); + let _ = capturer + .ask(StartCapturing) + .send() + .await + .map_err(SourceError::StartCapturing)?; let _ = ready_signal.send(Ok(())); loop { use futures::future::Either; - let check_err = || { - use cidre::ns; - - Result::<_, arc::R>::Ok(cap_fail::fail_err!( - "macos screen capture startup error", - ns::Error::with_domain(ns::ErrorDomain::os_status(), 1, None) - )) - }; - if let Err(e) = check_err() { - let _ = error_tx.send(e); - } - match futures::future::select( error_rx.recv_async(), control_signal.receiver().recv_async(), @@ -275,13 +271,18 @@ impl ScreenCaptureActor { frame_handler: Recipient, error_tx: Sender>, ) -> Result> { + cap_fail::fail_err!( + "macos::ScreenCaptureActor::new", + ns::Error::with_domain(ns::ErrorDomain::os_status(), 69420, None) + ); + let _error_tx = error_tx.clone(); let capturer_builder = scap_screencapturekit::Capturer::builder(target, settings) .with_output_sample_buf_cb(move |frame| { let check_err = || { Result::<_, arc::R>::Ok(cap_fail::fail_err!( - "macos screen capture frame error", - ns::Error::with_domain(ns::ErrorDomain::os_status(), 1, None) + "macos::ScreenCaptureActor output_sample_buf", + ns::Error::with_domain(ns::ErrorDomain::os_status(), 69420, None) )) }; if let Err(e) = check_err() { @@ -313,9 +314,11 @@ pub struct NewFrame(pub scap_screencapturekit::Frame); pub struct CaptureError(pub arc::R); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, thiserror::Error)] pub enum StartCapturingError { + #[error("AlreadyCapturing")] AlreadyCapturing, + #[error("Start: {0}")] Start(arc::R), } diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs index 92183b1733..34a1a66d41 100644 --- a/crates/recording/src/sources/screen_capture/mod.rs +++ b/crates/recording/src/sources/screen_capture/mod.rs @@ -266,7 +266,7 @@ impl ScreenCaptureSource { start_time: SystemTime, tokio_handle: tokio::runtime::Handle, ) -> Result { - cap_fail::fail!("media::screen_capture::init"); + cap_fail::fail!("ScreenCaptureSource::init"); let display = target.display().ok_or(ScreenCaptureInitError::NoDisplay)?; diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index 1a0a463f55..a222a23747 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -225,7 +225,6 @@ impl PipelineSourceTask for ScreenCaptureSource { }); let mut settings = scap_direct3d::Settings { - is_border_required: Some(false), pixel_format: AVFrameCapture::PIXEL_FORMAT, crop: config.crop_bounds.map(|b| { let position = b.position(); @@ -243,6 +242,19 @@ impl PipelineSourceTask for ScreenCaptureSource { ..Default::default() }; + if let Ok(true) = scap_direct3d::Settings::can_is_border_required() { + settings.is_border_required = Some(false); + } + + if let Ok(true) = scap_direct3d::Settings::can_is_cursor_capture_enabled() { + settings.is_cursor_capture_enabled = Some(config.show_cursor); + } + + if let Ok(true) = scap_direct3d::Settings::can_min_update_interval() { + settings.min_update_interval = + Some(Duration::from_secs_f64(1.0 / config.fps as f64)); + } + let display = Display::from_id(&config.display) .ok_or_else(|| SourceError::NoDisplay(config.display))?; @@ -251,8 +263,6 @@ impl PipelineSourceTask for ScreenCaptureSource { .try_as_capture_item() .map_err(SourceError::AsCaptureItem)?; - settings.is_cursor_capture_enabled = Some(config.show_cursor); - let _ = capturer .ask(StartCapturing { target: capture_item, @@ -339,6 +349,8 @@ impl Message for WindowsScreenCapture { let capturer = scap_direct3d::Capturer::new(msg.target, msg.settings); + trace!("Starting capturer with settings: {:?}", &msg.settings); + let capture_handle = capturer .start( move |frame| { @@ -359,6 +371,8 @@ impl Message for WindowsScreenCapture { ) .map_err(StartCapturingError::Inner)?; + info!("Capturer started"); + self.capture_handle = Some(capture_handle); Ok(()) diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs index 13fa7a5d80..8011c0a158 100644 --- a/crates/recording/src/studio_recording.rs +++ b/crates/recording/src/studio_recording.rs @@ -645,6 +645,10 @@ pub enum CreateSegmentPipelineError { NoDisplay, #[error("NoBounds")] NoBounds, + #[error("PipelineBuild/{0}")] + PipelineBuild(MediaError), + #[error("PipelinePlay/{0}")] + PipelinePlay(MediaError), #[error("Actor/{0}")] Actor(#[from] ActorError), #[error("{0}")] @@ -884,6 +888,16 @@ async fn create_segment_pipeline( None }; + let (mut pipeline, pipeline_done_rx) = pipeline_builder + .build() + .await + .map_err(CreateSegmentPipelineError::PipelineBuild)?; + + pipeline + .play() + .await + .map_err(CreateSegmentPipelineError::PipelinePlay)?; + let cursor = custom_cursor_capture.then(move || { let cursor = spawn_cursor_recorder( crop_bounds, @@ -900,13 +914,6 @@ async fn create_segment_pipeline( } }); - let (mut pipeline, pipeline_done_rx) = pipeline_builder - .build() - .await - .map_err(RecordingError::from)?; - - pipeline.play().await.map_err(RecordingError::from)?; - info!("pipeline playing"); Ok(( From 9d4a1c1e722ee9862db9a5be56a9750efdb2821e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 22 Aug 2025 19:23:44 +0800 Subject: [PATCH 46/47] last of windows error handling i think --- apps/desktop/src-tauri/src/windows.rs | 4 +- crates/displays/src/main.rs | 2 +- crates/displays/src/platform/win.rs | 5 +- .../src/sources/screen_capture/macos.rs | 12 +- .../src/sources/screen_capture/windows.rs | 119 ++++++++--- crates/scap-cpal/src/lib.rs | 2 +- crates/scap-direct3d/src/lib.rs | 191 ++++++++++-------- 7 files changed, 214 insertions(+), 121 deletions(-) diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 9477c1e974..d059271b68 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -237,6 +237,7 @@ impl ShowCapWindow { .maximizable(false) .always_on_top(true) .visible_on_all_workspaces(true) + .content_protected(true) .center() .build()?; @@ -490,8 +491,7 @@ impl ShowCapWindow { window } Self::InProgressRecording { countdown } => { - let mut width = 180.0 + 32.0; - + let width = 250.0; let height = 40.0; let window = self diff --git a/crates/displays/src/main.rs b/crates/displays/src/main.rs index e4b9cae41a..d504078070 100644 --- a/crates/displays/src/main.rs +++ b/crates/displays/src/main.rs @@ -4,7 +4,7 @@ fn main() { #[cfg(windows)] { use windows::Win32::UI::HiDpi::{ - PROCESS_DPI_UNAWARE, PROCESS_PER_MONITOR_DPI_AWARE, PROCESS_SYSTEM_DPI_AWARE, + PROCESS_PER_MONITOR_DPI_AWARE, SetProcessDpiAwareness, }; diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs index aba383557c..fc71e10ab8 100644 --- a/crates/displays/src/platform/win.rs +++ b/crates/displays/src/platform/win.rs @@ -38,8 +38,7 @@ use windows::{ }, UI::{ HiDpi::{ - DPI_AWARENESS_UNAWARE, GetDpiForMonitor, GetDpiForWindow, GetProcessDpiAwareness, - MDT_DEFAULT, MDT_EFFECTIVE_DPI, MDT_RAW_DPI, PROCESS_DPI_UNAWARE, + GetDpiForMonitor, GetDpiForWindow, GetProcessDpiAwareness, MDT_EFFECTIVE_DPI, PROCESS_PER_MONITOR_DPI_AWARE, }, Shell::ExtractIconExW, @@ -58,7 +57,7 @@ use windows::{ }; use crate::bounds::{ - LogicalBounds, LogicalPosition, LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, + LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, }; // All of this assumes PROCESS_PER_MONITOR_DPI_AWARE diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs index 57dbb5d4e2..cd41780b4b 100644 --- a/crates/recording/src/sources/screen_capture/macos.rs +++ b/crates/recording/src/sources/screen_capture/macos.rs @@ -218,12 +218,16 @@ impl PipelineSourceTask for ScreenCaptureSource { let _ = capturer .ask(StartCapturing) - .send() .await .map_err(SourceError::StartCapturing)?; let _ = ready_signal.send(Ok(())); + let stop = async move { + let _ = capturer.ask(StopCapturing).await; + let _ = capturer.stop_gracefully().await; + }; + loop { use futures::future::Either; @@ -235,19 +239,19 @@ impl PipelineSourceTask for ScreenCaptureSource { { Either::Left((Ok(error), _)) => { error!("Error capturing screen: {}", error); - let _ = stop_recipient.ask(StopCapturing).await; + stop.await; return Err(SourceError::DidStopWithError(error)); } Either::Right((Ok(ctrl), _)) => { if let Control::Shutdown = ctrl { - let _ = stop_recipient.ask(StopCapturing).await; + stop.await; return Ok(()); } } _ => { warn!("Screen capture recv channels shutdown, exiting."); - let _ = stop_recipient.ask(StopCapturing).await; + stop.await; return Ok(()); } diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index a222a23747..e8edaae4e2 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -1,5 +1,6 @@ use super::*; use ::windows::{Graphics::Capture::GraphicsCaptureItem, Win32::Graphics::Direct3D11::D3D11_BOX}; +use cap_fail::fail_err; use cpal::traits::{DeviceTrait, HostTrait}; use kameo::prelude::*; use scap_ffmpeg::*; @@ -7,7 +8,7 @@ use std::{ collections::VecDeque, time::{Duration, Instant}, }; -use tracing::info; +use tracing::{info, trace}; const WINDOW_DURATION: Duration = Duration::from_secs(3); const LOG_INTERVAL: Duration = Duration::from_secs(5); @@ -41,7 +42,7 @@ impl ScreenCaptureFormat for AVFrameCapture { } struct FrameHandler { - capturer: WeakActorRef, + capturer: WeakActorRef, start_time: SystemTime, frames_dropped: u32, last_cleanup: Instant, @@ -191,6 +192,14 @@ enum SourceError { NoDisplay(DisplayId), #[error("AsCaptureItem: {0}")] AsCaptureItem(::windows::core::Error), + #[error("StartCapturingVideo/{0}")] + StartCapturingVideo(SendError), + #[error("CreateAudioCapture/{0}")] + CreateAudioCapture(scap_cpal::CapturerError), + #[error("StartCapturingAudio/{0}")] + StartCapturingAudio(SendError), + #[error("Closed")] + Closed, } impl PipelineSourceTask for ScreenCaptureSource { @@ -210,9 +219,8 @@ impl PipelineSourceTask for ScreenCaptureSource { self.tokio_handle .block_on(async move { - let capturer = WindowsScreenCapture::spawn(WindowsScreenCapture::new()); - - let stop_recipient = capturer.clone().reply_recipient::(); + let (error_tx, error_rx) = flume::bounded(1); + let capturer = ScreenCaptureActor::spawn(ScreenCaptureActor::new(error_tx)); let frame_handler = FrameHandler::spawn(FrameHandler { capturer: capturer.downgrade(), @@ -263,21 +271,25 @@ impl PipelineSourceTask for ScreenCaptureSource { .try_as_capture_item() .map_err(SourceError::AsCaptureItem)?; - let _ = capturer + capturer .ask(StartCapturing { target: capture_item, settings, frame_handler: frame_handler.clone().recipient(), }) - .send() - .await; + .await + .map_err(SourceError::StartCapturingVideo)?; let audio_capture = if let Some(audio_tx) = audio_tx { let audio_capture = WindowsAudioCapture::spawn( - WindowsAudioCapture::new(audio_tx, start_time).unwrap(), + WindowsAudioCapture::new(audio_tx, start_time) + .map_err(SourceError::CreateAudioCapture)?, ); - let _ = audio_capture.ask(audio::StartCapturing).send().await; + audio_capture + .ask(audio::StartCapturing) + .await + .map_err(SourceError::StartCapturingAudio)?; Some(audio_capture) } else { @@ -286,37 +298,66 @@ impl PipelineSourceTask for ScreenCaptureSource { let _ = ready_signal.send(Ok(())); - while let Ok(msg) = control_signal.receiver.recv_async().await { - if let Control::Shutdown = msg { - let _ = stop_recipient.ask(StopCapturing).await; + let stop = async move { + let _ = capturer.ask(StopCapturing).await; + let _ = capturer.stop_gracefully().await; + + if let Some(audio_capture) = audio_capture { + let _ = audio_capture.ask(StopCapturing).await; + let _ = audio_capture.stop_gracefully().await; + } + }; - if let Some(audio_capture) = audio_capture { - let _ = audio_capture.ask(StopCapturing).await; + loop { + use futures::future::Either; + + match futures::future::select( + error_rx.recv_async(), + control_signal.receiver().recv_async(), + ) + .await + { + Either::Left((Ok(_), _)) => { + error!("Screen capture closed"); + stop.await; + return Err(SourceError::Closed); } + Either::Right((Ok(ctrl), _)) => { + if let Control::Shutdown = ctrl { + stop.await; + return Ok(()); + } + } + _ => { + warn!("Screen capture recv channels shutdown, exiting."); + + stop.await; - break; + return Ok(()); + } } } - - Ok::<_, SourceError>(()) }) .map_err(|e| e.to_string()) } } #[derive(Actor)] -pub struct WindowsScreenCapture { - capture_handle: Option, +struct ScreenCaptureActor { + capture_handle: Option, + error_tx: Sender<()>, } -impl WindowsScreenCapture { - pub fn new() -> Self { +impl ScreenCaptureActor { + pub fn new(error_tx: Sender<()>) -> Self { Self { capture_handle: None, + error_tx, } } } +#[derive(Clone)] pub struct StartCapturing { pub target: GraphicsCaptureItem, pub settings: scap_direct3d::Settings, @@ -324,10 +365,14 @@ pub struct StartCapturing { // error_handler: Option>, } -#[derive(Debug)] +#[derive(Debug, Clone, thiserror::Error)] pub enum StartCapturingError { + #[error("AlreadyCapturing")] AlreadyCapturing, - Inner(scap_direct3d::StartCapturerError), + #[error("CreateCapturer/{0}")] + CreateCapturer(scap_direct3d::NewCapturerError), + #[error("StartCapturer/{0}")] + StartCapturer(scap_direct3d::StartCapturerError), } pub struct NewFrame { @@ -335,7 +380,7 @@ pub struct NewFrame { pub display_time: SystemTime, } -impl Message for WindowsScreenCapture { +impl Message for ScreenCaptureActor { type Reply = Result<(), StartCapturingError>; async fn handle( @@ -347,11 +392,18 @@ impl Message for WindowsScreenCapture { return Err(StartCapturingError::AlreadyCapturing); } - let capturer = scap_direct3d::Capturer::new(msg.target, msg.settings); + fail_err!( + "WindowsScreenCapture.StartCapturing", + StartCapturingError::CreateCapturer(scap_direct3d::NewCapturerError::NotSupported) + ); trace!("Starting capturer with settings: {:?}", &msg.settings); - let capture_handle = capturer + let mut capture_handle = scap_direct3d::Capturer::new(msg.target, msg.settings) + .map_err(StartCapturingError::CreateCapturer)?; + + let error_tx = self.error_tx.clone(); + capture_handle .start( move |frame| { let display_time = SystemTime::now(); @@ -367,9 +419,13 @@ impl Message for WindowsScreenCapture { Ok(()) }, - || Ok(()), + move || { + let _ = error_tx.send(()); + + Ok(()) + }, ) - .map_err(StartCapturingError::Inner)?; + .map_err(StartCapturingError::StartCapturer)?; info!("Capturer started"); @@ -379,7 +435,7 @@ impl Message for WindowsScreenCapture { } } -impl Message for WindowsScreenCapture { +impl Message for ScreenCaptureActor { type Reply = Result<(), StopCapturingError>; async fn handle( @@ -387,7 +443,7 @@ impl Message for WindowsScreenCapture { _: StopCapturing, _: &mut Context, ) -> Self::Reply { - let Some(capturer) = self.capture_handle.take() else { + let Some(mut capturer) = self.capture_handle.take() else { return Err(StopCapturingError::NotCapturing); }; @@ -445,6 +501,7 @@ pub mod audio { } } + #[derive(Clone)] pub struct StartCapturing; impl Message for WindowsAudioCapture { diff --git a/crates/scap-cpal/src/lib.rs b/crates/scap-cpal/src/lib.rs index b4b71f08fd..73a89c3add 100644 --- a/crates/scap-cpal/src/lib.rs +++ b/crates/scap-cpal/src/lib.rs @@ -4,7 +4,7 @@ use cpal::{ }; use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Clone, Error, Debug)] pub enum CapturerError { #[error("NoDevice")] NoDevice, diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index e40bdf5c20..cca7ce570c 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -9,6 +9,7 @@ use std::{ atomic::{AtomicBool, Ordering}, mpsc::RecvError, }, + thread::JoinHandle, time::Duration, }; @@ -22,30 +23,27 @@ use windows::{ DirectX::{Direct3D11::IDirect3DDevice, DirectXPixelFormat}, }, Win32::{ - Foundation::{HANDLE, HMODULE, LPARAM, POINT, S_FALSE, WPARAM}, + Foundation::{HANDLE, HMODULE, LPARAM, S_FALSE, WPARAM}, Graphics::{ Direct3D::D3D_DRIVER_TYPE_HARDWARE, Direct3D11::{ D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, - D3D11_CPU_ACCESS_FLAG, D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, - D3D11_MAP_READ_WRITE, D3D11_MAPPED_SUBRESOURCE, D3D11_RESOURCE_MISC_FLAG, - D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING, - D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, + D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, D3D11_MAP_READ_WRITE, + D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, + D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING, D3D11CreateDevice, ID3D11Device, + ID3D11DeviceContext, ID3D11Texture2D, }, Dxgi::{ Common::{DXGI_FORMAT, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_SAMPLE_DESC}, IDXGIDevice, }, - Gdi::{HMONITOR, MONITOR_DEFAULTTONULL, MonitorFromPoint}, }, System::{ Threading::GetThreadId, WinRT::{ CreateDispatcherQueueController, DQTAT_COM_NONE, DQTYPE_THREAD_CURRENT, Direct3D11::{CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess}, - DispatcherQueueOptions, - Graphics::Capture::IGraphicsCaptureItemInterop, - RO_INIT_MULTITHREADED, RoInitialize, + DispatcherQueueOptions, RO_INIT_MULTITHREADED, RoInitialize, }, }, UI::WindowsAndMessaging::{ @@ -83,7 +81,7 @@ pub fn is_supported() -> windows::core::Result { )? && GraphicsCaptureSession::IsSupported()?) } -#[derive(Default, Debug)] +#[derive(Clone, Default, Debug)] pub struct Settings { pub is_border_required: Option, pub is_cursor_capture_enabled: Option, @@ -115,8 +113,8 @@ impl Settings { } } -#[derive(Debug, thiserror::Error)] -pub enum StartCapturerError { +#[derive(Clone, Debug, thiserror::Error)] +pub enum NewCapturerError { #[error("NotSupported")] NotSupported, #[error("BorderNotSupported")] @@ -125,8 +123,8 @@ pub enum StartCapturerError { CursorNotSupported, #[error("UpdateIntervalNotSupported")] UpdateIntervalNotSupported, - #[error("CreateRunner: {0}")] - CreateRunner(#[from] CreateRunnerError), + #[error("CreateRunner/{0}")] + CreateRunner(#[from] StartRunnerError), #[error("RecvTimeout")] RecvTimeout(#[from] RecvError), #[error("Other: {0}")] @@ -134,100 +132,140 @@ pub enum StartCapturerError { } pub struct Capturer { + stop_flag: Arc, item: GraphicsCaptureItem, settings: Settings, + thread_handle: Option>, } impl Capturer { - pub fn new(item: GraphicsCaptureItem, settings: Settings) -> Self { - Self { item, settings } - } - - pub fn start( - self, - callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, - closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static, - ) -> Result { + pub fn new( + item: GraphicsCaptureItem, + settings: Settings, + ) -> Result { if !is_supported()? { - return Err(StartCapturerError::NotSupported); + return Err(NewCapturerError::NotSupported); } - if self.settings.is_border_required.is_some() && !Settings::can_is_border_required()? { - return Err(StartCapturerError::BorderNotSupported); + if settings.is_border_required.is_some() && !Settings::can_is_border_required()? { + return Err(NewCapturerError::BorderNotSupported); } - if self.settings.is_cursor_capture_enabled.is_some() + if settings.is_cursor_capture_enabled.is_some() && !Settings::can_is_cursor_capture_enabled()? { - return Err(StartCapturerError::CursorNotSupported); + return Err(NewCapturerError::CursorNotSupported); } - if self.settings.min_update_interval.is_some() && !Settings::can_min_update_interval()? { - return Err(StartCapturerError::UpdateIntervalNotSupported); + if settings.min_update_interval.is_some() && !Settings::can_min_update_interval()? { + return Err(NewCapturerError::UpdateIntervalNotSupported); } let stop_flag = Arc::new(AtomicBool::new(false)); + + Ok(Capturer { + stop_flag, + item, + settings, + thread_handle: None, + }) + } +} + +#[derive(Clone, Debug, thiserror::Error)] +pub enum StartCapturerError { + #[error("AlreadyStarted")] + AlreadyStarted, + #[error("StartFailed/{0}")] + StartFailed(StartRunnerError), + #[error("RecvFailed")] + RecvFailed(RecvError), +} +impl Capturer { + pub fn start( + &mut self, + callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, + closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static, + ) -> Result<(), StartCapturerError> { + if self.thread_handle.is_some() { + return Err(StartCapturerError::AlreadyStarted); + } + let (started_tx, started_rx) = std::sync::mpsc::channel(); + + let item = self.item.clone(); + let settings = self.settings.clone(); + let stop_flag = self.stop_flag.clone(); + let thread_handle = std::thread::spawn({ - let stop_flag = stop_flag.clone(); move || { - let runner = Runner::start( - self.item, - self.settings, - callback, - closed_callback, - stop_flag, - ); - - let runner = match runner { + if let Err(e) = unsafe { RoInitialize(RO_INIT_MULTITHREADED) } + && e.code() != S_FALSE + { + return; + // return Err(CreateRunnerError::FailedToInitializeWinRT); + } + + match Runner::start(item, settings, callback, closed_callback, stop_flag) { Ok(runner) => { - started_tx.send(Ok(())); - runner + let _ = started_tx.send(Ok(())); + + runner.run(); } Err(e) => { - started_tx.send(Err(e)); - return; + let _ = started_tx.send(Err(e)); } }; - - runner.run(); } }); - started_rx.recv()??; + started_rx + .recv() + .map_err(StartCapturerError::RecvFailed)? + .map_err(StartCapturerError::StartFailed)?; - Ok(CaptureHandle { - stop_flag, - thread_handle, - }) + self.thread_handle = Some(thread_handle); + + Ok(()) } } -pub struct CaptureHandle { - stop_flag: Arc, - thread_handle: std::thread::JoinHandle<()>, +#[derive(Clone, Debug, thiserror::Error)] +pub enum StopCapturerError { + #[error("NotStarted")] + NotStarted, + #[error("PostMessageFailed")] + PostMessageFailed, + #[error("ThreadJoinFailed")] + ThreadJoinFailed, } -impl CaptureHandle { - pub fn stop(self) -> Result<(), &'static str> { +impl Capturer { + pub fn stop(&mut self) -> Result<(), StopCapturerError> { + let Some(thread_handle) = self.thread_handle.take() else { + return Err(StopCapturerError::NotStarted); + }; + self.stop_flag.store(true, Ordering::Relaxed); - let handle = HANDLE(self.thread_handle.as_raw_handle()); + let handle = HANDLE(thread_handle.as_raw_handle()); let thread_id = unsafe { GetThreadId(handle) }; while let Err(e) = unsafe { PostThreadMessageW(thread_id, WM_QUIT, WPARAM::default(), LPARAM::default()) } { - if self.thread_handle.is_finished() { + if thread_handle.is_finished() { break; } if e.code().0 != -2147023452 { - return Err("Failed to post message"); + return Err(StopCapturerError::PostMessageFailed); } } - self.thread_handle.join().map_err(|_| "Join failed") + thread_handle + .join() + .map_err(|_| StopCapturerError::ThreadJoinFailed) } } @@ -365,8 +403,8 @@ impl<'a> FrameBuffer<'a> { } } -#[derive(Debug, thiserror::Error)] -pub enum CreateRunnerError { +#[derive(Clone, Debug, thiserror::Error)] +pub enum StartRunnerError { #[error("Failed to initialize WinRT")] FailedToInitializeWinRT, #[error("DispatchQueue: {0}")] @@ -391,6 +429,7 @@ pub enum CreateRunnerError { Other(#[from] windows::core::Error), } +#[derive(Clone)] struct Runner { _session: GraphicsCaptureSession, _frame_pool: Direct3D11CaptureFramePool, @@ -403,13 +442,7 @@ impl Runner { mut callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static, mut closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static, stop_flag: Arc, - ) -> Result { - if let Err(e) = unsafe { RoInitialize(RO_INIT_MULTITHREADED) } - && e.code() != S_FALSE - { - return Err(CreateRunnerError::FailedToInitializeWinRT); - } - + ) -> Result { let queue_options = DispatcherQueueOptions { dwSize: std::mem::size_of::() as u32, threadType: DQTYPE_THREAD_CURRENT, @@ -417,7 +450,7 @@ impl Runner { }; let _controller = unsafe { CreateDispatcherQueueController(queue_options) } - .map_err(CreateRunnerError::DispatchQueue)?; + .map_err(StartRunnerError::DispatchQueue)?; let mut d3d_device = None; let mut d3d_context = None; @@ -435,7 +468,7 @@ impl Runner { Some(&mut d3d_context), ) } - .map_err(CreateRunnerError::D3DDevice)?; + .map_err(StartRunnerError::D3DDevice)?; let d3d_device = d3d_device.unwrap(); let d3d_context = d3d_context.unwrap(); @@ -445,7 +478,7 @@ impl Runner { let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }?; inspectable.cast::() })() - .map_err(CreateRunnerError::Direct3DDevice)?; + .map_err(StartRunnerError::Direct3DDevice)?; let frame_pool = Direct3D11CaptureFramePool::Create( &direct3d_device, @@ -453,11 +486,11 @@ impl Runner { 1, item.Size()?, ) - .map_err(CreateRunnerError::FramePool)?; + .map_err(StartRunnerError::FramePool)?; let session = frame_pool .CreateCaptureSession(&item) - .map_err(CreateRunnerError::CaptureSession)?; + .map_err(StartRunnerError::CaptureSession)?; if let Some(border_required) = settings.is_border_required { session.SetIsBorderRequired(border_required)?; @@ -492,9 +525,9 @@ impl Runner { let mut texture = None; unsafe { d3d_device.CreateTexture2D(&desc, None, Some(&mut texture)) } - .map_err(CreateRunnerError::CropTexture)?; + .map_err(StartRunnerError::CropTexture)?; - Ok::<_, CreateRunnerError>((texture.unwrap(), crop)) + Ok::<_, StartRunnerError>((texture.unwrap(), crop)) }) .transpose()?; @@ -556,18 +589,18 @@ impl Runner { }, ), ) - .map_err(CreateRunnerError::RegisterFrameArrived)?; + .map_err(StartRunnerError::RegisterFrameArrived)?; item.Closed( &TypedEventHandler::::new(move |_, _| { closed_callback() }), ) - .map_err(CreateRunnerError::RegisterClosed)?; + .map_err(StartRunnerError::RegisterClosed)?; session .StartCapture() - .map_err(CreateRunnerError::StartCapture)?; + .map_err(StartRunnerError::StartCapture)?; Ok(Self { _session: session, From 97504937e4b311b23448915b96b42ba19006f1bc Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 22 Aug 2025 19:42:14 +0800 Subject: [PATCH 47/47] format --- crates/displays/src/main.rs | 5 +---- crates/displays/src/platform/win.rs | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/displays/src/main.rs b/crates/displays/src/main.rs index d504078070..0a5ff9da16 100644 --- a/crates/displays/src/main.rs +++ b/crates/displays/src/main.rs @@ -3,10 +3,7 @@ use std::time::Duration; fn main() { #[cfg(windows)] { - use windows::Win32::UI::HiDpi::{ - PROCESS_PER_MONITOR_DPI_AWARE, - SetProcessDpiAwareness, - }; + use windows::Win32::UI::HiDpi::{PROCESS_PER_MONITOR_DPI_AWARE, SetProcessDpiAwareness}; unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE).unwrap() }; } diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs index fc71e10ab8..697ed47072 100644 --- a/crates/displays/src/platform/win.rs +++ b/crates/displays/src/platform/win.rs @@ -56,9 +56,7 @@ use windows::{ core::{BOOL, PCWSTR, PWSTR}, }; -use crate::bounds::{ - LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize, -}; +use crate::bounds::{LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize}; // All of this assumes PROCESS_PER_MONITOR_DPI_AWARE //