mirror of
https://gitlab.freedesktop.org/pulseaudio/pulseaudio.git
synced 2025-11-10 13:29:58 -05:00
1739 lines
50 KiB
C
1739 lines
50 KiB
C
/***
|
|
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 <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 "raop-client.h"
|
|
#include "raop-packet-buffer.h"
|
|
#include "raop-crypto.h"
|
|
#include "raop-util.h"
|
|
|
|
#include "rtsp_client.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
|
|
|
|
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
|
|
};
|
|
|
|
/* Timming 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_timming_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 definately 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;
|
|
|
|
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_get(c->pbuf, c->seq, max)))
|
|
return -1;
|
|
|
|
if (packet->length <= 0) {
|
|
pa_assert(block->index == offset);
|
|
|
|
if (!(packet = pa_raop_packet_buffer_get(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;
|
|
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_get(c->pbuf, c->seq, max)))
|
|
return -1;
|
|
|
|
packet->length = max;
|
|
packet->index = 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 || errno == EWOULDBLOCK)) {
|
|
pa_log_debug("Discarding UDP (audio, seq=%d) packet due to EAGAIN (%s)", c->seq, pa_cstrerror(errno));
|
|
return (ssize_t) 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_get(c->pbuf, seq + i, 0)))
|
|
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 || errno == EWOULDBLOCK)) {
|
|
pa_log_debug("Discarding UDP (audio-restransmitted, seq=%d) packet due to EAGAIN", seq + i);
|
|
continue;
|
|
}
|
|
|
|
total += written;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
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 transmited 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);
|
|
|
|
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]);
|
|
nbp = ntohs((uint16_t) packet[6]);
|
|
if (nbp <= 0)
|
|
return 1;
|
|
|
|
/* The market 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;
|
|
}
|
|
|
|
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_timming_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_timming_header, sizeof(udp_timming_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);
|
|
|
|
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_timming_header));
|
|
|
|
/* The market 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);
|
|
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);
|
|
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 {
|
|
*sa_port = htons(port);
|
|
|
|
if (bind(fd, sa, salen) < 0 && errno != EADDRINUSE) {
|
|
pa_log("bind_socket() failed: %s", pa_cstrerror(errno));
|
|
goto fail;
|
|
}
|
|
break;
|
|
} while (++port > 0);
|
|
|
|
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;
|
|
|
|
pa_log_debug("RAOP: CONNECTED");
|
|
|
|
ip = pa_rtsp_localip(c->rtsp);
|
|
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 IP4 %s\r\n"
|
|
"s=iTunes\r\n"
|
|
"c=IN IP4 %s\r\n"
|
|
"t=0 0\r\n"
|
|
"m=audio 0 RTP/AVP 96\r\n"
|
|
"a=rtpmap:96 AppleLossless\r\n"
|
|
"a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n",
|
|
c->sid, ip, 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);
|
|
iv = pa_raop_secret_get_iv(c->secret);
|
|
|
|
sdp = pa_sprintf_malloc(
|
|
"v=0\r\n"
|
|
"o=iTunes %s 0 IN IP4 %s\r\n"
|
|
"s=iTunes\r\n"
|
|
"c=IN IP4 %s\r\n"
|
|
"t=0 0\r\n"
|
|
"m=audio 0 RTP/AVP 96\r\n"
|
|
"a=rtpmap:96 AppleLossless\r\n"
|
|
"a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
|
|
"a=rsaaeskey:%s\r\n"
|
|
"a=aesiv:%s\r\n",
|
|
c->sid, ip, c->host, frames, key, iv);
|
|
|
|
pa_xfree(key);
|
|
pa_xfree(iv);
|
|
break;
|
|
}
|
|
}
|
|
|
|
pa_rtsp_announce(c->rtsp, sdp);
|
|
|
|
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, *trs, *token, *pc;
|
|
const char *token_state = NULL;
|
|
char delimiters[] = ";";
|
|
|
|
pa_log_debug("RAOP: SETUP");
|
|
|
|
ajs = pa_xstrdup(pa_headerlist_gets(headers, "Audio-Jack-Status"));
|
|
trs = pa_xstrdup(pa_headerlist_gets(headers, "Transport"));
|
|
|
|
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) {
|
|
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"))
|
|
pa_atou(pc + 1, &cport);
|
|
if (pa_streq(token, "timing_port"))
|
|
pa_atou(pc + 1, &tport);
|
|
*pc = '=';
|
|
}
|
|
pa_xfree(token);
|
|
}
|
|
} 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(trs);
|
|
pa_xfree(ajs);
|
|
break;
|
|
|
|
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)
|
|
pa_atoi(alt, &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");
|
|
|
|
c->is_recording = false;
|
|
|
|
break;
|
|
}
|
|
|
|
case STATE_TEARDOWN: {
|
|
pa_log_debug("RAOP: TEARDOWN");
|
|
|
|
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_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;
|
|
uint16_t rac;
|
|
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, sizeof(rac));
|
|
/* Generate a random Apple-Challenge key */
|
|
pa_raop_base64_encode(&rac, 8 * sizeof(rac), &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, *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))) {
|
|
val = NULL;
|
|
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);
|
|
val = NULL;
|
|
}
|
|
|
|
pa_xfree(token);
|
|
}
|
|
|
|
if (pa_safe_streq(mth, "Basic")) {
|
|
rtrim_char(realm, '\"');
|
|
|
|
pa_raop_basic_response(DEFAULT_USER_NAME, c->password, &response);
|
|
ath = pa_sprintf_malloc("Basic %s",
|
|
response);
|
|
|
|
pa_xfree(response);
|
|
pa_xfree(realm);
|
|
} else if (pa_safe_streq(mth, "Digest")) {
|
|
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);
|
|
|
|
pa_xfree(response);
|
|
pa_xfree(realm);
|
|
pa_xfree(nonce);
|
|
} else {
|
|
pa_log_error("unsupported authentication method: %s", mth);
|
|
pa_xfree(wath);
|
|
pa_xfree(mth);
|
|
goto error;
|
|
}
|
|
|
|
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 = pa_xstrdup(a.path_or_host);
|
|
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_log_debug("Cannot FLUSH, connection not established yet...)");
|
|
return 0;
|
|
} else if (!c->sci) {
|
|
pa_log_debug("FLUSH requires a preliminary authentication");
|
|
return 1;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|