QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
gstqgcqvideosink.cc
Go to the documentation of this file.
1#include "gstqgcqvideosink.h"
2
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>
10
11#include "GStreamerFrameMap.h"
12#include "GstQgcAllocation.h"
14#include "QGCLoggingCategory.h"
15#include "gstqgcelements.h"
16#if GST_CHECK_VERSION(1, 24, 0)
17#include <gst/video/video-info-dma.h>
18#endif
19
20#include <atomic>
21
22QGC_LOGGING_CATEGORY(GstQgcQVideoSinkLog, "Video.GStreamer.QgcQVideoSink")
23
24#define GST_CAT_DEFAULT gst_qgc_debug
25
26namespace {
27
30struct PrivState
31{
32 QVideoFrameFormat format;
33 // Written under GST_OBJECT_LOCK from the GUI thread, snapshotted by show_frame.
34 // Default (gpuEnabled=false) keeps the CPU memcpy path until the controller wires it.
35 HwVideoBufferContext hw_context = {};
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}; // sustained run escalates show_frame to error
41 // Per-element render counter (read via `frames-delivered`) so multi-receiver setups
42 // don't see a shared process-global total.
43 std::atomic<quint64> delivered_frames{0};
44 // Negotiated caps held from set_caps; avoids per-frame allocation and preserves DRM modifiers.
45 GstCaps* cached_caps{nullptr};
46#if defined(QGC_HAS_ANY_GPU_PATH)
47 // Per-caps-epoch resolved-path cache; reset on set_caps so a format change re-runs path selection.
48 HwResolvedPathCache resolved_path_cache = {};
49#endif
50};
51
52inline PrivState* priv_of(GstQgcQVideoSink* self)
53{
54 return static_cast<PrivState*>(self->priv);
55}
56
59QPointer<QVideoSink> snapshot_sink(GstQgcQVideoSink* self, HwVideoBufferContext* hwOut = nullptr)
60{
61 QPointer<QVideoSink> out;
62 GST_OBJECT_LOCK(self);
63 QVideoSink* raw = static_cast<QVideoSink*>(self->qvideosink);
64 out = raw;
65 if (hwOut)
66 *hwOut = priv_of(self)->hw_context;
67 GST_OBJECT_UNLOCK(self);
68 return out;
69}
70
75void push_frame_queued(GstQgcQVideoSink* self, QVideoFrame&& frame)
76{
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));
80 }
81 GST_OBJECT_UNLOCK(self);
82}
83
84} // namespace
85
88static GstStaticPadTemplate sink_template =
89 GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY);
90
91enum
92{
100};
101
102G_DEFINE_FINAL_TYPE(GstQgcQVideoSink, gst_qgc_q_video_sink, GST_TYPE_VIDEO_SINK)
103GST_ELEMENT_REGISTER_DEFINE(qgcqvideosink, "qgcqvideosink", GST_RANK_NONE, GST_TYPE_QGC_Q_VIDEO_SINK)
104
105static void gst_qgc_q_video_sink_init(GstQgcQVideoSink* self)
106{
107 self->qvideosink = nullptr;
108 self->active = TRUE;
109 self->gpu_zerocopy = FALSE;
110 self->caps_valid = FALSE;
111 gst_video_info_init(&self->video_info);
112 self->priv = new PrivState();
113 // sync=FALSE: drone video is "as fast as decoded"; GstBaseSink clock-sync would
114 // stall on live RTSP. async=FALSE skips preroll wait so caps changes don't stall.
115 gst_base_sink_set_sync(GST_BASE_SINK(self), FALSE);
116 gst_base_sink_set_async_enabled(GST_BASE_SINK(self), FALSE);
117 // Don't retain the last buffer: it would pin an upstream pool slot for the stream's lifetime.
118 gst_base_sink_set_last_sample_enabled(GST_BASE_SINK(self), FALSE);
119}
120
121void gst_qgc_q_video_sink_set_hw_context(GstQgcQVideoSink* self, const HwVideoBufferContext& ctx)
122{
123 if (!self)
124 return;
125 GST_OBJECT_LOCK(self);
126 priv_of(self)->hw_context = ctx;
127 GST_OBJECT_UNLOCK(self);
128}
129
130static void gst_qgc_q_video_sink_finalize(GObject* obj)
131{
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);
137}
138
139static void gst_qgc_q_video_sink_set_property(GObject* obj, guint id, const GValue* val, GParamSpec* pspec)
140{
141 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(obj);
142 GST_OBJECT_LOCK(self);
143 switch (id) {
144 case PROP_QVIDEOSINK: {
145 gpointer raw = g_value_get_pointer(val);
146 self->qvideosink = raw;
147 // Reset PTS guard on sink swap so a new sink doesn't see a stale last_pts_ns
148 // from the previous sink and erroneously drop the first frames.
149 priv_of(self)->last_pts_ns.store(-1, std::memory_order_relaxed);
150 break;
151 }
152 case PROP_ACTIVE:
153 // Read lock-free on the streaming thread (show_frame); publish atomically.
154 g_atomic_int_set(&self->active, g_value_get_boolean(val));
155 break;
157 self->gpu_zerocopy = g_value_get_boolean(val);
158 break;
159 default:
160 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, id, pspec);
161 break;
162 }
163 GST_OBJECT_UNLOCK(self);
164}
165
166static void gst_qgc_q_video_sink_get_property(GObject* obj, guint id, GValue* val, GParamSpec* pspec)
167{
168 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(obj);
169 GST_OBJECT_LOCK(self);
170 switch (id) {
171 case PROP_QVIDEOSINK:
172 g_value_set_pointer(val, self->qvideosink);
173 break;
174 case PROP_ACTIVE:
175 g_value_set_boolean(val, self->active);
176 break;
178 g_value_set_boolean(val, self->gpu_zerocopy);
179 break;
181 g_value_set_uint64(val, priv_of(self)->input_frames.load(std::memory_order_relaxed));
182 break;
184 g_value_set_uint64(val, priv_of(self)->dropped_frames.load(std::memory_order_relaxed));
185 break;
187 g_value_set_uint64(val, priv_of(self)->delivered_frames.load(std::memory_order_relaxed));
188 break;
189 default:
190 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, id, pspec);
191 break;
192 }
193 GST_OBJECT_UNLOCK(self);
194}
195
196static gboolean gst_qgc_q_video_sink_set_caps(GstBaseSink* bsink, GstCaps* caps)
197{
198 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(bsink);
199 PrivState* p = priv_of(self);
200
201 GstVideoInfo parsedInfo = {};
202 if (!GstHw::dmaDrmAwareVideoInfo(caps, &parsedInfo)) {
203 qCWarning(GstQgcQVideoSinkLog) << "set_caps: failed to parse video info from caps";
204 return FALSE;
205 }
206
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));
211 return FALSE;
212 }
213
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;
218 return FALSE;
219 }
220
221 QVideoFrameFormat fmt(QSize(w, h), pixelFormat);
222 applyColorimetry(fmt, parsedInfo, caps);
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));
227 }
228
229 self->video_info = parsedInfo;
230 p->format = std::move(fmt);
231 gst_clear_caps(&p->cached_caps);
232 // Hold the negotiated caps verbatim: rebuilding from video_info drops DRM modifiers and other
233 // negotiated fields the downstream frame mapping relies on.
234 p->cached_caps = gst_caps_ref(caps);
235#if defined(QGC_HAS_ANY_GPU_PATH)
236 p->resolved_path_cache = HwResolvedPathCache{};
237#endif
238 // New caps = new segment; clear PTS history so a restart/format change that resumes at a
239 // lower PTS isn't wedged by the monotonic-PTS guard in show_frame.
240 p->last_pts_ns.store(-1, std::memory_order_relaxed);
241 // caps_valid is read lock-free on the streaming thread (show_frame); publish atomically.
242 g_atomic_int_set(&self->caps_valid, TRUE);
243
244 // Posted on the bus so the controller can mirror negotiation state into Q_PROPERTY.
245 {
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));
250 g_free(fmtName);
251 }
252
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);
256 return TRUE;
257}
258
259// Sustained run of map failures (not a transient hiccup) means the import is broken — tear down + restart.
260constexpr quint64 kMaxConsecutiveMapFailures = 120;
261
262static const char* describeMappedPath([[maybe_unused]] const MappedFrame& m) noexcept
263{
264#if defined(QGC_HAS_ANY_GPU_PATH)
265 if (m.source == MappedFrame::Source::Gpu) {
266 switch (m.gpuPath) {
268 return "GPU/DmaBuf";
270 return "GPU/GlMemory";
272 return "GPU/D3D11";
274 return "GPU/D3D12";
276 return "GPU/IOSurface";
278 return "GPU/AHardwareBuffer";
280 return "GPU/Vulkan";
282 break;
283 }
284 return "GPU/Unknown";
285 }
286#endif
287 return "CPU";
288}
289
290static GstFlowReturn gst_qgc_q_video_sink_show_frame(GstVideoSink* vsink, GstBuffer* buf)
291{
292 GstQgcQVideoSink* self = GST_QGC_Q_VIDEO_SINK(vsink);
293 PrivState* p = priv_of(self);
294
295 p->input_frames.fetch_add(1, std::memory_order_relaxed);
296
297 if (!g_atomic_int_get(&self->caps_valid)) {
298 // Should never happen — GstBaseSink calls set_caps before show_frame.
299 return GST_FLOW_NOT_NEGOTIATED;
300 }
301 if (!g_atomic_int_get(&self->active)) {
302 p->dropped_frames.fetch_add(1, std::memory_order_relaxed);
303 return GST_FLOW_OK; // drop silently — controller drives the active flag
304 }
305
307 QPointer<QVideoSink> sink = snapshot_sink(self, &hwCtx);
308 if (!sink) {
309 p->dropped_frames.fetch_add(1, std::memory_order_relaxed);
310 return GST_FLOW_OK; // no destination yet; drop
311 }
312
313 // PTS regression guard ahead of mapping: a regressed timestamp wedges QVideoOutput's internal
314 // advance, and checking first avoids a wasted full-frame map on a buffer we'd drop anyway.
315 // last_pts_ns is advanced only once the frame is actually delivered (below) so a transient map
316 // failure doesn't push it past a buffer we never rendered.
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;
319 if (hasPts) {
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);
323 return GST_FLOW_OK;
324 }
325 }
326
327 // Build a cropped format copy only when crop meta is present (rare); otherwise pass
328 // p->format by reference to avoid a per-frame QVideoFrameFormat refcount bump.
329 QVideoFrameFormat croppedFmt;
330 const bool hasCrop = (gst_buffer_get_video_crop_meta(buf) != nullptr);
331 if (hasCrop) {
332 croppedFmt = applyCropMeta(p->format, buf);
333 }
334 MappedFrame mapped =
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);
338#else
339 nullptr);
340#endif
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;
346 }
348 qCWarning(GstQgcQVideoSinkLog) << "show_frame:" << c << "consecutive map failures — erroring out";
349 return GST_FLOW_ERROR;
350 }
351 return GST_FLOW_OK; // drop transient failure, keep the stream alive
352 }
353 p->consecutive_map_failures.store(0, std::memory_order_relaxed);
354
355 // Stream orientation is always IDENTITY here; tag-driven orientation lives in the
356 // controller (GST_TAG_IMAGE_ORIENTATION). Per-buffer GstVideoOrientationMeta still wins.
357 applyOrientationAndTiming(mapped.frame, buf, static_cast<int>(GST_VIDEO_ORIENTATION_IDENTITY));
358
359 // Telemetry — process-global `GstHwPathTelemetry` accumulator. Per-element render
360 // counts live in `delivered_frames` (read by the controller via `frames-delivered`).
361 if (mapped.source == MappedFrame::Source::Cpu) {
362 p->cpu_frames.fetch_add(1, std::memory_order_relaxed);
364#if defined(QGC_HAS_ANY_GPU_PATH)
365 // Stream started HW-capable but this frame fell back to CPU — record the demotion once per epoch.
366 if (mapped.demoted && !p->resolved_path_cache.demotionRecorded) {
367 p->resolved_path_cache.demotionRecorded = true;
369 }
370#endif
371 }
372#if defined(QGC_HAS_ANY_GPU_PATH)
373 else {
375 }
376#endif
377
378 const quint64 delivered = p->delivered_frames.fetch_add(1, std::memory_order_relaxed) + 1;
379 if (delivered == 1) {
380 qCInfo(GstQgcQVideoSinkLog).noquote()
381 << "first frame delivered via" << describeMappedPath(mapped) << "path"
382 << QStringLiteral("%1x%2").arg(mapped.frame.width()).arg(mapped.frame.height());
383 } else if ((delivered % 300) == 0) {
384 qCDebug(GstQgcQVideoSinkLog).noquote()
385 << "frame flow:" << describeMappedPath(mapped) << "delivered=" << delivered
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);
389 }
390 if (hasPts) {
391 p->last_pts_ns.store(pts, std::memory_order_release);
392 }
393 push_frame_queued(self, std::move(mapped.frame));
394 return GST_FLOW_OK;
395}
396
397// Add qgcqvideosink's consumed metas + min-buffer pool hint, then chain so GstBaseSink's default
398// allocation bookkeeping still runs. Replaces the former QUERY_DOWNSTREAM pad probe for ALLOCATION.
399static gboolean gst_qgc_q_video_sink_propose_allocation(GstBaseSink* bsink, GstQuery* query)
400{
402 // GstBaseSink/GstVideoSink install no default propose_allocation vmethod, so the parent pointer
403 // is NULL for a direct subclass; chaining unconditionally would call 0x0 on the first ALLOCATION query.
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);
407 }
408 return TRUE;
409}
410
411static void gst_qgc_q_video_sink_class_init(GstQgcQVideoSinkClass* klass)
412{
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);
417
418 gobject_class->set_property = gst_qgc_q_video_sink_set_property;
419 gobject_class->get_property = gst_qgc_q_video_sink_get_property;
420 gobject_class->finalize = gst_qgc_q_video_sink_finalize;
421
422 g_object_class_install_property(
423 gobject_class, PROP_QVIDEOSINK,
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)));
427
428 g_object_class_install_property(
429 gobject_class, PROP_ACTIVE,
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)));
433
434 g_object_class_install_property(
435 gobject_class, PROP_GPU_ZEROCOPY,
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)));
439
440 g_object_class_install_property(
441 gobject_class, PROP_FRAMES_INPUT,
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)));
444
445 g_object_class_install_property(
446 gobject_class, PROP_FRAMES_DROPPED,
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)));
452
453 g_object_class_install_property(
454 gobject_class, PROP_FRAMES_DELIVERED,
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)));
459
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);
464
465 basesink_class->set_caps = gst_qgc_q_video_sink_set_caps;
466 basesink_class->propose_allocation = gst_qgc_q_video_sink_propose_allocation;
467 videosink_class->show_frame = gst_qgc_q_video_sink_show_frame;
468}
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)
@ PROP_FRAMES_INPUT
@ PROP_FRAMES_DELIVERED
@ PROP_QVIDEOSINK
@ PROP_GPU_ZEROCOPY
@ PROP_FRAMES_DROPPED
@ PROP_ACTIVE
@ PROP_0
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...
QVideoFrame frame
enum MappedFrame::Source source