use std::time::Duration;
use crate::{EncoderSettings, Error, PixelFormat, RateControl, Size};
use ffcine_core::color::named_enum;
use std::fmt;
use std::str::FromStr;
named_enum!(
#[derive(Default)]
DeliveryProfile {
#[default]
Master = "master",
Preview = "preview",
Review = "review",
Phone = "phone",
}
);
pub const REVIEW_BUDGET: u64 = 50_000_000;
pub const AUDIO_BITRATE: u64 = 160_000;
#[derive(Debug, Clone, PartialEq)]
pub struct Plan {
pub encoder: EncoderSettings,
pub size: Option<Size>,
}
const MIN_BPP: f64 = 0.035;
const REVIEW_STEPS: [u32; 4] = [1080, 720, 540, 360];
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub fn fit_short_side(src: Size, short: u32) -> Size {
let s = src.width.min(src.height);
if s <= short {
return src;
}
let k = f64::from(short) / f64::from(s);
let even = |v: u32| ((f64::from(v) * k / 2.0).round() as u32 * 2).max(2);
Size {
width: even(src.width),
height: even(src.height),
}
}
impl DeliveryProfile {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub fn plan(self, src: Size, fps: f64, duration: Duration, budget: Option<u64>) -> Plan {
let gop = Some((fps * 2.0).round().max(1.0) as u32);
match self {
Self::Master => Plan {
encoder: EncoderSettings::nvenc_hevc_master(),
size: None,
},
Self::Preview => Plan {
encoder: EncoderSettings::x264_preview(),
size: None,
},
Self::Phone => {
let mut e = hevc_8bit(gop);
e.rate = RateControl::Crf(23.0);
e.extra
.extend(opts(&[("maxrate", "16000000"), ("bufsize", "32000000")]));
Plan {
encoder: e,
size: Some(fit_short_side(src, 1080)),
}
}
Self::Review => {
let bps = review_bitrate(budget.unwrap_or(REVIEW_BUDGET), duration);
let size = REVIEW_STEPS
.iter()
.map(|&s| fit_short_side(src, s))
.find(|s| {
bps as f64 / (f64::from(s.width) * f64::from(s.height) * fps.max(1.0))
>= MIN_BPP
})
.unwrap_or_else(|| fit_short_side(src, 360));
let mut e = hevc_8bit(gop);
e.rate = RateControl::Bitrate(bps);
let peak = (bps * 2).to_string();
e.extra.extend(opts(&[
("multipass", "fullres"),
("maxrate", &peak),
("bufsize", &peak),
]));
Plan {
encoder: e,
size: Some(size),
}
}
}
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub fn review_bitrate(budget: u64, duration: Duration) -> u64 {
let secs = duration.as_secs_f64().max(1.0);
let total = budget as f64 * 8.0 * 0.96 / secs;
((total - AUDIO_BITRATE as f64) as u64).max(200_000)
}
fn opts(kv: &[(&str, &str)]) -> Vec<(String, String)> {
kv.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect()
}
fn hevc_8bit(gop: Option<u32>) -> EncoderSettings {
EncoderSettings {
preset: Some("p7".into()),
tune: Some("hq".into()),
profile: Some("main".into()),
pixel_format: PixelFormat::Nv12,
gop,
extra: opts(&[
("rc", "vbr"),
("rc-lookahead", "32"),
("spatial-aq", "1"),
("temporal-aq", "1"),
]),
..EncoderSettings::nvenc_hevc_master()
}
}
#[cfg(test)]
mod tests {
use super::*;
const UHD: Size = Size {
width: 3840,
height: 2160,
};
#[test]
fn review_fits_the_budget() {
let d = Duration::from_secs(600);
let p = DeliveryProfile::Review.plan(UHD, 23.976, d, None);
let RateControl::Bitrate(b) = p.encoder.rate else {
panic!("review is bitrate-driven");
};
assert!((b + AUDIO_BITRATE) * 600 / 8 < REVIEW_BUDGET);
assert_eq!(p.size.map(|s| s.height), Some(540));
let p = DeliveryProfile::Review.plan(UHD, 23.976, Duration::from_secs(90), None);
assert_eq!(
p.size,
Some(Size {
width: 1920,
height: 1080
})
);
}
#[test]
fn phone_is_1080p_hevc_and_never_upscales() {
let p = DeliveryProfile::Phone.plan(UHD, 60.0, Duration::from_secs(60), None);
assert_eq!(p.size.map(|s| (s.width, s.height)), Some((1920, 1080)));
assert_eq!(p.encoder.profile.as_deref(), Some("main"));
let vertical = Size {
width: 1080,
height: 1920,
};
assert_eq!(fit_short_side(vertical, 1080), vertical);
}
}