Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
9 changes: 3 additions & 6 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager, Url};
use tracing::trace;

use crate::{
App, ArcLock, apply_camera_input, apply_mic_input, recording::StartRecordingInputs,
windows::ShowCapWindow,
};
use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -119,8 +116,8 @@ impl DeepLinkAction {
} => {
let state = app.state::<ArcLock<App>>();

apply_camera_input(app.clone(), state.clone(), camera).await?;
apply_mic_input(state.clone(), mic_label).await?;
crate::set_camera_input(app.clone(), state.clone(), camera).await?;
crate::set_mic_input(state.clone(), mic_label).await?;

let capture_target: ScreenCaptureTarget = match capture_mode {
CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays()
Expand Down
100 changes: 68 additions & 32 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,13 +331,6 @@ impl App {
#[specta::specta]
#[instrument(skip(state))]
async fn set_mic_input(state: MutableState<'_, App>, label: Option<String>) -> Result<(), String> {
apply_mic_input(state, label).await
}

pub(crate) async fn apply_mic_input(
state: MutableState<'_, App>,
label: Option<String>,
) -> Result<(), String> {
let (mic_feed, studio_handle, current_label) = {
let app = state.read().await;
let handle = match app.current_recording() {
Expand Down Expand Up @@ -421,14 +414,6 @@ async fn set_camera_input(
app_handle: AppHandle,
state: MutableState<'_, App>,
id: Option<DeviceOrModelID>,
) -> Result<(), String> {
apply_camera_input(app_handle, state, id).await
}

pub(crate) async fn apply_camera_input(
app_handle: AppHandle,
state: MutableState<'_, App>,
id: Option<DeviceOrModelID>,
) -> Result<(), String> {
let app = state.read().await;
let camera_feed = app.camera_feed.clone();
Expand Down Expand Up @@ -459,12 +444,39 @@ pub(crate) async fn apply_camera_input(
.map_err(|e| e.to_string())?;
}
Some(id) => {
camera_feed
.ask(feeds::camera::SetInput { id: id.clone() })
.await
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
let mut attempts = 0;
loop {
attempts += 1;

// We first ask the actor to set the input
// This returns a future that resolves when the camera is actually ready
let request = camera_feed
.ask(feeds::camera::SetInput { id: id.clone() })
.await
.map_err(|e| e.to_string());

let result = match request {
Ok(future) => future.await.map_err(|e| e.to_string()),
Err(e) => Err(e),
};

match result {
Ok(_) => break,
Err(e) => {
if attempts >= 3 {
return Err(format!(
"Failed to initialize camera after {} attempts: {}",
attempts, e
));
}
warn!(
"Failed to set camera input (attempt {}): {}. Retrying...",
attempts, e
);
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}

ShowCapWindow::Camera
.show(&app_handle)
Expand Down Expand Up @@ -741,11 +753,19 @@ enum CurrentRecordingTarget {
},
}

#[derive(Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub enum RecordingStatus {
Pending,
Recording,
}

#[derive(Serialize, Type)]
#[serde(rename_all = "camelCase")]
struct CurrentRecording {
target: CurrentRecordingTarget,
mode: RecordingMode,
status: RecordingStatus,
}

#[tauri::command]
Expand All @@ -756,10 +776,14 @@ async fn get_current_recording(
) -> Result<JsonValue<Option<CurrentRecording>>, ()> {
let state = state.read().await;

let (mode, capture_target) = match &state.recording_state {
let (mode, capture_target, status) = match &state.recording_state {
RecordingState::None => return Ok(JsonValue::new(&None)),
RecordingState::Pending { mode, target } => (*mode, target),
RecordingState::Active(inner) => (inner.mode(), inner.capture_target()),
RecordingState::Pending { mode, target } => (*mode, target, RecordingStatus::Pending),
RecordingState::Active(inner) => (
inner.mode(),
inner.capture_target(),
RecordingStatus::Recording,
),
};

let target = match capture_target {
Expand All @@ -777,7 +801,11 @@ async fn get_current_recording(
},
};

Ok(JsonValue::new(&Some(CurrentRecording { target, mode })))
Ok(JsonValue::new(&Some(CurrentRecording {
target,
mode,
status,
})))
}

#[derive(Serialize, Type, tauri_specta::Event, Clone)]
Expand Down Expand Up @@ -2558,8 +2586,8 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
.flatten()
.unwrap_or_default();

let _ = apply_mic_input(app.state(), settings.mic_name).await;
let _ = apply_camera_input(app.clone(), app.state(), settings.camera_id).await;
let _ = set_mic_input(app.state(), settings.mic_name).await;
let _ = set_camera_input(app.clone(), app.state(), settings.camera_id).await;

let _ = start_recording(app.clone(), app.state(), {
recording::StartRecordingInputs {
Expand Down Expand Up @@ -2635,6 +2663,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
.camera_feed
.ask(feeds::camera::RemoveInput)
.await;

app_state.selected_mic_label = None;
app_state.selected_camera_id = None;
app_state.camera_in_use = false;
Expand Down Expand Up @@ -2695,11 +2724,18 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
CapWindowId::Camera => {
let app = app.clone();
tokio::spawn(async move {
app.state::<ArcLock<App>>()
.write()
.await
.camera_preview
.on_window_close();
let state = app.state::<ArcLock<App>>();
let mut app_state = state.write().await;

app_state.camera_preview.on_window_close();

if !app_state.is_recording_active_or_pending() {
let _ = app_state
.camera_feed
.ask(feeds::camera::RemoveInput)
.await;
app_state.camera_in_use = false;
}
});
}
_ => {}
Expand Down
109 changes: 49 additions & 60 deletions apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ use crate::{
App, CurrentRecordingChanged, MutableState, NewStudioRecordingAdded, RecordingState,
RecordingStopped, VideoUploadInfo,
api::PresignedS3PutRequestMethod,
apply_camera_input, apply_mic_input,
audio::AppSounds,
auth::AuthStore,
create_screenshot,
Expand All @@ -56,7 +55,6 @@ use crate::{
},
open_external_link,
presets::PresetsStore,
recording_settings::RecordingSettingsStore,
thumbnails::*,
upload::{
InstantMultipartUpload, build_video_meta, compress_image, create_or_get_video, upload_video,
Expand Down Expand Up @@ -351,41 +349,6 @@ pub enum RecordingAction {
UpgradeRequired,
}

async fn restore_inputs_from_store_if_missing(app: &AppHandle, state: &MutableState<'_, App>) {
let guard = state.read().await;
let recording_active = !matches!(guard.recording_state, RecordingState::None);
let needs_mic = guard.selected_mic_label.is_none();
let needs_camera = guard.selected_camera_id.is_none();
drop(guard);

if recording_active || (!needs_mic && !needs_camera) {
return;
}

let settings = match RecordingSettingsStore::get(app) {
Ok(Some(settings)) => settings,
Ok(None) => return,
Err(err) => {
warn!(%err, "Failed to load recording settings while restoring inputs");
return;
}
};

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(_) => {}
}
}

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(_) => {}
}
}
}

#[tauri::command]
#[specta::specta]
#[tracing::instrument(name = "recording", skip_all)]
Expand All @@ -394,28 +357,10 @@ pub async fn start_recording(
state_mtx: MutableState<'_, App>,
inputs: StartRecordingInputs,
) -> Result<RecordingAction, String> {
restore_inputs_from_store_if_missing(&app, &state_mtx).await;

if !matches!(state_mtx.read().await.recording_state, RecordingState::None) {
return Err("Recording already in progress".to_string());
}

let has_camera_selected = {
let guard = state_mtx.read().await;
guard.selected_camera_id.is_some()
};
let camera_window_open = CapWindowId::Camera.get(&app).is_some();
let should_open_camera_preview =
matches!(inputs.mode, RecordingMode::Instant) && has_camera_selected && !camera_window_open;

if should_open_camera_preview {
ShowCapWindow::Camera
.show(&app)
.await
.map_err(|err| error!("Failed to show camera preview window: {err}"))
.ok();
}

let id = uuid::Uuid::new_v4().to_string();
let general_settings = GeneralSettingsStore::get(&app).ok().flatten();
let general_settings = general_settings.as_ref();
Expand Down Expand Up @@ -600,16 +545,57 @@ pub async fn start_recording(
let inputs = inputs.clone();
async move {
fail!("recording::spawn_actor");
let mut state = state_mtx.write().await;

use kameo::error::SendError;

let camera_feed = match state.camera_feed.ask(camera::Lock).await {
Ok(lock) => Some(Arc::new(lock)),
Err(SendError::HandlerError(camera::LockFeedError::NoInput)) => None,
// Initialize camera if selected but not active
let (camera_feed_actor, selected_camera_id) = {
let state = state_mtx.read().await;
(state.camera_feed.clone(), state.selected_camera_id.clone())
};

let camera_lock_result = camera_feed_actor.ask(camera::Lock).await;

let camera_feed_lock = match camera_lock_result {
Ok(lock) => Some(lock),
Err(SendError::HandlerError(camera::LockFeedError::NoInput)) => {
if let Some(id) = selected_camera_id {
info!(
"Camera selected but not initialized, initializing: {:?}",
id
);
match camera_feed_actor
.ask(camera::SetInput { id: id.clone() })
.await
{
Ok(fut) => match fut.await {
Ok(_) => match camera_feed_actor.ask(camera::Lock).await {
Ok(lock) => Some(lock),
Err(e) => {
warn!("Failed to lock camera after initialization: {}", e);
None
}
},
Err(e) => {
warn!("Failed to initialize camera: {}", e);
None
}
},
Err(e) => {
warn!("Failed to ask SetInput: {}", e);
None
}
}
} else {
None
}
}
Err(e) => return Err(anyhow!(e.to_string())),
};

let mut state = state_mtx.write().await;

let camera_feed = camera_feed_lock.map(Arc::new);

state.camera_in_use = camera_feed.is_some();

#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -1087,6 +1073,9 @@ async fn handle_recording_end(
}
let _ = app.mic_feed.ask(microphone::RemoveInput).await;
let _ = app.camera_feed.ask(camera::RemoveInput).await;
app.selected_mic_label = None;
app.selected_camera_id = None;
app.camera_in_use = false;
if let Some(win) = CapWindowId::Camera.get(&handle) {
win.close().ok();
}
Expand Down
Loading
Loading