From 4efbe0c44e2a69f148baf6c17439d9cc69aa4161 Mon Sep 17 00:00:00 2001
From: Dominic <git@msrd0.de>
Date: Mon, 30 Oct 2023 21:26:17 +0100
Subject: [PATCH] very basic rendering support

---
 src/main.rs          |   3 +-
 src/render/ffmpeg.rs |   4 +-
 src/render/filter.rs | 111 +++++++++++++++++++++++++++++++++++++++----
 src/render/mod.rs    |  61 ++++++++++++++++++++++++
 src/time.rs          |  30 ++++++++++++
 5 files changed, 198 insertions(+), 11 deletions(-)

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<PathBuf> {
+		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;
 
-- 
GitLab