[airplay] Support for airplay events (eg Homepod controls)

Ref. issue #1181
This commit is contained in:
ejurgensen 2021-07-31 01:16:23 +02:00 committed by GitHub
parent 246d8ae0ce
commit b6835fac29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 593 additions and 85 deletions

View File

@ -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 \

View File

@ -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);

View 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);
}

View 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__ */