use ffcine_reframe::Profile as Aspect;
use ffcine_render::{EncoderSettings, RateControl, Size};
use ffcine_timeline::Output;
use serde_json::{Value, json};
#[derive(Debug, Clone, PartialEq)]
pub struct Profile {
pub name: String,
pub size: Size,
pub aspect: Aspect,
pub encoder: EncoderSettings,
pub audio_bitrate: u32,
}
const PREVIEW_SHORT: u32 = 1080;
const MASTER_SHORT: u32 = 2160;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn sized(aspect: &Aspect, short: u32) -> Size {
let ar = aspect.aspect();
let even = |v: f64| ((v / 2.0).round() as u32 * 2).max(2);
if ar >= 1.0 {
Size {
width: even(f64::from(short) * ar),
height: short,
}
} else {
Size {
width: short,
height: even(f64::from(short) / ar),
}
}
}
impl Profile {
fn landscape(short: u32) -> Aspect {
let mut a = Aspect::preset("landscape").unwrap_or_else(|| Aspect::new("landscape", 16, 9));
let s = sized(&a, short);
(a.width, a.height) = (s.width, s.height);
a
}
pub fn preview() -> Self {
Self {
name: "preview".into(),
size: Size {
width: 1920,
height: 1080,
},
aspect: Self::landscape(PREVIEW_SHORT),
encoder: EncoderSettings {
preset: Some("p4".into()),
rate: RateControl::ConstQp(22),
..EncoderSettings::nvenc_hevc_master()
},
audio_bitrate: 192_000,
}
}
pub fn master() -> Self {
Self {
name: "master".into(),
size: Size {
width: 3840,
height: 2160,
},
aspect: Self::landscape(MASTER_SHORT),
encoder: EncoderSettings::nvenc_hevc_master(),
audio_bitrate: 320_000,
}
}
pub fn by_name(name: &str) -> Option<Self> {
match name {
"preview" => Some(Self::preview()),
"master" | "final" => Some(Self::master()),
_ => None,
}
}
pub fn resolve(name: &str, output: &Output, aspect: Option<&str>) -> Result<Self, String> {
let mut p = Self::by_name(name)
.ok_or_else(|| format!("unknown profile {name:?} (preview, master)"))?;
let mode = if p.name == "master" {
&output.master
} else {
&output.preview
};
let spec = aspect.or(output.aspect.as_deref()).unwrap_or("landscape");
let mut a: Aspect = spec.parse().map_err(|e| format!("render profile: {e}"))?;
let preset = Aspect::preset(spec).is_some();
let short = if p.name == "master" {
MASTER_SHORT
} else {
PREVIEW_SHORT
};
let size = match mode.size {
Some([w, h]) if aspect.is_none() && w > 0 && h > 0 => Size {
width: w,
height: h,
},
_ if preset => sized(&a, short),
_ => Size {
width: a.width,
height: a.height,
},
};
(a.width, a.height) = (size.width, size.height);
if let Some(m) = mode.max_punch {
a.max_punch = m;
}
p.size = size;
p.aspect = a;
Ok(p)
}
pub fn recipe(&self) -> Value {
json!({
"size": [self.size.width, self.size.height],
"aspect": self.aspect.name,
"max_punch": self.aspect.max_punch,
"encoder": format!("{:?}", self.encoder),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aspects_and_sizes() {
let out = Output::default();
let p = Profile::resolve("preview", &out, None).unwrap();
assert_eq!(p, Profile::preview());
let size = |q: &str, a: &str| {
let p = Profile::resolve(q, &out, Some(a)).unwrap();
(p.aspect.name.clone(), p.size.width, p.size.height)
};
assert_eq!(size("preview", "reel"), ("reel".into(), 1080, 1920));
assert_eq!(size("master", "9:16"), ("reel".into(), 2160, 3840));
assert_eq!(size("preview", "4:5"), ("feed".into(), 1080, 1350));
assert_eq!(size("master", "square"), ("square".into(), 2160, 2160));
assert_eq!(
size("master", "story=720x1280"),
("story".into(), 720, 1280)
);
let out: Output = serde_json::from_value(json!({
"aspect": "reel", "preview": {"size": [720, 1280], "max_punch": 1.12}
}))
.unwrap();
let p = Profile::resolve("preview", &out, None).unwrap();
assert_eq!(
(p.size.width, p.size.height, p.aspect.max_punch),
(720, 1280, 1.12)
);
let p = Profile::resolve("preview", &out, Some("square")).unwrap();
assert_eq!((p.size.width, p.size.height), (1080, 1080));
assert!(Profile::resolve("preview", &out, Some("bogus")).is_err());
assert_ne!(
Profile::preview().recipe(),
Profile::resolve("preview", &Output::default(), Some("reel"))
.unwrap()
.recipe()
);
}
}