diff --git a/Cargo.lock b/Cargo.lock index ea66481a..b50c761c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -468,6 +468,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", + "axum-macros", "base64 0.22.1", "bytes", "futures-util", @@ -518,6 +519,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -713,6 +725,19 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "build-time" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1219c19fc29b7bfd74b7968b420aff5bc951cf517800176e795d6b2300dd382" +dependencies = [ + "chrono", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "built" version = "0.7.4" @@ -824,15 +849,16 @@ name = "cap-export" version = "0.1.0" dependencies = [ "cap-editor", - "cap-ffmpeg-cli", "cap-flags", "cap-media", "cap-project", "cap-rendering", "cap-utils", + "ffmpeg-next", "futures", "image 0.25.5", "mp4", + "tauri", "tempfile", "thiserror 1.0.63", "tokio", @@ -877,6 +903,7 @@ dependencies = [ "core-graphics 0.24.0", "cpal 0.15.3 (git+https://github.com/RustAudio/cpal?rev=f43d36e55494993bbbde3299af0c53e5cdf4d4cf)", "ffmpeg-next", + "ffmpeg-sys-next", "flume 0.11.0", "futures", "indexmap 2.5.0", @@ -907,6 +934,8 @@ dependencies = [ "serde", "serde_json", "specta", + "tauri", + "tauri-plugin-store", ] [[package]] @@ -938,15 +967,18 @@ version = "0.1.0" dependencies = [ "anyhow", "bezier_easing", + "build-time", "bytemuck", "cap-flags", "cap-project", + "cap-recording", "ffmpeg-hw-device", "ffmpeg-next", "ffmpeg-sys-next", "futures", "futures-intrusive", "image 0.25.5", + "log", "nix 0.29.0", "pretty_assertions", "serde", diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index abcfb7b3..4fad366a 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -1,6 +1,6 @@ use crate::{ - get_video_metadata, upsert_editor_instance, windows::ShowCapWindow, RenderProgress, - VideoRecordingMetadata, VideoType, + general_settings::GeneralSettingsStore, get_video_metadata, upsert_editor_instance, + windows::ShowCapWindow, RenderProgress, VideoRecordingMetadata, VideoType, }; use cap_project::ProjectConfiguration; use std::path::PathBuf; @@ -44,10 +44,8 @@ pub async fn export_video( .unwrap_or(screen_metadata.duration), ); - // Calculate total frames with ceiling to ensure we don't exceed 100% - let total_frames = ((duration * 30.0).ceil() as u32).max(1); - let editor_instance = upsert_editor_instance(&app, video_id.clone()).await; + let total_frames = editor_instance.get_total_frames(); let output_path = editor_instance.meta().output_path(); @@ -72,6 +70,7 @@ pub async fn export_video( } let exporter = cap_export::Exporter::new( + &app, modified_project, output_path.clone(), move |frame_index| { @@ -91,11 +90,7 @@ pub async fn export_video( e.to_string() })?; - let result = if use_custom_muxer { - exporter.export_with_custom_muxer().await - } else { - exporter.export_with_ffmpeg_cli().await - }; + let result = exporter.export_with_custom_muxer().await; match result { Ok(_) => { diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index 88c11db1..4f7d91db 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -1,3 +1,4 @@ +use cap_project::Resolution; use serde::{Deserialize, Serialize}; use serde_json::json; use specta::Type; @@ -25,6 +26,8 @@ pub struct GeneralSettingsStore { pub has_completed_startup: bool, #[serde(default)] pub theme: AppTheme, + #[serde(default)] + pub recording_config: Option, } #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize, Type)] @@ -36,6 +39,25 @@ pub enum AppTheme { Dark, } +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct RecordingConfig { + pub fps: u32, + pub resolution: Resolution, +} + +impl Default for RecordingConfig { + fn default() -> Self { + Self { + fps: 30, + resolution: Resolution { + width: 1920, + height: 1080, + }, + } + } +} + fn true_b() -> bool { true } @@ -78,3 +100,20 @@ pub fn init(app: &AppHandle) { app.manage(GeneralSettingsState::new(store)); println!("GeneralSettingsState managed"); } + +#[tauri::command] +#[specta::specta] +pub async fn get_recording_config(app: AppHandle) -> Result { + let settings = GeneralSettingsStore::get(&app)?; + Ok(settings + .and_then(|s| s.recording_config) + .unwrap_or_default()) +} + +#[tauri::command] +#[specta::specta] +pub async fn set_recording_config(app: AppHandle, config: RecordingConfig) -> Result<(), String> { + GeneralSettingsStore::update(&app, |settings| { + settings.recording_config = Some(config); + }) +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 7dd8a984..a67d28aa 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -25,12 +25,12 @@ use cap_media::feeds::{AudioInputFeed, AudioInputSamplesSender}; use cap_media::frame_ws::WSFrame; use cap_media::sources::CaptureScreen; use cap_media::{feeds::CameraFeed, sources::ScreenCaptureTarget}; -use cap_project::{Content, ProjectConfiguration, RecordingMeta, SharingMeta}; +use cap_project::{Content, ProjectConfiguration, RecordingMeta, Resolution, SharingMeta}; use cap_recording::RecordingOptions; use cap_rendering::ProjectRecordings; use clipboard_rs::common::RustImage; use clipboard_rs::{Clipboard, ClipboardContext}; -use general_settings::GeneralSettingsStore; +use general_settings::{GeneralSettingsStore, RecordingConfig}; use mp4::Mp4Reader; // use display::{list_capture_windows, Bounds, CaptureTarget, FPS}; use notifications::NotificationType; @@ -91,14 +91,20 @@ pub enum VideoType { Camera, } -#[derive(Serialize, Deserialize, specta::Type)] -enum UploadResult { +#[derive(Serialize, Deserialize, specta::Type, Debug)] +pub enum UploadResult { Success(String), NotAuthenticated, PlanCheckFailed, UpgradeRequired, } +#[derive(Serialize, Deserialize, specta::Type, Debug)] +pub struct VideoRecordingMetadata { + pub duration: f64, + pub size: f64, +} + #[derive(Clone, Serialize, Deserialize, specta::Type)] pub struct PreCreatedVideo { id: String, @@ -377,9 +383,20 @@ type MutableState<'a, T> = State<'a, Arc>>; #[tauri::command] #[specta::specta] -async fn get_recording_options(state: MutableState<'_, App>) -> Result { +async fn get_recording_options( + app: AppHandle, + state: MutableState<'_, App>, +) -> Result { let mut state = state.write().await; + // Load settings from disk if they exist + if let Ok(Some(settings)) = GeneralSettingsStore::get(&app) { + if let Some(config) = settings.recording_config { + state.start_recording_options.fps = config.fps; + state.start_recording_options.output_resolution = Some(config.resolution); + } + } + // If there's a saved audio input but no feed, initialize it if let Some(audio_input_name) = state.start_recording_options.audio_input_name() { if state.audio_input_feed.is_none() { @@ -401,15 +418,28 @@ async fn get_recording_options(state: MutableState<'_, App>) -> Result, options: RecordingOptions, ) -> Result<(), String> { + // Update in-memory state state .write() .await - .set_start_recording_options(options) + .set_start_recording_options(options.clone()) .await?; + // Update persistent settings + GeneralSettingsStore::update(&app, |settings| { + settings.recording_config = Some(RecordingConfig { + fps: options.fps, + resolution: options.output_resolution.unwrap_or_else(|| Resolution { + width: 1920, + height: 1080, + }), + }); + })?; + Ok(()) } @@ -910,6 +940,7 @@ async fn copy_video_to_clipboard( Ok(()) } + #[derive(Serialize, Deserialize, specta::Type)] pub struct VideoRecordingMetadata { duration: f64, @@ -1865,6 +1896,7 @@ pub async fn run() { global_message_dialog, show_window, write_clipboard_string, + get_editor_total_frames, ]) .events(tauri_specta::collect_events![ RecordingOptionsChanged, @@ -1998,11 +2030,13 @@ pub async fn run() { audio_input_feed: None, start_recording_options: RecordingOptions { capture_target: ScreenCaptureTarget::Screen(CaptureScreen { - id: 1, - name: "Default".to_string(), + id: 0, + name: String::new(), }), camera_label: None, audio_input_name: None, + fps: 30, + output_resolution: None, }, current_recording: None, pre_created_video: None, @@ -2362,3 +2396,15 @@ trait EventExt: tauri_specta::Event { } impl EventExt for T {} + +#[tauri::command(async)] +#[specta::specta] +async fn get_editor_total_frames(app: AppHandle, video_id: String) -> Result { + let editor_instances = app.state::(); + let instances = editor_instances.lock().await; + + let instance = instances + .get(&video_id) + .ok_or_else(|| "Editor instance not found".to_string())?; + Ok(instance.get_total_frames()) +} diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index f0d957b0..838a7dbe 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -48,6 +48,14 @@ pub fn list_cameras() -> Vec { pub async fn start_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; + // Get the recording config + let config = GeneralSettingsStore::get(&app)? + .and_then(|s| s.recording_config) + .unwrap_or_default(); + + // Update the recording options with the configured FPS + state.start_recording_options.fps = config.fps; + let id = uuid::Uuid::new_v4().to_string(); let recording_dir = app @@ -152,7 +160,6 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res let now = Instant::now(); let completed_recording = current_recording.stop().await.map_err(|e| e.to_string())?; - println!("stopped recording in {:?}", now.elapsed()); if let Some(window) = CapWindowId::InProgressRecording.get(&app) { window.hide().unwrap(); @@ -175,14 +182,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res }; let display_screenshot = screenshots_dir.join("display.jpg"); - let now = Instant::now(); create_screenshot(display_output_path, display_screenshot.clone(), None).await?; - println!("created screenshot in {:?}", now.elapsed()); - - // let thumbnail = screenshots_dir.join("thumbnail.png"); - // let now = Instant::now(); - // create_thumbnail(display_screenshot, thumbnail, (100, 100)).await?; - // println!("created thumbnail in {:?}", now.elapsed()); let recording_dir = completed_recording.recording_dir.clone(); diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx index 668d39fe..f751ad4e 100644 --- a/apps/desktop/src/routes/(window-chrome)/(main).tsx +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -7,6 +7,8 @@ import { useQueryClient, } from "@tanstack/solid-query"; import { getVersion } from "@tauri-apps/api/app"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { LogicalSize } from "@tauri-apps/api/window"; import { cx } from "cva"; import { JSX, @@ -17,6 +19,7 @@ import { createResource, createSignal, onMount, + onCleanup, } from "solid-js"; import { createStore, produce } from "solid-js/store"; import { fetch } from "@tauri-apps/plugin-http"; @@ -51,11 +54,6 @@ const getAuth = cache(async () => { const value = await authStore.get(); const local = import.meta.env.VITE_LOCAL_MODE === "true"; - if (!value) { - if (local) return; - return redirect("/signin"); - } - const res = await apiClient.desktop.getUserPlan({ headers: await protectedHeaders(), }); @@ -102,6 +100,33 @@ export default function () { } } + // Enforce window size with multiple safeguards + const currentWindow = await getCurrentWindow(); + const MAIN_WINDOW_SIZE = { width: 300, height: 360 }; + + // Set initial size + currentWindow.setSize( + new LogicalSize(MAIN_WINDOW_SIZE.width, MAIN_WINDOW_SIZE.height) + ); + + // Check size when app regains focus + const unlistenFocus = await currentWindow.onFocusChanged( + ({ payload: focused }) => { + if (focused) { + currentWindow.setSize( + new LogicalSize(MAIN_WINDOW_SIZE.width, MAIN_WINDOW_SIZE.height) + ); + } + } + ); + + // Listen for resize events + const unlistenResize = await currentWindow.onResized(() => { + currentWindow.setSize( + new LogicalSize(MAIN_WINDOW_SIZE.width, MAIN_WINDOW_SIZE.height) + ); + }); + setTitlebar("hideMaximize", true); setTitlebar( "items", @@ -121,6 +146,11 @@ export default function () { ); + + onCleanup(() => { + unlistenFocus(); + unlistenResize(); + }); }); return ( @@ -147,26 +177,24 @@ export default function () {
- {window.FLAGS.customS3 && ( - - - - - - - Cap Apps - - - - - )} + + + + + + + Cap Apps + + + +