Skip to content

Commit a4d189d

Browse files
authored
feat: add download_ffmpeg_package_with_progress and auto_download_with_progress (#95)
1 parent 57e08ed commit a4d189d

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use ffmpeg_sidecar::download::{
2+
download_ffmpeg_package_with_progress, ffmpeg_download_url, unpack_ffmpeg,
3+
FfmpegDownloadProgressEvent,
4+
};
5+
use ffmpeg_sidecar::version::ffmpeg_version;
6+
use std::io::Write;
7+
8+
#[cfg(feature = "download_ffmpeg")]
9+
fn main() -> anyhow::Result<()> {
10+
use ffmpeg_sidecar::command::ffmpeg_is_installed;
11+
12+
if ffmpeg_is_installed() {
13+
println!("FFmpeg is already installed! 🎉");
14+
println!("For demo purposes, we'll re-download and unpack it anyway.");
15+
println!(
16+
"TIP: Use `auto_download_with_progress(progress_callback)` to skip manual customization."
17+
);
18+
}
19+
20+
let progress_callback = |e: FfmpegDownloadProgressEvent| match e {
21+
FfmpegDownloadProgressEvent::Starting => {
22+
println!("Starting download...");
23+
}
24+
FfmpegDownloadProgressEvent::Downloading {
25+
downloaded_bytes,
26+
total_bytes,
27+
} => {
28+
print!(
29+
"\rDownloaded {:.1}/{:.1} mB ",
30+
downloaded_bytes as f64 / 1024.0 / 1024.0,
31+
total_bytes as f64 / 1024.0 / 1024.0
32+
);
33+
std::io::stdout().flush().unwrap();
34+
}
35+
FfmpegDownloadProgressEvent::UnpackingArchive => {
36+
println!("\nUnpacking archive...");
37+
}
38+
FfmpegDownloadProgressEvent::Done => {
39+
println!("Ffmpeg downloaded successfully!")
40+
}
41+
};
42+
43+
force_download_with_progress(progress_callback)?;
44+
45+
let version = ffmpeg_version()?;
46+
println!("FFmpeg version: {version}");
47+
Ok(())
48+
}
49+
50+
#[cfg(feature = "download_ffmpeg")]
51+
pub fn force_download_with_progress(
52+
progress_callback: impl Fn(FfmpegDownloadProgressEvent),
53+
) -> anyhow::Result<()> {
54+
use ffmpeg_sidecar::{command::ffmpeg_is_installed, paths::sidecar_dir};
55+
56+
progress_callback(FfmpegDownloadProgressEvent::Starting);
57+
let download_url = ffmpeg_download_url()?;
58+
let destination = sidecar_dir()?;
59+
let archive_path =
60+
download_ffmpeg_package_with_progress(download_url, &destination, |e| progress_callback(e))?;
61+
progress_callback(FfmpegDownloadProgressEvent::UnpackingArchive);
62+
unpack_ffmpeg(&archive_path, &destination)?;
63+
progress_callback(FfmpegDownloadProgressEvent::Done);
64+
65+
if !ffmpeg_is_installed() {
66+
anyhow::bail!("FFmpeg failed to install, please install manually.");
67+
}
68+
69+
Ok(())
70+
}
71+
72+
#[cfg(not(feature = "download_ffmpeg"))]
73+
fn main() {
74+
eprintln!(r#"This example requires the "download_ffmpeg" feature to be enabled."#);
75+
println!("The feature is included by default unless manually disabled.");
76+
println!("Please run `cargo run --example download_ffmpeg`.");
77+
}

src/download.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,49 @@ pub fn auto_download() -> Result<()> {
7272
Ok(())
7373
}
7474

75+
pub enum FfmpegDownloadProgressEvent {
76+
Starting,
77+
Downloading {
78+
total_bytes: u64,
79+
downloaded_bytes: u64,
80+
},
81+
UnpackingArchive,
82+
Done,
83+
}
84+
85+
/// Check if FFmpeg is installed, and if it's not, download and unpack it.
86+
/// Automatically selects the correct binaries for Windows, Linux, and MacOS.
87+
/// The binaries will be placed in the same directory as the Rust executable.
88+
///
89+
/// Provides progress tracking via callback.
90+
///
91+
/// If FFmpeg is already installed, the method exits early without downloading
92+
/// anything.
93+
#[cfg(feature = "download_ffmpeg")]
94+
pub fn auto_download_with_progress(
95+
progress_callback: impl Fn(FfmpegDownloadProgressEvent),
96+
) -> Result<()> {
97+
use crate::{command::ffmpeg_is_installed, paths::sidecar_dir};
98+
99+
if ffmpeg_is_installed() {
100+
return Ok(());
101+
}
102+
103+
progress_callback(FfmpegDownloadProgressEvent::Starting);
104+
let download_url = ffmpeg_download_url()?;
105+
let destination = sidecar_dir()?;
106+
let archive_path = download_ffmpeg_package_with_progress(download_url, &destination, |e| progress_callback(e))?;
107+
progress_callback(FfmpegDownloadProgressEvent::UnpackingArchive);
108+
unpack_ffmpeg(&archive_path, &destination)?;
109+
progress_callback(FfmpegDownloadProgressEvent::Done);
110+
111+
if !ffmpeg_is_installed() {
112+
anyhow::bail!("FFmpeg failed to install, please install manually.");
113+
}
114+
115+
Ok(())
116+
}
117+
75118
/// Parse the the MacOS version number from a JSON string manifest file.
76119
///
77120
/// Example input: <https://evermeet.cx/ffmpeg/info/ffmpeg/release>
@@ -164,6 +207,70 @@ pub fn download_ffmpeg_package(url: &str, download_dir: &Path) -> Result<PathBuf
164207
Ok(archive_path)
165208
}
166209

210+
/// Make an HTTP request to download an archive from the latest published release online with progress tracking.
211+
#[cfg(feature = "download_ffmpeg")]
212+
pub fn download_ffmpeg_package_with_progress(
213+
url: &str,
214+
download_dir: &Path,
215+
progress_callback: impl Fn(FfmpegDownloadProgressEvent),
216+
) -> Result<PathBuf> {
217+
use anyhow::Context;
218+
use std::{
219+
fs::File,
220+
io::{copy, Read},
221+
path::Path,
222+
};
223+
224+
let filename = Path::new(url)
225+
.file_name()
226+
.context("Failed to get filename")?;
227+
228+
let archive_path = download_dir.join(filename);
229+
230+
let mut response = ureq::get(url).call().context("Failed to download ffmpeg")?;
231+
232+
let total_size = response
233+
.headers()
234+
.get("Content-Length")
235+
.and_then(|s| s.to_str().ok())
236+
.and_then(|s| s.parse::<u64>().ok())
237+
.unwrap_or(0);
238+
239+
let mut file =
240+
File::create(&archive_path).context("Failed to create file for ffmpeg download")?;
241+
242+
// Wrapper to track progress during io::copy
243+
struct ProgressReader<R, F> {
244+
inner: R,
245+
progress_callback: F,
246+
downloaded: u64,
247+
total: u64,
248+
}
249+
250+
impl<R: Read, F: Fn(FfmpegDownloadProgressEvent)> Read for ProgressReader<R, F> {
251+
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
252+
let n = self.inner.read(buf)?;
253+
self.downloaded += n as u64;
254+
(self.progress_callback)(FfmpegDownloadProgressEvent::Downloading {
255+
total_bytes: self.total,
256+
downloaded_bytes: self.downloaded,
257+
});
258+
Ok(n)
259+
}
260+
}
261+
262+
let mut progress_reader = ProgressReader {
263+
inner: response.body_mut().as_reader(),
264+
progress_callback,
265+
downloaded: 0,
266+
total: total_size,
267+
};
268+
269+
copy(&mut progress_reader, &mut file).context("Failed to write ffmpeg download to file")?;
270+
271+
Ok(archive_path)
272+
}
273+
167274
/// After downloading, unpacks the archive to a folder, moves the binaries to
168275
/// their final location, and deletes the archive and temporary folder.
169276
#[cfg(feature = "download_ffmpeg")]

0 commit comments

Comments
 (0)