/***
  This file is part of PulseAudio.

  Copyright 2008 Colin Guthrie
  Copyright 2013 Hajime Fujita
  Copyright 2013 Martin Blanchard

  PulseAudio is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published
  by the Free Software Foundation; either version 2.1 of the License,
  or (at your option) any later version.

  PulseAudio is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
***/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <math.h>

#ifdef HAVE_SYS_FILIO_H
#include <sys/filio.h>
#endif

#include <pulse/xmalloc.h>
#include <pulse/timeval.h>
#include <pulse/sample.h>

#include <pulsecore/core.h>
#include <pulsecore/core-error.h>
#include <pulsecore/core-rtclock.h>
#include <pulsecore/core-util.h>
#include <pulsecore/iochannel.h>
#include <pulsecore/arpa-inet.h>
#include <pulsecore/socket-client.h>
#include <pulsecore/socket-util.h>
#include <pulsecore/log.h>
#include <pulsecore/parseaddr.h>
#include <pulsecore/macro.h>
#include <pulsecore/memchunk.h>
#include <pulsecore/random.h>
#include <pulsecore/poll.h>

#include <modules/rtp/rtsp_client.h>

#include "raop-client.h"
#include "raop-packet-buffer.h"
#include "raop-crypto.h"
#include "raop-util.h"

#define DEFAULT_RAOP_PORT 5000

#define FRAMES_PER_TCP_PACKET 4096
#define FRAMES_PER_UDP_PACKET 352

#define RTX_BUFFERING_SECONDS 4

#define DEFAULT_TCP_AUDIO_PORT   6000
#define DEFAULT_UDP_AUDIO_PORT   6000
#define DEFAULT_UDP_CONTROL_PORT 6001
#define DEFAULT_UDP_TIMING_PORT  6002

#define DEFAULT_USER_AGENT "iTunes/11.0.4 (Windows; N)"
#define DEFAULT_USER_NAME  "iTunes"

#define JACK_STATUS_DISCONNECTED 0
#define JACK_STATUS_CONNECTED    1
#define JACK_TYPE_ANALOG         0
#define JACK_TYPE_DIGITAL        1

#define VOLUME_MAX  0.0
#define VOLUME_DEF -30.0
#define VOLUME_MIN -144.0

#define UDP_DEFAULT_PKT_BUF_SIZE 1000
#define APPLE_CHALLENGE_LENGTH 16

struct pa_raop_client {
    pa_core *core;
    char *host;
    uint16_t port;
    pa_rtsp_client *rtsp;
    char *sci, *sid;
    char *password;

    pa_raop_protocol_t protocol;
    pa_raop_encryption_t encryption;
    pa_raop_codec_t codec;

    pa_raop_secret *secret;

    int tcp_sfd;

    int udp_sfd;
    int udp_cfd;
    int udp_tfd;

    pa_raop_packet_buffer *pbuf;

    uint16_t seq;
    uint32_t rtptime;
    bool is_recording;
    uint32_t ssrc;

    bool is_first_packet;
    uint32_t sync_interval;
    uint32_t sync_count;

    uint8_t jack_type;
    uint8_t jack_status;

    pa_raop_client_state_cb_t state_callback;
    void *state_userdata;
};

/* Audio TCP packet header [16x8] (cf. rfc4571):
 *  [0,1]   Frame marker; seems always 0x2400
 *  [2,3]   RTP packet size (following): 0x0000 (to be set)
 *   [4,5]   RTP v2: 0x80
 *   [5]     Payload type: 0x60 | Marker bit: 0x80 (always set)
 *   [6,7]   Sequence number: 0x0000 (to be set)
 *   [8,11]  Timestamp: 0x00000000 (to be set)
 *   [12,15] SSRC: 0x00000000 (to be set) */
#define PAYLOAD_TCP_AUDIO_DATA 0x60
static const uint8_t tcp_audio_header[16] = {
    0x24, 0x00, 0x00, 0x00,
    0x80, 0xe0, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00
};

/* Audio UDP packet header [12x8] (cf. rfc3550):
 *  [0]    RTP v2: 0x80
 *  [1]    Payload type: 0x60
 *  [2,3]  Sequence number: 0x0000 (to be set)
 *  [4,7]  Timestamp: 0x00000000 (to be set)
 *  [8,12] SSRC: 0x00000000 (to be set) */
#define PAYLOAD_UDP_AUDIO_DATA 0x60
static const uint8_t udp_audio_header[12] = {
    0x80, 0x60, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00
};

/* Audio retransmission UDP packet header [4x8]:
 *  [0] RTP v2: 0x80
 *  [1] Payload type: 0x56 | Marker bit: 0x80 (always set)
 *  [2] Unknown; seems always 0x01
 *  [3] Unknown; seems some random number around 0x20~0x40 */
#define PAYLOAD_RETRANSMIT_REQUEST 0x55
#define PAYLOAD_RETRANSMIT_REPLY   0x56
static const uint8_t udp_audio_retrans_header[4] = {
    0x80, 0xd6, 0x00, 0x00
};

/* Sync packet header [8x8] (cf. rfc3550):
 *  [0]   RTP v2: 0x80
 *  [1]   Payload type: 0x54 | Marker bit: 0x80 (always set)
 *  [2,3] Sequence number: 0x0007
 *  [4,7] Timestamp: 0x00000000 (to be set) */
static const uint8_t udp_sync_header[8] = {
    0x80, 0xd4, 0x00, 0x07,
    0x00, 0x00, 0x00, 0x00
};

/* Timing packet header [8x8] (cf. rfc3550):
 *  [0]   RTP v2: 0x80
 *  [1]   Payload type: 0x53 | Marker bit: 0x80 (always set)
 *  [2,3] Sequence number: 0x0007
 *  [4,7] Timestamp: 0x00000000 (unused) */
#define PAYLOAD_TIMING_REQUEST  0x52
#define PAYLOAD_TIMING_REPLY    0x53
static const uint8_t udp_timing_header[8] = {
    0x80, 0xd3, 0x00, 0x07,
    0x00, 0x00, 0x00, 0x00
};

/**
 * Function to trim a given character at the end of a string (no realloc).
 * @param str Pointer to string
 * @param rc Character to trim
 */
static inline void rtrim_char(char *str, char rc) {
    char *sp = str + strlen(str) - 1;
    while (sp >= str && *sp == rc) {
        *sp = '\0';
        sp -= 1;
    }
}

/**
 * Function to convert a timeval to ntp timestamp.
 * @param tv Pointer to the timeval structure
 * @return The NTP timestamp
 */
static inline uint64_t timeval_to_ntp(struct timeval *tv) {
    uint64_t ntp = 0;

    /* Converting micro seconds to a fraction. */
    ntp = (uint64_t) tv->tv_usec * UINT32_MAX / PA_USEC_PER_SEC;
    /* Moving reference from  1 Jan 1970 to 1 Jan 1900 (seconds). */
    ntp |= (uint64_t) (tv->tv_sec + 0x83aa7e80) << 32;

    return ntp;
}

/**
 * Function to write bits into a buffer.
 * @param buffer Handle to the buffer. It will be incremented if new data requires it.
 * @param bit_pos A pointer to a position buffer to keep track the current write location (0 for MSB, 7 for LSB)
 * @param size A pointer to the byte size currently written. This allows the calling function to do simple buffer overflow checks
 * @param data The data to write
 * @param data_bit_len The number of bits from data to write
 */
static inline void bit_writer(uint8_t **buffer, uint8_t *bit_pos, size_t *size, uint8_t data, uint8_t data_bit_len) {
    int bits_left, bit_overflow;
    uint8_t bit_data;

    if (!data_bit_len)
        return;

    /* If bit pos is zero, we will definitely use at least one bit from the current byte so size increments. */
    if (!*bit_pos)
        *size += 1;

    /* Calc the number of bits left in the current byte of buffer. */
    bits_left = 7 - *bit_pos  + 1;
    /* Calc the overflow of bits in relation to how much space we have left... */
    bit_overflow = bits_left - data_bit_len;
    if (bit_overflow >= 0) {
        /* We can fit the new data in our current byte.
         * As we write from MSB->LSB we need to left shift by the overflow amount. */
        bit_data = data << bit_overflow;
        if (*bit_pos)
            **buffer |= bit_data;
        else
            **buffer = bit_data;
        /* If our data fits exactly into the current byte, we need to increment our pointer. */
        if (0 == bit_overflow) {
            /* Do not increment size as it will be incremented on next call as bit_pos is zero. */
            *buffer += 1;
            *bit_pos = 0;
        } else {
            *bit_pos += data_bit_len;
        }
    } else {
        /* bit_overflow is negative, there for we will need a new byte from our buffer
         * Firstly fill up what's left in the current byte. */
        bit_data = data >> -bit_overflow;
        **buffer |= bit_data;
        /* Increment our buffer pointer and size counter. */
        *buffer += 1;
        *size += 1;
        **buffer = data << (8 + bit_overflow);
        *bit_pos = -bit_overflow;
    }
}

static size_t write_ALAC_data(uint8_t *packet, const size_t max, uint8_t *raw, size_t *length, bool compress) {
    uint32_t nbs = (*length / 2) / 2;
    uint8_t *ibp, *maxibp;
    uint8_t *bp, bpos;
    size_t size = 0;

    bp = packet;
    pa_memzero(packet, max);
    size = bpos = 0;

    bit_writer(&bp, &bpos, &size, 1, 3); /* channel=1, stereo */
    bit_writer(&bp, &bpos, &size, 0, 4); /* Unknown */
    bit_writer(&bp, &bpos, &size, 0, 8); /* Unknown */
    bit_writer(&bp, &bpos, &size, 0, 4); /* Unknown */
    bit_writer(&bp, &bpos, &size, 1, 1); /* Hassize */
    bit_writer(&bp, &bpos, &size, 0, 2); /* Unused */
    bit_writer(&bp, &bpos, &size, 1, 1); /* Is-not-compressed */
    /* Size of data, integer, big endian. */
    bit_writer(&bp, &bpos, &size, (nbs >> 24) & 0xff, 8);
    bit_writer(&bp, &bpos, &size, (nbs >> 16) & 0xff, 8);
    bit_writer(&bp, &bpos, &size, (nbs >> 8)  & 0xff, 8);
    bit_writer(&bp, &bpos, &size, (nbs)       & 0xff, 8);

    ibp = raw;
    maxibp = raw + (4 * nbs) - 4;
    while (ibp <= maxibp) {
        /* Byte swap stereo data. */
        bit_writer(&bp, &bpos, &size, *(ibp + 1), 8);
        bit_writer(&bp, &bpos, &size, *(ibp + 0), 8);
        bit_writer(&bp, &bpos, &size, *(ibp + 3), 8);
        bit_writer(&bp, &bpos, &size, *(ibp + 2), 8);
        ibp += 4;
    }

    *length = (ibp - raw);
    return size;
}

static size_t build_tcp_audio_packet(pa_raop_client *c, pa_memchunk *block, pa_memchunk *packet) {
    const size_t head = sizeof(tcp_audio_header);
    uint32_t *buffer = NULL;
    uint8_t *raw = NULL;
    size_t length, size;

    raw = pa_memblock_acquire(block->memblock);
    buffer = pa_memblock_acquire(packet->memblock);
    buffer += packet->index / sizeof(uint32_t);
    raw += block->index;

    /* Wrap sequence number to 0 then UINT16_MAX is reached */
    if (c->seq == UINT16_MAX)
        c->seq = 0;
    else
        c->seq++;

    memcpy(buffer, tcp_audio_header, sizeof(tcp_audio_header));
    buffer[1] |= htonl((uint32_t) c->seq);
    buffer[2] = htonl(c->rtptime);
    buffer[3] = htonl(c->ssrc);

    length = block->length;
    size = sizeof(tcp_audio_header);
    if (c->codec == PA_RAOP_CODEC_ALAC)
        size += write_ALAC_data(((uint8_t *) buffer + head), packet->length - head, raw, &length, false);
    else {
        pa_log_debug("Only ALAC encoding is supported, sending zeros...");
        pa_memzero(((uint8_t *) buffer + head), packet->length - head);
        size += length;
    }

    c->rtptime += length / 4;

    pa_memblock_release(block->memblock);

    buffer[0] |= htonl((uint32_t) size - 4);
    if (c->encryption == PA_RAOP_ENCRYPTION_RSA)
        pa_raop_aes_encrypt(c->secret, (uint8_t *) buffer + head, size - head);

    pa_memblock_release(packet->memblock);
    packet->length = size;

    return size;
}

static ssize_t send_tcp_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) {
    static int write_type = 0;
    const size_t max = sizeof(tcp_audio_header) + 8 + 16384;
    pa_memchunk *packet = NULL;
    uint8_t *buffer = NULL;
    double progress = 0.0;
    ssize_t written = -1;
    size_t done = 0;

    if (!(packet = pa_raop_packet_buffer_retrieve(c->pbuf, c->seq)))
        return -1;

    if (packet->length <= 0) {
        pa_assert(block->index == offset);

        if (!(packet = pa_raop_packet_buffer_prepare(c->pbuf, c->seq + 1, max)))
            return -1;

        packet->index = 0;
        packet->length = max;
        if (!build_tcp_audio_packet(c, block, packet))
            return -1;
    }

    buffer = pa_memblock_acquire(packet->memblock);

    pa_assert(buffer);

    buffer += packet->index;
    if (buffer && packet->length > 0)
        written = pa_write(c->tcp_sfd, buffer, packet->length, &write_type);
    if (written > 0) {
        progress = (double) written / (double) packet->length;
        packet->length -= written;
        packet->index += written;

        done = block->length * progress;
        block->length -= done;
        block->index += done;
    }

    pa_memblock_release(packet->memblock);

    return written;
}

static size_t build_udp_audio_packet(pa_raop_client *c, pa_memchunk *block, pa_memchunk *packet) {
    const size_t head = sizeof(udp_audio_header);
    uint32_t *buffer = NULL;
    uint8_t *raw = NULL;
    size_t length, size;

    raw = pa_memblock_acquire(block->memblock);
    buffer = pa_memblock_acquire(packet->memblock);
    buffer += packet->index / sizeof(uint32_t);
    raw += block->index;

    memcpy(buffer, udp_audio_header, sizeof(udp_audio_header));
    if (c->is_first_packet)
        buffer[0] |= htonl((uint32_t) 0x80 << 16);
    buffer[0] |= htonl((uint32_t) c->seq);
    buffer[1] = htonl(c->rtptime);
    buffer[2] = htonl(c->ssrc);

    length = block->length;
    size = sizeof(udp_audio_header);
    if (c->codec == PA_RAOP_CODEC_ALAC)
        size += write_ALAC_data(((uint8_t *) buffer + head), packet->length - head, raw, &length, false);
    else {
        pa_log_debug("Only ALAC encoding is supported, sending zeros...");
        pa_memzero(((uint8_t *) buffer + head), packet->length - head);
        size += length;
    }

    c->rtptime += length / 4;

    /* Wrap sequence number to 0 then UINT16_MAX is reached */
    if (c->seq == UINT16_MAX)
        c->seq = 0;
    else
        c->seq++;

    pa_memblock_release(block->memblock);

    if (c->encryption == PA_RAOP_ENCRYPTION_RSA)
        pa_raop_aes_encrypt(c->secret, (uint8_t *) buffer + head, size - head);

    pa_memblock_release(packet->memblock);
    packet->length = size;

    return size;
}

static ssize_t send_udp_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) {
    const size_t max = sizeof(udp_audio_retrans_header) + sizeof(udp_audio_header) + 8 + 1408;
    pa_memchunk *packet = NULL;
    uint8_t *buffer = NULL;
    ssize_t written = -1;

    /* UDP packet has to be sent at once ! */
    pa_assert(block->index == offset);

    if (!(packet = pa_raop_packet_buffer_prepare(c->pbuf, c->seq, max)))
        return -1;

    packet->index = sizeof(udp_audio_retrans_header);
    packet->length = max - sizeof(udp_audio_retrans_header);
    if (!build_udp_audio_packet(c, block, packet))
        return -1;

    buffer = pa_memblock_acquire(packet->memblock);

    pa_assert(buffer);

    buffer += packet->index;
    if (buffer && packet->length > 0)
        written = pa_write(c->udp_sfd, buffer, packet->length, NULL);
    if (written < 0 && errno == EAGAIN) {
        pa_log_debug("Discarding UDP (audio, seq=%d) packet due to EAGAIN (%s)", c->seq, pa_cstrerror(errno));
        written = packet->length;
    }

    pa_memblock_release(packet->memblock);
    /* It is meaningless to preseve the partial data */
    block->index += block->length;
    block->length = 0;

    return written;
}

static size_t rebuild_udp_audio_packet(pa_raop_client *c, uint16_t seq, pa_memchunk *packet) {
    size_t size = sizeof(udp_audio_retrans_header);
    uint32_t *buffer = NULL;

    buffer = pa_memblock_acquire(packet->memblock);

    memcpy(buffer, udp_audio_retrans_header, sizeof(udp_audio_retrans_header));
    buffer[0] |= htonl((uint32_t) seq);
    size += packet->length;

    pa_memblock_release(packet->memblock);
    packet->length += sizeof(udp_audio_retrans_header);
    packet->index -= sizeof(udp_audio_retrans_header);

    return size;
}

static ssize_t resend_udp_audio_packets(pa_raop_client *c, uint16_t seq, uint16_t nbp) {
    ssize_t total = 0;
    int i = 0;

    for (i = 0; i < nbp; i++) {
        pa_memchunk *packet = NULL;
        uint8_t *buffer = NULL;
        ssize_t written = -1;

        if (!(packet = pa_raop_packet_buffer_retrieve(c->pbuf, seq + i)))
            continue;

        if (packet->index > 0) {
            if (!rebuild_udp_audio_packet(c, seq + i, packet))
                continue;
        }

        pa_assert(packet->index == 0);

        buffer = pa_memblock_acquire(packet->memblock);

        pa_assert(buffer);

        if (buffer && packet->length > 0)
            written = pa_write(c->udp_cfd, buffer, packet->length, NULL);
        if (written < 0 && errno == EAGAIN) {
            pa_log_debug("Discarding UDP (audio-retransmitted, seq=%d) packet due to EAGAIN", seq + i);
            pa_memblock_release(packet->memblock);
            continue;
        }

        pa_memblock_release(packet->memblock);
        total +=  written;
    }

    return total;
}

/* Caller has to free the allocated memory region for packet */
static size_t build_udp_sync_packet(pa_raop_client *c, uint32_t stamp, uint32_t **packet) {
    const size_t size = sizeof(udp_sync_header) + 12;
    const uint32_t delay = 88200;
    uint32_t *buffer = NULL;
    uint64_t transmitted = 0;
    struct timeval tv;

    *packet = NULL;
    if (!(buffer = pa_xmalloc0(size)))
        return 0;

    memcpy(buffer, udp_sync_header, sizeof(udp_sync_header));
    if (c->is_first_packet)
        buffer[0] |= 0x10;
    stamp -= delay;
    buffer[1] = htonl(stamp);
    /* Set the transmitted timestamp to current time. */
    transmitted = timeval_to_ntp(pa_rtclock_get(&tv));
    buffer[2] = htonl(transmitted >> 32);
    buffer[3] = htonl(transmitted & 0xffffffff);
    stamp += delay;
    buffer[4] = htonl(stamp);

    *packet = buffer;
    return size;
}

static ssize_t send_udp_sync_packet(pa_raop_client *c, uint32_t stamp) {
    uint32_t * packet = NULL;
    ssize_t written = 0;
    size_t size = 0;

    size = build_udp_sync_packet(c, stamp, &packet);
    if (packet != NULL && size > 0) {
        written = pa_loop_write(c->udp_cfd, packet, size, NULL);
        pa_xfree(packet);
    }

    return written;
}

static size_t handle_udp_control_packet(pa_raop_client *c, const uint8_t packet[], ssize_t size) {
    uint8_t payload = 0;
    uint16_t seq, nbp = 0;
    ssize_t written = 0;

    /* Control packets are 8 bytes long:  */
    if (size != 8 || packet[0] != 0x80)
        return 1;

    seq = ntohs((uint16_t) (packet[4] | packet[5] << 8));
    nbp = ntohs((uint16_t) (packet[6] | packet[7] << 8));
    if (nbp <= 0)
        return 1;

    /* The marker bit is always set (see rfc3550 for packet structure) ! */
    payload = packet[1] ^ 0x80;
    switch (payload) {
        case PAYLOAD_RETRANSMIT_REQUEST:
            pa_log_debug("Resending %u packets starting at %u", nbp, seq);
            written = resend_udp_audio_packets(c, seq, nbp);
            break;
        case PAYLOAD_RETRANSMIT_REPLY:
        default:
            pa_log_debug("Got an unexpected payload type on control channel (%u) !", payload);
            break;
    }

    return written;
}

/* Caller has to free the allocated memory region for packet */
static size_t build_udp_timing_packet(pa_raop_client *c, const uint32_t data[6], uint64_t received, uint32_t **packet) {
    const size_t size = sizeof(udp_timing_header) + 24;
    uint32_t *buffer = NULL;
    uint64_t transmitted = 0;
    struct timeval tv;

    *packet = NULL;
    if (!(buffer = pa_xmalloc0(size)))
        return 0;

    memcpy(buffer, udp_timing_header, sizeof(udp_timing_header));
    /* Copying originate timestamp from the incoming request packet. */
    buffer[2] = data[4];
    buffer[3] = data[5];
    /* Set the receive timestamp to reception time. */
    buffer[4] = htonl(received >> 32);
    buffer[5] = htonl(received & 0xffffffff);
    /* Set the transmit timestamp to current time. */
    transmitted = timeval_to_ntp(pa_rtclock_get(&tv));
    buffer[6] = htonl(transmitted >> 32);
    buffer[7] = htonl(transmitted & 0xffffffff);

    *packet = buffer;
    return size;
}

static ssize_t send_udp_timing_packet(pa_raop_client *c, const uint32_t data[6], uint64_t received) {
    uint32_t * packet = NULL;
    ssize_t written = 0;
    size_t size = 0;

    size = build_udp_timing_packet(c, data, received, &packet);
    if (packet != NULL && size > 0) {
        written = pa_loop_write(c->udp_tfd, packet, size, NULL);
        pa_xfree(packet);
    }

    return written;
}

static size_t handle_udp_timing_packet(pa_raop_client *c, const uint8_t packet[], ssize_t size) {
    const uint32_t * data = NULL;
    uint8_t payload = 0;
    struct timeval tv;
    size_t written = 0;
    uint64_t rci = 0;

    /* Timing packets are 32 bytes long: 1 x 8 RTP header (no ssrc) + 3 x 8 NTP timestamps */
    if (size != 32 || packet[0] != 0x80)
        return 0;

    rci = timeval_to_ntp(pa_rtclock_get(&tv));
    data = (uint32_t *) (packet + sizeof(udp_timing_header));

    /* The marker bit is always set (see rfc3550 for packet structure) ! */
    payload = packet[1] ^ 0x80;
    switch (payload) {
        case PAYLOAD_TIMING_REQUEST:
            pa_log_debug("Sending timing packet at %lu", rci);
            written = send_udp_timing_packet(c, data, rci);
            break;
        case PAYLOAD_TIMING_REPLY:
        default:
            pa_log_debug("Got an unexpected payload type on timing channel (%u) !", payload);
            break;
    }

    return written;
}

static int connect_udp_socket(pa_raop_client *c, int fd, uint16_t port) {
    struct sockaddr_in sa4;
#ifdef HAVE_IPV6
    struct sockaddr_in6 sa6;
#endif
    struct sockaddr *sa;
    socklen_t salen;
    sa_family_t af;

    pa_zero(sa4);
#ifdef HAVE_IPV6
    pa_zero(sa6);
#endif
    if (inet_pton(AF_INET, c->host, &sa4.sin_addr) > 0) {
        sa4.sin_family = af = AF_INET;
        sa4.sin_port = htons(port);
        sa = (struct sockaddr *) &sa4;
        salen = sizeof(sa4);
#ifdef HAVE_IPV6
    } else if (inet_pton(AF_INET6, c->host, &sa6.sin6_addr) > 0) {
        sa6.sin6_family = af = AF_INET6;
        sa6.sin6_port = htons(port);
        sa = (struct sockaddr *) &sa6;
        salen = sizeof(sa6);
#endif
    } else {
        pa_log("Invalid destination '%s'", c->host);
        goto fail;
    }

    if (fd < 0 && (fd = pa_socket_cloexec(af, SOCK_DGRAM, 0)) < 0) {
        pa_log("socket() failed: %s", pa_cstrerror(errno));
        goto fail;
    }

    /* If the socket queue is full, let's drop packets */
    pa_make_udp_socket_low_delay(fd);
    pa_make_fd_nonblock(fd);

    if (connect(fd, sa, salen) < 0) {
        pa_log("connect() failed: %s", pa_cstrerror(errno));
        goto fail;
    }

    pa_log_debug("Connected to %s on port %d (SOCK_DGRAM)", c->host, port);
    return fd;

fail:
    if (fd >= 0)
        pa_close(fd);

    return -1;
}

static int open_bind_udp_socket(pa_raop_client *c, uint16_t *actual_port) {
    int fd = -1;
    uint16_t port;
    struct sockaddr_in sa4;
#ifdef HAVE_IPV6
    struct sockaddr_in6 sa6;
#endif
    struct sockaddr *sa;
    uint16_t *sa_port;
    socklen_t salen;
    sa_family_t af;
    int one = 1;

    pa_assert(actual_port);

    port = *actual_port;

    pa_zero(sa4);
#ifdef HAVE_IPV6
    pa_zero(sa6);
#endif
    if (inet_pton(AF_INET, pa_rtsp_localip(c->rtsp), &sa4.sin_addr) > 0) {
        sa4.sin_family = af = AF_INET;
        sa4.sin_port = htons(port);
        sa4.sin_addr.s_addr = INADDR_ANY;
        sa = (struct sockaddr *) &sa4;
        salen = sizeof(sa4);
        sa_port = &sa4.sin_port;
#ifdef HAVE_IPV6
    } else if (inet_pton(AF_INET6, pa_rtsp_localip(c->rtsp), &sa6.sin6_addr) > 0) {
        sa6.sin6_family = af = AF_INET6;
        sa6.sin6_port = htons(port);
        sa6.sin6_addr = in6addr_any;
        sa = (struct sockaddr *) &sa6;
        salen = sizeof(sa6);
        sa_port = &sa6.sin6_port;
#endif
    } else {
        pa_log("Could not determine which address family to use");
        goto fail;
    }

    if ((fd = pa_socket_cloexec(af, SOCK_DGRAM, 0)) < 0) {
        pa_log("socket() failed: %s", pa_cstrerror(errno));
        goto fail;
    }

#ifdef SO_TIMESTAMP
    if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &one, sizeof(one)) < 0) {
        pa_log("setsockopt(SO_TIMESTAMP) failed: %s", pa_cstrerror(errno));
        goto fail;
    }
#else
    pa_log("SO_TIMESTAMP unsupported on this platform");
    goto fail;
#endif

    one = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) {
        pa_log("setsockopt(SO_REUSEADDR) failed: %s", pa_cstrerror(errno));
        goto fail;
    }

    do {
        int ret;

        *sa_port = htons(port);
        ret = bind(fd, sa, salen);
        if (!ret)
            break;
        if (ret < 0 && errno != EADDRINUSE) {
            pa_log("bind() failed: %s", pa_cstrerror(errno));
            goto fail;
        }
    } while (++port > 0);

    if (!port) {
        pa_log("Could not bind port");
        goto fail;
    }

    pa_log_debug("Socket bound to port %d (SOCK_DGRAM)", port);
    *actual_port = port;

    return fd;

fail:
    if (fd >= 0)
        pa_close(fd);

    return -1;
}

static void tcp_connection_cb(pa_socket_client *sc, pa_iochannel *io, void *userdata) {
    pa_raop_client *c = userdata;

    pa_assert(sc);
    pa_assert(c);

    pa_socket_client_unref(sc);

    if (!io) {
        pa_log("Connection failed: %s", pa_cstrerror(errno));
        return;
    }

    c->tcp_sfd = pa_iochannel_get_send_fd(io);
    pa_iochannel_set_noclose(io, true);
    pa_make_tcp_socket_low_delay(c->tcp_sfd);

    pa_iochannel_free(io);

    pa_log_debug("Connection established (TCP)");

    if (c->state_callback)
        c->state_callback(PA_RAOP_CONNECTED, c->state_userdata);
}

static void rtsp_stream_cb(pa_rtsp_client *rtsp, pa_rtsp_state_t state, pa_rtsp_status_t status, pa_headerlist *headers, void *userdata) {
    pa_raop_client *c = userdata;

    pa_assert(c);
    pa_assert(rtsp);
    pa_assert(rtsp == c->rtsp);

    switch (state) {
        case STATE_CONNECT: {
            char *key, *iv, *sdp = NULL;
            int frames = 0;
            const char *ip;
            char *url;
            int ipv;

            pa_log_debug("RAOP: CONNECTED");

            ip = pa_rtsp_localip(c->rtsp);
            if (pa_is_ip6_address(ip)) {
                ipv = 6;
                url = pa_sprintf_malloc("rtsp://[%s]/%s", ip, c->sid);
            } else {
                ipv = 4;
                url = pa_sprintf_malloc("rtsp://%s/%s", ip, c->sid);
            }
            pa_rtsp_set_url(c->rtsp, url);

            if (c->protocol == PA_RAOP_PROTOCOL_TCP)
                frames = FRAMES_PER_TCP_PACKET;
            else if (c->protocol == PA_RAOP_PROTOCOL_UDP)
                frames = FRAMES_PER_UDP_PACKET;

            switch(c->encryption) {
                case PA_RAOP_ENCRYPTION_NONE: {
                    sdp = pa_sprintf_malloc(
                        "v=0\r\n"
                        "o=iTunes %s 0 IN IP%d %s\r\n"
                        "s=iTunes\r\n"
                        "c=IN IP%d %s\r\n"
                        "t=0 0\r\n"
                        "m=audio 0 RTP/AVP 96\r\n"
                        "a=rtpmap:96 AppleLossless\r\n"
                        "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n",
                        c->sid, ipv, ip, ipv, c->host, frames);

                    break;
                }

                case PA_RAOP_ENCRYPTION_RSA:
                case PA_RAOP_ENCRYPTION_FAIRPLAY:
                case PA_RAOP_ENCRYPTION_MFISAP:
                case PA_RAOP_ENCRYPTION_FAIRPLAY_SAP25: {
                    key = pa_raop_secret_get_key(c->secret);
                    if (!key) {
                        pa_log("pa_raop_secret_get_key() failed.");
                        pa_rtsp_disconnect(rtsp);
                        /* FIXME: This is an unrecoverable failure. We should notify
                         * the pa_raop_client owner so that it could shut itself
                         * down. */
                        goto connect_finish;
                    }

                    iv = pa_raop_secret_get_iv(c->secret);

                    sdp = pa_sprintf_malloc(
                        "v=0\r\n"
                        "o=iTunes %s 0 IN IP%d %s\r\n"
                        "s=iTunes\r\n"
                        "c=IN IP%d %s\r\n"
                        "t=0 0\r\n"
                        "m=audio 0 RTP/AVP 96\r\n"
                        "a=rtpmap:96 AppleLossless\r\n"
                        "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
                        "a=rsaaeskey:%s\r\n"
                        "a=aesiv:%s\r\n",
                        c->sid, ipv, ip, ipv, c->host, frames, key, iv);

                    pa_xfree(key);
                    pa_xfree(iv);
                    break;
                }
            }

            pa_rtsp_announce(c->rtsp, sdp);

connect_finish:
            pa_xfree(sdp);
            pa_xfree(url);
            break;
        }

        case STATE_OPTIONS: {
            pa_log_debug("RAOP: OPTIONS (stream cb)");

            break;
        }

        case STATE_ANNOUNCE: {
            uint16_t cport = DEFAULT_UDP_CONTROL_PORT;
            uint16_t tport = DEFAULT_UDP_TIMING_PORT;
            char *trs = NULL;

            pa_log_debug("RAOP: ANNOUNCE");

            if (c->protocol == PA_RAOP_PROTOCOL_TCP) {
                trs = pa_sprintf_malloc(
                    "RTP/AVP/TCP;unicast;interleaved=0-1;mode=record");
            } else if (c->protocol == PA_RAOP_PROTOCOL_UDP) {
                c->udp_cfd = open_bind_udp_socket(c, &cport);
                c->udp_tfd  = open_bind_udp_socket(c, &tport);
                if (c->udp_cfd < 0 || c->udp_tfd < 0)
                    goto annonce_error;

                trs = pa_sprintf_malloc(
                    "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;"
                    "control_port=%d;timing_port=%d",
                    cport, tport);
            }

            pa_rtsp_setup(c->rtsp, trs);

            pa_xfree(trs);
            break;

        annonce_error:
            if (c->udp_cfd >= 0)
                pa_close(c->udp_cfd);
            c->udp_cfd = -1;
            if (c->udp_tfd >= 0)
                pa_close(c->udp_tfd);
            c->udp_tfd = -1;

            pa_rtsp_client_free(c->rtsp);

            pa_log_error("Aborting RTSP announce, failed creating required sockets");

            c->rtsp = NULL;
            pa_xfree(trs);
            break;
        }

        case STATE_SETUP: {
            pa_socket_client *sc = NULL;
            uint32_t sport = DEFAULT_UDP_AUDIO_PORT;
            uint32_t cport =0, tport = 0;
            char *ajs, *token, *pc, *trs;
            const char *token_state = NULL;
            char delimiters[] = ";";

            pa_log_debug("RAOP: SETUP");

            ajs = pa_xstrdup(pa_headerlist_gets(headers, "Audio-Jack-Status"));

            if (ajs) {
                c->jack_type = JACK_TYPE_ANALOG;
                c->jack_status = JACK_STATUS_DISCONNECTED;

                while ((token = pa_split(ajs, delimiters, &token_state))) {
                    if ((pc = strstr(token, "="))) {
                      *pc = 0;
                      if (pa_streq(token, "type") && pa_streq(pc + 1, "digital"))
                          c->jack_type = JACK_TYPE_DIGITAL;
                    } else {
                        if (pa_streq(token, "connected"))
                            c->jack_status = JACK_STATUS_CONNECTED;
                    }
                    pa_xfree(token);
                }

            } else {
                pa_log_warn("\"Audio-Jack-Status\" missing in RTSP setup response");
            }

            sport = pa_rtsp_serverport(c->rtsp);
            if (sport <= 0)
                goto setup_error;

            token_state = NULL;
            if (c->protocol == PA_RAOP_PROTOCOL_TCP) {
                if (!(sc = pa_socket_client_new_string(c->core->mainloop, true, c->host, sport)))
                    goto setup_error;

                pa_socket_client_ref(sc);
                pa_socket_client_set_callback(sc, tcp_connection_cb, c);

                pa_socket_client_unref(sc);
                sc = NULL;
            } else if (c->protocol == PA_RAOP_PROTOCOL_UDP) {
                trs = pa_xstrdup(pa_headerlist_gets(headers, "Transport"));

                if (trs) {
                    /* Now parse out the server port component of the response. */
                    while ((token = pa_split(trs, delimiters, &token_state))) {
                        if ((pc = strstr(token, "="))) {
                            *pc = 0;
                            if (pa_streq(token, "control_port")) {
                                if (pa_atou(pc + 1, &cport) < 0)
                                    goto setup_error_parse;
                            }
                            if (pa_streq(token, "timing_port")) {
                                if (pa_atou(pc + 1, &tport) < 0)
                                    goto setup_error_parse;
                            }
                            *pc = '=';
                        }
                        pa_xfree(token);
                    }
                    pa_xfree(trs);
                } else {
                    pa_log_warn("\"Transport\" missing in RTSP setup response");
                }

                if (cport <= 0 || tport <= 0)
                    goto setup_error;

                if ((c->udp_sfd = connect_udp_socket(c, -1, sport)) <= 0)
                    goto setup_error;
                if ((c->udp_cfd = connect_udp_socket(c, c->udp_cfd, cport)) <= 0)
                    goto setup_error;
                if ((c->udp_tfd = connect_udp_socket(c, c->udp_tfd, tport)) <= 0)
                    goto setup_error;

                pa_log_debug("Connection established (UDP;control_port=%d;timing_port=%d)", cport, tport);

                if (c->state_callback)
                    c->state_callback(PA_RAOP_CONNECTED, c->state_userdata);
            }

            pa_rtsp_record(c->rtsp, &c->seq, &c->rtptime);

            pa_xfree(ajs);
            break;

        setup_error_parse:
            pa_log("Failed parsing server port components");
            pa_xfree(token);
            pa_xfree(trs);
            /* fall-thru */
        setup_error:
            if (c->tcp_sfd >= 0)
                pa_close(c->tcp_sfd);
            c->tcp_sfd = -1;

            if (c->udp_sfd >= 0)
                pa_close(c->udp_sfd);
            c->udp_sfd = -1;

            c->udp_cfd = c->udp_tfd = -1;

            pa_rtsp_client_free(c->rtsp);

            pa_log_error("aborting RTSP setup, failed creating required sockets");

            if (c->state_callback)
                c->state_callback(PA_RAOP_DISCONNECTED, c->state_userdata);

            c->rtsp = NULL;
            break;
        }

        case STATE_RECORD: {
            int32_t latency = 0;
            uint32_t ssrc;
            char *alt;

            pa_log_debug("RAOP: RECORD");

            alt = pa_xstrdup(pa_headerlist_gets(headers, "Audio-Latency"));
            if (alt) {
                if (pa_atoi(alt, &latency) < 0)
                    pa_log("Failed to parse audio latency");
            }

            pa_raop_packet_buffer_reset(c->pbuf, c->seq);

            pa_random(&ssrc, sizeof(ssrc));
            c->is_first_packet = true;
            c->is_recording = true;
            c->sync_count = 0;
            c->ssrc = ssrc;

            if (c->state_callback)
                c->state_callback((int) PA_RAOP_RECORDING, c->state_userdata);

            pa_xfree(alt);
            break;
        }

        case STATE_SET_PARAMETER: {
            pa_log_debug("RAOP: SET_PARAMETER");

            break;
        }

        case STATE_FLUSH: {
            pa_log_debug("RAOP: FLUSHED");

            break;
        }

        case STATE_TEARDOWN: {
            pa_log_debug("RAOP: TEARDOWN");

            if (c->tcp_sfd >= 0)
                pa_close(c->tcp_sfd);
            c->tcp_sfd = -1;

            if (c->udp_sfd >= 0)
                pa_close(c->udp_sfd);
            c->udp_sfd = -1;

            /* Polling sockets will be closed by sink */
            c->udp_cfd = c->udp_tfd = -1;
            c->tcp_sfd = -1;

            pa_rtsp_client_free(c->rtsp);
            pa_xfree(c->sid);
            c->rtsp = NULL;
            c->sid = NULL;

            if (c->state_callback)
                c->state_callback(PA_RAOP_DISCONNECTED, c->state_userdata);

            break;
        }

        case STATE_DISCONNECTED: {
            pa_log_debug("RAOP: DISCONNECTED");

            c->is_recording = false;

            if (c->tcp_sfd >= 0)
                pa_close(c->tcp_sfd);
            c->tcp_sfd = -1;

            if (c->udp_sfd >= 0)
                pa_close(c->udp_sfd);
            c->udp_sfd = -1;

            /* Polling sockets will be closed by sink */
            c->udp_cfd = c->udp_tfd = -1;
            c->tcp_sfd = -1;

            pa_log_error("RTSP control channel closed (disconnected)");

            pa_rtsp_client_free(c->rtsp);
            pa_xfree(c->sid);
            c->rtsp = NULL;
            c->sid = NULL;

            if (c->state_callback)
                c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);

            break;
        }
    }
}

static void rtsp_auth_cb(pa_rtsp_client *rtsp, pa_rtsp_state_t state, pa_rtsp_status_t status, pa_headerlist *headers, void *userdata) {
    pa_raop_client *c = userdata;

    pa_assert(c);
    pa_assert(rtsp);
    pa_assert(rtsp == c->rtsp);

    switch (state) {
        case STATE_CONNECT: {
            char *sci = NULL, *sac = NULL;
            uint8_t rac[APPLE_CHALLENGE_LENGTH];
            struct {
                uint32_t ci1;
                uint32_t ci2;
            } rci;

            pa_random(&rci, sizeof(rci));
            /* Generate a random Client-Instance number */
            sci = pa_sprintf_malloc("%08x%08x",rci.ci1, rci.ci2);
            pa_rtsp_add_header(c->rtsp, "Client-Instance", sci);

            pa_random(rac, APPLE_CHALLENGE_LENGTH);
            /* Generate a random Apple-Challenge key */
            pa_raop_base64_encode(rac, APPLE_CHALLENGE_LENGTH, &sac);
            rtrim_char(sac, '=');
            pa_rtsp_add_header(c->rtsp, "Apple-Challenge", sac);

            pa_rtsp_options(c->rtsp);

            pa_xfree(sac);
            pa_xfree(sci);
            break;
        }

        case STATE_OPTIONS: {
            static bool waiting = false;
            const char *current = NULL;
            char space[] = " ";
            char *token, *ath = NULL;
            char *publ, *wath, *mth = NULL, *val;
            char *realm = NULL, *nonce = NULL, *response = NULL;
            char comma[] = ",";

            pa_log_debug("RAOP: OPTIONS (auth cb)");
            /* We do not consider the Apple-Response */
            pa_rtsp_remove_header(c->rtsp, "Apple-Challenge");

            if (STATUS_UNAUTHORIZED == status) {
                wath = pa_xstrdup(pa_headerlist_gets(headers, "WWW-Authenticate"));
                if (true == waiting) {
                    pa_xfree(wath);
                    goto fail;
                }

                if (wath) {
                    mth = pa_split(wath, space, &current);
                    while ((token = pa_split(wath, comma, &current))) {
                        if ((val = strstr(token, "="))) {
                            if (NULL == realm && val > strstr(token, "realm"))
                                realm = pa_xstrdup(val + 2);
                            else if (NULL == nonce && val > strstr(token, "nonce"))
                                nonce = pa_xstrdup(val + 2);
                        }

                        pa_xfree(token);
                    }
                }

                if (pa_safe_streq(mth, "Basic") && realm) {
                    rtrim_char(realm, '\"');

                    pa_raop_basic_response(DEFAULT_USER_NAME, c->password, &response);
                    ath = pa_sprintf_malloc("Basic %s",
                        response);
                } else if (pa_safe_streq(mth, "Digest") && realm && nonce) {
                    rtrim_char(realm, '\"');
                    rtrim_char(nonce, '\"');

                    pa_raop_digest_response(DEFAULT_USER_NAME, realm, c->password, nonce, "*", &response);
                    ath = pa_sprintf_malloc("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"*\", response=\"%s\"",
                        DEFAULT_USER_NAME, realm, nonce,
                        response);
                } else {
                    pa_log_error("unsupported authentication method: %s", mth);
                    pa_xfree(realm);
                    pa_xfree(nonce);
                    pa_xfree(wath);
                    pa_xfree(mth);
                    goto error;
                }

                pa_xfree(response);
                pa_xfree(realm);
                pa_xfree(nonce);
                pa_xfree(wath);
                pa_xfree(mth);

                pa_rtsp_add_header(c->rtsp, "Authorization", ath);
                pa_xfree(ath);

                waiting = true;
                pa_rtsp_options(c->rtsp);
                break;
            }

            if (STATUS_OK == status) {
                publ = pa_xstrdup(pa_headerlist_gets(headers, "Public"));
                c->sci = pa_xstrdup(pa_rtsp_get_header(c->rtsp, "Client-Instance"));

                if (c->password)
                    pa_xfree(c->password);
                pa_xfree(publ);
                c->password = NULL;
            }

            if (c->state_callback)
                c->state_callback((int) PA_RAOP_AUTHENTICATED, c->state_userdata);
            pa_rtsp_client_free(c->rtsp);
            c->rtsp = NULL;

            waiting = false;
            break;

        fail:
            if (c->state_callback)
                c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);
            pa_rtsp_client_free(c->rtsp);
            c->rtsp = NULL;

            pa_log_error("aborting authentication, wrong password");

            waiting = false;
            break;

        error:
            if (c->state_callback)
                c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);
            pa_rtsp_client_free(c->rtsp);
            c->rtsp = NULL;

            pa_log_error("aborting authentication, unexpected failure");

            waiting = false;
            break;
        }

        case STATE_ANNOUNCE:
        case STATE_SETUP:
        case STATE_RECORD:
        case STATE_SET_PARAMETER:
        case STATE_FLUSH:
        case STATE_TEARDOWN:
        case STATE_DISCONNECTED:
        default: {
            if (c->state_callback)
                c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata);
            pa_rtsp_client_free(c->rtsp);
            c->rtsp = NULL;

            if (c->sci)
                pa_xfree(c->sci);
            c->sci = NULL;

            break;
        }
    }
}

pa_raop_client* pa_raop_client_new(pa_core *core, const char *host, pa_raop_protocol_t protocol,
                                   pa_raop_encryption_t encryption, pa_raop_codec_t codec) {
    pa_raop_client *c;

    pa_parsed_address a;
    pa_sample_spec ss;
    size_t size = 2;

    pa_assert(core);
    pa_assert(host);

    if (pa_parse_address(host, &a) < 0)
        return NULL;

    if (a.type == PA_PARSED_ADDRESS_UNIX) {
        pa_xfree(a.path_or_host);
        return NULL;
    }

    c = pa_xnew0(pa_raop_client, 1);
    c->core = core;
    c->host = a.path_or_host; /* Will eventually be freed on destruction of c */
    if (a.port > 0)
        c->port = a.port;
    else
        c->port = DEFAULT_RAOP_PORT;
    c->rtsp = NULL;
    c->sci = c->sid = NULL;
    c->password = NULL;

    c->protocol = protocol;
    c->encryption = encryption;
    c->codec = codec;

    c->tcp_sfd = -1;

    c->udp_sfd = -1;
    c->udp_cfd = -1;
    c->udp_tfd = -1;

    c->secret = NULL;
    if (c->encryption != PA_RAOP_ENCRYPTION_NONE)
        c->secret = pa_raop_secret_new();

    ss = core->default_sample_spec;
    if (c->protocol == PA_RAOP_PROTOCOL_UDP)
        size = RTX_BUFFERING_SECONDS * ss.rate / FRAMES_PER_UDP_PACKET;

    c->is_recording = false;
    c->is_first_packet = true;
    /* Packet sync interval should be around 1s (UDP only) */
    c->sync_interval = ss.rate / FRAMES_PER_UDP_PACKET;
    c->sync_count = 0;

    c->pbuf = pa_raop_packet_buffer_new(c->core->mempool, size);

    return c;
}

void pa_raop_client_free(pa_raop_client *c) {
    pa_assert(c);

    pa_raop_packet_buffer_free(c->pbuf);

    pa_xfree(c->sid);
    pa_xfree(c->sci);
    if (c->secret)
        pa_raop_secret_free(c->secret);
    pa_xfree(c->password);
    c->sci = c->sid = NULL;
    c->password = NULL;
    c->secret = NULL;

    if (c->rtsp)
        pa_rtsp_client_free(c->rtsp);
    c->rtsp = NULL;

    pa_xfree(c->host);
    pa_xfree(c);
}

int pa_raop_client_authenticate (pa_raop_client *c, const char *password) {
    int rv = 0;

    pa_assert(c);

    if (c->rtsp || c->password) {
        pa_log_debug("Authentication/Connection already in progress...");
        return 0;
    }

    c->password = NULL;
    if (password)
        c->password = pa_xstrdup(password);
    c->rtsp = pa_rtsp_client_new(c->core->mainloop, c->host, c->port, DEFAULT_USER_AGENT);

    pa_assert(c->rtsp);

    pa_rtsp_set_callback(c->rtsp, rtsp_auth_cb, c);
    rv = pa_rtsp_connect(c->rtsp);
    return rv;
}

bool pa_raop_client_is_authenticated(pa_raop_client *c) {
    pa_assert(c);

    return (c->sci != NULL);
}

int pa_raop_client_announce(pa_raop_client *c) {
    uint32_t sid;
    int rv = 0;

    pa_assert(c);

    if (c->rtsp) {
        pa_log_debug("Connection already in progress...");
        return 0;
    } else if (!c->sci) {
        pa_log_debug("ANNOUNCE requires a preliminary authentication");
        return 1;
    }

    c->rtsp = pa_rtsp_client_new(c->core->mainloop, c->host, c->port, DEFAULT_USER_AGENT);

    pa_assert(c->rtsp);

    c->sync_count = 0;
    c->is_recording = false;
    c->is_first_packet = true;
    pa_random(&sid, sizeof(sid));
    c->sid = pa_sprintf_malloc("%u", sid);
    pa_rtsp_set_callback(c->rtsp, rtsp_stream_cb, c);

    rv = pa_rtsp_connect(c->rtsp);
    return rv;
}

bool pa_raop_client_is_alive(pa_raop_client *c) {
    pa_assert(c);

    if (!c->rtsp || !c->sci) {
        pa_log_debug("Not alive, connection not established yet...");
        return false;
    }

    switch (c->protocol) {
        case PA_RAOP_PROTOCOL_TCP:
            if (c->tcp_sfd >= 0)
                return true;
            break;
        case PA_RAOP_PROTOCOL_UDP:
            if (c->udp_sfd >= 0)
                return true;
            break;
        default:
            break;
    }

    return false;
}

bool pa_raop_client_can_stream(pa_raop_client *c) {
    pa_assert(c);

    if (!c->rtsp || !c->sci) {
        pa_log_debug("Can't stream, connection not established yet...");
        return false;
    }

    switch (c->protocol) {
        case PA_RAOP_PROTOCOL_TCP:
            if (c->tcp_sfd >= 0 && c->is_recording)
                return true;
            break;
        case PA_RAOP_PROTOCOL_UDP:
            if (c->udp_sfd >= 0 && c->is_recording)
                return true;
            break;
        default:
            break;
    }

    return false;
}

int pa_raop_client_stream(pa_raop_client *c) {
    int rv = 0;

    pa_assert(c);

    if (!c->rtsp || !c->sci) {
        pa_log_debug("Streaming's impossible, connection not established yet...");
        return 0;
    }

    switch (c->protocol) {
        case PA_RAOP_PROTOCOL_TCP:
            if (c->tcp_sfd >= 0 && !c->is_recording) {
                c->is_recording = true;
                c->is_first_packet = true;
                c->sync_count = 0;
            }
            break;
        case PA_RAOP_PROTOCOL_UDP:
            if (c->udp_sfd >= 0 && !c->is_recording) {
                c->is_recording = true;
                c->is_first_packet = true;
                c->sync_count = 0;
            }
            break;
        default:
            rv = 1;
            break;
    }

    return rv;
}

int pa_raop_client_set_volume(pa_raop_client *c, pa_volume_t volume) {
    char *param;
    int rv = 0;
    double db;

    pa_assert(c);

    if (!c->rtsp) {
        pa_log_debug("Cannot SET_PARAMETER, connection not established yet...");
        return 0;
    } else if (!c->sci) {
        pa_log_debug("SET_PARAMETER requires a preliminary authentication");
        return 1;
    }

    db = pa_sw_volume_to_dB(volume);
    if (db < VOLUME_MIN)
        db = VOLUME_MIN;
    else if (db > VOLUME_MAX)
        db = VOLUME_MAX;

    pa_log_debug("volume=%u db=%.6f", volume, db);

    param = pa_sprintf_malloc("volume: %0.6f\r\n", db);
    /* We just hit and hope, cannot wait for the callback. */
    if (c->rtsp != NULL && pa_rtsp_exec_ready(c->rtsp))
        rv = pa_rtsp_setparameter(c->rtsp, param);

    pa_xfree(param);
    return rv;
}

int pa_raop_client_flush(pa_raop_client *c) {
    int rv = 0;

    pa_assert(c);

    if (!c->rtsp || !pa_rtsp_exec_ready(c->rtsp)) {
        pa_log_debug("Cannot FLUSH, connection not established yet...)");
        return 0;
    } else if (!c->sci) {
        pa_log_debug("FLUSH requires a preliminary authentication");
        return 1;
    }

    c->is_recording = false;

    rv = pa_rtsp_flush(c->rtsp, c->seq, c->rtptime);
    return rv;
}

int pa_raop_client_teardown(pa_raop_client *c) {
    int rv = 0;

    pa_assert(c);

    if (!c->rtsp) {
        pa_log_debug("Cannot TEARDOWN, connection not established yet...");
        return 0;
    } else if (!c->sci) {
        pa_log_debug("TEARDOWN requires a preliminary authentication");
        return 1;
    }

    c->is_recording = false;

    rv = pa_rtsp_teardown(c->rtsp);
    return rv;
}

void pa_raop_client_get_frames_per_block(pa_raop_client *c, size_t *frames) {
    pa_assert(c);
    pa_assert(frames);

    switch (c->protocol) {
        case PA_RAOP_PROTOCOL_TCP:
            *frames = FRAMES_PER_TCP_PACKET;
            break;
        case PA_RAOP_PROTOCOL_UDP:
            *frames = FRAMES_PER_UDP_PACKET;
            break;
        default:
            *frames = 0;
            break;
    }
}

bool pa_raop_client_register_pollfd(pa_raop_client *c, pa_rtpoll *poll, pa_rtpoll_item **poll_item) {
    struct pollfd *pollfd = NULL;
    pa_rtpoll_item *item = NULL;
    bool oob = true;

    pa_assert(c);
    pa_assert(poll);
    pa_assert(poll_item);

    switch (c->protocol) {
        case PA_RAOP_PROTOCOL_TCP:
            item = pa_rtpoll_item_new(poll, PA_RTPOLL_NEVER, 1);
            pollfd = pa_rtpoll_item_get_pollfd(item, NULL);
            pollfd->fd = c->tcp_sfd;
            pollfd->events = POLLOUT;
            pollfd->revents = 0;
            *poll_item = item;
            oob = false;
            break;
        case PA_RAOP_PROTOCOL_UDP:
            item = pa_rtpoll_item_new(poll, PA_RTPOLL_NEVER, 2);
            pollfd = pa_rtpoll_item_get_pollfd(item, NULL);
            pollfd->fd = c->udp_cfd;
            pollfd->events = POLLIN | POLLPRI;
            pollfd->revents = 0;
            pollfd++;
            pollfd->fd = c->udp_tfd;
            pollfd->events = POLLIN | POLLPRI;
            pollfd->revents = 0;
            *poll_item = item;
            oob = true;
            break;
        default:
            *poll_item = NULL;
            break;
    }

    return oob;
}

pa_volume_t pa_raop_client_adjust_volume(pa_raop_client *c, pa_volume_t volume) {
    double minv, maxv;

    pa_assert(c);

    if (c->protocol != PA_RAOP_PROTOCOL_UDP)
        return volume;

    maxv = pa_sw_volume_from_dB(0.0);
    minv = maxv * pow(10.0, VOLUME_DEF / 60.0);

    /* Adjust volume so that it fits into VOLUME_DEF <= v <= 0 dB */
    return volume - volume * (minv / maxv) + minv;
}

void pa_raop_client_handle_oob_packet(pa_raop_client *c, const int fd, const uint8_t packet[], ssize_t size) {
    pa_assert(c);
    pa_assert(fd >= 0);
    pa_assert(packet);

    if (c->protocol == PA_RAOP_PROTOCOL_UDP) {
        if (fd == c->udp_cfd) {
            pa_log_debug("Received UDP control packet...");
            handle_udp_control_packet(c, packet, size);
        } else if (fd == c->udp_tfd) {
            pa_log_debug("Received UDP timing packet...");
            handle_udp_timing_packet(c, packet, size);
        }
    }
}

ssize_t pa_raop_client_send_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) {
    ssize_t written = 0;

    pa_assert(c);
    pa_assert(block);

    /* Sync RTP & NTP timestamp if required (UDP). */
    if (c->protocol == PA_RAOP_PROTOCOL_UDP) {
        c->sync_count++;
        if (c->is_first_packet || c->sync_count >= c->sync_interval) {
            send_udp_sync_packet(c, c->rtptime);
            c->sync_count = 0;
        }
    }

    switch (c->protocol) {
        case PA_RAOP_PROTOCOL_TCP:
            written = send_tcp_audio_packet(c, block, offset);
            break;
        case PA_RAOP_PROTOCOL_UDP:
            written = send_udp_audio_packet(c, block, offset);
            break;
        default:
            written = -1;
            break;
    }

    c->is_first_packet = false;
    return written;
}

void pa_raop_client_set_state_callback(pa_raop_client *c, pa_raop_client_state_cb_t callback, void *userdata) {
    pa_assert(c);

    c->state_callback = callback;
    c->state_userdata = userdata;
}
