Merge pull request #121 from crunchy-labs/feature/more-ffmpeg-options

More ffmpeg options
This commit is contained in:
ByteDream 2023-01-27 21:20:55 +01:00 committed by GitHub
commit 43e32e1453
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 363 additions and 123 deletions

7
Cargo.lock generated
View file

@ -315,6 +315,7 @@ dependencies = [
"sanitize-filename",
"serde",
"serde_json",
"shlex",
"signal-hook",
"sys-locale",
"tempfile",
@ -1476,6 +1477,12 @@ dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "signal-hook"
version = "0.3.14"

View file

@ -21,6 +21,7 @@ regex = "1.7"
sanitize-filename = "0.4"
serde = "1.0"
serde_json = "1.0"
shlex = "1.1"
signal-hook = "0.3"
tempfile = "3.3"
terminal_size = "0.2"

View file

@ -15,7 +15,7 @@ use crate::Execute;
use anyhow::{bail, Result};
use crunchyroll_rs::media::Resolution;
use crunchyroll_rs::{Locale, Media, MediaCollection, Series};
use log::{debug, error, info, warn};
use log::{debug, error, info};
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process::{Command, Stdio};
@ -97,14 +97,14 @@ pub struct Archive {
merge: MergeBehavior,
#[arg(help = format!("Presets for video converting. Can be used multiple times. \
Available presets: \n {}", FFmpegPreset::all().into_iter().map(|p| format!("{}: {}", p.to_string(), p.description())).collect::<Vec<String>>().join("\n ")))]
Available presets: \n {}", FFmpegPreset::available_matches_human_readable().join("\n ")))]
#[arg(long_help = format!("Presets for video converting. Can be used multiple times. \
Generally used to minify the file size with keeping (nearly) the same quality. \
It is recommended to only use this if you archive videos with high resolutions since low resolution videos tend to result in a larger file with any of the provided presets. \
Available presets: \n {}", FFmpegPreset::all().into_iter().map(|p| format!("{}: {}", p.to_string(), p.description())).collect::<Vec<String>>().join("\n ")))]
Available presets: \n {}", FFmpegPreset::available_matches_human_readable().join("\n ")))]
#[arg(long)]
#[arg(value_parser = FFmpegPreset::parse)]
ffmpeg_preset: Vec<FFmpegPreset>,
ffmpeg_preset: Option<FFmpegPreset>,
#[arg(
help = "Set which subtitle language should be set as default / auto shown when starting a video"
@ -138,12 +138,6 @@ impl Execute for Archive {
{
bail!("File extension is not '.mkv'. Currently only matroska / '.mkv' files are supported")
}
let _ = FFmpegPreset::ffmpeg_presets(self.ffmpeg_preset.clone())?;
if self.ffmpeg_preset.len() == 1
&& self.ffmpeg_preset.get(0).unwrap() == &FFmpegPreset::Nvidia
{
warn!("Skipping 'nvidia' hardware acceleration preset since no other codec preset was specified")
}
self.locale = all_locale_in_locales(self.locale.clone());
self.subtitle = all_locale_in_locales(self.subtitle.clone());
@ -361,7 +355,11 @@ async fn formats_from_series(
"Season {} of series {} is not available with {} audio",
season.first().unwrap().metadata.season_number,
series.title,
not_present_audio.into_iter().map(|l| l.to_string()).collect::<Vec<String>>().join(", ")
not_present_audio
.into_iter()
.map(|l| l.to_string())
.collect::<Vec<String>>()
.join(", ")
)
}
@ -546,8 +544,19 @@ fn generate_mkv(
}
}
let (input_presets, output_presets) =
FFmpegPreset::ffmpeg_presets(archive.ffmpeg_preset.clone())?;
let (input_presets, output_presets) = if let Some(preset) = archive.ffmpeg_preset.clone() {
preset.to_input_output_args()
} else {
(
vec![],
vec![
"-c:v".to_string(),
"copy".to_string(),
"-c:a".to_string(),
"copy".to_string(),
],
)
};
let mut command_args = vec!["-y".to_string()];
command_args.extend(input_presets);

View file

@ -1,7 +1,7 @@
use crate::cli::log::tab_info;
use crate::cli::utils::{
download_segments, find_multiple_seasons_with_same_number,
find_resolution, interactive_season_choosing, FFmpegPreset,
download_segments, find_multiple_seasons_with_same_number, find_resolution,
interactive_season_choosing, FFmpegPreset,
};
use crate::utils::context::Context;
use crate::utils::format::Format;
@ -66,14 +66,14 @@ pub struct Download {
resolution: Resolution,
#[arg(help = format!("Presets for video converting. Can be used multiple times. \
Available presets: \n {}", FFmpegPreset::all().into_iter().map(|p| format!("{}: {}", p.to_string(), p.description())).collect::<Vec<String>>().join("\n ")))]
Available presets: \n {}", FFmpegPreset::available_matches_human_readable().join("\n ")))]
#[arg(long_help = format!("Presets for video converting. Can be used multiple times. \
Generally used to minify the file size with keeping (nearly) the same quality. \
It is recommended to only use this if you download videos with high resolutions since low resolution videos tend to result in a larger file with any of the provided presets. \
Available presets: \n {}", FFmpegPreset::all().into_iter().map(|p| format!("{}: {}", p.to_string(), p.description())).collect::<Vec<String>>().join("\n ")))]
Available presets: \n {}", FFmpegPreset::available_matches_human_readable().join("\n ")))]
#[arg(long)]
#[arg(value_parser = FFmpegPreset::parse)]
ffmpeg_preset: Vec<FFmpegPreset>,
ffmpeg_preset: Option<FFmpegPreset>,
#[arg(help = "Skip files which are already existing")]
#[arg(long, default_value_t = false)]
@ -109,13 +109,6 @@ impl Execute for Download {
}
}
let _ = FFmpegPreset::ffmpeg_presets(self.ffmpeg_preset.clone())?;
if self.ffmpeg_preset.len() == 1
&& self.ffmpeg_preset.get(0).unwrap() == &FFmpegPreset::Nvidia
{
warn!("Skipping 'nvidia' hardware acceleration preset since no other codec preset was specified")
}
Ok(())
}
@ -277,8 +270,19 @@ async fn download_ffmpeg(
subtitle: Option<StreamSubtitle>,
mut target: PathBuf,
) -> Result<()> {
let (input_presets, mut output_presets) =
FFmpegPreset::ffmpeg_presets(download.ffmpeg_preset.clone())?;
let (input_presets, mut output_presets) = if let Some(preset) = download.ffmpeg_preset.clone() {
preset.to_input_output_args()
} else {
(
vec![],
vec![
"-c:v".to_string(),
"copy".to_string(),
"-c:a".to_string(),
"copy".to_string(),
],
)
};
// create parent directory if it does not exist
if let Some(parent) = target.parent() {

View file

@ -8,7 +8,9 @@ use log::{debug, LevelFilter};
use regex::Regex;
use std::borrow::{Borrow, BorrowMut};
use std::collections::BTreeMap;
use std::env;
use std::io::{BufRead, Write};
use std::str::FromStr;
use std::sync::{mpsc, Arc, Mutex};
use std::time::Duration;
use tokio::task::JoinSet;
@ -210,118 +212,335 @@ pub async fn download_segments(
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FFmpegPreset {
Nvidia,
Av1,
H265,
H264,
Predefined(FFmpegCodec, Option<FFmpegHwAccel>, FFmpegQuality),
Custom(Option<String>, Option<String>),
}
impl ToString for FFmpegPreset {
lazy_static! {
static ref PREDEFINED_PRESET: Regex = Regex::new(r"^\w+(-\w+)*?$").unwrap();
}
macro_rules! FFmpegEnum {
(enum $name:ident { $($field:ident),* }) => {
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum $name {
$(
$field
),*,
}
impl $name {
fn all() -> Vec<$name> {
vec![
$(
$name::$field
),*,
]
}
}
impl ToString for $name {
fn to_string(&self) -> String {
match self {
&FFmpegPreset::Nvidia => "nvidia",
&FFmpegPreset::Av1 => "av1",
&FFmpegPreset::H265 => "h265",
&FFmpegPreset::H264 => "h264",
$(
&$name::$field => stringify!($field).to_string().to_lowercase()
),*
}
.to_string()
}
}
impl FromStr for $name {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
$(
stringify!($field) => Ok($name::$field)
),*,
_ => bail!("{} is not a valid {}", s, stringify!($name).to_lowercase())
}
}
}
}
}
FFmpegEnum! {
enum FFmpegCodec {
H264,
H265,
Av1
}
}
FFmpegEnum! {
enum FFmpegHwAccel {
Nvidia
}
}
FFmpegEnum! {
enum FFmpegQuality {
Lossless,
Normal,
Low
}
}
impl FFmpegPreset {
pub(crate) fn all() -> Vec<FFmpegPreset> {
vec![
FFmpegPreset::Nvidia,
FFmpegPreset::Av1,
FFmpegPreset::H265,
FFmpegPreset::H264,
]
pub(crate) fn available_matches(
) -> Vec<(FFmpegCodec, Option<FFmpegHwAccel>, Option<FFmpegQuality>)> {
let codecs = vec![
(
FFmpegCodec::H264,
FFmpegHwAccel::all(),
FFmpegQuality::all(),
),
(
FFmpegCodec::H265,
FFmpegHwAccel::all(),
FFmpegQuality::all(),
),
(FFmpegCodec::Av1, vec![], FFmpegQuality::all()),
];
let mut return_values = vec![];
for (codec, hwaccels, qualities) in codecs {
return_values.push((codec.clone(), None, None));
for hwaccel in hwaccels.clone() {
return_values.push((codec.clone(), Some(hwaccel), None));
}
for quality in qualities.clone() {
return_values.push((codec.clone(), None, Some(quality)))
}
for hwaccel in hwaccels {
for quality in qualities.clone() {
return_values.push((codec.clone(), Some(hwaccel.clone()), Some(quality)))
}
}
}
pub(crate) fn description(self) -> String {
match self {
FFmpegPreset::Nvidia => "If you're have a nvidia card, use hardware / gpu accelerated video processing if available",
FFmpegPreset::Av1 => "Encode the video(s) with the av1 codec. Hardware acceleration is currently not possible with this",
FFmpegPreset::H265 => "Encode the video(s) with the h265 codec",
FFmpegPreset::H264 => "Encode the video(s) with the h264 codec"
}.to_string()
return_values
}
pub(crate) fn available_matches_human_readable() -> Vec<String> {
let mut return_values = vec![];
for (codec, hwaccel, quality) in FFmpegPreset::available_matches() {
let mut description_details = vec![];
if let Some(h) = &hwaccel {
description_details.push(format!("{} hardware acceleration", h.to_string()))
}
if let Some(q) = &quality {
description_details.push(format!("{} video quality/compression", q.to_string()))
}
let description = if description_details.len() == 0 {
format!(
"{} encoded with default video quality/compression",
codec.to_string()
)
} else if description_details.len() == 1 {
format!(
"{} encoded with {}",
codec.to_string(),
description_details[0]
)
} else {
let first = description_details.remove(0);
let last = description_details.remove(description_details.len() - 1);
let mid = if !description_details.is_empty() {
format!(", {} ", description_details.join(", "))
} else {
"".to_string()
};
format!(
"{} encoded with {}{} and {}",
codec.to_string(),
first,
mid,
last
)
};
return_values.push(format!(
"{} ({})",
vec![
Some(codec.to_string()),
hwaccel.map(|h| h.to_string()),
quality.map(|q| q.to_string())
]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("-"),
description
))
}
return_values
}
pub(crate) fn parse(s: &str) -> Result<FFmpegPreset, String> {
Ok(match s.to_lowercase().as_str() {
"nvidia" => FFmpegPreset::Nvidia,
"av1" => FFmpegPreset::Av1,
"h265" | "h.265" | "hevc" => FFmpegPreset::H265,
"h264" | "h.264" => FFmpegPreset::H264,
_ => return Err(format!("'{}' is not a valid ffmpeg preset", s)),
})
let env_ffmpeg_input_args = env::var("FFMPEG_INPUT_ARGS").ok();
let env_ffmpeg_output_args = env::var("FFMPEG_OUTPUT_ARGS").ok();
if env_ffmpeg_input_args.is_some() || env_ffmpeg_output_args.is_some() {
if let Some(input) = &env_ffmpeg_input_args {
if shlex::split(input).is_none() {
return Err(format!("Failed to parse custom ffmpeg input '{}' (`FFMPEG_INPUT_ARGS` env variable)", input));
}
}
if let Some(output) = &env_ffmpeg_output_args {
if shlex::split(output).is_none() {
return Err(format!("Failed to parse custom ffmpeg output '{}' (`FFMPEG_INPUT_ARGS` env variable)", output));
}
}
pub(crate) fn ffmpeg_presets(
mut presets: Vec<FFmpegPreset>,
) -> Result<(Vec<String>, Vec<String>)> {
fn preset_check_remove(presets: &mut Vec<FFmpegPreset>, preset: FFmpegPreset) -> bool {
if let Some(i) = presets.iter().position(|p| p == &preset) {
presets.remove(i);
true
return Ok(FFmpegPreset::Custom(
env_ffmpeg_input_args,
env_ffmpeg_output_args,
));
} else if !PREDEFINED_PRESET.is_match(s) {
return Ok(FFmpegPreset::Custom(None, Some(s.to_string())));
}
let mut codec: Option<FFmpegCodec> = None;
let mut hwaccel: Option<FFmpegHwAccel> = None;
let mut quality: Option<FFmpegQuality> = None;
for token in s.split('-') {
if let Some(c) = FFmpegCodec::all()
.into_iter()
.find(|p| p.to_string() == token.to_lowercase())
{
if let Some(cc) = codec {
return Err(format!(
"cannot use multiple codecs (found {} and {})",
cc.to_string(),
c.to_string()
));
}
codec = Some(c)
} else if let Some(h) = FFmpegHwAccel::all()
.into_iter()
.find(|p| p.to_string() == token.to_lowercase())
{
if let Some(hh) = hwaccel {
return Err(format!(
"cannot use multiple hardware accelerations (found {} and {})",
hh.to_string(),
h.to_string()
));
}
hwaccel = Some(h)
} else if let Some(q) = FFmpegQuality::all()
.into_iter()
.find(|p| p.to_string() == token.to_lowercase())
{
if let Some(qq) = quality {
return Err(format!(
"cannot use multiple ffmpeg preset qualities (found {} and {})",
qq.to_string(),
q.to_string()
));
}
quality = Some(q)
} else {
false
return Err(format!(
"'{}' is not a valid ffmpeg preset (unknown token '{}'",
s, token
));
}
}
let nvidia = preset_check_remove(&mut presets, FFmpegPreset::Nvidia);
if presets.len() > 1 {
bail!(
"Can only use one video codec, {} found: {}",
presets.len(),
presets
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(", ")
if let Some(c) = codec {
if !FFmpegPreset::available_matches().contains(&(
c.clone(),
hwaccel.clone(),
quality.clone(),
)) {
return Err(format!("ffmpeg preset is not supported"));
}
Ok(FFmpegPreset::Predefined(
c,
hwaccel,
quality.unwrap_or(FFmpegQuality::Normal),
))
} else {
Err(format!("cannot use ffmpeg preset with without a codec"))
}
}
pub(crate) fn to_input_output_args(self) -> (Vec<String>, Vec<String>) {
match self {
FFmpegPreset::Custom(input, output) => (
input.map_or(vec![], |i| shlex::split(&i).unwrap_or_default()),
output.map_or(vec![], |o| shlex::split(&o).unwrap_or_default()),
),
FFmpegPreset::Predefined(codec, hwaccel_opt, quality) => {
let mut input = vec![];
let mut output = vec![];
match codec {
FFmpegCodec::H264 => {
if let Some(hwaccel) = hwaccel_opt {
match hwaccel {
FFmpegHwAccel::Nvidia => {
input.extend(["-hwaccel", "cuvid", "-c:v", "h264_cuvid"]);
output.extend(["-c:v", "h264_nvenc", "-c:a", "copy"])
}
}
} else {
output.extend(["-c:v", "libx264", "-c:a", "copy"])
}
match quality {
FFmpegQuality::Lossless => output.extend(["-crf", "18"]),
FFmpegQuality::Normal => (),
FFmpegQuality::Low => output.extend(["-crf", "35"]),
}
}
FFmpegCodec::H265 => {
if let Some(hwaccel) = hwaccel_opt {
match hwaccel {
FFmpegHwAccel::Nvidia => {
input.extend(["-hwaccel", "cuvid", "-c:v", "h264_cuvid"]);
output.extend(["-c:v", "hevc_nvenc", "-c:a", "copy"])
}
}
} else {
output.extend(["-c:v", "libx265", "-c:a", "copy"])
}
match quality {
FFmpegQuality::Lossless => output.extend(["-crf", "20"]),
FFmpegQuality::Normal => (),
FFmpegQuality::Low => output.extend(["-crf", "35"]),
}
}
FFmpegCodec::Av1 => {
output.extend(["-c:v", "libsvtav1", "-c:a", "copy"]);
match quality {
FFmpegQuality::Lossless => output.extend(["-crf", "22"]),
FFmpegQuality::Normal => (),
FFmpegQuality::Low => output.extend(["-crf", "35"]),
}
}
}
(
input
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>(),
output
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>(),
)
}
let (mut input, mut output) = (vec![], vec![]);
for preset in presets {
if nvidia {
match preset {
FFmpegPreset::Av1 => bail!("'nvidia' hardware acceleration preset is not available in combination with the 'av1' codec preset"),
FFmpegPreset::H265 => {
input.extend(["-hwaccel", "cuvid", "-c:v", "h264_cuvid"]);
output.extend(["-c:v", "hevc_nvenc", "-c:a", "copy"]);
}
FFmpegPreset::H264 => {
input.extend(["-hwaccel", "cuvid", "-c:v", "h264_cuvid"]);
output.extend(["-c:v", "h264_nvenc", "-c:a", "copy"]);
}
_ => ()
}
} else {
match preset {
FFmpegPreset::Av1 => {
output.extend(["-c:v", "libaom-av1", "-c:a", "copy"]);
}
FFmpegPreset::H265 => {
output.extend(["-c:v", "libx265", "-c:a", "copy"]);
}
FFmpegPreset::H264 => {
output.extend(["-c:v", "libx264", "-c:a", "copy"]);
}
_ => (),
}
}
}
if output.is_empty() {
output.extend(["-c:v", "copy", "-c:a", "copy"])
}
Ok((
input.into_iter().map(|i| i.to_string()).collect(),
output.into_iter().map(|o| o.to_string()).collect(),
))
}
}