Skip to content
Merged
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ use tauri_plugin_notification::{NotificationExt, PermissionState};
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_shell::ShellExt;
use tauri_specta::Event;
#[cfg(target_os = "macos")]
use tokio::sync::Mutex;
use tokio::sync::{RwLock, oneshot};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
use tracing::{error, trace};
use upload::{S3UploadMeta, create_or_get_video, upload_image, upload_video};
Expand Down Expand Up @@ -320,13 +322,94 @@ pub struct RequestOpenSettings {
page: String,
}

#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
pub struct RequestScreenCapturePrewarm {
#[serde(default)]
pub force: bool,
}

#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
pub struct NewNotification {
title: String,
body: String,
is_error: bool,
}

#[cfg(target_os = "macos")]
#[derive(Clone, Copy, PartialEq, Eq)]
enum PrewarmState {
Idle,
Warming,
Warmed,
}

#[cfg(target_os = "macos")]
pub(crate) struct ScreenCapturePrewarmer {
state: Mutex<PrewarmState>,
}

#[cfg(target_os = "macos")]
impl Default for ScreenCapturePrewarmer {
fn default() -> Self {
Self {
state: Mutex::new(PrewarmState::Idle),
}
}
}

#[cfg(target_os = "macos")]
impl ScreenCapturePrewarmer {
async fn request(&self, force: bool) {
let should_start = {
let mut state = self.state.lock().await;

if force {
*state = PrewarmState::Idle;
}

match *state {
PrewarmState::Idle => {
*state = PrewarmState::Warming;
true
}
PrewarmState::Warming => {
trace!("ScreenCaptureKit prewarm already in progress");
false
}
PrewarmState::Warmed => {
if force {
*state = PrewarmState::Warming;
true
} else {
trace!("ScreenCaptureKit cache already warmed");
false
}
}
}
};

if !should_start {
return;
}

let warm_start = std::time::Instant::now();
let result = scap_targets::prewarm_shareable_content().await;

let mut state = self.state.lock().await;
match result {
Ok(()) => {
let elapsed_ms = warm_start.elapsed().as_micros() as f64 / 1000.0;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
*state = PrewarmState::Warmed;
trace!(elapsed_ms, "ScreenCaptureKit cache warmed");
}
Err(error) => {
*state = PrewarmState::Idle;
tracing::warn!(error = %error, "ScreenCaptureKit prewarm failed");
}
}
}
}

type ArcLock<T> = Arc<RwLock<T>>;
pub type MutableState<'a, T> = State<'a, Arc<RwLock<T>>>;

Expand Down Expand Up @@ -1937,6 +2020,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
RequestOpenRecordingPicker,
RequestNewScreenshot,
RequestOpenSettings,
RequestScreenCapturePrewarm,
NewNotification,
AuthenticationInvalid,
audio_meter::AudioInputLevelChange,
Expand Down Expand Up @@ -2075,6 +2159,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
fake_window::init(&app);
app.manage(target_select_overlay::WindowFocusManager::default());
app.manage(EditorWindowIds::default());
#[cfg(target_os = "macos")]
app.manage(ScreenCapturePrewarmer::default());

tokio::spawn({
let camera_feed = camera_feed.clone();
Expand Down Expand Up @@ -2204,6 +2290,12 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
.await;
});

#[cfg(target_os = "macos")]
RequestScreenCapturePrewarm::listen_any_spawn(&app, async |event, app| {
let prewarmer = app.state::<ScreenCapturePrewarmer>();
prewarmer.request(event.force).await;
});

let app_handle = app.clone();
app.deep_link().on_open_url(move |event| {
deeplink_actions::handle(&app_handle, event.urls());
Expand Down
18 changes: 16 additions & 2 deletions apps/desktop/src-tauri/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ use tauri::{
AppHandle, LogicalPosition, Manager, Monitor, PhysicalPosition, PhysicalSize, WebviewUrl,
WebviewWindow, WebviewWindowBuilder, Wry,
};
use tauri_specta::Event;
use tokio::sync::RwLock;
use tracing::{debug, error};
use tracing::{debug, error, warn};

use crate::{
App, ArcLock, fake_window,
App, ArcLock, RequestScreenCapturePrewarm, ScreenCapturePrewarmer, fake_window,
general_settings::{AppTheme, GeneralSettingsStore},
permissions,
recording_settings::RecordingTargetMode,
Expand Down Expand Up @@ -274,6 +275,19 @@ impl ShowCapWindow {
crate::platform::set_window_level(window.as_ref().window(), 50);
}

#[cfg(target_os = "macos")]
{
let app_handle = app.clone();
tauri::async_runtime::spawn(async move {
let prewarmer = app_handle.state::<ScreenCapturePrewarmer>();
prewarmer.request(false).await;
});

if let Err(error) = (RequestScreenCapturePrewarm { force: false }).emit(app) {
warn!(%error, "Failed to emit ScreenCaptureKit prewarm event");
}
}

window
}
Self::TargetSelectOverlay { display_id } => {
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ renderFrameEvent: RenderFrameEvent,
requestNewScreenshot: RequestNewScreenshot,
requestOpenRecordingPicker: RequestOpenRecordingPicker,
requestOpenSettings: RequestOpenSettings,
requestScreenCapturePrewarm: RequestScreenCapturePrewarm,
requestStartRecording: RequestStartRecording,
targetUnderCursor: TargetUnderCursor
}>({
Expand All @@ -311,6 +312,7 @@ renderFrameEvent: "render-frame-event",
requestNewScreenshot: "request-new-screenshot",
requestOpenRecordingPicker: "request-open-recording-picker",
requestOpenSettings: "request-open-settings",
requestScreenCapturePrewarm: "request-screen-capture-prewarm",
requestStartRecording: "request-start-recording",
targetUnderCursor: "target-under-cursor"
})
Expand Down Expand Up @@ -432,6 +434,7 @@ export type RenderFrameEvent = { frame_number: number; fps: number; resolution_b
export type RequestNewScreenshot = null
export type RequestOpenRecordingPicker = { target_mode: RecordingTargetMode | null }
export type RequestOpenSettings = { page: string }
export type RequestScreenCapturePrewarm = { force?: boolean }
export type RequestStartRecording = { mode: RecordingMode }
export type S3UploadMeta = { id: string }
export type SceneMode = "default" | "cameraOnly" | "hideCamera"
Expand Down
15 changes: 15 additions & 0 deletions crates/recording/src/capture_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,21 @@ pub async fn create_screen_capture(
) -> Result<ScreenCaptureReturn<ScreenCaptureMethod>, RecordingError> {
let (video_tx, video_rx) = flume::bounded(16);

#[cfg(target_os = "macos")]
{
let warm_start = std::time::Instant::now();
match scap_targets::prewarm_shareable_content().await {
Ok(()) => tracing::trace!(
elapsed_ms = warm_start.elapsed().as_micros() as f64 / 1000.0,
"ScreenCaptureKit cache ensured before capture"
),
Err(error) => tracing::warn!(
error = %error,
"ScreenCaptureKit prewarm failed before capture"
),
}
}

ScreenCaptureSource::<ScreenCaptureMethod>::init(
capture_target,
force_show_cursor,
Expand Down
2 changes: 1 addition & 1 deletion crates/recording/src/pipeline/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl PipelineBuilder {
// TODO: Shut down tasks if launch failed.
for (name, task) in tasks.into_iter() {
// TODO: Wait for these in parallel?
tokio::time::timeout(Duration::from_secs(5), task.ready_signal.recv_async())
tokio::time::timeout(Duration::from_secs(15), task.ready_signal.recv_async())
.await
.map_err(|_| MediaError::TaskLaunch(format!("task timed out: '{name}'")))?
.map_err(|e| MediaError::TaskLaunch(format!("'{name}' build / {e}")))??;
Expand Down
1 change: 1 addition & 0 deletions crates/scap-targets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ core-graphics = "0.24.0"
core-foundation = "0.10.0"
cocoa = "0.26.0"
objc = "0.2.7"
tokio = { workspace = true, features = ["sync"] }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true, features = [
Expand Down
2 changes: 2 additions & 0 deletions crates/scap-targets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ pub mod bounds;
pub mod platform;

use bounds::*;
#[cfg(target_os = "macos")]
pub use platform::prewarm_shareable_content;
pub use platform::{DisplayIdImpl, DisplayImpl, WindowIdImpl, WindowImpl};
use serde::{Deserialize, Serialize};
use specta::Type;
Expand Down
82 changes: 62 additions & 20 deletions crates/scap-targets/src/platform/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ use core_graphics::{
};

use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize};
use tracing::{trace, warn};

mod cache;

pub async fn prewarm_shareable_content() -> Result<(), arc::R<ns::Error>> {
cache::prewarm_shareable_content().await
}

#[derive(Clone, Copy)]
pub struct DisplayImpl(CGDisplay);
Expand Down Expand Up @@ -174,13 +181,17 @@ impl DisplayImpl {

impl DisplayImpl {
pub async fn as_sc(&self) -> Option<arc::R<sc::Display>> {
sc::ShareableContent::current()
.await
.ok()?
.displays()
.iter()
.find(|d| d.display_id().0 == self.0.id)
.map(|v| v.retained())
match cache::get_display(self.0.id).await {
Ok(display) => display,
Err(error) => {
warn!(
display_id = self.0.id,
error = %error,
"Failed to access ScreenCaptureKit display cache"
);
None
}
}
}

pub async fn as_content_filter(&self) -> Option<arc::R<sc::ContentFilter>> {
Expand All @@ -191,13 +202,40 @@ impl DisplayImpl {
&self,
windows: Vec<arc::R<sc::Window>>,
) -> Option<arc::R<sc::ContentFilter>> {
let excluded_windows =
ns::Array::from_slice_retained(windows.into_iter().collect::<Vec<_>>().as_slice());
let lookup_start = std::time::Instant::now();

let display = match cache::get_display(self.0.id).await {
Ok(Some(display)) => display,
Ok(None) => {
warn!(
display_id = self.0.id,
"Display missing from ScreenCaptureKit cache"
);
return None;
}
Err(error) => {
warn!(
display_id = self.0.id,
error = %error,
"Failed to resolve ScreenCaptureKit display"
);
return None;
}
};

let windows = windows.into_iter().collect::<Vec<_>>();
let excluded_windows = ns::Array::from_slice_retained(windows.as_slice());

Some(sc::ContentFilter::with_display_excluding_windows(
self.as_sc().await?.as_ref(),
&excluded_windows,
))
let filter =
sc::ContentFilter::with_display_excluding_windows(display.as_ref(), &excluded_windows);

trace!(
display_id = self.0.id,
elapsed_ms = lookup_start.elapsed().as_micros() as f64 / 1000.0,
"Created ScreenCaptureKit content filter"
);

Some(filter)
}
}

Expand Down Expand Up @@ -449,13 +487,17 @@ impl WindowImpl {
}

pub async fn as_sc(&self) -> Option<arc::R<sc::Window>> {
sc::ShareableContent::current()
.await
.ok()?
.windows()
.iter()
.find(|w| w.id() == self.0)
.map(|v| v.retained())
match cache::get_window(self.0).await {
Ok(window) => window,
Err(error) => {
warn!(
window_id = self.0,
error = %error,
"Failed to access ScreenCaptureKit window cache"
);
None
}
}
}

pub fn display(&self) -> Option<DisplayImpl> {
Expand Down
Loading
Loading