File indexing completed on 2024-04-28 09:21:51

0001 /*
0002     Render a PipeWire stream into a QtQuick scene as a standard Item
0003     SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "pipewiresourceitem.h"
0009 #include "glhelpers.h"
0010 #include "logging.h"
0011 #include "pipewiresourcestream.h"
0012 
0013 #include <QGuiApplication>
0014 #include <QOpenGLContext>
0015 #include <QOpenGLTexture>
0016 #include <QPainter>
0017 #include <QQuickWindow>
0018 #include <QRunnable>
0019 #include <QSGImageNode>
0020 #include <QSocketNotifier>
0021 #include <QThread>
0022 #include <qpa/qplatformnativeinterface.h>
0023 
0024 #include <EGL/eglext.h>
0025 #include <fcntl.h>
0026 #include <libdrm/drm_fourcc.h>
0027 #include <unistd.h>
0028 
0029 static void pwInit()
0030 {
0031     pw_init(nullptr, nullptr);
0032 }
0033 Q_COREAPP_STARTUP_FUNCTION(pwInit);
0034 
0035 class PipeWireSourceItemPrivate
0036 {
0037 public:
0038     uint m_nodeId = 0;
0039     std::optional<uint> m_fd;
0040     std::function<QSGTexture *()> m_createNextTexture;
0041     QScopedPointer<PipeWireSourceStream> m_stream;
0042     QScopedPointer<QOpenGLTexture> m_texture;
0043 
0044     EGLImage m_image = nullptr;
0045     bool m_needsRecreateTexture = false;
0046     bool m_allowDmaBuf = true;
0047 
0048     struct {
0049         QImage texture;
0050         std::optional<QPoint> position;
0051         QPoint hotspot;
0052         bool dirty = false;
0053     } m_cursor;
0054     std::optional<QRegion> m_damage;
0055 };
0056 
0057 class DiscardEglPixmapRunnable : public QRunnable
0058 {
0059 public:
0060     DiscardEglPixmapRunnable(EGLImageKHR image, QOpenGLTexture *texture)
0061         : m_image(image)
0062         , m_texture(texture)
0063     {
0064     }
0065 
0066     void run() override
0067     {
0068         if (m_image != EGL_NO_IMAGE_KHR) {
0069             eglDestroyImageKHR(eglGetCurrentDisplay(), m_image);
0070         }
0071 
0072         delete m_texture;
0073     }
0074 
0075 private:
0076     const EGLImageKHR m_image;
0077     QOpenGLTexture *m_texture;
0078 };
0079 
0080 PipeWireSourceItem::PipeWireSourceItem(QQuickItem *parent)
0081     : QQuickItem(parent)
0082     , d(new PipeWireSourceItemPrivate)
0083 {
0084     setFlag(ItemHasContents, true);
0085 
0086     connect(this, &QQuickItem::visibleChanged, this, [this]() {
0087         setEnabled(isVisible());
0088         if (d->m_stream)
0089             d->m_stream->setActive(isVisible());
0090     });
0091 }
0092 
0093 PipeWireSourceItem::~PipeWireSourceItem()
0094 {
0095     if (d->m_fd) {
0096         close(*d->m_fd);
0097     }
0098 }
0099 
0100 void PipeWireSourceItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
0101 {
0102     switch (change) {
0103     case ItemVisibleHasChanged:
0104         setEnabled(isVisible());
0105         if (d->m_stream)
0106             d->m_stream->setActive(isVisible() && data.boolValue && isComponentComplete());
0107         break;
0108     case ItemSceneChange:
0109         d->m_needsRecreateTexture = true;
0110         releaseResources();
0111         break;
0112     default:
0113         break;
0114     }
0115 
0116     QQuickItem::itemChange(change, data);
0117 }
0118 
0119 void PipeWireSourceItem::releaseResources()
0120 {
0121     if (window() && (d->m_image || d->m_texture)) {
0122         window()->scheduleRenderJob(new DiscardEglPixmapRunnable(d->m_image, d->m_texture.take()), QQuickWindow::NoStage);
0123         d->m_image = EGL_NO_IMAGE_KHR;
0124     }
0125 }
0126 
0127 void PipeWireSourceItem::setFd(uint fd)
0128 {
0129     if (fd == d->m_fd)
0130         return;
0131 
0132     if (d->m_fd) {
0133         close(*d->m_fd);
0134     }
0135     d->m_fd = fd;
0136     refresh();
0137     Q_EMIT fdChanged(fd);
0138 }
0139 
0140 void PipeWireSourceItem::resetFd()
0141 {
0142     if (!d->m_fd.has_value()) {
0143         return;
0144     }
0145 
0146     setEnabled(false);
0147     close(*d->m_fd);
0148     d->m_fd.reset();
0149     d->m_stream.reset(nullptr);
0150     d->m_createNextTexture = [] {
0151         return nullptr;
0152     };
0153     Q_EMIT streamSizeChanged();
0154 }
0155 
0156 void PipeWireSourceItem::refresh()
0157 {
0158     setEnabled(false);
0159 
0160     if (!isComponentComplete()) {
0161         return;
0162     }
0163 
0164     if (d->m_nodeId == 0) {
0165         releaseResources();
0166         d->m_stream.reset(nullptr);
0167         Q_EMIT streamSizeChanged();
0168 
0169         d->m_createNextTexture = [] {
0170             return nullptr;
0171         };
0172     } else {
0173         d->m_stream.reset(new PipeWireSourceStream(this));
0174         d->m_stream->setAllowDmaBuf(d->m_allowDmaBuf);
0175         Q_EMIT streamSizeChanged();
0176         connect(d->m_stream.get(), &PipeWireSourceStream::streamParametersChanged, this, &PipeWireSourceItem::streamSizeChanged);
0177         connect(d->m_stream.get(), &PipeWireSourceStream::streamParametersChanged, this, &PipeWireSourceItem::usingDmaBufChanged);
0178 
0179         d->m_stream->createStream(d->m_nodeId, d->m_fd.value_or(0));
0180         if (!d->m_stream->error().isEmpty()) {
0181             d->m_stream.reset(nullptr);
0182             d->m_nodeId = 0;
0183             return;
0184         }
0185         d->m_stream->setActive(isVisible() && isComponentComplete());
0186 
0187         connect(d->m_stream.data(), &PipeWireSourceStream::frameReceived, this, &PipeWireSourceItem::processFrame);
0188         connect(d->m_stream.data(), &PipeWireSourceStream::stateChanged, this, &PipeWireSourceItem::stateChanged);
0189     }
0190     Q_EMIT stateChanged();
0191 }
0192 
0193 void PipeWireSourceItem::setNodeId(uint nodeId)
0194 {
0195     if (nodeId == d->m_nodeId)
0196         return;
0197 
0198     d->m_nodeId = nodeId;
0199     refresh();
0200     Q_EMIT nodeIdChanged(nodeId);
0201 }
0202 
0203 class PipeWireRenderNode : public QSGNode
0204 {
0205 public:
0206     QSGImageNode *screenNode(QQuickWindow *window)
0207     {
0208         if (!m_screenNode) {
0209             m_screenNode = window->createImageNode();
0210             appendChildNode(m_screenNode);
0211         }
0212         return m_screenNode;
0213     }
0214     QSGImageNode *cursorNode(QQuickWindow *window)
0215     {
0216         if (!m_cursorNode) {
0217             m_cursorNode = window->createImageNode();
0218             appendChildNode(m_cursorNode);
0219         }
0220         return m_cursorNode;
0221     }
0222 
0223     QSGImageNode *damageNode(QQuickWindow *window)
0224     {
0225         if (!m_damageNode) {
0226             m_damageNode = window->createImageNode();
0227             appendChildNode(m_damageNode);
0228         }
0229         return m_damageNode;
0230     }
0231 
0232     void discardCursor()
0233     {
0234         if (m_cursorNode) {
0235             removeChildNode(m_cursorNode);
0236             delete m_cursorNode;
0237             m_cursorNode = nullptr;
0238         }
0239     }
0240 
0241     void discardDamage()
0242     {
0243         if (m_damageNode) {
0244             removeChildNode(m_damageNode);
0245             delete m_damageNode;
0246             m_damageNode = nullptr;
0247         }
0248     }
0249 
0250 private:
0251     QSGImageNode *m_screenNode = nullptr;
0252     QSGImageNode *m_cursorNode = nullptr;
0253     QSGImageNode *m_damageNode = nullptr;
0254 };
0255 
0256 QSGNode *PipeWireSourceItem::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *)
0257 {
0258     if (Q_UNLIKELY(!d->m_createNextTexture)) {
0259         return node;
0260     }
0261 
0262     auto texture = d->m_createNextTexture();
0263     if (!texture) {
0264         delete node;
0265         return nullptr;
0266     }
0267 
0268     QSGImageNode *screenNode;
0269     auto pwNode = dynamic_cast<PipeWireRenderNode *>(node);
0270     if (!pwNode) {
0271         delete node;
0272         pwNode = new PipeWireRenderNode;
0273         screenNode = window()->createImageNode();
0274         screenNode->setOwnsTexture(true);
0275         pwNode->appendChildNode(screenNode);
0276     } else {
0277         screenNode = static_cast<QSGImageNode *>(pwNode->childAtIndex(0));
0278     }
0279     screenNode->setTexture(texture);
0280 
0281     const auto br = boundingRect().toRect();
0282     QRect rect({0, 0}, texture->textureSize().scaled(br.size(), Qt::KeepAspectRatio));
0283     rect.moveCenter(br.center());
0284     screenNode->setRect(rect);
0285 
0286     if (!d->m_cursor.position.has_value() || d->m_cursor.texture.isNull()) {
0287         pwNode->discardCursor();
0288     } else {
0289         QSGImageNode *cursorNode = pwNode->cursorNode(window());
0290         if (d->m_cursor.dirty || !cursorNode->texture()) {
0291             cursorNode->setTexture(window()->createTextureFromImage(d->m_cursor.texture));
0292             d->m_cursor.dirty = false;
0293         }
0294         const qreal scale = qreal(rect.width()) / texture->textureSize().width();
0295         cursorNode->setRect(QRectF{rect.topLeft() + (d->m_cursor.position.value() * scale), d->m_cursor.texture.size() * scale});
0296         Q_ASSERT(cursorNode->texture());
0297     }
0298 
0299     if (!d->m_damage || d->m_damage->isEmpty()) {
0300         pwNode->discardDamage();
0301     } else {
0302         auto *damageNode = pwNode->damageNode(window());
0303         QImage damageImage(texture->textureSize(), QImage::Format_RGBA64_Premultiplied);
0304         damageImage.fill(Qt::transparent);
0305         QPainter p(&damageImage);
0306         p.setBrush(Qt::red);
0307         for (auto rect : *d->m_damage) {
0308             p.drawRect(rect);
0309         }
0310         damageNode->setTexture(window()->createTextureFromImage(damageImage));
0311         damageNode->setRect(rect);
0312         Q_ASSERT(damageNode->texture());
0313     }
0314     return pwNode;
0315 }
0316 
0317 QString PipeWireSourceItem::error() const
0318 {
0319     return d->m_stream->error();
0320 }
0321 
0322 void PipeWireSourceItem::processFrame(const PipeWireFrame &frame)
0323 {
0324     d->m_damage = frame.damage;
0325 
0326     if (frame.cursor) {
0327         d->m_cursor.position = frame.cursor->position;
0328         d->m_cursor.hotspot = frame.cursor->hotspot;
0329         if (!frame.cursor->texture.isNull()) {
0330             d->m_cursor.dirty = true;
0331             d->m_cursor.texture = frame.cursor->texture;
0332         }
0333     } else {
0334         d->m_cursor.position = std::nullopt;
0335         d->m_cursor.hotspot = {};
0336     }
0337 
0338     if (frame.dmabuf) {
0339         updateTextureDmaBuf(*frame.dmabuf, frame.format);
0340     } else if (frame.image) {
0341         updateTextureImage(*frame.image);
0342     }
0343 
0344     if (window() && window()->isVisible()) {
0345         update();
0346     }
0347 }
0348 
0349 void PipeWireSourceItem::updateTextureDmaBuf(const DmaBufAttributes &attribs, spa_video_format format)
0350 {
0351     if (!window()) {
0352         qCWarning(PIPEWIRE_LOGGING) << "Window not available" << this;
0353         return;
0354     }
0355 
0356     const auto openglContext = static_cast<QOpenGLContext *>(window()->rendererInterface()->getResource(window(), QSGRendererInterface::OpenGLContextResource));
0357     if (!openglContext || !d->m_stream) {
0358         qCWarning(PIPEWIRE_LOGGING) << "need a window and a context" << window();
0359         return;
0360     }
0361 
0362     d->m_createNextTexture = [this, format, attribs]() -> QSGTexture * {
0363         const EGLDisplay display = static_cast<EGLDisplay>(QGuiApplication::platformNativeInterface()->nativeResourceForIntegration("egldisplay"));
0364         if (d->m_image) {
0365             eglDestroyImageKHR(display, d->m_image);
0366         }
0367         const auto size = d->m_stream->size();
0368         d->m_image = GLHelpers::createImage(display, attribs, PipeWireSourceStream::spaVideoFormatToDrmFormat(format), size, nullptr);
0369         if (d->m_image == EGL_NO_IMAGE_KHR) {
0370             d->m_stream->renegotiateModifierFailed(format, attribs.modifier);
0371             return nullptr;
0372         }
0373         if (!d->m_texture) {
0374             d->m_texture.reset(new QOpenGLTexture(QOpenGLTexture::Target2D));
0375             bool created = d->m_texture->create();
0376             Q_ASSERT(created);
0377         }
0378 
0379         GLHelpers::initDebugOutput();
0380         d->m_texture->bind();
0381 
0382         glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)d->m_image);
0383 
0384         d->m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
0385         d->m_texture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
0386         d->m_texture->release();
0387         d->m_texture->setSize(size.width(), size.height());
0388 
0389         int textureId = d->m_texture->textureId();
0390         QQuickWindow::CreateTextureOption textureOption =
0391             format == SPA_VIDEO_FORMAT_ARGB || format == SPA_VIDEO_FORMAT_BGRA ? QQuickWindow::TextureHasAlphaChannel : QQuickWindow::TextureIsOpaque;
0392         setEnabled(true);
0393         return QNativeInterface::QSGOpenGLTexture::fromNative(textureId, window(), size, textureOption);
0394     };
0395 }
0396 
0397 void PipeWireSourceItem::updateTextureImage(const QImage &image)
0398 {
0399     if (!window()) {
0400         qCWarning(PIPEWIRE_LOGGING) << "pass";
0401         return;
0402     }
0403 
0404     d->m_createNextTexture = [this, image] {
0405         setEnabled(true);
0406         return window()->createTextureFromImage(image, QQuickWindow::TextureIsOpaque);
0407     };
0408 }
0409 
0410 void PipeWireSourceItem::componentComplete()
0411 {
0412     QQuickItem::componentComplete();
0413     if (d->m_nodeId != 0) {
0414         refresh();
0415     }
0416 }
0417 
0418 PipeWireSourceItem::StreamState PipeWireSourceItem::state() const
0419 {
0420     if (!d->m_stream) {
0421         return StreamState::Unconnected;
0422     }
0423     switch (d->m_stream->state()) {
0424     case PW_STREAM_STATE_ERROR:
0425         return StreamState::Error;
0426     case PW_STREAM_STATE_UNCONNECTED:
0427         return StreamState::Unconnected;
0428     case PW_STREAM_STATE_CONNECTING:
0429         return StreamState::Connecting;
0430     case PW_STREAM_STATE_PAUSED:
0431         return StreamState::Paused;
0432     case PW_STREAM_STATE_STREAMING:
0433         return StreamState::Streaming;
0434     default:
0435         return StreamState::Error;
0436     }
0437 }
0438 
0439 uint PipeWireSourceItem::fd() const
0440 {
0441     return d->m_fd.value_or(0);
0442 }
0443 
0444 uint PipeWireSourceItem::nodeId() const
0445 {
0446     return d->m_nodeId;
0447 }
0448 
0449 QSize PipeWireSourceItem::streamSize() const
0450 {
0451     if (!d->m_stream) {
0452         return QSize();
0453     }
0454     return d->m_stream->size();
0455 }
0456 
0457 bool PipeWireSourceItem::usingDmaBuf() const
0458 {
0459     return d->m_stream && d->m_stream->usingDmaBuf();
0460 }
0461 
0462 bool PipeWireSourceItem::allowDmaBuf() const
0463 {
0464     return d->m_stream && d->m_stream->allowDmaBuf();
0465 }
0466 
0467 void PipeWireSourceItem::setAllowDmaBuf(bool allowed)
0468 {
0469     d->m_allowDmaBuf = allowed;
0470     if (d->m_stream) {
0471         d->m_stream->setAllowDmaBuf(allowed);
0472     }
0473 }
0474 
0475 #include "moc_pipewiresourceitem.cpp"