1mod cache;
8mod encode;
9mod hw_encode;
10mod path;
11mod probe;
12#[cfg(feature = "revelo")]
13mod probe_revelo;
14#[cfg(feature = "revelo")]
15pub use probe_revelo::probe as probe_revelo;
16
17pub use cache::*;
18pub use encode::*;
19pub use hw_encode::*;
20pub use path::*;
21pub use probe::*;
22
23use serde::{Deserialize, Serialize};
24use std::fmt;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub enum Codec {
34 #[serde(rename = "libx264")]
36 X264,
37 #[serde(rename = "libx265")]
39 X265,
40 #[serde(rename = "libsvtav1")]
42 SvtAv1,
43
44 #[serde(rename = "h264_nvenc")]
47 NvencH264,
48 #[serde(rename = "h264_qsv")]
50 QsvH264,
51 #[serde(rename = "h264_videotoolbox")]
53 VideoToolboxH264,
54 #[serde(rename = "h264_vaapi")]
56 VaapiH264,
57 #[serde(rename = "h264_amf")]
59 AmfH264,
60
61 #[serde(rename = "hevc_nvenc")]
64 NvencH265,
65 #[serde(rename = "hevc_qsv")]
67 QsvH265,
68 #[serde(rename = "hevc_videotoolbox")]
70 VideoToolboxH265,
71 #[serde(rename = "hevc_vaapi")]
73 VaapiH265,
74 #[serde(rename = "hevc_amf")]
76 AmfH265,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum EncoderBackend {
82 Software,
84 Nvenc,
86 Qsv,
88 VideoToolbox,
90 Vaapi,
92 Amf,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum CodecFamily {
99 H264,
101 H265,
103 Av1,
105}
106
107impl Codec {
108 pub fn as_str(&self) -> &'static str {
110 match self {
111 Codec::X264 => "libx264",
112 Codec::X265 => "libx265",
113 Codec::SvtAv1 => "libsvtav1",
114 Codec::NvencH264 => "h264_nvenc",
115 Codec::QsvH264 => "h264_qsv",
116 Codec::VideoToolboxH264 => "h264_videotoolbox",
117 Codec::VaapiH264 => "h264_vaapi",
118 Codec::AmfH264 => "h264_amf",
119 Codec::NvencH265 => "hevc_nvenc",
120 Codec::QsvH265 => "hevc_qsv",
121 Codec::VideoToolboxH265 => "hevc_videotoolbox",
122 Codec::VaapiH265 => "hevc_vaapi",
123 Codec::AmfH265 => "hevc_amf",
124 }
125 }
126
127 pub fn backend(&self) -> EncoderBackend {
129 match self {
130 Codec::X264 | Codec::X265 | Codec::SvtAv1 => EncoderBackend::Software,
131 Codec::NvencH264 | Codec::NvencH265 => EncoderBackend::Nvenc,
132 Codec::QsvH264 | Codec::QsvH265 => EncoderBackend::Qsv,
133 Codec::VideoToolboxH264 | Codec::VideoToolboxH265 => EncoderBackend::VideoToolbox,
134 Codec::VaapiH264 | Codec::VaapiH265 => EncoderBackend::Vaapi,
135 Codec::AmfH264 | Codec::AmfH265 => EncoderBackend::Amf,
136 }
137 }
138
139 pub fn family(&self) -> CodecFamily {
141 match self {
142 Codec::X264
143 | Codec::NvencH264
144 | Codec::QsvH264
145 | Codec::VideoToolboxH264
146 | Codec::VaapiH264
147 | Codec::AmfH264 => CodecFamily::H264,
148 Codec::X265
149 | Codec::NvencH265
150 | Codec::QsvH265
151 | Codec::VideoToolboxH265
152 | Codec::VaapiH265
153 | Codec::AmfH265 => CodecFamily::H265,
154 Codec::SvtAv1 => CodecFamily::Av1,
155 }
156 }
157
158 pub fn is_hardware(&self) -> bool {
160 !matches!(self.backend(), EncoderBackend::Software)
161 }
162
163 pub fn is_software(&self) -> bool {
165 matches!(self.backend(), EncoderBackend::Software)
166 }
167}
168
169impl fmt::Display for Codec {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.write_str(self.as_str())
172 }
173}
174
175impl std::str::FromStr for Codec {
176 type Err = anyhow::Error;
177
178 fn from_str(s: &str) -> Result<Self, Self::Err> {
179 match s {
180 "libx264" | "x264" | "h264" => Ok(Codec::X264),
181 "libx265" | "x265" | "h265" | "hevc" => Ok(Codec::X265),
182 "libsvtav1" | "svtav1" | "av1" => Ok(Codec::SvtAv1),
183 "h264_nvenc" | "nvenc" | "nvenc_h264" => Ok(Codec::NvencH264),
185 "hevc_nvenc" | "nvenc_h265" | "nvenc_hevc" => Ok(Codec::NvencH265),
186 "h264_qsv" | "qsv" | "qsv_h264" => Ok(Codec::QsvH264),
188 "hevc_qsv" | "qsv_h265" | "qsv_hevc" => Ok(Codec::QsvH265),
189 "h264_videotoolbox" | "vt" | "vt_h264" | "videotoolbox" => Ok(Codec::VideoToolboxH264),
191 "hevc_videotoolbox" | "vt_h265" | "vt_hevc" => Ok(Codec::VideoToolboxH265),
192 "h264_vaapi" | "vaapi" | "vaapi_h264" => Ok(Codec::VaapiH264),
194 "hevc_vaapi" | "vaapi_h265" | "vaapi_hevc" => Ok(Codec::VaapiH265),
195 "h264_amf" | "amf" | "amf_h264" => Ok(Codec::AmfH264),
197 "hevc_amf" | "amf_h265" | "amf_hevc" => Ok(Codec::AmfH265),
198 _ => Err(anyhow::anyhow!("unknown codec: {s}")),
199 }
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
205pub struct Resolution {
206 pub width: i32,
208 pub height: i32,
210}
211
212impl Resolution {
213 pub const fn new(width: i32, height: i32) -> Self {
215 Self { width, height }
216 }
217
218 pub fn label(&self) -> String {
220 match self.height {
221 h if h >= 2160 => "2160p".into(),
222 h if h >= 1440 => "1440p".into(),
223 h if h >= 1080 => "1080p".into(),
224 h if h >= 720 => "720p".into(),
225 h if h >= 480 => "480p".into(),
226 h if h >= 360 => "360p".into(),
227 h if h >= 240 => "240p".into(),
228 h => format!("{h}p"),
229 }
230 }
231}
232
233impl fmt::Display for Resolution {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 write!(f, "{}x{}", self.width, self.height)
236 }
237}
238
239impl std::str::FromStr for Resolution {
240 type Err = anyhow::Error;
241
242 fn from_str(s: &str) -> Result<Self, Self::Err> {
243 match s.to_lowercase().as_str() {
244 "2160p" | "4k" => Ok(RES_2160P),
245 "1440p" => Ok(RES_1440P),
246 "1080p" => Ok(RES_1080P),
247 "720p" => Ok(RES_720P),
248 "480p" => Ok(RES_480P),
249 "360p" => Ok(RES_360P),
250 "240p" => Ok(RES_240P),
251 other => {
252 if let Some((w, h)) = other.split_once('x') {
253 Ok(Resolution::new(w.parse()?, h.parse()?))
254 } else {
255 Err(anyhow::anyhow!("invalid resolution: {other}"))
256 }
257 }
258 }
259 }
260}
261
262pub const RES_2160P: Resolution = Resolution::new(3840, 2160);
264pub const RES_1440P: Resolution = Resolution::new(2560, 1440);
266pub const RES_1080P: Resolution = Resolution::new(1920, 1080);
268pub const RES_720P: Resolution = Resolution::new(1280, 720);
270pub const RES_480P: Resolution = Resolution::new(854, 480);
272pub const RES_360P: Resolution = Resolution::new(640, 360);
274pub const RES_240P: Resolution = Resolution::new(426, 240);
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
279#[serde(rename_all = "lowercase")]
280pub enum RateControlMode {
281 #[default]
283 Crf,
284 CappedCrf,
286 Qp,
288 Vbr,
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_codec_as_str() {
298 assert_eq!(Codec::X264.as_str(), "libx264");
299 assert_eq!(Codec::X265.as_str(), "libx265");
300 assert_eq!(Codec::SvtAv1.as_str(), "libsvtav1");
301 assert_eq!(Codec::NvencH264.as_str(), "h264_nvenc");
302 assert_eq!(Codec::QsvH264.as_str(), "h264_qsv");
303 assert_eq!(Codec::VideoToolboxH264.as_str(), "h264_videotoolbox");
304 assert_eq!(Codec::VaapiH264.as_str(), "h264_vaapi");
305 assert_eq!(Codec::AmfH264.as_str(), "h264_amf");
306 assert_eq!(Codec::NvencH265.as_str(), "hevc_nvenc");
307 }
308
309 #[test]
310 fn test_codec_display() {
311 assert_eq!(format!("{}", Codec::X264), "libx264");
312 assert_eq!(format!("{}", Codec::NvencH264), "h264_nvenc");
313 }
314
315 #[test]
316 fn test_codec_from_str() {
317 assert_eq!("libx264".parse::<Codec>().unwrap(), Codec::X264);
318 assert_eq!("x264".parse::<Codec>().unwrap(), Codec::X264);
319 assert_eq!("h264".parse::<Codec>().unwrap(), Codec::X264);
320 assert_eq!("libx265".parse::<Codec>().unwrap(), Codec::X265);
321 assert_eq!("x265".parse::<Codec>().unwrap(), Codec::X265);
322 assert_eq!("h265".parse::<Codec>().unwrap(), Codec::X265);
323 assert_eq!("hevc".parse::<Codec>().unwrap(), Codec::X265);
324 assert_eq!("libsvtav1".parse::<Codec>().unwrap(), Codec::SvtAv1);
325 assert_eq!("svtav1".parse::<Codec>().unwrap(), Codec::SvtAv1);
326 assert_eq!("av1".parse::<Codec>().unwrap(), Codec::SvtAv1);
327 assert_eq!("h264_nvenc".parse::<Codec>().unwrap(), Codec::NvencH264);
328 assert_eq!("nvenc".parse::<Codec>().unwrap(), Codec::NvencH264);
329 assert_eq!("hevc_nvenc".parse::<Codec>().unwrap(), Codec::NvencH265);
330 assert_eq!("h264_qsv".parse::<Codec>().unwrap(), Codec::QsvH264);
331 assert_eq!("qsv".parse::<Codec>().unwrap(), Codec::QsvH264);
332 assert_eq!("vt".parse::<Codec>().unwrap(), Codec::VideoToolboxH264);
333 assert_eq!("h264_vaapi".parse::<Codec>().unwrap(), Codec::VaapiH264);
334 assert_eq!("vaapi".parse::<Codec>().unwrap(), Codec::VaapiH264);
335 assert_eq!("h264_amf".parse::<Codec>().unwrap(), Codec::AmfH264);
336 assert_eq!("amf".parse::<Codec>().unwrap(), Codec::AmfH264);
337 assert!("unknown".parse::<Codec>().is_err());
338 }
339
340 #[test]
341 fn test_codec_backend() {
342 assert_eq!(Codec::X264.backend(), EncoderBackend::Software);
343 assert_eq!(Codec::NvencH264.backend(), EncoderBackend::Nvenc);
344 assert_eq!(Codec::QsvH264.backend(), EncoderBackend::Qsv);
345 assert_eq!(Codec::VideoToolboxH264.backend(), EncoderBackend::VideoToolbox);
346 assert_eq!(Codec::VaapiH264.backend(), EncoderBackend::Vaapi);
347 assert_eq!(Codec::AmfH264.backend(), EncoderBackend::Amf);
348 }
349
350 #[test]
351 fn test_codec_family() {
352 assert_eq!(Codec::X264.family(), CodecFamily::H264);
353 assert_eq!(Codec::NvencH264.family(), CodecFamily::H264);
354 assert_eq!(Codec::X265.family(), CodecFamily::H265);
355 assert_eq!(Codec::NvencH265.family(), CodecFamily::H265);
356 assert_eq!(Codec::SvtAv1.family(), CodecFamily::Av1);
357 }
358
359 #[test]
360 fn test_codec_is_hardware() {
361 assert!(!Codec::X264.is_hardware());
362 assert!(!Codec::X265.is_hardware());
363 assert!(!Codec::SvtAv1.is_hardware());
364 assert!(Codec::NvencH264.is_hardware());
365 assert!(Codec::QsvH265.is_hardware());
366 assert!(Codec::VideoToolboxH264.is_hardware());
367 }
368
369 #[test]
370 fn test_codec_is_software() {
371 assert!(Codec::X264.is_software());
372 assert!(!Codec::NvencH264.is_software());
373 }
374
375 #[test]
376 fn test_codec_serde_roundtrip() {
377 for codec in &[
378 Codec::X264,
379 Codec::X265,
380 Codec::SvtAv1,
381 Codec::NvencH264,
382 Codec::NvencH265,
383 Codec::QsvH264,
384 ] {
385 let json = serde_json::to_string(codec).unwrap();
386 let back: Codec = serde_json::from_str(&json).unwrap();
387 assert_eq!(*codec, back);
388 }
389 }
390
391 #[test]
392 fn test_codec_eq() {
393 assert_eq!(Codec::X264, Codec::X264);
394 assert_ne!(Codec::X264, Codec::X265);
395 assert_ne!(Codec::X264, Codec::NvencH264);
396 }
397
398 #[test]
399 fn test_codec_hash() {
400 use std::collections::HashSet;
401 let mut set = HashSet::new();
402 set.insert(Codec::X264);
403 set.insert(Codec::X264);
404 assert_eq!(set.len(), 1);
405 }
406
407 #[test]
408 fn test_resolution_new() {
409 let r = Resolution::new(1920, 1080);
410 assert_eq!(r.width, 1920);
411 assert_eq!(r.height, 1080);
412 }
413
414 #[test]
415 fn test_resolution_label() {
416 assert_eq!(Resolution::new(3840, 2160).label(), "2160p");
417 assert_eq!(Resolution::new(2560, 1440).label(), "1440p");
418 assert_eq!(Resolution::new(1920, 1080).label(), "1080p");
419 assert_eq!(Resolution::new(1280, 720).label(), "720p");
420 assert_eq!(Resolution::new(854, 480).label(), "480p");
421 assert_eq!(Resolution::new(640, 360).label(), "360p");
422 assert_eq!(Resolution::new(426, 240).label(), "240p");
423 assert_eq!(Resolution::new(320, 200).label(), "200p");
424 }
425
426 #[test]
427 fn test_resolution_display() {
428 assert_eq!(format!("{}", Resolution::new(1920, 1080)), "1920x1080");
429 assert_eq!(format!("{}", Resolution::new(640, 360)), "640x360");
430 }
431
432 #[test]
433 fn test_resolution_from_str() {
434 assert_eq!("1080p".parse::<Resolution>().unwrap(), RES_1080P);
435 assert_eq!("720p".parse::<Resolution>().unwrap(), RES_720P);
436 assert_eq!("480p".parse::<Resolution>().unwrap(), RES_480P);
437 assert_eq!("360p".parse::<Resolution>().unwrap(), RES_360P);
438 assert_eq!("240p".parse::<Resolution>().unwrap(), RES_240P);
439 assert_eq!("1440p".parse::<Resolution>().unwrap(), RES_1440P);
440 assert_eq!("2160p".parse::<Resolution>().unwrap(), RES_2160P);
441 assert_eq!("4k".parse::<Resolution>().unwrap(), RES_2160P);
442 assert_eq!("1920x1080".parse::<Resolution>().unwrap(), RES_1080P);
443 assert_eq!("640x360".parse::<Resolution>().unwrap(), RES_360P);
444 assert!("invalid".parse::<Resolution>().is_err());
445 }
446
447 #[test]
448 fn test_resolution_serde_roundtrip() {
449 let r = RES_1080P;
450 let json = serde_json::to_string(&r).unwrap();
451 let back: Resolution = serde_json::from_str(&json).unwrap();
452 assert_eq!(r, back);
453 }
454
455 #[test]
456 fn test_resolution_const_equality() {
457 assert_eq!(RES_2160P, Resolution::new(3840, 2160));
458 assert_eq!(RES_1440P, Resolution::new(2560, 1440));
459 assert_eq!(RES_1080P, Resolution::new(1920, 1080));
460 assert_eq!(RES_720P, Resolution::new(1280, 720));
461 assert_eq!(RES_480P, Resolution::new(854, 480));
462 assert_eq!(RES_360P, Resolution::new(640, 360));
463 assert_eq!(RES_240P, Resolution::new(426, 240));
464 }
465
466 #[test]
467 fn test_rate_control_mode_default() {
468 assert_eq!(RateControlMode::default(), RateControlMode::Crf);
469 }
470
471 #[test]
472 fn test_rate_control_mode_serde() {
473 let json = serde_json::to_string(&RateControlMode::Crf).unwrap();
474 assert_eq!(json, "\"crf\"");
475 let back: RateControlMode = serde_json::from_str("\"vbr\"").unwrap();
476 assert_eq!(back, RateControlMode::Vbr);
477 }
478
479 #[test]
480 fn test_ffmpeg_path_default() {
481 let path = ffmpeg_path();
482 assert!(!path.is_empty());
483 }
484
485 #[test]
486 fn test_ffprobe_path_default() {
487 let path = ffprobe_path();
488 assert!(!path.is_empty());
489 }
490
491 #[test]
492 fn test_ffmpeg_path_respects_env() {
493 let old = std::env::var("VISER_FFMPEG").ok();
495 unsafe {
496 std::env::set_var("VISER_FFMPEG", "/custom/ffmpeg");
497 }
498 assert_eq!(ffmpeg_path(), "/custom/ffmpeg");
499 unsafe {
500 match old {
501 Some(v) => std::env::set_var("VISER_FFMPEG", v),
502 None => std::env::remove_var("VISER_FFMPEG"),
503 }
504 }
505 }
506
507 #[test]
508 fn test_ffprobe_path_respects_env() {
509 let old = std::env::var("VISER_FFPROBE").ok();
511 unsafe {
512 std::env::set_var("VISER_FFPROBE", "/custom/ffprobe");
513 }
514 assert_eq!(ffprobe_path(), "/custom/ffprobe");
515 unsafe {
516 match old {
517 Some(v) => std::env::set_var("VISER_FFPROBE", v),
518 None => std::env::remove_var("VISER_FFPROBE"),
519 }
520 }
521 }
522}