diff --git a/src-lib/Cargo.toml b/src-lib/Cargo.toml index fb430d0..2710f66 100644 --- a/src-lib/Cargo.toml +++ b/src-lib/Cargo.toml @@ -6,3 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + +[lib] +name = "lib" +path = "src/lib.rs" + +[[bin]] +name = "cli" +path = "src/cli/main.rs" diff --git a/src-lib/_all b/src-lib/_all deleted file mode 100644 index f06a36e..0000000 --- a/src-lib/_all +++ /dev/null @@ -1,375 +0,0 @@ -use std::str::FromStr; -use core::cmp::Ordering; -use std::fs; -use std::path::Path; -use std::io::Write; - -pub fn screenshotify(input: &String) -> Result, String> { - let output_d = format!("{}.d", input); - let ext = "jpg".to_string(); - let mut result = vec![]; - let _ = ify(input, |content_span| { - let output = format!("{}/{}.{}", output_d, result.len(), ext); - let _ = screenshot(&output, input, (content_span.stop + content_span.start) / 2.0)?; - result.push(output); - Ok(()) - })?; - Ok(result) -} - -pub fn clipify(input: &String) -> Result, String> { - let output_d = format!("{}.d", input); - let ext = input.split(".").last().unwrap(); - let mut result = vec![]; - let _ = ify(input, |content_span| { - let output = format!("{}/{}.{}", output_d, result.len(), ext); - let _ = clip(&output, input, content_span)?; - result.push(output); - Ok(()) - })?; - Ok(result) -} - -fn ify(input: &String, mut cb: impl FnMut(ContentSpan) -> Result<(), String>) -> Result<(), String> { - match inspect(input) { - Ok(inspection) => { - match inspection.iter() - .map(|x| cb(*x)) - .filter(|x| x.is_err()) - .map(|x| x.err()) - .filter(|x| x.is_some()) - .map(|x| x.unwrap()) - .nth(0) { - Some(err) => Err(format!("callback failed: {}", err)), - None => Ok(()), - } - }, - Err(msg) => Err(msg), - } -} - -pub fn clip(output: &String, input: &String, content_span: ContentSpan) -> Result<(), String> { - fs::create_dir_all(Path::new(output).parent().unwrap()).unwrap(); - match std::process::Command::new("ffmpeg") - .args([ - "-y", - "-ss", &content_span.start.to_string(), - "-i", input, - "-t", &(content_span.stop - content_span.start).to_string(), - output, - ]) - .output() { - Ok(output) => match output.status.success() { - true => Ok(()), - false => Err(format!("failed to ffmpeg clip {}: {}", input, String::from_utf8(output.stderr).unwrap())), - }, - Err(msg) => Err(format!("failed to ffmpeg clip {}: {}", input, msg)), - } -} - -pub fn screenshot(output: &String, input: &String, ts: f32) -> Result<(), String> { - match screenshot_jpg(input, ts) { - Ok(jpg) => { - match std::fs::File::create(output) { - Ok(mut f) => { - f.write_all(jpg).unwrap(); - Ok(()) - }, - Err(msg) => Err(format!("failed to open {} for writing: {}", output, msg)), - } - }, - Err(msg) => Err(msg), - } -} - -pub fn screenshot_jpg(input: &String, ts: f32) -> Result, String> { - match std::process::Command::new("ffmpeg") - .args([ - "-y", - "-ss", &ts.to_string(), - "-i", input, - "-frames:v", "1", - "-q:v", "2", - "-f", "jpg", - "-", - ]) - .output() { - Ok(output) => Ok(output.stdout), - Err(msg) => Err(format!("failed to ffmpeg screenshot {}: {}", input, msg)), - } -} - -pub fn inspect(file: &String) -> Result, String> { - match _inspect(file) { - Ok(inspection) => Ok(inspection.content_spans()), - Err(msg) => Err(msg) - } -} - -fn _inspect(file: &String) -> Result { - match std::process::Command::new("ffmpeg") - .args([ - "-i", file, - "-vf", "mpdecimate", - "-af", "silencedetect=n=-50dB:d=1", - "-loglevel", "debug", - "-f", "null", - "-", - ]) - .output() { - Ok(output) => { - let stderr = String::from_utf8(output.stderr).unwrap(); - let stdout = String::from_utf8(output.stdout).unwrap(); - let line_iter = stderr.split("\n").chain(stdout.split("\n")); - Ok(Inspection{ - lines: line_iter.map(|x| x.to_string()).collect(), - }) - }, - Err(msg) => Err(format!("failed to ffmpeg inspect {}: {}", file, msg)), - } -} - -struct Inspection { - lines: Vec, -} - -impl Inspection { - fn content_spans(&self) -> Vec { - self.overlap_spans( - &self.visual_spans(), - &self.audible_spans(), - ) - } - - fn overlap_spans(&self, a: &Vec, b: &Vec) -> Vec { - self.merge_unsorted_spans(a.iter().chain(b.iter()).map(|x| *x).collect()) - } - - fn audible_spans(&self) -> Vec { - self.spans_from_transitions(self.audible_transitions()) - } - - fn visual_spans(&self) -> Vec { - self.spans_from_transitions(self.visual_transitions()) - } - - fn visual_transitions(&self) -> Vec { - let lines: Vec<_> = self.lines.iter() - .filter(|x| x.contains("Parsed_mpdecimate_0")) - .filter(|x| x.contains("keep pts:") || x.contains("drop pts:")) - .collect(); - let mut lines_with_transitions = vec![]; - for i in 0..lines.len() { - if lines_with_transitions.len() == 0 { - lines_with_transitions.push(lines[i]); - } else { - let this_is_keep = lines[i].contains("keep"); - let prev_was_keep = lines_with_transitions.last().unwrap().contains("keep"); - if this_is_keep != prev_was_keep { - lines_with_transitions.push(lines[i-1]); - lines_with_transitions.push(lines[i]); - } - } - } - lines_with_transitions.iter() - .skip(2) - .filter(|x| x.contains("keep")) - .map(|x| x.split("pts_time:").nth(1).unwrap().split(" ").nth(0).unwrap()) - .map(|x| f32::from_str(x).unwrap()) - .collect() - } - - fn audible_transitions(&self) -> Vec { - let lines: Vec<_> = self.lines.iter() - .filter(|x| x.contains("[silencedetect @")) - .map(|x| x.split(" ").nth(4).unwrap()) - .map(|x| f32::from_str(x).unwrap()) - .collect(); - let mut lines_with_transitions = vec![]; - lines_with_transitions.push(0.0); - lines.iter().for_each(|x| lines_with_transitions.push(*x)); - lines_with_transitions.push(self.duration()); - lines_with_transitions - } - - fn duration(&self) -> f32 { - let ts = self.lines.iter() - .filter(|x| (*x).contains("Duration: ")) - .filter(|x| (*x).contains("start: ")) - .filter(|x| (*x).contains("bitrate: ")) - .nth(0) - .expect("did not find duration from ffmpeg") - .split(",").nth(0).unwrap() - .split(": ").nth(1).unwrap(); - let pieces: Vec<_> = ts.split(":") - .map(|x| f32::from_str(x).unwrap()) - .collect(); - assert_eq!(3, pieces.len()); - let hours = pieces[0] * 60.0 * 60.0; - let minutes = pieces[1] * 60.0; - let seconds = pieces[2]; - hours + minutes + seconds - } - - fn spans_from_transitions(&self, transitions: Vec) -> Vec { - let spans: Vec<_> = self.raw_spans_from_transitions(transitions).iter() - .filter(|x| x.stop - x.start > 0.25) // TODO const - .map(|x| ContentSpan{ - start: (x.start - 1.0).clamp(0.0, self.duration()), - stop: (x.stop + 1.0).clamp(0.0, self.duration()), // TODO const - }) - .collect(); - self.merge_spans(spans) - } - - fn merge_unsorted_spans(&self, spans: Vec) -> Vec { - let mut spans: Vec<_> = spans.iter().map(|x| *x).collect(); - spans.sort_by(|x, y| match x.start.partial_cmp(&y.start).unwrap() { - Ordering::Less => Ordering::Less, - Ordering::Equal => x.stop.partial_cmp(&y.stop).unwrap(), - Ordering::Greater => Ordering::Greater, - }); - self.merge_spans(spans) - } - - fn merge_spans(&self, spans: Vec) -> Vec { - let mut result: Vec<_> = vec![]; - let mut cur = *spans.iter().nth(0).or(Some(&ContentSpan{start: 0.0, stop: 0.0})).unwrap(); - for i in 1..spans.len() { - if spans[i].start - cur.stop > 5.0 { // TODO const - if !cur.empty() { - result.push(cur); - } - cur = spans[i]; - } else if spans[i].stop > cur.stop { - cur.stop = spans[i].stop - } - } - result.push(cur); - result - } - - fn raw_spans_from_transitions(&self, transitions: Vec) -> Vec { - let mut result = vec![]; - for i in (0..transitions.len()).step_by(2) { - let start = transitions[i]; - let mut stop = self.duration(); - if transitions.len() > i+1 { - stop = transitions[i+1]; - } - result.push(ContentSpan{start: start, stop: stop}); - } - result - } -} - -#[cfg(test)] -mod test_inspection { - use super::*; - const FILE: &str = "/Users/breel/Movies/bel_1_1.mp4"; - - #[test] - fn test_screenshotify() { - let result = screenshotify(&FILE.to_string()).unwrap(); - for i in result.iter() { - eprintln!("{}", i); - } - } - - #[test] - fn test_clipify() { - let result = clipify(&FILE.to_string()).unwrap(); - for i in result.iter() { - eprintln!("{}", i); - } - } - - #[test] - fn test_screenshot() { - let output = format!("{}.clipped.jpg", FILE); - screenshot( - &output, - &FILE.to_string(), - 3.0, - ).unwrap(); - - let inspection = _inspect(&output); - assert_eq!(true, inspection.is_ok()); - let inspection = inspection.unwrap(); - assert_eq!(0.04, inspection.duration()); - } - - #[test] - fn test_clip() { - let output = format!("{}.clipped.mp4", FILE); - clip( - &output, - &FILE.to_string(), - ContentSpan{start: 3.0, stop: 5.0}, - ).unwrap(); - - let inspection = _inspect(&output); - assert_eq!(true, inspection.is_ok()); - let inspection = inspection.unwrap(); - assert_eq!(2.0, inspection.duration()); - } - - #[test] - fn test_inspect() { - let inspection = _inspect(&FILE.to_string()); - assert_eq!(true, inspection.is_ok()); - let inspection = inspection.unwrap(); - - assert_eq!(28.14, inspection.duration()); - - assert_eq!(4, inspection.visual_transitions().len()); - assert_eq!( - vec![ContentSpan{start: 1.0520501, stop: 22.0043}], - inspection.visual_spans(), - ); - - assert_eq!(8, inspection.audible_transitions().len()); - assert_eq!( - vec![ContentSpan{start: 3.3723102, stop: 20.8207}], - inspection.audible_spans(), - ); - - assert_eq!(1, inspection.content_spans().len()); - assert_eq!( - vec![ContentSpan{start: 1.0520501, stop: 22.0043}], - inspection.content_spans(), - ); - - let lines = inspection.lines; - assert!(lines.iter().filter(|x| x.contains("Parsed_mpdecimate_0")).count() > 0); - assert!(lines.iter().filter(|x| x.contains("Duration: 00")).count() > 0); - assert!(lines.iter().filter(|x| x.contains("silence_end: ")).count() > 0); - assert!(lines.iter().filter(|x| x.contains("silence_start: ")).count() > 0); - } -} - -#[derive(Debug, PartialEq, Clone, Copy, PartialOrd)] -pub struct ContentSpan { - pub start: f32, - pub stop: f32, -} - -impl ContentSpan { - pub fn duration(&self) -> f32 { - self.stop - self.start - } - - fn empty(&self) -> bool { - *self == (ContentSpan{start: 0.0, stop: 0.0}) - } -} - -#[cfg(test)] -mod test_content_span { - use super::*; - - #[test] - fn test_duration() { - assert_eq!(1.0, ContentSpan{start: 1.0, stop: 2.0}.duration()); - } -} diff --git a/src-lib/src/cli/main.rs b/src-lib/src/cli/main.rs new file mode 100644 index 0000000..6f460fb --- /dev/null +++ b/src-lib/src/cli/main.rs @@ -0,0 +1,29 @@ +use lib; +use std::str::FromStr; + +fn main() { + let cmd = std::env::args().nth(1).expect("first argument must be [inspect] or [clip]"); + let file = std::env::args().nth(2).expect("second argument must be [path to file]"); + match cmd.as_ref() { + "clip" => { + let content_span = lib::video::ContentSpan{ + start: f32::from_str(&std::env::args().nth(3).expect("third argument must be [start time as f32 seconds]")).unwrap(), + stop: f32::from_str(&std::env::args().nth(4).expect("fourth argument must be [stop time as f32 seconds]")).unwrap(), + }; + let output = format!("{}.clipped.mp4", &file); + eprintln!("clipping {} from {} to {} as {}...", &file, &content_span.start, &content_span.stop, &output); + lib::video::clip(&output, &file, content_span).unwrap(); + }, + "inspect" => { + eprintln!("inspecting {}...", &file); + let content_spans = lib::video::inspect(&file).unwrap(); + content_spans.iter() + .for_each(|x| { + println!("{}..{}", x.start, x.stop); + }); + }, + _ => { + panic!("unrecognized command {}", cmd); + }, + }; +}