mirror of
https://github.com/badaix/snapcast.git
synced 2025-05-12 08:36:43 +02:00
Add null encoder for use with meta streams
This commit is contained in:
parent
7c1c257501
commit
876f424bae
12 changed files with 137 additions and 22 deletions
|
@ -98,6 +98,7 @@ Available stream sources are:
|
|||
- [file](doc/configuration.md#file): read PCM audio from a file
|
||||
- [process](doc/configuration.md#process): launches a process and reads audio from stdout
|
||||
- [tcp](doc/configuration.md#tcp-server): receives audio from a TCP socket, can act as client or server
|
||||
- [meta](doc/configuration.md#meta): read and mix audio from other stream sources
|
||||
|
||||
## Test
|
||||
|
||||
|
|
|
@ -197,3 +197,14 @@ The output of any audio player that uses alsa can be redirected to Snapcast by u
|
|||
[stream]
|
||||
stream = alsa://?name=SomeName&sampleformat=48000:16:2&device=hw:0,1,0
|
||||
```
|
||||
|
||||
### meta
|
||||
|
||||
Read and mix audio from other stream sources
|
||||
|
||||
```sh
|
||||
meta:///<name of source#1>/<name of source#2>/.../<name of source#N>?name=<name>
|
||||
```
|
||||
|
||||
Plays audio from the active source with the highest priority, with `source#1` having the highest priority and `source#N` the lowest.
|
||||
Use `codec=null` for stream sources that should only serve as input for meta streams
|
||||
|
|
|
@ -12,6 +12,7 @@ set(SERVER_SOURCES
|
|||
stream_session_ws.cpp
|
||||
encoder/encoder_factory.cpp
|
||||
encoder/pcm_encoder.cpp
|
||||
encoder/null_encoder.cpp
|
||||
streamreader/base64.cpp
|
||||
streamreader/stream_uri.cpp
|
||||
streamreader/stream_manager.cpp
|
||||
|
|
|
@ -44,7 +44,7 @@ endif
|
|||
|
||||
CXXFLAGS += $(ADD_CFLAGS) -std=c++14 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_VORBIS -DHAS_VORBIS_ENC -DHAS_OPUS -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
|
||||
LDFLAGS += $(ADD_LDFLAGS) -lvorbis -lvorbisenc -logg -lFLAC -lopus -lsoxr
|
||||
OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/meta_stream.o streamreader/librespot_stream.o streamreader/watchdog.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/ogg_encoder.o ../common/sample_format.o ../common/resampler.o
|
||||
OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/meta_stream.o streamreader/librespot_stream.o streamreader/watchdog.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/null_encoder.o encoder/ogg_encoder.o ../common/sample_format.o ../common/resampler.o
|
||||
|
||||
ifneq (,$(TARGET))
|
||||
CXXFLAGS += -D$(TARGET)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
***/
|
||||
|
||||
#include "encoder_factory.hpp"
|
||||
#include "null_encoder.hpp"
|
||||
#include "pcm_encoder.hpp"
|
||||
#if defined(HAS_OGG) && defined(HAS_VORBIS) && defined(HAS_VORBIS_ENC)
|
||||
#include "ogg_encoder.hpp"
|
||||
|
@ -48,6 +49,8 @@ std::unique_ptr<Encoder> EncoderFactory::createEncoder(const std::string& codecS
|
|||
}
|
||||
if (codec == "pcm")
|
||||
return std::make_unique<PcmEncoder>(codecOptions);
|
||||
else if (codec == "null")
|
||||
return std::make_unique<NullEncoder>(codecOptions);
|
||||
#if defined(HAS_OGG) && defined(HAS_VORBIS) && defined(HAS_VORBIS_ENC)
|
||||
else if (codec == "ogg")
|
||||
return std::make_unique<OggEncoder>(codecOptions);
|
||||
|
|
48
server/encoder/null_encoder.cpp
Normal file
48
server/encoder/null_encoder.cpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
/***
|
||||
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/>.
|
||||
***/
|
||||
|
||||
#include "null_encoder.hpp"
|
||||
|
||||
|
||||
namespace encoder
|
||||
{
|
||||
|
||||
|
||||
NullEncoder::NullEncoder(const std::string& codecOptions) : Encoder(codecOptions)
|
||||
{
|
||||
headerChunk_.reset(new msg::CodecHeader("null"));
|
||||
}
|
||||
|
||||
|
||||
void NullEncoder::encode(const msg::PcmChunk& chunk)
|
||||
{
|
||||
std::ignore = chunk;
|
||||
}
|
||||
|
||||
|
||||
void NullEncoder::initEncoder()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
std::string NullEncoder::name() const
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
} // namespace encoder
|
39
server/encoder/null_encoder.hpp
Normal file
39
server/encoder/null_encoder.hpp
Normal file
|
@ -0,0 +1,39 @@
|
|||
/***
|
||||
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 NULL_ENCODER_HPP
|
||||
#define NULL_ENCODER_HPP
|
||||
#include "encoder.hpp"
|
||||
|
||||
namespace encoder
|
||||
{
|
||||
|
||||
class NullEncoder : public Encoder
|
||||
{
|
||||
public:
|
||||
NullEncoder(const std::string& codecOptions = "");
|
||||
void encode(const msg::PcmChunk& chunk) override;
|
||||
std::string name() const override;
|
||||
|
||||
protected:
|
||||
void initEncoder() override;
|
||||
};
|
||||
|
||||
} // namespace encoder
|
||||
|
||||
#endif
|
|
@ -126,6 +126,7 @@ doc_root = /usr/share/snapserver/snapweb
|
|||
# tcp server: tcp://<listen IP, e.g. 127.0.0.1>:<port>?name=<name>[&mode=server]
|
||||
# tcp client: tcp://<server IP, e.g. 127.0.0.1>:<port>?name=<name>&mode=client
|
||||
# alsa: alsa://?name=<name>&device=<alsa device>
|
||||
# meta: meta:///<name of source#1>/<name of source#2>/.../<name of source#N>?name=<name>
|
||||
source = pipe:///tmp/snapfifo?name=default
|
||||
#source = tcp://127.0.0.1?name=mopidy_tcp
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace streamreader
|
|||
{
|
||||
|
||||
static constexpr auto LOG_TAG = "MetaStream";
|
||||
static constexpr auto kResyncTolerance = 50ms;
|
||||
// static constexpr auto kResyncTolerance = 50ms;
|
||||
|
||||
|
||||
MetaStream::MetaStream(PcmListener* pcmListener, std::vector<std::shared_ptr<PcmStream>> streams, boost::asio::io_context& ioc, const StreamUri& uri)
|
||||
|
@ -40,7 +40,7 @@ MetaStream::MetaStream(PcmListener* pcmListener, std::vector<std::shared_ptr<Pcm
|
|||
{
|
||||
if (component.empty())
|
||||
continue;
|
||||
LOG(INFO, LOG_TAG) << "Stream: " << component << "\n";
|
||||
|
||||
bool found = false;
|
||||
for (const auto stream : streams)
|
||||
{
|
||||
|
@ -56,9 +56,6 @@ MetaStream::MetaStream(PcmListener* pcmListener, std::vector<std::shared_ptr<Pcm
|
|||
throw SnapException("Unknown stream: \"" + component + "\"");
|
||||
}
|
||||
|
||||
for (const auto stream : streams_)
|
||||
LOG(INFO, LOG_TAG) << "Stream: " << stream->getName() << ", " << stream->getUri().toString() << "\n";
|
||||
|
||||
if (!streams_.empty())
|
||||
{
|
||||
active_stream_ = streams_.front();
|
||||
|
@ -108,6 +105,7 @@ void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state)
|
|||
|
||||
if (active_stream_ != stream)
|
||||
{
|
||||
LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", switching active stream: " << (active_stream_?active_stream_->getName():"<null>") << " => " << stream->getName() << "\n";
|
||||
active_stream_ = stream;
|
||||
resampler_ = make_unique<Resampler>(active_stream_->getSampleFormat(), sampleFormat_);
|
||||
}
|
||||
|
@ -146,16 +144,16 @@ void MetaStream::onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& ch
|
|||
// Read took longer, wait for the buffer to fill up
|
||||
if (next_read < 0ms)
|
||||
{
|
||||
if (next_read >= -kResyncTolerance)
|
||||
{
|
||||
LOG(INFO, LOG_TAG) << "next read < 0 (" << getName() << "): " << std::chrono::duration_cast<std::chrono::microseconds>(next_read).count() / 1000.
|
||||
<< " ms\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// if (next_read >= -kResyncTolerance)
|
||||
// {
|
||||
// LOG(INFO, LOG_TAG) << "next read < 0 (" << getName() << "): " << std::chrono::duration_cast<std::chrono::microseconds>(next_read).count() / 1000.
|
||||
// << " ms\n";
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
resync(-next_read);
|
||||
first_read_ = true;
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
if (resampler_ && resampler_->resamplingNeeded())
|
||||
|
|
|
@ -50,7 +50,7 @@ PcmStream::PcmStream(PcmListener* pcmListener, boost::asio::io_context& ioc, con
|
|||
if (uri_.query.find(kUriSampleFormat) == uri_.query.end())
|
||||
throw SnapException("Stream URI must have a sampleformat");
|
||||
sampleFormat_ = SampleFormat(uri_.query[kUriSampleFormat]);
|
||||
LOG(INFO, LOG_TAG) << "PcmStream sampleFormat: " << sampleFormat_.toString() << "\n";
|
||||
LOG(INFO, LOG_TAG) << "PcmStream: " << name_ << ", sampleFormat: " << sampleFormat_.toString() << "\n";
|
||||
|
||||
if (uri_.query.find(kUriChunkMs) != uri_.query.end())
|
||||
chunk_ms_ = cpt::stoul(uri_.query[kUriChunkMs]);
|
||||
|
@ -95,9 +95,15 @@ const SampleFormat& PcmStream::getSampleFormat() const
|
|||
}
|
||||
|
||||
|
||||
std::string PcmStream::getCodec() const
|
||||
{
|
||||
return encoder_->name();
|
||||
}
|
||||
|
||||
|
||||
void PcmStream::start()
|
||||
{
|
||||
LOG(DEBUG, LOG_TAG) << "Start, sampleformat: " << sampleFormat_.toString() << "\n";
|
||||
LOG(DEBUG, LOG_TAG) << "Start: " << name_ << ", sampleformat: " << sampleFormat_.toString() << "\n";
|
||||
encoder_->init([this](const encoder::Encoder& encoder, std::shared_ptr<msg::PcmChunk> chunk, double duration) { chunkEncoded(encoder, chunk, duration); },
|
||||
sampleFormat_);
|
||||
active_ = true;
|
||||
|
@ -120,7 +126,7 @@ void PcmStream::setState(ReaderState newState)
|
|||
{
|
||||
if (newState != state_)
|
||||
{
|
||||
LOG(INFO, LOG_TAG) << "State changed: " << static_cast<int>(state_) << " => " << static_cast<int>(newState) << "\n";
|
||||
LOG(INFO, LOG_TAG) << "State changed: " << name_ << ", state: " << static_cast<int>(state_) << " => " << static_cast<int>(newState) << "\n";
|
||||
state_ = newState;
|
||||
for (auto* listener : pcmListeners_)
|
||||
{
|
||||
|
@ -212,7 +218,7 @@ void PcmStream::setMeta(const json& jtag)
|
|||
{
|
||||
meta_.reset(new msg::StreamTags(jtag));
|
||||
meta_->msg["STREAM"] = name_;
|
||||
LOG(INFO, LOG_TAG) << "metadata=" << meta_->msg.dump(4) << "\n";
|
||||
LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", metadata=" << meta_->msg.dump(4) << "\n";
|
||||
|
||||
// Trigger a stream update
|
||||
for (auto* listener : pcmListeners_)
|
||||
|
|
|
@ -90,6 +90,7 @@ public:
|
|||
virtual const std::string& getName() const;
|
||||
virtual const std::string& getId() const;
|
||||
virtual const SampleFormat& getSampleFormat() const;
|
||||
virtual std::string getCodec() const;
|
||||
|
||||
std::shared_ptr<msg::StreamTags> getMeta() const;
|
||||
void setMeta(const json& j);
|
||||
|
|
|
@ -153,7 +153,12 @@ const PcmStreamPtr StreamManager::getDefaultStream()
|
|||
if (streams_.empty())
|
||||
return nullptr;
|
||||
|
||||
return streams_.front();
|
||||
for (const auto stream: streams_)
|
||||
{
|
||||
if (stream->getCodec() != "null")
|
||||
return stream;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
|
@ -198,6 +203,7 @@ json StreamManager::toJson() const
|
|||
{
|
||||
json result = json::array();
|
||||
for (auto stream : streams_)
|
||||
if (stream->getCodec() != "null")
|
||||
result.push_back(stream->toJson());
|
||||
return result;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue