Added to the daemon the support for Multicast DNS (using Linux Avahi) to allow discovery of remote audio sources and of RTSP for SDP transfer.

Added to the WebUI the possibility to directly select a remote source SDP file for a Sink.

New files:
daemon/mdns_client.hpp,cpp -> mDNS client implementation using Avahi client library
daemon/rtsp_client.hpp,cpp -> RTSP client implementation used to transfer SDP file
daemon/utils.cpp -> used for common utility functions
.clang-format -> added clang-format configuration file

Modified files:
daemon/CMakeList.txt -> added support for Avahi and option WITH_AVAHI=[yes/no] to compile the daemon with or without Avahi mDNS support
daemon/config.hpp,cpp -> added configuration option mdns_enabled to enable or disable mDNS discovery at runtime
daemon/json.cpp -> extended JSON config with mdns_enabled option
daemon/browser.hpp,cpp -> added support for mDNS client to the browser
daemon/session_manager.cpp -> added support for RTSP protocol to Source URL field and fixed issue with SDP file parsing
webui/RemoteSources.js -> added visualization of mDNS remote sources
webui/SinkEdit.js -> added the possibility to directly select a remote source SDP file for a Sink
webui/SourceInfo.js -> added visualization of protocol source (SAP, mDNS or local) for a source
ubuntu-packages.sh -> added libavahi-client-dev to the list of required packages
build.sh -> added WITH_AVAHI=yes option when invoking CMake
README.md -> added notes about mDNS support via Avahi
daemon/README.md -> added notes about mDNS support via Avahi, support for RTSP protocol in source and new mdns_enabled config param

Additional minor changes to remaining files.
This commit is contained in:
Andrea Bondavalli 2020-03-29 19:41:56 +02:00
parent 596500b130
commit 5deb6c1927
31 changed files with 1062 additions and 121 deletions

View File

@ -8,6 +8,7 @@ The daemon uses the following open source:
* **Merging Technologies ALSA RAVENNA/AES67 Driver** licensed under [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html). * **Merging Technologies ALSA RAVENNA/AES67 Driver** licensed under [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html).
* **cpp-httplib** licensed under the [MIT License](https://github.com/yhirose/cpp-httplib/blob/master/LICENSE) * **cpp-httplib** licensed under the [MIT License](https://github.com/yhirose/cpp-httplib/blob/master/LICENSE)
* **Avahi common & client libraries** licensed under the [LGPL License](https://github.com/lathiat/avahi/blob/master/LICENSE)
* **Boost libraries** licensed under the [Boost Software License](https://www.boost.org/LICENSE_1_0.txt) * **Boost libraries** licensed under the [Boost Software License](https://www.boost.org/LICENSE_1_0.txt)
## Repository content ## ## Repository content ##
@ -21,6 +22,7 @@ The daemon can be cross-compiled for multiple platforms and implements the follo
* session handling and SDP parsing and creation * session handling and SDP parsing and creation
* HTTP REST API for control and configuration * HTTP REST API for control and configuration
* SAP discovery protocol and SAP browser * SAP discovery protocol and SAP browser
* mDNS sources discovery (using Avahi) and SDP transfer via RTSP
* IGMP handling for SAP, RTP and PTP multicast traffic * IGMP handling for SAP, RTP and PTP multicast traffic
The directory also contains the daemon regression tests in the [tests](daemon/tests) subdirectory. To run daemon tests install the ALSA RAVENNA/AES67 kernel module enter the [tests](daemon/tests) subdirectory and run *./daemon-test -l all* The directory also contains the daemon regression tests in the [tests](daemon/tests) subdirectory. To run daemon tests install the ALSA RAVENNA/AES67 kernel module enter the [tests](daemon/tests) subdirectory and run *./daemon-test -l all*
@ -65,6 +67,7 @@ The daemon and the demo have been tested with **Ubuntu 18.04** distro on **x86/A
* node version >= 8.10.0 * node version >= 8.10.0
* npm version >= 3.5.2 * npm version >= 3.5.2
* boost libraries version >= 1.65.1 * boost libraries version >= 1.65.1
* Avahi service discovery (if enabled) >= 0.7
The BeagleBone® Black board with ARM Cortex-A8 32-Bit processor was used for testing on ARMv7. The BeagleBone® Black board with ARM Cortex-A8 32-Bit processor was used for testing on ARMv7.
See [Ubuntu 18.04 on BeagleBone® Black](https://elinux.org/BeagleBoardUbuntu) for additional information about how to setup Ubuntu on this board. See [Ubuntu 18.04 on BeagleBone® Black](https://elinux.org/BeagleBoardUbuntu) for additional information about how to setup Ubuntu on this board.

View File

@ -40,7 +40,7 @@ cd ..
cd daemon cd daemon
echo "Building aes67-daemon ..." echo "Building aes67-daemon ..."
cmake . cmake -DWITH_AVAHI=ON .
make make
cd .. cd ..

157
daemon/.clang-format Normal file
View File

@ -0,0 +1,157 @@
---
Language: Cpp
# BasedOnStyle: Chromium
AccessModifierOffset: -1
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: true
BinPackParameters: false
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 80
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
- Regex: '^<.*\.h>'
Priority: 1
- Regex: '^<.*'
Priority: 2
- Regex: '.*'
Priority: 3
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 2
IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Left
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
CanonicalDelimiter: ''
BasedOnStyle: google
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Auto
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseTab: Never
...

View File

@ -1,12 +1,24 @@
cmake_minimum_required(VERSION 3.7.0) cmake_minimum_required(VERSION 3.7.0)
project(aes67-daemon CXX) project(aes67-daemon CXX)
enable_testing() enable_testing()
option(WITH_AVAHI "Include mDNS support via Avahi" OFF)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "-g -O3 -DBOOST_LOG_DYN_LINK -DBOOST_LOG_USE_NATIVE_SYSLOG -Wall") set(CMAKE_CXX_FLAGS "-g -DBOOST_LOG_DYN_LINK -DBOOST_LOG_USE_NATIVE_SYSLOG -Wall")
set(RAVENNA_ALSA_LKM "../3rdparty/ravenna-alsa-lkm/") set(RAVENNA_ALSA_LKM "../3rdparty/ravenna-alsa-lkm/")
set(CPP_HTTPLIB " ../3rdparty/cpp-httplib/") set(CPP_HTTPLIB " ../3rdparty/cpp-httplib/")
find_library(AVAHI_LIBRARY-COMMON NAMES avahi-common)
find_library(AVAHI_LIBRARY-CLIENT NAMES avahi-client)
find_path(AVAHI_INCLUDE_DIR avahi-client/publish.h)
set(AVAHI_LIBRARIES ${AVAHI_LIBRARY-COMMON} ${AVAHI_LIBRARY-CLIENT})
set(AVAHI_INCLUDE_DIRS ${AVAHI_INCLUDE_DIR})
find_package(Boost COMPONENTS system thread log program_options REQUIRED) find_package(Boost COMPONENTS system thread log program_options REQUIRED)
include_directories(aes67-daemon ${RAVENNA_ALSA_LKM}/common ${RAVENNA_ALSA_LKM}/driver ${CPP_HTTPLIB} ${Boost_INCLUDE_DIR}) include_directories(aes67-daemon ${RAVENNA_ALSA_LKM}/common ${RAVENNA_ALSA_LKM}/driver ${CPP_HTTPLIB} ${Boost_INCLUDE_DIR})
add_executable(aes67-daemon error_code.cpp json.cpp main.cpp driver_handler.cpp driver_manager.cpp session_manager.cpp http_server.cpp config.cpp interface.cpp log.cpp sap.cpp browser.cpp) add_executable(aes67-daemon error_code.cpp json.cpp main.cpp driver_handler.cpp driver_manager.cpp session_manager.cpp http_server.cpp config.cpp interface.cpp log.cpp sap.cpp browser.cpp rtsp_client.cpp mdns_client.cpp utils.cpp)
add_subdirectory(tests) add_subdirectory(tests)
target_link_libraries(aes67-daemon ${Boost_LIBRARIES}) target_link_libraries(aes67-daemon ${Boost_LIBRARIES})
if(WITH_AVAHI)
MESSAGE(STATUS "WITH_AVAHI")
add_definitions(-D_USE_AVAHI_)
include_directories(aes67-daemon ${AVAHI_INCLUDE_DIRS})
target_link_libraries(aes67-daemon ${AVAHI_LIBRARIES})
endif()

View File

@ -8,6 +8,7 @@ The daemon is responsible for:
* provide an HTTP REST API for the daemon control and configuration * provide an HTTP REST API for the daemon control and configuration
* session handling and SDP parsing and creation * session handling and SDP parsing and creation
* SAP discovery protocol and SAP browser * SAP discovery protocol and SAP browser
* mDNS sources discovery (using Avahi) and SDP transfer via RTSP
* IGMP handling for SAP and RTP sessions * IGMP handling for SAP and RTP sessions
## Configuration file ## ## Configuration file ##
@ -232,6 +233,9 @@ where:
> JSON string specifying the IP address of the specified network device. > JSON string specifying the IP address of the specified network device.
> **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the IP address of the network device at startup time and will monitor it periodically. > **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the IP address of the network device at startup time and will monitor it periodically.
> **mdns\_enabled**
> JSON boolean specifying whether the mDNS discovery is enabled or disabled.
### JSON PTP Config<a name="ptp-config"></a> ### ### JSON PTP Config<a name="ptp-config"></a> ###
Example Example
@ -383,7 +387,8 @@ where:
> JSON boolean specifying whether the source SDP file is fetched from the HTTP URL specified in the **source** parameter or the SDP in the **sdp** parameter is used. > JSON boolean specifying whether the source SDP file is fetched from the HTTP URL specified in the **source** parameter or the SDP in the **sdp** parameter is used.
> **source** > **source**
> JSON string specifying the HTTP URL of the source SDP file. This parameter is mandatory if **use\_sdp** is false. > JSON string specifying the URL of the source SDP file. At present HTTP and RTSP protocols are supported.
> This parameter is mandatory if **use\_sdp** is false.
> **sdp** > **sdp**
> JSON string specifying the SDP of the source. This parameter is mandatory if **use\_sdp** is true. > JSON string specifying the SDP of the source. This parameter is mandatory if **use\_sdp** is true.

View File

@ -17,11 +17,10 @@
// 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 <experimental/map>
#include "browser.hpp" #include "browser.hpp"
using namespace std::chrono; using namespace std::chrono;
using second_t = std::chrono::duration<double, std::ratio<1> >; using second_t = duration<double, std::ratio<1> >;
std::shared_ptr<Browser> Browser::create( std::shared_ptr<Browser> Browser::create(
std::shared_ptr<Config> config) { std::shared_ptr<Config> config) {
@ -39,7 +38,8 @@ std::shared_ptr<Browser> Browser::create(
std::list<RemoteSource> Browser::get_remote_sources() { std::list<RemoteSource> Browser::get_remote_sources() {
std::list<RemoteSource> sources_list; std::list<RemoteSource> sources_list;
std::shared_lock sources_lock(sources_mutex_); std::shared_lock sources_lock(sources_mutex_);
for (auto const& [id, source] : sources_) { // return list of remote sources ordered by name
for (auto& source: sources_.get<name_tag>()) {
sources_list.push_back(source); sources_list.push_back(source);
} }
return sources_list; return sources_list;
@ -60,9 +60,10 @@ bool Browser::worker() {
sap_.set_multicast_interface(config_->get_ip_addr_str()); sap_.set_multicast_interface(config_->get_ip_addr_str());
// Join SAP muticast address // Join SAP muticast address
igmp_.join(config_->get_ip_addr_str(), config_->get_sap_mcast_addr()); igmp_.join(config_->get_ip_addr_str(), config_->get_sap_mcast_addr());
auto startup = steady_clock::now();
auto sap_timepoint = steady_clock::now(); auto sap_timepoint = steady_clock::now();
int sap_interval = 10; int sap_interval = 10;
auto mdns_timepoint = steady_clock::now();
int mdns_interval = 10;
while (running_) { while (running_) {
bool is_announce; bool is_announce;
@ -71,12 +72,14 @@ bool Browser::worker() {
std::string sdp; std::string sdp;
if (sap_.receive(is_announce, msg_id_hash, addr, sdp)) { if (sap_.receive(is_announce, msg_id_hash, addr, sdp)) {
char id[13]; std::stringstream ss;
snprintf(id, sizeof id, "%x%x", addr, msg_id_hash); ss << "sap:" << std::hex << addr << msg_id_hash;
//BOOST_LOG_TRIVIAL(debug) << "browser:: received SAP message for " << id; std::string id(ss.str());
BOOST_LOG_TRIVIAL(debug) << "browser:: received SAP message for " << id;
std::unique_lock sources_lock(sources_mutex_); std::unique_lock sources_lock(sources_mutex_);
auto it = sources_.find(id);
auto it = sources_.get<id_tag>().find(id);
if (it == sources_.end()) { if (it == sources_.end()) {
// Source is not in the map // Source is not in the map
if (is_announce) { if (is_announce) {
@ -89,23 +92,25 @@ bool Browser::worker() {
source.address = ip::address_v4(ntohl(addr)).to_string(); source.address = ip::address_v4(ntohl(addr)).to_string();
source.name = sdp_get_subject(sdp); source.name = sdp_get_subject(sdp);
source.last_seen = source.last_seen =
duration_cast<second_t>(steady_clock::now() - startup).count(); duration_cast<second_t>(steady_clock::now() - startup_).count();
source.announce_period = 360; //default period source.announce_period = 360; //default period
sources_[id] = source; sources_.insert(source);
} }
} else { } else {
// Source is already in the map // Source is already in the map
if (is_announce) { if (is_announce) {
BOOST_LOG_TRIVIAL(debug) BOOST_LOG_TRIVIAL(debug)
<< "browser:: refreshing SAP source " << (*it).second.id; << "browser:: refreshing SAP source " << it->id;
// annoucement, update last seen and announce period // annoucement, update last seen and announce period
auto upd_source{*it};
uint32_t last_seen = uint32_t last_seen =
duration_cast<second_t>(steady_clock::now() - startup).count(); duration_cast<second_t>(steady_clock::now() - startup_).count();
(*it).second.announce_period = last_seen - (*it).second.last_seen; upd_source.announce_period = last_seen - upd_source.last_seen;
(*it).second.last_seen = last_seen; upd_source.last_seen = last_seen;
sources_.replace(it, upd_source);
} else { } else {
BOOST_LOG_TRIVIAL(info) BOOST_LOG_TRIVIAL(info)
<< "browser:: removing SAP source " << (*it).second.id; << "browser:: removing SAP source " << it->id;
// deletion, remove entry // deletion, remove entry
sources_.erase(it); sources_.erase(it);
} }
@ -117,22 +122,79 @@ bool Browser::worker() {
> sap_interval) { > sap_interval) {
sap_timepoint = steady_clock::now(); sap_timepoint = steady_clock::now();
// remove all sessions no longer announced // remove all sessions no longer announced
auto offset = duration_cast<second_t>(steady_clock::now() - startup).count(); auto offset = duration_cast<second_t>(steady_clock::now() - startup_).count();
std::unique_lock sources_lock(sources_mutex_); std::unique_lock sources_lock(sources_mutex_);
std::experimental::erase_if(sources_, [offset](auto entry) { for (auto it = sources_.begin(); it != sources_.end();) {
if ((offset - entry.second.last_seen) > if (it->source == "SAP" &&
(entry.second.announce_period * 10)) { (offset - it->last_seen) > (it->announce_period * 10)) {
// remove from remote SAP sources // remove from remote SAP sources
BOOST_LOG_TRIVIAL(info) BOOST_LOG_TRIVIAL(info)
<< "browser:: SAP source " << entry.second.id << " timeout"; << "browser:: SAP source " << it->id << " timeout";
return true; it = sources_.erase(it);
} else {
it++;
} }
return false; }
}); }
// check if it's time to process the mDNS RTSP sources
if ((duration_cast<second_t>(steady_clock::now() - mdns_timepoint).count())
> mdns_interval) {
mdns_timepoint = steady_clock::now();
process_results();
} }
} }
return true; return true;
} }
void Browser::on_new_rtsp_source(const std::string& name,
const std::string& domain,
const RTSPSSource& s) {
uint32_t last_seen = duration_cast<second_t>(steady_clock::now() - startup_).count();
std::unique_lock sources_lock(sources_mutex_);
if (sources_.get<id_tag>().find(s.id) == sources_.end()) {
BOOST_LOG_TRIVIAL(info) << "browser:: adding RTSP source " << s.id;
sources_.insert({ s.id, s.source, s.address, name, s.sdp, last_seen, 0 });
}
}
void Browser::on_remove_rtsp_source(const std::string& name,
const std::string& domain) {
std::unique_lock sources_lock(sources_mutex_);
auto& name_idx = sources_.get<name_tag>();
for (auto it = name_idx.find(name); it != name_idx.end(); it++) {
if (it->source == "mDNS") {
BOOST_LOG_TRIVIAL(info) << "browser:: removing RTSP source " << it->id;
name_idx.erase(it);
break;
}
}
}
bool Browser::init() {
if (!running_) {
/* init mDNS client */
if (config_->get_mdns_enabled() && !MDNSClient::init()) {
return false;
}
running_ = true;
res_ = std::async(std::launch::async, &Browser::worker, this);
}
return true;
}
bool Browser::terminate() {
if (running_) {
running_ = false;
/* wait for worker to exit */
res_.get();
/* terminate mDNS client */
if (config_->get_mdns_enabled()) {
MDNSClient::terminate();
}
}
return true;
}

View File

@ -26,9 +26,18 @@
#include <chrono> #include <chrono>
#include <list> #include <list>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/indexed_by.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/member.hpp>
#include "config.hpp" #include "config.hpp"
#include "sap.hpp" #include "sap.hpp"
#include "igmp.hpp" #include "igmp.hpp"
#include "mdns_client.hpp"
using namespace boost::multi_index;
struct RemoteSource { struct RemoteSource {
std::string id; std::string id;
@ -40,50 +49,56 @@ struct RemoteSource {
uint32_t announce_period; /* period between annoucements */ uint32_t announce_period; /* period between annoucements */
}; };
class Browser { class Browser : public MDNSClient {
public: public:
static std::shared_ptr<Browser> create( static std::shared_ptr<Browser> create(
std::shared_ptr<Config> config); std::shared_ptr<Config> config);
Browser() = delete; Browser() = delete;
Browser(const Browser&) = delete; Browser(const Browser&) = delete;
Browser& operator=(const Browser&) = delete; Browser& operator=(const Browser&) = delete;
virtual ~Browser(){ stop(); }; virtual ~Browser(){ terminate(); };
bool start() { bool init() override;
if (!running_) { bool terminate() override;
running_ = true;
res_ = std::async(std::launch::async, &Browser::worker, this);
}
return true;
}
bool stop() {
if (running_) {
running_ = false;
return res_.get();
}
return true;
}
std::list<RemoteSource> get_remote_sources(); std::list<RemoteSource> get_remote_sources();
protected: protected:
// singleton, use create() to build // singleton, use create() to build
Browser(std::shared_ptr<Config> config) Browser(std::shared_ptr<Config> config):
: config_(config){}; config_(config),
startup_(std::chrono::steady_clock::now()){};
bool worker(); bool worker();
virtual void on_new_rtsp_source(
const std::string& name,
const std::string& domain,
const RTSPSSource& source) override;
virtual void on_remove_rtsp_source(
const std::string& name,
const std::string& domain) override;
std::shared_ptr<Config> config_; std::shared_ptr<Config> config_;
std::future<bool> res_; std::future<bool> res_;
std::atomic_bool running_{false}; std::atomic_bool running_{false};
/* current sources */ /* current sources */
std::map<std::string /* id */, RemoteSource> sources_; struct id_tag{};
using by_id = hashed_unique<tag<id_tag>, member<RemoteSource,
std::string, &RemoteSource::id>>;
struct name_tag{};
using by_name = ordered_non_unique<tag<name_tag>, member<RemoteSource,
std::string, &RemoteSource::name>>;
using sources_t = multi_index_container<RemoteSource,
indexed_by<by_id, by_name>>;
sources_t sources_;
mutable std::shared_mutex sources_mutex_; mutable std::shared_mutex sources_mutex_;
SAP sap_{config_->get_sap_mcast_addr()}; SAP sap_{config_->get_sap_mcast_addr()};
IGMP igmp_; IGMP igmp_;
std::chrono::time_point<std::chrono::steady_clock> startup_;
}; };
#endif #endif

View File

@ -65,12 +65,15 @@ std::shared_ptr<Config> Config::parse(const std::string& filename) {
config.max_tic_frame_size_ = 1024; config.max_tic_frame_size_ = 1024;
if (config.sample_rate_ == 0) if (config.sample_rate_ == 0)
config.sample_rate_ = 44100; config.sample_rate_ = 44100;
if (ip::address_v4::from_string(config.rtp_mcast_base_.c_str()).to_ulong() == boost::system::error_code ec;
INADDR_NONE) ip::address_v4::from_string(config.rtp_mcast_base_.c_str(), ec);
if (!ec) {
config.rtp_mcast_base_ = "239.1.0.1"; config.rtp_mcast_base_ = "239.1.0.1";
if (ip::address_v4::from_string(config.sap_mcast_addr_.c_str()).to_ulong() == }
INADDR_NONE) ip::address_v4::from_string(config.sap_mcast_addr_.c_str(), ec);
if (!ec) {
config.sap_mcast_addr_ = "224.2.127.254"; config.sap_mcast_addr_ = "224.2.127.254";
}
if (config.ptp_domain_ > 127) if (config.ptp_domain_ > 127)
if (config.ptp_domain_ > 127) if (config.ptp_domain_ > 127)
config.ptp_domain_ = 0; config.ptp_domain_ = 0;

View File

@ -57,6 +57,7 @@ class Config {
uint32_t get_ip_addr() const { return ip_addr_; }; uint32_t get_ip_addr() const { return ip_addr_; };
const std::string& get_ip_addr_str() const { return ip_str_; }; const std::string& get_ip_addr_str() const { return ip_str_; };
bool get_need_restart() const { return need_restart_; }; bool get_need_restart() const { return need_restart_; };
bool get_mdns_enabled() const { return mdns_enabled; };
void set_http_port(uint16_t http_port) { http_port_ = http_port; }; void set_http_port(uint16_t http_port) { http_port_ = http_port; };
void set_http_base_dir(const std::string& http_base_dir) { http_base_dir_ = http_base_dir; }; void set_http_base_dir(const std::string& http_base_dir) { http_base_dir_ = http_base_dir; };
@ -101,6 +102,9 @@ class Config {
void set_mac_addr(const std::array<uint8_t, 6>& mac_addr) { void set_mac_addr(const std::array<uint8_t, 6>& mac_addr) {
mac_addr_ = mac_addr; mac_addr_ = mac_addr;
}; };
void set_mdns_enabled(bool enabled) {
mdns_enabled = enabled;
};
private: private:
/* from json */ /* from json */
@ -121,6 +125,7 @@ class Config {
std::string syslog_server_{""}; std::string syslog_server_{""};
std::string status_file_{"./status.json"}; std::string status_file_{"./status.json"};
std::string interface_name_{"eth0"}; std::string interface_name_{"eth0"};
bool mdns_enabled{true};
/* set during init */ /* set during init */
std::array<uint8_t, 6> mac_addr_{0, 0, 0, 0, 0, 0}; std::array<uint8_t, 6> mac_addr_{0, 0, 0, 0, 0, 0};

View File

@ -15,5 +15,6 @@
"syslog_proto": "none", "syslog_proto": "none",
"syslog_server": "255.255.255.254:1234", "syslog_server": "255.255.255.254:1234",
"status_file": "./status.json", "status_file": "./status.json",
"mdns_enabled": true,
"interface_name": "lo" "interface_name": "lo"
} }

View File

@ -44,6 +44,8 @@ class DriverHandler {
virtual bool init(const Config& config); virtual bool init(const Config& config);
virtual bool terminate(); virtual bool terminate();
protected:
virtual void send_command(enum MT_ALSA_msg_id id, virtual void send_command(enum MT_ALSA_msg_id id,
size_t size = 0, size_t size = 0,
const uint8_t* data = nullptr); const uint8_t* data = nullptr);

View File

@ -77,7 +77,7 @@ static inline void set_error(
res.body = message; res.body = message;
} }
bool HttpServer::start() { bool HttpServer::init() {
/* setup http operations */ /* setup http operations */
if (!svr_.is_valid()) { if (!svr_.is_valid()) {
return false; return false;
@ -325,7 +325,7 @@ bool HttpServer::start() {
return retry; return retry;
} }
bool HttpServer::stop() { bool HttpServer::terminate() {
BOOST_LOG_TRIVIAL(info) << "http_server: stopping ... "; BOOST_LOG_TRIVIAL(info) << "http_server: stopping ... ";
svr_.stop(); svr_.stop();
return res_.get(); return res_.get();

View File

@ -35,8 +35,8 @@ class HttpServer {
: session_manager_(session_manager), : session_manager_(session_manager),
browser_(browser), browser_(browser),
config_(config) {}; config_(config) {};
bool start(); bool init();
bool stop(); bool terminate();
private: private:
std::shared_ptr<SessionManager> session_manager_; std::shared_ptr<SessionManager> session_manager_;

View File

@ -91,6 +91,7 @@ std::string config_to_json(const Config& config) {
<< ",\n \"syslog_server\": \"" << escape_json(config.get_syslog_server()) << "\"" << ",\n \"syslog_server\": \"" << escape_json(config.get_syslog_server()) << "\""
<< ",\n \"status_file\": \"" << escape_json(config.get_status_file()) << "\"" << ",\n \"status_file\": \"" << escape_json(config.get_status_file()) << "\""
<< ",\n \"interface_name\": \"" << escape_json(config.get_interface_name()) << "\"" << ",\n \"interface_name\": \"" << escape_json(config.get_interface_name()) << "\""
<< ",\n \"mdns_enabled\": \"" << std::boolalpha << config.get_mdns_enabled() << "\""
<< ",\n \"mac_addr\": \"" << escape_json(config.get_mac_addr_str()) << "\"" << ",\n \"mac_addr\": \"" << escape_json(config.get_mac_addr_str()) << "\""
<< ",\n \"ip_addr\": \"" << escape_json(config.get_ip_addr_str()) << "\"" << ",\n \"ip_addr\": \"" << escape_json(config.get_ip_addr_str()) << "\""
<< "\n}\n"; << "\n}\n";
@ -295,6 +296,8 @@ Config json_to_config_(std::istream& js, Config& config) {
config.set_syslog_proto(remove_undesired_chars(val.get_value<std::string>())); config.set_syslog_proto(remove_undesired_chars(val.get_value<std::string>()));
} else if (key == "syslog_server") { } else if (key == "syslog_server") {
config.set_syslog_server(remove_undesired_chars(val.get_value<std::string>())); config.set_syslog_server(remove_undesired_chars(val.get_value<std::string>()));
} else if (key == "mdns_enabled") {
config.set_mdns_enabled(val.get_value<bool>());
} else if (key == "mac_addr" || key == "ip_addr") { } else if (key == "mac_addr" || key == "ip_addr") {
/* ignored */ /* ignored */
} else { } else {

View File

@ -111,22 +111,22 @@ int main(int argc, char* argv[]) {
/* start session manager */ /* start session manager */
auto session_manager = SessionManager::create(driver, config); auto session_manager = SessionManager::create(driver, config);
if (session_manager == nullptr || !session_manager->start()) { if (session_manager == nullptr || !session_manager->init()) {
throw std::runtime_error( throw std::runtime_error(
std::string("SessionManager:: start failed")); std::string("SessionManager:: init failed"));
} }
/* start browser */ /* start browser */
auto browser = Browser::create(config); auto browser = Browser::create(config);
if (browser == nullptr || !browser->start()) { if (browser == nullptr || !browser->init()) {
throw std::runtime_error( throw std::runtime_error(
std::string("Browser:: start failed")); std::string("Browser:: init failed"));
} }
/* start http server */ /* start http server */
HttpServer http_server(session_manager, browser, config); HttpServer http_server(session_manager, browser, config);
if (!http_server.start()) { if (!http_server.init()) {
throw std::runtime_error(std::string("HttpServer:: start failed")); throw std::runtime_error(std::string("HttpServer:: init failed"));
} }
/* load session status from file */ /* load session status from file */
@ -154,21 +154,21 @@ int main(int argc, char* argv[]) {
session_manager->save_status(); session_manager->save_status();
/* stop http server */ /* stop http server */
if (!http_server.stop()) { if (!http_server.terminate()) {
throw std::runtime_error( throw std::runtime_error(
std::string("HttpServer:: stop failed")); std::string("HttpServer:: terminate failed"));
} }
/* stop browser */ /* stop browser */
if (!browser->stop()) { if (!browser->terminate()) {
throw std::runtime_error( throw std::runtime_error(
std::string("Browser:: stop failed")); std::string("Browser:: terminate failed"));
} }
/* stop session manager */ /* stop session manager */
if (!session_manager->stop()) { if (!session_manager->terminate()) {
throw std::runtime_error( throw std::runtime_error(
std::string("SessionManager:: stop failed")); std::string("SessionManager:: terminate failed"));
} }
/* stop driver manager */ /* stop driver manager */

255
daemon/mdns_client.cpp Normal file
View File

@ -0,0 +1,255 @@
//
// mdns_client.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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 "mdns_client.hpp"
#include <boost/asio.hpp>
#include "config.hpp"
#include "log.hpp"
#include "rtsp_client.hpp"
#ifdef _USE_AVAHI_
void MDNSClient::resolve_callback(AvahiServiceResolver* r,
AvahiIfIndex interface,
AvahiProtocol protocol,
AvahiResolverEvent event,
const char* name,
const char* type,
const char* domain,
const char* host_name,
const AvahiAddress* address,
uint16_t port,
AvahiStringList* txt,
AvahiLookupResultFlags flags,
void* userdata) {
MDNSClient& mdns = *(reinterpret_cast<MDNSClient*>(userdata));
/* Called whenever a service has been resolved successfully or timed out */
switch (event) {
case AVAHI_RESOLVER_FAILURE:
BOOST_LOG_TRIVIAL(error) << "avahi_client:: (Resolver) failed to resolve "
<< "service " << name << " of type " << type
<< " in domain " << domain << " : "
<< avahi_strerror(avahi_client_errno(
avahi_service_resolver_get_client(r)));
break;
case AVAHI_RESOLVER_FOUND:
BOOST_LOG_TRIVIAL(debug) << "avahi_client:: (Resolver) "
<< "service " << name << " of type " << type
<< " in domain " << domain;
char addr[AVAHI_ADDRESS_STR_MAX];
avahi_address_snprint(addr, sizeof(addr), address);
char info[256];
snprintf(info, sizeof(info),
"%s:%u (%s), "
"local: %i, "
"our_own: %i, "
"wide_area: %i, "
"multicast: %i, "
"cached: %i",
host_name, port, addr, !!(flags & AVAHI_LOOKUP_RESULT_LOCAL),
!!(flags & AVAHI_LOOKUP_RESULT_OUR_OWN),
!!(flags & AVAHI_LOOKUP_RESULT_WIDE_AREA),
!!(flags & AVAHI_LOOKUP_RESULT_MULTICAST),
!!(flags & AVAHI_LOOKUP_RESULT_CACHED));
BOOST_LOG_TRIVIAL(debug) << "avahi_client:: (Resolver) " << info;
boost::system::error_code ec;
boost::asio::ip::address_v4::from_string(addr, ec);
if (!ec) {
/* if valid IPv4 address retrieve source data via RTSP */
std::lock_guard<std::mutex> lock(mdns.sources_res_mutex_);
/* have fun ;-) */
mdns.sources_res_.emplace_back(std::async(
std::launch::async,
[&mdns, name_ = std::forward<std::string>(name),
domain_ = std::forward<std::string>(domain),
addr_ = std::forward<std::string>(addr),
port_ = std::forward<std::string>(std::to_string(port))] {
auto res = RTSPClient::describe(std::string("/by-name/") + name_,
addr_, port_);
if (res.first) {
mdns.on_new_rtsp_source(name_, domain_, res.second);
}
}));
}
break;
}
avahi_service_resolver_free(r);
}
void MDNSClient::browse_callback(AvahiServiceBrowser* b,
AvahiIfIndex interface,
AvahiProtocol protocol,
AvahiBrowserEvent event,
const char* name,
const char* type,
const char* domain,
AvahiLookupResultFlags flags,
void* userdata) {
MDNSClient& mdns = *(reinterpret_cast<MDNSClient*>(userdata));
/* Called whenever a new services becomes available on the LAN or is removed
* from the LAN */
switch (event) {
case AVAHI_BROWSER_FAILURE:
BOOST_LOG_TRIVIAL(fatal) << "avahi_client:: (Browser) "
<< avahi_strerror(avahi_client_errno(
avahi_service_browser_get_client(b)));
avahi_threaded_poll_quit(mdns.poll_.get());
return;
case AVAHI_BROWSER_NEW:
BOOST_LOG_TRIVIAL(info) << "avahi_client:: (Browser) NEW: "
<< "service " << name << " of type " << type
<< " in domain " << domain;
/* We ignore the returned resolver object. In the callback
function we free it. If the server is terminated before
the callback function is called the server will free
the resolver for us. */
if (!(avahi_service_resolver_new(mdns.client_.get(), interface, protocol,
name, type, domain, AVAHI_PROTO_UNSPEC,
AVAHI_LOOKUP_NO_TXT, resolve_callback,
&mdns))) {
BOOST_LOG_TRIVIAL(error)
<< "avahi_client:: "
<< "Failed to resolve service " << name << " : "
<< avahi_strerror(avahi_client_errno(mdns.client_.get()));
}
break;
case AVAHI_BROWSER_REMOVE:
BOOST_LOG_TRIVIAL(info) << "avahi_client:: (Browser) REMOVE: "
<< "service " << name << " of type " << type
<< " in domain " << domain;
mdns.on_remove_rtsp_source(name, domain);
break;
case AVAHI_BROWSER_ALL_FOR_NOW:
BOOST_LOG_TRIVIAL(debug) << "avahi_client:: (Browser) ALL_FOR_NOW";
break;
case AVAHI_BROWSER_CACHE_EXHAUSTED:
BOOST_LOG_TRIVIAL(debug) << "avahi_client:: (Browser) CACHE_EXHAUSTED";
break;
}
}
void MDNSClient::client_callback(AvahiClient* c,
AvahiClientState state,
void* userdata) {
MDNSClient& mdns = *(reinterpret_cast<MDNSClient*>(userdata));
/* Called whenever the client or server state changes */
if (state == AVAHI_CLIENT_FAILURE) {
BOOST_LOG_TRIVIAL(fatal) << "avahi_client:: server connection failure: "
<< avahi_strerror(avahi_client_errno(c));
avahi_threaded_poll_quit(mdns.poll_.get());
}
}
#endif
bool MDNSClient::init() {
if (running_) {
return true;
}
#ifdef _USE_AVAHI_
/* allocate poll loop object */
poll_.reset(avahi_threaded_poll_new());
if (poll_ == nullptr) {
BOOST_LOG_TRIVIAL(fatal)
<< "avahi_client:: failed to create threaded poll object";
return false;
}
/* allocate a new client */
int error;
client_.reset(avahi_client_new(avahi_threaded_poll_get(poll_.get()),
AVAHI_CLIENT_NO_FAIL, client_callback, this,
&error));
if (client_ == nullptr) {
BOOST_LOG_TRIVIAL(fatal)
<< "avahi_client:: failed to create client: " << avahi_strerror(error);
return false;
}
/* Create the service browser */
sb_.reset(avahi_service_browser_new(client_.get(), AVAHI_IF_UNSPEC,
AVAHI_PROTO_UNSPEC, "_rtsp._tcp", nullptr,
{}, browse_callback, this));
if (sb_ == nullptr) {
BOOST_LOG_TRIVIAL(fatal)
<< "avahi_client:: failed to create service browser: "
<< avahi_strerror(avahi_client_errno(client_.get()));
return false;
}
(void)avahi_threaded_poll_start(poll_.get());
#endif
running_ = true;
return true;
}
void MDNSClient::process_results() {
#ifdef _USE_AVAHI_
std::lock_guard<std::mutex> lock(sources_res_mutex_);
/* remove all completed results and populate remote sources list */
sources_res_.remove_if([](auto& result) {
if (!result.valid()) {
/* if invalid future remove from the list */
return true;
}
auto status = result.wait_for(std::chrono::milliseconds(0));
if (status == std::future_status::ready) {
result.get();
/* if completed remove from the list */
return true;
}
/* if not completed leave in the list */
return false;
});
#endif
}
bool MDNSClient::terminate() {
if (running_) {
running_ = false;
#ifdef _USE_AVAHI_
/* remove all completed results and populate remote sources list */
/* wait for all pending results and remove from list */
std::lock_guard<std::mutex> lock(sources_res_mutex_);
BOOST_LOG_TRIVIAL(fatal) << "avahi_client:: waiting for "
<< sources_res_.size() << " RTSP clients";
sources_res_.remove_if([](auto& result) {
if (result.valid()) {
result.wait();
}
return true;
});
avahi_threaded_poll_stop(poll_.get());
#endif
}
return true;
}

99
daemon/mdns_client.hpp Normal file
View File

@ -0,0 +1,99 @@
//
// mdns_client.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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 _AVAHI_CLIENT_HPP_
#define _AVAHI_CLIENT_HPP_
#ifdef _USE_AVAHI_
#include <avahi-client/client.h>
#include <avahi-client/lookup.h>
#include <avahi-common/error.h>
#include <avahi-common/malloc.h>
#include <avahi-common/thread-watch.h>
#endif
#include <future>
#include <list>
#include <shared_mutex>
#include <thread>
#include "config.hpp"
#include "rtsp_client.hpp"
class MDNSClient {
public:
MDNSClient(){};
MDNSClient(const MDNSClient&) = delete;
MDNSClient& operator=(const MDNSClient&) = delete;
virtual ~MDNSClient() { terminate(); };
virtual bool init();
virtual bool terminate();
protected:
virtual void on_new_rtsp_source(const std::string& name,
const std::string& domain,
const RTSPSSource& source) = 0;
virtual void on_remove_rtsp_source(const std::string& name,
const std::string& domain) = 0;
void process_results();
std::list<std::future<void> > sources_res_;
std::mutex sources_res_mutex_;
std::atomic_bool running_{false};
#ifdef _USE_AVAHI_
/* order is important here */
std::unique_ptr<AvahiThreadedPoll, decltype(&avahi_threaded_poll_free)> poll_{
nullptr, &avahi_threaded_poll_free};
std::unique_ptr< ::AvahiClient, decltype(&avahi_client_free)> client_{
nullptr, &avahi_client_free};
std::unique_ptr<AvahiServiceBrowser, decltype(&avahi_service_browser_free)>
sb_{nullptr, &avahi_service_browser_free};
static void resolve_callback(AvahiServiceResolver* r,
AvahiIfIndex interface,
AvahiProtocol protocol,
AvahiResolverEvent event,
const char* name,
const char* type,
const char* domain,
const char* host_name,
const AvahiAddress* address,
uint16_t port,
AvahiStringList* txt,
AvahiLookupResultFlags flags,
void* userdata);
static void browse_callback(AvahiServiceBrowser* b,
AvahiIfIndex interface,
AvahiProtocol protocol,
AvahiBrowserEvent event,
const char* name,
const char* type,
const char* domain,
AvahiLookupResultFlags flags,
void* userdata);
static void client_callback(AvahiClient* c,
AvahiClientState state,
void* userdata);
#endif
};
#endif

177
daemon/rtsp_client.cpp Normal file
View File

@ -0,0 +1,177 @@
//
// rtsp_client.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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 <httplib.h>
#include <boost/algorithm/string.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <iomanip>
#include <iostream>
#include <istream>
#include <ostream>
#include <string>
#include <chrono>
#include "log.hpp"
#include "utils.hpp"
#include "rtsp_client.hpp"
using namespace boost::asio;
using namespace boost::asio::ip;
using namespace boost::algorithm;
struct RtspResponse {
uint32_t cseq;
std::string content_type;
uint64_t content_length;
std::string body;
};
RtspResponse read_response(tcp::iostream& s, uint16_t max_length) {
RtspResponse res;
std::string header;
/*
RTSP/1.0 200 OK
CSeq: 312
Date: 23 Jan 1997 15:35:06 GMT
Content-Type: application/sdp
Content-Length: 376
*/
try {
while (std::getline(s, header) && header != "" && header != "\r") {
to_lower(header);
trim(header);
if (header.rfind("cseq:", 0) != std::string::npos) {
res.cseq = std::stoi(header.substr(5));
} else if (header.rfind("content-type:", 0) != std::string::npos) {
res.content_type = header.substr(13);
trim(res.content_type);
} else if (header.rfind("content-length:", 0) != std::string::npos) {
res.content_length = std::stoi(header.substr(15));
}
}
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "rtsp_client:: invalid response header, "
<< "cannot perform number conversion";
}
// read up to max_length
if (res.content_length > 0 && res.content_length < max_length) {
res.body.reserve(res.content_length);
std::copy_n(std::istreambuf_iterator(s), res.content_length,
std::back_inserter(res.body));
}
return res;
}
std::pair<bool, RTSPSSource> RTSPClient::describe(const std::string& path,
const std::string& address,
const std::string& port) {
RTSPSSource rs;
bool success{false};
try {
tcp::iostream s;
#if BOOST_VERSION < 106700
s.expires_from_now(boost::posix_time::seconds(client_timeout));
#else
s.expires_from_now(std::chrono::seconds(client_timeout));
#endif
BOOST_LOG_TRIVIAL(debug) << "rtsp_client:: connecting to "
<< "rtsp://" << address << ":" << port << path;
s.connect(address, port.length() ? port : dft_port);
if (!s) {
BOOST_LOG_TRIVIAL(warning)
<< "rtsp_client:: unable to connect to " << address << ":" << port;
return std::make_pair(success, rs);
}
uint16_t cseq = seq_number++;
s << "DESCRIBE rtsp://" << address << ":" << port
<< httplib::detail::encode_url(path) << " RTSP/1.0\r\n";
s << "CSeq: " << cseq << "\r\n";
s << "User-Agent: aes67-daemon\r\n";
s << "Accept: application/sdp\r\n\r\n";
// By default, the stream is tied with itself. This means that the stream
// automatically flush the buffered output before attempting a read. It is
// not necessary not explicitly flush the stream at this point.
// Check that response is OK.
std::string rtsp_version;
s >> rtsp_version;
unsigned int status_code;
s >> status_code;
std::string status_message;
std::getline(s, status_message);
if (!s || rtsp_version.substr(0, 5) != "RTSP/") {
BOOST_LOG_TRIVIAL(error) << "rtsp_client:: invalid response from "
<< "rtsp://" << address << ":" << port << path;
return std::make_pair(success, rs);
}
if (status_code != 200) {
BOOST_LOG_TRIVIAL(error) << "rtsp_client:: response with status code "
<< status_code << " from "
<< "rtsp://" << address << ":" << port << path;
return std::make_pair(success, rs);
}
auto res = read_response(s, max_body_length);
if (res.content_type.rfind("application/sdp", 0) == std::string::npos) {
BOOST_LOG_TRIVIAL(error) << "rtsp_client:: unsupported content-type "
<< res.content_type << " from "
<< "rtsp://" << address << ":" << port << path;
return std::make_pair(success, rs);
}
if (res.cseq != cseq) {
BOOST_LOG_TRIVIAL(error)
<< "rtsp_client:: invalid response sequence " << res.cseq << " from "
<< "rtsp://" << address << ":" << port << path;
return std::make_pair(success, rs);
}
std::stringstream ss;
ss << "rtsp:" << std::hex
<< crc16(reinterpret_cast<const uint8_t*>(res.body.c_str()),
res.body.length())
<< std::hex << ip::address_v4::from_string(address.c_str()).to_ulong();
rs.id = ss.str();
rs.source = "mDNS";
rs.address = address;
rs.sdp = std::move(res.body);
BOOST_LOG_TRIVIAL(info) << "rtsp_client:: describe completed "
<< "rtsp://" << address << ":" << port << path;
success = true;
} catch (std::exception& e) {
BOOST_LOG_TRIVIAL(error)
<< "rtsp_client:: error with "
<< "rtsp://" << address << ":" << port << path << ": " << e.what();
}
return std::make_pair(success, rs);
}

44
daemon/rtsp_client.hpp Normal file
View File

@ -0,0 +1,44 @@
//
// rtsp_include.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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 _RTSP_HPP_
#define _RTSP_HPP_
struct RTSPSSource {
std::string id;
std::string source;
std::string address;
std::string sdp;
};
class RTSPClient {
public:
constexpr static uint16_t max_body_length = 4096; // byte
constexpr static uint16_t client_timeout = 10; // sec
constexpr static const char dft_port[] = "554";
static std::pair<bool, RTSPSSource> describe(
const std::string& path,
const std::string& address,
const std::string& port = dft_port);
inline static std::atomic<uint16_t> seq_number;
};
#endif

View File

@ -99,7 +99,7 @@ bool SAP::receive(bool& is_announce,
is_announce = (buffer[0] == 0x20); is_announce = (buffer[0] == 0x20);
memcpy(&msg_id_hash, buffer + 2, sizeof(msg_id_hash)); memcpy(&msg_id_hash, buffer + 2, sizeof(msg_id_hash));
memcpy(&addr, buffer + 4, sizeof(addr)); memcpy(&addr, buffer + 4, sizeof(addr));
for (int i = 8; buffer[i] != 0 && i < length; i++) { for (int i = 8; buffer[i] != 0 && i < static_cast<int>(length); i++) {
buffer[i] = std::tolower(buffer[i]); buffer[i] = std::tolower(buffer[i]);
} }
if (!memcmp(buffer + 8, "application/sdp", 16)) { if (!memcmp(buffer + 8, "application/sdp", 16)) {

View File

@ -23,6 +23,7 @@
#include <boost/foreach.hpp> #include <boost/foreach.hpp>
#include <boost/property_tree/json_parser.hpp> #include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp> #include <boost/property_tree/ptree.hpp>
#include <boost/algorithm/string.hpp>
#include <experimental/map> #include <experimental/map>
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
@ -32,6 +33,8 @@
#include "json.hpp" #include "json.hpp"
#include "log.hpp" #include "log.hpp"
#include "session_manager.hpp" #include "session_manager.hpp"
#include "utils.hpp"
#include "rtsp_client.hpp"
static uint8_t get_codec_word_lenght(const std::string& codec) { static uint8_t get_codec_word_lenght(const std::string& codec) {
@ -122,6 +125,7 @@ bool SessionManager::parse_sdp(const std::string sdp, StreamInfo& info) const {
std::stringstream ssstrem(sdp); std::stringstream ssstrem(sdp);
std::string line; std::string line;
while (getline(ssstrem, line, '\n')) { while (getline(ssstrem, line, '\n')) {
boost::trim(line);
++num; ++num;
if (line[1] != '=') { if (line[1] != '=') {
BOOST_LOG_TRIVIAL(error) BOOST_LOG_TRIVIAL(error)
@ -289,7 +293,7 @@ bool SessionManager::parse_sdp(const std::string sdp, StreamInfo& info) const {
} }
} catch (...) { } catch (...) {
BOOST_LOG_TRIVIAL(fatal) << "session_manager:: invalid SDP at line " << num BOOST_LOG_TRIVIAL(fatal) << "session_manager:: invalid SDP at line " << num
<< ", cannot perform number convesrion"; << ", cannot perform number conversion";
return false; return false;
} }
@ -659,45 +663,55 @@ std::error_code SessionManager::add_sink(const StreamSink& sink) {
return DaemonErrc::invalid_url; return DaemonErrc::invalid_url;
} }
if (!boost::iequals(protocol, "http")) { std::string sdp;
BOOST_LOG_TRIVIAL(error) if (boost::iequals(protocol, "http")) {
<< "session_manager:: unsupported protocol in URL " << sink.source; httplib::Client cli(host.c_str(),
return DaemonErrc::invalid_url; !atoi(port.c_str()) ? 80 : atoi(port.c_str()));
} cli.set_timeout_sec(10);
auto res = cli.Get(path.c_str());
httplib::Client cli(host.c_str(), if (!res) {
!atoi(port.c_str()) ? 80 : atoi(port.c_str())); BOOST_LOG_TRIVIAL(error)
cli.set_timeout_sec(10); << "session_manager:: annot retrieve SDP from URL " << sink.source;
auto res = cli.Get(path.c_str()); return DaemonErrc::cannot_retrieve_sdp;
if (!res) { }
BOOST_LOG_TRIVIAL(error) if (res->status != 200) {
<< "session_manager:: cannot retrieve SDP from URL " << sink.source; BOOST_LOG_TRIVIAL(error)
return DaemonErrc::cannot_retrieve_sdp; << "session_manager:: cannot retrieve SDP from URL " << sink.source
} << " server reply " << res->status;
return DaemonErrc::cannot_retrieve_sdp;
if (res->status != 200) { }
BOOST_LOG_TRIVIAL(error) sdp = std::move(res->body);
<< "session_manager:: cannot retrieve SDP from URL " << sink.source } else if (boost::iequals(protocol, "rtsp")) {
<< " server reply " << res->status; auto res = RTSPClient::describe(path, host, port);
return DaemonErrc::cannot_retrieve_sdp; if (!res.first) {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: cannot retrieve SDP from URL " << sink.source;
return DaemonErrc::cannot_retrieve_sdp;
}
sdp = std::move(res.second.sdp);
} else {
BOOST_LOG_TRIVIAL(error)
<< "session_manager:: unsupported protocol in URL " << sink.source;
return DaemonErrc::invalid_url;
} }
BOOST_LOG_TRIVIAL(info) BOOST_LOG_TRIVIAL(info)
<< "session_manager:: SDP from URL " << sink.source << " :\n" << "session_manager:: SDP from URL " << sink.source << " :\n"
<< res->body; << sdp;
if (!parse_sdp(res->body, info)) { if (!parse_sdp(sdp, info)) {
return DaemonErrc::cannot_parse_sdp; return DaemonErrc::cannot_parse_sdp;
} }
info.sink_sdp = res->body; info.sink_sdp = std::move(sdp);
} else { } else {
BOOST_LOG_TRIVIAL(info) << "session_manager:: using SDP " << sink.sdp; BOOST_LOG_TRIVIAL(info) << "session_manager:: using SDP "
<< std::endl << sink.sdp;
if (!parse_sdp(sink.sdp, info)) { if (!parse_sdp(sink.sdp, info)) {
return DaemonErrc::cannot_parse_sdp; return DaemonErrc::cannot_parse_sdp;
} }
info.sink_sdp = sink.sdp; info.sink_sdp = std::move(sink.sdp);
} }
info.sink_source = sink.source; info.sink_source = sink.source;
info.sink_use_sdp = true; // save back and use with SDP file info.sink_use_sdp = true; // save back and use with SDP file
@ -841,21 +855,6 @@ void SessionManager::get_ptp_status(PTPStatus& status) const {
status = ptp_status_; status = ptp_status_;
} }
static uint16_t crc16(const uint8_t* p, size_t len) {
uint8_t x;
uint16_t crc = 0xFFFF;
while (len--) {
x = crc >> 8 ^ *p++;
x ^= x >> 4;
crc = (crc << 8) ^
(static_cast<uint16_t>(x << 12)) ^
(static_cast<uint16_t>(x << 5)) ^
(static_cast<uint16_t>(x));
}
return crc;
}
size_t SessionManager::process_sap() { size_t SessionManager::process_sap() {
size_t sdp_len_sum = 0; size_t sdp_len_sum = 0;
// set to contain sources currently announced // set to contain sources currently announced

View File

@ -101,10 +101,10 @@ class SessionManager {
SessionManager() = delete; SessionManager() = delete;
SessionManager(const SessionManager&) = delete; SessionManager(const SessionManager&) = delete;
SessionManager& operator=(const SessionManager&) = delete; SessionManager& operator=(const SessionManager&) = delete;
virtual ~SessionManager(){ stop(); }; virtual ~SessionManager(){ terminate(); };
// session manager interface // session manager interface
bool start() { bool init() {
if (!running_) { if (!running_) {
running_ = true; running_ = true;
res_ = std::async(std::launch::async, &SessionManager::worker, this); res_ = std::async(std::launch::async, &SessionManager::worker, this);
@ -112,7 +112,7 @@ class SessionManager {
return true; return true;
} }
bool stop() { bool terminate() {
if (running_) { if (running_) {
running_ = false; running_ = false;
auto ret = res_.get(); auto ret = res_.get();

View File

@ -6,7 +6,7 @@
"tic_frame_size_at_1fs": 192, "tic_frame_size_at_1fs": 192,
"max_tic_frame_size": 1024, "max_tic_frame_size": 1024,
"sample_rate": 44100, "sample_rate": 44100,
"rtp_mcast_base": "239.2.0.1", "rtp_mcast_base": "239.1.0.1",
"rtp_port": 6004, "rtp_port": 6004,
"ptp_domain": 0, "ptp_domain": 0,
"ptp_dscp": 46, "ptp_dscp": 46,
@ -16,6 +16,7 @@
"syslog_server": "255.255.255.254:1234", "syslog_server": "255.255.255.254:1234",
"status_file": "", "status_file": "",
"interface_name": "lo", "interface_name": "lo",
"mdns_enabled": "false",
"mac_addr": "00:00:00:00:00:00", "mac_addr": "00:00:00:00:00:00",
"ip_addr": "127.0.0.1" "ip_addr": "127.0.0.1"
} }

View File

@ -333,7 +333,7 @@ BOOST_AUTO_TEST_CASE(get_config) {
BOOST_CHECK_MESSAGE(tic_frame_size_at_1fs == 192, "config as excepcted"); BOOST_CHECK_MESSAGE(tic_frame_size_at_1fs == 192, "config as excepcted");
BOOST_CHECK_MESSAGE(max_tic_frame_size == 1024, "config as excepcted"); BOOST_CHECK_MESSAGE(max_tic_frame_size == 1024, "config as excepcted");
BOOST_CHECK_MESSAGE(sample_rate == 44100, "config as excepcted"); BOOST_CHECK_MESSAGE(sample_rate == 44100, "config as excepcted");
BOOST_CHECK_MESSAGE(rtp_mcast_base == "239.2.0.1", "config as excepcted"); BOOST_CHECK_MESSAGE(rtp_mcast_base == "239.1.0.1", "config as excepcted");
BOOST_CHECK_MESSAGE(rtp_port == 6004, "config as excepcted"); BOOST_CHECK_MESSAGE(rtp_port == 6004, "config as excepcted");
BOOST_CHECK_MESSAGE(ptp_domain == 0, "config as excepcted"); BOOST_CHECK_MESSAGE(ptp_domain == 0, "config as excepcted");
BOOST_CHECK_MESSAGE(ptp_dscp == 46, "config as excepcted"); BOOST_CHECK_MESSAGE(ptp_dscp == 46, "config as excepcted");

34
daemon/utils.cpp Normal file
View File

@ -0,0 +1,34 @@
//
// utils.cpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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 "utils.hpp"
uint16_t crc16(const uint8_t* p, size_t len) {
uint8_t x;
uint16_t crc = 0xFFFF;
while (len--) {
x = crc >> 8 ^ *p++;
x ^= x >> 4;
crc = (crc << 8) ^ (static_cast<uint16_t>(x << 12)) ^
(static_cast<uint16_t>(x << 5)) ^ (static_cast<uint16_t>(x));
}
return crc;
}

29
daemon/utils.hpp Normal file
View File

@ -0,0 +1,29 @@
//
// utils.hpp
//
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
// 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
// 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 _UTILS_HPP_
#define _UTILS_HPP_
#include <stdint.h>
#include <cstddef>
uint16_t crc16(const uint8_t* p, size_t len);
#endif

View File

@ -14,5 +14,6 @@ sudo apt-get install -y libboost-all-dev
sudo apt-get install -y valgrind sudo apt-get install -y valgrind
sudo apt-get install -y linux-sound-base alsa-base alsa-utils sudo apt-get install -y linux-sound-base alsa-base alsa-utils
sudo apt-get install -y linuxptp sudo apt-get install -y linuxptp
sudo apt-get install -y libavahi-client-dev
sudo apt install -y linux-headers-$(uname -r) sudo apt install -y linux-headers-$(uname -r)

View File

@ -68,7 +68,7 @@ class RemoteSourceEntry extends Component {
<td> <label>{this.state.rtp_address}</label> </td> <td> <label>{this.state.rtp_address}</label> </td>
<td align='center'> <label>{this.state.port}</label> </td> <td align='center'> <label>{this.state.port}</label> </td>
<td align='center'> <label>{this.props.last_seen}</label> </td> <td align='center'> <label>{this.props.last_seen}</label> </td>
<td align='center'> <label>{this.props.period}</label> </td> <td align='center'> <label>{this.props.source=='SAP' ? this.props.period : 'N/A'}</label> </td>
<td> <span className='pointer-area' onClick={this.handleInfoClick}> <img width='20' height='20' src='/info.png' alt=''/> </span> </td> <td> <span className='pointer-area' onClick={this.handleInfoClick}> <img width='20' height='20' src='/info.png' alt=''/> </span> </td>
</tr> </tr>
); );
@ -96,7 +96,7 @@ class RemoteSourceList extends Component {
<th>Name</th> <th>Name</th>
<th>RTP Address</th> <th>RTP Address</th>
<th>Port</th> <th>Port</th>
<th>Last seen</th> <th>Seen</th>
<th>Period</th> <th>Period</th>
</tr> </tr>
: <tr> : <tr>
@ -184,7 +184,7 @@ class RemoteSources extends Component {
closeInfo={this.closeInfo} closeInfo={this.closeInfo}
infoTitle={this.state.infoTitle} infoTitle={this.state.infoTitle}
id={this.state.source.id} id={this.state.source.id}
address={this.state.source.address} source={this.state.source.source}
name={this.state.source.name} name={this.state.source.name}
sdp={this.state.source.sdp} /> sdp={this.state.source.sdp} />
: undefined } : undefined }

View File

@ -51,6 +51,7 @@ class SinkEdit extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
sources: [],
id: this.props.sink.id, id: this.props.sink.id,
name: this.props.sink.name, name: this.props.sink.name,
nameErr: false, nameErr: false,
@ -75,10 +76,20 @@ class SinkEdit extends Component {
this.onChangeChannels = this.onChangeChannels.bind(this); this.onChangeChannels = this.onChangeChannels.bind(this);
this.onChangeChannelsMap = this.onChangeChannelsMap.bind(this); this.onChangeChannelsMap = this.onChangeChannelsMap.bind(this);
this.inputIsValid = this.inputIsValid.bind(this); this.inputIsValid = this.inputIsValid.bind(this);
this.fetchRemoteSources = this.fetchRemoteSources.bind(this);
this.onChangeRemoteSourceSDP = this.onChangeRemoteSourceSDP.bind(this);
}
fetchRemoteSources() {
RestAPI.getRemoteSources()
.then(response => response.json())
.then(
data => this.setState( { sources: data.remote_sources }))
} }
componentDidMount() { componentDidMount() {
Modal.setAppElement('body'); Modal.setAppElement('body');
this.fetchRemoteSources();
} }
addSink(message) { addSink(message) {
@ -130,6 +141,12 @@ class SinkEdit extends Component {
this.setState({ map: map }); this.setState({ map: map });
} }
onChangeRemoteSourceSDP(e) {
if (e.target.value) {
this.setState({ sdp: e.target.value });
}
}
inputIsValid() { inputIsValid() {
return !this.state.nameErr && return !this.state.nameErr &&
!this.state.sourceErr && !this.state.sourceErr &&
@ -160,11 +177,22 @@ class SinkEdit extends Component {
<th align="left"> <input type="checkbox" defaultChecked={this.state.useSdp} onChange={e => this.setState({useSdp: e.target.checked})} /> </th> <th align="left"> <input type="checkbox" defaultChecked={this.state.useSdp} onChange={e => this.setState({useSdp: e.target.checked})} /> </th>
</tr> </tr>
<tr> <tr>
<th align="left"> <label>Source URL</label> </th> <th align="left"> <font color={this.state.useSdp ? 'grey' : 'black'}>Source URL</font> </th>
<th align="left"> <input type='url' size="30" value={this.state.source} onChange={e => this.setState({source: e.target.value, sourceErr: !e.currentTarget.checkValidity()})} disabled={this.state.useSdp ? true : undefined} required/> </th> <th align="left"> <input type='url' size="30" value={this.state.source} onChange={e => this.setState({source: e.target.value, sourceErr: !e.currentTarget.checkValidity()})} disabled={this.state.useSdp ? true : undefined} required/> </th>
</tr> </tr>
<tr> <tr>
<th align="left"> <font color={this.state.source ? 'grey' : 'black'}>SDP</font> </th> <th align="left"> <font color={!this.state.useSdp ? 'grey' : 'black'}>Remote Source SDP</font> </th>
<th align="left">
<select value={this.state.sdp} onChange={this.onChangeRemoteSourceSDP} disabled={this.state.useSdp ? undefined : true}>
<option key='' value=''> -- select a remote source SDP -- </option>
{
this.state.sources.map((v) => <option key={v.id} value={v.sdp}>{v.source + ', ' + v.name}</option>)
}
</select>
</th>
</tr>
<tr>
<th align="left"> <font color={!this.state.useSdp ? 'grey' : 'black'}>SDP</font> </th>
<th align="left"> <textarea rows='15' cols='55' value={this.state.sdp} onChange={e => this.setState({sdp: e.target.value})} disabled={this.state.useSdp ? undefined : true} required/> </th> <th align="left"> <textarea rows='15' cols='55' value={this.state.sdp} onChange={e => this.setState({sdp: e.target.value})} disabled={this.state.useSdp ? undefined : true} required/> </th>
</tr> </tr>
<tr> <tr>

View File

@ -38,6 +38,7 @@ const infoCustomStyles = {
class SourceInfo extends Component { class SourceInfo extends Component {
static propTypes = { static propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
source: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
sdp: PropTypes.string.isRequired, sdp: PropTypes.string.isRequired,
closeInfo: PropTypes.func.isRequired, closeInfo: PropTypes.func.isRequired,
@ -72,6 +73,10 @@ class SourceInfo extends Component {
<th align="left"> <label>ID</label> </th> <th align="left"> <label>ID</label> </th>
<th align="left"> <input value={this.props.id} readOnly/> </th> <th align="left"> <input value={this.props.id} readOnly/> </th>
</tr> </tr>
<tr>
<th align="left"> <label>Source</label> </th>
<th align="left"> <input value={this.props.source} readOnly/> </th>
</tr>
<tr> <tr>
<th align="left"> <label>Name</label> </th> <th align="left"> <label>Name</label> </th>
<th align="left"> <input value={this.props.name} readOnly/> </th> <th align="left"> <input value={this.props.name} readOnly/> </th>

View File

@ -264,6 +264,7 @@ class Sources extends Component {
isInfo={this.state.isInfo} isInfo={this.state.isInfo}
id={this.state.source.id.toString()} id={this.state.source.id.toString()}
name={this.state.source.name} name={this.state.source.name}
source='local'
sdp={this.state.sdp} /> sdp={this.state.sdp} />
: undefined } : undefined }
{ this.state.editIsOpen ? { this.state.editIsOpen ?