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