Commit 18b0072f authored by Aleix Pol Gonzalez's avatar Aleix Pol Gonzalez 🐧 Committed by Aleix Pol Gonzalez
Browse files

Provide the components for screencasting

To be used for thumbnails in plasma-desktop and plasma-phone-components
parent 157a82d7
......@@ -2,9 +2,34 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR}/.. ${CMAKE_CURRENT_SOURCE_DIR}/.
add_library(taskmanagerplugin SHARED taskmanagerplugin.cpp)
target_link_libraries(taskmanagerplugin Qt5::Qml
taskmanager)
target_link_libraries(taskmanagerplugin taskmanager)
install(TARGETS taskmanagerplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/taskmanager)
install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/taskmanager)
if(TARGET PkgConfig::PipeWire)
ecm_qt_declare_logging_category(SRCS
HEADER logging.h
IDENTIFIER PIPEWIRE_LOGGING
CATEGORY_NAME plasma_workspace_pipewire_logging
DESCRIPTION "PipeWire components for window thumbnails"
EXPORT PlasmaWorkspacePipeWire)
ecm_qt_install_logging_categories(
EXPORT PlasmaWorkspacePipeWire
FILE myproject.categories
DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}"
)
ecm_add_qtwayland_client_protocol(SRCS
PROTOCOL ${PLASMA_WAYLAND_PROTOCOLS_DIR}/screencast.xml
BASENAME zkde-screencast-unstable-v1
)
target_compile_definitions(taskmanagerplugin PRIVATE -DWITH_PIPEWIRE)
target_sources(taskmanagerplugin PUBLIC pipewirecore.cpp pipewiresourceitem.cpp pipewiresourcestream.cpp screencasting.cpp screencastingrequest.cpp ${SRCS})
target_link_libraries(taskmanagerplugin PkgConfig::PipeWire
Qt5::Qml Qt5::Gui_EGL Qt5::Gui_GL Qt5::GuiPrivate
KF5::I18n
KF5::WaylandClient
Wayland::Client)
endif()
/*
* 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 "logging.h"
#include <spa/utils/result.h>
#include <KLocalizedString>
#include <QSocketNotifier>
PipeWireCore::PipeWireCore()
{
pw_init(nullptr, nullptr);
pwCoreEvents.version = PW_VERSION_CORE_EVENTS;
pwCoreEvents.error = &PipeWireCore::onCoreError;
}
void PipeWireCore::onCoreError(void* data, uint32_t id, int seq, int res, const char* message)
{
Q_UNUSED(seq)
qCWarning(PIPEWIRE_LOGGING) << "PipeWire remote error: " << message;
if (id == PW_ID_CORE && res == -EPIPE) {
PipeWireCore *pw = static_cast<PipeWireCore*>(data);
Q_EMIT pw->pipewireFailed(QString::fromUtf8(message));
}
}
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);
}
}
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(PIPEWIRE_LOGGING) << "pipewire_loop_iterate failed: " << spa_strerror (result);
}
);
pwContext = pw_context_new(pwMainLoop, nullptr, 0);
if (!pwContext) {
qCWarning(PIPEWIRE_LOGGING) << "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(PIPEWIRE_LOGGING) << "Failed to connect PipeWire context";
m_error = i18n("Failed to connect PipeWire context");
return false;
}
if (pw_loop_iterate(pwMainLoop, 0) < 0) {
qCWarning(PIPEWIRE_LOGGING) << "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);
if (ret->init()) {
global = ret;
}
}
return ret;
}
/*
* 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 <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);
};
/*
* Render a PipeWire stream into a QtQuick scene as a standard Item
* Copyright 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* 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.1 of the License, or (at your option) version 3, or any
* later version accepted by the membership of KDE e.V. (or its
* successor approved by the membership of KDE e.V.), which shall
* act as a proxy defined in Section 6 of version 3 of the license.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "pipewiresourceitem.h"
#include "pipewiresourcestream.h"
#include <QSocketNotifier>
#include <QSGImageNode>
#include <QGuiApplication>
#include <QQuickWindow>
#include <QOpenGLContext>
#include <QOpenGLTexture>
#include <QRunnable>
#include <QThread>
#include <qpa/qplatformnativeinterface.h>
#include <libdrm/drm_fourcc.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <QtPlatformHeaders/QEGLNativeContext>
static void pwInit () {pw_init(nullptr, nullptr);}
Q_COREAPP_STARTUP_FUNCTION(pwInit);
class DiscardEglPixmapRunnable : public QRunnable {
public:
DiscardEglPixmapRunnable(EGLImageKHR image, QOpenGLTexture* texture)
: m_image(image)
, m_texture(texture)
{}
void run() override {
if (m_image != EGL_NO_IMAGE_KHR) {
static auto eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
eglDestroyImageKHR(eglGetCurrentDisplay(), m_image);
}
delete m_texture;
}
private:
const EGLImageKHR m_image;
QOpenGLTexture* m_texture;
};
PipeWireSourceItem::PipeWireSourceItem(QQuickItem* parent)
: QQuickItem(parent)
{
setFlag(ItemHasContents, true);
connect(this, &QQuickItem::visibleChanged, this, [this] () {
if (m_stream)
m_stream->setActive(isVisible());
});
}
PipeWireSourceItem::~PipeWireSourceItem()
{
}
void PipeWireSourceItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data)
{
switch (change) {
case ItemVisibleHasChanged:
if (m_stream)
m_stream->setActive(data.boolValue && isComponentComplete());
break;
case ItemSceneChange:
m_needsRecreateTexture = true;
releaseResources();
break;
default:
break;
}
}
void PipeWireSourceItem::releaseResources()
{
if (window()) {
window()->scheduleRenderJob(new DiscardEglPixmapRunnable(m_image, m_texture.take()), QQuickWindow::NoStage);
m_image = EGL_NO_IMAGE_KHR;
}
}
void PipeWireSourceItem::setNodeId(uint nodeId)
{
if (nodeId == m_nodeId)
return;
m_nodeId = nodeId;
if (m_nodeId == 0) {
m_stream.reset(nullptr);
m_createNextTexture = [] { return nullptr; };
} else {
m_stream.reset(new PipeWireSourceStream(this));
m_stream->createStream(m_nodeId);
if (!m_stream->error().isEmpty()) {
m_stream.reset(nullptr);
m_nodeId = 0;
return;
}
m_stream->setActive(isVisible() && isComponentComplete());
connect(m_stream.data(), &PipeWireSourceStream::dmabufTextureReceived, this, &PipeWireSourceItem::updateTextureDmaBuf);
connect(m_stream.data(), &PipeWireSourceStream::imageTextureReceived, this, &PipeWireSourceItem::updateTextureImage);
}
Q_EMIT nodeIdChanged(nodeId);
}
QSGNode * PipeWireSourceItem::updatePaintNode(QSGNode* node, QQuickItem::UpdatePaintNodeData* )
{
if (m_createNextTexture) {
auto texture = m_createNextTexture();
if (!texture) {
delete node;
return nullptr;
}
if (m_needsRecreateTexture) {
delete node;
node = nullptr;
m_needsRecreateTexture = false;
}
QSGImageNode *textureNode = static_cast<QSGImageNode*>(node);
if (!textureNode) {
textureNode = window()->createImageNode();
textureNode->setOwnsTexture(true);
node = textureNode;
}
textureNode->setTexture(texture);
if (texture) {
const auto br = boundingRect().toRect();
QRect rect({0,0}, texture->textureSize().scaled(br.size(), Qt::KeepAspectRatio));
rect.moveCenter(br.center());
textureNode->setRect(rect);
}
}
return node;
}
QString PipeWireSourceItem::error() const
{
return m_stream->error();
}
static EGLImage createImage(EGLDisplay display, const QVector<DmaBufPlane> &planes, uint32_t format, const QSize &size)
{
const bool hasModifiers = planes[0].modifier != DRM_FORMAT_MOD_INVALID;
QVector<EGLint> attribs;
attribs << EGL_WIDTH << size.width()
<< EGL_HEIGHT << size.height()
<< EGL_LINUX_DRM_FOURCC_EXT << EGLint(format)
<< EGL_DMA_BUF_PLANE0_FD_EXT << planes[0].fd
<< EGL_DMA_BUF_PLANE0_OFFSET_EXT << EGLint(planes[0].offset)
<< EGL_DMA_BUF_PLANE0_PITCH_EXT << EGLint(planes[0].stride);
if (hasModifiers) {
attribs
<< EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT << EGLint(planes[0].modifier & 0xffffffff)
<< EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT << EGLint(planes[0].modifier >> 32);
}
if (planes.count() > 1) {
attribs
<< EGL_DMA_BUF_PLANE1_FD_EXT << planes[1].fd
<< EGL_DMA_BUF_PLANE1_OFFSET_EXT << EGLint(planes[1].offset)
<< EGL_DMA_BUF_PLANE1_PITCH_EXT << EGLint(planes[1].stride);
if (hasModifiers) {
attribs
<< EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT << EGLint(planes[1].modifier & 0xffffffff)
<< EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT << EGLint(planes[1].modifier >> 32);
}
}
if (planes.count() > 2) {
attribs
<< EGL_DMA_BUF_PLANE2_FD_EXT << planes[2].fd
<< EGL_DMA_BUF_PLANE2_OFFSET_EXT << EGLint(planes[2].offset)
<< EGL_DMA_BUF_PLANE2_PITCH_EXT << EGLint(planes[2].stride);
if (hasModifiers) {
attribs
<< EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT << EGLint(planes[2].modifier & 0xffffffff)
<< EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT << EGLint(planes[2].modifier >> 32);
}
}
if (planes.count() > 3) {
attribs
<< EGL_DMA_BUF_PLANE3_FD_EXT << planes[3].fd
<< EGL_DMA_BUF_PLANE3_OFFSET_EXT << EGLint(planes[3].offset)
<< EGL_DMA_BUF_PLANE3_PITCH_EXT << EGLint(planes[3].stride);
if (hasModifiers) {
attribs
<< EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT << EGLint(planes[3].modifier & 0xffffffff)
<< EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT << EGLint(planes[3].modifier >> 32);
}
}
attribs << EGL_NONE;
static auto eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC) eglGetProcAddress("eglCreateImageKHR");
Q_ASSERT(eglCreateImageKHR);
EGLImage ret = eglCreateImageKHR(display,
EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT,
(EGLClientBuffer) nullptr,
attribs.data());
if (ret == EGL_NO_IMAGE_KHR) {
qWarning() << "invalid image" << glGetError();
}
// Q_ASSERT(ret);
return ret;
}
void PipeWireSourceItem::updateTextureDmaBuf(const QVector<DmaBufPlane>& planes, uint32_t format)
{
if (!window() || !window()->openglContext() || !m_stream) {
qWarning() << "need a window and a context" << window();
return;
}
const EGLDisplay display = static_cast<EGLDisplay>( QGuiApplication::platformNativeInterface()->nativeResourceForIntegration("egldisplay"));
if (m_image) {
static auto eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC) eglGetProcAddress("eglDestroyImageKHR");
eglDestroyImageKHR(display, m_image);
}
const auto size = m_stream->size();
m_image = createImage(display, planes, format, size);
if (m_image == EGL_NO_IMAGE_KHR) {
QImage img(200, 200, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::blue);
updateTextureImage(img);
return;
}
m_createNextTexture = [this, size] {
if (!m_texture) {
m_texture.reset(new QOpenGLTexture(QOpenGLTexture::Target2D));
bool created = m_texture->create();
Q_ASSERT(created);
}
m_texture->bind();
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES) m_image);
m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
m_texture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
m_texture->release();
m_texture->setSize(size.width(), size.height());
int textureId = m_texture->textureId();
return window()->createTextureFromNativeObject(QQuickWindow::NativeObjectTexture, &textureId, 0 /*a vulkan thing?*/, size, {});
};
if (window()->isVisible()) {
update();
}
}
void PipeWireSourceItem::updateTextureImage(const QImage& image)
{
if (!window()) {
qWarning() << "pass";
return;
}
m_createNextTexture = [this, image] {
return window()->createTextureFromImage(image, QQuickWindow::TextureIsOpaque);
};
if (window()->isVisible())
update();
}
void PipeWireSourceItem::componentComplete()
{
if (m_stream)
m_stream->setActive(isVisible());
QQuickItem::componentComplete();
}
/*
* Render a PipeWire stream into a QtQuick scene as a standard Item
* Copyright 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* 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.1 of the License, or (at your option) version 3, or any
* later version accepted by the membership of KDE e.V. (or its
* successor approved by the membership of KDE e.V.), which shall
* act as a proxy defined in Section 6 of version 3 of the license.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <QQuickItem>
#include <pipewire/pipewire.h>
#include <spa/param/format-utils.h>
#include <spa/param/video/format-utils.h>
#include <spa/param/props.h>
struct DmaBufPlane;
class PipeWireSourceStream;
class QSGTexture;
class QOpenGLTexture;
typedef void *EGLImage;
class PipeWireSourceItem : public QQuickItem
{
Q_OBJECT
/// Specify the pipewire node id that we want to play
Q_PROPERTY(uint nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged)
public:
PipeWireSourceItem(QQuickItem* parent = nullptr);
~PipeWireSourceItem() override;
QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *data) override;
Q_SCRIPTABLE QString error() const;
void setNodeId(uint nodeId);
uint nodeId() const { return m_nodeId; }
void componentComplete() override;
void releaseResources() override;
Q_SIGNALS:
void nodeIdChanged(uint nodeId);
private:
void itemChange(ItemChange change, const ItemChangeData &data) override;
void updateTextureDmaBuf(const QVector<DmaBufPlane> &plane, uint32_t format);
void updateTextureImage(const QImage &image);
void setSize(const QSize &size);
uint m_nodeId = 0;
std::function<QSGTexture*()> m_createNextTexture;
QScopedPointer<PipeWireSourceStream> m_stream;
QScopedPointer<QOpenGLTexture> m_texture;
EGLImage m_image = nullptr;
bool m_needsRecreateTexture = false;
};
/*
* 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