Skip to main content

oximedia_proxy/
transcode_proxy.rs

1//! Proxy transcoding settings for OxiMedia proxy system.
2//!
3//! Provides proxy codec selection, bitrate ladders, and quality presets
4//! for efficient proxy generation workflows.
5
6#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9/// Codec choices for proxy generation.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum ProxyCodecChoice {
12    /// H.264 / AVC — widely compatible, CPU-efficient.
13    H264,
14    /// H.265 / HEVC — better compression, higher CPU cost.
15    H265,
16    /// VP9 — open format, good for web delivery proxies.
17    Vp9,
18    /// AV1 — best compression ratio, highest CPU cost.
19    Av1,
20    /// Apple ProRes Proxy — fast decode on Apple hardware.
21    ProResProxy,
22    /// DNxHD/DNxHR LB — fast decode on Avid systems.
23    DnxhdLb,
24}
25
26impl ProxyCodecChoice {
27    /// Returns the name of the codec as a string.
28    #[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    /// Returns the typical container format for this codec.
41    #[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    /// Whether this codec supports hardware acceleration on most platforms.
51    #[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/// A single rung of a proxy bitrate ladder.
64#[derive(Debug, Clone, PartialEq)]
65pub struct BitrateLadderRung {
66    /// Label for this rung (e.g., "1080p", "720p").
67    pub label: String,
68    /// Width in pixels.
69    pub width: u32,
70    /// Height in pixels.
71    pub height: u32,
72    /// Target bitrate in bits per second.
73    pub bitrate_bps: u64,
74    /// Codec for this rung.
75    pub codec: ProxyCodecChoice,
76}
77
78impl BitrateLadderRung {
79    /// Create a new bitrate ladder rung.
80    #[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    /// Pixel count for this rung.
98    #[must_use]
99    pub fn pixel_count(&self) -> u64 {
100        u64::from(self.width) * u64::from(self.height)
101    }
102
103    /// Bits per pixel at this rung's bitrate and 24fps.
104    #[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/// A proxy bitrate ladder containing multiple resolution rungs.
115#[derive(Debug, Clone, Default)]
116pub struct ProxyBitrateLadder {
117    /// Rungs from highest to lowest quality.
118    rungs: Vec<BitrateLadderRung>,
119}
120
121impl ProxyBitrateLadder {
122    /// Create an empty bitrate ladder.
123    #[must_use]
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Create a standard H.264 proxy ladder for offline editing.
129    #[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    /// Add a rung to the ladder.
164    pub fn add_rung(&mut self, rung: BitrateLadderRung) {
165        self.rungs.push(rung);
166    }
167
168    /// Number of rungs in the ladder.
169    #[must_use]
170    pub fn rung_count(&self) -> usize {
171        self.rungs.len()
172    }
173
174    /// Find the rung with the highest bitrate.
175    #[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    /// Find the rung with the lowest bitrate.
181    #[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    /// Find a rung by label.
187    #[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    /// All rungs in the ladder.
193    #[must_use]
194    pub fn rungs(&self) -> &[BitrateLadderRung] {
195        &self.rungs
196    }
197}
198
199/// Named quality preset for proxy transcoding.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum QualityPreset {
202    /// Ultra-low: smallest files, for mobile / slow network editing.
203    UltraLow,
204    /// Low: good for most offline editing scenarios.
205    Low,
206    /// Medium: balanced quality and file size.
207    Medium,
208    /// High: near-lossless proxy for critical color work.
209    High,
210}
211
212impl Default for QualityPreset {
213    fn default() -> Self {
214        Self::Low
215    }
216}
217
218/// Settings for proxy transcoding derived from a quality preset.
219#[derive(Debug, Clone)]
220pub struct ProxyTranscodeSettings {
221    /// Codec to use.
222    pub codec: ProxyCodecChoice,
223    /// Target bitrate in bps.
224    pub bitrate_bps: u64,
225    /// Width in pixels.
226    pub width: u32,
227    /// Height in pixels.
228    pub height: u32,
229    /// CRF value (lower = better quality; 0 = lossless for supported codecs).
230    pub crf: u8,
231    /// Number of encoding threads (0 = auto).
232    pub threads: u32,
233    /// Whether to copy the audio stream without re-encoding.
234    pub copy_audio: bool,
235}
236
237impl ProxyTranscodeSettings {
238    /// Create settings from a quality preset for a 1080p source.
239    #[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    /// Override the codec.
282    #[must_use]
283    pub fn with_codec(mut self, codec: ProxyCodecChoice) -> Self {
284        self.codec = codec;
285        self
286    }
287
288    /// Override the thread count.
289    #[must_use]
290    pub fn with_threads(mut self, threads: u32) -> Self {
291        self.threads = threads;
292        self
293    }
294
295    /// Override the CRF value.
296    #[must_use]
297    pub fn with_crf(mut self, crf: u8) -> Self {
298        self.crf = crf;
299        self
300    }
301
302    /// Resolution as a tuple (width, height).
303    #[must_use]
304    pub fn resolution(&self) -> (u32, u32) {
305        (self.width, self.height)
306    }
307
308    /// Estimated file size in megabytes for a given duration in seconds.
309    #[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        // 2_400_000 / (10000 * 24) = 10.0
357        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        // 3_000_000 bps * 10s / 8 / 1_000_000 = 3.75 MB
432        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}