support 50fps videos; enable +faststart

This commit is contained in:
Dominic 2023-11-15 15:44:58 +01:00
parent 0865076849
commit 2882fb286a
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
3 changed files with 65 additions and 26 deletions

View file

@ -9,8 +9,8 @@ end = "12"
fast = [["5", "7"], ["9", "11"]] fast = [["5", "7"], ["9", "11"]]
[source.metadata] [source.metadata]
source_duration = "12.533330" source_duration = "12.53333"
source_fps = "25/1" source_fps = "50/1"
source_tbn = "1/12800" source_tbn = "1/12800"
source_res = "FullHD" source_res = "FullHD"
source_sample_rate = 48000 source_sample_rate = 48000

View file

@ -50,25 +50,60 @@ impl FfmpegInput {
} }
} }
pub(crate) struct FfmpegOutput {
pub(crate) fps: Option<Rational>,
pub(crate) duration: Option<Time>,
pub(crate) time_base: Option<Rational>,
pub(crate) fps_mode_vfr: bool,
pub(crate) path: PathBuf
}
impl FfmpegOutput {
pub(crate) fn new(path: PathBuf) -> Self {
Self {
fps: None,
duration: None,
time_base: None,
fps_mode_vfr: false,
path
}
}
fn append_to_cmd(self, cmd: &mut Command) {
if let Some(fps) = self.fps {
cmd.arg("-r").arg(fps.to_string());
}
if let Some(duration) = self.duration {
cmd.arg("-t").arg(format_time(duration));
}
if let Some(time_base) = self.time_base {
cmd.arg("-enc_time_base").arg(time_base.to_string());
}
if self.fps_mode_vfr {
cmd.arg("-fps_mode").arg("vfr");
}
cmd.arg("-movflags").arg("+faststart");
cmd.arg(self.path);
}
}
pub(crate) struct Ffmpeg { pub(crate) struct Ffmpeg {
inputs: Vec<FfmpegInput>, inputs: Vec<FfmpegInput>,
filters: Vec<Filter>, filters: Vec<Filter>,
filters_output: Cow<'static, str>, filters_output: Cow<'static, str>,
loudnorm: bool, loudnorm: bool,
duration: Option<Time>, output: FfmpegOutput,
output: PathBuf,
filter_idx: usize filter_idx: usize
} }
impl Ffmpeg { impl Ffmpeg {
pub fn new(output: PathBuf) -> Self { pub fn new(output: FfmpegOutput) -> Self {
Self { Self {
inputs: Vec::new(), inputs: Vec::new(),
filters: Vec::new(), filters: Vec::new(),
filters_output: "0".into(), filters_output: "0".into(),
loudnorm: false, loudnorm: false,
duration: None,
output, output,
filter_idx: 0 filter_idx: 0
@ -100,11 +135,6 @@ impl Ffmpeg {
self self
} }
pub fn set_duration(&mut self, duration: Time) -> &mut Self {
self.duration = Some(duration);
self
}
pub fn run(mut self) -> anyhow::Result<()> { pub fn run(mut self) -> anyhow::Result<()> {
let mut cmd = cmd(); let mut cmd = cmd();
cmd.arg("ffmpeg").arg("-hide_banner").arg("-y"); cmd.arg("ffmpeg").arg("-hide_banner").arg("-y");
@ -157,13 +187,14 @@ impl Ffmpeg {
} }
// append encoding options // append encoding options
const QUALITY: &str = "24";
if vaapi { if vaapi {
cmd.arg("-c:v").arg("h264_vaapi"); cmd.arg("-c:v").arg("h264_vaapi");
cmd.arg("-rc_mode").arg("CQP"); cmd.arg("-rc_mode").arg("CQP");
cmd.arg("-global_quality").arg("24"); cmd.arg("-global_quality").arg(QUALITY);
} else if venc { } else if venc {
cmd.arg("-c:v").arg("libx264"); cmd.arg("-c:v").arg("libx264");
cmd.arg("-crf").arg("22"); cmd.arg("-crf").arg(QUALITY);
} else { } else {
cmd.arg("-c:v").arg("copy"); cmd.arg("-c:v").arg("copy");
} }
@ -174,11 +205,7 @@ impl Ffmpeg {
cmd.arg("-c:a").arg("copy"); cmd.arg("-c:a").arg("copy");
} }
if let Some(duration) = self.duration { self.output.append_to_cmd(&mut cmd);
cmd.arg("-t").arg(format_time(duration));
}
cmd.arg("-movflags").arg("+faststart");
cmd.arg(&self.output);
let status = cmd.status()?; let status = cmd.status()?;
if status.success() { if status.success() {

View file

@ -3,7 +3,7 @@
pub mod ffmpeg; pub mod ffmpeg;
mod filter; mod filter;
use self::filter::Filter; use self::{ffmpeg::FfmpegOutput, filter::Filter};
use crate::{ use crate::{
iotro::{intro, outro}, iotro::{intro, outro},
render::ffmpeg::{Ffmpeg, FfmpegInput}, render::ffmpeg::{Ffmpeg, FfmpegInput},
@ -117,10 +117,22 @@ pub(crate) struct Renderer<'a> {
target: PathBuf target: PathBuf
} }
fn svg2mp4(svg: PathBuf, mp4: PathBuf, duration: Time) -> anyhow::Result<()> { fn svg2mp4(
let mut ffmpeg = Ffmpeg::new(mp4); meta: &ProjectSourceMetadata,
svg: PathBuf,
mp4: PathBuf,
duration: Time
) -> anyhow::Result<()> {
let mut ffmpeg = Ffmpeg::new(FfmpegOutput {
fps: None,
duration: Some(duration),
time_base: Some(meta.source_tbn),
fps_mode_vfr: true,
path: mp4
});
ffmpeg.add_input(FfmpegInput { ffmpeg.add_input(FfmpegInput {
loop_input: true, loop_input: true,
fps: Some(meta.source_fps),
..FfmpegInput::new(svg) ..FfmpegInput::new(svg)
}); });
ffmpeg.add_filter(Filter::GenerateSilence { ffmpeg.add_filter(Filter::GenerateSilence {
@ -128,7 +140,6 @@ fn svg2mp4(svg: PathBuf, mp4: PathBuf, duration: Time) -> anyhow::Result<()> {
output: "out".into() output: "out".into()
}); });
ffmpeg.set_filter_output("out"); ffmpeg.set_filter_output("out");
ffmpeg.set_duration(duration);
ffmpeg.run() ffmpeg.run()
} }
@ -185,7 +196,7 @@ impl<'a> Renderer<'a> {
let source_sample_rate = let source_sample_rate =
ffprobe_audio("stream=sample_rate", &recording_txt)?.parse()?; ffprobe_audio("stream=sample_rate", &recording_txt)?.parse()?;
let recording_mp4 = self.recording_mp4(); let recording_mp4 = self.recording_mp4();
let mut ffmpeg = Ffmpeg::new(recording_mp4.clone()); let mut ffmpeg = Ffmpeg::new(FfmpegOutput::new(recording_mp4.clone()));
ffmpeg.add_input(FfmpegInput { ffmpeg.add_input(FfmpegInput {
concat: true, concat: true,
..FfmpegInput::new(recording_txt) ..FfmpegInput::new(recording_txt)
@ -210,6 +221,7 @@ impl<'a> Renderer<'a> {
source_res, source_res,
source_sample_rate source_sample_rate
}); });
let metadata = project.source.metadata.as_ref().unwrap();
println!("\x1B[1m ==> Preparing assets ...\x1B[0m"); println!("\x1B[1m ==> Preparing assets ...\x1B[0m");
@ -222,7 +234,7 @@ impl<'a> Renderer<'a> {
.into_bytes() .into_bytes()
)?; )?;
let intro_mp4 = self.target.join("intro.mp4"); let intro_mp4 = self.target.join("intro.mp4");
svg2mp4(intro_svg, intro_mp4, INTRO_LEN)?; svg2mp4(metadata, intro_svg, intro_mp4, INTRO_LEN)?;
// render outro to svg then mp4 // render outro to svg then mp4
let outro_svg = self.target.join("outro.svg"); let outro_svg = self.target.join("outro.svg");
@ -231,7 +243,7 @@ impl<'a> Renderer<'a> {
outro(source_res).to_string_pretty().into_bytes() outro(source_res).to_string_pretty().into_bytes()
)?; )?;
let outro_mp4 = self.target.join("outro.mp4"); let outro_mp4 = self.target.join("outro.mp4");
svg2mp4(outro_svg, outro_mp4, OUTRO_LEN)?; svg2mp4(metadata, outro_svg, outro_mp4, OUTRO_LEN)?;
// copy logo then render to png // copy logo then render to png
let logo_svg = self.target.join("logo.svg"); let logo_svg = self.target.join("logo.svg");
@ -272,7 +284,7 @@ impl<'a> Renderer<'a> {
pub(crate) fn render(&self, project: &mut Project) -> anyhow::Result<PathBuf> { pub(crate) fn render(&self, project: &mut Project) -> anyhow::Result<PathBuf> {
let output = self.video_mp4(project); let output = self.video_mp4(project);
let mut ffmpeg = Ffmpeg::new(output.clone()); let mut ffmpeg = Ffmpeg::new(FfmpegOutput::new(output.clone()));
// add all of our inputs // add all of our inputs
let intro = ffmpeg.add_input(FfmpegInput::new(self.target.join("intro.mp4"))); let intro = ffmpeg.add_input(FfmpegInput::new(self.target.join("intro.mp4")));