File indexing completed on 2024-12-08 04:55:26
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.dataFrame) { 0092 qDebug() << "image" << frame.dataFrame->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"