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";
#[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 start_number: u64,
pub init_template: String,
pub media_template: String,
pub availability_start_time: Option<String>,
pub minimum_update_period: Option<String>,
pub trick_mode: Option<TrickModeAdaptationSet>,
}
impl Default for DashPackager {
fn default() -> Self {
Self {
profiles: PROFILE_ISOFF_LIVE.to_string(),
dynamic: false,
start_number: 1,
init_template: String::from("init-stream$RepresentationID$.m4s"),
media_template: String::from("chunk-stream$RepresentationID$-$Number$.m4s"),
availability_start_time: None,
minimum_update_period: None,
trick_mode: None,
}
}
}
struct ReprInfo {
id: String,
kind: MediaKind,
codecs: String,
bandwidth: u64,
timescale: u32,
total_duration: u64,
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(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 mut info = ReprInfo {
id: track.spec.track_id.to_string(),
kind,
codecs,
bandwidth,
timescale,
total_duration,
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.dynamic {
if let Some(ast) = &self.availability_start_time {
mpd_attrs.push(("availabilityStartTime", ast.clone()));
}
if let Some(mup) = &self.minimum_update_period {
mpd_attrs.push(("minimumUpdatePeriod", mup.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 attrs = alloc::vec![
("contentType", kind.name().to_string()),
("mimeType", kind.mime_type().to_string()),
("segmentAlignment", "true".to_string()),
("startWithSAP", "1".to_string()),
];
w.open("AdaptationSet", &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()),
],
);
}
}
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");
}
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)?);
}
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)
}
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),
}
}
}