/* WirePlumber * * Copyright © 2020 Collabora Ltd. * @author George Kiagiadakis * * SPDX-License-Identifier: MIT */ /** * SECTION: endpoint-link * @title: PipeWire Endpoint Link */ #define G_LOG_DOMAIN "wp-endpoint-link" #include "endpoint-link.h" #include "session.h" #include "debug.h" #include "error.h" #include "wpenums.h" #include "private/pipewire-object-mixin.h" #include #include enum { SIGNAL_STATE_CHANGED, N_SIGNALS, }; static guint32 signals[N_SIGNALS] = {0}; static void wp_endpoint_link_pw_object_mixin_priv_interface_init ( WpPwObjectMixinPrivInterface * iface); /** * WpEndpointLink: * * The #WpEndpointLink class allows accessing the properties and methods of a * PipeWire endpoint link object (`struct pw_endpoint_link` from the * session-manager extension). * * A #WpEndpointLink is constructed internally when a new endpoint link appears * on the PipeWire registry and it is made available through the * #WpObjectManager API. */ G_DEFINE_TYPE_WITH_CODE (WpEndpointLink, wp_endpoint_link, WP_TYPE_GLOBAL_PROXY, G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_pw_object_mixin_object_interface_init) G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV, wp_endpoint_link_pw_object_mixin_priv_interface_init)); static void wp_endpoint_link_init (WpEndpointLink * self) { } static void wp_endpoint_link_activate_execute_step (WpObject * object, WpFeatureActivationTransition * transition, guint step, WpObjectFeatures missing) { switch (step) { case WP_PW_OBJECT_MIXIN_STEP_BIND: case WP_TRANSITION_STEP_ERROR: /* base class can handle BIND and ERROR */ WP_OBJECT_CLASS (wp_endpoint_link_parent_class)-> activate_execute_step (object, transition, step, missing); break; case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO: /* just wait, info will be emitted anyway after binding */ break; case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS: wp_pw_object_mixin_cache_params (object, missing); break; default: g_assert_not_reached (); } } static void wp_endpoint_link_deactivate (WpObject * object, WpObjectFeatures features) { wp_pw_object_mixin_deactivate (object, features); WP_OBJECT_CLASS (wp_endpoint_link_parent_class)->deactivate (object, features); } static const struct pw_endpoint_link_events endpoint_link_events = { PW_VERSION_ENDPOINT_LINK_EVENTS, .info = (HandleEventInfoFunc(endpoint_link)) wp_pw_object_mixin_handle_event_info, .param = wp_pw_object_mixin_handle_event_param, }; static void wp_endpoint_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy) { wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy, endpoint_link, &endpoint_link_events); } static void wp_endpoint_link_class_init (WpEndpointLinkClass * klass) { GObjectClass *object_class = (GObjectClass *) klass; WpObjectClass *wpobject_class = (WpObjectClass *) klass; WpProxyClass *proxy_class = (WpProxyClass *) klass; object_class->get_property = wp_pw_object_mixin_get_property; wpobject_class->get_supported_features = wp_pw_object_mixin_get_supported_features; wpobject_class->activate_get_next_step = wp_pw_object_mixin_activate_get_next_step; wpobject_class->activate_execute_step = wp_endpoint_link_activate_execute_step; wpobject_class->deactivate = wp_endpoint_link_deactivate; proxy_class->pw_iface_type = PW_TYPE_INTERFACE_EndpointLink; proxy_class->pw_iface_version = PW_VERSION_ENDPOINT_LINK; proxy_class->pw_proxy_created = wp_endpoint_link_pw_proxy_created; proxy_class->pw_proxy_destroyed = wp_pw_object_mixin_handle_pw_proxy_destroyed; wp_pw_object_mixin_class_override_properties (object_class); /** * WpEndpointLink::state-changed: * @self: the endpoint link * @old_state: the old state of the link * @new_state: the new state of the link * @error: (nullable): the error string if the new state is * %WP_ENDPOINT_LINK_STATE_ERROR * * Emitted when an endpoint link changes state */ signals[SIGNAL_STATE_CHANGED] = g_signal_new ( "state-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 3, WP_TYPE_ENDPOINT_LINK_STATE, WP_TYPE_ENDPOINT_LINK_STATE, G_TYPE_STRING); } static void wp_endpoint_link_process_info (gpointer instance, gpointer old_info, gpointer i) { const struct pw_endpoint_link_info *info = i; if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_STATE) { WpEndpointLinkState old_state = old_info ? (WpEndpointLinkState) ((struct pw_endpoint_link_info *) old_info)->state : WP_ENDPOINT_LINK_STATE_ERROR; g_signal_emit (instance, signals[SIGNAL_STATE_CHANGED], 0, old_state, info->state, info->error); } } static gint wp_endpoint_link_enum_params (gpointer instance, guint32 id, guint32 start, guint32 num, WpSpaPod *filter) { WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance); return pw_endpoint_link_enum_params (d->iface, 0, id, start, num, filter ? wp_spa_pod_get_spa_pod (filter) : NULL); } static gint wp_endpoint_link_set_param (gpointer instance, guint32 id, guint32 flags, WpSpaPod * param) { WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance); return pw_endpoint_link_set_param (d->iface, id, flags, wp_spa_pod_get_spa_pod (param)); } static void wp_endpoint_link_pw_object_mixin_priv_interface_init ( WpPwObjectMixinPrivInterface * iface) { wp_pw_object_mixin_priv_interface_info_init (iface, endpoint_link, ENDPOINT_LINK); iface->process_info = wp_endpoint_link_process_info; iface->enum_params = wp_endpoint_link_enum_params; iface->set_param = wp_endpoint_link_set_param; } /** * wp_endpoint_link_get_linked_object_ids: * @self: the endpoint link * @output_endpoint: (out) (optional): the bound id of the output (source) * endpoint * @input_endpoint: (out) (optional): the bound id of the input (sink) * endpoint * * Retrieves the ids of the objects that are linked by this endpoint link * * Note: Using this method requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO */ void wp_endpoint_link_get_linked_object_ids (WpEndpointLink * self, guint32 * output_endpoint, guint32 * input_endpoint) { g_return_if_fail (WP_IS_ENDPOINT_LINK (self)); WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self); struct pw_endpoint_link_info *info = d->info; g_return_if_fail (info); if (output_endpoint) *output_endpoint = info->output_endpoint_id; if (input_endpoint) *input_endpoint = info->input_endpoint_id; } /** * wp_endpoint_link_get_state: * @self: the endpoint link * @error: (out) (optional) (transfer none): the error string if the state is * %WP_ENDPOINT_LINK_STATE_ERROR * * Retrieves the current state of the link * * Note: Using this method requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO * Returns: the current state of the link */ WpEndpointLinkState wp_endpoint_link_get_state (WpEndpointLink * self, const gchar ** error) { g_return_val_if_fail (WP_IS_ENDPOINT_LINK (self), WP_ENDPOINT_LINK_STATE_ERROR); WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self); struct pw_endpoint_link_info *info = d->info; g_return_val_if_fail (info, WP_ENDPOINT_LINK_STATE_ERROR); if (error) *error = info->error; return (WpEndpointLinkState) info->state; } /** * wp_endpoint_link_request_state: * @self: the endpoint link * @target: the desired target state of the link * * Requests a state change on the link * * Note: Using this method requires %WP_PROXY_FEATURE_BOUND */ void wp_endpoint_link_request_state (WpEndpointLink * self, WpEndpointLinkState target) { g_return_if_fail (WP_IS_ENDPOINT_LINK (self)); WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self); g_return_if_fail (d->iface); pw_endpoint_link_request_state (d->iface, (enum pw_endpoint_link_state) target); } /* WpImplEndpointLink */ enum { IMPL_PROP_0, IMPL_PROP_ITEM, }; struct _WpImplEndpointLink { WpEndpointLink parent; struct spa_interface iface; struct pw_endpoint_link_info info; WpProperties *immutable_props; WpSiLink *item; }; static void wp_endpoint_link_impl_pw_object_mixin_priv_interface_init ( WpPwObjectMixinPrivInterface * iface); G_DEFINE_TYPE_WITH_CODE (WpImplEndpointLink, wp_impl_endpoint_link, WP_TYPE_ENDPOINT_LINK, G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV, wp_endpoint_link_impl_pw_object_mixin_priv_interface_init)) static void on_item_activated (WpSessionItem * item, GAsyncResult * res, gpointer data) { WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (data); g_autoptr (GError) error = NULL; if (!wp_object_activate_finish (WP_OBJECT (item), res, &error)) { wp_message_object (self, "failed to activate link: %s", error->message); self->info.error = g_strdup (error->message); /* on_si_link_flags_changed() will be called right after we return, taking care of the rest... */ } } static int impl_request_state (void *object, enum pw_endpoint_link_state state) { WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object); int ret = 0; if (state == self->info.state) return ret; switch (state) { case PW_ENDPOINT_LINK_STATE_ACTIVE: wp_object_activate (WP_OBJECT (self->item), WP_SESSION_ITEM_FEATURE_ACTIVE, NULL, (GAsyncReadyCallback) on_item_activated, self); break; case PW_ENDPOINT_LINK_STATE_INACTIVE: wp_object_deactivate (WP_OBJECT (self->item), WP_SESSION_ITEM_FEATURE_ACTIVE); break; default: ret = -EINVAL; break; } return ret; } static const struct pw_endpoint_link_methods impl_endpoint_link = { PW_VERSION_ENDPOINT_LINK_METHODS, .add_listener = (ImplAddListenerFunc(endpoint_link)) wp_pw_object_mixin_impl_add_listener, .subscribe_params = wp_pw_object_mixin_impl_subscribe_params, .enum_params = wp_pw_object_mixin_impl_enum_params, .set_param = wp_pw_object_mixin_impl_set_param, .request_state = impl_request_state, }; static void wp_impl_endpoint_link_init (WpImplEndpointLink * self) { WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self); self->iface = SPA_INTERFACE_INIT ( PW_TYPE_INTERFACE_EndpointLink, PW_VERSION_ENDPOINT_LINK, &impl_endpoint_link, self); d->info = &self->info; d->iface = &self->iface; } static void populate_properties (WpImplEndpointLink * self) { WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self); g_clear_pointer (&d->properties, wp_properties_unref); d->properties = wp_si_link_get_properties (self->item); if (!d->properties) d->properties = wp_properties_new_empty (); d->properties = wp_properties_ensure_unique_owner (d->properties); wp_properties_update (d->properties, self->immutable_props); self->info.props = (struct spa_dict *) wp_properties_peek_dict (d->properties); } static void on_si_link_properties_changed (WpSiLink * item, WpImplEndpointLink * self) { populate_properties (self); wp_pw_object_mixin_notify_info (self, PW_ENDPOINT_LINK_CHANGE_MASK_PROPS); } static void on_si_link_features_changed (WpSiLink * item, GParamSpec * param, WpImplEndpointLink * self) { enum pw_endpoint_link_state old_state = self->info.state; guint features = wp_object_get_active_features (WP_OBJECT (item)); if (features & WP_SESSION_ITEM_FEATURE_ACTIVE) self->info.state = PW_ENDPOINT_LINK_STATE_ACTIVE; else self->info.state = PW_ENDPOINT_LINK_STATE_INACTIVE; if (old_state != self->info.state) { wp_pw_object_mixin_notify_info (self, PW_ENDPOINT_LINK_CHANGE_MASK_STATE); g_signal_emit (self, signals[SIGNAL_STATE_CHANGED], 0, old_state, self->info.state, self->info.error); } } static void wp_impl_endpoint_link_constructed (GObject * object) { WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object); g_autoptr (GVariant) info = NULL; g_autoptr (GVariantIter) immutable_props = NULL; const gchar *key, *value; g_autoptr (WpSiPortInfo) si_out = NULL; g_autoptr (WpSiPortInfo) si_in = NULL; self->info.version = PW_VERSION_ENDPOINT_LINK_INFO; self->info.error = NULL; self->info.params = NULL; self->info.n_params = 0; /* get info from the interface */ info = wp_si_link_get_registration_info (self->item); g_variant_get (info, "a{ss}", &immutable_props); /* get the current state */ self->info.state = (wp_object_get_active_features (WP_OBJECT (self->item)) & WP_SESSION_ITEM_FEATURE_ACTIVE) ? PW_ENDPOINT_LINK_STATE_ACTIVE : PW_ENDPOINT_LINK_STATE_INACTIVE; /* associate with the session and the endpoints */ self->info.session_id = wp_session_item_get_associated_proxy_id ( WP_SESSION_ITEM (self->item), WP_TYPE_SESSION); si_out = wp_si_link_get_out_item (self->item); self->info.output_endpoint_id = wp_session_item_get_associated_proxy_id ( WP_SESSION_ITEM (si_out), WP_TYPE_ENDPOINT); si_in = wp_si_link_get_in_item (self->item); self->info.input_endpoint_id = wp_session_item_get_associated_proxy_id ( WP_SESSION_ITEM (si_in), WP_TYPE_ENDPOINT); /* construct export properties (these will come back through the registry and appear in wp_proxy_get_global_properties) */ self->immutable_props = wp_properties_new_empty (); wp_properties_setf (self->immutable_props, PW_KEY_SESSION_ID, "%d", self->info.session_id); wp_properties_setf (self->immutable_props, PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT, "%d", self->info.output_endpoint_id); wp_properties_setf (self->immutable_props, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT, "%d", self->info.input_endpoint_id); /* populate immutable (global) properties */ while (g_variant_iter_next (immutable_props, "{&s&s}", &key, &value)) wp_properties_set (self->immutable_props, key, value); /* populate standard properties */ populate_properties (self); /* subscribe to changes */ g_signal_connect_object (self->item, "link-properties-changed", G_CALLBACK (on_si_link_properties_changed), self, 0); g_signal_connect_object (self->item, "notify::active-features", G_CALLBACK (on_si_link_features_changed), self, 0); wp_object_update_features (WP_OBJECT (self), WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0); G_OBJECT_CLASS (wp_impl_endpoint_link_parent_class)->constructed (object); } static void wp_impl_endpoint_link_dispose (GObject * object) { WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object); g_clear_pointer (&self->immutable_props, wp_properties_unref); g_clear_pointer (&self->info.error, g_free); wp_object_update_features (WP_OBJECT (self), 0, WP_PIPEWIRE_OBJECT_FEATURE_INFO); G_OBJECT_CLASS (wp_impl_endpoint_link_parent_class)->dispose (object); } static void wp_impl_endpoint_link_set_property (GObject * object, guint property_id, const GValue * value, GParamSpec * pspec) { WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object); switch (property_id) { case IMPL_PROP_ITEM: self->item = g_value_get_object (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void wp_impl_endpoint_link_get_property (GObject * object, guint property_id, GValue * value, GParamSpec * pspec) { WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object); switch (property_id) { case IMPL_PROP_ITEM: g_value_set_object (value, self->item); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void wp_impl_endpoint_link_activate_execute_step (WpObject * object, WpFeatureActivationTransition * transition, guint step, WpObjectFeatures missing) { WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object); switch (step) { case WP_PW_OBJECT_MIXIN_STEP_BIND: { g_autoptr (WpCore) core = wp_object_get_core (object); struct pw_core *pw_core = wp_core_get_pw_core (core); /* no pw_core -> we are not connected */ if (!pw_core) { wp_transition_return_error (WP_TRANSITION (transition), g_error_new ( WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, "The WirePlumber core is not connected; " "object cannot be exported to PipeWire")); return; } /* bind */ wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core, PW_TYPE_INTERFACE_EndpointLink, wp_properties_peek_dict (self->immutable_props), &self->iface, 0)); break; } default: WP_OBJECT_CLASS (wp_impl_endpoint_link_parent_class)-> activate_execute_step (object, transition, step, missing); break; } } static void wp_impl_endpoint_link_class_init (WpImplEndpointLinkClass * klass) { GObjectClass *object_class = (GObjectClass *) klass; WpObjectClass *wpobject_class = (WpObjectClass *) klass; WpProxyClass *proxy_class = (WpProxyClass *) klass; object_class->constructed = wp_impl_endpoint_link_constructed; object_class->dispose = wp_impl_endpoint_link_dispose; object_class->set_property = wp_impl_endpoint_link_set_property; object_class->get_property = wp_impl_endpoint_link_get_property; wpobject_class->activate_execute_step = wp_impl_endpoint_link_activate_execute_step; proxy_class->pw_proxy_created = NULL; proxy_class->pw_proxy_destroyed = NULL; g_object_class_install_property (object_class, IMPL_PROP_ITEM, g_param_spec_object ("item", "item", "item", WP_TYPE_SI_LINK, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); } #define pw_endpoint_link_emit(hooks,method,version,...) \ spa_hook_list_call_simple(hooks, struct pw_endpoint_link_events, \ method, version, ##__VA_ARGS__) static void wp_impl_endpoint_link_emit_info (struct spa_hook_list * hooks, gconstpointer info) { pw_endpoint_link_emit (hooks, info, 0, info); } static void wp_impl_endpoint_link_emit_param (struct spa_hook_list * hooks, int seq, guint32 id, guint32 index, guint32 next, const struct spa_pod *param) { pw_endpoint_link_emit (hooks, param, 0, seq, id, index, next, param); } static void wp_endpoint_link_impl_pw_object_mixin_priv_interface_init ( WpPwObjectMixinPrivInterface * iface) { iface->flags |= WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE; iface->enum_params = NULL; iface->set_param = NULL; iface->emit_info = wp_impl_endpoint_link_emit_info; iface->emit_param = wp_impl_endpoint_link_emit_param; } WpImplEndpointLink * wp_impl_endpoint_link_new (WpCore * core, WpSiLink * item) { g_return_val_if_fail (WP_IS_CORE (core), NULL); return g_object_new (WP_TYPE_IMPL_ENDPOINT_LINK, "core", core, "item", item, NULL); }