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