mirror of https://github.com/Nheko-Reborn/nheko
parent
d916d6cd63
commit
1ba6a4d78d
@ -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 |
@ -0,0 +1,66 @@ |
||||
#pragma once |
||||
|
||||
#ifdef GSTREAMER_AVAILABLE |
||||
|
||||
#include <QDBusConnection> |
||||
#include <QDBusMessage> |
||||
#include <QDBusPendingCallWatcher> |
||||
#include <QDBusPendingReply> |
||||
#include <QDBusUnixFileDescriptor> |
||||
#include <QObject> |
||||
|
||||
class ScreenCastPortal final : public QObject |
||||
{ |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
struct Stream |
||||
{ |
||||
int fd; |
||||
quint32 nodeId; |
||||
}; |
||||
|
||||
static ScreenCastPortal &instance() |
||||
{ |
||||
static ScreenCastPortal instance; |
||||
return instance; |
||||
} |
||||
|
||||
void init(); |
||||
const Stream *getStream() const; |
||||
bool ready() const; |
||||
void close(bool reinit = false); |
||||
|
||||
public slots: |
||||
void createSessionHandler(uint response, const QVariantMap &results); |
||||
void closedHandler(uint response, const QVariantMap &results); |
||||
void selectSourcesHandler(uint response, const QVariantMap &results); |
||||
void startHandler(uint response, const QVariantMap &results); |
||||
|
||||
signals: |
||||
void readyChanged(); |
||||
|
||||
private: |
||||
void createSession(); |
||||
void getAvailableSourceTypes(); |
||||
void getAvailableCursorModes(); |
||||
void selectSources(); |
||||
void start(); |
||||
void openPipeWireRemote(); |
||||
QDBusObjectPath sessionHandle; |
||||
uint availableSourceTypes; |
||||
uint availableCursorModes; |
||||
|
||||
Stream stream; |
||||
|
||||
enum class State |
||||
{ |
||||
Closed, |
||||
Starting, |
||||
Started, |
||||
Closing, |
||||
}; |
||||
State state = State::Closed; |
||||
}; |
||||
|
||||
#endif |
Loading…
Reference in new issue