Support for text tags through snapcast.

Only added tags Spotify stream input from modified librespot
for now.
This commit is contained in:
frafall 2017-11-29 14:36:28 +01:00
parent af3ea660b9
commit 67083975b0
10 changed files with 206 additions and 92 deletions

View file

@ -30,9 +30,11 @@ else
TARGET_DIR ?= /usr
endif
# Simplify building debuggable executables 'make DEBUG=-g STRIP=echo'
DEBUG=-O3
# Include the process.hpp for usage in meta tags callback
CXXFLAGS += $(ADD_CFLAGS) -std=c++0x -Wall -Wno-unused-function -O3 -DASIO_STANDALONE -DVERSION=\"$(VERSION)\" -I. -I.. -isystem ../externals/asio/asio/include -I../externals/popl/include -I../externals/aixlog/include -I../server/streamreader
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
LDFLAGS = -logg -lFLAC
OBJ = snapClient.o stream.o clientConnection.o timeProvider.o player/player.o decoder/pcmDecoder.o decoder/oggDecoder.o decoder/flacDecoder.o controller.o ../message/pcmChunk.o ../common/sampleFormat.o

View file

@ -34,7 +34,7 @@
using namespace std;
Controller::Controller(const std::string& hostId, size_t instance) : MessageReceiver(),
Controller::Controller(const std::string& hostId, size_t instance, std::shared_ptr<MetadataAdapter> meta) : MessageReceiver(),
hostId_(hostId),
instance_(instance),
active_(false),
@ -42,6 +42,7 @@ Controller::Controller(const std::string& hostId, size_t instance) : MessageRece
stream_(nullptr),
decoder_(nullptr),
player_(nullptr),
meta_(meta),
serverSettings_(nullptr),
async_exception_(nullptr)
{
@ -138,21 +139,8 @@ void Controller::onMessageReceived(ClientConnection* connection, const msg::Base
streamTags_.reset(new msg::StreamTags());
streamTags_->deserialize(baseMessage, buffer);
LOG(INFO) << "Stream tags: artist = <" << streamTags_->getArtist() << ">, album = <" << streamTags_->getAlbum() << ">, track = <" << streamTags_->getTrack() << ">\n";
// And we should trigger the meta tags script if given
struct stat buffer;
if(stat(meta_callback_.c_str(), &buffer) == 0)
{
LOG(INFO) << "About to execute meta tag callback script!\n";
// Check if its there, set environment and execute it
// Probably can use the process thing from streamreader
}
else
{
LOG(INFO) << "Meta tag callback script not found!\n";
}
if(meta_)
meta_->push(streamTags_->msg);
}
if (baseMessage.type != message_type::kTime)
@ -175,11 +163,10 @@ bool Controller::sendTimeSyncMessage(long after)
}
void Controller::start(const PcmDevice& pcmDevice, const std::string& host, size_t port, int latency, const std::string& meta_callback)
void Controller::start(const PcmDevice& pcmDevice, const std::string& host, size_t port, int latency)
{
pcmDevice_ = pcmDevice;
latency_ = latency;
meta_callback_ = meta_callback;
clientConnection_.reset(new ClientConnection(this, host, port));
controllerThread_ = thread(&Controller::worker, this);
}

View file

@ -35,6 +35,7 @@
#endif
#include "clientConnection.h"
#include "stream.h"
#include "metadata.h"
/// Forwards PCM data to the audio player
@ -47,8 +48,8 @@
class Controller : public MessageReceiver
{
public:
Controller(const std::string& clientId, size_t instance);
void start(const PcmDevice& pcmDevice, const std::string& host, size_t port, int latency, const std::string& meta_callback);
Controller(const std::string& clientId, size_t instance, std::shared_ptr<MetadataAdapter> meta);
void start(const PcmDevice& pcmDevice, const std::string& host, size_t port, int latency);
void stop();
/// Implementation of MessageReceiver.
@ -74,6 +75,7 @@ private:
std::shared_ptr<Stream> stream_;
std::unique_ptr<Decoder> decoder_;
std::unique_ptr<Player> player_;
std::shared_ptr<MetadataAdapter> meta_;
std::shared_ptr<msg::ServerSettings> serverSettings_;
std::shared_ptr<msg::StreamTags> streamTags_;
std::shared_ptr<msg::CodecHeader> headerChunk_;

109
client/metadata.h Normal file
View file

@ -0,0 +1,109 @@
/***
This file is part of snapcast
Copyright (C) 2014-2017 Johannes Pohl
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef METADATA_H
#define METADATA_H
//#include <sys/types.h>
//#include <sys/stat.h>
//#include <fcntl.h>
//#include <limits.h>
//#include <cstdlib>
//#include <cstring>
//#include <iostream>
//#include <streambuf>
#include "json.hpp"
// Prefix used in output
#define METADATA std::string("metadata")
/*
* Implement a generic metadata output handler
*/
using json = nlohmann::json;
/*
* Base class, prints to stdout
*/
class MetadataAdapter
{
public:
MetadataAdapter()
{
reset();
}
void reset()
{
_msg.reset(new json);
}
std::string serialize()
{
return METADATA + ":" + _msg->dump() + "\n";
}
void tag(std::string name, std::string value)
{
(*_msg)[name] = value;
}
std::string operator[](std::string key)
{
try {
return (*_msg)[key];
}
catch (std::domain_error&)
{
return std::string();
}
}
virtual int push()
{
std::cout << serialize() << "\n";
return 0;
}
int push(json& jtag)
{
_msg.reset(new json(jtag));
return push();
}
protected:
std::shared_ptr<json> _msg;
};
/*
* Send metadata to stderr as json
*/
class MetaStderrAdapter: public MetadataAdapter
{
public:
using MetadataAdapter::push;
int push()
{
std::cerr << serialize() << "\n";
return 0;
}
};
#endif

View file

@ -33,6 +33,7 @@
#include "common/signalHandler.h"
#include "common/strCompat.h"
#include "common/utils.h"
#include "metadata.h"
using namespace std;
@ -73,7 +74,7 @@ int main (int argc, char **argv)
int exitcode = EXIT_SUCCESS;
try
{
string meta_callback("");
string meta_script("");
string soundcard("default");
string host("");
size_t port(1704);
@ -88,7 +89,8 @@ int main (int argc, char **argv)
auto listSwitch = op.add<Switch>("l", "list", "list pcm devices");
/*auto soundcardValue =*/ op.add<Value<string>>("s", "soundcard", "index or name of the soundcard", "default", &soundcard);
#endif
/*auto metaValue =*/ op.add<Value<string>>("m", "meta", "script to call on meta tags", "", &meta_callback);
auto metaStderr = op.add<Switch>("e", "mstderr", "send metadata to stderr");
//auto metaHook = op.add<Value<string>>("m", "mhook", "script to call on meta tags", "", &meta_script);
/*auto hostValue =*/ op.add<Value<string>>("h", "host", "server hostname or ip address", "", &host);
/*auto portValue =*/ op.add<Value<size_t>>("p", "port", "server port", 1704, &port);
#ifdef HAS_DAEMON
@ -144,6 +146,9 @@ int main (int argc, char **argv)
if (instance <= 0)
std::invalid_argument("instance id must be >= 1");
// XXX: Only one metadata option must be set
AixLog::Log::init<AixLog::SinkNative>("snapclient", AixLog::Severity::trace, AixLog::Type::special);
if (debugOption->is_set())
{
@ -228,11 +233,17 @@ int main (int argc, char **argv)
#endif
}
std::unique_ptr<Controller> controller(new Controller(hostIdValue->value(), instance));
// Setup metadata handling
std::shared_ptr<MetadataAdapter> meta;
meta.reset(new MetadataAdapter);
if(metaStderr)
meta.reset(new MetaStderrAdapter);
std::unique_ptr<Controller> controller(new Controller(hostIdValue->value(), instance, meta));
if (!g_terminated)
{
LOG(INFO) << "Latency: " << latency << "\n";
controller->start(pcmDevice, host, port, latency, meta_callback);
controller->start(pcmDevice, host, port, latency);
while(!g_terminated)
chronos::sleep(100);
controller->stop();

View file

@ -41,68 +41,33 @@ namespace msg
class StreamTags : public JsonMessage
{
public:
/*
Usage:
json jtag = {
{"artist", "Pink Floyd"},
{"album", "Dark Side of the Moon"},
{"track", "Money"},
{"spotifyid", "akjhasi7wehke7698"},
{"musicbrainzid", "akjhasi7wehke7698"},
};
this->meta_.reset(new msg::StreamTags(jtag));
Stream input can decide on tags, IDK... but smart
to use some common naming scheme
*/
StreamTags(json j) : JsonMessage(message_type::kStreamTags)
{
msg = j;
}
StreamTags() : JsonMessage(message_type::kStreamTags)
{
msg["meta_artist"] = "";
msg["meta_album"] = "";
msg["meta_track"] = "";
msg["meta_albumart"] = "";
}
virtual ~StreamTags()
{
}
json toJson() const
{
json j = {
{"artist", getArtist()},
{"album", getAlbum()},
{"track", getTrack()},
};
return j;
}
std::string getArtist() const
{
return msg["meta_artist"];
}
std::string getAlbum() const
{
return msg["meta_album"];
}
std::string getTrack() const
{
return msg["meta_track"];
}
std::string getAlbumArt() const
{
return msg["meta_albumart"];
}
void setArtist(std::string artist)
{
msg["meta_artist"] = artist;
}
void setAlbum(std::string album)
{
msg["meta_album"] = album;
}
void setTrack(std::string track)
{
msg["meta_track"] = track;
}
// Ascii encoded image XXX: more details
void setAlbumArt(std::string art)
{
msg["meta_albumart"] = art;
}
};
}

View file

@ -30,8 +30,11 @@ else
TARGET_DIR ?= /usr
endif
# Simplify building debuggable executables 'make DEBUG=-g STRIP=echo'
DEBUG=-O3
CXXFLAGS += $(ADD_CFLAGS) -std=c++0x -Wall -Wno-unused-function -O3 -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
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
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

View file

@ -43,7 +43,10 @@ void StreamServer::onMetaChanged(const PcmStream* pcmStream)
{
/// Notification: {"jsonrpc":"2.0","method":"Stream.OnMetadata","params":{"id":"stream 1", "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}
// Send meta to all connected clients
const auto meta = pcmStream->getMeta();
//cout << "metadata = " << meta->msg.dump(3) << "\n";
for (auto s : sessions_)
{
if (s->pcmStream().get() == pcmStream)
@ -51,9 +54,9 @@ void StreamServer::onMetaChanged(const PcmStream* pcmStream)
}
LOG(INFO) << "onMetaChanged (" << pcmStream->getName() << ")\n";
json notification = jsonrpcpp::Notification("Stream.OnMetadata", jsonrpcpp::Parameter("id", pcmStream->getId(), "meta", meta->toJson())).to_json();
json notification = jsonrpcpp::Notification("Stream.OnMetadata", jsonrpcpp::Parameter("id", pcmStream->getId(), "meta", meta->msg)).to_json();
controlServer_->send(notification.dump(), NULL);
cout << "Notification: " << notification.dump() << "\n";
////cout << "Notification: " << notification.dump() << "\n";
}
void StreamServer::onStateChanged(const PcmStream* pcmStream, const ReaderState& state)
@ -378,6 +381,33 @@ void StreamServer::ProcessRequest(const jsonrpcpp::request_ptr request, jsonrpcp
else
throw jsonrpcpp::MethodNotFoundException(request->id);
}
else if (request->method.find("Stream.") == 0)
{
if (request->method.find("Stream.SetMeta") == 0)
{
/// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.SetMeta","params":{"id":"Spotify","meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}}
/// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"stream 1"}}
/// Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
// Find stream
string streamId = request->params.get("stream_id");
PcmStreamPtr stream = streamManager_->getStream(streamId);
if (stream == nullptr)
throw jsonrpcpp::InternalErrorException("Stream not found", request->id);
// Set metadata from request
// stream->
// Update clients
// Setup response
// Trigger notifications
// onMetaChanged(stream);
// notification.reset(new jsonrpcpp::Notification("Client.OnLatencyChanged", jsonrpcpp::Parameter("id", clientInfo->id, "latency", clientInfo->config.latency)));
}
else
throw jsonrpcpp::MethodNotFoundException(request->id);
}
else
throw jsonrpcpp::MethodNotFoundException(request->id);

View file

@ -56,8 +56,7 @@ PcmStream::PcmStream(PcmListener* pcmListener, const StreamUri& uri) :
else
dryoutMs_ = 2000;
// meta_.reset(new msg::StreamTags);
meta_ = make_shared<msg::StreamTags>();
meta_.reset(new msg::StreamTags());
}
@ -172,8 +171,11 @@ json PcmStream::toJson() const
{"uri", uri_.toJson()},
{"id", getId()},
{"status", state},
{"meta", meta_->toJson()}
};
if(meta_)
j["meta"] = meta_->msg;
return j;
}

View file

@ -135,9 +135,12 @@ void SpotifyStream::onStderrMsg(const char* buffer, size_t n)
{
// Create a new meta struct?
LOG(INFO) << "Loading track <" << m[1] << "> <" << m[2] << ">\n";
getMeta()->setArtist(m[1]);
getMeta()->setAlbum("");
getMeta()->setTrack(m[2]);
json jtag = {
{"artist", (string)m[1]},
{"track", (string)m[2]}
};
meta_.reset(new msg::StreamTags(jtag));
// Trigger a stream update
if (pcmListener_)