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:
Josh Holmer 2022-03-28 11:23:08 -04:00 committed by GitHub
parent d9d15a90d0
commit 84d46bb40c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1012 additions and 193 deletions

3
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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()?;

View file

@ -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"

View file

@ -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()
{

View file

@ -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());
}

View file

@ -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 {

View file

@ -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;

View file

@ -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
View 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());
}

View file

@ -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
}
})
}

View file

@ -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());
}
}
}
}
}