Skip to content
Snippets Groups Projects
Unverified Commit 823d8ce5 authored by Dominic Meiser's avatar Dominic Meiser
Browse files

prepare writing ffmpeg commands

parent d59ca4ac
No related branches found
No related tags found
No related merge requests found
......@@ -10,7 +10,10 @@ edition = "2021"
anyhow = "1.0"
camino = "1.1"
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_with = "3.4"
svgwriter = "0.1"
toml = { package = "basic-toml", version = "0.1.4" }
//! 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()
}
#![warn(rust_2018_idioms)]
#![forbid(elided_lifetimes_in_paths, unsafe_code)]
mod iotro;
mod render;
mod time;
use crate::time::{parse_date, parse_time, Date, Time};
use camino::Utf8PathBuf as PathBuf;
use clap::Parser;
use rational::Rational;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use serde_with::{serde_as, DisplayFromStr, FromInto};
use std::{
collections::BTreeSet,
fmt::Display,
fs,
io::{self, BufRead as _, Write}
......@@ -23,10 +27,48 @@ struct Args {
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)]
struct Project {
lecture: ProjectLecture,
source: ProjectSource
source: ProjectSource,
progress: ProjectProgress
}
#[serde_as]
......@@ -41,10 +83,37 @@ struct ProjectLecture {
#[derive(Deserialize, Serialize)]
struct ProjectSource {
files: Vec<String>,
#[serde_as(as = "DisplayFromStr")]
first_file_start: Time,
#[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 {
......@@ -111,12 +180,15 @@ fn main() {
source: ProjectSource {
files,
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();
project
};
println!("{}", toml::to_string(&project).unwrap());
// render(&directory, &project).unwrap();
}
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!()
}
}
#![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(())
}
}
......@@ -6,9 +6,9 @@ use std::{
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Date {
year: u16,
month: u8,
day: u8
pub year: u16,
pub month: u8,
pub day: u8
}
impl FromStr for Date {
......@@ -78,8 +78,8 @@ pub fn format_date_long(d: Date) -> String {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Time {
seconds: u32,
micros: u32
pub seconds: u32,
pub micros: u32
}
impl FromStr for Time {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment