summaryrefslogtreecommitdiffstats
path: root/src/modules/raop
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/raop')
-rw-r--r--src/modules/raop/meson.build30
-rw-r--r--src/modules/raop/module-raop-discover.c537
-rw-r--r--src/modules/raop/module-raop-sink.c112
-rw-r--r--src/modules/raop/raop-client.c1854
-rw-r--r--src/modules/raop/raop-client.h86
-rw-r--r--src/modules/raop/raop-crypto.c214
-rw-r--r--src/modules/raop/raop-crypto.h35
-rw-r--r--src/modules/raop/raop-packet-buffer.c161
-rw-r--r--src/modules/raop/raop-packet-buffer.h40
-rw-r--r--src/modules/raop/raop-sink.c968
-rw-r--r--src/modules/raop/raop-sink.h33
-rw-r--r--src/modules/raop/raop-util.c211
-rw-r--r--src/modules/raop/raop-util.h41
13 files changed, 4322 insertions, 0 deletions
diff --git a/src/modules/raop/meson.build b/src/modules/raop/meson.build
new file mode 100644
index 0000000..3df07dc
--- /dev/null
+++ b/src/modules/raop/meson.build
@@ -0,0 +1,30 @@
+libraop_sources = [
+ 'raop-client.c',
+ 'raop-crypto.c',
+ 'raop-packet-buffer.c',
+ 'raop-sink.c',
+ 'raop-util.c',
+]
+
+libraop_headers = [
+ 'raop-client.h',
+ 'raop-crypto.h',
+ 'raop-packet-buffer.h',
+ 'raop-sink.h',
+ 'raop-util.h',
+]
+
+# FIXME: meson doesn't support multiple RPATH arguments currently
+rpath_dirs = join_paths(privlibdir) + ':' + join_paths(modlibexecdir)
+
+libraop = shared_library('raop',
+ libraop_sources,
+ libraop_headers,
+ c_args : [pa_c_args, server_c_args],
+ link_args : [nodelete_link_args],
+ include_directories : [configinc, topinc],
+ dependencies : [libpulse_dep, libpulsecommon_dep, libpulsecore_dep, librtp_dep, libm_dep, openssl_dep, libintl_dep],
+ install : true,
+ install_rpath : rpath_dirs,
+ install_dir : modlibexecdir,
+)
diff --git a/src/modules/raop/module-raop-discover.c b/src/modules/raop/module-raop-discover.c
new file mode 100644
index 0000000..2bbf466
--- /dev/null
+++ b/src/modules/raop/module-raop-discover.c
@@ -0,0 +1,537 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2008 Colin Guthrie
+
+ 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, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <avahi-client/client.h>
+#include <avahi-client/lookup.h>
+#include <avahi-common/alternative.h>
+#include <avahi-common/error.h>
+#include <avahi-common/domain.h>
+#include <avahi-common/malloc.h>
+
+#include <pulse/xmalloc.h>
+
+#include <pulsecore/core-util.h>
+#include <pulsecore/log.h>
+#include <pulsecore/hashmap.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/namereg.h>
+#include <pulsecore/avahi-wrap.h>
+
+#include "raop-util.h"
+
+PA_MODULE_AUTHOR("Colin Guthrie");
+PA_MODULE_DESCRIPTION("mDNS/DNS-SD Service Discovery of RAOP devices");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(true);
+PA_MODULE_USAGE(
+ "latency_msec=<audio latency - applies to all devices> ");
+
+#define SERVICE_TYPE_SINK "_raop._tcp"
+
+struct userdata {
+ pa_core *core;
+ pa_module *module;
+
+ AvahiPoll *avahi_poll;
+ AvahiClient *client;
+ AvahiServiceBrowser *sink_browser;
+
+ pa_hashmap *tunnels;
+
+ bool latency_set;
+ uint32_t latency;
+};
+
+static const char* const valid_modargs[] = {
+ "latency_msec",
+ NULL
+};
+
+struct tunnel {
+ AvahiIfIndex interface;
+ AvahiProtocol protocol;
+ char *name, *type, *domain;
+ uint32_t module_index;
+};
+
+static unsigned tunnel_hash(const void *p) {
+ const struct tunnel *t = p;
+
+ return
+ (unsigned) t->interface +
+ (unsigned) t->protocol +
+ pa_idxset_string_hash_func(t->name) +
+ pa_idxset_string_hash_func(t->type) +
+ pa_idxset_string_hash_func(t->domain);
+}
+
+static int tunnel_compare(const void *a, const void *b) {
+ const struct tunnel *ta = a, *tb = b;
+ int r;
+
+ if (ta->interface != tb->interface)
+ return 1;
+ if (ta->protocol != tb->protocol)
+ return 1;
+ if ((r = strcmp(ta->name, tb->name)))
+ return r;
+ if ((r = strcmp(ta->type, tb->type)))
+ return r;
+ if ((r = strcmp(ta->domain, tb->domain)))
+ return r;
+
+ return 0;
+}
+
+static struct tunnel* tunnel_new(
+ AvahiIfIndex interface, AvahiProtocol protocol,
+ const char *name, const char *type, const char *domain) {
+ struct tunnel *t;
+
+ t = pa_xnew(struct tunnel, 1);
+ t->interface = interface;
+ t->protocol = protocol;
+ t->name = pa_xstrdup(name);
+ t->type = pa_xstrdup(type);
+ t->domain = pa_xstrdup(domain);
+ t->module_index = PA_IDXSET_INVALID;
+
+ return t;
+}
+
+static void tunnel_free(struct tunnel *t) {
+ pa_assert(t);
+ pa_xfree(t->name);
+ pa_xfree(t->type);
+ pa_xfree(t->domain);
+ pa_xfree(t);
+}
+
+/* This functions returns RAOP audio latency as guessed by the
+ * device model header.
+ * Feel free to complete the possible values after testing with
+ * your hardware.
+ */
+static uint32_t guess_latency_from_device(const char *model) {
+ uint32_t default_latency = RAOP_DEFAULT_LATENCY;
+
+ if (pa_streq(model, "PIONEER,1")) {
+ /* Pioneer N-30 */
+ default_latency = 2352;
+ } else if (pa_streq(model, "ShairportSync")) {
+ /* Shairport - software AirPort server */
+ default_latency = 2352;
+ }
+
+ pa_log_debug("Default latency is %u ms for device model %s.", default_latency, model);
+ return default_latency;
+}
+
+static void resolver_cb(
+ AvahiServiceResolver *r,
+ AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiResolverEvent event,
+ const char *name, const char *type, const char *domain,
+ const char *host_name, const AvahiAddress *a, uint16_t port,
+ AvahiStringList *txt,
+ AvahiLookupResultFlags flags,
+ void *userdata) {
+ struct userdata *u = userdata;
+ struct tunnel *tnl;
+ char *device = NULL, *nicename, *dname, *vname, *args;
+ char *tp = NULL, *et = NULL, *cn = NULL;
+ char *ch = NULL, *ss = NULL, *sr = NULL;
+ char *dm = NULL;
+ char *t = NULL;
+ char at[AVAHI_ADDRESS_STR_MAX];
+ AvahiStringList *l;
+ pa_module *m;
+ uint32_t latency = RAOP_DEFAULT_LATENCY;
+
+ pa_assert(u);
+
+ tnl = tunnel_new(interface, protocol, name, type, domain);
+
+ if (event != AVAHI_RESOLVER_FOUND) {
+ pa_log("Resolving of '%s' failed: %s", name, avahi_strerror(avahi_client_errno(u->client)));
+ goto finish;
+ }
+
+ if ((nicename = strstr(name, "@"))) {
+ ++nicename;
+ if (strlen(nicename) > 0) {
+ pa_log_debug("Found RAOP: %s", nicename);
+ nicename = pa_escape(nicename, "\"'");
+ } else
+ nicename = NULL;
+ }
+
+ for (l = txt; l; l = l->next) {
+ char *key, *value;
+ pa_assert_se(avahi_string_list_get_pair(l, &key, &value, NULL) == 0);
+
+ pa_log_debug("Found key: '%s' with value: '%s'", key, value);
+ if (pa_streq(key, "device")) {
+ device = value;
+ value = NULL;
+ } else if (pa_streq(key, "tp")) {
+ /* Transport protocol:
+ * - TCP = only TCP,
+ * - UDP = only UDP,
+ * - TCP,UDP = both supported (UDP should be preferred) */
+ pa_xfree(tp);
+ if (pa_str_in_list(value, ",", "UDP"))
+ tp = pa_xstrdup("UDP");
+ else if (pa_str_in_list(value, ",", "TCP"))
+ tp = pa_xstrdup("TCP");
+ else
+ tp = pa_xstrdup(value);
+ } else if (pa_streq(key, "et")) {
+ /* Supported encryption types:
+ * - 0 = none,
+ * - 1 = RSA,
+ * - 2 = FairPlay,
+ * - 3 = MFiSAP,
+ * - 4 = FairPlay SAPv2.5. */
+ pa_xfree(et);
+ if (pa_str_in_list(value, ",", "1"))
+ et = pa_xstrdup("RSA");
+ else
+ et = pa_xstrdup("none");
+ } else if (pa_streq(key, "cn")) {
+ /* Suported audio codecs:
+ * - 0 = PCM,
+ * - 1 = ALAC,
+ * - 2 = AAC,
+ * - 3 = AAC ELD. */
+ pa_xfree(cn);
+ if (pa_str_in_list(value, ",", "1"))
+ cn = pa_xstrdup("ALAC");
+ else
+ cn = pa_xstrdup("PCM");
+ } else if (pa_streq(key, "md")) {
+ /* Supported metadata types:
+ * - 0 = text,
+ * - 1 = artwork,
+ * - 2 = progress. */
+ } else if (pa_streq(key, "pw")) {
+ /* Requires password ? (true/false) */
+ } else if (pa_streq(key, "ch")) {
+ /* Number of channels */
+ pa_xfree(ch);
+ ch = pa_xstrdup(value);
+ } else if (pa_streq(key, "ss")) {
+ /* Sample size */
+ pa_xfree(ss);
+ ss = pa_xstrdup(value);
+ } else if (pa_streq(key, "sr")) {
+ /* Sample rate */
+ pa_xfree(sr);
+ sr = pa_xstrdup(value);
+ } else if (pa_streq(key, "am")) {
+ /* Device model */
+ pa_xfree(dm);
+ dm = pa_xstrdup(value);
+ }
+
+ avahi_free(key);
+ avahi_free(value);
+ }
+
+ if (device)
+ dname = pa_sprintf_malloc("raop_output.%s.%s", host_name, device);
+ else
+ dname = pa_sprintf_malloc("raop_output.%s", host_name);
+
+ if (!(vname = pa_namereg_make_valid_name(dname))) {
+ pa_log("Cannot construct valid device name from '%s'.", dname);
+ avahi_free(device);
+ pa_xfree(dname);
+ pa_xfree(tp);
+ pa_xfree(et);
+ pa_xfree(cn);
+ pa_xfree(ch);
+ pa_xfree(ss);
+ pa_xfree(sr);
+ pa_xfree(dm);
+ goto finish;
+ }
+
+ avahi_free(device);
+ pa_xfree(dname);
+
+ avahi_address_snprint(at, sizeof(at), a);
+
+ if (nicename == NULL)
+ nicename = pa_xstrdup("RAOP");
+
+ if (dm == NULL)
+ dm = pa_xstrdup(_("Unknown device model"));
+
+ latency = guess_latency_from_device(dm);
+
+ args = pa_sprintf_malloc("server=[%s]:%u "
+ "sink_name=%s "
+ "sink_properties='device.description=\"%s\" device.model=\"%s\"'",
+ at, port,
+ vname,
+ nicename,
+ dm);
+ pa_xfree(nicename);
+ pa_xfree(dm);
+
+ if (tp != NULL) {
+ t = args;
+ args = pa_sprintf_malloc("%s protocol=%s", args, tp);
+ pa_xfree(tp);
+ pa_xfree(t);
+ }
+ if (et != NULL) {
+ t = args;
+ args = pa_sprintf_malloc("%s encryption=%s", args, et);
+ pa_xfree(et);
+ pa_xfree(t);
+ }
+ if (cn != NULL) {
+ t = args;
+ args = pa_sprintf_malloc("%s codec=%s", args, cn);
+ pa_xfree(cn);
+ pa_xfree(t);
+ }
+ if (ch != NULL) {
+ t = args;
+ args = pa_sprintf_malloc("%s channels=%s", args, ch);
+ pa_xfree(ch);
+ pa_xfree(t);
+ }
+ if (ss != NULL) {
+ t = args;
+ args = pa_sprintf_malloc("%s format=%s", args, ss);
+ pa_xfree(ss);
+ pa_xfree(t);
+ }
+ if (sr != NULL) {
+ t = args;
+ args = pa_sprintf_malloc("%s rate=%s", args, sr);
+ pa_xfree(sr);
+ pa_xfree(t);
+ }
+
+ if (u->latency_set)
+ latency = u->latency;
+
+ t = args;
+ args = pa_sprintf_malloc("%s latency_msec=%u", args, latency);
+ pa_xfree(t);
+
+ pa_log_debug("Loading module-raop-sink with arguments '%s'", args);
+
+ if (pa_module_load(&m, u->core, "module-raop-sink", args) >= 0) {
+ tnl->module_index = m->index;
+ pa_hashmap_put(u->tunnels, tnl, tnl);
+ tnl = NULL;
+ }
+
+ pa_xfree(vname);
+ pa_xfree(args);
+
+finish:
+ avahi_service_resolver_free(r);
+
+ if (tnl)
+ tunnel_free(tnl);
+}
+
+static void browser_cb(
+ AvahiServiceBrowser *b,
+ AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiBrowserEvent event,
+ const char *name, const char *type, const char *domain,
+ AvahiLookupResultFlags flags,
+ void *userdata) {
+ struct userdata *u = userdata;
+ struct tunnel *t;
+
+ pa_assert(u);
+
+ if (flags & AVAHI_LOOKUP_RESULT_LOCAL)
+ return;
+
+ t = tunnel_new(interface, protocol, name, type, domain);
+
+ if (event == AVAHI_BROWSER_NEW) {
+
+ if (!pa_hashmap_get(u->tunnels, t))
+ if (!(avahi_service_resolver_new(u->client, interface, protocol, name, type, domain, AVAHI_PROTO_UNSPEC, 0, resolver_cb, u)))
+ pa_log("avahi_service_resolver_new() failed: %s", avahi_strerror(avahi_client_errno(u->client)));
+
+ /* We ignore the returned resolver object here, since the we don't
+ * need to attach any special data to it, and we can still destroy
+ * it from the callback. */
+
+ } else if (event == AVAHI_BROWSER_REMOVE) {
+ struct tunnel *t2;
+
+ if ((t2 = pa_hashmap_get(u->tunnels, t))) {
+ pa_module_unload_request_by_index(u->core, t2->module_index, true);
+ pa_hashmap_remove(u->tunnels, t2);
+ tunnel_free(t2);
+ }
+ }
+
+ tunnel_free(t);
+}
+
+static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata) {
+ struct userdata *u = userdata;
+
+ pa_assert(c);
+ pa_assert(u);
+
+ u->client = c;
+
+ switch (state) {
+ case AVAHI_CLIENT_S_REGISTERING:
+ case AVAHI_CLIENT_S_RUNNING:
+ case AVAHI_CLIENT_S_COLLISION:
+ if (!u->sink_browser) {
+ if (!(u->sink_browser = avahi_service_browser_new(
+ c,
+ AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+ SERVICE_TYPE_SINK,
+ NULL,
+ 0,
+ browser_cb, u))) {
+
+ pa_log("avahi_service_browser_new() failed: %s", avahi_strerror(avahi_client_errno(c)));
+ pa_module_unload_request(u->module, true);
+ }
+ }
+
+ break;
+
+ case AVAHI_CLIENT_FAILURE:
+ if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED) {
+ int error;
+
+ pa_log_debug("Avahi daemon disconnected.");
+
+ /* Try to reconnect. */
+ if (!(u->client = avahi_client_new(u->avahi_poll, AVAHI_CLIENT_NO_FAIL, client_callback, u, &error))) {
+ pa_log("avahi_client_new() failed: %s", avahi_strerror(error));
+ pa_module_unload_request(u->module, true);
+ }
+ }
+
+ /* Fall through. */
+
+ case AVAHI_CLIENT_CONNECTING:
+ if (u->sink_browser) {
+ avahi_service_browser_free(u->sink_browser);
+ u->sink_browser = NULL;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+}
+
+int pa__init(pa_module *m) {
+ struct userdata *u;
+ pa_modargs *ma = NULL;
+ int error;
+
+ if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+ pa_log("Failed to parse module arguments.");
+ goto fail;
+ }
+
+ m->userdata = u = pa_xnew0(struct userdata, 1);
+ u->core = m->core;
+ u->module = m;
+
+ if (pa_modargs_get_value(ma, "latency_msec", NULL) != NULL) {
+ u->latency_set = true;
+ if (pa_modargs_get_value_u32(ma, "latency_msec", &u->latency) < 0) {
+ pa_log("Failed to parse latency_msec argument.");
+ goto fail;
+ }
+ }
+
+ u->tunnels = pa_hashmap_new(tunnel_hash, tunnel_compare);
+
+ u->avahi_poll = pa_avahi_poll_new(m->core->mainloop);
+
+ if (!(u->client = avahi_client_new(u->avahi_poll, AVAHI_CLIENT_NO_FAIL, client_callback, u, &error))) {
+ pa_log("pa_avahi_client_new() failed: %s", avahi_strerror(error));
+ goto fail;
+ }
+
+ pa_modargs_free(ma);
+
+ return 0;
+
+fail:
+ pa__done(m);
+
+ if (ma)
+ pa_modargs_free(ma);
+
+ return -1;
+}
+
+void pa__done(pa_module *m) {
+ struct userdata *u;
+
+ pa_assert(m);
+
+ if (!(u = m->userdata))
+ return;
+
+ if (u->client)
+ avahi_client_free(u->client);
+
+ if (u->avahi_poll)
+ pa_avahi_poll_free(u->avahi_poll);
+
+ if (u->tunnels) {
+ struct tunnel *t;
+
+ while ((t = pa_hashmap_steal_first(u->tunnels))) {
+ pa_module_unload_request_by_index(u->core, t->module_index, true);
+ tunnel_free(t);
+ }
+
+ pa_hashmap_free(u->tunnels);
+ }
+
+ pa_xfree(u);
+}
diff --git a/src/modules/raop/module-raop-sink.c b/src/modules/raop/module-raop-sink.c
new file mode 100644
index 0000000..393341a
--- /dev/null
+++ b/src/modules/raop/module-raop-sink.c
@@ -0,0 +1,112 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2008 Colin Guthrie
+
+ 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, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pulsecore/module.h>
+#include <pulsecore/sink.h>
+#include <pulsecore/modargs.h>
+
+#include "raop-sink.h"
+
+PA_MODULE_AUTHOR("Colin Guthrie");
+PA_MODULE_DESCRIPTION("RAOP Sink");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(false);
+PA_MODULE_USAGE(
+ "name=<name of the sink, to be prefixed> "
+ "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "server=<address> "
+ "protocol=<transport protocol> "
+ "encryption=<encryption type> "
+ "codec=<audio codec> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "username=<authentication user name, default: \"iTunes\"> "
+ "password=<authentication password> "
+ "latency_msec=<audio latency>");
+
+static const char* const valid_modargs[] = {
+ "name",
+ "sink_name",
+ "sink_properties",
+ "server",
+ "protocol",
+ "encryption",
+ "codec",
+ "format",
+ "rate",
+ "channels",
+ "channel_map",
+ "username",
+ "password",
+ "latency_msec",
+ "autoreconnect",
+ NULL
+};
+
+int pa__init(pa_module *m) {
+ pa_modargs *ma = NULL;
+
+ pa_assert(m);
+
+ if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+ pa_log("Failed to parse module arguments");
+ goto fail;
+ }
+
+ if (!(m->userdata = pa_raop_sink_new(m, ma, __FILE__)))
+ goto fail;
+
+ pa_modargs_free(ma);
+
+ return 0;
+
+fail:
+
+ if (ma)
+ pa_modargs_free(ma);
+
+ pa__done(m);
+
+ return -1;
+}
+
+int pa__get_n_used(pa_module *m) {
+ pa_sink *sink;
+
+ pa_assert(m);
+ pa_assert_se(sink = m->userdata);
+
+ return pa_sink_linked_by(sink);
+}
+
+void pa__done(pa_module *m) {
+ pa_sink *sink;
+
+ pa_assert(m);
+
+ if ((sink = m->userdata))
+ pa_raop_sink_free(sink);
+}
diff --git a/src/modules/raop/raop-client.c b/src/modules/raop/raop-client.c
new file mode 100644
index 0000000..885b3f1
--- /dev/null
+++ b/src/modules/raop/raop-client.c
@@ -0,0 +1,1854 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2008 Colin Guthrie
+ Copyright 2013 Hajime Fujita
+ Copyright 2013 Martin Blanchard
+
+ 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, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <math.h>
+
+#ifdef HAVE_SYS_FILIO_H
+#include <sys/filio.h>
+#endif
+
+#include <pulse/xmalloc.h>
+#include <pulse/timeval.h>
+#include <pulse/sample.h>
+
+#include <pulsecore/core.h>
+#include <pulsecore/core-error.h>
+#include <pulsecore/core-rtclock.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/iochannel.h>
+#include <pulsecore/arpa-inet.h>
+#include <pulsecore/socket-client.h>
+#include <pulsecore/socket-util.h>
+#include <pulsecore/log.h>
+#include <pulsecore/parseaddr.h>
+#include <pulsecore/macro.h>
+#include <pulsecore/memchunk.h>
+#include <pulsecore/random.h>
+#include <pulsecore/poll.h>
+
+#include <modules/rtp/rtsp_client.h>
+
+#include "raop-client.h"
+#include "raop-packet-buffer.h"
+#include "raop-crypto.h"
+#include "raop-util.h"
+
+#define DEFAULT_RAOP_PORT 5000
+
+#define FRAMES_PER_TCP_PACKET 4096
+#define FRAMES_PER_UDP_PACKET 352
+
+#define RTX_BUFFERING_SECONDS 4
+
+#define DEFAULT_TCP_AUDIO_PORT 6000
+#define DEFAULT_UDP_AUDIO_PORT 6000
+#define DEFAULT_UDP_CONTROL_PORT 6001
+#define DEFAULT_UDP_TIMING_PORT 6002
+
+#define DEFAULT_USER_AGENT "iTunes/11.0.4 (Windows; N)"
+#define DEFAULT_USER_NAME "iTunes"
+
+#define JACK_STATUS_DISCONNECTED 0
+#define JACK_STATUS_CONNECTED 1
+#define JACK_TYPE_ANALOG 0
+#define JACK_TYPE_DIGITAL 1
+
+#define VOLUME_MAX 0.0
+#define VOLUME_DEF -30.0
+#define VOLUME_MIN -144.0
+
+#define UDP_DEFAULT_PKT_BUF_SIZE 1000
+#define APPLE_CHALLENGE_LENGTH 16
+
+struct pa_raop_client {
+ pa_core *core;
+ char *host;
+ uint16_t port;
+ pa_rtsp_client *rtsp;
+ char *sci, *sid;
+ char *password;
+ bool autoreconnect;
+
+ pa_raop_protocol_t protocol;
+ pa_raop_encryption_t encryption;
+ pa_raop_codec_t codec;
+
+ pa_raop_secret *secret;
+
+ int tcp_sfd;
+
+ int udp_sfd;
+ int udp_cfd;
+ int udp_tfd;
+
+ pa_raop_packet_buffer *pbuf;
+
+ uint16_t seq;
+ uint32_t rtptime;
+ bool is_recording;
+ uint32_t ssrc;
+
+ bool is_first_packet;
+ uint32_t sync_interval;
+ uint32_t sync_count;
+
+ uint8_t jack_type;
+ uint8_t jack_status;
+
+ pa_raop_client_state_cb_t state_callback;
+ void *state_userdata;
+};
+
+/* Audio TCP packet header [16x8] (cf. rfc4571):
+ * [0,1] Frame marker; seems always 0x2400
+ * [2,3] RTP packet size (following): 0x0000 (to be set)
+ * [4,5] RTP v2: 0x80
+ * [5] Payload type: 0x60 | Marker bit: 0x80 (always set)
+ * [6,7] Sequence number: 0x0000 (to be set)
+ * [8,11] Timestamp: 0x00000000 (to be set)
+ * [12,15] SSRC: 0x00000000 (to be set) */
+#define PAYLOAD_TCP_AUDIO_DATA 0x60
+static const uint8_t tcp_audio_header[16] = {
+ 0x24, 0x00, 0x00, 0x00,
+ 0x80, 0xe0, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+};
+
+/* Audio UDP packet header [12x8] (cf. rfc3550):
+ * [0] RTP v2: 0x80
+ * [1] Payload type: 0x60
+ * [2,3] Sequence number: 0x0000 (to be set)
+ * [4,7] Timestamp: 0x00000000 (to be set)
+ * [8,12] SSRC: 0x00000000 (to be set) */
+#define PAYLOAD_UDP_AUDIO_DATA 0x60
+static const uint8_t udp_audio_header[12] = {
+ 0x80, 0x60, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+};
+
+/* Audio retransmission UDP packet header [4x8]:
+ * [0] RTP v2: 0x80
+ * [1] Payload type: 0x56 | Marker bit: 0x80 (always set)
+ * [2] Unknown; seems always 0x01
+ * [3] Unknown; seems some random number around 0x20~0x40 */
+#define PAYLOAD_RETRANSMIT_REQUEST 0x55
+#define PAYLOAD_RETRANSMIT_REPLY 0x56
+static const uint8_t udp_audio_retrans_header[4] = {
+ 0x80, 0xd6, 0x00, 0x00
+};
+
+/* Sync packet header [8x8] (cf. rfc3550):
+ * [0] RTP v2: 0x80
+ * [1] Payload type: 0x54 | Marker bit: 0x80 (always set)
+ * [2,3] Sequence number: 0x0007
+ * [4,7] Timestamp: 0x00000000 (to be set) */
+static const uint8_t udp_sync_header[8] = {
+ 0x80, 0xd4, 0x00, 0x07,
+ 0x00, 0x00, 0x00, 0x00
+};
+
+/* Timing packet header [8x8] (cf. rfc3550):
+ * [0] RTP v2: 0x80
+ * [1] Payload type: 0x53 | Marker bit: 0x80 (always set)
+ * [2,3] Sequence number: 0x0007
+ * [4,7] Timestamp: 0x00000000 (unused) */
+#define PAYLOAD_TIMING_REQUEST 0x52
+#define PAYLOAD_TIMING_REPLY 0x53
+static const uint8_t udp_timing_header[8] = {
+ 0x80, 0xd3, 0x00, 0x07,
+ 0x00, 0x00, 0x00, 0x00
+};
+
+/**
+ * Function to trim a given character at the end of a string (no realloc).
+ * @param str Pointer to string
+ * @param rc Character to trim
+ */
+static inline void rtrim_char(char *str, char rc) {
+ char *sp = str + strlen(str) - 1;
+ while (sp >= str && *sp == rc) {
+ *sp = '\0';
+ sp -= 1;
+ }
+}
+
+/**
+ * Function to convert a timeval to ntp timestamp.
+ * @param tv Pointer to the timeval structure
+ * @return The NTP timestamp
+ */
+static inline uint64_t timeval_to_ntp(struct timeval *tv) {
+ uint64_t ntp = 0;
+
+ /* Converting micro seconds to a fraction. */
+ ntp = (uint64_t) tv->tv_usec * UINT32_MAX / PA_USEC_PER_SEC;
+ /* Moving reference from 1 Jan 1970 to 1 Jan 1900 (seconds). */
+ ntp |= (uint64_t) (tv->tv_sec + 0x83aa7e80) << 32;
+
+ return ntp;
+}
+
+/**
+ * Function to write bits into a buffer.
+ * @param buffer Handle to the buffer. It will be incremented if new data requires it.
+ * @param bit_pos A pointer to a position buffer to keep track the current write location (0 for MSB, 7 for LSB)
+ * @param size A pointer to the byte size currently written. This allows the calling function to do simple buffer overflow checks
+ * @param data The data to write
+ * @param data_bit_len The number of bits from data to write
+ */
+static inline void bit_writer(uint8_t **buffer, uint8_t *bit_pos, size_t *size, uint8_t data, uint8_t data_bit_len) {
+ int bits_left, bit_overflow;
+ uint8_t bit_data;
+
+ if (!data_bit_len)
+ return;
+
+ /* If bit pos is zero, we will definitely use at least one bit from the current byte so size increments. */
+ if (!*bit_pos)
+ *size += 1;
+
+ /* Calc the number of bits left in the current byte of buffer. */
+ bits_left = 7 - *bit_pos + 1;
+ /* Calc the overflow of bits in relation to how much space we have left... */
+ bit_overflow = bits_left - data_bit_len;
+ if (bit_overflow >= 0) {
+ /* We can fit the new data in our current byte.
+ * As we write from MSB->LSB we need to left shift by the overflow amount. */
+ bit_data = data << bit_overflow;
+ if (*bit_pos)
+ **buffer |= bit_data;
+ else
+ **buffer = bit_data;
+ /* If our data fits exactly into the current byte, we need to increment our pointer. */
+ if (0 == bit_overflow) {
+ /* Do not increment size as it will be incremented on next call as bit_pos is zero. */
+ *buffer += 1;
+ *bit_pos = 0;
+ } else {
+ *bit_pos += data_bit_len;
+ }
+ } else {
+ /* bit_overflow is negative, there for we will need a new byte from our buffer
+ * Firstly fill up what's left in the current byte. */
+ bit_data = data >> -bit_overflow;
+ **buffer |= bit_data;
+ /* Increment our buffer pointer and size counter. */
+ *buffer += 1;
+ *size += 1;
+ **buffer = data << (8 + bit_overflow);
+ *bit_pos = -bit_overflow;
+ }
+}
+
+static size_t write_ALAC_data(uint8_t *packet, const size_t max, uint8_t *raw, size_t *length, bool compress) {
+ uint32_t nbs = (*length / 2) / 2;
+ uint8_t *ibp, *maxibp;
+ uint8_t *bp, bpos;
+ size_t size = 0;
+
+ bp = packet;
+ pa_memzero(packet, max);
+ size = bpos = 0;
+
+ bit_writer(&bp, &bpos, &size, 1, 3); /* channel=1, stereo */
+ bit_writer(&bp, &bpos, &size, 0, 4); /* Unknown */
+ bit_writer(&bp, &bpos, &size, 0, 8); /* Unknown */
+ bit_writer(&bp, &bpos, &size, 0, 4); /* Unknown */
+ bit_writer(&bp, &bpos, &size, 1, 1); /* Hassize */
+ bit_writer(&bp, &bpos, &size, 0, 2); /* Unused */
+ bit_writer(&bp, &bpos, &size, 1, 1); /* Is-not-compressed */
+ /* Size of data, integer, big endian. */
+ bit_writer(&bp, &bpos, &size, (nbs >> 24) & 0xff, 8);
+ bit_writer(&bp, &bpos, &size, (nbs >> 16) & 0xff, 8);
+ bit_writer(&bp, &bpos, &size, (nbs >> 8) & 0xff, 8);
+ bit_writer(&bp, &bpos, &size, (nbs) & 0xff, 8);
+
+ ibp = raw;
+ maxibp = raw + (4 * nbs) - 4;
+ while (ibp <= maxibp) {
+ /* Byte swap stereo data. */
+ bit_writer(&bp, &bpos, &size, *(ibp + 1), 8);
+ bit_writer(&bp, &bpos, &size, *(ibp + 0), 8);
+ bit_writer(&bp, &bpos, &size, *(ibp + 3), 8);
+ bit_writer(&bp, &bpos, &size, *(ibp + 2), 8);
+ ibp += 4;
+ }
+
+ *length = (ibp - raw);
+ return size;
+}
+
+static size_t build_tcp_audio_packet(pa_raop_client *c, pa_memchunk *block, pa_memchunk *packet) {
+ const size_t head = sizeof(tcp_audio_header);
+ uint32_t *buffer = NULL;
+ uint8_t *raw = NULL;
+ size_t length, size;
+
+ raw = pa_memblock_acquire(block->memblock);
+ buffer = pa_memblock_acquire(packet->memblock);
+ buffer += packet->index / sizeof(uint32_t);
+ raw += block->index;
+
+ /* Wrap sequence number to 0 then UINT16_MAX is reached */
+ if (c->seq == UINT16_MAX)
+ c->seq = 0;
+ else
+ c->seq++;
+
+ memcpy(buffer, tcp_audio_header, sizeof(tcp_audio_header));
+ buffer[1] |= htonl((uint32_t) c->seq);
+ buffer[2] = htonl(c->rtptime);
+ buffer[3] = htonl(c->ssrc);
+
+ length = block->length;
+ size = sizeof(tcp_audio_header);
+ if (c->codec == PA_RAOP_CODEC_ALAC)
+ size += write_ALAC_data(((uint8_t *) buffer + head), packet->length - head, raw, &length, false);
+ else {
+ pa_log_debug("Only ALAC encoding is supported, sending zeros...");
+ pa_memzero(((uint8_t *) buffer + head), packet->length - head);
+ size += length;
+ }
+
+ c->rtptime += length / 4;
+
+ pa_memblock_release(block->memblock);
+
+ buffer[0] |= htonl((uint32_t) size - 4);
+ if (c->encryption == PA_RAOP_ENCRYPTION_RSA)
+ pa_raop_aes_encrypt(c->secret, (uint8_t *) buffer + head, size - head);
+
+ pa_memblock_release(packet->memblock);
+ packet->length = size;
+
+ return size;
+}
+
+static ssize_t send_tcp_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) {
+ static int write_type = 0;
+ const size_t max = sizeof(tcp_audio_header) + 8 + 16384;
+ pa_memchunk *packet = NULL;
+ uint8_t *buffer = NULL;
+ double progress = 0.0;
+ ssize_t written = -1;
+ size_t done = 0;
+
+ packet = pa_raop_packet_buffer_retrieve(c->pbuf, c->seq);
+
+ if (!packet || (packet && packet->length <= 0)) {
+ pa_assert(block->index == offset);
+
+ if (!(packet = pa_raop_packet_buffer_prepare(c->pbuf, c->seq, max)))
+ return -1;
+
+ packet->index = 0;
+ packet->length = max;
+ if (!build_tcp_audio_packet(c, block, packet))
+ return -1;
+ }
+
+ buffer = pa_memblock_acquire(packet->memblock);
+
+ pa_assert(buffer);
+
+ buffer += packet->index;
+ if (buffer && packet->length > 0)
+ written = pa_write(c->tcp_sfd, buffer, packet->length, &write_type);
+ if (written > 0) {
+ progress = (double) written / (double) packet->length;
+ packet->length -= written;
+ packet->index += written;
+
+ done = block->length * progress;
+ block->length -= done;
+ block->index += done;
+ }
+
+ pa_memblock_release(packet->memblock);
+
+ return written;
+}
+
+static size_t build_udp_audio_packet(pa_raop_client *c, pa_memchunk *block, pa_memchunk *packet) {
+ const size_t head = sizeof(udp_audio_header);
+ uint32_t *buffer = NULL;
+ uint8_t *raw = NULL;
+ size_t length, size;
+
+ raw = pa_memblock_acquire(block->memblock);
+ buffer = pa_memblock_acquire(packet->memblock);
+ buffer += packet->index / sizeof(uint32_t);
+ raw += block->index;
+
+ memcpy(buffer, udp_audio_header, sizeof(udp_audio_header));
+ if (c->is_first_packet)
+ buffer[0] |= htonl((uint32_t) 0x80 << 16);
+ buffer[0] |= htonl((uint32_t) c->seq);
+ buffer[1] = htonl(c->rtptime);
+ buffer[2] = htonl(c->ssrc);
+
+ length = block->length;
+ size = sizeof(udp_audio_header);
+ if (c->codec == PA_RAOP_CODEC_ALAC)
+ size += write_ALAC_data(((uint8_t *) buffer + head), packet->length - head, raw, &length, false);
+ else {
+ pa_log_debug("Only ALAC encoding is supported, sending zeros...");
+ pa_memzero(((uint8_t *) buffer + head), packet->length - head);
+ size += length;
+ }
+
+ c->rtptime += length / 4;
+
+ /* Wrap sequence number to 0 then UINT16_MAX is reached */
+ if (c->seq == UINT16_MAX)
+ c->seq = 0;
+ else
+ c->seq++;
+
+ pa_memblock_release(block->memblock);
+
+ if (c->encryption == PA_RAOP_ENCRYPTION_RSA)
+ pa_raop_aes_encrypt(c->secret, (uint8_t *) buffer + head, size - head);
+
+ pa_memblock_release(packet->memblock);
+ packet->length = size;
+
+ return size;
+}
+
+static ssize_t send_udp_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) {
+ const size_t max = sizeof(udp_audio_retrans_header) + sizeof(udp_audio_header) + 8 + 1408;
+ pa_memchunk *packet = NULL;
+ uint8_t *buffer = NULL;
+ ssize_t written = -1;
+
+ /* UDP packet has to be sent at once ! */
+ pa_assert(block->index == offset);
+
+ if (!(packet = pa_raop_packet_buffer_prepare(c->pbuf, c->seq, max)))
+ return -1;
+
+ packet->index = sizeof(udp_audio_retrans_header);
+ packet->length = max - sizeof(udp_audio_retrans_header);
+ if (!build_udp_audio_packet(c, block, packet))
+ return -1;
+
+ buffer = pa_memblock_acquire(packet->memblock);
+
+ pa_assert(buffer);
+
+ buffer += packet->index;
+ if (buffer && packet->length > 0)
+ written = pa_write(c->udp_sfd, buffer, packet->length, NULL);
+ if (written < 0 && errno == EAGAIN) {
+ pa_log_debug("Discarding UDP (audio, seq=%d) packet due to EAGAIN (%s)", c->seq, pa_cstrerror(errno));
+ written = packet->length;
+ }
+
+ pa_memblock_release(packet->memblock);
+ /* It is meaningless to preseve the partial data */
+ block->index += block->length;
+ block->length = 0;
+
+ return written;
+}
+
+static size_t rebuild_udp_audio_packet(pa_raop_client *c, uint16_t seq, pa_memchunk *packet) {
+ size_t size = sizeof(udp_audio_retrans_header);
+ uint32_t *buffer = NULL;
+
+ buffer = pa_memblock_acquire(packet->memblock);
+
+ memcpy(buffer, udp_audio_retrans_header, sizeof(udp_audio_retrans_header));
+ buffer[0] |= htonl((uint32_t) seq);
+ size += packet->length;
+
+ pa_memblock_release(packet->memblock);
+ packet->length += sizeof(udp_audio_retrans_header);
+ packet->index -= sizeof(udp_audio_retrans_header);
+
+ return size;
+}
+
+static ssize_t resend_udp_audio_packets(pa_raop_client *c, uint16_t seq, uint16_t nbp) {
+ ssize_t total = 0;
+ int i = 0;
+
+ for (i = 0; i < nbp; i++) {
+ pa_memchunk *packet = NULL;
+ uint8_t *buffer = NULL;
+ ssize_t written = -1;
+
+ if (!(packet = pa_raop_packet_buffer_retrieve(c->pbuf, seq + i)))
+ continue;
+
+ if (packet->index > 0) {
+ if (!rebuild_udp_audio_packet(c, seq + i, packet))
+ continue;
+ }
+
+ pa_assert(packet->index == 0);
+
+ buffer = pa_memblock_acquire(packet->memblock);
+
+ pa_assert(buffer);
+
+ if (buffer && packet->length > 0)
+ written = pa_write(c->udp_cfd, buffer, packet->length, NULL);
+ if (written < 0 && errno == EAGAIN) {
+ pa_log_debug("Discarding UDP (audio-retransmitted, seq=%d) packet due to EAGAIN", seq + i);
+ pa_memblock_release(packet->memblock);
+ continue;
+ }
+
+ pa_memblock_release(packet->memblock);
+ total += written;
+ }
+
+ return total;
+}
+
+/* Caller has to free the allocated memory region for packet */
+static size_t build_udp_sync_packet(pa_raop_client *c, uint32_t stamp, uint32_t **packet) {
+ const size_t size = sizeof(udp_sync_header) + 12;
+ const uint32_t delay = 88200;
+ uint32_t *buffer = NULL;
+ uint64_t transmitted = 0;
+ struct timeval tv;
+
+ *packet = NULL;
+ if (!(buffer = pa_xmalloc0(size)))
+ return 0;
+
+ memcpy(buffer, udp_sync_header, sizeof(udp_sync_header));
+ if (c->is_first_packet)
+ buffer[0] |= 0x10;
+ stamp -= delay;
+ buffer[1] = htonl(stamp);
+ /* Set the transmitted timestamp to current time. */
+ transmitted = timeval_to_ntp(pa_rtclock_get(&tv));
+ buffer[2] = htonl(transmitted >> 32);
+ buffer[3] = htonl(transmitted & 0xffffffff);
+ stamp += delay;
+ buffer[4] = htonl(stamp);
+
+ *packet = buffer;
+ return size;
+}
+
+static ssize_t send_udp_sync_packet(pa_raop_client *c, uint32_t stamp) {
+ uint32_t * packet = NULL;
+ ssize_t written = 0;
+ size_t size = 0;
+
+ size = build_udp_sync_packet(c, stamp, &packet);
+ if (packet != NULL && size > 0) {
+ written = pa_loop_write(c->udp_cfd, packet, size, NULL);
+ pa_xfree(packet);
+ }
+
+ return written;
+}
+
+static size_t handle_udp_control_packet(pa_raop_client *c, const uint8_t packet[], ssize_t size) {
+ uint8_t payload = 0;
+ uint16_t seq, nbp = 0;
+ ssize_t written = 0;
+
+ /* Control packets are 8 bytes long: */
+ if (size != 8 || packet[0] != 0x80)
+ return 1;
+
+ seq = ntohs((uint16_t) (packet[4] | packet[5] << 8));
+ nbp = ntohs((uint16_t) (packet[6] | packet[7] << 8));
+ if (nbp <= 0)
+ return 1;
+
+ /* The marker bit is always set (see rfc3550 for packet structure) ! */
+ payload = packet[1] ^ 0x80;
+ switch (payload) {
+ case PAYLOAD_RETRANSMIT_REQUEST:
+ pa_log_debug("Resending %u packets starting at %u", nbp, seq);
+ written = resend_udp_audio_packets(c, seq, nbp);
+ break;
+ case PAYLOAD_RETRANSMIT_REPLY:
+ default:
+ pa_log_debug("Got an unexpected payload type on control channel (%u) !", payload);
+ break;
+ }
+
+ return written;
+}
+
+/* Caller has to free the allocated memory region for packet */
+static size_t build_udp_timing_packet(pa_raop_client *c, const uint32_t data[6], uint64_t received, uint32_t **packet) {
+ const size_t size = sizeof(udp_timing_header) + 24;
+ uint32_t *buffer = NULL;
+ uint64_t transmitted = 0;
+ struct timeval tv;
+
+ *packet = NULL;
+ if (!(buffer = pa_xmalloc0(size)))
+ return 0;
+
+ memcpy(buffer, udp_timing_header, sizeof(udp_timing_header));
+ /* Copying originate timestamp from the incoming request packet. */
+ buffer[2] = data[4];
+ buffer[3] = data[5];
+ /* Set the receive timestamp to reception time. */
+ buffer[4] = htonl(received >> 32);
+ buffer[5] = htonl(received & 0xffffffff);
+ /* Set the transmit timestamp to current time. */
+ transmitted = timeval_to_ntp(pa_rtclock_get(&tv));
+ buffer[6] = htonl(transmitted >> 32);
+ buffer[7] = htonl(transmitted & 0xffffffff);
+
+ *packet = buffer;
+ return size;
+}
+
+static ssize_t send_udp_timing_packet(pa_raop_client *c, const uint32_t data[6], uint64_t received) {
+ uint32_t * packet = NULL;
+ ssize_t written = 0;
+ size_t size = 0;
+
+ size = build_udp_timing_packet(c, data, received, &packet);
+ if (packet != NULL && size > 0) {
+ written = pa_loop_write(c->udp_tfd, packet, size, NULL);
+ pa_xfree(packet);
+ }
+
+ return written;
+}
+
+static size_t handle_udp_timing_packet(pa_raop_client *c, const uint8_t packet[], ssize_t size) {
+ const uint32_t * data = NULL;
+ uint8_t payload = 0;
+ struct timeval tv;
+ size_t written = 0;
+ uint64_t rci = 0;
+
+ /* Timing packets are 32 bytes long: 1 x 8 RTP header (no ssrc) + 3 x 8 NTP timestamps */
+ if (size != 32 || packet[0] != 0x80)
+ return 0;
+
+ rci = timeval_to_ntp(pa_rtclock_get(&tv));
+ data = (uint32_t *) (packet + sizeof(udp_timing_header));
+
+ /* The marker bit is always set (see rfc3550 for packet structure) ! */
+ payload = packet[1] ^ 0x80;
+ switch (payload) {
+ case PAYLOAD_TIMING_REQUEST:
+ pa_log_debug("Sending timing packet at %" PRIu64 , rci);
+ written = send_udp_timing_packet(c, data, rci);
+ break;
+ case PAYLOAD_TIMING_REPLY:
+ default:
+ pa_log_debug("Got an unexpected payload type on timing channel (%u) !", payload);
+ break;
+ }
+
+ return written;
+}
+
+static void send_initial_udp_timing_packet(pa_raop_client *c) {
+ uint32_t data[6] = { 0 };
+ struct timeval tv;
+ uint64_t initial_time = 0;
+
+ initial_time = timeval_to_ntp(pa_rtclock_get(&tv));
+ data[4] = htonl(initial_time >> 32);
+ data[5] = htonl(initial_time & 0xffffffff);
+
+ send_udp_timing_packet(c, data, initial_time);
+}
+
+static int connect_udp_socket(pa_raop_client *c, int fd, uint16_t port) {
+ struct sockaddr_in sa4;
+#ifdef HAVE_IPV6
+ struct sockaddr_in6 sa6;
+#endif
+ struct sockaddr *sa;
+ socklen_t salen;
+ sa_family_t af;
+
+ pa_zero(sa4);
+#ifdef HAVE_IPV6
+ pa_zero(sa6);
+#endif
+ if (inet_pton(AF_INET, c->host, &sa4.sin_addr) > 0) {
+ sa4.sin_family = af = AF_INET;
+ sa4.sin_port = htons(port);
+ sa = (struct sockaddr *) &sa4;
+ salen = sizeof(sa4);
+#ifdef HAVE_IPV6
+ } else if (inet_pton(AF_INET6, c->host, &sa6.sin6_addr) > 0) {
+ sa6.sin6_family = af = AF_INET6;
+ sa6.sin6_port = htons(port);
+ sa = (struct sockaddr *) &sa6;
+ salen = sizeof(sa6);
+#endif
+ } else {
+ pa_log("Invalid destination '%s'", c->host);
+ goto fail;
+ }
+
+ if (fd < 0 && (fd = pa_socket_cloexec(af, SOCK_DGRAM, 0)) < 0) {
+ pa_log("socket() failed: %s", pa_cstrerror(errno));
+ goto fail;
+ }
+
+ /* If the socket queue is full, let's drop packets */
+ pa_make_udp_socket_low_delay(fd);
+ pa_make_fd_nonblock(fd);
+
+ if (connect(fd, sa, salen) < 0) {
+ pa_log("connect() failed: %s", pa_cstrerror(errno));
+ goto fail;
+ }
+
+ pa_log_debug("Connected to %s on port %d (SOCK_DGRAM)", c->host, port);
+ return fd;
+
+fail:
+ if (fd >= 0)
+ pa_close(fd);
+
+ return -1;
+}
+
+static int open_bind_udp_socket(pa_raop_client *c, uint16_t *actual_port) {
+ int fd = -1;
+ uint16_t port;
+ struct sockaddr_in sa4;
+#ifdef HAVE_IPV6
+ struct sockaddr_in6 sa6;
+#endif
+ struct sockaddr *sa;
+ uint16_t *sa_port;
+ socklen_t salen;
+ sa_family_t af;
+ int one = 1;
+
+ pa_assert(actual_port);
+
+ port = *actual_port;
+
+ pa_zero(sa4);
+#ifdef HAVE_IPV6
+ pa_zero(sa6);
+#endif
+ if (inet_pton(AF_INET, pa_rtsp_localip(c->rtsp), &sa4.sin_addr) > 0) {
+ sa4.sin_family = af = AF_INET;
+ sa4.sin_port = htons(port);
+ sa4.sin_addr.s_addr = INADDR_ANY;
+ sa = (struct sockaddr *) &sa4;
+ salen = sizeof(sa4);
+ sa_port = &sa4.sin_port;
+#ifdef HAVE_IPV6
+ } else if (inet_pton(AF_INET6, pa_rtsp_localip(c->rtsp), &sa6.sin6_addr) > 0) {
+ sa6.sin6_family = af = AF_INET6;
+ sa6.sin6_port = htons(port);
+ sa6.sin6_addr = in6addr_any;
+ sa = (struct sockaddr *) &sa6;
+ salen = sizeof(sa6);
+ sa_port = &sa6.sin6_port;
+#endif
+ } else {
+ pa_log("Could not determine which address family to use");
+ goto fail;
+ }
+
+ if ((fd = pa_socket_cloexec(af, SOCK_DGRAM, 0)) < 0) {
+ pa_log("socket() failed: %s", pa_cstrerror(errno));
+ goto fail;
+ }
+
+#ifdef SO_TIMESTAMP
+ if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &one, sizeof(one)) < 0) {
+ pa_log("setsockopt(SO_TIMESTAMP) failed: %s", pa_cstrerror(errno));
+ goto fail;
+ }
+#else
+ pa_log("SO_TIMESTAMP unsupported on this platform");
+ goto fail;
+#endif
+
+ one = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) {
+ pa_log("setsockopt(SO_REUSEADDR) failed: %s", pa_cstrerror(errno));
+ goto fail;
+ }
+
+ do {
+ int ret;
+
+ *sa_port = htons(port);
+ ret = bind(fd, sa, salen);
+ if (!ret)
+ break;
+ if (ret < 0 && errno != EADDRINUSE) {
+ pa_log("bind() failed: %s", pa_cstrerror(errno));
+ goto fail;
+ }
+ } while (++port > 0);
+
+ if (!port) {
+ pa_log("Could not bind port");
+ goto fail;
+ }
+
+ pa_log_debug("Socket bound to port %d (SOCK_DGRAM)", port);
+ *actual_port = port;
+
+ return fd;
+
+fail:
+ if (fd >= 0)
+ pa_close(fd);
+
+ return -1;
+}
+
+static void tcp_connection_cb(pa_socket_client *sc, pa_iochannel *io, void *userdata) {
+ pa_raop_client *c = userdata;
+
+ pa_assert(sc);
+ pa_assert(c);
+
+ pa_socket_client_unref(sc);
+
+ if (!io) {
+ pa_log("Connection failed: %s", pa_cstrerror(errno));
+ return;
+ }
+
+ c->tcp_sfd = pa_iochannel_get_send_fd(io);
+ pa_iochannel_set_noclose(io, true);
+ pa_make_tcp_socket_low_delay(c->tcp_sfd);
+
+ pa_iochannel_free(io);
+
+ pa_log_debug("Connection established (TCP)");
+
+ if (c->state_callback)
+ c->state_callback(PA_RAOP_CONNECTED, c->state_userdata);
+}
+
+static void rtsp_stream_cb(pa_rtsp_client *rtsp, pa_rtsp_state_t state, pa_rtsp_status_t status, pa_headerlist *headers, void *userdata) {
+ pa_raop_client *c = userdata;
+
+ pa_assert(c);
+ pa_assert(rtsp);
+ pa_assert(rtsp == c->rtsp);
+
+ switch (state) {
+ case STATE_CONNECT: {
+ char *key, *iv, *sdp = NULL;
+ int frames = 0;
+ const char *ip;
+ char *url;
+ int ipv;
+
+ pa_log_debug("RAOP: CONNECTED");
+
+ ip = pa_rtsp_localip(c->rtsp);
+ if (pa_is_ip6_address(ip)) {
+ ipv = 6;
+ url = pa_sprintf_malloc("rtsp://[%s]/%s", ip, c->sid);
+ } else {
+ ipv = 4;
+ url = pa_sprintf_malloc("rtsp://%s/%s", ip, c->sid);
+ }
+ pa_rtsp_set_url(c->rtsp, url);
+
+ if (c->protocol == PA_RAOP_PROTOCOL_TCP)
+ frames = FRAMES_PER_TCP_PACKET;
+ else if (c->protocol == PA_RAOP_PROTOCOL_UDP)
+ frames = FRAMES_PER_UDP_PACKET;
+
+ switch(c->encryption) {
+ case PA_RAOP_ENCRYPTION_NONE: {
+ sdp = pa_sprintf_malloc(
+ "v=0\r\n"
+ "o=iTunes %s 0 IN IP%d %s\r\n"
+ "s=iTunes\r\n"
+ "c=IN IP%d %s\r\n"
+ "t=0 0\r\n"
+ "m=audio 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 AppleLossless\r\n"
+ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n",
+ c->sid, ipv, ip, ipv, c->host, frames);
+
+ break;
+ }
+
+ case PA_RAOP_ENCRYPTION_RSA:
+ case PA_RAOP_ENCRYPTION_FAIRPLAY:
+ case PA_RAOP_ENCRYPTION_MFISAP:
+ case PA_RAOP_ENCRYPTION_FAIRPLAY_SAP25: {
+ key = pa_raop_secret_get_key(c->secret);
+ if (!key) {
+ pa_log("pa_raop_secret_get_key() failed.");
+ pa_rtsp_disconnect(rtsp);
+ /* FIXME: This is an unrecoverable failure. We should notify
+ * the pa_raop_client owner so that it could shut itself
+ * down. */
+ goto connect_finish;
+ }
+
+ iv = pa_raop_secret_get_iv(c->secret);
+
+ sdp = pa_sprintf_malloc(
+ "v=0\r\n"
+ "o=iTunes %s 0 IN IP%d %s\r\n"
+ "s=iTunes\r\n"
+ "c=IN IP%d %s\r\n"
+ "t=0 0\r\n"
+ "m=audio 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 AppleLossless\r\n"
+ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
+ "a=rsaaeskey:%s\r\n"
+ "a=aesiv:%s\r\n",
+ c->sid, ipv, ip, ipv, c->host, frames, key, iv);
+
+ pa_xfree(key);
+ pa_xfree(iv);
+ break;
+ }
+ }
+
+ pa_rtsp_announce(c->rtsp, sdp);
+
+connect_finish:
+ pa_xfree(sdp);
+ pa_xfree(url);
+ break;
+ }
+
+ case STATE_OPTIONS: {
+ pa_log_debug("RAOP: OPTIONS (stream cb)");
+
+ break;
+ }
+
+ case STATE_ANNOUNCE: {
+ uint16_t cport = DEFAULT_UDP_CONTROL_PORT;
+ uint16_t tport = DEFAULT_UDP_TIMING_PORT;
+ char *trs = NULL;
+
+ pa_log_debug("RAOP: ANNOUNCE");
+
+ if (c->protocol == PA_RAOP_PROTOCOL_TCP) {
+ trs = pa_sprintf_malloc(
+ "RTP/AVP/TCP;unicast;interleaved=0-1;mode=record");
+ } else if (c->protocol == PA_RAOP_PROTOCOL_UDP) {
+ c->udp_cfd = open_bind_udp_socket(c, &cport);
+ c->udp_tfd = open_bind_udp_socket(c, &tport);
+ if (c->udp_cfd < 0 || c->udp_tfd < 0)
+ goto annonce_error;
+
+ trs = pa_sprintf_malloc(
+ "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;"
+ "control_port=%d;timing_port=%d",
+ cport, tport);
+ }
+
+ pa_rtsp_setup(c->rtsp, trs);
+
+ pa_xfree(trs);
+ break;
+
+ annonce_error:
+ if (c->udp_cfd >= 0)
+ pa_close(c->udp_cfd);
+ c->udp_cfd = -1;
+ if (c->udp_tfd >= 0)
+ pa_close(c->udp_tfd);
+ c->udp_tfd = -1;
+
+ pa_rtsp_client_free(c->rtsp);
+
+ pa_log_error("Aborting RTSP announce, failed creating required sockets");
+
+ c->rtsp = NULL;
+ pa_xfree(trs);
+ break;
+ }
+
+ case STATE_SETUP: {
+ pa_socket_client *sc = NULL;
+ uint32_t sport = DEFAULT_UDP_AUDIO_PORT;
+ uint32_t cport =0, tport = 0;
+ char *ajs, *token, *pc, *trs;
+ const char *token_state = NULL;
+ char delimiters[] = ";";
+
+ pa_log_debug("RAOP: SETUP");
+
+ ajs = pa_xstrdup(pa_headerlist_gets(headers, "Audio-Jack-Status"));
+
+ if (ajs) {
+ c->jack_type = JACK_TYPE_ANALOG;
+ c->jack_status = JACK_STATUS_DISCONNECTED;
+
+ while ((token = pa_split(ajs, delimiters, &token_state))) {
+ if ((pc = strstr(token, "="))) {
+ *pc = 0;
+ if (pa_streq(token, "type") && pa_streq(pc + 1, "digital"))
+ c->jack_type = JACK_TYPE_DIGITAL;
+ } else {
+ if (pa_streq(token, "connected"))
+ c->jack_status = JACK_STATUS_CONNECTED;
+ }
+ pa_xfree(token);
+ }
+
+ } else {
+ pa_log_warn("\"Audio-Jack-Status\" missing in RTSP setup response");
+ }
+
+ sport = pa_rtsp_serverport(c->rtsp);
+ if (sport <= 0)
+ goto setup_error;
+
+ token_state = NULL;
+ if (c->protocol == PA_RAOP_PROTOCOL_TCP) {
+ if (!(sc = pa_socket_client_new_string(c->core->mainloop, true, c->host, sport)))
+ goto setup_error;
+
+ pa_socket_client_ref(sc);
+ pa_socket_client_set_callback(sc, tcp_connection_cb, c);
+
+ pa_socket_client_unref(sc);
+ sc = NULL;
+ } else if (c->protocol == PA_RAOP_PROTOCOL_UDP) {
+ trs = pa_xstrdup(pa_headerlist_gets(headers, "Transport"));
+
+ if (trs) {
+ /* Now parse out the server port component of the response. */
+ while ((token = pa_split(trs, delimiters, &token_state))) {
+ if ((pc = strstr(token, "="))) {
+ *pc = 0;
+ if (pa_streq(token, "control_port")) {
+ if (pa_atou(pc + 1, &cport) < 0)
+ goto setup_error_parse;
+ }
+ if (pa_streq(token, "timing_port")) {
+ if (pa_atou(pc + 1, &tport) < 0)
+ goto setup_error_parse;
+ }
+ *pc = '=';
+ }
+ pa_xfree(token);
+ }
+ pa_xfree(trs);
+ } else {
+ pa_log_warn("\"Transport\" missing in RTSP setup response");
+ }
+
+ if (cport <= 0 || tport <= 0)
+ goto setup_error;
+
+ if ((c->udp_sfd = connect_udp_socket(c, -1, sport)) <= 0)
+ goto setup_error;
+ if ((c->udp_cfd = connect_udp_socket(c, c->udp_cfd, cport)) <= 0)
+ goto setup_error;
+ if ((c->udp_tfd = connect_udp_socket(c, c->udp_tfd, tport)) <= 0)
+ goto setup_error;
+
+ pa_log_debug("Connection established (UDP;control_port=%d;timing_port=%d)", cport, tport);
+
+ /* Send an initial UDP packet so a connection tracking firewall
+ * knows the src_ip:src_port <-> dest_ip:dest_port relation
+ * and accepts the incoming timing packets.
+ */
+ send_initial_udp_timing_packet(c);
+ pa_log_debug("Sent initial timing packet to UDP port %d", tport);
+
+ if (c->state_callback)
+ c->state_callback(PA_RAOP_CONNECTED, c->state_userdata);
+ }
+
+ pa_rtsp_record(c->rtsp, &c->seq, &c->rtptime);
+
+ pa_xfree(ajs);
+ break;
+
+ setup_error_parse:
+ pa_log("Failed parsing server port components");
+ pa_xfree(token);
+ pa_xfree(trs);
+ /* fall-thru */
+ setup_error:
+ if (c->tcp_sfd >= 0)
+ pa_close(c->tcp_sfd);
+ c->tcp_sfd = -1;
+
+ if (c->udp_sfd >= 0)
+ pa_close(c->udp_sfd);
+ c->udp_sfd = -1;
+
+ c->udp_cfd = c->udp_tfd = -1;
+
+ pa_rtsp_client_free(c->rtsp);
+
+ pa_log_error("aborting RTSP setup, failed creating required sockets");
+
+ if (c->state_callback)
+ c->state_callback(PA_RAOP_DISCONNECTED, c->state_userdata);
+
+ c->rtsp = NULL;
+ break;
+ }
+
+ case STATE_RECORD: {
+ int32_t latency = 0;
+ uint32_t ssrc;
+ char *alt;
+
+ pa_log_debug("RAOP: RECORD");
+
+ alt = pa_xstrdup(pa_headerlist_gets(headers, "Audio-Latency"));
+ if (alt) {
+ if (pa_atoi(alt, &latency) < 0)
+ pa_log("Failed to parse audio latency");
+ }
+
+ pa_raop_packet_buffer_reset(c->pbuf, c->seq);
+
+ pa_random(&ssrc, sizeof(ssrc));
+ c->is_first_packet = true;
+ c->is_recording = true;
+ c->sync_count = 0;
+ c->ssrc = ssrc;
+
+ if (c->state_callback)
+ c->state_callback((int) PA_RAOP_RECORDING, c->state_userdata);
+
+ pa_xfree(alt);
+ break;
+ }
+
+ case STATE_SET_PARAMETER: {
+ pa_log_debug("RAOP: SET_PARAMETER");
+
+ break;
+ }
+
+ case STATE_FLUSH: {
+ pa_log_debug("RAOP: FLUSHED");
+
+ break;
+ }
+
+ case STATE_TEARDOWN: {
+ pa_log_debug("RAOP: TEARDOWN");
+
+ if (c->tcp_sfd >= 0)
+ pa_close(c->tcp_sfd);
+ c->tcp_sfd = -1;
+
+ if (c->udp_sfd >= 0)
+ pa_close(c->udp_sfd);
+ c->udp_sfd = -1;
+
+ /* Polling sockets will be closed by sink */
+ c->udp_cfd = c->udp_tfd = -1;
+ c->tcp_sfd = -1;
+
+ pa_rtsp_client_free(c->rtsp);
+ pa_xfree(c->sid);
+ c->rtsp = NULL;
+ c->sid = NULL;
+
+ if (c->state_callback)
+ c->state_callback(PA_RAOP_DISCONNECTED, c->state_userdata);
+
+ break;
+ }
+
+ case STATE_DISCONNECTED: {
+ pa_log_debug("RAOP: DISCONNECTED");
+
+ c->is_recording = false;
+
+ if (c->tcp_sfd >= 0)
+ pa_close(c->tcp_sfd);
+ c->tcp_sfd = -1;
+
+ if (c->udp_sfd >= 0)
+ pa_close(c->udp_sfd);
+ c->udp_sfd = -1;
+
+ /* Polling sockets will be closed by sink */
+ c->udp_cfd = c->udp_tfd = -1;
+ c->tcp_sfd = -1;
+
+ pa_log_error("RTSP control channel closed (disconnected)");
+
+ pa_rtsp_client_free(c->rtsp);
+ pa_xfree(c->sid);
+ c->rtsp = NULL;
+ c->sid = NULL;
+
+ if (c->state_callback)
+ c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);
+
+ break;
+ }
+ }
+}
+
+static void rtsp_auth_cb(pa_rtsp_client *rtsp, pa_rtsp_state_t state, pa_rtsp_status_t status, pa_headerlist *headers, void *userdata) {
+ pa_raop_client *c = userdata;
+
+ pa_assert(c);
+ pa_assert(rtsp);
+ pa_assert(rtsp == c->rtsp);
+
+ switch (state) {
+ case STATE_CONNECT: {
+ char *sci = NULL, *sac = NULL;
+ uint8_t rac[APPLE_CHALLENGE_LENGTH];
+ struct {
+ uint32_t ci1;
+ uint32_t ci2;
+ } rci;
+
+ pa_random(&rci, sizeof(rci));
+ /* Generate a random Client-Instance number */
+ sci = pa_sprintf_malloc("%08x%08x",rci.ci1, rci.ci2);
+ pa_rtsp_add_header(c->rtsp, "Client-Instance", sci);
+
+ pa_random(rac, APPLE_CHALLENGE_LENGTH);
+ /* Generate a random Apple-Challenge key */
+ pa_raop_base64_encode(rac, APPLE_CHALLENGE_LENGTH, &sac);
+ rtrim_char(sac, '=');
+ pa_rtsp_add_header(c->rtsp, "Apple-Challenge", sac);
+
+ pa_rtsp_options(c->rtsp);
+
+ pa_xfree(sac);
+ pa_xfree(sci);
+ break;
+ }
+
+ case STATE_OPTIONS: {
+ static bool waiting = false;
+ const char *current = NULL;
+ char space[] = " ";
+ char *token, *ath = NULL;
+ char *publ, *wath, *mth = NULL, *val;
+ char *realm = NULL, *nonce = NULL, *response = NULL;
+ char comma[] = ",";
+
+ pa_log_debug("RAOP: OPTIONS (auth cb)");
+ /* We do not consider the Apple-Response */
+ pa_rtsp_remove_header(c->rtsp, "Apple-Challenge");
+
+ if (STATUS_UNAUTHORIZED == status) {
+ wath = pa_xstrdup(pa_headerlist_gets(headers, "WWW-Authenticate"));
+ if (true == waiting) {
+ pa_xfree(wath);
+ goto fail;
+ }
+
+ if (wath) {
+ mth = pa_split(wath, space, &current);
+ while ((token = pa_split(wath, comma, &current))) {
+ if ((val = strstr(token, "="))) {
+ if (NULL == realm && val > strstr(token, "realm"))
+ realm = pa_xstrdup(val + 2);
+ else if (NULL == nonce && val > strstr(token, "nonce"))
+ nonce = pa_xstrdup(val + 2);
+ }
+
+ pa_xfree(token);
+ }
+ }
+
+ if (pa_safe_streq(mth, "Basic") && realm) {
+ rtrim_char(realm, '\"');
+
+ pa_raop_basic_response(DEFAULT_USER_NAME, c->password, &response);
+ ath = pa_sprintf_malloc("Basic %s",
+ response);
+ } else if (pa_safe_streq(mth, "Digest") && realm && nonce) {
+ rtrim_char(realm, '\"');
+ rtrim_char(nonce, '\"');
+
+ pa_raop_digest_response(DEFAULT_USER_NAME, realm, c->password, nonce, "*", &response);
+ ath = pa_sprintf_malloc("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"*\", response=\"%s\"",
+ DEFAULT_USER_NAME, realm, nonce,
+ response);
+ } else {
+ pa_log_error("unsupported authentication method: %s", mth);
+ pa_xfree(realm);
+ pa_xfree(nonce);
+ pa_xfree(wath);
+ pa_xfree(mth);
+ goto error;
+ }
+
+ pa_xfree(response);
+ pa_xfree(realm);
+ pa_xfree(nonce);
+ pa_xfree(wath);
+ pa_xfree(mth);
+
+ pa_rtsp_add_header(c->rtsp, "Authorization", ath);
+ pa_xfree(ath);
+
+ waiting = true;
+ pa_rtsp_options(c->rtsp);
+ break;
+ }
+
+ if (STATUS_OK == status) {
+ publ = pa_xstrdup(pa_headerlist_gets(headers, "Public"));
+ c->sci = pa_xstrdup(pa_rtsp_get_header(c->rtsp, "Client-Instance"));
+
+ if (c->password)
+ pa_xfree(c->password);
+ pa_xfree(publ);
+ c->password = NULL;
+ }
+
+ pa_rtsp_client_free(c->rtsp);
+ c->rtsp = NULL;
+ /* Ensure everything is cleaned before calling the callback, otherwise it may raise a crash */
+ if (c->state_callback)
+ c->state_callback((int) PA_RAOP_AUTHENTICATED, c->state_userdata);
+
+ waiting = false;
+ break;
+
+ fail:
+ if (c->state_callback)
+ c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);
+ pa_rtsp_client_free(c->rtsp);
+ c->rtsp = NULL;
+
+ pa_log_error("aborting authentication, wrong password");
+
+ waiting = false;
+ break;
+
+ error:
+ if (c->state_callback)
+ c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);
+ pa_rtsp_client_free(c->rtsp);
+ c->rtsp = NULL;
+
+ pa_log_error("aborting authentication, unexpected failure");
+
+ waiting = false;
+ break;
+ }
+
+ case STATE_ANNOUNCE:
+ case STATE_SETUP:
+ case STATE_RECORD:
+ case STATE_SET_PARAMETER:
+ case STATE_FLUSH:
+ case STATE_TEARDOWN:
+ case STATE_DISCONNECTED:
+ default: {
+ if (c->state_callback)
+ c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);
+ pa_rtsp_client_free(c->rtsp);
+ c->rtsp = NULL;
+
+ if (c->sci)
+ pa_xfree(c->sci);
+ c->sci = NULL;
+
+ break;
+ }
+ }
+}
+
+
+void pa_raop_client_disconnect(pa_raop_client *c) {
+ c->is_recording = false;
+
+ if (c->tcp_sfd >= 0)
+ pa_close(c->tcp_sfd);
+ c->tcp_sfd = -1;
+
+ if (c->udp_sfd >= 0)
+ pa_close(c->udp_sfd);
+ c->udp_sfd = -1;
+
+ /* Polling sockets will be closed by sink */
+ c->udp_cfd = c->udp_tfd = -1;
+ c->tcp_sfd = -1;
+
+ pa_log_error("RTSP control channel closed (disconnected)");
+
+ if (c->rtsp)
+ pa_rtsp_client_free(c->rtsp);
+ if (c->sid)
+ pa_xfree(c->sid);
+ c->rtsp = NULL;
+ c->sid = NULL;
+
+ if (c->state_callback)
+ c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);
+
+}
+
+
+pa_raop_client* pa_raop_client_new(pa_core *core, const char *host, pa_raop_protocol_t protocol,
+ pa_raop_encryption_t encryption, pa_raop_codec_t codec, bool autoreconnect) {
+ pa_raop_client *c;
+
+ pa_parsed_address a;
+ pa_sample_spec ss;
+ size_t size = 2;
+
+ pa_assert(core);
+ pa_assert(host);
+
+ if (pa_parse_address(host, &a) < 0)
+ return NULL;
+
+ if (a.type == PA_PARSED_ADDRESS_UNIX) {
+ pa_xfree(a.path_or_host);
+ return NULL;
+ }
+
+ c = pa_xnew0(pa_raop_client, 1);
+ c->core = core;
+ c->host = a.path_or_host; /* Will eventually be freed on destruction of c */
+ if (a.port > 0)
+ c->port = a.port;
+ else
+ c->port = DEFAULT_RAOP_PORT;
+ c->rtsp = NULL;
+ c->sci = c->sid = NULL;
+ c->password = NULL;
+ c->autoreconnect = autoreconnect;
+
+ c->protocol = protocol;
+ c->encryption = encryption;
+ c->codec = codec;
+
+ c->tcp_sfd = -1;
+
+ c->udp_sfd = -1;
+ c->udp_cfd = -1;
+ c->udp_tfd = -1;
+
+ c->secret = NULL;
+ if (c->encryption != PA_RAOP_ENCRYPTION_NONE)
+ c->secret = pa_raop_secret_new();
+
+ ss = core->default_sample_spec;
+ if (c->protocol == PA_RAOP_PROTOCOL_UDP)
+ size = RTX_BUFFERING_SECONDS * ss.rate / FRAMES_PER_UDP_PACKET;
+
+ c->is_recording = false;
+ c->is_first_packet = true;
+ /* Packet sync interval should be around 1s (UDP only) */
+ c->sync_interval = ss.rate / FRAMES_PER_UDP_PACKET;
+ c->sync_count = 0;
+
+ c->pbuf = pa_raop_packet_buffer_new(c->core->mempool, size);
+
+ return c;
+}
+
+void pa_raop_client_free(pa_raop_client *c) {
+ pa_assert(c);
+
+ pa_raop_packet_buffer_free(c->pbuf);
+
+ pa_xfree(c->sid);
+ pa_xfree(c->sci);
+ if (c->secret)
+ pa_raop_secret_free(c->secret);
+ pa_xfree(c->password);
+ c->sci = c->sid = NULL;
+ c->password = NULL;
+ c->secret = NULL;
+
+ if (c->rtsp)
+ pa_rtsp_client_free(c->rtsp);
+ c->rtsp = NULL;
+
+ pa_xfree(c->host);
+ pa_xfree(c);
+}
+
+int pa_raop_client_authenticate (pa_raop_client *c, const char *password) {
+ int rv = 0;
+
+ pa_assert(c);
+
+ if (c->rtsp || c->password) {
+ pa_log_debug("Authentication/Connection already in progress...");
+ return 0;
+ }
+
+ c->password = NULL;
+ if (password)
+ c->password = pa_xstrdup(password);
+ c->rtsp = pa_rtsp_client_new(c->core->mainloop, c->host, c->port, DEFAULT_USER_AGENT, c->autoreconnect);
+
+ pa_assert(c->rtsp);
+
+ pa_rtsp_set_callback(c->rtsp, rtsp_auth_cb, c);
+ rv = pa_rtsp_connect(c->rtsp);
+ return rv;
+}
+
+bool pa_raop_client_is_authenticated(pa_raop_client *c) {
+ pa_assert(c);
+
+ return (c->sci != NULL);
+}
+
+int pa_raop_client_announce(pa_raop_client *c) {
+ uint32_t sid;
+ int rv = 0;
+
+ pa_assert(c);
+
+ if (c->rtsp) {
+ pa_log_debug("Connection already in progress...");
+ return 0;
+ } else if (!c->sci) {
+ pa_log_debug("ANNOUNCE requires a preliminary authentication");
+ return 1;
+ }
+
+ c->rtsp = pa_rtsp_client_new(c->core->mainloop, c->host, c->port, DEFAULT_USER_AGENT, c->autoreconnect);
+
+ pa_assert(c->rtsp);
+
+ c->sync_count = 0;
+ c->is_recording = false;
+ c->is_first_packet = true;
+ pa_random(&sid, sizeof(sid));
+ c->sid = pa_sprintf_malloc("%u", sid);
+ pa_rtsp_set_callback(c->rtsp, rtsp_stream_cb, c);
+
+ rv = pa_rtsp_connect(c->rtsp);
+ return rv;
+}
+
+bool pa_raop_client_is_alive(pa_raop_client *c) {
+ pa_assert(c);
+
+ if (!c->rtsp || !c->sci) {
+ pa_log_debug("Not alive, connection not established yet...");
+ return false;
+ }
+
+ switch (c->protocol) {
+ case PA_RAOP_PROTOCOL_TCP:
+ if (c->tcp_sfd >= 0)
+ return true;
+ break;
+ case PA_RAOP_PROTOCOL_UDP:
+ if (c->udp_sfd >= 0)
+ return true;
+ break;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool pa_raop_client_can_stream(pa_raop_client *c) {
+ pa_assert(c);
+
+ if (!c->rtsp || !c->sci) {
+ return false;
+ }
+
+ switch (c->protocol) {
+ case PA_RAOP_PROTOCOL_TCP:
+ if (c->tcp_sfd >= 0 && c->is_recording)
+ return true;
+ break;
+ case PA_RAOP_PROTOCOL_UDP:
+ if (c->udp_sfd >= 0 && c->is_recording)
+ return true;
+ break;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool pa_raop_client_is_recording(pa_raop_client *c) {
+ return c->is_recording;
+}
+
+int pa_raop_client_stream(pa_raop_client *c) {
+ int rv = 0;
+
+ pa_assert(c);
+
+ if (!c->rtsp || !c->sci) {
+ pa_log_debug("Streaming's impossible, connection not established yet...");
+ return 0;
+ }
+
+ switch (c->protocol) {
+ case PA_RAOP_PROTOCOL_TCP:
+ if (c->tcp_sfd >= 0 && !c->is_recording) {
+ c->is_recording = true;
+ c->is_first_packet = true;
+ c->sync_count = 0;
+ }
+ break;
+ case PA_RAOP_PROTOCOL_UDP:
+ if (c->udp_sfd >= 0 && !c->is_recording) {
+ c->is_recording = true;
+ c->is_first_packet = true;
+ c->sync_count = 0;
+ }
+ break;
+ default:
+ rv = 1;
+ break;
+ }
+
+ return rv;
+}
+
+int pa_raop_client_set_volume(pa_raop_client *c, pa_volume_t volume) {
+ char *param;
+ int rv = 0;
+ double db;
+
+ pa_assert(c);
+
+ if (!c->rtsp) {
+ pa_log_debug("Cannot SET_PARAMETER, connection not established yet...");
+ return 0;
+ } else if (!c->sci) {
+ pa_log_debug("SET_PARAMETER requires a preliminary authentication");
+ return 1;
+ }
+
+ db = pa_sw_volume_to_dB(volume);
+ if (db < VOLUME_MIN)
+ db = VOLUME_MIN;
+ else if (db > VOLUME_MAX)
+ db = VOLUME_MAX;
+
+ pa_log_debug("volume=%u db=%.6f", volume, db);
+
+ param = pa_sprintf_malloc("volume: %0.6f\r\n", db);
+ /* We just hit and hope, cannot wait for the callback. */
+ if (c->rtsp != NULL && pa_rtsp_exec_ready(c->rtsp))
+ rv = pa_rtsp_setparameter(c->rtsp, param);
+
+ pa_xfree(param);
+ return rv;
+}
+
+int pa_raop_client_flush(pa_raop_client *c) {
+ int rv = 0;
+
+ pa_assert(c);
+
+ if (!c->rtsp || !pa_rtsp_exec_ready(c->rtsp)) {
+ pa_log_debug("Cannot FLUSH, connection not established yet...)");
+ return 0;
+ } else if (!c->sci) {
+ pa_log_debug("FLUSH requires a preliminary authentication");
+ return 1;
+ }
+
+ c->is_recording = false;
+
+ rv = pa_rtsp_flush(c->rtsp, c->seq, c->rtptime);
+ return rv;
+}
+
+int pa_raop_client_teardown(pa_raop_client *c) {
+ int rv = 0;
+
+ pa_assert(c);
+
+ if (!c->rtsp) {
+ pa_log_debug("Cannot TEARDOWN, connection not established yet...");
+ return 0;
+ } else if (!c->sci) {
+ pa_log_debug("TEARDOWN requires a preliminary authentication");
+ return 1;
+ }
+
+ c->is_recording = false;
+
+ rv = pa_rtsp_teardown(c->rtsp);
+ return rv;
+}
+
+void pa_raop_client_get_frames_per_block(pa_raop_client *c, size_t *frames) {
+ pa_assert(c);
+ pa_assert(frames);
+
+ switch (c->protocol) {
+ case PA_RAOP_PROTOCOL_TCP:
+ *frames = FRAMES_PER_TCP_PACKET;
+ break;
+ case PA_RAOP_PROTOCOL_UDP:
+ *frames = FRAMES_PER_UDP_PACKET;
+ break;
+ default:
+ *frames = 0;
+ break;
+ }
+}
+
+bool pa_raop_client_register_pollfd(pa_raop_client *c, pa_rtpoll *poll, pa_rtpoll_item **poll_item) {
+ struct pollfd *pollfd = NULL;
+ pa_rtpoll_item *item = NULL;
+ bool oob = true;
+
+ pa_assert(c);
+ pa_assert(poll);
+ pa_assert(poll_item);
+
+ switch (c->protocol) {
+ case PA_RAOP_PROTOCOL_TCP:
+ item = pa_rtpoll_item_new(poll, PA_RTPOLL_NEVER, 1);
+ pollfd = pa_rtpoll_item_get_pollfd(item, NULL);
+ pollfd->fd = c->tcp_sfd;
+ pollfd->events = POLLOUT;
+ pollfd->revents = 0;
+ *poll_item = item;
+ oob = false;
+ break;
+ case PA_RAOP_PROTOCOL_UDP:
+ item = pa_rtpoll_item_new(poll, PA_RTPOLL_NEVER, 2);
+ pollfd = pa_rtpoll_item_get_pollfd(item, NULL);
+ pollfd->fd = c->udp_cfd;
+ pollfd->events = POLLIN | POLLPRI;
+ pollfd->revents = 0;
+ pollfd++;
+ pollfd->fd = c->udp_tfd;
+ pollfd->events = POLLIN | POLLPRI;
+ pollfd->revents = 0;
+ *poll_item = item;
+ oob = true;
+ break;
+ default:
+ *poll_item = NULL;
+ break;
+ }
+
+ return oob;
+}
+
+bool pa_raop_client_is_timing_fd(pa_raop_client *c, const int fd) {
+ return fd == c->udp_tfd;
+}
+
+pa_volume_t pa_raop_client_adjust_volume(pa_raop_client *c, pa_volume_t volume) {
+ double minv, maxv;
+
+ pa_assert(c);
+
+ if (c->protocol != PA_RAOP_PROTOCOL_UDP)
+ return volume;
+
+ maxv = pa_sw_volume_from_dB(0.0);
+ minv = maxv * pow(10.0, VOLUME_DEF / 60.0);
+
+ /* Adjust volume so that it fits into VOLUME_DEF <= v <= 0 dB */
+ return volume - volume * (minv / maxv) + minv;
+}
+
+void pa_raop_client_handle_oob_packet(pa_raop_client *c, const int fd, const uint8_t packet[], ssize_t size) {
+ pa_assert(c);
+ pa_assert(fd >= 0);
+ pa_assert(packet);
+
+ if (c->protocol == PA_RAOP_PROTOCOL_UDP) {
+ if (fd == c->udp_cfd) {
+ pa_log_debug("Received UDP control packet...");
+ handle_udp_control_packet(c, packet, size);
+ } else if (fd == c->udp_tfd) {
+ pa_log_debug("Received UDP timing packet...");
+ handle_udp_timing_packet(c, packet, size);
+ }
+ }
+}
+
+ssize_t pa_raop_client_send_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) {
+ ssize_t written = 0;
+
+ pa_assert(c);
+ pa_assert(block);
+
+ /* Sync RTP & NTP timestamp if required (UDP). */
+ if (c->protocol == PA_RAOP_PROTOCOL_UDP) {
+ c->sync_count++;
+ if (c->is_first_packet || c->sync_count >= c->sync_interval) {
+ send_udp_sync_packet(c, c->rtptime);
+ c->sync_count = 0;
+ }
+ }
+
+ switch (c->protocol) {
+ case PA_RAOP_PROTOCOL_TCP:
+ written = send_tcp_audio_packet(c, block, offset);
+ break;
+ case PA_RAOP_PROTOCOL_UDP:
+ written = send_udp_audio_packet(c, block, offset);
+ break;
+ default:
+ written = -1;
+ break;
+ }
+
+ c->is_first_packet = false;
+ return written;
+}
+
+void pa_raop_client_set_state_callback(pa_raop_client *c, pa_raop_client_state_cb_t callback, void *userdata) {
+ pa_assert(c);
+
+ c->state_callback = callback;
+ c->state_userdata = userdata;
+}
diff --git a/src/modules/raop/raop-client.h b/src/modules/raop/raop-client.h
new file mode 100644
index 0000000..faec01e
--- /dev/null
+++ b/src/modules/raop/raop-client.h
@@ -0,0 +1,86 @@
+#ifndef fooraopclientfoo
+#define fooraopclientfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2008 Colin Guthrie
+
+ 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, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <pulse/volume.h>
+
+#include <pulsecore/core.h>
+#include <pulsecore/memchunk.h>
+#include <pulsecore/rtpoll.h>
+
+typedef enum pa_raop_protocol {
+ PA_RAOP_PROTOCOL_TCP,
+ PA_RAOP_PROTOCOL_UDP
+} pa_raop_protocol_t;
+
+typedef enum pa_raop_encryption {
+ PA_RAOP_ENCRYPTION_NONE,
+ PA_RAOP_ENCRYPTION_RSA,
+ PA_RAOP_ENCRYPTION_FAIRPLAY,
+ PA_RAOP_ENCRYPTION_MFISAP,
+ PA_RAOP_ENCRYPTION_FAIRPLAY_SAP25
+} pa_raop_encryption_t;
+
+typedef enum pa_raop_codec {
+ PA_RAOP_CODEC_PCM,
+ PA_RAOP_CODEC_ALAC,
+ PA_RAOP_CODEC_AAC,
+ PA_RAOP_CODEC_AAC_ELD
+} pa_raop_codec_t;
+
+typedef struct pa_raop_client pa_raop_client;
+
+typedef enum pa_raop_state {
+ PA_RAOP_INVALID_STATE,
+ PA_RAOP_AUTHENTICATED,
+ PA_RAOP_CONNECTED,
+ PA_RAOP_RECORDING,
+ PA_RAOP_DISCONNECTED
+} pa_raop_state_t;
+
+pa_raop_client* pa_raop_client_new(pa_core *core, const char *host, pa_raop_protocol_t protocol,
+ pa_raop_encryption_t encryption, pa_raop_codec_t codec, bool autoreconnect);
+void pa_raop_client_free(pa_raop_client *c);
+
+int pa_raop_client_authenticate(pa_raop_client *c, const char *password);
+bool pa_raop_client_is_authenticated(pa_raop_client *c);
+
+int pa_raop_client_announce(pa_raop_client *c);
+bool pa_raop_client_is_alive(pa_raop_client *c);
+bool pa_raop_client_is_recording(pa_raop_client *c);
+bool pa_raop_client_can_stream(pa_raop_client *c);
+int pa_raop_client_stream(pa_raop_client *c);
+int pa_raop_client_set_volume(pa_raop_client *c, pa_volume_t volume);
+int pa_raop_client_flush(pa_raop_client *c);
+int pa_raop_client_teardown(pa_raop_client *c);
+void pa_raop_client_disconnect(pa_raop_client *c);
+
+void pa_raop_client_get_frames_per_block(pa_raop_client *c, size_t *size);
+bool pa_raop_client_register_pollfd(pa_raop_client *c, pa_rtpoll *poll, pa_rtpoll_item **poll_item);
+bool pa_raop_client_is_timing_fd(pa_raop_client *c, const int fd);
+pa_volume_t pa_raop_client_adjust_volume(pa_raop_client *c, pa_volume_t volume);
+void pa_raop_client_handle_oob_packet(pa_raop_client *c, const int fd, const uint8_t packet[], ssize_t size);
+ssize_t pa_raop_client_send_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset);
+
+typedef void (*pa_raop_client_state_cb_t)(pa_raop_state_t state, void *userdata);
+void pa_raop_client_set_state_callback(pa_raop_client *c, pa_raop_client_state_cb_t callback, void *userdata);
+
+#endif
diff --git a/src/modules/raop/raop-crypto.c b/src/modules/raop/raop-crypto.c
new file mode 100644
index 0000000..710e93c
--- /dev/null
+++ b/src/modules/raop/raop-crypto.c
@@ -0,0 +1,214 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2013 Martin Blanchard
+
+ 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 <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <openssl/err.h>
+#include <openssl/aes.h>
+#include <openssl/rsa.h>
+#include <openssl/bn.h>
+
+#include <pulse/xmalloc.h>
+
+#include <pulsecore/macro.h>
+#include <pulsecore/random.h>
+
+#include "raop-crypto.h"
+#include "raop-util.h"
+
+#define AES_CHUNK_SIZE 16
+
+/* Openssl 1.1.0 broke compatibility. Before 1.1.0 we had to set RSA->n and
+ * RSA->e manually, but after 1.1.0 the RSA struct is opaque and we have to use
+ * RSA_set0_key(). RSA_set0_key() is a new function added in 1.1.0. We could
+ * depend on openssl 1.1.0, but it may take some time before distributions will
+ * be able to upgrade to the new openssl version. To insulate ourselves from
+ * such transition problems, let's implement RSA_set0_key() ourselves if it's
+ * not available. */
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+static int RSA_set0_key(RSA *r, BIGNUM *n, BIGNUM *e, BIGNUM *d) {
+ r->n = n;
+ r->e = e;
+ return 1;
+}
+#endif
+
+struct pa_raop_secret {
+ uint8_t key[AES_CHUNK_SIZE]; /* Key for aes-cbc */
+ uint8_t iv[AES_CHUNK_SIZE]; /* Initialization vector for cbc */
+ AES_KEY aes; /* AES encryption */
+};
+
+static const char rsa_modulus[] =
+ "59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUtwC"
+ "5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDR"
+ "KSKv6kDqnw4UwPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuB"
+ "OitnZ/bDzPHrTOZz0Dew0uowxf/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJ"
+ "Q+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/UAaHqn9JdsBWLUEpVviYnh"
+ "imNVvYFZeCXg/IdTQ+x4IRdiXNv5hEew==";
+
+static const char rsa_exponent[] =
+ "AQAB";
+
+static int rsa_encrypt(uint8_t *data, int len, uint8_t *str) {
+ uint8_t modulus[256];
+ uint8_t exponent[8];
+ int size;
+ RSA *rsa;
+ BIGNUM *n_bn = NULL;
+ BIGNUM *e_bn = NULL;
+ int r;
+
+ pa_assert(data);
+ pa_assert(str);
+
+ rsa = RSA_new();
+ if (!rsa) {
+ pa_log("RSA_new() failed.");
+ goto fail;
+ }
+
+ size = pa_raop_base64_decode(rsa_modulus, modulus);
+
+ n_bn = BN_bin2bn(modulus, size, NULL);
+ if (!n_bn) {
+ pa_log("n_bn = BN_bin2bn() failed.");
+ goto fail;
+ }
+
+ size = pa_raop_base64_decode(rsa_exponent, exponent);
+
+ e_bn = BN_bin2bn(exponent, size, NULL);
+ if (!e_bn) {
+ pa_log("e_bn = BN_bin2bn() failed.");
+ goto fail;
+ }
+
+ r = RSA_set0_key(rsa, n_bn, e_bn, NULL);
+ if (r == 0) {
+ pa_log("RSA_set0_key() failed.");
+ goto fail;
+ }
+
+ /* The memory allocated for n_bn and e_bn is now managed by the RSA object.
+ * Let's set n_bn and e_bn to NULL to avoid freeing the memory in the error
+ * handling code. */
+ n_bn = NULL;
+ e_bn = NULL;
+
+ size = RSA_public_encrypt(len, data, str, rsa, RSA_PKCS1_OAEP_PADDING);
+ if (size == -1) {
+ pa_log("RSA_public_encrypt() failed.");
+ goto fail;
+ }
+
+ RSA_free(rsa);
+ return size;
+
+fail:
+ if (e_bn)
+ BN_free(e_bn);
+
+ if (n_bn)
+ BN_free(n_bn);
+
+ if (rsa)
+ RSA_free(rsa);
+
+ return -1;
+}
+
+pa_raop_secret* pa_raop_secret_new(void) {
+ pa_raop_secret *s = pa_xnew0(pa_raop_secret, 1);
+
+ pa_assert(s);
+
+ pa_random(s->key, sizeof(s->key));
+ AES_set_encrypt_key(s->key, 128, &s->aes);
+ pa_random(s->iv, sizeof(s->iv));
+
+ return s;
+}
+
+void pa_raop_secret_free(pa_raop_secret *s) {
+ pa_assert(s);
+
+ pa_xfree(s);
+}
+
+char* pa_raop_secret_get_iv(pa_raop_secret *s) {
+ char *base64_iv = NULL;
+
+ pa_assert(s);
+
+ pa_raop_base64_encode(s->iv, AES_CHUNK_SIZE, &base64_iv);
+
+ return base64_iv;
+}
+
+char* pa_raop_secret_get_key(pa_raop_secret *s) {
+ char *base64_key = NULL;
+ uint8_t rsa_key[512];
+ int size = 0;
+
+ pa_assert(s);
+
+ /* Encrypt our AES public key to send to the device */
+ size = rsa_encrypt(s->key, AES_CHUNK_SIZE, rsa_key);
+ if (size < 0) {
+ pa_log("rsa_encrypt() failed.");
+ return NULL;
+ }
+
+ pa_raop_base64_encode(rsa_key, size, &base64_key);
+
+ return base64_key;
+}
+
+int pa_raop_aes_encrypt(pa_raop_secret *s, uint8_t *data, int len) {
+ static uint8_t nv[AES_CHUNK_SIZE];
+ uint8_t *buffer;
+ int i = 0, j;
+
+ pa_assert(s);
+ pa_assert(data);
+
+ memcpy(nv, s->iv, AES_CHUNK_SIZE);
+
+ while (i + AES_CHUNK_SIZE <= len) {
+ buffer = data + i;
+ for (j = 0; j < AES_CHUNK_SIZE; ++j)
+ buffer[j] ^= nv[j];
+
+ AES_encrypt(buffer, buffer, &s->aes);
+
+ memcpy(nv, buffer, AES_CHUNK_SIZE);
+ i += AES_CHUNK_SIZE;
+ }
+
+ return i;
+}
diff --git a/src/modules/raop/raop-crypto.h b/src/modules/raop/raop-crypto.h
new file mode 100644
index 0000000..65f7577
--- /dev/null
+++ b/src/modules/raop/raop-crypto.h
@@ -0,0 +1,35 @@
+#ifndef fooraopcryptofoo
+#define fooraopcryptofoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2013 Martin Blanchard
+
+ 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.
+***/
+
+typedef struct pa_raop_secret pa_raop_secret;
+
+pa_raop_secret* pa_raop_secret_new(void);
+void pa_raop_secret_free(pa_raop_secret *s);
+
+char* pa_raop_secret_get_iv(pa_raop_secret *s);
+char* pa_raop_secret_get_key(pa_raop_secret *s);
+
+int pa_raop_aes_encrypt(pa_raop_secret *s, uint8_t *data, int len);
+
+#endif
diff --git a/src/modules/raop/raop-packet-buffer.c b/src/modules/raop/raop-packet-buffer.c
new file mode 100644
index 0000000..72fd729
--- /dev/null
+++ b/src/modules/raop/raop-packet-buffer.c
@@ -0,0 +1,161 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2013 Matthias Wabersich
+ Copyright 2013 Hajime Fujita
+
+ 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 <stdlib.h>
+#include <stdint.h>
+#include <limits.h>
+
+#include <pulse/xmalloc.h>
+
+#include <pulsecore/core-error.h>
+#include <pulsecore/macro.h>
+
+#include "raop-packet-buffer.h"
+
+struct pa_raop_packet_buffer {
+ pa_memchunk *packets;
+ pa_mempool *mempool;
+
+ size_t size;
+ size_t count;
+
+ uint16_t seq;
+ size_t pos;
+};
+
+pa_raop_packet_buffer *pa_raop_packet_buffer_new(pa_mempool *mempool, const size_t size) {
+ pa_raop_packet_buffer *pb = pa_xnew0(pa_raop_packet_buffer, 1);
+
+ pa_assert(mempool);
+ pa_assert(size > 0);
+
+ pb->count = 0;
+ pb->size = size;
+ pb->mempool = mempool;
+ pb->packets = pa_xnew0(pa_memchunk, size);
+ pb->seq = pb->pos = 0;
+
+ return pb;
+}
+
+void pa_raop_packet_buffer_free(pa_raop_packet_buffer *pb) {
+ size_t i;
+
+ pa_assert(pb);
+
+ for (i = 0; pb->packets && i < pb->size; i++) {
+ if (pb->packets[i].memblock)
+ pa_memblock_unref(pb->packets[i].memblock);
+ pa_memchunk_reset(&pb->packets[i]);
+ }
+
+ pa_xfree(pb->packets);
+ pb->packets = NULL;
+ pa_xfree(pb);
+}
+
+void pa_raop_packet_buffer_reset(pa_raop_packet_buffer *pb, uint16_t seq) {
+ size_t i;
+
+ pa_assert(pb);
+ pa_assert(pb->packets);
+
+ pb->pos = 0;
+ pb->count = 0;
+ pb->seq = (!seq) ? UINT16_MAX : seq - 1;
+ for (i = 0; i < pb->size; i++) {
+ if (pb->packets[i].memblock)
+ pa_memblock_unref(pb->packets[i].memblock);
+ pa_memchunk_reset(&pb->packets[i]);
+ }
+}
+
+pa_memchunk *pa_raop_packet_buffer_prepare(pa_raop_packet_buffer *pb, uint16_t seq, const size_t size) {
+ pa_memchunk *packet = NULL;
+ size_t i;
+
+ pa_assert(pb);
+ pa_assert(pb->packets);
+
+ if (seq == 0) {
+ /* 0 means seq reached UINT16_MAX and has been wrapped... */
+ pa_assert(pb->seq == UINT16_MAX);
+ pb->seq = 0;
+ } else {
+ /* ...otherwise, seq MUST have be increased! */
+ pa_assert(seq == pb->seq + 1);
+ pb->seq++;
+ }
+
+ i = (pb->pos + 1) % pb->size;
+
+ if (pb->packets[i].memblock)
+ pa_memblock_unref(pb->packets[i].memblock);
+ pa_memchunk_reset(&pb->packets[i]);
+
+ pb->packets[i].memblock = pa_memblock_new(pb->mempool, size);
+ pb->packets[i].length = size;
+ pb->packets[i].index = 0;
+
+ packet = &pb->packets[i];
+
+ if (pb->count < pb->size)
+ pb->count++;
+ pb->pos = i;
+
+ return packet;
+}
+
+pa_memchunk *pa_raop_packet_buffer_retrieve(pa_raop_packet_buffer *pb, uint16_t seq) {
+ pa_memchunk *packet = NULL;
+ size_t delta, i;
+
+ pa_assert(pb);
+ pa_assert(pb->packets);
+
+ if (seq == pb->seq)
+ packet = &pb->packets[pb->pos];
+ else {
+ if (seq < pb->seq) {
+ /* Regular case: pb->seq did not wrapped since seq. */
+ delta = pb->seq - seq;
+ } else {
+ /* Tricky case: pb->seq wrapped since seq! */
+ delta = pb->seq + (UINT16_MAX - seq);
+ }
+
+ /* If the requested packet is too old, do nothing and return */
+ if (delta > pb->count)
+ return NULL;
+
+ i = (pb->size + pb->pos - delta) % pb->size;
+
+ if (delta < pb->size && pb->packets[i].memblock)
+ packet = &pb->packets[i];
+ }
+
+ return packet;
+}
diff --git a/src/modules/raop/raop-packet-buffer.h b/src/modules/raop/raop-packet-buffer.h
new file mode 100644
index 0000000..c410298
--- /dev/null
+++ b/src/modules/raop/raop-packet-buffer.h
@@ -0,0 +1,40 @@
+#ifndef fooraoppacketbufferfoo
+#define fooraoppacketbufferfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2013 Matthias Wabersich
+ Copyright 2013 Hajime Fujita
+
+ 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/memblock.h>
+#include <pulsecore/memchunk.h>
+
+typedef struct pa_raop_packet_buffer pa_raop_packet_buffer;
+
+/* Allocates a new circular packet buffer, size: Maximum number of packets to store */
+pa_raop_packet_buffer *pa_raop_packet_buffer_new(pa_mempool *mempool, const size_t size);
+void pa_raop_packet_buffer_free(pa_raop_packet_buffer *pb);
+
+void pa_raop_packet_buffer_reset(pa_raop_packet_buffer *pb, uint16_t seq);
+
+pa_memchunk *pa_raop_packet_buffer_prepare(pa_raop_packet_buffer *pb, uint16_t seq, const size_t size);
+pa_memchunk *pa_raop_packet_buffer_retrieve(pa_raop_packet_buffer *pb, uint16_t seq);
+
+#endif
diff --git a/src/modules/raop/raop-sink.c b/src/modules/raop/raop-sink.c
new file mode 100644
index 0000000..114f6d1
--- /dev/null
+++ b/src/modules/raop/raop-sink.c
@@ -0,0 +1,968 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2008 Colin Guthrie
+ Copyright 2013 Hajime Fujita
+ Copyright 2013 Martin Blanchard
+
+ 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 <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <sys/ioctl.h>
+
+#ifdef HAVE_LINUX_SOCKIOS_H
+#include <linux/sockios.h>
+#endif
+
+#include <pulse/rtclock.h>
+#include <pulse/timeval.h>
+#include <pulse/volume.h>
+#include <pulse/xmalloc.h>
+
+#include <pulsecore/core.h>
+#include <pulsecore/i18n.h>
+#include <pulsecore/module.h>
+#include <pulsecore/memchunk.h>
+#include <pulsecore/sink.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/core-error.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/log.h>
+#include <pulsecore/macro.h>
+#include <pulsecore/thread.h>
+#include <pulsecore/thread-mq.h>
+#include <pulsecore/poll.h>
+#include <pulsecore/rtpoll.h>
+#include <pulsecore/core-rtclock.h>
+#include <pulsecore/time-smoother.h>
+
+#include "raop-sink.h"
+#include "raop-client.h"
+#include "raop-util.h"
+
+#define UDP_TIMING_PACKET_LOSS_MAX (30 * PA_USEC_PER_SEC)
+#define UDP_TIMING_PACKET_DISCONNECT_CYCLE 3
+
+struct userdata {
+ pa_core *core;
+ pa_module *module;
+ pa_sink *sink;
+ pa_card *card;
+
+ pa_thread *thread;
+ pa_thread_mq thread_mq;
+ pa_rtpoll *rtpoll;
+ pa_rtpoll_item *rtpoll_item;
+ bool oob;
+
+ pa_raop_client *raop;
+ char *server;
+ pa_raop_protocol_t protocol;
+ pa_raop_encryption_t encryption;
+ pa_raop_codec_t codec;
+ bool autoreconnect;
+ /* if true, behaves like a null-sink when disconnected */
+ bool autonull;
+
+ size_t block_size;
+ pa_usec_t block_usec;
+ pa_memchunk memchunk;
+
+ pa_usec_t delay;
+ pa_usec_t start;
+ pa_smoother *smoother;
+ uint64_t write_count;
+
+ uint32_t latency;
+ /* Consider as first I/O thread iteration, can be switched to true in autoreconnect mode */
+ bool first;
+};
+
+enum {
+ PA_SINK_MESSAGE_SET_RAOP_STATE = PA_SINK_MESSAGE_MAX,
+ PA_SINK_MESSAGE_DISCONNECT_REQUEST
+};
+
+static void userdata_free(struct userdata *u);
+
+static void sink_set_volume_cb(pa_sink *s);
+
+static void raop_state_cb(pa_raop_state_t state, void *userdata) {
+ struct userdata *u = userdata;
+
+ pa_assert(u);
+
+ pa_log_debug("State change received, informing IO thread...");
+
+ pa_asyncmsgq_post(u->thread_mq.inq, PA_MSGOBJECT(u->sink), PA_SINK_MESSAGE_SET_RAOP_STATE, PA_INT_TO_PTR(state), 0, NULL, NULL);
+}
+
+static int64_t sink_get_latency(const struct userdata *u) {
+ pa_usec_t now;
+ int64_t latency;
+
+ pa_assert(u);
+ pa_assert(u->smoother);
+
+ now = pa_rtclock_now();
+ now = pa_smoother_get(u->smoother, now);
+
+ latency = pa_bytes_to_usec(u->write_count, &u->sink->sample_spec) - (int64_t) now;
+
+ /* RAOP default latency */
+ latency += u->latency * PA_USEC_PER_MSEC;
+
+ return latency;
+}
+
+static int sink_process_msg(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *chunk) {
+ struct userdata *u = PA_SINK(o)->userdata;
+
+ pa_assert(u);
+ pa_assert(u->raop);
+
+ switch (code) {
+ /* Exception : for this message, we are in main thread, msg sent from the IO/thread
+ Done here, as alloc/free of rtsp_client is also done in this thread for other cases */
+ case PA_SINK_MESSAGE_DISCONNECT_REQUEST: {
+ if (u->sink->state == PA_SINK_RUNNING) {
+ /* Disconnect raop client, and restart the whole chain since
+ * the authentication token might be outdated */
+ pa_raop_client_disconnect(u->raop);
+ pa_raop_client_authenticate(u->raop, NULL);
+ }
+
+ return 0;
+ }
+
+ case PA_SINK_MESSAGE_GET_LATENCY: {
+ int64_t r = 0;
+
+ if (u->autonull || pa_raop_client_can_stream(u->raop))
+ r = sink_get_latency(u);
+
+ *((int64_t*) data) = r;
+
+ return 0;
+ }
+
+ case PA_SINK_MESSAGE_SET_RAOP_STATE: {
+ switch ((pa_raop_state_t) PA_PTR_TO_UINT(data)) {
+ case PA_RAOP_AUTHENTICATED: {
+ if (!pa_raop_client_is_authenticated(u->raop)) {
+ pa_module_unload_request(u->module, true);
+ }
+
+ if (u->autoreconnect && u->sink->state == PA_SINK_RUNNING) {
+ pa_usec_t now;
+ now = pa_rtclock_now();
+ pa_smoother_reset(u->smoother, now, false);
+
+ if (!pa_raop_client_is_alive(u->raop)) {
+ /* Connecting will trigger a RECORD and start steaming */
+ pa_raop_client_announce(u->raop);
+ }
+ }
+
+ return 0;
+ }
+
+ case PA_RAOP_CONNECTED: {
+ pa_assert(!u->rtpoll_item);
+
+ u->oob = pa_raop_client_register_pollfd(u->raop, u->rtpoll, &u->rtpoll_item);
+
+ return 0;
+ }
+
+ case PA_RAOP_RECORDING: {
+ pa_usec_t now;
+
+ now = pa_rtclock_now();
+ u->write_count = 0;
+ u->start = now;
+ u->first = true;
+ pa_rtpoll_set_timer_absolute(u->rtpoll, now);
+
+ if (u->sink->thread_info.state == PA_SINK_SUSPENDED) {
+ /* Our stream has been suspended so we just flush it... */
+ pa_rtpoll_set_timer_disabled(u->rtpoll);
+ pa_raop_client_flush(u->raop);
+ } else {
+ /* Set the initial volume */
+ sink_set_volume_cb(u->sink);
+ pa_sink_process_msg(o, PA_SINK_MESSAGE_SET_VOLUME, data, offset, chunk);
+ }
+
+ return 0;
+ }
+
+ case PA_RAOP_INVALID_STATE:
+ case PA_RAOP_DISCONNECTED: {
+ unsigned int nbfds = 0;
+ struct pollfd *pollfd;
+ unsigned int i;
+
+ if (u->rtpoll_item) {
+ pollfd = pa_rtpoll_item_get_pollfd(u->rtpoll_item, &nbfds);
+ if (pollfd) {
+ for (i = 0; i < nbfds; i++) {
+ if (pollfd->fd >= 0)
+ pa_close(pollfd->fd);
+ pollfd++;
+ }
+ }
+ pa_rtpoll_item_free(u->rtpoll_item);
+ u->rtpoll_item = NULL;
+ }
+
+ if (u->sink->thread_info.state == PA_SINK_SUSPENDED) {
+ pa_rtpoll_set_timer_disabled(u->rtpoll);
+
+ return 0;
+ }
+
+ if (u->autoreconnect) {
+ if (u->sink->thread_info.state != PA_SINK_IDLE) {
+ if (!u->autonull)
+ pa_rtpoll_set_timer_disabled(u->rtpoll);
+ pa_raop_client_authenticate(u->raop, NULL);
+ }
+ } else {
+ if (u->sink->thread_info.state != PA_SINK_IDLE)
+ pa_module_unload_request(u->module, true);
+ }
+
+ return 0;
+ }
+ }
+
+ return 0;
+ }
+ }
+
+ return pa_sink_process_msg(o, code, data, offset, chunk);
+}
+
+/* Called from the IO thread. */
+static int sink_set_state_in_io_thread_cb(pa_sink *s, pa_sink_state_t new_state, pa_suspend_cause_t new_suspend_cause) {
+ struct userdata *u;
+
+ pa_assert(s);
+ pa_assert_se(u = s->userdata);
+
+ /* It may be that only the suspend cause is changing, in which case there's
+ * nothing to do. */
+ if (new_state == s->thread_info.state)
+ return 0;
+
+ switch (new_state) {
+ case PA_SINK_SUSPENDED:
+ pa_log_debug("RAOP: SUSPENDED");
+
+ pa_assert(PA_SINK_IS_OPENED(s->thread_info.state));
+
+ /* Issue a TEARDOWN if we are still connected */
+ if (pa_raop_client_is_alive(u->raop)) {
+ pa_raop_client_teardown(u->raop);
+ }
+
+ break;
+
+ case PA_SINK_IDLE:
+ pa_log_debug("RAOP: IDLE");
+
+ /* Issue a FLUSH if we're coming from running state */
+ if (s->thread_info.state == PA_SINK_RUNNING) {
+ pa_rtpoll_set_timer_disabled(u->rtpoll);
+ pa_raop_client_flush(u->raop);
+ }
+
+ break;
+
+ case PA_SINK_RUNNING: {
+ pa_usec_t now;
+
+ pa_log_debug("RAOP: RUNNING");
+
+ now = pa_rtclock_now();
+ pa_smoother_reset(u->smoother, now, false);
+
+ /* If autonull is enabled, I/O thread is always eating chunks since
+ * it is emulating a null sink */
+ if (u->autonull) {
+ u->start = now;
+ u->write_count = 0;
+ u->first = true;
+ pa_rtpoll_set_timer_absolute(u->rtpoll, now);
+ }
+
+ if (!pa_raop_client_is_alive(u->raop)) {
+ /* Connecting will trigger a RECORD and start streaming */
+ pa_raop_client_announce(u->raop);
+ } else if (!pa_raop_client_is_recording(u->raop)) {
+ /* RECORD alredy sent, simply start streaming */
+ pa_raop_client_stream(u->raop);
+ pa_rtpoll_set_timer_absolute(u->rtpoll, now);
+ u->write_count = 0;
+ u->start = now;
+ }
+
+ break;
+ }
+
+ case PA_SINK_UNLINKED:
+ case PA_SINK_INIT:
+ case PA_SINK_INVALID_STATE:
+ break;
+ }
+
+ return 0;
+}
+
+static void sink_set_volume_cb(pa_sink *s) {
+ struct userdata *u = s->userdata;
+ pa_cvolume hw;
+ pa_volume_t v, v_orig;
+ char t[PA_CVOLUME_SNPRINT_VERBOSE_MAX];
+
+ pa_assert(u);
+
+ /* If we're muted we don't need to do anything. */
+ if (s->muted)
+ return;
+
+ /* Calculate the max volume of all channels.
+ * We'll use this as our (single) volume on the APEX device and emulate
+ * any variation in channel volumes in software. */
+ v = pa_cvolume_max(&s->real_volume);
+
+ v_orig = v;
+ v = pa_raop_client_adjust_volume(u->raop, v_orig);
+
+ pa_log_debug("Volume adjusted: orig=%u adjusted=%u", v_orig, v);
+
+ /* Create a pa_cvolume version of our single value. */
+ pa_cvolume_set(&hw, s->sample_spec.channels, v);
+
+ /* Perform any software manipulation of the volume needed. */
+ pa_sw_cvolume_divide(&s->soft_volume, &s->real_volume, &hw);
+
+ pa_log_debug("Requested volume: %s", pa_cvolume_snprint_verbose(t, sizeof(t), &s->real_volume, &s->channel_map, false));
+ pa_log_debug("Got hardware volume: %s", pa_cvolume_snprint_verbose(t, sizeof(t), &hw, &s->channel_map, false));
+ pa_log_debug("Calculated software volume: %s",
+ pa_cvolume_snprint_verbose(t, sizeof(t), &s->soft_volume, &s->channel_map, true));
+
+ /* Any necessary software volume manipulation is done so set
+ * our hw volume (or v as a single value) on the device. */
+ pa_raop_client_set_volume(u->raop, v);
+}
+
+static void sink_set_mute_cb(pa_sink *s) {
+ struct userdata *u = s->userdata;
+
+ pa_assert(u);
+ pa_assert(u->raop);
+
+ if (s->muted) {
+ pa_raop_client_set_volume(u->raop, PA_VOLUME_MUTED);
+ } else {
+ sink_set_volume_cb(s);
+ }
+}
+
+static void thread_func(void *userdata) {
+ struct userdata *u = userdata;
+ size_t offset = 0;
+ pa_usec_t last_timing = 0;
+ uint32_t check_timing_count = 1;
+ pa_usec_t intvl = 0;
+
+ pa_assert(u);
+
+ pa_log_debug("Thread starting up");
+
+ pa_thread_mq_install(&u->thread_mq);
+ pa_smoother_set_time_offset(u->smoother, pa_rtclock_now());
+
+ for (;;) {
+ struct pollfd *pollfd = NULL;
+ unsigned int i, nbfds = 0;
+ pa_usec_t now, estimated;
+ uint64_t position;
+ size_t index;
+ int ret;
+ bool canstream, sendstream, on_timeout;
+
+ /* Polling (audio data + control socket + timing socket). */
+ if ((ret = pa_rtpoll_run(u->rtpoll)) < 0)
+ goto fail;
+ else if (ret == 0)
+ goto finish;
+
+ if (PA_SINK_IS_OPENED(u->sink->thread_info.state)) {
+ if (u->sink->thread_info.rewind_requested)
+ pa_sink_process_rewind(u->sink, 0);
+ }
+
+ on_timeout = pa_rtpoll_timer_elapsed(u->rtpoll);
+ if (u->rtpoll_item) {
+ pollfd = pa_rtpoll_item_get_pollfd(u->rtpoll_item, &nbfds);
+ /* If !oob: streaming driven by pollds (POLLOUT) */
+ if (pollfd && !u->oob && !pollfd->revents) {
+ for (i = 0; i < nbfds; i++) {
+ pollfd->events = POLLOUT;
+ pollfd->revents = 0;
+
+ pollfd++;
+ }
+
+ continue;
+ }
+
+ /* if oob: streaming managed by timing, pollfd for oob sockets */
+ if (pollfd && u->oob && !on_timeout) {
+ uint8_t packet[32];
+ ssize_t read;
+
+ for (i = 0; i < nbfds; i++) {
+ if (pollfd->revents & POLLERR) {
+ if (u->autoreconnect && pa_raop_client_is_alive(u->raop)) {
+ pollfd->revents = 0;
+ pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->sink),
+ PA_SINK_MESSAGE_DISCONNECT_REQUEST, 0, 0, NULL, NULL);
+ continue;
+ }
+
+ /* one of UDP fds is in faulty state, may have been disconnected, this is fatal */
+ goto fail;
+ }
+ if (pollfd->revents & pollfd->events) {
+ pollfd->revents = 0;
+ read = pa_read(pollfd->fd, packet, sizeof(packet), NULL);
+ pa_raop_client_handle_oob_packet(u->raop, pollfd->fd, packet, read);
+ if (pa_raop_client_is_timing_fd(u->raop, pollfd->fd)) {
+ last_timing = pa_rtclock_now();
+ check_timing_count = 1;
+ }
+ }
+
+ pollfd++;
+ }
+
+ continue;
+ }
+ }
+
+ if (u->sink->thread_info.state != PA_SINK_RUNNING) {
+ continue;
+ }
+
+ if (u->first) {
+ last_timing = 0;
+ check_timing_count = 1;
+ intvl = 0;
+ u->first = false;
+ }
+
+ canstream = pa_raop_client_can_stream(u->raop);
+ now = pa_rtclock_now();
+
+ if (u->oob && u->autoreconnect && on_timeout) {
+ if (!canstream) {
+ last_timing = 0;
+ } else if (last_timing != 0) {
+ pa_usec_t since = now - last_timing;
+ /* Incoming Timing packets should be received every 3 seconds in UDP mode
+ according to raop specifications.
+ Here we disconnect if no packet received since UDP_TIMING_PACKET_LOSS_MAX seconds
+ We only detect timing packet requests interruptions (we do nothing if no packet received at all), since some clients do not implement RTCP Timing requests at all */
+
+ if (since > (UDP_TIMING_PACKET_LOSS_MAX/UDP_TIMING_PACKET_DISCONNECT_CYCLE)*check_timing_count) {
+ if (check_timing_count < UDP_TIMING_PACKET_DISCONNECT_CYCLE) {
+ uint32_t since_in_sec = since / PA_USEC_PER_SEC;
+ pa_log_warn(
+ "UDP Timing Packets Warn #%d/%d- Nothing received since %d seconds from %s",
+ check_timing_count,
+ UDP_TIMING_PACKET_DISCONNECT_CYCLE-1, since_in_sec, u->server);
+ check_timing_count++;
+ } else {
+ /* Limit reached, then request disconnect */
+ check_timing_count = 1;
+ last_timing = 0;
+ if (pa_raop_client_is_alive(u->raop)) {
+ pa_log_warn("UDP Timing Packets Warn limit reached - Requesting reconnect");
+ pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->sink),
+ PA_SINK_MESSAGE_DISCONNECT_REQUEST, 0, 0, NULL, NULL);
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ if (!u->autonull) {
+ if (!canstream) {
+ pa_log_debug("Can't stream, connection not established yet...");
+ continue;
+ }
+ /* This assertion is meant to silence a complaint from Coverity about
+ * pollfd being possibly NULL when we access it later. That's a false
+ * positive, because we check pa_raop_client_can_stream() above, and if
+ * that returns true, it means that the connection is up, and when the
+ * connection is up, pollfd will be non-NULL. */
+ pa_assert(pollfd);
+ }
+
+ if (u->memchunk.length <= 0) {
+ if (intvl < now + u->block_usec) {
+ if (u->memchunk.memblock)
+ pa_memblock_unref(u->memchunk.memblock);
+ pa_memchunk_reset(&u->memchunk);
+
+ /* Grab unencoded audio data from PulseAudio */
+ pa_sink_render_full(u->sink, u->block_size, &u->memchunk);
+ offset = u->memchunk.index;
+ }
+ }
+
+ if (u->memchunk.length > 0) {
+ index = u->memchunk.index;
+ sendstream = !u->autonull || (u->autonull && canstream);
+ if (sendstream && pa_raop_client_send_audio_packet(u->raop, &u->memchunk, offset) < 0) {
+ if (errno == EINTR) {
+ /* Just try again. */
+ pa_log_debug("Failed to write data to FIFO (EINTR), retrying");
+ if (u->autoreconnect) {
+ pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->sink), PA_SINK_MESSAGE_DISCONNECT_REQUEST,
+ 0, 0, NULL, NULL);
+ continue;
+ } else
+ goto fail;
+ } else if (errno != EAGAIN && !u->oob) {
+ /* Buffer is full, wait for POLLOUT. */
+ if (!u->oob) {
+ pollfd->events = POLLOUT;
+ pollfd->revents = 0;
+ }
+ } else {
+ pa_log("Failed to write data to FIFO: %s", pa_cstrerror(errno));
+ if (u->autoreconnect) {
+ pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->sink), PA_SINK_MESSAGE_DISCONNECT_REQUEST,
+ 0, 0, NULL, NULL);
+ continue;
+ } else
+ goto fail;
+ }
+ } else {
+ if (sendstream) {
+ u->write_count += (uint64_t) u->memchunk.index - (uint64_t) index;
+ } else {
+ u->write_count += u->memchunk.length;
+ u->memchunk.length = 0;
+ }
+ position = u->write_count - pa_usec_to_bytes(u->delay, &u->sink->sample_spec);
+
+ now = pa_rtclock_now();
+ estimated = pa_bytes_to_usec(position, &u->sink->sample_spec);
+ pa_smoother_put(u->smoother, now, estimated);
+
+ if ((u->autonull && !canstream) || (u->oob && canstream && on_timeout)) {
+ /* Sleep until next packet transmission */
+ intvl = u->start + pa_bytes_to_usec(u->write_count, &u->sink->sample_spec);
+ pa_rtpoll_set_timer_absolute(u->rtpoll, intvl);
+ } else if (!u->oob) {
+ if (u->memchunk.length > 0) {
+ pollfd->events = POLLOUT;
+ pollfd->revents = 0;
+ } else {
+ intvl = u->start + pa_bytes_to_usec(u->write_count, &u->sink->sample_spec);
+ pa_rtpoll_set_timer_absolute(u->rtpoll, intvl);
+ pollfd->revents = 0;
+ pollfd->events = 0;
+ }
+ }
+ }
+ }
+ }
+
+fail:
+ /* If this was no regular exit from the loop we have to continue
+ * processing messages until we received PA_MESSAGE_SHUTDOWN */
+ pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->core), PA_CORE_MESSAGE_UNLOAD_MODULE, u->module, 0, NULL, NULL);
+ pa_asyncmsgq_wait_for(u->thread_mq.inq, PA_MESSAGE_SHUTDOWN);
+
+finish:
+ pa_log_debug("Thread shutting down");
+}
+
+static int sink_set_port_cb(pa_sink *s, pa_device_port *p) {
+ return 0;
+}
+
+static pa_device_port *raop_create_port(struct userdata *u, const char *server) {
+ pa_device_port_new_data data;
+ pa_device_port *port;
+
+ pa_device_port_new_data_init(&data);
+
+ pa_device_port_new_data_set_name(&data, "network-output");
+ pa_device_port_new_data_set_description(&data, server);
+ pa_device_port_new_data_set_direction(&data, PA_DIRECTION_OUTPUT);
+ pa_device_port_new_data_set_type(&data, PA_DEVICE_PORT_TYPE_NETWORK);
+
+ port = pa_device_port_new(u->core, &data, 0);
+
+ pa_device_port_new_data_done(&data);
+
+ if (port == NULL)
+ return NULL;
+
+ pa_device_port_ref(port);
+
+ return port;
+}
+
+static pa_card_profile *raop_create_profile() {
+ pa_card_profile *profile;
+
+ profile = pa_card_profile_new("RAOP", _("RAOP standard profile"), 0);
+ profile->priority = 10;
+ profile->n_sinks = 1;
+ profile->n_sources = 0;
+ profile->max_sink_channels = 2;
+ profile->max_source_channels = 0;
+
+ return profile;
+}
+
+static pa_card *raop_create_card(pa_module *m, pa_device_port *port, pa_card_profile *profile, const char *server, const char *nicename) {
+ pa_card_new_data data;
+ pa_card *card;
+ char *card_name;
+
+ pa_card_new_data_init(&data);
+
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_STRING, server);
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_DESCRIPTION, nicename);
+ data.driver = __FILE__;
+
+ card_name = pa_sprintf_malloc("raop_client.%s", server);
+ pa_card_new_data_set_name(&data, card_name);
+ pa_xfree(card_name);
+
+ pa_hashmap_put(data.ports, port->name, port);
+ pa_hashmap_put(data.profiles, profile->name, profile);
+
+ card = pa_card_new(m->core, &data);
+
+ pa_card_new_data_done(&data);
+
+ if (card == NULL)
+ return NULL;
+
+ pa_card_choose_initial_profile(card);
+
+ pa_card_put(card);
+
+ return card;
+}
+
+pa_sink* pa_raop_sink_new(pa_module *m, pa_modargs *ma, const char *driver) {
+ struct userdata *u = NULL;
+ pa_sample_spec ss;
+ pa_channel_map map;
+ char *thread_name = NULL;
+ const char *server, *protocol, *encryption, *codec;
+ const char /* *username, */ *password;
+ pa_sink_new_data data;
+ const char *name = NULL;
+ const char *description = NULL;
+ pa_device_port *port;
+ pa_card_profile *profile;
+
+ pa_assert(m);
+ pa_assert(ma);
+
+ 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;
+ }
+
+ if (!(server = pa_modargs_get_value(ma, "server", NULL))) {
+ pa_log("Failed to parse server argument");
+ goto fail;
+ }
+
+ if (!(protocol = pa_modargs_get_value(ma, "protocol", NULL))) {
+ pa_log("Failed to parse protocol argument");
+ goto fail;
+ }
+
+ u = pa_xnew0(struct userdata, 1);
+ u->core = m->core;
+ u->module = m;
+ u->thread = NULL;
+ u->rtpoll = pa_rtpoll_new();
+ u->rtpoll_item = NULL;
+ u->latency = RAOP_DEFAULT_LATENCY;
+ u->autoreconnect = false;
+ u->server = pa_xstrdup(server);
+
+ if (pa_modargs_get_value_boolean(ma, "autoreconnect", &u->autoreconnect) < 0) {
+ pa_log("Failed to parse autoreconnect argument");
+ goto fail;
+ }
+ /* Linked for now, potentially ready for additional parameter */
+ u->autonull = u->autoreconnect;
+
+ if (pa_modargs_get_value_u32(ma, "latency_msec", &u->latency) < 0) {
+ pa_log("Failed to parse latency_msec argument");
+ goto fail;
+ }
+
+ if (pa_thread_mq_init(&u->thread_mq, m->core->mainloop, u->rtpoll) < 0) {
+ pa_log("pa_thread_mq_init() failed.");
+ goto fail;
+ }
+
+ u->oob = true;
+
+ u->block_size = 0;
+ pa_memchunk_reset(&u->memchunk);
+
+ u->delay = 0;
+ u->smoother = pa_smoother_new(
+ PA_USEC_PER_SEC,
+ PA_USEC_PER_SEC*2,
+ true,
+ true,
+ 10,
+ 0,
+ false);
+ u->write_count = 0;
+
+ if (pa_streq(protocol, "TCP")) {
+ u->protocol = PA_RAOP_PROTOCOL_TCP;
+ } else if (pa_streq(protocol, "UDP")) {
+ u->protocol = PA_RAOP_PROTOCOL_UDP;
+ } else {
+ pa_log("Unsupported transport protocol argument: %s", protocol);
+ goto fail;
+ }
+
+ encryption = pa_modargs_get_value(ma, "encryption", NULL);
+ codec = pa_modargs_get_value(ma, "codec", NULL);
+
+ if (!encryption) {
+ u->encryption = PA_RAOP_ENCRYPTION_NONE;
+ } else if (pa_streq(encryption, "none")) {
+ u->encryption = PA_RAOP_ENCRYPTION_NONE;
+ } else if (pa_streq(encryption, "RSA")) {
+ u->encryption = PA_RAOP_ENCRYPTION_RSA;
+ } else {
+ pa_log("Unsupported encryption type argument: %s", encryption);
+ goto fail;
+ }
+
+ if (!codec) {
+ u->codec = PA_RAOP_CODEC_PCM;
+ } else if (pa_streq(codec, "PCM")) {
+ u->codec = PA_RAOP_CODEC_PCM;
+ } else if (pa_streq(codec, "ALAC")) {
+ u->codec = PA_RAOP_CODEC_ALAC;
+ } else {
+ pa_log("Unsupported audio codec argument: %s", codec);
+ goto fail;
+ }
+
+ pa_sink_new_data_init(&data);
+ data.driver = driver;
+ data.module = m;
+
+ if ((name = pa_modargs_get_value(ma, "sink_name", NULL))) {
+ pa_sink_new_data_set_name(&data, name);
+ } else {
+ char *nick;
+
+ if ((name = pa_modargs_get_value(ma, "name", NULL)))
+ nick = pa_sprintf_malloc("raop_client.%s", name);
+ else
+ nick = pa_sprintf_malloc("raop_client.%s", server);
+ pa_sink_new_data_set_name(&data, nick);
+ pa_xfree(nick);
+ }
+
+ pa_sink_new_data_set_sample_spec(&data, &ss);
+ pa_sink_new_data_set_channel_map(&data, &map);
+
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_STRING, server);
+ pa_proplist_sets(data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "music");
+ pa_proplist_setf(data.proplist, PA_PROP_DEVICE_DESCRIPTION, "RAOP sink '%s'", server);
+
+ if (pa_modargs_get_proplist(ma, "sink_properties", data.proplist, PA_UPDATE_REPLACE) < 0) {
+ pa_log("Invalid properties");
+ pa_sink_new_data_done(&data);
+ goto fail;
+ }
+
+ port = raop_create_port(u, server);
+ if (port == NULL) {
+ pa_log("Failed to create port object");
+ goto fail;
+ }
+
+ profile = raop_create_profile();
+ pa_hashmap_put(port->profiles, profile->name, profile);
+
+ description = pa_proplist_gets(data.proplist, PA_PROP_DEVICE_DESCRIPTION);
+ if (description == NULL)
+ description = server;
+
+ u->card = raop_create_card(m, port, profile, server, description);
+ if (u->card == NULL) {
+ pa_log("Failed to create card object");
+ goto fail;
+ }
+
+ data.card = u->card;
+ pa_hashmap_put(data.ports, port->name, port);
+
+ u->sink = pa_sink_new(m->core, &data, PA_SINK_LATENCY | PA_SINK_NETWORK);
+ pa_sink_new_data_done(&data);
+
+ if (!(u->sink)) {
+ pa_log("Failed to create sink object");
+ goto fail;
+ }
+
+ u->sink->parent.process_msg = sink_process_msg;
+ u->sink->set_state_in_io_thread = sink_set_state_in_io_thread_cb;
+ pa_sink_set_set_volume_callback(u->sink, sink_set_volume_cb);
+ pa_sink_set_set_mute_callback(u->sink, sink_set_mute_cb);
+ u->sink->userdata = u;
+ u->sink->set_port = sink_set_port_cb;
+
+ pa_sink_set_asyncmsgq(u->sink, u->thread_mq.inq);
+ pa_sink_set_rtpoll(u->sink, u->rtpoll);
+
+ u->raop = pa_raop_client_new(u->core, server, u->protocol, u->encryption, u->codec, u->autoreconnect);
+
+ if (!(u->raop)) {
+ pa_log("Failed to create RAOP client object");
+ goto fail;
+ }
+
+ /* The number of frames per blocks is not negotiable... */
+ pa_raop_client_get_frames_per_block(u->raop, &u->block_size);
+ u->block_size *= pa_frame_size(&ss);
+ pa_sink_set_max_request(u->sink, u->block_size);
+ u->block_usec = pa_bytes_to_usec(u->block_size, &u->sink->sample_spec);
+
+ pa_raop_client_set_state_callback(u->raop, raop_state_cb, u);
+
+ thread_name = pa_sprintf_malloc("raop-sink-%s", server);
+ if (!(u->thread = pa_thread_new(thread_name, thread_func, u))) {
+ pa_log("Failed to create sink thread");
+ goto fail;
+ }
+ pa_xfree(thread_name);
+ thread_name = NULL;
+
+ pa_sink_put(u->sink);
+
+ /* username = pa_modargs_get_value(ma, "username", NULL); */
+ password = pa_modargs_get_value(ma, "password", NULL);
+ pa_raop_client_authenticate(u->raop, password );
+
+ return u->sink;
+
+fail:
+ pa_xfree(thread_name);
+
+ if (u)
+ userdata_free(u);
+
+ return NULL;
+}
+
+static void userdata_free(struct userdata *u) {
+ pa_assert(u);
+
+ 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->sink)
+ pa_sink_unref(u->sink);
+ u->sink = NULL;
+
+ if (u->rtpoll_item)
+ pa_rtpoll_item_free(u->rtpoll_item);
+ if (u->rtpoll)
+ pa_rtpoll_free(u->rtpoll);
+ u->rtpoll_item = NULL;
+ u->rtpoll = NULL;
+
+ if (u->memchunk.memblock)
+ pa_memblock_unref(u->memchunk.memblock);
+
+ if (u->raop)
+ pa_raop_client_free(u->raop);
+ u->raop = NULL;
+
+ if (u->smoother)
+ pa_smoother_free(u->smoother);
+ u->smoother = NULL;
+
+ if (u->card)
+ pa_card_free(u->card);
+ if (u->server)
+ pa_xfree(u->server);
+
+ pa_xfree(u);
+}
+
+void pa_raop_sink_free(pa_sink *s) {
+ struct userdata *u;
+
+ pa_sink_assert_ref(s);
+ pa_assert_se(u = s->userdata);
+
+ userdata_free(u);
+}
diff --git a/src/modules/raop/raop-sink.h b/src/modules/raop/raop-sink.h
new file mode 100644
index 0000000..dfa2f0c
--- /dev/null
+++ b/src/modules/raop/raop-sink.h
@@ -0,0 +1,33 @@
+#ifndef fooraopsinkfoo
+#define fooraopsinkfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2013 Martin Blanchard
+
+ 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/module.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/sink.h>
+
+pa_sink* pa_raop_sink_new(pa_module *m, pa_modargs *ma, const char *driver);
+
+void pa_raop_sink_free(pa_sink *s);
+
+#endif
diff --git a/src/modules/raop/raop-util.c b/src/modules/raop/raop-util.c
new file mode 100644
index 0000000..febc204
--- /dev/null
+++ b/src/modules/raop/raop-util.c
@@ -0,0 +1,211 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2008 Colin Guthrie
+ Copyright Kungliga Tekniska högskolan
+ Copyright 2013 Martin Blanchard
+
+ 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, see <http://www.gnu.org/licenses/>.
+***/
+
+/***
+ The base64 implementation was originally inspired by a file developed
+ by Kungliga Tekniska högskolan.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <openssl/err.h>
+#include <openssl/md5.h>
+
+#include <pulse/xmalloc.h>
+
+#include <pulsecore/core-util.h>
+#include <pulsecore/macro.h>
+
+#include "raop-util.h"
+
+#ifndef MD5_DIGEST_LENGTH
+#define MD5_DIGEST_LENGTH 16
+#endif
+
+#define MD5_HASH_LENGTH (2*MD5_DIGEST_LENGTH)
+
+#define BASE64_DECODE_ERROR 0xffffffff
+
+static const char base64_chars[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+static int char_position(char c) {
+ if (c >= 'A' && c <= 'Z')
+ return c - 'A' + 0;
+ if (c >= 'a' && c <= 'z')
+ return c - 'a' + 26;
+ if (c >= '0' && c <= '9')
+ return c - '0' + 52;
+ if (c == '+')
+ return 62;
+ if (c == '/')
+ return 63;
+
+ return -1;
+}
+
+static unsigned int token_decode(const char *token) {
+ unsigned int val = 0;
+ int marker = 0;
+ int i;
+
+ if (strlen(token) < 4)
+ return BASE64_DECODE_ERROR;
+ for (i = 0; i < 4; i++) {
+ val *= 64;
+ if (token[i] == '=')
+ marker++;
+ else if (marker > 0)
+ return BASE64_DECODE_ERROR;
+ else {
+ int lpos = char_position(token[i]);
+ if (lpos < 0)
+ return BASE64_DECODE_ERROR;
+ val += lpos;
+ }
+ }
+
+ if (marker > 2)
+ return BASE64_DECODE_ERROR;
+
+ return (marker << 24) | val;
+}
+
+int pa_raop_base64_encode(const void *data, int len, char **str) {
+ const unsigned char *q;
+ char *p, *s = NULL;
+ int i, c;
+
+ pa_assert(data);
+ pa_assert(str);
+
+ p = s = pa_xnew(char, len * 4 / 3 + 4);
+ q = (const unsigned char *) data;
+ for (i = 0; i < len;) {
+ c = q[i++];
+ c *= 256;
+ if (i < len)
+ c += q[i];
+ i++;
+ c *= 256;
+ if (i < len)
+ c += q[i];
+ i++;
+ p[0] = base64_chars[(c & 0x00fc0000) >> 18];
+ p[1] = base64_chars[(c & 0x0003f000) >> 12];
+ p[2] = base64_chars[(c & 0x00000fc0) >> 6];
+ p[3] = base64_chars[(c & 0x0000003f) >> 0];
+ if (i > len)
+ p[3] = '=';
+ if (i > len + 1)
+ p[2] = '=';
+ p += 4;
+ }
+
+ *p = 0;
+ *str = s;
+ return strlen(s);
+}
+
+int pa_raop_base64_decode(const char *str, void *data) {
+ const char *p;
+ unsigned char *q;
+
+ pa_assert(str);
+ pa_assert(data);
+
+ q = data;
+ for (p = str; *p && (*p == '=' || strchr(base64_chars, *p)); p += 4) {
+ unsigned int val = token_decode(p);
+ unsigned int marker = (val >> 24) & 0xff;
+ if (val == BASE64_DECODE_ERROR)
+ return -1;
+ *q++ = (val >> 16) & 0xff;
+ if (marker < 2)
+ *q++ = (val >> 8) & 0xff;
+ if (marker < 1)
+ *q++ = val & 0xff;
+ }
+
+ return q - (unsigned char *) data;
+}
+
+int pa_raop_md5_hash(const char *data, int len, char **str) {
+ unsigned char d[MD5_DIGEST_LENGTH];
+ char *s = NULL;
+ int i;
+
+ pa_assert(data);
+ pa_assert(str);
+
+ MD5((unsigned char*) data, len, d);
+ s = pa_xnew(char, MD5_HASH_LENGTH);
+ for (i = 0; i < MD5_DIGEST_LENGTH; i++)
+ sprintf(&s[2*i], "%02x", (unsigned int) d[i]);
+
+ *str = s;
+ s[MD5_HASH_LENGTH] = 0;
+ return strlen(s);
+}
+
+int pa_raop_basic_response(const char *user, const char *pwd, char **str) {
+ char *tmp, *B = NULL;
+
+ pa_assert(str);
+
+ tmp = pa_sprintf_malloc("%s:%s", user, pwd);
+ pa_raop_base64_encode(tmp, strlen(tmp), &B);
+ pa_xfree(tmp);
+
+ *str = B;
+ return strlen(B);
+}
+
+int pa_raop_digest_response(const char *user, const char *realm, const char *password,
+ const char *nonce, const char *uri, char **str) {
+ char *A1, *HA1, *A2, *HA2;
+ char *tmp, *KD = NULL;
+
+ pa_assert(str);
+
+ A1 = pa_sprintf_malloc("%s:%s:%s", user, realm, password);
+ pa_raop_md5_hash(A1, strlen(A1), &HA1);
+ pa_xfree(A1);
+
+ A2 = pa_sprintf_malloc("OPTIONS:%s", uri);
+ pa_raop_md5_hash(A2, strlen(A2), &HA2);
+ pa_xfree(A2);
+
+ tmp = pa_sprintf_malloc("%s:%s:%s", HA1, nonce, HA2);
+ pa_raop_md5_hash(tmp, strlen(tmp), &KD);
+ pa_xfree(tmp);
+
+ pa_xfree(HA1);
+ pa_xfree(HA2);
+
+ *str = KD;
+ return strlen(KD);
+}
diff --git a/src/modules/raop/raop-util.h b/src/modules/raop/raop-util.h
new file mode 100644
index 0000000..7c25e5c
--- /dev/null
+++ b/src/modules/raop/raop-util.h
@@ -0,0 +1,41 @@
+#ifndef fooraoputilfoo
+#define fooraoputilfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2008 Colin Guthrie
+ Copyright Kungliga Tekniska högskolan
+ Copyright 2013 Martin Blanchard
+
+ 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, see <http://www.gnu.org/licenses/>.
+***/
+
+/***
+ This file was originally inspired by a file developed by
+ Kungliga Tekniska högskolan.
+***/
+
+#define RAOP_DEFAULT_LATENCY 2000 /* msec */
+
+int pa_raop_base64_encode(const void *data, int len, char **str);
+int pa_raop_base64_decode(const char *str, void *data);
+
+int pa_raop_md5_hash(const char *data, int len, char **str);
+
+int pa_raop_basic_response(const char *user, const char *pwd, char **str);
+int pa_raop_digest_response(const char *user, const char *realm, const char *password,
+ const char *nonce, const char *uri, char **str);
+
+#endif