File indexing completed on 2024-05-12 09:01:26

0001 /* This file is part of the KDE project
0002    Copyright (C) 2018-2021 Jan Grulich <jgrulich@redhat.com>
0003    Copyright (C) 2018 Oleg Chernovskiy <kanedias@xaker.ru>
0004 
0005    This program is free software; you can redistribute it and/or
0006    modify it under the terms of the GNU General Public
0007    License as published by the Free Software Foundation; either
0008    version 3 of the License, or (at your option) any later version.
0009 */
0010 
0011 #include "config-krfb.h"
0012 
0013 // system
0014 #include <sys/mman.h>
0015 #include <cstring>
0016 
0017 // Qt
0018 #include <QCoreApplication>
0019 #include <QGuiApplication>
0020 #include <QScreen>
0021 #include <QSocketNotifier>
0022 #include <QDebug>
0023 #include <QRandomGenerator>
0024 
0025 #include <KWayland/Client/connection_thread.h>
0026 #include <KWayland/Client/registry.h>
0027 
0028 // pipewire
0029 #include <climits>
0030 
0031 #include "pw_framebuffer.h"
0032 #include "xdp_dbus_screencast_interface.h"
0033 #include "xdp_dbus_remotedesktop_interface.h"
0034 #include "krfb_fb_pipewire_debug.h"
0035 #include "screencasting.h"
0036 #include <PipeWireSourceStream>
0037 #include <kpipewire_version.h>
0038 #include <DmaBufHandler>
0039 
0040 static const int BYTES_PER_PIXEL = 4;
0041 static const uint MIN_SUPPORTED_XDP_KDE_SC_VERSION = 1;
0042 
0043 Q_DECLARE_METATYPE(PWFrameBuffer::Stream);
0044 Q_DECLARE_METATYPE(PWFrameBuffer::Streams);
0045 
0046 const QDBusArgument &operator >> (const QDBusArgument &arg, PWFrameBuffer::Stream &stream)
0047 {
0048     arg.beginStructure();
0049     arg >> stream.nodeId;
0050 
0051     arg.beginMap();
0052     while (!arg.atEnd()) {
0053         QString key;
0054         QVariant map;
0055         arg.beginMapEntry();
0056         arg >> key >> map;
0057         arg.endMapEntry();
0058         stream.map.insert(key, map);
0059     }
0060     arg.endMap();
0061     arg.endStructure();
0062 
0063     return arg;
0064 }
0065 
0066 /**
0067  * @brief The PWFrameBuffer::Private class - private counterpart of PWFramebuffer class. This is the entity where
0068  *        whole logic resides, for more info search for "d-pointer pattern" information.
0069  */
0070 class PWFrameBuffer::Private {
0071 public:
0072     Private(PWFrameBuffer *q);
0073     ~Private();
0074 
0075 private:
0076     friend class PWFrameBuffer;
0077 
0078     void initDbus();
0079 
0080     // dbus handling
0081     void handleSessionCreated(quint32 code, const QVariantMap &results);
0082     void handleDevicesSelected(quint32 code, const QVariantMap &results);
0083     void handleSourcesSelected(quint32 code, const QVariantMap &results);
0084     void handleRemoteDesktopStarted(quint32 code, const QVariantMap &results);
0085     void setVideoSize(const QSize &size);
0086 
0087     // pw handling
0088     void handleFrame(const PipeWireFrame &frame);
0089 
0090     // link to public interface
0091     PWFrameBuffer *q;
0092 
0093     // requests a session from XDG Desktop Portal
0094     // auto-generated and compiled from xdp_dbus_interface.xml file
0095     QScopedPointer<OrgFreedesktopPortalScreenCastInterface> dbusXdpScreenCastService;
0096     QScopedPointer<OrgFreedesktopPortalRemoteDesktopInterface> dbusXdpRemoteDesktopService;
0097 
0098     // XDP screencast session handle
0099     QDBusObjectPath sessionPath;
0100 
0101     // screen geometry holder
0102     QSize videoSize;
0103 
0104     // sanity indicator
0105     bool isValid = true;
0106     std::unique_ptr<PipeWireSourceStream> stream;
0107     std::optional<PipeWireCursor> cursor;
0108     DmaBufHandler m_dmabufHandler;
0109 };
0110 
0111 PWFrameBuffer::Private::Private(PWFrameBuffer *q)
0112     : q(q)
0113     , stream(new PipeWireSourceStream(q))
0114 {
0115     QObject::connect(stream.get(), &PipeWireSourceStream::frameReceived, q, [this] (const PipeWireFrame &frame) {
0116         handleFrame(frame);
0117     });
0118 }
0119 
0120 /**
0121  * @brief PWFrameBuffer::Private::initDbus - initialize D-Bus connectivity with XDG Desktop Portal.
0122  *        Based on XDG_CURRENT_DESKTOP environment variable it will give us implementation that we need,
0123  *        in case of KDE it is xdg-desktop-portal-kde binary.
0124  */
0125 void PWFrameBuffer::Private::initDbus()
0126 {
0127     qInfo() << "Initializing D-Bus connectivity with XDG Desktop Portal";
0128     dbusXdpScreenCastService.reset(new OrgFreedesktopPortalScreenCastInterface(QStringLiteral("org.freedesktop.portal.Desktop"),
0129                                                                      QStringLiteral("/org/freedesktop/portal/desktop"),
0130                                                                      QDBusConnection::sessionBus()));
0131     dbusXdpRemoteDesktopService.reset(new OrgFreedesktopPortalRemoteDesktopInterface(QStringLiteral("org.freedesktop.portal.Desktop"),
0132                                                                      QStringLiteral("/org/freedesktop/portal/desktop"),
0133                                                                      QDBusConnection::sessionBus()));
0134     auto version = dbusXdpScreenCastService->version();
0135     if (version < MIN_SUPPORTED_XDP_KDE_SC_VERSION) {
0136         qCWarning(KRFB_FB_PIPEWIRE) << "Unsupported XDG Portal screencast interface version:" << version;
0137         isValid = false;
0138         return;
0139     }
0140 
0141     // create session
0142     auto sessionParameters = QVariantMap {
0143         { QStringLiteral("session_handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) },
0144         { QStringLiteral("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) }
0145     };
0146     auto sessionReply = dbusXdpRemoteDesktopService->CreateSession(sessionParameters);
0147     sessionReply.waitForFinished();
0148     if (!sessionReply.isValid()) {
0149         qWarning("Couldn't initialize XDP-KDE screencast session");
0150         isValid = false;
0151         return;
0152     }
0153 
0154     qInfo() << "DBus session created: " << sessionReply.value().path();
0155     QDBusConnection::sessionBus().connect(QString(),
0156                                           sessionReply.value().path(),
0157                                           QStringLiteral("org.freedesktop.portal.Request"),
0158                                           QStringLiteral("Response"),
0159                                           this->q,
0160                                           SLOT(handleXdpSessionCreated(uint, QVariantMap)));
0161 }
0162 
0163 void PWFrameBuffer::handleXdpSessionCreated(quint32 code, const QVariantMap &results)
0164 {
0165     d->handleSessionCreated(code, results);
0166 }
0167 
0168 /**
0169  * @brief PWFrameBuffer::Private::handleSessionCreated - handle creation of ScreenCast session.
0170  *        XDG Portal answers with session path if it was able to successfully create the screencast.
0171  *
0172  * @param code return code for dbus call. Zero is success, non-zero means error
0173  * @param results map with results of call.
0174  */
0175 void PWFrameBuffer::Private::handleSessionCreated(quint32 code, const QVariantMap &results)
0176 {
0177     if (code != 0) {
0178         qCWarning(KRFB_FB_PIPEWIRE) << "Failed to create session: " << code;
0179         isValid = false;
0180         return;
0181     }
0182 
0183     sessionPath = QDBusObjectPath(results.value(QStringLiteral("session_handle")).toString());
0184 
0185     // select sources for the session
0186     auto selectionOptions = QVariantMap {
0187         // We have to specify it's an uint, otherwise xdg-desktop-portal will not forward it to backend implementation
0188         { QStringLiteral("types"), QVariant::fromValue<uint>(7) }, // request all (KeyBoard, Pointer, TouchScreen)
0189         { QStringLiteral("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) }
0190     };
0191     auto selectorReply = dbusXdpRemoteDesktopService->SelectDevices(sessionPath, selectionOptions);
0192     selectorReply.waitForFinished();
0193     if (!selectorReply.isValid()) {
0194         qCWarning(KRFB_FB_PIPEWIRE) << "Couldn't select devices for the remote-desktop session";
0195         isValid = false;
0196         return;
0197     }
0198     QDBusConnection::sessionBus().connect(QString(),
0199                                           selectorReply.value().path(),
0200                                           QStringLiteral("org.freedesktop.portal.Request"),
0201                                           QStringLiteral("Response"),
0202                                           this->q,
0203                                           SLOT(handleXdpDevicesSelected(uint, QVariantMap)));
0204 }
0205 
0206 void PWFrameBuffer::handleXdpDevicesSelected(quint32 code, const QVariantMap &results)
0207 {
0208     d->handleDevicesSelected(code, results);
0209 }
0210 
0211 /**
0212  * @brief PWFrameBuffer::Private::handleDevicesCreated - handle selection of devices we want to use for remote desktop
0213  *
0214  * @param code return code for dbus call. Zero is success, non-zero means error
0215  * @param results map with results of call.
0216  */
0217 void PWFrameBuffer::Private::handleDevicesSelected(quint32 code, const QVariantMap &results)
0218 {
0219     Q_UNUSED(results)
0220     if (code != 0) {
0221         qCWarning(KRFB_FB_PIPEWIRE) << "Failed to select devices: " << code;
0222         isValid = false;
0223         return;
0224     }
0225 
0226     // select sources for the session
0227     auto selectionOptions = QVariantMap {
0228         { QStringLiteral("types"), QVariant::fromValue<uint>(1) }, // only MONITOR is supported
0229         { QStringLiteral("multiple"), false },
0230         { QStringLiteral("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) }
0231     };
0232     auto selectorReply = dbusXdpScreenCastService->SelectSources(sessionPath, selectionOptions);
0233     selectorReply.waitForFinished();
0234     if (!selectorReply.isValid()) {
0235         qCWarning(KRFB_FB_PIPEWIRE) << "Couldn't select sources for the screen-casting session";
0236         isValid = false;
0237         return;
0238     }
0239     QDBusConnection::sessionBus().connect(QString(),
0240                                           selectorReply.value().path(),
0241                                           QStringLiteral("org.freedesktop.portal.Request"),
0242                                           QStringLiteral("Response"),
0243                                           this->q,
0244                                           SLOT(handleXdpSourcesSelected(uint, QVariantMap)));
0245 }
0246 
0247 void PWFrameBuffer::handleXdpSourcesSelected(quint32 code, const QVariantMap &results)
0248 {
0249     d->handleSourcesSelected(code, results);
0250 }
0251 
0252 /**
0253  * @brief PWFrameBuffer::Private::handleSourcesSelected - handle Screencast sources selection.
0254  *        XDG Portal shows a dialog at this point which allows you to select monitor from the list.
0255  *        This function is called after you make a selection.
0256  *
0257  * @param code return code for dbus call. Zero is success, non-zero means error
0258  * @param results map with results of call.
0259  */
0260 void PWFrameBuffer::Private::handleSourcesSelected(quint32 code, const QVariantMap &)
0261 {
0262     if (code != 0) {
0263         qCWarning(KRFB_FB_PIPEWIRE) << "Failed to select sources: " << code;
0264         isValid = false;
0265         return;
0266     }
0267 
0268     // start session
0269     auto startParameters = QVariantMap {
0270         { QStringLiteral("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) }
0271     };
0272     auto startReply = dbusXdpRemoteDesktopService->Start(sessionPath, QString(), startParameters);
0273     startReply.waitForFinished();
0274     QDBusConnection::sessionBus().connect(QString(),
0275                                           startReply.value().path(),
0276                                           QStringLiteral("org.freedesktop.portal.Request"),
0277                                           QStringLiteral("Response"),
0278                                           this->q,
0279                                           SLOT(handleXdpRemoteDesktopStarted(uint, QVariantMap)));
0280 }
0281 
0282 
0283 void PWFrameBuffer::handleXdpRemoteDesktopStarted(quint32 code, const QVariantMap &results)
0284 {
0285     d->handleRemoteDesktopStarted(code, results);
0286 }
0287 
0288 /**
0289  * @brief PWFrameBuffer::Private::handleScreencastStarted - handle Screencast start.
0290  *        At this point there shall be ready pipewire stream to consume.
0291  *
0292  * @param code return code for dbus call. Zero is success, non-zero means error
0293  * @param results map with results of call.
0294  */
0295 void PWFrameBuffer::Private::handleRemoteDesktopStarted(quint32 code, const QVariantMap &results)
0296 {
0297     if (code != 0) {
0298         qCWarning(KRFB_FB_PIPEWIRE) << "Failed to start screencast: " << code;
0299         isValid = false;
0300         return;
0301     }
0302 
0303     if (results.value(QStringLiteral("devices")).toUInt() == 0) {
0304         qCWarning(KRFB_FB_PIPEWIRE) << "No devices were granted" << results;
0305         isValid = false;
0306         return;
0307     }
0308 
0309     // there should be only one stream
0310     const Streams streams = qdbus_cast<Streams>(results.value(QStringLiteral("streams")));
0311     if (streams.isEmpty()) {
0312         // maybe we should check deeper with qdbus_cast but this suffices for now
0313         qCWarning(KRFB_FB_PIPEWIRE) << "Failed to get screencast streams";
0314         isValid = false;
0315         return;
0316     }
0317 
0318     auto streamReply = dbusXdpScreenCastService->OpenPipeWireRemote(sessionPath, QVariantMap());
0319     streamReply.waitForFinished();
0320     if (!streamReply.isValid()) {
0321         qCWarning(KRFB_FB_PIPEWIRE) << "Couldn't open pipewire remote for the screen-casting session";
0322         isValid = false;
0323         return;
0324     }
0325 
0326     QDBusUnixFileDescriptor pipewireFd = streamReply.value();
0327     if (!pipewireFd.isValid()) {
0328         qCWarning(KRFB_FB_PIPEWIRE) << "Couldn't get pipewire connection file descriptor";
0329         isValid = false;
0330         return;
0331     }
0332 
0333     if (!stream->createStream(streams.first().nodeId, pipewireFd.takeFileDescriptor())) {
0334         qCWarning(KRFB_FB_PIPEWIRE) << "Couldn't create the pipewire stream";
0335         isValid = false;
0336         return;
0337     }
0338     setVideoSize(qdbus_cast<QSize>(streams.first().map[QStringLiteral("size")].value<QDBusArgument>()));
0339 }
0340 
0341 void PWFrameBuffer::Private::handleFrame(const PipeWireFrame &frame)
0342 {
0343     cursor = frame.cursor;
0344 
0345 #if KPIPEWIRE_VERSION < QT_VERSION_CHECK(6, 0, 70)
0346     if (!frame.dmabuf && !frame.image) {
0347 #else
0348     if (!frame.dmabuf && !frame.dataFrame) {
0349 #endif
0350         qCDebug(KRFB_FB_PIPEWIRE) << "Got empty buffer. The buffer possibly carried only "
0351                                      "information about the mouse cursor.";
0352         return;
0353     }
0354 
0355 #if KPIPEWIRE_VERSION < QT_VERSION_CHECK(6, 0, 70)
0356     if (frame.image) {
0357         memcpy(q->fb, frame.image->constBits(), frame.image->sizeInBytes());
0358         setVideoSize(frame.image->size());
0359     }
0360 #else
0361     if (frame.dataFrame) {
0362         memcpy(q->fb, frame.dataFrame->data, frame.dataFrame->size.width() * frame.dataFrame->stride);
0363         setVideoSize(frame.dataFrame->size);
0364     }
0365 #endif
0366     else if (frame.dmabuf) {
0367         QImage src((uchar*) q->fb, videoSize.width(), videoSize.height(), QImage::Format_RGB32);
0368         if (!m_dmabufHandler.downloadFrame(src, frame)) {
0369             stream->renegotiateModifierFailed(frame.format, frame.dmabuf->modifier);
0370             qCDebug(KRFB_FB_PIPEWIRE) << "Failed to download frame.";
0371             return;
0372         }
0373         setVideoSize(src.size());
0374     } else {
0375         qCDebug(KRFB_FB_PIPEWIRE) << "Unknown kind of frame";
0376     }
0377 
0378     if (auto damage = frame.damage) {
0379         for (const auto &rect : *damage) {
0380             q->tiles.append(rect);
0381         }
0382     } else {
0383         q->tiles.append(QRect(0, 0, videoSize.width(), videoSize.height()));
0384     }
0385 }
0386 
0387 void PWFrameBuffer::Private::setVideoSize(const QSize &size)
0388 {
0389     if (q->fb && videoSize == size) {
0390         return;
0391     }
0392 
0393     free(q->fb);
0394     q->fb = static_cast<char*>(malloc(size.width() * size.height() * BYTES_PER_PIXEL));
0395     if (!q->fb) {
0396         qCWarning(KRFB_FB_PIPEWIRE) << "Failed to allocate buffer";
0397         isValid = false;
0398         return;
0399     }
0400     videoSize = size;
0401 
0402     Q_EMIT q->frameBufferChanged();
0403 }
0404 
0405 PWFrameBuffer::Private::~Private()
0406 {
0407 }
0408 
0409 PWFrameBuffer::PWFrameBuffer(QObject *parent)
0410     : FrameBuffer (parent),
0411       d(new Private(this))
0412 {
0413 }
0414 
0415 PWFrameBuffer::~PWFrameBuffer()
0416 {
0417     free(fb);
0418     fb = nullptr;
0419 }
0420 
0421 void PWFrameBuffer::initDBus()
0422 {
0423     d->initDbus();
0424 }
0425 
0426 void PWFrameBuffer::startVirtualMonitor(const QString& name, const QSize& resolution, qreal dpr)
0427 {
0428     d->videoSize = resolution * dpr;
0429     using namespace KWayland::Client;
0430     auto connection = ConnectionThread::fromApplication(this);
0431     if (!connection) {
0432         qWarning() << "Failed getting Wayland connection from QPA";
0433         QCoreApplication::exit(1);
0434         return;
0435     }
0436 
0437     auto registry = new Registry(this);
0438     connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry, name, dpr, resolution] (const QByteArray &interfaceName, quint32 wlname, quint32 version) {
0439         if (interfaceName != "zkde_screencast_unstable_v1")
0440             return;
0441 
0442         auto screencasting = new Screencasting(registry, wlname, version, this);
0443         auto r = screencasting->createVirtualMonitorStream(name, resolution, dpr, Screencasting::Metadata);
0444         connect(r, &ScreencastingStream::created, this, [this] (quint32 nodeId) {
0445             d->stream->createStream(nodeId, 0);
0446         });
0447     });
0448     registry->create(connection);
0449     registry->setup();
0450 }
0451 
0452 int PWFrameBuffer::depth()
0453 {
0454     return 32;
0455 }
0456 
0457 int PWFrameBuffer::height()
0458 {
0459     if (!d->videoSize.isValid()) {
0460         return 0;
0461     }
0462     return d->videoSize.height();
0463 }
0464 
0465 int PWFrameBuffer::width()
0466 {
0467     if (!d->videoSize.isValid()) {
0468         return 0;
0469     }
0470     return d->videoSize.width();
0471 }
0472 
0473 int PWFrameBuffer::paddedWidth()
0474 {
0475     return width() * 4;
0476 }
0477 
0478 void PWFrameBuffer::getServerFormat(rfbPixelFormat &format)
0479 {
0480     format.bitsPerPixel = 32;
0481     format.depth = 32;
0482     format.trueColour = true;
0483     format.bigEndian = false;
0484 }
0485 
0486 void PWFrameBuffer::startMonitor()
0487 {
0488 
0489 }
0490 
0491 void PWFrameBuffer::stopMonitor()
0492 {
0493 
0494 }
0495 
0496 QVariant PWFrameBuffer::customProperty(const QString &property) const
0497 {
0498     if (property == QLatin1String("stream_node_id")) {
0499         return QVariant::fromValue<uint>(d->stream->nodeId());
0500     } if (property == QLatin1String("session_handle")) {
0501         return QVariant::fromValue<QDBusObjectPath>(d->sessionPath);
0502     }
0503 
0504     return FrameBuffer::customProperty(property);
0505 }
0506 
0507 bool PWFrameBuffer::isValid() const
0508 {
0509     return d->isValid;
0510 }
0511 
0512 QPoint PWFrameBuffer::cursorPosition()
0513 {
0514     return d->cursor->position;
0515 }