From 8947eefaef7f976b7b943733d89902f5c51791d4 Mon Sep 17 00:00:00 2001 From: Mke Brady Date: Wed, 18 Feb 2015 19:23:21 +0000 Subject: [PATCH] Initial metadata code from abrasive --- Makefile.am | 2 +- common.h | 1 + configure.ac | 2 +- mdns.h | 2 +- metadata.c | 165 +++++++++++++++++++++++++++++++++++++++++++++++++++ metadata.h | 22 +++++++ player.h | 5 ++ rtsp.c | 109 +++++++++++++++++++++++++++++++--- shairport.c | 5 +- 9 files changed, 302 insertions(+), 11 deletions(-) create mode 100644 metadata.c create mode 100644 metadata.h diff --git a/Makefile.am b/Makefile.am index ce1f0ba4..391ab104 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,7 +1,7 @@ SUBDIRS = man bin_PROGRAMS = shairport-sync -shairport_sync_SOURCES = shairport.c rtsp.c mdns.c mdns_external.c common.c rtp.c player.c alac.c audio.c audio_dummy.c audio_pipe.c +shairport_sync_SOURCES = shairport.c metadata.c rtsp.c mdns.c mdns_external.c common.c rtp.c player.c alac.c audio.c audio_dummy.c audio_pipe.c if USE_CUSTOMPIDDIR AM_CFLAGS= \ diff --git a/common.h b/common.h index 3a3bca76..9871465d 100644 --- a/common.h +++ b/common.h @@ -41,6 +41,7 @@ enum stuffing_type { typedef struct { char *password; char *apname; + char *meta_dir; uint8_t hw_addr[6]; int port; int resyncthreshold; // if it get's out of whack my more than this, resync. Zero means never resync. diff --git a/configure.ac b/configure.ac index fc8ac3ab..d40495e2 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], [2.2.2], [mikebrady@eircom.net]) +AC_INIT([shairport-sync], [2.2.3], [mikebrady@eircom.net]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) diff --git a/mdns.h b/mdns.h index b3f00391..68c45180 100644 --- a/mdns.h +++ b/mdns.h @@ -13,7 +13,7 @@ typedef struct { void (*mdns_unregister)(void); } mdns_backend; -#define MDNS_RECORD "tp=UDP", "sm=false", "ek=1", "et=0,1", "cn=0,1", "ch=2", \ +#define MDNS_RECORD "tp=UDP", "sm=false", "ek=1", "et=0,1", "cn=0,1", "ch=2", "md=0,1", \ "ss=16", "sr=44100", "vn=3", "txtvers=1", \ config.password ? "pw=true" : "pw=false" diff --git a/metadata.c b/metadata.c new file mode 100644 index 00000000..30cde946 --- /dev/null +++ b/metadata.c @@ -0,0 +1,165 @@ +/* + * Metadate structure and utility methods. This file is part of Shairport. + * Copyright (c) Benjamin Maus 2013 + * 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 "metadata.h" + +metadata player_meta; +static int fd = -1; +static int dirty = 0; + +void metadata_set(char** field, const char* value) { + if (*field) { + if (!strcmp(*field, value)) + return; + free(*field); + } + *field = strdup(value); + dirty = 1; +} + +void metadata_open(void) { + if (!config.meta_dir) + return; + + const char fn[] = "now_playing"; + size_t pl = strlen(config.meta_dir) + 1 + strlen(fn); + + char* path = malloc(pl+1); + snprintf(path, pl+1, "%s/%s", config.meta_dir, fn); + + if (mkfifo(path, 0644) && errno != EEXIST) + die("Could not create metadata FIFO %s", path); + + fd = open(path, O_WRONLY | O_NONBLOCK); + if (fd < 0) + debug(1, "Could not open metadata FIFO %s. Will try again later.", path); + + free(path); +} + +static void metadata_close(void) { + close(fd); + fd = -1; +} + +static void print_one(const char *name, const char *value) { + write(fd, name, strlen(name)); + write(fd, "=", 1); + if (value) + write(fd, value, strlen(value)); + write(fd, "\n", 1); +} + +#define write_one(name) \ + print_one(#name, player_meta.name) + +void metadata_write(void) { + int ret; + + // readers may go away and come back + if (fd < 0) + metadata_open(); + if (fd < 0) + return; + + if (!dirty) + return; + + dirty = 0; + + write_one(artist); + write_one(title); + write_one(album); + write_one(artwork); + write_one(genre); + write_one(comment); + + ret = write(fd, "\n", 1); + if (ret < 1) // no reader + metadata_close(); +} + +void metadata_cover_image(const char *buf, int len, const char *ext) { + if (!config.meta_dir) + return; + + if (buf) { + debug(1, "Cover Art set\n"); + } else { + debug(1, "Cover Art cleared\n"); + return; + } + + uint8_t img_md5[16]; + MD5_CTX ctx; + MD5_Init(&ctx); + MD5_Update(&ctx, buf, len); + MD5_Final(img_md5, &ctx); + + char img_md5_str[33]; + int i; + for (i = 0; i < 16; i++) + sprintf(&img_md5_str[i*2], "%02x", (uint8_t)img_md5[i]); + + char *dir = config.meta_dir; + char *prefix = "cover-"; + + size_t pl = strlen(dir) + 1 + strlen(prefix) + strlen(img_md5_str) + 1 + strlen(ext); + + char *path = malloc(pl+1); + snprintf(path, pl+1, "%s/%s%s.%s", dir, prefix, img_md5_str, ext); + + int cover_fd = open(path, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR); + + if (cover_fd < 0) { + warn("Could not open file %s for writing cover art", path); + return; + } + + if (write(cover_fd, buf, len) < len) { + warn("writing %s failed\n", path); + free(path); + return; + } + close(cover_fd); + + debug(1, "Cover Art file is %s\n", path); + metadata_set(&player_meta.artwork, path+strlen(dir)+1); + + free(path); +} diff --git a/metadata.h b/metadata.h new file mode 100644 index 00000000..3ca5595c --- /dev/null +++ b/metadata.h @@ -0,0 +1,22 @@ +#ifndef _METADATA_H +#define _METADATA_H + +#include + +typedef struct { + char *artist; + char *title; + char *album; + char *artwork; + char *comment; + char *genre; +} metadata; + +void metadata_set(char** field, const char* value); +void metadata_open(void); +void metadata_write(void); +void metadata_cover_image(const char *buf, int len, const char *ext); + +extern metadata player_meta; + +#endif // _METADATA_H diff --git a/player.h b/player.h index 04109ecb..aebfaa58 100644 --- a/player.h +++ b/player.h @@ -2,6 +2,7 @@ #define _PLAYER_H #include "audio.h" +#include "metadata.h" typedef struct { uint8_t aesiv[16], aeskey[16]; @@ -20,6 +21,10 @@ void player_volume(double f); void player_flush(uint32_t timestamp); void player_resync(void); +void player_metadata(); +void player_cover_image(char *buf, int len, char *ext); +void player_cover_clear(); + void player_put_packet(seq_t seqno,uint32_t timestamp, uint8_t *data, int len); #endif //_PLAYER_H diff --git a/rtsp.c b/rtsp.c index 2ae4dff0..b398b586 100644 --- a/rtsp.c +++ b/rtsp.c @@ -54,6 +54,7 @@ #include "player.h" #include "rtp.h" #include "mdns.h" +#include "metadata.h" #ifdef AF_INET6 #define INETx_ADDRSTRLEN INET6_ADDRSTRLEN @@ -557,11 +558,8 @@ static void handle_ignore(rtsp_conn_info *conn, resp->respcode = 200; } -static void handle_set_parameter(rtsp_conn_info *conn, - rtsp_message *req, rtsp_message *resp) { - if (!req->contentlength) - debug(1, "received empty SET_PARAMETER request."); - +static void handle_set_parameter_parameter(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { char *cp = req->content; int cp_left = req->contentlength; char *next; @@ -571,14 +569,111 @@ static void handle_set_parameter(rtsp_conn_info *conn, if (!strncmp(cp, "volume: ", 8)) { float volume = atof(cp + 8); - // debug(1, "volume: %f.", volume); + debug(1, "volume: %f\n", volume); player_volume(volume); + } else if(!strncmp(cp, "progress: ", 10)) { + char *progress = cp + 10; + debug(1, "progress: %s\n", progress); } else { - debug(1, "unrecognised parameter: >>%s<< (%d).", cp, strlen(cp)); + debug(1, "unrecognised parameter: >>%s<< (%d)\n", cp, strlen(cp)); } cp = next; } +} +static void handle_set_parameter_metadata(rtsp_conn_info *conn, + rtsp_message *req, + rtsp_message *resp) { + char *cp = req->content; + int cl = req->contentlength; + + unsigned int off = 8; + + while (off < cl) { + char tag[5]; + strncpy(tag, cp+off, 4); + tag[4] = '\0'; + off += 4; + + uint32_t vl = ntohl(*(uint32_t *)(cp+off)); + off += sizeof(uint32_t); + + char *val = malloc(vl+1); + strncpy(val, cp+off, vl); + val[vl] = '\0'; + off += vl; + + debug(2, "Tag: %s Content: %s\n", tag, val); + + if (!strncmp(tag, "asal ", 4)) { + debug(1, "META Album: %s\n", val); + metadata_set(&player_meta.album, val); + } else if (!strncmp(tag, "asar ", 4)) { + debug(1, "META Artist: %s\n", val); + metadata_set(&player_meta.artist, val); + } else if (!strncmp(tag, "ascm ", 4)) { + debug(1, "META Comment: %s\n", val); + metadata_set(&player_meta.comment, val); + } else if (!strncmp(tag, "asgn ", 4)) { + debug(1, "META Genre: %s\n", val); + metadata_set(&player_meta.genre, val); + } else if (!strncmp(tag, "minm ", 4)) { + debug(1, "META Title: %s\n", val); + metadata_set(&player_meta.title, val); + } + + free(val); + } + + metadata_write(); +} + +static void handle_set_parameter_coverart(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { + char *cp = req->content; + int cl = req->contentlength; + + char *ct = msg_get_header(req, "Content-Type"); + + if (!strncmp(ct, "image/jpeg", 10)) { + metadata_cover_image(cp, cl, "jpg"); + } else if (!strncmp(ct, "image/png", 9)) { + metadata_cover_image(cp, cl, "png"); + } else { + metadata_cover_image(NULL, 0, NULL); + } +} + +static void handle_set_parameter(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { + if (!req->contentlength) + debug(1, "received empty SET_PARAMETER request\n"); + + char *ct = msg_get_header(req, "Content-Type"); + + if (ct) { + debug(2, "SET_PARAMETER Content-Type: %s\n", ct); + + if (!strncmp(ct, "application/x-dmap-tagged", 25)) { + debug(1, "received metadata tags in SET_PARAMETER request\n"); + + handle_set_parameter_metadata(conn, req, resp); + } else if (!strncmp(ct, "image/jpeg", 10) || + !strncmp(ct, "image/png", 9) || + !strncmp(ct, "image/none", 10)) { + debug(1, "received image in SET_PARAMETER request\n"); + + handle_set_parameter_coverart(conn, req, resp); + } else if (!strncmp(ct, "text/parameters", 15)) { + debug(1, "received parameters in SET_PARAMETER request\n"); + + handle_set_parameter_parameter(conn, req, resp); + } else { + debug(1, "received unknown Content-Type %s in SET_PARAMETER request\n", ct); + } + } else { + debug(1, "missing Content-Type header in SET_PARAMETER request\n"); + } resp->respcode = 200; } diff --git a/shairport.c b/shairport.c index f382195e..f8c8b5d2 100644 --- a/shairport.c +++ b/shairport.c @@ -51,6 +51,7 @@ #include "rtsp.h" #include "rtp.h" #include "mdns.h" +#include "metadata.h" #include #include @@ -215,6 +216,7 @@ int parse_options(int argc, char **argv) { { "timeout", 't', POPT_ARG_INT, &config.timeout, 0, NULL } , { "password", 0, POPT_ARG_STRING, &config.password, 0, NULL } , { "tolerance", 0, POPT_ARG_INT, &config.tolerance, 0, NULL } , + { "meta-dir", 'M', POPT_ARG_STRING, &config.meta_dir, 0, NULL } , POPT_AUTOHELP { NULL, 0, 0, NULL, 0 } }; @@ -271,7 +273,8 @@ int parse_options(int argc, char **argv) { debug(2,"busy timeout time is %d.",config.timeout); debug(2,"tolerance is %d frames.",config.tolerance); debug(2,"password is \"%s\".",config.password); - + debug(2,"metadata directory is \"%s\".",config.meta_dir); + return optind+1; }