Skip to content
Snippets Groups Projects
session-item.c 29 KiB
Newer Older
/* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

/**
 * SECTION: WpSessionItem
#include "session-item.h"
#include "wpenums.h"
#include "private/impl-endpoint.h"
struct _WpSiTransition
{
  WpTransition parent;
  guint (*get_next_step) (WpSessionItem * self, WpTransition * transition,
      guint step);
  void (*execute_step) (WpSessionItem * self, WpTransition * transition,
      guint step);
  void (*rollback) (WpSessionItem * self);
};

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);

  if (step != WP_TRANSITION_STEP_ERROR)
    self->execute_step (item, transition, step);
  else if (self->rollback)
    self->rollback (item);
}

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;
  GWeakRef parent;
  union {
    WpProxy *impl_proxy;
    WpImplEndpoint *impl_endpoint;
    WpImplEndpointLink *impl_link;
  };
  GHashTable *impl_streams;
};

enum {
  SIGNAL_FLAGS_CHANGED,
  N_SIGNALS
};

guint32 signals[N_SIGNALS] = {0};

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);
  g_weak_ref_init (&priv->parent, 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_weak_ref_clear (&priv->parent);

  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;
    g_autoptr (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) {
  }
  else if (proxy_type == WP_TYPE_ENDPOINT) {
    if (priv->impl_proxy && WP_IS_ENDPOINT (priv->impl_proxy))
  }
  else if (proxy_type == WP_TYPE_ENDPOINT_LINK) {
    if (priv->impl_proxy && WP_IS_ENDPOINT_LINK (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;
    ret = impl_stream ? g_object_ref (impl_stream) : NULL;
  wp_trace_object (self, "associated %s: " WP_OBJECT_FORMAT,
      g_type_name (proxy_type), WP_OBJECT_ARGS (ret));

  return ret;
wp_session_item_default_activate_get_next_step (WpSessionItem * self,
    WpTransition * transition, guint step)
{
  /* the default implementation just activates instantly,
     without taking any action */
  return WP_TRANSITION_STEP_NONE;
}

enum {
  EXPORT_STEP_ENDPOINT = WP_TRANSITION_STEP_CUSTOM_START,
  EXPORT_STEP_STREAMS,
  EXPORT_STEP_LINK,
  EXPORT_STEP_CONNECT_DESTROYED,
wp_session_item_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:
      return EXPORT_STEP_ENDPOINT;
      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 " WP_OBJECT_FORMAT,
              WP_OBJECT_ARGS (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 activated */
    if (g_hash_table_size (priv->impl_streams) ==
        wp_si_endpoint_get_n_streams (WP_SI_ENDPOINT (self)))
  case EXPORT_STEP_ENDPOINT_FT_STREAMS:
    return WP_TRANSITION_STEP_NONE;

    g_return_val_if_fail (WP_IS_SI_LINK (self), WP_TRANSITION_STEP_ERROR);
    return EXPORT_STEP_CONNECT_DESTROYED;

  case EXPORT_STEP_CONNECT_DESTROYED:
    return WP_TRANSITION_STEP_NONE;

  default:
    return WP_TRANSITION_STEP_ERROR;
  }
}

on_export_proxy_activated (WpObject * 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_object_activate_finish (proxy, res, &error)) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
  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_debug_object (self, "export proxy " WP_OBJECT_FORMAT " activated",
  wp_transition_advance (transition);
static gboolean
on_export_proxy_destroyed_deferred (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);

  g_return_val_if_fail (priv->impl_proxy, G_SOURCE_REMOVE);
  g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->export_rollback,
      G_SOURCE_REMOVE);

  wp_info_object (self, "destroying " WP_OBJECT_FORMAT
      " upon request by the server", WP_OBJECT_ARGS (priv->impl_proxy));

  WP_SESSION_ITEM_GET_CLASS (self)->export_rollback (self);

  priv->flags |= WP_SI_FLAG_EXPORT_ERROR;
  g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);

  return G_SOURCE_REMOVE;
}

static void
on_export_proxy_destroyed (WpObject * proxy, gpointer data)
{
  WpSessionItem *self = WP_SESSION_ITEM (data);
  g_autoptr (WpCore) core = wp_object_get_core (proxy);
  if (core)
    wp_core_idle_add_closure (core, NULL, g_cclosure_new_object (
          G_CALLBACK (on_export_proxy_destroyed_deferred), G_OBJECT (self)));
}

wp_session_item_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_object_get_core (WP_OBJECT (session));
  switch (step) {
  case EXPORT_STEP_ENDPOINT:
    priv->impl_endpoint = wp_impl_endpoint_new (core, WP_SI_ENDPOINT (self));

    wp_object_activate (WP_OBJECT (priv->impl_endpoint),
        WP_PIPEWIRE_OBJECT_FEATURES_ALL, NULL,
        (GAsyncReadyCallback) on_export_proxy_activated,
        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_object_activate (WP_OBJECT (impl_stream),
          WP_OBJECT_FEATURES_ALL, NULL,
          (GAsyncReadyCallback) on_export_proxy_activated,
          transition);

      /* the augment task holds a ref; object will be added to
         priv->impl_streams when activated */
      g_object_unref (impl_stream);
    }
    break;
  case EXPORT_STEP_ENDPOINT_FT_STREAMS:
    /* add feature streams only after the streams are exported, otherwise
       the endpoint will never be activated in the first place (because it
       internally waits for the streams to be ready) */
    wp_object_activate (WP_OBJECT (priv->impl_endpoint),
        (GAsyncReadyCallback) on_export_proxy_activated,
  case EXPORT_STEP_LINK:
    priv->impl_link = wp_impl_endpoint_link_new (core, WP_SI_LINK (self));

    wp_object_activate (WP_OBJECT (priv->impl_link),
        WP_OBJECT_FEATURES_ALL, NULL,
        (GAsyncReadyCallback) on_export_proxy_activated,
  case EXPORT_STEP_CONNECT_DESTROYED:
    g_signal_connect_object (priv->impl_proxy, "pw-proxy-destroyed",
        G_CALLBACK (on_export_proxy_destroyed), self, 0);
    wp_transition_advance (transition);
    break;

wp_session_item_default_export_rollback (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
  if (priv->impl_proxy)
    g_signal_handlers_disconnect_by_data (priv->impl_proxy, self);
  g_clear_pointer (&priv->impl_streams, g_hash_table_unref);
  g_clear_object (&priv->impl_proxy);
  g_weak_ref_set (&priv->session, NULL);
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->activate_get_next_step = wp_session_item_default_activate_get_next_step;
  klass->export_get_next_step = wp_session_item_default_export_get_next_step;
  klass->export_execute_step = wp_session_item_default_export_execute_step;
  klass->export_rollback = wp_session_item_default_export_rollback;
  /**
   * 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_parent:
 * @self: the session item
 *
 * Gets the item's parent, which is the #WpSessionBin this item has been added
 * to, or NULL if the item does not belong to a session bin.
 *
 * Returns: (nullable) (transfer full): the item's parent.
 */
WpSessionItem *
wp_session_item_get_parent (WpSessionItem * self)
{
  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL);

  WpSessionItemPrivate *priv =
      wp_session_item_get_instance_private (self);
  return g_weak_ref_get (&priv->parent);
}

/**
 * wp_session_item_set_parent:
 * @self: the session item
 * @parent: (transfer none): the parent item
 * Private API.
 * Sets the item's parent; used internally by #WpSessionBin.
 */
void
wp_session_item_set_parent (WpSessionItem *self, WpSessionItem *parent)
{
  g_return_if_fail (WP_IS_SESSION_ITEM (self));

  WpSessionItemPrivate *priv =
      wp_session_item_get_instance_private (self);

  g_weak_ref_set (&priv->parent, parent);
}

/**
 * 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);
}

/* clear the in progress flag before calling the callback, so that
   it's possible to call wp_session_item_export from within the callback */
on_activate_transition_pre_completed (gpointer data, GClosure *closure)
  WpTransition *transition = WP_TRANSITION (data);
  WpSessionItem *self = wp_transition_get_source_object (transition);
  WpSessionItemPrivate *priv =
      wp_session_item_get_instance_private (self);

  priv->flags &= ~WP_SI_FLAG_ACTIVATING;
}

static void
on_activate_transition_post_completed (gpointer data, GClosure *closure)
{
  WpTransition *transition = WP_TRANSITION (data);
  WpSessionItem *self = wp_transition_get_source_object (transition);
  WpSessionItemPrivate *priv =
      wp_session_item_get_instance_private (self);

  priv->flags |= wp_transition_had_error (transition) ?
      WP_SI_FLAG_ACTIVATE_ERROR : WP_SI_FLAG_ACTIVE;
  g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);
}

/**
 * wp_session_item_activate_closure:
 * @self: the session item
 * @closure: (transfer full): the closure to use when activation is completed
 * Activates the item asynchronously.
 * You can use wp_session_item_activate_finish() in the closure callback to get
 * the result of this operation.
 * This internally starts a #WpTransition that calls into
 * #WpSessionItemClass.activate_get_next_step() and
 * #WpSessionItemClass.activate_execute_step() to advance.
 * If the transition fails, #WpSessionItemClass.activate_rollback() is called
 * to reverse previous actions.
 *
 * The default implementation of the above virtual functions activates the
 * item successfully without doing anything. In order to implement a meaningful
 * session item, you should override all 3 of them.
 *
 * When this method is called, the %WP_SI_FLAG_ACTIVATING flag is set. When
 * the operation finishes successfully, that flag is cleared and replaced with
 * either %WP_SI_FLAG_ACTIVE or %WP_SI_FLAG_ACTIVATE_ERROR, depending on the
 * success outcome of the operation. In order to clear
 * %WP_SI_FLAG_ACTIVATE_ERROR, you can either call wp_session_item_deactivate()
 * or wp_session_item_activate() to try activating again.
 *
 * This method cannot be called if another operation (activation or export) is
 * in progress (%WP_SI_FLAGS_MASK_OPERATION_IN_PROGRESS) or if the item is
 * already activated.
wp_session_item_activate_closure (WpSessionItem * self, GClosure *closure)
{
  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_FLAGS_MASK_OPERATION_IN_PROGRESS | WP_SI_FLAG_ACTIVE)));
  /* TODO: add a way to cancel the transition if deactivate() is called in the meantime */
  WpTransition *transition = wp_transition_new_closure (
      wp_si_transition_get_type (), self, NULL, closure);
  wp_transition_set_source_tag (transition, wp_session_item_activate);

  g_closure_add_marshal_guards (closure,
      transition, on_activate_transition_pre_completed,
      transition, on_activate_transition_post_completed);
  priv->flags &= ~WP_SI_FLAG_ACTIVATE_ERROR;
  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)->activate_get_next_step;
  WP_SI_TRANSITION (transition)->execute_step =
      WP_SESSION_ITEM_GET_CLASS (self)->activate_execute_step;
  WP_SI_TRANSITION (transition)->rollback =
      WP_SESSION_ITEM_GET_CLASS (self)->activate_rollback;
  wp_transition_advance (transition);
}

/**
 * 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
 *
 * @callback and @callback_data version of wp_session_item_activate_closure()
 */
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_FLAGS_MASK_OPERATION_IN_PROGRESS | WP_SI_FLAG_ACTIVE)));

  GClosure *closure =
      g_cclosure_new (G_CALLBACK (callback), callback_data, NULL);

  wp_session_item_activate_closure (self, closure);
}

/**
 * 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 (WP_IS_SESSION_ITEM (self), FALSE);
  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:
 * @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.
wp_session_item_deactivate (WpSessionItem * self)
{
  g_return_if_fail (WP_IS_SESSION_ITEM (self));

  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
  static const guint flags = 0xf; /* all activation flags */
  if (priv->flags & WP_SI_FLAG_ACTIVE &&
      WP_SESSION_ITEM_GET_CLASS (self)->activate_rollback)
    WP_SESSION_ITEM_GET_CLASS (self)->activate_rollback (self);

  if (priv->flags & flags) {
    priv->flags &= ~flags;
    g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);
  }
on_export_transition_pre_completed (gpointer data, GClosure *closure)
  WpTransition *transition = WP_TRANSITION (data);
  WpSessionItem *self = wp_transition_get_source_object (transition);
  WpSessionItemPrivate *priv =
      wp_session_item_get_instance_private (self);

  priv->flags &= ~WP_SI_FLAG_EXPORTING;
}

static void
on_export_transition_post_completed (gpointer data, GClosure *closure)
{
  WpTransition *transition = WP_TRANSITION (data);
  WpSessionItem *self = wp_transition_get_source_object (transition);
  WpSessionItemPrivate *priv =
      wp_session_item_get_instance_private (self);

  priv->flags |= wp_transition_had_error (transition) ?
      WP_SI_FLAG_EXPORT_ERROR : WP_SI_FLAG_EXPORTED;
  g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);
}

 * @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. You can use wp_session_item_export_finish() in the
 * @callback to get the result of this operation.
 *
 * This internally starts a #WpTransition that calls into
 * #WpSessionItemClass.export_get_next_step() and
 * #WpSessionItemClass.export_execute_step() to advance.
 * If the transition fails, #WpSessionItemClass.export_rollback() is called
 * to reverse previous actions.
 *
 * Exporting is internally implemented 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. You can extend this to export custom interfaces by
 * overriding the virtual functions mentioned above.
 *
 * When this method is called, the %WP_SI_FLAG_EXPORTING flag is set. When
 * the operation finishes successfully, that flag is cleared and replaced with
 * either %WP_SI_FLAG_EXPORTED or %WP_SI_FLAG_EXPORT_ERROR, depending on the
 * success outcome of the operation. In order to clear
 * %WP_SI_FLAG_EXPORT_ERROR, you can either call wp_session_item_unexport()
 * or wp_session_item_export() to try exporting again.
 * This method cannot be called if another operation (activation or export) is
 * in progress (%WP_SI_FLAGS_MASK_OPERATION_IN_PROGRESS) or if the item is
 * already exported.
 */
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));

  WpSessionItemPrivate *priv =
      wp_session_item_get_instance_private (self);

  g_return_if_fail (!(priv->flags &
      (WP_SI_FLAGS_MASK_OPERATION_IN_PROGRESS | WP_SI_FLAG_EXPORTED)));
  g_weak_ref_set (&priv->session, session);

  GClosure *closure =
      g_cclosure_new (G_CALLBACK (callback), callback_data, NULL);

  /* TODO: add a way to cancel the transition if unexport() is called in the meantime */
  WpTransition *transition = wp_transition_new_closure (
      wp_si_transition_get_type (), self, NULL, closure);
  wp_transition_set_source_tag (transition, wp_session_item_export);

  g_closure_add_marshal_guards (closure,
      transition, on_export_transition_pre_completed,
      transition, on_export_transition_post_completed);
  wp_debug_object (self, "exporting item on session " WP_OBJECT_FORMAT,
      WP_OBJECT_ARGS (session));

  priv->flags &= ~WP_SI_FLAG_EXPORT_ERROR;
  priv->flags |= WP_SI_FLAG_EXPORTING;
  g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);

  WP_SI_TRANSITION (transition)->get_next_step =
      WP_SESSION_ITEM_GET_CLASS (self)->export_get_next_step;
  WP_SI_TRANSITION (transition)->execute_step =
      WP_SESSION_ITEM_GET_CLASS (self)->export_execute_step;
  WP_SI_TRANSITION (transition)->rollback =
      WP_SESSION_ITEM_GET_CLASS (self)->export_rollback;
  wp_transition_advance (transition);
 * @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 (
      g_async_result_is_tagged (res, wp_session_item_export), FALSE);
  return wp_transition_finish (res, error);
 * @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));

  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
  static const guint flags = 0xf0; /* all export flags */
  if (priv->flags & WP_SI_FLAG_EXPORTED &&
      WP_SESSION_ITEM_GET_CLASS (self)->export_rollback)
    WP_SESSION_ITEM_GET_CLASS (self)->export_rollback (self);

  if (priv->flags & flags) {
    priv->flags &= ~flags;
    g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);
  }