Add automatic FFmpeg download for standalone Windows experience

FFmpeg is now automatically downloaded on first run if not found,
eliminating the need for vcpkg or manual FFmpeg installation on Windows.

Changes:
- Add FFmpeg availability check and auto-download on startup
- Use ffprobe_path() for auto-downloaded binaries in video decoder
- Track FFmpeg availability in app state with graceful degradation
- Show warning banner when video conversion is unavailable
- Skip video files when adding to queue if FFmpeg unavailable
- Simplify build instructions in README (remove vcpkg recommendation)
This commit is contained in:
cottongin
2026-02-05 13:28:04 -05:00
parent b9c0c4feda
commit f70be74145
6 changed files with 105 additions and 17 deletions

2
Cargo.lock generated
View File

@@ -536,7 +536,7 @@ dependencies = [
[[package]] [[package]]
name = "avif-maker" name = "avif-maker"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"directories", "directories",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "avif-maker" name = "avif-maker"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
description = "Convert images and videos to AVIF format with a native GUI" description = "Convert images and videos to AVIF format with a native GUI"
license = "MIT" license = "MIT"
@@ -19,6 +19,7 @@ image = "0.25"
gif = "0.14" gif = "0.14"
# Video decoding (runs ffmpeg as subprocess - no FFI issues) # Video decoding (runs ffmpeg as subprocess - no FFI issues)
# Default features include "download_ffmpeg" for auto-downloading FFmpeg on first run
ffmpeg-sidecar = "2" ffmpeg-sidecar = "2"
# Async & utilities # Async & utilities

View File

@@ -20,45 +20,51 @@ A native GUI application to convert images and videos to AVIF format.
| PNG/APNG | `image` crate | Full | Static images | | PNG/APNG | `image` crate | Full | Static images |
| JPEG | `image` crate | N/A | No alpha | | JPEG | `image` crate | N/A | No alpha |
| WebP | `image` crate | Full | Static only | | 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 ## Build Requirements
### macOS ### macOS
```bash ```bash
# Install dependencies # Install build dependencies (libavif is built from source)
brew install libavif ffmpeg brew install cmake nasm
# Build and run # Build and run
cargo build --release cargo build --release
./target/release/avif-maker ./target/release/avif-maker
``` ```
FFmpeg is automatically downloaded on first run if not found in PATH.
### Linux (Ubuntu/Debian) ### Linux (Ubuntu/Debian)
```bash ```bash
# Install dependencies # Install build dependencies (libavif is built from source)
sudo apt install libavif-dev ffmpeg sudo apt install build-essential cmake nasm
# Build and run # Build and run
cargo build --release cargo build --release
./target/release/avif-maker ./target/release/avif-maker
``` ```
FFmpeg is automatically downloaded on first run if not found in PATH.
### Windows ### 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 ```powershell
# With vcpkg
vcpkg install libavif ffmpeg
# Set environment variables
$env:VCPKG_ROOT = "C:\path\to\vcpkg"
# Build # Build
cargo build --release cargo build --release
# Run
.\target\release\avif-maker.exe
``` ```
## Usage ## Usage

View File

@@ -118,10 +118,12 @@ pub struct AvifMakerApp {
pub show_console: bool, pub show_console: bool,
/// Start time for relative timestamps /// Start time for relative timestamps
start_time: std::time::Instant, start_time: std::time::Instant,
/// Whether FFmpeg is available for video conversion
ffmpeg_available: bool,
} }
impl AvifMakerApp { impl AvifMakerApp {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self { pub fn new(cc: &eframe::CreationContext<'_>, ffmpeg_available: bool) -> Self {
// Configure larger fonts for accessibility // Configure larger fonts for accessibility
let mut style = (*cc.egui_ctx.style()).clone(); let mut style = (*cc.egui_ctx.style()).clone();
@@ -154,9 +156,13 @@ impl AvifMakerApp {
console_log: Vec::new(), console_log: Vec::new(),
show_console: false, show_console: false,
start_time: std::time::Instant::now(), start_time: std::time::Instant::now(),
ffmpeg_available,
}; };
app.log("AVIF Maker started"); app.log("AVIF Maker started");
if !ffmpeg_available {
app.log("WARNING: FFmpeg not available - video conversion disabled");
}
app app
} }
@@ -205,8 +211,16 @@ impl AvifMakerApp {
} }
pub fn add_files(&mut self, paths: Vec<PathBuf>) { pub fn add_files(&mut self, paths: Vec<PathBuf>) {
let mut skipped_videos = 0;
for path in paths { for path in paths {
if Self::is_supported_format(&path) { 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 { let job = ConversionJob {
id: self.next_job_id, id: self.next_job_id,
input_path: path, input_path: path,
@@ -220,6 +234,25 @@ impl AvifMakerApp {
self.next_job_id += 1; 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 { 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())); 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); ui.add_space(4.0);
}); });

View File

@@ -1,6 +1,7 @@
use super::{DecodeError, DecodedFrames, DecoderTrait, Frame, ProgressCallback}; use super::{DecodeError, DecodedFrames, DecoderTrait, Frame, ProgressCallback};
use ffmpeg_sidecar::command::FfmpegCommand; use ffmpeg_sidecar::command::FfmpegCommand;
use ffmpeg_sidecar::event::{FfmpegEvent, OutputVideoFrame}; use ffmpeg_sidecar::event::{FfmpegEvent, OutputVideoFrame};
use ffmpeg_sidecar::ffprobe::ffprobe_path;
use std::path::Path; use std::path::Path;
pub struct VideoDecoder { pub struct VideoDecoder {
@@ -23,7 +24,8 @@ impl VideoDecoder {
fn get_video_info(path: &Path) -> Result<(f64, Option<f64>, Option<usize>), DecodeError> { fn get_video_info(path: &Path) -> Result<(f64, Option<f64>, Option<usize>), DecodeError> {
use std::process::Command; 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([ .args([
"-v", "quiet", "-v", "quiet",
"-select_streams", "v:0", "-select_streams", "v:0",

View File

@@ -7,11 +7,44 @@ mod ui;
use app::AvifMakerApp; use app::AvifMakerApp;
use eframe::egui; 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<()> { fn main() -> eframe::Result<()> {
// Initialize logging // Initialize logging
tracing_subscriber::fmt::init(); 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 { let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_inner_size([1000.0, 900.0]) .with_inner_size([1000.0, 900.0])
@@ -23,6 +56,6 @@ fn main() -> eframe::Result<()> {
eframe::run_native( eframe::run_native(
"AVIF Maker", "AVIF Maker",
native_options, native_options,
Box::new(|cc| Ok(Box::new(AvifMakerApp::new(cc)))), Box::new(move |cc| Ok(Box::new(AvifMakerApp::new(cc, ffmpeg_available)))),
) )
} }