summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJøger Hansegård <joger.hansegard@qt.io>2023-08-03 13:12:03 +0200
committerJøger Hansegård <joger.hansegard@qt.io>2023-08-19 00:12:04 +0200
commitefb672d13af3a46fa8447335e9e3c31e26b50e13 (patch)
tree9a1a7ddc626864bce39a15c601d307c05009ec58
parent44893dda471c4e7a59fdb6fed7e253ae36197a4d (diff)
Add integration tests for window capturing
The tests revealed occasional failure to start window capturing on Windows UWP platform. This was worked around using a retry loop when the error occurred. Fixes: QTBUG-115749 Change-Id: Id86c17f1b84fc9ba07e026ed6015cb2cc659a213 Reviewed-by: Artem Dyomin <artem.dyomin@qt.io> (cherry picked from commit f9e58abcd2f7ac2abf2b6e6fa38ac5c5cc11a0c2 with manual resolution of build issues) Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
-rw-r--r--src/plugins/multimedia/ffmpeg/qffmpegwindowcapture_uwp.cpp48
-rw-r--r--tests/auto/integration/CMakeLists.txt1
-rw-r--r--tests/auto/integration/qwindowcapturebackend/BLACKLIST13
-rw-r--r--tests/auto/integration/qwindowcapturebackend/CMakeLists.txt20
-rw-r--r--tests/auto/integration/qwindowcapturebackend/fixture.cpp235
-rw-r--r--tests/auto/integration/qwindowcapturebackend/fixture.h147
-rw-r--r--tests/auto/integration/qwindowcapturebackend/grabber.cpp62
-rw-r--r--tests/auto/integration/qwindowcapturebackend/grabber.h43
-rw-r--r--tests/auto/integration/qwindowcapturebackend/tst_qwindowcapturebackend.cpp267
-rw-r--r--tests/auto/integration/qwindowcapturebackend/widget.cpp125
-rw-r--r--tests/auto/integration/qwindowcapturebackend/widget.h43
11 files changed, 1000 insertions, 4 deletions
diff --git a/src/plugins/multimedia/ffmpeg/qffmpegwindowcapture_uwp.cpp b/src/plugins/multimedia/ffmpeg/qffmpegwindowcapture_uwp.cpp
index 959a3a962..b2d62306e 100644
--- a/src/plugins/multimedia/ffmpeg/qffmpegwindowcapture_uwp.cpp
+++ b/src/plugins/multimedia/ffmpeg/qffmpegwindowcapture_uwp.cpp
@@ -141,7 +141,8 @@ struct WindowGrabber
{
WindowGrabber() = default;
- WindowGrabber(IDXGIAdapter1 *adapter, HWND hwnd) : m_frameSize{ getWindowSize(hwnd) }
+ WindowGrabber(IDXGIAdapter1 *adapter, HWND hwnd)
+ : m_frameSize{ getWindowSize(hwnd) }, m_captureWindow{ hwnd }
{
check_hresult(D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, nullptr, 0,
D3D11_SDK_VERSION, m_device.put(), nullptr, nullptr));
@@ -174,8 +175,19 @@ struct WindowGrabber
com_ptr<IDXGISurface> tryGetFrame()
{
const Direct3D11CaptureFrame frame = m_framePool.TryGetNextFrame();
- if (!frame)
+ if (!frame) {
+
+ // Stop capture and report failure if window was closed. If we don't stop,
+ // testing shows that either we don't get any frames, or we get blank frames.
+ // Emitting an error will prevent this inconsistent behavior, and makes the
+ // Windows implementation behave like the Linux implementation
+ if (!IsWindow(m_captureWindow))
+ throw std::runtime_error("Window was closed");
+
+ // Blank frames may come spuriously if no new window texture
+ // is available yet.
return {};
+ }
if (m_frameSize != frame.ContentSize()) {
m_frameSize = frame.ContentSize();
@@ -193,8 +205,33 @@ private:
const auto interop = factory.as<IGraphicsCaptureItemInterop>();
GraphicsCaptureItem item = { nullptr };
- check_hresult(interop->CreateForWindow(hwnd, winrt::guid_of<GraphicsCaptureItem>(),
- winrt::put_abi(item)));
+ winrt::hresult status = S_OK;
+
+ // Attempt to create capture item with retry, because this occasionally fails,
+ // particularly in unit tests. When the failure code is E_INVALIDARG, it
+ // seems to help to sleep for a bit and retry. See QTBUG-116025.
+ constexpr int maxRetry = 10;
+ constexpr std::chrono::milliseconds retryDelay{ 100 };
+ for (int retryNum = 0; retryNum < maxRetry; ++retryNum) {
+
+ status = interop->CreateForWindow(hwnd, winrt::guid_of<GraphicsCaptureItem>(),
+ winrt::put_abi(item));
+
+ if (status != E_INVALIDARG)
+ break;
+
+ qCWarning(qLcWindowCaptureUwp)
+ << "Failed to create capture item:"
+ << QString::fromStdWString(winrt::hresult_error(status).message().c_str())
+ << "Retry number" << retryNum;
+
+ if (retryNum + 1 < maxRetry)
+ QThread::sleep(retryDelay);
+ }
+
+ // Throw if we fail to create the capture item
+ check_hresult(status);
+
return item;
}
@@ -243,6 +280,7 @@ private:
return texture.as<IDXGISurface>();
}
+ HWND m_captureWindow{};
winrt::Windows::Graphics::SizeInt32 m_frameSize{};
com_ptr<ID3D11Device> m_device;
Direct3D11CaptureFramePool m_framePool{ nullptr };
@@ -320,6 +358,8 @@ protected:
+ QString::fromWCharArray(err.message().c_str());
updateError(InternalError, message);
+ } catch (const std::runtime_error& e) {
+ updateError(CaptureFailed, QString::fromLatin1(e.what()));
}
return {};
diff --git a/tests/auto/integration/CMakeLists.txt b/tests/auto/integration/CMakeLists.txt
index 14d8814ee..b9869c214 100644
--- a/tests/auto/integration/CMakeLists.txt
+++ b/tests/auto/integration/CMakeLists.txt
@@ -14,6 +14,7 @@ if(TARGET Qt::Widgets)
add_subdirectory(qmediacapturesession)
add_subdirectory(qcamerabackend)
add_subdirectory(qscreencapture_integration)
+ add_subdirectory(qwindowcapturebackend)
endif()
if(TARGET Qt::Quick)
add_subdirectory(qquickvideooutput)
diff --git a/tests/auto/integration/qwindowcapturebackend/BLACKLIST b/tests/auto/integration/qwindowcapturebackend/BLACKLIST
new file mode 100644
index 000000000..bc176cf98
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/BLACKLIST
@@ -0,0 +1,13 @@
+macos ci
+
+#QTBUG-112827 on Android
+#QTBUG-111190, v4l2m2m issues
+[recorder_encodesFrames_toValidMediaFile]
+linux ci
+android ci
+
+#QTBUG-112827 on Android
+#QTBUG-111190, v4l2m2m issues
+[recorder_encodesFrames_toValidMediaFile_whenWindowResizes]
+linux ci
+android ci
diff --git a/tests/auto/integration/qwindowcapturebackend/CMakeLists.txt b/tests/auto/integration/qwindowcapturebackend/CMakeLists.txt
new file mode 100644
index 000000000..8f633a1da
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/CMakeLists.txt
@@ -0,0 +1,20 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_test(tst_qwindowcapturebackend
+ SOURCES
+ tst_qwindowcapturebackend.cpp
+ widget.h
+ widget.cpp
+ grabber.h
+ grabber.cpp
+ fixture.h
+ fixture.cpp
+ LIBRARIES
+ Qt::Multimedia
+ Qt::Gui
+ Qt::Widgets
+ Qt::MultimediaWidgets
+)
+
+
diff --git a/tests/auto/integration/qwindowcapturebackend/fixture.cpp b/tests/auto/integration/qwindowcapturebackend/fixture.cpp
new file mode 100644
index 000000000..98964eeda
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/fixture.cpp
@@ -0,0 +1,235 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "fixture.h"
+
+#include <qmediaplayer.h>
+#include <qvideowidget.h>
+#include <qsystemsemaphore.h>
+#include <quuid.h>
+
+DisableCursor::DisableCursor()
+{
+ QCursor cursor(Qt::BlankCursor);
+ QApplication::setOverrideCursor(cursor);
+}
+
+DisableCursor::~DisableCursor()
+{
+ QGuiApplication::restoreOverrideCursor();
+}
+
+WindowCaptureFixture::WindowCaptureFixture()
+{
+ m_session.setWindowCapture(&m_capture);
+ m_session.setVideoSink(&m_grabber);
+}
+
+QString WindowCaptureFixture::getResultsPath(const QString &fileName)
+{
+ const QString sep = QString::fromLatin1("--");
+
+ QString stem = QCoreApplication::applicationName();
+ if (const char *currentTest = QTest::currentTestFunction())
+ stem += sep + QString::fromLatin1(currentTest);
+
+ if (const char *currentTag = QTest::currentDataTag())
+ stem += sep + QString::fromLatin1(currentTag);
+
+ stem += sep + fileName;
+
+ const QDir resultsDir = qEnvironmentVariable("COIN_CTEST_RESULTSDIR", QDir::tempPath());
+
+ return resultsDir.filePath(stem);
+}
+
+bool WindowCaptureFixture::compareImages(QImage actual, const QImage &expected,
+ const QString &fileSuffix)
+{
+ // Convert to same format so that we can compare images
+ actual = actual.convertToFormat(expected.format());
+
+ if (actual == expected)
+ return true;
+
+ qWarning() << "Image comparison failed.";
+ qWarning() << "Actual image:";
+ qWarning() << actual;
+ qWarning() << "Expected image:";
+ qWarning() << expected;
+
+ const QString actualName = getResultsPath(QString("actual%1.png").arg(fileSuffix));
+ if (!actual.save(actualName))
+ qWarning() << "Failed to save actual file to " << actualName;
+
+ const QString expectedName = getResultsPath(QString("expected%1.png").arg(fileSuffix));
+ if (!expected.save(expectedName))
+ qWarning() << "Failed to save expected file to " << expectedName;
+
+ return false;
+}
+
+bool WindowCaptureWithWidgetFixture::start(QSize size)
+{
+ // In case of window capture failure, signal the grabber so we can stop
+ // waiting for frames that will never come.
+ connect(&m_capture, &QWindowCapture::errorOccurred, &m_grabber, &FrameGrabber::stop);
+
+ m_widget.setSize(size);
+
+ m_widget.show();
+
+ // Make sure window is in a state that allows it to be found by QWindowCapture.
+ // Not necessary on Windows, but seems to be necessary on some platforms.
+ if (!QTest::qWaitForWindowExposed(&m_widget, static_cast<int>(s_testTimeout.count()))) {
+ qWarning() << "Failed to display widget within timeout";
+ return false;
+ }
+
+ m_captureWindow = findCaptureWindow(m_widget.windowTitle());
+
+ if (!m_captureWindow.isValid())
+ return false;
+
+ m_capture.setWindow(m_captureWindow);
+ m_capture.setActive(true);
+
+ return true;
+}
+
+QVideoFrame WindowCaptureWithWidgetFixture::waitForFrame(qint64 noOlderThanTime)
+{
+ const std::vector<QVideoFrame> frames = m_grabber.waitAndTakeFrames(1u, noOlderThanTime);
+ if (frames.empty())
+ return QVideoFrame{};
+
+ return frames.back();
+}
+
+QCapturableWindow WindowCaptureWithWidgetFixture::findCaptureWindow(const QString &windowTitle)
+{
+ QList<QCapturableWindow> allWindows = QWindowCapture::capturableWindows();
+
+ const auto window = std::find_if(allWindows.begin(), allWindows.end(),
+ [windowTitle](const QCapturableWindow &win) {
+ return win.description() == windowTitle;
+ });
+
+ // Extra debug output to help understanding if test widget window could not be found
+ if (window == allWindows.end()) {
+ qDebug() << "Could not find window" << windowTitle << ". Existing capturable windows:";
+ std::for_each(allWindows.begin(), allWindows.end(), [](const QCapturableWindow &win) {
+ qDebug() << " " << win.description();
+ });
+ return QCapturableWindow{};
+ }
+
+ return *window;
+}
+
+void WindowCaptureWithWidgetAndRecorderFixture::start(QSize size, bool togglePattern)
+{
+ if (togglePattern) {
+ // Drive animation
+ connect(&m_grabber, &FrameGrabber::videoFrameChanged, &m_widget,
+ &TestWidget::togglePattern);
+ }
+
+ connect(&m_recorder, &QMediaRecorder::recorderStateChanged, this,
+ &WindowCaptureWithWidgetAndRecorderFixture::recorderStateChanged);
+
+ m_session.setRecorder(&m_recorder);
+ m_recorder.setQuality(QMediaRecorder::HighQuality);
+ m_recorder.setOutputLocation(QUrl::fromLocalFile(m_mediaFile));
+ m_recorder.setVideoResolution(size);
+
+ WindowCaptureWithWidgetFixture::start(size);
+
+ m_recorder.record();
+}
+
+bool WindowCaptureWithWidgetAndRecorderFixture::stop()
+{
+ m_recorder.stop();
+
+ const auto recorderStopped = [this] { return m_recorderState == QMediaRecorder::StoppedState; };
+
+ return QTest::qWaitFor(recorderStopped, static_cast<int>(s_testTimeout.count()));
+}
+
+bool WindowCaptureWithWidgetAndRecorderFixture::testVideoFilePlayback(const QString &fileName)
+{
+ QVideoWidget widget;
+
+ QMediaPlayer player;
+
+ bool playing = true;
+ connect(&player, &QMediaPlayer::playbackStateChanged, this,
+ [&](QMediaPlayer::PlaybackState state) {
+ if (state == QMediaPlayer::StoppedState)
+ playing = false;
+ });
+
+ QMediaPlayer::Error error = QMediaPlayer::NoError;
+ connect(&player, &QMediaPlayer::errorOccurred, this,
+ [&](QMediaPlayer::Error e, const QString &errorString) {
+ error = e;
+ qWarning() << errorString;
+ });
+
+ player.setSource(QUrl{ fileName });
+ player.setVideoOutput(&widget);
+ widget.show();
+ player.play();
+
+ const bool completed =
+ QTest::qWaitFor([&] { return !playing || error != QMediaPlayer::NoError; },
+ static_cast<int>(s_testTimeout.count()));
+
+ return completed && error == QMediaPlayer::NoError;
+}
+
+void WindowCaptureWithWidgetAndRecorderFixture::recorderStateChanged(
+ QMediaRecorder::RecorderState state)
+{
+ m_recorderState = state;
+}
+
+bool WindowCaptureWithWidgetInOtherProcessFixture::start()
+{
+ // In case of window capture failure, signal the grabber so we can stop
+ // waiting for frames that will never come.
+ connect(&m_capture, &QWindowCapture::errorOccurred, &m_grabber, &FrameGrabber::stop);
+
+ // Create a new window title that is also used as a semaphore key with less than 30 characters
+ const QString windowTitle = QString::number(qHash(QUuid::createUuid().toString()));
+
+ QSystemSemaphore windowVisible{ QNativeIpcKey{ windowTitle } };
+
+ // Start another instance of the test executable and ask it to show a
+ // its test widget.
+ m_windowProcess.setArguments({ "--show", windowTitle });
+ m_windowProcess.setProgram(QApplication::applicationFilePath());
+ m_windowProcess.start();
+
+ // Make sure window is in a state that allows it to be found by QWindowCapture.
+ // We do this by waiting for the process to release the semaphore once its window is visible
+ windowVisible.acquire();
+
+ m_captureWindow = findCaptureWindow(windowTitle);
+
+ if (!m_captureWindow.isValid())
+ return false;
+
+ // Start capturing the out-of-process window
+ m_capture.setWindow(m_captureWindow);
+ m_capture.setActive(true);
+
+ // Show in-process widget used to create a reference image
+ m_widget.show();
+
+ return true;
+}
+
+
+#include "moc_fixture.cpp"
diff --git a/tests/auto/integration/qwindowcapturebackend/fixture.h b/tests/auto/integration/qwindowcapturebackend/fixture.h
new file mode 100644
index 000000000..8549d6e9f
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/fixture.h
@@ -0,0 +1,147 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#ifndef WINDOW_CAPTURE_FIXTURE_H
+#define WINDOW_CAPTURE_FIXTURE_H
+
+#include "grabber.h"
+#include "widget.h"
+
+#include <chrono>
+#include <qmediacapturesession.h>
+#include <qmediarecorder.h>
+#include <qobject.h>
+#include <qsignalspy.h>
+#include <qtest.h>
+#include <qvideoframe.h>
+#include <qwindowcapture.h>
+#include <qprocess.h>
+
+QT_USE_NAMESPACE
+
+constexpr inline std::chrono::milliseconds s_testTimeout = std::chrono::seconds(60);
+
+/*!
+ Utility used to hide application cursor for image comparison tests.
+ On Windows, the mouse cursor is captured as part of the window capture.
+ This and can cause differences when comparing captured images with images
+ from QWindow::grab() which is used as a reference.
+*/
+struct DisableCursor final
+{
+ DisableCursor();
+ ~DisableCursor();
+
+ DisableCursor(const DisableCursor &) = delete;
+ DisableCursor &operator=(const DisableCursor &) = delete;
+};
+
+/*!
+ Fixture class that orchestrates setup/teardown of window capturing
+*/
+class WindowCaptureFixture : public QObject
+{
+ Q_OBJECT
+
+public:
+ WindowCaptureFixture();
+
+ /*!
+ Compare two images, ignoring format.
+ If images differ, diagnostic output is logged and images are saved to file.
+ */
+ static bool compareImages(QImage actual, const QImage &expected,
+ const QString &fileSuffix = "");
+
+ QMediaCaptureSession m_session;
+ QWindowCapture m_capture;
+ FrameGrabber m_grabber;
+
+ QSignalSpy m_errors{ &m_capture, &QWindowCapture::errorOccurred };
+ QSignalSpy m_activations{ &m_capture, &QWindowCapture::activeChanged };
+
+private:
+ /*!
+ Calculate a result path based upon a single filename.
+ On CI, the file will be located in COIN_CTEST_RESULTSDIR, and on developer
+ computers, the file will be located in TEMP.
+
+ The file name is on the form "testCase_testFunction_[dataTag_]fileName"
+ */
+ static QString getResultsPath(const QString &fileName);
+};
+
+/*!
+ Fixture class that extends window capture fixture with a capturable widget
+*/
+class WindowCaptureWithWidgetFixture : public WindowCaptureFixture
+{
+ Q_OBJECT
+
+public:
+ /*!
+ Starts capturing and returns true if successful.
+
+ Two phase initialization is used to be able to detect
+ failure to find widget window as a capturable window.
+ */
+ bool start(QSize size = { 60, 40 });
+
+ /*!
+ Waits until the a captured frame is received and returns it
+ */
+ QVideoFrame waitForFrame(qint64 noOlderThanTime = 0);
+
+ DisableCursor m_cursorDisabled; // Avoid mouse cursor causing image differences
+ TestWidget m_widget;
+ QCapturableWindow m_captureWindow;
+
+protected:
+ static QCapturableWindow findCaptureWindow(const QString &windowTitle);
+};
+
+class WindowCaptureWithWidgetInOtherProcessFixture : public WindowCaptureWithWidgetFixture
+{
+ Q_OBJECT
+
+public:
+ ~WindowCaptureWithWidgetInOtherProcessFixture() { m_windowProcess.close(); }
+
+ /*!
+ Create widget in separate process and start capturing its content
+ */
+ bool start();
+
+ QProcess m_windowProcess;
+};
+
+class WindowCaptureWithWidgetAndRecorderFixture : public WindowCaptureWithWidgetFixture
+{
+ Q_OBJECT
+
+public:
+ void start(QSize size = { 60, 40 }, bool togglePattern = true);
+
+ /*!
+ Stop recording.
+
+ Since recorder finalizes the file asynchronously, even after destructors are called,
+ we need to explicitly wait for the stopped state before ending the test. If we don't
+ do this, the media file can not be deleted by the QTemporaryDir at destruction.
+ */
+ bool stop();
+
+ bool testVideoFilePlayback(const QString& fileName);
+
+public slots:
+ void recorderStateChanged(QMediaRecorder::RecorderState state);
+
+public:
+ QTemporaryDir m_tempDir;
+ const QString m_mediaFile = m_tempDir.filePath("test.mp4");
+ QMediaRecorder m_recorder;
+ QMediaRecorder::RecorderState m_recorderState = QMediaRecorder::StoppedState;
+ QSignalSpy m_recorderErrors{ &m_recorder, &QMediaRecorder::errorOccurred };
+};
+
+#endif
diff --git a/tests/auto/integration/qwindowcapturebackend/grabber.cpp b/tests/auto/integration/qwindowcapturebackend/grabber.cpp
new file mode 100644
index 000000000..59f3e6577
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/grabber.cpp
@@ -0,0 +1,62 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "grabber.h"
+#include "fixture.h"
+
+#include <qtest.h>
+#include <qvideoframe.h>
+
+FrameGrabber::FrameGrabber()
+{
+ const auto copyFrame = [this](const QVideoFrame &frame) { m_frames.push_back(frame); };
+
+ connect(this, &QVideoSink::videoFrameChanged, this, copyFrame, Qt::DirectConnection);
+}
+
+const std::vector<QVideoFrame> &FrameGrabber::getFrames() const
+{
+ return m_frames;
+}
+
+std::vector<QVideoFrame> FrameGrabber::waitAndTakeFrames(size_t minCount, qint64 noOlderThanTime)
+{
+ m_frames.clear();
+
+ const auto enoughFramesOrStopped = [this, minCount, noOlderThanTime]() -> bool {
+ if (m_stopped)
+ return true; // Stop waiting
+
+ if (noOlderThanTime > 0) {
+ // Reject frames older than noOlderThanTime
+ const auto newEnd = std::remove_if(m_frames.begin(), m_frames.end(),
+ [noOlderThanTime](const QVideoFrame &frame) {
+ return frame.startTime() <= noOlderThanTime;
+ });
+ m_frames.erase(newEnd, m_frames.end());
+ }
+
+ return m_frames.size() >= minCount;
+ };
+
+ if (!QTest::qWaitFor(enoughFramesOrStopped, static_cast<int>(s_testTimeout.count())))
+ return {};
+
+ if (m_stopped)
+ return {};
+
+ return std::exchange(m_frames, {});
+}
+
+bool FrameGrabber::isStopped() const
+{
+ return m_stopped;
+}
+
+void FrameGrabber::stop()
+{
+ qWarning() << "Stopping grabber";
+ m_stopped = true;
+}
+
+#include "moc_grabber.cpp"
diff --git a/tests/auto/integration/qwindowcapturebackend/grabber.h b/tests/auto/integration/qwindowcapturebackend/grabber.h
new file mode 100644
index 000000000..aff78f33b
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/grabber.h
@@ -0,0 +1,43 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#ifndef WINDOW_CAPTURE_GRABBER_H
+#define WINDOW_CAPTURE_GRABBER_H
+
+#include <qvideosink.h>
+#include <vector>
+
+QT_USE_NAMESPACE
+
+/*!
+ The FrameGrabber stores frames that arrive from the window capture,
+ and is used to inspect captured frames in the tests.
+*/
+class FrameGrabber : public QVideoSink
+{
+ Q_OBJECT
+
+public:
+ FrameGrabber();
+
+ const std::vector<QVideoFrame> &getFrames() const;
+
+ /*!
+ Wait for at least \a minCount frames that are no older than noOlderThanTime.
+
+ Returns empty if not enough frames arrived, or if grabber was stopped before global timeout
+ elapsed.
+ */
+ std::vector<QVideoFrame> waitAndTakeFrames(size_t minCount, qint64 noOlderThanTime = 0);
+
+ bool isStopped() const;
+
+public slots:
+ void stop();
+
+private:
+ std::vector<QVideoFrame> m_frames;
+ bool m_stopped = false;
+};
+
+#endif
diff --git a/tests/auto/integration/qwindowcapturebackend/tst_qwindowcapturebackend.cpp b/tests/auto/integration/qwindowcapturebackend/tst_qwindowcapturebackend.cpp
new file mode 100644
index 000000000..1beaea069
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/tst_qwindowcapturebackend.cpp
@@ -0,0 +1,267 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+// TESTED_COMPONENT=src/multimedia
+
+#include "fixture.h"
+#include "widget.h"
+
+#include <qmediarecorder.h>
+#include <qpainter.h>
+#include <qsignalspy.h>
+#include <qtest.h>
+#include <qwindowcapture.h>
+#include <qcommandlineparser.h>
+
+#include <chrono>
+#include <vector>
+
+using std::chrono::duration_cast;
+using std::chrono::high_resolution_clock;
+using std::chrono::microseconds;
+
+QT_USE_NAMESPACE
+
+class tst_QWindowCaptureBackend : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ static void initTestCase()
+ {
+#if defined(Q_OS_LINUX)
+ if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci"
+ && qEnvironmentVariable("XDG_SESSION_TYPE").toLower() != "x11")
+ QSKIP("Skip on wayland; to be fixed");
+#endif
+
+ const QWindowCapture capture;
+ if (capture.error() == QWindowCapture::CapturingNotSupported)
+ QSKIP("Screen capturing not supported");
+ }
+
+ void isActive_returnsFalse_whenNotStarted()
+ {
+ const WindowCaptureFixture fixture;
+ QVERIFY(!fixture.m_capture.isActive());
+ }
+
+ void setActive_failsAndEmitEerrorOccurred_whenNoWindowSelected()
+ {
+ WindowCaptureFixture fixture;
+
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression{ ".*Screen capture fail.*" });
+ fixture.m_capture.setActive(true);
+
+ QVERIFY(!fixture.m_capture.isActive());
+ QVERIFY(!fixture.m_errors.empty());
+ }
+
+ void setActive_startsWindowCapture_whenCalledWithTrue()
+ {
+ WindowCaptureWithWidgetFixture fixture;
+ QVERIFY(fixture.start());
+
+ // Ensure that we have received a frame
+ QVERIFY(fixture.waitForFrame().isValid());
+
+ QCOMPARE(fixture.m_activations.size(), 1);
+ QVERIFY(fixture.m_errors.empty());
+ }
+
+ void capturedImage_equals_imageFromGrab_data()
+ {
+ QTest::addColumn<QSize>("windowSize");
+ QTest::newRow("single-pixel-window") << QSize{1, 1};
+ QTest::newRow("small-window") << QSize{60, 40};
+ QTest::newRow("odd-width-window") << QSize{ 61, 40 };
+ QTest::newRow("odd-height-window") << QSize{ 60, 41 };
+ QTest::newRow("big-window") << QApplication::primaryScreen()->size();
+ }
+
+ void capturedImage_equals_imageFromGrab()
+ {
+ QFETCH(QSize, windowSize);
+
+ WindowCaptureWithWidgetFixture fixture;
+ QVERIFY(fixture.start(windowSize));
+
+ const QImage expected = fixture.m_widget.grabImage();
+ const QImage actual = fixture.waitForFrame().toImage();
+
+ QVERIFY(fixture.compareImages(actual, expected));
+ }
+
+ void capturedImage_changes_whenWindowContentChanges()
+ {
+ WindowCaptureWithWidgetFixture fixture;
+ QVERIFY(fixture.start());
+
+ const auto startTime = high_resolution_clock::now();
+
+ const QVideoFrame colorFrame = fixture.waitForFrame();
+ QVERIFY(colorFrame.isValid());
+
+ fixture.m_widget.setDisplayPattern(TestWidget::Grid);
+
+ // Ignore all frames that were grabbed since the colored frame,
+ // to ensure that we get a frame after we changed display pattern
+ const high_resolution_clock::duration delay = high_resolution_clock::now() - startTime;
+ const QVideoFrame gridFrame = fixture.waitForFrame(
+ colorFrame.endTime() + duration_cast<microseconds>(delay).count());
+
+ QVERIFY(gridFrame.isValid());
+
+ // Make sure that the gridFrame has a different content than the colorFrame
+ QCOMPARE(gridFrame.size(), colorFrame.size());
+ QCOMPARE_NE(gridFrame.toImage(), colorFrame.toImage());
+
+ const QImage actualGridImage = fixture.m_widget.grabImage();
+ QVERIFY(fixture.compareImages(gridFrame.toImage(), actualGridImage));
+ }
+
+ void sequenceOfCapturedImages_compareEqual_whenWindowContentIsUnchanged()
+ {
+ WindowCaptureWithWidgetFixture fixture;
+ QVERIFY(fixture.start());
+
+ const std::vector<QVideoFrame> frames = fixture.m_grabber.waitAndTakeFrames(10);
+ QVERIFY(!frames.empty());
+
+ QImage firstFrame = frames.front().toImage();
+ QVERIFY(!firstFrame.isNull());
+
+ qsizetype index = 0;
+ for (const auto &frame : std::as_const(frames)){
+ QVERIFY(fixture.compareImages(frame.toImage(), firstFrame, QString::number(index)));
+ ++index;
+ }
+ }
+
+ void recorder_encodesFrames_toValidMediaFile_data()
+ {
+ QTest::addColumn<QSize>("windowSize");
+ //QTest::newRow("empty-window") << QSize{ 0, 0 }; TODO: Crash
+ //QTest::newRow("single-pixel-window") << QSize{ 1, 1 }; TODO: Crash
+ QTest::newRow("small-window") << QSize{ 60, 40 };
+ QTest::newRow("odd-width-window") << QSize{ 61, 40 };
+ QTest::newRow("odd-height-window") << QSize{ 60, 41 };
+ QTest::newRow("big-window") << QSize{ 800, 600 };
+ }
+
+ void recorder_encodesFrames_toValidMediaFile()
+ {
+ QFETCH(QSize, windowSize);
+
+ WindowCaptureWithWidgetAndRecorderFixture fixture;
+ fixture.start(windowSize);
+
+ // Wait on grabber to ensure that video recorder also get some frames
+ fixture.m_grabber.waitAndTakeFrames(60);
+
+ // Wait for recorder finalization
+ fixture.stop();
+
+ QVERIFY(fixture.m_recorderErrors.empty());
+ QVERIFY(QFile{ fixture.m_mediaFile }.exists());
+ QVERIFY(fixture.testVideoFilePlayback(fixture.m_mediaFile));
+ }
+
+ void recorder_encodesFrames_toValidMediaFile_whenWindowResizes_data()
+ {
+ QTest::addColumn<int>("increment");
+ QTest::newRow("shrink") << -1;
+ QTest::newRow("grow") << 1;
+ }
+
+ void recorder_encodesFrames_toValidMediaFile_whenWindowResizes()
+ {
+ QFETCH(int, increment);
+
+ QSize windowSize = { 200, 150 };
+ WindowCaptureWithWidgetAndRecorderFixture fixture;
+ fixture.start(windowSize, /*toggle pattern*/ false);
+
+ for (qsizetype i = 0; i < 20; ++i) {
+ windowSize.setWidth(windowSize.width() + increment);
+ windowSize.setHeight(windowSize.height() + increment);
+ fixture.m_widget.setSize(windowSize);
+
+ // Wait on grabber to ensure that video recorder also get some frames
+ fixture.m_grabber.waitAndTakeFrames(1);
+ }
+
+ // Wait for recorder finalization
+ fixture.stop();
+
+ QVERIFY(fixture.m_recorderErrors.empty());
+ QVERIFY(QFile{ fixture.m_mediaFile }.exists());
+ QVERIFY(fixture.testVideoFilePlayback(fixture.m_mediaFile));
+ }
+
+ void windowCapture_capturesWindowsInOtherProcesses()
+ {
+ WindowCaptureWithWidgetInOtherProcessFixture fixture;
+ QVERIFY(fixture.start());
+
+ // Get reference image from our in-process widget
+ const QImage expected = fixture.m_widget.grabImage();
+
+ // Get actual image grabbed from out-of-process widget
+ const QImage actual = fixture.waitForFrame().toImage();
+
+ QVERIFY(fixture.compareImages(actual, expected));
+ }
+
+ /*
+ This test is not a requirement per se, but we want all platforms
+ to behave the same. A reasonable alternative could have been to
+ treat closed window as a regular 'Stop' capture (not an error).
+ */
+ void windowCapture_stopsWithError_whenProcessCloses()
+ {
+ WindowCaptureWithWidgetInOtherProcessFixture fixture;
+ QVERIFY(fixture.start());
+
+ // Get capturing started
+ fixture.m_grabber.waitAndTakeFrames(3);
+
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression{ ".*Screen capture fail.*" });
+
+ // Closing the process waits for it to exit
+ fixture.m_windowProcess.close();
+
+ const bool captureFailed = QTest::qWaitFor([&] { return !fixture.m_errors.empty(); },
+ static_cast<int>(s_testTimeout.count()));
+
+ QVERIFY(captureFailed);
+ }
+};
+
+int main(int argc, char *argv[])
+{
+ QCommandLineParser cmd;
+ const QCommandLineOption showTestWidget{ QStringList{ "show" },
+ "Creates a test widget with given title",
+ "windowTitle" };
+ cmd.addOption(showTestWidget);
+ cmd.parse({ argv, argv + argc });
+
+ if (cmd.isSet(showTestWidget)) {
+ QApplication app{ argc, argv };
+ const QString windowTitle = cmd.value(showTestWidget);
+ const bool result = showCaptureWindow(windowTitle);
+ return result ? 0 : 1;
+ }
+
+ // If no special arguments are set, enter the regular QTest main routine
+ TESTLIB_SELFCOVERAGE_START("tst_QWindowCaptureatioBackend")
+ QT_PREPEND_NAMESPACE(QTest::Internal::callInitMain)<tst_QWindowCaptureBackend>();
+ QApplication app(argc, argv);
+ app.setAttribute(Qt::AA_Use96Dpi, true);
+ tst_QWindowCaptureBackend tc;
+ QTEST_SET_MAIN_SOURCE_PATH return QTest::qExec(&tc, argc, argv);
+
+}
+
+#include "tst_qwindowcapturebackend.moc"
diff --git a/tests/auto/integration/qwindowcapturebackend/widget.cpp b/tests/auto/integration/qwindowcapturebackend/widget.cpp
new file mode 100644
index 000000000..0c2336810
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/widget.cpp
@@ -0,0 +1,125 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "widget.h"
+#include "fixture.h"
+
+#include <qapplication.h>
+#include <qsystemsemaphore.h>
+#include <qtest.h>
+
+
+TestWidget::TestWidget(const QString &uuid, QScreen *screen)
+{
+ // Give each window a unique title so that we can uniquely identify it
+ setWindowTitle(uuid);
+
+ setScreen(screen ? screen : QApplication::primaryScreen());
+
+ // Use frameless hint because on Windows UWP platform, the window titlebar is captured,
+ // but the reference image acquired by 'QWindow::grab()' does not include titlebar.
+ // This allows us to do pixel-perfect matching of captured content.
+ setWindowFlags(Qt::Window | Qt::FramelessWindowHint);
+ setFixedSize(60, 40);
+}
+
+void TestWidget::setDisplayPattern(Pattern p)
+{
+ m_pattern = p;
+ repaint();
+}
+
+void TestWidget::setSize(QSize size)
+{
+ if (size == QApplication::primaryScreen()->size())
+ setWindowState(Qt::WindowMaximized);
+ else
+ setFixedSize(size);
+}
+
+QImage TestWidget::grabImage()
+{
+ return grab().toImage();
+}
+
+void TestWidget::togglePattern()
+{
+ Pattern p = m_pattern == ColoredSquares ? Grid : ColoredSquares;
+ setDisplayPattern(p);
+}
+
+void TestWidget::paintEvent(QPaintEvent *paintEvent)
+{
+ QPainter p(this);
+ p.setPen(Qt::NoPen);
+ p.setBrush(Qt::black);
+ p.drawRect(rect());
+
+ if (m_pattern == ColoredSquares)
+ drawColoredSquares(p);
+ else
+ drawGrid(p);
+
+ p.end();
+}
+
+void TestWidget::drawColoredSquares(QPainter &p)
+{
+ const std::vector<std::vector<Qt::GlobalColor>> colors = { { Qt::red, Qt::green, Qt::blue },
+ { Qt::white, Qt::white, Qt::white },
+ { Qt::blue, Qt::green, Qt::red } };
+
+ const QSize squareSize = size() / 3;
+ QRect rect{ QPoint{ 0, 0 }, squareSize };
+
+ for (const auto &row : colors) {
+ for (const auto &color : row) {
+ p.setBrush(color);
+ p.drawRect(rect);
+ rect.moveLeft(rect.left() + rect.width());
+ }
+ rect.moveTo({ 0, rect.bottom() });
+ }
+}
+
+void TestWidget::drawGrid(QPainter &p) const
+{
+ const QSize winSize = size();
+
+ p.setPen(Qt::white);
+
+ QLine vertical{ QPoint{ 5, 0 }, QPoint{ 5, winSize.height() } };
+ while (vertical.x1() < winSize.width()) {
+ p.drawLine(vertical);
+ vertical.translate(10, 0);
+ }
+ QLine horizontal{ QPoint{ 0, 5 }, QPoint{ winSize.width(), 5 } };
+ while (horizontal.y1() < winSize.height()) {
+ p.drawLine(horizontal);
+ horizontal.translate(0, 10);
+ }
+}
+
+bool showCaptureWindow(const QString &windowTitle)
+{
+ const QNativeIpcKey key{ windowTitle };
+ QSystemSemaphore windowVisible(key);
+
+ TestWidget widget{ windowTitle };
+ widget.show();
+
+ // Wait for window to be visible and suitable for window capturing
+ const bool result = QTest::qWaitForWindowExposed(&widget, s_testTimeout.count());
+ if (!result)
+ qDebug() << "Failed to show window";
+
+ // Signal to host process that the window is visible
+ windowVisible.release();
+
+ // Keep window visible until a termination signal is received
+ QApplication::exec();
+
+ return result;
+}
+
+#include "moc_widget.cpp"
diff --git a/tests/auto/integration/qwindowcapturebackend/widget.h b/tests/auto/integration/qwindowcapturebackend/widget.h
new file mode 100644
index 000000000..e74c29021
--- /dev/null
+++ b/tests/auto/integration/qwindowcapturebackend/widget.h
@@ -0,0 +1,43 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#ifndef WINDOW_CAPTURE_WIDGET_H
+#define WINDOW_CAPTURE_WIDGET_H
+
+#include <qwidget.h>
+#include <qscreen.h>
+#include <qpainter.h>
+#include <quuid.h>
+
+/*!
+ Window capable of drawing test patterns used for capture tests
+ */
+class TestWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ enum Pattern { ColoredSquares, Grid };
+
+ TestWidget(const QString &uuid = QUuid::createUuid().toString(), QScreen *screen = nullptr);
+
+ void setDisplayPattern(Pattern p);
+ void setSize(QSize size);
+ QImage grabImage();
+
+public slots:
+ void togglePattern();
+
+protected:
+ void paintEvent(QPaintEvent * /*event*/) override;
+
+private:
+ void drawColoredSquares(QPainter &p);
+ void drawGrid(QPainter &p) const;
+
+ Pattern m_pattern = ColoredSquares;
+};
+
+bool showCaptureWindow(const QString &windowTitle);
+
+#endif