prepare writing ffmpeg commands

This commit is contained in:
Dominic 2023-10-28 23:38:17 +02:00
parent d59ca4aceb
commit 823d8ce5dc
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
6 changed files with 434 additions and 11 deletions

View file

@ -10,7 +10,10 @@ edition = "2021"
anyhow = "1.0" anyhow = "1.0"
camino = "1.1" camino = "1.1"
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.4", features = ["derive"] }
ffmpeg = { package = "ffmpeg-next", version = "6.0" } #ffmpeg = { package = "ffmpeg-next", version = "6.0" }
indexmap = "1.9"
rational = "1.4"
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
serde_with = "3.4" serde_with = "3.4"
svgwriter = "0.1"
toml = { package = "basic-toml", version = "0.1.4" } toml = { package = "basic-toml", version = "0.1.4" }

115
src/iotro.rs Normal file
View file

@ -0,0 +1,115 @@
//! A module for writing intros and outros
use crate::{
time::{format_date_long, Date},
Resolution
};
use svgwriter::{
tags::{Group, Rect, TagWithPresentationAttributes, Text},
Graphic
};
#[repr(u16)]
enum FontSize {
Huge = 72,
Large = 56,
Big = 44
}
#[repr(u16)]
enum FontWeight {
Normal = 400,
SemiBold = 500,
Bold = 700
}
struct Iotro {
res: Resolution,
g: Group
}
impl Iotro {
fn new(res: Resolution) -> Self {
Self {
res,
g: Group::new()
.with_fill("white")
.with_text_anchor("middle")
.with_dominant_baseline("hanging")
.with_font_family("Noto Sans")
}
}
fn add_text<T: Into<String>>(
&mut self,
font_size: FontSize,
font_weight: FontWeight,
y_top: usize,
content: T
) {
let mut text = Text::new()
.with_x(960)
.with_y(y_top)
.with_font_size(font_size as u16)
.with_font_weight(font_weight as u16);
text.push(content.into());
self.g.push(text);
}
fn finish(self) -> Graphic {
let mut svg = Graphic::new();
svg.set_width(self.res.width());
svg.set_height(self.res.height());
svg.set_view_box("0 0 1920 1080");
svg.push(
Rect::new()
.with_fill("black")
.with_x(0)
.with_y(0)
.with_width(1920)
.with_height(1080)
);
svg.push(self.g);
svg
}
}
pub(crate) fn intro(res: Resolution, date: Date) -> Graphic {
use self::{FontSize::*, FontWeight::*};
let mut intro = Iotro::new(res);
intro.add_text(Huge, Bold, 110, "Mathematische Logik II");
intro.add_text(Huge, SemiBold, 250, "Prof. E. Grädel");
intro.add_text(
Huge,
SemiBold,
460,
format!("Vorlesung vom {}", format_date_long(date))
);
intro.add_text(
Big,
Normal,
870,
"Video erstellt von der Video AG, Fachschaft I/1"
);
intro.add_text(Big, Normal, 930, "https://video.fsmpi.rwth-aachen.de");
intro.add_text(Big, Normal, 990, "video@fsmpi.rwth-aachen.de");
intro.finish()
}
pub(crate) fn outro(res: Resolution) -> Graphic {
use self::{FontSize::*, FontWeight::*};
let mut outro = Iotro::new(res);
outro.add_text(Large, SemiBold, 50, "Video erstellt von der");
outro.add_text(Huge, Bold, 210, "Video AG, Fachschaft I/1");
outro.add_text(Large, Normal, 360, "Website der Fachschaft:");
outro.add_text(Large, Normal, 430, "https://www.fsmpi.rwth-aachen.de");
outro.add_text(Large, Normal, 570, "Videos herunterladen:");
outro.add_text(Large, Normal, 640, "https://video.fsmpi.rwth-aachen.de");
outro.add_text(Large, Normal, 780, "Fragen, Vorschläge und Feedback:");
outro.add_text(Large, Normal, 850, "video@fsmpi.rwth-aachen.de");
outro.finish()
}

View file

@ -1,14 +1,18 @@
#![warn(rust_2018_idioms)] #![warn(rust_2018_idioms)]
#![forbid(elided_lifetimes_in_paths, unsafe_code)] #![forbid(elided_lifetimes_in_paths, unsafe_code)]
mod iotro;
mod render;
mod time; mod time;
use crate::time::{parse_date, parse_time, Date, Time}; use crate::time::{parse_date, parse_time, Date, Time};
use camino::Utf8PathBuf as PathBuf; use camino::Utf8PathBuf as PathBuf;
use clap::Parser; use clap::Parser;
use rational::Rational;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr}; use serde_with::{serde_as, DisplayFromStr, FromInto};
use std::{ use std::{
collections::BTreeSet,
fmt::Display, fmt::Display,
fs, fs,
io::{self, BufRead as _, Write} io::{self, BufRead as _, Write}
@ -23,10 +27,48 @@ struct Args {
course: String course: String
} }
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
enum Resolution {
/// 640x360
nHD,
/// 1280x720
HD,
/// 1920x1080
FullHD,
/// 2560x1440
WQHD,
/// 3840x2160
UHD
}
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
}
}
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct Project { struct Project {
lecture: ProjectLecture, lecture: ProjectLecture,
source: ProjectSource source: ProjectSource,
progress: ProjectProgress
} }
#[serde_as] #[serde_as]
@ -41,10 +83,37 @@ struct ProjectLecture {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct ProjectSource { struct ProjectSource {
files: Vec<String>, files: Vec<String>,
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "DisplayFromStr")]
first_file_start: Time, first_file_start: Time,
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "DisplayFromStr")]
last_file_end: Time last_file_end: Time,
metadata: Option<ProjectSourceMetadata>
}
#[serde_as]
#[derive(Deserialize, Serialize)]
struct ProjectSourceMetadata {
/// The duration of the source video.
#[serde_as(as = "DisplayFromStr")]
source_duration: Time,
/// The FPS of the source video.
#[serde_as(as = "FromInto<(i128, i128)>")]
source_fps: Rational,
/// The time base of the source video.
source_tbn: u32,
/// The resolution of the source video.
source_res: Resolution,
/// The sample rate of the source audio.
source_sample_rate: u32
}
#[derive(Default, Deserialize, Serialize)]
struct ProjectProgress {
preprocessed: bool,
rendered: bool,
transcoded: BTreeSet<Resolution>
} }
fn ask_time(question: impl Display) -> Time { fn ask_time(question: impl Display) -> Time {
@ -111,12 +180,15 @@ fn main() {
source: ProjectSource { source: ProjectSource {
files, files,
first_file_start, first_file_start,
last_file_end last_file_end,
} metadata: None
},
progress: Default::default()
}; };
fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap(); fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
project project
}; };
println!("{}", toml::to_string(&project).unwrap()); println!("{}", toml::to_string(&project).unwrap());
// render(&directory, &project).unwrap();
} }

77
src/render/ffmpeg.rs Normal file
View file

@ -0,0 +1,77 @@
use super::cmd;
use crate::time::{format_time, Time};
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
use rational::Rational;
use std::process::Command;
pub(crate) struct FfmpegInput {
pub(crate) loop_input: bool,
pub(crate) fps: Option<Rational>,
pub(crate) start: Option<Time>,
pub(crate) duration: Option<Time>,
pub(crate) path: PathBuf
}
impl FfmpegInput {
pub(crate) fn new(path: PathBuf) -> Self {
Self {
loop_input: false,
fps: None,
start: None,
duration: None,
path
}
}
fn append_to_cmd(self, cmd: &mut Command) {
if self.loop_input {
cmd.arg("-loop").arg("1");
}
if let Some(fps) = self.fps {
cmd.arg("-r").arg(fps.to_string());
}
if let Some(start) = self.start {
cmd.arg("-ss").arg(format_time(start));
}
if let Some(duration) = self.duration {
cmd.arg("-t").arg(format_time(duration));
}
cmd.arg("-i").arg(self.path);
}
}
pub(crate) struct Ffmpeg {
inputs: Vec<FfmpegInput>,
output: PathBuf
}
impl Ffmpeg {
pub fn new(output: PathBuf) -> Self {
Self {
inputs: Vec::new(),
output
}
}
pub fn run(self) -> anyhow::Result<()> {
let mut cmd = cmd();
cmd.arg("ffmpeg").arg("-hide_banner");
// initialise a vaapi device if one exists
let vaapi_device: PathBuf = "/dev/dri/renderD128".into();
let vaapi = vaapi_device.exists();
if vaapi {
cmd.arg("-vaapi_device").arg(&vaapi_device);
}
// append all the inputs
for i in self.inputs {
i.append_to_cmd(&mut cmd);
}
// always try to synchronise audio
cmd.arg("-async").arg("1");
unimplemented!()
}
}

156
src/render/mod.rs Normal file
View file

@ -0,0 +1,156 @@
#![allow(warnings)]
pub mod ffmpeg;
use crate::{
iotro::intro,
time::{format_date, Time},
Project, ProjectSourceMetadata, Resolution
};
use anyhow::{bail, Context};
use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
use rational::Rational;
use std::{
fs::{self, File},
io::Write as _,
process::{Command, Stdio}
};
const INTRO_LEN: Time = Time {
seconds: 3,
micros: 0
};
const OUTRO_LEN: Time = Time {
seconds: 5,
micros: 0
};
const TRANSITION: &str = "fadeblack";
const TRANSITION_LEN: Time = Time {
seconds: 0,
micros: 200_000
};
fn cmd() -> Command {
let mut cmd = Command::new("busybox");
cmd.arg("ash")
.arg("-exuo")
.arg("pipefail")
.arg("-c")
.arg("exec");
cmd
}
fn ffprobe() -> Command {
let mut cmd = cmd();
cmd.arg("ffprobe")
.arg("-v")
.arg("error")
.arg("-of")
.arg("default=noprint_wrappers=1:nokey=1");
cmd
}
fn read_output(cmd: &mut Command) -> anyhow::Result<String> {
let out = cmd.stderr(Stdio::inherit()).output()?;
if !out.status.success() {
bail!(
"Executed command failed with exit status {:?}",
out.status.code()
);
}
String::from_utf8(out.stdout).context("Command returned non-utf8 output")
}
fn ffprobe_video(query: &str, input: &Path) -> anyhow::Result<String> {
read_output(
ffprobe()
.arg("-select_streams")
.arg("v:0")
.arg("-show_entries")
.arg(query)
.arg(input)
)
}
fn ffprobe_audio(query: &str, concat_input: &Path) -> anyhow::Result<String> {
read_output(
ffprobe()
.arg("-select_streams")
.arg("a:0")
.arg("-show_entries")
.arg(query)
.arg("-safe")
.arg("0")
.arg("-f")
.arg("concat")
.arg(concat_input)
)
}
fn ffmpeg() -> Command {
let mut cmd = cmd();
cmd.arg("ffmpeg")
.arg("-hide_banner")
.arg("-vaapi_device")
.arg("/dev/dri/renderD128");
cmd
}
fn render_svg(fps: Rational, tbn: u32, input: &Path, duration: Time, output: &Path) {
let mut cmd = ffmpeg();
cmd.arg("-framerate").arg(fps.to_string());
cmd.arg("-loop").arg("1");
cmd.arg("-i").arg(input);
cmd.arg("-c:v").arg("libx264");
}
pub(crate) struct Renderer<'a> {
/// The directory with all the sources.
directory: &'a Path,
/// The slug (i.e. 23ws-malo2-231016).
slug: String,
/// The target directory.
target: PathBuf
}
impl<'a> Renderer<'a> {
pub(crate) fn new(directory: &'a Path, project: &Project) -> anyhow::Result<Self> {
let slug = format!(
"{}-{}",
project.lecture.course,
format_date(project.lecture.date)
);
let target = directory.join(&slug);
Ok(Self {
directory,
slug,
target
})
}
pub(crate) fn preprocess(&self, project: &mut Project) -> anyhow::Result<()> {
assert!(!project.progress.preprocessed);
let recording_txt = self.target.join("recording.txt");
let mut file = File::create(recording_txt)?;
for filename in &project.source.files {
writeln!(file, "file {:?}", self.directory.join(filename).to_string());
}
drop(file);
println!("\x1B[1m ==> Concatenating Video and Normalising Audio ...");
let mut ffmpeg = Ffmpeg::new();
// project.source.metadata = Some(ProjectSourceMetadata {
// source_duration: ffprobe_video("format=duration", input)?.parse()?
// });
let intro_svg = self.target.join("intro.svg");
// fs::write(&intro_svg, intro(res, date));
let intro_mp4 = self.target.join("intro.mp4");
Ok(())
}
}

View file

@ -6,9 +6,9 @@ use std::{
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Date { pub struct Date {
year: u16, pub year: u16,
month: u8, pub month: u8,
day: u8 pub day: u8
} }
impl FromStr for Date { impl FromStr for Date {
@ -78,8 +78,8 @@ pub fn format_date_long(d: Date) -> String {
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Time { pub struct Time {
seconds: u32, pub seconds: u32,
micros: u32 pub micros: u32
} }
impl FromStr for Time { impl FromStr for Time {