diff --git a/plugins/linux-pipewire/CMakeLists.txt b/plugins/linux-pipewire/CMakeLists.txt index 0f85fe64d..faf610033 100644 --- a/plugins/linux-pipewire/CMakeLists.txt +++ b/plugins/linux-pipewire/CMakeLists.txt @@ -35,10 +35,10 @@ target_sources( PRIVATE linux-pipewire.c pipewire.c pipewire.h - pipewire-capture.c - pipewire-capture.h portal.c - portal.h) + portal.h + screencast-portal.c + screencast-portal.h) target_link_libraries( linux-pipewire PRIVATE OBS::libobs OBS::obsglad PipeWire::PipeWire GIO::GIO diff --git a/plugins/linux-pipewire/linux-pipewire.c b/plugins/linux-pipewire/linux-pipewire.c index 961fafaef..fea91254a 100644 --- a/plugins/linux-pipewire/linux-pipewire.c +++ b/plugins/linux-pipewire/linux-pipewire.c @@ -23,7 +23,7 @@ #include #include -#include "pipewire-capture.h" +#include "screencast-portal.h" OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("linux-pipewire", "en-US") @@ -36,15 +36,7 @@ bool obs_module_load(void) { pw_init(NULL, NULL); - // OBS PipeWire Screen Capture - switch (obs_get_nix_platform()) { -#ifdef ENABLE_WAYLAND - case OBS_NIX_PLATFORM_WAYLAND: -#endif - case OBS_NIX_PLATFORM_X11_EGL: - pipewire_capture_load(); - break; - } + screencast_portal_load(); return true; } diff --git a/plugins/linux-pipewire/pipewire-capture.c b/plugins/linux-pipewire/pipewire-capture.c deleted file mode 100644 index 130771491..000000000 --- a/plugins/linux-pipewire/pipewire-capture.c +++ /dev/null @@ -1,180 +0,0 @@ -/* pipewire-capture.c - * - * Copyright 2020 Georges Basile Stavracas Neto - * - * 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 2 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 . - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "pipewire.h" -#include "portal.h" - -/* obs_source_info methods */ - -static const char *pipewire_desktop_capture_get_name(void *data) -{ - UNUSED_PARAMETER(data); - return obs_module_text("PipeWireDesktopCapture"); -} - -static const char *pipewire_window_capture_get_name(void *data) -{ - UNUSED_PARAMETER(data); - return obs_module_text("PipeWireWindowCapture"); -} - -static void *pipewire_desktop_capture_create(obs_data_t *settings, - obs_source_t *source) -{ - return obs_pipewire_create(PORTAL_CAPTURE_TYPE_MONITOR, settings, - source); -} -static void *pipewire_window_capture_create(obs_data_t *settings, - obs_source_t *source) -{ - return obs_pipewire_create(PORTAL_CAPTURE_TYPE_WINDOW, settings, - source); -} - -static void pipewire_capture_destroy(void *data) -{ - obs_pipewire_destroy(data); -} - -static void pipewire_capture_save(void *data, obs_data_t *settings) -{ - obs_pipewire_save(data, settings); -} - -static void pipewire_capture_get_defaults(obs_data_t *settings) -{ - obs_pipewire_get_defaults(settings); -} - -static obs_properties_t *pipewire_capture_get_properties(void *data) -{ - enum portal_capture_type capture_type; - obs_pipewire_data *obs_pw = data; - - capture_type = obs_pipewire_get_capture_type(obs_pw); - - switch (capture_type) { - case PORTAL_CAPTURE_TYPE_MONITOR: - return obs_pipewire_get_properties(data, - "PipeWireSelectMonitor"); - case PORTAL_CAPTURE_TYPE_WINDOW: - return obs_pipewire_get_properties(data, - "PipeWireSelectWindow"); - case PORTAL_CAPTURE_TYPE_VIRTUAL: - default: - return NULL; - } -} - -static void pipewire_capture_update(void *data, obs_data_t *settings) -{ - obs_pipewire_update(data, settings); -} - -static void pipewire_capture_show(void *data) -{ - obs_pipewire_show(data); -} - -static void pipewire_capture_hide(void *data) -{ - obs_pipewire_hide(data); -} - -static uint32_t pipewire_capture_get_width(void *data) -{ - return obs_pipewire_get_width(data); -} - -static uint32_t pipewire_capture_get_height(void *data) -{ - return obs_pipewire_get_height(data); -} - -static void pipewire_capture_video_render(void *data, gs_effect_t *effect) -{ - obs_pipewire_video_render(data, effect); -} - -static bool initialized = false; - -void pipewire_capture_load(void) -{ - uint32_t available_capture_types = portal_get_available_capture_types(); - bool desktop_capture_available = - (available_capture_types & PORTAL_CAPTURE_TYPE_MONITOR) != 0; - bool window_capture_available = - (available_capture_types & PORTAL_CAPTURE_TYPE_WINDOW) != 0; - - if (available_capture_types == 0) { - blog(LOG_INFO, "[pipewire] No captures available"); - return; - } - - blog(LOG_INFO, "[pipewire] Available captures:"); - if (desktop_capture_available) - blog(LOG_INFO, "[pipewire] - Desktop capture"); - if (window_capture_available) - blog(LOG_INFO, "[pipewire] - Window capture"); - - // Desktop capture - const struct obs_source_info pipewire_desktop_capture_info = { - .id = "pipewire-desktop-capture-source", - .type = OBS_SOURCE_TYPE_INPUT, - .output_flags = OBS_SOURCE_VIDEO, - .get_name = pipewire_desktop_capture_get_name, - .create = pipewire_desktop_capture_create, - .destroy = pipewire_capture_destroy, - .save = pipewire_capture_save, - .get_defaults = pipewire_capture_get_defaults, - .get_properties = pipewire_capture_get_properties, - .update = pipewire_capture_update, - .show = pipewire_capture_show, - .hide = pipewire_capture_hide, - .get_width = pipewire_capture_get_width, - .get_height = pipewire_capture_get_height, - .video_render = pipewire_capture_video_render, - .icon_type = OBS_ICON_TYPE_DESKTOP_CAPTURE, - }; - if (desktop_capture_available) - obs_register_source(&pipewire_desktop_capture_info); - - // Window capture - const struct obs_source_info pipewire_window_capture_info = { - .id = "pipewire-window-capture-source", - .type = OBS_SOURCE_TYPE_INPUT, - .output_flags = OBS_SOURCE_VIDEO, - .get_name = pipewire_window_capture_get_name, - .create = pipewire_window_capture_create, - .destroy = pipewire_capture_destroy, - .save = pipewire_capture_save, - .get_defaults = pipewire_capture_get_defaults, - .get_properties = pipewire_capture_get_properties, - .update = pipewire_capture_update, - .show = pipewire_capture_show, - .hide = pipewire_capture_hide, - .get_width = pipewire_capture_get_width, - .get_height = pipewire_capture_get_height, - .video_render = pipewire_capture_video_render, - .icon_type = OBS_ICON_TYPE_WINDOW_CAPTURE, - }; - if (window_capture_available) - obs_register_source(&pipewire_window_capture_info); -} diff --git a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipewire/pipewire.c index 616640d54..72fdf823a 100644 --- a/plugins/linux-pipewire/pipewire.c +++ b/plugins/linux-pipewire/pipewire.c @@ -20,10 +20,7 @@ #include "pipewire.h" -#include "portal.h" - #include -#include #include #include @@ -32,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -42,9 +40,6 @@ #define SPA_POD_PROP_FLAG_DONT_FIXATE (1 << 4) #endif -#define REQUEST_PATH "/org/freedesktop/portal/desktop/request/%s/obs%u" -#define SESSION_PATH "/org/freedesktop/portal/desktop/session/%s/obs%u" - #define CURSOR_META_SIZE(width, height) \ (sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + \ width * height * 4) @@ -62,18 +57,9 @@ struct format_info { }; struct _obs_pipewire_data { - GCancellable *cancellable; - - char *sender_name; - char *session_handle; - char *restore_token; - uint32_t pipewire_node; int pipewire_fd; - obs_source_t *source; - obs_data_t *settings; - gs_texture_t *texture; struct pw_thread_loop *thread_loop; @@ -106,20 +92,12 @@ struct _obs_pipewire_data { gs_texture_t *texture; } cursor; - enum portal_capture_type capture_type; struct obs_video_info video_info; bool negotiated; DARRAY(struct format_info) format_info; }; -struct dbus_call_data { - obs_pipewire_data *obs_pw; - char *request_path; - guint signal_id; - gulong cancelled_id; -}; - /* auxiliary methods */ static bool parse_pw_version(struct obs_pw_version *dst, const char *version) @@ -151,118 +129,6 @@ static void update_pw_versions(obs_pipewire_data *obs_pw, const char *version) blog(LOG_WARNING, "[pipewire] failed to parse server version"); } -static const char *capture_type_to_string(enum portal_capture_type capture_type) -{ - switch (capture_type) { - case PORTAL_CAPTURE_TYPE_MONITOR: - return "desktop"; - case PORTAL_CAPTURE_TYPE_WINDOW: - return "window"; - case PORTAL_CAPTURE_TYPE_VIRTUAL: - default: - return "unknown"; - } - return "unknown"; -} - -static void new_request_path(obs_pipewire_data *data, char **out_path, - char **out_token) -{ - static uint32_t request_token_count = 0; - - request_token_count++; - - if (out_token) { - struct dstr str; - dstr_init(&str); - dstr_printf(&str, "obs%u", request_token_count); - *out_token = str.array; - } - - if (out_path) { - struct dstr str; - dstr_init(&str); - dstr_printf(&str, REQUEST_PATH, data->sender_name, - request_token_count); - *out_path = str.array; - } -} - -static void new_session_path(obs_pipewire_data *data, char **out_path, - char **out_token) -{ - static uint32_t session_token_count = 0; - - session_token_count++; - - if (out_token) { - struct dstr str; - dstr_init(&str); - dstr_printf(&str, "obs%u", session_token_count); - *out_token = str.array; - } - - if (out_path) { - struct dstr str; - dstr_init(&str); - dstr_printf(&str, SESSION_PATH, data->sender_name, - session_token_count); - *out_path = str.array; - } -} - -static void on_cancelled_cb(GCancellable *cancellable, void *data) -{ - UNUSED_PARAMETER(cancellable); - - struct dbus_call_data *call = data; - - blog(LOG_INFO, "[pipewire] Screencast session cancelled"); - - g_dbus_connection_call( - portal_get_dbus_connection(), "org.freedesktop.portal.Desktop", - call->request_path, "org.freedesktop.portal.Request", "Close", - NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); -} - -static struct dbus_call_data *subscribe_to_signal(obs_pipewire_data *obs_pw, - const char *path, - GDBusSignalCallback callback) -{ - struct dbus_call_data *call; - - call = bzalloc(sizeof(struct dbus_call_data)); - call->obs_pw = obs_pw; - call->request_path = bstrdup(path); - call->cancelled_id = g_signal_connect(obs_pw->cancellable, "cancelled", - G_CALLBACK(on_cancelled_cb), - call); - call->signal_id = g_dbus_connection_signal_subscribe( - portal_get_dbus_connection(), "org.freedesktop.portal.Desktop", - "org.freedesktop.portal.Request", "Response", - call->request_path, NULL, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, - callback, call, NULL); - - return call; -} - -static void dbus_call_data_free(struct dbus_call_data *call) -{ - if (!call) - return; - - if (call->signal_id) - g_dbus_connection_signal_unsubscribe( - portal_get_dbus_connection(), call->signal_id); - - if (call->cancelled_id > 0) - g_signal_handler_disconnect(call->obs_pw->cancellable, - call->cancelled_id); - - g_clear_pointer(&call->request_path, bfree); - bfree(call); -} - static void teardown_pipewire(obs_pipewire_data *obs_pw) { if (obs_pw->thread_loop) { @@ -286,25 +152,10 @@ static void teardown_pipewire(obs_pipewire_data *obs_pw) static void destroy_session(obs_pipewire_data *obs_pw) { - if (obs_pw->session_handle) { - g_dbus_connection_call(portal_get_dbus_connection(), - "org.freedesktop.portal.Desktop", - obs_pw->session_handle, - "org.freedesktop.portal.Session", - "Close", NULL, NULL, - G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, - NULL); - - g_clear_pointer(&obs_pw->session_handle, g_free); - } - - g_clear_pointer(&obs_pw->sender_name, bfree); obs_enter_graphics(); g_clear_pointer(&obs_pw->cursor.texture, gs_texture_destroy); g_clear_pointer(&obs_pw->texture, gs_texture_destroy); obs_leave_graphics(); - g_cancellable_cancel(obs_pw->cancellable); - g_clear_object(&obs_pw->cancellable); } static inline bool has_effective_crop(obs_pipewire_data *obs_pw) @@ -980,447 +831,17 @@ static void play_pipewire_stream(obs_pipewire_data *obs_pw) bfree(params); } -/* ------------------------------------------------- */ - -static void on_pipewire_remote_opened_cb(GObject *source, GAsyncResult *res, - void *user_data) -{ - g_autoptr(GUnixFDList) fd_list = NULL; - g_autoptr(GVariant) result = NULL; - g_autoptr(GError) error = NULL; - obs_pipewire_data *obs_pw = user_data; - int fd_index; - - result = g_dbus_proxy_call_with_unix_fd_list_finish( - G_DBUS_PROXY(source), &fd_list, res, &error); - if (error) { - if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - blog(LOG_ERROR, - "[pipewire] Error retrieving pipewire fd: %s", - error->message); - return; - } - - g_variant_get(result, "(h)", &fd_index, &error); - - obs_pw->pipewire_fd = g_unix_fd_list_get(fd_list, fd_index, &error); - if (error) { - if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - blog(LOG_ERROR, - "[pipewire] Error retrieving pipewire fd: %s", - error->message); - return; - } - - play_pipewire_stream(obs_pw); -} - -static void open_pipewire_remote(obs_pipewire_data *obs_pw) -{ - GVariantBuilder builder; - - g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); - - g_dbus_proxy_call_with_unix_fd_list( - portal_get_dbus_proxy(), "OpenPipeWireRemote", - g_variant_new("(oa{sv})", obs_pw->session_handle, &builder), - G_DBUS_CALL_FLAGS_NONE, -1, NULL, obs_pw->cancellable, - on_pipewire_remote_opened_cb, obs_pw); -} - -/* ------------------------------------------------- */ - -static void on_start_response_received_cb(GDBusConnection *connection, - const char *sender_name, - const char *object_path, - const char *interface_name, - const char *signal_name, - GVariant *parameters, void *user_data) -{ - UNUSED_PARAMETER(connection); - UNUSED_PARAMETER(sender_name); - UNUSED_PARAMETER(object_path); - UNUSED_PARAMETER(interface_name); - UNUSED_PARAMETER(signal_name); - - g_autoptr(GVariant) stream_properties = NULL; - g_autoptr(GVariant) streams = NULL; - g_autoptr(GVariant) result = NULL; - struct dbus_call_data *call = user_data; - obs_pipewire_data *obs_pw = call->obs_pw; - GVariantIter iter; - uint32_t response; - size_t n_streams; - - g_clear_pointer(&call, dbus_call_data_free); - - g_variant_get(parameters, "(u@a{sv})", &response, &result); - - if (response != 0) { - blog(LOG_WARNING, - "[pipewire] 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); - - n_streams = g_variant_iter_n_children(&iter); - if (n_streams != 1) { - blog(LOG_WARNING, - "[pipewire] Received more than one stream when only one was expected. " - "This is probably a bug in the desktop portal implementation you are " - "using."); - - // The KDE Desktop portal implementation sometimes sends an invalid - // response where more than one stream is attached, and only the - // last one is the one we're looking for. This is the only known - // buggy implementation, so let's at least try to make it work here. - for (size_t i = 0; i < n_streams - 1; i++) { - g_autoptr(GVariant) throwaway_properties = NULL; - uint32_t throwaway_pipewire_node; - - g_variant_iter_loop(&iter, "(u@a{sv})", - &throwaway_pipewire_node, - &throwaway_properties); - } - } - - g_variant_iter_loop(&iter, "(u@a{sv})", &obs_pw->pipewire_node, - &stream_properties); - - if (portal_get_screencast_version() >= 4) { - g_autoptr(GVariant) restore_token = NULL; - - g_clear_pointer(&obs_pw->restore_token, bfree); - - restore_token = g_variant_lookup_value(result, "restore_token", - G_VARIANT_TYPE_STRING); - if (restore_token) - obs_pw->restore_token = bstrdup( - g_variant_get_string(restore_token, NULL)); - - obs_source_save(obs_pw->source); - } - - blog(LOG_INFO, "[pipewire] %s selected, setting up screencast", - capture_type_to_string(obs_pw->capture_type)); - - open_pipewire_remote(obs_pw); -} - -static void on_started_cb(GObject *source, GAsyncResult *res, void *user_data) -{ - UNUSED_PARAMETER(user_data); - - g_autoptr(GVariant) result = NULL; - g_autoptr(GError) error = NULL; - - result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); - if (error) { - if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - blog(LOG_ERROR, - "[pipewire] Error selecting screencast source: %s", - error->message); - return; - } -} - -static void start(obs_pipewire_data *obs_pw) -{ - GVariantBuilder builder; - struct dbus_call_data *call; - char *request_token; - char *request_path; - - new_request_path(obs_pw, &request_path, &request_token); - - blog(LOG_INFO, "[pipewire] Asking for %s", - capture_type_to_string(obs_pw->capture_type)); - - call = subscribe_to_signal(obs_pw, request_path, - on_start_response_received_cb); - - g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); - g_variant_builder_add(&builder, "{sv}", "handle_token", - g_variant_new_string(request_token)); - - g_dbus_proxy_call(portal_get_dbus_proxy(), "Start", - g_variant_new("(osa{sv})", obs_pw->session_handle, "", - &builder), - G_DBUS_CALL_FLAGS_NONE, -1, obs_pw->cancellable, - on_started_cb, call); - - bfree(request_token); - bfree(request_path); -} - -/* ------------------------------------------------- */ - -static void on_select_source_response_received_cb( - GDBusConnection *connection, const char *sender_name, - const char *object_path, const char *interface_name, - const char *signal_name, GVariant *parameters, void *user_data) -{ - UNUSED_PARAMETER(connection); - UNUSED_PARAMETER(sender_name); - UNUSED_PARAMETER(object_path); - UNUSED_PARAMETER(interface_name); - UNUSED_PARAMETER(signal_name); - - g_autoptr(GVariant) ret = NULL; - struct dbus_call_data *call = user_data; - obs_pipewire_data *obs_pw = call->obs_pw; - uint32_t response; - - blog(LOG_DEBUG, "[pipewire] Response to select source received"); - - g_clear_pointer(&call, dbus_call_data_free); - - g_variant_get(parameters, "(u@a{sv})", &response, &ret); - - if (response != 0) { - blog(LOG_WARNING, - "[pipewire] Failed to select source, denied or cancelled by user"); - return; - } - - start(obs_pw); -} - -static void on_source_selected_cb(GObject *source, GAsyncResult *res, - void *user_data) -{ - UNUSED_PARAMETER(user_data); - - g_autoptr(GVariant) result = NULL; - g_autoptr(GError) error = NULL; - - result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); - if (error) { - if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - blog(LOG_ERROR, - "[pipewire] Error selecting screencast source: %s", - error->message); - return; - } -} - -static void select_source(obs_pipewire_data *obs_pw) -{ - struct dbus_call_data *call; - GVariantBuilder builder; - uint32_t available_cursor_modes; - char *request_token; - char *request_path; - - new_request_path(obs_pw, &request_path, &request_token); - - call = subscribe_to_signal(obs_pw, request_path, - on_select_source_response_received_cb); - - g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); - g_variant_builder_add(&builder, "{sv}", "types", - g_variant_new_uint32(obs_pw->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)); - - available_cursor_modes = portal_get_available_cursor_modes(); - - if (available_cursor_modes & PORTAL_CURSOR_MODE_METADATA) - g_variant_builder_add( - &builder, "{sv}", "cursor_mode", - g_variant_new_uint32(PORTAL_CURSOR_MODE_METADATA)); - else if ((available_cursor_modes & PORTAL_CURSOR_MODE_EMBEDDED) && - obs_pw->cursor.visible) - 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)); - - if (portal_get_screencast_version() >= 4) { - g_variant_builder_add(&builder, "{sv}", "persist_mode", - g_variant_new_uint32(2)); - if (obs_pw->restore_token && *obs_pw->restore_token) { - g_variant_builder_add( - &builder, "{sv}", "restore_token", - g_variant_new_string(obs_pw->restore_token)); - } - } - - g_dbus_proxy_call(portal_get_dbus_proxy(), "SelectSources", - g_variant_new("(oa{sv})", obs_pw->session_handle, - &builder), - G_DBUS_CALL_FLAGS_NONE, -1, obs_pw->cancellable, - on_source_selected_cb, call); - - bfree(request_token); - bfree(request_path); -} - -/* ------------------------------------------------- */ - -static void on_create_session_response_received_cb( - GDBusConnection *connection, const char *sender_name, - const char *object_path, const char *interface_name, - const char *signal_name, GVariant *parameters, void *user_data) -{ - UNUSED_PARAMETER(connection); - UNUSED_PARAMETER(sender_name); - UNUSED_PARAMETER(object_path); - UNUSED_PARAMETER(interface_name); - UNUSED_PARAMETER(signal_name); - - g_autoptr(GVariant) session_handle_variant = NULL; - g_autoptr(GVariant) result = NULL; - struct dbus_call_data *call = user_data; - obs_pipewire_data *obs_pw = call->obs_pw; - uint32_t response; - - g_clear_pointer(&call, dbus_call_data_free); - - g_variant_get(parameters, "(u@a{sv})", &response, &result); - - if (response != 0) { - blog(LOG_WARNING, - "[pipewire] Failed to create session, denied or cancelled by user"); - return; - } - - blog(LOG_INFO, "[pipewire] Screencast session created"); - - session_handle_variant = - g_variant_lookup_value(result, "session_handle", NULL); - obs_pw->session_handle = - g_variant_dup_string(session_handle_variant, NULL); - - select_source(obs_pw); -} - -static void on_session_created_cb(GObject *source, GAsyncResult *res, - void *user_data) -{ - UNUSED_PARAMETER(user_data); - - g_autoptr(GVariant) result = NULL; - g_autoptr(GError) error = NULL; - - result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); - if (error) { - if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - blog(LOG_ERROR, - "[pipewire] Error creating screencast session: %s", - error->message); - return; - } -} - -static void create_session(obs_pipewire_data *obs_pw) -{ - struct dbus_call_data *call; - GVariantBuilder builder; - char *session_token; - char *request_token; - char *request_path; - - new_request_path(obs_pw, &request_path, &request_token); - new_session_path(obs_pw, NULL, &session_token); - - call = subscribe_to_signal(obs_pw, request_path, - on_create_session_response_received_cb); - - 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(session_token)); - - g_dbus_proxy_call(portal_get_dbus_proxy(), "CreateSession", - g_variant_new("(a{sv})", &builder), - G_DBUS_CALL_FLAGS_NONE, -1, obs_pw->cancellable, - on_session_created_cb, call); - - bfree(session_token); - bfree(request_token); - bfree(request_path); -} - -/* ------------------------------------------------- */ - -static gboolean init_obs_pipewire(obs_pipewire_data *obs_pw) -{ - GDBusConnection *connection; - GDBusProxy *proxy; - char *aux; - - obs_pw->cancellable = g_cancellable_new(); - connection = portal_get_dbus_connection(); - if (!connection) - return FALSE; - proxy = portal_get_dbus_proxy(); - if (!proxy) - return FALSE; - - obs_pw->sender_name = - bstrdup(g_dbus_connection_get_unique_name(connection) + 1); - - /* Replace dots by underscores */ - while ((aux = strstr(obs_pw->sender_name, ".")) != NULL) - *aux = '_'; - - blog(LOG_INFO, "PipeWire initialized (sender name: %s)", - obs_pw->sender_name); - - create_session(obs_pw); - - return TRUE; -} - -static bool reload_session_cb(obs_properties_t *properties, - obs_property_t *property, void *data) -{ - UNUSED_PARAMETER(properties); - UNUSED_PARAMETER(property); - - obs_pipewire_data *obs_pw = data; - - g_clear_pointer(&obs_pw->restore_token, bfree); - - teardown_pipewire(obs_pw); - destroy_session(obs_pw); - - init_obs_pipewire(obs_pw); - - return false; -} - /* obs_source_info methods */ -void *obs_pipewire_create(enum portal_capture_type capture_type, - obs_data_t *settings, obs_source_t *source) +void *obs_pipewire_create(int pipewire_fd, int pipewire_node) { obs_pipewire_data *obs_pw = bzalloc(sizeof(obs_pipewire_data)); - obs_pw->source = source; - obs_pw->settings = settings; - obs_pw->capture_type = capture_type; - obs_pw->cursor.visible = obs_data_get_bool(settings, "ShowCursor"); - obs_pw->restore_token = - bstrdup(obs_data_get_string(settings, "RestoreToken")); - - if (!init_obs_pipewire(obs_pw)) { - g_clear_pointer(&obs_pw, bfree); - return NULL; - } + obs_pw->pipewire_fd = pipewire_fd; + obs_pw->pipewire_node = pipewire_node; init_format_info(obs_pw); + play_pipewire_stream(obs_pw); return obs_pw; } @@ -1433,43 +854,11 @@ void obs_pipewire_destroy(obs_pipewire_data *obs_pw) teardown_pipewire(obs_pw); destroy_session(obs_pw); - g_clear_pointer(&obs_pw->restore_token, bfree); clear_format_info(obs_pw); bfree(obs_pw); } -void obs_pipewire_save(obs_pipewire_data *obs_pw, obs_data_t *settings) -{ - obs_data_set_string(settings, "RestoreToken", obs_pw->restore_token); -} - -void obs_pipewire_get_defaults(obs_data_t *settings) -{ - obs_data_set_default_bool(settings, "ShowCursor", true); - obs_data_set_default_string(settings, "RestoreToken", NULL); -} - -obs_properties_t *obs_pipewire_get_properties(obs_pipewire_data *obs_pw, - const char *reload_string_id) -{ - obs_properties_t *properties; - - properties = obs_properties_create(); - obs_properties_add_button2(properties, "Reload", - obs_module_text(reload_string_id), - reload_session_cb, obs_pw); - obs_properties_add_bool(properties, "ShowCursor", - obs_module_text("ShowCursor")); - - return properties; -} - -void obs_pipewire_update(obs_pipewire_data *obs_pw, obs_data_t *settings) -{ - obs_pw->cursor.visible = obs_data_get_bool(settings, "ShowCursor"); -} - void obs_pipewire_show(obs_pipewire_data *obs_pw) { if (obs_pw->stream) @@ -1538,8 +927,8 @@ void obs_pipewire_video_render(obs_pipewire_data *obs_pw, gs_effect_t *effect) } } -enum portal_capture_type -obs_pipewire_get_capture_type(obs_pipewire_data *obs_pw) +void obs_pipewire_set_cursor_visible(obs_pipewire_data *obs_pw, + bool cursor_visible) { - return obs_pw->capture_type; + obs_pw->cursor.visible = cursor_visible; } diff --git a/plugins/linux-pipewire/pipewire.h b/plugins/linux-pipewire/pipewire.h index ebdf27292..05118bf7c 100644 --- a/plugins/linux-pipewire/pipewire.h +++ b/plugins/linux-pipewire/pipewire.h @@ -21,31 +21,17 @@ #pragma once #include -#include - -#include "portal.h" typedef struct _obs_pipewire_data obs_pipewire_data; -void *obs_pipewire_create(enum portal_capture_type capture_type, - obs_data_t *settings, obs_source_t *source); - +void *obs_pipewire_create(int pipewire_fd, int pipewire_node); void obs_pipewire_destroy(obs_pipewire_data *obs_pw); -void obs_pipewire_save(obs_pipewire_data *obs_pw, obs_data_t *settings); -void obs_pipewire_get_defaults(obs_data_t *settings); - -obs_properties_t *obs_pipewire_get_properties(obs_pipewire_data *obs_pw, - const char *reload_string_id); - -void obs_pipewire_update(obs_pipewire_data *obs_pw, obs_data_t *settings); - void obs_pipewire_show(obs_pipewire_data *obs_pw); - void obs_pipewire_hide(obs_pipewire_data *obs_pw); uint32_t obs_pipewire_get_width(obs_pipewire_data *obs_pw); uint32_t obs_pipewire_get_height(obs_pipewire_data *obs_pw); void obs_pipewire_video_render(obs_pipewire_data *obs_pw, gs_effect_t *effect); -enum portal_capture_type -obs_pipewire_get_capture_type(obs_pipewire_data *obs_pw); +void obs_pipewire_set_cursor_visible(obs_pipewire_data *obs_pw, + bool cursor_visible); diff --git a/plugins/linux-pipewire/portal.c b/plugins/linux-pipewire/portal.c index b21ae6d27..94266b193 100644 --- a/plugins/linux-pipewire/portal.c +++ b/plugins/linux-pipewire/portal.c @@ -21,10 +21,14 @@ #include "portal.h" #include "pipewire.h" -static GDBusConnection *connection = NULL; -static GDBusProxy *proxy = NULL; +#include -static void ensure_proxy(void) +#define REQUEST_PATH "/org/freedesktop/portal/desktop/request/%s/obs%u" +#define SESSION_PATH "/org/freedesktop/portal/desktop/session/%s/obs%u" + +static GDBusConnection *connection = NULL; + +static void ensure_connection(void) { g_autoptr(GError) error = NULL; if (!connection) { @@ -37,85 +41,83 @@ static void ensure_proxy(void) return; } } - - if (!proxy) { - 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) { - blog(LOG_WARNING, - "[portals] Error retrieving D-Bus proxy: %s", - error->message); - return; - } - } } -uint32_t portal_get_available_capture_types(void) +char *get_sender_name(void) { - g_autoptr(GVariant) cached_source_types = NULL; - uint32_t available_source_types; + char *sender_name; + char *aux; - ensure_proxy(); + ensure_connection(); - if (!proxy) - return 0; + sender_name = + bstrdup(g_dbus_connection_get_unique_name(connection) + 1); - cached_source_types = - g_dbus_proxy_get_cached_property(proxy, "AvailableSourceTypes"); - available_source_types = - cached_source_types ? g_variant_get_uint32(cached_source_types) - : 0; + /* Replace dots by underscores */ + while ((aux = strstr(sender_name, ".")) != NULL) + *aux = '_'; - return available_source_types; -} - -uint32_t portal_get_available_cursor_modes(void) -{ - g_autoptr(GVariant) cached_cursor_modes = NULL; - uint32_t available_cursor_modes; - - ensure_proxy(); - - if (!proxy) - return 0; - - 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; - - return available_cursor_modes; -} - -uint32_t portal_get_screencast_version(void) -{ - g_autoptr(GVariant) cached_version = NULL; - uint32_t version; - - ensure_proxy(); - - if (!proxy) - return 0; - - cached_version = g_dbus_proxy_get_cached_property(proxy, "version"); - version = cached_version ? g_variant_get_uint32(cached_version) : 0; - - return version; + return sender_name; } GDBusConnection *portal_get_dbus_connection(void) { - ensure_proxy(); + ensure_connection(); return connection; } -GDBusProxy *portal_get_dbus_proxy(void) +void portal_create_request_path(char **out_path, char **out_token) { - ensure_proxy(); - return proxy; + static uint32_t request_token_count = 0; + + request_token_count++; + + if (out_token) { + struct dstr str; + dstr_init(&str); + dstr_printf(&str, "obs%u", request_token_count); + *out_token = str.array; + } + + if (out_path) { + char *sender_name; + struct dstr str; + + sender_name = get_sender_name(); + + dstr_init(&str); + dstr_printf(&str, REQUEST_PATH, sender_name, + request_token_count); + *out_path = str.array; + + bfree(sender_name); + } +} + +void portal_create_session_path(char **out_path, char **out_token) +{ + static uint32_t session_token_count = 0; + + session_token_count++; + + if (out_token) { + struct dstr str; + dstr_init(&str); + dstr_printf(&str, "obs%u", session_token_count); + *out_token = str.array; + } + + if (out_path) { + char *sender_name; + struct dstr str; + + sender_name = get_sender_name(); + + dstr_init(&str); + dstr_printf(&str, SESSION_PATH, sender_name, + session_token_count); + *out_path = str.array; + + bfree(sender_name); + } } diff --git a/plugins/linux-pipewire/portal.h b/plugins/linux-pipewire/portal.h index 85ad56732..56dfa56c4 100644 --- a/plugins/linux-pipewire/portal.h +++ b/plugins/linux-pipewire/portal.h @@ -23,21 +23,7 @@ #include #include -enum portal_capture_type { - PORTAL_CAPTURE_TYPE_MONITOR = 1 << 0, - PORTAL_CAPTURE_TYPE_WINDOW = 1 << 1, - PORTAL_CAPTURE_TYPE_VIRTUAL = 1 << 2, -}; - -enum portal_cursor_mode { - PORTAL_CURSOR_MODE_HIDDEN = 1 << 0, - PORTAL_CURSOR_MODE_EMBEDDED = 1 << 1, - PORTAL_CURSOR_MODE_METADATA = 1 << 2, -}; - -uint32_t portal_get_available_capture_types(void); -uint32_t portal_get_available_cursor_modes(void); -uint32_t portal_get_screencast_version(void); - GDBusConnection *portal_get_dbus_connection(void); -GDBusProxy *portal_get_dbus_proxy(void); + +void portal_create_request_path(char **out_path, char **out_token); +void portal_create_session_path(char **out_path, char **out_token); diff --git a/plugins/linux-pipewire/screencast-portal.c b/plugins/linux-pipewire/screencast-portal.c new file mode 100644 index 000000000..dfdaa63cd --- /dev/null +++ b/plugins/linux-pipewire/screencast-portal.c @@ -0,0 +1,869 @@ +/* screencast-portal.c + * + * Copyright 2022 Georges Basile Stavracas Neto + * + * 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 2 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 . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "pipewire.h" +#include "portal.h" + +#include + +enum portal_capture_type { + PORTAL_CAPTURE_TYPE_MONITOR = 1 << 0, + PORTAL_CAPTURE_TYPE_WINDOW = 1 << 1, + PORTAL_CAPTURE_TYPE_VIRTUAL = 1 << 2, +}; + +enum portal_cursor_mode { + PORTAL_CURSOR_MODE_HIDDEN = 1 << 0, + PORTAL_CURSOR_MODE_EMBEDDED = 1 << 1, + PORTAL_CURSOR_MODE_METADATA = 1 << 2, +}; + +struct screencast_portal_capture { + enum portal_capture_type capture_type; + + GCancellable *cancellable; + + char *session_handle; + char *restore_token; + + obs_source_t *source; + obs_data_t *settings; + + uint32_t pipewire_node; + bool cursor_visible; + + obs_pipewire_data *obs_pw; +}; + +/* ------------------------------------------------- */ + +static GDBusProxy *screencast_proxy = NULL; + +static void ensure_screencast_portal_proxy(void) +{ + g_autoptr(GError) error = NULL; + if (!screencast_proxy) { + screencast_proxy = g_dbus_proxy_new_sync( + portal_get_dbus_connection(), G_DBUS_PROXY_FLAGS_NONE, + NULL, "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.ScreenCast", NULL, &error); + + if (error) { + blog(LOG_WARNING, + "[portals] Error retrieving D-Bus proxy: %s", + error->message); + return; + } + } +} + +static GDBusProxy *get_screencast_portal_proxy(void) +{ + ensure_screencast_portal_proxy(); + return screencast_proxy; +} + +static uint32_t get_available_capture_types(void) +{ + g_autoptr(GVariant) cached_source_types = NULL; + uint32_t available_source_types; + + ensure_screencast_portal_proxy(); + + if (!screencast_proxy) + return 0; + + cached_source_types = g_dbus_proxy_get_cached_property( + screencast_proxy, "AvailableSourceTypes"); + available_source_types = + cached_source_types ? g_variant_get_uint32(cached_source_types) + : 0; + + return available_source_types; +} + +static uint32_t get_available_cursor_modes(void) +{ + g_autoptr(GVariant) cached_cursor_modes = NULL; + uint32_t available_cursor_modes; + + ensure_screencast_portal_proxy(); + + if (!screencast_proxy) + return 0; + + cached_cursor_modes = g_dbus_proxy_get_cached_property( + screencast_proxy, "AvailableCursorModes"); + available_cursor_modes = + cached_cursor_modes ? g_variant_get_uint32(cached_cursor_modes) + : 0; + + return available_cursor_modes; +} + +static uint32_t get_screencast_version(void) +{ + g_autoptr(GVariant) cached_version = NULL; + uint32_t version; + + ensure_screencast_portal_proxy(); + + if (!screencast_proxy) + return 0; + + cached_version = + g_dbus_proxy_get_cached_property(screencast_proxy, "version"); + version = cached_version ? g_variant_get_uint32(cached_version) : 0; + + return version; +} + +/* ------------------------------------------------- */ + +struct dbus_call_data { + struct screencast_portal_capture *capture; + char *request_path; + guint signal_id; + gulong cancelled_id; +}; + +static const char *capture_type_to_string(enum portal_capture_type capture_type) +{ + switch (capture_type) { + case PORTAL_CAPTURE_TYPE_MONITOR: + return "desktop"; + case PORTAL_CAPTURE_TYPE_WINDOW: + return "window"; + case PORTAL_CAPTURE_TYPE_VIRTUAL: + default: + return "unknown"; + } + return "unknown"; +} + +static void on_cancelled_cb(GCancellable *cancellable, void *data) +{ + UNUSED_PARAMETER(cancellable); + + struct dbus_call_data *call = data; + + blog(LOG_INFO, "[pipewire] Screencast session cancelled"); + + g_dbus_connection_call( + portal_get_dbus_connection(), "org.freedesktop.portal.Desktop", + call->request_path, "org.freedesktop.portal.Request", "Close", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static struct dbus_call_data * +subscribe_to_signal(struct screencast_portal_capture *capture, const char *path, + GDBusSignalCallback callback) +{ + struct dbus_call_data *call; + + call = bzalloc(sizeof(struct dbus_call_data)); + call->capture = capture; + call->request_path = bstrdup(path); + call->cancelled_id = g_signal_connect(capture->cancellable, "cancelled", + G_CALLBACK(on_cancelled_cb), + call); + call->signal_id = g_dbus_connection_signal_subscribe( + portal_get_dbus_connection(), "org.freedesktop.portal.Desktop", + "org.freedesktop.portal.Request", "Response", + call->request_path, NULL, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + callback, call, NULL); + + return call; +} + +static void dbus_call_data_free(struct dbus_call_data *call) +{ + if (!call) + return; + + if (call->signal_id) + g_dbus_connection_signal_unsubscribe( + portal_get_dbus_connection(), call->signal_id); + + if (call->cancelled_id > 0) + g_signal_handler_disconnect(call->capture->cancellable, + call->cancelled_id); + + g_clear_pointer(&call->request_path, bfree); + bfree(call); +} + +/* ------------------------------------------------- */ + +static void on_pipewire_remote_opened_cb(GObject *source, GAsyncResult *res, + void *user_data) +{ + struct screencast_portal_capture *capture; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + int pipewire_fd; + int fd_index; + + capture = user_data; + result = g_dbus_proxy_call_with_unix_fd_list_finish( + G_DBUS_PROXY(source), &fd_list, res, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + blog(LOG_ERROR, + "[pipewire] Error retrieving pipewire fd: %s", + error->message); + return; + } + + g_variant_get(result, "(h)", &fd_index, &error); + + pipewire_fd = g_unix_fd_list_get(fd_list, fd_index, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + blog(LOG_ERROR, + "[pipewire] Error retrieving pipewire fd: %s", + error->message); + return; + } + + capture->obs_pw = + obs_pipewire_create(pipewire_fd, capture->pipewire_node); + obs_pipewire_set_cursor_visible(capture->obs_pw, + capture->cursor_visible); +} + +static void open_pipewire_remote(struct screencast_portal_capture *capture) +{ + GVariantBuilder builder; + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + + g_dbus_proxy_call_with_unix_fd_list( + get_screencast_portal_proxy(), "OpenPipeWireRemote", + g_variant_new("(oa{sv})", capture->session_handle, &builder), + G_DBUS_CALL_FLAGS_NONE, -1, NULL, capture->cancellable, + on_pipewire_remote_opened_cb, capture); +} + +/* ------------------------------------------------- */ + +static void on_start_response_received_cb(GDBusConnection *connection, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, void *user_data) +{ + UNUSED_PARAMETER(connection); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + + struct screencast_portal_capture *capture; + g_autoptr(GVariant) stream_properties = NULL; + g_autoptr(GVariant) streams = NULL; + g_autoptr(GVariant) result = NULL; + struct dbus_call_data *call = user_data; + GVariantIter iter; + uint32_t response; + size_t n_streams; + + capture = call->capture; + g_clear_pointer(&call, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &result); + + if (response != 0) { + blog(LOG_WARNING, + "[pipewire] 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); + + n_streams = g_variant_iter_n_children(&iter); + if (n_streams != 1) { + blog(LOG_WARNING, + "[pipewire] Received more than one stream when only one was expected. " + "This is probably a bug in the desktop portal implementation you are " + "using."); + + // The KDE Desktop portal implementation sometimes sends an invalid + // response where more than one stream is attached, and only the + // last one is the one we're looking for. This is the only known + // buggy implementation, so let's at least try to make it work here. + for (size_t i = 0; i < n_streams - 1; i++) { + g_autoptr(GVariant) throwaway_properties = NULL; + uint32_t throwaway_pipewire_node; + + g_variant_iter_loop(&iter, "(u@a{sv})", + &throwaway_pipewire_node, + &throwaway_properties); + } + } + + g_variant_iter_loop(&iter, "(u@a{sv})", &capture->pipewire_node, + &stream_properties); + + if (get_screencast_version() >= 4) { + g_autoptr(GVariant) restore_token = NULL; + + g_clear_pointer(&capture->restore_token, bfree); + + restore_token = g_variant_lookup_value(result, "restore_token", + G_VARIANT_TYPE_STRING); + if (restore_token) + capture->restore_token = bstrdup( + g_variant_get_string(restore_token, NULL)); + + obs_source_save(capture->source); + } + + blog(LOG_INFO, "[pipewire] %s selected, setting up screencast", + capture_type_to_string(capture->capture_type)); + + open_pipewire_remote(capture); +} + +static void on_started_cb(GObject *source, GAsyncResult *res, void *user_data) +{ + UNUSED_PARAMETER(user_data); + + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + + result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + blog(LOG_ERROR, + "[pipewire] Error selecting screencast source: %s", + error->message); + return; + } +} + +static void start(struct screencast_portal_capture *capture) +{ + GVariantBuilder builder; + struct dbus_call_data *call; + char *request_token; + char *request_path; + + portal_create_request_path(&request_path, &request_token); + + blog(LOG_INFO, "[pipewire] Asking for %s", + capture_type_to_string(capture->capture_type)); + + call = subscribe_to_signal(capture, request_path, + on_start_response_received_cb); + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "handle_token", + g_variant_new_string(request_token)); + + g_dbus_proxy_call(get_screencast_portal_proxy(), "Start", + g_variant_new("(osa{sv})", capture->session_handle, + "", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, capture->cancellable, + on_started_cb, call); + + bfree(request_token); + bfree(request_path); +} + +/* ------------------------------------------------- */ + +static void on_select_source_response_received_cb( + GDBusConnection *connection, const char *sender_name, + const char *object_path, const char *interface_name, + const char *signal_name, GVariant *parameters, void *user_data) +{ + UNUSED_PARAMETER(connection); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + + struct screencast_portal_capture *capture; + g_autoptr(GVariant) ret = NULL; + struct dbus_call_data *call = user_data; + uint32_t response; + + capture = call->capture; + + blog(LOG_DEBUG, "[pipewire] Response to select source received"); + + g_clear_pointer(&call, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &ret); + + if (response != 0) { + blog(LOG_WARNING, + "[pipewire] Failed to select source, denied or cancelled by user"); + return; + } + + start(capture); +} + +static void on_source_selected_cb(GObject *source, GAsyncResult *res, + void *user_data) +{ + UNUSED_PARAMETER(user_data); + + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + + result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + blog(LOG_ERROR, + "[pipewire] Error selecting screencast source: %s", + error->message); + return; + } +} + +static void select_source(struct screencast_portal_capture *capture) +{ + struct dbus_call_data *call; + GVariantBuilder builder; + uint32_t available_cursor_modes; + char *request_token; + char *request_path; + + portal_create_request_path(&request_path, &request_token); + + call = subscribe_to_signal(capture, request_path, + on_select_source_response_received_cb); + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "types", + g_variant_new_uint32(capture->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)); + + available_cursor_modes = get_available_cursor_modes(); + + if (available_cursor_modes & PORTAL_CURSOR_MODE_METADATA) + g_variant_builder_add( + &builder, "{sv}", "cursor_mode", + g_variant_new_uint32(PORTAL_CURSOR_MODE_METADATA)); + else if ((available_cursor_modes & PORTAL_CURSOR_MODE_EMBEDDED) && + capture->cursor_visible) + 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)); + + if (get_screencast_version() >= 4) { + g_variant_builder_add(&builder, "{sv}", "persist_mode", + g_variant_new_uint32(2)); + if (capture->restore_token && *capture->restore_token) { + g_variant_builder_add( + &builder, "{sv}", "restore_token", + g_variant_new_string(capture->restore_token)); + } + } + + g_dbus_proxy_call(get_screencast_portal_proxy(), "SelectSources", + g_variant_new("(oa{sv})", capture->session_handle, + &builder), + G_DBUS_CALL_FLAGS_NONE, -1, capture->cancellable, + on_source_selected_cb, call); + + bfree(request_token); + bfree(request_path); +} + +/* ------------------------------------------------- */ + +static void on_create_session_response_received_cb( + GDBusConnection *connection, const char *sender_name, + const char *object_path, const char *interface_name, + const char *signal_name, GVariant *parameters, void *user_data) +{ + UNUSED_PARAMETER(connection); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + + struct screencast_portal_capture *capture; + g_autoptr(GVariant) session_handle_variant = NULL; + g_autoptr(GVariant) result = NULL; + struct dbus_call_data *call = user_data; + uint32_t response; + + capture = call->capture; + + g_clear_pointer(&call, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &result); + + if (response != 0) { + blog(LOG_WARNING, + "[pipewire] Failed to create session, denied or cancelled by user"); + return; + } + + blog(LOG_INFO, "[pipewire] Screencast session created"); + + session_handle_variant = + g_variant_lookup_value(result, "session_handle", NULL); + capture->session_handle = + g_variant_dup_string(session_handle_variant, NULL); + + select_source(capture); +} + +static void on_session_created_cb(GObject *source, GAsyncResult *res, + void *user_data) +{ + UNUSED_PARAMETER(user_data); + + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + + result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + blog(LOG_ERROR, + "[pipewire] Error creating screencast session: %s", + error->message); + return; + } +} + +static void create_session(struct screencast_portal_capture *capture) +{ + struct dbus_call_data *call; + GVariantBuilder builder; + char *session_token; + char *request_token; + char *request_path; + + portal_create_request_path(&request_path, &request_token); + portal_create_session_path(NULL, &session_token); + + call = subscribe_to_signal(capture, request_path, + on_create_session_response_received_cb); + + 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(session_token)); + + g_dbus_proxy_call(get_screencast_portal_proxy(), "CreateSession", + g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, capture->cancellable, + on_session_created_cb, call); + + bfree(session_token); + bfree(request_token); + bfree(request_path); +} + +/* ------------------------------------------------- */ + +static gboolean +init_screencast_capture(struct screencast_portal_capture *capture) +{ + GDBusConnection *connection; + GDBusProxy *proxy; + char *aux; + + capture->cancellable = g_cancellable_new(); + connection = portal_get_dbus_connection(); + if (!connection) + return FALSE; + proxy = get_screencast_portal_proxy(); + if (!proxy) + return FALSE; + + blog(LOG_INFO, "PipeWire initialized"); + + create_session(capture); + + return TRUE; +} + +static bool reload_session_cb(obs_properties_t *properties, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(properties); + UNUSED_PARAMETER(property); + + struct screencast_portal_capture *capture = data; + + g_clear_pointer(&capture->restore_token, bfree); + g_clear_pointer(&capture->obs_pw, obs_pipewire_destroy); + + init_screencast_capture(capture); + + return false; +} + +/* obs_source_info methods */ + +static const char *screencast_portal_desktop_capture_get_name(void *data) +{ + UNUSED_PARAMETER(data); + return obs_module_text("PipeWireDesktopCapture"); +} + +static const char *screencast_portal_window_capture_get_name(void *data) +{ + UNUSED_PARAMETER(data); + return obs_module_text("PipeWireWindowCapture"); +} + +static void *screencast_portal_desktop_capture_create(obs_data_t *settings, + obs_source_t *source) +{ + struct screencast_portal_capture *capture; + + capture = bzalloc(sizeof(struct screencast_portal_capture)); + capture->capture_type = PORTAL_CAPTURE_TYPE_MONITOR; + capture->cursor_visible = obs_data_get_bool(settings, "ShowCursor"); + capture->restore_token = + bstrdup(obs_data_get_string(settings, "RestoreToken")); + capture->source = source; + + init_screencast_capture(capture); + + return capture; +} +static void *screencast_portal_window_capture_create(obs_data_t *settings, + obs_source_t *source) +{ + struct screencast_portal_capture *capture; + + capture = bzalloc(sizeof(struct screencast_portal_capture)); + capture->capture_type = PORTAL_CAPTURE_TYPE_WINDOW; + capture->cursor_visible = obs_data_get_bool(settings, "ShowCursor"); + capture->restore_token = + bstrdup(obs_data_get_string(settings, "RestoreToken")); + capture->source = source; + + init_screencast_capture(capture); + + return capture; +} + +static void screencast_portal_capture_destroy(void *data) +{ + struct screencast_portal_capture *capture = data; + + if (!capture) + return; + + if (capture->session_handle) { + g_dbus_connection_call(portal_get_dbus_connection(), + "org.freedesktop.portal.Desktop", + capture->session_handle, + "org.freedesktop.portal.Session", + "Close", NULL, NULL, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, + NULL); + + g_clear_pointer(&capture->session_handle, g_free); + } + + g_clear_pointer(&capture->restore_token, bfree); + + obs_pipewire_destroy(capture->obs_pw); + g_cancellable_cancel(capture->cancellable); + g_clear_object(&capture->cancellable); + bfree(capture); +} + +static void screencast_portal_capture_save(void *data, obs_data_t *settings) +{ + struct screencast_portal_capture *capture = data; + + obs_data_set_string(settings, "RestoreToken", capture->restore_token); +} + +static void screencast_portal_capture_get_defaults(obs_data_t *settings) +{ + obs_data_set_default_bool(settings, "ShowCursor", true); + obs_data_set_default_string(settings, "RestoreToken", NULL); +} + +static obs_properties_t *screencast_portal_capture_get_properties(void *data) +{ + struct screencast_portal_capture *capture = data; + const char *reload_string_id; + obs_properties_t *properties; + + switch (capture->capture_type) { + case PORTAL_CAPTURE_TYPE_MONITOR: + reload_string_id = "PipeWireSelectMonitor"; + break; + case PORTAL_CAPTURE_TYPE_WINDOW: + reload_string_id = "PipeWireSelectWindow"; + break; + case PORTAL_CAPTURE_TYPE_VIRTUAL: + default: + return NULL; + } + + properties = obs_properties_create(); + obs_properties_add_button2(properties, "Reload", + obs_module_text(reload_string_id), + reload_session_cb, capture); + obs_properties_add_bool(properties, "ShowCursor", + obs_module_text("ShowCursor")); + + return properties; +} + +static void screencast_portal_capture_update(void *data, obs_data_t *settings) +{ + struct screencast_portal_capture *capture = data; + + capture->cursor_visible = obs_data_get_bool(settings, "ShowCursor"); + + if (capture->obs_pw) + obs_pipewire_set_cursor_visible(capture->obs_pw, + capture->cursor_visible); +} + +static void screencast_portal_capture_show(void *data) +{ + struct screencast_portal_capture *capture = data; + + if (capture->obs_pw) + obs_pipewire_show(capture->obs_pw); +} + +static void screencast_portal_capture_hide(void *data) +{ + struct screencast_portal_capture *capture = data; + + if (capture->obs_pw) + obs_pipewire_hide(capture->obs_pw); +} + +static uint32_t screencast_portal_capture_get_width(void *data) +{ + struct screencast_portal_capture *capture = data; + + if (capture->obs_pw) + return obs_pipewire_get_width(capture->obs_pw); + else + return 0; +} + +static uint32_t screencast_portal_capture_get_height(void *data) +{ + struct screencast_portal_capture *capture = data; + + if (capture->obs_pw) + return obs_pipewire_get_height(capture->obs_pw); + else + return 0; +} + +static void screencast_portal_capture_video_render(void *data, + gs_effect_t *effect) +{ + struct screencast_portal_capture *capture = data; + + if (capture->obs_pw) + obs_pipewire_video_render(capture->obs_pw, effect); +} + +void screencast_portal_load(void) +{ + uint32_t available_capture_types = get_available_capture_types(); + bool desktop_capture_available = + (available_capture_types & PORTAL_CAPTURE_TYPE_MONITOR) != 0; + bool window_capture_available = + (available_capture_types & PORTAL_CAPTURE_TYPE_WINDOW) != 0; + + if (available_capture_types == 0) { + blog(LOG_INFO, "[pipewire] No captures available"); + return; + } + + blog(LOG_INFO, "[pipewire] Available captures:"); + if (desktop_capture_available) + blog(LOG_INFO, "[pipewire] - Desktop capture"); + if (window_capture_available) + blog(LOG_INFO, "[pipewire] - Window capture"); + + // Desktop capture + const struct obs_source_info screencast_portal_desktop_capture_info = { + .id = "pipewire-desktop-capture-source", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_VIDEO, + .get_name = screencast_portal_desktop_capture_get_name, + .create = screencast_portal_desktop_capture_create, + .destroy = screencast_portal_capture_destroy, + .save = screencast_portal_capture_save, + .get_defaults = screencast_portal_capture_get_defaults, + .get_properties = screencast_portal_capture_get_properties, + .update = screencast_portal_capture_update, + .show = screencast_portal_capture_show, + .hide = screencast_portal_capture_hide, + .get_width = screencast_portal_capture_get_width, + .get_height = screencast_portal_capture_get_height, + .video_render = screencast_portal_capture_video_render, + .icon_type = OBS_ICON_TYPE_DESKTOP_CAPTURE, + }; + if (desktop_capture_available) + obs_register_source(&screencast_portal_desktop_capture_info); + + // Window capture + const struct obs_source_info screencast_portal_window_capture_info = { + .id = "pipewire-window-capture-source", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_VIDEO, + .get_name = screencast_portal_window_capture_get_name, + .create = screencast_portal_window_capture_create, + .destroy = screencast_portal_capture_destroy, + .save = screencast_portal_capture_save, + .get_defaults = screencast_portal_capture_get_defaults, + .get_properties = screencast_portal_capture_get_properties, + .update = screencast_portal_capture_update, + .show = screencast_portal_capture_show, + .hide = screencast_portal_capture_hide, + .get_width = screencast_portal_capture_get_width, + .get_height = screencast_portal_capture_get_height, + .video_render = screencast_portal_capture_video_render, + .icon_type = OBS_ICON_TYPE_WINDOW_CAPTURE, + }; + if (window_capture_available) + obs_register_source(&screencast_portal_window_capture_info); +} diff --git a/plugins/linux-pipewire/pipewire-capture.h b/plugins/linux-pipewire/screencast-portal.h similarity index 85% rename from plugins/linux-pipewire/pipewire-capture.h rename to plugins/linux-pipewire/screencast-portal.h index 8f9c86d47..874d5c350 100644 --- a/plugins/linux-pipewire/pipewire-capture.h +++ b/plugins/linux-pipewire/screencast-portal.h @@ -1,6 +1,6 @@ -/* pipewire-capture.h +/* screencast-portal.h * - * Copyright 2020 Georges Basile Stavracas Neto + * Copyright 2022 Georges Basile Stavracas Neto * * 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 @@ -20,4 +20,4 @@ #pragma once -void pipewire_capture_load(void); +void screencast_portal_load(void);