From 4a10d10d47c4dbcfe75653672b211114f143621a Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 1 Nov 2023 08:56:20 +0100 Subject: [PATCH 01/82] attempt to feed complex filtergraph into graph2dot --- src/render/ffmpeg.rs | 52 +++++++++++++++++++++++++++++++++++++------- src/render/mod.rs | 12 +++++----- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 05dbf4b..bf3836d 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -1,12 +1,17 @@ -use super::{cmd, filter::Filter}; +use super::{filter::Filter, new_cmd}; use crate::{ render::filter::channel, time::{format_time, Time} }; use anyhow::bail; -use camino::Utf8PathBuf as PathBuf; +use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf}; use rational::Rational; -use std::{borrow::Cow, fmt::Write as _, process::Command}; +use std::{ + borrow::Cow, + fmt::Write as _, + io::Write as _, + process::{Command, Stdio} +}; pub(crate) struct FfmpegInput { pub(crate) concat: bool, @@ -29,7 +34,7 @@ impl FfmpegInput { } } - fn append_to_cmd(self, cmd: &mut Command) { + fn append_to_cmd(&self, cmd: &mut Command) { if self.concat { cmd.arg("-f").arg("concat").arg("-safe").arg("0"); } @@ -45,7 +50,7 @@ impl FfmpegInput { if let Some(duration) = self.duration { cmd.arg("-t").arg(format_time(duration)); } - cmd.arg("-i").arg(self.path); + cmd.arg("-i").arg(&self.path); } } @@ -104,8 +109,8 @@ impl Ffmpeg { self } - pub fn run(mut self) -> anyhow::Result<()> { - let mut cmd = cmd(); + pub fn run(mut self, filter_graph: Option<&Path>) -> anyhow::Result<()> { + let mut cmd = new_cmd(); cmd.arg("ffmpeg").arg("-hide_banner").arg("-y"); // determine whether the video need to be re-encoded @@ -120,7 +125,7 @@ impl Ffmpeg { } // append all the inputs - for i in self.inputs { + for i in &self.inputs { i.append_to_cmd(&mut cmd); } @@ -149,6 +154,37 @@ impl Ffmpeg { } else { write!(complex, "{}null[v]", channel('v', &self.filters_output)); } + + if let Some(filter_graph) = filter_graph { + let mut graph2dot = new_cmd(); + graph2dot.stdin(Stdio::piped()); + graph2dot.stdout(Stdio::piped()); + graph2dot.arg("graph2dot"); + let mut graph2dot = graph2dot.spawn()?; + + let mut dot = new_cmd(); + dot.stdin(Stdio::from(graph2dot.stdout.take().unwrap())); + dot.arg("dot").arg("-Tpng").arg("-o").arg(filter_graph); + let mut dot = dot.spawn()?; + + let mut stdin = graph2dot.stdin.take().unwrap(); + for (i, _) in self.inputs.iter().enumerate() { + writeln!(stdin, "nullsrc[{i}];[{i}]nullsink;"); + } + writeln!(stdin, "{complex};")?; + writeln!(stdin, "[v]nullsink"); + drop(stdin); + + let status = graph2dot.wait()?; + if !status.success() { + bail!("graph2dot failed with status code {:?}", status.code()); + } + let status = dot.wait()?; + if !status.success() { + bail!("graphviz dot failed with status code {:?}", status.code()); + } + } + cmd.arg("-filter_complex").arg(complex); cmd.arg("-map").arg("[v]"); cmd.arg("-map").arg(channel('a', &self.filters_output)); diff --git a/src/render/mod.rs b/src/render/mod.rs index 1c963e0..b47b689 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -38,7 +38,7 @@ const FF_MULTIPLIER: usize = 8; const FF_LOGO_SIZE: usize = 128; const LOGO_SIZE: usize = 96; -fn cmd() -> Command { +fn new_cmd() -> Command { let mut cmd = Command::new("busybox"); cmd.arg("ash") .arg("-exuo") @@ -49,7 +49,7 @@ fn cmd() -> Command { } fn ffprobe() -> Command { - let mut cmd = cmd(); + let mut cmd = new_cmd(); cmd.arg("ffprobe") .arg("-v") .arg("error") @@ -119,11 +119,11 @@ fn svg2mp4(svg: PathBuf, mp4: PathBuf, duration: Time) -> anyhow::Result<()> { }); ffmpeg.set_filter_output("out"); ffmpeg.set_duration(duration); - ffmpeg.run() + ffmpeg.run(None) } fn svg2png(svg: &Path, png: &Path, size: usize) -> anyhow::Result<()> { - let mut cmd = cmd(); + let mut cmd = new_cmd(); let size = size.to_string(); cmd.arg("inkscape") .arg("-w") @@ -181,7 +181,7 @@ impl<'a> Renderer<'a> { ..FfmpegInput::new(recording_txt) }); ffmpeg.enable_loudnorm(); - ffmpeg.run()?; + ffmpeg.run(None)?; let width = ffprobe_video("stream=width", &recording_mp4)?.parse()?; let height = ffprobe_video("stream=height", &recording_mp4)?.parse()?; @@ -425,7 +425,7 @@ impl<'a> Renderer<'a> { // we're done :) ffmpeg.set_filter_output(overlay); - ffmpeg.run()?; + ffmpeg.run(Some(&self.target.join("filter_graph.png")))?; Ok(output) } From 2a8ba70e235e5807cb27e734ce8faec1b1ec21de Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 2 Nov 2023 10:23:31 +0100 Subject: [PATCH 02/82] limit the ram usage of ffmpeg --- src/render/mod.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/render/mod.rs b/src/render/mod.rs index 1c963e0..b5ea7e4 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -21,6 +21,8 @@ use std::{ process::{Command, Stdio} }; +const MEMORY_LIMIT: &str = "2G"; + const INTRO_LEN: Time = Time { seconds: 3, micros: 0 @@ -39,8 +41,18 @@ const FF_LOGO_SIZE: usize = 128; const LOGO_SIZE: usize = 96; fn cmd() -> Command { - let mut cmd = Command::new("busybox"); - cmd.arg("ash") + // we use systemd-run to limit the process memory + // I tried others like ulimit, chpst or isolate, but none worked + let mut cmd = Command::new("systemd-run"); + cmd.arg("--scope") + .arg("-q") + .arg("--expand-environment=no") + .arg("-p") + .arg(format!("MemoryMax={MEMORY_LIMIT}")) + .arg("--user"); + // we use busybox ash for having a shell that outputs commands with -x + cmd.arg("busybox") + .arg("ash") .arg("-exuo") .arg("pipefail") .arg("-c") From f4fe73bf5fe8af90e503599624667b9b6f442a0e Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 2 Nov 2023 11:07:35 +0100 Subject: [PATCH 03/82] use -ss/-t instead of trim --- src/render/ffmpeg.rs | 1 + src/render/mod.rs | 34 ++++++++++++---------------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 05dbf4b..9e3c476 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -40,6 +40,7 @@ impl FfmpegInput { cmd.arg("-r").arg(fps.to_string()); } if let Some(start) = self.start { + cmd.arg("-seek_streams_individually").arg("false"); cmd.arg("-ss").arg(format_time(start)); } if let Some(duration) = self.duration { diff --git a/src/render/mod.rs b/src/render/mod.rs index b5ea7e4..46e0c5b 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -279,7 +279,7 @@ impl<'a> Renderer<'a> { // 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 rec_file = 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"))); @@ -297,12 +297,10 @@ impl<'a> Renderer<'a> { project.source.fast.sort(); for (i, (ff_st, ff_end)) in project.source.fast.iter().rev().enumerate() { if let Some(prev_end) = part2_end_of_the_start { - let recffbetween = format!("recff{i}between"); - ffmpeg.add_filter(Filter::Trim { - input: rec.clone().into(), + let recffbetween = ffmpeg.add_input(FfmpegInput { start: Some(*ff_end), - duration: Some(prev_end), - output: recffbetween.clone().into() + duration: Some(prev_end - *ff_end), + ..FfmpegInput::new(rec_file.clone()) }); part2.push_front(recffbetween.into()); } else { @@ -310,12 +308,10 @@ impl<'a> Renderer<'a> { } part2_end_of_the_start = Some(*ff_st); - let recffpart = format!("recff{i}part"); - ffmpeg.add_filter(Filter::Trim { - input: rec.clone().into(), + let recffpart = ffmpeg.add_input(FfmpegInput { start: Some(*ff_st), duration: Some(*ff_end - *ff_st), - output: recffpart.clone().into() + ..FfmpegInput::new(rec_file.clone()) }); let recff = format!("recff{i}"); @@ -333,34 +329,28 @@ impl<'a> Renderer<'a> { let end = project.source.end.unwrap(); let part2_last_part_duration; if part2.is_empty() { - let rectrim = "rectrim"; part2_last_part_duration = end - start; - ffmpeg.add_filter(Filter::Trim { - input: rec.into(), + let rectrim = ffmpeg.add_input(FfmpegInput { start: Some(start), duration: Some(part2_last_part_duration), - output: rectrim.into() + ..FfmpegInput::new(rec_file.clone()) }); part2.push_back(rectrim.into()); } // otherwise add the first and last parts separately else { - let rectrimst = "rectrimst"; - ffmpeg.add_filter(Filter::Trim { - input: rec.clone().into(), + let rectrimst = ffmpeg.add_input(FfmpegInput { start: Some(start), duration: Some(part2_end_of_the_start.unwrap() - start), - output: rectrimst.into() + ..FfmpegInput::new(rec_file.clone()) }); part2.push_front(rectrimst.into()); - let rectrimend = "rectrimend"; part2_last_part_duration = end - part2_start_of_the_end.unwrap(); - ffmpeg.add_filter(Filter::Trim { - input: rec.into(), + let rectrimend = ffmpeg.add_input(FfmpegInput { start: Some(part2_start_of_the_end.unwrap()), duration: Some(part2_last_part_duration), - output: rectrimend.into() + ..FfmpegInput::new(rec_file.clone()) }); part2.push_back(rectrimend.into()); } From ca3ab6a7885424ad1e0ca2e2a971ba71408862a4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 2 Nov 2023 21:33:21 +0100 Subject: [PATCH 04/82] fix memory limit --- src/main.rs | 11 +++++++++-- src/render/mod.rs | 6 ++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8f3eb2c..512ab42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,16 +19,22 @@ use std::{ collections::BTreeSet, fmt::Display, fs, - io::{self, BufRead as _, Write} + io::{self, BufRead as _, Write}, + sync::RwLock }; +static MEM_LIMIT: RwLock = RwLock::new(String::new()); + #[derive(Debug, Parser)] struct Args { #[clap(short = 'C', long, default_value = ".")] directory: PathBuf, #[clap(short = 'c', long, default_value = "23ws-malo")] - course: String + course: String, + + #[clap(short, long, default_value = "2G")] + mem_limit: String } #[allow(non_camel_case_types, clippy::upper_case_acronyms)] @@ -146,6 +152,7 @@ fn ask_time(question: impl Display) -> Time { fn main() { let args = Args::parse(); + *(MEM_LIMIT.write().unwrap()) = args.mem_limit; // process arguments let directory = args.directory.canonicalize_utf8().unwrap(); diff --git a/src/render/mod.rs b/src/render/mod.rs index 46e0c5b..6310971 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -8,7 +8,7 @@ use crate::{ iotro::{intro, outro}, render::ffmpeg::{Ffmpeg, FfmpegInput}, time::{format_date, Time}, - Project, ProjectSourceMetadata, Resolution + Project, ProjectSourceMetadata, Resolution, MEM_LIMIT }; use anyhow::{bail, Context}; use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf}; @@ -21,8 +21,6 @@ use std::{ process::{Command, Stdio} }; -const MEMORY_LIMIT: &str = "2G"; - const INTRO_LEN: Time = Time { seconds: 3, micros: 0 @@ -48,7 +46,7 @@ fn cmd() -> Command { .arg("-q") .arg("--expand-environment=no") .arg("-p") - .arg(format!("MemoryMax={MEMORY_LIMIT}")) + .arg(format!("MemoryMax={}", MEM_LIMIT.read().unwrap())) .arg("--user"); // we use busybox ash for having a shell that outputs commands with -x cmd.arg("busybox") From 164c6f9bacd342839cd44e831f8ccfc209eb4caf Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 2 Nov 2023 22:40:49 +0100 Subject: [PATCH 05/82] set render quality to 24 --- src/main.rs | 2 +- src/render/ffmpeg.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 512ab42..04e5791 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,7 @@ struct Args { #[clap(short = 'c', long, default_value = "23ws-malo")] course: String, - #[clap(short, long, default_value = "2G")] + #[clap(short, long, default_value = "8G")] mem_limit: String } diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 9e3c476..9af578c 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -160,7 +160,7 @@ impl Ffmpeg { if vaapi { cmd.arg("-c:v").arg("h264_vaapi"); cmd.arg("-rc_mode").arg("CQP"); - cmd.arg("-global_quality").arg("22"); + cmd.arg("-global_quality").arg("24"); } else if venc { cmd.arg("-c:v").arg("libx264"); cmd.arg("-crf").arg("22"); From 268c4b3af7d4e8b01a7ba4b2e76faf18227f7373 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 2 Nov 2023 23:48:56 +0100 Subject: [PATCH 06/82] add missing alpha filter on the logo --- src/render/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/render/mod.rs b/src/render/mod.rs index 6310971..56e6354 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -414,10 +414,16 @@ impl<'a> Renderer<'a> { }); // overlay the logo + let logoalpha = "logoalpha"; + ffmpeg.add_filter(Filter::Alpha { + input: logo.into(), + alpha: 0.5, + output: logoalpha.into() + }); let overlay = "overlay"; ffmpeg.add_filter(Filter::Overlay { video_input: concat.into(), - overlay_input: logo.into(), + overlay_input: logoalpha.into(), x: "main_w-overlay_w-130".into(), y: "main_h-overlay_h-65".into(), output: overlay.into() From feb8596bfc31621cf8a3dd1c44d95929be686eb2 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 3 Nov 2023 10:02:30 +0100 Subject: [PATCH 07/82] support rescaling --- src/main.rs | 117 ++++++++++++++++++++++++++++++++-------------- src/render/mod.rs | 54 ++++++++++++++++----- 2 files changed, 125 insertions(+), 46 deletions(-) diff --git a/src/main.rs b/src/main.rs index 04e5791..8df5f65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,41 +37,56 @@ struct Args { mem_limit: String } -#[allow(non_camel_case_types, clippy::upper_case_acronyms)] -#[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] -enum Resolution { - /// 640x360 - nHD, - /// 1280x720 - HD, - /// 1920x1080 - FullHD, - /// 2560x1440 - WQHD, - /// 3840x2160 - UHD +macro_rules! resolutions { + ($($res:ident: $width:literal x $height:literal at $bitrate:literal),+) => { + #[allow(non_camel_case_types, clippy::upper_case_acronyms)] + #[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] + enum Resolution { + $( + #[doc = concat!(stringify!($width), "x", stringify!($height))] + $res + ),+ + } + + const NUM_RESOLUTIONS: usize = { + let mut num = 0; + $(num += 1; stringify!($res);)+ + num + }; + + impl Resolution { + fn values() -> [Self; NUM_RESOLUTIONS] { + [$(Self::$res),+] + } + + fn width(self) -> usize { + match self { + $(Self::$res => $width),+ + } + } + + fn height(self) -> usize { + match self { + $(Self::$res => $height),+ + } + } + + fn bitrate(self) -> &'static str { + match self { + $(Self::$res => $bitrate),+ + } + } + } + } } -impl Resolution { - fn width(self) -> usize { - match self { - Self::nHD => 640, - Self::HD => 1280, - Self::FullHD => 1920, - Self::WQHD => 2560, - Self::UHD => 3840 - } - } - - fn height(self) -> usize { - match self { - Self::nHD => 360, - Self::HD => 720, - Self::FullHD => 1080, - Self::WQHD => 1440, - Self::UHD => 2160 - } - } +resolutions! { + nHD: 640 x 360 at "500k", + HD: 1280 x 720 at "1M", + FullHD: 1920 x 1080 at "2M", + WQHD: 2560 x 1440 at "3M", + // TODO qsx muss mal sagen wieviel bitrate für 4k + UHD: 3840 x 2160 at "4M" } #[derive(Deserialize, Serialize)] @@ -245,6 +260,38 @@ fn main() { fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap(); } - let video = renderer.render(&mut project).unwrap(); - println!("\x1B[1m ==> DONE :)\x1B[0m Video: {video}"); + // render the video + let mut videos = Vec::new(); + videos.push(if !project.progress.rendered { + let video = renderer.render(&mut project).unwrap(); + project.progress.rendered = true; + + println!("{}", toml::to_string(&project).unwrap()); + fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap(); + + video + } else { + renderer.video_mp4(&project) + }); + + // rescale the video + for res in Resolution::values() { + if res >= project.source.metadata.as_ref().unwrap().source_res { + continue; + } + if !project.progress.transcoded.contains(&res) { + videos.push(renderer.rescale(res, &project).unwrap()); + project.progress.transcoded.insert(res); + + println!("{}", toml::to_string(&project).unwrap()); + fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()) + .unwrap(); + } + } + + println!("\x1B[1m ==> DONE :)\x1B[0m"); + println!(" Videos:"); + for v in &videos { + println!(" -> {v}"); + } } diff --git a/src/render/mod.rs b/src/render/mod.rs index 56e6354..49c62b8 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -261,18 +261,17 @@ impl<'a> Renderer<'a> { Ok(()) } + fn video_mp4_res(&self, res: Resolution) -> PathBuf { + self.target + .join(format!("{}-{}p.mp4", self.slug, res.height())) + } + + pub(crate) fn video_mp4(&self, project: &Project) -> PathBuf { + self.video_mp4_res(project.source.metadata.as_ref().unwrap().source_res) + } + 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 - .height() - )); + let output = self.video_mp4(project); let mut ffmpeg = Ffmpeg::new(output.clone()); // add all of our inputs @@ -435,4 +434,37 @@ impl<'a> Renderer<'a> { Ok(output) } + + pub fn rescale(&self, res: Resolution, project: &Project) -> anyhow::Result { + let input = self.video_mp4(project); + let output = self.video_mp4_res(res); + println!("\x1B[1m ==> Rescaling to {}p\x1B[0m", res.height()); + + let mut ffmpeg = cmd(); + ffmpeg.arg("ffmpeg").arg("-hide_banner"); + // TODO do we just always want hwaccel? + ffmpeg + .arg("-hwaccel") + .arg("vaapi") + .arg("-hwaccel_device") + .arg("/dev/dri/renderD128") + .arg("-hwaccel_output_format") + .arg("vaapi"); + ffmpeg.arg("-i").arg(input); + ffmpeg.arg("-vf").arg(format!( + "scale_vaapi=w={}:h={}", + res.width(), + res.height() + )); + ffmpeg.arg("-c:a").arg("copy").arg("-c:v").arg("h264_vaapi"); + ffmpeg.arg("-b:v").arg(res.bitrate()); + ffmpeg.arg(&output); + + let status = ffmpeg.status()?; + if status.success() { + Ok(output) + } else { + bail!("ffmpeg failed with exit code {:?}", status.code()) + } + } } From 371071ca47d12659fa07cec65d3de06f381a0ce9 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 14 Nov 2023 10:30:17 +0100 Subject: [PATCH 08/82] transcode in reverse order --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 8df5f65..cc6e18e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -275,7 +275,7 @@ fn main() { }); // rescale the video - for res in Resolution::values() { + for res in Resolution::values().into_iter().rev() { if res >= project.source.metadata.as_ref().unwrap().source_res { continue; } From 24ea4ebe07d78d60f63d363c4a2bf4efe02cbf27 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 14 Nov 2023 10:36:12 +0100 Subject: [PATCH 09/82] make transcoding optional --- src/main.rs | 49 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index cc6e18e..44e034a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ use std::{ fmt::Display, fs, io::{self, BufRead as _, Write}, + str::FromStr, sync::RwLock }; @@ -27,20 +28,27 @@ static MEM_LIMIT: RwLock = RwLock::new(String::new()); #[derive(Debug, Parser)] struct Args { + /// The root directory of the project. It should contain the raw video file(s). #[clap(short = 'C', long, default_value = ".")] directory: PathBuf, - #[clap(short = 'c', long, default_value = "23ws-malo")] + /// The slug of the course, e.g. "23ws-malo2". + #[clap(short = 'c', long, default_value = "23ws-malo2")] course: String, + /// The memory limit for external tools like ffmpeg. #[clap(short, long, default_value = "8G")] - mem_limit: String + mem_limit: String, + + /// Transcode the final video clip down to the minimum resolution specified. + #[clap(short, long)] + transcode: Option } macro_rules! resolutions { ($($res:ident: $width:literal x $height:literal at $bitrate:literal),+) => { #[allow(non_camel_case_types, clippy::upper_case_acronyms)] - #[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] + #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] enum Resolution { $( #[doc = concat!(stringify!($width), "x", stringify!($height))] @@ -77,6 +85,17 @@ macro_rules! resolutions { } } } + + impl FromStr for Resolution { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + Ok(match s { + $(concat!(stringify!($height), "p") => Self::$res,)+ + _ => anyhow::bail!("Unknown Resolution: {s:?}") + }) + } + } } } @@ -275,17 +294,21 @@ fn main() { }); // rescale the video - for res in Resolution::values().into_iter().rev() { - if res >= project.source.metadata.as_ref().unwrap().source_res { - continue; - } - if !project.progress.transcoded.contains(&res) { - videos.push(renderer.rescale(res, &project).unwrap()); - project.progress.transcoded.insert(res); + if let Some(lowest_res) = args.transcode { + for res in Resolution::values().into_iter().rev() { + if res >= project.source.metadata.as_ref().unwrap().source_res + || res < lowest_res + { + continue; + } + if !project.progress.transcoded.contains(&res) { + videos.push(renderer.rescale(res, &project).unwrap()); + project.progress.transcoded.insert(res); - println!("{}", toml::to_string(&project).unwrap()); - fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()) - .unwrap(); + println!("{}", toml::to_string(&project).unwrap()); + fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()) + .unwrap(); + } } } From 270233ca5cabb23c8a99ea02d977fc483f6f3860 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 15 Nov 2023 14:13:57 +0100 Subject: [PATCH 10/82] add faststart flag --- src/render/ffmpeg.rs | 1 + src/render/mod.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 9af578c..86f6f20 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -177,6 +177,7 @@ impl Ffmpeg { if let Some(duration) = self.duration { cmd.arg("-t").arg(format_time(duration)); } + cmd.arg("-movflags").arg("+faststart"); cmd.arg(&self.output); let status = cmd.status()?; diff --git a/src/render/mod.rs b/src/render/mod.rs index 49c62b8..5e4257c 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -458,6 +458,7 @@ impl<'a> Renderer<'a> { )); ffmpeg.arg("-c:a").arg("copy").arg("-c:v").arg("h264_vaapi"); ffmpeg.arg("-b:v").arg(res.bitrate()); + ffmpeg.arg("-movflags").arg("+faststart"); ffmpeg.arg(&output); let status = ffmpeg.status()?; From 086507684992159828357aab1e687a06e82c92d3 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 15 Nov 2023 14:14:18 +0100 Subject: [PATCH 11/82] setup test with 50 fps --- 230101/project.toml | 23 +++++++++++++++++++++++ 230101/setup.sh | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 230101/project.toml diff --git a/230101/project.toml b/230101/project.toml new file mode 100644 index 0000000..3907d26 --- /dev/null +++ b/230101/project.toml @@ -0,0 +1,23 @@ +[lecture] +course = "23ws-malo" +date = "230101" + +[source] +files = ["C01.mp4", "C02.mp4", "C03.mp4"] +start = "2" +end = "12" +fast = [["5", "7"], ["9", "11"]] + +[source.metadata] +source_duration = "12.533330" +source_fps = "25/1" +source_tbn = "1/12800" +source_res = "FullHD" +source_sample_rate = 48000 + +[progress] +preprocessed = false +asked_start_end = true +asked_fast = true +rendered = false +transcoded = [] diff --git a/230101/setup.sh b/230101/setup.sh index 0697088..9f66fb4 100755 --- a/230101/setup.sh +++ b/230101/setup.sh @@ -17,7 +17,7 @@ function render_clip() { aevalsrc=sin($freq*2*PI*t):s=48000,pan=stereo|c0=c0|c1=c0[a] " \ -map "[v]" -map "[a]" \ - -c:v h264_vaapi -r 25 -t 4 \ + -c:v h264_vaapi -r 50 -t 4 \ -c:a aac -b:a 128000 \ "$out" } From 2882fb286aea349154102d4385fe622b8473b313 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 15 Nov 2023 15:44:58 +0100 Subject: [PATCH 12/82] support 50fps videos; enable +faststart --- 230101/project.toml | 4 +-- src/render/ffmpeg.rs | 59 ++++++++++++++++++++++++++++++++------------ src/render/mod.rs | 28 +++++++++++++++------ 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/230101/project.toml b/230101/project.toml index 3907d26..8fff98f 100644 --- a/230101/project.toml +++ b/230101/project.toml @@ -9,8 +9,8 @@ end = "12" fast = [["5", "7"], ["9", "11"]] [source.metadata] -source_duration = "12.533330" -source_fps = "25/1" +source_duration = "12.53333" +source_fps = "50/1" source_tbn = "1/12800" source_res = "FullHD" source_sample_rate = 48000 diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs index 86f6f20..63657cd 100644 --- a/src/render/ffmpeg.rs +++ b/src/render/ffmpeg.rs @@ -50,25 +50,60 @@ impl FfmpegInput { } } +pub(crate) struct FfmpegOutput { + pub(crate) fps: Option, + pub(crate) duration: Option