From c820f01ca7632e3d778cd0028908376e6152fa2f Mon Sep 17 00:00:00 2001 From: frafall Date: Fri, 1 Dec 2017 20:29:38 +0100 Subject: [PATCH] Added metadata support for Shairplay-sync/Airplay interface --- externals/base64.h | 120 +++++++++++++++++++++ server/Makefile | 6 +- server/streamreader/airplayStream.cpp | 143 +++++++++++++++++++++++++- server/streamreader/airplayStream.h | 43 ++++++++ server/streamreader/pcmStream.cpp | 6 +- server/streamreader/pcmStream.h | 1 - 6 files changed, 312 insertions(+), 7 deletions(-) create mode 100644 externals/base64.h diff --git a/externals/base64.h b/externals/base64.h new file mode 100644 index 00000000..feda6a0e --- /dev/null +++ b/externals/base64.h @@ -0,0 +1,120 @@ +/* + base64.cpp and base64.h + + Copyright (C) 2004-2008 René Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + René Nyffenegger rene.nyffenegger@adp-gmbh.ch + +*/ + +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + +static inline bool is_base64(unsigned char c) { + return (isalnum(c) || (c == '+') || (c == '/')); +} + +std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) { + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + while (in_len--) { + char_array_3[i++] = *(bytes_to_encode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for(i = 0; (i <4) ; i++) + ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) + { + for(j = i; j < 3; j++) + char_array_3[j] = '\0'; + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (j = 0; (j < i + 1); j++) + ret += base64_chars[char_array_4[j]]; + + while((i++ < 3)) + ret += '='; + + } + + return ret; + +} +std::string base64_decode(std::string const& encoded_string) { + int in_len = encoded_string.size(); + int i = 0; + int j = 0; + int in_ = 0; + unsigned char char_array_4[4], char_array_3[3]; + std::string ret; + + while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i ==4) { + for (i = 0; i <4; i++) + char_array_4[i] = base64_chars.find(char_array_4[i]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; (i < 3); i++) + ret += char_array_3[i]; + i = 0; + } + } + + if (i) { + for (j = i; j <4; j++) + char_array_4[j] = 0; + + for (j = 0; j <4; j++) + char_array_4[j] = base64_chars.find(char_array_4[j]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; + } + + return ret; +} + diff --git a/server/Makefile b/server/Makefile index 18ef0d43..94f9cb35 100644 --- a/server/Makefile +++ b/server/Makefile @@ -35,7 +35,7 @@ DEBUG=-O3 CXXFLAGS += $(ADD_CFLAGS) -std=c++0x -Wall -Wno-unused-function $(DEBUG) -DASIO_STANDALONE -DVERSION=\"$(VERSION)\" -I. -I.. -isystem ../externals/asio/asio/include -I../externals/popl/include -I../externals/aixlog/include -I../externals/jsonrpcpp/lib -I../externals -LDFLAGS = -lvorbis -lvorbisenc -logg -lFLAC +LDFLAGS = -lvorbis -lvorbisenc -logg -lFLAC OBJ = snapServer.o config.o controlServer.o controlSession.o streamServer.o streamSession.o streamreader/streamUri.o streamreader/streamManager.o streamreader/pcmStream.o streamreader/pipeStream.o streamreader/fileStream.o streamreader/processStream.o streamreader/airplayStream.o streamreader/spotifyStream.o streamreader/watchdog.o encoder/encoderFactory.o encoder/flacEncoder.o encoder/pcmEncoder.o encoder/oggEncoder.o ../common/sampleFormat.o ../message/pcmChunk.o ../externals/jsonrpcpp/lib/jsonrp.o @@ -81,8 +81,8 @@ else CXX = g++ STRIP = strip -CXXFLAGS += -DHAS_AVAHI -DHAS_DAEMON -pthread -LDFLAGS = -lrt -lvorbis -lvorbisenc -logg -lFLAC -lavahi-client -lavahi-common -static-libgcc -static-libstdc++ +CXXFLAGS += -DHAS_AVAHI -DHAS_DAEMON -DHAS_EXPAT -pthread +LDFLAGS = -lrt -lexpat -lvorbis -lvorbisenc -logg -lFLAC -lavahi-client -lavahi-common -static-libgcc -static-libstdc++ OBJ += ../common/daemon.o publishZeroConf/publishAvahi.o endif diff --git a/server/streamreader/airplayStream.cpp b/server/streamreader/airplayStream.cpp index 24ecae22..511cd8f3 100644 --- a/server/streamreader/airplayStream.cpp +++ b/server/streamreader/airplayStream.cpp @@ -22,16 +22,37 @@ #include "common/utils.h" #include "aixlog.hpp" +#ifdef HAS_EXPAT +#include "base64.h" +#endif using namespace std; +static string hex2str(string input) +{ + typedef unsigned char byte; + unsigned long x = strtoul(input.c_str(), 0, 16); + byte a[] = {byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x), 0}; + return string((char *)a); +} - +/* + * Expat is used in metadata parsing from Shairport-sync. + * Without HAS_EXPAT defined no parsing will occur. + * + * This is currently defined in airplayStream.h, prolly should + * move to Makefile? + */ AirplayStream::AirplayStream(PcmListener* pcmListener, const StreamUri& uri) : ProcessStream(pcmListener, uri), port_(5000) { logStderr_ = true; + pipePath_ = "/tmp/shairmeta." + to_string(getpid()); + cout << "Pipe [" << pipePath_ << "]\n"; + + // XXX: Check if pipe exists, delete or throw error + sampleFormat_ = SampleFormat("44100:16:2"); uri_.query["sampleformat"] = sampleFormat_.getFormat(); @@ -39,16 +60,85 @@ AirplayStream::AirplayStream(PcmListener* pcmListener, const StreamUri& uri) : P string devicename = uri_.getQuery("devicename", "Snapcast"); params_wo_port_ = "--name=\"" + devicename + "\" --output=stdout"; + params_wo_port_ += " --metadata-pipename " + pipePath_; params_ = params_wo_port_ + " --port=" + cpt::to_string(port_); + + pipeReaderThread_ = thread(&AirplayStream::pipeReader, this); + pipeReaderThread_.detach(); } AirplayStream::~AirplayStream() { + parse(string("")); + XML_ParserFree(parser_); +} + +int AirplayStream::parse(string line) +{ + enum XML_Status result; + + if((result = XML_Parse(parser_, line.c_str(), line.length(), false)) == XML_STATUS_ERROR) + { + XML_ParserFree(parser_); + createParser(); + } + return result; +} + +void AirplayStream::createParser() +{ + parser_ = XML_ParserCreate("UTF-8"); + XML_SetElementHandler(parser_, element_start, element_end); + XML_SetCharacterDataHandler(parser_, data); + XML_SetUserData(parser_, this); + + // Make an outer element to keep parsing going + parse(string("")); +} + +void AirplayStream::push() +{ + string data = entry_->data; + if(entry_->isBase64 && entry_->length > 0) + data = base64_decode(data); + + if(entry_->type == "ssnc" && entry_->code == "mdst") + jtag_ = json(); + + if(entry_->code == "asal") jtag_["ALBUM"] = data; + if(entry_->code == "asar") jtag_["ARTIST"] = data; + if(entry_->code == "minm") jtag_["TITLE"] = data; + + if(entry_->type == "ssnc" && entry_->code == "mden"){ + //LOG(INFO) << "metadata=" << jtag_.dump(4) << "\n"; + setMeta(jtag_); + } } -void AirplayStream::initExeAndPath(const std::string& filename) +void AirplayStream::pipeReader() +{ + createParser(); + + while(true) + { + ifstream pipe(pipePath_); + + if(pipe){ + string line; + + while(getline(pipe, line)){ + parse(line); + } + } + + // Wait a little until we try to open it again + this_thread::sleep_for(chrono::milliseconds(500)); + } +} + +void AirplayStream::initExeAndPath(const string& filename) { path_ = ""; exe_ = findExe(filename); @@ -86,3 +176,52 @@ void AirplayStream::onStderrMsg(const char* buffer, size_t n) } } +#ifdef HAS_EXPAT +void XMLCALL AirplayStream::element_start(void *userdata, const char *element_name, const char **attr) +{ + AirplayStream *self = (AirplayStream *)userdata; + string name(element_name); + + self->buf_.assign(""); + if(name == "item") self->entry_.reset(new TageEntry); + + for(int i = 0; attr[i]; i += 2){ + string name(attr[i]); + string value(attr[i+1]); + if(name == "encoding") + self->entry_->isBase64 = (value == "base64"); // Quick & dirty.. + } +} + +void XMLCALL AirplayStream::element_end(void *userdata, const char *element_name) +{ + AirplayStream *self = (AirplayStream *)userdata; + string name(element_name); + + if(name == "code") + self->entry_->code.assign(hex2str(self->buf_)); + + else if(name == "type") + self->entry_->type.assign(hex2str(self->buf_)); + + else if(name == "length") + self->entry_->length = strtoul(self->buf_.c_str(), 0, 10); + + else if(name == "data") + self->entry_->data = self->buf_; + + else if(name == "item") + self->push(); + + else if(name == "metatags") ; + else cout << "Unknown tag <" << name << ">\n"; +} + +void XMLCALL AirplayStream::data(void *userdata, const char *content, int length) +{ + AirplayStream *self = (AirplayStream *)userdata; + string value(content, (size_t)length); + self->buf_.append(value); +} +#endif + diff --git a/server/streamreader/airplayStream.h b/server/streamreader/airplayStream.h index 8a027d3c..5b995c17 100644 --- a/server/streamreader/airplayStream.h +++ b/server/streamreader/airplayStream.h @@ -21,7 +21,28 @@ #include "processStream.h" +/* + * Expat is used in metadata parsing from Shairport-sync. + * Without HAS_EXPAT defined no parsing will occur. + */ +#ifdef HAS_EXPAT +#include +#endif + +class TageEntry +{ +public: + TageEntry(): isBase64(false), length(0) {} + + std::string code; + std::string type; + std::string data; + bool isBase64; + int length; +}; + /// Starts shairport-sync and reads PCM data from stdout + /** * Starts librespot, reads PCM data from stdout, and passes the data to an encoder. * Implements EncoderListener to get the encoded data. @@ -37,10 +58,32 @@ public: virtual ~AirplayStream(); protected: +#ifdef HAS_EXPAT + XML_Parser parser_; +#endif + std::unique_ptr entry_; + std::string buf_; + json jtag_; + + void pipeReader(); +#ifdef HAS_EXPAT + int parse(std::string line); + void createParser(); + void push(); +#endif + virtual void onStderrMsg(const char* buffer, size_t n); virtual void initExeAndPath(const std::string& filename); size_t port_; + std::string pipePath_; std::string params_wo_port_; + std::thread pipeReaderThread_; + +#ifdef HAS_EXPAT + static void XMLCALL element_start(void *userdata, const char *element_name, const char **attr); + static void XMLCALL element_end(void *userdata, const char *element_name); + static void XMLCALL data(void *userdata, const char *content, int length); +#endif }; diff --git a/server/streamreader/pcmStream.cpp b/server/streamreader/pcmStream.cpp index 1762e5f1..b26733da 100644 --- a/server/streamreader/pcmStream.cpp +++ b/server/streamreader/pcmStream.cpp @@ -56,7 +56,9 @@ PcmStream::PcmStream(PcmListener* pcmListener, const StreamUri& uri) : else dryoutMs_ = 2000; - meta_.reset(new msg::StreamTags()); + //meta_.reset(new msg::StreamTags()); + //meta_->msg["stream"] = name_; + setMeta(json()); } @@ -187,6 +189,8 @@ std::shared_ptr PcmStream::getMeta() const void PcmStream::setMeta(json jtag) { meta_.reset(new msg::StreamTags(jtag)); + meta_->msg["STREAM"] = name_; + LOG(INFO) << "metadata=" << meta_->msg.dump(4) << "\n"; // Trigger a stream update if (pcmListener_) diff --git a/server/streamreader/pcmStream.h b/server/streamreader/pcmStream.h index 7c4d4917..6f151e66 100644 --- a/server/streamreader/pcmStream.h +++ b/server/streamreader/pcmStream.h @@ -96,7 +96,6 @@ protected: std::thread thread_; std::atomic active_; - virtual void worker() = 0; virtual bool sleep(int32_t ms); void setState(const ReaderState& newState);