Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "framegrabber_p.h"
#include "mediabackendutils_p.h"
#include <QtTest/qtest.h>
#include <QtTest/private/qtestblacklist_p.h>
#include <QtMultimedia/qvideoframe.h>
#include <QtCore/q20vector.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qelapsedtimer.h>
#include <QtCore/qscopeguard.h>
#include <utility>
QT_BEGIN_NAMESPACE
namespace QtMultimediaTest {
std::chrono::milliseconds surfaceCaptureTestTimeout()
{
// A blacklisted test is expected to fail, so don't make it wait for the full
// timeout to do so.
if (const char *testFunction = QTest::currentTestFunction();
testFunction && QTestPrivate::checkBlackLists(testFunction, QTest::currentDataTag())) {
return std::chrono::seconds(1);
}
if (isCI())
return std::chrono::seconds(60);
return std::chrono::seconds(5);
}
[[nodiscard]] static std::optional<std::chrono::milliseconds> maxFrameInterval(std::optional<qreal> frameRate)
{
if (!frameRate || *frameRate <= 0)
return std::nullopt;
// Slightly longer than the nominal interval. More forgiving on loaded CI machines.
const qreal margin = isCI() ? 3.0 : 1.5;
return std::chrono::milliseconds(qRound64(1000.0 / *frameRate * margin));
}
static qint64 startTimeOf(const QVideoFrame &frame)
{
return frame.startTime();
}
static qint64 startTimeOf(const VideoFrameInfo &info)
{
return info.startTime;
}
static VideoFrameInfo makeVideoFrameInfo(const QVideoFrame &frame)
{
return VideoFrameInfo{
frame.isValid(),
frame.size(),
frame.pixelFormat(),
frame.startTime(),
frame.endTime(),
};
}
FrameGrabber::FrameGrabber()
{
connect(this, &QVideoSink::videoFrameChanged, this, &FrameGrabber::onFrameReceived);
}
void FrameGrabber::onFrameReceived(const QVideoFrame &frame)
{
m_frameInfos.push_back(makeVideoFrameInfo(frame));
if (m_retainVideoFrames)
m_videoFrames.push_back(frame);
if (!frame.isValid()) {
// A null-frame ends the stream, and the next valid frame starts a new one.
resetFirstFrame();
m_streamStarted = false;
} else if (!m_streamStarted) {
m_firstFrameInfo = m_frameInfos.back();
if (m_retainFirstVideoFrame)
m_firstVideoFrame = frame;
m_streamStarted = true;
}
}
const std::vector<VideoFrameInfo> &FrameGrabber::getFrameInfos() const
{
return m_frameInfos;
}
std::vector<VideoFrameInfo> FrameGrabber::waitAndTakeFrameInfos(
size_t minCount,
std::optional<qreal> expectedFrameRate,
qint64 noOlderThanTime)
{
return waitAndTake(m_frameInfos, minCount, expectedFrameRate, noOlderThanTime);
}
std::vector<QVideoFrame> FrameGrabber::waitAndTakeVideoFrames(
size_t minCount,
std::optional<qreal> expectedFrameRate,
qint64 noOlderThanTime)
{
// Holding on to QVideoFrames can stall backends that use a fixed-size
// frame pool, so only retain them while the caller is waiting for them.
m_retainVideoFrames = true;
const auto stopRetaining = qScopeGuard([this] {
m_retainVideoFrames = false;
m_videoFrames.clear();
});
return waitAndTake(m_videoFrames, minCount, expectedFrameRate, noOlderThanTime);
}
template <typename Frame>
std::vector<Frame> FrameGrabber::waitAndTake(
std::vector<Frame> &frames,
size_t minCount,
std::optional<qreal> expectedFrameRate,
qint64 noOlderThanTime)
{
m_frameInfos.clear();
m_videoFrames.clear();
const auto enoughFramesOrStopped = [this, &frames, minCount, noOlderThanTime]() -> bool {
if (m_stopped)
return true; // Stop waiting
// ensure that all signals &QVideoSink::videoFrameChanged have been processed
QCoreApplication::processEvents(QEventLoop::AllEvents);
if (noOlderThanTime > 0) {
// Reject frames older than noOlderThanTime
q20::erase_if(frames, [noOlderThanTime](const Frame &frame) {
return startTimeOf(frame) <= noOlderThanTime;
});
}
return frames.size() >= minCount;
};
// The first frame may be delayed by initialization. Beyond that, each
// frame is due within the frame interval.
std::chrono::milliseconds timeout = surfaceCaptureTestTimeout();
std::optional<std::chrono::milliseconds> interval = maxFrameInterval(expectedFrameRate);
if (interval)
timeout += *interval * qint64(minCount);
if (!QTest::qWaitFor(enoughFramesOrStopped, timeout))
return {};
if (m_stopped)
return {};
return std::exchange(frames, {});
}
bool FrameGrabber::waitForFirstFrame()
{
const auto firstFrameReceivedOrStopped = [this] {
return m_stopped || m_firstFrameInfo;
};
if (!QTest::qWaitFor(firstFrameReceivedOrStopped, surfaceCaptureTestTimeout()))
return false;
return !m_stopped;
}
std::optional<VideoFrameInfo> FrameGrabber::tryWaitForFirstFrameInfo()
{
if (!waitForFirstFrame())
return std::nullopt;
return m_firstFrameInfo;
}
std::optional<QVideoFrame> FrameGrabber::consumeFirstVideoFrame()
{
Q_ASSERT_X(
m_retainFirstVideoFrame,
"FrameGrabber::consumeFirstVideoFrame",
"setRetainFirstVideoFrame() must be enabled before the stream starts");
if (!waitForFirstFrame())
return std::nullopt;
return std::exchange(m_firstVideoFrame, std::nullopt);
}
void FrameGrabber::setRetainFirstVideoFrame(bool retain)
{
m_retainFirstVideoFrame = retain;
if (!retain)
m_firstVideoFrame.reset();
}
void FrameGrabber::resetFirstFrame()
{
m_firstFrameInfo.reset();
m_firstVideoFrame.reset();
}
std::optional<std::chrono::milliseconds> FrameGrabber::durationBetweenFrames(
qsizetype frameCount,
std::optional<qreal> expectedFrameRate)
{
Q_ASSERT(frameCount > 0);
using namespace std::chrono;
QElapsedTimer timer;
QElapsedTimer sinceLastFrame;
qsizetype framesReceived = 0;
QObject context;
connect(this, &QVideoSink::videoFrameChanged, &context, [&]() {
if (framesReceived++ == 0)
timer.start();
sinceLastFrame.start();
});
std::optional<milliseconds> maxInterval = maxFrameInterval(expectedFrameRate);
// Fails early if the frames stop coming at the expected rate.
auto allFramesReceivedOrStalled = [&]() {
if (framesReceived > frameCount)
return true;
return maxInterval && framesReceived > 0 && sinceLastFrame.hasExpired(maxInterval->count());
};
// Without an expected frame rate, assume the stream runs at 1 FPS minimum.
// With one, the first frame gets the global timeout, and each of the rest are
// checked against the frame interval above.
milliseconds timeout = surfaceCaptureTestTimeout();
if (maxInterval)
timeout += *maxInterval * frameCount;
else
timeout = isCI() ? 10s * frameCount : 2s * frameCount;
if (!QTest::qWaitFor(allFramesReceivedOrStalled, timeout) || framesReceived <= frameCount)
return std::nullopt;
return milliseconds(timer.elapsed() / frameCount);
}
bool FrameGrabber::isStopped() const
{
return m_stopped;
}
void FrameGrabber::onCaptureActiveChanged(bool active)
{
// Not all backends end the stream with a null-frame, so treat activation
// as the start of a new stream as well.
if (active) {
resetFirstFrame();
m_streamStarted = false;
}
}
void FrameGrabber::stop()
{
m_stopped = true;
}
} // namespace QtMultimediaTest
QT_END_NAMESPACE
#include "moc_framegrabber_p.cpp"