diff options
Diffstat (limited to 'src/modules/raop')
-rw-r--r-- | src/modules/raop/meson.build | 30 | ||||
-rw-r--r-- | src/modules/raop/module-raop-discover.c | 537 | ||||
-rw-r--r-- | src/modules/raop/module-raop-sink.c | 112 | ||||
-rw-r--r-- | src/modules/raop/raop-client.c | 1854 | ||||
-rw-r--r-- | src/modules/raop/raop-client.h | 86 | ||||
-rw-r--r-- | src/modules/raop/raop-crypto.c | 214 | ||||
-rw-r--r-- | src/modules/raop/raop-crypto.h | 35 | ||||
-rw-r--r-- | src/modules/raop/raop-packet-buffer.c | 161 | ||||
-rw-r--r-- | src/modules/raop/raop-packet-buffer.h | 40 | ||||
-rw-r--r-- | src/modules/raop/raop-sink.c | 968 | ||||
-rw-r--r-- | src/modules/raop/raop-sink.h | 33 | ||||
-rw-r--r-- | src/modules/raop/raop-util.c | 211 | ||||
-rw-r--r-- | src/modules/raop/raop-util.h | 41 |
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, ¤t); + while ((token = pa_split(wath, comma, ¤t))) { + 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 |