File indexing completed on 2024-12-01 08:04:22

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 <KSignalHandler>
0008 #include <QCommandLineParser>
0009 #include <QDebug>
0010 #include <QGuiApplication>
0011 #include <QScreen>
0012 
0013 #include "screencasting.h"
0014 #include "xdp_dbus_remotedesktop_interface.h"
0015 #include "xdp_dbus_screencast_interface.h"
0016 #include <DmaBufHandler>
0017 #include <PipeWireEncodedStream>
0018 #include <PipeWireSourceStream>
0019 #include <QDBusArgument>
0020 #include <unistd.h>
0021 
0022 using namespace Qt::StringLiterals;
0023 
0024 static bool s_encodedStream = false;
0025 static std::optional<Fraction> s_framerate;
0026 static std::optional<QByteArray> s_encoder;
0027 static Screencasting::CursorMode s_cursorMode = Screencasting::Embedded;
0028 
0029 static QString createHandleToken()
0030 {
0031     return QStringLiteral("kpipewireheadlesstest%1").arg(QRandomGenerator::global()->generate());
0032 }
0033 
0034 void createStream(int nodeId, std::optional<int> fd = {})
0035 {
0036     if (s_encodedStream) {
0037         auto encoded = new PipeWireEncodedStream(qGuiApp);
0038         encoded->setNodeId(nodeId);
0039         if (fd) {
0040             encoded->setFd(*fd);
0041         }
0042         if (s_framerate) {
0043             encoded->setMaxFramerate(*s_framerate);
0044         }
0045         if (s_encoder) {
0046             PipeWireBaseEncodedStream::Encoder enc = PipeWireBaseEncodedStream::NoEncoder;
0047             if (s_encoder.value() == QByteArray("H264Main")) {
0048                 enc = PipeWireBaseEncodedStream::H264Main;
0049             } else if (s_encoder.value() == QByteArray("H264Baseline")) {
0050                 enc = PipeWireBaseEncodedStream::H264Baseline;
0051             } else if (s_encoder.value() == QByteArray("VP8")) {
0052                 enc = PipeWireBaseEncodedStream::VP8;
0053             } else if (s_encoder.value() == QByteArray("VP9")) {
0054                 enc = PipeWireBaseEncodedStream::VP9;
0055             }
0056             encoded->setEncoder(enc);
0057         }
0058         encoded->setActive(true);
0059         QObject::connect(encoded, &PipeWireEncodedStream::newPacket, qGuiApp, [](const PipeWireEncodedStream::Packet &packet) {
0060             qDebug() << "packet received" << packet.data().size() << "key:" << packet.isKeyFrame();
0061         });
0062         QObject::connect(encoded, &PipeWireEncodedStream::cursorChanged, qGuiApp, [](const PipeWireCursor &cursor) {
0063             qDebug() << "cursor received. position:" << cursor.position << "hotspot:" << cursor.hotspot << "image:" << cursor.texture;
0064         });
0065         QObject::connect(KSignalHandler::self(), &KSignalHandler::signalReceived, encoded, [encoded] {
0066             encoded->setActive(false);
0067             exit(0);
0068         });
0069         return;
0070     }
0071     auto pwStream = new PipeWireSourceStream(qGuiApp);
0072     pwStream->setAllowDmaBuf(false);
0073     if (s_framerate) {
0074         pwStream->setMaxFramerate(*s_framerate);
0075     }
0076     if (!pwStream->createStream(nodeId, 0)) {
0077         qWarning() << "failed!" << pwStream->error();
0078         exit(1);
0079     }
0080 
0081     auto handler = std::make_shared<DmaBufHandler>();
0082     QObject::connect(pwStream, &PipeWireSourceStream::frameReceived, qGuiApp, [handler, pwStream](const PipeWireFrame &frame) {
0083         if (frame.dmabuf) {
0084             QImage qimage(pwStream->size(), QImage::Format_RGBA8888);
0085             if (!handler->downloadFrame(qimage, frame)) {
0086                 qDebug() << "failed to download frame";
0087                 pwStream->renegotiateModifierFailed(frame.format, frame.dmabuf->modifier);
0088             } else {
0089                 qDebug() << "dmabuf" << frame.format;
0090             }
0091         } else if (frame.image) {
0092             qDebug() << "image" << frame.image->format() << frame.format;
0093         } else {
0094             qDebug() << "no-frame";
0095         }
0096     });
0097     QObject::connect(KSignalHandler::self(), &KSignalHandler::signalReceived, pwStream, [pwStream] {
0098         pwStream->setActive(false);
0099         exit(0);
0100     });
0101 }
0102 
0103 void processStream(ScreencastingStream *stream)
0104 {
0105     QObject::connect(stream, &ScreencastingStream::created, qGuiApp, [](int nodeId) {
0106         createStream(nodeId);
0107     });
0108 }
0109 
0110 void checkPlasmaScreens()
0111 {
0112     auto screencasting = new Screencasting(qGuiApp);
0113     for (auto screen : qGuiApp->screens()) {
0114         auto stream = screencasting->createOutputStream(screen->name(), s_cursorMode);
0115         processStream(stream);
0116     }
0117 }
0118 
0119 void checkPlasmaWorkspace()
0120 {
0121     auto screencasting = new Screencasting(qGuiApp);
0122     QRegion region;
0123     for (auto screen : qGuiApp->screens()) {
0124         region |= screen->geometry();
0125     }
0126     auto stream = screencasting->createRegionStream(region.boundingRect(), 1, s_cursorMode);
0127     processStream(stream);
0128 }
0129 
0130 using Stream = struct {
0131     uint nodeId;
0132     QVariantMap map;
0133 };
0134 using Streams = QList<Stream>;
0135 
0136 Q_DECLARE_METATYPE(Stream);
0137 Q_DECLARE_METATYPE(Streams);
0138 
0139 const QDBusArgument &operator>>(const QDBusArgument &arg, Stream &stream)
0140 {
0141     arg.beginStructure();
0142     arg >> stream.nodeId;
0143 
0144     arg.beginMap();
0145     while (!arg.atEnd()) {
0146         QString key;
0147         QVariant map;
0148         arg.beginMapEntry();
0149         arg >> key >> map;
0150         arg.endMapEntry();
0151         stream.map.insert(key, map);
0152     }
0153     arg.endMap();
0154     arg.endStructure();
0155 
0156     return arg;
0157 }
0158 
0159 class XdpScreenCast : public QObject
0160 {
0161     Q_OBJECT
0162 public:
0163     XdpScreenCast(QObject *parent)
0164         : QObject(parent)
0165     {
0166         initDbus();
0167     }
0168 
0169     void initDbus()
0170     {
0171         dbusXdpScreenCastService.reset(new OrgFreedesktopPortalScreenCastInterface(QStringLiteral("org.freedesktop.portal.Desktop"),
0172                                                                                    QStringLiteral("/org/freedesktop/portal/desktop"),
0173                                                                                    QDBusConnection::sessionBus()));
0174 
0175         qInfo() << "Initializing D-Bus connectivity with XDG Desktop Portal" << dbusXdpScreenCastService->version();
0176         Q_ASSERT(dbusXdpScreenCastService->isValid());
0177 
0178         // create session
0179         auto sessionParameters =
0180             QVariantMap{{QStringLiteral("session_handle_token"), createHandleToken()}, {QStringLiteral("handle_token"), createHandleToken()}};
0181         auto sessionReply = dbusXdpScreenCastService->CreateSession(sessionParameters);
0182         sessionReply.waitForFinished();
0183         if (!sessionReply.isValid()) {
0184             qWarning() << "Couldn't initialize XDP-KDE screencast session" << sessionReply.error();
0185             exit(1);
0186             return;
0187         }
0188 
0189         qInfo() << "DBus session created: " << sessionReply.value().path()
0190                 << QDBusConnection::sessionBus().connect(QString(),
0191                                                          sessionReply.value().path(),
0192                                                          QStringLiteral("org.freedesktop.portal.Request"),
0193                                                          QStringLiteral("Response"),
0194                                                          this,
0195                                                          SLOT(handleSessionCreated(uint, QVariantMap)));
0196     }
0197 
0198 public Q_SLOTS:
0199     void handleSessionCreated(quint32 code, const QVariantMap &results)
0200     {
0201         if (code != 0) {
0202             qWarning() << "Failed to create session: " << code;
0203             exit(1);
0204             return;
0205         }
0206 
0207         sessionPath = QDBusObjectPath(results.value(QStringLiteral("session_handle")).toString());
0208 
0209         // select sources for the session
0210         const QVariantMap sourcesParameters = {{QLatin1String("handle_token"), createHandleToken()},
0211                                                {QLatin1String("types"), dbusXdpScreenCastService->availableSourceTypes()},
0212                                                {QLatin1String("multiple"), false},
0213                                                {QLatin1String("cursor_mode"), uint(2 /*Embedded*/)}};
0214         auto selectorReply = dbusXdpScreenCastService->SelectSources(sessionPath, sourcesParameters);
0215         selectorReply.waitForFinished();
0216         if (!selectorReply.isValid()) {
0217             qWarning() << "Couldn't select devices for the remote-desktop session";
0218             exit(1);
0219             return;
0220         }
0221         QDBusConnection::sessionBus().connect(QString(),
0222                                               selectorReply.value().path(),
0223                                               QStringLiteral("org.freedesktop.portal.Request"),
0224                                               QStringLiteral("Response"),
0225                                               this,
0226                                               SLOT(handleSourcesSelected(uint, QVariantMap)));
0227     }
0228 
0229     void handleSourcesSelected(quint32 code, const QVariantMap &)
0230     {
0231         if (code != 0) {
0232             qWarning() << "Failed to select sources: " << code;
0233             exit(1);
0234             return;
0235         }
0236 
0237         // start session
0238         auto startParameters = QVariantMap{{QStringLiteral("handle_token"), createHandleToken()}};
0239         auto startReply = dbusXdpScreenCastService->Start(sessionPath, QString(), startParameters);
0240         startReply.waitForFinished();
0241         QDBusConnection::sessionBus().connect(QString(),
0242                                               startReply.value().path(),
0243                                               QStringLiteral("org.freedesktop.portal.Request"),
0244                                               QStringLiteral("Response"),
0245                                               this,
0246                                               SLOT(handleRemoteDesktopStarted(uint, QVariantMap)));
0247     }
0248 
0249     void handleRemoteDesktopStarted(quint32 code, const QVariantMap &results)
0250     {
0251         if (code != 0) {
0252             qWarning() << "Failed to start screencast: " << code;
0253             exit(1);
0254             return;
0255         }
0256 
0257         // there should be only one stream
0258         const Streams streams = qdbus_cast<Streams>(results.value(QStringLiteral("streams")));
0259         if (streams.isEmpty()) {
0260             // maybe we should check deeper with qdbus_cast but this suffices for now
0261             qWarning() << "Failed to get screencast streams";
0262             exit(1);
0263             return;
0264         }
0265 
0266         const QVariantMap startParameters = {
0267             { QLatin1String("handle_token"), createHandleToken() }
0268         };
0269 
0270         auto streamReply = dbusXdpScreenCastService->OpenPipeWireRemote(sessionPath, startParameters);
0271         streamReply.waitForFinished();
0272         if (!streamReply.isValid()) {
0273             qWarning() << "Couldn't open pipewire remote for the screen-casting session";
0274             exit(1);
0275             return;
0276         }
0277 
0278         auto pipewireFd = streamReply.value();
0279         if (!pipewireFd.isValid()) {
0280             qWarning() << "Couldn't get pipewire connection file descriptor";
0281             exit(1);
0282             return;
0283         }
0284         const uint fd = pipewireFd.takeFileDescriptor();
0285         for (auto x : streams) {
0286             createStream(x.nodeId, fd);
0287         }
0288     }
0289 
0290 private:
0291     QScopedPointer<OrgFreedesktopPortalScreenCastInterface> dbusXdpScreenCastService;
0292     QDBusObjectPath sessionPath;
0293 };
0294 
0295 class XdpRemoteDesktop : public QObject
0296 {
0297     Q_OBJECT
0298 public:
0299     XdpRemoteDesktop(QObject *parent)
0300         : QObject(parent)
0301     {
0302         initDbus();
0303     }
0304 
0305     void initDbus()
0306     {
0307         dbusXdpScreenCastService.reset(new OrgFreedesktopPortalScreenCastInterface(QStringLiteral("org.freedesktop.portal.Desktop"),
0308                                                                                    QStringLiteral("/org/freedesktop/portal/desktop"),
0309                                                                                    QDBusConnection::sessionBus()));
0310         dbusXdpRemoteDesktopService.reset(new OrgFreedesktopPortalRemoteDesktopInterface(QStringLiteral("org.freedesktop.portal.Desktop"),
0311                                                                                          QStringLiteral("/org/freedesktop/portal/desktop"),
0312                                                                                          QDBusConnection::sessionBus()));
0313 
0314         qInfo() << "Initializing D-Bus connectivity with XDG Desktop Portal" << dbusXdpScreenCastService->version();
0315         Q_ASSERT(dbusXdpScreenCastService->isValid());
0316         Q_ASSERT(dbusXdpRemoteDesktopService->isValid());
0317 
0318         // create session
0319         auto sessionParameters =
0320             QVariantMap{{QStringLiteral("session_handle_token"), createHandleToken()}, {QStringLiteral("handle_token"), createHandleToken()}};
0321         auto sessionReply = dbusXdpRemoteDesktopService->CreateSession(sessionParameters);
0322         sessionReply.waitForFinished();
0323         if (!sessionReply.isValid()) {
0324             qWarning() << "Couldn't initialize XDP-KDE screencast session" << sessionReply.error();
0325             exit(1);
0326             return;
0327         }
0328 
0329         qInfo() << "DBus session created: " << sessionReply.value().path()
0330                 << QDBusConnection::sessionBus().connect(QString(),
0331                                                          sessionReply.value().path(),
0332                                                          QStringLiteral("org.freedesktop.portal.Request"),
0333                                                          QStringLiteral("Response"),
0334                                                          this,
0335                                                          SLOT(handleSessionCreated(uint, QVariantMap)));
0336     }
0337 
0338 public Q_SLOTS:
0339     void handleSessionCreated(quint32 code, const QVariantMap &results)
0340     {
0341         if (code != 0) {
0342             qWarning() << "Failed to create session: " << code;
0343             exit(1);
0344             return;
0345         }
0346 
0347         sessionPath = QDBusObjectPath(results.value(QStringLiteral("session_handle")).toString());
0348 
0349         // select sources for the session
0350         auto selectionOptions = QVariantMap{// We have to specify it's an uint, otherwise xdg-desktop-portal will not forward it to backend implementation
0351                                             {QStringLiteral("types"), QVariant::fromValue<uint>(7)}, // request all (KeyBoard, Pointer, TouchScreen)
0352                                             {QStringLiteral("handle_token"), createHandleToken()}};
0353         auto selectorReply = dbusXdpRemoteDesktopService->SelectDevices(sessionPath, selectionOptions);
0354         selectorReply.waitForFinished();
0355         if (!selectorReply.isValid()) {
0356             qWarning() << "Couldn't select devices for the remote-desktop session";
0357             exit(1);
0358             return;
0359         }
0360         QDBusConnection::sessionBus().connect(QString(),
0361                                               selectorReply.value().path(),
0362                                               QStringLiteral("org.freedesktop.portal.Request"),
0363                                               QStringLiteral("Response"),
0364                                               this,
0365                                               SLOT(handleDevicesSelected(uint, QVariantMap)));
0366     }
0367 
0368     void handleDevicesSelected(quint32 code, const QVariantMap &results)
0369     {
0370         Q_UNUSED(results)
0371         if (code != 0) {
0372             qWarning() << "Failed to select devices: " << code;
0373             exit(1);
0374             return;
0375         }
0376 
0377         // select sources for the session
0378         auto selectionOptions = QVariantMap{{QStringLiteral("types"), QVariant::fromValue<uint>(7)},
0379                                             {QStringLiteral("multiple"), false},
0380                                             {QStringLiteral("handle_token"), createHandleToken()}};
0381         auto selectorReply = dbusXdpScreenCastService->SelectSources(sessionPath, selectionOptions);
0382         selectorReply.waitForFinished();
0383         if (!selectorReply.isValid()) {
0384             qWarning() << "Couldn't select sources for the screen-casting session";
0385             exit(1);
0386             return;
0387         }
0388         QDBusConnection::sessionBus().connect(QString(),
0389                                               selectorReply.value().path(),
0390                                               QStringLiteral("org.freedesktop.portal.Request"),
0391                                               QStringLiteral("Response"),
0392                                               this,
0393                                               SLOT(handleSourcesSelected(uint, QVariantMap)));
0394     }
0395 
0396     void handleSourcesSelected(quint32 code, const QVariantMap &)
0397     {
0398         if (code != 0) {
0399             qWarning() << "Failed to select sources: " << code;
0400             exit(1);
0401             return;
0402         }
0403 
0404         // start session
0405         auto startParameters = QVariantMap{{QStringLiteral("handle_token"), createHandleToken()}};
0406         auto startReply = dbusXdpRemoteDesktopService->Start(sessionPath, QString(), startParameters);
0407         startReply.waitForFinished();
0408         QDBusConnection::sessionBus().connect(QString(),
0409                                               startReply.value().path(),
0410                                               QStringLiteral("org.freedesktop.portal.Request"),
0411                                               QStringLiteral("Response"),
0412                                               this,
0413                                               SLOT(handleRemoteDesktopStarted(uint, QVariantMap)));
0414     }
0415 
0416     void handleRemoteDesktopStarted(quint32 code, const QVariantMap &results)
0417     {
0418         if (code != 0) {
0419             qWarning() << "Failed to start screencast: " << code;
0420             exit(1);
0421             return;
0422         }
0423 
0424         if (results.value(QStringLiteral("devices")).toUInt() == 0) {
0425             qWarning() << "No devices were granted" << results;
0426             exit(1);
0427             return;
0428         }
0429 
0430         // there should be only one stream
0431         const Streams streams = qdbus_cast<Streams>(results.value(QStringLiteral("streams")));
0432         if (streams.isEmpty()) {
0433             // maybe we should check deeper with qdbus_cast but this suffices for now
0434             qWarning() << "Failed to get screencast streams";
0435             exit(1);
0436             return;
0437         }
0438 
0439         auto streamReply = dbusXdpScreenCastService->OpenPipeWireRemote(sessionPath, QVariantMap());
0440         streamReply.waitForFinished();
0441         if (!streamReply.isValid()) {
0442             qWarning() << "Couldn't open pipewire remote for the screen-casting session";
0443             exit(1);
0444             return;
0445         }
0446 
0447         auto pipewireFd = streamReply.value();
0448         if (!pipewireFd.isValid()) {
0449             qWarning() << "Couldn't get pipewire connection file descriptor";
0450             exit(1);
0451             return;
0452         }
0453 
0454         const uint fd = pipewireFd.takeFileDescriptor();
0455         for (auto x : streams) {
0456             createStream(x.nodeId, fd);
0457         }
0458     }
0459 
0460 private:
0461     QScopedPointer<OrgFreedesktopPortalScreenCastInterface> dbusXdpScreenCastService;
0462     QScopedPointer<OrgFreedesktopPortalRemoteDesktopInterface> dbusXdpRemoteDesktopService;
0463     QDBusObjectPath sessionPath;
0464 };
0465 
0466 int main(int argc, char **argv)
0467 {
0468     QGuiApplication app(argc, argv);
0469 
0470     {
0471         QCommandLineParser parser;
0472         const QMap<QString, Screencasting::CursorMode> cursorOptions = {
0473             {QStringLiteral("hidden"), Screencasting::CursorMode::Hidden},
0474             {QStringLiteral("embedded"), Screencasting::CursorMode::Embedded},
0475             {QStringLiteral("metadata"), Screencasting::CursorMode::Metadata},
0476         };
0477 
0478         QCommandLineOption cursorOption(QStringLiteral("cursor"),
0479                                         QStringList(cursorOptions.keys()).join(QStringLiteral(", ")),
0480                                         QStringLiteral("mode"),
0481                                         QStringLiteral("metadata"));
0482 
0483         KSignalHandler::self()->watchSignal(SIGTERM);
0484         KSignalHandler::self()->watchSignal(SIGINT);
0485 
0486         QCommandLineOption useXdpRD(QStringLiteral("xdp-remotedesktop"), QStringLiteral("Uses the XDG Desktop Portal RemoteDesktop interface"));
0487         parser.addOption(useXdpRD);
0488         QCommandLineOption useXdpSC(QStringLiteral("xdp-screencast"), QStringLiteral("Uses the XDG Desktop Portal ScreenCast interface"));
0489         parser.addOption(useXdpSC);
0490         QCommandLineOption useWorkspace(QStringLiteral("workspace"), QStringLiteral("Uses the Plasma screencasting workspace feed"));
0491         parser.addOption(useWorkspace);
0492         QCommandLineOption encodedStream(QStringLiteral("encoded"), QStringLiteral("Reports encoded streams with PipeWireEncodedStream"));
0493         parser.addOption(encodedStream);
0494         QCommandLineOption streamEncoder(QStringLiteral("encoder"),
0495                                          QStringLiteral("Which encoding to use with PipeWireEncodedStream"),
0496                                          u"encoding"_s,
0497                                          u"libvpx"_s);
0498         parser.addOption(streamEncoder);
0499         QCommandLineOption streamFramerate(QStringLiteral("framerate"),
0500                                            QStringLiteral("Makes sure a framerate is requested (format 30/1 would mean 30fps)"),
0501                                            QStringLiteral("num/denom"));
0502         parser.addOption(streamFramerate);
0503         parser.addOption(cursorOption);
0504         parser.addHelpOption();
0505         parser.process(app);
0506 
0507         s_cursorMode = cursorOptions[parser.value(cursorOption).toLower()];
0508         s_encodedStream = parser.isSet(encodedStream);
0509         if (parser.isSet(streamEncoder)) {
0510             s_encoder = parser.value(streamEncoder).toUtf8();
0511         }
0512         if (parser.isSet(streamFramerate)) {
0513             const auto framerateString = parser.value(streamFramerate).split(u'/');
0514             if (framerateString.count() != 2) {
0515                 qWarning() << "wrong framerate" << framerateString;
0516                 return 1;
0517             }
0518             s_framerate = {framerateString.constFirst().toUInt(), framerateString.constLast().toUInt()};
0519         }
0520 
0521         if (parser.isSet(useXdpRD)) {
0522             new XdpRemoteDesktop(&app);
0523         } else if (parser.isSet(useXdpSC)) {
0524             new XdpScreenCast(&app);
0525         } else if (parser.isSet(useWorkspace)) {
0526             checkPlasmaWorkspace();
0527         } else {
0528             checkPlasmaScreens();
0529         }
0530     }
0531 
0532     return app.exec();
0533 }
0534 
0535 #include "HeadlessTest.moc"