diff --git a/src/main.rs b/src/main.rs index 8e54dc4..8f3eb2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -238,5 +238,6 @@ fn main() { fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap(); } - // render(&directory, &project).unwrap(); + let video = renderer.render(&mut project).unwrap(); + println!("\x1B[1m ==> DONE :)\x1B[0m Video: {video}"); } diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 4ac812b..05dbf4b 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -74,9 +74,9 @@ impl Ffmpeg { } } - pub fn add_input(&mut self, input: FfmpegInput) -> &mut Self { + pub fn add_input(&mut self, input: FfmpegInput) -> String { self.inputs.push(input); - self + (self.inputs.len() - 1).to_string() } pub fn add_filter(&mut self, filter: Filter) -> &mut Self { diff --git a/src/render/filter.rs b/src/render/filter.rs index 438b40c..c51aa23 100644 --- a/src/render/filter.rs +++ b/src/render/filter.rs @@ -46,6 +46,16 @@ pub(crate) enum Filter { GenerateSilence { video: Cow<'static, str>, output: Cow<'static, str> + }, + + /// Fast forward. Too complex to explain. Its magic. + FastForward { + input: Cow<'static, str>, + ffinput: Cow<'static, str>, + start: Time, + duration: Time, + multiplier: usize, + output: Cow<'static, str> } } @@ -85,13 +95,13 @@ impl Filter { } => { let mut args = String::new(); if let Some(start) = start { - write!(args, "start={}", format_time(*start)); + write!(args, "start={start}"); } if let Some(duration) = duration { if !args.is_empty() { args += ":"; } - write!(args, "duration={}", format_time(*duration)); + write!(args, "duration={duration}"); } writeln!( complex, @@ -161,11 +171,7 @@ impl Filter { duration, output } => { - let args = format!( - "{direction}:st={}:d={}", - format_time(*start), - format_time(*duration) - ); + let args = format!("{direction}:st={start}:d={duration}"); writeln!( complex, "{}fade={args}{};", @@ -190,8 +196,84 @@ impl Filter { writeln!(complex, "aevalsrc=0:s=48000{};", channel('a', output)); }, - _ => unimplemented!() + Self::FastForward { + input, + ffinput, + start, + duration, + multiplier, + output + } => { + let end = *start + *duration; + + // ok so let's start by duplicating the audio and video 3 times + let vin = next_tmp_3(filter_idx); + let ain = next_tmp_3(filter_idx); + writeln!( + complex, + "{}split=3{}{}{};", + channel('v', input), + vin[0], + vin[1], + vin[2] + ); + writeln!( + complex, + "{}asplit=3{}{}{};", + channel('v', input), + vin[0], + vin[1], + vin[2] + ); + + // next we cut those audio/videos into before, ff, after + let vcut = next_tmp_3(filter_idx); + let acut = next_tmp_3(filter_idx); + writeln!(complex, "{}trim=duration={start}{};", vin[0], vcut[0]); + writeln!(complex, "{}atrim=duration={start}{};", ain[0], acut[0]); + writeln!( + complex, + "{}trim=start={start}:duration={duration}{};", + vin[1], vcut[1] + ); + writeln!( + complex, + "{}atrim=start={start}:duration={duration}{};", + ain[1], acut[1] + ); + writeln!(complex, "{}trim=start={end}{};", vin[2], vcut[2]); + writeln!(complex, "{}atrim=start={end}{};", ain[2], acut[2]); + + // now we speed up the ff part + let vff = next_tmp(filter_idx); + let aff = next_tmp(filter_idx); + writeln!(complex, "{}setpts=PTS/{multiplier}{vff};", vcut[1]); + writeln!(complex, "{}atempo={multiplier}{aff};", acut[1]); + + // and we overlay the vff part + let voverlay = next_tmp(filter_idx); + writeln!( + complex, + "{vff}{}overlay=x=main_w/2-overlay_w/2:y=main_h/2-overlay_h/2{voverlay};", + channel('v', ffinput) + ); + + // and finally we concatenate everything back together + writeln!( + complex, + "{}{}{voverlay}{aff}{}{}concat=n=3:v=1:a=1{}{};", + vcut[0], + acut[0], + vcut[2], + acut[2], + channel('v', output), + channel('a', output) + ); + } } + + // add a newline after every filter to ease debugging + writeln!(complex); } } @@ -202,3 +284,16 @@ pub(super) fn channel(channel: char, id: &str) -> String { format!("[{id}:{channel}]") } } + +fn next_tmp(filter_idx: &mut usize) -> String { + *filter_idx += 1; + format!("[tmp{filter_idx}]") +} + +fn next_tmp_3(filter_idx: &mut usize) -> [String; 3] { + [ + next_tmp(filter_idx), + next_tmp(filter_idx), + next_tmp(filter_idx) + ] +} diff --git a/src/render/mod.rs b/src/render/mod.rs index 026cd79..521aae7 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -14,6 +14,7 @@ use anyhow::{bail, Context}; use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf}; use rational::Rational; use std::{ + borrow::Cow, fs::{self, File}, io::Write as _, process::{Command, Stdio} @@ -251,4 +252,64 @@ impl<'a> Renderer<'a> { Ok(()) } + + pub(crate) fn render(&self, project: &mut Project) -> anyhow::Result { + let mut output = self.target.join(format!( + "{}-{}p.mp4", + self.slug, + project.source.metadata.as_ref().unwrap().source_res.width() + )); + let mut ffmpeg = Ffmpeg::new(output.clone()); + + // add all of our inputs + let intro = ffmpeg.add_input(FfmpegInput::new(self.target.join("intro.mp4"))); + let rec = ffmpeg.add_input(FfmpegInput::new(self.target.join("recording.mp4"))); + let outro = ffmpeg.add_input(FfmpegInput::new(self.target.join("outro.mp4"))); + let logo = ffmpeg.add_input(FfmpegInput::new(self.target.join("logo.png"))); + let ff = ffmpeg.add_input(FfmpegInput::new(self.target.join("fastforward.png"))); + + let mut part1: Cow<'static, str> = intro.into(); + let mut part2: Cow<'static, str> = rec.into(); + let mut part3: Cow<'static, str> = outro.into(); + + // trim the recording + let rectrim = "rectrim"; + let start = project.source.start.unwrap(); + let duration = project.source.end.unwrap() - start; + ffmpeg.add_filter(Filter::Trim { + input: part2, + start: Some(start), + duration: Some(duration), + output: rectrim.into() + }); + part2 = rectrim.into(); + + // TODO ff + + // TODO fade + + // concatenate everything + let concat = "concat"; + ffmpeg.add_filter(Filter::Concat { + inputs: vec![part1, part2, part3], + n: 3, + output: concat.into() + }); + + // overlay the logo + let overlay = "overlay"; + ffmpeg.add_filter(Filter::Overlay { + video_input: concat.into(), + overlay_input: logo.into(), + x: "main_w-overlay_w-130".into(), + y: "main_h-overlay_h-65".into(), + output: overlay.into() + }); + + // we're done :) + ffmpeg.set_filter_output(overlay); + ffmpeg.run()?; + + Ok(output) + } } diff --git a/src/time.rs b/src/time.rs index d6f29bd..e799cc8 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,6 +1,7 @@ use anyhow::bail; use std::{ fmt::{self, Display, Write as _}, + ops::{Add, Sub}, str::FromStr }; @@ -82,6 +83,35 @@ pub struct Time { pub micros: u32 } +impl Add for Time { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + let mut seconds = self.seconds + rhs.seconds; + let mut micros = self.micros + rhs.micros; + if micros >= 1_000_000 { + seconds += 1; + micros -= 1_000_000; + } + Self { seconds, micros } + } +} + +impl Sub for Time { + type Output = Self; + + fn sub(mut self, rhs: Self) -> Self { + if rhs.micros > self.micros { + self.seconds -= 1; + self.micros += 1_000_000; + } + Self { + seconds: self.seconds - rhs.seconds, + micros: self.micros - rhs.micros + } + } +} + impl FromStr for Time { type Err = anyhow::Error;