1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum CodecFamily {
3 ProRes,
4 DNxHR,
5 HEVC,
6 H264,
7 AV1,
8 VP9,
9}
10
11impl CodecFamily {
12 pub fn name(&self) -> &'static str {
13 match self {
14 CodecFamily::ProRes => "ProRes",
15 CodecFamily::DNxHR => "DNxHR",
16 CodecFamily::HEVC => "HEVC",
17 CodecFamily::H264 => "H.264",
18 CodecFamily::AV1 => "AV1",
19 CodecFamily::VP9 => "VP9",
20 }
21 }
22
23 pub fn all() -> &'static [CodecFamily] {
24 &[
25 CodecFamily::ProRes,
26 CodecFamily::DNxHR,
27 CodecFamily::HEVC,
28 CodecFamily::H264,
29 CodecFamily::AV1,
30 CodecFamily::VP9,
31 ]
32 }
33
34 pub fn next(self) -> Self {
35 let all = Self::all();
36 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
37 all[(pos + 1) % all.len()]
38 }
39
40 pub fn prev(self) -> Self {
41 let all = Self::all();
42 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
43 all[(pos + all.len() - 1) % all.len()]
44 }
45
46 pub fn to_ffmpeg_args(
52 &self,
53 hevc_encoder: &str,
54 h264_encoder: &str,
55 av1_encoder: &str,
56 prores_encoder: &str,
57 prores: ProResProfile,
58 dnxhr: DnxhrProfile,
59 hevc: HevcProfile,
60 h264: H264Profile,
61 av1: Av1Profile,
62 vp9: Vp9Profile,
63 rate_control: &RateControl,
64 is_wide_gamut: bool,
65 ) -> (String, String, Vec<String>) {
66 let mut base_codec_name: String = String::new();
67 let mut base_pix_fmt: String = String::new();
68 let mut base_extra: Vec<&'static str> = Vec::new();
69
70 match self {
71 CodecFamily::ProRes => {
72 let (profile_v, base_pix) = match prores {
73 ProResProfile::Proxy => ("0", "yuv422p10le"),
74 ProResProfile::LT => ("1", "yuv422p10le"),
75 ProResProfile::Standard => ("2", "yuv422p10le"),
76 ProResProfile::HQ => ("3", "yuv422p10le"),
77 ProResProfile::P4444 => ("4", "yuva444p10le"),
78 ProResProfile::XQ4444 => ("5", "yuva444p12le"),
79 };
80 let pix_fmt = match (is_wide_gamut, prores) {
81 (true, ProResProfile::P4444 | ProResProfile::XQ4444) => base_pix,
82 (true, _) => "gbrp10le",
83 (false, _) => base_pix,
84 };
85 base_codec_name = prores_encoder.to_string();
86 base_pix_fmt = pix_fmt.to_string();
87 base_extra = vec!["-profile:v", profile_v];
88 }
89 CodecFamily::DNxHR => {
90 let (profile_str, pix_fmt) = match dnxhr {
91 DnxhrProfile::SQ => ("dnxhr_sq", "yuv422p10le"),
92 DnxhrProfile::HD => ("dnxhr_hd", "yuv422p10le"),
93 DnxhrProfile::HDX => ("dnxhr_hdx", "yuv422p10le"),
94 DnxhrProfile::HQX => ("dnxhr_hqx", "yuv422p10le"),
95 DnxhrProfile::P444 => ("dnxhr_444", "yuv444p10le"),
96 };
97 base_codec_name = "dnxhd".to_string();
98 base_pix_fmt = pix_fmt.to_string();
99 base_extra = vec!["-profile:v", profile_str];
100 }
101 CodecFamily::HEVC => {
102 match hevc_encoder {
103 "libx265" => {
104 if is_wide_gamut {
105 base_codec_name = "libx265".to_string();
106 base_pix_fmt = "gbrp10le".to_string();
107 base_extra = vec![];
108 } else {
109 let pix_fmt = match hevc {
110 HevcProfile::Main10_420 => "yuv420p10le",
111 HevcProfile::Main10_444 => "yuv444p10le",
112 };
113 base_codec_name = "libx265".to_string();
114 base_pix_fmt = pix_fmt.to_string();
115 base_extra = vec!["-preset", "slow"];
116 }
117 }
118 "hevc_nvenc" => {
119 base_codec_name = "hevc_nvenc".to_string();
120 base_pix_fmt = "p010le".to_string();
121 base_extra = vec!["-preset", "p6"];
122 }
123 "hevc_amf" => {
124 base_codec_name = "hevc_amf".to_string();
125 base_pix_fmt = "p010le".to_string();
126 base_extra = vec!["-quality", "quality"];
127 }
128 "hevc_qsv" => {
129 base_codec_name = "hevc_qsv".to_string();
130 base_pix_fmt = "p010le".to_string();
131 }
132 "hevc_videotoolbox" => {
133 base_codec_name = "hevc_videotoolbox".to_string();
134 base_pix_fmt = "p010le".to_string();
135 base_extra = vec!["-realtime", "true"];
136 }
137 _ => {
138 if is_wide_gamut {
139 base_codec_name = "libx265".to_string();
140 base_pix_fmt = "gbrp10le".to_string();
141 base_extra = vec!["-preset", "slow"];
142 } else {
143 let pix_fmt = match hevc {
144 HevcProfile::Main10_420 => "yuv420p10le",
145 HevcProfile::Main10_444 => "yuv444p10le",
146 };
147 base_codec_name = "libx265".to_string();
148 base_pix_fmt = pix_fmt.to_string();
149 base_extra = vec!["-pix_fmt", pix_fmt, "-preset", "slow"];
150 }
151 }
152 }
153 }
154 CodecFamily::H264 => {
155 if is_wide_gamut {
156 base_codec_name = "libx265".to_string();
159 base_pix_fmt = "gbrp10le".to_string();
160 base_extra = vec!["-pix_fmt", "gbrp10le"];
161 } else {
162 match h264_encoder {
163 "h264_nvenc" => {
164 let (pf, ext) = match h264 {
165 H264Profile::High10bit => ("p010le", vec!["-preset", "p6", "-profile:v", "high10"]),
166 H264Profile::Main8bit => ("yuv420p", vec!["-preset", "p6"]),
167 };
168 base_codec_name = "h264_nvenc".to_string();
169 base_pix_fmt = pf.to_string();
170 base_extra = ext;
171 }
172 "h264_amf" => {
173 let (pf, ext) = match h264 {
174 H264Profile::High10bit => ("p010le", vec!["-quality", "quality"]),
175 H264Profile::Main8bit => ("yuv420p", vec!["-quality", "quality"]),
176 };
177 base_codec_name = "h264_amf".to_string();
178 base_pix_fmt = pf.to_string();
179 base_extra = ext;
180 }
181 "h264_qsv" => {
182 let pf = match h264 {
183 H264Profile::High10bit => "p010le",
184 H264Profile::Main8bit => "yuv420p",
185 };
186 base_codec_name = "h264_qsv".to_string();
187 base_pix_fmt = pf.to_string();
188 }
189 "h264_videotoolbox" => {
190 let pf = match h264 {
191 H264Profile::High10bit => "p010le",
192 H264Profile::Main8bit => "yuv420p",
193 };
194 base_codec_name = "h264_videotoolbox".to_string();
195 base_pix_fmt = pf.to_string();
196 base_extra = vec!["-realtime", "true"];
197 }
198 _ => {
199 let (pf, ext) = match h264 {
200 H264Profile::Main8bit => ("yuv420p", vec!["-preset", "slow"]),
201 H264Profile::High10bit => ("yuv422p10le", vec!["-preset", "slow"]),
202 };
203 base_codec_name = "libx264".to_string();
204 base_pix_fmt = pf.to_string();
205 base_extra = ext;
206 }
207 }
208 }
209 }
210 CodecFamily::AV1 => {
211 match av1_encoder {
212 "libsvtav1" => {
213 base_codec_name = "libsvtav1".to_string();
214 base_pix_fmt = match av1 {
215 Av1Profile::Profile0_420_10bit => "yuv420p10le",
216 Av1Profile::Profile1_444_10bit => "yuv444p10le",
217 }.to_string();
218 base_extra = vec!["-preset", "8"];
219 }
220 "av1_nvenc" => {
221 base_codec_name = "av1_nvenc".to_string();
222 base_pix_fmt = match av1 {
223 Av1Profile::Profile0_420_10bit => "p010le",
224 Av1Profile::Profile1_444_10bit => "yuv444p10le",
225 }.to_string();
226 base_extra = vec!["-preset", "p6"];
227 }
228 "av1_amf" => {
229 base_codec_name = "av1_amf".to_string();
230 base_pix_fmt = match av1 {
231 Av1Profile::Profile0_420_10bit => "p010le",
232 Av1Profile::Profile1_444_10bit => "yuv444p10le",
233 }.to_string();
234 base_extra = vec!["-quality", "quality"];
235 }
236 "av1_qsv" => {
237 base_codec_name = "av1_qsv".to_string();
238 base_pix_fmt = match av1 {
239 Av1Profile::Profile0_420_10bit => "p010le",
240 Av1Profile::Profile1_444_10bit => "yuv444p10le",
241 }.to_string();
242 }
243 _ => {
244 base_codec_name = "libsvtav1".to_string();
245 base_pix_fmt = match av1 {
246 Av1Profile::Profile0_420_10bit => "yuv420p10le",
247 Av1Profile::Profile1_444_10bit => "yuv444p10le",
248 }.to_string();
249 base_extra = vec!["-preset", "8"];
250 }
251 }
252 }
253 CodecFamily::VP9 => {
254 base_codec_name = "libvpx-vp9".to_string();
258 base_pix_fmt = match vp9 {
259 Vp9Profile::Profile2_420_10bit => "yuv420p10le".to_string(),
260 Vp9Profile::Profile3_444_10bit => "yuv444p10le".to_string(),
261 };
262 base_extra = vec![];
263 }
264 }
265
266 let mut extra: Vec<String> = base_extra.iter().map(|&s| s.to_string()).collect();
268
269 if is_wide_gamut && !base_pix_fmt.starts_with("gbrp") && !base_pix_fmt.starts_with("rgb") {
279 extra.push("-vf".into());
280 extra.push(format!("scale=flags=accurate_rnd+full_chroma_int:out_color_matrix=bt2020nc:out_range=full,format={}", base_pix_fmt));
281 }
282
283 match self {
288 CodecFamily::HEVC => {
289 extra.extend(rate_control_args(rate_control, hevc_encoder));
290 }
291 CodecFamily::H264 => {
292 extra.extend(rate_control_args(rate_control, h264_encoder));
293 }
294 CodecFamily::AV1 => {
295 extra.extend(rate_control_args(rate_control, av1_encoder));
296 }
297 CodecFamily::VP9 => {
298 extra.extend(rate_control_args(rate_control, "libvpx-vp9"));
302 }
303 CodecFamily::ProRes | CodecFamily::DNxHR => {}
304 }
305
306 tracing::debug!("ffmpeg args: codec={} pix_fmt={} extra={:?}",
307 base_codec_name, base_pix_fmt, extra);
308
309 (base_codec_name, base_pix_fmt, extra)
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum ProResProfile {
315 Proxy,
316 LT,
317 Standard,
318 HQ,
319 P4444,
320 XQ4444,
321}
322
323impl ProResProfile {
324 pub fn name(&self) -> &'static str {
325 match self {
326 ProResProfile::Proxy => "Proxy",
327 ProResProfile::LT => "LT",
328 ProResProfile::Standard => "Standard",
329 ProResProfile::HQ => "HQ",
330 ProResProfile::P4444 => "4444",
331 ProResProfile::XQ4444 => "4444 XQ",
332 }
333 }
334
335 pub fn all() -> &'static [ProResProfile] {
336 &[
337 ProResProfile::Proxy,
338 ProResProfile::LT,
339 ProResProfile::Standard,
340 ProResProfile::HQ,
341 ProResProfile::P4444,
342 ProResProfile::XQ4444,
343 ]
344 }
345
346 pub fn next(self) -> Self {
347 let all = Self::all();
348 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
349 all[(pos + 1) % all.len()]
350 }
351
352 pub fn prev(self) -> Self {
353 let all = Self::all();
354 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
355 all[(pos + all.len() - 1) % all.len()]
356 }
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360pub enum DnxhrProfile {
361 SQ,
362 HD,
363 HDX,
364 HQX,
365 P444,
366}
367
368impl DnxhrProfile {
369 pub fn name(&self) -> &'static str {
370 match self {
371 DnxhrProfile::SQ => "SQ",
372 DnxhrProfile::HD => "HD",
373 DnxhrProfile::HDX => "HDX",
374 DnxhrProfile::HQX => "HQX",
375 DnxhrProfile::P444 => "444",
376 }
377 }
378
379 pub fn all() -> &'static [DnxhrProfile] {
380 &[DnxhrProfile::SQ, DnxhrProfile::HD, DnxhrProfile::HDX, DnxhrProfile::HQX, DnxhrProfile::P444]
381 }
382
383 pub fn next(self) -> Self {
384 let all = Self::all();
385 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
386 all[(pos + 1) % all.len()]
387 }
388
389 pub fn prev(self) -> Self {
390 let all = Self::all();
391 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
392 all[(pos + all.len() - 1) % all.len()]
393 }
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum HevcProfile {
398 Main10_420,
399 Main10_444,
400}
401
402impl HevcProfile {
403 pub fn name(&self) -> &'static str {
404 match self {
405 HevcProfile::Main10_420 => "Main 10 4:2:0",
406 HevcProfile::Main10_444 => "Main 10 4:4:4",
407 }
408 }
409
410 pub fn is_8bit(&self) -> bool {
411 false
412 }
413
414 pub fn all() -> &'static [HevcProfile] {
415 &[HevcProfile::Main10_420, HevcProfile::Main10_444]
416 }
417
418 pub fn next(self) -> Self {
419 let all = Self::all();
420 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
421 all[(pos + 1) % all.len()]
422 }
423
424 pub fn prev(self) -> Self {
425 let all = Self::all();
426 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
427 all[(pos + all.len() - 1) % all.len()]
428 }
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432pub enum H264Profile {
433 Main8bit,
434 High10bit,
435}
436
437impl H264Profile {
438 pub fn name(&self) -> &'static str {
439 match self {
440 H264Profile::Main8bit => "Main 8-bit",
441 H264Profile::High10bit => "High 10-bit",
442 }
443 }
444
445 pub fn is_8bit(&self) -> bool {
446 matches!(self, H264Profile::Main8bit)
447 }
448
449 pub fn all() -> &'static [H264Profile] {
450 &[H264Profile::Main8bit, H264Profile::High10bit]
451 }
452
453 pub fn next(self) -> Self {
454 let all = Self::all();
455 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
456 all[(pos + 1) % all.len()]
457 }
458
459 pub fn prev(self) -> Self {
460 let all = Self::all();
461 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
462 all[(pos + all.len() - 1) % all.len()]
463 }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum Av1Profile {
468 Profile0_420_10bit,
469 Profile1_444_10bit,
470}
471
472impl Av1Profile {
473 pub fn name(&self) -> &'static str {
474 match self {
475 Av1Profile::Profile0_420_10bit => "Profile 0 4:2:0 10-bit",
476 Av1Profile::Profile1_444_10bit => "Profile 1 4:4:4 10-bit",
477 }
478 }
479
480 pub fn is_8bit(&self) -> bool {
481 false
482 }
483
484 pub fn all() -> &'static [Av1Profile] {
485 &[Av1Profile::Profile0_420_10bit, Av1Profile::Profile1_444_10bit]
486 }
487
488 pub fn next(self) -> Self {
489 let all = Self::all();
490 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
491 all[(pos + 1) % all.len()]
492 }
493
494 pub fn prev(self) -> Self {
495 let all = Self::all();
496 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
497 all[(pos + all.len() - 1) % all.len()]
498 }
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub enum Vp9Profile {
503 Profile2_420_10bit,
504 Profile3_444_10bit,
505}
506
507impl Vp9Profile {
508 pub fn name(&self) -> &'static str {
509 match self {
510 Vp9Profile::Profile2_420_10bit => "Profile 2 4:2:0 10-bit",
511 Vp9Profile::Profile3_444_10bit => "Profile 3 4:4:4 10-bit",
512 }
513 }
514
515 pub fn is_8bit(&self) -> bool {
516 false
517 }
518
519 pub fn all() -> &'static [Vp9Profile] {
520 &[Vp9Profile::Profile2_420_10bit, Vp9Profile::Profile3_444_10bit]
521 }
522
523 pub fn next(self) -> Self {
524 let all = Self::all();
525 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
526 all[(pos + 1) % all.len()]
527 }
528
529 pub fn prev(self) -> Self {
530 let all = Self::all();
531 let pos = all.iter().position(|&x| x == self).unwrap_or(0);
532 all[(pos + all.len() - 1) % all.len()]
533 }
534}
535
536#[derive(Debug, Clone)]
547pub enum RateControl {
548 Lossless,
549 High,
550 Standard,
551 Master400M,
552 Standard150M,
553 Custom(String),
554}
555
556impl RateControl {
557 pub fn name(&self) -> String {
558 match self {
559 RateControl::Lossless => "Lossless".to_string(),
560 RateControl::High => "High Quality".to_string(),
561 RateControl::Standard => "Standard".to_string(),
562 RateControl::Master400M => "Master 400M".to_string(),
563 RateControl::Standard150M => "Standard 150M".to_string(),
564 RateControl::Custom(v) => {
565 if v.is_empty() {
566 "Custom: []".to_string()
567 } else {
568 format!("Custom: [{}]", v)
569 }
570 }
571 }
572 }
573
574 pub fn next(&self) -> Self {
575 match self {
576 RateControl::Lossless => RateControl::High,
577 RateControl::High => RateControl::Standard,
578 RateControl::Standard => RateControl::Master400M,
579 RateControl::Master400M => RateControl::Standard150M,
580 RateControl::Standard150M => RateControl::Custom(String::new()),
581 RateControl::Custom(_) => RateControl::Lossless,
582 }
583 }
584
585 pub fn prev(&self) -> Self {
586 match self {
587 RateControl::Lossless => RateControl::Custom(String::new()),
588 RateControl::High => RateControl::Lossless,
589 RateControl::Standard => RateControl::High,
590 RateControl::Master400M => RateControl::Standard,
591 RateControl::Standard150M => RateControl::Master400M,
592 RateControl::Custom(_) => RateControl::Standard150M,
593 }
594 }
595}
596
597pub fn rate_control_args(rc: &RateControl, encoder_name: &str) -> Vec<String> {
610 let is_hw = !encoder_name.starts_with("lib");
611 let is_videotoolbox = encoder_name.ends_with("_videotoolbox");
612 let is_nvenc = encoder_name.ends_with("_nvenc");
613 let needs_bv0_for_crf = matches!(encoder_name, "libvpx-vp9" | "libaom-av1");
614
615 let cq = |value: &str| -> Vec<String> {
617 if is_videotoolbox {
618 vec!["-quality".into(), value.into()]
619 } else if is_hw {
620 vec!["-cq".into(), value.into()]
621 } else if needs_bv0_for_crf {
622 vec!["-crf".into(), value.into(), "-b:v".into(), "0".into()]
623 } else {
624 vec!["-crf".into(), value.into()]
625 }
626 };
627
628 let bitrate = |value: &str| -> Vec<String> {
630 let mut v = vec![
631 "-b:v".into(), value.into(),
632 "-maxrate".into(), value.into(),
633 ];
634 if is_nvenc {
635 v.push("-rc:v".into());
639 v.push("vbr".into());
640 }
641 v
642 };
643
644 match rc {
645 RateControl::Lossless => {
646 if is_videotoolbox {
647 vec!["-quality".into(), "lossless".into()]
648 } else {
649 cq("16")
650 }
651 }
652 RateControl::High => {
653 if is_videotoolbox {
654 vec!["-quality".into(), "max".into()]
655 } else {
656 cq("20")
657 }
658 }
659 RateControl::Standard => {
660 if is_videotoolbox {
661 vec!["-quality".into(), "high".into()]
662 } else {
663 cq("24")
664 }
665 }
666 RateControl::Master400M => bitrate("400M"),
667 RateControl::Standard150M => bitrate("150M"),
668 RateControl::Custom(val) => {
669 if val.is_empty() {
670 return vec![];
671 }
672 let upper = val.to_uppercase();
673 if upper.ends_with('M') || upper.ends_with('K') {
674 bitrate(val)
675 } else if val.parse::<f64>().is_ok() {
676 cq(val)
677 } else {
678 vec![val.clone()]
680 }
681 }
682 }
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn rate_control_lossless_software_uses_crf() {
691 let args = rate_control_args(&RateControl::Lossless, "libx265");
692 assert_eq!(args, vec!["-crf", "16"]);
693 }
694
695 #[test]
696 fn rate_control_lossless_nvenc_uses_cq() {
697 let args = rate_control_args(&RateControl::Lossless, "hevc_nvenc");
698 assert_eq!(args, vec!["-cq", "16"]);
699 }
700
701 #[test]
702 fn rate_control_lossless_videotoolbox_uses_quality_lossless() {
703 let args = rate_control_args(&RateControl::Lossless, "hevc_videotoolbox");
704 assert_eq!(args, vec!["-quality", "lossless"]);
705 }
706
707 #[test]
708 fn rate_control_nvenc_bitrate_mode_pins_rc_to_vbr() {
709 let args = rate_control_args(&RateControl::Master400M, "hevc_nvenc");
712 assert!(args.contains(&"-b:v".to_string()));
713 assert!(args.contains(&"-maxrate".to_string()));
714 assert!(args.contains(&"-rc:v".to_string()));
715 assert!(args.contains(&"vbr".to_string()));
716 }
717
718 #[test]
719 fn rate_control_vp9_crf_adds_bv0() {
720 let args = rate_control_args(&RateControl::Standard, "libvpx-vp9");
723 assert!(args.contains(&"-crf".to_string()));
724 assert!(args.contains(&"24".to_string()));
725 assert!(args.contains(&"-b:v".to_string()));
726 assert!(args.contains(&"0".to_string()));
727 }
728
729 #[test]
730 fn rate_control_libaom_av1_crf_adds_bv0() {
731 let args = rate_control_args(&RateControl::High, "libaom-av1");
732 assert!(args.contains(&"-crf".to_string()));
733 assert!(args.contains(&"20".to_string()));
734 assert!(args.contains(&"-b:v".to_string()));
735 assert!(args.contains(&"0".to_string()));
736 }
737
738 #[test]
739 fn rate_control_custom_numeric_routes_to_cq() {
740 let args = rate_control_args(&RateControl::Custom("18".into()), "libx265");
741 assert_eq!(args, vec!["-crf", "18"]);
742 }
743
744 #[test]
745 fn rate_control_custom_bitrate_routes_to_bv() {
746 let args = rate_control_args(&RateControl::Custom("50M".into()), "libx265");
747 assert!(args.contains(&"-b:v".to_string()));
748 assert!(args.contains(&"50M".to_string()));
749 }
750
751 #[test]
752 fn rate_control_custom_empty_returns_empty() {
753 let args = rate_control_args(&RateControl::Custom(String::new()), "libx265");
754 assert!(args.is_empty());
755 }
756}