use ffmpeg_common::{
CommandBuilder, Error, LogLevel, MediaPath, Process, ProcessConfig, Result, StreamSpecifier,
};
use std::path::PathBuf;
use std::time::Duration;
use tracing::info;
use crate::format::OutputFormat;
use crate::parsers::{parse_output, ProbeResult};
use crate::types::{ProbeSection, ReadInterval};
#[derive(Debug, Clone)]
pub struct FFprobeBuilder {
executable: PathBuf,
input: Option<MediaPath>,
output_format: OutputFormat,
show_sections: Vec<ProbeSection>,
show_entries: Option<String>,
select_streams: Option<StreamSpecifier>,
show_data: bool,
show_data_hash: Option<String>,
count_frames: bool,
count_packets: bool,
read_intervals: Vec<ReadInterval>,
show_private_data: bool,
log_level: Option<LogLevel>,
pretty: bool,
unit: bool,
prefix: bool,
byte_binary_prefix: bool,
sexagesimal: bool,
options: Vec<(String, String)>,
timeout: Option<Duration>,
}
impl FFprobeBuilder {
pub fn new() -> Result<Self> {
let executable = ffmpeg_common::process::find_executable("ffprobe")?;
Ok(Self {
executable,
input: None,
output_format: OutputFormat::Json,
show_sections: Vec::new(),
show_entries: None,
select_streams: None,
show_data: false,
show_data_hash: None,
count_frames: false,
count_packets: false,
read_intervals: Vec::new(),
show_private_data: true,
log_level: None,
pretty: false,
unit: false,
prefix: false,
byte_binary_prefix: false,
sexagesimal: false,
options: Vec::new(),
timeout: None,
})
}
pub fn with_executable(path: impl Into<PathBuf>) -> Self {
Self {
executable: path.into(),
input: None,
output_format: OutputFormat::Json,
show_sections: Vec::new(),
show_entries: None,
select_streams: None,
show_data: false,
show_data_hash: None,
count_frames: false,
count_packets: false,
read_intervals: Vec::new(),
show_private_data: true,
log_level: None,
pretty: false,
unit: false,
prefix: false,
byte_binary_prefix: false,
sexagesimal: false,
options: Vec::new(),
timeout: None,
}
}
pub fn input(mut self, input: impl Into<MediaPath>) -> Self {
self.input = Some(input.into());
self
}
pub fn output_format(mut self, format: OutputFormat) -> Self {
self.output_format = format;
self
}
pub fn show_format(mut self) -> Self {
if !self.show_sections.contains(&ProbeSection::Format) {
self.show_sections.push(ProbeSection::Format);
}
self
}
pub fn show_streams(mut self) -> Self {
if !self.show_sections.contains(&ProbeSection::Streams) {
self.show_sections.push(ProbeSection::Streams);
}
self
}
pub fn show_packets(mut self) -> Self {
if !self.show_sections.contains(&ProbeSection::Packets) {
self.show_sections.push(ProbeSection::Packets);
}
self
}
pub fn show_frames(mut self) -> Self {
if !self.show_sections.contains(&ProbeSection::Frames) {
self.show_sections.push(ProbeSection::Frames);
}
self
}
pub fn show_programs(mut self) -> Self {
if !self.show_sections.contains(&ProbeSection::Programs) {
self.show_sections.push(ProbeSection::Programs);
}
self
}
pub fn show_chapters(mut self) -> Self {
if !self.show_sections.contains(&ProbeSection::Chapters) {
self.show_sections.push(ProbeSection::Chapters);
}
self
}
pub fn show_error(mut self) -> Self {
if !self.show_sections.contains(&ProbeSection::Error) {
self.show_sections.push(ProbeSection::Error);
}
self
}
pub fn show_entries(mut self, entries: impl Into<String>) -> Self {
self.show_entries = Some(entries.into());
self
}
pub fn select_streams(mut self, spec: StreamSpecifier) -> Self {
self.select_streams = Some(spec);
self
}
pub fn show_data(mut self, enable: bool) -> Self {
self.show_data = enable;
self
}
pub fn show_data_hash(mut self, algorithm: impl Into<String>) -> Self {
self.show_data_hash = Some(algorithm.into());
self
}
pub fn count_frames(mut self, enable: bool) -> Self {
self.count_frames = enable;
self
}
pub fn count_packets(mut self, enable: bool) -> Self {
self.count_packets = enable;
self
}
pub fn read_interval(mut self, interval: ReadInterval) -> Self {
self.read_intervals.push(interval);
self
}
pub fn show_private_data(mut self, enable: bool) -> Self {
self.show_private_data = enable;
self
}
pub fn log_level(mut self, level: LogLevel) -> Self {
self.log_level = Some(level);
self
}
pub fn pretty(mut self, enable: bool) -> Self {
self.pretty = enable;
self
}
pub fn unit(mut self, enable: bool) -> Self {
self.unit = enable;
self
}
pub fn prefix(mut self, enable: bool) -> Self {
self.prefix = enable;
self
}
pub fn byte_binary_prefix(mut self, enable: bool) -> Self {
self.byte_binary_prefix = enable;
self
}
pub fn sexagesimal(mut self, enable: bool) -> Self {
self.sexagesimal = enable;
self
}
pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.options.push((key.into(), value.into()));
self
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = Some(duration);
self
}
fn validate(&self) -> Result<()> {
if self.input.is_none() {
return Err(Error::InvalidArgument("No input specified".to_string()));
}
Ok(())
}
pub fn build_args(&self) -> Result<Vec<String>> {
self.validate()?;
let mut cmd = CommandBuilder::new();
if let Some(level) = self.log_level {
cmd = cmd.option("-loglevel", level.as_str());
}
cmd = cmd.option("-print_format", self.output_format.as_str());
for section in &self.show_sections {
cmd = cmd.flag(format!("-show_{}", section.as_str()));
}
if let Some(ref entries) = self.show_entries {
cmd = cmd.option("-show_entries", entries);
}
if let Some(ref spec) = self.select_streams {
cmd = cmd.option("-select_streams", spec.to_string());
}
if self.show_data {
cmd = cmd.flag("-show_data");
}
if let Some(ref hash) = self.show_data_hash {
cmd = cmd.option("-show_data_hash", hash);
}
if self.count_frames {
cmd = cmd.flag("-count_frames");
}
if self.count_packets {
cmd = cmd.flag("-count_packets");
}
if !self.read_intervals.is_empty() {
let intervals = self
.read_intervals
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",");
cmd = cmd.option("-read_intervals", intervals);
}
if !self.show_private_data {
cmd = cmd.flag("-noprivate");
}
if self.pretty {
cmd = cmd.flag("-pretty");
} else {
if self.unit {
cmd = cmd.flag("-unit");
}
if self.prefix {
cmd = cmd.flag("-prefix");
}
if self.byte_binary_prefix {
cmd = cmd.flag("-byte_binary_prefix");
}
if self.sexagesimal {
cmd = cmd.flag("-sexagesimal");
}
}
for (key, value) in &self.options {
cmd = cmd.option(key, value);
}
if let Some(ref input) = self.input {
cmd = cmd.arg(input.as_str());
}
Ok(cmd.build())
}
pub async fn run(self) -> Result<ProbeResult> {
let args = self.build_args()?;
info!("Running FFprobe with args: {:?}", args);
let mut config = ProcessConfig::new(&self.executable)
.capture_stdout(true)
.capture_stderr(true);
if let Some(timeout) = self.timeout {
config = config.timeout(timeout);
}
let output = Process::spawn(config, args).await?.wait().await?;
if !output.success() {
return Err(Error::process_failed(
"FFprobe failed",
Some(output.status),
output.stderr_str(),
));
}
let stdout = output
.stdout_str()
.ok_or_else(|| Error::InvalidOutput("No output from ffprobe".to_string()))?;
parse_output(&stdout, self.output_format)
}
pub fn command(&self) -> Result<String> {
let args = self.build_args()?;
Ok(format!(
"{} {}",
self.executable.display(),
args.join(" ")
))
}
}
impl Default for FFprobeBuilder {
fn default() -> Self {
Self::new().expect("FFprobe executable not found")
}
}
impl FFprobeBuilder {
pub fn probe(input: impl Into<MediaPath>) -> Self {
Self::new()
.unwrap()
.input(input)
.show_format()
.show_streams()
}
pub fn probe_format(input: impl Into<MediaPath>) -> Self {
Self::new()
.unwrap()
.input(input)
.show_format()
}
pub fn probe_streams(input: impl Into<MediaPath>) -> Self {
Self::new()
.unwrap()
.input(input)
.show_streams()
}
pub fn probe_detailed(input: impl Into<MediaPath>) -> Self {
Self::new()
.unwrap()
.input(input)
.show_format()
.show_streams()
.show_chapters()
.show_programs()
.count_frames(true)
.count_packets(true)
}
pub fn probe_stream(input: impl Into<MediaPath>, stream: StreamSpecifier) -> Self {
Self::new()
.unwrap()
.input(input)
.show_streams()
.select_streams(stream)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ffmpeg_common::StreamType;
#[test]
fn test_basic_probe() {
let builder = FFprobeBuilder::probe("input.mp4");
let args = builder.build_args().unwrap();
assert!(args.contains(&"-print_format".to_string()));
assert!(args.contains(&"json".to_string()));
assert!(args.contains(&"-show_format".to_string()));
assert!(args.contains(&"-show_streams".to_string()));
assert!(args.contains(&"input.mp4".to_string()));
}
#[test]
fn test_output_format() {
let builder = FFprobeBuilder::new()
.unwrap()
.input("input.mp4")
.output_format(OutputFormat::Xml)
.show_format();
let args = builder.build_args().unwrap();
assert!(args.contains(&"xml".to_string()));
}
#[test]
fn test_stream_selection() {
let builder = FFprobeBuilder::probe_stream(
"input.mp4",
StreamSpecifier::Type(StreamType::Audio),
);
let args = builder.build_args().unwrap();
assert!(args.contains(&"-select_streams".to_string()));
assert!(args.contains(&"a".to_string()));
}
#[test]
fn test_display_options() {
let builder = FFprobeBuilder::new()
.unwrap()
.input("input.mp4")
.pretty(true);
let args = builder.build_args().unwrap();
assert!(args.contains(&"-pretty".to_string()));
}
}