File indexing completed on 2024-04-28 04:58:58

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