1mod cache;
8mod color;
9mod encode;
10mod hw_encode;
11mod path;
12mod probe;
13#[cfg(feature = "revelo")]
14mod probe_revelo;
15#[cfg(feature = "revelo")]
16pub use probe_revelo::probe as probe_revelo;
17
18pub use cache::*;
19pub use color::*;
20pub use encode::*;
21pub use hw_encode::*;
22pub use path::*;
23pub use probe::*;
24
25use serde::{Deserialize, Serialize};
26use std::fmt;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub enum Codec {
36 #[serde(rename = "libx264")]
38 X264,
39 #[serde(rename = "libx265")]
41 X265,
42 #[serde(rename = "libsvtav1")]
44 SvtAv1,
45
46 #[serde(rename = "h264_nvenc")]
49 NvencH264,
50 #[serde(rename = "h264_qsv")]
52 QsvH264,
53 #[serde(rename = "h264_videotoolbox")]
55 VideoToolboxH264,
56 #[serde(rename = "h264_vaapi")]
58 VaapiH264,
59 #[serde(rename = "h264_amf")]
61 AmfH264,
62
63 #[serde(rename = "hevc_nvenc")]
66 NvencH265,
67 #[serde(rename = "hevc_qsv")]
69 QsvH265,
70 #[serde(rename = "hevc_videotoolbox")]
72 VideoToolboxH265,
73 #[serde(rename = "hevc_vaapi")]
75 VaapiH265,
76 #[serde(rename = "hevc_amf")]
78 AmfH265,
79
80 #[serde(rename = "av1_nvenc")]
84 NvencAv1,
85 #[serde(rename = "av1_qsv")]
87 QsvAv1,
88 #[serde(rename = "av1_vaapi")]
90 VaapiAv1,
91 #[serde(rename = "av1_amf")]
93 AmfAv1,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum EncoderBackend {
99 Software,
101 Nvenc,
103 Qsv,
105 VideoToolbox,
107 Vaapi,
109 Amf,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum CodecFamily {
116 H264,
118 H265,
120 Av1,
122}
123
124impl Codec {
125 pub fn as_str(&self) -> &'static str {
127 match self {
128 Codec::X264 => "libx264",
129 Codec::X265 => "libx265",
130 Codec::SvtAv1 => "libsvtav1",
131 Codec::NvencH264 => "h264_nvenc",
132 Codec::QsvH264 => "h264_qsv",
133 Codec::VideoToolboxH264 => "h264_videotoolbox",
134 Codec::VaapiH264 => "h264_vaapi",
135 Codec::AmfH264 => "h264_amf",
136 Codec::NvencH265 => "hevc_nvenc",
137 Codec::QsvH265 => "hevc_qsv",
138 Codec::VideoToolboxH265 => "hevc_videotoolbox",
139 Codec::VaapiH265 => "hevc_vaapi",
140 Codec::AmfH265 => "hevc_amf",
141 Codec::NvencAv1 => "av1_nvenc",
142 Codec::QsvAv1 => "av1_qsv",
143 Codec::VaapiAv1 => "av1_vaapi",
144 Codec::AmfAv1 => "av1_amf",
145 }
146 }
147
148 pub fn backend(&self) -> EncoderBackend {
150 match self {
151 Codec::X264 | Codec::X265 | Codec::SvtAv1 => EncoderBackend::Software,
152 Codec::NvencH264 | Codec::NvencH265 | Codec::NvencAv1 => EncoderBackend::Nvenc,
153 Codec::QsvH264 | Codec::QsvH265 | Codec::QsvAv1 => EncoderBackend::Qsv,
154 Codec::VideoToolboxH264 | Codec::VideoToolboxH265 => EncoderBackend::VideoToolbox,
155 Codec::VaapiH264 | Codec::VaapiH265 | Codec::VaapiAv1 => EncoderBackend::Vaapi,
156 Codec::AmfH264 | Codec::AmfH265 | Codec::AmfAv1 => EncoderBackend::Amf,
157 }
158 }
159
160 pub fn family(&self) -> CodecFamily {
162 match self {
163 Codec::X264
164 | Codec::NvencH264
165 | Codec::QsvH264
166 | Codec::VideoToolboxH264
167 | Codec::VaapiH264
168 | Codec::AmfH264 => CodecFamily::H264,
169 Codec::X265
170 | Codec::NvencH265
171 | Codec::QsvH265
172 | Codec::VideoToolboxH265
173 | Codec::VaapiH265
174 | Codec::AmfH265 => CodecFamily::H265,
175 Codec::SvtAv1 | Codec::NvencAv1 | Codec::QsvAv1 | Codec::VaapiAv1 | Codec::AmfAv1 => {
176 CodecFamily::Av1
177 }
178 }
179 }
180
181 pub fn is_hardware(&self) -> bool {
183 !matches!(self.backend(), EncoderBackend::Software)
184 }
185
186 pub fn is_software(&self) -> bool {
188 matches!(self.backend(), EncoderBackend::Software)
189 }
190}
191
192impl fmt::Display for Codec {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.write_str(self.as_str())
195 }
196}
197
198impl std::str::FromStr for Codec {
199 type Err = anyhow::Error;
200
201 fn from_str(s: &str) -> Result<Self, Self::Err> {
202 match s {
203 "libx264" | "x264" | "h264" => Ok(Codec::X264),
204 "libx265" | "x265" | "h265" | "hevc" => Ok(Codec::X265),
205 "libsvtav1" | "svtav1" | "av1" => Ok(Codec::SvtAv1),
206 "h264_nvenc" | "nvenc" | "nvenc_h264" => Ok(Codec::NvencH264),
208 "hevc_nvenc" | "nvenc_h265" | "nvenc_hevc" => Ok(Codec::NvencH265),
209 "h264_qsv" | "qsv" | "qsv_h264" => Ok(Codec::QsvH264),
211 "hevc_qsv" | "qsv_h265" | "qsv_hevc" => Ok(Codec::QsvH265),
212 "h264_videotoolbox" | "vt" | "vt_h264" | "videotoolbox" => Ok(Codec::VideoToolboxH264),
214 "hevc_videotoolbox" | "vt_h265" | "vt_hevc" => Ok(Codec::VideoToolboxH265),
215 "h264_vaapi" | "vaapi" | "vaapi_h264" => Ok(Codec::VaapiH264),
217 "hevc_vaapi" | "vaapi_h265" | "vaapi_hevc" => Ok(Codec::VaapiH265),
218 "h264_amf" | "amf" | "amf_h264" => Ok(Codec::AmfH264),
220 "hevc_amf" | "amf_h265" | "amf_hevc" => Ok(Codec::AmfH265),
221 "av1_nvenc" | "nvenc_av1" => Ok(Codec::NvencAv1),
223 "av1_qsv" | "qsv_av1" => Ok(Codec::QsvAv1),
224 "av1_vaapi" | "vaapi_av1" => Ok(Codec::VaapiAv1),
225 "av1_amf" | "amf_av1" => Ok(Codec::AmfAv1),
226 _ => Err(anyhow::anyhow!("unknown codec: {s}")),
227 }
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
233pub struct Resolution {
234 pub width: i32,
236 pub height: i32,
238}
239
240impl Resolution {
241 pub const fn new(width: i32, height: i32) -> Self {
243 Self { width, height }
244 }
245
246 pub fn label(&self) -> String {
248 match self.height {
249 h if h >= 2160 => "2160p".into(),
250 h if h >= 1440 => "1440p".into(),
251 h if h >= 1080 => "1080p".into(),
252 h if h >= 720 => "720p".into(),
253 h if h >= 480 => "480p".into(),
254 h if h >= 360 => "360p".into(),
255 h if h >= 240 => "240p".into(),
256 h => format!("{h}p"),
257 }
258 }
259}
260
261impl fmt::Display for Resolution {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 write!(f, "{}x{}", self.width, self.height)
264 }
265}
266
267impl std::str::FromStr for Resolution {
268 type Err = anyhow::Error;
269
270 fn from_str(s: &str) -> Result<Self, Self::Err> {
271 match s.to_lowercase().as_str() {
272 "2160p" | "4k" => Ok(RES_2160P),
273 "1440p" => Ok(RES_1440P),
274 "1080p" => Ok(RES_1080P),
275 "720p" => Ok(RES_720P),
276 "480p" => Ok(RES_480P),
277 "360p" => Ok(RES_360P),
278 "240p" => Ok(RES_240P),
279 other => {
280 if let Some((w, h)) = other.split_once('x') {
281 Ok(Resolution::new(w.parse()?, h.parse()?))
282 } else {
283 Err(anyhow::anyhow!("invalid resolution: {other}"))
284 }
285 }
286 }
287 }
288}
289
290pub const RES_2160P: Resolution = Resolution::new(3840, 2160);
292pub const RES_1440P: Resolution = Resolution::new(2560, 1440);
294pub const RES_1080P: Resolution = Resolution::new(1920, 1080);
296pub const RES_720P: Resolution = Resolution::new(1280, 720);
298pub const RES_480P: Resolution = Resolution::new(854, 480);
300pub const RES_360P: Resolution = Resolution::new(640, 360);
302pub const RES_240P: Resolution = Resolution::new(426, 240);
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
307#[serde(rename_all = "lowercase")]
308pub enum RateControlMode {
309 #[default]
311 Crf,
312 CappedCrf,
314 Qp,
316 Vbr,
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_codec_as_str() {
326 assert_eq!(Codec::X264.as_str(), "libx264");
327 assert_eq!(Codec::X265.as_str(), "libx265");
328 assert_eq!(Codec::SvtAv1.as_str(), "libsvtav1");
329 assert_eq!(Codec::NvencH264.as_str(), "h264_nvenc");
330 assert_eq!(Codec::QsvH264.as_str(), "h264_qsv");
331 assert_eq!(Codec::VideoToolboxH264.as_str(), "h264_videotoolbox");
332 assert_eq!(Codec::VaapiH264.as_str(), "h264_vaapi");
333 assert_eq!(Codec::AmfH264.as_str(), "h264_amf");
334 assert_eq!(Codec::NvencH265.as_str(), "hevc_nvenc");
335 }
336
337 #[test]
338 fn test_codec_display() {
339 assert_eq!(format!("{}", Codec::X264), "libx264");
340 assert_eq!(format!("{}", Codec::NvencH264), "h264_nvenc");
341 }
342
343 #[test]
344 fn test_codec_from_str() {
345 assert_eq!("libx264".parse::<Codec>().unwrap(), Codec::X264);
346 assert_eq!("x264".parse::<Codec>().unwrap(), Codec::X264);
347 assert_eq!("h264".parse::<Codec>().unwrap(), Codec::X264);
348 assert_eq!("libx265".parse::<Codec>().unwrap(), Codec::X265);
349 assert_eq!("x265".parse::<Codec>().unwrap(), Codec::X265);
350 assert_eq!("h265".parse::<Codec>().unwrap(), Codec::X265);
351 assert_eq!("hevc".parse::<Codec>().unwrap(), Codec::X265);
352 assert_eq!("libsvtav1".parse::<Codec>().unwrap(), Codec::SvtAv1);
353 assert_eq!("svtav1".parse::<Codec>().unwrap(), Codec::SvtAv1);
354 assert_eq!("av1".parse::<Codec>().unwrap(), Codec::SvtAv1);
355 assert_eq!("h264_nvenc".parse::<Codec>().unwrap(), Codec::NvencH264);
356 assert_eq!("nvenc".parse::<Codec>().unwrap(), Codec::NvencH264);
357 assert_eq!("hevc_nvenc".parse::<Codec>().unwrap(), Codec::NvencH265);
358 assert_eq!("h264_qsv".parse::<Codec>().unwrap(), Codec::QsvH264);
359 assert_eq!("qsv".parse::<Codec>().unwrap(), Codec::QsvH264);
360 assert_eq!("vt".parse::<Codec>().unwrap(), Codec::VideoToolboxH264);
361 assert_eq!("h264_vaapi".parse::<Codec>().unwrap(), Codec::VaapiH264);
362 assert_eq!("vaapi".parse::<Codec>().unwrap(), Codec::VaapiH264);
363 assert_eq!("h264_amf".parse::<Codec>().unwrap(), Codec::AmfH264);
364 assert_eq!("amf".parse::<Codec>().unwrap(), Codec::AmfH264);
365 assert!("unknown".parse::<Codec>().is_err());
366 }
367
368 #[test]
369 fn test_codec_backend() {
370 assert_eq!(Codec::X264.backend(), EncoderBackend::Software);
371 assert_eq!(Codec::NvencH264.backend(), EncoderBackend::Nvenc);
372 assert_eq!(Codec::QsvH264.backend(), EncoderBackend::Qsv);
373 assert_eq!(Codec::VideoToolboxH264.backend(), EncoderBackend::VideoToolbox);
374 assert_eq!(Codec::VaapiH264.backend(), EncoderBackend::Vaapi);
375 assert_eq!(Codec::AmfH264.backend(), EncoderBackend::Amf);
376 }
377
378 #[test]
379 fn test_codec_family() {
380 assert_eq!(Codec::X264.family(), CodecFamily::H264);
381 assert_eq!(Codec::NvencH264.family(), CodecFamily::H264);
382 assert_eq!(Codec::X265.family(), CodecFamily::H265);
383 assert_eq!(Codec::NvencH265.family(), CodecFamily::H265);
384 assert_eq!(Codec::SvtAv1.family(), CodecFamily::Av1);
385 }
386
387 #[test]
388 fn test_codec_is_hardware() {
389 assert!(!Codec::X264.is_hardware());
390 assert!(!Codec::X265.is_hardware());
391 assert!(!Codec::SvtAv1.is_hardware());
392 assert!(Codec::NvencH264.is_hardware());
393 assert!(Codec::QsvH265.is_hardware());
394 assert!(Codec::VideoToolboxH264.is_hardware());
395 }
396
397 #[test]
398 fn test_codec_is_software() {
399 assert!(Codec::X264.is_software());
400 assert!(!Codec::NvencH264.is_software());
401 }
402
403 #[test]
404 fn test_codec_serde_roundtrip() {
405 for codec in &[
406 Codec::X264,
407 Codec::X265,
408 Codec::SvtAv1,
409 Codec::NvencH264,
410 Codec::NvencH265,
411 Codec::QsvH264,
412 ] {
413 let json = serde_json::to_string(codec).unwrap();
414 let back: Codec = serde_json::from_str(&json).unwrap();
415 assert_eq!(*codec, back);
416 }
417 }
418
419 #[test]
420 fn test_av1_hw_codec_as_str() {
421 assert_eq!(Codec::NvencAv1.as_str(), "av1_nvenc");
422 assert_eq!(Codec::QsvAv1.as_str(), "av1_qsv");
423 assert_eq!(Codec::VaapiAv1.as_str(), "av1_vaapi");
424 assert_eq!(Codec::AmfAv1.as_str(), "av1_amf");
425 }
426
427 #[test]
428 fn test_av1_hw_codec_from_str() {
429 assert_eq!("av1_nvenc".parse::<Codec>().unwrap(), Codec::NvencAv1);
430 assert_eq!("nvenc_av1".parse::<Codec>().unwrap(), Codec::NvencAv1);
431 assert_eq!("av1_qsv".parse::<Codec>().unwrap(), Codec::QsvAv1);
432 assert_eq!("av1_vaapi".parse::<Codec>().unwrap(), Codec::VaapiAv1);
433 assert_eq!("av1_amf".parse::<Codec>().unwrap(), Codec::AmfAv1);
434 }
435
436 #[test]
437 fn test_av1_hw_codec_backend_and_family() {
438 for codec in &[Codec::NvencAv1, Codec::QsvAv1, Codec::VaapiAv1, Codec::AmfAv1] {
439 assert_eq!(codec.family(), CodecFamily::Av1);
440 assert!(codec.is_hardware());
441 }
442 assert_eq!(Codec::NvencAv1.backend(), EncoderBackend::Nvenc);
443 assert_eq!(Codec::QsvAv1.backend(), EncoderBackend::Qsv);
444 assert_eq!(Codec::VaapiAv1.backend(), EncoderBackend::Vaapi);
445 assert_eq!(Codec::AmfAv1.backend(), EncoderBackend::Amf);
446 }
447
448 #[test]
449 fn test_codec_eq() {
450 assert_eq!(Codec::X264, Codec::X264);
451 assert_ne!(Codec::X264, Codec::X265);
452 assert_ne!(Codec::X264, Codec::NvencH264);
453 }
454
455 #[test]
456 fn test_codec_hash() {
457 use std::collections::HashSet;
458 let mut set = HashSet::new();
459 set.insert(Codec::X264);
460 set.insert(Codec::X264);
461 assert_eq!(set.len(), 1);
462 }
463
464 #[test]
465 fn test_resolution_new() {
466 let r = Resolution::new(1920, 1080);
467 assert_eq!(r.width, 1920);
468 assert_eq!(r.height, 1080);
469 }
470
471 #[test]
472 fn test_resolution_label() {
473 assert_eq!(Resolution::new(3840, 2160).label(), "2160p");
474 assert_eq!(Resolution::new(2560, 1440).label(), "1440p");
475 assert_eq!(Resolution::new(1920, 1080).label(), "1080p");
476 assert_eq!(Resolution::new(1280, 720).label(), "720p");
477 assert_eq!(Resolution::new(854, 480).label(), "480p");
478 assert_eq!(Resolution::new(640, 360).label(), "360p");
479 assert_eq!(Resolution::new(426, 240).label(), "240p");
480 assert_eq!(Resolution::new(320, 200).label(), "200p");
481 }
482
483 #[test]
484 fn test_resolution_display() {
485 assert_eq!(format!("{}", Resolution::new(1920, 1080)), "1920x1080");
486 assert_eq!(format!("{}", Resolution::new(640, 360)), "640x360");
487 }
488
489 #[test]
490 fn test_resolution_from_str() {
491 assert_eq!("1080p".parse::<Resolution>().unwrap(), RES_1080P);
492 assert_eq!("720p".parse::<Resolution>().unwrap(), RES_720P);
493 assert_eq!("480p".parse::<Resolution>().unwrap(), RES_480P);
494 assert_eq!("360p".parse::<Resolution>().unwrap(), RES_360P);
495 assert_eq!("240p".parse::<Resolution>().unwrap(), RES_240P);
496 assert_eq!("1440p".parse::<Resolution>().unwrap(), RES_1440P);
497 assert_eq!("2160p".parse::<Resolution>().unwrap(), RES_2160P);
498 assert_eq!("4k".parse::<Resolution>().unwrap(), RES_2160P);
499 assert_eq!("1920x1080".parse::<Resolution>().unwrap(), RES_1080P);
500 assert_eq!("640x360".parse::<Resolution>().unwrap(), RES_360P);
501 assert!("invalid".parse::<Resolution>().is_err());
502 }
503
504 #[test]
505 fn test_resolution_serde_roundtrip() {
506 let r = RES_1080P;
507 let json = serde_json::to_string(&r).unwrap();
508 let back: Resolution = serde_json::from_str(&json).unwrap();
509 assert_eq!(r, back);
510 }
511
512 #[test]
513 fn test_resolution_const_equality() {
514 assert_eq!(RES_2160P, Resolution::new(3840, 2160));
515 assert_eq!(RES_1440P, Resolution::new(2560, 1440));
516 assert_eq!(RES_1080P, Resolution::new(1920, 1080));
517 assert_eq!(RES_720P, Resolution::new(1280, 720));
518 assert_eq!(RES_480P, Resolution::new(854, 480));
519 assert_eq!(RES_360P, Resolution::new(640, 360));
520 assert_eq!(RES_240P, Resolution::new(426, 240));
521 }
522
523 #[test]
524 fn test_rate_control_mode_default() {
525 assert_eq!(RateControlMode::default(), RateControlMode::Crf);
526 }
527
528 #[test]
529 fn test_rate_control_mode_serde() {
530 let json = serde_json::to_string(&RateControlMode::Crf).unwrap();
531 assert_eq!(json, "\"crf\"");
532 let back: RateControlMode = serde_json::from_str("\"vbr\"").unwrap();
533 assert_eq!(back, RateControlMode::Vbr);
534 }
535
536 #[test]
537 fn test_ffmpeg_path_default() {
538 let path = ffmpeg_path();
539 assert!(!path.is_empty());
540 }
541
542 #[test]
543 fn test_ffprobe_path_default() {
544 let path = ffprobe_path();
545 assert!(!path.is_empty());
546 }
547
548 #[test]
549 fn test_ffmpeg_path_respects_env() {
550 let old = std::env::var("VISER_FFMPEG").ok();
552 unsafe {
553 std::env::set_var("VISER_FFMPEG", "/custom/ffmpeg");
554 }
555 assert_eq!(ffmpeg_path(), "/custom/ffmpeg");
556 unsafe {
557 match old {
558 Some(v) => std::env::set_var("VISER_FFMPEG", v),
559 None => std::env::remove_var("VISER_FFMPEG"),
560 }
561 }
562 }
563
564 #[test]
565 fn test_ffprobe_path_respects_env() {
566 let old = std::env::var("VISER_FFPROBE").ok();
568 unsafe {
569 std::env::set_var("VISER_FFPROBE", "/custom/ffprobe");
570 }
571 assert_eq!(ffprobe_path(), "/custom/ffprobe");
572 unsafe {
573 match old {
574 Some(v) => std::env::set_var("VISER_FFPROBE", v),
575 None => std::env::remove_var("VISER_FFPROBE"),
576 }
577 }
578 }
579}