diff --git a/src/voip/ScreenCastPortal.cpp b/src/voip/ScreenCastPortal.cpp
new file mode 100644
index 00000000..321373d9
--- /dev/null
+++ b/src/voip/ScreenCastPortal.cpp
@@ -0,0 +1,471 @@
+#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 <random>
+
+static QString
+make_token()
+{
+ thread_local std::random_device rng;
+ std::uniform_int_distribution<char> index_dist(0, 9);
+
+ std::string token;
+ token.reserve(5 + 64);
+ token += "nheko";
+
+ for (uint8_t i = 0; i < 64; ++i)
+ token.push_back('0' + index_dist(rng));
+
+ return QString::fromStdString(std::move(token));
+}
+
+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;
+}
+
+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) {
+ // Remaining handler will abort.
+ state = State::Closed;
+ }
+ break;
+ case State::Started: {
+ state = State::Closing;
+ 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) {
+ 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 &)
+{
+ if (response != 0) {
+ nhlog::ui()->error("org.freedekstop.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();
+ QDBusConnection::sessionBus().connect(QStringLiteral("org.freedesktop.portal.Desktop"),
+ handle_path(handle_token),
+ QStringLiteral("org.freedesktop.portal.Request"),
+ QStringLiteral("Response"),
+ this,
+ SLOT(createSessionHandler(uint, QVariantMap)));
+
+ 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);
+ connect(
+ watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) {
+ QDBusPendingReply<QDBusObjectPath> reply = *self;
+ self->deleteLater();
+
+ if (!reply.isValid()) {
+ nhlog::ui()->error("org.freedesktop.portal.ScreenCast (CreateSession): {}",
+ reply.error().message().toStdString());
+ close();
+ }
+ });
+}
+
+void
+ScreenCastPortal::createSessionHandler(uint response, const QVariantMap &results)
+{
+ switch (state) {
+ case State::Closed:
+ nhlog::ui()->warn("ScreenCastPortal not starting");
+ break;
+ case State::Starting: {
+ if (response != 0) {
+ nhlog::ui()->error("org.freedekstop.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());
+
+ getAvailableSourceTypes();
+ } break;
+ case State::Started:
+ nhlog::ui()->warn("ScreenCastPortal already started");
+ break;
+ case State::Closing:
+ break;
+ }
+}
+
+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);
+ connect(
+ watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) {
+ QDBusPendingReply<QDBusVariant> reply = *self;
+ self->deleteLater();
+
+ if (!reply.isValid()) {
+ nhlog::ui()->error("org.freedesktop.DBus.Properties (Get AvailableSourceTypes): {}",
+ reply.error().message().toStdString());
+ close();
+ return;
+ }
+
+ switch (state) {
+ case State::Closed:
+ nhlog::ui()->warn("ScreenCastPortal not starting");
+ break;
+ case State::Starting: {
+ 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();
+ } break;
+ case State::Started:
+ nhlog::ui()->warn("ScreenCastPortal already started");
+ break;
+ case State::Closing:
+ break;
+ }
+ });
+}
+
+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);
+ connect(
+ watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) {
+ QDBusPendingReply<QDBusVariant> reply = *self;
+ self->deleteLater();
+
+ if (!reply.isValid()) {
+ nhlog::ui()->error("org.freedesktop.DBus.Properties (Get AvailableCursorModes): {}",
+ reply.error().message().toStdString());
+ close();
+ return;
+ }
+
+ switch (state) {
+ case State::Closed:
+ nhlog::ui()->warn("ScreenCastPortal not starting");
+ break;
+ case State::Starting: {
+ 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();
+ } break;
+ case State::Started:
+ nhlog::ui()->warn("ScreenCastPortal already started");
+ break;
+ case State::Closing:
+ break;
+ }
+ });
+}
+
+void
+ScreenCastPortal::selectSources()
+{
+ // Connect before sending the request to avoid missing the reply
+ auto handle_token = make_token();
+ QDBusConnection::sessionBus().connect(QString(),
+ handle_path(handle_token),
+ QStringLiteral("org.freedesktop.portal.Request"),
+ QStringLiteral("Response"),
+ this,
+ SLOT(selectSourcesHandler(uint, QVariantMap)));
+
+ 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) {
+ 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 &)
+{
+ switch (state) {
+ case State::Closed:
+ nhlog::ui()->warn("ScreenCastPortal not starting");
+ break;
+ case State::Starting: {
+ if (response != 0) {
+ nhlog::ui()->error("org.freedekstop.portal.ScreenCast (SelectSources Response): {}",
+ response);
+ close();
+ return;
+ }
+ start();
+ } break;
+ case State::Started:
+ nhlog::ui()->warn("ScreenCastPortal already started");
+ break;
+ case State::Closing:
+ break;
+ }
+}
+
+void
+ScreenCastPortal::start()
+{
+ // Connect before sending the request to avoid missing the reply
+ auto handle_token = make_token();
+ QDBusConnection::sessionBus().connect(QString(),
+ handle_path(handle_token),
+ QStringLiteral("org.freedesktop.portal.Request"),
+ QStringLiteral("Response"),
+ this,
+ SLOT(startHandler(uint, QVariantMap)));
+
+ 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) {
+ QDBusPendingReply<QDBusObjectPath> reply = *self;
+ if (!reply.isValid()) {
+ nhlog::ui()->error("org.freedesktop.portal.ScreenCast (Start): {}",
+ reply.error().message().toStdString());
+ } else {
+ }
+ });
+}
+
+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)
+{
+ 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) {
+ QDBusPendingReply<QDBusUnixFileDescriptor> reply = *self;
+ if (!reply.isValid()) {
+ nhlog::ui()->error("org.freedesktop.portal.ScreenCast (OpenPipeWireRemote): {}",
+ reply.error().message().toStdString());
+ close();
+ } else {
+ stream.fd = reply.value().fileDescriptor();
+ nhlog::ui()->debug("org.freedesktop.portal.ScreenCast: fd = {}", stream.fd);
+
+ state = State::Started;
+ emit readyChanged();
+ }
+ });
+}
+
+#endif
|