mirror of
https://gitlab.freedesktop.org/pulseaudio/pulseaudio.git
synced 2025-10-29 05:40:23 -04:00
Merge commit 'coling/airtunes-0.9.13'
This commit is contained in:
commit
8e3e88df8b
12 changed files with 2753 additions and 2 deletions
41
configure.ac
41
configure.ac
|
|
@ -1000,6 +1000,41 @@ AC_SUBST(POLKIT_LIBS)
|
|||
AC_SUBST(HAVE_POLKIT)
|
||||
AM_CONDITIONAL([HAVE_POLKIT], [test "x$HAVE_POLKIT" = x1])
|
||||
|
||||
#### OpenSSL support (optional) ####
|
||||
|
||||
AC_ARG_ENABLE([openssl],
|
||||
AC_HELP_STRING([--disable-openssl], [Disable OpenSSL support (used for Airtunes/RAOP)]),
|
||||
[
|
||||
case "${enableval}" in
|
||||
yes) openssl=yes ;;
|
||||
no) openssl=no ;;
|
||||
*) AC_MSG_ERROR(bad value ${enableval} for --disable-openssl) ;;
|
||||
esac
|
||||
],
|
||||
[openssl=auto])
|
||||
|
||||
if test "x${openssl}" != xno ; then
|
||||
|
||||
PKG_CHECK_MODULES(OPENSSL, [ openssl > 0.9 ],
|
||||
[
|
||||
HAVE_OPENSSL=1
|
||||
AC_DEFINE([HAVE_OPENSSL], 1, [Have OpenSSL])
|
||||
],
|
||||
[
|
||||
HAVE_OPENSSL=0
|
||||
if test "x$openssl" = xyes ; then
|
||||
AC_MSG_ERROR([*** OpenSSL support not found])
|
||||
fi
|
||||
])
|
||||
else
|
||||
HAVE_OPENSSL=0
|
||||
fi
|
||||
|
||||
AC_SUBST(OPENSSL_CFLAGS)
|
||||
AC_SUBST(OPENSSL_LIBS)
|
||||
AC_SUBST(HAVE_OPENSSL)
|
||||
AM_CONDITIONAL([HAVE_OPENSSL], [test "x$HAVE_OPENSSL" = x1])
|
||||
|
||||
### Build and Install man pages ###
|
||||
AC_ARG_ENABLE(manpages,
|
||||
AS_HELP_STRING([--disable-manpages],[Disable building and installation of man pages]),
|
||||
|
|
@ -1201,6 +1236,11 @@ if test "x${HAVE_POLKIT}" = "x1" ; then
|
|||
ENABLE_POLKIT=yes
|
||||
fi
|
||||
|
||||
ENABLE_OPENSSL=no
|
||||
if test "x${HAVE_OPENSSL}" = "x1" ; then
|
||||
ENABLE_OPENSSL=yes
|
||||
fi
|
||||
|
||||
ENABLE_PER_USER_ESOUND_SOCKET=no
|
||||
if test "x$per_user_esound_socket" = "x1" ; then
|
||||
ENABLE_PER_USER_ESOUND_SOCKET=yes
|
||||
|
|
@ -1232,6 +1272,7 @@ echo "
|
|||
Enable TCP Wrappers: ${ENABLE_TCPWRAP}
|
||||
Enable libsamplerate: ${ENABLE_LIBSAMPLERATE}
|
||||
Enable PolicyKit: ${ENABLE_POLKIT}
|
||||
Enable OpenSSL (for Airtunes): ${ENABLE_OPENSSL}
|
||||
System User: ${PA_SYSTEM_USER}
|
||||
System Group: ${PA_SYSTEM_GROUP}
|
||||
Realtime Group: ${PA_REALTIME_GROUP}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ AM_CFLAGS += -DPA_MACHINE_ID=\"$(localstatedir)/lib/dbus/machine-id\"
|
|||
# This cool debug trap works on i386/gcc only
|
||||
AM_CFLAGS += '-DDEBUG_TRAP=__asm__("int $$3")'
|
||||
|
||||
if HAVE_OPENSSL
|
||||
AM_CFLAGS += -I$(top_builddir)/src/modules/raop
|
||||
endif
|
||||
|
||||
AM_LIBADD = $(PTHREAD_LIBS) $(INTLLIBS)
|
||||
AM_LDADD = $(PTHREAD_LIBS) $(INTLLIBS)
|
||||
|
||||
|
|
@ -89,6 +93,7 @@ PA_THREAD_OBJS = \
|
|||
pulsecore/semaphore-posix.c pulsecore/semaphore.h
|
||||
endif
|
||||
|
||||
|
||||
###################################
|
||||
# Extra files #
|
||||
###################################
|
||||
|
|
@ -1004,9 +1009,21 @@ libsocket_util_la_SOURCES = \
|
|||
libsocket_util_la_LDFLAGS = -avoid-version
|
||||
libsocket_util_la_LIBADD = $(AM_LIBADD) $(WINSOCK_LIBS) libpulsecore.la
|
||||
|
||||
librtp_la_SOURCES = modules/rtp/rtp.c modules/rtp/rtp.h modules/rtp/sdp.c modules/rtp/sdp.h modules/rtp/sap.c modules/rtp/sap.h
|
||||
librtp_la_SOURCES = \
|
||||
modules/rtp/rtp.c modules/rtp/rtp.h \
|
||||
modules/rtp/sdp.c modules/rtp/sdp.h \
|
||||
modules/rtp/sap.c modules/rtp/sap.h \
|
||||
modules/rtp/rtsp_client.c modules/rtp/rtsp_client.h \
|
||||
modules/rtp/headerlist.c modules/rtp/headerlist.h
|
||||
librtp_la_LDFLAGS = -avoid-version
|
||||
librtp_la_LIBADD = $(AM_LIBADD) libpulsecore.la
|
||||
librtp_la_LIBADD = $(AM_LIBADD) libsocket-util.la libiochannel.la libsocket-client.la libioline.la libpulsecore.la
|
||||
|
||||
libraop_la_SOURCES = \
|
||||
modules/raop/raop_client.c modules/raop/raop_client.h \
|
||||
modules/raop/base64.c modules/raop/base64.h
|
||||
libraop_la_CFLAGS = $(AM_CFLAGS) $(OPENSSL_CFLAGS)
|
||||
libraop_la_LDFLAGS = -avoid-version
|
||||
libraop_la_LIBADD = $(AM_LIBADD) $(OPENSSL_LIBS) libsocket-util.la libiochannel.la libsocket-client.la libioline.la libpulsecore.la librtp.la
|
||||
|
||||
# X11
|
||||
|
||||
|
|
@ -1178,6 +1195,17 @@ pulselibexec_PROGRAMS += \
|
|||
proximity-helper
|
||||
endif
|
||||
|
||||
if HAVE_OPENSSL
|
||||
modlibexec_LTLIBRARIES += \
|
||||
libraop.la \
|
||||
module-raop-sink.la
|
||||
if HAVE_AVAHI
|
||||
modlibexec_LTLIBRARIES += \
|
||||
module-raop-discover.la
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
# These are generated by a M4 script
|
||||
|
||||
SYMDEF_FILES = \
|
||||
|
|
@ -1234,6 +1262,8 @@ SYMDEF_FILES = \
|
|||
modules/bluetooth/module-bluetooth-proximity-symdef.h \
|
||||
modules/bluetooth/module-bluetooth-discover-symdef.h \
|
||||
modules/bluetooth/module-bluetooth-device-symdef.h \
|
||||
modules/module-raop-sink-symdef.h \
|
||||
modules/module-raop-discover-symdef.h \
|
||||
modules/gconf/module-gconf-symdef.h \
|
||||
modules/module-position-event-sounds-symdef.h \
|
||||
modules/module-console-kit-symdef.h \
|
||||
|
|
@ -1603,6 +1633,17 @@ module_bluetooth_device_la_LDFLAGS = -module -avoid-version
|
|||
module_bluetooth_device_la_LIBADD = $(AM_LIBADD) $(DBUS_LIBS) libpulsecore.la libdbus-util.la libbluetooth-ipc.la libbluetooth-sbc.la libsocket-util.la
|
||||
module_bluetooth_device_la_CFLAGS = $(AM_CFLAGS) $(DBUS_CFLAGS)
|
||||
|
||||
# Apple Airtunes/RAOP
|
||||
module_raop_sink_la_SOURCES = modules/module-raop-sink.c
|
||||
module_raop_sink_la_LDFLAGS = -module -avoid-version
|
||||
module_raop_sink_la_LIBADD = $(AM_LIBADD) libpulsecore.la libiochannel.la librtp.la libraop.la
|
||||
|
||||
module_raop_discover_la_SOURCES = modules/module-raop-discover.c
|
||||
module_raop_discover_la_LDFLAGS = -module -avoid-version
|
||||
module_raop_discover_la_LIBADD = $(AM_LIBADD) $(AVAHI_LIBS) libavahi-wrap.la libpulsecore.la
|
||||
module_raop_discover_la_CFLAGS = $(AM_CFLAGS) $(AVAHI_CFLAGS)
|
||||
|
||||
|
||||
###################################
|
||||
# Some minor stuff #
|
||||
###################################
|
||||
|
|
|
|||
380
src/modules/module-raop-discover.c
Normal file
380
src/modules/module-raop-discover.c
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
/***
|
||||
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 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 <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 <pulse/util.h>
|
||||
|
||||
#include <pulsecore/sink.h>
|
||||
#include <pulsecore/source.h>
|
||||
#include <pulsecore/native-common.h>
|
||||
#include <pulsecore/core-util.h>
|
||||
#include <pulsecore/log.h>
|
||||
#include <pulsecore/core-subscribe.h>
|
||||
#include <pulsecore/hashmap.h>
|
||||
#include <pulsecore/modargs.h>
|
||||
#include <pulsecore/namereg.h>
|
||||
#include <pulsecore/avahi-wrap.h>
|
||||
|
||||
#include "module-raop-discover-symdef.h"
|
||||
|
||||
PA_MODULE_AUTHOR("Colin Guthrie");
|
||||
PA_MODULE_DESCRIPTION("mDNS/DNS-SD Service Discovery of Airtunes");
|
||||
PA_MODULE_VERSION(PACKAGE_VERSION);
|
||||
PA_MODULE_LOAD_ONCE(TRUE);
|
||||
|
||||
#define SERVICE_TYPE_SINK "_raop._tcp"
|
||||
|
||||
static const char* const valid_modargs[] = {
|
||||
NULL
|
||||
};
|
||||
|
||||
struct tunnel {
|
||||
AvahiIfIndex interface;
|
||||
AvahiProtocol protocol;
|
||||
char *name, *type, *domain;
|
||||
uint32_t module_index;
|
||||
};
|
||||
|
||||
struct userdata {
|
||||
pa_core *core;
|
||||
pa_module *module;
|
||||
AvahiPoll *avahi_poll;
|
||||
AvahiClient *client;
|
||||
AvahiServiceBrowser *sink_browser;
|
||||
|
||||
pa_hashmap *tunnels;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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)));
|
||||
else {
|
||||
char *device = NULL, *dname, *vname, *args;
|
||||
char at[AVAHI_ADDRESS_STR_MAX];
|
||||
AvahiStringList *l;
|
||||
pa_module *m;
|
||||
|
||||
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 (strcmp(key, "device") == 0) {
|
||||
pa_xfree(device);
|
||||
device = value;
|
||||
value = NULL;
|
||||
}
|
||||
avahi_free(key);
|
||||
avahi_free(value);
|
||||
}
|
||||
|
||||
if (device)
|
||||
dname = pa_sprintf_malloc("airtunes.%s.%s", host_name, device);
|
||||
else
|
||||
dname = pa_sprintf_malloc("airtunes.%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);
|
||||
goto finish;
|
||||
}
|
||||
pa_xfree(dname);
|
||||
|
||||
/*
|
||||
TODO: allow this syntax of server name in things....
|
||||
args = pa_sprintf_malloc("server=[%s]:%u "
|
||||
"sink_name=%s",
|
||||
avahi_address_snprint(at, sizeof(at), a), port,
|
||||
vname);*/
|
||||
args = pa_sprintf_malloc("server=%s "
|
||||
"sink_name=%s",
|
||||
avahi_address_snprint(at, sizeof(at), a),
|
||||
vname);
|
||||
|
||||
pa_log_debug("Loading module-raop-sink with arguments '%s'", args);
|
||||
|
||||
if ((m = pa_module_load(u->core, "module-raop-sink", args))) {
|
||||
tnl->module_index = m->index;
|
||||
pa_hashmap_put(u->tunnels, tnl, tnl);
|
||||
tnl = NULL;
|
||||
}
|
||||
|
||||
pa_xfree(vname);
|
||||
pa_xfree(args);
|
||||
avahi_free(device);
|
||||
}
|
||||
|
||||
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 destory
|
||||
* it from the callback */
|
||||
|
||||
} else if (event == AVAHI_BROWSER_REMOVE) {
|
||||
struct tunnel *t2;
|
||||
|
||||
if ((t2 = pa_hashmap_get(u->tunnels, t))) {
|
||||
pa_module_unload_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.");
|
||||
|
||||
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: ;
|
||||
}
|
||||
}
|
||||
|
||||
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_xnew(struct userdata, 1);
|
||||
u->core = m->core;
|
||||
u->module = m;
|
||||
u->sink_browser = NULL;
|
||||
|
||||
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_by_index(u->core, t->module_index, TRUE);
|
||||
tunnel_free(t);
|
||||
}
|
||||
|
||||
pa_hashmap_free(u->tunnels, NULL, NULL);
|
||||
}
|
||||
|
||||
pa_xfree(u);
|
||||
}
|
||||
675
src/modules/module-raop-sink.c
Normal file
675
src/modules/module-raop-sink.c
Normal file
|
|
@ -0,0 +1,675 @@
|
|||
/***
|
||||
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 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 <sys/stat.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#include <poll.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/xmalloc.h>
|
||||
#include <pulse/timeval.h>
|
||||
|
||||
#include <pulsecore/core-error.h>
|
||||
#include <pulsecore/iochannel.h>
|
||||
#include <pulsecore/sink.h>
|
||||
#include <pulsecore/module.h>
|
||||
#include <pulsecore/core-util.h>
|
||||
#include <pulsecore/modargs.h>
|
||||
#include <pulsecore/log.h>
|
||||
#include <pulsecore/socket-client.h>
|
||||
#include <pulsecore/authkey.h>
|
||||
#include <pulsecore/thread-mq.h>
|
||||
#include <pulsecore/thread.h>
|
||||
#include <pulsecore/time-smoother.h>
|
||||
#include <pulsecore/rtclock.h>
|
||||
#include <pulsecore/socket-util.h>
|
||||
|
||||
#include "module-raop-sink-symdef.h"
|
||||
#include "rtp.h"
|
||||
#include "sdp.h"
|
||||
#include "sap.h"
|
||||
#include "raop_client.h"
|
||||
|
||||
PA_MODULE_AUTHOR("Colin Guthrie");
|
||||
PA_MODULE_DESCRIPTION("RAOP Sink (Apple Airtunes)");
|
||||
PA_MODULE_VERSION(PACKAGE_VERSION);
|
||||
PA_MODULE_LOAD_ONCE(FALSE);
|
||||
PA_MODULE_USAGE(
|
||||
"sink_name=<name for the sink> "
|
||||
"server=<address> "
|
||||
"format=<sample format> "
|
||||
"channels=<number of channels> "
|
||||
"rate=<sample rate>");
|
||||
|
||||
#define DEFAULT_SINK_NAME "airtunes"
|
||||
|
||||
struct userdata {
|
||||
pa_core *core;
|
||||
pa_module *module;
|
||||
pa_sink *sink;
|
||||
|
||||
pa_thread_mq thread_mq;
|
||||
pa_rtpoll *rtpoll;
|
||||
pa_rtpoll_item *rtpoll_item;
|
||||
pa_thread *thread;
|
||||
|
||||
pa_memchunk raw_memchunk;
|
||||
pa_memchunk encoded_memchunk;
|
||||
|
||||
void *write_data;
|
||||
size_t write_length, write_index;
|
||||
|
||||
void *read_data;
|
||||
size_t read_length, read_index;
|
||||
|
||||
pa_usec_t latency;
|
||||
|
||||
pa_volume_t volume;
|
||||
pa_bool_t muted;
|
||||
|
||||
/*esd_format_t format;*/
|
||||
int32_t rate;
|
||||
|
||||
pa_smoother *smoother;
|
||||
int fd;
|
||||
|
||||
int64_t offset;
|
||||
int64_t encoding_overhead;
|
||||
int32_t next_encoding_overhead;
|
||||
double encoding_ratio;
|
||||
|
||||
pa_raop_client *raop;
|
||||
|
||||
size_t block_size;
|
||||
};
|
||||
|
||||
static const char* const valid_modargs[] = {
|
||||
"server",
|
||||
"rate",
|
||||
"format",
|
||||
"channels",
|
||||
"sink_name",
|
||||
NULL
|
||||
};
|
||||
|
||||
enum {
|
||||
SINK_MESSAGE_PASS_SOCKET = PA_SINK_MESSAGE_MAX,
|
||||
SINK_MESSAGE_RIP_SOCKET
|
||||
};
|
||||
|
||||
static void on_connection(PA_GCC_UNUSED int fd, void*userdata) {
|
||||
struct userdata *u = userdata;
|
||||
pa_assert(u);
|
||||
|
||||
pa_assert(u->fd < 0);
|
||||
u->fd = fd;
|
||||
|
||||
/* Set the initial volume */
|
||||
pa_raop_client_set_volume(u->raop, u->volume);
|
||||
|
||||
pa_log_debug("Connection authenticated, handing fd to IO thread...");
|
||||
|
||||
pa_asyncmsgq_post(u->thread_mq.inq, PA_MSGOBJECT(u->sink), SINK_MESSAGE_PASS_SOCKET, NULL, 0, NULL, NULL);
|
||||
}
|
||||
|
||||
static void on_close(void*userdata) {
|
||||
struct userdata *u = userdata;
|
||||
pa_assert(u);
|
||||
|
||||
pa_log_debug("Connection closed, informing IO thread...");
|
||||
|
||||
pa_asyncmsgq_post(u->thread_mq.inq, PA_MSGOBJECT(u->sink), SINK_MESSAGE_RIP_SOCKET, NULL, 0, NULL, NULL);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
switch (code) {
|
||||
|
||||
case PA_SINK_MESSAGE_SET_STATE:
|
||||
|
||||
switch ((pa_sink_state_t) PA_PTR_TO_UINT(data)) {
|
||||
|
||||
case PA_SINK_SUSPENDED:
|
||||
pa_assert(PA_SINK_IS_OPENED(u->sink->thread_info.state));
|
||||
|
||||
pa_smoother_pause(u->smoother, pa_rtclock_usec());
|
||||
|
||||
/* Issue a FLUSH if we are connected */
|
||||
if (u->fd >= 0) {
|
||||
pa_raop_flush(u->raop);
|
||||
}
|
||||
break;
|
||||
|
||||
case PA_SINK_IDLE:
|
||||
case PA_SINK_RUNNING:
|
||||
|
||||
if (u->sink->thread_info.state == PA_SINK_SUSPENDED) {
|
||||
pa_smoother_resume(u->smoother, pa_rtclock_usec());
|
||||
|
||||
/* The connection can be closed when idle, so check to
|
||||
see if we need to reestablish it */
|
||||
if (u->fd < 0)
|
||||
pa_raop_connect(u->raop);
|
||||
else
|
||||
pa_raop_flush(u->raop);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PA_SINK_UNLINKED:
|
||||
case PA_SINK_INIT:
|
||||
;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PA_SINK_MESSAGE_GET_LATENCY: {
|
||||
pa_usec_t w, r;
|
||||
|
||||
r = pa_smoother_get(u->smoother, pa_rtclock_usec());
|
||||
w = pa_bytes_to_usec((u->offset - u->encoding_overhead + (u->encoded_memchunk.length / u->encoding_ratio)), &u->sink->sample_spec);
|
||||
|
||||
*((pa_usec_t*) data) = w > r ? w - r : 0;
|
||||
break;
|
||||
}
|
||||
|
||||
case SINK_MESSAGE_PASS_SOCKET: {
|
||||
struct pollfd *pollfd;
|
||||
|
||||
pa_assert(!u->rtpoll_item);
|
||||
|
||||
u->rtpoll_item = pa_rtpoll_item_new(u->rtpoll, PA_RTPOLL_NEVER, 1);
|
||||
pollfd = pa_rtpoll_item_get_pollfd(u->rtpoll_item, NULL);
|
||||
pollfd->fd = u->fd;
|
||||
pollfd->events = POLLOUT;
|
||||
/*pollfd->events = */pollfd->revents = 0;
|
||||
|
||||
if (u->sink->thread_info.state == PA_SINK_SUSPENDED) {
|
||||
/* Our stream has been suspended so we just flush it.... */
|
||||
pa_raop_flush(u->raop);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case SINK_MESSAGE_RIP_SOCKET: {
|
||||
pa_assert(u->fd >= 0);
|
||||
|
||||
pa_close(u->fd);
|
||||
u->fd = -1;
|
||||
|
||||
if (u->sink->thread_info.state == PA_SINK_SUSPENDED) {
|
||||
|
||||
pa_log_debug("RTSP control connection closed, but we're suspended so let's not worry about it... we'll open it again later");
|
||||
|
||||
if (u->rtpoll_item)
|
||||
pa_rtpoll_item_free(u->rtpoll_item);
|
||||
u->rtpoll_item = NULL;
|
||||
} else {
|
||||
/* Quesiton: is this valid here: or should we do some sort of:
|
||||
return pa_sink_process_msg(PA_MSGOBJECT(u->core), PA_CORE_MESSAGE_UNLOAD_MODULE, u->module, 0, NULL);
|
||||
?? */
|
||||
pa_module_unload_request(u->module, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return pa_sink_process_msg(o, code, data, offset, chunk);
|
||||
}
|
||||
|
||||
static int sink_get_volume_cb(pa_sink *s) {
|
||||
struct userdata *u = s->userdata;
|
||||
int i;
|
||||
|
||||
pa_assert(u);
|
||||
|
||||
for (i = 0; i < s->sample_spec.channels; i++) {
|
||||
s->volume.values[i] = u->volume;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sink_set_volume_cb(pa_sink *s) {
|
||||
struct userdata *u = s->userdata;
|
||||
int rv;
|
||||
|
||||
pa_assert(u);
|
||||
|
||||
/* If we're muted, we fake it */
|
||||
if (u->muted)
|
||||
return 0;
|
||||
|
||||
pa_assert(s->sample_spec.channels > 0);
|
||||
|
||||
/* Avoid pointless volume sets */
|
||||
if (u->volume == s->volume.values[0])
|
||||
return 0;
|
||||
|
||||
rv = pa_raop_client_set_volume(u->raop, s->volume.values[0]);
|
||||
if (0 == rv)
|
||||
u->volume = s->volume.values[0];
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
static int sink_get_mute_cb(pa_sink *s) {
|
||||
struct userdata *u = s->userdata;
|
||||
|
||||
pa_assert(u);
|
||||
|
||||
s->muted = u->muted;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sink_set_mute_cb(pa_sink *s) {
|
||||
struct userdata *u = s->userdata;
|
||||
int rv;
|
||||
|
||||
pa_assert(u);
|
||||
|
||||
rv = pa_raop_client_set_volume(u->raop, (s->muted ? PA_VOLUME_MUTED : u->volume));
|
||||
u->muted = s->muted;
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void thread_func(void *userdata) {
|
||||
struct userdata *u = userdata;
|
||||
int write_type = 0;
|
||||
pa_memchunk silence;
|
||||
uint32_t silence_overhead = 0;
|
||||
double silence_ratio = 0;
|
||||
|
||||
pa_assert(u);
|
||||
|
||||
pa_log_debug("Thread starting up");
|
||||
|
||||
pa_thread_mq_install(&u->thread_mq);
|
||||
pa_rtpoll_install(u->rtpoll);
|
||||
|
||||
pa_smoother_set_time_offset(u->smoother, pa_rtclock_usec());
|
||||
|
||||
/* Create a chunk of memory that is our encoded silence sample. */
|
||||
pa_memchunk_reset(&silence);
|
||||
|
||||
for (;;) {
|
||||
int ret;
|
||||
|
||||
if (PA_SINK_IS_OPENED(u->sink->thread_info.state))
|
||||
if (u->sink->thread_info.rewind_requested)
|
||||
pa_sink_process_rewind(u->sink, 0);
|
||||
|
||||
if (u->rtpoll_item) {
|
||||
struct pollfd *pollfd;
|
||||
pollfd = pa_rtpoll_item_get_pollfd(u->rtpoll_item, NULL);
|
||||
|
||||
/* Render some data and write it to the fifo */
|
||||
if (/*PA_SINK_IS_OPENED(u->sink->thread_info.state) && */pollfd->revents) {
|
||||
pa_usec_t usec;
|
||||
int64_t n;
|
||||
void *p;
|
||||
|
||||
if (!silence.memblock) {
|
||||
pa_memchunk silence_tmp;
|
||||
|
||||
pa_memchunk_reset(&silence_tmp);
|
||||
silence_tmp.memblock = pa_memblock_new(u->core->mempool, 4096);
|
||||
silence_tmp.length = 4096;
|
||||
p = pa_memblock_acquire(silence_tmp.memblock);
|
||||
memset(p, 0, 4096);
|
||||
pa_memblock_release(silence_tmp.memblock);
|
||||
pa_raop_client_encode_sample(u->raop, &silence_tmp, &silence);
|
||||
pa_assert(0 == silence_tmp.length);
|
||||
silence_overhead = silence_tmp.length - 4096;
|
||||
silence_ratio = silence_tmp.length / 4096;
|
||||
pa_memblock_unref(silence_tmp.memblock);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
ssize_t l;
|
||||
|
||||
if (u->encoded_memchunk.length <= 0) {
|
||||
if (u->encoded_memchunk.memblock)
|
||||
pa_memblock_unref(u->encoded_memchunk.memblock);
|
||||
if (PA_SINK_IS_OPENED(u->sink->thread_info.state)) {
|
||||
size_t rl;
|
||||
|
||||
/* We render real data */
|
||||
if (u->raw_memchunk.length <= 0) {
|
||||
if (u->raw_memchunk.memblock)
|
||||
pa_memblock_unref(u->raw_memchunk.memblock);
|
||||
pa_memchunk_reset(&u->raw_memchunk);
|
||||
|
||||
/* Grab unencoded data */
|
||||
pa_sink_render(u->sink, u->block_size, &u->raw_memchunk);
|
||||
}
|
||||
pa_assert(u->raw_memchunk.length > 0);
|
||||
|
||||
/* Encode it */
|
||||
rl = u->raw_memchunk.length;
|
||||
u->encoding_overhead += u->next_encoding_overhead;
|
||||
pa_raop_client_encode_sample(u->raop, &u->raw_memchunk, &u->encoded_memchunk);
|
||||
u->next_encoding_overhead = (u->encoded_memchunk.length - (rl - u->raw_memchunk.length));
|
||||
u->encoding_ratio = u->encoded_memchunk.length / (rl - u->raw_memchunk.length);
|
||||
} else {
|
||||
/* We render some silence into our memchunk */
|
||||
memcpy(&u->encoded_memchunk, &silence, sizeof(pa_memchunk));
|
||||
pa_memblock_ref(silence.memblock);
|
||||
|
||||
/* Calculate/store some values to be used with the smoother */
|
||||
u->next_encoding_overhead = silence_overhead;
|
||||
u->encoding_ratio = silence_ratio;
|
||||
}
|
||||
}
|
||||
pa_assert(u->encoded_memchunk.length > 0);
|
||||
|
||||
p = pa_memblock_acquire(u->encoded_memchunk.memblock);
|
||||
l = pa_write(u->fd, (uint8_t*) p + u->encoded_memchunk.index, u->encoded_memchunk.length, &write_type);
|
||||
pa_memblock_release(u->encoded_memchunk.memblock);
|
||||
|
||||
pa_assert(l != 0);
|
||||
|
||||
if (l < 0) {
|
||||
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
else if (errno == EAGAIN) {
|
||||
|
||||
/* OK, we filled all socket buffers up
|
||||
* now. */
|
||||
goto filled_up;
|
||||
|
||||
} else {
|
||||
pa_log("Failed to write data to FIFO: %s", pa_cstrerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
|
||||
} else {
|
||||
u->offset += l;
|
||||
|
||||
u->encoded_memchunk.index += l;
|
||||
u->encoded_memchunk.length -= l;
|
||||
|
||||
pollfd->revents = 0;
|
||||
|
||||
if (u->encoded_memchunk.length > 0) {
|
||||
/* we've completely written the encoded data, so update our overhead */
|
||||
u->encoding_overhead += u->next_encoding_overhead;
|
||||
|
||||
/* OK, we wrote less that we asked for,
|
||||
* hence we can assume that the socket
|
||||
* buffers are full now */
|
||||
goto filled_up;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filled_up:
|
||||
|
||||
/* At this spot we know that the socket buffers are
|
||||
* fully filled up. This is the best time to estimate
|
||||
* the playback position of the server */
|
||||
|
||||
n = u->offset - u->encoding_overhead;
|
||||
|
||||
#ifdef SIOCOUTQ
|
||||
{
|
||||
int l;
|
||||
if (ioctl(u->fd, SIOCOUTQ, &l) >= 0 && l > 0)
|
||||
n -= (l / u->encoding_ratio);
|
||||
}
|
||||
#endif
|
||||
|
||||
usec = pa_bytes_to_usec(n, &u->sink->sample_spec);
|
||||
|
||||
if (usec > u->latency)
|
||||
usec -= u->latency;
|
||||
else
|
||||
usec = 0;
|
||||
|
||||
pa_smoother_put(u->smoother, pa_rtclock_usec(), usec);
|
||||
}
|
||||
|
||||
/* Hmm, nothing to do. Let's sleep */
|
||||
pollfd->events = POLLOUT; /*PA_SINK_IS_OPENED(u->sink->thread_info.state) ? POLLOUT : 0;*/
|
||||
}
|
||||
|
||||
if ((ret = pa_rtpoll_run(u->rtpoll, TRUE)) < 0)
|
||||
goto fail;
|
||||
|
||||
if (ret == 0)
|
||||
goto finish;
|
||||
|
||||
if (u->rtpoll_item) {
|
||||
struct pollfd* pollfd;
|
||||
|
||||
pollfd = pa_rtpoll_item_get_pollfd(u->rtpoll_item, NULL);
|
||||
|
||||
if (pollfd->revents & ~POLLOUT) {
|
||||
if (u->sink->thread_info.state != PA_SINK_SUSPENDED) {
|
||||
pa_log("FIFO shutdown.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* We expect this to happen on occasion if we are not sending data.
|
||||
It's perfectly natural and normal and natural */
|
||||
if (u->rtpoll_item)
|
||||
pa_rtpoll_item_free(u->rtpoll_item);
|
||||
u->rtpoll_item = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
if (silence.memblock)
|
||||
pa_memblock_unref(silence.memblock);
|
||||
pa_log_debug("Thread shutting down");
|
||||
}
|
||||
|
||||
int pa__init(pa_module*m) {
|
||||
struct userdata *u = NULL;
|
||||
pa_sample_spec ss;
|
||||
pa_modargs *ma = NULL;
|
||||
const char *server;
|
||||
pa_sink_new_data data;
|
||||
|
||||
pa_assert(m);
|
||||
|
||||
if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
|
||||
pa_log("failed to parse module arguments");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
ss = m->core->default_sample_spec;
|
||||
if (pa_modargs_get_sample_spec(ma, &ss) < 0) {
|
||||
pa_log("invalid sample format specification");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if ((/*ss.format != PA_SAMPLE_U8 &&*/ ss.format != PA_SAMPLE_S16NE) ||
|
||||
(ss.channels > 2)) {
|
||||
pa_log("sample type support is limited to mono/stereo and U8 or S16NE sample data");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
u = pa_xnew0(struct userdata, 1);
|
||||
u->core = m->core;
|
||||
u->module = m;
|
||||
m->userdata = u;
|
||||
u->fd = -1;
|
||||
u->smoother = pa_smoother_new(PA_USEC_PER_SEC, PA_USEC_PER_SEC*2, TRUE, 10);
|
||||
pa_memchunk_reset(&u->raw_memchunk);
|
||||
pa_memchunk_reset(&u->encoded_memchunk);
|
||||
u->offset = 0;
|
||||
u->encoding_overhead = 0;
|
||||
u->next_encoding_overhead = 0;
|
||||
u->encoding_ratio = 1.0;
|
||||
|
||||
u->volume = roundf(0.7 * PA_VOLUME_NORM);
|
||||
u->muted = FALSE;
|
||||
|
||||
u->rtpoll = pa_rtpoll_new();
|
||||
pa_thread_mq_init(&u->thread_mq, m->core->mainloop, u->rtpoll);
|
||||
u->rtpoll_item = NULL;
|
||||
|
||||
/*u->format =
|
||||
(ss.format == PA_SAMPLE_U8 ? ESD_BITS8 : ESD_BITS16) |
|
||||
(ss.channels == 2 ? ESD_STEREO : ESD_MONO);*/
|
||||
u->rate = ss.rate;
|
||||
u->block_size = pa_usec_to_bytes(PA_USEC_PER_SEC/20, &ss);
|
||||
|
||||
u->read_data = u->write_data = NULL;
|
||||
u->read_index = u->write_index = u->read_length = u->write_length = 0;
|
||||
|
||||
/*u->state = STATE_AUTH;*/
|
||||
u->latency = 0;
|
||||
|
||||
if (!(server = pa_modargs_get_value(ma, "server", NULL))) {
|
||||
pa_log("No server argument given.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
pa_sink_new_data_init(&data);
|
||||
data.driver = __FILE__;
|
||||
data.module = m;
|
||||
pa_sink_new_data_set_name(&data, pa_modargs_get_value(ma, "sink_name", DEFAULT_SINK_NAME));
|
||||
pa_sink_new_data_set_sample_spec(&data, &ss);
|
||||
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_STRING, server);
|
||||
pa_proplist_setf(data.proplist, PA_PROP_DEVICE_DESCRIPTION, "Airtunes sink '%s'", server);
|
||||
|
||||
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.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
u->sink->parent.process_msg = sink_process_msg;
|
||||
u->sink->userdata = u;
|
||||
u->sink->get_volume = sink_get_volume_cb;
|
||||
u->sink->set_volume = sink_set_volume_cb;
|
||||
u->sink->get_mute = sink_get_mute_cb;
|
||||
u->sink->set_mute = sink_set_mute_cb;
|
||||
u->sink->flags = PA_SINK_LATENCY|PA_SINK_NETWORK|PA_SINK_HW_VOLUME_CTRL;
|
||||
|
||||
pa_sink_set_asyncmsgq(u->sink, u->thread_mq.inq);
|
||||
pa_sink_set_rtpoll(u->sink, u->rtpoll);
|
||||
|
||||
if (!(u->raop = pa_raop_client_new(u->core, server))) {
|
||||
pa_log("Failed to connect to server.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
pa_raop_client_set_callback(u->raop, on_connection, u);
|
||||
pa_raop_client_set_closed_callback(u->raop, on_close, u);
|
||||
|
||||
if (!(u->thread = pa_thread_new(thread_func, u))) {
|
||||
pa_log("Failed to create thread.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
pa_sink_put(u->sink);
|
||||
|
||||
pa_modargs_free(ma);
|
||||
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
if (ma)
|
||||
pa_modargs_free(ma);
|
||||
|
||||
pa__done(m);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void pa__done(pa_module*m) {
|
||||
struct userdata *u;
|
||||
pa_assert(m);
|
||||
|
||||
if (!(u = m->userdata))
|
||||
return;
|
||||
|
||||
if (u->sink)
|
||||
pa_sink_unlink(u->sink);
|
||||
|
||||
if (u->thread) {
|
||||
pa_asyncmsgq_send(u->thread_mq.inq, NULL, PA_MESSAGE_SHUTDOWN, NULL, 0, NULL);
|
||||
pa_thread_free(u->thread);
|
||||
}
|
||||
|
||||
pa_thread_mq_done(&u->thread_mq);
|
||||
|
||||
if (u->sink)
|
||||
pa_sink_unref(u->sink);
|
||||
|
||||
if (u->rtpoll_item)
|
||||
pa_rtpoll_item_free(u->rtpoll_item);
|
||||
|
||||
if (u->rtpoll)
|
||||
pa_rtpoll_free(u->rtpoll);
|
||||
|
||||
if (u->raw_memchunk.memblock)
|
||||
pa_memblock_unref(u->raw_memchunk.memblock);
|
||||
|
||||
if (u->encoded_memchunk.memblock)
|
||||
pa_memblock_unref(u->encoded_memchunk.memblock);
|
||||
|
||||
if (u->raop)
|
||||
pa_raop_client_free(u->raop);
|
||||
|
||||
pa_xfree(u->read_data);
|
||||
pa_xfree(u->write_data);
|
||||
|
||||
if (u->smoother)
|
||||
pa_smoother_free(u->smoother);
|
||||
|
||||
if (u->fd >= 0)
|
||||
pa_close(u->fd);
|
||||
|
||||
pa_xfree(u);
|
||||
}
|
||||
126
src/modules/raop/base64.c
Normal file
126
src/modules/raop/base64.c
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/***
|
||||
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 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.
|
||||
***/
|
||||
|
||||
/*
|
||||
This file was originally inspired by a file developed by
|
||||
Kungliga Tekniska H<EFBFBD>gskolan
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include <config.h>
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <pulse/xmalloc.h>
|
||||
|
||||
#include "base64.h"
|
||||
|
||||
static const char base64_chars[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static int pos(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;
|
||||
}
|
||||
|
||||
int pa_base64_encode(const void *data, int size, char **str)
|
||||
{
|
||||
char *s, *p;
|
||||
int i;
|
||||
int c;
|
||||
const unsigned char *q;
|
||||
|
||||
p = s = pa_xnew(char, size * 4 / 3 + 4);
|
||||
q = (const unsigned char *) data;
|
||||
i = 0;
|
||||
for (i = 0; i < size;) {
|
||||
c = q[i++];
|
||||
c *= 256;
|
||||
if (i < size)
|
||||
c += q[i];
|
||||
i++;
|
||||
c *= 256;
|
||||
if (i < size)
|
||||
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 > size)
|
||||
p[3] = '=';
|
||||
if (i > size + 1)
|
||||
p[2] = '=';
|
||||
p += 4;
|
||||
}
|
||||
*p = 0;
|
||||
*str = s;
|
||||
return strlen(s);
|
||||
}
|
||||
|
||||
#define DECODE_ERROR 0xffffffff
|
||||
|
||||
static unsigned int token_decode(const char *token)
|
||||
{
|
||||
int i;
|
||||
unsigned int val = 0;
|
||||
int marker = 0;
|
||||
if (strlen(token) < 4)
|
||||
return DECODE_ERROR;
|
||||
for (i = 0; i < 4; i++) {
|
||||
val *= 64;
|
||||
if (token[i] == '=')
|
||||
marker++;
|
||||
else if (marker > 0)
|
||||
return DECODE_ERROR;
|
||||
else
|
||||
val += pos(token[i]);
|
||||
}
|
||||
if (marker > 2)
|
||||
return DECODE_ERROR;
|
||||
return (marker << 24) | val;
|
||||
}
|
||||
|
||||
int pa_base64_decode(const char *str, void *data)
|
||||
{
|
||||
const char *p;
|
||||
unsigned char *q;
|
||||
|
||||
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 == 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;
|
||||
}
|
||||
34
src/modules/raop/base64.h
Normal file
34
src/modules/raop/base64.h
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef foobase64hfoo
|
||||
#define foobase64hfoo
|
||||
|
||||
/***
|
||||
This file is part of PulseAudio.
|
||||
|
||||
Copyright 2008 Colin Guthrie
|
||||
Copyright Kungliga Tekniska Høgskolan
|
||||
|
||||
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 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.
|
||||
***/
|
||||
|
||||
/*
|
||||
This file was originally inspired by a file developed by
|
||||
Kungliga Tekniska Høgskolan
|
||||
*/
|
||||
|
||||
int pa_base64_encode(const void *data, int size, char **str);
|
||||
int pa_base64_decode(const char *str, void *data);
|
||||
|
||||
#endif
|
||||
561
src/modules/raop/raop_client.c
Normal file
561
src/modules/raop/raop_client.c
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
/***
|
||||
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 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 <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
#ifdef HAVE_SYS_FILIO_H
|
||||
#include <sys/filio.h>
|
||||
#endif
|
||||
|
||||
/* TODO: Replace OpenSSL with NSS */
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/engine.h>
|
||||
|
||||
#include <pulse/xmalloc.h>
|
||||
|
||||
#include <pulsecore/core-error.h>
|
||||
#include <pulsecore/core-util.h>
|
||||
#include <pulsecore/socket-util.h>
|
||||
#include <pulsecore/log.h>
|
||||
#include <pulsecore/macro.h>
|
||||
#include <pulsecore/strbuf.h>
|
||||
#include <pulsecore/random.h>
|
||||
#include <pulsecore/poll.h>
|
||||
|
||||
#include "raop_client.h"
|
||||
#include "rtsp_client.h"
|
||||
#include "base64.h"
|
||||
|
||||
#define AES_CHUNKSIZE 16
|
||||
|
||||
#define JACK_STATUS_DISCONNECTED 0
|
||||
#define JACK_STATUS_CONNECTED 1
|
||||
|
||||
#define JACK_TYPE_ANALOG 0
|
||||
#define JACK_TYPE_DIGITAL 1
|
||||
|
||||
#define VOLUME_DEF -30
|
||||
#define VOLUME_MIN -144
|
||||
#define VOLUME_MAX 0
|
||||
|
||||
|
||||
struct pa_raop_client {
|
||||
pa_core *core;
|
||||
char *host;
|
||||
char *sid;
|
||||
pa_rtsp_client *rtsp;
|
||||
|
||||
uint8_t jack_type;
|
||||
uint8_t jack_status;
|
||||
|
||||
/* Encryption Related bits */
|
||||
AES_KEY aes;
|
||||
uint8_t aes_iv[AES_CHUNKSIZE]; /* initialization vector for aes-cbc */
|
||||
uint8_t aes_nv[AES_CHUNKSIZE]; /* next vector for aes-cbc */
|
||||
uint8_t aes_key[AES_CHUNKSIZE]; /* key for aes-cbc */
|
||||
|
||||
pa_socket_client *sc;
|
||||
int fd;
|
||||
|
||||
uint16_t seq;
|
||||
uint32_t rtptime;
|
||||
|
||||
pa_raop_client_cb_t callback;
|
||||
void* userdata;
|
||||
pa_raop_client_closed_cb_t closed_callback;
|
||||
void* closed_userdata;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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, int *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 definatly 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 incremeneted 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 int rsa_encrypt(uint8_t *text, int len, uint8_t *res) {
|
||||
const char n[] =
|
||||
"59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUtwC"
|
||||
"5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDR"
|
||||
"KSKv6kDqnw4UwPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuB"
|
||||
"OitnZ/bDzPHrTOZz0Dew0uowxf/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJ"
|
||||
"Q+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/UAaHqn9JdsBWLUEpVviYnh"
|
||||
"imNVvYFZeCXg/IdTQ+x4IRdiXNv5hEew==";
|
||||
const char e[] = "AQAB";
|
||||
uint8_t modules[256];
|
||||
uint8_t exponent[8];
|
||||
int size;
|
||||
RSA *rsa;
|
||||
|
||||
rsa = RSA_new();
|
||||
size = pa_base64_decode(n, modules);
|
||||
rsa->n = BN_bin2bn(modules, size, NULL);
|
||||
size = pa_base64_decode(e, exponent);
|
||||
rsa->e = BN_bin2bn(exponent, size, NULL);
|
||||
|
||||
size = RSA_public_encrypt(len, text, res, rsa, RSA_PKCS1_OAEP_PADDING);
|
||||
RSA_free(rsa);
|
||||
return size;
|
||||
}
|
||||
|
||||
static int aes_encrypt(pa_raop_client* c, uint8_t *data, int size)
|
||||
{
|
||||
uint8_t *buf;
|
||||
int i=0, j;
|
||||
|
||||
pa_assert(c);
|
||||
|
||||
memcpy(c->aes_nv, c->aes_iv, AES_CHUNKSIZE);
|
||||
while (i+AES_CHUNKSIZE <= size) {
|
||||
buf = data + i;
|
||||
for (j=0; j<AES_CHUNKSIZE; ++j)
|
||||
buf[j] ^= c->aes_nv[j];
|
||||
|
||||
AES_encrypt(buf, buf, &c->aes);
|
||||
memcpy(c->aes_nv, buf, AES_CHUNKSIZE);
|
||||
i += AES_CHUNKSIZE;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static inline void rtrimchar(char *str, char rc)
|
||||
{
|
||||
char *sp = str + strlen(str) - 1;
|
||||
while (sp >= str && *sp == rc) {
|
||||
*sp = '\0';
|
||||
sp -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_connection(pa_socket_client *sc, pa_iochannel *io, void *userdata) {
|
||||
pa_raop_client *c = userdata;
|
||||
|
||||
pa_assert(sc);
|
||||
pa_assert(c);
|
||||
pa_assert(c->sc == sc);
|
||||
pa_assert(c->fd < 0);
|
||||
pa_assert(c->callback);
|
||||
|
||||
pa_socket_client_unref(c->sc);
|
||||
c->sc = NULL;
|
||||
|
||||
if (!io) {
|
||||
pa_log("Connection failed: %s", pa_cstrerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
c->fd = pa_iochannel_get_send_fd(io);
|
||||
|
||||
pa_iochannel_set_noclose(io, TRUE);
|
||||
pa_iochannel_socket_set_sndbuf(io, 1024);
|
||||
pa_iochannel_free(io);
|
||||
|
||||
pa_make_tcp_socket_low_delay(c->fd);
|
||||
|
||||
pa_log_debug("Connection established");
|
||||
c->callback(c->fd, c->userdata);
|
||||
}
|
||||
|
||||
static void rtsp_cb(pa_rtsp_client *rtsp, pa_rtsp_state state, 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: {
|
||||
int i;
|
||||
uint8_t rsakey[512];
|
||||
char *key, *iv, *sac, *sdp;
|
||||
uint16_t rand_data;
|
||||
const char *ip;
|
||||
char *url;
|
||||
|
||||
pa_log_debug("RAOP: CONNECTED");
|
||||
ip = pa_rtsp_localip(c->rtsp);
|
||||
/* First of all set the url properly */
|
||||
url = pa_sprintf_malloc("rtsp://%s/%s", ip, c->sid);
|
||||
pa_rtsp_set_url(c->rtsp, url);
|
||||
pa_xfree(url);
|
||||
|
||||
/* Now encrypt our aes_public key to send to the device */
|
||||
i = rsa_encrypt(c->aes_key, AES_CHUNKSIZE, rsakey);
|
||||
pa_base64_encode(rsakey, i, &key);
|
||||
rtrimchar(key, '=');
|
||||
pa_base64_encode(c->aes_iv, AES_CHUNKSIZE, &iv);
|
||||
rtrimchar(iv, '=');
|
||||
|
||||
pa_random(&rand_data, sizeof(rand_data));
|
||||
pa_base64_encode(&rand_data, AES_CHUNKSIZE, &sac);
|
||||
rtrimchar(sac, '=');
|
||||
pa_rtsp_add_header(c->rtsp, "Apple-Challenge", sac);
|
||||
sdp = pa_sprintf_malloc(
|
||||
"v=0\r\n"
|
||||
"o=iTunes %s 0 IN IP4 %s\r\n"
|
||||
"s=iTunes\r\n"
|
||||
"c=IN IP4 %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 4096 0 16 40 10 14 2 255 0 0 44100\r\n"
|
||||
"a=rsaaeskey:%s\r\n"
|
||||
"a=aesiv:%s\r\n",
|
||||
c->sid, ip, c->host, key, iv);
|
||||
pa_rtsp_announce(c->rtsp, sdp);
|
||||
pa_xfree(key);
|
||||
pa_xfree(iv);
|
||||
pa_xfree(sac);
|
||||
pa_xfree(sdp);
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_ANNOUNCE:
|
||||
pa_log_debug("RAOP: ANNOUNCED");
|
||||
pa_rtsp_remove_header(c->rtsp, "Apple-Challenge");
|
||||
pa_rtsp_setup(c->rtsp);
|
||||
break;
|
||||
|
||||
case STATE_SETUP: {
|
||||
char *aj = pa_xstrdup(pa_headerlist_gets(headers, "Audio-Jack-Status"));
|
||||
pa_log_debug("RAOP: SETUP");
|
||||
if (aj) {
|
||||
char *token, *pc;
|
||||
char delimiters[] = ";";
|
||||
const char* token_state = NULL;
|
||||
c->jack_type = JACK_TYPE_ANALOG;
|
||||
c->jack_status = JACK_STATUS_DISCONNECTED;
|
||||
|
||||
while ((token = pa_split(aj, delimiters, &token_state))) {
|
||||
if ((pc = strstr(token, "="))) {
|
||||
*pc = 0;
|
||||
if (!strcmp(token, "type") && !strcmp(pc+1, "digital")) {
|
||||
c->jack_type = JACK_TYPE_DIGITAL;
|
||||
}
|
||||
} else {
|
||||
if (!strcmp(token,"connected"))
|
||||
c->jack_status = JACK_STATUS_CONNECTED;
|
||||
}
|
||||
pa_xfree(token);
|
||||
}
|
||||
pa_xfree(aj);
|
||||
} else {
|
||||
pa_log_warn("Audio Jack Status missing");
|
||||
}
|
||||
pa_rtsp_record(c->rtsp, &c->seq, &c->rtptime);
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_RECORD: {
|
||||
uint32_t port = pa_rtsp_serverport(c->rtsp);
|
||||
pa_log_debug("RAOP: RECORDED");
|
||||
|
||||
if (!(c->sc = pa_socket_client_new_string(c->core->mainloop, c->host, port))) {
|
||||
pa_log("failed to connect to server '%s:%d'", c->host, port);
|
||||
return;
|
||||
}
|
||||
pa_socket_client_set_callback(c->sc, on_connection, c);
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_FLUSH:
|
||||
pa_log_debug("RAOP: FLUSHED");
|
||||
break;
|
||||
|
||||
case STATE_TEARDOWN:
|
||||
case STATE_SET_PARAMETER:
|
||||
pa_log_debug("RAOP: SET_PARAMETER");
|
||||
break;
|
||||
case STATE_DISCONNECTED:
|
||||
pa_assert(c->closed_callback);
|
||||
pa_assert(c->rtsp);
|
||||
|
||||
pa_log_debug("RTSP control channel closed");
|
||||
pa_rtsp_client_free(c->rtsp);
|
||||
c->rtsp = NULL;
|
||||
if (c->fd > 0) {
|
||||
/* We do not close the fd, we leave it to the closed callback to do that */
|
||||
c->fd = -1;
|
||||
}
|
||||
if (c->sc) {
|
||||
pa_socket_client_unref(c->sc);
|
||||
c->sc = NULL;
|
||||
}
|
||||
pa_xfree(c->sid);
|
||||
c->sid = NULL;
|
||||
c->closed_callback(c->closed_userdata);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pa_raop_client* pa_raop_client_new(pa_core *core, const char* host)
|
||||
{
|
||||
pa_raop_client* c = pa_xnew0(pa_raop_client, 1);
|
||||
|
||||
pa_assert(core);
|
||||
pa_assert(host);
|
||||
|
||||
c->core = core;
|
||||
c->fd = -1;
|
||||
c->host = pa_xstrdup(host);
|
||||
|
||||
if (pa_raop_connect(c)) {
|
||||
pa_raop_client_free(c);
|
||||
return NULL;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
void pa_raop_client_free(pa_raop_client* c)
|
||||
{
|
||||
pa_assert(c);
|
||||
|
||||
if (c->rtsp)
|
||||
pa_rtsp_client_free(c->rtsp);
|
||||
pa_xfree(c->host);
|
||||
pa_xfree(c);
|
||||
}
|
||||
|
||||
|
||||
int pa_raop_connect(pa_raop_client* c)
|
||||
{
|
||||
char *sci;
|
||||
struct {
|
||||
uint32_t a;
|
||||
uint32_t b;
|
||||
uint32_t c;
|
||||
} rand_data;
|
||||
|
||||
pa_assert(c);
|
||||
|
||||
if (c->rtsp) {
|
||||
pa_log_debug("Connection already in progress");
|
||||
return 0;
|
||||
}
|
||||
|
||||
c->rtsp = pa_rtsp_client_new(c->core->mainloop, c->host, 5000, "iTunes/4.6 (Macintosh; U; PPC Mac OS X 10.3)");
|
||||
|
||||
/* Initialise the AES encryption system */
|
||||
pa_random(c->aes_iv, sizeof(c->aes_iv));
|
||||
pa_random(c->aes_key, sizeof(c->aes_key));
|
||||
memcpy(c->aes_nv, c->aes_iv, sizeof(c->aes_nv));
|
||||
AES_set_encrypt_key(c->aes_key, 128, &c->aes);
|
||||
|
||||
/* Generate random instance id */
|
||||
pa_random(&rand_data, sizeof(rand_data));
|
||||
c->sid = pa_sprintf_malloc("%u", rand_data.a);
|
||||
sci = pa_sprintf_malloc("%08x%08x",rand_data.b, rand_data.c);
|
||||
pa_rtsp_add_header(c->rtsp, "Client-Instance", sci);
|
||||
pa_xfree(sci);
|
||||
pa_rtsp_set_callback(c->rtsp, rtsp_cb, c);
|
||||
return pa_rtsp_connect(c->rtsp);
|
||||
}
|
||||
|
||||
|
||||
int pa_raop_flush(pa_raop_client* c)
|
||||
{
|
||||
pa_assert(c);
|
||||
|
||||
pa_rtsp_flush(c->rtsp, c->seq, c->rtptime);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int pa_raop_client_set_volume(pa_raop_client* c, pa_volume_t volume)
|
||||
{
|
||||
int rv;
|
||||
double db;
|
||||
char *param;
|
||||
|
||||
pa_assert(c);
|
||||
|
||||
db = pa_sw_volume_to_dB(volume);
|
||||
if (db < VOLUME_MIN)
|
||||
db = VOLUME_MIN;
|
||||
else if (db > VOLUME_MAX)
|
||||
db = VOLUME_MAX;
|
||||
|
||||
param = pa_sprintf_malloc("volume: %0.6f\r\n", db);
|
||||
|
||||
/* We just hit and hope, cannot wait for the callback */
|
||||
rv = pa_rtsp_setparameter(c->rtsp, param);
|
||||
pa_xfree(param);
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
int pa_raop_client_encode_sample(pa_raop_client* c, pa_memchunk* raw, pa_memchunk* encoded)
|
||||
{
|
||||
uint16_t len;
|
||||
size_t bufmax;
|
||||
uint8_t *bp, bpos;
|
||||
uint8_t *ibp, *maxibp;
|
||||
int size;
|
||||
uint8_t *b, *p;
|
||||
uint32_t bsize;
|
||||
size_t length;
|
||||
static uint8_t header[] = {
|
||||
0x24, 0x00, 0x00, 0x00,
|
||||
0xF0, 0xFF, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
int header_size = sizeof(header);
|
||||
|
||||
pa_assert(c);
|
||||
pa_assert(c->fd > 0);
|
||||
pa_assert(raw);
|
||||
pa_assert(raw->memblock);
|
||||
pa_assert(raw->length > 0);
|
||||
pa_assert(encoded);
|
||||
|
||||
/* We have to send 4 byte chunks */
|
||||
bsize = (int)(raw->length / 4);
|
||||
length = bsize * 4;
|
||||
|
||||
/* Leave 16 bytes extra to allow for the ALAC header which is about 55 bits */
|
||||
bufmax = length + header_size + 16;
|
||||
pa_memchunk_reset(encoded);
|
||||
encoded->memblock = pa_memblock_new(c->core->mempool, bufmax);
|
||||
b = pa_memblock_acquire(encoded->memblock);
|
||||
memcpy(b, header, header_size);
|
||||
|
||||
/* Now write the actual samples */
|
||||
bp = b + header_size;
|
||||
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,(bsize>>24)&0xff,8);
|
||||
bit_writer(&bp,&bpos,&size,(bsize>>16)&0xff,8);
|
||||
bit_writer(&bp,&bpos,&size,(bsize>>8)&0xff,8);
|
||||
bit_writer(&bp,&bpos,&size,(bsize)&0xff,8);
|
||||
|
||||
ibp = p = pa_memblock_acquire(raw->memblock);
|
||||
maxibp = p + raw->length - 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;
|
||||
raw->index += 4;
|
||||
raw->length -= 4;
|
||||
}
|
||||
pa_memblock_release(raw->memblock);
|
||||
encoded->length = header_size + size;
|
||||
|
||||
/* store the lenght (endian swapped: make this better) */
|
||||
len = size + header_size - 4;
|
||||
*(b + 2) = len >> 8;
|
||||
*(b + 3) = len & 0xff;
|
||||
|
||||
/* encrypt our data */
|
||||
aes_encrypt(c, (b + header_size), size);
|
||||
|
||||
/* We're done with the chunk */
|
||||
pa_memblock_release(encoded->memblock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void pa_raop_client_set_callback(pa_raop_client* c, pa_raop_client_cb_t callback, void *userdata)
|
||||
{
|
||||
pa_assert(c);
|
||||
|
||||
c->callback = callback;
|
||||
c->userdata = userdata;
|
||||
}
|
||||
|
||||
void pa_raop_client_set_closed_callback(pa_raop_client* c, pa_raop_client_closed_cb_t callback, void *userdata)
|
||||
{
|
||||
pa_assert(c);
|
||||
|
||||
c->closed_callback = callback;
|
||||
c->closed_userdata = userdata;
|
||||
}
|
||||
46
src/modules/raop/raop_client.h
Normal file
46
src/modules/raop/raop_client.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#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 of the License,
|
||||
or (at your option) any later version.
|
||||
|
||||
PulseAudio is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with PulseAudio; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
|
||||
USA.
|
||||
***/
|
||||
|
||||
#include <pulse/mainloop-api.h>
|
||||
#include <pulsecore/iochannel.h>
|
||||
#include <pulsecore/core.h>
|
||||
|
||||
typedef struct pa_raop_client pa_raop_client;
|
||||
|
||||
pa_raop_client* pa_raop_client_new(pa_core *core, const char* host);
|
||||
void pa_raop_client_free(pa_raop_client* c);
|
||||
|
||||
int pa_raop_connect(pa_raop_client* c);
|
||||
int pa_raop_flush(pa_raop_client* c);
|
||||
|
||||
int pa_raop_client_set_volume(pa_raop_client* c, pa_volume_t volume);
|
||||
int pa_raop_client_encode_sample(pa_raop_client* c, pa_memchunk* raw, pa_memchunk* encoded);
|
||||
|
||||
typedef void (*pa_raop_client_cb_t)(int fd, void *userdata);
|
||||
void pa_raop_client_set_callback(pa_raop_client* c, pa_raop_client_cb_t callback, void *userdata);
|
||||
|
||||
typedef void (*pa_raop_client_closed_cb_t)(void *userdata);
|
||||
void pa_raop_client_set_closed_callback(pa_raop_client* c, pa_raop_client_closed_cb_t callback, void *userdata);
|
||||
|
||||
#endif
|
||||
186
src/modules/rtp/headerlist.c
Normal file
186
src/modules/rtp/headerlist.c
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/***
|
||||
This file is part of PulseAudio.
|
||||
|
||||
Copyright 2008 Colin Guthrie
|
||||
Copyright 2007 Lennart Poettering
|
||||
|
||||
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
|
||||
Lesser 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 <string.h>
|
||||
|
||||
#include <pulse/xmalloc.h>
|
||||
|
||||
#include <pulsecore/hashmap.h>
|
||||
#include <pulsecore/strbuf.h>
|
||||
#include <pulsecore/core-util.h>
|
||||
|
||||
#include "headerlist.h"
|
||||
|
||||
struct header {
|
||||
char *key;
|
||||
void *value;
|
||||
size_t nbytes;
|
||||
};
|
||||
|
||||
#define MAKE_HASHMAP(p) ((pa_hashmap*) (p))
|
||||
#define MAKE_HEADERLIST(p) ((pa_headerlist*) (p))
|
||||
|
||||
static void header_free(struct header *hdr) {
|
||||
pa_assert(hdr);
|
||||
|
||||
pa_xfree(hdr->key);
|
||||
pa_xfree(hdr->value);
|
||||
pa_xfree(hdr);
|
||||
}
|
||||
|
||||
pa_headerlist* pa_headerlist_new(void) {
|
||||
return MAKE_HEADERLIST(pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func));
|
||||
}
|
||||
|
||||
void pa_headerlist_free(pa_headerlist* p) {
|
||||
struct header *hdr;
|
||||
|
||||
while ((hdr = pa_hashmap_steal_first(MAKE_HASHMAP(p))))
|
||||
header_free(hdr);
|
||||
|
||||
pa_hashmap_free(MAKE_HASHMAP(p), NULL, NULL);
|
||||
}
|
||||
|
||||
int pa_headerlist_puts(pa_headerlist *p, const char *key, const char *value) {
|
||||
struct header *hdr;
|
||||
pa_bool_t add = FALSE;
|
||||
|
||||
pa_assert(p);
|
||||
pa_assert(key);
|
||||
|
||||
if (!(hdr = pa_hashmap_get(MAKE_HASHMAP(p), key))) {
|
||||
hdr = pa_xnew(struct header, 1);
|
||||
hdr->key = pa_xstrdup(key);
|
||||
add = TRUE;
|
||||
} else
|
||||
pa_xfree(hdr->value);
|
||||
|
||||
hdr->value = pa_xstrdup(value);
|
||||
hdr->nbytes = strlen(value)+1;
|
||||
|
||||
if (add)
|
||||
pa_hashmap_put(MAKE_HASHMAP(p), hdr->key, hdr);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pa_headerlist_putsappend(pa_headerlist *p, const char *key, const char *value) {
|
||||
struct header *hdr;
|
||||
pa_bool_t add = FALSE;
|
||||
|
||||
pa_assert(p);
|
||||
pa_assert(key);
|
||||
|
||||
if (!(hdr = pa_hashmap_get(MAKE_HASHMAP(p), key))) {
|
||||
hdr = pa_xnew(struct header, 1);
|
||||
hdr->key = pa_xstrdup(key);
|
||||
hdr->value = pa_xstrdup(value);
|
||||
add = TRUE;
|
||||
} else {
|
||||
void *newval = pa_sprintf_malloc("%s%s", (char*)hdr->value, value);
|
||||
pa_xfree(hdr->value);
|
||||
hdr->value = newval;
|
||||
}
|
||||
hdr->nbytes = strlen(hdr->value)+1;
|
||||
|
||||
if (add)
|
||||
pa_hashmap_put(MAKE_HASHMAP(p), hdr->key, hdr);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *pa_headerlist_gets(pa_headerlist *p, const char *key) {
|
||||
struct header *hdr;
|
||||
|
||||
pa_assert(p);
|
||||
pa_assert(key);
|
||||
|
||||
if (!(hdr = pa_hashmap_get(MAKE_HASHMAP(p), key)))
|
||||
return NULL;
|
||||
|
||||
if (hdr->nbytes <= 0)
|
||||
return NULL;
|
||||
|
||||
if (((char*) hdr->value)[hdr->nbytes-1] != 0)
|
||||
return NULL;
|
||||
|
||||
if (strlen((char*) hdr->value) != hdr->nbytes-1)
|
||||
return NULL;
|
||||
|
||||
return (char*) hdr->value;
|
||||
}
|
||||
|
||||
int pa_headerlist_remove(pa_headerlist *p, const char *key) {
|
||||
struct header *hdr;
|
||||
|
||||
pa_assert(p);
|
||||
pa_assert(key);
|
||||
|
||||
if (!(hdr = pa_hashmap_remove(MAKE_HASHMAP(p), key)))
|
||||
return -1;
|
||||
|
||||
header_free(hdr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *pa_headerlist_iterate(pa_headerlist *p, void **state) {
|
||||
struct header *hdr;
|
||||
|
||||
if (!(hdr = pa_hashmap_iterate(MAKE_HASHMAP(p), state, NULL)))
|
||||
return NULL;
|
||||
|
||||
return hdr->key;
|
||||
}
|
||||
|
||||
char *pa_headerlist_to_string(pa_headerlist *p) {
|
||||
const char *key;
|
||||
void *state = NULL;
|
||||
pa_strbuf *buf;
|
||||
|
||||
pa_assert(p);
|
||||
|
||||
buf = pa_strbuf_new();
|
||||
|
||||
while ((key = pa_headerlist_iterate(p, &state))) {
|
||||
|
||||
const char *v;
|
||||
|
||||
if ((v = pa_headerlist_gets(p, key)))
|
||||
pa_strbuf_printf(buf, "%s: %s\r\n", key, v);
|
||||
}
|
||||
|
||||
return pa_strbuf_tostring_free(buf);
|
||||
}
|
||||
|
||||
int pa_headerlist_contains(pa_headerlist *p, const char *key) {
|
||||
pa_assert(p);
|
||||
pa_assert(key);
|
||||
|
||||
if (!(pa_hashmap_get(MAKE_HASHMAP(p), key)))
|
||||
return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
46
src/modules/rtp/headerlist.h
Normal file
46
src/modules/rtp/headerlist.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#ifndef foopulseheaderlisthfoo
|
||||
#define foopulseheaderlisthfoo
|
||||
|
||||
/***
|
||||
This file is part of PulseAudio.
|
||||
|
||||
Copyright 2008 Colin Guthrie
|
||||
Copyright 2007 Lennart Poettering
|
||||
|
||||
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
|
||||
Lesser 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/macro.h>
|
||||
|
||||
typedef struct pa_headerlist pa_headerlist;
|
||||
|
||||
pa_headerlist* pa_headerlist_new(void);
|
||||
void pa_headerlist_free(pa_headerlist* p);
|
||||
|
||||
int pa_headerlist_puts(pa_headerlist *p, const char *key, const char *value);
|
||||
int pa_headerlist_putsappend(pa_headerlist *p, const char *key, const char *value);
|
||||
|
||||
const char *pa_headerlist_gets(pa_headerlist *p, const char *key);
|
||||
|
||||
int pa_headerlist_remove(pa_headerlist *p, const char *key);
|
||||
|
||||
const char *pa_headerlist_iterate(pa_headerlist *p, void **state);
|
||||
|
||||
char *pa_headerlist_to_string(pa_headerlist *p);
|
||||
|
||||
int pa_headerlist_contains(pa_headerlist *p, const char *key);
|
||||
|
||||
#endif
|
||||
542
src/modules/rtp/rtsp_client.c
Normal file
542
src/modules/rtp/rtsp_client.c
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
/***
|
||||
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 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 <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
#ifdef HAVE_SYS_FILIO_H
|
||||
#include <sys/filio.h>
|
||||
#endif
|
||||
|
||||
#include <pulse/xmalloc.h>
|
||||
|
||||
#include <pulsecore/core-error.h>
|
||||
#include <pulsecore/core-util.h>
|
||||
#include <pulsecore/socket-util.h>
|
||||
#include <pulsecore/log.h>
|
||||
#include <pulsecore/macro.h>
|
||||
#include <pulsecore/strbuf.h>
|
||||
#include <pulsecore/poll.h>
|
||||
#include <pulsecore/ioline.h>
|
||||
|
||||
#include "rtsp_client.h"
|
||||
|
||||
struct pa_rtsp_client {
|
||||
pa_mainloop_api *mainloop;
|
||||
char *hostname;
|
||||
uint16_t port;
|
||||
|
||||
pa_socket_client *sc;
|
||||
pa_iochannel *io;
|
||||
pa_ioline *ioline;
|
||||
|
||||
pa_rtsp_cb_t callback;
|
||||
|
||||
void *userdata;
|
||||
const char *useragent;
|
||||
|
||||
pa_rtsp_state state;
|
||||
uint8_t waiting;
|
||||
|
||||
pa_headerlist* headers;
|
||||
char *last_header;
|
||||
pa_strbuf *header_buffer;
|
||||
pa_headerlist* response_headers;
|
||||
|
||||
char *localip;
|
||||
char *url;
|
||||
uint16_t rtp_port;
|
||||
uint32_t cseq;
|
||||
char *session;
|
||||
char *transport;
|
||||
};
|
||||
|
||||
pa_rtsp_client* pa_rtsp_client_new(pa_mainloop_api *mainloop, const char* hostname, uint16_t port, const char* useragent) {
|
||||
pa_rtsp_client *c;
|
||||
|
||||
pa_assert(mainloop);
|
||||
pa_assert(hostname);
|
||||
pa_assert(port > 0);
|
||||
|
||||
c = pa_xnew0(pa_rtsp_client, 1);
|
||||
c->mainloop = mainloop;
|
||||
c->hostname = pa_xstrdup(hostname);
|
||||
c->port = port;
|
||||
c->headers = pa_headerlist_new();
|
||||
|
||||
if (useragent)
|
||||
c->useragent = useragent;
|
||||
else
|
||||
c->useragent = "PulseAudio RTSP Client";
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
void pa_rtsp_client_free(pa_rtsp_client* c) {
|
||||
if (c) {
|
||||
if (c->sc)
|
||||
pa_socket_client_unref(c->sc);
|
||||
if (c->ioline)
|
||||
pa_ioline_close(c->ioline);
|
||||
else if (c->io)
|
||||
pa_iochannel_free(c->io);
|
||||
|
||||
pa_xfree(c->hostname);
|
||||
pa_xfree(c->url);
|
||||
pa_xfree(c->localip);
|
||||
pa_xfree(c->session);
|
||||
pa_xfree(c->transport);
|
||||
pa_xfree(c->last_header);
|
||||
if (c->header_buffer)
|
||||
pa_strbuf_free(c->header_buffer);
|
||||
if (c->response_headers)
|
||||
pa_headerlist_free(c->response_headers);
|
||||
pa_headerlist_free(c->headers);
|
||||
}
|
||||
pa_xfree(c);
|
||||
}
|
||||
|
||||
|
||||
static void headers_read(pa_rtsp_client *c) {
|
||||
char* token;
|
||||
char delimiters[] = ";";
|
||||
|
||||
pa_assert(c);
|
||||
pa_assert(c->response_headers);
|
||||
pa_assert(c->callback);
|
||||
|
||||
/* Deal with a SETUP response */
|
||||
if (STATE_SETUP == c->state) {
|
||||
const char* token_state = NULL;
|
||||
const char* pc = NULL;
|
||||
c->session = pa_xstrdup(pa_headerlist_gets(c->response_headers, "Session"));
|
||||
c->transport = pa_xstrdup(pa_headerlist_gets(c->response_headers, "Transport"));
|
||||
|
||||
if (!c->session || !c->transport) {
|
||||
pa_headerlist_free(c->response_headers);
|
||||
c->response_headers = NULL;
|
||||
pa_log("Invalid SETUP response.");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Now parse out the server port component of the response. */
|
||||
while ((token = pa_split(c->transport, delimiters, &token_state))) {
|
||||
if ((pc = strstr(token, "="))) {
|
||||
if (0 == strncmp(token, "server_port", 11)) {
|
||||
pa_atou(pc+1, (uint32_t*)(&c->rtp_port));
|
||||
pa_xfree(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
pa_xfree(token);
|
||||
}
|
||||
if (0 == c->rtp_port) {
|
||||
/* Error no server_port in response */
|
||||
pa_headerlist_free(c->response_headers);
|
||||
c->response_headers = NULL;
|
||||
pa_log("Invalid SETUP response (no port number).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Call our callback */
|
||||
c->callback(c, c->state, c->response_headers, c->userdata);
|
||||
|
||||
pa_headerlist_free(c->response_headers);
|
||||
c->response_headers = NULL;
|
||||
}
|
||||
|
||||
|
||||
static void line_callback(pa_ioline *line, const char *s, void *userdata) {
|
||||
char *delimpos;
|
||||
char *s2, *s2p;
|
||||
|
||||
pa_rtsp_client *c = userdata;
|
||||
pa_assert(line);
|
||||
pa_assert(c);
|
||||
pa_assert(c->callback);
|
||||
|
||||
if (!s) {
|
||||
/* Keep the ioline/iochannel open as they will be freed automatically */
|
||||
c->ioline = NULL;
|
||||
c->io = NULL;
|
||||
c->callback(c, STATE_DISCONNECTED, NULL, c->userdata);
|
||||
return;
|
||||
}
|
||||
|
||||
s2 = pa_xstrdup(s);
|
||||
/* Trim trailing carriage returns */
|
||||
s2p = s2 + strlen(s2) - 1;
|
||||
while (s2p >= s2 && '\r' == *s2p) {
|
||||
*s2p = '\0';
|
||||
s2p -= 1;
|
||||
}
|
||||
if (c->waiting && 0 == strcmp("RTSP/1.0 200 OK", s2)) {
|
||||
c->waiting = 0;
|
||||
pa_assert(!c->response_headers);
|
||||
c->response_headers = pa_headerlist_new();
|
||||
goto exit;
|
||||
}
|
||||
if (c->waiting) {
|
||||
pa_log_warn("Unexpected response: %s", s2);
|
||||
goto exit;;
|
||||
}
|
||||
if (!strlen(s2)) {
|
||||
/* End of headers */
|
||||
/* We will have a header left from our looping itteration, so add it in :) */
|
||||
if (c->last_header) {
|
||||
/* This is not a continuation header so let's dump it into our proplist */
|
||||
pa_headerlist_puts(c->response_headers, c->last_header, pa_strbuf_tostring_free(c->header_buffer));
|
||||
pa_xfree(c->last_header);
|
||||
c->last_header = NULL;
|
||||
c->header_buffer= NULL;
|
||||
}
|
||||
|
||||
pa_log_debug("Full response received. Dispatching");
|
||||
headers_read(c);
|
||||
c->waiting = 1;
|
||||
goto exit;
|
||||
}
|
||||
|
||||
/* Read and parse a header (we know it's not empty) */
|
||||
/* TODO: Move header reading into the headerlist. */
|
||||
|
||||
/* If the first character is a space, it's a continuation header */
|
||||
if (c->last_header && ' ' == s2[0]) {
|
||||
pa_assert(c->header_buffer);
|
||||
|
||||
/* Add this line to the buffer (sans the space. */
|
||||
pa_strbuf_puts(c->header_buffer, &(s2[1]));
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (c->last_header) {
|
||||
/* This is not a continuation header so let's dump the full
|
||||
header/value into our proplist */
|
||||
pa_headerlist_puts(c->response_headers, c->last_header, pa_strbuf_tostring_free(c->header_buffer));
|
||||
pa_xfree(c->last_header);
|
||||
c->last_header = NULL;
|
||||
c->header_buffer = NULL;
|
||||
}
|
||||
|
||||
delimpos = strstr(s2, ":");
|
||||
if (!delimpos) {
|
||||
pa_log_warn("Unexpected response when expecting header: %s", s);
|
||||
goto exit;
|
||||
}
|
||||
|
||||
pa_assert(!c->header_buffer);
|
||||
pa_assert(!c->last_header);
|
||||
|
||||
c->header_buffer = pa_strbuf_new();
|
||||
if (strlen(delimpos) > 1) {
|
||||
/* Cut our line off so we can copy the header name out */
|
||||
*delimpos++ = '\0';
|
||||
|
||||
/* Trim the front of any spaces */
|
||||
while (' ' == *delimpos)
|
||||
++delimpos;
|
||||
|
||||
pa_strbuf_puts(c->header_buffer, delimpos);
|
||||
} else {
|
||||
/* Cut our line off so we can copy the header name out */
|
||||
*delimpos = '\0';
|
||||
}
|
||||
|
||||
/* Save the header name */
|
||||
c->last_header = pa_xstrdup(s2);
|
||||
exit:
|
||||
pa_xfree(s2);
|
||||
}
|
||||
|
||||
|
||||
static void on_connection(pa_socket_client *sc, pa_iochannel *io, void *userdata) {
|
||||
pa_rtsp_client *c = userdata;
|
||||
union {
|
||||
struct sockaddr sa;
|
||||
struct sockaddr_in in;
|
||||
struct sockaddr_in6 in6;
|
||||
} sa;
|
||||
socklen_t sa_len = sizeof(sa);
|
||||
|
||||
pa_assert(sc);
|
||||
pa_assert(c);
|
||||
pa_assert(STATE_CONNECT == c->state);
|
||||
pa_assert(c->sc == sc);
|
||||
pa_socket_client_unref(c->sc);
|
||||
c->sc = NULL;
|
||||
|
||||
if (!io) {
|
||||
pa_log("Connection failed: %s", pa_cstrerror(errno));
|
||||
return;
|
||||
}
|
||||
pa_assert(!c->io);
|
||||
c->io = io;
|
||||
|
||||
c->ioline = pa_ioline_new(io);
|
||||
pa_ioline_set_callback(c->ioline, line_callback, c);
|
||||
|
||||
/* Get the local IP address for use externally */
|
||||
if (0 == getsockname(pa_iochannel_get_recv_fd(io), &sa.sa, &sa_len)) {
|
||||
char buf[INET6_ADDRSTRLEN];
|
||||
const char *res = NULL;
|
||||
|
||||
if (AF_INET == sa.sa.sa_family) {
|
||||
if ((res = inet_ntop(sa.sa.sa_family, &sa.in.sin_addr, buf, sizeof(buf)))) {
|
||||
c->localip = pa_xstrdup(res);
|
||||
}
|
||||
} else if (AF_INET6 == sa.sa.sa_family) {
|
||||
if ((res = inet_ntop(AF_INET6, &sa.in6.sin6_addr, buf, sizeof(buf)))) {
|
||||
c->localip = pa_sprintf_malloc("[%s]", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
pa_log_debug("Established RTSP connection from local ip %s", c->localip);
|
||||
|
||||
if (c->callback)
|
||||
c->callback(c, c->state, NULL, c->userdata);
|
||||
}
|
||||
|
||||
int pa_rtsp_connect(pa_rtsp_client *c) {
|
||||
pa_assert(c);
|
||||
pa_assert(!c->sc);
|
||||
|
||||
pa_xfree(c->session);
|
||||
c->session = NULL;
|
||||
|
||||
if (!(c->sc = pa_socket_client_new_string(c->mainloop, c->hostname, c->port))) {
|
||||
pa_log("failed to connect to server '%s:%d'", c->hostname, c->port);
|
||||
return -1;
|
||||
}
|
||||
|
||||
pa_socket_client_set_callback(c->sc, on_connection, c);
|
||||
c->waiting = 1;
|
||||
c->state = STATE_CONNECT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pa_rtsp_set_callback(pa_rtsp_client *c, pa_rtsp_cb_t callback, void *userdata) {
|
||||
pa_assert(c);
|
||||
|
||||
c->callback = callback;
|
||||
c->userdata = userdata;
|
||||
}
|
||||
|
||||
void pa_rtsp_disconnect(pa_rtsp_client *c) {
|
||||
pa_assert(c);
|
||||
|
||||
if (c->io)
|
||||
pa_iochannel_free(c->io);
|
||||
c->io = NULL;
|
||||
}
|
||||
|
||||
|
||||
const char* pa_rtsp_localip(pa_rtsp_client* c) {
|
||||
pa_assert(c);
|
||||
|
||||
return c->localip;
|
||||
}
|
||||
|
||||
uint32_t pa_rtsp_serverport(pa_rtsp_client* c) {
|
||||
pa_assert(c);
|
||||
|
||||
return c->rtp_port;
|
||||
}
|
||||
|
||||
void pa_rtsp_set_url(pa_rtsp_client* c, const char* url) {
|
||||
pa_assert(c);
|
||||
|
||||
c->url = pa_xstrdup(url);
|
||||
}
|
||||
|
||||
void pa_rtsp_add_header(pa_rtsp_client *c, const char* key, const char* value)
|
||||
{
|
||||
pa_assert(c);
|
||||
pa_assert(key);
|
||||
pa_assert(value);
|
||||
|
||||
pa_headerlist_puts(c->headers, key, value);
|
||||
}
|
||||
|
||||
void pa_rtsp_remove_header(pa_rtsp_client *c, const char* key)
|
||||
{
|
||||
pa_assert(c);
|
||||
pa_assert(key);
|
||||
|
||||
pa_headerlist_remove(c->headers, key);
|
||||
}
|
||||
|
||||
static int rtsp_exec(pa_rtsp_client* c, const char* cmd,
|
||||
const char* content_type, const char* content,
|
||||
int expect_response,
|
||||
pa_headerlist* headers) {
|
||||
pa_strbuf* buf;
|
||||
char* hdrs;
|
||||
ssize_t l;
|
||||
|
||||
pa_assert(c);
|
||||
pa_assert(c->url);
|
||||
|
||||
if (!cmd)
|
||||
return -1;
|
||||
|
||||
pa_log_debug("Sending command: %s", cmd);
|
||||
|
||||
buf = pa_strbuf_new();
|
||||
pa_strbuf_printf(buf, "%s %s RTSP/1.0\r\nCSeq: %d\r\n", cmd, c->url, ++c->cseq);
|
||||
if (c->session)
|
||||
pa_strbuf_printf(buf, "Session: %s\r\n", c->session);
|
||||
|
||||
/* Add the headers */
|
||||
if (headers) {
|
||||
hdrs = pa_headerlist_to_string(headers);
|
||||
pa_strbuf_puts(buf, hdrs);
|
||||
pa_xfree(hdrs);
|
||||
}
|
||||
|
||||
if (content_type && content) {
|
||||
pa_strbuf_printf(buf, "Content-Type: %s\r\nContent-Length: %d\r\n",
|
||||
content_type, (int)strlen(content));
|
||||
}
|
||||
|
||||
pa_strbuf_printf(buf, "User-Agent: %s\r\n", c->useragent);
|
||||
|
||||
if (c->headers) {
|
||||
hdrs = pa_headerlist_to_string(c->headers);
|
||||
pa_strbuf_puts(buf, hdrs);
|
||||
pa_xfree(hdrs);
|
||||
}
|
||||
|
||||
pa_strbuf_puts(buf, "\r\n");
|
||||
|
||||
if (content_type && content) {
|
||||
pa_strbuf_puts(buf, content);
|
||||
}
|
||||
|
||||
/* Our packet is created... now we can send it :) */
|
||||
hdrs = pa_strbuf_tostring_free(buf);
|
||||
/*pa_log_debug("Submitting request:");
|
||||
pa_log_debug(hdrs);*/
|
||||
l = pa_iochannel_write(c->io, hdrs, strlen(hdrs));
|
||||
pa_xfree(hdrs);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int pa_rtsp_announce(pa_rtsp_client *c, const char* sdp) {
|
||||
pa_assert(c);
|
||||
if (!sdp)
|
||||
return -1;
|
||||
|
||||
c->state = STATE_ANNOUNCE;
|
||||
return rtsp_exec(c, "ANNOUNCE", "application/sdp", sdp, 1, NULL);
|
||||
}
|
||||
|
||||
|
||||
int pa_rtsp_setup(pa_rtsp_client* c) {
|
||||
pa_headerlist* headers;
|
||||
int rv;
|
||||
|
||||
pa_assert(c);
|
||||
|
||||
headers = pa_headerlist_new();
|
||||
pa_headerlist_puts(headers, "Transport", "RTP/AVP/TCP;unicast;interleaved=0-1;mode=record");
|
||||
|
||||
c->state = STATE_SETUP;
|
||||
rv = rtsp_exec(c, "SETUP", NULL, NULL, 1, headers);
|
||||
pa_headerlist_free(headers);
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
int pa_rtsp_record(pa_rtsp_client* c, uint16_t* seq, uint32_t* rtptime) {
|
||||
pa_headerlist* headers;
|
||||
int rv;
|
||||
char *info;
|
||||
|
||||
pa_assert(c);
|
||||
if (!c->session) {
|
||||
/* No seesion in progres */
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Todo: Generate these values randomly as per spec */
|
||||
*seq = *rtptime = 0;
|
||||
|
||||
headers = pa_headerlist_new();
|
||||
pa_headerlist_puts(headers, "Range", "npt=0-");
|
||||
info = pa_sprintf_malloc("seq=%u;rtptime=%u", *seq, *rtptime);
|
||||
pa_headerlist_puts(headers, "RTP-Info", info);
|
||||
pa_xfree(info);
|
||||
|
||||
c->state = STATE_RECORD;
|
||||
rv = rtsp_exec(c, "RECORD", NULL, NULL, 1, headers);
|
||||
pa_headerlist_free(headers);
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
int pa_rtsp_teardown(pa_rtsp_client *c) {
|
||||
pa_assert(c);
|
||||
|
||||
c->state = STATE_TEARDOWN;
|
||||
return rtsp_exec(c, "TEARDOWN", NULL, NULL, 0, NULL);
|
||||
}
|
||||
|
||||
|
||||
int pa_rtsp_setparameter(pa_rtsp_client *c, const char* param) {
|
||||
pa_assert(c);
|
||||
if (!param)
|
||||
return -1;
|
||||
|
||||
c->state = STATE_SET_PARAMETER;
|
||||
return rtsp_exec(c, "SET_PARAMETER", "text/parameters", param, 1, NULL);
|
||||
}
|
||||
|
||||
|
||||
int pa_rtsp_flush(pa_rtsp_client *c, uint16_t seq, uint32_t rtptime) {
|
||||
pa_headerlist* headers;
|
||||
int rv;
|
||||
char *info;
|
||||
|
||||
pa_assert(c);
|
||||
|
||||
headers = pa_headerlist_new();
|
||||
info = pa_sprintf_malloc("seq=%u;rtptime=%u", seq, rtptime);
|
||||
pa_headerlist_puts(headers, "RTP-Info", info);
|
||||
pa_xfree(info);
|
||||
|
||||
c->state = STATE_FLUSH;
|
||||
rv = rtsp_exec(c, "FLUSH", NULL, NULL, 1, headers);
|
||||
pa_headerlist_free(headers);
|
||||
return rv;
|
||||
}
|
||||
73
src/modules/rtp/rtsp_client.h
Normal file
73
src/modules/rtp/rtsp_client.h
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#ifndef foortspclienthfoo
|
||||
#define foortspclienthfoo
|
||||
|
||||
/***
|
||||
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 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 <inttypes.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <netdb.h>
|
||||
|
||||
#include <pulsecore/memblockq.h>
|
||||
#include <pulsecore/memchunk.h>
|
||||
#include <pulsecore/socket-client.h>
|
||||
#include <pulse/mainloop-api.h>
|
||||
|
||||
#include "headerlist.h"
|
||||
|
||||
typedef struct pa_rtsp_client pa_rtsp_client;
|
||||
typedef enum {
|
||||
STATE_CONNECT,
|
||||
STATE_ANNOUNCE,
|
||||
STATE_SETUP,
|
||||
STATE_RECORD,
|
||||
STATE_FLUSH,
|
||||
STATE_TEARDOWN,
|
||||
STATE_SET_PARAMETER,
|
||||
STATE_DISCONNECTED
|
||||
} pa_rtsp_state;
|
||||
typedef void (*pa_rtsp_cb_t)(pa_rtsp_client *c, pa_rtsp_state state, pa_headerlist* hl, void *userdata);
|
||||
|
||||
pa_rtsp_client* pa_rtsp_client_new(pa_mainloop_api *mainloop, const char* hostname, uint16_t port, const char* useragent);
|
||||
void pa_rtsp_client_free(pa_rtsp_client* c);
|
||||
|
||||
int pa_rtsp_connect(pa_rtsp_client* c);
|
||||
void pa_rtsp_set_callback(pa_rtsp_client *c, pa_rtsp_cb_t callback, void *userdata);
|
||||
|
||||
void pa_rtsp_disconnect(pa_rtsp_client* c);
|
||||
|
||||
const char* pa_rtsp_localip(pa_rtsp_client* c);
|
||||
uint32_t pa_rtsp_serverport(pa_rtsp_client* c);
|
||||
void pa_rtsp_set_url(pa_rtsp_client* c, const char* url);
|
||||
void pa_rtsp_add_header(pa_rtsp_client *c, const char* key, const char* value);
|
||||
void pa_rtsp_remove_header(pa_rtsp_client *c, const char* key);
|
||||
|
||||
int pa_rtsp_announce(pa_rtsp_client* c, const char* sdp);
|
||||
|
||||
int pa_rtsp_setup(pa_rtsp_client* c);
|
||||
int pa_rtsp_record(pa_rtsp_client* c, uint16_t* seq, uint32_t* rtptime);
|
||||
int pa_rtsp_teardown(pa_rtsp_client* c);
|
||||
|
||||
int pa_rtsp_setparameter(pa_rtsp_client* c, const char* param);
|
||||
int pa_rtsp_flush(pa_rtsp_client* c, uint16_t seq, uint32_t rtptime);
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue