From d0b87a900f79fb03a81b3ec578e44ee1b3e0e24f Mon Sep 17 00:00:00 2001 From: Aleix Pol Date: Tue, 21 Feb 2023 03:37:30 +0100 Subject: [PATCH] screencasting: Add an autotest Adds an autotest that makes sure a screencasting stream works as expected. Adds an optional dependency to KPipeWire only effective to run the test. --- .kde-ci.yml | 1 + CMakeLists.txt | 1 + autotests/integration/CMakeLists.txt | 4 + .../integration/generic_scene_opengl_test.h | 2 +- autotests/integration/kwin_wayland_test.h | 81 ++++++++ autotests/integration/screencasting_test.cpp | 182 ++++++++++++++++++ autotests/integration/test_helpers.cpp | 23 ++- 7 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 autotests/integration/screencasting_test.cpp diff --git a/.kde-ci.yml b/.kde-ci.yml index e834c1d404..514f9d8311 100644 --- a/.kde-ci.yml +++ b/.kde-ci.yml @@ -32,6 +32,7 @@ Dependencies: 'libraries/plasma-wayland-protocols': '@latest-kf6' 'plasma/breeze': '@same' 'plasma/kdecoration': '@same' + 'plasma/kpipewire': '@same' 'plasma/kscreenlocker': '@same' Options: diff --git a/CMakeLists.txt b/CMakeLists.txt index f00b846efc..9c50170ca8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ endif() if (BUILD_TESTING) find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS WaylandClient) + find_package(KPipeWire) if (Qt6WaylandClient_VERSION VERSION_LESS "6.4.1") # TODO Plasma 6: Drop once minimum Qt version is 6.4.1+ include(Qt6WaylandClientMacrosKde) endif() diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt index 735a5a296f..639e4b58d5 100644 --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -17,6 +17,7 @@ qt6_generate_wayland_protocol_client_sources(KWinIntegrationTestFramework ${WaylandProtocols_DATADIR}/staging/fractional-scale/fractional-scale-v1.xml ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-output-device-v2.xml ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-output-management-v2.xml + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/zkde-screencast-unstable-v1.xml ) target_sources(KWinIntegrationTestFramework PRIVATE @@ -110,6 +111,9 @@ integrationTest(WAYLAND_ONLY NAME testScreenEdges SRCS screenedges_test.cpp) integrationTest(WAYLAND_ONLY NAME testOutputChanges SRCS outputchanges_test.cpp) integrationTest(WAYLAND_ONLY NAME testTiles SRCS tiles_test.cpp) integrationTest(WAYLAND_ONLY NAME testFractionalScaling SRCS fractional_scaling_test.cpp) +if (TARGET K::KPipeWire) + integrationTest(WAYLAND_ONLY NAME testScreencasting SRCS screencasting_test.cpp LIBS K::KPipeWire) +endif() qt_add_dbus_interfaces(DBUS_SRCS ${CMAKE_BINARY_DIR}/src/org.kde.kwin.VirtualKeyboard.xml) integrationTest(WAYLAND_ONLY NAME testVirtualKeyboardDBus SRCS test_virtualkeyboard_dbus.cpp ${DBUS_SRCS}) diff --git a/autotests/integration/generic_scene_opengl_test.h b/autotests/integration/generic_scene_opengl_test.h index 85cb7e76e6..177f1eef9e 100644 --- a/autotests/integration/generic_scene_opengl_test.h +++ b/autotests/integration/generic_scene_opengl_test.h @@ -22,8 +22,8 @@ protected: private Q_SLOTS: void initTestCase(); void cleanup(); - void testRestart(); private: + void testRestart(); QByteArray m_envVariable; }; diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h index d9b917b4a7..5c464212f1 100644 --- a/autotests/integration/kwin_wayland_test.h +++ b/autotests/integration/kwin_wayland_test.h @@ -17,6 +17,7 @@ #include #include +#include #include "qwayland-fractional-scale-v1.h" #include "qwayland-idle-inhibit-unstable-v1.h" @@ -27,6 +28,7 @@ #include "qwayland-wlr-layer-shell-unstable-v1.h" #include "qwayland-xdg-decoration-unstable-v1.h" #include "qwayland-xdg-shell.h" +#include "qwayland-zkde-screencast-unstable-v1.h" namespace KWayland { @@ -57,6 +59,8 @@ class zwp_text_input_v3; class zwp_text_input_manager_v3; } +class ScreencastingV1; + namespace KWin { namespace Xwl @@ -107,6 +111,7 @@ private: namespace Test { +class ScreencastingV1; class MockInputMethod; class TextInputManagerV3 : public QtWayland::zwp_text_input_manager_v3 @@ -499,6 +504,7 @@ enum class AdditionalWaylandInterface { TextInputManagerV3 = 1 << 13, OutputDeviceV2 = 1 << 14, FractionalScaleManagerV1 = 1 << 15, + ScreencastingV1 = 1 << 16, }; Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface) @@ -590,6 +596,7 @@ WaylandOutputManagementV2 *waylandOutputManagementV2(); KWayland::Client::TextInputManager *waylandTextInputManager(); QVector waylandOutputs(); KWayland::Client::Output *waylandOutput(const QString &name); +ScreencastingV1 *screencasting(); QVector waylandOutputDevicesV2(); bool waitForWaylandSurface(Window *window); @@ -658,6 +665,8 @@ Window *waitForWaylandWindowShown(int timeout = 5000); */ Window *renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format = QImage::Format_ARGB32, int timeout = 5000); +Window *renderAndWaitForShown(KWayland::Client::Surface *surface, const QImage &img, int timeout = 5000); + /** * Waits for the @p window to be destroyed. */ @@ -692,6 +701,78 @@ XcbConnectionPtr createX11Connection(); MockInputMethod *inputMethod(); KWayland::Client::Surface *inputPanelSurface(); +class ScreencastingStreamV1 : public QObject, public QtWayland::zkde_screencast_stream_unstable_v1 +{ + Q_OBJECT + friend class ScreencastingV1; + +public: + ScreencastingStreamV1(QObject *parent) + : QObject(parent) + { + } + + ~ScreencastingStreamV1() override + { + if (isInitialized()) { + close(); + } + } + + quint32 nodeId() const + { + Q_ASSERT(m_nodeId.has_value()); + return *m_nodeId; + } + + void zkde_screencast_stream_unstable_v1_created(uint32_t node) override + { + m_nodeId = node; + Q_EMIT created(node); + } + + void zkde_screencast_stream_unstable_v1_closed() override + { + Q_EMIT closed(); + } + + void zkde_screencast_stream_unstable_v1_failed(const QString &error) override + { + Q_EMIT failed(error); + } + +Q_SIGNALS: + void created(quint32 nodeid); + void failed(const QString &error); + void closed(); + +private: + std::optional m_nodeId; +}; + +class ScreencastingV1 : public QObject, public QtWayland::zkde_screencast_unstable_v1 +{ + Q_OBJECT +public: + explicit ScreencastingV1(QObject *parent = nullptr) + : QObject(parent) + { + } + + ScreencastingStreamV1 *createOutputStream(wl_output *output, pointer mode) + { + auto stream = new ScreencastingStreamV1(this); + stream->init(stream_output(output, mode)); + return stream; + } + + ScreencastingStreamV1 *createWindowStream(const QString &uuid, pointer mode) + { + auto stream = new ScreencastingStreamV1(this); + stream->init(stream_window(uuid, mode)); + return stream; + } +}; } } diff --git a/autotests/integration/screencasting_test.cpp b/autotests/integration/screencasting_test.cpp new file mode 100644 index 0000000000..df02137783 --- /dev/null +++ b/autotests/integration/screencasting_test.cpp @@ -0,0 +1,182 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "composite.h" +#include "core/output.h" +#include "core/outputbackend.h" +#include "generic_scene_opengl_test.h" +#include "pointer_input.h" +#include "scene/workspacescene.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(PipeWireFrame); + +#define QCOMPAREIMG(actual, expected, id) \ + { \ + if ((actual) != (expected)) { \ + const auto actualFile = QStringLiteral("appium_artifact_actual_%1.png").arg(id); \ + const auto expectedFile = QStringLiteral("appium_artifact_expected_%1.png").arg(id); \ + (actual).save(actualFile); \ + (expected).save(expectedFile); \ + qDebug() << "Generated failed file" << actualFile << expectedFile; \ + } \ + QCOMPARE(actual, expected); \ + } + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_buffer_size_change-0"); + +class ScreencastingTest : public GenericSceneOpenGLTest +{ + Q_OBJECT +public: + ScreencastingTest() + : GenericSceneOpenGLTest(QByteArrayLiteral("O2")) + { + qRegisterMetaType(); + + auto wrap = [this](const QString &process, const QStringList &arguments = {}) { + // Make sure PipeWire is running. If it's already running it will just exit + QProcess *p = new QProcess(this); + p->setProcessChannelMode(QProcess::MergedChannels); + p->setArguments(arguments); + connect(this, &QObject::destroyed, p, [p] { + p->terminate(); + p->waitForFinished(); + p->kill(); + }); + connect(p, &QProcess::errorOccurred, p, [p](auto status) { + qDebug() << "error" << status << p->program(); + }); + connect(p, &QProcess::finished, p, [p](int code, auto status) { + if (code != 0) { + qDebug() << p->readAll(); + } + qDebug() << "finished" << code << status << p->program(); + }); + p->setProgram(process); + p->start(); + }; + + // If I run this outside the CI, it breaks the system's pipewire + if (qgetenv("KDECI_BUILD") == "TRUE") { + wrap("pipewire"); + wrap("dbus-launch", {"wireplumber"}); + } + } +private Q_SLOTS: + void init(); + void testWindowCasting(); + void testOutputCasting(); + +private: + std::optional oneFrameAndClose(Test::ScreencastingStreamV1 *stream); +}; + +void ScreencastingTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::ScreencastingV1)); + QVERIFY(KWin::Test::screencasting()); + Cursors::self()->hideCursor(); +} + +std::optional ScreencastingTest::oneFrameAndClose(Test::ScreencastingStreamV1 *stream) +{ + Q_ASSERT(stream); + PipeWireSourceStream pwStream; + qDebug() << "start" << stream; + connect(stream, &Test::ScreencastingStreamV1::failed, qGuiApp, [](const QString &error) { + qDebug() << "stream failed with error" << error; + Q_ASSERT(false); + }); + connect(stream, &Test::ScreencastingStreamV1::closed, qGuiApp, [&pwStream] { + pwStream.setActive(false); + }); + connect(stream, &Test::ScreencastingStreamV1::created, qGuiApp, [&pwStream](quint32 nodeId) { + pwStream.createStream(nodeId, 0); + }); + + std::optional img; + connect(&pwStream, &PipeWireSourceStream::frameReceived, qGuiApp, [&img](const PipeWireFrame &frame) { + img = frame.image; + }); + + QSignalSpy spy(&pwStream, &PipeWireSourceStream::frameReceived); + if (!spy.wait()) { + qDebug() << "Did not receive any frames"; + } + pwStream.stopStreaming(); + return img; +} + +void ScreencastingTest::testWindowCasting() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + QImage sourceImage(QSize(30, 10), QImage::Format_RGBA8888_Premultiplied); + sourceImage.fill(Qt::red); + + Window *window = Test::renderAndWaitForShown(surface.get(), sourceImage); + QVERIFY(window); + + auto stream = KWin::Test::screencasting()->createWindowStream(window->internalId().toString(), QtWayland::zkde_screencast_unstable_v1::pointer_hidden); + + std::optional img = oneFrameAndClose(stream); + QVERIFY(img); + img->convertTo(sourceImage.format()); + QCOMPAREIMG(*img, sourceImage, QLatin1String("window_cast")); +} + +void ScreencastingTest::testOutputCasting() +{ + auto theOutput = KWin::Test::waylandOutputs().constFirst(); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + QImage sourceImage(theOutput->pixelSize(), QImage::Format_RGBA8888_Premultiplied); + sourceImage.fill(Qt::green); + { + QPainter p(&sourceImage); + p.drawRect(100, 100, 100, 100); + } + + Window *window = Test::renderAndWaitForShown(surface.get(), sourceImage); + QVERIFY(window); + QCOMPARE(window->frameGeometry(), window->output()->geometry()); + + auto stream = KWin::Test::screencasting()->createOutputStream(theOutput->output(), QtWayland::zkde_screencast_unstable_v1::pointer_hidden); + + std::optional img = oneFrameAndClose(stream); + QVERIFY(img); + img->convertTo(sourceImage.format()); + QCOMPAREIMG(*img, sourceImage, QLatin1String("output_cast")); +} + +} + +WAYLANDTEST_MAIN(KWin::ScreencastingTest) +#include "screencasting_test.moc" diff --git a/autotests/integration/test_helpers.cpp b/autotests/integration/test_helpers.cpp index 89417a7002..f4cb9a801b 100644 --- a/autotests/integration/test_helpers.cpp +++ b/autotests/integration/test_helpers.cpp @@ -17,6 +17,7 @@ #include "wayland/display.h" #include "wayland_server.h" #include "workspace.h" +#include #include #include @@ -254,6 +255,7 @@ static struct LayerShellV1 *layerShellV1 = nullptr; TextInputManagerV3 *textInputManagerV3 = nullptr; FractionalScaleManagerV1 *fractionalScaleManagerV1 = nullptr; + ScreencastingV1 *screencastingV1 = nullptr; } s_waylandConnection; MockInputMethod *inputMethod() @@ -425,6 +427,13 @@ bool setupWaylandConnection(AdditionalWaylandInterfaces flags) return; } } + if (flags & AdditionalWaylandInterface::ScreencastingV1) { + if (interface == zkde_screencast_unstable_v1_interface.name) { + s_waylandConnection.screencastingV1 = new ScreencastingV1(); + s_waylandConnection.screencastingV1->init(*registry, name, version); + return; + } + } }); QSignalSpy allAnnounced(registry, &KWayland::Client::Registry::interfacesAnnounced); @@ -652,6 +661,11 @@ KWayland::Client::Output *waylandOutput(const QString &name) return nullptr; } +ScreencastingV1 *screencasting() +{ + return s_waylandConnection.screencastingV1; +} + QVector waylandOutputDevicesV2() { return s_waylandConnection.outputDevicesV2; @@ -729,12 +743,19 @@ Window *waitForWaylandWindowShown(int timeout) } Window *renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format, int timeout) +{ + QImage img(size, format); + img.fill(color); + return renderAndWaitForShown(surface, img, timeout); +} + +Window *renderAndWaitForShown(KWayland::Client::Surface *surface, const QImage &img, int timeout) { QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); if (!windowAddedSpy.isValid()) { return nullptr; } - render(surface, size, color, format); + render(surface, img); flushWaylandConnection(); if (!windowAddedSpy.wait(timeout)) { return nullptr;