diff --git a/Cargo.lock b/Cargo.lock index 873a338..57e0dc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,7 +536,7 @@ dependencies = [ [[package]] name = "avif-maker" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "directories", diff --git a/Cargo.toml b/Cargo.toml index 53d2004..568c6f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "avif-maker" -version = "0.1.0" +version = "0.1.1" edition = "2021" description = "Convert images and videos to AVIF format with a native GUI" license = "MIT" @@ -19,6 +19,7 @@ image = "0.25" gif = "0.14" # Video decoding (runs ffmpeg as subprocess - no FFI issues) +# Default features include "download_ffmpeg" for auto-downloading FFmpeg on first run ffmpeg-sidecar = "2" # Async & utilities diff --git a/README.md b/README.md index 84ed9a6..7c85ce6 100644 --- a/README.md +++ b/README.md @@ -20,45 +20,51 @@ A native GUI application to convert images and videos to AVIF format. | PNG/APNG | `image` crate | Full | Static images | | JPEG | `image` crate | N/A | No alpha | | WebP | `image` crate | Full | Static only | -| MP4/MOV/WebM | `ffmpeg-sidecar` | Codec-dependent | Requires ffmpeg in PATH | +| MP4/MOV/WebM | `ffmpeg-sidecar` | Codec-dependent | FFmpeg auto-downloaded on first run | ## Build Requirements ### macOS ```bash -# Install dependencies -brew install libavif ffmpeg +# Install build dependencies (libavif is built from source) +brew install cmake nasm # Build and run cargo build --release ./target/release/avif-maker ``` +FFmpeg is automatically downloaded on first run if not found in PATH. + ### Linux (Ubuntu/Debian) ```bash -# Install dependencies -sudo apt install libavif-dev ffmpeg +# Install build dependencies (libavif is built from source) +sudo apt install build-essential cmake nasm # Build and run cargo build --release ./target/release/avif-maker ``` +FFmpeg is automatically downloaded on first run if not found in PATH. + ### Windows -Use vcpkg or pre-built binaries for libavif and FFmpeg. +The application automatically downloads FFmpeg on first run if not found. No manual setup required for end users. + +To build from source, install: +- [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) (with C++ workload) +- [CMake](https://cmake.org/download/) +- [NASM](https://www.nasm.us/) (add to PATH) ```powershell -# With vcpkg -vcpkg install libavif ffmpeg - -# Set environment variables -$env:VCPKG_ROOT = "C:\path\to\vcpkg" - # Build cargo build --release + +# Run +.\target\release\avif-maker.exe ``` ## Usage diff --git a/src/app.rs b/src/app.rs index 0825587..eaedf4d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -118,10 +118,12 @@ pub struct AvifMakerApp { pub show_console: bool, /// Start time for relative timestamps start_time: std::time::Instant, + /// Whether FFmpeg is available for video conversion + ffmpeg_available: bool, } impl AvifMakerApp { - pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + pub fn new(cc: &eframe::CreationContext<'_>, ffmpeg_available: bool) -> Self { // Configure larger fonts for accessibility let mut style = (*cc.egui_ctx.style()).clone(); @@ -154,9 +156,13 @@ impl AvifMakerApp { console_log: Vec::new(), show_console: false, start_time: std::time::Instant::now(), + ffmpeg_available, }; app.log("AVIF Maker started"); + if !ffmpeg_available { + app.log("WARNING: FFmpeg not available - video conversion disabled"); + } app } @@ -205,8 +211,16 @@ impl AvifMakerApp { } pub fn add_files(&mut self, paths: Vec) { + let mut skipped_videos = 0; + for path in paths { if Self::is_supported_format(&path) { + // Skip video files if FFmpeg is not available + if Self::is_video_format(&path) && !self.ffmpeg_available { + skipped_videos += 1; + continue; + } + let job = ConversionJob { id: self.next_job_id, input_path: path, @@ -220,6 +234,25 @@ impl AvifMakerApp { self.next_job_id += 1; } } + + if skipped_videos > 0 { + self.log(format!( + "Skipped {} video file(s) - FFmpeg not available. Restart the app with internet to download FFmpeg.", + skipped_videos + )); + } + } + + fn is_video_format(path: &PathBuf) -> bool { + let ext = path + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_lowercase()); + + matches!( + ext.as_deref(), + Some("mp4") | Some("mov") | Some("webm") | Some("mkv") | Some("avi") | Some("m4v") + ) } fn is_supported_format(path: &PathBuf) -> bool { @@ -564,6 +597,19 @@ impl eframe::App for AvifMakerApp { ui.label(format!("{} files in queue", self.jobs.len())); }); }); + + // Show warning if FFmpeg is not available + if !self.ffmpeg_available { + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("Video conversion unavailable - FFmpeg could not be downloaded. Restart with internet connection.") + .color(egui::Color32::from_rgb(255, 180, 100)) + .small() + ); + }); + } + ui.add_space(4.0); }); diff --git a/src/decoder/video.rs b/src/decoder/video.rs index 0526096..f042c7f 100644 --- a/src/decoder/video.rs +++ b/src/decoder/video.rs @@ -1,6 +1,7 @@ use super::{DecodeError, DecodedFrames, DecoderTrait, Frame, ProgressCallback}; use ffmpeg_sidecar::command::FfmpegCommand; use ffmpeg_sidecar::event::{FfmpegEvent, OutputVideoFrame}; +use ffmpeg_sidecar::ffprobe::ffprobe_path; use std::path::Path; pub struct VideoDecoder { @@ -23,7 +24,8 @@ impl VideoDecoder { fn get_video_info(path: &Path) -> Result<(f64, Option, Option), DecodeError> { use std::process::Command; - let output = Command::new("ffprobe") + // Use ffprobe_path() to find the binary (works with auto-downloaded FFmpeg) + let output = Command::new(ffprobe_path()) .args([ "-v", "quiet", "-select_streams", "v:0", diff --git a/src/main.rs b/src/main.rs index c9610a9..c847b7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,11 +7,44 @@ mod ui; use app::AvifMakerApp; use eframe::egui; +use ffmpeg_sidecar::command::ffmpeg_is_installed; +use ffmpeg_sidecar::download::auto_download; + +/// Ensures FFmpeg is available, downloading it if necessary. +/// Returns true if FFmpeg is available, false if download failed. +fn ensure_ffmpeg() -> bool { + if ffmpeg_is_installed() { + tracing::info!("FFmpeg found in PATH or alongside executable"); + return true; + } + + tracing::info!("FFmpeg not found, attempting to download..."); + + // On Windows without a console, we can't show progress easily before the GUI starts. + // The download is ~100MB and happens once on first run. + match auto_download() { + Ok(()) => { + tracing::info!("FFmpeg downloaded successfully"); + true + } + Err(e) => { + tracing::error!("Failed to download FFmpeg: {}. Video features will be unavailable.", e); + // Don't fail completely - the app can still work with images + false + } + } +} fn main() -> eframe::Result<()> { // Initialize logging tracing_subscriber::fmt::init(); + // Ensure FFmpeg is available (downloads automatically on first run if needed) + let ffmpeg_available = ensure_ffmpeg(); + if !ffmpeg_available { + tracing::warn!("Starting without FFmpeg - video conversion will not be available"); + } + let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([1000.0, 900.0]) @@ -23,6 +56,6 @@ fn main() -> eframe::Result<()> { eframe::run_native( "AVIF Maker", native_options, - Box::new(|cc| Ok(Box::new(AvifMakerApp::new(cc)))), + Box::new(move |cc| Ok(Box::new(AvifMakerApp::new(cc, ffmpeg_available)))), ) }