mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-14 16:25:03 -05:00
[chromecast] Fix host address, some more commands, add flush timer
This commit is contained in:
parent
5049426573
commit
7d6c670fee
@ -180,7 +180,6 @@ outputs_write(uint8_t *buf, uint64_t rtptime)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO output of this makes little sense
|
||||
int
|
||||
outputs_flush(output_status_cb cb, uint64_t rtptime)
|
||||
{
|
||||
@ -194,7 +193,7 @@ outputs_flush(output_status_cb cb, uint64_t rtptime)
|
||||
continue;
|
||||
|
||||
if (outputs[i]->flush)
|
||||
ret = outputs[i]->flush(cb, rtptime);
|
||||
ret += outputs[i]->flush(cb, rtptime);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -167,7 +167,7 @@ struct output_definition
|
||||
// Write stream data to the output devices
|
||||
void (*write)(uint8_t *buf, uint64_t rtptime);
|
||||
|
||||
// Flush all sessions
|
||||
// Flush all sessions, the return must be number of sessions pending the flush
|
||||
int (*flush)(output_status_cb cb, uint64_t rtptime);
|
||||
|
||||
// Change the call back associated with a session
|
||||
|
@ -29,6 +29,7 @@
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <endian.h>
|
||||
@ -90,6 +91,9 @@ struct cast_session
|
||||
|
||||
float volume;
|
||||
|
||||
// IP address URL of forked-daapd's mp3 stream
|
||||
char stream_url[128];
|
||||
|
||||
// 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
|
||||
// from the peer with that request id arrives.
|
||||
@ -99,6 +103,7 @@ struct cast_session
|
||||
// Session info from the ChromeCast
|
||||
char *transport_id;
|
||||
char *session_id;
|
||||
int media_session_id;
|
||||
|
||||
/* Do not dereference - only passed to the status cb */
|
||||
struct output_device *device;
|
||||
@ -122,6 +127,8 @@ enum cast_msg_types
|
||||
MEDIA_GET_STATUS,
|
||||
MEDIA_STATUS,
|
||||
MEDIA_LOAD,
|
||||
MEDIA_PLAY,
|
||||
MEDIA_PAUSE,
|
||||
MEDIA_STOP,
|
||||
MEDIA_LOAD_FAILED,
|
||||
SET_VOLUME,
|
||||
@ -143,6 +150,7 @@ struct cast_msg_payload
|
||||
int request_id;
|
||||
const char *session_id;
|
||||
const char *transport_id;
|
||||
int media_session_id;
|
||||
};
|
||||
|
||||
// Array of the cast messages that we use. Must be in sync with cast_msg_types.
|
||||
@ -215,10 +223,22 @@ struct cast_msg_basic cast_msg[] =
|
||||
.payload = "{'currentTime':0,'media':{'contentId':'%s','streamType':'LIVE','contentType':'audio/mp3'},'customData':{},'sessionId':'%s','requestId':%d,'type':'LOAD','autoplay':1}",
|
||||
.flags = USE_TRANSPORT_ID | USE_REQUEST_ID,
|
||||
},
|
||||
{
|
||||
.type = MEDIA_PLAY,
|
||||
.namespace = NS_MEDIA,
|
||||
.payload = "{'mediaSessionId':%d,'sessionId':'%s','type':'PLAY','requestId':%d}",
|
||||
.flags = USE_TRANSPORT_ID | USE_REQUEST_ID,
|
||||
},
|
||||
{
|
||||
.type = MEDIA_PAUSE,
|
||||
.namespace = NS_MEDIA,
|
||||
.payload = "{'mediaSessionId':%d,'sessionId':'%s','type':'PAUSE','requestId':%d}",
|
||||
.flags = USE_TRANSPORT_ID | USE_REQUEST_ID,
|
||||
},
|
||||
{
|
||||
.type = MEDIA_STOP,
|
||||
.namespace = NS_RECEIVER,
|
||||
.payload = "{'mediaSessionId':1,'sessionId':'%s','type':'STOP','requestId':%d}",
|
||||
.namespace = NS_MEDIA,
|
||||
.payload = "{'mediaSessionId':%d,'sessionId':'%s','type':'STOP','requestId':%d}",
|
||||
.flags = USE_TRANSPORT_ID | USE_REQUEST_ID,
|
||||
},
|
||||
{
|
||||
@ -242,8 +262,7 @@ extern struct event_base *evbase_player;
|
||||
/* Globals */
|
||||
static gnutls_certificate_credentials_t tls_credentials;
|
||||
static struct cast_session *sessions;
|
||||
|
||||
static char cast_stream_url[1024];
|
||||
static struct event *flush_timer;
|
||||
|
||||
|
||||
static int
|
||||
@ -311,6 +330,63 @@ tcp_close(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[128];
|
||||
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;
|
||||
getifaddrs(&ifap);
|
||||
for (ifa = ifap; !found && ifa; ifa = ifa->ifa_next)
|
||||
{
|
||||
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");
|
||||
snprintf(out, len, "http://%s:%d/stream.mp3", host_addr, port);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *
|
||||
squote_to_dquote(char *buf)
|
||||
{
|
||||
@ -403,9 +479,9 @@ cast_msg_send(struct cast_session *cs, enum cast_msg_types type, cast_reply_cb r
|
||||
if (cast_msg[type].flags & USE_REQUEST_ID_ONLY)
|
||||
snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->request_id);
|
||||
else if (type == MEDIA_LOAD)
|
||||
snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, TEST_STREAM_URL, cs->session_id, cs->request_id);
|
||||
else if (type == MEDIA_STOP)
|
||||
snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->session_id, cs->request_id);
|
||||
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
|
||||
@ -479,10 +555,17 @@ cast_msg_parse(struct cast_msg_payload *payload, char *s)
|
||||
payload->request_id = json_object_get_int(needle);
|
||||
|
||||
// Might be done now
|
||||
if (payload->type != RECEIVER_STATUS)
|
||||
if ((payload->type != RECEIVER_STATUS) && (payload->type != MEDIA_STATUS))
|
||||
return haystack;
|
||||
|
||||
// Isn't this marvelous
|
||||
if ( json_object_object_get_ex(haystack, "status", &needle) &&
|
||||
(json_object_get_type(needle) == json_type_array) &&
|
||||
(somehay = json_object_array_get_idx(needle, 0)) &&
|
||||
json_object_object_get_ex(somehay, "mediaSessionId", &needle) &&
|
||||
(json_object_get_type(needle) == json_type_int) )
|
||||
payload->media_session_id = json_object_get_int(needle);
|
||||
|
||||
if ( ! (json_object_object_get_ex(haystack, "status", &somehay) &&
|
||||
json_object_object_get_ex(somehay, "applications", &needle) &&
|
||||
(json_object_get_type(needle) == json_type_array) &&
|
||||
@ -706,9 +789,21 @@ cast_cb_startup_connect(struct cast_session *cs, struct cast_msg_payload *payloa
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Not really a cb right now
|
||||
static void
|
||||
cast_cb_close(struct cast_session *cs, struct cast_msg_payload *payload)
|
||||
{
|
||||
output_status_cb status_cb;
|
||||
|
||||
cs->state = OUTPUT_STATE_STOPPED;
|
||||
|
||||
/* Session shut down, tell our user */
|
||||
status_cb = cs->status_cb;
|
||||
cs->status_cb = NULL;
|
||||
|
||||
status_cb(cs->device, cs->output_session, cs->state);
|
||||
|
||||
cast_session_cleanup(cs);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -722,6 +817,11 @@ cast_cb_load(struct cast_session *cs, struct cast_msg_payload *payload)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload->media_session_id)
|
||||
DPRINTF(E_LOG, L_CAST, "Did not get a media session id?\n");
|
||||
else
|
||||
cs->media_session_id = payload->media_session_id;
|
||||
|
||||
cs->state = OUTPUT_STATE_STREAMING;
|
||||
|
||||
DPRINTF(E_DBG, L_CAST, "Media loaded\n");
|
||||
@ -729,6 +829,19 @@ cast_cb_load(struct cast_session *cs, struct cast_msg_payload *payload)
|
||||
cs->status_cb(cs->device, cs->output_session, cs->state);
|
||||
}
|
||||
|
||||
static void
|
||||
cast_cb_stop(struct cast_session *cs, struct cast_msg_payload *payload)
|
||||
{
|
||||
output_status_cb status_cb;
|
||||
|
||||
cs->state = OUTPUT_STATE_CONNECTED;
|
||||
|
||||
status_cb = cs->status_cb;
|
||||
cs->status_cb = NULL;
|
||||
status_cb(cs->device, cs->output_session, cs->state);
|
||||
//TODO
|
||||
}
|
||||
|
||||
static void
|
||||
cast_cb_volume(struct cast_session *cs, struct cast_msg_payload *payload)
|
||||
{
|
||||
@ -739,6 +852,21 @@ cast_cb_volume(struct cast_session *cs, struct cast_msg_payload *payload)
|
||||
status_cb(cs->device, cs->output_session, cs->state);
|
||||
}
|
||||
|
||||
static void
|
||||
cast_cb_flush(struct cast_session *cs, struct cast_msg_payload *payload)
|
||||
{
|
||||
output_status_cb status_cb;
|
||||
|
||||
DPRINTF(E_DBG, L_CAST, "Cast CB flush called\n");
|
||||
|
||||
cs->state = OUTPUT_STATE_CONNECTED;
|
||||
|
||||
/* Let our user know */
|
||||
status_cb = cs->status_cb;
|
||||
cs->status_cb = NULL;
|
||||
status_cb(cs->device, cs->output_session, cs->state);
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void
|
||||
@ -852,7 +980,17 @@ cast_session_make(struct output_device *device, int family, output_status_cb cb)
|
||||
|
||||
cs->server_fd = tcp_connect(address, port, family);
|
||||
if (cs->server_fd < 0)
|
||||
goto out_deinit_gnutls;
|
||||
{
|
||||
DPRINTF(E_LOG, L_CAST, "Could not connect to %s\n", device->name);
|
||||
goto out_deinit_gnutls;
|
||||
}
|
||||
|
||||
ret = stream_url_make(cs->stream_url, sizeof(cs->stream_url), address, family);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_CAST, "Bug! Could find a network interface on same subnet as %s\n", device->name);
|
||||
goto out_close_connection;
|
||||
}
|
||||
|
||||
// TODO Add a timeout to detect connection problems
|
||||
cs->ev = event_new(evbase_player, cs->server_fd, EV_READ | EV_PERSIST, cast_listen_cb, cs);
|
||||
@ -1028,9 +1166,13 @@ cast_device_stop(struct output_session *session)
|
||||
struct cast_session *cs = session->session;
|
||||
|
||||
if (!(cs->state & OUTPUT_STATE_F_CONNECTED))
|
||||
cast_session_cleanup(cs);
|
||||
else
|
||||
cast_msg_send(cs, CLOSE, cast_cb_close);
|
||||
{
|
||||
cast_session_cleanup(cs);
|
||||
return;
|
||||
}
|
||||
|
||||
cast_msg_send(cs, CLOSE, NULL);
|
||||
cast_cb_close(cs, NULL);
|
||||
}
|
||||
|
||||
static int
|
||||
@ -1066,6 +1208,9 @@ cast_playback_start(uint64_t next_pkt, struct timespec *ts)
|
||||
{
|
||||
struct cast_session *cs;
|
||||
|
||||
if (evtimer_pending(flush_timer, NULL))
|
||||
event_del(flush_timer);
|
||||
|
||||
for (cs = sessions; cs; cs = cs->next)
|
||||
{
|
||||
if (cs->state == OUTPUT_STATE_CONNECTED)
|
||||
@ -1073,6 +1218,66 @@ cast_playback_start(uint64_t next_pkt, struct timespec *ts)
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
cast_playback_stop(void)
|
||||
{
|
||||
struct cast_session *cs;
|
||||
int ret;
|
||||
|
||||
for (cs = sessions; cs; cs = cs->next)
|
||||
{
|
||||
ret = cast_msg_send(cs, MEDIA_STOP, cast_cb_stop);
|
||||
if (ret < 0)
|
||||
DPRINTF(E_LOG, L_CAST, "MEDIA_STOP request failed!\n");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
cast_flush_timer_cb(int fd, short what, void *arg)
|
||||
{
|
||||
DPRINTF(E_DBG, L_CAST, "Flush timer expired; tearing down all sessions\n");
|
||||
|
||||
cast_playback_stop();
|
||||
}
|
||||
|
||||
static int
|
||||
cast_flush(output_status_cb cb, uint64_t rtptime)
|
||||
{
|
||||
struct cast_session *cs;
|
||||
struct cast_session *next;
|
||||
struct timeval tv;
|
||||
int pending;
|
||||
int ret;
|
||||
|
||||
pending = 0;
|
||||
for (cs = sessions; cs; cs = next)
|
||||
{
|
||||
next = cs->next;
|
||||
|
||||
if (cs->state != OUTPUT_STATE_STREAMING)
|
||||
continue;
|
||||
|
||||
ret = cast_msg_send(cs, MEDIA_PAUSE, cast_cb_flush);
|
||||
if (ret < 0)
|
||||
{
|
||||
cast_session_failure(cs);
|
||||
continue;
|
||||
}
|
||||
|
||||
cs->status_cb = cb;
|
||||
pending++;
|
||||
}
|
||||
|
||||
if (pending > 0)
|
||||
{
|
||||
evutil_timerclear(&tv);
|
||||
tv.tv_sec = 10;
|
||||
evtimer_add(flush_timer, &tv);
|
||||
}
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
static void
|
||||
cast_set_status_cb(struct output_session *session, output_status_cb cb)
|
||||
{
|
||||
@ -1081,12 +1286,9 @@ cast_set_status_cb(struct output_session *session, output_status_cb cb)
|
||||
cs->status_cb = cb;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
cast_init(void)
|
||||
{
|
||||
char hostname[256];
|
||||
int port;
|
||||
int mdns_flags;
|
||||
int i;
|
||||
int ret;
|
||||
@ -1101,16 +1303,6 @@ cast_init(void)
|
||||
}
|
||||
}
|
||||
|
||||
ret = gethostname(hostname, sizeof(hostname));
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_CAST, "Could not determine own hostname: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
port = cfg_getint(cfg_getsec(cfg, "library"), "port");
|
||||
|
||||
snprintf(cast_stream_url, sizeof(cast_stream_url), "http://%s:%d/stream.mp3", hostname, port);
|
||||
|
||||
// TODO Setting the cert file may not be required
|
||||
if ( ((ret = gnutls_global_init()) != GNUTLS_E_SUCCESS) ||
|
||||
((ret = gnutls_certificate_allocate_credentials(&tls_credentials)) != GNUTLS_E_SUCCESS) ||
|
||||
@ -1120,17 +1312,26 @@ cast_init(void)
|
||||
return -1;
|
||||
}
|
||||
|
||||
flush_timer = evtimer_new(evbase_player, cast_flush_timer_cb, NULL);
|
||||
if (!flush_timer)
|
||||
{
|
||||
DPRINTF(E_LOG, L_CAST, "Out of memory for flush timer\n");
|
||||
goto out_tls_deinit;
|
||||
}
|
||||
|
||||
mdns_flags = MDNS_WANT_V4 | MDNS_WANT_V6 | MDNS_WANT_V6LL;
|
||||
|
||||
ret = mdns_browse("_googlecast._tcp", mdns_flags, cast_device_cb);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Could not add mDNS browser for Chromecast devices\n");
|
||||
goto out_tls_deinit;
|
||||
goto out_free_flush_timer;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
out_free_flush_timer:
|
||||
event_free(flush_timer);
|
||||
out_tls_deinit:
|
||||
gnutls_certificate_free_credentials(tls_credentials);
|
||||
gnutls_global_deinit();
|
||||
@ -1149,6 +1350,8 @@ cast_deinit(void)
|
||||
cast_session_free(cs);
|
||||
}
|
||||
|
||||
event_free(flush_timer);
|
||||
|
||||
gnutls_certificate_free_credentials(tls_credentials);
|
||||
gnutls_global_deinit();
|
||||
}
|
||||
@ -1163,13 +1366,13 @@ struct output_definition output_cast =
|
||||
.deinit = cast_deinit,
|
||||
.device_start = cast_device_start,
|
||||
.device_stop = cast_device_stop,
|
||||
/* .device_probe = cast_device_probe,
|
||||
.device_free_extra = cast_device_free_extra,*/
|
||||
// .device_probe = cast_device_probe,
|
||||
// .device_free_extra is unset - nothing to free
|
||||
.device_volume_set = cast_volume_set,
|
||||
.playback_start = cast_playback_start,
|
||||
/* .playback_stop = cast_playback_stop,
|
||||
.write = cast_write,
|
||||
.flush = cast_flush,*/
|
||||
.playback_stop = cast_playback_stop,
|
||||
// .write is unset - we don't write, the Chromecast will read our mp3 stream
|
||||
.flush = cast_flush,
|
||||
.status_cb = cast_set_status_cb,
|
||||
/* .metadata_prepare = cast_metadata_prepare,
|
||||
.metadata_send = cast_metadata_send,
|
||||
|
Loading…
Reference in New Issue
Block a user