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:
parent
596500b130
commit
5deb6c1927
@ -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.
|
||||
|
2
build.sh
2
build.sh
@ -40,7 +40,7 @@ cd ..
|
||||
|
||||
cd daemon
|
||||
echo "Building aes67-daemon ..."
|
||||
cmake .
|
||||
cmake -DWITH_AVAHI=ON .
|
||||
make
|
||||
cd ..
|
||||
|
||||
|
157
daemon/.clang-format
Normal file
157
daemon/.clang-format
Normal 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
|
||||
...
|
||||
|
@ -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()
|
||||
|
@ -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<a name="ptp-config"></a> ###
|
||||
|
||||
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.
|
||||
|
@ -17,11 +17,10 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#include <experimental/map>
|
||||
#include "browser.hpp"
|
||||
|
||||
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<Config> config) {
|
||||
@ -39,7 +38,8 @@ std::shared_ptr<Browser> Browser::create(
|
||||
std::list<RemoteSource> Browser::get_remote_sources() {
|
||||
std::list<RemoteSource> 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<name_tag>()) {
|
||||
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<id_tag>().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<second_t>(steady_clock::now() - startup).count();
|
||||
duration_cast<second_t>(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<second_t>(steady_clock::now() - startup).count();
|
||||
(*it).second.announce_period = last_seen - (*it).second.last_seen;
|
||||
(*it).second.last_seen = last_seen;
|
||||
duration_cast<second_t>(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<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::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<second_t>(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<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;
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,18 @@
|
||||
#include <chrono>
|
||||
#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 "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<Browser> create(
|
||||
std::shared_ptr<Config> 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<RemoteSource> get_remote_sources();
|
||||
|
||||
protected:
|
||||
// singleton, use create() to build
|
||||
Browser(std::shared_ptr<Config> config)
|
||||
: config_(config){};
|
||||
Browser(std::shared_ptr<Config> 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> config_;
|
||||
std::future<bool> res_;
|
||||
std::atomic_bool running_{false};
|
||||
|
||||
/* 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_;
|
||||
|
||||
SAP sap_{config_->get_sap_mcast_addr()};
|
||||
IGMP igmp_;
|
||||
std::chrono::time_point<std::chrono::steady_clock> startup_;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -65,12 +65,15 @@ std::shared_ptr<Config> 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;
|
||||
|
@ -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<uint8_t, 6>& 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<uint8_t, 6> mac_addr_{0, 0, 0, 0, 0, 0};
|
||||
|
@ -15,5 +15,6 @@
|
||||
"syslog_proto": "none",
|
||||
"syslog_server": "255.255.255.254:1234",
|
||||
"status_file": "./status.json",
|
||||
"mdns_enabled": true,
|
||||
"interface_name": "lo"
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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<SessionManager> session_manager_;
|
||||
|
@ -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<std::string>()));
|
||||
} else if (key == "syslog_server") {
|
||||
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") {
|
||||
/* ignored */
|
||||
} else {
|
||||
|
@ -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 */
|
||||
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 */
|
||||
|
255
daemon/mdns_client.cpp
Normal file
255
daemon/mdns_client.cpp
Normal 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
99
daemon/mdns_client.hpp
Normal 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
177
daemon/rtsp_client.cpp
Normal 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
44
daemon/rtsp_client.hpp
Normal 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
|
@ -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<int>(length); i++) {
|
||||
buffer[i] = std::tolower(buffer[i]);
|
||||
}
|
||||
if (!memcmp(buffer + 8, "application/sdp", 16)) {
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include <boost/foreach.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <experimental/map>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
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:: cannot retrieve SDP from URL " << sink.source;
|
||||
<< "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<uint16_t>(x << 12)) ^
|
||||
(static_cast<uint16_t>(x << 5)) ^
|
||||
(static_cast<uint16_t>(x));
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
size_t SessionManager::process_sap() {
|
||||
size_t sdp_len_sum = 0;
|
||||
// set to contain sources currently announced
|
||||
|
@ -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();
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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");
|
||||
|
34
daemon/utils.cpp
Normal file
34
daemon/utils.cpp
Normal 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
29
daemon/utils.hpp
Normal 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
|
@ -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)
|
||||
|
||||
|
@ -68,7 +68,7 @@ class RemoteSourceEntry extends Component {
|
||||
<td> <label>{this.state.rtp_address}</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.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>
|
||||
</tr>
|
||||
);
|
||||
@ -96,7 +96,7 @@ class RemoteSourceList extends Component {
|
||||
<th>Name</th>
|
||||
<th>RTP Address</th>
|
||||
<th>Port</th>
|
||||
<th>Last seen</th>
|
||||
<th>Seen</th>
|
||||
<th>Period</th>
|
||||
</tr>
|
||||
: <tr>
|
||||
@ -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 }
|
||||
|
@ -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 {
|
||||
<th align="left"> <input type="checkbox" defaultChecked={this.state.useSdp} onChange={e => this.setState({useSdp: e.target.checked})} /> </th>
|
||||
</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>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -38,6 +38,7 @@ const infoCustomStyles = {
|
||||
class SourceInfo extends Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
source: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
sdp: PropTypes.string.isRequired,
|
||||
closeInfo: PropTypes.func.isRequired,
|
||||
@ -72,6 +73,10 @@ class SourceInfo extends Component {
|
||||
<th align="left"> <label>ID</label> </th>
|
||||
<th align="left"> <input value={this.props.id} readOnly/> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="left"> <label>Source</label> </th>
|
||||
<th align="left"> <input value={this.props.source} readOnly/> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="left"> <label>Name</label> </th>
|
||||
<th align="left"> <input value={this.props.name} readOnly/> </th>
|
||||
|
@ -264,6 +264,7 @@ class Sources extends Component {
|
||||
isInfo={this.state.isInfo}
|
||||
id={this.state.source.id.toString()}
|
||||
name={this.state.source.name}
|
||||
source='local'
|
||||
sdp={this.state.sdp} />
|
||||
: undefined }
|
||||
{ this.state.editIsOpen ?
|
||||
|
Loading…
x
Reference in New Issue
Block a user