3#include <QtCore/QMetaObject>
4#include <QtCore/QPointer>
5#include <QtMultimedia/QVideoFrame>
6#include <QtMultimedia/QVideoFrameFormat>
7#include <QtMultimedia/QVideoSink>
8#include <gst/video/video-info.h>
9#include <gst/video/video.h>
16#if GST_CHECK_VERSION(1, 24, 0)
17#include <gst/video/video-info-dma.h>
24#define GST_CAT_DEFAULT gst_qgc_debug
32 QVideoFrameFormat format;
36 std::atomic<quint64> cpu_frames{0};
37 std::atomic<int64_t> last_pts_ns{-1};
38 std::atomic<quint64> input_frames{0};
39 std::atomic<quint64> dropped_frames{0};
40 std::atomic<quint64> consecutive_map_failures{0};
43 std::atomic<quint64> delivered_frames{0};
45 GstCaps* cached_caps{
nullptr};
46#if defined(QGC_HAS_ANY_GPU_PATH)
52inline PrivState* priv_of(GstQgcQVideoSink* self)
54 return static_cast<PrivState*
>(self->priv);
61 QPointer<QVideoSink> out;
62 GST_OBJECT_LOCK(self);
63 QVideoSink* raw =
static_cast<QVideoSink*
>(self->qvideosink);
66 *hwOut = priv_of(self)->hw_context;
67 GST_OBJECT_UNLOCK(self);
75void push_frame_queued(GstQgcQVideoSink* self, QVideoFrame&& frame)
77 GST_OBJECT_LOCK(self);
78 if (QVideoSink* sink =
static_cast<QVideoSink*
>(self->qvideosink)) {
79 QMetaObject::invokeMethod(sink, &QVideoSink::setVideoFrame, Qt::QueuedConnection, std::move(frame));
81 GST_OBJECT_UNLOCK(self);
89 GST_STATIC_PAD_TEMPLATE(
"sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY);
107 self->qvideosink =
nullptr;
109 self->gpu_zerocopy = FALSE;
110 self->caps_valid = FALSE;
111 gst_video_info_init(&self->video_info);
112 self->priv =
new PrivState();
115 gst_base_sink_set_sync(GST_BASE_SINK(self), FALSE);
116 gst_base_sink_set_async_enabled(GST_BASE_SINK(self), FALSE);
118 gst_base_sink_set_last_sample_enabled(GST_BASE_SINK(self), FALSE);
125 GST_OBJECT_LOCK(self);
126 priv_of(self)->hw_context = ctx;
127 GST_OBJECT_UNLOCK(self);
132 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(obj);
133 gst_clear_caps(&priv_of(self)->cached_caps);
134 delete priv_of(self);
135 self->priv =
nullptr;
136 G_OBJECT_CLASS(gst_qgc_q_video_sink_parent_class)->finalize(obj);
141 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(obj);
142 GST_OBJECT_LOCK(self);
145 gpointer raw = g_value_get_pointer(val);
146 self->qvideosink = raw;
149 priv_of(self)->last_pts_ns.store(-1, std::memory_order_relaxed);
154 g_atomic_int_set(&self->active, g_value_get_boolean(val));
157 self->gpu_zerocopy = g_value_get_boolean(val);
160 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj,
id, pspec);
163 GST_OBJECT_UNLOCK(self);
168 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(obj);
169 GST_OBJECT_LOCK(self);
172 g_value_set_pointer(val, self->qvideosink);
175 g_value_set_boolean(val, self->active);
178 g_value_set_boolean(val, self->gpu_zerocopy);
181 g_value_set_uint64(val, priv_of(self)->input_frames.load(std::memory_order_relaxed));
184 g_value_set_uint64(val, priv_of(self)->dropped_frames.load(std::memory_order_relaxed));
187 g_value_set_uint64(val, priv_of(self)->delivered_frames.load(std::memory_order_relaxed));
190 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj,
id, pspec);
193 GST_OBJECT_UNLOCK(self);
198 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(bsink);
199 PrivState* p = priv_of(self);
201 GstVideoInfo parsedInfo = {};
203 qCWarning(GstQgcQVideoSinkLog) <<
"set_caps: failed to parse video info from caps";
207 const QVideoFrameFormat::PixelFormat pixelFormat =
toQtPixelFormat(GST_VIDEO_INFO_FORMAT(&parsedInfo));
208 if (pixelFormat == QVideoFrameFormat::Format_Invalid) {
209 qCWarning(GstQgcQVideoSinkLog) <<
"set_caps: unsupported video format"
210 << gst_video_format_to_string(GST_VIDEO_INFO_FORMAT(&parsedInfo));
214 const int w = GST_VIDEO_INFO_WIDTH(&parsedInfo);
215 const int h = GST_VIDEO_INFO_HEIGHT(&parsedInfo);
216 if (w <= 0 || h <= 0) {
217 qCWarning(GstQgcQVideoSinkLog) <<
"set_caps: invalid dimensions" << w <<
"x" << h;
221 QVideoFrameFormat fmt(QSize(w, h), pixelFormat);
223 const int fpsN = GST_VIDEO_INFO_FPS_N(&parsedInfo);
224 const int fpsD = GST_VIDEO_INFO_FPS_D(&parsedInfo);
225 if (fpsN > 0 && fpsD > 0) {
226 fmt.setStreamFrameRate(
static_cast<qreal
>(fpsN) /
static_cast<qreal
>(fpsD));
229 self->video_info = parsedInfo;
230 p->format = std::move(fmt);
231 gst_clear_caps(&p->cached_caps);
234 p->cached_caps = gst_caps_ref(caps);
235#if defined(QGC_HAS_ANY_GPU_PATH)
240 p->last_pts_ns.store(-1, std::memory_order_relaxed);
242 g_atomic_int_set(&self->caps_valid, TRUE);
246 gchar* fmtName = g_strdup(gst_video_format_to_string(GST_VIDEO_INFO_FORMAT(&parsedInfo)));
247 GstStructure* s = gst_structure_new(
"qgc-caps-info",
"width", G_TYPE_INT, w,
"height", G_TYPE_INT, h,
"format",
248 G_TYPE_STRING, fmtName,
nullptr);
249 gst_element_post_message(GST_ELEMENT(self), gst_message_new_element(GST_OBJECT(self), s));
253 qCInfo(GstQgcQVideoSinkLog).noquote()
254 <<
"set_caps: format=" << gst_video_format_to_string(GST_VIDEO_INFO_FORMAT(&parsedInfo)) << w <<
"x" << h
255 <<
"pixfmt=" << int(pixelFormat);
264#if defined(QGC_HAS_ANY_GPU_PATH)
270 return "GPU/GlMemory";
276 return "GPU/IOSurface";
278 return "GPU/AHardwareBuffer";
284 return "GPU/Unknown";
292 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(vsink);
293 PrivState* p = priv_of(self);
295 p->input_frames.fetch_add(1, std::memory_order_relaxed);
297 if (!g_atomic_int_get(&self->caps_valid)) {
299 return GST_FLOW_NOT_NEGOTIATED;
301 if (!g_atomic_int_get(&self->active)) {
302 p->dropped_frames.fetch_add(1, std::memory_order_relaxed);
307 QPointer<QVideoSink> sink = snapshot_sink(self, &hwCtx);
309 p->dropped_frames.fetch_add(1, std::memory_order_relaxed);
317 const bool hasPts = GST_BUFFER_PTS_IS_VALID(buf);
318 const int64_t pts = hasPts ?
static_cast<int64_t
>(GST_BUFFER_PTS(buf)) : -1;
320 const int64_t lastPts = p->last_pts_ns.load(std::memory_order_acquire);
321 if (lastPts >= 0 && pts < lastPts) {
322 p->dropped_frames.fetch_add(1, std::memory_order_relaxed);
329 QVideoFrameFormat croppedFmt;
330 const bool hasCrop = (gst_buffer_get_video_crop_meta(buf) !=
nullptr);
335 mapSampleToFrame(buf, p->cached_caps, self->video_info, hasCrop ? croppedFmt : p->format, hwCtx,
336#if defined(QGC_HAS_ANY_GPU_PATH)
337 &p->resolved_path_cache);
341 if (!mapped.
frame.isValid()) {
342 p->dropped_frames.fetch_add(1, std::memory_order_relaxed);
343 const quint64 c = p->consecutive_map_failures.fetch_add(1, std::memory_order_relaxed) + 1;
344 if ((c & 0x3F) == 1) {
345 qCWarning(GstQgcQVideoSinkLog) <<
"show_frame: mapping failed, consecutive=" << c;
348 qCWarning(GstQgcQVideoSinkLog) <<
"show_frame:" << c <<
"consecutive map failures — erroring out";
349 return GST_FLOW_ERROR;
353 p->consecutive_map_failures.store(0, std::memory_order_relaxed);
362 p->cpu_frames.fetch_add(1, std::memory_order_relaxed);
364#if defined(QGC_HAS_ANY_GPU_PATH)
366 if (mapped.demoted && !p->resolved_path_cache.demotionRecorded) {
367 p->resolved_path_cache.demotionRecorded =
true;
372#if defined(QGC_HAS_ANY_GPU_PATH)
378 const quint64
delivered = p->delivered_frames.fetch_add(1, std::memory_order_relaxed) + 1;
380 qCInfo(GstQgcQVideoSinkLog).noquote()
382 << QStringLiteral(
"%1x%2").arg(mapped.
frame.width()).arg(mapped.
frame.height());
384 qCDebug(GstQgcQVideoSinkLog).noquote()
386 <<
"input=" << p->input_frames.load(std::memory_order_relaxed)
387 <<
"dropped=" << p->dropped_frames.load(std::memory_order_relaxed)
388 <<
"cpuFrames=" << p->cpu_frames.load(std::memory_order_relaxed);
391 p->last_pts_ns.store(pts, std::memory_order_release);
393 push_frame_queued(self, std::move(mapped.
frame));
404 GstBaseSinkClass* parentClass = GST_BASE_SINK_CLASS(gst_qgc_q_video_sink_parent_class);
405 if (parentClass->propose_allocation) {
406 return parentClass->propose_allocation(bsink, query);
413 GObjectClass* gobject_class = G_OBJECT_CLASS(klass);
414 GstElementClass* element_class = GST_ELEMENT_CLASS(klass);
415 GstBaseSinkClass* basesink_class = GST_BASE_SINK_CLASS(klass);
416 GstVideoSinkClass* videosink_class = GST_VIDEO_SINK_CLASS(klass);
422 g_object_class_install_property(
424 g_param_spec_pointer(
"qvideosink",
"QVideoSink target",
425 "QVideoSink* to push frames into. Caller-owned; element never unrefs.",
426 (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)));
428 g_object_class_install_property(
430 g_param_spec_boolean(
"active",
"Active",
431 "When FALSE, show_frame drops buffers instead of pushing to the QVideoSink.", TRUE,
432 (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
434 g_object_class_install_property(
436 g_param_spec_boolean(
"gpu-zerocopy",
"GPU zero-copy",
437 "Attempt GPU-zerocopy mapping in show_frame; false forces CPU memcpy.", FALSE,
438 (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)));
440 g_object_class_install_property(
442 g_param_spec_uint64(
"frames-input",
"Frames input",
"Total buffers seen by show_frame, including drops.", 0,
443 G_MAXUINT64, 0, (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));
445 g_object_class_install_property(
447 g_param_spec_uint64(
"frames-dropped",
"Frames dropped",
448 "Buffers rejected by show_frame (inactive sink, missing QVideoSink, "
449 "PTS regression, or map failure). Detailed map failures are tracked separately "
450 "via GstHwPathTelemetry::peekMapFailureCount.",
451 0, G_MAXUINT64, 0, (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));
453 g_object_class_install_property(
455 g_param_spec_uint64(
"frames-delivered",
"Frames delivered",
456 "Buffers that survived every drop check and were queued to the QVideoSink. "
457 "Per-element — the GUI controller reads this for the QML frameCount.",
458 0, G_MAXUINT64, 0, (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));
460 gst_element_class_set_static_metadata(element_class,
"QGC QVideoSink",
"Sink/Video",
461 "Pushes decoded GstVideoFrames into a Qt QVideoSink",
462 "QGroundControl <https://qgroundcontrol.com/>");
463 gst_element_class_add_static_pad_template(element_class, &
sink_template);
QVideoFrameFormat::PixelFormat toQtPixelFormat(GstVideoFormat fmt)
QVideoFrameFormat applyCropMeta(QVideoFrameFormat format, GstBuffer *buffer)
Apply video crop meta to format's viewport. Pass-through when no crop meta present.
void applyColorimetry(QVideoFrameFormat &format, const GstVideoInfo &info, GstCaps *caps)
MappedFrame mapSampleToFrame(GstBuffer *buffer, GstCaps *caps, const GstVideoInfo &info, const QVideoFrameFormat &format, const HwVideoBufferContext &hwContext, HwResolvedPathCache *pathCache) noexcept
void applyOrientationAndTiming(QVideoFrame &frame, GstBuffer *buffer, int streamOrientation)
std::atomic< quint64 > delivered
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static gboolean gst_qgc_q_video_sink_set_caps(GstBaseSink *bsink, GstCaps *caps)
static void gst_qgc_q_video_sink_get_property(GObject *obj, guint id, GValue *val, GParamSpec *pspec)
static void gst_qgc_q_video_sink_set_property(GObject *obj, guint id, const GValue *val, GParamSpec *pspec)
static GstStaticPadTemplate sink_template
static void gst_qgc_q_video_sink_finalize(GObject *obj)
static gboolean gst_qgc_q_video_sink_propose_allocation(GstBaseSink *bsink, GstQuery *query)
static GstFlowReturn gst_qgc_q_video_sink_show_frame(GstVideoSink *vsink, GstBuffer *buf)
static void gst_qgc_q_video_sink_init(GstQgcQVideoSink *self)
static void gst_qgc_q_video_sink_class_init(GstQgcQVideoSinkClass *klass)
constexpr quint64 kMaxConsecutiveMapFailures
void gst_qgc_q_video_sink_set_hw_context(GstQgcQVideoSink *self, const HwVideoBufferContext &ctx)
static const char * describeMappedPath(const MappedFrame &m) noexcept
#define GST_TYPE_QGC_Q_VIDEO_SINK
G_DEFINE_FINAL_TYPE(GstQgcVideoSinkBin, gst_qgc_video_sink_bin, GST_TYPE_BIN)
void recordStreamDemotion(HwVideoBufferPath negotiated) noexcept
One-shot-per-epoch event: a stream that negotiated a HW path demoted to CPU. Distinct from per-frame ...
void recordDelivered(HwVideoBufferPath path) noexcept
Frames successfully delivered via this path.
bool dmaDrmAwareVideoInfo(GstCaps *caps, GstVideoInfo *info)
void populateAllocationQuery(GstQuery *query)
Platform context for the factory; encapsulates EGL handles so callers don't need path-specific ifdefs...
enum MappedFrame::Source source