[cast] Add a simple delay to stream to a have little bit of sync

Since it is unknown how to do real sync on Chromecast, this commit instead adds
a primitive delay to the stream, so that it is at least somewhat closer to
Airplay/local audio.

Also some cleanup of unused stuff.
This commit is contained in:
ejurgensen 2019-08-23 19:22:33 +02:00
parent a5987addd4
commit 78373af201
2 changed files with 140 additions and 148 deletions

View File

@ -118,7 +118,7 @@ static cfg_opt_t sec_audio[] =
CFG_STR("mixer", NULL, CFGF_NONE),
CFG_STR("mixer_device", NULL, CFGF_NONE),
CFG_BOOL("sync_disable", cfg_false, CFGF_NONE),
CFG_INT("offset", 0, CFGF_NONE), // deprecated
CFG_INT("offset", 0, CFGF_DEPRECATED),
CFG_INT("offset_ms", 0, CFGF_NONE),
CFG_INT("adjust_period_seconds", 100, CFGF_NONE),
CFG_END()
@ -138,6 +138,7 @@ static cfg_opt_t sec_airplay[] =
static cfg_opt_t sec_chromecast[] =
{
CFG_BOOL("exclude", cfg_false, CFGF_NONE),
CFG_INT("offset_ms", 0, CFGF_NONE),
CFG_END()
};

View File

@ -96,6 +96,20 @@
#define CAST_QUALITY_BITS_PER_SAMPLE_DEFAULT 16
#define CAST_QUALITY_CHANNELS_DEFAULT 2
// This makes the rtp session buffer 4 seconds of audio (4 sec * 50 pkts/sec),
// which can be used for delayed transmission (and retransmission)
#define CAST_PACKET_BUFFER_SIZE 200
// Max (absolute) value the user is allowed to set offset_ms in the config file
#define CAST_OFFSET_MAX 1000
// This is just my measurement of how much extra delay is required to start at
// the same time as Airplay. The value was found experimentally.
#define CAST_DEVICE_START_DELAY_MS 100
// See cast_packet_header_make()
#define CAST_HEADER_SIZE 11
/* Notes
* OFFER/ANSWER <-webrtc
* RTCP/RTP
@ -117,10 +131,6 @@ union sockaddr_all
struct cast_session;
struct cast_msg_payload;
// See cast_packet_header_make()
#define CAST_HEADER_SIZE 11
#define CAST_PACKET_BUFFER_SIZE 1000
static struct encode_ctx *cast_encode_ctx;
static struct evbuffer *cast_encoded_data;
@ -128,12 +138,10 @@ typedef void (*cast_reply_cb)(struct cast_session *cs, struct cast_msg_payload *
// Session is starting up
#define CAST_STATE_F_STARTUP (1 << 13)
// The default receiver app is ready
// The receiver app is ready
#define CAST_STATE_F_MEDIA_CONNECTED (1 << 14)
// Media is loaded in the receiver app
#define CAST_STATE_F_MEDIA_LOADED (1 << 15)
// Media is playing in the receiver app
#define CAST_STATE_F_MEDIA_STREAMING (1 << 16)
#define CAST_STATE_F_MEDIA_STREAMING (1 << 15)
// Beware, the order of this enum has meaning
enum cast_state
@ -150,7 +158,9 @@ enum cast_state
CAST_STATE_MEDIA_LAUNCHED = CAST_STATE_F_STARTUP | 0x03,
// CONNECT, GET_STATUS and OFFER made to receiver app
CAST_STATE_MEDIA_CONNECTED = CAST_STATE_F_MEDIA_CONNECTED,
// After OFFER
// Buffering packets (playback not started yet)
CAST_STATE_MEDIA_BUFFERING = CAST_STATE_F_MEDIA_CONNECTED | 0x01,
// Streaming (playback started)
CAST_STATE_MEDIA_STREAMING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_STREAMING,
};
@ -166,11 +176,6 @@ struct cast_master_session
uint8_t *rawbuf;
size_t rawbuf_size;
int samples_per_packet;
// Number of samples that we tell the output to buffer (this will mean that
// the position that we send in the sync packages are offset by this amount
// compared to the rtptimes of the corresponding RTP packages we are sending)
int output_buffer_samples;
};
struct cast_session
@ -201,8 +206,10 @@ struct cast_session
uint32_t ssrc_id;
// IP address URL of forked-daapd's mp3 stream
char stream_url[128];
// For initial buffering (delay playback to achieve some sort of sync).
struct timespec start_pts;
struct timespec offset_ts;
uint16_t seqnum_next;
// Outgoing request which have the USE_REQUEST_ID flag get a new id, and a
// callback is registered. The callback is called when an incoming message
@ -515,78 +522,6 @@ cast_disconnect(int fd)
close(fd);
}
static int
stream_url_make(char *out, size_t len, const char *peer_addr, int family)
{
struct ifaddrs *ifap;
struct ifaddrs *ifa;
union sockaddr_all haddr;
union sockaddr_all hmask;
union sockaddr_all paddr;
char host_addr[64];
unsigned short port;
int found;
int ret;
if (family == AF_INET)
ret = inet_pton(AF_INET, peer_addr, &paddr.sin.sin_addr);
else
ret = inet_pton(AF_INET6, peer_addr, &paddr.sin6.sin6_addr);
if (ret != 1)
return -1;
found = 0;
ret = getifaddrs(&ifap);
if (ret < 0)
{
DPRINTF(E_LOG, L_CAST, "Could not get interface address: %s\n", strerror(errno));
return -1;
}
for (ifa = ifap; !found && ifa; ifa = ifa->ifa_next)
{
if (!ifa->ifa_addr)
{
DPRINTF(E_LOG, L_CAST, "Skipping null address from getifaddrs()\n");
continue;
}
if (ifa->ifa_addr->sa_family != family)
continue;
if (family == AF_INET)
{
memcpy(&haddr.sin, ifa->ifa_addr, sizeof(struct sockaddr_in));
memcpy(&hmask.sin, ifa->ifa_netmask, sizeof(struct sockaddr_in));
found = ((haddr.sin.sin_addr.s_addr & hmask.sin.sin_addr.s_addr) ==
(paddr.sin.sin_addr.s_addr & hmask.sin.sin_addr.s_addr));
if (found)
inet_ntop(family, &haddr.sin.sin_addr, host_addr, sizeof(host_addr));
}
else if (family == AF_INET6)
{
memcpy(&haddr.sin6, ifa->ifa_addr, sizeof(struct sockaddr_in6));
found = (memcmp(&haddr.sin6.sin6_addr.s6_addr, &paddr.sin6.sin6_addr.s6_addr, 8) == 0);
if (found)
inet_ntop(family, &haddr.sin6.sin6_addr, host_addr, sizeof(host_addr));
}
}
freeifaddrs(ifap);
if (!found)
return -1;
port = cfg_getint(cfg_getsec(cfg, "library"), "port");
if (family == AF_INET)
snprintf(out, len, "http://%s:%d/stream.mp3", host_addr, port);
else
snprintf(out, len, "http://[%s]:%d/stream.mp3", host_addr, port);
return 0;
}
static char *
squote_to_dquote(char *buf)
{
@ -740,10 +675,6 @@ cast_msg_send(struct cast_session *cs, enum cast_msg_types type, cast_reply_cb r
snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->request_id, cs->ssrc_id);
else if (type == PRESENTATION)
snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->session_id, cs->request_id);
else if (type == MEDIA_LOAD)
snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->stream_url, cs->session_id, cs->request_id);
else if ((type == MEDIA_PLAY) || (type == MEDIA_PAUSE) || (type == MEDIA_STOP))
snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->media_session_id, cs->session_id, cs->request_id);
else if (type == SET_VOLUME)
snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->volume, cs->request_id);
else
@ -1005,7 +936,7 @@ cast_status(struct cast_session *cs)
case CAST_STATE_DISCONNECTED ... CAST_STATE_MEDIA_LAUNCHED:
state = OUTPUT_STATE_STARTUP;
break;
case CAST_STATE_MEDIA_CONNECTED:
case CAST_STATE_MEDIA_CONNECTED ... CAST_STATE_MEDIA_BUFFERING:
state = OUTPUT_STATE_CONNECTED;
break;
case CAST_STATE_MEDIA_STREAMING:
@ -1513,7 +1444,6 @@ master_session_make(struct media_quality *quality)
cms->quality = *quality;
cms->samples_per_packet = CAST_SAMPLES_PER_PACKET;
cms->rawbuf_size = STOB(cms->samples_per_packet, quality->bits_per_sample, quality->channels);
cms->output_buffer_samples = OUTPUTS_BUFFER_DURATION * quality->sample_rate;
CHECK_NULL(L_CAST, cms->rawbuf = malloc(cms->rawbuf_size));
CHECK_NULL(L_CAST, cms->evbuf = evbuffer_new());
@ -1527,10 +1457,12 @@ static struct cast_session *
cast_session_make(struct output_device *device, int family, int callback_id)
{
struct cast_session *cs;
cfg_t *chromecast;
const char *proto;
const char *err;
char *address;
unsigned short port;
uint64_t offset_ms;
int flags;
int ret;
@ -1588,13 +1520,22 @@ cast_session_make(struct output_device *device, int family, int callback_id)
goto out_deinit_gnutls;
}
ret = stream_url_make(cs->stream_url, sizeof(cs->stream_url), address, family);
if (ret < 0)
chromecast = cfg_gettsec(cfg, "chromecast", device->name);
offset_ms = chromecast ? cfg_getint(chromecast, "offset_ms") : 0;
if (abs(offset_ms) > CAST_OFFSET_MAX)
{
DPRINTF(E_LOG, L_CAST, "Bug! Could find a network interface on same subnet as %s\n", device->name);
goto out_close_connection;
DPRINTF(E_LOG, L_CAST, "Ignoring invalid configuration of Chromecast offset (%" PRIu64 " ms)\n", offset_ms);
offset_ms = 0;
}
offset_ms += OUTPUTS_BUFFER_DURATION * 1000 + CAST_DEVICE_START_DELAY_MS;
cs->offset_ts.tv_sec = (offset_ms / 1000);
cs->offset_ts.tv_nsec = (offset_ms % 1000) * 1000000UL;
DPRINTF(E_LOG, L_CAST, "Offset is set to %lu:%lu\n", cs->offset_ts.tv_sec, cs->offset_ts.tv_nsec);
cs->ev = event_new(evbase_player, cs->server_fd, EV_READ | EV_PERSIST, cast_listen_cb, cs);
if (!cs->ev)
{
@ -1686,6 +1627,7 @@ cast_session_shutdown(struct cast_session *cs, enum cast_state wanted_state)
pending = 1;
break;
case CAST_STATE_MEDIA_BUFFERING:
case CAST_STATE_MEDIA_CONNECTED:
cast_disconnect(cs->udp_fd);
cs->udp_fd = -1;
@ -1834,7 +1776,7 @@ packet_send(struct cast_session *cs, struct rtp_packet *pkt)
return 0;
}
/* DPRINTF(E_DBG, L_PLAYER, "RTP PACKET seqnum %u, rtptime %u, payload 0x%x, pktbuf_s %zu\n",
/* DPRINTF(E_DBG, L_CAST, "RTP PACKET seqnum %u, rtptime %u, payload 0x%x, pktbuf_s %zu\n",
cs->master_session->rtp_session->seqnum,
cs->master_session->rtp_session->pos,
pkt->header[1],
@ -1844,12 +1786,34 @@ packet_send(struct cast_session *cs, struct rtp_packet *pkt)
return 0;
}
static int
packets_send(struct cast_master_session *cms)
static inline int
packets_send(struct cast_session *cs, struct rtp_session *rtp_session)
{
struct rtp_packet *pkt;
int ret;
// Note that the loop must work even though seqnum wraps around, so we use !=, not <
for (; cs->seqnum_next != rtp_session->seqnum; cs->seqnum_next++)
{
pkt = rtp_packet_get(rtp_session, cs->seqnum_next);
if (!pkt)
{
DPRINTF(E_WARN, L_CAST, "Packet to '%s' is missing in our buffer\n", cs->devname);
return 0; // Don't fail session over a missing packet (or should we?)
}
ret = packet_send(cs, pkt);
if (ret < 0)
return -1;
}
return 0;
}
static int
packet_make(struct cast_master_session *cms)
{
struct rtp_packet *pkt;
struct cast_session *cs;
struct cast_session *next;
int len;
int ret;
@ -1866,28 +1830,38 @@ packets_send(struct cast_master_session *cms)
if (ret < 0)
return -1;
for (cs = cast_sessions; cs; cs = next)
{
next = cs->next;
if (cs->master_session != cms || !(cs->state & CAST_STATE_F_MEDIA_CONNECTED))
continue;
ret = packet_send(cs, pkt);
if (ret < 0)
{
// Downgrade state immediately to avoid further write attempts
cs->state = CAST_STATE_MEDIA_LAUNCHED;
cast_session_shutdown(cs, CAST_STATE_FAILED);
}
}
// Commits packet to retransmit buffer, and prepares the session for the next packet
rtp_packet_commit(cms->rtp_session, pkt);
return 0;
}
static inline int
packets_make(struct cast_master_session *cms, struct output_data *odata)
{
int ret;
int npkts;
// TODO avoid this copy
evbuffer_add(cms->evbuf, odata->buffer, odata->bufsize);
cms->evbuf_samples += odata->samples;
// Make as many packets as we have data for (one packet requires rawbuf_size bytes)
npkts = 0;
while (evbuffer_get_length(cms->evbuf) >= cms->rawbuf_size)
{
evbuffer_remove(cms->evbuf, cms->rawbuf, cms->rawbuf_size);
cms->evbuf_samples -= cms->samples_per_packet;
ret = packet_make(cms);
if (ret == 0)
npkts++;
}
return npkts;
}
/* TODO This does not currently work - need to investigate what sync the devices support
static void
packets_sync_send(struct cast_master_session *cms, struct timespec pts)
@ -1920,7 +1894,7 @@ packets_sync_send(struct cast_master_session *cms, struct timespec pts)
sync_pkt = rtp_sync_packet_next(cms->rtp_session, &cur_stamp, 0x80);
packet_send(cs, sync_pkt);
DPRINTF(E_DBG, L_PLAYER, "Start sync packet sent to '%s': cur_pos=%" PRIu32 ", cur_ts=%lu:%lu, now=%lu:%lu, rtptime=%" PRIu32 ",\n",
DPRINTF(E_DBG, L_CAST, "Start sync packet sent to '%s': cur_pos=%" PRIu32 ", cur_ts=%lu:%lu, now=%lu:%lu, rtptime=%" PRIu32 ",\n",
cs->devname, cur_stamp.pos, cur_stamp.ts.tv_sec, cur_stamp.ts.tv_nsec, ts.tv_sec, ts.tv_nsec, cms->rtp_session->pos);
}
else if (is_sync_time && cs->state == CAST_STATE_MEDIA_STREAMING)
@ -2045,46 +2019,63 @@ cast_device_volume_set(struct output_device *device, int callback_id)
static void
cast_write(struct output_buffer *obuf)
{
struct cast_master_session *cms;
struct cast_session *cs;
struct cast_session *next;
struct timespec ts;
int i;
int ret;
if (!cast_sessions)
return;
cms = cast_master_session;
for (i = 0; obuf->data[i].buffer; i++)
{
if (!quality_is_equal(&obuf->data[i].quality, &cast_quality_default))
continue;
// Sends sync packets to new sessions, and if it is sync time then also to old sessions
// packets_sync_send(cms, obuf->pts);
// TODO avoid this copy
evbuffer_add(cms->evbuf, obuf->data[i].buffer, obuf->data[i].bufsize);
cms->evbuf_samples += obuf->data[i].samples;
// Send as many packets as we have data for (one packet requires rawbuf_size bytes)
while (evbuffer_get_length(cms->evbuf) >= cms->rawbuf_size)
{
evbuffer_remove(cms->evbuf, cms->rawbuf, cms->rawbuf_size);
cms->evbuf_samples -= cms->samples_per_packet;
packets_send(cms);
}
if (quality_is_equal(&obuf->data[i].quality, &cast_quality_default))
break;
}
// Check for devices that have joined since last write (we have already sent them
// initialization sync and rtp packets via packets_sync_send and packets_send)
for (cs = cast_sessions; cs; cs = cs->next)
if (!obuf->data[i].buffer)
{
if (cs->state != CAST_STATE_MEDIA_CONNECTED)
DPRINTF(E_LOG, L_CAST, "Bug! Output not delivering required data quality\n");
return;
}
// Converts the raw audio in the output_buffer to Chromecast packets
packets_make(cast_master_session, &obuf->data[i]);
for (cs = cast_sessions; cs; cs = next)
{
next = cs->next;
if (!(cs->state & CAST_STATE_F_MEDIA_CONNECTED))
continue;
if (cs->state == CAST_STATE_MEDIA_CONNECTED)
{
// Sets that playback will start at time = start_pts with the packet that comes after seqnum_last
cs->start_pts = timespec_add(obuf->pts, cs->offset_ts);
cs->seqnum_next = cast_master_session->rtp_session->seqnum;
cs->state = CAST_STATE_MEDIA_BUFFERING;
clock_gettime(CLOCK_MONOTONIC, &ts);
DPRINTF(E_DBG, L_CAST, "Start time is %lu:%lu, current time is %lu:%lu\n", cs->start_pts.tv_sec, cs->start_pts.tv_nsec, ts.tv_sec, ts.tv_nsec);
}
if (cs->state == CAST_STATE_MEDIA_BUFFERING)
{
clock_gettime(CLOCK_MONOTONIC, &ts);
if (timespec_cmp(cs->start_pts, ts) > 0)
continue; // Keep buffering
cs->state = CAST_STATE_MEDIA_STREAMING;
// Make a cb?
}
ret = packets_send(cs, cast_master_session->rtp_session);
if (ret < 0)
{
// Downgrade state immediately to avoid further write attempts (session shutdown is async)
cs->state = CAST_STATE_MEDIA_LAUNCHED;
cast_session_shutdown(cs, CAST_STATE_FAILED);
}
}
}