av1an_core/encoder/
mod.rs

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        // If the version failed to parse, check if it accepts old arguments
35        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        // assume an old version of SVT-AV1 if the version and unprocessed tokens failed
47        // to parse, as the format for v0.9.0+ should be the same
48        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
62// Encoder Maximum Speed Values
63const 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    /// Composes 1st pass command for 1 pass encoding
172    #[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    /// Composes 1st pass command for 2 pass encoding
207    #[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    /// Composes 2st pass command for 2 pass encoding
284    #[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    /// Returns default settings for the encoder
361    #[inline]
362    pub fn get_default_arguments(self, (cols, rows): (u32, u32)) -> Vec<String> {
363        match self {
364            // aomenc automatically infers the correct bit depth, and thus for aomenc, not
365            // specifying the bit depth is actually more accurate because if for example
366            // you specify `--pix-format yuv420p`, aomenc will encode 10-bit when that
367            // is not actually the desired pixel format.
368            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            // vpxenc does not infer the pixel format from the input, so `-b 10` is still required
411            // to work with the default pixel format (yuv420p10le).
412            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    /// Return number of default passes for encoder
488    #[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    /// Default quantizer range target quality mode
497    #[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    /// Returns quantizer percentage (0-1) relative to entire range for encoder
508    #[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, // 0-63
512            Self::rav1e => quantizer as f64 / 256.0,                          // 0-255
513            Self::x264 | Self::x265 => quantizer as f64 / 52.0,               // 0-51
514        };
515
516        // Clamp to 0-1 in case quantizer is out of expected range
517        percentage.clamp(0.0, 1.0)
518    }
519
520    /// Returns help command for encoder
521    #[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    /// Returns version text for encoder, or None if encoder is not available in
534    /// PATH
535    #[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    /// Get the name of the executable/binary for the encoder
601    #[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    /// Get the name of the video format associated with the encoder
614    #[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    /// Get the default output extension for the encoder
625    #[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    /// Returns function pointer used for matching Q/CRF arguments in command
635    /// line
636    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    /// Returns changed q/crf in command line arguments
676    #[inline]
677    pub fn man_command(self, mut params: Vec<String>, q: f32) -> Vec<String> {
678        let index = list_index(&params, 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    /// Parses the number of encoded frames
691    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                      // SAFETY: We verified that the CPU has the required feature set
700                      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    /// Returns command used for target quality probing
714    #[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    /// Returns command used for target quality probing (slow, correctness
902    /// focused version)
903    #[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    /// Function `remove_patterns` that takes in args and patterns and removes
949    /// all instances of the patterns from the args.
950    #[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 does not contain =, we need to remove the index that follows.
956                if !pattern.contains('=') {
957                    args.remove(index);
958                }
959            }
960        }
961    }
962
963    #[expect(clippy::too_many_arguments)]
964    #[inline]
965    /// Constructs tuple of commands for target quality probing
966    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
1072// The supported bit depths are taken from ffmpeg,
1073// e.g.: `ffmpeg -h encoder=libx264`
1074create_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);