Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
framegrabber.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
5
7
8#include <QtTest/qtest.h>
9#include <QtTest/private/qtestblacklist_p.h>
10#include <QtMultimedia/qvideoframe.h>
11#include <QtCore/q20vector.h>
12#include <QtCore/qcoreapplication.h>
13#include <QtCore/qelapsedtimer.h>
14#include <QtCore/qscopeguard.h>
15
16#include <utility>
17
18QT_BEGIN_NAMESPACE
19
21
22std::chrono::milliseconds surfaceCaptureTestTimeout()
23{
24 // A blacklisted test is expected to fail, so don't make it wait for the full
25 // timeout to do so.
26 if (const char *testFunction = QTest::currentTestFunction();
27 testFunction && QTestPrivate::checkBlackLists(testFunction, QTest::currentDataTag())) {
28 return std::chrono::seconds(1);
29 }
30
31 if (isCI())
32 return std::chrono::seconds(60);
33 return std::chrono::seconds(5);
34}
35
36[[nodiscard]] static std::optional<std::chrono::milliseconds> maxFrameInterval(std::optional<qreal> frameRate)
37{
38 if (!frameRate || *frameRate <= 0)
39 return std::nullopt;
40
41 // Slightly longer than the nominal interval. More forgiving on loaded CI machines.
42 const qreal margin = isCI() ? 3.0 : 1.5;
43 return std::chrono::milliseconds(qRound64(1000.0 / *frameRate * margin));
44}
45
46static qint64 startTimeOf(const QVideoFrame &frame)
47{
48 return frame.startTime();
49}
50
52{
53 return info.startTime;
54}
55
56static VideoFrameInfo makeVideoFrameInfo(const QVideoFrame &frame)
57{
58 return VideoFrameInfo{
59 frame.isValid(),
60 frame.size(),
61 frame.pixelFormat(),
62 frame.startTime(),
63 frame.endTime(),
64 };
65}
66
67FrameGrabber::FrameGrabber()
68{
69 connect(this, &QVideoSink::videoFrameChanged, this, &FrameGrabber::onFrameReceived);
70}
71
72void FrameGrabber::onFrameReceived(const QVideoFrame &frame)
73{
74 m_frameInfos.push_back(makeVideoFrameInfo(frame));
75 if (m_retainVideoFrames)
76 m_videoFrames.push_back(frame);
77
78 if (!frame.isValid()) {
79 // A null-frame ends the stream, and the next valid frame starts a new one.
80 resetFirstFrame();
81 m_streamStarted = false;
82 } else if (!m_streamStarted) {
83 m_firstFrameInfo = m_frameInfos.back();
84 if (m_retainFirstVideoFrame)
85 m_firstVideoFrame = frame;
86 m_streamStarted = true;
87 }
88}
89
91{
92 return m_frameInfos;
93}
94
96 size_t minCount,
97 std::optional<qreal> expectedFrameRate,
98 qint64 noOlderThanTime)
99{
100 return waitAndTake(m_frameInfos, minCount, expectedFrameRate, noOlderThanTime);
101}
102
104 size_t minCount,
105 std::optional<qreal> expectedFrameRate,
106 qint64 noOlderThanTime)
107{
108 // Holding on to QVideoFrames can stall backends that use a fixed-size
109 // frame pool, so only retain them while the caller is waiting for them.
110 m_retainVideoFrames = true;
111 const auto stopRetaining = qScopeGuard([this] {
112 m_retainVideoFrames = false;
113 m_videoFrames.clear();
114 });
115
116 return waitAndTake(m_videoFrames, minCount, expectedFrameRate, noOlderThanTime);
117}
118
119template <typename Frame>
120std::vector<Frame> FrameGrabber::waitAndTake(
121 std::vector<Frame> &frames,
122 size_t minCount,
123 std::optional<qreal> expectedFrameRate,
124 qint64 noOlderThanTime)
125{
126 m_frameInfos.clear();
127 m_videoFrames.clear();
128
129 const auto enoughFramesOrStopped = [this, &frames, minCount, noOlderThanTime]() -> bool {
130 if (m_stopped)
131 return true; // Stop waiting
132
133 // ensure that all signals &QVideoSink::videoFrameChanged have been processed
134 QCoreApplication::processEvents(QEventLoop::AllEvents);
135
136 if (noOlderThanTime > 0) {
137 // Reject frames older than noOlderThanTime
138 q20::erase_if(frames, [noOlderThanTime](const Frame &frame) {
139 return startTimeOf(frame) <= noOlderThanTime;
140 });
141 }
142
143 return frames.size() >= minCount;
144 };
145
146 // The first frame may be delayed by initialization. Beyond that, each
147 // frame is due within the frame interval.
148 std::chrono::milliseconds timeout = surfaceCaptureTestTimeout();
149 std::optional<std::chrono::milliseconds> interval = maxFrameInterval(expectedFrameRate);
150 if (interval)
151 timeout += *interval * qint64(minCount);
152
153 if (!QTest::qWaitFor(enoughFramesOrStopped, timeout))
154 return {};
155
156 if (m_stopped)
157 return {};
158
159 return std::exchange(frames, {});
160}
161
162bool FrameGrabber::waitForFirstFrame()
163{
164 const auto firstFrameReceivedOrStopped = [this] {
165 return m_stopped || m_firstFrameInfo;
166 };
167
168 if (!QTest::qWaitFor(firstFrameReceivedOrStopped, surfaceCaptureTestTimeout()))
169 return false;
170
171 return !m_stopped;
172}
173
175{
176 if (!waitForFirstFrame())
177 return std::nullopt;
178
179 return m_firstFrameInfo;
180}
181
183{
184 Q_ASSERT_X(
185 m_retainFirstVideoFrame,
186 "FrameGrabber::consumeFirstVideoFrame",
187 "setRetainFirstVideoFrame() must be enabled before the stream starts");
188
189 if (!waitForFirstFrame())
190 return std::nullopt;
191
192 return std::exchange(m_firstVideoFrame, std::nullopt);
193}
194
196{
197 m_retainFirstVideoFrame = retain;
198 if (!retain)
199 m_firstVideoFrame.reset();
200}
201
202void FrameGrabber::resetFirstFrame()
203{
204 m_firstFrameInfo.reset();
205 m_firstVideoFrame.reset();
206}
207
208std::optional<std::chrono::milliseconds> FrameGrabber::durationBetweenFrames(
209 qsizetype frameCount,
210 std::optional<qreal> expectedFrameRate)
211{
212 Q_ASSERT(frameCount > 0);
213
214 using namespace std::chrono;
215
216 QElapsedTimer timer;
217 QElapsedTimer sinceLastFrame;
218 qsizetype framesReceived = 0;
219
220 QObject context;
221 connect(this, &QVideoSink::videoFrameChanged, &context, [&]() {
222 if (framesReceived++ == 0)
223 timer.start();
224 sinceLastFrame.start();
225 });
226
227 std::optional<milliseconds> maxInterval = maxFrameInterval(expectedFrameRate);
228
229 // Fails early if the frames stop coming at the expected rate.
230 auto allFramesReceivedOrStalled = [&]() {
231 if (framesReceived > frameCount)
232 return true;
233 return maxInterval && framesReceived > 0 && sinceLastFrame.hasExpired(maxInterval->count());
234 };
235
236 // Without an expected frame rate, assume the stream runs at 1 FPS minimum.
237 // With one, the first frame gets the global timeout, and each of the rest are
238 // checked against the frame interval above.
239 milliseconds timeout = surfaceCaptureTestTimeout();
240 if (maxInterval)
241 timeout += *maxInterval * frameCount;
242 else
243 timeout = isCI() ? 10s * frameCount : 2s * frameCount;
244
245 if (!QTest::qWaitFor(allFramesReceivedOrStalled, timeout) || framesReceived <= frameCount)
246 return std::nullopt;
247
248 return milliseconds(timer.elapsed() / frameCount);
249}
250
252{
253 return m_stopped;
254}
255
257{
258 // Not all backends end the stream with a null-frame, so treat activation
259 // as the start of a new stream as well.
260 if (active) {
261 resetFirstFrame();
262 m_streamStarted = false;
263 }
264}
265
266void FrameGrabber::stop()
267{
268 qWarning() << "Stopping grabber";
269 m_stopped = true;
270}
271
272} // namespace QtMultimediaTest
273
274QT_END_NAMESPACE
275
276#include "moc_framegrabber_p.cpp"
The FrameGrabber stores frames that arrive from a surface capture, and is used to inspect captured fr...
std::vector< QVideoFrame > waitAndTakeVideoFrames(size_t minCount, std::optional< qreal > expectedFrameRate=std::nullopt, qint64 noOlderThanTime=0)
Same as waitAndTakeFrameInfos(), but returns the video frames themselves.
void onCaptureActiveChanged(bool active)
Starts tracking a new stream when the capture becomes active, so that tryWaitForFirstFrameInfo() wait...
std::vector< VideoFrameInfo > waitAndTakeFrameInfos(size_t minCount, std::optional< qreal > expectedFrameRate=std::nullopt, qint64 noOlderThanTime=0)
Wait for at least minCount frames that are no older than noOlderThanTime.
void setRetainFirstVideoFrame(bool retain)
std::optional< std::chrono::milliseconds > durationBetweenFrames(qsizetype frameCount=1, std::optional< qreal > expectedFrameRate=std::nullopt)
Measures the average duration between frameCount consecutive frames.
const std::vector< VideoFrameInfo > & getFrameInfos() const
std::optional< QVideoFrame > consumeFirstVideoFrame()
Same as tryWaitForFirstFrameInfo(), but returns the video frame itself, and consumes it.
std::optional< VideoFrameInfo > tryWaitForFirstFrameInfo()
Waits for the first frame of the current stream, and returns a copy of its info.
static std::optional< std::chrono::milliseconds > maxFrameInterval(std::optional< qreal > frameRate)
static qint64 startTimeOf(const VideoFrameInfo &info)
static VideoFrameInfo makeVideoFrameInfo(const QVideoFrame &frame)
std::chrono::milliseconds surfaceCaptureTestTimeout()
static qint64 startTimeOf(const QVideoFrame &frame)