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