File indexing completed on 2024-04-28 16:55:47

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Red Hat Inc
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  *
0006  * SPDX-FileCopyrightText: 2018 Jan Grulich <jgrulich@redhat.com>
0007  * SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0008  */
0009 
0010 #include "screencast.h"
0011 #include "notificationinhibition.h"
0012 #include "request.h"
0013 #include "screencast_debug.h"
0014 #include "screenchooserdialog.h"
0015 #include "session.h"
0016 #include "utils.h"
0017 #include "waylandintegration.h"
0018 
0019 #include <KConfigGroup>
0020 #include <KLocalizedString>
0021 #include <KSharedConfig>
0022 #include <KWayland/Client/plasmawindowmodel.h>
0023 
0024 #include <QDBusArgument>
0025 #include <QDBusMetaType>
0026 #include <QDataStream>
0027 #include <QIODevice>
0028 
0029 struct RestoreData {
0030     static uint currentRestoreDataVersion()
0031     {
0032         return 1;
0033     }
0034 
0035     QString session;
0036     quint32 version = 0;
0037     QVariantMap payload;
0038 };
0039 
0040 const QDBusArgument &operator<<(QDBusArgument &arg, const RestoreData &data)
0041 {
0042     arg.beginStructure();
0043     arg << data.session;
0044     arg << data.version;
0045 
0046     QByteArray payloadSerialised;
0047     {
0048         QDataStream ds(&payloadSerialised, QIODevice::WriteOnly);
0049         ds << data.payload;
0050     }
0051 
0052     arg << QDBusVariant(payloadSerialised);
0053     arg.endStructure();
0054     return arg;
0055 }
0056 const QDBusArgument &operator>>(const QDBusArgument &arg, RestoreData &data)
0057 {
0058     arg.beginStructure();
0059     arg >> data.session;
0060     arg >> data.version;
0061 
0062     QDBusVariant payloadVariant;
0063     arg >> payloadVariant;
0064     {
0065         QByteArray payloadSerialised = payloadVariant.variant().toByteArray();
0066         QDataStream ds(&payloadSerialised, QIODevice::ReadOnly);
0067         ds >> data.payload;
0068     }
0069     arg.endStructure();
0070     return arg;
0071 }
0072 
0073 QDebug operator<<(QDebug dbg, const RestoreData &c)
0074 {
0075     dbg.nospace() << "RestoreData(" << c.session << ", " << c.version << ", " << c.payload << ")";
0076     return dbg.space();
0077 }
0078 
0079 Q_DECLARE_METATYPE(RestoreData)
0080 
0081 ScreenCastPortal::ScreenCastPortal(QObject *parent)
0082     : QDBusAbstractAdaptor(parent)
0083 {
0084     qDBusRegisterMetaType<RestoreData>();
0085 }
0086 
0087 ScreenCastPortal::~ScreenCastPortal()
0088 {
0089 }
0090 
0091 bool ScreenCastPortal::inhibitionsEnabled() const
0092 {
0093     if (!WaylandIntegration::isStreamingAvailable()) {
0094         return false;
0095     }
0096 
0097     auto cfg = KSharedConfig::openConfig(QStringLiteral("plasmanotifyrc"));
0098 
0099     KConfigGroup grp(cfg, "DoNotDisturb");
0100 
0101     return grp.readEntry("WhenScreenSharing", true);
0102 }
0103 
0104 uint ScreenCastPortal::CreateSession(const QDBusObjectPath &handle,
0105                                      const QDBusObjectPath &session_handle,
0106                                      const QString &app_id,
0107                                      const QVariantMap &options,
0108                                      QVariantMap &results)
0109 {
0110     Q_UNUSED(results)
0111 
0112     qCDebug(XdgDesktopPortalKdeScreenCast) << "CreateSession called with parameters:";
0113     qCDebug(XdgDesktopPortalKdeScreenCast) << "    handle: " << handle.path();
0114     qCDebug(XdgDesktopPortalKdeScreenCast) << "    session_handle: " << session_handle.path();
0115     qCDebug(XdgDesktopPortalKdeScreenCast) << "    app_id: " << app_id;
0116     qCDebug(XdgDesktopPortalKdeScreenCast) << "    options: " << options;
0117 
0118     Session *session = Session::createSession(this, Session::ScreenCast, app_id, session_handle.path());
0119 
0120     if (!session) {
0121         return 2;
0122     }
0123 
0124     if (!WaylandIntegration::isStreamingAvailable()) {
0125         qCWarning(XdgDesktopPortalKdeScreenCast) << "zkde_screencast_unstable_v1 does not seem to be available";
0126         return 2;
0127     }
0128 
0129     connect(session, &Session::closed, [session] {
0130         auto screencastSession = qobject_cast<ScreenCastSession *>(session);
0131         const auto streams = screencastSession->streams();
0132         for (const WaylandIntegration::Stream &stream : streams) {
0133             WaylandIntegration::stopStreaming(stream.nodeId);
0134         }
0135     });
0136     return 0;
0137 }
0138 
0139 uint ScreenCastPortal::SelectSources(const QDBusObjectPath &handle,
0140                                      const QDBusObjectPath &session_handle,
0141                                      const QString &app_id,
0142                                      const QVariantMap &options,
0143                                      QVariantMap &results)
0144 {
0145     Q_UNUSED(results)
0146 
0147     qCDebug(XdgDesktopPortalKdeScreenCast) << "SelectSource called with parameters:";
0148     qCDebug(XdgDesktopPortalKdeScreenCast) << "    handle: " << handle.path();
0149     qCDebug(XdgDesktopPortalKdeScreenCast) << "    session_handle: " << session_handle.path();
0150     qCDebug(XdgDesktopPortalKdeScreenCast) << "    app_id: " << app_id;
0151     qCDebug(XdgDesktopPortalKdeScreenCast) << "    options: " << options;
0152 
0153     ScreenCastSession *session = qobject_cast<ScreenCastSession *>(Session::getSession(session_handle.path()));
0154 
0155     if (!session) {
0156         qCWarning(XdgDesktopPortalKdeScreenCast) << "Tried to select sources on non-existing session " << session_handle.path();
0157         return 2;
0158     }
0159 
0160     session->setOptions(options);
0161 
0162     // Might be also a RemoteDesktopSession
0163     if (session->type() == Session::RemoteDesktop) {
0164         RemoteDesktopSession *remoteDesktopSession = qobject_cast<RemoteDesktopSession *>(session);
0165         if (remoteDesktopSession) {
0166             remoteDesktopSession->setScreenSharingEnabled(true);
0167         }
0168     }
0169 
0170     return 0;
0171 }
0172 
0173 uint ScreenCastPortal::Start(const QDBusObjectPath &handle,
0174                              const QDBusObjectPath &session_handle,
0175                              const QString &app_id,
0176                              const QString &parent_window,
0177                              const QVariantMap &options,
0178                              QVariantMap &results)
0179 {
0180     qCDebug(XdgDesktopPortalKdeScreenCast) << "Start called with parameters:";
0181     qCDebug(XdgDesktopPortalKdeScreenCast) << "    handle: " << handle.path();
0182     qCDebug(XdgDesktopPortalKdeScreenCast) << "    session_handle: " << session_handle.path();
0183     qCDebug(XdgDesktopPortalKdeScreenCast) << "    app_id: " << app_id;
0184     qCDebug(XdgDesktopPortalKdeScreenCast) << "    parent_window: " << parent_window;
0185     qCDebug(XdgDesktopPortalKdeScreenCast) << "    options: " << options;
0186 
0187     QPointer<ScreenCastSession> session = qobject_cast<ScreenCastSession *>(Session::getSession(session_handle.path()));
0188 
0189     if (!session) {
0190         qCWarning(XdgDesktopPortalKdeScreenCast) << "Tried to call start on non-existing session " << session_handle.path();
0191         return 2;
0192     }
0193 
0194     if (WaylandIntegration::screens().isEmpty()) {
0195         qCWarning(XdgDesktopPortalKdeScreenCast) << "Failed to show dialog as there is no screen to select";
0196         return 2;
0197     }
0198 
0199     const PersistMode persist = session->persistMode();
0200     bool valid = false;
0201     QList<Output> selectedOutputs;
0202     QVector<QMap<int, QVariant>> selectedWindows;
0203     if (persist != NoPersist && session->restoreData().isValid()) {
0204         const RestoreData restoreData = qdbus_cast<RestoreData>(session->restoreData().value<QDBusArgument>());
0205         if (restoreData.session == QLatin1String("KDE") && restoreData.version == RestoreData::currentRestoreDataVersion()) {
0206             const QVariantMap restoreDataPayload = restoreData.payload;
0207             const QVariantList restoreOutputs = restoreDataPayload[QStringLiteral("outputs")].toList();
0208             if (!restoreOutputs.isEmpty()) {
0209                 OutputsModel model(OutputsModel::WorkspaceIncluded, this);
0210                 for (const auto &outputUniqueId : restoreOutputs) {
0211                     for (int i = 0, c = model.rowCount(); i < c; ++i) {
0212                         const Output &iOutput = model.outputAt(i);
0213                         if (iOutput.uniqueId() == outputUniqueId) {
0214                             selectedOutputs << iOutput;
0215                         }
0216                     }
0217                 }
0218                 valid = selectedOutputs.count() == restoreOutputs.count();
0219             }
0220 
0221             const QStringList restoreWindows = restoreDataPayload[QStringLiteral("windows")].toStringList();
0222             if (!restoreWindows.isEmpty()) {
0223                 const KWayland::Client::PlasmaWindowModel model(WaylandIntegration::plasmaWindowManagement());
0224                 for (const QString &windowUuid : restoreWindows) {
0225                     for (int i = 0, c = model.rowCount(); i < c; ++i) {
0226                         const QModelIndex index = model.index(i, 0);
0227 
0228                         if (model.data(index, KWayland::Client::PlasmaWindowModel::Uuid) == windowUuid) {
0229                             selectedWindows << model.itemData(index);
0230                         }
0231                     }
0232                 }
0233                 valid = selectedWindows.count() == restoreWindows.count();
0234             }
0235         }
0236     }
0237 
0238     bool allowRestore = valid;
0239     if (!valid) {
0240         QScopedPointer<ScreenChooserDialog, QScopedPointerDeleteLater> screenDialog(
0241             new ScreenChooserDialog(app_id, session->multipleSources(), SourceTypes(session->types())));
0242         connect(session, &Session::closed, screenDialog.data(), &ScreenChooserDialog::reject);
0243         Utils::setParentWindow(screenDialog->windowHandle(), parent_window);
0244         Request::makeClosableDialogRequest(handle, screenDialog.get());
0245         valid = screenDialog->exec();
0246         if (valid) {
0247             allowRestore = screenDialog->allowRestore();
0248             selectedOutputs = screenDialog->selectedOutputs();
0249             selectedWindows = screenDialog->selectedWindows();
0250         }
0251     }
0252 
0253     if (valid && session) {
0254         QVariantList outputs;
0255         QStringList windows;
0256         WaylandIntegration::Streams streams;
0257         Screencasting::CursorMode cursorMode = Screencasting::CursorMode(session->cursorMode());
0258         for (const auto &output : qAsConst(selectedOutputs)) {
0259             WaylandIntegration::Stream stream;
0260             switch (output.outputType()) {
0261             case WaylandIntegration::WaylandOutput::Workspace:
0262                 stream = WaylandIntegration::startStreamingWorkspace(cursorMode);
0263                 break;
0264             case WaylandIntegration::WaylandOutput::Virtual:
0265                 stream = WaylandIntegration::startStreamingVirtual(output.uniqueId(), {1920, 1080}, cursorMode);
0266                 break;
0267             default:
0268                 stream = WaylandIntegration::startStreamingOutput(output.waylandOutputName(), cursorMode);
0269                 break;
0270             }
0271 
0272             if (!stream.isValid()) {
0273                 qCWarning(XdgDesktopPortalKdeScreenCast) << "Invalid screen!" << output.outputType() << output.uniqueId();
0274                 return 2;
0275             }
0276 
0277             if (allowRestore) {
0278                 outputs += output.uniqueId();
0279             }
0280             streams << stream;
0281         }
0282         for (const auto &win : qAsConst(selectedWindows)) {
0283             WaylandIntegration::Stream stream = WaylandIntegration::startStreamingWindow(win, cursorMode);
0284             if (!stream.isValid()) {
0285                 qCWarning(XdgDesktopPortalKdeScreenCast) << "Invalid window!" << win;
0286                 return 2;
0287             }
0288 
0289             if (allowRestore) {
0290                 windows += win[KWayland::Client::PlasmaWindowModel::Uuid].toString();
0291             }
0292             streams << stream;
0293         }
0294 
0295         if (streams.isEmpty()) {
0296             qCWarning(XdgDesktopPortalKdeScreenCast) << "Pipewire stream is not ready to be streamed";
0297             return 2;
0298         }
0299 
0300         session->setStreams(streams);
0301         results.insert(QStringLiteral("streams"), QVariant::fromValue<WaylandIntegration::Streams>(streams));
0302         if (allowRestore) {
0303             results.insert("persist_mode", quint32(persist));
0304             if (persist != NoPersist) {
0305                 const RestoreData restoreData = {"KDE",
0306                                                  RestoreData::currentRestoreDataVersion(),
0307                                                  QVariantMap{
0308                                                      {"outputs", outputs},
0309                                                      {"windows", windows},
0310                                                  }};
0311                 results.insert("restore_data", QVariant::fromValue<RestoreData>(restoreData));
0312             }
0313         }
0314 
0315         if (inhibitionsEnabled()) {
0316             new NotificationInhibition(app_id, i18nc("Do not disturb mode is enabled because...", "Screen sharing in progress"), session);
0317         }
0318         return 0;
0319     }
0320 
0321     return 1;
0322 }