File indexing completed on 2024-12-01 11:07:07
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"