use std::time::Duration;
use crate::cli::{Label, LabelArgs};
use crate::ffmpeg::{self, LabelConfig};
use crate::{output, terminal};
pub fn run(args: LabelArgs) -> anyhow::Result<()> {
if !args.source.exists() {
anyhow::bail!("Source file not found: {}", args.source.display());
}
if !args.source.is_file() {
anyhow::bail!("Source is not a file: {}", args.source.display());
}
let output_path = output::resolve_label_output_path(&args.source, args.output.as_deref())?;
let config = LabelConfig::new(&args.source, args.labels, &output_path).with_font(args.font);
let ffmpeg_run = ffmpeg::run_to_completion(ffmpeg::build_label_command(&config), args.verbose);
let (status, stderr) = match ffmpeg_run {
Ok(run) => run,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
anyhow::bail!("ffmpeg not found. Install it with: brew install ffmpeg");
}
Err(e) => {
output::remove_partial_output(&output_path);
return Err(e.into());
}
};
if !status.success() {
output::remove_partial_output(&output_path);
if let Some(diagnosis) = diagnose_drawtext_failure(&stderr) {
anyhow::bail!("{}", diagnosis);
}
if args.verbose {
anyhow::bail!("ffmpeg exited with {}", status);
}
anyhow::bail!("ffmpeg failed:\n{}", stderr);
}
if let Some(written) = ffmpeg::parse_written_length(&stderr) {
warn_about_unreachable_labels(&config.labels, written);
}
terminal::print_label_saved(&output_path);
Ok(())
}
fn warn_about_unreachable_labels(labels: &[Label], written: Duration) {
for (number, label) in labels.iter().enumerate() {
if label.start >= written {
terminal::print_warning(&format!(
"label {} ('{}') starts at {}ms, past the end of the source video \
({}ms), so it never appears",
number + 1,
label.text,
label.start.as_millis(),
written.as_millis()
));
}
}
}
fn diagnose_drawtext_failure(stderr: &str) -> Option<String> {
if stderr.contains("No such filter: 'drawtext'") {
return Some(String::from(
"This ffmpeg was built without the drawtext filter, which labels need.\n\
Install Homebrew's full build with libfreetype and put it first on PATH:\n\
brew install ffmpeg-full\n\
export PATH=\"$(brew --prefix ffmpeg-full)/bin:$PATH\"",
));
}
if stderr.contains("Cannot find a valid font")
|| stderr.contains("No font filename provided")
|| stderr.contains("Font not found")
{
return Some(String::from(
"ffmpeg could not find a font to draw labels with.\n\
Point it at one explicitly, e.g.:\n\
--font /System/Library/Fonts/Helvetica.ttc",
));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagnose_drawtext_failure_explains_a_missing_filter() {
let diagnosis =
diagnose_drawtext_failure("[AVFilterGraph] No such filter: 'drawtext'").unwrap();
assert!(
diagnosis.contains("libfreetype"),
"should name what the ffmpeg build is missing, got: {}",
diagnosis
);
assert!(diagnosis.contains("brew install ffmpeg-full"));
assert!(diagnosis.contains("brew --prefix ffmpeg-full"));
}
#[test]
fn diagnose_drawtext_failure_explains_a_missing_font() {
let diagnosis = diagnose_drawtext_failure(
"[Parsed_drawtext_0] Cannot find a valid font for the family Sans",
)
.unwrap();
assert!(
diagnosis.contains("--font"),
"should point at the --font flag, got: {}",
diagnosis
);
}
#[test]
fn diagnose_drawtext_failure_passes_other_errors_through() {
assert_eq!(
diagnose_drawtext_failure("Invalid data found when processing input"),
None
);
}
}