diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,18 @@ find_package(LibVNCServer REQUIRED) +find_package(PipeWire) +set_package_properties(PipeWire PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for pipewire screencast plugin" +) + +find_package(SPA) +set_package_properties(SPA PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for pipewire screencast plugin" +) + include_directories ("${CMAKE_CURRENT_BINARY_DIR}/krfb" "${CMAKE_CURRENT_SOURCE_DIR}/krfb" 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.1) + +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.1 + 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/cmake/modules/FindSPA.cmake b/cmake/modules/FindSPA.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/FindSPA.cmake @@ -0,0 +1,109 @@ +#.rst: +# FindSPA +# ------- +# +# Try to find the Simple Plugin API (SPA) on a Unix system. +# +# This will define the following variables: +# +# ``SPA_FOUND`` +# True if (the requested version of) SPA is available +# ``SPA_VERSION`` +# The version of SPA +# ``SPA_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``SPA::SPA`` +# target +# ``SPA_INCLUDE_DIRSS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``SPA_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``SPA_FOUND`` is TRUE, it will also define the following imported target: +# +# ``SPA::SPA`` +# The SPA 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_SPA QUIET libspa-0.1) + +set(SPA_DEFINITIONS "${PKG_SPA_CFLAGS_OTHER}") +set(SPA_VERSION "${PKG_SPA_VERSION}") + +find_path(SPA_INCLUDE_DIRS + NAMES + spa/pod/pod.h + HINTS + ${PKG_SPA_INCLUDE_DIRS} +) + +find_library(SPA_LIBRARIES + NAMES + spa-lib + HINTS + ${PKG_SPA_LIBRARIES_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SPA + FOUND_VAR + SPA_FOUND + REQUIRED_VARS + SPA_LIBRARIES + SPA_INCLUDE_DIRS + VERSION_VAR + SPA_VERSION +) + +if(SPA_FOUND AND NOT TARGET SPA::SPA) + add_library(SPA::SPA UNKNOWN IMPORTED) + set_target_properties(SPA::SPA PROPERTIES + IMPORTED_LOCATION "${SPA_LIBRARIES}" + INTERFACE_COMPILE_OPTIONS "${SPA_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${SPA_INCLUDE_DIRS}" + ) +endif() + +mark_as_advanced(SPA_LIBRARIES SPA_INCLUDE_DIRS) + +include(FeatureSummary) +set_package_properties(SPA PROPERTIES + DESCRIPTION "Simple Plugin API" +) 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} AND ${SPA_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,33 @@ +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 + xdp_dbus_interface.xml + xdp_dbus_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 + SPA::SPA +) + +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,50 @@ +/* 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. +*/ +#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: + PWFrameBuffer(WId winid, QObject *parent = nullptr); + virtual ~PWFrameBuffer() override; + + int depth() override; + int height() override; + int width() override; + void getServerFormat(rfbPixelFormat &format) override; + void startMonitor() override; + void stopMonitor() override; + + bool isValid() const; + +private slots: + void handleXdpSessionCreated(quint32 code, QVariantMap results); + void handleXdpSourcesSelected(quint32 code, QVariantMap results); + void handleXdpScreenCastStarted(quint32 code, QVariantMap results); + +private: + void processPwEvents(); + + 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,651 @@ +/* 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. +*/ + +// system +#include +#include +// Qt +#include +#include +#include +#include +#include +#include +#include +// pipewire +#include +#include +#include +#include +#include +#include +#include + +#include "pw_framebuffer.h" +#include "xdp_dbus_interface.h" + +static const uint MIN_SUPPORTED_XDP_KDE_SC_VERSION = 1; +static const QDBusObjectPath KRFB_TOKEN(QLatin1String("/krfb-pw-plugin")); + +/** + * @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; +}; + +/** + * @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, struct spa_pod *format); + static void onNewBuffer(void *data, uint32_t id); + + void initWayland(); + void initDbus(); + void initPw(); + void initializePwTypes(); + + // dbus handling + void handleSessionCreated(quint32 &code, QVariantMap &results); + void handleSourcesSelected(quint32 &code, QVariantMap &results); + void handleScreencastStarted(quint32 &code, QVariantMap &results); + + // pw handling + void processPwEvents(); + void createReceivingStream(); + void handleFrame(spa_buffer *buf); + + // link to public interface + PWFrameBuffer *q; + + // pipewire stuff + pw_core *pwCore = nullptr; + pw_type *pwCoreType = nullptr; + pw_remote *pwRemote = nullptr; + pw_stream *pwStream = nullptr; + pw_loop *pwLoop = nullptr; + QScopedPointer pwType; + + // 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 + QScopedPointer videoFormat; + + // listens on pipewire socket + QScopedPointer socketNotifier; + + // requests a session from XDG Desktop Portal + // auto-generated and compiled from xdp_dbus_interface.xml file + QScopedPointer dbusXdpService; + + // XDP screencast session handle + QDBusObjectPath sessionPath; + // Pipewire file descriptor + QDBusUnixFileDescriptor pipewireFd; + + // counters for dbus exchange + quint32 requestCounter = 0; + quint32 sessionCounter = 0; + + // screen geometry holder + struct { + quint32 width; + quint32 height; + } screenGeometry; + + // real image with allocated memory which poses as a destination when we get a buffer from pipewire + // and as source when we pass the frame back to protocol + QImage fbImage; + + // 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.new_buffer = &onNewBuffer; +} + +/** + * @brief PWFrameBuffer::Private::initWayland - initializes screen info and Wayland connectivity. + * For now just grabs first available screen and uses its dimensions for framebuffer. + */ +void PWFrameBuffer::Private::initWayland() +{ + qInfo() << "Initializing screen info"; + auto screen = qApp->screens().at(0); + auto screenSize = screen->geometry(); + screenGeometry.width = static_cast(screenSize.width()); + screenGeometry.height = static_cast(screenSize.height()); + fbImage = QImage(screenSize.width(), screenSize.height(), QImage::Format_RGBX8888); +} + +/** + * @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"; + dbusXdpService.reset(new OrgFreedesktopPortalScreenCastInterface(QLatin1String("org.freedesktop.portal.Desktop"), + QLatin1String("/org/freedesktop/portal/desktop"), + QDBusConnection::sessionBus())); + if (!dbusXdpService->isValid()) { + qWarning("Can't find XDG Portal screencast interface"); + isValid = false; + return; + } + + auto version = dbusXdpService->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 { + { QLatin1String("session_handle_token"), QString::number(sessionCounter++) }, + { QLatin1String("handle_token"), QString::number(requestCounter++) } + }; + auto sessionReply = dbusXdpService->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 { + { QLatin1String("types"), 1u }, // only MONITOR is supported + { QLatin1String("multiple"), false }, + { QLatin1String("handle_token"), QString::number(requestCounter++) } + }; + auto selectorReply = dbusXdpService->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 { + { QLatin1String("handle_token"), QString::number(requestCounter++) } + }; + auto startReply = dbusXdpService->Start(sessionPath, QString(), startParameters); + startReply.waitForFinished(); + QDBusConnection::sessionBus().connect(QString(), + startReply.value().path(), + QLatin1String("org.freedesktop.portal.Request"), + QLatin1String("Response"), + this->q, + SLOT(handleXdpScreenCastStarted(uint, QVariantMap))); +} + + +void PWFrameBuffer::handleXdpScreenCastStarted(quint32 code, QVariantMap results) +{ + d->handleScreencastStarted(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::handleScreencastStarted(quint32 &code, QVariantMap &results) +{ + if (code != 0) { + qWarning() << "Failed to start screencast: " << code; + isValid = false; + return; + } + + // there should be only one stream + auto streams = results.value("streams"); + if (streams.isNull()) { + // maybe we should check deeper with qdbus_cast but this suffices for now + qWarning() << "Failed to get screencast streams"; + isValid = false; + return; + } + + auto streamReply = dbusXdpService->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; + } + 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); + socketNotifier.reset(new QSocketNotifier(pw_loop_get_fd(pwLoop), QSocketNotifier::Read)); + QObject::connect(socketNotifier.data(), &QSocketNotifier::activated, this->q, &PWFrameBuffer::processPwEvents); + + // create PipeWire core object (required) + pwCore = pw_core_new(pwLoop, nullptr); + pwCoreType = pw_core_get_type(pwCore); + + // pw_remote should be initialized before type maps or connection error will happen + pwRemote = pw_remote_new(pwCore, nullptr, 0); + + // init type maps + initializePwTypes(); + + // init PipeWire remote, add listener to handle events + pw_remote_add_listener(pwRemote, &remoteListener, &pwRemoteEvents, this); + pw_remote_connect_fd(pwRemote, pipewireFd.fileDescriptor()); +} + +/** + * @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 + auto map = pwCoreType->map; + + pwType.reset(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); + + // must be called after type system is mapped + // calling it before causes unpredictable memory corruption and errors + spa_debug_set_type_map(pwCoreType->map); +} + +/** + * @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 + */ +void PWFrameBuffer::Private::onStreamFormatChanged(void *data, struct spa_pod *format) +{ + 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.reset(new spa_video_info_raw); + spa_format_video_raw_parse(format, d->videoFormat.data(), &d->pwType->format_video); + + 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 {buffer, sizeof(buffer)}; + + // setup buffers and meta header for new format + struct spa_pod *params[2]; + 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))); + + 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::onNewBuffer(void *data, uint32_t id) +{ + qDebug() << "New buffer received" << id; + auto *d = static_cast(data); + + auto buf = pw_stream_peek_buffer(d->pwStream, id); + d->handleFrame(buf); + + pw_stream_recycle_buffer(d->pwStream, id); +} + +void PWFrameBuffer::Private::handleFrame(spa_buffer *buf) +{ + auto mapLength = buf->datas[0].maxsize + buf->datas[0].mapoffset; + + void *mapped; // full length of mapped data + void *src; // real pixel data in this buffer + if (buf->datas[0].type == pwCoreType->data.MemFd || buf->datas[0].type == pwCoreType->data.DmaBuf) { + mapped = mmap(nullptr, mapLength, PROT_READ, MAP_PRIVATE, buf->datas[0].fd, 0); + src = SPA_MEMBER(mapped, buf->datas[0].mapoffset, void); + } else if (buf->datas[0].type == pwCoreType->data.MemPtr) { + mapped = nullptr; + src = buf->datas[0].data; + } else { + qWarning() << "Got unsupported buffer type" << buf->datas[0].type; + return; + } + + qint32 srcStride = buf->datas[0].chunk->stride; + if (srcStride != q->paddedWidth()) { + qWarning() << "Got buffer with stride different from screen stride" << srcStride << "!=" << q->paddedWidth(); + return; + } + + fbImage.bits(); + q->tiles.append(fbImage.rect()); + std::memcpy(fbImage.bits(), src, buf->datas[0].maxsize); + + if (mapped) + munmap(mapped, mapLength); +} + +/** + * @brief PWFrameBuffer::Private::processPwEvents - called when Pipewire socket notifies there's + * data to process by remote interface. + */ +void PWFrameBuffer::Private::processPwEvents() { + qDebug() << "Iterating over pipewire loop..."; + + int result = pw_loop_iterate(pwLoop, 0); + if (result < 0) { + qWarning() << "Failed to iterate over pipewire loop: " << spa_strerror(result); + } +} + +/** + * @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() +{ + auto pwScreenBounds = spa_rectangle {screenGeometry.width, screenGeometry.height}; + + auto pwFramerate = spa_fraction {25, 1}; + auto pwFramerateMin = spa_fraction {0, 1}; + auto 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{buffer, sizeof(buffer)}; + 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, "R", &pwScreenBounds, + ":", pwType->format_video.framerate, "F", &pwFramerateMin, + ":", pwType->format_video.max_framerate, "Fr", &pwFramerate, 2, &pwFramerateMin, &pwFramerateMax)); + spa_debug_pod(params[0], SPA_DEBUG_FLAG_FORMAT); + + pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); + auto flags = static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE); + if (pw_stream_connect(pwStream, PW_DIRECTION_INPUT, nullptr, flags, params, 1) != 0) { + qWarning() << "Could not connect receiving stream"; + isValid = false; + } +} + +PWFrameBuffer::Private::~Private() +{ + if (pwStream) { + pw_stream_disconnect(pwStream); + pw_stream_destroy(pwStream); + } + if (pwRemote) { + pw_remote_disconnect(pwRemote); + } + + if (pwCore) + pw_core_destroy(pwCore); + + if (pwLoop) { + pw_loop_leave(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(); + + // connect to Wayland and PipeWire sockets + d->initWayland(); + + // framebuffer from public interface will point directly to image data + fb = reinterpret_cast(d->fbImage.bits()); +} + +PWFrameBuffer::~PWFrameBuffer() +{ + fb = nullptr; +} + +void PWFrameBuffer::processPwEvents() +{ + d->processPwEvents(); +} + +int PWFrameBuffer::depth() +{ + return 32; +} + +int PWFrameBuffer::height() +{ + return static_cast(d->screenGeometry.height); +} + +int PWFrameBuffer::width() +{ + return static_cast(d->screenGeometry.width); +} + +void PWFrameBuffer::getServerFormat(rfbPixelFormat &format) +{ + format.bitsPerPixel = 32; + format.depth = 32; + format.trueColour = true; + format.bigEndian = false; +} + +void PWFrameBuffer::startMonitor() +{ + +} + +void PWFrameBuffer::stopMonitor() +{ + +} + +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/framebuffers/pipewire/xdp_dbus_interface.xml b/framebuffers/pipewire/xdp_dbus_interface.xml new file mode 100644 --- /dev/null +++ b/framebuffers/pipewire/xdp_dbus_interface.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +