[pulseaudio-discuss] [PATCH v2 3/5] tunnel-manager: New module for managing tunnels to remote servers

Tanu Kaskinen tanu.kaskinen at linux.intel.com
Mon Dec 1 05:06:05 PST 2014


The initial use case for this module is to connect user PulseAudio
instances to the system instance via tunnel sinks and sources. This
is the so called "cascaded setup". The point of doing that is to allow
multiple users to access the same hardware simultaneously.

The module connects to zero or more remote servers and builds an
internal representation of all sinks and sources in those servers.
Currently the module doesn't actually load any tunnel sinks or
sources, because the tunnel sink and source code have to be first
modified a bit (the next patch does that).

The remote servers are configured in a separate configuration file
that is named tunnel-manager.conf. Currently only the server address
can be configured, but I expect more configuration options to be
implement in the future, such as options for defining the policy of
which remote devices to use. Here's an example of the configuration
file syntax:

    [RemoteServer foo]
    address = /run/pulse/native

    [RemoteServer bar]
    address = example.com

The bulk of the functionality is not in module-tunnel-manager.so, but
in libtunnel-manager.so, which will be used also by the tunnel sink
and source modules to access the information that the tunnel manager
has previously queried from the remote server.
---
 src/Makefile.am                                    |  17 +-
 src/modules/tunnel-manager/module-tunnel-manager.c |  65 ++
 src/modules/tunnel-manager/tunnel-manager-config.c | 203 +++++
 src/modules/tunnel-manager/tunnel-manager-config.h |  50 ++
 src/modules/tunnel-manager/tunnel-manager.c        | 840 +++++++++++++++++++++
 src/modules/tunnel-manager/tunnel-manager.h        |  88 +++
 6 files changed, 1262 insertions(+), 1 deletion(-)
 create mode 100644 src/modules/tunnel-manager/module-tunnel-manager.c
 create mode 100644 src/modules/tunnel-manager/tunnel-manager-config.c
 create mode 100644 src/modules/tunnel-manager/tunnel-manager-config.h
 create mode 100644 src/modules/tunnel-manager/tunnel-manager.c
 create mode 100644 src/modules/tunnel-manager/tunnel-manager.h

diff --git a/src/Makefile.am b/src/Makefile.am
index 4e60a98..2fc6403 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1045,7 +1045,8 @@ modlibexec_LTLIBRARIES = \
 		libprotocol-cli.la \
 		libprotocol-simple.la \
 		libprotocol-http.la \
-		libprotocol-native.la
+		libprotocol-native.la \
+		libtunnel-manager.la
 
 if HAVE_WEBRTC
 modlibexec_LTLIBRARIES += libwebrtc-util.la
@@ -1092,6 +1093,12 @@ libprotocol_native_la_CFLAGS += $(DBUS_CFLAGS)
 libprotocol_native_la_LIBADD += $(DBUS_LIBS)
 endif
 
+libtunnel_manager_la_SOURCES = \
+		modules/tunnel-manager/tunnel-manager.c modules/tunnel-manager/tunnel-manager.h \
+		modules/tunnel-manager/tunnel-manager-config.c modules/tunnel-manager/tunnel-manager-config.h
+libtunnel_manager_la_LDFLAGS = $(AM_LDFLAGS) -avoid-version
+libtunnel_manager_la_LIBADD = $(AM_LIBADD) libpulsecore- at PA_MAJORMINOR@.la libpulsecommon- at PA_MAJORMINOR@.la libpulse.la
+
 if HAVE_ESOUND
 libprotocol_esound_la_SOURCES = pulsecore/protocol-esound.c pulsecore/protocol-esound.h pulsecore/esound.h
 libprotocol_esound_la_LDFLAGS = $(AM_LDFLAGS) -avoid-version
@@ -1134,6 +1141,7 @@ modlibexec_LTLIBRARIES += \
 		module-cli.la \
 		module-cli-protocol-tcp.la \
 		module-simple-protocol-tcp.la \
+		module-tunnel-manager.la \
 		module-null-sink.la \
 		module-null-source.la \
 		module-sine-source.la \
@@ -1434,6 +1442,7 @@ SYMDEF_FILES = \
 		module-pipe-source-symdef.h \
 		module-simple-protocol-tcp-symdef.h \
 		module-simple-protocol-unix-symdef.h \
+		module-tunnel-manager-symdef.h \
 		module-native-protocol-tcp-symdef.h \
 		module-native-protocol-unix-symdef.h \
 		module-native-protocol-fd-symdef.h \
@@ -1549,6 +1558,12 @@ module_simple_protocol_unix_la_CFLAGS = -DUSE_UNIX_SOCKETS -DUSE_PROTOCOL_SIMPLE
 module_simple_protocol_unix_la_LDFLAGS = $(MODULE_LDFLAGS)
 module_simple_protocol_unix_la_LIBADD = $(MODULE_LIBADD) libprotocol-simple.la
 
+# Tunnel manager
+
+module_tunnel_manager_la_SOURCES = modules/tunnel-manager/module-tunnel-manager.c
+module_tunnel_manager_la_LDFLAGS = $(MODULE_LDFLAGS)
+module_tunnel_manager_la_LIBADD = $(MODULE_LIBADD) libtunnel-manager.la
+
 # CLI protocol
 
 module_cli_la_SOURCES = modules/module-cli.c
diff --git a/src/modules/tunnel-manager/module-tunnel-manager.c b/src/modules/tunnel-manager/module-tunnel-manager.c
new file mode 100644
index 0000000..1f8a752
--- /dev/null
+++ b/src/modules/tunnel-manager/module-tunnel-manager.c
@@ -0,0 +1,65 @@
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2014 Intel Corporation
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with PulseAudio; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+  USA.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "module-tunnel-manager-symdef.h"
+
+#include <modules/tunnel-manager/tunnel-manager.h>
+
+#include <pulsecore/i18n.h>
+
+PA_MODULE_AUTHOR("Tanu Kaskinen");
+PA_MODULE_DESCRIPTION(_("Manage tunnels to other PulseAudio servers"));
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(true);
+
+struct userdata {
+    pa_tunnel_manager *tunnel_manager;
+};
+
+int pa__init(pa_module *module) {
+    struct userdata *u;
+
+    pa_assert(module);
+
+    u = module->userdata = pa_xnew0(struct userdata, 1);
+    u->tunnel_manager = pa_tunnel_manager_get(module->core, true);
+
+    return 0;
+}
+
+void pa__done(pa_module *module) {
+    struct userdata *u;
+
+    pa_assert(module);
+
+    u = module->userdata;
+    if (!u)
+        return;
+
+    if (u->tunnel_manager)
+        pa_tunnel_manager_unref(u->tunnel_manager);
+
+    pa_xfree(u);
+}
diff --git a/src/modules/tunnel-manager/tunnel-manager-config.c b/src/modules/tunnel-manager/tunnel-manager-config.c
new file mode 100644
index 0000000..ec8e829
--- /dev/null
+++ b/src/modules/tunnel-manager/tunnel-manager-config.c
@@ -0,0 +1,203 @@
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2014 Intel Corporation
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with PulseAudio; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+  USA.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "tunnel-manager-config.h"
+
+#include <pulse/xmalloc.h>
+
+#include <pulsecore/conf-parser.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/namereg.h>
+
+#define REMOTE_SERVER_SECTION_NAME "RemoteServer"
+#define REMOTE_SERVER_SECTION_PREFIX REMOTE_SERVER_SECTION_NAME " "
+
+static int remote_server_config_new(pa_tunnel_manager_config *manager_config, const char *name,
+                                    pa_tunnel_manager_remote_server_config **_r);
+static void remote_server_config_free(pa_tunnel_manager_remote_server_config *config);
+
+static pa_tunnel_manager_config_value *config_value_new(const char *value, const char *filename, unsigned lineno) {
+    pa_tunnel_manager_config_value *config_value;
+
+    pa_assert(value);
+    pa_assert(filename);
+
+    config_value = pa_xnew0(pa_tunnel_manager_config_value, 1);
+    config_value->value = pa_xstrdup(value);
+    config_value->filename = pa_xstrdup(filename);
+    config_value->lineno = lineno;
+
+    return config_value;
+}
+
+static void config_value_free(pa_tunnel_manager_config_value *value) {
+    pa_assert(value);
+
+    pa_xfree(value->filename);
+    pa_xfree(value->value);
+    pa_xfree(value);
+}
+
+static int get_remote_server_config(pa_tunnel_manager_config *manager_config, const char *section,
+                                    pa_tunnel_manager_remote_server_config **_r) {
+    char *name = NULL;
+    pa_tunnel_manager_remote_server_config *server_config;
+    int r;
+
+    pa_assert(manager_config);
+    pa_assert(section);
+    pa_assert(_r);
+
+    name = pa_xstrdup(section + strlen(REMOTE_SERVER_SECTION_PREFIX));
+    name = pa_strip(name);
+
+    server_config = pa_hashmap_get(manager_config->remote_servers, name);
+    if (server_config)
+        goto success;
+
+    r = remote_server_config_new(manager_config, name, &server_config);
+    if (r < 0)
+        goto fail;
+
+success:
+    pa_xfree(name);
+
+    *_r = server_config;
+    return 0;
+
+fail:
+    pa_xfree(name);
+
+    return r;
+}
+
+static int parse_config_value(pa_config_parser_state *state) {
+    pa_tunnel_manager_config *manager_config;
+
+    pa_assert(state);
+
+    manager_config = state->userdata;
+
+    if (!state->section) {
+        pa_log("[%s:%u] \"%s\" is not valid in the General section.", state->filename, state->lineno, state->lvalue);
+        return 0;
+    }
+
+    if (pa_startswith(state->section, REMOTE_SERVER_SECTION_PREFIX)) {
+        int r;
+        pa_tunnel_manager_remote_server_config *server_config;
+
+        r = get_remote_server_config(manager_config, state->section, &server_config);
+        if (r < 0) {
+            pa_log("[%s:%u] Invalid section: \"%s\"", state->filename, state->lineno, state->section);
+            return 0;
+        }
+
+        if (pa_streq(state->lvalue, "address")) {
+            if (server_config->address)
+                config_value_free(server_config->address);
+
+            server_config->address = config_value_new(state->rvalue, state->filename, state->lineno);
+        } else
+            pa_log("[%s:%u] \"%s\" is not valid in the " REMOTE_SERVER_SECTION_NAME " section.", state->filename,
+                   state->lineno, state->section);
+    } else
+        pa_log("[%s:%u] Invalid section: \"%s\"", state->filename, state->lineno, state->section);
+
+    return 0;
+}
+
+pa_tunnel_manager_config *pa_tunnel_manager_config_new(void) {
+    pa_tunnel_manager_config *config;
+    FILE *f;
+    char *fn = NULL;
+
+    config = pa_xnew0(pa_tunnel_manager_config, 1);
+    config->remote_servers = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+
+    f = pa_open_config_file(PA_DEFAULT_CONFIG_DIR PA_PATH_SEP "tunnel-manager.conf", "tunnel-manager.conf", NULL, &fn);
+    if (f) {
+        pa_config_item config_items[] = {
+            { NULL, parse_config_value, NULL, NULL },
+            { NULL, NULL, NULL, NULL },
+        };
+
+        pa_config_parse(fn, f, config_items, NULL, config);
+        pa_xfree(fn);
+        fn = NULL;
+        fclose(f);
+        f = NULL;
+    }
+
+    return config;
+}
+
+void pa_tunnel_manager_config_free(pa_tunnel_manager_config *manager_config) {
+    pa_assert(manager_config);
+
+    if (manager_config->remote_servers) {
+        pa_tunnel_manager_remote_server_config *server_config;
+
+        while ((server_config = pa_hashmap_first(manager_config->remote_servers)))
+            remote_server_config_free(server_config);
+
+        pa_hashmap_free(manager_config->remote_servers);
+    }
+
+    pa_xfree(manager_config);
+}
+
+static int remote_server_config_new(pa_tunnel_manager_config *manager_config, const char *name,
+                                    pa_tunnel_manager_remote_server_config **_r) {
+    pa_tunnel_manager_remote_server_config *server_config = NULL;
+
+    pa_assert(manager_config);
+    pa_assert(name);
+    pa_assert(_r);
+
+    if (!pa_namereg_is_valid_name(name))
+        return -PA_ERR_INVALID;
+
+    server_config = pa_xnew0(pa_tunnel_manager_remote_server_config, 1);
+    server_config->manager_config = manager_config;
+    server_config->name = pa_xstrdup(name);
+
+    pa_assert_se(pa_hashmap_put(manager_config->remote_servers, server_config->name, server_config) >= 0);
+
+    *_r = server_config;
+    return 0;
+}
+
+static void remote_server_config_free(pa_tunnel_manager_remote_server_config *config) {
+    pa_assert(config);
+
+    pa_hashmap_remove(config->manager_config->remote_servers, config->name);
+
+    if (config->address)
+        config_value_free(config->address);
+
+    pa_xfree(config->name);
+    pa_xfree(config);
+}
diff --git a/src/modules/tunnel-manager/tunnel-manager-config.h b/src/modules/tunnel-manager/tunnel-manager-config.h
new file mode 100644
index 0000000..8eac0e8
--- /dev/null
+++ b/src/modules/tunnel-manager/tunnel-manager-config.h
@@ -0,0 +1,50 @@
+#ifndef footunnelmanagerconfighfoo
+#define footunnelmanagerconfighfoo
+
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2014 Intel Corporation
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with PulseAudio; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+  USA.
+***/
+
+#include <pulsecore/hashmap.h>
+
+typedef struct pa_tunnel_manager_config_value pa_tunnel_manager_config_value;
+typedef struct pa_tunnel_manager_config pa_tunnel_manager_config;
+typedef struct pa_tunnel_manager_remote_server_config pa_tunnel_manager_remote_server_config;
+
+struct pa_tunnel_manager_config_value {
+    char *value;
+    char *filename;
+    unsigned lineno;
+};
+
+struct pa_tunnel_manager_config {
+    pa_hashmap *remote_servers; /* name -> pa_tunnel_manager_remote_server_config */
+};
+
+pa_tunnel_manager_config *pa_tunnel_manager_config_new(void);
+void pa_tunnel_manager_config_free(pa_tunnel_manager_config *manager_config);
+
+struct pa_tunnel_manager_remote_server_config {
+    pa_tunnel_manager_config *manager_config;
+    char *name;
+    pa_tunnel_manager_config_value *address;
+};
+
+#endif
diff --git a/src/modules/tunnel-manager/tunnel-manager.c b/src/modules/tunnel-manager/tunnel-manager.c
new file mode 100644
index 0000000..cb8b3a6
--- /dev/null
+++ b/src/modules/tunnel-manager/tunnel-manager.c
@@ -0,0 +1,840 @@
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2014 Intel Corporation
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with PulseAudio; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+  USA.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "tunnel-manager.h"
+
+#include <modules/tunnel-manager/tunnel-manager-config.h>
+
+#include <pulse/error.h>
+#include <pulse/introspect.h>
+#include <pulse/subscribe.h>
+
+#include <pulsecore/core-util.h>
+#include <pulsecore/namereg.h>
+#include <pulsecore/parseaddr.h>
+#include <pulsecore/shared.h>
+
+#define MAX_DEVICES_PER_SERVER 50
+
+static void remote_server_new(pa_tunnel_manager *manager, pa_tunnel_manager_remote_server_config *config);
+static void remote_server_set_up_connection(pa_tunnel_manager_remote_server *server);
+static void remote_server_free(pa_tunnel_manager_remote_server *server);
+static void remote_server_set_failed(pa_tunnel_manager_remote_server *server, bool failed);
+
+static void remote_device_new(pa_tunnel_manager_remote_server *server, pa_device_type_t type, const void *info);
+static void remote_device_free(pa_tunnel_manager_remote_device *device);
+static void remote_device_update(pa_tunnel_manager_remote_device *device);
+
+struct device_stub {
+    pa_tunnel_manager_remote_server *server;
+    pa_device_type_t type;
+    uint32_t index;
+    bool unlinked;
+
+    pa_operation *get_info_operation;
+
+    /* These are a workaround for the problem that the introspection API's info
+     * callbacks are called multiple times, which means that if the userdata
+     * needs to be freed during the callbacks, the freeing needs to be
+     * postponed until the last call. */
+    bool can_free;
+    bool dead;
+};
+
+static void device_stub_new(pa_tunnel_manager_remote_server *server, pa_device_type_t type, uint32_t idx);
+static void device_stub_unlink(struct device_stub *stub);
+static void device_stub_free(struct device_stub *stub);
+
+static pa_tunnel_manager *tunnel_manager_new(pa_core *core) {
+    pa_tunnel_manager *manager;
+    pa_tunnel_manager_config *manager_config;
+    pa_tunnel_manager_remote_server_config *server_config;
+    void *state;
+
+    pa_assert(core);
+
+    manager = pa_xnew0(pa_tunnel_manager, 1);
+    manager->core = core;
+    manager->remote_servers = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+    manager->refcnt = 1;
+
+    manager_config = pa_tunnel_manager_config_new();
+
+    PA_HASHMAP_FOREACH(server_config, manager_config->remote_servers, state)
+        remote_server_new(manager, server_config);
+
+    pa_tunnel_manager_config_free(manager_config);
+
+    pa_shared_set(core, "tunnel_manager", manager);
+
+    return manager;
+}
+
+static void tunnel_manager_free(pa_tunnel_manager *manager) {
+    pa_assert(manager);
+    pa_assert(manager->refcnt == 0);
+
+    pa_shared_remove(manager->core, "tunnel_manager");
+
+    if (manager->remote_servers) {
+        pa_tunnel_manager_remote_server *server;
+
+        while ((server = pa_hashmap_first(manager->remote_servers)))
+            remote_server_free(server);
+
+        pa_hashmap_free(manager->remote_servers);
+    }
+
+    pa_xfree(manager);
+}
+
+pa_tunnel_manager *pa_tunnel_manager_get(pa_core *core, bool ref) {
+    pa_tunnel_manager *manager;
+
+    pa_assert(core);
+
+    manager = pa_shared_get(core, "tunnel_manager");
+    if (manager) {
+        if (ref)
+            manager->refcnt++;
+
+        return manager;
+    }
+
+    if (ref)
+        return tunnel_manager_new(core);
+
+    return NULL;
+}
+
+void pa_tunnel_manager_unref(pa_tunnel_manager *manager) {
+    pa_assert(manager);
+    pa_assert(manager->refcnt > 0);
+
+    manager->refcnt--;
+
+    if (manager->refcnt == 0)
+        tunnel_manager_free(manager);
+}
+
+static void remote_server_new(pa_tunnel_manager *manager, pa_tunnel_manager_remote_server_config *config) {
+    int r;
+    pa_parsed_address parsed_address;
+    pa_tunnel_manager_remote_server *server = NULL;
+
+    pa_assert(manager);
+    pa_assert(config);
+
+    if (!config->address) {
+        pa_log("No address configured for remote server %s.", config->name);
+        return;
+    }
+
+    r = pa_parse_address(config->address->value, &parsed_address);
+    if (r < 0) {
+        pa_log("[%s:%u] Invalid address: \"%s\"", config->address->filename, config->address->lineno, config->address->value);
+        return;
+    }
+
+    pa_xfree(parsed_address.path_or_host);
+
+    server = pa_xnew0(pa_tunnel_manager_remote_server, 1);
+    server->manager = manager;
+    server->name = pa_xstrdup(config->name);
+    server->address = pa_xstrdup(config->address->value);
+    server->devices = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+    server->device_stubs = pa_hashmap_new(NULL, NULL);
+
+    pa_assert_se(pa_hashmap_put(manager->remote_servers, server->name, server) >= 0);
+
+    pa_log_debug("Created remote server %s.", server->name);
+    pa_log_debug("    Address: %s", server->address);
+    pa_log_debug("    Failed: %s", pa_boolean_to_string(server->failed));
+
+    remote_server_set_up_connection(server);
+}
+
+static const char *device_type_to_string(pa_device_type_t type) {
+    switch (type) {
+        case PA_DEVICE_TYPE_SINK:   return "sink";
+        case PA_DEVICE_TYPE_SOURCE: return "source";
+    }
+
+    pa_assert_not_reached();
+}
+
+static void subscribe_cb(pa_context *context, pa_subscription_event_type_t event_type, uint32_t idx, void *userdata) {
+    pa_tunnel_manager_remote_server *server = userdata;
+    pa_device_type_t device_type;
+    pa_tunnel_manager_remote_device *device;
+    void *state;
+
+    pa_assert(context);
+    pa_assert(server);
+
+    if ((event_type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK)
+        device_type = PA_DEVICE_TYPE_SINK;
+    else if ((event_type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE)
+        device_type = PA_DEVICE_TYPE_SOURCE;
+    else {
+        pa_log("[%s] Unexpected event facility: %u", server->name,
+               (unsigned) (event_type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK));
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    if (idx == PA_INVALID_INDEX) {
+        pa_log("[%s] Invalid %s index.", server->name, device_type_to_string(device_type));
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    if ((event_type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) {
+        if ((device_type == PA_DEVICE_TYPE_SINK && server->list_sinks_operation)
+                || (device_type == PA_DEVICE_TYPE_SOURCE && server->list_sources_operation))
+            return;
+
+        device_stub_new(server, device_type, idx);
+
+        return;
+    }
+
+    if ((event_type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+        struct device_stub *stub;
+
+        PA_HASHMAP_FOREACH(device, server->devices, state) {
+            if (device->type == device_type && device->index == idx) {
+                remote_device_free(device);
+                return;
+            }
+        }
+
+        PA_HASHMAP_FOREACH(stub, server->device_stubs, state) {
+            if (stub->type == device_type && stub->index == idx) {
+                device_stub_free(stub);
+                return;
+            }
+        }
+
+        return;
+    }
+
+    if ((event_type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE) {
+        PA_HASHMAP_FOREACH(device, server->devices, state) {
+            if (device->type == device_type && device->index == idx) {
+                remote_device_update(device);
+                return;
+            }
+        }
+
+        return;
+    }
+}
+
+static void subscribe_success_cb(pa_context *context, int success, void *userdata) {
+    pa_tunnel_manager_remote_server *server = userdata;
+
+    pa_assert(context);
+    pa_assert(server);
+
+    if (!success) {
+        pa_log("[%s] Subscribing to device events failed: %s", server->name, pa_strerror(pa_context_errno(context)));
+        remote_server_set_failed(server, true);
+    }
+}
+
+static void get_sink_info_list_cb(pa_context *context, const pa_sink_info *info, int is_last, void *userdata) {
+    pa_tunnel_manager_remote_server *server = userdata;
+
+    pa_assert(context);
+    pa_assert(server);
+
+    if (server->list_sinks_operation) {
+        pa_operation_unref(server->list_sinks_operation);
+        server->list_sinks_operation = NULL;
+    }
+
+    if (is_last < 0) {
+        pa_log("[%s] Listing sinks failed: %s", server->name, pa_strerror(pa_context_errno(context)));
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    if (is_last)
+        return;
+
+    remote_device_new(server, PA_DEVICE_TYPE_SINK, info);
+}
+
+static void get_source_info_list_cb(pa_context *context, const pa_source_info *info, int is_last, void *userdata) {
+    pa_tunnel_manager_remote_server *server = userdata;
+
+    pa_assert(context);
+    pa_assert(server);
+
+    if (server->list_sources_operation) {
+        pa_operation_unref(server->list_sources_operation);
+        server->list_sources_operation = NULL;
+    }
+
+    if (is_last < 0) {
+        pa_log("[%s] Listing sources failed: %s", server->name, pa_strerror(pa_context_errno(context)));
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    if (is_last)
+        return;
+
+    remote_device_new(server, PA_DEVICE_TYPE_SOURCE, info);
+}
+
+static void context_state_cb(pa_context *context, void *userdata) {
+    pa_tunnel_manager_remote_server *server = userdata;
+    pa_context_state_t state;
+
+    pa_assert(context);
+    pa_assert(server);
+    pa_assert(context == server->context);
+
+    state = pa_context_get_state(context);
+
+    switch (state) {
+        case PA_CONTEXT_READY: {
+            pa_operation *operation;
+
+            pa_context_set_subscribe_callback(context, subscribe_cb, server);
+            operation = pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE,
+                                             subscribe_success_cb, server);
+            if (operation)
+                pa_operation_unref(operation);
+            else {
+                pa_log("[%s] pa_context_subscribe() failed: %s", server->name, pa_strerror(pa_context_errno(context)));
+                remote_server_set_failed(server, true);
+                return;
+            }
+
+            pa_assert(!server->list_sinks_operation);
+            pa_assert(!server->list_sources_operation);
+
+            server->list_sinks_operation = pa_context_get_sink_info_list(server->context, get_sink_info_list_cb, server);
+            if (!server->list_sinks_operation) {
+                pa_log("[%s] pa_context_get_sink_info_list() failed: %s", server->name,
+                       pa_strerror(pa_context_errno(context)));
+                remote_server_set_failed(server, true);
+                return;
+            }
+
+            server->list_sources_operation = pa_context_get_source_info_list(server->context, get_source_info_list_cb, server);
+            if (!server->list_sources_operation) {
+                pa_log("[%s] pa_context_get_source_info_list() failed: %s", server->name,
+                       pa_strerror(pa_context_errno(context)));
+                remote_server_set_failed(server, true);
+                return;
+            }
+
+            return;
+        }
+
+        case PA_CONTEXT_FAILED:
+            pa_log("[%s] Context failed: %s", server->name, pa_strerror(pa_context_errno(context)));
+            remote_server_set_failed(server, true);
+            return;
+
+        default:
+            return;
+    }
+}
+
+static void remote_server_set_up_connection(pa_tunnel_manager_remote_server *server) {
+    pa_assert(server);
+    pa_assert(!server->context);
+
+    server->context = pa_context_new(server->manager->core->mainloop, "PulseAudio");
+    if (server->context) {
+        int r;
+
+        r = pa_context_connect(server->context, server->address, PA_CONTEXT_NOFLAGS, NULL);
+        if (r >= 0)
+            pa_context_set_state_callback(server->context, context_state_cb, server);
+        else {
+            pa_log("[%s] pa_context_connect() failed: %s", server->name, pa_strerror(pa_context_errno(server->context)));
+            remote_server_set_failed(server, true);
+        }
+    } else {
+        pa_log("[%s] pa_context_new() failed.", server->name);
+        remote_server_set_failed(server, true);
+    }
+}
+
+static void remote_server_tear_down_connection(pa_tunnel_manager_remote_server *server) {
+    pa_assert(server);
+
+    if (server->device_stubs) {
+        struct device_stub *stub;
+
+        while ((stub = pa_hashmap_first(server->device_stubs)))
+            device_stub_free(stub);
+    }
+
+    if (server->devices) {
+        pa_tunnel_manager_remote_device *device;
+
+        while ((device = pa_hashmap_first(server->devices)))
+            remote_device_free(device);
+    }
+
+    if (server->list_sources_operation) {
+        pa_operation_cancel(server->list_sources_operation);
+        pa_operation_unref(server->list_sources_operation);
+        server->list_sources_operation = NULL;
+    }
+
+    if (server->list_sinks_operation) {
+        pa_operation_cancel(server->list_sinks_operation);
+        pa_operation_unref(server->list_sinks_operation);
+        server->list_sinks_operation = NULL;
+    }
+
+    if (server->context) {
+        pa_context_disconnect(server->context);
+        pa_context_unref(server->context);
+        server->context = NULL;
+    }
+}
+
+static void remote_server_free(pa_tunnel_manager_remote_server *server) {
+    pa_assert(server);
+
+    pa_log_debug("Freeing remote server %s.", server->name);
+
+    pa_hashmap_remove(server->manager->remote_servers, server->name);
+
+    remote_server_tear_down_connection(server);
+
+    if (server->device_stubs) {
+        pa_assert(pa_hashmap_isempty(server->device_stubs));
+        pa_hashmap_free(server->device_stubs);
+    }
+
+    if (server->devices) {
+        pa_assert(pa_hashmap_isempty(server->devices));
+        pa_hashmap_free(server->devices);
+    }
+
+    pa_xfree(server->address);
+    pa_xfree(server->name);
+    pa_xfree(server);
+}
+
+static void remote_server_set_failed(pa_tunnel_manager_remote_server *server, bool failed) {
+    pa_assert(server);
+
+    if (failed == server->failed)
+        return;
+
+    server->failed = failed;
+
+    pa_log_debug("[%s] Failed changed from %s to %s.", server->name, pa_boolean_to_string(!failed),
+                 pa_boolean_to_string(failed));
+
+    if (failed)
+        remote_server_tear_down_connection(server);
+}
+
+static void remote_device_new(pa_tunnel_manager_remote_server *server, pa_device_type_t type, const void *info) {
+    const char *name = NULL;
+    uint32_t idx = PA_INVALID_INDEX;
+    pa_proplist *proplist = NULL;
+    const pa_sample_spec *sample_spec = NULL;
+    const pa_channel_map *channel_map = NULL;
+    bool is_monitor = false;
+    pa_tunnel_manager_remote_device *device;
+    unsigned i;
+    char sample_spec_str[PA_SAMPLE_SPEC_SNPRINT_MAX];
+    char channel_map_str[PA_CHANNEL_MAP_SNPRINT_MAX];
+
+    pa_assert(server);
+    pa_assert(info);
+
+    switch (type) {
+        case PA_DEVICE_TYPE_SINK: {
+            const pa_sink_info *sink_info = info;
+
+            name = sink_info->name;
+            idx = sink_info->index;
+            proplist = sink_info->proplist;
+            sample_spec = &sink_info->sample_spec;
+            channel_map = &sink_info->channel_map;
+            break;
+        }
+
+        case PA_DEVICE_TYPE_SOURCE: {
+            const pa_source_info *source_info = info;
+
+            name = source_info->name;
+            idx = source_info->index;
+            proplist = source_info->proplist;
+            sample_spec = &source_info->sample_spec;
+            channel_map = &source_info->channel_map;
+            is_monitor = !!source_info->monitor_of_sink_name;
+
+            break;
+        }
+    }
+
+    /* TODO: This check should be done in libpulse. */
+    if (!name || !pa_namereg_is_valid_name(name)) {
+        pa_log("[%s] Invalid remote device name: %s", server->name, pa_strnull(name));
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    if (pa_hashmap_get(server->devices, name)) {
+        pa_log("[%s] Duplicate remote device name: %s", server->name, name);
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    if (pa_hashmap_size(server->devices) + pa_hashmap_size(server->device_stubs) >= MAX_DEVICES_PER_SERVER) {
+        pa_log("[%s] Maximum number of devices reached, can't create a new remote device.", server->name);
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    /* TODO: This check should be done in libpulse. */
+    if (!pa_sample_spec_valid(sample_spec)) {
+        pa_log("[%s %s] Invalid sample spec.", server->name, name);
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    /* TODO: This check should be done in libpulse. */
+    if (!pa_channel_map_valid(channel_map)) {
+        pa_log("[%s %s] Invalid channel map.", server->name, name);
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    device = pa_xnew0(pa_tunnel_manager_remote_device, 1);
+    device->server = server;
+    device->type = type;
+    device->name = pa_xstrdup(name);
+    device->index = idx;
+    device->proplist = pa_proplist_copy(proplist);
+    device->sample_spec = *sample_spec;
+    device->channel_map = *channel_map;
+    device->is_monitor = is_monitor;
+
+    for (i = 0; i < PA_TUNNEL_MANAGER_REMOTE_DEVICE_HOOK_MAX; i++)
+        pa_hook_init(&device->hooks[i], device);
+
+    device->can_free = true;
+
+    pa_hashmap_put(server->devices, device->name, device);
+
+    pa_log_debug("[%s] Created remote device %s.", server->name, device->name);
+    pa_log_debug("        Type: %s", device_type_to_string(type));
+    pa_log_debug("        Index: %u", idx);
+    pa_log_debug("        Sample spec: %s", pa_sample_spec_snprint(sample_spec_str, sizeof(sample_spec_str), sample_spec));
+    pa_log_debug("        Channel map: %s", pa_channel_map_snprint(channel_map_str, sizeof(channel_map_str), channel_map));
+    pa_log_debug("        Is monitor: %s", pa_boolean_to_string(device->is_monitor));
+}
+
+static void remote_device_free(pa_tunnel_manager_remote_device *device) {
+    unsigned i;
+
+    pa_assert(device);
+
+    pa_log_debug("[%s] Freeing remote device %s.", device->server->name, device->name);
+
+    pa_hashmap_remove(device->server->devices, device->name);
+    pa_hook_fire(&device->hooks[PA_TUNNEL_MANAGER_REMOTE_DEVICE_HOOK_UNLINKED], NULL);
+
+    if (device->get_info_operation) {
+        pa_operation_cancel(device->get_info_operation);
+        pa_operation_unref(device->get_info_operation);
+    }
+
+    for (i = 0; i < PA_TUNNEL_MANAGER_REMOTE_DEVICE_HOOK_MAX; i++)
+        pa_hook_done(&device->hooks[i]);
+
+    if (device->proplist)
+        pa_proplist_free(device->proplist);
+
+    pa_xfree(device->name);
+    pa_xfree(device);
+}
+
+static void remote_device_set_proplist(pa_tunnel_manager_remote_device *device, pa_proplist *proplist) {
+    pa_assert(device);
+    pa_assert(proplist);
+
+    if (pa_proplist_equal(proplist, device->proplist))
+        return;
+
+    pa_proplist_update(device->proplist, PA_UPDATE_SET, proplist);
+
+    pa_log_debug("[%s %s] Proplist changed.", device->server->name, device->name);
+
+    pa_hook_fire(&device->hooks[PA_TUNNEL_MANAGER_REMOTE_DEVICE_HOOK_PROPLIST_CHANGED], NULL);
+}
+
+static void remote_device_get_info_cb(pa_context *context, const void *info, int is_last, void *userdata) {
+    pa_tunnel_manager_remote_device *device = userdata;
+    pa_proplist *proplist = NULL;
+
+    pa_assert(context);
+    pa_assert(device);
+
+    if (device->get_info_operation) {
+        pa_operation_unref(device->get_info_operation);
+        device->get_info_operation = NULL;
+    }
+
+    if (is_last < 0) {
+        pa_log_debug("[%s %s] Getting info failed: %s", device->server->name, device->name,
+                     pa_strerror(pa_context_errno(context)));
+        return;
+    }
+
+    if (is_last) {
+        device->can_free = true;
+
+        if (device->dead)
+            remote_device_free(device);
+
+        return;
+    }
+
+    device->can_free = false;
+
+    if (device->dead)
+        return;
+
+    switch (device->type) {
+        case PA_DEVICE_TYPE_SINK:
+            proplist = ((const pa_sink_info *) info)->proplist;
+            break;
+
+        case PA_DEVICE_TYPE_SOURCE:
+            proplist = ((const pa_source_info *) info)->proplist;
+            break;
+    }
+
+    remote_device_set_proplist(device, proplist);
+}
+
+static void remote_device_update(pa_tunnel_manager_remote_device *device) {
+    pa_assert(device);
+
+    if (device->get_info_operation)
+        return;
+
+    switch (device->type) {
+        case PA_DEVICE_TYPE_SINK:
+            device->get_info_operation = pa_context_get_sink_info_by_name(device->server->context, device->name,
+                                                                          (pa_sink_info_cb_t) remote_device_get_info_cb,
+                                                                          device);
+            break;
+
+        case PA_DEVICE_TYPE_SOURCE:
+            device->get_info_operation = pa_context_get_source_info_by_name(device->server->context, device->name,
+                                                                            (pa_source_info_cb_t) remote_device_get_info_cb,
+                                                                            device);
+            break;
+    }
+
+    if (!device->get_info_operation) {
+        pa_log("[%s %s] pa_context_get_%s_info_by_name() failed: %s", device->server->name, device->name,
+               device_type_to_string(device->type), pa_strerror(pa_context_errno(device->server->context)));
+        remote_server_set_failed(device->server, true);
+    }
+}
+
+static void device_stub_get_info_cb(pa_context *context, const void *info, int is_last, void *userdata) {
+    struct device_stub *stub = userdata;
+    uint32_t idx = PA_INVALID_INDEX;
+
+    pa_assert(context);
+    pa_assert(stub);
+
+    if (stub->get_info_operation) {
+        pa_operation_unref(stub->get_info_operation);
+        stub->get_info_operation = NULL;
+    }
+
+    if (is_last < 0) {
+        pa_log_debug("[%s] Getting info for %s %u failed: %s", stub->server->name, device_type_to_string(stub->type),
+                     stub->index, pa_strerror(pa_context_errno(context)));
+        device_stub_free(stub);
+        return;
+    }
+
+    if (is_last) {
+        stub->can_free = true;
+
+        /* TODO: libpulse should ensure that the get info operation doesn't
+         * return an empty result. Then this check wouldn't be needed. */
+        if (!stub->unlinked) {
+            pa_log("[%s] No info received for %s %u.", stub->server->name, device_type_to_string(stub->type), stub->index);
+            remote_server_set_failed(stub->server, true);
+            return;
+        }
+
+        device_stub_free(stub);
+        return;
+    }
+
+    /* This callback will still be called at least once, so we need to keep the
+     * stub alive. */
+    stub->can_free = false;
+
+    if (stub->dead)
+        return;
+
+    /* TODO: libpulse should ensure that the get info operation doesn't return
+     * more than one result. Then this check wouldn't be needed. */
+    if (stub->unlinked) {
+        pa_log("[%s] Multiple info structs received for %s %u.", stub->server->name, device_type_to_string(stub->type),
+               stub->index);
+        remote_server_set_failed(stub->server, true);
+        return;
+    }
+
+    /* remote_device_new() checks whether the maximum device limit has been
+     * reached, and device stubs count towards that limit. This stub shouldn't
+     * any more count towards the limit, so let's remove the stub from the
+     * server's accounting. */
+    device_stub_unlink(stub);
+
+    switch (stub->type) {
+        case PA_DEVICE_TYPE_SINK:
+            idx = ((const pa_sink_info *) info)->index;
+            break;
+
+        case PA_DEVICE_TYPE_SOURCE:
+            idx = ((const pa_source_info *) info)->index;
+            break;
+    }
+
+    if (idx != stub->index) {
+        pa_log("[%s] Index mismatch for %s %u.", stub->server->name, device_type_to_string(stub->type), stub->index);
+        remote_server_set_failed(stub->server, true);
+        return;
+    }
+
+    if (stub->type == PA_DEVICE_TYPE_SOURCE && ((const pa_source_info *) info)->monitor_of_sink != PA_INVALID_INDEX)
+        return;
+
+    remote_device_new(stub->server, stub->type, info);
+}
+
+static void device_stub_new(pa_tunnel_manager_remote_server *server, pa_device_type_t type, uint32_t idx) {
+    pa_tunnel_manager_remote_device *device;
+    void *state;
+    struct device_stub *stub;
+
+    pa_assert(server);
+
+    PA_HASHMAP_FOREACH(device, server->devices, state) {
+        if (device->type == type && device->index == idx) {
+            pa_log("[%s] Duplicate %s index %u.", server->name, device_type_to_string(type), idx);
+            remote_server_set_failed(server, true);
+            return;
+        }
+    }
+
+    PA_HASHMAP_FOREACH(stub, server->device_stubs, state) {
+        if (stub->type == type && stub->index == idx) {
+            pa_log("[%s] Duplicate %s index %u.", server->name, device_type_to_string(type), idx);
+            remote_server_set_failed(server, true);
+            return;
+        }
+    }
+
+    if (pa_hashmap_size(server->devices) + pa_hashmap_size(server->device_stubs) >= MAX_DEVICES_PER_SERVER) {
+        pa_log("[%s] Maximum number of devices reached, can't create a new remote device.", server->name);
+        remote_server_set_failed(server, true);
+        return;
+    }
+
+    stub = pa_xnew0(struct device_stub, 1);
+    stub->server = server;
+    stub->type = type;
+    stub->index = idx;
+    stub->can_free = true;
+
+    pa_hashmap_put(server->device_stubs, stub, stub);
+
+    switch (type) {
+        case PA_DEVICE_TYPE_SINK:
+            stub->get_info_operation = pa_context_get_sink_info_by_index(server->context, idx,
+                                                                         (pa_sink_info_cb_t) device_stub_get_info_cb,
+                                                                         stub);
+            break;
+
+        case PA_DEVICE_TYPE_SOURCE:
+            stub->get_info_operation = pa_context_get_source_info_by_index(server->context, idx,
+                                                                           (pa_source_info_cb_t) device_stub_get_info_cb,
+                                                                           stub);
+            break;
+    }
+
+    if (!stub->get_info_operation) {
+        pa_log("[%s] pa_context_get_%s_info_by_index() failed: %s", server->name, device_type_to_string(type),
+               pa_strerror(pa_context_errno(server->context)));
+        remote_server_set_failed(server, true);
+        return;
+    }
+}
+
+static void device_stub_unlink(struct device_stub *stub) {
+    pa_assert(stub);
+
+    if (stub->unlinked)
+        return;
+
+    stub->unlinked = true;
+
+    pa_hashmap_remove(stub->server->device_stubs, stub);
+
+    if (stub->get_info_operation) {
+        pa_operation_cancel(stub->get_info_operation);
+        pa_operation_unref(stub->get_info_operation);
+        stub->get_info_operation = NULL;
+    }
+}
+
+static void device_stub_free(struct device_stub *stub) {
+    pa_assert(stub);
+
+    device_stub_unlink(stub);
+
+    if (stub->can_free)
+        pa_xfree(stub);
+    else
+        stub->dead = true;
+}
diff --git a/src/modules/tunnel-manager/tunnel-manager.h b/src/modules/tunnel-manager/tunnel-manager.h
new file mode 100644
index 0000000..9775fd1
--- /dev/null
+++ b/src/modules/tunnel-manager/tunnel-manager.h
@@ -0,0 +1,88 @@
+#ifndef footunnelmanagerhfoo
+#define footunnelmanagerhfoo
+
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2014 Intel Corporation
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with PulseAudio; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+  USA.
+***/
+
+#include <pulse/context.h>
+
+#include <pulsecore/core.h>
+
+typedef struct pa_tunnel_manager pa_tunnel_manager;
+typedef struct pa_tunnel_manager_remote_server pa_tunnel_manager_remote_server;
+typedef struct pa_tunnel_manager_remote_device pa_tunnel_manager_remote_device;
+
+struct pa_tunnel_manager {
+    pa_core *core;
+    pa_hashmap *remote_servers; /* name -> pa_tunnel_manager_remote_server */
+
+    unsigned refcnt;
+};
+
+/* If ref is true, the reference count of the manager is incremented, and also
+ * the manager is created if it doesn't exist yet. If ref is false, the
+ * reference count is not incremented, and if the manager doesn't exist, the
+ * function returns NULL. */
+pa_tunnel_manager *pa_tunnel_manager_get(pa_core *core, bool ref);
+
+void pa_tunnel_manager_unref(pa_tunnel_manager *manager);
+
+struct pa_tunnel_manager_remote_server {
+    pa_tunnel_manager *manager;
+    char *name;
+    char *address;
+    pa_hashmap *devices; /* name -> pa_tunnel_manager_remote_device */
+    bool failed;
+
+    pa_context *context;
+    pa_operation *list_sinks_operation;
+    pa_operation *list_sources_operation;
+    pa_hashmap *device_stubs; /* struct device_stub -> struct device_stub (hashmap-as-a-set) */
+};
+
+enum {
+    PA_TUNNEL_MANAGER_REMOTE_DEVICE_HOOK_UNLINKED,
+    PA_TUNNEL_MANAGER_REMOTE_DEVICE_HOOK_PROPLIST_CHANGED,
+    PA_TUNNEL_MANAGER_REMOTE_DEVICE_HOOK_MAX,
+};
+
+struct pa_tunnel_manager_remote_device {
+    pa_tunnel_manager_remote_server *server;
+    char *name;
+    pa_device_type_t type;
+    uint32_t index;
+    pa_proplist *proplist;
+    pa_sample_spec sample_spec;
+    pa_channel_map channel_map;
+    bool is_monitor;
+    pa_hook hooks[PA_TUNNEL_MANAGER_REMOTE_DEVICE_HOOK_MAX];
+
+    pa_operation *get_info_operation;
+
+    /* These are a workaround for the problem that the introspection API's info
+     * callbacks are called multiple times, which means that if the userdata
+     * needs to be freed during the callbacks, the freeing needs to be
+     * postponed until the last call. */
+    bool can_free;
+    bool dead;
+};
+
+#endif
-- 
1.9.3



More information about the pulseaudio-discuss mailing list