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#[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#[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 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
114pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
125#[serde(rename_all = "lowercase")]
126pub enum RateControlMode {
127 #[default]
129 Crf,
130 CappedCrf,
132 Qp,
134 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 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 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}