Skip to main content

viser_ffmpeg/
lib.rs

1//! FFmpeg/FFprobe wrapper for the `viser` video-encoding-optimizer workspace.
2//!
3//! Provides typed primitives (codecs, resolutions, rate-control modes) plus
4//! functions to encode (`encode`), probe media (`probe`), and resolve the
5//! `ffmpeg`/`ffprobe` binary paths. A `ProbeCache` deduplicates probe calls.
6
7mod cache;
8mod color;
9mod encode;
10mod hdr;
11mod hw_encode;
12mod path;
13mod probe;
14#[cfg(feature = "revelo")]
15mod probe_revelo;
16#[cfg(feature = "revelo")]
17pub use probe_revelo::probe as probe_revelo;
18
19pub use cache::*;
20pub use color::*;
21pub use encode::*;
22pub use hdr::*;
23pub use hw_encode::*;
24pub use path::*;
25pub use probe::*;
26
27use serde::{Deserialize, Serialize};
28use std::fmt;
29
30/// Supported video codec.
31///
32/// Software encoders (libx264, libx265, libsvtav1, libvpx-vp9) are always available.
33/// Hardware encoder variants require FFmpeg built with the matching SDK
34/// and a GPU with the matching ASIC at runtime; availability is detected
35/// via `ffmpeg -encoders`.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum Codec {
38    /// H.264/AVC via `libx264`.
39    #[serde(rename = "libx264")]
40    X264,
41    /// H.265/HEVC via `libx265`.
42    #[serde(rename = "libx265")]
43    X265,
44    /// AV1 via `libsvtav1` (SVT-AV1).
45    #[serde(rename = "libsvtav1")]
46    SvtAv1,
47    /// VP9 via `libvpx-vp9`.
48    #[serde(rename = "libvpx-vp9")]
49    Vp9,
50
51    // ── Hardware encoders (H.264) ──
52    /// NVIDIA NVENC H.264 (`h264_nvenc`).
53    #[serde(rename = "h264_nvenc")]
54    NvencH264,
55    /// Intel QuickSync H.264 (`h264_qsv`).
56    #[serde(rename = "h264_qsv")]
57    QsvH264,
58    /// Apple VideoToolbox H.264 (`h264_videotoolbox`).
59    #[serde(rename = "h264_videotoolbox")]
60    VideoToolboxH264,
61    /// Linux VAAPI H.264 (`h264_vaapi`).
62    #[serde(rename = "h264_vaapi")]
63    VaapiH264,
64    /// AMD AMF H.264 (`h264_amf`).
65    #[serde(rename = "h264_amf")]
66    AmfH264,
67
68    // ── Hardware encoders (H.265/HEVC) ──
69    /// NVIDIA NVENC HEVC (`hevc_nvenc`).
70    #[serde(rename = "hevc_nvenc")]
71    NvencH265,
72    /// Intel QuickSync HEVC (`hevc_qsv`).
73    #[serde(rename = "hevc_qsv")]
74    QsvH265,
75    /// Apple VideoToolbox HEVC (`hevc_videotoolbox`).
76    #[serde(rename = "hevc_videotoolbox")]
77    VideoToolboxH265,
78    /// Linux VAAPI HEVC (`hevc_vaapi`).
79    #[serde(rename = "hevc_vaapi")]
80    VaapiH265,
81    /// AMD AMF HEVC (`hevc_amf`).
82    #[serde(rename = "hevc_amf")]
83    AmfH265,
84
85    // ── Hardware encoders (AV1) ──
86    // Apple VideoToolbox has no AV1 encoder, so there is no `av1_videotoolbox`.
87    /// NVIDIA NVENC AV1 (`av1_nvenc`) — Ada/Blackwell and newer.
88    #[serde(rename = "av1_nvenc")]
89    NvencAv1,
90    /// Intel QuickSync AV1 (`av1_qsv`) — Arc/Battlemage and newer.
91    #[serde(rename = "av1_qsv")]
92    QsvAv1,
93    /// Linux VAAPI AV1 (`av1_vaapi`) — Arc/Battlemage, RDNA3+ and newer.
94    #[serde(rename = "av1_vaapi")]
95    VaapiAv1,
96    /// AMD AMF AV1 (`av1_amf`) — RDNA3+ and newer.
97    #[serde(rename = "av1_amf")]
98    AmfAv1,
99}
100
101/// Hardware encoder backend (GPU vendor / API).
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum EncoderBackend {
104    /// Software encoder (libx264, libx265, libsvtav1, libvpx-vp9).
105    Software,
106    /// NVIDIA NVENC.
107    Nvenc,
108    /// Intel QuickSync.
109    Qsv,
110    /// Apple VideoToolbox.
111    VideoToolbox,
112    /// Linux VAAPI.
113    Vaapi,
114    /// AMD AMF.
115    Amf,
116}
117
118/// Codec family (compression standard).
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum CodecFamily {
121    /// H.264/AVC.
122    H264,
123    /// H.265/HEVC.
124    H265,
125    /// AV1.
126    Av1,
127    /// VP9.
128    Vp9,
129}
130
131impl Codec {
132    /// FFmpeg encoder name for this codec (e.g. `"libx264"`).
133    pub fn as_str(&self) -> &'static str {
134        match self {
135            Codec::X264 => "libx264",
136            Codec::X265 => "libx265",
137            Codec::SvtAv1 => "libsvtav1",
138            Codec::Vp9 => "libvpx-vp9",
139            Codec::NvencH264 => "h264_nvenc",
140            Codec::QsvH264 => "h264_qsv",
141            Codec::VideoToolboxH264 => "h264_videotoolbox",
142            Codec::VaapiH264 => "h264_vaapi",
143            Codec::AmfH264 => "h264_amf",
144            Codec::NvencH265 => "hevc_nvenc",
145            Codec::QsvH265 => "hevc_qsv",
146            Codec::VideoToolboxH265 => "hevc_videotoolbox",
147            Codec::VaapiH265 => "hevc_vaapi",
148            Codec::AmfH265 => "hevc_amf",
149            Codec::NvencAv1 => "av1_nvenc",
150            Codec::QsvAv1 => "av1_qsv",
151            Codec::VaapiAv1 => "av1_vaapi",
152            Codec::AmfAv1 => "av1_amf",
153        }
154    }
155
156    /// Hardware encoder backend for this codec.
157    pub fn backend(&self) -> EncoderBackend {
158        match self {
159            Codec::X264 | Codec::X265 | Codec::SvtAv1 | Codec::Vp9 => EncoderBackend::Software,
160            Codec::NvencH264 | Codec::NvencH265 | Codec::NvencAv1 => EncoderBackend::Nvenc,
161            Codec::QsvH264 | Codec::QsvH265 | Codec::QsvAv1 => EncoderBackend::Qsv,
162            Codec::VideoToolboxH264 | Codec::VideoToolboxH265 => EncoderBackend::VideoToolbox,
163            Codec::VaapiH264 | Codec::VaapiH265 | Codec::VaapiAv1 => EncoderBackend::Vaapi,
164            Codec::AmfH264 | Codec::AmfH265 | Codec::AmfAv1 => EncoderBackend::Amf,
165        }
166    }
167
168    /// Codec family (compression standard).
169    pub fn family(&self) -> CodecFamily {
170        match self {
171            Codec::X264
172            | Codec::NvencH264
173            | Codec::QsvH264
174            | Codec::VideoToolboxH264
175            | Codec::VaapiH264
176            | Codec::AmfH264 => CodecFamily::H264,
177            Codec::X265
178            | Codec::NvencH265
179            | Codec::QsvH265
180            | Codec::VideoToolboxH265
181            | Codec::VaapiH265
182            | Codec::AmfH265 => CodecFamily::H265,
183            Codec::SvtAv1 | Codec::NvencAv1 | Codec::QsvAv1 | Codec::VaapiAv1 | Codec::AmfAv1 => {
184                CodecFamily::Av1
185            }
186            Codec::Vp9 => CodecFamily::Vp9,
187        }
188    }
189
190    /// Whether this codec uses a hardware encoder backend.
191    pub fn is_hardware(&self) -> bool {
192        !matches!(self.backend(), EncoderBackend::Software)
193    }
194
195    /// Whether this codec is a software encoder.
196    pub fn is_software(&self) -> bool {
197        matches!(self.backend(), EncoderBackend::Software)
198    }
199}
200
201impl fmt::Display for Codec {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        f.write_str(self.as_str())
204    }
205}
206
207impl std::str::FromStr for Codec {
208    type Err = anyhow::Error;
209
210    fn from_str(s: &str) -> Result<Self, Self::Err> {
211        match s {
212            "libx264" | "x264" | "h264" => Ok(Codec::X264),
213            "libx265" | "x265" | "h265" | "hevc" => Ok(Codec::X265),
214            "libsvtav1" | "svtav1" | "av1" => Ok(Codec::SvtAv1),
215            "libvpx-vp9" | "vp9" | "libvpx" => Ok(Codec::Vp9),
216            // NVENC
217            "h264_nvenc" | "nvenc" | "nvenc_h264" => Ok(Codec::NvencH264),
218            "hevc_nvenc" | "nvenc_h265" | "nvenc_hevc" => Ok(Codec::NvencH265),
219            // QuickSync
220            "h264_qsv" | "qsv" | "qsv_h264" => Ok(Codec::QsvH264),
221            "hevc_qsv" | "qsv_h265" | "qsv_hevc" => Ok(Codec::QsvH265),
222            // VideoToolbox
223            "h264_videotoolbox" | "vt" | "vt_h264" | "videotoolbox" => Ok(Codec::VideoToolboxH264),
224            "hevc_videotoolbox" | "vt_h265" | "vt_hevc" => Ok(Codec::VideoToolboxH265),
225            // VAAPI
226            "h264_vaapi" | "vaapi" | "vaapi_h264" => Ok(Codec::VaapiH264),
227            "hevc_vaapi" | "vaapi_h265" | "vaapi_hevc" => Ok(Codec::VaapiH265),
228            // AMF
229            "h264_amf" | "amf" | "amf_h264" => Ok(Codec::AmfH264),
230            "hevc_amf" | "amf_h265" | "amf_hevc" => Ok(Codec::AmfH265),
231            // AV1 hardware
232            "av1_nvenc" | "nvenc_av1" => Ok(Codec::NvencAv1),
233            "av1_qsv" | "qsv_av1" => Ok(Codec::QsvAv1),
234            "av1_vaapi" | "vaapi_av1" => Ok(Codec::VaapiAv1),
235            "av1_amf" | "amf_av1" => Ok(Codec::AmfAv1),
236            _ => Err(anyhow::anyhow!("unknown codec: {s}")),
237        }
238    }
239}
240
241/// Video resolution.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
243pub struct Resolution {
244    /// Pixel width.
245    pub width: i32,
246    /// Pixel height.
247    pub height: i32,
248}
249
250impl Resolution {
251    /// Creates a resolution from width and height in pixels.
252    pub const fn new(width: i32, height: i32) -> Self {
253        Self { width, height }
254    }
255
256    /// Human-friendly label like "1080p", "720p", etc.
257    pub fn label(&self) -> String {
258        match self.height {
259            h if h >= 2160 => "2160p".into(),
260            h if h >= 1440 => "1440p".into(),
261            h if h >= 1080 => "1080p".into(),
262            h if h >= 720 => "720p".into(),
263            h if h >= 480 => "480p".into(),
264            h if h >= 360 => "360p".into(),
265            h if h >= 240 => "240p".into(),
266            h => format!("{h}p"),
267        }
268    }
269}
270
271impl fmt::Display for Resolution {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        write!(f, "{}x{}", self.width, self.height)
274    }
275}
276
277impl std::str::FromStr for Resolution {
278    type Err = anyhow::Error;
279
280    fn from_str(s: &str) -> Result<Self, Self::Err> {
281        match s.to_lowercase().as_str() {
282            "2160p" | "4k" => Ok(RES_2160P),
283            "1440p" => Ok(RES_1440P),
284            "1080p" => Ok(RES_1080P),
285            "720p" => Ok(RES_720P),
286            "480p" => Ok(RES_480P),
287            "360p" => Ok(RES_360P),
288            "240p" => Ok(RES_240P),
289            other => {
290                if let Some((w, h)) = other.split_once('x') {
291                    Ok(Resolution::new(w.parse()?, h.parse()?))
292                } else {
293                    Err(anyhow::anyhow!("invalid resolution: {other}"))
294                }
295            }
296        }
297    }
298}
299
300/// 3840x2160 (4K UHD), 16:9.
301pub const RES_2160P: Resolution = Resolution::new(3840, 2160);
302/// 2560x1440 (QHD), 16:9.
303pub const RES_1440P: Resolution = Resolution::new(2560, 1440);
304/// 1920x1080 (Full HD), 16:9.
305pub const RES_1080P: Resolution = Resolution::new(1920, 1080);
306/// 1280x720 (HD), 16:9.
307pub const RES_720P: Resolution = Resolution::new(1280, 720);
308/// 854x480 (SD), 16:9.
309pub const RES_480P: Resolution = Resolution::new(854, 480);
310/// 640x360, 16:9.
311pub const RES_360P: Resolution = Resolution::new(640, 360);
312/// 426x240, 16:9.
313pub const RES_240P: Resolution = Resolution::new(426, 240);
314
315/// Rate control mode for encoding.
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
317#[serde(rename_all = "lowercase")]
318pub enum RateControlMode {
319    /// Constant rate factor (default).
320    #[default]
321    Crf,
322    /// CRF with VBV/decoder-model bitrate cap.
323    CappedCrf,
324    /// Fixed quantizer (Netflix-style, no R-D optimization).
325    Qp,
326    /// 2-pass variable bitrate (for final delivery encodes).
327    Vbr,
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_codec_as_str() {
336        assert_eq!(Codec::X264.as_str(), "libx264");
337        assert_eq!(Codec::X265.as_str(), "libx265");
338        assert_eq!(Codec::SvtAv1.as_str(), "libsvtav1");
339        assert_eq!(Codec::Vp9.as_str(), "libvpx-vp9");
340        assert_eq!(Codec::NvencH264.as_str(), "h264_nvenc");
341        assert_eq!(Codec::QsvH264.as_str(), "h264_qsv");
342        assert_eq!(Codec::VideoToolboxH264.as_str(), "h264_videotoolbox");
343        assert_eq!(Codec::VaapiH264.as_str(), "h264_vaapi");
344        assert_eq!(Codec::AmfH264.as_str(), "h264_amf");
345        assert_eq!(Codec::NvencH265.as_str(), "hevc_nvenc");
346    }
347
348    #[test]
349    fn test_codec_display() {
350        assert_eq!(format!("{}", Codec::X264), "libx264");
351        assert_eq!(format!("{}", Codec::NvencH264), "h264_nvenc");
352    }
353
354    #[test]
355    fn test_codec_from_str() {
356        assert_eq!("libx264".parse::<Codec>().unwrap(), Codec::X264);
357        assert_eq!("x264".parse::<Codec>().unwrap(), Codec::X264);
358        assert_eq!("h264".parse::<Codec>().unwrap(), Codec::X264);
359        assert_eq!("libx265".parse::<Codec>().unwrap(), Codec::X265);
360        assert_eq!("x265".parse::<Codec>().unwrap(), Codec::X265);
361        assert_eq!("h265".parse::<Codec>().unwrap(), Codec::X265);
362        assert_eq!("hevc".parse::<Codec>().unwrap(), Codec::X265);
363        assert_eq!("libsvtav1".parse::<Codec>().unwrap(), Codec::SvtAv1);
364        assert_eq!("svtav1".parse::<Codec>().unwrap(), Codec::SvtAv1);
365        assert_eq!("av1".parse::<Codec>().unwrap(), Codec::SvtAv1);
366        assert_eq!("libvpx-vp9".parse::<Codec>().unwrap(), Codec::Vp9);
367        assert_eq!("vp9".parse::<Codec>().unwrap(), Codec::Vp9);
368        assert_eq!("h264_nvenc".parse::<Codec>().unwrap(), Codec::NvencH264);
369        assert_eq!("nvenc".parse::<Codec>().unwrap(), Codec::NvencH264);
370        assert_eq!("hevc_nvenc".parse::<Codec>().unwrap(), Codec::NvencH265);
371        assert_eq!("h264_qsv".parse::<Codec>().unwrap(), Codec::QsvH264);
372        assert_eq!("qsv".parse::<Codec>().unwrap(), Codec::QsvH264);
373        assert_eq!("vt".parse::<Codec>().unwrap(), Codec::VideoToolboxH264);
374        assert_eq!("h264_vaapi".parse::<Codec>().unwrap(), Codec::VaapiH264);
375        assert_eq!("vaapi".parse::<Codec>().unwrap(), Codec::VaapiH264);
376        assert_eq!("h264_amf".parse::<Codec>().unwrap(), Codec::AmfH264);
377        assert_eq!("amf".parse::<Codec>().unwrap(), Codec::AmfH264);
378        assert!("unknown".parse::<Codec>().is_err());
379    }
380
381    #[test]
382    fn test_codec_backend() {
383        assert_eq!(Codec::X264.backend(), EncoderBackend::Software);
384        assert_eq!(Codec::NvencH264.backend(), EncoderBackend::Nvenc);
385        assert_eq!(Codec::QsvH264.backend(), EncoderBackend::Qsv);
386        assert_eq!(Codec::VideoToolboxH264.backend(), EncoderBackend::VideoToolbox);
387        assert_eq!(Codec::VaapiH264.backend(), EncoderBackend::Vaapi);
388        assert_eq!(Codec::AmfH264.backend(), EncoderBackend::Amf);
389    }
390
391    #[test]
392    fn test_codec_family() {
393        assert_eq!(Codec::X264.family(), CodecFamily::H264);
394        assert_eq!(Codec::NvencH264.family(), CodecFamily::H264);
395        assert_eq!(Codec::X265.family(), CodecFamily::H265);
396        assert_eq!(Codec::NvencH265.family(), CodecFamily::H265);
397        assert_eq!(Codec::SvtAv1.family(), CodecFamily::Av1);
398        assert_eq!(Codec::Vp9.family(), CodecFamily::Vp9);
399    }
400
401    #[test]
402    fn test_codec_is_hardware() {
403        assert!(!Codec::X264.is_hardware());
404        assert!(!Codec::X265.is_hardware());
405        assert!(!Codec::SvtAv1.is_hardware());
406        assert!(Codec::NvencH264.is_hardware());
407        assert!(Codec::QsvH265.is_hardware());
408        assert!(Codec::VideoToolboxH264.is_hardware());
409    }
410
411    #[test]
412    fn test_codec_is_software() {
413        assert!(Codec::X264.is_software());
414        assert!(!Codec::NvencH264.is_software());
415    }
416
417    #[test]
418    fn test_codec_serde_roundtrip() {
419        for codec in &[
420            Codec::X264,
421            Codec::X265,
422            Codec::SvtAv1,
423            Codec::NvencH264,
424            Codec::NvencH265,
425            Codec::QsvH264,
426        ] {
427            let json = serde_json::to_string(codec).unwrap();
428            let back: Codec = serde_json::from_str(&json).unwrap();
429            assert_eq!(*codec, back);
430        }
431    }
432
433    #[test]
434    fn test_av1_hw_codec_as_str() {
435        assert_eq!(Codec::NvencAv1.as_str(), "av1_nvenc");
436        assert_eq!(Codec::QsvAv1.as_str(), "av1_qsv");
437        assert_eq!(Codec::VaapiAv1.as_str(), "av1_vaapi");
438        assert_eq!(Codec::AmfAv1.as_str(), "av1_amf");
439    }
440
441    #[test]
442    fn test_av1_hw_codec_from_str() {
443        assert_eq!("av1_nvenc".parse::<Codec>().unwrap(), Codec::NvencAv1);
444        assert_eq!("nvenc_av1".parse::<Codec>().unwrap(), Codec::NvencAv1);
445        assert_eq!("av1_qsv".parse::<Codec>().unwrap(), Codec::QsvAv1);
446        assert_eq!("av1_vaapi".parse::<Codec>().unwrap(), Codec::VaapiAv1);
447        assert_eq!("av1_amf".parse::<Codec>().unwrap(), Codec::AmfAv1);
448    }
449
450    #[test]
451    fn test_av1_hw_codec_backend_and_family() {
452        for codec in &[Codec::NvencAv1, Codec::QsvAv1, Codec::VaapiAv1, Codec::AmfAv1] {
453            assert_eq!(codec.family(), CodecFamily::Av1);
454            assert!(codec.is_hardware());
455        }
456        assert_eq!(Codec::NvencAv1.backend(), EncoderBackend::Nvenc);
457        assert_eq!(Codec::QsvAv1.backend(), EncoderBackend::Qsv);
458        assert_eq!(Codec::VaapiAv1.backend(), EncoderBackend::Vaapi);
459        assert_eq!(Codec::AmfAv1.backend(), EncoderBackend::Amf);
460    }
461
462    #[test]
463    fn test_codec_eq() {
464        assert_eq!(Codec::X264, Codec::X264);
465        assert_ne!(Codec::X264, Codec::X265);
466        assert_ne!(Codec::X264, Codec::NvencH264);
467    }
468
469    #[test]
470    fn test_codec_hash() {
471        use std::collections::HashSet;
472        let mut set = HashSet::new();
473        set.insert(Codec::X264);
474        set.insert(Codec::X264);
475        assert_eq!(set.len(), 1);
476    }
477
478    #[test]
479    fn test_resolution_new() {
480        let r = Resolution::new(1920, 1080);
481        assert_eq!(r.width, 1920);
482        assert_eq!(r.height, 1080);
483    }
484
485    #[test]
486    fn test_resolution_label() {
487        assert_eq!(Resolution::new(3840, 2160).label(), "2160p");
488        assert_eq!(Resolution::new(2560, 1440).label(), "1440p");
489        assert_eq!(Resolution::new(1920, 1080).label(), "1080p");
490        assert_eq!(Resolution::new(1280, 720).label(), "720p");
491        assert_eq!(Resolution::new(854, 480).label(), "480p");
492        assert_eq!(Resolution::new(640, 360).label(), "360p");
493        assert_eq!(Resolution::new(426, 240).label(), "240p");
494        assert_eq!(Resolution::new(320, 200).label(), "200p");
495    }
496
497    #[test]
498    fn test_resolution_display() {
499        assert_eq!(format!("{}", Resolution::new(1920, 1080)), "1920x1080");
500        assert_eq!(format!("{}", Resolution::new(640, 360)), "640x360");
501    }
502
503    #[test]
504    fn test_resolution_from_str() {
505        assert_eq!("1080p".parse::<Resolution>().unwrap(), RES_1080P);
506        assert_eq!("720p".parse::<Resolution>().unwrap(), RES_720P);
507        assert_eq!("480p".parse::<Resolution>().unwrap(), RES_480P);
508        assert_eq!("360p".parse::<Resolution>().unwrap(), RES_360P);
509        assert_eq!("240p".parse::<Resolution>().unwrap(), RES_240P);
510        assert_eq!("1440p".parse::<Resolution>().unwrap(), RES_1440P);
511        assert_eq!("2160p".parse::<Resolution>().unwrap(), RES_2160P);
512        assert_eq!("4k".parse::<Resolution>().unwrap(), RES_2160P);
513        assert_eq!("1920x1080".parse::<Resolution>().unwrap(), RES_1080P);
514        assert_eq!("640x360".parse::<Resolution>().unwrap(), RES_360P);
515        assert!("invalid".parse::<Resolution>().is_err());
516    }
517
518    #[test]
519    fn test_resolution_serde_roundtrip() {
520        let r = RES_1080P;
521        let json = serde_json::to_string(&r).unwrap();
522        let back: Resolution = serde_json::from_str(&json).unwrap();
523        assert_eq!(r, back);
524    }
525
526    #[test]
527    fn test_resolution_const_equality() {
528        assert_eq!(RES_2160P, Resolution::new(3840, 2160));
529        assert_eq!(RES_1440P, Resolution::new(2560, 1440));
530        assert_eq!(RES_1080P, Resolution::new(1920, 1080));
531        assert_eq!(RES_720P, Resolution::new(1280, 720));
532        assert_eq!(RES_480P, Resolution::new(854, 480));
533        assert_eq!(RES_360P, Resolution::new(640, 360));
534        assert_eq!(RES_240P, Resolution::new(426, 240));
535    }
536
537    #[test]
538    fn test_rate_control_mode_default() {
539        assert_eq!(RateControlMode::default(), RateControlMode::Crf);
540    }
541
542    #[test]
543    fn test_rate_control_mode_serde() {
544        let json = serde_json::to_string(&RateControlMode::Crf).unwrap();
545        assert_eq!(json, "\"crf\"");
546        let back: RateControlMode = serde_json::from_str("\"vbr\"").unwrap();
547        assert_eq!(back, RateControlMode::Vbr);
548    }
549
550    #[test]
551    fn test_ffmpeg_path_default() {
552        let path = ffmpeg_path();
553        assert!(!path.is_empty());
554    }
555
556    #[test]
557    fn test_ffprobe_path_default() {
558        let path = ffprobe_path();
559        assert!(!path.is_empty());
560    }
561
562    #[test]
563    fn test_ffmpeg_path_respects_env() {
564        // SAFETY: test-only env var manipulation, single-threaded test
565        let old = std::env::var("VISER_FFMPEG").ok();
566        unsafe {
567            std::env::set_var("VISER_FFMPEG", "/custom/ffmpeg");
568        }
569        assert_eq!(ffmpeg_path(), "/custom/ffmpeg");
570        unsafe {
571            match old {
572                Some(v) => std::env::set_var("VISER_FFMPEG", v),
573                None => std::env::remove_var("VISER_FFMPEG"),
574            }
575        }
576    }
577
578    #[test]
579    fn test_ffprobe_path_respects_env() {
580        // SAFETY: test-only env var manipulation, single-threaded test
581        let old = std::env::var("VISER_FFPROBE").ok();
582        unsafe {
583            std::env::set_var("VISER_FFPROBE", "/custom/ffprobe");
584        }
585        assert_eq!(ffprobe_path(), "/custom/ffprobe");
586        unsafe {
587            match old {
588                Some(v) => std::env::set_var("VISER_FFPROBE", v),
589                None => std::env::remove_var("VISER_FFPROBE"),
590            }
591        }
592    }
593}