mirror of
https://github.com/badaix/snapcast.git
synced 2025-05-10 15:46:42 +02:00
Switch stream readers to use asio event loop
This commit is contained in:
parent
3eab397543
commit
6d7e25e9af
30 changed files with 630 additions and 583 deletions
|
@ -159,7 +159,7 @@ Snapserver supports [librespot](https://github.com/librespot-org/librespot) with
|
||||||
### Process
|
### Process
|
||||||
Snapserver can start any process and read PCM data from the stdout of the process:
|
Snapserver can start any process and read PCM data from the stdout of the process:
|
||||||
|
|
||||||
Configure snapserver with `stream = process:///path/to/process?name=Process[¶ms=<--my list --of params>][&logStderr=false]`
|
Configure snapserver with `stream = process:///path/to/process?name=Process[¶ms=<--my list --of params>][&log_stderr=false]`
|
||||||
|
|
||||||
|
|
||||||
### Line-in
|
### Line-in
|
||||||
|
|
|
@ -14,6 +14,7 @@ set(SERVER_SOURCES
|
||||||
streamreader/pcm_stream.cpp
|
streamreader/pcm_stream.cpp
|
||||||
streamreader/tcp_stream.cpp
|
streamreader/tcp_stream.cpp
|
||||||
streamreader/pipe_stream.cpp
|
streamreader/pipe_stream.cpp
|
||||||
|
streamreader/posix_stream.cpp
|
||||||
streamreader/file_stream.cpp
|
streamreader/file_stream.cpp
|
||||||
streamreader/airplay_stream.cpp
|
streamreader/airplay_stream.cpp
|
||||||
streamreader/librespot_stream.cpp
|
streamreader/librespot_stream.cpp
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
using namespace streamreader;
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,8 +16,8 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef STREAM_SERVER_H
|
#ifndef STREAM_SERVER_HPP
|
||||||
#define STREAM_SERVER_H
|
#define STREAM_SERVER_HPP
|
||||||
|
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
#include "stream_session.hpp"
|
#include "stream_session.hpp"
|
||||||
#include "streamreader/stream_manager.hpp"
|
#include "streamreader/stream_manager.hpp"
|
||||||
|
|
||||||
|
using namespace streamreader;
|
||||||
|
|
||||||
using boost::asio::ip::tcp;
|
using boost::asio::ip::tcp;
|
||||||
using acceptor_ptr = std::unique_ptr<tcp::acceptor>;
|
using acceptor_ptr = std::unique_ptr<tcp::acceptor>;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,6 +23,10 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
using namespace streamreader;
|
||||||
|
|
||||||
|
|
||||||
|
static constexpr auto LOG_TAG = "StreamSession";
|
||||||
|
|
||||||
|
|
||||||
StreamSession::StreamSession(boost::asio::io_context& ioc, MessageReceiver* receiver, tcp::socket&& socket)
|
StreamSession::StreamSession(boost::asio::io_context& ioc, MessageReceiver* receiver, tcp::socket&& socket)
|
||||||
|
@ -48,7 +52,7 @@ void StreamSession::read_next()
|
||||||
}
|
}
|
||||||
catch (const std::bad_weak_ptr& e)
|
catch (const std::bad_weak_ptr& e)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "read_next: Error getting shared from this\n";
|
LOG(ERROR, LOG_TAG) << "read_next: Error getting shared from this\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,23 +60,23 @@ void StreamSession::read_next()
|
||||||
boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t length) mutable {
|
boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t length) mutable {
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Error reading message header of length " << length << ": " << ec.message() << "\n";
|
LOG(ERROR, LOG_TAG) << "Error reading message header of length " << length << ": " << ec.message() << "\n";
|
||||||
messageReceiver_->onDisconnect(this);
|
messageReceiver_->onDisconnect(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
baseMessage_.deserialize(buffer_.data());
|
baseMessage_.deserialize(buffer_.data());
|
||||||
LOG(DEBUG) << "getNextMessage: " << baseMessage_.type << ", size: " << baseMessage_.size << ", id: " << baseMessage_.id
|
LOG(DEBUG, LOG_TAG) << "getNextMessage: " << baseMessage_.type << ", size: " << baseMessage_.size << ", id: " << baseMessage_.id
|
||||||
<< ", refers: " << baseMessage_.refersTo << "\n";
|
<< ", refers: " << baseMessage_.refersTo << "\n";
|
||||||
if (baseMessage_.type > message_type::kLast)
|
if (baseMessage_.type > message_type::kLast)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "unknown message type received: " << baseMessage_.type << ", size: " << baseMessage_.size << "\n";
|
LOG(ERROR, LOG_TAG) << "unknown message type received: " << baseMessage_.type << ", size: " << baseMessage_.size << "\n";
|
||||||
messageReceiver_->onDisconnect(this);
|
messageReceiver_->onDisconnect(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (baseMessage_.size > msg::max_size)
|
else if (baseMessage_.size > msg::max_size)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "received message of type " << baseMessage_.type << " to large: " << baseMessage_.size << "\n";
|
LOG(ERROR, LOG_TAG) << "received message of type " << baseMessage_.type << " to large: " << baseMessage_.size << "\n";
|
||||||
messageReceiver_->onDisconnect(this);
|
messageReceiver_->onDisconnect(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -85,7 +89,7 @@ void StreamSession::read_next()
|
||||||
boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t length) mutable {
|
boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t length) mutable {
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Error reading message body of length " << length << ": " << ec.message() << "\n";
|
LOG(ERROR, LOG_TAG) << "Error reading message body of length " << length << ": " << ec.message() << "\n";
|
||||||
messageReceiver_->onDisconnect(this);
|
messageReceiver_->onDisconnect(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -121,15 +125,15 @@ void StreamSession::start()
|
||||||
|
|
||||||
void StreamSession::stop()
|
void StreamSession::stop()
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "StreamSession::stop\n";
|
LOG(DEBUG, LOG_TAG) << "StreamSession::stop\n";
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
|
socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
|
||||||
if (ec)
|
if (ec)
|
||||||
LOG(ERROR) << "Error in socket shutdown: " << ec.message() << "\n";
|
LOG(ERROR, LOG_TAG) << "Error in socket shutdown: " << ec.message() << "\n";
|
||||||
socket_.close(ec);
|
socket_.close(ec);
|
||||||
if (ec)
|
if (ec)
|
||||||
LOG(ERROR) << "Error in socket close: " << ec.message() << "\n";
|
LOG(ERROR, LOG_TAG) << "Error in socket close: " << ec.message() << "\n";
|
||||||
LOG(DEBUG) << "StreamSession stopped\n";
|
LOG(DEBUG, LOG_TAG) << "StreamSession stopped\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,7 +146,7 @@ void StreamSession::send_next()
|
||||||
}
|
}
|
||||||
catch (const std::bad_weak_ptr& e)
|
catch (const std::bad_weak_ptr& e)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "send_next: Error getting shared from this\n";
|
LOG(ERROR, LOG_TAG) << "send_next: Error getting shared from this\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +156,7 @@ void StreamSession::send_next()
|
||||||
messages_.pop_front();
|
messages_.pop_front();
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "StreamSession write error (msg lenght: " << length << "): " << ec.message() << "\n";
|
LOG(ERROR, LOG_TAG) << "StreamSession write error (msg lenght: " << length << "): " << ec.message() << "\n";
|
||||||
messageReceiver_->onDisconnect(this);
|
messageReceiver_->onDisconnect(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -172,7 +176,7 @@ void StreamSession::sendAsync(shared_const_buffer const_buf, bool send_now)
|
||||||
messages_.push_back(const_buf);
|
messages_.push_back(const_buf);
|
||||||
if (messages_.size() > 1)
|
if (messages_.size() > 1)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "outstanding async_write\n";
|
LOG(DEBUG, LOG_TAG) << "outstanding async_write\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
send_next();
|
send_next();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,8 +16,8 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef STREAM_SESSION_H
|
#ifndef STREAM_SESSION_HPP
|
||||||
#define STREAM_SESSION_H
|
#define STREAM_SESSION_HPP
|
||||||
|
|
||||||
#include "common/queue.h"
|
#include "common/queue.h"
|
||||||
#include "message/message.hpp"
|
#include "message/message.hpp"
|
||||||
|
@ -121,8 +121,8 @@ public:
|
||||||
return socket_.remote_endpoint().address().to_string();
|
return socket_.remote_endpoint().address().to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPcmStream(PcmStreamPtr pcmStream);
|
void setPcmStream(streamreader::PcmStreamPtr pcmStream);
|
||||||
const PcmStreamPtr pcmStream() const;
|
const streamreader::PcmStreamPtr pcmStream() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void read_next();
|
void read_next();
|
||||||
|
@ -134,7 +134,7 @@ protected:
|
||||||
tcp::socket socket_;
|
tcp::socket socket_;
|
||||||
MessageReceiver* messageReceiver_;
|
MessageReceiver* messageReceiver_;
|
||||||
size_t bufferMs_;
|
size_t bufferMs_;
|
||||||
PcmStreamPtr pcmStream_;
|
streamreader::PcmStreamPtr pcmStream_;
|
||||||
boost::asio::io_context::strand strand_;
|
boost::asio::io_context::strand strand_;
|
||||||
std::deque<shared_const_buffer> messages_;
|
std::deque<shared_const_buffer> messages_;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,13 +25,21 @@
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
static string hex2str(string input)
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
|
static constexpr auto LOG_TAG = "AirplayStream";
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
string hex2str(string input)
|
||||||
{
|
{
|
||||||
typedef unsigned char byte;
|
typedef unsigned char byte;
|
||||||
unsigned long x = strtoul(input.c_str(), nullptr, 16);
|
unsigned long x = strtoul(input.c_str(), nullptr, 16);
|
||||||
byte a[] = {byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x), 0};
|
byte a[] = {byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x), 0};
|
||||||
return string((char*)a);
|
return string((char*)a);
|
||||||
}
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Expat is used in metadata parsing from Shairport-sync.
|
* Expat is used in metadata parsing from Shairport-sync.
|
||||||
|
@ -115,7 +123,7 @@ void AirplayStream::push()
|
||||||
|
|
||||||
if (entry_->type == "ssnc" && entry_->code == "mden")
|
if (entry_->type == "ssnc" && entry_->code == "mden")
|
||||||
{
|
{
|
||||||
// LOG(INFO) << "metadata=" << jtag_.dump(4) << "\n";
|
// LOG(INFO, LOG_TAG) << "metadata=" << jtag_.dump(4) << "\n";
|
||||||
setMeta(jtag_);
|
setMeta(jtag_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,22 +175,21 @@ void AirplayStream::initExeAndPath(const string& filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void AirplayStream::onStderrMsg(const char* buffer, size_t n)
|
void AirplayStream::onStderrMsg(const std::string& line)
|
||||||
{
|
{
|
||||||
string logmsg = utils::string::trim_copy(string(buffer, n));
|
if (line.empty())
|
||||||
if (logmsg.empty())
|
|
||||||
return;
|
return;
|
||||||
LOG(INFO) << "(" << getName() << ") " << logmsg << "\n";
|
LOG(INFO, LOG_TAG) << "(" << getName() << ") " << line << "\n";
|
||||||
if (logmsg.find("Is another Shairport Sync running on this device") != string::npos)
|
if (line.find("Is another Shairport Sync running on this device") != string::npos)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Seem there is another Shairport Sync runnig on port " << port_ << ", switching to port " << port_ + 1 << "\n";
|
LOG(ERROR, LOG_TAG) << "Seem there is another Shairport Sync runnig on port " << port_ << ", switching to port " << port_ + 1 << "\n";
|
||||||
++port_;
|
++port_;
|
||||||
params_ = params_wo_port_ + " --port=" + cpt::to_string(port_);
|
params_ = params_wo_port_ + " --port=" + cpt::to_string(port_);
|
||||||
}
|
}
|
||||||
else if (logmsg.find("Invalid audio output specified") != string::npos)
|
else if (line.find("Invalid audio output specified") != string::npos)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "shairport sync compiled without stdout audio backend\n";
|
LOG(ERROR, LOG_TAG) << "shairport sync compiled without stdout audio backend\n";
|
||||||
LOG(ERROR) << "build with: \"./configure --with-stdout --with-avahi --with-ssl=openssl --with-metadata\"\n";
|
LOG(ERROR, LOG_TAG) << "build with: \"./configure --with-stdout --with-avahi --with-ssl=openssl --with-metadata\"\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,4 +244,7 @@ void XMLCALL AirplayStream::data(void* userdata, const char* content, int length
|
||||||
string value(content, (size_t)length);
|
string value(content, (size_t)length);
|
||||||
self->buf_.append(value);
|
self->buf_.append(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,8 +16,8 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef AIRPLAY_STREAM_H
|
#ifndef AIRPLAY_STREAM_HPP
|
||||||
#define AIRPLAY_STREAM_H
|
#define AIRPLAY_STREAM_HPP
|
||||||
|
|
||||||
#include "process_stream.hpp"
|
#include "process_stream.hpp"
|
||||||
|
|
||||||
|
@ -29,6 +29,9 @@
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
class TageEntry
|
class TageEntry
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -74,7 +77,7 @@ protected:
|
||||||
void push();
|
void push();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void onStderrMsg(const char* buffer, size_t n) override;
|
void onStderrMsg(const std::string& line) override;
|
||||||
void initExeAndPath(const std::string& filename) override;
|
void initExeAndPath(const std::string& filename) override;
|
||||||
size_t port_;
|
size_t port_;
|
||||||
std::string pipePath_;
|
std::string pipePath_;
|
||||||
|
@ -88,5 +91,6 @@ protected:
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,7 +24,8 @@
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
template <typename ReadStream>
|
template <typename ReadStream>
|
||||||
class AsioStream : public PcmStream, public std::enable_shared_from_this<AsioStream<ReadStream>>
|
class AsioStream : public PcmStream, public std::enable_shared_from_this<AsioStream<ReadStream>>
|
||||||
|
@ -36,9 +37,12 @@ public:
|
||||||
void start() override;
|
void start() override;
|
||||||
void stop() override;
|
void stop() override;
|
||||||
|
|
||||||
|
virtual void connect();
|
||||||
|
virtual void disconnect();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void connect() = 0;
|
virtual void do_connect() = 0;
|
||||||
virtual void disconnect() = 0;
|
virtual void do_disconnect() = 0;
|
||||||
virtual void on_connect();
|
virtual void on_connect();
|
||||||
virtual void do_read();
|
virtual void do_read();
|
||||||
void check_state();
|
void check_state();
|
||||||
|
@ -83,7 +87,7 @@ void AsioStream<ReadStream>::check_state()
|
||||||
state_timer_.async_wait([self, this, last_read](const boost::system::error_code& ec) {
|
state_timer_.async_wait([self, this, last_read](const boost::system::error_code& ec) {
|
||||||
if (!ec)
|
if (!ec)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "check state last: " << last_read << ", read: " << bytes_read_ << "\n";
|
LOG(DEBUG, "AsioStream") << "check state last: " << last_read << ", read: " << bytes_read_ << "\n";
|
||||||
if (bytes_read_ != last_read)
|
if (bytes_read_ != last_read)
|
||||||
setState(ReaderState::kPlaying);
|
setState(ReaderState::kPlaying);
|
||||||
else
|
else
|
||||||
|
@ -104,6 +108,20 @@ void AsioStream<ReadStream>::start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
template <typename ReadStream>
|
||||||
|
void AsioStream<ReadStream>::connect()
|
||||||
|
{
|
||||||
|
do_connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
template <typename ReadStream>
|
||||||
|
void AsioStream<ReadStream>::disconnect()
|
||||||
|
{
|
||||||
|
do_disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
template <typename ReadStream>
|
template <typename ReadStream>
|
||||||
void AsioStream<ReadStream>::stop()
|
void AsioStream<ReadStream>::stop()
|
||||||
{
|
{
|
||||||
|
@ -126,19 +144,19 @@ void AsioStream<ReadStream>::on_connect()
|
||||||
template <typename ReadStream>
|
template <typename ReadStream>
|
||||||
void AsioStream<ReadStream>::do_read()
|
void AsioStream<ReadStream>::do_read()
|
||||||
{
|
{
|
||||||
// LOG(DEBUG) << "do_read\n";
|
// LOG(DEBUG, "AsioStream") << "do_read\n";
|
||||||
auto self = this->shared_from_this();
|
auto self = this->shared_from_this();
|
||||||
boost::asio::async_read(*stream_, boost::asio::buffer(chunk_->payload, chunk_->payloadSize),
|
boost::asio::async_read(
|
||||||
[this, self](boost::system::error_code ec, std::size_t length) mutable {
|
*stream_, boost::asio::buffer(chunk_->payload, chunk_->payloadSize), [this, self](boost::system::error_code ec, std::size_t length) mutable {
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Error reading message: " << ec.message() << ", length: " << length << "\n";
|
LOG(ERROR, "AsioStream") << "Error reading message: " << ec.message() << ", length: " << length << "\n";
|
||||||
connect();
|
connect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes_read_ += length;
|
bytes_read_ += length;
|
||||||
// LOG(DEBUG) << "Read: " << length << " bytes\n";
|
// LOG(DEBUG, "AsioStream") << "Read: " << length << " bytes\n";
|
||||||
// First read after connect. Set the initial read timestamp
|
// First read after connect. Set the initial read timestamp
|
||||||
// the timestamp will be incremented after encoding,
|
// the timestamp will be incremented after encoding,
|
||||||
// since we do not know how much the encoder actually encoded
|
// since we do not know how much the encoder actually encoded
|
||||||
|
@ -150,7 +168,7 @@ void AsioStream<ReadStream>::do_read()
|
||||||
auto stream2systime_diff = chronos::diff<std::chrono::milliseconds>(now, tvEncodedChunk_);
|
auto stream2systime_diff = chronos::diff<std::chrono::milliseconds>(now, tvEncodedChunk_);
|
||||||
if (stream2systime_diff > chronos::sec(5) + chronos::msec(chunk_ms_))
|
if (stream2systime_diff > chronos::sec(5) + chronos::msec(chunk_ms_))
|
||||||
{
|
{
|
||||||
LOG(WARNING) << "Stream and system time out of sync: " << stream2systime_diff.count() << "ms, resetting stream time.\n";
|
LOG(WARNING, "AsioStream") << "Stream and system time out of sync: " << stream2systime_diff.count() << "ms, resetting stream time.\n";
|
||||||
first_ = true;
|
first_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +190,7 @@ void AsioStream<ReadStream>::do_read()
|
||||||
read_timer_.async_wait([self, this](const boost::system::error_code& ec) {
|
read_timer_.async_wait([self, this](const boost::system::error_code& ec) {
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Error during async wait: " << ec.message() << "\n";
|
LOG(ERROR, "AsioStream") << "Error during async wait: " << ec.message() << "\n";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -192,6 +210,6 @@ void AsioStream<ReadStream>::do_read()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -28,85 +28,32 @@
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
|
||||||
FileStream::FileStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : PcmStream(pcmListener, ioc, uri)
|
|
||||||
{
|
{
|
||||||
ifs.open(uri_.path.c_str(), std::ifstream::in | std::ifstream::binary);
|
|
||||||
if (!ifs.good())
|
static constexpr auto LOG_TAG = "FileStream";
|
||||||
|
|
||||||
|
|
||||||
|
FileStream::FileStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : PosixStream(pcmListener, ioc, uri)
|
||||||
|
{
|
||||||
|
struct stat buffer;
|
||||||
|
if (stat(uri_.path.c_str(), &buffer) != 0)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "failed to open PCM file: \"" + uri_.path + "\"\n";
|
throw SnapException("Failed to open PCM file: \"" + uri_.path + "\"");
|
||||||
throw SnapException("failed to open PCM file: \"" + uri_.path + "\"");
|
}
|
||||||
|
else if ((buffer.st_mode & S_IFMT) != S_IFREG)
|
||||||
|
{
|
||||||
|
throw SnapException("Not a regular file: \"" + uri_.path + "\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
FileStream::~FileStream()
|
void FileStream::do_connect()
|
||||||
{
|
{
|
||||||
ifs.close();
|
LOG(DEBUG, LOG_TAG) << "connect\n";
|
||||||
|
int fd = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK);
|
||||||
|
stream_ = std::make_unique<boost::asio::posix::stream_descriptor>(ioc_, fd);
|
||||||
|
on_connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
void FileStream::worker()
|
|
||||||
{
|
|
||||||
timeval tvChunk;
|
|
||||||
std::unique_ptr<msg::PcmChunk> chunk(new msg::PcmChunk(sampleFormat_, chunk_ms_));
|
|
||||||
|
|
||||||
ifs.seekg(0, ifs.end);
|
|
||||||
size_t length = ifs.tellg();
|
|
||||||
ifs.seekg(0, ifs.beg);
|
|
||||||
|
|
||||||
setState(ReaderState::kPlaying);
|
|
||||||
|
|
||||||
while (active_)
|
|
||||||
{
|
|
||||||
chronos::systemtimeofday(&tvChunk);
|
|
||||||
tvEncodedChunk_ = tvChunk;
|
|
||||||
long nextTick = chronos::getTickCount();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (active_)
|
|
||||||
{
|
|
||||||
chunk->timestamp.sec = tvChunk.tv_sec;
|
|
||||||
chunk->timestamp.usec = tvChunk.tv_usec;
|
|
||||||
size_t toRead = chunk->payloadSize;
|
|
||||||
size_t count = 0;
|
|
||||||
|
|
||||||
size_t pos = ifs.tellg();
|
|
||||||
size_t left = length - pos;
|
|
||||||
if (left < toRead)
|
|
||||||
{
|
|
||||||
ifs.read(chunk->payload, left);
|
|
||||||
ifs.seekg(0, ifs.beg);
|
|
||||||
count = left;
|
|
||||||
}
|
|
||||||
ifs.read(chunk->payload + count, toRead - count);
|
|
||||||
|
|
||||||
encoder_->encode(chunk.get());
|
|
||||||
if (!active_)
|
|
||||||
break;
|
|
||||||
nextTick += chunk_ms_;
|
|
||||||
chronos::addUs(tvChunk, chunk_ms_ * 1000);
|
|
||||||
long currentTick = chronos::getTickCount();
|
|
||||||
|
|
||||||
if (nextTick >= currentTick)
|
|
||||||
{
|
|
||||||
// LOG(INFO) << "sleep: " << nextTick - currentTick << "\n";
|
|
||||||
if (!sleep(nextTick - currentTick))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
chronos::systemtimeofday(&tvChunk);
|
|
||||||
tvEncodedChunk_ = tvChunk;
|
|
||||||
pcmListener_->onResync(this, currentTick - nextTick);
|
|
||||||
nextTick = currentTick;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
LOG(ERROR) << "(FileStream) Exception: " << e.what() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,12 +16,13 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef FILE_STREAM_H
|
#ifndef FILE_STREAM_HPP
|
||||||
#define FILE_STREAM_H
|
#define FILE_STREAM_HPP
|
||||||
|
|
||||||
#include "pcm_stream.hpp"
|
#include "posix_stream.hpp"
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
/// Reads and decodes PCM data from a file
|
/// Reads and decodes PCM data from a file
|
||||||
/**
|
/**
|
||||||
|
@ -29,17 +30,16 @@
|
||||||
* Implements EncoderListener to get the encoded data.
|
* Implements EncoderListener to get the encoded data.
|
||||||
* Data is passed to the PcmListener
|
* Data is passed to the PcmListener
|
||||||
*/
|
*/
|
||||||
class FileStream : public PcmStream
|
class FileStream : public PosixStream
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
/// ctor. Encoded PCM data is passed to the PipeListener
|
/// ctor. Encoded PCM data is passed to the PipeListener
|
||||||
FileStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
FileStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
||||||
~FileStream() override;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void worker() override;
|
void do_connect() override;
|
||||||
std::ifstream ifs;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -26,20 +26,26 @@
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
|
static constexpr auto LOG_TAG = "LibrespotStream";
|
||||||
|
|
||||||
|
|
||||||
LibrespotStream::LibrespotStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : ProcessStream(pcmListener, ioc, uri)
|
LibrespotStream::LibrespotStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : ProcessStream(pcmListener, ioc, uri)
|
||||||
{
|
{
|
||||||
sampleFormat_ = SampleFormat("44100:16:2");
|
sampleFormat_ = SampleFormat("44100:16:2");
|
||||||
uri_.query["sampleformat"] = sampleFormat_.getFormat();
|
uri_.query["sampleformat"] = sampleFormat_.getFormat();
|
||||||
|
wd_timeout_sec_ = cpt::stoul(uri_.getQuery("wd_timeout", "7800")); ///< 130min
|
||||||
|
|
||||||
string username = uri_.getQuery("username", "");
|
string username = uri_.getQuery("username", "");
|
||||||
string password = uri_.getQuery("password", "");
|
string password = uri_.getQuery("password", "");
|
||||||
string cache = uri_.getQuery("cache", "");
|
string cache = uri_.getQuery("cache", "");
|
||||||
string volume = uri_.getQuery("volume", "");
|
string volume = uri_.getQuery("volume", "100");
|
||||||
string bitrate = uri_.getQuery("bitrate", "320");
|
string bitrate = uri_.getQuery("bitrate", "320");
|
||||||
string devicename = uri_.getQuery("devicename", "Snapcast");
|
string devicename = uri_.getQuery("devicename", "Snapcast");
|
||||||
string onevent = uri_.getQuery("onevent", "");
|
string onevent = uri_.getQuery("onevent", "");
|
||||||
|
bool normalize = (uri_.getQuery("normalize", "false") == "true");
|
||||||
|
|
||||||
if (username.empty() != password.empty())
|
if (username.empty() != password.empty())
|
||||||
throw SnapException("missing parameter \"username\" or \"password\" (must provide both, or neither)");
|
throw SnapException("missing parameter \"username\" or \"password\" (must provide both, or neither)");
|
||||||
|
@ -54,18 +60,18 @@ LibrespotStream::LibrespotStream(PcmListener* pcmListener, boost::asio::io_conte
|
||||||
params_ += " --initial-volume \"" + volume + "\"";
|
params_ += " --initial-volume \"" + volume + "\"";
|
||||||
if (!onevent.empty())
|
if (!onevent.empty())
|
||||||
params_ += " --onevent \"" + onevent + "\"";
|
params_ += " --onevent \"" + onevent + "\"";
|
||||||
|
if (normalize)
|
||||||
|
params_ += " --enable-volume-normalisation";
|
||||||
|
params_ += " --verbose";
|
||||||
|
|
||||||
if (uri_.query.find("username") != uri_.query.end())
|
if (uri_.query.find("username") != uri_.query.end())
|
||||||
uri_.query["username"] = "xxx";
|
uri_.query["username"] = "xxx";
|
||||||
if (uri_.query.find("password") != uri_.query.end())
|
if (uri_.query.find("password") != uri_.query.end())
|
||||||
uri_.query["password"] = "xxx";
|
uri_.query["password"] = "xxx";
|
||||||
// LOG(INFO) << "params: " << params << "\n";
|
// LOG(INFO, LOG_TAG) << "params: " << params << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
LibrespotStream::~LibrespotStream() = default;
|
|
||||||
|
|
||||||
|
|
||||||
void LibrespotStream::initExeAndPath(const std::string& filename)
|
void LibrespotStream::initExeAndPath(const std::string& filename)
|
||||||
{
|
{
|
||||||
path_ = "";
|
path_ = "";
|
||||||
|
@ -88,7 +94,7 @@ void LibrespotStream::initExeAndPath(const std::string& filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void LibrespotStream::onStderrMsg(const char* buffer, size_t n)
|
void LibrespotStream::onStderrMsg(const std::string& line)
|
||||||
{
|
{
|
||||||
static bool libreelec_patched = false;
|
static bool libreelec_patched = false;
|
||||||
smatch m;
|
smatch m;
|
||||||
|
@ -113,13 +119,10 @@ void LibrespotStream::onStderrMsg(const char* buffer, size_t n)
|
||||||
// 2016-11-03 09-00-18 [out] INFO:librespot::main_helper: librespot 6fa4e4d (2016-09-21). Built on 2016-10-27.
|
// 2016-11-03 09-00-18 [out] INFO:librespot::main_helper: librespot 6fa4e4d (2016-09-21). Built on 2016-10-27.
|
||||||
// 2016-11-03 09-00-18 [out] INFO:librespot::session: Connecting to AP lon3-accesspoint-a34.ap.spotify.com:443
|
// 2016-11-03 09-00-18 [out] INFO:librespot::session: Connecting to AP lon3-accesspoint-a34.ap.spotify.com:443
|
||||||
// 2016-11-03 09-00-18 [out] INFO:librespot::session: Authenticated !
|
// 2016-11-03 09-00-18 [out] INFO:librespot::session: Authenticated !
|
||||||
watchdog_->trigger();
|
|
||||||
string logmsg = utils::string::trim_copy(string(buffer, n));
|
|
||||||
|
|
||||||
if ((logmsg.find("allocated stream") == string::npos) && (logmsg.find("Got channel") == string::npos) && (logmsg.find('\0') == string::npos) &&
|
if ((line.find("allocated stream") == string::npos) && (line.find("Got channel") == string::npos) && (line.find('\0') == string::npos) && (line.size() > 4))
|
||||||
(logmsg.size() > 4))
|
|
||||||
{
|
{
|
||||||
LOG(INFO) << "(" << getName() << ") " << logmsg << "\n";
|
LOG(INFO, LOG_TAG) << "(" << getName() << ") " << line << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Librespot patch:
|
// Librespot patch:
|
||||||
|
@ -132,9 +135,9 @@ void LibrespotStream::onStderrMsg(const char* buffer, size_t n)
|
||||||
if (!libreelec_patched)
|
if (!libreelec_patched)
|
||||||
{
|
{
|
||||||
static regex re_nonpatched("Track \"(.*)\" loaded");
|
static regex re_nonpatched("Track \"(.*)\" loaded");
|
||||||
if (regex_search(logmsg, m, re_nonpatched))
|
if (regex_search(line, m, re_nonpatched))
|
||||||
{
|
{
|
||||||
LOG(INFO) << "metadata: <" << m[1] << ">\n";
|
LOG(INFO, LOG_TAG) << "metadata: <" << m[1] << ">\n";
|
||||||
|
|
||||||
json jtag = {{"TITLE", (string)m[1]}};
|
json jtag = {{"TITLE", (string)m[1]}};
|
||||||
setMeta(jtag);
|
setMeta(jtag);
|
||||||
|
@ -143,28 +146,13 @@ void LibrespotStream::onStderrMsg(const char* buffer, size_t n)
|
||||||
|
|
||||||
// Parse the patched version
|
// Parse the patched version
|
||||||
static regex re_patched("metadata:(.*)");
|
static regex re_patched("metadata:(.*)");
|
||||||
if (regex_search(logmsg, m, re_patched))
|
if (regex_search(line, m, re_patched))
|
||||||
{
|
{
|
||||||
LOG(INFO) << "metadata: <" << m[1] << ">\n";
|
LOG(INFO, LOG_TAG) << "metadata: <" << m[1] << ">\n";
|
||||||
|
|
||||||
setMeta(json::parse(m[1].str()));
|
setMeta(json::parse(m[1].str()));
|
||||||
libreelec_patched = true;
|
libreelec_patched = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
void LibrespotStream::stderrReader()
|
|
||||||
{
|
|
||||||
watchdog_.reset(new Watchdog(this));
|
|
||||||
/// 130min
|
|
||||||
watchdog_->start(130 * 60 * 1000);
|
|
||||||
ProcessStream::stderrReader();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void LibrespotStream::onTimeout(const Watchdog* /*watchdog*/, size_t ms)
|
|
||||||
{
|
|
||||||
LOG(ERROR) << "Spotify timeout: " << ms / 1000 << "\n";
|
|
||||||
if (process_)
|
|
||||||
process_->kill();
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,11 +16,13 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef SPOTIFY_STREAM_H
|
#ifndef SPOTIFY_STREAM_HPP
|
||||||
#define SPOTIFY_STREAM_H
|
#define SPOTIFY_STREAM_HPP
|
||||||
|
|
||||||
#include "process_stream.hpp"
|
#include "process_stream.hpp"
|
||||||
#include "watchdog.h"
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
/// Starts librespot and reads PCM data from stdout
|
/// Starts librespot and reads PCM data from stdout
|
||||||
/**
|
/**
|
||||||
|
@ -31,22 +33,17 @@
|
||||||
* snapserver -s "spotify:///librespot?name=Spotify&username=<my username>&password=<my password>[&devicename=Snapcast][&bitrate=320][&volume=<volume in
|
* snapserver -s "spotify:///librespot?name=Spotify&username=<my username>&password=<my password>[&devicename=Snapcast][&bitrate=320][&volume=<volume in
|
||||||
* percent>][&cache=<cache dir>]"
|
* percent>][&cache=<cache dir>]"
|
||||||
*/
|
*/
|
||||||
class LibrespotStream : public ProcessStream, WatchdogListener
|
class LibrespotStream : public ProcessStream
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
/// ctor. Encoded PCM data is passed to the PipeListener
|
/// ctor. Encoded PCM data is passed to the PipeListener
|
||||||
LibrespotStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
LibrespotStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
||||||
~LibrespotStream() override;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::unique_ptr<Watchdog> watchdog_;
|
void onStderrMsg(const std::string& line) override;
|
||||||
|
|
||||||
void stderrReader() override;
|
|
||||||
void onStderrMsg(const char* buffer, size_t n) override;
|
|
||||||
void initExeAndPath(const std::string& filename) override;
|
void initExeAndPath(const std::string& filename) override;
|
||||||
|
|
||||||
void onTimeout(const Watchdog* watchdog, size_t ms) override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -29,6 +29,8 @@
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
PcmStream::PcmStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
|
PcmStream::PcmStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
|
||||||
|
@ -96,28 +98,12 @@ void PcmStream::start()
|
||||||
LOG(DEBUG) << "PcmStream start: " << sampleFormat_.getFormat() << "\n";
|
LOG(DEBUG) << "PcmStream start: " << sampleFormat_.getFormat() << "\n";
|
||||||
encoder_->init(this, sampleFormat_);
|
encoder_->init(this, sampleFormat_);
|
||||||
active_ = true;
|
active_ = true;
|
||||||
thread_ = thread(&PcmStream::worker, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PcmStream::stop()
|
void PcmStream::stop()
|
||||||
{
|
{
|
||||||
if (!active_ && !thread_.joinable())
|
|
||||||
return;
|
|
||||||
|
|
||||||
active_ = false;
|
active_ = false;
|
||||||
cv_.notify_one();
|
|
||||||
if (thread_.joinable())
|
|
||||||
thread_.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bool PcmStream::sleep(int32_t ms)
|
|
||||||
{
|
|
||||||
if (ms < 0)
|
|
||||||
return true;
|
|
||||||
std::unique_lock<std::mutex> lck(mtx_);
|
|
||||||
return (!cv_.wait_for(lck, std::chrono::milliseconds(ms), [this] { return !active_; }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -188,3 +174,5 @@ void PcmStream::setMeta(const json& jtag)
|
||||||
if (pcmListener_)
|
if (pcmListener_)
|
||||||
pcmListener_->onMetaChanged(this);
|
pcmListener_->onMetaChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,8 +16,8 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef PCM_STREAM_H
|
#ifndef PCM_STREAM_HPP
|
||||||
#define PCM_STREAM_H
|
#define PCM_STREAM_HPP
|
||||||
|
|
||||||
#include "common/json.hpp"
|
#include "common/json.hpp"
|
||||||
#include "common/sample_format.hpp"
|
#include "common/sample_format.hpp"
|
||||||
|
@ -34,6 +34,9 @@
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
class PcmStream;
|
class PcmStream;
|
||||||
|
|
||||||
enum class ReaderState
|
enum class ReaderState
|
||||||
|
@ -98,13 +101,8 @@ public:
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::condition_variable cv_;
|
|
||||||
std::mutex mtx_;
|
|
||||||
std::thread thread_;
|
|
||||||
std::atomic<bool> active_;
|
std::atomic<bool> active_;
|
||||||
|
|
||||||
virtual void worker(){};
|
|
||||||
virtual bool sleep(int32_t ms);
|
|
||||||
void setState(const ReaderState& newState);
|
void setState(const ReaderState& newState);
|
||||||
|
|
||||||
timeval tvEncodedChunk_;
|
timeval tvEncodedChunk_;
|
||||||
|
@ -119,5 +117,6 @@ protected:
|
||||||
boost::asio::io_context& ioc_;
|
boost::asio::io_context& ioc_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -30,14 +30,18 @@
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
|
static constexpr auto LOG_TAG = "PipeStream";
|
||||||
|
|
||||||
|
|
||||||
PipeStream::PipeStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : AsioStream<stream_descriptor>(pcmListener, ioc, uri)
|
PipeStream::PipeStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : PosixStream(pcmListener, ioc, uri)
|
||||||
{
|
{
|
||||||
umask(0);
|
umask(0);
|
||||||
string mode = uri_.getQuery("mode", "create");
|
string mode = uri_.getQuery("mode", "create");
|
||||||
|
|
||||||
LOG(INFO) << "PipeStream mode: " << mode << "\n";
|
LOG(INFO, LOG_TAG) << "PipeStream mode: " << mode << "\n";
|
||||||
if ((mode != "read") && (mode != "create"))
|
if ((mode != "read") && (mode != "create"))
|
||||||
throw SnapException("create mode for fifo must be \"read\" or \"create\"");
|
throw SnapException("create mode for fifo must be \"read\" or \"create\"");
|
||||||
|
|
||||||
|
@ -46,101 +50,14 @@ PipeStream::PipeStream(PcmListener* pcmListener, boost::asio::io_context& ioc, c
|
||||||
if ((mkfifo(uri_.path.c_str(), 0666) != 0) && (errno != EEXIST))
|
if ((mkfifo(uri_.path.c_str(), 0666) != 0) && (errno != EEXIST))
|
||||||
throw SnapException("failed to make fifo \"" + uri_.path + "\": " + cpt::to_string(errno));
|
throw SnapException("failed to make fifo \"" + uri_.path + "\": " + cpt::to_string(errno));
|
||||||
}
|
}
|
||||||
chunk_ = make_unique<msg::PcmChunk>(sampleFormat_, chunk_ms_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PipeStream::connect()
|
void PipeStream::do_connect()
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "connect\n";
|
LOG(DEBUG, LOG_TAG) << "connect\n";
|
||||||
auto self = shared_from_this();
|
int fd = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK);
|
||||||
fd_ = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK);
|
stream_ = std::make_unique<boost::asio::posix::stream_descriptor>(ioc_, fd);
|
||||||
stream_ = std::make_unique<boost::asio::posix::stream_descriptor>(ioc_, fd_);
|
|
||||||
on_connect();
|
on_connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PipeStream::disconnect()
|
|
||||||
{
|
|
||||||
stream_->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void PipeStream::do_read()
|
|
||||||
{
|
|
||||||
auto self = this->shared_from_this();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (fd_ == -1)
|
|
||||||
throw SnapException("failed to open fifo: \"" + uri_.path + "\"");
|
|
||||||
|
|
||||||
int toRead = chunk_->payloadSize;
|
|
||||||
int len = 0;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int count = read(fd_, chunk_->payload + len, toRead - len);
|
|
||||||
if (count < 0)
|
|
||||||
{
|
|
||||||
LOG(DEBUG) << "count < 0: " << errno
|
|
||||||
<< " && idleBytes < maxIdleBytes, ms: " << 1000 * chunk_->payloadSize / (sampleFormat_.rate * sampleFormat_.frameSize) << "\n";
|
|
||||||
memset(chunk_->payload + len, 0, toRead - len);
|
|
||||||
len += toRead - len;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (count == 0)
|
|
||||||
{
|
|
||||||
throw SnapException("end of file");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// LOG(DEBUG) << "count: " << count << "\n";
|
|
||||||
len += count;
|
|
||||||
bytes_read_ += len;
|
|
||||||
}
|
|
||||||
} while (len < toRead);
|
|
||||||
|
|
||||||
if (first_)
|
|
||||||
{
|
|
||||||
first_ = false;
|
|
||||||
chronos::systemtimeofday(&tvEncodedChunk_);
|
|
||||||
nextTick_ = chronos::getTickCount() + buffer_ms_;
|
|
||||||
}
|
|
||||||
encoder_->encode(chunk_.get());
|
|
||||||
nextTick_ += chunk_ms_;
|
|
||||||
long currentTick = chronos::getTickCount();
|
|
||||||
|
|
||||||
if (nextTick_ >= currentTick)
|
|
||||||
{
|
|
||||||
read_timer_.expires_after(std::chrono::milliseconds(nextTick_ - currentTick));
|
|
||||||
read_timer_.async_wait([self, this](const boost::system::error_code& ec) {
|
|
||||||
if (ec)
|
|
||||||
{
|
|
||||||
LOG(ERROR) << "Error during async wait: " << ec.message() << "\n";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
do_read();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pcmListener_->onResync(this, currentTick - nextTick_);
|
|
||||||
nextTick_ = currentTick + buffer_ms_;
|
|
||||||
first_ = true;
|
|
||||||
do_read();
|
|
||||||
}
|
|
||||||
|
|
||||||
lastException_ = "";
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
if (lastException_ != e.what())
|
|
||||||
{
|
|
||||||
LOG(ERROR) << "(PipeStream) Exception: " << e.what() << std::endl;
|
|
||||||
lastException_ = e.what();
|
|
||||||
}
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,10 +16,13 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef PIPE_STREAM_H
|
#ifndef PIPE_STREAM_HPP
|
||||||
#define PIPE_STREAM_H
|
#define PIPE_STREAM_HPP
|
||||||
|
|
||||||
#include "asio_stream.hpp"
|
#include "posix_stream.hpp"
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
using boost::asio::posix::stream_descriptor;
|
using boost::asio::posix::stream_descriptor;
|
||||||
|
|
||||||
|
@ -30,19 +33,16 @@ using boost::asio::posix::stream_descriptor;
|
||||||
* Implements EncoderListener to get the encoded data.
|
* Implements EncoderListener to get the encoded data.
|
||||||
* Data is passed to the PcmListener
|
* Data is passed to the PcmListener
|
||||||
*/
|
*/
|
||||||
class PipeStream : public AsioStream<stream_descriptor>
|
class PipeStream : public PosixStream
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
/// ctor. Encoded PCM data is passed to the PipeListener
|
/// ctor. Encoded PCM data is passed to the PipeListener
|
||||||
PipeStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
PipeStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void connect() override;
|
void do_connect() override;
|
||||||
void disconnect() override;
|
|
||||||
void do_read() override;
|
|
||||||
std::string lastException_;
|
|
||||||
int fd_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
164
server/streamreader/posix_stream.cpp
Normal file
164
server/streamreader/posix_stream.cpp
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
/***
|
||||||
|
This file is part of snapcast
|
||||||
|
Copyright (C) 2014-2019 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/>.
|
||||||
|
***/
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "common/aixlog.hpp"
|
||||||
|
#include "common/snap_exception.hpp"
|
||||||
|
#include "common/str_compat.hpp"
|
||||||
|
#include "posix_stream.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
|
static constexpr auto LOG_TAG = "PosixStream";
|
||||||
|
|
||||||
|
|
||||||
|
PosixStream::PosixStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : AsioStream<stream_descriptor>(pcmListener, ioc, uri)
|
||||||
|
{
|
||||||
|
if (uri_.query.find("dryout_ms") != uri_.query.end())
|
||||||
|
dryout_ms_ = cpt::stoul(uri_.query["dryout_ms"]);
|
||||||
|
else
|
||||||
|
dryout_ms_ = 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PosixStream::connect()
|
||||||
|
{
|
||||||
|
if (!active_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
do_connect();
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "Connect exception: " << e.what() << "\n";
|
||||||
|
auto self = this->shared_from_this();
|
||||||
|
read_timer_.expires_after(std::chrono::milliseconds(100));
|
||||||
|
read_timer_.async_wait([self, this](const boost::system::error_code& ec) {
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "Error during async wait: " << ec.message() << "\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PosixStream::do_disconnect()
|
||||||
|
{
|
||||||
|
if (stream_->is_open())
|
||||||
|
stream_->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PosixStream::do_read()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!stream_->is_open())
|
||||||
|
throw SnapException("failed to open stream: \"" + uri_.path + "\"");
|
||||||
|
|
||||||
|
int toRead = chunk_->payloadSize;
|
||||||
|
int len = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int count = read(stream_->native_handle(), chunk_->payload + len, toRead - len);
|
||||||
|
if (count < 0)
|
||||||
|
{
|
||||||
|
LOG(DEBUG, LOG_TAG) << "count < 0: " << errno
|
||||||
|
<< " && idleBytes < maxIdleBytes, ms: " << 1000 * chunk_->payloadSize / (sampleFormat_.rate * sampleFormat_.frameSize)
|
||||||
|
<< "\n";
|
||||||
|
memset(chunk_->payload + len, 0, toRead - len);
|
||||||
|
len += toRead - len;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (count == 0)
|
||||||
|
{
|
||||||
|
throw SnapException("end of file");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// LOG(DEBUG) << "count: " << count << "\n";
|
||||||
|
len += count;
|
||||||
|
bytes_read_ += len;
|
||||||
|
}
|
||||||
|
} while (len < toRead);
|
||||||
|
|
||||||
|
if (first_)
|
||||||
|
{
|
||||||
|
first_ = false;
|
||||||
|
chronos::systemtimeofday(&tvEncodedChunk_);
|
||||||
|
nextTick_ = chronos::getTickCount() + buffer_ms_;
|
||||||
|
}
|
||||||
|
encoder_->encode(chunk_.get());
|
||||||
|
nextTick_ += chunk_ms_;
|
||||||
|
long currentTick = chronos::getTickCount();
|
||||||
|
|
||||||
|
if (nextTick_ >= currentTick)
|
||||||
|
{
|
||||||
|
auto self = this->shared_from_this();
|
||||||
|
read_timer_.expires_after(std::chrono::milliseconds(nextTick_ - currentTick));
|
||||||
|
read_timer_.async_wait([self, this](const boost::system::error_code& ec) {
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "Error during async wait: " << ec.message() << "\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
do_read();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pcmListener_->onResync(this, currentTick - nextTick_);
|
||||||
|
nextTick_ = currentTick + buffer_ms_;
|
||||||
|
first_ = true;
|
||||||
|
do_read();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastException_ = "";
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
if (lastException_ != e.what())
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << std::endl;
|
||||||
|
lastException_ = e.what();
|
||||||
|
}
|
||||||
|
disconnect();
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace streamreader
|
52
server/streamreader/posix_stream.hpp
Normal file
52
server/streamreader/posix_stream.hpp
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/***
|
||||||
|
This file is part of snapcast
|
||||||
|
Copyright (C) 2014-2020 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 POSIX_STREAM_HPP
|
||||||
|
#define POSIX_STREAM_HPP
|
||||||
|
|
||||||
|
#include "asio_stream.hpp"
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
|
using boost::asio::posix::stream_descriptor;
|
||||||
|
|
||||||
|
|
||||||
|
/// Reads and decodes PCM data from a file descriptor
|
||||||
|
/**
|
||||||
|
* Reads PCM from a file descriptor and passes the data to an encoder.
|
||||||
|
* Implements EncoderListener to get the encoded data.
|
||||||
|
* Data is passed to the PcmListener
|
||||||
|
*/
|
||||||
|
class PosixStream : public AsioStream<stream_descriptor>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// ctor. Encoded PCM data is passed to the PipeListener
|
||||||
|
PosixStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void connect() override;
|
||||||
|
void do_disconnect() override;
|
||||||
|
void do_read() override;
|
||||||
|
std::string lastException_;
|
||||||
|
size_t dryout_ms_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
|
#endif
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
@ -27,6 +27,9 @@
|
||||||
// Copyright (c) 2015-2016 Ole Christian Eidheim
|
// Copyright (c) 2015-2016 Ole Christian Eidheim
|
||||||
// Thanks, Christian :-)
|
// Thanks, Christian :-)
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
/// Create a new process given command and run path.
|
/// Create a new process given command and run path.
|
||||||
/// Thus, at the moment, if read_stdout==nullptr, read_stderr==nullptr and open_stdin==false,
|
/// Thus, at the moment, if read_stdout==nullptr, read_stderr==nullptr and open_stdin==false,
|
||||||
/// the stdout, stderr and stdin are sent to the parent process instead.
|
/// the stdout, stderr and stdin are sent to the parent process instead.
|
||||||
|
@ -239,4 +242,6 @@ private:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif // TINY_PROCESS_LIBRARY_HPP_
|
#endif // TINY_PROCESS_LIBRARY_HPP_
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -29,25 +29,19 @@
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
|
static constexpr auto LOG_TAG = "ProcessStream";
|
||||||
|
|
||||||
|
|
||||||
ProcessStream::ProcessStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
|
ProcessStream::ProcessStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
|
||||||
: PcmStream(pcmListener, ioc, uri), path_(""), process_(nullptr)
|
: PosixStream(pcmListener, ioc, uri), path_(""), process_(nullptr)
|
||||||
{
|
{
|
||||||
params_ = uri_.getQuery("params");
|
params_ = uri_.getQuery("params");
|
||||||
logStderr_ = (uri_.getQuery("logStderr", "false") == "true");
|
wd_timeout_sec_ = cpt::stoul(uri_.getQuery("wd_timeout", "0"));
|
||||||
|
LOG(DEBUG, LOG_TAG) << "Watchdog timeout: " << wd_timeout_sec_ << "\n";
|
||||||
if (uri_.query.find("dryout_ms") != uri_.query.end())
|
logStderr_ = (uri_.getQuery("log_stderr", "false") == "true");
|
||||||
dryoutMs_ = cpt::stoul(uri_.query["dryout_ms"]);
|
|
||||||
else
|
|
||||||
dryoutMs_ = 2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ProcessStream::~ProcessStream()
|
|
||||||
{
|
|
||||||
if (process_)
|
|
||||||
process_->kill();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,140 +97,81 @@ void ProcessStream::initExeAndPath(const std::string& filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void ProcessStream::start()
|
void ProcessStream::do_connect()
|
||||||
{
|
{
|
||||||
|
if (!active_)
|
||||||
|
return;
|
||||||
initExeAndPath(uri_.path);
|
initExeAndPath(uri_.path);
|
||||||
PcmStream::start();
|
LOG(DEBUG, LOG_TAG) << "Launching: '" << path_ + exe_ << "', with params: '" << params_ << "', in path: '" << path_ << "'\n";
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void ProcessStream::stop()
|
|
||||||
{
|
|
||||||
if (process_)
|
|
||||||
process_->kill();
|
|
||||||
PcmStream::stop();
|
|
||||||
|
|
||||||
/// thread is detached, so it is not joinable
|
|
||||||
if (stderrReaderThread_.joinable())
|
|
||||||
stderrReaderThread_.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void ProcessStream::onStderrMsg(const char* buffer, size_t n)
|
|
||||||
{
|
|
||||||
if (logStderr_)
|
|
||||||
{
|
|
||||||
string line = utils::string::trim_copy(string(buffer, n));
|
|
||||||
if ((line.find('\0') == string::npos) && !line.empty())
|
|
||||||
LOG(INFO) << "(" << getName() << ") " << line << "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void ProcessStream::stderrReader()
|
|
||||||
{
|
|
||||||
size_t buffer_size = 8192;
|
|
||||||
auto buffer = std::unique_ptr<char[]>(new char[buffer_size]);
|
|
||||||
ssize_t n;
|
|
||||||
stringstream message;
|
|
||||||
while (active_ && (n = read(process_->getStderr(), buffer.get(), buffer_size)) > 0)
|
|
||||||
onStderrMsg(buffer.get(), n);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void ProcessStream::worker()
|
|
||||||
{
|
|
||||||
timeval tvChunk;
|
|
||||||
std::unique_ptr<msg::PcmChunk> chunk(new msg::PcmChunk(sampleFormat_, chunk_ms_));
|
|
||||||
setState(ReaderState::kPlaying);
|
|
||||||
string lastException = "";
|
|
||||||
|
|
||||||
while (active_)
|
|
||||||
{
|
|
||||||
process_.reset(new Process(path_ + exe_ + " " + params_, path_));
|
process_.reset(new Process(path_ + exe_ + " " + params_, path_));
|
||||||
int flags = fcntl(process_->getStdout(), F_GETFL, 0);
|
int flags = fcntl(process_->getStdout(), F_GETFL, 0);
|
||||||
fcntl(process_->getStdout(), F_SETFL, flags | O_NONBLOCK);
|
fcntl(process_->getStdout(), F_SETFL, flags | O_NONBLOCK);
|
||||||
|
stream_ = make_unique<stream_descriptor>(ioc_, process_->getStdout());
|
||||||
stderrReaderThread_ = thread(&ProcessStream::stderrReader, this);
|
stream_stderr_ = make_unique<stream_descriptor>(ioc_, process_->getStderr());
|
||||||
stderrReaderThread_.detach();
|
on_connect();
|
||||||
|
if (wd_timeout_sec_ > 0)
|
||||||
chronos::systemtimeofday(&tvChunk);
|
|
||||||
tvEncodedChunk_ = tvChunk;
|
|
||||||
long nextTick = chronos::getTickCount();
|
|
||||||
int idleBytes = 0;
|
|
||||||
int maxIdleBytes = sampleFormat_.rate * sampleFormat_.frameSize * dryoutMs_ / 1000;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
while (active_)
|
watchdog_ = make_unique<Watchdog>(ioc_, this);
|
||||||
{
|
watchdog_->start(std::chrono::seconds(wd_timeout_sec_));
|
||||||
chunk->timestamp.sec = tvChunk.tv_sec;
|
|
||||||
chunk->timestamp.usec = tvChunk.tv_usec;
|
|
||||||
int toRead = chunk->payloadSize;
|
|
||||||
int len = 0;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int count = read(process_->getStdout(), chunk->payload + len, toRead - len);
|
|
||||||
if (count < 0 && idleBytes < maxIdleBytes)
|
|
||||||
{
|
|
||||||
memset(chunk->payload + len, 0, toRead - len);
|
|
||||||
idleBytes += toRead - len;
|
|
||||||
len += toRead - len;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (count < 0)
|
|
||||||
{
|
|
||||||
setState(ReaderState::kIdle);
|
|
||||||
if (!sleep(100))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (count == 0)
|
|
||||||
throw SnapException("end of file");
|
|
||||||
else
|
|
||||||
{
|
|
||||||
len += count;
|
|
||||||
idleBytes = 0;
|
|
||||||
}
|
|
||||||
} while ((len < toRead) && active_);
|
|
||||||
|
|
||||||
if (!active_)
|
|
||||||
break;
|
|
||||||
|
|
||||||
encoder_->encode(chunk.get());
|
|
||||||
|
|
||||||
if (!active_)
|
|
||||||
break;
|
|
||||||
|
|
||||||
nextTick += chunk_ms_;
|
|
||||||
chronos::addUs(tvChunk, chunk_ms_ * 1000);
|
|
||||||
long currentTick = chronos::getTickCount();
|
|
||||||
|
|
||||||
if (nextTick >= currentTick)
|
|
||||||
{
|
|
||||||
setState(ReaderState::kPlaying);
|
|
||||||
if (!sleep(nextTick - currentTick))
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
chronos::systemtimeofday(&tvChunk);
|
watchdog_ = nullptr;
|
||||||
tvEncodedChunk_ = tvChunk;
|
|
||||||
pcmListener_->onResync(this, currentTick - nextTick);
|
|
||||||
nextTick = currentTick;
|
|
||||||
}
|
}
|
||||||
|
stderrReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
lastException = "";
|
|
||||||
}
|
void ProcessStream::do_disconnect()
|
||||||
}
|
{
|
||||||
catch (const std::exception& e)
|
if (process_)
|
||||||
{
|
|
||||||
if (lastException != e.what())
|
|
||||||
{
|
|
||||||
LOG(ERROR) << "(PipeStream) Exception: " << e.what() << std::endl;
|
|
||||||
lastException = e.what();
|
|
||||||
}
|
|
||||||
process_->kill();
|
process_->kill();
|
||||||
if (!sleep(30000))
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
void ProcessStream::onStderrMsg(const std::string& line)
|
||||||
|
{
|
||||||
|
if (logStderr_)
|
||||||
|
{
|
||||||
|
LOG(INFO, LOG_TAG) << "(" << getName() << ") " << line << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ProcessStream::stderrReadLine()
|
||||||
|
{
|
||||||
|
const std::string delimiter = "\n";
|
||||||
|
auto self(shared_from_this());
|
||||||
|
boost::asio::async_read_until(
|
||||||
|
*stream_stderr_, streambuf_stderr_, delimiter, [this, self, delimiter](const std::error_code& ec, std::size_t bytes_transferred) {
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "Error while reading from control socket: " << ec.message() << "\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watchdog_)
|
||||||
|
watchdog_->trigger();
|
||||||
|
|
||||||
|
// Extract up to the first delimiter.
|
||||||
|
std::string line{buffers_begin(streambuf_stderr_.data()), buffers_begin(streambuf_stderr_.data()) + bytes_transferred - delimiter.length()};
|
||||||
|
if (!line.empty())
|
||||||
|
{
|
||||||
|
if (line.back() == '\r')
|
||||||
|
line.resize(line.size() - 1);
|
||||||
|
onStderrMsg(line);
|
||||||
|
}
|
||||||
|
streambuf_stderr_.consume(bytes_transferred);
|
||||||
|
stderrReadLine();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ProcessStream::onTimeout(const Watchdog& /*watchdog*/, std::chrono::milliseconds ms)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "Watchdog timeout: " << ms.count() / 1000 << "s\n";
|
||||||
|
if (process_)
|
||||||
|
process_->kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,16 +16,18 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef PROCESS_STREAM_H
|
#ifndef PROCESS_STREAM_HPP
|
||||||
#define PROCESS_STREAM_H
|
#define PROCESS_STREAM_HPP
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "pcm_stream.hpp"
|
#include "posix_stream.hpp"
|
||||||
#include "process.hpp"
|
#include "process.hpp"
|
||||||
|
#include "watchdog.hpp"
|
||||||
|
|
||||||
// TODO: switch to AsioStream, maybe use boost::process library
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
/// Starts an external process and reads and PCM data from stdout
|
/// Starts an external process and reads and PCM data from stdout
|
||||||
/**
|
/**
|
||||||
|
@ -33,33 +35,39 @@
|
||||||
* Implements EncoderListener to get the encoded data.
|
* Implements EncoderListener to get the encoded data.
|
||||||
* Data is passed to the PcmListener
|
* Data is passed to the PcmListener
|
||||||
*/
|
*/
|
||||||
class ProcessStream : public PcmStream
|
class ProcessStream : public PosixStream, public WatchdogListener
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
/// ctor. Encoded PCM data is passed to the PipeListener
|
/// ctor. Encoded PCM data is passed to the PipeListener
|
||||||
ProcessStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
ProcessStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
||||||
~ProcessStream() override;
|
~ProcessStream() override = default;
|
||||||
|
|
||||||
void start() override;
|
|
||||||
void stop() override;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void do_connect() override;
|
||||||
|
void do_disconnect() override;
|
||||||
|
|
||||||
std::string exe_;
|
std::string exe_;
|
||||||
std::string path_;
|
std::string path_;
|
||||||
std::string params_;
|
std::string params_;
|
||||||
std::unique_ptr<Process> process_;
|
std::unique_ptr<Process> process_;
|
||||||
std::thread stderrReaderThread_;
|
|
||||||
bool logStderr_;
|
|
||||||
size_t dryoutMs_;
|
|
||||||
|
|
||||||
void worker() override;
|
bool logStderr_;
|
||||||
virtual void stderrReader();
|
boost::asio::streambuf streambuf_stderr_;
|
||||||
virtual void onStderrMsg(const char* buffer, size_t n);
|
std::unique_ptr<stream_descriptor> stream_stderr_;
|
||||||
|
|
||||||
|
// void worker() override;
|
||||||
|
virtual void stderrReadLine();
|
||||||
|
virtual void onStderrMsg(const std::string& line);
|
||||||
virtual void initExeAndPath(const std::string& filename);
|
virtual void initExeAndPath(const std::string& filename);
|
||||||
|
|
||||||
bool fileExists(const std::string& filename);
|
bool fileExists(const std::string& filename);
|
||||||
std::string findExe(const std::string& filename);
|
std::string findExe(const std::string& filename);
|
||||||
|
|
||||||
|
size_t wd_timeout_sec_;
|
||||||
|
std::unique_ptr<Watchdog> watchdog_;
|
||||||
|
void onTimeout(const Watchdog& watchdog, std::chrono::milliseconds ms) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,13 +25,14 @@
|
||||||
#include "file_stream.hpp"
|
#include "file_stream.hpp"
|
||||||
#include "librespot_stream.hpp"
|
#include "librespot_stream.hpp"
|
||||||
#include "pipe_stream.hpp"
|
#include "pipe_stream.hpp"
|
||||||
// #include "pipe_stream_old.hpp"
|
|
||||||
#include "process_stream.hpp"
|
#include "process_stream.hpp"
|
||||||
#include "tcp_stream.hpp"
|
#include "tcp_stream.hpp"
|
||||||
|
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
StreamManager::StreamManager(PcmListener* pcmListener, boost::asio::io_context& ioc, const std::string& defaultSampleFormat, const std::string& defaultCodec,
|
StreamManager::StreamManager(PcmListener* pcmListener, boost::asio::io_context& ioc, const std::string& defaultSampleFormat, const std::string& defaultCodec,
|
||||||
size_t defaultChunkBufferMs)
|
size_t defaultChunkBufferMs)
|
||||||
|
@ -64,10 +65,6 @@ PcmStreamPtr StreamManager::addStream(const std::string& uri)
|
||||||
{
|
{
|
||||||
stream = make_shared<PipeStream>(pcmListener_, ioc_, streamUri);
|
stream = make_shared<PipeStream>(pcmListener_, ioc_, streamUri);
|
||||||
}
|
}
|
||||||
// else if (streamUri.scheme == "pipe.old")
|
|
||||||
// {
|
|
||||||
// stream = make_shared<PipeStreamOld>(pcmListener_, ioc_, streamUri);
|
|
||||||
// }
|
|
||||||
else if (streamUri.scheme == "file")
|
else if (streamUri.scheme == "file")
|
||||||
{
|
{
|
||||||
stream = make_shared<FileStream>(pcmListener_, ioc_, streamUri);
|
stream = make_shared<FileStream>(pcmListener_, ioc_, streamUri);
|
||||||
|
@ -95,7 +92,7 @@ PcmStreamPtr StreamManager::addStream(const std::string& uri)
|
||||||
|
|
||||||
if (stream)
|
if (stream)
|
||||||
{
|
{
|
||||||
for (auto s : streams_)
|
for (const auto& s : streams_)
|
||||||
{
|
{
|
||||||
if (s->getName() == stream->getName())
|
if (s->getName() == stream->getName())
|
||||||
throw SnapException("Stream with name \"" + stream->getName() + "\" already exists");
|
throw SnapException("Stream with name \"" + stream->getName() + "\" already exists");
|
||||||
|
@ -165,3 +162,5 @@ json StreamManager::toJson() const
|
||||||
result.push_back(stream->toJson());
|
result.push_back(stream->toJson());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
#ifndef PCM_READER_FACTORY_H
|
/***
|
||||||
#define PCM_READER_FACTORY_H
|
This file is part of snapcast
|
||||||
|
Copyright (C) 2014-2020 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 STREAM_MANAGER_HPP
|
||||||
|
#define STREAM_MANAGER_HPP
|
||||||
|
|
||||||
#include "pcm_stream.hpp"
|
#include "pcm_stream.hpp"
|
||||||
#include <boost/asio/io_context.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
|
@ -7,6 +25,9 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
typedef std::shared_ptr<PcmStream> PcmStreamPtr;
|
typedef std::shared_ptr<PcmStream> PcmStreamPtr;
|
||||||
|
|
||||||
class StreamManager
|
class StreamManager
|
||||||
|
@ -33,5 +54,6 @@ private:
|
||||||
boost::asio::io_context& ioc_;
|
boost::asio::io_context& ioc_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,6 +25,8 @@
|
||||||
using namespace std;
|
using namespace std;
|
||||||
namespace strutils = utils::string;
|
namespace strutils = utils::string;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
StreamUri::StreamUri(const std::string& uri)
|
StreamUri::StreamUri(const std::string& uri)
|
||||||
{
|
{
|
||||||
|
@ -32,7 +34,6 @@ StreamUri::StreamUri(const std::string& uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void StreamUri::parse(const std::string& streamUri)
|
void StreamUri::parse(const std::string& streamUri)
|
||||||
{
|
{
|
||||||
// https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
|
// https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
|
||||||
|
@ -146,3 +147,4 @@ std::string StreamUri::getQuery(const std::string& key, const std::string& def)
|
||||||
return iter->second;
|
return iter->second;
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,8 +16,8 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef READER_URI_H
|
#ifndef STREAM_URI_HPP
|
||||||
#define READER_URI_H
|
#define STREAM_URI_HPP
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
@ -27,6 +27,8 @@
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
// scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
|
// scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
|
||||||
struct StreamUri
|
struct StreamUri
|
||||||
|
@ -56,5 +58,6 @@ struct StreamUri
|
||||||
std::string toString() const;
|
std::string toString() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -32,7 +32,8 @@
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
TcpStream::TcpStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
|
TcpStream::TcpStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
|
||||||
: AsioStream<tcp::socket>(pcmListener, ioc, uri), reconnect_timer_(ioc)
|
: AsioStream<tcp::socket>(pcmListener, ioc, uri), reconnect_timer_(ioc)
|
||||||
|
@ -62,7 +63,7 @@ TcpStream::TcpStream(PcmListener* pcmListener, boost::asio::io_context& ioc, con
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void TcpStream::connect()
|
void TcpStream::do_connect()
|
||||||
{
|
{
|
||||||
if (!active_)
|
if (!active_)
|
||||||
return;
|
return;
|
||||||
|
@ -108,7 +109,7 @@ void TcpStream::connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void TcpStream::disconnect()
|
void TcpStream::do_disconnect()
|
||||||
{
|
{
|
||||||
if (stream_)
|
if (stream_)
|
||||||
stream_->close();
|
stream_->close();
|
||||||
|
@ -116,3 +117,4 @@ void TcpStream::disconnect()
|
||||||
acceptor_->cancel();
|
acceptor_->cancel();
|
||||||
reconnect_timer_.cancel();
|
reconnect_timer_.cancel();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,13 +16,15 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef TCP_STREAM_H
|
#ifndef TCP_STREAM_HPP
|
||||||
#define TCP_STREAM_H
|
#define TCP_STREAM_HPP
|
||||||
|
|
||||||
#include "asio_stream.hpp"
|
#include "asio_stream.hpp"
|
||||||
|
|
||||||
using boost::asio::ip::tcp;
|
using boost::asio::ip::tcp;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
/// Reads and decodes PCM data from a named pipe
|
/// Reads and decodes PCM data from a named pipe
|
||||||
/**
|
/**
|
||||||
|
@ -37,8 +39,8 @@ public:
|
||||||
TcpStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
TcpStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void connect() override;
|
void do_connect() override;
|
||||||
void disconnect() override;
|
void do_disconnect() override;
|
||||||
std::unique_ptr<tcp::acceptor> acceptor_;
|
std::unique_ptr<tcp::acceptor> acceptor_;
|
||||||
std::string host_;
|
std::string host_;
|
||||||
size_t port_;
|
size_t port_;
|
||||||
|
@ -46,5 +48,6 @@ protected:
|
||||||
boost::asio::steady_timer reconnect_timer_;
|
boost::asio::steady_timer reconnect_timer_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,14 +16,16 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#include "watchdog.h"
|
#include "watchdog.hpp"
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
Watchdog::Watchdog(WatchdogListener* listener) : listener_(listener), thread_(nullptr), active_(false)
|
Watchdog::Watchdog(boost::asio::io_context& ioc, WatchdogListener* listener) : timer_(ioc), listener_(listener)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,49 +36,29 @@ Watchdog::~Watchdog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Watchdog::start(size_t timeoutMs)
|
void Watchdog::start(const std::chrono::milliseconds& timeout)
|
||||||
{
|
{
|
||||||
timeoutMs_ = timeoutMs;
|
timeout_ms_ = timeout;
|
||||||
if (!thread_ || !active_)
|
|
||||||
{
|
|
||||||
active_ = true;
|
|
||||||
thread_.reset(new thread(&Watchdog::worker, this));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
trigger();
|
trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Watchdog::stop()
|
void Watchdog::stop()
|
||||||
{
|
{
|
||||||
active_ = false;
|
timer_.cancel();
|
||||||
trigger();
|
|
||||||
if (thread_ && thread_->joinable())
|
|
||||||
thread_->join();
|
|
||||||
thread_ = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Watchdog::trigger()
|
void Watchdog::trigger()
|
||||||
{
|
{
|
||||||
// std::unique_lock<std::mutex> lck(mtx_);
|
timer_.cancel();
|
||||||
cv_.notify_one();
|
timer_.expires_after(timeout_ms_);
|
||||||
|
timer_.async_wait([this](const boost::system::error_code& ec) {
|
||||||
|
if (!ec)
|
||||||
|
{
|
||||||
|
listener_->onTimeout(*this, timeout_ms_);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
void Watchdog::worker()
|
|
||||||
{
|
|
||||||
while (active_)
|
|
||||||
{
|
|
||||||
std::unique_lock<std::mutex> lck(mtx_);
|
|
||||||
if (cv_.wait_for(lck, std::chrono::milliseconds(timeoutMs_)) == std::cv_status::timeout)
|
|
||||||
{
|
|
||||||
if (listener_)
|
|
||||||
{
|
|
||||||
listener_->onTimeout(this, timeoutMs_);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
active_ = false;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/***
|
/***
|
||||||
This file is part of snapcast
|
This file is part of snapcast
|
||||||
Copyright (C) 2014-2019 Johannes Pohl
|
Copyright (C) 2014-2020 Johannes Pohl
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,15 +16,14 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
***/
|
***/
|
||||||
|
|
||||||
#ifndef WATCH_DOG_H
|
#ifndef WATCH_DOG_HPP
|
||||||
#define WATCH_DOG_H
|
#define WATCH_DOG_HPP
|
||||||
|
|
||||||
#include <atomic>
|
#include <boost/asio.hpp>
|
||||||
#include <condition_variable>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
|
namespace streamreader
|
||||||
|
{
|
||||||
|
|
||||||
class Watchdog;
|
class Watchdog;
|
||||||
|
|
||||||
|
@ -32,7 +31,7 @@ class Watchdog;
|
||||||
class WatchdogListener
|
class WatchdogListener
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual void onTimeout(const Watchdog* watchdog, size_t ms) = 0;
|
virtual void onTimeout(const Watchdog& watchdog, std::chrono::milliseconds ms) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,23 +39,19 @@ public:
|
||||||
class Watchdog
|
class Watchdog
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Watchdog(WatchdogListener* listener = nullptr);
|
Watchdog(boost::asio::io_context& ioc, WatchdogListener* listener = nullptr);
|
||||||
virtual ~Watchdog();
|
virtual ~Watchdog();
|
||||||
|
|
||||||
void start(size_t timeoutMs);
|
void start(const std::chrono::milliseconds& timeout);
|
||||||
void stop();
|
void stop();
|
||||||
void trigger();
|
void trigger();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
boost::asio::steady_timer timer_;
|
||||||
WatchdogListener* listener_;
|
WatchdogListener* listener_;
|
||||||
std::condition_variable cv_;
|
std::chrono::milliseconds timeout_ms_;
|
||||||
std::mutex mtx_;
|
|
||||||
std::unique_ptr<std::thread> thread_;
|
|
||||||
size_t timeoutMs_;
|
|
||||||
std::atomic<bool> active_;
|
|
||||||
|
|
||||||
void worker();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace streamreader
|
||||||
|
|
||||||
#endif
|
#endif
|
Loading…
Add table
Add a link
Reference in a new issue