use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
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 broadcast_common::Parse;
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";
const ISO_639_LANGUAGE_DESCRIPTOR_TAG: u8 = 0x0A;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
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]>,
}
#[derive(Debug, Clone)]
pub struct InbandEventStream {
pub scheme_id_uri: String,
pub value: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
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>,
width: Option<u32>,
height: Option<u32>,
frame_rate: Option<String>,
audio_sampling_rate: Option<u32>,
audio_channels: Option<u16>,
}
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 = asc_from_esds(esds)?;
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" }),
}
}
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 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 mut info = ReprInfo {
id: track.spec.track_id.to_string(),
kind,
codecs,
bandwidth,
timescale,
total_duration,
segment_durations,
lang,
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 = asc_from_esds(esds).ok();
info.audio_sampling_rate = Some(
asc.as_ref()
.and_then(asc_sampling_rate)
.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 { .. } => {}
}
Ok(info)
}
fn render(&self, reprs: &[ReprInfo]) -> String {
let mut w = XmlWriter::new();
w.declaration();
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", "PT2.0S".to_string()),
];
if self
.content_protection
.iter()
.any(|c| c.default_kid.is_some())
{
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_tenths(max_tenths)));
}
w.open("MPD", &mpd_attrs);
w.open(
"Period",
&[("id", "0".to_string()), ("start", "PT0.0S".to_string())],
);
for kind in [MediaKind::Video, MediaKind::Audio] {
let set: Vec<&ReprInfo> = reprs.iter().filter(|r| r.kind == kind).collect();
if set.is_empty() {
continue;
}
self.write_adaptation_set(&mut w, kind, &set);
}
if let Some(tm) = &self.trick_mode {
self.write_trick_adaptation_set(&mut w, tm);
}
w.close("Period");
w.close("MPD");
w.finish()
}
fn write_adaptation_set(&self, w: &mut XmlWriter, kind: MediaKind, set: &[&ReprInfo]) {
let mut attrs = alloc::vec![
("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));
}
w.open("AdaptationSet", &attrs);
w.empty(
"Role",
&[
("schemeIdUri", ROLE_SCHEME.to_string()),
("value", ROLE_MAIN.to_string()),
],
);
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()));
}
w.empty("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()));
}
w.open("Representation", &rattrs);
if kind == MediaKind::Audio {
if let Some(ch) = r.audio_channels {
w.empty(
"AudioChannelConfiguration",
&[
("schemeIdUri", AUDIO_CHANNEL_SCHEME.to_string()),
("value", ch.to_string()),
],
);
}
}
self.write_segment_template(w, r);
w.close("Representation");
}
w.close("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,
};
w.empty(
"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)");
w.open(
"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);
w.close("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()));
w.open("AdaptationSet", &as_attrs);
w.empty(
"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()));
}
w.open("Representation", &rattrs);
w.empty(
"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()),
],
);
w.close("Representation");
w.close("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 asc_from_esds(esds: &crate::mp4esds::EsdsBox) -> Result<AudioSpecificConfig> {
let dsi = esds
.es_descriptor
.decoder_config
.as_ref()
.and_then(|dc| dc.decoder_specific_info.as_ref())
.ok_or(Error::UnexpectedBox {
expected: "DecoderSpecificInfo (AudioSpecificConfig) in esds",
})?;
AudioSpecificConfig::parse(&dsi.data)
}
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 asc_sampling_rate(asc: &AudioSpecificConfig) -> Option<u32> {
if let Some(fs) = asc.sampling_frequency {
return Some(fs);
}
const RATES: [u32; 13] = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
];
RATES
.get(asc.sampling_frequency_index.raw() as usize)
.copied()
}
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 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_tenths(tenths: u64) -> String {
format!("PT{}.{}S", tenths / 10, tenths % 10)
}
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) {
if let Ok(s) = core::str::from_utf8(code) {
return Some(s.to_ascii_lowercase());
}
}
}
i = body_end;
}
None
}
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)));
}
w.empty("ContentProtection", &attrs);
}
fn format_kid(kid: &[u8; 16]) -> String {
let hex = hex_lower(kid);
format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32]
)
}
fn hex_lower(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
fn write_segment_timeline(w: &mut XmlWriter, durations: &[u64]) {
w.open("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()));
}
w.empty("S", &attrs);
t += d * run as u64;
idx += run;
}
w.close("SegmentTimeline");
}
struct XmlWriter {
buf: String,
depth: usize,
}
impl XmlWriter {
fn new() -> Self {
Self {
buf: String::new(),
depth: 0,
}
}
fn declaration(&mut self) {
self.buf
.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
}
fn indent(&mut self) {
for _ in 0..self.depth {
self.buf.push_str(" ");
}
}
fn attrs(&mut self, attrs: &[(&str, String)]) {
for (k, v) in attrs {
self.buf.push(' ');
self.buf.push_str(k);
self.buf.push_str("=\"");
escape_into(&mut self.buf, v);
self.buf.push('"');
}
}
fn open(&mut self, name: &str, attrs: &[(&str, String)]) {
self.indent();
self.buf.push('<');
self.buf.push_str(name);
self.attrs(attrs);
self.buf.push_str(">\n");
self.depth += 1;
}
fn empty(&mut self, name: &str, attrs: &[(&str, String)]) {
self.indent();
self.buf.push('<');
self.buf.push_str(name);
self.attrs(attrs);
self.buf.push_str("/>\n");
}
fn close(&mut self, name: &str) {
self.depth = self.depth.saturating_sub(1);
self.indent();
self.buf.push_str("</");
self.buf.push_str(name);
self.buf.push_str(">\n");
}
fn finish(self) -> String {
self.buf
}
}
fn escape_into(out: &mut String, s: &str) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
}