pub fn ffprobe(path: impl AsRef<std::path::Path>) -> Result<FfProbe, FfProbeError> {
ffprobe_config(
Config {
count_frames: false,
ffprobe_bin: "ffprobe".into(),
},
path,
)
}
pub fn ffprobe_config(
config: Config,
path: impl AsRef<std::path::Path>,
) -> Result<FfProbe, FfProbeError> {
let path = path.as_ref();
let mut cmd = std::process::Command::new(config.ffprobe_bin);
cmd.args([
"-v",
"quiet",
"-show_format",
"-show_streams",
"-print_format",
"json",
]);
if config.count_frames {
cmd.arg("-count_frames");
}
cmd.arg(path);
let out = cmd.output().map_err(FfProbeError::Io)?;
if !out.status.success() {
return Err(FfProbeError::Status(out));
}
serde_json::from_slice::<FfProbe>(&out.stdout).map_err(FfProbeError::Deserialize)
}
#[derive(Clone, Debug)]
pub struct Config {
count_frames: bool,
ffprobe_bin: std::path::PathBuf,
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
}
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
config: Config {
count_frames: false,
ffprobe_bin: "ffprobe".into(),
},
}
}
pub fn count_frames(mut self, count_frames: bool) -> Self {
self.config.count_frames = count_frames;
self
}
pub fn ffprobe_bin(mut self, ffprobe_bin: impl AsRef<std::path::Path>) -> Self {
self.config.ffprobe_bin = ffprobe_bin.as_ref().to_path_buf();
self
}
pub fn build(self) -> Config {
self.config
}
pub fn run(self, path: impl AsRef<std::path::Path>) -> Result<FfProbe, FfProbeError> {
ffprobe_config(self.config, path)
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum FfProbeError {
Io(std::io::Error),
Status(std::process::Output),
Deserialize(serde_json::Error),
}
impl std::fmt::Display for FfProbeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FfProbeError::Io(e) => e.fmt(f),
FfProbeError::Status(o) => {
write!(
f,
"ffprobe exited with status code {}: {}",
o.status,
String::from_utf8_lossy(&o.stderr)
)
}
FfProbeError::Deserialize(e) => e.fmt(f),
}
}
}
impl std::error::Error for FfProbeError {}
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
pub struct FfProbe {
pub streams: Vec<Stream>,
pub format: Format,
}
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
pub struct Stream {
pub index: i64,
pub codec_name: Option<String>,
pub sample_aspect_ratio: Option<String>,
pub display_aspect_ratio: Option<String>,
pub color_range: Option<String>,
pub color_space: Option<String>,
pub bits_per_raw_sample: Option<String>,
pub channel_layout: Option<String>,
pub max_bit_rate: Option<String>,
pub nb_frames: Option<String>,
pub nb_read_frames: Option<String>,
pub codec_long_name: Option<String>,
pub codec_type: Option<String>,
pub codec_time_base: Option<String>,
pub codec_tag_string: String,
pub codec_tag: String,
pub sample_fmt: Option<String>,
pub sample_rate: Option<String>,
pub channels: Option<i64>,
pub bits_per_sample: Option<i64>,
pub r_frame_rate: String,
pub avg_frame_rate: String,
pub time_base: String,
pub start_pts: Option<i64>,
pub start_time: Option<String>,
pub duration_ts: Option<i64>,
pub duration: Option<String>,
pub bit_rate: Option<String>,
pub disposition: Disposition,
pub tags: Option<StreamTags>,
pub profile: Option<String>,
pub width: Option<i64>,
pub height: Option<i64>,
pub coded_width: Option<i64>,
pub coded_height: Option<i64>,
pub closed_captions: Option<i64>,
pub has_b_frames: Option<i64>,
pub pix_fmt: Option<String>,
pub level: Option<i64>,
pub chroma_location: Option<String>,
pub refs: Option<i64>,
pub is_avc: Option<String>,
pub nal_length: Option<String>,
pub nal_length_size: Option<String>,
pub field_order: Option<String>,
pub id: Option<String>,
#[serde(default)]
pub side_data_list: Vec<SideData>,
}
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
#[allow(clippy::derive_partial_eq_without_eq)]
pub struct SideData {
pub side_data_type: String,
}
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
#[allow(clippy::derive_partial_eq_without_eq)]
pub struct Disposition {
pub default: i64,
pub dub: i64,
pub original: i64,
pub comment: i64,
pub lyrics: i64,
pub karaoke: i64,
pub forced: i64,
pub hearing_impaired: i64,
pub visual_impaired: i64,
pub clean_effects: i64,
pub attached_pic: i64,
pub timed_thumbnails: i64,
}
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
#[allow(clippy::derive_partial_eq_without_eq)]
pub struct StreamTags {
pub language: Option<String>,
pub creation_time: Option<String>,
pub handler_name: Option<String>,
pub encoder: Option<String>,
pub timecode: Option<String>,
pub reel_name: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
pub struct Format {
pub filename: String,
pub nb_streams: i64,
pub nb_programs: i64,
pub format_name: String,
pub format_long_name: String,
pub start_time: Option<String>,
pub duration: Option<String>,
#[serde(default)]
pub size: String,
pub bit_rate: Option<String>,
pub probe_score: i64,
pub tags: Option<FormatTags>,
}
impl Format {
pub fn try_get_duration(
&self,
) -> Option<Result<std::time::Duration, std::num::ParseFloatError>> {
self.duration
.as_ref()
.map(|duration| match duration.parse::<f64>() {
Ok(num) => Ok(std::time::Duration::from_secs_f64(num)),
Err(error) => Err(error),
})
}
pub fn get_duration(&self) -> Option<std::time::Duration> {
self.try_get_duration()?.ok()
}
}
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
#[allow(clippy::derive_partial_eq_without_eq)]
pub struct FormatTags {
#[serde(rename = "WMFSDKNeeded")]
pub wmfsdkneeded: Option<String>,
#[serde(rename = "DeviceConformanceTemplate")]
pub device_conformance_template: Option<String>,
#[serde(rename = "WMFSDKVersion")]
pub wmfsdkversion: Option<String>,
#[serde(rename = "IsVBR")]
pub is_vbr: Option<String>,
pub major_brand: Option<String>,
pub minor_version: Option<String>,
pub compatible_brands: Option<String>,
pub creation_time: Option<String>,
pub encoder: Option<String>,
#[serde(flatten)]
pub extra: std::collections::HashMap<String, serde_json::Value>,
}