From ebe1383a8ef99237487869e05344367119931633 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 01:42:04 +0800 Subject: [PATCH 1/2] add tauri-specta --- apps/desktop/next.config.mjs | 4 +- apps/desktop/src-tauri/Cargo.lock | 107 +++++++++++++++++- apps/desktop/src-tauri/Cargo.toml | 2 + apps/desktop/src-tauri/src/main.rs | 44 +++++-- apps/desktop/src-tauri/src/media.rs | 61 +++++----- apps/desktop/src-tauri/src/recording.rs | 26 +++-- apps/desktop/src-tauri/src/utils.rs | 23 ++-- apps/desktop/src/app/page.tsx | 7 +- .../src/components/windows/Permissions.tsx | 9 +- .../src/components/windows/inner/Recorder.tsx | 15 +-- apps/desktop/src/utils/commands.ts | 57 ++++++++++ apps/desktop/src/utils/recording/utils.ts | 4 +- 12 files changed, 275 insertions(+), 84 deletions(-) create mode 100644 apps/desktop/src/utils/commands.ts diff --git a/apps/desktop/next.config.mjs b/apps/desktop/next.config.mjs index 89769149..ecd205e0 100644 --- a/apps/desktop/next.config.mjs +++ b/apps/desktop/next.config.mjs @@ -1,11 +1,11 @@ import { withSentryConfig } from "@sentry/nextjs"; /** @type {import('next').NextConfig} */ -import("dotenv").then(({ config }) => config({ path: "../../.env" })); - import fs from "fs"; import path from "path"; +fs.copyFileSync(path.resolve("../../.env"), path.resolve("./.env")); + const packageJson = JSON.parse( fs.readFileSync(path.resolve("./package.json"), "utf8") ); diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 99e9ffad..a785dfdd 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "ab_glyph" version = "0.2.23" @@ -421,7 +427,7 @@ dependencies = [ "bitflags 2.4.2", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "proc-macro2", @@ -630,12 +636,14 @@ dependencies = [ "sentry", "serde", "serde_json", + "specta", "tauri", "tauri-build", "tauri-plugin-context-menu", "tauri-plugin-deep-link", "tauri-plugin-oauth", "tauri-plugin-positioner", + "tauri-specta", "tokio", "tokio-util", "urlencoding", @@ -1219,6 +1227,15 @@ dependencies = [ "libloading 0.8.1", ] +[[package]] +name = "document-features" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +dependencies = [ + "litrs", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -2323,6 +2340,18 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "infer" version = "0.9.0" @@ -2380,6 +2409,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -2643,6 +2681,12 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.11" @@ -3342,6 +3386,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -4497,6 +4547,37 @@ dependencies = [ "system-deps 5.0.0", ] +[[package]] +name = "specta" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2240c3aa020aa61d2c569087d213baafbb212f4ceb9de9dd162376ea6aa0fe3" +dependencies = [ + "document-features", + "indoc 1.0.9", + "once_cell", + "paste", + "serde", + "serde_json", + "specta-macros", + "tauri", + "thiserror", +] + +[[package]] +name = "specta-macros" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4605306321c356e03873b8ee71d7592a5e7c508add325c3ed0677c16fdf1bcfb" +dependencies = [ + "Inflector", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", + "termcolor", +] + [[package]] name = "spin" version = "0.9.8" @@ -4964,6 +5045,21 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa034c38b7bdfeccc606eca0b030a1e67a20b78e7642edef09816b7e1ff9a9de" +dependencies = [ + "heck 0.4.1", + "indoc 2.0.5", + "serde", + "serde_json", + "specta", + "tauri", + "thiserror", +] + [[package]] name = "tauri-utils" version = "1.5.3" @@ -5038,6 +5134,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thin-slice" version = "0.1.1" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index cbb4f8af..e2d2f4ba 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -44,6 +44,8 @@ jpeg-encoder = "0.6.0" nix = "0.20.0" urlencoding = "2.1.2" bytes = "1.0" +tauri-specta = { version = "1", features = ["javascript", "typescript"] } +specta = "1" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 23df038f..e80fde88 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -8,7 +8,7 @@ use regex::Regex; use tokio::sync::Mutex; use std::sync::atomic::{AtomicBool}; use std::{vec}; -use tauri::{command, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTraySubmenu, Window}; +use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTraySubmenu, Window}; use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial}; use window_shadows::set_shadow; use tauri_plugin_positioner::{WindowExt, Position}; @@ -34,9 +34,9 @@ use ffmpeg_sidecar::{ use winit::monitor::{MonitorHandle, VideoMode}; -fn main() { +fn main() { let _ = fix_path_env::fix(); - + std::panic::set_hook(Box::new(|info| { eprintln!("Thread panicked: {:?}", info); })); @@ -71,7 +71,8 @@ fn main() { handle_ffmpeg_installation().expect("Failed to install FFmpeg"); - #[command] + #[tauri::command] + #[specta::specta] async fn start_server(window: Window) -> Result { start(move |url| { let _ = window.emit("redirect_uri", url); @@ -80,6 +81,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn open_screen_capture_preferences() { #[cfg(target_os = "macos")] std::process::Command::new("open") @@ -89,6 +91,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn open_mic_preferences() { #[cfg(target_os = "macos")] std::process::Command::new("open") @@ -98,6 +101,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn open_camera_preferences() { #[cfg(target_os = "macos")] std::process::Command::new("open") @@ -107,6 +111,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn reset_screen_permissions() { #[cfg(target_os = "macos")] std::process::Command::new("tccutil") @@ -118,6 +123,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn reset_microphone_permissions() { #[cfg(target_os = "macos")] std::process::Command::new("tccutil") @@ -129,6 +135,7 @@ fn main() { } #[tauri::command] + #[specta::specta] fn reset_camera_permissions() { #[cfg(target_os = "macos")] std::process::Command::new("tccutil") @@ -191,20 +198,35 @@ fn main() { let tray = SystemTray::new().with_menu(create_tray_menu(None)).with_menu_on_left_click(false).with_title("Cap"); + #[cfg(debug_assertions)] + tauri_specta::ts::export(specta::collect_types![ + start_dual_recording, + stop_all_recordings, + enumerate_audio_devices, + start_server, + open_screen_capture_preferences, + open_mic_preferences, + open_camera_preferences, + has_screen_capture_access, + reset_screen_permissions, + reset_microphone_permissions, + reset_camera_permissions, + ], "../src/utils/commands.ts").unwrap(); + tauri::Builder::default() .plugin(tauri_plugin_oauth::init()) .plugin(tauri_plugin_positioner::init()) .setup(move |app| { let handle = app.handle(); - if let Some(options_window) = app.get_window("main") { + if let Some(options_window) = app.get_window("main") { let _ = options_window.move_window(Position::Center); #[cfg(target_os = "macos")] apply_vibrancy(&options_window, NSVisualEffectMaterial::MediumLight, None, Some(16.0)).expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS"); #[cfg(target_os = "windows")] apply_blur(&options_window, Some((255, 255, 255, 255))).expect("Unsupported platform! 'apply_blur' is only supported on Windows"); - + set_shadow(&options_window, true).expect("Unsupported platform!"); } @@ -249,7 +271,7 @@ fn main() { } } }); - + let tray_handle = app.tray_handle(); app.listen_global("media-devices-set", move|event| { #[derive(serde::Deserialize)] @@ -265,7 +287,7 @@ fn main() { let id_prefix = if kind == DeviceKind::Video { "video" } else { - "audio" + "audio" }; let mut none_item = CustomMenuItem::new(format!("in_{}_none", id_prefix), "None"); if selected_device.is_none() { @@ -277,13 +299,13 @@ fn main() { .filter(|device| device.kind == kind) .fold(initial, |tray_items, device| { let mut menu_item = CustomMenuItem::new(format!("in_{}_{}", id_prefix, device.id), &device.label); - + if let Some(selected) = selected_device { if selected.label == device.label { menu_item = menu_item.selected(); } } - + tray_items.add_item(menu_item) }) } @@ -348,7 +370,7 @@ fn main() { let kind = if item_id.contains("video") { "videoinput" } else { "audioinput" }; app.emit_all("tray-set-device-id", SetDevicePayload { - device_type: kind.to_string(), + device_type: kind.to_string(), id: if device_id == "none" { None } else { Some(device_id) } }).expect("Failed to emit tray set media device event to windows"); } diff --git a/apps/desktop/src-tauri/src/media.rs b/apps/desktop/src-tauri/src/media.rs index 3b55744e..436eaa0c 100644 --- a/apps/desktop/src-tauri/src/media.rs +++ b/apps/desktop/src-tauri/src/media.rs @@ -65,20 +65,20 @@ impl MediaRecorder { self.options = Some(options.clone()); println!("Custom device: {:?}", custom_device); - + let host = cpal::default_host(); let devices = host.devices().expect("Failed to get devices"); let _display = Display::primary().expect("Failed to find primary display"); let w = max_screen_width; let h = max_screen_height; - + let adjusted_width = w & !2; let adjusted_height = h & !2; let capture_size = adjusted_width * adjusted_height * 4; let (audio_tx, audio_rx) = tokio::sync::mpsc::channel::>(2048); let (video_tx, video_rx) = tokio::sync::mpsc::channel::>(2048); let calculated_stride = (adjusted_width * 4) as usize; - + println!("Display width: {}", w); println!("Display height: {}", h); println!("Adjusted width: {}", adjusted_width); @@ -103,7 +103,7 @@ impl MediaRecorder { let video_channel_receiver = Arc::new(Mutex::new(self.video_channel_receiver.take())); let should_stop = Arc::clone(&self.should_stop); - + let mut input_devices = devices.filter_map(|device| { let supported_input_configs = device.supported_input_configs(); if supported_input_configs.is_ok() && supported_input_configs.unwrap().count() > 0 { @@ -144,23 +144,23 @@ impl MediaRecorder { println!("Sample rate: {}", sample_rate); println!("Channels: {}", channels); println!("Sample format: {}", sample_format); - + let ffmpeg_binary_path_str = ffmpeg_path_as_str().unwrap().to_owned(); println!("FFmpeg binary path: {}", ffmpeg_binary_path_str); - + let audio_file_path_owned = audio_file_path.to_owned(); let video_file_path_owned = video_file_path.to_owned(); let sample_rate_str = sample_rate.to_string(); let channels_str = channels.to_string(); - + let ffmpeg_audio_stdin = self.ffmpeg_audio_stdin.clone(); let ffmpeg_video_stdin = self.ffmpeg_video_stdin.clone(); let err_fn = move |err| { eprintln!("an error occurred on stream: {}", err); }; - + if custom_device != Some("None") { println!("Building input stream..."); @@ -171,17 +171,17 @@ impl MediaRecorder { let audio_start_time = Arc::clone(&audio_start_time); move |data: &[i8], _: &_| { let mut first_frame_time_guard = audio_start_time.try_lock(); - + let bytes = data.iter().map(|&sample| sample as u8).collect::>(); if let Some(sender) = &audio_channel_sender { if sender.try_send(bytes).is_err() { eprintln!("Channel send error. Dropping data."); } } - + if let Ok(ref mut start_time_option) = first_frame_time_guard { if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); + **start_time_option = Some(Instant::now()); println!("Audio start time captured"); } @@ -194,7 +194,7 @@ impl MediaRecorder { SampleFormat::I16 => device.build_input_stream( &config.into(), { - let audio_start_time = Arc::clone(&audio_start_time); + let audio_start_time = Arc::clone(&audio_start_time); move |data: &[i16], _: &_| { let mut first_frame_time_guard = audio_start_time.try_lock(); @@ -208,7 +208,7 @@ impl MediaRecorder { if let Ok(ref mut start_time_option) = first_frame_time_guard { if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); + **start_time_option = Some(Instant::now()); println!("Audio start time captured"); } @@ -235,7 +235,7 @@ impl MediaRecorder { if let Ok(ref mut start_time_option) = first_frame_time_guard { if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); + **start_time_option = Some(Instant::now()); println!("Audio start time captured"); } @@ -262,7 +262,7 @@ impl MediaRecorder { if let Ok(ref mut start_time_option) = first_frame_time_guard { if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); + **start_time_option = Some(Instant::now()); println!("Audio start time captured"); } @@ -274,16 +274,16 @@ impl MediaRecorder { ), _sample_format => Err(cpal::BuildStreamError::DeviceNotAvailable), }; - + let stream = stream_result.map_err(|_| "Failed to build input stream")?; self.stream = Some(stream); self.trigger_play()?; } - let video_start_time_clone = Arc::clone(&video_start_time); + let video_start_time_clone = Arc::clone(&video_start_time); let screenshot_file_path_owned = format!("{}/screen-capture.jpg", screenshot_file_path); let capture_frame_at = Duration::from_secs(3); - + std::thread::spawn(move || { println!("Starting video recording capture thread..."); @@ -301,7 +301,7 @@ impl MediaRecorder { let start_time = Instant::now(); let mut time_next = Instant::now() + spf; let mut screenshot_captured: bool = false; - + while !should_stop.load(Ordering::SeqCst) { let options_clone = options.clone(); let now = Instant::now(); @@ -375,7 +375,7 @@ impl MediaRecorder { if let Ok(ref mut start_time_option) = first_frame_time_guard { if start_time_option.is_none() { - **start_time_option = Some(Instant::now()); + **start_time_option = Some(Instant::now()); println!("Video start time captured"); } @@ -413,7 +413,7 @@ impl MediaRecorder { let audio_segment_list_filename = format!("{}/segment_list.txt", audio_file_path_owned); let video_output_chunk_pattern = format!("{}/video_recording_%03d.ts", video_file_path_owned); let video_segment_list_filename = format!("{}/segment_list.txt", video_file_path_owned); - + let mut audio_filters = Vec::new(); if channels > 2 { @@ -488,7 +488,7 @@ impl MediaRecorder { let (video_child, video_stdin) = self.start_video_ffmpeg_processes(&ffmpeg_binary_path_str, &ffmpeg_video_command).await.map_err(|e| e.to_string())?; println!("Video process started"); - + if let Some(ffmpeg_audio_stdin) = &self.ffmpeg_audio_stdin { let mut audio_stdin_lock = ffmpeg_audio_stdin.lock().await; *audio_stdin_lock = audio_stdin; @@ -530,7 +530,7 @@ impl MediaRecorder { } } }); - + if custom_device != Some("None") { self.ffmpeg_audio_process = audio_child; } @@ -540,9 +540,9 @@ impl MediaRecorder { self.video_file_path = Some(video_file_path_owned); self.ffmpeg_video_process = Some(video_child); self.device_name = Some(device.name().expect("Failed to get device name")); - + println!("End of the start_audio_recording function"); - + Ok(()) } @@ -671,6 +671,7 @@ impl MediaRecorder { } #[tauri::command] +#[specta::specta] pub fn enumerate_audio_devices() -> Vec { let host = cpal::default_host(); let default_device = host.default_input_device().expect("No default input device available"); @@ -697,8 +698,8 @@ pub fn enumerate_audio_devices() -> Vec { use tokio::io::{BufReader, AsyncBufReadExt}; async fn start_recording_process( - ffmpeg_binary_path_str: &str, - args: &[String], + ffmpeg_binary_path_str: &str, + args: &[String], ) -> Result { let mut process = Command::new(ffmpeg_binary_path_str) .args(args) @@ -725,7 +726,7 @@ async fn wait_for_start_times( loop { let audio_start_locked = audio_start_time.lock().await; let video_start_locked = video_start_time.lock().await; - + if audio_start_locked.is_some() && video_start_locked.is_some() { let audio_start = *audio_start_locked.as_ref().unwrap(); let video_start = *video_start_locked.as_ref().unwrap(); @@ -755,7 +756,7 @@ async fn adjust_ffmpeg_commands_based_on_start_times( println!("Video start: {:?}", video_start); // Convert the duration difference to a float representing seconds - let offset_seconds = duration_difference.as_secs() as f64 + let offset_seconds = duration_difference.as_secs() as f64 + duration_difference.subsec_nanos() as f64 * 1e-9; // Depending on which started first, adjust the relevant FFmpeg command @@ -773,4 +774,4 @@ async fn adjust_ffmpeg_commands_based_on_start_times( println!("Applying -itsoffset {:.3} to audio", offset_seconds); } -} \ No newline at end of file +} diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 73b26010..d7bc2492 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -30,7 +30,7 @@ unsafe impl Sync for RecordingState {} unsafe impl Send for MediaRecorder {} unsafe impl Sync for MediaRecorder {} -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, specta::Type)] pub struct RecordingOptions { pub user_id: String, pub video_id: String, @@ -42,20 +42,21 @@ pub struct RecordingOptions { } #[tauri::command] +#[specta::specta] pub async fn start_dual_recording( state: State<'_, Arc>>, options: RecordingOptions, ) -> Result<(), String> { println!("Starting screen recording..."); let mut state_guard = state.lock().await; - + let shutdown_flag = Arc::new(AtomicBool::new(false)); let data_dir = state_guard.data_dir.as_ref() .ok_or("Data directory is not set in the recording state".to_string())?.clone(); println!("data_dir: {:?}", data_dir); - + let audio_chunks_dir = data_dir.join("chunks/audio"); let video_chunks_dir = data_dir.join("chunks/video"); let screenshot_dir = data_dir.join("screenshots"); @@ -63,13 +64,13 @@ pub async fn start_dual_recording( clean_and_create_dir(&audio_chunks_dir)?; clean_and_create_dir(&video_chunks_dir)?; clean_and_create_dir(&screenshot_dir)?; - + let audio_name = if options.audio_name.is_empty() { None } else { Some(options.audio_name.clone()) }; - + let media_recording_preparation = prepare_media_recording(&options, &audio_chunks_dir, &video_chunks_dir, &screenshot_dir, audio_name, state_guard.max_screen_width, state_guard.max_screen_height); let media_recording_result = media_recording_preparation.await.map_err(|e| e.to_string())?; @@ -108,11 +109,12 @@ pub async fn start_dual_recording( } #[tauri::command] +#[specta::specta] pub async fn stop_all_recordings(state: State<'_, Arc>>) -> Result<(), String> { let mut guard = state.lock().await; - + println!("Stopping media recording..."); - + guard.shutdown_flag.store(true, Ordering::SeqCst); if let Some(mut media_process) = guard.media_process.take() { @@ -126,13 +128,13 @@ pub async fn stop_all_recordings(state: State<'_, Arc>>) - }; if !is_local_mode { - while !guard.video_uploading_finished.load(Ordering::SeqCst) + while !guard.video_uploading_finished.load(Ordering::SeqCst) || !guard.audio_uploading_finished.load(Ordering::SeqCst) { println!("Waiting for uploads to finish..."); tokio::time::sleep(Duration::from_millis(50)).await; } } - + println!("All recordings and uploads stopped."); Ok(()) @@ -153,7 +155,7 @@ fn clean_and_create_dir(dir: &Path) -> Result<(), String> { File::create(&segment_list_path).map_err(|e| e.to_string())?; Ok(()) }, - Err(e) => Err(e.to_string()), + Err(e) => Err(e.to_string()), } } else { Ok(()) @@ -203,7 +205,7 @@ async fn start_upload_loop( if !upload_tasks.is_empty() { let _ = join_all(upload_tasks).await; } - + tokio::time::sleep(Duration::from_millis(50)).await; } uploading_finished.store(true, Ordering::SeqCst); @@ -240,4 +242,4 @@ async fn prepare_media_recording( let screenshot_dir_path = screenshot_dir.to_str().unwrap(); media_recorder.start_media_recording(options.clone(), audio_file_path, screenshot_dir_path, video_file_path, audio_name.as_ref().map(String::as_str), max_screen_width, max_screen_height).await?; Ok(media_recorder) -} \ No newline at end of file +} diff --git a/apps/desktop/src-tauri/src/utils.rs b/apps/desktop/src-tauri/src/utils.rs index 135b276d..34b4213f 100644 --- a/apps/desktop/src-tauri/src/utils.rs +++ b/apps/desktop/src-tauri/src/utils.rs @@ -1,15 +1,14 @@ -use std::process::{Command}; -use ffmpeg_sidecar::{ - paths::sidecar_dir, -}; use capture::{Capturer, Display}; -use std::time::{Duration, Instant}; +use ffmpeg_sidecar::paths::sidecar_dir; +use std::io::ErrorKind::WouldBlock; use std::panic; use std::path::Path; +use std::process::Command; use std::thread; -use std::io::ErrorKind::WouldBlock; +use std::time::{Duration, Instant}; #[tauri::command] +#[specta::specta] pub fn has_screen_capture_access() -> bool { let display = match Display::primary() { Ok(display) => display, @@ -29,10 +28,10 @@ pub fn has_screen_capture_access() -> bool { Ok(capturer) => { println!("Capturer created"); capturer - }, + } Err(e) => { println!("Capturer not created: {}", e); - return false; + return false; } }; @@ -50,7 +49,7 @@ pub fn has_screen_capture_access() -> bool { Ok(_frame) => { println!("Frame captured"); return true; - }, + } Err(error) => { if error.kind() == WouldBlock { thread::sleep(one_frame); @@ -93,9 +92,9 @@ pub fn ffmpeg_path_as_str() -> Result { } else { "ffmpeg" }; - + let path = sidecar_dir().map_err(|e| e.to_string())?.join(binary_name); - + if Path::new(&path).exists() { path.to_str() .map(|s| s.to_owned()) @@ -115,4 +114,4 @@ pub fn create_named_pipe(path: &str) -> Result<(), nix::Error> { pub fn remove_named_pipe(path: &str) -> Result<(), std::io::Error> { std::fs::remove_file(path)?; Ok(()) -} \ No newline at end of file +} diff --git a/apps/desktop/src/app/page.tsx b/apps/desktop/src/app/page.tsx index 80a89bf5..d8ac87c0 100644 --- a/apps/desktop/src/app/page.tsx +++ b/apps/desktop/src/app/page.tsx @@ -12,6 +12,7 @@ import { getVersion } from "@tauri-apps/api/app"; import { invoke } from "@tauri-apps/api/tauri"; import toast from "react-hot-toast"; import { authFetch } from "@/utils/auth/helpers"; +import * as commands from "@/utils/commands"; export const dynamic = "force-static"; @@ -32,9 +33,9 @@ export default function CameraPage() { localStorage.setItem("cap_test_build_version", appVersion); if (localStorage.getItem("permissions")) { - await invoke("reset_screen_permissions"); - await invoke("reset_camera_permissions"); - await invoke("reset_microphone_permissions"); + await commands.resetScreenPermissions(); + await commands.resetCameraPermissions(); + await commands.resetMicrophonePermissions(); console.log("Permissions reset"); const permissions = JSON.parse( localStorage.getItem("permissions") || "{}" diff --git a/apps/desktop/src/components/windows/Permissions.tsx b/apps/desktop/src/components/windows/Permissions.tsx index 28902e94..c443d9ab 100644 --- a/apps/desktop/src/components/windows/Permissions.tsx +++ b/apps/desktop/src/components/windows/Permissions.tsx @@ -3,6 +3,7 @@ import { Button, LogoBadge } from "@cap/ui"; import { useEffect, useState } from "react"; import { savePermissions, getPermissions } from "@/utils/helpers"; +import * as commands from "@/utils/commands"; import { invoke } from "@tauri-apps/api/tauri"; export const Permissions = () => { @@ -19,7 +20,7 @@ export const Permissions = () => { }); const checkScreenCapture = async () => { - const hasAccess = await invoke("has_screen_capture_access"); + const hasAccess = await commands.hasScreenCaptureAccess(); if (hasAccess) { await savePermissions("screen", true); setPermissions((prev) => ({ @@ -108,11 +109,11 @@ export const Permissions = () => { const handlePermissionOpened = (permission: string) => { if (permission === "screen") { - invoke("open_screen_capture_preferences"); + commands.openScreenCapturePreferences(); } else if (permission === "camera") { - invoke("open_camera_preferences"); + commands.openCameraPreferences(); } else if (permission === "microphone") { - invoke("open_mic_preferences"); + commands.openMicPreferences(); } }; diff --git a/apps/desktop/src/components/windows/inner/Recorder.tsx b/apps/desktop/src/components/windows/inner/Recorder.tsx index fcb4a721..bb76af7c 100644 --- a/apps/desktop/src/components/windows/inner/Recorder.tsx +++ b/apps/desktop/src/components/windows/inner/Recorder.tsx @@ -19,6 +19,7 @@ import { isUserPro, } from "@cap/utils"; import { openLinkInBrowser } from "@/utils/helpers"; +import * as commands from "@/utils/commands"; import toast, { Toaster } from "react-hot-toast"; import { authFetch } from "@/utils/auth/helpers"; @@ -203,8 +204,8 @@ export const Recorder = () => { }); await emit("toggle-recording", true); try { - await invoke("start_dual_recording", { - options: { + await commands + .startDualRecording({ user_id: videoData.user_id, video_id: videoData.id, audio_name: selectedAudioDevice?.label ?? "None", @@ -212,10 +213,10 @@ export const Recorder = () => { aws_bucket: videoData.aws_bucket, screen_index: "Capture screen 0", video_index: String(selectedVideoDevice?.index), - }, - }).catch((error) => { - console.error("Error invoking start_screen_recording:", error); - }); + }) + .catch((error) => { + console.error("Error invoking start_screen_recording:", error); + }); } catch (error) { console.error("Error starting screen recording:", error); setStartingRecording(false); @@ -265,7 +266,7 @@ export const Recorder = () => { console.log("Stopping recordings..."); try { - await invoke("stop_all_recordings"); + await commands.stopAllRecordings(); } catch (error) { console.error("Error stopping recording:", error); } diff --git a/apps/desktop/src/utils/commands.ts b/apps/desktop/src/utils/commands.ts new file mode 100644 index 00000000..8ef1f19a --- /dev/null +++ b/apps/desktop/src/utils/commands.ts @@ -0,0 +1,57 @@ +/* eslint-disable */ +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +declare global { + interface Window { + __TAURI_INVOKE__(cmd: string, args?: Record): Promise; + } +} + +// Function avoids 'window not defined' in SSR +const invoke = () => window.__TAURI_INVOKE__; + +export function startDualRecording(options: RecordingOptions) { + return invoke()("start_dual_recording", { options }) +} + +export function stopAllRecordings() { + return invoke()("stop_all_recordings") +} + +export function enumerateAudioDevices() { + return invoke()("enumerate_audio_devices") +} + +export function startServer() { + return invoke()("start_server") +} + +export function openScreenCapturePreferences() { + return invoke()("open_screen_capture_preferences") +} + +export function openMicPreferences() { + return invoke()("open_mic_preferences") +} + +export function openCameraPreferences() { + return invoke()("open_camera_preferences") +} + +export function hasScreenCaptureAccess() { + return invoke()("has_screen_capture_access") +} + +export function resetScreenPermissions() { + return invoke()("reset_screen_permissions") +} + +export function resetMicrophonePermissions() { + return invoke()("reset_microphone_permissions") +} + +export function resetCameraPermissions() { + return invoke()("reset_camera_permissions") +} + +export type RecordingOptions = { user_id: string; video_id: string; screen_index: string; video_index: string; audio_name: string; aws_region: string; aws_bucket: string } diff --git a/apps/desktop/src/utils/recording/utils.ts b/apps/desktop/src/utils/recording/utils.ts index c05e19b9..2b42b2b7 100644 --- a/apps/desktop/src/utils/recording/utils.ts +++ b/apps/desktop/src/utils/recording/utils.ts @@ -1,6 +1,6 @@ "use client"; -import { invoke } from "@tauri-apps/api/tauri"; +import * as commands from "@/utils/commands"; export const enumerateAndStoreDevices = async () => { if (typeof navigator !== "undefined" && typeof window !== "undefined") { @@ -8,7 +8,7 @@ export const enumerateAndStoreDevices = async () => { video: true, }); const video = await navigator.mediaDevices.enumerateDevices(); - const audio: string[] = await invoke("enumerate_audio_devices"); + const audio: string[] = await commands.enumerateAudioDevices(); const videoDevices = video.filter((device) => device.kind === "videoinput"); const audioDevices = audio.map((device) => { return { From d51d01d6338e0f6fa5d161645097f23c13a3083a Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 13 Jun 2024 01:57:06 +0800 Subject: [PATCH 2/2] generate_handler macro --- apps/desktop/src-tauri/src/main.rs | 51 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index e80fde88..f199f5d6 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -33,6 +33,17 @@ use ffmpeg_sidecar::{ use winit::monitor::{MonitorHandle, VideoMode}; +macro_rules! generate_handler { + ($($command:ident),*) => {{ + #[cfg(debug_assertions)] + tauri_specta::ts::export( + specta::collect_types![$($command),*], + "../src/utils/commands.ts" + ).unwrap(); + + tauri::generate_handler![$($command),*] + }} +} fn main() { let _ = fix_path_env::fix(); @@ -152,10 +163,14 @@ fn main() { })); let event_loop = winit::event_loop::EventLoop::new().expect("Failed to create event loop"); - let monitor: MonitorHandle = event_loop.primary_monitor().expect("No primary monitor found"); + let monitor: MonitorHandle = event_loop + .primary_monitor() + .expect("No primary monitor found"); let video_modes: Vec = monitor.video_modes().collect(); - let max_mode = video_modes.iter().max_by_key(|mode| mode.size().width * mode.size().height); + let max_mode = video_modes + .iter() + .max_by_key(|mode| mode.size().width * mode.size().height); let (max_width, max_height) = if let Some(max_mode) = max_mode { println!("Maximum resolution: {:?}", max_mode.size()); @@ -167,10 +182,10 @@ fn main() { #[derive(serde::Deserialize, PartialEq)] enum DeviceKind { - #[serde(alias="videoinput")] + #[serde(alias = "videoinput")] Video, - #[serde(alias="audioinput")] - Audio + #[serde(alias = "audioinput")] + Audio, } #[derive(serde::Deserialize)] @@ -178,7 +193,7 @@ fn main() { struct MediaDevice { id: String, kind: DeviceKind, - label: String + label: String, } fn create_tray_menu(submenus: Option>) -> SystemTrayMenu { @@ -196,22 +211,10 @@ fn main() { .add_item(CustomMenuItem::new("quit".to_string(), "Quit").accelerator("CmdOrControl+Q")) } - let tray = SystemTray::new().with_menu(create_tray_menu(None)).with_menu_on_left_click(false).with_title("Cap"); - - #[cfg(debug_assertions)] - tauri_specta::ts::export(specta::collect_types![ - start_dual_recording, - stop_all_recordings, - enumerate_audio_devices, - start_server, - open_screen_capture_preferences, - open_mic_preferences, - open_camera_preferences, - has_screen_capture_access, - reset_screen_permissions, - reset_microphone_permissions, - reset_camera_permissions, - ], "../src/utils/commands.ts").unwrap(); + let tray = SystemTray::new() + .with_menu(create_tray_menu(None)) + .with_menu_on_left_click(false) + .with_title("Cap"); tauri::Builder::default() .plugin(tauri_plugin_oauth::init()) @@ -322,7 +325,7 @@ fn main() { Ok(()) }) - .invoke_handler(tauri::generate_handler![ + .invoke_handler(generate_handler![ start_dual_recording, stop_all_recordings, enumerate_audio_devices, @@ -333,7 +336,7 @@ fn main() { has_screen_capture_access, reset_screen_permissions, reset_microphone_permissions, - reset_camera_permissions, + reset_camera_permissions ]) .plugin(tauri_plugin_context_menu::init()) .system_tray(tray)