/*** 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 . ***/ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #ifdef HAVE_SYS_FILIO_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; }