Skip to content

Commit

Permalink
ffmpeg executable version check with cache
Browse files Browse the repository at this point in the history
  • Loading branch information
Wumpf committed Nov 12, 2024
1 parent ae25546 commit 10ab7e6
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 41 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6478,6 +6478,7 @@ dependencies = [
name = "re_video"
version = "0.20.0-alpha.4+dev"
dependencies = [
"anyhow",
"bit-vec",
"cfg_aliases 0.2.1",
"criterion",
Expand All @@ -6487,6 +6488,7 @@ dependencies = [
"indicatif",
"itertools 0.13.0",
"js-sys",
"once_cell",
"parking_lot",
"re_build_info",
"re_build_tools",
Expand Down
2 changes: 2 additions & 0 deletions crates/store/re_video/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ re_build_info.workspace = true
re_log.workspace = true
re_tracing.workspace = true

anyhow.workspace = true
bit-vec.workspace = true
crossbeam.workspace = true
econtext.workspace = true
itertools.workspace = true
once_cell.workspace = true
parking_lot.workspace = true
re_mp4.workspace = true
thiserror.workspace = true
Expand Down
70 changes: 40 additions & 30 deletions crates/store/re_video/src/decode/ffmpeg_h264/ffmpeg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,27 @@ use crate::{
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Couldn't find an installation of the FFmpeg executable.")]
FfmpegNotInstalled {
/// Download URL for the latest version of `FFmpeg` on the current platform.
/// None if the platform is not supported.
// TODO(andreas): as of writing, ffmpeg-sidecar doesn't define a download URL for linux arm.
download_url: Option<&'static str>,
},
FfmpegNotInstalled,

#[error("Failed to start FFmpeg: {0}")]
FailedToStartFfmpeg(std::io::Error),

#[error("FFmpeg version is {actual_version}. Only versions >= {minimum_version_major}.{minimum_version_minor} are officially supported.")]
UnsupportedFFmpegVersion {
actual_version: FFmpegVersion,

/// Copy of [`FFMPEG_MINIMUM_VERSION_MAJOR`].
minimum_version_major: u32,

/// Copy of [`FFMPEG_MINIMUM_VERSION_MINOR`].
minimum_version_minor: u32,
},

// TODO(andreas): This error can have a variety of reasons and is as such redundant to some of the others.
// It works with an inner error because some of the error sources are behind an anyhow::Error inside of ffmpeg-sidecar.
#[error("Failed to determine FFmpeg version: {0}")]
FailedToDetermineFFmpegVersion(anyhow::Error),

#[error("Failed to get stdin handle")]
NoStdin,

Expand Down Expand Up @@ -74,21 +85,6 @@ pub enum Error {

#[error("Failed to parse sequence parameter set.")]
SpsParsing,

#[error("FFmpeg version is {actual_version}. Only versions >= {minimum_version_major}.{minimum_version_minor} are officially supported.")]
UnsupportedFFmpegVersion {
actual_version: FFmpegVersion,
/// Download URL for the latest version of `FFmpeg` on the current platform.
/// None if the platform is not supported.
// TODO(andreas): as of writing, ffmpeg-sidecar doesn't define a download URL for linux arm.
download_url: Option<&'static str>,

/// Copy of [`FFMPEG_MINIMUM_VERSION_MAJOR`].
minimum_version_major: u32,

/// Copy of [`FFMPEG_MINIMUM_VERSION_MINOR`].
minimum_version_minor: u32,
},
}

impl From<Error> for crate::decode::Error {
Expand Down Expand Up @@ -167,12 +163,6 @@ impl FfmpegProcessAndListener {
) -> Result<Self, Error> {
re_tracing::profile_function!();

if !ffmpeg_sidecar::command::ffmpeg_is_installed() {
return Err(Error::FfmpegNotInstalled {
download_url: ffmpeg_sidecar::download::ffmpeg_download_url().ok(),
});
}

let sps_result = H264Sps::parse_from_avcc(&avcc);
if let Ok(sps) = &sps_result {
re_log::trace!("Successfully parsed SPS for {debug_name}:\n{sps:?}");
Expand Down Expand Up @@ -596,8 +586,6 @@ fn read_ffmpeg_output(
}

FfmpegEvent::ParsedVersion(ffmpeg_version) => {
re_log::debug_once!("FFmpeg version is {}", ffmpeg_version.version);

fn download_advice() -> String {
if let Ok(download_url) = ffmpeg_sidecar::download::ffmpeg_download_url() {
format!("\nYou can download an up to date version for your system at {download_url}.")
Expand All @@ -612,7 +600,6 @@ fn read_ffmpeg_output(
if !ffmpeg_version.is_compatible() {
(on_output.lock().as_ref()?)(Err(Error::UnsupportedFFmpegVersion {
actual_version: ffmpeg_version,
download_url: ffmpeg_sidecar::download::ffmpeg_download_url().ok(),
minimum_version_major: FFMPEG_MINIMUM_VERSION_MAJOR,
minimum_version_minor: FFMPEG_MINIMUM_VERSION_MINOR,
}
Expand Down Expand Up @@ -666,6 +653,29 @@ impl FfmpegCliH264Decoder {
) -> Result<Self, Error> {
re_tracing::profile_function!();

// TODO(ab): Pass exectuable path here.

Check warning on line 656 in crates/store/re_video/src/decode/ffmpeg_h264/ffmpeg.rs

View workflow job for this annotation

GitHub Actions / Checks / Spell Check

"exectuable" should be "executable".
if !ffmpeg_sidecar::command::ffmpeg_is_installed() {
return Err(Error::FfmpegNotInstalled);
}

// Check the version once ahead of running FFmpeg.
// The error is still handled if it happens while running FFmpeg, but it's a bit unclear if we can get it to start in the first place then.
// TODO(ab): Pass exectuable path here.

Check warning on line 663 in crates/store/re_video/src/decode/ffmpeg_h264/ffmpeg.rs

View workflow job for this annotation

GitHub Actions / Checks / Spell Check

"exectuable" should be "executable".
match FFmpegVersion::for_executable(None) {
Ok(version) => {
if !version.is_compatible() {
return Err(Error::UnsupportedFFmpegVersion {
actual_version: version,
minimum_version_major: FFMPEG_MINIMUM_VERSION_MAJOR,
minimum_version_minor: FFMPEG_MINIMUM_VERSION_MINOR,
});
}
}
Err(err) => {
return Err(Error::FailedToDetermineFFmpegVersion(err));
}
}

let on_output = Arc::new(on_output);
let ffmpeg = FfmpegProcessAndListener::new(&debug_name, on_output.clone(), avcc.clone())?;

Expand Down
7 changes: 7 additions & 0 deletions crates/store/re_video/src/decode/ffmpeg_h264/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ mod version;

pub use ffmpeg::{Error, FfmpegCliH264Decoder};
pub use version::{FFmpegVersion, FFMPEG_MINIMUM_VERSION_MAJOR, FFMPEG_MINIMUM_VERSION_MINOR};

/// Download URL for the latest version of `FFmpeg` on the current platform.
/// None if the platform is not supported.
// TODO(andreas): as of writing, ffmpeg-sidecar doesn't define a download URL for linux arm.
pub fn ffmpeg_download_url() -> Option<&'static str> {
ffmpeg_sidecar::download::ffmpeg_download_url().ok()
}
55 changes: 53 additions & 2 deletions crates/store/re_video/src/decode/ffmpeg_h264/version.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use std::{collections::HashMap, path::PathBuf};

use once_cell::sync::Lazy;
use parking_lot::Mutex;

// FFmpeg 5.1 "Riemann" is from 2022-07-22.
// It's simply the oldest I tested manually as of writing. We might be able to go lower.
// However, we also know that FFmpeg 4.4 is already no longer working.
pub const FFMPEG_MINIMUM_VERSION_MAJOR: u32 = 5;
pub const FFMPEG_MINIMUM_VERSION_MINOR: u32 = 1;

/// A successfully parsed FFmpeg version.
#[derive(Debug, PartialEq, Eq)]
/// A successfully parsed `FFmpeg` version.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FFmpegVersion {
major: u32,
minor: u32,
Expand Down Expand Up @@ -49,6 +54,52 @@ impl FFmpegVersion {
})
}

/// Try to parse the `FFmpeg` version for a given `FFmpeg` executable.
///
/// If none is passed for the path, it uses `ffmpeg` from PATH.
///
/// Internally caches the result per path together with its modification time to re-run/parse the version only if the file has changed.
pub fn for_executable(path: Option<&std::path::Path>) -> anyhow::Result<Self> {
type VersionMap = HashMap<PathBuf, (Option<std::time::SystemTime>, FFmpegVersion)>;
static CACHE: Lazy<Mutex<VersionMap>> = Lazy::new(|| Mutex::new(HashMap::new()));

re_tracing::profile_function!();

// Retrieve file modification time first.
let modification_time = if let Some(path) = path {
path.metadata()
.map_err(|err| anyhow::anyhow!("Failed to read file: {err}"))?
.modified()
.ok()
} else {
None
};

// Check first if we already have the version cached.
let mut cache = CACHE.lock();
let cache_key = path.unwrap_or(&std::path::Path::new("ffmpeg"));
if let Some(cached) = cache.get(cache_key) {
if modification_time == cached.0 {
return Ok(cached.1.clone());
}
}

// Run FFmpeg (or whatever was passed to us) to get the version.
let raw_version = if let Some(path) = path {
ffmpeg_sidecar::version::ffmpeg_version_with_path(path)
} else {
ffmpeg_sidecar::version::ffmpeg_version()
}?;
let version = Self::parse(&raw_version)
.ok_or_else(|| anyhow::anyhow!("Failed to parse FFmpeg version: {raw_version}"))?;
cache.insert(
cache_key.to_path_buf(),
(modification_time, version.clone()),
);

Ok(version)
}

/// Returns true if this version is compatible with Rerun's minimum requirements.
pub fn is_compatible(&self) -> bool {
self.major > FFMPEG_MINIMUM_VERSION_MAJOR
Expand Down
2 changes: 1 addition & 1 deletion crates/store/re_video/src/decode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ mod av1;
mod ffmpeg_h264;

#[cfg(with_ffmpeg)]
pub use ffmpeg_h264::Error as FfmpegError;
pub use ffmpeg_h264::{ffmpeg_download_url, Error as FfmpegError};

#[cfg(target_arch = "wasm32")]
mod webcodecs;
Expand Down
14 changes: 6 additions & 8 deletions crates/viewer/re_data_ui/src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,12 @@ pub fn show_decoded_frame_info(
) = &err
{
match err.as_ref() {
re_video::decode::FfmpegError::UnsupportedFFmpegVersion {
download_url: Some(url),
..
}
| re_video::decode::FfmpegError::FfmpegNotInstalled {
download_url: Some(url),
} => {
ui.markdown_ui(&format!("You can download a build of `FFmpeg` [here]({url}). For Rerun to be able to use it, its binaries need to be reachable from `PATH`."));
re_video::decode::FfmpegError::UnsupportedFFmpegVersion { .. }
| re_video::decode::FfmpegError::FailedToDetermineFFmpegVersion(_)
| re_video::decode::FfmpegError::FfmpegNotInstalled => {
if let Some(download_url) = re_video::decode::ffmpeg_download_url() {
ui.markdown_ui(&format!("You can download a build of `FFmpeg` [here]({download_url}). For Rerun to be able to use it, its binaries need to be reachable from `PATH`."));
}
}

_ => {}
Expand Down

0 comments on commit 10ab7e6

Please sign in to comment.