Source code
Revision control
Copy as Markdown
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "tst_qwindowcapturebackend_fixture.h"
#include <QtCore/qoperatingsystemversion.h>
#include <QtCore/qsystemsemaphore.h>
#include <QtCore/quuid.h>
#include <QtMultimedia/qmediaplayer.h>
#include <QtMultimediaWidgets/qvideowidget.h>
using namespace std::chrono_literals;
using QtMultimediaTest::FrameGrabber;
using QtMultimediaTest::VideoFrameInfo;
using QtMultimediaTest::surfaceCaptureTestTimeout;
std::chrono::milliseconds WindowCaptureFixture::windowGeometrySettleDelay = 0ms;
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);
connect(
&m_capture,
&QWindowCapture::activeChanged,
&m_grabber,
&FrameGrabber::onCaptureActiveChanged);
}
std::optional<VideoFrameInfo> WindowCaptureFixture::tryWaitForFirstFrameInfo()
{
return m_grabber.tryWaitForFirstFrameInfo();
}
std::optional<QVideoFrame> WindowCaptureFixture::consumeFirstVideoFrame()
{
return m_grabber.consumeFirstVideoFrame();
}
void WindowCaptureFixture::waitForWindowGeometryToSettle(FirstFrameSizePolicy policy) const
{
if (policy == FirstFrameSizePolicy::MustMatchWindowSize && windowGeometrySettleDelay > 0ms)
QTest::qWait(windowGeometrySettleDelay);
}
QString WindowCaptureFixture::getResultsPath(const QString &fileName)
{
const QString sep = QStringLiteral("--");
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(QStringLiteral("actual%1.png").arg(fileSuffix));
if (!actual.save(actualName))
qWarning() << "Failed to save actual file to " << actualName;
const QString expectedName = getResultsPath(QStringLiteral("expected%1.png").arg(fileSuffix));
if (!expected.save(expectedName))
qWarning() << "Failed to save expected file to " << expectedName;
return false;
}
bool WindowCaptureWithWidgetFixture::start(QSize size, FirstFrameSizePolicy firstFrameSizePolicy)
{
// 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, surfaceCaptureTestTimeout())) {
qWarning() << "Failed to display widget within timeout";
return false;
}
std::optional<QCapturableWindow> foundCapturableWindow = findCaptureWindow(
m_widget.windowTitle(),
m_widget.windowHandle());
if (!foundCapturableWindow || !foundCapturableWindow->isValid())
return false;
// If the test requires it, let the capture backend observe the window's
// geometry before we start the stream, so the very first frame already
// reflects the correct size.
waitForWindowGeometryToSettle(firstFrameSizePolicy);
m_capture.setWindow(*foundCapturableWindow);
m_capture.setActive(true);
m_captureWindow = *foundCapturableWindow;
return true;
}
// On macOS, capturable windows are filtered out if they contain no bundle identifier.
// For this integration test, all our spawned windows have no bundle identifier and so
// our title-based search will always fail on macOS. As an optional approach, we try
// construct a QCapturableWindow from the given QWindow.
std::optional<QCapturableWindow> WindowCaptureWithWidgetFixture::findCaptureWindow(
const QString &windowTitle,
QWindow *sourceWindow)
{
// The QWindow handle is optional. If supplied, try to construct a valid QCapturableWindow
// handle from it. Otherwise fallback to title based search.
if (sourceWindow) {
auto constructedHandle = QCapturableWindow{ sourceWindow };
if (constructedHandle.isValid())
return constructedHandle;
}
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 std::nullopt;
}
return *window;
}
void WindowCaptureWithWidgetAndRecorderFixture::start(QSize size, bool togglePattern)
{
if (togglePattern)
m_widget.setDisplayPattern(TestWidget::Pattern::Animated);
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, surfaceCaptureTestTimeout());
}
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;
},
surfaceCaptureTestTimeout());
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();
std::optional<QCapturableWindow> foundCapturableWindow = findCaptureWindow(windowTitle);
if (!foundCapturableWindow || !foundCapturableWindow->isValid())
return false;
// Start capturing the out-of-process window
m_capture.setWindow(*foundCapturableWindow);
m_capture.setActive(true);
// Show in-process widget used to create a reference image
m_widget.show();
m_captureWindow = *foundCapturableWindow;
return true;
}
#include "moc_tst_qwindowcapturebackend_fixture.cpp"