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
|