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