use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::time::Duration;
use crate::aac_asc::AudioSpecificConfig;
use crate::error::{Error, Result};
use crate::media::{Media, Track};
use crate::pipeline::CodecConfig;
use crate::sps::rfc6381_avc1;
use quick_xml::Writer;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
pub const MPD_NAMESPACE: &str = "urn:mpeg:dash:schema:mpd:2011";
pub const PROFILE_ISOFF_LIVE: &str = "urn:mpeg:dash:profile:isoff-live:2011";
const AUDIO_CHANNEL_SCHEME: &str = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011";
pub const TRICKMODE_SCHEME: &str = "urn:mpeg:dash:trickmode:2016";
const MIME_VIDEO: &str = "video/mp4";
const MIME_AUDIO: &str = "audio/mp4";
const CODECS_VP8: &str = "vp8";
const CODECS_VORBIS: &str = "vorbis";
const ROLE_SCHEME: &str = "urn:mpeg:dash:role:2011";
const ROLE_MAIN: &str = "main";
const CENC_NAMESPACE: &str = "urn:mpeg:cenc:2013";
pub const MP4_PROTECTION_SCHEME_URI: &str = "urn:mpeg:dash:mp4protection:2011";
const ISO_639_LANGUAGE_DESCRIPTOR_TAG: u8 = 0x0A;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Addressing {
#[default]
Number,
Timeline,
}
impl Addressing {
pub fn name(&self) -> &'static str {
match self {
Addressing::Number => "number",
Addressing::Timeline => "timeline",
}
}
}
broadcast_common::impl_spec_display!(Addressing);
#[derive(Debug, Clone)]
pub struct TrackSegments {
pub track_id: u32,
pub durations: Vec<u64>,
}
#[derive(Debug, Clone)]
pub struct ContentProtectionSystem {
pub scheme_id_uri: String,
pub value: Option<String>,
pub default_kid: Option<[u8; 16]>,
pub pssh: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct InbandEventStream {
pub scheme_id_uri: String,
pub value: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MediaKind {
Video,
Audio,
}
impl MediaKind {
pub fn name(&self) -> &'static str {
match self {
MediaKind::Video => "video",
MediaKind::Audio => "audio",
}
}
fn mime_type(&self) -> &'static str {
match self {
MediaKind::Video => MIME_VIDEO,
MediaKind::Audio => MIME_AUDIO,
}
}
fn of(config: &CodecConfig) -> Self {
if is_audio(config) {
MediaKind::Audio
} else {
MediaKind::Video
}
}
}
broadcast_common::impl_spec_display!(MediaKind);
fn is_audio(config: &CodecConfig) -> bool {
matches!(
config,
CodecConfig::Aac { .. }
| CodecConfig::Ac3 { .. }
| CodecConfig::Eac3 { .. }
| CodecConfig::Opus { .. }
| CodecConfig::Flac { .. }
| CodecConfig::Ac4 { .. }
| CodecConfig::MpegH { .. }
| CodecConfig::Dts { .. }
| CodecConfig::MpegAudio { .. }
| CodecConfig::Vorbis { .. }
)
}
#[derive(Debug, Clone)]
pub struct TrickModeAdaptationSet {
pub id: String,
pub main_adaptation_set_id: String,
pub max_playout_rate: u32,
pub repr: TrickModeRepr,
}
#[derive(Debug, Clone)]
pub struct TrickModeRepr {
pub id: String,
pub codecs: String,
pub bandwidth: u64,
pub width: Option<u32>,
pub height: Option<u32>,
pub timescale: u32,
pub total_duration: u64,
}
#[derive(Debug, Clone)]
pub struct DashPackager {
pub profiles: String,
pub dynamic: bool,
pub addressing: Addressing,
pub segments: Vec<TrackSegments>,
pub start_number: u64,
pub init_template: String,
pub media_template: String,
pub media_template_time: String,
pub availability_start_time: Option<String>,
pub publish_time: Option<String>,
pub minimum_update_period: Option<String>,
pub time_shift_buffer_depth: Option<String>,
pub suggested_presentation_delay: Option<String>,
pub content_protection: Vec<ContentProtectionSystem>,
pub inband_event_streams: Vec<InbandEventStream>,
pub trick_mode: Option<TrickModeAdaptationSet>,
}
impl Default for DashPackager {
fn default() -> Self {
Self {
profiles: PROFILE_ISOFF_LIVE.to_string(),
dynamic: false,
addressing: Addressing::Number,
segments: Vec::new(),
start_number: 1,
init_template: String::from("init-stream$RepresentationID$.m4s"),
media_template: String::from("chunk-stream$RepresentationID$-$Number$.m4s"),
media_template_time: String::from("chunk-stream$RepresentationID$-$Time$.m4s"),
availability_start_time: None,
publish_time: None,
minimum_update_period: None,
time_shift_buffer_depth: None,
suggested_presentation_delay: None,
content_protection: Vec::new(),
inband_event_streams: Vec::new(),
trick_mode: None,
}
}
}
struct ReprInfo {
id: String,
kind: MediaKind,
codecs: String,
bandwidth: u64,
timescale: u32,
total_duration: u64,
segment_durations: Option<Vec<u64>>,
lang: Option<String>,
encryption_scheme: Option<crate::cenc::CencScheme>,
encryption_kid: Option<[u8; 16]>,
width: Option<u32>,
height: Option<u32>,
frame_rate: Option<String>,
audio_sampling_rate: Option<u32>,
audio_channels: Option<u16>,
}
pub fn rfc6381_codec_string(config: &CodecConfig) -> Result<String> {
DashPackager::codec_string(config)
}
impl DashPackager {
pub fn with_profiles(profiles: impl Into<String>) -> Self {
Self {
profiles: profiles.into(),
..Self::default()
}
}
fn codec_string(config: &CodecConfig) -> Result<String> {
match config {
CodecConfig::Avc { config, .. } => Ok(rfc6381_avc1(
config.config.profile_indication,
config.config.profile_compatibility,
config.config.level_indication,
)),
CodecConfig::Hevc { config, .. } => Ok(config.config.rfc6381()),
CodecConfig::Vvc { config, .. } => Ok(config.config.rfc6381()),
CodecConfig::Aac { esds, .. } => {
let asc = esds.audio_specific_config()?;
Ok(asc.rfc6381())
}
CodecConfig::Ac3 { config, .. } => Ok(config.rfc6381().to_string()),
CodecConfig::Eac3 { config, .. } => Ok(config.rfc6381().to_string()),
CodecConfig::Opus { config, .. } => Ok(config.rfc6381().to_string()),
CodecConfig::Flac { config, .. } => Ok(config.rfc6381().to_string()),
CodecConfig::Ac4 { config, .. } => Ok(config.rfc6381().to_string()),
CodecConfig::Av1 { config, .. } => Ok(config.rfc6381()),
CodecConfig::Vp9 { config, .. } => Ok(config.rfc6381()),
CodecConfig::MpegH { config, .. } => Ok(config.rfc6381()),
CodecConfig::Dts { codec_fourcc, .. } => {
Ok(crate::dts::DtsSpecificBox::rfc6381(codec_fourcc).to_string())
}
CodecConfig::Mpeg2Video { esds, .. } => Ok(format!("mp4v.{:02X}", oti_of(esds))),
CodecConfig::MpegAudio { esds, .. } => Ok(format!("mp4a.{:02X}", oti_of(esds))),
CodecConfig::Vp8 { .. } => Ok(CODECS_VP8.to_string()),
CodecConfig::Vorbis { .. } => Ok(CODECS_VORBIS.to_string()),
CodecConfig::Data { .. } => Err(Error::UnsupportedCodec { codec: "Data" }),
CodecConfig::Subtitle { .. } => Err(Error::UnsupportedCodec { codec: "Subtitle" }),
}
}
fn repr_info(&self, track: &Track) -> Result<ReprInfo> {
let config = &track.spec.config;
let timescale = track.spec.timescale.max(1);
let total_duration: u64 = track
.samples
.iter()
.map(|s| s.duration.unwrap_or(0) as u64)
.sum();
let total_bytes: u64 = track.samples.iter().map(|s| s.data.len() as u64).sum();
let bandwidth = if total_duration > 0 {
let bits_times_ts = total_bytes
.saturating_mul(8)
.saturating_mul(timescale as u64);
div_round(bits_times_ts, total_duration)
} else {
0
}
.max(1);
let kind = MediaKind::of(config);
let codecs = Self::codec_string(config)?;
let segment_durations = self
.segments
.iter()
.find(|s| s.track_id == track.spec.track_id)
.map(|s| s.durations.clone());
let lang = if kind == MediaKind::Audio {
lang_from_es_info(&track.spec.es_info_descriptors)
} else {
None
};
let (encryption_scheme, encryption_kid) = match &track.encryption {
Some(enc) => (Some(enc.scheme), Some(enc.tenc.default_kid)),
None => (None, None),
};
let mut info = ReprInfo {
id: track.spec.track_id.to_string(),
kind,
codecs,
bandwidth,
timescale,
total_duration,
segment_durations,
lang,
encryption_scheme,
encryption_kid,
width: None,
height: None,
frame_rate: None,
audio_sampling_rate: None,
audio_channels: None,
};
match config {
CodecConfig::Avc {
config: avc,
width,
height,
} => {
let (w, h) = avc
.config
.sps
.first()
.and_then(|sps| sps.decode().ok())
.map(|i| (i.width, i.height))
.unwrap_or((*width as u32, *height as u32));
info.width = Some(w);
info.height = Some(h);
info.frame_rate = frame_rate_from_samples(&track.samples, info.timescale);
}
CodecConfig::Hevc { width, height, .. }
| CodecConfig::Vvc { width, height, .. }
| CodecConfig::Av1 { width, height, .. }
| CodecConfig::Vp9 { width, height, .. }
| CodecConfig::Vp8 { width, height, .. }
| CodecConfig::Mpeg2Video { width, height, .. } => {
info.width = Some(*width as u32);
info.height = Some(*height as u32);
info.frame_rate = frame_rate_from_samples(&track.samples, info.timescale);
}
CodecConfig::Aac {
sample_rate,
channel_count,
esds,
..
} => {
let asc = esds.audio_specific_config().ok();
info.audio_sampling_rate = Some(
asc.as_ref()
.and_then(AudioSpecificConfig::effective_sampling_frequency)
.unwrap_or(*sample_rate),
);
info.audio_channels = Some(*channel_count);
}
CodecConfig::Ac3 {
sample_rate,
channel_count,
..
}
| CodecConfig::Eac3 {
sample_rate,
channel_count,
..
}
| CodecConfig::Opus {
sample_rate,
channel_count,
..
}
| CodecConfig::Flac {
sample_rate,
channel_count,
..
}
| CodecConfig::Ac4 {
sample_rate,
channel_count,
..
}
| CodecConfig::MpegH {
sample_rate,
channel_count,
..
}
| CodecConfig::Dts {
sample_rate,
channel_count,
..
}
| CodecConfig::MpegAudio {
sample_rate,
channel_count,
..
} => {
info.audio_sampling_rate = Some(*sample_rate);
info.audio_channels = Some(*channel_count);
}
CodecConfig::Vorbis {
sample_rate,
channels,
..
} => {
info.audio_sampling_rate = Some(*sample_rate);
info.audio_channels = Some(*channels);
}
CodecConfig::Data { .. } => {}
CodecConfig::Subtitle { .. } => {}
}
Ok(info)
}
fn render(&self, reprs: &[ReprInfo]) -> String {
let mut writer = new_writer();
let w = &mut writer;
declaration(w);
let mut mpd_attrs = alloc::vec![
("xmlns", MPD_NAMESPACE.to_string()),
("profiles", self.profiles.clone()),
(
"type",
if self.dynamic { "dynamic" } else { "static" }.to_string(),
),
("minBufferTime", xs_duration(Duration::from_secs(2))),
];
let cenc_namespace_needed = self
.content_protection
.iter()
.any(|c| c.default_kid.is_some() || c.pssh.is_some())
|| reprs.iter().any(|r| r.encryption_scheme.is_some());
if cenc_namespace_needed {
mpd_attrs.push(("xmlns:cenc", CENC_NAMESPACE.to_string()));
}
if self.dynamic {
if let Some(ast) = &self.availability_start_time {
mpd_attrs.push(("availabilityStartTime", ast.clone()));
}
if let Some(pt) = &self.publish_time {
mpd_attrs.push(("publishTime", pt.clone()));
}
if let Some(mup) = &self.minimum_update_period {
mpd_attrs.push(("minimumUpdatePeriod", mup.clone()));
}
if let Some(tsbd) = &self.time_shift_buffer_depth {
mpd_attrs.push(("timeShiftBufferDepth", tsbd.clone()));
}
if let Some(spd) = &self.suggested_presentation_delay {
mpd_attrs.push(("suggestedPresentationDelay", spd.clone()));
}
} else {
let max_tenths = reprs
.iter()
.map(|r| div_round(r.total_duration.saturating_mul(10), r.timescale as u64))
.max()
.unwrap_or(0);
mpd_attrs.push((
"mediaPresentationDuration",
xs_duration(Duration::from_millis(max_tenths.saturating_mul(100))),
));
}
open(w, "MPD", &mpd_attrs);
open(
w,
"Period",
&[
("id", "0".to_string()),
("start", xs_duration(Duration::ZERO)),
],
);
let mut next_id = 0u32;
for kind in [MediaKind::Video, MediaKind::Audio] {
let groups = adaption_set_groups(reprs, kind);
for group in groups {
let id = loop {
let candidate = format!("t{next_id}");
next_id += 1;
if self.trick_mode.as_ref().is_none_or(|tm| tm.id != candidate) {
break candidate;
}
};
self.write_adaptation_set(w, kind, &group, &id);
}
}
if let Some(tm) = &self.trick_mode {
self.write_trick_adaptation_set(w, tm);
}
close(w, "Period");
close(w, "MPD");
finish(writer)
}
fn write_adaptation_set(
&self,
w: &mut XmlWriter,
kind: MediaKind,
set: &[&ReprInfo],
id: &str,
) {
let mut attrs = alloc::vec![
("id", id.to_string()),
("contentType", kind.name().to_string()),
("mimeType", kind.mime_type().to_string()),
("segmentAlignment", "true".to_string()),
("startWithSAP", "1".to_string()),
];
if let Some(lang) = common_lang(set) {
attrs.push(("lang", lang));
}
open(w, "AdaptationSet", &attrs);
empty(
w,
"Role",
&[
("schemeIdUri", ROLE_SCHEME.to_string()),
("value", ROLE_MAIN.to_string()),
],
);
if let Some((scheme, kid)) = set
.iter()
.find_map(|r| r.encryption_scheme.zip(r.encryption_kid))
{
write_content_protection(
w,
&ContentProtectionSystem {
scheme_id_uri: MP4_PROTECTION_SCHEME_URI.to_string(),
value: Some(scheme.name().to_string()),
default_kid: Some(kid),
pssh: None,
},
);
}
for cp in &self.content_protection {
write_content_protection(w, cp);
}
if kind == MediaKind::Video {
for ies in &self.inband_event_streams {
let mut ies_attrs = alloc::vec![("schemeIdUri", ies.scheme_id_uri.clone())];
if let Some(v) = &ies.value {
ies_attrs.push(("value", v.clone()));
}
empty(w, "InbandEventStream", &ies_attrs);
}
}
for r in set {
let mut rattrs = alloc::vec![
("id", r.id.clone()),
("mimeType", kind.mime_type().to_string()),
("codecs", r.codecs.clone()),
("bandwidth", r.bandwidth.to_string()),
];
if let (Some(wd), Some(ht)) = (r.width, r.height) {
rattrs.push(("width", wd.to_string()));
rattrs.push(("height", ht.to_string()));
}
if let Some(fr) = &r.frame_rate {
rattrs.push(("frameRate", fr.clone()));
}
if let Some(sr) = r.audio_sampling_rate {
rattrs.push(("audioSamplingRate", sr.to_string()));
}
open(w, "Representation", &rattrs);
if kind == MediaKind::Audio
&& let Some(ch) = r.audio_channels
{
empty(
w,
"AudioChannelConfiguration",
&[
("schemeIdUri", AUDIO_CHANNEL_SCHEME.to_string()),
("value", ch.to_string()),
],
);
}
self.write_segment_template(w, r);
close(w, "Representation");
}
close(w, "AdaptationSet");
}
fn write_segment_template(&self, w: &mut XmlWriter, r: &ReprInfo) {
match self.addressing {
Addressing::Number => {
let duration = match &r.segment_durations {
Some(durs) if !durs.is_empty() => durs[0],
_ => r.total_duration,
};
empty(
w,
"SegmentTemplate",
&[
("timescale", r.timescale.to_string()),
("duration", duration.to_string()),
("startNumber", self.start_number.to_string()),
("initialization", self.init_template.clone()),
("media", self.media_template.clone()),
],
);
}
Addressing::Timeline => {
let durations = r
.segment_durations
.as_ref()
.expect("Addressing::Timeline requires segment_durations (validated earlier)");
open(
w,
"SegmentTemplate",
&[
("timescale", r.timescale.to_string()),
("startNumber", self.start_number.to_string()),
("initialization", self.init_template.clone()),
("media", self.media_template_time.clone()),
],
);
write_segment_timeline(w, durations);
close(w, "SegmentTemplate");
}
}
}
fn write_trick_adaptation_set(&self, w: &mut XmlWriter, tm: &TrickModeAdaptationSet) {
let mut as_attrs = alloc::vec![
("id", tm.id.clone()),
("contentType", "video".to_string()),
("mimeType", MIME_VIDEO.to_string()),
("maxPlayoutRate", tm.max_playout_rate.to_string()),
("codingDependency", "false".to_string()),
];
as_attrs.push(("segmentAlignment", "true".to_string()));
open(w, "AdaptationSet", &as_attrs);
empty(
w,
"SupplementalProperty",
&[
("schemeIdUri", TRICKMODE_SCHEME.to_string()),
("value", tm.main_adaptation_set_id.clone()),
],
);
let r = &tm.repr;
let mut rattrs = alloc::vec![
("id", r.id.clone()),
("mimeType", MIME_VIDEO.to_string()),
("codecs", r.codecs.clone()),
("bandwidth", r.bandwidth.to_string()),
];
if let (Some(w_px), Some(h_px)) = (r.width, r.height) {
rattrs.push(("width", w_px.to_string()));
rattrs.push(("height", h_px.to_string()));
}
open(w, "Representation", &rattrs);
empty(
w,
"SegmentTemplate",
&[
("timescale", r.timescale.to_string()),
("duration", r.total_duration.to_string()),
("startNumber", self.start_number.to_string()),
("initialization", self.init_template.clone()),
("media", self.media_template.clone()),
],
);
close(w, "Representation");
close(w, "AdaptationSet");
}
}
impl broadcast_common::Package for DashPackager {
type Media = Media;
type Output = String;
type Error = Error;
fn package(&mut self, media: &Media) -> Result<String> {
if media.tracks.is_empty() {
return Err(Error::InvalidInput("cannot package a Media with no tracks"));
}
let mut reprs = Vec::with_capacity(media.tracks.len());
for t in &media.tracks {
reprs.push(self.repr_info(t)?);
}
if self.addressing == Addressing::Timeline {
for r in &reprs {
if r.segment_durations.as_ref().is_none_or(Vec::is_empty) {
return Err(Error::InvalidInput(
"Addressing::Timeline requires a non-empty TrackSegments entry \
in DashPackager::segments for every track",
));
}
}
}
Ok(self.render(&reprs))
}
}
fn oti_of(esds: &crate::mp4esds::EsdsBox) -> u8 {
esds.es_descriptor
.decoder_config
.as_ref()
.map_or(0, |dc| dc.object_type_indication.0)
}
fn frame_rate_from_samples(samples: &[crate::pipeline::Sample], timescale: u32) -> Option<String> {
if samples.is_empty() {
return None;
}
let total: u64 = samples.iter().map(|s| s.duration.unwrap_or(0) as u64).sum();
if total == 0 {
return None;
}
let avg = total / samples.len() as u64;
if avg == 0 {
return None;
}
let num = timescale as u64;
let den = avg;
let g = gcd(num, den);
Some(format!("{}/{}", num / g, den / g))
}
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a.max(1)
}
fn div_round(num: u64, den: u64) -> u64 {
(num + den / 2) / den
}
fn xs_duration(d: Duration) -> String {
let signed = jiff::SignedDuration::try_from(d).unwrap_or(jiff::SignedDuration::MAX);
jiff::fmt::temporal::SpanPrinter::new().duration_to_string(&signed)
}
fn lang_from_es_info(descriptors: &[u8]) -> Option<String> {
let mut i = 0usize;
while i + 2 <= descriptors.len() {
let tag = descriptors[i];
let len = descriptors[i + 1] as usize;
let body_start = i + 2;
let body_end = body_start + len;
if body_end > descriptors.len() {
break;
}
if tag == ISO_639_LANGUAGE_DESCRIPTOR_TAG && len >= 4 {
let code = &descriptors[body_start..body_start + 3];
if code.iter().all(u8::is_ascii_alphabetic)
&& let Ok(s) = core::str::from_utf8(code)
{
return Some(s.to_ascii_lowercase());
}
}
i = body_end;
}
None
}
fn adaption_set_groups<'a>(reprs: &'a [ReprInfo], kind: MediaKind) -> Vec<Vec<&'a ReprInfo>> {
let mut groups: Vec<(Option<String>, String, Vec<&'a ReprInfo>)> = Vec::new();
for r in reprs.iter().filter(|r| r.kind == kind) {
let family = codec_family(&r.codecs).to_string();
match groups.iter_mut().find(|(lang, fam, _)| {
*fam == family && language_key(lang.as_deref()) == language_key(r.lang.as_deref())
}) {
Some((_, _, set)) => set.push(r),
None => groups.push((r.lang.clone(), family, alloc::vec![r])),
}
}
groups.into_iter().map(|(_, _, set)| set).collect()
}
fn codec_family(codecs: &str) -> &str {
let base = codecs.split(';').next().unwrap_or(codecs).trim();
base.split('.').next().unwrap_or(base)
}
fn language_key(lang: Option<&str>) -> Option<&str> {
match lang {
None | Some("und") => None,
Some(l) => Some(l),
}
}
fn common_lang(set: &[&ReprInfo]) -> Option<String> {
let first = set.first()?.lang.clone()?;
if set
.iter()
.all(|r| r.lang.as_deref() == Some(first.as_str()))
{
Some(first)
} else {
None
}
}
fn write_content_protection(w: &mut XmlWriter, cp: &ContentProtectionSystem) {
let mut attrs = alloc::vec![("schemeIdUri", cp.scheme_id_uri.clone())];
if let Some(v) = &cp.value {
attrs.push(("value", v.clone()));
}
if let Some(kid) = &cp.default_kid {
attrs.push(("cenc:default_KID", format_kid(kid)));
}
match &cp.pssh {
Some(pssh) => {
open(w, "ContentProtection", &attrs);
text_element(w, "cenc:pssh", &crate::rtp::base64_encode(pssh));
close(w, "ContentProtection");
}
None => empty(w, "ContentProtection", &attrs),
}
}
fn format_kid(kid: &[u8; 16]) -> String {
let hex = ::hex::encode(kid);
format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32]
)
}
fn write_segment_timeline(w: &mut XmlWriter, durations: &[u64]) {
open(w, "SegmentTimeline", &[]);
let mut t: u64 = 0;
let mut idx = 0usize;
let mut first = true;
while idx < durations.len() {
let d = durations[idx];
let mut run = 1usize;
while idx + run < durations.len() && durations[idx + run] == d {
run += 1;
}
let mut attrs: Vec<(&str, String)> = Vec::new();
if first {
attrs.push(("t", t.to_string()));
first = false;
}
attrs.push(("d", d.to_string()));
if run > 1 {
attrs.push(("r", (run - 1).to_string()));
}
empty(w, "S", &attrs);
t += d * run as u64;
idx += run;
}
close(w, "SegmentTimeline");
}
type XmlWriter = Writer<Vec<u8>>;
fn new_writer() -> XmlWriter {
Writer::new_with_indent(Vec::new(), b' ', 2)
}
fn declaration(w: &mut XmlWriter) {
let _ = w.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)));
}
fn element_tag<'a>(name: &'a str, attrs: &'a [(&str, String)]) -> BytesStart<'a> {
let mut start = BytesStart::new(name);
for (key, value) in attrs {
start.push_attribute((*key, value.as_str()));
}
start
}
fn open(w: &mut XmlWriter, name: &str, attrs: &[(&str, String)]) {
let _ = w.write_event(Event::Start(element_tag(name, attrs)));
}
fn empty(w: &mut XmlWriter, name: &str, attrs: &[(&str, String)]) {
let _ = w.write_event(Event::Empty(element_tag(name, attrs)));
}
fn close(w: &mut XmlWriter, name: &str) {
let _ = w.write_event(Event::End(BytesEnd::new(name)));
}
fn text_element(w: &mut XmlWriter, name: &str, text: &str) {
open(w, name, &[]);
let _ = w.write_event(Event::Text(BytesText::new(text)));
close(w, name);
}
fn finish(w: XmlWriter) -> String {
let mut out = String::from_utf8_lossy(&w.into_inner()).into_owned();
out.push('\n');
out
}
#[cfg(test)]
mod xs_duration_tests {
use super::xs_duration;
use core::time::Duration;
#[test]
fn xs_duration_is_the_shortest_iso_8601_form() {
assert_eq!(xs_duration(Duration::from_secs(2)), "PT2S");
assert_eq!(xs_duration(Duration::ZERO), "PT0S");
assert_eq!(xs_duration(Duration::from_millis(2500)), "PT2.5S");
assert_eq!(xs_duration(Duration::from_secs(90)), "PT1M30S");
assert_eq!(xs_duration(Duration::from_secs(3600)), "PT1H");
for d in [
Duration::from_millis(100),
Duration::from_secs(7200),
Duration::new(61, 500_000_000),
] {
assert_eq!(
crate::dash_parse::parse_iso8601_duration(&xs_duration(d)).unwrap(),
d
);
}
}
}