#![allow(warnings)] pub mod ffmpeg; mod filter; use crate::{ iotro::intro, render::ffmpeg::Ffmpeg, 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 { 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 { 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 { 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 { 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 logo = self.target.join("logo.svg"); fs::write( &logo, include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/logo.svg")) )?; let fastforward = self.target.join("fastforward.svg"); fs::write( &fastforward, include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/assets/fastforward.svg" )) )?; 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 recording_mp4 = self.target.join("recording.mp4"); let mut ffmpeg = Ffmpeg::new(recording_mp4); // 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(()) } }