use std::io::Cursor;
use xot::{Xot, output};
use xot::xmlname::NameStrInfo;
use xmlparser::{ElementEnd, Token, Tokenizer};
use tracing::{trace, warn, error};
use bytes::Bytes;
use crate::DashMpdError;
#[derive(Clone, Debug)]
pub struct StppDocument {
xot: Xot,
styles: Vec<xot::Node>,
regions: Vec<xot::Node>,
paragraphs: Vec<xot::Node>,
warned_binary_contents: bool,
}
impl Default for StppDocument {
fn default() -> Self {
Self::new()
}
}
impl StppDocument {
#[must_use]
pub fn new() -> StppDocument {
StppDocument {
xot: Xot::new(),
styles: Vec::new(),
regions: Vec::new(),
paragraphs: Vec::new(),
warned_binary_contents: false,
}
}
pub fn add_from_mp4(&mut self, bytes: &Bytes) -> Result<(), DashMpdError> {
use mp4_atom::ReadFrom;
let mut buf = Cursor::new(bytes);
loop {
match Option::<mp4_atom::Any>::read_from(&mut buf) {
Ok(maybe_atom) => {
match maybe_atom {
Some(mp4_atom::Any::Mdat(mdat)) => if let Ok(xml) = str::from_utf8(&mdat.data) {
self.add_content(xml)?;
},
Some(_) => (),
None => break,
}
},
Err(e) => warn!("Malformed MP4 box: {e:?}"),
}
}
Ok(())
}
pub fn add_bytes(&mut self, bytes: &Bytes) -> Result<(), DashMpdError> {
if let Ok(xml) = str::from_utf8(bytes) {
self.add_content(xml)?;
} else {
if !self.warned_binary_contents {
warn!("Ignoring invalid XML in STPP subs: {}", String::from_utf8_lossy(bytes));
self.warned_binary_contents = true;
}
}
Ok(())
}
fn find_child_named(&mut self, node: xot::Node, name: &str) -> Option<xot::Node> {
self.xot.children(node)
.find(|n| self.xot.node_name_ref(*n)
.is_ok_and(
|nn| nn.is_some_and(
|nnn| nnn.local_name().eq(name))))
}
pub fn add_content(&mut self, xml: &str) -> Result<(), DashMpdError> {
trace!("adding STPP content {xml}");
let mut clean_xml = xml;
let epos = identify_xml_endpos(xml)
.map_err(|_| DashMpdError::Parsing(String::from("calculating XML endpos")))?;
if epos < xml.len() {
clean_xml = &clean_xml[0..epos];
}
let root = self.xot.parse(clean_xml)
.map_err(|e| {
error!("Failure parsing STPP XML: {e:?}");
error!("Failing XML: {xml}");
DashMpdError::Parsing(String::from("parsing STPP XML"))
})?;
let tt = self.xot.document_element(root)
.map_err(|_| DashMpdError::Parsing(String::from("extracting STPP XML root")))?;
if !self.xot.element(tt).is_some_and(
|n| self.xot.name_ns_str(n.name()).0.eq("tt")) {
warn!("Missing tt root element in STPP XML: {xml}");
return Ok(());
}
let xml_ns = self.xot.add_namespace("http://www.w3.org/XML/1998/namespace");
let id_name = self.xot.add_name_ns("id", xml_ns);
if let Some(head) = self.find_child_named(tt, "head") {
for d in self.xot.descendants(head) {
if self.xot.element(d).is_some_and(|n| self.xot.name_ns_str(n.name()).0.eq("style")) {
if let Some(new_id) = self.xot.attributes(d).get(id_name) {
if !self.styles.iter().any(|s| self.xot.attributes(*s)
.get(id_name)
.is_some_and(|id| id.eq(new_id))) {
self.styles.push(d);
}
} else {
self.styles.push(d);
}
}
}
for d in self.xot.descendants(head) {
if self.xot.element(d).is_some_and(|n| self.xot.name_ns_str(n.name()).0.eq("region")) {
if let Some(new_id) = self.xot.attributes(d).get(id_name) {
if !self.regions.iter().any(|s| self.xot.attributes(*s)
.get(id_name).is_some_and(|id| id.eq(new_id))) {
self.regions.push(d);
}
} else {
self.regions.push(d);
}
}
}
}
if let Some(body) = self.find_child_named(tt, "body") {
for d in self.xot.children(body) {
self.paragraphs.push(d);
}
}
Ok(())
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(&mut self) -> String {
let empty_xml = r#"<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:xml="http://www.w3.org/XML/1998/namespace" xml:lang="fr"></tt>"#;
let ttml_ns = self.xot.add_namespace("http://www.w3.org/ns/ttml");
let root = self.xot.parse(empty_xml).unwrap();
let tt = self.xot.document_element(root).unwrap();
let head_name = self.xot.add_name_ns("head", ttml_ns);
let head = self.xot.new_element(head_name);
self.xot.create_missing_prefixes(head).unwrap();
let _ = self.xot.append(tt, head);
let body_name = self.xot.add_name_ns("body", ttml_ns);
let body = self.xot.new_element(body_name);
self.xot.create_missing_prefixes(body).unwrap();
let _ = self.xot.append(tt, body);
let div_name = self.xot.add_name_ns("div", ttml_ns);
let div = self.xot.new_element(div_name);
let _ = self.xot.append(body, div);
let styling_name = self.xot.add_name_ns("styling", ttml_ns);
let styling = self.xot.new_element(styling_name);
self.xot.create_missing_prefixes(styling).unwrap();
let _ = self.xot.append(head, styling);
for s in &self.styles {
let new = self.xot.clone_with_prefixes(*s);
let _ = self.xot.append(styling, new);
}
let layout_name = self.xot.add_name_ns("layout", ttml_ns);
let layout = self.xot.new_element(layout_name);
self.xot.create_missing_prefixes(layout).unwrap();
let _ = self.xot.append(head,layout);
for r in &self.regions {
let new = self.xot.clone_with_prefixes(*r);
let _ = self.xot.append(layout, new);
}
for p in &self.paragraphs {
let new = self.xot.clone_with_prefixes(*p);
let _ = self.xot.append(div, new);
}
self.xot.create_missing_prefixes(tt).unwrap();
self.xot.deduplicate_namespaces(tt);
self.xot.serialize_xml_string(output::xml::Parameters {
declaration: Some(output::xml::Declaration {
encoding: Some("UTF-8".to_string()),
..Default::default()
}),
..Default::default()
}, tt).unwrap()
}
}
fn identify_xml_endpos(input: &str) -> Result<usize, xmlparser::Error> {
let mut depth = 0;
let mut xml_end;
for token in Tokenizer::from(input) {
let token = token?;
xml_end = token.span().end();
match token {
Token::ElementStart { .. } => depth += 1,
Token::ElementEnd { end: ElementEnd::Close(..), .. } => {
depth -= 1;
if depth == 0 {
return Ok(xml_end);
}
},
Token::ElementEnd { end: ElementEnd::Empty, .. } => {
depth -= 1;
if depth == 0 {
return Ok(xml_end);
}
},
_ => {}
}
}
Err(xmlparser::Error::UnknownToken(
xmlparser::TextPos::new(1, 1),
))
}