From 8acc573da9aedf79731888f1c797f9ee5de38e1e Mon Sep 17 00:00:00 2001 From: msabiniok Date: Thu, 29 Jan 2026 15:18:47 +0100 Subject: [PATCH 1/4] feat: Add rdp444 utility plugin scaffolding. --- meson_options.txt | 2 +- plugins/meson.build | 1 + plugins/rdp444/gstrdp444combine.c | 186 ++++++++++++++++++++++++++++++ plugins/rdp444/gstrdp444combine.h | 41 +++++++ plugins/rdp444/gstrdp444split.c | 90 +++++++++++++++ plugins/rdp444/gstrdp444split.h | 43 +++++++ plugins/rdp444/meson.build | 15 +++ plugins/rdp444/plugin.c | 26 +++++ 8 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 plugins/rdp444/gstrdp444combine.c create mode 100644 plugins/rdp444/gstrdp444combine.h create mode 100644 plugins/rdp444/gstrdp444split.c create mode 100644 plugins/rdp444/gstrdp444split.h create mode 100644 plugins/rdp444/meson.build create mode 100644 plugins/rdp444/plugin.c diff --git a/meson_options.txt b/meson_options.txt index ee3e2c6ba..0a713a928 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,6 +1,6 @@ option('ttml', type : 'feature', value : 'auto', description : 'Build GStreamer Fluendo TTML Element') option('injectbin', type : 'feature', value : 'auto', description : 'Build GStreamer Fluendo injectbin Element') - +option('rdp444', type : 'feature', value : 'auto', description : 'Build GStreamer Fluendo RDP YUV444 encoding plugin') # gst-fluendo-ttml options option('ttml_build_ttmlparse', type : 'feature', value : 'enabled', description : 'gst-fluendo-ttml: build the ttmlparse element') option('ttml_build_ttmlrender', type : 'feature', value : 'enabled', description : 'gst-fluendo-ttml: build the ttmlrender element') diff --git a/plugins/meson.build b/plugins/meson.build index bb3c5d56e..8b62cef96 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -8,6 +8,7 @@ plugins = { # Plugin Name Supported OS Description 'ttml': { 'os': ['linux', 'windows', 'darwin'], 'desc': 'GStreamer Fluendo TTML Element' }, 'injectbin': { 'os': ['linux', 'windows', 'darwin'], 'desc': 'GStreamer Fluendo dynamic pipeline rebuild element' }, + 'rdp444': { 'os': ['linux', 'windows', 'darwin'], 'desc': 'GStreamer Fluendo RDP YUV444 element' }, } # Meson builds OSX libraries with '.dylib' extension. However, the name_suffix diff --git a/plugins/rdp444/gstrdp444combine.c b/plugins/rdp444/gstrdp444combine.c new file mode 100644 index 000000000..2d02260da --- /dev/null +++ b/plugins/rdp444/gstrdp444combine.c @@ -0,0 +1,186 @@ +/* rdp444combine + * Copyright (C) 2026 Fluendo + * + * rdp444combine: Combines YUV 4:2:0 and Chroma 4:2:0 into 4:4:4 video/x-raw + +/** + * SECTION:element-rdp444combine + * @title: rdp444combine + * @short_description: Combines YUV 4:2:0 and Chroma 4:2:0 into 4:4:4 video/x-raw + */ + +#include "gstrdp444combine.h" + +GST_DEBUG_CATEGORY (gst_debug_rdp444combine); +#define GST_CAT_DEFAULT gst_debug_rdp444combine +#define gst_rdp444combine_parent_class parent_class + +enum +{ + PROP_0, + PROP_LAST +}; + +static GstStaticPadTemplate sink_yuv_templ = +GST_STATIC_PAD_TEMPLATE ("sink_yuv420", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("video/x-raw, format=I420") +); +static GstStaticPadTemplate sink_chroma_templ = +GST_STATIC_PAD_TEMPLATE ("sink_chroma420", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("video/x-raw, format=I420") +); + +static GstStaticPadTemplate src_templ = +GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("video/x-raw, format=Y444") +); + +G_DEFINE_TYPE (GstRDP444Combine, gst_rdp444_combine, GST_TYPE_AGGREGATOR); +GST_ELEMENT_REGISTER_DEFINE (rdp444combine, "rdp444combine", GST_RANK_NONE, + gst_rdp444_combine_get_type ()); + + +static void +gst_rdp444_combine_dispose (GObject * object) +{ + GstRDP444Combine *rdp444combine = GST_RDP444_COMBINE (object); +} + +static void +gst_rdp444_combine_finalize (GObject * object) +{ + GstRDP444Combine *rdp444combine = GST_RDP444_COMBINE (object); +} + + +static void +gst_rdp444_combine_set_property (GObject * object, guint prop_id, + GValue const *value, GParamSpec * pspec) +{ + GstRDP444Combine *rdp444combine = GST_RDP444_COMBINE (object); +} + +static void +gst_rdp444_combine_get_property (GObject * object, guint prop_id, GValue * value, + GParamSpec * pspec) +{ + GstRDP444Combine *rdp444combine = GST_RDP444_COMBINE (object); +} + +static gboolean +get_pad_resolution (GstPad *pad, gint *width, gint *height) +{ + GstCaps *caps = NULL; + GstStructure *structure = NULL; + gboolean result = FALSE; + + if (!pad || !width || !height) + return FALSE; + + caps = gst_pad_get_current_caps (pad); + if (caps) { + structure = gst_caps_get_structure (caps, 0); + if (gst_structure_get_int (structure, "width", width) && + gst_structure_get_int (structure, "height", height)) { + result = TRUE; + } + gst_caps_unref (caps); + } + + return result; +} + + +static GstFlowReturn +gst_rdp444_combine_aggregate (GstAggregator * aggregator, gboolean timeout) +{ + GstPad *sink_yuv = gst_element_get_static_pad (GST_ELEMENT (aggregator), "sink_yuv420"); + GstPad *sink_chroma = gst_element_get_static_pad (GST_ELEMENT (aggregator), "sink_chroma420"); + gint width_yuv = 0, height_yuv = 0; + gint width_chroma = 0, height_chroma = 0; + + if ( + !get_pad_resolution(sink_yuv, &width_yuv, &height_yuv) == FALSE & + !get_pad_resolution(sink_chroma, &width_chroma, &height_chroma)){ + GST_ERROR_OBJECT (aggregator, "Could not get pad resolutions"); + return GST_FLOW_ERROR; + } + + if (width_yuv != width_chroma || height_yuv != height_chroma) { + GST_ERROR_OBJECT (aggregator, "Resolution mismatch: YUV %dx%d vs Chroma %dx%d", + width_yuv, height_yuv, width_chroma, height_chroma); + return GST_FLOW_ERROR; + } + + GstCaps *caps_yuv = NULL; + if (sink_yuv) { + caps_yuv = gst_pad_get_current_caps (sink_yuv); + gst_object_unref (sink_yuv); + } + + if (caps_yuv) { + GstCaps *out_caps = gst_caps_copy (caps_yuv); + gst_caps_set_simple (out_caps, "format", G_TYPE_STRING, "Y444", NULL); + gst_aggregator_set_src_caps (aggregator, out_caps); + gst_caps_unref (caps_yuv); + gst_caps_unref (out_caps); + } + + return GST_FLOW_OK; +} + +static void +gst_rdp444_combine_class_init (GstRDP444CombineClass * klass) +{ + GObjectClass *object_class; + GstElementClass *element_class; + GstAggregatorClass *aggregator_class; + + object_class = G_OBJECT_CLASS (klass); + element_class = GST_ELEMENT_CLASS (klass); + aggregator_class = GST_AGGREGATOR_CLASS (klass); + + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&sink_yuv_templ)); + + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&sink_chroma_templ)); + + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&src_templ)); + + object_class->dispose = GST_DEBUG_FUNCPTR (gst_rdp444_combine_dispose); + object_class->finalize = GST_DEBUG_FUNCPTR (gst_rdp444_combine_finalize); + object_class->set_property = GST_DEBUG_FUNCPTR (gst_rdp444_combine_set_property); + object_class->get_property = GST_DEBUG_FUNCPTR (gst_rdp444_combine_get_property); + + aggregator_class->aggregate = GST_DEBUG_FUNCPTR (gst_rdp444_combine_aggregate); + + gst_element_class_set_static_metadata (element_class, + "rdp444combine", + "Element", + "Combines YUV 4:2:0 and Chroma 4:2:0 into 4:4:4 video/x-raw", + "Fluendo S.A. "); +} + +static void +gst_rdp444_combine_init (GstRDP444Combine * rdp444combine) +{ + GstAggregator *agg = GST_AGGREGATOR (rdp444combine); + + GstPad *srcpad = + gst_pad_new_from_static_template (&src_templ, "src"); + + gst_element_add_pad (GST_ELEMENT (rdp444combine), srcpad); + gst_element_add_pad(GST_ELEMENT (rdp444combine), + gst_pad_new_from_static_template (&sink_yuv_templ, "sink_yuv420")); + gst_element_add_pad(GST_ELEMENT (rdp444combine), + gst_pad_new_from_static_template (&sink_chroma_templ, "sink_chroma420")); + +} diff --git a/plugins/rdp444/gstrdp444combine.h b/plugins/rdp444/gstrdp444combine.h new file mode 100644 index 000000000..235201ad4 --- /dev/null +++ b/plugins/rdp444/gstrdp444combine.h @@ -0,0 +1,41 @@ +/* rdp444combine.h - RDP 444 Combiner element + * Copyright (C) 2026 Fluendo + * + * gstrdp444combine.h: Header for GstRDP444Combine Object + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef GSTRDP444COMBINE_H___ +#define GSTRDP444COMBINE_H___ + +#include +#include + +G_BEGIN_DECLS + +#define GST_TYPE_RDP444_COMBINE (gst_rdp444_combine_get_type()) +G_DECLARE_FINAL_TYPE (GstRDP444Combine, gst_rdp444_combine, GST, RDP444_COMBINE, GstAggregator) + +struct _GstRDP444Combine +{ + GstAggregator parent; +}; + +GST_ELEMENT_REGISTER_DECLARE (rdp444combine); + +G_END_DECLS +#endif diff --git a/plugins/rdp444/gstrdp444split.c b/plugins/rdp444/gstrdp444split.c new file mode 100644 index 000000000..eb1834b33 --- /dev/null +++ b/plugins/rdp444/gstrdp444split.c @@ -0,0 +1,90 @@ +/* rdp444split + * Copyright (C) 2026 Fluendo + * + * rdp444split: Splits 4:4:4 video/x-raw into YUV 4:2:0 and Chroma 4:2:0 + */ + +/** + * SECTION:element-rdp444split + * @title: rdp444split + * @short_description: Splits 4:4:4 video/x-raw into YUV 4:2:0 and Chroma 4:2:0 + */ + +#include "gstrdp444split.h" + +GST_DEBUG_CATEGORY_STATIC (gst_debug_rdp444split); +#define GST_CAT_DEFAULT gst_debug_rdp444split + +G_DEFINE_TYPE (GstRDP444Split, gst_rdp444_split, GST_TYPE_ELEMENT); +GST_ELEMENT_REGISTER_DEFINE (rdp444split, "rdp444split", GST_RANK_NONE, + gst_rdp444_split_get_type ()); + +static GstStaticPadTemplate sink_templ = +GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("video/x-raw, format=Y444") +); + +static GstStaticPadTemplate src0_templ = +GST_STATIC_PAD_TEMPLATE ("src0", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("video/x-raw, format=I420") +); + +static GstStaticPadTemplate src1_templ = +GST_STATIC_PAD_TEMPLATE ("src1", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("video/x-raw, format=I420") +); + +static GstFlowReturn rdp444split_chain (GstPad * pad, GstRDP444Split * split, + GstBuffer * buf); + +static GstFlowReturn +rdp444split_chain (GstPad * pad, GstRDP444Split * split, + GstBuffer * buf) +{ + // here do the splitting + return GST_FLOW_OK; +} + +static void +gst_rdp444_split_class_init (GstRDP444SplitClass * klass) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS (klass); + + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&sink_templ)); + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&src0_templ)); + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&src1_templ)); + + + + gst_element_class_set_static_metadata (element_class, + "RDP 444 Splitter", "Video/Splitter", + "Splits 4:4:4 video/x-raw into YUV 4:2:0 and Chroma 4:2:0", + "Fluendo "); +} + +static void +gst_rdp444_split_init (GstRDP444Split * self) +{ + self->sinkpad = + gst_pad_new_from_static_template (&sink_templ, "sink"); + gst_element_add_pad (GST_ELEMENT (self), self->sinkpad); + gst_pad_set_chain_function (self->sinkpad, + (GstPadChainFunction)(rdp444split_chain)); + + self->srcpad_yuv420 = + gst_pad_new_from_static_template (&src0_templ, "src0"); + gst_element_add_pad (GST_ELEMENT (self), self->srcpad_yuv420); + + self->srcpad_chroma420 = + gst_pad_new_from_static_template (&src1_templ, "src1"); + gst_element_add_pad (GST_ELEMENT (self), self->srcpad_chroma420); +} diff --git a/plugins/rdp444/gstrdp444split.h b/plugins/rdp444/gstrdp444split.h new file mode 100644 index 000000000..bdf206420 --- /dev/null +++ b/plugins/rdp444/gstrdp444split.h @@ -0,0 +1,43 @@ +/* rdp444split.h - RDP 444 Splitter element + * Copyright (C) 2026 Fluendo + * + * gstrdp444split.h: Header for GstRDP444Split Object + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef GSTRDP444SPLIT_H___ +#define GSTRDP444SPLIT_H___ + +#include + +G_BEGIN_DECLS + +#define GST_TYPE_RDP444_SPLIT (gst_rdp444_split_get_type()) +G_DECLARE_FINAL_TYPE (GstRDP444Split, gst_rdp444_split, GST, RDP444_SPLIT, GstElement) + +struct _GstRDP444Split +{ + GstElement parent; + + GstPad *sinkpad; + GstPad *srcpad_yuv420, *srcpad_chroma420; +}; + +GST_ELEMENT_REGISTER_DECLARE (rdp444split); + +G_END_DECLS +#endif diff --git a/plugins/rdp444/meson.build b/plugins/rdp444/meson.build new file mode 100644 index 000000000..4e90b410c --- /dev/null +++ b/plugins/rdp444/meson.build @@ -0,0 +1,15 @@ +configure_file( output : 'config.h', configuration : plugin_configuration_data) + +library('gstrdp444', + sources: [ + 'gstrdp444combine.c', + 'gstrdp444split.c', + 'plugin.c' + ], + include_directories : [common_include_dir], + c_args: ['-DHAVE_CONFIG_H'], + dependencies: [gst_dep, gstbase_dep], + install : true, + install_dir : plugins_install_dir, + name_suffix: library_suffix, +) diff --git a/plugins/rdp444/plugin.c b/plugins/rdp444/plugin.c new file mode 100644 index 000000000..253327e8f --- /dev/null +++ b/plugins/rdp444/plugin.c @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2026 Fluendo + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "gst-fluendo.h" +#include "gstrdp444combine.h" +#include "gstrdp444split.h" + +static gboolean +plugin_init (GstPlugin * plugin) +{ + gboolean ret = FALSE; + + ret |= GST_ELEMENT_REGISTER (rdp444combine, plugin); + ret |= GST_ELEMENT_REGISTER (rdp444split, plugin); + + return ret; +} + +FLUENDO_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, "rdp444", + rdp444, "RDP 444 utils plugin", plugin_init, VERSION, + FLUENDO_DEFAULT_LICENSE, PACKAGE_NAME, "http://www.fluendo.com"); From 1f491be0215602a7ff70fae2f088c0980b106c26 Mon Sep 17 00:00:00 2001 From: msabiniok Date: Mon, 2 Feb 2026 17:56:46 +0100 Subject: [PATCH 2/4] feat: implement rdp444split video splitting --- plugins/rdp444/gstrdp444split.c | 330 +++++++++++++++++++++++++++++--- plugins/rdp444/meson.build | 2 +- 2 files changed, 309 insertions(+), 23 deletions(-) diff --git a/plugins/rdp444/gstrdp444split.c b/plugins/rdp444/gstrdp444split.c index eb1834b33..b3b3cc022 100644 --- a/plugins/rdp444/gstrdp444split.c +++ b/plugins/rdp444/gstrdp444split.c @@ -8,9 +8,23 @@ * SECTION:element-rdp444split * @title: rdp444split * @short_description: Splits 4:4:4 video/x-raw into YUV 4:2:0 and Chroma 4:2:0 + * + * ## Example Pipeline + * + * ``` shell + * gst-launch-1.0 videotestsrc ! rdp444split name=demux demux.src_yuv420 ! queue ! \ + * videoconvert ! xvimagesink demux.src_chroma420 ! queue ! videoconvert ! xvimagesink + * ``` + * + * @note A significant portion of this code was generated with AI assistance. + * Please review and verify functionality before use. + * + * @warning AI-generated code may require human validation for correctness, + * security, and compliance with project standards. */ #include "gstrdp444split.h" +#include GST_DEBUG_CATEGORY_STATIC (gst_debug_rdp444split); #define GST_CAT_DEFAULT gst_debug_rdp444split @@ -27,28 +41,298 @@ GST_STATIC_PAD_TEMPLATE ("sink", ); static GstStaticPadTemplate src0_templ = -GST_STATIC_PAD_TEMPLATE ("src0", +GST_STATIC_PAD_TEMPLATE ("src_yuv420", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS ("video/x-raw, format=I420") ); static GstStaticPadTemplate src1_templ = -GST_STATIC_PAD_TEMPLATE ("src1", +GST_STATIC_PAD_TEMPLATE ("src_chroma420", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS ("video/x-raw, format=I420") ); -static GstFlowReturn rdp444split_chain (GstPad * pad, GstRDP444Split * split, +static gboolean rdp444split_sink_event (GstPad * pad, GstObject * parent, + GstEvent * event); +static GstFlowReturn rdp444split_chain (GstPad * pad, GstObject * parent, GstBuffer * buf); +static gboolean +rdp444split_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) +{ + GstRDP444Split *split = GST_RDP444_SPLIT (parent); + gboolean ret = TRUE; + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_CAPS: + { + GstCaps *caps; + GstVideoInfo vinfo_in, vinfo_out; + GstCaps *out_caps; + GstEvent *out_event; + + gst_event_parse_caps (event, &caps); + GST_DEBUG_OBJECT (split, "Got caps: %" GST_PTR_FORMAT, caps); + + if (!gst_video_info_from_caps (&vinfo_in, caps)) { + GST_ERROR_OBJECT (split, "Failed to parse caps"); + ret = FALSE; + break; + } + + if (GST_VIDEO_INFO_FORMAT (&vinfo_in) != GST_VIDEO_FORMAT_Y444) { + GST_ERROR_OBJECT (split, "Expected Y444 format"); + ret = FALSE; + break; + } + + // Create I420 output caps + gst_video_info_init (&vinfo_out); + gst_video_info_set_format (&vinfo_out, GST_VIDEO_FORMAT_I420, + GST_VIDEO_INFO_WIDTH (&vinfo_in), + GST_VIDEO_INFO_HEIGHT (&vinfo_in)); + GST_VIDEO_INFO_FPS_N (&vinfo_out) = GST_VIDEO_INFO_FPS_N (&vinfo_in); + GST_VIDEO_INFO_FPS_D (&vinfo_out) = GST_VIDEO_INFO_FPS_D (&vinfo_in); + GST_VIDEO_INFO_PAR_N (&vinfo_out) = GST_VIDEO_INFO_PAR_N (&vinfo_in); + GST_VIDEO_INFO_PAR_D (&vinfo_out) = GST_VIDEO_INFO_PAR_D (&vinfo_in); + + out_caps = gst_video_info_to_caps (&vinfo_out); + + // Push caps to both source pads + out_event = gst_event_new_caps (out_caps); + ret = gst_pad_push_event (split->srcpad_yuv420, gst_event_ref (out_event)); + ret &= gst_pad_push_event (split->srcpad_chroma420, out_event); + + gst_caps_unref (out_caps); + gst_event_unref (event); + return ret; + } + default: + // Use default event handling for other events + return gst_pad_event_default (pad, parent, event); + } + + gst_event_unref (event); + return ret; +} + static GstFlowReturn -rdp444split_chain (GstPad * pad, GstRDP444Split * split, - GstBuffer * buf) +rdp444split_chain (GstPad * pad, GstObject * parent, GstBuffer * buf) { - // here do the splitting - return GST_FLOW_OK; + GstRDP444Split *split = GST_RDP444_SPLIT (parent); + GstMapInfo map_in; + GstVideoInfo vinfo_in, vinfo_out; + GstCaps *caps; + GstBuffer *buf_main = NULL; + GstBuffer *buf_aux = NULL; + GstFlowReturn ret_main = GST_FLOW_OK; + GstFlowReturn ret_aux = GST_FLOW_OK; + + GST_LOG_OBJECT (split, "Processing Y444 buffer"); + + // Get and validate input caps + caps = gst_pad_get_current_caps (split->sinkpad); + if (!caps) { + GST_ERROR_OBJECT (split, "No caps on sink pad"); + gst_buffer_unref (buf); + return GST_FLOW_ERROR; + } + + if (!gst_video_info_from_caps (&vinfo_in, caps)) { + GST_ERROR_OBJECT (split, "Failed to parse input caps"); + gst_caps_unref (caps); + gst_buffer_unref (buf); + return GST_FLOW_ERROR; + } + gst_caps_unref (caps); + + // Verify Y444 input format + if (GST_VIDEO_INFO_FORMAT (&vinfo_in) != GST_VIDEO_FORMAT_Y444) { + GST_ERROR_OBJECT (split, "Input format must be Y444, got %s", + gst_video_format_to_string (GST_VIDEO_INFO_FORMAT (&vinfo_in))); + gst_buffer_unref (buf); + return GST_FLOW_ERROR; + } + + // Map input buffer + if (!gst_buffer_map (buf, &map_in, GST_MAP_READ)) { + GST_ERROR_OBJECT (split, "Failed to map input buffer"); + gst_buffer_unref (buf); + return GST_FLOW_ERROR; + } + + gint W = GST_VIDEO_INFO_WIDTH (&vinfo_in); + gint H = GST_VIDEO_INFO_HEIGHT (&vinfo_in); + + // Get input plane pointers and strides + gint stride_y444 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_in, 0); + gint stride_u444 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_in, 1); + gint stride_v444 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_in, 2); + + guint8 *Y444 = map_in.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 0); + guint8 *U444 = map_in.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 1); + guint8 *V444 = map_in.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 2); + + // Setup I420 output format + gst_video_info_init (&vinfo_out); + gst_video_info_set_format (&vinfo_out, GST_VIDEO_FORMAT_I420, W, H); + + // Allocate output buffers + buf_main = gst_buffer_new_allocate (NULL, GST_VIDEO_INFO_SIZE (&vinfo_out), NULL); + buf_aux = gst_buffer_new_allocate (NULL, GST_VIDEO_INFO_SIZE (&vinfo_out), NULL); + + if (!buf_main || !buf_aux) { + GST_ERROR_OBJECT (split, "Failed to allocate output buffers"); + ret_main = GST_FLOW_ERROR; + goto error; + } + + // Copy metadata and timestamps + gst_buffer_copy_into (buf_main, buf, + GST_BUFFER_COPY_METADATA | GST_BUFFER_COPY_TIMESTAMPS, + 0, -1); + gst_buffer_copy_into (buf_aux, buf, + GST_BUFFER_COPY_METADATA | GST_BUFFER_COPY_TIMESTAMPS, + 0, -1); + + // Map output buffers + GstMapInfo map_main, map_aux; + if (!gst_buffer_map (buf_main, &map_main, GST_MAP_WRITE) || + !gst_buffer_map (buf_aux, &map_aux, GST_MAP_WRITE)) { + GST_ERROR_OBJECT (split, "Failed to map output buffers"); + ret_main = GST_FLOW_ERROR; + goto error; + } + + // Get output plane pointers and strides + gint stride_y420 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_out, 0); + gint stride_u420 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_out, 1); + gint stride_v420 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_out, 2); + + guint8 *Y420_main = map_main.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 0); + guint8 *U420_main = map_main.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 1); + guint8 *V420_main = map_main.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 2); + + guint8 *Y420_aux = map_aux.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 0); + guint8 *U420_aux = map_aux.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 1); + guint8 *V420_aux = map_aux.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 2); + + /* + * Microsoft RDP AVC444v2 Encoding (from MS-RDPEGFX spec) + * + * Main View (B1, B2, B3): + * B1: Y₄₂₀(x,y) = Y₄₄₄(x,y) - Full luma + * B2: U₄₂₀(x,y) = U₄₄₄(2x, 2y) - Even pixel U + * B3: V₄₂₀(x,y) = V₄₄₄(2x, 2y) - Even pixel V + * + * Auxiliary View (B4-B9): + * B4: Y₄₂₀(x,y) = U₄₄₄(2x+1, y) - U odd columns, left half + * B5: Y₄₂₀(W/2+x,y) = V₄₄₄(2x+1, y) - V odd columns, right half + * B6: U₄₂₀(x,y) = U₄₄₄(4x, 2y+1) - U odd rows, left quarter + * B7: U₄₂₀(W/4+x,y) = V₄₄₄(4x, 2y+1) - V odd rows, right quarter + * B8: V₄₂₀(x,y) = U₄₄₄(4x+2, 2y+1) - U odd rows offset, left quarter + * B9: V₄₂₀(W/4+x,y) = V₄₄₄(4x+2, 2y+1) - V odd rows offset, right quarter + */ + + // ========== MAIN VIEW ========== + + // B1: Copy full luma plane + for (gint y = 0; y < H; y++) { + memcpy (Y420_main + y * stride_y420, Y444 + y * stride_y444, W); + } + + // B2, B3: Sample even pixels for U and V + for (gint y = 0; y < H / 2; y++) { + for (gint x = 0; x < W / 2; x++) { + U420_main[y * stride_u420 + x] = U444[(2 * y) * stride_u444 + (2 * x)]; + V420_main[y * stride_v420 + x] = V444[(2 * y) * stride_v444 + (2 * x)]; + } + } + + // ========== AUXILIARY VIEW ========== + + // B4 & B5: Y plane contains U and V odd columns + for (gint y = 0; y < H; y++) { + for (gint x = 0; x < W / 2; x++) { + gint src_x = 2 * x + 1; + if (src_x < W) { + Y420_aux[y * stride_y420 + x] = U444[y * stride_u444 + src_x]; + } + } + + for (gint x = 0; x < W / 2; x++) { + gint src_x = 2 * x + 1; + gint dst_x = W / 2 + x; + if (src_x < W) { + Y420_aux[y * stride_y420 + dst_x] = V444[y * stride_v444 + src_x]; + } + } + } + + // B6, B7, B8, B9: U and V planes + for (gint y = 0; y < H / 2; y++) { + gint src_y = 2 * y + 1; + + for (gint x = 0; x < W / 4; x++) { + gint src_x = 4 * x; + if (src_x < W && src_y < H) { + U420_aux[y * stride_u420 + x] = U444[src_y * stride_u444 + src_x]; + } + } + + for (gint x = 0; x < W / 4; x++) { + gint src_x = 4 * x; + gint dst_x = W / 4 + x; + if (src_x < W && src_y < H && dst_x < W / 2) { + U420_aux[y * stride_u420 + dst_x] = V444[src_y * stride_v444 + src_x]; + } + } + + for (gint x = 0; x < W / 4; x++) { + gint src_x = 4 * x + 2; + if (src_x < W && src_y < H) { + V420_aux[y * stride_v420 + x] = U444[src_y * stride_u444 + src_x]; + } + } + + for (gint x = 0; x < W / 4; x++) { + gint src_x = 4 * x + 2; + gint dst_x = W / 4 + x; + if (src_x < W && src_y < H && dst_x < W / 2) { + V420_aux[y * stride_v420 + dst_x] = V444[src_y * stride_v444 + src_x]; + } + } + } + + // Unmap buffers + gst_buffer_unmap (buf_main, &map_main); + gst_buffer_unmap (buf_aux, &map_aux); + gst_buffer_unmap (buf, &map_in); + + // Done with input buffer + gst_buffer_unref (buf); + + GST_LOG_OBJECT (split, "Pushing buffers"); + + // Push buffers - ownership is transferred + ret_main = gst_pad_push (split->srcpad_yuv420, buf_main); + ret_aux = gst_pad_push (split->srcpad_chroma420, buf_aux); + + // Return worst of the two flow returns + if (ret_main != GST_FLOW_OK) + return ret_main; + return ret_aux; + +error: + if (buf_main) + gst_buffer_unref (buf_main); + if (buf_aux) + gst_buffer_unref (buf_aux); + gst_buffer_unmap (buf, &map_in); + gst_buffer_unref (buf); + return ret_main; } static void @@ -56,6 +340,9 @@ gst_rdp444_split_class_init (GstRDP444SplitClass * klass) { GstElementClass *element_class = GST_ELEMENT_CLASS (klass); + GST_DEBUG_CATEGORY_INIT (gst_debug_rdp444split, "rdp444split", 0, + "RDP 444 Splitter"); + gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&sink_templ)); gst_element_class_add_pad_template (element_class, @@ -63,8 +350,6 @@ gst_rdp444_split_class_init (GstRDP444SplitClass * klass) gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&src1_templ)); - - gst_element_class_set_static_metadata (element_class, "RDP 444 Splitter", "Video/Splitter", "Splits 4:4:4 video/x-raw into YUV 4:2:0 and Chroma 4:2:0", @@ -74,17 +359,18 @@ gst_rdp444_split_class_init (GstRDP444SplitClass * klass) static void gst_rdp444_split_init (GstRDP444Split * self) { - self->sinkpad = - gst_pad_new_from_static_template (&sink_templ, "sink"); - gst_element_add_pad (GST_ELEMENT (self), self->sinkpad); - gst_pad_set_chain_function (self->sinkpad, - (GstPadChainFunction)(rdp444split_chain)); - - self->srcpad_yuv420 = - gst_pad_new_from_static_template (&src0_templ, "src0"); - gst_element_add_pad (GST_ELEMENT (self), self->srcpad_yuv420); - - self->srcpad_chroma420 = - gst_pad_new_from_static_template (&src1_templ, "src1"); - gst_element_add_pad (GST_ELEMENT (self), self->srcpad_chroma420); + self->sinkpad = gst_pad_new_from_static_template (&sink_templ, "sink"); + gst_pad_set_chain_function (self->sinkpad, + GST_DEBUG_FUNCPTR (rdp444split_chain)); + gst_pad_set_event_function (self->sinkpad, + GST_DEBUG_FUNCPTR (rdp444split_sink_event)); + gst_element_add_pad (GST_ELEMENT (self), self->sinkpad); + + self->srcpad_yuv420 = + gst_pad_new_from_static_template (&src0_templ, "src_yuv420"); + gst_element_add_pad (GST_ELEMENT (self), self->srcpad_yuv420); + + self->srcpad_chroma420 = + gst_pad_new_from_static_template (&src1_templ, "src_chroma420"); + gst_element_add_pad (GST_ELEMENT (self), self->srcpad_chroma420); } diff --git a/plugins/rdp444/meson.build b/plugins/rdp444/meson.build index 4e90b410c..92383ce9c 100644 --- a/plugins/rdp444/meson.build +++ b/plugins/rdp444/meson.build @@ -8,7 +8,7 @@ library('gstrdp444', ], include_directories : [common_include_dir], c_args: ['-DHAVE_CONFIG_H'], - dependencies: [gst_dep, gstbase_dep], + dependencies: [gst_dep, gstbase_dep, gstvideo_dep], install : true, install_dir : plugins_install_dir, name_suffix: library_suffix, From 62a1b23ca9b089f951da6b0410f504a846c51042 Mon Sep 17 00:00:00 2001 From: msabiniok Date: Tue, 3 Feb 2026 17:19:33 +0100 Subject: [PATCH 3/4] feature: implement rdp444combine element --- plugins/rdp444/gstrdp444combine.c | 371 ++++++++++++++++++++++-------- plugins/rdp444/gstrdp444split.c | 1 + 2 files changed, 278 insertions(+), 94 deletions(-) diff --git a/plugins/rdp444/gstrdp444combine.c b/plugins/rdp444/gstrdp444combine.c index 2d02260da..a83fd5f6b 100644 --- a/plugins/rdp444/gstrdp444combine.c +++ b/plugins/rdp444/gstrdp444combine.c @@ -2,14 +2,30 @@ * Copyright (C) 2026 Fluendo * * rdp444combine: Combines YUV 4:2:0 and Chroma 4:2:0 into 4:4:4 video/x-raw - + */ /** * SECTION:element-rdp444combine * @title: rdp444combine * @short_description: Combines YUV 4:2:0 and Chroma 4:2:0 into 4:4:4 video/x-raw + * + * * ## Example Pipeline + * + * ``` shell + * gst-launch-1.0 videotestsrc ! rdp444split name=demux demux.src_yuv420 ! queue ! + * mux.sink_yuv420 demux.src_chroma420 ! queue ! \ + * mux.sink_chroma420 rdp444combine name=mux ! \ + * video/x-raw,format=Y444,width=320,height=240 ! videoconvert ! xvimagesink + * ``` + * + * @note A significant portion of this code was generated with AI assistance. + * Please review and verify functionality before use. + * + * @warning AI-generated code may require human validation for correctness, + * security, and compliance with project standards. */ #include "gstrdp444combine.h" +#include GST_DEBUG_CATEGORY (gst_debug_rdp444combine); #define GST_CAT_DEFAULT gst_debug_rdp444combine @@ -25,127 +41,295 @@ static GstStaticPadTemplate sink_yuv_templ = GST_STATIC_PAD_TEMPLATE ("sink_yuv420", GST_PAD_SINK, GST_PAD_ALWAYS, - GST_STATIC_CAPS ("video/x-raw, format=I420") + GST_STATIC_CAPS ("video/x-raw, format=I420, " + "width=(int)[1,MAX], height=(int)[1,MAX], " + "framerate=(fraction)[0/1,MAX]") ); static GstStaticPadTemplate sink_chroma_templ = GST_STATIC_PAD_TEMPLATE ("sink_chroma420", GST_PAD_SINK, GST_PAD_ALWAYS, - GST_STATIC_CAPS ("video/x-raw, format=I420") + GST_STATIC_CAPS ("video/x-raw, format=I420, " + "width=(int)[1,MAX], height=(int)[1,MAX], " + "framerate=(fraction)[0/1,MAX]") ); static GstStaticPadTemplate src_templ = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, - GST_STATIC_CAPS ("video/x-raw, format=Y444") + GST_STATIC_CAPS ("video/x-raw, format=Y444, " + "width=(int)[1,MAX], height=(int)[1,MAX], " + "framerate=(fraction)[0/1,MAX]") ); + G_DEFINE_TYPE (GstRDP444Combine, gst_rdp444_combine, GST_TYPE_AGGREGATOR); GST_ELEMENT_REGISTER_DEFINE (rdp444combine, "rdp444combine", GST_RANK_NONE, gst_rdp444_combine_get_type ()); -static void -gst_rdp444_combine_dispose (GObject * object) +static GstFlowReturn +gst_rdp444_combine_aggregate (GstAggregator * aggregator, gboolean timeout) { - GstRDP444Combine *rdp444combine = GST_RDP444_COMBINE (object); -} + GstBuffer *buf_main = NULL; + GstBuffer *buf_aux = NULL; + GstBuffer *buf_out = NULL; + GstFlowReturn ret = GST_FLOW_OK; + GstMapInfo map_main, map_aux, map_out; + GstVideoInfo vinfo_in, vinfo_out; + GstCaps *caps_in = NULL; + GstPad *sink_yuv = NULL; + GstPad *sink_chroma = NULL; + GstAggregatorPad *agg_yuv = NULL; + GstAggregatorPad *agg_chroma = NULL; + + // Get aggregator pads + sink_yuv = gst_element_get_static_pad (GST_ELEMENT (aggregator), "sink_yuv420"); + sink_chroma = gst_element_get_static_pad (GST_ELEMENT (aggregator), "sink_chroma420"); + + if (!sink_yuv || !sink_chroma) { + GST_ERROR_OBJECT (aggregator, "Failed to get sink pads"); + ret = GST_FLOW_ERROR; + goto cleanup_pads; + } -static void -gst_rdp444_combine_finalize (GObject * object) -{ - GstRDP444Combine *rdp444combine = GST_RDP444_COMBINE (object); -} + agg_yuv = GST_AGGREGATOR_PAD (sink_yuv); + agg_chroma = GST_AGGREGATOR_PAD (sink_chroma); + // Pop buffers from both pads + buf_main = gst_aggregator_pad_pop_buffer (agg_yuv); + buf_aux = gst_aggregator_pad_pop_buffer (agg_chroma); -static void -gst_rdp444_combine_set_property (GObject * object, guint prop_id, - GValue const *value, GParamSpec * pspec) -{ - GstRDP444Combine *rdp444combine = GST_RDP444_COMBINE (object); -} + if (!buf_main || !buf_aux) { + GST_DEBUG_OBJECT (aggregator, "Waiting for buffers on both pads"); + ret = GST_FLOW_OK; + goto cleanup; + } -static void -gst_rdp444_combine_get_property (GObject * object, guint prop_id, GValue * value, - GParamSpec * pspec) -{ - GstRDP444Combine *rdp444combine = GST_RDP444_COMBINE (object); -} + GST_LOG_OBJECT (aggregator, "Processing I420 buffers to Y444"); -static gboolean -get_pad_resolution (GstPad *pad, gint *width, gint *height) -{ - GstCaps *caps = NULL; - GstStructure *structure = NULL; - gboolean result = FALSE; - - if (!pad || !width || !height) - return FALSE; - - caps = gst_pad_get_current_caps (pad); - if (caps) { - structure = gst_caps_get_structure (caps, 0); - if (gst_structure_get_int (structure, "width", width) && - gst_structure_get_int (structure, "height", height)) { - result = TRUE; - } - gst_caps_unref (caps); + // Get input caps and validate + caps_in = gst_pad_get_current_caps (sink_yuv); + if (!caps_in) { + GST_ERROR_OBJECT (aggregator, "No caps on YUV sink pad"); + ret = GST_FLOW_ERROR; + goto cleanup; } - return result; -} + if (!gst_video_info_from_caps (&vinfo_in, caps_in)) { + GST_ERROR_OBJECT (aggregator, "Failed to parse input caps"); + ret = GST_FLOW_ERROR; + goto cleanup; + } + if (GST_VIDEO_INFO_FORMAT (&vinfo_in) != GST_VIDEO_FORMAT_I420) { + GST_ERROR_OBJECT (aggregator, "Input format must be I420"); + ret = GST_FLOW_ERROR; + goto cleanup; + } -static GstFlowReturn -gst_rdp444_combine_aggregate (GstAggregator * aggregator, gboolean timeout) -{ - GstPad *sink_yuv = gst_element_get_static_pad (GST_ELEMENT (aggregator), "sink_yuv420"); - GstPad *sink_chroma = gst_element_get_static_pad (GST_ELEMENT (aggregator), "sink_chroma420"); - gint width_yuv = 0, height_yuv = 0; - gint width_chroma = 0, height_chroma = 0; - - if ( - !get_pad_resolution(sink_yuv, &width_yuv, &height_yuv) == FALSE & - !get_pad_resolution(sink_chroma, &width_chroma, &height_chroma)){ - GST_ERROR_OBJECT (aggregator, "Could not get pad resolutions"); - return GST_FLOW_ERROR; + gint W = GST_VIDEO_INFO_WIDTH (&vinfo_in); + gint H = GST_VIDEO_INFO_HEIGHT (&vinfo_in); + + // Setup Y444 output format + gst_video_info_init (&vinfo_out); + gst_video_info_set_format (&vinfo_out, GST_VIDEO_FORMAT_Y444, W, H); + GST_VIDEO_INFO_FPS_N (&vinfo_out) = GST_VIDEO_INFO_FPS_N (&vinfo_in); + GST_VIDEO_INFO_FPS_D (&vinfo_out) = GST_VIDEO_INFO_FPS_D (&vinfo_in); + GST_VIDEO_INFO_PAR_N (&vinfo_out) = GST_VIDEO_INFO_PAR_N (&vinfo_in); + GST_VIDEO_INFO_PAR_D (&vinfo_out) = GST_VIDEO_INFO_PAR_D (&vinfo_in); + + // Allocate output buffer + buf_out = gst_buffer_new_allocate (NULL, GST_VIDEO_INFO_SIZE (&vinfo_out), NULL); + if (!buf_out) { + GST_ERROR_OBJECT (aggregator, "Failed to allocate output buffer"); + ret = GST_FLOW_ERROR; + goto cleanup; } - if (width_yuv != width_chroma || height_yuv != height_chroma) { - GST_ERROR_OBJECT (aggregator, "Resolution mismatch: YUV %dx%d vs Chroma %dx%d", - width_yuv, height_yuv, width_chroma, height_chroma); - return GST_FLOW_ERROR; + // Copy metadata from main buffer + gst_buffer_copy_into (buf_out, buf_main, + GST_BUFFER_COPY_METADATA | GST_BUFFER_COPY_TIMESTAMPS, + 0, -1); + + // Map all buffers + if (!gst_buffer_map (buf_main, &map_main, GST_MAP_READ)) { + GST_ERROR_OBJECT (aggregator, "Failed to map main buffer"); + ret = GST_FLOW_ERROR; + goto cleanup; } - GstCaps *caps_yuv = NULL; - if (sink_yuv) { - caps_yuv = gst_pad_get_current_caps (sink_yuv); - gst_object_unref (sink_yuv); + if (!gst_buffer_map (buf_aux, &map_aux, GST_MAP_READ)) { + GST_ERROR_OBJECT (aggregator, "Failed to map aux buffer"); + gst_buffer_unmap (buf_main, &map_main); + ret = GST_FLOW_ERROR; + goto cleanup; + } + + if (!gst_buffer_map (buf_out, &map_out, GST_MAP_WRITE)) { + GST_ERROR_OBJECT (aggregator, "Failed to map output buffer"); + gst_buffer_unmap (buf_main, &map_main); + gst_buffer_unmap (buf_aux, &map_aux); + ret = GST_FLOW_ERROR; + goto cleanup; + } + + // Get I420 input plane pointers and strides + gint stride_y420 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_in, 0); + gint stride_u420 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_in, 1); + gint stride_v420 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_in, 2); + + guint8 *Y420_main = map_main.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 0); + guint8 *U420_main = map_main.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 1); + guint8 *V420_main = map_main.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 2); + + guint8 *Y420_aux = map_aux.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 0); + guint8 *U420_aux = map_aux.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 1); + guint8 *V420_aux = map_aux.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_in, 2); + + // Get Y444 output plane pointers and strides + gint stride_y444 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_out, 0); + gint stride_u444 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_out, 1); + gint stride_v444 = GST_VIDEO_INFO_PLANE_STRIDE (&vinfo_out, 2); + + guint8 *Y444 = map_out.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 0); + guint8 *U444 = map_out.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 1); + guint8 *V444 = map_out.data + GST_VIDEO_INFO_PLANE_OFFSET (&vinfo_out, 2); + + /* + * Microsoft RDP AVC444v2 Decoding (Inverse of Encoding) + * + * Reconstructing Y444 from Main View (B1, B2, B3) and Auxiliary View (B4-B9): + * Y₄₄₄(x,y) = Y₄₂₀_main(x,y) - From B1: Full luma + * U₄₄₄(2x, 2y) = U₄₂₀_main(x,y) - From B2: Even pixel U + * V₄₄₄(2x, 2y) = V₄₂₀_main(x,y) - From B3: Even pixel V + * U₄₄₄(2x+1, y) = Y₄₂₀_aux(x,y) - From B4: U odd columns, left half + * V₄₄₄(2x+1, y) = Y₄₂₀_aux(W/2+x,y) - From B5: V odd columns, right half + * U₄₄₄(4x, 2y+1) = U₄₂₀_aux(x,y) - From B6: U odd rows, left quarter + * V₄₄₄(4x, 2y+1) = U₄₂₀_aux(W/4+x,y) - From B7: V odd rows, right quarter + * U₄₄₄(4x+2, 2y+1) = V₄₂₀_aux(x,y) - From B8: U odd rows offset, left quarter + * V₄₄₄(4x+2, 2y+1) = V₄₂₀_aux(W/4+x,y)- From B9: V odd rows offset, right quarter + */ + + // ========== RECONSTRUCT FROM MAIN VIEW ========== + + // B1: Copy full luma plane from main + for (gint y = 0; y < H; y++) { + memcpy (Y444 + y * stride_y444, Y420_main + y * stride_y420, W); + } + + // B2, B3: Reconstruct even pixels for U and V from main + for (gint y = 0; y < H / 2; y++) { + for (gint x = 0; x < W / 2; x++) { + U444[(2 * y) * stride_u444 + (2 * x)] = U420_main[y * stride_u420 + x]; + V444[(2 * y) * stride_v444 + (2 * x)] = V420_main[y * stride_v420 + x]; + } + } + + // ========== RECONSTRUCT FROM AUXILIARY VIEW ========== + + // B4 & B5: Reconstruct U and V odd columns from auxiliary Y plane + for (gint y = 0; y < H; y++) { + // B4: U odd columns from left half + for (gint x = 0; x < W / 2; x++) { + gint dst_x = 2 * x + 1; + if (dst_x < W) { + U444[y * stride_u444 + dst_x] = Y420_aux[y * stride_y420 + x]; + } + } + + // B5: V odd columns from right half + for (gint x = 0; x < W / 2; x++) { + gint dst_x = 2 * x + 1; + gint src_x = W / 2 + x; + if (dst_x < W) { + V444[y * stride_v444 + dst_x] = Y420_aux[y * stride_y420 + src_x]; + } + } } - if (caps_yuv) { - GstCaps *out_caps = gst_caps_copy (caps_yuv); - gst_caps_set_simple (out_caps, "format", G_TYPE_STRING, "Y444", NULL); - gst_aggregator_set_src_caps (aggregator, out_caps); - gst_caps_unref (caps_yuv); - gst_caps_unref (out_caps); + // B6, B7, B8, B9: Reconstruct U and V odd rows from auxiliary U and V planes + for (gint y = 0; y < H / 2; y++) { + gint dst_y = 2 * y + 1; + + // B6: U odd rows from auxiliary U left quarter + for (gint x = 0; x < W / 4; x++) { + gint dst_x = 4 * x; + if (dst_x < W && dst_y < H) { + U444[dst_y * stride_u444 + dst_x] = U420_aux[y * stride_u420 + x]; + } + } + + // B7: V odd rows from auxiliary U right quarter + for (gint x = 0; x < W / 4; x++) { + gint dst_x = 4 * x; + gint src_x = W / 4 + x; + if (dst_x < W && dst_y < H && src_x < W / 2) { + V444[dst_y * stride_v444 + dst_x] = U420_aux[y * stride_u420 + src_x]; + } + } + + // B8: U odd rows offset from auxiliary V left quarter + for (gint x = 0; x < W / 4; x++) { + gint dst_x = 4 * x + 2; + if (dst_x < W && dst_y < H) { + U444[dst_y * stride_u444 + dst_x] = V420_aux[y * stride_v420 + x]; + } + } + + // B9: V odd rows offset from auxiliary V right quarter + for (gint x = 0; x < W / 4; x++) { + gint dst_x = 4 * x + 2; + gint src_x = W / 4 + x; + if (dst_x < W && dst_y < H && src_x < W / 2) { + V444[dst_y * stride_v444 + dst_x] = V420_aux[y * stride_v420 + src_x]; + } + } } - return GST_FLOW_OK; + // Unmap buffers + gst_buffer_unmap (buf_out, &map_out); + gst_buffer_unmap (buf_aux, &map_aux); + gst_buffer_unmap (buf_main, &map_main); + + GST_LOG_OBJECT (aggregator, "Pushing Y444 output buffer"); + + // Push the output buffer + ret = gst_aggregator_finish_buffer (aggregator, buf_out); + buf_out = NULL; // Ownership transferred + +cleanup: + if (caps_in) + gst_caps_unref (caps_in); + if (buf_main) + gst_buffer_unref (buf_main); + if (buf_aux) + gst_buffer_unref (buf_aux); + if (buf_out) + gst_buffer_unref (buf_out); + +cleanup_pads: + if (sink_yuv) + gst_object_unref (sink_yuv); + if (sink_chroma) + gst_object_unref (sink_chroma); + + return ret; } static void gst_rdp444_combine_class_init (GstRDP444CombineClass * klass) { - GObjectClass *object_class; GstElementClass *element_class; GstAggregatorClass *aggregator_class; - object_class = G_OBJECT_CLASS (klass); element_class = GST_ELEMENT_CLASS (klass); aggregator_class = GST_AGGREGATOR_CLASS (klass); + GST_DEBUG_CATEGORY_INIT (gst_debug_rdp444combine, "rdp444combine", 0, + "RDP 444 Combiner"); + gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&sink_yuv_templ)); @@ -155,32 +339,31 @@ gst_rdp444_combine_class_init (GstRDP444CombineClass * klass) gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&src_templ)); - object_class->dispose = GST_DEBUG_FUNCPTR (gst_rdp444_combine_dispose); - object_class->finalize = GST_DEBUG_FUNCPTR (gst_rdp444_combine_finalize); - object_class->set_property = GST_DEBUG_FUNCPTR (gst_rdp444_combine_set_property); - object_class->get_property = GST_DEBUG_FUNCPTR (gst_rdp444_combine_get_property); - aggregator_class->aggregate = GST_DEBUG_FUNCPTR (gst_rdp444_combine_aggregate); gst_element_class_set_static_metadata (element_class, - "rdp444combine", - "Element", + "RDP 444 Combiner", "Video/Combiner", "Combines YUV 4:2:0 and Chroma 4:2:0 into 4:4:4 video/x-raw", - "Fluendo S.A. "); + "Fluendo "); } static void gst_rdp444_combine_init (GstRDP444Combine * rdp444combine) { - GstAggregator *agg = GST_AGGREGATOR (rdp444combine); - - GstPad *srcpad = - gst_pad_new_from_static_template (&src_templ, "src"); - - gst_element_add_pad (GST_ELEMENT (rdp444combine), srcpad); - gst_element_add_pad(GST_ELEMENT (rdp444combine), - gst_pad_new_from_static_template (&sink_yuv_templ, "sink_yuv420")); - gst_element_add_pad(GST_ELEMENT (rdp444combine), - gst_pad_new_from_static_template (&sink_chroma_templ, "sink_chroma420")); - + GstPadTemplate *templ; + GstAggregatorPad *pad; + + // Create sink_yuv420 pad + templ = gst_static_pad_template_get (&sink_yuv_templ); + pad = GST_AGGREGATOR_PAD (g_object_new (GST_TYPE_AGGREGATOR_PAD, + "name", "sink_yuv420", "direction", GST_PAD_SINK, "template", templ, NULL)); + gst_object_unref (templ); + gst_element_add_pad (GST_ELEMENT (rdp444combine), GST_PAD (pad)); + + // Create sink_chroma420 pad + templ = gst_static_pad_template_get (&sink_chroma_templ); + pad = GST_AGGREGATOR_PAD (g_object_new (GST_TYPE_AGGREGATOR_PAD, + "name", "sink_chroma420", "direction", GST_PAD_SINK, "template", templ, NULL)); + gst_object_unref (templ); + gst_element_add_pad (GST_ELEMENT (rdp444combine), GST_PAD (pad)); } diff --git a/plugins/rdp444/gstrdp444split.c b/plugins/rdp444/gstrdp444split.c index b3b3cc022..cb3dedec0 100644 --- a/plugins/rdp444/gstrdp444split.c +++ b/plugins/rdp444/gstrdp444split.c @@ -121,6 +121,7 @@ rdp444split_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) static GstFlowReturn rdp444split_chain (GstPad * pad, GstObject * parent, GstBuffer * buf) { + GstRDP444Split *split = GST_RDP444_SPLIT (parent); GstMapInfo map_in; GstVideoInfo vinfo_in, vinfo_out; From ee3c3bba52d541ded396de3974ab78db31e9482d Mon Sep 17 00:00:00 2001 From: msabiniok Date: Wed, 4 Feb 2026 13:10:42 +0100 Subject: [PATCH 4/4] doc: Add rdp44 README.md --- plugins/rdp444/README.md | 165 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 plugins/rdp444/README.md diff --git a/plugins/rdp444/README.md b/plugins/rdp444/README.md new file mode 100644 index 000000000..fb3c2bc31 --- /dev/null +++ b/plugins/rdp444/README.md @@ -0,0 +1,165 @@ +# RDP 4:4:4 GStreamer Elements (AI Generated) + +## Overview + +This plugin provides two complementary GStreamer elements that implement the Microsoft RDP AVC444v2 encoding scheme for splitting and combining 4:4:4 chroma video streams. This encoding is used in Remote Desktop Protocol (RDP) to efficiently transmit full chroma video over two separate 4:2:0 video streams. + +### Elements + +- **rdp444split** - Splits Y444 (4:4:4) video into two I420 (4:2:0) streams +- **rdp444combine** - Combines two I420 (4:2:0) streams back into Y444 (4:4:4) video + +## rdp444split + +### Description + +The `rdp444split` element takes a single Y444 input stream and splits it into two I420 outputs according to the Microsoft RDP AVC444v2 specification. The element distributes the chroma information across two 4:2:0 streams in a way that allows perfect reconstruction. + +### Pads + +- **Sink pad**: `sink` + - Caps: `video/x-raw, format=Y444` + +- **Source pads**: + - `src_yuv420` - Main view containing full luma and even-pixel chroma + - `src_chroma420` - Auxiliary view containing odd-pixel chroma information + - Caps: `video/x-raw, format=I420` + +### Encoding Scheme + +The element implements the Microsoft RDP AVC444v2 encoding specification: + +**Main View (src_yuv420):** +- B1: Y₄₂₀(x,y) = Y₄₄₄(x,y) - Full luma plane +- B2: U₄₂₀(x,y) = U₄₄₄(2x, 2y) - Even pixel U samples +- B3: V₄₂₀(x,y) = V₄₄₄(2x, 2y) - Even pixel V samples + +**Auxiliary View (src_chroma420):** +- B4: Y₄₂₀(x,y) = U₄₄₄(2x+1, y) - U odd columns (left half) +- B5: Y₄₂₀(W/2+x,y) = V₄₄₄(2x+1, y) - V odd columns (right half) +- B6: U₄₂₀(x,y) = U₄₄₄(4x, 2y+1) - U odd rows (left quarter) +- B7: U₄₂₀(W/4+x,y) = V₄₄₄(4x, 2y+1) - V odd rows (right quarter) +- B8: V₄₂₀(x,y) = U₄₄₄(4x+2, 2y+1) - U odd rows offset (left quarter) +- B9: V₄₂₀(W/4+x,y) = V₄₄₄(4x+2, 2y+1) - V odd rows offset (right quarter) + +### Properties + +None + +## rdp444combine + +### Description + +The `rdp444combine` element performs the inverse operation of `rdp444split`. It takes two I420 input streams and reconstructs the original Y444 output by reversing the AVC444v2 encoding scheme. + +### Pads + +- **Sink pads**: + - `sink_yuv420` - Main view (full luma + even chroma) + - `sink_chroma420` - Auxiliary view (odd chroma samples) + - Caps: `video/x-raw, format=I420` + +- **Source pad**: `src` + - Caps: `video/x-raw, format=Y444` + +### Decoding Scheme + +The element reconstructs Y444 by reversing the encoding: + +- Y₄₄₄(x,y) = Y₄₂₀_main(x,y) - From B1: Full luma +- U₄₄₄(2x, 2y) = U₄₂₀_main(x,y) - From B2: Even pixel U +- V₄₄₄(2x, 2y) = V₄₂₀_main(x,y) - From B3: Even pixel V +- U₄₄₄(2x+1, y) = Y₄₂₀_aux(x,y) - From B4: U odd columns +- V₄₄₄(2x+1, y) = Y₄₂₀_aux(W/2+x,y) - From B5: V odd columns +- U₄₄₄(4x, 2y+1) = U₄₂₀_aux(x,y) - From B6: U odd rows +- V₄₄₄(4x, 2y+1) = U₄₂₀_aux(W/4+x,y) - From B7: V odd rows +- U₄₄₄(4x+2, 2y+1) = V₄₂₀_aux(x,y) - From B8: U odd rows offset +- V₄₄₄(4x+2, 2y+1) = V₄₂₀_aux(W/4+x,y) - From B9: V odd rows offset + +### Properties + +None + +## Example Pipelines + +### Round-trip Test + +Complete pipeline that splits and recombines Y444 video: + +```bash +gst-launch-1.0 -v videotestsrc ! \ + "video/x-raw,format=Y444,width=640,height=480,framerate=30/1" ! \ + rdp444split name=split \ + split.src_yuv420 ! queue ! combine.sink_yuv420 \ + split.src_chroma420 ! queue ! combine.sink_chroma420 \ + rdp444combine name=combine ! \ + "video/x-raw,format=Y444,width=640,height=480" ! \ + videoconvert ! autovideosink +``` + +This pipeline demonstrates: +1. Generating Y444 test pattern (640x480) +2. Splitting into two I420 streams (main + auxiliary) +3. Routing through queues for synchronization +4. Recombining back to Y444 +5. Displaying the reconstructed video + +### Viewing Split Output + +To visualize the split streams (note: auxiliary view will appear corrupted as shown in the example image): + +```bash +# View main stream +gst-launch-1.0 videotestsrc ! \ + "video/x-raw,format=Y444,width=640,height=480" ! \ + rdp444split name=split \ + split.src_yuv420 ! videoconvert ! autovideosink + +# View auxiliary stream (chroma data packed into I420) +gst-launch-1.0 videotestsrc ! \ + "video/x-raw,format=Y444,width=640,height=480" ! \ + rdp444split name=split \ + split.src_chroma420 ! videoconvert ! autovideosink +``` + +**Note**: When viewing the auxiliary stream directly, the output will appear corrupted/fragmented because it contains packed chroma samples that are not meant to be displayed as a normal image. This is expected behavior - the auxiliary stream only makes sense when recombined with the main stream. + +## Technical Details + +### Implementation + +Both elements are implemented as GStreamer bin elements: +- `rdp444split` extends `GstElement` with custom pad management +- `rdp444combine` extends `GstAggregator` for synchronized input handling + +### Performance Considerations + +- The split operation uses `memcpy()` for luma plane copying (efficient) +- Chroma resampling involves per-pixel operations +- Combine operation reconstructs full chroma resolution +- Both elements preserve timestamps and metadata + +### Use Cases + +1. **Remote Desktop Protocol**: Transmit 4:4:4 video over RDP using two H.264 streams +2. **High-quality video transmission**: Leverage existing 4:2:0 encoders for 4:4:4 content +3. **Video conferencing**: Maintain full chroma resolution in bandwidth-constrained scenarios +4. **Testing**: Verify codec implementations and video processing pipelines + +## Building + +The plugin is built as part of the flu-plugins-oss project: + +```bash +meson setup builddir +ninja -C builddir +``` + +## References + +- [MS-RDPEGFX]: Remote Desktop Protocol: Graphics Pipeline Extension +## License + +Copyright (C) 2026 Fluendo + +See LICENSE file for details.