From b9d8e20b6bff6b51f73106b688bd6e5d98e08215 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 21 Apr 2025 20:36:34 +0200 Subject: new transcoding args system --- common/src/config.rs | 35 +++++----- common/src/stream.rs | 2 +- remuxer/src/fragment.rs | 2 +- remuxer/src/matroska_to_mpeg4.rs | 2 +- remuxer/src/metadata.rs | 2 + stream/src/fragment.rs | 9 +++ stream/src/stream_info.rs | 14 ++-- transcoder/src/fragment.rs | 140 ++++++++++++++++++++++++--------------- 8 files changed, 124 insertions(+), 82 deletions(-) diff --git a/common/src/config.rs b/common/src/config.rs index 522fa0f..4ec43eb 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -29,28 +29,25 @@ pub struct GlobalConfig { #[serde(default)] pub default_permission_set: PermissionSet, #[serde(default)] - pub encoders: EncoderArgs, + pub transcoder: TranscoderConfig, } +#[rustfmt::skip] #[derive(Debug, Deserialize, Serialize, Default)] -pub struct EncoderArgs { - pub avc: Option, - pub hevc: Option, - pub vp8: Option, - pub vp9: Option, - pub av1: Option, - pub generic: Option, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Copy)] -#[serde(rename_all = "snake_case")] -pub enum EncoderClass { - Aom, - Svt, - X26n, - Vpx, - Vaapi, - Rkmpp, +pub struct TranscoderConfig { + #[serde(default)] pub offer_avc: bool, + #[serde(default)] pub offer_hevc: bool, + #[serde(default)] pub offer_vp8: bool, + #[serde(default)] pub offer_vp9: bool, + #[serde(default)] pub offer_av1: bool, + #[serde(default)] pub enable_rkmpp: bool, + #[serde(default)] pub enable_rkrga: bool, + #[serde(default)] pub use_svtav1: bool, + #[serde(default)] pub use_rav1e: bool, + pub svtav1_preset: Option, // 0..=13, high is fast + pub rav1e_preset: Option, // 0..=10 + pub aom_preset: Option, // 0..=8, high is fast + pub x264_preset: Option, } #[derive(Serialize, Deserialize, Debug, Default)] diff --git a/common/src/stream.rs b/common/src/stream.rs index 79cb380..81dd298 100644 --- a/common/src/stream.rs +++ b/common/src/stream.rs @@ -82,7 +82,7 @@ pub struct StreamTrackInfo { pub formats: Vec, } -#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum TrackKind { Video, diff --git a/remuxer/src/fragment.rs b/remuxer/src/fragment.rs index 439c700..45a671f 100644 --- a/remuxer/src/fragment.rs +++ b/remuxer/src/fragment.rs @@ -20,7 +20,7 @@ use std::{ path::Path, }; -const FRAGMENT_LENGTH: f64 = 2.; +const FRAGMENT_LENGTH: f64 = 4.; pub fn fragment_index(path: &Path, track: u64) -> Result>> { let meta = matroska_metadata(path)?; diff --git a/remuxer/src/matroska_to_mpeg4.rs b/remuxer/src/matroska_to_mpeg4.rs index e8268e7..ab7d5ab 100644 --- a/remuxer/src/matroska_to_mpeg4.rs +++ b/remuxer/src/matroska_to_mpeg4.rs @@ -17,7 +17,7 @@ pub fn matroska_to_mpeg4( ) -> Result<()> { let path = format!("/tmp/jellything-tc-hack-{:016x}", random::()); let args = format!( - "-hide_banner -loglevel warning -f matroska -i pipe:0 -copyts -c copy -f mp4 -movflags frag_keyframe+empty_moov {path}" + "-hide_banner -loglevel warning -f matroska -i pipe:0 -c copy -f mp4 -movflags frag_keyframe+empty_moov {path}" ); let mut child = Command::new("ffmpeg") .args(args.split(" ")) diff --git a/remuxer/src/metadata.rs b/remuxer/src/metadata.rs index 4a496fe..c2931f4 100644 --- a/remuxer/src/metadata.rs +++ b/remuxer/src/metadata.rs @@ -23,6 +23,8 @@ use std::{ sync::Arc, }; +pub use ebml_struct::matroska::TrackEntry as MatroskaTrackEntry; + #[derive(Debug, Encode, Decode, Clone)] pub struct MatroskaMetadata { pub info: Option, diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index 0652df2..9e8c3bd 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -40,6 +40,14 @@ pub async fn fragment_stream( .formats .get(format_num) .ok_or(anyhow!("format not found"))?; + let orig_track = iinfo.metadata[file_index] + .tracks + .as_ref() + .unwrap() + .entries + .iter() + .find(|t| t.track_number == track_num) + .unwrap(); if format.remux { match container { @@ -81,6 +89,7 @@ pub async fn fragment_stream( } else { let location = transcode( track.kind, + orig_track, format, &format!("{path:?} {track_num} {index}"), move |b| { diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs index 3a33b46..1005edc 100644 --- a/stream/src/stream_info.rs +++ b/stream/src/stream_info.rs @@ -34,7 +34,7 @@ async fn async_get_track_sizes(path: PathBuf) -> Result> { pub(crate) struct InternalStreamInfo { pub paths: Vec, - pub _metadata: Vec>, + pub metadata: Vec>, pub track_to_file: Vec<(usize, u64)>, } @@ -76,7 +76,7 @@ pub(crate) async fn stream_info(info: Arc) -> Result<(InternalStream }; Ok(( InternalStreamInfo { - _metadata: metadata_arr, + metadata: metadata_arr, paths, track_to_file, }, @@ -119,11 +119,11 @@ fn stream_formats(t: &TrackEntry, remux_bitrate: f64) -> Vec { // most codecs use chroma subsampling that requires even dims let h = ((w * sh) / sw) & !1; // clear last bit to ensure even height. for (cid, enable) in [ - ("V_AV1", CONF.encoders.av1.is_some()), - ("V_VP8", CONF.encoders.vp8.is_some()), - ("V_VP9", CONF.encoders.vp9.is_some()), - ("V_MPEG4/ISO/AVC", CONF.encoders.avc.is_some()), - ("V_MPEGH/ISO/HEVC", CONF.encoders.hevc.is_some()), + ("V_AV1", CONF.transcoder.offer_av1), + ("V_VP8", CONF.transcoder.offer_vp8), + ("V_VP9", CONF.transcoder.offer_vp9), + ("V_MPEG4/ISO/AVC", CONF.transcoder.offer_avc), + ("V_MPEGH/ISO/HEVC", CONF.transcoder.offer_hevc), ] { if enable { formats.push(StreamFormatInfo { diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index 7c5cadd..027e80f 100644 --- a/transcoder/src/fragment.rs +++ b/transcoder/src/fragment.rs @@ -4,12 +4,18 @@ Copyright (C) 2025 metamuffin */ use crate::LOCAL_VIDEO_TRANSCODING_TASKS; +use anyhow::Result; use jellybase::{ cache::{async_cache_file, CachePath}, - common::stream::{StreamFormatInfo, TrackKind}, + common::{ + config::TranscoderConfig, + stream::{StreamFormatInfo, TrackKind}, + }, CONF, }; -use log::{debug, info}; +use jellyremuxer::metadata::MatroskaTrackEntry; +use log::info; +use std::fmt::Write; use std::process::Stdio; use tokio::{ io::copy, @@ -21,62 +27,16 @@ use tokio::{ pub async fn transcode( kind: TrackKind, + orig_metadata: &MatroskaTrackEntry, format: &StreamFormatInfo, input_key: &str, input: impl FnOnce(ChildStdin), ) -> anyhow::Result { - let template = match format.codec.as_str() { - "V_MPEG4/ISO/AVC" => CONF.encoders.avc.as_ref(), - "V_MPEGH/ISO/HEVC" => CONF.encoders.hevc.as_ref(), - "V_VP8" => CONF.encoders.vp8.as_ref(), - "V_VP9" => CONF.encoders.vp9.as_ref(), - "V_AV1" => CONF.encoders.av1.as_ref(), - _ => None, - } - .or(CONF.encoders.generic.as_ref()) - .cloned() - .unwrap_or("ffmpeg %a".to_owned()); - - let filter = match kind { - TrackKind::Video => "-vf scale=%w:%h", - TrackKind::Audio => "", - TrackKind::Subtitle => "", - }; - let typechar = match kind { - TrackKind::Video => "v", - TrackKind::Audio => "a", - TrackKind::Subtitle => "s", - }; - let fallback_encoder = match format.codec.as_str() { - "A_OPUS" => "libopus", - "V_VP8" => "libvpx", - "V_VP9" => "libvpx-vp9", - "V_AV1" => "libaom", // svtav1 is x86 only :( - "V_MPEG4/ISO/AVC" => "libx264", - "V_MPEGH/ISO/HEVC" => "libx265", - _ => "", - }; - - let args = template - .replace("%a", "-hide_banner %i %f %e %o") - .replace("%i", "-f matroska -i pipe:0 -copyts") - .replace("%o", "-f matroska pipe:1") - .replace("%f", &filter) - .replace("%w", &format.width.unwrap_or_default().to_string()) - .replace("%h", &format.height.unwrap_or_default().to_string()) - .replace("%e", "-c:%t %c -b:%t %r") - .replace("%t", typechar) - .replace("%c", fallback_encoder) - .replace("%r", &(format.bitrate as i64).to_string()) - .replace(" ", " "); - - async_cache_file("frag-tc", (input_key, &args), async |mut output| { + let command = transcode_command(kind, orig_metadata, format, &CONF.transcoder).unwrap(); + async_cache_file("frag-tc", (input_key, &command), async |mut output| { let _permit = LOCAL_VIDEO_TRANSCODING_TASKS.acquire().await?; - debug!("transcoding fragment with {format:?}"); - - info!("encoding with {:?}", args); - - let mut args = args.split(" "); + info!("encoding with {command:?}"); + let mut args = command.split(" "); let mut proc = Command::new(args.next().unwrap()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -95,3 +55,77 @@ pub async fn transcode( }) .await } + +fn transcode_command( + kind: TrackKind, + orig_metadata: &MatroskaTrackEntry, + format: &StreamFormatInfo, + config: &TranscoderConfig, +) -> Result { + let br = format.bitrate as u64; + let w = format.width.unwrap_or(0); + let h = format.height.unwrap_or(0); + let mut o = String::new(); + write!(o, "ffmpeg -hide_banner ")?; + + if kind == TrackKind::Video { + if config.enable_rkmpp { + write!(o, "-hwaccel rkmpp -hwaccel_output_format drm_prime ")?; + } + if config.enable_rkrga { + write!(o, "-afbc rga ")?; + } + + write!(o, "-f matroska -i pipe:0 -copyts ")?; + + if config.enable_rkrga { + write!(o, "-vf scale_rkrga=w={w}:h={h}:format=nv12:afbc=1 ")?; + } else { + write!(o, "-vf scale={w}:{h} ")?; + } + + match format.codec.as_str() { + "V_MPEG4/ISO/AVC" if config.enable_rkmpp => { + write!(o, "-c:v h264_rkmpp -profile:v high -b:v {br} ")? + } + "V_MPEGH/ISO/HEVC" if config.enable_rkmpp => { + write!(o, "-c:v h265_rkmpp -profile:v high -b:v {br} ")? + } + "A_AV1" if config.use_svtav1 => { + let p = config.svtav1_preset.unwrap_or(8); + write!(o, "-c:v libsvtav1 -preset {p} -b:v {br} ")? + } + "A_AV1" if config.use_rav1e => { + let p = config.rav1e_preset.unwrap_or(8); + write!(o, "-c:v librav1e -speed {p} -b:v {br} ")? + } + "V_MPEG4/ISO/AVC" => { + let p = config.x264_preset.clone().unwrap_or("fast".to_string()); + write!(o, "-c:v libx264 -preset {p} -b:v {br} ")? + } + "V_MPEGH/ISO/HEVC" => write!(o, "-c:v libx265 -b:v {br} ")?, + "V_VP8" => write!(o, "-c:v libvpx -b:v {br} ")?, + "V_VP9" => write!(o, "-c:v libvpx-vp9 -b:v {br} ")?, + "V_AV1" => { + let p = config.aom_preset.unwrap_or(1); + write!(o, "-c:v libaom -cpu-used {p} -row-mt 1 -b:v {br} ")? + } + _ => todo!(), + }; + } else if kind == TrackKind::Audio { + write!(o, "-f matroska -i pipe:0 -copyts ")?; + if format.codec == "A_OPUS" && orig_metadata.audio.as_ref().unwrap().channels > 2 { + write!(o, "-ac 2 ")?; + } + match format.codec.as_str() { + "A_OPUS" => write!(o, "-c:a libopus -b:a {br} ")?, + _ => todo!(), + } + } else { + write!(o, "-f matroska -i pipe:0 -copyts ")?; + todo!() + } + + write!(o, "-f matroska pipe:1")?; + Ok(o) +} -- cgit v1.2.3-70-g09d2