Skip to main content

viser_ffmpeg/
lib.rs

1mod cache;
2mod encode;
3mod path;
4mod probe;
5#[cfg(feature = "revelo")]
6mod probe_revelo;
7#[cfg(feature = "revelo")]
8pub use probe_revelo::probe as probe_revelo;
9
10pub use cache::*;
11pub use encode::*;
12pub use path::*;
13pub use probe::*;
14
15use serde::{Deserialize, Serialize};
16use std::fmt;
17
18/// Supported video codec.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum Codec {
21    #[serde(rename = "libx264")]
22    X264,
23    #[serde(rename = "libx265")]
24    X265,
25    #[serde(rename = "libsvtav1")]
26    SvtAv1,
27}
28
29impl Codec {
30    pub fn as_str(&self) -> &'static str {
31        match self {
32            Codec::X264 => "libx264",
33            Codec::X265 => "libx265",
34            Codec::SvtAv1 => "libsvtav1",
35        }
36    }
37}
38
39impl fmt::Display for Codec {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        f.write_str(self.as_str())
42    }
43}
44
45impl std::str::FromStr for Codec {
46    type Err = anyhow::Error;
47
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        match s {
50            "libx264" | "x264" | "h264" => Ok(Codec::X264),
51            "libx265" | "x265" | "h265" | "hevc" => Ok(Codec::X265),
52            "libsvtav1" | "svtav1" | "av1" => Ok(Codec::SvtAv1),
53            _ => Err(anyhow::anyhow!("unknown codec: {s}")),
54        }
55    }
56}
57
58/// Video resolution.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub struct Resolution {
61    pub width: i32,
62    pub height: i32,
63}
64
65impl Resolution {
66    pub const fn new(width: i32, height: i32) -> Self {
67        Self { width, height }
68    }
69
70    /// Human-friendly label like "1080p", "720p", etc.
71    pub fn label(&self) -> String {
72        match self.height {
73            h if h >= 2160 => "2160p".into(),
74            h if h >= 1440 => "1440p".into(),
75            h if h >= 1080 => "1080p".into(),
76            h if h >= 720 => "720p".into(),
77            h if h >= 480 => "480p".into(),
78            h if h >= 360 => "360p".into(),
79            h if h >= 240 => "240p".into(),
80            h => format!("{h}p"),
81        }
82    }
83}
84
85impl fmt::Display for Resolution {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}x{}", self.width, self.height)
88    }
89}
90
91impl std::str::FromStr for Resolution {
92    type Err = anyhow::Error;
93
94    fn from_str(s: &str) -> Result<Self, Self::Err> {
95        match s.to_lowercase().as_str() {
96            "2160p" | "4k" => Ok(RES_2160P),
97            "1440p" => Ok(RES_1440P),
98            "1080p" => Ok(RES_1080P),
99            "720p" => Ok(RES_720P),
100            "480p" => Ok(RES_480P),
101            "360p" => Ok(RES_360P),
102            "240p" => Ok(RES_240P),
103            other => {
104                if let Some((w, h)) = other.split_once('x') {
105                    Ok(Resolution::new(w.parse()?, h.parse()?))
106                } else {
107                    Err(anyhow::anyhow!("invalid resolution: {other}"))
108                }
109            }
110        }
111    }
112}
113
114/// Common resolutions (16:9 aspect ratio).
115pub const RES_2160P: Resolution = Resolution::new(3840, 2160);
116pub const RES_1440P: Resolution = Resolution::new(2560, 1440);
117pub const RES_1080P: Resolution = Resolution::new(1920, 1080);
118pub const RES_720P: Resolution = Resolution::new(1280, 720);
119pub const RES_480P: Resolution = Resolution::new(854, 480);
120pub const RES_360P: Resolution = Resolution::new(640, 360);
121pub const RES_240P: Resolution = Resolution::new(426, 240);
122
123/// Rate control mode for encoding.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
125#[serde(rename_all = "lowercase")]
126pub enum RateControlMode {
127    /// Constant rate factor (default).
128    #[default]
129    Crf,
130    /// CRF with VBV/decoder-model bitrate cap.
131    CappedCrf,
132    /// Fixed quantizer (Netflix-style, no R-D optimization).
133    Qp,
134    /// 2-pass variable bitrate (for final delivery encodes).
135    Vbr,
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_codec_as_str() {
144        assert_eq!(Codec::X264.as_str(), "libx264");
145        assert_eq!(Codec::X265.as_str(), "libx265");
146        assert_eq!(Codec::SvtAv1.as_str(), "libsvtav1");
147    }
148
149    #[test]
150    fn test_codec_display() {
151        assert_eq!(format!("{}", Codec::X264), "libx264");
152        assert_eq!(format!("{}", Codec::X265), "libx265");
153        assert_eq!(format!("{}", Codec::SvtAv1), "libsvtav1");
154    }
155
156    #[test]
157    fn test_codec_from_str() {
158        assert_eq!("libx264".parse::<Codec>().unwrap(), Codec::X264);
159        assert_eq!("x264".parse::<Codec>().unwrap(), Codec::X264);
160        assert_eq!("h264".parse::<Codec>().unwrap(), Codec::X264);
161        assert_eq!("libx265".parse::<Codec>().unwrap(), Codec::X265);
162        assert_eq!("x265".parse::<Codec>().unwrap(), Codec::X265);
163        assert_eq!("h265".parse::<Codec>().unwrap(), Codec::X265);
164        assert_eq!("hevc".parse::<Codec>().unwrap(), Codec::X265);
165        assert_eq!("libsvtav1".parse::<Codec>().unwrap(), Codec::SvtAv1);
166        assert_eq!("svtav1".parse::<Codec>().unwrap(), Codec::SvtAv1);
167        assert_eq!("av1".parse::<Codec>().unwrap(), Codec::SvtAv1);
168        assert!("unknown".parse::<Codec>().is_err());
169    }
170
171    #[test]
172    fn test_codec_serde_roundtrip() {
173        for codec in &[Codec::X264, Codec::X265, Codec::SvtAv1] {
174            let json = serde_json::to_string(codec).unwrap();
175            let back: Codec = serde_json::from_str(&json).unwrap();
176            assert_eq!(*codec, back);
177        }
178    }
179
180    #[test]
181    fn test_codec_eq() {
182        assert_eq!(Codec::X264, Codec::X264);
183        assert_ne!(Codec::X264, Codec::X265);
184    }
185
186    #[test]
187    fn test_codec_hash() {
188        use std::collections::HashSet;
189        let mut set = HashSet::new();
190        set.insert(Codec::X264);
191        set.insert(Codec::X264);
192        assert_eq!(set.len(), 1);
193    }
194
195    #[test]
196    fn test_resolution_new() {
197        let r = Resolution::new(1920, 1080);
198        assert_eq!(r.width, 1920);
199        assert_eq!(r.height, 1080);
200    }
201
202    #[test]
203    fn test_resolution_label() {
204        assert_eq!(Resolution::new(3840, 2160).label(), "2160p");
205        assert_eq!(Resolution::new(2560, 1440).label(), "1440p");
206        assert_eq!(Resolution::new(1920, 1080).label(), "1080p");
207        assert_eq!(Resolution::new(1280, 720).label(), "720p");
208        assert_eq!(Resolution::new(854, 480).label(), "480p");
209        assert_eq!(Resolution::new(640, 360).label(), "360p");
210        assert_eq!(Resolution::new(426, 240).label(), "240p");
211        assert_eq!(Resolution::new(320, 200).label(), "200p");
212    }
213
214    #[test]
215    fn test_resolution_display() {
216        assert_eq!(format!("{}", Resolution::new(1920, 1080)), "1920x1080");
217        assert_eq!(format!("{}", Resolution::new(640, 360)), "640x360");
218    }
219
220    #[test]
221    fn test_resolution_from_str() {
222        assert_eq!("1080p".parse::<Resolution>().unwrap(), RES_1080P);
223        assert_eq!("720p".parse::<Resolution>().unwrap(), RES_720P);
224        assert_eq!("480p".parse::<Resolution>().unwrap(), RES_480P);
225        assert_eq!("360p".parse::<Resolution>().unwrap(), RES_360P);
226        assert_eq!("240p".parse::<Resolution>().unwrap(), RES_240P);
227        assert_eq!("1440p".parse::<Resolution>().unwrap(), RES_1440P);
228        assert_eq!("2160p".parse::<Resolution>().unwrap(), RES_2160P);
229        assert_eq!("4k".parse::<Resolution>().unwrap(), RES_2160P);
230        assert_eq!("1920x1080".parse::<Resolution>().unwrap(), RES_1080P);
231        assert_eq!("640x360".parse::<Resolution>().unwrap(), RES_360P);
232        assert!("invalid".parse::<Resolution>().is_err());
233    }
234
235    #[test]
236    fn test_resolution_serde_roundtrip() {
237        let r = RES_1080P;
238        let json = serde_json::to_string(&r).unwrap();
239        let back: Resolution = serde_json::from_str(&json).unwrap();
240        assert_eq!(r, back);
241    }
242
243    #[test]
244    fn test_resolution_const_equality() {
245        assert_eq!(RES_2160P, Resolution::new(3840, 2160));
246        assert_eq!(RES_1440P, Resolution::new(2560, 1440));
247        assert_eq!(RES_1080P, Resolution::new(1920, 1080));
248        assert_eq!(RES_720P, Resolution::new(1280, 720));
249        assert_eq!(RES_480P, Resolution::new(854, 480));
250        assert_eq!(RES_360P, Resolution::new(640, 360));
251        assert_eq!(RES_240P, Resolution::new(426, 240));
252    }
253
254    #[test]
255    fn test_rate_control_mode_default() {
256        assert_eq!(RateControlMode::default(), RateControlMode::Crf);
257    }
258
259    #[test]
260    fn test_rate_control_mode_serde() {
261        let json = serde_json::to_string(&RateControlMode::Crf).unwrap();
262        assert_eq!(json, "\"crf\"");
263        let back: RateControlMode = serde_json::from_str("\"vbr\"").unwrap();
264        assert_eq!(back, RateControlMode::Vbr);
265    }
266
267    #[test]
268    fn test_ffmpeg_path_default() {
269        let path = ffmpeg_path();
270        assert!(!path.is_empty());
271    }
272
273    #[test]
274    fn test_ffprobe_path_default() {
275        let path = ffprobe_path();
276        assert!(!path.is_empty());
277    }
278
279    #[test]
280    fn test_ffmpeg_path_respects_env() {
281        // SAFETY: test-only env var manipulation, single-threaded test
282        let old = std::env::var("VISER_FFMPEG").ok();
283        unsafe {
284            std::env::set_var("VISER_FFMPEG", "/custom/ffmpeg");
285        }
286        assert_eq!(ffmpeg_path(), "/custom/ffmpeg");
287        unsafe {
288            match old {
289                Some(v) => std::env::set_var("VISER_FFMPEG", v),
290                None => std::env::remove_var("VISER_FFMPEG"),
291            }
292        }
293    }
294
295    #[test]
296    fn test_ffprobe_path_respects_env() {
297        // SAFETY: test-only env var manipulation, single-threaded test
298        let old = std::env::var("VISER_FFPROBE").ok();
299        unsafe {
300            std::env::set_var("VISER_FFPROBE", "/custom/ffprobe");
301        }
302        assert_eq!(ffprobe_path(), "/custom/ffprobe");
303        unsafe {
304            match old {
305                Some(v) => std::env::set_var("VISER_FFPROBE", v),
306                None => std::env::remove_var("VISER_FFPROBE"),
307            }
308        }
309    }
310}