onvif: Switch from minidom to xmltree for parsing ONVIF timed metadata

minidom doesn't handle various valid but suboptimal XML documents.
This commit is contained in:
Sebastian Dröge 2022-10-12 18:01:07 +03:00 committed by Sebastian Dröge
parent 97e0852156
commit b2ddb34258
4 changed files with 287 additions and 243 deletions

View file

@ -15,11 +15,11 @@ gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/g
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] } gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
once_cell = "1.0" once_cell = "1.0"
xmlparser = "0.13" xmlparser = "0.13"
minidom = "0.15"
chrono = { version = "0.4", default-features = false } chrono = { version = "0.4", default-features = false }
cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core", features=["use_glib"] } cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core", features=["use_glib"] }
pango = { git = "https://github.com/gtk-rs/gtk-rs-core" } pango = { git = "https://github.com/gtk-rs/gtk-rs-core" }
pangocairo = { git = "https://github.com/gtk-rs/gtk-rs-core" } pangocairo = { git = "https://github.com/gtk-rs/gtk-rs-core" }
xmltree = "0.10"
[lib] [lib]
name = "gstrsonvif" name = "gstrsonvif"

View file

@ -21,6 +21,9 @@ mod onvifmetadataoverlay;
mod onvifmetadataparse; mod onvifmetadataparse;
mod onvifmetadatapay; mod onvifmetadatapay;
// ONVIF Timed Metadata schema
pub(crate) const ONVIF_METADATA_SCHEMA: &str = "http://www.onvif.org/ver10/schema";
// Offset in nanoseconds from midnight 01-01-1900 (prime epoch) to // Offset in nanoseconds from midnight 01-01-1900 (prime epoch) to
// midnight 01-01-1970 (UNIX epoch) // midnight 01-01-1970 (UNIX epoch)
pub(crate) const PRIME_EPOCH_OFFSET: gst::ClockTime = gst::ClockTime::from_seconds(2_208_988_800); pub(crate) const PRIME_EPOCH_OFFSET: gst::ClockTime = gst::ClockTime::from_seconds(2_208_988_800);
@ -43,7 +46,7 @@ pub(crate) fn lookup_reference_timestamp(buffer: &gst::Buffer) -> Option<gst::Cl
None None
} }
pub(crate) fn xml_from_buffer(buffer: &gst::Buffer) -> Result<minidom::Element, gst::ErrorMessage> { pub(crate) fn xml_from_buffer(buffer: &gst::Buffer) -> Result<xmltree::Element, gst::ErrorMessage> {
let map = buffer.map_readable().map_err(|_| { let map = buffer.map_readable().map_err(|_| {
gst::error_msg!(gst::ResourceError::Read, ["Failed to map buffer readable"]) gst::error_msg!(gst::ResourceError::Read, ["Failed to map buffer readable"])
})?; })?;
@ -55,7 +58,7 @@ pub(crate) fn xml_from_buffer(buffer: &gst::Buffer) -> Result<minidom::Element,
) )
})?; })?;
let root = utf8.parse::<minidom::Element>().map_err(|err| { let root = xmltree::Element::parse(std::io::Cursor::new(utf8)).map_err(|err| {
gst::error_msg!( gst::error_msg!(
gst::ResourceError::Read, gst::ResourceError::Read,
["Failed to parse buffer as XML: {}", err] ["Failed to parse buffer as XML: {}", err]
@ -66,40 +69,45 @@ pub(crate) fn xml_from_buffer(buffer: &gst::Buffer) -> Result<minidom::Element,
} }
pub(crate) fn iterate_video_analytics_frames( pub(crate) fn iterate_video_analytics_frames(
root: &minidom::Element, root: &xmltree::Element,
) -> impl Iterator< ) -> impl Iterator<
Item = Result<(chrono::DateTime<chrono::FixedOffset>, &minidom::Element), gst::ErrorMessage>, Item = Result<(chrono::DateTime<chrono::FixedOffset>, &xmltree::Element), gst::ErrorMessage>,
> { > {
root.get_child("VideoAnalytics", "http://www.onvif.org/ver10/schema") root.get_child(("VideoAnalytics", ONVIF_METADATA_SCHEMA))
.map(|analytics| { .map(|analytics| {
analytics.children().filter_map(|el| { analytics
// We are only interested in associating Frame metadata with video frames .children
if el.is("Frame", "http://www.onvif.org/ver10/schema") { .iter()
let timestamp = match el.attr("UtcTime") { .filter_map(|n| n.as_element())
Some(timestamp) => timestamp, .filter_map(|el| {
None => { // We are only interested in associating Frame metadata with video frames
return Some(Err(gst::error_msg!( if el.name == "Frame" && el.namespace.as_deref() == Some(ONVIF_METADATA_SCHEMA)
gst::ResourceError::Read, {
["Frame element has no UtcTime attribute"] let timestamp = match el.attributes.get("UtcTime") {
))); Some(timestamp) => timestamp,
} None => {
}; return Some(Err(gst::error_msg!(
gst::ResourceError::Read,
["Frame element has no UtcTime attribute"]
)));
}
};
let dt = match chrono::DateTime::parse_from_rfc3339(timestamp) { let dt = match chrono::DateTime::parse_from_rfc3339(timestamp) {
Ok(dt) => dt, Ok(dt) => dt,
Err(err) => { Err(err) => {
return Some(Err(gst::error_msg!( return Some(Err(gst::error_msg!(
gst::ResourceError::Read, gst::ResourceError::Read,
["Failed to parse UtcTime {}: {}", timestamp, err] ["Failed to parse UtcTime {}: {}", timestamp, err]
))); )));
} }
}; };
Some(Ok((dt, el))) Some(Ok((dt, el)))
} else { } else {
None None
} }
}) })
}) })
.into_iter() .into_iter()
.flatten() .flatten()

View file

@ -9,8 +9,6 @@ use once_cell::sync::Lazy;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Mutex; use std::sync::Mutex;
use minidom::Element;
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| { static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new( gst::DebugCategory::new(
"onvifmetadataoverlay", "onvifmetadataoverlay",
@ -445,212 +443,231 @@ impl OnvifMetadataOverlay {
gst::FlowError::Error gst::FlowError::Error
})?; })?;
let root = utf8.parse::<Element>().map_err(|err| { let root =
gst::element_imp_error!( xmltree::Element::parse(std::io::Cursor::new(utf8)).map_err(|err| {
self, gst::element_imp_error!(
gst::ResourceError::Read, self,
["Failed to parse buffer as XML: {}", err] gst::ResourceError::Read,
); ["Failed to parse buffer as XML: {}", err]
);
gst::FlowError::Error gst::FlowError::Error
})?; })?;
for object in root for object in root
.get_child("VideoAnalytics", "http://www.onvif.org/ver10/schema") .get_child(("VideoAnalytics", crate::ONVIF_METADATA_SCHEMA))
.map(|el| el.children().into_iter().collect()) .map(|e| e.children.iter().filter_map(|n| n.as_element()))
.unwrap_or_else(Vec::new) .into_iter()
.flatten()
{ {
if object.is("Frame", "http://www.onvif.org/ver10/schema") { if object.name == "Frame"
for object in object.children() { && object.namespace.as_deref() == Some(crate::ONVIF_METADATA_SCHEMA)
if object.is("Object", "http://www.onvif.org/ver10/schema") { {
gst::trace!(CAT, imp: self, "Handling object {:?}", object); for object in object
.children
.iter()
.filter_map(|n| n.as_element())
.filter(|e| {
e.name == "Object"
&& e.namespace.as_deref()
== Some(crate::ONVIF_METADATA_SCHEMA)
})
{
gst::trace!(CAT, imp: self, "Handling object {:?}", object);
let object_id = match object.attr("ObjectId") { let object_id = match object.attributes.get("ObjectId") {
Some(id) => id.to_string(), Some(id) => id.to_string(),
None => { None => {
gst::warning!( gst::warning!(
CAT,
imp: self,
"XML Object with no ObjectId"
);
continue;
}
};
if !object_ids.insert(object_id.clone()) {
gst::debug!(
CAT, CAT,
"Skipping older version of object {}", imp: self,
object_id "XML Object with no ObjectId"
); );
continue; continue;
} }
};
let appearance = match object.get_child( if !object_ids.insert(object_id.clone()) {
"Appearance", gst::debug!(
"http://www.onvif.org/ver10/schema", CAT,
) { "Skipping older version of object {}",
Some(appearance) => appearance, object_id
None => continue, );
}; continue;
}
let shape = match appearance let appearance = match object
.get_child("Shape", "http://www.onvif.org/ver10/schema") .get_child(("Appearance", crate::ONVIF_METADATA_SCHEMA))
{
Some(appearance) => appearance,
None => continue,
};
let shape = match appearance
.get_child(("Shape", crate::ONVIF_METADATA_SCHEMA))
{
Some(shape) => shape,
None => continue,
};
let tag = appearance
.get_child(("Class", crate::ONVIF_METADATA_SCHEMA))
.and_then(|class| {
class.get_child(("Type", crate::ONVIF_METADATA_SCHEMA))
})
.and_then(|t| t.get_text())
.map(|t| t.into_owned());
let bbox = match shape
.get_child(("BoundingBox", crate::ONVIF_METADATA_SCHEMA))
{
Some(bbox) => bbox,
None => {
gst::warning!(
CAT,
imp: self,
"XML Shape with no BoundingBox"
);
continue;
}
};
let left: f64 = match bbox
.attributes
.get("left")
.and_then(|val| val.parse().ok())
{
Some(val) => val,
None => {
gst::warning!(
CAT,
imp: self,
"BoundingBox with no left attribute"
);
continue;
}
};
let right: f64 = match bbox
.attributes
.get("right")
.and_then(|val| val.parse().ok())
{
Some(val) => val,
None => {
gst::warning!(
CAT,
imp: self,
"BoundingBox with no right attribute"
);
continue;
}
};
let top: f64 = match bbox
.attributes
.get("top")
.and_then(|val| val.parse().ok())
{
Some(val) => val,
None => {
gst::warning!(
CAT,
imp: self,
"BoundingBox with no top attribute"
);
continue;
}
};
let bottom: f64 = match bbox
.attributes
.get("bottom")
.and_then(|val| val.parse().ok())
{
Some(val) => val,
None => {
gst::warning!(
CAT,
imp: self,
"BoundingBox with no bottom attribute"
);
continue;
}
};
let x1 = width / 2 + ((left * (width / 2) as f64) as i32);
let y1 = height / 2 - ((top * (height / 2) as f64) as i32);
let x2 = width / 2 + ((right * (width / 2) as f64) as i32);
let y2 = height / 2 - ((bottom * (height / 2) as f64) as i32);
let w = (x2 - x1) as u32;
let h = (y2 - y1) as u32;
let mut points = vec![];
if let Some(polygon) =
shape.get_child(("Polygon", crate::ONVIF_METADATA_SCHEMA))
{
for point in
polygon.children.iter().filter_map(|n| n.as_element())
{ {
Some(shape) => shape, if point.name == "Point"
None => continue, && point.namespace.as_deref()
}; == Some(crate::ONVIF_METADATA_SCHEMA)
{
let tag = appearance let px: f64 = match point
.get_child("Class", "http://www.onvif.org/ver10/schema") .attributes
.and_then(|class| { .get("x")
class.get_child( .and_then(|val| val.parse().ok())
"Type",
"http://www.onvif.org/ver10/schema",
)
})
.map(|t| t.text());
let bbox = match shape.get_child(
"BoundingBox",
"http://www.onvif.org/ver10/schema",
) {
Some(bbox) => bbox,
None => {
gst::warning!(
CAT,
imp: self,
"XML Shape with no BoundingBox"
);
continue;
}
};
let left: f64 =
match bbox.attr("left").and_then(|val| val.parse().ok()) {
Some(val) => val,
None => {
gst::warning!(
CAT,
imp: self,
"BoundingBox with no left attribute"
);
continue;
}
};
let right: f64 =
match bbox.attr("right").and_then(|val| val.parse().ok()) {
Some(val) => val,
None => {
gst::warning!(
CAT,
imp: self,
"BoundingBox with no right attribute"
);
continue;
}
};
let top: f64 =
match bbox.attr("top").and_then(|val| val.parse().ok()) {
Some(val) => val,
None => {
gst::warning!(
CAT,
imp: self,
"BoundingBox with no top attribute"
);
continue;
}
};
let bottom: f64 = match bbox
.attr("bottom")
.and_then(|val| val.parse().ok())
{
Some(val) => val,
None => {
gst::warning!(
CAT,
imp: self,
"BoundingBox with no bottom attribute"
);
continue;
}
};
let x1 = width / 2 + ((left * (width / 2) as f64) as i32);
let y1 = height / 2 - ((top * (height / 2) as f64) as i32);
let x2 = width / 2 + ((right * (width / 2) as f64) as i32);
let y2 = height / 2 - ((bottom * (height / 2) as f64) as i32);
let w = (x2 - x1) as u32;
let h = (y2 - y1) as u32;
let mut points = vec![];
if let Some(polygon) = shape
.get_child("Polygon", "http://www.onvif.org/ver10/schema")
{
for point in polygon.children() {
if point
.is("Point", "http://www.onvif.org/ver10/schema")
{ {
let px: f64 = match point Some(val) => val,
.attr("x") None => {
.and_then(|val| val.parse().ok()) gst::warning!(
{ CAT,
Some(val) => val, imp: self,
None => { "Point with no x attribute"
gst::warning!( );
CAT, continue;
imp: self, }
"Point with no x attribute" };
);
continue;
}
};
let py: f64 = match point let py: f64 = match point
.attr("y") .attributes
.and_then(|val| val.parse().ok()) .get("y")
{ .and_then(|val| val.parse().ok())
Some(val) => val, {
None => { Some(val) => val,
gst::warning!( None => {
CAT, gst::warning!(
imp: self, CAT,
"Point with no y attribute" imp: self,
); "Point with no y attribute"
continue; );
} continue;
}; }
};
let px = let px = width / 2 + ((px * (width / 2) as f64) as i32);
width / 2 + ((px * (width / 2) as f64) as i32); let px = (px as u32).saturating_sub(x1 as u32).min(w);
let px =
(px as u32).saturating_sub(x1 as u32).min(w);
let py = height / 2 let py =
- ((py * (height / 2) as f64) as i32); height / 2 - ((py * (height / 2) as f64) as i32);
let py = let py = (py as u32).saturating_sub(y1 as u32).min(h);
(py as u32).saturating_sub(y1 as u32).min(h);
points.push(Point { x: px, y: py }); points.push(Point { x: px, y: py });
}
} }
} }
shapes.push(Shape {
x: x1 as u32,
y: y1 as u32,
width: w,
height: h,
points,
tag,
});
} }
shapes.push(Shape {
x: x1 as u32,
y: y1 as u32,
width: w,
height: h,
points,
tag,
});
} }
} }
} }

View file

@ -76,18 +76,19 @@ impl Default for Settings {
#[derive(Debug)] #[derive(Debug)]
struct Frame { struct Frame {
video_analytics: minidom::Element, video_analytics: xmltree::Element,
other_elements: Vec<minidom::Element>, other_elements: Vec<xmltree::Element>,
events: Vec<gst::Event>, events: Vec<gst::Event>,
} }
impl Default for Frame { impl Default for Frame {
fn default() -> Self { fn default() -> Self {
let mut video_analytics = xmltree::Element::new("VideoAnalytics");
video_analytics.namespace = Some(String::from(crate::ONVIF_METADATA_SCHEMA));
video_analytics.prefix = Some(String::from("tt"));
Frame { Frame {
video_analytics: minidom::Element::bare( video_analytics,
"VideoAnalytics",
"http://www.onvif.org/ver10/schema",
),
other_elements: Vec::new(), other_elements: Vec::new(),
events: Vec::new(), events: Vec::new(),
} }
@ -372,22 +373,32 @@ impl OnvifMetadataParse {
.entry(dt_unix_ns) .entry(dt_unix_ns)
.or_insert_with(Frame::default); .or_insert_with(Frame::default);
frame.video_analytics.append_child(el.clone()); frame
.video_analytics
.children
.push(xmltree::XMLNode::Element(el.clone()));
} }
let utc_time = running_time_to_utc_time(utc_time_running_time_mapping, running_time) let utc_time = running_time_to_utc_time(utc_time_running_time_mapping, running_time)
.unwrap_or(gst::ClockTime::ZERO); .unwrap_or(gst::ClockTime::ZERO);
for child in root.children() { for child in root.children.iter().filter_map(|n| n.as_element()) {
let frame = queued_frames.entry(utc_time).or_insert_with(Frame::default); let frame = queued_frames.entry(utc_time).or_insert_with(Frame::default);
if child.is("VideoAnalytics", "http://www.onvif.org/ver10/schema") { if child.name == "VideoAnalytics"
for subchild in child.children() { && child.namespace.as_deref() == Some(crate::ONVIF_METADATA_SCHEMA)
if subchild.is("Frame", "http://www.onvif.org/ver10/schema") { {
for subchild in child.children.iter().filter_map(|n| n.as_element()) {
if subchild.name == "Frame"
&& subchild.namespace.as_deref() == Some(crate::ONVIF_METADATA_SCHEMA)
{
continue; continue;
} }
frame.video_analytics.append_child(subchild.clone()); frame
.video_analytics
.children
.push(xmltree::XMLNode::Element(subchild.clone()));
} }
} else { } else {
frame.other_elements.push(child.clone()); frame.other_elements.push(child.clone());
@ -678,8 +689,7 @@ impl OnvifMetadataParse {
} }
}; };
if frame.video_analytics.children().next().is_none() && frame.other_elements.is_empty() if frame.video_analytics.children.is_empty() && frame.other_elements.is_empty() {
{
// Generate a gap event if there's no actual data for this time // Generate a gap event if there's no actual data for this time
if !had_events { if !had_events {
data.push(BufferOrEvent::Event( data.push(BufferOrEvent::Event(
@ -753,21 +763,30 @@ impl OnvifMetadataParse {
frame_pts frame_pts
); );
let mut xml = let mut xml = xmltree::Element::new("MetadataStream");
minidom::Element::builder("MetadataStream", "http://www.onvif.org/ver10/schema") xml.namespaces
.prefix(Some("tt".into()), "http://www.onvif.org/ver10/schema") .get_or_insert_with(|| xmltree::Namespace(Default::default()))
.unwrap() .put("tt", crate::ONVIF_METADATA_SCHEMA);
.build(); xml.namespace = Some(String::from(crate::ONVIF_METADATA_SCHEMA));
xml.prefix = Some(String::from("tt"));
if video_analytics.children().next().is_some() { if !video_analytics.children.is_empty() {
xml.append_child(video_analytics); xml.children
.push(xmltree::XMLNode::Element(video_analytics));
} }
for child in other_elements { for child in other_elements {
xml.append_child(child); xml.children.push(xmltree::XMLNode::Element(child));
} }
let mut vec = Vec::new(); let mut vec = Vec::new();
if let Err(err) = xml.write_to_decl(&mut vec) { if let Err(err) = xml.write_with_config(
&mut vec,
xmltree::EmitterConfig {
write_document_declaration: false,
perform_indent: true,
..xmltree::EmitterConfig::default()
},
) {
gst::error!(CAT, imp: self, "Can't serialize XML element: {}", err); gst::error!(CAT, imp: self, "Can't serialize XML element: {}", err);
for event in eos_events { for event in eos_events {
data.push(BufferOrEvent::Event(event)); data.push(BufferOrEvent::Event(event));