aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-03-04 01:50:37 +0100
committermetamuffin <metamuffin@disroot.org>2026-03-04 01:50:37 +0100
commit3eaba0353512ee6ebb909722fde931136b44a4f8 (patch)
tree06d1352bfd6c170fae6cf2af21eb3efaf3b9fe35
parent4c70753ee7311f644401669e6fde7b4a6cd32992 (diff)
downloadjellything-3eaba0353512ee6ebb909722fde931136b44a4f8.tar
jellything-3eaba0353512ee6ebb909722fde931136b44a4f8.tar.bz2
jellything-3eaba0353512ee6ebb909722fde931136b44a4f8.tar.zst
codec parameter string in stream info
-rw-r--r--stream/src/dash.rs2
-rw-r--r--stream/src/fragment.rs18
-rw-r--r--stream/src/stream_info.rs91
-rw-r--r--stream/types/src/lib.rs40
-rw-r--r--transcoder/src/fragment.rs48
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<Box<dyn Read + Send + Sync>> {
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<StreamFormatInfo> {
+fn stream_formats(
+ cache: &Cache,
+ config: &Config,
+ t: &TrackEntry,
+ remux_bitrate: f64,
+) -> Result<Vec<StreamFormatInfo>> {
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<St
width: t.video.as_ref().map(|v| v.pixel_width),
height: t.video.as_ref().map(|v| v.pixel_height),
});
-
match t.track_type {
TrackType::Video => {
let sw = t.video.as_ref().unwrap().pixel_width;
@@ -101,6 +108,9 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec<St
if w > 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<St
("V_MPEGH/ISO/HEVC", config.offer_hevc),
] {
if enable {
- formats.push(StreamFormatInfo {
+ let mut f = StreamFormatInfo {
codec: cid.to_string(),
- codec_param: String::new(), // TODO
+ codec_param: String::new(), // assigned later
bitrate: (remux_bitrate * 3.).min(br),
remux: false,
containers: containers_by_codec(cid),
@@ -122,7 +132,16 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec<St
samplerate: None,
channels: None,
bit_depth: None,
- });
+ };
+ let init = transcode_init(
+ cache,
+ &config.transcoder,
+ track_type_mk(t.track_type),
+ &formats[0],
+ &f,
+ )?;
+ f.codec_param = codec_param(&init.tracks.unwrap().entries[0]);
+ formats.push(f);
}
}
}
@@ -147,7 +166,16 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec<St
_ => {}
}
- 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<StreamContainer> {
@@ -168,3 +196,50 @@ pub(crate) fn write_stream_info(info: &SMediaInfo) -> Result<Box<dyn Read + Send
fn media_duration(info: &matroska::Info) -> 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 <metamuffin.org>
*/
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<u8>,
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
+impl std::hash::Hash for StreamFormatInfo {
+ fn hash<H: std::hash::Hasher>(&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<Segment> {
- 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<Segment>,
) -> Result<Segment> {
- 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<String> {
- 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!(),
}