diff --git a/lib/wp/proxy-link.c b/lib/wp/proxy-link.c
index ff99fbc9b060e71852a246c37031a8914c83f898..fdea049d1f411c30fb115c324b415d2d413fe208 100644
--- a/lib/wp/proxy-link.c
+++ b/lib/wp/proxy-link.c
@@ -13,22 +13,66 @@
 struct _WpProxyLink
 {
   WpProxy parent;
+  struct pw_link_info *info;
 
   /* The link proxy listener */
   struct spa_hook listener;
 };
 
+enum {
+  PROP_0,
+  PROP_INFO,
+  PROP_PROPERTIES,
+};
+
 G_DEFINE_TYPE (WpProxyLink, wp_proxy_link, WP_TYPE_PROXY)
 
+static void
+wp_proxy_link_init (WpProxyLink * self)
+{
+}
+
+static void
+wp_proxy_link_finalize (GObject * object)
+{
+  WpProxyLink *self = WP_PROXY_LINK (object);
+
+  g_clear_pointer (&self->info, pw_link_info_free);
+
+  G_OBJECT_CLASS (wp_proxy_link_parent_class)->finalize (object);
+}
+
+static void
+wp_proxy_link_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
+{
+  WpProxyLink *self = WP_PROXY_LINK (object);
+
+  switch (property_id) {
+  case PROP_INFO:
+    g_value_set_pointer (value, self->info);
+    break;
+  case PROP_PROPERTIES:
+    g_value_take_boxed (value, wp_proxy_link_get_properties (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
 static void
 link_event_info(void *data, const struct pw_link_info *info)
 {
-  WpProxy *proxy = WP_PROXY (data);
+  WpProxyLink *self = WP_PROXY_LINK (data);
+
+  self->info = pw_link_info_update (self->info, info);
+  g_object_notify (G_OBJECT (self), "info");
+
+  if (info->change_mask & PW_LINK_CHANGE_MASK_PROPS)
+    g_object_notify (G_OBJECT (self), "properties");
 
-  wp_proxy_update_native_info (proxy, info,
-      (WpProxyNativeInfoUpdate) pw_link_info_update,
-      (GDestroyNotify) pw_link_info_free);
-  wp_proxy_set_feature_ready (proxy, WP_PROXY_FEATURE_INFO);
+  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
 }
 
 static const struct pw_link_proxy_events link_events = {
@@ -36,11 +80,6 @@ static const struct pw_link_proxy_events link_events = {
   .info = link_event_info,
 };
 
-static void
-wp_proxy_link_init (WpProxyLink * self)
-{
-}
-
 static void
 wp_proxy_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
@@ -52,7 +91,32 @@ wp_proxy_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 static void
 wp_proxy_link_class_init (WpProxyLinkClass * klass)
 {
+  GObjectClass *object_class = (GObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
+  object_class->finalize = wp_proxy_link_finalize;
+  object_class->get_property = wp_proxy_link_get_property;
+
   proxy_class->pw_proxy_created = wp_proxy_link_pw_proxy_created;
+
+  g_object_class_install_property (object_class, PROP_INFO,
+      g_param_spec_pointer ("info", "info", "The struct pw_link_info *",
+          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (object_class, PROP_PROPERTIES,
+      g_param_spec_boxed ("properties", "properties",
+          "The pipewire properties of the proxy", WP_TYPE_PROPERTIES,
+          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+}
+
+const struct pw_link_info *
+wp_proxy_link_get_info (WpProxyLink * self)
+{
+  return self->info;
+}
+
+WpProperties *
+wp_proxy_link_get_properties (WpProxyLink * self)
+{
+  return wp_properties_new_wrap_dict (self->info->props);
 }
diff --git a/lib/wp/proxy-link.h b/lib/wp/proxy-link.h
index 1a7521badb1f038d0ddad330f18ca112c6d9dc4c..e4c15224a1c5f87a48687db14bcf34c81c7def9e 100644
--- a/lib/wp/proxy-link.h
+++ b/lib/wp/proxy-link.h
@@ -13,15 +13,14 @@
 
 G_BEGIN_DECLS
 
+struct pw_link_info;
+
 #define WP_TYPE_PROXY_LINK (wp_proxy_link_get_type ())
 G_DECLARE_FINAL_TYPE (WpProxyLink, wp_proxy_link, WP, PROXY_LINK, WpProxy)
 
-static inline const struct pw_link_info *
-wp_proxy_link_get_info (WpProxyLink * self)
-{
-  return (const struct pw_link_info *)
-      wp_proxy_get_native_info (WP_PROXY (self));
-}
+const struct pw_link_info * wp_proxy_link_get_info (WpProxyLink * self);
+
+WpProperties * wp_proxy_link_get_properties (WpProxyLink * self);
 
 G_END_DECLS
 
diff --git a/lib/wp/proxy-node.c b/lib/wp/proxy-node.c
index 86443327ddea8e04cb7418381d3dba55083960eb..9d263767e0aa2bb1e7b1572263cb9add00e5ca5f 100644
--- a/lib/wp/proxy-node.c
+++ b/lib/wp/proxy-node.c
@@ -7,40 +7,106 @@
  */
 
 #include "proxy-node.h"
+#include "error.h"
 
 #include <pipewire/pipewire.h>
 
 struct _WpProxyNode
 {
   WpProxy parent;
+  struct pw_node_info *info;
 
   /* The node proxy listener */
   struct spa_hook listener;
 };
 
+enum {
+  PROP_0,
+  PROP_INFO,
+  PROP_PROPERTIES,
+};
+
+enum {
+  SIGNAL_PARAM,
+  N_SIGNALS
+};
+
+static guint32 signals[N_SIGNALS];
+
 G_DEFINE_TYPE (WpProxyNode, wp_proxy_node, WP_TYPE_PROXY)
 
+static void
+wp_proxy_node_init (WpProxyNode * self)
+{
+}
+
+static void
+wp_proxy_node_finalize (GObject * object)
+{
+  WpProxyNode *self = WP_PROXY_NODE (object);
+
+  g_clear_pointer (&self->info, pw_node_info_free);
+
+  G_OBJECT_CLASS (wp_proxy_node_parent_class)->finalize (object);
+}
+
+static void
+wp_proxy_node_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
+{
+  WpProxyNode *self = WP_PROXY_NODE (object);
+
+  switch (property_id) {
+  case PROP_INFO:
+    g_value_set_pointer (value, self->info);
+    break;
+  case PROP_PROPERTIES:
+    g_value_take_boxed (value, wp_proxy_node_get_properties (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
 static void
 node_event_info(void *data, const struct pw_node_info *info)
 {
-  WpProxy *proxy = WP_PROXY (data);
+  WpProxyNode *self = WP_PROXY_NODE (data);
+
+  self->info = pw_node_info_update (self->info, info);
+  g_object_notify (G_OBJECT (self), "info");
+
+  if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)
+    g_object_notify (G_OBJECT (self), "properties");
+
+  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
+}
+
+static void
+node_event_param (void *data, int seq, uint32_t id, uint32_t index,
+    uint32_t next, const struct spa_pod *param)
+{
+  WpProxyNode *self = WP_PROXY_NODE (data);
+  GTask *task;
 
-  wp_proxy_update_native_info (proxy, info,
-      (WpProxyNativeInfoUpdate) pw_node_info_update,
-      (GDestroyNotify) pw_node_info_free);
-  wp_proxy_set_feature_ready (proxy, WP_PROXY_FEATURE_INFO);
+  g_signal_emit (self, signals[SIGNAL_PARAM], 0, seq, id, index, next, param);
+
+  /* if this param event was emited because of enum_params_collect(),
+   * copy the param in the result array of that API */
+  task = wp_proxy_find_async_task (WP_PROXY (self), seq, FALSE);
+  if (task) {
+    GPtrArray *array = g_task_get_task_data (task);
+    g_ptr_array_add (array, spa_pod_copy (param));
+  }
 }
 
 static const struct pw_node_proxy_events node_events = {
   PW_VERSION_NODE_PROXY_EVENTS,
   .info = node_event_info,
+  .param = node_event_param,
 };
 
-static void
-wp_proxy_node_init (WpProxyNode * self)
-{
-}
-
 static void
 wp_proxy_node_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
@@ -52,7 +118,173 @@ wp_proxy_node_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 static void
 wp_proxy_node_class_init (WpProxyNodeClass * klass)
 {
+  GObjectClass *object_class = (GObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
+  object_class->finalize = wp_proxy_node_finalize;
+  object_class->get_property = wp_proxy_node_get_property;
+
   proxy_class->pw_proxy_created = wp_proxy_node_pw_proxy_created;
+
+  g_object_class_install_property (object_class, PROP_INFO,
+      g_param_spec_pointer ("info", "info", "The struct pw_node_info *",
+          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (object_class, PROP_PROPERTIES,
+      g_param_spec_boxed ("properties", "properties",
+          "The pipewire properties of the proxy", WP_TYPE_PROPERTIES,
+          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  signals[SIGNAL_PARAM] = g_signal_new ("param", G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 5,
+      G_TYPE_INT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_POINTER);
+}
+
+const struct pw_node_info *
+wp_proxy_node_get_info (WpProxyNode * self)
+{
+  return self->info;
+}
+
+WpProperties *
+wp_proxy_node_get_properties (WpProxyNode * self)
+{
+  return wp_properties_new_wrap_dict (self->info->props);
+}
+
+static void
+enum_params_done (WpProxy * proxy, GAsyncResult * res, gpointer data)
+{
+  int seq = GPOINTER_TO_INT (data);
+  g_autoptr (GError) error = NULL;
+  g_autoptr (GTask) task = NULL;
+
+  /* finish the sync task */
+  wp_proxy_sync_finish (proxy, res, &error);
+
+  /* find the enum params task and return from it */
+  task = wp_proxy_find_async_task (proxy, seq, TRUE);
+  g_return_if_fail (task != NULL);
+
+  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);
+  }
+}
+
+void
+wp_proxy_node_enum_params_collect (WpProxyNode * self,
+    guint32 id, const struct spa_pod *filter,
+    GCancellable * cancellable, GAsyncReadyCallback callback,
+    gpointer user_data)
+{
+  g_autoptr (GTask) task = NULL;
+  int seq;
+  GPtrArray *params;
+
+  g_return_if_fail (WP_IS_PROXY_NODE (self));
+
+  /* create task for enum_params */
+  task = g_task_new (self, cancellable, callback, user_data);
+  params = g_ptr_array_new_with_free_func (free);
+  g_task_set_task_data (task, params, (GDestroyNotify) g_ptr_array_unref);
+
+  /* call enum_params */
+  seq = wp_proxy_node_enum_params (self, id, filter);
+  if (G_UNLIKELY (seq < 0)) {
+    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
+        WP_LIBRARY_ERROR_OPERATION_FAILED, "enum_params failed: %s",
+        g_strerror (-seq));
+    return;
+  }
+  wp_proxy_register_async_task (WP_PROXY (self), seq, g_steal_pointer (&task));
+
+  /* call sync */
+  wp_proxy_sync (WP_PROXY (self), cancellable,
+      (GAsyncReadyCallback) enum_params_done, GINT_TO_POINTER (seq));
+}
+
+/**
+ * wp_proxy_node_enum_params_collect_finish:
+ *
+ * Returns: (transfer full) (element-type spa_pod*):
+ *    the collected params
+ */
+GPtrArray *
+wp_proxy_node_enum_params_collect_finish (WpProxyNode * self,
+    GAsyncResult * res, GError ** error)
+{
+  g_return_val_if_fail (WP_IS_PROXY_NODE (self), NULL);
+  g_return_val_if_fail (g_task_is_valid (res, self), NULL);
+
+  return g_task_propagate_pointer (G_TASK (res), error);
+}
+
+gint
+wp_proxy_node_enum_params (WpProxyNode * self,
+    guint32 id, const struct spa_pod *filter)
+{
+  struct pw_node_proxy *pwp;
+  int enum_params_result;
+
+  g_return_val_if_fail (WP_IS_PROXY_NODE (self), -EINVAL);
+
+  pwp = (struct pw_node_proxy *) wp_proxy_get_pw_proxy (WP_PROXY (self));
+  g_return_val_if_fail (pwp != NULL, -EINVAL);
+
+  enum_params_result = pw_node_proxy_enum_params (pwp, 0, id, 0, -1, filter);
+  g_warn_if_fail (enum_params_result >= 0);
+
+  return enum_params_result;
+}
+
+void
+wp_proxy_node_subscribe_params (WpProxyNode * self, guint32 n_ids, ...)
+{
+  va_list args;
+  guint32 *ids = g_alloca (n_ids * sizeof (guint32));
+
+  va_start (args, n_ids);
+  for (gint i = 0; i < n_ids; i++)
+    ids[i] = va_arg (args, guint32);
+  va_end (args);
+
+  wp_proxy_node_subscribe_params_array (self, n_ids, ids);
+}
+
+void
+wp_proxy_node_subscribe_params_array (WpProxyNode * self, guint32 n_ids,
+    guint32 *ids)
+{
+  struct pw_node_proxy *pwp;
+  int node_subscribe_params_result;
+
+  g_return_if_fail (WP_IS_PROXY_NODE (self));
+
+  pwp = (struct pw_node_proxy *) wp_proxy_get_pw_proxy (WP_PROXY (self));
+  g_return_if_fail (pwp != NULL);
+
+  node_subscribe_params_result = pw_node_proxy_subscribe_params (
+      pwp, ids, n_ids);
+  g_warn_if_fail (node_subscribe_params_result >= 0);
+}
+
+void
+wp_proxy_node_set_param (WpProxyNode * self, guint32 id,
+    guint32 flags, const struct spa_pod *param)
+{
+  struct pw_node_proxy *pwp;
+  int node_set_param_result;
+
+  g_return_if_fail (WP_IS_PROXY_NODE (self));
+
+  pwp = (struct pw_node_proxy *) wp_proxy_get_pw_proxy (WP_PROXY (self));
+  g_return_if_fail (pwp != NULL);
+
+  node_set_param_result = pw_node_proxy_set_param (
+      pwp, id, flags, param);
+  g_warn_if_fail (node_set_param_result >= 0);
 }
diff --git a/lib/wp/proxy-node.h b/lib/wp/proxy-node.h
index 7d6050dcb8f0202f3c8d6adb9284b2089d78d927..5365d234e950550752e45a9b1201645f102eb6f8 100644
--- a/lib/wp/proxy-node.h
+++ b/lib/wp/proxy-node.h
@@ -13,15 +13,31 @@
 
 G_BEGIN_DECLS
 
+struct spa_pod;
+struct pw_node_info;
+
 #define WP_TYPE_PROXY_NODE (wp_proxy_node_get_type ())
 G_DECLARE_FINAL_TYPE (WpProxyNode, wp_proxy_node, WP, PROXY_NODE, WpProxy)
 
-static inline const struct pw_node_info *
-wp_proxy_node_get_info (WpProxyNode * self)
-{
-  return (const struct pw_node_info *)
-      wp_proxy_get_native_info (WP_PROXY (self));
-}
+const struct pw_node_info * wp_proxy_node_get_info (WpProxyNode * self);
+
+WpProperties * wp_proxy_node_get_properties (WpProxyNode * self);
+
+void wp_proxy_node_enum_params_collect (WpProxyNode * self,
+    guint32 id, const struct spa_pod *filter,
+    GCancellable * cancellable, GAsyncReadyCallback callback,
+    gpointer user_data);
+GPtrArray * wp_proxy_node_enum_params_collect_finish (WpProxyNode * self,
+    GAsyncResult * res, GError ** error);
+gint wp_proxy_node_enum_params (WpProxyNode * self,
+    guint32 id, const struct spa_pod *filter);
+
+void wp_proxy_node_subscribe_params (WpProxyNode * self, guint32 n_ids, ...);
+void wp_proxy_node_subscribe_params_array (WpProxyNode * self, guint32 n_ids,
+    guint32 *ids);
+
+void wp_proxy_node_set_param (WpProxyNode * self, guint32 id,
+    guint32 flags, const struct spa_pod *param);
 
 G_END_DECLS
 
diff --git a/lib/wp/proxy-port.c b/lib/wp/proxy-port.c
index 5cc68ba2355fb82ca3d69e5a38f6aa529aa6776d..1e213bf88a2506562f523820a17b259c03b83f33 100644
--- a/lib/wp/proxy-port.c
+++ b/lib/wp/proxy-port.c
@@ -7,34 +7,80 @@
  */
 
 #include "proxy-port.h"
+#include "error.h"
 
 #include <pipewire/pipewire.h>
-#include <spa/param/audio/format-utils.h>
 
 struct _WpProxyPort
 {
   WpProxy parent;
+  struct pw_port_info *info;
 
   /* The port proxy listener */
   struct spa_hook listener;
+};
+
+enum {
+  PROP_0,
+  PROP_INFO,
+  PROP_PROPERTIES,
+};
 
-  /* The port format */
-  uint32_t media_type;
-  uint32_t media_subtype;
-  struct spa_audio_info_raw format;
+enum {
+  SIGNAL_PARAM,
+  N_SIGNALS
 };
 
+static guint32 signals[N_SIGNALS];
+
 G_DEFINE_TYPE (WpProxyPort, wp_proxy_port, WP_TYPE_PROXY)
 
+static void
+wp_proxy_port_init (WpProxyPort * self)
+{
+}
+
+static void
+wp_proxy_port_finalize (GObject * object)
+{
+  WpProxyPort *self = WP_PROXY_PORT (object);
+
+  g_clear_pointer (&self->info, pw_port_info_free);
+
+  G_OBJECT_CLASS (wp_proxy_port_parent_class)->finalize (object);
+}
+
+static void
+wp_proxy_port_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
+{
+  WpProxyPort *self = WP_PROXY_PORT (object);
+
+  switch (property_id) {
+  case PROP_INFO:
+    g_value_set_pointer (value, self->info);
+    break;
+  case PROP_PROPERTIES:
+    g_value_take_boxed (value, wp_proxy_port_get_properties (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
 static void
 port_event_info(void *data, const struct pw_port_info *info)
 {
-  WpProxy *proxy = WP_PROXY (data);
+  WpProxyPort *self = WP_PROXY_PORT (data);
+
+  self->info = pw_port_info_update (self->info, info);
+  g_object_notify (G_OBJECT (self), "info");
+
+  if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
+    g_object_notify (G_OBJECT (self), "properties");
 
-  wp_proxy_update_native_info (proxy, info,
-      (WpProxyNativeInfoUpdate) pw_port_info_update,
-      (GDestroyNotify) pw_port_info_free);
-  wp_proxy_set_feature_ready (proxy, WP_PROXY_FEATURE_INFO);
+  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
 }
 
 static void
@@ -42,23 +88,17 @@ port_event_param(void *data, int seq, uint32_t id, uint32_t index,
     uint32_t next, const struct spa_pod *param)
 {
   WpProxyPort *self = WP_PROXY_PORT (data);
+  GTask *task;
 
-  /* Only handle EnumFormat */
-  if (id != SPA_PARAM_EnumFormat)
-    return;
-
-  /* Parse the format */
-  spa_format_parse(param, &self->media_type, &self->media_subtype);
+  g_signal_emit (self, signals[SIGNAL_PARAM], 0, seq, id, index, next, param);
 
-  /* Only handle raw audio formats for now */
-  if (self->media_type == SPA_MEDIA_TYPE_audio &&
-      self->media_subtype == SPA_MEDIA_SUBTYPE_raw) {
-    /* Parse the raw audio format */
-    spa_pod_fixate ((struct spa_pod *) param);
-    spa_format_audio_raw_parse (param, &self->format);
+  /* if this param event was emited because of enum_params_collect(),
+   * copy the param in the result array of that API */
+  task = wp_proxy_find_async_task (WP_PROXY (self), seq, FALSE);
+  if (task) {
+    GPtrArray *array = g_task_get_task_data (task);
+    g_ptr_array_add (array, spa_pod_copy (param));
   }
-
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_PORT_FEATURE_FORMAT);
 }
 
 static const struct pw_port_proxy_events port_events = {
@@ -68,44 +108,166 @@ static const struct pw_port_proxy_events port_events = {
 };
 
 static void
-wp_proxy_port_init (WpProxyPort * self)
+wp_proxy_port_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
+  WpProxyPort *self = WP_PROXY_PORT (proxy);
+  pw_port_proxy_add_listener ((struct pw_port_proxy *) pw_proxy,
+      &self->listener, &port_events, self);
 }
 
 static void
-wp_proxy_port_augment (WpProxy * proxy, WpProxyFeatures features)
+wp_proxy_port_class_init (WpProxyPortClass * klass)
 {
-  /* call the default implementation to ensure we have a proxy, if necessary */
-  WP_PROXY_CLASS (wp_proxy_port_parent_class)->augment (proxy, features);
+  GObjectClass *object_class = (GObjectClass *) klass;
+  WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  if (features & WP_PROXY_PORT_FEATURE_FORMAT) {
-    struct pw_proxy *pwp = wp_proxy_get_pw_proxy (proxy);
-    g_return_if_fail (pwp != NULL);
+  object_class->finalize = wp_proxy_port_finalize;
+  object_class->get_property = wp_proxy_port_get_property;
 
-    pw_port_proxy_enum_params ((struct pw_port_proxy *) pwp, 0,
-        SPA_PARAM_EnumFormat, 0, -1, NULL);
-  }
+  proxy_class->pw_proxy_created = wp_proxy_port_pw_proxy_created;
+
+  g_object_class_install_property (object_class, PROP_INFO,
+      g_param_spec_pointer ("info", "info", "The struct pw_port_info *",
+          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (object_class, PROP_PROPERTIES,
+      g_param_spec_boxed ("properties", "properties",
+          "The pipewire properties of the proxy", WP_TYPE_PROPERTIES,
+          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  signals[SIGNAL_PARAM] = g_signal_new ("param", G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 5,
+      G_TYPE_INT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_POINTER);
 }
 
-static void
-wp_proxy_port_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
+const struct pw_port_info *
+wp_proxy_port_get_info (WpProxyPort * self)
 {
-  WpProxyPort *self = WP_PROXY_PORT (proxy);
-  pw_port_proxy_add_listener ((struct pw_port_proxy *) pw_proxy,
-      &self->listener, &port_events, self);
+  return self->info;
+}
+
+WpProperties *
+wp_proxy_port_get_properties (WpProxyPort * self)
+{
+  return wp_properties_new_wrap_dict (self->info->props);
 }
 
 static void
-wp_proxy_port_class_init (WpProxyPortClass * klass)
+enum_params_done (WpProxy * proxy, GAsyncResult * res, gpointer data)
 {
-  WpProxyClass *proxy_class = (WpProxyClass *) klass;
+  int seq = GPOINTER_TO_INT (data);
+  g_autoptr (GError) error = NULL;
+  g_autoptr (GTask) task = NULL;
 
-  proxy_class->augment = wp_proxy_port_augment;
-  proxy_class->pw_proxy_created = wp_proxy_port_pw_proxy_created;
+  /* finish the sync task */
+  wp_proxy_sync_finish (proxy, res, &error);
+
+  /* find the enum params task and return from it */
+  task = wp_proxy_find_async_task (proxy, seq, TRUE);
+  g_return_if_fail (task != NULL);
+
+  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);
+  }
 }
 
-const struct spa_audio_info_raw *
-wp_proxy_port_get_format (WpProxyPort * self)
+void
+wp_proxy_port_enum_params_collect (WpProxyPort * self,
+    guint32 id, const struct spa_pod *filter,
+    GCancellable * cancellable, GAsyncReadyCallback callback,
+    gpointer user_data)
 {
-  return &self->format;
+  g_autoptr (GTask) task = NULL;
+  int seq;
+  GPtrArray *params;
+
+  g_return_if_fail (WP_IS_PROXY_PORT (self));
+
+  /* create task for enum_params */
+  task = g_task_new (self, cancellable, callback, user_data);
+  params = g_ptr_array_new_with_free_func (free);
+  g_task_set_task_data (task, params, (GDestroyNotify) g_ptr_array_unref);
+
+  /* call enum_params */
+  seq = wp_proxy_port_enum_params (self, id, filter);
+  if (G_UNLIKELY (seq < 0)) {
+    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
+        WP_LIBRARY_ERROR_OPERATION_FAILED, "enum_params failed: %s",
+        g_strerror (-seq));
+    return;
+  }
+  wp_proxy_register_async_task (WP_PROXY (self), seq, g_steal_pointer (&task));
+
+  /* call sync */
+  wp_proxy_sync (WP_PROXY (self), cancellable,
+      (GAsyncReadyCallback) enum_params_done, GINT_TO_POINTER (seq));
+}
+
+/**
+ * wp_proxy_port_enum_params_collect_finish:
+ *
+ * Returns: (transfer full) (element-type spa_pod*):
+ *    the collected params
+ */
+GPtrArray *
+wp_proxy_port_enum_params_collect_finish (WpProxyPort * self,
+    GAsyncResult * res, GError ** error)
+{
+  g_return_val_if_fail (WP_IS_PROXY_PORT (self), NULL);
+  g_return_val_if_fail (g_task_is_valid (res, self), NULL);
+
+  return g_task_propagate_pointer (G_TASK (res), error);
+}
+
+gint
+wp_proxy_port_enum_params (WpProxyPort * self,
+    guint32 id, const struct spa_pod *filter)
+{
+  struct pw_port_proxy *pwp;
+  int enum_params_result;
+
+  g_return_val_if_fail (WP_IS_PROXY_PORT (self), -EINVAL);
+
+  pwp = (struct pw_port_proxy *) wp_proxy_get_pw_proxy (WP_PROXY (self));
+  g_return_val_if_fail (pwp != NULL, -EINVAL);
+
+  enum_params_result = pw_port_proxy_enum_params (pwp, 0, id, 0, -1, filter);
+  g_warn_if_fail (enum_params_result >= 0);
+
+  return enum_params_result;
+}
+
+void
+wp_proxy_port_subscribe_params (WpProxyPort * self, guint32 n_ids, ...)
+{
+  va_list args;
+  guint32 *ids = g_alloca (n_ids * sizeof (guint32));
+
+  va_start (args, n_ids);
+  for (gint i = 0; i < n_ids; i++)
+    ids[i] = va_arg (args, guint32);
+  va_end (args);
+
+  wp_proxy_port_subscribe_params_array (self, n_ids, ids);
+}
+
+void
+wp_proxy_port_subscribe_params_array (WpProxyPort * self, guint32 n_ids,
+    guint32 *ids)
+{
+  struct pw_port_proxy *pwp;
+  int port_subscribe_params_result;
+
+  g_return_if_fail (WP_IS_PROXY_PORT (self));
+
+  pwp = (struct pw_port_proxy *) wp_proxy_get_pw_proxy (WP_PROXY (self));
+  g_return_if_fail (pwp != NULL);
+
+  port_subscribe_params_result = pw_port_proxy_subscribe_params (
+      pwp, ids, n_ids);
+  g_warn_if_fail (port_subscribe_params_result >= 0);
 }
diff --git a/lib/wp/proxy-port.h b/lib/wp/proxy-port.h
index 79261899155c9d54737b9fb9d3e3e967771397fc..4db82985080b0d815ea50950f747ab2425b068cc 100644
--- a/lib/wp/proxy-port.h
+++ b/lib/wp/proxy-port.h
@@ -13,21 +13,28 @@
 
 G_BEGIN_DECLS
 
-typedef enum { /*< flags >*/
-  WP_PROXY_PORT_FEATURE_FORMAT = (WP_PROXY_FEATURE_LAST << 0),
-} WpProxyPortFeatures;
+struct spa_pod;
+struct pw_port_info;
 
 #define WP_TYPE_PROXY_PORT (wp_proxy_port_get_type ())
 G_DECLARE_FINAL_TYPE (WpProxyPort, wp_proxy_port, WP, PROXY_PORT, WpProxy)
 
-static inline const struct pw_port_info *
-wp_proxy_port_get_info (WpProxyPort * self)
-{
-  return (const struct pw_port_info *)
-      wp_proxy_get_native_info (WP_PROXY (self));
-}
+const struct pw_port_info * wp_proxy_port_get_info (WpProxyPort * self);
 
-const struct spa_audio_info_raw *wp_proxy_port_get_format (WpProxyPort * self);
+WpProperties * wp_proxy_port_get_properties (WpProxyPort * self);
+
+void wp_proxy_port_enum_params_collect (WpProxyPort * self,
+    guint32 id, const struct spa_pod *filter,
+    GCancellable * cancellable, GAsyncReadyCallback callback,
+    gpointer user_data);
+GPtrArray * wp_proxy_port_enum_params_collect_finish (WpProxyPort * self,
+    GAsyncResult * res, GError ** error);
+gint wp_proxy_port_enum_params (WpProxyPort * self,
+    guint32 id, const struct spa_pod *filter);
+
+void wp_proxy_port_subscribe_params (WpProxyPort * self, guint32 n_ids, ...);
+void wp_proxy_port_subscribe_params_array (WpProxyPort * self, guint32 n_ids,
+    guint32 *ids);
 
 G_END_DECLS
 
diff --git a/lib/wp/proxy.c b/lib/wp/proxy.c
index 79515792823ab760de1c81b403fc315dc1760376..d3236d2369077b1a453a7fe77c372911474de807 100644
--- a/lib/wp/proxy.c
+++ b/lib/wp/proxy.c
@@ -33,8 +33,6 @@ struct _WpProxyPrivate
   guint32 iface_version;
 
   struct pw_proxy *pw_proxy;
-  gpointer native_info;
-  GDestroyNotify native_info_destroy;
 
   /* The proxy listener */
   struct spa_hook listener;
@@ -43,6 +41,8 @@ struct _WpProxyPrivate
   WpProxyFeatures ft_ready;
   WpProxyFeatures ft_wanted;
   GTask *task;
+
+  GHashTable *async_tasks; // <int seq, GTask*>
 };
 
 enum {
@@ -56,7 +56,6 @@ enum {
   PROP_INTERFACE_QUARK,
   PROP_INTERFACE_VERSION,
   PROP_PW_PROXY,
-  PROP_NATIVE_INFO,
   PROP_FEATURES,
 };
 
@@ -132,6 +131,8 @@ proxy_event_destroy (void *data)
 {
   WpProxy *self = WP_PROXY (data);
   WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
+  GHashTableIter iter;
+  GTask *task;
 
   priv->pw_proxy = NULL;
 
@@ -145,18 +146,40 @@ proxy_event_destroy (void *data)
         "pipewire node proxy destroyed before finishing");
     g_clear_object (&priv->task);
   }
+
+  g_hash_table_iter_init (&iter, priv->async_tasks);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &task)) {
+    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
+        WP_LIBRARY_ERROR_OPERATION_FAILED,
+        "pipewire node proxy destroyed before finishing");
+    g_hash_table_iter_remove (&iter);
+  }
+}
+
+static void
+proxy_event_done (void *data, int seq)
+{
+  WpProxy *self = WP_PROXY (data);
+  g_autoptr (GTask) task;
+
+  if ((task = wp_proxy_find_async_task (self, seq, TRUE)))
+    g_task_return_boolean (task, TRUE);
 }
 
 static const struct pw_proxy_events proxy_events = {
   PW_VERSION_PROXY_EVENTS,
   .destroy = proxy_event_destroy,
+  .done = proxy_event_done,
 };
 
 static void
 wp_proxy_init (WpProxy * self)
 {
   WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
+
   g_weak_ref_init (&priv->remote, NULL);
+  priv->async_tasks = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+      NULL, g_object_unref);
 }
 
 static void
@@ -185,9 +208,9 @@ wp_proxy_finalize (GObject * object)
 
   g_clear_object (&priv->task);
   g_clear_pointer (&priv->global_props, wp_properties_unref);
-  g_clear_pointer (&priv->native_info, priv->native_info_destroy);
   g_clear_pointer (&priv->pw_proxy, pw_proxy_destroy);
   g_weak_ref_clear (&priv->remote);
+  g_clear_pointer (&priv->async_tasks, g_hash_table_unref);
 
   G_OBJECT_CLASS (wp_proxy_parent_class)->finalize (object);
 }
@@ -220,9 +243,6 @@ wp_proxy_set_property (GObject * object, guint property_id,
   case PROP_PW_PROXY:
     priv->pw_proxy = g_value_get_pointer (value);
     break;
-  case PROP_NATIVE_INFO:
-    priv->native_info = g_value_get_pointer (value);
-    break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
     break;
@@ -264,9 +284,6 @@ wp_proxy_get_property (GObject * object, guint property_id, GValue * value,
   case PROP_PW_PROXY:
     g_value_set_pointer (value, priv->pw_proxy);
     break;
-  case PROP_NATIVE_INFO:
-    g_value_set_pointer (value, priv->native_info);
-    break;
   case PROP_FEATURES:
     g_value_set_flags (value, priv->ft_ready);
     break;
@@ -359,11 +376,6 @@ wp_proxy_class_init (WpProxyClass * klass)
       g_param_spec_pointer ("pw-proxy", "pw-proxy", "The struct pw_proxy *",
           G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
 
-  g_object_class_install_property (object_class, PROP_NATIVE_INFO,
-      g_param_spec_pointer ("native-info", "native-info",
-          "The struct pw_XXXX_info *",
-          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
   g_object_class_install_property (object_class, PROP_FEATURES,
       g_param_spec_flags ("features", "features",
           "The ready WpProxyFeatures on this proxy", WP_TYPE_PROXY_FEATURES, 0,
@@ -660,36 +672,78 @@ wp_proxy_get_pw_proxy (WpProxy * self)
   return priv->pw_proxy;
 }
 
-gconstpointer
-wp_proxy_get_native_info (WpProxy * self)
+void
+wp_proxy_sync (WpProxy * self, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
 {
   WpProxyPrivate *priv;
+  g_autoptr (GTask) task = NULL;
+  int seq;
 
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
+  g_return_if_fail (WP_IS_PROXY (self));
 
   priv = wp_proxy_get_instance_private (self);
-  return priv->native_info;
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  if (G_UNLIKELY (!priv->pw_proxy)) {
+    g_warn_if_reached ();
+    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
+        WP_LIBRARY_ERROR_INVARIANT, "No pipewire proxy");
+    return;
+  }
+
+  seq = pw_proxy_sync (priv->pw_proxy, 0);
+  if (G_UNLIKELY (seq < 0)) {
+    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
+        WP_LIBRARY_ERROR_OPERATION_FAILED, "pw_proxy_sync failed: %s",
+        g_strerror (-seq));
+    return;
+  }
+
+  wp_proxy_register_async_task (self, seq, g_steal_pointer (&task));
+}
+
+gboolean
+wp_proxy_sync_finish (WpProxy * self, GAsyncResult * res, GError ** error)
+{
+  g_return_val_if_fail (WP_IS_PROXY (self), FALSE);
+  g_return_val_if_fail (g_task_is_valid (res, self), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (res), error);
 }
 
+/**
+ * wp_proxy_register_async_task: (skip)
+ */
 void
-wp_proxy_update_native_info (WpProxy * self, gconstpointer info,
-    WpProxyNativeInfoUpdate update, GDestroyNotify destroy)
+wp_proxy_register_async_task (WpProxy * self, int seq, GTask * task)
 {
   WpProxyPrivate *priv;
 
   g_return_if_fail (WP_IS_PROXY (self));
-  g_return_if_fail (destroy != NULL);
+  g_return_if_fail (g_task_is_valid (task, self));
 
   priv = wp_proxy_get_instance_private (self);
+  g_hash_table_insert (priv->async_tasks, GINT_TO_POINTER (seq), task);
+}
 
-  if (update) {
-    priv->native_info = update (priv->native_info, info);
-  } else {
-    g_clear_pointer (&priv->native_info, priv->native_info_destroy);
-    priv->native_info = (gpointer) info;
-  }
+/**
+ * wp_proxy_find_async_task: (skip)
+ */
+GTask *
+wp_proxy_find_async_task (WpProxy * self, int seq, gboolean steal)
+{
+  WpProxyPrivate *priv;
+  GTask *task = NULL;
+
+  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
 
-  priv->native_info_destroy = destroy;
+  priv = wp_proxy_get_instance_private (self);
+  if (steal)
+    g_hash_table_steal_extended (priv->async_tasks, GINT_TO_POINTER (seq),
+        NULL, (gpointer *) &task);
+  else
+    task = g_hash_table_lookup (priv->async_tasks, GINT_TO_POINTER (seq));
 
-  g_object_notify (G_OBJECT (self), "native-info");
+  return task;
 }
diff --git a/lib/wp/proxy.h b/lib/wp/proxy.h
index 9114858b999b4bebb71b6db3f7d1cfab376efda0..99a359b8eb3e5b37c07b3811b97b5a1aaff42229 100644
--- a/lib/wp/proxy.h
+++ b/lib/wp/proxy.h
@@ -20,7 +20,7 @@ G_BEGIN_DECLS
 struct pw_proxy;
 
 typedef enum { /*< flags >*/
-  WP_PROXY_FEATURE_PW_PROXY = (1 << 0),
+  WP_PROXY_FEATURE_PW_PROXY     = (1 << 0),
   WP_PROXY_FEATURE_INFO         = (1 << 1),
 
   WP_PROXY_FEATURE_LAST         = (1 << 5), /*< skip >*/
@@ -67,19 +67,20 @@ GQuark wp_proxy_get_interface_quark (WpProxy * self);
 guint32 wp_proxy_get_interface_version (WpProxy * self);
 
 struct pw_proxy * wp_proxy_get_pw_proxy (WpProxy * self);
-gconstpointer wp_proxy_get_native_info (WpProxy * self);
 
-/* for subclasses only */
-
-typedef gpointer (*WpProxyNativeInfoUpdate) (gpointer old_info,
-    gconstpointer new_info);
+void wp_proxy_sync (WpProxy * self, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data);
+gboolean wp_proxy_sync_finish (WpProxy * self, GAsyncResult * res,
+    GError ** error);
 
-void wp_proxy_update_native_info (WpProxy * self, gconstpointer info,
-    WpProxyNativeInfoUpdate update, GDestroyNotify destroy);
+/* for subclasses only */
 
 void wp_proxy_set_feature_ready (WpProxy * self, WpProxyFeatures feature);
 void wp_proxy_augment_error (WpProxy * self, GError * error);
 
+void wp_proxy_register_async_task (WpProxy * self, int seq, GTask * task);
+GTask * wp_proxy_find_async_task (WpProxy * self, int seq, gboolean steal);
+
 gboolean wp_proxy_bind_global (WpProxy * self);
 
 G_END_DECLS
diff --git a/tests/proxy.c b/tests/proxy.c
index 0d39c576a17e7c968aee0f712e286fb7486ee725..4665c2e00d5b39371b434c72347d06a7037e8f13 100644
--- a/tests/proxy.c
+++ b/tests/proxy.c
@@ -96,7 +96,6 @@ test_proxy_basic_global_added (WpRemote *remote, WpProxy *proxy,
 
   g_assert_cmphex (wp_proxy_get_features (proxy), ==, 0);
   g_assert_null (wp_proxy_get_pw_proxy (proxy));
-  g_assert_null (wp_proxy_get_native_info (proxy));
 
   {
     g_autoptr (WpProperties) props = wp_proxy_get_global_properties (proxy);