diff options
Diffstat (limited to 'stream')
| -rw-r--r-- | stream/src/dash.rs | 2 | ||||
| -rw-r--r-- | stream/src/fragment.rs | 18 | ||||
| -rw-r--r-- | stream/src/stream_info.rs | 91 | ||||
| -rw-r--r-- | stream/types/src/lib.rs | 40 |
4 files changed, 127 insertions, 24 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, |