Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 71 additions & 41 deletions src/converter/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,62 @@ impl Conversion {
default.to_string()
}

// workarounds for NVENC for "weirder" videos
async fn nvenc_args(
&self,
gpu: &ConverterGPU,
resolution: (u32, u32),
fps: u32,
job: &super::job::Job,
) -> anyhow::Result<Vec<String>> {
let (width, height) = resolution;
let is_4k = width == 3840 || height == 2160;
let is_above_4k = width > 3840 || height > 2160;
let pix_fmt = job.pix_fmt().await?;

// choose codec
// prefer original codec, force h265 if original is h264 and (10bit or 4k)
let codecs = job.codecs().await?;
let has_h265 = codecs.0.to_lowercase().contains("hevc");
let has_h264 = codecs.0.to_lowercase().contains("h264");
let is_10bit = pix_fmt.contains("10le") || pix_fmt.contains("10be");
let (codec_order, default) = if has_h265 || (has_h264 && (is_10bit || is_4k || is_above_4k))
{
(&["hevc"][..], "libx265")
} else if has_h264 {
(&["h264"][..], "libx264")
} else {
(&["h264"][..], "libx264")
};

// do we still really need to check for codec support? this function is only called if gpu is nvidia
let encoder = self
.accelerated_or_default_codec(gpu, codec_order, default)
.await;

let mut args = vec!["-c:v".to_string(), encoder.clone()];

// convert to 8 bit if 10 bit on h264_nvenc
if is_10bit && encoder == "h264_nvenc" {
args.extend(["-pix_fmt".to_string(), "yuv420p".to_string()]);
}

if fps > 240 {
args.extend(["-r".to_string(), "240".to_string()]);
}

// scale to 160:-1 if width is less than 160
if width < 160 {
args.extend(["-vf".to_string(), "scale=160:-1".to_string()]);
}
Ok(args)
}

pub async fn to_args(
&self,
speed: &ConversionSpeed,
gpu: &ConverterGPU,
resolution: (u32, u32),
bitrate: u64,
fps: u32,
job: &super::job::Job,
Expand All @@ -93,52 +145,30 @@ impl Conversion {
| ConverterFormat::ThreeGP
| ConverterFormat::ThreeG2
| ConverterFormat::H264 => {
let encoder = self
.accelerated_or_default_codec(gpu, &["h264"], "libx264")
.await;

let mut args = vec!["-c:v".to_string(), encoder.clone()];

let (width, height) = job.resolution().await?;
let is_4k = width >= 3840 || height >= 2160;
let pix_fmt = job.pix_fmt().await?;

// convert to 8bit if 10bit (h264_nvenc does not support 10bit)
// could probably use h265 instead?
let is_10bit = pix_fmt.contains("10le") || pix_fmt.contains("10be");
if is_10bit {
args.extend(["-pix_fmt".to_string(), "yuv420p".to_string()]);
if matches!(gpu, ConverterGPU::NVIDIA) {
self.nvenc_args(gpu, resolution, fps, job).await?
} else {
let encoder = self
.accelerated_or_default_codec(gpu, &["h264"], "libx264")
.await;
vec![
"-c:v".to_string(),
encoder,
"-c:a".to_string(),
"aac".to_string(),
"-strict".to_string(),
"experimental".to_string(),
]
}

if is_4k {
args.extend(["-level:v".to_string(), "5.2".to_string()]);
if fps > 120 {
args.extend(["-r".to_string(), "120".to_string()]);
}
}

// scale to 160:-1 if width is less than 160
if width < 160 {
args.extend(["-vf".to_string(), "scale=160:-1".to_string()]);
}

args.extend([
"-c:a".to_string(),
"aac".to_string(),
"-strict".to_string(),
"experimental".to_string(),
]);

args
}

ConverterFormat::GIF => {
vec![
"-filter_complex".to_string(),
format!(
"fps={},scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=64[p];[s1][p]paletteuse=dither=bayer",
fps.min(24)
)
"-filter_complex".to_string(),
format!(
"fps={},scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=64[p];[s1][p]paletteuse=dither=bayer",
fps.min(24)
)
]
}

Expand Down
113 changes: 74 additions & 39 deletions src/converter/job.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use log::warn;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use uuid::Uuid;

const DEFAULT_BITRATE: u64 = 4 * 1_000_000;
const BITRATE_MULTIPLIER: f64 = 2.5;

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Job {
Expand Down Expand Up @@ -51,9 +49,7 @@ impl Job {
self.state == JobState::Processing
}

// TODO: scale based on resolution
pub async fn bitrate(&mut self) -> anyhow::Result<u64> {
// Ok(DEFAULT_BITRATE)
if let Some(bitrate) = self.bitrate {
return Ok(bitrate);
}
Expand All @@ -73,14 +69,24 @@ impl Job {
.output()
.await?;

let bitrate = String::from_utf8(output.stdout)?;
let bitrate = match bitrate.trim().parse::<u64>() {
Ok(bitrate) => bitrate,
Err(_) => DEFAULT_BITRATE,
// use detected bitrate
let bitrate = String::from_utf8(output.stdout)?.trim().parse::<u64>().ok();
if let Some(bitrate_value) = bitrate {
return Ok(bitrate_value);
}

// else check resolution and use default bitrate (based on resolution)
let (width, height) = self.resolution().await?;
let default_bitrate = match (width, height) {
(w, h) if w >= 3840 || h >= 2160 => 30_000_000, // 4K - 30 Mbps
(w, h) if w >= 2560 || h >= 1440 => 14_000_000, // 2K - 14 Mbps
(w, h) if w >= 1920 || h >= 1080 => 7_000_000, // 1080p - 7 Mbps
(w, h) if w >= 1280 || h >= 720 => 4_000_000, // 720p - 4 Mbps
_ => 1_500_000, // SD - 1.5 Mbps
};

self.bitrate = Some(bitrate);
Ok(((bitrate as f64) * BITRATE_MULTIPLIER) as u64)
self.bitrate = Some(default_bitrate);
Ok(default_bitrate)
}

pub async fn total_frames(&mut self) -> anyhow::Result<u64> {
Expand Down Expand Up @@ -125,6 +131,8 @@ impl Job {
return Ok(fps);
}

let path = format!("input/{}.{}", self.id, self.from);

let output = Command::new("ffprobe")
.args([
"-v",
Expand All @@ -135,32 +143,45 @@ impl Job {
"stream=r_frame_rate",
"-of",
"default=nokey=1:noprint_wrappers=1",
&format!("input/{}.{}", self.id, self.from),
&path,
])
.output()
.await?;

// its gonna look like "30000/1001"
let fps = String::from_utf8(output.stdout)
.map_err(|e| anyhow::anyhow!("failed to parse fps: {}", e))?;

let fps = fps.trim().split('/').collect::<Vec<&str>>();
let fps = if fps.len() == 1 {
fps[0].parse::<u32>()?
} else if fps.len() == 2 {
let numerator = fps[0].parse::<u32>()?;
let denominator = fps[1].parse::<u32>()?;
(numerator as f64 / denominator as f64).round() as u32
} else if fps.len() == 3 {
let numerator = fps[0].parse::<u32>()?;
let denominator = fps[2].parse::<u32>()?;
(numerator as f64 / denominator as f64).round() as u32
let fps_out = String::from_utf8(output.stdout)?;
let fps_trim = fps_out
.lines()
.find(|l| !l.trim().is_empty())
.map(|s| s.trim())
.unwrap_or("");

if fps_trim.is_empty() {
warn!("ffprobe returned empty fps for {}", path);
let default = 30u32;
self.fps = Some(default);
return Ok(default);
}

// parse fps which could be in the form of "30", "29.97", or "30000/1001"
let parsed = if let Some((n_str, d_str)) = fps_trim.split_once('/') {
match (n_str.trim().parse::<f64>(), d_str.trim().parse::<f64>()) {
(Ok(n), Ok(d)) if d != 0.0 => Some((n / d).round() as u32),
_ => None,
}
} else {
return Err(anyhow::anyhow!("failed to parse fps"));
fps_trim.parse::<f64>().ok().map(|f| f.round() as u32)
};

self.fps = Some(fps);
Ok(fps)
let result = parsed.unwrap_or_else(|| {
warn!(
"failed to parse fps '{}' from ffprobe for {}",
fps_trim, path
);
30u32
});

self.fps = Some(result);
Ok(result)
}

pub async fn bitrate_and_fps(&mut self) -> anyhow::Result<(u64, u32)> {
Expand All @@ -186,15 +207,28 @@ impl Job {
.output()
.await?;

let res_str = String::from_utf8(output.stdout)?;
let mut parts = res_str.trim().split('x');
let res_out = String::from_utf8(output.stdout)?;
let res_str = res_out
.lines()
.find(|l| !l.trim().is_empty())
.map(|s| s.trim())
.ok_or_else(|| {
anyhow::anyhow!("failed to get resolution from ffprobe output: {}", res_out)
})?;
let mut parts = res_str.split('x');
let width = parts
.next()
.ok_or_else(|| anyhow::anyhow!("failed to get width"))?
.ok_or_else(|| {
anyhow::anyhow!("failed to get width from ffprobe output: '{}'", res_str)
})?
.trim()
.parse::<u32>()?;
let height = parts
.next()
.ok_or_else(|| anyhow::anyhow!("failed to get height"))?
.ok_or_else(|| {
anyhow::anyhow!("failed to get height from ffprobe output: '{}'", res_str)
})?
.trim()
.parse::<u32>()?;

Ok((width, height))
Expand All @@ -218,13 +252,14 @@ impl Job {
.output()
.await?;

let pix_fmt = String::from_utf8(output.stdout)?
let pix_out = String::from_utf8(output.stdout)?;
let pix = pix_out
.lines()
.next()
.ok_or_else(|| anyhow::anyhow!("failed to get pixel format"))?
.to_string();
.find(|l| !l.trim().is_empty())
.map(|s| s.trim().to_string())
.ok_or_else(|| anyhow::anyhow!("failed to get pixel format from ffprobe output"))?;

Ok(pix_fmt)
Ok(pix)
}

pub async fn codecs(&self) -> anyhow::Result<(String, String)> {
Expand Down
20 changes: 19 additions & 1 deletion src/converter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ impl Converter {
// let fps = job.fps().await?;
// the above but we run in parallel
let (bitrate, fps) = job.bitrate_and_fps().await?;
let (width, height) = job.resolution().await?;
let args = self
.conversion
.to_args(&self.speed, gpu, bitrate, fps, job)
.to_args(&self.speed, gpu, (width, height), bitrate, fps, job)
.await?;
let args = args.iter().map(|s| s.as_str()).collect::<Vec<&str>>();
let args = args.as_slice();
Expand All @@ -80,6 +81,23 @@ impl Converter {
.map(|s| s.to_string())
.collect::<Vec<String>>();

// if video is more than 4k on nvenc, remove -hwaccel cuda to avoid "Video width 7680 not within range from 48 to 4096"
// error from the h264_nvenc *decoder*, guh
let command = if matches!(gpu, gpu::ConverterGPU::NVIDIA) {
let (width, height) = job.resolution().await?;
if width > 3840 || height > 2160 {
command
.iter()
.filter(|s| *s != "-hwaccel" && *s != "cuda")
.cloned()
.collect()
} else {
command
}
} else {
command
};

info!("running 'ffmpeg {}'", command.join(" "));

let mut process = Command::new("ffmpeg")
Expand Down
1 change: 0 additions & 1 deletion src/converter/speed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ impl ConversionSpeed {

if *to != ConverterFormat::GIF {
args.push("-b:v".to_string());
let bitrate = (bitrate as f64 * self.to_bitrate_mul()) as u64;
args.push(bitrate.to_string());
}

Expand Down
20 changes: 0 additions & 20 deletions src/job/compression.rs

This file was deleted.

Loading
Loading