use alloc::string::{String, ToString};
use quick_xml::Reader;
use quick_xml::Writer;
use quick_xml::events::{BytesEnd, BytesStart, Event};
use broadcast_common::Package;
use crate::dash::DashPackager;
use crate::error::{Error, Result};
use crate::media::Media;
const MPD_ELEMENT: &str = "MPD";
const SEGMENT_TEMPLATE: &str = "SegmentTemplate";
const BASE_MPD_NOT_WELL_FORMED: &str = "base MPD is not well-formed XML";
#[derive(Debug, Clone)]
pub struct LlDashPackager {
pub base: DashPackager,
pub segment_duration_secs: f64,
pub chunk_duration_secs: f64,
pub latency_target_ms: u32,
pub playback_rate: Option<(f64, f64)>,
pub utc_timing: Option<(String, String)>,
}
pub const UTCTIMING_HTTP_HEAD_2014: &str = "urn:mpeg:dash:utc:http-head:2014";
pub const UTCTIMING_HTTP_XSDATE_2014: &str = "urn:mpeg:dash:utc:http-xsdate:2014";
pub const UTCTIMING_HTTP_ISO_2014: &str = "urn:mpeg:dash:utc:http-iso:2014";
pub const UTCTIMING_NTP_2014: &str = "urn:mpeg:dash:utc:ntp:2014";
pub const UTCTIMING_HTTP_NTP_2014: &str = "urn:mpeg:dash:utc:http-ntp:2014";
pub const UTCTIMING_DIRECT_2014: &str = "urn:mpeg:dash:utc:direct:2014";
impl LlDashPackager {
pub fn new(
segment_duration_secs: f64,
chunk_duration_secs: f64,
latency_target_ms: u32,
availability_start_time: impl Into<String>,
) -> Result<Self> {
if !(segment_duration_secs.is_finite() && segment_duration_secs > 0.0) {
return Err(Error::InvalidInput(
"segment_duration_secs must be positive and finite",
));
}
if !(chunk_duration_secs.is_finite() && chunk_duration_secs > 0.0) {
return Err(Error::InvalidInput(
"chunk_duration_secs must be positive and finite",
));
}
if chunk_duration_secs > segment_duration_secs {
return Err(Error::InvalidInput(
"chunk_duration_secs must not exceed segment_duration_secs",
));
}
let base = DashPackager {
dynamic: true,
availability_start_time: Some(availability_start_time.into()),
..DashPackager::default()
};
Ok(Self {
base,
segment_duration_secs,
chunk_duration_secs,
latency_target_ms,
playback_rate: None,
utc_timing: None,
})
}
pub fn with_playback_rate(mut self, min: f64, max: f64) -> Self {
self.playback_rate = Some((min, max));
self
}
pub fn with_utc_timing(mut self, scheme: impl Into<String>, value: impl Into<String>) -> Self {
self.utc_timing = Some((scheme.into(), value.into()));
self
}
pub fn availability_time_offset(&self) -> f64 {
(self.segment_duration_secs - self.chunk_duration_secs).max(0.0)
}
}
impl Package for LlDashPackager {
type Media = Media;
type Output = String;
type Error = Error;
fn package(&mut self, media: &Media) -> Result<String> {
let base_xml = self.base.package(media)?;
self.inject_ll(&base_xml)
}
}
impl LlDashPackager {
fn inject_ll(&self, xml: &str) -> Result<String> {
let ato_str = format_secs(self.availability_time_offset());
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
let mut utc_written = false;
loop {
let event = reader
.read_event()
.map_err(|_| Error::InvalidInput(BASE_MPD_NOT_WELL_FORMED))?;
match event {
Event::Eof => break,
Event::Start(e) if e.name().as_ref() == SEGMENT_TEMPLATE => {
let _ = writer.write_event(Event::Start(self.ll_template(&e, &ato_str)));
}
Event::Empty(e) if e.name().as_ref() == SEGMENT_TEMPLATE => {
let _ = writer.write_event(Event::Empty(self.ll_template(&e, &ato_str)));
}
Event::Start(e) if e.name().as_ref() == MPD_ELEMENT => {
let _ = writer.write_event(Event::Start(e));
self.write_service_description(&mut writer);
}
Event::End(e) if e.name().as_ref() == "Period" && !utc_written => {
let _ = writer.write_event(Event::End(e));
if let Some((scheme, value)) = &self.utc_timing {
let mut timing = BytesStart::new("UTCTiming");
timing.push_attribute(("schemeIdUri", scheme.as_str()));
timing.push_attribute(("value", value.as_str()));
let _ = writer.write_event(Event::Empty(timing));
}
utc_written = true;
}
other => {
let _ = writer.write_event(other);
}
}
}
let mut out = String::from_utf8_lossy(&writer.into_inner()).into_owned();
out.push('\n');
Ok(out)
}
fn ll_template<'a>(&self, tag: &BytesStart<'a>, ato: &str) -> BytesStart<'a> {
let mut out = tag.to_owned();
out.push_attribute(("availabilityTimeOffset", ato));
out.push_attribute(("availabilityTimeComplete", "false"));
out
}
fn write_service_description(&self, w: &mut Writer<Vec<u8>>) {
let mut sd = BytesStart::new("ServiceDescription");
sd.push_attribute(("id", "0"));
let _ = w.write_event(Event::Start(sd));
let mut latency = BytesStart::new("Latency");
latency.push_attribute(("target", self.latency_target_ms.to_string().as_str()));
let _ = w.write_event(Event::Empty(latency));
if let Some((min, max)) = self.playback_rate {
let mut rate = BytesStart::new("PlaybackRate");
rate.push_attribute(("min", format_secs(min).as_str()));
rate.push_attribute(("max", format_secs(max).as_str()));
let _ = w.write_event(Event::Empty(rate));
}
let _ = w.write_event(Event::End(BytesEnd::new("ServiceDescription")));
}
}
fn format_secs(v: f64) -> String {
let millis = (v * 1000.0 + 0.5) as u64;
let whole = millis / 1000;
let frac = millis % 1000;
if frac == 0 {
return whole.to_string();
}
let mut f = format!("{frac:03}");
while f.ends_with('0') {
f.pop();
}
format!("{whole}.{f}")
}