mod av1;
mod ivf;
pub(crate) mod montage;
mod poster;
mod probe;
mod size;
mod source;
pub(crate) use poster::poster;
use std::path::{Path, PathBuf};
use std::process::Command;
use yog_formats::video::{BEST_QUALITY, Video, WORST_QUALITY};
use crate::ffmpeg::{INPUT, STDOUT, Scratch, run};
const AV1_OUT: &[&str] = &[
"-c:v",
"libsvtav1",
"-svtav1-params",
"keyint=2s",
"-pix_fmt",
"yuv420p",
"-colorspace",
"bt709",
"-color_range",
"tv",
"-f",
"ivf",
STDOUT,
];
const SVT_LOG: (&str, &str) = ("SVT_LOG", "1");
#[derive(Debug)]
pub(crate) struct Rendered {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) fps_num: u32,
pub(crate) fps_den: u32,
pub(crate) sizes: Vec<u32>,
pub(crate) packets: Vec<u8>,
pub(crate) keyframes: Vec<u32>,
pub(crate) sound: Option<Vec<u8>>,
}
impl Rendered {
pub(crate) fn def(
self,
id: String,
v: &Video,
sound: Option<String>,
) -> yog_registry::VideoDef {
yog_registry::VideoDef {
id,
width: self.width,
height: self.height,
fps_num: self.fps_num,
fps_den: self.fps_den,
sizes: self.sizes,
packets: self.packets,
keyframes: self.keyframes,
sound,
volume: v.volume,
looping: v.looping,
}
}
}
enum Made<'a> {
Source(PathBuf),
Montage(&'a yog_formats::video::Montage, Vec<montage::Placed<'a>>),
}
fn made<'a>(v: &'a Video, root: &Path, dir: &Scratch) -> Result<Made<'a>, String> {
match (&v.source, &v.montage) {
(Some(r), None) => {
let bytes = crate::media::video(root, &r.set, &r.key)
.ok_or_else(|| format!("no video `{r}` in the mod's .yres"))?;
Ok(Made::Source(dir.input(INPUT, &bytes)?))
}
(None, Some(m)) => Ok(Made::Montage(m, montage::lay(m, root, dir)?)),
_ => Err("a video shows a `.yres` video (`source`) or a `montage`".into()),
}
}
fn picture(v: &Video, made: &Made) -> Result<(Command, (u32, u32)), String> {
match made {
Made::Source(input) => source::picture(v, input),
Made::Montage(_, placed) => montage::picture(v, placed),
}
}
pub(crate) fn render(v: &Video, root: &Path) -> Result<Rendered, String> {
if v.quality > WORST_QUALITY {
return Err(format!(
"quality {}: AV1's is {BEST_QUALITY} (best) to {WORST_QUALITY}",
v.quality
));
}
let dir = Scratch::new()?;
let made = made(v, root, &dir)?;
let (mut cmd, (width, height)) = picture(v, &made)?;
cmd.env(SVT_LOG.0, SVT_LOG.1)
.args(["-crf", &v.quality.to_string()])
.args(AV1_OUT);
let ivf = run(&mut cmd, "encode the video as AV1")?;
let sound = match &made {
Made::Source(input) => source::sound(v, input)?,
Made::Montage(m, placed) => montage::sound(v, m, placed, root)?,
};
Ok(Rendered {
width,
height,
sound,
..cut(&ivf)?
})
}
fn cut(bytes: &[u8]) -> Result<Rendered, String> {
let ivf = ivf::parse(bytes)?;
if ivf.packets.is_empty() {
return Err("the video has no frames".into());
}
let keyframes = (0u32..)
.zip(&ivf.packets)
.filter(|(_, p)| av1::is_keyframe(p))
.map(|(i, _)| i)
.collect::<Vec<_>>();
if keyframes.first() != Some(&0) {
return Err("the video's first frame isn't a keyframe".into());
}
Ok(Rendered {
width: ivf.width,
height: ivf.height,
fps_num: ivf.rate,
fps_den: ivf.scale,
sizes: ivf.packets.iter().map(|p| p.len() as u32).collect(),
packets: ivf.packets.concat(),
keyframes,
sound: None,
})
}
#[cfg(test)]
pub(crate) mod tests;