diff --git a/Makefile b/Makefile index dd4dc8b07..1fa20f0c4 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ CFLAGS ?= -O2 -Wall PREFIX ?= /usr/local -SRCS := shairport.c daemon.c rtsp.c mdns.c mdns_external.c mdns_tinysvcmdns.c common.c rtp.c metadata.c player.c alac.c audio.c audio_dummy.c audio_pipe.c tinysvcmdns.c +SRCS := shairport.c daemon.c rtsp.c mdns.c mdns_external.c mdns_tinysvcmdns.c common.c rtp.c metadata.c dacp.c player.c alac.c audio.c audio_dummy.c audio_pipe.c tinysvcmdns.c DEPS := config.mk alac.h audio.h common.h daemon.h getopt_long.h mdns.h metadata.h player.h rtp.h rtsp.h tinysvcmdns.h ifdef CONFIG_SNDIO diff --git a/README.md b/README.md index 32133aa6f..f0d20d7ab 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,44 @@ An example:: genre=Rock comment= +DACP +---- +Most devices allow the AirPlay receiver to have basic remote control over the stream. This is done via simple HTTP GET requests from the +receiver to the source device. The device sends a `DACP-ID` header to allow the receiver to find hostname and port numbers from mDNS, as well as an +`Active-Remote` header that must be used as an authentication key for issuing control commands. Similar to the Metadata feature outlined above, +this information can be read from a fifo after a device announces an audio stream. Using the `-D ` flag will cause a fifo named +`dacp` to be created in the directory specified. This fifo will have a similar format as the metadata. + +Fields: +``` +dacp_id=CFAE09D9DA7527E9 +active_remote=3951361179 +``` + +To issue control commands, you must do an mDNS lookup for `iTunes_Ctrl_._dacp._tcp.local`. iTunes will always advertise on port 3689 so depending +on the environment, it may be possible to skip the mDNS lookup if you already know the source IP or hostname. iOS devices seem to choose a random high-numbered +port. Once you have found the host and port number, you may issue HTTP GET requests in the following format: + +`http://:/ctrl-int/1/` + +Where `` may be one of: +``` +beginff +beginrew +mutetoggle +nextitem +previtem +pause +playpause +play +stop +playresume (after issuing beginff or beginrew) +shuffle_songs +volumedown +volumeup +``` +Your HTTP request must include an `Active-Remote` header with the value obtained from the fifo. + Thanks ------ diff --git a/common.h b/common.h index 811d3f9cf..2f3dfebe1 100644 --- a/common.h +++ b/common.h @@ -31,6 +31,7 @@ typedef struct { char *cmd_start, *cmd_stop; int cmd_blocking; char *meta_dir; + char *dacp_dir; char *pidfile; char *logfile; char *errfile; diff --git a/dacp.c b/dacp.c new file mode 100644 index 000000000..0f88c9712 --- /dev/null +++ b/dacp.c @@ -0,0 +1,111 @@ +/* + * DACP remote methods. This file is part of Shairport. + * Copyright (c) 2014 Josh Butts + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "dacp.h" + +dacpdata player_dacp; +static int fd = -1; +static int dirty = 0; + +void dacp_set(char** field, const char* value) { + if (*field) { + if (!strcmp(*field, value)) + return; + free(*field); + } + *field = strdup(value); + dirty = 1; +} + +void dacp_open(void) { + if (!config.dacp_dir) + return; + + const char fn[] = "dacp"; + size_t pl = strlen(config.dacp_dir) + 1 + strlen(fn); + + char* path = malloc(pl+1); + snprintf(path, pl+1, "%s/%s", config.dacp_dir, fn); + + if (mkfifo(path, 0644) && errno != EEXIST) + die("Could not create dacp FIFO %s", path); + + fd = open(path, O_WRONLY | O_NONBLOCK); + if (fd < 0) + debug(1, "Could not open dacp FIFO %s. Will try again later.", path); + + free(path); +} + +static void dacp_close(void) { + close(fd); + fd = -1; +} + +static void print_one(const char *name, const char *value) { + write_unchecked(fd, name, strlen(name)); + write_unchecked(fd, "=", 1); + if (value) + write_unchecked(fd, value, strlen(value)); + write_unchecked(fd, "\n", 1); +} + +#define write_one(name) \ + print_one(#name, player_dacp.name) + +void dacp_write(void) { + int ret; + + // readers may go away and come back + if (fd < 0) + dacp_open(); + if (fd < 0) + return; + + if (!dirty) + return; + + dirty = 0; + + write_one(dacp_id); + write_one(active_remote); + + ret = write(fd, "\n", 1); + if (ret < 1) // no reader + dacp_close(); +} \ No newline at end of file diff --git a/dacp.h b/dacp.h new file mode 100644 index 000000000..06025e3f6 --- /dev/null +++ b/dacp.h @@ -0,0 +1,17 @@ +#ifndef _DACP_H +#define _DACP_H + +#include + +typedef struct { + char *dacp_id; + char *active_remote; +} dacpdata; + +void dacp_set(char** field, const char* value); +void dacp_open(void); +void dacp_write(void); + +extern dacpdata player_dacp; + +#endif // _DACP_H diff --git a/rtsp.c b/rtsp.c index 41c8d87cf..8f8249713 100644 --- a/rtsp.c +++ b/rtsp.c @@ -46,6 +46,7 @@ #include "rtp.h" #include "mdns.h" #include "metadata.h" +#include "dacp.h" #ifdef AF_INET6 #define INETx_ADDRSTRLEN INET6_ADDRSTRLEN @@ -604,6 +605,16 @@ static void handle_announce(rtsp_conn_info *conn, for (i=0; istream.fmtp)/sizeof(conn->stream.fmtp[0]); i++) conn->stream.fmtp[i] = atoi(strsep(&pfmtp, " \t")); + int c; + for (c = 0; c < req->nheaders; c++) { + if (!strcmp(req->name[c], "DACP-ID")) { + dacp_set(&player_dacp.dacp_id, req->value[c]); + } else if (!strcmp(req->name[c], "Active-Remote")) { + dacp_set(&player_dacp.active_remote, req->value[c]); + } + } + dacp_write(); + resp->respcode = 200; } diff --git a/shairport.c b/shairport.c index 8a3f89d39..c7e28481f 100644 --- a/shairport.c +++ b/shairport.c @@ -38,6 +38,7 @@ #include "mdns.h" #include "getopt_long.h" #include "metadata.h" +#include "dacp.h" static const char *version = #include "version.h" @@ -107,6 +108,7 @@ void usage(char *progname) { printf(" -E, --on-stop=COMMAND run a shell command when playback ends\n"); printf(" -w, --wait-cmd block while the shell command(s) run\n"); printf(" -M, --meta-dir=DIR set a directory to write metadata and album cover art to\n"); + printf(" -D, --dacp-dir=DIR set a directory to write DACP remote control info to\n"); printf(" -o, --output=BACKEND select audio output method\n"); printf(" -m, --mdns=BACKEND force the use of BACKEND to advertise the service\n"); @@ -138,12 +140,13 @@ int parse_options(int argc, char **argv) { {"wait-cmd", no_argument, NULL, 'w'}, {"meta-dir", required_argument, NULL, 'M'}, {"mdns", required_argument, NULL, 'm'}, + {"dacp-dir", required_argument, NULL, 'D'}, {NULL, 0, NULL, 0} }; int opt; while ((opt = getopt_long(argc, argv, - "+hdvP:l:e:p:a:k:o:b:B:E:M:wm:", + "+hdvP:l:e:p:a:k:o:b:B:E:M:D:wm:", long_options, NULL)) > 0) { switch (opt) { default: @@ -197,6 +200,9 @@ int parse_options(int argc, char **argv) { case 'm': config.mdns_name = optarg; break; + case 'D': + config.dacp_dir = optarg; + break; } } return optind; @@ -315,6 +321,9 @@ int main(int argc, char **argv) { if (config.meta_dir) metadata_open(); + if (config.dacp_dir) + dacp_open(); + rtsp_listen_loop(); // should not.