1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-12 19:45:23 +02:00
jami-client-qt/src/app/screencastportal.cpp
ovari123 41cb6528c2 misc: unify terminology
{Noun} {verb} successfully
A(n) [type] error occurred while [attempting to] {verb} {noun}.
placeAudioCall → startAudioCall
placeVideoCall → startVideoCall
reconnectTry → reconnectAttempt

Change-Id: I918961894fc23989920727009031cc6a2ac1d8f3
GitLab: #1730
2024-11-01 15:30:44 -04:00

521 lines
No EOL
18 KiB
C++

/*!
* Copyright (C) 2024 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "screencastportal.h"
#include <QDebug>
#include <unistd.h>
#define REQUEST_PATH "/org/freedesktop/portal/desktop/request/%s/%s"
/*
* PipeWire supported cursor modes
*/
enum PortalCursorMode {
PORTAL_CURSOR_MODE_HIDDEN = 1 << 0,
PORTAL_CURSOR_MODE_EMBEDDED = 1 << 1,
PORTAL_CURSOR_MODE_METADATA = 1 << 2,
};
/*
* Helper function to allow getPipewireFd to stop and return an error
* code if a DBus operation/callback fails.
*/
void
ScreenCastPortal::abort(int error, const char* message)
{
portal_error = error;
qWarning() << "Aborting:" << message;
if (glib_main_loop && g_main_loop_is_running(glib_main_loop)) {
g_main_loop_quit(glib_main_loop);
}
}
/*
* Callback to free a DbusCallData object's memory and unsubscribe from the
* associated dbus signal.
*/
void
ScreenCastPortal::dbusCallDataFree(DbusCallData* ptr_dbus_call_data)
{
if (!ptr_dbus_call_data)
return;
if (ptr_dbus_call_data->signal_id)
g_dbus_connection_signal_unsubscribe(ptr_dbus_call_data->portal->connection,
ptr_dbus_call_data->signal_id);
g_clear_pointer(&ptr_dbus_call_data->request_path, g_free);
}
DbusCallData*
ScreenCastPortal::subscribeToSignal(const char* path, GDBusSignalCallback callback)
{
DbusCallData* ptr_dbus_call_data = new DbusCallData;
ptr_dbus_call_data->portal = this;
ptr_dbus_call_data->request_path = g_strdup(path);
ptr_dbus_call_data->signal_id
= g_dbus_connection_signal_subscribe(connection,
"org.freedesktop.portal.Desktop" /*sender*/,
"org.freedesktop.portal.Request" /*interface_name*/,
"Response" /*member: dbus signal name*/,
ptr_dbus_call_data->request_path /*object_path*/,
NULL,
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
callback,
ptr_dbus_call_data,
NULL);
return ptr_dbus_call_data;
}
void
ScreenCastPortal::openPipewireRemote()
{
GUnixFDList* fd_list = NULL;
GVariant* result = NULL;
GError* error = NULL;
int fd_index;
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
result = g_dbus_proxy_call_with_unix_fd_list_sync(proxy,
"OpenPipeWireRemote",
g_variant_new("(oa{sv})",
session_handle,
&builder),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&fd_list,
NULL,
&error);
if (error)
goto fail;
g_variant_get(result, "(h)", &fd_index);
g_variant_unref(result);
pipewireFd = g_unix_fd_list_get(fd_list, fd_index, &error);
g_object_unref(fd_list);
if (error)
goto fail;
g_main_loop_quit(glib_main_loop);
return;
fail:
qWarning() << "Error retrieving PipeWire fd:" << error->message;
g_error_free(error);
abort(EIO, "Failed to open PipeWire remote");
}
void
ScreenCastPortal::onStartResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data)
{
GVariant* stream_properties = NULL;
GVariant* streams = NULL;
GVariant* result = NULL;
GVariantIter iter;
uint32_t response;
DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
ScreenCastPortal* portal = ptr_dbus_call_data->portal;
g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
g_variant_get(parameters, "(u@a{sv})", &response, &result);
if (response) {
g_variant_unref(result);
portal->abort(EACCES, "Failed to start screencast, denied or cancelled by user");
return;
}
streams = g_variant_lookup_value(result, "streams", G_VARIANT_TYPE_ARRAY);
g_variant_iter_init(&iter, streams);
g_variant_iter_loop(&iter, "(u@a{sv})", &portal->pipewireNode, &stream_properties);
qInfo() << "Monitor selected, setting up screencast\n";
g_variant_unref(result);
g_variant_unref(streams);
g_variant_unref(stream_properties);
portal->openPipewireRemote();
}
int
ScreenCastPortal::callDBusMethod(const gchar* method_name, GVariant* parameters)
{
GVariant* result;
GError* error = NULL;
result = g_dbus_proxy_call_sync(proxy,
method_name,
parameters,
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error);
if (error) {
qWarning() << "Call to DBus method" << method_name << "failed:" << error->message;
g_error_free(error);
return EIO;
}
g_variant_unref(result);
return 0;
}
void
ScreenCastPortal::start()
{
int ret;
const char* request_token;
g_autofree char* request_path;
GVariantBuilder builder;
GVariant* parameters;
struct DbusCallData* ptr_dbus_call_data;
request_token = "pipewiregrabStart";
request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
qInfo() << "Asking for monitor...";
ptr_dbus_call_data = subscribeToSignal(request_path, onStartResponseReceivedCallback);
if (!ptr_dbus_call_data) {
abort(ENOMEM, "Failed to allocate DBus call data");
return;
}
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
parameters = g_variant_new("(osa{sv})", session_handle, "", &builder);
ret = callDBusMethod("Start", parameters);
if (ret != 0)
abort(ret, "Failed to start screen cast session");
}
void
ScreenCastPortal::onSelectSourcesResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data)
{
GVariant* ret = NULL;
uint32_t response;
struct DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
ScreenCastPortal* portal = ptr_dbus_call_data->portal;
g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
g_variant_get(parameters, "(u@a{sv})", &response, &ret);
g_variant_unref(ret);
if (response) {
portal->abort(EACCES, "Failed to select screencast sources, denied or cancelled by user");
return;
}
portal->start();
}
void
ScreenCastPortal::selectSources()
{
int ret;
const char* request_token;
g_autofree char* request_path;
GVariantBuilder builder;
GVariant* parameters;
struct DbusCallData* ptr_dbus_call_data;
request_token = "pipewiregrabSelectSources";
request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
ptr_dbus_call_data = subscribeToSignal(request_path, onSelectSourcesResponseReceivedCallback);
if (!ptr_dbus_call_data) {
abort(ENOMEM, "Failed to allocate DBus call data");
return;
}
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(capture_type));
g_variant_builder_add(&builder, "{sv}", "multiple", g_variant_new_boolean(FALSE));
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
if ((available_cursor_modes & PORTAL_CURSOR_MODE_EMBEDDED) && draw_mouse)
g_variant_builder_add(&builder,
"{sv}",
"cursor_mode",
g_variant_new_uint32(PORTAL_CURSOR_MODE_EMBEDDED));
else
g_variant_builder_add(&builder,
"{sv}",
"cursor_mode",
g_variant_new_uint32(PORTAL_CURSOR_MODE_HIDDEN));
parameters = g_variant_new("(oa{sv})", session_handle, &builder);
ret = callDBusMethod("SelectSources", parameters);
if (ret != 0)
abort(ret, "Failed to select sources for screen cast session");
}
void
ScreenCastPortal::onCreateSessionResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data)
{
uint32_t response;
GVariant* result = NULL;
DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
ScreenCastPortal* portal = ptr_dbus_call_data->portal;
g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
g_variant_get(parameters, "(u@a{sv})", &response, &result);
if (response != 0) {
g_variant_unref(result);
portal->abort(EACCES, "Failed to create screencast session, denied or cancelled by user");
return;
}
qDebug() << "Screencast session created";
g_variant_lookup(result, "session_handle", "s", &portal->session_handle);
g_variant_unref(result);
portal->selectSources();
}
void
ScreenCastPortal::createSession()
{
int ret;
GVariantBuilder builder;
GVariant* parameters;
const char* request_token;
g_autofree char* request_path;
DbusCallData* ptr_dbus_call_data;
request_token = "pipewiregrabCreateSession";
request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
ptr_dbus_call_data = subscribeToSignal(request_path, onCreateSessionResponseReceivedCallback);
if (!ptr_dbus_call_data) {
abort(ENOMEM, "Failed to allocate DBus call data");
return;
}
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
g_variant_builder_add(&builder,
"{sv}",
"session_handle_token",
g_variant_new_string("pipewiregrab"));
parameters = g_variant_new("(a{sv})", &builder);
ret = callDBusMethod("CreateSession", parameters);
if (ret != 0)
abort(ret, "Failed to create screen cast session");
}
/*
* Helper function: get available cursor modes and update the
* PipewireGrabContext accordingly
*/
void
ScreenCastPortal::updateAvailableCursorModes()
{
GVariant* cached_cursor_modes = NULL;
cached_cursor_modes = g_dbus_proxy_get_cached_property(proxy, "AvailableCursorModes");
available_cursor_modes = cached_cursor_modes ? g_variant_get_uint32(cached_cursor_modes) : 0;
// Only use embedded or hidden mode for now
available_cursor_modes &= PORTAL_CURSOR_MODE_EMBEDDED | PORTAL_CURSOR_MODE_HIDDEN;
g_variant_unref(cached_cursor_modes);
}
int
ScreenCastPortal::createDBusProxy()
{
GError* error = NULL;
proxy = g_dbus_proxy_new_sync(connection,
G_DBUS_PROXY_FLAGS_NONE,
NULL,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.ScreenCast",
NULL,
&error);
if (error) {
qWarning() << "Error creating proxy:" << error->message;
g_error_free(error);
return EPERM;
}
return 0;
}
/*
* Create DBus connection and related objects
*/
int
ScreenCastPortal::createDBusConnection()
{
char* aux;
GError* error = NULL;
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (error) {
qWarning() << "Error getting session bus:" << error->message;
g_error_free(error);
return EPERM;
}
sender_name = g_strdup(g_dbus_connection_get_unique_name(connection) + 1);
while ((aux = g_strstr_len(sender_name, -1, ".")) != NULL)
*aux = '_';
return 0;
}
/*
* Use XDG Desktop Portal's ScreenCast interface to open a file descriptor that
* can be used by PipeWire to access the screen cast streams.
* (https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html)
*/
int
ScreenCastPortal::getPipewireFd()
{
int ret = 0;
GMainContext* glib_main_context;
// Create a new GLib context and set it as the default for the current thread.
// This ensures that the callbacks from DBus operations started in this thread are
// handled by the GLib main loop defined below, even if pipewiregrab_init was
// called by a program which also uses GLib and already had its own main loop running.
glib_main_context = g_main_context_new();
g_main_context_push_thread_default(glib_main_context);
glib_main_loop = g_main_loop_new(glib_main_context, FALSE);
if (!glib_main_loop) {
qWarning() << "g_main_loop_new failed!";
ret = ENOMEM;
}
ret = createDBusConnection();
if (ret != 0)
goto exit_glib_loop;
ret = createDBusProxy();
if (ret != 0)
goto exit_glib_loop;
updateAvailableCursorModes();
createSession();
if (portal_error) {
ret = portal_error;
goto exit_glib_loop;
}
g_main_loop_run(glib_main_loop);
// The main loop will run until it's stopped by openPipewireRemote (if
// all DBus method calls were successful), abort (in case of error) or
// on_cancelled_callback (if a DBus request is cancelled).
// In the latter two cases, pw_ctx->portal_error gets set to a nonzero value.
if (portal_error)
ret = portal_error;
exit_glib_loop:
g_main_loop_unref(glib_main_loop);
glib_main_loop = NULL;
g_main_context_pop_thread_default(glib_main_context);
g_main_context_unref(glib_main_context);
return ret;
}
ScreenCastPortal::ScreenCastPortal(PortalCaptureType captureType)
: draw_mouse(true)
, pipewireFd(0)
{
switch (captureType) {
case PortalCaptureType::SCREEN:
capture_type = 1;
break;
case PortalCaptureType::WINDOW:
capture_type = 2;
break;
}
}
ScreenCastPortal::~ScreenCastPortal()
{
if (session_handle) {
g_dbus_connection_call(connection,
"org.freedesktop.portal.Desktop",
session_handle,
"org.freedesktop.portal.Session",
"Close",
NULL,
NULL,
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
NULL,
NULL);
g_clear_pointer(&session_handle, g_free);
}
g_clear_object(&connection);
g_clear_object(&proxy);
g_clear_pointer(&sender_name, g_free);
#ifndef ENABLE_LIBWRAP
// If the daemon is running as a separate process, then it can't directly use the
// PipeWire file descriptor opened by the client, so it will have to duplicate it.
// The duplicated file descriptor will be closed by the daemon, but the original
// file descriptor needs to be closed by the client.
if (close(pipewireFd) != 0) {
int err = errno;
qWarning() << "An error occurred while attempting to close PipeWire file descriptor: errno ="
<< err;
} else {
qInfo() << "PipeWire file descriptor closed successfully.";
}
#endif
}