diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:03:18 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:03:18 +0000 |
commit | 2dd5bc6a074165ddfbd57c4bd52c2d2dac8f47a1 (patch) | |
tree | 465b29cb405d3af0b0ad50c78e1dccc636594fec /src/modules/raop/raop-client.c | |
parent | Initial commit. (diff) | |
download | pulseaudio-upstream.tar.xz pulseaudio-upstream.zip |
Adding upstream version 14.2.upstream/14.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/modules/raop/raop-client.c')
-rw-r--r-- | src/modules/raop/raop-client.c | 1854 |
1 files changed, 1854 insertions, 0 deletions
diff --git a/src/modules/raop/raop-client.c b/src/modules/raop/raop-client.c new file mode 100644 index 0000000..885b3f1 --- /dev/null +++ b/src/modules/raop/raop-client.c @@ -0,0 +1,1854 @@ +/*** + This file is part of PulseAudio. + + Copyright 2008 Colin Guthrie + Copyright 2013 Hajime Fujita + Copyright 2013 Martin Blanchard + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, + or (at your option) any later version. + + PulseAudio is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <stdint.h> +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <sys/ioctl.h> +#include <math.h> + +#ifdef HAVE_SYS_FILIO_H +#include <sys/filio.h> +#endif + +#include <pulse/xmalloc.h> +#include <pulse/timeval.h> +#include <pulse/sample.h> + +#include <pulsecore/core.h> +#include <pulsecore/core-error.h> +#include <pulsecore/core-rtclock.h> +#include <pulsecore/core-util.h> +#include <pulsecore/iochannel.h> +#include <pulsecore/arpa-inet.h> +#include <pulsecore/socket-client.h> +#include <pulsecore/socket-util.h> +#include <pulsecore/log.h> +#include <pulsecore/parseaddr.h> +#include <pulsecore/macro.h> +#include <pulsecore/memchunk.h> +#include <pulsecore/random.h> +#include <pulsecore/poll.h> + +#include <modules/rtp/rtsp_client.h> + +#include "raop-client.h" +#include "raop-packet-buffer.h" +#include "raop-crypto.h" +#include "raop-util.h" + +#define DEFAULT_RAOP_PORT 5000 + +#define FRAMES_PER_TCP_PACKET 4096 +#define FRAMES_PER_UDP_PACKET 352 + +#define RTX_BUFFERING_SECONDS 4 + +#define DEFAULT_TCP_AUDIO_PORT 6000 +#define DEFAULT_UDP_AUDIO_PORT 6000 +#define DEFAULT_UDP_CONTROL_PORT 6001 +#define DEFAULT_UDP_TIMING_PORT 6002 + +#define DEFAULT_USER_AGENT "iTunes/11.0.4 (Windows; N)" +#define DEFAULT_USER_NAME "iTunes" + +#define JACK_STATUS_DISCONNECTED 0 +#define JACK_STATUS_CONNECTED 1 +#define JACK_TYPE_ANALOG 0 +#define JACK_TYPE_DIGITAL 1 + +#define VOLUME_MAX 0.0 +#define VOLUME_DEF -30.0 +#define VOLUME_MIN -144.0 + +#define UDP_DEFAULT_PKT_BUF_SIZE 1000 +#define APPLE_CHALLENGE_LENGTH 16 + +struct pa_raop_client { + pa_core *core; + char *host; + uint16_t port; + pa_rtsp_client *rtsp; + char *sci, *sid; + char *password; + bool autoreconnect; + + pa_raop_protocol_t protocol; + pa_raop_encryption_t encryption; + pa_raop_codec_t codec; + + pa_raop_secret *secret; + + int tcp_sfd; + + int udp_sfd; + int udp_cfd; + int udp_tfd; + + pa_raop_packet_buffer *pbuf; + + uint16_t seq; + uint32_t rtptime; + bool is_recording; + uint32_t ssrc; + + bool is_first_packet; + uint32_t sync_interval; + uint32_t sync_count; + + uint8_t jack_type; + uint8_t jack_status; + + pa_raop_client_state_cb_t state_callback; + void *state_userdata; +}; + +/* Audio TCP packet header [16x8] (cf. rfc4571): + * [0,1] Frame marker; seems always 0x2400 + * [2,3] RTP packet size (following): 0x0000 (to be set) + * [4,5] RTP v2: 0x80 + * [5] Payload type: 0x60 | Marker bit: 0x80 (always set) + * [6,7] Sequence number: 0x0000 (to be set) + * [8,11] Timestamp: 0x00000000 (to be set) + * [12,15] SSRC: 0x00000000 (to be set) */ +#define PAYLOAD_TCP_AUDIO_DATA 0x60 +static const uint8_t tcp_audio_header[16] = { + 0x24, 0x00, 0x00, 0x00, + 0x80, 0xe0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 +}; + +/* Audio UDP packet header [12x8] (cf. rfc3550): + * [0] RTP v2: 0x80 + * [1] Payload type: 0x60 + * [2,3] Sequence number: 0x0000 (to be set) + * [4,7] Timestamp: 0x00000000 (to be set) + * [8,12] SSRC: 0x00000000 (to be set) */ +#define PAYLOAD_UDP_AUDIO_DATA 0x60 +static const uint8_t udp_audio_header[12] = { + 0x80, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 +}; + +/* Audio retransmission UDP packet header [4x8]: + * [0] RTP v2: 0x80 + * [1] Payload type: 0x56 | Marker bit: 0x80 (always set) + * [2] Unknown; seems always 0x01 + * [3] Unknown; seems some random number around 0x20~0x40 */ +#define PAYLOAD_RETRANSMIT_REQUEST 0x55 +#define PAYLOAD_RETRANSMIT_REPLY 0x56 +static const uint8_t udp_audio_retrans_header[4] = { + 0x80, 0xd6, 0x00, 0x00 +}; + +/* Sync packet header [8x8] (cf. rfc3550): + * [0] RTP v2: 0x80 + * [1] Payload type: 0x54 | Marker bit: 0x80 (always set) + * [2,3] Sequence number: 0x0007 + * [4,7] Timestamp: 0x00000000 (to be set) */ +static const uint8_t udp_sync_header[8] = { + 0x80, 0xd4, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x00 +}; + +/* Timing packet header [8x8] (cf. rfc3550): + * [0] RTP v2: 0x80 + * [1] Payload type: 0x53 | Marker bit: 0x80 (always set) + * [2,3] Sequence number: 0x0007 + * [4,7] Timestamp: 0x00000000 (unused) */ +#define PAYLOAD_TIMING_REQUEST 0x52 +#define PAYLOAD_TIMING_REPLY 0x53 +static const uint8_t udp_timing_header[8] = { + 0x80, 0xd3, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x00 +}; + +/** + * Function to trim a given character at the end of a string (no realloc). + * @param str Pointer to string + * @param rc Character to trim + */ +static inline void rtrim_char(char *str, char rc) { + char *sp = str + strlen(str) - 1; + while (sp >= str && *sp == rc) { + *sp = '\0'; + sp -= 1; + } +} + +/** + * Function to convert a timeval to ntp timestamp. + * @param tv Pointer to the timeval structure + * @return The NTP timestamp + */ +static inline uint64_t timeval_to_ntp(struct timeval *tv) { + uint64_t ntp = 0; + + /* Converting micro seconds to a fraction. */ + ntp = (uint64_t) tv->tv_usec * UINT32_MAX / PA_USEC_PER_SEC; + /* Moving reference from 1 Jan 1970 to 1 Jan 1900 (seconds). */ + ntp |= (uint64_t) (tv->tv_sec + 0x83aa7e80) << 32; + + return ntp; +} + +/** + * Function to write bits into a buffer. + * @param buffer Handle to the buffer. It will be incremented if new data requires it. + * @param bit_pos A pointer to a position buffer to keep track the current write location (0 for MSB, 7 for LSB) + * @param size A pointer to the byte size currently written. This allows the calling function to do simple buffer overflow checks + * @param data The data to write + * @param data_bit_len The number of bits from data to write + */ +static inline void bit_writer(uint8_t **buffer, uint8_t *bit_pos, size_t *size, uint8_t data, uint8_t data_bit_len) { + int bits_left, bit_overflow; + uint8_t bit_data; + + if (!data_bit_len) + return; + + /* If bit pos is zero, we will definitely use at least one bit from the current byte so size increments. */ + if (!*bit_pos) + *size += 1; + + /* Calc the number of bits left in the current byte of buffer. */ + bits_left = 7 - *bit_pos + 1; + /* Calc the overflow of bits in relation to how much space we have left... */ + bit_overflow = bits_left - data_bit_len; + if (bit_overflow >= 0) { + /* We can fit the new data in our current byte. + * As we write from MSB->LSB we need to left shift by the overflow amount. */ + bit_data = data << bit_overflow; + if (*bit_pos) + **buffer |= bit_data; + else + **buffer = bit_data; + /* If our data fits exactly into the current byte, we need to increment our pointer. */ + if (0 == bit_overflow) { + /* Do not increment size as it will be incremented on next call as bit_pos is zero. */ + *buffer += 1; + *bit_pos = 0; + } else { + *bit_pos += data_bit_len; + } + } else { + /* bit_overflow is negative, there for we will need a new byte from our buffer + * Firstly fill up what's left in the current byte. */ + bit_data = data >> -bit_overflow; + **buffer |= bit_data; + /* Increment our buffer pointer and size counter. */ + *buffer += 1; + *size += 1; + **buffer = data << (8 + bit_overflow); + *bit_pos = -bit_overflow; + } +} + +static size_t write_ALAC_data(uint8_t *packet, const size_t max, uint8_t *raw, size_t *length, bool compress) { + uint32_t nbs = (*length / 2) / 2; + uint8_t *ibp, *maxibp; + uint8_t *bp, bpos; + size_t size = 0; + + bp = packet; + pa_memzero(packet, max); + size = bpos = 0; + + bit_writer(&bp, &bpos, &size, 1, 3); /* channel=1, stereo */ + bit_writer(&bp, &bpos, &size, 0, 4); /* Unknown */ + bit_writer(&bp, &bpos, &size, 0, 8); /* Unknown */ + bit_writer(&bp, &bpos, &size, 0, 4); /* Unknown */ + bit_writer(&bp, &bpos, &size, 1, 1); /* Hassize */ + bit_writer(&bp, &bpos, &size, 0, 2); /* Unused */ + bit_writer(&bp, &bpos, &size, 1, 1); /* Is-not-compressed */ + /* Size of data, integer, big endian. */ + bit_writer(&bp, &bpos, &size, (nbs >> 24) & 0xff, 8); + bit_writer(&bp, &bpos, &size, (nbs >> 16) & 0xff, 8); + bit_writer(&bp, &bpos, &size, (nbs >> 8) & 0xff, 8); + bit_writer(&bp, &bpos, &size, (nbs) & 0xff, 8); + + ibp = raw; + maxibp = raw + (4 * nbs) - 4; + while (ibp <= maxibp) { + /* Byte swap stereo data. */ + bit_writer(&bp, &bpos, &size, *(ibp + 1), 8); + bit_writer(&bp, &bpos, &size, *(ibp + 0), 8); + bit_writer(&bp, &bpos, &size, *(ibp + 3), 8); + bit_writer(&bp, &bpos, &size, *(ibp + 2), 8); + ibp += 4; + } + + *length = (ibp - raw); + return size; +} + +static size_t build_tcp_audio_packet(pa_raop_client *c, pa_memchunk *block, pa_memchunk *packet) { + const size_t head = sizeof(tcp_audio_header); + uint32_t *buffer = NULL; + uint8_t *raw = NULL; + size_t length, size; + + raw = pa_memblock_acquire(block->memblock); + buffer = pa_memblock_acquire(packet->memblock); + buffer += packet->index / sizeof(uint32_t); + raw += block->index; + + /* Wrap sequence number to 0 then UINT16_MAX is reached */ + if (c->seq == UINT16_MAX) + c->seq = 0; + else + c->seq++; + + memcpy(buffer, tcp_audio_header, sizeof(tcp_audio_header)); + buffer[1] |= htonl((uint32_t) c->seq); + buffer[2] = htonl(c->rtptime); + buffer[3] = htonl(c->ssrc); + + length = block->length; + size = sizeof(tcp_audio_header); + if (c->codec == PA_RAOP_CODEC_ALAC) + size += write_ALAC_data(((uint8_t *) buffer + head), packet->length - head, raw, &length, false); + else { + pa_log_debug("Only ALAC encoding is supported, sending zeros..."); + pa_memzero(((uint8_t *) buffer + head), packet->length - head); + size += length; + } + + c->rtptime += length / 4; + + pa_memblock_release(block->memblock); + + buffer[0] |= htonl((uint32_t) size - 4); + if (c->encryption == PA_RAOP_ENCRYPTION_RSA) + pa_raop_aes_encrypt(c->secret, (uint8_t *) buffer + head, size - head); + + pa_memblock_release(packet->memblock); + packet->length = size; + + return size; +} + +static ssize_t send_tcp_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) { + static int write_type = 0; + const size_t max = sizeof(tcp_audio_header) + 8 + 16384; + pa_memchunk *packet = NULL; + uint8_t *buffer = NULL; + double progress = 0.0; + ssize_t written = -1; + size_t done = 0; + + packet = pa_raop_packet_buffer_retrieve(c->pbuf, c->seq); + + if (!packet || (packet && packet->length <= 0)) { + pa_assert(block->index == offset); + + if (!(packet = pa_raop_packet_buffer_prepare(c->pbuf, c->seq, max))) + return -1; + + packet->index = 0; + packet->length = max; + if (!build_tcp_audio_packet(c, block, packet)) + return -1; + } + + buffer = pa_memblock_acquire(packet->memblock); + + pa_assert(buffer); + + buffer += packet->index; + if (buffer && packet->length > 0) + written = pa_write(c->tcp_sfd, buffer, packet->length, &write_type); + if (written > 0) { + progress = (double) written / (double) packet->length; + packet->length -= written; + packet->index += written; + + done = block->length * progress; + block->length -= done; + block->index += done; + } + + pa_memblock_release(packet->memblock); + + return written; +} + +static size_t build_udp_audio_packet(pa_raop_client *c, pa_memchunk *block, pa_memchunk *packet) { + const size_t head = sizeof(udp_audio_header); + uint32_t *buffer = NULL; + uint8_t *raw = NULL; + size_t length, size; + + raw = pa_memblock_acquire(block->memblock); + buffer = pa_memblock_acquire(packet->memblock); + buffer += packet->index / sizeof(uint32_t); + raw += block->index; + + memcpy(buffer, udp_audio_header, sizeof(udp_audio_header)); + if (c->is_first_packet) + buffer[0] |= htonl((uint32_t) 0x80 << 16); + buffer[0] |= htonl((uint32_t) c->seq); + buffer[1] = htonl(c->rtptime); + buffer[2] = htonl(c->ssrc); + + length = block->length; + size = sizeof(udp_audio_header); + if (c->codec == PA_RAOP_CODEC_ALAC) + size += write_ALAC_data(((uint8_t *) buffer + head), packet->length - head, raw, &length, false); + else { + pa_log_debug("Only ALAC encoding is supported, sending zeros..."); + pa_memzero(((uint8_t *) buffer + head), packet->length - head); + size += length; + } + + c->rtptime += length / 4; + + /* Wrap sequence number to 0 then UINT16_MAX is reached */ + if (c->seq == UINT16_MAX) + c->seq = 0; + else + c->seq++; + + pa_memblock_release(block->memblock); + + if (c->encryption == PA_RAOP_ENCRYPTION_RSA) + pa_raop_aes_encrypt(c->secret, (uint8_t *) buffer + head, size - head); + + pa_memblock_release(packet->memblock); + packet->length = size; + + return size; +} + +static ssize_t send_udp_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) { + const size_t max = sizeof(udp_audio_retrans_header) + sizeof(udp_audio_header) + 8 + 1408; + pa_memchunk *packet = NULL; + uint8_t *buffer = NULL; + ssize_t written = -1; + + /* UDP packet has to be sent at once ! */ + pa_assert(block->index == offset); + + if (!(packet = pa_raop_packet_buffer_prepare(c->pbuf, c->seq, max))) + return -1; + + packet->index = sizeof(udp_audio_retrans_header); + packet->length = max - sizeof(udp_audio_retrans_header); + if (!build_udp_audio_packet(c, block, packet)) + return -1; + + buffer = pa_memblock_acquire(packet->memblock); + + pa_assert(buffer); + + buffer += packet->index; + if (buffer && packet->length > 0) + written = pa_write(c->udp_sfd, buffer, packet->length, NULL); + if (written < 0 && errno == EAGAIN) { + pa_log_debug("Discarding UDP (audio, seq=%d) packet due to EAGAIN (%s)", c->seq, pa_cstrerror(errno)); + written = packet->length; + } + + pa_memblock_release(packet->memblock); + /* It is meaningless to preseve the partial data */ + block->index += block->length; + block->length = 0; + + return written; +} + +static size_t rebuild_udp_audio_packet(pa_raop_client *c, uint16_t seq, pa_memchunk *packet) { + size_t size = sizeof(udp_audio_retrans_header); + uint32_t *buffer = NULL; + + buffer = pa_memblock_acquire(packet->memblock); + + memcpy(buffer, udp_audio_retrans_header, sizeof(udp_audio_retrans_header)); + buffer[0] |= htonl((uint32_t) seq); + size += packet->length; + + pa_memblock_release(packet->memblock); + packet->length += sizeof(udp_audio_retrans_header); + packet->index -= sizeof(udp_audio_retrans_header); + + return size; +} + +static ssize_t resend_udp_audio_packets(pa_raop_client *c, uint16_t seq, uint16_t nbp) { + ssize_t total = 0; + int i = 0; + + for (i = 0; i < nbp; i++) { + pa_memchunk *packet = NULL; + uint8_t *buffer = NULL; + ssize_t written = -1; + + if (!(packet = pa_raop_packet_buffer_retrieve(c->pbuf, seq + i))) + continue; + + if (packet->index > 0) { + if (!rebuild_udp_audio_packet(c, seq + i, packet)) + continue; + } + + pa_assert(packet->index == 0); + + buffer = pa_memblock_acquire(packet->memblock); + + pa_assert(buffer); + + if (buffer && packet->length > 0) + written = pa_write(c->udp_cfd, buffer, packet->length, NULL); + if (written < 0 && errno == EAGAIN) { + pa_log_debug("Discarding UDP (audio-retransmitted, seq=%d) packet due to EAGAIN", seq + i); + pa_memblock_release(packet->memblock); + continue; + } + + pa_memblock_release(packet->memblock); + total += written; + } + + return total; +} + +/* Caller has to free the allocated memory region for packet */ +static size_t build_udp_sync_packet(pa_raop_client *c, uint32_t stamp, uint32_t **packet) { + const size_t size = sizeof(udp_sync_header) + 12; + const uint32_t delay = 88200; + uint32_t *buffer = NULL; + uint64_t transmitted = 0; + struct timeval tv; + + *packet = NULL; + if (!(buffer = pa_xmalloc0(size))) + return 0; + + memcpy(buffer, udp_sync_header, sizeof(udp_sync_header)); + if (c->is_first_packet) + buffer[0] |= 0x10; + stamp -= delay; + buffer[1] = htonl(stamp); + /* Set the transmitted timestamp to current time. */ + transmitted = timeval_to_ntp(pa_rtclock_get(&tv)); + buffer[2] = htonl(transmitted >> 32); + buffer[3] = htonl(transmitted & 0xffffffff); + stamp += delay; + buffer[4] = htonl(stamp); + + *packet = buffer; + return size; +} + +static ssize_t send_udp_sync_packet(pa_raop_client *c, uint32_t stamp) { + uint32_t * packet = NULL; + ssize_t written = 0; + size_t size = 0; + + size = build_udp_sync_packet(c, stamp, &packet); + if (packet != NULL && size > 0) { + written = pa_loop_write(c->udp_cfd, packet, size, NULL); + pa_xfree(packet); + } + + return written; +} + +static size_t handle_udp_control_packet(pa_raop_client *c, const uint8_t packet[], ssize_t size) { + uint8_t payload = 0; + uint16_t seq, nbp = 0; + ssize_t written = 0; + + /* Control packets are 8 bytes long: */ + if (size != 8 || packet[0] != 0x80) + return 1; + + seq = ntohs((uint16_t) (packet[4] | packet[5] << 8)); + nbp = ntohs((uint16_t) (packet[6] | packet[7] << 8)); + if (nbp <= 0) + return 1; + + /* The marker bit is always set (see rfc3550 for packet structure) ! */ + payload = packet[1] ^ 0x80; + switch (payload) { + case PAYLOAD_RETRANSMIT_REQUEST: + pa_log_debug("Resending %u packets starting at %u", nbp, seq); + written = resend_udp_audio_packets(c, seq, nbp); + break; + case PAYLOAD_RETRANSMIT_REPLY: + default: + pa_log_debug("Got an unexpected payload type on control channel (%u) !", payload); + break; + } + + return written; +} + +/* Caller has to free the allocated memory region for packet */ +static size_t build_udp_timing_packet(pa_raop_client *c, const uint32_t data[6], uint64_t received, uint32_t **packet) { + const size_t size = sizeof(udp_timing_header) + 24; + uint32_t *buffer = NULL; + uint64_t transmitted = 0; + struct timeval tv; + + *packet = NULL; + if (!(buffer = pa_xmalloc0(size))) + return 0; + + memcpy(buffer, udp_timing_header, sizeof(udp_timing_header)); + /* Copying originate timestamp from the incoming request packet. */ + buffer[2] = data[4]; + buffer[3] = data[5]; + /* Set the receive timestamp to reception time. */ + buffer[4] = htonl(received >> 32); + buffer[5] = htonl(received & 0xffffffff); + /* Set the transmit timestamp to current time. */ + transmitted = timeval_to_ntp(pa_rtclock_get(&tv)); + buffer[6] = htonl(transmitted >> 32); + buffer[7] = htonl(transmitted & 0xffffffff); + + *packet = buffer; + return size; +} + +static ssize_t send_udp_timing_packet(pa_raop_client *c, const uint32_t data[6], uint64_t received) { + uint32_t * packet = NULL; + ssize_t written = 0; + size_t size = 0; + + size = build_udp_timing_packet(c, data, received, &packet); + if (packet != NULL && size > 0) { + written = pa_loop_write(c->udp_tfd, packet, size, NULL); + pa_xfree(packet); + } + + return written; +} + +static size_t handle_udp_timing_packet(pa_raop_client *c, const uint8_t packet[], ssize_t size) { + const uint32_t * data = NULL; + uint8_t payload = 0; + struct timeval tv; + size_t written = 0; + uint64_t rci = 0; + + /* Timing packets are 32 bytes long: 1 x 8 RTP header (no ssrc) + 3 x 8 NTP timestamps */ + if (size != 32 || packet[0] != 0x80) + return 0; + + rci = timeval_to_ntp(pa_rtclock_get(&tv)); + data = (uint32_t *) (packet + sizeof(udp_timing_header)); + + /* The marker bit is always set (see rfc3550 for packet structure) ! */ + payload = packet[1] ^ 0x80; + switch (payload) { + case PAYLOAD_TIMING_REQUEST: + pa_log_debug("Sending timing packet at %" PRIu64 , rci); + written = send_udp_timing_packet(c, data, rci); + break; + case PAYLOAD_TIMING_REPLY: + default: + pa_log_debug("Got an unexpected payload type on timing channel (%u) !", payload); + break; + } + + return written; +} + +static void send_initial_udp_timing_packet(pa_raop_client *c) { + uint32_t data[6] = { 0 }; + struct timeval tv; + uint64_t initial_time = 0; + + initial_time = timeval_to_ntp(pa_rtclock_get(&tv)); + data[4] = htonl(initial_time >> 32); + data[5] = htonl(initial_time & 0xffffffff); + + send_udp_timing_packet(c, data, initial_time); +} + +static int connect_udp_socket(pa_raop_client *c, int fd, uint16_t port) { + struct sockaddr_in sa4; +#ifdef HAVE_IPV6 + struct sockaddr_in6 sa6; +#endif + struct sockaddr *sa; + socklen_t salen; + sa_family_t af; + + pa_zero(sa4); +#ifdef HAVE_IPV6 + pa_zero(sa6); +#endif + if (inet_pton(AF_INET, c->host, &sa4.sin_addr) > 0) { + sa4.sin_family = af = AF_INET; + sa4.sin_port = htons(port); + sa = (struct sockaddr *) &sa4; + salen = sizeof(sa4); +#ifdef HAVE_IPV6 + } else if (inet_pton(AF_INET6, c->host, &sa6.sin6_addr) > 0) { + sa6.sin6_family = af = AF_INET6; + sa6.sin6_port = htons(port); + sa = (struct sockaddr *) &sa6; + salen = sizeof(sa6); +#endif + } else { + pa_log("Invalid destination '%s'", c->host); + goto fail; + } + + if (fd < 0 && (fd = pa_socket_cloexec(af, SOCK_DGRAM, 0)) < 0) { + pa_log("socket() failed: %s", pa_cstrerror(errno)); + goto fail; + } + + /* If the socket queue is full, let's drop packets */ + pa_make_udp_socket_low_delay(fd); + pa_make_fd_nonblock(fd); + + if (connect(fd, sa, salen) < 0) { + pa_log("connect() failed: %s", pa_cstrerror(errno)); + goto fail; + } + + pa_log_debug("Connected to %s on port %d (SOCK_DGRAM)", c->host, port); + return fd; + +fail: + if (fd >= 0) + pa_close(fd); + + return -1; +} + +static int open_bind_udp_socket(pa_raop_client *c, uint16_t *actual_port) { + int fd = -1; + uint16_t port; + struct sockaddr_in sa4; +#ifdef HAVE_IPV6 + struct sockaddr_in6 sa6; +#endif + struct sockaddr *sa; + uint16_t *sa_port; + socklen_t salen; + sa_family_t af; + int one = 1; + + pa_assert(actual_port); + + port = *actual_port; + + pa_zero(sa4); +#ifdef HAVE_IPV6 + pa_zero(sa6); +#endif + if (inet_pton(AF_INET, pa_rtsp_localip(c->rtsp), &sa4.sin_addr) > 0) { + sa4.sin_family = af = AF_INET; + sa4.sin_port = htons(port); + sa4.sin_addr.s_addr = INADDR_ANY; + sa = (struct sockaddr *) &sa4; + salen = sizeof(sa4); + sa_port = &sa4.sin_port; +#ifdef HAVE_IPV6 + } else if (inet_pton(AF_INET6, pa_rtsp_localip(c->rtsp), &sa6.sin6_addr) > 0) { + sa6.sin6_family = af = AF_INET6; + sa6.sin6_port = htons(port); + sa6.sin6_addr = in6addr_any; + sa = (struct sockaddr *) &sa6; + salen = sizeof(sa6); + sa_port = &sa6.sin6_port; +#endif + } else { + pa_log("Could not determine which address family to use"); + goto fail; + } + + if ((fd = pa_socket_cloexec(af, SOCK_DGRAM, 0)) < 0) { + pa_log("socket() failed: %s", pa_cstrerror(errno)); + goto fail; + } + +#ifdef SO_TIMESTAMP + if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &one, sizeof(one)) < 0) { + pa_log("setsockopt(SO_TIMESTAMP) failed: %s", pa_cstrerror(errno)); + goto fail; + } +#else + pa_log("SO_TIMESTAMP unsupported on this platform"); + goto fail; +#endif + + one = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) { + pa_log("setsockopt(SO_REUSEADDR) failed: %s", pa_cstrerror(errno)); + goto fail; + } + + do { + int ret; + + *sa_port = htons(port); + ret = bind(fd, sa, salen); + if (!ret) + break; + if (ret < 0 && errno != EADDRINUSE) { + pa_log("bind() failed: %s", pa_cstrerror(errno)); + goto fail; + } + } while (++port > 0); + + if (!port) { + pa_log("Could not bind port"); + goto fail; + } + + pa_log_debug("Socket bound to port %d (SOCK_DGRAM)", port); + *actual_port = port; + + return fd; + +fail: + if (fd >= 0) + pa_close(fd); + + return -1; +} + +static void tcp_connection_cb(pa_socket_client *sc, pa_iochannel *io, void *userdata) { + pa_raop_client *c = userdata; + + pa_assert(sc); + pa_assert(c); + + pa_socket_client_unref(sc); + + if (!io) { + pa_log("Connection failed: %s", pa_cstrerror(errno)); + return; + } + + c->tcp_sfd = pa_iochannel_get_send_fd(io); + pa_iochannel_set_noclose(io, true); + pa_make_tcp_socket_low_delay(c->tcp_sfd); + + pa_iochannel_free(io); + + pa_log_debug("Connection established (TCP)"); + + if (c->state_callback) + c->state_callback(PA_RAOP_CONNECTED, c->state_userdata); +} + +static void rtsp_stream_cb(pa_rtsp_client *rtsp, pa_rtsp_state_t state, pa_rtsp_status_t status, pa_headerlist *headers, void *userdata) { + pa_raop_client *c = userdata; + + pa_assert(c); + pa_assert(rtsp); + pa_assert(rtsp == c->rtsp); + + switch (state) { + case STATE_CONNECT: { + char *key, *iv, *sdp = NULL; + int frames = 0; + const char *ip; + char *url; + int ipv; + + pa_log_debug("RAOP: CONNECTED"); + + ip = pa_rtsp_localip(c->rtsp); + if (pa_is_ip6_address(ip)) { + ipv = 6; + url = pa_sprintf_malloc("rtsp://[%s]/%s", ip, c->sid); + } else { + ipv = 4; + url = pa_sprintf_malloc("rtsp://%s/%s", ip, c->sid); + } + pa_rtsp_set_url(c->rtsp, url); + + if (c->protocol == PA_RAOP_PROTOCOL_TCP) + frames = FRAMES_PER_TCP_PACKET; + else if (c->protocol == PA_RAOP_PROTOCOL_UDP) + frames = FRAMES_PER_UDP_PACKET; + + switch(c->encryption) { + case PA_RAOP_ENCRYPTION_NONE: { + sdp = pa_sprintf_malloc( + "v=0\r\n" + "o=iTunes %s 0 IN IP%d %s\r\n" + "s=iTunes\r\n" + "c=IN IP%d %s\r\n" + "t=0 0\r\n" + "m=audio 0 RTP/AVP 96\r\n" + "a=rtpmap:96 AppleLossless\r\n" + "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n", + c->sid, ipv, ip, ipv, c->host, frames); + + break; + } + + case PA_RAOP_ENCRYPTION_RSA: + case PA_RAOP_ENCRYPTION_FAIRPLAY: + case PA_RAOP_ENCRYPTION_MFISAP: + case PA_RAOP_ENCRYPTION_FAIRPLAY_SAP25: { + key = pa_raop_secret_get_key(c->secret); + if (!key) { + pa_log("pa_raop_secret_get_key() failed."); + pa_rtsp_disconnect(rtsp); + /* FIXME: This is an unrecoverable failure. We should notify + * the pa_raop_client owner so that it could shut itself + * down. */ + goto connect_finish; + } + + iv = pa_raop_secret_get_iv(c->secret); + + sdp = pa_sprintf_malloc( + "v=0\r\n" + "o=iTunes %s 0 IN IP%d %s\r\n" + "s=iTunes\r\n" + "c=IN IP%d %s\r\n" + "t=0 0\r\n" + "m=audio 0 RTP/AVP 96\r\n" + "a=rtpmap:96 AppleLossless\r\n" + "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n" + "a=rsaaeskey:%s\r\n" + "a=aesiv:%s\r\n", + c->sid, ipv, ip, ipv, c->host, frames, key, iv); + + pa_xfree(key); + pa_xfree(iv); + break; + } + } + + pa_rtsp_announce(c->rtsp, sdp); + +connect_finish: + pa_xfree(sdp); + pa_xfree(url); + break; + } + + case STATE_OPTIONS: { + pa_log_debug("RAOP: OPTIONS (stream cb)"); + + break; + } + + case STATE_ANNOUNCE: { + uint16_t cport = DEFAULT_UDP_CONTROL_PORT; + uint16_t tport = DEFAULT_UDP_TIMING_PORT; + char *trs = NULL; + + pa_log_debug("RAOP: ANNOUNCE"); + + if (c->protocol == PA_RAOP_PROTOCOL_TCP) { + trs = pa_sprintf_malloc( + "RTP/AVP/TCP;unicast;interleaved=0-1;mode=record"); + } else if (c->protocol == PA_RAOP_PROTOCOL_UDP) { + c->udp_cfd = open_bind_udp_socket(c, &cport); + c->udp_tfd = open_bind_udp_socket(c, &tport); + if (c->udp_cfd < 0 || c->udp_tfd < 0) + goto annonce_error; + + trs = pa_sprintf_malloc( + "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;" + "control_port=%d;timing_port=%d", + cport, tport); + } + + pa_rtsp_setup(c->rtsp, trs); + + pa_xfree(trs); + break; + + annonce_error: + if (c->udp_cfd >= 0) + pa_close(c->udp_cfd); + c->udp_cfd = -1; + if (c->udp_tfd >= 0) + pa_close(c->udp_tfd); + c->udp_tfd = -1; + + pa_rtsp_client_free(c->rtsp); + + pa_log_error("Aborting RTSP announce, failed creating required sockets"); + + c->rtsp = NULL; + pa_xfree(trs); + break; + } + + case STATE_SETUP: { + pa_socket_client *sc = NULL; + uint32_t sport = DEFAULT_UDP_AUDIO_PORT; + uint32_t cport =0, tport = 0; + char *ajs, *token, *pc, *trs; + const char *token_state = NULL; + char delimiters[] = ";"; + + pa_log_debug("RAOP: SETUP"); + + ajs = pa_xstrdup(pa_headerlist_gets(headers, "Audio-Jack-Status")); + + if (ajs) { + c->jack_type = JACK_TYPE_ANALOG; + c->jack_status = JACK_STATUS_DISCONNECTED; + + while ((token = pa_split(ajs, delimiters, &token_state))) { + if ((pc = strstr(token, "="))) { + *pc = 0; + if (pa_streq(token, "type") && pa_streq(pc + 1, "digital")) + c->jack_type = JACK_TYPE_DIGITAL; + } else { + if (pa_streq(token, "connected")) + c->jack_status = JACK_STATUS_CONNECTED; + } + pa_xfree(token); + } + + } else { + pa_log_warn("\"Audio-Jack-Status\" missing in RTSP setup response"); + } + + sport = pa_rtsp_serverport(c->rtsp); + if (sport <= 0) + goto setup_error; + + token_state = NULL; + if (c->protocol == PA_RAOP_PROTOCOL_TCP) { + if (!(sc = pa_socket_client_new_string(c->core->mainloop, true, c->host, sport))) + goto setup_error; + + pa_socket_client_ref(sc); + pa_socket_client_set_callback(sc, tcp_connection_cb, c); + + pa_socket_client_unref(sc); + sc = NULL; + } else if (c->protocol == PA_RAOP_PROTOCOL_UDP) { + trs = pa_xstrdup(pa_headerlist_gets(headers, "Transport")); + + if (trs) { + /* Now parse out the server port component of the response. */ + while ((token = pa_split(trs, delimiters, &token_state))) { + if ((pc = strstr(token, "="))) { + *pc = 0; + if (pa_streq(token, "control_port")) { + if (pa_atou(pc + 1, &cport) < 0) + goto setup_error_parse; + } + if (pa_streq(token, "timing_port")) { + if (pa_atou(pc + 1, &tport) < 0) + goto setup_error_parse; + } + *pc = '='; + } + pa_xfree(token); + } + pa_xfree(trs); + } else { + pa_log_warn("\"Transport\" missing in RTSP setup response"); + } + + if (cport <= 0 || tport <= 0) + goto setup_error; + + if ((c->udp_sfd = connect_udp_socket(c, -1, sport)) <= 0) + goto setup_error; + if ((c->udp_cfd = connect_udp_socket(c, c->udp_cfd, cport)) <= 0) + goto setup_error; + if ((c->udp_tfd = connect_udp_socket(c, c->udp_tfd, tport)) <= 0) + goto setup_error; + + pa_log_debug("Connection established (UDP;control_port=%d;timing_port=%d)", cport, tport); + + /* Send an initial UDP packet so a connection tracking firewall + * knows the src_ip:src_port <-> dest_ip:dest_port relation + * and accepts the incoming timing packets. + */ + send_initial_udp_timing_packet(c); + pa_log_debug("Sent initial timing packet to UDP port %d", tport); + + if (c->state_callback) + c->state_callback(PA_RAOP_CONNECTED, c->state_userdata); + } + + pa_rtsp_record(c->rtsp, &c->seq, &c->rtptime); + + pa_xfree(ajs); + break; + + setup_error_parse: + pa_log("Failed parsing server port components"); + pa_xfree(token); + pa_xfree(trs); + /* fall-thru */ + setup_error: + if (c->tcp_sfd >= 0) + pa_close(c->tcp_sfd); + c->tcp_sfd = -1; + + if (c->udp_sfd >= 0) + pa_close(c->udp_sfd); + c->udp_sfd = -1; + + c->udp_cfd = c->udp_tfd = -1; + + pa_rtsp_client_free(c->rtsp); + + pa_log_error("aborting RTSP setup, failed creating required sockets"); + + if (c->state_callback) + c->state_callback(PA_RAOP_DISCONNECTED, c->state_userdata); + + c->rtsp = NULL; + break; + } + + case STATE_RECORD: { + int32_t latency = 0; + uint32_t ssrc; + char *alt; + + pa_log_debug("RAOP: RECORD"); + + alt = pa_xstrdup(pa_headerlist_gets(headers, "Audio-Latency")); + if (alt) { + if (pa_atoi(alt, &latency) < 0) + pa_log("Failed to parse audio latency"); + } + + pa_raop_packet_buffer_reset(c->pbuf, c->seq); + + pa_random(&ssrc, sizeof(ssrc)); + c->is_first_packet = true; + c->is_recording = true; + c->sync_count = 0; + c->ssrc = ssrc; + + if (c->state_callback) + c->state_callback((int) PA_RAOP_RECORDING, c->state_userdata); + + pa_xfree(alt); + break; + } + + case STATE_SET_PARAMETER: { + pa_log_debug("RAOP: SET_PARAMETER"); + + break; + } + + case STATE_FLUSH: { + pa_log_debug("RAOP: FLUSHED"); + + break; + } + + case STATE_TEARDOWN: { + pa_log_debug("RAOP: TEARDOWN"); + + if (c->tcp_sfd >= 0) + pa_close(c->tcp_sfd); + c->tcp_sfd = -1; + + if (c->udp_sfd >= 0) + pa_close(c->udp_sfd); + c->udp_sfd = -1; + + /* Polling sockets will be closed by sink */ + c->udp_cfd = c->udp_tfd = -1; + c->tcp_sfd = -1; + + pa_rtsp_client_free(c->rtsp); + pa_xfree(c->sid); + c->rtsp = NULL; + c->sid = NULL; + + if (c->state_callback) + c->state_callback(PA_RAOP_DISCONNECTED, c->state_userdata); + + break; + } + + case STATE_DISCONNECTED: { + pa_log_debug("RAOP: DISCONNECTED"); + + c->is_recording = false; + + if (c->tcp_sfd >= 0) + pa_close(c->tcp_sfd); + c->tcp_sfd = -1; + + if (c->udp_sfd >= 0) + pa_close(c->udp_sfd); + c->udp_sfd = -1; + + /* Polling sockets will be closed by sink */ + c->udp_cfd = c->udp_tfd = -1; + c->tcp_sfd = -1; + + pa_log_error("RTSP control channel closed (disconnected)"); + + pa_rtsp_client_free(c->rtsp); + pa_xfree(c->sid); + c->rtsp = NULL; + c->sid = NULL; + + if (c->state_callback) + c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata); + + break; + } + } +} + +static void rtsp_auth_cb(pa_rtsp_client *rtsp, pa_rtsp_state_t state, pa_rtsp_status_t status, pa_headerlist *headers, void *userdata) { + pa_raop_client *c = userdata; + + pa_assert(c); + pa_assert(rtsp); + pa_assert(rtsp == c->rtsp); + + switch (state) { + case STATE_CONNECT: { + char *sci = NULL, *sac = NULL; + uint8_t rac[APPLE_CHALLENGE_LENGTH]; + struct { + uint32_t ci1; + uint32_t ci2; + } rci; + + pa_random(&rci, sizeof(rci)); + /* Generate a random Client-Instance number */ + sci = pa_sprintf_malloc("%08x%08x",rci.ci1, rci.ci2); + pa_rtsp_add_header(c->rtsp, "Client-Instance", sci); + + pa_random(rac, APPLE_CHALLENGE_LENGTH); + /* Generate a random Apple-Challenge key */ + pa_raop_base64_encode(rac, APPLE_CHALLENGE_LENGTH, &sac); + rtrim_char(sac, '='); + pa_rtsp_add_header(c->rtsp, "Apple-Challenge", sac); + + pa_rtsp_options(c->rtsp); + + pa_xfree(sac); + pa_xfree(sci); + break; + } + + case STATE_OPTIONS: { + static bool waiting = false; + const char *current = NULL; + char space[] = " "; + char *token, *ath = NULL; + char *publ, *wath, *mth = NULL, *val; + char *realm = NULL, *nonce = NULL, *response = NULL; + char comma[] = ","; + + pa_log_debug("RAOP: OPTIONS (auth cb)"); + /* We do not consider the Apple-Response */ + pa_rtsp_remove_header(c->rtsp, "Apple-Challenge"); + + if (STATUS_UNAUTHORIZED == status) { + wath = pa_xstrdup(pa_headerlist_gets(headers, "WWW-Authenticate")); + if (true == waiting) { + pa_xfree(wath); + goto fail; + } + + if (wath) { + mth = pa_split(wath, space, ¤t); + while ((token = pa_split(wath, comma, ¤t))) { + if ((val = strstr(token, "="))) { + if (NULL == realm && val > strstr(token, "realm")) + realm = pa_xstrdup(val + 2); + else if (NULL == nonce && val > strstr(token, "nonce")) + nonce = pa_xstrdup(val + 2); + } + + pa_xfree(token); + } + } + + if (pa_safe_streq(mth, "Basic") && realm) { + rtrim_char(realm, '\"'); + + pa_raop_basic_response(DEFAULT_USER_NAME, c->password, &response); + ath = pa_sprintf_malloc("Basic %s", + response); + } else if (pa_safe_streq(mth, "Digest") && realm && nonce) { + rtrim_char(realm, '\"'); + rtrim_char(nonce, '\"'); + + pa_raop_digest_response(DEFAULT_USER_NAME, realm, c->password, nonce, "*", &response); + ath = pa_sprintf_malloc("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"*\", response=\"%s\"", + DEFAULT_USER_NAME, realm, nonce, + response); + } else { + pa_log_error("unsupported authentication method: %s", mth); + pa_xfree(realm); + pa_xfree(nonce); + pa_xfree(wath); + pa_xfree(mth); + goto error; + } + + pa_xfree(response); + pa_xfree(realm); + pa_xfree(nonce); + pa_xfree(wath); + pa_xfree(mth); + + pa_rtsp_add_header(c->rtsp, "Authorization", ath); + pa_xfree(ath); + + waiting = true; + pa_rtsp_options(c->rtsp); + break; + } + + if (STATUS_OK == status) { + publ = pa_xstrdup(pa_headerlist_gets(headers, "Public")); + c->sci = pa_xstrdup(pa_rtsp_get_header(c->rtsp, "Client-Instance")); + + if (c->password) + pa_xfree(c->password); + pa_xfree(publ); + c->password = NULL; + } + + pa_rtsp_client_free(c->rtsp); + c->rtsp = NULL; + /* Ensure everything is cleaned before calling the callback, otherwise it may raise a crash */ + if (c->state_callback) + c->state_callback((int) PA_RAOP_AUTHENTICATED, c->state_userdata); + + waiting = false; + break; + + fail: + if (c->state_callback) + c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata); + pa_rtsp_client_free(c->rtsp); + c->rtsp = NULL; + + pa_log_error("aborting authentication, wrong password"); + + waiting = false; + break; + + error: + if (c->state_callback) + c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata); + pa_rtsp_client_free(c->rtsp); + c->rtsp = NULL; + + pa_log_error("aborting authentication, unexpected failure"); + + waiting = false; + break; + } + + case STATE_ANNOUNCE: + case STATE_SETUP: + case STATE_RECORD: + case STATE_SET_PARAMETER: + case STATE_FLUSH: + case STATE_TEARDOWN: + case STATE_DISCONNECTED: + default: { + if (c->state_callback) + c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata); + pa_rtsp_client_free(c->rtsp); + c->rtsp = NULL; + + if (c->sci) + pa_xfree(c->sci); + c->sci = NULL; + + break; + } + } +} + + +void pa_raop_client_disconnect(pa_raop_client *c) { + c->is_recording = false; + + if (c->tcp_sfd >= 0) + pa_close(c->tcp_sfd); + c->tcp_sfd = -1; + + if (c->udp_sfd >= 0) + pa_close(c->udp_sfd); + c->udp_sfd = -1; + + /* Polling sockets will be closed by sink */ + c->udp_cfd = c->udp_tfd = -1; + c->tcp_sfd = -1; + + pa_log_error("RTSP control channel closed (disconnected)"); + + if (c->rtsp) + pa_rtsp_client_free(c->rtsp); + if (c->sid) + pa_xfree(c->sid); + c->rtsp = NULL; + c->sid = NULL; + + if (c->state_callback) + c->state_callback((int) PA_RAOP_DISCONNECTED, c->state_userdata); + +} + + +pa_raop_client* pa_raop_client_new(pa_core *core, const char *host, pa_raop_protocol_t protocol, + pa_raop_encryption_t encryption, pa_raop_codec_t codec, bool autoreconnect) { + pa_raop_client *c; + + pa_parsed_address a; + pa_sample_spec ss; + size_t size = 2; + + pa_assert(core); + pa_assert(host); + + if (pa_parse_address(host, &a) < 0) + return NULL; + + if (a.type == PA_PARSED_ADDRESS_UNIX) { + pa_xfree(a.path_or_host); + return NULL; + } + + c = pa_xnew0(pa_raop_client, 1); + c->core = core; + c->host = a.path_or_host; /* Will eventually be freed on destruction of c */ + if (a.port > 0) + c->port = a.port; + else + c->port = DEFAULT_RAOP_PORT; + c->rtsp = NULL; + c->sci = c->sid = NULL; + c->password = NULL; + c->autoreconnect = autoreconnect; + + c->protocol = protocol; + c->encryption = encryption; + c->codec = codec; + + c->tcp_sfd = -1; + + c->udp_sfd = -1; + c->udp_cfd = -1; + c->udp_tfd = -1; + + c->secret = NULL; + if (c->encryption != PA_RAOP_ENCRYPTION_NONE) + c->secret = pa_raop_secret_new(); + + ss = core->default_sample_spec; + if (c->protocol == PA_RAOP_PROTOCOL_UDP) + size = RTX_BUFFERING_SECONDS * ss.rate / FRAMES_PER_UDP_PACKET; + + c->is_recording = false; + c->is_first_packet = true; + /* Packet sync interval should be around 1s (UDP only) */ + c->sync_interval = ss.rate / FRAMES_PER_UDP_PACKET; + c->sync_count = 0; + + c->pbuf = pa_raop_packet_buffer_new(c->core->mempool, size); + + return c; +} + +void pa_raop_client_free(pa_raop_client *c) { + pa_assert(c); + + pa_raop_packet_buffer_free(c->pbuf); + + pa_xfree(c->sid); + pa_xfree(c->sci); + if (c->secret) + pa_raop_secret_free(c->secret); + pa_xfree(c->password); + c->sci = c->sid = NULL; + c->password = NULL; + c->secret = NULL; + + if (c->rtsp) + pa_rtsp_client_free(c->rtsp); + c->rtsp = NULL; + + pa_xfree(c->host); + pa_xfree(c); +} + +int pa_raop_client_authenticate (pa_raop_client *c, const char *password) { + int rv = 0; + + pa_assert(c); + + if (c->rtsp || c->password) { + pa_log_debug("Authentication/Connection already in progress..."); + return 0; + } + + c->password = NULL; + if (password) + c->password = pa_xstrdup(password); + c->rtsp = pa_rtsp_client_new(c->core->mainloop, c->host, c->port, DEFAULT_USER_AGENT, c->autoreconnect); + + pa_assert(c->rtsp); + + pa_rtsp_set_callback(c->rtsp, rtsp_auth_cb, c); + rv = pa_rtsp_connect(c->rtsp); + return rv; +} + +bool pa_raop_client_is_authenticated(pa_raop_client *c) { + pa_assert(c); + + return (c->sci != NULL); +} + +int pa_raop_client_announce(pa_raop_client *c) { + uint32_t sid; + int rv = 0; + + pa_assert(c); + + if (c->rtsp) { + pa_log_debug("Connection already in progress..."); + return 0; + } else if (!c->sci) { + pa_log_debug("ANNOUNCE requires a preliminary authentication"); + return 1; + } + + c->rtsp = pa_rtsp_client_new(c->core->mainloop, c->host, c->port, DEFAULT_USER_AGENT, c->autoreconnect); + + pa_assert(c->rtsp); + + c->sync_count = 0; + c->is_recording = false; + c->is_first_packet = true; + pa_random(&sid, sizeof(sid)); + c->sid = pa_sprintf_malloc("%u", sid); + pa_rtsp_set_callback(c->rtsp, rtsp_stream_cb, c); + + rv = pa_rtsp_connect(c->rtsp); + return rv; +} + +bool pa_raop_client_is_alive(pa_raop_client *c) { + pa_assert(c); + + if (!c->rtsp || !c->sci) { + pa_log_debug("Not alive, connection not established yet..."); + return false; + } + + switch (c->protocol) { + case PA_RAOP_PROTOCOL_TCP: + if (c->tcp_sfd >= 0) + return true; + break; + case PA_RAOP_PROTOCOL_UDP: + if (c->udp_sfd >= 0) + return true; + break; + default: + break; + } + + return false; +} + +bool pa_raop_client_can_stream(pa_raop_client *c) { + pa_assert(c); + + if (!c->rtsp || !c->sci) { + return false; + } + + switch (c->protocol) { + case PA_RAOP_PROTOCOL_TCP: + if (c->tcp_sfd >= 0 && c->is_recording) + return true; + break; + case PA_RAOP_PROTOCOL_UDP: + if (c->udp_sfd >= 0 && c->is_recording) + return true; + break; + default: + break; + } + + return false; +} + +bool pa_raop_client_is_recording(pa_raop_client *c) { + return c->is_recording; +} + +int pa_raop_client_stream(pa_raop_client *c) { + int rv = 0; + + pa_assert(c); + + if (!c->rtsp || !c->sci) { + pa_log_debug("Streaming's impossible, connection not established yet..."); + return 0; + } + + switch (c->protocol) { + case PA_RAOP_PROTOCOL_TCP: + if (c->tcp_sfd >= 0 && !c->is_recording) { + c->is_recording = true; + c->is_first_packet = true; + c->sync_count = 0; + } + break; + case PA_RAOP_PROTOCOL_UDP: + if (c->udp_sfd >= 0 && !c->is_recording) { + c->is_recording = true; + c->is_first_packet = true; + c->sync_count = 0; + } + break; + default: + rv = 1; + break; + } + + return rv; +} + +int pa_raop_client_set_volume(pa_raop_client *c, pa_volume_t volume) { + char *param; + int rv = 0; + double db; + + pa_assert(c); + + if (!c->rtsp) { + pa_log_debug("Cannot SET_PARAMETER, connection not established yet..."); + return 0; + } else if (!c->sci) { + pa_log_debug("SET_PARAMETER requires a preliminary authentication"); + return 1; + } + + db = pa_sw_volume_to_dB(volume); + if (db < VOLUME_MIN) + db = VOLUME_MIN; + else if (db > VOLUME_MAX) + db = VOLUME_MAX; + + pa_log_debug("volume=%u db=%.6f", volume, db); + + param = pa_sprintf_malloc("volume: %0.6f\r\n", db); + /* We just hit and hope, cannot wait for the callback. */ + if (c->rtsp != NULL && pa_rtsp_exec_ready(c->rtsp)) + rv = pa_rtsp_setparameter(c->rtsp, param); + + pa_xfree(param); + return rv; +} + +int pa_raop_client_flush(pa_raop_client *c) { + int rv = 0; + + pa_assert(c); + + if (!c->rtsp || !pa_rtsp_exec_ready(c->rtsp)) { + pa_log_debug("Cannot FLUSH, connection not established yet...)"); + return 0; + } else if (!c->sci) { + pa_log_debug("FLUSH requires a preliminary authentication"); + return 1; + } + + c->is_recording = false; + + rv = pa_rtsp_flush(c->rtsp, c->seq, c->rtptime); + return rv; +} + +int pa_raop_client_teardown(pa_raop_client *c) { + int rv = 0; + + pa_assert(c); + + if (!c->rtsp) { + pa_log_debug("Cannot TEARDOWN, connection not established yet..."); + return 0; + } else if (!c->sci) { + pa_log_debug("TEARDOWN requires a preliminary authentication"); + return 1; + } + + c->is_recording = false; + + rv = pa_rtsp_teardown(c->rtsp); + return rv; +} + +void pa_raop_client_get_frames_per_block(pa_raop_client *c, size_t *frames) { + pa_assert(c); + pa_assert(frames); + + switch (c->protocol) { + case PA_RAOP_PROTOCOL_TCP: + *frames = FRAMES_PER_TCP_PACKET; + break; + case PA_RAOP_PROTOCOL_UDP: + *frames = FRAMES_PER_UDP_PACKET; + break; + default: + *frames = 0; + break; + } +} + +bool pa_raop_client_register_pollfd(pa_raop_client *c, pa_rtpoll *poll, pa_rtpoll_item **poll_item) { + struct pollfd *pollfd = NULL; + pa_rtpoll_item *item = NULL; + bool oob = true; + + pa_assert(c); + pa_assert(poll); + pa_assert(poll_item); + + switch (c->protocol) { + case PA_RAOP_PROTOCOL_TCP: + item = pa_rtpoll_item_new(poll, PA_RTPOLL_NEVER, 1); + pollfd = pa_rtpoll_item_get_pollfd(item, NULL); + pollfd->fd = c->tcp_sfd; + pollfd->events = POLLOUT; + pollfd->revents = 0; + *poll_item = item; + oob = false; + break; + case PA_RAOP_PROTOCOL_UDP: + item = pa_rtpoll_item_new(poll, PA_RTPOLL_NEVER, 2); + pollfd = pa_rtpoll_item_get_pollfd(item, NULL); + pollfd->fd = c->udp_cfd; + pollfd->events = POLLIN | POLLPRI; + pollfd->revents = 0; + pollfd++; + pollfd->fd = c->udp_tfd; + pollfd->events = POLLIN | POLLPRI; + pollfd->revents = 0; + *poll_item = item; + oob = true; + break; + default: + *poll_item = NULL; + break; + } + + return oob; +} + +bool pa_raop_client_is_timing_fd(pa_raop_client *c, const int fd) { + return fd == c->udp_tfd; +} + +pa_volume_t pa_raop_client_adjust_volume(pa_raop_client *c, pa_volume_t volume) { + double minv, maxv; + + pa_assert(c); + + if (c->protocol != PA_RAOP_PROTOCOL_UDP) + return volume; + + maxv = pa_sw_volume_from_dB(0.0); + minv = maxv * pow(10.0, VOLUME_DEF / 60.0); + + /* Adjust volume so that it fits into VOLUME_DEF <= v <= 0 dB */ + return volume - volume * (minv / maxv) + minv; +} + +void pa_raop_client_handle_oob_packet(pa_raop_client *c, const int fd, const uint8_t packet[], ssize_t size) { + pa_assert(c); + pa_assert(fd >= 0); + pa_assert(packet); + + if (c->protocol == PA_RAOP_PROTOCOL_UDP) { + if (fd == c->udp_cfd) { + pa_log_debug("Received UDP control packet..."); + handle_udp_control_packet(c, packet, size); + } else if (fd == c->udp_tfd) { + pa_log_debug("Received UDP timing packet..."); + handle_udp_timing_packet(c, packet, size); + } + } +} + +ssize_t pa_raop_client_send_audio_packet(pa_raop_client *c, pa_memchunk *block, size_t offset) { + ssize_t written = 0; + + pa_assert(c); + pa_assert(block); + + /* Sync RTP & NTP timestamp if required (UDP). */ + if (c->protocol == PA_RAOP_PROTOCOL_UDP) { + c->sync_count++; + if (c->is_first_packet || c->sync_count >= c->sync_interval) { + send_udp_sync_packet(c, c->rtptime); + c->sync_count = 0; + } + } + + switch (c->protocol) { + case PA_RAOP_PROTOCOL_TCP: + written = send_tcp_audio_packet(c, block, offset); + break; + case PA_RAOP_PROTOCOL_UDP: + written = send_udp_audio_packet(c, block, offset); + break; + default: + written = -1; + break; + } + + c->is_first_packet = false; + return written; +} + +void pa_raop_client_set_state_callback(pa_raop_client *c, pa_raop_client_state_cb_t callback, void *userdata) { + pa_assert(c); + + c->state_callback = callback; + c->state_userdata = userdata; +} |