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 }