mirror of
https://github.com/outbackdingo/UltraGrid.git
synced 2026-03-22 06:40:26 +00:00
475 lines
18 KiB
C++
475 lines
18 KiB
C++
/**
|
|
* @file utils/dbus_portal.cpp
|
|
* @author Matej Hrica <492778@mail.muni.cz>
|
|
* @author Martin Piatka <piatka@cesnet.cz>
|
|
*/
|
|
/*
|
|
* Copyright (c) 2023 CESNET z.s.p.o.
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, is permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of CESNET nor the names of its contributors may be
|
|
* used to endorse or promote products derived from this software without
|
|
* specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
|
|
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
|
* EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
|
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <memory>
|
|
#include <cassert>
|
|
#include <future>
|
|
#include <fstream>
|
|
#include <gio/gio.h>
|
|
#include <gio/gunixfdlist.h>
|
|
#include <glib-object.h> // for GObject, g_object_unref
|
|
#include <glib.h> // for g_variant_builder_add, GVariant, g_vari...
|
|
#include <glib/gtypes.h> // for gint32
|
|
#include <initializer_list> // for initializer_list
|
|
|
|
#include "debug.h"
|
|
#include "misc.h"
|
|
#include "dbus_portal.hpp"
|
|
|
|
|
|
#define MOD_NAME "[dbus] "
|
|
|
|
namespace {
|
|
struct request_path_t {
|
|
std::string token;
|
|
std::string path;
|
|
|
|
static request_path_t create(const std::string &name) {
|
|
++token_counter;
|
|
|
|
auto token = std::string("uv") + std::to_string(token_counter);
|
|
request_path_t result = {
|
|
.token = token,
|
|
.path = std::string("/org/freedesktop/portal/desktop/request/") + name + "/" + token
|
|
};
|
|
|
|
LOG(LOG_LEVEL_DEBUG) << "new request: '" << result.path << "'\n";
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
static unsigned int token_counter;
|
|
};
|
|
|
|
unsigned int request_path_t::token_counter = 0;
|
|
|
|
struct session_path_t {
|
|
std::string token;
|
|
std::string path;
|
|
|
|
static session_path_t create(const std::string &name) {
|
|
++token_counter;
|
|
|
|
auto token = std::string("uv") + std::to_string(token_counter);
|
|
return {
|
|
.token = token,
|
|
.path = std::string("/org/freedesktop/portal/desktop/session/") + name + "/" + token
|
|
};
|
|
}
|
|
|
|
private:
|
|
static unsigned int token_counter;
|
|
};
|
|
|
|
unsigned int session_path_t::token_counter = 0;
|
|
|
|
struct GVariant_deleter { void operator()(GVariant *a) { g_variant_unref(a); } };
|
|
using GVariant_uniq = std::unique_ptr<GVariant, GVariant_deleter>;
|
|
|
|
|
|
} //anon namespace
|
|
|
|
class ScreenCastPortal_impl {
|
|
private:
|
|
template<auto callback>
|
|
void call_with_request(const char* method_name,
|
|
std::initializer_list<GVariant*> arguments,
|
|
GVariantBuilder ¶ms_builder);
|
|
|
|
static void pipewire_opened(GObject *source,
|
|
GAsyncResult *res,
|
|
void *user_data);
|
|
|
|
static void started(uint32_t response, GVariant *results, void *user_data);
|
|
static void sources_selected(uint32_t response, GVariant *results, ScreenCastPortal_impl *s);
|
|
static void session_created(uint32_t response, GVariant *results, ScreenCastPortal_impl *s);
|
|
|
|
GMainLoop *dbus_loop;
|
|
GDBusConnection *connection;
|
|
GDBusProxy *screencast_proxy;
|
|
std::string unique_name;
|
|
session_path_t session;
|
|
|
|
std::string restore_file;
|
|
bool show_cursor = false;
|
|
|
|
int pw_fd = -1;
|
|
uint32_t pw_node = UINT32_MAX;
|
|
|
|
std::promise<std::string> promise;
|
|
|
|
public:
|
|
// see https://flatpak.github.io/xdg-desktop-portal/#gdbus-signal-org-freedesktop-portal-Request.Response
|
|
static constexpr uint32_t REQUEST_RESPONSE_OK = 0;
|
|
static constexpr uint32_t REQUEST_RESPONSE_CANCELLED_BY_USER = 1;
|
|
static constexpr uint32_t REQUEST_RESPONSE_OTHER_ERROR = 2;
|
|
|
|
ScreenCastPortal_impl();
|
|
~ScreenCastPortal_impl();
|
|
|
|
void run(std::string restore_file, bool show_cursor);
|
|
|
|
std::future<std::string> get_future() {
|
|
return promise.get_future();
|
|
}
|
|
|
|
int get_pw_fd(){ return pw_fd; }
|
|
uint32_t get_pw_node(){ return pw_node; }
|
|
|
|
|
|
void run_loop() {
|
|
g_main_loop_run(dbus_loop);
|
|
LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "finished dbus loop \n";
|
|
}
|
|
|
|
void quit_loop() {
|
|
g_main_loop_quit(dbus_loop);
|
|
}
|
|
|
|
GDBusProxy *proxy() {
|
|
return screencast_proxy;
|
|
}
|
|
|
|
GDBusConnection *dbus_connection() const {
|
|
return connection;
|
|
}
|
|
|
|
const std::string& sender_name() const
|
|
{
|
|
return unique_name;
|
|
}
|
|
|
|
const std::string& session_path() const
|
|
{
|
|
return session.path;
|
|
}
|
|
|
|
const std::string& session_token() const
|
|
{
|
|
return session.token;
|
|
}
|
|
};
|
|
|
|
ScreenCastPortal_impl::ScreenCastPortal_impl(){
|
|
GError *error = nullptr;
|
|
|
|
dbus_loop = g_main_loop_new(nullptr, false);
|
|
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
|
|
g_assert_no_error(error);
|
|
assert(connection != nullptr);
|
|
|
|
unique_name = g_dbus_connection_get_unique_name(connection) + 1;
|
|
std::replace(unique_name.begin(), unique_name.end(), '.', '_');
|
|
screencast_proxy = g_dbus_proxy_new_sync(
|
|
connection, G_DBUS_PROXY_FLAGS_NONE, nullptr,
|
|
"org.freedesktop.portal.Desktop",
|
|
"/org/freedesktop/portal/desktop",
|
|
"org.freedesktop.portal.ScreenCast", nullptr, &error);
|
|
g_assert_no_error(error);
|
|
assert(screencast_proxy != nullptr);
|
|
|
|
session = session_path_t::create(unique_name);
|
|
LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "session path: '" << session.path << "'" << " token: '" << session.token << "'\n";
|
|
}
|
|
|
|
ScreenCastPortal_impl::~ScreenCastPortal_impl() {
|
|
g_dbus_connection_call(dbus_connection(),
|
|
"org.freedesktop.portal.Desktop",
|
|
session.path.c_str(),
|
|
"org.freedesktop.portal.Session",
|
|
"Close", nullptr, nullptr,
|
|
G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr,
|
|
nullptr);
|
|
g_main_loop_quit(dbus_loop);
|
|
g_object_unref(screencast_proxy);
|
|
g_object_unref(connection);
|
|
g_main_loop_unref(dbus_loop);
|
|
}
|
|
|
|
template<auto func>
|
|
void CallbackWrapper(GDBusConnection *connection,
|
|
const gchar *sender_name,
|
|
const gchar *object_path,
|
|
const gchar *interface_name,
|
|
const gchar *signal_name,
|
|
GVariant *parameters,
|
|
gpointer user_data)
|
|
{
|
|
(void) sender_name;
|
|
(void) interface_name;
|
|
(void) signal_name;
|
|
|
|
uint32_t response;
|
|
GVariant_uniq results;
|
|
g_variant_get(parameters, "(u@a{sv})", &response, out_ptr(results).operator GVariant**());
|
|
|
|
func(response, results.get(), static_cast<ScreenCastPortal_impl *>(user_data));
|
|
|
|
g_dbus_connection_call(connection, "org.freedesktop.portal.Desktop",
|
|
object_path, "org.freedesktop.portal.Request", "Close",
|
|
nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr, nullptr);
|
|
|
|
}
|
|
|
|
template<auto callback>
|
|
void ScreenCastPortal_impl::call_with_request(const char* method_name,
|
|
std::initializer_list<GVariant*> arguments,
|
|
GVariantBuilder ¶ms_builder)
|
|
{
|
|
assert(method_name != nullptr);
|
|
request_path_t request_path = request_path_t::create(sender_name());
|
|
LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "call_with_request: '" << method_name << "' request: '" << request_path.path << "'\n";
|
|
|
|
g_dbus_connection_signal_subscribe(connection, "org.freedesktop.portal.Desktop",
|
|
"org.freedesktop.portal.Request",
|
|
"Response",
|
|
request_path.path.c_str(),
|
|
nullptr,
|
|
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
|
|
CallbackWrapper<callback>,
|
|
this,
|
|
nullptr);
|
|
|
|
auto call_finished = [](GObject *source_object, GAsyncResult *result, gpointer user_data) {
|
|
auto s = static_cast<ScreenCastPortal_impl *>(user_data);
|
|
GError *error = nullptr;
|
|
GVariant_uniq result_finished(g_dbus_proxy_call_finish(G_DBUS_PROXY(source_object), result, &error));
|
|
|
|
if(error != nullptr){
|
|
s->promise.set_value(error->message);
|
|
return;
|
|
}
|
|
|
|
const char *path = nullptr;
|
|
g_variant_get(result_finished.get(), "(o)", &path);
|
|
LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "call_with_request finished: '" << path << "'\n";
|
|
};
|
|
|
|
|
|
g_variant_builder_add(¶ms_builder, "{sv}", "handle_token", g_variant_new_string(request_path.token.c_str()));
|
|
|
|
GVariantBuilder args_builder;
|
|
g_variant_builder_init(&args_builder, G_VARIANT_TYPE_TUPLE);
|
|
for(GVariant* arg : arguments){
|
|
g_variant_builder_add_value(&args_builder, arg);
|
|
}
|
|
g_variant_builder_add_value(&args_builder, g_variant_builder_end(¶ms_builder));
|
|
|
|
g_dbus_proxy_call(screencast_proxy, method_name, g_variant_builder_end(&args_builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, call_finished, this);
|
|
}
|
|
|
|
static void on_portal_session_closed([[maybe_unused]] GDBusConnection *connection,
|
|
[[maybe_unused]] const gchar *sender_name,
|
|
[[maybe_unused]] const gchar *object_path,
|
|
[[maybe_unused]] const gchar *interface_name,
|
|
[[maybe_unused]] const gchar *signal_name,
|
|
[[maybe_unused]] GVariant *parameters,
|
|
[[maybe_unused]] gpointer user_data)
|
|
{
|
|
//Doesn't seem to be called by GNOME
|
|
LOG(LOG_LEVEL_INFO) << MOD_NAME "session closed by compositor\n";
|
|
}
|
|
|
|
void ScreenCastPortal_impl::pipewire_opened(GObject *source,
|
|
GAsyncResult *res,
|
|
void *user_data)
|
|
{
|
|
auto s = static_cast<ScreenCastPortal_impl *>(user_data);
|
|
GError *error = nullptr;
|
|
GUnixFDList *fd_list = nullptr;
|
|
|
|
GVariant_uniq result(g_dbus_proxy_call_with_unix_fd_list_finish(G_DBUS_PROXY(source), &fd_list, res, &error));
|
|
g_assert_no_error(error);
|
|
|
|
gint32 handle;
|
|
g_variant_get(result.get(), "(h)", &handle);
|
|
assert(handle == 0); //it should always be the first index
|
|
|
|
s->pw_fd = g_unix_fd_list_get(fd_list, handle, &error);
|
|
g_assert_no_error(error);
|
|
|
|
s->promise.set_value("");
|
|
}
|
|
|
|
void ScreenCastPortal_impl::started(uint32_t response, GVariant *results, void *user_data) {
|
|
auto s = static_cast<ScreenCastPortal_impl *>(user_data);
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME "started: " << g_variant_print(results, true) << "\n";
|
|
|
|
if(response == ScreenCastPortal_impl::REQUEST_RESPONSE_CANCELLED_BY_USER) {
|
|
s->promise.set_value("failed to start (dialog cancelled by user)");
|
|
return;
|
|
} else if(response != ScreenCastPortal_impl::REQUEST_RESPONSE_OK) {
|
|
s->promise.set_value("failed to start (unknown reason)");
|
|
return;
|
|
}
|
|
|
|
const char *restore_token = nullptr;
|
|
if (g_variant_lookup(results, "restore_token", "s", &restore_token)){
|
|
std::ofstream file(s->restore_file);
|
|
file<<restore_token;
|
|
}
|
|
|
|
GVariant *streams = g_variant_lookup_value(results, "streams", G_VARIANT_TYPE_ARRAY);
|
|
GVariant *stream_properties;
|
|
GVariantIter iter;
|
|
g_variant_iter_init(&iter, streams);
|
|
assert(g_variant_iter_n_children(&iter) == 1);
|
|
bool got_item = g_variant_iter_loop(&iter, "(u@a{sv})", &s->pw_node, &stream_properties);
|
|
assert(got_item);
|
|
|
|
GVariantBuilder builder;
|
|
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
|
|
g_dbus_proxy_call_with_unix_fd_list(s->proxy(), "OpenPipeWireRemote",
|
|
g_variant_new("(oa{sv})", s->session_path().c_str(), &builder),
|
|
G_DBUS_CALL_FLAGS_NONE, -1,
|
|
nullptr, nullptr, ScreenCastPortal_impl::pipewire_opened, s);
|
|
g_variant_builder_clear(&builder);
|
|
}
|
|
|
|
void ScreenCastPortal_impl::sources_selected(uint32_t response, GVariant *results, ScreenCastPortal_impl *s) {
|
|
gchar *pretty = g_variant_print(results, true);
|
|
LOG(LOG_LEVEL_INFO) << MOD_NAME "selected sources: " << pretty << "\n";
|
|
g_free((gpointer) pretty);
|
|
|
|
if(response != ScreenCastPortal_impl::REQUEST_RESPONSE_OK) {
|
|
s->promise.set_value("Failed to select sources");
|
|
return;
|
|
}
|
|
|
|
{
|
|
GVariantBuilder options;
|
|
g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
|
|
s->call_with_request<started>("Start", {g_variant_new_object_path(s->session_path().c_str()), /*parent window: */ g_variant_new_string("")}, options);
|
|
}
|
|
};
|
|
|
|
void ScreenCastPortal_impl::session_created(uint32_t response, GVariant *results, ScreenCastPortal_impl *s) {
|
|
if(response != ScreenCastPortal_impl::REQUEST_RESPONSE_OK) {
|
|
s->promise.set_value("Failed to create session");
|
|
return;
|
|
}
|
|
|
|
const char *session_handle = nullptr;
|
|
g_variant_lookup(results, "session_handle", "s", &session_handle);
|
|
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME "session created with handle: " << session_handle << "\n";
|
|
assert(s->session_path() == session_handle);
|
|
|
|
{
|
|
GVariantBuilder params;
|
|
g_variant_builder_init(¶ms, G_VARIANT_TYPE_VARDICT);
|
|
g_variant_builder_add(¶ms, "{sv}", "types", g_variant_new_uint32(3)); // 1 full screen, 2 - a window, 3 - both
|
|
g_variant_builder_add(¶ms, "{sv}", "multiple", g_variant_new_boolean(false));
|
|
if(s->show_cursor)
|
|
g_variant_builder_add(¶ms, "{sv}", "cursor_mode", g_variant_new_uint32(2));
|
|
|
|
if(!s->restore_file.empty()){
|
|
std::string token;
|
|
std::ifstream file(s->restore_file);
|
|
|
|
if(file.is_open()) {
|
|
std::ostringstream ss;
|
|
ss << file.rdbuf();
|
|
token = ss.str();
|
|
}
|
|
|
|
// 0: Do not persist (default), 1: Permissions persist as long as the application is running, 2: Permissions persist until explicitly revoked
|
|
g_variant_builder_add(¶ms, "{sv}", "persist_mode", g_variant_new_uint32(2));
|
|
if(!token.empty())
|
|
g_variant_builder_add(¶ms, "{sv}", "restore_token", g_variant_new_string(token.c_str()));
|
|
}
|
|
|
|
s->call_with_request<sources_selected>("SelectSources", {g_variant_new_object_path(s->session_path().c_str())}, params);
|
|
}
|
|
};
|
|
|
|
void ScreenCastPortal_impl::run(std::string restore_file, bool show_cursor){
|
|
this->restore_file = restore_file;
|
|
this->show_cursor = show_cursor;
|
|
|
|
g_dbus_connection_signal_subscribe(dbus_connection(),
|
|
nullptr, // sender
|
|
"org.freedesktop.portal.Session", // interface_name
|
|
"closed", //signal name
|
|
session_path().c_str(), // object path
|
|
nullptr, // arg0
|
|
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
|
|
on_portal_session_closed,
|
|
nullptr,
|
|
nullptr);
|
|
|
|
GVariantBuilder params;
|
|
g_variant_builder_init(¶ms, G_VARIANT_TYPE_VARDICT);
|
|
g_variant_builder_add(¶ms, "{sv}", "session_handle_token", g_variant_new_string(session_token().c_str()));
|
|
|
|
call_with_request<session_created>("CreateSession", {}, params);
|
|
|
|
run_loop();
|
|
}
|
|
|
|
ScreenCastPortal::ScreenCastPortal():
|
|
impl(std::make_unique<ScreenCastPortal_impl>())
|
|
{
|
|
|
|
}
|
|
|
|
ScreenCastPortal::~ScreenCastPortal(){
|
|
if(thread.joinable()){
|
|
impl->quit_loop();
|
|
thread.join();
|
|
}
|
|
}
|
|
|
|
ScreenCastPortalResult ScreenCastPortal::run(std::string restore_file, bool show_cursor){
|
|
auto future = impl->get_future();
|
|
|
|
thread = std::thread(&ScreenCastPortal_impl::run, impl.get(),
|
|
restore_file, show_cursor);
|
|
|
|
auto error = future.get();
|
|
if(!error.empty()){
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "%s\n", error.c_str());
|
|
return {-1, UINT32_MAX};
|
|
}
|
|
|
|
return {impl->get_pw_fd(), impl->get_pw_node()};
|
|
}
|