1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum ProxyCodecChoice {
12 H264,
14 H265,
16 Vp9,
18 Av1,
20 ProResProxy,
22 DnxhdLb,
24}
25
26impl ProxyCodecChoice {
27 #[must_use]
29 pub fn name(&self) -> &'static str {
30 match self {
31 Self::H264 => "h264",
32 Self::H265 => "hevc",
33 Self::Vp9 => "vp9",
34 Self::Av1 => "av1",
35 Self::ProResProxy => "prores_ks",
36 Self::DnxhdLb => "dnxhd",
37 }
38 }
39
40 #[must_use]
42 pub fn container(&self) -> &'static str {
43 match self {
44 Self::H264 | Self::H265 | Self::Vp9 | Self::Av1 => "mp4",
45 Self::ProResProxy => "mov",
46 Self::DnxhdLb => "mxf",
47 }
48 }
49
50 #[must_use]
52 pub fn hardware_accelerated(&self) -> bool {
53 matches!(self, Self::H264 | Self::H265)
54 }
55}
56
57impl Default for ProxyCodecChoice {
58 fn default() -> Self {
59 Self::H264
60 }
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub struct BitrateLadderRung {
66 pub label: String,
68 pub width: u32,
70 pub height: u32,
72 pub bitrate_bps: u64,
74 pub codec: ProxyCodecChoice,
76}
77
78impl BitrateLadderRung {
79 #[must_use]
81 pub fn new(
82 label: impl Into<String>,
83 width: u32,
84 height: u32,
85 bitrate_bps: u64,
86 codec: ProxyCodecChoice,
87 ) -> Self {
88 Self {
89 label: label.into(),
90 width,
91 height,
92 bitrate_bps,
93 codec,
94 }
95 }
96
97 #[must_use]
99 pub fn pixel_count(&self) -> u64 {
100 u64::from(self.width) * u64::from(self.height)
101 }
102
103 #[must_use]
105 pub fn bits_per_pixel_at_24fps(&self) -> f64 {
106 let pixels_per_second = self.pixel_count() as f64 * 24.0;
107 if pixels_per_second <= 0.0 {
108 return 0.0;
109 }
110 self.bitrate_bps as f64 / pixels_per_second
111 }
112}
113
114#[derive(Debug, Clone, Default)]
116pub struct ProxyBitrateLadder {
117 rungs: Vec<BitrateLadderRung>,
119}
120
121impl ProxyBitrateLadder {
122 #[must_use]
124 pub fn new() -> Self {
125 Self::default()
126 }
127
128 #[must_use]
130 pub fn standard_h264() -> Self {
131 let mut ladder = Self::new();
132 ladder.add_rung(BitrateLadderRung::new(
133 "1080p",
134 1920,
135 1080,
136 8_000_000,
137 ProxyCodecChoice::H264,
138 ));
139 ladder.add_rung(BitrateLadderRung::new(
140 "720p",
141 1280,
142 720,
143 4_000_000,
144 ProxyCodecChoice::H264,
145 ));
146 ladder.add_rung(BitrateLadderRung::new(
147 "540p",
148 960,
149 540,
150 2_000_000,
151 ProxyCodecChoice::H264,
152 ));
153 ladder.add_rung(BitrateLadderRung::new(
154 "quarter",
155 480,
156 270,
157 800_000,
158 ProxyCodecChoice::H264,
159 ));
160 ladder
161 }
162
163 pub fn add_rung(&mut self, rung: BitrateLadderRung) {
165 self.rungs.push(rung);
166 }
167
168 #[must_use]
170 pub fn rung_count(&self) -> usize {
171 self.rungs.len()
172 }
173
174 #[must_use]
176 pub fn highest_quality_rung(&self) -> Option<&BitrateLadderRung> {
177 self.rungs.iter().max_by_key(|r| r.bitrate_bps)
178 }
179
180 #[must_use]
182 pub fn lowest_quality_rung(&self) -> Option<&BitrateLadderRung> {
183 self.rungs.iter().min_by_key(|r| r.bitrate_bps)
184 }
185
186 #[must_use]
188 pub fn find_by_label(&self, label: &str) -> Option<&BitrateLadderRung> {
189 self.rungs.iter().find(|r| r.label == label)
190 }
191
192 #[must_use]
194 pub fn rungs(&self) -> &[BitrateLadderRung] {
195 &self.rungs
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum QualityPreset {
202 UltraLow,
204 Low,
206 Medium,
208 High,
210}
211
212impl Default for QualityPreset {
213 fn default() -> Self {
214 Self::Low
215 }
216}
217
218#[derive(Debug, Clone)]
220pub struct ProxyTranscodeSettings {
221 pub codec: ProxyCodecChoice,
223 pub bitrate_bps: u64,
225 pub width: u32,
227 pub height: u32,
229 pub crf: u8,
231 pub threads: u32,
233 pub copy_audio: bool,
235}
236
237impl ProxyTranscodeSettings {
238 #[must_use]
240 pub fn from_preset_1080p(preset: QualityPreset) -> Self {
241 match preset {
242 QualityPreset::UltraLow => Self {
243 codec: ProxyCodecChoice::H264,
244 bitrate_bps: 1_000_000,
245 width: 480,
246 height: 270,
247 crf: 35,
248 threads: 0,
249 copy_audio: true,
250 },
251 QualityPreset::Low => Self {
252 codec: ProxyCodecChoice::H264,
253 bitrate_bps: 3_000_000,
254 width: 960,
255 height: 540,
256 crf: 28,
257 threads: 0,
258 copy_audio: true,
259 },
260 QualityPreset::Medium => Self {
261 codec: ProxyCodecChoice::H264,
262 bitrate_bps: 6_000_000,
263 width: 1280,
264 height: 720,
265 crf: 23,
266 threads: 0,
267 copy_audio: true,
268 },
269 QualityPreset::High => Self {
270 codec: ProxyCodecChoice::H265,
271 bitrate_bps: 12_000_000,
272 width: 1920,
273 height: 1080,
274 crf: 18,
275 threads: 0,
276 copy_audio: true,
277 },
278 }
279 }
280
281 #[must_use]
283 pub fn with_codec(mut self, codec: ProxyCodecChoice) -> Self {
284 self.codec = codec;
285 self
286 }
287
288 #[must_use]
290 pub fn with_threads(mut self, threads: u32) -> Self {
291 self.threads = threads;
292 self
293 }
294
295 #[must_use]
297 pub fn with_crf(mut self, crf: u8) -> Self {
298 self.crf = crf;
299 self
300 }
301
302 #[must_use]
304 pub fn resolution(&self) -> (u32, u32) {
305 (self.width, self.height)
306 }
307
308 #[must_use]
310 pub fn estimated_size_mb(&self, duration_secs: f64) -> f64 {
311 (self.bitrate_bps as f64 * duration_secs) / (8.0 * 1_000_000.0)
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_codec_name() {
321 assert_eq!(ProxyCodecChoice::H264.name(), "h264");
322 assert_eq!(ProxyCodecChoice::H265.name(), "hevc");
323 assert_eq!(ProxyCodecChoice::Vp9.name(), "vp9");
324 assert_eq!(ProxyCodecChoice::ProResProxy.name(), "prores_ks");
325 }
326
327 #[test]
328 fn test_codec_container() {
329 assert_eq!(ProxyCodecChoice::H264.container(), "mp4");
330 assert_eq!(ProxyCodecChoice::ProResProxy.container(), "mov");
331 assert_eq!(ProxyCodecChoice::DnxhdLb.container(), "mxf");
332 }
333
334 #[test]
335 fn test_codec_hardware_accelerated() {
336 assert!(ProxyCodecChoice::H264.hardware_accelerated());
337 assert!(ProxyCodecChoice::H265.hardware_accelerated());
338 assert!(!ProxyCodecChoice::Av1.hardware_accelerated());
339 assert!(!ProxyCodecChoice::Vp9.hardware_accelerated());
340 }
341
342 #[test]
343 fn test_codec_default() {
344 assert_eq!(ProxyCodecChoice::default(), ProxyCodecChoice::H264);
345 }
346
347 #[test]
348 fn test_bitrate_ladder_rung_pixel_count() {
349 let rung = BitrateLadderRung::new("1080p", 1920, 1080, 8_000_000, ProxyCodecChoice::H264);
350 assert_eq!(rung.pixel_count(), 1920 * 1080);
351 }
352
353 #[test]
354 fn test_bitrate_ladder_rung_bits_per_pixel() {
355 let rung = BitrateLadderRung::new("test", 100, 100, 2_400_000, ProxyCodecChoice::H264);
356 let bpp = rung.bits_per_pixel_at_24fps();
358 assert!((bpp - 10.0).abs() < 1e-6);
359 }
360
361 #[test]
362 fn test_proxy_bitrate_ladder_standard_h264() {
363 let ladder = ProxyBitrateLadder::standard_h264();
364 assert_eq!(ladder.rung_count(), 4);
365 }
366
367 #[test]
368 fn test_proxy_bitrate_ladder_highest_quality() {
369 let ladder = ProxyBitrateLadder::standard_h264();
370 let rung = ladder
371 .highest_quality_rung()
372 .expect("should succeed in test");
373 assert_eq!(rung.bitrate_bps, 8_000_000);
374 }
375
376 #[test]
377 fn test_proxy_bitrate_ladder_lowest_quality() {
378 let ladder = ProxyBitrateLadder::standard_h264();
379 let rung = ladder
380 .lowest_quality_rung()
381 .expect("should succeed in test");
382 assert_eq!(rung.bitrate_bps, 800_000);
383 }
384
385 #[test]
386 fn test_proxy_bitrate_ladder_find_by_label() {
387 let ladder = ProxyBitrateLadder::standard_h264();
388 assert!(ladder.find_by_label("720p").is_some());
389 assert!(ladder.find_by_label("4k").is_none());
390 }
391
392 #[test]
393 fn test_proxy_bitrate_ladder_empty() {
394 let ladder = ProxyBitrateLadder::new();
395 assert_eq!(ladder.rung_count(), 0);
396 assert!(ladder.highest_quality_rung().is_none());
397 assert!(ladder.lowest_quality_rung().is_none());
398 }
399
400 #[test]
401 fn test_proxy_transcode_settings_from_preset_low() {
402 let settings = ProxyTranscodeSettings::from_preset_1080p(QualityPreset::Low);
403 assert_eq!(settings.codec, ProxyCodecChoice::H264);
404 assert_eq!(settings.width, 960);
405 assert_eq!(settings.height, 540);
406 }
407
408 #[test]
409 fn test_proxy_transcode_settings_from_preset_high() {
410 let settings = ProxyTranscodeSettings::from_preset_1080p(QualityPreset::High);
411 assert_eq!(settings.codec, ProxyCodecChoice::H265);
412 assert_eq!(settings.width, 1920);
413 }
414
415 #[test]
416 fn test_proxy_transcode_settings_with_codec() {
417 let settings = ProxyTranscodeSettings::from_preset_1080p(QualityPreset::Low)
418 .with_codec(ProxyCodecChoice::Vp9);
419 assert_eq!(settings.codec, ProxyCodecChoice::Vp9);
420 }
421
422 #[test]
423 fn test_proxy_transcode_settings_resolution() {
424 let settings = ProxyTranscodeSettings::from_preset_1080p(QualityPreset::Medium);
425 assert_eq!(settings.resolution(), (1280, 720));
426 }
427
428 #[test]
429 fn test_proxy_transcode_settings_estimated_size() {
430 let settings = ProxyTranscodeSettings::from_preset_1080p(QualityPreset::Low);
431 let size = settings.estimated_size_mb(10.0);
433 assert!((size - 3.75).abs() < 1e-6);
434 }
435
436 #[test]
437 fn test_quality_preset_default() {
438 assert_eq!(QualityPreset::default(), QualityPreset::Low);
439 }
440}