1use std::io::Cursor;
53use xot::{Xot, output};
54use xot::xmlname::NameStrInfo;
55use xmlparser::{ElementEnd, Token, Tokenizer};
56use tracing::{trace, warn, error};
57use bytes::Bytes;
58use crate::DashMpdError;
59
60
61#[derive(Clone, Debug)]
62pub struct StppDocument {
63 xot: Xot,
64 styles: Vec<xot::Node>,
65 regions: Vec<xot::Node>,
66 paragraphs: Vec<xot::Node>,
68 warned_binary_contents: bool,
69}
70
71impl Default for StppDocument {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl StppDocument {
80 #[must_use]
81 pub fn new() -> StppDocument {
82 StppDocument {
83 xot: Xot::new(),
84 styles: Vec::new(),
85 regions: Vec::new(),
86 paragraphs: Vec::new(),
87 warned_binary_contents: false,
88 }
89 }
90
91
92 pub fn add_from_mp4(&mut self, bytes: &Bytes) -> Result<(), DashMpdError> {
98 use mp4_atom::ReadFrom;
99
100 let mut buf = Cursor::new(bytes);
101 loop {
102 match Option::<mp4_atom::Any>::read_from(&mut buf) {
103 Ok(maybe_atom) => {
104 match maybe_atom {
105 Some(mp4_atom::Any::Mdat(mdat)) => if let Ok(xml) = str::from_utf8(&mdat.data) {
107 self.add_content(xml)?;
108 },
109 Some(_) => (),
110 None => break,
111 }
112 },
113 Err(e) => warn!("Malformed MP4 box: {e:?}"),
114 }
115 }
116 Ok(())
117 }
118
119 pub fn add_bytes(&mut self, bytes: &Bytes) -> Result<(), DashMpdError> {
121 if let Ok(xml) = str::from_utf8(bytes) {
122 self.add_content(xml)?;
123 } else {
124 if !self.warned_binary_contents {
127 warn!("Ignoring invalid XML in STPP subs: {}", String::from_utf8_lossy(bytes));
128 self.warned_binary_contents = true;
129 }
130 }
131 Ok(())
132 }
133
134 fn find_child_named(&mut self, node: xot::Node, name: &str) -> Option<xot::Node> {
135 self.xot.children(node)
136 .find(|n| self.xot.node_name_ref(*n)
137 .is_ok_and(
138 |nn| nn.is_some_and(
139 |nnn| nnn.local_name().eq(name))))
140 }
141
142
143 pub fn add_content(&mut self, xml: &str) -> Result<(), DashMpdError> {
148 trace!("adding STPP content {xml}");
149 let mut clean_xml = xml;
150 let epos = identify_xml_endpos(xml)
151 .map_err(|_| DashMpdError::Parsing(String::from("calculating XML endpos")))?;
152 if epos < xml.len() {
153 clean_xml = &clean_xml[0..epos];
154 }
155 let root = self.xot.parse(clean_xml)
156 .map_err(|e| {
157 error!("Failure parsing STPP XML: {e:?}");
158 error!("Failing XML: {xml}");
159 DashMpdError::Parsing(String::from("parsing STPP XML"))
160 })?;
161 let tt = self.xot.document_element(root)
162 .map_err(|_| DashMpdError::Parsing(String::from("extracting STPP XML root")))?;
163 if !self.xot.element(tt).is_some_and(
164 |n| self.xot.name_ns_str(n.name()).0.eq("tt")) {
165 warn!("Missing tt root element in STPP XML: {xml}");
166 return Ok(());
167 }
168 let xml_ns = self.xot.add_namespace("http://www.w3.org/XML/1998/namespace");
169 let id_name = self.xot.add_name_ns("id", xml_ns);
170 if let Some(head) = self.find_child_named(tt, "head") {
171 for d in self.xot.descendants(head) {
172 if self.xot.element(d).is_some_and(|n| self.xot.name_ns_str(n.name()).0.eq("style")) {
173 if let Some(new_id) = self.xot.attributes(d).get(id_name) {
175 if !self.styles.iter().any(|s| self.xot.attributes(*s)
176 .get(id_name)
177 .is_some_and(|id| id.eq(new_id))) {
178 self.styles.push(d);
179 }
180 } else {
181 self.styles.push(d);
183 }
184 }
185 }
186 for d in self.xot.descendants(head) {
187 if self.xot.element(d).is_some_and(|n| self.xot.name_ns_str(n.name()).0.eq("region")) {
188 if let Some(new_id) = self.xot.attributes(d).get(id_name) {
189 if !self.regions.iter().any(|s| self.xot.attributes(*s)
190 .get(id_name).is_some_and(|id| id.eq(new_id))) {
191 self.regions.push(d);
192 }
193 } else {
194 self.regions.push(d);
196 }
197 }
198 }
199 }
200 if let Some(body) = self.find_child_named(tt, "body") {
201 for d in self.xot.children(body) {
203 self.paragraphs.push(d);
204 }
205 }
206 Ok(())
207 }
208
209 #[allow(clippy::inherent_to_string)]
213 pub fn to_string(&mut self) -> String {
214 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>"#;
215 let ttml_ns = self.xot.add_namespace("http://www.w3.org/ns/ttml");
216 let root = self.xot.parse(empty_xml).unwrap();
217 let tt = self.xot.document_element(root).unwrap();
218 let head_name = self.xot.add_name_ns("head", ttml_ns);
219 let head = self.xot.new_element(head_name);
220 self.xot.create_missing_prefixes(head).unwrap();
221 let _ = self.xot.append(tt, head);
222 let body_name = self.xot.add_name_ns("body", ttml_ns);
223 let body = self.xot.new_element(body_name);
224 self.xot.create_missing_prefixes(body).unwrap();
225 let _ = self.xot.append(tt, body);
226 let div_name = self.xot.add_name_ns("div", ttml_ns);
227 let div = self.xot.new_element(div_name);
228 let _ = self.xot.append(body, div);
229 let styling_name = self.xot.add_name_ns("styling", ttml_ns);
230 let styling = self.xot.new_element(styling_name);
231 self.xot.create_missing_prefixes(styling).unwrap();
232 let _ = self.xot.append(head, styling);
233 for s in &self.styles {
234 let new = self.xot.clone_with_prefixes(*s);
235 let _ = self.xot.append(styling, new);
236 }
237 let layout_name = self.xot.add_name_ns("layout", ttml_ns);
238 let layout = self.xot.new_element(layout_name);
239 self.xot.create_missing_prefixes(layout).unwrap();
240 let _ = self.xot.append(head,layout);
241 for r in &self.regions {
242 let new = self.xot.clone_with_prefixes(*r);
243 let _ = self.xot.append(layout, new);
244 }
245 for p in &self.paragraphs {
246 let new = self.xot.clone_with_prefixes(*p);
247 let _ = self.xot.append(div, new);
248 }
249 self.xot.create_missing_prefixes(tt).unwrap();
250 self.xot.deduplicate_namespaces(tt);
251 self.xot.serialize_xml_string(output::xml::Parameters {
252 declaration: Some(output::xml::Declaration {
253 encoding: Some("UTF-8".to_string()),
254 ..Default::default()
255 }),
256 ..Default::default()
257 }, tt).unwrap()
258 }
259}
260
261
262fn identify_xml_endpos(input: &str) -> Result<usize, xmlparser::Error> {
265 let mut depth = 0;
266 let mut xml_end;
267 for token in Tokenizer::from(input) {
268 let token = token?;
269 xml_end = token.span().end();
270 match token {
271 Token::ElementStart { .. } => depth += 1,
272 Token::ElementEnd { end: ElementEnd::Close(..), .. } => {
273 depth -= 1;
274 if depth == 0 {
275 return Ok(xml_end);
277 }
278 },
279 Token::ElementEnd { end: ElementEnd::Empty, .. } => {
280 depth -= 1;
281 if depth == 0 {
282 return Ok(xml_end);
284 }
285 },
286 _ => {}
287 }
288 }
289 Err(xmlparser::Error::UnknownToken(
290 xmlparser::TextPos::new(1, 1),
291 ))
292}
293