screencasting: integrate zkde_screencast_unstable_v1

Includes a PipeWire implementation that will send the relevant streams
to the processes that need them.
This commit is contained in:
Aleix Pol 2020-07-22 19:29:23 +02:00
parent 27ea1b9527
commit 38eb72efe3
18 changed files with 1213 additions and 2 deletions

View file

@ -391,6 +391,10 @@ add_feature_info("SCHED_RESET_ON_FORK"
HAVE_SCHED_RESET_ON_FORK
"Required for running kwin_wayland with real-time scheduling")
pkg_check_modules(PipeWire IMPORTED_TARGET libpipewire-0.3)
add_feature_info(PipeWire PipeWire_FOUND "Required for Wayland screencasting")
configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h)
########### global ###############
@ -449,6 +453,7 @@ set(kwin_SRCS
decorations/decorations_logging.cpp
decorations/settings.cpp
deleted.cpp
dmabuftexture.cpp
effectloader.cpp
effects.cpp
egl_context_attribute_builder.cpp
@ -709,6 +714,7 @@ set_target_properties(kwin PROPERTIES
)
target_link_libraries(kwin ${kwinLibs} kwinglutils ${epoxy_LIBRARY})
generate_export_header(kwin EXPORT_FILE_NAME kwin_export.h)
if(CMAKE_SYSTEM MATCHES "FreeBSD")
@ -749,11 +755,28 @@ ecm_qt_declare_logging_category(kwin_XWAYLAND_SRCS
set(kwin_WAYLAND_SRCS
main_wayland.cpp
screencast/screencastmanager.cpp
screencast/pipewirecore.cpp
screencast/pipewirestream.cpp
tabletmodemanager.cpp
)
ecm_qt_declare_logging_category(kwin_WAYLAND_SRCS
HEADER
kwinpipewire_logging.h
IDENTIFIER
KWIN_PIPEWIRE
CATEGORY_NAME
kwin_pipewire
DEFAULT_SEVERITY
Warning
)
add_executable(kwin_wayland ${kwin_WAYLAND_SRCS} ${kwin_XWAYLAND_SRCS})
target_link_libraries(kwin_wayland kwin KF5::Crash)
target_link_libraries(kwin_wayland
kwin
PkgConfig::PipeWire # required for PipewireStream
KF5::Crash
)
if (HAVE_LIBCAP)
target_link_libraries(kwin_wayland ${Libcap_LIBRARIES})
endif()

40
dmabuftexture.cpp Normal file
View file

@ -0,0 +1,40 @@
/*
* Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Aleix Pol Gonzalez <aleixpol@kde.org>
*/
#include "dmabuftexture.h"
#include "kwineglimagetexture.h"
#include "kwinglutils.h"
using namespace KWin;
DmaBufTexture::DmaBufTexture(KWin::GLTexture *texture)
: m_texture(texture)
, m_framebuffer(new KWin::GLRenderTarget(*m_texture))
{
}
DmaBufTexture::~DmaBufTexture() = default;
KWin::GLRenderTarget *DmaBufTexture::framebuffer() const
{
return m_framebuffer.data();
}

45
dmabuftexture.h Normal file
View file

@ -0,0 +1,45 @@
/*
* Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Aleix Pol Gonzalez <aleixpol@kde.org>
*/
#pragma once
#include "kwin_export.h"
#include <QScopedPointer>
namespace KWin
{
class GLRenderTarget;
class GLTexture;
class KWIN_EXPORT DmaBufTexture
{
public:
explicit DmaBufTexture(KWin::GLTexture* texture);
virtual ~DmaBufTexture();
virtual quint32 stride() const = 0;
virtual int fd() const = 0;
KWin::GLRenderTarget* framebuffer() const;
protected:
QScopedPointer<KWin::GLTexture> m_texture;
QScopedPointer<KWin::GLRenderTarget> m_framebuffer;
};
}

View file

@ -88,6 +88,7 @@ set(kwin_GLUTILSLIB_SRCS
kwingltexture.cpp
kwinglutils.cpp
kwinglutils_funcs.cpp
kwineglimagetexture.cpp
logging.cpp
)

View file

@ -0,0 +1,44 @@
/********************************************************************
KWin - the KDE window manager
This file is part of the KDE project.
Copyright (C) 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include "kwineglimagetexture.h"
using namespace KWin;
#include <QDebug>
#include <epoxy/egl.h>
EGLImageTexture::EGLImageTexture(EGLDisplay display, EGLImage image, int internalFormat, const QSize &size)
: GLTexture(internalFormat, size, 1, true)
, m_image(image)
, m_display(display)
{
if (m_image == EGL_NO_IMAGE_KHR) {
return;
}
bind();
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, m_image);
}
EGLImageTexture::~EGLImageTexture()
{
eglDestroyImageKHR(m_display, m_image);
}

View file

@ -0,0 +1,45 @@
/********************************************************************
KWin - the KDE window manager
This file is part of the KDE project.
Copyright (C) 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#pragma once
#include <kwinglutils_export.h>
#include <kwingltexture.h>
typedef void *EGLImageKHR;
typedef void *EGLDisplay;
typedef void *EGLClientBuffer;
namespace KWin
{
class KWINGLUTILS_EXPORT EGLImageTexture : public GLTexture
{
public:
EGLImageTexture(EGLDisplay display, EGLImageKHR image, int internalFormat, const QSize &size);
~EGLImageTexture() override;
private:
EGLImageKHR m_image;
EGLDisplay m_display;
};
}

View file

@ -26,12 +26,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "platform.h"
#include "effects.h"
#include "tabletmodemanager.h"
#include "screencast/screencastmanager.h"
#include "wayland_server.h"
#include "xwl/xwayland.h"
// KWayland
#include <KWaylandServer/display.h>
#include <KWaylandServer/seat_interface.h>
// KDE
#include <KCrash>
#include <KLocalizedString>
@ -163,6 +165,7 @@ void ApplicationWayland::performStartup()
VirtualKeyboard::create(this);
createBackend();
TabletModeManager::create(this);
new ScreencastManager(this);
}
void ApplicationWayland::createBackend()

0
pipewirestream.cpp Normal file
View file

0
pipewirestream.h Normal file
View file

View file

@ -46,6 +46,7 @@ class Manager;
class AbstractOutput;
class Edge;
class Compositor;
class DmaBufTexture;
class OverlayWindow;
class OpenGLBackend;
class Outline;
@ -83,6 +84,10 @@ public:
virtual Screens *createScreens(QObject *parent = nullptr);
virtual OpenGLBackend *createOpenGLBackend();
virtual QPainterBackend *createQPainterBackend();
virtual DmaBufTexture *createDmaBufTexture(const QSize &size) {
Q_UNUSED(size);
return nullptr;
}
/**
* Informs the Platform that it is about to go down and shall do appropriate cleanup.

115
screencast/pipewirecore.cpp Normal file
View file

@ -0,0 +1,115 @@
/*
* Copyright © 2018-2020 Red Hat, Inc
* Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Jan Grulich <jgrulich@redhat.com>
* Aleix Pol Gonzalez <aleixpol@kde.org>
*/
#include "pipewirecore.h"
#include <QDebug>
#include <QSocketNotifier>
#include <KLocalizedString>
#include "kwinpipewire_logging.h"
PipeWireCore::PipeWireCore()
{
pw_init(nullptr, nullptr);
pwCoreEvents.version = PW_VERSION_CORE_EVENTS;
pwCoreEvents.error = &PipeWireCore::onCoreError;
}
PipeWireCore::~PipeWireCore()
{
if (pwMainLoop) {
pw_loop_leave(pwMainLoop);
}
if (pwCore) {
pw_core_disconnect(pwCore);
}
if (pwContext) {
pw_context_destroy(pwContext);
}
if (pwMainLoop) {
pw_loop_destroy(pwMainLoop);
}
}
void PipeWireCore::onCoreError(void* data, uint32_t id, int seq, int res, const char* message)
{
Q_UNUSED(seq)
qCWarning(KWIN_PIPEWIRE) << "PipeWire remote error: " << message;
if (id == PW_ID_CORE && res == -EPIPE) {
PipeWireCore *pw = static_cast<PipeWireCore*>(data);
Q_EMIT pw->pipewireFailed(QString::fromUtf8(message));
}
}
bool PipeWireCore::init()
{
pwMainLoop = pw_loop_new(nullptr);
pw_loop_enter(pwMainLoop);
QSocketNotifier *notifier = new QSocketNotifier(pw_loop_get_fd(pwMainLoop), QSocketNotifier::Read, this);
connect(notifier, &QSocketNotifier::activated, this, [this] {
int result = pw_loop_iterate (pwMainLoop, 0);
if (result < 0)
qCWarning(KWIN_PIPEWIRE) << "pipewire_loop_iterate failed: " << result;
}
);
pwContext = pw_context_new(pwMainLoop, nullptr, 0);
if (!pwContext) {
qCWarning(KWIN_PIPEWIRE) << "Failed to create PipeWire context";
m_error = i18n("Failed to create PipeWire context");
return false;
}
pwCore = pw_context_connect(pwContext, nullptr, 0);
if (!pwCore) {
qCWarning(KWIN_PIPEWIRE) << "Failed to connect PipeWire context";
m_error = i18n("Failed to connect PipeWire context");
return false;
}
if (pw_loop_iterate(pwMainLoop, 0) < 0) {
qCWarning(KWIN_PIPEWIRE) << "Failed to start main PipeWire loop";
m_error = i18n("Failed to start main PipeWire loop");
return false;
}
pw_core_add_listener(pwCore, &coreListener, &pwCoreEvents, this);
return true;
}
QSharedPointer< PipeWireCore > PipeWireCore::self()
{
static QWeakPointer<PipeWireCore> global;
QSharedPointer<PipeWireCore> ret;
if (global) {
ret = global.toStrongRef();
} else {
ret.reset(new PipeWireCore);
ret->init();
global = ret;
}
return ret;
}

54
screencast/pipewirecore.h Normal file
View file

@ -0,0 +1,54 @@
/*
* Copyright © 2018-2020 Red Hat, Inc
* Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Jan Grulich <jgrulich@redhat.com>
* Aleix Pol Gonzalez <aleixpol@kde.org>
*/
#pragma once
#include <QObject>
#include <QDebug>
#include <spa/utils/hook.h>
#include <pipewire/pipewire.h>
class PipeWireCore : public QObject
{
Q_OBJECT
public:
PipeWireCore();
static void onCoreError(void *data, uint32_t id, int seq, int res, const char *message);
~PipeWireCore();
bool init();
static QSharedPointer<PipeWireCore> self();
struct pw_core *pwCore = nullptr;
struct pw_context *pwContext = nullptr;
struct pw_loop *pwMainLoop = nullptr;
spa_hook coreListener;
QString m_error;
pw_core_events pwCoreEvents = {};
Q_SIGNALS:
void pipewireFailed(const QString &message);
};

View file

@ -0,0 +1,476 @@
/*
* Copyright © 2018-2020 Red Hat, Inc
* Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Jan Grulich <jgrulich@redhat.com>
* Aleix Pol Gonzalez <aleixpol@kde.org>
*/
#include "pipewirestream.h"
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <gbm.h>
#include <spa/buffer/meta.h>
#include <QLoggingCategory>
#include <QPainter>
#include "utils.h"
#include "cursor.h"
#include "main.h"
#include "platform.h"
#include "scenes/opengl/egl_dmabuf.h"
#include "platformsupport/scenes/opengl/drm_fourcc.h"
#include "kwineglimagetexture.h"
#include "dmabuftexture.h"
#include "pipewirecore.h"
#include "kwinpipewire_logging.h"
#include <KLocalizedString>
void PipeWireStream::onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message)
{
PipeWireStream *pw = static_cast<PipeWireStream*>(data);
qCDebug(KWIN_PIPEWIRE) << "state changed"<< pw_stream_state_as_string(old) << " -> " << pw_stream_state_as_string(state) << error_message;
switch (state) {
case PW_STREAM_STATE_ERROR:
qCWarning(KWIN_PIPEWIRE) << "Stream error: " << error_message;
break;
case PW_STREAM_STATE_PAUSED:
if (pw->nodeId() == 0 && pw->pwStream) {
pw->pwNodeId = pw_stream_get_node_id(pw->pwStream);
Q_EMIT pw->streamReady(pw->nodeId());
}
break;
case PW_STREAM_STATE_STREAMING:
Q_EMIT pw->startStreaming();
break;
case PW_STREAM_STATE_CONNECTING:
break;
case PW_STREAM_STATE_UNCONNECTED:
if (!pw->m_stopped) {
Q_EMIT pw->stopStreaming();
}
break;
}
}
#define CURSOR_BPP 4
#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \
sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP)
void PipeWireStream::newStreamParams()
{
const int bpp = videoFormat.format == SPA_VIDEO_FORMAT_RGB || videoFormat.format == SPA_VIDEO_FORMAT_BGR ? 3 : 4;
auto stride = SPA_ROUND_UP_N (m_resolution.width() * bpp, 4);
uint8_t paramsBuffer[1024];
spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT (paramsBuffer, sizeof (paramsBuffer));
spa_rectangle resolution = SPA_RECTANGLE(uint32_t(m_resolution.width()), uint32_t(m_resolution.height()));
const auto cursorSize = KWin::Cursors::self()->currentCursor()->themeSize();
const spa_pod *params[] = {
(spa_pod*) spa_pod_builder_add_object(&pod_builder,
SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&resolution),
SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(16, 2, 16),
SPA_PARAM_BUFFERS_blocks, SPA_POD_Int (1),
SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride),
SPA_PARAM_BUFFERS_size, SPA_POD_Int(stride * m_resolution.height()),
SPA_PARAM_BUFFERS_align, SPA_POD_Int(16)),
(spa_pod*) spa_pod_builder_add_object (&pod_builder,
SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
SPA_PARAM_META_type, SPA_POD_Id (SPA_META_Cursor),
SPA_PARAM_META_size, SPA_POD_Int (CURSOR_META_SIZE (cursorSize, cursorSize)))
};
pw_stream_update_params(pwStream, params, 2);
}
void PipeWireStream::onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format)
{
if (!format || id != SPA_PARAM_Format) {
return;
}
PipeWireStream *pw = static_cast<PipeWireStream *>(data);
spa_format_video_raw_parse (format, &pw->videoFormat);
qCDebug(KWIN_PIPEWIRE) << "Stream format changed" << pw << pw->videoFormat.format;
pw->newStreamParams();
}
void PipeWireStream::onStreamAddBuffer(void *data, pw_buffer *buffer)
{
PipeWireStream *stream = static_cast<PipeWireStream *>(data);
struct spa_data *spa_data = buffer->buffer->datas;
spa_data->mapoffset = 0;
spa_data->flags = SPA_DATA_FLAG_READWRITE;
QSharedPointer<KWin::DmaBufTexture> dmabuf (KWin::kwinApp()->platform()->createDmaBufTexture(stream->m_resolution));
if (dmabuf) {
spa_data->type = SPA_DATA_DmaBuf;
spa_data->fd = dmabuf->fd();
spa_data->data = NULL;
spa_data->maxsize = dmabuf->stride() * stream->m_resolution.height();
stream->m_dmabufDataForPwBuffer.insert(buffer, dmabuf);
} else {
const int bytesPerPixel = stream->m_hasAlpha ? 4 : 3;
const int stride = SPA_ROUND_UP_N (stream->m_resolution.width() * bytesPerPixel, 4);
spa_data->maxsize = stride * stream->m_resolution.height();
spa_data->type = SPA_DATA_MemFd;
spa_data->fd = memfd_create("kwin-screencast-memfd", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (spa_data->fd == -1) {
qCCritical(KWIN_PIPEWIRE) << "memfd: Can't create memfd";
return;
}
spa_data->mapoffset = 0;
if (ftruncate (spa_data->fd, spa_data->maxsize) < 0) {
qCCritical(KWIN_PIPEWIRE) << "memfd: Can't truncate to" << spa_data->maxsize;
return;
}
unsigned int seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL;
if (fcntl(spa_data->fd, F_ADD_SEALS, seals) == -1)
qCWarning(KWIN_PIPEWIRE) << "memfd: Failed to add seals";
spa_data->data = mmap(NULL,
spa_data->maxsize,
PROT_READ | PROT_WRITE,
MAP_SHARED,
spa_data->fd,
spa_data->mapoffset);
if (spa_data->data == MAP_FAILED)
qCCritical(KWIN_PIPEWIRE) << "memfd: Failed to mmap memory";
else
qCDebug(KWIN_PIPEWIRE) << "memfd: created successfully" << spa_data->data << spa_data->maxsize;
}
}
void PipeWireStream::onStreamRemoveBuffer(void *data, pw_buffer *buffer)
{
PipeWireStream *stream = static_cast<PipeWireStream *>(data);
struct spa_buffer *spa_buffer = buffer->buffer;
struct spa_data *spa_data = spa_buffer->datas;
if (spa_data->type == SPA_DATA_DmaBuf) {
stream->m_dmabufDataForPwBuffer.remove(buffer);
} else if (spa_data->type == SPA_DATA_MemFd) {
munmap (spa_data->data, spa_data->maxsize);
close (spa_data->fd);
}
}
PipeWireStream::PipeWireStream(bool hasAlpha, const QSize &resolution, QObject *parent)
: QObject(parent)
, m_resolution(resolution)
, m_hasAlpha(hasAlpha)
{
pwStreamEvents.version = PW_VERSION_STREAM_EVENTS;
pwStreamEvents.add_buffer = &PipeWireStream::onStreamAddBuffer;
pwStreamEvents.remove_buffer = &PipeWireStream::onStreamRemoveBuffer;
pwStreamEvents.state_changed = &PipeWireStream::onStreamStateChanged;
pwStreamEvents.param_changed = &PipeWireStream::onStreamParamChanged;
}
PipeWireStream::~PipeWireStream()
{
m_stopped = true;
if (pwStream) {
pw_stream_destroy(pwStream);
}
}
bool PipeWireStream::init()
{
pwCore = PipeWireCore::self();
if (!pwCore->m_error.isEmpty()) {
m_error = pwCore->m_error;
return false;
}
connect(pwCore.data(), &PipeWireCore::pipewireFailed, this, &PipeWireStream::coreFailed);
if (!createStream()) {
qCWarning(KWIN_PIPEWIRE) << "Failed to create PipeWire stream";
m_error = i18n("Failed to create PipeWire stream");
return false;
}
return true;
}
uint PipeWireStream::framerate()
{
if (pwStream) {
return videoFormat.max_framerate.num / videoFormat.max_framerate.denom;
}
return 0;
}
uint PipeWireStream::nodeId()
{
return pwNodeId;
}
bool PipeWireStream::createStream()
{
const QByteArray objname = "kwin-screencast-" + objectName().toUtf8();
pwStream = pw_stream_new(pwCore->pwCore, objname, nullptr);
uint8_t buffer[1024];
spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
spa_fraction minFramerate = SPA_FRACTION(1, 1);
spa_fraction maxFramerate = SPA_FRACTION(25, 1);
spa_fraction defaultFramerate = SPA_FRACTION(0, 1);
spa_rectangle resolution = SPA_RECTANGLE(uint32_t(m_resolution.width()), uint32_t(m_resolution.height()));
const auto format = m_hasAlpha || m_gbmDevice ? SPA_VIDEO_FORMAT_BGRA : SPA_VIDEO_FORMAT_BGR;
const spa_pod *param = (spa_pod*)spa_pod_builder_add_object(&podBuilder,
SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
SPA_FORMAT_VIDEO_format, SPA_POD_Id(format),
SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&resolution),
SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&defaultFramerate),
SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&maxFramerate, &minFramerate, &maxFramerate));
pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this);
auto flags = pw_stream_flags(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_ALLOC_BUFFERS);
if (pw_stream_connect(pwStream, PW_DIRECTION_OUTPUT, SPA_ID_INVALID, flags, &param, 1) != 0) {
qCWarning(KWIN_PIPEWIRE) << "Could not connect to stream";
pw_stream_destroy(pwStream);
return false;
}
if (m_cursor.mode == KWaylandServer::ScreencastInterface::Embedded) {
connect(KWin::Cursors::self(), &KWin::Cursors::positionChanged, this, [this] {
if (m_cursor.lastFrameTexture) {
m_repainting = true;
recordFrame(m_cursor.lastFrameTexture.data(), QRegion{m_cursor.lastRect} | cursorGeometry(KWin::Cursors::self()->currentCursor()));
m_repainting = false;
}
});
}
return true;
}
void PipeWireStream::coreFailed(const QString &errorMessage)
{
m_error = errorMessage;
Q_EMIT stopStreaming();
}
void PipeWireStream::stop()
{
m_stopped = true;
delete this;
}
static KWin::GLTexture *copyTexture(KWin::GLTexture *texture)
{
KWin::GLTexture *copy = new KWin::GLTexture(texture->internalFormat(), texture->size());
copy->setFilter(GL_LINEAR);
copy->setWrapMode(GL_CLAMP_TO_EDGE);
const QRect r({}, texture->size());
copy->bind();
glCopyTextureSubImage2D(copy->texture(), 0, 0, 0, 0, 0, r.width(), r.height());
copy->unbind();
return copy;
}
void PipeWireStream::recordFrame(KWin::GLTexture* frameTexture, const QRegion &damagedRegion)
{
Q_ASSERT(!m_stopped);
Q_ASSERT(frameTexture);
if (frameTexture->size() != m_resolution) {
m_resolution = frameTexture->size();
newStreamParams();
return;
}
const char *error = "";
auto state = pw_stream_get_state(pwStream, &error);
if (state != PW_STREAM_STATE_STREAMING) {
if (error) {
qCWarning(KWIN_PIPEWIRE) << "Failed to record frame: stream is not active" << error;
}
return;
}
struct pw_buffer *buffer = pw_stream_dequeue_buffer(pwStream);
if (!buffer) {
return;
}
struct spa_buffer *spa_buffer = buffer->buffer;
struct spa_data *spa_data = spa_buffer->datas;
uint8_t *data = (uint8_t *) spa_data->data;
if (!data && spa_buffer->datas->type != SPA_DATA_DmaBuf) {
qCWarning(KWIN_PIPEWIRE) << "Failed to record frame: invalid buffer data";
pw_stream_queue_buffer(pwStream, buffer);
return;
}
const auto size = frameTexture->size();
spa_data->chunk->offset = 0;
if (data) {
const int bpp = data && !m_hasAlpha ? 3 : 4;
const uint stride = SPA_ROUND_UP_N (size.width() * bpp, 4);
const uint bufferSize = stride * size.height();
if (bufferSize > spa_data->maxsize) {
qCDebug(KWIN_PIPEWIRE) << "Failed to record frame: frame is too big";
pw_stream_queue_buffer(pwStream, buffer);
return;
}
spa_data->chunk->size = bufferSize;
spa_data->chunk->stride = stride;
frameTexture->bind();
glGetTextureImage(frameTexture->texture(), 0, m_hasAlpha ? GL_BGRA : GL_BGR, GL_UNSIGNED_BYTE, bufferSize, data);
auto cursor = KWin::Cursors::self()->currentCursor();
if (m_cursor.mode == KWaylandServer::ScreencastInterface::Embedded && m_cursor.viewport.contains(cursor->pos())) {
QImage dest(data, size.width(), size.height(), QImage::Format_RGBA8888_Premultiplied);
QPainter painter(&dest);
const auto position = (cursor->pos() - m_cursor.viewport.topLeft() - cursor->hotspot()) * m_cursor.scale;
painter.drawImage(QRect{position, cursor->image().size()}, cursor->image());
}
} else {
using namespace KWin;
auto &buf = m_dmabufDataForPwBuffer[buffer];
spa_data->chunk->stride = buf->stride();
spa_data->chunk->size = spa_data->maxsize;
GLRenderTarget::pushRenderTarget(buf->framebuffer());
frameTexture->bind();
QRect r(QPoint(), size);
auto shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture);
QMatrix4x4 mvp;
mvp.ortho(r);
shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp);
QRegion dr = damagedRegion;
if (m_cursor.texture) {
dr |= m_cursor.lastRect;
}
frameTexture->render(damagedRegion, r, true);
auto cursor = KWin::Cursors::self()->currentCursor();
if (m_cursor.mode == KWaylandServer::ScreencastInterface::Embedded && m_cursor.viewport.contains(cursor->pos())) {
if (!m_repainting) //We need to copy the last version of the stream to render the moved cursor on top
m_cursor.lastFrameTexture.reset(copyTexture(frameTexture));
if (!m_cursor.texture || m_cursor.lastKey != cursor->image().cacheKey())
m_cursor.texture.reset(new KWin::GLTexture(cursor->image()));
m_cursor.texture->setYInverted(false);
m_cursor.texture->bind();
const auto cursorRect = cursorGeometry(cursor);
mvp.translate(cursorRect.left(), r.height() - cursorRect.top() - cursor->image().height() * m_cursor.scale);
shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
m_cursor.texture->render(cursorRect, cursorRect, true);
glDisable(GL_BLEND);
m_cursor.texture->unbind();
m_cursor.lastRect = cursorRect;
}
ShaderManager::instance()->popShader();
GLRenderTarget::popRenderTarget();
}
frameTexture->unbind();
if (m_cursor.mode == KWaylandServer::ScreencastInterface::Metadata) {
sendCursorData(KWin::Cursors::self()->currentCursor(),
(spa_meta_cursor *) spa_buffer_find_meta_data (spa_buffer, SPA_META_Cursor, sizeof (spa_meta_cursor)));
}
pw_stream_queue_buffer(pwStream, buffer);
}
QRect PipeWireStream::cursorGeometry(KWin::Cursor *cursor) const
{
const auto position = (cursor->pos() - m_cursor.viewport.topLeft() - cursor->hotspot()) * m_cursor.scale;
return QRect{position, m_cursor.texture->size()};
}
void PipeWireStream::sendCursorData(KWin::Cursor* cursor, spa_meta_cursor *spa_meta_cursor)
{
if (!cursor || !spa_meta_cursor) {
return;
}
const auto position = (cursor->pos() - m_cursor.viewport.topLeft()) * m_cursor.scale;
spa_meta_cursor->id = 1;
spa_meta_cursor->position.x = position.x();
spa_meta_cursor->position.y = position.y();
spa_meta_cursor->hotspot.x = cursor->hotspot().x() * m_cursor.scale;
spa_meta_cursor->hotspot.y = cursor->hotspot().y() * m_cursor.scale;
spa_meta_cursor->bitmap_offset = 0;
const QImage image = cursor->image();
if (image.cacheKey() == m_cursor.lastKey) {
return;
}
m_cursor.lastKey = image.cacheKey();
spa_meta_cursor->bitmap_offset = sizeof (struct spa_meta_cursor);
struct spa_meta_bitmap *spa_meta_bitmap = SPA_MEMBER (spa_meta_cursor,
spa_meta_cursor->bitmap_offset,
struct spa_meta_bitmap);
spa_meta_bitmap->format = SPA_VIDEO_FORMAT_RGBA;
spa_meta_bitmap->offset = sizeof (struct spa_meta_bitmap);
uint8_t *bitmap_data = SPA_MEMBER (spa_meta_bitmap, spa_meta_bitmap->offset, uint8_t);
QImage dest(bitmap_data, image.width(), image.height(), QImage::Format_RGBA8888_Premultiplied);
spa_meta_bitmap->size.width = image.width();
spa_meta_bitmap->size.height = image.height();
spa_meta_bitmap->stride = dest.bytesPerLine();
QPainter painter(&dest);
painter.drawImage(QPoint(), image);
}
void PipeWireStream::setCursorMode(KWaylandServer::ScreencastInterface::CursorMode mode, qreal scale, const QRect &viewport)
{
m_cursor.mode = mode;
m_cursor.scale = scale;
m_cursor.viewport = viewport;
}

114
screencast/pipewirestream.h Normal file
View file

@ -0,0 +1,114 @@
/*
* Copyright © 2018-2020 Red Hat, Inc
* Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Jan Grulich <jgrulich@redhat.com>
* Aleix Pol Gonzalez <aleixpol@kde.org>
*/
#pragma once
#include <QObject>
#include <QSize>
#include <QHash>
#include <QSharedPointer>
#include <KWaylandServer/screencast_interface.h>
#include "kwinglobals.h"
#include "config-kwin.h"
#include <pipewire/pipewire.h>
#include <spa/param/format-utils.h>
#include <spa/param/video/format-utils.h>
#include <spa/param/props.h>
#undef Status
namespace KWin
{
class Cursor;
class GLTexture;
class DmaBufTexture;
}
class PipeWireCore;
class KWIN_EXPORT PipeWireStream : public QObject
{
Q_OBJECT
public:
explicit PipeWireStream(bool hasAlpha, const QSize &resolution, QObject *parent);
~PipeWireStream();
bool init();
uint framerate();
uint nodeId();
QString error() const {
return m_error;
}
void stop();
/** Renders @p frame into the current framebuffer into the stream */
void recordFrame(KWin::GLTexture *frame, const QRegion &damagedRegion);
void setCursorMode(KWaylandServer::ScreencastInterface::CursorMode mode, qreal scale, const QRect &viewport);
Q_SIGNALS:
void streamReady(quint32 nodeId);
void startStreaming();
void stopStreaming();
private:
static void onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format);
static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message);
static void onStreamAddBuffer(void *data, pw_buffer *buffer);
static void onStreamRemoveBuffer(void *data, pw_buffer *buffer);
bool createStream();
void updateParams();
void coreFailed(const QString &errorMessage);
void sendCursorData(KWin::Cursor* cursor, spa_meta_cursor *spa_cursor);
void newStreamParams();
QSharedPointer<PipeWireCore> pwCore;
struct pw_stream *pwStream = nullptr;
spa_hook streamListener;
pw_stream_events pwStreamEvents = {};
uint32_t pwNodeId = 0;
QSize m_resolution;
bool m_stopped = false;
spa_video_info_raw videoFormat;
QString m_error;
const bool m_hasAlpha;
struct gbm_device *m_gbmDevice = nullptr;
struct {
KWaylandServer::ScreencastInterface::CursorMode mode = KWaylandServer::ScreencastInterface::Hidden;
qreal scale = 1;
QRect viewport;
qint64 lastKey = 0;
QRect lastRect;
QScopedPointer<KWin::GLTexture> texture;
QScopedPointer<KWin::GLTexture> lastFrameTexture;
} m_cursor;
bool m_repainting = false;
QRect cursorGeometry(KWin::Cursor *cursor) const;
QHash<struct pw_buffer *, QSharedPointer<KWin::DmaBufTexture>> m_dmabufDataForPwBuffer;
};

View file

@ -0,0 +1,190 @@
/*
* Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Aleix Pol Gonzalez <aleixpol@kde.org>
*/
#include "screencastmanager.h"
#include "scene.h"
#include "workspace.h"
#include "composite.h"
#include "platform.h"
#include "abstract_wayland_output.h"
#include "plugins/scenes/opengl/scene_opengl.h"
#include "pipewirestream.h"
#include <KWaylandServer/output_interface.h>
#include <KLocalizedString>
#include <abstract_client.h>
#include <effects.h>
#include <deleted.h>
using namespace KWin;
ScreencastManager::ScreencastManager(QObject *parent)
: QObject(parent)
{
connect(waylandServer()->screencast(), &KWaylandServer::ScreencastInterface::windowScreencastRequested, this, &ScreencastManager::streamWindow);
connect(waylandServer()->screencast(), &KWaylandServer::ScreencastInterface::outputScreencastRequested, this, &ScreencastManager::streamOutput);
}
class EGLFence : public QObject
{
public:
EGLFence(EGLDisplay eglDisplay)
: m_eglDisplay(eglDisplay)
, m_sync(eglCreateSync(eglDisplay, EGL_SYNC_FENCE_KHR, NULL))
{
Q_ASSERT(m_sync);
glFinish();
}
bool clientWaitSync()
{
glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
int ret = eglClientWaitSync(m_eglDisplay, m_sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, 0);
Q_ASSERT(ret == EGL_CONDITION_SATISFIED_KHR);
return ret == EGL_CONDITION_SATISFIED_KHR;
}
~EGLFence() {
auto ret = eglDestroySyncKHR(m_eglDisplay, m_sync);
Q_ASSERT(ret == EGL_TRUE);
}
private:
const EGLDisplay m_eglDisplay;
const EGLSyncKHR m_sync;
};
class WindowStream : public PipeWireStream
{
public:
WindowStream(KWin::Toplevel *toplevel, QObject *parent)
: PipeWireStream(toplevel->hasAlpha(), toplevel->clientSize() * toplevel->bufferScale(), parent)
, m_toplevel(toplevel)
{
if (AbstractClient *client = qobject_cast<AbstractClient *>(toplevel)) {
setObjectName(client->desktopFileName());
}
connect(toplevel, &Toplevel::windowClosed, this, &PipeWireStream::stopStreaming);
connect(this, &PipeWireStream::startStreaming, this, &WindowStream::startFeeding);
}
private:
void startFeeding() {
auto scene = KWin::Compositor::self()->scene();
connect(scene, &Scene::frameRendered, this, &WindowStream::bufferToStream);
connect(m_toplevel, &Toplevel::damaged, this, &WindowStream::includeDamage);
m_toplevel->damaged(m_toplevel, m_toplevel->frameGeometry());
}
void includeDamage(KWin::Toplevel *toplevel, const QRect &damage) {
Q_ASSERT(m_toplevel == toplevel);
m_damagedRegion |= damage;
}
void bufferToStream () {
if (m_damagedRegion.isEmpty()) {
return;
}
EGLFence fence(kwinApp()->platform()->sceneEglDisplay());
QSharedPointer<GLTexture> frameTexture(m_toplevel->effectWindow()->sceneWindow()->windowTexture());
const bool wasYInverted = frameTexture->isYInverted();
frameTexture->setYInverted(false);
recordFrame(frameTexture.data(), m_damagedRegion);
frameTexture->setYInverted(wasYInverted);
m_damagedRegion = {};
bool b = fence.clientWaitSync();
Q_ASSERT(b);
}
QRegion m_damagedRegion;
KWin::Toplevel *m_toplevel;
};
void ScreencastManager::streamWindow(KWaylandServer::ScreencastStreamInterface *waylandStream, const QString &winid)
{
auto *toplevel = KWin::Workspace::self()->findToplevel(winid);
if (!toplevel) {
waylandStream->sendFailed(i18n("Could not find window id %1", winid));
return;
}
auto stream = new WindowStream(toplevel, this);
integrateStreams(waylandStream, stream);
}
void ScreencastManager::streamOutput(KWaylandServer::ScreencastStreamInterface *waylandStream,
::wl_resource *outputResource,
KWaylandServer::ScreencastInterface::CursorMode mode)
{
auto outputIface = KWaylandServer::OutputInterface::get(outputResource);
if (!outputIface) {
waylandStream->sendFailed(i18n("Invalid output"));
return;
}
const auto outputs = kwinApp()->platform()->enabledOutputs();
AbstractWaylandOutput *streamOutput = nullptr;
for (auto output : outputs) {
if (static_cast<AbstractWaylandOutput *>(output)->waylandOutput() == outputIface) {
streamOutput = static_cast<AbstractWaylandOutput *>(output);
}
}
if (!streamOutput) {
waylandStream->sendFailed(i18n("Could not find output"));
return;
}
auto stream = new PipeWireStream(true, streamOutput->pixelSize(), this);
stream->setObjectName(streamOutput->name());
stream->setCursorMode(mode, streamOutput->scale(), streamOutput->geometry());
connect(streamOutput, &QObject::destroyed, stream, &PipeWireStream::stopStreaming);
auto bufferToStream = [streamOutput, stream] (const QRegion &damagedRegion) {
auto scene = KWin::Compositor::self()->scene();
auto texture = scene->textureForOutput(streamOutput);
const QRect frame({}, streamOutput->modeSize());
const QRegion region = damagedRegion.isEmpty() || streamOutput->pixelSize() != streamOutput->modeSize() ? frame : damagedRegion.translated(-streamOutput->geometry().topLeft()).intersected(frame);
stream->recordFrame(texture.data(), region);
};
connect(stream, &PipeWireStream::startStreaming, waylandStream, [streamOutput, stream, bufferToStream] {
KWin::Compositor::self()->addRepaint(streamOutput->geometry());
connect(streamOutput, &AbstractWaylandOutput::outputChange, stream, bufferToStream);
});
integrateStreams(waylandStream, stream);
}
void ScreencastManager::integrateStreams(KWaylandServer::ScreencastStreamInterface *waylandStream, PipeWireStream *stream)
{
connect(waylandStream, &KWaylandServer::ScreencastStreamInterface::finished, stream, &PipeWireStream::stop);
connect(stream, &PipeWireStream::stopStreaming, waylandStream, [stream, waylandStream] {
waylandStream->sendClosed();
delete stream;
});
connect(stream, &PipeWireStream::streamReady, stream, [waylandStream] (uint nodeid) {
waylandStream->sendCreated(nodeid);
});
if (!stream->init()) {
waylandStream->sendFailed(stream->error());
delete stream;
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright © 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Jan Grulich <jgrulich@redhat.com>
* Aleix Pol Gonzalez <aleixpol@kde.org>
*/
#pragma once
#include "wayland_server.h"
#include <KWaylandServer/screencast_interface.h>
class PipeWireStream;
namespace KWin
{
class AbstractEglBackend;
}
class ScreencastManager
: public QObject
{
Q_OBJECT
public:
ScreencastManager(QObject *parent);
void streamWindow(KWaylandServer::ScreencastStreamInterface *stream, const QString &winid);
void streamOutput(KWaylandServer::ScreencastStreamInterface *stream,
::wl_resource *outputResource,
KWaylandServer::ScreencastInterface::CursorMode mode);
private:
void integrateStreams(KWaylandServer::ScreencastStreamInterface *waylandStream, PipeWireStream *pipewireStream);
};

View file

@ -240,7 +240,7 @@ public:
return KWin::fetchRequestedInterfaces(client->executablePath());
}
const QSet<QByteArray> interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate"};
const QSet<QByteArray> interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate", "zkde_screencast_unstable_v1"};
QSet<QString> m_reported;
bool allowInterface(KWaylandServer::ClientConnection *client, const QByteArray &interfaceName) override {
@ -458,6 +458,8 @@ bool WaylandServer::init(const QByteArray &socketName, InitializationFlags flags
m_keyState = m_display->createKeyStateInterface(m_display);
m_keyState->create();
m_screencast = m_display->createScreencastInterface(m_display);
return true;
}

View file

@ -69,6 +69,7 @@ class LinuxDmabufUnstableV1Buffer;
class TabletManagerInterface;
class KeyboardShortcutsInhibitManagerV1Interface;
class XdgDecorationManagerV1Interface;
class ScreencastInterface;
}
@ -127,6 +128,9 @@ public:
{
return m_windowManagement;
}
KWaylandServer::ScreencastInterface *screencast() {
return m_screencast;
}
KWaylandServer::ServerSideDecorationManagerInterface *decorationManager() const {
return m_decorationManager;
}
@ -281,6 +285,7 @@ private:
KWaylandServer::LinuxDmabufUnstableV1Interface *m_linuxDmabuf = nullptr;
KWaylandServer::KeyboardShortcutsInhibitManagerV1Interface *m_keyboardShortcutsInhibitManager = nullptr;
QSet<KWaylandServer::LinuxDmabufUnstableV1Buffer*> m_linuxDmabufBuffers;
KWaylandServer::ScreencastInterface *m_screencast = nullptr;
struct {
KWaylandServer::ClientConnection *client = nullptr;
QMetaObject::Connection destroyConnection;