Revision control
++ // 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"