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