Added metadata support for Shairplay-sync/Airplay interface

This commit is contained in:
frafall 2017-12-01 20:29:38 +01:00
parent ce17b0010a
commit c820f01ca7
6 changed files with 312 additions and 7 deletions

120
externals/base64.h vendored Normal file
View file

@ -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;
}

View file

@ -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

View file

@ -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("</metatags>"));
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("<metatags>"));
}
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

View file

@ -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 <expat.h>
#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<TageEntry> 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
};

View file

@ -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<msg::StreamTags> 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_)

View file

@ -96,7 +96,6 @@ protected:
std::thread thread_;
std::atomic<bool> active_;
virtual void worker() = 0;
virtual bool sleep(int32_t ms);
void setState(const ReaderState& newState);