From 3eaba0353512ee6ebb909722fde931136b44a4f8 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 4 Mar 2026 01:50:37 +0100 Subject: codec parameter string in stream info --- stream/src/dash.rs | 2 +- stream/src/fragment.rs | 18 +++------ stream/src/stream_info.rs | 91 ++++++++++++++++++++++++++++++++++++++++++---- stream/types/src/lib.rs | 40 +++++++++++++++++++- transcoder/src/fragment.rs | 48 +++++++++++------------- 5 files changed, 148 insertions(+), 51 deletions(-) diff --git a/stream/src/dash.rs b/stream/src/dash.rs index 17fe43e..01d0019 100644 --- a/stream/src/dash.rs +++ b/stream/src/dash.rs @@ -29,7 +29,7 @@ pub fn dash(sinfo: &SMediaInfo) -> Result> { type=\"static\" \ mediaPresentationDuration=\"{}\" \ maxSegmentDuration=\"PT5.0S\" \ - minBufferTime=\"PT10.4S\">", + minBufferTime=\"PT10.0S\">", Time(info.duration) )?; diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index e759c86..ea730ed 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -141,30 +141,22 @@ pub fn fragment_stream( .tracks .get(track_num) .ok_or(anyhow!("track not found"))?; - let format = track + let output_format = track .formats .get(format_num) .ok_or(anyhow!("format not found"))?; - let segment = if format.remux { + let segment = if output_format.remux { fragment_remux(&sinfo, &media_path, file_track_num, index, false)? } else { - let mk_track = iinfo.metadata[file_index] - .tracks - .as_ref() - .unwrap() - .entries - .iter() - .find(|t| t.track_number == file_track_num) - .unwrap(); - + let input_format = track.formats.iter().find(|t| t.remux).unwrap(); transcode( &sinfo.cache, &sinfo.config.transcoder, track.kind, &format!("{}-T{file_track_num}-I{index}", HashKey(&media_path)), - format, - mk_track, + input_format, + output_format, || { let init = fragment_init(&sinfo, track_num, format_num)?; let seg = fragment_remux(&sinfo, &media_path, file_track_num, index, true)?; diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs index 4a2896f..16b77c2 100644 --- a/stream/src/stream_info.rs +++ b/stream/src/stream_info.rs @@ -5,11 +5,14 @@ */ use crate::{Config, SMediaInfo, cues::generate_cues, metadata::read_metadata}; use anyhow::Result; +use jellycache::Cache; use jellyremuxer::matroska::{self, Segment, TrackEntry, TrackType}; use jellystream_types::{ StreamContainer, StreamFormatInfo, StreamInfo, StreamTrackInfo, TrackKind, }; +use jellytranscoder::fragment::transcode_init; use std::{ + fmt::Write, io::{Cursor, Read}, path::PathBuf, sync::Arc, @@ -47,7 +50,7 @@ pub(crate) fn stream_info(info: &SMediaInfo) -> Result<(InternalStreamInfo, Stre matroska::TrackType::Subtitle => TrackKind::Subtitle, _ => todo!(), }, - formats: stream_formats(&info.config, t, byterate * 8.), + formats: stream_formats(&info.cache, &info.config, t, byterate * 8.)?, }); track_to_file.push((i, t.track_number)); } @@ -71,11 +74,16 @@ pub(crate) fn stream_info(info: &SMediaInfo) -> Result<(InternalStreamInfo, Stre )) } -fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec { +fn stream_formats( + cache: &Cache, + config: &Config, + t: &TrackEntry, + remux_bitrate: f64, +) -> Result> { let mut formats = Vec::new(); formats.push(StreamFormatInfo { codec: t.codec_id.to_string(), - codec_param: String::new(), // TODO + codec_param: codec_param(t), remux: true, bitrate: remux_bitrate, containers: containers_by_codec(&t.codec_id), @@ -85,7 +93,6 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec { let sw = t.video.as_ref().unwrap().pixel_width; @@ -101,6 +108,9 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec sw { continue; } + if br > remux_bitrate { + continue; + } // 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 [ @@ -111,9 +121,9 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec Vec Vec {} } - formats + Ok(formats) +} + +fn track_type_mk(tt: TrackType) -> TrackKind { + match tt { + TrackType::Video => TrackKind::Video, + TrackType::Audio => TrackKind::Audio, + TrackType::Subtitle => TrackKind::Subtitle, + _ => todo!(), + } } fn containers_by_codec(codec: &str) -> Vec { @@ -168,3 +196,50 @@ pub(crate) fn write_stream_info(info: &SMediaInfo) -> Result f64 { (info.duration.unwrap_or_default() * info.timestamp_scale as f64) / 1_000_000_000. } + +fn codec_param(te: &TrackEntry) -> String { + let cp = te.codec_private.as_ref().unwrap(); + match te.codec_id.as_str() { + "A_OPUS" => "opus".to_string(), + "A_VORBIS" => "vorbis".to_string(), + "V_MPEG4/ISO/AVC" => format!("avc1.{:02x}{:02x}{:02x}", cp[1], cp[2], cp[3]), + "V_MPEGH/ISO/HEVC" => { + let general_profile_space = cp[1] >> 6; + let general_tier_flag = (cp[1] >> 5) & 0b1; + let general_profile_idc = cp[1] & 0b11111; + let general_level_idc = cp[12]; + let general_profile_compatibility_flag = + u32::from_be_bytes([cp[2], cp[3], cp[4], cp[5]]); + let mut d = String::new(); + for &flag in &cp[6..12] { + write!(d, ".{flag:02x}").unwrap(); + } + format!( + "hvc1.{}{}.{:x}.{}{}{d}", + match general_profile_space { + 0 => "", + 1 => "A", + 2 => "B", + 3 => "C", + _ => unreachable!(), + }, + general_profile_idc, + general_profile_compatibility_flag, + match general_tier_flag { + 0 => 'L', + 1 => 'H', + _ => unreachable!(), + }, + general_level_idc, + ) + } + "V_AV1" => { + let seq_profile = (cp[1] >> 5) & 0b111; + format!("av01") + } + "A_AAC" => { + format!("aac1") + } + x => todo!("{x:?}"), + } +} diff --git a/stream/types/src/lib.rs b/stream/types/src/lib.rs index d1583e9..433b757 100644 --- a/stream/types/src/lib.rs +++ b/stream/types/src/lib.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin */ use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, fmt::Display, str::FromStr}; +use std::{collections::BTreeMap, fmt::Display, fmt::Write, str::FromStr}; pub type TrackNum = usize; pub type FormatNum = usize; @@ -92,7 +92,43 @@ pub struct StreamFormatInfo { pub bit_depth: Option, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +impl std::hash::Hash for StreamFormatInfo { + fn hash(&self, state: &mut H) { + self.codec.hash(state); + self.codec_param.hash(state); + (self.bitrate as i64).hash(state); + self.remux.hash(state); + self.containers.hash(state); + self.width.hash(state); + self.height.hash(state); + (self.samplerate.unwrap_or(0.) as i64).hash(state); + self.channels.hash(state); + self.bit_depth.hash(state); + } +} +impl StreamFormatInfo { + pub fn metadata_str(&self) -> String { + let mut o = String::new(); + if let Some(w) = self.width { + write!(o, "w{w}").unwrap(); + } + if let Some(h) = self.height { + write!(o, "h{h}").unwrap(); + } + if let Some(r) = self.samplerate { + write!(o, "r{r:.02}").unwrap(); + } + if let Some(b) = self.bit_depth { + write!(o, "b{b}").unwrap(); + } + if let Some(c) = self.channels { + write!(o, "c{c}").unwrap(); + } + o + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash)] #[serde(rename_all = "lowercase")] pub enum StreamContainer { WebM, diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index b6c3b2d..a7a6f42 100644 --- a/transcoder/src/fragment.rs +++ b/transcoder/src/fragment.rs @@ -15,7 +15,7 @@ use std::{ process::{Command, Stdio}, thread::spawn, }; -use winter_matroska::{Segment, TrackEntry as MatroskaTrackEntry}; +use winter_matroska::Segment; // TODO odd video resolutions can cause errors when transcoding to YUV42{0,2} // TODO with an implementation that cant handle it (SVT-AV1 is such an impl). @@ -24,19 +24,14 @@ pub fn transcode_init( cache: &Cache, config: &Config, kind: TrackKind, - input_key: &str, + input_format: &StreamFormatInfo, output_format: &StreamFormatInfo, ) -> Result { - let command = transcode_command( - kind, - &MatroskaTrackEntry::default(), - output_format, - true, - config, - )?; + let command = transcode_command(kind, input_format, output_format, true, config)?; let output = cache.cache( &format!( - "transcode/media-fragment/{input_key}-{}.mkv", + "transcode/media-init/{}-{}.mkv", + input_format.metadata_str(), HashKey(&command) ), || { @@ -65,11 +60,11 @@ pub fn transcode( config: &Config, kind: TrackKind, input_key: &str, + input_format: &StreamFormatInfo, output_format: &StreamFormatInfo, - input_track: &MatroskaTrackEntry, segment: impl FnOnce() -> Result, ) -> Result { - let command = transcode_command(kind, &input_track, output_format, false, config).unwrap(); + let command = transcode_command(kind, &input_format, output_format, false, config).unwrap(); let output = cache.cache( &format!( @@ -131,14 +126,14 @@ pub fn transcode( fn transcode_command( kind: TrackKind, - orig_metadata: &MatroskaTrackEntry, - format: &StreamFormatInfo, + input: &StreamFormatInfo, + output: &StreamFormatInfo, dummy: bool, config: &Config, ) -> Result { - let br = format.bitrate as u64; - let w = format.width.unwrap_or(0); - let h = format.height.unwrap_or(0); + let br = output.bitrate as u64; + let w = output.width.unwrap_or(0); + let h = output.height.unwrap_or(0); let mut o = String::new(); write!(o, "ffmpeg -hide_banner ")?; @@ -151,18 +146,17 @@ fn transcode_command( } if dummy { - write!(o, "-f lavfi -i testsrc2 -to 1 ")?; + write!(o, "-f lavfi -i testsrc2=s={w}x{h},format=yuv420p -to 1 ")?; } else { 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} ")?; + } } - 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() { + match output.codec.as_str() { "V_MPEG4/ISO/AVC" if config.enable_rkmpp => { write!(o, "-c:v h264_rkmpp -profile:v high -b:v {br} ")? } @@ -192,10 +186,10 @@ fn transcode_command( }; } 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 { + if output.codec == "A_OPUS" && input.channels.unwrap_or(2) > 2 { write!(o, "-ac 2 ")?; } - match format.codec.as_str() { + match output.codec.as_str() { "A_OPUS" => write!(o, "-c:a libopus -b:a {br} ")?, _ => todo!(), } -- cgit v1.3