Skip to main content

viser_ffmpeg/
lib.rs

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