diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-03-03 22:36:42 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-03-03 22:36:42 +0100 |
| commit | 4c70753ee7311f644401669e6fde7b4a6cd32992 (patch) | |
| tree | 2c1cc89367d76b918d6e33857ed8a2e346f2daa3 /stream | |
| parent | 0b07910ad847a8c4431b8be244b7105b7b23f6e2 (diff) | |
| download | jellything-4c70753ee7311f644401669e6fde7b4a6cd32992.tar jellything-4c70753ee7311f644401669e6fde7b4a6cd32992.tar.bz2 jellything-4c70753ee7311f644401669e6fde7b4a6cd32992.tar.zst | |
dash
Diffstat (limited to 'stream')
| -rw-r--r-- | stream/src/dash.rs | 190 | ||||
| -rw-r--r-- | stream/src/fragment.rs | 184 | ||||
| -rw-r--r-- | stream/src/hls.rs | 9 | ||||
| -rw-r--r-- | stream/src/lib.rs | 31 | ||||
| -rw-r--r-- | stream/src/stream_info.rs | 3 | ||||
| -rw-r--r-- | stream/types/src/lib.rs | 71 |
6 files changed, 379 insertions, 109 deletions
diff --git a/stream/src/dash.rs b/stream/src/dash.rs new file mode 100644 index 0000000..17fe43e --- /dev/null +++ b/stream/src/dash.rs @@ -0,0 +1,190 @@ +/* + 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::{SMediaInfo, fragment_index::fragment_index, stream_info}; +use anyhow::Result; +use jellystream_types::{StreamContainer, StreamFormatInfo, TrackKind}; +use std::{ + fmt::{Display, Write}, + io::{Cursor, Read}, + ops::Range, +}; + +pub fn dash(sinfo: &SMediaInfo) -> Result<Box<dyn Read + Send + Sync>> { + let (_iinfo, info) = stream_info(&sinfo)?; + + let mut out = String::new(); + + writeln!( + out, + "<?xml version=\"1.0\" encoding=\"utf-8\"?> \ + <MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \ + xmlns=\"urn:mpeg:dash:schema:mpd:2011\" \ + xmlns:xlink=\"http://www.w3.org/1999/xlink\" \ + xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\" \ + profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" \ + type=\"static\" \ + mediaPresentationDuration=\"{}\" \ + maxSegmentDuration=\"PT5.0S\" \ + minBufferTime=\"PT10.4S\">", + Time(info.duration) + )?; + + writeln!(out, "<ProgramInformation></ProgramInformation>")?; + writeln!(out, "<ServiceDescription id=\"0\"></ServiceDescription>")?; + writeln!(out, r#"<Period id="0" start="PT0.0S">"#)?; + for (as_id, track) in info.tracks.iter().enumerate() { + let frags = fragment_index(&sinfo, as_id)?; + match track.kind { + TrackKind::Video => { + let max_width = track + .formats + .iter() + .flat_map(|f| f.width) + .max() + .unwrap_or_default(); + let max_height = track + .formats + .iter() + .flat_map(|f| f.height) + .max() + .unwrap_or_default(); + let framerate = "2997/1000"; + let par = "16:9"; // TODO + writeln!( + out, + "<AdaptationSet \ + id=\"{as_id}\" \ + contentType=\"video\" \ + startWithSAP=\"1\" \ + segmentAlignment=\"true\" \ + bitstreamSwitching=\"true\" \ + frameRate=\"{framerate}\" \ + maxWidth=\"{max_width}\" \ + maxHeight=\"{max_height}\" \ + par=\"{par}\" \ + lang=\"eng\">" + )?; + for (repr_id, format) in track.formats.iter().enumerate() { + let StreamFormatInfo { + width: Some(width), + height: Some(height), + bitrate, + .. + } = format + else { + unreachable!() + }; + let container = StreamContainer::WebM; + let container_mime = container.mime_type(); + let codec_param = &format.codec_param; + writeln!( + out, + "<Representation \ + id=\"{repr_id}\" \ + mimeType=\"{container_mime}\" \ + codecs=\"{codec_param}\" \ + bandwidth=\"{bitrate}\" \ + width=\"{width}\" \ + height=\"{height}\" \ + scanType=\"unknown\" \ + sar=\"1:1\">" + )?; + write_segment_template(&mut out, as_id, container, &frags)?; + writeln!(out, "</Representation>")?; + } + writeln!(out, "</AdaptationSet>")?; + } + TrackKind::Audio => { + writeln!( + out, + "<AdaptationSet \ + id=\"{as_id}\" \ + contentType=\"audio\" \ + startWithSAP=\"1\" \ + segmentAlignment=\"true\" \ + bitstreamSwitching=\"true\">" + )?; + for (repr_id, format) in track.formats.iter().enumerate() { + let StreamFormatInfo { + bitrate, + samplerate: Some(samplerate), + .. + } = format + else { + unreachable!() + }; + let container = StreamContainer::WebM; + let container_mime = container.mime_type(); + let codec_param = &format.codec_param; + writeln!( + out, + "<Representation \ + id=\"{repr_id}\" \ + mimeType=\"{container_mime}\" \ + codecs=\"{codec_param}\" \ + bandwidth=\"{bitrate}\" \ + audioSamplingRate=\"{samplerate:.0}\">" + )?; + write_segment_template(&mut out, as_id, container, &frags)?; + writeln!(out, "</Representation>")?; + } + writeln!(out, "</AdaptationSet>")?; + } + TrackKind::Subtitle => (), + } + } + writeln!(out, r#"</Period>"#)?; + + writeln!(out, r#"</MPD>"#)?; + + Ok(Box::new(Cursor::new(out))) +} + +fn write_segment_template( + out: &mut String, + as_id: usize, + container: StreamContainer, + frags: &[Range<f64>], +) -> Result<()> { + writeln!( + out, + "<SegmentTemplate \ + timescale=\"1000\" \ + initialization=\"stream?fragmentinit&t={as_id}&c={container}&f=$RepresentationID$\" \ + media=\"stream?fragment&t={as_id}&c={container}&f=$RepresentationID$&i=$Number$\" \ + startNumber=\"0\">" + )?; + writeln!(out, "{}", Timeline(&frags))?; + writeln!(out, "</SegmentTemplate>")?; + Ok(()) +} + +struct Time(f64); +impl Display for Time { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PT{:.01}S", self.0) + } +} +struct Timeline<'a>(&'a [Range<f64>]); +impl Display for Timeline<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "<SegmentTimeline>")?; + let mut last_t = 0; + for (i, r) in self.0.iter().enumerate() { + let t = (r.start * 1000.) as i64; + let d = t - last_t; + last_t = t; + if i == 0 { + writeln!(f, r#"<S t="0" d="{d}" />"#)?; + } else { + writeln!(f, r#"<S d="{d}" />"#)?; + } + } + writeln!(f, "</SegmentTimeline>")?; + Ok(()) + } +} diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index 62bb0b6..e759c86 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -3,41 +3,35 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{ - SMediaInfo, - cues::{GeneratedCue, generate_cues}, - stream_info, -}; -use anyhow::{Result, anyhow}; +use crate::{SMediaInfo, cues::generate_cues, stream_info}; +use anyhow::{Ok, Result, anyhow}; use jellycache::HashKey; use jellyremuxer::{ ContainerFormat, demuxers::create_demuxer_autodetect, - matroska::{self, Segment}, - muxers::write_fragment, + matroska::{self, Info, Segment}, + muxers::{write_frag, write_init}, }; use jellystream_types::{FormatNum, IndexNum, StreamContainer, TrackNum}; use jellytranscoder::fragment::transcode; use std::{ fs::File, io::{Cursor, Read}, + path::Path, sync::Arc, }; -pub fn fragment_stream( - sinfo: Arc<SMediaInfo>, +pub fn fragment_init( + sinfo: &SMediaInfo, track: TrackNum, - index: IndexNum, format_num: FormatNum, - container: StreamContainer, -) -> Result<Box<dyn Read + Send + Sync>> { +) -> Result<Segment> { let (iinfo, info) = stream_info(&sinfo)?; let (file_index, track_num) = *iinfo .track_to_file .get(track) .ok_or(anyhow!("track not found"))?; - let media_path = iinfo.paths[file_index].clone(); let track = info.tracks.get(track).ok_or(anyhow!("track not found"))?; let format = track .formats @@ -52,33 +46,49 @@ pub fn fragment_stream( .iter() .find(|t| t.track_number == track_num) .unwrap(); - let timestamp_scale = iinfo.metadata[file_index].info.timestamp_scale; - let total_duration = iinfo.metadata[file_index].info.duration; + let duration = iinfo.metadata[file_index].info.duration; + + if !format.remux {} + + Ok(Segment { + tracks: Some(matroska::Tracks { + entries: vec![mk_track.to_owned()], + }), + info: Info { + duration, + timestamp_scale, + ..Default::default() + }, + ..Default::default() + }) +} + +pub fn fragment_init_stream( + sinfo: Arc<SMediaInfo>, + track: TrackNum, + format_num: FormatNum, + container: StreamContainer, +) -> Result<Box<dyn Read + Send + Sync>> { + let init = fragment_init(&sinfo, track, format_num)?; + let mut buf = Vec::new(); + write_init(map_container(container), &mut buf, init)?; + Ok(Box::new(Cursor::new(buf))) +} + +pub fn fragment_remux( + sinfo: &SMediaInfo, + media_path: &Path, + file_track_num: u64, + index: IndexNum, + next_kf: bool, +) -> Result<Segment> { let cue_stat = generate_cues(&sinfo.cache, &media_path)?; let start_cue = cue_stat .cues .get(index) .ok_or(anyhow!("fragment index out of range"))?; - let end_cue = cue_stat - .cues - .get(index + 1) - .copied() - .unwrap_or(GeneratedCue { - position: 0, - time: total_duration.unwrap_or_default() as u64 * timestamp_scale, // TODO rounding? - }); let cluster_offset = start_cue.position; - let duration = (end_cue.time - start_cue.time) as f64 / timestamp_scale as f64; - - let mk_info = matroska::Info { - duration: Some(duration), - timestamp_scale, - ..Default::default() - }; - let mk_tracks = matroska::Tracks { - entries: vec![mk_track.to_owned()], - }; let (mut cluster, next_cluster) = { let media_file = File::open(&media_path)?; @@ -90,49 +100,93 @@ pub fn fragment_stream( .read_cluster()? .ok_or(anyhow!("cluster unexpectedly missing"))? .1, - media.read_cluster()?.map(|(_, x)| x), + next_kf + .then(|| Ok(media.read_cluster()?.map(|(_, x)| x))) + .transpose()? + .flatten(), ) }; - cluster.simple_blocks.retain(|b| b.track == track_num); - cluster.block_groups.retain(|b| b.block.track == track_num); - let next_kf = next_cluster.and_then(|x| { - x.simple_blocks - .iter() - .find(|b| b.track == track_num) - .cloned() - }); + cluster.simple_blocks.retain(|b| b.track == file_track_num); + cluster + .block_groups + .retain(|b| b.block.track == file_track_num); - let jr_container = match container { - StreamContainer::WebM => ContainerFormat::Webm, - StreamContainer::Matroska => ContainerFormat::Matroska, - StreamContainer::WebVTT => todo!(), - StreamContainer::MPEG4 => ContainerFormat::Mpeg4, - StreamContainer::JVTT => todo!(), - }; + let mut clusters = vec![cluster]; + if let Some(next_cluster) = next_cluster { + clusters.push(next_cluster); + } - let mut segment = Segment { - info: mk_info, - tracks: Some(mk_tracks), - clusters: vec![cluster], + Ok(Segment { + clusters, ..Default::default() - }; - segment.info.writing_app = - concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION")).to_string(); + }) +} - if !format.remux { - segment = transcode( +pub fn fragment_stream( + sinfo: Arc<SMediaInfo>, + track_num: TrackNum, + index: IndexNum, + format_num: FormatNum, + container: StreamContainer, +) -> Result<Box<dyn Read + Send + Sync>> { + let (iinfo, info) = stream_info(&sinfo)?; + + let (file_index, file_track_num) = *iinfo + .track_to_file + .get(track_num) + .ok_or(anyhow!("track not found"))?; + let media_path = iinfo.paths[file_index].clone(); + let track = info + .tracks + .get(track_num) + .ok_or(anyhow!("track not found"))?; + let format = track + .formats + .get(format_num) + .ok_or(anyhow!("format not found"))?; + + let segment = if 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(); + + transcode( &sinfo.cache, &sinfo.config.transcoder, track.kind, - &format!("{}-T{track_num}-I{index}", HashKey(media_path)), + &format!("{}-T{file_track_num}-I{index}", HashKey(&media_path)), format, - segment, - next_kf, - )?; - } + mk_track, + || { + 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 + }) + }, + )? + }; let mut out = Vec::new(); - write_fragment(jr_container, &mut out, segment)?; + write_frag(map_container(container), &mut out, segment)?; Ok(Box::new(Cursor::new(out))) } + +fn map_container(container: StreamContainer) -> ContainerFormat { + match container { + StreamContainer::WebM => ContainerFormat::Webm, + StreamContainer::Matroska => ContainerFormat::Matroska, + StreamContainer::WebVTT => todo!(), + StreamContainer::MPEG4 => ContainerFormat::Mpeg4, + StreamContainer::JVTT => todo!(), + } +} diff --git a/stream/src/hls.rs b/stream/src/hls.rs index 9414877..571d2b3 100644 --- a/stream/src/hls.rs +++ b/stream/src/hls.rs @@ -13,10 +13,7 @@ use std::{ ops::Range, }; -pub fn hls_multivariant_stream( - info: &SMediaInfo, - container: StreamContainer, -) -> Result<Box<dyn Read + Send + Sync>> { +pub fn hls_multivariant_stream(info: &SMediaInfo) -> Result<Box<dyn Read + Send + Sync>> { let (_iinfo, info) = stream_info(&info)?; let mut out = String::new(); @@ -29,7 +26,6 @@ pub fn hls_multivariant_stream( "stream{}", StreamSpec::HlsVariant { track: i, - container, format: j } .to_query() @@ -55,7 +51,6 @@ pub fn hls_variant_stream( info: &SMediaInfo, track: TrackNum, format: FormatNum, - container: StreamContainer, ) -> Result<Box<dyn Read + Send + Sync>> { let frags = fragment_index(&info, track)?; let (_, info) = stream_info(&info)?; @@ -75,7 +70,7 @@ pub fn hls_variant_stream( StreamSpec::Fragment { track, index, - container, + container: StreamContainer::MPEG4, format, } .to_query() diff --git a/stream/src/lib.rs b/stream/src/lib.rs index a94f4c7..3f6e93f 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -5,6 +5,7 @@ */ #![feature(iterator_try_collect)] pub mod cues; +pub mod dash; mod fragment; mod fragment_index; mod hls; @@ -17,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::{StreamContainer, StreamSpec}; +use jellystream_types::StreamSpec; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeSet, @@ -29,6 +30,8 @@ use std::{ }; use stream_info::{stream_info, write_stream_info}; +use crate::{dash::dash, fragment::fragment_init_stream}; + #[rustfmt::skip] #[derive(Debug, Deserialize, Serialize, Default)] pub struct Config { @@ -53,24 +56,18 @@ pub struct StreamHead { } pub fn stream_head(spec: &StreamSpec) -> StreamHead { - use StreamContainer::*; use StreamSpec::*; - let container_ct = |x: StreamContainer| match x { - WebM => "video/webm", - Matroska => "video/x-matroska", - WebVTT => "text/vtt", - JVTT => "application/jellything-vtt+json", - MPEG4 => "video/mp4", - }; let range_supported = matches!(spec, Remux { .. } | Original { .. }); let content_type = match spec { Original { .. } => "video/x-matroska", HlsMultiVariant { .. } => "application/vnd.apple.mpegurl", HlsVariant { .. } => "application/vnd.apple.mpegurl", Info => "application/jellything-stream-info+json", + Dash => "application/dash+xml", FragmentIndex { .. } => "application/jellything-frag-index+json", - Fragment { container, .. } => container_ct(*container), - Remux { container, .. } => container_ct(*container), + FragmentInit { container, .. } => container.mime_type(), + Fragment { container, .. } => container.mime_type(), + Remux { container, .. } => container.mime_type(), }; StreamHead { content_type, @@ -85,14 +82,16 @@ pub fn stream( ) -> Result<Box<dyn Read + Send + Sync>> { match spec { StreamSpec::Original { track } => original_stream(info, track, range), - StreamSpec::HlsMultiVariant { container } => hls_multivariant_stream(&info, container), - StreamSpec::HlsVariant { + StreamSpec::HlsMultiVariant => hls_multivariant_stream(&info), + StreamSpec::HlsVariant { track, format } => hls_variant_stream(&info, track, format), + StreamSpec::Info => write_stream_info(&info), + StreamSpec::Dash => dash(&info), + StreamSpec::FragmentIndex { track } => fragment_index_stream(info, track), + StreamSpec::FragmentInit { track, container, format, - } => hls_variant_stream(&info, track, format, container), - StreamSpec::Info => write_stream_info(&info), - StreamSpec::FragmentIndex { track } => fragment_index_stream(info, track), + } => fragment_init_stream(info, track, format, container), StreamSpec::Fragment { track, index, diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs index 3312c71..4a2896f 100644 --- a/stream/src/stream_info.rs +++ b/stream/src/stream_info.rs @@ -75,6 +75,7 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec<St let mut formats = Vec::new(); formats.push(StreamFormatInfo { codec: t.codec_id.to_string(), + codec_param: String::new(), // TODO remux: true, bitrate: remux_bitrate, containers: containers_by_codec(&t.codec_id), @@ -112,6 +113,7 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec<St if enable { formats.push(StreamFormatInfo { codec: cid.to_string(), + codec_param: String::new(), // TODO bitrate: (remux_bitrate * 3.).min(br), remux: false, containers: containers_by_codec(cid), @@ -129,6 +131,7 @@ fn stream_formats(config: &Config, t: &TrackEntry, remux_bitrate: f64) -> Vec<St for br in [256e3, 128e3, 64e3] { formats.push(StreamFormatInfo { codec: "A_OPUS".to_string(), + codec_param: "opus".to_string(), bitrate: br, remux: false, containers: containers_by_codec("A_OPUS"), diff --git a/stream/types/src/lib.rs b/stream/types/src/lib.rs index edafe00..d1583e9 100644 --- a/stream/types/src/lib.rs +++ b/stream/types/src/lib.rs @@ -26,18 +26,21 @@ pub enum StreamSpec { Original { track: TrackNum, }, - HlsMultiVariant { - container: StreamContainer, - }, + HlsMultiVariant, HlsVariant { track: TrackNum, - container: StreamContainer, format: FormatNum, }, + Dash, Info, FragmentIndex { track: TrackNum, }, + FragmentInit { + track: TrackNum, + container: StreamContainer, + format: FormatNum, + }, Fragment { track: TrackNum, index: IndexNum, @@ -77,6 +80,7 @@ pub enum TrackKind { #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct StreamFormatInfo { pub codec: String, + pub codec_param: String, pub bitrate: f64, pub remux: bool, pub containers: Vec<StreamContainer>, @@ -98,6 +102,18 @@ pub enum StreamContainer { JVTT, } +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", + } + } +} + impl Display for TrackKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { @@ -122,18 +138,22 @@ impl StreamSpec { ) } StreamSpec::Original { track } => format!("?original&track={track}"), - StreamSpec::HlsMultiVariant { container } => { - format!("?hlsmultivariant&container={container}") + StreamSpec::HlsMultiVariant => { + format!("?hlsmultivariant") + } + StreamSpec::HlsVariant { track, format } => { + format!("?hlsvariant&track={track}&format={format}") } - StreamSpec::HlsVariant { - track, - container, - format, - } => format!("?hlsvariant&track={track}&container={container}&format={format}"), StreamSpec::Info => "?info".to_string(), + StreamSpec::Dash => "?info".to_string(), StreamSpec::FragmentIndex { track } => { format!("?fragmentindex&track={track}") } + StreamSpec::FragmentInit { + track, + container, + format, + } => format!("?fragmentinit&track={track}&container={container}&format={format}"), StreamSpec::Fragment { track, index, @@ -157,18 +177,22 @@ impl StreamSpec { ) } StreamSpec::Original { track } => format!("?original&t={track}"), - StreamSpec::HlsMultiVariant { container } => { - format!("?hlsmultivariant&c={container}") + StreamSpec::HlsMultiVariant => { + format!("?hlsmultivariant") + } + StreamSpec::HlsVariant { track, format } => { + format!("?hlsvariant&t={track}&f={format}") } - StreamSpec::HlsVariant { - track, - container, - format, - } => format!("?hlsvariant&t={track}&c={container}&f={format}"), StreamSpec::Info => "?info".to_string(), + StreamSpec::Dash => "?dash".to_string(), StreamSpec::FragmentIndex { track } => { format!("?fragmentindex&t={track}") } + StreamSpec::FragmentInit { + track, + container, + format, + } => format!("?fragmentinit&t={track}&c={container}&f={format}"), StreamSpec::Fragment { track, index, @@ -194,14 +218,19 @@ impl StreamSpec { }; if query.contains_key("info") { Ok(Self::Info) + } else if query.contains_key("dash") { + Ok(Self::Dash) } else if query.contains_key("hlsmultivariant") { - Ok(Self::HlsMultiVariant { - container: get_container()?, - }) + Ok(Self::HlsMultiVariant) } else if query.contains_key("hlsvariant") { Ok(Self::HlsVariant { track: get_num("track", "t")? as TrackNum, format: get_num("format", "f")? as FormatNum, + }) + } else if query.contains_key("fragmentinit") { + Ok(Self::FragmentInit { + track: get_num("track", "t")? as TrackNum, + format: get_num("format", "f")? as FormatNum, container: get_container()?, }) } else if query.contains_key("fragment") { |