1
0
mirror of https://invent.kde.org/network/krfb synced 2024-06-28 22:14:41 +00:00

Implement Pipewire framebuffer

This commit is contained in:
Oleg Chernovskiy 2018-05-22 22:15:12 +03:00
parent 6fc934b0ef
commit 211b046906
No known key found for this signature in database
GPG Key ID: 31CCA288E2976C11
11 changed files with 1270 additions and 1 deletions

View File

@ -71,6 +71,18 @@ set(CMAKE_MODULE_PATH
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"

View File

@ -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 <alex.merry@kde.org>
# Copyright 2014 Martin Gräßlin <mgraesslin@kde.org>
# Copyright 2018 Jan Grulich <jgrulich@redhat.com>
#
# 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"
)

109
cmake/modules/FindSPA.cmake Normal file
View File

@ -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 <alex.merry@kde.org>
# Copyright 2014 Martin Gräßlin <mgraesslin@kde.org>
# Copyright 2018 Jan Grulich <jgrulich@redhat.com>
# 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"
)

View File

@ -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()

View File

@ -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
)

View File

@ -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"
}
}

View File

@ -0,0 +1,650 @@
/* This file is part of the KDE project
Copyright (C) 2018 Oleg Chernovskiy <kanedias@xaker.ru>
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 <sys/mman.h>
#include <cstring>
// Qt
#include <QX11Info>
#include <QCoreApplication>
#include <QGuiApplication>
#include <QScreen>
#include <QSocketNotifier>
#include <QDebug>
#include <KRandom>
// pipewire
#include <pipewire/pipewire.h>
#include <pipewire/global.h>
#include <spa/param/video/format-utils.h>
#include <spa/param/format-utils.h>
#include <spa/param/props.h>
#include <spa/lib/debug.h>
#include <limits.h>
#include "pw_framebuffer.h"
#include "xdp_dbus_interface.h"
static const uint MIN_SUPPORTED_XDP_KDE_SC_VERSION = 1;
/**
* @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> 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<spa_video_info_raw> videoFormat;
// listens on pipewire socket
QScopedPointer<QSocketNotifier> socketNotifier;
// requests a session from XDG Desktop Portal
// auto-generated and compiled from xdp_dbus_interface.xml file
QScopedPointer<OrgFreedesktopPortalScreenCastInterface> 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<quint32>(screenSize.width());
screenGeometry.height = static_cast<quint32>(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<PWFrameBuffer::Private*>(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<PWFrameBuffer::Private *>(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<PWFrameBuffer::Private *>(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 *>(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 *>(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<PWFrameBuffer::Private *>(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 *>(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_flags>(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<char *>(d->fbImage.bits());
}
PWFrameBuffer::~PWFrameBuffer()
{
fb = nullptr;
}
void PWFrameBuffer::processPwEvents()
{
d->processPwEvents();
}
int PWFrameBuffer::depth()
{
return 32;
}
int PWFrameBuffer::height()
{
return static_cast<qint32>(d->screenGeometry.height);
}
int PWFrameBuffer::width()
{
return static_cast<qint32>(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;
}

View File

@ -0,0 +1,50 @@
/* This file is part of the KDE project
Copyright (C) 2018 Oleg Chernovskiy <kanedias@xaker.ru>
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 <QWidget>
#include <QVariantMap>
/**
* @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 <kanedias@xaker.ru>
*/
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<Private> d;
};
#endif

View File

@ -0,0 +1,53 @@
/* This file is part of the KDE project
Copyright (C) 2018 Oleg Chernovskiy <kanedias@xaker.ru>
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 <KPluginFactory>
K_PLUGIN_FACTORY_WITH_JSON(PWFrameBufferPluginFactory, "krfb_framebuffer_pw.json",
registerPlugin<PWFrameBufferPlugin>();)
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"

View File

@ -0,0 +1,45 @@
/* This file is part of the KDE project
Copyright (C) 2018 Oleg Chernovskiy <kanedias@xaker.ru>
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 <QWidget>
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

View File

@ -0,0 +1,187 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2017-2018 Red Hat, Inc.
This library 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 <http://www.gnu.org/licenses/>.
-->
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
<!--
org.freedesktop.portal.ScreenCast:
@short_description: Screen cast portal
-->
<interface name="org.freedesktop.portal.ScreenCast">
<!--
CreateSession:
@options: Vardict with optional further information
@handle: Object path for the #org.freedesktop.portal.Request object representing this call
Create a screen cast session. A successfully created session can at
any time be closed using org.freedesktop.portal.Session::Close, or may
at any time be closed by the portal implementation, which will be
signalled via org.freedesktop.portal.Session::Closed.
The following results get returned via the #org.freedesktop.portal.Request::Response signal:
<variablelist>
<varlistentry>
<term>session_handle o</term>
<listitem><para>
The session handle. An object path for the
#org.freedesktop.portal.Session object representing the created
session.
</para></listitem>
</varlistentry>
</variablelist>
-->
<method name="CreateSession">
<arg type="a{sv}" name="options" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<arg type="o" name="handle" direction="out"/>
</method>
<!--
SelectSources:
@session_handle: Object path for the #org.freedesktop.portal.Session object
@options: Vardict with optional further information
@handle: Object path for the #org.freedesktop.portal.Request object representing this call
Configure what the screen cast session should record. This method must
be called before starting the session.
Passing invalid input to this method will cause the session to be
closed. An application may only attempt to select sources once per
session.
Supported keys in the @options vardict include:
<variablelist>
<varlistentry>
<term>types u</term>
<listitem><para>
Bitmask of what types of content to record. Default is MONITOR.
</para></listitem>
</varlistentry>
<varlistentry>
<term>multiple b</term>
<listitem><para>
Whether to allow selecting multiple sources. Default is no.
</para></listitem>
</varlistentry>
</variablelist>
For available source types, see the AvailableSourceTypes property.
-->
<method name="SelectSources">
<arg type="o" name="session_handle" direction="in"/>
<arg type="a{sv}" name="options" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
<arg type="o" name="handle" direction="out"/>
</method>
<!--
Start:
@session_handle: Object path for the #org.freedesktop.portal.Session object
@parent_window: Identifier for the application window
@options: Vardict with optional further information
@handle: Object path for the #org.freedesktop.portal.Request object representing this call
Start the screen cast session. This will typically result the portal
presenting a dialog letting the user do the selection set up by
SelectSources. An application can only attempt start a session once.
A screen cast session may only be started after having selected sources
using org.freedesktop.portal.ScreenCast::SelectSources.
The @parent_window identifier must be of the form "x11:$XID" for an X11
window. Support for other window systems may be added in the future.
The following results get returned via the
#org.freedesktop.portal.Request::Response signal:
<variablelist>
<varlistentry>
<term>streams a(ua{sv})</term>
<listitem><para>
An array of PipeWire streams. Each stream consists of a PipeWire
node ID (the first element in the tuple, and a Vardict of
properties.
The array will contain a single stream if 'multiple' (see
SelectSources) was set to 'false', or at least one stream if
'multiple' was set to 'true' as part of the SelectSources method.
</para></listitem>
</varlistentry>
</variablelist>
Stream properties include:
<variablelist>
<varlistentry>
<term>position (ii)</term>
<listitem><para>
A tuple consisting of the position (x, y) in the compositor
coordinate space. Note that the position may not be equivalent to a
position in a pixel coordinate space. Only available for monitor
streams.
</para></listitem>
</varlistentry>
<varlistentry>
<term>size (ii)</term>
<listitem><para>
A tuple consisting of (width, height). The size represents the size
of the stream as it is displayed in the compositor coordinate
space. Note that this size may not be equivalent to a size in a
pixel coordinate space. The size may differ from the size of the
stream.
</para></listitem>
</varlistentry>
</variablelist>
-->
<method name="Start">
<arg type="o" name="session_handle" direction="in"/>
<arg type="s" name="parent_window" direction="in"/>
<arg type="a{sv}" name="options" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMap"/>
<arg type="o" name="handle" direction="out"/>
</method>
<!--
OpenPipeWireRemote:
@session_handle: Object path for the #org.freedesktop.portal.Session object
@options: Vardict with optional further information
@fd: File descriptor of an open PipeWire remote.
Open a file descriptor to the PipeWire remote where the screen cast
streams are available. The file descriptor should be used to create a
<classname>pw_remote</classname> object, by using
<function>pw_remote_connect_fd</function>. Only the screen cast stream
nodes will be available from this PipeWire node.
-->
<method name="OpenPipeWireRemote">
<annotation name="org.gtk.GDBus.C.Name" value="open_pipewire_remote"/>
<annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
<arg type="o" name="session_handle" direction="in"/>
<arg type="a{sv}" name="options" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
<arg type="h" name="fd" direction="out"/>
</method>
<!--
AvailableSourceTypes:
A bitmask of available source types. Currently defined types are:
<simplelist>
<member>1: MONITOR</member>
<member>2: WINDOW</member>
</simplelist>
-->
<property name="AvailableSourceTypes" type="u" access="read"/>
<property name="version" type="u" access="read"/>
</interface>
</node>