use chrono::{DateTime, Utc};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct Representation {
pub id: String,
pub bandwidth: u64,
pub width: u32,
pub height: u32,
pub codecs: String,
pub init_url: String,
pub media_template: String,
}
pub struct MpdBuilder {
mpd_type: String,
availability_start_time: Option<DateTime<Utc>>,
min_buffer_time: Duration,
suggested_presentation_delay: Option<Duration>,
time_shift_buffer_depth: Option<Duration>,
video_representations: Vec<Representation>,
audio_representations: Vec<Representation>,
segment_duration: Duration,
low_latency: Option<Duration>,
}
impl MpdBuilder {
#[must_use]
pub fn new() -> Self {
Self {
mpd_type: "static".to_string(),
availability_start_time: None,
min_buffer_time: Duration::from_secs(2),
suggested_presentation_delay: None,
time_shift_buffer_depth: None,
video_representations: Vec::new(),
audio_representations: Vec::new(),
segment_duration: Duration::from_secs(2),
low_latency: None,
}
}
#[must_use]
pub fn live(mut self) -> Self {
self.mpd_type = "dynamic".to_string();
self.availability_start_time = Some(Utc::now());
self
}
#[must_use]
pub fn availability_start_time(mut self, time: DateTime<Utc>) -> Self {
self.availability_start_time = Some(time);
self
}
#[must_use]
pub const fn min_buffer_time(mut self, duration: Duration) -> Self {
self.min_buffer_time = duration;
self
}
#[must_use]
pub fn suggested_presentation_delay(mut self, duration: Duration) -> Self {
self.suggested_presentation_delay = Some(duration);
self
}
#[must_use]
pub fn time_shift_buffer_depth(mut self, duration: Duration) -> Self {
self.time_shift_buffer_depth = Some(duration);
self
}
#[must_use]
pub const fn segment_duration(mut self, duration: Duration) -> Self {
self.segment_duration = duration;
self
}
#[must_use]
pub fn low_latency(mut self, chunk_duration: Duration) -> Self {
self.low_latency = Some(chunk_duration);
self
}
#[must_use]
pub fn add_video_representation(
mut self,
id: String,
bandwidth: u64,
width: u32,
height: u32,
codecs: String,
) -> Self {
self.video_representations.push(Representation {
id: id.clone(),
bandwidth,
width,
height,
codecs,
init_url: format!("init_video_{id}.mp4"),
media_template: format!("video_$Number$_{id}.m4s"),
});
self
}
#[must_use]
pub fn add_audio_representation(mut self, id: String, bandwidth: u64, codecs: String) -> Self {
self.audio_representations.push(Representation {
id: id.clone(),
bandwidth,
width: 0,
height: 0,
codecs,
init_url: format!("init_audio_{id}.mp4"),
media_template: format!("audio_$Number$_{id}.m4s"),
});
self
}
#[must_use]
pub fn build(self) -> String {
let mut mpd = String::new();
mpd.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
mpd.push_str("<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\" ");
mpd.push_str("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
mpd.push_str("xsi:schemaLocation=\"urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\" ");
mpd.push_str(&format!("type=\"{}\" ", self.mpd_type));
if let Some(start_time) = self.availability_start_time {
mpd.push_str(&format!(
"availabilityStartTime=\"{}\" ",
start_time.to_rfc3339()
));
}
mpd.push_str(&format!(
"minBufferTime=\"PT{}S\" ",
self.min_buffer_time.as_secs()
));
if let Some(delay) = self.suggested_presentation_delay {
mpd.push_str(&format!(
"suggestedPresentationDelay=\"PT{}S\" ",
delay.as_secs()
));
}
if let Some(depth) = self.time_shift_buffer_depth {
mpd.push_str(&format!("timeShiftBufferDepth=\"PT{}S\" ", depth.as_secs()));
}
mpd.push_str("profiles=\"urn:mpeg:dash:profile:isoff-live:2011\">\n");
mpd.push_str(" <Period>\n");
if !self.video_representations.is_empty() {
mpd.push_str(" <AdaptationSet mimeType=\"video/mp4\" ");
mpd.push_str("segmentAlignment=\"true\" ");
mpd.push_str("startWithSAP=\"1\">\n");
for repr in &self.video_representations {
mpd.push_str(&format!(" <Representation id=\"{}\" ", repr.id));
mpd.push_str(&format!("bandwidth=\"{}\" ", repr.bandwidth));
mpd.push_str(&format!("width=\"{}\" ", repr.width));
mpd.push_str(&format!("height=\"{}\" ", repr.height));
mpd.push_str(&format!("codecs=\"{}\">\n", repr.codecs));
mpd.push_str(" <SegmentTemplate ");
mpd.push_str(&format!(
"timescale=\"1000\" duration=\"{}\" ",
self.segment_duration.as_millis()
));
mpd.push_str(&format!("initialization=\"{}\" ", repr.init_url));
mpd.push_str(&format!("media=\"{}\" ", repr.media_template));
mpd.push_str("startNumber=\"1\"/>\n");
mpd.push_str(" </Representation>\n");
}
mpd.push_str(" </AdaptationSet>\n");
}
if !self.audio_representations.is_empty() {
mpd.push_str(" <AdaptationSet mimeType=\"audio/mp4\" ");
mpd.push_str("segmentAlignment=\"true\" ");
mpd.push_str("startWithSAP=\"1\">\n");
for repr in &self.audio_representations {
mpd.push_str(&format!(" <Representation id=\"{}\" ", repr.id));
mpd.push_str(&format!("bandwidth=\"{}\" ", repr.bandwidth));
mpd.push_str(&format!("codecs=\"{}\">\n", repr.codecs));
mpd.push_str(" <SegmentTemplate ");
mpd.push_str(&format!(
"timescale=\"1000\" duration=\"{}\" ",
self.segment_duration.as_millis()
));
mpd.push_str(&format!("initialization=\"{}\" ", repr.init_url));
mpd.push_str(&format!("media=\"{}\" ", repr.media_template));
mpd.push_str("startNumber=\"1\"/>\n");
mpd.push_str(" </Representation>\n");
}
mpd.push_str(" </AdaptationSet>\n");
}
mpd.push_str(" </Period>\n");
mpd.push_str("</MPD>\n");
mpd
}
}
impl Default for MpdBuilder {
fn default() -> Self {
Self::new()
}
}