1use std::time::Duration;
8
9use crate::{EncoderSettings, Error, PixelFormat, RateControl, Size};
10use ffcine_core::color::named_enum;
11use std::fmt;
12use std::str::FromStr;
13
14named_enum!(
15 #[derive(Default)]
17 DeliveryProfile {
18 #[default]
20 Master = "master",
21 Preview = "preview",
23 Review = "review",
25 Phone = "phone",
27 }
28);
29
30pub const REVIEW_BUDGET: u64 = 50_000_000;
32
33pub const AUDIO_BITRATE: u64 = 160_000;
35
36#[derive(Debug, Clone, PartialEq)]
38pub struct Plan {
39 pub encoder: EncoderSettings,
40 pub size: Option<Size>,
42}
43
44const MIN_BPP: f64 = 0.035;
47
48const REVIEW_STEPS: [u32; 4] = [1080, 720, 540, 360];
50
51#[allow(
54 clippy::cast_possible_truncation,
55 clippy::cast_sign_loss,
56 clippy::cast_precision_loss
57)]
58pub fn fit_short_side(src: Size, short: u32) -> Size {
59 let s = src.width.min(src.height);
60 if s <= short {
61 return src;
62 }
63 let k = f64::from(short) / f64::from(s);
64 let even = |v: u32| ((f64::from(v) * k / 2.0).round() as u32 * 2).max(2);
65 Size {
66 width: even(src.width),
67 height: even(src.height),
68 }
69}
70
71impl DeliveryProfile {
72 #[allow(
75 clippy::cast_possible_truncation,
76 clippy::cast_sign_loss,
77 clippy::cast_precision_loss
78 )]
79 pub fn plan(self, src: Size, fps: f64, duration: Duration, budget: Option<u64>) -> Plan {
80 let gop = Some((fps * 2.0).round().max(1.0) as u32);
81 match self {
82 Self::Master => Plan {
83 encoder: EncoderSettings::nvenc_hevc_master(),
84 size: None,
85 },
86 Self::Preview => Plan {
87 encoder: EncoderSettings::x264_preview(),
88 size: None,
89 },
90 Self::Phone => {
91 let mut e = hevc_8bit(gop);
92 e.rate = RateControl::Crf(23.0);
93 e.extra
94 .extend(opts(&[("maxrate", "16000000"), ("bufsize", "32000000")]));
95 Plan {
96 encoder: e,
97 size: Some(fit_short_side(src, 1080)),
98 }
99 }
100 Self::Review => {
101 let bps = review_bitrate(budget.unwrap_or(REVIEW_BUDGET), duration);
102 let size = REVIEW_STEPS
103 .iter()
104 .map(|&s| fit_short_side(src, s))
105 .find(|s| {
106 bps as f64 / (f64::from(s.width) * f64::from(s.height) * fps.max(1.0))
107 >= MIN_BPP
108 })
109 .unwrap_or_else(|| fit_short_side(src, 360));
110 let mut e = hevc_8bit(gop);
111 e.rate = RateControl::Bitrate(bps);
112 let peak = (bps * 2).to_string();
113 e.extra.extend(opts(&[
114 ("multipass", "fullres"),
115 ("maxrate", &peak),
116 ("bufsize", &peak),
117 ]));
118 Plan {
119 encoder: e,
120 size: Some(size),
121 }
122 }
123 }
124 }
125}
126
127#[allow(
130 clippy::cast_possible_truncation,
131 clippy::cast_sign_loss,
132 clippy::cast_precision_loss
133)]
134pub fn review_bitrate(budget: u64, duration: Duration) -> u64 {
135 let secs = duration.as_secs_f64().max(1.0);
136 let total = budget as f64 * 8.0 * 0.96 / secs;
137 ((total - AUDIO_BITRATE as f64) as u64).max(200_000)
138}
139
140fn opts(kv: &[(&str, &str)]) -> Vec<(String, String)> {
141 kv.iter()
142 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
143 .collect()
144}
145
146fn hevc_8bit(gop: Option<u32>) -> EncoderSettings {
148 EncoderSettings {
149 preset: Some("p7".into()),
150 tune: Some("hq".into()),
151 profile: Some("main".into()),
152 pixel_format: PixelFormat::Nv12,
153 gop,
154 extra: opts(&[
155 ("rc", "vbr"),
156 ("rc-lookahead", "32"),
157 ("spatial-aq", "1"),
158 ("temporal-aq", "1"),
159 ]),
160 ..EncoderSettings::nvenc_hevc_master()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 const UHD: Size = Size {
169 width: 3840,
170 height: 2160,
171 };
172
173 #[test]
174 fn review_fits_the_budget() {
175 let d = Duration::from_secs(600);
177 let p = DeliveryProfile::Review.plan(UHD, 23.976, d, None);
178 let RateControl::Bitrate(b) = p.encoder.rate else {
179 panic!("review is bitrate-driven");
180 };
181 assert!((b + AUDIO_BITRATE) * 600 / 8 < REVIEW_BUDGET);
182 assert_eq!(p.size.map(|s| s.height), Some(540));
183 let p = DeliveryProfile::Review.plan(UHD, 23.976, Duration::from_secs(90), None);
185 assert_eq!(
186 p.size,
187 Some(Size {
188 width: 1920,
189 height: 1080
190 })
191 );
192 }
193
194 #[test]
195 fn phone_is_1080p_hevc_and_never_upscales() {
196 let p = DeliveryProfile::Phone.plan(UHD, 60.0, Duration::from_secs(60), None);
197 assert_eq!(p.size.map(|s| (s.width, s.height)), Some((1920, 1080)));
198 assert_eq!(p.encoder.profile.as_deref(), Some("main"));
199 let vertical = Size {
200 width: 1080,
201 height: 1920,
202 };
203 assert_eq!(fit_short_side(vertical, 1080), vertical);
204 }
205}