parent
2a7a7fd141
commit
bf4dd63531
10 changed files with 11 additions and 979 deletions
@ -1,100 +0,0 @@ |
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org> |
||||
SPDX-FileContributor: Jan Grulich <jgrulich@redhat.com> |
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later |
||||
*/ |
||||
|
||||
#include "pipewirecore.h" |
||||
#include "logging.h" |
||||
#include <KLocalizedString> |
||||
#include <QSocketNotifier> |
||||
#include <spa/utils/result.h> |
||||
|
||||
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; |
||||
} |
||||
@ -1,37 +0,0 @@ |
||||
/*
|
||||
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org> |
||||
SPDX-FileContributor: Jan Grulich <jgrulich@redhat.com> |
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later |
||||
*/ |
||||
|
||||
#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); |
||||
}; |
||||
@ -1,310 +0,0 @@ |
||||
/*
|
||||
Render a PipeWire stream into a QtQuick scene as a standard Item |
||||
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org> |
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
||||
*/ |
||||
|
||||
#include "pipewiresourceitem.h" |
||||
#include "logging.h" |
||||
#include "pipewiresourcestream.h" |
||||
|
||||
#include <QGuiApplication> |
||||
#include <QOpenGLContext> |
||||
#include <QOpenGLTexture> |
||||
#include <QQuickWindow> |
||||
#include <QRunnable> |
||||
#include <QSGImageNode> |
||||
#include <QSocketNotifier> |
||||
#include <QThread> |
||||
#include <qpa/qplatformnativeinterface.h> |
||||
|
||||
#include <EGL/egl.h> |
||||
#include <EGL/eglext.h> |
||||
#include <libdrm/drm_fourcc.h> |
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) |
||||
#include <QtPlatformHeaders/QEGLNativeContext> |
||||
#endif |
||||
|
||||
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]() { |
||||
setEnabled(isVisible()); |
||||
if (m_stream) |
||||
m_stream->setActive(isVisible()); |
||||
}); |
||||
} |
||||
|
||||
PipeWireSourceItem::~PipeWireSourceItem() |
||||
{ |
||||
} |
||||
|
||||
void PipeWireSourceItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) |
||||
{ |
||||
switch (change) { |
||||
case ItemVisibleHasChanged: |
||||
setEnabled(isVisible()); |
||||
if (m_stream) |
||||
m_stream->setActive(isVisible() && 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.release()), QQuickWindow::NoStage); |
||||
m_image = EGL_NO_IMAGE_KHR; |
||||
} |
||||
} |
||||
|
||||
void PipeWireSourceItem::setNodeId(uint nodeId) |
||||
{ |
||||
if (nodeId == m_nodeId) |
||||
return; |
||||
|
||||
m_nodeId = nodeId; |
||||
setEnabled(false); |
||||
|
||||
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.get(), &PipeWireSourceStream::dmabufTextureReceived, this, &PipeWireSourceItem::updateTextureDmaBuf); |
||||
connect(m_stream.get(), &PipeWireSourceStream::imageTextureReceived, this, &PipeWireSourceItem::updateTextureImage); |
||||
} |
||||
|
||||
Q_EMIT nodeIdChanged(nodeId); |
||||
} |
||||
|
||||
QSGNode *PipeWireSourceItem::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *) |
||||
{ |
||||
if (Q_UNLIKELY(!m_createNextTexture)) { |
||||
return node; |
||||
} |
||||
|
||||
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); |
||||
} |
||||
textureNode->setTexture(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 textureNode; |
||||
} |
||||
|
||||
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) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "invalid image" << glGetError(); |
||||
} |
||||
// Q_ASSERT(ret);
|
||||
return ret; |
||||
} |
||||
|
||||
void PipeWireSourceItem::updateTextureDmaBuf(const QVector<DmaBufPlane> &planes, uint32_t format) |
||||
{ |
||||
static auto s_glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); |
||||
if (!s_glEGLImageTargetTexture2DOES) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "glEGLImageTargetTexture2DOES is not available" << window(); |
||||
return; |
||||
} |
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) |
||||
const auto openglContext = window()->openglContext(); |
||||
#else |
||||
const auto openglContext = static_cast<QOpenGLContext *>(window()->rendererInterface()->getResource(window(), QSGRendererInterface::OpenGLContextResource)); |
||||
#endif |
||||
if (!window() || !openglContext || !m_stream) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "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, format] { |
||||
if (!m_texture) { |
||||
m_texture.reset(new QOpenGLTexture(QOpenGLTexture::Target2D)); |
||||
bool created = m_texture->create(); |
||||
Q_ASSERT(created); |
||||
} |
||||
|
||||
m_texture->bind(); |
||||
|
||||
s_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(); |
||||
QQuickWindow::CreateTextureOption textureOption = format == DRM_FORMAT_ARGB8888 ? QQuickWindow::TextureHasAlphaChannel : QQuickWindow::TextureIsOpaque; |
||||
setEnabled(true); |
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) |
||||
return window()->createTextureFromNativeObject(QQuickWindow::NativeObjectTexture, &textureId, 0 /*a vulkan thing?*/, size, textureOption); |
||||
#else |
||||
return QNativeInterface::QSGOpenGLTexture::fromNative(textureId, window(), size, textureOption); |
||||
#endif |
||||
; |
||||
}; |
||||
if (window()->isVisible()) { |
||||
update(); |
||||
} |
||||
} |
||||
|
||||
void PipeWireSourceItem::updateTextureImage(const QImage &image) |
||||
{ |
||||
if (!window()) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "pass"; |
||||
return; |
||||
} |
||||
|
||||
m_createNextTexture = [this, image] { |
||||
setEnabled(true); |
||||
return window()->createTextureFromImage(image, QQuickWindow::TextureIsOpaque); |
||||
}; |
||||
if (window()->isVisible()) |
||||
update(); |
||||
} |
||||
|
||||
void PipeWireSourceItem::componentComplete() |
||||
{ |
||||
if (m_stream) |
||||
m_stream->setActive(isVisible()); |
||||
QQuickItem::componentComplete(); |
||||
} |
||||
@ -1,60 +0,0 @@ |
||||
/*
|
||||
Render a PipeWire stream into a QtQuick scene as a standard Item |
||||
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org> |
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <QQuickItem> |
||||
#include <functional> |
||||
|
||||
#include <pipewire/pipewire.h> |
||||
#include <spa/param/format-utils.h> |
||||
#include <spa/param/props.h> |
||||
#include <spa/param/video/format-utils.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); |
||||
|
||||
uint m_nodeId = 0; |
||||
std::function<QSGTexture *()> m_createNextTexture; |
||||
std::unique_ptr<PipeWireSourceStream> m_stream; |
||||
std::unique_ptr<QOpenGLTexture> m_texture; |
||||
|
||||
EGLImage m_image = nullptr; |
||||
bool m_needsRecreateTexture = false; |
||||
}; |
||||
@ -1,373 +0,0 @@ |
||||
/*
|
||||
SPDX-FileCopyrightText: 2018-2020 Red Hat Inc |
||||
SPDX-FileCopyrightText: 2020-2021 Aleix Pol Gonzalez <aleixpol@kde.org> |
||||
SPDX-FileContributor: Jan Grulich <jgrulich@redhat.com> |
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later |
||||
*/ |
||||
|
||||
#include "pipewiresourcestream.h" |
||||
#include "logging.h" |
||||
#include "pipewirecore.h" |
||||
|
||||
#include <fcntl.h> |
||||
#include <libdrm/drm_fourcc.h> |
||||
#include <spa/utils/result.h> |
||||
#include <sys/ioctl.h> |
||||
#include <sys/mman.h> |
||||
#include <unistd.h> |
||||
|
||||
#include <QGuiApplication> |
||||
#include <QLoggingCategory> |
||||
#include <QOpenGLTexture> |
||||
#include <QSocketNotifier> |
||||
#include <QVersionNumber> |
||||
#include <qpa/qplatformnativeinterface.h> |
||||
|
||||
#include <KLocalizedString> |
||||
|
||||
#include <EGL/egl.h> |
||||
#include <EGL/eglext.h> |
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) |
||||
#include <QtPlatformHeaders/QEGLNativeContext> |
||||
#endif |
||||
#undef Status |
||||
|
||||
#if !PW_CHECK_VERSION(0, 3, 29) |
||||
#define SPA_POD_PROP_FLAG_MANDATORY (1u << 3) |
||||
#endif |
||||
#if !PW_CHECK_VERSION(0, 3, 33) |
||||
#define SPA_POD_PROP_FLAG_DONT_FIXATE (1u << 4) |
||||
#endif |
||||
|
||||
static uint32_t SpaPixelFormatToDrmFormat(uint32_t spa_format) |
||||
{ |
||||
switch (spa_format) { |
||||
case SPA_VIDEO_FORMAT_RGBA: |
||||
return DRM_FORMAT_ABGR8888; |
||||
case SPA_VIDEO_FORMAT_RGBx: |
||||
return DRM_FORMAT_XBGR8888; |
||||
case SPA_VIDEO_FORMAT_BGRA: |
||||
return DRM_FORMAT_ARGB8888; |
||||
case SPA_VIDEO_FORMAT_BGRx: |
||||
return DRM_FORMAT_XRGB8888; |
||||
default: |
||||
return DRM_FORMAT_INVALID; |
||||
} |
||||
} |
||||
|
||||
static std::vector<uint64_t> queryDmaBufModifiers(EGLDisplay display, uint32_t format) |
||||
{ |
||||
static auto eglQueryDmaBufModifiersEXT = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT"); |
||||
static auto eglQueryDmaBufFormatsEXT = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT"); |
||||
if (!eglQueryDmaBufFormatsEXT || !eglQueryDmaBufModifiersEXT) { |
||||
return {}; |
||||
} |
||||
|
||||
uint32_t drm_format = SpaPixelFormatToDrmFormat(format); |
||||
if (drm_format == DRM_FORMAT_INVALID) { |
||||
qCDebug(PIPEWIRE_LOGGING) << "Failed to find matching DRM format." << format; |
||||
return {}; |
||||
} |
||||
|
||||
EGLint count = 0; |
||||
EGLBoolean success = eglQueryDmaBufFormatsEXT(display, 0, nullptr, &count); |
||||
|
||||
if (!success || count == 0) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "Failed to query DMA-BUF format count."; |
||||
return {}; |
||||
} |
||||
|
||||
std::vector<uint32_t> formats(count); |
||||
if (!eglQueryDmaBufFormatsEXT(display, count, reinterpret_cast<EGLint *>(formats.data()), &count)) { |
||||
if (!success) |
||||
qCWarning(PIPEWIRE_LOGGING) << "Failed to query DMA-BUF formats."; |
||||
return {}; |
||||
} |
||||
|
||||
if (std::find(formats.begin(), formats.end(), drm_format) == formats.end()) { |
||||
qCDebug(PIPEWIRE_LOGGING) << "Format " << drm_format << " not supported for modifiers."; |
||||
return {DRM_FORMAT_MOD_INVALID}; |
||||
} |
||||
|
||||
success = eglQueryDmaBufModifiersEXT(display, drm_format, 0, nullptr, nullptr, &count); |
||||
if (!success) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "Failed to query DMA-BUF modifier count."; |
||||
return {}; |
||||
} |
||||
|
||||
std::vector<uint64_t> modifiers(count); |
||||
if (count > 0) { |
||||
if (!eglQueryDmaBufModifiersEXT(display, drm_format, count, modifiers.data(), nullptr, &count)) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "Failed to query DMA-BUF modifiers."; |
||||
} |
||||
} |
||||
|
||||
// Support modifier-less buffers
|
||||
modifiers.push_back(DRM_FORMAT_MOD_INVALID); |
||||
return modifiers; |
||||
} |
||||
|
||||
void PipeWireSourceStream::onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message) |
||||
{ |
||||
PipeWireSourceStream *pw = static_cast<PipeWireSourceStream *>(data); |
||||
qCDebug(PIPEWIRE_LOGGING) << "state changed" << pw_stream_state_as_string(old) << "->" << pw_stream_state_as_string(state) << error_message; |
||||
|
||||
switch (state) { |
||||
case PW_STREAM_STATE_ERROR: |
||||
qCWarning(PIPEWIRE_LOGGING) << "Stream error: " << error_message; |
||||
break; |
||||
case PW_STREAM_STATE_PAUSED: |
||||
Q_EMIT pw->streamReady(); |
||||
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; |
||||
} |
||||
} |
||||
|
||||
static spa_pod *buildFormat(spa_pod_builder *builder, spa_video_format format, const std::vector<uint64_t> &modifiers = {}) |
||||
{ |
||||
spa_pod_frame f[2]; |
||||
const spa_rectangle pw_min_screen_bounds{1, 1}; |
||||
const spa_rectangle pw_max_screen_bounds{UINT32_MAX, UINT32_MAX}; |
||||
|
||||
spa_pod_builder_push_object(builder, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); |
||||
spa_pod_builder_add(builder, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); |
||||
spa_pod_builder_add(builder, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); |
||||
spa_pod_builder_add(builder, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); |
||||
|
||||
if (modifiers.size()) { |
||||
auto pw_version = QVersionNumber::fromString(pw_get_library_version()); |
||||
|
||||
// SPA_POD_PROP_FLAG_DONT_FIXATE can be used with PipeWire >= 0.3.33
|
||||
if (pw_version >= QVersionNumber(0, 3, 33)) { |
||||
spa_pod_builder_prop(builder, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); |
||||
} else { |
||||
spa_pod_builder_prop(builder, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY); |
||||
} |
||||
spa_pod_builder_push_choice(builder, &f[1], SPA_CHOICE_Enum, 0); |
||||
// mofifiers from the array
|
||||
for (auto it = modifiers.begin(); it != modifiers.end(); it++) { |
||||
spa_pod_builder_long(builder, *it); |
||||
if (it == modifiers.begin()) { |
||||
spa_pod_builder_long(builder, *it); |
||||
} |
||||
} |
||||
spa_pod_builder_pop(builder, &f[1]); |
||||
} |
||||
|
||||
spa_pod_builder_add(builder, SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&pw_min_screen_bounds, &pw_min_screen_bounds, &pw_max_screen_bounds), 0); |
||||
|
||||
return static_cast<spa_pod *>(spa_pod_builder_pop(builder, &f[0])); |
||||
} |
||||
|
||||
void PipeWireSourceStream::onStreamParamChanged(void *data, uint32_t id, const struct spa_pod *format) |
||||
{ |
||||
if (!format || id != SPA_PARAM_Format) { |
||||
return; |
||||
} |
||||
|
||||
PipeWireSourceStream *pw = static_cast<PipeWireSourceStream *>(data); |
||||
spa_format_video_raw_parse(format, &pw->videoFormat); |
||||
|
||||
const int32_t width = pw->videoFormat.size.width; |
||||
const int32_t height = pw->videoFormat.size.height; |
||||
const int bpp = pw->videoFormat.format == SPA_VIDEO_FORMAT_RGB || pw->videoFormat.format == SPA_VIDEO_FORMAT_BGR ? 3 : 4; |
||||
const quint32 stride = SPA_ROUND_UP_N(width * bpp, 4); |
||||
qCDebug(PIPEWIRE_LOGGING) << "Stream format changed"; |
||||
const int32_t size = height * stride; |
||||
|
||||
uint8_t paramsBuffer[1024]; |
||||
spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(paramsBuffer, sizeof(paramsBuffer)); |
||||
|
||||
const auto bufferTypes = pw->m_allowDmaBuf && spa_pod_find_prop(format, nullptr, SPA_FORMAT_VIDEO_modifier) |
||||
? (1 << SPA_DATA_DmaBuf) | (1 << SPA_DATA_MemFd) | (1 << SPA_DATA_MemPtr) |
||||
: (1 << SPA_DATA_MemFd) | (1 << SPA_DATA_MemPtr); |
||||
|
||||
const spa_pod *param = (spa_pod *)spa_pod_builder_add_object(&pod_builder, |
||||
SPA_TYPE_OBJECT_ParamBuffers, |
||||
SPA_PARAM_Buffers, |
||||
SPA_PARAM_BUFFERS_buffers, |
||||
SPA_POD_CHOICE_RANGE_Int(16, 2, 16), |
||||
SPA_PARAM_BUFFERS_blocks, |
||||
SPA_POD_Int(1), |
||||
SPA_PARAM_BUFFERS_size, |
||||
SPA_POD_Int(size), |
||||
SPA_PARAM_BUFFERS_stride, |
||||
SPA_POD_CHOICE_RANGE_Int(stride, stride, INT32_MAX), |
||||
SPA_PARAM_BUFFERS_align, |
||||
SPA_POD_Int(16), |
||||
SPA_PARAM_BUFFERS_dataType, |
||||
SPA_POD_CHOICE_FLAGS_Int(bufferTypes)); |
||||
pw_stream_update_params(pw->pwStream, ¶m, 1); |
||||
} |
||||
|
||||
static void onProcess(void *data) |
||||
{ |
||||
PipeWireSourceStream *stream = static_cast<PipeWireSourceStream *>(data); |
||||
stream->process(); |
||||
} |
||||
|
||||
PipeWireSourceStream::PipeWireSourceStream(QObject *parent) |
||||
: QObject(parent) |
||||
{ |
||||
pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; |
||||
pwStreamEvents.process = &onProcess; |
||||
pwStreamEvents.state_changed = &PipeWireSourceStream::onStreamStateChanged; |
||||
pwStreamEvents.param_changed = &PipeWireSourceStream::onStreamParamChanged; |
||||
} |
||||
|
||||
PipeWireSourceStream::~PipeWireSourceStream() |
||||
{ |
||||
m_stopped = true; |
||||
if (pwStream) { |
||||
pw_stream_destroy(pwStream); |
||||
} |
||||
} |
||||
|
||||
uint PipeWireSourceStream::framerate() |
||||
{ |
||||
if (pwStream) { |
||||
return videoFormat.max_framerate.num / videoFormat.max_framerate.denom; |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
uint PipeWireSourceStream::nodeId() |
||||
{ |
||||
return pwNodeId; |
||||
} |
||||
|
||||
bool PipeWireSourceStream::createStream(uint nodeid) |
||||
{ |
||||
pwCore = PipeWireCore::self(); |
||||
if (!pwCore->m_error.isEmpty()) { |
||||
m_error = pwCore->m_error; |
||||
return false; |
||||
} |
||||
|
||||
connect(pwCore.data(), &PipeWireCore::pipewireFailed, this, &PipeWireSourceStream::coreFailed); |
||||
|
||||
pwStream = pw_stream_new(pwCore->pwCore, "plasma-screencast", nullptr); |
||||
pwNodeId = nodeid; |
||||
pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); |
||||
|
||||
uint8_t buffer[4096]; |
||||
spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); |
||||
|
||||
const QVector<spa_video_format> formats = |
||||
{SPA_VIDEO_FORMAT_RGBx, SPA_VIDEO_FORMAT_RGBA, SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_RGB, SPA_VIDEO_FORMAT_BGR}; |
||||
QVector<const spa_pod *> params; |
||||
params.reserve(formats.size() * 2); |
||||
const EGLDisplay display = static_cast<EGLDisplay>(QGuiApplication::platformNativeInterface()->nativeResourceForIntegration("egldisplay")); |
||||
for (spa_video_format format : formats) { |
||||
if (m_allowDmaBuf) { |
||||
if (auto modifiers = queryDmaBufModifiers(display, format); modifiers.size() > 0) { |
||||
params += buildFormat(&podBuilder, format, modifiers); |
||||
} |
||||
} |
||||
|
||||
params += buildFormat(&podBuilder, format, {}); |
||||
} |
||||
|
||||
pw_stream_flags s = (pw_stream_flags)(PW_STREAM_FLAG_DONT_RECONNECT | PW_STREAM_FLAG_AUTOCONNECT); |
||||
if (pw_stream_connect(pwStream, PW_DIRECTION_INPUT, pwNodeId, s, params.data(), params.size()) != 0) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "Could not connect to stream"; |
||||
pw_stream_destroy(pwStream); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
void PipeWireSourceStream::handleFrame(struct pw_buffer *buffer) |
||||
{ |
||||
spa_buffer *spaBuffer = buffer->buffer; |
||||
|
||||
if (spaBuffer->datas->chunk->size == 0) { |
||||
return; |
||||
} |
||||
|
||||
if (spaBuffer->datas->type == SPA_DATA_MemFd) { |
||||
uint8_t *map = |
||||
static_cast<uint8_t *>(mmap(nullptr, spaBuffer->datas->maxsize + spaBuffer->datas->mapoffset, PROT_READ, MAP_PRIVATE, spaBuffer->datas->fd, 0)); |
||||
|
||||
if (map == MAP_FAILED) { |
||||
qCWarning(PIPEWIRE_LOGGING) << "Failed to mmap the memory: " << strerror(errno); |
||||
return; |
||||
} |
||||
const QImage::Format format = spaBuffer->datas->chunk->stride / videoFormat.size.width == 3 ? QImage::Format_RGB888 : QImage::Format_ARGB32; |
||||
|
||||
QImage img(map, videoFormat.size.width, videoFormat.size.height, spaBuffer->datas->chunk->stride, format); |
||||
Q_EMIT imageTextureReceived(img.copy()); |
||||
|
||||
munmap(map, spaBuffer->datas->maxsize + spaBuffer->datas->mapoffset); |
||||
} else if (spaBuffer->datas->type == SPA_DATA_DmaBuf) { |
||||
QVector<DmaBufPlane> planes; |
||||
planes.reserve(spaBuffer->n_datas); |
||||
for (uint i = 0; i < spaBuffer->n_datas; ++i) { |
||||
const auto &data = spaBuffer->datas[i]; |
||||
|
||||
DmaBufPlane plane; |
||||
plane.fd = data.fd; |
||||
plane.stride = data.chunk->stride; |
||||
plane.offset = data.chunk->offset; |
||||
plane.modifier = DRM_FORMAT_MOD_INVALID; |
||||
planes += plane; |
||||
} |
||||
Q_EMIT dmabufTextureReceived(planes, DRM_FORMAT_ARGB8888); |
||||
} else if (spaBuffer->datas->type == SPA_DATA_MemPtr) { |
||||
QImage img(static_cast<uint8_t *>(spaBuffer->datas->data), |
||||
videoFormat.size.width, |
||||
videoFormat.size.height, |
||||
spaBuffer->datas->chunk->stride, |
||||
QImage::Format_ARGB32); |
||||
Q_EMIT imageTextureReceived(img); |
||||
} else { |
||||
qCWarning(PIPEWIRE_LOGGING) << "unsupported buffer type" << spaBuffer->datas->type; |
||||
QImage errorImage(200, 200, QImage::Format_ARGB32_Premultiplied); |
||||
errorImage.fill(Qt::red); |
||||
Q_EMIT imageTextureReceived(errorImage); |
||||
} |
||||
} |
||||
|
||||
void PipeWireSourceStream::coreFailed(const QString &errorMessage) |
||||
{ |
||||
m_error = errorMessage; |
||||
Q_EMIT stopStreaming(); |
||||
} |
||||
|
||||
void PipeWireSourceStream::process() |
||||
{ |
||||
pw_buffer *buf = pw_stream_dequeue_buffer(pwStream); |
||||
if (!buf) { |
||||
return; |
||||
} |
||||
|
||||
handleFrame(buf); |
||||
|
||||
pw_stream_queue_buffer(pwStream, buf); |
||||
} |
||||
|
||||
void PipeWireSourceStream::stop() |
||||
{ |
||||
if (!m_stopped) |
||||
pw_stream_set_active(pwStream, false); |
||||
m_stopped = true; |
||||
delete this; |
||||
} |
||||
|
||||
void PipeWireSourceStream::setActive(bool active) |
||||
{ |
||||
Q_ASSERT(pwStream); |
||||
pw_stream_set_active(pwStream, active); |
||||
} |
||||
@ -1,91 +0,0 @@ |
||||
/*
|
||||
SPDX-FileCopyrightText: 2018-2020 Red Hat Inc |
||||
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org> |
||||
SPDX-FileContributor: Jan Grulich <jgrulich@redhat.com> |
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <QHash> |
||||
#include <QObject> |
||||
#include <QSharedPointer> |
||||
#include <QSize> |
||||
|
||||
#include <pipewire/pipewire.h> |
||||
#include <spa/param/format-utils.h> |
||||
#include <spa/param/props.h> |
||||
#include <spa/param/video/format-utils.h> |
||||
|
||||
#undef Status |
||||
|
||||
namespace KWin |
||||
{ |
||||
class AbstractEglBackend; |
||||
class GLTexture; |
||||
} |
||||
class PipeWireCore; |
||||
|
||||
typedef void *EGLDisplay; |
||||
|
||||
struct DmaBufPlane { |
||||
int fd; /// The dmabuf file descriptor
|
||||
uint32_t offset; /// The offset from the start of buffer
|
||||
uint32_t stride; /// The distance from the start of a row to the next row in bytes
|
||||
uint64_t modifier = 0; /// The layout modifier
|
||||
}; |
||||
|
||||
class PipeWireSourceStream : public QObject |
||||
{ |
||||
Q_OBJECT |
||||
public: |
||||
explicit PipeWireSourceStream(QObject *parent); |
||||
~PipeWireSourceStream(); |
||||
|
||||
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); |
||||
|
||||
uint framerate(); |
||||
uint nodeId(); |
||||
QString error() const |
||||
{ |
||||
return m_error; |
||||
} |
||||
|
||||
QSize size() const |
||||
{ |
||||
return QSize(videoFormat.size.width, videoFormat.size.height); |
||||
} |
||||
bool createStream(uint nodeid); |
||||
void stop(); |
||||
void setActive(bool active); |
||||
|
||||
void handleFrame(struct pw_buffer *buffer); |
||||
void process(); |
||||
|
||||
bool setAllowDmaBuf(bool allowed); |
||||
|
||||
Q_SIGNALS: |
||||
void streamReady(); |
||||
void startStreaming(); |
||||
void stopStreaming(); |
||||
void dmabufTextureReceived(const QVector<DmaBufPlane> &planes, uint32_t format); |
||||
void imageTextureReceived(const QImage &image); |
||||
|
||||
private: |
||||
void coreFailed(const QString &errorMessage); |
||||
|
||||
QSharedPointer<PipeWireCore> pwCore; |
||||
pw_stream *pwStream = nullptr; |
||||
spa_hook streamListener; |
||||
pw_stream_events pwStreamEvents = {}; |
||||
|
||||
uint32_t pwNodeId = 0; |
||||
|
||||
bool m_stopped = false; |
||||
|
||||
spa_video_info_raw videoFormat; |
||||
QString m_error; |
||||
bool m_allowDmaBuf = true; |
||||
}; |
||||
Loading…
Reference in new issue