mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-26 23:25:56 -05:00
[airplay] Support for airplay events (eg Homepod controls)
Ref. issue #1181
This commit is contained in:
parent
246d8ae0ce
commit
b6835fac29
@ -140,6 +140,7 @@ owntone_SOURCES = main.c \
|
||||
outputs.h outputs.c \
|
||||
outputs/rtp_common.h outputs/rtp_common.c \
|
||||
outputs/raop.c outputs/airplay.c $(PAIR_AP_SRC) \
|
||||
outputs/airplay_events.c outputs/airplay_events.h \
|
||||
outputs/streaming.c outputs/dummy.c outputs/fifo.c \
|
||||
$(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \
|
||||
evrtsp/rtsp.c evrtsp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \
|
||||
|
@ -54,6 +54,7 @@
|
||||
#include "transcode.h"
|
||||
#include "outputs.h"
|
||||
|
||||
#include "airplay_events.h"
|
||||
#include "pair_ap/pair.h"
|
||||
|
||||
/* List of TODO's for AirPlay 2
|
||||
@ -84,6 +85,10 @@
|
||||
|
||||
#define AIRPLAY_RTP_PAYLOADTYPE 0x60
|
||||
|
||||
// For transient pairing the key_len will be 64 bytes, but only 32 are used for
|
||||
// audio payload encryption. For normal pairing the key is 32 bytes.
|
||||
#define AIRPLAY_AUDIO_KEY_LEN 32
|
||||
|
||||
// How many RTP packets keep in a buffer for retransmission
|
||||
#define AIRPLAY_PACKET_BUFFER_SIZE 1000
|
||||
|
||||
@ -269,18 +274,16 @@ struct airplay_session
|
||||
/* Pairing, see pair.h */
|
||||
enum pair_type pair_type;
|
||||
struct pair_cipher_context *control_cipher_ctx;
|
||||
struct pair_cipher_context *events_cipher_ctx;
|
||||
struct pair_verify_context *pair_verify_ctx;
|
||||
struct pair_setup_context *pair_setup_ctx;
|
||||
|
||||
uint8_t shared_secret[32];
|
||||
uint8_t shared_secret[64];
|
||||
size_t shared_secret_len; // 32 or 64, see AIRPLAY_AUDIO_KEY_LEN for comment
|
||||
|
||||
gcry_cipher_hd_t packet_cipher_hd;
|
||||
|
||||
int server_fd;
|
||||
|
||||
int events_fd;
|
||||
struct event *eventsev;
|
||||
|
||||
struct airplay_service *timing_svc;
|
||||
struct airplay_service *control_svc;
|
||||
|
||||
@ -1210,21 +1213,14 @@ session_free(struct airplay_session *rs)
|
||||
if (rs->deferredev)
|
||||
event_free(rs->deferredev);
|
||||
|
||||
if (rs->eventsev)
|
||||
event_free(rs->eventsev);
|
||||
|
||||
if (rs->server_fd >= 0)
|
||||
close(rs->server_fd);
|
||||
|
||||
if (rs->events_fd >= 0)
|
||||
close(rs->events_fd);
|
||||
|
||||
chacha_close(rs->packet_cipher_hd);
|
||||
|
||||
pair_setup_free(rs->pair_setup_ctx);
|
||||
pair_verify_free(rs->pair_verify_ctx);
|
||||
pair_cipher_free(rs->control_cipher_ctx);
|
||||
pair_cipher_free(rs->events_cipher_ctx);
|
||||
|
||||
free(rs->local_address);
|
||||
free(rs->realm);
|
||||
@ -1413,15 +1409,18 @@ static int
|
||||
session_cipher_setup(struct airplay_session *rs, const uint8_t *key, size_t key_len)
|
||||
{
|
||||
struct pair_cipher_context *control_cipher_ctx = NULL;
|
||||
struct pair_cipher_context *events_cipher_ctx = NULL;
|
||||
gcry_cipher_hd_t packet_cipher_hd = NULL;
|
||||
|
||||
if (key_len < sizeof(rs->shared_secret)) // For transient pairing the key_len will be 64 bytes, and rs->shared_secret is 32 bytes
|
||||
// For transient pairing the key_len will be 64 bytes, and rs->shared_secret is 32 bytes
|
||||
if (key_len < AIRPLAY_AUDIO_KEY_LEN || key_len > sizeof(rs->shared_secret))
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Ciphering setup error: Unexpected key length (%zu)\n", key_len);
|
||||
goto error;
|
||||
}
|
||||
|
||||
rs->shared_secret_len = key_len;
|
||||
memcpy(rs->shared_secret, key, key_len);
|
||||
|
||||
control_cipher_ctx = pair_cipher_new(rs->pair_type, 0, key, key_len);
|
||||
if (!control_cipher_ctx)
|
||||
{
|
||||
@ -1429,17 +1428,7 @@ session_cipher_setup(struct airplay_session *rs, const uint8_t *key, size_t key_
|
||||
goto error;
|
||||
}
|
||||
|
||||
events_cipher_ctx = pair_cipher_new(rs->pair_type, 1, key, key_len);
|
||||
if (!events_cipher_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not create events ciphering context\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
// Copy the first 32 bytes, will be used for encrypting audio payload
|
||||
memcpy(rs->shared_secret, key, sizeof(rs->shared_secret));
|
||||
|
||||
packet_cipher_hd = chacha_open(rs->shared_secret, sizeof(rs->shared_secret));
|
||||
packet_cipher_hd = chacha_open(rs->shared_secret, AIRPLAY_AUDIO_KEY_LEN);
|
||||
if (!packet_cipher_hd)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not create packet ciphering handle\n");
|
||||
@ -1450,7 +1439,6 @@ session_cipher_setup(struct airplay_session *rs, const uint8_t *key, size_t key_
|
||||
|
||||
rs->state = AIRPLAY_STATE_ENCRYPTED;
|
||||
rs->control_cipher_ctx = control_cipher_ctx;
|
||||
rs->events_cipher_ctx = events_cipher_ctx;
|
||||
rs->packet_cipher_hd = packet_cipher_hd;
|
||||
|
||||
evrtsp_connection_set_ciphercb(rs->ctrl, rtsp_cipher, rs);
|
||||
@ -1459,7 +1447,6 @@ session_cipher_setup(struct airplay_session *rs, const uint8_t *key, size_t key_
|
||||
|
||||
error:
|
||||
pair_cipher_free(control_cipher_ctx);
|
||||
pair_cipher_free(events_cipher_ctx);
|
||||
chacha_close(packet_cipher_hd);
|
||||
return -1;
|
||||
}
|
||||
@ -1566,7 +1553,6 @@ session_make(struct output_device *rd, int callback_id)
|
||||
rs->callback_id = callback_id;
|
||||
|
||||
rs->server_fd = -1;
|
||||
rs->events_fd = -1;
|
||||
|
||||
rs->password = rd->password;
|
||||
|
||||
@ -2310,42 +2296,6 @@ control_svc_cb(int fd, short what, void *arg)
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------------- Event receiver ------------------------------*/
|
||||
|
||||
// TODO actually handle events...
|
||||
static void
|
||||
event_channel_cb(int fd, short what, void *arg)
|
||||
{
|
||||
struct airplay_session *rs = arg;
|
||||
ssize_t in_len;
|
||||
int ret;
|
||||
uint8_t in[4096]; //TODO
|
||||
uint8_t *out;
|
||||
size_t out_len = 0;
|
||||
|
||||
in_len = recv(fd, in, sizeof(in), 0);
|
||||
if (in_len < 0)
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "Event channel to '%s' returned an error: %s\n", rs->devname, strerror(errno));
|
||||
|
||||
if (in_len <= 0)
|
||||
return;
|
||||
|
||||
DPRINTF(E_DBG, L_AIRPLAY, "Received an event from '%s' (len=%zd)\n", rs->devname, in_len);
|
||||
|
||||
if (in_len == sizeof(in))
|
||||
return; // Longer than expected, give up
|
||||
|
||||
ret = pair_decrypt(&out, &out_len, in, in_len, rs->events_cipher_ctx);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_DBG, L_AIRPLAY, "Error decrypting event from '%s', error was: %s\n", rs->devname, pair_cipher_errmsg(rs->events_cipher_ctx));
|
||||
return;
|
||||
}
|
||||
|
||||
DHEXDUMP(E_DBG, L_AIRPLAY, out, out_len, "Decrypted incoming event\n");
|
||||
}
|
||||
|
||||
|
||||
/* -------------------- Handlers for sending RTSP requests ------------------ */
|
||||
|
||||
static int
|
||||
@ -2552,7 +2502,7 @@ payload_make_setup_stream(struct evrtsp_request *req, struct airplay_session *rs
|
||||
wplist_dict_add_bool(stream, "isMedia", true); // ?
|
||||
wplist_dict_add_uint(stream, "latencyMax", 88200); // TODO how do these latencys work?
|
||||
wplist_dict_add_uint(stream, "latencyMin", 11025);
|
||||
wplist_dict_add_data(stream, "shk", rs->shared_secret, sizeof(rs->shared_secret));
|
||||
wplist_dict_add_data(stream, "shk", rs->shared_secret, AIRPLAY_AUDIO_KEY_LEN);
|
||||
wplist_dict_add_uint(stream, "spf", AIRPLAY_SAMPLES_PER_PACKET); // frames per packet
|
||||
wplist_dict_add_uint(stream, "sr", AIRPLAY_QUALITY_SAMPLE_RATE_DEFAULT); // sample rate
|
||||
wplist_dict_add_uint(stream, "type", AIRPLAY_RTP_PAYLOADTYPE); // RTP type, 0x60 = 96 real time, 103 buffered
|
||||
@ -2963,16 +2913,11 @@ response_handler_setup_stream(struct evrtsp_request *req, struct airplay_session
|
||||
}
|
||||
|
||||
// Reverse connection, used to receive playback events from device
|
||||
rs->events_fd = net_connect(rs->address, rs->events_port, SOCK_STREAM, "AirPlay events");
|
||||
if (rs->events_fd < 0)
|
||||
ret = airplay_events_listen(rs->devname, rs->address, rs->events_port, rs->shared_secret, rs->shared_secret_len);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "Could not connect to '%s' events port %u, proceeding anyway\n", rs->devname, rs->events_port);
|
||||
}
|
||||
else
|
||||
{
|
||||
rs->eventsev = event_new(evbase_player, rs->events_fd, EV_READ | EV_PERSIST, event_channel_cb, rs);
|
||||
event_add(rs->eventsev, NULL);
|
||||
}
|
||||
|
||||
rs->state = AIRPLAY_STATE_SETUP;
|
||||
|
||||
@ -3252,12 +3197,6 @@ response_handler_pair_setup2(struct evrtsp_request *req, struct airplay_session
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (shared_secret_len < sizeof(rs->shared_secret)) // We expect 64 bytes, and rs->shared_secret is 32 bytes
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Transient setup result error: Unexpected key length (%zu)\n", shared_secret_len);
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = session_cipher_setup(rs, shared_secret, shared_secret_len);
|
||||
if (ret < 0)
|
||||
{
|
||||
@ -3354,12 +3293,6 @@ response_handler_pair_verify2(struct evrtsp_request *req, struct airplay_session
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (sizeof(rs->shared_secret) != shared_secret_len)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Pair verify result error: Unexpected key length (%zu)\n", shared_secret_len);
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = session_cipher_setup(rs, shared_secret, shared_secret_len);
|
||||
if (ret < 0)
|
||||
{
|
||||
@ -4081,15 +4014,24 @@ airplay_init(void)
|
||||
goto out_stop_timing;
|
||||
}
|
||||
|
||||
ret = airplay_events_init();
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "AirPlay events failed to start\n");
|
||||
goto out_stop_control;
|
||||
}
|
||||
|
||||
ret = mdns_browse("_airplay._tcp", airplay_device_cb, MDNS_CONNECTION_TEST);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not add mDNS browser for AirPlay devices\n");
|
||||
goto out_stop_control;
|
||||
goto out_stop_events;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
out_stop_events:
|
||||
airplay_events_deinit();
|
||||
out_stop_control:
|
||||
service_stop(&airplay_control_svc);
|
||||
out_stop_timing:
|
||||
@ -4105,6 +4047,7 @@ airplay_deinit(void)
|
||||
{
|
||||
struct airplay_session *rs;
|
||||
|
||||
airplay_events_deinit();
|
||||
service_stop(&airplay_control_svc);
|
||||
service_stop(&airplay_timing_svc);
|
||||
|
||||
|
551
src/outputs/airplay_events.c
Normal file
551
src/outputs/airplay_events.c
Normal file
@ -0,0 +1,551 @@
|
||||
/*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <inttypes.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <plist/plist.h>
|
||||
|
||||
#include "airplay_events.h"
|
||||
#include "commands.h"
|
||||
#include "misc.h"
|
||||
#include "logger.h"
|
||||
#include "player.h"
|
||||
#include "pair_ap/pair.h"
|
||||
|
||||
#define RTSP_VERSION "RTSP/1.0"
|
||||
|
||||
enum airplay_events
|
||||
{
|
||||
AIRPLAY_EVENT_UNKNOWN,
|
||||
AIRPLAY_EVENT_PLAY,
|
||||
AIRPLAY_EVENT_PAUSE,
|
||||
AIRPLAY_EVENT_NEXT,
|
||||
AIRPLAY_EVENT_PREV,
|
||||
};
|
||||
|
||||
struct airplay_events_client
|
||||
{
|
||||
char *name;
|
||||
int fd;
|
||||
struct event *listener;
|
||||
struct pair_cipher_context *cipher_ctx;
|
||||
|
||||
struct evbuffer *incoming;
|
||||
struct evbuffer *pending;
|
||||
|
||||
struct airplay_events_client *next;
|
||||
};
|
||||
|
||||
struct rtsp_message
|
||||
{
|
||||
int content_length;
|
||||
char *content_type;
|
||||
char *first_line;
|
||||
int cseq;
|
||||
|
||||
const uint8_t *body;
|
||||
size_t bodylen;
|
||||
|
||||
const uint8_t *data;
|
||||
size_t datalen;
|
||||
};
|
||||
|
||||
|
||||
static pthread_t thread_id;
|
||||
static struct event_base *evbase;
|
||||
static struct commands_base *cmdbase;
|
||||
static struct airplay_events_client *airplay_events_clients;
|
||||
|
||||
// Forwards
|
||||
static void
|
||||
incoming_cb(int fd, short what, void *arg);
|
||||
|
||||
|
||||
/* ---------------------------- Client handling ----------------------------- */
|
||||
|
||||
static void
|
||||
client_free(struct airplay_events_client *client)
|
||||
{
|
||||
if (!client)
|
||||
return;
|
||||
|
||||
if (client->listener)
|
||||
event_free(client->listener);
|
||||
|
||||
evbuffer_free(client->incoming);
|
||||
evbuffer_free(client->pending);
|
||||
|
||||
free(client->name);
|
||||
pair_cipher_free(client->cipher_ctx);
|
||||
|
||||
free(client);
|
||||
}
|
||||
|
||||
static void
|
||||
client_remove(struct airplay_events_client *client)
|
||||
{
|
||||
struct airplay_events_client *iter;
|
||||
|
||||
if (client == airplay_events_clients)
|
||||
airplay_events_clients = client->next;
|
||||
else
|
||||
{
|
||||
for (iter = airplay_events_clients; iter && (iter->next != client); iter = iter->next)
|
||||
; /* EMPTY */
|
||||
|
||||
if (iter)
|
||||
iter->next = client->next;
|
||||
}
|
||||
|
||||
client_free(client);
|
||||
}
|
||||
|
||||
static int
|
||||
client_add(const char *name, int fd, const uint8_t *key, size_t key_len)
|
||||
{
|
||||
struct airplay_events_client *client;
|
||||
|
||||
CHECK_NULL(L_AIRPLAY, client = calloc(1, sizeof(struct airplay_events_client)));
|
||||
CHECK_NULL(L_AIRPLAY, client->name = strdup(name));
|
||||
CHECK_NULL(L_AIRPLAY, client->incoming = evbuffer_new());
|
||||
CHECK_NULL(L_AIRPLAY, client->pending = evbuffer_new());
|
||||
|
||||
client->fd = fd;
|
||||
|
||||
client->listener = event_new(evbase, fd, EV_READ | EV_PERSIST, incoming_cb, client);
|
||||
if (!client->listener)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not listen for AirPlay events from '%s', invalid fd or out of memory\n", name);
|
||||
goto error;
|
||||
}
|
||||
|
||||
client->cipher_ctx = pair_cipher_new(PAIR_CLIENT_HOMEKIT_NORMAL, 1, key, key_len);
|
||||
if (!client->cipher_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not listen for AirPlay events from '%s': Could not create ciphering context\n", name);
|
||||
goto error;
|
||||
}
|
||||
|
||||
event_add(client->listener, NULL);
|
||||
|
||||
client->next = airplay_events_clients;
|
||||
airplay_events_clients = client;
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
client_free(client);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------- Ciphering ------------------------------- */
|
||||
|
||||
static int
|
||||
buffer_decrypt(struct evbuffer *output, struct evbuffer *input, struct pair_cipher_context *cipher_ctx)
|
||||
{
|
||||
uint8_t *in;
|
||||
size_t in_len;
|
||||
ssize_t bytes_decrypted;
|
||||
uint8_t *plain;
|
||||
size_t plain_len;
|
||||
|
||||
in = evbuffer_pullup(input, -1);
|
||||
in_len = evbuffer_get_length(input);
|
||||
|
||||
// Note that bytes_decrypted is not necessarily equal to plain_len
|
||||
bytes_decrypted = pair_decrypt(&plain, &plain_len, in, in_len, cipher_ctx);
|
||||
if (bytes_decrypted < 0)
|
||||
return -1;
|
||||
|
||||
evbuffer_add(output, plain, plain_len);
|
||||
evbuffer_drain(input, bytes_decrypted);
|
||||
free(plain);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
buffer_encrypt(struct evbuffer *output, uint8_t *in, size_t in_len, struct pair_cipher_context *cipher_ctx)
|
||||
{
|
||||
uint8_t *out;
|
||||
size_t out_len;
|
||||
int ret;
|
||||
|
||||
ret = pair_encrypt(&out, &out_len, in, in_len, cipher_ctx);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
evbuffer_add(output, out, out_len);
|
||||
free(out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------- Message construction/parsing ----------------------- */
|
||||
|
||||
static void
|
||||
response_headers_add(struct evbuffer *response, int cseq, size_t content_length, const char *content_type)
|
||||
{
|
||||
evbuffer_add_printf(response, "%s 200 OK\r\n", RTSP_VERSION);
|
||||
evbuffer_add_printf(response, "Server: %s/1.0\r\n", PACKAGE_NAME);
|
||||
if (content_length)
|
||||
evbuffer_add_printf(response, "Content-Length: %zu\r\n", content_length);
|
||||
if (content_type)
|
||||
evbuffer_add_printf(response, "Content-Type: %s\r\n", content_type);
|
||||
if (cseq)
|
||||
evbuffer_add_printf(response, "CSeq: %d\r\n", cseq);
|
||||
evbuffer_add_printf(response, "\r\n");
|
||||
}
|
||||
|
||||
static void
|
||||
response_create_from_raw(struct evbuffer *response, uint8_t *body, size_t body_len, int cseq, const char *content_type)
|
||||
{
|
||||
response_headers_add(response, cseq, body_len, content_type);
|
||||
|
||||
if (body)
|
||||
evbuffer_add(response, body, body_len);
|
||||
}
|
||||
|
||||
static int
|
||||
body_find(uint8_t **body, size_t *body_len, uint8_t *in, size_t in_len)
|
||||
{
|
||||
const char *plist_header = "bplist";
|
||||
size_t plist_header_len = strlen(plist_header);
|
||||
|
||||
*body_len = in_len;
|
||||
for (*body = in; *body_len > plist_header_len; (*body)++, (*body_len)--)
|
||||
{
|
||||
if (memcmp(*body, plist_header, plist_header_len) == 0)
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
rtsp_parse(enum airplay_events *event, uint8_t *in, size_t in_len)
|
||||
{
|
||||
uint8_t *body;
|
||||
size_t body_len;
|
||||
plist_t request = NULL;
|
||||
plist_t item;
|
||||
char *type = NULL;
|
||||
char *value = NULL;
|
||||
int ret;
|
||||
|
||||
DHEXDUMP(E_DBG, L_AIRPLAY, in, in_len, "Incoming event\n");
|
||||
|
||||
ret = body_find(&body, &body_len, in, in_len);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "Could not parse incoming event, no plist body found\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
plist_from_bin((char *)body, (uint32_t)body_len, &request);
|
||||
if (!request)
|
||||
{
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "Could not parse incoming event plist\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TODO remove
|
||||
char *xml = NULL;
|
||||
uint32_t xml_len;
|
||||
plist_to_xml(request, &xml, &xml_len);
|
||||
DPRINTF(E_DBG, L_AIRPLAY, "%s\n", xml);
|
||||
|
||||
item = plist_dict_get_item(request, "type");
|
||||
if (item)
|
||||
{
|
||||
plist_get_string_val(item, &type);
|
||||
}
|
||||
|
||||
item = plist_dict_get_item(request, "value");
|
||||
if (item)
|
||||
{
|
||||
plist_get_string_val(item, &value);
|
||||
}
|
||||
|
||||
if (!type || !value)
|
||||
{
|
||||
DPRINTF(E_DBG, L_AIRPLAY, "AirPlay event has no type/value: type=%s, value=%s\n", type, value);
|
||||
goto error;
|
||||
}
|
||||
else if (strcmp(type, "sendMediaRemoteCommand") != 0)
|
||||
{
|
||||
DPRINTF(E_DBG, L_AIRPLAY, "Incoming event not of type sendMediaRemoteCommand\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
DPRINTF(E_INFO, L_AIRPLAY, "Received event type '%s', value '%s'\n", type, value);
|
||||
|
||||
if (strcmp(value, "paus") == 0)
|
||||
*event = AIRPLAY_EVENT_PAUSE;
|
||||
else if (strcmp(value, "play") == 0)
|
||||
*event = AIRPLAY_EVENT_PLAY;
|
||||
else if (strcmp(value, "nitm") == 0)
|
||||
*event = AIRPLAY_EVENT_NEXT;
|
||||
else if (strcmp(value, "pitm") == 0)
|
||||
*event = AIRPLAY_EVENT_PREV;
|
||||
else
|
||||
*event = AIRPLAY_EVENT_UNKNOWN;
|
||||
|
||||
free(type);
|
||||
free(value);
|
||||
plist_free(request);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
free(type);
|
||||
free(value);
|
||||
plist_free(request);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------- Message handling ----------------------------- */
|
||||
|
||||
static void
|
||||
handle_event(enum airplay_events event)
|
||||
{
|
||||
struct player_status status;
|
||||
|
||||
player_get_status(&status);
|
||||
|
||||
switch (event)
|
||||
{
|
||||
case AIRPLAY_EVENT_PLAY:
|
||||
case AIRPLAY_EVENT_PAUSE:
|
||||
if (status.status == PLAY_PLAYING)
|
||||
player_playback_pause();
|
||||
else
|
||||
player_playback_start();
|
||||
break;
|
||||
case AIRPLAY_EVENT_NEXT:
|
||||
player_playback_next();
|
||||
break;
|
||||
case AIRPLAY_EVENT_PREV:
|
||||
player_playback_prev();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
respond(struct airplay_events_client *client)
|
||||
{
|
||||
struct evbuffer *response;
|
||||
struct evbuffer *encrypted;
|
||||
uint8_t *plain;
|
||||
size_t plain_len;
|
||||
int ret;
|
||||
|
||||
CHECK_NULL(L_AIRPLAY, response = evbuffer_new());
|
||||
response_create_from_raw(response, NULL, 0, 0, NULL);
|
||||
|
||||
plain = evbuffer_pullup(response, -1);
|
||||
plain_len = evbuffer_get_length(response);
|
||||
|
||||
CHECK_NULL(L_AIRPLAY, encrypted = evbuffer_new());
|
||||
|
||||
ret = buffer_encrypt(encrypted, plain, plain_len, client->cipher_ctx);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "Could not encrypt AirPlay event data response: %s\n", pair_cipher_errmsg(client->cipher_ctx));
|
||||
return -1;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
ret = evbuffer_write(encrypted, client->fd);
|
||||
if (ret <= 0)
|
||||
goto error;
|
||||
} while (evbuffer_get_length(encrypted) > 0);
|
||||
|
||||
evbuffer_free(encrypted);
|
||||
evbuffer_free(response);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
evbuffer_free(encrypted);
|
||||
evbuffer_free(response);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void
|
||||
incoming_cb(int fd, short what, void *arg)
|
||||
{
|
||||
struct airplay_events_client *client = arg;
|
||||
enum airplay_events event;
|
||||
uint8_t *plain;
|
||||
size_t plain_len;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_AIRPLAY, "AirPlay event from '%s'\n", client->name);
|
||||
|
||||
ret = evbuffer_read(client->incoming, fd, -1);
|
||||
if (ret == 0)
|
||||
{
|
||||
DPRINTF(E_DBG, L_AIRPLAY, "'%s' disconnected from the event channel\n", client->name);
|
||||
goto disconnect;
|
||||
}
|
||||
else if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "AirPlay event connection to '%s' returned an error\n", client->name);
|
||||
goto disconnect;
|
||||
}
|
||||
|
||||
ret = buffer_decrypt(client->pending, client->incoming, client->cipher_ctx);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "Could not decrypt incoming AirPlay event data: %s\n", pair_cipher_errmsg(client->cipher_ctx));
|
||||
goto disconnect;
|
||||
}
|
||||
|
||||
plain = evbuffer_pullup(client->pending, -1);
|
||||
plain_len = evbuffer_get_length(client->pending);
|
||||
|
||||
ret = rtsp_parse(&event, plain, plain_len);
|
||||
if (ret < 0) // A message type we don't know about, so ignore
|
||||
{
|
||||
evbuffer_drain(client->pending, -1);
|
||||
return;
|
||||
}
|
||||
else if (ret == 1)
|
||||
{
|
||||
DPRINTF(E_SPAM, L_AIRPLAY, "Incomplete RTSP event message, waiting for more data\n");
|
||||
return;
|
||||
}
|
||||
|
||||
evbuffer_drain(client->pending, -1);
|
||||
|
||||
handle_event(event);
|
||||
|
||||
ret = respond(client);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "Could not send AirPlay event response\n");
|
||||
goto disconnect;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
disconnect:
|
||||
client_remove(client);
|
||||
return;
|
||||
}
|
||||
|
||||
/* -------------------- Event loop (thread: airplay events ) ---------------- */
|
||||
|
||||
static void *
|
||||
airplay_events(void *arg)
|
||||
{
|
||||
event_base_dispatch(evbase);
|
||||
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------- Interface -------------------------------- */
|
||||
|
||||
int
|
||||
airplay_events_listen(const char *name, const char *address, unsigned short port, const uint8_t *key, size_t key_len)
|
||||
{
|
||||
int fd;
|
||||
int ret;
|
||||
|
||||
fd = net_connect(address, port, SOCK_STREAM, "AirPlay events");
|
||||
if (fd < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = client_add(name, fd, key, key_len);
|
||||
if (ret < 0)
|
||||
{
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* Thread: main */
|
||||
int
|
||||
airplay_events_init(void)
|
||||
{
|
||||
int ret;
|
||||
|
||||
CHECK_NULL(L_AIRPLAY, evbase = event_base_new());
|
||||
CHECK_NULL(L_AIRPLAY, cmdbase = commands_base_new(evbase, NULL));
|
||||
|
||||
DPRINTF(E_INFO, L_AIRPLAY, "AirPlay events thread init\n");
|
||||
|
||||
ret = pthread_create(&thread_id, NULL, airplay_events, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not spawn AirPlay events thread: %s\n", strerror(errno));
|
||||
|
||||
goto error;
|
||||
}
|
||||
|
||||
// TODO thread_name_set(thread_id, "airplay events");
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
airplay_events_deinit();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Thread: main */
|
||||
void
|
||||
airplay_events_deinit(void)
|
||||
{
|
||||
int ret;
|
||||
|
||||
commands_base_destroy(cmdbase);
|
||||
|
||||
ret = pthread_join(thread_id, NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not join AirPlay events thread: %s\n", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
while (airplay_events_clients)
|
||||
{
|
||||
client_remove(airplay_events_clients);
|
||||
}
|
||||
|
||||
event_base_free(evbase);
|
||||
}
|
13
src/outputs/airplay_events.h
Normal file
13
src/outputs/airplay_events.h
Normal file
@ -0,0 +1,13 @@
|
||||
#ifndef __AIRPLAY_EVENTS_H__
|
||||
#define __AIRPLAY_EVENTS_H__
|
||||
|
||||
int
|
||||
airplay_events_listen(const char *name, const char *address, unsigned short port, const uint8_t *key, size_t key_len);
|
||||
|
||||
int
|
||||
airplay_events_init(void);
|
||||
|
||||
void
|
||||
airplay_events_deinit(void);
|
||||
|
||||
#endif /* !__AIRPLAY_EVENTS_H__ */
|
Loading…
Reference in New Issue
Block a user