Added SAP browser functionalities to the daemon and the WebUI
- added "sap_mcast_addr" parameter to daemon conguration and WebUI to configure the SAP multicast address used for sending and receiving source announcements - added REST API to retrieve the remote sources collected by the daemon (GET /api/browse/sources) - added Browser tab to the WebUI to visualize info on the available remote sources - added Info function to sources listed in the Sources tab to visualize the associated SDP file - extended daemon regression tests to test the SAP Browser
This commit is contained in:
parent
f57046a478
commit
d99bf3ed4a
13
README.md
13
README.md
@ -8,7 +8,7 @@ The daemon uses the following open source:
|
|||||||
|
|
||||||
* **Merging Technologies ALSA RAVENNA/AES67 Driver** licensed under [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
* **Merging Technologies ALSA RAVENNA/AES67 Driver** licensed under [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
||||||
* **cpp-httplib** licensed under the [MIT License](https://github.com/yhirose/cpp-httplib/blob/master/LICENSE)
|
* **cpp-httplib** licensed under the [MIT License](https://github.com/yhirose/cpp-httplib/blob/master/LICENSE)
|
||||||
* **Boost libraries** licensed with [Boost Software License](https://www.boost.org/LICENSE_1_0.txt)
|
* **Boost libraries** licensed under the [Boost Software License](https://www.boost.org/LICENSE_1_0.txt)
|
||||||
|
|
||||||
## Repository content ##
|
## Repository content ##
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ The daemon can be cross-compiled for multiple platforms and implements the follo
|
|||||||
* control and configuration of up to 64 sources and sinks using the ALSA RAVENNA/AES67 driver via netlink
|
* 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
|
* session handling and SDP parsing and creation
|
||||||
* HTTP REST API for control and configuration
|
* HTTP REST API for control and configuration
|
||||||
* SAP discovery protocol implementation
|
* SAP discovery protocol implementation and SAP browser implementation
|
||||||
* IGMP handling for SAP, RTP and PTP multicast traffic
|
* IGMP handling for SAP, RTP and PTP multicast traffic
|
||||||
|
|
||||||
The directory also contains the daemon regression tests in the [tests](daemon/tests) subdirectory. To run daemon tests install the ALSA RAVENNA/AES67 kernel module enter the [tests](daemon/tests) subdirectory and run *./daemon-test -l all*
|
The directory also contains the daemon regression tests in the [tests](daemon/tests) subdirectory. To run daemon tests install the ALSA RAVENNA/AES67 kernel module enter the [tests](daemon/tests) subdirectory and run *./daemon-test -l all*
|
||||||
@ -101,9 +101,9 @@ The demo performs the following operations:
|
|||||||
## Interoperability tests ##
|
## Interoperability tests ##
|
||||||
To run interoperability tests using the [Hasseb audio over Ethernet receiver](http://hasseb.fi/shop2/index.php?route=product/product&product_id=62) follow these steps:
|
To run interoperability tests using the [Hasseb audio over Ethernet receiver](http://hasseb.fi/shop2/index.php?route=product/product&product_id=62) follow these steps:
|
||||||
|
|
||||||
* open he daemon configuration file *daemon.conf* and change the following parameters:
|
* open the daemon configuration file *daemon.conf* and change the following parameters:
|
||||||
* set the network interface name to your Ethernet card, e.g.: *"interface\_name": "eth0"*
|
* set network interface name to your Ethernet card, e.g.: *"interface\_name": "eth0"*
|
||||||
* set the default sample rate to 48Khz: *"sample\_rate": 48000*
|
* set default sample rate to 48Khz: *"sample\_rate": 48000*
|
||||||
* verify that PulseAdio is not running. See [PulseAudio](#notes).
|
* verify that PulseAdio is not running. See [PulseAudio](#notes).
|
||||||
* install the ALSA RAVENNA/AES67 module with:
|
* install the ALSA RAVENNA/AES67 module with:
|
||||||
*sudo insmod 3rdparty/ravenna-alsa-lkm/driver/MergingRavennaALSA.ko*
|
*sudo insmod 3rdparty/ravenna-alsa-lkm/driver/MergingRavennaALSA.ko*
|
||||||
@ -112,8 +112,7 @@ To run interoperability tests using the [Hasseb audio over Ethernet receiver](ht
|
|||||||
* open the Daemon WebUi *http://[address:8080]* and do the following:
|
* open the Daemon WebUi *http://[address:8080]* and do the following:
|
||||||
* go to Config tab and verify that the sample rate is set to 48KHz
|
* go to Config tab and verify that the sample rate is set to 48KHz
|
||||||
* go to Sources tab and add a new Source using the plus button, set Codec to L24 and press the Submit button
|
* go to Sources tab and add a new Source using the plus button, set Codec to L24 and press the Submit button
|
||||||
* go to Sinks tab and add a new Sink using the plus button, use the specified Source URL and press the Submit button
|
* go to Sources tab and click on the info icon on the right of the newly created source and copy the SDP file
|
||||||
* edit the newly created Sink and copy the SDP file reported in SDP
|
|
||||||
* open the Hasseb WebUI and do the following:
|
* open the Hasseb WebUI and do the following:
|
||||||
* deselect the "PTP slave only" checkbox to enable PTP master on Hasseb device
|
* 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
|
* select the "Add SDP file manually" checkbox and copy the previous Source SDP into the SDP field
|
||||||
|
@ -7,6 +7,6 @@ set(RAVENNA_ALSA_LKM "../3rdparty/ravenna-alsa-lkm/")
|
|||||||
set(CPP_HTTPLIB " ../3rdparty/cpp-httplib/")
|
set(CPP_HTTPLIB " ../3rdparty/cpp-httplib/")
|
||||||
find_package(Boost COMPONENTS system thread log program_options REQUIRED)
|
find_package(Boost COMPONENTS system thread log program_options REQUIRED)
|
||||||
include_directories(aes67-daemon ${RAVENNA_ALSA_LKM}/common ${RAVENNA_ALSA_LKM}/driver ${CPP_HTTPLIB} ${Boost_INCLUDE_DIR})
|
include_directories(aes67-daemon ${RAVENNA_ALSA_LKM}/common ${RAVENNA_ALSA_LKM}/driver ${CPP_HTTPLIB} ${Boost_INCLUDE_DIR})
|
||||||
add_executable(aes67-daemon error_code.cpp json.cpp main.cpp driver_handler.cpp driver_manager.cpp session_manager.cpp http_server.cpp config.cpp interface.cpp log.cpp)
|
add_executable(aes67-daemon error_code.cpp json.cpp main.cpp driver_handler.cpp driver_manager.cpp session_manager.cpp http_server.cpp config.cpp interface.cpp log.cpp sap.cpp browser.cpp)
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
target_link_libraries(aes67-daemon ${Boost_LIBRARIES})
|
target_link_libraries(aes67-daemon ${Boost_LIBRARIES})
|
||||||
|
@ -7,7 +7,7 @@ The daemon is responsible for:
|
|||||||
* communication and configuration of the device driver
|
* communication and configuration of the device driver
|
||||||
* provide an HTTP REST API for the daemon control and configuration
|
* provide an HTTP REST API for the daemon control and configuration
|
||||||
* session handling and SDP parsing and creation
|
* session handling and SDP parsing and creation
|
||||||
* SAP discovery protocol implementation
|
* SAP discovery protocol implementation and SAP browser
|
||||||
* IGMP handling for SAP and RTP sessions
|
* IGMP handling for SAP and RTP sessions
|
||||||
|
|
||||||
## Configuration file ##
|
## Configuration file ##
|
||||||
@ -128,6 +128,14 @@ In case of failure the server returns a **text/plain** content type with the cat
|
|||||||
* **Body type** application/json
|
* **Body type** application/json
|
||||||
* **Body** [RTP Streams params](#rtp-streams)
|
* **Body** [RTP Streams params](#rtp-streams)
|
||||||
|
|
||||||
|
### Get all remote RTP Sources ###
|
||||||
|
* **Description** retrieve all the remote sources collected via SAP
|
||||||
|
* **URL** /api/browse/sources
|
||||||
|
* **Method** GET
|
||||||
|
* **URL Params** none
|
||||||
|
* **Body type** application/json
|
||||||
|
* **Body** [RTP Remote Sources params](#rtp-remote-sources)
|
||||||
|
|
||||||
## HTTP REST API structures ##
|
## HTTP REST API structures ##
|
||||||
|
|
||||||
### JSON Config<a name="config"></a> ###
|
### JSON Config<a name="config"></a> ###
|
||||||
@ -149,6 +157,7 @@ Example
|
|||||||
"frame_size_at_1fs": 192,
|
"frame_size_at_1fs": 192,
|
||||||
"sample_rate": 44100,
|
"sample_rate": 44100,
|
||||||
"max_tic_frame_size": 1024,
|
"max_tic_frame_size": 1024,
|
||||||
|
"sap_mcast_addr": "239.255.255.255",
|
||||||
"sap_interval": 30,
|
"sap_interval": 30,
|
||||||
"mac_addr": "01:00:5e:01:00:01",
|
"mac_addr": "01:00:5e:01:00:01",
|
||||||
"ip_addr": "127.0.0.1"
|
"ip_addr": "127.0.0.1"
|
||||||
@ -208,6 +217,10 @@ where:
|
|||||||
> JSON number specifying the max tick frame size.
|
> JSON number specifying the max tick frame size.
|
||||||
> In case of a high value of *tic_frame_size_at_1fs*, this must be set to 8192.
|
> In case of a high value of *tic_frame_size_at_1fs*, this must be set to 8192.
|
||||||
|
|
||||||
|
> **sap\_mcast\_addr**
|
||||||
|
> JSON string specifying the SAP multicast address used for both announcing local sources and browsing remote sources.
|
||||||
|
> By default and according to SAP RFC this address is 224.2.127.254, but many devices use 239.255.255.255.
|
||||||
|
|
||||||
> **sap\_interval**
|
> **sap\_interval**
|
||||||
> JSON number specifying the SAP interval in seconds to use. Use 0 for automatic and RFC compliant interval. Default is 30secs.
|
> JSON number specifying the SAP interval in seconds to use. Use 0 for automatic and RFC compliant interval. Default is 30secs.
|
||||||
|
|
||||||
@ -520,3 +533,57 @@ where:
|
|||||||
> Every sink is identified by the JSON number **id** (in the range 0 - 63).
|
> Every sink is identified by the JSON number **id** (in the range 0 - 63).
|
||||||
> See [RTP Sink params](#rtp-sink) for all the other parameters.
|
> See [RTP Sink params](#rtp-sink) for all the other parameters.
|
||||||
|
|
||||||
|
### JSON Remote Sources<a name="rtp-remote-sources"></a> ###
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
{
|
||||||
|
"remote_sources": [
|
||||||
|
{
|
||||||
|
"source": "SAP",
|
||||||
|
"id": "d00000a611d",
|
||||||
|
"name": "ALSA Source 2",
|
||||||
|
"sdp": "v=0\no=- 2 0 IN IP4 10.0.0.13\ns=ALSA Source 2\nc=IN IP4 239.1.0.3/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.3/15\na=rtpmap:98 L16/48000/2\na=sync-time:0\na=framecount:48\na=ptime:1\na=mediaclk:direct=0\na=ts-refclk:ptp=IEEE1588-2008:00-10-4B-FF-FE-7A-87-FC:0\na=recvonly\n",
|
||||||
|
"last_seen": 2768,
|
||||||
|
"announce_period": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "SAP",
|
||||||
|
"id": "d00000a8dd5",
|
||||||
|
"name": "ALSA Source 1",
|
||||||
|
"sdp": "v=0\no=- 1 0 IN IP4 10.0.0.13\ns=ALSA Source 1\nc=IN IP4 239.1.0.2/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.2/15\na=rtpmap:98 L16/48000/2\na=sync-time:0\na=framecount:48\na=ptime:1\na=mediaclk:direct=0\na=ts-refclk:ptp=IEEE1588-2008:00-10-4B-FF-FE-7A-87-FC:0\na=recvonly\n",
|
||||||
|
"last_seen": 2768,
|
||||||
|
"announce_period": 30
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
where:
|
||||||
|
|
||||||
|
> **remote\_sources**
|
||||||
|
> JSON array of the remote sources collected so far.
|
||||||
|
> Every source is identified by the unique JSON string **id**.
|
||||||
|
|
||||||
|
> **source**
|
||||||
|
> JSON string specifying the protocol used to collect the remote source.
|
||||||
|
|
||||||
|
> **id**
|
||||||
|
> JSON string specifying the remote source unique id.
|
||||||
|
|
||||||
|
> **name**
|
||||||
|
> JSON string specifying the remote source name announced in the SDP file.
|
||||||
|
|
||||||
|
> **address**
|
||||||
|
> JSON string specifying the remote source address announced.
|
||||||
|
|
||||||
|
> **sdp**
|
||||||
|
> JSON string specifying the remote source SDP.
|
||||||
|
|
||||||
|
> **last\_seen**
|
||||||
|
> JSON number specifying the last time the source was announced.
|
||||||
|
> This time is expressed in seconds since the daemon startup.
|
||||||
|
|
||||||
|
> **announce_period**
|
||||||
|
> JSON number specifying the meausured period in seconds between the last source announcements.
|
||||||
|
> A remote source is automatically removed if it doesn't get announced for **announce\_period** x 10 seconds.
|
||||||
|
|
||||||
|
137
daemon/browser.cpp
Normal file
137
daemon/browser.cpp
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
//
|
||||||
|
// browser.cpp
|
||||||
|
//
|
||||||
|
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <experimental/map>
|
||||||
|
#include "browser.hpp"
|
||||||
|
|
||||||
|
using namespace std::chrono;
|
||||||
|
using second_t = std::chrono::duration<double, std::ratio<1> >;
|
||||||
|
|
||||||
|
std::shared_ptr<Browser> Browser::create(
|
||||||
|
std::shared_ptr<Config> config) {
|
||||||
|
// no need to be thread-safe here
|
||||||
|
static std::weak_ptr<Browser> instance;
|
||||||
|
if (auto ptr = instance.lock()) {
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
auto ptr =
|
||||||
|
std::shared_ptr<Browser>(new Browser(config));
|
||||||
|
instance = ptr;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::list<RemoteSource> Browser::get_remote_sources() {
|
||||||
|
std::list<RemoteSource> sources_list;
|
||||||
|
std::shared_lock sources_lock(sources_mutex_);
|
||||||
|
for (auto const& [id, source] : sources_) {
|
||||||
|
sources_list.emplace_back(source);
|
||||||
|
}
|
||||||
|
return sources_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string sdp_get_subject(const std::string& sdp) {
|
||||||
|
std::stringstream ssstrem(sdp);
|
||||||
|
std::string line;
|
||||||
|
while (getline(ssstrem, line, '\n')) {
|
||||||
|
if (line.substr(0, 2) == "s=") {
|
||||||
|
return line.substr(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Browser::worker() {
|
||||||
|
sap_.set_multicast_interface(config_->get_ip_addr_str());
|
||||||
|
// Join SAP muticast address
|
||||||
|
igmp_.join(config_->get_ip_addr_str(), config_->get_sap_mcast_addr());
|
||||||
|
auto startup = steady_clock::now();
|
||||||
|
auto sap_timepoint = steady_clock::now();
|
||||||
|
int sap_interval = 10;
|
||||||
|
|
||||||
|
while (running_) {
|
||||||
|
bool is_announce;
|
||||||
|
uint16_t msg_id_hash;
|
||||||
|
uint32_t addr;
|
||||||
|
std::string sdp;
|
||||||
|
|
||||||
|
if (sap_.receive(is_announce, msg_id_hash, addr, sdp)) {
|
||||||
|
char id[13];
|
||||||
|
snprintf(id, sizeof id, "%x%x", addr, msg_id_hash);
|
||||||
|
//BOOST_LOG_TRIVIAL(debug) << "browser:: received SAP message for " << id;
|
||||||
|
|
||||||
|
std::unique_lock sources_lock(sources_mutex_);
|
||||||
|
auto it = sources_.find(id);
|
||||||
|
if (it == sources_.end()) {
|
||||||
|
// Source is not in the map
|
||||||
|
if (is_announce) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "browser:: adding SAP source " << id;
|
||||||
|
// 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);
|
||||||
|
source.last_seen =
|
||||||
|
duration_cast<second_t>(steady_clock::now() - startup).count();
|
||||||
|
source.announce_period = 360; //default period
|
||||||
|
sources_[id] = source;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Source is already in the mamap
|
||||||
|
if (is_announce) {
|
||||||
|
BOOST_LOG_TRIVIAL(debug)
|
||||||
|
<< "browser:: refreshing SAP source " << (*it).second.id;
|
||||||
|
// annoucement, update last seen and announce periods
|
||||||
|
uint32_t last_seen =
|
||||||
|
duration_cast<second_t>(steady_clock::now() - startup).count();
|
||||||
|
(*it).second.announce_period = last_seen - (*it).second.last_seen;
|
||||||
|
(*it).second.last_seen = last_seen;
|
||||||
|
} else {
|
||||||
|
BOOST_LOG_TRIVIAL(info)
|
||||||
|
<< "browser:: removing SAP source " << (*it).second.id;
|
||||||
|
// deletion, remove entry
|
||||||
|
sources_.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it's time to check the SAP remote sources
|
||||||
|
if ((duration_cast<second_t>(steady_clock::now() - sap_timepoint).count())
|
||||||
|
> sap_interval) {
|
||||||
|
sap_timepoint = steady_clock::now();
|
||||||
|
// remove all sessions no longer announced
|
||||||
|
auto offset = duration_cast<second_t>(steady_clock::now() - startup).count();
|
||||||
|
|
||||||
|
std::unique_lock sources_lock(sources_mutex_);
|
||||||
|
std::experimental::erase_if(sources_, [offset](auto entry) {
|
||||||
|
if ((offset - entry.second.last_seen) > (entry.second.announce_period * 10)) {
|
||||||
|
// remove from remote SAP sources
|
||||||
|
BOOST_LOG_TRIVIAL(info)
|
||||||
|
<< "browser:: SAP source " << entry.second.id << " timeout";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
88
daemon/browser.hpp
Normal file
88
daemon/browser.hpp
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// browser.hpp
|
||||||
|
//
|
||||||
|
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef _BROWSER_HPP_
|
||||||
|
#define _BROWSER_HPP_
|
||||||
|
|
||||||
|
#include <future>
|
||||||
|
#include <shared_mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#include "config.hpp"
|
||||||
|
#include "sap.hpp"
|
||||||
|
#include "igmp.hpp"
|
||||||
|
|
||||||
|
struct RemoteSource {
|
||||||
|
std::string id;
|
||||||
|
std::string source;
|
||||||
|
std::string address;
|
||||||
|
std::string name;
|
||||||
|
std::string sdp;
|
||||||
|
uint32_t last_seen; /* seconds from daemon startup */
|
||||||
|
uint32_t announce_period; /* period between annoucementis */
|
||||||
|
};
|
||||||
|
|
||||||
|
class Browser {
|
||||||
|
public:
|
||||||
|
static std::shared_ptr<Browser> create(
|
||||||
|
std::shared_ptr<Config> config);
|
||||||
|
Browser(const Browser&) = delete;
|
||||||
|
Browser& operator=(const Browser&) = delete;
|
||||||
|
virtual ~Browser(){ stop(); };
|
||||||
|
|
||||||
|
// session manager interface
|
||||||
|
bool start() {
|
||||||
|
if (!running_) {
|
||||||
|
running_ = true;
|
||||||
|
res_ = std::async(std::launch::async, &Browser::worker, this);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool stop() {
|
||||||
|
if (running_) {
|
||||||
|
running_ = false;
|
||||||
|
return res_.get();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::list<RemoteSource> get_remote_sources();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// singleton, use create() to build
|
||||||
|
Browser(std::shared_ptr<Config> config)
|
||||||
|
: config_(config){};
|
||||||
|
|
||||||
|
bool worker();
|
||||||
|
|
||||||
|
std::shared_ptr<Config> config_;
|
||||||
|
std::future<bool> res_;
|
||||||
|
std::atomic_bool running_{false};
|
||||||
|
|
||||||
|
/* current sources */
|
||||||
|
std::map<std::string /* id */, RemoteSource> sources_;
|
||||||
|
mutable std::shared_mutex sources_mutex_;
|
||||||
|
|
||||||
|
SAP sap_{config_->get_sap_mcast_addr()};
|
||||||
|
IGMP igmp_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
@ -68,6 +68,10 @@ std::shared_ptr<Config> Config::parse(const std::string& filename) {
|
|||||||
if (ip::address_v4::from_string(config.rtp_mcast_base_.c_str()).to_ulong() ==
|
if (ip::address_v4::from_string(config.rtp_mcast_base_.c_str()).to_ulong() ==
|
||||||
INADDR_NONE)
|
INADDR_NONE)
|
||||||
config.rtp_mcast_base_ = "239.1.0.1";
|
config.rtp_mcast_base_ = "239.1.0.1";
|
||||||
|
if (ip::address_v4::from_string(config.sap_mcast_addr_.c_str()).to_ulong() ==
|
||||||
|
INADDR_NONE)
|
||||||
|
config.sap_mcast_addr_ = "224.2.127.254";
|
||||||
|
if (config.ptp_domain_ > 127)
|
||||||
if (config.ptp_domain_ > 127)
|
if (config.ptp_domain_ > 127)
|
||||||
config.ptp_domain_ = 0;
|
config.ptp_domain_ = 0;
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ class Config {
|
|||||||
uint32_t get_max_tic_frame_size() const { return max_tic_frame_size_; };
|
uint32_t get_max_tic_frame_size() const { return max_tic_frame_size_; };
|
||||||
uint32_t get_sample_rate() const { return sample_rate_; };
|
uint32_t get_sample_rate() const { return sample_rate_; };
|
||||||
const std::string& get_rtp_mcast_base() const { return rtp_mcast_base_; };
|
const std::string& get_rtp_mcast_base() const { return rtp_mcast_base_; };
|
||||||
|
const std::string& get_sap_mcast_addr() const { return sap_mcast_addr_; };
|
||||||
uint16_t get_rtp_port() const { return rtp_port_; };
|
uint16_t get_rtp_port() const { return rtp_port_; };
|
||||||
uint8_t get_ptp_domain() const { return ptp_domain_; };
|
uint8_t get_ptp_domain() const { return ptp_domain_; };
|
||||||
uint8_t get_ptp_dscp() const { return ptp_dscp_; };
|
uint8_t get_ptp_dscp() const { return ptp_dscp_; };
|
||||||
@ -73,6 +74,9 @@ class Config {
|
|||||||
void set_rtp_mcast_base(const std::string& rtp_mcast_base) {
|
void set_rtp_mcast_base(const std::string& rtp_mcast_base) {
|
||||||
rtp_mcast_base_ = rtp_mcast_base;
|
rtp_mcast_base_ = rtp_mcast_base;
|
||||||
};
|
};
|
||||||
|
void set_sap_mcast_addr(const std::string& sap_mcast_addr) {
|
||||||
|
sap_mcast_addr_ = sap_mcast_addr;
|
||||||
|
};
|
||||||
void set_rtp_port(uint16_t rtp_port) { rtp_port_ = rtp_port; };
|
void set_rtp_port(uint16_t rtp_port) { rtp_port_ = rtp_port; };
|
||||||
void set_ptp_domain(uint8_t ptp_domain) { ptp_domain_ = ptp_domain; };
|
void set_ptp_domain(uint8_t ptp_domain) { ptp_domain_ = ptp_domain; };
|
||||||
void set_ptp_dscp(uint8_t ptp_dscp) { ptp_dscp_ = ptp_dscp; };
|
void set_ptp_dscp(uint8_t ptp_dscp) { ptp_dscp_ = ptp_dscp; };
|
||||||
@ -108,6 +112,7 @@ class Config {
|
|||||||
uint32_t max_tic_frame_size_{1024};
|
uint32_t max_tic_frame_size_{1024};
|
||||||
uint32_t sample_rate_{44100};
|
uint32_t sample_rate_{44100};
|
||||||
std::string rtp_mcast_base_{"239.1.0.1"};
|
std::string rtp_mcast_base_{"239.1.0.1"};
|
||||||
|
std::string sap_mcast_addr_{"224.2.127.254"};
|
||||||
uint16_t rtp_port_{5004};
|
uint16_t rtp_port_{5004};
|
||||||
uint8_t ptp_domain_{0};
|
uint8_t ptp_domain_{0};
|
||||||
uint8_t ptp_dscp_{46};
|
uint8_t ptp_dscp_{46};
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"rtp_port": 5004,
|
"rtp_port": 5004,
|
||||||
"ptp_domain": 0,
|
"ptp_domain": 0,
|
||||||
"ptp_dscp": 48,
|
"ptp_dscp": 48,
|
||||||
|
"sap_mcast_addr": "239.255.255.255",
|
||||||
"sap_interval": 30,
|
"sap_interval": 30,
|
||||||
"syslog_proto": "none",
|
"syslog_proto": "none",
|
||||||
"syslog_server": "255.255.255.254:1234",
|
"syslog_server": "255.255.255.254:1234",
|
||||||
|
@ -85,7 +85,7 @@ bool HttpServer::start() {
|
|||||||
|
|
||||||
svr_.set_base_dir(config_->get_http_base_dir().c_str());
|
svr_.set_base_dir(config_->get_http_base_dir().c_str());
|
||||||
|
|
||||||
svr_.Get("(/|/Config|/PTP|/Sources|/Sinks)", [&](const Request& req, Response& res) {
|
svr_.Get("(/|/Config|/PTP|/Sources|/Sinks|/Browser)", [&](const Request& req, Response& res) {
|
||||||
std::ifstream file(config_->get_http_base_dir() + "/index.html");
|
std::ifstream file(config_->get_http_base_dir() + "/index.html");
|
||||||
std::stringstream buffer;
|
std::stringstream buffer;
|
||||||
buffer << file.rdbuf();
|
buffer << file.rdbuf();
|
||||||
@ -280,6 +280,13 @@ bool HttpServer::start() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* get remote sources */
|
||||||
|
svr_.Get("/api/browse/sources", [this](const Request& req, Response& res) {
|
||||||
|
auto const sources = browser_->get_remote_sources();
|
||||||
|
set_headers(res, "application/json");
|
||||||
|
res.body = remote_sources_to_json(sources);
|
||||||
|
});
|
||||||
|
|
||||||
svr_.set_logger([](const Request& req, const Response& res) {
|
svr_.set_logger([](const Request& req, const Response& res) {
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
BOOST_LOG_TRIVIAL(info) << "http_server:: " << req.method << " "
|
BOOST_LOG_TRIVIAL(info) << "http_server:: " << req.method << " "
|
||||||
|
@ -24,19 +24,23 @@
|
|||||||
|
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
#include "session_manager.hpp"
|
#include "session_manager.hpp"
|
||||||
|
#include "browser.hpp"
|
||||||
|
|
||||||
class HttpServer {
|
class HttpServer {
|
||||||
public:
|
public:
|
||||||
HttpServer() = delete;
|
HttpServer() = delete;
|
||||||
HttpServer(std::shared_ptr<SessionManager> session_manager,
|
HttpServer(std::shared_ptr<SessionManager> session_manager,
|
||||||
|
std::shared_ptr<Browser> browser,
|
||||||
std::shared_ptr<Config> config)
|
std::shared_ptr<Config> config)
|
||||||
: session_manager_(session_manager),
|
: session_manager_(session_manager),
|
||||||
|
browser_(browser),
|
||||||
config_(config) {};
|
config_(config) {};
|
||||||
bool start();
|
bool start();
|
||||||
bool stop();
|
bool stop();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::shared_ptr<SessionManager> session_manager_;
|
std::shared_ptr<SessionManager> session_manager_;
|
||||||
|
std::shared_ptr<Browser> browser_;
|
||||||
std::shared_ptr<Config> config_;
|
std::shared_ptr<Config> config_;
|
||||||
httplib::Server svr_;
|
httplib::Server svr_;
|
||||||
std::future<bool> res_;
|
std::future<bool> res_;
|
||||||
|
@ -85,6 +85,7 @@ std::string config_to_json(const Config& config) {
|
|||||||
<< ",\n \"rtp_port\": " << config.get_rtp_port()
|
<< ",\n \"rtp_port\": " << config.get_rtp_port()
|
||||||
<< ",\n \"ptp_domain\": " << unsigned(config.get_ptp_domain())
|
<< ",\n \"ptp_domain\": " << unsigned(config.get_ptp_domain())
|
||||||
<< ",\n \"ptp_dscp\": " << unsigned(config.get_ptp_dscp())
|
<< ",\n \"ptp_dscp\": " << unsigned(config.get_ptp_dscp())
|
||||||
|
<< ",\n \"sap_mcast_addr\": \"" << escape_json(config.get_sap_mcast_addr()) << "\""
|
||||||
<< ",\n \"sap_interval\": " << config.get_sap_interval()
|
<< ",\n \"sap_interval\": " << config.get_sap_interval()
|
||||||
<< ",\n \"syslog_proto\": \"" << escape_json(config.get_syslog_proto()) << "\""
|
<< ",\n \"syslog_proto\": \"" << escape_json(config.get_syslog_proto()) << "\""
|
||||||
<< ",\n \"syslog_server\": \"" << escape_json(config.get_syslog_server()) << "\""
|
<< ",\n \"syslog_server\": \"" << escape_json(config.get_syslog_server()) << "\""
|
||||||
@ -226,6 +227,34 @@ std::string streams_to_json(const std::list<StreamSource>& sources,
|
|||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string remote_source_to_json(const RemoteSource& source) {
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "\n {"
|
||||||
|
<< "\n \"source\": \"" << escape_json(source.source) << "\""
|
||||||
|
<< ",\n \"id\": \"" << escape_json(source.id) << "\""
|
||||||
|
<< ",\n \"name\": \"" << escape_json(source.name) << "\""
|
||||||
|
<< ",\n \"address\": \"" << escape_json(source.address) << "\""
|
||||||
|
<< ",\n \"sdp\": \"" << escape_json(source.sdp) << "\""
|
||||||
|
<< ",\n \"last_seen\": " << unsigned(source.last_seen)
|
||||||
|
<< ",\n \"announce_period\": " << unsigned(source.announce_period)
|
||||||
|
<< " \n }";
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string remote_sources_to_json(const std::list<RemoteSource>& sources) {
|
||||||
|
int count = 0;
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "{\n \"remote_sources\": [";
|
||||||
|
for (auto const& source: sources) {
|
||||||
|
if (count++) {
|
||||||
|
ss << ", ";
|
||||||
|
}
|
||||||
|
ss << remote_source_to_json(source);
|
||||||
|
}
|
||||||
|
ss << " ]\n}\n";
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
Config json_to_config_(std::istream& js, Config& config) {
|
Config json_to_config_(std::istream& js, Config& config) {
|
||||||
try {
|
try {
|
||||||
boost::property_tree::ptree pt;
|
boost::property_tree::ptree pt;
|
||||||
@ -256,6 +285,8 @@ Config json_to_config_(std::istream& js, Config& config) {
|
|||||||
config.set_ptp_domain(val.get_value<uint8_t>());
|
config.set_ptp_domain(val.get_value<uint8_t>());
|
||||||
} else if (key == "ptp_dscp") {
|
} else if (key == "ptp_dscp") {
|
||||||
config.set_ptp_dscp(val.get_value<uint8_t>());
|
config.set_ptp_dscp(val.get_value<uint8_t>());
|
||||||
|
} else if (key == "sap_mcast_addr") {
|
||||||
|
config.set_sap_mcast_addr(remove_undesired_chars(val.get_value<std::string>()));
|
||||||
} else if (key == "sap_interval") {
|
} else if (key == "sap_interval") {
|
||||||
config.set_sap_interval(val.get_value<uint16_t>());
|
config.set_sap_interval(val.get_value<uint16_t>());
|
||||||
} else if (key == "status_file") {
|
} else if (key == "status_file") {
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
#include "session_manager.hpp"
|
#include "session_manager.hpp"
|
||||||
|
#include "browser.hpp"
|
||||||
|
|
||||||
/* JSON serializers */
|
/* JSON serializers */
|
||||||
std::string config_to_json(const Config& config);
|
std::string config_to_json(const Config& config);
|
||||||
@ -34,6 +35,8 @@ std::string sources_to_json(const std::list<StreamSource>& sources);
|
|||||||
std::string sinks_to_json(const std::list<StreamSink>& sinks);
|
std::string sinks_to_json(const std::list<StreamSink>& sinks);
|
||||||
std::string streams_to_json(const std::list<StreamSource>& sources,
|
std::string streams_to_json(const std::list<StreamSource>& sources,
|
||||||
const std::list<StreamSink>& sinks);
|
const std::list<StreamSink>& sinks);
|
||||||
|
std::string remote_source_to_json(const RemoteSource& source);
|
||||||
|
std::string remote_sources_to_json(const std::list<RemoteSource>& sources);
|
||||||
|
|
||||||
/* JSON deserializers */
|
/* JSON deserializers */
|
||||||
Config json_to_config(std::istream& jstream, const Config& curCconfig);
|
Config json_to_config(std::istream& jstream, const Config& curCconfig);
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
#include "session_manager.hpp"
|
#include "session_manager.hpp"
|
||||||
#include "interface.hpp"
|
#include "interface.hpp"
|
||||||
|
#include "browser.hpp"
|
||||||
|
|
||||||
namespace po = boost::program_options;
|
namespace po = boost::program_options;
|
||||||
namespace postyle = boost::program_options::command_line_style;
|
namespace postyle = boost::program_options::command_line_style;
|
||||||
@ -115,8 +116,15 @@ int main(int argc, char* argv[]) {
|
|||||||
std::string("SessionManager:: start failed"));
|
std::string("SessionManager:: start failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* start browser */
|
||||||
|
auto browser = Browser::create(config);
|
||||||
|
if (browser == nullptr || !browser->start()) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("Browser:: start failed"));
|
||||||
|
}
|
||||||
|
|
||||||
/* start http server */
|
/* start http server */
|
||||||
HttpServer http_server(session_manager, config);
|
HttpServer http_server(session_manager, browser, config);
|
||||||
if (!http_server.start()) {
|
if (!http_server.start()) {
|
||||||
throw std::runtime_error(std::string("HttpServer:: start failed"));
|
throw std::runtime_error(std::string("HttpServer:: start failed"));
|
||||||
}
|
}
|
||||||
@ -151,6 +159,12 @@ int main(int argc, char* argv[]) {
|
|||||||
std::string("HttpServer:: stop failed"));
|
std::string("HttpServer:: stop failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* stop browser */
|
||||||
|
if (!browser->stop()) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("Browser:: stop failed"));
|
||||||
|
}
|
||||||
|
|
||||||
/* stop session manager */
|
/* stop session manager */
|
||||||
if (!session_manager->stop()) {
|
if (!session_manager->stop()) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
@ -161,7 +175,7 @@ int main(int argc, char* argv[]) {
|
|||||||
if (!driver->terminate()) {
|
if (!driver->terminate()) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
std::string("DriverManager:: terminate failed"));
|
std::string("DriverManager:: terminate failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (std::exception& e) {
|
} catch (std::exception& e) {
|
||||||
BOOST_LOG_TRIVIAL(fatal) << "main:: fatal exception error: " << e.what();
|
BOOST_LOG_TRIVIAL(fatal) << "main:: fatal exception error: " << e.what();
|
||||||
|
@ -59,8 +59,7 @@ class nl_endpoint {
|
|||||||
|
|
||||||
std::size_t size() const { return sizeof(sockaddr); }
|
std::size_t size() const { return sizeof(sockaddr); }
|
||||||
|
|
||||||
void resize(std::size_t size) { /* nothing we can do here */
|
void resize(std::size_t size) { /* nothing we can do here */ }
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t capacity() const { return sizeof(sockaddr); }
|
std::size_t capacity() const { return sizeof(sockaddr); }
|
||||||
};
|
};
|
||||||
|
158
daemon/sap.cpp
Normal file
158
daemon/sap.cpp
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
//
|
||||||
|
// sap.hpp
|
||||||
|
//
|
||||||
|
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <boost/bind.hpp>
|
||||||
|
#include "sap.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
using namespace boost::asio;
|
||||||
|
using namespace boost::asio::ip;
|
||||||
|
|
||||||
|
|
||||||
|
SAP::SAP(const std::string& sap_mcast_addr) :
|
||||||
|
addr_(sap_mcast_addr)
|
||||||
|
// remote_endpoint_(ip::address::from_string(addr_), port)
|
||||||
|
{
|
||||||
|
socket_.open(boost::asio::ip::udp::v4());
|
||||||
|
socket_.set_option(udp::socket::reuse_address(true));
|
||||||
|
socket_.bind(listen_endpoint_);
|
||||||
|
check_deadline();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SAP::set_multicast_interface(const std::string& interface_ip) {
|
||||||
|
ip::address_v4 local_interface = ip::address_v4::from_string(interface_ip);
|
||||||
|
ip::multicast::outbound_interface oi_option(local_interface);
|
||||||
|
boost::system::error_code ec;
|
||||||
|
socket_.set_option(oi_option, ec);
|
||||||
|
if (ec) {
|
||||||
|
BOOST_LOG_TRIVIAL(error)
|
||||||
|
<< "sap::outbound_interface option " << ec.message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ip::multicast::enable_loopback el_option(true);
|
||||||
|
socket_.set_option(el_option, ec);
|
||||||
|
if (ec) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "sap::enable_loopback option " << ec.message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SAP::announcement(uint16_t msg_id_hash,
|
||||||
|
uint32_t addr,
|
||||||
|
const std::string& sdp) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "sap::announcement " << std::hex << msg_id_hash;
|
||||||
|
return send(true, msg_id_hash, htonl(addr), sdp);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SAP::deletion(uint16_t msg_id_hash,
|
||||||
|
uint32_t addr,
|
||||||
|
const std::string& sdp) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "sap::deletion " << std::hex << msg_id_hash;
|
||||||
|
return send(false, msg_id_hash, htonl(addr), sdp);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SAP::receive(bool& is_announce,
|
||||||
|
uint16_t& msg_id_hash,
|
||||||
|
uint32_t& addr,
|
||||||
|
std::string& sdp,
|
||||||
|
int tout_secs) {
|
||||||
|
// Set a deadline for the asynchronous operation
|
||||||
|
deadline_.expires_from_now(boost::posix_time::seconds(tout_secs));
|
||||||
|
|
||||||
|
boost::system::error_code ec = boost::asio::error::would_block;
|
||||||
|
ip::udp::endpoint endpoint;
|
||||||
|
uint8_t buffer[max_length];
|
||||||
|
std::size_t length = 0;
|
||||||
|
// Start the asynchronous operation itself. The handle_receive function
|
||||||
|
// used as a callback will update the ec and length variables.
|
||||||
|
socket_.async_receive_from(
|
||||||
|
boost::asio::buffer(buffer), endpoint,
|
||||||
|
boost::bind(&SAP::handle_receive, _1, _2, &ec, &length));
|
||||||
|
|
||||||
|
// Block until the asynchronous operation has completed.
|
||||||
|
do {
|
||||||
|
io_service_.run_one();
|
||||||
|
} while (ec == boost::asio::error::would_block);
|
||||||
|
|
||||||
|
|
||||||
|
if (!ec && length > 4 && (buffer[0] == 0x20 || buffer[0] == 0x24)) {
|
||||||
|
// only accept SAP announce or delete v2 with IPv4
|
||||||
|
// no reserved, no compress, no encryption
|
||||||
|
// and content/type = application/sdp
|
||||||
|
is_announce = (buffer[0] == 0x20);
|
||||||
|
memcpy(&msg_id_hash, buffer + 2, sizeof(msg_id_hash));
|
||||||
|
memcpy(&addr, buffer + 4, sizeof(addr));
|
||||||
|
for (int i = 8; buffer[i] != 0 && i < length; i++) {
|
||||||
|
buffer[i] = std::tolower(buffer[i]);
|
||||||
|
}
|
||||||
|
if (!memcmp(buffer + 8, "application/sdp", 16)) {
|
||||||
|
sdp.assign(buffer + SAP::sap_header_len, buffer + length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SAP::handle_receive(const boost::system::error_code& ec,
|
||||||
|
std::size_t length,
|
||||||
|
boost::system::error_code* out_ec,
|
||||||
|
std::size_t* out_length) {
|
||||||
|
*out_ec = ec;
|
||||||
|
*out_length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SAP::check_deadline() {
|
||||||
|
if (deadline_.expires_at() <= deadline_timer::traits_type::now()) {
|
||||||
|
// cancel receive operation
|
||||||
|
socket_.cancel();
|
||||||
|
deadline_.expires_at(boost::posix_time::pos_infin);
|
||||||
|
//BOOST_LOG_TRIVIAL(debug) << "SAP:: timeout expired when receiving";
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline_.async_wait(boost::bind(&SAP::check_deadline, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SAP::send(bool is_announce,
|
||||||
|
uint16_t msg_id_hash,
|
||||||
|
uint32_t addr,
|
||||||
|
const std::string& sdp) {
|
||||||
|
if (sdp.length() > max_length - sap_header_len) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "sap:: SDP is too long";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint8_t buffer[max_length];
|
||||||
|
|
||||||
|
buffer[0] = is_announce ? 0x20 : 0x24;
|
||||||
|
buffer[1] = 0;
|
||||||
|
memcpy(buffer + 2, &msg_id_hash, 2);
|
||||||
|
memcpy(buffer + 4, &addr, 4);
|
||||||
|
memcpy(buffer + 8, "application/sdp", 16); /* include trailing 0 */
|
||||||
|
memcpy(buffer + sap_header_len, sdp.c_str(), sdp.length());
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket_.send_to(boost::asio::buffer(buffer, sap_header_len + sdp.length()),
|
||||||
|
remote_endpoint_);
|
||||||
|
} catch (boost::system::error_code& ec) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "sap::send_to " << ec.message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
@ -24,10 +24,10 @@
|
|||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
|
|
||||||
using namespace boost::asio;
|
using namespace boost::asio;
|
||||||
|
using boost::asio::deadline_timer;
|
||||||
|
|
||||||
class SAP {
|
class SAP {
|
||||||
public:
|
public:
|
||||||
constexpr static const char addr[] = "224.2.127.254";
|
|
||||||
constexpr static uint16_t port = 9875;
|
constexpr static uint16_t port = 9875;
|
||||||
constexpr static uint16_t max_deletions = 3;
|
constexpr static uint16_t max_deletions = 3;
|
||||||
constexpr static uint16_t bandwidth_limit = 4000; // bits x xsec
|
constexpr static uint16_t bandwidth_limit = 4000; // bits x xsec
|
||||||
@ -35,72 +35,39 @@ class SAP {
|
|||||||
constexpr static uint16_t sap_header_len = 24;
|
constexpr static uint16_t sap_header_len = 24;
|
||||||
constexpr static uint16_t max_length = 4096;
|
constexpr static uint16_t max_length = 4096;
|
||||||
|
|
||||||
SAP() { socket_.open(boost::asio::ip::udp::v4()); };
|
SAP() = delete;
|
||||||
|
SAP(const std::string& sap_mcast_addr);
|
||||||
bool set_multicast_interface(const std::string& interface_ip) {
|
|
||||||
ip::address_v4 local_interface = ip::address_v4::from_string(interface_ip);
|
|
||||||
ip::multicast::outbound_interface oi_option(local_interface);
|
|
||||||
boost::system::error_code ec;
|
|
||||||
socket_.set_option(oi_option, ec);
|
|
||||||
if (ec) {
|
|
||||||
BOOST_LOG_TRIVIAL(error)
|
|
||||||
<< "sap::outbound_interface option " << ec.message();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ip::multicast::enable_loopback el_option(true);
|
|
||||||
socket_.set_option(el_option, ec);
|
|
||||||
if (ec) {
|
|
||||||
BOOST_LOG_TRIVIAL(error)
|
|
||||||
<< "sap::enable_loopback option " << ec.message();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
bool set_multicast_interface(const std::string& interface_ip);
|
||||||
bool announcement(uint16_t msg_id_hash,
|
bool announcement(uint16_t msg_id_hash,
|
||||||
uint32_t addr,
|
uint32_t addr,
|
||||||
const std::string& sdp) {
|
const std::string& sdp);
|
||||||
BOOST_LOG_TRIVIAL(info) << "sap::announcement " << std::hex << msg_id_hash;
|
bool deletion(uint16_t msg_id_hash, uint32_t addr, const std::string& sdp);
|
||||||
return send(true, msg_id_hash, htonl(addr), sdp);
|
bool receive(bool& is_announce,
|
||||||
}
|
uint16_t& msg_id_hash,
|
||||||
|
uint32_t& addr,
|
||||||
bool deletion(uint16_t msg_id_hash, uint32_t addr, const std::string& sdp) {
|
std::string& sdp,
|
||||||
BOOST_LOG_TRIVIAL(info) << "sap::deletetion " << std::hex << msg_id_hash;
|
int tout_secs = 1);
|
||||||
return send(false, msg_id_hash, htonl(addr), sdp);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static void handle_receive(const boost::system::error_code& ec,
|
||||||
|
std::size_t length,
|
||||||
|
boost::system::error_code* out_ec,
|
||||||
|
std::size_t* out_length);
|
||||||
|
void check_deadline();
|
||||||
bool send(bool is_announce,
|
bool send(bool is_announce,
|
||||||
uint16_t msg_id_hash,
|
uint16_t msg_id_hash,
|
||||||
uint32_t addr,
|
uint32_t addr,
|
||||||
const std::string& sdp) {
|
const std::string& sdp);
|
||||||
if (sdp.length() > max_length - sap_header_len) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "sap:: SDP is too long";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
uint8_t buffer[max_length];
|
|
||||||
|
|
||||||
buffer[0] = is_announce ? 0x20 : 0x24;
|
|
||||||
buffer[1] = 0;
|
|
||||||
memcpy(buffer + 2, &msg_id_hash, 2);
|
|
||||||
memcpy(buffer + 4, &addr, 4);
|
|
||||||
memcpy(buffer + 8, "application/sdp", 16); /* include trailing 0 */
|
|
||||||
memcpy(buffer + sap_header_len, sdp.c_str(), sdp.length());
|
|
||||||
|
|
||||||
try {
|
|
||||||
socket_.send_to(
|
|
||||||
boost::asio::buffer(buffer, sap_header_len + sdp.length()), remote_);
|
|
||||||
} catch (boost::system::error_code& ec) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "sap::send_to " << ec.message();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
std::string addr_;
|
||||||
io_service io_service_;
|
io_service io_service_;
|
||||||
ip::udp::socket socket_{io_service_};
|
ip::udp::socket socket_{io_service_};
|
||||||
ip::udp::endpoint remote_{
|
ip::udp::endpoint remote_endpoint_{
|
||||||
ip::udp::endpoint(ip::address::from_string(addr), port)};
|
ip::udp::endpoint(ip::address::from_string(addr_), port)};
|
||||||
|
ip::udp::endpoint listen_endpoint_{
|
||||||
|
ip::udp::endpoint(ip::address::from_string("0.0.0.0"), port)};
|
||||||
|
deadline_timer deadline_{io_service_};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -190,7 +190,7 @@ class SessionManager {
|
|||||||
PTPStatus ptp_status_;
|
PTPStatus ptp_status_;
|
||||||
mutable std::shared_mutex ptp_mutex_;
|
mutable std::shared_mutex ptp_mutex_;
|
||||||
|
|
||||||
SAP sap_;
|
SAP sap_{config_->get_sap_mcast_addr()};
|
||||||
IGMP igmp_;
|
IGMP igmp_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"rtp_port": 6004,
|
"rtp_port": 6004,
|
||||||
"ptp_domain": 0,
|
"ptp_domain": 0,
|
||||||
"ptp_dscp": 46,
|
"ptp_dscp": 46,
|
||||||
|
"sap_mcast_addr": "224.2.127.254",
|
||||||
"sap_interval": 1,
|
"sap_interval": 1,
|
||||||
"syslog_proto": "none",
|
"syslog_proto": "none",
|
||||||
"syslog_server": "255.255.255.254:1234",
|
"syslog_server": "255.255.255.254:1234",
|
||||||
|
@ -50,7 +50,7 @@ struct DaemonInstance {
|
|||||||
DaemonInstance() {
|
DaemonInstance() {
|
||||||
BOOST_TEST_MESSAGE("Starting up test daemon instance ...");
|
BOOST_TEST_MESSAGE("Starting up test daemon instance ...");
|
||||||
int retry = 10;
|
int retry = 10;
|
||||||
while (--retry && daemon_.running()) {
|
while (retry-- && daemon_.running()) {
|
||||||
BOOST_TEST_MESSAGE("Checking daemon instance ...");
|
BOOST_TEST_MESSAGE("Checking daemon instance ...");
|
||||||
httplib::Client cli(g_daemon_address, g_daemon_port);
|
httplib::Client cli(g_daemon_address, g_daemon_port);
|
||||||
auto res = cli.Get("/");
|
auto res = cli.Get("/");
|
||||||
@ -284,6 +284,12 @@ struct Client {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> get_remote_sources() {
|
||||||
|
std::string url = std::string("/api/browse/sources");
|
||||||
|
auto res = cli_.Get(url.c_str());
|
||||||
|
return std::make_pair(res->status == 200, res->body);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
httplib::Client cli_{g_daemon_address, g_daemon_port};
|
httplib::Client cli_{g_daemon_address, g_daemon_port};
|
||||||
io_service io_service_;
|
io_service io_service_;
|
||||||
@ -432,7 +438,30 @@ BOOST_AUTO_TEST_CASE(source_check_sap) {
|
|||||||
cli.sap_wait_announcement(0, sdp.second);
|
cli.sap_wait_announcement(0, sdp.second);
|
||||||
BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0");
|
BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0");
|
||||||
cli.sap_wait_deletion(0, sdp.second, 3);
|
cli.sap_wait_deletion(0, sdp.second, 3);
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(10));
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(source_check_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");
|
||||||
|
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<std::string>("sdp") == sdp.second,
|
||||||
|
"returned source " + v.second.get<std::string>("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");
|
||||||
|
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_AUTO_TEST_CASE(sink_check_status) {
|
BOOST_AUTO_TEST_CASE(sink_check_status) {
|
||||||
@ -578,7 +607,7 @@ BOOST_AUTO_TEST_CASE(add_remove_update_check_all) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOST_AUTO_TEST_CASE(add_remove_check_sap_all) {
|
BOOST_AUTO_TEST_CASE(add_remove_check_sap_browser_all) {
|
||||||
Client cli;
|
Client cli;
|
||||||
for (int id = 0; id < 64; id++) {
|
for (int id = 0; id < 64; id++) {
|
||||||
BOOST_REQUIRE_MESSAGE(cli.add_source(id),
|
BOOST_REQUIRE_MESSAGE(cli.add_source(id),
|
||||||
@ -589,15 +618,20 @@ BOOST_AUTO_TEST_CASE(add_remove_check_sap_all) {
|
|||||||
BOOST_REQUIRE_MESSAGE(sdp.first, std::string("got source sdp ") + std::to_string(id));
|
BOOST_REQUIRE_MESSAGE(sdp.first, std::string("got source sdp ") + std::to_string(id));
|
||||||
cli.sap_wait_announcement(id, sdp.second);
|
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++) {
|
for (int id = 0; id < 64; id++) {
|
||||||
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id),
|
BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(id),
|
||||||
std::string("added sink ") + std::to_string(id));
|
std::string("added sink ") + std::to_string(id));
|
||||||
}
|
}
|
||||||
auto json = cli.get_streams();
|
json = cli.get_streams();
|
||||||
BOOST_REQUIRE_MESSAGE(json.first, "got streams");
|
BOOST_REQUIRE_MESSAGE(json.first, "got streams");
|
||||||
boost::property_tree::ptree pt;
|
std::stringstream ss1(json.second);
|
||||||
std::stringstream ss(json.second);
|
boost::property_tree::read_json(ss1, pt);
|
||||||
boost::property_tree::read_json(ss, pt);
|
|
||||||
uint8_t id = 0;
|
uint8_t id = 0;
|
||||||
BOOST_FOREACH (auto const& v, pt.get_child("sources")) {
|
BOOST_FOREACH (auto const& v, pt.get_child("sources")) {
|
||||||
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
|
BOOST_REQUIRE_MESSAGE(v.second.get<uint8_t>("id") == id,
|
||||||
@ -615,6 +649,11 @@ BOOST_AUTO_TEST_CASE(add_remove_check_sap_all) {
|
|||||||
std::string("removed source ") + std::to_string(id));
|
std::string("removed source ") + std::to_string(id));
|
||||||
}
|
}
|
||||||
cli.sap_wait_all_deletions();
|
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++) {
|
for (int id = 0; id < 64; id++) {
|
||||||
BOOST_REQUIRE_MESSAGE(cli.remove_sink(id),
|
BOOST_REQUIRE_MESSAGE(cli.remove_sink(id),
|
||||||
std::string("removed sink ") + std::to_string(id));
|
std::string("removed sink ") + std::to_string(id));
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>React App</title>
|
<title>AES64 Daemon WebUI</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
BIN
webui/public/info.png
Executable file
BIN
webui/public/info.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
@ -78,6 +78,7 @@ class Config extends Component {
|
|||||||
rtpPort: data.rtp_port,
|
rtpPort: data.rtp_port,
|
||||||
ptpDomain: data.ptp_domain,
|
ptpDomain: data.ptp_domain,
|
||||||
ptpDscp: data.ptp_dscp,
|
ptpDscp: data.ptp_dscp,
|
||||||
|
sapMcastAddr: data.sap_mcast_addr,
|
||||||
sapInterval: data.sap_interval,
|
sapInterval: data.sap_interval,
|
||||||
syslogProto: data.syslog_proto,
|
syslogProto: data.syslog_proto,
|
||||||
syslogServer: data.syslog_server,
|
syslogServer: data.syslog_server,
|
||||||
@ -95,6 +96,7 @@ class Config extends Component {
|
|||||||
!this.state.ticFrameSizeAt1fsErr &&
|
!this.state.ticFrameSizeAt1fsErr &&
|
||||||
!this.state.maxTicFrameSizeErr &&
|
!this.state.maxTicFrameSizeErr &&
|
||||||
!this.state.rtpMcastBaseErr &&
|
!this.state.rtpMcastBaseErr &&
|
||||||
|
!this.state.sapMcastAddrErr &&
|
||||||
!this.state.rtpPortErr &&
|
!this.state.rtpPortErr &&
|
||||||
!this.state.sapIntervalErr &&
|
!this.state.sapIntervalErr &&
|
||||||
!this.state.syslogServerErr &&
|
!this.state.syslogServerErr &&
|
||||||
@ -113,6 +115,7 @@ class Config extends Component {
|
|||||||
this.state.ticFrameSizeAt1fs,
|
this.state.ticFrameSizeAt1fs,
|
||||||
this.state.sampleRate,
|
this.state.sampleRate,
|
||||||
this.state.maxTicFrameSize,
|
this.state.maxTicFrameSize,
|
||||||
|
this.state.sapMcastAddr,
|
||||||
this.state.sapInterval)
|
this.state.sapInterval)
|
||||||
.then(response => toast.success('Config updated, daemon restart ...'));
|
.then(response => toast.success('Config updated, daemon restart ...'));
|
||||||
}
|
}
|
||||||
@ -154,12 +157,16 @@ class Config extends Component {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="left"> <label>RTP base address</label> </th>
|
<th align="left"> <label>RTP base address</label> </th>
|
||||||
<th align="left"> <input type="text" minLength="7" maxLength="15" size="15" pattern="^((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$" value={this.state.rtpMcastBase} onChange={e => this.setState({rtpMcastBase: e.target.value, rtpMcastBaseErr: !e.currentTarget.checkValidity()})} required/> </th>
|
<th align="left"> <input type="text" minLength="7" maxLength="15" size="15" pattern="^2(?:2[4-9]|3\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d?|0)){3}$" value={this.state.rtpMcastBase} onChange={e => this.setState({rtpMcastBase: e.target.value, rtpMcastBaseErr: !e.currentTarget.checkValidity()})} required/> </th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="left"> <label>RTP port</label> </th>
|
<th align="left"> <label>RTP port</label> </th>
|
||||||
<th align="left"> <input type='number' min='1024' max='65536' className='input-number' value={this.state.rtpPort} onChange={e => this.setState({rtpPort: e.target.value, rtpPortErr: !e.currentTarget.checkValidity()})} required/> </th>
|
<th align="left"> <input type='number' min='1024' max='65536' className='input-number' value={this.state.rtpPort} onChange={e => this.setState({rtpPort: e.target.value, rtpPortErr: !e.currentTarget.checkValidity()})} required/> </th>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th align="left"> <label>SAP multicast address</label> </th>
|
||||||
|
<th align="left"> <input type="text" minLength="7" maxLength="15" size="15" pattern="^2(?:2[4-9]|3\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d?|0)){3}$" value={this.state.sapMcastAddr} onChange={e => this.setState({sapMcastAddr: e.target.value, sapMcastAddrErr: !e.currentTarget.checkValidity()})} required/> </th>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="left"> <label>SAP interval (sec)</label> </th>
|
<th align="left"> <label>SAP interval (sec)</label> </th>
|
||||||
<th align="left"> <input type='number' min='0' max='255' className='input-number' value={this.state.sapInterval} onChange={e => this.setState({sapInterval: e.target.value, sapIntervalErr: !e.currentTarget.checkValidity()})} required/> </th>
|
<th align="left"> <input type='number' min='0' max='255' className='input-number' value={this.state.sapInterval} onChange={e => this.setState({sapInterval: e.target.value, sapIntervalErr: !e.currentTarget.checkValidity()})} required/> </th>
|
||||||
|
@ -26,6 +26,7 @@ import PTP from './PTP';
|
|||||||
import Config from './Config';
|
import Config from './Config';
|
||||||
import Sources from './Sources';
|
import Sources from './Sources';
|
||||||
import Sinks from './Sinks';
|
import Sinks from './Sinks';
|
||||||
|
import RemoteSources from './RemoteSources';
|
||||||
|
|
||||||
require('./styles.css');
|
require('./styles.css');
|
||||||
|
|
||||||
@ -51,6 +52,9 @@ class ConfigTabs extends Component {
|
|||||||
<div label="Sinks">
|
<div label="Sinks">
|
||||||
<Sinks/>
|
<Sinks/>
|
||||||
</div>
|
</div>
|
||||||
|
<div label="Browser">
|
||||||
|
<RemoteSources/>
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
196
webui/src/RemoteSources.js
Normal file
196
webui/src/RemoteSources.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
//
|
||||||
|
// RemoteSource.js
|
||||||
|
//
|
||||||
|
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import RestAPI from './Services';
|
||||||
|
import Loader from './Loader';
|
||||||
|
import SourceInfo from './SourceInfo';
|
||||||
|
|
||||||
|
require('./styles.css');
|
||||||
|
|
||||||
|
class RemoteSourceEntry extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
source: PropTypes.string.isRequired,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
address: PropTypes.string.isRequired,
|
||||||
|
sdp: PropTypes.string.isRequired,
|
||||||
|
last_seen: PropTypes.number.isRequired,
|
||||||
|
period: PropTypes.number.isRequired,
|
||||||
|
onInfoClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
rtp_address: 'n/a',
|
||||||
|
port: 'n/a'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInfoClick = () => {
|
||||||
|
this.props.onInfoClick(this.props.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
var rtp_address = this.props.sdp.match(/(c=IN IP4 )([0-9.]+)/g);
|
||||||
|
var port = this.props.sdp.match(/(m=audio )([0-9]+)/g);
|
||||||
|
if (rtp_address && port) {
|
||||||
|
this.setState({ rtp_address: rtp_address[0].substr(9), port: port[0].substr(8) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<tr className='tr-browser'>
|
||||||
|
<td> <label>{this.props.source}</label> </td>
|
||||||
|
<td> <label>{this.props.address}</label> </td>
|
||||||
|
<td> <label>{this.props.name}</label> </td>
|
||||||
|
<td> <label>{this.state.rtp_address}</label> </td>
|
||||||
|
<td align='center'> <label>{this.state.port}</label> </td>
|
||||||
|
<td align='center'> <label>{this.props.last_seen}</label> </td>
|
||||||
|
<td align='center'> <label>{this.props.period}</label> </td>
|
||||||
|
<td> <span className='pointer-area' onClick={this.handleInfoClick}> <img width='20' height='20' src='/info.png' alt=''/> </span> </td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteSourceList extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
onReloadClick: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
handleReloadClick = () => {
|
||||||
|
this.props.onReloadClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id='remote-sources-table'>
|
||||||
|
<table className="table-stream"><tbody>
|
||||||
|
{this.props.sources.length > 0 ?
|
||||||
|
<tr className='tr-stream'>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>RTP Address</th>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>Last seen</th>
|
||||||
|
<th>Period</th>
|
||||||
|
</tr>
|
||||||
|
: <tr>
|
||||||
|
<th>No remote sources found.</th>
|
||||||
|
</tr> }
|
||||||
|
{this.props.sources}
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
<span className='pointer-area' onClick={this.handleReloadClick}> <img width='30' height='30' src='/reload.png' alt=''/> </span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteSources extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
sources: [],
|
||||||
|
isLoading: false,
|
||||||
|
infoIsOpen: false,
|
||||||
|
infoTitle: ''
|
||||||
|
};
|
||||||
|
this.onInfoClick = this.onInfoClick.bind(this);
|
||||||
|
this.onReloadClick = this.onReloadClick.bind(this);
|
||||||
|
this.openInfo = this.openInfo.bind(this);
|
||||||
|
this.closeInfo = this.closeInfo.bind(this);
|
||||||
|
this.fetchRemoteSources = this.fetchRemoteSources.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRemoteSources() {
|
||||||
|
this.setState({isLoading: true});
|
||||||
|
RestAPI.getRemoteSources()
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(
|
||||||
|
data => this.setState( { sources: data.remote_sources, isLoading: false }))
|
||||||
|
.catch(err => this.setState( { isLoading: false } ));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetchRemoteSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
openInfo(title, source) {
|
||||||
|
this.setState({infoIsOpen: true, infoTitle: title, source: source});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeInfo() {
|
||||||
|
this.setState({infoIsOpen: false});
|
||||||
|
this.fetchRemoteSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
onInfoClick(id) {
|
||||||
|
const source = this.state.sources.find(s => s.id === id);
|
||||||
|
this.openInfo("Announced Source Info", source, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onReloadClick() {
|
||||||
|
this.fetchRemoteSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.state.sources.sort((a, b) => (a.id > b.id) ? 1 : -1);
|
||||||
|
const sources = this.state.sources.map((source) => (
|
||||||
|
<RemoteSourceEntry key={source.id}
|
||||||
|
source={source.source}
|
||||||
|
id={source.id}
|
||||||
|
address={source.address}
|
||||||
|
name={source.name}
|
||||||
|
sdp={source.sdp}
|
||||||
|
last_seen={source.last_seen}
|
||||||
|
period={source.announce_period}
|
||||||
|
onInfoClick={this.onInfoClick}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div id='sources'>
|
||||||
|
{ this.state.isLoading ? <Loader/>
|
||||||
|
: <RemoteSourceList
|
||||||
|
onReloadClick={this.onReloadClick}
|
||||||
|
sources={sources} /> }
|
||||||
|
{ this.state.infoIsOpen ?
|
||||||
|
<SourceInfo infoIsOpen={this.state.infoIsOpen}
|
||||||
|
closeInfo={this.closeInfo}
|
||||||
|
infoTitle={this.state.infoTitle}
|
||||||
|
id={this.state.source.id}
|
||||||
|
address={this.state.source.address}
|
||||||
|
name={this.state.source.name}
|
||||||
|
sdp={this.state.source.sdp} />
|
||||||
|
: undefined }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoteSources;
|
@ -31,6 +31,7 @@ const source = '/source';
|
|||||||
const sdp = '/sdp';
|
const sdp = '/sdp';
|
||||||
const sink = '/sink';
|
const sink = '/sink';
|
||||||
const status = '/status';
|
const status = '/status';
|
||||||
|
const browseSources = '/browse/sources';
|
||||||
|
|
||||||
const defaultParams = {
|
const defaultParams = {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@ -75,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_interval) {
|
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) {
|
||||||
return this.doFetch(config, {
|
return this.doFetch(config, {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
log_severity: parseInt(log_severity, 10),
|
log_severity: parseInt(log_severity, 10),
|
||||||
@ -87,6 +88,7 @@ export default class RestAPI {
|
|||||||
tic_frame_size_at_1fs: parseInt(tic_frame_size_at_1fs, 10),
|
tic_frame_size_at_1fs: parseInt(tic_frame_size_at_1fs, 10),
|
||||||
sample_rate: parseInt(sample_rate, 10),
|
sample_rate: parseInt(sample_rate, 10),
|
||||||
max_tic_frame_size: parseInt(max_tic_frame_size, 10),
|
max_tic_frame_size: parseInt(max_tic_frame_size, 10),
|
||||||
|
sap_mcast_addr: sap_mcast_addr,
|
||||||
sap_interval: parseInt(sap_interval, 10)
|
sap_interval: parseInt(sap_interval, 10)
|
||||||
}),
|
}),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
@ -220,4 +222,11 @@ export default class RestAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getRemoteSources() {
|
||||||
|
return this.doFetch(browseSources).catch(err => {
|
||||||
|
toast.error('Browse sources get failed: ' + err.message)
|
||||||
|
return Promise.reject(Error(err.message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
94
webui/src/SourceInfo.js
Normal file
94
webui/src/SourceInfo.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// SourceInfo.js
|
||||||
|
//
|
||||||
|
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
|
||||||
|
require('./styles.css');
|
||||||
|
|
||||||
|
const infoCustomStyles = {
|
||||||
|
content : {
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
right: 'auto',
|
||||||
|
bottom: 'auto',
|
||||||
|
marginRight: '-50%',
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class SourceInfo extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
sdp: PropTypes.string.isRequired,
|
||||||
|
closeInfo: PropTypes.func.isRequired,
|
||||||
|
infoIsOpen: PropTypes.bool.isRequired,
|
||||||
|
infoTitle: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.onClose = this.onClose.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
Modal.setAppElement('body');
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
this.props.closeInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id='source-info'>
|
||||||
|
<Modal ariaHideApp={false}
|
||||||
|
isOpen={this.props.infoIsOpen}
|
||||||
|
onRequestClose={this.props.closeInfo}
|
||||||
|
style={infoCustomStyles}
|
||||||
|
contentLabel="Srouce SDP Info">
|
||||||
|
<h2><center>{this.props.infoTitle}</center></h2>
|
||||||
|
<table><tbody>
|
||||||
|
<tr>
|
||||||
|
<th align="left"> <label>ID</label> </th>
|
||||||
|
<th align="left"> <input value={this.props.id} readOnly/> </th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th align="left"> <label>Name</label> </th>
|
||||||
|
<th align="left"> <input value={this.props.name} readOnly/> </th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th align="left"> <label>SDP</label> </th>
|
||||||
|
<th align="left"> <textarea rows='15' cols='55' value={this.props.sdp} readOnly/> </th>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
<br/>
|
||||||
|
<div style={{textAlign: 'center'}}>
|
||||||
|
<button onClick={this.onClose}>Close</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SourceInfo;
|
@ -25,6 +25,7 @@ import RestAPI from './Services';
|
|||||||
import Loader from './Loader';
|
import Loader from './Loader';
|
||||||
import SourceEdit from './SourceEdit';
|
import SourceEdit from './SourceEdit';
|
||||||
import SourceRemove from './SourceRemove';
|
import SourceRemove from './SourceRemove';
|
||||||
|
import SourceInfo from './SourceInfo';
|
||||||
|
|
||||||
require('./styles.css');
|
require('./styles.css');
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ class SourceEntry extends Component {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
channels: PropTypes.number.isRequired,
|
channels: PropTypes.number.isRequired,
|
||||||
onEditClick: PropTypes.func.isRequired,
|
onEditClick: PropTypes.func.isRequired,
|
||||||
|
onInfoClick: PropTypes.func.isRequired,
|
||||||
onTrashClick: PropTypes.func.isRequired
|
onTrashClick: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,10 +43,15 @@ class SourceEntry extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
address: 'n/a',
|
address: 'n/a',
|
||||||
port: 'n/a'
|
port: 'n/a',
|
||||||
|
sdp: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleInfoClick = () => {
|
||||||
|
this.props.onInfoClick(this.props.id, this.state.sdp);
|
||||||
|
};
|
||||||
|
|
||||||
handleEditClick = () => {
|
handleEditClick = () => {
|
||||||
this.props.onEditClick(this.props.id);
|
this.props.onEditClick(this.props.id);
|
||||||
};
|
};
|
||||||
@ -59,6 +66,7 @@ class SourceEntry extends Component {
|
|||||||
.then(function(sdp) {
|
.then(function(sdp) {
|
||||||
var address = sdp.match(/(c=IN IP4 )([0-9.]+)/g);
|
var address = sdp.match(/(c=IN IP4 )([0-9.]+)/g);
|
||||||
var port = sdp.match(/(m=audio )([0-9]+)/g);
|
var port = sdp.match(/(m=audio )([0-9]+)/g);
|
||||||
|
this.setState({ sdp: sdp });
|
||||||
if (address && port) {
|
if (address && port) {
|
||||||
this.setState({ address: address[0].substr(9), port: port[0].substr(8) });
|
this.setState({ address: address[0].substr(9), port: port[0].substr(8) });
|
||||||
}
|
}
|
||||||
@ -73,6 +81,7 @@ class SourceEntry extends Component {
|
|||||||
<td> <label>{this.state.address}</label> </td>
|
<td> <label>{this.state.address}</label> </td>
|
||||||
<td> <label>{this.state.port}</label> </td>
|
<td> <label>{this.state.port}</label> </td>
|
||||||
<td align='center'> <label>{this.props.channels}</label> </td>
|
<td align='center'> <label>{this.props.channels}</label> </td>
|
||||||
|
<td> <span className='pointer-area' onClick={this.handleInfoClick}> <img width='20' height='20' src='/info.png' alt=''/> </span> </td>
|
||||||
<td> <span className='pointer-area' onClick={this.handleEditClick}> <img width='20' height='20' src='/edit.png' alt=''/> </span> </td>
|
<td> <span className='pointer-area' onClick={this.handleEditClick}> <img width='20' height='20' src='/edit.png' alt=''/> </span> </td>
|
||||||
<td> <span className='pointer-area' onClick={this.handleTrashClick}> <img width='20' height='20' src='/trash.png' alt=''/> </span> </td>
|
<td> <span className='pointer-area' onClick={this.handleTrashClick}> <img width='20' height='20' src='/trash.png' alt=''/> </span> </td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -131,16 +140,21 @@ class Sources extends Component {
|
|||||||
source: {},
|
source: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
|
isInfo: false,
|
||||||
editIsOpen: false,
|
editIsOpen: false,
|
||||||
|
infoIsOpen: false,
|
||||||
removeIsOpen: false,
|
removeIsOpen: false,
|
||||||
editTitle: ''
|
editTitle: ''
|
||||||
};
|
};
|
||||||
|
this.onInfoClick = this.onInfoClick.bind(this);
|
||||||
this.onEditClick = this.onEditClick.bind(this);
|
this.onEditClick = this.onEditClick.bind(this);
|
||||||
this.onTrashClick = this.onTrashClick.bind(this);
|
this.onTrashClick = this.onTrashClick.bind(this);
|
||||||
this.onAddClick = this.onAddClick.bind(this);
|
this.onAddClick = this.onAddClick.bind(this);
|
||||||
this.onReloadClick = this.onReloadClick.bind(this);
|
this.onReloadClick = this.onReloadClick.bind(this);
|
||||||
|
this.openInfo = this.openInfo.bind(this);
|
||||||
this.openEdit = this.openEdit.bind(this);
|
this.openEdit = this.openEdit.bind(this);
|
||||||
this.closeEdit = this.closeEdit.bind(this);
|
this.closeEdit = this.closeEdit.bind(this);
|
||||||
|
this.closeInfo = this.closeInfo.bind(this);
|
||||||
this.applyEdit = this.applyEdit.bind(this);
|
this.applyEdit = this.applyEdit.bind(this);
|
||||||
this.fetchSources = this.fetchSources.bind(this);
|
this.fetchSources = this.fetchSources.bind(this);
|
||||||
}
|
}
|
||||||
@ -158,6 +172,10 @@ class Sources extends Component {
|
|||||||
this.fetchSources();
|
this.fetchSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openInfo(title, source, sdp, isInfo) {
|
||||||
|
this.setState({infoIsOpen: true, infoTitle: title, source: source, sdp: sdp, isInfo: isInfo});
|
||||||
|
}
|
||||||
|
|
||||||
openEdit(title, source, isEdit) {
|
openEdit(title, source, isEdit) {
|
||||||
this.setState({editIsOpen: true, editTitle: title, source: source, isEdit: isEdit});
|
this.setState({editIsOpen: true, editTitle: title, source: source, isEdit: isEdit});
|
||||||
}
|
}
|
||||||
@ -173,6 +191,15 @@ class Sources extends Component {
|
|||||||
this.fetchSources();
|
this.fetchSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeInfo() {
|
||||||
|
this.setState({infoIsOpen: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
onInfoClick(id, sdp) {
|
||||||
|
const source = this.state.sources.find(s => s.id === id);
|
||||||
|
this.openInfo("Local Source Info", source, sdp, true);
|
||||||
|
}
|
||||||
|
|
||||||
onEditClick(id) {
|
onEditClick(id) {
|
||||||
const source = this.state.sources.find(s => s.id === id);
|
const source = this.state.sources.find(s => s.id === id);
|
||||||
this.openEdit("Edit Source " + id, source, true);
|
this.openEdit("Edit Source " + id, source, true);
|
||||||
@ -219,6 +246,7 @@ class Sources extends Component {
|
|||||||
id={source.id}
|
id={source.id}
|
||||||
name={source.name}
|
name={source.name}
|
||||||
channels={source.map.length}
|
channels={source.map.length}
|
||||||
|
onInfoClick={this.onInfoClick}
|
||||||
onEditClick={this.onEditClick}
|
onEditClick={this.onEditClick}
|
||||||
onTrashClick={this.onTrashClick}
|
onTrashClick={this.onTrashClick}
|
||||||
/>
|
/>
|
||||||
@ -229,6 +257,15 @@ class Sources extends Component {
|
|||||||
: <SourceList onAddClick={this.onAddClick}
|
: <SourceList onAddClick={this.onAddClick}
|
||||||
onReloadClick={this.onReloadClick}
|
onReloadClick={this.onReloadClick}
|
||||||
sources={sources} /> }
|
sources={sources} /> }
|
||||||
|
{ this.state.infoIsOpen ?
|
||||||
|
<SourceInfo infoIsOpen={this.state.infoIsOpen}
|
||||||
|
closeInfo={this.closeInfo}
|
||||||
|
infoTitle={this.state.infoTitle}
|
||||||
|
isInfo={this.state.isInfo}
|
||||||
|
id={this.state.source.id.toString()}
|
||||||
|
name={this.state.source.name}
|
||||||
|
sdp={this.state.sdp} />
|
||||||
|
: undefined }
|
||||||
{ this.state.editIsOpen ?
|
{ this.state.editIsOpen ?
|
||||||
<SourceEdit editIsOpen={this.state.editIsOpen}
|
<SourceEdit editIsOpen={this.state.editIsOpen}
|
||||||
closeEdit={this.closeEdit}
|
closeEdit={this.closeEdit}
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Route, BrowserRouter as Router, Switch } from 'react-router-dom'
|
import { Route, BrowserRouter as Router, Switch } from 'react-router-dom'
|
||||||
import { render } from "react-dom";
|
import { render } from "react-dom";
|
||||||
@ -37,6 +36,7 @@ function App() {
|
|||||||
<Route exact path='/PTP' component={() => <ConfigTabs key='PTP' currentTab='PTP' />} />
|
<Route exact path='/PTP' component={() => <ConfigTabs key='PTP' currentTab='PTP' />} />
|
||||||
<Route exact path='/Sources' component={() => <ConfigTabs key='Sources' currentTab='Sources' />} />
|
<Route exact path='/Sources' component={() => <ConfigTabs key='Sources' currentTab='Sources' />} />
|
||||||
<Route exact path='/Sinks' component={() => <ConfigTabs key='Sinks' currentTab='Sinks' />} />
|
<Route exact path='/Sinks' component={() => <ConfigTabs key='Sinks' currentTab='Sinks' />} />
|
||||||
|
<Route exact path='/Browser' component={() => <ConfigTabs key='Browser' currentTab='Browser' />} />
|
||||||
<Route component={() => <ConfigTabs key='Config' currentTab='Config' />} />
|
<Route component={() => <ConfigTabs key='Config' currentTab='Config' />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user