From bd65517b7d5adb2e944f824f306aeebc8e0d887b Mon Sep 17 00:00:00 2001
From: George Kiagiadakis <george.kiagiadakis@collabora.com>
Date: Wed, 25 Nov 2020 14:02:33 +0200
Subject: [PATCH] pw-object-mixin: refactor, implement param caching and
 features for impl objects

Now the WpPipewireObject interface is directly implemented by the mixin
and there is another interface that users of the mixin must implement
in order for the mixin to work proprely.

A lot of manual stuff that proxy classes had to do before are now
in the mixin. Also most of the data that would normally reside in Private
structures is now in the mixin data structure (stored as qdata on the object).
This is achieving the best amount of code reuse so far.

For impl objects (WpImpl*) there are also default implementations of the
standard pipewire object methods and the INFO & PARAM_* features are
more coherently enabled during the whole lifetime of these objects.
---
 lib/wp/client.c                               |  97 +-
 lib/wp/device.c                               | 136 +--
 lib/wp/endpoint-link.c                        | 544 ++++------
 lib/wp/endpoint-stream.c                      | 562 +++++-----
 lib/wp/endpoint.c                             | 609 ++++++-----
 lib/wp/global-proxy.c                         |   2 +
 lib/wp/link.c                                 | 111 +-
 lib/wp/node.c                                 | 183 ++--
 lib/wp/port.c                                 | 131 +--
 lib/wp/private/pipewire-object-mixin.c        | 968 +++++++++++++++---
 lib/wp/private/pipewire-object-mixin.h        | 272 +++--
 lib/wp/proxy-interfaces.c                     |  40 +-
 lib/wp/proxy-interfaces.h                     |  18 +-
 lib/wp/session.c                              | 342 +++----
 .../module-dbus-reservation/reserve-device.c  |   2 +-
 modules/module-device-activation.c            |   2 +-
 modules/module-si-adapter.c                   |   4 +-
 modules/module-si-bluez5-endpoint.c           |   3 +-
 modules/module-si-convert.c                   |  30 +-
 19 files changed, 2200 insertions(+), 1856 deletions(-)

diff --git a/lib/wp/client.c b/lib/wp/client.c
index 10b08739..5622e3ad 100644
--- a/lib/wp/client.c
+++ b/lib/wp/client.c
@@ -19,11 +19,10 @@
 struct _WpClient
 {
   WpGlobalProxy parent;
-  struct pw_client_info *info;
-  struct spa_hook listener;
 };
 
-static void wp_client_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_client_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpClient:
@@ -34,67 +33,46 @@ static void wp_client_pipewire_object_interface_init (WpPipewireObjectInterface
  * #WpObjectManager API.
  */
 G_DEFINE_TYPE_WITH_CODE (WpClient, wp_client, WP_TYPE_GLOBAL_PROXY,
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_client_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_client_pw_object_mixin_priv_interface_init))
 
 static void
 wp_client_init (WpClient * self)
 {
 }
 
-static WpObjectFeatures
-wp_client_get_supported_features (WpObject * object)
-{
-  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO;
-}
-
 static void
 wp_client_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    /* just wait, info will be emitted anyway after binding */
-    break;
-  default:
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
     WP_OBJECT_CLASS (wp_client_parent_class)->
         activate_execute_step (object, transition, step, missing);
     break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  default:
+    g_assert_not_reached ();
   }
 }
 
-static void
-client_event_info(void *data, const struct pw_client_info *info)
-{
-  WpClient *self = WP_CLIENT (data);
-
-  self->info = pw_client_info_update (self->info, info);
-  wp_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_CLIENT_CHANGE_MASK_PROPS, 0);
-}
-
 static const struct pw_client_events client_events = {
   PW_VERSION_CLIENT_EVENTS,
-  .info = client_event_info,
+  .info = (HandleEventInfoFunc(client)) wp_pw_object_mixin_handle_event_info,
 };
 
 static void
 wp_client_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
-  WpClient *self = WP_CLIENT (proxy);
-  pw_client_add_listener ((struct pw_port *) pw_proxy,
-      &self->listener, &client_events, self);
-}
-
-static void
-wp_client_pw_proxy_destroyed (WpProxy * proxy)
-{
-  g_clear_pointer (&WP_CLIENT (proxy)->info, pw_client_info_free);
-  wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      client, &client_events);
 }
 
 static void
@@ -104,49 +82,28 @@ wp_client_class_init (WpClientClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->get_property = wp_pipewire_object_mixin_get_property;
+  object_class->get_property = wp_pw_object_mixin_get_property;
 
-  wpobject_class->get_supported_features = wp_client_get_supported_features;
+  wpobject_class->get_supported_features =
+      wp_pw_object_mixin_get_supported_features;
   wpobject_class->activate_get_next_step =
-      wp_pipewire_object_mixin_activate_get_next_step;
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step = wp_client_activate_execute_step;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Client;
   proxy_class->pw_iface_version = PW_VERSION_CLIENT;
   proxy_class->pw_proxy_created = wp_client_pw_proxy_created;
-  proxy_class->pw_proxy_destroyed = wp_client_pw_proxy_destroyed;
-
-  wp_pipewire_object_mixin_class_override_properties (object_class);
-}
+  proxy_class->pw_proxy_destroyed =
+      wp_pw_object_mixin_handle_pw_proxy_destroyed;
 
-static gconstpointer
-wp_client_get_native_info (WpPipewireObject * obj)
-{
-  return WP_CLIENT (obj)->info;
-}
-
-static WpProperties *
-wp_client_get_properties (WpPipewireObject * obj)
-{
-  return wp_properties_new_wrap_dict (WP_CLIENT (obj)->info->props);
-}
-
-static GVariant *
-wp_client_get_param_info (WpPipewireObject * obj)
-{
-  return NULL;
+  wp_pw_object_mixin_class_override_properties (object_class);
 }
 
 static void
-wp_client_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+wp_client_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_client_get_native_info;
-  iface->get_properties = wp_client_get_properties;
-  iface->get_param_info = wp_client_get_param_info;
-  iface->enum_params = wp_pipewire_object_mixin_enum_params_unimplemented;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
-  iface->set_param = wp_pipewire_object_mixin_set_param_unimplemented;
+  wp_pw_object_mixin_priv_interface_info_init_no_params (iface, client, CLIENT);
 }
 
 /**
diff --git a/lib/wp/device.c b/lib/wp/device.c
index 2a5ab8de..be96d1d9 100644
--- a/lib/wp/device.c
+++ b/lib/wp/device.c
@@ -16,19 +16,20 @@
 #include "device.h"
 #include "node.h"
 #include "core.h"
+#include "debug.h"
 #include "private/pipewire-object-mixin.h"
 
 #include <pipewire/impl.h>
 #include <spa/monitor/device.h>
+#include <spa/utils/result.h>
 
 struct _WpDevice
 {
   WpGlobalProxy parent;
-  struct pw_device_info *info;
-  struct spa_hook listener;
 };
 
-static void wp_device_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_device_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpDevice:
@@ -43,84 +44,57 @@ static void wp_device_pipewire_object_interface_init (WpPipewireObjectInterface
  * on the remote PipeWire server by calling into a factory.
  */
 G_DEFINE_TYPE_WITH_CODE (WpDevice, wp_device, WP_TYPE_GLOBAL_PROXY,
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_device_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_device_pw_object_mixin_priv_interface_init));
 
 static void
 wp_device_init (WpDevice * self)
 {
 }
 
-static WpObjectFeatures
-wp_device_get_supported_features (WpObject * object)
-{
-  WpDevice *self = WP_DEVICE (object);
-
-  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO |
-      wp_pipewire_object_mixin_param_info_to_features (
-          self->info ? self->info->params : NULL,
-          self->info ? self->info->n_params : 0);
-}
-
 static void
 wp_device_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    wp_pipewire_object_mixin_cache_info (object, transition);
-    break;
-  default:
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
     WP_OBJECT_CLASS (wp_device_parent_class)->
         activate_execute_step (object, transition, step, missing);
     break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
+    wp_pw_object_mixin_cache_params (object, missing);
+    break;
+  default:
+    g_assert_not_reached ();
   }
 }
 
 static void
 wp_device_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  wp_pipewire_object_mixin_deactivate (object, features);
-
+  wp_pw_object_mixin_deactivate (object, features);
   WP_OBJECT_CLASS (wp_device_parent_class)->deactivate (object, features);
 }
 
-static void
-device_event_info(void *data, const struct pw_device_info *info)
-{
-  WpDevice *self = WP_DEVICE (data);
-
-  self->info = pw_device_info_update (self->info, info);
-  wp_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_DEVICE_CHANGE_MASK_PROPS, PW_DEVICE_CHANGE_MASK_PARAMS);
-}
-
 static const struct pw_device_events device_events = {
   PW_VERSION_DEVICE_EVENTS,
-  .info = device_event_info,
-  .param = wp_pipewire_object_mixin_handle_event_param,
+  .info = (HandleEventInfoFunc(device)) wp_pw_object_mixin_handle_event_info,
+  .param = wp_pw_object_mixin_handle_event_param,
 };
 
 static void
 wp_device_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
-  WpDevice *self = WP_DEVICE (proxy);
-  pw_device_add_listener ((struct pw_port *) pw_proxy,
-      &self->listener, &device_events, self);
-}
-
-static void
-wp_device_pw_proxy_destroyed (WpProxy * proxy)
-{
-  g_clear_pointer (&WP_DEVICE (proxy)->info, pw_device_info_free);
-  wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
-
-  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
-      WP_OBJECT_FEATURES_ALL);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      device, &device_events);
 }
 
 static void
@@ -130,66 +104,48 @@ wp_device_class_init (WpDeviceClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->get_property = wp_pipewire_object_mixin_get_property;
+  object_class->get_property = wp_pw_object_mixin_get_property;
 
-  wpobject_class->get_supported_features = wp_device_get_supported_features;
+  wpobject_class->get_supported_features =
+      wp_pw_object_mixin_get_supported_features;
   wpobject_class->activate_get_next_step =
-      wp_pipewire_object_mixin_activate_get_next_step;
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step = wp_device_activate_execute_step;
   wpobject_class->deactivate = wp_device_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Device;
   proxy_class->pw_iface_version = PW_VERSION_DEVICE;
   proxy_class->pw_proxy_created = wp_device_pw_proxy_created;
-  proxy_class->pw_proxy_destroyed = wp_device_pw_proxy_destroyed;
+  proxy_class->pw_proxy_destroyed =
+      wp_pw_object_mixin_handle_pw_proxy_destroyed;
 
-  wp_pipewire_object_mixin_class_override_properties (object_class);
+  wp_pw_object_mixin_class_override_properties (object_class);
 }
 
-static gconstpointer
-wp_device_get_native_info (WpPipewireObject * obj)
+static gint
+wp_device_enum_params (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
 {
-  return WP_DEVICE (obj)->info;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_device_enum_params (d->iface, 0, id, start, num,
+      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
 }
 
-static WpProperties *
-wp_device_get_properties (WpPipewireObject * obj)
-{
-  return wp_properties_new_wrap_dict (WP_DEVICE (obj)->info->props);
-}
-
-static GVariant *
-wp_device_get_param_info (WpPipewireObject * obj)
-{
-  WpDevice *self = WP_DEVICE (obj);
-  return wp_pipewire_object_mixin_param_info_to_gvariant (self->info->params,
-      self->info->n_params);
-}
-
-static void
-wp_device_enum_params (WpPipewireObject * obj, const gchar * id,
-    WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
-{
-  wp_pipewire_object_mixin_enum_params (pw_device, obj, id, filter, cancellable,
-      callback, user_data);
-}
-
-static void
-wp_device_set_param (WpPipewireObject * obj, const gchar * id, WpSpaPod * param)
+static gint
+wp_device_set_param (gpointer instance, guint32 id, guint32 flags,
+    WpSpaPod * param)
 {
-  wp_pipewire_object_mixin_set_param (pw_device, obj, id, param);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_device_set_param (d->iface, id, flags,
+      wp_spa_pod_get_spa_pod (param));
 }
 
 static void
-wp_device_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+wp_device_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_device_get_native_info;
-  iface->get_properties = wp_device_get_properties;
-  iface->get_param_info = wp_device_get_param_info;
+  wp_pw_object_mixin_priv_interface_info_init (iface, device, DEVICE);
   iface->enum_params = wp_device_enum_params;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
   iface->set_param = wp_device_set_param;
 }
 
diff --git a/lib/wp/endpoint-link.c b/lib/wp/endpoint-link.c
index 209c6eb3..4262cd4e 100644
--- a/lib/wp/endpoint-link.c
+++ b/lib/wp/endpoint-link.c
@@ -14,6 +14,7 @@
 #define G_LOG_DOMAIN "wp-endpoint-link"
 
 #include "endpoint-link.h"
+#include "debug.h"
 #include "error.h"
 #include "wpenums.h"
 #include "private/impl-endpoint.h"
@@ -29,16 +30,8 @@ enum {
 
 static guint32 signals[N_SIGNALS] = {0};
 
-typedef struct _WpEndpointLinkPrivate WpEndpointLinkPrivate;
-struct _WpEndpointLinkPrivate
-{
-  WpProperties *properties;
-  struct pw_endpoint_link_info *info;
-  struct pw_endpoint_link *iface;
-  struct spa_hook listener;
-};
-
-static void wp_endpoint_link_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_endpoint_link_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpEndpointLink:
@@ -52,107 +45,57 @@ static void wp_endpoint_link_pipewire_object_interface_init (WpPipewireObjectInt
  * #WpObjectManager API.
  */
 G_DEFINE_TYPE_WITH_CODE (WpEndpointLink, wp_endpoint_link, WP_TYPE_GLOBAL_PROXY,
-    G_ADD_PRIVATE (WpEndpointLink)
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_endpoint_link_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_endpoint_link_pw_object_mixin_priv_interface_init));
 
 static void
 wp_endpoint_link_init (WpEndpointLink * self)
 {
 }
 
-static WpObjectFeatures
-wp_endpoint_link_get_supported_features (WpObject * object)
-{
-  WpEndpointLink *self = WP_ENDPOINT_LINK (object);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-
-  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO |
-      wp_pipewire_object_mixin_param_info_to_features (
-          priv->info ? priv->info->params : NULL,
-          priv->info ? priv->info->n_params : 0);
-}
-
 static void
 wp_endpoint_link_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    wp_pipewire_object_mixin_cache_info (object, transition);
-    break;
-  default:
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
     WP_OBJECT_CLASS (wp_endpoint_link_parent_class)->
         activate_execute_step (object, transition, step, missing);
     break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
+    wp_pw_object_mixin_cache_params (object, missing);
+    break;
+  default:
+    g_assert_not_reached ();
   }
 }
 
 static void
 wp_endpoint_link_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  wp_pipewire_object_mixin_deactivate (object, features);
-
+  wp_pw_object_mixin_deactivate (object, features);
   WP_OBJECT_CLASS (wp_endpoint_link_parent_class)->deactivate (object, features);
 }
 
-static void
-endpoint_link_event_info (void *data, const struct pw_endpoint_link_info *info)
-{
-  WpEndpointLink *self = WP_ENDPOINT_LINK (data);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-  WpEndpointLinkState old_state = priv->info ?
-      (WpEndpointLinkState) priv->info->state : WP_ENDPOINT_LINK_STATE_ERROR;
-
-  priv->info = pw_endpoint_link_info_update (priv->info, info);
-
-  if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PROPS) {
-    g_clear_pointer (&priv->properties, wp_properties_unref);
-    priv->properties = wp_properties_new_wrap_dict (priv->info->props);
-  }
-
-  wp_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_STATE) {
-    g_signal_emit (self, signals[SIGNAL_STATE_CHANGED], 0,
-        old_state, info->state, info->error);
-  }
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_ENDPOINT_LINK_CHANGE_MASK_PROPS, PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS);
-}
-
 static const struct pw_endpoint_link_events endpoint_link_events = {
   PW_VERSION_ENDPOINT_LINK_EVENTS,
-  .info = endpoint_link_event_info,
-  .param = wp_pipewire_object_mixin_handle_event_param,
+  .info = (HandleEventInfoFunc(endpoint_link)) wp_pw_object_mixin_handle_event_info,
+  .param = wp_pw_object_mixin_handle_event_param,
 };
 
 static void
 wp_endpoint_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
-  WpEndpointLink *self = WP_ENDPOINT_LINK (proxy);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-
-  priv->iface = (struct pw_endpoint_link *) pw_proxy;
-  pw_endpoint_link_add_listener (priv->iface, &priv->listener,
-      &endpoint_link_events, self);
-}
-
-static void
-wp_endpoint_link_pw_proxy_destroyed (WpProxy * proxy)
-{
-  WpEndpointLink *self = WP_ENDPOINT_LINK (proxy);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&priv->info, pw_endpoint_link_info_free);
-  wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
-
-  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
-      WP_OBJECT_FEATURES_ALL);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      endpoint_link, &endpoint_link_events);
 }
 
 static void
@@ -162,12 +105,12 @@ wp_endpoint_link_class_init (WpEndpointLinkClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->get_property = wp_pipewire_object_mixin_get_property;
+  object_class->get_property = wp_pw_object_mixin_get_property;
 
   wpobject_class->get_supported_features =
-      wp_endpoint_link_get_supported_features;
+      wp_pw_object_mixin_get_supported_features;
   wpobject_class->activate_get_next_step =
-      wp_pipewire_object_mixin_activate_get_next_step;
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step =
       wp_endpoint_link_activate_execute_step;
   wpobject_class->deactivate = wp_endpoint_link_deactivate;
@@ -175,9 +118,10 @@ wp_endpoint_link_class_init (WpEndpointLinkClass * klass)
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_EndpointLink;
   proxy_class->pw_iface_version = PW_VERSION_ENDPOINT_LINK;
   proxy_class->pw_proxy_created = wp_endpoint_link_pw_proxy_created;
-  proxy_class->pw_proxy_destroyed = wp_endpoint_link_pw_proxy_destroyed;
+  proxy_class->pw_proxy_destroyed =
+      wp_pw_object_mixin_handle_pw_proxy_destroyed;
 
-  wp_pipewire_object_mixin_class_override_properties (object_class);
+  wp_pw_object_mixin_class_override_properties (object_class);
 
   /**
    * WpEndpointLink::state-changed:
@@ -195,60 +139,46 @@ wp_endpoint_link_class_init (WpEndpointLinkClass * klass)
       WP_TYPE_ENDPOINT_LINK_STATE, WP_TYPE_ENDPOINT_LINK_STATE, G_TYPE_STRING);
 }
 
-static gconstpointer
-wp_endpoint_link_get_native_info (WpPipewireObject * obj)
-{
-  WpEndpointLink *self = WP_ENDPOINT_LINK (obj);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-
-  return priv->info;
-}
-
-static WpProperties *
-wp_endpoint_link_get_properties (WpPipewireObject * obj)
-{
-  WpEndpointLink *self = WP_ENDPOINT_LINK (obj);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-
-  return wp_properties_ref (priv->properties);
-}
-
-static GVariant *
-wp_endpoint_link_get_param_info (WpPipewireObject * obj)
+static void
+wp_endpoint_link_process_info (gpointer instance, gpointer old_info, gpointer i)
 {
-  WpEndpointLink *self = WP_ENDPOINT_LINK (obj);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
+  const struct pw_endpoint_link_info *info = i;
 
-  return wp_pipewire_object_mixin_param_info_to_gvariant (priv->info->params,
-      priv->info->n_params);
+  if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_STATE) {
+    WpEndpointLinkState old_state = old_info ?
+        (WpEndpointLinkState) ((struct pw_endpoint_link_info *) old_info)->state
+        : WP_ENDPOINT_LINK_STATE_ERROR;
+    g_signal_emit (instance, signals[SIGNAL_STATE_CHANGED], 0,
+        old_state, info->state, info->error);
+  }
 }
 
-static void
-wp_endpoint_link_enum_params (WpPipewireObject * obj, const gchar * id,
-    WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
+static gint
+wp_endpoint_link_enum_params (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
 {
-  wp_pipewire_object_mixin_enum_params (pw_endpoint_link, obj, id, filter,
-      cancellable, callback, user_data);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_endpoint_link_enum_params (d->iface, 0, id, start, num,
+      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
 }
 
-static void
-wp_endpoint_link_set_param (WpPipewireObject * obj, const gchar * id,
+static gint
+wp_endpoint_link_set_param (gpointer instance, guint32 id, guint32 flags,
     WpSpaPod * param)
 {
-  wp_pipewire_object_mixin_set_param (pw_endpoint_link, obj, id, param);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_endpoint_link_set_param (d->iface, id, flags,
+      wp_spa_pod_get_spa_pod (param));
 }
 
 static void
-wp_endpoint_link_pipewire_object_interface_init (
-    WpPipewireObjectInterface * iface)
+wp_endpoint_link_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_endpoint_link_get_native_info;
-  iface->get_properties = wp_endpoint_link_get_properties;
-  iface->get_param_info = wp_endpoint_link_get_param_info;
+  wp_pw_object_mixin_priv_interface_info_init (iface,
+      endpoint_link, ENDPOINT_LINK);
+  iface->process_info = wp_endpoint_link_process_info;
   iface->enum_params = wp_endpoint_link_enum_params;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
   iface->set_param = wp_endpoint_link_set_param;
 }
 
@@ -275,17 +205,18 @@ wp_endpoint_link_get_linked_object_ids (WpEndpointLink * self,
 {
   g_return_if_fail (WP_IS_ENDPOINT_LINK (self));
 
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-  g_return_if_fail (priv->info);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  struct pw_endpoint_link_info *info = d->info;
+  g_return_if_fail (info);
 
   if (output_endpoint)
-    *output_endpoint = priv->info->output_endpoint_id;
+    *output_endpoint = info->output_endpoint_id;
   if (output_stream)
-    *output_stream = priv->info->output_stream_id;
+    *output_stream = info->output_stream_id;
   if (input_endpoint)
-    *input_endpoint = priv->info->input_endpoint_id;
+    *input_endpoint = info->input_endpoint_id;
   if (input_stream)
-    *input_stream = priv->info->input_stream_id;
+    *input_stream = info->input_stream_id;
 }
 
 /**
@@ -304,12 +235,13 @@ wp_endpoint_link_get_state (WpEndpointLink * self, const gchar ** error)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT_LINK (self), WP_ENDPOINT_LINK_STATE_ERROR);
 
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-  g_return_val_if_fail (priv->info, WP_ENDPOINT_LINK_STATE_ERROR);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  struct pw_endpoint_link_info *info = d->info;
+  g_return_val_if_fail (info, WP_ENDPOINT_LINK_STATE_ERROR);
 
   if (error)
-    *error = priv->info->error;
-  return (WpEndpointLinkState) priv->info->state;
+    *error = info->error;
+  return (WpEndpointLinkState) info->state;
 }
 
 /**
@@ -327,10 +259,10 @@ wp_endpoint_link_request_state (WpEndpointLink * self,
 {
   g_return_if_fail (WP_IS_ENDPOINT_LINK (self));
 
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-  g_return_if_fail (priv->iface);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  g_return_if_fail (d->iface);
 
-  pw_endpoint_link_request_state (priv->iface,
+  pw_endpoint_link_request_state (d->iface,
       (enum pw_endpoint_link_state) target);
 }
 
@@ -346,62 +278,18 @@ struct _WpImplEndpointLink
   WpEndpointLink parent;
 
   struct spa_interface iface;
-  struct spa_hook_list hooks;
   struct pw_endpoint_link_info info;
+  WpProperties *immutable_props;
 
   WpSiLink *item;
 };
 
-G_DEFINE_TYPE (WpImplEndpointLink, wp_impl_endpoint_link, WP_TYPE_ENDPOINT_LINK)
+static void wp_endpoint_link_impl_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
-#define pw_endpoint_link_emit(hooks,method,version,...) \
-    spa_hook_list_call_simple(hooks, struct pw_endpoint_link_events, \
-        method, version, ##__VA_ARGS__)
-
-#define pw_endpoint_link_emit_info(hooks,...)  \
-    pw_endpoint_link_emit(hooks, info, 0, ##__VA_ARGS__)
-#define pw_endpoint_link_emit_param(hooks,...) \
-    pw_endpoint_link_emit(hooks, param, 0, ##__VA_ARGS__)
-
-static int
-impl_add_listener(void *object,
-    struct spa_hook *listener,
-    const struct pw_endpoint_link_events *events,
-    void *data)
-{
-  WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object);
-  struct spa_hook_list save;
-
-  spa_hook_list_isolate (&self->hooks, &save, listener, events, data);
-
-  self->info.change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_ALL;
-  pw_endpoint_link_emit_info (&self->hooks, &self->info);
-  self->info.change_mask = 0;
-
-  spa_hook_list_join (&self->hooks, &save);
-  return 0;
-}
-
-static int
-impl_enum_params (void *object, int seq,
-    uint32_t id, uint32_t start, uint32_t num,
-    const struct spa_pod *filter)
-{
-  return -ENOENT;
-}
-
-static int
-impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
-{
-  return 0;
-}
-
-static int
-impl_set_param (void *object, uint32_t id, uint32_t flags,
-    const struct spa_pod *param)
-{
-  return -ENOENT;
-}
+G_DEFINE_TYPE_WITH_CODE (WpImplEndpointLink, wp_impl_endpoint_link, WP_TYPE_ENDPOINT_LINK,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_endpoint_link_impl_pw_object_mixin_priv_interface_init))
 
 static void
 on_item_activated (WpSessionItem * item, GAsyncResult * res, gpointer data)
@@ -444,40 +332,48 @@ impl_request_state (void *object, enum pw_endpoint_link_state state)
 
 static const struct pw_endpoint_link_methods impl_endpoint_link = {
   PW_VERSION_ENDPOINT_LINK_METHODS,
-  .add_listener = impl_add_listener,
-  .subscribe_params = impl_subscribe_params,
-  .enum_params = impl_enum_params,
-  .set_param = impl_set_param,
+  .add_listener =
+      (ImplAddListenerFunc(endpoint_link)) wp_pw_object_mixin_impl_add_listener,
+  .subscribe_params = wp_pw_object_mixin_impl_subscribe_params,
+  .enum_params = wp_pw_object_mixin_impl_enum_params,
+  .set_param = wp_pw_object_mixin_impl_set_param,
   .request_state = impl_request_state,
 };
 
 static void
-populate_properties (WpImplEndpointLink * self, WpProperties *global_props)
+wp_impl_endpoint_link_init (WpImplEndpointLink * self)
 {
-  WpEndpointLinkPrivate *priv =
-      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
-
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  priv->properties = wp_si_link_get_properties (self->item);
-  if (!priv->properties)
-    priv->properties = wp_properties_new_empty ();
-  priv->properties = wp_properties_ensure_unique_owner (priv->properties);
-  wp_properties_update (priv->properties, global_props);
-
-  self->info.props = priv->properties ?
-      (struct spa_dict *) wp_properties_peek_dict (priv->properties) : NULL;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+
+  self->iface = SPA_INTERFACE_INIT (
+      PW_TYPE_INTERFACE_EndpointLink,
+      PW_VERSION_ENDPOINT_LINK,
+      &impl_endpoint_link, self);
+
+  d->info = &self->info;
+  d->iface = &self->iface;
 }
 
 static void
-on_si_link_properties_changed (WpSiLink * item, WpImplEndpointLink * self)
+populate_properties (WpImplEndpointLink * self)
 {
-  populate_properties (self,
-      wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (self)));
-  g_object_notify (G_OBJECT (self), "properties");
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+
+  g_clear_pointer (&d->properties, wp_properties_unref);
+  d->properties = wp_si_link_get_properties (self->item);
+  if (!d->properties)
+    d->properties = wp_properties_new_empty ();
+  d->properties = wp_properties_ensure_unique_owner (d->properties);
+  wp_properties_update (d->properties, self->immutable_props);
 
-  self->info.change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_PROPS;
-  pw_endpoint_link_emit_info (&self->hooks, &self->info);
-  self->info.change_mask = 0;
+  self->info.props = (struct spa_dict *) wp_properties_peek_dict (d->properties);
+}
+
+static void
+on_si_link_properties_changed (WpSiLink * item, WpImplEndpointLink * self)
+{
+  populate_properties (self);
+  wp_pw_object_mixin_notify_info (self, PW_ENDPOINT_LINK_CHANGE_MASK_PROPS);
 }
 
 static void
@@ -499,30 +395,98 @@ on_si_link_flags_changed (WpSiLink * item, WpSiFlags flags,
     g_clear_pointer (&self->info.error, g_free);
 
   if (old_state != self->info.state) {
-    self->info.change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_STATE;
-    pw_endpoint_link_emit_info (&self->hooks, &self->info);
-    self->info.change_mask = 0;
-
+    wp_pw_object_mixin_notify_info (self, PW_ENDPOINT_LINK_CHANGE_MASK_STATE);
     g_signal_emit (self, signals[SIGNAL_STATE_CHANGED], 0,
         old_state, self->info.state, self->info.error);
   }
 }
 
 static void
-wp_impl_endpoint_link_init (WpImplEndpointLink * self)
+wp_impl_endpoint_link_constructed (GObject * object)
 {
-  /* 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 */
-  WpEndpointLinkPrivate *priv =
-      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
+  WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object);
+  g_autoptr (GVariant) info = NULL;
+  g_autoptr (GVariantIter) immutable_props = NULL;
+  const gchar *key, *value;
+  WpSiStream *stream;
+
+  self->info.version = PW_VERSION_ENDPOINT_LINK_INFO;
+  self->info.error = NULL;
+  self->info.params = NULL;
+  self->info.n_params = 0;
+
+  /* get info from the interface */
+  info = wp_si_link_get_registration_info (self->item);
+  g_variant_get (info, "a{ss}", &immutable_props);
+
+  /* get the current state */
+  self->info.state =
+      (wp_session_item_get_flags (WP_SESSION_ITEM (self->item))
+          & WP_SI_FLAG_ACTIVE)
+      ? PW_ENDPOINT_LINK_STATE_ACTIVE
+      : PW_ENDPOINT_LINK_STATE_INACTIVE;
+
+  /* associate with the session, the endpoints and the streams */
+  self->info.session_id = wp_session_item_get_associated_proxy_id (
+      WP_SESSION_ITEM (self->item), WP_TYPE_SESSION);
+
+  stream = wp_si_link_get_out_stream (self->item);
+  self->info.output_endpoint_id = wp_session_item_get_associated_proxy_id (
+      WP_SESSION_ITEM (stream), WP_TYPE_ENDPOINT);
+  self->info.output_stream_id = wp_session_item_get_associated_proxy_id (
+      WP_SESSION_ITEM (stream), WP_TYPE_ENDPOINT_STREAM);
+
+  stream = wp_si_link_get_in_stream (self->item);
+  self->info.input_endpoint_id = wp_session_item_get_associated_proxy_id (
+      WP_SESSION_ITEM (stream), WP_TYPE_ENDPOINT);
+  self->info.input_stream_id = wp_session_item_get_associated_proxy_id (
+      WP_SESSION_ITEM (stream), WP_TYPE_ENDPOINT_STREAM);
+
+  /* construct export properties (these will come back through
+      the registry and appear in wp_proxy_get_global_properties) */
+  self->immutable_props = wp_properties_new_empty ();
+  wp_properties_setf (self->immutable_props,
+      PW_KEY_SESSION_ID, "%d", self->info.session_id);
+  wp_properties_setf (self->immutable_props,
+      PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT, "%d", self->info.output_endpoint_id);
+  wp_properties_setf (self->immutable_props,
+      PW_KEY_ENDPOINT_LINK_OUTPUT_STREAM, "%d", self->info.output_stream_id);
+  wp_properties_setf (self->immutable_props,
+      PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT, "%d", self->info.input_endpoint_id);
+  wp_properties_setf (self->immutable_props,
+      PW_KEY_ENDPOINT_LINK_INPUT_STREAM, "%d", self->info.input_stream_id);
+
+  /* populate immutable (global) properties */
+  while (g_variant_iter_next (immutable_props, "{&s&s}", &key, &value))
+    wp_properties_set (self->immutable_props, key, value);
+
+  /* populate standard properties */
+  populate_properties (self);
+
+  /* subscribe to changes */
+  g_signal_connect_object (self->item, "link-properties-changed",
+      G_CALLBACK (on_si_link_properties_changed), self, 0);
+  g_signal_connect_object (self->item, "flags-changed",
+      G_CALLBACK (on_si_link_flags_changed), self, 0);
 
-  self->iface = SPA_INTERFACE_INIT (
-      PW_TYPE_INTERFACE_EndpointLink,
-      PW_VERSION_ENDPOINT_LINK,
-      &impl_endpoint_link, self);
-  spa_hook_list_init (&self->hooks);
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
+
+  G_OBJECT_CLASS (wp_impl_endpoint_link_parent_class)->constructed (object);
+}
 
-  priv->iface = (struct pw_endpoint_link *) &self->iface;
+static void
+wp_impl_endpoint_link_dispose (GObject * object)
+{
+  WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object);
+
+  g_clear_pointer (&self->immutable_props, wp_properties_unref);
+  g_clear_pointer (&self->info.error, g_free);
+
+  wp_object_update_features (WP_OBJECT (self), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+
+  G_OBJECT_CLASS (wp_impl_endpoint_link_parent_class)->dispose (object);
 }
 
 static void
@@ -563,16 +527,9 @@ wp_impl_endpoint_link_activate_execute_step (WpObject * object,
     WpObjectFeatures missing)
 {
   WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object);
-  WpEndpointLinkPrivate *priv =
-      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
 
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND: {
-    g_autoptr (GVariant) info = NULL;
-    g_autoptr (GVariantIter) immutable_props = NULL;
-    g_autoptr (WpProperties) props = NULL;
-    const gchar *key, *value;
-    WpSiStream *stream;
+  case WP_PW_OBJECT_MIXIN_STEP_BIND: {
     g_autoptr (WpCore) core = wp_object_get_core (object);
     struct pw_core *pw_core = wp_core_get_pw_core (core);
 
@@ -585,77 +542,11 @@ wp_impl_endpoint_link_activate_execute_step (WpObject * object,
       return;
     }
 
-    /* get info from the interface */
-    info = wp_si_link_get_registration_info (self->item);
-    g_variant_get (info, "a{ss}", &immutable_props);
-
-    /* get the current state */
-    self->info.state =
-        (wp_session_item_get_flags (WP_SESSION_ITEM (self->item))
-            & WP_SI_FLAG_ACTIVE)
-        ? PW_ENDPOINT_LINK_STATE_ACTIVE
-        : PW_ENDPOINT_LINK_STATE_INACTIVE;
-
-    /* associate with the session, the endpoints and the streams */
-    self->info.session_id = wp_session_item_get_associated_proxy_id (
-        WP_SESSION_ITEM (self->item), WP_TYPE_SESSION);
-
-    stream = wp_si_link_get_out_stream (self->item);
-    self->info.output_endpoint_id = wp_session_item_get_associated_proxy_id (
-        WP_SESSION_ITEM (stream), WP_TYPE_ENDPOINT);
-    self->info.output_stream_id = wp_session_item_get_associated_proxy_id (
-        WP_SESSION_ITEM (stream), WP_TYPE_ENDPOINT_STREAM);
-
-    stream = wp_si_link_get_in_stream (self->item);
-    self->info.input_endpoint_id = wp_session_item_get_associated_proxy_id (
-        WP_SESSION_ITEM (stream), WP_TYPE_ENDPOINT);
-    self->info.input_stream_id = wp_session_item_get_associated_proxy_id (
-        WP_SESSION_ITEM (stream), WP_TYPE_ENDPOINT_STREAM);
-
-    /* construct export properties (these will come back through
-       the registry and appear in wp_proxy_get_global_properties) */
-    props = wp_properties_new_empty ();
-    wp_properties_setf (props, PW_KEY_SESSION_ID, "%d", self->info.session_id);
-    wp_properties_setf (props, PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT, "%d",
-        self->info.output_endpoint_id);
-    wp_properties_setf (props, PW_KEY_ENDPOINT_LINK_OUTPUT_STREAM, "%d",
-        self->info.output_stream_id);
-    wp_properties_setf (props, PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT, "%d",
-        self->info.input_endpoint_id);
-    wp_properties_setf (props, PW_KEY_ENDPOINT_LINK_INPUT_STREAM, "%d",
-        self->info.input_stream_id);
-
-    /* populate immutable (global) properties */
-    while (g_variant_iter_next (immutable_props, "{&s&s}", &key, &value))
-      wp_properties_set (props, key, value);
-
-    /* populate standard properties */
-    populate_properties (self, props);
-
-    /* subscribe to changes */
-    g_signal_connect_object (self->item, "link-properties-changed",
-        G_CALLBACK (on_si_link_properties_changed), self, 0);
-    g_signal_connect_object (self->item, "flags-changed",
-        G_CALLBACK (on_si_link_flags_changed), self, 0);
-
-    /* finalize info struct */
-    self->info.version = PW_VERSION_ENDPOINT_LINK_INFO;
-    self->info.error = NULL;
-    self->info.params = NULL;
-    self->info.n_params = 0;
-    priv->info = &self->info;
-
     /* bind */
     wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
             PW_TYPE_INTERFACE_EndpointLink,
-            wp_properties_peek_dict (props),
-            priv->iface, 0));
-
-    /* notify */
-    wp_object_update_features (object, WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-    g_object_notify (G_OBJECT (self), "properties");
-    g_object_notify (G_OBJECT (self), "param-info");
-
+            wp_properties_peek_dict (self->immutable_props),
+            &self->iface, 0));
     break;
   }
   default:
@@ -665,21 +556,6 @@ wp_impl_endpoint_link_activate_execute_step (WpObject * object,
   }
 }
 
-static void
-wp_impl_endpoint_link_pw_proxy_destroyed (WpProxy * proxy)
-{
-  WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (proxy);
-  WpEndpointLinkPrivate *priv =
-      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
-
-  g_signal_handlers_disconnect_by_data (self->item, self);
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&self->info.error, g_free);
-  priv->info = NULL;
-  wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
-}
-
 static void
 wp_impl_endpoint_link_class_init (WpImplEndpointLinkClass * klass)
 {
@@ -687,6 +563,8 @@ wp_impl_endpoint_link_class_init (WpImplEndpointLinkClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
+  object_class->constructed = wp_impl_endpoint_link_constructed;
+  object_class->dispose = wp_impl_endpoint_link_dispose;
   object_class->set_property = wp_impl_endpoint_link_set_property;
   object_class->get_property = wp_impl_endpoint_link_get_property;
 
@@ -694,13 +572,41 @@ wp_impl_endpoint_link_class_init (WpImplEndpointLinkClass * klass)
       wp_impl_endpoint_link_activate_execute_step;
 
   proxy_class->pw_proxy_created = NULL;
-  proxy_class->pw_proxy_destroyed = wp_impl_endpoint_link_pw_proxy_destroyed;
+  proxy_class->pw_proxy_destroyed = NULL;
 
   g_object_class_install_property (object_class, IMPL_PROP_ITEM,
       g_param_spec_object ("item", "item", "item", WP_TYPE_SI_LINK,
           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 }
 
+#define pw_endpoint_link_emit(hooks,method,version,...) \
+    spa_hook_list_call_simple(hooks, struct pw_endpoint_link_events, \
+        method, version, ##__VA_ARGS__)
+
+static void
+wp_impl_endpoint_link_emit_info (struct spa_hook_list * hooks, gconstpointer info)
+{
+  pw_endpoint_link_emit (hooks, info, 0, info);
+}
+
+static void
+wp_impl_endpoint_link_emit_param (struct spa_hook_list * hooks, int seq,
+      guint32 id, guint32 index, guint32 next, const struct spa_pod *param)
+{
+  pw_endpoint_link_emit (hooks, param, 0, seq, id, index, next, param);
+}
+
+static void
+wp_endpoint_link_impl_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
+{
+  iface->flags |= WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE;
+  iface->enum_params = NULL;
+  iface->set_param = NULL;
+  iface->emit_info = wp_impl_endpoint_link_emit_info;
+  iface->emit_param = wp_impl_endpoint_link_emit_param;
+}
+
 WpImplEndpointLink *
 wp_impl_endpoint_link_new (WpCore * core, WpSiLink * item)
 {
diff --git a/lib/wp/endpoint-stream.c b/lib/wp/endpoint-stream.c
index 269557f5..88bcf1fa 100644
--- a/lib/wp/endpoint-stream.c
+++ b/lib/wp/endpoint-stream.c
@@ -16,6 +16,8 @@
 #include "endpoint-stream.h"
 #include "node.h"
 #include "error.h"
+#include "debug.h"
+#include "spa-type.h"
 #include "private/impl-endpoint.h"
 #include "private/pipewire-object-mixin.h"
 
@@ -23,19 +25,11 @@
 #include <pipewire/extensions/session-manager/introspect-funcs.h>
 
 enum {
-  PROP_NAME = WP_PIPEWIRE_OBJECT_MIXIN_PROP_CUSTOM_START,
+  PROP_NAME = WP_PW_OBJECT_MIXIN_PROP_CUSTOM_START,
 };
 
-typedef struct _WpEndpointStreamPrivate WpEndpointStreamPrivate;
-struct _WpEndpointStreamPrivate
-{
-  WpProperties *properties;
-  struct pw_endpoint_stream_info *info;
-  struct pw_endpoint_stream *iface;
-  struct spa_hook listener;
-};
-
-static void wp_endpoint_stream_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_endpoint_stream_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpEndpointStream:
@@ -49,8 +43,10 @@ static void wp_endpoint_stream_pipewire_object_interface_init (WpPipewireObjectI
  * API.
  */
 G_DEFINE_TYPE_WITH_CODE (WpEndpointStream, wp_endpoint_stream, WP_TYPE_GLOBAL_PROXY,
-    G_ADD_PRIVATE (WpEndpointStream)
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_endpoint_stream_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_endpoint_stream_pw_object_mixin_priv_interface_init))
 
 static void
 wp_endpoint_stream_init (WpEndpointStream * self)
@@ -61,106 +57,60 @@ static void
 wp_endpoint_stream_get_property (GObject * object, guint property_id,
     GValue * value, GParamSpec * pspec)
 {
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (object);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
 
   switch (property_id) {
   case PROP_NAME:
-    g_value_set_string (value, priv->info ? priv->info->name : NULL);
+    g_value_set_string (value, d->info ?
+        ((struct pw_endpoint_stream_info *) d->info)->name : NULL);
     break;
   default:
-    wp_pipewire_object_mixin_get_property (object, property_id, value, pspec);
+    wp_pw_object_mixin_get_property (object, property_id, value, pspec);
     break;
   }
 }
 
-static WpObjectFeatures
-wp_endpoint_stream_get_supported_features (WpObject * object)
-{
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (object);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO |
-      wp_pipewire_object_mixin_param_info_to_features (
-          priv->info ? priv->info->params : NULL,
-          priv->info ? priv->info->n_params : 0);
-}
-
 static void
 wp_endpoint_stream_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    wp_pipewire_object_mixin_cache_info (object, transition);
-    break;
-  default:
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
     WP_OBJECT_CLASS (wp_endpoint_stream_parent_class)->
         activate_execute_step (object, transition, step, missing);
     break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
+    wp_pw_object_mixin_cache_params (object, missing);
+    break;
+  default:
+    g_assert_not_reached ();
   }
 }
 
 static void
 wp_endpoint_stream_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  wp_pipewire_object_mixin_deactivate (object, features);
-
+  wp_pw_object_mixin_deactivate (object, features);
   WP_OBJECT_CLASS (wp_endpoint_stream_parent_class)->deactivate (object, features);
 }
 
-static void
-endpoint_stream_event_info (void *data, const struct pw_endpoint_stream_info *info)
-{
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (data);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  priv->info = pw_endpoint_stream_info_update (priv->info, info);
-
-  if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS) {
-    g_clear_pointer (&priv->properties, wp_properties_unref);
-    priv->properties = wp_properties_new_wrap_dict (priv->info->props);
-  }
-
-  wp_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS,
-      PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS);
-}
-
 static const struct pw_endpoint_stream_events endpoint_stream_events = {
   PW_VERSION_ENDPOINT_STREAM_EVENTS,
-  .info = endpoint_stream_event_info,
-  .param = wp_pipewire_object_mixin_handle_event_param,
+  .info = (HandleEventInfoFunc(endpoint_stream)) wp_pw_object_mixin_handle_event_info,
+  .param = wp_pw_object_mixin_handle_event_param,
 };
 
 static void
 wp_endpoint_stream_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (proxy);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  priv->iface = (struct pw_endpoint_stream *) pw_proxy;
-  pw_endpoint_stream_add_listener (priv->iface, &priv->listener,
-      &endpoint_stream_events, self);
-}
-
-static void
-wp_endpoint_stream_pw_proxy_destroyed (WpProxy * proxy)
-{
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (proxy);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&priv->info, pw_endpoint_stream_info_free);
-  wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
-
-  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
-      WP_OBJECT_FEATURES_ALL);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      endpoint_stream, &endpoint_stream_events);
 }
 
 static void
@@ -173,9 +123,9 @@ wp_endpoint_stream_class_init (WpEndpointStreamClass * klass)
   object_class->get_property = wp_endpoint_stream_get_property;
 
   wpobject_class->get_supported_features =
-      wp_endpoint_stream_get_supported_features;
+      wp_pw_object_mixin_get_supported_features;
   wpobject_class->activate_get_next_step =
-      wp_pipewire_object_mixin_activate_get_next_step;
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step =
       wp_endpoint_stream_activate_execute_step;
   wpobject_class->deactivate = wp_endpoint_stream_deactivate;
@@ -183,9 +133,10 @@ wp_endpoint_stream_class_init (WpEndpointStreamClass * klass)
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_EndpointStream;
   proxy_class->pw_iface_version = PW_VERSION_ENDPOINT_STREAM;
   proxy_class->pw_proxy_created = wp_endpoint_stream_pw_proxy_created;
-  proxy_class->pw_proxy_destroyed = wp_endpoint_stream_pw_proxy_destroyed;
+  proxy_class->pw_proxy_destroyed =
+      wp_pw_object_mixin_handle_pw_proxy_destroyed;
 
-  wp_pipewire_object_mixin_class_override_properties (object_class);
+  wp_pw_object_mixin_class_override_properties (object_class);
 
   /**
    * WpEndpointStream:name:
@@ -197,60 +148,31 @@ wp_endpoint_stream_class_init (WpEndpointStreamClass * klass)
           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 }
 
-static gconstpointer
-wp_endpoint_stream_get_native_info (WpPipewireObject * obj)
+static gint
+wp_endpoint_stream_enum_params (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
 {
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (obj);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  return priv->info;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_endpoint_stream_enum_params (d->iface, 0, id, start, num,
+      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
 }
 
-static WpProperties *
-wp_endpoint_stream_get_properties (WpPipewireObject * obj)
-{
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (obj);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  return wp_properties_ref (priv->properties);
-}
-
-static GVariant *
-wp_endpoint_stream_get_param_info (WpPipewireObject * obj)
-{
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (obj);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  return wp_pipewire_object_mixin_param_info_to_gvariant (priv->info->params,
-      priv->info->n_params);
-}
-
-static void
-wp_endpoint_stream_enum_params (WpPipewireObject * obj, const gchar * id,
-    WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
-{
-  wp_pipewire_object_mixin_enum_params (pw_endpoint_stream, obj, id, filter,
-      cancellable, callback, user_data);
-}
-
-static void
-wp_endpoint_stream_set_param (WpPipewireObject * obj, const gchar * id,
+static gint
+wp_endpoint_stream_set_param (gpointer instance, guint32 id, guint32 flags,
     WpSpaPod * param)
 {
-  wp_pipewire_object_mixin_set_param (pw_endpoint_stream, obj, id, param);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_endpoint_stream_set_param (d->iface, id, flags,
+      wp_spa_pod_get_spa_pod (param));
 }
 
 static void
-wp_endpoint_stream_pipewire_object_interface_init (
-    WpPipewireObjectInterface * iface)
+wp_endpoint_stream_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_endpoint_stream_get_native_info;
-  iface->get_properties = wp_endpoint_stream_get_properties;
-  iface->get_param_info = wp_endpoint_stream_get_param_info;
+  wp_pw_object_mixin_priv_interface_info_init (iface,
+      endpoint_stream, ENDPOINT_STREAM);
   iface->enum_params = wp_endpoint_stream_enum_params;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
   iface->set_param = wp_endpoint_stream_set_param;
 }
 
@@ -267,8 +189,8 @@ wp_endpoint_stream_get_name (WpEndpointStream * self)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, NULL);
 
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-  return priv->info->name;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  return ((struct pw_endpoint_stream_info *) d->info)->name;
 }
 
 
@@ -284,121 +206,151 @@ struct _WpImplEndpointStream
   WpEndpointStream parent;
 
   struct spa_interface iface;
-  struct spa_hook_list hooks;
   struct pw_endpoint_stream_info info;
+  WpProperties *immutable_props;
 
   WpSiStream *item;
 };
 
-G_DEFINE_TYPE (WpImplEndpointStream, wp_impl_endpoint_stream, WP_TYPE_ENDPOINT_STREAM)
+static void wp_endpoint_stream_impl_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
-#define pw_endpoint_stream_emit(hooks,method,version,...) \
-    spa_hook_list_call_simple(hooks, struct pw_endpoint_stream_events, \
-        method, version, ##__VA_ARGS__)
+G_DEFINE_TYPE_WITH_CODE (WpImplEndpointStream, wp_impl_endpoint_stream, WP_TYPE_ENDPOINT_STREAM,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_endpoint_stream_impl_pw_object_mixin_priv_interface_init))
 
-#define pw_endpoint_stream_emit_info(hooks,...)  \
-    pw_endpoint_stream_emit(hooks, info, 0, ##__VA_ARGS__)
-#define pw_endpoint_stream_emit_param(hooks,...) \
-    pw_endpoint_stream_emit(hooks, param, 0, ##__VA_ARGS__)
-
-// static struct spa_param_info impl_param_info[] = {
-//   SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
-//   SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
-// };
-
-static int
-impl_add_listener(void *object,
-    struct spa_hook *listener,
-    const struct pw_endpoint_stream_events *events,
-    void *data)
-{
-  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
-  struct spa_hook_list save;
+static struct spa_param_info impl_param_info[] = {
+  SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
+  SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
+};
 
-  spa_hook_list_isolate (&self->hooks, &save, listener, events, data);
+static const struct pw_endpoint_stream_methods impl_endpoint_stream = {
+  PW_VERSION_ENDPOINT_STREAM_METHODS,
+  .add_listener =
+      (ImplAddListenerFunc(endpoint_stream)) wp_pw_object_mixin_impl_add_listener,
+  .subscribe_params = wp_pw_object_mixin_impl_subscribe_params,
+  .enum_params = wp_pw_object_mixin_impl_enum_params,
+  .set_param = wp_pw_object_mixin_impl_set_param,
+};
 
-  self->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_ALL
-      & ~PW_ENDPOINT_STREAM_CHANGE_MASK_LINK_PARAMS;
-  pw_endpoint_stream_emit_info (&self->hooks, &self->info);
-  self->info.change_mask = 0;
+static void
+wp_impl_endpoint_stream_init (WpImplEndpointStream * self)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
 
-  spa_hook_list_join (&self->hooks, &save);
-  return 0;
-}
+  self->iface = SPA_INTERFACE_INIT (
+      PW_TYPE_INTERFACE_EndpointStream,
+      PW_VERSION_ENDPOINT_STREAM,
+      &impl_endpoint_stream, self);
 
-static int
-impl_enum_params (void *object, int seq,
-    uint32_t id, uint32_t start, uint32_t num,
-    const struct spa_pod *filter)
-{
-  return -ENOENT;
+  d->info = &self->info;
+  d->iface = &self->iface;
 }
 
-static int
-impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+static void
+populate_properties (WpImplEndpointStream * self)
 {
-  return 0;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+
+  g_clear_pointer (&d->properties, wp_properties_unref);
+  d->properties = wp_si_stream_get_properties (self->item);
+  if (!d->properties)
+    d->properties = wp_properties_new_empty ();
+  d->properties = wp_properties_ensure_unique_owner (d->properties);
+  wp_properties_update (d->properties, self->immutable_props);
+
+  self->info.props = (struct spa_dict *) wp_properties_peek_dict (d->properties);
 }
 
-static int
-impl_set_param (void *object, uint32_t id, uint32_t flags,
-    const struct spa_pod *param)
+static void
+on_si_stream_properties_changed (WpSiStream * item, WpImplEndpointStream * self)
 {
-  return -ENOENT;
+  populate_properties (self);
+  wp_pw_object_mixin_notify_info (self, PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS);
 }
 
-static const struct pw_endpoint_stream_methods impl_endpoint_stream = {
-  PW_VERSION_ENDPOINT_STREAM_METHODS,
-  .add_listener = impl_add_listener,
-  .subscribe_params = impl_subscribe_params,
-  .enum_params = impl_enum_params,
-  .set_param = impl_set_param,
-};
-
 static void
-populate_properties (WpImplEndpointStream * self, WpProperties *global_props)
+on_node_params_changed (WpNode * node, guint32 param_id, WpImplEndpoint * self)
 {
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
-
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  priv->properties = wp_si_stream_get_properties (self->item);
-  if (!priv->properties)
-    priv->properties = wp_properties_new_empty ();
-  priv->properties = wp_properties_ensure_unique_owner (priv->properties);
-  wp_properties_update (priv->properties, global_props);
-
-  self->info.props = priv->properties ?
-      (struct spa_dict *) wp_properties_peek_dict (priv->properties) : NULL;
+  if (param_id == SPA_PARAM_PropInfo || param_id == SPA_PARAM_Props)
+    wp_pw_object_mixin_notify_params_changed (self, param_id);
 }
 
 static void
-on_si_stream_properties_changed (WpSiStream * item, WpImplEndpointStream * self)
+wp_impl_endpoint_stream_constructed (GObject * object)
 {
-  populate_properties (self,
-      wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (self)));
-  g_object_notify (G_OBJECT (self), "properties");
+  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
+  g_autoptr (GVariant) info = NULL;
+  g_autoptr (GVariantIter) immutable_props = NULL;
+  g_autoptr (WpObject) node = NULL;
+  const gchar *key, *value;
+
+  self->info.version = PW_VERSION_ENDPOINT_STREAM_INFO;
 
-  self->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS;
-  pw_endpoint_stream_emit_info (&self->hooks, &self->info);
-  self->info.change_mask = 0;
+  /* get info from the interface */
+  info = wp_si_stream_get_registration_info (self->item);
+  g_variant_get (info, "(sa{ss})", &self->info.name, &immutable_props);
+
+  /* associate with the endpoint */
+  self->info.endpoint_id = wp_session_item_get_associated_proxy_id (
+      WP_SESSION_ITEM (self->item), WP_TYPE_ENDPOINT);
+
+  /* construct export properties (these will come back through
+      the registry and appear in wp_proxy_get_global_properties) */
+  self->immutable_props = wp_properties_new (
+      PW_KEY_ENDPOINT_STREAM_NAME, self->info.name,
+      NULL);
+  wp_properties_setf (self->immutable_props, PW_KEY_ENDPOINT_ID,
+      "%d", self->info.endpoint_id);
+
+  /* populate immutable (global) properties */
+  while (g_variant_iter_next (immutable_props, "{&s&s}", &key, &value))
+    wp_properties_set (self->immutable_props, key, value);
+
+  /* populate standard properties */
+  populate_properties (self);
+
+  /* subscribe to changes */
+  g_signal_connect_object (self->item, "stream-properties-changed",
+      G_CALLBACK (on_si_stream_properties_changed), self, 0);
+
+  /* if the item has a node, proxy its ParamProps */
+  node = wp_session_item_get_associated_proxy (
+      WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
+  if (node && (wp_object_get_active_features (node) &
+                  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS)) {
+    self->info.params = impl_param_info;
+    self->info.n_params = G_N_ELEMENTS (impl_param_info);
+
+    g_signal_connect_object (node, "params-changed",
+        G_CALLBACK (on_node_params_changed), self, 0);
+
+    wp_object_update_features (WP_OBJECT (self),
+        WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS, 0);
+  } else {
+    self->info.params = NULL;
+    self->info.n_params = 0;
+  }
+
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
+
+  G_OBJECT_CLASS (wp_impl_endpoint_stream_parent_class)->constructed (object);
 }
 
 static void
-wp_impl_endpoint_stream_init (WpImplEndpointStream * self)
+wp_impl_endpoint_stream_dispose (GObject * object)
 {
-  /* 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 */
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
+  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
 
-  self->iface = SPA_INTERFACE_INIT (
-      PW_TYPE_INTERFACE_EndpointStream,
-      PW_VERSION_ENDPOINT_STREAM,
-      &impl_endpoint_stream, self);
-  spa_hook_list_init (&self->hooks);
+  g_clear_pointer (&self->immutable_props, wp_properties_unref);
+  g_clear_pointer (&self->info.name, g_free);
 
-  priv->iface = (struct pw_endpoint_stream *) &self->iface;
+  wp_object_update_features (WP_OBJECT (self), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS);
+
+  G_OBJECT_CLASS (wp_impl_endpoint_stream_parent_class)->dispose (object);
 }
 
 static void
@@ -434,7 +386,7 @@ wp_impl_endpoint_stream_get_property (GObject * object, guint property_id,
 }
 
 enum {
-  STEP_ACTIVATE_NODE = WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+  STEP_ACTIVATE_NODE = WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START,
 };
 
 static guint
@@ -444,27 +396,28 @@ wp_impl_endpoint_stream_activate_get_next_step (WpObject * object,
 {
   WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
 
-  /* bind if not already bound */
-  if (missing & WP_PROXY_FEATURE_BOUND) {
+  /* before anything else, if the item has a node,
+     cache its props so that enum_params works */
+  if (missing & WP_PIPEWIRE_OBJECT_FEATURES_ALL) {
     g_autoptr (WpObject) node = wp_session_item_get_associated_proxy (
         WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
 
-    /* if the item has a node, cache its props so that enum_params works */
-    // if (node && !(wp_object_get_active_features (node) &
-    //                   WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS))
-    //   return STEP_ACTIVATE_NODE;
-    // else
-      return WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND;
+    if (node && (wp_object_get_supported_features (node) &
+                    WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS) &&
+               !(wp_object_get_active_features (node) &
+                    WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS))
+      return STEP_ACTIVATE_NODE;
   }
-  /* cache info if supported */
-  else
-    return WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO;
+
+  return WP_OBJECT_CLASS (wp_impl_endpoint_stream_parent_class)->
+      activate_get_next_step (object, transition, step, missing);
 }
 
 static void
 wp_impl_endpoint_stream_node_activated (WpObject * node,
     GAsyncResult * res, WpTransition * transition)
 {
+  WpImplEndpointStream *self = wp_transition_get_source_object (transition);
   g_autoptr (GError) error = NULL;
 
   if (!wp_object_activate_finish (node, res, &error)) {
@@ -472,7 +425,15 @@ wp_impl_endpoint_stream_node_activated (WpObject * node,
     return;
   }
 
-  wp_transition_advance (transition);
+  self->info.params = impl_param_info;
+  self->info.n_params = G_N_ELEMENTS (impl_param_info);
+
+  g_signal_connect_object (node, "params-changed",
+      G_CALLBACK (on_node_params_changed), self, 0);
+
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS, 0);
+  wp_pw_object_mixin_notify_info (self, PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS);
 }
 
 static void
@@ -481,8 +442,6 @@ wp_impl_endpoint_stream_activate_execute_step (WpObject * object,
     WpObjectFeatures missing)
 {
   WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
 
   switch (step) {
   case STEP_ACTIVATE_NODE: {
@@ -490,15 +449,12 @@ wp_impl_endpoint_stream_activate_execute_step (WpObject * object,
         WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
 
     wp_object_activate (node,
-        WP_PROXY_FEATURE_BOUND /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */,
+        WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS,
         NULL, (GAsyncReadyCallback) wp_impl_endpoint_stream_node_activated,
         transition);
     break;
   }
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND: {
-    g_autoptr (GVariantIter) immutable_properties = NULL;
-    g_autoptr (WpProperties) properties = NULL;
-    const gchar *key, *value;
+  case WP_PW_OBJECT_MIXIN_STEP_BIND: {
     g_autoptr (WpCore) core = wp_object_get_core (object);
     struct pw_core *pw_core = wp_core_get_pw_core (core);
 
@@ -511,57 +467,11 @@ wp_impl_endpoint_stream_activate_execute_step (WpObject * object,
       return;
     }
 
-    wp_debug_object (self, "exporting");
-
-    /* get info from the interface */
-    {
-      g_autoptr (GVariant) info = NULL;
-      info = wp_si_stream_get_registration_info (self->item);
-      g_variant_get (info, "(sa{ss})", &self->info.name, &immutable_properties);
-
-      /* associate with the endpoint */
-      self->info.endpoint_id = wp_session_item_get_associated_proxy_id (
-          WP_SESSION_ITEM (self->item), WP_TYPE_ENDPOINT);
-    }
-
-    /* construct export properties (these will come back through
-        the registry and appear in wp_proxy_get_global_properties) */
-    properties = wp_properties_new (
-        PW_KEY_ENDPOINT_STREAM_NAME, self->info.name,
-        NULL);
-    wp_properties_setf (properties, PW_KEY_ENDPOINT_ID,
-        "%d", self->info.endpoint_id);
-
-    /* populate immutable (global) properties */
-    while (g_variant_iter_next (immutable_properties, "{&s&s}", &key, &value))
-      wp_properties_set (properties, key, value);
-
-    /* populate standard properties */
-    populate_properties (self, properties);
-
-    /* subscribe to changes */
-    g_signal_connect_object (self->item, "stream-properties-changed",
-        G_CALLBACK (on_si_stream_properties_changed), self, 0);
-
-    /* finalize info struct */
-    self->info.version = PW_VERSION_ENDPOINT_STREAM_INFO;
-    self->info.params = NULL;
-    self->info.n_params = 0;
-    priv->info = &self->info;
-
     /* bind */
     wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
             PW_TYPE_INTERFACE_EndpointStream,
-            wp_properties_peek_dict (properties),
-            priv->iface, 0));
-
-    /* notify */
-    wp_object_update_features (object,
-        WP_PIPEWIRE_OBJECT_FEATURE_INFO
-        /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */, 0);
-    g_object_notify (G_OBJECT (self), "properties");
-    g_object_notify (G_OBJECT (self), "param-info");
-
+            wp_properties_peek_dict (self->immutable_props),
+            &self->iface, 0));
     break;
   }
   default:
@@ -571,22 +481,6 @@ wp_impl_endpoint_stream_activate_execute_step (WpObject * object,
   }
 }
 
-static void
-wp_impl_endpoint_stream_pw_proxy_destroyed (WpProxy * proxy)
-{
-  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (proxy);
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
-
-  g_signal_handlers_disconnect_by_data (self->item, self);
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&self->info.name, g_free);
-  priv->info = NULL;
-  wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO
-      /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */);
-}
-
 static void
 wp_impl_endpoint_stream_class_init (WpImplEndpointStreamClass * klass)
 {
@@ -594,6 +488,8 @@ wp_impl_endpoint_stream_class_init (WpImplEndpointStreamClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
+  object_class->constructed = wp_impl_endpoint_stream_constructed;
+  object_class->dispose = wp_impl_endpoint_stream_dispose;
   object_class->set_property = wp_impl_endpoint_stream_set_property;
   object_class->get_property = wp_impl_endpoint_stream_get_property;
 
@@ -603,13 +499,85 @@ wp_impl_endpoint_stream_class_init (WpImplEndpointStreamClass * klass)
       wp_impl_endpoint_stream_activate_execute_step;
 
   proxy_class->pw_proxy_created = NULL;
-  proxy_class->pw_proxy_destroyed = wp_impl_endpoint_stream_pw_proxy_destroyed;
+  proxy_class->pw_proxy_destroyed = NULL;
 
   g_object_class_install_property (object_class, IMPL_PROP_ITEM,
       g_param_spec_object ("item", "item", "item", WP_TYPE_SI_STREAM,
           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 }
 
+static GPtrArray *
+wp_impl_endpoint_stream_enum_params_sync (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
+{
+  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (instance);
+  g_autoptr (WpPipewireObject) node = wp_session_item_get_associated_proxy (
+        WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
+
+  if (!node) {
+    wp_warning_object (self, "associated node is no longer available");
+    return NULL;
+  }
+
+  /* bypass a few things, knowing that the node
+     caches params in the mixin param store */
+  WpPwObjectMixinData *data = wp_pw_object_mixin_get_data (node);
+  GPtrArray *params = wp_pw_object_mixin_get_stored_params (data, id);
+  /* TODO filter */
+
+  return params;
+}
+
+static gint
+wp_impl_endpoint_stream_set_param (gpointer instance, guint32 id, guint32 flags,
+    WpSpaPod * param)
+{
+  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (instance);
+  g_autoptr (WpPipewireObject) node = wp_session_item_get_associated_proxy (
+        WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
+  const gchar *idstr = NULL;
+
+  if (!node) {
+    wp_warning_object (self, "associated node is no longer available");
+    return -EPIPE;
+  }
+
+  if (!wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, id, NULL, &idstr, NULL)) {
+    wp_critical_object (self, "invalid param id: %u", id);
+    return -EINVAL;
+  }
+
+  return wp_pipewire_object_set_param (node, idstr, flags, param) ? 0 : -EIO;
+}
+
+#define pw_endpoint_stream_emit(hooks,method,version,...) \
+    spa_hook_list_call_simple(hooks, struct pw_endpoint_stream_events, \
+        method, version, ##__VA_ARGS__)
+
+static void
+wp_impl_endpoint_stream_emit_info (struct spa_hook_list * hooks, gconstpointer info)
+{
+  pw_endpoint_stream_emit (hooks, info, 0, info);
+}
+
+static void
+wp_impl_endpoint_stream_emit_param (struct spa_hook_list * hooks, int seq,
+      guint32 id, guint32 index, guint32 next, const struct spa_pod *param)
+{
+  pw_endpoint_stream_emit (hooks, param, 0, seq, id, index, next, param);
+}
+
+static void
+wp_endpoint_stream_impl_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
+{
+  iface->flags |= WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE;
+  iface->enum_params_sync = wp_impl_endpoint_stream_enum_params_sync;
+  iface->set_param = wp_impl_endpoint_stream_set_param;
+  iface->emit_info = wp_impl_endpoint_stream_emit_info;
+  iface->emit_param = wp_impl_endpoint_stream_emit_param;
+}
+
 WpImplEndpointStream *
 wp_impl_endpoint_stream_new (WpCore * core, WpSiStream * item)
 {
diff --git a/lib/wp/endpoint.c b/lib/wp/endpoint.c
index bc3e87e2..853f74a9 100644
--- a/lib/wp/endpoint.c
+++ b/lib/wp/endpoint.c
@@ -11,6 +11,7 @@
  * @title: PIpeWire Endpoint
  */
 
+#include "spa/param/param.h"
 #define G_LOG_DOMAIN "wp-endpoint"
 
 #include "endpoint.h"
@@ -18,16 +19,19 @@
 #include "session.h"
 #include "object-manager.h"
 #include "error.h"
+#include "debug.h"
 #include "wpenums.h"
+#include "spa-type.h"
 #include "si-factory.h"
 #include "private/impl-endpoint.h"
 #include "private/pipewire-object-mixin.h"
 
 #include <pipewire/extensions/session-manager.h>
 #include <pipewire/extensions/session-manager/introspect-funcs.h>
+#include <spa/utils/result.h>
 
 enum {
-  PROP_NAME = WP_PIPEWIRE_OBJECT_MIXIN_PROP_CUSTOM_START,
+  PROP_NAME = WP_PW_OBJECT_MIXIN_PROP_CUSTOM_START,
   PROP_MEDIA_CLASS,
   PROP_DIRECTION,
 };
@@ -42,14 +46,11 @@ static guint32 signals[N_SIGNALS] = {0};
 typedef struct _WpEndpointPrivate WpEndpointPrivate;
 struct _WpEndpointPrivate
 {
-  WpProperties *properties;
-  struct pw_endpoint_info *info;
-  struct pw_endpoint *iface;
-  struct spa_hook listener;
   WpObjectManager *streams_om;
 };
 
-static void wp_endpoint_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_endpoint_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpEndpoint:
@@ -63,7 +64,10 @@ static void wp_endpoint_pipewire_object_interface_init (WpPipewireObjectInterfac
  */
 G_DEFINE_TYPE_WITH_CODE (WpEndpoint, wp_endpoint, WP_TYPE_GLOBAL_PROXY,
     G_ADD_PRIVATE (WpEndpoint)
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_endpoint_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_endpoint_pw_object_mixin_priv_interface_init))
 
 static void
 wp_endpoint_init (WpEndpoint * self)
@@ -74,21 +78,23 @@ static void
 wp_endpoint_get_property (GObject * object, guint property_id,
     GValue * value, GParamSpec * pspec)
 {
-  WpEndpoint *self = WP_ENDPOINT (object);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
 
   switch (property_id) {
   case PROP_NAME:
-    g_value_set_string (value, priv->info ? priv->info->name : NULL);
+    g_value_set_string (value, d->info ?
+        ((struct pw_endpoint_info *) d->info)->name : NULL);
     break;
   case PROP_MEDIA_CLASS:
-    g_value_set_string (value, priv->info ? priv->info->media_class : NULL);
+    g_value_set_string (value, d->info ?
+        ((struct pw_endpoint_info *) d->info)->media_class : NULL);
     break;
   case PROP_DIRECTION:
-    g_value_set_enum (value, priv->info ? priv->info->direction : 0);
+    g_value_set_enum (value, d->info ?
+        ((struct pw_endpoint_info *) d->info)->direction : 0);
     break;
   default:
-    wp_pipewire_object_mixin_get_property (object, property_id, value, pspec);
+    wp_pw_object_mixin_get_property (object, property_id, value, pspec);
     break;
   }
 }
@@ -111,12 +117,14 @@ wp_endpoint_emit_streams_changed (WpObjectManager *streams_om,
 static void
 wp_endpoint_enable_feature_streams (WpEndpoint * self)
 {
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
   g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
   guint32 bound_id = wp_proxy_get_bound_id (WP_PROXY (self));
+  guint32 n_streams = ((struct pw_endpoint_info *) d->info)->n_streams;
 
   wp_debug_object (self, "enabling WP_ENDPOINT_FEATURE_STREAMS, bound_id:%u, "
-      "n_streams:%u", bound_id, priv->info->n_streams);
+      "n_streams:%u", bound_id, n_streams);
 
   priv->streams_om = wp_object_manager_new ();
   /* proxy endpoint stream -> check for endpoint.id in global properties */
@@ -140,7 +148,7 @@ wp_endpoint_enable_feature_streams (WpEndpoint * self)
      and we get an endpoint with 0 streams in the WpSession's endpoints
      object manager... so, unless the endpoint really has no streams,
      wait for them to be prepared by waiting for the "objects-changed" only */
-  if (G_UNLIKELY (priv->info->n_streams == 0)) {
+  if (G_UNLIKELY (n_streams == 0)) {
     g_signal_connect_object (priv->streams_om, "installed",
         G_CALLBACK (wp_endpoint_on_streams_om_installed), self, 0);
   }
@@ -153,67 +161,48 @@ wp_endpoint_enable_feature_streams (WpEndpoint * self)
 static WpObjectFeatures
 wp_endpoint_get_supported_features (WpObject * object)
 {
-  WpEndpoint *self = WP_ENDPOINT (object);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-
-  return
-      WP_PROXY_FEATURE_BOUND |
-      WP_ENDPOINT_FEATURE_STREAMS |
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
-      wp_pipewire_object_mixin_param_info_to_features (
-          priv->info ? priv->info->params : NULL,
-          priv->info ? priv->info->n_params : 0);
+  return wp_pw_object_mixin_get_supported_features(object)
+      | WP_ENDPOINT_FEATURE_STREAMS;
 }
 
 enum {
-  STEP_STREAMS = WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+  STEP_STREAMS = WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START,
 };
 
-static guint
-wp_endpoint_activate_get_next_step (WpObject * object,
-    WpFeatureActivationTransition * transition, guint step,
-    WpObjectFeatures missing)
-{
-  step = wp_pipewire_object_mixin_activate_get_next_step (object, transition,
-      step, missing);
-
-  /* extend the mixin's state machine; when the only remaining feature to
-     enable is FEATURE_STREAMS, advance to STEP_STREAMS */
-  if (step == WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO &&
-      missing == WP_ENDPOINT_FEATURE_STREAMS)
-    return STEP_STREAMS;
-
-  return step;
-}
-
 static void
 wp_endpoint_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    wp_pipewire_object_mixin_cache_info (object, transition);
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
+    WP_OBJECT_CLASS (wp_endpoint_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
+    wp_pw_object_mixin_cache_params (object, missing);
     break;
   case STEP_STREAMS:
     wp_endpoint_enable_feature_streams (WP_ENDPOINT (object));
     break;
   default:
-    WP_OBJECT_CLASS (wp_endpoint_parent_class)->
-        activate_execute_step (object, transition, step, missing);
-    break;
+    g_assert_not_reached ();
   }
 }
 
 static void
 wp_endpoint_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  WpEndpoint *self = WP_ENDPOINT (object);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-
-  wp_pipewire_object_mixin_deactivate (object, features);
+  wp_pw_object_mixin_deactivate (object, features);
 
   if (features & WP_ENDPOINT_FEATURE_STREAMS) {
+    WpEndpoint *self = WP_ENDPOINT (object);
+    WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
     g_clear_object (&priv->streams_om);
     wp_object_update_features (object, 0, WP_ENDPOINT_FEATURE_STREAMS);
   }
@@ -221,41 +210,17 @@ wp_endpoint_deactivate (WpObject * object, WpObjectFeatures features)
   WP_OBJECT_CLASS (wp_endpoint_parent_class)->deactivate (object, features);
 }
 
-static void
-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 = 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_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_ENDPOINT_CHANGE_MASK_PROPS, PW_ENDPOINT_CHANGE_MASK_PARAMS);
-}
-
 static const struct pw_endpoint_events endpoint_events = {
   PW_VERSION_ENDPOINT_EVENTS,
-  .info = endpoint_event_info,
-  .param = wp_pipewire_object_mixin_handle_event_param,
+  .info = (HandleEventInfoFunc(endpoint)) wp_pw_object_mixin_handle_event_info,
+  .param = wp_pw_object_mixin_handle_event_param,
 };
 
 static void
 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);
-
-  priv->iface = (struct pw_endpoint *) pw_proxy;
-  pw_endpoint_add_listener (priv->iface, &priv->listener, &endpoint_events,
-      self);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      endpoint, &endpoint_events);
 }
 
 static void
@@ -264,14 +229,11 @@ wp_endpoint_pw_proxy_destroyed (WpProxy * proxy)
   WpEndpoint *self = WP_ENDPOINT (proxy);
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
 
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&priv->info, pw_endpoint_info_free);
+  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
+
   g_clear_object (&priv->streams_om);
   wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO | WP_ENDPOINT_FEATURE_STREAMS);
-
-  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
-      WP_OBJECT_FEATURES_ALL);
+      WP_ENDPOINT_FEATURE_STREAMS);
 }
 
 static void
@@ -284,7 +246,8 @@ wp_endpoint_class_init (WpEndpointClass * klass)
   object_class->get_property = wp_endpoint_get_property;
 
   wpobject_class->get_supported_features = wp_endpoint_get_supported_features;
-  wpobject_class->activate_get_next_step = wp_endpoint_activate_get_next_step;
+  wpobject_class->activate_get_next_step =
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step = wp_endpoint_activate_execute_step;
   wpobject_class->deactivate = wp_endpoint_deactivate;
 
@@ -293,7 +256,7 @@ wp_endpoint_class_init (WpEndpointClass * klass)
   proxy_class->pw_proxy_created = wp_endpoint_pw_proxy_created;
   proxy_class->pw_proxy_destroyed = wp_endpoint_pw_proxy_destroyed;
 
-  wp_pipewire_object_mixin_class_override_properties (object_class);
+  wp_pw_object_mixin_class_override_properties (object_class);
 
   /**
    * WpEndpoint::streams-changed:
@@ -334,60 +297,30 @@ wp_endpoint_class_init (WpEndpointClass * klass)
           WP_TYPE_DIRECTION, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 }
 
-static gconstpointer
-wp_endpoint_get_native_info (WpPipewireObject * obj)
-{
-  WpEndpoint *self = WP_ENDPOINT (obj);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-
-  return priv->info;
-}
-
-static WpProperties *
-wp_endpoint_get_properties (WpPipewireObject * obj)
-{
-  WpEndpoint *self = WP_ENDPOINT (obj);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-
-  return wp_properties_ref (priv->properties);
-}
-
-static GVariant *
-wp_endpoint_get_param_info (WpPipewireObject * obj)
+static gint
+wp_endpoint_enum_params (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
 {
-  WpEndpoint *self = WP_ENDPOINT (obj);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-
-  return wp_pipewire_object_mixin_param_info_to_gvariant (priv->info->params,
-      priv->info->n_params);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_endpoint_enum_params (d->iface, 0, id, start, num,
+      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
 }
 
-static void
-wp_endpoint_enum_params (WpPipewireObject * obj, const gchar * id,
-    WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
-{
-  wp_pipewire_object_mixin_enum_params (pw_endpoint, obj, id, filter,
-      cancellable, callback, user_data);
-}
-
-static void
-wp_endpoint_set_param (WpPipewireObject * obj, const gchar * id,
+static gint
+wp_endpoint_set_param (gpointer instance, guint32 id, guint32 flags,
     WpSpaPod * param)
 {
-  wp_pipewire_object_mixin_set_param (pw_endpoint, obj, id, param);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_endpoint_set_param (d->iface, id, flags,
+      wp_spa_pod_get_spa_pod (param));
 }
 
 static void
-wp_endpoint_pipewire_object_interface_init (
-    WpPipewireObjectInterface * iface)
+wp_endpoint_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_endpoint_get_native_info;
-  iface->get_properties = wp_endpoint_get_properties;
-  iface->get_param_info = wp_endpoint_get_param_info;
+  wp_pw_object_mixin_priv_interface_info_init (iface, endpoint, ENDPOINT);
   iface->enum_params = wp_endpoint_enum_params;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
   iface->set_param = wp_endpoint_set_param;
 }
 
@@ -406,8 +339,8 @@ wp_endpoint_get_name (WpEndpoint * self)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, NULL);
 
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-  return priv->info->name;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  return ((struct pw_endpoint_info *) d->info)->name;
 }
 
 /**
@@ -425,8 +358,8 @@ wp_endpoint_get_media_class (WpEndpoint * self)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, NULL);
 
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-  return priv->info->media_class;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  return ((struct pw_endpoint_info *) d->info)->media_class;
 }
 
 /**
@@ -444,8 +377,8 @@ wp_endpoint_get_direction (WpEndpoint * self)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-  return (WpDirection) priv->info->direction;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  return (WpDirection) ((struct pw_endpoint_info *) d->info)->direction;
 }
 
 /**
@@ -606,11 +539,10 @@ wp_endpoint_lookup_stream_full (WpEndpoint * self, WpObjectInterest * interest)
 void
 wp_endpoint_create_link (WpEndpoint * self, WpProperties * props)
 {
-  WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
   int res;
 
-  res = pw_endpoint_create_link (priv->iface, wp_properties_peek_dict (props));
+  res = pw_endpoint_create_link (d->iface, wp_properties_peek_dict (props));
   if (res < 0) {
     wp_warning_object (self, "pw_endpoint_create_link: %d: %s", res,
         spa_strerror (res));
@@ -629,65 +561,23 @@ struct _WpImplEndpoint
   WpEndpoint parent;
 
   struct spa_interface iface;
-  struct spa_hook_list hooks;
   struct pw_endpoint_info info;
+  WpProperties *immutable_props;
 
   WpSiEndpoint *item;
 };
 
-G_DEFINE_TYPE (WpImplEndpoint, wp_impl_endpoint, WP_TYPE_ENDPOINT)
+static void wp_endpoint_impl_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
-#define pw_endpoint_emit(hooks,method,version,...) \
-    spa_hook_list_call_simple(hooks, struct pw_endpoint_events, \
-        method, version, ##__VA_ARGS__)
-
-#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__)
-
-// static struct spa_param_info impl_param_info[] = {
-//   SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
-//   SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
-// };
-
-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;
-
-  spa_hook_list_isolate (&self->hooks, &save, listener, events, data);
-
-  self->info.change_mask = PW_ENDPOINT_CHANGE_MASK_ALL;
-  pw_endpoint_emit_info (&self->hooks, &self->info);
-  self->info.change_mask = 0;
-
-  spa_hook_list_join (&self->hooks, &save);
-  return 0;
-}
+G_DEFINE_TYPE_WITH_CODE (WpImplEndpoint, wp_impl_endpoint, WP_TYPE_ENDPOINT,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_endpoint_impl_pw_object_mixin_priv_interface_init))
 
-static int
-impl_enum_params (void *object, int seq,
-    uint32_t id, uint32_t start, uint32_t num,
-    const struct spa_pod *filter)
-{
-  return -ENOENT;
-}
-
-static int
-impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
-{
-  return 0;
-}
-
-static int
-impl_set_param (void *object, uint32_t id, uint32_t flags,
-    const struct spa_pod *param)
-{
-  return -ENOENT;
-}
+static struct spa_param_info impl_param_info[] = {
+  SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
+  SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
+};
 
 static void
 on_si_link_exported (WpSessionItem * link, GAsyncResult * res, gpointer data)
@@ -865,57 +755,137 @@ impl_create_link (void *object, const struct spa_dict *props)
 
 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,
+  .add_listener =
+      (ImplAddListenerFunc(endpoint)) wp_pw_object_mixin_impl_add_listener,
+  .subscribe_params = wp_pw_object_mixin_impl_subscribe_params,
+  .enum_params = wp_pw_object_mixin_impl_enum_params,
+  .set_param = wp_pw_object_mixin_impl_set_param,
   .create_link = impl_create_link,
 };
 
 static void
-populate_properties (WpImplEndpoint * self, WpProperties *global_props)
+wp_impl_endpoint_init (WpImplEndpoint * self)
 {
-  WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
 
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  priv->properties = wp_si_endpoint_get_properties (self->item);
-  if (!priv->properties)
-    priv->properties = wp_properties_new_empty ();
-  priv->properties = wp_properties_ensure_unique_owner (priv->properties);
-  wp_properties_update (priv->properties, global_props);
+  self->iface = SPA_INTERFACE_INIT (
+      PW_TYPE_INTERFACE_Endpoint,
+      PW_VERSION_ENDPOINT,
+      &impl_endpoint, self);
 
-  self->info.props = priv->properties ?
-      (struct spa_dict *) wp_properties_peek_dict (priv->properties) : NULL;
+  d->info = &self->info;
+  d->iface = &self->iface;
+}
+
+static void
+populate_properties (WpImplEndpoint * self)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+
+  g_clear_pointer (&d->properties, wp_properties_unref);
+  d->properties = wp_si_endpoint_get_properties (self->item);
+  if (!d->properties)
+    d->properties = wp_properties_new_empty ();
+  d->properties = wp_properties_ensure_unique_owner (d->properties);
+  wp_properties_update (d->properties, self->immutable_props);
+
+  self->info.props = (struct spa_dict *) wp_properties_peek_dict (d->properties);
 }
 
 static void
 on_si_endpoint_properties_changed (WpSiEndpoint * item, WpImplEndpoint * self)
 {
-  populate_properties (self,
-      wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (self)));
-  g_object_notify (G_OBJECT (self), "properties");
+  populate_properties (self);
+  wp_pw_object_mixin_notify_info (self, PW_ENDPOINT_CHANGE_MASK_PROPS);
+}
 
-  self->info.change_mask = PW_ENDPOINT_CHANGE_MASK_PROPS;
-  pw_endpoint_emit_info (&self->hooks, &self->info);
-  self->info.change_mask = 0;
+static void
+on_node_params_changed (WpNode * node, guint32 param_id, WpImplEndpoint * self)
+{
+  if (param_id == SPA_PARAM_PropInfo || param_id == SPA_PARAM_Props)
+    wp_pw_object_mixin_notify_params_changed (self, param_id);
 }
 
 static void
-wp_impl_endpoint_init (WpImplEndpoint * self)
+wp_impl_endpoint_constructed (GObject * object)
 {
-  /* 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));
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
+  g_autoptr (GVariant) info = NULL;
+  g_autoptr (GVariantIter) immutable_props = NULL;
+  g_autoptr (WpObject) node = NULL;
+  const gchar *key, *value;
+  guchar direction;
 
-  self->iface = SPA_INTERFACE_INIT (
-      PW_TYPE_INTERFACE_Endpoint,
-      PW_VERSION_ENDPOINT,
-      &impl_endpoint, self);
-  spa_hook_list_init (&self->hooks);
+  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;
+  self->info.n_streams = wp_si_endpoint_get_n_streams (self->item);
 
-  priv->iface = (struct pw_endpoint *) &self->iface;
+  /* associate with the session */
+  self->info.session_id = wp_session_item_get_associated_proxy_id (
+      WP_SESSION_ITEM (self->item), WP_TYPE_SESSION);
+
+  /* construct export properties (these will come back through
+    the registry and appear in wp_proxy_get_global_properties) */
+  self->immutable_props = wp_properties_new (
+      PW_KEY_ENDPOINT_NAME, self->info.name,
+      PW_KEY_MEDIA_CLASS, self->info.media_class,
+      NULL);
+  wp_properties_setf (self->immutable_props, PW_KEY_SESSION_ID,
+      "%d", self->info.session_id);
+
+  /* populate immutable (global) properties */
+  while (g_variant_iter_next (immutable_props, "{&s&s}", &key, &value))
+    wp_properties_set (self->immutable_props, key, value);
+
+  /* populate standard properties */
+  populate_properties (self);
+
+  /* subscribe to changes */
+  g_signal_connect_object (self->item, "endpoint-properties-changed",
+      G_CALLBACK (on_si_endpoint_properties_changed), self, 0);
+
+  /* if the item has a node, proxy its ParamProps */
+  node = wp_session_item_get_associated_proxy (
+      WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
+  if (node && (wp_object_get_active_features (node) &
+                  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS)) {
+    self->info.params = impl_param_info;
+    self->info.n_params = G_N_ELEMENTS (impl_param_info);
+
+    g_signal_connect_object (node, "params-changed",
+        G_CALLBACK (on_node_params_changed), self, 0);
+
+    wp_object_update_features (WP_OBJECT (self),
+        WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS, 0);
+  } else {
+    self->info.params = NULL;
+    self->info.n_params = 0;
+  }
+
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
+
+  G_OBJECT_CLASS (wp_impl_endpoint_parent_class)->constructed (object);
+}
+
+static void
+wp_impl_endpoint_dispose (GObject * object)
+{
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
+
+  g_clear_pointer (&self->immutable_props, wp_properties_unref);
+  g_clear_pointer (&self->info.name, g_free);
+
+  wp_object_update_features (WP_OBJECT (self), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS);
+
+  G_OBJECT_CLASS (wp_impl_endpoint_parent_class)->dispose (object);
 }
 
 static void
@@ -961,29 +931,28 @@ wp_impl_endpoint_activate_get_next_step (WpObject * object,
 {
   WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
 
-  /* bind if not already bound */
-  if (missing & WP_PROXY_FEATURE_BOUND) {
+  /* before anything else, if the item has a node,
+     cache its props so that enum_params works */
+  if (missing & WP_PIPEWIRE_OBJECT_FEATURES_ALL) {
     g_autoptr (WpObject) node = wp_session_item_get_associated_proxy (
         WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
 
-    /* if the item has a node, cache its props so that enum_params works */
-    // if (node && !(wp_object_get_active_features (node) &
-    //                   WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS))
-    //   return STEP_ACTIVATE_NODE;
-    // else
-      return WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND;
+    if (node && (wp_object_get_supported_features (node) &
+                    WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS) &&
+               !(wp_object_get_active_features (node) &
+                    WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS))
+      return STEP_ACTIVATE_NODE;
   }
-  /* enable FEATURE_STREAMS when there is nothing else left to activate */
-  else if (missing == WP_ENDPOINT_FEATURE_STREAMS)
-    return STEP_STREAMS;
-  else
-    return WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO;
+
+  return WP_OBJECT_CLASS (wp_impl_endpoint_parent_class)->
+      activate_get_next_step (object, transition, step, missing);
 }
 
 static void
 wp_impl_endpoint_node_activated (WpObject * node,
     GAsyncResult * res, WpTransition * transition)
 {
+  WpImplEndpoint *self = wp_transition_get_source_object (transition);
   g_autoptr (GError) error = NULL;
 
   if (!wp_object_activate_finish (node, res, &error)) {
@@ -991,7 +960,15 @@ wp_impl_endpoint_node_activated (WpObject * node,
     return;
   }
 
-  wp_transition_advance (transition);
+  self->info.params = impl_param_info;
+  self->info.n_params = G_N_ELEMENTS (impl_param_info);
+
+  g_signal_connect_object (node, "params-changed",
+      G_CALLBACK (on_node_params_changed), self, 0);
+
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS, 0);
+  wp_pw_object_mixin_notify_info (self, PW_ENDPOINT_CHANGE_MASK_PARAMS);
 }
 
 static void
@@ -1000,8 +977,6 @@ wp_impl_endpoint_activate_execute_step (WpObject * object,
     WpObjectFeatures missing)
 {
   WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
-  WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
 
   switch (step) {
   case STEP_ACTIVATE_NODE: {
@@ -1009,14 +984,12 @@ wp_impl_endpoint_activate_execute_step (WpObject * object,
         WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
 
     wp_object_activate (node,
-        WP_PROXY_FEATURE_BOUND /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */,
+        WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS,
         NULL, (GAsyncReadyCallback) wp_impl_endpoint_node_activated,
         transition);
     break;
   }
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND: {
-    g_autoptr (GVariantIter) immutable_properties = NULL;
-    g_autoptr (WpProperties) properties = NULL;
+  case WP_PW_OBJECT_MIXIN_STEP_BIND: {
     g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
     struct pw_core *pw_core = wp_core_get_pw_core (core);
 
@@ -1029,67 +1002,11 @@ wp_impl_endpoint_activate_execute_step (WpObject * object,
       return;
     }
 
-    wp_debug_object (self, "exporting");
-
-    /* get info from the interface */
-    {
-      g_autoptr (GVariant) info = NULL;
-      guchar direction;
-
-      info = wp_si_endpoint_get_registration_info (self->item);
-      g_variant_get (info, "(ssya{ss})", &self->info.name,
-          &self->info.media_class, &direction, &immutable_properties);
-
-      self->info.direction = (enum pw_direction) direction;
-      self->info.n_streams = wp_si_endpoint_get_n_streams (self->item);
-
-      /* associate with the session */
-      self->info.session_id = wp_session_item_get_associated_proxy_id (
-          WP_SESSION_ITEM (self->item), WP_TYPE_SESSION);
-    }
-
-    /* construct export properties (these will come back through
-      the registry and appear in wp_proxy_get_global_properties) */
-    properties = wp_properties_new (
-        PW_KEY_ENDPOINT_NAME, self->info.name,
-        PW_KEY_MEDIA_CLASS, self->info.media_class,
-        NULL);
-    wp_properties_setf (properties, PW_KEY_SESSION_ID,
-        "%d", self->info.session_id);
-
-    /* populate immutable (global) properties */
-    {
-      const gchar *key, *value;
-      while (g_variant_iter_next (immutable_properties, "{&s&s}", &key, &value))
-        wp_properties_set (properties, key, value);
-    }
-
-    /* populate standard properties */
-    populate_properties (self, properties);
-
-    /* subscribe to changes */
-    g_signal_connect_object (self->item, "endpoint-properties-changed",
-        G_CALLBACK (on_si_endpoint_properties_changed), self, 0);
-
-    /* finalize info struct */
-    self->info.version = PW_VERSION_ENDPOINT_INFO;
-    self->info.params = NULL;
-    self->info.n_params = 0;
-    priv->info = &self->info;
-
     /* bind */
     wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
             PW_TYPE_INTERFACE_Endpoint,
-            wp_properties_peek_dict (properties),
-            priv->iface, 0));
-
-    /* notify */
-    wp_object_update_features (object,
-        WP_PIPEWIRE_OBJECT_FEATURE_INFO
-        /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */, 0);
-    g_object_notify (G_OBJECT (self), "properties");
-    g_object_notify (G_OBJECT (self), "param-info");
-
+            wp_properties_peek_dict (self->immutable_props),
+            &self->iface, 0));
     break;
   }
   default:
@@ -1102,18 +1019,12 @@ wp_impl_endpoint_activate_execute_step (WpObject * object,
 static void
 wp_impl_endpoint_pw_proxy_destroyed (WpProxy * proxy)
 {
-  WpImplEndpoint *self = WP_IMPL_ENDPOINT (proxy);
   WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
+      wp_endpoint_get_instance_private (WP_ENDPOINT (proxy));
 
-  g_signal_handlers_disconnect_by_data (self->item, self);
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&self->info.name, g_free);
-  g_clear_pointer (&self->info.media_class, g_free);
-  priv->info = NULL;
+  g_clear_object (&priv->streams_om);
   wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO
-      /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */);
+      WP_ENDPOINT_FEATURE_STREAMS);
 }
 
 static void
@@ -1123,6 +1034,8 @@ wp_impl_endpoint_class_init (WpImplEndpointClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
+  object_class->constructed = wp_impl_endpoint_constructed;
+  object_class->dispose = wp_impl_endpoint_dispose;
   object_class->set_property = wp_impl_endpoint_set_property;
   object_class->get_property = wp_impl_endpoint_get_property;
 
@@ -1139,6 +1052,78 @@ wp_impl_endpoint_class_init (WpImplEndpointClass * klass)
           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 }
 
+static GPtrArray *
+wp_impl_endpoint_enum_params_sync (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
+{
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (instance);
+  g_autoptr (WpPipewireObject) node = wp_session_item_get_associated_proxy (
+        WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
+
+  if (!node) {
+    wp_warning_object (self, "associated node is no longer available");
+    return NULL;
+  }
+
+  /* bypass a few things, knowing that the node
+     caches params in the mixin param store */
+  WpPwObjectMixinData *data = wp_pw_object_mixin_get_data (node);
+  GPtrArray *params = wp_pw_object_mixin_get_stored_params (data, id);
+  /* TODO filter */
+
+  return params;
+}
+
+static gint
+wp_impl_endpoint_set_param (gpointer instance, guint32 id, guint32 flags,
+    WpSpaPod * param)
+{
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (instance);
+  g_autoptr (WpPipewireObject) node = wp_session_item_get_associated_proxy (
+        WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
+  const gchar *idstr = NULL;
+
+  if (!node) {
+    wp_warning_object (self, "associated node is no longer available");
+    return -EPIPE;
+  }
+
+  if (!wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, id, NULL, &idstr, NULL)) {
+    wp_critical_object (self, "invalid param id: %u", id);
+    return -EINVAL;
+  }
+
+  return wp_pipewire_object_set_param (node, idstr, flags, param) ? 0 : -EIO;
+}
+
+#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_emit_info (struct spa_hook_list * hooks, gconstpointer info)
+{
+  pw_endpoint_emit (hooks, info, 0, info);
+}
+
+static void
+wp_impl_endpoint_emit_param (struct spa_hook_list * hooks, int seq,
+      guint32 id, guint32 index, guint32 next, const struct spa_pod *param)
+{
+  pw_endpoint_emit (hooks, param, 0, seq, id, index, next, param);
+}
+
+static void
+wp_endpoint_impl_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
+{
+  iface->flags |= WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE;
+  iface->enum_params_sync = wp_impl_endpoint_enum_params_sync;
+  iface->set_param = wp_impl_endpoint_set_param;
+  iface->emit_info = wp_impl_endpoint_emit_info;
+  iface->emit_param = wp_impl_endpoint_emit_param;
+}
+
 WpImplEndpoint *
 wp_impl_endpoint_new (WpCore * core, WpSiEndpoint * item)
 {
diff --git a/lib/wp/global-proxy.c b/lib/wp/global-proxy.c
index 7de94a45..1f751ecb 100644
--- a/lib/wp/global-proxy.c
+++ b/lib/wp/global-proxy.c
@@ -121,6 +121,8 @@ wp_global_proxy_activate_get_next_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
+  /* we only support BOUND, so this is the only
+     feature that can be in @missing */
   g_return_val_if_fail (missing == WP_PROXY_FEATURE_BOUND,
       WP_TRANSITION_STEP_ERROR);
 
diff --git a/lib/wp/link.c b/lib/wp/link.c
index 0a030b11..13666317 100644
--- a/lib/wp/link.c
+++ b/lib/wp/link.c
@@ -19,11 +19,10 @@
 struct _WpLink
 {
   WpGlobalProxy parent;
-  struct pw_link_info *info;
-  struct spa_hook listener;
 };
 
-static void wp_link_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_link_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpLink:
@@ -38,67 +37,46 @@ static void wp_link_pipewire_object_interface_init (WpPipewireObjectInterface *
  * on the remote PipeWire server by calling into a factory.
  */
 G_DEFINE_TYPE_WITH_CODE (WpLink, wp_link, WP_TYPE_GLOBAL_PROXY,
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_link_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_link_pw_object_mixin_priv_interface_init))
 
 static void
 wp_link_init (WpLink * self)
 {
 }
 
-static WpObjectFeatures
-wp_link_get_supported_features (WpObject * object)
-{
-  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO;
-}
-
 static void
 wp_link_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    /* just wait, info will be emitted anyway after binding */
-    break;
-  default:
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
     WP_OBJECT_CLASS (wp_link_parent_class)->
         activate_execute_step (object, transition, step, missing);
     break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  default:
+    g_assert_not_reached ();
   }
 }
 
-static void
-link_event_info(void *data, const struct pw_link_info *info)
-{
-  WpLink *self = WP_LINK (data);
-
-  self->info = pw_link_info_update (self->info, info);
-  wp_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_LINK_CHANGE_MASK_PROPS, 0);
-}
-
 static const struct pw_link_events link_events = {
   PW_VERSION_LINK_EVENTS,
-  .info = link_event_info,
+  .info = (HandleEventInfoFunc(link)) wp_pw_object_mixin_handle_event_info,
 };
 
 static void
 wp_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
-  WpLink *self = WP_LINK (proxy);
-  pw_link_add_listener ((struct pw_link *) pw_proxy,
-      &self->listener, &link_events, self);
-}
-
-static void
-wp_link_pw_proxy_destroyed (WpProxy * proxy)
-{
-  g_clear_pointer (&WP_LINK (proxy)->info, pw_link_info_free);
-  wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      link, &link_events);
 }
 
 static void
@@ -108,49 +86,28 @@ wp_link_class_init (WpLinkClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->get_property = wp_pipewire_object_mixin_get_property;
+  object_class->get_property = wp_pw_object_mixin_get_property;
 
-  wpobject_class->get_supported_features = wp_link_get_supported_features;
+  wpobject_class->get_supported_features =
+      wp_pw_object_mixin_get_supported_features;
   wpobject_class->activate_get_next_step =
-      wp_pipewire_object_mixin_activate_get_next_step;
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step = wp_link_activate_execute_step;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Link;
   proxy_class->pw_iface_version = PW_VERSION_LINK;
   proxy_class->pw_proxy_created = wp_link_pw_proxy_created;
-  proxy_class->pw_proxy_destroyed = wp_link_pw_proxy_destroyed;
-
-  wp_pipewire_object_mixin_class_override_properties (object_class);
-}
-
-static gconstpointer
-wp_link_get_native_info (WpPipewireObject * obj)
-{
-  return WP_LINK (obj)->info;
-}
+  proxy_class->pw_proxy_destroyed =
+      wp_pw_object_mixin_handle_pw_proxy_destroyed;
 
-static WpProperties *
-wp_link_get_properties (WpPipewireObject * obj)
-{
-  return wp_properties_new_wrap_dict (WP_LINK (obj)->info->props);
-}
-
-static GVariant *
-wp_link_get_param_info (WpPipewireObject * obj)
-{
-  return NULL;
+  wp_pw_object_mixin_class_override_properties (object_class);
 }
 
 static void
-wp_link_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+wp_link_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_link_get_native_info;
-  iface->get_properties = wp_link_get_properties;
-  iface->get_param_info = wp_link_get_param_info;
-  iface->enum_params = wp_pipewire_object_mixin_enum_params_unimplemented;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
-  iface->set_param = wp_pipewire_object_mixin_set_param_unimplemented;
+  wp_pw_object_mixin_priv_interface_info_init_no_params (iface, link, LINK);
 }
 
 /**
@@ -202,7 +159,7 @@ wp_link_new_from_factory (WpCore * core,
  *
  * Retrieves the ids of the objects that are linked by this link
  *
- * Note: Using this method requires %WP_PROXY_FEATURE_INFO
+ * Note: Using this method requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
  */
 void
 wp_link_get_linked_object_ids (WpLink * self,
@@ -211,12 +168,16 @@ wp_link_get_linked_object_ids (WpLink * self,
 {
   g_return_if_fail (WP_IS_LINK (self));
 
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  struct pw_link_info *info = d->info;
+  g_return_if_fail (info);
+
   if (output_node)
-    *output_node = self->info->output_node_id;
+    *output_node = info->output_node_id;
   if (output_port)
-    *output_port = self->info->output_port_id;
+    *output_port = info->output_port_id;
   if (input_node)
-    *input_node = self->info->input_node_id;
+    *input_node = info->input_node_id;
   if (input_port)
-    *input_port = self->info->input_port_id;
+    *input_port = info->input_port_id;
 }
diff --git a/lib/wp/node.c b/lib/wp/node.c
index fbd85053..309643e3 100644
--- a/lib/wp/node.c
+++ b/lib/wp/node.c
@@ -16,6 +16,7 @@
 #include "node.h"
 #include "core.h"
 #include "object-manager.h"
+#include "debug.h"
 #include "wpenums.h"
 #include "private/pipewire-object-mixin.h"
 
@@ -32,12 +33,11 @@ static guint32 signals[N_SIGNALS] = {0};
 struct _WpNode
 {
   WpGlobalProxy parent;
-  struct pw_node_info *info;
-  struct spa_hook listener;
   WpObjectManager *ports_om;
 };
 
-static void wp_node_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_node_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpNode:
@@ -52,7 +52,10 @@ static void wp_node_pipewire_object_interface_init (WpPipewireObjectInterface *
  * on the remote PipeWire server by calling into a factory.
  */
 G_DEFINE_TYPE_WITH_CODE (WpNode, wp_node, WP_TYPE_GLOBAL_PROXY,
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_node_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_node_pw_object_mixin_priv_interface_init))
 
 static void
 wp_node_init (WpNode * self)
@@ -99,64 +102,47 @@ wp_node_enable_feature_ports (WpNode * self)
 static WpObjectFeatures
 wp_node_get_supported_features (WpObject * object)
 {
-  WpNode *self = WP_NODE (object);
-  return
-      WP_PROXY_FEATURE_BOUND |
-      WP_NODE_FEATURE_PORTS |
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
-      wp_pipewire_object_mixin_param_info_to_features (
-          self->info ? self->info->params : NULL,
-          self->info ? self->info->n_params : 0);
+  return wp_pw_object_mixin_get_supported_features (object)
+      | WP_NODE_FEATURE_PORTS;
 }
 
 enum {
-  STEP_PORTS = WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+  STEP_PORTS = WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START,
 };
 
-static guint
-wp_node_activate_get_next_step (WpObject * object,
-    WpFeatureActivationTransition * transition, guint step,
-    WpObjectFeatures missing)
-{
-  step = wp_pipewire_object_mixin_activate_get_next_step (object, transition,
-      step, missing);
-
-  /* extend the mixin's state machine; when the only remaining feature to
-     enable is FEATURE_PORTS, advance to STEP_PORTS */
-  if (step == WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO &&
-      missing == WP_NODE_FEATURE_PORTS)
-    return STEP_PORTS;
-
-  return step;
-}
-
 static void
 wp_node_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    wp_pipewire_object_mixin_cache_info (object, transition);
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
+    WP_OBJECT_CLASS (wp_node_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
+    wp_pw_object_mixin_cache_params (object, missing);
     break;
   case STEP_PORTS:
     wp_node_enable_feature_ports (WP_NODE (object));
     break;
   default:
-    WP_OBJECT_CLASS (wp_node_parent_class)->
-        activate_execute_step (object, transition, step, missing);
-    break;
+    g_assert_not_reached ();
   }
 }
 
 static void
 wp_node_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  WpNode *self = WP_NODE (object);
-
-  wp_pipewire_object_mixin_deactivate (object, features);
+  wp_pw_object_mixin_deactivate (object, features);
 
   if (features & WP_NODE_FEATURE_PORTS) {
+    WpNode *self = WP_NODE (object);
     g_clear_object (&self->ports_om);
     wp_object_update_features (object, 0, WP_NODE_FEATURE_PORTS);
   }
@@ -164,37 +150,17 @@ wp_node_deactivate (WpObject * object, WpObjectFeatures features)
   WP_OBJECT_CLASS (wp_node_parent_class)->deactivate (object, features);
 }
 
-static void
-node_event_info(void *data, const struct pw_node_info *info)
-{
-  WpNode *self = WP_NODE (data);
-  enum pw_node_state old_state = self->info ?
-      self->info->state : PW_NODE_STATE_CREATING;
-
-  self->info = pw_node_info_update (self->info, info);
-  wp_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  if (info->change_mask & PW_NODE_CHANGE_MASK_STATE)
-    g_signal_emit (self, signals[SIGNAL_STATE_CHANGED], 0, old_state,
-        self->info->state);
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_NODE_CHANGE_MASK_PROPS, PW_NODE_CHANGE_MASK_PARAMS);
-}
-
 static const struct pw_node_events node_events = {
   PW_VERSION_NODE_EVENTS,
-  .info = node_event_info,
-  .param = wp_pipewire_object_mixin_handle_event_param,
+  .info = (HandleEventInfoFunc(node)) wp_pw_object_mixin_handle_event_info,
+  .param = wp_pw_object_mixin_handle_event_param,
 };
 
 static void
 wp_node_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
-  WpNode *self = WP_NODE (proxy);
-  pw_node_add_listener ((struct pw_port *) pw_proxy,
-      &self->listener, &node_events, self);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      node, &node_events);
 }
 
 static void
@@ -202,13 +168,10 @@ wp_node_pw_proxy_destroyed (WpProxy * proxy)
 {
   WpNode *self = WP_NODE (proxy);
 
-  g_clear_pointer (&self->info, pw_node_info_free);
-  g_clear_object (&self->ports_om);
-  wp_object_update_features (WP_OBJECT (self), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO | WP_NODE_FEATURE_PORTS);
+  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
 
-  wp_pipewire_object_mixin_deactivate (WP_OBJECT (self),
-      WP_OBJECT_FEATURES_ALL);
+  g_clear_object (&self->ports_om);
+  wp_object_update_features (WP_OBJECT (self), 0, WP_NODE_FEATURE_PORTS);
 }
 
 static void
@@ -218,10 +181,11 @@ wp_node_class_init (WpNodeClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->get_property = wp_pipewire_object_mixin_get_property;
+  object_class->get_property = wp_pw_object_mixin_get_property;
 
   wpobject_class->get_supported_features = wp_node_get_supported_features;
-  wpobject_class->activate_get_next_step = wp_node_activate_get_next_step;
+  wpobject_class->activate_get_next_step =
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step = wp_node_activate_execute_step;
   wpobject_class->deactivate = wp_node_deactivate;
 
@@ -230,7 +194,7 @@ wp_node_class_init (WpNodeClass * klass)
   proxy_class->pw_proxy_created = wp_node_pw_proxy_created;
   proxy_class->pw_proxy_destroyed = wp_node_pw_proxy_destroyed;
 
-  wp_pipewire_object_mixin_class_override_properties (object_class);
+  wp_pw_object_mixin_class_override_properties (object_class);
 
   /**
    * WpNode::state-changed:
@@ -258,50 +222,44 @@ wp_node_class_init (WpNodeClass * klass)
       G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 }
 
-static gconstpointer
-wp_node_get_native_info (WpPipewireObject * obj)
-{
-  return WP_NODE (obj)->info;
-}
-
-static WpProperties *
-wp_node_get_properties (WpPipewireObject * obj)
+static void
+wp_node_process_info (gpointer instance, gpointer old_info, gpointer i)
 {
-  return wp_properties_new_wrap_dict (WP_NODE (obj)->info->props);
-}
+  const struct pw_node_info *info = i;
 
-static GVariant *
-wp_node_get_param_info (WpPipewireObject * obj)
-{
-  WpNode *self = WP_NODE (obj);
-  return wp_pipewire_object_mixin_param_info_to_gvariant (self->info->params,
-      self->info->n_params);
+  if (info->change_mask & PW_NODE_CHANGE_MASK_STATE) {
+    enum pw_node_state old_state = old_info ?
+        ((struct pw_node_info *) old_info)->state : PW_NODE_STATE_CREATING;
+    g_signal_emit (instance, signals[SIGNAL_STATE_CHANGED], 0,
+        old_state, info->state);
+  }
 }
 
-static void
-wp_node_enum_params (WpPipewireObject * obj, const gchar * id,
-    WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
+static gint
+wp_node_enum_params (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
 {
-  wp_pipewire_object_mixin_enum_params (pw_node, obj, id, filter, cancellable,
-      callback, user_data);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_node_enum_params (d->iface, 0, id, start, num,
+      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
 }
 
-static void
-wp_node_set_param (WpPipewireObject * obj, const gchar * id, WpSpaPod * param)
+static gint
+wp_node_set_param (gpointer instance, guint32 id, guint32 flags,
+    WpSpaPod * param)
 {
-  wp_pipewire_object_mixin_set_param (pw_node, obj, id, param);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_node_set_param (d->iface, id, flags,
+      wp_spa_pod_get_spa_pod (param));
 }
 
 static void
-wp_node_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+wp_node_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_node_get_native_info;
-  iface->get_properties = wp_node_get_properties;
-  iface->get_param_info = wp_node_get_param_info;
+  wp_pw_object_mixin_priv_interface_info_init (iface, node, NODE);
+  iface->process_info = wp_node_process_info;
   iface->enum_params = wp_node_enum_params;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
   iface->set_param = wp_node_set_param;
 }
 
@@ -351,9 +309,12 @@ wp_node_get_state (WpNode * self, const gchar ** error)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, WP_NODE_STATE_ERROR);
 
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  const struct pw_node_info *info = d->info;
+
   if (error)
-    *error = self->info->error;
-  return (WpNodeState) self->info->state;
+    *error = info->error;
+  return (WpNodeState) info->state;
 }
 
 /**
@@ -372,9 +333,12 @@ wp_node_get_n_input_ports (WpNode * self, guint * max)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  const struct pw_node_info *info = d->info;
+
   if (max)
-    *max = self->info->max_input_ports;
-  return self->info->n_input_ports;
+    *max = info->max_input_ports;
+  return info->n_input_ports;
 }
 
 /**
@@ -393,9 +357,12 @@ wp_node_get_n_output_ports (WpNode * self, guint * max)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  const struct pw_node_info *info = d->info;
+
   if (max)
-    *max = self->info->max_output_ports;
-  return self->info->n_output_ports;
+    *max = info->max_output_ports;
+  return info->n_output_ports;
 }
 
 /**
diff --git a/lib/wp/port.c b/lib/wp/port.c
index 5d501140..c29c176d 100644
--- a/lib/wp/port.c
+++ b/lib/wp/port.c
@@ -19,11 +19,10 @@
 struct _WpPort
 {
   WpGlobalProxy parent;
-  struct pw_port_info *info;
-  struct spa_hook listener;
 };
 
-static void wp_port_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_port_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpPort:
@@ -35,84 +34,57 @@ static void wp_port_pipewire_object_interface_init (WpPipewireObjectInterface *
  * PipeWire registry and it is made available through the #WpObjectManager API.
  */
 G_DEFINE_TYPE_WITH_CODE (WpPort, wp_port, WP_TYPE_GLOBAL_PROXY,
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_port_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_port_pw_object_mixin_priv_interface_init))
 
 static void
 wp_port_init (WpPort * self)
 {
 }
 
-static WpObjectFeatures
-wp_port_get_supported_features (WpObject * object)
-{
-  WpPort *self = WP_PORT (object);
-
-  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO |
-      wp_pipewire_object_mixin_param_info_to_features (
-          self->info ? self->info->params : NULL,
-          self->info ? self->info->n_params : 0);
-}
-
 static void
 wp_port_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    wp_pipewire_object_mixin_cache_info (object, transition);
-    break;
-  default:
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
     WP_OBJECT_CLASS (wp_port_parent_class)->
         activate_execute_step (object, transition, step, missing);
     break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
+    wp_pw_object_mixin_cache_params (object, missing);
+    break;
+  default:
+    g_assert_not_reached ();
   }
 }
 
 static void
 wp_port_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  wp_pipewire_object_mixin_deactivate (object, features);
-
+  wp_pw_object_mixin_deactivate (object, features);
   WP_OBJECT_CLASS (wp_port_parent_class)->deactivate (object, features);
 }
 
-static void
-port_event_info(void *data, const struct pw_port_info *info)
-{
-  WpPort *self = WP_PORT (data);
-
-  self->info = pw_port_info_update (self->info, info);
-  wp_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_PORT_CHANGE_MASK_PROPS, PW_PORT_CHANGE_MASK_PARAMS);
-}
-
 static const struct pw_port_events port_events = {
   PW_VERSION_PORT_EVENTS,
-  .info = port_event_info,
-  .param = wp_pipewire_object_mixin_handle_event_param,
+  .info = (HandleEventInfoFunc(port)) wp_pw_object_mixin_handle_event_info,
+  .param = wp_pw_object_mixin_handle_event_param,
 };
 
 static void
 wp_port_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
-  WpPort *self = WP_PORT (proxy);
-  pw_port_add_listener ((struct pw_port *) pw_proxy,
-      &self->listener, &port_events, self);
-}
-
-static void
-wp_port_pw_proxy_destroyed (WpProxy * proxy)
-{
-  g_clear_pointer (&WP_PORT (proxy)->info, pw_port_info_free);
-  wp_object_update_features (WP_OBJECT (proxy), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
-
-  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
-      WP_OBJECT_FEATURES_ALL);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      port, &port_events);
 }
 
 static void
@@ -122,61 +94,39 @@ wp_port_class_init (WpPortClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->get_property = wp_pipewire_object_mixin_get_property;
+  object_class->get_property = wp_pw_object_mixin_get_property;
 
-  wpobject_class->get_supported_features = wp_port_get_supported_features;
+  wpobject_class->get_supported_features =
+      wp_pw_object_mixin_get_supported_features;
   wpobject_class->activate_get_next_step =
-      wp_pipewire_object_mixin_activate_get_next_step;
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step = wp_port_activate_execute_step;
   wpobject_class->deactivate = wp_port_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Port;
   proxy_class->pw_iface_version = PW_VERSION_PORT;
   proxy_class->pw_proxy_created = wp_port_pw_proxy_created;
-  proxy_class->pw_proxy_destroyed = wp_port_pw_proxy_destroyed;
-
-  wp_pipewire_object_mixin_class_override_properties (object_class);
-}
-
-static gconstpointer
-wp_port_get_native_info (WpPipewireObject * obj)
-{
-  return WP_PORT (obj)->info;
-}
-
-static WpProperties *
-wp_port_get_properties (WpPipewireObject * obj)
-{
-  return wp_properties_new_wrap_dict (WP_PORT (obj)->info->props);
-}
+  proxy_class->pw_proxy_destroyed =
+      wp_pw_object_mixin_handle_pw_proxy_destroyed;
 
-static GVariant *
-wp_port_get_param_info (WpPipewireObject * obj)
-{
-  WpPort *self = WP_PORT (obj);
-  return wp_pipewire_object_mixin_param_info_to_gvariant (self->info->params,
-      self->info->n_params);
+  wp_pw_object_mixin_class_override_properties (object_class);
 }
 
-static void
-wp_port_enum_params (WpPipewireObject * obj, const gchar * id,
-    WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
+static gint
+wp_port_enum_params (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
 {
-  wp_pipewire_object_mixin_enum_params (pw_port, obj, id, filter, cancellable,
-      callback, user_data);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_port_enum_params (d->iface, 0, id, start, num,
+      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
 }
 
 static void
-wp_port_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+wp_port_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_port_get_native_info;
-  iface->get_properties = wp_port_get_properties;
-  iface->get_param_info = wp_port_get_param_info;
+  wp_pw_object_mixin_priv_interface_info_init (iface, port, PORT);
   iface->enum_params = wp_port_enum_params;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
-  iface->set_param = wp_pipewire_object_mixin_set_param_unimplemented;
 }
 
 WpDirection
@@ -186,5 +136,8 @@ wp_port_get_direction (WpPort * self)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  return (WpDirection) self->info->direction;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  const struct pw_port_info *info = d->info;
+
+  return (WpDirection) info->direction;
 }
diff --git a/lib/wp/private/pipewire-object-mixin.c b/lib/wp/private/pipewire-object-mixin.c
index 49b316d6..e88ff998 100644
--- a/lib/wp/private/pipewire-object-mixin.c
+++ b/lib/wp/private/pipewire-object-mixin.c
@@ -10,24 +10,445 @@
 
 #include "private/pipewire-object-mixin.h"
 #include "core.h"
+#include "spa-type.h"
+#include "spa-pod.h"
+#include "debug.h"
 #include "error.h"
 
-G_DEFINE_QUARK (WpPipewireObjectMixinEnumParamsTasks, enum_params_tasks)
+#include <spa/utils/result.h>
+
+G_DEFINE_INTERFACE (WpPwObjectMixinPriv, wp_pw_object_mixin_priv, WP_TYPE_PROXY)
+
+static void
+wp_pw_object_mixin_priv_default_init (WpPwObjectMixinPrivInterface * iface)
+{
+}
+
+static struct spa_param_info *
+find_param_info (gpointer instance, guint32 id)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
+
+  /* offsets are 0 on objects that don't support params */
+  if (d->info && iface->n_params_offset && iface->param_info_offset) {
+    struct spa_param_info * param_info =
+        G_STRUCT_MEMBER (struct spa_param_info *, d->info, iface->param_info_offset);
+    guint32 n_params =
+        G_STRUCT_MEMBER (guint32, d->info, iface->n_params_offset);
+
+    for (guint i = 0; i < n_params; i++) {
+      if (param_info[i].id == id)
+        return &param_info[i];
+    }
+  }
+  return NULL;
+}
+
+/*************/
+/* INTERFACE */
+
+static gconstpointer
+wp_pw_object_mixin_get_native_info (WpPipewireObject * obj)
+{
+  return wp_pw_object_mixin_get_data (obj)->info;
+}
+
+static WpProperties *
+wp_pw_object_mixin_get_properties (WpPipewireObject * obj)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (obj);
+  return d->properties ? wp_properties_ref (d->properties) : NULL;
+}
+
+static GVariant *
+wp_pw_object_mixin_get_param_info (WpPipewireObject * obj)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (obj);
+  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
+  struct spa_param_info *info;
+  guint32 n_params;
+  g_auto (GVariantBuilder) b =
+      G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_DICTIONARY);
+
+  if (!d->info ||
+      iface->param_info_offset == 0 ||
+      iface->n_params_offset == 0)
+    return NULL;
+
+  info = G_STRUCT_MEMBER (struct spa_param_info *, d->info, iface->param_info_offset);
+  n_params = G_STRUCT_MEMBER (guint32, d->info, iface->n_params_offset);
+
+  g_variant_builder_init (&b, G_VARIANT_TYPE ("a{ss}"));
+
+  for (guint i = 0; i < n_params; i++) {
+    const gchar *nick = NULL;
+    gchar flags[3];
+    guint flags_idx = 0;
+
+    wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, info[i].id, NULL, &nick,
+        NULL);
+    g_return_val_if_fail (nick != NULL, NULL);
+
+    if (info[i].flags & SPA_PARAM_INFO_READ)
+      flags[flags_idx++] = 'r';
+    if (info[i].flags & SPA_PARAM_INFO_WRITE)
+      flags[flags_idx++] = 'w';
+    flags[flags_idx] = '\0';
+
+    g_variant_builder_add (&b, "{ss}", nick, flags);
+  }
+
+  return g_variant_builder_end (&b);
+}
+
+static void
+enum_params_done (WpCore * core, GAsyncResult * res, gpointer data)
+{
+  g_autoptr (GTask) task = G_TASK (data);
+  g_autoptr (GError) error = NULL;
+  gpointer instance = g_task_get_source_object (G_TASK (data));
+  GPtrArray *params = g_task_get_task_data (task);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+
+  /* finish the sync task */
+  wp_core_sync_finish (core, res, &error);
+
+  /* return if the task was cancelled */
+  if (g_task_get_completed (task))
+    return;
+
+  /* remove the task from the stored list; ref is held by the g_autoptr */
+  d->enum_params_tasks = g_list_remove (d->enum_params_tasks, task);
+
+  wp_debug_object (instance, "got %u params, %s, task " WP_OBJECT_FORMAT,
+      params->len, error ? "with error" : "ok", WP_OBJECT_ARGS (task));
+
+  if (error)
+    g_task_return_error (task, g_steal_pointer (&error));
+  else {
+    g_task_return_pointer (task, g_ptr_array_ref (params),
+        (GDestroyNotify) g_ptr_array_unref);
+  }
+}
+
+static void
+wp_pw_object_mixin_enum_params_unchecked (gpointer obj,
+    guint32 id, WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (obj);
+  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
+  g_autoptr (GTask) task = NULL;
+  gint seq = 0;
+  GPtrArray *params = NULL;
+
+  g_return_if_fail (iface->enum_params_sync || iface->enum_params);
+
+  if (iface->enum_params_sync) {
+    params = iface->enum_params_sync (obj, id, 0, -1, filter);
+  } else {
+    seq = iface->enum_params (obj, id, 0, -1, filter);
+
+    /* return early if seq contains an error */
+    if (G_UNLIKELY (SPA_RESULT_IS_ERROR (seq))) {
+      wp_message_object (obj, "enum_params failed: %s", spa_strerror (seq));
+      g_task_report_new_error (obj, callback, user_data, NULL,
+          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
+          "enum_params failed: %s", spa_strerror (seq));
+      return;
+    }
+  }
+
+  if (!params)
+    params = g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);
+
+  /* create task */
+  task = g_task_new (obj, cancellable, callback, user_data);
+
+  /* debug */
+  if (wp_log_level_is_enabled (G_LOG_LEVEL_DEBUG)) {
+    const gchar *name = NULL;
+    wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, id, &name, NULL, NULL);
+
+    wp_debug_object (obj, "enum id %u (%s), seq 0x%x (%u), task "
+        WP_OBJECT_FORMAT "%s", id, name, seq, seq, WP_OBJECT_ARGS (task),
+        iface->enum_params_sync ? ", sync" : "");
+  }
+
+  if (iface->enum_params_sync) {
+    g_task_return_pointer (task, params, (GDestroyNotify) g_ptr_array_unref);
+  } else {
+    g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (obj));
+
+    /* store */
+    g_task_set_task_data (task, params, (GDestroyNotify) g_ptr_array_unref);
+    g_task_set_source_tag (task, GINT_TO_POINTER (seq));
+    d->enum_params_tasks = g_list_append (d->enum_params_tasks, task);
+
+    /* call sync */
+    wp_core_sync (core, cancellable, (GAsyncReadyCallback) enum_params_done,
+        g_object_ref (task));
+  }
+}
+
+static void
+wp_pw_object_mixin_enum_params (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
+  guint32 param_id = 0;
+
+  if (!(iface->enum_params || iface->enum_params_sync)) {
+    g_task_report_new_error (obj, callback, user_data, NULL,
+        WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+        "enum_params is not supported on this object");
+    return;
+  }
+
+  /* translate the id */
+  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &param_id,
+          NULL, NULL)) {
+    wp_critical_object (obj, "invalid param id: %s", id);
+    return;
+  }
+
+  wp_pw_object_mixin_enum_params_unchecked (obj, param_id, filter,
+      cancellable, callback, user_data);
+}
+
+static WpIterator *
+wp_pw_object_mixin_enum_params_finish (WpPipewireObject * obj,
+    GAsyncResult * res, GError ** error)
+{
+  g_return_val_if_fail (g_task_is_valid (res, obj), NULL);
+
+  GPtrArray *array = g_task_propagate_pointer (G_TASK (res), error);
+  if (!array)
+    return NULL;
+
+  return wp_iterator_new_ptr_array (array, WP_TYPE_SPA_POD);
+}
+
+static WpIterator *
+wp_pw_object_mixin_enum_params_sync (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod * filter)
+{
+  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
+  GPtrArray *params = NULL;
+  guint32 param_id = 0;
+
+  /* translate the id */
+  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &param_id,
+          NULL, NULL)) {
+    wp_critical_object (obj, "invalid param id: %s", id);
+    return NULL;
+  }
+
+  if (iface->enum_params_sync) {
+    /* use enum_params_sync if supported */
+    params = iface->enum_params_sync (obj, param_id, 0, -1, filter);
+  } else {
+    /* otherwise, find and return the cached params */
+    WpPwObjectMixinData *data = wp_pw_object_mixin_get_data (obj);
+    params = wp_pw_object_mixin_get_stored_params (data, param_id);
+    /* TODO filter */
+  }
+
+  return params ? wp_iterator_new_ptr_array (params, WP_TYPE_SPA_POD) : NULL;
+}
+
+static gboolean
+wp_pw_object_mixin_set_param (WpPipewireObject * obj, const gchar * id,
+    guint32 flags, WpSpaPod * param)
+{
+  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
+  guint32 param_id = 0;
+  gint ret;
+
+  if (!iface->set_param) {
+    wp_warning_object (obj, "set_param is not supported on this object");
+    return FALSE;
+  }
+
+  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &param_id,
+          NULL, NULL)) {
+    wp_critical_object (obj, "invalid param id: %s", id);
+    wp_spa_pod_unref (param);
+    return FALSE;
+  }
+
+  ret = iface->set_param (obj, param_id, flags, param);
+
+  if (G_UNLIKELY (SPA_RESULT_IS_ERROR (ret))) {
+    wp_message_object (obj, "set_param failed: %s", spa_strerror (ret));
+    return FALSE;
+  }
+  return TRUE;
+}
 
 void
-wp_pipewire_object_mixin_get_property (GObject * object, guint property_id,
+wp_pw_object_mixin_object_interface_init (WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_pw_object_mixin_get_native_info;
+  iface->get_properties = wp_pw_object_mixin_get_properties;
+  iface->get_param_info = wp_pw_object_mixin_get_param_info;
+  iface->enum_params = wp_pw_object_mixin_enum_params;
+  iface->enum_params_finish = wp_pw_object_mixin_enum_params_finish;
+  iface->enum_params_sync = wp_pw_object_mixin_enum_params_sync;
+  iface->set_param = wp_pw_object_mixin_set_param;
+}
+
+/********/
+/* DATA */
+
+G_DEFINE_QUARK (WpPwObjectMixinData, wp_pw_object_mixin_data)
+
+static void wp_pw_object_mixin_param_store_free (gpointer data);
+
+static WpPwObjectMixinData *
+wp_pw_object_mixin_data_new (void)
+{
+  WpPwObjectMixinData *d = g_slice_new0 (WpPwObjectMixinData);
+  spa_hook_list_init (&d->hooks);
+  return d;
+}
+
+static void
+wp_pw_object_mixin_data_free (gpointer data)
+{
+  WpPwObjectMixinData *d = data;
+  g_clear_pointer (&d->properties, wp_properties_unref);
+  g_list_free_full (d->params, wp_pw_object_mixin_param_store_free);
+  g_clear_pointer (&d->subscribed_ids, g_array_unref);
+  g_warn_if_fail (d->enum_params_tasks == NULL);
+  g_slice_free (WpPwObjectMixinData, d);
+}
+
+WpPwObjectMixinData *
+wp_pw_object_mixin_get_data (gpointer instance)
+{
+  WpPwObjectMixinData *d = g_object_get_qdata (G_OBJECT (instance),
+      wp_pw_object_mixin_data_quark ());
+  if (G_UNLIKELY (!d)) {
+    d = wp_pw_object_mixin_data_new ();
+    g_object_set_qdata_full (G_OBJECT (instance),
+        wp_pw_object_mixin_data_quark (), d, wp_pw_object_mixin_data_free);
+  }
+  return d;
+}
+
+/****************/
+/* PARAMS STORE */
+
+typedef struct _WpPwObjectMixinParamStore WpPwObjectMixinParamStore;
+struct _WpPwObjectMixinParamStore
+{
+  guint32 param_id;
+  GPtrArray *params;
+};
+
+static WpPwObjectMixinParamStore *
+wp_pw_object_mixin_param_store_new (void)
+{
+  WpPwObjectMixinParamStore *d = g_slice_new0 (WpPwObjectMixinParamStore);
+  return d;
+}
+
+static void
+wp_pw_object_mixin_param_store_free (gpointer data)
+{
+  WpPwObjectMixinParamStore * p = data;
+  g_clear_pointer (&p->params, g_ptr_array_unref);
+  g_slice_free (WpPwObjectMixinParamStore, p);
+}
+
+static gint
+param_store_has_id (gconstpointer param, gconstpointer id)
+{
+  guint32 param_id = ((const WpPwObjectMixinParamStore *) param)->param_id;
+  return (param_id == GPOINTER_TO_UINT (id)) ? 0 : 1;
+}
+
+GPtrArray *
+wp_pw_object_mixin_get_stored_params (WpPwObjectMixinData * data, guint32 id)
+{
+  GList *link = g_list_find_custom (data->params, GUINT_TO_POINTER (id),
+      param_store_has_id);
+  WpPwObjectMixinParamStore *s = link ? link->data : NULL;
+  return (s && s->params) ? g_ptr_array_ref (s->params) : NULL;
+}
+
+void
+wp_pw_object_mixin_store_param (WpPwObjectMixinData * data, guint32 id,
+    guint32 flags, gpointer param)
+{
+  GList *link = g_list_find_custom (data->params, GUINT_TO_POINTER (id),
+      param_store_has_id);
+  WpPwObjectMixinParamStore *s = link ? link->data : NULL;
+  gint16 index = (gint16) (flags & 0xffff);
+
+  /* if the link exists, data must also exist */
+  g_warn_if_fail (!link || link->data);
+
+  if (!s) {
+    if (flags & WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE)
+      return;
+    s = wp_pw_object_mixin_param_store_new ();
+    s->param_id = id;
+    data->params = g_list_append (data->params, s);
+  }
+  else if (s && (flags & WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE)) {
+    wp_pw_object_mixin_param_store_free (s);
+    data->params = g_list_remove_link (data->params, link);
+    return;
+  }
+
+  if (flags & WP_PW_OBJECT_MIXIN_STORE_PARAM_CLEAR)
+    g_clear_pointer (&s->params, g_ptr_array_unref);
+
+  if (!param)
+    return;
+
+  if (flags & WP_PW_OBJECT_MIXIN_STORE_PARAM_ARRAY) {
+    if (!s->params)
+      s->params = (GPtrArray *) param;
+    else
+      g_ptr_array_extend_and_steal (s->params, (GPtrArray *) param);
+  }
+  else {
+    WpSpaPod *param_pod = param;
+
+    if (!s->params)
+      s->params =
+          g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);
+
+    /* copy if necessary to make sure we don't reference
+       `const struct spa_pod *` data allocated on the stack */
+    param_pod = wp_spa_pod_ensure_unique_owner (param_pod);
+    g_ptr_array_insert (s->params, index, param_pod);
+  }
+}
+
+/******************/
+/* PROPERTIES API */
+
+void
+wp_pw_object_mixin_get_property (GObject * object, guint property_id,
     GValue * value, GParamSpec * pspec)
 {
   switch (property_id) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_PROP_NATIVE_INFO:
+  case WP_PW_OBJECT_MIXIN_PROP_NATIVE_INFO:
     g_value_set_pointer (value, (gpointer)
         wp_pipewire_object_get_native_info (WP_PIPEWIRE_OBJECT (object)));
     break;
-  case WP_PIPEWIRE_OBJECT_MIXIN_PROP_PROPERTIES:
+  case WP_PW_OBJECT_MIXIN_PROP_PROPERTIES:
     g_value_set_boxed (value,
         wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (object)));
     break;
-  case WP_PIPEWIRE_OBJECT_MIXIN_PROP_PARAM_INFO:
+  case WP_PW_OBJECT_MIXIN_PROP_PARAM_INFO:
     g_value_set_variant (value,
         wp_pipewire_object_get_param_info (WP_PIPEWIRE_OBJECT (object)));
     break;
@@ -38,70 +459,300 @@ wp_pipewire_object_mixin_get_property (GObject * object, guint property_id,
 }
 
 void
-wp_pipewire_object_mixin_class_override_properties (GObjectClass * klass)
+wp_pw_object_mixin_class_override_properties (GObjectClass * klass)
 {
   g_object_class_override_property (klass,
-      WP_PIPEWIRE_OBJECT_MIXIN_PROP_NATIVE_INFO, "native-info");
+      WP_PW_OBJECT_MIXIN_PROP_NATIVE_INFO, "native-info");
   g_object_class_override_property (klass,
-      WP_PIPEWIRE_OBJECT_MIXIN_PROP_PROPERTIES, "properties");
+      WP_PW_OBJECT_MIXIN_PROP_PROPERTIES, "properties");
   g_object_class_override_property (klass,
-      WP_PIPEWIRE_OBJECT_MIXIN_PROP_PARAM_INFO, "param-info");
+      WP_PW_OBJECT_MIXIN_PROP_PARAM_INFO, "param-info");
 }
 
+/****************/
+/* FEATURES API */
+
 static const struct {
-  gint param_id;
   WpObjectFeatures feature;
-} feature_mappings[] = {
-  { SPA_PARAM_Props, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS },
-  { SPA_PARAM_Format, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT },
-  { SPA_PARAM_Profile, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE },
-  { SPA_PARAM_PortConfig, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG },
-  { SPA_PARAM_Route, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE },
+  guint32 param_ids[2];
+} params_features[] = {
+  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS, { SPA_PARAM_PropInfo, SPA_PARAM_Props } },
+  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT, { SPA_PARAM_EnumFormat, SPA_PARAM_Format } },
+  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE, { SPA_PARAM_EnumProfile, SPA_PARAM_Profile } },
+  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG, { SPA_PARAM_EnumPortConfig, SPA_PARAM_PortConfig } },
+  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE, { SPA_PARAM_EnumRoute, SPA_PARAM_Route } },
 };
 
-WpObjectFeatures
-wp_pipewire_object_mixin_param_info_to_features (struct spa_param_info * info,
-    guint n_params)
+static WpObjectFeatures
+get_feature_for_param_id (guint32 param_id)
 {
-  WpObjectFeatures ft = 0;
+  for (guint i = 0; i < G_N_ELEMENTS (params_features); i++) {
+    if (params_features[i].param_ids[0] == param_id ||
+        params_features[i].param_ids[1] == param_id)
+      return params_features[i].feature;
+  }
+  return 0;
+}
 
-  for (gint i = 0; i < n_params; i++) {
-    for (gint j = 0; j < G_N_ELEMENTS (feature_mappings); j++) {
-      if (info[i].id == feature_mappings[j].param_id &&
-          info[i].flags & SPA_PARAM_INFO_READ)
-        ft |= feature_mappings[j].feature;
-    }
+WpObjectFeatures
+wp_pw_object_mixin_get_supported_features (WpObject * object)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (object);
+  WpObjectFeatures ft =
+      WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO;
+
+  if (d->info && iface->n_params_offset && iface->param_info_offset) {
+    struct spa_param_info * param_info =
+        G_STRUCT_MEMBER (struct spa_param_info *, d->info, iface->param_info_offset);
+    guint32 n_params =
+        G_STRUCT_MEMBER (guint32, d->info, iface->n_params_offset);
+
+    for (guint i = 0; i < n_params; i++)
+      ft |= get_feature_for_param_id (param_info[i].id);
   }
   return ft;
 }
 
 guint
-wp_pipewire_object_mixin_activate_get_next_step (WpObject * object,
+wp_pw_object_mixin_activate_get_next_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
+
   /* bind if not already bound */
-  if (missing & WP_PROXY_FEATURE_BOUND)
-    return WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND;
-  /* then cache info */
+  if (missing & WP_PROXY_FEATURE_BOUND || !d->iface)
+    return WP_PW_OBJECT_MIXIN_STEP_BIND;
+  /* wait for info before proceeding, if necessary */
+  else if ((missing & WP_PIPEWIRE_OBJECT_FEATURES_ALL) && !d->info)
+    return WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO;
+  /* then cache params */
+  else if (missing & WP_PIPEWIRE_OBJECT_FEATURES_ALL)
+    return WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS;
   else
-    return WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO;
+    return WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START;
 
   /* returning to STEP_NONE is handled by WpFeatureActivationTransition */
 }
 
+static void
+enum_params_for_cache_done (GObject * object, GAsyncResult * res, gpointer data)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
+  guint32 param_id = GPOINTER_TO_UINT (data);
+  g_autoptr (GError) error = NULL;
+  g_autoptr (GPtrArray) params = NULL;
+
+  params = g_task_propagate_pointer (G_TASK (res), &error);
+  if (error) {
+    wp_warning_object (object, "enum params failed: %s", error->message);
+    return;
+  }
+
+  wp_debug_object (object, "cached params id:%u, n_params:%u", param_id,
+      params->len);
+
+  wp_pw_object_mixin_store_param (d, param_id,
+      WP_PW_OBJECT_MIXIN_STORE_PARAM_ARRAY |
+      WP_PW_OBJECT_MIXIN_STORE_PARAM_CLEAR |
+      WP_PW_OBJECT_MIXIN_STORE_PARAM_APPEND,
+      g_steal_pointer (&params));
+
+  g_signal_emit_by_name (object, "params-changed", param_id);
+}
+
+G_DEFINE_QUARK (WpPwObjectMixinParamCacheActivatedFeatures, activated_features)
+
+static void
+param_cache_features_enabled (WpCore * core, GAsyncResult * res, gpointer data)
+{
+  WpObject *object = WP_OBJECT (data);
+  WpObjectFeatures activated = GPOINTER_TO_UINT (
+      g_object_get_qdata (G_OBJECT (object), activated_features_quark ()));
+  wp_object_update_features (object, activated, 0);
+}
+
+void
+wp_pw_object_mixin_cache_params (WpObject * object, WpObjectFeatures missing)
+{
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (object);
+  g_autoptr (WpCore) core = wp_object_get_core (object);
+  struct spa_param_info * param_info;
+  WpObjectFeatures activated = 0;
+
+  g_return_if_fail (!(iface->flags & WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE));
+
+  for (gint i = 0; i < G_N_ELEMENTS (params_features); i++) {
+    if (missing & params_features[i].feature) {
+      param_info = find_param_info (object, params_features[i].param_ids[0]);
+      if (param_info && param_info->flags & SPA_PARAM_INFO_READ) {
+        wp_pw_object_mixin_enum_params_unchecked (object,
+            param_info->id, NULL, NULL, enum_params_for_cache_done,
+            GUINT_TO_POINTER (param_info->id));
+      }
+
+      param_info = find_param_info (object, params_features[i].param_ids[1]);
+      if (param_info && param_info->flags & SPA_PARAM_INFO_READ) {
+        wp_pw_object_mixin_enum_params_unchecked (object,
+            param_info->id, NULL, NULL, enum_params_for_cache_done,
+            GUINT_TO_POINTER (param_info->id));
+      }
+
+      activated |= params_features[i].feature;
+    }
+  }
+
+  g_object_set_qdata (G_OBJECT (object),
+      activated_features_quark (), GUINT_TO_POINTER (activated));
+  wp_core_sync (core, NULL,
+      (GAsyncReadyCallback) param_cache_features_enabled, object);
+}
+
 void
-wp_pipewire_object_mixin_cache_info (WpObject * object,
-    WpFeatureActivationTransition * transition)
+wp_pw_object_mixin_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  /* TODO */
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (object);
+
+  /* deactivate param caching */
+  if (!(iface->flags & WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE)) {
+    for (gint i = 0; i < G_N_ELEMENTS (params_features); i++) {
+      if (features & params_features[i].feature) {
+        wp_pw_object_mixin_store_param (d, params_features[i].param_ids[0],
+            WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE, NULL);
+        wp_pw_object_mixin_store_param (d, params_features[i].param_ids[1],
+            WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE, NULL);
+        wp_object_update_features (object, 0, params_features[i].feature);
+      }
+    }
+  }
 }
 
+/************************/
+/* PROXY EVENT HANDLERS */
+
 void
-wp_pipewire_object_mixin_deactivate (WpObject * object,
-    WpObjectFeatures features)
+wp_pw_object_mixin_handle_pw_proxy_destroyed (WpProxy * proxy)
 {
-  /* TODO */
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (proxy);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (proxy);
+
+  g_clear_pointer (&d->properties, wp_properties_unref);
+  g_clear_pointer (&d->info, iface->free_info);
+  d->iface = NULL;
+
+  /* deactivate param caching */
+  if (!(iface->flags & WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE)) {
+    for (gint i = 0; i < G_N_ELEMENTS (params_features); i++) {
+      wp_pw_object_mixin_store_param (d, params_features[i].param_ids[0],
+          WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE, NULL);
+      wp_pw_object_mixin_store_param (d, params_features[i].param_ids[1],
+          WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE, NULL);
+    }
+  }
+
+  /* cancel enum_params tasks */
+  {
+    GList *link;
+    for (link = g_list_first (d->enum_params_tasks);
+         link; link = g_list_first (d->enum_params_tasks)) {
+      g_task_return_new_error (G_TASK (link->data),
+          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
+          "pipewire proxy destroyed before finishing");
+      d->enum_params_tasks = g_list_remove_link (d->enum_params_tasks, link);
+    }
+  }
+
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURES_ALL);
+}
+
+/***************************/
+/* PIPEWIRE EVENT HANDLERS */
+
+void
+wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
+  guint32 change_mask =
+      G_STRUCT_MEMBER (guint32, update, iface->change_mask_offset);
+  guint32 process_info_change_mask =
+      change_mask & ~(iface->CHANGE_MASK_PROPS | iface->CHANGE_MASK_PARAMS);
+  gpointer old_info = NULL;
+
+  wp_debug_object (instance, "info, change_mask:0x%x [%s%s]", change_mask,
+      (change_mask & iface->CHANGE_MASK_PROPS) ? "props," : "",
+      (change_mask & iface->CHANGE_MASK_PARAMS) ? "params," : "");
+
+  /* make a copy of d->info for process_info() */
+  if (iface->process_info && d->info && process_info_change_mask) {
+    /* copy everything that changed except props and params, for efficiency;
+       process_info() is only interested in variables that are not PROPS & PARAMS */
+    G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) =
+        process_info_change_mask;
+    old_info = iface->update_info (NULL, d->info);
+  }
+
+  /* update params */
+  if (!(iface->flags & WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE) &&
+       (change_mask & iface->CHANGE_MASK_PARAMS) && d->info) {
+    struct spa_param_info * old_param_info =
+        G_STRUCT_MEMBER (struct spa_param_info *, d->info, iface->param_info_offset);
+    struct spa_param_info * param_info =
+        G_STRUCT_MEMBER (struct spa_param_info *, update, iface->param_info_offset);
+    guint32 old_n_params =
+        G_STRUCT_MEMBER (guint32, d->info, iface->n_params_offset);
+    guint32 n_params =
+        G_STRUCT_MEMBER (guint32, update, iface->n_params_offset);
+    WpObjectFeatures active_ft =
+        wp_object_get_active_features (WP_OBJECT (instance));
+
+    for (guint i = 0; i < n_params; i++) {
+      /* param changes when flags change */
+      if (i >= old_n_params || old_param_info[i].flags != param_info[i].flags) {
+        /* update cached params if the relevant feature is active */
+        if (active_ft & get_feature_for_param_id (param_info[i].id) &&
+            param_info[i].flags & SPA_PARAM_INFO_READ)
+        {
+          wp_pw_object_mixin_enum_params_unchecked (instance,
+              param_info[i].id, NULL, NULL, enum_params_for_cache_done,
+              GUINT_TO_POINTER (param_info[i].id));
+        }
+      }
+    }
+  }
+
+  /* update our info struct */
+  d->info = iface->update_info (d->info, update);
+  wp_object_update_features (WP_OBJECT (instance),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
+
+  /* update properties */
+  if (change_mask & iface->CHANGE_MASK_PROPS) {
+    const struct spa_dict * props =
+        G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
+
+    g_clear_pointer (&d->properties, wp_properties_unref);
+    d->properties = wp_properties_new_wrap_dict (props);
+
+    g_object_notify (G_OBJECT (instance), "properties");
+  }
+
+  if (change_mask & iface->CHANGE_MASK_PARAMS)
+    g_object_notify (G_OBJECT (instance), "param-info");
+
+  /* custom handling, if required */
+  if (iface->process_info && process_info_change_mask) {
+    iface->process_info (instance, old_info, d->info);
+    g_clear_pointer (&old_info, iface->free_info);
+  }
 }
 
 static gint
@@ -112,147 +763,196 @@ task_has_seq (gconstpointer task, gconstpointer seq)
 }
 
 void
-wp_pipewire_object_mixin_handle_event_param (gpointer instance, int seq,
+wp_pw_object_mixin_handle_event_param (gpointer instance, int seq,
     uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param)
 {
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
   g_autoptr (WpSpaPod) w_param = wp_spa_pod_new_wrap_const (param);
   GList *list;
   GTask *task;
 
-  list = g_object_get_qdata (G_OBJECT (instance), enum_params_tasks_quark ());
-  list = g_list_find_custom (list, GINT_TO_POINTER (seq), task_has_seq);
+  list = g_list_find_custom (d->enum_params_tasks, GINT_TO_POINTER (seq),
+      task_has_seq);
   task = list ? G_TASK (list->data) : NULL;
 
+  wp_trace_boxed (WP_TYPE_SPA_POD, w_param,
+      WP_OBJECT_FORMAT " param id:%u, index:%u",
+      WP_OBJECT_ARGS (instance), id, index);
+
   if (task) {
     GPtrArray *array = g_task_get_task_data (task);
     g_ptr_array_add (array, wp_spa_pod_copy (w_param));
+  } else {
+    /* this should never happen */
+    wp_warning_object (instance,
+        "param event was received without calling enum_params");
   }
 }
 
-GVariant *
-wp_pipewire_object_mixin_param_info_to_gvariant (struct spa_param_info * info,
-    guint n_params)
+/***********************************/
+/* PIPEWIRE METHOD IMPLEMENTATIONS */
+
+int
+wp_pw_object_mixin_impl_add_listener (gpointer instance,
+    struct spa_hook *listener, gconstpointer events, gpointer data)
 {
-  g_auto (GVariantBuilder) b =
-      G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_DICTIONARY);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
+  struct spa_hook_list save;
 
-  if (!info || n_params == 0)
-    return NULL;
+  spa_hook_list_isolate (&d->hooks, &save, listener, events, data);
 
-  for (guint i = 0; i < n_params; i++) {
-    const gchar *nick = NULL;
-    gchar flags[3];
-    guint flags_idx = 0;
+  G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = iface->CHANGE_MASK_ALL;
+  iface->emit_info (&d->hooks, d->info);
+  G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = 0;
 
-    wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, info[i].id, NULL, &nick,
-        NULL);
-    g_return_val_if_fail (nick != NULL, NULL);
+  spa_hook_list_join (&d->hooks, &save);
+  return 0;
+}
 
-    if (info[i].flags & SPA_PARAM_INFO_READ)
-      flags[flags_idx++] = 'r';
-    if (info[i].flags & SPA_PARAM_INFO_WRITE)
-      flags[flags_idx++] = 'w';
-    flags[flags_idx] = '\0';
+int
+wp_pw_object_mixin_impl_enum_params (gpointer instance, int seq,
+    guint32 id, guint32 start, guint32 num, const struct spa_pod *filter)
+{
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
+  g_autoptr (GPtrArray) params = NULL;
+  g_autoptr (WpSpaPod) filter_pod = NULL;
 
-    g_variant_builder_add (&b, "{ss}", nick, flags);
-  }
+  if (!iface->enum_params_sync)
+    return -ENOTSUP;
 
-  return g_variant_builder_end (&b);
-}
+  struct spa_param_info * info = find_param_info (instance, id);
+  if (!info || !(info->flags & SPA_PARAM_INFO_READ))
+    return -EINVAL;
 
-WpIterator *
-wp_pipewire_object_mixin_enum_params_finish (WpPipewireObject * obj,
-    GAsyncResult * res, GError ** error)
-{
-  g_return_val_if_fail (g_task_is_valid (res, obj), NULL);
+  filter_pod = filter ? wp_spa_pod_new_wrap_const (filter) : NULL;
+  params = iface->enum_params_sync (instance, id, start, num, filter_pod);
 
-  GPtrArray *array = g_task_propagate_pointer (G_TASK (res), error);
-  if (!array)
-    return NULL;
+  if (params) {
+    for (guint i = 0; i < params->len; i++) {
+      WpSpaPod *pod = g_ptr_array_index (params, i);
 
-  return wp_iterator_new_ptr_array (array, WP_TYPE_SPA_POD);
+      wp_trace_boxed (WP_TYPE_SPA_POD, pod,
+          WP_OBJECT_FORMAT " emit param id:%u, index:%u",
+          WP_OBJECT_ARGS (instance), id, start+i);
+
+      iface->emit_param (&d->hooks, seq, id, start+i, start+i+1,
+          wp_spa_pod_get_spa_pod (pod));
+    }
+  }
+  return 0;
 }
 
-WpIterator *
-wp_pipewire_object_mixin_enum_cached_params (WpPipewireObject * obj,
-    const gchar * id)
+int
+wp_pw_object_mixin_impl_subscribe_params (gpointer instance,
+    guint32 *ids, guint32 n_ids)
 {
-  return NULL; //TODO
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
+
+  if (!iface->enum_params_sync)
+    return -ENOTSUP;
+
+  for (guint i = 0; i < n_ids; i++)
+    wp_pw_object_mixin_impl_enum_params (instance, 1, ids[i], 0, -1, NULL);
+
+  if (!d->subscribed_ids)
+    d->subscribed_ids = g_array_new (FALSE, FALSE, sizeof (guint32));
+
+  /* FIXME: deduplicate stored ids */
+  g_array_append_vals (d->subscribed_ids, ids, n_ids);
+  return 0;
 }
 
-void
-wp_pipewire_object_mixin_enum_params_unimplemented (WpPipewireObject * obj,
-    const gchar * id, WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
+int
+wp_pw_object_mixin_impl_set_param (gpointer instance, guint32 id,
+    guint32 flags, const struct spa_pod *param)
 {
-  wp_pipewire_object_mixin_create_enum_params_task (obj, 0, cancellable,
-      callback, user_data);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
+
+  if (!iface->set_param)
+    return -ENOTSUP;
+
+  struct spa_param_info * info = find_param_info (instance, id);
+  if (!info || !(info->flags & SPA_PARAM_INFO_WRITE))
+    return -EINVAL;
+
+  WpSpaPod *param_pod = wp_spa_pod_new_wrap_const (param);
+
+  wp_trace_boxed (WP_TYPE_SPA_POD, param_pod,
+          WP_OBJECT_FORMAT " set_param id:%u flags:0x%x",
+          WP_OBJECT_ARGS (instance), id, flags);
+
+  return iface->set_param (instance, id, flags, param_pod);
 }
 
+/**********************/
+/*      NOTIFIERS     */
+
 void
-wp_pipewire_object_mixin_set_param_unimplemented (WpPipewireObject * obj,
-    const gchar * id, WpSpaPod * param)
+wp_pw_object_mixin_notify_info (gpointer instance, guint32 change_mask)
 {
-  wp_warning_object (obj,
-      "setting params is not implemented on this object type");
-}
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
 
-static void
-enum_params_done (WpCore * core, GAsyncResult * res, gpointer data)
-{
-  g_autoptr (GTask) task = G_TASK (data);
-  g_autoptr (GError) error = NULL;
-  gpointer instance = g_task_get_source_object (G_TASK (data));
-  GList *list;
+  wp_debug_object (instance, "notify info, change_mask:0x%x [%s%s]",
+      change_mask,
+      (change_mask & iface->CHANGE_MASK_PROPS) ? "props," : "",
+      (change_mask & iface->CHANGE_MASK_PARAMS) ? "params," : "");
 
-  /* finish the sync task */
-  wp_core_sync_finish (core, res, &error);
+  G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) =
+      (change_mask & iface->CHANGE_MASK_ALL);
+  iface->emit_info (&d->hooks, d->info);
+  G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = 0;
 
-  /* remove the task from the stored list; ref is held by the g_autoptr */
-  list = g_object_get_qdata (G_OBJECT (instance), enum_params_tasks_quark ());
-  list = g_list_remove (list, instance);
-  g_object_set_qdata (G_OBJECT (instance), enum_params_tasks_quark (), list);
+  if (change_mask & iface->CHANGE_MASK_PROPS)
+    g_object_notify (G_OBJECT (instance), "properties");
 
-  if (error)
-    g_task_return_error (task, g_steal_pointer (&error));
-  else {
-    GPtrArray *params = g_task_get_task_data (task);
-    g_task_return_pointer (task, g_ptr_array_ref (params),
-        (GDestroyNotify) g_ptr_array_unref);
-  }
+  if (change_mask & iface->CHANGE_MASK_PARAMS)
+    g_object_notify (G_OBJECT (instance), "param-info");
 }
 
 void
-wp_pipewire_object_mixin_create_enum_params_task (gpointer instance,
-    gint seq, GCancellable * cancellable, GAsyncReadyCallback callback,
-    gpointer user_data)
+wp_pw_object_mixin_notify_params_changed (gpointer instance, guint32 id)
 {
-  g_autoptr (GTask) task = NULL;
-  GPtrArray *params;
-  GList *list;
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  WpPwObjectMixinPrivInterface *iface =
+      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
+  gboolean subscribed = FALSE;
+
+  struct spa_param_info * info = find_param_info (instance, id);
+  g_return_if_fail (info);
+
+  if (d->subscribed_ids) {
+    for (guint i = 0; i < d->subscribed_ids->len; i++) {
+      if (g_array_index (d->subscribed_ids, guint32, i) == id) {
+        subscribed = TRUE;
+        break;
+      }
+    }
+  }
 
-  /* create task */
-  task = g_task_new (instance, cancellable, callback, user_data);
-  params = g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);
-  g_task_set_task_data (task, params, (GDestroyNotify) g_ptr_array_unref);
-  g_task_set_source_tag (task, GINT_TO_POINTER (seq));
-
-  /* return early if seq contains an error */
-  if (G_UNLIKELY (SPA_RESULT_IS_ERROR (seq))) {
-    wp_message_object (instance, "enum_params failed: %s", spa_strerror (seq));
-    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
-        WP_LIBRARY_ERROR_OPERATION_FAILED, "enum_params failed: %s",
-        spa_strerror (seq));
-    return;
+  if (wp_log_level_is_enabled (G_LOG_LEVEL_DEBUG)) {
+    const gchar *name = NULL;
+    wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, id, &name, NULL, NULL);
+    wp_debug_object (instance, "notify param id:%u (%s)", id, name);
   }
 
-  /* store */
-  list = g_object_get_qdata (G_OBJECT (instance), enum_params_tasks_quark ());
-  list = g_list_append (list, g_object_ref (task));
-  g_object_set_qdata (G_OBJECT (instance), enum_params_tasks_quark (), list);
+  /* toggle the serial flag; this notifies that there is a data change */
+  info->flags ^= SPA_PARAM_INFO_SERIAL;
+
+  G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) =
+      iface->CHANGE_MASK_PARAMS;
+  iface->emit_info (&d->hooks, d->info);
+  G_STRUCT_MEMBER (guint32, d->info, iface->change_mask_offset) = 0;
 
-  /* call sync */
-  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (instance));
-  wp_core_sync (core, cancellable, (GAsyncReadyCallback) enum_params_done,
-      task);
+  if (subscribed)
+    wp_pw_object_mixin_impl_enum_params (instance, 1, id, 0, -1, NULL);
+  g_signal_emit_by_name (instance, "params-changed", id);
 }
diff --git a/lib/wp/private/pipewire-object-mixin.h b/lib/wp/private/pipewire-object-mixin.h
index fc86101a..1a8a688d 100644
--- a/lib/wp/private/pipewire-object-mixin.h
+++ b/lib/wp/private/pipewire-object-mixin.h
@@ -10,151 +10,237 @@
 #define __WIREPLUMBER_PIPEWIRE_OBJECT_MIXIN_H__
 
 #include "proxy-interfaces.h"
-#include "spa-type.h"
-#include "spa-pod.h"
-#include "debug.h"
 
 #include <pipewire/pipewire.h>
-#include <spa/utils/result.h>
 
 G_BEGIN_DECLS
 
 enum {
   /* this is the same STEP_BIND as in WpGlobalProxy */
-  WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
-  WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO,
+  WP_PW_OBJECT_MIXIN_STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
+  WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO,
+  WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS,
 
-  WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+  WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START,
 };
 
 enum {
-  WP_PIPEWIRE_OBJECT_MIXIN_PROP_0,
+  WP_PW_OBJECT_MIXIN_PROP_0,
 
   // WpPipewireObject
-  WP_PIPEWIRE_OBJECT_MIXIN_PROP_NATIVE_INFO,
-  WP_PIPEWIRE_OBJECT_MIXIN_PROP_PROPERTIES,
-  WP_PIPEWIRE_OBJECT_MIXIN_PROP_PARAM_INFO,
+  WP_PW_OBJECT_MIXIN_PROP_NATIVE_INFO,
+  WP_PW_OBJECT_MIXIN_PROP_PROPERTIES,
+  WP_PW_OBJECT_MIXIN_PROP_PARAM_INFO,
 
-  WP_PIPEWIRE_OBJECT_MIXIN_PROP_CUSTOM_START,
+  WP_PW_OBJECT_MIXIN_PROP_CUSTOM_START,
 };
 
+#define WP_TYPE_PW_OBJECT_MIXIN_PRIV (wp_pw_object_mixin_priv_get_type ())
+G_DECLARE_INTERFACE (WpPwObjectMixinPriv, wp_pw_object_mixin_priv,
+                     WP, PW_OBJECT_MIXIN_PRIV, WpProxy)
+
+struct _WpPwObjectMixinPrivInterface
+{
+  GTypeInterface parent;
+
+  /* WpPwObjectMixinPriv-specific flags */
+#define WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE (1 << 0)
+  guint32 flags;
+
+  /* pipewire info struct abstraction layer */
+  gsize info_size;
+  gsize change_mask_offset;
+  gsize props_offset;
+  gsize param_info_offset;
+  gsize n_params_offset;
+
+  guint32 CHANGE_MASK_ALL;
+  guint32 CHANGE_MASK_PROPS;
+  guint32 CHANGE_MASK_PARAMS;
+
+  gpointer (*update_info) (gpointer info, gconstpointer update);
+  void (*free_info) (gpointer info);
+
+  /* to further process info struct updates - for proxy objects only */
+  void (*process_info) (gpointer instance, gpointer old_info, gpointer info);
+
+  /* pipewire interface methods - proxy & impl */
+  gint (*enum_params) (gpointer instance, guint32 id,
+      guint32 start, guint32 num, WpSpaPod *filter);
+  GPtrArray * (*enum_params_sync) (gpointer instance, guint32 id,
+      guint32 start, guint32 num, WpSpaPod *filter);
+  gint (*set_param) (gpointer instance, guint32 id, guint32 flags,
+      WpSpaPod *param /* transfer full */);
+
+  /* pipewire interface events - for impl objects only */
+  void (*emit_info) (struct spa_hook_list * hooks, gconstpointer info);
+  void (*emit_param) (struct spa_hook_list * hooks, int seq,
+      guint32 id, guint32 index, guint32 next, const struct spa_pod *param);
+};
+
+/* fills in info struct abstraction layer in WpPwObjectMixinPrivInterface */
+#define wp_pw_object_mixin_priv_interface_info_init(iface, type, TYPE) \
+({ \
+  iface->info_size = sizeof (struct pw_ ## type ## _info); \
+  iface->change_mask_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, change_mask); \
+  iface->props_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, props); \
+  iface->param_info_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, params); \
+  iface->n_params_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, n_params); \
+  iface->CHANGE_MASK_ALL = PW_ ## TYPE ## _CHANGE_MASK_ALL; \
+  iface->CHANGE_MASK_PROPS = PW_ ## TYPE ## _CHANGE_MASK_PROPS; \
+  iface->CHANGE_MASK_PARAMS = PW_ ## TYPE ## _CHANGE_MASK_PARAMS; \
+  iface->update_info = (gpointer (*)(gpointer, gconstpointer)) pw_ ## type ## _info_update; \
+  iface->free_info = (void (*)(gpointer)) pw_ ## type ## _info_free; \
+})
+
+/* same as above, for types that don't have params */
+#define wp_pw_object_mixin_priv_interface_info_init_no_params(iface, type, TYPE) \
+({ \
+  iface->flags = WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE; \
+  iface->info_size = sizeof (struct pw_ ## type ## _info); \
+  iface->change_mask_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, change_mask); \
+  iface->props_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, props); \
+  iface->param_info_offset = 0; \
+  iface->n_params_offset = 0; \
+  iface->CHANGE_MASK_ALL = PW_ ## TYPE ## _CHANGE_MASK_ALL; \
+  iface->CHANGE_MASK_PROPS = PW_ ## TYPE ## _CHANGE_MASK_PROPS; \
+  iface->CHANGE_MASK_PARAMS = 0; \
+  iface->update_info = (gpointer (*)(gpointer, gconstpointer)) pw_ ## type ## _info_update; \
+  iface->free_info = (void (*)(gpointer)) pw_ ## type ## _info_free; \
+})
+
+/*************/
+/* INTERFACE */
+
+/* implements WpPipewireObject for an object that implements WpPwObjectMixinPriv */
+void wp_pw_object_mixin_object_interface_init (WpPipewireObjectInterface * iface);
+
+/********/
+/* DATA */
+
+typedef struct _WpPwObjectMixinData WpPwObjectMixinData;
+struct _WpPwObjectMixinData
+{
+  gpointer info;            /* pointer to the info struct */
+  gpointer iface;           /* pointer to the interface (ex. pw_endpoint) */
+  struct spa_hook listener;
+  struct spa_hook_list hooks;
+  WpProperties *properties;
+  GList *enum_params_tasks;  /* element-type: GTask* */
+  GList *params;             /* element-type: WpPwObjectMixinParamStore* */
+  GArray *subscribed_ids;    /* element-type: guint32 */
+};
+
+/* get mixin data (stored as qdata on the @instance) */
+WpPwObjectMixinData * wp_pw_object_mixin_get_data (gpointer instance);
+
+/****************/
+/* PARAMS STORE */
+
+/* param store access; (transfer container) */
+GPtrArray * wp_pw_object_mixin_get_stored_params (WpPwObjectMixinData * data,
+    guint32 id);
+
+/* param store manipulation
+ * @flags: see below
+ * @param: (transfer full): WpSpaPod* or GPtrArray* */
+void wp_pw_object_mixin_store_param (WpPwObjectMixinData * data, guint32 id,
+    guint32 flags, gpointer param);
+
+/* set the index at which to store the new param */
+#define WP_PW_OBJECT_MIXIN_STORE_PARAM_SET(x)  ((x) & 0x7fff)
+#define WP_PW_OBJECT_MIXIN_STORE_PARAM_APPEND  (0xffff)
+#define WP_PW_OBJECT_MIXIN_STORE_PARAM_PREPEND (0)
+/* @param is a GPtrArray* */
+#define WP_PW_OBJECT_MIXIN_STORE_PARAM_ARRAY   (1 << 16)
+/* clear the existing array of params before storing */
+#define WP_PW_OBJECT_MIXIN_STORE_PARAM_CLEAR   (1 << 17)
+/* completely remove stored params for @id */
+#define WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE  (1 << 18)
+
 /******************/
 /* PROPERTIES API */
 
 /* assign to get_property or chain it from there */
-void wp_pipewire_object_mixin_get_property (GObject * object, guint property_id,
+void wp_pw_object_mixin_get_property (GObject * object, guint property_id,
     GValue * value, GParamSpec * pspec);
 
 /* call from class_init */
-void wp_pipewire_object_mixin_class_override_properties (GObjectClass * klass);
+void wp_pw_object_mixin_class_override_properties (GObjectClass * klass);
 
 /****************/
 /* FEATURES API */
 
 /* call from get_supported_features */
-WpObjectFeatures wp_pipewire_object_mixin_param_info_to_features (
-    struct spa_param_info * info, guint n_params);
+WpObjectFeatures wp_pw_object_mixin_get_supported_features (WpObject * object);
 
 /* assign directly to activate_get_next_step */
-guint wp_pipewire_object_mixin_activate_get_next_step (WpObject * object,
+guint wp_pw_object_mixin_activate_get_next_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing);
 
 /* call from activate_execute_step when
-   step == WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO */
-void wp_pipewire_object_mixin_cache_info (WpObject * object,
-    WpFeatureActivationTransition * transition);
+   step == WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS */
+void wp_pw_object_mixin_cache_params (WpObject * object,
+    WpObjectFeatures missing);
 
 /* handle deactivation of PARAM_* caching features */
-void wp_pipewire_object_mixin_deactivate (WpObject * object,
+void wp_pw_object_mixin_deactivate (WpObject * object,
     WpObjectFeatures features);
 
-/***************************/
-/* PIPEWIRE EVENT HANDLERS */
+/************************/
+/* PROXY EVENT HANDLERS */
+/*  (for proxy objects) */
 
-/* call at the end of the info event callback */
-#define wp_pipewire_object_mixin_handle_event_info(instance, info, CM_PROPS, CM_PARAMS) \
+#define wp_pw_object_mixin_handle_pw_proxy_created(instance, pw_proxy, type, events) \
 ({ \
-  if (info->change_mask & CM_PROPS) \
-    g_object_notify (G_OBJECT (instance), "properties"); \
-  \
-  if (info->change_mask & CM_PARAMS) \
-    g_object_notify (G_OBJECT (instance), "param-info"); \
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance); \
+  d->iface = pw_proxy; \
+  pw_ ## type ## _add_listener ((struct pw_ ## type *) pw_proxy, \
+      &d->listener, events, instance); \
 })
 
-/* assign as the param event callback */
-void wp_pipewire_object_mixin_handle_event_param (gpointer instance, int seq,
-    uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param);
+void wp_pw_object_mixin_handle_pw_proxy_destroyed (WpProxy * proxy);
 
-/***********************/
-/* PIPEWIRE OBJECT API */
+/***************************/
+/* PIPEWIRE EVENT HANDLERS */
+/*   (for proxy objects)   */
 
-/* call from get_param_info */
-GVariant * wp_pipewire_object_mixin_param_info_to_gvariant (
-    struct spa_param_info * info, guint n_params);
+#define HandleEventInfoFunc(type) \
+  void (*)(void *, const struct pw_ ## type ## _info *)
 
-/* assign directly to enum_params_finish */
-WpIterator * wp_pipewire_object_mixin_enum_params_finish (WpPipewireObject * obj,
-    GAsyncResult * res, GError ** error);
+void wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer info);
 
-/* assign directly to enum_cached_params */
-WpIterator * wp_pipewire_object_mixin_enum_cached_params (WpPipewireObject * obj,
-    const gchar * id);
+/* assign as the param event callback */
+void wp_pw_object_mixin_handle_event_param (gpointer instance, int seq,
+    guint32 id, guint32 index, guint32 next, const struct spa_pod *param);
 
-/* assign to enum_params on objects that don't support params (like pw_link) */
-void wp_pipewire_object_mixin_enum_params_unimplemented (WpPipewireObject * obj,
-    const gchar * id, WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data);
+/***********************************/
+/* PIPEWIRE METHOD IMPLEMENTATIONS */
+/*       (for impl objects)        */
 
-/* assign to set_param on objects that don't support params (like pw_link) */
-void wp_pipewire_object_mixin_set_param_unimplemented (WpPipewireObject * obj,
-    const gchar * id, WpSpaPod * param);
+#define ImplAddListenerFunc(type) \
+  int (*)(void *, struct spa_hook *, const struct pw_ ## type ## _events *, void *)
 
-/* call from enum_params */
-#define wp_pipewire_object_mixin_enum_params(type, instance, id, filter, cancellable, callback, user_data) \
-({ \
-  struct type *pwp; \
-  gint seq; \
-  guint32 id_num = 0; \
-  \
-  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &id_num, \
-          NULL, NULL)) { \
-    wp_critical_object (instance, "invalid param id: %s", id); \
-  } else { \
-    pwp = (struct type *) wp_proxy_get_pw_proxy (WP_PROXY (instance)); \
-    seq = type ## _enum_params (pwp, 0, id_num, 0, -1, \
-        filter ? wp_spa_pod_get_spa_pod (filter) : NULL); \
-    \
-    wp_pipewire_object_mixin_create_enum_params_task (instance, seq, cancellable, \
-        callback, user_data); \
-  } \
-})
+int wp_pw_object_mixin_impl_add_listener (gpointer instance,
+    struct spa_hook *listener, gconstpointer events, gpointer data);
 
-/* call from set_param */
-#define wp_pipewire_object_mixin_set_param(type, instance, id, param) \
-({ \
-  struct type *pwp; \
-  gint ret; \
-  guint32 id_num = 0; \
-  \
-  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &id_num, \
-          NULL, NULL)) { \
-    wp_critical_object (instance, "invalid param id: %s", id); \
-  } else { \
-    pwp = (struct type *) wp_proxy_get_pw_proxy (WP_PROXY (instance)); \
-    ret = type ## _set_param (pwp, id_num, 0, wp_spa_pod_get_spa_pod (param)); \
-    if (G_UNLIKELY (SPA_RESULT_IS_ERROR (ret))) { \
-      wp_message_object (instance, "set_param failed: %s", spa_strerror (ret)); \
-    } \
-  } \
-})
+int wp_pw_object_mixin_impl_enum_params (gpointer instance, int seq,
+    guint32 id, guint32 start, guint32 num, const struct spa_pod *filter);
+
+int wp_pw_object_mixin_impl_subscribe_params (gpointer instance,
+    guint32 *ids, guint32 n_ids);
+
+int wp_pw_object_mixin_impl_set_param (gpointer instance, guint32 id,
+    guint32 flags, const struct spa_pod *param);
+
+/**********************/
+/*      NOTIFIERS     */
+/* (for impl objects) */
 
-/************/
-/* INTERNAL */
+void wp_pw_object_mixin_notify_info (gpointer instance, guint32 change_mask);
 
-void wp_pipewire_object_mixin_create_enum_params_task (gpointer instance,
-    gint seq, GCancellable * cancellable, GAsyncReadyCallback callback,
-    gpointer user_data);
+void wp_pw_object_mixin_notify_params_changed (gpointer instance, guint32 id);
 
 G_END_DECLS
 
diff --git a/lib/wp/proxy-interfaces.c b/lib/wp/proxy-interfaces.c
index 8524da7c..b228e653 100644
--- a/lib/wp/proxy-interfaces.c
+++ b/lib/wp/proxy-interfaces.c
@@ -45,8 +45,20 @@ wp_pipewire_object_default_init (WpPipewireObjectInterface * iface)
           "The param info of the object", G_VARIANT_TYPE ("a{ss}"), NULL,
           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
+  /**
+   * WpPipewireObject::params-changed:
+   * @self: the pipewire object
+   * @id: the parameter id
+   *
+   * Emitted when the params for @id have changed. On proxies that cache
+   * params from a remote object, this is emitted after the cached values
+   * have changed.
+   *
+   * You can expect this to be emitted only when the relevant
+   * WP_PIPEWIRE_OBJECT_FEATURE_PARAM_* has been activated.
+   */
   g_signal_new ("params-changed", G_TYPE_FROM_INTERFACE (iface),
-      G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
+      G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT);
 }
 
 /**
@@ -216,6 +228,7 @@ wp_pipewire_object_enum_params_finish (WpPipewireObject * self,
  * wp_pipewire_object_enum_cached_params
  * @self: the pipewire object
  * @id: the parameter id to enumerate
+ * @filter: (nullable): a param filter or %NULL
  *
  * This method can be used to retrieve object parameters in a synchronous way
  * (in contrast with wp_pipewire_object_enum_params(), which is async),
@@ -234,30 +247,35 @@ wp_pipewire_object_enum_params_finish (WpPipewireObject * self,
  *    the items in the iterator are #WpSpaPod
  */
 WpIterator *
-wp_pipewire_object_enum_cached_params (WpPipewireObject * self,
-    const gchar * id)
+wp_pipewire_object_enum_params_sync (WpPipewireObject * self,
+    const gchar * id, WpSpaPod * filter)
 {
   g_return_val_if_fail (WP_IS_PIPEWIRE_OBJECT (self), NULL);
-  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_cached_params,
+  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_params_sync,
       NULL);
 
-  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_cached_params (self, id);
+  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_params_sync (self, id,
+      filter);
 }
 
 /**
  * wp_pipewire_object_set_param:
  * @self: the pipewire object
  * @id: the parameter id to set
- * @param: the parameter to set
+ * @flags: optional flags or 0
+ * @param: (transfer full): the parameter to set
  *
  * Sets a parameter on the object.
+ *
+ * Returns: %TRUE on success, %FALSE if setting the param failed
  */
-void
+gboolean
 wp_pipewire_object_set_param (WpPipewireObject * self, const gchar * id,
-    WpSpaPod * param)
+    guint32 flags, WpSpaPod * param)
 {
-  g_return_if_fail (WP_IS_PIPEWIRE_OBJECT (self));
-  g_return_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->set_param);
+  g_return_val_if_fail (WP_IS_PIPEWIRE_OBJECT (self), FALSE);
+  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->set_param, FALSE);
 
-  WP_PIPEWIRE_OBJECT_GET_IFACE (self)->set_param (self, id, param);
+  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->set_param (self, id, flags,
+      param);
 }
diff --git a/lib/wp/proxy-interfaces.h b/lib/wp/proxy-interfaces.h
index 7b0d3ba8..62b83280 100644
--- a/lib/wp/proxy-interfaces.h
+++ b/lib/wp/proxy-interfaces.h
@@ -36,17 +36,17 @@ struct _WpPipewireObjectInterface
   GVariant * (*get_param_info) (WpPipewireObject * self);
 
   void (*enum_params) (WpPipewireObject * self, const gchar * id,
-      WpSpaPod *filter, GCancellable * cancellable,
+      WpSpaPod * filter, GCancellable * cancellable,
       GAsyncReadyCallback callback, gpointer user_data);
 
   WpIterator * (*enum_params_finish) (WpPipewireObject * self,
       GAsyncResult * res, GError ** error);
 
-  WpIterator * (*enum_cached_params) (WpPipewireObject * self,
-      const gchar * id);
+  WpIterator * (*enum_params_sync) (WpPipewireObject * self,
+      const gchar * id, WpSpaPod * filter);
 
-  void (*set_param) (WpPipewireObject * self, const gchar * id,
-      WpSpaPod * param);
+  gboolean (*set_param) (WpPipewireObject * self, const gchar * id,
+      guint32 flags, WpSpaPod * param);
 };
 
 WP_API
@@ -75,12 +75,12 @@ WpIterator * wp_pipewire_object_enum_params_finish (WpPipewireObject * self,
     GAsyncResult * res, GError ** error);
 
 WP_API
-WpIterator * wp_pipewire_object_enum_cached_params (WpPipewireObject * self,
-    const gchar * id);
+WpIterator * wp_pipewire_object_enum_params_sync (WpPipewireObject * self,
+    const gchar * id, WpSpaPod * filter);
 
 WP_API
-void wp_pipewire_object_set_param (WpPipewireObject * self, const gchar * id,
-    WpSpaPod * param);
+gboolean wp_pipewire_object_set_param (WpPipewireObject * self,
+    const gchar * id, guint32 flags, WpSpaPod * param);
 
 
 G_END_DECLS
diff --git a/lib/wp/session.c b/lib/wp/session.c
index 59220ebc..e289b182 100644
--- a/lib/wp/session.c
+++ b/lib/wp/session.c
@@ -16,6 +16,7 @@
 #include "session.h"
 #include "object-manager.h"
 #include "error.h"
+#include "debug.h"
 #include "wpenums.h"
 #include "private/impl-endpoint.h"
 #include "private/pipewire-object-mixin.h"
@@ -35,15 +36,12 @@ static guint32 signals[N_SIGNALS] = {0};
 typedef struct _WpSessionPrivate WpSessionPrivate;
 struct _WpSessionPrivate
 {
-  WpProperties *properties;
-  struct pw_session_info *info;
-  struct pw_session *iface;
-  struct spa_hook listener;
   WpObjectManager *endpoints_om;
   WpObjectManager *links_om;
 };
 
-static void wp_session_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+static void wp_session_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
 
 /**
  * WpSession:
@@ -57,7 +55,10 @@ static void wp_session_pipewire_object_interface_init (WpPipewireObjectInterface
  */
 G_DEFINE_TYPE_WITH_CODE (WpSession, wp_session, WP_TYPE_GLOBAL_PROXY,
     G_ADD_PRIVATE (WpSession)
-    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_session_pipewire_object_interface_init));
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
+        wp_pw_object_mixin_object_interface_init)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_session_pw_object_mixin_priv_interface_init))
 
 static void
 wp_session_init (WpSession * self)
@@ -156,57 +157,39 @@ wp_session_enable_features_endpoints_links (WpSession * self,
 static WpObjectFeatures
 wp_session_get_supported_features (WpObject * object)
 {
-  WpSession *self = WP_SESSION (object);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-
-  return
-      WP_PROXY_FEATURE_BOUND |
-      WP_SESSION_FEATURE_ENDPOINTS |
-      WP_SESSION_FEATURE_LINKS |
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
-      wp_pipewire_object_mixin_param_info_to_features (
-          priv->info ? priv->info->params : NULL,
-          priv->info ? priv->info->n_params : 0);
+  return wp_pw_object_mixin_get_supported_features(object)
+      | WP_SESSION_FEATURE_ENDPOINTS
+      | WP_SESSION_FEATURE_LINKS;
 }
 
 enum {
-  STEP_CHILDREN = WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+  STEP_CHILDREN = WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START,
 };
 
-static guint
-wp_session_activate_get_next_step (WpObject * object,
-    WpFeatureActivationTransition * transition, guint step,
-    WpObjectFeatures missing)
-{
-  step = wp_pipewire_object_mixin_activate_get_next_step (object, transition,
-      step, missing);
-
-  /* extend the mixin's state machine; when the only remaining feature(s) to
-     enable are FEATURE_ENDPOINTS or FEATURE_LINKS, advance to STEP_CHILDREN */
-  if (step == WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO &&
-      (missing & ~(WP_SESSION_FEATURE_ENDPOINTS | WP_SESSION_FEATURE_LINKS)) == 0)
-    return STEP_CHILDREN;
-
-  return step;
-}
-
 static void
 wp_session_activate_execute_step (WpObject * object,
     WpFeatureActivationTransition * transition, guint step,
     WpObjectFeatures missing)
 {
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
-    wp_pipewire_object_mixin_cache_info (object, transition);
+  case WP_PW_OBJECT_MIXIN_STEP_BIND:
+  case WP_TRANSITION_STEP_ERROR:
+    /* base class can handle BIND and ERROR */
+    WP_OBJECT_CLASS (wp_session_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
+    wp_pw_object_mixin_cache_params (object, missing);
     break;
   case STEP_CHILDREN:
     wp_session_enable_features_endpoints_links (WP_SESSION (object),
         missing);
     break;
   default:
-    WP_OBJECT_CLASS (wp_session_parent_class)->
-        activate_execute_step (object, transition, step, missing);
-    break;
+    g_assert_not_reached ();
   }
 }
 
@@ -216,7 +199,7 @@ wp_session_deactivate (WpObject * object, WpObjectFeatures features)
   WpSession *self = WP_SESSION (object);
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
 
-  wp_pipewire_object_mixin_deactivate (object, features);
+  wp_pw_object_mixin_deactivate (object, features);
 
   if (features & WP_SESSION_FEATURE_ENDPOINTS) {
     g_clear_object (&priv->endpoints_om);
@@ -230,40 +213,17 @@ wp_session_deactivate (WpObject * object, WpObjectFeatures features)
   WP_OBJECT_CLASS (wp_session_parent_class)->deactivate (object, features);
 }
 
-static void
-session_event_info (void *data, const struct pw_session_info *info)
-{
-  WpSession *self = WP_SESSION (data);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-
-  priv->info = pw_session_info_update (priv->info, info);
-
-  if (info->change_mask & PW_SESSION_CHANGE_MASK_PROPS) {
-    g_clear_pointer (&priv->properties, wp_properties_unref);
-    priv->properties = wp_properties_new_wrap_dict (priv->info->props);
-  }
-
-  wp_object_update_features (WP_OBJECT (self),
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
-
-  wp_pipewire_object_mixin_handle_event_info (self, info,
-      PW_SESSION_CHANGE_MASK_PROPS, PW_SESSION_CHANGE_MASK_PARAMS);
-}
-
 static const struct pw_session_events session_events = {
   PW_VERSION_SESSION_EVENTS,
-  .info = session_event_info,
-  .param = wp_pipewire_object_mixin_handle_event_param,
+  .info = (HandleEventInfoFunc(session)) wp_pw_object_mixin_handle_event_info,
+  .param = wp_pw_object_mixin_handle_event_param,
 };
 
 static void
 wp_session_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
-  WpSession *self = WP_SESSION (proxy);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-
-  priv->iface = (struct pw_session *) pw_proxy;
-  pw_session_add_listener (priv->iface, &priv->listener, &session_events, self);
+  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
+      session, &session_events);
 }
 
 static void
@@ -272,17 +232,13 @@ wp_session_pw_proxy_destroyed (WpProxy * proxy)
   WpSession *self = WP_SESSION (proxy);
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
 
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&priv->info, pw_session_info_free);
+  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
+
   g_clear_object (&priv->endpoints_om);
   g_clear_object (&priv->links_om);
   wp_object_update_features (WP_OBJECT (self), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
       WP_SESSION_FEATURE_ENDPOINTS |
       WP_SESSION_FEATURE_LINKS);
-
-  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
-      WP_OBJECT_FEATURES_ALL);
 }
 
 static void
@@ -292,10 +248,11 @@ wp_session_class_init (WpSessionClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->get_property = wp_pipewire_object_mixin_get_property;
+  object_class->get_property = wp_pw_object_mixin_get_property;
 
   wpobject_class->get_supported_features = wp_session_get_supported_features;
-  wpobject_class->activate_get_next_step = wp_session_activate_get_next_step;
+  wpobject_class->activate_get_next_step =
+      wp_pw_object_mixin_activate_get_next_step;
   wpobject_class->activate_execute_step = wp_session_activate_execute_step;
   wpobject_class->deactivate = wp_session_deactivate;
 
@@ -304,7 +261,7 @@ wp_session_class_init (WpSessionClass * klass)
   proxy_class->pw_proxy_created = wp_session_pw_proxy_created;
   proxy_class->pw_proxy_destroyed = wp_session_pw_proxy_destroyed;
 
-  wp_pipewire_object_mixin_class_override_properties (object_class);
+  wp_pw_object_mixin_class_override_properties (object_class);
 
   /**
    * WpSession::default-endpoint-changed:
@@ -344,60 +301,30 @@ wp_session_class_init (WpSessionClass * klass)
       G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 }
 
-static gconstpointer
-wp_session_get_native_info (WpPipewireObject * obj)
+static gint
+wp_session_enum_params (gpointer instance, guint32 id,
+    guint32 start, guint32 num, WpSpaPod *filter)
 {
-  WpSession *self = WP_SESSION (obj);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-
-  return priv->info;
-}
-
-static WpProperties *
-wp_session_get_properties (WpPipewireObject * obj)
-{
-  WpSession *self = WP_SESSION (obj);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-
-  return wp_properties_ref (priv->properties);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_session_enum_params (d->iface, 0, id, start, num,
+      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
 }
 
-static GVariant *
-wp_session_get_param_info (WpPipewireObject * obj)
-{
-  WpSession *self = WP_SESSION (obj);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-
-  return wp_pipewire_object_mixin_param_info_to_gvariant (priv->info->params,
-      priv->info->n_params);
-}
-
-static void
-wp_session_enum_params (WpPipewireObject * obj, const gchar * id,
-    WpSpaPod *filter, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
-{
-  wp_pipewire_object_mixin_enum_params (pw_session, obj, id, filter,
-      cancellable, callback, user_data);
-}
-
-static void
-wp_session_set_param (WpPipewireObject * obj, const gchar * id,
+static gint
+wp_session_set_param (gpointer instance, guint32 id, guint32 flags,
     WpSpaPod * param)
 {
-  wp_pipewire_object_mixin_set_param (pw_session, obj, id, param);
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
+  return pw_session_set_param (d->iface, id, flags,
+      wp_spa_pod_get_spa_pod (param));
 }
 
 static void
-wp_session_pipewire_object_interface_init (
-    WpPipewireObjectInterface * iface)
+wp_session_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
 {
-  iface->get_native_info = wp_session_get_native_info;
-  iface->get_properties = wp_session_get_properties;
-  iface->get_param_info = wp_session_get_param_info;
+  wp_pw_object_mixin_priv_interface_info_init (iface, session, SESSION);
   iface->enum_params = wp_session_enum_params;
-  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
-  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
   iface->set_param = wp_session_set_param;
 }
 
@@ -416,8 +343,8 @@ wp_session_get_name (WpSession * self)
   g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_PIPEWIRE_OBJECT_FEATURE_INFO, NULL);
 
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-  return wp_properties_get (priv->properties, "session.name");
+  return wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (self),
+      "session.name");
 }
 
 /**
@@ -690,106 +617,63 @@ struct _WpImplSession
   WpSession parent;
 
   struct spa_interface iface;
-  struct spa_hook_list hooks;
   struct pw_session_info info;
 };
 
+
+static void wp_session_impl_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface);
+
 /**
  * WpImplSession:
  *
  * A #WpImplSession allows implementing a session and exporting it to PipeWire.
  * To export a #WpImplSession, activate %WP_PROXY_FEATURE_BOUND.
  */
-G_DEFINE_TYPE (WpImplSession, wp_impl_session, WP_TYPE_SESSION)
-
-#define pw_session_emit(hooks,method,version,...) \
-    spa_hook_list_call_simple(hooks, struct pw_session_events, \
-        method, version, ##__VA_ARGS__)
-
-#define pw_session_emit_info(hooks,...)  pw_session_emit(hooks, info, 0, ##__VA_ARGS__)
-#define pw_session_emit_param(hooks,...) pw_session_emit(hooks, param, 0, ##__VA_ARGS__)
-
-static int
-impl_add_listener(void *object,
-    struct spa_hook *listener,
-    const struct pw_session_events *events,
-    void *data)
-{
-  WpImplSession *self = WP_IMPL_SESSION (object);
-  struct spa_hook_list save;
-
-  spa_hook_list_isolate (&self->hooks, &save, listener, events, data);
-
-  self->info.change_mask = PW_SESSION_CHANGE_MASK_ALL;
-  pw_session_emit_info (&self->hooks, &self->info);
-  self->info.change_mask = 0;
-
-  spa_hook_list_join (&self->hooks, &save);
-  return 0;
-}
-
-static int
-impl_enum_params (void *object, int seq,
-    uint32_t id, uint32_t start, uint32_t num,
-    const struct spa_pod *filter)
-{
-  return -ENOENT;
-}
-
-static int
-impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
-{
-  return 0;
-}
-
-static int
-impl_set_param (void *object, uint32_t id, uint32_t flags,
-    const struct spa_pod *param)
-{
-  return -ENOENT;
-}
+G_DEFINE_TYPE_WITH_CODE (WpImplSession, wp_impl_session, WP_TYPE_SESSION,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
+        wp_session_impl_pw_object_mixin_priv_interface_init))
 
 static const struct pw_session_methods impl_session = {
   PW_VERSION_SESSION_METHODS,
-  .add_listener = impl_add_listener,
-  .subscribe_params = impl_subscribe_params,
-  .enum_params = impl_enum_params,
-  .set_param = impl_set_param,
+  .add_listener =
+      (ImplAddListenerFunc(session)) wp_pw_object_mixin_impl_add_listener,
+  .subscribe_params = wp_pw_object_mixin_impl_subscribe_params,
+  .enum_params = wp_pw_object_mixin_impl_enum_params,
+  .set_param = wp_pw_object_mixin_impl_set_param,
 };
 
 static void
 wp_impl_session_init (WpImplSession * self)
 {
-  /* 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 */
-  WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
 
   self->iface = SPA_INTERFACE_INIT (
       PW_TYPE_INTERFACE_Session,
       PW_VERSION_SESSION,
       &impl_session, self);
-  spa_hook_list_init (&self->hooks);
 
-  priv->iface = (struct pw_session *) &self->iface;
+  d->info = &self->info;
+  d->iface = &self->iface;
 
   /* prepare INFO */
-  priv->properties = wp_properties_new_empty ();
+  d->properties = wp_properties_new_empty ();
   self->info.version = PW_VERSION_SESSION_INFO;
-  self->info.props =
-      (struct spa_dict *) wp_properties_peek_dict (priv->properties);
+  self->info.props = (struct spa_dict *) wp_properties_peek_dict (d->properties);
   self->info.params = NULL;
   self->info.n_params = 0;
-  priv->info = &self->info;
+
+  wp_object_update_features (WP_OBJECT (self),
+        WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 }
 
 static void
-wp_impl_session_finalize (GObject * object)
+wp_impl_session_dispose (GObject * object)
 {
-  WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (object));
-
-  g_clear_pointer (&priv->properties, wp_properties_unref);
+  wp_object_update_features (WP_OBJECT (object), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
 
-  G_OBJECT_CLASS (wp_impl_session_parent_class)->finalize (object);
+  G_OBJECT_CLASS (wp_impl_session_parent_class)->dispose (object);
 }
 
 static void
@@ -798,10 +682,10 @@ wp_impl_session_activate_execute_step (WpObject * object,
     WpObjectFeatures missing)
 {
   WpImplSession *self = WP_IMPL_SESSION (object);
-  WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
 
   switch (step) {
-  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND: {
+  case WP_PW_OBJECT_MIXIN_STEP_BIND: {
     g_autoptr (WpCore) core = wp_object_get_core (object);
     struct pw_core *pw_core = wp_core_get_pw_core (core);
 
@@ -815,17 +699,14 @@ wp_impl_session_activate_execute_step (WpObject * object,
     }
 
     /* make sure these props are not present; they are added by the server */
-    wp_properties_set (priv->properties, PW_KEY_OBJECT_ID, NULL);
-    wp_properties_set (priv->properties, PW_KEY_CLIENT_ID, NULL);
-    wp_properties_set (priv->properties, PW_KEY_FACTORY_ID, NULL);
+    wp_properties_set (d->properties, PW_KEY_OBJECT_ID, NULL);
+    wp_properties_set (d->properties, PW_KEY_CLIENT_ID, NULL);
+    wp_properties_set (d->properties, PW_KEY_FACTORY_ID, NULL);
 
     wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
             PW_TYPE_INTERFACE_Session,
-            wp_properties_peek_dict (priv->properties),
-            priv->iface, 0));
-
-    wp_object_update_features (WP_OBJECT (self),
-        WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
+            wp_properties_peek_dict (d->properties),
+            &self->iface, 0));
     break;
   }
   default:
@@ -844,7 +725,6 @@ wp_impl_session_pw_proxy_destroyed (WpProxy * proxy)
   g_clear_object (&priv->endpoints_om);
   g_clear_object (&priv->links_om);
   wp_object_update_features (WP_OBJECT (self), 0,
-      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
       WP_SESSION_FEATURE_ENDPOINTS |
       WP_SESSION_FEATURE_LINKS);
 }
@@ -856,7 +736,7 @@ wp_impl_session_class_init (WpImplSessionClass * klass)
   WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_impl_session_finalize;
+  object_class->dispose = wp_impl_session_dispose;
 
   wpobject_class->activate_execute_step = wp_impl_session_activate_execute_step;
 
@@ -864,6 +744,32 @@ wp_impl_session_class_init (WpImplSessionClass * klass)
   proxy_class->pw_proxy_destroyed = wp_impl_session_pw_proxy_destroyed;
 }
 
+#define pw_session_emit(hooks,method,version,...) \
+    spa_hook_list_call_simple(hooks, struct pw_session_events, \
+        method, version, ##__VA_ARGS__)
+
+static void
+wp_impl_session_emit_info (struct spa_hook_list * hooks, gconstpointer info)
+{
+  pw_session_emit (hooks, info, 0, info);
+}
+
+static void
+wp_impl_session_emit_param (struct spa_hook_list * hooks, int seq,
+      guint32 id, guint32 index, guint32 next, const struct spa_pod *param)
+{
+  pw_session_emit (hooks, param, 0, seq, id, index, next, param);
+}
+
+static void
+wp_session_impl_pw_object_mixin_priv_interface_init (
+    WpPwObjectMixinPrivInterface * iface)
+{
+  iface->flags |= WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE;
+  iface->emit_info = wp_impl_session_emit_info;
+  iface->emit_param = wp_impl_session_emit_param;
+}
+
 /**
  * wp_impl_session_new:
  * @core: the #WpCore
@@ -896,21 +802,11 @@ void
 wp_impl_session_set_property (WpImplSession * self,
     const gchar * key, const gchar * value)
 {
-  WpSessionPrivate *priv;
-
   g_return_if_fail (WP_IS_IMPL_SESSION (self));
-  priv = wp_session_get_instance_private (WP_SESSION (self));
-
-  wp_properties_set (priv->properties, key, value);
 
-  g_object_notify (G_OBJECT (self), "properties");
-
-  /* update only after the session has been exported */
-  if (wp_object_get_active_features (WP_OBJECT (self)) & WP_PROXY_FEATURE_BOUND) {
-    self->info.change_mask = PW_SESSION_CHANGE_MASK_PROPS;
-    pw_session_emit_info (&self->hooks, &self->info);
-    self->info.change_mask = 0;
-  }
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  wp_properties_set (d->properties, key, value);
+  wp_pw_object_mixin_notify_info (self, PW_SESSION_CHANGE_MASK_PROPS);
 }
 
 /**
@@ -929,19 +825,9 @@ void
 wp_impl_session_update_properties (WpImplSession * self,
     WpProperties * updates)
 {
-  WpSessionPrivate *priv;
-
   g_return_if_fail (WP_IS_IMPL_SESSION (self));
-  priv = wp_session_get_instance_private (WP_SESSION (self));
 
-  wp_properties_update (priv->properties, updates);
-
-  g_object_notify (G_OBJECT (self), "properties");
-
-  /* update only after the session has been exported */
-  if (wp_object_get_active_features (WP_OBJECT (self)) & WP_PROXY_FEATURE_BOUND) {
-    self->info.change_mask = PW_SESSION_CHANGE_MASK_PROPS;
-    pw_session_emit_info (&self->hooks, &self->info);
-    self->info.change_mask = 0;
-  }
+  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
+  wp_properties_update (d->properties, updates);
+  wp_pw_object_mixin_notify_info (self, PW_SESSION_CHANGE_MASK_PROPS);
 }
diff --git a/modules/module-dbus-reservation/reserve-device.c b/modules/module-dbus-reservation/reserve-device.c
index 042afd3b..6b872e40 100644
--- a/modules/module-dbus-reservation/reserve-device.c
+++ b/modules/module-dbus-reservation/reserve-device.c
@@ -46,7 +46,7 @@ set_device_profile (WpPipewireObject *device, gint index)
       "index", "i", index,
       NULL);
   wp_debug_object (device, "set profile %d", index);
-  wp_pipewire_object_set_param (device, "Profile", profile);
+  wp_pipewire_object_set_param (device, "Profile", 0, profile);
 }
 
 static gint
diff --git a/modules/module-device-activation.c b/modules/module-device-activation.c
index bb2b938d..812c4733 100644
--- a/modules/module-device-activation.c
+++ b/modules/module-device-activation.c
@@ -32,7 +32,7 @@ set_device_profile (WpPipewireObject *device, gint index)
       "index", "i", index,
       NULL);
   wp_debug_object (device, "set profile %d", index);
-  wp_pipewire_object_set_param (device, "Profile", profile);
+  wp_pipewire_object_set_param (device, "Profile", 0, profile);
 }
 
 static void
diff --git a/modules/module-si-adapter.c b/modules/module-si-adapter.c
index d9b143bb..33ac017a 100644
--- a/modules/module-si-adapter.c
+++ b/modules/module-si-adapter.c
@@ -312,7 +312,7 @@ si_adapter_activate_execute_step (WpSessionItem * item,
       /* set the chosen device/client format on the node */
       format = format_audio_raw_build (&self->format);
       wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (self->node),
-          "Format", format);
+          "Format", 0, format);
 
       /* now choose the DSP format: keep the chanels but use F32 plannar @ 48K */
       self->format.format = SPA_AUDIO_FORMAT_F32P;
@@ -335,7 +335,7 @@ si_adapter_activate_execute_step (WpSessionItem * item,
           "format",     "P", port_format,
           NULL);
       wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (self->node),
-          "PortConfig", pod);
+          "PortConfig", 0, pod);
 
       g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self->node));
       wp_core_sync (core, NULL,
diff --git a/modules/module-si-bluez5-endpoint.c b/modules/module-si-bluez5-endpoint.c
index 3369d043..86bdcf88 100644
--- a/modules/module-si-bluez5-endpoint.c
+++ b/modules/module-si-bluez5-endpoint.c
@@ -456,7 +456,8 @@ set_device_profile (WpDevice *device, gint index)
       "Profile", "Profile",
       "index", "i", index,
       NULL);
-  wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (device), "Profile", profile);
+  wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (device),
+      "Profile", 0, profile);
 }
 
 static void
diff --git a/modules/module-si-convert.c b/modules/module-si-convert.c
index fbab4f43..a6da805c 100644
--- a/modules/module-si-convert.c
+++ b/modules/module-si-convert.c
@@ -248,7 +248,6 @@ si_convert_activate_execute_step (WpSessionItem * item,
       g_autoptr (WpProperties) props = NULL;
       g_autoptr (GVariant) target_config = NULL;
       g_autoptr (WpSpaPod) format = NULL;
-      g_autoptr (WpSpaPod) pod = NULL;
       guint32 channels = 2;
       guint32 rate;
 
@@ -302,23 +301,22 @@ si_convert_activate_execute_step (WpSessionItem * item,
         the same format, but with altered volume.
         In the future we need to consider writing a simpler volume node for this,
         as doing merge + split is heavy for our needs */
-      pod = wp_spa_pod_new_object ("PortConfig", "PortConfig",
-          "direction",  "I", pw_direction_reverse (self->direction),
-          "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_dsp,
-          "format",     "P", format,
-          NULL);
       wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (self->node),
-          "PortConfig", pod);
-      g_clear_pointer (&pod, wp_spa_pod_unref);
-
-      pod = wp_spa_pod_new_object ("PortConfig", "PortConfig",
-          "direction",  "I", self->direction,
-          "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_dsp,
-          "control",    "b", self->control_port,
-          "format",     "P", format,
-          NULL);
+          "PortConfig", 0,
+          wp_spa_pod_new_object ("PortConfig", "PortConfig",
+              "direction",  "I", pw_direction_reverse (self->direction),
+              "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_dsp,
+              "format",     "P", format,
+              NULL));
+
       wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (self->node),
-          "PortConfig", pod);
+          "PortConfig", 0,
+          wp_spa_pod_new_object ("PortConfig", "PortConfig",
+              "direction",  "I", self->direction,
+              "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_dsp,
+              "control",    "b", self->control_port,
+              "format",     "P", format,
+              NULL));
 
       wp_object_activate (WP_OBJECT (self->node),
           WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | WP_NODE_FEATURE_PORTS, NULL,
-- 
GitLab