diff options
Diffstat (limited to 'src/voip/ScreenCastPortal.cpp')
-rw-r--r-- | src/voip/ScreenCastPortal.cpp | 516 |
1 files changed, 516 insertions, 0 deletions
diff --git a/src/voip/ScreenCastPortal.cpp b/src/voip/ScreenCastPortal.cpp new file mode 100644 index 00000000..31cddba0 --- /dev/null +++ b/src/voip/ScreenCastPortal.cpp @@ -0,0 +1,516 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifdef GSTREAMER_AVAILABLE + +#include "ScreenCastPortal.h" +#include "ChatPage.h" +#include "Logging.h" +#include "UserSettingsPage.h" + +#include <QDBusConnection> +#include <QDBusMessage> +#include <QDBusPendingCallWatcher> +#include <QDBusPendingReply> +#include <QDBusUnixFileDescriptor> +#include <mtxclient/utils.hpp> +#include <random> + +static QString +make_token() +{ + return QString::fromStdString("nheko" + mtx::client::utils::random_token(64, false)); +} + +static QString +handle_path(QString handle_token) +{ + QString sender = QDBusConnection::sessionBus().baseService(); + if (sender[0] == ':') + sender.remove(0, 1); + sender.replace(".", "_"); + return QStringLiteral("/org/freedesktop/portal/desktop/request/") + sender + + QStringLiteral("/") + handle_token; +} + +bool +ScreenCastPortal::makeConnection(QString service, + QString path, + QString interface, + QString name, + const char *slot) +{ + if (QDBusConnection::sessionBus().connect(service, path, interface, name, this, slot)) { + last_connection = { + std::move(service), std::move(path), std::move(interface), std::move(name), slot}; + return true; + } + return false; +} + +void +ScreenCastPortal::disconnectClose() +{ + QDBusConnection::sessionBus().disconnect(QStringLiteral("org.freedesktop.portal.Desktop"), + sessionHandle.path(), + QStringLiteral("org.freedesktop.portal.Session"), + QStringLiteral("Closed"), + this, + SLOT(closedHandler(QVariantMap))); +} + +void +ScreenCastPortal::removeConnection() +{ + if (!last_connection.has_value()) + return; + + const auto &connection = *last_connection; + QDBusConnection::sessionBus().disconnect(connection[0], + connection[1], + connection[2], + connection[3], + this, + connection[4].toLocal8Bit().data()); + last_connection = std::nullopt; +} + +void +ScreenCastPortal::init() +{ + switch (state) { + case State::Closed: + state = State::Starting; + createSession(); + break; + case State::Starting: + nhlog::ui()->warn("ScreenCastPortal already starting"); + break; + case State::Started: + close(true); + break; + case State::Closing: + nhlog::ui()->warn("ScreenCastPortal still closing"); + break; + } +} + +const ScreenCastPortal::Stream * +ScreenCastPortal::getStream() const +{ + if (state != State::Started) + return nullptr; + else + return &stream; +} + +bool +ScreenCastPortal::ready() const +{ + return state == State::Started; +} + +void +ScreenCastPortal::close(bool reinit) +{ + switch (state) { + case State::Closed: + if (reinit) + init(); + break; + case State::Starting: + if (!reinit) { + disconnectClose(); + removeConnection(); + state = State::Closed; + } + break; + case State::Started: { + state = State::Closing; + disconnectClose(); + // Close file descriptor if it was opened + stream = Stream{}; + + emit readyChanged(); + + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.portal.Desktop"), + sessionHandle.path(), + QStringLiteral("org.freedesktop.portal.Session"), + QStringLiteral("Close")); + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect(watcher, + &QDBusPendingCallWatcher::finished, + this, + [this, reinit](QDBusPendingCallWatcher *self) { + self->deleteLater(); + QDBusPendingReply reply = *self; + + if (!reply.isValid()) { + nhlog::ui()->warn("org.freedesktop.portal.ScreenCast (Close): {}", + reply.error().message().toStdString()); + } + state = State::Closed; + if (reinit) + init(); + }); + } break; + case State::Closing: + nhlog::ui()->warn("ScreenCastPortal already closing"); + break; + } +} + +void +ScreenCastPortal::closedHandler(uint response, const QVariantMap &) +{ + removeConnection(); + disconnectClose(); + + if (response != 0) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast (Closed): {}", response); + } + + nhlog::ui()->debug("org.freedesktop.portal.ScreenCast: Connection closed"); + state = State::Closed; + emit readyChanged(); +} + +void +ScreenCastPortal::createSession() +{ + // Connect before sending the request to avoid missing the reply + QString handle_token = make_token(); + if (!makeConnection(QStringLiteral("org.freedesktop.portal.Desktop"), + handle_path(handle_token), + QStringLiteral("org.freedesktop.portal.Request"), + QStringLiteral("Response"), + SLOT(createSessionHandler(uint, QVariantMap)))) { + nhlog::ui()->error( + "Connection to signal Response for org.freedesktop.portal.Request failed"); + close(); + return; + } + + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.ScreenCast"), + QStringLiteral("CreateSession")); + msg << QVariantMap{{QStringLiteral("handle_token"), handle_token}, + {QStringLiteral("session_handle_token"), make_token()}}; + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect( + watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + QDBusPendingReply<QDBusObjectPath> reply = *self; + + if (!reply.isValid()) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast (CreateSession): {}", + reply.error().message().toStdString()); + close(); + } + }); +} + +void +ScreenCastPortal::createSessionHandler(uint response, const QVariantMap &results) +{ + removeConnection(); + + if (state != State::Starting) { + nhlog::ui()->warn("ScreenCastPortal not starting"); + return; + } + if (response != 0) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast (CreateSession Response): {}", + response); + close(); + return; + } + + sessionHandle = QDBusObjectPath(results.value(QStringLiteral("session_handle")).toString()); + + nhlog::ui()->debug("org.freedesktop.portal.ScreenCast: sessionHandle = {}", + sessionHandle.path().toStdString()); + + QDBusConnection::sessionBus().connect(QStringLiteral("org.freedesktop.portal.Desktop"), + sessionHandle.path(), + QStringLiteral("org.freedesktop.portal.Session"), + QStringLiteral("Closed"), + this, + SLOT(closedHandler(QVariantMap))); + + getAvailableSourceTypes(); +} + +void +ScreenCastPortal::getAvailableSourceTypes() +{ + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("Get")); + msg << QStringLiteral("org.freedesktop.portal.ScreenCast") + << QStringLiteral("AvailableSourceTypes"); + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect( + watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + QDBusPendingReply<QDBusVariant> reply = *self; + + if (!reply.isValid()) { + nhlog::ui()->error("org.freedesktop.DBus.Properties (Get AvailableSourceTypes): {}", + reply.error().message().toStdString()); + close(); + return; + } + + if (state != State::Starting) { + nhlog::ui()->warn("ScreenCastPortal not starting"); + return; + } + const auto &value = reply.value().variant(); + if (value.canConvert<uint>()) { + availableSourceTypes = value.value<uint>(); + } else { + nhlog::ui()->error("Invalid reply from org.freedesktop.DBus.Properties (Get " + "AvailableSourceTypes)"); + close(); + return; + } + + getAvailableCursorModes(); + }); +} + +void +ScreenCastPortal::getAvailableCursorModes() +{ + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("Get")); + msg << QStringLiteral("org.freedesktop.portal.ScreenCast") + << QStringLiteral("AvailableCursorModes"); + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect( + watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + QDBusPendingReply<QDBusVariant> reply = *self; + + if (!reply.isValid()) { + nhlog::ui()->error("org.freedesktop.DBus.Properties (Get AvailableCursorModes): {}", + reply.error().message().toStdString()); + close(); + return; + } + + if (state != State::Starting) { + nhlog::ui()->warn("ScreenCastPortal not starting"); + return; + } + const auto &value = reply.value().variant(); + if (value.canConvert<uint>()) { + availableCursorModes = value.value<uint>(); + } else { + nhlog::ui()->error("Invalid reply from org.freedesktop.DBus.Properties (Get " + "AvailableCursorModes)"); + close(); + return; + } + + selectSources(); + }); +} + +void +ScreenCastPortal::selectSources() +{ + // Connect before sending the request to avoid missing the reply + auto handle_token = make_token(); + if (!makeConnection(QString(), + handle_path(handle_token), + QStringLiteral("org.freedesktop.portal.Request"), + QStringLiteral("Response"), + SLOT(selectSourcesHandler(uint, QVariantMap)))) { + nhlog::ui()->error( + "Connection to signal Response for org.freedesktop.portal.Request failed"); + close(); + return; + } + + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.ScreenCast"), + QStringLiteral("SelectSources")); + + QVariantMap options{{QStringLiteral("multiple"), false}, + {QStringLiteral("types"), availableSourceTypes}, + {QStringLiteral("handle_token"), handle_token}}; + + auto settings = ChatPage::instance()->userSettings(); + if (settings->screenShareHideCursor() && (availableCursorModes & (uint)1) != 0) { + options["cursor_mode"] = (uint)1; + } + + msg << QVariant::fromValue(sessionHandle) << options; + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect( + watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + QDBusPendingReply<QDBusObjectPath> reply = *self; + + if (!reply.isValid()) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast (SelectSources): {}", + reply.error().message().toStdString()); + close(); + } + }); +} + +void +ScreenCastPortal::selectSourcesHandler(uint response, const QVariantMap &) +{ + removeConnection(); + + if (state != State::Starting) { + nhlog::ui()->warn("ScreenCastPortal not starting"); + return; + } + if (response != 0) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast (SelectSources Response): {}", + response); + close(); + return; + } + start(); +} + +void +ScreenCastPortal::start() +{ + // Connect before sending the request to avoid missing the reply + auto handle_token = make_token(); + if (!makeConnection(QString(), + handle_path(handle_token), + QStringLiteral("org.freedesktop.portal.Request"), + QStringLiteral("Response"), + SLOT(startHandler(uint, QVariantMap)))) { + nhlog::ui()->error("Connection to org.freedesktop.portal.Request Response failed"); + close(); + return; + } + + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.ScreenCast"), + QStringLiteral("Start")); + msg << QVariant::fromValue(sessionHandle) << QString() + << QVariantMap{{QStringLiteral("handle_token"), handle_token}}; + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect( + watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + QDBusPendingReply<QDBusObjectPath> reply = *self; + + if (!reply.isValid()) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast (Start): {}", + reply.error().message().toStdString()); + } + }); +} + +struct PipeWireStream +{ + quint32 nodeId = 0; + QVariantMap map; +}; + +Q_DECLARE_METATYPE(PipeWireStream) + +const QDBusArgument & +operator>>(const QDBusArgument &argument, PipeWireStream &stream) +{ + argument.beginStructure(); + argument >> stream.nodeId; + argument.beginMap(); + while (!argument.atEnd()) { + QString key; + QVariant map; + argument.beginMapEntry(); + argument >> key >> map; + argument.endMapEntry(); + stream.map.insert(key, map); + } + argument.endMap(); + argument.endStructure(); + return argument; +} + +void +ScreenCastPortal::startHandler(uint response, const QVariantMap &results) +{ + removeConnection(); + + if (response != 0) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast (Start Response): {}", response); + close(); + return; + } + + QVector<PipeWireStream> streams = + qdbus_cast<QVector<PipeWireStream>>(results.value(QStringLiteral("streams"))); + if (streams.size() == 0) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast: No stream was returned"); + close(); + return; + } + + stream.nodeId = streams[0].nodeId; + nhlog::ui()->debug("org.freedesktop.portal.ScreenCast: nodeId = {}", stream.nodeId); + openPipeWireRemote(); +} + +void +ScreenCastPortal::openPipeWireRemote() +{ + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.ScreenCast"), + QStringLiteral("OpenPipeWireRemote")); + msg << QVariant::fromValue(sessionHandle) << QVariantMap{}; + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect( + watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + QDBusPendingReply<QDBusUnixFileDescriptor> reply = *self; + + if (!reply.isValid()) { + nhlog::ui()->error("org.freedesktop.portal.ScreenCast (OpenPipeWireRemote): {}", + reply.error().message().toStdString()); + close(); + } else { + stream.fd = std::move(reply.value()); + nhlog::ui()->error("org.freedesktop.portal.ScreenCast: fd = {}", + stream.fd.fileDescriptor()); + state = State::Started; + emit readyChanged(); + } + }); +} + +#endif |