diff --git a/CMakeLists.txt b/CMakeLists.txt index f77ec8f8f..97b88e060 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -391,6 +391,10 @@ add_feature_info("SCHED_RESET_ON_FORK" HAVE_SCHED_RESET_ON_FORK "Required for running kwin_wayland with real-time scheduling") + +pkg_check_modules(PipeWire IMPORTED_TARGET libpipewire-0.3) +add_feature_info(PipeWire PipeWire_FOUND "Required for Wayland screencasting") + configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h) ########### global ############### @@ -449,6 +453,7 @@ set(kwin_SRCS decorations/decorations_logging.cpp decorations/settings.cpp deleted.cpp + dmabuftexture.cpp effectloader.cpp effects.cpp egl_context_attribute_builder.cpp @@ -709,6 +714,7 @@ set_target_properties(kwin PROPERTIES ) target_link_libraries(kwin ${kwinLibs} kwinglutils ${epoxy_LIBRARY}) + generate_export_header(kwin EXPORT_FILE_NAME kwin_export.h) if(CMAKE_SYSTEM MATCHES "FreeBSD") @@ -749,11 +755,28 @@ ecm_qt_declare_logging_category(kwin_XWAYLAND_SRCS set(kwin_WAYLAND_SRCS main_wayland.cpp + screencast/screencastmanager.cpp + screencast/pipewirecore.cpp + screencast/pipewirestream.cpp tabletmodemanager.cpp ) +ecm_qt_declare_logging_category(kwin_WAYLAND_SRCS + HEADER + kwinpipewire_logging.h + IDENTIFIER + KWIN_PIPEWIRE + CATEGORY_NAME + kwin_pipewire + DEFAULT_SEVERITY + Warning +) add_executable(kwin_wayland ${kwin_WAYLAND_SRCS} ${kwin_XWAYLAND_SRCS}) -target_link_libraries(kwin_wayland kwin KF5::Crash) +target_link_libraries(kwin_wayland + kwin + PkgConfig::PipeWire # required for PipewireStream + KF5::Crash +) if (HAVE_LIBCAP) target_link_libraries(kwin_wayland ${Libcap_LIBRARIES}) endif() diff --git a/dmabuftexture.cpp b/dmabuftexture.cpp new file mode 100644 index 000000000..88a86136e --- /dev/null +++ b/dmabuftexture.cpp @@ -0,0 +1,40 @@ +/* + * Copyright © 2020 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Aleix Pol Gonzalez + */ + +#include "dmabuftexture.h" + +#include "kwineglimagetexture.h" +#include "kwinglutils.h" + +using namespace KWin; + +DmaBufTexture::DmaBufTexture(KWin::GLTexture *texture) + : m_texture(texture) + , m_framebuffer(new KWin::GLRenderTarget(*m_texture)) +{ +} + +DmaBufTexture::~DmaBufTexture() = default; + +KWin::GLRenderTarget *DmaBufTexture::framebuffer() const +{ + return m_framebuffer.data(); +} + diff --git a/dmabuftexture.h b/dmabuftexture.h new file mode 100644 index 000000000..2175cf74c --- /dev/null +++ b/dmabuftexture.h @@ -0,0 +1,45 @@ +/* + * Copyright © 2020 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Aleix Pol Gonzalez + */ + +#pragma once +#include "kwin_export.h" +#include + +namespace KWin +{ +class GLRenderTarget; +class GLTexture; + +class KWIN_EXPORT DmaBufTexture +{ +public: + explicit DmaBufTexture(KWin::GLTexture* texture); + virtual ~DmaBufTexture(); + + virtual quint32 stride() const = 0; + virtual int fd() const = 0; + KWin::GLRenderTarget* framebuffer() const; + +protected: + QScopedPointer m_texture; + QScopedPointer m_framebuffer; +}; + +} diff --git a/libkwineffects/CMakeLists.txt b/libkwineffects/CMakeLists.txt index 2b320ad5a..5f9ac3173 100644 --- a/libkwineffects/CMakeLists.txt +++ b/libkwineffects/CMakeLists.txt @@ -88,6 +88,7 @@ set(kwin_GLUTILSLIB_SRCS kwingltexture.cpp kwinglutils.cpp kwinglutils_funcs.cpp + kwineglimagetexture.cpp logging.cpp ) diff --git a/libkwineffects/kwineglimagetexture.cpp b/libkwineffects/kwineglimagetexture.cpp new file mode 100644 index 000000000..96bd82faa --- /dev/null +++ b/libkwineffects/kwineglimagetexture.cpp @@ -0,0 +1,44 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2020 Aleix Pol Gonzalez + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ + +#include "kwineglimagetexture.h" + +using namespace KWin; + +#include +#include + +EGLImageTexture::EGLImageTexture(EGLDisplay display, EGLImage image, int internalFormat, const QSize &size) + : GLTexture(internalFormat, size, 1, true) + , m_image(image) + , m_display(display) +{ + if (m_image == EGL_NO_IMAGE_KHR) { + return; + } + + bind(); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, m_image); +} + +EGLImageTexture::~EGLImageTexture() +{ + eglDestroyImageKHR(m_display, m_image); +} diff --git a/libkwineffects/kwineglimagetexture.h b/libkwineffects/kwineglimagetexture.h new file mode 100644 index 000000000..4fd605c2c --- /dev/null +++ b/libkwineffects/kwineglimagetexture.h @@ -0,0 +1,45 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2020 Aleix Pol Gonzalez + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ + +#pragma once + +#include +#include + +typedef void *EGLImageKHR; +typedef void *EGLDisplay; +typedef void *EGLClientBuffer; + +namespace KWin +{ + +class KWINGLUTILS_EXPORT EGLImageTexture : public GLTexture +{ +public: + EGLImageTexture(EGLDisplay display, EGLImageKHR image, int internalFormat, const QSize &size); + ~EGLImageTexture() override; + +private: + EGLImageKHR m_image; + EGLDisplay m_display; +}; + +} + diff --git a/main_wayland.cpp b/main_wayland.cpp index f5ed9ffd8..81ac441a5 100644 --- a/main_wayland.cpp +++ b/main_wayland.cpp @@ -26,12 +26,14 @@ along with this program. If not, see . #include "platform.h" #include "effects.h" #include "tabletmodemanager.h" +#include "screencast/screencastmanager.h" #include "wayland_server.h" #include "xwl/xwayland.h" // KWayland #include #include + // KDE #include #include @@ -163,6 +165,7 @@ void ApplicationWayland::performStartup() VirtualKeyboard::create(this); createBackend(); TabletModeManager::create(this); + new ScreencastManager(this); } void ApplicationWayland::createBackend() diff --git a/pipewirestream.cpp b/pipewirestream.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/pipewirestream.h b/pipewirestream.h new file mode 100644 index 000000000..e69de29bb diff --git a/platform.h b/platform.h index 4e12ca774..2792eee9c 100644 --- a/platform.h +++ b/platform.h @@ -46,6 +46,7 @@ class Manager; class AbstractOutput; class Edge; class Compositor; +class DmaBufTexture; class OverlayWindow; class OpenGLBackend; class Outline; @@ -83,6 +84,10 @@ public: virtual Screens *createScreens(QObject *parent = nullptr); virtual OpenGLBackend *createOpenGLBackend(); virtual QPainterBackend *createQPainterBackend(); + virtual DmaBufTexture *createDmaBufTexture(const QSize &size) { + Q_UNUSED(size); + return nullptr; + } /** * Informs the Platform that it is about to go down and shall do appropriate cleanup. diff --git a/screencast/pipewirecore.cpp b/screencast/pipewirecore.cpp new file mode 100644 index 000000000..592aec872 --- /dev/null +++ b/screencast/pipewirecore.cpp @@ -0,0 +1,115 @@ +/* + * Copyright © 2018-2020 Red Hat, Inc + * Copyright © 2020 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + * Aleix Pol Gonzalez + */ + +#include "pipewirecore.h" +#include +#include +#include +#include "kwinpipewire_logging.h" + +PipeWireCore::PipeWireCore() +{ + pw_init(nullptr, nullptr); + pwCoreEvents.version = PW_VERSION_CORE_EVENTS; + pwCoreEvents.error = &PipeWireCore::onCoreError; +} + +PipeWireCore::~PipeWireCore() +{ + if (pwMainLoop) { + pw_loop_leave(pwMainLoop); + } + + if (pwCore) { + pw_core_disconnect(pwCore); + } + + if (pwContext) { + pw_context_destroy(pwContext); + } + + if (pwMainLoop) { + pw_loop_destroy(pwMainLoop); + } +} + +void PipeWireCore::onCoreError(void* data, uint32_t id, int seq, int res, const char* message) +{ + Q_UNUSED(seq) + + qCWarning(KWIN_PIPEWIRE) << "PipeWire remote error: " << message; + if (id == PW_ID_CORE && res == -EPIPE) { + PipeWireCore *pw = static_cast(data); + Q_EMIT pw->pipewireFailed(QString::fromUtf8(message)); + } +} + +bool PipeWireCore::init() +{ + pwMainLoop = pw_loop_new(nullptr); + pw_loop_enter(pwMainLoop); + + QSocketNotifier *notifier = new QSocketNotifier(pw_loop_get_fd(pwMainLoop), QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, [this] { + int result = pw_loop_iterate (pwMainLoop, 0); + if (result < 0) + qCWarning(KWIN_PIPEWIRE) << "pipewire_loop_iterate failed: " << result; + } + ); + + pwContext = pw_context_new(pwMainLoop, nullptr, 0); + if (!pwContext) { + qCWarning(KWIN_PIPEWIRE) << "Failed to create PipeWire context"; + m_error = i18n("Failed to create PipeWire context"); + return false; + } + + pwCore = pw_context_connect(pwContext, nullptr, 0); + if (!pwCore) { + qCWarning(KWIN_PIPEWIRE) << "Failed to connect PipeWire context"; + m_error = i18n("Failed to connect PipeWire context"); + return false; + } + + if (pw_loop_iterate(pwMainLoop, 0) < 0) { + qCWarning(KWIN_PIPEWIRE) << "Failed to start main PipeWire loop"; + m_error = i18n("Failed to start main PipeWire loop"); + return false; + } + + pw_core_add_listener(pwCore, &coreListener, &pwCoreEvents, this); + return true; +} + +QSharedPointer< PipeWireCore > PipeWireCore::self() +{ + static QWeakPointer global; + QSharedPointer ret; + if (global) { + ret = global.toStrongRef(); + } else { + ret.reset(new PipeWireCore); + ret->init(); + global = ret; + } + return ret; +} diff --git a/screencast/pipewirecore.h b/screencast/pipewirecore.h new file mode 100644 index 000000000..ccf1ab122 --- /dev/null +++ b/screencast/pipewirecore.h @@ -0,0 +1,54 @@ +/* + * Copyright © 2018-2020 Red Hat, Inc + * Copyright © 2020 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + * Aleix Pol Gonzalez + */ + +#pragma once + +#include +#include +#include +#include + +class PipeWireCore : public QObject +{ + Q_OBJECT +public: + PipeWireCore(); + + static void onCoreError(void *data, uint32_t id, int seq, int res, const char *message); + + ~PipeWireCore(); + + bool init(); + + static QSharedPointer self(); + + struct pw_core *pwCore = nullptr; + struct pw_context *pwContext = nullptr; + struct pw_loop *pwMainLoop = nullptr; + spa_hook coreListener; + QString m_error; + + pw_core_events pwCoreEvents = {}; + +Q_SIGNALS: + void pipewireFailed(const QString &message); +}; diff --git a/screencast/pipewirestream.cpp b/screencast/pipewirestream.cpp new file mode 100644 index 000000000..e7e67dc80 --- /dev/null +++ b/screencast/pipewirestream.cpp @@ -0,0 +1,476 @@ +/* + * Copyright © 2018-2020 Red Hat, Inc + * Copyright © 2020 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + * Aleix Pol Gonzalez + */ + +#include "pipewirestream.h" + +#include +#include +#include +#include +#include + +#include +#include +#include "utils.h" +#include "cursor.h" +#include "main.h" +#include "platform.h" +#include "scenes/opengl/egl_dmabuf.h" +#include "platformsupport/scenes/opengl/drm_fourcc.h" +#include "kwineglimagetexture.h" +#include "dmabuftexture.h" +#include "pipewirecore.h" +#include "kwinpipewire_logging.h" + +#include + +void PipeWireStream::onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message) +{ + PipeWireStream *pw = static_cast(data); + qCDebug(KWIN_PIPEWIRE) << "state changed"<< pw_stream_state_as_string(old) << " -> " << pw_stream_state_as_string(state) << error_message; + + switch (state) { + case PW_STREAM_STATE_ERROR: + qCWarning(KWIN_PIPEWIRE) << "Stream error: " << error_message; + break; + case PW_STREAM_STATE_PAUSED: + if (pw->nodeId() == 0 && pw->pwStream) { + pw->pwNodeId = pw_stream_get_node_id(pw->pwStream); + Q_EMIT pw->streamReady(pw->nodeId()); + } + break; + case PW_STREAM_STATE_STREAMING: + Q_EMIT pw->startStreaming(); + break; + case PW_STREAM_STATE_CONNECTING: + break; + case PW_STREAM_STATE_UNCONNECTED: + if (!pw->m_stopped) { + Q_EMIT pw->stopStreaming(); + } + break; + } +} + +#define CURSOR_BPP 4 +#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \ + sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP) + +void PipeWireStream::newStreamParams() +{ + const int bpp = videoFormat.format == SPA_VIDEO_FORMAT_RGB || videoFormat.format == SPA_VIDEO_FORMAT_BGR ? 3 : 4; + auto stride = SPA_ROUND_UP_N (m_resolution.width() * bpp, 4); + + uint8_t paramsBuffer[1024]; + spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT (paramsBuffer, sizeof (paramsBuffer)); + + spa_rectangle resolution = SPA_RECTANGLE(uint32_t(m_resolution.width()), uint32_t(m_resolution.height())); + const auto cursorSize = KWin::Cursors::self()->currentCursor()->themeSize(); + const spa_pod *params[] = { + (spa_pod*) spa_pod_builder_add_object(&pod_builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&resolution), + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(16, 2, 16), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int (1), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(stride * m_resolution.height()), + SPA_PARAM_BUFFERS_align, SPA_POD_Int(16)), + (spa_pod*) spa_pod_builder_add_object (&pod_builder, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Cursor), + SPA_PARAM_META_size, SPA_POD_Int (CURSOR_META_SIZE (cursorSize, cursorSize))) + }; + pw_stream_update_params(pwStream, params, 2); +} + +void PipeWireStream::onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format) +{ + if (!format || id != SPA_PARAM_Format) { + return; + } + + PipeWireStream *pw = static_cast(data); + spa_format_video_raw_parse (format, &pw->videoFormat); + qCDebug(KWIN_PIPEWIRE) << "Stream format changed" << pw << pw->videoFormat.format; + pw->newStreamParams(); +} + +void PipeWireStream::onStreamAddBuffer(void *data, pw_buffer *buffer) +{ + PipeWireStream *stream = static_cast(data); + struct spa_data *spa_data = buffer->buffer->datas; + + spa_data->mapoffset = 0; + spa_data->flags = SPA_DATA_FLAG_READWRITE; + + QSharedPointer dmabuf (KWin::kwinApp()->platform()->createDmaBufTexture(stream->m_resolution)); + if (dmabuf) { + spa_data->type = SPA_DATA_DmaBuf; + spa_data->fd = dmabuf->fd(); + spa_data->data = NULL; + spa_data->maxsize = dmabuf->stride() * stream->m_resolution.height(); + + stream->m_dmabufDataForPwBuffer.insert(buffer, dmabuf); + } else { + const int bytesPerPixel = stream->m_hasAlpha ? 4 : 3; + const int stride = SPA_ROUND_UP_N (stream->m_resolution.width() * bytesPerPixel, 4); + spa_data->maxsize = stride * stream->m_resolution.height(); + spa_data->type = SPA_DATA_MemFd; + spa_data->fd = memfd_create("kwin-screencast-memfd", MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (spa_data->fd == -1) { + qCCritical(KWIN_PIPEWIRE) << "memfd: Can't create memfd"; + return; + } + spa_data->mapoffset = 0; + + if (ftruncate (spa_data->fd, spa_data->maxsize) < 0) { + qCCritical(KWIN_PIPEWIRE) << "memfd: Can't truncate to" << spa_data->maxsize; + return; + } + + unsigned int seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL; + if (fcntl(spa_data->fd, F_ADD_SEALS, seals) == -1) + qCWarning(KWIN_PIPEWIRE) << "memfd: Failed to add seals"; + + spa_data->data = mmap(NULL, + spa_data->maxsize, + PROT_READ | PROT_WRITE, + MAP_SHARED, + spa_data->fd, + spa_data->mapoffset); + if (spa_data->data == MAP_FAILED) + qCCritical(KWIN_PIPEWIRE) << "memfd: Failed to mmap memory"; + else + qCDebug(KWIN_PIPEWIRE) << "memfd: created successfully" << spa_data->data << spa_data->maxsize; + } +} + +void PipeWireStream::onStreamRemoveBuffer(void *data, pw_buffer *buffer) +{ + PipeWireStream *stream = static_cast(data); + + struct spa_buffer *spa_buffer = buffer->buffer; + struct spa_data *spa_data = spa_buffer->datas; + + if (spa_data->type == SPA_DATA_DmaBuf) { + stream->m_dmabufDataForPwBuffer.remove(buffer); + } else if (spa_data->type == SPA_DATA_MemFd) { + munmap (spa_data->data, spa_data->maxsize); + close (spa_data->fd); + } +} + +PipeWireStream::PipeWireStream(bool hasAlpha, const QSize &resolution, QObject *parent) + : QObject(parent) + , m_resolution(resolution) + , m_hasAlpha(hasAlpha) +{ + pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; + pwStreamEvents.add_buffer = &PipeWireStream::onStreamAddBuffer; + pwStreamEvents.remove_buffer = &PipeWireStream::onStreamRemoveBuffer; + pwStreamEvents.state_changed = &PipeWireStream::onStreamStateChanged; + pwStreamEvents.param_changed = &PipeWireStream::onStreamParamChanged; +} + +PipeWireStream::~PipeWireStream() +{ + m_stopped = true; + if (pwStream) { + pw_stream_destroy(pwStream); + } +} + +bool PipeWireStream::init() +{ + pwCore = PipeWireCore::self(); + if (!pwCore->m_error.isEmpty()) { + m_error = pwCore->m_error; + return false; + } + + connect(pwCore.data(), &PipeWireCore::pipewireFailed, this, &PipeWireStream::coreFailed); + + if (!createStream()) { + qCWarning(KWIN_PIPEWIRE) << "Failed to create PipeWire stream"; + m_error = i18n("Failed to create PipeWire stream"); + return false; + } + + return true; +} + +uint PipeWireStream::framerate() +{ + if (pwStream) { + return videoFormat.max_framerate.num / videoFormat.max_framerate.denom; + } + + return 0; +} + +uint PipeWireStream::nodeId() +{ + return pwNodeId; +} + +bool PipeWireStream::createStream() +{ + const QByteArray objname = "kwin-screencast-" + objectName().toUtf8(); + pwStream = pw_stream_new(pwCore->pwCore, objname, nullptr); + + uint8_t buffer[1024]; + spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + spa_fraction minFramerate = SPA_FRACTION(1, 1); + spa_fraction maxFramerate = SPA_FRACTION(25, 1); + spa_fraction defaultFramerate = SPA_FRACTION(0, 1); + + spa_rectangle resolution = SPA_RECTANGLE(uint32_t(m_resolution.width()), uint32_t(m_resolution.height())); + + const auto format = m_hasAlpha || m_gbmDevice ? SPA_VIDEO_FORMAT_BGRA : SPA_VIDEO_FORMAT_BGR; + + const spa_pod *param = (spa_pod*)spa_pod_builder_add_object(&podBuilder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&resolution), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&defaultFramerate), + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&maxFramerate, &minFramerate, &maxFramerate)); + + pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); + auto flags = pw_stream_flags(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_ALLOC_BUFFERS); + + if (pw_stream_connect(pwStream, PW_DIRECTION_OUTPUT, SPA_ID_INVALID, flags, ¶m, 1) != 0) { + qCWarning(KWIN_PIPEWIRE) << "Could not connect to stream"; + pw_stream_destroy(pwStream); + return false; + } + + if (m_cursor.mode == KWaylandServer::ScreencastInterface::Embedded) { + connect(KWin::Cursors::self(), &KWin::Cursors::positionChanged, this, [this] { + if (m_cursor.lastFrameTexture) { + m_repainting = true; + recordFrame(m_cursor.lastFrameTexture.data(), QRegion{m_cursor.lastRect} | cursorGeometry(KWin::Cursors::self()->currentCursor())); + m_repainting = false; + } + }); + } + + return true; +} +void PipeWireStream::coreFailed(const QString &errorMessage) +{ + m_error = errorMessage; + Q_EMIT stopStreaming(); +} + +void PipeWireStream::stop() +{ + m_stopped = true; + delete this; +} + +static KWin::GLTexture *copyTexture(KWin::GLTexture *texture) +{ + KWin::GLTexture *copy = new KWin::GLTexture(texture->internalFormat(), texture->size()); + copy->setFilter(GL_LINEAR); + copy->setWrapMode(GL_CLAMP_TO_EDGE); + + const QRect r({}, texture->size()); + + copy->bind(); + glCopyTextureSubImage2D(copy->texture(), 0, 0, 0, 0, 0, r.width(), r.height()); + copy->unbind(); + return copy; +} + +void PipeWireStream::recordFrame(KWin::GLTexture* frameTexture, const QRegion &damagedRegion) +{ + Q_ASSERT(!m_stopped); + Q_ASSERT(frameTexture); + + if (frameTexture->size() != m_resolution) { + m_resolution = frameTexture->size(); + newStreamParams(); + return; + } + + const char *error = ""; + auto state = pw_stream_get_state(pwStream, &error); + if (state != PW_STREAM_STATE_STREAMING) { + if (error) { + qCWarning(KWIN_PIPEWIRE) << "Failed to record frame: stream is not active" << error; + } + return; + } + + struct pw_buffer *buffer = pw_stream_dequeue_buffer(pwStream); + + if (!buffer) { + return; + } + + struct spa_buffer *spa_buffer = buffer->buffer; + struct spa_data *spa_data = spa_buffer->datas; + + uint8_t *data = (uint8_t *) spa_data->data; + if (!data && spa_buffer->datas->type != SPA_DATA_DmaBuf) { + qCWarning(KWIN_PIPEWIRE) << "Failed to record frame: invalid buffer data"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + const auto size = frameTexture->size(); + spa_data->chunk->offset = 0; + if (data) { + const int bpp = data && !m_hasAlpha ? 3 : 4; + const uint stride = SPA_ROUND_UP_N (size.width() * bpp, 4); + const uint bufferSize = stride * size.height(); + + if (bufferSize > spa_data->maxsize) { + qCDebug(KWIN_PIPEWIRE) << "Failed to record frame: frame is too big"; + pw_stream_queue_buffer(pwStream, buffer); + return; + } + + spa_data->chunk->size = bufferSize; + spa_data->chunk->stride = stride; + + frameTexture->bind(); + glGetTextureImage(frameTexture->texture(), 0, m_hasAlpha ? GL_BGRA : GL_BGR, GL_UNSIGNED_BYTE, bufferSize, data); + auto cursor = KWin::Cursors::self()->currentCursor(); + if (m_cursor.mode == KWaylandServer::ScreencastInterface::Embedded && m_cursor.viewport.contains(cursor->pos())) { + QImage dest(data, size.width(), size.height(), QImage::Format_RGBA8888_Premultiplied); + QPainter painter(&dest); + const auto position = (cursor->pos() - m_cursor.viewport.topLeft() - cursor->hotspot()) * m_cursor.scale; + painter.drawImage(QRect{position, cursor->image().size()}, cursor->image()); + } + } else { + using namespace KWin; + auto &buf = m_dmabufDataForPwBuffer[buffer]; + + spa_data->chunk->stride = buf->stride(); + spa_data->chunk->size = spa_data->maxsize; + + GLRenderTarget::pushRenderTarget(buf->framebuffer()); + frameTexture->bind(); + + QRect r(QPoint(), size); + auto shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture); + + QMatrix4x4 mvp; + mvp.ortho(r); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + + QRegion dr = damagedRegion; + if (m_cursor.texture) { + dr |= m_cursor.lastRect; + } + + frameTexture->render(damagedRegion, r, true); + + auto cursor = KWin::Cursors::self()->currentCursor(); + if (m_cursor.mode == KWaylandServer::ScreencastInterface::Embedded && m_cursor.viewport.contains(cursor->pos())) { + if (!m_repainting) //We need to copy the last version of the stream to render the moved cursor on top + m_cursor.lastFrameTexture.reset(copyTexture(frameTexture)); + + if (!m_cursor.texture || m_cursor.lastKey != cursor->image().cacheKey()) + m_cursor.texture.reset(new KWin::GLTexture(cursor->image())); + + m_cursor.texture->setYInverted(false); + m_cursor.texture->bind(); + const auto cursorRect = cursorGeometry(cursor); + mvp.translate(cursorRect.left(), r.height() - cursorRect.top() - cursor->image().height() * m_cursor.scale); + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + m_cursor.texture->render(cursorRect, cursorRect, true); + glDisable(GL_BLEND); + m_cursor.texture->unbind(); + m_cursor.lastRect = cursorRect; + } + ShaderManager::instance()->popShader(); + + GLRenderTarget::popRenderTarget(); + } + frameTexture->unbind(); + + if (m_cursor.mode == KWaylandServer::ScreencastInterface::Metadata) { + sendCursorData(KWin::Cursors::self()->currentCursor(), + (spa_meta_cursor *) spa_buffer_find_meta_data (spa_buffer, SPA_META_Cursor, sizeof (spa_meta_cursor))); + } + + pw_stream_queue_buffer(pwStream, buffer); +} + +QRect PipeWireStream::cursorGeometry(KWin::Cursor *cursor) const +{ + const auto position = (cursor->pos() - m_cursor.viewport.topLeft() - cursor->hotspot()) * m_cursor.scale; + return QRect{position, m_cursor.texture->size()}; +} + +void PipeWireStream::sendCursorData(KWin::Cursor* cursor, spa_meta_cursor *spa_meta_cursor) +{ + if (!cursor || !spa_meta_cursor) { + return; + } + + const auto position = (cursor->pos() - m_cursor.viewport.topLeft()) * m_cursor.scale; + + spa_meta_cursor->id = 1; + spa_meta_cursor->position.x = position.x(); + spa_meta_cursor->position.y = position.y(); + spa_meta_cursor->hotspot.x = cursor->hotspot().x() * m_cursor.scale; + spa_meta_cursor->hotspot.y = cursor->hotspot().y() * m_cursor.scale; + spa_meta_cursor->bitmap_offset = 0; + + const QImage image = cursor->image(); + if (image.cacheKey() == m_cursor.lastKey) { + return; + } + + m_cursor.lastKey = image.cacheKey(); + spa_meta_cursor->bitmap_offset = sizeof (struct spa_meta_cursor); + + struct spa_meta_bitmap *spa_meta_bitmap = SPA_MEMBER (spa_meta_cursor, + spa_meta_cursor->bitmap_offset, + struct spa_meta_bitmap); + spa_meta_bitmap->format = SPA_VIDEO_FORMAT_RGBA; + spa_meta_bitmap->offset = sizeof (struct spa_meta_bitmap); + + uint8_t *bitmap_data = SPA_MEMBER (spa_meta_bitmap, spa_meta_bitmap->offset, uint8_t); + QImage dest(bitmap_data, image.width(), image.height(), QImage::Format_RGBA8888_Premultiplied); + spa_meta_bitmap->size.width = image.width(); + spa_meta_bitmap->size.height = image.height(); + spa_meta_bitmap->stride = dest.bytesPerLine(); + + QPainter painter(&dest); + painter.drawImage(QPoint(), image); +} + +void PipeWireStream::setCursorMode(KWaylandServer::ScreencastInterface::CursorMode mode, qreal scale, const QRect &viewport) +{ + m_cursor.mode = mode; + m_cursor.scale = scale; + m_cursor.viewport = viewport; +} diff --git a/screencast/pipewirestream.h b/screencast/pipewirestream.h new file mode 100644 index 000000000..7402f6896 --- /dev/null +++ b/screencast/pipewirestream.h @@ -0,0 +1,114 @@ +/* + * Copyright © 2018-2020 Red Hat, Inc + * Copyright © 2020 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + * Aleix Pol Gonzalez + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "kwinglobals.h" +#include "config-kwin.h" +#include +#include +#include +#include + +#undef Status + +namespace KWin +{ + class Cursor; + class GLTexture; + class DmaBufTexture; +} +class PipeWireCore; + +class KWIN_EXPORT PipeWireStream : public QObject +{ + Q_OBJECT +public: + explicit PipeWireStream(bool hasAlpha, const QSize &resolution, QObject *parent); + ~PipeWireStream(); + + bool init(); + uint framerate(); + uint nodeId(); + QString error() const { + return m_error; + } + + void stop(); + + /** Renders @p frame into the current framebuffer into the stream */ + void recordFrame(KWin::GLTexture *frame, const QRegion &damagedRegion); + + void setCursorMode(KWaylandServer::ScreencastInterface::CursorMode mode, qreal scale, const QRect &viewport); + +Q_SIGNALS: + void streamReady(quint32 nodeId); + void startStreaming(); + void stopStreaming(); + +private: + static void onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format); + static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message); + static void onStreamAddBuffer(void *data, pw_buffer *buffer); + static void onStreamRemoveBuffer(void *data, pw_buffer *buffer); + + bool createStream(); + void updateParams(); + void coreFailed(const QString &errorMessage); + void sendCursorData(KWin::Cursor* cursor, spa_meta_cursor *spa_cursor); + void newStreamParams(); + + QSharedPointer pwCore; + struct pw_stream *pwStream = nullptr; + spa_hook streamListener; + pw_stream_events pwStreamEvents = {}; + + uint32_t pwNodeId = 0; + + QSize m_resolution; + bool m_stopped = false; + + spa_video_info_raw videoFormat; + QString m_error; + const bool m_hasAlpha; + struct gbm_device *m_gbmDevice = nullptr; + + struct { + KWaylandServer::ScreencastInterface::CursorMode mode = KWaylandServer::ScreencastInterface::Hidden; + qreal scale = 1; + QRect viewport; + qint64 lastKey = 0; + QRect lastRect; + QScopedPointer texture; + QScopedPointer lastFrameTexture; + } m_cursor; + bool m_repainting = false; + QRect cursorGeometry(KWin::Cursor *cursor) const; + + QHash> m_dmabufDataForPwBuffer; +}; diff --git a/screencast/screencastmanager.cpp b/screencast/screencastmanager.cpp new file mode 100644 index 000000000..cd820ace8 --- /dev/null +++ b/screencast/screencastmanager.cpp @@ -0,0 +1,190 @@ +/* + * Copyright © 2020 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Aleix Pol Gonzalez + */ + +#include "screencastmanager.h" +#include "scene.h" +#include "workspace.h" +#include "composite.h" +#include "platform.h" +#include "abstract_wayland_output.h" +#include "plugins/scenes/opengl/scene_opengl.h" +#include "pipewirestream.h" + +#include +#include +#include +#include +#include + +using namespace KWin; + +ScreencastManager::ScreencastManager(QObject *parent) + : QObject(parent) +{ + connect(waylandServer()->screencast(), &KWaylandServer::ScreencastInterface::windowScreencastRequested, this, &ScreencastManager::streamWindow); + connect(waylandServer()->screencast(), &KWaylandServer::ScreencastInterface::outputScreencastRequested, this, &ScreencastManager::streamOutput); +} +class EGLFence : public QObject +{ +public: + EGLFence(EGLDisplay eglDisplay) + : m_eglDisplay(eglDisplay) + , m_sync(eglCreateSync(eglDisplay, EGL_SYNC_FENCE_KHR, NULL)) + { + Q_ASSERT(m_sync); + glFinish(); + } + + bool clientWaitSync() + { + glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + int ret = eglClientWaitSync(m_eglDisplay, m_sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, 0); + Q_ASSERT(ret == EGL_CONDITION_SATISFIED_KHR); + return ret == EGL_CONDITION_SATISFIED_KHR; + } + + ~EGLFence() { + auto ret = eglDestroySyncKHR(m_eglDisplay, m_sync); + Q_ASSERT(ret == EGL_TRUE); + } + +private: + const EGLDisplay m_eglDisplay; + const EGLSyncKHR m_sync; +}; + +class WindowStream : public PipeWireStream +{ +public: + WindowStream(KWin::Toplevel *toplevel, QObject *parent) + : PipeWireStream(toplevel->hasAlpha(), toplevel->clientSize() * toplevel->bufferScale(), parent) + , m_toplevel(toplevel) + { + if (AbstractClient *client = qobject_cast(toplevel)) { + setObjectName(client->desktopFileName()); + } + connect(toplevel, &Toplevel::windowClosed, this, &PipeWireStream::stopStreaming); + connect(this, &PipeWireStream::startStreaming, this, &WindowStream::startFeeding); + } + +private: + void startFeeding() { + auto scene = KWin::Compositor::self()->scene(); + connect(scene, &Scene::frameRendered, this, &WindowStream::bufferToStream); + + connect(m_toplevel, &Toplevel::damaged, this, &WindowStream::includeDamage); + m_toplevel->damaged(m_toplevel, m_toplevel->frameGeometry()); + } + + void includeDamage(KWin::Toplevel *toplevel, const QRect &damage) { + Q_ASSERT(m_toplevel == toplevel); + m_damagedRegion |= damage; + } + + void bufferToStream () { + if (m_damagedRegion.isEmpty()) { + return; + } + EGLFence fence(kwinApp()->platform()->sceneEglDisplay()); + QSharedPointer frameTexture(m_toplevel->effectWindow()->sceneWindow()->windowTexture()); + const bool wasYInverted = frameTexture->isYInverted(); + frameTexture->setYInverted(false); + + recordFrame(frameTexture.data(), m_damagedRegion); + frameTexture->setYInverted(wasYInverted); + m_damagedRegion = {}; + bool b = fence.clientWaitSync(); + Q_ASSERT(b); + } + + QRegion m_damagedRegion; + KWin::Toplevel *m_toplevel; +}; + +void ScreencastManager::streamWindow(KWaylandServer::ScreencastStreamInterface *waylandStream, const QString &winid) +{ + auto *toplevel = KWin::Workspace::self()->findToplevel(winid); + + if (!toplevel) { + waylandStream->sendFailed(i18n("Could not find window id %1", winid)); + return; + } + + auto stream = new WindowStream(toplevel, this); + integrateStreams(waylandStream, stream); +} + +void ScreencastManager::streamOutput(KWaylandServer::ScreencastStreamInterface *waylandStream, + ::wl_resource *outputResource, + KWaylandServer::ScreencastInterface::CursorMode mode) +{ + auto outputIface = KWaylandServer::OutputInterface::get(outputResource); + if (!outputIface) { + waylandStream->sendFailed(i18n("Invalid output")); + return; + } + + const auto outputs = kwinApp()->platform()->enabledOutputs(); + AbstractWaylandOutput *streamOutput = nullptr; + for (auto output : outputs) { + if (static_cast(output)->waylandOutput() == outputIface) { + streamOutput = static_cast(output); + } + } + + if (!streamOutput) { + waylandStream->sendFailed(i18n("Could not find output")); + return; + } + + auto stream = new PipeWireStream(true, streamOutput->pixelSize(), this); + stream->setObjectName(streamOutput->name()); + stream->setCursorMode(mode, streamOutput->scale(), streamOutput->geometry()); + connect(streamOutput, &QObject::destroyed, stream, &PipeWireStream::stopStreaming); + auto bufferToStream = [streamOutput, stream] (const QRegion &damagedRegion) { + auto scene = KWin::Compositor::self()->scene(); + auto texture = scene->textureForOutput(streamOutput); + + const QRect frame({}, streamOutput->modeSize()); + const QRegion region = damagedRegion.isEmpty() || streamOutput->pixelSize() != streamOutput->modeSize() ? frame : damagedRegion.translated(-streamOutput->geometry().topLeft()).intersected(frame); + stream->recordFrame(texture.data(), region); + }; + connect(stream, &PipeWireStream::startStreaming, waylandStream, [streamOutput, stream, bufferToStream] { + KWin::Compositor::self()->addRepaint(streamOutput->geometry()); + connect(streamOutput, &AbstractWaylandOutput::outputChange, stream, bufferToStream); + }); + integrateStreams(waylandStream, stream); +} + +void ScreencastManager::integrateStreams(KWaylandServer::ScreencastStreamInterface *waylandStream, PipeWireStream *stream) +{ + connect(waylandStream, &KWaylandServer::ScreencastStreamInterface::finished, stream, &PipeWireStream::stop); + connect(stream, &PipeWireStream::stopStreaming, waylandStream, [stream, waylandStream] { + waylandStream->sendClosed(); + delete stream; + }); + connect(stream, &PipeWireStream::streamReady, stream, [waylandStream] (uint nodeid) { + waylandStream->sendCreated(nodeid); + }); + if (!stream->init()) { + waylandStream->sendFailed(stream->error()); + delete stream; + } +} diff --git a/screencast/screencastmanager.h b/screencast/screencastmanager.h new file mode 100644 index 000000000..084b06ab9 --- /dev/null +++ b/screencast/screencastmanager.h @@ -0,0 +1,49 @@ +/* + * Copyright © 2020 Aleix Pol Gonzalez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Jan Grulich + * Aleix Pol Gonzalez + */ + +#pragma once + +#include "wayland_server.h" +#include + +class PipeWireStream; + +namespace KWin +{ +class AbstractEglBackend; +} + +class ScreencastManager + : public QObject +{ + Q_OBJECT +public: + ScreencastManager(QObject *parent); + + void streamWindow(KWaylandServer::ScreencastStreamInterface *stream, const QString &winid); + void streamOutput(KWaylandServer::ScreencastStreamInterface *stream, + ::wl_resource *outputResource, + KWaylandServer::ScreencastInterface::CursorMode mode); + +private: + void integrateStreams(KWaylandServer::ScreencastStreamInterface *waylandStream, PipeWireStream *pipewireStream); +}; + diff --git a/wayland_server.cpp b/wayland_server.cpp index 7f5b97757..4d5f6c9e9 100644 --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -240,7 +240,7 @@ public: return KWin::fetchRequestedInterfaces(client->executablePath()); } - const QSet interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate"}; + const QSet interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate", "zkde_screencast_unstable_v1"}; QSet m_reported; bool allowInterface(KWaylandServer::ClientConnection *client, const QByteArray &interfaceName) override { @@ -458,6 +458,8 @@ bool WaylandServer::init(const QByteArray &socketName, InitializationFlags flags m_keyState = m_display->createKeyStateInterface(m_display); m_keyState->create(); + m_screencast = m_display->createScreencastInterface(m_display); + return true; } diff --git a/wayland_server.h b/wayland_server.h index f1bdbc302..95cee5ff1 100644 --- a/wayland_server.h +++ b/wayland_server.h @@ -69,6 +69,7 @@ class LinuxDmabufUnstableV1Buffer; class TabletManagerInterface; class KeyboardShortcutsInhibitManagerV1Interface; class XdgDecorationManagerV1Interface; +class ScreencastInterface; } @@ -127,6 +128,9 @@ public: { return m_windowManagement; } + KWaylandServer::ScreencastInterface *screencast() { + return m_screencast; + } KWaylandServer::ServerSideDecorationManagerInterface *decorationManager() const { return m_decorationManager; } @@ -281,6 +285,7 @@ private: KWaylandServer::LinuxDmabufUnstableV1Interface *m_linuxDmabuf = nullptr; KWaylandServer::KeyboardShortcutsInhibitManagerV1Interface *m_keyboardShortcutsInhibitManager = nullptr; QSet m_linuxDmabufBuffers; + KWaylandServer::ScreencastInterface *m_screencast = nullptr; struct { KWaylandServer::ClientConnection *client = nullptr; QMetaObject::Connection destroyConnection;