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).
 | 
			
		||||
* **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 ##
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
* session handling and SDP parsing and creation
 | 
			
		||||
* 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
 | 
			
		||||
 | 
			
		||||
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 ##
 | 
			
		||||
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:
 | 
			
		||||
  * set the network interface name to your Ethernet card, e.g.: *"interface\_name": "eth0"*
 | 
			
		||||
  * set the default sample rate to 48Khz: *"sample\_rate": 48000*
 | 
			
		||||
* open the daemon configuration file *daemon.conf* and change the following parameters:
 | 
			
		||||
  * set network interface name to your Ethernet card, e.g.: *"interface\_name": "eth0"*
 | 
			
		||||
  * set default sample rate to 48Khz: *"sample\_rate": 48000*
 | 
			
		||||
* verify that PulseAdio is not running. See [PulseAudio](#notes).
 | 
			
		||||
* install the ALSA RAVENNA/AES67 module with:     
 | 
			
		||||
    *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:
 | 
			
		||||
  * 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 Sinks tab and add a new Sink using the plus button, use the specified Source URL and press the Submit button
 | 
			
		||||
  * edit the newly created Sink and copy the SDP file reported in SDP
 | 
			
		||||
  * go to Sources tab and click on the info icon on the right of the newly created source and copy the SDP file
 | 
			
		||||
* 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
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,6 @@ set(RAVENNA_ALSA_LKM "../3rdparty/ravenna-alsa-lkm/")
 | 
			
		||||
set(CPP_HTTPLIB " ../3rdparty/cpp-httplib/")
 | 
			
		||||
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)
 | 
			
		||||
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)
 | 
			
		||||
target_link_libraries(aes67-daemon ${Boost_LIBRARIES})
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ The daemon is responsible for:
 | 
			
		||||
* communication and configuration of the device driver
 | 
			
		||||
* provide an HTTP REST API for the daemon control and configuration
 | 
			
		||||
* 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
 | 
			
		||||
 | 
			
		||||
## 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** [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 ##
 | 
			
		||||
 | 
			
		||||
### JSON Config<a name="config"></a> ###
 | 
			
		||||
@ -149,6 +157,7 @@ Example
 | 
			
		||||
      "frame_size_at_1fs": 192,
 | 
			
		||||
      "sample_rate": 44100,
 | 
			
		||||
      "max_tic_frame_size": 1024,
 | 
			
		||||
      "sap_mcast_addr": "239.255.255.255",
 | 
			
		||||
      "sap_interval": 30,
 | 
			
		||||
      "mac_addr": "01:00:5e:01:00:01",
 | 
			
		||||
      "ip_addr": "127.0.0.1"
 | 
			
		||||
@ -208,6 +217,10 @@ where:
 | 
			
		||||
> 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.
 | 
			
		||||
 | 
			
		||||
> **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**
 | 
			
		||||
> 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). 
 | 
			
		||||
> 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() ==
 | 
			
		||||
      INADDR_NONE)
 | 
			
		||||
    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)
 | 
			
		||||
    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_sample_rate() const { return sample_rate_; };
 | 
			
		||||
  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_; };
 | 
			
		||||
  uint8_t get_ptp_domain() const { return ptp_domain_; };
 | 
			
		||||
  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) {
 | 
			
		||||
    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_ptp_domain(uint8_t ptp_domain) { ptp_domain_ = ptp_domain; };
 | 
			
		||||
  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 sample_rate_{44100};
 | 
			
		||||
  std::string rtp_mcast_base_{"239.1.0.1"};
 | 
			
		||||
  std::string sap_mcast_addr_{"224.2.127.254"};
 | 
			
		||||
  uint16_t rtp_port_{5004};
 | 
			
		||||
  uint8_t ptp_domain_{0};
 | 
			
		||||
  uint8_t ptp_dscp_{46};
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@
 | 
			
		||||
  "rtp_port": 5004,
 | 
			
		||||
  "ptp_domain": 0,
 | 
			
		||||
  "ptp_dscp": 48,
 | 
			
		||||
  "sap_mcast_addr": "239.255.255.255",
 | 
			
		||||
  "sap_interval": 30,
 | 
			
		||||
  "syslog_proto": "none",
 | 
			
		||||
  "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_.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::stringstream buffer;
 | 
			
		||||
    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) {
 | 
			
		||||
    if (res.status == 200) {
 | 
			
		||||
      BOOST_LOG_TRIVIAL(info) << "http_server:: " << req.method << " "
 | 
			
		||||
 | 
			
		||||
@ -24,19 +24,23 @@
 | 
			
		||||
 | 
			
		||||
#include "config.hpp"
 | 
			
		||||
#include "session_manager.hpp"
 | 
			
		||||
#include "browser.hpp"
 | 
			
		||||
 | 
			
		||||
class HttpServer {
 | 
			
		||||
 public:
 | 
			
		||||
  HttpServer() = delete;
 | 
			
		||||
  HttpServer(std::shared_ptr<SessionManager> session_manager,
 | 
			
		||||
             std::shared_ptr<Browser> browser,
 | 
			
		||||
             std::shared_ptr<Config> config)
 | 
			
		||||
      : session_manager_(session_manager),
 | 
			
		||||
        browser_(browser),
 | 
			
		||||
        config_(config) {};
 | 
			
		||||
  bool start();
 | 
			
		||||
  bool stop();
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  std::shared_ptr<SessionManager> session_manager_;
 | 
			
		||||
  std::shared_ptr<Browser> browser_;
 | 
			
		||||
  std::shared_ptr<Config> config_;
 | 
			
		||||
  httplib::Server svr_;
 | 
			
		||||
  std::future<bool> res_;
 | 
			
		||||
 | 
			
		||||
@ -85,6 +85,7 @@ std::string config_to_json(const Config& config) {
 | 
			
		||||
     << ",\n  \"rtp_port\": " << config.get_rtp_port()
 | 
			
		||||
     << ",\n  \"ptp_domain\": " << unsigned(config.get_ptp_domain())
 | 
			
		||||
     << ",\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  \"syslog_proto\": \"" << escape_json(config.get_syslog_proto()) << "\""
 | 
			
		||||
     << ",\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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
  try {
 | 
			
		||||
    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>());
 | 
			
		||||
      } else if (key == "ptp_dscp") {
 | 
			
		||||
        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") {
 | 
			
		||||
        config.set_sap_interval(val.get_value<uint16_t>());
 | 
			
		||||
      } else if (key == "status_file") {
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@
 | 
			
		||||
 | 
			
		||||
#include <list>
 | 
			
		||||
#include "session_manager.hpp"
 | 
			
		||||
#include "browser.hpp"
 | 
			
		||||
 | 
			
		||||
/* JSON serializers */
 | 
			
		||||
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 streams_to_json(const std::list<StreamSource>& sources,
 | 
			
		||||
                            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 */
 | 
			
		||||
Config json_to_config(std::istream& jstream, const Config& curCconfig);
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@
 | 
			
		||||
#include "log.hpp"
 | 
			
		||||
#include "session_manager.hpp"
 | 
			
		||||
#include "interface.hpp"
 | 
			
		||||
#include "browser.hpp"
 | 
			
		||||
 | 
			
		||||
namespace po = boost::program_options;
 | 
			
		||||
namespace postyle = boost::program_options::command_line_style;
 | 
			
		||||
@ -115,8 +116,15 @@ int main(int argc, char* argv[]) {
 | 
			
		||||
          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 */
 | 
			
		||||
      HttpServer http_server(session_manager, config);
 | 
			
		||||
      HttpServer http_server(session_manager, browser, config);
 | 
			
		||||
      if (!http_server.start()) {
 | 
			
		||||
        throw std::runtime_error(std::string("HttpServer:: start failed"));
 | 
			
		||||
      }
 | 
			
		||||
@ -151,6 +159,12 @@ int main(int argc, char* argv[]) {
 | 
			
		||||
            std::string("HttpServer:: stop failed"));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      /* stop browser */
 | 
			
		||||
      if (!browser->stop()) {
 | 
			
		||||
        throw std::runtime_error(
 | 
			
		||||
            std::string("Browser:: stop failed"));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      /* stop session manager */
 | 
			
		||||
      if (!session_manager->stop()) {
 | 
			
		||||
        throw std::runtime_error(
 | 
			
		||||
 | 
			
		||||
@ -59,8 +59,7 @@ class nl_endpoint {
 | 
			
		||||
 | 
			
		||||
  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); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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"
 | 
			
		||||
 | 
			
		||||
using namespace boost::asio;
 | 
			
		||||
using boost::asio::deadline_timer;
 | 
			
		||||
 | 
			
		||||
class SAP {
 | 
			
		||||
 public:
 | 
			
		||||
  constexpr static const char addr[] = "224.2.127.254";
 | 
			
		||||
  constexpr static uint16_t port = 9875;
 | 
			
		||||
  constexpr static uint16_t max_deletions = 3;
 | 
			
		||||
  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 max_length = 4096;
 | 
			
		||||
 | 
			
		||||
  SAP() { socket_.open(boost::asio::ip::udp::v4()); };
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
  }
 | 
			
		||||
  SAP() = delete;
 | 
			
		||||
  SAP(const std::string& sap_mcast_addr);
 | 
			
		||||
 | 
			
		||||
  bool set_multicast_interface(const std::string& interface_ip);
 | 
			
		||||
  bool 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 deletion(uint16_t msg_id_hash, uint32_t addr, const std::string& sdp) {
 | 
			
		||||
    BOOST_LOG_TRIVIAL(info) << "sap::deletetion " << std::hex << msg_id_hash;
 | 
			
		||||
    return send(false, msg_id_hash, htonl(addr), sdp);
 | 
			
		||||
  }
 | 
			
		||||
                    const std::string& sdp);
 | 
			
		||||
  bool deletion(uint16_t msg_id_hash, uint32_t addr, const std::string& sdp);
 | 
			
		||||
  bool receive(bool& is_announce,
 | 
			
		||||
               uint16_t& msg_id_hash,
 | 
			
		||||
               uint32_t& addr,
 | 
			
		||||
               std::string& sdp,
 | 
			
		||||
               int tout_secs = 1);
 | 
			
		||||
 | 
			
		||||
 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,
 | 
			
		||||
            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_);
 | 
			
		||||
    } catch (boost::system::error_code& ec) {
 | 
			
		||||
      BOOST_LOG_TRIVIAL(error) << "sap::send_to " << ec.message();
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
            const std::string& sdp);
 | 
			
		||||
 | 
			
		||||
  std::string addr_;
 | 
			
		||||
  io_service io_service_;
 | 
			
		||||
  ip::udp::socket socket_{io_service_};
 | 
			
		||||
  ip::udp::endpoint remote_{
 | 
			
		||||
      ip::udp::endpoint(ip::address::from_string(addr), port)};
 | 
			
		||||
  ip::udp::endpoint remote_endpoint_{
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
@ -190,7 +190,7 @@ class SessionManager {
 | 
			
		||||
  PTPStatus ptp_status_;
 | 
			
		||||
  mutable std::shared_mutex ptp_mutex_;
 | 
			
		||||
 | 
			
		||||
  SAP sap_;
 | 
			
		||||
  SAP sap_{config_->get_sap_mcast_addr()};
 | 
			
		||||
  IGMP igmp_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@
 | 
			
		||||
  "rtp_port": 6004,
 | 
			
		||||
  "ptp_domain": 0,
 | 
			
		||||
  "ptp_dscp": 46,
 | 
			
		||||
  "sap_mcast_addr": "224.2.127.254",
 | 
			
		||||
  "sap_interval": 1,
 | 
			
		||||
  "syslog_proto": "none",
 | 
			
		||||
  "syslog_server": "255.255.255.254:1234",
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,7 @@ struct DaemonInstance {
 | 
			
		||||
  DaemonInstance() {
 | 
			
		||||
    BOOST_TEST_MESSAGE("Starting up test daemon instance ...");
 | 
			
		||||
    int retry = 10;
 | 
			
		||||
    while (--retry && daemon_.running()) {
 | 
			
		||||
    while (retry-- && daemon_.running()) {
 | 
			
		||||
      BOOST_TEST_MESSAGE("Checking daemon instance ...");
 | 
			
		||||
      httplib::Client cli(g_daemon_address, g_daemon_port);
 | 
			
		||||
      auto res = cli.Get("/");
 | 
			
		||||
@ -284,6 +284,12 @@ struct Client {
 | 
			
		||||
    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:
 | 
			
		||||
  httplib::Client cli_{g_daemon_address, g_daemon_port};
 | 
			
		||||
  io_service io_service_;
 | 
			
		||||
@ -432,7 +438,30 @@ BOOST_AUTO_TEST_CASE(source_check_sap) {
 | 
			
		||||
  cli.sap_wait_announcement(0, sdp.second);
 | 
			
		||||
  BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0");
 | 
			
		||||
  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) {
 | 
			
		||||
@ -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;
 | 
			
		||||
  for (int id = 0; id < 64; 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));
 | 
			
		||||
    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++) {
 | 
			
		||||
    BOOST_REQUIRE_MESSAGE(cli.add_sink_sdp(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::property_tree::ptree pt;
 | 
			
		||||
  std::stringstream ss(json.second);
 | 
			
		||||
  boost::property_tree::read_json(ss, pt);
 | 
			
		||||
  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<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));
 | 
			
		||||
  }
 | 
			
		||||
  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++) {
 | 
			
		||||
    BOOST_REQUIRE_MESSAGE(cli.remove_sink(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.
 | 
			
		||||
      Learn how to configure a non-root public URL by running `npm run build`.
 | 
			
		||||
    -->
 | 
			
		||||
    <title>React App</title>
 | 
			
		||||
    <title>AES64 Daemon WebUI</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <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,
 | 
			
		||||
            ptpDomain: data.ptp_domain,
 | 
			
		||||
            ptpDscp: data.ptp_dscp,
 | 
			
		||||
            sapMcastAddr: data.sap_mcast_addr,
 | 
			
		||||
            sapInterval: data.sap_interval,
 | 
			
		||||
            syslogProto: data.syslog_proto,
 | 
			
		||||
            syslogServer: data.syslog_server,
 | 
			
		||||
@ -95,6 +96,7 @@ class Config extends Component {
 | 
			
		||||
      !this.state.ticFrameSizeAt1fsErr &&
 | 
			
		||||
      !this.state.maxTicFrameSizeErr &&
 | 
			
		||||
      !this.state.rtpMcastBaseErr &&
 | 
			
		||||
      !this.state.sapMcastAddrErr &&
 | 
			
		||||
      !this.state.rtpPortErr &&
 | 
			
		||||
      !this.state.sapIntervalErr &&
 | 
			
		||||
      !this.state.syslogServerErr &&
 | 
			
		||||
@ -113,6 +115,7 @@ class Config extends Component {
 | 
			
		||||
      this.state.ticFrameSizeAt1fs, 
 | 
			
		||||
      this.state.sampleRate, 
 | 
			
		||||
      this.state.maxTicFrameSize, 
 | 
			
		||||
      this.state.sapMcastAddr,
 | 
			
		||||
      this.state.sapInterval)
 | 
			
		||||
    .then(response => toast.success('Config updated, daemon restart ...'));
 | 
			
		||||
  }
 | 
			
		||||
@ -154,12 +157,16 @@ class Config extends Component {
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <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>
 | 
			
		||||
            <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>
 | 
			
		||||
          </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>
 | 
			
		||||
            <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>
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ import PTP from './PTP';
 | 
			
		||||
import Config from './Config';
 | 
			
		||||
import Sources from './Sources';
 | 
			
		||||
import Sinks from './Sinks';
 | 
			
		||||
import RemoteSources from './RemoteSources';
 | 
			
		||||
 | 
			
		||||
require('./styles.css');
 | 
			
		||||
 | 
			
		||||
@ -51,6 +52,9 @@ class ConfigTabs extends Component {
 | 
			
		||||
          <div label="Sinks">
 | 
			
		||||
            <Sinks/>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div label="Browser">
 | 
			
		||||
            <RemoteSources/>
 | 
			
		||||
          </div>
 | 
			
		||||
        </Tabs>
 | 
			
		||||
       </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 sink = '/sink';
 | 
			
		||||
const status = '/status';
 | 
			
		||||
const browseSources = '/browse/sources';
 | 
			
		||||
 | 
			
		||||
const defaultParams = {
 | 
			
		||||
  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, {
 | 
			
		||||
      body: JSON.stringify({
 | 
			
		||||
        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),
 | 
			
		||||
        sample_rate: parseInt(sample_rate, 10),
 | 
			
		||||
        max_tic_frame_size: parseInt(max_tic_frame_size, 10),
 | 
			
		||||
        sap_mcast_addr: sap_mcast_addr,
 | 
			
		||||
        sap_interval: parseInt(sap_interval, 10)
 | 
			
		||||
      }),
 | 
			
		||||
      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 SourceEdit from './SourceEdit';
 | 
			
		||||
import SourceRemove from './SourceRemove';
 | 
			
		||||
import SourceInfo from './SourceInfo';
 | 
			
		||||
 | 
			
		||||
require('./styles.css');
 | 
			
		||||
 | 
			
		||||
@ -34,6 +35,7 @@ class SourceEntry extends Component {
 | 
			
		||||
    name: PropTypes.string.isRequired,
 | 
			
		||||
    channels: PropTypes.number.isRequired,
 | 
			
		||||
    onEditClick: PropTypes.func.isRequired,
 | 
			
		||||
    onInfoClick: PropTypes.func.isRequired,
 | 
			
		||||
    onTrashClick: PropTypes.func.isRequired
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -41,10 +43,15 @@ class SourceEntry extends Component {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.state = { 
 | 
			
		||||
      address: 'n/a',
 | 
			
		||||
      port: 'n/a' 
 | 
			
		||||
      port: 'n/a',
 | 
			
		||||
      sdp: '' 
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleInfoClick = () => {
 | 
			
		||||
    this.props.onInfoClick(this.props.id, this.state.sdp);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleEditClick = () => {
 | 
			
		||||
    this.props.onEditClick(this.props.id);
 | 
			
		||||
  };
 | 
			
		||||
@ -59,6 +66,7 @@ class SourceEntry extends Component {
 | 
			
		||||
      .then(function(sdp) {
 | 
			
		||||
        var address = sdp.match(/(c=IN IP4 )([0-9.]+)/g);
 | 
			
		||||
        var port = sdp.match(/(m=audio )([0-9]+)/g);
 | 
			
		||||
        this.setState({ sdp: sdp });
 | 
			
		||||
        if (address && port) {
 | 
			
		||||
          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.port}</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.handleTrashClick}> <img width='20' height='20' src='/trash.png' alt=''/> </span> </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
@ -131,16 +140,21 @@ class Sources extends Component {
 | 
			
		||||
      source: {}, 
 | 
			
		||||
      isLoading: false, 
 | 
			
		||||
      isEdit: false, 
 | 
			
		||||
      isInfo: false, 
 | 
			
		||||
      editIsOpen: false, 
 | 
			
		||||
      infoIsOpen: false, 
 | 
			
		||||
      removeIsOpen: false, 
 | 
			
		||||
      editTitle: '' 
 | 
			
		||||
    };
 | 
			
		||||
    this.onInfoClick = this.onInfoClick.bind(this);
 | 
			
		||||
    this.onEditClick = this.onEditClick.bind(this);
 | 
			
		||||
    this.onTrashClick = this.onTrashClick.bind(this);
 | 
			
		||||
    this.onAddClick = this.onAddClick.bind(this);
 | 
			
		||||
    this.onReloadClick = this.onReloadClick.bind(this);
 | 
			
		||||
    this.openInfo = this.openInfo.bind(this);
 | 
			
		||||
    this.openEdit = this.openEdit.bind(this);
 | 
			
		||||
    this.closeEdit = this.closeEdit.bind(this);
 | 
			
		||||
    this.closeInfo = this.closeInfo.bind(this);
 | 
			
		||||
    this.applyEdit = this.applyEdit.bind(this);
 | 
			
		||||
    this.fetchSources = this.fetchSources.bind(this);
 | 
			
		||||
  }
 | 
			
		||||
@ -158,6 +172,10 @@ class Sources extends Component {
 | 
			
		||||
    this.fetchSources();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openInfo(title, source, sdp, isInfo) {
 | 
			
		||||
    this.setState({infoIsOpen: true, infoTitle: title, source: source, sdp: sdp, isInfo: isInfo});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openEdit(title, source, isEdit) {
 | 
			
		||||
    this.setState({editIsOpen: true, editTitle: title, source: source, isEdit: isEdit});
 | 
			
		||||
  }
 | 
			
		||||
@ -173,6 +191,15 @@ class Sources extends Component {
 | 
			
		||||
    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) {
 | 
			
		||||
    const source = this.state.sources.find(s => s.id === id);
 | 
			
		||||
    this.openEdit("Edit Source " + id, source, true);
 | 
			
		||||
@ -219,6 +246,7 @@ class Sources extends Component {
 | 
			
		||||
        id={source.id}
 | 
			
		||||
        name={source.name}
 | 
			
		||||
        channels={source.map.length}
 | 
			
		||||
        onInfoClick={this.onInfoClick}
 | 
			
		||||
        onEditClick={this.onEditClick}
 | 
			
		||||
        onTrashClick={this.onTrashClick}
 | 
			
		||||
      />
 | 
			
		||||
@ -229,6 +257,15 @@ class Sources extends Component {
 | 
			
		||||
	   : <SourceList onAddClick={this.onAddClick} 
 | 
			
		||||
               onReloadClick={this.onReloadClick}
 | 
			
		||||
               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 ?
 | 
			
		||||
        <SourceEdit editIsOpen={this.state.editIsOpen}
 | 
			
		||||
          closeEdit={this.closeEdit}
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,8 @@
 | 
			
		||||
//  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 from 'react'
 | 
			
		||||
import { Route, BrowserRouter as Router, Switch } from 'react-router-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='/Sources' component={() => <ConfigTabs key='Sources' currentTab='Sources' />} />
 | 
			
		||||
        <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' />} />
 | 
			
		||||
      </Switch>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user