support 50fps videos; enable +faststart
This commit is contained in:
parent
0865076849
commit
2882fb286a
3 changed files with 65 additions and 26 deletions
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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")));
|
||||||
|
|
Loading…
Reference in a new issue