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/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. diff --git a/plugins/rdp444/gstrdp444combine.c b/plugins/rdp444/gstrdp444combine.c new file mode 100644 index 000000000..a83fd5f6b --- /dev/null +++ b/plugins/rdp444/gstrdp444combine.c @@ -0,0 +1,369 @@ +/* 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 + * + * * ## 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 +#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, " + "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, " + "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, " + "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 GstFlowReturn +gst_rdp444_combine_aggregate (GstAggregator * aggregator, gboolean timeout) +{ + 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; + } + + 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); + + if (!buf_main || !buf_aux) { + GST_DEBUG_OBJECT (aggregator, "Waiting for buffers on both pads"); + ret = GST_FLOW_OK; + goto cleanup; + } + + GST_LOG_OBJECT (aggregator, "Processing I420 buffers to Y444"); + + // 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; + } + + 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; + } + + 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; + } + + // 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; + } + + 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]; + } + } + } + + // 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]; + } + } + } + + // 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) +{ + GstElementClass *element_class; + GstAggregatorClass *aggregator_class; + + 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)); + + 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)); + + aggregator_class->aggregate = GST_DEBUG_FUNCPTR (gst_rdp444_combine_aggregate); + + gst_element_class_set_static_metadata (element_class, + "RDP 444 Combiner", "Video/Combiner", + "Combines YUV 4:2:0 and Chroma 4:2:0 into 4:4:4 video/x-raw", + "Fluendo "); +} + +static void +gst_rdp444_combine_init (GstRDP444Combine * rdp444combine) +{ + 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/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..cb3dedec0 --- /dev/null +++ b/plugins/rdp444/gstrdp444split.c @@ -0,0 +1,377 @@ +/* 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 + * + * ## 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 + +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 ("src_yuv420", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("video/x-raw, format=I420") +); + +static GstStaticPadTemplate src1_templ = +GST_STATIC_PAD_TEMPLATE ("src_chroma420", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("video/x-raw, format=I420") +); + +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, GstObject * parent, GstBuffer * buf) +{ + + 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 +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, + 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_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/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..92383ce9c --- /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, gstvideo_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");