[pulseaudio-discuss] [PATCH 2/2] add module-tunnel-sink-new: experimental rewrite of module-tunnel using libpulse. Old module-tunnel shares duplicated functionality with libpulse because it is implementing pulse protocol again. It is also not as much tested as libpulse it and not refactored.
Alexander Couzens
lynxis at fe80.eu
Mon Aug 5 20:30:09 PDT 2013
Signed-off-by: Alexander Couzens <lynxis at fe80.eu>
---
src/Makefile.am | 6 +
src/modules/module-tunnel-sink-new.c | 624 +++++++++++++++++++++++++++++++++++
2 files changed, 630 insertions(+)
create mode 100644 src/modules/module-tunnel-sink-new.c
diff --git a/src/Makefile.am b/src/Makefile.am
index 6de6e96..27477e9 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1097,6 +1097,7 @@ modlibexec_LTLIBRARIES += \
module-remap-sink.la \
module-remap-source.la \
module-ladspa-sink.la \
+ module-tunnel-sink-new.la \
module-tunnel-sink.la \
module-tunnel-source.la \
module-position-event-sounds.la \
@@ -1368,6 +1369,7 @@ SYMDEF_FILES = \
module-ladspa-sink-symdef.h \
module-equalizer-sink-symdef.h \
module-match-symdef.h \
+ module-tunnel-sink-new-symdef.h \
module-tunnel-sink-symdef.h \
module-tunnel-source-symdef.h \
module-null-sink-symdef.h \
@@ -1638,6 +1640,10 @@ module_match_la_SOURCES = modules/module-match.c
module_match_la_LDFLAGS = $(MODULE_LDFLAGS)
module_match_la_LIBADD = $(MODULE_LIBADD)
+module_tunnel_sink_new_la_SOURCES = modules/module-tunnel-sink-new.c
+module_tunnel_sink_new_la_LDFLAGS = $(MODULE_LDFLAGS)
+module_tunnel_sink_new_la_LIBADD = $(MODULE_LIBADD)
+
module_tunnel_sink_la_SOURCES = modules/module-tunnel.c
module_tunnel_sink_la_CFLAGS = -DTUNNEL_SINK=1 $(AM_CFLAGS)
module_tunnel_sink_la_LDFLAGS = $(MODULE_LDFLAGS)
diff --git a/src/modules/module-tunnel-sink-new.c b/src/modules/module-tunnel-sink-new.c
new file mode 100644
index 0000000..5e1eb59
--- /dev/null
+++ b/src/modules/module-tunnel-sink-new.c
@@ -0,0 +1,624 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2013 Alexander Couzens
+
+ 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 <pulse/context.h>
+#include <pulse/timeval.h>
+#include <pulse/xmalloc.h>
+#include <pulse/stream.h>
+#include <pulse/mainloop.h>
+#include <pulse/subscribe.h>
+#include <pulse/introspect.h>
+
+#include <pulsecore/core.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/i18n.h>
+#include <pulsecore/sink.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/log.h>
+#include <pulsecore/thread.h>
+#include <pulsecore/thread-mq.h>
+#include <pulsecore/poll.h>
+#include <pulsecore/proplist-util.h>
+
+#include "module-tunnel-sink-new-symdef.h"
+
+PA_MODULE_AUTHOR("Alexander Couzens");
+PA_MODULE_DESCRIPTION("Create a network sink which connects via a stream to a remote PulseAudio server");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(false);
+PA_MODULE_USAGE(
+ "server=<address> "
+ "sink=<name of the remote sink> "
+ "sink_name=<name for the local sink> "
+ "sink_properties=<properties for the local sink> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "rate=<sample rate> "
+ "channel_map=<channel map>"
+ );
+
+#define TUNNEL_THREAD_FAILED_MAINLOOP 1
+
+/* libpulse callbacks */
+static void stream_state_callback(pa_stream *stream, void *userdata);
+static void context_state_callback(pa_context *c, void *userdata);
+static void context_subscribe_callback(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata);
+static void context_sink_input_info_callback(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata);
+/* used for calls we ignore the response */
+static void context_ignore_success_callback(pa_context *c, int success, void *userdata);
+
+static void sink_update_requested_latency_cb(pa_sink *s);
+static void sink_write_volume_callback(pa_sink *sink);
+
+struct userdata {
+ pa_module *module;
+ pa_sink *sink;
+ pa_thread *thread;
+ pa_thread_mq thread_mq;
+ pa_mainloop *thread_mainloop;
+ pa_mainloop_api *thread_mainloop_api;
+
+ /* libpulse context */
+ pa_context *context;
+ pa_stream *stream;
+
+ /* volume is applied on the remote server - this is similiar to a hw mixer */
+ /* TODO: check if a saved volume got restored in a correct way */
+ pa_cvolume volume;
+ pa_buffer_attr bufferattr;
+
+ bool connected;
+
+ char *remote_server;
+ char *remote_sink_name;
+};
+
+static const char* const valid_modargs[] = {
+ "sink_name",
+ "sink_properties",
+ "server",
+ "sink",
+ "format",
+ "channels",
+ "rate",
+ "channel_map",
+ "cookie", /* unimplemented */
+ "reconnect", /* reconnect if server comes back again - unimplemented*/
+ NULL,
+};
+
+static pa_proplist* tunnel_new_proplist(struct userdata *u) {
+ pa_proplist *proplist = pa_proplist_new();
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "PulseAudio");
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "org.PulseAudio.PulseAudio");
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION);
+ pa_init_proplist(proplist);
+
+ return proplist;
+}
+
+static void thread_func(void *userdata) {
+ struct userdata *u = userdata;
+ pa_proplist *proplist;
+ pa_memchunk memchunk;
+
+ pa_assert(u);
+
+ pa_log_debug("Thread starting up");
+ pa_thread_mq_install(&u->thread_mq);
+
+ pa_memchunk_reset(&memchunk);
+
+ proplist = tunnel_new_proplist(u);
+ /* init libpulse */
+ u->context = pa_context_new_with_proplist(pa_mainloop_get_api(u->thread_mainloop),
+ "PulseAudio",
+ proplist);
+ pa_proplist_free(proplist);
+
+ if (!u->context) {
+ pa_log("Failed to create libpulse context");
+ goto fail;
+ }
+
+ pa_context_set_subscribe_callback(u->context, context_subscribe_callback, u);
+ pa_context_set_state_callback(u->context, context_state_callback, u);
+ if (pa_context_connect(u->context,
+ u->remote_server,
+ PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN,
+ NULL) < 0) {
+ pa_log("Failed to connect libpulse context");
+ goto fail;
+ }
+
+ for (;;) {
+ int ret;
+ const void *p;
+
+ size_t writable = 0;
+
+ if (pa_mainloop_iterate(u->thread_mainloop, 1, &ret) < 0) {
+ if (ret == 0)
+ goto finish;
+ else
+ goto fail;
+ }
+
+ if (PA_UNLIKELY(u->sink->thread_info.rewind_requested))
+ pa_sink_process_rewind(u->sink, 0);
+
+ if (u->connected &&
+ PA_STREAM_IS_GOOD(pa_stream_get_state(u->stream)) &&
+ PA_SINK_IS_LINKED(u->sink->thread_info.state)) {
+ /* TODO: use IS_RUNNING + cork stream */
+
+ if (pa_stream_is_corked(u->stream)) {
+ pa_stream_cork(u->stream, 0, NULL, NULL);
+ } else {
+ writable = pa_stream_writable_size(u->stream);
+ if (writable > 0) {
+ if (memchunk.length <= 0)
+ pa_sink_render(u->sink, writable, &memchunk);
+
+ pa_assert(memchunk.length > 0);
+
+ /* we have new data to write */
+ p = (const uint8_t *) pa_memblock_acquire(memchunk.memblock);
+ /* TODO: ZERO COPY! */
+ ret = pa_stream_write(u->stream,
+ ((uint8_t*) p + memchunk.index),
+ memchunk.length,
+ NULL, /**< A cleanup routine for the data or NULL to request an internal copy */
+ 0, /** offset */
+ PA_SEEK_RELATIVE
+ );
+ pa_memblock_release(memchunk.memblock);
+ pa_memblock_unref(memchunk.memblock);
+ pa_memchunk_reset(&memchunk);
+
+ if (ret != 0) {
+ /* TODO: we should consider a state change or is that already done ? */
+ pa_log_warn("Could not write data into the stream ... ret = %i", ret);
+ }
+ }
+ }
+ }
+ }
+fail:
+ /* If this was no regular exit from the loop we have to continue
+ * processing messages until we received PA_MESSAGE_SHUTDOWN
+ *
+ * Note: is this a race condition? When a PA_MESSAGE_SHUTDOWN already within the queue?
+ */
+ pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->module->core), PA_CORE_MESSAGE_UNLOAD_MODULE, u->module, 0, NULL, NULL);
+ pa_asyncmsgq_wait_for(u->thread_mq.inq, PA_MESSAGE_SHUTDOWN);
+
+finish:
+
+ if (memchunk.memblock)
+ pa_memblock_unref(memchunk.memblock);
+
+ if (u->stream) {
+ pa_stream_disconnect(u->stream);
+ pa_stream_unref(u->stream);
+ u->stream = NULL;
+ }
+
+ if (u->context) {
+ pa_context_disconnect(u->context);
+ pa_context_unref(u->context);
+ u->context = NULL;
+ }
+
+ pa_log_debug("Thread shutting down");
+}
+
+static void context_sink_input_info_callback(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata) {
+ struct userdata *u = userdata;
+
+ pa_assert(u);
+
+ if (!i)
+ return;
+
+ if (eol < 0) {
+ return;
+ }
+
+ if ((pa_context_get_server_protocol_version(c) < 20) || (i->has_volume)) {
+ u->volume = i->volume;
+ pa_sink_update_volume_and_mute(u->sink);
+ }
+}
+
+static void stream_state_callback(pa_stream *stream, void *userdata) {
+ struct userdata *u = userdata;
+
+ pa_assert(u);
+
+ switch (pa_stream_get_state(stream)) {
+ case PA_STREAM_FAILED:
+ pa_log_error("Stream failed.");
+ u->connected = false;
+ u->thread_mainloop_api->quit(u->thread_mainloop_api, TUNNEL_THREAD_FAILED_MAINLOOP);
+ break;
+ case PA_STREAM_TERMINATED:
+ pa_log_debug("Stream terminated.");
+ break;
+ default:
+ break;
+ }
+}
+
+static void context_subscribe_callback(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) {
+ struct userdata *u = userdata;
+
+ pa_assert(userdata);
+
+ switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
+ case PA_SUBSCRIPTION_EVENT_SINK_INPUT: {
+ if (u->stream) {
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE) {
+ if (pa_stream_get_index(u->stream) == idx) {
+ pa_context_get_sink_input_info(u->context, idx, context_sink_input_info_callback, u);
+ }
+ }
+ }
+ break;
+ }
+ default:
+ /* ignoring event */
+ break;
+ }
+}
+
+/* active ignoring */
+static void context_ignore_success_callback(pa_context *c, int success, void *userdata) {
+}
+
+static void context_state_callback(pa_context *c, void *userdata) {
+ struct userdata *u = userdata;
+ int c_errno;
+
+ pa_assert(u);
+
+ switch (pa_context_get_state(c)) {
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ break;
+ case PA_CONTEXT_READY: {
+ pa_proplist *proplist;
+ const char *username = pa_get_user_name_malloc();
+ const char *hostname = pa_get_host_name_malloc();
+ /* TODO: old tunnel say 'Null-Output' */
+ char *stream_name = pa_sprintf_malloc("%s for %s@%s", "Tunnel", username, hostname);
+
+ pa_log_debug("Connection successful. Creating stream.");
+ pa_assert(!u->stream);
+
+ proplist = tunnel_new_proplist(u);
+ pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, "sound");
+ pa_assert(proplist);
+
+ u->stream = pa_stream_new_with_proplist(u->context,
+ stream_name,
+ &u->sink->sample_spec,
+ &u->sink->channel_map,
+ proplist);
+ pa_proplist_free(proplist);
+ pa_xfree(stream_name);
+
+ if(!u->stream) {
+ pa_log_error("Could not create a stream.");
+ u->thread_mainloop_api->quit(u->thread_mainloop_api, TUNNEL_THREAD_FAILED_MAINLOOP);
+ return;
+ }
+
+
+ pa_context_subscribe(u->context, PA_SUBSCRIPTION_MASK_SINK_INPUT, NULL, NULL);
+
+ pa_stream_set_state_callback(u->stream, stream_state_callback, userdata);
+ if (pa_stream_connect_playback(u->stream,
+ u->remote_sink_name,
+ &u->bufferattr,
+ PA_STREAM_START_CORKED | PA_STREAM_AUTO_TIMING_UPDATE,
+ &u->volume,
+ NULL) < 0) {
+ /* failed */
+ }
+ u->connected = true;
+ break;
+ }
+ case PA_CONTEXT_FAILED:
+ c_errno = pa_context_errno(u->context);
+ pa_log_debug("Context failed with err %d.", c_errno);
+ u->connected = false;
+ u->thread_mainloop_api->quit(u->thread_mainloop_api, TUNNEL_THREAD_FAILED_MAINLOOP);
+ break;
+ case PA_CONTEXT_TERMINATED:
+ pa_log_debug("Context terminated.");
+ u->connected = false;
+ u->thread_mainloop_api->quit(u->thread_mainloop_api, TUNNEL_THREAD_FAILED_MAINLOOP);
+ break;
+ default:
+ break;
+ }
+}
+
+static void sink_get_volume_callback(pa_sink *s) {
+ struct userdata *u = s->userdata;
+
+ pa_assert(u);
+
+ if (!pa_cvolume_equal(&u->volume, &s->real_volume)) {
+ s->real_volume = u->volume;
+ pa_cvolume_set(&s->soft_volume, s->sample_spec.channels, PA_VOLUME_NORM);
+ }
+}
+
+static void sink_set_volume_callback(pa_sink *s) {
+ struct userdata *u = s->userdata;
+
+ if (!u->stream)
+ return;
+
+ u->volume = s->real_volume;
+
+ pa_context_set_sink_input_volume(u->context, pa_stream_get_index(u->stream), &u->volume, context_ignore_success_callback, NULL);
+}
+
+static void sink_update_requested_latency_cb(pa_sink *s) {
+ struct userdata *u;
+ size_t nbytes;
+ pa_usec_t block_usec;
+
+ pa_sink_assert_ref(s);
+ pa_assert_se(u = s->userdata);
+
+ block_usec = pa_sink_get_requested_latency_within_thread(s);
+
+ if (block_usec == (pa_usec_t) -1)
+ block_usec = s->thread_info.max_latency;
+
+ nbytes = pa_usec_to_bytes(block_usec, &s->sample_spec);
+ pa_sink_set_max_rewind_within_thread(s, nbytes);
+ pa_sink_set_max_request_within_thread(s, nbytes);
+
+ if (block_usec != (pa_usec_t) -1) {
+ u->bufferattr.tlength = nbytes;
+ }
+
+ if (u->stream && PA_STREAM_IS_GOOD(pa_stream_get_state(u->stream))) {
+ pa_stream_set_buffer_attr(u->stream, &u->bufferattr, NULL, NULL);
+ }
+}
+
+static void sink_write_volume_callback(pa_sink *s) {
+ struct userdata *u = s->userdata;
+ pa_cvolume hw_vol = s->thread_info.current_hw_volume;
+
+ pa_assert(u);
+}
+
+static int sink_process_msg_cb(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *chunk) {
+ struct userdata *u = PA_SINK(o)->userdata;
+
+ switch (code) {
+ case PA_SINK_MESSAGE_GET_LATENCY: {
+ int negative;
+ pa_usec_t remote_latency;
+
+ if (!PA_SINK_IS_LINKED(u->sink->thread_info.state)) {
+ *((pa_usec_t*) data) = 0;
+ return 0;
+ }
+
+ if (!u->stream) {
+ *((pa_usec_t*) data) = 0;
+ return 0;
+ }
+
+ if (!PA_STREAM_IS_GOOD(pa_stream_get_state(u->stream))) {
+ *((pa_usec_t*) data) = 0;
+ return 0;
+ }
+
+ if (pa_stream_get_latency(u->stream, &remote_latency, &negative) < 0) {
+ *((pa_usec_t*) data) = 0;
+ return 0;
+ }
+
+ *((pa_usec_t*) data) =
+ /* Add the latency from libpulse */
+ remote_latency;
+ /* do we have to add more latency here ? */
+ return 0;
+ }
+ }
+ return pa_sink_process_msg(o, code, data, offset, chunk);
+}
+
+int pa__init(pa_module *m) {
+ struct userdata *u = NULL;
+ pa_modargs *ma = NULL;
+ pa_sink_new_data sink_data;
+ pa_sample_spec ss;
+ pa_channel_map map;
+ const char *remote_server = NULL;
+ const char *sink_name = NULL;
+ char *default_sink_name = NULL;
+
+ pa_assert(m);
+
+ if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+ pa_log("Failed to parse module arguments.");
+ goto fail;
+ }
+
+ ss = m->core->default_sample_spec;
+ map = m->core->default_channel_map;
+ if (pa_modargs_get_sample_spec_and_channel_map(ma, &ss, &map, PA_CHANNEL_MAP_DEFAULT) < 0) {
+ pa_log("Invalid sample format specification or channel map");
+ goto fail;
+ }
+
+ remote_server = pa_modargs_get_value(ma, "server", NULL);
+ if (!remote_server) {
+ pa_log("No server given!");
+ goto fail;
+ }
+
+ u = pa_xnew0(struct userdata, 1);
+ u->module = m;
+ m->userdata = u;
+ u->remote_server = pa_xstrdup(remote_server);
+ u->thread_mainloop = pa_mainloop_new();
+ if (u->thread_mainloop == NULL) {
+ pa_log("Failed to create mainloop");
+ goto fail;
+ }
+ u->thread_mainloop_api = pa_mainloop_get_api(u->thread_mainloop);
+
+ u->remote_sink_name = pa_xstrdup(pa_modargs_get_value(ma, "sink", NULL));
+
+ pa_cvolume_init(&u->volume);
+ pa_cvolume_reset(&u->volume, ss.channels);
+
+ u->bufferattr.maxlength = (uint32_t) -1;
+ u->bufferattr.minreq = (uint32_t) -1;
+ u->bufferattr.prebuf = (uint32_t) -1;
+ u->bufferattr.tlength = (uint32_t) -1;
+
+ pa_thread_mq_init_thread_mainloop(&u->thread_mq, m->core->mainloop, pa_mainloop_get_api(u->thread_mainloop));
+
+ /* Create sink */
+ pa_sink_new_data_init(&sink_data);
+ sink_data.driver = __FILE__;
+ sink_data.module = m;
+
+ default_sink_name = pa_sprintf_malloc("tunnel-sink-new.%s", remote_server);
+ sink_name = pa_modargs_get_value(ma, "sink_name", default_sink_name);
+
+ pa_sink_new_data_set_name(&sink_data, sink_name);
+ pa_sink_new_data_set_sample_spec(&sink_data, &ss);
+ pa_sink_new_data_set_channel_map(&sink_data, &map);
+
+ pa_proplist_sets(sink_data.proplist, PA_PROP_DEVICE_CLASS, "sound");
+ pa_proplist_setf(sink_data.proplist,
+ PA_PROP_DEVICE_DESCRIPTION,
+ _("Tunnel to %s/%s"),
+ remote_server,
+ pa_strempty(u->remote_sink_name));
+
+ if (pa_modargs_get_proplist(ma, "sink_properties", sink_data.proplist, PA_UPDATE_REPLACE) < 0) {
+ pa_log("Invalid properties");
+ pa_sink_new_data_done(&sink_data);
+ goto fail;
+ }
+ /* TODO: check PA_SINK_LATENCY + PA_SINK_DYNAMIC_LATENCY */
+ if (!(u->sink = pa_sink_new(m->core, &sink_data, (PA_SINK_LATENCY|PA_SINK_DYNAMIC_LATENCY|PA_SINK_NETWORK)))) {
+ pa_log("Failed to create sink.");
+ pa_sink_new_data_done(&sink_data);
+ goto fail;
+ }
+
+ pa_sink_new_data_done(&sink_data);
+ u->sink->userdata = u;
+
+ /* sink callbacks */
+ u->sink->parent.process_msg = sink_process_msg_cb;
+ u->sink->update_requested_latency = sink_update_requested_latency_cb;
+
+ /* set thread queue */
+ pa_sink_set_asyncmsgq(u->sink, u->thread_mq.inq);
+
+ pa_sink_set_get_volume_callback(u->sink, sink_get_volume_callback);
+ pa_sink_set_set_volume_callback(u->sink, sink_set_volume_callback);
+ pa_sink_set_write_volume_callback(u->sink, sink_write_volume_callback);
+ /* TODO: latency / rewind
+ u->sink->update_requested_latency = sink_update_requested_latency_cb;
+ u->block_usec = BLOCK_USEC;
+ nbytes = pa_usec_to_bytes(u->block_usec, &u->sink->sample_spec);
+ pa_sink_set_max_rewind(u->sink, nbytes);
+ pa_sink_set_max_request(u->sink, nbytes);
+ pa_sink_set_latency_range(u->sink, 0, BLOCK_USEC); */
+
+ if (!(u->thread = pa_thread_new("tunnel-sink", thread_func, u))) {
+ pa_log("Failed to create thread.");
+ goto fail;
+ }
+
+ pa_sink_put(u->sink);
+ pa_modargs_free(ma);
+ pa_xfree(default_sink_name);
+
+ return 0;
+
+fail:
+ if (ma)
+ pa_modargs_free(ma);
+
+ if (default_sink_name)
+ pa_xfree(default_sink_name);
+
+ pa__done(m);
+
+ return -1;
+}
+
+void pa__done(pa_module *m) {
+ struct userdata *u;
+
+ pa_assert(m);
+
+ if (!(u = m->userdata))
+ return;
+
+ if (u->sink)
+ pa_sink_unlink(u->sink);
+
+ if (u->thread) {
+ pa_asyncmsgq_send(u->thread_mq.inq, NULL, PA_MESSAGE_SHUTDOWN, NULL, 0, NULL);
+ pa_thread_free(u->thread);
+ }
+
+ pa_thread_mq_done(&u->thread_mq);
+
+ if (u->thread_mainloop)
+ pa_mainloop_free(u->thread_mainloop);
+
+ if (u->remote_sink_name)
+ pa_xfree(u->remote_sink_name);
+
+ if (u->remote_server)
+ pa_xfree(u->remote_server);
+
+ if (u->sink)
+ pa_sink_unref(u->sink);
+
+ pa_xfree(u);
+}
--
1.8.3.4
More information about the pulseaudio-discuss
mailing list