diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,9 @@ include(ECMAddAppIcon) include(ECMSetupVersion) include(FeatureSummary) +include(CheckIncludeFile) + +check_include_file("linux/input.h" HAVE_LINUX_INPUT_H) find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core DBus Widgets X11Extras) @@ -73,6 +76,12 @@ find_package(LibVNCServer REQUIRED) +find_package(PipeWire) +set_package_properties(PipeWire PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for pipewire screencast plugin" +) + ecm_setup_version(PROJECT VARIABLE_PREFIX KRFB VERSION_HEADER "krfb_version.h") diff --git a/cmake/modules/FindPipeWire.cmake b/cmake/modules/FindPipeWire.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/FindPipeWire.cmake @@ -0,0 +1,109 @@ +#.rst: +# FindPipeWire +# ------- +# +# Try to find PipeWire on a Unix system. +# +# This will define the following variables: +# +# ``PipeWire_FOUND`` +# True if (the requested version of) PipeWire is available +# ``PipeWire_VERSION`` +# The version of PipeWire +# ``PipeWire_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``PipeWire::PipeWire`` +# target +# ``PipeWire_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``PipeWire_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``PipeWire_FOUND`` is TRUE, it will also define the following imported target: +# +# ``PipeWire::PipeWire`` +# The PipeWire library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# Copyright 2018 Jan Grulich +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +# Use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +find_package(PkgConfig QUIET) +pkg_check_modules(PKG_PipeWire QUIET libpipewire-0.2 libpipewire-0.3) + +set(PipeWire_DEFINITIONS "${PKG_PipeWire_CFLAGS_OTHER}") +set(PipeWire_VERSION "${PKG_PipeWire_VERSION}") + +find_path(PipeWire_INCLUDE_DIRS + NAMES + pipewire/pipewire.h + HINTS + ${PKG_PipeWire_INCLUDE_DIRS} +) + +find_library(PipeWire_LIBRARIES + NAMES + pipewire-0.2 pipewire-0.3 + HINTS + ${PKG_PipeWire_LIBRARIES_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PipeWire + FOUND_VAR + PipeWire_FOUND + REQUIRED_VARS + PipeWire_LIBRARIES + PipeWire_INCLUDE_DIRS + VERSION_VAR + PipeWire_VERSION +) + +if(PipeWire_FOUND AND NOT TARGET PipeWire::PipeWire) + add_library(PipeWire::PipeWire UNKNOWN IMPORTED) + set_target_properties(PipeWire::PipeWire PROPERTIES + IMPORTED_LOCATION "${PipeWire_LIBRARIES}" + INTERFACE_COMPILE_OPTIONS "${PipeWire_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${PipeWire_INCLUDE_DIRS}" + ) +endif() + +mark_as_advanced(PipeWire_LIBRARIES PipeWire_INCLUDE_DIRS) + +include(FeatureSummary) +set_package_properties(PipeWire PROPERTIES + URL "http://www.pipewire.org" + DESCRIPTION "PipeWire - multimedia processing" +) diff --git a/framebuffers/CMakeLists.txt b/framebuffers/CMakeLists.txt --- a/framebuffers/CMakeLists.txt +++ b/framebuffers/CMakeLists.txt @@ -1,5 +1,9 @@ add_subdirectory (qt) if (${XCB_DAMAGE_FOUND} AND ${XCB_SHM_FOUND} AND ${XCB_IMAGE_FOUND}) -add_subdirectory (xcb) + add_subdirectory (xcb) +endif() + +if (${PipeWire_FOUND}) + add_subdirectory(pipewire) endif() diff --git a/framebuffers/pipewire/CMakeLists.txt b/framebuffers/pipewire/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/framebuffers/pipewire/CMakeLists.txt @@ -0,0 +1,39 @@ +include_directories (${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +set (krfb_framebuffer_pw_SRCS + pw_framebuffer.cpp + pw_framebufferplugin.cpp +) + + +qt5_add_dbus_interface( + krfb_framebuffer_pw_SRCS + ${CMAKE_SOURCE_DIR}/krfb/dbus/xdp_dbus_screencast_interface.xml + xdp_dbus_screencast_interface +) + +qt5_add_dbus_interface( + krfb_framebuffer_pw_SRCS + ${CMAKE_SOURCE_DIR}/krfb/dbus/xdp_dbus_remotedesktop_interface.xml + xdp_dbus_remotedesktop_interface +) + +add_library(krfb_framebuffer_pw + MODULE + ${krfb_framebuffer_pw_SRCS} +) + +target_link_libraries (krfb_framebuffer_pw + Qt5::Core + Qt5::Gui + Qt5::DBus + KF5::CoreAddons + krfbprivate + PipeWire::PipeWire +) + +install (TARGETS krfb_framebuffer_pw + DESTINATION ${PLUGIN_INSTALL_DIR}/krfb +) diff --git a/framebuffers/pipewire/krfb_framebuffer_pw.json b/framebuffers/pipewire/krfb_framebuffer_pw.json new file mode 100644 --- /dev/null +++ b/framebuffers/pipewire/krfb_framebuffer_pw.json @@ -0,0 +1,17 @@ +{ + "Encoding": "UTF-8", + "KPlugin": { + "Description": "PipeWire based Framebuffer for KRfb.", + "Description[x-test]": "xxPipeWire based Framebuffer for KRfb.xx", + "EnabledByDefault": true, + "Id": "pw", + "License": "GPL3", + "Name": "PipeWire Framebuffer for KRfb", + "Name[x-test]": "xxPipeWire Framebuffer for KRfbxx", + "ServiceTypes": [ + "krfb/framebuffer" + ], + "Version": "0.1", + "Website": "http://www.kde.org" + } +} diff --git a/framebuffers/pipewire/pw_framebuffer.h b/framebuffers/pipewire/pw_framebuffer.h new file mode 100644 --- /dev/null +++ b/framebuffers/pipewire/pw_framebuffer.h @@ -0,0 +1,59 @@ +/* This file is part of the KDE project + Copyright (C) 2018 Oleg Chernovskiy + Copyright (C) 2018 Jan Grulich + + 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 3 of the License, or (at your option) any later version. +*/ +#ifndef KRFB_FRAMEBUFFER_XCB_XCB_FRAMEBUFFER_H +#define KRFB_FRAMEBUFFER_XCB_XCB_FRAMEBUFFER_H + +#include "framebuffer.h" +#include +#include + +/** + * @brief The PWFrameBuffer class - framebuffer implementation based on XDG Desktop Portal ScreenCast interface. + * The design relies heavily on a presence of XDG D-Bus service and PipeWire daemon. + * + * @author Oleg Chernovskiy + */ +class PWFrameBuffer: public FrameBuffer +{ + Q_OBJECT +public: + typedef struct { + uint nodeId; + QVariantMap map; + } Stream; + typedef QList Streams; + + PWFrameBuffer(WId winid, QObject *parent = nullptr); + virtual ~PWFrameBuffer() override; + + int depth() override; + int height() override; + int width() override; + int paddedWidth() override; + void getServerFormat(rfbPixelFormat &format) override; + void startMonitor() override; + void stopMonitor() override; + + QVariant customProperty(const QString &property) const override; + + bool isValid() const; + +private Q_SLOTS: + void handleXdpSessionCreated(quint32 code, QVariantMap results); + void handleXdpDevicesSelected(quint32 code, QVariantMap results); + void handleXdpSourcesSelected(quint32 code, QVariantMap results); + void handleXdpRemoteDesktopStarted(quint32 code, QVariantMap results); + +private: + class Private; + const QScopedPointer d; +}; + +#endif diff --git a/framebuffers/pipewire/pw_framebuffer.cpp b/framebuffers/pipewire/pw_framebuffer.cpp new file mode 100644 --- /dev/null +++ b/framebuffers/pipewire/pw_framebuffer.cpp @@ -0,0 +1,799 @@ +/* This file is part of the KDE project + Copyright (C) 2018 Oleg Chernovskiy + Copyright (C) 2018 Jan Grulich + + 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 3 of the License, or (at your option) any later version. +*/ + +// system +#include +#include + +// Qt +#include +#include +#include +#include +#include +#include +#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) +#include +#endif + +// pipewire +#include + +#if !PW_CHECK_VERSION(0, 2, 9) +#include +#include +#include +#include +#endif +#include +#include + +#include +#include +#include +#include + +#include + +#include "pw_framebuffer.h" +#include "xdp_dbus_screencast_interface.h" +#include "xdp_dbus_remotedesktop_interface.h" + +static const uint MIN_SUPPORTED_XDP_KDE_SC_VERSION = 1; + +Q_DECLARE_METATYPE(PWFrameBuffer::Stream); +Q_DECLARE_METATYPE(PWFrameBuffer::Streams); + +const QDBusArgument &operator >> (const QDBusArgument &arg, PWFrameBuffer::Stream &stream) +{ + arg.beginStructure(); + arg >> stream.nodeId; + + arg.beginMap(); + while (!arg.atEnd()) { + QString key; + QVariant map; + arg.beginMapEntry(); + arg >> key >> map; + arg.endMapEntry(); + stream.map.insert(key, map); + } + arg.endMap(); + arg.endStructure(); + + return arg; +} + +#if !PW_CHECK_VERSION(0, 2, 9) +/** + * @brief The PwType class - helper class to contain pointers to raw C pipewire media mappings + */ +class PwType { +public: + spa_type_media_type media_type; + spa_type_media_subtype media_subtype; + spa_type_format_video format_video; + spa_type_video_format video_format; +}; +#endif + +/** + * @brief The PWFrameBuffer::Private class - private counterpart of PWFramebuffer class. This is the entity where + * whole logic resides, for more info search for "d-pointer pattern" information. + */ +class PWFrameBuffer::Private { +public: + Private(PWFrameBuffer *q); + ~Private(); + +private: + friend class PWFrameBuffer; + + static void onStateChanged(void *data, pw_remote_state old, pw_remote_state state, const char *error); + static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message); + static void onStreamFormatChanged(void *data, const struct spa_pod *format); + static void onStreamProcess(void *data); + + void initDbus(); + void initPw(); +#if !PW_CHECK_VERSION(0, 2, 9) + void initializePwTypes(); +#endif + + // dbus handling + void handleSessionCreated(quint32 &code, QVariantMap &results); + void handleDevicesSelected(quint32 &code, QVariantMap &results); + void handleSourcesSelected(quint32 &code, QVariantMap &results); + void handleRemoteDesktopStarted(quint32 &code, QVariantMap &results); + + // pw handling + void createReceivingStream(); + void handleFrame(pw_buffer *pwBuffer); + + // link to public interface + PWFrameBuffer *q; + + // pipewire stuff +#if PW_CHECK_VERSION(0, 2, 9) + struct pw_core *pwCore = nullptr; + struct pw_loop *pwLoop = nullptr; + struct pw_stream *pwStream = nullptr; + struct pw_remote *pwRemote = nullptr; + struct pw_thread_loop *pwMainLoop = nullptr; +#else + pw_core *pwCore = nullptr; + pw_loop *pwLoop = nullptr; + pw_stream *pwStream = nullptr; + pw_remote *pwRemote = nullptr; + pw_thread_loop *pwMainLoop = nullptr; + pw_type *pwCoreType = nullptr; + PwType *pwType = nullptr; +#endif + + uint pwStreamNodeId = 0; + + // event handlers + pw_remote_events pwRemoteEvents = {}; + pw_stream_events pwStreamEvents = {}; + + // wayland-like listeners + // ...of events that happen in pipewire server + spa_hook remoteListener = {}; + // ...of events that happen with the stream we consume + spa_hook streamListener = {}; + + // negotiated video format + spa_video_info_raw *videoFormat = nullptr; + + // requests a session from XDG Desktop Portal + // auto-generated and compiled from xdp_dbus_interface.xml file + QScopedPointer dbusXdpScreenCastService; + QScopedPointer dbusXdpRemoteDesktopService; + + // XDP screencast session handle + QDBusObjectPath sessionPath; + // Pipewire file descriptor + QDBusUnixFileDescriptor pipewireFd; + + // screen geometry holder + struct { + quint32 width; + quint32 height; + } screenGeometry; + + // Allowed devices + uint devices; + + // sanity indicator + bool isValid = true; +}; + +PWFrameBuffer::Private::Private(PWFrameBuffer *q) : q(q) +{ + // initialize event handlers, remote end and stream-related + pwRemoteEvents.version = PW_VERSION_REMOTE_EVENTS; + pwRemoteEvents.state_changed = &onStateChanged; + + pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; + pwStreamEvents.state_changed = &onStreamStateChanged; + pwStreamEvents.format_changed = &onStreamFormatChanged; + pwStreamEvents.process = &onStreamProcess; +} + +/** + * @brief PWFrameBuffer::Private::initDbus - initialize D-Bus connectivity with XDG Desktop Portal. + * Based on XDG_CURRENT_DESKTOP environment variable it will give us implementation that we need, + * in case of KDE it is xdg-desktop-portal-kde binary. + */ +void PWFrameBuffer::Private::initDbus() +{ + qInfo() << "Initializing D-Bus connectivity with XDG Desktop Portal"; + dbusXdpScreenCastService.reset(new OrgFreedesktopPortalScreenCastInterface(QLatin1String("org.freedesktop.portal.Desktop"), + QLatin1String("/org/freedesktop/portal/desktop"), + QDBusConnection::sessionBus())); + dbusXdpRemoteDesktopService.reset(new OrgFreedesktopPortalRemoteDesktopInterface(QLatin1String("org.freedesktop.portal.Desktop"), + QLatin1String("/org/freedesktop/portal/desktop"), + QDBusConnection::sessionBus())); + auto version = dbusXdpScreenCastService->version(); + if (version < MIN_SUPPORTED_XDP_KDE_SC_VERSION) { + qWarning() << "Unsupported XDG Portal screencast interface version:" << version; + isValid = false; + return; + } + + // create session + auto sessionParameters = QVariantMap { +#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) + { QLatin1String("session_handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) }, + { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) } +#else + { QLatin1String("session_handle_token"), QStringLiteral("krfb%1").arg(qrand()) }, + { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(qrand()) } +#endif + }; + auto sessionReply = dbusXdpRemoteDesktopService->CreateSession(sessionParameters); + sessionReply.waitForFinished(); + if (!sessionReply.isValid()) { + qWarning("Couldn't initialize XDP-KDE screencast session"); + isValid = false; + return; + } + + qInfo() << "DBus session created: " << sessionReply.value().path(); + QDBusConnection::sessionBus().connect(QString(), + sessionReply.value().path(), + QLatin1String("org.freedesktop.portal.Request"), + QLatin1String("Response"), + this->q, + SLOT(handleXdpSessionCreated(uint, QVariantMap))); +} + +void PWFrameBuffer::handleXdpSessionCreated(quint32 code, QVariantMap results) +{ + d->handleSessionCreated(code, results); +} + +/** + * @brief PWFrameBuffer::Private::handleSessionCreated - handle creation of ScreenCast session. + * XDG Portal answers with session path if it was able to successfully create the screencast. + * + * @param code return code for dbus call. Zero is success, non-zero means error + * @param results map with results of call. + */ +void PWFrameBuffer::Private::handleSessionCreated(quint32 &code, QVariantMap &results) +{ + if (code != 0) { + qWarning() << "Failed to create session: " << code; + isValid = false; + return; + } + + sessionPath = QDBusObjectPath(results.value(QLatin1String("session_handle")).toString()); + + // select sources for the session + auto selectionOptions = QVariantMap { + // We have to specify it's an uint, otherwise xdg-desktop-portal will not forward it to backend implementation + { QLatin1String("types"),QVariant::fromValue(7) }, // request all (KeyBoard, Pointer, TouchScreen) +#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) + { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) } +#else + { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(qrand()) } +#endif + }; + auto selectorReply = dbusXdpRemoteDesktopService->SelectDevices(sessionPath, selectionOptions); + selectorReply.waitForFinished(); + if (!selectorReply.isValid()) { + qWarning() << "Couldn't select devices for the remote-desktop session"; + isValid = false; + return; + } + QDBusConnection::sessionBus().connect(QString(), + selectorReply.value().path(), + QLatin1String("org.freedesktop.portal.Request"), + QLatin1String("Response"), + this->q, + SLOT(handleXdpDevicesSelected(uint, QVariantMap))); +} + +void PWFrameBuffer::handleXdpDevicesSelected(quint32 code, QVariantMap results) +{ + d->handleDevicesSelected(code, results); +} + +/** + * @brief PWFrameBuffer::Private::handleDevicesCreated - handle selection of devices we want to use for remote desktop + * + * @param code return code for dbus call. Zero is success, non-zero means error + * @param results map with results of call. + */ +void PWFrameBuffer::Private::handleDevicesSelected(quint32 &code, QVariantMap &results) +{ + if (code != 0) { + qWarning() << "Failed to select devices: " << code; + isValid = false; + return; + } + + // select sources for the session + auto selectionOptions = QVariantMap { + { QLatin1String("types"), 1 }, // only MONITOR is supported + { QLatin1String("multiple"), false }, +#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) + { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) } +#else + { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(qrand()) } +#endif + }; + auto selectorReply = dbusXdpScreenCastService->SelectSources(sessionPath, selectionOptions); + selectorReply.waitForFinished(); + if (!selectorReply.isValid()) { + qWarning() << "Couldn't select sources for the screen-casting session"; + isValid = false; + return; + } + QDBusConnection::sessionBus().connect(QString(), + selectorReply.value().path(), + QLatin1String("org.freedesktop.portal.Request"), + QLatin1String("Response"), + this->q, + SLOT(handleXdpSourcesSelected(uint, QVariantMap))); +} + +void PWFrameBuffer::handleXdpSourcesSelected(quint32 code, QVariantMap results) +{ + d->handleSourcesSelected(code, results); +} + +/** + * @brief PWFrameBuffer::Private::handleSourcesSelected - handle Screencast sources selection. + * XDG Portal shows a dialog at this point which allows you to select monitor from the list. + * This function is called after you make a selection. + * + * @param code return code for dbus call. Zero is success, non-zero means error + * @param results map with results of call. + */ +void PWFrameBuffer::Private::handleSourcesSelected(quint32 &code, QVariantMap &) +{ + if (code != 0) { + qWarning() << "Failed to select sources: " << code; + isValid = false; + return; + } + + // start session + auto startParameters = QVariantMap { +#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) + { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) } +#else + { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(qrand()) } +#endif + }; + auto startReply = dbusXdpRemoteDesktopService->Start(sessionPath, QString(), startParameters); + startReply.waitForFinished(); + QDBusConnection::sessionBus().connect(QString(), + startReply.value().path(), + QLatin1String("org.freedesktop.portal.Request"), + QLatin1String("Response"), + this->q, + SLOT(handleXdpRemoteDesktopStarted(uint, QVariantMap))); +} + + +void PWFrameBuffer::handleXdpRemoteDesktopStarted(quint32 code, QVariantMap results) +{ + d->handleRemoteDesktopStarted(code, results); +} + +/** + * @brief PWFrameBuffer::Private::handleScreencastStarted - handle Screencast start. + * At this point there shall be ready pipewire stream to consume. + * + * @param code return code for dbus call. Zero is success, non-zero means error + * @param results map with results of call. + */ +void PWFrameBuffer::Private::handleRemoteDesktopStarted(quint32 &code, QVariantMap &results) +{ + if (code != 0) { + qWarning() << "Failed to start screencast: " << code; + isValid = false; + return; + } + + // there should be only one stream + Streams streams = qdbus_cast(results.value(QLatin1String("streams"))); + if (streams.isEmpty()) { + // maybe we should check deeper with qdbus_cast but this suffices for now + qWarning() << "Failed to get screencast streams"; + isValid = false; + return; + } + + auto streamReply = dbusXdpScreenCastService->OpenPipeWireRemote(sessionPath, QVariantMap()); + streamReply.waitForFinished(); + if (!streamReply.isValid()) { + qWarning() << "Couldn't open pipewire remote for the screen-casting session"; + isValid = false; + return; + } + + pipewireFd = streamReply.value(); + if (!pipewireFd.isValid()) { + qWarning() << "Couldn't get pipewire connection file descriptor"; + isValid = false; + return; + } + + QSize streamResolution = qdbus_cast(streams.first().map.value(QLatin1String("size"))); + screenGeometry.width = streamResolution.width(); + screenGeometry.height = streamResolution.height(); + + devices = results.value(QLatin1String("types")).toUInt(); + + pwStreamNodeId = streams.first().nodeId; + + // Reallocate our buffer with actual needed size + q->fb = static_cast(malloc(screenGeometry.width * screenGeometry.height * 4)); + + if (!q->fb) { + qWarning() << "Failed to allocate buffer"; + return; + } + + Q_EMIT q->frameBufferChanged(); + + initPw(); +} + +/** + * @brief PWFrameBuffer::Private::initPw - initialize Pipewire socket connectivity. + * pipewireFd should be pointing to existing file descriptor that was passed by D-Bus at this point. + */ +void PWFrameBuffer::Private::initPw() { + qInfo() << "Initializing Pipewire connectivity"; + + // init pipewire (required) + pw_init(nullptr, nullptr); // args are not used anyways + + // initialize our source + pwLoop = pw_loop_new(nullptr); + pwMainLoop = pw_thread_loop_new(pwLoop, "pipewire-main-loop"); + + // create PipeWire core object (required) + pwCore = pw_core_new(pwLoop, nullptr); +#if !PW_CHECK_VERSION(0, 2, 9) + pwCoreType = pw_core_get_type(pwCore); + + // init type maps + initializePwTypes(); +#endif + + // pw_remote should be initialized before type maps or connection error will happen + pwRemote = pw_remote_new(pwCore, nullptr, 0); + + // init PipeWire remote, add listener to handle events + pw_remote_add_listener(pwRemote, &remoteListener, &pwRemoteEvents, this); + pw_remote_connect_fd(pwRemote, pipewireFd.fileDescriptor()); + + if (pw_thread_loop_start(pwMainLoop) < 0) { + qWarning() << "Failed to start main PipeWire loop"; + isValid = false; + } +} + +#if !PW_CHECK_VERSION(0, 2, 9) +/** + * @brief PWFrameBuffer::Private::initializePwTypes - helper method to initialize and map all needed + * Pipewire types from core to type structure. + */ +void PWFrameBuffer::Private::initializePwTypes() +{ + // raw C-like PipeWire type map + spa_type_map *map = pwCoreType->map; + pwType = new PwType(); + + spa_type_media_type_map(map, &pwType->media_type); + spa_type_media_subtype_map(map, &pwType->media_subtype); + spa_type_format_video_map(map, &pwType->format_video); + spa_type_video_format_map(map, &pwType->video_format); +} +#endif + +/** + * @brief PWFrameBuffer::Private::onStateChanged - global state tracking for pipewire connection + * @param data pointer that you have set in pw_remote_add_listener call's last argument + * @param state new state that connection has changed to + * @param error optional error message, is set to non-null if state is error + */ +void PWFrameBuffer::Private::onStateChanged(void *data, pw_remote_state /*old*/, pw_remote_state state, const char *error) +{ + qInfo() << "remote state: " << pw_remote_state_as_string(state); + + PWFrameBuffer::Private *d = static_cast(data); + + switch (state) { + case PW_REMOTE_STATE_ERROR: + qWarning() << "remote error: " << error; + break; + case PW_REMOTE_STATE_CONNECTED: + d->createReceivingStream(); + break; + default: + qInfo() << "remote state: " << pw_remote_state_as_string(state); + break; + } +} + +/** + * @brief PWFrameBuffer::Private::onStreamStateChanged - called whenever stream state changes on pipewire server + * @param data pointer that you have set in pw_stream_add_listener call's last argument + * @param state new state that stream has changed to + * @param error_message optional error message, is set to non-null if state is error + */ +void PWFrameBuffer::Private::onStreamStateChanged(void *data, pw_stream_state /*old*/, pw_stream_state state, const char *error_message) +{ + qInfo() << "Stream state changed: " << pw_stream_state_as_string(state); + + auto *d = static_cast(data); + + switch (state) { + case PW_STREAM_STATE_ERROR: + qWarning() << "pipewire stream error: " << error_message; + break; + case PW_STREAM_STATE_CONFIGURE: + pw_stream_set_active(d->pwStream, true); + break; + default: + break; + } +} + +/** + * @brief PWFrameBuffer::Private::onStreamFormatChanged - being executed after stream is set to active + * and after setup has been requested to connect to it. The actual video format is being negotiated here. + * @param data pointer that you have set in pw_stream_add_listener call's last argument + * @param format format that's being proposed + */ +#if defined(PW_API_PRE_0_2_0) +void PWFrameBuffer::Private::onStreamFormatChanged(void *data, struct spa_pod *format) +#else +void PWFrameBuffer::Private::onStreamFormatChanged(void *data, const struct spa_pod *format) +#endif // defined(PW_API_PRE_0_2_0) +{ + qInfo() << "Stream format changed"; + auto *d = static_cast(data); + + const int bpp = 4; + + if (!format) { + pw_stream_finish_format(d->pwStream, 0, nullptr, 0); + return; + } + + d->videoFormat = new spa_video_info_raw(); +#if PW_CHECK_VERSION(0, 2, 9) + spa_format_video_raw_parse(format, d->videoFormat); +#else + spa_format_video_raw_parse(format, d->videoFormat, &d->pwType->format_video); +#endif + auto width = d->videoFormat->size.width; + auto height = d->videoFormat->size.height; + auto stride = SPA_ROUND_UP_N(width * bpp, 4); + auto size = height * stride; + + uint8_t buffer[1024]; + auto builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + // setup buffers and meta header for new format + const struct spa_pod *params[2]; + +#if PW_CHECK_VERSION(0, 2, 9) + params[0] = reinterpret_cast(spa_pod_builder_add_object(&builder, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + ":", SPA_PARAM_BUFFERS_size, "i", size, + ":", SPA_PARAM_BUFFERS_stride, "i", stride, + ":", SPA_PARAM_BUFFERS_buffers, "?ri", SPA_CHOICE_RANGE(8, 1, 32), + ":", SPA_PARAM_BUFFERS_align, "i", 16)); + params[1] = reinterpret_cast(spa_pod_builder_add_object(&builder, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + ":", SPA_PARAM_META_type, "I", SPA_META_Header, + ":", SPA_PARAM_META_size, "i", sizeof(struct spa_meta_header))); +#else + params[0] = reinterpret_cast(spa_pod_builder_object(&builder, + d->pwCoreType->param.idBuffers, d->pwCoreType->param_buffers.Buffers, + ":", d->pwCoreType->param_buffers.size, "i", size, + ":", d->pwCoreType->param_buffers.stride, "i", stride, + ":", d->pwCoreType->param_buffers.buffers, "iru", 8, SPA_POD_PROP_MIN_MAX(1, 32), + ":", d->pwCoreType->param_buffers.align, "i", 16)); + params[1] = reinterpret_cast(spa_pod_builder_object(&builder, + d->pwCoreType->param.idMeta, d->pwCoreType->param_meta.Meta, + ":", d->pwCoreType->param_meta.type, "I", d->pwCoreType->meta.Header, + ":", d->pwCoreType->param_meta.size, "i", sizeof(struct spa_meta_header))); +#endif + + pw_stream_finish_format(d->pwStream, 0, params, 2); +} + +/** + * @brief PWFrameBuffer::Private::onNewBuffer - called when new buffer is available in pipewire stream + * @param data pointer that you have set in pw_stream_add_listener call's last argument + * @param id + */ +void PWFrameBuffer::Private::onStreamProcess(void *data) +{ + auto *d = static_cast(data); + + pw_buffer *buf; + if (!(buf = pw_stream_dequeue_buffer(d->pwStream))) { + return; + } + + d->handleFrame(buf); + + pw_stream_queue_buffer(d->pwStream, buf); +} + +void PWFrameBuffer::Private::handleFrame(pw_buffer *pwBuffer) +{ + auto *spaBuffer = pwBuffer->buffer; + void *src = nullptr; + + src = spaBuffer->datas[0].data; + if (!src) { + return; + } + + quint32 maxSize = spaBuffer->datas[0].maxsize; + qint32 srcStride = spaBuffer->datas[0].chunk->stride; + if (srcStride != q->paddedWidth()) { + qWarning() << "Got buffer with stride different from screen stride" << srcStride << "!=" << q->paddedWidth(); + return; + } + + q->tiles.append(QRect(0, 0, q->width(), q->height())); + std::memcpy(q->fb, src, maxSize); +} + +/** + * @brief PWFrameBuffer::Private::createReceivingStream - create a stream that will consume Pipewire buffers + * and copy the framebuffer to the existing image that we track. The state of the stream and configuration + * are later handled by the corresponding listener. + */ +void PWFrameBuffer::Private::createReceivingStream() +{ + spa_rectangle pwMinScreenBounds = SPA_RECTANGLE(1, 1); + spa_rectangle pwMaxScreenBounds = SPA_RECTANGLE(screenGeometry.width, screenGeometry.height); + + spa_fraction pwFramerateMin = SPA_FRACTION(0, 1); + spa_fraction pwFramerateMax = SPA_FRACTION(60, 1); + + auto reuseProps = pw_properties_new("pipewire.client.reuse", "1", nullptr); // null marks end of varargs + pwStream = pw_stream_new(pwRemote, "krfb-fb-consume-stream", reuseProps); + + uint8_t buffer[1024] = {}; + const spa_pod *params[1]; + auto builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + +#if PW_CHECK_VERSION(0, 2, 9) + params[0] = reinterpret_cast(spa_pod_builder_add_object(&builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + ":", SPA_FORMAT_mediaType, "I", SPA_MEDIA_TYPE_video, + ":", SPA_FORMAT_mediaSubtype, "I", SPA_MEDIA_SUBTYPE_raw, + ":", SPA_FORMAT_VIDEO_format, "I", SPA_VIDEO_FORMAT_RGBx, + ":", SPA_FORMAT_VIDEO_size, "?rR", SPA_CHOICE_RANGE(&pwMaxScreenBounds, &pwMinScreenBounds, &pwMaxScreenBounds), + ":", SPA_FORMAT_VIDEO_framerate, "F", &pwFramerateMin, + ":", SPA_FORMAT_VIDEO_maxFramerate, "?rF", SPA_CHOICE_RANGE(&pwFramerateMax, &pwFramerateMin, &pwFramerateMax))); +#else + params[0] = reinterpret_cast(spa_pod_builder_object(&builder, + pwCoreType->param.idEnumFormat, pwCoreType->spa_format, + "I", pwType->media_type.video, + "I", pwType->media_subtype.raw, + ":", pwType->format_video.format, "I", pwType->video_format.RGBx, + ":", pwType->format_video.size, "Rru", &pwMaxScreenBounds, SPA_POD_PROP_MIN_MAX(&pwMinScreenBounds, &pwMaxScreenBounds), + ":", pwType->format_video.framerate, "F", &pwFramerateMin, + ":", pwType->format_video.max_framerate, "Fru", &pwFramerateMax, 2, &pwFramerateMin, &pwFramerateMax)); +#endif + + pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); + auto flags = static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE | PW_STREAM_FLAG_MAP_BUFFERS); +#if PW_CHECK_VERSION(0, 2, 9) + if (pw_stream_connect(pwStream, PW_DIRECTION_INPUT, pwStreamNodeId, flags, params, 1) != 0) { +#else + if (pw_stream_connect(pwStream, PW_DIRECTION_INPUT, nullptr, flags, params, 1) != 0) { +#endif + qWarning() << "Could not connect receiving stream"; + isValid = false; + } +} + +PWFrameBuffer::Private::~Private() +{ + if (pwMainLoop) { + pw_thread_loop_stop(pwMainLoop); + } + +#if !PW_CHECK_VERSION(0, 2, 9) + if (pwType) { + delete pwType; + } +#endif + + if (pwStream) { + pw_stream_destroy(pwStream); + } + + if (pwRemote) { + pw_remote_destroy(pwRemote); + } + + if (pwCore) + pw_core_destroy(pwCore); + + if (pwMainLoop) { + pw_thread_loop_destroy(pwMainLoop); + } + + if (pwLoop) { + pw_loop_destroy(pwLoop); + } +} + +PWFrameBuffer::PWFrameBuffer(WId winid, QObject *parent) + : FrameBuffer (winid, parent), + d(new Private(this)) +{ + // D-Bus is most important in init chain, no toys for us if something is wrong with XDP + // PipeWire connectivity is initialized after D-Bus session is started + d->initDbus(); + + // FIXME: for now use some initial size, later on we will reallocate this with the actual size we get from portal + d->screenGeometry.width = 800; + d->screenGeometry.height = 600; + fb = nullptr; +} + +PWFrameBuffer::~PWFrameBuffer() +{ + free(fb); + fb = nullptr; +} + +int PWFrameBuffer::depth() +{ + return 32; +} + +int PWFrameBuffer::height() +{ + return static_cast(d->screenGeometry.height); +} + +int PWFrameBuffer::width() +{ + return static_cast(d->screenGeometry.width); +} + +int PWFrameBuffer::paddedWidth() +{ + return width() * 4; +} + +void PWFrameBuffer::getServerFormat(rfbPixelFormat &format) +{ + format.bitsPerPixel = 32; + format.depth = 32; + format.trueColour = true; + format.bigEndian = false; +} + +void PWFrameBuffer::startMonitor() +{ + +} + +void PWFrameBuffer::stopMonitor() +{ + +} + +QVariant PWFrameBuffer::customProperty(const QString &property) const +{ + if (property == QLatin1String("stream_node_id")) { + return QVariant::fromValue(d->pwStreamNodeId); + } if (property == QLatin1String("session_handle")) { + return QVariant::fromValue(d->sessionPath); + } + + return FrameBuffer::customProperty(property); +} + +bool PWFrameBuffer::isValid() const +{ + return d->isValid; +} diff --git a/framebuffers/pipewire/pw_framebufferplugin.h b/framebuffers/pipewire/pw_framebufferplugin.h new file mode 100644 --- /dev/null +++ b/framebuffers/pipewire/pw_framebufferplugin.h @@ -0,0 +1,45 @@ + /* This file is part of the KDE project + Copyright (C) 2018 Oleg Chernovskiy + + 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 3 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; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KRFB_FRAMEBUFFER_PW_PWFRAMEBUFFERPLUGIN_H +#define KRFB_FRAMEBUFFER_PW_PWFRAMEBUFFERPLUGIN_H + + +#include "framebufferplugin.h" +#include + + +class FrameBuffer; + +class PWFrameBufferPlugin: public FrameBufferPlugin +{ + Q_OBJECT + +public: + PWFrameBufferPlugin(QObject *parent, const QVariantList &args); + virtual ~PWFrameBufferPlugin() override; + + FrameBuffer *frameBuffer(WId id) override; + +private: + Q_DISABLE_COPY(PWFrameBufferPlugin) +}; + + +#endif // Header guard diff --git a/framebuffers/pipewire/pw_framebufferplugin.cpp b/framebuffers/pipewire/pw_framebufferplugin.cpp new file mode 100644 --- /dev/null +++ b/framebuffers/pipewire/pw_framebufferplugin.cpp @@ -0,0 +1,53 @@ +/* This file is part of the KDE project + Copyright (C) 2018 Oleg Chernovskiy + + 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 3 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; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + + +#include "pw_framebufferplugin.h" +#include "pw_framebuffer.h" +#include + + +K_PLUGIN_FACTORY_WITH_JSON(PWFrameBufferPluginFactory, "krfb_framebuffer_pw.json", + registerPlugin();) + +PWFrameBufferPlugin::PWFrameBufferPlugin(QObject *parent, const QVariantList &args) + : FrameBufferPlugin(parent, args) +{ +} + + +PWFrameBufferPlugin::~PWFrameBufferPlugin() +{ +} + + +FrameBuffer *PWFrameBufferPlugin::frameBuffer(WId id) +{ + auto pwfb = new PWFrameBuffer(id); + + // sanity check for dbus/wayland/pipewire errors + if (!pwfb->isValid()) { + delete pwfb; + return nullptr; + } + + return pwfb; +} + +#include "pw_framebufferplugin.moc" diff --git a/krfb.kdev4 b/krfb.kdev4 deleted file mode 100644 --- a/krfb.kdev4 +++ /dev/null @@ -1,3 +0,0 @@ -[Project] -Manager=KDevCMakeManager -Name=krfb diff --git a/krfb/CMakeLists.txt b/krfb/CMakeLists.txt --- a/krfb/CMakeLists.txt +++ b/krfb/CMakeLists.txt @@ -19,6 +19,7 @@ ) generate_export_header(krfbprivate BASE_NAME krfbprivate) + target_link_libraries (krfbprivate Qt5::Core Qt5::Widgets @@ -73,6 +74,18 @@ ui/mainwidget.ui ) +qt5_add_dbus_interface( + krfb_SRCS + dbus/xdp_dbus_screencast_interface.xml + dbus/xdp_dbus_screencast_interface +) + +qt5_add_dbus_interface( + krfb_SRCS + dbus/xdp_dbus_remotedesktop_interface.xml + dbus/xdp_dbus_remotedesktop_interface +) + qt5_add_resources(krfb_SRCS krfb.qrc ) @@ -87,6 +100,7 @@ ${X11_Xext_LIB} ${X11_X11_LIB} ${X11_Xdamage_LIB} + Qt5::DBus Qt5::Network KF5::Completion KF5::CoreAddons @@ -123,4 +137,4 @@ install (FILES krfb.notifyrc DESTINATION ${DATA_INSTALL_DIR}/krfb ) - + diff --git a/krfb/dbus/xdp_dbus_remotedesktop_interface.xml b/krfb/dbus/xdp_dbus_remotedesktop_interface.xml new file mode 100644 --- /dev/null +++ b/krfb/dbus/xdp_dbus_remotedesktop_interface.xml @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/krfb/dbus/xdp_dbus_screencast_interface.xml b/krfb/dbus/xdp_dbus_screencast_interface.xml new file mode 100644 --- /dev/null +++ b/krfb/dbus/xdp_dbus_screencast_interface.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/krfb/events.cpp b/krfb/events.cpp --- a/krfb/events.cpp +++ b/krfb/events.cpp @@ -23,6 +23,12 @@ */ #include "events.h" +#include "krfbconfig.h" +#include "rfbservermanager.h" + +#include "dbus/xdp_dbus_remotedesktop_interface.h" + +#include #include #include @@ -33,6 +39,8 @@ #include #include +#include + enum { LEFTSHIFT = 1, RIGHTSHIFT = 2, @@ -55,6 +63,10 @@ //mouse int buttonMask; + int x; + int y; + + QScopedPointer dbusXdpRemoteDesktopService; private: void init(); @@ -69,39 +81,44 @@ void EventData::init() { - dpy = QX11Info::display(); buttonMask = 0; - //initialize keycodes - KeySym key, *keymap; - int i, j, minkey, maxkey, syms_per_keycode; - - memset(modifiers, -1, sizeof(modifiers)); - - XDisplayKeycodes(dpy, &minkey, &maxkey); - Q_ASSERT(minkey >= 8); - Q_ASSERT(maxkey < 256); - keymap = (KeySym *) XGetKeyboardMapping(dpy, minkey, - (maxkey - minkey + 1), - &syms_per_keycode); - Q_ASSERT(keymap); - - for (i = minkey; i <= maxkey; i++) { - for (j = 0; j < syms_per_keycode; j++) { - key = keymap[(i-minkey)*syms_per_keycode+j]; - - if (key >= ' ' && key < 0x100 && i == XKeysymToKeycode(dpy, key)) { - keycodes[key] = i; - modifiers[key] = j; + if (QX11Info::isPlatformX11()) { + dpy = QX11Info::display(); + //initialize keycodes + KeySym key, *keymap; + int i, j, minkey, maxkey, syms_per_keycode; + + memset(modifiers, -1, sizeof(modifiers)); + + XDisplayKeycodes(dpy, &minkey, &maxkey); + Q_ASSERT(minkey >= 8); + Q_ASSERT(maxkey < 256); + keymap = (KeySym *) XGetKeyboardMapping(dpy, minkey, + (maxkey - minkey + 1), + &syms_per_keycode); + Q_ASSERT(keymap); + + for (i = minkey; i <= maxkey; i++) { + for (j = 0; j < syms_per_keycode; j++) { + key = keymap[(i-minkey)*syms_per_keycode+j]; + + if (key >= ' ' && key < 0x100 && i == XKeysymToKeycode(dpy, key)) { + keycodes[key] = i; + modifiers[key] = j; + } } } - } - leftShiftCode = XKeysymToKeycode(dpy, XK_Shift_L); - rightShiftCode = XKeysymToKeycode(dpy, XK_Shift_R); - altGrCode = XKeysymToKeycode(dpy, XK_Mode_switch); + leftShiftCode = XKeysymToKeycode(dpy, XK_Shift_L); + rightShiftCode = XKeysymToKeycode(dpy, XK_Shift_R); + altGrCode = XKeysymToKeycode(dpy, XK_Mode_switch); + + XFree((char *)keymap); + } - XFree((char *)keymap); + dbusXdpRemoteDesktopService.reset(new OrgFreedesktopPortalRemoteDesktopInterface(QLatin1String("org.freedesktop.portal.Desktop"), + QLatin1String("/org/freedesktop/portal/desktop"), QDBusConnection::sessionBus())); } /* this function adjusts the modifiers according to mod (as from modifiers) and data->modifierState */ @@ -146,55 +163,117 @@ #define ADJUSTMOD(sym,state) \ if(keySym==sym) { if(down) data->modifierState|=state; else data->modifierState&=~state; } - ADJUSTMOD(XK_Shift_L, LEFTSHIFT); - ADJUSTMOD(XK_Shift_R, RIGHTSHIFT); - ADJUSTMOD(XK_Mode_switch, ALTGR); + if (QX11Info::isPlatformX11()) { + ADJUSTMOD(XK_Shift_L, LEFTSHIFT); + ADJUSTMOD(XK_Shift_R, RIGHTSHIFT); + ADJUSTMOD(XK_Mode_switch, ALTGR); - if (keySym >= ' ' && keySym < 0x100) { - KeyCode k; + if (keySym >= ' ' && keySym < 0x100) { + KeyCode k; - if (down) { - tweakModifiers(data->modifiers[keySym], True); - } + if (down) { + tweakModifiers(data->modifiers[keySym], True); + } - k = data->keycodes[keySym]; + k = data->keycodes[keySym]; - if (k != NoSymbol) { - XTestFakeKeyEvent(data->dpy, k, down, CurrentTime); - } + if (k != NoSymbol) { + XTestFakeKeyEvent(data->dpy, k, down, CurrentTime); + } - if (down) { - tweakModifiers(data->modifiers[keySym], False); - } - } else { - KeyCode k = XKeysymToKeycode(data->dpy, keySym); + if (down) { + tweakModifiers(data->modifiers[keySym], False); + } + } else { + KeyCode k = XKeysymToKeycode(data->dpy, keySym); - if (k != NoSymbol) { - XTestFakeKeyEvent(data->dpy, k, down, CurrentTime); + if (k != NoSymbol) { + XTestFakeKeyEvent(data->dpy, k, down, CurrentTime); + } } } + + // Wayland platform and pipweire plugin in use + if (KrfbConfig::preferredFrameBufferPlugin() == QStringLiteral("pw")) { + + } } void EventHandler::handlePointer(int buttonMask, int x, int y) { - QDesktopWidget *desktopWidget = QApplication::desktop(); + if (QX11Info::isPlatformX11()) { + QDesktopWidget *desktopWidget = QApplication::desktop(); - int screen = desktopWidget->screenNumber(); + int screen = desktopWidget->screenNumber(); - if (screen < 0) { - screen = 0; - } + if (screen < 0) { + screen = 0; + } - XTestFakeMotionEvent(data->dpy, screen, x, y, CurrentTime); + XTestFakeMotionEvent(data->dpy, screen, x, y, CurrentTime); - for (int i = 0; i < 5; i++) { - if ((data->buttonMask&(1 << i)) != (buttonMask&(1 << i))) { - XTestFakeButtonEvent(data->dpy, - i + 1, - (buttonMask&(1 << i)) ? True : False, - CurrentTime); + for (int i = 0; i < 5; i++) { + if ((data->buttonMask&(1 << i)) != (buttonMask&(1 << i))) { + XTestFakeButtonEvent(data->dpy, + i + 1, + (buttonMask&(1 << i)) ? True : False, + CurrentTime); + } } + + data->buttonMask = buttonMask; } - data->buttonMask = buttonMask; + // Wayland platform and pipweire plugin in use + if (KrfbConfig::preferredFrameBufferPlugin() == QStringLiteral("pw")) { + QSharedPointer fb = RfbServerManager::instance()->framebuffer(); + const uint streamNodeId = fb->customProperty(QLatin1String("stream_node_id")).toUInt(); + const QDBusObjectPath sessionHandle = fb->customProperty(QLatin1String("session_handle")).value(); + + if (x != data->x || y != data->y) { + data->dbusXdpRemoteDesktopService->NotifyPointerMotionAbsolute(sessionHandle, QVariantMap(), streamNodeId, x, y); + data->x = x; + data->y = y; + } + + if (buttonMask != data->buttonMask) { + int i = 0; + QVector buttons = { BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, 0, 0, 0, 0, BTN_SIDE, BTN_EXTRA }; + for (auto it = buttons.constBegin(); it != buttons.constEnd(); ++it) { + int prevButtonState = (data->buttonMask >> i) & 0x01; + int currentButtonState = (buttonMask >> i) & 0x01; + + if (prevButtonState != currentButtonState) { + if (*it) { + data->dbusXdpRemoteDesktopService->NotifyPointerButton(sessionHandle, QVariantMap(), *it, buttonMask); + } else { + int axis = 0; + int steps = 0; + switch (i) { + case 3: + axis = 0; // Vertical + steps = -1; + break; + case 4: + axis = 0; // Vertical + steps = 1; + break; + case 5: + axis = 1; // Horizontal + steps = -1; + break; + case 6: + axis = 1; // Horizontal + steps = 1; + break; + } + + data->dbusXdpRemoteDesktopService->NotifyPointerAxisDiscrete(sessionHandle, QVariantMap(), axis, steps); + } + } + ++i; + } + data->buttonMask = buttonMask; + } + } } diff --git a/krfb/framebuffer.h b/krfb/framebuffer.h --- a/krfb/framebuffer.h +++ b/krfb/framebuffer.h @@ -17,6 +17,7 @@ #include #include #include +#include #include @@ -44,6 +45,10 @@ virtual void getServerFormat(rfbPixelFormat &format); + virtual QVariant customProperty(const QString &property) const; +Q_SIGNALS: + void frameBufferChanged(); + protected: WId win; char *fb; diff --git a/krfb/framebuffer.cpp b/krfb/framebuffer.cpp --- a/krfb/framebuffer.cpp +++ b/krfb/framebuffer.cpp @@ -50,6 +50,11 @@ { } +QVariant FrameBuffer::customProperty(const QString &property) const +{ + return QVariant(); +} + int FrameBuffer::depth() { return 32; diff --git a/krfb/main.cpp b/krfb/main.cpp --- a/krfb/main.cpp +++ b/krfb/main.cpp @@ -39,7 +39,6 @@ static const char description[] = I18N_NOOP("VNC-compatible server to share " "desktops"); - static bool checkX11Capabilities() { int bp1, bp2, majorv, minorv; @@ -57,7 +56,6 @@ return true; } - static void checkOldX11PluginConfig() { if (KrfbConfig::preferredFrameBufferPlugin() == QStringLiteral("x11")) { qDebug() << "Detected deprecated configuration: preferredFrameBufferPlugin = x11"; @@ -71,7 +69,6 @@ } } - int main(int argc, char *argv[]) { QApplication app(argc, argv); @@ -121,12 +118,14 @@ app.setQuitOnLastWindowClosed(false); - if (!checkX11Capabilities()) { - return 1; - } + if (QX11Info::isPlatformX11()) { + if (!checkX11Capabilities()) { + return 1; + } - // upgrade the configuration - checkOldX11PluginConfig(); + // upgrade the configuration + checkOldX11PluginConfig(); + } //init the core InvitationsRfbServer::init(); diff --git a/krfb/rfbserver.h b/krfb/rfbserver.h --- a/krfb/rfbserver.h +++ b/krfb/rfbserver.h @@ -43,6 +43,7 @@ virtual bool start(); virtual void stop(); + void updateFrameBuffer(char *fb, int width, int height, int depth); void updateScreen(const QList & modifiedTiles); void updateCursorPosition(const QPoint & position); diff --git a/krfb/rfbserver.cpp b/krfb/rfbserver.cpp --- a/krfb/rfbserver.cpp +++ b/krfb/rfbserver.cpp @@ -24,6 +24,7 @@ #include #include #include +#include struct RfbServer::Private { @@ -143,8 +144,11 @@ d->ipv6notifier = new QSocketNotifier(d->screen->listen6Sock, QSocketNotifier::Read, this); connect(d->ipv6notifier, &QSocketNotifier::activated, this, &RfbServer::onListenSocketActivated); } - connect(QApplication::clipboard(), &QClipboard::dataChanged, - this, &RfbServer::krfbSendServerCutText); + + if (QX11Info::isPlatformX11()) { + connect(QApplication::clipboard(), &QClipboard::dataChanged, + this, &RfbServer::krfbSendServerCutText); + } return true; } @@ -162,6 +166,17 @@ } } +void RfbServer::updateFrameBuffer(char *fb, int width, int height, int depth) +{ + int bpp = depth >> 3; + + if (bpp != 1 && bpp != 2 && bpp != 4) { + bpp = 4; + } + + rfbNewFramebuffer(d->screen, fb, width, height, 8, 3, bpp); +} + void RfbServer::updateScreen(const QList & modifiedTiles) { if (d->screen) { diff --git a/krfb/rfbservermanager.h b/krfb/rfbservermanager.h --- a/krfb/rfbservermanager.h +++ b/krfb/rfbservermanager.h @@ -21,6 +21,7 @@ #define RFBSERVERMANAGER_H #include "rfb.h" +#include "framebuffer.h" #include class RfbClient; @@ -33,12 +34,14 @@ public: static RfbServerManager *instance(); + QSharedPointer framebuffer() const; Q_SIGNALS: void clientConnected(RfbClient *cc); void clientDisconnected(RfbClient *cc); private Q_SLOTS: void init(); + void updateFrameBuffer(); void updateScreens(); void cleanup(); diff --git a/krfb/rfbservermanager.cpp b/krfb/rfbservermanager.cpp --- a/krfb/rfbservermanager.cpp +++ b/krfb/rfbservermanager.cpp @@ -90,7 +90,6 @@ return &s_instance->server; } - struct RfbServerManager::Private { QSharedPointer fb; @@ -113,6 +112,11 @@ delete d; } +QSharedPointer RfbServerManager::framebuffer() const +{ + return d->fb; +} + void RfbServerManager::init() { //qDebug(); @@ -123,10 +127,18 @@ d->desktopName = QStringLiteral("%1@%2 (shared desktop)") //FIXME check if we can use utf8 .arg(KUser().loginName(),QHostInfo::localHostName()).toLatin1(); + connect(d->fb.data(), &FrameBuffer::frameBufferChanged, this, &RfbServerManager::updateFrameBuffer); connect(&d->rfbUpdateTimer, &QTimer::timeout, this, &RfbServerManager::updateScreens); connect(qApp, &QApplication::aboutToQuit, this, &RfbServerManager::cleanup); } +void RfbServerManager::updateFrameBuffer() +{ + Q_FOREACH(RfbServer *server, d->servers) { + server->updateFrameBuffer(d->fb->data(), d->fb->width(), d->fb->height(), d->fb->depth()); + } +} + void RfbServerManager::updateScreens() { QList rects = d->fb->modifiedTiles(); @@ -194,7 +206,6 @@ screen->paddedWidthInBytes = d->fb->paddedWidth(); d->fb->getServerFormat(screen->serverFormat); screen->frameBuffer = d->fb->data(); - screen->desktopName = d->desktopName.constData(); screen->cursor = d->myCursor; }