From 0dbfe78a10754ddc0d2daaa07fce2f53bc46c9c6 Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Thu, 23 Apr 2020 11:45:58 -0700 Subject: [PATCH] - Added support for mDNS/RTSP sources advertisement compatible with Ravenna standard. - mDNS advertisement for all local Sources is implemented by mdns_server.[cpp,hpp] and based on Linux Avahi. - RTSP server implementation supports DESCRIBE method to return SDP of local Sources and supports persistent connection but doesn't provide service updates via UPDATE method. - Modified RTSP client to browse for _ravenna_session subtype of _rtsp._tcp services only. - Modified SAP and mDNS discovery to avoid returning local Sources advertised by the daemon. - Added "rtsp_port" and "node_id" config parameters. - rtsp_port is a read/write parameter that contains the port of the RTSP server. - node_id is a read only parameter that contains the unique daemon identifier used in mDNS and SAP sources announcements. - Modified session manager to check that every Source and Sink created by the user has a unique name. - Modified WebUI to visualize node_id and to visualize and edit rtsp_port parameters in Config tab. - Extended regression test to verify proper behaviour of mDNS/RTSP sources advertisement and discovery. - Modified REST API to browse remote sources to allow browsing of SAP, mDNS and all sources via HTTP GET /api/browse/sources/[all|mdns|sap]. - Amended daemon documentation. --- README.md | 33 +++- daemon/CMakeLists.txt | 2 +- daemon/README.md | 30 +++- daemon/browser.cpp | 37 ++-- daemon/browser.hpp | 5 +- daemon/config.hpp | 3 + daemon/daemon.conf | 1 + daemon/error_code.cpp | 2 + daemon/error_code.hpp | 1 + daemon/http_server.cpp | 5 +- daemon/json.cpp | 7 +- daemon/main.cpp | 15 ++ daemon/mdns_client.cpp | 63 ++++--- daemon/mdns_client.hpp | 16 +- daemon/mdns_server.cpp | 338 +++++++++++++++++++++++++++++++++++ daemon/mdns_server.hpp | 81 +++++++++ daemon/rtsp_client.cpp | 8 +- daemon/rtsp_client.hpp | 10 +- daemon/rtsp_server.cpp | 239 +++++++++++++++++++++++++ daemon/rtsp_server.hpp | 117 ++++++++++++ daemon/sap.cpp | 3 +- daemon/session_manager.cpp | 172 +++++++++--------- daemon/session_manager.hpp | 21 ++- daemon/tests/CMakeLists.txt | 4 + daemon/tests/daemon.conf | 6 +- daemon/tests/daemon_test.cpp | 201 ++++++++++++++++----- daemon/utils.cpp | 60 +++++++ daemon/utils.hpp | 16 +- demo/daemon.conf | 1 + run_demo.sh | 2 +- webui/src/Config.js | 14 ++ webui/src/Services.js | 5 +- 32 files changed, 1290 insertions(+), 228 deletions(-) create mode 100644 daemon/mdns_server.cpp create mode 100644 daemon/mdns_server.hpp create mode 100644 daemon/rtsp_server.cpp create mode 100644 daemon/rtsp_server.hpp diff --git a/README.md b/README.md index 8f79897..6b230e4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ See [https://en.wikipedia.org/wiki/AES67](https://en.wikipedia.org/wiki/AES67) f # Introduction -The daemon is a Linux process that uses the [Merging Technologies ALSA RAVENNA/AES67 Driver](https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master) to handles PTP synchronization and RTP streams and exposes a REST interface for configuration and status monitoring. +The daemon is a Linux process that uses the [Merging Technologies ALSA RAVENNA/AES67 Driver](https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master) to handle PTP synchronization and RTP streams and exposes a REST interface for configuration and status monitoring. The **ALSA AES67 Driver** implements a virtual ALSA audio device that can be configured using _Sources_ and _Sinks_ and it's clocked using the PTP clock. A _Source_ reads audio samples from the ALSA playback device and sends RTP packets to a configured multicast address. @@ -40,15 +40,16 @@ The daemon uses the following open source: This directory contains the AES67 daemon source code. The daemon can be cross-compiled for multiple platforms and implements the following functionalities: +* communication and configuration of the ALSA RAVENNA/AES67 device driver * control and configuration of up to 64 sources and sinks using the ALSA RAVENNA/AES67 driver via netlink * 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* +* HTTP REST API for the daemon control and configuration +* SAP sources discovery and advertisement compatible with AES67 standard +* mDNS sources discovery and advertisement (using Linux Avahi) compatible with Ravenna standard +* RTSP client and server to retrieve or return SDP files via DESCRIBE method +* IGMP handling for SAP and RTP sessions +The directory also contains the daemon regression tests in the [tests](daemon/tests) subdirectory. See the [README](daemon/README.md) file in this directory for additional information about the AES67 daemon configuration and the HTTP REST API. ### [webui](webui) directory ### @@ -85,7 +86,7 @@ This directory contains a the daemon configuration and status files used to run The daemon and the demo have been tested with **Ubuntu 18.04** distro on **ARMv7** and with **Ubuntu 18.04, 19.10 and 20.04** distros on **x86** using: * Linux kernel version >= 4.14.x -* GCC version >= 7.4 / clang >= 6.0.0 (C++17 support required, clang is required to compile on ARMv7) +* GCC version >= 7.4 / clang >= 6.0.0 (C++17 support required) * cmake version >= 3.10.2 * node version >= 8.10.0 * npm version >= 3.5.2 @@ -108,7 +109,20 @@ The script performs the following operations: * build and deploy the WebUI * build the AES67 daemon -## Run the Demo ## +## Run the regression tests ## +To run daemon regression tests install the ALSA RAVENNA/AES67 kernel module with: + + sudo insmod 3rdparty/ravenna-alsa-lkm/driver/MergingRavennaALSA.ko + +setup the kernel parameters with: + + sudo sysctl -w net/ipv4/igmp_max_memberships=66 + +make sure that no instances of the aes67-daemon are running, enter the [tests](daemon/tests) subdirectory and run: + + ./daemon-test + +## Run the demo ## To run a simple demo use the [run\_demo.sh](run_demo.sh) script. See [script notes](#notes). @@ -147,6 +161,7 @@ To run interoperability tests using the [Hasseb audio over Ethernet receiver](ht * open the Hasseb WebUI and do the following: * deselect the "PTP slave only" checkbox to enable PTP master on Hasseb device * select the "Add SDP file manually" checkbox and copy the previous Source SDP into the SDP field + * alternatively to the step above, select in the "Stream name" drop down the daemon Source just added * press the Submit button * return to the daemon WebUI, click on the PTP tab and wait for the "PTP Status" to report "locked" * open a shell on the Linux host and start the playback on the ravenna ALSA device. For example to playback a test sound use: diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index bb1a29b..558acc7 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -13,7 +13,7 @@ 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 rtsp_client.cpp mdns_client.cpp utils.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 mdns_server.cpp rtsp_server.cpp utils.cpp) add_subdirectory(tests) target_link_libraries(aes67-daemon ${Boost_LIBRARIES}) if(WITH_AVAHI) diff --git a/daemon/README.md b/daemon/README.md index aeb931f..90ff888 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -4,13 +4,16 @@ AES67 daemon uses the Merging Technologies device driver (MergingRavennaALSA.ko) The daemon is responsible for: -* communication and configuration of the device driver -* provide an HTTP REST API for the daemon control and configuration +* communication and configuration of the ALSA RAVENNA/AES67 device driver +* control and configuration of up to 64 sources and sinks using the ALSA RAVENNA/AES67 driver via netlink * session handling and SDP parsing and creation -* SAP discovery protocol and SAP browser -* mDNS sources discovery (using Avahi) and SDP transfer via RTSP +* HTTP REST API for the daemon control and configuration +* SAP sources discovery and advertisement compatible with AES67 standard +* mDNS sources discovery and advertisement (using Linux Avahi) compatible with Ravenna standard +* RTSP client and server to retrieve or return SDP files via DESCRIBE method * IGMP handling for SAP and RTP sessions + ## Configuration file ## The daemon uses a JSON file to store the configuration parameters. @@ -130,10 +133,10 @@ In case of failure the server returns a **text/plain** content type with the cat * **Body** [RTP Streams params](#rtp-streams) ### Get all remote RTP Sources ### -* **Description** retrieve all the remote sources collected via SAP -* **URL** /api/browse/sources +* **Description** retrieve all the remote sources collected via SAP and mDNS +* **URL** /api/browse/sources/[all|mdns|sap] * **Method** GET -* **URL Params** none +* **URL Params** all=[all sources], mdns=[mDNS sources only], sap=[sap sources only] * **Body type** application/json * **Body** [RTP Remote Sources params](#rtp-remote-sources) @@ -146,6 +149,7 @@ Example { "interface_name": "lo", "http_port": 8080, + "rtsp_port": 8854, "log_severity": 2, "syslog_proto": "none", "syslog_server": "255.255.255.254:1234", @@ -161,7 +165,8 @@ Example "sap_mcast_addr": "239.255.255.255", "sap_interval": 30, "mac_addr": "01:00:5e:01:00:01", - "ip_addr": "127.0.0.1" + "ip_addr": "127.0.0.1", + "node_id": "AES67 daemon ubuntu-d9aca383" } where: @@ -170,7 +175,10 @@ where: > JSON string specifying the network interface used by the daemon and the driver for both the RTP, PTP, SAP and HTTP traffic. > **http\_port** -> JSON number specifying the HTTP port number used by the embedded web server in the daemon implementing the REST interface. +> JSON number specifying the HTTP port number used by the web server in the daemon implementing the REST interface. + +> **rtsp\_port** +> JSON number specifying the RTSP port number used by the RTSP server in the daemon. > **log\_severity** > JSON integer specifying the process log severity level (0 to 5). @@ -236,6 +244,10 @@ where: > **mdns\_enabled** > JSON boolean specifying whether the mDNS discovery is enabled or disabled. +> **node\_id** +> JSON string specifying the unique node identifier used to identify mDNS, SAP and SDP services announced by the daemon. +> **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the node id at startup time. + ### JSON PTP Config ### Example diff --git a/daemon/browser.cpp b/daemon/browser.cpp index 91b2cde..f276000 100644 --- a/daemon/browser.cpp +++ b/daemon/browser.cpp @@ -18,6 +18,8 @@ // #include + +#include "utils.hpp" #include "browser.hpp" using namespace boost::algorithm; @@ -37,12 +39,16 @@ std::shared_ptr Browser::create( return ptr; } -std::list Browser::get_remote_sources() const { +std::list Browser::get_remote_sources( + const std::string& _source) const { std::list sources_list; std::shared_lock sources_lock(sources_mutex_); // return list of remote sources ordered by name for (const auto& source: sources_.get()) { - sources_list.push_back(source); + if (boost::iequals(source.source, _source) || + boost::iequals("all", _source)) { + sources_list.push_back(source); + } } return sources_list; } @@ -52,7 +58,9 @@ static std::string sdp_get_subject(const std::string& sdp) { std::string line; while (getline(ssstrem, line, '\n')) { if (line.substr(0, 2) == "s=") { - return line.substr(2); + auto subject = line.substr(2); + trim(subject); + return subject; } } return ""; @@ -75,7 +83,7 @@ bool Browser::worker() { if (sap_.receive(is_announce, msg_id_hash, addr, sdp)) { std::stringstream ss; - ss << "sap:" << std::hex << addr << msg_id_hash; + ss << "sap:" << msg_id_hash; std::string id(ss.str()); BOOST_LOG_TRIVIAL(debug) << "browser:: received SAP message for " << id; @@ -86,19 +94,12 @@ bool Browser::worker() { // Source is not in the map if (is_announce) { // annoucement, add new source - RemoteSource source; - source.id = id; - source.sdp = sdp; - source.source = "SAP"; - source.address = ip::address_v4(ntohl(addr)).to_string(); - source.name = sdp_get_subject(sdp); - trim(source.name); - source.last_seen = - duration_cast(steady_clock::now() - startup_).count(); - source.announce_period = 360; //default period - BOOST_LOG_TRIVIAL(info) << "browser:: adding SAP source " << source.id - << " name " << source.name; - sources_.insert(source); + sources_.insert({ + id, "SAP", ip::address_v4(ntohl(addr)).to_string(), + sdp_get_subject(sdp), {}, sdp, + static_cast(duration_cast(steady_clock::now() + - startup_).count()), + 360 }); } } else { // Source is already in the map @@ -156,7 +157,7 @@ bool Browser::worker() { void Browser::on_new_rtsp_source(const std::string& name, const std::string& domain, - const RTSPSSource& s) { + const RtspSource& s) { uint32_t last_seen = duration_cast(steady_clock::now() - startup_).count(); std::unique_lock sources_lock(sources_mutex_); diff --git a/daemon/browser.hpp b/daemon/browser.hpp index 200c29d..6f400a4 100644 --- a/daemon/browser.hpp +++ b/daemon/browser.hpp @@ -62,7 +62,8 @@ class Browser : public MDNSClient { bool init() override; bool terminate() override; - std::list get_remote_sources() const; + std::list get_remote_sources( + const std::string& source = "all") const; protected: // singleton, use create() to build @@ -75,7 +76,7 @@ class Browser : public MDNSClient { virtual void on_new_rtsp_source( const std::string& name, const std::string& domain, - const RTSPSSource& source) override; + const RtspSource& source) override; virtual void on_remove_rtsp_source( const std::string& name, const std::string& domain) override; diff --git a/daemon/config.hpp b/daemon/config.hpp index a88e21d..24d8b7e 100644 --- a/daemon/config.hpp +++ b/daemon/config.hpp @@ -33,6 +33,7 @@ class Config { /* attributes retrieved from config json */ uint16_t get_http_port() const { return http_port_; }; + uint16_t get_rtsp_port() const { return rtsp_port_; }; const std::string get_http_base_dir() const { return http_base_dir_; }; int get_log_severity() const { return log_severity_; }; uint32_t get_playout_delay() const { return playout_delay_; }; @@ -61,6 +62,7 @@ class Config { int get_interface_idx() { return interface_idx_; }; void set_http_port(uint16_t http_port) { http_port_ = http_port; }; + void set_rtsp_port(uint16_t rtsp_port) { rtsp_port_ = rtsp_port; }; void set_http_base_dir(const std::string& http_base_dir) { http_base_dir_ = http_base_dir; }; void set_log_severity(int log_severity) { log_severity_ = log_severity; }; void set_playout_delay(uint32_t playout_delay) { @@ -111,6 +113,7 @@ class Config { private: /* from json */ uint16_t http_port_{8080}; + uint16_t rtsp_port_{8854}; std::string http_base_dir_{"../webui/build"}; int log_severity_{2}; uint32_t playout_delay_{0}; diff --git a/daemon/daemon.conf b/daemon/daemon.conf index 0a056e1..f0258bf 100644 --- a/daemon/daemon.conf +++ b/daemon/daemon.conf @@ -1,5 +1,6 @@ { "http_port": 8080, + "rtsp_port": 8854, "http_base_dir": "../webui/build", "log_severity": 2, "playout_delay": 0, diff --git a/daemon/error_code.cpp b/daemon/error_code.cpp index 9f96662..7ca0ac8 100644 --- a/daemon/error_code.cpp +++ b/daemon/error_code.cpp @@ -93,6 +93,8 @@ std::string DaemonErrCategory::message(int ev) const { return "invalid stream id"; case DaemonErrc::stream_id_in_use: return "stream id is in use"; + case DaemonErrc::stream_name_in_use: + return "stream name is in use"; case DaemonErrc::stream_id_not_in_use: return "stream not in use"; case DaemonErrc::invalid_url: diff --git a/daemon/error_code.hpp b/daemon/error_code.hpp index 484dd39..5749e25 100644 --- a/daemon/error_code.hpp +++ b/daemon/error_code.hpp @@ -51,6 +51,7 @@ enum class DaemonErrc { invalid_url = 43, // daemon invalid URL cannot_retrieve_sdp = 44, // daemon cannot retrieve SDP cannot_parse_sdp = 45, // daemon cannot parse SDP + stream_name_in_use = 46, // daemon source or sink name in use send_invalid_size = 50, // daemon data size too big for buffer send_u2k_failed = 51, // daemon failed to send command to driver send_k2u_failed = 52, // daemon failed to send event response to driver diff --git a/daemon/http_server.cpp b/daemon/http_server.cpp index c62e343..3530b9c 100644 --- a/daemon/http_server.cpp +++ b/daemon/http_server.cpp @@ -281,8 +281,9 @@ bool HttpServer::init() { }); /* get remote sources */ - svr_.Get("/api/browse/sources", [this](const Request& req, Response& res) { - auto const sources = browser_->get_remote_sources(); + svr_.Get("/api/browse/sources/(all|mdns|sap)", + [this](const Request& req, Response& res) { + auto const sources = browser_->get_remote_sources(req.matches[1]); set_headers(res, "application/json"); res.body = remote_sources_to_json(sources); }); diff --git a/daemon/json.cpp b/daemon/json.cpp index eb12d14..4ffceb8 100644 --- a/daemon/json.cpp +++ b/daemon/json.cpp @@ -26,6 +26,7 @@ #include "json.hpp" #include "log.hpp" +#include "utils.hpp" static inline std::string remove_undesired_chars(const std::string& s) { @@ -75,6 +76,7 @@ std::string config_to_json(const Config& config) { std::stringstream ss; ss << "{" << "\n \"http_port\": " << config.get_http_port() + << ",\n \"rtsp_port\": " << config.get_rtsp_port() << ",\n \"http_base_dir\": \"" << config.get_http_base_dir() << "\"" << ",\n \"log_severity\": " << config.get_log_severity() << ",\n \"playout_delay\": " << config.get_playout_delay() @@ -94,6 +96,7 @@ std::string config_to_json(const Config& config) { << ",\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 \"node_id\": \"" << escape_json(get_node_id()) << "\"" << "\n}\n"; return ss.str(); } @@ -265,6 +268,8 @@ Config json_to_config_(std::istream& js, Config& config) { for (auto const& [key, val] : pt) { if (key == "http_port") { config.set_http_port(val.get_value()); + } else if (key == "rtsp_port") { + config.set_rtsp_port(val.get_value()); } else if (key == "http_base_dir") { config.set_http_base_dir(remove_undesired_chars(val.get_value())); } else if (key == "log_severity") { @@ -299,7 +304,7 @@ 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 == "mac_addr" || key == "ip_addr") { + } else if (key == "mac_addr" || key == "ip_addr" || key == "node_id" ) { /* ignored */ } else { std::cerr << "Warning: unkown configuration option " << key diff --git a/daemon/main.cpp b/daemon/main.cpp index 0f7a0ff..2f4ca8f 100644 --- a/daemon/main.cpp +++ b/daemon/main.cpp @@ -24,6 +24,7 @@ #include "config.hpp" #include "driver_manager.hpp" #include "http_server.hpp" +#include "rtsp_server.hpp" #include "log.hpp" #include "session_manager.hpp" #include "interface.hpp" @@ -116,6 +117,12 @@ int main(int argc, char* argv[]) { std::string("SessionManager:: init failed")); } + /* start rtsp server */ + RtspServer rtsp_server(session_manager, config); + if (!rtsp_server.init()) { + throw std::runtime_error(std::string("RtspServer:: init failed")); + } + /* start browser */ auto browser = Browser::create(config); if (browser == nullptr || !browser->init()) { @@ -147,6 +154,8 @@ int main(int argc, char* argv[]) { break; } + rtsp_server.process(); + std::this_thread::sleep_for(std::chrono::seconds(1)); } @@ -165,6 +174,12 @@ int main(int argc, char* argv[]) { std::string("Browser:: terminate failed")); } + /* stop rtsp server */ + if (!rtsp_server.terminate()) { + throw std::runtime_error( + std::string("RtspServer:: terminate failed")); + } + /* stop session manager */ if (!session_manager->terminate()) { throw std::runtime_error( diff --git a/daemon/mdns_client.cpp b/daemon/mdns_client.cpp index 742465f..297be23 100644 --- a/daemon/mdns_client.cpp +++ b/daemon/mdns_client.cpp @@ -44,7 +44,7 @@ void MDNSClient::resolve_callback(AvahiServiceResolver* r, /* 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 " + BOOST_LOG_TRIVIAL(error) << "mdns_client:: (Resolver) failed to resolve " << "service " << name << " of type " << type << " in domain " << domain << " : " << avahi_strerror(avahi_client_errno( @@ -52,12 +52,18 @@ void MDNSClient::resolve_callback(AvahiServiceResolver* r, break; case AVAHI_RESOLVER_FOUND: - BOOST_LOG_TRIVIAL(debug) << "avahi_client:: (Resolver) " + BOOST_LOG_TRIVIAL(debug) << "mdns_client:: (Resolver) " << "service " << name << " of type " << type << " in domain " << domain; char addr[AVAHI_ADDRESS_STR_MAX]; - avahi_address_snprint(addr, sizeof(addr), address); + if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && + (mdns.config_->get_interface_name() == "lo")) { + ::strncpy(addr, mdns.config_->get_ip_addr_str().c_str(), sizeof addr - 1); + addr[sizeof addr - 1] = 0; + } else { + avahi_address_snprint(addr, sizeof(addr), address); + } char info[256]; snprintf(info, sizeof(info), @@ -67,27 +73,30 @@ void MDNSClient::resolve_callback(AvahiServiceResolver* r, "wide_area: %i, " "multicast: %i, " "cached: %i", - host_name, port, addr, !!(flags & AVAHI_LOOKUP_RESULT_LOCAL), + 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_LOG_TRIVIAL(debug) << "mdns_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 */ + /* if not on loopback interface we don't want to receive self announced sessions + * or if on loopback interface we want only local announced sessions */ + if ((!(flags & AVAHI_LOOKUP_RESULT_LOCAL) && + (mdns.config_->get_interface_name() != "lo")) || + ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && + (mdns.config_->get_interface_name() == "lo"))) { 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), + [&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_, + auto res = RtspClient::describe(std::string("/by-name/") + name_, addr_, port_); if (res.first) { mdns.on_new_rtsp_source(name_, domain_, res.second); @@ -115,14 +124,14 @@ void MDNSClient::browse_callback(AvahiServiceBrowser* b, * from the LAN */ switch (event) { case AVAHI_BROWSER_FAILURE: - BOOST_LOG_TRIVIAL(fatal) << "avahi_client:: (Browser) " + BOOST_LOG_TRIVIAL(fatal) << "mdns_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: " + BOOST_LOG_TRIVIAL(info) << "mdns_client:: (Browser) NEW: " << "service " << name << " of type " << type << " in domain " << domain; /* We ignore the returned resolver object. In the callback @@ -134,25 +143,25 @@ void MDNSClient::browse_callback(AvahiServiceBrowser* b, AVAHI_LOOKUP_NO_TXT, resolve_callback, &mdns))) { BOOST_LOG_TRIVIAL(error) - << "avahi_client:: " + << "mdns_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: " + BOOST_LOG_TRIVIAL(info) << "mdns_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"; + BOOST_LOG_TRIVIAL(debug) << "mdns_client:: (Browser) ALL_FOR_NOW"; break; case AVAHI_BROWSER_CACHE_EXHAUSTED: - BOOST_LOG_TRIVIAL(debug) << "avahi_client:: (Browser) CACHE_EXHAUSTED"; + BOOST_LOG_TRIVIAL(debug) << "mdns_client:: (Browser) CACHE_EXHAUSTED"; break; } } @@ -165,7 +174,7 @@ void MDNSClient::client_callback(AvahiClient* client, switch (state) { case AVAHI_CLIENT_FAILURE: - BOOST_LOG_TRIVIAL(fatal) << "avahi_client:: server connection failure: " + BOOST_LOG_TRIVIAL(fatal) << "mdns_client:: server connection failure: " << avahi_strerror(avahi_client_errno(client)); /* TODO reconnect if disconnected */ avahi_threaded_poll_quit(mdns.poll_.get()); @@ -177,11 +186,12 @@ void MDNSClient::client_callback(AvahiClient* client, /* Create the service browser */ mdns.sb_.reset(avahi_service_browser_new(client, mdns.config_->get_interface_idx(), - AVAHI_PROTO_INET, "_rtsp._tcp", nullptr, - {}, browse_callback, &mdns)); + AVAHI_PROTO_INET, + "_ravenna_session._sub._rtsp._tcp", + nullptr, {}, browse_callback, &mdns)); if (mdns.sb_ == nullptr) { BOOST_LOG_TRIVIAL(fatal) - << "avahi_client:: failed to create service browser: " + << "mdns_client:: failed to create service browser: " << avahi_strerror(avahi_client_errno(mdns.client_.get())); avahi_threaded_poll_quit(mdns.poll_.get()); } @@ -204,7 +214,7 @@ bool MDNSClient::init() { poll_.reset(avahi_threaded_poll_new()); if (poll_ == nullptr) { BOOST_LOG_TRIVIAL(fatal) - << "avahi_client:: failed to create threaded poll object"; + << "mdns_client:: failed to create threaded poll object"; return false; } @@ -215,7 +225,7 @@ bool MDNSClient::init() { &error)); if (client_ == nullptr) { BOOST_LOG_TRIVIAL(fatal) - << "avahi_client:: failed to create client: " << avahi_strerror(error); + << "mdns_client:: failed to create client: " << avahi_strerror(error); return false; } @@ -250,11 +260,10 @@ 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"; + BOOST_LOG_TRIVIAL(info) << "mdns_client:: waiting for " + << sources_res_.size() << " RTSP clients"; sources_res_.remove_if([](auto& result) { if (result.valid()) { result.wait(); diff --git a/daemon/mdns_client.hpp b/daemon/mdns_client.hpp index acff811..e7c8640 100644 --- a/daemon/mdns_client.hpp +++ b/daemon/mdns_client.hpp @@ -17,8 +17,8 @@ // along with this program. If not, see . // -#ifndef _AVAHI_CLIENT_HPP_ -#define _AVAHI_CLIENT_HPP_ +#ifndef _MDNS_CLIENT_HPP_ +#define _MDNS_CLIENT_HPP_ #ifdef _USE_AVAHI_ #include @@ -50,7 +50,7 @@ class MDNSClient { protected: virtual void on_new_rtsp_source(const std::string& name, const std::string& domain, - const RTSPSSource& source) = 0; + const RtspSource& source) = 0; virtual void on_remove_rtsp_source(const std::string& name, const std::string& domain) = 0; @@ -59,13 +59,14 @@ class MDNSClient { std::mutex sources_res_mutex_; std::atomic_bool running_{false}; + std::shared_ptr config_; #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 + poll_{nullptr, &avahi_threaded_poll_free}; + std::unique_ptr + client_{nullptr, &avahi_client_free}; std::unique_ptr sb_{nullptr, &avahi_service_browser_free}; @@ -95,7 +96,6 @@ class MDNSClient { AvahiClientState state, void* userdata); - std::shared_ptr config_; #endif }; diff --git a/daemon/mdns_server.cpp b/daemon/mdns_server.cpp new file mode 100644 index 0000000..50b251a --- /dev/null +++ b/daemon/mdns_server.cpp @@ -0,0 +1,338 @@ +// +// mdns_server.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_server.hpp" + +#include + +#include "config.hpp" +#include "interface.hpp" +#include "log.hpp" +#include "utils.hpp" + +#ifdef _USE_AVAHI_ + +struct AvahiLockGuard { + AvahiLockGuard() = delete; + AvahiLockGuard(AvahiThreadedPoll* poll) : poll_(poll) { + if (poll_ != nullptr) { + avahi_threaded_poll_lock(poll_); + } + } + ~AvahiLockGuard() { + if (poll_ != nullptr) { + avahi_threaded_poll_unlock(poll_); + } + } + + private: + AvahiThreadedPoll* poll_{nullptr}; +}; + +void MDNSServer::entry_group_callback(AvahiEntryGroup* g, + AvahiEntryGroupState state, + void* userdata) { + MDNSServer& mdns = *(reinterpret_cast(userdata)); + /* Called whenever the entry group state changes */ + auto it = mdns.groups_.right.find(g); + if (it == mdns.groups_.right.end()) { + BOOST_LOG_TRIVIAL(debug) + << "mdns_server:: cannot find name associated to group, skipping ..."; + return; + } + + switch (state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED: + /* The entry group has been established successfully */ + BOOST_LOG_TRIVIAL(debug) << "mdns_server:: entry group name (" + << it->second << ") established"; + break; + + case AVAHI_ENTRY_GROUP_COLLISION: { + BOOST_LOG_TRIVIAL(warning) + << "mdns_server:: entry group name (" << it->second << ") collision"; + break; + } + + case AVAHI_ENTRY_GROUP_FAILURE: + BOOST_LOG_TRIVIAL(error) << "mdns_server:: entry group name " + << "(" << it->second << ") " + << "failure" + << avahi_strerror(avahi_client_errno( + avahi_entry_group_get_client(g))); + /* Some kind of failure happened while we were registering our services */ + avahi_threaded_poll_quit(mdns.poll_.get()); + break; + + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + break; + } +} + +bool MDNSServer::create_services(AvahiClient* client) { + if (!config_->get_mdns_enabled()) { + return false; + } + + std::unique_ptr group{ + avahi_entry_group_new(client, entry_group_callback, this), + &avahi_entry_group_free}; + if (group == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: create_services() failed avahi_entry_group_new(): " + << avahi_strerror(avahi_client_errno(client)); + return false; + } + + /* register ravenna services, without user defined name */ + int ret = avahi_entry_group_add_service( + group.get(), this->config_->get_interface_idx(), AVAHI_PROTO_INET, {}, + node_id_.c_str(), "_http._tcp", nullptr, nullptr, + this->config_->get_http_port(), nullptr); + if (ret < 0) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: add_service() " << node_id_ + << "failed to add entry _http._tcp" << avahi_strerror(ret); + return false; + } + BOOST_LOG_TRIVIAL(info) << "mdns_server:: adding service _http._tcp for " + << node_id_; + + ret = avahi_entry_group_add_service_subtype( + group.get(), this->config_->get_interface_idx(), AVAHI_PROTO_INET, {}, + node_id_.c_str(), "_http._tcp", nullptr, "_ravenna._sub._http._tcp"); + if (ret < 0) { + BOOST_LOG_TRIVIAL(error) << "mdns_server:: add_service() " << node_id_ + << "failed to add subtype _ravenna._sub._http._tcp" + << avahi_strerror(ret); + return false; + } + + ret = avahi_entry_group_add_service( + group.get(), this->config_->get_interface_idx(), AVAHI_PROTO_INET, {}, + node_id_.c_str(), "_rtsp._tcp", nullptr, nullptr, + this->config_->get_rtsp_port(), nullptr); + if (ret < 0) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: add_service() " << node_id_ + << "failed to add entry _rtsp._tcp" << avahi_strerror(ret); + return false; + } + BOOST_LOG_TRIVIAL(info) << "mdns_server:: adding service _rtsp._tcp for " + << node_id_; + + ret = avahi_entry_group_add_service_subtype( + group.get(), this->config_->get_interface_idx(), AVAHI_PROTO_INET, {}, + node_id_.c_str(), "_rtsp._tcp", nullptr, "_ravenna._sub._rtsp._tcp"); + if (ret < 0) { + BOOST_LOG_TRIVIAL(error) << "mdns_server:: add_service() " << node_id_ + << "failed to add subtype _ravenna._sub._rtsp._tcp" + << avahi_strerror(ret); + return false; + } + + /* Tell the server to register the service */ + ret = avahi_entry_group_commit(group.get()); + if (ret < 0) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: create_services() failed to commit entry group " + << avahi_strerror(ret); + return false; + } + groups_.insert(entry_group_bimap_t::value_type(node_id_, group.release())); + return true; +} + +#endif + +bool MDNSServer::add_service(const std::string& name, const std::string& sdp) { + if (!running_ || !config_->get_mdns_enabled()) { + return false; + } + +#ifdef _USE_AVAHI_ + AvahiLockGuard avahi_lock(this->poll_.get()); + if (avahi_client_get_state(client_.get()) != AVAHI_CLIENT_S_RUNNING) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: add_service() failed client is not running"; + return false; + } + + std::unique_ptr group{ + avahi_entry_group_new(client_.get(), entry_group_callback, this), + &avahi_entry_group_free}; + if (group == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: add_service() failed avahi_entry_group_new(): " + << avahi_strerror(avahi_client_errno(client_.get())); + return false; + } + + std::stringstream ss; + ss << node_id_ << " " << name; + + int ret = avahi_entry_group_add_service( + group.get(), this->config_->get_interface_idx(), AVAHI_PROTO_INET, {}, + ss.str().c_str(), "_rtsp._tcp", nullptr, nullptr, + this->config_->get_rtsp_port(), nullptr); + if (ret < 0) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: add_service() " << ss.str() + << "failed to add service _rtsp._tcp" << avahi_strerror(ret); + return false; + } + + ret = avahi_entry_group_add_service_subtype( + group.get(), this->config_->get_interface_idx(), AVAHI_PROTO_INET, {}, + ss.str().c_str(), "_rtsp._tcp", nullptr, + "_ravenna_session._sub._rtsp._tcp"); + if (ret < 0) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: add_service() " << ss.str() + << "failed to add subtype _ravenna_session._sub._rtsp._tcp" + << avahi_strerror(ret); + return false; + } + + /* Tell the server to register the service */ + ret = avahi_entry_group_commit(group.get()); + if (ret < 0) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: add_service() " << ss.str() + << "failed to commit entry group " << avahi_strerror(ret); + return false; + } + + BOOST_LOG_TRIVIAL(info) << "mdns_server:: adding service for " << ss.str(); + groups_.insert(entry_group_bimap_t::value_type(name, group.release())); +#endif + return true; +} + +bool MDNSServer::remove_service(const std::string& name) { + if (!running_ || !config_->get_mdns_enabled()) { + return false; + } + +#ifdef _USE_AVAHI_ + AvahiLockGuard avahi_lock(poll_.get()); + if (avahi_client_get_state(client_.get()) != AVAHI_CLIENT_S_RUNNING) { + BOOST_LOG_TRIVIAL(error) + << "mdns_server:: remove_sub_service() failed client is not running"; + return false; + } + + auto it = groups_.left.find(name); + if (it == groups_.left.end()) { + return false; + } + BOOST_LOG_TRIVIAL(info) << "mdns_server:: removing service _rtsp._tcp for " + << name; + avahi_entry_group_free(it->second); + groups_.left.erase(name); +#endif + + return true; +} + +#ifdef _USE_AVAHI_ +void MDNSServer::client_callback(AvahiClient* client, + AvahiClientState state, + void* userdata) { + MDNSServer& mdns = *(reinterpret_cast(userdata)); + /* Called whenever the client or server state changes */ + + switch (state) { + case AVAHI_CLIENT_FAILURE: + BOOST_LOG_TRIVIAL(fatal) << "mdns_server:: server connection failure: " + << avahi_strerror(avahi_client_errno(client)); + /* TODO reconnect if disconnected */ + avahi_threaded_poll_quit(mdns.poll_.get()); + break; + + case AVAHI_CLIENT_S_RUNNING: + /* The server has startup successfully and registered its host + * name on the network, so it's time to create our services */ + if (!mdns.create_services(client)) { + avahi_threaded_poll_quit(mdns.poll_.get()); + } + break; + + case AVAHI_CLIENT_S_REGISTERING: + /* The server records are now being established. This + * might be caused by a host name change. We need to wait + * for our own records to register until the host name is + * properly esatblished. */ + + case AVAHI_CLIENT_S_COLLISION: + /* Let's drop our registered services. When the server is back + * in AVAHI_SERVER_RUNNING state we will register them + * again with the new host name. */ + /* TODO */ + break; + + case AVAHI_CLIENT_CONNECTING: + break; + } +} +#endif + +bool MDNSServer::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) + << "mdns_server:: 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) + << "mdns_server:: failed to create client: " << avahi_strerror(error); + return false; + } + + (void)avahi_threaded_poll_start(poll_.get()); +#endif + running_ = true; + return true; +} + +bool MDNSServer::terminate() { + if (running_) { + running_ = false; +#ifdef _USE_AVAHI_ + /* remove base services */ + groups_.left.erase(node_id_); + avahi_threaded_poll_stop(poll_.get()); +#endif + } + return true; +} diff --git a/daemon/mdns_server.hpp b/daemon/mdns_server.hpp new file mode 100644 index 0000000..1c8139e --- /dev/null +++ b/daemon/mdns_server.hpp @@ -0,0 +1,81 @@ +// +// mdns_server.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 _MDNS_SERVER_HPP_ +#define _MDNS_SERVER_HPP_ + +#ifdef _USE_AVAHI_ +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include + +#include "config.hpp" +#include "utils.hpp" + +class MDNSServer { + public: + MDNSServer(std::shared_ptr config) : config_(config){}; + MDNSServer() = delete; + MDNSServer(const MDNSServer&) = delete; + MDNSServer& operator=(const MDNSServer&) = delete; + virtual ~MDNSServer() { terminate(); }; + + virtual bool init(); + virtual bool terminate(); + + bool add_service(const std::string& name, const std::string& sdp); + bool remove_service(const std::string& name); + + protected: + std::atomic_bool running_{false}; + std::shared_ptr config_; + std::string node_id_{get_node_id()}; + +#ifdef _USE_AVAHI_ + using entry_group_bimap_t = + boost::bimap; + + entry_group_bimap_t groups_; + + /* order is important here */ + std::unique_ptr poll_{ + nullptr, &avahi_threaded_poll_free}; + std::unique_ptr client_{ + nullptr, &avahi_client_free}; + + static void entry_group_callback(AvahiEntryGroup* g, + AvahiEntryGroupState state, + void* userdata); + static void client_callback(AvahiClient* c, + AvahiClientState state, + void* userdata); + + bool create_services(AvahiClient* client); +#endif +}; + +#endif diff --git a/daemon/rtsp_client.cpp b/daemon/rtsp_client.cpp index 409e247..a0779bb 100644 --- a/daemon/rtsp_client.cpp +++ b/daemon/rtsp_client.cpp @@ -83,10 +83,10 @@ RtspResponse read_response(tcp::iostream& s, uint16_t max_length) { return res; } -std::pair RTSPClient::describe(const std::string& path, +std::pair RtspClient::describe(const std::string& path, const std::string& address, const std::string& port) { - RTSPSSource rs; + RtspSource rs; bool success{false}; try { tcp::iostream s; @@ -155,8 +155,8 @@ std::pair RTSPClient::describe(const std::string& path, 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(); + res.body.length()); + /*<< std::hex << ip::address_v4::from_string(address.c_str()).to_ulong();*/ rs.id = ss.str(); rs.source = "mDNS"; diff --git a/daemon/rtsp_client.hpp b/daemon/rtsp_client.hpp index 60b2561..473df1e 100644 --- a/daemon/rtsp_client.hpp +++ b/daemon/rtsp_client.hpp @@ -17,23 +17,23 @@ // along with this program. If not, see . // -#ifndef _RTSP_HPP_ -#define _RTSP_HPP_ +#ifndef _RTSP_CLIENT_HPP_ +#define _RTSP_CLIENT_HPP_ -struct RTSPSSource { +struct RtspSource { std::string id; std::string source; std::string address; std::string sdp; }; -class RTSPClient { +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( + static std::pair describe( const std::string& path, const std::string& address, const std::string& port = dft_port); diff --git a/daemon/rtsp_server.cpp b/daemon/rtsp_server.cpp new file mode 100644 index 0000000..5048d2c --- /dev/null +++ b/daemon/rtsp_server.cpp @@ -0,0 +1,239 @@ +// rtsp_server.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 "rtsp_server.hpp" + +#include "utils.hpp" + +using boost::asio::ip::tcp; + +void RtspServer::worker() { + io_service_.run(); +} + +void RtspServer::process() { + /* cleanup of expired sessions */ + std::lock_guard lock(mutex_); + for (unsigned int i = 0; i < sessions_.size(); i++) { + if (duration_cast(steady_clock::now() - sessions_start_point_[i]) + .count() > RtspSession::session_tout_secs) { + auto session = sessions_[i].lock(); + if (session != nullptr) { + session->stop(); + } + } + } +} + +void RtspServer::accept() { + acceptor_.async_accept(socket_, [this](boost::system::error_code ec) { + if (!ec) { + std::lock_guard lock(mutex_); + /* check for free sessions */ + unsigned int i = 0; + for (; i < sessions_.size(); i++) { + if (sessions_[i].use_count() == 0) { + auto session = std::make_shared(session_manager_, + std::move(socket_)); + + sessions_[i] = session; + sessions_start_point_[i] = steady_clock::now(); + session->start(); + break; + } + } + + if (i == sessions_.size()) { + BOOST_LOG_TRIVIAL(warning) + << "rtsp_server:: too many clients connected, " + << socket_.remote_endpoint() << " closing..."; + socket_.close(); + } + } + accept(); + }); +} + +bool RtspSession::process_request() { + /* + DESCRIBE rtsp://127.0.0.1:8080/by-name/test RTSP/1.0 + CSeq: 312 + User-Agent: pippo + Accept: application/sdp + + */ + data_[length_] = 0; + std::stringstream sstream(data_); + /* read the request */ + if (!getline(sstream, request_, '\n')) { + return false; + } + consumed_ = request_.length() + 1; + boost::trim(request_); + std::vector fields; + split(fields, request_, boost::is_any_of(" ")); + if (fields.size() < 3) { + return false; + } + /* read the header */ + bool is_end{false}; + std::string header; + while (getline(sstream, header, '\n')) { + consumed_ += header.length() + 1; + if (header == "" || header == "\r") { + is_end = true; + break; + } + boost::to_lower(header); + boost::trim(header); + if (header.rfind("cseq:", 0) != std::string::npos) { + try { + cseq_ = stoi(header.substr(5)); + } catch (...) { + break; + } + } + } + + if (!is_end) { + return false; + } + + if (cseq_ < 0) { + BOOST_LOG_TRIVIAL(error) << "rtsp_server:: CSeq not specified from " + << socket_.remote_endpoint(); + send_error(400, "Bad Request"); + } else if (fields[2].substr(0, 5) != "RTSP/") { + BOOST_LOG_TRIVIAL(error) + << "rtsp_server:: no RTSP specified from " << socket_.remote_endpoint(); + send_error(400, "Bad Request"); + } else if (fields[0] != "DESCRIBE") { + send_error(405, "Method Not Allowed"); + } /*else if (fields[1].substr(0, 7) != "rtsp://") { + BOOST_LOG_TRIVIAL(error) + << "rtsp_server:: no rtsp protocol from " << socket_.remote_endpoint(); + send_error(404, "Not supported"); + } */ + else { + boost::trim(fields[1]); + build_response(fields[1]); + } + return true; +} + +void RtspSession::build_response(const std::string& url) { + auto const [ok, protocol, host, port, path] = parse_url(url); + if (!ok) { + BOOST_LOG_TRIVIAL(error) << "rtsp_server:: cannot parse URL " << url + << " from " << socket_.remote_endpoint(); + send_error(400, "Bad Request"); + return; + } + + auto base_path = std::string("/by-name/") + get_node_id() + " "; + uint8_t id = SessionManager::stream_id_max + 1; + if (path.rfind(base_path) != std::string::npos) { + /* extract the source name from path and retrive the id */ + id = session_manager_->get_source_id(path.substr(base_path.length())); + } else if (path.rfind("/by-id/") != std::string::npos) { + try { + id = stoi(path.substr(7)); + } catch (...) { + } + } + if (id != (SessionManager::stream_id_max + 1)) { + std::string sdp; + if (!session_manager_->get_source_sdp(id, sdp)) { + std::stringstream ss; + ss << "RTSP/1.0 200 OK\r\n" + << "CSeq: " << cseq_ << "\r\n" + << "Content-Length: " << sdp.length() << "\r\n" + << "Content-Type: application/sdp\r\n" + << "\r\n" + << sdp; + BOOST_LOG_TRIVIAL(info) + << "rtsp_server:: " << request_ << " response 200 to " + << socket_.remote_endpoint(); + send_response(ss.str()); + return; + } + } + send_error(404, "Not found"); +} + +void RtspSession::read_request() { + auto self(shared_from_this()); + if (length_ == max_length) { + /* request cannot be consumed and we execeed max length */ + stop(); + } else { + socket_.async_read_some( + boost::asio::buffer(data_ + length_, max_length - length_), + [this, self](boost::system::error_code ec, std::size_t length) { + if (!ec) { + BOOST_LOG_TRIVIAL(debug) << "rtsp_server:: received " << length + << " from " << socket_.remote_endpoint(); + length_ += length; + while (length_ && process_request()) { + /* step to the next request */ + std::memmove(data_, data_ + consumed_, length_ - consumed_); + length_ -= consumed_; + cseq_ = -1; + } + /* read more data */ + read_request(); + } + }); + } +} + +void RtspSession::send_error(int status_code, const std::string& description) { + BOOST_LOG_TRIVIAL(error) << "rtsp_server:: " << request_ << " response " + << status_code << " to " + << socket_.remote_endpoint(); + std::stringstream ss; + ss << "RTSP/1.0 " << status_code << " " << description << "\r\n"; + if (cseq_ >= 0) { + ss << "CSeq: " << cseq_ << "\r\n"; + } + ss << "\n\r"; + send_response(ss.str()); +} + +void RtspSession::send_response(const std::string& response) { + auto self(shared_from_this()); + boost::asio::async_write( + socket_, boost::asio::buffer(response.c_str(), response.length()), + [self](boost::system::error_code ec, std::size_t /*length*/) { + if (!ec) { + // we accept multiple requests within timeout + // stop(); + } + }); +} + +void RtspSession::start() { + BOOST_LOG_TRIVIAL(debug) << "rtsp_server:: starting session with " + << socket_.remote_endpoint(); + read_request(); +} + +void RtspSession::stop() { + BOOST_LOG_TRIVIAL(debug) << "rtsp_server:: stopping session with " + << socket_.remote_endpoint(); + socket_.close(); +} diff --git a/daemon/rtsp_server.hpp b/daemon/rtsp_server.hpp new file mode 100644 index 0000000..bd33d9d --- /dev/null +++ b/daemon/rtsp_server.hpp @@ -0,0 +1,117 @@ +// rtsp_server.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 . + +#include +#include +#include +#include +#include +#include + +#include "session_manager.hpp" + +#ifndef _RTSP_SERVER_HPP_ +#define _RTSP_SERVER_HPP_ + +using namespace std::chrono; +using boost::asio::deadline_timer; +using boost::asio::ip::tcp; +using second_t = duration >; + +class RtspSession : public std::enable_shared_from_this { + public: + constexpr static uint16_t max_length = 4096; // byte + constexpr static uint16_t session_tout_secs = 10; // sec + + RtspSession(std::shared_ptr session_manager, + tcp::socket socket) + : session_manager_(session_manager), + socket_(std::move(socket)), + length_{0}, + cseq_{-1}, + consumed_{0} {} + + virtual ~RtspSession() { + BOOST_LOG_TRIVIAL(debug) << "rtsp_server:: session end"; + } + + void start(); + void stop(); + + private: + bool process_request(); + void build_response(const std::string& url); + void read_request(); + void send_error(int status_code, const std::string& description); + void send_response(const std::string& response); + + std::shared_ptr session_manager_; + tcp::socket socket_; + char data_[max_length + 1]; + std::string request_; + size_t length_{0}; + int32_t cseq_{-1}; + size_t consumed_{0}; +}; + +class RtspServer { + public: + RtspServer() = delete; + RtspServer(std::shared_ptr session_manager, + std::shared_ptr config) + : session_manager_(session_manager), + config_(config), + sessions_(SessionManager::stream_id_max), + sessions_start_point_(SessionManager::stream_id_max), + acceptor_(io_service_, + tcp::endpoint(boost::asio::ip::address::from_string( + config_->get_ip_addr_str()), + config_->get_rtsp_port())), + socket_(io_service_) {} + + bool init() { + accept(); + /* start rtsp server on a separate thread */ + res_ = std::async(std::launch::async, &RtspServer::worker, this); + return true; + } + + bool terminate() { + BOOST_LOG_TRIVIAL(info) << "rtsp_server: stopping ... "; + io_service_.stop(); + res_.get(); + return true; + } + + void process(); + + private: + void worker(); + void accept(); + + std::mutex mutex_; + boost::asio::io_service io_service_; + std::shared_ptr session_manager_; + std::shared_ptr config_; + std::vector > sessions_; + std::vector > sessions_start_point_; + tcp::acceptor acceptor_; + tcp::socket socket_{io_service_}; + std::future res_; +}; + +#endif diff --git a/daemon/sap.cpp b/daemon/sap.cpp index 89203f8..b886191 100644 --- a/daemon/sap.cpp +++ b/daemon/sap.cpp @@ -45,7 +45,8 @@ bool SAP::set_multicast_interface(const std::string& interface_ip) { << "sap::outbound_interface option " << ec.message(); return false; } - ip::multicast::enable_loopback el_option(true); + /* we don't want receive self announced sessions */ + ip::multicast::enable_loopback el_option(false); socket_.set_option(el_option, ec); if (ec) { BOOST_LOG_TRIVIAL(error) << "sap::enable_loopback option " << ec.message(); diff --git a/daemon/session_manager.cpp b/daemon/session_manager.cpp index b452814..054e83a 100644 --- a/daemon/session_manager.cpp +++ b/daemon/session_manager.cpp @@ -60,46 +60,6 @@ static uint8_t get_codec_word_lenght(const std::string& codec) { return 0; } -static std::tuple -parse_url(const std::string& _url) { - std::string url = httplib::detail::decode_url(_url); - size_t protocol_sep_pos = url.find_first_of("://"); - if (protocol_sep_pos == std::string::npos) { - /* no protocol, invalid URL */ - return std::make_tuple(false, "", "", "", ""); - } - - std::string port, host, path("/"); - std::string protocol = url.substr(0, protocol_sep_pos); - std::string url_new = url.substr(protocol_sep_pos + 3); - size_t path_sep_pos = url_new.find_first_of("/"); - size_t port_sep_pos = url_new.find_first_of(":"); - if (port_sep_pos != std::string::npos) { - /* port specified */ - if (path_sep_pos != std::string::npos) { - /* path specified */ - port = url_new.substr(port_sep_pos + 1, path_sep_pos - port_sep_pos - 1); - path = url_new.substr(path_sep_pos); - } else { - /* path not specified */ - port = url_new.substr(port_sep_pos + 1); - } - host = url_new.substr(0, port_sep_pos); - } else if (path_sep_pos != std::string::npos) { - /* port not specified, path specified */ - host = url_new.substr(0, path_sep_pos); - path = url_new.substr(path_sep_pos); - } else { - /* port and path not specified */ - host = url_new; - } - return std::make_tuple(host.length() > 0, protocol, host, port, path); -} - bool SessionManager::parse_sdp(const std::string sdp, StreamInfo& info) const { /* v=0 @@ -362,37 +322,35 @@ std::list SessionManager::get_sources() const { StreamSource SessionManager::get_source_(uint8_t id, const StreamInfo& info) const { - StreamSource source; - source.id = id; - source.enabled = info.enabled; - source.name = info.stream.m_cName; - source.io = info.io; - source.max_samples_per_packet = info.stream.m_ui32MaxSamplesPerPacket; - source.codec = info.stream.m_cCodec; - source.ttl = info.stream.m_byTTL; - source.payload_type = info.stream.m_byPayloadType; - source.dscp = info.stream.m_ucDSCP; - source.refclk_ptp_traceable = info.refclk_ptp_traceable; - for (auto i = 0; i < info.stream.m_byNbOfChannels; i++) { - source.map.push_back(info.stream.m_aui32Routing[i]); - } - return source; + return { + id, + info.enabled, + info.stream.m_cName, + info.io, + info.stream.m_ui32MaxSamplesPerPacket, + info.stream.m_cCodec, + info.stream.m_byTTL, + info.stream.m_byPayloadType, + info.stream.m_ucDSCP, + info.refclk_ptp_traceable, + { info.stream.m_aui32Routing, info.stream.m_aui32Routing + + info.stream.m_byNbOfChannels } + }; } StreamSink SessionManager::get_sink_(uint8_t id, const StreamInfo& info) const { - StreamSink sink; - sink.id = id; - sink.name = info.stream.m_cName; - sink.io = info.io; - sink.use_sdp = info.sink_use_sdp; - sink.sdp = info.sink_sdp; - sink.source = info.sink_source; - sink.delay = info.stream.m_ui32PlayOutDelay; - sink.ignore_refclk_gmid = info.ignore_refclk_gmid; - for (auto i = 0; i < info.stream.m_byNbOfChannels; i++) { - sink.map.push_back(info.stream.m_aui32Routing[i]); - } - return sink; + return { + id, + info.stream.m_cName, + info.io, + info.sink_use_sdp, + info.sink_source, + info.sink_sdp, + info.stream.m_ui32PlayOutDelay, + info.ignore_refclk_gmid, + { info.stream.m_aui32Routing, info.stream.m_aui32Routing + + info.stream.m_byNbOfChannels } + }; } bool SessionManager::load_status() { @@ -455,6 +413,26 @@ static std::array get_mcast_mac_addr(uint32_t mcast_ip) { static_cast(mcast_ip) }; } +uint8_t SessionManager::get_source_id(const std::string& name) const { + const auto it = source_names_.find(name); + return it != source_names_.end() ? it->second : stream_id_max; +} + +void SessionManager::on_add_source(const StreamSource& source, + const StreamInfo& info) { + igmp_.join(config_->get_ip_addr_str(), + ip::address_v4(info.stream.m_ui32DestIP).to_string()); + mdns_.add_service(source.name, get_source_sdp_(source.id, info)); + source_names_[source.name] = source.id; +} + +void SessionManager::on_remove_source(const StreamInfo& info) { + igmp_.leave(config_->get_ip_addr_str(), + ip::address_v4(info.stream.m_ui32DestIP).to_string()); + mdns_.remove_service(info.stream.m_cName); + source_names_.erase(info.stream.m_cName); +} + std::error_code SessionManager::add_source(const StreamSource& source) { if (source.id > stream_id_max) { BOOST_LOG_TRIVIAL(error) << "session_manager:: source id " @@ -504,9 +482,12 @@ std::error_code SessionManager::add_source(const StreamSource& source) { // remove previous stream if enabled if ((*it).second.enabled) { (void)driver_->remove_rtp_stream((*it).second.handle); - igmp_.leave(config_->get_ip_addr_str(), - ip::address_v4((*it).second.stream.m_ui32DestIP).to_string()); + on_remove_source((*it).second); } + } else if (source_names_.find(source.name) != source_names_.end()) { + BOOST_LOG_TRIVIAL(error) << "session_manager:: source name " + << source.name << " is in use"; + return DaemonErrc::stream_name_in_use; } std::error_code ret; @@ -519,8 +500,7 @@ std::error_code SessionManager::add_source(const StreamSource& source) { } return ret; } - igmp_.join(config_->get_ip_addr_str(), - ip::address_v4(info.stream.m_ui32DestIP).to_string()); + on_add_source(source, info); } // update source map @@ -557,7 +537,7 @@ std::string SessionManager::get_source_sdp_(uint32_t id, ss << "v=0\n" << "o=- " << static_cast(id) << " 0 IN IP4 " << ip::address_v4(info.stream.m_ui32SrcIP).to_string() << "\n" - << "s=" << info.stream.m_cName << "\n" + << "s=" << get_node_id() << " " << info.stream.m_cName << "\n" << "c=IN IP4 " << ip::address_v4(info.stream.m_ui32DestIP).to_string() << "/" << static_cast(info.stream.m_byTTL) << "\n" << "t=0 0\n" @@ -619,8 +599,7 @@ std::error_code SessionManager::remove_source(uint32_t id) { if (info.enabled) { ret = driver_->remove_rtp_stream(info.handle); if (!ret) { - igmp_.leave(config_->get_ip_addr_str(), - ip::address_v4(info.stream.m_ui32DestIP).to_string()); + on_remove_source(info); } } if (!ret) { @@ -630,6 +609,24 @@ std::error_code SessionManager::remove_source(uint32_t id) { return ret; } +uint8_t SessionManager::get_sink_id(const std::string& name) const { + const auto it = sink_names_.find(name); + return it != sink_names_.end() ? it->second : (stream_id_max + 1); +} + +void SessionManager::on_add_sink(const StreamSink& sink, + const StreamInfo& info) { + igmp_.join(config_->get_ip_addr_str(), + ip::address_v4(info.stream.m_ui32DestIP).to_string()); + sink_names_[sink.name] = sink.id; +} + +void SessionManager::on_remove_sink(const StreamInfo& info) { + igmp_.leave(config_->get_ip_addr_str(), + ip::address_v4(info.stream.m_ui32DestIP).to_string()); + sink_names_.erase(info.stream.m_cName); +} + std::error_code SessionManager::add_sink(const StreamSink& sink) { if (sink.id > stream_id_max) { BOOST_LOG_TRIVIAL(error) << "session_manager:: sink id " @@ -678,7 +675,7 @@ std::error_code SessionManager::add_sink(const StreamSink& sink) { } sdp = std::move(res->body); } else if (boost::iequals(protocol, "rtsp")) { - auto res = RTSPClient::describe(path, host, port); + auto res = RtspClient::describe(path, host, port); if (!res.first) { BOOST_LOG_TRIVIAL(error) << "session_manager:: cannot retrieve SDP from URL " << sink.source; @@ -742,8 +739,11 @@ std::error_code SessionManager::add_sink(const StreamSink& sink) { << std::to_string(sink.id) << " is in use, updating"; // remove previous stream (void)driver_->remove_rtp_stream((*it).second.handle); - igmp_.leave(config_->get_ip_addr_str(), - ip::address_v4((*it).second.stream.m_ui32DestIP).to_string()); + on_remove_sink((*it).second); + } else if (sink_names_.find(sink.name) != sink_names_.end()) { + BOOST_LOG_TRIVIAL(error) << "session_manager:: sink name " + << sink.name << " is in use"; + return DaemonErrc::stream_name_in_use; } auto ret = driver_->add_rtp_stream(info.stream, info.handle); @@ -755,9 +755,7 @@ std::error_code SessionManager::add_sink(const StreamSink& sink) { return ret; } - igmp_.join(config_->get_ip_addr_str(), - ip::address_v4(info.stream.m_ui32DestIP).to_string()); - + on_add_sink(sink, info); // update sinks map sinks_[sink.id] = info; BOOST_LOG_TRIVIAL(info) << "session_manager:: added sink " @@ -785,6 +783,7 @@ std::error_code SessionManager::remove_sink(uint32_t id) { if (!ret) { igmp_.leave(config_->get_ip_addr_str(), ip::address_v4(info.stream.m_ui32DestIP).to_string()); + on_remove_sink(info); sinks_.erase(id); } @@ -937,16 +936,11 @@ bool SessionManager::worker() { // return false; } else { char ptp_clock_id[24]; + uint8_t* pui64GMID = reinterpret_cast(&ptp_status.ui64GMID); snprintf(ptp_clock_id, sizeof(ptp_clock_id), "%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X", - (reinterpret_cast(&ptp_status.ui64GMID)[0]), - (reinterpret_cast(&ptp_status.ui64GMID)[1]), - (reinterpret_cast(&ptp_status.ui64GMID)[2]), - (reinterpret_cast(&ptp_status.ui64GMID)[3]), - (reinterpret_cast(&ptp_status.ui64GMID)[4]), - (reinterpret_cast(&ptp_status.ui64GMID)[5]), - (reinterpret_cast(&ptp_status.ui64GMID)[6]), - (reinterpret_cast(&ptp_status.ui64GMID)[7])); + pui64GMID[0], pui64GMID[1], pui64GMID[2], pui64GMID[3], + pui64GMID[4], pui64GMID[5], pui64GMID[6], pui64GMID[7]); // update PTP clock status std::unique_lock ptp_lock(ptp_mutex_); // update status diff --git a/daemon/session_manager.hpp b/daemon/session_manager.hpp index 5a1c1b6..020d905 100644 --- a/daemon/session_manager.hpp +++ b/daemon/session_manager.hpp @@ -30,19 +30,20 @@ #include "driver_manager.hpp" #include "igmp.hpp" #include "sap.hpp" +#include "mdns_server.hpp" struct StreamSource { uint8_t id{0}; bool enabled{false}; std::string name; std::string io; - std::vector map; uint32_t max_samples_per_packet{0}; std::string codec; uint8_t ttl{0}; uint8_t payload_type{0}; uint8_t dscp{0}; bool refclk_ptp_traceable{false}; + std::vector map; }; struct StreamSink { @@ -106,6 +107,10 @@ class SessionManager { // session manager interface bool init() { if (!running_) { + /* init mDNS server */ + if (config_->get_mdns_enabled() && !mdns_.init()) { + return false; + } running_ = true; res_ = std::async(std::launch::async, &SessionManager::worker, this); } @@ -122,6 +127,9 @@ class SessionManager { for (auto sink : get_sinks()) { remove_sink(sink.id); } + if (config_->get_mdns_enabled()) { + mdns_.terminate(); + } return ret; } return true; @@ -132,12 +140,14 @@ class SessionManager { std::list get_sources() const; std::error_code get_source_sdp(uint32_t id, std::string& sdp) const; std::error_code remove_source(uint32_t id); + uint8_t get_source_id(const std::string& name) const; std::error_code add_sink(const StreamSink& sink); std::error_code get_sink(uint8_t id, StreamSink& sink) const; std::list get_sinks() const; std::error_code get_sink_status(uint32_t id, SinkStreamStatus& status) const; std::error_code remove_sink(uint32_t id); + uint8_t get_sink_id(const std::string& name) const; std::error_code set_ptp_config(const PTPConfig& config); void get_ptp_config(PTPConfig& config) const; @@ -152,6 +162,12 @@ class SessionManager { constexpr static const char ptp_primary_mcast_addr[] = "224.0.1.129"; constexpr static const char ptp_pdelay_mcast_addr[] = "224.0.1.107"; + void on_add_source(const StreamSource& source, const StreamInfo& info); + void on_remove_source(const StreamInfo& info); + + void on_add_sink(const StreamSink& sink, const StreamInfo& info); + void on_remove_sink(const StreamInfo& info); + std::string get_removed_source_sdp_(uint32_t id, uint32_t src_addr) const; std::string get_source_sdp_(uint32_t id, const StreamInfo& info) const; StreamSource get_source_(uint8_t id, const StreamInfo& info) const; @@ -174,10 +190,12 @@ class SessionManager { /* current sources */ std::map sources_; + std::map source_names_; mutable std::shared_mutex sources_mutex_; /* current sinks */ std::map sinks_; + std::map sink_names_; mutable std::shared_mutex sinks_mutex_; /* current announced sources */ @@ -192,6 +210,7 @@ class SessionManager { PTPStatus ptp_status_; mutable std::shared_mutex ptp_mutex_; + MDNSServer mdns_{config_}; SAP sap_{config_->get_sap_mcast_addr()}; IGMP igmp_; }; diff --git a/daemon/tests/CMakeLists.txt b/daemon/tests/CMakeLists.txt index 68aacc4..1f3392a 100644 --- a/daemon/tests/CMakeLists.txt +++ b/daemon/tests/CMakeLists.txt @@ -8,3 +8,7 @@ include_directories(aes67-daemon ${CPP_HTTPLIB} ${Boost_INCLUDE_DIR}) add_executable(daemon-test daemon_test.cpp) target_link_libraries(daemon-test ${Boost_LIBRARIES}) add_test(daemon-test daemon-test) +if(WITH_AVAHI) + MESSAGE(STATUS "WITH_AVAHI") + add_definitions(-D_USE_AVAHI_) +endif() diff --git a/daemon/tests/daemon.conf b/daemon/tests/daemon.conf index 2ba1838..886de29 100644 --- a/daemon/tests/daemon.conf +++ b/daemon/tests/daemon.conf @@ -1,5 +1,6 @@ { "http_port": 9999, + "rtsp_port": 9997, "http_base_dir": ".", "log_severity": 5, "playout_delay": 0, @@ -16,7 +17,8 @@ "syslog_server": "255.255.255.254:1234", "status_file": "", "interface_name": "lo", - "mdns_enabled": false, + "mdns_enabled": true, "mac_addr": "00:00:00:00:00:00", - "ip_addr": "127.0.0.1" + "ip_addr": "127.0.0.1", + "node_id": "AES67 daemon d9aca383" } diff --git a/daemon/tests/daemon_test.cpp b/daemon/tests/daemon_test.cpp index e5361e8..23bb109 100644 --- a/daemon/tests/daemon_test.cpp +++ b/daemon/tests/daemon_test.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include @@ -42,6 +43,7 @@ constexpr static const char g_sap_address[] = "224.2.127.254"; constexpr static uint16_t g_sap_port = 9875; constexpr static uint16_t g_udp_size = 1024; constexpr static uint16_t g_sap_header_len = 24; +constexpr static uint16_t g_stream_id_max = 64; using namespace boost::process; using namespace boost::asio::ip; @@ -152,6 +154,8 @@ struct Client { "refclk_ptp_traceable": false } )"; + + boost::replace_first(json, "ALSA", "ALSA " + std::to_string(id)); std::string url = std::string("/api/source/") + std::to_string(id); auto res = cli_.Put(url.c_str(), json, "application/json"); BOOST_REQUIRE_MESSAGE(res != nullptr, "server returned response"); @@ -214,6 +218,7 @@ struct Client { } )"; + boost::replace_first(json, "ALSA", "ALSA " + std::to_string(id)); std::string url = std::string("/api/sink/") + std::to_string(id); auto res = cli_.Put(url.c_str(), json, "application/json"); BOOST_REQUIRE_MESSAGE(res != nullptr, "server returned response"); @@ -223,7 +228,6 @@ struct Client { bool add_sink_url(int id) { std::string json1 = R"( { - "name": "ALSA", "io": "Audio Device", "use_sdp": false, "sdp": "", @@ -233,6 +237,7 @@ struct Client { )"; std::string json = json1 + + std::string("\"name\": \"ALSA " + std::to_string(id) + "\",\n") + std::string("\"source\": \"http://") + g_daemon_address + ":" + std::to_string(g_daemon_port) + std::string("/api/source/sdp/") + std::to_string(id) + "\"\n}"; @@ -270,7 +275,7 @@ struct Client { void sap_wait_all_deletions() { char data[g_udp_size]; std::set ids; - while (ids.size() < 64) { + while (ids.size() < g_stream_id_max) { auto len = socket_.receive(boost::asio::buffer(data, g_udp_size)); if (len <= g_sap_header_len) { continue; @@ -280,7 +285,7 @@ struct Client { //o=- 56 0 IN IP4 127.0.0.1 ids.insert(std::atoi(sap_sdp_.c_str() + 3)); BOOST_TEST_MESSAGE("waiting deletion for " + - std::to_string(64 - ids.size()) + " sources"); + std::to_string(g_stream_id_max - ids.size()) + " sources"); } } } @@ -302,8 +307,15 @@ struct Client { return true; } - std::pair get_remote_sources() { - std::string url = std::string("/api/browse/sources"); + std::pair get_remote_sap_sources() { + std::string url = std::string("/api/browse/sources/sap"); + auto res = cli_.Get(url.c_str()); + BOOST_REQUIRE_MESSAGE(res != nullptr, "server returned response"); + return std::make_pair(res->status == 200, res->body); + } + + std::pair get_remote_mdns_sources() { + std::string url = std::string("/api/browse/sources/mdns"); auto res = cli_.Get(url.c_str()); BOOST_REQUIRE_MESSAGE(res != nullptr, "server returned response"); return std::make_pair(res->status == 200, res->body); @@ -413,13 +425,15 @@ BOOST_AUTO_TEST_CASE(set_ptp_config) { BOOST_AUTO_TEST_CASE(add_invalid_source) { Client cli; - BOOST_REQUIRE_MESSAGE(!cli.add_source(64), "not added source 64"); + BOOST_REQUIRE_MESSAGE(!cli.add_source(g_stream_id_max), + "not added source " + std::to_string(g_stream_id_max)); BOOST_REQUIRE_MESSAGE(!cli.add_source(-1), "not added source -1"); } BOOST_AUTO_TEST_CASE(remove_invalid_source) { Client cli; - BOOST_REQUIRE_MESSAGE(!cli.remove_source(64), "not removed source 64"); + BOOST_REQUIRE_MESSAGE(!cli.remove_source(g_stream_id_max), + "not removed source " + std::to_string(g_stream_id_max)); BOOST_REQUIRE_MESSAGE(!cli.remove_source(-1), "not removed source -1"); } @@ -459,30 +473,58 @@ BOOST_AUTO_TEST_CASE(source_check_sap) { cli.sap_wait_deletion(0, sdp.second, 3); } -BOOST_AUTO_TEST_CASE(source_check_browser) { +BOOST_AUTO_TEST_CASE(source_check_sap_browser) { Client cli; BOOST_REQUIRE_MESSAGE(cli.add_source(0), "added source 0"); auto sdp = cli.get_source_sdp(0); BOOST_REQUIRE_MESSAGE(sdp.first, "got source sdp 0"); cli.sap_wait_announcement(0, sdp.second); - auto json = cli.get_remote_sources(); - BOOST_REQUIRE_MESSAGE(json.first, "got remote sources"); + auto json = cli.get_remote_sap_sources(); + BOOST_REQUIRE_MESSAGE(json.first, "got remote sap sources"); boost::property_tree::ptree pt; std::stringstream ss(json.second); boost::property_tree::read_json(ss, pt); BOOST_FOREACH (auto const& v, pt.get_child("remote_sources")) { BOOST_REQUIRE_MESSAGE(v.second.get("sdp") == sdp.second, - "returned source " + v.second.get("id")); + "returned sap source " + v.second.get("id")); } BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0"); cli.sap_wait_deletion(0, sdp.second, 3); - json = cli.get_remote_sources(); - BOOST_REQUIRE_MESSAGE(json.first, "got remote sources"); + json = cli.get_remote_sap_sources(); + BOOST_REQUIRE_MESSAGE(json.first, "got remote sap sources"); std::stringstream ss1(json.second); boost::property_tree::read_json(ss1, pt); - BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == 0, "no remote sources"); + BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == 0, + "no remote sap sources"); } +#ifdef _USE_AVAHI_ +BOOST_AUTO_TEST_CASE(source_check_mdns_browser) { + Client cli; + BOOST_REQUIRE_MESSAGE(cli.add_source(0), "added source 0"); + auto sdp = cli.get_source_sdp(0); + BOOST_REQUIRE_MESSAGE(sdp.first, "got source sdp 0"); + cli.sap_wait_announcement(0, sdp.second); + auto json = cli.get_remote_mdns_sources(); + BOOST_REQUIRE_MESSAGE(json.first, "got remote mdns sources"); + boost::property_tree::ptree pt; + std::stringstream ss(json.second); + boost::property_tree::read_json(ss, pt); + BOOST_FOREACH (auto const& v, pt.get_child("remote_sources")) { + BOOST_REQUIRE_MESSAGE(v.second.get("sdp") == sdp.second, + "returned mdns source " + v.second.get("id")); + } + BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0"); + cli.sap_wait_deletion(0, sdp.second, 3); + json = cli.get_remote_mdns_sources(); + BOOST_REQUIRE_MESSAGE(json.first, "got remote mdns sources"); + std::stringstream ss1(json.second); + boost::property_tree::read_json(ss1, pt); + BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == 0, + "no remote mdns sources"); +} +#endif + BOOST_AUTO_TEST_CASE(sink_check_status) { Client cli; BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(0), "added sink 0"); @@ -500,7 +542,7 @@ BOOST_AUTO_TEST_CASE(sink_check_status) { BOOST_AUTO_TEST_CASE(add_remove_all_sources) { Client cli; - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_source(id), std::string("added source ") + std::to_string(id)); } @@ -515,7 +557,7 @@ BOOST_AUTO_TEST_CASE(add_remove_all_sources) { "returned source " + std::to_string(id)); ++id; } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.remove_source(id), std::string("removed source ") + std::to_string(id)); } @@ -523,7 +565,7 @@ BOOST_AUTO_TEST_CASE(add_remove_all_sources) { BOOST_AUTO_TEST_CASE(add_remove_all_sinks) { Client cli; - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id), std::string("added sink ") + std::to_string(id)); } @@ -538,7 +580,7 @@ BOOST_AUTO_TEST_CASE(add_remove_all_sinks) { "returned sink " + std::to_string(id)); ++id; } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.remove_sink(id), std::string("removed sink ") + std::to_string(id)); } @@ -546,11 +588,11 @@ BOOST_AUTO_TEST_CASE(add_remove_all_sinks) { BOOST_AUTO_TEST_CASE(add_remove_check_all) { Client cli; - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_source(id), std::string("added source ") + std::to_string(id)); } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id), std::string("added sink ") + std::to_string(id)); } @@ -571,11 +613,11 @@ BOOST_AUTO_TEST_CASE(add_remove_check_all) { "returned sink " + std::to_string(id)); ++id; } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.remove_source(id), std::string("removed source ") + std::to_string(id)); } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.remove_sink(id), std::string("removed sink ") + std::to_string(id)); } @@ -583,19 +625,19 @@ BOOST_AUTO_TEST_CASE(add_remove_check_all) { BOOST_AUTO_TEST_CASE(add_remove_update_check_all) { Client cli; - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_source(id), std::string("added source ") + std::to_string(id)); } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_source(id), std::string("updated source ") + std::to_string(id)); } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id), std::string("added sink ") + std::to_string(id)); } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_sink_url(id), std::string("updated sink ") + std::to_string(id)); } @@ -616,11 +658,11 @@ BOOST_AUTO_TEST_CASE(add_remove_update_check_all) { "returned sink " + std::to_string(id)); ++id; } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.remove_source(id), std::string("removed source ") + std::to_string(id)); } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.remove_sink(id), std::string("removed sink ") + std::to_string(id)); } @@ -628,26 +670,32 @@ BOOST_AUTO_TEST_CASE(add_remove_update_check_all) { BOOST_AUTO_TEST_CASE(add_remove_check_sap_browser_all) { Client cli; - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_source(id), std::string("added source ") + std::to_string(id)); } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { auto sdp = cli.get_source_sdp(id); BOOST_REQUIRE_MESSAGE(sdp.first, std::string("got source sdp ") + std::to_string(id)); cli.sap_wait_announcement(id, sdp.second); } - auto json = cli.get_remote_sources(); - BOOST_REQUIRE_MESSAGE(json.first, "got remote sources"); boost::property_tree::ptree pt; - std::stringstream ss(json.second); - boost::property_tree::read_json(ss, pt); - BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == 64, "found 64 remote sources"); - for (int id = 0; id < 64; id++) { + int retry = 10; + do { + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto json = cli.get_remote_sap_sources(); + BOOST_REQUIRE_MESSAGE(json.first, "got remote sap sources"); + std::stringstream ss(json.second); + boost::property_tree::read_json(ss, pt); + BOOST_TEST_MESSAGE(std::to_string(pt.get_child("remote_sources").size())); + } while (pt.get_child("remote_sources").size() != g_stream_id_max && retry--); + BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == g_stream_id_max, + "found " + std::to_string(pt.get_child("remote_sources").size()) + " remote sap sources"); + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id), std::string("added sink ") + std::to_string(id)); } - json = cli.get_streams(); + auto json = cli.get_streams(); BOOST_REQUIRE_MESSAGE(json.first, "got streams"); std::stringstream ss1(json.second); boost::property_tree::read_json(ss1, pt); @@ -663,18 +711,83 @@ BOOST_AUTO_TEST_CASE(add_remove_check_sap_browser_all) { "returned sink " + std::to_string(id)); ++id; } - for (int id = 0; id < 64; id++) { + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.remove_source(id), std::string("removed source ") + std::to_string(id)); } cli.sap_wait_all_deletions(); - json = cli.get_remote_sources(); - BOOST_REQUIRE_MESSAGE(json.first, "got remote sources"); - std::stringstream ss2(json.second); - boost::property_tree::read_json(ss2, pt); - BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == 0, "no remote sources"); - for (int id = 0; id < 64; id++) { + retry = 10; + do { + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto json = cli.get_remote_sap_sources(); + BOOST_REQUIRE_MESSAGE(json.first, "got remote sap sources"); + std::stringstream ss2(json.second); + boost::property_tree::read_json(ss2, pt); + BOOST_TEST_MESSAGE(std::to_string(pt.get_child("remote_sources").size())); + } while (pt.get_child("remote_sources").size() > 0 && retry--); + BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == 0, "no remote sap sources"); + for (int id = 0; id < g_stream_id_max; id++) { BOOST_REQUIRE_MESSAGE(cli.remove_sink(id), std::string("removed sink ") + std::to_string(id)); } } + +#ifdef _USE_AVAHI_ +BOOST_AUTO_TEST_CASE(add_remove_check_mdns_browser_all) { + Client cli; + for (int id = 0; id < g_stream_id_max; id++) { + BOOST_REQUIRE_MESSAGE(cli.add_source(id), + std::string("added source ") + std::to_string(id)); + } + boost::property_tree::ptree pt; + int retry = 10; + do { + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto json = cli.get_remote_mdns_sources(); + BOOST_REQUIRE_MESSAGE(json.first, "got remote mdns sources"); + std::stringstream ss(json.second); + boost::property_tree::read_json(ss, pt); + BOOST_TEST_MESSAGE(std::to_string(pt.get_child("remote_sources").size())); + } while (pt.get_child("remote_sources").size() != g_stream_id_max && retry--); + BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == g_stream_id_max, + "found " + std::to_string(pt.get_child("remote_sources").size()) + " remote mdns sources"); + for (int id = 0; id < g_stream_id_max; id++) { + BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id), + std::string("added sink ") + std::to_string(id)); + } + auto json = cli.get_streams(); + BOOST_REQUIRE_MESSAGE(json.first, "got streams"); + std::stringstream ss1(json.second); + boost::property_tree::read_json(ss1, pt); + uint8_t id = 0; + BOOST_FOREACH (auto const& v, pt.get_child("sources")) { + BOOST_REQUIRE_MESSAGE(v.second.get("id") == id, + "returned source " + std::to_string(id)); + ++id; + } + id = 0; + BOOST_FOREACH (auto const& v, pt.get_child("sinks")) { + BOOST_REQUIRE_MESSAGE(v.second.get("id") == id, + "returned sink " + std::to_string(id)); + ++id; + } + for (int id = 0; id < g_stream_id_max; id++) { + BOOST_REQUIRE_MESSAGE(cli.remove_source(id), + std::string("removed source ") + std::to_string(id)); + } + retry = 10; + do { + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto json = cli.get_remote_mdns_sources(); + BOOST_REQUIRE_MESSAGE(json.first, "got remote mdns sources"); + std::stringstream ss2(json.second); + boost::property_tree::read_json(ss2, pt); + BOOST_TEST_MESSAGE(std::to_string(pt.get_child("remote_sources").size())); + } while (pt.get_child("remote_sources").size() > 0 && retry--); + BOOST_REQUIRE_MESSAGE(pt.get_child("remote_sources").size() == 0, "no remote mdns sources"); + for (int id = 0; id < g_stream_id_max; id++) { + BOOST_REQUIRE_MESSAGE(cli.remove_sink(id), + std::string("removed sink ") + std::to_string(id)); + } +} +#endif diff --git a/daemon/utils.cpp b/daemon/utils.cpp index 46bbd65..9f3dc60 100644 --- a/daemon/utils.cpp +++ b/daemon/utils.cpp @@ -32,3 +32,63 @@ uint16_t crc16(const uint8_t* p, size_t len) { } return crc; } + +std::tuple +parse_url(const std::string& _url) { + std::string url = httplib::detail::decode_url(_url); + size_t protocol_sep_pos = url.find_first_of("://"); + if (protocol_sep_pos == std::string::npos) { + /* no protocol, invalid URL */ + return std::make_tuple(false, "", "", "", ""); + } + + std::string port, host, path("/"); + std::string protocol = url.substr(0, protocol_sep_pos); + std::string url_new = url.substr(protocol_sep_pos + 3); + size_t path_sep_pos = url_new.find_first_of("/"); + size_t port_sep_pos = url_new.find_first_of(":"); + if (port_sep_pos != std::string::npos) { + /* port specified */ + if (path_sep_pos != std::string::npos) { + /* path specified */ + port = url_new.substr(port_sep_pos + 1, path_sep_pos - port_sep_pos - 1); + path = url_new.substr(path_sep_pos); + } else { + /* path not specified */ + port = url_new.substr(port_sep_pos + 1); + } + host = url_new.substr(0, port_sep_pos); + } else if (path_sep_pos != std::string::npos) { + /* port not specified, path specified */ + host = url_new.substr(0, path_sep_pos); + path = url_new.substr(path_sep_pos); + } else { + /* port and path not specified */ + host = url_new; + } + return std::make_tuple(host.length() > 0, protocol, host, port, path); +} + +std::string get_host_id() { + char hostname[64]; + gethostname(hostname, sizeof hostname); + std::stringstream ss; + ss << std::hex << (uint32_t)gethostid(); + return ss.str(); +} + +std::string get_host_name() { + char hostname[64]; + gethostname(hostname, sizeof hostname); + return hostname; +} + +std::string get_node_id() { + std::stringstream ss; + ss << "AES67 daemon " << get_host_id(); + return ss.str(); +} diff --git a/daemon/utils.hpp b/daemon/utils.hpp index 07345f7..9af9e72 100644 --- a/daemon/utils.hpp +++ b/daemon/utils.hpp @@ -20,10 +20,22 @@ #ifndef _UTILS_HPP_ #define _UTILS_HPP_ -#include - +#include #include +#include + uint16_t crc16(const uint8_t* p, size_t len); +std::tuple +parse_url(const std::string& _url); + +std::string get_host_id(); +std::string get_host_name(); +std::string get_node_id(); + #endif diff --git a/demo/daemon.conf b/demo/daemon.conf index 0c10fe5..bcbf809 100644 --- a/demo/daemon.conf +++ b/demo/daemon.conf @@ -1,5 +1,6 @@ { "http_port": 8080, + "rtsp_port": 8854, "http_base_dir": "./webui/build", "log_severity": 3, "playout_delay": 0, diff --git a/run_demo.sh b/run_demo.sh index 0d6a5d4..33ff60c 100755 --- a/run_demo.sh +++ b/run_demo.sh @@ -40,7 +40,7 @@ fi trap cleanup EXIT #configure system parms -sudo sysctl -w net/ipv4/igmp_max_memberships=64 +sudo sysctl -w net/ipv4/igmp_max_memberships=66 if [ -x /usr/bin/pulseaudio ]; then #stop pulseaudio, this seems to open/close ALSA continuosly diff --git a/webui/src/Config.js b/webui/src/Config.js index cbbbcae..db1b000 100644 --- a/webui/src/Config.js +++ b/webui/src/Config.js @@ -31,6 +31,8 @@ class Config extends Component { super(props); this.state = { httpPort: '', + rtspPort: '', + rtspPortErr: false, logSeverity: '', playoutDelay: '', playoutDelayErr: false, @@ -70,6 +72,7 @@ class Config extends Component { data => this.setState( { httpPort: data.http_port, + rtspPort: data.rtsp_port, logSeverity: data.log_severity, playoutDelay: data.playout_delay, ticFrameSizeAt1fs: data.tic_frame_size_at_1fs, @@ -88,6 +91,7 @@ class Config extends Component { interfaceName: data.interface_name, macAddr: data.mac_addr, ipAddr: data.ip_addr, + nodeId: data.node_id, isConfigLoading: false })) .catch(err => this.setState({isConfigLoading: false})); @@ -100,6 +104,7 @@ class Config extends Component { !this.state.rtpMcastBaseErr && !this.state.sapMcastAddrErr && !this.state.rtpPortErr && + !this.state.rtspPortErr && !this.state.sapIntervalErr && !this.state.syslogServerErr && !this.state.isConfigLoading; @@ -113,6 +118,7 @@ class Config extends Component { this.state.syslogServer, this.state.rtpMcastBase, this.state.rtpPort, + this.state.rtspPort, this.state.playoutDelay, this.state.ticFrameSizeAt1fs, this.state.sampleRate, @@ -154,10 +160,18 @@ class Config extends Component {
{this.state.isConfigLoading ? :

Network Config

} + + + + + + + + diff --git a/webui/src/Services.js b/webui/src/Services.js index 38d0c3b..148695a 100644 --- a/webui/src/Services.js +++ b/webui/src/Services.js @@ -31,7 +31,7 @@ const source = '/source'; const sdp = '/sdp'; const sink = '/sink'; const status = '/status'; -const browseSources = '/browse/sources'; +const browseSources = '/browse/sources/all'; const defaultParams = { credentials: 'same-origin', @@ -76,7 +76,7 @@ export default class RestAPI { }); } - static setConfig(log_severity, syslog_proto, syslog_server, rtp_mcast_base, rtp_port, playout_delay, tic_frame_size_at_1fs, sample_rate, max_tic_frame_size, sap_mcast_addr, sap_interval, mdns_enabled) { + static setConfig(log_severity, syslog_proto, syslog_server, rtp_mcast_base, rtp_port, rtsp_port, playout_delay, tic_frame_size_at_1fs, sample_rate, max_tic_frame_size, sap_mcast_addr, sap_interval, mdns_enabled) { return this.doFetch(config, { body: JSON.stringify({ log_severity: parseInt(log_severity, 10), @@ -84,6 +84,7 @@ export default class RestAPI { syslog_server: syslog_server, rtp_mcast_base: rtp_mcast_base, rtp_port: parseInt(rtp_port, 10), + rtsp_port: parseInt(rtsp_port, 10), playout_delay: parseInt(playout_delay, 10), tic_frame_size_at_1fs: parseInt(tic_frame_size_at_1fs, 10), sample_rate: parseInt(sample_rate, 10),
this.setState({rtspPort: e.target.value, rtspPortErr: !e.currentTarget.checkValidity()})} required/>
this.setState({rtpMcastBase: e.target.value, rtpMcastBaseErr: !e.currentTarget.checkValidity()})} required/>