1use crate::generate::ProxyGenerationSettings;
7use crate::{ProxyError, Result};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ProxyCodec {
13 H264,
15 H265,
17 Vp9,
19 ProRes422Proxy,
21 DnxHd36,
23 Custom(String),
25}
26
27impl ProxyCodec {
28 #[must_use]
30 pub fn as_str(&self) -> &str {
31 match self {
32 Self::H264 => "h264",
33 Self::H265 => "h265",
34 Self::Vp9 => "vp9",
35 Self::ProRes422Proxy => "prores_proxy",
36 Self::DnxHd36 => "dnxhd36",
37 Self::Custom(s) => s.as_str(),
38 }
39 }
40
41 #[must_use]
43 pub fn recommended_container(&self) -> &'static str {
44 match self {
45 Self::H264 | Self::H265 | Self::Vp9 => "mp4",
46 Self::ProRes422Proxy => "mov",
47 Self::DnxHd36 => "mxf",
48 Self::Custom(_) => "mp4",
49 }
50 }
51
52 #[must_use]
54 pub const fn hw_accel_supported(&self) -> bool {
55 matches!(self, Self::H264 | Self::H265)
56 }
57}
58
59impl std::fmt::Display for ProxyCodec {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 write!(f, "{}", self.as_str())
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
67pub enum ProxyResolutionMode {
68 ScaleFactor(f32),
70 Fixed(u32, u32),
72 FitWithin {
74 max_width: u32,
76 max_height: u32,
78 },
79}
80
81impl ProxyResolutionMode {
82 pub fn compute_output(&self, orig_w: u32, orig_h: u32) -> Result<(u32, u32)> {
88 match self {
89 Self::ScaleFactor(scale) => {
90 if *scale <= 0.0 || *scale > 4.0 {
91 return Err(ProxyError::InvalidInput(format!(
92 "Scale factor must be in (0, 4], got {scale}"
93 )));
94 }
95 let w = ((orig_w as f32 * scale) as u32).max(2);
96 let h = ((orig_h as f32 * scale) as u32).max(2);
97 Ok((w & !1, h & !1))
99 }
100 Self::Fixed(w, h) => {
101 if *w == 0 || *h == 0 {
102 return Err(ProxyError::InvalidInput(
103 "Fixed dimensions must be > 0".to_string(),
104 ));
105 }
106 Ok((*w & !1, *h & !1))
107 }
108 Self::FitWithin {
109 max_width,
110 max_height,
111 } => {
112 if *max_width == 0 || *max_height == 0 {
113 return Err(ProxyError::InvalidInput(
114 "FitWithin bounds must be > 0".to_string(),
115 ));
116 }
117 let w_ratio = *max_width as f32 / orig_w as f32;
118 let h_ratio = *max_height as f32 / orig_h as f32;
119 let scale = w_ratio.min(h_ratio);
120 let w = ((orig_w as f32 * scale) as u32).max(2);
121 let h = ((orig_h as f32 * scale) as u32).max(2);
122 Ok((w & !1, h & !1))
123 }
124 }
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ProxySpec {
131 pub name: String,
133 pub resolution: ProxyResolutionMode,
135 pub codec: ProxyCodec,
137 pub video_bitrate: u64,
139 pub audio_codec: String,
141 pub audio_bitrate: u64,
143 pub container: Option<String>,
145 pub preserve_timecode: bool,
147 pub preserve_metadata: bool,
149 pub use_hw_accel: bool,
151 pub quality_preset: String,
153}
154
155impl ProxySpec {
156 #[must_use]
158 pub fn new(
159 name: impl Into<String>,
160 resolution: ProxyResolutionMode,
161 codec: ProxyCodec,
162 video_bitrate: u64,
163 ) -> Self {
164 Self {
165 name: name.into(),
166 resolution,
167 codec,
168 video_bitrate,
169 audio_codec: "aac".to_string(),
170 audio_bitrate: 128_000,
171 container: None,
172 preserve_timecode: true,
173 preserve_metadata: true,
174 use_hw_accel: true,
175 quality_preset: "fast".to_string(),
176 }
177 }
178
179 #[must_use]
181 pub fn container_format(&self) -> &str {
182 self.container
183 .as_deref()
184 .unwrap_or_else(|| self.codec.recommended_container())
185 }
186
187 pub fn validate(&self) -> Result<()> {
193 if self.name.is_empty() {
194 return Err(ProxyError::InvalidInput(
195 "Spec name cannot be empty".to_string(),
196 ));
197 }
198 if self.video_bitrate == 0 {
199 return Err(ProxyError::InvalidInput(
200 "Video bitrate must be > 0".to_string(),
201 ));
202 }
203 if let ProxyResolutionMode::ScaleFactor(s) = self.resolution {
204 if s <= 0.0 || s > 4.0 {
205 return Err(ProxyError::InvalidInput(format!(
206 "Scale factor {s} out of range (0, 4]"
207 )));
208 }
209 }
210 Ok(())
211 }
212
213 #[must_use]
215 pub fn to_generation_settings(&self) -> ProxyGenerationSettings {
216 let scale_factor = match self.resolution {
217 ProxyResolutionMode::ScaleFactor(s) => s,
218 ProxyResolutionMode::Fixed(_, _) | ProxyResolutionMode::FitWithin { .. } => 0.5,
219 };
220 ProxyGenerationSettings {
221 scale_factor,
222 codec: self.codec.as_str().to_string(),
223 bitrate: self.video_bitrate,
224 audio_codec: self.audio_codec.clone(),
225 audio_bitrate: self.audio_bitrate,
226 preserve_frame_rate: true,
227 preserve_timecode: self.preserve_timecode,
228 preserve_metadata: self.preserve_metadata,
229 container: self.container_format().to_string(),
230 use_hw_accel: self.use_hw_accel,
231 threads: 0,
232 quality_preset: self.quality_preset.clone(),
233 }
234 }
235
236 #[must_use]
238 pub fn quarter_h264() -> Self {
239 Self::new(
240 "Quarter H.264",
241 ProxyResolutionMode::ScaleFactor(0.25),
242 ProxyCodec::H264,
243 2_000_000,
244 )
245 }
246
247 #[must_use]
249 pub fn half_h264() -> Self {
250 Self::new(
251 "Half H.264",
252 ProxyResolutionMode::ScaleFactor(0.5),
253 ProxyCodec::H264,
254 5_000_000,
255 )
256 }
257
258 #[must_use]
260 pub fn hd_h265() -> Self {
261 Self::new(
262 "HD H.265",
263 ProxyResolutionMode::FitWithin {
264 max_width: 1920,
265 max_height: 1080,
266 },
267 ProxyCodec::H265,
268 4_000_000,
269 )
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_proxy_codec_as_str() {
279 assert_eq!(ProxyCodec::H264.as_str(), "h264");
280 assert_eq!(ProxyCodec::H265.as_str(), "h265");
281 assert_eq!(ProxyCodec::Vp9.as_str(), "vp9");
282 assert_eq!(ProxyCodec::ProRes422Proxy.as_str(), "prores_proxy");
283 assert_eq!(ProxyCodec::DnxHd36.as_str(), "dnxhd36");
284 assert_eq!(
285 ProxyCodec::Custom("mycodec".to_string()).as_str(),
286 "mycodec"
287 );
288 }
289
290 #[test]
291 fn test_proxy_codec_container() {
292 assert_eq!(ProxyCodec::H264.recommended_container(), "mp4");
293 assert_eq!(ProxyCodec::ProRes422Proxy.recommended_container(), "mov");
294 assert_eq!(ProxyCodec::DnxHd36.recommended_container(), "mxf");
295 }
296
297 #[test]
298 fn test_proxy_codec_hw_accel() {
299 assert!(ProxyCodec::H264.hw_accel_supported());
300 assert!(ProxyCodec::H265.hw_accel_supported());
301 assert!(!ProxyCodec::Vp9.hw_accel_supported());
302 }
303
304 #[test]
305 fn test_proxy_codec_display() {
306 assert_eq!(ProxyCodec::H264.to_string(), "h264");
307 }
308
309 #[test]
310 fn test_resolution_mode_scale_factor() {
311 let (w, h) = ProxyResolutionMode::ScaleFactor(0.25)
312 .compute_output(1920, 1080)
313 .expect("should succeed in test");
314 assert_eq!(w, 480);
315 assert_eq!(h, 270);
316 }
317
318 #[test]
319 fn test_resolution_mode_fixed() {
320 let (w, h) = ProxyResolutionMode::Fixed(640, 360)
321 .compute_output(1920, 1080)
322 .expect("should succeed in test");
323 assert_eq!(w, 640);
324 assert_eq!(h, 360);
325 }
326
327 #[test]
328 fn test_resolution_mode_fit_within() {
329 let (w, h) = ProxyResolutionMode::FitWithin {
330 max_width: 960,
331 max_height: 540,
332 }
333 .compute_output(1920, 1080)
334 .expect("should succeed in test");
335 assert_eq!(w, 960);
336 assert_eq!(h, 540);
337 }
338
339 #[test]
340 fn test_resolution_mode_fit_within_portrait() {
341 let (w, h) = ProxyResolutionMode::FitWithin {
343 max_width: 1920,
344 max_height: 1080,
345 }
346 .compute_output(1080, 1920)
347 .expect("should succeed in test");
348 assert!(h <= 1080, "Height {h} should not exceed 1080");
349 assert!(w <= 1920, "Width {w} should not exceed 1920");
350 }
351
352 #[test]
353 fn test_resolution_mode_scale_factor_invalid() {
354 let result = ProxyResolutionMode::ScaleFactor(-0.1).compute_output(1920, 1080);
355 assert!(result.is_err());
356 let result2 = ProxyResolutionMode::ScaleFactor(5.0).compute_output(1920, 1080);
357 assert!(result2.is_err());
358 }
359
360 #[test]
361 fn test_resolution_mode_fixed_zero() {
362 let result = ProxyResolutionMode::Fixed(0, 360).compute_output(1920, 1080);
363 assert!(result.is_err());
364 }
365
366 #[test]
367 fn test_proxy_spec_new() {
368 let spec = ProxySpec::new(
369 "Test",
370 ProxyResolutionMode::ScaleFactor(0.5),
371 ProxyCodec::H264,
372 5_000_000,
373 );
374 assert_eq!(spec.name, "Test");
375 assert_eq!(spec.video_bitrate, 5_000_000);
376 assert!(spec.preserve_timecode);
377 }
378
379 #[test]
380 fn test_proxy_spec_validate_ok() {
381 let spec = ProxySpec::quarter_h264();
382 assert!(spec.validate().is_ok());
383 }
384
385 #[test]
386 fn test_proxy_spec_validate_empty_name() {
387 let mut spec = ProxySpec::quarter_h264();
388 spec.name = String::new();
389 assert!(spec.validate().is_err());
390 }
391
392 #[test]
393 fn test_proxy_spec_validate_zero_bitrate() {
394 let mut spec = ProxySpec::quarter_h264();
395 spec.video_bitrate = 0;
396 assert!(spec.validate().is_err());
397 }
398
399 #[test]
400 fn test_proxy_spec_predefined() {
401 let q = ProxySpec::quarter_h264();
402 assert_eq!(q.codec, ProxyCodec::H264);
403 assert_eq!(q.video_bitrate, 2_000_000);
404
405 let h = ProxySpec::half_h264();
406 assert_eq!(h.video_bitrate, 5_000_000);
407
408 let hd = ProxySpec::hd_h265();
409 assert_eq!(hd.codec, ProxyCodec::H265);
410 }
411
412 #[test]
413 fn test_proxy_spec_container_format() {
414 let spec = ProxySpec::quarter_h264();
415 assert_eq!(spec.container_format(), "mp4");
416
417 let mut prores_spec = spec.clone();
418 prores_spec.codec = ProxyCodec::ProRes422Proxy;
419 assert_eq!(prores_spec.container_format(), "mov");
420
421 let mut custom_spec = spec.clone();
422 custom_spec.container = Some("mkv".to_string());
423 assert_eq!(custom_spec.container_format(), "mkv");
424 }
425
426 #[test]
427 fn test_proxy_spec_to_generation_settings() {
428 let spec = ProxySpec::quarter_h264();
429 let settings = spec.to_generation_settings();
430 assert!((settings.scale_factor - 0.25).abs() < f32::EPSILON);
431 assert_eq!(settings.codec, "h264");
432 assert_eq!(settings.bitrate, 2_000_000);
433 assert_eq!(settings.container, "mp4");
434 }
435
436 #[test]
437 fn test_resolution_output_even_dimensions() {
438 let (w, h) = ProxyResolutionMode::ScaleFactor(0.33)
440 .compute_output(1920, 1080)
441 .expect("should succeed in test");
442 assert_eq!(w % 2, 0, "Width must be even");
443 assert_eq!(h % 2, 0, "Height must be even");
444 }
445}