Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0a5fea6
Refactor project config save with custom debounce
richiemcilroy Nov 17, 2025
bd1fc73
fmt
richiemcilroy Nov 17, 2025
b61958c
Merge branch 'main' into editor-perf
richiemcilroy Nov 17, 2025
5d95f24
Handle excluded windows in macOS screen capture
richiemcilroy Nov 17, 2025
dbe8ae1
Refactor waveform rendering in ClipTrack
richiemcilroy Nov 17, 2025
cced653
Improve camera frame forwarding and logging
richiemcilroy Nov 17, 2025
37eb7c8
Update settings.local.json
richiemcilroy Nov 17, 2025
8b403a8
Update recording UI container styles
richiemcilroy Nov 18, 2025
184ad59
lost a days work (I F'D UP)
richiemcilroy Nov 18, 2025
fee3391
Reset camera and mic state on window close and recording end
richiemcilroy Nov 18, 2025
d2b8fd4
Update close button to use window close action
richiemcilroy Nov 18, 2025
6aca28e
Add initializing state to recording flow
richiemcilroy Nov 18, 2025
41fbc5c
Improve camera initialization and recording state handling
richiemcilroy Nov 18, 2025
44a5348
Refactor type casting for currentRecording data
richiemcilroy Nov 18, 2025
6dd3ff1
Hide native camera preview toggle on Windows
richiemcilroy Nov 18, 2025
ce63e02
Add resizing to camera window
richiemcilroy Nov 18, 2025
dd39425
Set Mellow as default for CursorAnimationStyle
richiemcilroy Nov 18, 2025
831fb76
Fix import order in experimental settings route
richiemcilroy Nov 18, 2025
891b122
Improve camera feed sender handling and logging
richiemcilroy Nov 18, 2025
2b504dd
Update camera.tsx
richiemcilroy Nov 18, 2025
d24e92c
Add code formatting guidelines to documentation
richiemcilroy Nov 18, 2025
e6e9320
Improve logging and error context in recording pipeline
richiemcilroy Nov 18, 2025
5de394f
Update apps/desktop/src-tauri/src/camera.rs
richiemcilroy Nov 18, 2025
0066eb1
Add camera overlay bounds update and revert logic
richiemcilroy Nov 18, 2025
dec0d8e
Merge branch 'editor-perf' of https://github.com/CapSoftware/Cap into…
richiemcilroy Nov 18, 2025
05f39bb
Improve countdown animation and fix recording logic
richiemcilroy Nov 18, 2025
fd30b14
Add checked_duration_since for timestamp types
richiemcilroy Nov 18, 2025
a0aeffa
Refactor duration calculation for readability
richiemcilroy Nov 18, 2025
51e2f6e
misc packages
richiemcilroy Nov 18, 2025
6db1d2d
Remove unused setCamera mutation in camera page
richiemcilroy Nov 18, 2025
ff78eba
Clarify and emphasize no code comments policy
richiemcilroy Nov 19, 2025
756d296
Improve camera window resizing and positioning logic
richiemcilroy Nov 19, 2025
0510be8
Add __CAP__ property to Window interface
richiemcilroy Nov 19, 2025
695bb66
Fix requestAnimationFrame cleanup in overlay
richiemcilroy Nov 19, 2025
ef920b2
gen'd files
richiemcilroy Nov 19, 2025
2f75043
clippy bits
richiemcilroy Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 112 additions & 17 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
any::Any,
collections::{HashMap, VecDeque},
error::Error as StdError,
mem,
panic::AssertUnwindSafe,
path::{Path, PathBuf},
str::FromStr,
Expand Down Expand Up @@ -64,6 +65,8 @@
web_api::ManagerExt,
windows::{CapWindowId, ShowCapWindow},
};
#[cfg(target_os = "macos")]
use scap_targets::Window;

#[derive(Clone)]
pub struct InProgressRecordingCommon {
Expand Down Expand Up @@ -107,6 +110,7 @@
#[cfg(target_os = "macos")]
async fn acquire_shareable_content_for_target(
capture_target: &ScreenCaptureTarget,
excluded_windows: &[scap_targets::WindowId],
) -> anyhow::Result<SendableShareableContent> {
let mut refreshed = false;

Expand All @@ -118,13 +122,20 @@
.ok_or_else(|| anyhow!("GetShareableContent/NotAvailable"))?,
);

if !shareable_content_missing_target_display(capture_target, shareable_content.retained())
.await
{
let sc_content = shareable_content.retained();
let missing_display =
shareable_content_missing_target_display(capture_target, sc_content.clone()).await;
let missing_windows =
shareable_content_missing_windows(excluded_windows, sc_content.clone()).await;

if !missing_display && (!missing_windows || refreshed) {
if missing_windows && refreshed {
debug!("Excluded windows missing from refreshed ScreenCaptureKit content");
}
return Ok(shareable_content);
}

if refreshed {
if refreshed && missing_display {
return Err(anyhow!("GetShareableContent/DisplayMissing"));
}

Expand All @@ -150,6 +161,74 @@
}
}

#[cfg(target_os = "macos")]
async fn shareable_content_missing_windows(
excluded_windows: &[scap_targets::WindowId],
shareable_content: cidre::arc::R<cidre::sc::ShareableContent>,
) -> bool {
if excluded_windows.is_empty() {
return false;
}

for window_id in excluded_windows {
let Some(window) = Window::from_id(window_id) else {
continue;
};

if window
.raw_handle()
.as_sc(shareable_content.clone())
.await
.is_none()
{
return true;
}
}

false
}

#[cfg(target_os = "macos")]
async fn prune_excluded_windows_without_shareable_content(
excluded_windows: &mut Vec<scap_targets::WindowId>,
shareable_content: cidre::arc::R<cidre::sc::ShareableContent>,
) {
if excluded_windows.is_empty() {
return;
}

let mut removed = 0usize;
let mut pruned = Vec::with_capacity(excluded_windows.len());
let current = mem::take(excluded_windows);

for window_id in current {
let Some(window) = Window::from_id(&window_id) else {
removed += 1;
continue;
};

if window
.raw_handle()
.as_sc(shareable_content.clone())
.await
.is_some()
{
pruned.push(window_id);
} else {
removed += 1;
}
}

if removed > 0 {
debug!(
removed,
"Dropping excluded windows missing from ScreenCaptureKit content"
);
}

*excluded_windows = pruned;
}

#[cfg(target_os = "macos")]
fn is_shareable_content_error(err: &anyhow::Error) -> bool {
err.chain().any(|cause| {
Expand Down Expand Up @@ -372,17 +451,17 @@
};

if let Some(mic) = settings.mic_name.clone().filter(|_| needs_mic) {
match apply_mic_input(app.state(), Some(mic)).await {
Err(err) => warn!(%err, "Failed to restore microphone input"),
Ok(_) => {}
}

Check warning on line 457 in apps/desktop/src-tauri/src/recording.rs

View workflow job for this annotation

GitHub Actions / Clippy (aarch64-apple-darwin, macos-latest)

you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`

warning: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let` --> apps/desktop/src-tauri/src/recording.rs:454:9 | 454 | / match apply_mic_input(app.state(), Some(mic)).await { 455 | | Err(err) => warn!(%err, "Failed to restore microphone input"), 456 | | Ok(_) => {} 457 | | } | |_________^ help: try: `if let Err(err) = apply_mic_input(app.state(), Some(mic)).await { warn!(%err, "Failed to restore microphone input") }` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.91.0/index.html#single_match = note: `#[warn(clippy::single_match)]` on by default
}

if let Some(camera) = settings.camera_id.clone().filter(|_| needs_camera) {
match apply_camera_input(app.clone(), app.state(), Some(camera)).await {
Err(err) => warn!(%err, "Failed to restore camera input"),
Ok(_) => {}
}

Check warning on line 464 in apps/desktop/src-tauri/src/recording.rs

View workflow job for this annotation

GitHub Actions / Clippy (aarch64-apple-darwin, macos-latest)

you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`

warning: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let` --> apps/desktop/src-tauri/src/recording.rs:461:9 | 461 | / match apply_camera_input(app.clone(), app.state(), Some(camera)).await { 462 | | Err(err) => warn!(%err, "Failed to restore camera input"), 463 | | Ok(_) => {} 464 | | } | |_________^ help: try: `if let Err(err) = apply_camera_input(app.clone(), app.state(), Some(camera)).await { warn!(%err, "Failed to restore camera input") }` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.91.0/index.html#single_match
}
}

Expand Down Expand Up @@ -613,17 +692,7 @@
state.camera_in_use = camera_feed.is_some();

#[cfg(target_os = "macos")]
let mut shareable_content =
acquire_shareable_content_for_target(&inputs.capture_target).await?;

let common = InProgressRecordingCommon {
target_name,
inputs: inputs.clone(),
recording_dir: recording_dir.clone(),
};

#[cfg(target_os = "macos")]
let excluded_windows = {
let mut excluded_windows = {
let window_exclusions = general_settings
.as_ref()
.map_or_else(general_settings::default_excluded_windows, |settings| {
Expand All @@ -633,6 +702,24 @@
crate::window_exclusion::resolve_window_ids(&window_exclusions)
};

#[cfg(target_os = "macos")]
let mut shareable_content =
acquire_shareable_content_for_target(&inputs.capture_target, &excluded_windows)
.await?;

#[cfg(target_os = "macos")]
prune_excluded_windows_without_shareable_content(
&mut excluded_windows,
shareable_content.retained(),
)
.await;

let common = InProgressRecordingCommon {
target_name,
inputs: inputs.clone(),
recording_dir: recording_dir.clone(),
};

let mut mic_restart_attempts = 0;

let done_fut = loop {
Expand Down Expand Up @@ -751,8 +838,16 @@
}
#[cfg(target_os = "macos")]
Err(err) if is_shareable_content_error(&err) => {
shareable_content =
acquire_shareable_content_for_target(&inputs.capture_target).await?;
shareable_content = acquire_shareable_content_for_target(
&inputs.capture_target,
&excluded_windows,
)
.await?;
prune_excluded_windows_without_shareable_content(
&mut excluded_windows,
shareable_content.retained(),
)
.await;
continue;
}
Err(err) if mic_restart_attempts == 0 && mic_actor_not_running(&err) => {
Expand Down
57 changes: 53 additions & 4 deletions apps/desktop/src/routes/editor/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { createContextProvider } from "@solid-primitives/context";
import { trackStore } from "@solid-primitives/deep";
import { createEventListener } from "@solid-primitives/event-listener";
import { createUndoHistory } from "@solid-primitives/history";
import { debounce } from "@solid-primitives/scheduled";
import { createQuery, skipToken } from "@tanstack/solid-query";
import {
type Accessor,
Expand All @@ -15,6 +14,7 @@ import {
createResource,
createSignal,
on,
onCleanup,
} from "solid-js";
import { createStore, produce, reconcile, unwrap } from "solid-js/store";

Expand Down Expand Up @@ -51,6 +51,7 @@ export const OUTPUT_SIZE = {
};

export const MAX_ZOOM_IN = 3;
const PROJECT_SAVE_DEBOUNCE_MS = 250;

export type RenderState =
| { type: "starting" }
Expand Down Expand Up @@ -295,14 +296,62 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider(
},
};

let projectSaveTimeout: number | undefined;
let saveInFlight = false;
let shouldResave = false;
let hasPendingProjectSave = false;

const flushProjectConfig = async () => {
if (!hasPendingProjectSave && !saveInFlight) return;
if (saveInFlight) {
if (hasPendingProjectSave) {
shouldResave = true;
}
return;
}
saveInFlight = true;
shouldResave = false;
hasPendingProjectSave = false;
try {
await commands.setProjectConfig(serializeProjectConfiguration(project));
} catch (error) {
console.error("Failed to persist project config", error);
} finally {
saveInFlight = false;
if (shouldResave) {
shouldResave = false;
void flushProjectConfig();
}
}
};

const scheduleProjectConfigSave = () => {
hasPendingProjectSave = true;
if (projectSaveTimeout) {
clearTimeout(projectSaveTimeout);
}
projectSaveTimeout = window.setTimeout(() => {
projectSaveTimeout = undefined;
void flushProjectConfig();
}, PROJECT_SAVE_DEBOUNCE_MS);
};

onCleanup(() => {
if (projectSaveTimeout) {
clearTimeout(projectSaveTimeout);
projectSaveTimeout = undefined;
}
void flushProjectConfig();
});

createEffect(
on(
() => {
trackStore(project);
},
debounce(() => {
commands.setProjectConfig(serializeProjectConfiguration(project));
}),
() => {
scheduleProjectConfigSave();
},
{ defer: true },
),
);
Expand Down
Loading