File indexing completed on 2024-12-29 05:17:37

0001 /*
0002  * App to render feeds coming from xdg-desktop-portal
0003  *
0004  * SPDX-License-Identifier: LicenseRef-KDE-Accepted-GPL
0005  * SPDX-FileCopyrightText: 2023 David Edmundson <kde@davidedmundson.co.uk>
0006  * SPDX-FileCopyrightText: 2023 Aleix Pol <aleixpol@kde.org>
0007  */
0008 
0009 #include "xwaylandvideobridge.h"
0010 #include <QGuiApplication>
0011 #include <QLoggingCategory>
0012 #include <QTimer>
0013 #include <QQuickWindow>
0014 #include <QAction>
0015 
0016 #include <KLocalizedString>
0017 #include <KFileUtils>
0018 #include <KStatusNotifierItem>
0019 
0020 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0021 #include <PipeWireSourceItem>
0022 #else
0023 #include <KPipeWire/pipewiresourceitem.h>
0024 #endif
0025 
0026 #include "xdp_dbus_screencast_interface.h"
0027 #include "contentswindow.h"
0028 #include "x11recordingnotifier.h"
0029 #include "xwaylandvideobridge_debug.h"
0030 
0031 Q_DECLARE_METATYPE(Stream)
0032 
0033 QDebug operator<<(QDebug debug, const Stream& stream)
0034 {
0035     QDebugStateSaver saver(debug);
0036     debug.nospace() << "Stream(id: " << stream.nodeId << ", opts: " << stream.opts << ')';
0037     return debug;
0038 }
0039 
0040 
0041 const QDBusArgument &operator<<(const QDBusArgument &argument, const Stream &/*stream*/)
0042 {
0043     argument.beginStructure();
0044     //     argument << stream.id << stream.opts;
0045     argument.endStructure();
0046     return argument;
0047 }
0048 
0049 const QDBusArgument &operator>>(const QDBusArgument &argument, Stream &stream)
0050 {
0051     argument.beginStructure();
0052     argument >> stream.nodeId >> stream.opts;
0053     argument.endStructure();
0054     return argument;
0055 }
0056 
0057 const QDBusArgument &operator>>(const QDBusArgument &argument, QVector<Stream> &stream)
0058 {
0059     argument.beginArray();
0060     while ( !argument.atEnd() ) {
0061         Stream element;
0062         argument >> element;
0063         stream.append( element );
0064     }
0065     argument.endArray();
0066     return argument;
0067 }
0068 
0069 XwaylandVideoBridge::XwaylandVideoBridge(QObject* parent)
0070     : QObject(parent)
0071     , iface(new OrgFreedesktopPortalScreenCastInterface(
0072                 QLatin1String("org.freedesktop.portal.Desktop"), QLatin1String("/org/freedesktop/portal/desktop"), QDBusConnection::sessionBus(), this))
0073     , m_handleToken(QStringLiteral("xwaylandvideobridge%1").arg(QRandomGenerator::global()->generate()))
0074     , m_quitTimer(new QTimer(this))
0075     , m_window(new ContentsWindow)
0076     , m_sni(new KStatusNotifierItem("pipewireToXProxy", this))
0077 {
0078     m_quitTimer->setInterval(5000);
0079     m_quitTimer->setSingleShot(true);
0080     connect(m_quitTimer, &QTimer::timeout, this, &XwaylandVideoBridge::closeSession);
0081 
0082 
0083 
0084 
0085     auto notifier = new X11RecordingNotifier(m_window->winId(), this);
0086 
0087     connect(notifier, &X11RecordingNotifier::isRedirectedChanged, this, [this, notifier]() {
0088         if (notifier->isRedirected()) {
0089             m_quitTimer->stop();
0090             // this is a bit racey, there's a point where we wait for a reply from the portal
0091             if (m_path.path().isEmpty()) {
0092                 init();
0093             }
0094         } else {
0095             m_quitTimer->start();
0096         }
0097     });
0098 
0099     m_sni->setTitle("Wayland to X11 Video bridge");
0100     m_sni->setIconByName("video-display");
0101     auto quitAction = new QAction(i18n("Quit"), this);
0102     connect(quitAction, &QAction::triggered, qApp, &QGuiApplication::quit);
0103     m_sni->addAction("quitAction", quitAction);
0104     m_sni->setStatus(KStatusNotifierItem::Passive);
0105 
0106     m_window->show();
0107 }
0108 
0109 XwaylandVideoBridge::~XwaylandVideoBridge() = default;
0110 
0111 void XwaylandVideoBridge::startStream(const QDBusObjectPath& path)
0112 {
0113     m_path = path;
0114 
0115     CursorModes availableCursorModes = static_cast<CursorModes>(iface->availableCursorModes());
0116     CursorMode cursorMode = CursorMode::Hidden;
0117     if (availableCursorModes.testFlag(CursorMode::Metadata)) {
0118         cursorMode = CursorMode::Metadata;
0119     } else if (availableCursorModes.testFlag(CursorMode::Embedded)) {
0120         cursorMode = CursorMode::Embedded;
0121     } else {
0122         qCWarning(XWAYLANDBRIDGE) << "Portal does not support any cursor modes. Cursors will be hidden";
0123     }
0124 
0125     const QVariantMap sourcesParameters = {
0126         { QLatin1String("handle_token"), m_handleToken },
0127         { QLatin1String("types"), iface->availableSourceTypes() },
0128         { QLatin1String("multiple"), false }, //for now?
0129         { QLatin1String("cursor_mode"), static_cast<uint>(cursorMode) }
0130     };
0131 
0132     auto reply = iface->SelectSources(m_path, sourcesParameters);
0133     reply.waitForFinished();
0134 
0135     if (reply.isError()) {
0136         qCWarning(XWAYLANDBRIDGE) << "Could not select sources" << reply.error();
0137         exit(1);
0138         return;
0139     }
0140     qCDebug(XWAYLANDBRIDGE) << "select sources done" << reply.value().path();
0141 }
0142 
0143 void XwaylandVideoBridge::response(uint code, const QVariantMap& results)
0144 {
0145     if (code == 1) {
0146         qCDebug(XWAYLANDBRIDGE) << "XDG session cancelled";
0147         closeSession();
0148         return;
0149     } else if (code > 0) {
0150         qCWarning(XWAYLANDBRIDGE) << "XDG session failed:" << results << code;
0151         exit(1);
0152         return;
0153     }
0154 
0155     const auto streamsIt = results.constFind("streams");
0156     if (streamsIt != results.constEnd()) {
0157         QVector<Stream> streams;
0158         streamsIt->value<QDBusArgument>() >> streams;
0159 
0160         handleStreams(streams);
0161         return;
0162     }
0163 
0164     const auto handleIt = results.constFind(QStringLiteral("session_handle"));
0165     if (handleIt != results.constEnd()) {
0166         startStream(QDBusObjectPath(handleIt->toString()));
0167         return;
0168     }
0169 
0170     qCDebug(XWAYLANDBRIDGE) << "params" << results << code;
0171     if (results.isEmpty()) {
0172         start();
0173         return;
0174     }
0175 }
0176 
0177 void XwaylandVideoBridge::init()
0178 {
0179     const QVariantMap sessionParameters = {
0180         { QLatin1String("session_handle_token"), m_handleToken },
0181         { QLatin1String("handle_token"), m_handleToken }
0182     };
0183     auto sessionReply = iface->CreateSession(sessionParameters);
0184     sessionReply.waitForFinished();
0185     if (!sessionReply.isValid()) {
0186         qCWarning(XWAYLANDBRIDGE) << "Couldn't initialize the remote control session";
0187         exit(1);
0188         return;
0189     }
0190 
0191     const bool ret = QDBusConnection::sessionBus().connect(QString(),
0192                                                            sessionReply.value().path(),
0193                                                            QLatin1String("org.freedesktop.portal.Request"),
0194                                                            QLatin1String("Response"),
0195                                                            this,
0196                                                            SLOT(response(uint, QVariantMap)));
0197     if (!ret) {
0198         qCWarning(XWAYLANDBRIDGE) << "failed to create session";
0199         exit(2);
0200         return;
0201     }
0202 
0203     qDBusRegisterMetaType<Stream>();
0204     qDBusRegisterMetaType<QVector<Stream>>();
0205 }
0206 
0207 void XwaylandVideoBridge::start()
0208 {
0209     const QVariantMap startParameters = {
0210         { QLatin1String("handle_token"), m_handleToken }
0211     };
0212 
0213     auto reply = iface->Start(m_path, QStringLiteral("org.kde.xwaylandvideobridge"), startParameters);
0214     reply.waitForFinished();
0215 
0216     if (reply.isError()) {
0217         qCWarning(XWAYLANDBRIDGE) << "Could not start stream" << reply.error();
0218         exit(1);
0219         return;
0220     }
0221 }
0222 
0223 void XwaylandVideoBridge::handleStreams(const QVector<Stream> &streams)
0224 {
0225     m_sni->setStatus(KStatusNotifierItem::Active);
0226 
0227     const QVariantMap startParameters = {
0228         { QLatin1String("handle_token"), m_handleToken }
0229     };
0230 
0231     auto reply = iface->OpenPipeWireRemote(m_path, startParameters);
0232     reply.waitForFinished();
0233 
0234     if (reply.isError()) {
0235         qCWarning(XWAYLANDBRIDGE) << "Could not start stream" << reply.error();
0236         exit(1);
0237         return;
0238     }
0239     const int fd = reply.value().takeFileDescriptor();
0240 
0241     if (streams.count() < 1) {
0242         qCWarning(XWAYLANDBRIDGE) << "No streams available";
0243         exit(1);
0244     }
0245 
0246     m_pipeWireItem = new PipeWireSourceItem(m_window->contentItem());
0247     m_pipeWireItem->setFd(fd);
0248     m_pipeWireItem->setNodeId(streams[0].nodeId);
0249     m_pipeWireItem->setVisible(true);
0250 
0251     if (m_pipeWireItem->state() == PipeWireSourceItem::StreamState::Streaming) {
0252         m_pipeWireItem->setSize(m_pipeWireItem->streamSize());
0253         m_window->resize(m_pipeWireItem->size().toSize());
0254     }
0255 
0256     connect(m_pipeWireItem, &PipeWireSourceItem::streamSizeChanged, this, [this]() {
0257         m_pipeWireItem->setSize(m_pipeWireItem->streamSize());
0258     });
0259 
0260 
0261     connect(m_pipeWireItem, &QQuickItem::widthChanged, this, [this]() {
0262         m_window->resize(m_pipeWireItem->size().toSize());
0263     });
0264     connect(m_pipeWireItem, &QQuickItem::heightChanged, this, [this]() {
0265         m_window->resize(m_pipeWireItem->size().toSize());
0266     });
0267 
0268     connect(m_pipeWireItem, &PipeWireSourceItem::stateChanged, this, [this]{
0269         if (m_pipeWireItem->state() == PipeWireSourceItem::StreamState::Unconnected) {
0270             closeSession();
0271         }
0272     });
0273 }
0274 
0275 void XwaylandVideoBridge::closeSession()
0276 {
0277 
0278     m_handleToken = QStringLiteral("xwaylandvideobridge%1").arg(QRandomGenerator::global()->generate());
0279     m_quitTimer->stop();
0280     m_sni->setStatus(KStatusNotifierItem::Passive);
0281 
0282     if (m_path.path().isEmpty())
0283         return;
0284     QDBusMessage closeScreencastSession = QDBusMessage::createMethodCall(QLatin1String("org.freedesktop.portal.Desktop"),
0285                                                                          m_path.path(),
0286                                                                          QLatin1String("org.freedesktop.portal.Session"),
0287                                                                          QLatin1String("Close"));
0288     m_path = {};
0289 
0290     if (m_pipeWireItem) {
0291         disconnect(m_pipeWireItem, nullptr, this, nullptr);
0292         m_pipeWireItem->deleteLater();
0293         m_pipeWireItem = nullptr;
0294     }
0295     QDBusConnection::sessionBus().call(closeScreencastSession);
0296 }