mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-11-26 13:31:00 +00:00
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:
parent
97e0852156
commit
b2ddb34258
4 changed files with 287 additions and 243 deletions
|
@ -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"] }
|
||||
once_cell = "1.0"
|
||||
xmlparser = "0.13"
|
||||
minidom = "0.15"
|
||||
chrono = { version = "0.4", default-features = false }
|
||||
cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core", features=["use_glib"] }
|
||||
pango = { git = "https://github.com/gtk-rs/gtk-rs-core" }
|
||||
pangocairo = { git = "https://github.com/gtk-rs/gtk-rs-core" }
|
||||
xmltree = "0.10"
|
||||
|
||||
[lib]
|
||||
name = "gstrsonvif"
|
||||
|
|
|
@ -21,6 +21,9 @@ mod onvifmetadataoverlay;
|
|||
mod onvifmetadataparse;
|
||||
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
|
||||
// midnight 01-01-1970 (UNIX epoch)
|
||||
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
|
||||
}
|
||||
|
||||
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(|_| {
|
||||
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::ResourceError::Read,
|
||||
["Failed to parse buffer as XML: {}", err]
|
||||
|
@ -66,16 +69,21 @@ pub(crate) fn xml_from_buffer(buffer: &gst::Buffer) -> Result<minidom::Element,
|
|||
}
|
||||
|
||||
pub(crate) fn iterate_video_analytics_frames(
|
||||
root: &minidom::Element,
|
||||
root: &xmltree::Element,
|
||||
) -> 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| {
|
||||
analytics.children().filter_map(|el| {
|
||||
analytics
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|n| n.as_element())
|
||||
.filter_map(|el| {
|
||||
// We are only interested in associating Frame metadata with video frames
|
||||
if el.is("Frame", "http://www.onvif.org/ver10/schema") {
|
||||
let timestamp = match el.attr("UtcTime") {
|
||||
if el.name == "Frame" && el.namespace.as_deref() == Some(ONVIF_METADATA_SCHEMA)
|
||||
{
|
||||
let timestamp = match el.attributes.get("UtcTime") {
|
||||
Some(timestamp) => timestamp,
|
||||
None => {
|
||||
return Some(Err(gst::error_msg!(
|
||||
|
|
|
@ -9,8 +9,6 @@ use once_cell::sync::Lazy;
|
|||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use minidom::Element;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"onvifmetadataoverlay",
|
||||
|
@ -445,7 +443,8 @@ impl OnvifMetadataOverlay {
|
|||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let root = utf8.parse::<Element>().map_err(|err| {
|
||||
let root =
|
||||
xmltree::Element::parse(std::io::Cursor::new(utf8)).map_err(|err| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::ResourceError::Read,
|
||||
|
@ -456,16 +455,27 @@ impl OnvifMetadataOverlay {
|
|||
})?;
|
||||
|
||||
for object in root
|
||||
.get_child("VideoAnalytics", "http://www.onvif.org/ver10/schema")
|
||||
.map(|el| el.children().into_iter().collect())
|
||||
.unwrap_or_else(Vec::new)
|
||||
.get_child(("VideoAnalytics", crate::ONVIF_METADATA_SCHEMA))
|
||||
.map(|e| e.children.iter().filter_map(|n| n.as_element()))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if object.name == "Frame"
|
||||
&& object.namespace.as_deref() == Some(crate::ONVIF_METADATA_SCHEMA)
|
||||
{
|
||||
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)
|
||||
})
|
||||
{
|
||||
if object.is("Frame", "http://www.onvif.org/ver10/schema") {
|
||||
for object in object.children() {
|
||||
if object.is("Object", "http://www.onvif.org/ver10/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(),
|
||||
None => {
|
||||
gst::warning!(
|
||||
|
@ -486,35 +496,31 @@ impl OnvifMetadataOverlay {
|
|||
continue;
|
||||
}
|
||||
|
||||
let appearance = match object.get_child(
|
||||
"Appearance",
|
||||
"http://www.onvif.org/ver10/schema",
|
||||
) {
|
||||
let appearance = match object
|
||||
.get_child(("Appearance", crate::ONVIF_METADATA_SCHEMA))
|
||||
{
|
||||
Some(appearance) => appearance,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let shape = match appearance
|
||||
.get_child("Shape", "http://www.onvif.org/ver10/schema")
|
||||
.get_child(("Shape", crate::ONVIF_METADATA_SCHEMA))
|
||||
{
|
||||
Some(shape) => shape,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let tag = appearance
|
||||
.get_child("Class", "http://www.onvif.org/ver10/schema")
|
||||
.get_child(("Class", crate::ONVIF_METADATA_SCHEMA))
|
||||
.and_then(|class| {
|
||||
class.get_child(
|
||||
"Type",
|
||||
"http://www.onvif.org/ver10/schema",
|
||||
)
|
||||
class.get_child(("Type", crate::ONVIF_METADATA_SCHEMA))
|
||||
})
|
||||
.map(|t| t.text());
|
||||
.and_then(|t| t.get_text())
|
||||
.map(|t| t.into_owned());
|
||||
|
||||
let bbox = match shape.get_child(
|
||||
"BoundingBox",
|
||||
"http://www.onvif.org/ver10/schema",
|
||||
) {
|
||||
let bbox = match shape
|
||||
.get_child(("BoundingBox", crate::ONVIF_METADATA_SCHEMA))
|
||||
{
|
||||
Some(bbox) => bbox,
|
||||
None => {
|
||||
gst::warning!(
|
||||
|
@ -526,8 +532,11 @@ impl OnvifMetadataOverlay {
|
|||
}
|
||||
};
|
||||
|
||||
let left: f64 =
|
||||
match bbox.attr("left").and_then(|val| val.parse().ok()) {
|
||||
let left: f64 = match bbox
|
||||
.attributes
|
||||
.get("left")
|
||||
.and_then(|val| val.parse().ok())
|
||||
{
|
||||
Some(val) => val,
|
||||
None => {
|
||||
gst::warning!(
|
||||
|
@ -539,8 +548,11 @@ impl OnvifMetadataOverlay {
|
|||
}
|
||||
};
|
||||
|
||||
let right: f64 =
|
||||
match bbox.attr("right").and_then(|val| val.parse().ok()) {
|
||||
let right: f64 = match bbox
|
||||
.attributes
|
||||
.get("right")
|
||||
.and_then(|val| val.parse().ok())
|
||||
{
|
||||
Some(val) => val,
|
||||
None => {
|
||||
gst::warning!(
|
||||
|
@ -552,8 +564,11 @@ impl OnvifMetadataOverlay {
|
|||
}
|
||||
};
|
||||
|
||||
let top: f64 =
|
||||
match bbox.attr("top").and_then(|val| val.parse().ok()) {
|
||||
let top: f64 = match bbox
|
||||
.attributes
|
||||
.get("top")
|
||||
.and_then(|val| val.parse().ok())
|
||||
{
|
||||
Some(val) => val,
|
||||
None => {
|
||||
gst::warning!(
|
||||
|
@ -566,7 +581,8 @@ impl OnvifMetadataOverlay {
|
|||
};
|
||||
|
||||
let bottom: f64 = match bbox
|
||||
.attr("bottom")
|
||||
.attributes
|
||||
.get("bottom")
|
||||
.and_then(|val| val.parse().ok())
|
||||
{
|
||||
Some(val) => val,
|
||||
|
@ -590,15 +606,19 @@ impl OnvifMetadataOverlay {
|
|||
|
||||
let mut points = vec![];
|
||||
|
||||
if let Some(polygon) = shape
|
||||
.get_child("Polygon", "http://www.onvif.org/ver10/schema")
|
||||
if let Some(polygon) =
|
||||
shape.get_child(("Polygon", crate::ONVIF_METADATA_SCHEMA))
|
||||
{
|
||||
for point in polygon.children() {
|
||||
if point
|
||||
.is("Point", "http://www.onvif.org/ver10/schema")
|
||||
for point in
|
||||
polygon.children.iter().filter_map(|n| n.as_element())
|
||||
{
|
||||
if point.name == "Point"
|
||||
&& point.namespace.as_deref()
|
||||
== Some(crate::ONVIF_METADATA_SCHEMA)
|
||||
{
|
||||
let px: f64 = match point
|
||||
.attr("x")
|
||||
.attributes
|
||||
.get("x")
|
||||
.and_then(|val| val.parse().ok())
|
||||
{
|
||||
Some(val) => val,
|
||||
|
@ -613,7 +633,8 @@ impl OnvifMetadataOverlay {
|
|||
};
|
||||
|
||||
let py: f64 = match point
|
||||
.attr("y")
|
||||
.attributes
|
||||
.get("y")
|
||||
.and_then(|val| val.parse().ok())
|
||||
{
|
||||
Some(val) => val,
|
||||
|
@ -627,15 +648,12 @@ impl OnvifMetadataOverlay {
|
|||
}
|
||||
};
|
||||
|
||||
let px =
|
||||
width / 2 + ((px * (width / 2) as f64) as i32);
|
||||
let px =
|
||||
(px as u32).saturating_sub(x1 as u32).min(w);
|
||||
let px = width / 2 + ((px * (width / 2) as f64) as i32);
|
||||
let px = (px as u32).saturating_sub(x1 as u32).min(w);
|
||||
|
||||
let py = height / 2
|
||||
- ((py * (height / 2) as f64) as i32);
|
||||
let py =
|
||||
(py as u32).saturating_sub(y1 as u32).min(h);
|
||||
height / 2 - ((py * (height / 2) as f64) as i32);
|
||||
let py = (py as u32).saturating_sub(y1 as u32).min(h);
|
||||
|
||||
points.push(Point { x: px, y: py });
|
||||
}
|
||||
|
@ -654,7 +672,6 @@ impl OnvifMetadataOverlay {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !frames.is_empty() {
|
||||
self.overlay_shapes(&mut state, shapes);
|
||||
|
|
|
@ -76,18 +76,19 @@ impl Default for Settings {
|
|||
|
||||
#[derive(Debug)]
|
||||
struct Frame {
|
||||
video_analytics: minidom::Element,
|
||||
other_elements: Vec<minidom::Element>,
|
||||
video_analytics: xmltree::Element,
|
||||
other_elements: Vec<xmltree::Element>,
|
||||
events: Vec<gst::Event>,
|
||||
}
|
||||
|
||||
impl Default for Frame {
|
||||
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 {
|
||||
video_analytics: minidom::Element::bare(
|
||||
"VideoAnalytics",
|
||||
"http://www.onvif.org/ver10/schema",
|
||||
),
|
||||
video_analytics,
|
||||
other_elements: Vec::new(),
|
||||
events: Vec::new(),
|
||||
}
|
||||
|
@ -372,22 +373,32 @@ impl OnvifMetadataParse {
|
|||
.entry(dt_unix_ns)
|
||||
.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)
|
||||
.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);
|
||||
|
||||
if child.is("VideoAnalytics", "http://www.onvif.org/ver10/schema") {
|
||||
for subchild in child.children() {
|
||||
if subchild.is("Frame", "http://www.onvif.org/ver10/schema") {
|
||||
if child.name == "VideoAnalytics"
|
||||
&& child.namespace.as_deref() == Some(crate::ONVIF_METADATA_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;
|
||||
}
|
||||
|
||||
frame.video_analytics.append_child(subchild.clone());
|
||||
frame
|
||||
.video_analytics
|
||||
.children
|
||||
.push(xmltree::XMLNode::Element(subchild.clone()));
|
||||
}
|
||||
} else {
|
||||
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
|
||||
if !had_events {
|
||||
data.push(BufferOrEvent::Event(
|
||||
|
@ -753,21 +763,30 @@ impl OnvifMetadataParse {
|
|||
frame_pts
|
||||
);
|
||||
|
||||
let mut xml =
|
||||
minidom::Element::builder("MetadataStream", "http://www.onvif.org/ver10/schema")
|
||||
.prefix(Some("tt".into()), "http://www.onvif.org/ver10/schema")
|
||||
.unwrap()
|
||||
.build();
|
||||
let mut xml = xmltree::Element::new("MetadataStream");
|
||||
xml.namespaces
|
||||
.get_or_insert_with(|| xmltree::Namespace(Default::default()))
|
||||
.put("tt", crate::ONVIF_METADATA_SCHEMA);
|
||||
xml.namespace = Some(String::from(crate::ONVIF_METADATA_SCHEMA));
|
||||
xml.prefix = Some(String::from("tt"));
|
||||
|
||||
if video_analytics.children().next().is_some() {
|
||||
xml.append_child(video_analytics);
|
||||
if !video_analytics.children.is_empty() {
|
||||
xml.children
|
||||
.push(xmltree::XMLNode::Element(video_analytics));
|
||||
}
|
||||
for child in other_elements {
|
||||
xml.append_child(child);
|
||||
xml.children.push(xmltree::XMLNode::Element(child));
|
||||
}
|
||||
|
||||
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);
|
||||
for event in eos_events {
|
||||
data.push(BufferOrEvent::Event(event));
|
||||
|
|
Loading…
Reference in a new issue