From 405e8ba0d5765c9201527cfa87f928d6fc2c9749 Mon Sep 17 00:00:00 2001
From: George Kiagiadakis <george.kiagiadakis@collabora.com>
Date: Fri, 20 Mar 2020 14:45:17 +0200
Subject: [PATCH] session-item / endpoint: implement exporting a WpSiEndpoint

* introduces API to export session items
* introduces small changes in the WpSiEndpoint & WpSiStream
  interfaces to make it nicer to work with
* ports WpImplEndpoint to use PW_TYPE_INTERFACE_Endpoint
  to export. Depends on:
  https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/246
  (was merged after 0.3.2)
---
 lib/wp/endpoint.c           | 640 +++++++++++++++++-------------------
 lib/wp/endpoint.h           |  42 ---
 lib/wp/private.h            |  14 +
 lib/wp/session-item.c       | 158 ++++++++-
 lib/wp/session-item.h       |  36 +-
 lib/wp/si-interfaces.c      |  86 ++---
 lib/wp/si-interfaces.h      |  23 +-
 lib/wp/spa-props.c          |  36 +-
 modules/module-si-adapter.c |  63 ++--
 tests/wp/endpoint.c         | 230 ++++++++++---
 10 files changed, 777 insertions(+), 551 deletions(-)

diff --git a/lib/wp/endpoint.c b/lib/wp/endpoint.c
index 8dfa99bf..7722cc4f 100644
--- a/lib/wp/endpoint.c
+++ b/lib/wp/endpoint.c
@@ -22,14 +22,18 @@
  */
 
 #include "endpoint.h"
+#include "session.h"
 #include "private.h"
 #include "error.h"
 #include "wpenums.h"
 
 #include <pipewire/pipewire.h>
 #include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
 #include <spa/pod/builder.h>
 #include <spa/pod/parser.h>
+#include <spa/pod/filter.h>
 
 enum {
   SIGNAL_CONTROL_CHANGED,
@@ -38,64 +42,6 @@ enum {
 
 static guint32 signals[N_SIGNALS] = {0};
 
-/* helpers */
-
-static struct pw_endpoint_info *
-endpoint_info_update (struct pw_endpoint_info *info,
-    WpProperties ** props_storage,
-    const struct pw_endpoint_info *update)
-{
-  if (update == NULL)
-    return info;
-
-  if (info == NULL) {
-    info = calloc(1, sizeof(struct pw_endpoint_info));
-    if (info == NULL)
-      return NULL;
-
-    info->id = update->id;
-    info->name = g_strdup(update->name);
-    info->media_class = g_strdup(update->media_class);
-    info->direction = update->direction;
-    info->flags = update->flags;
-  }
-  info->change_mask = update->change_mask;
-
-  if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_STREAMS)
-    info->n_streams = update->n_streams;
-
-  if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_SESSION)
-    info->session_id = update->session_id;
-
-  if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) {
-    if (*props_storage)
-      wp_properties_unref (*props_storage);
-    *props_storage = wp_properties_new_copy_dict (update->props);
-    info->props = (struct spa_dict *) wp_properties_peek_dict (*props_storage);
-  }
-  if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
-    info->n_params = update->n_params;
-    free((void *) info->params);
-    if (update->params) {
-      size_t size = info->n_params * sizeof(struct spa_param_info);
-      info->params = malloc(size);
-      memcpy(info->params, update->params, size);
-    }
-    else
-      info->params = NULL;
-  }
-  return info;
-}
-
-static void
-endpoint_info_free (struct pw_endpoint_info *info)
-{
-  g_free(info->name);
-  g_free(info->media_class);
-  free((void *) info->params);
-  free(info);
-}
-
 /* WpEndpoint */
 
 typedef struct _WpEndpointPrivate WpEndpointPrivate;
@@ -104,6 +50,7 @@ struct _WpEndpointPrivate
   WpProperties *properties;
   WpSpaProps spa_props;
   struct pw_endpoint_info *info;
+  struct pw_endpoint *iface;
   struct spa_hook listener;
 };
 
@@ -120,8 +67,8 @@ wp_endpoint_finalize (GObject * object)
   WpEndpoint *self = WP_ENDPOINT (object);
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
 
-  g_clear_pointer (&priv->info, endpoint_info_free);
   g_clear_pointer (&priv->properties, wp_properties_unref);
+  g_clear_pointer (&priv->info, pw_endpoint_info_free);
   wp_spa_props_clear (&priv->spa_props);
 
   G_OBJECT_CLASS (wp_endpoint_parent_class)->finalize (object);
@@ -168,12 +115,12 @@ static gint
 wp_endpoint_enum_params (WpProxy * self, guint32 id, guint32 start,
     guint32 num, const struct spa_pod *filter)
 {
-  struct pw_endpoint *pwp;
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
   int endpoint_enum_params_result;
 
-  pwp = (struct pw_endpoint *) wp_proxy_get_pw_proxy (self);
-  endpoint_enum_params_result = pw_endpoint_enum_params (pwp, 0, id, start, num,
-      filter);
+  endpoint_enum_params_result = pw_endpoint_enum_params (priv->iface, 0, id,
+      start, num, filter);
   g_warn_if_fail (endpoint_enum_params_result >= 0);
 
   return endpoint_enum_params_result;
@@ -182,12 +129,12 @@ wp_endpoint_enum_params (WpProxy * self, guint32 id, guint32 start,
 static gint
 wp_endpoint_subscribe_params (WpProxy * self, guint32 n_ids, guint32 *ids)
 {
-  struct pw_endpoint *pwp;
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
   int endpoint_subscribe_params_result;
 
-  pwp = (struct pw_endpoint *) wp_proxy_get_pw_proxy (self);
-  endpoint_subscribe_params_result = pw_endpoint_subscribe_params (pwp, ids,
-      n_ids);
+  endpoint_subscribe_params_result = pw_endpoint_subscribe_params (priv->iface,
+      ids, n_ids);
   g_warn_if_fail (endpoint_subscribe_params_result >= 0);
 
   return endpoint_subscribe_params_result;
@@ -197,11 +144,12 @@ static gint
 wp_endpoint_set_param (WpProxy * self, guint32 id, guint32 flags,
     const struct spa_pod *param)
 {
-  struct pw_endpoint *pwp;
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
   int endpoint_set_param_result;
 
-  pwp = (struct pw_endpoint *) wp_proxy_get_pw_proxy (self);
-  endpoint_set_param_result = pw_endpoint_set_param (pwp, id, flags, param);
+  endpoint_set_param_result = pw_endpoint_set_param (priv->iface, id, flags,
+      param);
   g_warn_if_fail (endpoint_set_param_result >= 0);
 
   return endpoint_set_param_result;
@@ -213,9 +161,14 @@ endpoint_event_info (void *data, const struct pw_endpoint_info *info)
   WpEndpoint *self = WP_ENDPOINT (data);
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
 
-  priv->info = endpoint_info_update (priv->info, &priv->properties, info);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
+  priv->info = pw_endpoint_info_update (priv->info, info);
+
+  if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) {
+    g_clear_pointer (&priv->properties, wp_properties_unref);
+    priv->properties = wp_properties_new_wrap_dict (priv->info->props);
+  }
 
+  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
   g_object_notify (G_OBJECT (self), "info");
 
   if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS)
@@ -234,8 +187,9 @@ wp_endpoint_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
   WpEndpoint *self = WP_ENDPOINT (proxy);
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
 
-  pw_endpoint_add_listener ((struct pw_endpoint *) pw_proxy,
-      &priv->listener, &endpoint_events, self);
+  priv->iface = (struct pw_endpoint *) pw_proxy;
+  pw_endpoint_add_listener (priv->iface, &priv->listener, &endpoint_events,
+      self);
 }
 
 static void
@@ -300,17 +254,10 @@ set_control (WpEndpoint * self, guint32 control_id,
 {
   char buf[1024];
   struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof (buf));
-  struct pw_endpoint *pw_proxy = NULL;
-
-  /* set the default endpoint id as a property param on the endpoint;
-     our spa_props will be updated by the param event */
 
-  pw_proxy = (struct pw_endpoint *) wp_proxy_get_pw_proxy (WP_PROXY (self));
-  if (!pw_proxy)
-    return FALSE;
+  /* our spa_props will be updated by the param event */
 
-  pw_endpoint_set_param (pw_proxy,
-      SPA_PARAM_Props, 0,
+  WP_PROXY_GET_CLASS (self)->set_param (WP_PROXY (self), SPA_PARAM_Props, 0,
       spa_pod_builder_add_object (&b,
           SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
           control_id, SPA_POD_Pod (pod)));
@@ -546,358 +493,372 @@ wp_endpoint_set_control_float (WpEndpoint * self, guint32 control_id,
 
 /* WpImplEndpoint */
 
-typedef struct _WpImplEndpointPrivate WpImplEndpointPrivate;
-struct _WpImplEndpointPrivate
-{
-  WpEndpointPrivate *pp;
-  struct pw_endpoint_info info;
-  struct spa_param_info param_info[2];
+enum {
+  IMPL_PROP_0,
+  IMPL_PROP_ITEM,
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpImplEndpoint, wp_impl_endpoint, WP_TYPE_ENDPOINT)
-
-static void
-wp_impl_endpoint_init (WpImplEndpoint * self)
+struct _WpImplEndpoint
 {
-  WpImplEndpointPrivate *priv = wp_impl_endpoint_get_instance_private (self);
+  WpEndpoint parent;
 
-  /* store a pointer to the parent's private; we use that structure
-    as well to optimize memory usage and to be able to re-use some of the
-    parent's methods without reimplementing them */
-  priv->pp = wp_endpoint_get_instance_private (WP_ENDPOINT (self));
+  struct spa_interface iface;
+  struct spa_hook_list hooks;
+  struct pw_endpoint_info info;
+  gboolean subscribed;
 
-  priv->pp->properties = wp_properties_new_empty ();
+  WpSiEndpoint *item;
+};
 
-  priv->param_info[0] = SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
-  priv->param_info[1] = SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+G_DEFINE_TYPE (WpImplEndpoint, wp_impl_endpoint, WP_TYPE_ENDPOINT)
 
-  priv->info.version = PW_VERSION_ENDPOINT_INFO;
-  priv->info.props =
-      (struct spa_dict *) wp_properties_peek_dict (priv->pp->properties);
-  priv->info.params = priv->param_info;
-  priv->info.n_params = SPA_N_ELEMENTS (priv->param_info);
-  priv->pp->info = &priv->info;
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_ENDPOINT_FEATURE_CONTROLS);
-}
+#define pw_endpoint_emit(hooks,method,version,...) \
+    spa_hook_list_call_simple(hooks, struct pw_endpoint_events, \
+        method, version, ##__VA_ARGS__)
 
-static void
-wp_impl_endpoint_finalize (GObject * object)
-{
-  WpImplEndpointPrivate *priv =
-      wp_impl_endpoint_get_instance_private (WP_IMPL_ENDPOINT (object));
+#define pw_endpoint_emit_info(hooks,...)  pw_endpoint_emit(hooks, info, 0, ##__VA_ARGS__)
+#define pw_endpoint_emit_param(hooks,...) pw_endpoint_emit(hooks, param, 0, ##__VA_ARGS__)
 
-  /* set to NULL to prevent parent's finalize from calling free() on it */
-  priv->pp->info = NULL;
-  g_free (priv->info.name);
-  g_free (priv->info.media_class);
+static int
+impl_add_listener(void *object,
+    struct spa_hook *listener,
+    const struct pw_endpoint_events *events,
+    void *data)
+{
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
+  struct spa_hook_list save;
 
-  G_OBJECT_CLASS (wp_impl_endpoint_parent_class)->finalize (object);
+  spa_hook_list_isolate (&self->hooks, &save, listener, events, data);
+  pw_endpoint_emit_info (&self->hooks, &self->info);
+  spa_hook_list_join (&self->hooks, &save);
+  return 0;
 }
 
-static void
-client_endpoint_update (WpImplEndpoint * self, guint32 change_mask,
-    guint32 info_change_mask)
+static int
+impl_enum_params (void *object, int seq,
+    uint32_t id, uint32_t start, uint32_t num,
+    const struct spa_pod *filter)
 {
-  WpImplEndpointPrivate *priv = wp_impl_endpoint_get_instance_private (self);
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
   char buf[1024];
   struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof (buf));
-  struct pw_client_endpoint *pw_proxy = NULL;
-  struct pw_endpoint_info *info = NULL;
-  g_autoptr (GPtrArray) params = NULL;
-
-  pw_proxy = (struct pw_client_endpoint *) wp_proxy_get_pw_proxy (WP_PROXY (self));
+  struct spa_pod *result;
+  guint count = 0;
 
-  if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_PARAMS) {
-    params = wp_spa_props_build_all_pods (&priv->pp->spa_props, &b);
-  }
-  if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_INFO) {
-    info = &priv->info;
-    info->change_mask = info_change_mask;
+  switch (id) {
+    case SPA_PARAM_PropInfo: {
+      g_autoptr (GPtrArray) params =
+          wp_spa_props_build_propinfo (&priv->spa_props, &b);
+
+      for (guint i = start; i < params->len; i++) {
+        struct spa_pod *param = g_ptr_array_index (params, i);
+
+        if (spa_pod_filter (&b, &result, param, filter) == 0) {
+          pw_endpoint_emit_param (&self->hooks, seq, id, i, i+1, result);
+          wp_proxy_handle_event_param (self, seq, id, i, i+1, result);
+          if (++count == num)
+            break;
+        }
+      }
+      break;
+    }
+    case SPA_PARAM_Props: {
+      if (start == 0) {
+        struct spa_pod *param = wp_spa_props_build_props (&priv->spa_props, &b);
+        if (spa_pod_filter (&b, &result, param, filter) == 0) {
+          pw_endpoint_emit_param (&self->hooks, seq, id, 0, 1, result);
+          wp_proxy_handle_event_param (self, seq, id, 0, 1, result);
+        }
+      }
+      break;
+    }
+    default:
+      return -ENOENT;
   }
 
-  pw_client_endpoint_update (pw_proxy,
-      change_mask,
-      params ? params->len : 0,
-      (const struct spa_pod **) (params ? params->pdata : NULL),
-      info);
+  return 0;
+}
+
+static int
+impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
 
-  if (info)
-    info->change_mask = 0;
+  for (guint i = 0; i < n_ids; i++) {
+    if (ids[i] == SPA_PARAM_Props)
+      self->subscribed = TRUE;
+    impl_enum_params (self, 1, ids[i], 0, UINT32_MAX, NULL);
+  }
+  return 0;
 }
 
 static int
-client_endpoint_set_param (void *object,
-    uint32_t id, uint32_t flags, const struct spa_pod *param)
+impl_set_param (void *object, uint32_t id, uint32_t flags,
+    const struct spa_pod *param)
 {
   WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
-  WpImplEndpointPrivate *priv = wp_impl_endpoint_get_instance_private (self);
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
   g_autoptr (GArray) changed_ids = NULL;
-  guint32 prop_id;
 
   if (id != SPA_PARAM_Props)
     return -ENOENT;
 
   changed_ids = g_array_new (FALSE, FALSE, sizeof (guint32));
-  wp_spa_props_store_from_props (&priv->pp->spa_props, param, changed_ids);
+  wp_spa_props_store_from_props (&priv->spa_props, param, changed_ids);
 
+  /* notify subscribers */
+  if (self->subscribed)
+    impl_enum_params (self, 1, SPA_PARAM_Props, 0, UINT32_MAX, NULL);
+
+  /* notify controls locally */
   for (guint i = 0; i < changed_ids->len; i++) {
-    prop_id = g_array_index (changed_ids, guint32, i);
+    guint32 prop_id = g_array_index (changed_ids, guint32, i);
     g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0, prop_id);
   }
 
-  client_endpoint_update (self, PW_CLIENT_ENDPOINT_UPDATE_PARAMS, 0);
-
   return 0;
 }
 
-static struct pw_client_endpoint_events client_endpoint_events = {
-  PW_VERSION_CLIENT_ENDPOINT_EVENTS,
-  .set_param = client_endpoint_set_param,
+static int
+impl_create_link (void *object, const struct spa_dict *props)
+{
+  return -ENOTSUP;
+}
+
+static const struct pw_endpoint_methods impl_endpoint = {
+  PW_VERSION_ENDPOINT_METHODS,
+  .add_listener = impl_add_listener,
+  .subscribe_params = impl_subscribe_params,
+  .enum_params = impl_enum_params,
+  .set_param = impl_set_param,
+  .create_link = impl_create_link,
 };
 
 static void
-wp_impl_endpoint_augment (WpProxy * proxy, WpProxyFeatures features)
+populate_endpoint_info (WpImplEndpoint * self, guint32 change_mask)
 {
-  WpImplEndpoint *self = WP_IMPL_ENDPOINT (proxy);
-  WpImplEndpointPrivate *priv = wp_impl_endpoint_get_instance_private (self);
+  self->info.change_mask = change_mask & PW_ENDPOINT_CHANGE_MASK_ALL;
 
-  /* if any of the default features is requested, make sure BOUND
-     is also requested, as they all depend on binding the endpoint */
-  if (features & WP_PROXY_FEATURES_STANDARD)
-    features |= WP_PROXY_FEATURE_BOUND;
+  if (change_mask & PW_ENDPOINT_CHANGE_MASK_STREAMS) {
+    self->info.n_streams = wp_si_endpoint_get_n_streams (self->item);
+  }
 
-  if (features & WP_PROXY_FEATURE_BOUND) {
-    g_autoptr (WpCore) core = wp_proxy_get_core (proxy);
-    struct pw_core *pw_core = wp_core_get_pw_core (core);
-    struct pw_proxy *pw_proxy = NULL;
+  if (change_mask & PW_ENDPOINT_CHANGE_MASK_SESSION) {
+    g_autoptr (WpSession) session =
+        wp_session_item_get_session (WP_SESSION_ITEM (self->item));
+    self->info.session_id =
+        session ? wp_proxy_get_bound_id (WP_PROXY (session)) : SPA_ID_INVALID;
+  }
 
-    /* no pw_core -> we are not connected */
-    if (!pw_core) {
-      wp_proxy_augment_error (proxy, g_error_new (WP_DOMAIN_LIBRARY,
-            WP_LIBRARY_ERROR_OPERATION_FAILED,
-            "The WirePlumber core is not connected; "
-            "object cannot be exported to PipeWire"));
-      return;
-    }
+  if (change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) {
+    WpEndpointPrivate *priv =
+        wp_endpoint_get_instance_private (WP_ENDPOINT (self));
 
-    /* make sure these props are not present; they are added by the server */
-    wp_properties_set (priv->pp->properties, PW_KEY_OBJECT_ID, NULL);
-    wp_properties_set (priv->pp->properties, PW_KEY_CLIENT_ID, NULL);
-    wp_properties_set (priv->pp->properties, PW_KEY_FACTORY_ID, NULL);
+    g_clear_pointer (&priv->properties, wp_properties_unref);
+    priv->properties = wp_si_endpoint_get_properties (self->item);
 
-    /* add must-have global properties */
-    wp_properties_set (priv->pp->properties,
-        PW_KEY_ENDPOINT_NAME, priv->info.name);
-    wp_properties_set (priv->pp->properties,
-        PW_KEY_MEDIA_CLASS, priv->info.media_class);
+    self->info.props = priv->properties ?
+        (struct spa_dict *) wp_properties_peek_dict (priv->properties) : NULL;
 
-    pw_proxy = pw_core_create_object (pw_core, "client-endpoint",
-        PW_TYPE_INTERFACE_ClientEndpoint, PW_VERSION_CLIENT_ENDPOINT,
-        wp_properties_peek_dict (priv->pp->properties), 0);
-    wp_proxy_set_pw_proxy (proxy, pw_proxy);
+    g_object_notify (G_OBJECT (self), "properties");
+  }
 
-    pw_client_endpoint_add_listener (pw_proxy, &priv->pp->listener,
-        &client_endpoint_events, self);
+  if (change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+    static struct spa_param_info param_info[] = {
+      SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
+      SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
+    };
 
-    client_endpoint_update (WP_IMPL_ENDPOINT (self),
-        PW_CLIENT_ENDPOINT_UPDATE_PARAMS | PW_CLIENT_ENDPOINT_UPDATE_INFO,
-        PW_ENDPOINT_CHANGE_MASK_ALL);
+    self->info.params = param_info;
+    self->info.n_params = SPA_N_ELEMENTS (param_info);
   }
+
+  g_object_notify (G_OBJECT (self), "info");
 }
 
-static gint
-wp_impl_endpoint_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+static void
+on_si_endpoint_properties_changed (WpSiEndpoint * item, WpImplEndpoint * self)
 {
-  return client_endpoint_set_param (self, id, flags, param);
+  populate_endpoint_info (self, PW_ENDPOINT_CHANGE_MASK_PROPS);
 }
 
-static gboolean
-wp_impl_endpoint_set_control (WpEndpoint * endpoint, guint32 control_id,
-    const struct spa_pod * pod)
+static void
+wp_impl_endpoint_init (WpImplEndpoint * self)
 {
-  WpImplEndpointPrivate *priv =
-      wp_impl_endpoint_get_instance_private (WP_IMPL_ENDPOINT (endpoint));
+  /* reuse the parent's private to optimize memory usage and to be able
+     to re-use some of the parent's methods without reimplementing them */
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
 
-  if (wp_spa_props_store_pod (&priv->pp->spa_props, control_id, pod) < 0)
-    return FALSE;
+  self->iface = SPA_INTERFACE_INIT (
+      PW_TYPE_INTERFACE_Endpoint,
+      PW_VERSION_ENDPOINT,
+      &impl_endpoint, self);
+  spa_hook_list_init (&self->hooks);
 
-  g_signal_emit (endpoint, signals[SIGNAL_CONTROL_CHANGED], 0, control_id);
+  priv->iface = (struct pw_endpoint *) &self->iface;
 
-  /* update only after the endpoint has been exported */
-  if (wp_proxy_get_features (WP_PROXY (endpoint)) & WP_PROXY_FEATURE_BOUND) {
-    client_endpoint_update (WP_IMPL_ENDPOINT (endpoint),
-        PW_CLIENT_ENDPOINT_UPDATE_PARAMS, 0);
-  }
-
-  return TRUE;
+  wp_proxy_set_feature_ready (WP_PROXY (self), WP_ENDPOINT_FEATURE_CONTROLS);
 }
 
 static void
-wp_impl_endpoint_class_init (WpImplEndpointClass * klass)
+wp_impl_endpoint_finalize (GObject * object)
 {
-  GObjectClass *object_class = (GObjectClass *) klass;
-  WpProxyClass *proxy_class = (WpProxyClass *) klass;
-  WpEndpointClass *endpoint_class = (WpEndpointClass *) klass;
-
-  object_class->finalize = wp_impl_endpoint_finalize;
-
-  proxy_class->augment = wp_impl_endpoint_augment;
-  proxy_class->enum_params = NULL;
-  proxy_class->subscribe_params = NULL;
-  proxy_class->set_param = wp_impl_endpoint_set_param;
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
 
-  proxy_class->pw_proxy_created = NULL;
-  proxy_class->param = NULL;
+  g_free (self->info.name);
+  g_free (self->info.media_class);
+  priv->info = NULL;
 
-  endpoint_class->set_control = wp_impl_endpoint_set_control;
+  G_OBJECT_CLASS (wp_impl_endpoint_parent_class)->finalize (object);
 }
 
-/**
- * wp_impl_endpoint_new:
- * @core: the #WpCore
- *
- * Returns: (transfer full): the newly constructed endpoint implementation
- */
-WpImplEndpoint *
-wp_impl_endpoint_new (WpCore * core)
+static void
+wp_impl_endpoint_set_property (GObject * object, guint property_id,
+    const GValue * value, GParamSpec * pspec)
 {
-  g_return_val_if_fail (WP_IS_CORE (core), NULL);
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
 
-  return g_object_new (WP_TYPE_IMPL_ENDPOINT,
-      "core", core,
-      NULL);
+  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;
+  }
 }
 
-/**
- * wp_impl_endpoint_set_property:
- * @self: the endpoint implementation
- * @key: a property key
- * @value: a property value
- *
- * Sets the specified property on the PipeWire properties of the endpoint.
- *
- * If this property is set before exporting the endpoint, then it is also used
- * in the construction process of the endpoint object and appears as a global
- * property.
- */
-void
-wp_impl_endpoint_set_property (WpImplEndpoint * self,
-    const gchar * key, const gchar * value)
+static void
+wp_impl_endpoint_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
 {
-  WpImplEndpointPrivate *priv;
-
-  g_return_if_fail (WP_IS_IMPL_ENDPOINT (self));
-  priv = wp_impl_endpoint_get_instance_private (self);
-
-  wp_properties_set (priv->pp->properties, key, value);
-
-  g_object_notify (G_OBJECT (self), "properties");
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
 
-  /* update only after the endpoint has been exported */
-  if (wp_proxy_get_features (WP_PROXY (self)) & WP_PROXY_FEATURE_BOUND) {
-    client_endpoint_update (self, PW_CLIENT_ENDPOINT_UPDATE_INFO,
-        PW_ENDPOINT_CHANGE_MASK_PROPS);
+  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;
   }
 }
 
-/**
- * wp_impl_endpoint_update_properties:
- * @self: the endpoint implementation
- * @updates: a set of properties to add or update in the endpoint's properties
- *
- * Adds or updates the values of the PipeWire properties of the endpoint
- * using the properties in @updates as a source.
- *
- * If the properties are set before exporting the endpoint, then they are also
- * used in the construction process of the endpoint object and appear as
- * global properties.
- */
-void
-wp_impl_endpoint_update_properties (WpImplEndpoint * self,
-    WpProperties * updates)
+static void
+wp_impl_endpoint_augment (WpProxy * proxy, WpProxyFeatures features)
 {
-  WpImplEndpointPrivate *priv;
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (proxy);
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
+  g_autoptr (GVariant) info = NULL;
+  g_autoptr (GVariantIter) immutable_props = NULL;
+  g_autoptr (WpProperties) props = NULL;
+
+  /* PW_PROXY depends on BOUND */
+  if (features & WP_PROXY_FEATURE_PW_PROXY)
+    features |= WP_PROXY_FEATURE_BOUND;
 
-  g_return_if_fail (WP_IS_IMPL_ENDPOINT (self));
-  priv = wp_impl_endpoint_get_instance_private (self);
+  /* BOUND depends on INFO */
+  if (features & WP_PROXY_FEATURE_BOUND)
+    features |= WP_PROXY_FEATURE_INFO;
+
+  if (features & WP_PROXY_FEATURE_INFO) {
+    guchar direction;
+    const gchar *key, *value;
+
+    /* initialize info struct */
+    priv->info = &self->info;
+    self->info.version = PW_VERSION_ENDPOINT_INFO;
+
+    info = wp_si_endpoint_get_registration_info (self->item);
+    g_variant_get (info, "(ssya{ss})",
+        &self->info.name,
+        &self->info.media_class,
+        &direction,
+        &immutable_props);
+    self->info.direction = (enum pw_direction) direction;
+
+    populate_endpoint_info (self, PW_ENDPOINT_CHANGE_MASK_ALL);
+
+    /* subscribe to changes */
+    g_signal_connect_object (self->item, "endpoint-properties-changed",
+        G_CALLBACK (on_si_endpoint_properties_changed), self, 0);
+
+    /* construct export properties (these will come back through
+       the registry and appear in wp_proxy_get_global_properties) */
+    props = wp_properties_new (
+        PW_KEY_ENDPOINT_NAME, self->info.name,
+        PW_KEY_MEDIA_CLASS, self->info.media_class,
+        NULL);
+    if (self->info.session_id != SPA_ID_INVALID) {
+      wp_properties_setf (props, PW_KEY_SESSION_ID, "%u",
+          self->info.session_id);
+    }
+    while (g_variant_iter_next (immutable_props, "{&s&s}", &key, &value)) {
+      wp_properties_set (props, key, value);
+    }
+
+    wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
+  }
 
-  wp_properties_update_from_dict (priv->pp->properties,
-      wp_properties_peek_dict (updates));
+  if (features & WP_PROXY_FEATURE_BOUND) {
+    g_autoptr (WpCore) core = wp_proxy_get_core (proxy);
+    struct pw_core *pw_core = wp_core_get_pw_core (core);
 
-  g_object_notify (G_OBJECT (self), "properties");
+    /* no pw_core -> we are not connected */
+    if (!pw_core) {
+      wp_proxy_augment_error (proxy, g_error_new (WP_DOMAIN_LIBRARY,
+            WP_LIBRARY_ERROR_OPERATION_FAILED,
+            "The WirePlumber core is not connected; "
+            "object cannot be exported to PipeWire"));
+      return;
+    }
 
-  /* update only after the endpoint has been exported */
-  if (wp_proxy_get_features (WP_PROXY (self)) & WP_PROXY_FEATURE_BOUND) {
-    client_endpoint_update (self, PW_CLIENT_ENDPOINT_UPDATE_INFO,
-        PW_ENDPOINT_CHANGE_MASK_PROPS);
+    wp_proxy_set_pw_proxy (proxy, pw_core_export (pw_core,
+            PW_TYPE_INTERFACE_Endpoint,
+            wp_properties_peek_dict (props),
+            priv->iface, 0));
   }
 }
 
-/**
- * wp_impl_endpoint_set_name:
- * @self: the endpoint implementation
- * @name: the name to set
- *
- * Sets the name of the endpoint to be @name.
- *
- * This only makes sense to set before exporting the endpoint.
- */
-void
-wp_impl_endpoint_set_name (WpImplEndpoint * self, const gchar * name)
+static void
+wp_impl_endpoint_class_init (WpImplEndpointClass * klass)
 {
-  WpImplEndpointPrivate *priv;
-
-  g_return_if_fail (WP_IS_IMPL_ENDPOINT (self));
-  priv = wp_impl_endpoint_get_instance_private (self);
+  GObjectClass *object_class = (GObjectClass *) klass;
+  WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  g_free (priv->info.name);
-  priv->info.name = g_strdup (name);
-}
+  object_class->finalize = wp_impl_endpoint_finalize;
+  object_class->set_property = wp_impl_endpoint_set_property;
+  object_class->get_property = wp_impl_endpoint_get_property;
 
-/**
- * wp_impl_endpoint_set_media_class:
- * @self: the endpoint implementation
- * @media_class: the media class to set
- *
- * Sets the media class of the endpoint to be @media_class.
- *
- * This only makes sense to set before exporting the endpoint.
- */
-void
-wp_impl_endpoint_set_media_class (WpImplEndpoint * self,
-    const gchar * media_class)
-{
-  WpImplEndpointPrivate *priv;
+  proxy_class->augment = wp_impl_endpoint_augment;
 
-  g_return_if_fail (WP_IS_IMPL_ENDPOINT (self));
-  priv = wp_impl_endpoint_get_instance_private (self);
+  proxy_class->pw_proxy_created = NULL;
+  proxy_class->param = NULL;
 
-  g_free (priv->info.media_class);
-  priv->info.media_class = g_strdup (media_class);
+  g_object_class_install_property (object_class, IMPL_PROP_ITEM,
+      g_param_spec_object ("item", "item", "item", WP_TYPE_SI_ENDPOINT,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 }
 
-/**
- * wp_impl_endpoint_set_direction:
- * @self: the endpoint implementation
- * @dir: the direction to set
- *
- * Sets the direction of the endpoint to be @dir.
- *
- * This only makes sense to set before exporting the endpoint.
- */
-void
-wp_impl_endpoint_set_direction (WpImplEndpoint * self, WpDirection dir)
+WpImplEndpoint *
+wp_impl_endpoint_new (WpCore * core, WpSiEndpoint * item)
 {
-  WpImplEndpointPrivate *priv;
-
-  g_return_if_fail (WP_IS_IMPL_ENDPOINT (self));
-  priv = wp_impl_endpoint_get_instance_private (self);
+  g_return_val_if_fail (WP_IS_CORE (core), NULL);
 
-  priv->info.direction = (enum pw_direction) dir;
+  return g_object_new (WP_TYPE_IMPL_ENDPOINT,
+      "core", core,
+      "item", item,
+      NULL);
 }
 
+#if 0
 /**
  * wp_impl_endpoint_register_control:
  * @self: the endpoint implementation
@@ -936,3 +897,4 @@ wp_impl_endpoint_register_control (WpImplEndpoint * self,
     break;
   }
 }
+#endif
diff --git a/lib/wp/endpoint.h b/lib/wp/endpoint.h
index f3ca9330..ad500c7c 100644
--- a/lib/wp/endpoint.h
+++ b/lib/wp/endpoint.h
@@ -112,48 +112,6 @@ WP_API
 gboolean wp_endpoint_set_control_float (WpEndpoint * self, guint32 control_id,
     gfloat value);
 
-/**
- * WP_TYPE_IMPL_ENDPOINT:
- *
- * The #WpImplEndpoint #GType
- */
-#define WP_TYPE_IMPL_ENDPOINT (wp_impl_endpoint_get_type ())
-WP_API
-G_DECLARE_DERIVABLE_TYPE (WpImplEndpoint, wp_impl_endpoint,
-                          WP, IMPL_ENDPOINT, WpEndpoint)
-
-struct _WpImplEndpointClass
-{
-  WpEndpointClass parent_class;
-};
-
-WP_API
-WpImplEndpoint * wp_impl_endpoint_new (WpCore * core);
-
-WP_API
-void wp_impl_endpoint_set_property (WpImplEndpoint * self,
-    const gchar * key, const gchar * value);
-
-WP_API
-void wp_impl_endpoint_update_properties (WpImplEndpoint * self,
-    WpProperties * updates);
-
-WP_API
-void wp_impl_endpoint_set_name (WpImplEndpoint * self,
-    const gchar * name);
-
-WP_API
-void wp_impl_endpoint_set_media_class (WpImplEndpoint * self,
-    const gchar * media_class);
-
-WP_API
-void wp_impl_endpoint_set_direction (WpImplEndpoint * self,
-    WpDirection dir);
-
-WP_API
-void wp_impl_endpoint_register_control (WpImplEndpoint * self,
-    WpEndpointControl control);
-
 G_END_DECLS
 
 #endif
diff --git a/lib/wp/private.h b/lib/wp/private.h
index e48b2d6d..66051266 100644
--- a/lib/wp/private.h
+++ b/lib/wp/private.h
@@ -12,6 +12,8 @@
 #include "core.h"
 #include "object-manager.h"
 #include "proxy.h"
+#include "endpoint.h"
+#include "si-interfaces.h"
 
 #include <stdint.h>
 #include <pipewire/pipewire.h>
@@ -135,6 +137,10 @@ gint wp_spa_props_store_pod (WpSpaProps * self, guint32 id,
 gint wp_spa_props_store_from_props (WpSpaProps * self,
     const struct spa_pod * props, GArray * changed_ids);
 
+struct spa_pod * wp_spa_props_build_props (WpSpaProps * self,
+    struct spa_pod_builder * b);
+GPtrArray * wp_spa_props_build_propinfo (WpSpaProps * self,
+    struct spa_pod_builder * b);
 GPtrArray * wp_spa_props_build_all_pods (WpSpaProps * self,
     struct spa_pod_builder * b);
 struct spa_pod * wp_spa_props_build_update (WpSpaProps * self, guint32 id,
@@ -168,6 +174,14 @@ wp_spa_props_build_pod (gchar * buffer, gsize size, ...)
       wp_spa_props_build_pod (b, sizeof (b), ##__VA_ARGS__, NULL)); \
 })
 
+/* impl endpoint */
+
+#define WP_TYPE_IMPL_ENDPOINT (wp_impl_endpoint_get_type ())
+G_DECLARE_FINAL_TYPE (WpImplEndpoint, wp_impl_endpoint,
+                      WP, IMPL_ENDPOINT, WpEndpoint)
+
+WpImplEndpoint * wp_impl_endpoint_new (WpCore * core, WpSiEndpoint * item);
+
 G_END_DECLS
 
 #endif
diff --git a/lib/wp/session-item.c b/lib/wp/session-item.c
index 0a7d9f9e..5c88a0cf 100644
--- a/lib/wp/session-item.c
+++ b/lib/wp/session-item.c
@@ -12,6 +12,8 @@
  */
 
 #include "session-item.h"
+#include "private.h"
+#include "error.h"
 #include "wpenums.h"
 
 typedef struct _WpSessionItemPrivate WpSessionItemPrivate;
@@ -19,6 +21,8 @@ struct _WpSessionItemPrivate
 {
   GWeakRef session;
   guint32 flags;
+
+  WpImplEndpoint *impl_endpoint;
 };
 
 enum {
@@ -56,8 +60,7 @@ static void
 wp_session_item_finalize (GObject * object)
 {
   WpSessionItem * self = WP_SESSION_ITEM (object);
-  WpSessionItemPrivate *priv =
-      wp_session_item_get_instance_private (self);
+  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
 
   g_weak_ref_clear (&priv->session);
 
@@ -76,13 +79,92 @@ wp_session_item_default_get_next_step (WpSessionItem * self,
 static void
 wp_session_item_default_reset (WpSessionItem * self)
 {
-  WpSessionItemPrivate *priv =
-      wp_session_item_get_instance_private (self);
+  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
+
+  wp_session_item_unexport (self);
 
   priv->flags &= ~(WP_SI_FLAG_ACTIVE | WP_SI_FLAG_IN_ERROR);
   g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);
 }
 
+static void
+on_export_proxy_augmented (WpProxy * proxy, GAsyncResult * res, gpointer data)
+{
+  g_autoptr (GTask) task = G_TASK (data);
+  g_autoptr (GError) error = NULL;
+  WpSessionItem *self = WP_SESSION_ITEM (g_task_get_source_object (task));
+  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
+
+  priv->flags &= ~WP_SI_FLAG_EXPORTING;
+
+  if (!wp_proxy_augment_finish (proxy, res, &error)) {
+    g_weak_ref_set (&priv->session, NULL);
+    g_clear_object (&priv->impl_endpoint);
+    g_task_return_error (task, g_steal_pointer (&error));
+    return;
+  }
+
+  priv->flags |= WP_SI_FLAG_EXPORTED;
+  g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);
+  g_task_return_boolean (task, TRUE);
+}
+
+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);
+  g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (session));
+  g_autoptr (GTask) task = NULL;
+
+  g_return_if_fail (priv->flags & WP_SI_FLAG_ACTIVE);
+  g_return_if_fail (!(priv->flags & (WP_SI_FLAG_EXPORTING | WP_SI_FLAG_EXPORTED)));
+
+  task = g_task_new (self, cancellable, callback, callback_data);
+  g_task_set_source_tag (task, wp_session_item_default_export);
+
+  if (WP_IS_SI_ENDPOINT (self)) {
+    g_weak_ref_set (&priv->session, session);
+    priv->flags |= WP_SI_FLAG_EXPORTING;
+    priv->impl_endpoint = wp_impl_endpoint_new (core, WP_SI_ENDPOINT (self));
+
+    g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);
+
+    wp_proxy_augment (WP_PROXY (priv->impl_endpoint),
+        WP_PROXY_FEATURES_STANDARD, NULL,
+        (GAsyncReadyCallback) on_export_proxy_augmented,
+        g_steal_pointer (&task));
+  }
+  else {
+    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
+        WP_LIBRARY_ERROR_INVALID_ARGUMENT,
+        "Cannot export WpSessionItem of unknown type (%s:%p)",
+        G_OBJECT_TYPE_NAME (self), self);
+  }
+}
+
+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 g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+wp_session_item_default_unexport (WpSessionItem * self)
+{
+  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
+
+  //TODO cancel job if EXPORTING
+
+  g_clear_object (&priv->impl_endpoint);
+  priv->flags &= ~(WP_SI_FLAG_EXPORTING | WP_SI_FLAG_EXPORTED);
+}
+
 static void
 wp_session_item_class_init (WpSessionItemClass * klass)
 {
@@ -94,6 +176,9 @@ wp_session_item_class_init (WpSessionItemClass * klass)
   klass->reset = wp_session_item_default_reset;
 
   klass->get_next_step = wp_session_item_default_get_next_step;
+  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:
@@ -358,3 +443,68 @@ wp_session_item_reset (WpSessionItem * self)
 
   WP_SESSION_ITEM_GET_CLASS (self)->reset (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);
+
+  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);
+
+  WP_SESSION_ITEM_GET_CLASS (self)->unexport (self);
+}
diff --git a/lib/wp/session-item.h b/lib/wp/session-item.h
index 5446581e..07407dc8 100644
--- a/lib/wp/session-item.h
+++ b/lib/wp/session-item.h
@@ -10,11 +10,10 @@
 #define __WIREPLUMBER_SESSION_ITEM_H__
 
 #include "transition.h"
+#include "session.h"
 
 G_BEGIN_DECLS
 
-typedef struct _WpSession WpSession;
-
 /**
  * WP_TYPE_SESSION_ITEM:
  *
@@ -29,23 +28,25 @@ G_DECLARE_DERIVABLE_TYPE (WpSessionItem, wp_session_item,
  * WpSiFlags:
  * @WP_SI_FLAG_ACTIVATING: set when an activation transition is in progress
  * @WP_SI_FLAG_ACTIVE: set when an activation transition completes successfully
- * @WP_SI_FLAG_EXPORTED: set when the item has exported all necessary objects
- *   to PipeWire
  * @WP_SI_FLAG_IN_ERROR: set when there was an error in the activation process;
  *   to recover, the handler must call wp_session_item_reset() before anything
  *   else
  * @WP_SI_FLAG_CONFIGURED: must be set by subclasses when all the required
  *   (%WP_SI_CONFIG_OPTION_REQUIRED) configuration options have been set
+ * @WP_SI_FLAG_EXPORTING: set when an export operation is in progress
+ * @WP_SI_FLAG_EXPORTED: set when the item has exported all necessary objects
+ *   to PipeWire
  */
 typedef enum {
   /* immutable flags, set internally */
   WP_SI_FLAG_ACTIVATING = (1<<0),
   WP_SI_FLAG_ACTIVE = (1<<1),
-  WP_SI_FLAG_EXPORTED = (1<<2),
-  WP_SI_FLAG_IN_ERROR = (1<<3),
+  WP_SI_FLAG_IN_ERROR = (1<<4),
 
   /* flags that can be changed by subclasses */
   WP_SI_FLAG_CONFIGURED = (1<<8),
+  WP_SI_FLAG_EXPORTING = (1<<9),
+  WP_SI_FLAG_EXPORTED = (1<<10),
 
   /* implementation-specific flags */
   WP_SI_FLAG_CUSTOM_START = (1<<16),
@@ -70,6 +71,9 @@ typedef enum {
  * @execute_step: Implements #WpTransitionClass.execute_step() for the
  *   transition of wp_session_item_activate()
  * @reset: See wp_session_item_reset()
+ * @export: See wp_session_item_export()
+ * @export_finish: See wp_session_item_export_finish()
+ * @unexport: See wp_session_item_unexport()
  */
 struct _WpSessionItemClass
 {
@@ -84,6 +88,13 @@ struct _WpSessionItemClass
       guint step);
 
   void (*reset) (WpSessionItem * self);
+
+  void (*export) (WpSessionItem * self,
+      WpSession * session, GCancellable * cancellable,
+      GAsyncReadyCallback callback, gpointer callback_data);
+  gboolean (*export_finish) (WpSessionItem * self, GAsyncResult * res,
+      GError ** error);
+  void (*unexport) (WpSessionItem * self);
 };
 
 /* properties */
@@ -121,6 +132,19 @@ gboolean wp_session_item_activate_finish (WpSessionItem * self,
 WP_API
 void wp_session_item_reset (WpSessionItem * self);
 
+/* exporting */
+
+WP_API
+void wp_session_item_export (WpSessionItem * self, WpSession * session,
+    GAsyncReadyCallback callback, gpointer callback_data);
+
+WP_API
+gboolean wp_session_item_export_finish (WpSessionItem * self,
+    GAsyncResult * res, GError ** error);
+
+WP_API
+void wp_session_item_unexport (WpSessionItem * self);
+
 G_END_DECLS
 
 #endif
diff --git a/lib/wp/si-interfaces.c b/lib/wp/si-interfaces.c
index a4c2506a..f583d26d 100644
--- a/lib/wp/si-interfaces.c
+++ b/lib/wp/si-interfaces.c
@@ -23,66 +23,33 @@ G_DEFINE_INTERFACE (WpSiEndpoint, wp_si_endpoint, WP_TYPE_SESSION_ITEM)
 static void
 wp_si_endpoint_default_init (WpSiEndpointInterface * iface)
 {
-}
-
-/**
- * wp_si_endpoint_get_name: (virtual get_name)
- * @self: the session item
- *
- * Returns: (transfer none): the name of the endpoint
- */
-const gchar *
-wp_si_endpoint_get_name (WpSiEndpoint * self)
-{
-  g_return_val_if_fail (WP_IS_SI_ENDPOINT (self), NULL);
-  g_return_val_if_fail (WP_SI_ENDPOINT_GET_IFACE (self)->get_name, NULL);
+  g_signal_new ("endpoint-properties-changed", G_TYPE_FROM_INTERFACE (iface),
+      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 
-  return WP_SI_ENDPOINT_GET_IFACE (self)->get_name (self);
+  g_signal_new ("endpoint-streams-changed", G_TYPE_FROM_INTERFACE (iface),
+      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 }
 
 /**
- * wp_si_endpoint_get_media_class: (virtual get_media_class)
+ * wp_si_endpoint_get_registration_info: (virtual get_registration_info)
  * @self: the session item
  *
- * Returns: (transfer none): the media class of the endpoint
- */
-const gchar *
-wp_si_endpoint_get_media_class (WpSiEndpoint * self)
-{
-  g_return_val_if_fail (WP_IS_SI_ENDPOINT (self), NULL);
-  g_return_val_if_fail (WP_SI_ENDPOINT_GET_IFACE (self)->get_media_class, NULL);
-
-  return WP_SI_ENDPOINT_GET_IFACE (self)->get_media_class (self);
-}
-
-/**
- * wp_si_endpoint_get_direction: (virtual get_direction)
- * @self: the session item
- *
- * Returns: the direction of the endpoint
- */
-WpDirection
-wp_si_endpoint_get_direction (WpSiEndpoint * self)
-{
-  g_return_val_if_fail (WP_IS_SI_ENDPOINT (self), 0);
-  g_return_val_if_fail (WP_SI_ENDPOINT_GET_IFACE (self)->get_direction, 0);
-
-  return WP_SI_ENDPOINT_GET_IFACE (self)->get_direction (self);
-}
-
-/**
- * wp_si_endpoint_get_priority: (virtual get_priority)
- * @self: the session item
+ * This should return information that is used for registering the endpoint,
+ * as a GVariant tuple of type (ssya{ss}) that contains, in order:
+ *  - s: the endpoint's name
+ *  - s: the media class
+ *  - y: the direction
+ *  - a{ss}: additional properties to be added to the list of global properties
  *
- * Returns: the priority of the endpoint
+ * Returns: (transfer full): registration info for the endpoint
  */
-guint
-wp_si_endpoint_get_priority (WpSiEndpoint * self)
+GVariant *
+wp_si_endpoint_get_registration_info (WpSiEndpoint * self)
 {
-  g_return_val_if_fail (WP_IS_SI_ENDPOINT (self), 0);
-  g_return_val_if_fail (WP_SI_ENDPOINT_GET_IFACE (self)->get_priority, 0);
+  g_return_val_if_fail (WP_IS_SI_ENDPOINT (self), NULL);
+  g_return_val_if_fail (WP_SI_ENDPOINT_GET_IFACE (self)->get_registration_info, NULL);
 
-  return WP_SI_ENDPOINT_GET_IFACE (self)->get_priority (self);
+  return WP_SI_ENDPOINT_GET_IFACE (self)->get_registration_info (self);
 }
 
 /**
@@ -197,21 +164,28 @@ G_DEFINE_INTERFACE (WpSiStream, wp_si_stream, WP_TYPE_SESSION_ITEM)
 static void
 wp_si_stream_default_init (WpSiStreamInterface * iface)
 {
+  g_signal_new ("stream-properties-changed", G_TYPE_FROM_INTERFACE (iface),
+      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 }
 
 /**
- * wp_si_stream_get_name: (virtual get_name)
+ * wp_si_stream_get_registration_info: (virtual get_registration_info)
  * @self: the session item
  *
- * Returns: (transfer none): the name of the stream
+ * This should return information that is used for registering the stream,
+ * as a GVariant tuple of type (sa{ss}) that contains, in order:
+ *  - s: the stream's name
+ *  - a{ss}: additional properties to be added to the list of global properties
+ *
+ * Returns: (transfer full): registration info for the stream
  */
-const gchar *
-wp_si_stream_get_name (WpSiStream * self)
+GVariant *
+wp_si_stream_get_registration_info (WpSiStream * self)
 {
   g_return_val_if_fail (WP_IS_SI_STREAM (self), NULL);
-  g_return_val_if_fail (WP_SI_STREAM_GET_IFACE (self)->get_name, NULL);
+  g_return_val_if_fail (WP_SI_STREAM_GET_IFACE (self)->get_registration_info, NULL);
 
-  return WP_SI_STREAM_GET_IFACE (self)->get_name (self);
+  return WP_SI_STREAM_GET_IFACE (self)->get_registration_info (self);
 }
 
 /**
diff --git a/lib/wp/si-interfaces.h b/lib/wp/si-interfaces.h
index cf6dc57f..f31cf8f4 100644
--- a/lib/wp/si-interfaces.h
+++ b/lib/wp/si-interfaces.h
@@ -17,6 +17,8 @@ G_BEGIN_DECLS
 
 typedef struct _WpSiStream WpSiStream;
 
+
+
 /**
  * WP_TYPE_SI_ENDPOINT:
  *
@@ -31,11 +33,7 @@ struct _WpSiEndpointInterface
 {
   GTypeInterface interface;
 
-  const gchar * (*get_name) (WpSiEndpoint * self);
-  const gchar * (*get_media_class) (WpSiEndpoint * self);
-  const gchar * (*get_role) (WpSiEndpoint * self);
-  WpDirection (*get_direction) (WpSiEndpoint * self);
-  guint (*get_priority) (WpSiEndpoint * self);
+  GVariant * (*get_registration_info) (WpSiEndpoint * self);
   WpProperties * (*get_properties) (WpSiEndpoint * self);
 
   guint (*get_n_streams) (WpSiEndpoint * self);
@@ -43,16 +41,7 @@ struct _WpSiEndpointInterface
 };
 
 WP_API
-const gchar * wp_si_endpoint_get_name (WpSiEndpoint * self);
-
-WP_API
-const gchar * wp_si_endpoint_get_media_class (WpSiEndpoint * self);
-
-WP_API
-WpDirection wp_si_endpoint_get_direction (WpSiEndpoint * self);
-
-WP_API
-guint wp_si_endpoint_get_priority (WpSiEndpoint * self);
+GVariant * wp_si_endpoint_get_registration_info (WpSiEndpoint * self);
 
 WP_API
 WpProperties * wp_si_endpoint_get_properties (WpSiEndpoint * self);
@@ -102,14 +91,14 @@ struct _WpSiStreamInterface
 {
   GTypeInterface interface;
 
-  const gchar * (*get_name) (WpSiStream * self);
+  GVariant * (*get_registration_info) (WpSiStream * self);
   WpProperties * (*get_properties) (WpSiStream * self);
 
   WpSiEndpoint * (*get_parent_endpoint) (WpSiStream * self);
 };
 
 WP_API
-const gchar * wp_si_stream_get_name (WpSiStream * self);
+GVariant * wp_si_stream_get_registration_info (WpSiStream * self);
 
 WP_API
 WpProperties * wp_si_stream_get_properties (WpSiStream * self);
diff --git a/lib/wp/spa-props.c b/lib/wp/spa-props.c
index aa99248a..bbe3ab85 100644
--- a/lib/wp/spa-props.c
+++ b/lib/wp/spa-props.c
@@ -224,16 +224,12 @@ wp_spa_props_store_from_props (WpSpaProps * self, const struct spa_pod * props,
   return count;
 }
 
-// for exported update / prop_info + props
-GPtrArray *
-wp_spa_props_build_all_pods (WpSpaProps * self, struct spa_pod_builder * b)
+struct spa_pod *
+wp_spa_props_build_props (WpSpaProps * self, struct spa_pod_builder * b)
 {
-  GPtrArray *res = g_ptr_array_new ();
-  GList *l;
   struct spa_pod_frame f;
-  struct spa_pod *pod;
+  GList *l;
 
-  /* Props */
   spa_pod_builder_push_object (b, &f, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
   for (l = self->entries; l != NULL; l = g_list_next (l)) {
     struct entry * e = (struct entry *) l->data;
@@ -242,10 +238,16 @@ wp_spa_props_build_all_pods (WpSpaProps * self, struct spa_pod_builder * b)
       spa_pod_builder_primitive (b, e->value);
     }
   }
-  pod = spa_pod_builder_pop (b, &f);
-  g_ptr_array_add (res, pod);
+  return spa_pod_builder_pop (b, &f);
+}
+
+GPtrArray *
+wp_spa_props_build_propinfo (WpSpaProps * self, struct spa_pod_builder * b)
+{
+  GPtrArray *res = g_ptr_array_new ();
+  GList *l;
+  struct spa_pod *pod;
 
-  /* PropInfo */
   for (l = self->entries; l != NULL; l = g_list_next (l)) {
     struct entry * e = (struct entry *) l->data;
     pod = spa_pod_builder_add_object (b,
@@ -259,6 +261,20 @@ wp_spa_props_build_all_pods (WpSpaProps * self, struct spa_pod_builder * b)
   return res;
 }
 
+// for exported update / prop_info + props
+GPtrArray *
+wp_spa_props_build_all_pods (WpSpaProps * self, struct spa_pod_builder * b)
+{
+  GPtrArray *res;
+  struct spa_pod *pod;
+
+  pod = wp_spa_props_build_props (self, b);
+  res = wp_spa_props_build_propinfo (self, b);
+  g_ptr_array_insert (res, 0, pod);
+
+  return res;
+}
+
 // proxy set --> value to props object -> push
 struct spa_pod *
 wp_spa_props_build_update (WpSpaProps * self, guint32 id,
diff --git a/modules/module-si-adapter.c b/modules/module-si-adapter.c
index e94f625a..221f2601 100644
--- a/modules/module-si-adapter.c
+++ b/modules/module-si-adapter.c
@@ -380,39 +380,19 @@ si_adapter_multi_endpoint_init (WpSiMultiEndpointInterface * iface)
   iface->get_endpoint = si_adapter_get_endpoint;
 }
 
-static const gchar *
-si_adapter_get_name (WpSiEndpoint * item)
-{
-  WpSiAdapter *self = WP_SI_ADAPTER (item);
-  return self->name;
-}
-
-static const gchar *
-si_adapter_get_media_class (WpSiEndpoint * item)
-{
-  WpSiAdapter *self = WP_SI_ADAPTER (item);
-  return self->media_class;
-}
-
-static const gchar *
-si_adapter_get_role (WpSiEndpoint * item)
+static GVariant *
+si_adapter_get_registration_info (WpSiEndpoint * item)
 {
   WpSiAdapter *self = WP_SI_ADAPTER (item);
-  return self->role;
-}
+  GVariantBuilder b;
 
-static WpDirection
-si_adapter_get_direction (WpSiEndpoint * item)
-{
-  WpSiAdapter *self = WP_SI_ADAPTER (item);
-  return self->direction;
-}
+  g_variant_builder_init (&b, G_VARIANT_TYPE ("(ssya{ss})"));
+  g_variant_builder_add (&b, "s", self->name);
+  g_variant_builder_add (&b, "s", self->media_class);
+  g_variant_builder_add (&b, "y", (guchar) self->direction);
+  g_variant_builder_add (&b, "a{ss}", NULL);
 
-static guint
-si_adapter_get_priority (WpSiEndpoint * item)
-{
-  WpSiAdapter *self = WP_SI_ADAPTER (item);
-  return self->priority;
+  return g_variant_builder_end (&b);
 }
 
 static WpProperties *
@@ -422,7 +402,10 @@ si_adapter_get_properties (WpSiEndpoint * item)
   g_autoptr (WpProperties) node_props = NULL;
   WpProperties *result;
 
-  result = wp_properties_new_empty ();
+  result = wp_properties_new (
+      PW_KEY_MEDIA_ROLE, self->role,
+      "endpoint.priority", self->priority,
+      NULL);
 
   /* copy useful properties from the node */
   node_props = wp_proxy_get_properties (WP_PROXY (self->node));
@@ -464,20 +447,22 @@ si_adapter_get_stream (WpSiEndpoint * item, guint index)
 static void
 si_adapter_endpoint_init (WpSiEndpointInterface * iface)
 {
-  iface->get_name = si_adapter_get_name;
-  iface->get_media_class = si_adapter_get_media_class;
-  iface->get_role = si_adapter_get_role;
-  iface->get_direction = si_adapter_get_direction;
-  iface->get_priority = si_adapter_get_priority;
+  iface->get_registration_info = si_adapter_get_registration_info;
   iface->get_properties = si_adapter_get_properties;
   iface->get_n_streams = si_adapter_get_n_streams;
   iface->get_stream = si_adapter_get_stream;
 }
 
-static const gchar *
-si_adapter_get_stream_name (WpSiStream * self)
+static GVariant *
+si_adapter_get_stream_registration_info (WpSiStream * self)
 {
-  return "default";
+  GVariantBuilder b;
+
+  g_variant_builder_init (&b, G_VARIANT_TYPE ("(sa{ss})"));
+  g_variant_builder_add (&b, "s", "default");
+  g_variant_builder_add (&b, "a{ss}", NULL);
+
+  return g_variant_builder_end (&b);
 }
 
 static WpProperties *
@@ -495,7 +480,7 @@ si_adapter_get_stream_parent_endpoint (WpSiStream * self)
 static void
 si_adapter_stream_init (WpSiStreamInterface * iface)
 {
-  iface->get_name = si_adapter_get_stream_name;
+  iface->get_registration_info = si_adapter_get_stream_registration_info;
   iface->get_properties = si_adapter_get_stream_properties;
   iface->get_parent_endpoint = si_adapter_get_stream_parent_endpoint;
 }
diff --git a/tests/wp/endpoint.c b/tests/wp/endpoint.c
index c7fc6808..b292a79f 100644
--- a/tests/wp/endpoint.c
+++ b/tests/wp/endpoint.c
@@ -12,6 +12,108 @@
 
 #include "test-server.h"
 
+struct _TestSiEndpoint
+{
+  WpSessionItem parent;
+  const gchar *name;
+  const gchar *media_class;
+  WpDirection direction;
+};
+
+G_DECLARE_FINAL_TYPE (TestSiEndpoint, test_si_endpoint,
+                      TEST, SI_ENDPOINT, WpSessionItem)
+
+static GVariant *
+test_si_endpoint_get_registration_info (WpSiEndpoint * item)
+{
+  TestSiEndpoint *self = TEST_SI_ENDPOINT (item);
+  GVariantBuilder b;
+
+  g_variant_builder_init (&b, G_VARIANT_TYPE ("(ssya{ss})"));
+  g_variant_builder_add (&b, "s", self->name);
+  g_variant_builder_add (&b, "s", self->media_class);
+  g_variant_builder_add (&b, "y", (guchar) self->direction);
+  g_variant_builder_add (&b, "a{ss}", NULL);
+
+  return g_variant_builder_end (&b);
+}
+
+static WpProperties *
+test_si_endpoint_get_properties (WpSiEndpoint * item)
+{
+  return wp_properties_new ("test.property", "test-value", NULL);
+}
+
+static guint
+test_si_endpoint_get_n_streams (WpSiEndpoint * item)
+{
+  return 1;
+}
+
+static WpSiStream *
+test_si_endpoint_get_stream (WpSiEndpoint * item, guint index)
+{
+  g_return_val_if_fail (index == 0, NULL);
+  return WP_SI_STREAM (item);
+}
+
+static void
+test_si_endpoint_endpoint_init (WpSiEndpointInterface * iface)
+{
+  iface->get_registration_info = test_si_endpoint_get_registration_info;
+  iface->get_properties = test_si_endpoint_get_properties;
+  iface->get_n_streams = test_si_endpoint_get_n_streams;
+  iface->get_stream = test_si_endpoint_get_stream;
+}
+
+static GVariant *
+test_si_endpoint_get_stream_registration_info (WpSiStream * self)
+{
+  GVariantBuilder b;
+
+  g_variant_builder_init (&b, G_VARIANT_TYPE ("(sa{ss})"));
+  g_variant_builder_add (&b, "s", "default");
+  g_variant_builder_add (&b, "a{ss}", NULL);
+
+  return g_variant_builder_end (&b);
+}
+
+static WpProperties *
+test_si_endpoint_get_stream_properties (WpSiStream * self)
+{
+  return wp_properties_new ("stream.property", "test-value-2", NULL);
+}
+
+static WpSiEndpoint *
+test_si_endpoint_get_stream_parent_endpoint (WpSiStream * self)
+{
+  return WP_SI_ENDPOINT (self);
+}
+
+static void
+test_si_endpoint_stream_init (WpSiStreamInterface * iface)
+{
+  iface->get_registration_info = test_si_endpoint_get_stream_registration_info;
+  iface->get_properties = test_si_endpoint_get_stream_properties;
+  iface->get_parent_endpoint = test_si_endpoint_get_stream_parent_endpoint;
+}
+
+G_DEFINE_TYPE_WITH_CODE (TestSiEndpoint, test_si_endpoint, WP_TYPE_SESSION_ITEM,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_SI_ENDPOINT, test_si_endpoint_endpoint_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_SI_STREAM, test_si_endpoint_stream_init))
+
+static void
+test_si_endpoint_init (TestSiEndpoint * self)
+{
+}
+
+static void
+test_si_endpoint_class_init (TestSiEndpointClass * klass)
+{
+}
+
+/*******************/
+
 typedef struct {
   /* the local pipewire server */
   WpTestServer server;
@@ -29,7 +131,7 @@ typedef struct {
   WpCore *proxy_core;
   WpObjectManager *proxy_om;
 
-  WpImplEndpoint *impl_endpoint;
+  WpProxy *impl_endpoint;
   WpProxy *proxy_endpoint;
 
   gint n_events;
@@ -114,10 +216,11 @@ test_endpoint_basic_impl_object_added (WpObjectManager *om,
 {
   g_debug ("impl object added");
 
-  g_assert_true (WP_IS_IMPL_ENDPOINT (endpoint));
+  g_assert_true (WP_IS_ENDPOINT (endpoint));
+  g_assert_cmpstr (G_OBJECT_TYPE_NAME (endpoint), ==, "WpImplEndpoint");
 
   g_assert_null (fixture->impl_endpoint);
-  fixture->impl_endpoint = WP_IMPL_ENDPOINT (endpoint);
+  fixture->impl_endpoint = WP_PROXY (endpoint);
 
   if (++fixture->n_events == 3)
     g_main_loop_quit (fixture->loop);
@@ -129,7 +232,8 @@ test_endpoint_basic_impl_object_removed (WpObjectManager *om,
 {
   g_debug ("impl object removed");
 
-  g_assert_true (WP_IS_IMPL_ENDPOINT (endpoint));
+  g_assert_true (WP_IS_ENDPOINT (endpoint));
+  g_assert_cmpstr (G_OBJECT_TYPE_NAME (endpoint), ==, "WpImplEndpoint");
 
   g_assert_nonnull (fixture->impl_endpoint);
   fixture->impl_endpoint = NULL;
@@ -145,6 +249,7 @@ test_endpoint_basic_proxy_object_added (WpObjectManager *om,
   g_debug ("proxy object added");
 
   g_assert_true (WP_IS_ENDPOINT (endpoint));
+  g_assert_cmpstr (G_OBJECT_TYPE_NAME (endpoint), ==, "WpEndpoint");
 
   g_assert_null (fixture->proxy_endpoint);
   fixture->proxy_endpoint = WP_PROXY (endpoint);
@@ -160,6 +265,7 @@ test_endpoint_basic_proxy_object_removed (WpObjectManager *om,
   g_debug ("proxy object removed");
 
   g_assert_true (WP_IS_ENDPOINT (endpoint));
+  g_assert_cmpstr (G_OBJECT_TYPE_NAME (endpoint), ==, "WpEndpoint");
 
   g_assert_nonnull (fixture->proxy_endpoint);
   fixture->proxy_endpoint = NULL;
@@ -169,22 +275,49 @@ test_endpoint_basic_proxy_object_removed (WpObjectManager *om,
 }
 
 static void
-test_endpoint_basic_export_done (WpProxy * endpoint, GAsyncResult * res,
+test_endpoint_basic_activate_done (WpSessionItem * item, GAsyncResult * res,
     TestEndpointFixture *fixture)
 {
   g_autoptr (GError) error = NULL;
 
-  g_debug ("export done");
+  g_debug ("activate done");
 
-  g_assert_true (wp_proxy_augment_finish (endpoint, res, &error));
+  g_assert_true (wp_session_item_activate_finish (item, res, &error));
   g_assert_no_error (error);
+}
+
+static void
+test_endpoint_basic_export_done (WpSessionItem * item, GAsyncResult * res,
+    TestEndpointFixture *fixture)
+{
+  g_autoptr (GError) error = NULL;
+
+  g_debug ("export done");
 
-  g_assert_true (WP_IS_IMPL_ENDPOINT (endpoint));
+  g_assert_true (wp_session_item_export_finish (item, res, &error));
+  g_assert_no_error (error);
 
   if (++fixture->n_events == 3)
     g_main_loop_quit (fixture->loop);
 }
 
+static void
+test_endpoint_basic_session_bound (WpProxy * session, GAsyncResult * res,
+    TestEndpointFixture *fixture)
+{
+  g_autoptr (GError) error = NULL;
+
+  g_debug ("session export done");
+
+  g_assert_true (wp_proxy_augment_finish (session, res, &error));
+  g_assert_no_error (error);
+
+  g_assert_true (WP_IS_IMPL_SESSION (session));
+
+  g_main_loop_quit (fixture->loop);
+}
+
+#if 0
 static void
 test_endpoint_basic_control_changed (WpEndpoint * endpoint,
     guint32 control_id, TestEndpointFixture *fixture)
@@ -209,13 +342,15 @@ test_endpoint_basic_notify_properties (WpEndpoint * endpoint, GParamSpec * param
   if (++fixture->n_events == 2)
     g_main_loop_quit (fixture->loop);
 }
+#endif
 
 static void
 test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
 {
-  g_autoptr (WpImplEndpoint) endpoint = NULL;
-  gfloat float_value;
-  gboolean boolean_value;
+  g_autoptr (TestSiEndpoint) endpoint = NULL;
+  g_autoptr (WpImplSession) session = NULL;
+  // gfloat float_value;
+  // gboolean boolean_value;
 
   /* set up the export side */
   g_signal_connect (fixture->export_om, "object-added",
@@ -223,7 +358,7 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
   g_signal_connect (fixture->export_om, "object-removed",
       (GCallback) test_endpoint_basic_impl_object_removed, fixture);
   wp_object_manager_add_interest (fixture->export_om,
-      WP_TYPE_IMPL_ENDPOINT, NULL,
+      WP_TYPE_ENDPOINT, NULL,
       WP_PROXY_FEATURES_STANDARD | WP_ENDPOINT_FEATURE_CONTROLS);
   wp_core_install_object_manager (fixture->export_core, fixture->export_om);
 
@@ -241,32 +376,27 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
 
   g_assert_true (wp_core_connect (fixture->proxy_core));
 
-  /* create endpoint */
-  endpoint = wp_impl_endpoint_new (fixture->export_core);
-  wp_impl_endpoint_set_property (endpoint, "test.property", "test-value");
-  wp_impl_endpoint_register_control (endpoint, WP_ENDPOINT_CONTROL_VOLUME);
-  wp_impl_endpoint_register_control (endpoint, WP_ENDPOINT_CONTROL_MUTE);
-  g_assert_true (wp_endpoint_set_control_float (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_VOLUME, 0.7f));
-  g_assert_true (wp_endpoint_set_control_boolean (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_MUTE, TRUE));
+  /* create session */
+  session = wp_impl_session_new (fixture->export_core);
+  wp_proxy_augment (WP_PROXY (session), WP_PROXY_FEATURE_BOUND, NULL,
+      (GAsyncReadyCallback) test_endpoint_basic_session_bound, fixture);
 
-  /* verify properties are set before export */
-  {
-    g_autoptr (WpProperties) props =
-        wp_proxy_get_properties (WP_PROXY (endpoint));
-    g_assert_cmpstr (wp_properties_get (props, "test.property"), ==,
-        "test-value");
-  }
-  g_assert_true (wp_endpoint_get_control_float (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_VOLUME, &float_value));
-  g_assert_true (wp_endpoint_get_control_boolean (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_MUTE, &boolean_value));
-  g_assert_cmpfloat_with_epsilon (float_value, 0.7f, 0.001);
-  g_assert_cmpint (boolean_value, ==, TRUE);
+  /* run until session is bound */
+  g_main_loop_run (fixture->loop);
+  g_assert_cmpint (wp_proxy_get_features (WP_PROXY (session)), &,
+      WP_PROXY_FEATURE_BOUND);
+  g_assert_cmpint (wp_proxy_get_bound_id (WP_PROXY (session)), >, 0);
 
-  /* do export */
-  wp_proxy_augment (WP_PROXY (endpoint), WP_PROXY_FEATURE_BOUND, NULL,
+  /* create endpoint */
+  endpoint = g_object_new (test_si_endpoint_get_type (), NULL);
+  endpoint->name = "test-endpoint";
+  endpoint->media_class = "Audio/Source";
+  endpoint->direction = WP_DIRECTION_OUTPUT;
+  wp_session_item_activate (WP_SESSION_ITEM (endpoint),
+      (GAsyncReadyCallback) test_endpoint_basic_activate_done, fixture);
+  g_assert_cmpint (wp_session_item_get_flags (WP_SESSION_ITEM (endpoint)),
+      &, WP_SI_FLAG_ACTIVE);
+  wp_session_item_export (WP_SESSION_ITEM (endpoint), WP_SESSION (session),
       (GAsyncReadyCallback) test_endpoint_basic_export_done, fixture);
 
   /* run until objects are created and features are cached */
@@ -275,7 +405,6 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
   g_assert_cmpint (fixture->n_events, ==, 3);
   g_assert_nonnull (fixture->impl_endpoint);
   g_assert_nonnull (fixture->proxy_endpoint);
-  g_assert_true (fixture->impl_endpoint == endpoint);
 
   /* test round 1: verify the values on the proxy */
 
@@ -286,14 +415,38 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
       WP_ENDPOINT_FEATURE_CONTROLS);
 
   g_assert_cmpuint (wp_proxy_get_bound_id (fixture->proxy_endpoint), ==,
-      wp_proxy_get_bound_id (WP_PROXY (endpoint)));
+      wp_proxy_get_bound_id (fixture->impl_endpoint));
 
   {
     g_autoptr (WpProperties) props =
         wp_proxy_get_properties (fixture->proxy_endpoint);
+
     g_assert_cmpstr (wp_properties_get (props, "test.property"), ==,
         "test-value");
   }
+
+  {
+    g_autoptr (WpProperties) props =
+        wp_proxy_get_global_properties (fixture->proxy_endpoint);
+    g_autofree gchar * session_id = g_strdup_printf ("%u",
+        wp_proxy_get_bound_id (WP_PROXY (session)));
+
+    g_assert_cmpstr (wp_properties_get (props, PW_KEY_ENDPOINT_NAME), ==,
+        "test-endpoint");
+    g_assert_cmpstr (wp_properties_get (props, PW_KEY_MEDIA_CLASS), ==,
+        "Audio/Source");
+    g_assert_cmpstr (wp_properties_get (props, PW_KEY_SESSION_ID), ==,
+        session_id);
+  }
+
+  g_assert_cmpstr ("test-endpoint", ==,
+      wp_endpoint_get_name (WP_ENDPOINT (fixture->proxy_endpoint)));
+  g_assert_cmpstr ("Audio/Source", ==,
+      wp_endpoint_get_media_class (WP_ENDPOINT (fixture->proxy_endpoint)));
+  g_assert_cmpint (WP_DIRECTION_OUTPUT, ==,
+      wp_endpoint_get_direction (WP_ENDPOINT (fixture->proxy_endpoint)));
+
+#if 0
   g_assert_true (wp_endpoint_get_control_float (
           WP_ENDPOINT (fixture->proxy_endpoint),
           WP_ENDPOINT_CONTROL_VOLUME, &float_value));
@@ -390,6 +543,7 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
     g_assert_cmpstr (wp_properties_get (props, "test.property"), ==,
         "changed-value");
   }
+#endif
 
   /* destroy impl endpoint */
   fixture->n_events = 0;
-- 
GitLab