From fd56780575f5a255062e4faf51759f79bdfb07f0 Mon Sep 17 00:00:00 2001 From: mike Date: Wed, 27 Apr 2016 12:09:19 +0100 Subject: [PATCH] Added option to broadcast metadata over UDP --- README.md | 10 +++++++++- common.h | 3 +++ rtsp.c | 35 +++++++++++++++++++++++++++++++++++ scripts/shairport-sync.conf | 3 +++ shairport.c | 13 +++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 92998d65..af63b91f 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ What else? * Hardware Mute — Shairport Sync will mute properly if the hardware supports it. * Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.15 seconds. * Non-Interruptible — Shairport Sync sends back a "busy" signal if it's already playing audio from another source, so other sources can't disrupt an existing Shairport Sync session. (If a source disappears without warning, the session automatically terminates after two minutes and the device becomes available again.) -* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in iOS. +* Metadata — Shairport Sync can deliver metadata supplied by the source, such as Album Name, Artist Name, Cover Art, etc. through a pipe or UDP socket to a recipient application program — see https://github.com/mikebrady/shairport-sync-metadata-reader for a sample recipient. Sources that supply metadata include iTunes and the Music app in iOS. * Raw Audio — Shairport Sync can deliver raw PCM audio to standard output or to a pipe. This output is delivered synchronously with the source after the appropriate latency and is not interpolated or "stuffed" on its way through Shairport Sync. * Autotools and Libtool Support — the Shairport Sync build process uses GNU `autotools` and `libtool` to examine and configure the build environment — important for portability and for cross compilation. Previous versions of Shairport looked at the current system to determine which packages were available, instead of looking at the target system for what packages were available. @@ -342,6 +342,14 @@ alsa = { }; ``` +Metadata broadcasting over UDP +------------------------------ +As an alternative to sending metadata to a pipe, the `socket_address` and `socket_port` tags may be set in the metadata group to cause Shairport Sync to broadcast UDP packets containing the track metadata. + +The advantage of UDP is that packets can be sent to a single listener or, if a multicast address is used, to multiple listeners. It also allows metadata to be routed to a different host. However UDP has a maximum packet size of about 65000 bytes; while large enough for most data, Cover Art will often exceed this value. Any metadata exceeding this limit will not be sent over the socket interface. The maximum packet size may be set with the `socket_msglength` tag to any value between 500 and 65000 to control this - lower values may be used to ensure that each UDP packet is sent in a single network frame. The default is 500. Other than this restriction, metadata sent over the socket interface is identical to metadata sent over the pipe interface. + +The UDP metadata format is very simple - the first four bytes are the metadata *type*, and the next four bytes are the metadata *code* (both are sent in network byte order - see https://github.com/mikebrady/shairport-sync-metadata-reader for a definition of those terms). The remaining bytes of the packet, if any, make up the raw value of the metadata. + Latency ------- Latency is the exact time from a sound signal's original timestamp until that signal actually "appears" on the output of the audio output device, usually a Digital to Audio Converter (DAC), irrespective of any internal delays, processing times, etc. in the computer. diff --git a/common.h b/common.h index 25e24bbd..1b0cf639 100644 --- a/common.h +++ b/common.h @@ -50,6 +50,9 @@ typedef struct { #ifdef CONFIG_METADATA int metadata_enabled; char *metadata_pipename; + char *metadata_sockaddr; + int metadata_sockport; + int metadata_sockmsglength; int get_coverart; #endif uint8_t hw_addr[6]; diff --git a/rtsp.c b/rtsp.c index 66cd9b9b..4d973009 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1023,6 +1023,9 @@ char *base64_encode_so(const unsigned char *data, size_t input_length, static int fd = -1; static int dirty = 0; pc_queue metadata_queue; +static int metadata_sock; +static struct sockaddr_in metadata_sockaddr; +static char *metadata_sockmsg; #define metadata_queue_size 500 metadata_package metadata_queue_items[metadata_queue_size]; @@ -1032,6 +1035,24 @@ void metadata_create(void) { if (config.metadata_enabled == 0) return; + // Unlike metadata pipe, socket is opened once and stays open, + // so we can call it in create + if (config.metadata_sockaddr && config.metadata_sockport) { + metadata_sock = socket(AF_INET, SOCK_DGRAM, 0); + if (metadata_sock < 0) { + debug(1, "Could not open metadata socket"); + } else { + bzero((char *)&metadata_sockaddr, sizeof(metadata_sockaddr)); + metadata_sockaddr.sin_family = AF_INET; + metadata_sockaddr.sin_addr.s_addr = inet_addr(config.metadata_sockaddr); + metadata_sockaddr.sin_port = htons(config.metadata_sockport); + if (!(metadata_sockmsg = malloc(config.metadata_sockmsglength))) { + die("Could not malloc metadata socket buffer"); + } + memset(metadata_sockmsg, 0, config.metadata_sockmsglength); + } + } + size_t pl = strlen(config.metadata_pipename) + 1; char *path = malloc(pl + 1); @@ -1070,6 +1091,20 @@ void metadata_process(uint32_t type, uint32_t code, char *data, debug(2, "Process metadata with type %x, code %x and length %u.", type, code, length); int ret; + + if (metadata_sock >= 0 && length < config.metadata_sockmsglength - 8) { + char *ptr = metadata_sockmsg; + uint32_t v; + v = htonl(type); + memcpy(ptr, &v, 4); + ptr += 4; + v = htonl(code); + memcpy(ptr, &v, 4); + ptr += 4; + memcpy(ptr, data, length); + sendto(metadata_sock, metadata_sockmsg, length + 8, 0, (struct sockaddr *)&metadata_sockaddr, sizeof(metadata_sockaddr)); + } + // readers may go away and come back if (fd < 0) metadata_open(); diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 050ee364..a75298b6 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -27,6 +27,9 @@ metadata = // enabled = "no"; // et to yes to get Shairport Sync to solicit metadata from the source and to pass it on via a pipe // include_cover_art = "no"; // set to "yes" to get Shairport Sync to solicit cover art from the source and pass it via the pipe. You must also set "enabled" to "yes". // pipe_name = "/tmp/shairport-sync-metadata"; +// socket_address = "226.0.0.1"; // if set to a host name or IP address, UDP packets containing metadata will be sent to this address. May be a multicast address. "socket-port" must be non-zero and "enabled" must be set to yes" +// socket_port = "5555"; // if socket_address is set, the port to send UDP packets to +// socket_msglength = "65000"; // the maximum packet size for any UDP metadata. This will be clipped to be between 500 or 65000. The default is 500. }; // Advanced parameters for controlling how a Shairport Sync runs diff --git a/shairport.c b/shairport.c index b50fa0a8..041986c5 100644 --- a/shairport.c +++ b/shairport.c @@ -477,6 +477,17 @@ int parse_options(int argc, char **argv) { if (config_lookup_string(config.cfg, "metadata.pipe_name", &str)) { config.metadata_pipename = (char *)str; } + + if (config_lookup_string(config.cfg, "metadata.socket_address", &str)) { + config.metadata_sockaddr = (char *)str; + } + if (config_lookup_int(config.cfg, "metadata.socket_port", &value)) { + config.metadata_sockport = value; + } + config.metadata_sockmsglength = 500; + if (config_lookup_int(config.cfg, "metadata.socket_msglength", &value)) { + config.metadata_sockmsglength = value < 500 ? 500 : value > 65000 ? 65000 : value; + } #endif if (config_lookup_string(config.cfg, "sessioncontrol.run_this_before_play_begins", &str)) { @@ -991,6 +1002,8 @@ int main(int argc, char **argv) { #ifdef CONFIG_METADATA debug(1, "metdata enabled is %d.", config.metadata_enabled); debug(1, "metadata pipename is \"%s\".", config.metadata_pipename); + debug(1, "metadata socket address is \"%s\" port %d.", config.metadata_sockaddr, config.metadata_sockport); + debug(1, "metadata socket packet size is \"%d\".", config.metadata_sockmsglength); debug(1, "get-coverart is %d.", config.get_coverart); #endif -- 2.47.2