aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-03-04 17:52:03 +0100
committermetamuffin <metamuffin@disroot.org>2026-03-04 17:52:03 +0100
commit7ded052e22df1be30b29a2943b2bbe9196152a2d (patch)
tree1c6aae6e342101c78dd97f304ab2868ee7e2665d
parent3eaba0353512ee6ebb909722fde931136b44a4f8 (diff)
downloadjellything-7ded052e22df1be30b29a2943b2bbe9196152a2d.tar
jellything-7ded052e22df1be30b29a2943b2bbe9196152a2d.tar.bz2
jellything-7ded052e22df1be30b29a2943b2bbe9196152a2d.tar.zst
move codec parameter string code in remuxer; init frag for transcodes
-rw-r--r--remuxer/src/codec_param/av1.rs38
-rw-r--r--remuxer/src/codec_param/hevc.rs55
-rw-r--r--remuxer/src/codec_param/mod.rs27
-rw-r--r--remuxer/src/lib.rs2
-rw-r--r--stream/src/dash.rs22
-rw-r--r--stream/src/fragment.rs92
-rw-r--r--stream/src/lib.rs9
-rw-r--r--stream/src/stream_info.rs53
-rw-r--r--stream/types/src/lib.rs17
9 files changed, 224 insertions, 91 deletions
diff --git a/remuxer/src/codec_param/av1.rs b/remuxer/src/codec_param/av1.rs
new file mode 100644
index 0000000..5641e77
--- /dev/null
+++ b/remuxer/src/codec_param/av1.rs
@@ -0,0 +1,38 @@
+/*
+ This file is part of jellything (https://codeberg.org/metamuffin/jellything)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2026 metamuffin <metamuffin.org>
+*/
+
+pub fn av1_codec_param(cp: &[u8]) -> String {
+ let profile = (cp[1] >> 5) & 0b111;
+ let level = cp[1] & 0b11111;
+ let tier = (cp[2] >> 7) & 0b1;
+ let high_bitdepth = (cp[2] >> 6) & 0b1;
+ let twelve_bit = (cp[2] >> 5) & 0b1;
+ let _monochrome = (cp[2] >> 4) & 0b1;
+ let _css_x = (cp[2] >> 3) & 0b1;
+ let _css_y = (cp[2] >> 2) & 0b1;
+ let _css_pos = cp[2] & 0b11;
+
+ let tier_char = if tier == 1 { 'H' } else { 'M' };
+ let bit_depth = if twelve_bit == 1 {
+ 12
+ } else if high_bitdepth == 1 {
+ 10
+ } else {
+ 0
+ };
+ format!(
+ "av01.{profile}.{level:02}{tier_char}.{bit_depth:02}" // .{monochrome}.{css_x}{css_y}{css_pos}
+ )
+}
+
+#[test]
+fn sample1() {
+ assert_eq!(av1_codec_param(&[0x81, 0x04, 0x4E]), "av01.0.04M.10");
+}
+#[test]
+fn sample2() {
+ assert_eq!(av1_codec_param(&[0x81, 0x35, 0xF4]), "av01.1.21H.12");
+}
diff --git a/remuxer/src/codec_param/hevc.rs b/remuxer/src/codec_param/hevc.rs
new file mode 100644
index 0000000..3459258
--- /dev/null
+++ b/remuxer/src/codec_param/hevc.rs
@@ -0,0 +1,55 @@
+/*
+ This file is part of jellything (https://codeberg.org/metamuffin/jellything)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2026 metamuffin <metamuffin.org>
+*/
+
+use std::fmt::Write;
+
+pub(super) fn hevc_codec_param(cp: &[u8]) -> String {
+ 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]]).reverse_bits();
+ let mut d = String::new();
+ let trailing_zeroes = cp[6..12].iter().rev().take_while(|x| **x == 0).count();
+ for &flag in &cp[6..12 - trailing_zeroes] {
+ 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,
+ )
+}
+
+#[test]
+fn sample1() {
+ let cp = [
+ 0x01, 0x02, 0x20, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F,
+ ];
+ assert_eq!(hevc_codec_param(&cp), "hvc1.2.4.L63.90")
+}
+
+#[test]
+fn sample2() {
+ let cp = [
+ 0x01, 0x01, 0x60, 0x00, 0x00, 0x00, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78,
+ ];
+ assert_eq!(hevc_codec_param(&cp), "hvc1.1.6.L120.b0")
+}
diff --git a/remuxer/src/codec_param/mod.rs b/remuxer/src/codec_param/mod.rs
new file mode 100644
index 0000000..8c0b6b7
--- /dev/null
+++ b/remuxer/src/codec_param/mod.rs
@@ -0,0 +1,27 @@
+/*
+ This file is part of jellything (https://codeberg.org/metamuffin/jellything)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2026 metamuffin <metamuffin.org>
+*/
+
+use crate::codec_param::{av1::av1_codec_param, hevc::hevc_codec_param};
+use winter_matroska::TrackEntry;
+
+mod av1;
+mod hevc;
+
+pub fn codec_param(te: &TrackEntry) -> String {
+ let cp = te.codec_private.as_ref().unwrap();
+ match te.codec_id.as_str() {
+ "A_AAC" => format!("mp4a.40.2"), // TODO
+ "A_FLAC" => "flac".to_string(),
+ "A_OPUS" => "opus".to_string(),
+ "A_VORBIS" => "vorbis".to_string(),
+
+ "V_AV1" => av1_codec_param(cp),
+ "V_MPEG4/ISO/AVC" => format!("avc1.{:02x}{:02x}{:02x}", cp[1], cp[2], cp[3]),
+ "V_MPEGH/ISO/HEVC" => hevc_codec_param(cp),
+
+ x => todo!("{x:?}"),
+ }
+}
diff --git a/remuxer/src/lib.rs b/remuxer/src/lib.rs
index e4b0fdc..ad5de53 100644
--- a/remuxer/src/lib.rs
+++ b/remuxer/src/lib.rs
@@ -7,7 +7,9 @@
pub mod demuxers;
pub mod magic;
pub mod muxers;
+mod codec_param;
+pub use codec_param::codec_param;
pub use winter_matroska as matroska;
#[derive(Debug, Clone, Copy, PartialEq)]
diff --git a/stream/src/dash.rs b/stream/src/dash.rs
index 01d0019..b9b9b3a 100644
--- a/stream/src/dash.rs
+++ b/stream/src/dash.rs
@@ -78,8 +78,8 @@ pub fn dash(sinfo: &SMediaInfo) -> Result<Box<dyn Read + Send + Sync>> {
else {
unreachable!()
};
- let container = StreamContainer::WebM;
- let container_mime = container.mime_type();
+ let container = choose_container(format);
+ let container_mime = container.mime_type(track.kind);
let codec_param = &format.codec_param;
writeln!(
out,
@@ -87,7 +87,7 @@ pub fn dash(sinfo: &SMediaInfo) -> Result<Box<dyn Read + Send + Sync>> {
id=\"{repr_id}\" \
mimeType=\"{container_mime}\" \
codecs=\"{codec_param}\" \
- bandwidth=\"{bitrate}\" \
+ bandwidth=\"{bitrate:.0}\" \
width=\"{width}\" \
height=\"{height}\" \
scanType=\"unknown\" \
@@ -117,8 +117,8 @@ pub fn dash(sinfo: &SMediaInfo) -> Result<Box<dyn Read + Send + Sync>> {
else {
unreachable!()
};
- let container = StreamContainer::WebM;
- let container_mime = container.mime_type();
+ let container = choose_container(format);
+ let container_mime = container.mime_type(track.kind);
let codec_param = &format.codec_param;
writeln!(
out,
@@ -126,7 +126,7 @@ pub fn dash(sinfo: &SMediaInfo) -> Result<Box<dyn Read + Send + Sync>> {
id=\"{repr_id}\" \
mimeType=\"{container_mime}\" \
codecs=\"{codec_param}\" \
- bandwidth=\"{bitrate}\" \
+ bandwidth=\"{bitrate:.0}\" \
audioSamplingRate=\"{samplerate:.0}\">"
)?;
write_segment_template(&mut out, as_id, container, &frags)?;
@@ -144,6 +144,16 @@ pub fn dash(sinfo: &SMediaInfo) -> Result<Box<dyn Read + Send + Sync>> {
Ok(Box::new(Cursor::new(out)))
}
+fn choose_container(format: &StreamFormatInfo) -> StreamContainer {
+ if format.containers.contains(&StreamContainer::WebM) {
+ StreamContainer::WebM
+ } else if format.containers.contains(&StreamContainer::MPEG4) {
+ StreamContainer::MPEG4
+ } else {
+ StreamContainer::Matroska
+ }
+}
+
fn write_segment_template(
out: &mut String,
as_id: usize,
diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs
index ea730ed..de7cd11 100644
--- a/stream/src/fragment.rs
+++ b/stream/src/fragment.rs
@@ -12,7 +12,9 @@ use jellyremuxer::{
matroska::{self, Info, Segment},
muxers::{write_frag, write_init},
};
-use jellystream_types::{FormatNum, IndexNum, StreamContainer, TrackNum};
+use jellystream_types::{
+ FormatNum, IndexNum, StreamContainer, StreamFormatInfo, StreamTrackInfo, TrackNum,
+};
use jellytranscoder::fragment::transcode;
use std::{
fs::File,
@@ -23,37 +25,52 @@ use std::{
pub fn fragment_init(
sinfo: &SMediaInfo,
- track: TrackNum,
+ track_num: TrackNum,
format_num: FormatNum,
) -> Result<Segment> {
let (iinfo, info) = stream_info(&sinfo)?;
- let (file_index, track_num) = *iinfo
+ let (file_index, file_track_num) = *iinfo
.track_to_file
- .get(track)
+ .get(track_num)
+ .ok_or(anyhow!("track not found"))?;
+ let track = info
+ .tracks
+ .get(track_num)
.ok_or(anyhow!("track not found"))?;
- let track = info.tracks.get(track).ok_or(anyhow!("track not found"))?;
let format = track
.formats
.get(format_num)
.ok_or(anyhow!("format not found"))?;
- let mk_track = iinfo.metadata[file_index]
+ let mut mk_track = iinfo.metadata[file_index]
.tracks
.as_ref()
.unwrap()
.entries
.iter()
- .find(|t| t.track_number == track_num)
- .unwrap();
+ .find(|t| t.track_number == file_track_num)
+ .unwrap()
+ .to_owned();
let timestamp_scale = iinfo.metadata[file_index].info.timestamp_scale;
let duration = iinfo.metadata[file_index].info.duration;
- if !format.remux {}
+ if !format.remux {
+ let seg = fragment_transcode_full(
+ sinfo,
+ track,
+ track_num,
+ file_track_num,
+ 0,
+ &iinfo.paths[file_index],
+ format,
+ )?;
+ mk_track = seg.tracks.unwrap().entries[0].clone();
+ }
Ok(Segment {
tracks: Some(matroska::Tracks {
- entries: vec![mk_track.to_owned()],
+ entries: vec![mk_track],
}),
info: Info {
duration,
@@ -123,6 +140,39 @@ pub fn fragment_remux(
})
}
+pub fn fragment_transcode_full(
+ sinfo: &SMediaInfo,
+ track: &StreamTrackInfo,
+ track_num: TrackNum,
+ file_track_num: u64,
+ index: IndexNum,
+ media_path: &Path,
+ output_format: &StreamFormatInfo,
+) -> Result<Segment> {
+ let (input_format_num, input_format) = track
+ .formats
+ .iter()
+ .enumerate()
+ .find(|(_, t)| t.remux)
+ .unwrap();
+ transcode(
+ &sinfo.cache,
+ &sinfo.config.transcoder,
+ track.kind,
+ &format!("{}-T{file_track_num}-I{index}", HashKey(&media_path)),
+ input_format,
+ output_format,
+ || {
+ let init = fragment_init(&sinfo, track_num, input_format_num)?;
+ let seg = fragment_remux(&sinfo, &media_path, file_track_num, index, true)?;
+ Ok(Segment {
+ clusters: seg.clusters,
+ ..init
+ })
+ },
+ )
+}
+
pub fn fragment_stream(
sinfo: Arc<SMediaInfo>,
track_num: TrackNum,
@@ -149,22 +199,14 @@ pub fn fragment_stream(
let segment = if output_format.remux {
fragment_remux(&sinfo, &media_path, file_track_num, index, false)?
} else {
- 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)),
- input_format,
+ fragment_transcode_full(
+ &sinfo,
+ track,
+ track_num,
+ file_track_num,
+ index,
+ &media_path,
output_format,
- || {
- let init = fragment_init(&sinfo, track_num, format_num)?;
- let seg = fragment_remux(&sinfo, &media_path, file_track_num, index, true)?;
- Ok(Segment {
- clusters: seg.clusters,
- ..init
- })
- },
)?
};
diff --git a/stream/src/lib.rs b/stream/src/lib.rs
index 3f6e93f..801f29c 100644
--- a/stream/src/lib.rs
+++ b/stream/src/lib.rs
@@ -18,7 +18,7 @@ use fragment::fragment_stream;
use fragment_index::fragment_index_stream;
use hls::{hls_multivariant_stream, hls_variant_stream};
use jellycache::Cache;
-use jellystream_types::StreamSpec;
+use jellystream_types::{StreamSpec, TrackKind};
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeSet,
@@ -56,6 +56,7 @@ pub struct StreamHead {
}
pub fn stream_head(spec: &StreamSpec) -> StreamHead {
+ let kind = TrackKind::Video; // TODO
use StreamSpec::*;
let range_supported = matches!(spec, Remux { .. } | Original { .. });
let content_type = match spec {
@@ -65,9 +66,9 @@ pub fn stream_head(spec: &StreamSpec) -> StreamHead {
Info => "application/jellything-stream-info+json",
Dash => "application/dash+xml",
FragmentIndex { .. } => "application/jellything-frag-index+json",
- FragmentInit { container, .. } => container.mime_type(),
- Fragment { container, .. } => container.mime_type(),
- Remux { container, .. } => container.mime_type(),
+ FragmentInit { container, .. } => container.mime_type(kind),
+ Fragment { container, .. } => container.mime_type(kind),
+ Remux { container, .. } => container.mime_type(kind),
};
StreamHead {
content_type,
diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs
index 16b77c2..1cc1663 100644
--- a/stream/src/stream_info.rs
+++ b/stream/src/stream_info.rs
@@ -6,13 +6,15 @@
use crate::{Config, SMediaInfo, cues::generate_cues, metadata::read_metadata};
use anyhow::Result;
use jellycache::Cache;
-use jellyremuxer::matroska::{self, Segment, TrackEntry, TrackType};
+use jellyremuxer::{
+ codec_param,
+ 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,
@@ -196,50 +198,3 @@ 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 433b757..50227ec 100644
--- a/stream/types/src/lib.rs
+++ b/stream/types/src/lib.rs
@@ -139,13 +139,16 @@ pub enum StreamContainer {
}
impl StreamContainer {
- pub fn mime_type(&self) -> &'static str {
- match self {
- Self::WebM => "video/webm",
- Self::Matroska => "video/x-matroska",
- Self::WebVTT => "text/vtt",
- Self::JVTT => "application/jellything-vtt+json",
- Self::MPEG4 => "video/mp4",
+ pub fn mime_type(&self, kind: TrackKind) -> &'static str {
+ match (self, kind) {
+ (Self::WebM, TrackKind::Audio) => "audio/webm",
+ (Self::WebM, _) => "video/webm",
+ (Self::Matroska, TrackKind::Audio) => "audio/x-matroska",
+ (Self::Matroska, _) => "video/x-matroska",
+ (Self::MPEG4, TrackKind::Audio) => "audio/mp4",
+ (Self::MPEG4, _) => "video/mp4",
+ (Self::WebVTT, _) => "text/vtt",
+ (Self::JVTT, _) => "application/jellything-vtt+json",
}
}
}