diff --git a/.github/prebuild.js b/.github/prebuild.js index 135328e8d..46754bf5c 100644 --- a/.github/prebuild.js +++ b/.github/prebuild.js @@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url"; import { exec as execCb } from "node:child_process"; import { env } from "node:process"; import { promisify } from "node:util"; -import extract from "extract-zip"; const exec = promisify(execCb); const signId = env.APPLE_SIGNING_IDENTITY || "-"; diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9730ca8fe..17f7f3d6d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -142,6 +142,7 @@ jobs: - name: Build MacOS Apps working-directory: apps/desktop run: | + export TARGET_TRIPLE=${{ matrix.settings.target }} node ${{ github.workspace }}/.github/prebuild.js ${{ matrix.settings.prebuild }} pnpm tauri build --target ${{ matrix.settings.target }} env: diff --git a/Cargo.lock b/Cargo.lock index 6cc772bdd..e50603db3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -659,6 +659,8 @@ version = "0.1.0" dependencies = [ "cap-utils", "ffmpeg-sidecar 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tauri", + "tauri-plugin-shell", ] [[package]] @@ -1398,7 +1400,7 @@ dependencies = [ [[package]] name = "desktop" -version = "0.3.0-alpha.1" +version = "0.3.0-alpha.2" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 025afab46..fca993bba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ members = [ [workspace.dependencies] ffmpeg-next = { git = "https://github.com/CapSoftware/rust-ffmpeg", rev = "11f8d264bbc0" } +tauri = { version = "2.0.0-rc" } diff --git a/apps/desktop/scripts/prepareSidecars.js b/apps/desktop/scripts/prepareSidecars.js new file mode 100644 index 000000000..857aae898 --- /dev/null +++ b/apps/desktop/scripts/prepareSidecars.js @@ -0,0 +1,71 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { exec as execCb } from "node:child_process"; +import { promisify } from "node:util"; + +const exec = promisify(execCb); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const binariesDir = path.join(__dirname, "../../../target/binaries"); + +const FFMPEG_BINARIES = { + "aarch64-apple-darwin": { + url: "https://cap-ffmpeg.s3.amazonaws.com/ffmpegarm.zip", + path: "./ffmpeg", + }, + "x86_64-apple-darwin": { + url: "https://cap-ffmpeg.s3.amazonaws.com/ffmpeg-7.0.1.zip", + path: "./ffmpeg", + }, +}; + +async function getRustupTarget() { + const { stdout } = await exec("rustup show"); + const line = stdout + .split("\n") + .find((line) => line.includes("Default host:")); + + return line.split(":")[1].trim(); +} + +async function exists(path) { + return await fs + .access(path) + .then(() => true) + .catch(() => false); +} + +async function main() { + const targetTriple = process.env.TARGET_TRIPLE ?? (await getRustupTarget()); + const binaries = FFMPEG_BINARIES[targetTriple]; + + const ffmpegDownloadPath = path.join(binariesDir, "ffmpeg-download"); + if (!(await exists(ffmpegDownloadPath))) { + await fs.mkdir(binariesDir, { recursive: true }); + console.log("downloading ffmpeg archive"); + const archive = await fetch(binaries.url) + .then((r) => r.blob()) + .then((b) => b.arrayBuffer()) + .then((a) => Buffer.from(a)); + + await fs.writeFile(ffmpegDownloadPath, archive); + } + + const ffmpegUnzippedPath = path.join(binariesDir, "ffmpeg-unzipped"); + if (!(await exists(ffmpegUnzippedPath))) { + console.log("extracting ffmpeg archive"); + await exec(`unzip ${ffmpegDownloadPath} -d ${ffmpegUnzippedPath}`); + } + + const ffmpegSidecarName = `ffmpeg-${targetTriple}`; + console.log(`copying ffmpeg binary '${ffmpegSidecarName}`); + await fs.copyFile( + path.join(ffmpegUnzippedPath, binaries.path), + path.join(binariesDir, ffmpegSidecarName) + ); +} + +main(); diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 1312f1a98..2601c24fe 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "desktop" -version = "0.3.0-alpha.1" +version = "0.3.0-alpha.2" description = "A Tauri App" authors = ["you"] edition = "2021" @@ -16,7 +16,7 @@ tauri-build = { version = "2.0.0-rc", features = [] } swift-rs = { version = "1.0.6", features = ["build"] } [dependencies] -tauri = { version = "2.0.0-rc", features = [ +tauri = { workspace = true, features = [ "macos-private-api", "protocol-asset", "tray-icon", diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index d860e1e6a..261851f6b 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build(); } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 5caebaff9..98faf0989 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -862,6 +862,7 @@ async fn remove_fake_window( const PREV_RECORDINGS_WINDOW: &str = "prev-recordings"; +// must not be async bc of panel #[tauri::command] #[specta::specta] fn show_previous_recordings_window(app: AppHandle) { @@ -908,7 +909,7 @@ fn show_previous_recordings_window(app: AppHandle) { window.make_transparent().ok(); let panel = window.to_panel().unwrap(); - panel.set_level(NSMainMenuWindowLevel + 1); + panel.set_level(NSMainMenuWindowLevel); panel.set_collection_behaviour( NSWindowCollectionBehavior::NSWindowCollectionBehaviorTransient @@ -962,7 +963,7 @@ fn show_previous_recordings_window(app: AppHandle) { }); } -#[tauri::command] +#[tauri::command(async)] #[specta::specta] fn open_editor(app: AppHandle, id: String) { let window = WebviewWindow::builder( @@ -983,7 +984,7 @@ fn open_editor(app: AppHandle, id: String) { window.set_traffic_lights_inset(20.0, 48.0).unwrap(); } -#[tauri::command] +#[tauri::command(async)] #[specta::specta] fn close_previous_recordings_window(app: AppHandle) { if let Ok(panel) = app.get_webview_panel(PREV_RECORDINGS_WINDOW) { @@ -1006,7 +1007,7 @@ fn on_recording_options_change(app: &AppHandle, options: &RecordingOptions) { RecordingOptionsChanged.emit(app).ok(); } -#[tauri::command] +#[tauri::command(async)] #[specta::specta] fn focus_captures_panel(app: AppHandle) { let panel = app.get_webview_panel(PREV_RECORDINGS_WINDOW).unwrap(); @@ -1089,7 +1090,7 @@ async fn save_project_config(app: AppHandle, video_id: String, config: ProjectCo .unwrap(); } -#[tauri::command] +#[tauri::command(async)] #[specta::specta] fn open_in_finder(path: PathBuf) { Command::new("open") @@ -1111,7 +1112,7 @@ async fn list_audio_devices() -> Result, ()> { .map_err(|_| ()) } -#[tauri::command] +#[tauri::command(async)] #[specta::specta] fn open_main_window(app: AppHandle) { if let Some(window) = app.get_webview_window("main") { @@ -1296,12 +1297,6 @@ pub fn run() { let app_handle = app.handle().clone(); - if let Err(_error) = FFmpeg::install_if_necessary() { - println!("Failed to install FFmpeg, which is required for Cap to function. Shutting down now"); - // TODO: UI message instead - panic!("Failed to install FFmpeg, which is required for Cap to function. Shutting down now") - }; - if permissions::do_permissions_check().necessary_granted() { open_main_window(app_handle.clone()); } else { @@ -1313,7 +1308,7 @@ pub fn run() { start_recording_options: RecordingOptions { capture_target: CaptureTarget::Screen, camera_label: None, - audio_input_name: None + audio_input_name: None, }, current_recording: None, }))); @@ -1334,21 +1329,21 @@ pub fn run() { Ok(()) }) .on_window_event(|window, event| { - let label = window.label(); - if label.starts_with("editor-") { - if let WindowEvent::CloseRequested {..} = event { - let id = label.strip_prefix("editor-").unwrap().to_string(); - - let app = window.app_handle().clone(); - - tokio::spawn(async move { - if let Some(editor) = remove_editor_instance(&app, id.clone()).await { - editor.dispose().await; - } - }); - } - } - }) + let label = window.label(); + if label.starts_with("editor-") { + if let WindowEvent::CloseRequested { .. } = event { + let id = label.strip_prefix("editor-").unwrap().to_string(); + + let app = window.app_handle().clone(); + + tokio::spawn(async move { + if let Some(editor) = remove_editor_instance(&app, id.clone()).await { + editor.dispose().await; + } + }); + } + } + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index e2499144f..d69ee6009 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -3,9 +3,9 @@ "productName": "Cap", "identifier": "so.cap.desktop", "build": { - "beforeDevCommand": "pnpm localdev", + "beforeDevCommand": "node scripts/prepareSidecars.js && pnpm localdev", "devUrl": "http://localhost:3001", - "beforeBuildCommand": "pnpm build", + "beforeBuildCommand": "node scripts/prepareSidecars.js && pnpm build", "frontendDist": "../.output/public" }, "app": { @@ -37,6 +37,7 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "externalBin": ["../../../target/binaries/ffmpeg"] } } diff --git a/crates/ffmpeg/Cargo.toml b/crates/ffmpeg/Cargo.toml index 80c721da1..e2fea2a4a 100644 --- a/crates/ffmpeg/Cargo.toml +++ b/crates/ffmpeg/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" [dependencies] ffmpeg-sidecar = "1.1.0" cap-utils = { path = "../utils" } +tauri = { workspace = true } +tauri-plugin-shell = "2.0.0-rc" diff --git a/crates/ffmpeg/src/lib.rs b/crates/ffmpeg/src/lib.rs index d1abe8e51..7989b9b62 100644 --- a/crates/ffmpeg/src/lib.rs +++ b/crates/ffmpeg/src/lib.rs @@ -1,26 +1,16 @@ -mod utils; -pub use utils::*; - +use cap_utils::create_named_pipe; use std::{ ffi::OsString, io::{Read, Write}, ops::Deref, - path::PathBuf, + path::{Path, PathBuf}, process::{Child, ChildStdin, Command, Stdio}, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, }; - -use ffmpeg_sidecar::{ - command::ffmpeg_is_installed, - download::{check_latest_version, download_ffmpeg_package, ffmpeg_download_url, unpack_ffmpeg}, - paths::sidecar_dir, - version::ffmpeg_version, -}; - -use cap_utils::create_named_pipe; +use tauri::utils::platform; pub struct FFmpegProcess { pub ffmpeg_stdin: ChildStdin, @@ -202,6 +192,8 @@ impl ApplyFFmpegArgs for FFmpegRawVideoInput { command.args(["-r", &self.fps.to_string()]); } + dbg!(PathBuf::from(&self.input).exists()); + // if self.offset != 0.0 { // command.args(["-itsoffset", &self.offset.to_string()]); // } @@ -247,18 +239,10 @@ pub struct FFmpeg { source_index: u8, } -impl Default for FFmpeg { - fn default() -> Self { - Self::new() - } -} - impl FFmpeg { pub fn new() -> Self { - let ffmpeg_binary_path_str = ffmpeg_path_as_str().unwrap().to_owned(); - Self { - command: Command::new(ffmpeg_binary_path_str), + command: Command::new(dbg!(relative_command_path("ffmpeg").unwrap())).into(), source_index: 0, } } @@ -309,29 +293,14 @@ impl FFmpeg { pub fn start(self) -> FFmpegProcess { FFmpegProcess::spawn(self.command) } +} - pub fn install_if_necessary() -> Result<(), String> { - if ffmpeg_is_installed() { - return Ok(()); - } - - match check_latest_version() { - Ok(version) => println!("Latest available version: {}", version), - Err(e) => println!("Skipping version check due to error: {e}"), - } - - let download_url = ffmpeg_download_url().map_err(|e| e.to_string())?; - let destination = sidecar_dir().map_err(|e| e.to_string())?; - - let archive_path = - download_ffmpeg_package(download_url, &destination).map_err(|e| e.to_string())?; - - unpack_ffmpeg(&archive_path, &destination).map_err(|e| e.to_string())?; - - let version = ffmpeg_version().map_err(|e| e.to_string())?; - - println!("Done! Installed FFmpeg version {} 🏁", version); - - Ok(()) +fn relative_command_path(command: impl AsRef) -> Result { + match platform::current_exe()?.parent() { + #[cfg(windows)] + Some(exe_dir) => Ok(exe_dir.join(command.as_ref()).with_extension("exe")), + #[cfg(not(windows))] + Some(exe_dir) => Ok(exe_dir.join(command.as_ref())), + None => Err(tauri_plugin_shell::Error::CurrentExeHasNoParent), } } diff --git a/crates/ffmpeg/src/utils.rs b/crates/ffmpeg/src/utils.rs deleted file mode 100644 index 872402dfc..000000000 --- a/crates/ffmpeg/src/utils.rs +++ /dev/null @@ -1,20 +0,0 @@ -use ffmpeg_sidecar::paths::sidecar_dir; -use std::path::Path; - -pub(crate) fn ffmpeg_path_as_str() -> Result { - let binary_name = if cfg!(target_os = "windows") { - "ffmpeg.exe" - } 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()) - .ok_or_else(|| "Failed to convert FFmpeg binary path to string".to_string()) - } else { - Ok("ffmpeg".to_string()) - } -} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 8a52ecaec..33a11bc23 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -nix = "0.29.0" +nix = { version = "0.29.0", features = ["fs"] } [target.'cfg(target_os = "windows")'.dependencies] -winapi = "0.3.9" \ No newline at end of file +winapi = "0.3.9" diff --git a/package.json b/package.json index d84e54c5b..16ede9614 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "format": "prettier --write \"**/*.{ts,tsx,md}\"", "db:push": "dotenv -e .env -- pnpm --dir packages/database db:push", "db:generate": "dotenv -e .env -- pnpm --dir packages/database db:generate", - "tauri:build": "dotenv -e .env -- pnpm --dir apps/desktop tauri build --verbose" + "tauri:build": "dotenv -e .env -- pnpm --dir apps/desktop tauri build --verbose", + "postinstall": "node scripts/postinstall.js" }, "devDependencies": { "@turbo/gen": "^1.9.7",