Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions examples/download_ffmpeg_with_progress.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use ffmpeg_sidecar::download::{
download_ffmpeg_package_with_progress, ffmpeg_download_url, unpack_ffmpeg,
FfmpegDownloadProgressEvent,
};
use ffmpeg_sidecar::version::ffmpeg_version;
use std::io::Write;

#[cfg(feature = "download_ffmpeg")]
fn main() -> anyhow::Result<()> {
use ffmpeg_sidecar::command::ffmpeg_is_installed;

if ffmpeg_is_installed() {
println!("FFmpeg is already installed! 🎉");
println!("For demo purposes, we'll re-download and unpack it anyway.");
println!(
"TIP: Use `auto_download_with_progress(progress_callback)` to skip manual customization."
);
}

let progress_callback = |e: FfmpegDownloadProgressEvent| match e {
FfmpegDownloadProgressEvent::Starting => {
println!("Starting download...");
}
FfmpegDownloadProgressEvent::Downloading {
downloaded_bytes,
total_bytes,
} => {
print!(
"\rDownloaded {:.1}/{:.1} mB ",
downloaded_bytes as f64 / 1024.0 / 1024.0,
total_bytes as f64 / 1024.0 / 1024.0
);
std::io::stdout().flush().unwrap();
}
FfmpegDownloadProgressEvent::UnpackingArchive => {
println!("\nUnpacking archive...");
}
FfmpegDownloadProgressEvent::Done => {
println!("Ffmpeg downloaded successfully!")
}
};

force_download_with_progress(progress_callback)?;

let version = ffmpeg_version()?;
println!("FFmpeg version: {version}");
Ok(())
}

#[cfg(feature = "download_ffmpeg")]
pub fn force_download_with_progress(
progress_callback: impl Fn(FfmpegDownloadProgressEvent),
) -> anyhow::Result<()> {
use ffmpeg_sidecar::{command::ffmpeg_is_installed, paths::sidecar_dir};

progress_callback(FfmpegDownloadProgressEvent::Starting);
let download_url = ffmpeg_download_url()?;
let destination = sidecar_dir()?;
let archive_path =
download_ffmpeg_package_with_progress(download_url, &destination, |e| progress_callback(e))?;
progress_callback(FfmpegDownloadProgressEvent::UnpackingArchive);
unpack_ffmpeg(&archive_path, &destination)?;
progress_callback(FfmpegDownloadProgressEvent::Done);

if !ffmpeg_is_installed() {
anyhow::bail!("FFmpeg failed to install, please install manually.");
}

Ok(())
}

#[cfg(not(feature = "download_ffmpeg"))]
fn main() {
eprintln!(r#"This example requires the "download_ffmpeg" feature to be enabled."#);
println!("The feature is included by default unless manually disabled.");
println!("Please run `cargo run --example download_ffmpeg`.");
}
107 changes: 107 additions & 0 deletions src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,49 @@ pub fn auto_download() -> Result<()> {
Ok(())
}

pub enum FfmpegDownloadProgressEvent {
Starting,
Downloading {
total_bytes: u64,
downloaded_bytes: u64,
},
UnpackingArchive,
Done,
}

/// Check if FFmpeg is installed, and if it's not, download and unpack it.
/// Automatically selects the correct binaries for Windows, Linux, and MacOS.
/// The binaries will be placed in the same directory as the Rust executable.
///
/// Provides progress tracking via callback.
///
/// If FFmpeg is already installed, the method exits early without downloading
/// anything.
#[cfg(feature = "download_ffmpeg")]
pub fn auto_download_with_progress(
progress_callback: impl Fn(FfmpegDownloadProgressEvent),
) -> Result<()> {
use crate::{command::ffmpeg_is_installed, paths::sidecar_dir};

if ffmpeg_is_installed() {
return Ok(());
}

progress_callback(FfmpegDownloadProgressEvent::Starting);
let download_url = ffmpeg_download_url()?;
let destination = sidecar_dir()?;
let archive_path = download_ffmpeg_package_with_progress(download_url, &destination, |e| progress_callback(e))?;
progress_callback(FfmpegDownloadProgressEvent::UnpackingArchive);
unpack_ffmpeg(&archive_path, &destination)?;
progress_callback(FfmpegDownloadProgressEvent::Done);

if !ffmpeg_is_installed() {
anyhow::bail!("FFmpeg failed to install, please install manually.");
}

Ok(())
}

/// Parse the the MacOS version number from a JSON string manifest file.
///
/// Example input: <https://evermeet.cx/ffmpeg/info/ffmpeg/release>
Expand Down Expand Up @@ -164,6 +207,70 @@ pub fn download_ffmpeg_package(url: &str, download_dir: &Path) -> Result<PathBuf
Ok(archive_path)
}

/// Make an HTTP request to download an archive from the latest published release online with progress tracking.
#[cfg(feature = "download_ffmpeg")]
pub fn download_ffmpeg_package_with_progress(
url: &str,
download_dir: &Path,
progress_callback: impl Fn(FfmpegDownloadProgressEvent),
) -> Result<PathBuf> {
use anyhow::Context;
use std::{
fs::File,
io::{copy, Read},
path::Path,
};

let filename = Path::new(url)
.file_name()
.context("Failed to get filename")?;

let archive_path = download_dir.join(filename);

let mut response = ureq::get(url).call().context("Failed to download ffmpeg")?;

let total_size = response
.headers()
.get("Content-Length")
.and_then(|s| s.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);

let mut file =
File::create(&archive_path).context("Failed to create file for ffmpeg download")?;

// Wrapper to track progress during io::copy
struct ProgressReader<R, F> {
inner: R,
progress_callback: F,
downloaded: u64,
total: u64,
}

impl<R: Read, F: Fn(FfmpegDownloadProgressEvent)> Read for ProgressReader<R, F> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.inner.read(buf)?;
self.downloaded += n as u64;
(self.progress_callback)(FfmpegDownloadProgressEvent::Downloading {
total_bytes: self.total,
downloaded_bytes: self.downloaded,
});
Ok(n)
}
}

let mut progress_reader = ProgressReader {
inner: response.body_mut().as_reader(),
progress_callback,
downloaded: 0,
total: total_size,
};

copy(&mut progress_reader, &mut file).context("Failed to write ffmpeg download to file")?;

Ok(archive_path)
}

/// After downloading, unpacks the archive to a folder, moves the binaries to
/// their final location, and deletes the archive and temporary folder.
#[cfg(feature = "download_ffmpeg")]
Expand Down