QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GstAppSinkAdapter.h
Go to the documentation of this file.
1#pragma once
2
3#include <QtCore/QObject>
4#include <QtCore/QElapsedTimer>
5#include <QtCore/QList>
6#include <QtCore/QMutex>
7#include <QtCore/QPointer>
8#include <QtCore/QTimer>
9#include <QtMultimedia/QVideoFrame>
10#include <QtMultimedia/QVideoFrameFormat>
11
12#include <atomic>
13#include <cstdint>
14
15#include <gst/gst.h>
16#include <gst/app/gstappsink.h>
17#include <gst/video/video-info.h>
18
19#if defined(QGC_HAS_GST_DMABUF_GPU_PATH) || defined(QGC_HAS_GST_AHARDWAREBUFFER_GPU_PATH)
20#include <EGL/egl.h>
21#endif
22
23class QVideoSink;
24
25// File-scope helpers (kept out of the anonymous namespace so unit tests can call them directly).
26QVideoFrameFormat::ColorSpace toQtColorSpace (GstVideoColorMatrix matrix);
27QVideoFrameFormat::ColorTransfer toQtColorTransfer(GstVideoTransferFunction transfer);
28QVideoFrameFormat::PixelFormat toQtPixelFormat (GstVideoFormat fmt);
29QVideoFrameFormat applyCropMeta (QVideoFrameFormat format, GstBuffer *buffer);
30// Umbrella header — pulls in GstVideoOrientationMethod (always present) plus the
31// per-buffer meta accessor (gated below by QGC_HAS_GST_VIDEO_ORIENTATION_META).
32// The standalone <gst/video/video-orientation.h> isn't shipped in every gst-video
33// install; the umbrella is the portable spelling.
34#include <gst/video/video.h>
35void applyOrientationToFrame(QVideoFrame &frame, GstVideoOrientationMethod method);
36
43class GstAppSinkAdapter : public QObject
44{
45 Q_OBJECT
46
47 Q_PROPERTY(quint64 gpuFrameCount READ gpuFrameCount NOTIFY frameCountsChanged)
48 Q_PROPERTY(quint64 cpuFrameCount READ cpuFrameCount NOTIFY frameCountsChanged)
49 Q_PROPERTY(quint64 gpuFallbackCount READ gpuFallbackCount NOTIFY frameCountsChanged)
51 Q_PROPERTY(quint64 appsinkInputFrames READ appsinkInputFrames NOTIFY frameCountsChanged)
55
56public:
57 explicit GstAppSinkAdapter(QObject *parent = nullptr);
58 ~GstAppSinkAdapter() override;
59
62 bool setup(GstElement *sinkBin, QVideoSink *videoSink);
63
65 void teardown();
66
68 void requestLatencyRefresh() noexcept { _latencyRefreshPending.store(true, std::memory_order_relaxed); }
69
75 void setActive(bool active);
76
80 void setRefreshRate(qreal hz);
81
85 void setSmoothingEnabled(bool enabled, qreal refreshHz);
86
87 quint64 gpuFrameCount() const noexcept;
88 quint64 cpuFrameCount() const noexcept;
89 quint64 gpuFallbackCount() const noexcept;
90 quint64 appsinkInputFrames() const noexcept;
91 quint64 appsinkDroppedFrames() const noexcept;
92
93signals:
95
96private:
97 static GstFlowReturn onNewSample(GstAppSink *appsink, gpointer userData);
101 static GstPadProbeReturn appsinkBufferProbe(GstPad *pad, GstPadProbeInfo *info, gpointer userData);
102
103 void _logFrameStats() const;
105 quint64 _deliveredFrames() const noexcept;
106
108 void _pushQosUpstream(GstAppSink *appsink, GstBuffer *buffer);
110 void _refreshLatency();
111
114 void _deliverFrame(QPointer<QVideoSink> sink, QVideoFrame &&frame, int64_t ptsNs);
115
117 void _onSmoothingTick();
118
119 QTimer _telemetryEmitTimer;
120
121 // Never held while pushing a frame to QVideoSink (avoids priority inversion with the render thread).
122 mutable QMutex _stateMutex;
123
124 QPointer<QVideoSink> _videoSink;
125 GstElement *_appsink = nullptr;
126 // Ref-held: the probe is installed on the appsink's sink pad; we keep the pad alive so
127 // teardown can target it for removal even if _appsink ownership changes.
128 GstPad *_appsinkProbePad = nullptr;
129 gulong _appsinkProbeId = 0;
130 std::atomic<quint64> _appsinkInputFrames{0};
131 // Set by GST_EVENT_FLUSH_START on the appsink sink pad, cleared by FLUSH_STOP.
132 // Drops new_sample callbacks while flushing so a buffer queued before a pipeline reset
133 // can't surface as a stale frame after FLUSH_STOP. Read on the streaming thread, written
134 // on the upstream serializer thread — both ordered by GStreamer's event semantics.
135 std::atomic<bool> _flushing{false};
136 // setActive(false) drops samples at onNewSample so a torn-down stream's last frame
137 // can't ghost when the user switches sources. Default true for backwards compatibility.
138 std::atomic<bool> _active{true};
139 // Stream-level orientation from upstream GST_TAG_IMAGE_ORIENTATION. Per-buffer
140 // GstVideoOrientationMeta still wins when present (gated by QGC_HAS_GST_VIDEO_ORIENTATION_META);
141 // this fallback works on any gst-video install since gst_video_orientation_from_tag is
142 // in the umbrella header.
143 std::atomic<int> _streamOrientation{static_cast<int>(GST_VIDEO_ORIENTATION_IDENTITY)};
144
145 // 0 = use bin's 33 ms default. Streaming thread reads on caps change.
146 std::atomic<quint64> _refreshPeriodNs{0};
147
148 // Smoothing ring: producer (streaming) writes under _smoothingMutex, timer (owner thread) reads.
149 // _smoothingClock is started before _smoothingEnabled flips so producer always sees started clock.
150 static constexpr int kSmoothingRingCapacity = 3;
151 static constexpr int64_t kSmoothingThresholdNs = 70 * 1000000;
152 struct SmoothingEntry {
153 QVideoFrame frame;
154 int64_t ptsNs; // stream PTS (or fallback wall-time when PTS missing)
155 qint64 enqueuedNs; // QElapsedTimer::nsecsElapsed at enqueue
156 };
157 std::atomic<bool> _smoothingEnabled{false};
158 mutable QMutex _smoothingMutex;
159 QList<SmoothingEntry> _smoothingRing;
160 QTimer _smoothingTickTimer;
161 QElapsedTimer _smoothingClock;
162 int64_t _smoothingFirstPtsNs = -1;
163 qint64 _smoothingFirstClockNs = 0;
164 std::atomic<quint64> _smoothingDroppedFrames{0};
165 // -1 = no prior; reset by DISCONT and FLUSH_STOP. Without this a seek wedges QVideoOutput.
166 std::atomic<int64_t> _lastDeliveredPtsNs{-1};
167
168 // Held ref prevents GstCaps address reuse that would produce a false identity-cache hit with stale colorimetry.
169 GstCaps *_cachedCapsKey = nullptr;
170 GstVideoInfo _cachedInfo{};
171 QVideoFrameFormat _cachedFormat;
172 int _cachedPixelFormat = 0; // QVideoFrameFormat::Format_Invalid sentinel
173 QString _cachedAllocatorName; // negotiated allocator from first sample of each caps change
174
175 // Steady-clock ns checkpoint + matching total-frames snapshot for fps-over-window in _logFrameStats.
176 // Both mutated from the streaming thread; readers (currently none) get a relaxed view.
177 mutable std::atomic<qint64> _lastStatsAtNs{0};
178 mutable std::atomic<quint64> _lastStatsTotal{0};
179
180 // Tracks the last total-frames count observed by the emit timer so we can suppress
181 // no-change frameCountsChanged() emissions (idle adapters waste ~60 emits/min otherwise).
182 quint64 _lastEmittedFrameTotal = 0;
183
184 // Format warning throttle: only warn on first occurrence of each unsupported format.
185 std::atomic<GstVideoFormat> _lastWarnedFormat{GST_VIDEO_FORMAT_UNKNOWN};
186
187 // Counters are written from the GStreamer streaming thread and read from the GUI thread
188 // (Q_PROPERTY getters + telemetry timer). Atomics keep the read/write race TSan-clean.
189 std::atomic<quint64> _cpuFrames{0};
190
191#if defined(QGC_HAS_ANY_GPU_PATH)
192 bool _gpuPathEnabled = false;
193#endif
194#if defined(QGC_HAS_GST_DMABUF_GPU_PATH)
195 // Set at setup() to a Wayland/X11 EGL display when zero-copy is enabled and
196 // resolvable. EGL_NO_DISPLAY disables the GPU path for this adapter instance.
197 EGLDisplay _eglDisplay = EGL_NO_DISPLAY;
198 std::atomic<quint64> _gpuFrames{0}; // DMABuf zero-copy frames delivered
199#endif
200#if defined(QGC_HAS_GST_GLMEMORY_GPU_PATH)
201 std::atomic<quint64> _glFrames{0}; // GLMemory zero-copy frames delivered
202#endif
203#if defined(QGC_HAS_GST_D3D11_GPU_PATH)
204 std::atomic<quint64> _d3d11Frames{0}; // D3D11Memory zero-copy frames delivered
205#endif
206#if defined(QGC_HAS_GST_D3D12_GPU_PATH)
207 std::atomic<quint64> _d3d12Frames{0}; // D3D12Memory zero-copy frames delivered
208#endif
209#if defined(QGC_HAS_GST_IOSURFACE_GPU_PATH)
210 std::atomic<quint64> _iosurfaceFrames{0}; // IOSurface/CVPixelBuffer zero-copy frames delivered
211#endif
212#if defined(QGC_HAS_GST_AHARDWAREBUFFER_GPU_PATH)
213 EGLDisplay _ahwbEglDisplay = EGL_NO_DISPLAY;
214 std::atomic<quint64> _ahwbFrames{0}; // AHardwareBuffer zero-copy frames delivered
215#endif
216
217 // QoS upstream feedback — EWMA mirrors gstbasesink.c DO_RUNNING_AVG window constants.
218 bool _qosUpstreamEnabled = true;
219 // Number of samples delivered so far; skip first kQosWarmup before sending events.
220 quint64 _qosSampleCount = 0;
221 // EWMA of actual interval / expected interval; >1.0 means we're consuming slower than source.
222 double _qosAvgRate = 1.0;
223 GstClockTime _qosLastPts = GST_CLOCK_TIME_NONE;
224 GstClockTime _qosLastArrivalNs = 0;
225 static constexpr quint64 kQosWarmup = 10;
226 static constexpr int kQosInterval = 8; // emit every N-th frame to avoid event spam
227 // Latency fields — all written and read exclusively on the streaming thread;
228 // teardown() resets them only after the pipeline is NULL (no concurrent streaming).
229 GstClockTime _pipelineMinLatencyNs = 0;
230 bool _latencyValid = false; // false until first successful GST_QUERY_LATENCY
231 std::atomic<bool> _latencyRefreshPending{false}; // set by requestLatencyRefresh() from any thread
232 static constexpr quint64 kLatencyRefreshInterval = 256; // re-query every N frames
233};
QVideoFrameFormat::PixelFormat toQtPixelFormat(GstVideoFormat fmt)
QVideoFrameFormat::ColorTransfer toQtColorTransfer(GstVideoTransferFunction transfer)
QVideoFrameFormat applyCropMeta(QVideoFrameFormat format, GstBuffer *buffer)
QVideoFrameFormat::ColorSpace toQtColorSpace(GstVideoColorMatrix matrix)
void applyOrientationToFrame(QVideoFrame &frame, GstVideoOrientationMethod method)
struct _GstElement GstElement
Bridges a GStreamer appsink to a Qt QVideoSink.
void teardown()
Disconnect the callback (safe to call multiple times).
quint64 gpuFrameCount() const noexcept
void frameCountsChanged()
void setActive(bool active)
void setSmoothingEnabled(bool enabled, qreal refreshHz)
quint64 cpuFrameCount() const noexcept
void setRefreshRate(qreal hz)
void requestLatencyRefresh() noexcept
Signal the adapter to refresh pipeline latency on the next streaming-thread tick.
bool setup(GstElement *sinkBin, QVideoSink *videoSink)
quint64 gpuFallbackCount() const noexcept
quint64 appsinkInputFrames() const noexcept
quint64 appsinkDroppedFrames() const noexcept