aboutsummaryrefslogtreecommitdiff
path: root/stream/src/dash.rs
diff options
context:
space:
mode:
Diffstat (limited to 'stream/src/dash.rs')
-rw-r--r--stream/src/dash.rs190
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&amp;t={as_id}&amp;c={container}&amp;f=$RepresentationID$\" \
+ media=\"stream?fragment&amp;t={as_id}&amp;c={container}&amp;f=$RepresentationID$&amp;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(())
+ }
+}