6#include <initializer_list>
12#define GST_CAT_DEFAULT gst_qgc_debug
31 GST_STATIC_PAD_TEMPLATE(
"sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS(
"video/x-raw(ANY)"));
40 explicit BinChain(GstBin* bin) : m_bin(bin) {}
48 gst_element_remove_pad(GST_ELEMENT(m_bin), m_ghost);
50 while (m_ownedCount > 0) {
51 gst_bin_remove(m_bin, m_owned[--m_ownedCount]);
55 BinChain(
const BinChain&) =
delete;
56 BinChain& operator=(
const BinChain&) =
delete;
63 if (m_ownedCount == m_owned.size()) {
64 gst_object_unref(element);
67 if (!gst_bin_add(m_bin, element)) {
68 gst_object_unref(element);
71 m_owned[m_ownedCount++] = element;
75 bool linkChain(std::initializer_list<GstElement*> chain)
82 if (prev && !gst_element_link(prev, element)) {
92 GstPad* sinkpad = gst_element_get_static_pad(head,
"sink");
96 m_ghost = gst_ghost_pad_new(
"sink", sinkpad);
97 gst_object_unref(sinkpad);
101 if (!gst_element_add_pad(GST_ELEMENT(m_bin), m_ghost)) {
102 gst_object_unref(m_ghost);
109 void commit() { m_committed =
true; }
113 std::array<GstElement*, 5> m_owned{};
114 std::size_t m_ownedCount = 0;
115 GstPad* m_ghost =
nullptr;
116 bool m_committed =
false;
121#ifdef QGC_GST_BUILD_TESTING
122gboolean gst_qgc_video_sink_bin_rejects_failed_adopt_for_test()
124 GstElement* target = gst_bin_new(
"qgc-adopt-target");
125 GstElement* parent = gst_bin_new(
"qgc-adopt-existing-parent");
126 GstElement* element = gst_element_factory_make(
"identity",
"preparented");
127 if (!target || !parent || !element) {
128 gst_clear_object(&element);
129 gst_clear_object(&parent);
130 gst_clear_object(&target);
134 if (!gst_bin_add(GST_BIN(parent), element)) {
135 gst_object_unref(parent);
136 gst_object_unref(target);
140 auto* chain =
new BinChain(GST_BIN(target));
141 gst_object_ref(element);
146 GstObject* currentParent = gst_object_get_parent(GST_OBJECT(element));
147 const gboolean rejected = (adopted ==
nullptr) && (currentParent == GST_OBJECT(parent));
149 gst_object_unref(currentParent);
151 gst_object_unref(parent);
152 gst_object_unref(target);
157#define gst_qgc_video_sink_bin_parent_class parent_class
171 GObjectClass* object_class = G_OBJECT_CLASS(klass);
172 GstElementClass* element_class = GST_ELEMENT_CLASS(klass);
181 "gpu-zerocopy",
"GPU zero-copy",
"Enable the platform GPU zero-copy path. Construct-only.", FALSE,
182 (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
185 g_param_spec_string(
"conversion-element",
"Conversion element factory",
186 "Factory name to use for the CPU branch's color conversion. Empty/NULL = auto-probe.", NULL,
187 (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
190 "disable-par",
"Disable PAR=1/1 capsfilter",
191 "Skip the pixel-aspect-ratio=1/1 capsfilter on the CPU branch (workaround for "
192 "v4l2 drivers without VIDIOC_CROPCAP). Construct-only.",
193 FALSE, (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
196 g_param_spec_boolean(
"sync",
"Sync",
"Proxied to the inner basesink: sync rendering to the clock.", FALSE,
197 (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
202 g_param_spec_boolean(
"qos",
"QoS",
"Proxied to the inner basesink: generate QoS events upstream.", FALSE,
203 (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
206 "processing-deadline",
"Processing deadline",
207 "Proxied to the inner basesink: maximum buffer processing time in nanoseconds.", 0, G_MAXUINT64,
208 G_GUINT64_CONSTANT(20000000), (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
212 gst_element_class_add_static_pad_template(GST_ELEMENT_CLASS(klass), &
sink_factory);
214 gst_element_class_set_static_metadata(element_class,
"QGC Video Sink Bin",
"Sink/Video/Bin",
215 "QVideoSink-based video sink for QGroundControl",
"QGroundControl team");
220 self->videoconvert = NULL;
221 self->glupload = NULL;
222 self->videosink = NULL;
223 self->format_capsfilter = NULL;
224 self->par_capsfilter = NULL;
225 self->gpu_zerocopy = FALSE;
226 self->conversion_element = NULL;
227 self->disable_par = FALSE;
230 self->processing_deadline = G_GUINT64_CONSTANT(20000000);
236 if (self->conversion_element && self->conversion_element[0] !=
'\0') {
237 GstElement* e = gst_element_factory_make(self->conversion_element, NULL);
239 GST_INFO_OBJECT(self,
"Using conversion-element override '%s'", self->conversion_element);
242 GST_WARNING_OBJECT(self,
"conversion-element='%s' factory missing — falling through to defaults",
243 self->conversion_element);
245 static const char* kSoCFactories[] = {
"imxvideoconvert_g2d",
"nvvidconv", NULL};
246 for (
int i = 0; kSoCFactories[i] != NULL; ++i) {
247 if (
GstElement* e = gst_element_factory_make(kSoCFactories[i], NULL)) {
248 GST_INFO_OBJECT(self,
"Using SoC conversion element '%s'", kSoCFactories[i]);
252 return gst_element_factory_make(
"videoconvert", NULL);
258 BinChain chain(GST_BIN(self));
259 if (!chain.adopt(videosink)) {
260 gst_object_unref(capsf);
261 GST_ERROR_OBJECT(self,
"Failed to add qgcqvideosink GPU sink element to bin");
264 if (!chain.adopt(capsf)) {
265 GST_ERROR_OBJECT(self,
"Failed to add qgcqvideosink GPU capsfilter element to bin");
270 GstCaps* caps = gst_caps_from_string(capsStr.c_str());
272 GST_ERROR_OBJECT(self,
"gst_caps_from_string() returned NULL for GPU caps");
277 g_object_set(capsf,
"caps", caps, NULL);
278 gst_caps_unref(caps);
280#if defined(QGC_GST_BIN_USE_GLUPLOAD)
281 GstElement* glupload = chain.adopt(gst_element_factory_make(
"glupload", NULL));
283 GST_ERROR_OBJECT(self,
"Failed to create glupload element");
288 GstElement* glcolorconvert = chain.adopt(gst_element_factory_make(
"glcolorconvert", NULL));
289 if (!glcolorconvert) {
290 GST_ERROR_OBJECT(self,
"Failed to create glcolorconvert element");
293 if (!chain.linkChain({glupload, glcolorconvert, capsf, videosink}) || !chain.ghostSink(glupload)) {
294 GST_ERROR_OBJECT(self,
"Failed to link/ghost glupload→glcolorconvert→capsfilter→qgcqvideosink GPU path");
299#if defined(QGC_HAS_GST_D3D11_GPU_PATH)
306 GstElement* d3d11upload = gst_element_factory_make(
"d3d11upload",
nullptr);
307 GstElement* d3d11convert = gst_element_factory_make(
"d3d11convert",
nullptr);
308 if (d3d11upload && d3d11convert) {
309 if (!chain.adopt(d3d11upload)) {
310 gst_clear_object(&d3d11convert);
311 GST_ERROR_OBJECT(self,
"Failed to add d3d11upload to GPU path");
314 if (!chain.adopt(d3d11convert)) {
315 GST_ERROR_OBJECT(self,
"Failed to add d3d11convert to GPU path");
318 if (!chain.linkChain({d3d11upload, d3d11convert, capsf, videosink}) || !chain.ghostSink(d3d11upload)) {
319 GST_ERROR_OBJECT(self,
"Failed to link/ghost d3d11upload→d3d11convert→capsfilter→qgcqvideosink");
323 gst_clear_object(&d3d11upload);
324 gst_clear_object(&d3d11convert);
325 if (!chain.linkChain({capsf, videosink}) || !chain.ghostSink(capsf)) {
326 GST_ERROR_OBJECT(self,
"Failed to link/ghost qgcqvideosink (GPU path, d3d11 convert unavailable)");
331 if (!chain.linkChain({capsf, videosink}) || !chain.ghostSink(capsf)) {
332 GST_ERROR_OBJECT(self,
"Failed to link/ghost qgcqvideosink (GPU path)");
339 self->glupload = glupload;
340 self->format_capsfilter = capsf;
341 self->videosink = videosink;
343#if defined(QGC_GST_BIN_USE_GLUPLOAD)
344 GST_INFO_OBJECT(self,
"Using glupload→qgcqvideosink GPU path (DMABuf→GL EGLImage import)");
345#elif defined(QGC_GST_BIN_USE_DMABUF)
346 GST_INFO_OBJECT(self,
"Using qgcqvideosink GPU path (direct DMABuf import, no glupload)");
348 GST_INFO_OBJECT(self,
"Using qgcqvideosink GPU path (native memory passthrough)");
356 BinChain chain(GST_BIN(self));
357 if (!chain.adopt(videosink)) {
358 gst_object_unref(capsf);
359 GST_ERROR_OBJECT(self,
"Failed to add qgcqvideosink CPU sink element to bin");
362 if (!chain.adopt(capsf)) {
363 GST_ERROR_OBJECT(self,
"Failed to add qgcqvideosink CPU capsfilter element to bin");
369 GST_ERROR_OBJECT(self,
"Failed to create video conversion element");
376 GST_ERROR_OBJECT(self,
"gst_caps_from_string() returned NULL for CPU caps");
379 g_object_set(capsf,
"caps", caps, NULL);
380 gst_caps_unref(caps);
385 if (!self->disable_par) {
386 par = chain.adopt(gst_element_factory_make(
"capsfilter",
"qgc-par-filter"));
388 GST_WARNING_OBJECT(self,
"capsfilter factory missing — PAR normalization disabled");
390 GstCaps* parCaps = gst_caps_from_string(
"video/x-raw, pixel-aspect-ratio=(fraction)1/1");
392 g_object_set(par,
"caps", parCaps, NULL);
393 gst_caps_unref(parCaps);
398 const bool linked = par ? chain.linkChain({videoconvert, par, capsf, videosink})
399 : chain.linkChain({videoconvert, capsf, videosink});
400 if (!linked || !chain.ghostSink(videoconvert)) {
401 GST_ERROR_OBJECT(self,
"Failed to link/ghost CPU path");
406 self->videoconvert = videoconvert;
407 self->par_capsfilter = par;
408 self->format_capsfilter = capsf;
409 self->videosink = videosink;
411 GST_INFO_OBJECT(self,
"Using qgcqvideosink CPU path (videoconvert%s → caps → qgcqvideosink → QVideoSink)",
412 par ?
" → PAR=1/1" :
"");
419 GstElement* videosink = gst_element_factory_make_full(
"qgcqvideosink",
"name",
"qgcqvideosink",
"gpu-zerocopy",
420 self->gpu_zerocopy, NULL);
422 GST_ERROR_OBJECT(self,
"Failed to create qgcqvideosink element");
425 g_object_set(videosink,
"active", (gboolean) TRUE,
"sync", (gboolean) self->sync,
"qos", (gboolean) self->qos,
426 "processing-deadline", self->processing_deadline, NULL);
428 GstElement* capsf = gst_element_factory_make(
"capsfilter",
"qgc-format-filter");
430 GST_ERROR_OBJECT(self,
"Failed to create capsfilter for qgcqvideosink");
431 gst_clear_object(&videosink);
443 if (GstPad* videosinkPad = gst_element_get_static_pad(videosink,
"sink")) {
444 const gulong probe_id = gst_pad_add_probe(videosinkPad, GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM,
448 self,
"gst_pad_add_probe(QUERY_DOWNSTREAM) returned 0 — qgcqvideosink query interception disabled");
450 gst_object_unref(videosinkPad);
464 GstQgcVideoSinkBin* self = GST_QGC_VIDEO_SINK_BIN(
object);
465 self->videosink = NULL;
466 self->videoconvert = NULL;
467 self->par_capsfilter = NULL;
468 self->format_capsfilter = NULL;
469 self->glupload = NULL;
470 g_clear_pointer(&self->conversion_element, g_free);
478 GstQgcVideoSinkBin* self = GST_QGC_VIDEO_SINK_BIN(element);
479 if (transition == GST_STATE_CHANGE_NULL_TO_READY && !self->videosink) {
480 GST_ELEMENT_ERROR(self, RESOURCE, NOT_FOUND,
481 (
"qgcvideosinkbin construction failed; cannot transition to READY"),
482 (
"see prior GST_ERROR messages from this element for the underlying cause"));
483 return GST_STATE_CHANGE_FAILURE;
490 GstQgcVideoSinkBin* self = GST_QGC_VIDEO_SINK_BIN(
object);
494 self->gpu_zerocopy = g_value_get_boolean(value);
497 GST_OBJECT_LOCK(self);
498 g_free(self->conversion_element);
499 self->conversion_element = g_value_dup_string(value);
500 GST_OBJECT_UNLOCK(self);
503 self->disable_par = g_value_get_boolean(value);
506 self->sync = g_value_get_boolean(value);
508 g_object_set(self->videosink,
"sync", (gboolean) self->sync, NULL);
511 self->qos = g_value_get_boolean(value);
513 g_object_set(self->videosink,
"qos", (gboolean) self->qos, NULL);
516 self->processing_deadline = g_value_get_uint64(value);
518 g_object_set(self->videosink,
"processing-deadline", self->processing_deadline, NULL);
521 G_OBJECT_WARN_INVALID_PROPERTY_ID(
object, prop_id, pspec);
528 GstQgcVideoSinkBin* self = GST_QGC_VIDEO_SINK_BIN(
object);
532 g_value_set_boolean(value, self->gpu_zerocopy);
535 GST_OBJECT_LOCK(self);
536 g_value_set_string(value, self->conversion_element);
537 GST_OBJECT_UNLOCK(self);
540 g_value_set_boolean(value, self->disable_par);
543 g_value_set_boolean(value, self->sync);
546 g_value_set_boolean(value, self->qos);
549 g_value_set_uint64(value, self->processing_deadline);
552 G_OBJECT_WARN_INVALID_PROPERTY_ID(
object, prop_id, pspec);
559 g_return_val_if_fail(GST_IS_QGC_VIDEO_SINK_BIN(self), NULL);
560 return self->videosink ? GST_ELEMENT(gst_object_ref(self->videosink)) : NULL;
565 if (!bin || !GST_IS_QGC_VIDEO_SINK_BIN(bin)) {
568 return GST_QGC_VIDEO_SINK_BIN(bin)->gpu_zerocopy;
struct _GstElement GstElement
void qgc_element_init(GstPlugin *plugin)
static GstElement * gst_qgc_video_sink_bin_make_conversion_element(GstQgcVideoSinkBin *self)
static void gst_qgc_video_sink_bin_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
static gboolean wireCpuPath(GstQgcVideoSinkBin *self, GstElement *videosink, GstElement *capsf)
static void gst_qgc_video_sink_bin_setup(GstQgcVideoSinkBin *self)
@ PROP_CONVERSION_ELEMENT
@ PROP_PROCESSING_DEADLINE
static GParamSpec * properties[PROP_LAST]
#define gst_qgc_video_sink_bin_parent_class
static void gst_qgc_video_sink_bin_class_init(GstQgcVideoSinkBinClass *klass)
static GstStaticPadTemplate sink_factory
G_DEFINE_FINAL_TYPE(GstQgcVideoSinkBin, gst_qgc_video_sink_bin, GST_TYPE_BIN)
GstElement * gst_qgc_video_sink_bin_get_qvideosink(GstQgcVideoSinkBin *self)
Returns the internal qgcqvideosink element, transfer-full (caller unrefs); NULL if not yet constructe...
static GstStateChangeReturn gst_qgc_video_sink_bin_change_state(GstElement *element, GstStateChange transition)
static void gst_qgc_video_sink_bin_constructed(GObject *object)
static gboolean wireGpuPath(GstQgcVideoSinkBin *self, GstElement *videosink, GstElement *capsf)
static void gst_qgc_video_sink_bin_dispose(GObject *object)
static void gst_qgc_video_sink_bin_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
GST_ELEMENT_REGISTER_DEFINE_WITH_CODE(qgcvideosinkbin, "qgcvideosinkbin", GST_RANK_NONE, GST_TYPE_QGC_VIDEO_SINK_BIN, qgc_element_init(plugin))
gboolean gst_qgc_video_sink_bin_get_gpu_zerocopy(GstElement *bin)
Whether the bin built its GPU zero-copy pipeline (mirrors "gpu-zerocopy"); NULL-safe (FALSE).
static void gst_qgc_video_sink_bin_init(GstQgcVideoSinkBin *self)
#define GST_TYPE_QGC_VIDEO_SINK_BIN
GstPadProbeReturn videosinkQueryProbe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
std::string buildGpuCapsString()
std::string buildCpuCapsString()