very basic rendering support
This commit is contained in:
parent
889dbbce5a
commit
4efbe0c44e
5 changed files with 198 additions and 11 deletions
|
@ -238,5 +238,6 @@ fn main() {
|
||||||
fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
|
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}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.inputs.push(input);
|
||||||
self
|
(self.inputs.len() - 1).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_filter(&mut self, filter: Filter) -> &mut Self {
|
pub fn add_filter(&mut self, filter: Filter) -> &mut Self {
|
||||||
|
|
|
@ -46,6 +46,16 @@ pub(crate) enum Filter {
|
||||||
GenerateSilence {
|
GenerateSilence {
|
||||||
video: Cow<'static, str>,
|
video: Cow<'static, str>,
|
||||||
output: 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();
|
let mut args = String::new();
|
||||||
if let Some(start) = start {
|
if let Some(start) = start {
|
||||||
write!(args, "start={}", format_time(*start));
|
write!(args, "start={start}");
|
||||||
}
|
}
|
||||||
if let Some(duration) = duration {
|
if let Some(duration) = duration {
|
||||||
if !args.is_empty() {
|
if !args.is_empty() {
|
||||||
args += ":";
|
args += ":";
|
||||||
}
|
}
|
||||||
write!(args, "duration={}", format_time(*duration));
|
write!(args, "duration={duration}");
|
||||||
}
|
}
|
||||||
writeln!(
|
writeln!(
|
||||||
complex,
|
complex,
|
||||||
|
@ -161,11 +171,7 @@ impl Filter {
|
||||||
duration,
|
duration,
|
||||||
output
|
output
|
||||||
} => {
|
} => {
|
||||||
let args = format!(
|
let args = format!("{direction}:st={start}:d={duration}");
|
||||||
"{direction}:st={}:d={}",
|
|
||||||
format_time(*start),
|
|
||||||
format_time(*duration)
|
|
||||||
);
|
|
||||||
writeln!(
|
writeln!(
|
||||||
complex,
|
complex,
|
||||||
"{}fade={args}{};",
|
"{}fade={args}{};",
|
||||||
|
@ -190,9 +196,85 @@ impl Filter {
|
||||||
writeln!(complex, "aevalsrc=0:s=48000{};", channel('a', output));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn channel(channel: char, id: &str) -> String {
|
pub(super) fn channel(channel: char, id: &str) -> String {
|
||||||
|
@ -202,3 +284,16 @@ pub(super) fn channel(channel: char, id: &str) -> String {
|
||||||
format!("[{id}:{channel}]")
|
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)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ use anyhow::{bail, Context};
|
||||||
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
|
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
|
||||||
use rational::Rational;
|
use rational::Rational;
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::Write as _,
|
io::Write as _,
|
||||||
process::{Command, Stdio}
|
process::{Command, Stdio}
|
||||||
|
@ -251,4 +252,64 @@ impl<'a> Renderer<'a> {
|
||||||
|
|
||||||
Ok(())
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
30
src/time.rs
30
src/time.rs
|
@ -1,6 +1,7 @@
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Display, Write as _},
|
fmt::{self, Display, Write as _},
|
||||||
|
ops::{Add, Sub},
|
||||||
str::FromStr
|
str::FromStr
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,6 +83,35 @@ pub struct Time {
|
||||||
pub micros: u32
|
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 {
|
impl FromStr for Time {
|
||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue