diff --git a/lib/wp/client.c b/lib/wp/client.c
index 10b08739f845b5190128738af84e5984d3e254b3..5622e3adc0895b3db570f60a618924f4de18f188 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 2a5ab8de15fb34cad5efefd9de909e4c679e5134..be96d1d94f742008f81a6d6ff8e3dca91b7b246b 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 209c6eb37a80f03a0c750be08ea0fd69d375a44e..4262cd4e4a102d91fc282ca54699f0f64af3a680 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 269557f521cf239b3b78a2ea3a25762075876fed..88bcf1facac4803f7ab79bc41dd5a6f51011d1c6 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 bc3e87e2320887f7c81458722974dfd783d26646..853f74a938b14e81a8555d42481a1d8824036b0d 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 7de94a45bf1dc5541bbccd7319427c5212b29893..1f751ecbf18c3aebf26a70b21836bbb7f79238d5 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 0a030b1125364df33bfa89d0db9c27958f644c8b..136663177354a8a43e0eaa702b184a7c1b2984af 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 fbd850537c67fd13759ac8aa6731e97290e22213..309643e3c127558133bec5480ff8597e8e295a56 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 5d501140588e4f31a862dfd9d915b42f8aab0783..c29c176d7329424d37110ce06e4f17f545a7d60a 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 49b316d6f2f12f4ee0c38b41b6361cfe6301be67..e88ff9988535bc96dcceb8fa256f9f33187b2287 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 fc86101aa76816161c5f3c3502d5ea3610ab357e..1a8a688db9b7be0448ba5262801fa1f850940b52 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 8524da7c9e4669fc0aa549e0d3205f9107ccd711..b228e653ce45cd0494c25f8cac3b6006ac5328cb 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 7b0d3ba85f55163f0437a464efa92a130d057762..62b8328074f4a6340edd835b76a59d71f1d3bb7f 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 59220ebc36411ae9a7dc33eb1de2e5ae75e9241b..e289b18204111f258e62746770d6f74b8259eeb5 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 042afd3b3f0fa35e814a3f5bc62502171f5a98af..6b872e40b3329a285b54ed620b92d4327527d5b3 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 bb2b938d2e5d1dabec4abc42f8d0b7015385ac41..812c4733321202a1c0eaafc30a2c63c14e7d3dd9 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 d9b143bbb6f7403139d295de3b83a2c193c91084..33ac017ac3c64357e3bddae496a7117c34894d34 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 3369d04307292d80fdf0b10e34e69e0d018db76b..86bdcf88ca55ee0efb2edc3763271e947fe516cd 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 fbab4f43a72a0b066793ebacbd7283793d99096d..a6da805c5f65475f49d80166e1472390ad0cfda8 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,