diff --git a/src/video/ffmpeg.rs b/src/video/ffmpeg.rs index 080af0d..21dccc7 100644 --- a/src/video/ffmpeg.rs +++ b/src/video/ffmpeg.rs @@ -5,13 +5,19 @@ use log::{debug, error, info, trace, warn}; use std::future::Future; use std::io::{Result, Stdout}; use std::process::{Output, Stdio}; +use std::time::Instant; use tokio::process::Command; pub struct Ffmpeg; +pub enum GifType { + Overview, + OverviewVideo { duration: u32 }, +} + impl Ffmpeg { async fn _generate_playlist(&self, input_file: &str, output_file: &str) -> Result { - let ffmpeg_result: Result = tokio::process::Command::new("ffmpeg") + let ffmpeg_result: Result = Command::new("ffmpeg") .arg("-i") .arg(input_file) .arg("-c:v") @@ -41,7 +47,7 @@ impl Ffmpeg { ffmpeg_result.map(|_| output_file.to_string()) } - async fn get_video_duration(&self, input_file: &str) -> Result { + async fn get_video_duration(&self, input_file: &str) -> Result { Command::new("ffprobe") .args(["-i", input_file]) .args(["-show_entries", "format=duration"]) @@ -51,66 +57,101 @@ impl Ffmpeg { .await .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string()) .inspect(|duration| debug!("Found video duration: {:?}", duration)) + .and_then(|duration| { + duration + .parse::() + .map(|duration| duration as u32) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) + }) + .inspect(|duration| debug!("Found video duration: {:?}", duration)) } - pub async fn generate_video_gif( &self, input_file: &str, output_file: &str, - duration_sec: u32, + gif_type: GifType, ) -> Result { info!("Creating gif for: '{}'", input_file); - let temp_dir = tempfile::tempdir()?; - let temp_path = temp_dir - .path() - .to_str() - .expect("Unable to make temp_dir a string"); + match gif_type { + GifType::Overview => { + let temp_dir = tempfile::tempdir()?; + let temp_path = temp_dir + .path() + .to_str() + .expect("Unable to make temp_dir a string"); - match self - .get_video_duration(input_file) - .and_then(|duration| { - debug!("Creating gif frames for '{}'", input_file); + match self + .get_video_duration(input_file) + .and_then(|duration| { + debug!("Creating gif frames for '{}'", input_file); - Command::new("ffmpeg") - .args(["-i", input_file]) - .args(["-vf", &format!("fps=20/{}", duration)]) - .args(["-q:v", "2"]) - .stderr(Stdio::null()) - .arg(format!("{}/frame_%03d.jpg", temp_path)) - .status() - }) - .and_then(|_| { - debug!("Generating palette"); + Command::new("ffmpeg") + .args(["-i", input_file]) + .args(["-vf", &format!("fps=20/{}", duration)]) + .args(["-q:v", "2"]) + .stderr(Stdio::null()) + .arg(format!("{}/frame_%03d.jpg", temp_path)) + .status() + }) + .and_then(|_| { + debug!("Generating palette"); - Command::new("ffmpeg") - .args(["-i", &format!("{}/frame_%03d.jpg", temp_path)]) - .args(["-vf", "palettegen"]) - .arg(format!("{}/palette.png", temp_path)) - .stderr(Stdio::null()) - .status() - }) - .and_then(|_| { - debug!("Creating gif for: '{}'", input_file); - self.create_gif_from_frames(temp_path, output_file) - }) - .await - { - Ok(exit_code) => { - if exit_code == 0 { - info!("Created gif for '{}' -> '{}'", input_file, output_file); - } else { - warn!( - "Failed to create gif for '{}' with exit code: {}", - input_file, exit_code - ); + Command::new("ffmpeg") + .args(["-i", &format!("{}/frame_%03d.jpg", temp_path)]) + .args(["-vf", "palettegen"]) + .arg(format!("{}/palette.png", temp_path)) + .stderr(Stdio::null()) + .status() + }) + .and_then(|_| { + debug!("Creating gif for: '{}'", input_file); + self.create_gif_from_frames(temp_path, output_file) + }) + .await + { + Ok(exit_code) => { + if exit_code == 0 { + info!("Created gif for '{}' -> '{}'", input_file, output_file); + } else { + warn!( + "Failed to create gif for '{}' with exit code: {}", + input_file, exit_code + ); + } + } + Err(e) => { + error!("Error creating gif for '{}': {:?}", input_file, e); + } } } - Err(e) => { - error!("Error creating gif for '{}': {:?}", input_file, e); + GifType::OverviewVideo { duration } => { + let start = Instant::now(); + + match self + .get_video_duration(input_file) + .and_then(|input_duration| { + Command::new("ffmpeg") + .args(["-i", input_file]) + .args([ + "-vf", + // Grab 1 second of frames equally spaced to create a 'duration' second long video scaled to 720px on longest side + &format!( + "select='lt(mod(t,{}),1)',setpts=N/FRAME_RATE/TB,scale='if(gt(iw,ih),720,-2)':'if(gt(ih,iw),720,-2)", + input_duration / duration + ), + ]) + .arg("-an") + .arg(output_file) + .status() + }) + .await + { + Ok(out) => info!("Finished clip '{}' with code {:?} in {:?}", output_file, out.code(), start.elapsed()), + Err(e) => error!("Error creating video overview: {}", e), + } } } - Ok(output_file.to_string()) } @@ -126,10 +167,11 @@ impl Ffmpeg { "[0:v]scale=480:-1:flags=lanczos,crop='min(in_w,in_h)':'min(in_w,in_h)':(in_w-out_w)/2:(in_h-out_h)/2, paletteuse", ]) .args(["-loop", "0"]) // loop forever + .args(["-final_delay", "75"]) .arg(output_file) .stderr(Stdio::null()) .status() - .map_ok(|out| out.code().unwrap_or( -1)) + .map_ok(|out| out.code().unwrap_or(-1)) .inspect_ok(|output| debug!("ffmpeg gif create exit code: {:?}", output)) .await }