[vlc-devel] [PATCH 2/2] Add Apple AirTunes access output plugin

Michael Hanselmann public at hansmi.ch
Tue Dec 2 01:06:01 CET 2008


Apple's AirPort Express can play audio streamed over the network. The
protocol used was reverse-engineered by Jon Lech Johansen in 2004. He
also released a proof of concept implementation named JustePort.

A variant of HTTP is used to negotiate details before sending music
data over an AES encrypted TCP connection. RSA is used to encrypt the
AES key before transfering it via an HTTP header. To this day, only
the public part of the RSA key has been made public while the private
part remains unknown.

VLC uses libgcrypt for encryption. Unfortunately, libgcrypt doesn't
support the OAEP standard as defined in RFC2437 and AirTunes demands
OAEP padding for RSA encrypted data. OAEP has been reimplemented from
scratch based on the specification for this plugin. Maybe these
functions, MGF1 and AddOaepPadding, could be moved to libvlc at a
later point.

All input data must be encoded using the Apple Lossless codec. Updating
the volume while playing is not yet supported. Except for logging,
no use is made of the audio delay and jack type reported by the device.

AirTunes devices announce themselves on the network using Zeroconf, but
discovering them is not implemented in this plugin. Their service type
is "_raop._tcp".

The "airtunes" plugin can be used like this:
--sout='#transcode{acodec=alac,channels=2}:'\
'std{access=airtunes,mux=raw,dst=hostname}'

Signed-off-by: Michael Hanselmann <public at hansmi.ch>
---
 configure.ac                     |   16 +
 modules/access_output/Modules.am |    1 +
 modules/access_output/airtunes.c | 1385 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 1402 insertions(+), 0 deletions(-)
 create mode 100644 modules/access_output/airtunes.c

diff --git a/configure.ac b/configure.ac
index ba63700..443f6f7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -5300,6 +5300,22 @@ AS_IF([test "${enable_remoteosd}" != "no"], [
 
 
 dnl
+dnl AirTunes plugin
+dnl
+AC_ARG_ENABLE(airtunes,
+  [  --enable-airtunes       AirTunes output plugin (default enabled)])
+if test "${enable_airtunes}" != "no"; then
+  AS_IF([test "${have_libgcrypt}" = "yes"],[
+    VLC_ADD_PLUGIN([access_output_airtunes])
+    VLC_ADD_LIBS([access_output_airtunes], [${LIBGCRYPT_LIBS}])
+    VLC_ADD_CFLAGS([access_output_airtunes], [${LIBGCRYPT_CFLAGS}])
+  ], [
+    AC_MSG_ERROR([libgcrypt support required for AirTunes plugin])
+  ])
+fi
+
+
+dnl
 dnl update checking system
 dnl
 AC_ARG_ENABLE(update-check,
diff --git a/modules/access_output/Modules.am b/modules/access_output/Modules.am
index 36e051b..019b369 100644
--- a/modules/access_output/Modules.am
+++ b/modules/access_output/Modules.am
@@ -4,6 +4,7 @@ SOURCES_access_output_udp = udp.c
 SOURCES_access_output_http = http.c bonjour.c bonjour.h
 SOURCES_access_output_shout = shout.c
 SOURCES_access_output_rtmp = rtmp.c ../access/rtmp/rtmp_amf_flv.c ../access/rtmp/rtmp_amf_flv.h
+SOURCES_access_output_airtunes = airtunes.c
 
 libvlc_LTLIBRARIES += \
 	libaccess_output_dummy_plugin.la \
diff --git a/modules/access_output/airtunes.c b/modules/access_output/airtunes.c
new file mode 100644
index 0000000..c92ec9a
--- /dev/null
+++ b/modules/access_output/airtunes.c
@@ -0,0 +1,1385 @@
+/*****************************************************************************
+ * airtunes.c: Apple AirTunes/Airport Express streaming support
+ *****************************************************************************
+ * Copyright (C) 2008 the VideoLAN team
+ * $Id$
+ *
+ * Author: Michael Hanselmann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+/*****************************************************************************
+ * Preamble
+ *****************************************************************************/
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <assert.h>
+#include <string.h>
+
+#include <gcrypt.h>
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_sout.h>
+#include <vlc_block.h>
+#include <vlc_network.h>
+#include <vlc_strings.h>
+#include <vlc_charset.h>
+#include <vlc_gcrypt.h>
+
+#define AIRTUNES_PORT 5000
+#define AIRTUNES_USER_AGENT "iTunes/8.0.1 (Macintosh; N; Intel)"
+
+
+static const char psz_airtunes_rsa_pubkey[] =
+    "e7d744f2a2e2788b6c1f55a08eb70544a8fa7945aa8be6c62ce5f51cbdd4dc68"
+    "42fe3d1083dd2edec1bfd4252dc02e6f398bdf0e6148ea84855e2e442da6d626"
+    "64f674a1f304929ade4f6893ef2df6e711a8c77a0d91c9d980822e50d12922af"
+    "ea40ea9f0e14c0f76938c5f3882fc0323dd9fe55155f51bb5921c201629fd733"
+    "52d5e2efaabf9ba048d7b813a2b6767f6c3ccf1eb4ce673d037b0d2ea30c5fff"
+    "eb06f8d08adde409571a9c689fef10728855dd8cfb9a8bef5c8943ef3b5faa15"
+    "dde698beddf3599603eb3e6f61372bb628f6559f599a78bf500687aa7f4976c0"
+    "562d412956f8989e18a6355bd81597825e0fc875343ec782117625cdbf98447b";
+
+static const char psz_airtunes_rsa_exp[] = "010001";
+
+static const char psz_delim_space[] = " ";
+static const char psz_delim_colon[] = ":";
+static const char psz_delim_equal[] = "=";
+static const char psz_delim_semicolon[] = ";";
+
+
+/*****************************************************************************
+ * Prototypes
+ *****************************************************************************/
+static int Open( vlc_object_t * );
+static void Close( vlc_object_t * );
+
+static ssize_t Write( sout_access_out_t *, block_t * );
+static int Seek( sout_access_out_t *, off_t  );
+
+static int VolumeCallback( vlc_object_t *p_this, char const *psz_cmd,
+                           vlc_value_t oldval, vlc_value_t newval,
+                           void *p_data );
+
+typedef enum
+{
+    JACK_TYPE_NONE = 0,
+    JACK_TYPE_ANALOG,
+    JACK_TYPE_DIGITAL,
+} jack_type_t;
+
+struct sout_access_out_sys_t
+{
+    /* Input parameters */
+    char *psz_host;
+    int i_volume;
+
+    /* Connection state */
+    int i_control_fd;
+    int i_stream_fd;
+
+    uint8_t ps_aes_key[16];
+    uint8_t ps_aes_iv[16];
+    gcry_cipher_hd_t aes_ctx;
+
+    char *psz_url;
+    char *psz_client_instance;
+    char *psz_session;
+
+    int i_cseq;
+    int i_server_port;
+    int i_audio_latency;
+    int i_jack_type;
+
+    /* Send buffer */
+    size_t i_sendbuf_len;
+    uint8_t *p_sendbuf;
+};
+
+
+/*****************************************************************************
+ * Module descriptor
+ *****************************************************************************/
+#define SOUT_CFG_PREFIX "sout-airtunes-"
+
+#define VOLUME_TEXT N_("Volume")
+#define VOLUME_LONGTEXT N_("Output volume for analog output: 0 for silence, " \
+                           "1..255 from almost silent to very loud.")
+
+vlc_module_begin();
+    set_description( N_("AirTunes audio output") );
+    set_capability( "sout access", 0 );
+    add_shortcut( "airtunes" );
+    set_shortname( "AirTunes" )
+    set_category( CAT_SOUT );
+    set_subcategory( SUBCAT_SOUT_ACO );
+    add_integer_with_range( SOUT_CFG_PREFIX "volume", 100, 0, 255, NULL,
+                            VOLUME_TEXT, VOLUME_LONGTEXT, false );
+    set_callbacks( Open, Close );
+vlc_module_end();
+
+static const char *const ppsz_sout_options[] = {
+    "volume",
+    NULL
+};
+
+
+/*****************************************************************************
+ * Utilities:
+ *****************************************************************************/
+static void FreeSys( sout_access_out_sys_t *p_sys )
+{
+    if ( p_sys->i_control_fd >= 0 )
+        net_Close( p_sys->i_control_fd );
+    if ( p_sys->i_stream_fd >= 0 )
+        net_Close( p_sys->i_stream_fd );
+
+    gcry_cipher_close( p_sys->aes_ctx );
+
+    free( p_sys->p_sendbuf );
+    free( p_sys->psz_host );
+    free( p_sys->psz_url );
+    free( p_sys->psz_session );
+    free( p_sys->psz_client_instance );
+    free( p_sys );
+}
+
+static void RemoveBase64Padding( char *str )
+{
+    char *ps_pos = strchr( str, '=' );
+    if ( ps_pos != NULL )
+        *ps_pos = '\0';
+}
+
+static int _CheckForGcryptError( sout_access_out_t *p_access,
+                                 gcry_error_t i_gcrypt_err,
+                                 unsigned int i_line )
+{
+    if ( i_gcrypt_err != GPG_ERR_NO_ERROR )
+    {
+        msg_Err( p_access, "gcrypt error (line %d): %s", i_line,
+                 gpg_strerror( i_gcrypt_err ) );
+        return 1;
+    }
+
+    return 0;
+}
+
+/* Wrapper to pass line number for easier debugging */
+#define CheckForGcryptError( p_this, i_gcrypt_err ) \
+    _CheckForGcryptError( p_this, i_gcrypt_err, __LINE__ )
+
+/* MGF1 is specified in RFC2437, section 10.2.1. Variables are named after the
+ * specification.
+ */
+static int MGF1( vlc_object_t *p_this,
+                 unsigned char *mask, size_t l,
+                 const unsigned char *Z, const size_t zLen,
+                 const int Hash )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    gcry_error_t i_gcrypt_err;
+    gcry_md_hd_t md_handle = NULL;
+    unsigned int hLen;
+    unsigned char *ps_md;
+    uint32_t counter = 0;
+    uint8_t C[4];
+    size_t i_copylen;
+    int i_err = VLC_SUCCESS;
+
+    assert( mask != NULL );
+    assert( Z != NULL );
+
+    hLen = gcry_md_get_algo_dlen( Hash );
+
+    i_gcrypt_err = gcry_md_open( &md_handle, Hash, 0 );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    while ( l > 0 )
+    {
+        /* 3. For counter from 0 to \lceil{l / hLen}\rceil-1, do the following:
+         * a. Convert counter to an octet string C of length 4 with the
+         *    primitive I2OSP: C = I2OSP (counter, 4)
+         */
+        C[0] = (counter >> 24) & 0xff;
+        C[1] = (counter >> 16) & 0xff;
+        C[2] = (counter >> 8) & 0xff;
+        C[3] = counter & 0xff;
+        ++counter;
+
+        /* b. Concatenate the hash of the seed Z and C to the octet string T:
+         *    T = T || Hash (Z || C)
+         */
+        gcry_md_reset( md_handle );
+        gcry_md_write( md_handle, Z, zLen );
+        gcry_md_write( md_handle, C, 4 );
+        ps_md = gcry_md_read( md_handle, Hash );
+
+        /* 4. Output the leading l octets of T as the octet string mask. */
+        i_copylen = __MIN( l, hLen );
+        memcpy( mask, ps_md, i_copylen );
+        mask += i_copylen;
+        l -= i_copylen;
+    }
+
+error:
+    gcry_md_close( md_handle );
+
+    return i_err;
+}
+
+/* EME-OAEP-ENCODE is specified in RFC2437, section 9.1.1.1. Variables are
+ * named after the specification.
+ */
+static int AddOaepPadding( vlc_object_t *p_this,
+                           unsigned char *EM, const size_t emLenWithPrefix,
+                           const unsigned char *M, const size_t mLen,
+                           const unsigned char *P, const size_t pLen )
+{
+    const int Hash = GCRY_MD_SHA1;
+    const unsigned int hLen = gcry_md_get_algo_dlen( Hash );
+    unsigned char *seed = NULL;
+    unsigned char *DB = NULL;
+    unsigned char *dbMask = NULL;
+    unsigned char *seedMask = NULL;
+    size_t emLen;
+    size_t psLen;
+    size_t i;
+    int i_err = VLC_SUCCESS;
+
+    /* Space for 0x00 prefix in EM. */
+    emLen = emLenWithPrefix - 1;
+
+    /* Step 2:
+     * If ||M|| > emLen-2hLen-1 then output "message too long" and stop.
+     */
+    if ( mLen > (emLen - (2 * hLen) - 1) )
+    {
+        msg_Err( p_this , "Message too long" );
+        goto error;
+    }
+
+    /* Step 3:
+     * Generate an octet string PS consisting of emLen-||M||-2hLen-1 zero
+     * octets. The length of PS may be 0.
+     */
+    psLen = emLen - mLen - (2 * hLen) - 1;
+
+    /*
+     * Step 5:
+     * Concatenate pHash, PS, the message M, and other padding to form a data
+     * block DB as: DB = pHash || PS || 01 || M
+     */
+    DB = calloc( 1, hLen + psLen + 1 + mLen );
+    dbMask = calloc( 1, emLen - hLen );
+    seedMask = calloc( 1, hLen );
+
+    if ( DB == NULL ||
+         dbMask == NULL ||
+         seedMask == NULL )
+    {
+        i_err = VLC_ENOMEM;
+        goto error;
+    }
+
+    /* Step 4:
+     * Let pHash = Hash(P), an octet string of length hLen.
+     */
+    gcry_md_hash_buffer( Hash, DB, P, pLen );
+
+    /* Step 3:
+     * Generate an octet string PS consisting of emLen-||M||-2hLen-1 zero
+     * octets. The length of PS may be 0.
+     */
+    memset( DB + hLen, 0, psLen );
+
+    /* Step 5:
+     * Concatenate pHash, PS, the message M, and other padding to form a data
+     * block DB as: DB = pHash || PS || 01 || M
+     */
+    DB[hLen + psLen] = 0x01;
+    memcpy( DB + hLen + psLen + 1, M, mLen );
+
+    /* Step 6:
+     * Generate a random octet string seed of length hLen
+     */
+    seed = gcry_random_bytes( hLen, GCRY_STRONG_RANDOM );
+    if ( seed == NULL )
+    {
+        i_err = VLC_ENOMEM;
+        goto error;
+    }
+
+    /* Step 7:
+     * Let dbMask = MGF(seed, emLen-hLen).
+     */
+    i_err = MGF1( p_this, dbMask, emLen - hLen, seed, hLen, Hash );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    /* Step 8:
+     * Let maskedDB = DB \xor dbMask.
+     */
+    for ( i = 0; i < (emLen - hLen); ++i )
+        DB[i] ^= dbMask[i];
+
+    /* Step 9:
+     * Let seedMask = MGF(maskedDB, hLen).
+     */
+    i_err = MGF1( p_this, seedMask, hLen, DB, emLen - hLen, Hash );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    /* Step 10:
+     * Let maskedSeed = seed \xor seedMask.
+     */
+    for ( i = 0; i < hLen; ++i )
+        seed[i] ^= seedMask[i];
+
+    /* Step 11:
+     * Let EM = maskedSeed || maskedDB.
+     */
+    assert( (1 + hLen + (hLen + psLen + 1 + mLen)) == emLenWithPrefix );
+    EM[0] = 0x00;
+    memcpy( EM + 1, seed, hLen );
+    memcpy( EM + 1 + hLen, DB, hLen + psLen + 1 + mLen );
+
+    /* Step 12:
+     * Output EM.
+     */
+
+error:
+    free( DB );
+    free( dbMask );
+    free( seedMask );
+    free( seed );
+
+    return i_err;
+}
+
+static int EncryptAesKeyBase64( vlc_object_t *p_this, char **result )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    gcry_error_t i_gcrypt_err;
+    gcry_sexp_t sexp_rsa_params = NULL;
+    gcry_sexp_t sexp_input = NULL;
+    gcry_sexp_t sexp_encrypted = NULL;
+    gcry_sexp_t sexp_token_a = NULL;
+    gcry_mpi_t mpi_pubkey = NULL;
+    gcry_mpi_t mpi_exp = NULL;
+    gcry_mpi_t mpi_input = NULL;
+    gcry_mpi_t mpi_output = NULL;
+    unsigned char ps_padded_key[256];
+    unsigned char *ps_value;
+    size_t i_value_size;
+    int i_err = VLC_SUCCESS;
+
+    /* Add RSA-OAES-SHA1 padding */
+    i_err = AddOaepPadding( p_this,
+                            ps_padded_key, sizeof( ps_padded_key ),
+                            p_sys->ps_aes_key, sizeof( p_sys->ps_aes_key ),
+                            NULL, 0 );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    /* Read public key */
+    i_gcrypt_err = gcry_mpi_scan( &mpi_pubkey, GCRYMPI_FMT_HEX,
+                                  psz_airtunes_rsa_pubkey, 0, NULL );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Read exponent */
+    i_gcrypt_err = gcry_mpi_scan( &mpi_exp, GCRYMPI_FMT_HEX,
+                                  psz_airtunes_rsa_exp, 0,
+                                  NULL );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* If the input data starts with a set bit (0x80), gcrypt thinks it's a
+     * signed integer and complains. Prefixing it with a zero byte (\0)
+     * works, but involves more work. Converting it to an MPI in our code is
+     * cleaner.
+     */
+    i_gcrypt_err = gcry_mpi_scan( &mpi_input, GCRYMPI_FMT_USG,
+                                  ps_padded_key, sizeof( ps_padded_key ),
+                                  NULL);
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Build S-expression with RSA parameters */
+    i_gcrypt_err = gcry_sexp_build( &sexp_rsa_params, NULL,
+                                    "(public-key(rsa(n %m)(e %m)))",
+                                    mpi_pubkey, mpi_exp );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Build S-expression for data */
+    i_gcrypt_err = gcry_sexp_build( &sexp_input, NULL, "(data(value %m))",
+                                    mpi_input );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Encrypt data */
+    i_gcrypt_err = gcry_pk_encrypt( &sexp_encrypted, sexp_input,
+                                    sexp_rsa_params );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Extract encrypted data */
+    sexp_token_a = gcry_sexp_find_token( sexp_encrypted, "a", 0 );
+    if ( !sexp_token_a )
+    {
+        msg_Err( p_this , "Token 'a' not found in result S-expression" );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    mpi_output = gcry_sexp_nth_mpi( sexp_token_a, 1, GCRYMPI_FMT_USG );
+    if ( !mpi_output )
+    {
+        msg_Err( p_this, "Unable to extract MPI from result" );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Copy encrypted data into char array */
+    i_gcrypt_err = gcry_mpi_aprint( GCRYMPI_FMT_USG, &ps_value, &i_value_size,
+                                    mpi_output );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Encode in Base64 */
+    *result = vlc_b64_encode_binary( ps_value, i_value_size );
+
+error:
+    gcry_sexp_release( sexp_rsa_params );
+    gcry_sexp_release( sexp_input );
+    gcry_sexp_release( sexp_encrypted );
+    gcry_sexp_release( sexp_token_a );
+    gcry_mpi_release( mpi_pubkey );
+    gcry_mpi_release( mpi_exp );
+    gcry_mpi_release( mpi_input );
+    gcry_mpi_release( mpi_output );
+
+    return i_err;
+}
+
+static int SplitHeader( char **ppsz_next, char **ppsz_name,
+                        char **ppsz_value )
+{
+    /* Find semicolon (separator between assignments) */
+    *ppsz_name = strsep( ppsz_next, psz_delim_semicolon );
+    if ( *ppsz_name )
+    {
+        /* Skip spaces */
+        *ppsz_name += strspn( *ppsz_name, psz_delim_space );
+
+        /* Get value */
+        *ppsz_value = *ppsz_name;
+        strsep( ppsz_value, psz_delim_equal );
+    }
+    else
+        *ppsz_value = NULL;
+
+    return !!*ppsz_name;
+}
+
+static void FreeHeader( void *p_value, void *p_data )
+{
+    VLC_UNUSED( p_data );
+    free( p_value );
+}
+
+static int ReadStatusLine( vlc_object_t *p_this )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    char *psz_original = NULL;
+    char *psz_line = NULL;
+    char *psz_token;
+    char *psz_next;
+    int i_err = VLC_SUCCESS;
+
+    psz_line = net_Gets( p_this, p_sys->i_control_fd, NULL );
+    if ( !psz_line )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    psz_original = strdup( psz_line );
+    psz_next = psz_line;
+
+    /* Protocol field */
+    psz_token = strsep( &psz_next, psz_delim_space );
+    if ( !psz_token || strncmp( psz_token, "RTSP/1.", 7 ) != 0 )
+    {
+        msg_Err( p_this, "Unknown protocol (%s)", psz_original );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Status field */
+    psz_token = strsep( &psz_next, psz_delim_space );
+    if ( !psz_token || strcmp( psz_token, "200" ) != 0 )
+    {
+        msg_Err( p_this, "Request failed (%s)", psz_original );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+error:
+    free( psz_original );
+    free( psz_line );
+
+    return i_err;
+}
+
+static int ReadHeader( vlc_object_t *p_this,
+                       vlc_dictionary_t *p_resp_headers,
+                       int *done )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    char *psz_original = NULL;
+    char *psz_line = NULL;
+    char *psz_token;
+    char *psz_next;
+    char *psz_name;
+    char *psz_value;
+    int i_err = VLC_SUCCESS;
+
+    psz_line = net_Gets( p_this, p_sys->i_control_fd, NULL );
+    if ( !psz_line )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Empty line for response end */
+    if ( psz_line[0] == '\0' )
+        *done = 1;
+    else if ( p_resp_headers )
+    {
+        psz_original = strdup( psz_line );
+        psz_next = psz_line;
+
+        psz_token = strsep( &psz_next, psz_delim_colon );
+        if ( !psz_token || psz_next[0] != ' ' )
+        {
+            msg_Err( p_this, "Invalid header format (%s)", psz_original );
+            i_err = VLC_EGENERIC;
+            goto error;
+        }
+
+        psz_name = psz_token;
+        psz_value = psz_next + 1;
+
+        vlc_dictionary_insert( p_resp_headers, psz_name, strdup( psz_value ) );
+    }
+
+error:
+    free( psz_original );
+    free( psz_line );
+
+    return i_err;
+}
+
+static int WriteAuxHeaders( vlc_object_t *p_this,
+                            vlc_dictionary_t *p_req_headers )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    char **ppsz_keys = NULL;
+    char *psz_key;
+    char *psz_value;
+    int i_err = VLC_SUCCESS;
+    int i_rc;
+    size_t i;
+
+    ppsz_keys = vlc_dictionary_all_keys( p_req_headers );
+    for ( i = 0; ppsz_keys[i]; ++i )
+    {
+        psz_key = ppsz_keys[i];
+        psz_value = vlc_dictionary_value_for_key( p_req_headers, psz_key );
+
+        i_rc = net_Printf( p_this, p_sys->i_control_fd, NULL,
+                           "%s: %s\r\n", psz_key, psz_value );
+        if ( i_rc < 0 )
+        {
+            i_err = VLC_EGENERIC;
+            goto error;
+        }
+    }
+
+error:
+    for ( i = 0; ppsz_keys[i]; ++i )
+        free( ppsz_keys[i] );
+    free( ppsz_keys );
+
+    return i_err;
+}
+
+static int SendRequest( vlc_object_t *p_this, const char *psz_method,
+                        const char *psz_content_type, const char *psz_body,
+                        vlc_dictionary_t *p_req_headers )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    const unsigned char psz_headers_end[] = "\r\n";
+    size_t i_body_length = 0;
+    int i_err = VLC_SUCCESS;
+    int i_rc;
+
+    i_rc = net_Printf( p_this, p_sys->i_control_fd, NULL,
+                       "%s %s RTSP/1.0\r\n"
+                       "User-Agent: " AIRTUNES_USER_AGENT "\r\n"
+                       "Client-Instance: %s\r\n"
+                       "CSeq: %d\r\n",
+                       psz_method, p_sys->psz_url,
+                       p_sys->psz_client_instance,
+                       ++p_sys->i_cseq );
+    if ( i_rc < 0 )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    if ( psz_content_type )
+    {
+        i_rc = net_Printf( p_this, p_sys->i_control_fd, NULL,
+                           "Content-Type: %s\r\n", psz_content_type );
+        if ( i_rc < 0 )
+        {
+            i_err = VLC_ENOMEM;
+            goto error;
+        }
+    }
+
+    if ( psz_body )
+    {
+        i_body_length = strlen( psz_body );
+
+        i_rc = net_Printf( p_this, p_sys->i_control_fd, NULL,
+                           "Content-Length: %u\r\n",
+                           (unsigned int)i_body_length );
+        if ( i_rc < 0 )
+        {
+            i_err = VLC_ENOMEM;
+            goto error;
+        }
+    }
+
+    if ( p_req_headers )
+    {
+        i_err = WriteAuxHeaders( p_this, p_req_headers );
+        if ( i_err != VLC_SUCCESS )
+            goto error;
+    }
+
+    i_rc = net_Write( p_this, p_sys->i_control_fd, NULL,
+                      psz_headers_end, sizeof( psz_headers_end ) - 1 );
+    if ( i_rc < 0 )
+    {
+        i_err = VLC_ENOMEM;
+        goto error;
+    }
+
+    if ( psz_body )
+        net_Write( p_this, p_sys->i_control_fd, NULL,
+                   psz_body, i_body_length );
+
+error:
+    return i_err;
+}
+
+static int ExecRequest( vlc_object_t *p_this, const char *psz_method,
+                        const char *psz_content_type, const char *psz_body,
+                        vlc_dictionary_t *p_req_headers,
+                        vlc_dictionary_t *p_resp_headers )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    int headers_done;
+    int i_err = VLC_SUCCESS;
+
+    if ( p_sys->i_control_fd < 0 )
+    {
+        msg_Err( p_this, "Control connection not open" );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Send request */
+    i_err = SendRequest( p_this, psz_method, psz_content_type, psz_body,
+                         p_req_headers);
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    /* Read status line */
+    i_err = ReadStatusLine( p_this );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    if ( p_resp_headers )
+        vlc_dictionary_clear( p_resp_headers, FreeHeader, NULL );
+
+    /* Read headers */
+    headers_done = 0;
+    while ( !headers_done )
+    {
+        i_err = ReadHeader( p_this, p_resp_headers, &headers_done );
+        if ( i_err != VLC_SUCCESS )
+            goto error;
+    }
+
+error:
+    return i_err;
+}
+
+static int AnnounceSDP( vlc_object_t *p_this, char *psz_local,
+                        uint32_t i_session_id )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    vlc_dictionary_t req_headers;
+    vlc_dictionary_t resp_headers;
+    unsigned char ps_sac[16];
+    char *psz_sdp = NULL;
+    char *psz_sac_base64 = NULL;
+    char *psz_aes_key_base64 = NULL;
+    char *psz_aes_iv_base64 = NULL;
+    int i_err = VLC_SUCCESS;
+    int i_rc;
+
+    vlc_dictionary_init( &req_headers, 0 );
+    vlc_dictionary_init( &resp_headers, 0 );
+
+    /* Encrypt AES key and encode it in Base64 */
+    i_rc = EncryptAesKeyBase64( p_this, &psz_aes_key_base64 );
+    if ( i_rc != VLC_SUCCESS || psz_aes_key_base64 == NULL )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+    RemoveBase64Padding( psz_aes_key_base64 );
+
+    /* Encode AES IV in Base64 */
+    psz_aes_iv_base64 = vlc_b64_encode_binary( p_sys->ps_aes_iv,
+                                               sizeof( p_sys->ps_aes_iv ) );
+    if ( psz_aes_iv_base64 == NULL )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+    RemoveBase64Padding( psz_aes_iv_base64 );
+
+    /* Random bytes for Apple-Challenge header */
+    gcry_randomize( ps_sac, sizeof( ps_sac ), GCRY_STRONG_RANDOM );
+
+    psz_sac_base64 = vlc_b64_encode_binary( ps_sac, sizeof( ps_sac ) );
+    if ( psz_sac_base64 == NULL )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+    RemoveBase64Padding( psz_sac_base64 );
+
+    /* Build SDP */
+    i_rc = asprintf( &psz_sdp,
+                     "v=0\r\n"
+                     "o=iTunes %u 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",
+                     i_session_id, psz_local, p_sys->psz_host,
+                     psz_aes_key_base64, psz_aes_iv_base64 );
+
+    if ( i_rc < 0 )
+    {
+        i_err = VLC_ENOMEM;
+        goto error;
+    }
+
+    /* Build and send request */
+    vlc_dictionary_insert( &req_headers, "Apple-Challenge", psz_sac_base64 );
+
+    i_err = ExecRequest( p_this, "ANNOUNCE", "application/sdp", psz_sdp,
+                         &req_headers, &resp_headers);
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+error:
+    vlc_dictionary_clear( &req_headers, NULL, NULL );
+    vlc_dictionary_clear( &resp_headers, FreeHeader, NULL );
+
+    free( psz_sdp );
+    free( psz_sac_base64 );
+    free( psz_aes_key_base64 );
+    free( psz_aes_iv_base64 );
+
+    return i_err;
+}
+
+static int SendSetup( vlc_object_t *p_this )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    vlc_dictionary_t req_headers;
+    vlc_dictionary_t resp_headers;
+    int i_err = VLC_SUCCESS;
+    char *psz_tmp;
+    char *psz_next;
+    char *psz_name;
+    char *psz_value;
+
+    vlc_dictionary_init( &req_headers, 0 );
+    vlc_dictionary_init( &resp_headers, 0 );
+
+    vlc_dictionary_insert( &req_headers, "Transport",
+                           ((void*)"RTP/AVP/TCP;unicast;interleaved=0-1;"
+                            "mode=record") );
+
+    i_err = ExecRequest( p_this, "SETUP", NULL, NULL,
+                         &req_headers, &resp_headers );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    psz_tmp = vlc_dictionary_value_for_key( &resp_headers, "Session" );
+    if ( !psz_tmp )
+    {
+        msg_Err( p_this, "Missing 'Session' header during setup" );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    free( p_sys->psz_session );
+    p_sys->psz_session = strdup( psz_tmp );
+
+    /* Get server_port */
+    psz_next = vlc_dictionary_value_for_key( &resp_headers, "Transport" );
+    while ( SplitHeader( &psz_next, &psz_name, &psz_value ) )
+    {
+        if ( psz_value && strcmp( psz_name, "server_port" ) == 0 )
+        {
+            p_sys->i_server_port = atoi( psz_value );
+            break;
+        }
+    }
+
+    if ( !p_sys->i_server_port )
+    {
+        msg_Err( p_this, "Missing 'server_port' during setup" );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Get jack type */
+    psz_next = vlc_dictionary_value_for_key( &resp_headers,
+                                             "Audio-Jack-Status" );
+    while ( SplitHeader( &psz_next, &psz_name, &psz_value ) )
+    {
+        if ( strcmp( psz_name, "type" ) != 0 )
+            continue;
+
+        if ( strcmp( psz_value, "analog" ) == 0 )
+            p_sys->i_jack_type = JACK_TYPE_ANALOG;
+
+        else if ( strcmp( psz_value, "analog" ) == 0 )
+            p_sys->i_jack_type = JACK_TYPE_DIGITAL;
+
+        break;
+    }
+
+error:
+    vlc_dictionary_clear( &req_headers, NULL, NULL );
+    vlc_dictionary_clear( &resp_headers, FreeHeader, NULL );
+
+    return i_err;
+}
+
+static int SendRecord( vlc_object_t *p_this )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    vlc_dictionary_t req_headers;
+    vlc_dictionary_t resp_headers;
+    int i_err = VLC_SUCCESS;
+    char *psz_value;
+
+    vlc_dictionary_init( &req_headers, 0 );
+    vlc_dictionary_init( &resp_headers, 0 );
+
+    vlc_dictionary_insert( &req_headers, "Range", (void *)"npt=0-" );
+    vlc_dictionary_insert( &req_headers, "RTP-Info",
+                           (void *)"seq=0;rtptime=0" );
+    vlc_dictionary_insert( &req_headers, "Session",
+                           (void *)p_sys->psz_session );
+
+    i_err = ExecRequest( p_this, "RECORD", NULL, NULL,
+                         &req_headers, &resp_headers );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    psz_value = vlc_dictionary_value_for_key( &resp_headers, "Audio-Latency" );
+    if ( psz_value )
+        p_sys->i_audio_latency = atoi( psz_value );
+    else
+        p_sys->i_audio_latency = 0;
+
+error:
+    vlc_dictionary_clear( &req_headers, NULL, NULL );
+    vlc_dictionary_clear( &resp_headers, FreeHeader, NULL );
+
+    return i_err;
+}
+
+static int SendFlush( vlc_object_t *p_this )
+{
+    VLC_UNUSED( p_this );
+
+    vlc_dictionary_t req_headers;
+    int i_err = VLC_SUCCESS;
+
+    vlc_dictionary_init( &req_headers, 0 );
+
+    vlc_dictionary_insert( &req_headers, "RTP-Info",
+                           (void *)"seq=0;rtptime=0" );
+
+    i_err = ExecRequest( p_this, "FLUSH", NULL, NULL,
+                         &req_headers, NULL );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+error:
+    vlc_dictionary_clear( &req_headers, NULL, NULL );
+
+    return i_err;
+}
+
+static int SendTeardown( vlc_object_t *p_this )
+{
+    int i_err = VLC_SUCCESS;
+
+    i_err = ExecRequest( p_this, "TEARDOWN", NULL, NULL, NULL, NULL );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+error:
+    return i_err;
+}
+
+static int UpdateVolume( vlc_object_t *p_this )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    vlc_dictionary_t req_headers;
+    char *psz_parameters = NULL;
+    double d_volume;
+    int i_err = VLC_SUCCESS;
+    int i_rc;
+
+    vlc_dictionary_init( &req_headers, 0 );
+
+    /* Our volume is 0..255, AirTunes is -144..0 (-144 off, -30..0 on) */
+
+    /* Limit range */
+    p_sys->i_volume = __MAX( 0, __MIN( p_sys->i_volume, 255 ) );
+
+    if ( p_sys->i_volume == 0 )
+        d_volume = -144.0;
+    else
+        d_volume = -30 + ( ( (double)p_sys->i_volume ) * 30.0 / 255.0 );
+
+    /* Format without using locales */
+    i_rc = us_asprintf( &psz_parameters, "volume: %0.6f\r\n", d_volume );
+    if ( i_rc < 0 )
+    {
+        i_err = VLC_ENOMEM;
+        goto error;
+    }
+
+    vlc_dictionary_insert( &req_headers, "Session",
+                           (void *)p_sys->psz_session );
+
+    i_err = ExecRequest( p_this, "SET_PARAMETER",
+                         "text/parameters", psz_parameters,
+                         &req_headers, NULL );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+error:
+    vlc_dictionary_clear( &req_headers, NULL, NULL );
+    free( psz_parameters );
+
+    return i_err;
+}
+
+static void LogInfo( vlc_object_t *p_this )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    const char *psz_jack_name;
+
+    msg_Info( p_this, "Audio latency: %d", p_sys->i_audio_latency );
+
+    switch ( p_sys->i_jack_type )
+    {
+        case JACK_TYPE_ANALOG:
+            psz_jack_name = "analog";
+            break;
+
+        case JACK_TYPE_DIGITAL:
+            psz_jack_name = "digital";
+            break;
+
+        case JACK_TYPE_NONE:
+        default:
+            psz_jack_name = "none";
+            break;
+    }
+
+    msg_Info( p_this, "Jack type: %s", psz_jack_name );
+}
+
+
+/*****************************************************************************
+ * Open:
+ *****************************************************************************/
+static int Open( vlc_object_t *p_this )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys;
+    char psz_local[NI_MAXNUMERICHOST];
+    gcry_error_t i_gcrypt_err;
+    int i_err = VLC_SUCCESS;
+    uint32_t i_session_id;
+    uint64_t i_client_instance;
+
+    vlc_gcrypt_init();
+
+    config_ChainParse( p_access, SOUT_CFG_PREFIX, ppsz_sout_options,
+                       p_access->p_cfg );
+
+    p_sys = malloc( sizeof( *p_sys ) );
+    if ( !p_sys )
+    {
+        i_err = VLC_ENOMEM;
+        goto error;
+    }
+    memset( p_sys, 0, sizeof( *p_sys ) );
+
+    p_access->p_sys = p_sys;
+    p_access->pf_write = Write;
+    p_access->pf_seek = Seek;
+
+    p_sys->i_control_fd = -1;
+    p_sys->i_stream_fd = -1;
+    p_sys->i_volume = config_GetInt( p_access, SOUT_CFG_PREFIX "volume");
+    p_sys->i_jack_type = JACK_TYPE_NONE;
+
+    if ( p_access->psz_path == NULL || strlen( p_access->psz_path ) == 0 )
+    {
+        msg_Err( p_this, "Missing host" );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    p_sys->psz_host = strdup( p_access->psz_path );
+
+    var_AddCallback( p_access, SOUT_CFG_PREFIX "volume",
+                     VolumeCallback, NULL );
+
+    /* Open control connection */
+    p_sys->i_control_fd = net_ConnectTCP( p_access, p_sys->psz_host,
+                                          AIRTUNES_PORT );
+    if ( p_sys->i_control_fd < 0 )
+    {
+        msg_Err( p_this, "Cannot establish control connection to %s:%d (%m)",
+                 p_sys->psz_host, AIRTUNES_PORT );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Get local IP address */
+    if ( net_GetSockAddress( p_sys->i_control_fd, psz_local, NULL ) )
+    {
+        msg_Err( p_this, "cannot get local IP address" );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Random session ID */
+    gcry_randomize( &i_session_id, sizeof( i_session_id ),
+                    GCRY_STRONG_RANDOM );
+
+    /* Random client instance */
+    gcry_randomize( &i_client_instance, sizeof( i_client_instance ),
+                    GCRY_STRONG_RANDOM );
+    if ( asprintf( &p_sys->psz_client_instance, "%016llX",
+                   i_client_instance ) < 0 )
+    {
+        i_err = VLC_ENOMEM;
+        goto error;
+    }
+
+    /* Build session URL */
+    if ( asprintf( &p_sys->psz_url, "rtsp://%s/%u",
+                   psz_local, i_session_id ) < 0 )
+    {
+        i_err = VLC_ENOMEM;
+        goto error;
+    }
+
+    /* Generate AES key and IV */
+    gcry_randomize( p_sys->ps_aes_key, sizeof( p_sys->ps_aes_key ),
+                    GCRY_STRONG_RANDOM );
+    gcry_randomize( p_sys->ps_aes_iv, sizeof( p_sys->ps_aes_iv ),
+                    GCRY_STRONG_RANDOM );
+
+    /* Setup AES */
+    i_gcrypt_err = gcry_cipher_open( &p_sys->aes_ctx, GCRY_CIPHER_AES,
+                                     GCRY_CIPHER_MODE_CBC, 0 );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Set key */
+    i_gcrypt_err = gcry_cipher_setkey( p_sys->aes_ctx, p_sys->ps_aes_key,
+                                       sizeof( p_sys->ps_aes_key ) );
+    if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+    {
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+    /* Protocol handshake */
+    i_err = AnnounceSDP( p_this, psz_local, i_session_id );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    i_err = SendSetup( p_this );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    i_err = SendRecord( p_this );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    i_err = UpdateVolume( p_this );
+    if ( i_err != VLC_SUCCESS )
+        goto error;
+
+    LogInfo( p_this );
+
+    /* Open stream connection */
+    p_sys->i_stream_fd = net_ConnectTCP( p_access, p_sys->psz_host,
+                                         p_sys->i_server_port );
+    if ( p_sys->i_stream_fd < 0 )
+    {
+        msg_Err( p_this, "Cannot establish stream connection to %s:%d (%m)",
+                 p_sys->psz_host, p_sys->i_server_port );
+        i_err = VLC_EGENERIC;
+        goto error;
+    }
+
+error:
+    if ( i_err != VLC_SUCCESS )
+        FreeSys( p_sys );
+
+    return i_err;
+}
+
+
+/*****************************************************************************
+ * Close:
+ *****************************************************************************/
+static void Close( vlc_object_t * p_this )
+{
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+
+    SendFlush( p_this );
+    SendTeardown( p_this );
+
+    FreeSys( p_sys );
+}
+
+
+/*****************************************************************************
+ * Write:
+ *****************************************************************************/
+static ssize_t Write( sout_access_out_t *p_access, block_t *p_buffer )
+{
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+    gcry_error_t i_gcrypt_err;
+    block_t *p_next;
+    size_t i_len;
+    size_t i_payload_len;
+    size_t i_realloc_len;
+    size_t i_write;
+    int rc;
+
+    const uint8_t header[16] = {
+        0x24, 0x00, 0x00, 0x00,
+        0xf0, 0xff, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00,
+    };
+
+    i_write = 0;
+
+    while ( p_buffer )
+    {
+        i_len = sizeof( header ) + p_buffer->i_buffer;
+
+        /* Buffer resize needed? */
+        if ( i_len > p_sys->i_sendbuf_len || p_sys->p_sendbuf == NULL )
+        {
+            /* Grow in blocks of 4K */
+            i_realloc_len = (1 + (i_len / 4096)) * 4096;
+
+            p_sys->p_sendbuf = realloc( p_sys->p_sendbuf, i_realloc_len );
+            if ( p_sys->p_sendbuf == NULL )
+                goto error;
+
+            p_sys->i_sendbuf_len = i_realloc_len;
+        }
+
+        /* Fill buffer */
+        memcpy( p_sys->p_sendbuf, header, sizeof( header ) );
+        memcpy( p_sys->p_sendbuf + sizeof( header ),
+                p_buffer->p_buffer, p_buffer->i_buffer );
+
+        /* Calculate payload length and update header */
+        i_payload_len = i_len - 4;
+        if ( i_payload_len > 0xffff )
+        {
+            msg_Err( p_access, "Buffer is too long (%u bytes)",
+                     (unsigned int)i_payload_len );
+            goto error;
+        }
+
+        p_sys->p_sendbuf[2] = ( i_payload_len >> 8 ) & 0xff;
+        p_sys->p_sendbuf[3] = i_payload_len & 0xff;
+
+        /* Reset cipher */
+        i_gcrypt_err = gcry_cipher_reset( p_sys->aes_ctx );
+        if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+            goto error;
+
+        /* Set IV */
+        i_gcrypt_err = gcry_cipher_setiv( p_sys->aes_ctx, p_sys->ps_aes_iv,
+                                          sizeof( p_sys->ps_aes_iv ) );
+        if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+            goto error;
+
+        /* Encrypt in place. Only full blocks of 16 bytes are encrypted,
+         * the rest (0-15 bytes) is left unencrypted.
+         */
+        i_gcrypt_err =
+            gcry_cipher_encrypt( p_sys->aes_ctx,
+                                 p_sys->p_sendbuf + sizeof( header ),
+                                 ( p_buffer->i_buffer / 16 ) * 16,
+                                 NULL, 0 );
+        if ( CheckForGcryptError( p_access, i_gcrypt_err ) )
+            goto error;
+
+        /* Send data */
+        rc = net_Write( p_access, p_sys->i_stream_fd, NULL,
+                        p_sys->p_sendbuf, i_len );
+        if ( rc < 0 )
+            goto error;
+
+        i_write += p_buffer->i_buffer;
+
+        p_next = p_buffer->p_next;
+        block_Release( p_buffer );
+        p_buffer = p_next;
+    }
+
+    return i_write;
+
+error:
+    block_ChainRelease( p_buffer );
+    return -1;
+}
+
+/*****************************************************************************
+ * Seek: seek to a specific location in a file
+ *****************************************************************************/
+static int Seek( sout_access_out_t *p_access, off_t i_pos )
+{
+    VLC_UNUSED( i_pos );
+    msg_Warn( p_access, "AirTunes sout access cannot seek" );
+    return VLC_EGENERIC;
+}
+
+/*****************************************************************************
+ * VolumeCallback: called when the volume is changed on the fly.
+ *****************************************************************************/
+static int VolumeCallback( vlc_object_t *p_this, char const *psz_cmd,
+                           vlc_value_t oldval, vlc_value_t newval,
+                           void *p_data )
+{
+    VLC_UNUSED(psz_cmd);
+    VLC_UNUSED(oldval);
+    VLC_UNUSED(p_data);
+    VLC_UNUSED(newval);
+    sout_access_out_t *p_access = (sout_access_out_t*)p_this;
+    sout_access_out_sys_t *p_sys = p_access->p_sys;
+
+    /* TODO: Implement volume change */
+    VLC_UNUSED(p_sys);
+
+    return VLC_SUCCESS;
+}
-- 
1.5.4.3




More information about the vlc-devel mailing list