diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-03-04 17:52:03 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-03-04 17:52:03 +0100 |
| commit | 7ded052e22df1be30b29a2943b2bbe9196152a2d (patch) | |
| tree | 1c6aae6e342101c78dd97f304ab2868ee7e2665d | |
| parent | 3eaba0353512ee6ebb909722fde931136b44a4f8 (diff) | |
| download | jellything-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.rs | 38 | ||||
| -rw-r--r-- | remuxer/src/codec_param/hevc.rs | 55 | ||||
| -rw-r--r-- | remuxer/src/codec_param/mod.rs | 27 | ||||
| -rw-r--r-- | remuxer/src/lib.rs | 2 | ||||
| -rw-r--r-- | stream/src/dash.rs | 22 | ||||
| -rw-r--r-- | stream/src/fragment.rs | 92 | ||||
| -rw-r--r-- | stream/src/lib.rs | 9 | ||||
| -rw-r--r-- | stream/src/stream_info.rs | 53 | ||||
| -rw-r--r-- | stream/types/src/lib.rs | 17 |
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", } } } |