diff options
-rw-r--r-- | ChangeLog | 35 | ||||
-rw-r--r-- | configure.ac | 125 | ||||
-rw-r--r-- | ext/apexsink/LGPL-3.0.txt | 165 | ||||
-rw-r--r-- | ext/apexsink/Makefile.am | 8 | ||||
-rw-r--r-- | ext/apexsink/gstapexplugin.c | 44 | ||||
-rw-r--r-- | ext/apexsink/gstapexraop.c | 690 | ||||
-rw-r--r-- | ext/apexsink/gstapexraop.h | 124 | ||||
-rw-r--r-- | ext/apexsink/gstapexsink.c | 571 | ||||
-rw-r--r-- | ext/apexsink/gstapexsink.h | 85 |
9 files changed, 1847 insertions, 0 deletions
@@ -1,3 +1,38 @@ +2008-08-28 Sebastian Dröge <sebastian.droege@collabora.co.uk> + + Patch by: Jérémie Bernard <gremimail at gmail dot com> + + * configure.ac: + * ext/apexsink/LGPL-3.0.txt: + * ext/apexsink/Makefile.am: + * ext/apexsink/gstapexplugin.c: (plugin_init): + * ext/apexsink/gstapexraop.c: (g_strdel), (gst_apexraop_send), + (gst_apexraop_recv), (gst_apexraop_new), (gst_apexraop_free), + (gst_apexraop_set_host), (gst_apexraop_get_host), + (gst_apexraop_set_port), (gst_apexraop_get_port), + (gst_apexraop_set_useragent), (gst_apexraop_get_useragent), + (gst_apexraop_connect), (gst_apexraop_get_jacktype), + (gst_apexraop_get_jackstatus), (gst_apexraop_close), + (gst_apexraop_set_volume), (gst_apexraop_write_bits), + (gst_apexraop_write), (gst_apexraop_flush): + * ext/apexsink/gstapexraop.h: + * ext/apexsink/gstapexsink.c: (gst_apexsink_jackstatus_get_type), + (gst_apexsink_jacktype_get_type), (gst_apexsink_interfaces_init), + (gst_apexsink_implements_interface_init), + (gst_apexsink_mixer_interface_init), + (gst_apexsink_interface_supported), + (gst_apexsink_mixer_list_tracks), (gst_apexsink_mixer_set_volume), + (gst_apexsink_mixer_get_volume), (gst_apexsink_base_init), + (gst_apexsink_class_init), (gst_apexsink_init), + (gst_apexsink_set_property), (gst_apexsink_get_property), + (gst_apexsink_finalise), (gst_apexsink_open), + (gst_apexsink_prepare), (gst_apexsink_write), + (gst_apexsink_unprepare), (gst_apexsink_delay), + (gst_apexsink_reset), (gst_apexsink_close): + * ext/apexsink/gstapexsink.h: + Add apexsink for audio output to Apple AirPort Express Wireless + devices. Fixes bug #542510. + 2008-08-28 Wim Taymans <wim.taymans@collabora.co.uk> * gst/rtpmanager/gstrtpsession.c: (gst_rtp_session_send_rtcp), diff --git a/configure.ac b/configure.ac index 48ca5480..b24c3ad9 100644 --- a/configure.ac +++ b/configure.ac @@ -346,6 +346,129 @@ AG_GST_CHECK_FEATURE(AMRWB, [amrwb library], amrwb, [ AC_SUBST(AMRWB_LIBS)) ]) +dnl *** apexsink *** +translit(dnm, m, l) AM_CONDITIONAL(USE_APEXSINK, true) +AG_GST_CHECK_FEATURE(APEXSINK, [AirPort Express Wireless sink], apexsink, [ + + HAVE_APEXSINK="yes" + # The big search for OpenSSL + # copied from openssh's configure.ac + AC_ARG_WITH(ssl-dir, + [ --with-ssl-dir=PATH Specify path to OpenSSL installation ], + [ + if test "x$withval" != "xno" ; then + tryssldir=$withval + fi + ] + ) + + AC_SEARCH_LIBS(socket, [socket]) + AC_SEARCH_LIBS(gethostbyname, [nsl]) + + saved_LIBS="$LIBS" + saved_LDFLAGS="$LDFLAGS" + saved_CPPFLAGS="$CPPFLAGS" + if test "x$prefix" != "xNONE" ; then + tryssldir="$tryssldir $prefix" + fi + AC_CACHE_CHECK([for OpenSSL directory], ac_cv_openssldir, [ + for ssldir in $tryssldir "" /usr/local/openssl /usr/lib/openssl /usr/local/ssl /usr/lib/ssl /usr/local /usr/athena /usr/pkg /opt /opt/openssl ; do + CPPFLAGS="$saved_CPPFLAGS" + LDFLAGS="$saved_LDFLAGS" + LIBS="$saved_LIBS -lssl -lcrypto" + + # Skip directories if they don't exist + if test ! -z "$ssldir" -a ! -d "$ssldir" ; then + continue; + fi + if test ! -z "$ssldir" -a "x$ssldir" != "x/usr"; then + # Try to use $ssldir/lib if it exists, otherwise + # $ssldir + if test -d "$ssldir/lib" ; then + LDFLAGS="-L$ssldir/lib $saved_LDFLAGS" + if test ! -z "$need_dash_r" ; then + LDFLAGS="-R$ssldir/lib $LDFLAGS" + fi + else + LDFLAGS="-L$ssldir $saved_LDFLAGS" + if test ! -z "$need_dash_r" ; then + LDFLAGS="-R$ssldir $LDFLAGS" + fi + fi + # Try to use $ssldir/include if it exists, otherwise + # $ssldir + if test -d "$ssldir/include" ; then + CPPFLAGS="-I$ssldir/include $saved_CPPFLAGS" + else + CPPFLAGS="-I$ssldir $saved_CPPFLAGS" + fi + fi + + # Basic test to check for compatible version and correct linking + # *does not* test for RSA - that comes later. + AC_TRY_RUN( + [ +#include <string.h> +#include <openssl/rand.h> +int main(void) +{ + char a[2048]; + memset(a, 0, sizeof(a)); + RAND_add(a, sizeof(a), sizeof(a)); + return(RAND_status() <= 0); +} + ], + [ + found_crypto=1 + break; + ], [] + ) + + if test ! -z "$found_crypto" ; then + break; + fi + done + + if test -z "$found_crypto" ; then + HAVE_APEXSINK="no" + fi + if test -z "$ssldir" ; then + ssldir="(system)" + fi + + ac_cv_openssldir=$ssldir + ]) + if (test ! -z "$ac_cv_openssldir" && test "x$ac_cv_openssldir" != "x(system)") ; + then + dnl Need to recover ssldir - test above runs in subshell + ssldir=$ac_cv_openssldir + if test ! -z "$ssldir" -a "x$ssldir" != "x/usr"; then + # Try to use $ssldir/lib if it exists, otherwise + # $ssldir + if test -d "$ssldir/lib" ; then + APEXSINK_LIBS="-L$ssldir/lib $saved_LDFLAGS" + if test ! -z "$need_dash_r" ; then + APEXSINK_LIBS="-R$ssldir/lib $LDFLAGS" + fi + else + APEXSINK_LDFLAGS="-L$ssldir $saved_LDFLAGS" + if test ! -z "$need_dash_r" ; then + APEXSINK_LIBS="-R$ssldir $LDFLAGS" + fi + fi + # Try to use $ssldir/include if it exists, otherwise + # $ssldir + if test -d "$ssldir/include" ; then + APEXSINK_CFLAGS="-I$ssldir/include $saved_CPPFLAGS" + else + APEXSINK_CFLAGS="-I$ssldir $saved_CPPFLAGS" + fi + fi + fi + APEXSINK_LIBS="$APEXSINK_LIBS $LIBS" + LIBS="$saved_LIBS" +]) + dnl *** BZ2 *** translit(dnm, m, l) AM_CONDITIONAL(USE_BZ2, true) AG_GST_CHECK_FEATURE(BZ2, [bz2 library], bz2, [ @@ -1146,6 +1269,7 @@ AM_CONDITIONAL(USE_QUICKTIME, false) AM_CONDITIONAL(USE_VCD, false) AM_CONDITIONAL(USE_ALSA, false) AM_CONDITIONAL(USE_AMRWB, false) +AM_CONDITIONAL(USE_APEXSINK, false) AM_CONDITIONAL(USE_BZ2, false) AM_CONDITIONAL(USE_CDAUDIO, false) AM_CONDITIONAL(USE_CELT, false) @@ -1308,6 +1432,7 @@ examples/directfb/Makefile examples/switch/Makefile ext/amrwb/Makefile ext/alsaspdif/Makefile +ext/apexsink/Makefile ext/bz2/Makefile ext/cdaudio/Makefile ext/celt/Makefile diff --git a/ext/apexsink/LGPL-3.0.txt b/ext/apexsink/LGPL-3.0.txt new file mode 100644 index 00000000..fc8a5de7 --- /dev/null +++ b/ext/apexsink/LGPL-3.0.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/ext/apexsink/Makefile.am b/ext/apexsink/Makefile.am new file mode 100644 index 00000000..2b8ca57d --- /dev/null +++ b/ext/apexsink/Makefile.am @@ -0,0 +1,8 @@ +plugin_LTLIBRARIES = libgstapexsink.la + +libgstapexsink_la_SOURCES = gstapexplugin.c gstapexraop.c gstapexsink.c +libgstapexsink_la_CFLAGS = $(APEXSINK_CFLAGS) $(GST_PLUGINS_BASE_CFLAGS) $(GST_CFLAGS) +libgstapexsink_la_LIBADD = $(APEXSINK_LIBS) $(GST_PLUGINS_BASE_LIBS) $(GST_LIBS) -lgstaudio-$(GST_MAJORMINOR) -lgstinterfaces-$(GST_MAJORMINOR) +libgstapexsink_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS) + +noinst_HEADERS = gstapexraop.h gstapexsink.h diff --git a/ext/apexsink/gstapexplugin.c b/ext/apexsink/gstapexplugin.c new file mode 100644 index 00000000..014d5a2d --- /dev/null +++ b/ext/apexsink/gstapexplugin.c @@ -0,0 +1,44 @@ +/* GStreamer AirPort Express Plugin + * + * Copyright (C) 2008 Jérémie Bernard [GRemi] <gremimail@gmail.com> + * + * gstapexpugin.c + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; 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 <gst/gst.h> +#include <gstapexsink.h> + +static gboolean +plugin_init (GstPlugin * plugin) +{ + return gst_element_register (plugin, GST_APEX_SINK_NAME, GST_RANK_PRIMARY, + GST_TYPE_APEX_SINK); +} + +/* plugin export resolution */ +GST_PLUGIN_DEFINE + (GST_VERSION_MAJOR, + GST_VERSION_MINOR, + "apex", + "Apple AirPort Express Plugin", + plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN) diff --git a/ext/apexsink/gstapexraop.c b/ext/apexsink/gstapexraop.c new file mode 100644 index 00000000..164a4ccf --- /dev/null +++ b/ext/apexsink/gstapexraop.c @@ -0,0 +1,690 @@ +/* GStreamer - Remote Audio Access Protocol (RAOP) as used in Apple iTunes to stream music to the Airport Express (ApEx) - + * + * RAOP is based on the Real Time Streaming Protocol (RTSP) but with an extra challenge-response RSA based authentication step. + * This interface accepts RAW PCM data and set it as AES encrypted ALAC while performing emission. + * + * Copyright (C) 2008 Jérémie Bernard [GRemi] <gremimail@gmail.com> + * + * gstapexraop.c + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; 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 "gstapexraop.h" + +/* private constants */ +#define GST_APEX_RAOP_VOLUME_MIN -144 +#define GST_APEX_RAOP_VOLUME_MAX 0 + +#define GST_APEX_RAOP_HDR_DEFAULT_LENGTH 1024 +#define GST_APEX_RAOP_SDP_DEFAULT_LENGTH 2048 + +const static gchar GST_APEX_RAOP_RSA_PUBLIC_MOD[] = + "59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUtwC" + "5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDR" + "KSKv6kDqnw4UwPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuB" + "OitnZ/bDzPHrTOZz0Dew0uowxf/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJ" + "Q+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/UAaHqn9JdsBWLUEpVviYnh" + "imNVvYFZeCXg/IdTQ+x4IRdiXNv5hEew=="; + +const static gchar GST_APEX_RAOP_RSA_PUBLIC_EXP[] = "AQAB"; + +const static gchar GST_APEX_RAOP_USER_AGENT[] = + "iTunes/4.6 (Macintosh; U; PPC Mac OS X 10.3)"; + +const static guchar GST_APEX_RAOP_FRAME_HEADER[] = { + 0x24, 0x00, 0x00, 0x00, + 0xF0, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 +}; + +const static int GST_APEX_RAOP_FRAME_HEADER_SIZE = 16; + +const static int GST_APEX_RAOP_ALAC_HEADER_SIZE = 3; + +/* string extra utility */ +static gint +g_strdel (gchar * str, gchar rc) +{ + int i = 0, j = 0, len, num = 0; + len = strlen (str); + while (i < len) { + if (str[i] == rc) { + for (j = i; j < len; j++) + str[j] = str[j + 1]; + len--; + num++; + } else { + i++; + } + } + return num; +} + +/* socket utilities */ +static int +gst_apexraop_send (int desc, void *data, size_t len) +{ + int total = 0, bytesleft = len, n = 0; + + while (total < len) { + n = send (desc, ((const char *) data) + total, bytesleft, 0); + if (n == -1) + break; + total += n; + bytesleft -= n; + } + + return n == -1 ? -1 : total; +} + +static int +gst_apexraop_recv (int desc, void *data, size_t len) +{ + bzero (data, len); + return recv (desc, data, len, 0); +} + +/* public opaque handle resolution */ +typedef struct +{ + guchar aes_ky[AES_BLOCK_SIZE]; /* AES random key */ + guchar aes_iv[AES_BLOCK_SIZE]; /* AES random initial vector */ + + guchar url_abspath[16]; /* header url random absolute path addon, ANNOUNCE id */ + gint cseq; /* header rtsp inc cseq */ + guchar cid[24]; /* header client instance id */ + gchar *session; /* header raop negotiated session id, once SETUP performed */ + gchar *ua; /* header user agent */ + + GstApExJackType jack_type; /* APEX connected jack type, once ANNOUNCE performed */ + GstApExJackStatus jack_status; /* APEX connected jack status, once ANNOUNCE performed */ + + gchar *host; /* APEX target ip */ + guint ctrl_port; /* APEX target control port */ + guint data_port; /* APEX negotiated data port, once SETUP performed */ + + int ctrl_sd; /* control socket */ + struct sockaddr_in ctrl_sd_in; + + int data_sd; /* data socket */ + struct sockaddr_in data_sd_in; +} +_GstApExRAOP; + +/* raop apex struct allocation */ +GstApExRAOP * +gst_apexraop_new (const gchar * host, const guint16 port) +{ + _GstApExRAOP *apexraop; + + apexraop = (_GstApExRAOP *) g_malloc0 (sizeof (_GstApExRAOP)); + + apexraop->host = g_strdup (host); + apexraop->ctrl_port = port; + apexraop->ua = g_strdup (GST_APEX_RAOP_USER_AGENT); + apexraop->jack_type = GST_APEX_JACK_TYPE_UNDEFINED; + apexraop->jack_status = GST_APEX_JACK_STATUS_DISCONNECTED; + + return (GstApExRAOP *) apexraop; +} + +/* raop apex struct freeing */ +void +gst_apexraop_free (GstApExRAOP * con) +{ + _GstApExRAOP *conn; + conn = (_GstApExRAOP *) con; + + g_free (conn->host); + g_free (conn->session); + g_free (conn->ua); + g_free (conn); +} + +/* host affectation */ +void +gst_apexraop_set_host (GstApExRAOP * con, const gchar * host) +{ + _GstApExRAOP *conn; + conn = (_GstApExRAOP *) con; + + g_free (conn->host); + conn->host = g_strdup (host); +} + +/* host reader */ +gchar * +gst_apexraop_get_host (GstApExRAOP * con) +{ + _GstApExRAOP *conn; + conn = (_GstApExRAOP *) con; + + return g_strdup (conn->host); +} + +/* control port affectation */ +void +gst_apexraop_set_port (GstApExRAOP * con, const guint16 port) +{ + _GstApExRAOP *conn; + conn = (_GstApExRAOP *) con; + + conn->ctrl_port = port; +} + +/* control port reader */ +guint16 +gst_apexraop_get_port (GstApExRAOP * con) +{ + _GstApExRAOP *conn; + conn = (_GstApExRAOP *) con; + + return conn->ctrl_port; +} + +/* user agent affectation */ +void +gst_apexraop_set_useragent (GstApExRAOP * con, const gchar * useragent) +{ + _GstApExRAOP *conn; + conn = (_GstApExRAOP *) con; + + g_free (conn->ua); + conn->ua = g_strdup (useragent); +} + +/* user agent reader */ +gchar * +gst_apexraop_get_useragent (GstApExRAOP * con) +{ + _GstApExRAOP *conn; + conn = (_GstApExRAOP *) con; + + return g_strdup (conn->ua); +} + +/* raop apex connection sequence */ +GstRTSPStatusCode +gst_apexraop_connect (GstApExRAOP * con) +{ + gchar *ac, *ky, *iv, *s, inaddr[INET_ADDRSTRLEN], + creq[GST_APEX_RAOP_SDP_DEFAULT_LENGTH], + hreq[GST_APEX_RAOP_HDR_DEFAULT_LENGTH], *req; + RSA *rsa; + guchar *mod, *exp, buf[4 + 8 + 16], rsakey[512]; + gsize size; + struct sockaddr_in ioaddr; + socklen_t iolen; + GstRTSPStatusCode res; + _GstApExRAOP *conn; + + conn = (_GstApExRAOP *) con; + + if ((conn->ctrl_sd = socket (AF_INET, SOCK_STREAM, 0)) < 0) + return GST_RTSP_STS_DESTINATION_UNREACHABLE; + + conn->ctrl_sd_in.sin_family = AF_INET; + conn->ctrl_sd_in.sin_port = htons (conn->ctrl_port); + + if (!inet_aton (conn->host, &conn->ctrl_sd_in.sin_addr)) { + struct hostent *hp = (struct hostent *) gethostbyname (conn->host); + if (hp == NULL) + return GST_RTSP_STS_DESTINATION_UNREACHABLE; + memcpy (&conn->ctrl_sd_in.sin_addr, hp->h_addr, hp->h_length); + } + + if (connect (conn->ctrl_sd, (struct sockaddr *) &conn->ctrl_sd_in, + sizeof (conn->ctrl_sd_in)) < 0) + return GST_RTSP_STS_DESTINATION_UNREACHABLE; + + RAND_bytes (buf, sizeof (buf)); + sprintf ((gchar *) conn->url_abspath, "%lu", *((gulong *) buf)); + ac = g_base64_encode (buf + 12, 16); + g_strdel (ac, '='); + sprintf ((char *) conn->cid, "%08lx%08lx", *((gulong *) (buf + 4)), + *((gulong *) (buf + 8))); + + RAND_bytes (conn->aes_ky, AES_BLOCK_SIZE); + RAND_bytes (conn->aes_iv, AES_BLOCK_SIZE); + + rsa = RSA_new (); + mod = g_base64_decode (GST_APEX_RAOP_RSA_PUBLIC_MOD, &size); + rsa->n = BN_bin2bn (mod, size, NULL); + exp = g_base64_decode (GST_APEX_RAOP_RSA_PUBLIC_EXP, &size); + rsa->e = BN_bin2bn (exp, size, NULL); + size = + RSA_public_encrypt (AES_BLOCK_SIZE, conn->aes_ky, rsakey, rsa, + RSA_PKCS1_OAEP_PADDING); + + ky = g_base64_encode (rsakey, size); + iv = g_base64_encode (conn->aes_iv, AES_BLOCK_SIZE); + g_strdel (ky, '='); + g_strdel (iv, '='); + + iolen = sizeof (struct sockaddr); + getsockname (conn->ctrl_sd, (struct sockaddr *) &ioaddr, &iolen); + inet_ntop (AF_INET, &(ioaddr.sin_addr), inaddr, INET_ADDRSTRLEN); + + sprintf (creq, + "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 %d 0 %d 40 10 14 %d 255 0 0 %d\r\n" + "a=rsaaeskey:%s\r\n" + "a=aesiv:%s\r\n", + conn->url_abspath, + inaddr, + conn->host, + GST_APEX_RAOP_SAMPLES_PER_FRAME, + GST_APEX_RAOP_BYTES_PER_CHANNEL * 8, + GST_APEX_RAOP_CHANNELS, GST_APEX_RAOP_BITRATE, ky, iv); + + sprintf (hreq, + "ANNOUNCE rtsp://%s/%s RTSP/1.0\r\n" + "CSeq: %d\r\n" + "Client-Instance: %s\r\n" + "User-Agent: %s\r\n" + "Content-Type: application/sdp\r\n" + "Content-Length: %d\r\n" + "Apple-Challenge: %s\r\n", + conn->host, + conn->url_abspath, ++conn->cseq, conn->cid, conn->ua, strlen (creq), ac); + + RSA_free (rsa); + g_free (ky); + g_free (iv); + g_free (ac); + g_free (mod); + g_free (exp); + + req = g_strconcat (hreq, "\r\n", creq, NULL); + + if (gst_apexraop_send (conn->ctrl_sd, req, strlen (req)) <= 0) { + g_free (req); + return GST_RTSP_STS_GONE; + } + + g_free (req); + + if (gst_apexraop_recv (conn->ctrl_sd, hreq, + GST_APEX_RAOP_HDR_DEFAULT_LENGTH) <= 0) + return GST_RTSP_STS_GONE; + + sscanf (hreq, "%*s %d", (int *) &res); + + if (res != GST_RTSP_STS_OK) + return res; + + s = g_strrstr (hreq, "Audio-Jack-Status"); + + if (s != NULL) { + gchar status[128]; + sscanf (s, "%*s %s", status); + + if (strcmp (status, "connected;") == 0) + conn->jack_status = GST_APEX_JACK_STATUS_CONNECTED; + else if (strcmp (status, "disconnected;") == 0) + conn->jack_status = GST_APEX_JACK_STATUS_DISCONNECTED; + else + conn->jack_status = GST_APEX_JACK_STATUS_UNDEFINED; + + s = g_strrstr (s, "type="); + + if (s != NULL) { + strtok (s, "="); + s = strtok (NULL, "\n"); + + if (strcmp (s, "analog")) + conn->jack_type = GST_APEX_JACK_TYPE_ANALOG; + else if (strcmp (s, "digital")) + conn->jack_type = GST_APEX_JACK_TYPE_DIGITAL; + else + conn->jack_type = GST_APEX_JACK_TYPE_UNDEFINED; + } + } + + sprintf (hreq, + "SETUP rtsp://%s/%s RTSP/1.0\r\n" + "CSeq: %d\r\n" + "Client-Instance: %s\r\n" + "User-Agent: %s\r\n" + "Transport: RTP/AVP/TCP;unicast;interleaved=0-1;mode=record\r\n" + "\r\n", conn->host, conn->url_abspath, ++conn->cseq, conn->cid, conn->ua); + + if (gst_apexraop_send (conn->ctrl_sd, hreq, strlen (hreq)) <= 0) + return GST_RTSP_STS_GONE; + + if (gst_apexraop_recv (conn->ctrl_sd, hreq, + GST_APEX_RAOP_HDR_DEFAULT_LENGTH) <= 0) + return GST_RTSP_STS_GONE; + + sscanf (hreq, "%*s %d", (int *) &res); + + if (res != GST_RTSP_STS_OK) + return res; + + s = g_strrstr (hreq, "Session"); + + if (s != NULL) { + gchar session[128]; + sscanf (s, "%*s %s", session); + conn->session = g_strdup (session); + } else + return GST_RTSP_STS_PRECONDITION_FAILED; + + s = g_strrstr (hreq, "server_port"); + if (s != NULL) { + sscanf (s, "server_port=%d", &conn->data_port); + } else + return GST_RTSP_STS_PRECONDITION_FAILED; + + sprintf (hreq, + "RECORD rtsp://%s/%s RTSP/1.0\r\n" + "CSeq: %d\r\n" + "Client-Instance: %s\r\n" + "User-Agent: %s\r\n" + "Session: %s\r\n" + "Range: npt=0-\r\n" + "RTP-Info: seq=0;rtptime=0\r\n" + "\r\n", + conn->host, + conn->url_abspath, ++conn->cseq, conn->cid, conn->ua, conn->session); + + if (gst_apexraop_send (conn->ctrl_sd, hreq, strlen (hreq)) <= 0) + return GST_RTSP_STS_GONE; + + if (gst_apexraop_recv (conn->ctrl_sd, hreq, + GST_APEX_RAOP_HDR_DEFAULT_LENGTH) <= 0) + return GST_RTSP_STS_GONE; + + sscanf (hreq, "%*s %d", (int *) &res); + + if (res != GST_RTSP_STS_OK) + return res; + + if ((conn->data_sd = socket (AF_INET, SOCK_STREAM, 0)) < 0) + return GST_RTSP_STS_DESTINATION_UNREACHABLE; + + conn->data_sd_in.sin_family = AF_INET; + conn->data_sd_in.sin_port = htons (conn->data_port); + + memcpy (&conn->data_sd_in.sin_addr, &conn->ctrl_sd_in.sin_addr, + sizeof (conn->ctrl_sd_in.sin_addr)); + + if (connect (conn->data_sd, (struct sockaddr *) &conn->data_sd_in, + sizeof (conn->data_sd_in)) < 0) + return GST_RTSP_STS_DESTINATION_UNREACHABLE; + + return res; +} + +/* raop apex jack type access */ +GstApExJackType +gst_apexraop_get_jacktype (GstApExRAOP * con) +{ + _GstApExRAOP *conn; + + conn = (_GstApExRAOP *) con; + + if (!conn) + return GST_APEX_JACK_TYPE_UNDEFINED; + + return conn->jack_type; +} + +/* raop apex jack status access */ +GstApExJackStatus +gst_apexraop_get_jackstatus (GstApExRAOP * con) +{ + _GstApExRAOP *conn; + + conn = (_GstApExRAOP *) con; + + if (!conn) + return GST_APEX_JACK_STATUS_UNDEFINED; + + return conn->jack_status; +} + +/* raop apex sockets close */ +void +gst_apexraop_close (GstApExRAOP * con) +{ + gchar hreq[GST_APEX_RAOP_HDR_DEFAULT_LENGTH]; + _GstApExRAOP *conn; + + conn = (_GstApExRAOP *) con; + + sprintf (hreq, + "TEARDOWN rtsp://%s/%s RTSP/1.0\r\n" + "CSeq: %d\r\n" + "Client-Instance: %s\r\n" + "User-Agent: %s\r\n" + "Session: %s\r\n" + "\r\n", + conn->host, + conn->url_abspath, ++conn->cseq, conn->cid, conn->ua, conn->session); + + gst_apexraop_send (conn->ctrl_sd, hreq, strlen (hreq)); + gst_apexraop_recv (conn->ctrl_sd, hreq, GST_APEX_RAOP_HDR_DEFAULT_LENGTH); + + if (conn->ctrl_sd != 0) + close (conn->ctrl_sd); + if (conn->data_sd != 0) + close (conn->data_sd); +} + +/* raop apex volume set */ +GstRTSPStatusCode +gst_apexraop_set_volume (GstApExRAOP * con, const guint volume) +{ + gint v; + gchar creq[GST_APEX_RAOP_SDP_DEFAULT_LENGTH], + hreq[GST_APEX_RAOP_HDR_DEFAULT_LENGTH], *req, vol[128]; + GstRTSPStatusCode res; + _GstApExRAOP *conn; + + conn = (_GstApExRAOP *) con; + + v = GST_APEX_RAOP_VOLUME_MIN + (GST_APEX_RAOP_VOLUME_MAX - + GST_APEX_RAOP_VOLUME_MIN) * volume / 100.; + sprintf (vol, "volume: %d.000000\r\n", v); + + sprintf (creq, "%s\r\n", vol); + + sprintf (hreq, + "SET_PARAMETER rtsp://%s/%s RTSP/1.0\r\n" + "CSeq: %d\r\n" + "Client-Instance: %s\r\n" + "User-Agent: %s\r\n" + "Session: %s\r\n" + "Content-Type: text/parameters\r\n" + "Content-Length: %d\r\n", + conn->host, + conn->url_abspath, + ++conn->cseq, conn->cid, conn->ua, conn->session, strlen (creq) + ); + + req = g_strconcat (hreq, "\r\n", creq, NULL); + + if (gst_apexraop_send (conn->ctrl_sd, req, strlen (req)) <= 0) { + g_free (req); + return GST_RTSP_STS_GONE; + } + + g_free (req); + + if (gst_apexraop_recv (conn->ctrl_sd, hreq, + GST_APEX_RAOP_HDR_DEFAULT_LENGTH) <= 0) + return GST_RTSP_STS_GONE; + + sscanf (hreq, "%*s %d", (int *) &res); + + return res; +} + +/* raop apex raw data alac encapsulation, encryption and emission, http://wiki.multimedia.cx/index.php?title=Apple_Lossless_Audio_Coding */ +static void inline +gst_apexraop_write_bits (guchar * buffer, int data, int numbits, + int *bit_offset, int *byte_offset) +{ + const static guchar masks[] = + { 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF }; + + if (((*bit_offset) != 0) && (((*bit_offset) + numbits) > 8)) { + gint numwritebits; + guchar bitstowrite; + + numwritebits = 8 - (*bit_offset); + bitstowrite = + (guchar) ((data >> (numbits - numwritebits)) << (8 - (*bit_offset) - + numwritebits)); + buffer[(*byte_offset)] |= bitstowrite; + numbits -= numwritebits; + (*bit_offset) = 0; + (*byte_offset)++; + } + + while (numbits >= 8) { + guchar bitstowrite; + + bitstowrite = (guchar) ((data >> (numbits - 8)) & 0xFF); + buffer[(*byte_offset)] |= bitstowrite; + numbits -= 8; + (*bit_offset) = 0; + (*byte_offset)++; + } + + if (numbits > 0) { + guchar bitstowrite; + bitstowrite = + (guchar) ((data & masks[numbits]) << (8 - (*bit_offset) - numbits)); + buffer[(*byte_offset)] |= bitstowrite; + (*bit_offset) += numbits; + if ((*bit_offset) == 8) { + (*byte_offset)++; + (*bit_offset) = 0; + } + } +} + +guint +gst_apexraop_write (GstApExRAOP * con, gpointer rawdata, guint length) +{ + guchar *buffer, *frame_data; + gushort len; + gint bit_offset, byte_offset, i, out_len, res; + EVP_CIPHER_CTX aes_ctx; + _GstApExRAOP *conn; + + conn = (_GstApExRAOP *) con; + + buffer = + (guchar *) g_malloc0 (GST_APEX_RAOP_FRAME_HEADER_SIZE + + GST_APEX_RAOP_ALAC_HEADER_SIZE + length); + + memcpy (buffer, GST_APEX_RAOP_FRAME_HEADER, GST_APEX_RAOP_FRAME_HEADER_SIZE); + + len = + length + GST_APEX_RAOP_FRAME_HEADER_SIZE + + GST_APEX_RAOP_ALAC_HEADER_SIZE - 4; + buffer[2] = len >> 8; + buffer[3] = len & 0xff; + + bit_offset = 0; + byte_offset = 0; + frame_data = buffer + GST_APEX_RAOP_FRAME_HEADER_SIZE; + + gst_apexraop_write_bits (frame_data, 1, 3, &bit_offset, &byte_offset); /* channels, 0 mono, 1 stereo */ + gst_apexraop_write_bits (frame_data, 0, 4, &bit_offset, &byte_offset); /* unknown */ + gst_apexraop_write_bits (frame_data, 0, 8, &bit_offset, &byte_offset); /* unknown (12 bits) */ + gst_apexraop_write_bits (frame_data, 0, 4, &bit_offset, &byte_offset); + gst_apexraop_write_bits (frame_data, 0, 1, &bit_offset, &byte_offset); /* has size flag */ + gst_apexraop_write_bits (frame_data, 0, 2, &bit_offset, &byte_offset); /* unknown */ + gst_apexraop_write_bits (frame_data, 1, 1, &bit_offset, &byte_offset); /* no compression flag */ + + for (i = 0; i < length; i += 2) { + gst_apexraop_write_bits (frame_data, ((guchar *) rawdata)[i + 1], 8, + &bit_offset, &byte_offset); + gst_apexraop_write_bits (frame_data, ((guchar *) rawdata)[i], 8, + &bit_offset, &byte_offset); + } + + EVP_CIPHER_CTX_init (&aes_ctx); + EVP_CipherInit_ex (&aes_ctx, EVP_aes_128_cbc (), NULL, conn->aes_ky, + conn->aes_iv, AES_ENCRYPT); + EVP_CipherUpdate (&aes_ctx, frame_data, &out_len, frame_data, /*( */ + GST_APEX_RAOP_ALAC_HEADER_SIZE + + length /*) / AES_BLOCK_SIZE * AES_BLOCK_SIZE */ ); + EVP_CIPHER_CTX_cleanup (&aes_ctx); + + res = + gst_apexraop_send (conn->data_sd, buffer, + GST_APEX_RAOP_FRAME_HEADER_SIZE + GST_APEX_RAOP_ALAC_HEADER_SIZE + + length); + + g_free (buffer); + + return (guint) ((res >= + (GST_APEX_RAOP_FRAME_HEADER_SIZE + + GST_APEX_RAOP_ALAC_HEADER_SIZE)) ? (res - + GST_APEX_RAOP_FRAME_HEADER_SIZE - + GST_APEX_RAOP_ALAC_HEADER_SIZE) : 0); +} + +/* raop apex buffer flush */ +GstRTSPStatusCode +gst_apexraop_flush (GstApExRAOP * con) +{ + gchar hreq[GST_APEX_RAOP_HDR_DEFAULT_LENGTH]; + GstRTSPStatusCode res; + _GstApExRAOP *conn; + + conn = (_GstApExRAOP *) con; + + sprintf (hreq, + "FLUSH rtsp://%s/%s RTSP/1.0\r\n" + "CSeq: %d\r\n" + "Client-Instance: %s\r\n" + "User-Agent: %s\r\n" + "Session: %s\r\n" + "RTP-Info: seq=0;rtptime=0\r\n" + "\r\n", + conn->host, + conn->url_abspath, ++conn->cseq, conn->cid, conn->ua, conn->session); + + if (gst_apexraop_send (conn->ctrl_sd, hreq, strlen (hreq)) <= 0) + return GST_RTSP_STS_GONE; + + if (gst_apexraop_recv (conn->ctrl_sd, hreq, + GST_APEX_RAOP_HDR_DEFAULT_LENGTH) <= 0) + return GST_RTSP_STS_GONE; + + sscanf (hreq, "%*s %d", (int *) &res); + + return res; +} diff --git a/ext/apexsink/gstapexraop.h b/ext/apexsink/gstapexraop.h new file mode 100644 index 00000000..fe1ba41b --- /dev/null +++ b/ext/apexsink/gstapexraop.h @@ -0,0 +1,124 @@ +/* GStreamer - Remote Audio Access Protocol (RAOP) as used in Apple iTunes to stream music to the Airport Express (ApEx) - + * + * RAOP is based on the Real Time Streaming Protocol (RTSP) but with an extra challenge-response RSA based authentication step. + * This interface accepts RAW PCM data and set it as AES encrypted ALAC while performing emission. + * + * Copyright (C) 2008 Jérémie Bernard [GRemi] <gremimail@gmail.com> + * + * gstapexraop.h + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GST_APEXRAOP_H__ +#define __GST_APEXRAOP_H__ + +#include <gst/gst.h> +#include <gst/rtsp/gstrtspdefs.h> + +#include <openssl/rand.h> +#include <openssl/rsa.h> +#include <openssl/aes.h> +#include <openssl/evp.h> + +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +#include <netinet/in.h> +#include <netdb.h> + +#include <arpa/inet.h> + +G_BEGIN_DECLS + +/* raop fixed parameters */ +#define GST_APEX_RAOP_BITRATE 44100 +#define GST_APEX_RAOP_SAMPLES_PER_FRAME 4096 +#define GST_APEX_RAOP_BYTES_PER_CHANNEL 2 +#define GST_APEX_RAOP_CHANNELS 2 +#define GST_APEX_RAOP_BYTES_PER_SAMPLE (GST_APEX_RAOP_CHANNELS * GST_APEX_RAOP_BYTES_PER_CHANNEL) + +/* gst associated caps fields specification */ +#define GST_APEX_RAOP_INPUT_TYPE "audio/x-raw-int" +#define GST_APEX_RAOP_INPUT_WIDTH "16" +#define GST_APEX_RAOP_INPUT_DEPTH GST_APEX_RAOP_INPUT_WIDTH +#define GST_APEX_RAOP_INPUT_ENDIAN "LITTLE_ENDIAN" +#define GST_APEX_RAOP_INPUT_CHANNELS "2" +#define GST_APEX_RAOP_INPUT_BIT_RATE "44100" +#define GST_APEX_RAOP_INPUT_SIGNED "TRUE" + +typedef enum +{ + GST_APEX_JACK_TYPE_UNDEFINED = 0, + GST_APEX_JACK_TYPE_ANALOG, + GST_APEX_JACK_TYPE_DIGITAL, +} +GstApExJackType; + +typedef enum +{ + GST_APEX_JACK_STATUS_UNDEFINED = 0, + GST_APEX_JACK_STATUS_DISCONNECTED, + GST_APEX_JACK_STATUS_CONNECTED, +} +GstApExJackStatus; + +/* raop context handle */ +typedef struct +{ +} GstApExRAOP; + +/* host might be null and port might be 0 while instanciating */ +GstApExRAOP *gst_apexraop_new (const gchar * host, const guint16 port); +void gst_apexraop_free (GstApExRAOP * conn); + +/* must not be connected yet while setting the host target */ +void gst_apexraop_set_host (GstApExRAOP * conn, const gchar * host); +gchar *gst_apexraop_get_host (GstApExRAOP * conn); + +/* must not be connected yet while setting the port target */ +void gst_apexraop_set_port (GstApExRAOP * conn, const guint16 port); +guint16 gst_apexraop_get_port (GstApExRAOP * conn); + +/* optional affectation, default iTunes user agent internaly used */ +void gst_apexraop_set_useragent (GstApExRAOP * conn, const gchar * useragent); +gchar *gst_apexraop_get_useragent (GstApExRAOP * conn); + +/* once allocation and configuration performed, manages the raop ANNOUNCE, SETUP and RECORD sequences, + * open both ctrl and data channels */ +GstRTSPStatusCode gst_apexraop_connect (GstApExRAOP * conn); + +/* close the currently used session, manages raop TEARDOWN sequence and closes the used sockets */ +void gst_apexraop_close (GstApExRAOP * conn); + +/* once connected, set the apex target volume, manages SET_PARAMETER sequence */ +GstRTSPStatusCode gst_apexraop_set_volume (GstApExRAOP * conn, + const guint volume); + +/* write raw samples typed as defined by the fixed raop parameters, flush the apex buffer */ +guint gst_apexraop_write (GstApExRAOP * conn, gpointer rawdata, guint length); +GstRTSPStatusCode gst_apexraop_flush (GstApExRAOP * conn); + +/* retrieve the connected apex jack type and status */ +GstApExJackType gst_apexraop_get_jacktype (GstApExRAOP * conn); +GstApExJackStatus gst_apexraop_get_jackstatus (GstApExRAOP * conn); + +G_END_DECLS + +#endif + diff --git a/ext/apexsink/gstapexsink.c b/ext/apexsink/gstapexsink.c new file mode 100644 index 00000000..5957093d --- /dev/null +++ b/ext/apexsink/gstapexsink.c @@ -0,0 +1,571 @@ +/* GStreamer - AirPort Express Audio Sink - + * + * Remote Audio Access Protocol (RAOP) as used in Apple iTunes to stream music to the Airport Express (ApEx) - + * RAOP is based on the Real Time Streaming Protocol (RTSP) but with an extra challenge-response RSA based authentication step. + * + * RAW PCM input only as defined by the following GST_STATIC_PAD_TEMPLATE + * + * Copyright (C) 2008 Jérémie Bernard [GRemi] <gremimail@gmail.com> + * + * gstapexsink.c + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; 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 "gstapexsink.h" + +GST_DEBUG_CATEGORY_STATIC (apexsink_debug); +#define GST_CAT_DEFAULT apexsink_debug + +static GstStaticPadTemplate gst_apexsink_sink_factory = GST_STATIC_PAD_TEMPLATE + ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS + (GST_APEX_RAOP_INPUT_TYPE "," + "width = (int) " GST_APEX_RAOP_INPUT_WIDTH "," + "depth = (int) " GST_APEX_RAOP_INPUT_DEPTH "," + "endianness = (int) " GST_APEX_RAOP_INPUT_ENDIAN "," + "channels = (int) " GST_APEX_RAOP_INPUT_CHANNELS "," + "rate = (int) " GST_APEX_RAOP_INPUT_BIT_RATE "," + "signed = (boolean) " GST_APEX_RAOP_INPUT_SIGNED) + ); + + +enum +{ + APEX_PROP_HOST = 1, + APEX_PROP_PORT, + APEX_PROP_VOLUME, + APEX_PROP_JACK_TYPE, + APEX_PROP_JACK_STATUS, +}; + +#define DEFAULT_APEX_HOST "" +#define DEFAULT_APEX_PORT 5000 +#define DEFAULT_APEX_VOLUME 75 +#define DEFAULT_APEX_JACK_TYPE GST_APEX_JACK_TYPE_UNDEFINED +#define DEFAULT_APEX_JACK_STATUS GST_APEX_JACK_STATUS_UNDEFINED + +/* genum apex jack resolution */ +GType +gst_apexsink_jackstatus_get_type (void) +{ + static GType jackstatus_type = 0; + static GEnumValue jackstatus[] = { + {GST_APEX_JACK_STATUS_UNDEFINED, "GST_APEX_JACK_STATUS_UNDEFINED", + "Jack status undefined"}, + {GST_APEX_JACK_STATUS_DISCONNECTED, "GST_APEX_JACK_STATUS_DISCONNECTED", + "Jack disconnected"}, + {GST_APEX_JACK_STATUS_CONNECTED, "GST_APEX_JACK_STATUS_CONNECTED", + "Jack connected"}, + {0, NULL, NULL}, + }; + + if (!jackstatus_type) { + jackstatus_type = g_enum_register_static ("GstApExJackStatus", jackstatus); + } + + return jackstatus_type; +} + +GType +gst_apexsink_jacktype_get_type (void) +{ + static GType jacktype_type = 0; + static GEnumValue jacktype[] = { + {GST_APEX_JACK_TYPE_UNDEFINED, "GST_APEX_JACK_TYPE_UNDEFINED", + "Undefined jack type"}, + {GST_APEX_JACK_TYPE_ANALOG, "GST_APEX_JACK_TYPE_ANALOG", "Analog jack"}, + {GST_APEX_JACK_TYPE_DIGITAL, "GST_APEX_JACK_TYPE_DIGITAL", "Digital jack"}, + {0, NULL, NULL}, + }; + + if (!jacktype_type) { + jacktype_type = g_enum_register_static ("GstApExJackType", jacktype); + } + + return jacktype_type; +} + + +static void gst_apexsink_base_init (gpointer g_class); +static void gst_apexsink_class_init (GstApExSinkClass * klass); +static void gst_apexsink_init (GstApExSink * apexsink, + GstApExSinkClass * g_class); + +static void gst_apexsink_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_apexsink_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); +static void gst_apexsink_finalise (GObject * object); + +static gboolean gst_apexsink_open (GstAudioSink * asink); +static gboolean gst_apexsink_prepare (GstAudioSink * asink, + GstRingBufferSpec * spec); +static guint gst_apexsink_write (GstAudioSink * asink, gpointer data, + guint length); +static gboolean gst_apexsink_unprepare (GstAudioSink * asink); +static guint gst_apexsink_delay (GstAudioSink * asink); +static void gst_apexsink_reset (GstAudioSink * asink); +static gboolean gst_apexsink_close (GstAudioSink * asink); + +/* mixer interface standard api */ +static void gst_apexsink_interfaces_init (GType type); +static void gst_apexsink_implements_interface_init (GstImplementsInterfaceClass + * iface); +static void gst_apexsink_mixer_interface_init (GstMixerClass * iface); + +static gboolean gst_apexsink_interface_supported (GstImplementsInterface * + iface, GType iface_type); +static const GList *gst_apexsink_mixer_list_tracks (GstMixer * mixer); +static void gst_apexsink_mixer_set_volume (GstMixer * mixer, + GstMixerTrack * track, gint * volumes); +static void gst_apexsink_mixer_get_volume (GstMixer * mixer, + GstMixerTrack * track, gint * volumes); + +GST_BOILERPLATE_FULL (GstApExSink, gst_apexsink, GstAudioSink, + GST_TYPE_AUDIO_SINK, gst_apexsink_interfaces_init); + +/* apex sink interface(s) stuff */ +static void +gst_apexsink_interfaces_init (GType type) +{ + static const GInterfaceInfo implements_interface_info = + { (GInterfaceInitFunc) gst_apexsink_implements_interface_init, NULL, + NULL + }; + static const GInterfaceInfo mixer_interface_info = + { (GInterfaceInitFunc) gst_apexsink_mixer_interface_init, NULL, NULL }; + + g_type_add_interface_static (type, GST_TYPE_IMPLEMENTS_INTERFACE, + &implements_interface_info); + g_type_add_interface_static (type, GST_TYPE_MIXER, &mixer_interface_info); +} + +static void +gst_apexsink_implements_interface_init (GstImplementsInterfaceClass * iface) +{ + iface->supported = gst_apexsink_interface_supported; +} + +static void +gst_apexsink_mixer_interface_init (GstMixerClass * iface) +{ + GST_MIXER_TYPE (iface) = GST_MIXER_SOFTWARE; + + iface->list_tracks = gst_apexsink_mixer_list_tracks; + iface->set_volume = gst_apexsink_mixer_set_volume; + iface->get_volume = gst_apexsink_mixer_get_volume; +} + +static gboolean +gst_apexsink_interface_supported (GstImplementsInterface * iface, + GType iface_type) +{ + g_return_val_if_fail (iface_type == GST_TYPE_MIXER, FALSE); + + return TRUE; +} + +static const GList * +gst_apexsink_mixer_list_tracks (GstMixer * mixer) +{ + GstApExSink *apexsink = GST_APEX_SINK (mixer); + + return apexsink->tracks; +} + +static void +gst_apexsink_mixer_set_volume (GstMixer * mixer, GstMixerTrack * track, + gint * volumes) +{ + GstApExSink *apexsink = GST_APEX_SINK (mixer); + + apexsink->volume = volumes[0]; + + if (apexsink->gst_apexraop != NULL) + gst_apexraop_set_volume (apexsink->gst_apexraop, apexsink->volume); +} + +static void +gst_apexsink_mixer_get_volume (GstMixer * mixer, GstMixerTrack * track, + gint * volumes) +{ + GstApExSink *apexsink = GST_APEX_SINK (mixer); + + volumes[0] = apexsink->volume; +} + +/* sink base init */ +static void +gst_apexsink_base_init (gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS (g_class); + + gst_element_class_set_details_simple (element_class, + "Apple AirPort Express Audio Sink", "Sink/Audio/Wireless", + "Output stream to an AirPort Express", + "Jérémie Bernard [GRemi] <gremimail@gmail.com>"); + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&gst_apexsink_sink_factory)); +} + +/* sink class init */ +static void +gst_apexsink_class_init (GstApExSinkClass * klass) +{ + GST_DEBUG_CATEGORY_INIT (apexsink_debug, GST_APEX_SINK_NAME, 0, + "AirPort Express sink"); + + parent_class = g_type_class_peek_parent (klass); + + ((GObjectClass *) klass)->get_property = + GST_DEBUG_FUNCPTR (gst_apexsink_get_property); + ((GObjectClass *) klass)->set_property = + GST_DEBUG_FUNCPTR (gst_apexsink_set_property); + ((GObjectClass *) klass)->finalize = + GST_DEBUG_FUNCPTR (gst_apexsink_finalise); + + ((GstAudioSinkClass *) klass)->open = GST_DEBUG_FUNCPTR (gst_apexsink_open); + ((GstAudioSinkClass *) klass)->prepare = + GST_DEBUG_FUNCPTR (gst_apexsink_prepare); + ((GstAudioSinkClass *) klass)->write = GST_DEBUG_FUNCPTR (gst_apexsink_write); + ((GstAudioSinkClass *) klass)->unprepare = + GST_DEBUG_FUNCPTR (gst_apexsink_unprepare); + ((GstAudioSinkClass *) klass)->delay = GST_DEBUG_FUNCPTR (gst_apexsink_delay); + ((GstAudioSinkClass *) klass)->reset = GST_DEBUG_FUNCPTR (gst_apexsink_reset); + ((GstAudioSinkClass *) klass)->close = GST_DEBUG_FUNCPTR (gst_apexsink_close); + + g_object_class_install_property ((GObjectClass *) klass, APEX_PROP_HOST, + g_param_spec_string ("host", "Host", "AirPort Express target host", + DEFAULT_APEX_HOST, G_PARAM_READWRITE)); + g_object_class_install_property ((GObjectClass *) klass, APEX_PROP_PORT, + g_param_spec_uint ("port", "Port", "AirPort Express target port", 0, + 32000, DEFAULT_APEX_PORT, G_PARAM_READWRITE)); + g_object_class_install_property ((GObjectClass *) klass, APEX_PROP_VOLUME, + g_param_spec_uint ("volume", "Volume", "AirPort Express target volume", 0, + 100, DEFAULT_APEX_VOLUME, G_PARAM_READWRITE)); + g_object_class_install_property ((GObjectClass *) klass, APEX_PROP_JACK_TYPE, + g_param_spec_enum ("jack_type", "Jack Type", + "AirPort Express connected jack type", GST_APEX_SINK_JACKTYPE_TYPE, + DEFAULT_APEX_JACK_TYPE, G_PARAM_READABLE)); + g_object_class_install_property ((GObjectClass *) klass, + APEX_PROP_JACK_STATUS, g_param_spec_enum ("jack_status", "Jack Status", + "AirPort Express jack connection status", + GST_APEX_SINK_JACKSTATUS_TYPE, DEFAULT_APEX_JACK_STATUS, + G_PARAM_READABLE)); +} + +/* sink plugin instance init */ +static void +gst_apexsink_init (GstApExSink * apexsink, GstApExSinkClass * g_class) +{ + GstMixerTrack *track = NULL; + + track = g_object_new (GST_TYPE_MIXER_TRACK, NULL); + track->label = g_strdup ("Airport Express"); + track->num_channels = GST_APEX_RAOP_CHANNELS; + track->min_volume = 0; + track->max_volume = 100; + track->flags = GST_MIXER_TRACK_OUTPUT; + + apexsink->host = g_strdup (DEFAULT_APEX_HOST); + apexsink->port = DEFAULT_APEX_PORT; + apexsink->volume = DEFAULT_APEX_VOLUME; + apexsink->gst_apexraop = NULL; + apexsink->tracks = g_list_append (apexsink->tracks, track); + + GST_INFO_OBJECT (apexsink, + "ApEx sink default initialization, target=\"%s\", port=\"%d\", volume=\"%d\%\"", + apexsink->host, apexsink->port, apexsink->volume); +} + +/* apex sink set property */ +static void +gst_apexsink_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstApExSink *sink = GST_APEX_SINK (object); + + switch (prop_id) { + case APEX_PROP_HOST: + { + if (sink->gst_apexraop == NULL) { + g_free (sink->host); + sink->host = g_value_dup_string (value); + + GST_INFO_OBJECT (sink, "ApEx sink target set to \"%s\"", sink->host); + } else + G_OBJECT_WARN_INVALID_PSPEC (object, "host", prop_id, pspec); + } + break; + case APEX_PROP_PORT: + { + if (sink->gst_apexraop == NULL) { + sink->port = g_value_get_uint (value); + + GST_INFO_OBJECT (sink, "ApEx port set to \"%d\"", sink->port); + } else + G_OBJECT_WARN_INVALID_PSPEC (object, "port", prop_id, pspec); + } + break; + case APEX_PROP_VOLUME: + { + sink->volume = g_value_get_uint (value); + + if (sink->gst_apexraop != NULL) + gst_apexraop_set_volume (sink->gst_apexraop, sink->volume); + + GST_INFO_OBJECT (sink, "ApEx volume set to \"%d\%\"", sink->volume); + } + break; + default: + { + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } + break; + } +} + +/* apex sink get property */ +static void +gst_apexsink_get_property (GObject * object, guint prop_id, GValue * value, + GParamSpec * pspec) +{ + GstApExSink *sink = GST_APEX_SINK (object); + + switch (prop_id) { + case APEX_PROP_HOST: + { + g_value_set_string (value, sink->host); + } + break; + case APEX_PROP_PORT: + { + g_value_set_uint (value, sink->port); + } + break; + case APEX_PROP_VOLUME: + { + g_value_set_uint (value, sink->volume); + } + break; + case APEX_PROP_JACK_TYPE: + { + g_value_set_enum (value, gst_apexraop_get_jacktype (sink->gst_apexraop)); + } + break; + case APEX_PROP_JACK_STATUS: + { + g_value_set_enum (value, + gst_apexraop_get_jackstatus (sink->gst_apexraop)); + } + break; + default: + { + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } + break; + } +} + +/* apex sink finalize */ +static void +gst_apexsink_finalise (GObject * object) +{ + GstApExSink *sink = GST_APEX_SINK (object); + + if (sink->tracks) { + g_list_foreach (sink->tracks, (GFunc) g_object_unref, NULL); + g_list_free (sink->tracks); + sink->tracks = NULL; + } + + g_free (sink->host); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +/* sink open : open the device */ +static gboolean +gst_apexsink_open (GstAudioSink * asink) +{ + int res; + GstApExSink *apexsink = (GstApExSink *) asink; + + apexsink->gst_apexraop = gst_apexraop_new (apexsink->host, apexsink->port); + + if ((res = gst_apexraop_connect (apexsink->gst_apexraop)) != GST_RTSP_STS_OK) { + GST_ERROR_OBJECT (apexsink, + "%s : network or RAOP failure, connection refused or timeout, RTSP code=%d", + apexsink->host, res); + return FALSE; + } + + GST_INFO_OBJECT (apexsink, + "OPEN : ApEx sink successfully connected to \"%s:%d\", ANNOUNCE, SETUP and RECORD requests performed", + apexsink->host, apexsink->port); + + switch (gst_apexraop_get_jackstatus (apexsink->gst_apexraop)) { + case GST_APEX_JACK_STATUS_CONNECTED: + { + GST_INFO_OBJECT (apexsink, "OPEN : ApEx jack is connected"); + } + break; + case GST_APEX_JACK_STATUS_DISCONNECTED: + { + GST_WARNING_OBJECT (apexsink, "OPEN : ApEx jack is disconnected !"); + } + break; + default: + { + GST_WARNING_OBJECT (apexsink, "OPEN : ApEx jack status is undefined !"); + } + break; + } + + switch (gst_apexraop_get_jacktype (apexsink->gst_apexraop)) { + case GST_APEX_JACK_TYPE_ANALOG: + { + GST_INFO_OBJECT (apexsink, "OPEN : ApEx jack type is analog"); + } + break; + case GST_APEX_JACK_TYPE_DIGITAL: + { + GST_INFO_OBJECT (apexsink, "OPEN : ApEx jack type is digital"); + } + break; + default: + { + GST_WARNING_OBJECT (apexsink, "OPEN : ApEx jack type is undefined !"); + } + break; + } + + if ((res = + gst_apexraop_set_volume (apexsink->gst_apexraop, + apexsink->volume)) != GST_RTSP_STS_OK) { + GST_WARNING_OBJECT (apexsink, + "%s : could not set initial volume to \"%d\%\", RTSP code=%d", + apexsink->host, apexsink->volume, res); + } else { + GST_INFO_OBJECT (apexsink, + "OPEN : ApEx sink successfully set volume to \"%d\%\"", + apexsink->volume); + } + + return TRUE; +} + +/* prepare sink : configure the device with the specified format */ +static gboolean +gst_apexsink_prepare (GstAudioSink * asink, GstRingBufferSpec * spec) +{ + GstApExSink *apexsink = (GstApExSink *) asink; + + apexsink->latency_time = spec->latency_time; + + spec->segsize = + GST_APEX_RAOP_SAMPLES_PER_FRAME * GST_APEX_RAOP_BYTES_PER_SAMPLE; + spec->segtotal = 1; + + bzero (spec->silence_sample, sizeof (spec->silence_sample)); + + GST_INFO_OBJECT (apexsink, + "PREPARE : ApEx sink ready to stream at %dHz, %d bytes per sample, %d channels, %d bytes segments (%dkB/s)", + spec->rate, spec->bytes_per_sample, spec->channels, spec->segsize, + spec->rate * spec->bytes_per_sample / 1000); + + return TRUE; +} + +/* sink write : write samples to the device */ +static guint +gst_apexsink_write (GstAudioSink * asink, gpointer data, guint length) +{ + GstApExSink *apexsink = (GstApExSink *) asink; + + if (gst_apexraop_write (apexsink->gst_apexraop, data, length) != length) { + GST_INFO_OBJECT (apexsink, + "WRITE : %d bytes not fully sended, skipping frame samples...", length); + } else { + GST_INFO_OBJECT (apexsink, "WRITE : %d bytes sended", length); + + usleep ((gulong) ((length * 1000000.) / (GST_APEX_RAOP_BITRATE * + GST_APEX_RAOP_BYTES_PER_SAMPLE) - apexsink->latency_time)); + } + + return length; +} + +/* unprepare sink : undo operations done by prepare */ +static gboolean +gst_apexsink_unprepare (GstAudioSink * asink) +{ + GstApExSink *apexsink = (GstApExSink *) asink; + + GST_INFO_OBJECT (apexsink, "UNPREPARE"); + + return TRUE; +} + +/* delay sink : get the estimated number of samples written but not played yet by the device */ +static guint +gst_apexsink_delay (GstAudioSink * asink) +{ + GstApExSink *apexsink = (GstApExSink *) asink; + + GST_INFO_OBJECT (apexsink, "DELAY"); + + return 0; +} + +/* reset sink : unblock writes and flush the device */ +static void +gst_apexsink_reset (GstAudioSink * asink) +{ + int res; + GstApExSink *apexsink = (GstApExSink *) asink; + + GST_INFO_OBJECT (apexsink, "RESET : flushing buffer..."); + + if ((res = gst_apexraop_flush (apexsink->gst_apexraop)) == GST_RTSP_STS_OK) { + GST_INFO_OBJECT (apexsink, "RESET : ApEx buffer flush success"); + } else { + GST_WARNING_OBJECT (apexsink, + "RESET : could not flush ApEx buffer, RTSP code=%d", res); + } +} + +/* sink close : close the device */ +static gboolean +gst_apexsink_close (GstAudioSink * asink) +{ + GstApExSink *apexsink = (GstApExSink *) asink; + + gst_apexraop_close (apexsink->gst_apexraop); + gst_apexraop_free (apexsink->gst_apexraop); + + GST_INFO_OBJECT (apexsink, "CLOSE : ApEx sink closed connection"); + + return TRUE; +} diff --git a/ext/apexsink/gstapexsink.h b/ext/apexsink/gstapexsink.h new file mode 100644 index 00000000..11d2f1ef --- /dev/null +++ b/ext/apexsink/gstapexsink.h @@ -0,0 +1,85 @@ +/* GStreamer - AirPort Express (ApEx) Audio Sink - + * + * Remote Audio Access Protocol (RAOP) as used in Apple iTunes to stream music to the Airport Express (ApEx) - + * RAOP is based on the Real Time Streaming Protocol (RTSP) but with an extra challenge-response RSA based authentication step. + * + * RAW PCM input only as defined by the following GST_STATIC_PAD_TEMPLATE regarding the expected gstapexraop input format. + * + * Copyright (C) 2008 Jérémie Bernard [GRemi] <gremimail@gmail.com> + * + * gstapexsink.h + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GST_APEXSINK_H__ +#define __GST_APEXSINK_H__ + +#include "gstapexraop.h" + +#include <gst/audio/gstaudiosink.h> +#include <gst/interfaces/mixer.h> + +G_BEGIN_DECLS + +/* standard gstreamer macros */ +#define GST_TYPE_APEX_SINK (gst_apexsink_get_type()) +#define GST_APEX_SINK(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_APEX_SINK,GstApExSink)) +#define GST_APEX_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_APEX_SINK,GstApExSinkClass)) +#define GST_IS_APEX_SINK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_APEX_SINK)) +#define GST_IS_APEX_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_APEX_SINK)) +#define GST_APEX_SINK_CAST(obj) ((GstApExSink*)(obj)) +#define GST_APEX_SINK_NAME "apexsink" +#define GST_APEX_SINK_JACKTYPE_TYPE (gst_apexsink_jacktype_get_type()) +#define GST_APEX_SINK_JACKSTATUS_TYPE (gst_apexsink_jackstatus_get_type()) +/* ApEx classes declaration */ +typedef struct _GstApExSink GstApExSink; +typedef struct _GstApExSinkClass GstApExSinkClass; + +struct _GstApExSink +{ + /* base definition */ + GstAudioSink sink; + + /* public read/write sink properties */ + gchar *host; + guint port; + guint volume; + + /* private attributes : latency time local copy, tracks list of the mixer interface */ + guint64 latency_time; + GList *tracks; + + /* private apex client */ + GstApExRAOP *gst_apexraop; +}; + +struct _GstApExSinkClass +{ + GstAudioSinkClass parent_class; +}; + +/* genum jack access */ +GType gst_apexsink_jackstatus_get_type (void); +GType gst_apexsink_jacktype_get_type (void); + +/* audio sink standard api */ +GType gst_apexsink_get_type (void); + +G_END_DECLS + +#endif |