Add zones support (#598)
* Add zones support Currently implemented: - Overriding the selected encoder & number of passes - Overriding or adding video params - Overriding photon noise setting - Overriding min/max scene length Closes #267 * Error if zoned encoder does not support output pixel format * Fix crash if zones change number of passes * Set passes to 1 for zones with rt mode
This commit is contained in:
parent
d9d15a90d0
commit
84d46bb40c
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -135,7 +135,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "av-scenechange"
|
||||
version = "0.7.2"
|
||||
source = "git+https://github.com/rust-av/av-scenechange?rev=9425e3597b27c79cd970756b7cdb9c24cb2c4d03#9425e3597b27c79cd970756b7cdb9c24cb2c4d03"
|
||||
source = "git+https://github.com/rust-av/av-scenechange?rev=0c63f493200eeccc21193d129fc63cb1265e40fa#0c63f493200eeccc21193d129fc63cb1265e40fa"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rav1e",
|
||||
|
@ -195,6 +195,7 @@ dependencies = [
|
|||
"itertools",
|
||||
"log",
|
||||
"memchr",
|
||||
"nom",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
|
|
|
@ -47,7 +47,7 @@ debug-assertions = true
|
|||
overflow-checks = true
|
||||
|
||||
[patch.crates-io]
|
||||
av-scenechange = { git = "https://github.com/rust-av/av-scenechange", rev = "9425e3597b27c79cd970756b7cdb9c24cb2c4d03" }
|
||||
av-scenechange = { git = "https://github.com/rust-av/av-scenechange", rev = "0c63f493200eeccc21193d129fc63cb1265e40fa" }
|
||||
# TODO: switch to release version once the fix for av_get_best_stream is published on crates.io.
|
||||
ffmpeg-next = { git = "https://github.com/zmwangx/rust-ffmpeg", rev = "0054b0e51b35ed240b193c7a93455714b4d75726" }
|
||||
console = { git = "https://github.com/console-rs/console", rev = "5484ea9d9f6884f6685349708e27bf08fab7703c" }
|
||||
|
|
|
@ -353,6 +353,47 @@ pub struct CliOpts {
|
|||
#[clap(long, default_value = "yuv420p10le", help_heading = "ENCODING")]
|
||||
pub pix_format: Pixel,
|
||||
|
||||
/// Path to a file specifying zones within the video with differing encoder settings.
|
||||
///
|
||||
/// The zones file should include one zone per line, with each arg within a zone space-separated.
|
||||
/// No quotes or escaping are needed around the encoder args, as these are assumed to be the last argument.
|
||||
/// The zone args on each line should be in this order:
|
||||
///
|
||||
/// start_frame end_frame encoder reset(opt) video_params
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```ignore
|
||||
/// 136 169 aom --photon-noise 4 --cq-level=32
|
||||
/// 169 1330 rav1e reset -s 3 -q 42
|
||||
/// ```
|
||||
///
|
||||
/// Example line 1 will encode frames 136-168 using aomenc with the argument `--cq-level=32`
|
||||
/// and enable av1an's `--photon-noise` option.
|
||||
/// Note that the end frame number is *exclusive*. The start and end frame will both be forced
|
||||
/// to be scenecuts. Additional scene detection will still be applied within the zones.
|
||||
/// `-1` can be used to refer to the last frame in the video.
|
||||
///
|
||||
/// The default behavior as shown on line 1 is to preserve any options passed to
|
||||
/// `--video-params` or `--photon-noise` in av1an, and append or overwrite
|
||||
/// the additional zone settings.
|
||||
///
|
||||
/// Example line 2 will encode frames 169-1329 using rav1e. The `reset` keyword instructs
|
||||
/// av1an to ignore any settings which affect the encoder, and use only the
|
||||
/// parameters from this zone.
|
||||
///
|
||||
/// For segments where no zone is specified, the settings passed to av1an itself will be used.
|
||||
///
|
||||
/// The video params which may be specified include any parameters that are allowed by
|
||||
/// the encoder, as well as the following av1an options:
|
||||
///
|
||||
/// - `-x`/`--extra-split`
|
||||
/// - `--min-scene-len`
|
||||
/// - `--passes`
|
||||
/// - `--photon-noise` (aomenc only)
|
||||
#[clap(long, parse(from_os_str), help_heading = "ENCODING")]
|
||||
pub zones: Option<PathBuf>,
|
||||
|
||||
/// Plot an SVG of the VMAF for the encode
|
||||
///
|
||||
/// This option is independent of --target-quality, i.e. it can be used with or without it.
|
||||
|
@ -607,6 +648,7 @@ pub fn parse_cli(args: CliOpts) -> anyhow::Result<Vec<EncodeArgs>> {
|
|||
workers: args.workers,
|
||||
set_thread_affinity: args.set_thread_affinity,
|
||||
vs_script: None,
|
||||
zones: args.zones.clone(),
|
||||
};
|
||||
|
||||
arg.startup_check()?;
|
||||
|
|
|
@ -44,6 +44,7 @@ paste = "1.0.5"
|
|||
simdutf8 = "0.1.3"
|
||||
parking_lot = "0.12.0"
|
||||
cfg-if = "1.0.0"
|
||||
nom = "7.1.1"
|
||||
# TODO: move all of this CLI stuff to av1an-cli
|
||||
ansi_term = "0.12.1"
|
||||
|
||||
|
|
|
@ -218,11 +218,21 @@ impl<'a> Broker<'a> {
|
|||
let padding = printable_base10_digits(self.total_chunks - 1) as usize;
|
||||
|
||||
// Run all passes for this chunk
|
||||
let encoder = chunk
|
||||
.overrides
|
||||
.as_ref()
|
||||
.map_or(self.project.encoder, |ovr| ovr.encoder);
|
||||
let passes = chunk
|
||||
.overrides
|
||||
.as_ref()
|
||||
.map_or(self.project.passes, |ovr| ovr.passes);
|
||||
let mut tpl_crash_workaround = false;
|
||||
for current_pass in 1..=self.project.passes {
|
||||
for current_pass in 1..=passes {
|
||||
for r#try in 1..=self.max_tries {
|
||||
let res = self.project.create_pipes(
|
||||
chunk,
|
||||
encoder,
|
||||
passes,
|
||||
current_pass,
|
||||
worker_id,
|
||||
padding,
|
||||
|
@ -246,7 +256,7 @@ impl<'a> Broker<'a> {
|
|||
// since `Broker::encoding_loop` will print the error message as well
|
||||
warn!("Encoder failed (on chunk {}):\n{}", chunk.index, e);
|
||||
|
||||
if self.project.encoder == Encoder::aom
|
||||
if encoder == Encoder::aom
|
||||
&& !tpl_crash_workaround
|
||||
&& memmem::rfind(e.stderr.as_bytes(), b"av1_tpl_stats_ready").is_some()
|
||||
{
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::scenes::ZoneOptions;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{ffi::OsString, path::Path};
|
||||
|
||||
|
@ -8,6 +9,7 @@ pub struct Chunk {
|
|||
pub source: Vec<OsString>,
|
||||
pub output_ext: String,
|
||||
pub frames: usize,
|
||||
pub overrides: Option<ZoneOptions>,
|
||||
// do not break compatibility with output produced by older versions of av1an
|
||||
/// Optional target quality CQ level
|
||||
#[serde(rename = "per_shot_target_quality_cq")]
|
||||
|
@ -15,24 +17,6 @@ pub struct Chunk {
|
|||
}
|
||||
|
||||
impl Chunk {
|
||||
pub fn new(
|
||||
temp: String,
|
||||
index: usize,
|
||||
source: Vec<OsString>,
|
||||
output_ext: &'static str,
|
||||
frames: usize,
|
||||
per_shot_target_quality_cq: Option<u32>,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
Ok(Self {
|
||||
temp,
|
||||
index,
|
||||
source,
|
||||
output_ext: output_ext.to_owned(),
|
||||
frames,
|
||||
tq_cq: per_shot_target_quality_cq,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns numeric name of chunk `00001`
|
||||
pub fn name(&self) -> String {
|
||||
format!("{:05}", self.index)
|
||||
|
@ -62,6 +46,7 @@ mod tests {
|
|||
output_ext: "ivf".to_owned(),
|
||||
frames: 5,
|
||||
tq_cq: None,
|
||||
overrides: None,
|
||||
};
|
||||
assert_eq!("00001", ch.name());
|
||||
}
|
||||
|
@ -74,6 +59,7 @@ mod tests {
|
|||
output_ext: "ivf".to_owned(),
|
||||
frames: 5,
|
||||
tq_cq: None,
|
||||
overrides: None,
|
||||
};
|
||||
assert_eq!("10000", ch.name());
|
||||
}
|
||||
|
@ -87,6 +73,7 @@ mod tests {
|
|||
output_ext: "ivf".to_owned(),
|
||||
frames: 5,
|
||||
tq_cq: None,
|
||||
overrides: None,
|
||||
};
|
||||
assert_eq!("d/encode/00001.ivf", ch.output());
|
||||
}
|
||||
|
|
|
@ -435,6 +435,15 @@ impl Encoder {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn format(self) -> &'static str {
|
||||
match self {
|
||||
Self::aom | Self::rav1e | Self::svt_av1 => "av1",
|
||||
Self::vpx => "vpx",
|
||||
Self::x264 => "h264",
|
||||
Self::x265 => "h265",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default output extension for the encoder
|
||||
pub const fn output_extension(&self) -> &'static str {
|
||||
match &self {
|
||||
|
|
|
@ -57,6 +57,7 @@ mod grain;
|
|||
pub(crate) mod parse;
|
||||
pub mod progress_bar;
|
||||
pub mod scene_detect;
|
||||
mod scenes;
|
||||
pub mod settings;
|
||||
pub mod split;
|
||||
pub mod target_quality;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use ffmpeg::format::Pixel;
|
||||
use itertools::Itertools;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::Encoder;
|
||||
|
@ -6,6 +7,7 @@ use crate::{into_smallvec, progress_bar, Input, ScenecutMethod, Verbosity};
|
|||
use ansi_term::Style;
|
||||
use av_scenechange::{detect_scene_changes, DetectionOptions, SceneDetectionSpeed};
|
||||
|
||||
use crate::scenes::Scene;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
|
||||
|
@ -18,7 +20,8 @@ pub fn av_scenechange_detect(
|
|||
sc_pix_format: Option<Pixel>,
|
||||
sc_method: ScenecutMethod,
|
||||
sc_downscale_height: Option<usize>,
|
||||
) -> anyhow::Result<(Vec<usize>, usize)> {
|
||||
zones: &[Scene],
|
||||
) -> anyhow::Result<(Vec<Scene>, usize)> {
|
||||
if verbosity != Verbosity::Quiet {
|
||||
if atty::is(atty::Stream::Stderr) {
|
||||
eprintln!("{}", Style::default().bold().paint("Scene detection"));
|
||||
|
@ -38,45 +41,44 @@ pub fn av_scenechange_detect(
|
|||
frames
|
||||
});
|
||||
|
||||
let mut scenecuts = scene_detect(
|
||||
let scenes = scene_detect(
|
||||
input,
|
||||
encoder,
|
||||
total_frames,
|
||||
if verbosity == Verbosity::Quiet {
|
||||
None
|
||||
} else {
|
||||
Some(Box::new(|frames, _keyframes| {
|
||||
Some(&|frames, _keyframes| {
|
||||
progress_bar::set_pos(frames as u64);
|
||||
}))
|
||||
})
|
||||
},
|
||||
min_scene_len,
|
||||
sc_pix_format,
|
||||
sc_method,
|
||||
sc_downscale_height,
|
||||
zones,
|
||||
)?;
|
||||
|
||||
let frames = frame_thread.join().unwrap();
|
||||
|
||||
progress_bar::finish_progress_bar();
|
||||
|
||||
if scenecuts[0] == 0 {
|
||||
// TODO refactor the chunk creation to not require this
|
||||
// Currently, this is required for compatibility with create_video_queue_vs
|
||||
scenecuts.remove(0);
|
||||
}
|
||||
|
||||
Ok((scenecuts, frames))
|
||||
Ok((scenes, frames))
|
||||
}
|
||||
|
||||
/// Detect scene changes using rav1e scene detector.
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
pub fn scene_detect(
|
||||
input: &Input,
|
||||
encoder: Encoder,
|
||||
callback: Option<Box<dyn Fn(usize, usize)>>,
|
||||
total_frames: usize,
|
||||
callback: Option<&dyn Fn(usize, usize)>,
|
||||
min_scene_len: usize,
|
||||
sc_pix_format: Option<Pixel>,
|
||||
sc_method: ScenecutMethod,
|
||||
sc_downscale_height: Option<usize>,
|
||||
) -> anyhow::Result<Vec<usize>> {
|
||||
zones: &[Scene],
|
||||
) -> anyhow::Result<Vec<Scene>> {
|
||||
let bit_depth;
|
||||
|
||||
let filters: SmallVec<[String; 4]> = match (sc_downscale_height, sc_pix_format) {
|
||||
|
@ -140,17 +142,86 @@ pub fn scene_detect(
|
|||
}
|
||||
})?;
|
||||
|
||||
let options = DetectionOptions {
|
||||
min_scenecut_distance: Some(min_scene_len),
|
||||
analysis_speed: match sc_method {
|
||||
ScenecutMethod::Fast => SceneDetectionSpeed::Fast,
|
||||
ScenecutMethod::Standard => SceneDetectionSpeed::Standard,
|
||||
},
|
||||
..DetectionOptions::default()
|
||||
};
|
||||
Ok(if bit_depth > 8 {
|
||||
detect_scene_changes::<_, u16>(decoder, options, callback).scene_changes
|
||||
let mut scenes = Vec::new();
|
||||
let mut cur_zone = zones.first().filter(|frame| frame.start_frame == 0);
|
||||
let mut next_zone_idx = if zones.is_empty() {
|
||||
None
|
||||
} else if cur_zone.is_some() {
|
||||
if zones.len() == 1 {
|
||||
None
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
} else {
|
||||
detect_scene_changes::<_, u8>(decoder, options, callback).scene_changes
|
||||
})
|
||||
Some(0)
|
||||
};
|
||||
let mut frames_read = 0;
|
||||
loop {
|
||||
let mut min_scene_len = min_scene_len;
|
||||
if let Some(zone) = cur_zone {
|
||||
if let Some(ref overrides) = zone.zone_overrides {
|
||||
min_scene_len = overrides.min_scene_len;
|
||||
}
|
||||
};
|
||||
let options = DetectionOptions {
|
||||
min_scenecut_distance: Some(min_scene_len),
|
||||
analysis_speed: match sc_method {
|
||||
ScenecutMethod::Fast => SceneDetectionSpeed::Fast,
|
||||
ScenecutMethod::Standard => SceneDetectionSpeed::Standard,
|
||||
},
|
||||
..DetectionOptions::default()
|
||||
};
|
||||
let frame_limit = if let Some(zone) = cur_zone {
|
||||
Some(zone.end_frame - zone.start_frame)
|
||||
} else if let Some(next_idx) = next_zone_idx {
|
||||
let zone = &zones[next_idx];
|
||||
Some(zone.start_frame - frames_read)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let scene_changes = if bit_depth > 8 {
|
||||
detect_scene_changes::<_, u16>(decoder, options, frame_limit, callback).scene_changes
|
||||
} else {
|
||||
detect_scene_changes::<_, u8>(decoder, options, frame_limit, callback).scene_changes
|
||||
};
|
||||
for (start, end) in scene_changes.iter().copied().tuple_windows() {
|
||||
scenes.push(Scene {
|
||||
start_frame: start + frames_read,
|
||||
end_frame: end + frames_read,
|
||||
zone_overrides: cur_zone.and_then(|zone| zone.zone_overrides.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
scenes.push(Scene {
|
||||
start_frame: scenes
|
||||
.last()
|
||||
.map(|scene| scene.end_frame)
|
||||
.unwrap_or_default(),
|
||||
end_frame: if let Some(limit) = frame_limit {
|
||||
frames_read += limit;
|
||||
frames_read
|
||||
} else {
|
||||
total_frames
|
||||
},
|
||||
zone_overrides: cur_zone.and_then(|zone| zone.zone_overrides.clone()),
|
||||
});
|
||||
if let Some(next_idx) = next_zone_idx {
|
||||
if cur_zone.map_or(true, |zone| zone.end_frame == zones[next_idx].start_frame) {
|
||||
cur_zone = Some(&zones[next_idx]);
|
||||
next_zone_idx = if next_idx + 1 == zones.len() {
|
||||
None
|
||||
} else {
|
||||
Some(next_idx + 1)
|
||||
};
|
||||
} else {
|
||||
cur_zone = None;
|
||||
}
|
||||
} else if cur_zone.map_or(true, |zone| zone.end_frame == total_frames) {
|
||||
// End of video
|
||||
break;
|
||||
} else {
|
||||
cur_zone = None;
|
||||
}
|
||||
}
|
||||
Ok(scenes)
|
||||
}
|
||||
|
|
452
av1an-core/src/scenes.rs
Normal file
452
av1an-core/src/scenes.rs
Normal file
|
@ -0,0 +1,452 @@
|
|||
use crate::parse::valid_params;
|
||||
use crate::settings::{invalid_params, suggest_fix, EncodeArgs};
|
||||
use crate::Encoder;
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, bail};
|
||||
use itertools::Itertools;
|
||||
use nom::branch::alt;
|
||||
use nom::bytes::complete::{tag, take_till, take_while};
|
||||
use nom::character::complete::{char, digit1, space1};
|
||||
use nom::combinator::{map, map_res, opt, recognize, rest};
|
||||
use nom::multi::{many1, separated_list0};
|
||||
use nom::sequence::{preceded, tuple};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::process::{exit, Command};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct Scene {
|
||||
pub start_frame: usize,
|
||||
// Reminding again that end_frame is *exclusive*
|
||||
pub end_frame: usize,
|
||||
pub zone_overrides: Option<ZoneOptions>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ZoneOptions {
|
||||
pub encoder: Encoder,
|
||||
pub passes: u8,
|
||||
pub video_params: Vec<String>,
|
||||
pub photon_noise: Option<u8>,
|
||||
pub extra_splits_len: Option<usize>,
|
||||
pub min_scene_len: usize,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub fn parse_from_zone(input: &str, encode_args: &EncodeArgs) -> Result<Self> {
|
||||
let (_, (start, _, end, _, encoder, reset, zone_args)): (
|
||||
_,
|
||||
(usize, _, usize, _, Encoder, bool, &str),
|
||||
) = tuple::<_, _, nom::error::Error<&str>, _>((
|
||||
map_res(digit1, str::parse),
|
||||
many1(char(' ')),
|
||||
map_res(alt((tag("-1"), digit1)), |res: &str| {
|
||||
if res == "-1" {
|
||||
Ok(encode_args.frames)
|
||||
} else {
|
||||
res.parse::<usize>()
|
||||
}
|
||||
}),
|
||||
many1(char(' ')),
|
||||
map_res(
|
||||
alt((
|
||||
tag("aom"),
|
||||
tag("rav1e"),
|
||||
tag("x264"),
|
||||
tag("x265"),
|
||||
tag("vpx"),
|
||||
tag("svt-av1"),
|
||||
)),
|
||||
Encoder::from_str,
|
||||
),
|
||||
map(
|
||||
opt(preceded(many1(char(' ')), tag("reset"))),
|
||||
|res: Option<&str>| res.is_some(),
|
||||
),
|
||||
map(
|
||||
opt(preceded(many1(char(' ')), rest)),
|
||||
|res: Option<&str>| res.unwrap_or_default().trim(),
|
||||
),
|
||||
))(input)
|
||||
.map_err(|e| anyhow!("Invalid zone file syntax: {}", e))?;
|
||||
if start >= end {
|
||||
bail!("Start frame must be earlier than the end frame");
|
||||
}
|
||||
if start >= encode_args.frames || end > encode_args.frames {
|
||||
bail!("Start and end frames must not be past the end of the video");
|
||||
}
|
||||
if encoder.format() != encode_args.encoder.format() {
|
||||
bail!(
|
||||
"Zone specifies using {}, but this cannot be used in the same file as {}",
|
||||
encoder,
|
||||
encode_args.encoder,
|
||||
);
|
||||
}
|
||||
if encoder != encode_args.encoder {
|
||||
if encoder
|
||||
.get_format_bit_depth(encode_args.output_pix_format.format)
|
||||
.is_err()
|
||||
{
|
||||
bail!(
|
||||
"Output pixel format {:?} is not supported by {} (used in zones file)",
|
||||
encode_args.output_pix_format.format,
|
||||
encoder
|
||||
);
|
||||
}
|
||||
if !reset {
|
||||
bail!("Zone includes encoder change but previous args were kept. You probably meant to specify \"reset\".");
|
||||
}
|
||||
}
|
||||
|
||||
// Inherit from encode args or reset to defaults
|
||||
let mut video_params = if reset {
|
||||
Vec::new()
|
||||
} else {
|
||||
encode_args.video_params.clone()
|
||||
};
|
||||
let mut passes = if reset {
|
||||
encoder.get_default_pass()
|
||||
} else {
|
||||
encode_args.passes
|
||||
};
|
||||
let mut photon_noise = if reset {
|
||||
None
|
||||
} else {
|
||||
encode_args.photon_noise
|
||||
};
|
||||
let mut extra_splits_len = encode_args.extra_splits_len;
|
||||
let mut min_scene_len = encode_args.min_scene_len;
|
||||
|
||||
// Parse overrides
|
||||
let zone_args: (&str, Vec<(&str, Option<&str>)>) =
|
||||
separated_list0::<_, _, _, nom::error::Error<&str>, _, _>(
|
||||
space1,
|
||||
tuple((
|
||||
recognize(tuple((
|
||||
alt((tag("--"), tag("-"))),
|
||||
take_till(|c| c == '=' || c == ' '),
|
||||
))),
|
||||
opt(preceded(alt((space1, tag("="))), take_while(|c| c != ' '))),
|
||||
)),
|
||||
)(zone_args)
|
||||
.map_err(|e| anyhow!("Invalid zone file syntax: {}", e))?;
|
||||
let mut zone_args = zone_args.1.into_iter().collect::<HashMap<_, _>>();
|
||||
if let Some(zone_passes) = zone_args.remove("--passes") {
|
||||
passes = zone_passes.unwrap().parse().unwrap();
|
||||
} else if [Encoder::aom, Encoder::vpx].contains(&encoder) && zone_args.contains_key("--rt") {
|
||||
passes = 1;
|
||||
}
|
||||
if let Some(zone_photon_noise) = zone_args.remove("--photon-noise") {
|
||||
photon_noise = Some(zone_photon_noise.unwrap().parse().unwrap());
|
||||
}
|
||||
if let Some(zone_xs) = zone_args
|
||||
.remove("-x")
|
||||
.or_else(|| zone_args.remove("--extra-split"))
|
||||
{
|
||||
extra_splits_len = Some(zone_xs.unwrap().parse().unwrap());
|
||||
}
|
||||
if let Some(zone_min_scene_len) = zone_args.remove("--min-scene-len") {
|
||||
min_scene_len = zone_min_scene_len.unwrap().parse().unwrap();
|
||||
}
|
||||
let raw_zone_args = if [Encoder::aom, Encoder::vpx].contains(&encoder) {
|
||||
zone_args
|
||||
.into_iter()
|
||||
.map(|(key, value)| {
|
||||
value.map_or_else(|| key.to_string(), |value| format!("{}={}", key, value))
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
} else {
|
||||
zone_args
|
||||
.keys()
|
||||
.map(|&k| Some(k.to_string()))
|
||||
.interleave(
|
||||
zone_args
|
||||
.values()
|
||||
.map(|v| v.map(std::string::ToString::to_string)),
|
||||
)
|
||||
.flatten()
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
|
||||
if !encode_args.force {
|
||||
let help_text = {
|
||||
let [cmd, arg] = encoder.help_command();
|
||||
String::from_utf8(Command::new(cmd).arg(arg).output().unwrap().stdout).unwrap()
|
||||
};
|
||||
let valid_params = valid_params(&help_text, encoder);
|
||||
let interleaved_args: Vec<&str> = raw_zone_args
|
||||
.iter()
|
||||
.filter_map(|param| {
|
||||
if param.starts_with('-') && [Encoder::aom, Encoder::vpx].contains(&encoder) {
|
||||
// These encoders require args to be passed using an equal sign,
|
||||
// e.g. `--cq-level=30`
|
||||
param.split('=').next()
|
||||
} else {
|
||||
// The other encoders use a space, so we don't need to do extra splitting,
|
||||
// e.g. `--crf 30`
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let invalid_params = invalid_params(&interleaved_args, &valid_params);
|
||||
|
||||
for wrong_param in &invalid_params {
|
||||
eprintln!("'{}' isn't a valid parameter for {}", wrong_param, encoder);
|
||||
if let Some(suggestion) = suggest_fix(wrong_param, &valid_params) {
|
||||
eprintln!("\tDid you mean '{}'?", suggestion);
|
||||
}
|
||||
}
|
||||
|
||||
if !invalid_params.is_empty() {
|
||||
println!("\nTo continue anyway, run av1an with '--force'");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
for arg in raw_zone_args {
|
||||
if arg.starts_with("--")
|
||||
|| (arg.starts_with('-') && arg.chars().nth(1).map_or(false, char::is_alphabetic))
|
||||
{
|
||||
let key = arg.split_once('=').map_or(arg.as_str(), |split| split.0);
|
||||
if let Some(pos) = video_params
|
||||
.iter()
|
||||
.position(|param| param == key || param.starts_with(&format!("{}=", key)))
|
||||
{
|
||||
video_params.remove(pos);
|
||||
if let Some(next) = video_params.get(pos) {
|
||||
if !([Encoder::aom, Encoder::vpx].contains(&encoder)
|
||||
|| next.starts_with("--")
|
||||
|| (next.starts_with('-') && next.chars().nth(1).map_or(false, char::is_alphabetic)))
|
||||
{
|
||||
video_params.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
video_params.push(arg);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
start_frame: start,
|
||||
end_frame: end,
|
||||
zone_overrides: Some(ZoneOptions {
|
||||
encoder,
|
||||
passes,
|
||||
video_params,
|
||||
photon_noise,
|
||||
extra_splits_len,
|
||||
min_scene_len,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn get_test_args() -> EncodeArgs {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ffmpeg::format::Pixel;
|
||||
|
||||
use crate::{
|
||||
concat::ConcatMethod,
|
||||
into_vec,
|
||||
settings::{InputPixelFormat, PixelFormat},
|
||||
ChunkMethod, ChunkOrdering, Input, ScenecutMethod, SplitMethod, Verbosity,
|
||||
};
|
||||
|
||||
EncodeArgs {
|
||||
frames: 6900,
|
||||
log_file: PathBuf::new(),
|
||||
ffmpeg_filter_args: Vec::new(),
|
||||
temp: String::new(),
|
||||
force: false,
|
||||
passes: 2,
|
||||
video_params: into_vec!["--cq-level=40", "--cpu-used=0", "--aq-mode=1"],
|
||||
output_file: String::new(),
|
||||
audio_params: Vec::new(),
|
||||
chunk_method: ChunkMethod::LSMASH,
|
||||
chunk_order: ChunkOrdering::Random,
|
||||
concat: ConcatMethod::FFmpeg,
|
||||
encoder: Encoder::aom,
|
||||
extra_splits_len: Some(100),
|
||||
photon_noise: Some(10),
|
||||
sc_pix_format: None,
|
||||
keep: false,
|
||||
max_tries: 3,
|
||||
min_q: None,
|
||||
max_q: None,
|
||||
min_scene_len: 10,
|
||||
vmaf_threads: None,
|
||||
input_pix_format: InputPixelFormat::FFmpeg {
|
||||
format: Pixel::YUV420P10LE,
|
||||
},
|
||||
input: Input::Video(PathBuf::new()),
|
||||
output_pix_format: PixelFormat {
|
||||
format: Pixel::YUV420P10LE,
|
||||
bit_depth: 10,
|
||||
},
|
||||
probe_slow: false,
|
||||
probes: 1,
|
||||
probing_rate: 1,
|
||||
resume: false,
|
||||
scenes: None,
|
||||
split_method: SplitMethod::AvScenechange,
|
||||
sc_method: ScenecutMethod::Standard,
|
||||
sc_only: false,
|
||||
sc_downscale_height: None,
|
||||
target_quality: None,
|
||||
verbosity: Verbosity::Normal,
|
||||
vmaf: false,
|
||||
vmaf_filter: None,
|
||||
vmaf_path: None,
|
||||
vmaf_res: String::new(),
|
||||
workers: 1,
|
||||
set_thread_affinity: None,
|
||||
vs_script: None,
|
||||
zones: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_zones_args() {
|
||||
let input = "45 729 aom --cq-level=20 --photon-noise 4 -x 60 --min-scene-len 12";
|
||||
let args = get_test_args();
|
||||
let result = Scene::parse_from_zone(input, &args).unwrap();
|
||||
assert_eq!(result.start_frame, 45);
|
||||
assert_eq!(result.end_frame, 729);
|
||||
|
||||
let zone_overrides = result.zone_overrides.unwrap();
|
||||
assert_eq!(zone_overrides.encoder, Encoder::aom);
|
||||
assert_eq!(zone_overrides.extra_splits_len, Some(60));
|
||||
assert_eq!(zone_overrides.min_scene_len, 12);
|
||||
assert_eq!(zone_overrides.photon_noise, Some(4));
|
||||
assert!(!zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cq-level=40".to_owned()));
|
||||
assert!(zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cq-level=20".to_owned()));
|
||||
assert!(zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cpu-used=0".to_owned()));
|
||||
assert!(zone_overrides
|
||||
.video_params
|
||||
.contains(&"--aq-mode=1".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_zones_reset() {
|
||||
let input = "729 1337 aom reset --cq-level=20 --cpu-used=5";
|
||||
let args = get_test_args();
|
||||
let result = Scene::parse_from_zone(input, &args).unwrap();
|
||||
assert_eq!(result.start_frame, 729);
|
||||
assert_eq!(result.end_frame, 1337);
|
||||
|
||||
let zone_overrides = result.zone_overrides.unwrap();
|
||||
assert_eq!(zone_overrides.encoder, Encoder::aom);
|
||||
// In the current implementation, scenecut settings should be preserved
|
||||
// unless manually overridden. Settings which affect the encoder,
|
||||
// including photon noise, should be reset.
|
||||
assert_eq!(zone_overrides.extra_splits_len, Some(100));
|
||||
assert_eq!(zone_overrides.min_scene_len, 10);
|
||||
assert_eq!(zone_overrides.photon_noise, None);
|
||||
assert!(!zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cq-level=40".to_owned()));
|
||||
assert!(!zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cpu-used=0".to_owned()));
|
||||
assert!(!zone_overrides
|
||||
.video_params
|
||||
.contains(&"--aq-mode=1".to_owned()));
|
||||
assert!(zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cq-level=20".to_owned()));
|
||||
assert!(zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cpu-used=5".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_zones_encoder_changed() {
|
||||
let input = "729 1337 rav1e reset -s 3 -q 45";
|
||||
let args = get_test_args();
|
||||
let result = Scene::parse_from_zone(input, &args).unwrap();
|
||||
assert_eq!(result.start_frame, 729);
|
||||
assert_eq!(result.end_frame, 1337);
|
||||
|
||||
let zone_overrides = result.zone_overrides.unwrap();
|
||||
assert_eq!(zone_overrides.encoder, Encoder::rav1e);
|
||||
assert_eq!(zone_overrides.extra_splits_len, Some(100));
|
||||
assert_eq!(zone_overrides.min_scene_len, 10);
|
||||
assert_eq!(zone_overrides.photon_noise, None);
|
||||
assert!(!zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cq-level=40".to_owned()));
|
||||
assert!(!zone_overrides
|
||||
.video_params
|
||||
.contains(&"--cpu-used=0".to_owned()));
|
||||
assert!(!zone_overrides
|
||||
.video_params
|
||||
.contains(&"--aq-mode=1".to_owned()));
|
||||
assert!(zone_overrides
|
||||
.video_params
|
||||
.windows(2)
|
||||
.any(|window| window[0] == "-s" && window[1] == "3"));
|
||||
assert!(zone_overrides
|
||||
.video_params
|
||||
.windows(2)
|
||||
.any(|window| window[0] == "-q" && window[1] == "45"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_zones_encoder_changed_no_reset() {
|
||||
let input = "729 1337 rav1e -s 3 -q 45";
|
||||
let args = get_test_args();
|
||||
let result = Scene::parse_from_zone(input, &args);
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
"Zone includes encoder change but previous args were kept. You probably meant to specify \"reset\"."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_zones_no_args() {
|
||||
let input = "2459 5000 rav1e";
|
||||
let args = get_test_args();
|
||||
let result = Scene::parse_from_zone(input, &args);
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
"Zone includes encoder change but previous args were kept. You probably meant to specify \"reset\"."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_zones_format_mismatch() {
|
||||
let input = "5000 -1 x264 reset";
|
||||
let args = get_test_args();
|
||||
let result = Scene::parse_from_zone(input, &args);
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
"Zone specifies using x264, but this cannot be used in the same file as aom"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_zones_no_args_reset() {
|
||||
let input = "5000 -1 rav1e reset";
|
||||
let args = get_test_args();
|
||||
|
||||
// This is weird, but can technically work for some encoders so we'll allow it.
|
||||
let result = Scene::parse_from_zone(input, &args).unwrap();
|
||||
assert_eq!(result.start_frame, 5000);
|
||||
assert_eq!(result.end_frame, 6900);
|
||||
|
||||
let zone_overrides = result.zone_overrides.unwrap();
|
||||
assert_eq!(zone_overrides.encoder, Encoder::rav1e);
|
||||
assert_eq!(zone_overrides.extra_splits_len, Some(100));
|
||||
assert_eq!(zone_overrides.min_scene_len, 10);
|
||||
assert_eq!(zone_overrides.photon_noise, None);
|
||||
assert!(zone_overrides.video_params.is_empty());
|
||||
}
|
|
@ -31,6 +31,7 @@ use rand::prelude::SliceRandom;
|
|||
use rand::thread_rng;
|
||||
use std::borrow::Borrow;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::thread;
|
||||
use std::{
|
||||
|
@ -52,6 +53,7 @@ use std::{
|
|||
|
||||
use ansi_term::{Color, Style};
|
||||
|
||||
use crate::scenes::{Scene, ZoneOptions};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::ChildStderr;
|
||||
|
||||
|
@ -94,6 +96,7 @@ pub struct EncodeArgs {
|
|||
pub workers: usize,
|
||||
pub set_thread_affinity: Option<usize>,
|
||||
pub photon_noise: Option<u8>,
|
||||
pub zones: Option<PathBuf>,
|
||||
|
||||
// FFmpeg params
|
||||
pub ffmpeg_filter_args: Vec<String>,
|
||||
|
@ -224,6 +227,8 @@ impl EncodeArgs {
|
|||
pub fn create_pipes(
|
||||
&self,
|
||||
chunk: &Chunk,
|
||||
encoder: Encoder,
|
||||
passes: u8,
|
||||
current_pass: u8,
|
||||
worker_id: usize,
|
||||
padding: usize,
|
||||
|
@ -235,27 +240,24 @@ impl EncodeArgs {
|
|||
.join("split")
|
||||
.join(format!("{}_fpf", chunk.name()));
|
||||
|
||||
let mut video_params = self.video_params.clone();
|
||||
let mut video_params = chunk
|
||||
.overrides
|
||||
.as_ref()
|
||||
.map_or_else(|| self.video_params.clone(), |ovr| ovr.video_params.clone());
|
||||
if tpl_crash_workaround {
|
||||
// In aomenc for duplicate arguments, whichever is specified last takes precedence.
|
||||
video_params.push("--enable-tpl-model=0".to_string());
|
||||
}
|
||||
let mut enc_cmd = if self.passes == 1 {
|
||||
self.encoder.compose_1_1_pass(video_params, chunk.output())
|
||||
let mut enc_cmd = if passes == 1 {
|
||||
encoder.compose_1_1_pass(video_params, chunk.output())
|
||||
} else if current_pass == 1 {
|
||||
self
|
||||
.encoder
|
||||
.compose_1_2_pass(video_params, fpf_file.to_str().unwrap())
|
||||
encoder.compose_1_2_pass(video_params, fpf_file.to_str().unwrap())
|
||||
} else {
|
||||
self
|
||||
.encoder
|
||||
.compose_2_2_pass(video_params, fpf_file.to_str().unwrap(), chunk.output())
|
||||
encoder.compose_2_2_pass(video_params, fpf_file.to_str().unwrap(), chunk.output())
|
||||
};
|
||||
|
||||
if let Some(per_shot_target_quality_cq) = chunk.tq_cq {
|
||||
enc_cmd = self
|
||||
.encoder
|
||||
.man_command(enc_cmd, per_shot_target_quality_cq as usize);
|
||||
enc_cmd = encoder.man_command(enc_cmd, per_shot_target_quality_cq as usize);
|
||||
}
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
|
@ -397,7 +399,7 @@ impl EncodeArgs {
|
|||
enc_stderr.push_str(line);
|
||||
enc_stderr.push('\n');
|
||||
|
||||
if let Some(new) = self.encoder.parse_encoded_frames(line) {
|
||||
if let Some(new) = encoder.parse_encoded_frames(line) {
|
||||
if new > frame {
|
||||
if self.verbosity == Verbosity::Normal {
|
||||
inc_bar((new - frame) as u64);
|
||||
|
@ -442,42 +444,8 @@ impl EncodeArgs {
|
|||
}
|
||||
|
||||
fn validate_encoder_params(&self) {
|
||||
#[must_use]
|
||||
fn invalid_params<'a>(
|
||||
params: &'a [&'a str],
|
||||
valid_options: &'a HashSet<Cow<'a, str>>,
|
||||
) -> Vec<&'a str> {
|
||||
params
|
||||
.iter()
|
||||
.filter(|param| !valid_options.contains(Borrow::<str>::borrow(&**param)))
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn suggest_fix<'a>(
|
||||
wrong_arg: &str,
|
||||
arg_dictionary: &'a HashSet<Cow<'a, str>>,
|
||||
) -> Option<&'a str> {
|
||||
// Minimum threshold to consider a suggestion similar enough that it could be a typo
|
||||
const MIN_THRESHOLD: f64 = 0.75;
|
||||
|
||||
arg_dictionary
|
||||
.iter()
|
||||
.map(|arg| (arg, strsim::jaro_winkler(arg, wrong_arg)))
|
||||
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less))
|
||||
.and_then(|(suggestion, score)| {
|
||||
if score > MIN_THRESHOLD {
|
||||
Some(suggestion.borrow())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let video_params: Vec<&str> = self
|
||||
.video_params
|
||||
.as_slice()
|
||||
.iter()
|
||||
.filter_map(|param| {
|
||||
if param.starts_with('-') && [Encoder::aom, Encoder::vpx].contains(&self.encoder) {
|
||||
|
@ -684,18 +652,18 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
}
|
||||
}
|
||||
|
||||
fn create_encoding_queue(&mut self, splits: Vec<usize>) -> anyhow::Result<Vec<Chunk>> {
|
||||
fn create_encoding_queue(&mut self, scenes: &[Scene]) -> anyhow::Result<Vec<Chunk>> {
|
||||
let mut chunks = match &self.input {
|
||||
Input::Video(_) => match self.chunk_method {
|
||||
ChunkMethod::FFMS2 | ChunkMethod::LSMASH => {
|
||||
let vs_script = self.vs_script.as_ref().unwrap().as_path();
|
||||
self.create_video_queue_vs(splits, vs_script)
|
||||
self.create_video_queue_vs(scenes, vs_script)
|
||||
}
|
||||
ChunkMethod::Hybrid => self.create_video_queue_hybrid(splits)?,
|
||||
ChunkMethod::Select => self.create_video_queue_select(splits),
|
||||
ChunkMethod::Segment => self.create_video_queue_segment(&splits)?,
|
||||
ChunkMethod::Hybrid => self.create_video_queue_hybrid(scenes)?,
|
||||
ChunkMethod::Select => self.create_video_queue_select(scenes),
|
||||
ChunkMethod::Segment => self.create_video_queue_segment(scenes)?,
|
||||
},
|
||||
Input::VapourSynth(vs_script) => self.create_video_queue_vs(splits, vs_script.as_path()),
|
||||
Input::VapourSynth(vs_script) => self.create_video_queue_vs(scenes, vs_script.as_path()),
|
||||
};
|
||||
|
||||
match self.chunk_order {
|
||||
|
@ -716,7 +684,7 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
Ok(chunks)
|
||||
}
|
||||
|
||||
fn calc_split_locations(&self) -> anyhow::Result<(Vec<usize>, usize)> {
|
||||
fn calc_split_locations(&self) -> anyhow::Result<(Vec<Scene>, usize)> {
|
||||
Ok(match self.split_method {
|
||||
SplitMethod::AvScenechange => av_scenechange_detect(
|
||||
&self.input,
|
||||
|
@ -727,14 +695,34 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
self.sc_pix_format,
|
||||
self.sc_method,
|
||||
self.sc_downscale_height,
|
||||
&self.parse_zones()?,
|
||||
)?,
|
||||
SplitMethod::None => (Vec::new(), self.input.frames()?),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_zones(&self) -> anyhow::Result<Vec<Scene>> {
|
||||
let mut zones = Vec::new();
|
||||
if let Some(ref zones_file) = self.zones {
|
||||
let input = fs::read_to_string(&zones_file)?;
|
||||
for zone_line in input.lines().map(str::trim).filter(|line| !line.is_empty()) {
|
||||
zones.push(Scene::parse_from_zone(zone_line, self)?);
|
||||
}
|
||||
zones.sort_unstable_by_key(|zone| zone.start_frame);
|
||||
let mut segments = BTreeSet::new();
|
||||
for zone in &zones {
|
||||
if segments.contains(&zone.start_frame) {
|
||||
bail!("Zones file contains overlapping zones");
|
||||
}
|
||||
segments.extend(zone.start_frame..zone.end_frame);
|
||||
}
|
||||
}
|
||||
Ok(zones)
|
||||
}
|
||||
|
||||
// If we are not resuming, then do scene detection. Otherwise: get scenes from
|
||||
// scenes.json and return that.
|
||||
fn split_routine(&mut self) -> anyhow::Result<Vec<usize>> {
|
||||
fn split_routine(&mut self) -> anyhow::Result<Vec<Scene>> {
|
||||
let scene_file = self.scenes.as_ref().map_or_else(
|
||||
|| Cow::Owned(Path::new(&self.temp).join("scenes.json")),
|
||||
|path| Cow::Borrowed(path.as_path()),
|
||||
|
@ -746,6 +734,7 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
crate::split::read_scenes_from_file(scene_file.as_ref())?
|
||||
} else {
|
||||
used_existing_cuts = false;
|
||||
self.frames = self.input.frames()?;
|
||||
self.calc_split_locations()?
|
||||
};
|
||||
self.frames = frames;
|
||||
|
@ -777,6 +766,7 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
src_path: &Path,
|
||||
frame_start: usize,
|
||||
mut frame_end: usize,
|
||||
overrides: Option<ZoneOptions>,
|
||||
) -> Chunk {
|
||||
assert!(
|
||||
frame_start < frame_end,
|
||||
|
@ -816,25 +806,15 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
source: ffmpeg_gen_cmd,
|
||||
output_ext: output_ext.to_owned(),
|
||||
frames,
|
||||
overrides,
|
||||
..Chunk::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_vs_chunk(
|
||||
&self,
|
||||
index: usize,
|
||||
vs_script: &Path,
|
||||
frame_start: usize,
|
||||
mut frame_end: usize,
|
||||
) -> Chunk {
|
||||
assert!(
|
||||
frame_start < frame_end,
|
||||
"Can't make a chunk with <= 0 frames!"
|
||||
);
|
||||
|
||||
let frames = frame_end - frame_start;
|
||||
fn create_vs_chunk(&self, index: usize, vs_script: &Path, scene: &Scene) -> Chunk {
|
||||
let frames = scene.end_frame - scene.start_frame;
|
||||
// the frame end boundary is actually a frame that should be included in the next chunk
|
||||
frame_end -= 1;
|
||||
let frame_end = scene.end_frame - 1;
|
||||
|
||||
let vspipe_cmd_gen: Vec<OsString> = into_vec![
|
||||
"vspipe",
|
||||
|
@ -842,7 +822,7 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
"-y",
|
||||
"-",
|
||||
"-s",
|
||||
format!("{}", frame_start),
|
||||
format!("{}", scene.start_frame),
|
||||
"-e",
|
||||
format!("{}", frame_end),
|
||||
];
|
||||
|
@ -855,51 +835,54 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
source: vspipe_cmd_gen,
|
||||
output_ext: output_ext.to_owned(),
|
||||
frames,
|
||||
overrides: scene.zone_overrides.clone(),
|
||||
..Chunk::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_video_queue_vs(&self, splits: Vec<usize>, vs_script: &Path) -> Vec<Chunk> {
|
||||
let chunk_boundaries = iter::once(0)
|
||||
.chain(splits)
|
||||
.chain(iter::once(self.frames))
|
||||
.tuple_windows();
|
||||
|
||||
let chunk_queue: Vec<Chunk> = chunk_boundaries
|
||||
fn create_video_queue_vs(&self, scenes: &[Scene], vs_script: &Path) -> Vec<Chunk> {
|
||||
let chunk_queue: Vec<Chunk> = scenes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, (frame_start, frame_end))| {
|
||||
self.create_vs_chunk(index, vs_script, frame_start, frame_end)
|
||||
})
|
||||
.map(|(index, scene)| self.create_vs_chunk(index, vs_script, scene))
|
||||
.collect();
|
||||
|
||||
chunk_queue
|
||||
}
|
||||
|
||||
fn create_video_queue_select(&self, splits: Vec<usize>) -> Vec<Chunk> {
|
||||
let last_frame = self.frames;
|
||||
|
||||
fn create_video_queue_select(&self, scenes: &[Scene]) -> Vec<Chunk> {
|
||||
let input = self.input.as_video_path();
|
||||
|
||||
let chunk_boundaries = iter::once(0)
|
||||
.chain(splits)
|
||||
.chain(iter::once(last_frame))
|
||||
.tuple_windows();
|
||||
|
||||
let chunk_queue: Vec<Chunk> = chunk_boundaries
|
||||
let chunk_queue: Vec<Chunk> = scenes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, (frame_start, frame_end))| {
|
||||
self.create_select_chunk(index, input, frame_start, frame_end)
|
||||
.map(|(index, scene)| {
|
||||
self.create_select_chunk(
|
||||
index,
|
||||
input,
|
||||
scene.start_frame,
|
||||
scene.end_frame,
|
||||
scene.zone_overrides.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
chunk_queue
|
||||
}
|
||||
|
||||
fn create_video_queue_segment(&self, splits: &[usize]) -> anyhow::Result<Vec<Chunk>> {
|
||||
fn create_video_queue_segment(&self, scenes: &[Scene]) -> anyhow::Result<Vec<Chunk>> {
|
||||
let input = self.input.as_video_path();
|
||||
|
||||
debug!("Splitting video");
|
||||
segment(input, &self.temp, splits);
|
||||
segment(
|
||||
input,
|
||||
&self.temp,
|
||||
&scenes
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|scene| scene.start_frame)
|
||||
.collect::<Vec<usize>>(),
|
||||
);
|
||||
debug!("Splitting done");
|
||||
|
||||
let source_path = Path::new(&self.temp).join("split");
|
||||
|
@ -913,27 +896,26 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
let chunk_queue: Vec<Chunk> = queue_files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, file)| self.create_chunk_from_segment(index, file.as_path().to_str().unwrap()))
|
||||
.map(|(index, file)| {
|
||||
self.create_chunk_from_segment(
|
||||
index,
|
||||
file.as_path().to_str().unwrap(),
|
||||
scenes[index].zone_overrides.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(chunk_queue)
|
||||
}
|
||||
|
||||
fn create_video_queue_hybrid(&self, split_locations: Vec<usize>) -> anyhow::Result<Vec<Chunk>> {
|
||||
let mut splits = Vec::with_capacity(2 + split_locations.len());
|
||||
splits.push(0);
|
||||
splits.extend(split_locations);
|
||||
splits.push(self.frames);
|
||||
|
||||
fn create_video_queue_hybrid(&self, scenes: &[Scene]) -> anyhow::Result<Vec<Chunk>> {
|
||||
let input = self.input.as_video_path();
|
||||
|
||||
let keyframes = crate::ffmpeg::get_keyframes(input).unwrap();
|
||||
|
||||
let segments_set: Vec<(usize, usize)> = splits.iter().copied().tuple_windows().collect();
|
||||
|
||||
let to_split: Vec<usize> = keyframes
|
||||
.iter()
|
||||
.filter(|kf| splits.contains(kf))
|
||||
.filter(|kf| scenes.iter().any(|scene| scene.start_frame == **kf))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
|
@ -950,11 +932,13 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
.chain(iter::once(self.frames))
|
||||
.tuple_windows();
|
||||
|
||||
let mut segments = Vec::with_capacity(segments_set.len());
|
||||
let mut segments = Vec::with_capacity(scenes.len());
|
||||
for (file, (x, y)) in queue_files.iter().zip(kf_list) {
|
||||
for &(s0, s1) in &segments_set {
|
||||
for s in scenes {
|
||||
let s0 = s.start_frame;
|
||||
let s1 = s.end_frame;
|
||||
if s0 >= x && s1 <= y && s0 < s1 {
|
||||
segments.push((file.as_path(), (s0 - x, s1 - x)));
|
||||
segments.push((file.as_path(), (s0 - x, s1 - x, s)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -962,13 +946,20 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
let chunk_queue: Vec<Chunk> = segments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, &(file, (start, end)))| self.create_select_chunk(index, file, start, end))
|
||||
.map(|(index, &(file, (start, end, scene)))| {
|
||||
self.create_select_chunk(index, file, start, end, scene.zone_overrides.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(chunk_queue)
|
||||
}
|
||||
|
||||
fn create_chunk_from_segment(&self, index: usize, file: &str) -> Chunk {
|
||||
fn create_chunk_from_segment(
|
||||
&self,
|
||||
index: usize,
|
||||
file: &str,
|
||||
overrides: Option<ZoneOptions>,
|
||||
) -> Chunk {
|
||||
let ffmpeg_gen_cmd: Vec<OsString> = into_vec![
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
|
@ -994,12 +985,13 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
source: ffmpeg_gen_cmd,
|
||||
output_ext: output_ext.to_owned(),
|
||||
index,
|
||||
overrides,
|
||||
..Chunk::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns unfinished chunks and number of total chunks
|
||||
fn load_or_gen_chunk_queue(&mut self, splits: Vec<usize>) -> anyhow::Result<(Vec<Chunk>, usize)> {
|
||||
fn load_or_gen_chunk_queue(&mut self, splits: &[Scene]) -> anyhow::Result<(Vec<Chunk>, usize)> {
|
||||
if self.resume {
|
||||
let mut chunks = read_chunk_queue(self.temp.as_ref())?;
|
||||
let num_chunks = chunks.len();
|
||||
|
@ -1070,7 +1062,7 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
exit(0);
|
||||
}
|
||||
|
||||
let (chunk_queue, total_chunks) = self.load_or_gen_chunk_queue(splits)?;
|
||||
let (mut chunk_queue, total_chunks) = self.load_or_gen_chunk_queue(&splits)?;
|
||||
|
||||
if self.resume {
|
||||
let chunks_done = get_done().done.len();
|
||||
|
@ -1086,16 +1078,17 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
vspipe_cache.join().unwrap();
|
||||
}
|
||||
|
||||
let mut grain_table = None;
|
||||
if let Some(strength) = self.photon_noise {
|
||||
let grain_table = Path::new(&self.temp).join("grain.tbl");
|
||||
if !grain_table.exists() {
|
||||
let table = Path::new(&self.temp).join("grain.tbl");
|
||||
if !table.exists() {
|
||||
debug!(
|
||||
"Generating grain table at ISO {}",
|
||||
u32::from(strength) * 100
|
||||
);
|
||||
let (width, height) = self.input.resolution()?;
|
||||
let transfer = self.input.transfer_function()?;
|
||||
create_film_grain_file(&grain_table, strength, width, height, transfer)?;
|
||||
create_film_grain_file(&table, strength, width, height, transfer)?;
|
||||
} else {
|
||||
debug!("Using existing grain table");
|
||||
}
|
||||
|
@ -1104,10 +1097,40 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
self
|
||||
.video_params
|
||||
.retain(|param| !param.starts_with("--denoise-noise-level="));
|
||||
self.video_params.push(format!(
|
||||
"--film-grain-table={}",
|
||||
grain_table.to_str().unwrap()
|
||||
));
|
||||
self
|
||||
.video_params
|
||||
.push(format!("--film-grain-table={}", table.to_str().unwrap()));
|
||||
grain_table = Some(table);
|
||||
}
|
||||
|
||||
for chunk in &mut chunk_queue {
|
||||
// Also apply grain tables to zone overrides
|
||||
if let Some(strength) = chunk.overrides.as_ref().and_then(|ovr| ovr.photon_noise) {
|
||||
let grain_table = if Some(strength) == self.photon_noise {
|
||||
// We can reuse the existing photon noise table from the main encode
|
||||
grain_table.clone().unwrap()
|
||||
} else {
|
||||
let grain_table = Path::new(&self.temp).join(&format!("chunk{}-grain.tbl", chunk.index));
|
||||
debug!(
|
||||
"Generating grain table at ISO {}",
|
||||
u32::from(strength) * 100
|
||||
);
|
||||
let (width, height) = self.input.resolution()?;
|
||||
let transfer = self.input.transfer_function()?;
|
||||
create_film_grain_file(&grain_table, strength, width, height, transfer)?;
|
||||
grain_table
|
||||
};
|
||||
|
||||
// We should not use a grain table together with aom's grain generation
|
||||
let overrides = chunk.overrides.as_mut().unwrap();
|
||||
overrides
|
||||
.video_params
|
||||
.retain(|param| !param.starts_with("--denoise-noise-level="));
|
||||
overrides.video_params.push(format!(
|
||||
"--film-grain-table={}",
|
||||
grain_table.to_str().unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
crossbeam_utils::thread::scope(|s| -> anyhow::Result<()> {
|
||||
|
@ -1284,3 +1307,36 @@ properly into a mkv file. Specify mkvmerge as the concatenation method by settin
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn invalid_params<'a>(
|
||||
params: &'a [&'a str],
|
||||
valid_options: &'a HashSet<Cow<'a, str>>,
|
||||
) -> Vec<&'a str> {
|
||||
params
|
||||
.iter()
|
||||
.filter(|param| !valid_options.contains(Borrow::<str>::borrow(&**param)))
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn suggest_fix<'a>(
|
||||
wrong_arg: &str,
|
||||
arg_dictionary: &'a HashSet<Cow<'a, str>>,
|
||||
) -> Option<&'a str> {
|
||||
// Minimum threshold to consider a suggestion similar enough that it could be a typo
|
||||
const MIN_THRESHOLD: f64 = 0.75;
|
||||
|
||||
arg_dictionary
|
||||
.iter()
|
||||
.map(|arg| (arg, strsim::jaro_winkler(arg, wrong_arg)))
|
||||
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less))
|
||||
.and_then(|(suggestion, score)| {
|
||||
if score > MIN_THRESHOLD {
|
||||
Some(suggestion.borrow())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use crate::scenes::Scene;
|
||||
use anyhow::Context;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::prelude::*,
|
||||
io::BufReader,
|
||||
iter,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
string::ToString,
|
||||
|
@ -52,47 +51,58 @@ pub fn segment(input: impl AsRef<Path>, temp: impl AsRef<Path>, segments: &[usiz
|
|||
assert!(out.status.success(), "FFmpeg failed to segment: {:#?}", out);
|
||||
}
|
||||
|
||||
pub fn extra_splits(splits: &[usize], total_frames: usize, split_size: usize) -> Vec<usize> {
|
||||
let mut new_splits: Vec<usize> = splits.to_vec();
|
||||
pub fn extra_splits(scenes: &[Scene], total_frames: usize, split_size: usize) -> Vec<Scene> {
|
||||
let mut new_scenes: Vec<Scene> = Vec::with_capacity(scenes.len());
|
||||
|
||||
if let Some(&last_frame) = splits.last() {
|
||||
if let Some(scene) = scenes.last() {
|
||||
assert!(
|
||||
last_frame < total_frames,
|
||||
scene.end_frame <= total_frames,
|
||||
"scenecut reported at index {}, but there are only {} frames",
|
||||
last_frame,
|
||||
scene.end_frame,
|
||||
total_frames
|
||||
);
|
||||
}
|
||||
|
||||
let total_length = iter::once(0)
|
||||
.chain(splits.iter().copied())
|
||||
.chain(iter::once(total_frames))
|
||||
.tuple_windows();
|
||||
|
||||
for (x, y) in total_length {
|
||||
let distance = y - x;
|
||||
for scene in scenes.iter() {
|
||||
let distance = scene.end_frame - scene.start_frame;
|
||||
let split_size = scene
|
||||
.zone_overrides
|
||||
.as_ref()
|
||||
.map_or(split_size, |ovr| ovr.extra_splits_len.unwrap_or(usize::MAX));
|
||||
if distance > split_size {
|
||||
let additional_splits = (distance / split_size) + 1;
|
||||
for n in 1..additional_splits {
|
||||
let new_split = (distance as f64 * (n as f64 / additional_splits as f64)) as usize + x;
|
||||
new_splits.push(new_split);
|
||||
let new_split =
|
||||
(distance as f64 * (n as f64 / additional_splits as f64)) as usize + scene.start_frame;
|
||||
new_scenes.push(Scene {
|
||||
start_frame: new_scenes
|
||||
.last()
|
||||
.map_or(scene.start_frame, |scene| scene.end_frame),
|
||||
end_frame: new_split,
|
||||
..scene.clone()
|
||||
});
|
||||
}
|
||||
}
|
||||
new_scenes.push(Scene {
|
||||
start_frame: new_scenes
|
||||
.last()
|
||||
.map_or(scene.start_frame, |scene| scene.end_frame),
|
||||
end_frame: scene.end_frame,
|
||||
..scene.clone()
|
||||
});
|
||||
}
|
||||
|
||||
new_splits.sort_unstable();
|
||||
|
||||
new_splits
|
||||
new_scenes
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct ScenesData {
|
||||
scenes: Vec<usize>,
|
||||
scenes: Vec<Scene>,
|
||||
frames: usize,
|
||||
}
|
||||
|
||||
pub fn write_scenes_to_file(
|
||||
scenes: &[usize],
|
||||
scenes: &[Scene],
|
||||
total_frames: usize,
|
||||
scene_path: impl AsRef<Path>,
|
||||
) -> std::io::Result<()> {
|
||||
|
@ -112,7 +122,7 @@ pub fn write_scenes_to_file(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_scenes_from_file(scene_path: &Path) -> anyhow::Result<(Vec<usize>, usize)> {
|
||||
pub fn read_scenes_from_file(scene_path: &Path) -> anyhow::Result<(Vec<Scene>, usize)> {
|
||||
let file = File::open(scene_path)?;
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
|
@ -129,16 +139,32 @@ pub fn read_scenes_from_file(scene_path: &Path) -> anyhow::Result<(Vec<usize>, u
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{encoder::Encoder, into_vec, scenes::ZoneOptions};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extra_split_no_segments() {
|
||||
let total_frames = 300;
|
||||
let split_size = 240;
|
||||
let done = extra_splits(&[], total_frames, split_size);
|
||||
let expected_split_locations = vec![150];
|
||||
let done = extra_splits(
|
||||
&[Scene {
|
||||
start_frame: 0,
|
||||
end_frame: 300,
|
||||
zone_overrides: None,
|
||||
}],
|
||||
total_frames,
|
||||
split_size,
|
||||
);
|
||||
let expected_split_locations = vec![0usize, 150];
|
||||
|
||||
assert_eq!(expected_split_locations, done);
|
||||
assert_eq!(
|
||||
expected_split_locations,
|
||||
done
|
||||
.into_iter()
|
||||
.map(|done| done.start_frame)
|
||||
.collect::<Vec<usize>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -146,15 +172,178 @@ mod tests {
|
|||
let total_frames = 2000;
|
||||
let split_size = 130;
|
||||
let done = extra_splits(
|
||||
&[150, 460, 728, 822, 876, 890, 1100, 1399, 1709],
|
||||
&[
|
||||
Scene {
|
||||
start_frame: 0,
|
||||
end_frame: 150,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 150,
|
||||
end_frame: 460,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 460,
|
||||
end_frame: 728,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 728,
|
||||
end_frame: 822,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 822,
|
||||
end_frame: 876,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 876,
|
||||
end_frame: 890,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 890,
|
||||
end_frame: 1100,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 1100,
|
||||
end_frame: 1399,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 1399,
|
||||
end_frame: 1709,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 1709,
|
||||
end_frame: 2000,
|
||||
zone_overrides: None,
|
||||
},
|
||||
],
|
||||
total_frames,
|
||||
split_size,
|
||||
);
|
||||
let expected_split_locations = [
|
||||
75, 150, 253, 356, 460, 549, 638, 728, 822, 876, 890, 995, 1100, 1199, 1299, 1399, 1502,
|
||||
1605, 1709, 1806, 1903,
|
||||
0usize, 75, 150, 253, 356, 460, 549, 638, 728, 822, 876, 890, 995, 1100, 1199, 1299, 1399,
|
||||
1502, 1605, 1709, 1806, 1903,
|
||||
];
|
||||
|
||||
assert_eq!(&expected_split_locations, &*done);
|
||||
assert_eq!(
|
||||
expected_split_locations,
|
||||
done
|
||||
.into_iter()
|
||||
.map(|done| done.start_frame)
|
||||
.collect::<Vec<usize>>()
|
||||
.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extra_split_preserves_zone_overrides() {
|
||||
let total_frames = 2000;
|
||||
let split_size = 130;
|
||||
let done = extra_splits(
|
||||
&[
|
||||
Scene {
|
||||
start_frame: 0,
|
||||
end_frame: 150,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 150,
|
||||
end_frame: 460,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 460,
|
||||
end_frame: 728,
|
||||
zone_overrides: Some(ZoneOptions {
|
||||
encoder: Encoder::rav1e,
|
||||
passes: 1,
|
||||
extra_splits_len: Some(50),
|
||||
min_scene_len: 12,
|
||||
photon_noise: None,
|
||||
video_params: into_vec!["--speed", "8"],
|
||||
}),
|
||||
},
|
||||
Scene {
|
||||
start_frame: 728,
|
||||
end_frame: 822,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 822,
|
||||
end_frame: 876,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 876,
|
||||
end_frame: 890,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 890,
|
||||
end_frame: 1100,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 1100,
|
||||
end_frame: 1399,
|
||||
zone_overrides: None,
|
||||
},
|
||||
Scene {
|
||||
start_frame: 1399,
|
||||
end_frame: 1709,
|
||||
zone_overrides: Some(ZoneOptions {
|
||||
encoder: Encoder::rav1e,
|
||||
passes: 1,
|
||||
extra_splits_len: Some(split_size),
|
||||
min_scene_len: 12,
|
||||
photon_noise: None,
|
||||
video_params: into_vec!["--speed", "3"],
|
||||
}),
|
||||
},
|
||||
Scene {
|
||||
start_frame: 1709,
|
||||
end_frame: 2000,
|
||||
zone_overrides: None,
|
||||
},
|
||||
],
|
||||
total_frames,
|
||||
split_size,
|
||||
);
|
||||
let expected_split_locations = [
|
||||
0, 75, 150, 253, 356, 460, 504, 549, 594, 638, 683, 728, 822, 876, 890, 995, 1100, 1199,
|
||||
1299, 1399, 1502, 1605, 1709, 1806, 1903,
|
||||
];
|
||||
|
||||
for (i, scene) in done.into_iter().enumerate() {
|
||||
assert_eq!(scene.start_frame, expected_split_locations[i]);
|
||||
match scene.start_frame {
|
||||
460..=727 => {
|
||||
assert!(scene.zone_overrides.is_some());
|
||||
let overrides = scene.zone_overrides.unwrap();
|
||||
assert_eq!(
|
||||
overrides.video_params,
|
||||
vec!["--speed".to_owned(), "8".to_owned()]
|
||||
);
|
||||
}
|
||||
1399..=1708 => {
|
||||
assert!(scene.zone_overrides.is_some());
|
||||
let overrides = scene.zone_overrides.unwrap();
|
||||
assert_eq!(
|
||||
overrides.video_params,
|
||||
vec!["--speed".to_owned(), "3".to_owned()]
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
assert!(scene.zone_overrides.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue