From 436e22c2833870e16cb2c205d2e6f02b891a8a2d Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Sat, 16 Jun 2018 08:38:56 +0100 Subject: [PATCH 01/17] Update RELEASENOTES.md --- RELEASENOTES.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4658ccff..c1000322 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,12 @@ +Version 3.2d62 +==== +**Bug Fix** +* Restore compatability with Synology AudioStation/5.2. Thanks to [Jörg Krause](https://github.com/joerg-krause) for identifying both the issue itself and the likely location of the fix needed. It's not clear if the problem lies with Shairport Sync or Synology – the fix was to ensure that an RTSP reply was sent in one `SEND` call, which shouldn't be important. + Version 3.2d61 ==== **Bug Fix** -* Add a pthread_cancel cleanup routine to put_packet to unlock the player rw mutex when cancelled. Sincere thanks to [davidhq](https://github.com/davidhq) for help investigating this. +* Add a pthread_cancel cleanup routine to put_packet to unlock the player rw mutex when cancelled. Sincere thanks to [David Krmpotić](https://github.com/davidhq) for help investigating this. Version 3.2d57, 3.2d58, 3.2d59, 3.2d60 ==== From 4aeec6b248c3a14cb193b2690f31b63dd8c5af7b Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Sat, 16 Jun 2018 08:47:37 +0100 Subject: [PATCH 02/17] Update RELEASENOTES.md --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c1000322..501eef30 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,7 @@ Version 3.2d62 ==== **Bug Fix** -* Restore compatability with Synology AudioStation/5.2. Thanks to [Jörg Krause](https://github.com/joerg-krause) for identifying both the issue itself and the likely location of the fix needed. It's not clear if the problem lies with Shairport Sync or Synology – the fix was to ensure that an RTSP reply was sent in one `SEND` call, which shouldn't be important. +* Restore compatibility with Synology AudioStation/5.2. Thanks to [Jörg Krause](https://github.com/joerg-krause) for identifying both the issue itself and the likely location of the fix needed. It's not clear if the problem lies with Shairport Sync or Synology – the fix was to ensure that an RTSP reply was sent in one `SEND` call, which shouldn't be important. Version 3.2d61 ==== From 78fce27faa89b1b9e3bc12626d70973bfda928ac Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Sat, 16 Jun 2018 08:48:40 +0100 Subject: [PATCH 03/17] Update RELEASENOTES.md --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 501eef30..85cf1635 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,9 +11,9 @@ Version 3.2d61 Version 3.2d57, 3.2d58, 3.2d59, 3.2d60 ==== **Bug Chasing** -* Debug messaging has been enhanced when a TEARDOWN message is received. +* Debug messaging has been enhanced when a `TEARDOWN` message is received. * Check both timed and untimed mutex locks and give estimates of how long to wait. Generate debug messages when appropriate. -* Unlock the ab_mutex when sending a resend request. +* Unlock the `ab_mutex` when sending a resend request. Version 3.2d56 ==== From fec42e66ea61b389e74fd668d8a6b204239e8d19 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Sat, 16 Jun 2018 08:49:08 +0100 Subject: [PATCH 04/17] Update RELEASENOTES.md --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 85cf1635..d7484523 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,7 +22,7 @@ Version 3.2d56 If hardware muting is used, the output device will be muted when Shairport Sync releases it, potentially causing problems for other audio programs using the device. To avoid this, `use_hardware_mute_if_available` is disabled by default. **Bug Chasing** -* A debug message has been enhanced when a TEARDOWN message is received. +* A debug message has been enhanced when a `TEARDOWN` message is received. Version 3.2d55 ==== From de180967626e641a27c05b31f8c52f171a77d8d5 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Tue, 19 Jun 2018 17:18:57 +0100 Subject: [PATCH 05/17] add some debug messages to avahi code returns --- mdns_avahi.c | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/mdns_avahi.c b/mdns_avahi.c index b1085bb7..0a02a720 100644 --- a/mdns_avahi.c +++ b/mdns_avahi.c @@ -47,6 +47,13 @@ #include #include +#define check_avahi_response(debugLevelArg, veryUnLikelyArgumentName) \ + { \ + int rc = veryUnLikelyArgumentName; \ + if (rc) \ + debug(debugLevelArg, "avahi call response %d at __FILE__, __LINE__)", rc); \ + } + typedef struct { AvahiThreadedPoll *service_poll; AvahiClient *service_client; @@ -104,7 +111,7 @@ static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIn } } } - avahi_service_resolver_free(r); + check_avahi_response(1, avahi_service_resolver_free(r)); } static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event, const char *name, const char *type, @@ -166,8 +173,12 @@ static void egroup_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, /* A service name collision with a remote service * happened. Let's pick a new name */ + debug(1, "avahi name collision -- look for another"); n = avahi_alternative_service_name(service_name); - avahi_free(service_name); + if (service_name) + avahi_free(service_name); + else + debug(1, "avahi attempt to free a NULL service name"); service_name = n; debug(2, "avahi: service name collision, renaming service to '%s'", service_name); @@ -248,7 +259,7 @@ static void client_callback(AvahiClient *c, AvahiClientState state, switch (state) { case AVAHI_CLIENT_S_REGISTERING: if (group) - avahi_entry_group_reset(group); + check_avahi_response(1, avahi_entry_group_reset(group)); break; case AVAHI_CLIENT_S_RUNNING: @@ -260,8 +271,12 @@ static void client_callback(AvahiClient *c, AvahiClientState state, debug(1, "avahi: client failure: %s", avahi_strerror(err)); if (err == AVAHI_ERR_DISCONNECTED) { + debug(1, "avahi client -- we have been disconnected, so let's reconnect."); /* We have been disconnected, so lets reconnect */ - avahi_client_free(c); + if (c) + avahi_client_free(c); + else + debug(1, "Attempt to free NULL avahi client"); c = NULL; group = NULL; @@ -307,8 +322,12 @@ static void service_client_callback(AvahiClient *c, AvahiClientState state, void debug(1, "avahi: service client failure: %s", avahi_strerror(err)); if (err == AVAHI_ERR_DISCONNECTED) { + debug(1, "avahi client has been disconnected, so reconnect"); /* We have been disconnected, so lets reconnect */ - avahi_client_free(c); + if (c) + avahi_client_free(c); + else + debug(1, "avahi attempt to free a NULL client"); c = NULL; if (!(dbs->service_client = @@ -366,18 +385,21 @@ static void avahi_unregister(void) { debug(1, "avahi: avahi_unregister."); if (tpoll) { avahi_threaded_poll_stop(tpoll); - + if (client) { avahi_client_free(client); client = NULL; - }; - + } else { + debug(1, "avahi attempting to unregister a NULL client"); + } avahi_threaded_poll_free(tpoll); tpoll = NULL; } if (service_name) free(service_name); + else + debug(1, "avahi attempt to free NULL service name"); service_name = NULL; } @@ -418,7 +440,7 @@ void *avahi_dacp_monitor(char *dacp_id) { if (!(dbs->service_browser = avahi_service_browser_new(dbs->service_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_dacp._tcp", NULL, 0, browse_callback, (void *)dbs))) { - warn("Failed to create service browser: %s\n", + warn("failed to create avahi service browser: %s\n", avahi_strerror(avahi_client_errno(dbs->service_client))); avahi_client_free(dbs->service_client); avahi_threaded_poll_free(dbs->service_poll); From 02694948f3e0229529e42102a817b35fd144bf63 Mon Sep 17 00:00:00 2001 From: Till Zimmermann Date: Tue, 19 Jun 2018 19:40:08 +0200 Subject: [PATCH 06/17] Initial MQTT Version --- Makefile.am | 4 + common.h | 16 +++ configure.ac | 20 +++- mqtt.c | 209 ++++++++++++++++++++++++++++++++++++ mqtt.h | 15 +++ rtsp.c | 9 ++ scripts/shairport-sync.conf | 22 ++++ shairport.c | 119 +++++++++++++++----- 8 files changed, 388 insertions(+), 26 deletions(-) create mode 100644 mqtt.c create mode 100644 mqtt.h diff --git a/Makefile.am b/Makefile.am index f0bcca7f..e5f33afd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -79,6 +79,10 @@ if USE_METADATA_HUB shairport_sync_SOURCES += metadata_hub.c endif +if USE_MQTT +shairport_sync_SOURCES += mqtt.c +endif + if USE_DACP_CLIENT shairport_sync_SOURCES += dacp.c tinyhttp/chunk.c tinyhttp/header.c tinyhttp/http.c endif diff --git a/common.h b/common.h index 87493cb1..86cfa1a1 100644 --- a/common.h +++ b/common.h @@ -91,6 +91,22 @@ typedef struct { int metadata_sockport; size_t metadata_sockmsglength; int get_coverart; +#endif +#ifdef CONFIG_MQTT + int mqtt_enabled; + char *mqtt_hostname; + int mqtt_port; + char *mqtt_username; + char *mqtt_password; + char *mqtt_capath; + char *mqtt_cafile; + char *mqtt_certfile; + char *mqtt_keyfile; + char *mqtt_topic; + int mqtt_publish_raw; + int mqtt_publish_parsed; + int mqtt_publish_cover; + int mqtt_enable_remote; #endif uint8_t hw_addr[6]; int port; diff --git a/configure.ac b/configure.ac index 5a98fa84..f79041c5 100644 --- a/configure.ac +++ b/configure.ac @@ -314,7 +314,25 @@ AM_CONDITIONAL([USE_MPRIS_CLIENT], [test "x$HAS_MPRIS_CLIENT" = "x1"]) #AM_CONDITIONAL([USE_DBUS_CORE_AND_DACP], [test "x$HAS_MPRIS" = "x1" -o "x$HAS_DBUS" = "x1"]) -if test "x$HAS_MPRIS" = "x1" -o "x$HAS_DBUS" = "x1" ; then +# Look for mqtt flag +AC_ARG_WITH(mqtt-interface, [ --with-mqtt-interface = include support for the native Shairport Sync MQTT interface], [ + AC_MSG_RESULT(>>Including MQTT support) + HAS_MQTT=1 + AC_DEFINE([HAVE_MQTT], 1, [Needed by the compiler.]) + AC_DEFINE([CONFIG_MQTT], 1, [Needed by the compiler.]) + AC_CHECK_LIB([mosquitto], [mosquitto_lib_init], , AC_MSG_ERROR(MQTT support requires the mosquitto library!)) + AC_CHECK_HEADERS([mosquitto.h],,[AC_MSG_ERROR([ header missing])]) + AC_DEFINE([HAVE_MOSQUITTO],[1],[Define to 1 if you have the mosquitto library]) + + AC_MSG_RESULT(>>Enabling metadata as it is required by mqtt) + AC_DEFINE([CONFIG_METADATA], 1, [Needed by the compiler.]) + AC_DEFINE([USE_METADATA], 1, [Needed by the compiler.]) + PKG_CHECK_MODULES([GIO_UNIX], [gio-unix-2.0 >= 2.30.0],[CFLAGS="${GIO_UNIX_CFLAGS} ${CFLAGS}" LIBS="${GIO_UNIX_LIBS} ${LIBS}"],[AC_MSG_ERROR(dbus messaging support for mpris requires the glib 2.0 library -- libglib2.0-dev suggested!)]) + ],) +AM_CONDITIONAL([USE_MQTT], [test "x$HAS_MQTT" = "x1"]) + + +if test "x$HAS_MPRIS" = "x1" -o "x$HAS_DBUS" = "x1" -o "x$HAS_MQTT"; then AC_MSG_RESULT(>>Including the metadata hub) HAS_METADATA_HUB=1 AC_DEFINE([HAVE_METADATA_HUB], 1, [Needed by the compiler.]) diff --git a/mqtt.c b/mqtt.c new file mode 100644 index 00000000..478c476f --- /dev/null +++ b/mqtt.c @@ -0,0 +1,209 @@ +#include +#include +#include + +#include "config.h" + +#include "common.h" +#include "player.h" +#include "rtsp.h" + +#include "rtp.h" + +#include "dacp.h" +#include +#include "mqtt.h" + +//this holds the mosquitto client +struct mosquitto *mosq = NULL; +char *topic = NULL; + +//mosquitto logging +void _cb_log(struct mosquitto *mosq, void *userdata, int level, const char *str){ + switch(level){ + case MOSQ_LOG_DEBUG: + debug(1, str); + break; + case MOSQ_LOG_INFO: + debug(2, str); + break; + case MOSQ_LOG_NOTICE: + debug(3, str); + break; + case MOSQ_LOG_WARNING: + inform(str); + break; + case MOSQ_LOG_ERR: { + die("MQTT: Error: %s\n", str); + } + } +} + +//mosquitto message handler +void on_message(struct mosquitto* mosq, void* userdata, const struct mosquitto_message* msg){ + + //null-terminate the payload + char payload[msg->payloadlen+1]; + memcpy(payload,msg->payload,msg->payloadlen); + payload[msg->payloadlen]=0; + + debug(1, "[MQTT]: received Message on topic %s: %s\n",msg->topic, payload); + + //All recognized commands + char* commands[] = { + "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", + "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup", + NULL}; + + int it=0; + + //send command if it's a valid one + while(commands[it++]!=NULL){ + if( msg->payloadlen>=strlen(commands[it]) && + strncmp(msg->payload, commands[it], strlen(commands[it]))==0 + ){ + debug(1, "[MQTT]: DACP Command: %s\n",commands[it]); + send_simple_dacp_command(commands[it]); + break; + } + } +} + +void on_disconnect(struct mosquitto* mosq, void* userdata, int rc){ + debug(1, "[MQTT]: disconnected"); +} + +void on_connect(struct mosquitto* mosq, void* userdata, int rc){ + debug(1, "[MQTT]: connected"); + + //subscribe if requested + if(config.mqtt_enable_remote){ + char remotetopic[strlen(config.mqtt_topic)+8]; + snprintf(remotetopic,strlen(config.mqtt_topic)+8,"%s/remote",config.mqtt_topic); + mosquitto_subscribe(mosq,NULL,remotetopic,0); + } +} + +//helper function to publish under a topic and automatically append the main topic +void mqtt_publish(char* topic, char* data, uint32_t length){ + char fulltopic[strlen(config.mqtt_topic)+strlen(topic)+3]; + snprintf(fulltopic, strlen(config.mqtt_topic)+strlen(topic)+2, "%s/%s", config.mqtt_topic, topic); + debug(1, "[MQTT]: publishing under %s",fulltopic); + mosquitto_publish(mosq, NULL, fulltopic, length, data, 0, 0); +} + +//handler for incoming metadata +void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length){ + if(config.mqtt_publish_raw){ + uint32_t val; + char topic[] = "____/____"; + + val=htonl(type); + memcpy(topic,&val, 4); + val=htonl(code); + memcpy(topic+5,&val, 4); + mqtt_publish(topic, data, length); + } + if(config.mqtt_publish_parsed){ + if(type=='core'){ + switch (code) { + case 'asar': + mqtt_publish("artist", data, length); + break; + case 'asal': + mqtt_publish("album", data, length); + break; + case 'minm': + mqtt_publish("title", data, length); + break; + case 'asgn': + mqtt_publish("genre", data, length); + break; + case 'asfm': + mqtt_publish("format", data, length); + break; + } + }else if(type=='ssnc'){ + switch (code) { + case 'asal': + mqtt_publish("songalbum", data, length); + break; + case 'pvol': + mqtt_publish("volume", data, length); + break; + case 'clip': + mqtt_publish("client_ip", data, length); + break; + case 'pbeg': + mqtt_publish("play_start", data, length); + break; + case 'pend': + mqtt_publish("play_end", data, length); + break; + case 'pfls': + mqtt_publish("play_flush", data, length); + break; + case 'prsm': + mqtt_publish("play_resume", data, length); + break; + case 'PICT': + if(config.mqtt_publish_parsed){ + mqtt_publish("cover", data, length); + } + break; + } + } + } + + return; +} + + +int initialise_mqtt() { + debug(1, "Initialising MQTT"); + if(config.mqtt_hostname==NULL){ + debug(1, "[MQTT]: Not initialized, as the hostname is not set"); + return 0; + } + int keepalive = 60; + mosquitto_lib_init(); + if( !(mosq = mosquitto_new(config.service_name, true, NULL)) ){ + die("[MQTT]: FATAL: Could not create mosquitto object! %d\n", mosq); + } + + if( + config.mqtt_cafile != NULL || + config.mqtt_capath != NULL || + config.mqtt_certfile != NULL || + config.mqtt_keyfile != NULL + ){ + if(mosquitto_tls_set(mosq,config.mqtt_cafile, config.mqtt_capath, config.mqtt_certfile, config.mqtt_keyfile, NULL) != MOSQ_ERR_SUCCESS) { + die("[MQTT]: TLS Setup failed"); + } + } + + if( + config.mqtt_username != NULL || + config.mqtt_password != NULL + ){ + if(mosquitto_username_pw_set(mosq,config.mqtt_username,config.mqtt_password) != MOSQ_ERR_SUCCESS) { + die("[MQTT]: Username/Password set failed"); + } + } + mosquitto_log_callback_set(mosq, _cb_log); + + if(config.mqtt_enable_remote){ + mosquitto_message_callback_set(mosq, on_message); + } + + mosquitto_disconnect_callback_set(mosq, on_disconnect); + mosquitto_connect_callback_set(mosq, on_connect); + if(mosquitto_connect(mosq, config.mqtt_hostname, config.mqtt_port, keepalive)){ + inform("[MQTT]: Could not establish a mqtt connection"); + } + if(mosquitto_loop_start(mosq) != MOSQ_ERR_SUCCESS){ + inform("[MQTT]: Could start MQTT Main loop"); + } + + return 0; +} diff --git a/mqtt.h b/mqtt.h new file mode 100644 index 00000000..b5ec5ab0 --- /dev/null +++ b/mqtt.h @@ -0,0 +1,15 @@ +#ifndef MQTT_H +#define MQTT_H +#include +#include + + +int initialise_mqtt(); +void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length); +void mqtt_publish(char* topic, char* data, uint32_t length); +void mqtt_setup(); +void on_connect(struct mosquitto* mosq, void* userdata, int rc); +void on_disconnect(struct mosquitto* mosq, void* userdata, int rc); +void on_message(struct mosquitto* mosq, void* userdata, const struct mosquitto_message* msg); +void _cb_log(struct mosquitto *mosq, void *userdata, int level, const char *str); +#endif /* #ifndef MQTT_H */ diff --git a/rtsp.c b/rtsp.c index 0267e7f5..84c9ae20 100644 --- a/rtsp.c +++ b/rtsp.c @@ -67,6 +67,10 @@ #include "metadata_hub.h" #endif +#ifdef HAVE_MQTT +#include "mqtt.h" +#endif + #ifdef AF_INET6 #define INETx_ADDRSTRLEN INET6_ADDRSTRLEN #else @@ -1240,6 +1244,11 @@ void *metadata_thread_function(__attribute__((unused)) void *ignore) { metadata_process(pack.type, pack.code, pack.data, pack.length); #ifdef HAVE_METADATA_HUB metadata_hub_process_metadata(pack.type, pack.code, pack.data, pack.length); +#endif +#ifdef HAVE_MQTT + if(config.mqtt_enabled){ + mqtt_process_metadata(pack.type, pack.code, pack.data, pack.length); + } #endif } if (pack.carrier) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 2f7f1378..4f307aa8 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -144,6 +144,28 @@ metadata = // 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. }; +// How to enable the MQTT-metadata/remote-service +mqtt = +{ +// enabled = "no"; // set this to yes to enable the mqtt-metadata-service +// hostname = "iot.eclipse.org"; // Hostname of the MQTT Broker +// port = "1883"; +// username = NULL; //set this to a string with your username, to enable username authentication +// password = NULL; //set this to a string with your password, to enable username & password authentication +// capath = NULL; //set this to the folder with the CA-Certificates to be accepted for the server certificate. If not set, TLS is not used +// cafile = NULL; //this may be used as an (exclusive) alternative to capath with a single file for all ca-certificates +// certfile = NULL; //set this to a string to a user certificate to enable MQTT Client certificates. keyfile must also be set! +// keyfile = NULL; //private key for MQTT Client authentication +// topic = NULL; //MQTT topic where this instance of shairport-sync should publish. If not set, the general.name value is used. +// publish_raw = "no"; //whether to publish all available metadata under the codes given in the 'metadata' docs. +// publish_parsed = "no"; //whether to publish a small (but useful) subset of metadata under human-understandable topics +// Currently published topics:artist,album,title,genre,format,songalbum,volume,client_ip, +// Additionally, empty messages at the topics play_start,play_end,play_flush,play_resume are published +// publish_cover = "no"; //whether to publish the cover over mqtt in binary form. This may lead to a bit of load on the broker +// enable_remote; //whether to remote control via MQTT. RC is available under `topic`/remote. +// Available commands are "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup" +} + // Diagnostic settings. These are for diagnostic and debugging only. Normally you sould leave them commented out diagnostics = { diff --git a/shairport.c b/shairport.c index 69109e8b..192451d4 100644 --- a/shairport.c +++ b/shairport.c @@ -72,6 +72,10 @@ #include "dbus-service.h" #endif +#ifdef HAVE_MQTT +#include "mqtt.h" +#endif + #ifdef HAVE_MPRIS #include "mpris-service.h" #endif @@ -91,6 +95,24 @@ #include #endif +static inline int config_set_lookup_bool(config_t* cfg, char* where, int* dst) { + const char *str = 0; + if (config_lookup_string(cfg, where, &str)) { + if (strcasecmp(str, "no") == 0){ + (*dst)=0; + return 1; + }else if (strcasecmp(str, "yes") == 0){ + (*dst)=1; + return 1; + }else{ + die("Invalid %s option choice \"%s\". It should be \"yes\" or \"no\"", where, str); + return 0; + } + }else{ + return 0; + } +} + static int shutting_down = 0; char configuration_file_path[4096 + 1]; char actual_configuration_file_path[4096 + 1]; @@ -379,26 +401,11 @@ int parse_options(int argc, char **argv) { int daemonisewithout = 0; int daemonisewith = 0; /* Get the Daemonize setting. */ - if (config_lookup_string(config.cfg, "sessioncontrol.daemonize_with_pid_file", &str)) { - if (strcasecmp(str, "no") == 0) - daemonisewith = 0; - else if (strcasecmp(str, "yes") == 0) - daemonisewith = 1; - else - die("Invalid daemonize_with_pid_file option choice \"%s\". It should be \"yes\" or " - "\"no\""); - } + config_set_lookup_bool(config.cfg, "sessioncontrol.daemonize_with_pid_file", &daemonisewith); /* Get the Just_Daemonize setting. */ - if (config_lookup_string(config.cfg, "sessioncontrol.daemonize_without_pid_file", &str)) { - if (strcasecmp(str, "no") == 0) - daemonisewithout = 0; - else if (strcasecmp(str, "yes") == 0) - daemonisewithout = 1; - else - die("Invalid daemonize_without_pid_file option choice \"%s\". It should be \"yes\" or " - "\"no\""); - } + config_set_lookup_bool(config.cfg, "sessioncontrol.daemonize_without_pid_file", &daemonisewithout); + if ((daemonisewith) && (daemonisewithout)) die("Select either daemonize_with_pid_file or daemonize_without_pid_file -- you have " "selected both!"); @@ -467,15 +474,9 @@ int parse_options(int argc, char **argv) { } /* Get the statistics setting. */ - if (config_lookup_string(config.cfg, "general.statistics", &str)) { + if (!config_set_lookup_bool(config.cfg, "general.statistics", &(config.statistics_requested))) { warn("The \"general\" \"statistics\" setting is deprecated. Please use the \"diagnostics\" " "\"statistics\" setting instead."); - if (strcasecmp(str, "no") == 0) - config.statistics_requested = 0; - else if (strcasecmp(str, "yes") == 0) - config.statistics_requested = 1; - else - die("Invalid statistics option choice \"%s\". It should be \"yes\" or \"no\""); } /* The old drift tolerance setting. */ @@ -978,6 +979,68 @@ int parse_options(int argc, char **argv) { free(i3); free(vs); +#ifdef CONFIG_MQTT + int tmpval=0; + config_set_lookup_bool(config.cfg, "mqtt.enabled", &config.mqtt_enabled); + if(config.mqtt_enabled && !config.metadata_enabled){ + die("You need to have metadata enabled in order to use mqtt"); + } + if (config_lookup_string(config.cfg, "mqtt.hostname", &str)) { + config.mqtt_hostname = (char *)str; + //TODO: Document that, if this is false, whole mqtt func is disabled + } + if (config_lookup_int(config.cfg, "mqtt.port", &tmpval)) { + config.mqtt_port = tmpval; + }else{ + //TODO: Is this the correct way to set a default value? + config.mqtt_port = 1883; + } + + if (config_lookup_string(config.cfg, "mqtt.username", &str)) { + config.mqtt_username = (char *)str; + } + if (config_lookup_string(config.cfg, "mqtt.password", &str)) { + config.mqtt_password = (char *)str; + } + int capath=0; + if (config_lookup_string(config.cfg, "mqtt.capath", &str)) { + config.mqtt_capath = (char *)str; + capath=1; + } + if (config_lookup_string(config.cfg, "mqtt.cafile", &str)) { + if(capath) + die("Supply either mqtt cafile or mqtt capath -- you have supplied both!"); + config.mqtt_cafile = (char *)str; + } + int certkeynum=0; + if (config_lookup_string(config.cfg, "mqtt.certfile", &str)) { + config.mqtt_certfile = (char *)str; + certkeynum++; + } + if (config_lookup_string(config.cfg, "mqtt.keyfile", &str)) { + config.mqtt_keyfile = (char *)str; + certkeynum++; + } + if( certkeynum!=0 && certkeynum!=2){ + die("If you want to use TLS Client Authentication, you have to specify " + "mqtt.certfile AND mqtt.keyfile.\nYou have supplied only one of them.\n" + "If you do not want to use TLS Client Authentication, leave both empty." + ); + } + if (config_lookup_string(config.cfg, "mqtt.topic", &str)) { + config.mqtt_topic = (char *)str; + }else{ + int topic_length=1+strlen(config.service_name)+1; + char* topic=malloc(topic_length+1); + snprintf(topic,topic_length,"/%s/",config.service_name); + config.mqtt_topic = topic; + } + config_set_lookup_bool(config.cfg, "mqtt.publish_raw", &config.mqtt_publish_raw); + config_set_lookup_bool(config.cfg, "mqtt.publish_parsed", &config.mqtt_publish_parsed); + config_set_lookup_bool(config.cfg, "mqtt.publish_cover", &config.mqtt_publish_cover); + config_set_lookup_bool(config.cfg, "mqtt.enable_remote", &config.mqtt_enable_remote); +#endif + // now, check and calculate the pid directory #ifdef USE_CUSTOM_PID_DIR char *use_this_pid_dir = PIDDIR; @@ -1508,6 +1571,12 @@ int main(int argc, char **argv) { #endif #endif +#ifdef HAVE_MQTT + if(config.mqtt_enabled){ + initialise_mqtt(); + } +#endif + // daemon_log(LOG_INFO, "Successful Startup"); rtsp_listen_loop(); From c445510d5a322bf395a09d8f9bce77a730fc52b6 Mon Sep 17 00:00:00 2001 From: Till Zimmermann Date: Tue, 19 Jun 2018 20:25:22 +0200 Subject: [PATCH 07/17] minor improvements on the mqtt feature --- README.md | 3 +++ configure.ac | 4 ++-- mqtt.c | 40 +++++++++++++++++++++++----------------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 7301968e..20ef018f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ What else? * 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. +* A native mqtt interface for metadata and/or controls Heritage ------- @@ -124,6 +125,7 @@ The following libraries are required: Optional: * libsoxr * libalac (This is a library containing the Apple ALAC decoder.) +* libmosquitto (To enable the mqtt-metadata-interface) Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power — chances are an embedded processor won't be able to keep up. @@ -166,6 +168,7 @@ $ autoreconf -i -f - `--with-soxr` for libsoxr-based resampling. - `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location. - `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader. +- `--with-mqtt-client` to add support for publishing metadata and remote control via [MQTT](https://en.wikipedia.org/wiki/MQTT) - `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten. - `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use. - `--with-apple-alac` to include the Apple ALAC Decoder. diff --git a/configure.ac b/configure.ac index f79041c5..5b1520c7 100644 --- a/configure.ac +++ b/configure.ac @@ -315,7 +315,7 @@ AM_CONDITIONAL([USE_MPRIS_CLIENT], [test "x$HAS_MPRIS_CLIENT" = "x1"]) #AM_CONDITIONAL([USE_DBUS_CORE_AND_DACP], [test "x$HAS_MPRIS" = "x1" -o "x$HAS_DBUS" = "x1"]) # Look for mqtt flag -AC_ARG_WITH(mqtt-interface, [ --with-mqtt-interface = include support for the native Shairport Sync MQTT interface], [ +AC_ARG_WITH(mqtt-client, [ --with-mqtt-client = include support for the native Shairport Sync MQTT interface], [ AC_MSG_RESULT(>>Including MQTT support) HAS_MQTT=1 AC_DEFINE([HAVE_MQTT], 1, [Needed by the compiler.]) @@ -332,7 +332,7 @@ AC_ARG_WITH(mqtt-interface, [ --with-mqtt-interface = include support for the n AM_CONDITIONAL([USE_MQTT], [test "x$HAS_MQTT" = "x1"]) -if test "x$HAS_MPRIS" = "x1" -o "x$HAS_DBUS" = "x1" -o "x$HAS_MQTT"; then +if test "x$HAS_MPRIS" = "x1" || test "x$HAS_DBUS" = "x1" || test "x$HAS_MQTT" = "x1"; then AC_MSG_RESULT(>>Including the metadata hub) HAS_METADATA_HUB=1 AC_DEFINE([HAVE_METADATA_HUB], 1, [Needed by the compiler.]) diff --git a/mqtt.c b/mqtt.c index 478c476f..85efcf6e 100644 --- a/mqtt.c +++ b/mqtt.c @@ -15,11 +15,12 @@ #include "mqtt.h" //this holds the mosquitto client -struct mosquitto *mosq = NULL; +struct mosquitto *global_mosq = NULL; char *topic = NULL; //mosquitto logging -void _cb_log(struct mosquitto *mosq, void *userdata, int level, const char *str){ +void _cb_log( __attribute__((unused)) struct mosquitto *mosq, + __attribute__((unused)) void *userdata, int level, const char *str){ switch(level){ case MOSQ_LOG_DEBUG: debug(1, str); @@ -40,7 +41,8 @@ void _cb_log(struct mosquitto *mosq, void *userdata, int level, const char *str) } //mosquitto message handler -void on_message(struct mosquitto* mosq, void* userdata, const struct mosquitto_message* msg){ +void on_message( __attribute__((unused)) struct mosquitto* mosq, + __attribute__((unused)) void* userdata, const struct mosquitto_message* msg){ //null-terminate the payload char payload[msg->payloadlen+1]; @@ -59,7 +61,7 @@ void on_message(struct mosquitto* mosq, void* userdata, const struct mosquitto_m //send command if it's a valid one while(commands[it++]!=NULL){ - if( msg->payloadlen>=strlen(commands[it]) && + if( (size_t)msg->payloadlen>=strlen(commands[it]) && strncmp(msg->payload, commands[it], strlen(commands[it]))==0 ){ debug(1, "[MQTT]: DACP Command: %s\n",commands[it]); @@ -69,11 +71,15 @@ void on_message(struct mosquitto* mosq, void* userdata, const struct mosquitto_m } } -void on_disconnect(struct mosquitto* mosq, void* userdata, int rc){ +void on_disconnect( __attribute__((unused)) struct mosquitto* mosq, + __attribute__((unused)) void* userdata, + __attribute__((unused)) int rc){ debug(1, "[MQTT]: disconnected"); } -void on_connect(struct mosquitto* mosq, void* userdata, int rc){ +void on_connect(struct mosquitto* mosq, + __attribute__((unused)) void* userdata, + __attribute__((unused)) int rc){ debug(1, "[MQTT]: connected"); //subscribe if requested @@ -89,7 +95,7 @@ void mqtt_publish(char* topic, char* data, uint32_t length){ char fulltopic[strlen(config.mqtt_topic)+strlen(topic)+3]; snprintf(fulltopic, strlen(config.mqtt_topic)+strlen(topic)+2, "%s/%s", config.mqtt_topic, topic); debug(1, "[MQTT]: publishing under %s",fulltopic); - mosquitto_publish(mosq, NULL, fulltopic, length, data, 0, 0); + mosquitto_publish(global_mosq, NULL, fulltopic, length, data, 0, 0); } //handler for incoming metadata @@ -167,8 +173,8 @@ int initialise_mqtt() { } int keepalive = 60; mosquitto_lib_init(); - if( !(mosq = mosquitto_new(config.service_name, true, NULL)) ){ - die("[MQTT]: FATAL: Could not create mosquitto object! %d\n", mosq); + if( !(global_mosq = mosquitto_new(config.service_name, true, NULL)) ){ + die("[MQTT]: FATAL: Could not create mosquitto object! %d\n", global_mosq); } if( @@ -177,7 +183,7 @@ int initialise_mqtt() { config.mqtt_certfile != NULL || config.mqtt_keyfile != NULL ){ - if(mosquitto_tls_set(mosq,config.mqtt_cafile, config.mqtt_capath, config.mqtt_certfile, config.mqtt_keyfile, NULL) != MOSQ_ERR_SUCCESS) { + if(mosquitto_tls_set(global_mosq,config.mqtt_cafile, config.mqtt_capath, config.mqtt_certfile, config.mqtt_keyfile, NULL) != MOSQ_ERR_SUCCESS) { die("[MQTT]: TLS Setup failed"); } } @@ -186,22 +192,22 @@ int initialise_mqtt() { config.mqtt_username != NULL || config.mqtt_password != NULL ){ - if(mosquitto_username_pw_set(mosq,config.mqtt_username,config.mqtt_password) != MOSQ_ERR_SUCCESS) { + if(mosquitto_username_pw_set(global_mosq,config.mqtt_username,config.mqtt_password) != MOSQ_ERR_SUCCESS) { die("[MQTT]: Username/Password set failed"); } } - mosquitto_log_callback_set(mosq, _cb_log); + mosquitto_log_callback_set(global_mosq, _cb_log); if(config.mqtt_enable_remote){ - mosquitto_message_callback_set(mosq, on_message); + mosquitto_message_callback_set(global_mosq, on_message); } - mosquitto_disconnect_callback_set(mosq, on_disconnect); - mosquitto_connect_callback_set(mosq, on_connect); - if(mosquitto_connect(mosq, config.mqtt_hostname, config.mqtt_port, keepalive)){ + mosquitto_disconnect_callback_set(global_mosq, on_disconnect); + mosquitto_connect_callback_set(global_mosq, on_connect); + if(mosquitto_connect(global_mosq, config.mqtt_hostname, config.mqtt_port, keepalive)){ inform("[MQTT]: Could not establish a mqtt connection"); } - if(mosquitto_loop_start(mosq) != MOSQ_ERR_SUCCESS){ + if(mosquitto_loop_start(global_mosq) != MOSQ_ERR_SUCCESS){ inform("[MQTT]: Could start MQTT Main loop"); } From 3bfafa48a2ba635156bd84ac1894310855251f7f Mon Sep 17 00:00:00 2001 From: Till Zimmermann Date: Tue, 19 Jun 2018 21:08:22 +0200 Subject: [PATCH 08/17] Fixed handling w/o config file. --- scripts/shairport-sync.conf | 2 +- shairport.c | 125 +++++++++++++++++++----------------- 2 files changed, 66 insertions(+), 61 deletions(-) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 4f307aa8..4ff2bd4c 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -162,7 +162,7 @@ mqtt = // Currently published topics:artist,album,title,genre,format,songalbum,volume,client_ip, // Additionally, empty messages at the topics play_start,play_end,play_flush,play_resume are published // publish_cover = "no"; //whether to publish the cover over mqtt in binary form. This may lead to a bit of load on the broker -// enable_remote; //whether to remote control via MQTT. RC is available under `topic`/remote. +// enable_remote = "no"; //whether to remote control via MQTT. RC is available under `topic`/remote. // Available commands are "command", "beginff", "beginrew", "mutetoggle", "nextitem", "previtem", "pause", "playpause", "play", "stop", "playresume", "shuffle_songs", "volumedown", "volumeup" } diff --git a/shairport.c b/shairport.c index 192451d4..c5450ad7 100644 --- a/shairport.c +++ b/shairport.c @@ -880,6 +880,63 @@ int parse_options(int argc, char **argv) { } #endif +#ifdef CONFIG_MQTT + int tmpval=0; + config_set_lookup_bool(config.cfg, "mqtt.enabled", &config.mqtt_enabled); + if(config.mqtt_enabled && !config.metadata_enabled){ + die("You need to have metadata enabled in order to use mqtt"); + } + if (config_lookup_string(config.cfg, "mqtt.hostname", &str)) { + config.mqtt_hostname = (char *)str; + //TODO: Document that, if this is false, whole mqtt func is disabled + } + if (config_lookup_int(config.cfg, "mqtt.port", &tmpval)) { + config.mqtt_port = tmpval; + }else{ + //TODO: Is this the correct way to set a default value? + config.mqtt_port = 1883; + } + + if (config_lookup_string(config.cfg, "mqtt.username", &str)) { + config.mqtt_username = (char *)str; + } + if (config_lookup_string(config.cfg, "mqtt.password", &str)) { + config.mqtt_password = (char *)str; + } + int capath=0; + if (config_lookup_string(config.cfg, "mqtt.capath", &str)) { + config.mqtt_capath = (char *)str; + capath=1; + } + if (config_lookup_string(config.cfg, "mqtt.cafile", &str)) { + if(capath) + die("Supply either mqtt cafile or mqtt capath -- you have supplied both!"); + config.mqtt_cafile = (char *)str; + } + int certkeynum=0; + if (config_lookup_string(config.cfg, "mqtt.certfile", &str)) { + config.mqtt_certfile = (char *)str; + certkeynum++; + } + if (config_lookup_string(config.cfg, "mqtt.keyfile", &str)) { + config.mqtt_keyfile = (char *)str; + certkeynum++; + } + if( certkeynum!=0 && certkeynum!=2){ + die("If you want to use TLS Client Authentication, you have to specify " + "mqtt.certfile AND mqtt.keyfile.\nYou have supplied only one of them.\n" + "If you do not want to use TLS Client Authentication, leave both empty." + ); + } + + if(config_lookup_string(config.cfg, "mqtt.topic", &str)){ + config.mqtt_topic = (char *)str; + } + config_set_lookup_bool(config.cfg, "mqtt.publish_raw", &config.mqtt_publish_raw); + config_set_lookup_bool(config.cfg, "mqtt.publish_parsed", &config.mqtt_publish_parsed); + config_set_lookup_bool(config.cfg, "mqtt.publish_cover", &config.mqtt_publish_cover); + config_set_lookup_bool(config.cfg, "mqtt.enable_remote", &config.mqtt_enable_remote); +#endif free(config_file_real_path); } @@ -978,67 +1035,15 @@ int parse_options(int argc, char **argv) { free(i2); free(i3); free(vs); - + #ifdef CONFIG_MQTT - int tmpval=0; - config_set_lookup_bool(config.cfg, "mqtt.enabled", &config.mqtt_enabled); - if(config.mqtt_enabled && !config.metadata_enabled){ - die("You need to have metadata enabled in order to use mqtt"); - } - if (config_lookup_string(config.cfg, "mqtt.hostname", &str)) { - config.mqtt_hostname = (char *)str; - //TODO: Document that, if this is false, whole mqtt func is disabled - } - if (config_lookup_int(config.cfg, "mqtt.port", &tmpval)) { - config.mqtt_port = tmpval; - }else{ - //TODO: Is this the correct way to set a default value? - config.mqtt_port = 1883; - } - - if (config_lookup_string(config.cfg, "mqtt.username", &str)) { - config.mqtt_username = (char *)str; - } - if (config_lookup_string(config.cfg, "mqtt.password", &str)) { - config.mqtt_password = (char *)str; - } - int capath=0; - if (config_lookup_string(config.cfg, "mqtt.capath", &str)) { - config.mqtt_capath = (char *)str; - capath=1; - } - if (config_lookup_string(config.cfg, "mqtt.cafile", &str)) { - if(capath) - die("Supply either mqtt cafile or mqtt capath -- you have supplied both!"); - config.mqtt_cafile = (char *)str; - } - int certkeynum=0; - if (config_lookup_string(config.cfg, "mqtt.certfile", &str)) { - config.mqtt_certfile = (char *)str; - certkeynum++; - } - if (config_lookup_string(config.cfg, "mqtt.keyfile", &str)) { - config.mqtt_keyfile = (char *)str; - certkeynum++; - } - if( certkeynum!=0 && certkeynum!=2){ - die("If you want to use TLS Client Authentication, you have to specify " - "mqtt.certfile AND mqtt.keyfile.\nYou have supplied only one of them.\n" - "If you do not want to use TLS Client Authentication, leave both empty." - ); - } - if (config_lookup_string(config.cfg, "mqtt.topic", &str)) { - config.mqtt_topic = (char *)str; - }else{ - int topic_length=1+strlen(config.service_name)+1; - char* topic=malloc(topic_length+1); - snprintf(topic,topic_length,"/%s/",config.service_name); - config.mqtt_topic = topic; - } - config_set_lookup_bool(config.cfg, "mqtt.publish_raw", &config.mqtt_publish_raw); - config_set_lookup_bool(config.cfg, "mqtt.publish_parsed", &config.mqtt_publish_parsed); - config_set_lookup_bool(config.cfg, "mqtt.publish_cover", &config.mqtt_publish_cover); - config_set_lookup_bool(config.cfg, "mqtt.enable_remote", &config.mqtt_enable_remote); + // mqtt topic was not set. As we have the service name just now, set it + if(config.mqtt_topic==NULL){ + int topic_length=1+strlen(config.service_name)+1; + char* topic=malloc(topic_length+1); + snprintf(topic,topic_length,"/%s/",config.service_name); + config.mqtt_topic = topic; + } #endif // now, check and calculate the pid directory From 3d1086ea2e698af134cb02ef297129ddd637f286 Mon Sep 17 00:00:00 2001 From: Till Zimmermann Date: Wed, 20 Jun 2018 18:58:34 +0200 Subject: [PATCH 09/17] Fixed not connected bug, added version string --- common.c | 3 +++ mqtt.c | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/common.c b/common.c index 306b3c2f..118b6c0f 100644 --- a/common.c +++ b/common.c @@ -1128,6 +1128,9 @@ char *get_version_string() { #ifdef CONFIG_METADATA strcat(version_string, "-metadata"); #endif +#ifdef CONFIG_MQTT + strcat(version_string, "-mqtt"); +#endif #ifdef HAVE_DBUS strcat(version_string, "-dbus"); #endif diff --git a/mqtt.c b/mqtt.c index 85efcf6e..c08a4318 100644 --- a/mqtt.c +++ b/mqtt.c @@ -17,6 +17,7 @@ //this holds the mosquitto client struct mosquitto *global_mosq = NULL; char *topic = NULL; +int connected = 0; //mosquitto logging void _cb_log( __attribute__((unused)) struct mosquitto *mosq, @@ -74,12 +75,14 @@ void on_message( __attribute__((unused)) struct mosquitto* mosq, void on_disconnect( __attribute__((unused)) struct mosquitto* mosq, __attribute__((unused)) void* userdata, __attribute__((unused)) int rc){ + connected = 0; debug(1, "[MQTT]: disconnected"); } void on_connect(struct mosquitto* mosq, __attribute__((unused)) void* userdata, __attribute__((unused)) int rc){ + connected = 1; debug(1, "[MQTT]: connected"); //subscribe if requested @@ -95,11 +98,26 @@ void mqtt_publish(char* topic, char* data, uint32_t length){ char fulltopic[strlen(config.mqtt_topic)+strlen(topic)+3]; snprintf(fulltopic, strlen(config.mqtt_topic)+strlen(topic)+2, "%s/%s", config.mqtt_topic, topic); debug(1, "[MQTT]: publishing under %s",fulltopic); - mosquitto_publish(global_mosq, NULL, fulltopic, length, data, 0, 0); + + int rc; + if((rc=mosquitto_publish(global_mosq, NULL, fulltopic, length, data, 0, 0))!=MOSQ_ERR_SUCCESS) { + switch(rc){ + case MOSQ_ERR_NO_CONN: + debug(1, "[MQTT]: Publish failed: not connected to broker"); + break; + default: + debug(1, "[MQTT]: Publish failed: unknown error"); + break; + } + } } //handler for incoming metadata void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length){ + if(global_mosq==NULL || connected!=1){ + debug(3, "[MQTT]: Client not connected, skipping metadata handling"); + return; + } if(config.mqtt_publish_raw){ uint32_t val; char topic[] = "____/____"; From cd85578b32cafcc39c1671d87c2bc6c8399cfb9f Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Wed, 20 Jun 2018 22:22:27 +0100 Subject: [PATCH 10/17] Update RELEASENOTES.md --- RELEASENOTES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d7484523..57948050 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,3 +1,8 @@ +Version 3.2.d63 +==== +**Enhancement** +* An [MQTT](https://en.wikipedia.org/wiki/MQTT) client interface is introduced, with support for metadata and for some remote control commands, thanks to the work [Till Zimmermann](https://github.com/tillz). MQTT is a fairly often used protocol in FOSS home-automation projects (as well as in commercial ones), and as Shairport Sync is often used in these setups, this adds client support for this protocol. + Version 3.2d62 ==== **Bug Fix** From aed836b5fc4679c10f0645d6177003224485ba07 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Wed, 20 Jun 2018 22:23:51 +0100 Subject: [PATCH 11/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20ef018f..fd487a77 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ What else? * 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. -* A native mqtt interface for metadata and/or controls +* A native [MQTT](https://en.wikipedia.org/wiki/MQTT) interface for metadata and/or controls. Heritage ------- From 51019541859a4cabb8d1106b467fc4d1fa2e7fc1 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Wed, 20 Jun 2018 22:26:11 +0100 Subject: [PATCH 12/17] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fd487a77..0fb02c9b 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ What else? * Fast Response — With hardware volume control, response is instantaneous; otherwise the response time is 0.15 seconds with `alsa`, 0.35 seconds with `sndio`. * 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 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. +* A native [MQTT](https://en.wikipedia.org/wiki/MQTT) interface for metadata and/or controls. * 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. -* A native [MQTT](https://en.wikipedia.org/wiki/MQTT) interface for metadata and/or controls. Heritage ------- @@ -125,7 +125,7 @@ The following libraries are required: Optional: * libsoxr * libalac (This is a library containing the Apple ALAC decoder.) -* libmosquitto (To enable the mqtt-metadata-interface) +* libmosquitto (This is to enable the mqtt-metadata-interface.) Many Linux distributions have Avahi and OpenSSL already in place, so normally it probably makes sense to choose those options rather than tinysvcmdns or mbed TLS. The `libsoxr` library is available in recent Linux distributions, but it requires lots of processor power — chances are an embedded processor won't be able to keep up. @@ -168,7 +168,7 @@ $ autoreconf -i -f - `--with-soxr` for libsoxr-based resampling. - `--with-piddir` for specifying where the PID file should be stored. This directory is normally chosen automatically. The directory must be writable. If you use this option, you may have to edit the init script to search for the PID file in your new location. - `--with-metadata` to add support for Shairport Sync to pipe metadata to a compatible application of your choice. See https://github.com/mikebrady/shairport-sync-metadata-reader for a sample metadata reader. -- `--with-mqtt-client` to add support for publishing metadata and remote control via [MQTT](https://en.wikipedia.org/wiki/MQTT) +- `--with-mqtt-client` to add support for publishing metadata and remote control via [MQTT](https://en.wikipedia.org/wiki/MQTT). - `--with-configfiles` to install a configuration file and a separate sample file at the `make install` stage. Default is to install. An existing `/etc/shairport-sync.conf` will not be overwritten. - `--with-pkg-config` to use pkg-config to find libraries. Default is to use pkg-config — this option is for special purpose use. - `--with-apple-alac` to include the Apple ALAC Decoder. From 0d48a3d96aa8974d6783b4248b11098edaee98f8 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Thu, 21 Jun 2018 16:15:53 +0100 Subject: [PATCH 13/17] improve the timed mutex lock code and make it compatible with os x, we hope. --- common.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/common.c b/common.c index 118b6c0f..2c8810c9 100644 --- a/common.c +++ b/common.c @@ -1013,22 +1013,47 @@ void sps_nanosleep(const time_t sec, const long nanosec) { debug(1, "Error in sps_nanosleep of %d sec and %ld nanoseconds: %d.", sec, nanosec, errno); } +// Mac OS X doesn't have pthread_mutex_timedlock +// Also note that timing must be relative to CLOCK_REALTIME + +#ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, const char *debugmessage, int debuglevel) { - useconds_t time_to_wait = dally_time; - int r = pthread_mutex_trylock(mutex); - while ((r == EBUSY) && (time_to_wait > 0)) { - useconds_t st = time_to_wait; - if (st > 20000) - st = 20000; - sps_nanosleep(0, st * 1000); - time_to_wait -= st; - r = pthread_mutex_trylock(mutex); - } + struct timespec tn; + clock_gettime(CLOCK_REALTIME, &tn); + uint64_t tnfpsec = tn.tv_sec; + if (tnfpsec > 0x100000000) + warn("clock_gettime seconds overflow!"); + uint64_t tnfpnsec = tn.tv_nsec; + if (tnfpnsec > 0x100000000) + warn("clock_gettime nanoseconds seconds overflow!"); + tnfpsec = tnfpsec << 32; + tnfpnsec = tnfpnsec << 32; + tnfpnsec = tnfpnsec / 1000000000; + + uint64_t time_now_in_fp = tnfpsec + tnfpnsec; // types okay + + uint64_t dally_time_in_fp = dally_time; // microseconds + dally_time_in_fp = (dally_time_in_fp << 32) / 1000000; // convert to fp format + uint64_t time_then = time_now_in_fp + dally_time_in_fp; + + uint64_t time_then_nsec = time_then & 0xffffffff; // remove integral part + time_then_nsec = time_then_nsec * 1000000000; // multiply fractional part to nanoseconds + + struct timespec timeoutTime; + + time_then = time_then >> 32; // get the seconds + time_then_nsec = time_then_nsec >> 32; // and the nanoseconds + + timeoutTime.tv_sec = time_then; + timeoutTime.tv_nsec = time_then_nsec; + + int r = pthread_mutex_timedlock(mutex, &timeoutTime); + if ((r != 0) && (debugmessage != NULL)) { char errstr[1000]; - if (r == EBUSY) + if (r == ETIMEDOUT) debug(debuglevel, "waiting for a mutex, maximum expected time of %d microseconds exceeded \"%s\".", dally_time, debugmessage); @@ -1038,6 +1063,36 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, } return r; } +#endif +#ifdef COMPILE_FOR_OSX +int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, + const char *debugmessage, int debuglevel) { + + useconds_t time_to_wait = dally_time; + int r = pthread_mutex_trylock(mutex); + while ((r == EBUSY) && (time_to_wait > 0)) { + useconds_t st = time_to_wait; + if (st > 1000) + st = 1000; + sps_nanosleep(0, st * 1000); + time_to_wait -= st; + r = pthread_mutex_trylock(mutex); + } + if ((r != 0) && (debugmessage != NULL)) { + char errstr[1000]; + if (r == EBUSY) { + debug(debuglevel, + "waiting for a mutex, maximum expected time of %d microseconds exceeded \"%s\".", + dally_time, debugmessage); + r = ETIMEDOUT; // for compatibility + } else { + debug(debuglevel, "error %d: \"%s\" waiting for a mutex: \"%s\".", r, + strerror_r(r, errstr, sizeof(errstr)), debugmessage); + } + } + return r; +} +#endif int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char *filename, const int line, int debuglevel) { @@ -1047,7 +1102,7 @@ int _debug_mutex_lock(pthread_mutex_t *mutex, useconds_t dally_time, const char snprintf(dstring, sizeof(dstring), "%s:%d", filename, line); debug(3, "debug_mutex_lock at \"%s\".", dstring); int result = sps_pthread_mutex_timedlock(mutex, dally_time, dstring, debuglevel); - if (result == EBUSY) { + if (result == ETIMEDOUT) { result = pthread_mutex_lock(mutex); uint64_t time_delay = get_absolute_time_in_fp() - time_at_start; uint64_t divisor = (uint64_t)1 << 32; From 365843faa6449850419654745694a6727d6139ae Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Thu, 21 Jun 2018 17:44:26 +0100 Subject: [PATCH 14/17] beginning to add a progress string item to the native dbus interface --- dbus-service.c | 11 +++++++++++ metadata_hub.c | 10 ++++++++++ metadata_hub.h | 1 + org.gnome.ShairportSync.xml | 1 + 4 files changed, 23 insertions(+) diff --git a/dbus-service.c b/dbus-service.c index 20150abd..1a952ee9 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -43,6 +43,17 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) shairport_sync_advanced_remote_control_set_available(shairportSyncAdvancedRemoteControlSkeleton, FALSE); } + + if (argc->progress_string) { + debug(1, "Check progress string"); + th = shairport_sync_remote_control_get_progress_string( + shairportSyncRemoteControlSkeleton); + if ((th == NULL) || (strcasecmp(th, argc->progress_string) != 0)) { + debug(1, "Progress string should be changed"); + shairport_sync_remote_control_set_progress_string( + shairportSyncRemoteControlSkeleton, argc->progress_string); + } + } switch (argc->player_state) { case PS_NOT_AVAILABLE: diff --git a/metadata_hub.c b/metadata_hub.c index 4f3fc0f8..d1006b43 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -517,6 +517,16 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin } break; */ + case 'prgr': + if ((metadata_store.progress_string == NULL) || + (strncmp(metadata_store.progress_string, data, length) != 0)) { + metadata_hub_modify_prolog(); + release_char_string(&metadata_store.progress_string); + metadata_store.progress_string = strndup(data, length); + debug(1, "MH Progress String set to: \"%s\"", metadata_store.progress_string); + metadata_hub_modify_epilog(1); + } + break; case 'svip': if ((metadata_store.server_ip == NULL) || (strncmp(metadata_store.server_ip, data, length) != 0)) { diff --git a/metadata_hub.h b/metadata_hub.h index 8d003703..66368f98 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -56,6 +56,7 @@ typedef struct metadata_bundle { char *client_ip; // IP number used by the audio source (i.e. the "client"), which is also the DACP // server char *server_ip; // IP number used by Shairport Sync + char *progress_string; // progress string, emitted by the source from time to time int player_thread_active; // true if a play thread is running int dacp_server_active; // true if there's a reachable DACP server (assumed to be the Airplay // client) ; false otherwise diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index ae404be7..aa693fd5 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -33,6 +33,7 @@ + From 7d446832bff86198a33d4749d343ba43bf66fe06 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Thu, 21 Jun 2018 18:04:24 +0100 Subject: [PATCH 15/17] Quieten some debug messages --- dbus-service.c | 4 ++-- metadata_hub.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dbus-service.c b/dbus-service.c index 1a952ee9..ebcd3371 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -45,11 +45,11 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) } if (argc->progress_string) { - debug(1, "Check progress string"); + // debug(1, "Check progress string"); th = shairport_sync_remote_control_get_progress_string( shairportSyncRemoteControlSkeleton); if ((th == NULL) || (strcasecmp(th, argc->progress_string) != 0)) { - debug(1, "Progress string should be changed"); + // debug(1, "Progress string should be changed"); shairport_sync_remote_control_set_progress_string( shairportSyncRemoteControlSkeleton, argc->progress_string); } diff --git a/metadata_hub.c b/metadata_hub.c index d1006b43..9d648fcd 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -523,7 +523,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin metadata_hub_modify_prolog(); release_char_string(&metadata_store.progress_string); metadata_store.progress_string = strndup(data, length); - debug(1, "MH Progress String set to: \"%s\"", metadata_store.progress_string); + debug(2, "MH Progress String set to: \"%s\"", metadata_store.progress_string); metadata_hub_modify_epilog(1); } break; From 49700bb979db0f2183dbb0ade808fd9d6ab58259 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Thu, 21 Jun 2018 18:07:23 +0100 Subject: [PATCH 16/17] Update RELEASENOTES.md --- RELEASENOTES.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 57948050..32de93f1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,3 +1,11 @@ +Version 3.2.d64 +==== +**Enhancement** +* An extra property has been added to the RemoteControl section of the native Shairport Sync D-Bus interface. It is the "progress" metadata emitted from time to time by the AirPlay source and consisting of RTP timestamps for the start of the current play sequence, the current play point and the end of the play sequence. + +**Bug Fix** +* Code to monitor the amount of time a mutex lock request took is improved. + Version 3.2.d63 ==== **Enhancement** From 63dbbd481250a1675db6ffd8fdec480d13f71226 Mon Sep 17 00:00:00 2001 From: Mike Brady Date: Thu, 21 Jun 2018 18:08:05 +0100 Subject: [PATCH 17/17] Update configure.ac --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 5b1520c7..23b4baac 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [3.2d62], [mikebrady@eircom.net]) +AC_INIT([shairport-sync], [3.2d64], [mikebrady@eircom.net]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h])