/* WirePlumber * * Copyright © 2022 Collabora Ltd. * @author George Kiagiadakis * * SPDX-License-Identifier: MIT */ #include WP_DEFINE_LOCAL_LOG_TOPIC ("m-standard-event-source") /* * Module subscribes for certain object manager events to and pushes them as * events on to the Event Stack. */ enum { ACTION_GET_OBJECT_MANAGER, ACTION_CREATE_EVENT, ACTION_PUSH_EVENT, ACTION_SCHEDULE_RESCAN, N_SIGNALS }; typedef enum { OBJECT_TYPE_PORT, OBJECT_TYPE_LINK, OBJECT_TYPE_NODE, OBJECT_TYPE_SESSION_ITEM, OBJECT_TYPE_CLIENT, OBJECT_TYPE_DEVICE, OBJECT_TYPE_METADATA, N_OBJECT_TYPES, OBJECT_TYPE_INVALID = N_OBJECT_TYPES } ObjectType; typedef enum { RESCAN_CONTEXT_LINKING, RESCAN_CONTEXT_DEFAULT_NODES, N_RESCAN_CONTEXTS, } RescanContext; static GType rescan_context_get_type (void) { static gsize gtype_id = 0; static const GEnumValue values[] = { { RESCAN_CONTEXT_LINKING, "RESCAN_CONTEXT_LINKING", "linking" }, { RESCAN_CONTEXT_DEFAULT_NODES, "RESCAN_CONTEXT_DEFAULT_NODES", "default-nodes" }, { 0, NULL, NULL } }; if (g_once_init_enter (>ype_id)) { GType new_type = g_enum_register_static ( g_intern_static_string ("WpStandardEventSource_RescanContext"), values); g_once_init_leave (>ype_id, new_type); } return (GType) gtype_id; } #define TYPE_RESCAN_CONTEXT (rescan_context_get_type ()) struct _WpStandardEventSource { WpPlugin parent; WpObjectManager *oms[N_OBJECT_TYPES]; WpEventHook *rescan_done_hook; gboolean rescan_scheduled[N_RESCAN_CONTEXTS]; gint n_oms_installed; }; static guint signals[N_SIGNALS] = {0}; G_DECLARE_FINAL_TYPE (WpStandardEventSource, wp_standard_event_source, WP, STANDARD_EVENT_SOURCE, WpPlugin) G_DEFINE_TYPE (WpStandardEventSource, wp_standard_event_source, WP_TYPE_PLUGIN) static void wp_standard_event_source_init (WpStandardEventSource * self) { } static GType object_type_to_gtype (ObjectType type) { switch (type) { case OBJECT_TYPE_PORT: return WP_TYPE_PORT; case OBJECT_TYPE_LINK: return WP_TYPE_LINK; case OBJECT_TYPE_NODE: return WP_TYPE_NODE; case OBJECT_TYPE_SESSION_ITEM: return WP_TYPE_SESSION_ITEM; case OBJECT_TYPE_CLIENT: return WP_TYPE_CLIENT; case OBJECT_TYPE_DEVICE: return WP_TYPE_DEVICE; case OBJECT_TYPE_METADATA: return WP_TYPE_METADATA; default: g_assert_not_reached (); } } static ObjectType type_str_to_object_type (const gchar * type_str) { if (!g_strcmp0 (type_str, "port")) return OBJECT_TYPE_PORT; else if (!g_strcmp0 (type_str, "link")) return OBJECT_TYPE_LINK; else if (!g_strcmp0 (type_str, "node")) return OBJECT_TYPE_NODE; else if (!g_strcmp0 (type_str, "session-item")) return OBJECT_TYPE_SESSION_ITEM; else if (!g_strcmp0 (type_str, "client")) return OBJECT_TYPE_CLIENT; else if (!g_strcmp0 (type_str, "device")) return OBJECT_TYPE_DEVICE; else if (!g_strcmp0 (type_str, "metadata")) return OBJECT_TYPE_METADATA; else return OBJECT_TYPE_INVALID; } static const gchar * get_object_type (gpointer obj, WpProperties **properties) { /* keeping these sorted by the frequency of events related to these objects */ if (WP_IS_PORT (obj)) return "port"; else if (WP_IS_LINK (obj)) return "link"; else if (WP_IS_NODE (obj)) return "node"; else if (WP_IS_SESSION_ITEM (obj)) { if (*properties == NULL) *properties = wp_properties_new_empty(); if (WP_IS_SI_LINKABLE (obj)) { wp_properties_set (*properties, "event.session-item.interface", "linkable"); } else if (WP_IS_SI_LINK (obj)) { wp_properties_set (*properties, "event.session-item.interface", "link"); } return "session-item"; } else if (WP_IS_CLIENT (obj)) return "client"; else if (WP_IS_DEVICE (obj)) return "device"; else if (WP_IS_METADATA (obj)) return "metadata"; wp_debug_object (obj, "Unknown global proxy type"); return G_OBJECT_TYPE_NAME (obj); } static gint get_default_event_priority (const gchar *event_type) { if (g_str_has_prefix (event_type, "select-")) return 500; else if (!g_strcmp0 (event_type, "rescan-for-default-nodes")) return -490; else if (!g_strcmp0 (event_type, "rescan-for-linking")) return -500; else if (!g_strcmp0 (event_type, "node-state-changed")) return 50; else if (!g_strcmp0 (event_type, "metadata-changed")) return 50; else if (g_str_has_suffix (event_type, "-params-changed")) return 50; else if (g_str_has_prefix (event_type, "client-")) return 200; else if (g_str_has_prefix (event_type, "device-")) return 170; else if (g_str_has_prefix (event_type, "port-")) return 150; else if (g_str_has_prefix (event_type, "node-")) return 130; else if (g_str_has_prefix (event_type, "session-item-")) return 110; else if (g_str_has_suffix (event_type, "-added") || g_str_has_suffix (event_type, "-removed")) return 20; wp_debug ("Unknown event type: %s, using priority 0", event_type); return 0; } static WpObjectManager * wp_standard_event_get_object_manager (WpStandardEventSource *self, const gchar * type_str) { ObjectType type = type_str_to_object_type (type_str); if (G_UNLIKELY (type == OBJECT_TYPE_INVALID)) { wp_critical_object (self, "object type '%s' is not valid", type_str); return NULL; } g_return_val_if_fail (self->oms[type], NULL); return g_object_ref (self->oms[type]); } static WpEvent * wp_standard_event_source_create_event (WpStandardEventSource *self, const gchar *event_type, gpointer subject, WpProperties *misc_properties) { g_autoptr (WpEvent) event = NULL; g_autoptr (WpProperties) properties = wp_properties_new_empty (); g_autofree gchar *full_event_type = NULL; const gchar *subject_type = subject ? get_object_type (subject, &properties) : NULL; if (subject_type) { wp_properties_set (properties, "event.subject.type", subject_type); /* prefix the event with the subject type, unless it is a select-* event */ if (!g_str_has_prefix (event_type, "select-")) { full_event_type = g_strdup_printf ("%s-%s", subject_type, event_type); event_type = full_event_type; } } if (misc_properties) wp_properties_add (properties, misc_properties); gint priority = get_default_event_priority (event_type); wp_debug_object (self, "pushing event '%s', prio %d, subject " WP_OBJECT_FORMAT " (%s)", event_type, priority, WP_OBJECT_ARGS (subject), subject_type); event = wp_event_new (event_type, priority, g_steal_pointer (&properties), G_OBJECT (self), G_OBJECT (subject)); /* watch for subject pw-proxy-destroyed and cancel event, unless this is a "removed" event, in which case we expect the proxy to be destroyed and the event should still go through */ if (subject && !g_str_has_suffix (event_type, "-removed") && g_type_is_a (G_OBJECT_TYPE (subject), WP_TYPE_PROXY)) { g_signal_connect_object (subject, "pw-proxy-destroyed", (GCallback) g_cancellable_cancel, wp_event_get_cancellable (event), G_CONNECT_SWAPPED); } return g_steal_pointer (&event); } static void wp_standard_event_source_push_event (WpStandardEventSource *self, const gchar *event_type, gpointer subject, WpProperties *misc_properties) { g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); g_return_if_fail (core); g_autoptr (WpEventDispatcher) dispatcher = wp_event_dispatcher_get_instance (core); g_return_if_fail (dispatcher); wp_event_dispatcher_push_event (dispatcher, wp_standard_event_source_create_event ( self, event_type, subject, misc_properties)); } static void wp_standard_event_source_schedule_rescan (WpStandardEventSource *self, RescanContext context) { if (!self->rescan_scheduled[context]) { g_autoptr (GEnumClass) klass = g_type_class_ref (TYPE_RESCAN_CONTEXT); GEnumValue *value = g_enum_get_value (klass, context); g_autofree gchar *event_type = g_strdup_printf ("rescan-for-%s", value->value_nick); wp_standard_event_source_push_event (self, event_type, NULL, NULL); self->rescan_scheduled[context] = TRUE; } } static void on_metadata_changed (WpMetadata *obj, guint32 subject, const gchar *key, const gchar *spa_type, const gchar *value, WpStandardEventSource *self) { g_autoptr (WpProperties) properties = wp_properties_new_empty (); wp_properties_setf (properties, "event.subject.id", "%u", subject); wp_properties_set (properties, "event.subject.key", key); wp_properties_set (properties, "event.subject.spa_type", spa_type); wp_properties_set (properties, "event.subject.value", value); wp_standard_event_source_push_event (self, "changed", obj, properties); } static void on_params_changed (WpPipewireObject *obj, const gchar *id, WpStandardEventSource *self) { g_autoptr (WpProperties) properties = wp_properties_new_empty (); wp_properties_set (properties, "event.subject.param-id", id); wp_standard_event_source_push_event (self, "params-changed", obj, properties); } static void on_node_state_changed (WpNode *obj, WpNodeState old_state, WpNodeState new_state, WpStandardEventSource *self) { g_autoptr (GEnumClass) klass = g_type_class_ref (WP_TYPE_NODE_STATE); GEnumValue *old_value = g_enum_get_value (klass, old_state); GEnumValue *new_value = g_enum_get_value (klass, new_state); g_autoptr (WpProperties) properties = wp_properties_new ( "event.subject.old-state", old_value ? old_value->value_nick : NULL, "event.subject.new-state", new_value ? new_value->value_nick : NULL, NULL); wp_standard_event_source_push_event (self, "state-changed", obj, properties); } static void on_object_added (WpObjectManager *om, WpObject *obj, WpStandardEventSource *self) { wp_standard_event_source_push_event (self, "added", obj, NULL); if (WP_IS_PIPEWIRE_OBJECT (obj)) { g_signal_connect_object (obj, "params-changed", G_CALLBACK (on_params_changed), self, 0); } if (WP_IS_NODE (obj)) { g_signal_connect_object (obj, "state-changed", G_CALLBACK (on_node_state_changed), self, 0); } else if (WP_IS_METADATA (obj)) { g_signal_connect_object (obj, "changed", G_CALLBACK (on_metadata_changed), self, 0); } } static void on_object_removed (WpObjectManager *om, WpObject *obj, WpStandardEventSource *self) { wp_standard_event_source_push_event (self, "removed", obj, NULL); } static void on_om_installed (WpObjectManager * om, WpStandardEventSource * self) { if (++self->n_oms_installed == N_OBJECT_TYPES) wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); } static void on_rescan_done (WpEvent * event, WpStandardEventSource * self) { g_autoptr (WpProperties) properties = wp_event_get_properties (event); const gchar *event_type = wp_properties_get (properties, "event.type"); /* the event type is "rescan-for-" and the enum nickname is just "", so we get the substring from the 12th character onwards */ g_autoptr (GEnumClass) klass = g_type_class_ref (TYPE_RESCAN_CONTEXT); GEnumValue *value = g_enum_get_value_by_nick (klass, &event_type[11]); g_return_if_fail (value != NULL && value->value_nick != NULL); self->rescan_scheduled[value->value] = FALSE; } static void wp_standard_event_source_enable (WpPlugin * plugin, WpTransition * transition) { WpStandardEventSource * self = WP_STANDARD_EVENT_SOURCE (plugin); g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin)); g_return_if_fail (core); g_autoptr (WpEventDispatcher) dispatcher = wp_event_dispatcher_get_instance (core); g_return_if_fail (dispatcher); /* install object managers */ self->n_oms_installed = 0; for (gint i = 0; i < N_OBJECT_TYPES; i++) { GType gtype = object_type_to_gtype (i); self->oms[i] = wp_object_manager_new (); wp_object_manager_add_interest (self->oms[i], gtype, NULL); wp_object_manager_request_object_features (self->oms[i], gtype, WP_OBJECT_FEATURES_ALL); g_signal_connect_object (self->oms[i], "object-added", G_CALLBACK (on_object_added), self, 0); g_signal_connect_object (self->oms[i], "object-removed", G_CALLBACK (on_object_removed), self, 0); g_signal_connect_object (self->oms[i], "installed", G_CALLBACK (on_om_installed), self, 0); wp_core_install_object_manager (core, self->oms[i]); } /* install hook to restore the rescan_scheduled state after rescanning */ self->rescan_done_hook = wp_simple_event_hook_new ( "m-standard-event-source/rescan-done", NULL, NULL, g_cclosure_new_object ((GCallback) on_rescan_done, G_OBJECT (self))); wp_interest_event_hook_add_interest ( WP_INTEREST_EVENT_HOOK (self->rescan_done_hook), WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "#s", "rescan-for-*", NULL); wp_event_dispatcher_register_hook (dispatcher, self->rescan_done_hook); } static void wp_standard_event_source_disable (WpPlugin * plugin) { WpStandardEventSource * self = WP_STANDARD_EVENT_SOURCE (plugin); g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin)); g_autoptr (WpEventDispatcher) dispatcher = core ? wp_event_dispatcher_get_instance (core) : NULL; for (gint i = 0; i < N_OBJECT_TYPES; i++) g_clear_object (&self->oms[i]); if (dispatcher) wp_event_dispatcher_unregister_hook (dispatcher, self->rescan_done_hook); g_clear_object (&self->rescan_done_hook); } static void wp_standard_event_source_class_init (WpStandardEventSourceClass * klass) { WpPluginClass *plugin_class = (WpPluginClass *) klass; plugin_class->enable = wp_standard_event_source_enable; plugin_class->disable = wp_standard_event_source_disable; signals[ACTION_GET_OBJECT_MANAGER] = g_signal_new_class_handler ( "get-object-manager", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, (GCallback) wp_standard_event_get_object_manager, NULL, NULL, NULL, WP_TYPE_OBJECT_MANAGER, 1, G_TYPE_STRING); signals[ACTION_CREATE_EVENT] = g_signal_new_class_handler ( "create-event", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, (GCallback) wp_standard_event_source_create_event, NULL, NULL, NULL, WP_TYPE_EVENT, 3, G_TYPE_STRING, WP_TYPE_OBJECT, WP_TYPE_PROPERTIES); signals[ACTION_PUSH_EVENT] = g_signal_new_class_handler ( "push-event", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, (GCallback) wp_standard_event_source_push_event, NULL, NULL, NULL, G_TYPE_NONE, 3, G_TYPE_STRING, WP_TYPE_OBJECT, WP_TYPE_PROPERTIES); signals[ACTION_SCHEDULE_RESCAN] = g_signal_new_class_handler ( "schedule-rescan", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, (GCallback) wp_standard_event_source_schedule_rescan, NULL, NULL, NULL, G_TYPE_NONE, 1, TYPE_RESCAN_CONTEXT); } WP_PLUGIN_EXPORT GObject * wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error) { return G_OBJECT (g_object_new (wp_standard_event_source_get_type (), "name", "standard-event-source", "core", core, NULL)); }