From 1e051407d55c28580c3e42b54fc6973f4b0f41cc Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 17 Nov 2018 01:04:27 +0100 Subject: [PATCH] [raop] Listen for _airplay._tcp and make it a condition for device removal (issue #496) --- src/outputs/raop.c | 156 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 142 insertions(+), 14 deletions(-) diff --git a/src/outputs/raop.c b/src/outputs/raop.c index f3a8e246..d08982fd 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -159,8 +159,11 @@ struct raop_extra { enum raop_devtype devtype; + // "ek" param in the mdns announcement bool encrypt; + // "md" param in the mdns announcement bool wants_metadata; + // Part of the "et" param in the mdns announcement bool supports_auth_setup; }; @@ -236,6 +239,15 @@ struct raop_metadata struct raop_metadata *next; }; +struct raop_airplay_announcement +{ + uint64_t id; + char *name; + int family; + + struct raop_airplay_announcement *next; +}; + struct raop_service { int fd; @@ -339,6 +351,9 @@ static struct timeval keep_alive_tv = { 30, 0 }; /* Sessions */ static struct raop_session *sessions; +/* _airplay._tcp announcements */ +static struct raop_airplay_announcement *raop_airplay_announcements; + /* ALAC bits writer - big endian * p outgoing buffer pointer @@ -4557,6 +4572,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha { struct output_device *rd; struct raop_extra *re; + struct raop_airplay_announcement *an; cfg_t *airplay; const char *p; char *at_name; @@ -4592,12 +4608,6 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha { DPRINTF(E_LOG, L_RAOP, "Excluding AirPlay device '%s' as set in config\n", at_name); - return; - } - if (airplay && cfg_getbool(airplay, "permanent") && (port < 0)) - { - DPRINTF(E_INFO, L_RAOP, "AirPlay device '%s' disappeared, but set as permanent in config\n", at_name); - return; } @@ -4612,7 +4622,21 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha if (port < 0) { - /* Device stopped advertising */ + // Device stopped advertising, but let's see if we really should remove it + if (airplay && cfg_getbool(airplay, "permanent")) + { + DPRINTF(E_INFO, L_RAOP, "AirPlay device '%s' disappeared, but set as permanent in config\n", at_name); + goto free_rd; + } + for (an = raop_airplay_announcements; an; an = an->next) + { + if (!(id == an->id && family == an->family)) + continue; + + DPRINTF(E_DBG, L_RAOP, "Removal of AirPlay device '%s' deferred for _airplay._tcp announcement\n", at_name); + goto free_rd; + } + switch (family) { case AF_INET: @@ -4631,7 +4655,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha return; } - /* Protocol */ + // Protocol p = keyval_get(txt, "tp"); if (!p) { @@ -4654,7 +4678,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha goto free_rd; } - /* Password protection */ + // Password protection password = NULL; p = keyval_get(txt, "pw"); if (!p) @@ -4686,7 +4710,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha rd->password = password; - /* Device verification */ + // Device verification p = keyval_get(txt, "sf"); if (p && (safe_hextou64(p, &sf) == 0)) { @@ -4696,7 +4720,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha // Note: device_add() in player.c will get the auth key from the db if available } - /* Device type */ + // Device type re->devtype = RAOP_DEV_OTHER; p = keyval_get(txt, "am"); @@ -4713,14 +4737,14 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha else if (*p == '\0') DPRINTF(E_LOG, L_RAOP, "AirPlay device '%s': am has no value\n", at_name); - /* Encrypt stream */ + // Encrypt stream p = keyval_get(txt, "ek"); if (p && (*p == '1')) re->encrypt = 1; else re->encrypt = 0; - /* Metadata support */ + // Metadata support p = keyval_get(txt, "md"); if (p && (*p != '\0')) re->wants_metadata = 1; @@ -4776,6 +4800,103 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha outputs_device_free(rd); } +// Here is the deal with browsing for _airplay._tcp (even though we mainly use +// _raop._tcp): Some devices (Apple HomePod) stop announcing _raop._tcp records +// despite being online. They will, however, keep announcing _airplay._tcp, so +// we make it a condition that _airplay._tcp also times out before removing a +// device. +static void +raop_device_airplay_cb(const char *name, const char *type, const char *domain, const char *hostname, int family, const char *address, int port, struct keyval *txt) +{ + struct raop_airplay_announcement *an; + struct raop_airplay_announcement *prev; + const char *p; + char deviceid[13]; + char raopname[128]; + int i; + uint64_t id; + int ret; + + DPRINTF(E_DBG, L_RAOP, "_airplay._tcp event from '%s' (address '%s', port %d, family %d)\n", name, address, port, family); + +#ifdef DEBUG + for (an = raop_airplay_announcements; an; an = an->next) + { + snprintf(raopname, sizeof(raopname), "%" PRIX64 "@%s", an->id, an->name); + DPRINTF(E_DBG, L_RAOP, "Dump of _airplay._tcp announcements in list: '%s' (family %d)\n", raopname, an->family); + } +#endif + + // Device is gone (note, depending on order it might already have been removed) + if (port < 0) + { + prev = NULL; + for (an = raop_airplay_announcements; an; an = an->next) + { + if (strcmp(name, an->name) == 0 && family == an->family) + break; + + prev = an; + } + + if (!an) + return; + + // Remove from list so raop_device_cb will not find it + if (!prev) + raop_airplay_announcements = an->next; + else + prev->next = an->next; + + snprintf(raopname, sizeof(raopname), "%" PRIX64 "@%s", an->id, an->name); + raop_device_cb(raopname, type, domain, hostname, family, address, port, txt); + + free(an->name); + free(an); + return; + } + + // Find device's ID + p = keyval_get(txt, "deviceid"); + if (!p) + { + DPRINTF(E_WARN, L_RAOP, "_airplay._tcp event from '%s' without deviceid in txt field\n", name); + return; + } + + // Converts p="AB:CD:EF:01:02:03" -> deviceid="ABCDEF0123456" + for (i = 0; (*p != '\0') && (i < sizeof(deviceid)); p++) + { + if (*p == ':') + continue; + + deviceid[i] = *p; + i++; + } + deviceid[sizeof(deviceid) - 1] = '\0'; + + ret = safe_hextou64(deviceid, &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_RAOP, "Could not read AirPlay device ID ('%s') from '%s'\n", deviceid, name); + return; + } + + // If we already have the announcement registered we are done + for (an = raop_airplay_announcements; an; an = an->next) + { + if (id == an->id && family == an->family) + return; + } + + an = calloc(1, sizeof(struct raop_airplay_announcement)); + an->id = id; + an->name = strdup(name); + an->family = family; + an->next = raop_airplay_announcements; + raop_airplay_announcements = an; +} + static int raop_device_start_generic(struct output_device *rd, output_status_cb cb, uint64_t rtptime, bool only_probe) { @@ -5034,11 +5155,18 @@ raop_init(void) ret = mdns_browse("_raop._tcp", family, raop_device_cb, MDNS_CONNECTION_TEST); if (ret < 0) { - DPRINTF(E_LOG, L_RAOP, "Could not add mDNS browser for AirPlay devices\n"); + DPRINTF(E_LOG, L_RAOP, "Could not add mDNS browser for AirPlay devices (_raop._tcp)\n"); goto out_stop_control; } + ret = mdns_browse("_airplay._tcp", family, raop_device_airplay_cb, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_RAOP, "Could not add mDNS browser for AirPlay devices (_airplay._tcp)\n"); + + goto out_stop_control; + } return 0;