diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index a29daa3e4..ac211f898 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -46,7 +46,7 @@ const overrides = new Map([ ["src/shared/api/relayClientSession.ts", 930], // durable websocket session manager with reconnect/replay/recovery state + sendTypingIndicator + fetchChannelHistoryBefore + subscribeToChannelLive (huddle TTS) + subscribeToHuddleEvents (huddle indicator) + disconnect() for workspace switch teardown + fetchEvents/subscribeLive/publishEvent for NIP-RS read state + publishUserStatus/subscribeToUserStatusUpdates (NIP-38) ["src/shared/api/tauri.ts", 1100], // remote agent provider API bindings + canvas API functions ["src-tauri/src/lib.rs", 710], // sprout-media:// proxy + Range headers + Sprout nest init (ensure_nest) in setup() + huddle command registration + PTT global shortcut handler + persona pack commands + app_handle storage for event emission - ["src-tauri/src/commands/media.rs", 720], // ffmpeg video transcode + poster frame extraction + run_ffmpeg_with_timeout (find_ffmpeg, is_video_file, transcode_to_mp4, extract_poster_frame, transcode_and_extract_poster) + spawn_blocking wrappers + tests + ["src-tauri/src/commands/media.rs", 730], // ffmpeg video transcode + poster frame extraction + run_ffmpeg_with_timeout (find_ffmpeg via resolve_command, is_video_file, transcode_to_mp4, extract_poster_frame, transcode_and_extract_poster) + spawn_blocking wrappers + tests ["src-tauri/src/commands/agents.rs", 881], // remote agent lifecycle routing (local + provider branches) + scope enforcement + persona pack metadata wiring + mcp_toolsets field + NIP-OA auth_tag in deploy payload ["src-tauri/src/commands/messages.rs", 510], // feed multi-query + NIP-50 search + forum thread resolution + thread ref + reactions via REQ ["src-tauri/src/nostr_convert.rs", 870], // 12 Nostr event→model converters (channels, profiles, members, notes, search, agents, relay members) + 20 unit tests diff --git a/desktop/src-tauri/src/commands/media.rs b/desktop/src-tauri/src/commands/media.rs index b778fea36..be9811e57 100644 --- a/desktop/src-tauri/src/commands/media.rs +++ b/desktop/src-tauri/src/commands/media.rs @@ -5,6 +5,7 @@ use sha2::{Digest, Sha256}; use tauri::State; use crate::app_state::AppState; +use crate::managed_agents::resolve_command; use crate::relay::relay_api_base_url_with_override; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -234,27 +235,30 @@ pub async fn upload_media( // ── Video transcode helpers ────────────────────────────────────────────────── -/// Check if ffmpeg is available on PATH. -fn find_ffmpeg() -> Result<(), String> { - match std::process::Command::new("ffmpeg") +/// Locate ffmpeg using the same discovery logic as managed agents +/// (login shell PATH, /opt/homebrew/bin, /usr/local/bin, etc.). +/// Returns the resolved absolute path on success. +fn find_ffmpeg() -> Result { + let ffmpeg_path = resolve_command("ffmpeg", None).ok_or_else(|| { + "ffmpeg is required for video uploads but was not found.\n\n\ + Install it:\n \ + macOS: brew install ffmpeg\n \ + Linux: sudo apt install ffmpeg\n \ + Windows: winget install ffmpeg" + .to_string() + })?; + + match std::process::Command::new(&ffmpeg_path) .arg("-version") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() { - Ok(s) if s.success() => Ok(()), + Ok(s) if s.success() => Ok(ffmpeg_path), Ok(_) => Err( "ffmpeg was found but returned an error — it may be broken or misconfigured" .to_string(), ), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err( - "ffmpeg is required for video uploads but was not found.\n\n\ - Install it:\n \ - macOS: brew install ffmpeg\n \ - Linux: sudo apt install ffmpeg\n \ - Windows: winget install ffmpeg" - .to_string(), - ), Err(e) => Err(format!("failed to check for ffmpeg: {e}")), } } @@ -330,15 +334,16 @@ fn run_ffmpeg_with_timeout( /// relay's `validate_video_file()`. /// /// Returns the path to a temp file. Caller must clean up. -fn transcode_to_mp4(source: &std::path::Path) -> Result { - find_ffmpeg()?; - +fn transcode_to_mp4( + source: &std::path::Path, + ffmpeg: &std::path::Path, +) -> Result { // UUID-based temp path — unique across concurrent uploads. let output = std::env::temp_dir().join(format!("sprout-transcode-{}.mp4", uuid::Uuid::new_v4())); let result = run_ffmpeg_with_timeout( - std::process::Command::new("ffmpeg") + std::process::Command::new(ffmpeg) .args(["-y", "-loglevel", "error"]) // suppress progress spam — prevents stderr pipe deadlock .arg("-i") .arg(source) // OsStr — handles non-UTF-8 paths on Unix @@ -386,7 +391,10 @@ fn transcode_to_mp4(source: &std::path::Path) -> Result Result { +fn extract_poster_frame( + mp4_path: &std::path::Path, + ffmpeg: &std::path::Path, +) -> Result { let output = std::env::temp_dir().join(format!("sprout-poster-{}.jpg", uuid::Uuid::new_v4())); // Poster extraction is a single-frame decode — 30s is generous. @@ -394,7 +402,7 @@ fn extract_poster_frame(mp4_path: &std::path::Path) -> Result Result Result Result<(Vec, Option>), String> { - let transcoded = transcode_to_mp4(source)?; + let ffmpeg_path = find_ffmpeg()?; + let transcoded = transcode_to_mp4(source, &ffmpeg_path)?; // Extract poster from the transcoded file (not the original — guarantees decodability). - let poster_bytes = match extract_poster_frame(&transcoded) { + let poster_bytes = match extract_poster_frame(&transcoded, &ffmpeg_path) { Ok(poster_path) => { let bytes = std::fs::read(&poster_path).ok(); let _ = std::fs::remove_file(&poster_path);