diff options
Diffstat (limited to 'stream/src/dash.rs')
| -rw-r--r-- | stream/src/dash.rs | 190 |
1 files changed, 190 insertions, 0 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(()) + } +} |