Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanbabcock committed Feb 14, 2024
2 parents ac374d5 + c4802b7 commit 4c94cbc
Show file tree
Hide file tree
Showing 14 changed files with 98 additions and 191 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ license = "MIT"
crate-type = ["lib"]

[dependencies]
# there are none!
anyhow = "1.0.79"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
## Features

-Zero dependencies
-Minimal dependencies
- ⚡ Automatic FFmpeg CLI download (if needed)
- 🤗 Support for Windows, MacOS, and Linux
- 🧪 Thoroughly unit tested
Expand Down
3 changes: 1 addition & 2 deletions examples/download_ffmpeg.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use ffmpeg_sidecar::{
command::ffmpeg_is_installed,
download::{check_latest_version, download_ffmpeg_package, ffmpeg_download_url, unpack_ffmpeg},
error::Result,
paths::sidecar_dir,
version::ffmpeg_version,
};

fn main() -> Result<()> {
fn main() -> anyhow::Result<()> {
if ffmpeg_is_installed() {
println!("FFmpeg is already installed! 🎉");
println!("For demo purposes, we'll re-download and unpack it anyway.");
Expand Down
4 changes: 2 additions & 2 deletions examples/hello_world.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use ffmpeg_sidecar::{command::FfmpegCommand, error::Result, event::FfmpegEvent};
use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};

/// Iterates over the frames of a `testsrc`.
///
/// ```console
/// cargo run --example hello_world
/// ```
fn main() -> Result<()> {
fn main() -> anyhow::Result<()> {
FfmpegCommand::new() // <- Builder API like `std::process::Command`
.testsrc() // <- Discoverable aliases for FFmpeg args
.rawvideo() // <- Convenient argument presets
Expand Down
11 changes: 6 additions & 5 deletions src/child.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use std::{
process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus},
};

use crate::error::{Error, Result};
use anyhow::Context;

use crate::iter::FfmpegIterator;

/// A wrapper around [`std::process::Child`] containing a spawned FFmpeg command.
Expand All @@ -22,7 +23,7 @@ impl FfmpegChild {
/// - Progress updates
/// - Errors and warnings
/// - Raw output frames
pub fn iter(&mut self) -> Result<FfmpegIterator> {
pub fn iter(&mut self) -> anyhow::Result<FfmpegIterator> {
FfmpegIterator::new(self)
}

Expand Down Expand Up @@ -66,12 +67,12 @@ impl FfmpegChild {
/// q quit
/// s Show QP histogram
/// ```
pub fn send_stdin_command(&mut self, command: &[u8]) -> Result<()> {
pub fn send_stdin_command(&mut self, command: &[u8]) -> anyhow::Result<()> {
let mut stdin = self
.inner
.stdin
.take()
.ok_or_else(|| Error::msg("Missing child stdin"))?;
.context("Missing child stdin")?;
stdin.write_all(command)?;
self.inner.stdin.replace(stdin);
Ok(())
Expand All @@ -83,7 +84,7 @@ impl FfmpegChild {
/// This method returns after the command has been sent; the actual shut down
/// may take a few more frames as ffmpeg flushes its buffers and writes the
/// trailer, if applicable.
pub fn quit(&mut self) -> Result<()> {
pub fn quit(&mut self) -> anyhow::Result<()> {
self.send_stdin_command(b"q")
}

Expand Down
85 changes: 45 additions & 40 deletions src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@ use std::{
process::{Command, ExitStatus, Stdio},
};

use crate::{
command::ffmpeg_is_installed,
error::{Error, Result},
paths::sidecar_dir,
};
use anyhow::Context;

use crate::{command::ffmpeg_is_installed, paths::sidecar_dir};

pub const UNPACK_DIRNAME: &str = "ffmpeg_release_temp";

/// URL of a manifest file containing the latest published build of FFmpeg. The
/// correct URL for the target platform is baked in at compile time.
pub fn ffmpeg_manifest_url() -> Result<&'static str> {
pub fn ffmpeg_manifest_url() -> anyhow::Result<&'static str> {
if cfg!(not(target_arch = "x86_64")) {
return Err(Error::msg(
"Downloads must be manually provided for non-x86_64 architectures",
));
anyhow::bail!("Downloads must be manually provided for non-x86_64 architectures");
}

if cfg!(target_os = "windows") {
Expand All @@ -29,13 +25,13 @@ pub fn ffmpeg_manifest_url() -> Result<&'static str> {
} else if cfg!(target_os = "linux") {
Ok("https://johnvansickle.com/ffmpeg/release-readme.txt")
} else {
Err(Error::msg("Unsupported platform"))
anyhow::bail!("Unsupported platform")
}
}

/// URL for the latest published FFmpeg release. The correct URL for the target
/// platform is baked in at compile time.
pub fn ffmpeg_download_url() -> Result<&'static str> {
pub fn ffmpeg_download_url() -> anyhow::Result<&'static str> {
if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
Ok("https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip")
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
Expand All @@ -45,9 +41,7 @@ pub fn ffmpeg_download_url() -> Result<&'static str> {
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
Ok("https://www.osxexperts.net/ffmpeg6arm.zip") // Mac M1
} else {
Err(Error::msg(
"Unsupported platform; you can provide your own URL instead and call download_ffmpeg_package directly.",
))
anyhow::bail!("Unsupported platform; you can provide your own URL instead and call download_ffmpeg_package directly.")
}
}

Expand All @@ -57,7 +51,7 @@ pub fn ffmpeg_download_url() -> Result<&'static str> {
///
/// If FFmpeg is already installed, the method exits early without downloading
/// anything.
pub fn auto_download() -> Result<()> {
pub fn auto_download() -> anyhow::Result<()> {
if ffmpeg_is_installed() {
return Ok(());
}
Expand All @@ -67,12 +61,11 @@ pub fn auto_download() -> Result<()> {
let archive_path = download_ffmpeg_package(download_url, &destination)?;
unpack_ffmpeg(&archive_path, &destination)?;

match ffmpeg_is_installed() {
false => Err(Error::msg(
"FFmpeg failed to install, please install manually.",
)),
true => Ok(()),
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.
Expand Down Expand Up @@ -115,7 +108,7 @@ pub fn parse_linux_version(version: &str) -> Option<String> {
}

/// Invoke cURL on the command line to download a file, returning it as a string.
pub fn curl(url: &str) -> Result<String> {
pub fn curl(url: &str) -> anyhow::Result<String> {
let mut child = Command::new("curl")
.args(["-L", url])
.stderr(Stdio::null())
Expand All @@ -125,61 +118,63 @@ pub fn curl(url: &str) -> Result<String> {
let stdout = child
.stdout
.take()
.ok_or_else(|| Error::msg("Failed to get stdout"))?;
.context("Failed to get stdout")?;

let mut string = String::new();
std::io::BufReader::new(stdout).read_to_string(&mut string)?;
Ok(string)
}

/// Invoke cURL on the command line to download a file, writing to a file.
pub fn curl_to_file(url: &str, destination: &str) -> Result<ExitStatus> {
pub fn curl_to_file(url: &str, destination: &str) -> anyhow::Result<ExitStatus> {
Command::new("curl")
.args(["-L", url])
.args(["-o", destination])
.status()
.map_err(Error::from)
.map_err(Into::into)
}

/// Makes an HTTP request to obtain the latest version available online,
/// automatically choosing the correct URL for the current platform.
pub fn check_latest_version() -> Result<String> {
pub fn check_latest_version() -> anyhow::Result<String> {
let string = curl(ffmpeg_manifest_url()?)?;

if cfg!(target_os = "windows") {
Ok(string)
} else if cfg!(target_os = "macos") {
Ok(parse_macos_version(&string).ok_or("failed to parse version number (macos variant)")?)
parse_macos_version(&string).context("failed to parse version number (macos variant)")
} else if cfg!(target_os = "linux") {
Ok(parse_linux_version(&string).ok_or("failed to parse version number (linux variant)")?)
parse_linux_version(&string).context("failed to parse version number (linux variant)")
} else {
Err(Error::msg("Unsupported platform"))
Err(anyhow::Error::msg("Unsupported platform"))
}
}

/// Invoke `curl` to download an archive (ZIP on windows, TAR on linux and mac)
/// from the latest published release online.
pub fn download_ffmpeg_package(url: &str, download_dir: &Path) -> Result<PathBuf> {
pub fn download_ffmpeg_package(url: &str, download_dir: &Path) -> anyhow::Result<PathBuf> {
let filename = Path::new(url)
.file_name()
.ok_or_else(|| Error::msg("Failed to get filename"))?;
.context("Failed to get filename")?;

let archive_path = download_dir.join(filename);

let archive_filename = archive_path.to_str().ok_or("invalid download path")?;
let archive_filename = archive_path
.to_str()
.context("invalid download path")?;

let exit_status = curl_to_file(url, archive_filename)?;

if !exit_status.success() {
return Err(Error::msg("Failed to download ffmpeg"));
anyhow::bail!("Failed to download ffmpeg");
}

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.
pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()> {
pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> anyhow::Result<()> {
let temp_dirname = UNPACK_DIRNAME;
let temp_folder = binary_folder.join(temp_dirname);
create_dir_all(&temp_folder)?;
Expand All @@ -192,13 +187,13 @@ pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()>
.status()?
.success()
.then_some(())
.ok_or("Failed to unpack ffmpeg")?;
.context("Failed to unpack ffmpeg")?;

// Move binaries
let (ffmpeg, ffplay, ffprobe) = if cfg!(target_os = "windows") {
let inner_folder = read_dir(&temp_folder)?
.next()
.ok_or("Failed to get inner folder")??;
.context("Failed to get inner folder")??;
(
inner_folder.path().join("bin/ffmpeg.exe"),
inner_folder.path().join("bin/ffplay.exe"),
Expand All @@ -207,7 +202,7 @@ pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()>
} else if cfg!(target_os = "linux") {
let inner_folder = read_dir(&temp_folder)?
.next()
.ok_or("Failed to get inner folder")??;
.context("Failed to get inner folder")??;
(
inner_folder.path().join("./ffmpeg"),
inner_folder.path().join("./ffplay"), // <- no ffplay on linux
Expand All @@ -220,18 +215,28 @@ pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()>
temp_folder.join("ffprobe"), // <-- no ffprobe on mac
)
} else {
return Err(Error::msg("Unsupported platform"));
anyhow::bail!("Unsupported platform");
};

// Move binaries
rename(&ffmpeg, binary_folder.join(ffmpeg.file_name().ok_or(())?))?;
let move_bin = |path: &Path| {
let file_name = binary_folder.join(
path
.file_name()
.with_context(|| format!("Path {} does not have a file_name", path.to_string_lossy()))?,
);
rename(path, file_name)?;
anyhow::Ok(())
};

move_bin(&ffmpeg)?;

if ffprobe.exists() {
rename(&ffprobe, binary_folder.join(ffprobe.file_name().ok_or(())?))?;
move_bin(&ffprobe)?;
}

if ffplay.exists() {
rename(&ffplay, binary_folder.join(ffplay.file_name().ok_or(())?))?;
move_bin(&ffplay)?;
}

// Delete archive and unpacked files
Expand Down
Loading

0 comments on commit 4c94cbc

Please sign in to comment.