File indexing completed on 2024-03-24 17:02:24

0001 /*
0002     SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez <aleixpol@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include <QCommandLineParser>
0008 #include <QDebug>
0009 #include <QGuiApplication>
0010 #include <QScreen>
0011 
0012 #include "screencasting.h"
0013 #include "xdp_dbus_remotedesktop_interface.h"
0014 #include "xdp_dbus_screencast_interface.h"
0015 #include <DmaBufHandler>
0016 #include <PipeWireSourceStream>
0017 #include <QDBusArgument>
0018 #include <unistd.h>
0019 
0020 static QString createHandleToken()
0021 {
0022     return QStringLiteral("kpipewireheadlesstest%1").arg(QRandomGenerator::global()->generate());
0023 }
0024 
0025 void checkPlasmaScreens()
0026 {
0027     auto screencasting = new Screencasting(qGuiApp);
0028     for (auto screen : qGuiApp->screens()) {
0029         auto stream = screencasting->createOutputStream(screen->name(), Screencasting::Embedded);
0030         QObject::connect(stream, &ScreencastingStream::created, qGuiApp, [stream] {
0031             auto pwStream = new PipeWireSourceStream(qGuiApp);
0032             if (!pwStream->createStream(stream->nodeId(), 0)) {
0033                 qWarning() << "failed!" << pwStream->error();
0034                 exit(1);
0035             }
0036 
0037             auto handler = std::make_shared<DmaBufHandler>();
0038             QObject::connect(pwStream, &PipeWireSourceStream::frameReceived, qGuiApp, [handler, pwStream](const PipeWireFrame &frame) {
0039                 QImage qimage(pwStream->size(), QImage::Format_RGBA8888);
0040                 if (!handler->downloadFrame(qimage, frame)) {
0041                     qDebug() << "failed to download frame";
0042                     pwStream->renegotiateModifierFailed(frame.format, frame.dmabuf->modifier);
0043                 } else {
0044                     qDebug() << ".";
0045                 }
0046             });
0047         });
0048     }
0049 }
0050 
0051 void checkPlasmaWorkspace()
0052 {
0053     auto screencasting = new Screencasting(qGuiApp);
0054     QRegion region;
0055     for (auto screen : qGuiApp->screens()) {
0056         region |= screen->geometry();
0057     }
0058     auto stream = screencasting->createRegionStream(region.boundingRect(), 1, Screencasting::Embedded);
0059     QObject::connect(stream, &ScreencastingStream::created, qGuiApp, [stream] {
0060         auto pwStream = new PipeWireSourceStream(qGuiApp);
0061         if (!pwStream->createStream(stream->nodeId(), 0)) {
0062             qWarning() << "failed!" << pwStream->error();
0063             exit(1);
0064         }
0065 
0066         auto handler = std::make_shared<DmaBufHandler>();
0067         QObject::connect(pwStream, &PipeWireSourceStream::frameReceived, qGuiApp, [handler, pwStream](const PipeWireFrame &frame) {
0068             QImage qimage(pwStream->size(), QImage::Format_RGBA8888);
0069             if (!handler->downloadFrame(qimage, frame)) {
0070                 qDebug() << "failed to download frame";
0071                 pwStream->renegotiateModifierFailed(frame.format, frame.dmabuf->modifier);
0072             } else {
0073                 qDebug() << ".";
0074             }
0075         });
0076     });
0077 }
0078 
0079 using Stream = struct {
0080     uint nodeId;
0081     QVariantMap map;
0082 };
0083 using Streams = QList<Stream>;
0084 
0085 Q_DECLARE_METATYPE(Stream);
0086 Q_DECLARE_METATYPE(Streams);
0087 
0088 const QDBusArgument &operator>>(const QDBusArgument &arg, Stream &stream)
0089 {
0090     arg.beginStructure();
0091     arg >> stream.nodeId;
0092 
0093     arg.beginMap();
0094     while (!arg.atEnd()) {
0095         QString key;
0096         QVariant map;
0097         arg.beginMapEntry();
0098         arg >> key >> map;
0099         arg.endMapEntry();
0100         stream.map.insert(key, map);
0101     }
0102     arg.endMap();
0103     arg.endStructure();
0104 
0105     return arg;
0106 }
0107 
0108 class XdpScreenCast : public QObject
0109 {
0110     Q_OBJECT
0111 public:
0112     XdpScreenCast(QObject *parent)
0113         : QObject(parent)
0114     {
0115         initDbus();
0116     }
0117 
0118     void initDbus()
0119     {
0120         dbusXdpScreenCastService.reset(new OrgFreedesktopPortalScreenCastInterface(QStringLiteral("org.freedesktop.portal.Desktop"),
0121                                                                                    QStringLiteral("/org/freedesktop/portal/desktop"),
0122                                                                                    QDBusConnection::sessionBus()));
0123 
0124         qInfo() << "Initializing D-Bus connectivity with XDG Desktop Portal" << dbusXdpScreenCastService->version();
0125         Q_ASSERT(dbusXdpScreenCastService->isValid());
0126 
0127         // create session
0128         auto sessionParameters =
0129             QVariantMap{{QStringLiteral("session_handle_token"), createHandleToken()}, {QStringLiteral("handle_token"), createHandleToken()}};
0130         auto sessionReply = dbusXdpScreenCastService->CreateSession(sessionParameters);
0131         sessionReply.waitForFinished();
0132         if (!sessionReply.isValid()) {
0133             qWarning() << "Couldn't initialize XDP-KDE screencast session" << sessionReply.error();
0134             exit(1);
0135             return;
0136         }
0137 
0138         qInfo() << "DBus session created: " << sessionReply.value().path()
0139                 << QDBusConnection::sessionBus().connect(QString(),
0140                                                          sessionReply.value().path(),
0141                                                          QStringLiteral("org.freedesktop.portal.Request"),
0142                                                          QStringLiteral("Response"),
0143                                                          this,
0144                                                          SLOT(handleSessionCreated(uint, QVariantMap)));
0145     }
0146 
0147 public Q_SLOTS:
0148     void handleSessionCreated(quint32 code, const QVariantMap &results)
0149     {
0150         if (code != 0) {
0151             qWarning() << "Failed to create session: " << code;
0152             exit(1);
0153             return;
0154         }
0155 
0156         sessionPath = QDBusObjectPath(results.value(QStringLiteral("session_handle")).toString());
0157 
0158         // select sources for the session
0159         const QVariantMap sourcesParameters = {{QLatin1String("handle_token"), createHandleToken()},
0160                                                {QLatin1String("types"), dbusXdpScreenCastService->availableSourceTypes()},
0161                                                {QLatin1String("multiple"), false},
0162                                                {QLatin1String("cursor_mode"), uint(2 /*Embedded*/)}};
0163         auto selectorReply = dbusXdpScreenCastService->SelectSources(sessionPath, sourcesParameters);
0164         selectorReply.waitForFinished();
0165         if (!selectorReply.isValid()) {
0166             qWarning() << "Couldn't select devices for the remote-desktop session";
0167             exit(1);
0168             return;
0169         }
0170         QDBusConnection::sessionBus().connect(QString(),
0171                                               selectorReply.value().path(),
0172                                               QStringLiteral("org.freedesktop.portal.Request"),
0173                                               QStringLiteral("Response"),
0174                                               this,
0175                                               SLOT(handleSourcesSelected(uint, QVariantMap)));
0176     }
0177 
0178     void handleSourcesSelected(quint32 code, const QVariantMap &)
0179     {
0180         if (code != 0) {
0181             qWarning() << "Failed to select sources: " << code;
0182             exit(1);
0183             return;
0184         }
0185 
0186         // start session
0187         auto startParameters = QVariantMap{{QStringLiteral("handle_token"), createHandleToken()}};
0188         auto startReply = dbusXdpScreenCastService->Start(sessionPath, QString(), startParameters);
0189         startReply.waitForFinished();
0190         QDBusConnection::sessionBus().connect(QString(),
0191                                               startReply.value().path(),
0192                                               QStringLiteral("org.freedesktop.portal.Request"),
0193                                               QStringLiteral("Response"),
0194                                               this,
0195                                               SLOT(handleRemoteDesktopStarted(uint, QVariantMap)));
0196     }
0197 
0198     void handleRemoteDesktopStarted(quint32 code, const QVariantMap &results)
0199     {
0200         if (code != 0) {
0201             qWarning() << "Failed to start screencast: " << code;
0202             exit(1);
0203             return;
0204         }
0205 
0206         // there should be only one stream
0207         const Streams streams = qdbus_cast<Streams>(results.value(QStringLiteral("streams")));
0208         if (streams.isEmpty()) {
0209             // maybe we should check deeper with qdbus_cast but this suffices for now
0210             qWarning() << "Failed to get screencast streams";
0211             exit(1);
0212             return;
0213         }
0214 
0215         const QVariantMap startParameters = {
0216             { QLatin1String("handle_token"), createHandleToken() }
0217         };
0218 
0219         auto streamReply = dbusXdpScreenCastService->OpenPipeWireRemote(sessionPath, startParameters);
0220         streamReply.waitForFinished();
0221         if (!streamReply.isValid()) {
0222             qWarning() << "Couldn't open pipewire remote for the screen-casting session";
0223             exit(1);
0224             return;
0225         }
0226 
0227         auto pipewireFd = streamReply.value();
0228         if (!pipewireFd.isValid()) {
0229             qWarning() << "Couldn't get pipewire connection file descriptor";
0230             exit(1);
0231             return;
0232         }
0233 
0234         const int fd = pipewireFd.takeFileDescriptor();
0235         if (!stream.createStream(streams.first().nodeId, fd)) {
0236             qWarning() << "Couldn't create the pipewire stream";
0237             exit(1);
0238             return;
0239         }
0240 
0241         QObject::connect(&stream, &PipeWireSourceStream::frameReceived, this, [](const PipeWireFrame &frame) {
0242             qDebug() << "." << frame.format;
0243         });
0244 
0245         QObject::connect(&stream, &PipeWireSourceStream::stopStreaming, this, [fd] {
0246             close(fd);
0247         });
0248     }
0249 
0250 private:
0251     QScopedPointer<OrgFreedesktopPortalScreenCastInterface> dbusXdpScreenCastService;
0252     QDBusObjectPath sessionPath;
0253     PipeWireSourceStream stream;
0254 };
0255 
0256 class XdpRemoteDesktop : public QObject
0257 {
0258     Q_OBJECT
0259 public:
0260     XdpRemoteDesktop(QObject *parent)
0261         : QObject(parent)
0262     {
0263         initDbus();
0264     }
0265 
0266     void initDbus()
0267     {
0268         dbusXdpScreenCastService.reset(new OrgFreedesktopPortalScreenCastInterface(QStringLiteral("org.freedesktop.portal.Desktop"),
0269                                                                                    QStringLiteral("/org/freedesktop/portal/desktop"),
0270                                                                                    QDBusConnection::sessionBus()));
0271         dbusXdpRemoteDesktopService.reset(new OrgFreedesktopPortalRemoteDesktopInterface(QStringLiteral("org.freedesktop.portal.Desktop"),
0272                                                                                          QStringLiteral("/org/freedesktop/portal/desktop"),
0273                                                                                          QDBusConnection::sessionBus()));
0274 
0275         qInfo() << "Initializing D-Bus connectivity with XDG Desktop Portal" << dbusXdpScreenCastService->version();
0276         Q_ASSERT(dbusXdpScreenCastService->isValid());
0277         Q_ASSERT(dbusXdpRemoteDesktopService->isValid());
0278 
0279         // create session
0280         auto sessionParameters =
0281             QVariantMap{{QStringLiteral("session_handle_token"), createHandleToken()}, {QStringLiteral("handle_token"), createHandleToken()}};
0282         auto sessionReply = dbusXdpRemoteDesktopService->CreateSession(sessionParameters);
0283         sessionReply.waitForFinished();
0284         if (!sessionReply.isValid()) {
0285             qWarning() << "Couldn't initialize XDP-KDE screencast session" << sessionReply.error();
0286             exit(1);
0287             return;
0288         }
0289 
0290         qInfo() << "DBus session created: " << sessionReply.value().path()
0291                 << QDBusConnection::sessionBus().connect(QString(),
0292                                                          sessionReply.value().path(),
0293                                                          QStringLiteral("org.freedesktop.portal.Request"),
0294                                                          QStringLiteral("Response"),
0295                                                          this,
0296                                                          SLOT(handleSessionCreated(uint, QVariantMap)));
0297     }
0298 
0299 public Q_SLOTS:
0300     void handleSessionCreated(quint32 code, const QVariantMap &results)
0301     {
0302         if (code != 0) {
0303             qWarning() << "Failed to create session: " << code;
0304             exit(1);
0305             return;
0306         }
0307 
0308         sessionPath = QDBusObjectPath(results.value(QStringLiteral("session_handle")).toString());
0309 
0310         // select sources for the session
0311         auto selectionOptions = QVariantMap{// We have to specify it's an uint, otherwise xdg-desktop-portal will not forward it to backend implementation
0312                                             {QStringLiteral("types"), QVariant::fromValue<uint>(7)}, // request all (KeyBoard, Pointer, TouchScreen)
0313                                             {QStringLiteral("handle_token"), createHandleToken()}};
0314         auto selectorReply = dbusXdpRemoteDesktopService->SelectDevices(sessionPath, selectionOptions);
0315         selectorReply.waitForFinished();
0316         if (!selectorReply.isValid()) {
0317             qWarning() << "Couldn't select devices for the remote-desktop session";
0318             exit(1);
0319             return;
0320         }
0321         QDBusConnection::sessionBus().connect(QString(),
0322                                               selectorReply.value().path(),
0323                                               QStringLiteral("org.freedesktop.portal.Request"),
0324                                               QStringLiteral("Response"),
0325                                               this,
0326                                               SLOT(handleDevicesSelected(uint, QVariantMap)));
0327     }
0328 
0329     void handleDevicesSelected(quint32 code, const QVariantMap &results)
0330     {
0331         Q_UNUSED(results)
0332         if (code != 0) {
0333             qWarning() << "Failed to select devices: " << code;
0334             exit(1);
0335             return;
0336         }
0337 
0338         // select sources for the session
0339         auto selectionOptions = QVariantMap{{QStringLiteral("types"), QVariant::fromValue<uint>(7)},
0340                                             {QStringLiteral("multiple"), false},
0341                                             {QStringLiteral("handle_token"), createHandleToken()}};
0342         auto selectorReply = dbusXdpScreenCastService->SelectSources(sessionPath, selectionOptions);
0343         selectorReply.waitForFinished();
0344         if (!selectorReply.isValid()) {
0345             qWarning() << "Couldn't select sources for the screen-casting session";
0346             exit(1);
0347             return;
0348         }
0349         QDBusConnection::sessionBus().connect(QString(),
0350                                               selectorReply.value().path(),
0351                                               QStringLiteral("org.freedesktop.portal.Request"),
0352                                               QStringLiteral("Response"),
0353                                               this,
0354                                               SLOT(handleSourcesSelected(uint, QVariantMap)));
0355     }
0356 
0357     void handleSourcesSelected(quint32 code, const QVariantMap &)
0358     {
0359         if (code != 0) {
0360             qWarning() << "Failed to select sources: " << code;
0361             exit(1);
0362             return;
0363         }
0364 
0365         // start session
0366         auto startParameters = QVariantMap{{QStringLiteral("handle_token"), createHandleToken()}};
0367         auto startReply = dbusXdpRemoteDesktopService->Start(sessionPath, QString(), startParameters);
0368         startReply.waitForFinished();
0369         QDBusConnection::sessionBus().connect(QString(),
0370                                               startReply.value().path(),
0371                                               QStringLiteral("org.freedesktop.portal.Request"),
0372                                               QStringLiteral("Response"),
0373                                               this,
0374                                               SLOT(handleRemoteDesktopStarted(uint, QVariantMap)));
0375     }
0376 
0377     void handleRemoteDesktopStarted(quint32 code, const QVariantMap &results)
0378     {
0379         if (code != 0) {
0380             qWarning() << "Failed to start screencast: " << code;
0381             exit(1);
0382             return;
0383         }
0384 
0385         if (results.value(QStringLiteral("devices")).toUInt() == 0) {
0386             qWarning() << "No devices were granted" << results;
0387             exit(1);
0388             return;
0389         }
0390 
0391         // there should be only one stream
0392         const Streams streams = qdbus_cast<Streams>(results.value(QStringLiteral("streams")));
0393         if (streams.isEmpty()) {
0394             // maybe we should check deeper with qdbus_cast but this suffices for now
0395             qWarning() << "Failed to get screencast streams";
0396             exit(1);
0397             return;
0398         }
0399 
0400         auto streamReply = dbusXdpScreenCastService->OpenPipeWireRemote(sessionPath, QVariantMap());
0401         streamReply.waitForFinished();
0402         if (!streamReply.isValid()) {
0403             qWarning() << "Couldn't open pipewire remote for the screen-casting session";
0404             exit(1);
0405             return;
0406         }
0407 
0408         auto pipewireFd = streamReply.value();
0409         if (!pipewireFd.isValid()) {
0410             qWarning() << "Couldn't get pipewire connection file descriptor";
0411             exit(1);
0412             return;
0413         }
0414 
0415         const uint fd = pipewireFd.takeFileDescriptor();
0416         if (!stream.createStream(streams.first().nodeId, fd)) {
0417             qWarning() << "Couldn't create the pipewire stream";
0418             exit(1);
0419             return;
0420         }
0421 
0422         QObject::connect(&stream, &PipeWireSourceStream::frameReceived, this, [](const PipeWireFrame &frame) {
0423             qDebug() << "." << frame.format;
0424         });
0425 
0426         QObject::connect(&stream, &PipeWireSourceStream::stopStreaming, this, [fd] {
0427             close(fd);
0428         });
0429     }
0430 
0431 private:
0432     QScopedPointer<OrgFreedesktopPortalScreenCastInterface> dbusXdpScreenCastService;
0433     QScopedPointer<OrgFreedesktopPortalRemoteDesktopInterface> dbusXdpRemoteDesktopService;
0434     QDBusObjectPath sessionPath;
0435     PipeWireSourceStream stream;
0436 };
0437 
0438 int main(int argc, char **argv)
0439 {
0440     QGuiApplication app(argc, argv);
0441 
0442     {
0443         QCommandLineParser parser;
0444         QCommandLineOption useXdpRD(QStringLiteral("xdp-remotedesktop"), QStringLiteral("Uses the XDG Desktop Portal RemoteDesktop interface"));
0445         parser.addOption(useXdpRD);
0446         QCommandLineOption useXdpSC(QStringLiteral("xdp-screencast"), QStringLiteral("Uses the XDG Desktop Portal ScreenCast interface"));
0447         parser.addOption(useXdpSC);
0448         QCommandLineOption useWorkspace(QStringLiteral("workspace"), QStringLiteral("Uses the Plasma screencasting workspace feed"));
0449         parser.addOption(useWorkspace);
0450         parser.addHelpOption();
0451         parser.process(app);
0452 
0453         if (parser.isSet(useXdpRD)) {
0454             new XdpRemoteDesktop(&app);
0455         } else if (parser.isSet(useXdpSC)) {
0456             new XdpScreenCast(&app);
0457         } else if (parser.isSet(useWorkspace)) {
0458             checkPlasmaWorkspace();
0459         } else {
0460             checkPlasmaScreens();
0461         }
0462     }
0463 
0464     return app.exec();
0465 }
0466 
0467 #include "HeadlessTest.moc"