From 5deb6c19278c390e3577451de6a00415b9b08a77 Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Sun, 29 Mar 2020 19:41:56 +0200 Subject: [PATCH] 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. --- README.md | 3 + build.sh | 2 +- daemon/.clang-format | 157 +++++++++++++++++++++ daemon/CMakeLists.txt | 16 ++- daemon/README.md | 7 +- daemon/browser.cpp | 108 +++++++++++---- daemon/browser.hpp | 55 +++++--- daemon/config.cpp | 11 +- daemon/config.hpp | 5 + daemon/daemon.conf | 1 + daemon/driver_handler.hpp | 2 + daemon/http_server.cpp | 4 +- daemon/http_server.hpp | 4 +- daemon/json.cpp | 3 + daemon/main.cpp | 26 ++-- daemon/mdns_client.cpp | 255 +++++++++++++++++++++++++++++++++++ daemon/mdns_client.hpp | 99 ++++++++++++++ daemon/rtsp_client.cpp | 177 ++++++++++++++++++++++++ daemon/rtsp_client.hpp | 44 ++++++ daemon/sap.cpp | 2 +- daemon/session_manager.cpp | 83 ++++++------ daemon/session_manager.hpp | 6 +- daemon/tests/daemon.conf | 3 +- daemon/tests/daemon_test.cpp | 2 +- daemon/utils.cpp | 34 +++++ daemon/utils.hpp | 29 ++++ ubuntu-packages.sh | 1 + webui/src/RemoteSources.js | 6 +- webui/src/SinkEdit.js | 32 ++++- webui/src/SourceInfo.js | 5 + webui/src/Sources.js | 1 + 31 files changed, 1062 insertions(+), 121 deletions(-) create mode 100644 daemon/.clang-format create mode 100644 daemon/mdns_client.cpp create mode 100644 daemon/mdns_client.hpp create mode 100644 daemon/rtsp_client.cpp create mode 100644 daemon/rtsp_client.hpp create mode 100644 daemon/utils.cpp create mode 100644 daemon/utils.hpp diff --git a/README.md b/README.md index 693e256..374c133 100644 --- a/README.md +++ b/README.md @@ -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). * **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) ## 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 * HTTP REST API for control and configuration * 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 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 * npm version >= 3.5.2 * 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. See [Ubuntu 18.04 on BeagleBone® Black](https://elinux.org/BeagleBoardUbuntu) for additional information about how to setup Ubuntu on this board. diff --git a/build.sh b/build.sh index 01b95af..eb45a45 100755 --- a/build.sh +++ b/build.sh @@ -40,7 +40,7 @@ cd .. cd daemon echo "Building aes67-daemon ..." -cmake . +cmake -DWITH_AVAHI=ON . make cd .. diff --git a/daemon/.clang-format b/daemon/.clang-format new file mode 100644 index 0000000..d46c7cc --- /dev/null +++ b/daemon/.clang-format @@ -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: '^' + 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 +... + diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index f184c45..bb1a29b 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -1,12 +1,24 @@ cmake_minimum_required(VERSION 3.7.0) project(aes67-daemon CXX) enable_testing() +option(WITH_AVAHI "Include mDNS support via Avahi" OFF) 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(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) 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) 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() diff --git a/daemon/README.md b/daemon/README.md index 9d7b32b..7c5a70a 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -8,6 +8,7 @@ The daemon is responsible for: * provide an HTTP REST API for the daemon control and configuration * session handling and SDP parsing and creation * SAP discovery protocol and SAP browser +* mDNS sources discovery (using Avahi) and SDP transfer via RTSP * IGMP handling for SAP and RTP sessions ## Configuration file ## @@ -232,6 +233,9 @@ where: > 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. +> **mdns\_enabled** +> JSON boolean specifying whether the mDNS discovery is enabled or disabled. + ### JSON PTP Config ### 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. > **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** > JSON string specifying the SDP of the source. This parameter is mandatory if **use\_sdp** is true. diff --git a/daemon/browser.cpp b/daemon/browser.cpp index 4968b05..24d525d 100644 --- a/daemon/browser.cpp +++ b/daemon/browser.cpp @@ -17,11 +17,10 @@ // along with this program. If not, see . // -#include #include "browser.hpp" using namespace std::chrono; -using second_t = std::chrono::duration >; +using second_t = duration >; std::shared_ptr Browser::create( std::shared_ptr config) { @@ -39,7 +38,8 @@ std::shared_ptr Browser::create( std::list Browser::get_remote_sources() { std::list sources_list; 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()) { sources_list.push_back(source); } return sources_list; @@ -60,9 +60,10 @@ bool Browser::worker() { sap_.set_multicast_interface(config_->get_ip_addr_str()); // Join SAP muticast address igmp_.join(config_->get_ip_addr_str(), config_->get_sap_mcast_addr()); - auto startup = steady_clock::now(); auto sap_timepoint = steady_clock::now(); int sap_interval = 10; + auto mdns_timepoint = steady_clock::now(); + int mdns_interval = 10; while (running_) { bool is_announce; @@ -71,12 +72,14 @@ bool Browser::worker() { std::string sdp; if (sap_.receive(is_announce, msg_id_hash, addr, sdp)) { - char id[13]; - snprintf(id, sizeof id, "%x%x", addr, msg_id_hash); - //BOOST_LOG_TRIVIAL(debug) << "browser:: received SAP message for " << id; + std::stringstream ss; + ss << "sap:" << std::hex << addr << msg_id_hash; + std::string id(ss.str()); + BOOST_LOG_TRIVIAL(debug) << "browser:: received SAP message for " << id; std::unique_lock sources_lock(sources_mutex_); - auto it = sources_.find(id); + + auto it = sources_.get().find(id); if (it == sources_.end()) { // Source is not in the map if (is_announce) { @@ -89,23 +92,25 @@ bool Browser::worker() { source.address = ip::address_v4(ntohl(addr)).to_string(); source.name = sdp_get_subject(sdp); source.last_seen = - duration_cast(steady_clock::now() - startup).count(); + duration_cast(steady_clock::now() - startup_).count(); source.announce_period = 360; //default period - sources_[id] = source; + sources_.insert(source); } } else { // Source is already in the map if (is_announce) { BOOST_LOG_TRIVIAL(debug) - << "browser:: refreshing SAP source " << (*it).second.id; + << "browser:: refreshing SAP source " << it->id; // annoucement, update last seen and announce period + auto upd_source{*it}; uint32_t last_seen = - duration_cast(steady_clock::now() - startup).count(); - (*it).second.announce_period = last_seen - (*it).second.last_seen; - (*it).second.last_seen = last_seen; + duration_cast(steady_clock::now() - startup_).count(); + upd_source.announce_period = last_seen - upd_source.last_seen; + upd_source.last_seen = last_seen; + sources_.replace(it, upd_source); } else { BOOST_LOG_TRIVIAL(info) - << "browser:: removing SAP source " << (*it).second.id; + << "browser:: removing SAP source " << it->id; // deletion, remove entry sources_.erase(it); } @@ -117,22 +122,79 @@ bool Browser::worker() { > sap_interval) { sap_timepoint = steady_clock::now(); // remove all sessions no longer announced - auto offset = duration_cast(steady_clock::now() - startup).count(); + auto offset = duration_cast(steady_clock::now() - startup_).count(); std::unique_lock sources_lock(sources_mutex_); - std::experimental::erase_if(sources_, [offset](auto entry) { - if ((offset - entry.second.last_seen) > - (entry.second.announce_period * 10)) { + for (auto it = sources_.begin(); it != sources_.end();) { + if (it->source == "SAP" && + (offset - it->last_seen) > (it->announce_period * 10)) { // remove from remote SAP sources BOOST_LOG_TRIVIAL(info) - << "browser:: SAP source " << entry.second.id << " timeout"; - return true; + << "browser:: SAP source " << it->id << " timeout"; + it = sources_.erase(it); + } else { + it++; } - return false; - }); + } + } + + // check if it's time to process the mDNS RTSP sources + if ((duration_cast(steady_clock::now() - mdns_timepoint).count()) + > mdns_interval) { + mdns_timepoint = steady_clock::now(); + process_results(); } } 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(steady_clock::now() - startup_).count(); + std::unique_lock sources_lock(sources_mutex_); + if (sources_.get().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(); + 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; +} + diff --git a/daemon/browser.hpp b/daemon/browser.hpp index f784220..c593604 100644 --- a/daemon/browser.hpp +++ b/daemon/browser.hpp @@ -26,9 +26,18 @@ #include #include +#include +#include +#include +#include +#include + #include "config.hpp" #include "sap.hpp" #include "igmp.hpp" +#include "mdns_client.hpp" + +using namespace boost::multi_index; struct RemoteSource { std::string id; @@ -40,50 +49,56 @@ struct RemoteSource { uint32_t announce_period; /* period between annoucements */ }; -class Browser { +class Browser : public MDNSClient { public: static std::shared_ptr create( std::shared_ptr config); Browser() = delete; Browser(const Browser&) = delete; Browser& operator=(const Browser&) = delete; - virtual ~Browser(){ stop(); }; + virtual ~Browser(){ terminate(); }; - bool start() { - if (!running_) { - running_ = true; - res_ = std::async(std::launch::async, &Browser::worker, this); - } - return true; - } - - bool stop() { - if (running_) { - running_ = false; - return res_.get(); - } - return true; - } + bool init() override; + bool terminate() override; std::list get_remote_sources(); protected: // singleton, use create() to build - Browser(std::shared_ptr config) - : config_(config){}; + Browser(std::shared_ptr config): + config_(config), + startup_(std::chrono::steady_clock::now()){}; 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_; std::future res_; std::atomic_bool running_{false}; /* current sources */ - std::map sources_; + struct id_tag{}; + using by_id = hashed_unique, member>; + struct name_tag{}; + using by_name = ordered_non_unique, member>; + using sources_t = multi_index_container>; + + sources_t sources_; mutable std::shared_mutex sources_mutex_; SAP sap_{config_->get_sap_mcast_addr()}; IGMP igmp_; + std::chrono::time_point startup_; }; #endif diff --git a/daemon/config.cpp b/daemon/config.cpp index ae27562..83ecd9f 100644 --- a/daemon/config.cpp +++ b/daemon/config.cpp @@ -65,12 +65,15 @@ std::shared_ptr Config::parse(const std::string& filename) { config.max_tic_frame_size_ = 1024; if (config.sample_rate_ == 0) config.sample_rate_ = 44100; - if (ip::address_v4::from_string(config.rtp_mcast_base_.c_str()).to_ulong() == - INADDR_NONE) + boost::system::error_code ec; + ip::address_v4::from_string(config.rtp_mcast_base_.c_str(), ec); + if (!ec) { 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"; + } if (config.ptp_domain_ > 127) if (config.ptp_domain_ > 127) config.ptp_domain_ = 0; diff --git a/daemon/config.hpp b/daemon/config.hpp index a9ebc71..fbe7c41 100644 --- a/daemon/config.hpp +++ b/daemon/config.hpp @@ -57,6 +57,7 @@ class Config { uint32_t get_ip_addr() const { return ip_addr_; }; const std::string& get_ip_addr_str() const { return ip_str_; }; 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_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& mac_addr) { mac_addr_ = mac_addr; }; + void set_mdns_enabled(bool enabled) { + mdns_enabled = enabled; + }; private: /* from json */ @@ -121,6 +125,7 @@ class Config { std::string syslog_server_{""}; std::string status_file_{"./status.json"}; std::string interface_name_{"eth0"}; + bool mdns_enabled{true}; /* set during init */ std::array mac_addr_{0, 0, 0, 0, 0, 0}; diff --git a/daemon/daemon.conf b/daemon/daemon.conf index 370318c..0a056e1 100644 --- a/daemon/daemon.conf +++ b/daemon/daemon.conf @@ -15,5 +15,6 @@ "syslog_proto": "none", "syslog_server": "255.255.255.254:1234", "status_file": "./status.json", + "mdns_enabled": true, "interface_name": "lo" } diff --git a/daemon/driver_handler.hpp b/daemon/driver_handler.hpp index df0f65f..b6f5297 100644 --- a/daemon/driver_handler.hpp +++ b/daemon/driver_handler.hpp @@ -44,6 +44,8 @@ class DriverHandler { virtual bool init(const Config& config); virtual bool terminate(); + + protected: virtual void send_command(enum MT_ALSA_msg_id id, size_t size = 0, const uint8_t* data = nullptr); diff --git a/daemon/http_server.cpp b/daemon/http_server.cpp index 5b9bfcd..c62e343 100644 --- a/daemon/http_server.cpp +++ b/daemon/http_server.cpp @@ -77,7 +77,7 @@ static inline void set_error( res.body = message; } -bool HttpServer::start() { +bool HttpServer::init() { /* setup http operations */ if (!svr_.is_valid()) { return false; @@ -325,7 +325,7 @@ bool HttpServer::start() { return retry; } -bool HttpServer::stop() { +bool HttpServer::terminate() { BOOST_LOG_TRIVIAL(info) << "http_server: stopping ... "; svr_.stop(); return res_.get(); diff --git a/daemon/http_server.hpp b/daemon/http_server.hpp index 22db076..13ff59a 100644 --- a/daemon/http_server.hpp +++ b/daemon/http_server.hpp @@ -35,8 +35,8 @@ class HttpServer { : session_manager_(session_manager), browser_(browser), config_(config) {}; - bool start(); - bool stop(); + bool init(); + bool terminate(); private: std::shared_ptr session_manager_; diff --git a/daemon/json.cpp b/daemon/json.cpp index b29eb1d..340b9ce 100644 --- a/daemon/json.cpp +++ b/daemon/json.cpp @@ -91,6 +91,7 @@ std::string config_to_json(const Config& config) { << ",\n \"syslog_server\": \"" << escape_json(config.get_syslog_server()) << "\"" << ",\n \"status_file\": \"" << escape_json(config.get_status_file()) << "\"" << ",\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 \"ip_addr\": \"" << escape_json(config.get_ip_addr_str()) << "\"" << "\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())); } else if (key == "syslog_server") { config.set_syslog_server(remove_undesired_chars(val.get_value())); + } else if (key == "mdns_enabled") { + config.set_mdns_enabled(val.get_value()); } else if (key == "mac_addr" || key == "ip_addr") { /* ignored */ } else { diff --git a/daemon/main.cpp b/daemon/main.cpp index 9f91f50..0f7a0ff 100644 --- a/daemon/main.cpp +++ b/daemon/main.cpp @@ -111,22 +111,22 @@ int main(int argc, char* argv[]) { /* start session manager */ 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( - std::string("SessionManager:: start failed")); + std::string("SessionManager:: init failed")); } - /* start browser */ + /* start browser */ auto browser = Browser::create(config); - if (browser == nullptr || !browser->start()) { + if (browser == nullptr || !browser->init()) { throw std::runtime_error( - std::string("Browser:: start failed")); + std::string("Browser:: init failed")); } /* start http server */ HttpServer http_server(session_manager, browser, config); - if (!http_server.start()) { - throw std::runtime_error(std::string("HttpServer:: start failed")); + if (!http_server.init()) { + throw std::runtime_error(std::string("HttpServer:: init failed")); } /* load session status from file */ @@ -154,21 +154,21 @@ int main(int argc, char* argv[]) { session_manager->save_status(); /* stop http server */ - if (!http_server.stop()) { + if (!http_server.terminate()) { throw std::runtime_error( - std::string("HttpServer:: stop failed")); + std::string("HttpServer:: terminate failed")); } /* stop browser */ - if (!browser->stop()) { + if (!browser->terminate()) { throw std::runtime_error( - std::string("Browser:: stop failed")); + std::string("Browser:: terminate failed")); } /* stop session manager */ - if (!session_manager->stop()) { + if (!session_manager->terminate()) { throw std::runtime_error( - std::string("SessionManager:: stop failed")); + std::string("SessionManager:: terminate failed")); } /* stop driver manager */ diff --git a/daemon/mdns_client.cpp b/daemon/mdns_client.cpp new file mode 100644 index 0000000..cd577c6 --- /dev/null +++ b/daemon/mdns_client.cpp @@ -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 . +// + +#include "mdns_client.hpp" + +#include + +#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(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 lock(mdns.sources_res_mutex_); + + /* have fun ;-) */ + mdns.sources_res_.emplace_back(std::async( + std::launch::async, + [&mdns, name_ = std::forward(name), + domain_ = std::forward(domain), + addr_ = std::forward(addr), + port_ = std::forward(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(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(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 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 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; +} diff --git a/daemon/mdns_client.hpp b/daemon/mdns_client.hpp new file mode 100644 index 0000000..28de978 --- /dev/null +++ b/daemon/mdns_client.hpp @@ -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 . +// + +#ifndef _AVAHI_CLIENT_HPP_ +#define _AVAHI_CLIENT_HPP_ + +#ifdef _USE_AVAHI_ +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include + +#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 > sources_res_; + std::mutex sources_res_mutex_; + + std::atomic_bool running_{false}; + +#ifdef _USE_AVAHI_ + /* order is important here */ + std::unique_ptr poll_{ + nullptr, &avahi_threaded_poll_free}; + std::unique_ptr< ::AvahiClient, decltype(&avahi_client_free)> client_{ + nullptr, &avahi_client_free}; + std::unique_ptr + 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 diff --git a/daemon/rtsp_client.cpp b/daemon/rtsp_client.cpp new file mode 100644 index 0000000..f6a75ee --- /dev/null +++ b/daemon/rtsp_client.cpp @@ -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 . +// + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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(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); +} diff --git a/daemon/rtsp_client.hpp b/daemon/rtsp_client.hpp new file mode 100644 index 0000000..60b2561 --- /dev/null +++ b/daemon/rtsp_client.hpp @@ -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 . +// + +#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 describe( + const std::string& path, + const std::string& address, + const std::string& port = dft_port); + + inline static std::atomic seq_number; +}; + +#endif diff --git a/daemon/sap.cpp b/daemon/sap.cpp index eb542af..89203f8 100644 --- a/daemon/sap.cpp +++ b/daemon/sap.cpp @@ -99,7 +99,7 @@ bool SAP::receive(bool& is_announce, is_announce = (buffer[0] == 0x20); memcpy(&msg_id_hash, buffer + 2, sizeof(msg_id_hash)); 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(length); i++) { buffer[i] = std::tolower(buffer[i]); } if (!memcmp(buffer + 8, "application/sdp", 16)) { diff --git a/daemon/session_manager.cpp b/daemon/session_manager.cpp index 8f3b3a4..e93ca08 100644 --- a/daemon/session_manager.cpp +++ b/daemon/session_manager.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,8 @@ #include "json.hpp" #include "log.hpp" #include "session_manager.hpp" +#include "utils.hpp" +#include "rtsp_client.hpp" 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::string line; while (getline(ssstrem, line, '\n')) { + boost::trim(line); ++num; if (line[1] != '=') { BOOST_LOG_TRIVIAL(error) @@ -289,7 +293,7 @@ bool SessionManager::parse_sdp(const std::string sdp, StreamInfo& info) const { } } catch (...) { BOOST_LOG_TRIVIAL(fatal) << "session_manager:: invalid SDP at line " << num - << ", cannot perform number convesrion"; + << ", cannot perform number conversion"; return false; } @@ -659,45 +663,55 @@ std::error_code SessionManager::add_sink(const StreamSink& sink) { return DaemonErrc::invalid_url; } - if (!boost::iequals(protocol, "http")) { - BOOST_LOG_TRIVIAL(error) - << "session_manager:: unsupported protocol in URL " << sink.source; - return DaemonErrc::invalid_url; - } - - httplib::Client cli(host.c_str(), - !atoi(port.c_str()) ? 80 : atoi(port.c_str())); - cli.set_timeout_sec(10); - auto res = cli.Get(path.c_str()); - if (!res) { - BOOST_LOG_TRIVIAL(error) - << "session_manager:: cannot retrieve SDP from URL " << sink.source; - return DaemonErrc::cannot_retrieve_sdp; - } - - if (res->status != 200) { - BOOST_LOG_TRIVIAL(error) - << "session_manager:: cannot retrieve SDP from URL " << sink.source - << " server reply " << res->status; - return DaemonErrc::cannot_retrieve_sdp; + std::string sdp; + if (boost::iequals(protocol, "http")) { + httplib::Client cli(host.c_str(), + !atoi(port.c_str()) ? 80 : atoi(port.c_str())); + cli.set_timeout_sec(10); + auto res = cli.Get(path.c_str()); + if (!res) { + BOOST_LOG_TRIVIAL(error) + << "session_manager:: annot retrieve SDP from URL " << sink.source; + return DaemonErrc::cannot_retrieve_sdp; + } + if (res->status != 200) { + BOOST_LOG_TRIVIAL(error) + << "session_manager:: cannot retrieve SDP from URL " << sink.source + << " server reply " << res->status; + return DaemonErrc::cannot_retrieve_sdp; + } + sdp = std::move(res->body); + } else if (boost::iequals(protocol, "rtsp")) { + auto res = RTSPClient::describe(path, host, port); + 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) << "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; } - info.sink_sdp = res->body; + info.sink_sdp = std::move(sdp); } 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)) { return DaemonErrc::cannot_parse_sdp; } - info.sink_sdp = sink.sdp; + info.sink_sdp = std::move(sink.sdp); } info.sink_source = sink.source; 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_; } -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(x << 12)) ^ - (static_cast(x << 5)) ^ - (static_cast(x)); - } - return crc; -} - size_t SessionManager::process_sap() { size_t sdp_len_sum = 0; // set to contain sources currently announced diff --git a/daemon/session_manager.hpp b/daemon/session_manager.hpp index beb3b98..5a1c1b6 100644 --- a/daemon/session_manager.hpp +++ b/daemon/session_manager.hpp @@ -101,10 +101,10 @@ class SessionManager { SessionManager() = delete; SessionManager(const SessionManager&) = delete; SessionManager& operator=(const SessionManager&) = delete; - virtual ~SessionManager(){ stop(); }; + virtual ~SessionManager(){ terminate(); }; // session manager interface - bool start() { + bool init() { if (!running_) { running_ = true; res_ = std::async(std::launch::async, &SessionManager::worker, this); @@ -112,7 +112,7 @@ class SessionManager { return true; } - bool stop() { + bool terminate() { if (running_) { running_ = false; auto ret = res_.get(); diff --git a/daemon/tests/daemon.conf b/daemon/tests/daemon.conf index 18f8c51..609897c 100644 --- a/daemon/tests/daemon.conf +++ b/daemon/tests/daemon.conf @@ -6,7 +6,7 @@ "tic_frame_size_at_1fs": 192, "max_tic_frame_size": 1024, "sample_rate": 44100, - "rtp_mcast_base": "239.2.0.1", + "rtp_mcast_base": "239.1.0.1", "rtp_port": 6004, "ptp_domain": 0, "ptp_dscp": 46, @@ -16,6 +16,7 @@ "syslog_server": "255.255.255.254:1234", "status_file": "", "interface_name": "lo", + "mdns_enabled": "false", "mac_addr": "00:00:00:00:00:00", "ip_addr": "127.0.0.1" } diff --git a/daemon/tests/daemon_test.cpp b/daemon/tests/daemon_test.cpp index f47234c..3cd4cf8 100644 --- a/daemon/tests/daemon_test.cpp +++ b/daemon/tests/daemon_test.cpp @@ -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(max_tic_frame_size == 1024, "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(ptp_domain == 0, "config as excepcted"); BOOST_CHECK_MESSAGE(ptp_dscp == 46, "config as excepcted"); diff --git a/daemon/utils.cpp b/daemon/utils.cpp new file mode 100644 index 0000000..46bbd65 --- /dev/null +++ b/daemon/utils.cpp @@ -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 . +// +// + +#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(x << 12)) ^ + (static_cast(x << 5)) ^ (static_cast(x)); + } + return crc; +} diff --git a/daemon/utils.hpp b/daemon/utils.hpp new file mode 100644 index 0000000..07345f7 --- /dev/null +++ b/daemon/utils.hpp @@ -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 . +// + +#ifndef _UTILS_HPP_ +#define _UTILS_HPP_ + +#include + +#include + +uint16_t crc16(const uint8_t* p, size_t len); + +#endif diff --git a/ubuntu-packages.sh b/ubuntu-packages.sh index d6b8310..964e9ff 100755 --- a/ubuntu-packages.sh +++ b/ubuntu-packages.sh @@ -14,5 +14,6 @@ sudo apt-get install -y libboost-all-dev sudo apt-get install -y valgrind sudo apt-get install -y linux-sound-base alsa-base alsa-utils sudo apt-get install -y linuxptp +sudo apt-get install -y libavahi-client-dev sudo apt install -y linux-headers-$(uname -r) diff --git a/webui/src/RemoteSources.js b/webui/src/RemoteSources.js index 7ebc232..38274e1 100644 --- a/webui/src/RemoteSources.js +++ b/webui/src/RemoteSources.js @@ -68,7 +68,7 @@ class RemoteSourceEntry extends Component { - + ); @@ -96,7 +96,7 @@ class RemoteSourceList extends Component { Name RTP Address Port - Last seen + Seen Period : @@ -184,7 +184,7 @@ class RemoteSources extends Component { closeInfo={this.closeInfo} infoTitle={this.state.infoTitle} id={this.state.source.id} - address={this.state.source.address} + source={this.state.source.source} name={this.state.source.name} sdp={this.state.source.sdp} /> : undefined } diff --git a/webui/src/SinkEdit.js b/webui/src/SinkEdit.js index 01ea4ad..a860ba6 100644 --- a/webui/src/SinkEdit.js +++ b/webui/src/SinkEdit.js @@ -51,6 +51,7 @@ class SinkEdit extends Component { constructor(props) { super(props); this.state = { + sources: [], id: this.props.sink.id, name: this.props.sink.name, nameErr: false, @@ -75,10 +76,20 @@ class SinkEdit extends Component { this.onChangeChannels = this.onChangeChannels.bind(this); this.onChangeChannelsMap = this.onChangeChannelsMap.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() { Modal.setAppElement('body'); + this.fetchRemoteSources(); } addSink(message) { @@ -130,6 +141,12 @@ class SinkEdit extends Component { this.setState({ map: map }); } + onChangeRemoteSourceSDP(e) { + if (e.target.value) { + this.setState({ sdp: e.target.value }); + } + } + inputIsValid() { return !this.state.nameErr && !this.state.sourceErr && @@ -160,11 +177,22 @@ class SinkEdit extends Component { this.setState({useSdp: e.target.checked})} /> - + Source URL this.setState({source: e.target.value, sourceErr: !e.currentTarget.checkValidity()})} disabled={this.state.useSdp ? true : undefined} required/> - SDP + Remote Source SDP + + + + + + SDP