From 0fccbf57d69405e7a749f8d0252fbb88aa4c2467 Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Mon, 30 Mar 2020 19:17:30 +0200 Subject: [PATCH 1/3] Changed demo configuration to loopback test with 8 channels --- demo/status.json | 6 +++--- run_demo.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/status.json b/demo/status.json index f329272..1f6cb8e 100644 --- a/demo/status.json +++ b/demo/status.json @@ -11,7 +11,7 @@ "payload_type": 98, "dscp": 34, "refclk_ptp_traceable": false, - "map": [ 0, 1 ] + "map": [ 0, 1, 2, 3, 4, 5, 6, 7 ] } ], "sinks": [ { @@ -20,9 +20,9 @@ "io": "Audio Device", "use_sdp": true, "source": "http://127.0.0.1:8080/api/source/sdp/0", - "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=IEEE1588-2008:00-00-00-FF-FE-00-00-00:0\na=recvonly\n", + "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/8\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=IEEE1588-2008:00-10-4B-FF-FE-7A-87-FC:0\na=recvonly\n", "delay": 576, "ignore_refclk_gmid": true, - "map": [ 0, 1 ] + "map": [ 0, 1, 2, 3, 4, 5, 6, 7 ] } ] } diff --git a/run_demo.sh b/run_demo.sh index aca11e9..0d6a5d4 100755 --- a/run_demo.sh +++ b/run_demo.sh @@ -76,12 +76,12 @@ sleep 30 #starting recording on sink echo "Starting to record 60 secs from sink ..." -arecord -D plughw:RAVENNA -f cd -d 60 -r 44100 -c 2 -t wav /tmp/sink_test.wav > /dev/null 2>&1 & +arecord -D plughw:RAVENNA -f cd -d 60 -r 44100 -c 8 -t wav /tmp/sink_test.wav > /dev/null 2>&1 & sleep 10 #starting playback on source echo "Starting to playback test on source ..." -speaker-test -D plughw:RAVENNA -r 44100 -c 2 -t sine > /dev/null 2>&1 & +speaker-test -D plughw:RAVENNA -r 44100 -c 8 -t sine > /dev/null 2>&1 & while killall -0 arecord 2>/dev/null ; do sleep 1 From 22cdd55085db02b0e4bf40dcb24523dc88dd715e Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Tue, 31 Mar 2020 18:46:20 +0200 Subject: [PATCH 2/3] Addded short introduction --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b88b469..745a11d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,24 @@ # AES67 Linux Daemon -AES67 Linux Daemon is a Linux implementation of AES67 interoperability standard used to distribute and synchronize real time audio over Ethernet. -See https://en.wikipedia.org/wiki/AES67 for additional info. +AES67 Linux Daemon is a Linux implementation of AES67 interoperability standard used to distribute and synchronize real time audio over Ethernet. +See [https://en.wikipedia.org/wiki/AES67](https://en.wikipedia.org/wiki/AES67) for additional info. + +The daemon 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 **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. +A _Sink_ receives RTP packets from a specific multicast address and writes them in the ALSA capture device. + +A user can use the ALSA capture device to receive synchronized incoming audio samples from an RTP stream and the ALSA playback device to send synchronized audio samples to an RTP stream. +The binding between a _Source_ and the ALSA playback device is determined by the channels used during the playback and the configured _Source_ channels map. The binding between a _Sink_ and the ALSA capture device is determined by the channels used while recoding and the configured _Sink_ channels map. + +The driver handles the PTP and RTP packets processing and acts as a PTP clock slave to synchronize with a master clock on the specified PTP domain. All the configured _Sources_ and _Sinks_ are synchronized using the same PTP clock. + +The daemon communicates with the driver for control, configuration and status monitoring only by using _netlink_ sockets. +The daemon implements a REST interface to configure and monitor the _Sources_, the _Sinks_ and PTP slave. See [README](daemon/README.md) for additional info. +The daemon also implements SAP for sources annoncements and discovery and mDNS sources discovery and SDP transfer via RTSP. + +A WebUI is provided to allow daemon and driver configuration and monitoring. The WebUI uses the daemon REST API and exposes all the supported configuration paramaters for the daemon, the PTP slave clock, the _Sources_ and the _Sinks_. The WebUI can also be used to monitor the PTP slave status and the _Sinks_ status and to browse the remote SAP and mDNS sources. ## License ## From 0dbfe78a10754ddc0d2daaa07fce2f53bc46c9c6 Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Thu, 23 Apr 2020 11:45:58 -0700 Subject: [PATCH 3/3] - 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/>