1#[cfg(test)]
2mod tests;
3
4use std::{
5 borrow::Cow,
6 cmp,
7 fmt::Display,
8 iter::Iterator,
9 path::PathBuf,
10 process::Command,
11 sync::OnceLock,
12};
13
14use arrayvec::ArrayVec;
15use cfg_if::cfg_if;
16use itertools::chain;
17use once_cell::sync::Lazy;
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20static SVT_AV1_QUARTER_STEP_SUPPORT: OnceLock<bool> = OnceLock::new();
21
22pub static USE_OLD_SVT_AV1: Lazy<bool> = Lazy::new(|| {
23 let version = Command::new("SvtAv1EncApp")
24 .arg("--version")
25 .output()
26 .expect("failed to run svt-av1");
27
28 if let Some((major, minor, _)) = parse_svt_av1_version(&version.stdout) {
29 match major {
30 0 => minor < 9,
31 1.. => false,
32 }
33 } else {
34 let output = Command::new("SvtAv1EncApp")
36 .arg("--cdef-level")
37 .arg("0")
38 .output()
39 .expect("failed to run svt-av1");
40
41 let out = if output.stdout.is_empty() {
42 output.stderr
43 } else {
44 output.stdout
45 };
46 parse_svt_av1_unprocessed_tokens(&out).is_none_or(|tokens| tokens.is_empty())
49 }
50});
51
52use crate::{
53 ffmpeg::{compose_ffmpeg_pipe, FFPixelFormat},
54 inplace_vec,
55 into_array,
56 into_vec,
57 list_index,
58};
59
60const NULL: &str = if cfg!(windows) { "nul" } else { "/dev/null" };
61
62const MAXIMUM_SPEED_AOM: u8 = 6;
64const MAXIMUM_SPEED_RAV1E: u8 = 10;
65const MAXIMUM_SPEED_VPX: u8 = 9;
66const MAXIMUM_SPEED_OLD_SVT_AV1: u8 = 8;
67const MAXIMUM_SPEED_SVT_AV1: u8 = 12;
68const MAXIMUM_SPEED_X264: &str = "medium";
69const MAXIMUM_SPEED_X265: &str = "fast";
70
71#[expect(non_camel_case_types)]
72#[derive(
73 Clone,
74 Copy,
75 PartialEq,
76 Eq,
77 Serialize,
78 Deserialize,
79 Debug,
80 strum::EnumString,
81 strum::IntoStaticStr,
82)]
83pub enum Encoder {
84 aom,
85 rav1e,
86 vpx,
87 #[strum(serialize = "svt-av1")]
88 svt_av1,
89 x264,
90 x265,
91}
92
93#[tracing::instrument(level = "debug")]
94pub(crate) fn parse_svt_av1_version(version: &[u8]) -> Option<(u32, u32, u32)> {
95 let v_idx = memchr::memchr(b'v', version)?;
96 let s = version.get(v_idx + 1..)?;
97 let s = simdutf8::basic::from_utf8(s).ok()?;
98 let version = s
99 .split_ascii_whitespace()
100 .next()?
101 .split('.')
102 .filter_map(|s| s.split('-').next())
103 .filter_map(|s| s.parse::<u32>().ok())
104 .collect::<ArrayVec<u32, 3>>();
105
106 if let [major, minor, patch] = version[..] {
107 Some((major, minor, patch))
108 } else {
109 None
110 }
111}
112
113#[tracing::instrument(level = "debug")]
114pub(crate) fn parse_svt_av1_unprocessed_tokens(output: &[u8]) -> Option<Vec<String>> {
115 let start_idx = memchr::memmem::find(output, b"Unprocessed tokens:")?;
116 let sub_output = output.get(start_idx..)?;
117 let end_idx = sub_output.iter().position(|&b| b == b'\n')?;
118 let unprocessed_tokens_line = simdutf8::basic::from_utf8(sub_output.get(0..end_idx)?).ok()?;
119 let unprocessed_tokens = unprocessed_tokens_line
120 .split_ascii_whitespace()
121 .skip(2)
122 .map(String::from)
123 .collect::<Vec<_>>();
124
125 Some(unprocessed_tokens)
126}
127
128#[tracing::instrument(level = "debug")]
129pub(crate) fn svt_av1_supports_quarter_steps(temp: &str) -> bool {
130 *SVT_AV1_QUARTER_STEP_SUPPORT.get_or_init(|| {
131 use std::{fs, io::Write, path::Path, process::Stdio};
132
133 let test_file = Path::new(temp).join("test_q.y4m");
134 let result = (|| -> Option<bool> {
135 let mut f = fs::File::create(&test_file).ok()?;
136 writeln!(f, "YUV4MPEG2 W320 H240 F30:1 Ip A0:0 C420jpeg").ok()?;
137 writeln!(f, "FRAME").ok()?;
138 f.write_all(&vec![0u8; 320 * 240 + 2 * 160 * 120]).ok()?;
139 drop(f);
140
141 Command::new("SvtAv1EncApp")
142 .args(["-i", test_file.to_str()?, "--crf", "50.75", "-b", NULL])
143 .stdout(Stdio::null())
144 .stderr(Stdio::null())
145 .status()
146 .ok()
147 .map(|s| s.success())
148 })();
149
150 let _ = fs::remove_file(test_file);
151 result.unwrap_or(false)
152 })
153}
154
155pub(crate) fn format_q(q: f32) -> String {
156 if q.fract().abs() < 1e-6 {
157 format!("{:.0}", q)
158 } else {
159 format!("{:.2}", q)
160 }
161}
162
163impl Display for Encoder {
164 #[inline]
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 f.write_str(<&'static str>::from(self))
167 }
168}
169
170impl Encoder {
171 #[inline]
173 pub fn compose_1_1_pass(self, params: Vec<String>, output: String) -> Vec<String> {
174 match self {
175 Self::aom => chain!(into_array!["aomenc", "--passes=1"], params, into_array![
176 "-o", output, "-"
177 ],)
178 .collect(),
179 Self::rav1e => chain!(into_array!["rav1e", "-", "-y"], params, into_array![
180 "--output", output
181 ])
182 .collect(),
183 Self::vpx => chain!(into_array!["vpxenc", "--passes=1"], params, into_array![
184 "-o", output, "-"
185 ])
186 .collect(),
187 Self::svt_av1 => chain!(
188 into_array!["SvtAv1EncApp", "-i", "stdin", "--progress", "2"],
189 params,
190 into_array!["-b", output],
191 )
192 .collect(),
193 Self::x264 => chain!(
194 into_array!["x264", "--stitchable", "--log-level", "error", "--demuxer", "y4m",],
195 params,
196 into_array!["-", "-o", output]
197 )
198 .collect(),
199 Self::x265 => chain!(into_array!["x265", "--y4m"], params, into_array![
200 "--input", "-", "-o", output
201 ])
202 .collect(),
203 }
204 }
205
206 #[inline]
208 pub fn compose_1_2_pass(self, params: Vec<String>, fpf: &str) -> Vec<String> {
209 match self {
210 Self::aom => chain!(
211 into_array!["aomenc", "--passes=2", "--pass=1"],
212 params,
213 into_array![format!("--fpf={fpf}.log"), "-o", NULL, "-"],
214 )
215 .collect(),
216 Self::rav1e => chain!(
217 into_array!["rav1e", "-", "-y", "--quiet",],
218 params,
219 into_array!["--first-pass", format!("{fpf}.stat"), "--output", NULL]
220 )
221 .collect(),
222 Self::vpx => chain!(
223 into_array!["vpxenc", "--passes=2", "--pass=1"],
224 params,
225 into_array![format!("--fpf={fpf}.log"), "-o", NULL, "-"],
226 )
227 .collect(),
228 Self::svt_av1 => chain!(
229 into_array![
230 "SvtAv1EncApp",
231 "-i",
232 "stdin",
233 "--progress",
234 "2",
235 "--irefresh-type",
236 "2",
237 ],
238 params,
239 into_array!["--pass", "1", "--stats", format!("{fpf}.stat"), "-b", NULL,],
240 )
241 .collect(),
242 Self::x264 => chain!(
243 into_array![
244 "x264",
245 "--stitchable",
246 "--log-level",
247 "error",
248 "--pass",
249 "1",
250 "--demuxer",
251 "y4m",
252 ],
253 params,
254 into_array!["--stats", format!("{fpf}.log"), "-", "-o", NULL]
255 )
256 .collect(),
257 Self::x265 => chain!(
258 into_array![
259 "x265",
260 "--repeat-headers",
261 "--log-level",
262 "error",
263 "--pass",
264 "1",
265 "--y4m",
266 ],
267 params,
268 into_array![
269 "--stats",
270 format!("{fpf}.log"),
271 "--analysis-reuse-file",
272 format!("{fpf}_analysis.dat"),
273 "--input",
274 "-",
275 "-o",
276 NULL
277 ]
278 )
279 .collect(),
280 }
281 }
282
283 #[inline]
285 pub fn compose_2_2_pass(self, params: Vec<String>, fpf: &str, output: String) -> Vec<String> {
286 match self {
287 Self::aom => chain!(
288 into_array!["aomenc", "--passes=2", "--pass=2"],
289 params,
290 into_array![format!("--fpf={fpf}.log"), "-o", output, "-"],
291 )
292 .collect(),
293 Self::rav1e => chain!(
294 into_array!["rav1e", "-", "-y", "--quiet",],
295 params,
296 into_array!["--second-pass", format!("{fpf}.stat"), "--output", output]
297 )
298 .collect(),
299 Self::vpx => chain!(
300 into_array!["vpxenc", "--passes=2", "--pass=2"],
301 params,
302 into_array![format!("--fpf={fpf}.log"), "-o", output, "-"],
303 )
304 .collect(),
305 Self::svt_av1 => chain!(
306 into_array![
307 "SvtAv1EncApp",
308 "-i",
309 "stdin",
310 "--progress",
311 "2",
312 "--irefresh-type",
313 "2",
314 ],
315 params,
316 into_array!["--pass", "2", "--stats", format!("{fpf}.stat"), "-b", output,],
317 )
318 .collect(),
319 Self::x264 => chain!(
320 into_array![
321 "x264",
322 "--stitchable",
323 "--log-level",
324 "error",
325 "--pass",
326 "2",
327 "--demuxer",
328 "y4m",
329 ],
330 params,
331 into_array!["--stats", format!("{fpf}.log"), "-", "-o", output]
332 )
333 .collect(),
334 Self::x265 => chain!(
335 into_array![
336 "x265",
337 "--repeat-headers",
338 "--log-level",
339 "error",
340 "--pass",
341 "2",
342 "--y4m",
343 ],
344 params,
345 into_array![
346 "--stats",
347 format!("{fpf}.log"),
348 "--analysis-reuse-file",
349 format!("{fpf}_analysis.dat"),
350 "--input",
351 "-",
352 "-o",
353 output
354 ]
355 )
356 .collect(),
357 }
358 }
359
360 #[inline]
362 pub fn get_default_arguments(self, (cols, rows): (u32, u32)) -> Vec<String> {
363 match self {
364 Encoder::aom => {
369 let defaults: Vec<String> = into_vec![
370 "--threads=8",
371 "--cpu-used=6",
372 "--end-usage=q",
373 "--cq-level=30",
374 "--disable-kf",
375 "--kf-max-dist=9999"
376 ];
377
378 if cols > 1 || rows > 1 {
379 let columns = cols.ilog2();
380 let rows = rows.ilog2();
381
382 let aom_tiles: Vec<String> = into_vec![
383 format!("--tile-columns={columns}"),
384 format!("--tile-rows={rows}")
385 ];
386 chain!(defaults, aom_tiles).collect()
387 } else {
388 defaults
389 }
390 },
391 Encoder::rav1e => {
392 let defaults: Vec<String> = into_vec![
393 "--speed",
394 "6",
395 "--quantizer",
396 "100",
397 "--keyint",
398 "0",
399 "--no-scene-detection",
400 ];
401
402 if cols > 1 || rows > 1 {
403 let tiles: Vec<String> =
404 into_vec!["--tiles", format!("{tiles}", tiles = cols * rows)];
405 chain!(defaults, tiles).collect()
406 } else {
407 defaults
408 }
409 },
410 Encoder::vpx => {
413 let defaults = into_vec![
414 "--codec=vp9",
415 "-b",
416 "10",
417 "--profile=2",
418 "--threads=4",
419 "--cpu-used=2",
420 "--end-usage=q",
421 "--cq-level=30",
422 "--row-mt=1",
423 "--auto-alt-ref=6",
424 "--disable-kf",
425 "--kf-max-dist=9999"
426 ];
427
428 if cols > 1 || rows > 1 {
429 let columns = cols.ilog2();
430 let rows = rows.ilog2();
431
432 let aom_tiles: Vec<String> = into_vec![
433 format!("--tile-columns={columns}"),
434 format!("--tile-rows={rows}")
435 ];
436 chain!(defaults, aom_tiles).collect()
437 } else {
438 defaults
439 }
440 },
441 Encoder::svt_av1 => {
442 let defaults = into_vec![
443 "--preset", "4", "--keyint", "0", "--scd", "0", "--rc", "0", "--crf", "25"
444 ];
445 if cols > 1 || rows > 1 {
446 let columns = cols.ilog2();
447 let rows = rows.ilog2();
448
449 let tiles: Vec<String> = into_vec![
450 "--tile-columns",
451 columns.to_string(),
452 "--tile-rows",
453 rows.to_string()
454 ];
455 chain!(defaults, tiles).collect()
456 } else {
457 defaults
458 }
459 },
460 Encoder::x264 => into_vec![
461 "--preset",
462 "slow",
463 "--crf",
464 "25",
465 "--keyint",
466 "infinite",
467 "--scenecut",
468 "0",
469 ],
470 Encoder::x265 => into_vec![
471 "--preset",
472 "slow",
473 "--crf",
474 "25",
475 "-D",
476 "10",
477 "--level-idc",
478 "5.0",
479 "--keyint",
480 "-1",
481 "--scenecut",
482 "0",
483 ],
484 }
485 }
486
487 #[inline]
489 pub const fn get_default_pass(self) -> u8 {
490 match self {
491 Self::aom | Self::vpx => 2,
492 _ => 1,
493 }
494 }
495
496 #[inline]
498 pub const fn get_default_cq_range(self) -> (usize, usize) {
499 match self {
500 Self::aom | Self::vpx => (15, 55),
501 Self::rav1e => (50, 140),
502 Self::svt_av1 => (15, 50),
503 Self::x264 | Self::x265 => (15, 35),
504 }
505 }
506
507 #[inline]
509 pub fn get_cq_relative_percentage(self, quantizer: usize) -> f64 {
510 let percentage = match self {
511 Self::aom | Self::vpx | Self::svt_av1 => quantizer as f64 / 64.0, Self::rav1e => quantizer as f64 / 256.0, Self::x264 | Self::x265 => quantizer as f64 / 52.0, };
515
516 percentage.clamp(0.0, 1.0)
518 }
519
520 #[inline]
522 pub const fn help_command(self) -> [&'static str; 2] {
523 match self {
524 Self::aom => ["aomenc", "--help"],
525 Self::rav1e => ["rav1e", "--help"],
526 Self::vpx => ["vpxenc", "--help"],
527 Self::svt_av1 => ["SvtAv1EncApp", "--help"],
528 Self::x264 => ["x264", "--fullhelp"],
529 Self::x265 => ["x265", "--fullhelp"],
530 }
531 }
532
533 #[inline]
536 pub fn version_text(self) -> Option<String> {
537 match self {
538 Self::aom => {
539 let result = Command::new("aomenc").arg("--help").output().ok()?;
540 let stdout = String::from_utf8_lossy(&result.stdout);
541 let version_line = stdout.lines().find(|line| line.starts_with(" av1"))?;
542 Some(
543 version_line
544 .split_once('-')
545 .expect("unexpected aom version string format")
546 .1
547 .replace("(default)", "")
548 .trim()
549 .to_string(),
550 )
551 },
552 Self::rav1e => {
553 let result = Command::new("rav1e").arg("--version").output().ok()?;
554 let stdout = String::from_utf8_lossy(&result.stdout);
555 let version_line = stdout.lines().find(|line| line.starts_with("rav1e"))?;
556 Some(version_line.to_string())
557 },
558 Self::vpx => {
559 let result = Command::new("vpxenc").arg("--help").output().ok()?;
560 let stdout = String::from_utf8_lossy(&result.stdout);
561 let version_line = stdout.lines().find(|line| line.starts_with(" vp9"))?;
562 Some(
563 version_line
564 .split_once('-')
565 .expect("unexpected vpx version string format")
566 .1
567 .replace("(default)", "")
568 .trim()
569 .to_string(),
570 )
571 },
572 Self::svt_av1 => {
573 let result = Command::new("SvtAv1EncApp").arg("--version").output().ok()?;
574 let stdout = String::from_utf8_lossy(&result.stdout);
575 let version_line = stdout.lines().find(|line| line.starts_with("SVT-AV1"))?;
576 Some(version_line.to_string())
577 },
578 Self::x264 => {
579 let result = Command::new("x264").arg("--version").output().ok()?;
580 let stdout = String::from_utf8_lossy(&result.stdout);
581 let version_line = stdout.lines().find(|line| line.starts_with("x264"))?;
582 Some(version_line.to_string())
583 },
584 Self::x265 => {
585 let result = Command::new("x265").arg("--version").output().ok()?;
586 let stderr = String::from_utf8_lossy(&result.stderr);
587 let version_line = stderr.lines().find(|line| line.starts_with("x265"))?;
588 Some(
589 version_line
590 .split_once(':')
591 .expect("unexpected x265 version string format")
592 .1
593 .trim()
594 .to_string(),
595 )
596 },
597 }
598 }
599
600 #[inline]
602 pub const fn bin(self) -> &'static str {
603 match self {
604 Self::aom => "aomenc",
605 Self::rav1e => "rav1e",
606 Self::vpx => "vpxenc",
607 Self::svt_av1 => "SvtAv1EncApp",
608 Self::x264 => "x264",
609 Self::x265 => "x265",
610 }
611 }
612
613 #[inline]
615 pub const fn format(self) -> &'static str {
616 match self {
617 Self::aom | Self::rav1e | Self::svt_av1 => "av1",
618 Self::vpx => "vpx",
619 Self::x264 => "h264",
620 Self::x265 => "h265",
621 }
622 }
623
624 #[inline]
626 pub const fn output_extension(&self) -> &'static str {
627 match &self {
628 Self::aom | Self::rav1e | Self::vpx | Self::svt_av1 => "ivf",
629 Self::x264 => "264",
630 Self::x265 => "hevc",
631 }
632 }
633
634 fn q_match_fn(self) -> fn(&str) -> bool {
637 match self {
638 Self::aom | Self::vpx => |p| p.starts_with("--cq-level="),
639 Self::rav1e => |p| p == "--quantizer",
640 Self::svt_av1 => |p| matches!(p, "--qp" | "-q" | "--crf"),
641 Self::x264 | Self::x265 => |p| p == "--crf",
642 }
643 }
644
645 fn replace_q(self, index: usize, q: f32) -> (usize, String) {
646 match self {
647 Self::aom | Self::vpx => (index, format!("--cq-level={}", q.round() as usize)),
648 Self::rav1e => (index + 1, (q.round() as usize).to_string()),
649 Self::svt_av1 | Self::x265 | Self::x264 => {
650 let q_str = format_q(q);
651 (index + 1, q_str)
652 },
653 }
654 }
655
656 fn insert_q(self, q: f32) -> ArrayVec<String, 2> {
657 let mut output = ArrayVec::new();
658 match self {
659 Self::aom | Self::vpx => {
660 output.push(format!("--cq-level={}", q.round() as usize));
661 },
662 Self::rav1e => {
663 output.push("--quantizer".into());
664 output.push((q.round() as usize).to_string());
665 },
666 Self::svt_av1 | Self::x264 | Self::x265 => {
667 output.push("--crf".into());
668 let q_str = format_q(q);
669 output.push(q_str);
670 },
671 }
672 output
673 }
674
675 #[inline]
677 pub fn man_command(self, mut params: Vec<String>, q: f32) -> Vec<String> {
678 let index = list_index(¶ms, self.q_match_fn());
679 if let Some(index) = index {
680 let (replace_index, replace_q) = self.replace_q(index, q);
681 params[replace_index] = replace_q;
682 } else {
683 let args = self.insert_q(q);
684 params.extend_from_slice(&args);
685 }
686
687 params
688 }
689
690 pub(crate) fn parse_encoded_frames(self, line: &str) -> Option<u64> {
692 use crate::parse::*;
693
694 match self {
695 Self::aom | Self::vpx => {
696 cfg_if! {
697 if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] {
698 if is_x86_feature_detected!("sse4.1") && is_x86_feature_detected!("ssse3") {
699 return unsafe { parse_aom_vpx_frames_sse41(line.as_bytes()) };
701 }
702 }
703 }
704
705 parse_aom_vpx_frames(line)
706 },
707 Self::rav1e => parse_rav1e_frames(line),
708 Self::svt_av1 => parse_svt_av1_frames(line),
709 Self::x264 | Self::x265 => parse_x26x_frames(line),
710 }
711 }
712
713 #[inline]
715 pub fn construct_target_quality_command(
716 self,
717 threads: usize,
718 q: f32,
719 ) -> Vec<Cow<'static, str>> {
720 match &self {
721 Self::aom => inplace_vec![
722 "aomenc",
723 "--passes=1",
724 format!("--threads={threads}"),
725 "--tile-columns=2",
726 "--tile-rows=1",
727 "--end-usage=q",
728 "-b",
729 "8",
730 format!("--cpu-used={}", MAXIMUM_SPEED_AOM),
731 format!("--cq-level={q}"),
732 "--enable-filter-intra=0",
733 "--enable-smooth-intra=0",
734 "--enable-paeth-intra=0",
735 "--enable-cfl-intra=0",
736 "--enable-angle-delta=0",
737 "--reduced-tx-type-set=1",
738 "--enable-intra-edge-filter=0",
739 "--enable-order-hint=0",
740 "--enable-flip-idtx=0",
741 "--enable-global-motion=0",
742 "--enable-cdef=0",
743 "--max-reference-frames=3",
744 "--cdf-update-mode=2",
745 "--enable-tpl-model=0",
746 "--sb-size=64",
747 "--min-partition-size=32",
748 "--disable-kf",
749 "--kf-max-dist=9999"
750 ],
751 Self::rav1e => inplace_vec![
752 "rav1e",
753 "-y",
754 "-s",
755 MAXIMUM_SPEED_RAV1E.to_string(),
756 "--threads",
757 threads.to_string(),
758 "--tiles",
759 "16",
760 "--quantizer",
761 (q.round() as usize).to_string(),
762 "--low-latency",
763 "--rdo-lookahead-frames",
764 "5",
765 "--no-scene-detection",
766 ],
767 Self::vpx => inplace_vec![
768 "vpxenc",
769 "-b",
770 "10",
771 "--profile=2",
772 "--passes=1",
773 "--pass=1",
774 "--codec=vp9",
775 format!("--threads={threads}"),
776 format!("--cpu-used={}", MAXIMUM_SPEED_VPX),
777 "--end-usage=q",
778 format!("--cq-level={q}"),
779 "--row-mt=1",
780 "--disable-kf",
781 "--kf-max-dist=9999"
782 ],
783 Self::svt_av1 => {
784 if *USE_OLD_SVT_AV1 {
785 inplace_vec![
786 "SvtAv1EncApp",
787 "-i",
788 "stdin",
789 "--lp",
790 threads.to_string(),
791 "--preset",
792 MAXIMUM_SPEED_OLD_SVT_AV1.to_string(),
793 "--keyint",
794 "240",
795 "--crf",
796 format_q(q),
797 "--tile-rows",
798 "1",
799 "--tile-columns",
800 "2",
801 "--pred-struct",
802 "0",
803 "--sg-filter-mode",
804 "0",
805 "--enable-restoration-filtering",
806 "0",
807 "--cdef-level",
808 "0",
809 "--disable-dlf",
810 "0",
811 "--mrp-level",
812 "0",
813 "--enable-mfmv",
814 "0",
815 "--enable-local-warp",
816 "0",
817 "--enable-global-motion",
818 "0",
819 "--enable-interintra-comp",
820 "0",
821 "--obmc-level",
822 "0",
823 "--rdoq-level",
824 "0",
825 "--filter-intra-level",
826 "0",
827 "--enable-intra-edge-filter",
828 "0",
829 "--enable-pic-based-rate-est",
830 "0",
831 "--pred-me",
832 "0",
833 "--bipred-3x3",
834 "0",
835 "--compound",
836 "0",
837 "--ext-block",
838 "0",
839 "--hbd-md",
840 "0",
841 "--palette-level",
842 "0",
843 "--umv",
844 "0",
845 "--tf-level",
846 "3",
847 ]
848 } else {
849 inplace_vec![
850 "SvtAv1EncApp",
851 "-i",
852 "stdin",
853 "--lp",
854 threads.to_string(),
855 "--preset",
856 MAXIMUM_SPEED_SVT_AV1.to_string(),
857 "--keyint",
858 "240",
859 "--crf",
860 format_q(q),
861 "--tile-rows",
862 "1",
863 "--tile-columns",
864 "2",
865 ]
866 }
867 },
868 Self::x264 => inplace_vec![
869 "x264",
870 "--log-level",
871 "error",
872 "--demuxer",
873 "y4m",
874 "-",
875 "--no-progress",
876 "--threads",
877 threads.to_string(),
878 "--preset",
879 MAXIMUM_SPEED_X264,
880 "--crf",
881 format!("{:.2}", q),
882 ],
883 Self::x265 => inplace_vec![
884 "x265",
885 "--log-level",
886 "0",
887 "--no-progress",
888 "--y4m",
889 "--frame-threads",
890 cmp::min(threads, 16).to_string(),
891 "--preset",
892 MAXIMUM_SPEED_X265,
893 "--crf",
894 format!("{:.2}", q),
895 "--input",
896 "-",
897 ],
898 }
899 }
900
901 #[inline]
904 pub fn construct_target_quality_command_probe_slow(self, q: f32) -> Vec<Cow<'static, str>> {
905 match &self {
906 Self::aom => {
907 inplace_vec!["aomenc", "--passes=1", format!("--cq-level={}", q.round() as usize)]
908 },
909 Self::rav1e => {
910 inplace_vec!["rav1e", "-y", "--quantizer", (q.round() as usize).to_string()]
911 },
912 Self::vpx => inplace_vec![
913 "vpxenc",
914 "--passes=1",
915 "--pass=1",
916 "--codec=vp9",
917 "--end-usage=q",
918 format!("--cq-level={}", q.round() as usize),
919 ],
920 Self::svt_av1 => {
921 inplace_vec!["SvtAv1EncApp", "-i", "stdin", "--crf", format_q(q),]
922 },
923 Self::x264 => inplace_vec![
924 "x264",
925 "--log-level",
926 "error",
927 "--demuxer",
928 "y4m",
929 "-",
930 "--no-progress",
931 "--crf",
932 format!("{:.2}", q),
933 ],
934 Self::x265 => inplace_vec![
935 "x265",
936 "--log-level",
937 "0",
938 "--no-progress",
939 "--y4m",
940 "--crf",
941 format!("{:.2}", q),
942 "--input",
943 "-",
944 ],
945 }
946 }
947
948 #[inline]
951 pub fn remove_patterns(args: &mut Vec<String>, patterns: &[&str]) {
952 for pattern in patterns {
953 if let Some(index) = args.iter().position(|value| value.contains(pattern)) {
954 args.remove(index);
955 if !pattern.contains('=') {
957 args.remove(index);
958 }
959 }
960 }
961 }
962
963 #[expect(clippy::too_many_arguments)]
964 #[inline]
965 pub fn probe_cmd(
967 self,
968 temp: String,
969 chunk_index: usize,
970 q: f32,
971 pix_fmt: FFPixelFormat,
972 probing_rate: usize,
973 vmaf_threads: usize,
974 custom_video_params: Option<Vec<String>>,
975 ) -> (Option<Vec<String>>, Vec<Cow<'static, str>>) {
976 let filters = if probing_rate > 1 {
977 vec![
978 "-vf".to_string(),
979 format!("select=not(mod(n\\,{probing_rate}))"),
980 "-vsync".to_string(),
981 "0".to_string(),
982 ]
983 } else {
984 Vec::new()
985 };
986
987 let pipe = Some(compose_ffmpeg_pipe(filters, pix_fmt));
988
989 let extension = match self {
990 Encoder::x264 => "264",
991 Encoder::x265 => "hevc",
992 _ => "ivf",
993 };
994 let q_str = format_q(q);
995 let probe_name = format!("v_{index:05}_{q_str}.{extension}", index = chunk_index);
996
997 let probe = PathBuf::from(temp).join("split").join(&probe_name);
998 let probe_path = probe.to_string_lossy().to_string();
999
1000 let params: Vec<Cow<str>> = custom_video_params.map_or_else(
1001 || self.construct_target_quality_command(vmaf_threads, q),
1002 |mut video_params| {
1003 let quantizer_patterns =
1004 ["--cq-level=", "--passes=", "--pass=", "--crf", "--quantizer"];
1005 Self::remove_patterns(&mut video_params, &quantizer_patterns);
1006
1007 let mut ps = self.construct_target_quality_command_probe_slow(q);
1008
1009 ps.reserve(video_params.len());
1010 for arg in video_params {
1011 ps.push(Cow::Owned(arg));
1012 }
1013
1014 ps
1015 },
1016 );
1017
1018 let output: Vec<Cow<str>> = match self {
1019 Self::svt_av1 => chain!(params, into_array!["-b", probe_path]).collect(),
1020 Self::aom | Self::rav1e | Self::vpx | Self::x264 => {
1021 chain!(params, into_array!["-o", probe_path, "-"]).collect()
1022 },
1023 Self::x265 => chain!(params, into_array!["-o", probe_path]).collect(),
1024 };
1025
1026 (pipe, output)
1027 }
1028
1029 #[inline]
1030 pub fn get_format_bit_depth(
1031 self,
1032 format: FFPixelFormat,
1033 ) -> Result<usize, UnsupportedPixelFormatError> {
1034 macro_rules! impl_this_function {
1035 ($($encoder:ident),*) => {
1036 match self {
1037 $(
1038 Encoder::$encoder => pastey::paste! { [<get_ $encoder _format_bit_depth>](format) },
1039 )*
1040 }
1041 };
1042 }
1043 impl_this_function!(x264, x265, vpx, aom, rav1e, svt_av1)
1044 }
1045}
1046
1047#[derive(Error, Debug)]
1048pub enum UnsupportedPixelFormatError {
1049 #[error("{0} does not support {1:?}")]
1050 UnsupportedFormat(Encoder, FFPixelFormat),
1051}
1052
1053macro_rules! create_get_format_bit_depth_function {
1054 ($encoder:ident, 8: $_8bit_fmts:expr, 10: $_10bit_fmts:expr, 12: $_12bit_fmts:expr) => {
1055 pastey::paste! {
1056 pub fn [<get_ $encoder _format_bit_depth>](format: FFPixelFormat) -> Result<usize, UnsupportedPixelFormatError> {
1057 use FFPixelFormat::*;
1058 if $_8bit_fmts.contains(&format) {
1059 Ok(8)
1060 } else if $_10bit_fmts.contains(&format) {
1061 Ok(10)
1062 } else if $_12bit_fmts.contains(&format) {
1063 Ok(12)
1064 } else {
1065 Err(UnsupportedPixelFormatError::UnsupportedFormat(Encoder::$encoder, format))
1066 }
1067 }
1068 }
1069 };
1070}
1071
1072create_get_format_bit_depth_function!(
1075 x264,
1076 8: [YUV420P, YUVJ420P, YUV422P, YUVJ422P, YUV444P, YUVJ444P, NV12, NV16, NV21, GRAY8],
1077 10: [YUV420P10LE, YUV422P10LE, YUV444P10LE, NV20LE, GRAY10LE],
1078 12: []
1079);
1080create_get_format_bit_depth_function!(
1081 x265,
1082 8: [YUV420P, YUVJ420P, YUV422P, YUVJ422P, YUV444P, YUVJ444P, GBRP, GRAY8],
1083 10: [YUV420P10LE, YUV422P10LE, YUV444P10LE, GBRP10LE, GRAY10LE],
1084 12: [YUV420P12LE, YUV422P12LE, YUV444P12LE, GBRP12LE, GRAY12LE]
1085);
1086create_get_format_bit_depth_function!(
1087 vpx,
1088 8: [YUV420P, YUVA420P, YUV422P, YUV440P, YUV444P, GBRP],
1089 10: [YUV420P10LE, YUV422P10LE, YUV440P10LE, YUV444P10LE, GBRP10LE],
1090 12: [YUV420P12LE, YUV422P12LE, YUV440P12LE, YUV444P12LE, GBRP12LE]
1091);
1092create_get_format_bit_depth_function!(
1093 aom,
1094 8: [YUV420P, YUV422P, YUV444P, GBRP, GRAY8],
1095 10: [YUV420P10LE, YUV422P10LE, YUV444P10LE, GBRP10LE, GRAY10LE],
1096 12: [YUV420P12LE, YUV422P12LE, YUV444P12LE, GBRP12LE, GRAY12LE,]
1097);
1098create_get_format_bit_depth_function!(
1099 rav1e,
1100 8: [YUV420P, YUVJ420P, YUV422P, YUVJ422P, YUV444P, YUVJ444P],
1101 10: [YUV420P10LE, YUV422P10LE, YUV444P10LE],
1102 12: [YUV420P12LE, YUV422P12LE, YUV444P12LE,]
1103);
1104create_get_format_bit_depth_function!(
1105 svt_av1,
1106 8: [YUV420P],
1107 10: [YUV420P10LE],
1108 12: []
1109);