diff --git a/config.h.meson b/config.h.meson index 928a758ae..5ad68d839 100644 --- a/config.h.meson +++ b/config.h.meson @@ -22,6 +22,9 @@ /* Define if we have NetworkManager */ #mesondefine HAVE_NETWORKMANAGER +/* Define if we have Pipewire */ +#mesondefine HAVE_PIPEWIRE + /* Define if we have systemd */ #mesondefine HAVE_SYSTEMD diff --git a/meson.build b/meson.build index a7eeaf4e6..b6bfc58da 100644 --- a/meson.build +++ b/meson.build @@ -32,6 +32,7 @@ startup_req = '>= 0.11' systemd_req = '>= 246' ibus_req = '>= 1.5.19' gnome_desktop_req = '>= 40' +pipewire_req = '>= 0.3.49' nm_req = '>= 1.10.4' secret_req = '>= 0.18' @@ -102,6 +103,14 @@ else have_networkmanager = false endif +if get_option('camera_monitor') + libpipewire_dep = dependency('libpipewire-0.3', version: pipewire_req) + have_pipewire = true +else + libpipewire_dep = [] + have_pipewire = false +endif + if get_option('systemd') libsystemd_dep = dependency('libsystemd', version: systemd_req) systemd_dep = dependency('systemd', version: systemd_req) @@ -136,6 +145,7 @@ cdata.set_quoted('VERSION', meson.project_version()) cdata.set_quoted('PACKAGE_VERSION', meson.project_version()) cdata.set('HAVE_NETWORKMANAGER', have_networkmanager) +cdata.set('HAVE_PIPEWIRE', have_pipewire) cdata.set('HAVE_SYSTEMD', have_systemd) # New API added in glib-2.57.2 @@ -311,6 +321,7 @@ meson.add_dist_script('meson/generate-stylesheets.py') meson.add_dist_script('meson/check-version.py', meson.project_version(), 'NEWS') summary_options = { + 'camera_monitor': get_option('camera_monitor'), 'networkmanager': get_option('networkmanager'), 'systemd': get_option('systemd'), 'extensions_app': get_option('extensions_app'), diff --git a/meson_options.txt b/meson_options.txt index 2d317e602..6e83d92f2 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,9 @@ +option('camera_monitor', + type: 'boolean', + value: true, + description: 'Enable the camera monitor' +) + option('extensions_tool', type: 'boolean', value: true, diff --git a/src/meson.build b/src/meson.build index 7ede16f45..3e18aad93 100644 --- a/src/meson.build +++ b/src/meson.build @@ -63,7 +63,8 @@ gnome_shell_deps = [ gi_dep, polkit_dep, gcr_dep, - libsystemd_dep + libsystemd_dep, + libpipewire_dep, ] gnome_shell_deps += nm_deps @@ -97,6 +98,7 @@ libshell_public_headers = [ 'shell-app-system.h', 'shell-app-usage.h', 'shell-blur-effect.h', + 'shell-camera-monitor.h', 'shell-glsl-effect.h', 'shell-global.h', 'shell-invert-lightness-effect.h', @@ -136,6 +138,7 @@ libshell_sources = [ 'shell-app-system.c', 'shell-app-usage.c', 'shell-blur-effect.c', + 'shell-camera-monitor.c', 'shell-global.c', 'shell-glsl-effect.c', 'shell-invert-lightness-effect.c', diff --git a/src/shell-camera-monitor.c b/src/shell-camera-monitor.c new file mode 100644 index 000000000..34bf2ee10 --- /dev/null +++ b/src/shell-camera-monitor.c @@ -0,0 +1,435 @@ +/* + * Shell camera monitor + * + * Copyright (C) 2023 Collabora Ltd. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "shell-camera-monitor.h" + +#ifdef HAVE_PIPEWIRE +#include +#include +#endif + +#define RECONNECT_DELAY_MS 5000 + +enum { + PROP_0, + PROP_CAMERAS_IN_USE, + PROP_LAST +}; + +static GParamSpec *obj_props[PROP_LAST]; + +struct _ShellCameraMonitor { + GObject parent; + + gboolean cameras_in_use; + +#ifdef HAVE_PIPEWIRE + GPtrArray *node_list; + guint reconnect_id; + + GSource *pipewire_source; + struct pw_context *context; + struct pw_core *core; + struct pw_registry *registry; + struct spa_hook core_listener; + struct spa_hook registry_listener; +#endif +}; + +G_DEFINE_TYPE (ShellCameraMonitor, shell_camera_monitor, G_TYPE_OBJECT); + +#ifdef HAVE_PIPEWIRE +typedef struct _Node { + ShellCameraMonitor *monitor; + gboolean running; + struct spa_hook proxy_listener; + struct spa_hook object_listener; +} Node; + +typedef struct _MetaPipeWireSource +{ + GSource source; + + struct pw_loop *pipewire_loop; +} MetaPipeWireSource; + +static gboolean shell_camera_monitor_connect_core (ShellCameraMonitor *monitor); +static void shell_camera_monitor_disconnect_core (ShellCameraMonitor *monitor); + +static void +shell_camera_monitor_update_state (ShellCameraMonitor *monitor) +{ + gboolean new_cameras_in_use = FALSE; + int i; + + for (i = 0; i < monitor->node_list->len; i++) + { + struct pw_proxy *proxy; + Node *node; + + proxy = g_ptr_array_index (monitor->node_list, i); + node = pw_proxy_get_user_data (proxy); + + if (node->running) + { + new_cameras_in_use = TRUE; + break; + } + } + + if (new_cameras_in_use != monitor->cameras_in_use) + { + monitor->cameras_in_use = new_cameras_in_use; + g_object_notify_by_pspec (G_OBJECT (monitor), + obj_props[PROP_CAMERAS_IN_USE]); + } +} + +static void +proxy_destroy (void *data) +{ + Node *node = data; + + spa_hook_remove (&node->proxy_listener); + spa_hook_remove (&node->object_listener); +} + +static const struct pw_proxy_events proxy_events = { + PW_VERSION_PROXY_EVENTS, + .destroy = proxy_destroy, +}; + +static void +node_event_info (void *data, + const struct pw_node_info *info) +{ + Node *node = data; + + node->running = (info->state == PW_NODE_STATE_RUNNING || + info->state == PW_NODE_STATE_IDLE); + + shell_camera_monitor_update_state (node->monitor); +} + +static const struct pw_node_events node_events = { + PW_VERSION_NODE_EVENTS, + .info = node_event_info, +}; + +static void +registry_event_global (void *data, + uint32_t id, + uint32_t permissions, + const char *type, + uint32_t version, + const struct spa_dict *props) +{ + ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (data); + struct pw_proxy *proxy; + const char *prop_str; + Node *node; + + if (!props || !(spa_streq (type, PW_TYPE_INTERFACE_Node))) + return; + + prop_str = spa_dict_lookup (props, PW_KEY_MEDIA_ROLE); + if (!prop_str || (strcmp (prop_str, "Camera") != 0)) + return; + + proxy = pw_registry_bind (monitor->registry, + id, + PW_TYPE_INTERFACE_Node, + PW_VERSION_NODE, + sizeof (Node)); + node = pw_proxy_get_user_data (proxy); + node->monitor = monitor; + + pw_proxy_add_listener (proxy, + &node->proxy_listener, + &proxy_events, + node); + + pw_proxy_add_object_listener (proxy, + &node->object_listener, + &node_events, + node); + + g_ptr_array_add (monitor->node_list, proxy); +} + +static void +registry_event_global_remove (void *data, + uint32_t id) +{ + ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (data); + struct pw_proxy *proxy_to_remove = NULL; + int i; + + for (i = 0; i < monitor->node_list->len; i++) + { + struct pw_proxy *proxy; + + proxy = g_ptr_array_index (monitor->node_list, i); + if (pw_proxy_get_bound_id (proxy) == id) + { + proxy_to_remove = proxy; + break; + } + } + + if (proxy_to_remove) + g_ptr_array_remove (monitor->node_list, proxy_to_remove); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +static void +idle_reconnect (gpointer data) +{ + ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (data); + + if (shell_camera_monitor_connect_core (monitor)) + monitor->reconnect_id = 0; + else + monitor->reconnect_id = + g_timeout_add_once (RECONNECT_DELAY_MS, idle_reconnect, monitor); +} + +static void +on_core_error (void *data, + uint32_t id, + int seq, + int res, + const char *message) +{ + if (id == 0 && res == -EPIPE) + { + ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (data); + + shell_camera_monitor_disconnect_core (monitor); + if (monitor->cameras_in_use) + { + monitor->cameras_in_use = FALSE; + g_object_notify_by_pspec (G_OBJECT (monitor), + obj_props[PROP_CAMERAS_IN_USE]); + } + + if (monitor->reconnect_id == 0) + monitor->reconnect_id = + g_timeout_add_once (RECONNECT_DELAY_MS, idle_reconnect, monitor); + } +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = on_core_error, +}; + +static gboolean +shell_camera_monitor_connect_core (ShellCameraMonitor *monitor) +{ + monitor->core = pw_context_connect (monitor->context, NULL, 0); + if (!monitor->core) + return FALSE; + + pw_core_add_listener (monitor->core, + &monitor->core_listener, + &core_events, + monitor); + + monitor->registry = pw_core_get_registry (monitor->core, + PW_VERSION_REGISTRY, + 0); + pw_registry_add_listener (monitor->registry, + &monitor->registry_listener, + ®istry_events, + monitor); + return TRUE; +} + +static void +shell_camera_monitor_disconnect_core (ShellCameraMonitor *monitor) +{ + g_ptr_array_set_size (monitor->node_list, 0); + + g_clear_pointer ((struct pw_proxy**) &monitor->registry, pw_proxy_destroy); + spa_hook_remove (&monitor->registry_listener); + g_clear_pointer (&monitor->core, pw_core_disconnect); + spa_hook_remove (&monitor->core_listener); +} + +static gboolean +pipewire_loop_source_prepare (GSource *source, + int *timeout) +{ + *timeout = -1; + return FALSE; +} + +static gboolean +pipewire_loop_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + MetaPipeWireSource *pipewire_source = (MetaPipeWireSource *) source; + int result; + + result = pw_loop_iterate (pipewire_source->pipewire_loop, 0); + if (result < 0) + g_warning ("pipewire_loop_iterate failed: %s", spa_strerror (result)); + + return TRUE; +} + +static void +pipewire_loop_source_finalize (GSource *source) +{ + MetaPipeWireSource *pipewire_source = (MetaPipeWireSource *) source; + + pw_loop_leave (pipewire_source->pipewire_loop); + pw_loop_destroy (pipewire_source->pipewire_loop); +} + +static GSourceFuncs pipewire_source_funcs = +{ + pipewire_loop_source_prepare, + NULL, + pipewire_loop_source_dispatch, + pipewire_loop_source_finalize +}; + +static GSource * +create_pipewire_source (struct pw_loop *pipewire_loop) +{ + GSource *source; + MetaPipeWireSource *pipewire_source; + + source = g_source_new (&pipewire_source_funcs, sizeof (MetaPipeWireSource)); + g_source_set_name (source, "[gnome-shell] PipeWire"); + + pipewire_source = (MetaPipeWireSource *) source; + pipewire_source->pipewire_loop = pipewire_loop; + + g_source_add_unix_fd (source, + pw_loop_get_fd (pipewire_source->pipewire_loop), + G_IO_IN | G_IO_ERR); + + pw_loop_enter (pipewire_source->pipewire_loop); + g_source_attach (source, NULL); + g_source_unref (source); + + return source; +} + +#endif + +static void +shell_camera_monitor_finalize (GObject *object) +{ +#ifdef HAVE_PIPEWIRE + ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (object); + + shell_camera_monitor_disconnect_core (monitor); + g_clear_pointer (&monitor->node_list, g_ptr_array_unref); + g_clear_pointer (&monitor->context, pw_context_destroy); + g_clear_pointer (&monitor->pipewire_source, g_source_destroy); + g_clear_handle_id (&monitor->reconnect_id, g_source_remove); + pw_deinit (); +#endif + + G_OBJECT_CLASS (shell_camera_monitor_parent_class)->finalize (object); +} + +static void +shell_camera_monitor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (object); + + switch (prop_id) + { + case PROP_CAMERAS_IN_USE: + g_value_set_boolean (value, monitor->cameras_in_use); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_camera_monitor_class_init (ShellCameraMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = shell_camera_monitor_finalize; + object_class->get_property = shell_camera_monitor_get_property; + + obj_props[PROP_CAMERAS_IN_USE] = + g_param_spec_boolean ("cameras-in-use", + "Cameras in use", + "Whether any camera is currently used by an app", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST, obj_props); +} + +static void +shell_camera_monitor_init (ShellCameraMonitor *monitor) +{ +#ifdef HAVE_PIPEWIRE + struct pw_loop *pipewire_loop; + + monitor->node_list = + g_ptr_array_new_full (5, (GDestroyNotify) pw_proxy_destroy); + + pw_init (NULL, NULL); + + pipewire_loop = pw_loop_new (NULL); + if (!pipewire_loop) + goto error; + + monitor->pipewire_source = create_pipewire_source (pipewire_loop); + if (!monitor->pipewire_source) + goto error; + + monitor->context = pw_context_new (pipewire_loop, NULL, 0); + if (!monitor->context) + goto error; + + if (!shell_camera_monitor_connect_core (monitor)) + goto error; + + return; + +error: + g_message ("Failed to start camera monitor"); +#endif +} diff --git a/src/shell-camera-monitor.h b/src/shell-camera-monitor.h new file mode 100644 index 000000000..9f9811686 --- /dev/null +++ b/src/shell-camera-monitor.h @@ -0,0 +1,35 @@ +/* + * Shell camera monitor + * + * Copyright (C) 2023 Collabora Ltd. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef __SHELL_CAMERA_MONITOR_H__ +#define __SHELL_CAMERA_MONITOR_H__ + +#include + +G_BEGIN_DECLS + +#define SHELL_TYPE_CAMERA_MONITOR (shell_camera_monitor_get_type ()) +G_DECLARE_FINAL_TYPE (ShellCameraMonitor, shell_camera_monitor, + SHELL, CAMERA_MONITOR, GObject) + +G_END_DECLS + +#endif /* __SHELL_CAMERA_MONITOR_H__ */