diff --git a/Cargo.lock b/Cargo.lock index c136d914..5ef0e991 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2756,6 +2756,21 @@ dependencies = [ "regex", ] +[[package]] +name = "gst-plugin-relationmeta" +version = "0.14.0-alpha.1" +dependencies = [ + "chrono", + "glib", + "gst-plugin-version-helper", + "gstreamer", + "gstreamer-analytics", + "gstreamer-base", + "gstreamer-rtp", + "gstreamer-video", + "xmltree", +] + [[package]] name = "gst-plugin-reqwest" version = "0.14.0-alpha.1" @@ -3189,6 +3204,28 @@ dependencies = [ "system-deps 7.0.3", ] +[[package]] +name = "gstreamer-analytics" +version = "0.24.0" +source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#d1ece4bb12bba4cbe9cc933206caaa2b79c7011d" +dependencies = [ + "glib", + "gstreamer", + "gstreamer-analytics-sys", + "libc", +] + +[[package]] +name = "gstreamer-analytics-sys" +version = "0.24.0" +source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#d1ece4bb12bba4cbe9cc933206caaa2b79c7011d" +dependencies = [ + "glib-sys", + "gstreamer-sys", + "libc", + "system-deps 7.0.3", +] + [[package]] name = "gstreamer-app" version = "0.24.0" diff --git a/Cargo.toml b/Cargo.toml index 641a9c1f..59de4982 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "net/onvif", "net/raptorq", "net/reqwest", + "net/relationmeta", "net/rtp", "net/rtsp", "net/webrtchttp", @@ -86,6 +87,7 @@ default-members = [ "net/onvif", "net/raptorq", "net/reqwest", + "net/relationmeta", "net/rtp", "net/rtsp", "net/webrtchttp", @@ -142,6 +144,7 @@ gdk-x11 = { package = "gdk4-x11", git = "https://github.com/gtk-rs/gtk4-rs", bra gdk-win32 = { package = "gdk4-win32", git = "https://github.com/gtk-rs/gtk4-rs", branch = "main", features = ["v4_4"]} gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" } gst-allocators = { package = "gstreamer-allocators", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" } +gst-analytics = { package = "gstreamer-analytics", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" } gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" } gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" } gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" } diff --git a/ci/utils.py b/ci/utils.py index 1ae6be13..d2944db8 100644 --- a/ci/utils.py +++ b/ci/utils.py @@ -24,6 +24,7 @@ RS_PREFIXED = [ 'rtp', 'rtsp', 'inter', + 'relationmeta', ] OVERRIDE = { diff --git a/dependencies.py b/dependencies.py index 3f11e209..3933f4d6 100755 --- a/dependencies.py +++ b/dependencies.py @@ -32,6 +32,7 @@ RENAMES = { 'rsrtsp': 'rtsp', 'rswebp': 'webp', 'rsonvif': 'onvif', + 'rsrelationmeta': 'relationmeta', 'rstracers': 'tracers', 'rsclosedcaption': 'closedcaption', 'rswebrtc': 'webrtc', diff --git a/docs/plugins/gst_plugins_cache.json b/docs/plugins/gst_plugins_cache.json index edd54cb5..35442730 100644 --- a/docs/plugins/gst_plugins_cache.json +++ b/docs/plugins/gst_plugins_cache.json @@ -7967,6 +7967,78 @@ "tracers": {}, "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" }, + "rsrelationmeta": { + "description": "GStreamer Rust Relation Meta Plugin", + "elements": { + "relationmeta2onvifmeta": { + "author": "Benjamin Gaignard ", + "description": "Convert relation metadata to ONVIF metadata", + "hierarchy": [ + "GstRelationMeta2OnvifMeta", + "GstElement", + "GstObject", + "GInitiallyUnowned", + "GObject" + ], + "klass": "Metadata", + "pad-templates": { + "sink": { + "caps": "ANY", + "direction": "sink", + "presence": "always" + }, + "src": { + "caps": "ANY", + "direction": "src", + "presence": "always" + } + }, + "properties": { + "time-source": { + "blurb": "Time source for UTC timestamps", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "clock (0)", + "mutable": "ready", + "readable": true, + "type": "GstRsOnvifNtpTimeSource", + "writable": true + } + }, + "rank": "none" + } + }, + "filename": "gstrsrelationmeta", + "license": "MPL-2.0", + "other-types": { + "GstRsOnvifNtpTimeSource": { + "kind": "enum", + "values": [ + { + "desc": "UNIX time based on realtime clock.", + "name": "clock", + "value": "0" + }, + { + "desc": "Running time is in UTC", + "name": "running-time", + "value": "1" + }, + { + "desc": "Pipeline clock is UTC", + "name": "clock-time", + "value": "2" + } + ] + } + }, + "package": "gst-plugin-relationmeta", + "source": "gst-plugin-relationmeta", + "tracers": {}, + "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" + }, "rsrtp": { "description": "GStreamer Rust RTP Plugin", "elements": { diff --git a/meson.build b/meson.build index 44b19c03..31bdaf22 100644 --- a/meson.build +++ b/meson.build @@ -91,6 +91,9 @@ endif if get_option('threadshare').allowed() or get_option('rtsp').allowed() deps += [['gstreamer-net-1.0', 'gstreamer', 'gst_net_dep', 'gst_net']] endif +if get_option('relationmeta').allowed() + deps += [['gstreamer-analytics-1.0', 'gst-plugins-bad', 'gstanalytics_dep', 'gstanalytics']] +endif glib_dep = dependency('glib-2.0', version: glib_req) deps_cache += {'glib-2.0': glib_dep} @@ -155,6 +158,7 @@ plugins = { }, 'raptorq': {'library': 'libgstraptorq'}, 'reqwest': {'library': 'libgstreqwest'}, + 'relationmeta': {'library': 'libgstrsrelationmeta'}, 'rtsp': {'library': 'libgstrsrtsp'}, 'rtp': {'library': 'libgstrsrtp'}, 'webrtchttp': {'library': 'libgstwebrtchttp'}, diff --git a/meson_options.txt b/meson_options.txt index 69bf53e0..b6e6d396 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -33,6 +33,7 @@ option('ndi', type: 'feature', value: 'auto', description: 'Build ndi plugin') option('onvif', type: 'feature', value: 'auto', description: 'Build onvif plugin') option('raptorq', type: 'feature', value: 'auto', description: 'Build raptorq plugin') option('reqwest', type: 'feature', value: 'auto', description: 'Build reqwest plugin') +option('relationmeta', type: 'feature', value: 'auto', description: 'Build relationmeta plugin') option('rtsp', type: 'feature', value: 'auto', description: 'Build rtsp plugin') option('rtp', type: 'feature', value: 'auto', description: 'Build rtp plugin') option('webrtc', type: 'feature', value: 'auto', yield: true, description: 'Build webrtc plugin') diff --git a/net/relationmeta/Cargo.toml b/net/relationmeta/Cargo.toml new file mode 100644 index 00000000..fc48c979 --- /dev/null +++ b/net/relationmeta/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "gst-plugin-relationmeta" +version.workspace = true +authors = ["Benjamin Gaignard "] +repository.workspace = true +license = "MPL-2.0" +description = "GStreamer Rust Relation Meta Plugin" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +gst = { workspace = true, features = ["v1_24"] } +gst-rtp = { workspace = true, features = ["v1_24"] } +gst-base = { workspace = true, features = ["v1_24"] } +gst-video = { workspace = true, features = ["v1_24"] } +gst-analytics = { workspace = true } +chrono = { version = "0.4.31", default-features = false } +xmltree = "0.10" +glib = { workspace = true, features = ["v2_62"] } + +[lib] +name = "gstrsrelationmeta" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper.workspace = true + +[features] +static = [] +capi = [] +doc = ["gst/v1_18"] + +[package.metadata.capi] +min_version = "0.9.21" + +[package.metadata.capi.header] +enabled = false + +[package.metadata.capi.library] +install_subdir = "gstreamer-1.0" +versioning = false +import_library = false + +[package.metadata.capi.pkg_config] +requires_private = "gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0, gstreamer-analytics-1.0" diff --git a/net/relationmeta/build.rs b/net/relationmeta/build.rs new file mode 100644 index 00000000..cda12e57 --- /dev/null +++ b/net/relationmeta/build.rs @@ -0,0 +1,3 @@ +fn main() { + gst_plugin_version_helper::info() +} diff --git a/net/relationmeta/src/lib.rs b/net/relationmeta/src/lib.rs new file mode 100644 index 00000000..36d6f4e5 --- /dev/null +++ b/net/relationmeta/src/lib.rs @@ -0,0 +1,42 @@ +// Copyright (C) 2024 Benjamin Gaignard +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 +#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)] + +/** + * plugin-rsrelationmeta: + * + * Since: plugins-rs-0.13.0 + */ +use gst::glib; + +pub(crate) const ONVIF_METADATA_SCHEMA: &str = "http://www.onvif.org/ver10/schema"; +pub(crate) const ONVIF_METADATA_PREFIX: &str = "tt"; + +mod relationmeta2onvifmeta; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + relationmeta2onvifmeta::register(plugin)?; + + if !gst::meta::CustomMeta::is_registered("OnvifXMLFrameMeta") { + gst::meta::CustomMeta::register("OnvifXMLFrameMeta", &[]); + } + + Ok(()) +} + +gst::plugin_define!( + rsrelationmeta, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MPL-2.0", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/net/relationmeta/src/relationmeta2onvifmeta/imp.rs b/net/relationmeta/src/relationmeta2onvifmeta/imp.rs new file mode 100644 index 00000000..258517b7 --- /dev/null +++ b/net/relationmeta/src/relationmeta2onvifmeta/imp.rs @@ -0,0 +1,449 @@ +// Copyright (C) 2024 Benjamin Gaignard +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gst::glib; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst_analytics::*; +use std::sync::LazyLock; + +use std::sync::Mutex; + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)] +#[repr(u32)] +#[enum_type(name = "GstRsOnvifNtpTimeSource")] +pub enum TimeSource { + #[enum_value(name = "UNIX time based on realtime clock.", nick = "clock")] + Clock, + #[enum_value(name = "Running time is in UTC", nick = "running-time")] + RunningTime, + #[enum_value(name = "Pipeline clock is UTC", nick = "clock-time")] + ClockTime, +} + +const DEFAULT_TIME_SOURCE: TimeSource = TimeSource::Clock; + +#[derive(Debug, Clone, Copy)] +struct Settings { + timesource: TimeSource, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + timesource: DEFAULT_TIME_SOURCE, + } + } +} + +#[derive(Default)] +struct State { + video_info: Option, + segment: gst::FormattedSegment, +} + +pub struct RelationMeta2OnvifMeta { + // Input media stream with relation metadata + sinkpad: gst::Pad, + // Output media stream with ONVIF metadata + srcpad: gst::Pad, + state: Mutex, + settings: Mutex, +} + +static CAT: LazyLock = LazyLock::new(|| { + gst::DebugCategory::new( + "relationmeta2onvifmeta", + gst::DebugColorFlags::empty(), + Some("Relation metadata to ONVIF metadata"), + ) +}); + +#[glib::object_subclass] +impl ObjectSubclass for RelationMeta2OnvifMeta { + const NAME: &'static str = "GstRelationMeta2OnvifMeta"; + type Type = super::RelationMeta2OnvifMeta; + type ParentType = gst::Element; + + fn with_class(klass: &Self::Class) -> Self { + let templ = klass.pad_template("sink").unwrap(); + let sinkpad = gst::Pad::builder_from_template(&templ) + .chain_function(|pad, parent, buffer| { + RelationMeta2OnvifMeta::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |convert| convert.sink_chain(pad, buffer), + ) + }) + .event_function(|pad, parent, event| { + RelationMeta2OnvifMeta::catch_panic_pad_function( + parent, + || false, + |convert| convert.sink_event(pad, event), + ) + }) + .flags(gst::PadFlags::PROXY_CAPS) + .flags(gst::PadFlags::PROXY_ALLOCATION) + .build(); + let templ = klass.pad_template("src").unwrap(); + let srcpad = gst::Pad::builder_from_template(&templ) + .flags(gst::PadFlags::PROXY_CAPS) + .flags(gst::PadFlags::PROXY_ALLOCATION) + .build(); + + Self { + srcpad, + sinkpad, + state: Mutex::new(State::default()), + settings: Mutex::new(Settings::default()), + } + } +} + +impl ObjectImpl for RelationMeta2OnvifMeta { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: LazyLock> = LazyLock::new(|| { + vec![ + glib::ParamSpecEnum::builder_with_default("time-source", DEFAULT_TIME_SOURCE) + .nick("time source") + .blurb("Time source for UTC timestamps") + .mutable_ready() + .build(), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + match pspec.name() { + "time-source" => { + let mut settings = self.settings.lock().unwrap(); + settings.timesource = value.get::().expect("type checked upstream"); + } + _ => unreachable!(), + } + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "time-source" => { + let settings = self.settings.lock().unwrap(); + settings.timesource.to_value() + } + _ => unimplemented!(), + } + } + + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + obj.add_pad(&self.sinkpad).unwrap(); + obj.add_pad(&self.srcpad).unwrap(); + } +} + +impl GstObjectImpl for RelationMeta2OnvifMeta {} + +impl ElementImpl for RelationMeta2OnvifMeta { + fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { + static ELEMENT_METADATA: LazyLock = LazyLock::new(|| { + gst::subclass::ElementMetadata::new( + "Relation metadata to ONVIF metadata", + "Metadata", + "Convert relation metadata to ONVIF metadata", + "Benjamin Gaignard ", + ) + }); + + Some(&*ELEMENT_METADATA) + } + + fn pad_templates() -> &'static [gst::PadTemplate] { + static PAD_TEMPLATES: LazyLock> = LazyLock::new(|| { + let sink_caps = gst::Caps::new_any(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + + let src_caps = gst::Caps::new_any(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + + vec![src_pad_template, sink_pad_template] + }); + + PAD_TEMPLATES.as_ref() + } +} + +impl RelationMeta2OnvifMeta { + fn sink_event(&self, pad: &gst::Pad, event: gst::Event) -> bool { + use gst::EventView; + + match event.view() { + EventView::Caps(c) => { + let mut state = self.state.lock().unwrap(); + let info = match gst_video::VideoInfo::from_caps(c.caps()) { + Ok(info) => info, + Err(_) => { + gst::error!(CAT, obj = pad, "Failed to parse caps {:?}", c.caps()); + return false; + } + }; + state.video_info = Some(info); + self.srcpad.push_event(event) + } + EventView::Segment(s) => { + let mut state = self.state.lock().unwrap(); + let segment = s.segment().clone(); + let segment = match segment.downcast::() { + Ok(segment) => segment, + Err(_) => { + gst::element_imp_error!( + self, + gst::CoreError::Event, + ["Only time segments are supported"] + ); + return false; + } + }; + + state.segment = segment.clone(); + gst::Pad::event_default(pad, Some(&*self.obj()), event) + } + _ => gst::Pad::event_default(pad, Some(&*self.obj()), event), + } + } + + fn get_utc_time(&self, running_time: gst::ClockTime) -> gst::ClockTime { + match self.settings.lock().unwrap().timesource { + TimeSource::Clock => { + if let (Some(base_time), Some(clock)) = (self.obj().base_time(), self.obj().clock()) + { + let utc_now = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() + .try_into() + .unwrap(); + let utc_now = gst::ClockTime::from_nseconds(utc_now); + let running_time_now = clock.time().unwrap() - base_time; + + let rt_diff = utc_now - running_time_now; + running_time_now + rt_diff + } else { + gst::error!( + CAT, + imp = self, + "Can only use the clock if the pipeline has a clock" + ); + gst::ClockTime::ZERO + } + } + TimeSource::RunningTime => running_time, + TimeSource::ClockTime => { + if let Some(base_time) = self.obj().base_time() { + running_time + base_time + } else { + gst::warning!( + CAT, + imp = self, + "Clock time was selected but there is no base time" + ); + gst::ClockTime::ZERO + } + } + } + } + + fn sink_chain( + &self, + pad: &gst::Pad, + mut input_buffer: gst::Buffer, + ) -> Result { + let metas = input_buffer.meta::(); + + 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(crate::ONVIF_METADATA_PREFIX)); + + let mut video_analytics = xmltree::Element::new("VideoAnalytics"); + video_analytics.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + let state = self.state.lock().unwrap(); + + let current_time = + self.get_utc_time(state.segment.to_running_time(input_buffer.pts()).unwrap()); + let dt = + gst::DateTime::from_unix_epoch_utc_usecs(current_time.useconds().try_into().unwrap()) + .unwrap() + .to_g_date_time() + .unwrap() + .format_iso8601() + .unwrap(); + + let mut frame = xmltree::Element::new("Frame"); + frame.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + frame + .attributes + .insert("UtcTime".to_string(), dt.to_string()); + + let mut transformation = xmltree::Element::new("Transformation"); + transformation.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + + let mut translate = xmltree::Element::new("Translate"); + translate.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + translate + .attributes + .insert("x".to_string(), (-1.0).to_string()); + translate + .attributes + .insert("y".to_string(), (-1.0).to_string()); + + let mut scale = xmltree::Element::new("Scale"); + scale.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + + let video_info = state.video_info.as_ref().unwrap(); + let x = format!("{:.5}", 2_f64 / (video_info.width() as f64)); + let y = format!("{:.5}", 2_f64 / (video_info.height() as f64)); + scale.attributes.insert("x".to_string(), x); + scale.attributes.insert("y".to_string(), y); + + transformation + .children + .push(xmltree::XMLNode::Element(translate)); + transformation + .children + .push(xmltree::XMLNode::Element(scale)); + + frame + .children + .push(xmltree::XMLNode::Element(transformation)); + + if let Some(metas) = metas { + for meta in metas.iter::() { + let loc = meta.location().unwrap(); + + let mut object = xmltree::Element::new("Object"); + object.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + object + .attributes + .insert("ObjectId".to_string(), meta.id().to_string()); + + let mut appearance = xmltree::Element::new("Appearance"); + appearance.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + + let mut shape = xmltree::Element::new("Shape"); + shape.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + + let mut bounding_box = xmltree::Element::new("BoundingBox"); + bounding_box.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + bounding_box + .attributes + .insert("left".to_string(), loc.x.to_string()); + bounding_box + .attributes + .insert("top".to_string(), loc.y.to_string()); + bounding_box + .attributes + .insert("right".to_string(), (loc.x + loc.w).to_string()); + bounding_box + .attributes + .insert("bottom".to_string(), (loc.y + loc.h).to_string()); + shape.children.push(xmltree::XMLNode::Element(bounding_box)); + + let mut class = xmltree::Element::new("Class"); + class.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + + let mut t = xmltree::Element::new("Type"); + t.prefix = Some(String::from(crate::ONVIF_METADATA_PREFIX)); + t.attributes + .insert("Likelihood".to_string(), loc.loc_conf_lvl.to_string()); + let obj_name = if let Some(obj_type) = meta.obj_type() { + obj_type.as_str() + } else { + "Unknown" + }; + t.children + .push(xmltree::XMLNode::Text(obj_name.to_string())); + + gst::trace!( + CAT, + imp = self, + "Transformed object: {}x{}@({}h{}) prob: {} name: {:?}", + loc.w, + loc.w, + loc.x, + loc.y, + loc.loc_conf_lvl, + meta.obj_type() + ); + + class.children.push(xmltree::XMLNode::Element(t)); + + appearance.children.push(xmltree::XMLNode::Element(shape)); + appearance.children.push(xmltree::XMLNode::Element(class)); + + object.children.push(xmltree::XMLNode::Element(appearance)); + + frame.children.push(xmltree::XMLNode::Element(object)); + } + } else { + gst::trace!(CAT, imp = self, "No meta, nothing to transform"); + } + + video_analytics + .children + .push(xmltree::XMLNode::Element(frame)); + xml.children + .push(xmltree::XMLNode::Element(video_analytics)); + + let mut text = Vec::new(); + if let Err(err) = xml.write_with_config( + &mut text, + xmltree::EmitterConfig { + perform_indent: true, + ..xmltree::EmitterConfig::default() + }, + ) { + gst::error!(CAT, obj = pad, "Can't serialize XML element: {}", err); + } + + gst::trace!( + CAT, + imp = self, + "Generated ONVIF xml: {}", + std::str::from_utf8(&text).unwrap() + ); + + let meta_buf = gst::Buffer::from_mut_slice(text); + let mut buflist = gst::BufferList::new(); + buflist.get_mut().unwrap().add(meta_buf); + + let buf = input_buffer.make_mut(); + let mut onvif_meta = gst::meta::CustomMeta::add(buf, "OnvifXMLFrameMeta").unwrap(); + let s = onvif_meta.mut_structure(); + s.set("frames", buflist); + + gst::trace!(CAT, obj = pad, "Send buffer {:?}", buf); + self.srcpad.push(input_buffer) + } +} diff --git a/net/relationmeta/src/relationmeta2onvifmeta/mod.rs b/net/relationmeta/src/relationmeta2onvifmeta/mod.rs new file mode 100644 index 00000000..da3523cb --- /dev/null +++ b/net/relationmeta/src/relationmeta2onvifmeta/mod.rs @@ -0,0 +1,28 @@ +// Copyright (C) 2024 Benjamin Gaignard +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use gst::glib; +use gst::prelude::*; + +mod imp; + +glib::wrapper! { + pub struct RelationMeta2OnvifMeta(ObjectSubclass) @extends gst::Element, gst::Object; +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + #[cfg(feature = "doc")] + imp::TimeSource::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty()); + + gst::Element::register( + Some(plugin), + "relationmeta2onvifmeta", + gst::Rank::NONE, + RelationMeta2OnvifMeta::static_type(), + ) +}