diff --git a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipewire/pipewire.c index a33d5a106..ffaf77e79 100644 --- a/plugins/linux-pipewire/pipewire.c +++ b/plugins/linux-pipewire/pipewire.c @@ -59,17 +59,9 @@ struct format_info { }; struct _obs_pipewire_data { - GCancellable *cancellable; - - 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; @@ -102,20 +94,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) @@ -147,72 +131,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 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) { @@ -236,24 +154,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); - } - 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) @@ -929,439 +833,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; - - portal_create_request_path(&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; - - portal_create_request_path(&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; - - portal_create_request_path(&request_path, &request_token); - portal_create_session_path(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; - - blog(LOG_INFO, "PipeWire initialized"); - - 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; } @@ -1374,43 +856,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) @@ -1479,8 +929,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..ce5adfcdd 100644 --- a/plugins/linux-pipewire/pipewire.h +++ b/plugins/linux-pipewire/pipewire.h @@ -27,25 +27,14 @@ 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/screencast-portal.c b/plugins/linux-pipewire/screencast-portal.c index 98ca7bca6..bc338bae7 100644 --- a/plugins/linux-pipewire/screencast-portal.c +++ b/plugins/linux-pipewire/screencast-portal.c @@ -21,10 +21,522 @@ #include "pipewire.h" #include "portal.h" +#include + 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; }; +/* ------------------------------------------------- */ + +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( + portal_get_dbus_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 (portal_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(portal_get_dbus_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 = 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) && + 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 (portal_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(portal_get_dbus_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(portal_get_dbus_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 = portal_get_dbus_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) @@ -45,8 +557,13 @@ static void *screencast_portal_desktop_capture_create(obs_data_t *settings, struct screencast_portal_capture *capture; capture = bzalloc(sizeof(struct screencast_portal_capture)); - capture->obs_pw = obs_pipewire_create(PORTAL_CAPTURE_TYPE_MONITOR, - settings, source); + 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; } @@ -56,8 +573,13 @@ static void *screencast_portal_window_capture_create(obs_data_t *settings, struct screencast_portal_capture *capture; capture = bzalloc(sizeof(struct screencast_portal_capture)); - capture->obs_pw = obs_pipewire_create(PORTAL_CAPTURE_TYPE_WINDOW, - settings, source); + 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; } @@ -69,7 +591,23 @@ static void screencast_portal_capture_destroy(void *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); } @@ -77,67 +615,88 @@ static void screencast_portal_capture_save(void *data, obs_data_t *settings) { struct screencast_portal_capture *capture = data; - obs_pipewire_save(capture->obs_pw, settings); + obs_data_set_string(settings, "RestoreToken", capture->restore_token); } static void screencast_portal_capture_get_defaults(obs_data_t *settings) { - obs_pipewire_get_defaults(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; - enum portal_capture_type capture_type; + const char *reload_string_id; + obs_properties_t *properties; - capture_type = obs_pipewire_get_capture_type(capture->obs_pw); - - switch (capture_type) { + switch (capture->capture_type) { case PORTAL_CAPTURE_TYPE_MONITOR: - return obs_pipewire_get_properties(capture->obs_pw, - "PipeWireSelectMonitor"); + reload_string_id = "PipeWireSelectMonitor"; + break; case PORTAL_CAPTURE_TYPE_WINDOW: - return obs_pipewire_get_properties(capture->obs_pw, - "PipeWireSelectWindow"); + 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; - obs_pipewire_update(capture->obs_pw, settings); + 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; - obs_pipewire_show(capture->obs_pw); + if (capture->obs_pw) + obs_pipewire_show(capture->obs_pw); } static void screencast_portal_capture_hide(void *data) { struct screencast_portal_capture *capture = data; - obs_pipewire_hide(capture->obs_pw); + 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; - return obs_pipewire_get_width(capture->obs_pw); + 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; - return obs_pipewire_get_height(capture->obs_pw); + if (capture->obs_pw) + return obs_pipewire_get_height(capture->obs_pw); + else + return 0; } static void screencast_portal_capture_video_render(void *data, @@ -145,7 +704,8 @@ static void screencast_portal_capture_video_render(void *data, { struct screencast_portal_capture *capture = data; - obs_pipewire_video_render(capture->obs_pw, effect); + if (capture->obs_pw) + obs_pipewire_video_render(capture->obs_pw, effect); } void screencast_portal_load(void)