/* WirePlumber * * Copyright © 2020 Collabora Ltd. * @author George Kiagiadakis * * SPDX-License-Identifier: MIT */ /** * SECTION: WpSessionItem * @title: Session Items */ #define G_LOG_DOMAIN "wp-si" #include "session-item.h" #include "private.h" #include "error.h" #include "wpenums.h" struct _WpSiTransition { WpTransition parent; guint (*get_next_step) (WpSessionItem * self, WpTransition * transition, guint step); void (*execute_step) (WpSessionItem * self, WpTransition * transition, guint step); }; G_DECLARE_FINAL_TYPE (WpSiTransition, wp_si_transition, WP, SI_TRANSITION, WpTransition); G_DEFINE_TYPE (WpSiTransition, wp_si_transition, WP_TYPE_TRANSITION) static void wp_si_transition_init (WpSiTransition * transition) {} static guint wp_si_transition_get_next_step (WpTransition * transition, guint step) { WpSiTransition *self = WP_SI_TRANSITION (transition); WpSessionItem *item = wp_transition_get_source_object (transition); g_return_val_if_fail (self->get_next_step, WP_TRANSITION_STEP_ERROR); step = self->get_next_step (item, transition, step); g_return_val_if_fail (step == WP_TRANSITION_STEP_NONE || self->execute_step, WP_TRANSITION_STEP_ERROR); return step; } static void wp_si_transition_execute_step (WpTransition * transition, guint step) { WpSiTransition *self = WP_SI_TRANSITION (transition); WpSessionItem *item = wp_transition_get_source_object (transition); self->execute_step (item, transition, step); } static void wp_si_transition_class_init (WpSiTransitionClass * klass) { WpTransitionClass *transition_class = (WpTransitionClass *) klass; transition_class->get_next_step = wp_si_transition_get_next_step; transition_class->execute_step = wp_si_transition_execute_step; } typedef struct _WpSessionItemPrivate WpSessionItemPrivate; struct _WpSessionItemPrivate { GWeakRef session; guint32 flags; union { WpProxy *impl_proxy; WpImplEndpoint *impl_endpoint; WpImplEndpointLink *impl_link; }; GHashTable *impl_streams; }; enum { SIGNAL_FLAGS_CHANGED, N_SIGNALS }; guint32 signals[N_SIGNALS] = {0}; /** * WpSessionItem: */ G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpSessionItem, wp_session_item, G_TYPE_OBJECT) static void wp_session_item_init (WpSessionItem * self) { WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); g_weak_ref_init (&priv->session, NULL); } static void wp_session_item_dispose (GObject * object) { WpSessionItem * self = WP_SESSION_ITEM (object); wp_session_item_reset (self); G_OBJECT_CLASS (wp_session_item_parent_class)->dispose (object); } static void wp_session_item_finalize (GObject * object) { WpSessionItem * self = WP_SESSION_ITEM (object); WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); g_weak_ref_clear (&priv->session); G_OBJECT_CLASS (wp_session_item_parent_class)->finalize (object); } static void wp_session_item_default_reset (WpSessionItem * self) { wp_session_item_unexport (self); wp_session_item_deactivate (self); } static gpointer wp_session_item_default_get_associated_proxy (WpSessionItem * self, GType proxy_type) { WpSessionItemPrivate *priv; if (WP_IS_SI_STREAM (self)) { WpSiEndpoint *ep = wp_si_stream_get_parent_endpoint (WP_SI_STREAM (self)); priv = wp_session_item_get_instance_private (WP_SESSION_ITEM (ep)); } else { priv = wp_session_item_get_instance_private (self); } if (proxy_type == WP_TYPE_SESSION) { return g_weak_ref_get (&priv->session); } else if (proxy_type == WP_TYPE_ENDPOINT) { if (priv->impl_proxy && WP_IS_ENDPOINT (priv->impl_proxy)) return g_object_ref (priv->impl_proxy); } else if (proxy_type == WP_TYPE_ENDPOINT_LINK) { if (priv->impl_proxy && WP_IS_ENDPOINT_LINK (priv->impl_proxy)) return g_object_ref (priv->impl_proxy); } else if (proxy_type == WP_TYPE_ENDPOINT_STREAM) { gpointer impl_stream = priv->impl_streams ? g_hash_table_lookup (priv->impl_streams, self) : NULL; return impl_stream ? g_object_ref (impl_stream) : NULL; } return NULL; } static guint wp_session_item_default_get_next_step (WpSessionItem * self, WpTransition * transition, guint step) { /* the default implementation just activates instantly, without taking any action */ return WP_TRANSITION_STEP_NONE; } static void wp_session_item_default_execute_step (WpSessionItem * self, WpTransition * transition, guint step) { switch (step) { case WP_TRANSITION_STEP_NONE: case WP_TRANSITION_STEP_ERROR: break; default: g_return_if_reached (); } } static void wp_session_item_default_deactivate (WpSessionItem * self) { WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); static const guint flags = (WP_SI_FLAG_ACTIVATING | WP_SI_FLAG_ACTIVE | WP_SI_FLAG_IN_ERROR); if (priv->flags & flags) { priv->flags &= ~flags; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); } } enum { EXPORT_STEP_ENDPOINT = WP_TRANSITION_STEP_CUSTOM_START, EXPORT_STEP_STREAMS, EXPORT_STEP_LINK, EXPORT_STEP_FINISH, }; static guint default_export_get_next_step (WpSessionItem * self, WpTransition * transition, guint step) { WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); switch (step) { case WP_TRANSITION_STEP_NONE: if (WP_IS_SI_ENDPOINT (self)) { priv->flags |= WP_SI_FLAG_EXPORTING; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); return EXPORT_STEP_ENDPOINT; } else if (WP_IS_SI_LINK (self)) { priv->flags |= WP_SI_FLAG_EXPORTING; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); return EXPORT_STEP_LINK; } else { wp_transition_return_error (transition, g_error_new ( WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT, "Cannot export WpSessionItem of unknown type (%s:%p)", G_OBJECT_TYPE_NAME (self), self)); return WP_TRANSITION_STEP_ERROR; } case EXPORT_STEP_ENDPOINT: g_return_val_if_fail (WP_IS_SI_ENDPOINT (self), WP_TRANSITION_STEP_ERROR); return EXPORT_STEP_STREAMS; case EXPORT_STEP_STREAMS: g_return_val_if_fail (WP_IS_SI_ENDPOINT (self), WP_TRANSITION_STEP_ERROR); g_return_val_if_fail (priv->impl_streams, WP_TRANSITION_STEP_ERROR); /* go to next step only when all impl proxies are augmented */ if (g_hash_table_size (priv->impl_streams) == wp_si_endpoint_get_n_streams (WP_SI_ENDPOINT (self))) return EXPORT_STEP_FINISH; else return step; case EXPORT_STEP_LINK: g_return_val_if_fail (WP_IS_SI_LINK (self), WP_TRANSITION_STEP_ERROR); return EXPORT_STEP_FINISH; case EXPORT_STEP_FINISH: return WP_TRANSITION_STEP_NONE; default: return WP_TRANSITION_STEP_ERROR; } } static void on_export_proxy_augmented (WpProxy * proxy, GAsyncResult * res, gpointer data) { WpTransition *transition = WP_TRANSITION (data); WpSessionItem *self = wp_transition_get_source_object (transition); WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); g_autoptr (GError) error = NULL; if (!wp_proxy_augment_finish (proxy, res, &error)) { wp_transition_return_error (transition, g_steal_pointer (&error)); return; } if (WP_IS_IMPL_ENDPOINT_STREAM (proxy)) { g_autoptr (WpSiStream) si_stream = NULL; g_object_get (proxy, "item", &si_stream, NULL); g_return_if_fail (si_stream != NULL); g_hash_table_insert (priv->impl_streams, si_stream, g_object_ref (proxy)); } wp_transition_advance (transition); } static void default_export_execute_step (WpSessionItem * self, WpTransition * transition, guint step) { WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); g_autoptr (WpSession) session = g_weak_ref_get (&priv->session); g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (session)); switch (step) { case EXPORT_STEP_ENDPOINT: priv->impl_endpoint = wp_impl_endpoint_new (core, WP_SI_ENDPOINT (self)); wp_proxy_augment (WP_PROXY (priv->impl_endpoint), WP_ENDPOINT_FEATURES_STANDARD, NULL, (GAsyncReadyCallback) on_export_proxy_augmented, transition); break; case EXPORT_STEP_STREAMS: { guint i, n_streams; priv->impl_streams = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref); n_streams = wp_si_endpoint_get_n_streams (WP_SI_ENDPOINT (self)); for (i = 0; i < n_streams; i++) { WpSiStream *stream = wp_si_endpoint_get_stream (WP_SI_ENDPOINT (self), i); WpImplEndpointStream *impl_stream = wp_impl_endpoint_stream_new (core, stream); wp_proxy_augment (WP_PROXY (impl_stream), WP_PROXY_FEATURES_STANDARD, NULL, (GAsyncReadyCallback) on_export_proxy_augmented, transition); /* the augment task holds a ref; object will be added to priv->impl_streams when augmented */ g_object_unref (impl_stream); } break; } case EXPORT_STEP_LINK: priv->impl_link = wp_impl_endpoint_link_new (core, WP_SI_LINK (self)); wp_proxy_augment (WP_PROXY (priv->impl_link), WP_PROXY_FEATURES_STANDARD, NULL, (GAsyncReadyCallback) on_export_proxy_augmented, transition); break; case EXPORT_STEP_FINISH: priv->flags &= ~WP_SI_FLAG_EXPORTING; priv->flags |= WP_SI_FLAG_EXPORTED; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); wp_transition_advance (transition); break; case WP_TRANSITION_STEP_ERROR: g_clear_pointer (&priv->impl_streams, g_hash_table_unref); g_clear_object (&priv->impl_proxy); g_weak_ref_set (&priv->session, NULL); if (priv->flags & WP_SI_FLAG_EXPORTING) { priv->flags &= ~WP_SI_FLAG_EXPORTING; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); } break; } } static void wp_session_item_default_export (WpSessionItem * self, WpSession * session, GCancellable * cancellable, GAsyncReadyCallback callback, gpointer callback_data) { WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); WpTransition *transition; g_weak_ref_set (&priv->session, session); transition = wp_transition_new (wp_si_transition_get_type (), self, cancellable, callback, callback_data); wp_transition_set_source_tag (transition, wp_session_item_default_export); WP_SI_TRANSITION (transition)->get_next_step = default_export_get_next_step; WP_SI_TRANSITION (transition)->execute_step = default_export_execute_step; wp_transition_advance (transition); } static gboolean wp_session_item_default_export_finish (WpSessionItem * self, GAsyncResult * res, GError ** error) { g_return_val_if_fail ( g_async_result_is_tagged (res, wp_session_item_default_export), FALSE); return wp_transition_finish (res, error); } static void wp_session_item_default_unexport (WpSessionItem * self) { WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); static const guint flags = (WP_SI_FLAG_EXPORTING | WP_SI_FLAG_EXPORTED); g_clear_pointer (&priv->impl_streams, g_hash_table_unref); g_clear_object (&priv->impl_proxy); g_weak_ref_set (&priv->session, NULL); if (priv->flags & flags) { priv->flags &= ~flags; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); } } static void wp_session_item_class_init (WpSessionItemClass * klass) { GObjectClass *object_class = (GObjectClass *) klass; object_class->dispose = wp_session_item_dispose; object_class->finalize = wp_session_item_finalize; klass->reset = wp_session_item_default_reset; klass->get_associated_proxy = wp_session_item_default_get_associated_proxy; klass->get_next_step = wp_session_item_default_get_next_step; klass->execute_step = wp_session_item_default_execute_step; klass->deactivate = wp_session_item_default_deactivate; klass->export = wp_session_item_default_export; klass->export_finish = wp_session_item_default_export_finish; klass->unexport = wp_session_item_default_unexport; /** * WpSessionItem::flags-changed: * @self: the session item * @flags: the current flags */ signals[SIGNAL_FLAGS_CHANGED] = g_signal_new ( "flags-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, WP_TYPE_SI_FLAGS); } /** * wp_session_item_reset: (virtual reset) * @self: the session item * * Resets the state of the item, deactivating it, unexporting it and * resetting configuration options as well. */ void wp_session_item_reset (WpSessionItem * self) { g_return_if_fail (WP_IS_SESSION_ITEM (self)); g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->reset); WP_SESSION_ITEM_GET_CLASS (self)->reset (self); } /** * wp_session_item_get_flags: * @self: the session item * * Returns: the item's flags */ WpSiFlags wp_session_item_get_flags (WpSessionItem * self) { g_return_val_if_fail (WP_IS_SESSION_ITEM (self), 0); WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); return priv->flags; } /** * wp_session_item_set_flag: * @self: the session item * @flag: the flag to set * * Sets the specified @flag on this item. * * Note that bits 1-8 cannot be set using this function, they can only * be changed internally. */ void wp_session_item_set_flag (WpSessionItem * self, WpSiFlags flag) { g_return_if_fail (WP_IS_SESSION_ITEM (self)); WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); /* mask to make sure we are not changing an immutable flag */ flag &= ~((1<<8) - 1); if (flag != 0) { priv->flags |= flag; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); } } /** * wp_session_item_clear_flag: * @self: the session item * @flag: the flag to clear * * Clears the specified @flag from this item. * * Note that bits 1-8 cannot be cleared using this function, they can only * be changed internally. */ void wp_session_item_clear_flag (WpSessionItem * self, WpSiFlags flag) { g_return_if_fail (WP_IS_SESSION_ITEM (self)); WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); /* mask to make sure we are not changing an immutable flag */ flag &= ~((1<<8) - 1); if (flag != 0) { priv->flags &= ~flag; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); } } /** * wp_session_item_get_associated_proxy: (virtual get_associated_proxy) * @self: the session item * @proxy_type: a #WpProxy subclass #GType * * An associated proxy is a #WpProxy subclass instance that is somehow related * to this item. For example: * - An exported #WpSiEndpoint should have at least: * - an associated #WpEndpoint * - an associated #WpSession * - An exported #WpSiStream should have at least: * - an associated #WpEndpointStream * - an associated #WpEndpoint * - In cases where the item wraps a single PipeWire node, it should also * have an associated #WpNode * * Returns: (nullable) (transfer full) (type WpProxy): the associated proxy * of the specified @proxy_type, or %NULL if there is no association to * such a proxy */ gpointer wp_session_item_get_associated_proxy (WpSessionItem * self, GType proxy_type) { g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL); g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->get_associated_proxy, NULL); g_return_val_if_fail (g_type_is_a (proxy_type, WP_TYPE_PROXY), NULL); return WP_SESSION_ITEM_GET_CLASS (self)->get_associated_proxy (self, proxy_type); } /** * wp_session_item_get_associated_proxy_id: * @self: the session item * @proxy_type: a #WpProxy subclass #GType * * Returns: the bound id of the associated proxy of the specified @proxy_type, * or `SPA_ID_INVALID` if there is no association to such a proxy */ guint32 wp_session_item_get_associated_proxy_id (WpSessionItem * self, GType proxy_type) { g_autoptr (WpProxy) proxy = wp_session_item_get_associated_proxy (self, proxy_type); if (!proxy) return SPA_ID_INVALID; return wp_proxy_get_bound_id (proxy); } /** * wp_session_item_configure: (virtual configure) * @self: the session item * @args: (transfer floating): the configuration options to set * (`a{sv}` dictionary, mapping option names to values) * * Returns: %TRUE on success, %FALSE if the options could not be set */ gboolean wp_session_item_configure (WpSessionItem * self, GVariant * args) { g_autoptr (GVariant) args_ref = g_variant_ref_sink (args); g_return_val_if_fail (WP_IS_SESSION_ITEM (self), FALSE); g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->configure, FALSE); g_return_val_if_fail (g_variant_is_of_type (args, G_VARIANT_TYPE_VARDICT), FALSE); return WP_SESSION_ITEM_GET_CLASS (self)->configure (self, args); } /** * wp_session_item_get_configuration: (virtual get_configuration) * @self: the session item * * Returns: (transfer floating): the active configuration, as a `a{sv}` dictionary */ GVariant * wp_session_item_get_configuration (WpSessionItem * self) { g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL); g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->get_configuration, NULL); return WP_SESSION_ITEM_GET_CLASS (self)->get_configuration (self); } static void on_transition_completed (WpTransition * transition, GParamSpec * pspec, WpSessionItem * self) { WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); if (wp_transition_had_error (transition)) priv->flags |= WP_SI_FLAG_IN_ERROR; else priv->flags |= WP_SI_FLAG_ACTIVE; priv->flags &= ~WP_SI_FLAG_ACTIVATING; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); } /** * wp_session_item_activate: * @self: the session item * @callback: (scope async): a callback to call when activation is finished * @callback_data: (closure): data passed to @callback * * Activates the item asynchronously. This internally starts a #WpTransition * that calls into #WpSessionItemClass.get_next_step() and * #WpSessionItemClass.execute_step() to advance. * * You can use wp_session_item_activate_finish() in the @callback to figure out * the result of this operation. * * Normally this function is called internally by the session; there is no need * to activate an item externally, except for unit testing purposes. */ void wp_session_item_activate (WpSessionItem * self, GAsyncReadyCallback callback, gpointer callback_data) { g_return_if_fail (WP_IS_SESSION_ITEM (self)); WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); g_return_if_fail (!(priv->flags & (WP_SI_FLAG_ACTIVATING | WP_SI_FLAG_ACTIVE))); /* TODO: add a way to cancel the transition if reset() is called in the meantime */ WpTransition *transition = wp_transition_new (wp_si_transition_get_type (), self, NULL, callback, callback_data); wp_transition_set_source_tag (transition, wp_session_item_activate); g_signal_connect (transition, "notify::completed", (GCallback) on_transition_completed, self); priv->flags |= WP_SI_FLAG_ACTIVATING; g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags); WP_SI_TRANSITION (transition)->get_next_step = WP_SESSION_ITEM_GET_CLASS (self)->get_next_step; WP_SI_TRANSITION (transition)->execute_step = WP_SESSION_ITEM_GET_CLASS (self)->execute_step; wp_transition_advance (transition); } /** * wp_session_item_activate_finish: * @self: the session item * @res: the async operation result * @error: (out) (optional): the error of the operation, if any * * Returns: %TRUE if the item is now activateed, %FALSE if there was an error */ gboolean wp_session_item_activate_finish (WpSessionItem * self, GAsyncResult * res, GError ** error) { g_return_val_if_fail ( g_async_result_is_tagged (res, wp_session_item_activate), FALSE); return wp_transition_finish (res, error); } /** * wp_session_item_deactivate: (virtual deactivate) * @self: the session item * * De-activates the item and/or cancels any ongoing activation operation. * * If the item was not activated, this method does nothing. */ void wp_session_item_deactivate (WpSessionItem * self) { g_return_if_fail (WP_IS_SESSION_ITEM (self)); g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->deactivate); //TODO cancel job if ACTIVATING WP_SESSION_ITEM_GET_CLASS (self)->deactivate (self); } /** * wp_session_item_export: (virtual export) * @self: the session item * @session: the session on which to export this item * @callback: (scope async): a callback to call when exporting is finished * @callback_data: (closure): data passed to @callback * * Exports this item asynchronously on PipeWire, making it part of the * specified @session. * * Exporting only makes sense for endpoints (items that implement #WpSiEndpoint) * and endpoint links (items that implement #WpSiLink). On other items the * default implementation will immediately call the @callback, reporting error. */ void wp_session_item_export (WpSessionItem * self, WpSession * session, GAsyncReadyCallback callback, gpointer callback_data) { g_return_if_fail (WP_IS_SESSION_ITEM (self)); g_return_if_fail (WP_IS_SESSION (session)); g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->export); WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self); g_return_if_fail (!(priv->flags & (WP_SI_FLAG_EXPORTING | WP_SI_FLAG_EXPORTED))); WP_SESSION_ITEM_GET_CLASS (self)->export (self, session, NULL, callback, callback_data); } /** * wp_session_item_export_finish: (virtual export_finish) * @self: the session item * @res: the async operation result * @error: (out) (optional): the error of the operation, if any * * Returns: %TRUE if the item is now exported, %FALSE if there was an error */ gboolean wp_session_item_export_finish (WpSessionItem * self, GAsyncResult * res, GError ** error) { g_return_val_if_fail (WP_IS_SESSION_ITEM (self), FALSE); g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->export_finish, FALSE); return WP_SESSION_ITEM_GET_CLASS (self)->export_finish (self, res, error); } /** * wp_session_item_unexport: (virtual unexport) * @self: the session item * * Reverses the effects of a previous call to wp_session_item_export(). * This means that after this method is called: * - The item is no longer exported on PipeWire * - The item is no longer associated with a session * - If an export operation was in progress, it is cancelled. * * If the item was not exported, this method does nothing. */ void wp_session_item_unexport (WpSessionItem * self) { g_return_if_fail (WP_IS_SESSION_ITEM (self)); g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->unexport); //TODO cancel job if EXPORTING WP_SESSION_ITEM_GET_CLASS (self)->unexport (self); }