From 75534a383cc32f8688e44957b6046041ac881f0e Mon Sep 17 00:00:00 2001 From: Bel LaPointe <153096461+breel-render@users.noreply.github.com> Date: Tue, 26 Dec 2023 21:13:07 -0500 Subject: [PATCH] alllllmost ported i think --- src-lib/src/ffmpeg.rs | 224 ++++++++++++++++++++++++++++++++++++++++++ src-lib/src/lib.rs | 2 + 2 files changed, 226 insertions(+) create mode 100644 src-lib/src/ffmpeg.rs diff --git a/src-lib/src/ffmpeg.rs b/src-lib/src/ffmpeg.rs new file mode 100644 index 0000000..0db4fd9 --- /dev/null +++ b/src-lib/src/ffmpeg.rs @@ -0,0 +1,224 @@ +use std::str::FromStr; +use core::cmp::Ordering; + +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 {}: {}", file, msg)), + } +} + +struct Inspection { + lines: Vec, +} + +impl Inspection { + pub 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) + .unwrap() + .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::*; + + #[test] + fn test_inspect() { + let file = "/Users/breel/Movies/bel_1_1.mp4".to_string(); + let inspection = inspect(&file); + 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 { + start: f32, + 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/lib.rs b/src-lib/src/lib.rs index 7d12d9a..2ce735b 100644 --- a/src-lib/src/lib.rs +++ b/src-lib/src/lib.rs @@ -1,3 +1,5 @@ +mod ffmpeg; + pub fn add(left: usize, right: usize) -> usize { left + right }