diff --git a/README.md b/README.md
index 48d77b9..71b2b59 100644
--- a/README.md
+++ b/README.md
@@ -1,132 +1,131 @@
-# AES67 Linux Daemon
-
-## License ##
-
-AES67 daemon and the WebUI are licensed under [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html).
-
-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)
-
-## Repository content ##
-
-### [daemon](daemon) directory ###
-
-This directory contains the AES67 daemon source code.
-The daemon can be cross-compiled for multiple platforms and implements the following functionalities:
-
-* 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
-* 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*
-
-See the [README](daemon/README.md) file in this directory for additional information about the AES67 daemon configuration and the HTTP REST API.
-
-### [webui](webui) directory ###
-
-This directory contains the AES67 daemon WebUI configuration implemented using React.
-With the WebUI a user can do the following operations:
-
-* change the daemon configuration, this causes a daemon restart
-* edit PTP clock slave configuration and monitor PTP slave status
-* add and edit RTP Sources
-* add, edit and monitor RTP Sinks
-
-### [3rdparty](3rdparty) directory ###
-
-This directory is used to download the 3rdparty open source.
-The [patches](3rdparty/patches) subdirectory contains patches applied to the ALSA RAVENNA/AES67 module to compile with the Linux Kernel 5.x and on ARMv7 platforms and to enable operations on the network loopback device (for testing purposes).
-
- The ALSA RAVENNA/AES67 kernel module is responsible for:
-
-* registering as an ALSA driver
-* generating and receiving RTP audio packets
-* PTP slave operations and PTP driven interrupt loop
-* netlink communication between user and kernel
-
-See [ALSA RAVENNA/AES67 Driver README](https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master/README.md) for additional information about the Merging Technologies module and for proper Linux Kernel configuration and tuning.
-
-### [demo](demo) directory ###
-
-This directory contains a the daemon configuration and status files used to run a short demo on the network loopback device. The [demo](#demo) is described below.
-
-## Prerequisite ##
-
-The daemon and the demo have been tested with **Ubuntu 18.04** distro on **x86/ARMv7** and with **Ubuntu 19.10** distro on **x86** using:
-
-* Linux kernel version >= 4.14.x
-* GCC version >= 7.4 / clang >= 6.0.0 (C++17 support required, clang is required to compile on ARMv7)
-* cmake version >= 3.10.2
-* node version >= 8.10.0
-* npm version >= 3.5.2
-* boost libraries version >= 1.65.1
-
-The BeagleBone® Black board with ARM Cortex-A8 32-Bit processor was used for testing on ARMv7.
-See [Ubuntu 18.04 on BeagleBone® Black](https://elinux.org/BeagleBoardUbuntu) for additional information about how to setup Ubuntu on this board.
-
-The [ubuntu-packages.sh](ubuntu-packages.sh) script can be used to install all the packages required to compile and run the AES67 daemon, the daemon tests and the [demo](#demo). See [PulseAudio and scripts notes](#notes).
-
-## How to build ##
-Make sure you have all the required packages installed, see [prerequisite](#prerequisite).
-To compile the AES67 daemon and the WebUI you can use the [build.sh](build.sh) script, see [script notes](#notes).
-The script performs the following operations:
-
-* checkout, patch and build the Merging Technologies ALSA RAVENNA/AES67 module
-* checkout the cpp-httplib
-* build and deploy the WebUI
-* build the AES67 daemon
-
-## Run the Demo ##
-
-To run a simple demo use the [run\_demo.sh](run_demo.sh) script. See [script notes](#notes).
-
-The demo performs the following operations:
-
-* setup system parameters
-* stop PulseAudio (if installed). This uses and keeps busy the ALSA playback and capture devices causing instability problems. See [PulseAudio](#notes).
-* install the ALSA RAVENNA/AES67 module
-* start the ptp4l as master clock on the network loopback device
-* start the AES67 daemon and creates a source and a sink according to the status file in the demo directory
-* open a browser on the daemon PTP status page
-* wait for the Ravenna driver PTP slave to synchronize
-* start recording on the configured ALSA sink for 60 seconds to the wave file in *./demo/sink_test.wav*
-* start playing a test sound on the configured ALSA source
-* wait for the recording to complete and terminate ptp4l and the AES67 daemon
-
-## 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*
-* 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*
-* run the daemon using the new configuration file:
- *aes67-daemon -c daemon.conf*
-* 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
-* 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
- * press the Submit button
-* return to the daemon WebUI, click on the PTP tab and wait for the "PTP Status" to report "locked"
-* open a shell on the Linux host and start the playback on the ravenna ALSA device. For example to playback a test sound use: *speaker-test -D plughw:RAVENNA -r 48000 -c 2 -t sine*
-
-## Notes ##
-
-
-* All the scripts in this repository are provided as a reference to help setting up the system and run a simple demo.
- They have been tested on **Ubuntu 18.04** and **19.10** distros only.
-* PulseAudio can create instability problems.
-Before running the daemon verify that PulseAudio is not running with *ps ax | grep pulseaudio*
-In case it's running try to execute the script *daemon/scripts/disable_pulseaudio.sh* to stop it. If after this the process is still alive consider renaming the executable with *sudo mv /usr/bin/pulseaudio /usr/bin/_pulseaudio* and reboot the system.
-
+# AES67 Linux Daemon
+
+## License ##
+
+AES67 daemon and the WebUI are licensed under [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html).
+
+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 under the [Boost Software License](https://www.boost.org/LICENSE_1_0.txt)
+
+## Repository content ##
+
+### [daemon](daemon) directory ###
+
+This directory contains the AES67 daemon source code.
+The daemon can be cross-compiled for multiple platforms and implements the following functionalities:
+
+* 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 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*
+
+See the [README](daemon/README.md) file in this directory for additional information about the AES67 daemon configuration and the HTTP REST API.
+
+### [webui](webui) directory ###
+
+This directory contains the AES67 daemon WebUI configuration implemented using React.
+With the WebUI a user can do the following operations:
+
+* change the daemon configuration, this causes a daemon restart
+* edit PTP clock slave configuration and monitor PTP slave status
+* add and edit RTP Sources
+* add, edit and monitor RTP Sinks
+
+### [3rdparty](3rdparty) directory ###
+
+This directory is used to download the 3rdparty open source.
+The [patches](3rdparty/patches) subdirectory contains patches applied to the ALSA RAVENNA/AES67 module to compile with the Linux Kernel 5.x and on ARMv7 platforms and to enable operations on the network loopback device (for testing purposes).
+
+ The ALSA RAVENNA/AES67 kernel module is responsible for:
+
+* registering as an ALSA driver
+* generating and receiving RTP audio packets
+* PTP slave operations and PTP driven interrupt loop
+* netlink communication between user and kernel
+
+See [ALSA RAVENNA/AES67 Driver README](https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master/README.md) for additional information about the Merging Technologies module and for proper Linux Kernel configuration and tuning.
+
+### [demo](demo) directory ###
+
+This directory contains a the daemon configuration and status files used to run a short demo on the network loopback device. The [demo](#demo) is described below.
+
+## Prerequisite ##
+
+The daemon and the demo have been tested with **Ubuntu 18.04** distro on **x86/ARMv7** and with **Ubuntu 19.10** distro on **x86** using:
+
+* Linux kernel version >= 4.14.x
+* GCC version >= 7.4 / clang >= 6.0.0 (C++17 support required, clang is required to compile on ARMv7)
+* cmake version >= 3.10.2
+* node version >= 8.10.0
+* npm version >= 3.5.2
+* boost libraries version >= 1.65.1
+
+The BeagleBone® Black board with ARM Cortex-A8 32-Bit processor was used for testing on ARMv7.
+See [Ubuntu 18.04 on BeagleBone® Black](https://elinux.org/BeagleBoardUbuntu) for additional information about how to setup Ubuntu on this board.
+
+The [ubuntu-packages.sh](ubuntu-packages.sh) script can be used to install all the packages required to compile and run the AES67 daemon, the daemon tests and the [demo](#demo). See [PulseAudio and scripts notes](#notes).
+
+## How to build ##
+Make sure you have all the required packages installed, see [prerequisite](#prerequisite).
+To compile the AES67 daemon and the WebUI you can use the [build.sh](build.sh) script, see [script notes](#notes).
+The script performs the following operations:
+
+* checkout, patch and build the Merging Technologies ALSA RAVENNA/AES67 module
+* checkout the cpp-httplib
+* build and deploy the WebUI
+* build the AES67 daemon
+
+## Run the Demo ##
+
+To run a simple demo use the [run\_demo.sh](run_demo.sh) script. See [script notes](#notes).
+
+The demo performs the following operations:
+
+* setup system parameters
+* stop PulseAudio (if installed). This uses and keeps busy the ALSA playback and capture devices causing instability problems. See [PulseAudio](#notes).
+* install the ALSA RAVENNA/AES67 module
+* start the ptp4l as master clock on the network loopback device
+* start the AES67 daemon and creates a source and a sink according to the status file in the demo directory
+* open a browser on the daemon PTP status page
+* wait for the Ravenna driver PTP slave to synchronize
+* start recording on the configured ALSA sink for 60 seconds to the wave file in *./demo/sink_test.wav*
+* start playing a test sound on the configured ALSA source
+* wait for the recording to complete and terminate ptp4l and the AES67 daemon
+
+## 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 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*
+* run the daemon using the new configuration file:
+ *aes67-daemon -c daemon.conf*
+* 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 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
+ * press the Submit button
+* return to the daemon WebUI, click on the PTP tab and wait for the "PTP Status" to report "locked"
+* open a shell on the Linux host and start the playback on the ravenna ALSA device. For example to playback a test sound use: *speaker-test -D plughw:RAVENNA -r 48000 -c 2 -t sine*
+
+## Notes ##
+
+
+* All the scripts in this repository are provided as a reference to help setting up the system and run a simple demo.
+ They have been tested on **Ubuntu 18.04** and **19.10** distros only.
+* PulseAudio can create instability problems.
+Before running the daemon verify that PulseAudio is not running with *ps ax | grep pulseaudio*
+In case it's running try to execute the script *daemon/scripts/disable_pulseaudio.sh* to stop it. If after this the process is still alive consider renaming the executable with *sudo mv /usr/bin/pulseaudio /usr/bin/_pulseaudio* and reboot the system.
+
diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt
index 56c49d5..f184c45 100644
--- a/daemon/CMakeLists.txt
+++ b/daemon/CMakeLists.txt
@@ -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})
diff --git a/daemon/README.md b/daemon/README.md
index c1da5c9..6c170cc 100644
--- a/daemon/README.md
+++ b/daemon/README.md
@@ -1,522 +1,589 @@
-# AES67 Daemon #
-
-AES67 daemon uses the Merging Technologies device driver (MergingRavennaALSA.ko) to implement basic AES67 functionalities. See [ALSA RAVENNA/AES67 Driver README](https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master/README.md) for additional information.
-
-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
-* IGMP handling for SAP and RTP sessions
-
-## Configuration file ##
-
-The daemon uses a JSON file to store the configuration parameters.
-The config file must be specified at startup time and it gets updated when new params are set via the REST interface.
-See [JSON config params](#config) for additional info on the configuration parameters.
-If a status file is specified in the daemon's configuration the server will load it at startup ad will save it at termination.
-The status file contains all the configured sources and sinks (streams).
-See [JSON streams](#rtp-streams) for additional info on the status file format and its parameters.
-
-## HTTP REST API ##
-
-The daemon implements a REST API interface to configure and control the driver.
-All operations returns HTTP *200* status code in case of success and HTTP *4xx* or *5xx* status code in case of failure.
-In case of failure the server returns a **text/plain** content type with the category and a description of the error occurred.
-**_NOTE:_** At present the embedded HTTP server doesn't implement neither HTTPS nor user authentication.
-
-### Get Daemon Configuration ###
-* **URL** /api/config
-* **Method** GET
-* **URL Params** none
-* **Body Type** application/json
-* **Body** [Config params](#config)
-
-### Set Daemon Configuration ###
-* **URL** /api/config
-* **Method** POST
-* **URL Params** none
-* **Body Type** application/json
-* **Body** [Config params](#config)
-
-### Get PTP Configuration ###
-* **URL** /api/ptp/config
-* **Method** GET
-* **URL Params** none
-* **Body Type** application/json
-* **Body** [PTP Config params](#ptp-config)
-
-### Set PTP Configuration ###
-* **URL** /api/ptp/config
-* **Method** POST
-* **URL Params** none
-* **Body Type** application/json
-* **Body** [PTP Config params](#ptp-config)
-
-### Get PTP Status ###
-* **URL** /api/ptp/status
-* **Method** GET
-* **URL Params** none
-* **Body Type** application/json
-* **Body** [PTP Status params](#ptp-status)
-
-### Add RTP Source ###
-* **Description** add or update the RTP source specified by the *id*
-* **URL** /api/source/:id
-* **Method** PUT
-* **URL Params** id=[integer in the range (0-63)]
-* **Body Type** application/json
-* **Body** [RTP Source params](#rtp-source)
-
-### Remove RTP Source ###
-* **Description** remove the RTP sink specified by the *id*
-* **URL** /api/source/:id
-* **Method** DELETE
-* **URL Params** id=[integer in the range (0-63)]
-* **Body** none
-
-### Get RTP Source SDP file ###
-* **Description** retrieve the SDP of the source specified by *id*
-* **URL** /api/source/sdp/:id
-* **Method** GET
-* **URL Params** id=[integer in the range (0-63)]
-* **Body Type** application/sdp
-* **Body** [Example SDP file for a source](#rtp-source-sdp)
-
-### Add RTP Sink ###
-* **Description** add or update the RTP sink specified by the *id*
-* **URL** /api/sink/:id
-* **Method** PUT
-* **URL Params** id=[integer in the range (0-63)]
-* **Body Type** application/json
-* **Body** [RTP Sink params](#rtp-sink)
-
-### Remove RTP Sink ###
-* **Description** remove the RTP sink specified by *id*
-* **URL** /api/sink/:id
-* **Method** DELETE
-* **URL Params** id=[integer in the range (0-63)]
-* **Body** none
-
-### Get RTP Sink status ###
-* **Description** retrieve the status of the sink specified by *id*
-* **URL** /api/sink/status/:id
-* **Method** GET
-* **URL Params** id=[integer in the range (0-63)]
-* **Body Type** application/json
-* **Body** [RTP Sink status params](#rtp-sink-status)
-
-### Get all configured RTP Sources ###
-* **URL** /api/sources
-* **Method** GET
-* **URL Params** none
-* **Body type** application/json
-* **Body** [RTP Sources params](#rtp-sources)
-
-### Get all configured RTP Sinks ###
-* **URL** /api/sinks
-* **Method** GET
-* **URL Params** none
-* **Body type** application/json
-* **Body** [RTP Sinks params](#rtp-sinks)
-
-### Get all configured RTP Sources and Sinks (Streams) ###
-* **URL** /api/streams
-* **Method** GET
-* **URL Params** none
-* **Body type** application/json
-* **Body** [RTP Streams params](#rtp-streams)
-
-## HTTP REST API structures ##
-
-### JSON Config ###
-
-Example
-
- {
- "interface_name": "lo",
- "http_port": 8080,
- "log_severity": 2,
- "syslog_proto": "none",
- "syslog_server": "255.255.255.254:1234",
- "rtp_mcast_base": "239.2.0.1",
- "status_file": "./status.json",
- "rtp_port": "5004",
- "ptp_domain": 0,
- "ptp_dscp": 46,
- "playout_delay": 0,
- "frame_size_at_1fs": 192,
- "sample_rate": 44100,
- "max_tic_frame_size": 1024,
- "sap_interval": 30,
- "mac_addr": "01:00:5e:01:00:01",
- "ip_addr": "127.0.0.1"
- }
-
-where:
-
-> **interface\_name**
-> JSON string specifying the network interface used by the daemon and the driver for both the RTP, PTP, SAP and HTTP traffic.
-
-> **http\_port**
-> JSON number specifying the HTTP port number used by the embedded web server in the daemon implementing the REST interface.
-
-> **log\_severity**
-> JSON integer specifying the process log severity level (0 to 5).
-> All traces major or equal to the specified level are enabled. (0=trace, 1=debug, 2=info, 3=warning, 4=error, 5=fatal).
-
-> **syslog\_proto**
-> JSON string specifying the syslog protocol used for logging.
-> This can be an empty string to log to the local syslog, "udp" to send syslog messages
-> to a remote server or "none" to disable the syslog logging.
-> When "none" is used the client writes the logs to the standard output.
-
-> **syslog\_server**
-> JSON string specifying the syslog server address used for logging.
-
-> **status\_file**
-> JSON string specifying the file that will contain the sessions status.
-> The file is loaded when the daemon starts and is saved when the daemon exits.
-
-> **rtp\_mcast\_base**
-> JSON string specifying the default base RTP IPv4 multicast address used by a source.
-> The specific multicast RTP address is the base address plus the source id number.
-> For example if the base address is 239.2.0.1 and source id is 1 the RTP source address used is 239.2.0.2.
-
-> **rtp_port**
-> JSON number specifying the RTP port used by the sources.
-
-> **ptp\_domain**
-> JSON number specifying the PTP clock domain of the master clock the driver will attempt to synchronize to.
-
-> **ptp\_dscp**
-> JSON number specifying the IP DSCP used in IPv4 header for PTP traffic.
-> Valid values are 48 (CS6) and 46 (EF).
-
-> **sample\_rate**
-> JSON number specifying the default sample rate.
-> Valid values are 44100Hz, 48000Hz and 96000Hz.
-
-> **playout\_delay**
-> JSON number specifying the default safety playout delay at 1FS in samples.
-
-> **tic\_frame\_size\_at\_1fs**
-> JSON number specifying the RTP frame size at 1FS in samples.
-
-> **max\_tic\_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.
-
-> **sap\_interval**
-> JSON number specifying the SAP interval in seconds to use. Use 0 for automatic and RFC compliant interval. Default is 30secs.
-
-> **mac\_addr**
-> JSON string specifying the MAC address of the specified network device.
-> **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the MAC address of the network device at startup time.
-
-> **ip\_addr**
-> JSON string specifying the IP address of the specified network device.
-> **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the IP address of the network device at startup time and will monitor it periodically.
-
-### JSON PTP Config ###
-
-Example
-
- {
- "domain": 0,
- "dscp": 46
- }
-
-where:
-
-> **domain**
-> JSON number specifying the PTP clock domain of the master clock the driver will attempt to synchronize to.
-
-> **dscp**
-> JSON number specifying the IP DSCP used in IPv4 header for PTP traffic.
-> Valid values are 48 (CS6) and 46 (EF).
-
-### JSON PTP Status ###
-
-Example
-
- {
- "status": "unlocked",
- "gmid": "00-00-00-FF-FE-00-00-00",
- "jitter": 0
- }
-
-where:
-
-> **status**
-> JSON string specifying the PTP slave status. This can be *unlocked*, *locking* or *locked*.
-
-> **gmid**
-> JSON string specifying GrandMaster clock ID we are currently synchronized to.
-
-> **jitter**
-> JSON number specifying the measured PTP packet delay jitter.
-
-### JSON RTP source ###
-
-Example:
-
- {
- "enabled": true,
- "name": "ALSA Source 0",
- "io": "Audio Device",
- "codec": "L16",
- "max_samples_per_packet": 48,
- "ttl": 15,
- "payload_type": 98,
- "dscp": 34,
- "refclk_ptp_traceable": true,
- "map": [0, 1]
- }
-
-where:
-
-> **enabled**
-> JSON bool specifying whether the source is enabled and will be announced or not.
-
-> **name**
-> JSON string specifying the source name.
-
-> **io**
-> JSON string specifying the IO name.
-
-> **codec**
-> JSON string specifying codec in use.
-> Valid values are L16 and L24.
-
-> **max\_sample\_per\_packet**
-> JSON number specifying the max number of samples contained in one RTP packet.
-> Valid values are 12, 16, 48, 96, 192.
-> See the table below for correspondent max RTP packet duration:
-> | | 44.1Khz | 48Khz | 96Khz |
-> |-----|----------|---------|---------|
-> | 12 | 272µs | 250µs | 125µs |
-> | 16 | 363µs | 333µs | 166µs |
-> | 48 | 1.088ms | 1ms | 500µs |
-> | 96 | 2.177ms | 2ms | 1ms |
-> | 192 | 4.353ms | 4ms | 2ms |
-
-> **ttl**
-> JSON number specifying RTP packets Time To Live.
-
-> **payload\_type**
-> JSON number specifying RTP Payload Type.
-
-> **dscp**
-> JSON number specifying the IP DSCP used in IPv4 header for RTP traffic.
-> Valid values are 46 (EF), 34 (AF41), 26 (AF31), 0 (BE).
-
-> **refclk\_ptp\_traceable**
-> JSON boolean specifying whether the PTP reference clock is traceable or not.
-> A reference clock source is traceable if it is known to be delivering traceable time.
-
-> **map**
-> JSON array of integers specifying the mapping between the RTP source and the ALSA playback device channels used during playback. The length of this map determines the number of channels of the source.
-
-### RTP source SDP ###
-
-Example:
-
- v=0
- o=- 0 0 IN IP4 127.0.0.1
- s=ALSA Source 0
- c=IN IP4 239.1.0.1/15
- t=0 0
- a=clock-domain:PTPv2 0
- m=audio 5004 RTP/AVP 98
- c=IN IP4 239.1.0.1/15
- a=rtpmap:98 L16/44100/2
- a=sync-time:0
- a=framecount:48
- a=ptime:1.08843537415
- a=mediaclk:direct=0
- a=ts-refclk:ptp=traceable
- a=recvonly
-
-### JSON RTP sink ###
-
-Example:
-
- {
- "name": "ALSA Sink 0",
- "io": "Audio Device",
- "delay": 576,
- "use_sdp": false,
- "source": "http://127.0.0.1:8080/api/source/sdp/0",
- "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
- "ignore_refclk_gmid": false,
- "map": [0, 1]
- }
-
-where:
-
-> **name**
-> JSON string specifying the source name.
-
-> **io**
-> JSON string specifying the IO name.
-
-> **delay**
-> JSON number specifying the playout delay of the sink in samples.
-> **_NOTE:_** The specified delay cannot be less than the source max samples size announced by the SDP file.
-
-> **use\_sdp**
-> JSON boolean specifying whether the source SDP file is fetched from the HTTP URL specified in the **source** parameter or the SDP in the **sdp** parameter is used.
-
-> **source**
-> JSON string specifying the HTTP URL of the source SDP file. This parameter is mandatory if **use\_sdp** is false.
-
-> **sdp**
-> JSON string specifying the SDP of the source. This parameter is mandatory if **use\_sdp** is true.
-> See [example SDP file for a source](#rtp-source-sdp)
-
-> **ignore\_refclk\_gmid**
-> JSON boolean specifying whether the grand master reference clock ID specified in the SDP file of the source must be compared with the master reference clock to which the current PTP slave clock is syncronized.
-
-> **map**
-> JSON array of integers specifying the mapping between the RTP sink and the ALSA capture device channels used during recording. The length of this map determines the number of channels of the sink.
-
-### JSON RTP sink status ###
-
-Example:
-
- {
- "sink_flags":
- {
- "rtp_seq_id_error": false,
- "rtp_ssrc_error": false,
- "rtp_payload_type_error": false,
- "rtp_sac_error": false,
- "receiving_rtp_packet": false,
- "some_muted": false,
- "muted": true
- },
- "sink_min_time": 0
-}
-
-where:
-
-> **sink\_flags**
-> JSON object containing a set of flags reporting the RTP sink status.
-
-> - **rtp\_seq\_id\_error** JSON boolean specifying whether a wrong RTP sequence was detected.
-
-> - **rtp\_ssrc\_error** JSON boolean specifying whether a wrong RTP source is contributing to the incoming stream.
-
-> - **rtp\_payload\_type\_error** JSON boolean specifying whether a wrong payload type was received.
-
-> - **rtp\_sac\_error** JSON boolean specifying whether a packet with a wrong RTP timestamp was received.
-
-> - **receiving\_rtp\_packet** JSON boolean specifying whether the sink is currently receiving RTP packets from the source.
-
-> - **some\_muted** JSON boolean (not used)
-
-> - **muted** JSON boolean specifying whether the sink is currently muted.
-
-> **sink\_min\_time** JSON number specifying the minimum source RTP packet arrival time.
-
-### JSON RTP Sources ###
-
-Example:
-
- {
- "sources": [
- {
- "id": 0,
- "enabled": true,
- "name": "ALSA Source 0",
- "io": "Audio Device",
- "max_samples_per_packet": 48,
- "codec": "L16",
- "ttl": 15,
- "payload_type": 98,
- "dscp": 34,
- "refclk_ptp_traceable": true,
- "map": [ 0, 1 ]
- } ]
- }
-
-where:
-
-> **sources**
-> JSON array of the configured sources.
-> Every source is identified by the JSON number **id** (in the range 0 - 63).
-> See [RTP Source params](#rtp-source) for all the other parameters.
-
-### JSON RTP Sinks ###
-
-Example:
-
- {
- "sinks": [
- {
- "id": 0,
- "name": "ALSA Sink 0",
- "io": "Audio Device",
- "use_sdp": true,
- "source": "http://127.0.0.1:8080/api/source/sdp/0",
- "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
- "delay": 576,
- "ignore_refclk_gmid": false,
- "map": [ 0, 1 ]
- } ]
- }
-
-where:
-
-> **sinks**
-> JSON array of the configured sinks.
-> 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 RTP Streams ###
-
-Example:
-
- {
- "sources": [
- {
- "id": 0,
- "enabled": true,
- "name": "ALSA Source 0",
- "io": "Audio Device",
- "max_samples_per_packet": 48,
- "codec": "L16",
- "ttl": 15,
- "payload_type": 98,
- "dscp": 34,
- "refclk_ptp_traceable": true,
- "map": [ 0, 1 ]
- } ],
- "sinks": [
- {
- "id": 0,
- "name": "ALSA Sink 0",
- "io": "Audio Device",
- "use_sdp": true,
- "source": "http://127.0.0.1:8080/api/source/sdp/0",
- "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
- "delay": 576,
- "ignore_refclk_gmid": false,
- "map": [ 0, 1 ]
- } ]
- }
-
-where:
-
-> **sources**
-> JSON array of the configured sources.
-> Every source is identified by the JSON number **id** (in the range 0 - 63).
-> See [RTP Source params](#rtp-source) for all the other parameters.
-
-> **sinks**
-> JSON array of the configured sinks.
-> 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.
-
+# AES67 Daemon #
+
+AES67 daemon uses the Merging Technologies device driver (MergingRavennaALSA.ko) to implement basic AES67 functionalities. See [ALSA RAVENNA/AES67 Driver README](https://bitbucket.org/MergingTechnologies/ravenna-alsa-lkm/src/master/README.md) for additional information.
+
+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 and SAP browser
+* IGMP handling for SAP and RTP sessions
+
+## Configuration file ##
+
+The daemon uses a JSON file to store the configuration parameters.
+The config file must be specified at startup time and it gets updated when new params are set via the REST interface.
+See [JSON config params](#config) for additional info on the configuration parameters.
+If a status file is specified in the daemon's configuration the server will load it at startup ad will save it at termination.
+The status file contains all the configured sources and sinks (streams).
+See [JSON streams](#rtp-streams) for additional info on the status file format and its parameters.
+
+## HTTP REST API ##
+
+The daemon implements a REST API interface to configure and control the driver.
+All operations returns HTTP *200* status code in case of success and HTTP *4xx* or *5xx* status code in case of failure.
+In case of failure the server returns a **text/plain** content type with the category and a description of the error occurred.
+**_NOTE:_** At present the embedded HTTP server doesn't implement neither HTTPS nor user authentication.
+
+### Get Daemon Configuration ###
+* **URL** /api/config
+* **Method** GET
+* **URL Params** none
+* **Body Type** application/json
+* **Body** [Config params](#config)
+
+### Set Daemon Configuration ###
+* **URL** /api/config
+* **Method** POST
+* **URL Params** none
+* **Body Type** application/json
+* **Body** [Config params](#config)
+
+### Get PTP Configuration ###
+* **URL** /api/ptp/config
+* **Method** GET
+* **URL Params** none
+* **Body Type** application/json
+* **Body** [PTP Config params](#ptp-config)
+
+### Set PTP Configuration ###
+* **URL** /api/ptp/config
+* **Method** POST
+* **URL Params** none
+* **Body Type** application/json
+* **Body** [PTP Config params](#ptp-config)
+
+### Get PTP Status ###
+* **URL** /api/ptp/status
+* **Method** GET
+* **URL Params** none
+* **Body Type** application/json
+* **Body** [PTP Status params](#ptp-status)
+
+### Add RTP Source ###
+* **Description** add or update the RTP source specified by the *id*
+* **URL** /api/source/:id
+* **Method** PUT
+* **URL Params** id=[integer in the range (0-63)]
+* **Body Type** application/json
+* **Body** [RTP Source params](#rtp-source)
+
+### Remove RTP Source ###
+* **Description** remove the RTP sink specified by the *id*
+* **URL** /api/source/:id
+* **Method** DELETE
+* **URL Params** id=[integer in the range (0-63)]
+* **Body** none
+
+### Get RTP Source SDP file ###
+* **Description** retrieve the SDP of the source specified by *id*
+* **URL** /api/source/sdp/:id
+* **Method** GET
+* **URL Params** id=[integer in the range (0-63)]
+* **Body Type** application/sdp
+* **Body** [Example SDP file for a source](#rtp-source-sdp)
+
+### Add RTP Sink ###
+* **Description** add or update the RTP sink specified by the *id*
+* **URL** /api/sink/:id
+* **Method** PUT
+* **URL Params** id=[integer in the range (0-63)]
+* **Body Type** application/json
+* **Body** [RTP Sink params](#rtp-sink)
+
+### Remove RTP Sink ###
+* **Description** remove the RTP sink specified by *id*
+* **URL** /api/sink/:id
+* **Method** DELETE
+* **URL Params** id=[integer in the range (0-63)]
+* **Body** none
+
+### Get RTP Sink status ###
+* **Description** retrieve the status of the sink specified by *id*
+* **URL** /api/sink/status/:id
+* **Method** GET
+* **URL Params** id=[integer in the range (0-63)]
+* **Body Type** application/json
+* **Body** [RTP Sink status params](#rtp-sink-status)
+
+### Get all configured RTP Sources ###
+* **URL** /api/sources
+* **Method** GET
+* **URL Params** none
+* **Body type** application/json
+* **Body** [RTP Sources params](#rtp-sources)
+
+### Get all configured RTP Sinks ###
+* **URL** /api/sinks
+* **Method** GET
+* **URL Params** none
+* **Body type** application/json
+* **Body** [RTP Sinks params](#rtp-sinks)
+
+### Get all configured RTP Sources and Sinks (Streams) ###
+* **URL** /api/streams
+* **Method** GET
+* **URL Params** none
+* **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 ###
+
+Example
+
+ {
+ "interface_name": "lo",
+ "http_port": 8080,
+ "log_severity": 2,
+ "syslog_proto": "none",
+ "syslog_server": "255.255.255.254:1234",
+ "rtp_mcast_base": "239.2.0.1",
+ "status_file": "./status.json",
+ "rtp_port": "5004",
+ "ptp_domain": 0,
+ "ptp_dscp": 46,
+ "playout_delay": 0,
+ "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"
+ }
+
+where:
+
+> **interface\_name**
+> JSON string specifying the network interface used by the daemon and the driver for both the RTP, PTP, SAP and HTTP traffic.
+
+> **http\_port**
+> JSON number specifying the HTTP port number used by the embedded web server in the daemon implementing the REST interface.
+
+> **log\_severity**
+> JSON integer specifying the process log severity level (0 to 5).
+> All traces major or equal to the specified level are enabled. (0=trace, 1=debug, 2=info, 3=warning, 4=error, 5=fatal).
+
+> **syslog\_proto**
+> JSON string specifying the syslog protocol used for logging.
+> This can be an empty string to log to the local syslog, "udp" to send syslog messages
+> to a remote server or "none" to disable the syslog logging.
+> When "none" is used the client writes the logs to the standard output.
+
+> **syslog\_server**
+> JSON string specifying the syslog server address used for logging.
+
+> **status\_file**
+> JSON string specifying the file that will contain the sessions status.
+> The file is loaded when the daemon starts and is saved when the daemon exits.
+
+> **rtp\_mcast\_base**
+> JSON string specifying the default base RTP IPv4 multicast address used by a source.
+> The specific multicast RTP address is the base address plus the source id number.
+> For example if the base address is 239.2.0.1 and source id is 1 the RTP source address used is 239.2.0.2.
+
+> **rtp_port**
+> JSON number specifying the RTP port used by the sources.
+
+> **ptp\_domain**
+> JSON number specifying the PTP clock domain of the master clock the driver will attempt to synchronize to.
+
+> **ptp\_dscp**
+> JSON number specifying the IP DSCP used in IPv4 header for PTP traffic.
+> Valid values are 48 (CS6) and 46 (EF).
+
+> **sample\_rate**
+> JSON number specifying the default sample rate.
+> Valid values are 44100Hz, 48000Hz and 96000Hz.
+
+> **playout\_delay**
+> JSON number specifying the default safety playout delay at 1FS in samples.
+
+> **tic\_frame\_size\_at\_1fs**
+> JSON number specifying the RTP frame size at 1FS in samples.
+
+> **max\_tic\_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.
+
+> **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.
+
+> **mac\_addr**
+> JSON string specifying the MAC address of the specified network device.
+> **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the MAC address of the network device at startup time.
+
+> **ip\_addr**
+> JSON string specifying the IP address of the specified network device.
+> **_NOTE:_** This parameter is read-only and cannot be set. The server will determine the IP address of the network device at startup time and will monitor it periodically.
+
+### JSON PTP Config ###
+
+Example
+
+ {
+ "domain": 0,
+ "dscp": 46
+ }
+
+where:
+
+> **domain**
+> JSON number specifying the PTP clock domain of the master clock the driver will attempt to synchronize to.
+
+> **dscp**
+> JSON number specifying the IP DSCP used in IPv4 header for PTP traffic.
+> Valid values are 48 (CS6) and 46 (EF).
+
+### JSON PTP Status ###
+
+Example
+
+ {
+ "status": "unlocked",
+ "gmid": "00-00-00-FF-FE-00-00-00",
+ "jitter": 0
+ }
+
+where:
+
+> **status**
+> JSON string specifying the PTP slave status. This can be *unlocked*, *locking* or *locked*.
+
+> **gmid**
+> JSON string specifying GrandMaster clock ID we are currently synchronized to.
+
+> **jitter**
+> JSON number specifying the measured PTP packet delay jitter.
+
+### JSON RTP source ###
+
+Example:
+
+ {
+ "enabled": true,
+ "name": "ALSA Source 0",
+ "io": "Audio Device",
+ "codec": "L16",
+ "max_samples_per_packet": 48,
+ "ttl": 15,
+ "payload_type": 98,
+ "dscp": 34,
+ "refclk_ptp_traceable": true,
+ "map": [0, 1]
+ }
+
+where:
+
+> **enabled**
+> JSON bool specifying whether the source is enabled and will be announced or not.
+
+> **name**
+> JSON string specifying the source name.
+
+> **io**
+> JSON string specifying the IO name.
+
+> **codec**
+> JSON string specifying codec in use.
+> Valid values are L16 and L24.
+
+> **max\_sample\_per\_packet**
+> JSON number specifying the max number of samples contained in one RTP packet.
+> Valid values are 12, 16, 48, 96, 192.
+> See the table below for correspondent max RTP packet duration:
+> | | 44.1Khz | 48Khz | 96Khz |
+> |-----|----------|---------|---------|
+> | 12 | 272µs | 250µs | 125µs |
+> | 16 | 363µs | 333µs | 166µs |
+> | 48 | 1.088ms | 1ms | 500µs |
+> | 96 | 2.177ms | 2ms | 1ms |
+> | 192 | 4.353ms | 4ms | 2ms |
+
+> **ttl**
+> JSON number specifying RTP packets Time To Live.
+
+> **payload\_type**
+> JSON number specifying RTP Payload Type.
+
+> **dscp**
+> JSON number specifying the IP DSCP used in IPv4 header for RTP traffic.
+> Valid values are 46 (EF), 34 (AF41), 26 (AF31), 0 (BE).
+
+> **refclk\_ptp\_traceable**
+> JSON boolean specifying whether the PTP reference clock is traceable or not.
+> A reference clock source is traceable if it is known to be delivering traceable time.
+
+> **map**
+> JSON array of integers specifying the mapping between the RTP source and the ALSA playback device channels used during playback. The length of this map determines the number of channels of the source.
+
+### RTP source SDP ###
+
+Example:
+
+ v=0
+ o=- 0 0 IN IP4 127.0.0.1
+ s=ALSA Source 0
+ c=IN IP4 239.1.0.1/15
+ t=0 0
+ a=clock-domain:PTPv2 0
+ m=audio 5004 RTP/AVP 98
+ c=IN IP4 239.1.0.1/15
+ a=rtpmap:98 L16/44100/2
+ a=sync-time:0
+ a=framecount:48
+ a=ptime:1.08843537415
+ a=mediaclk:direct=0
+ a=ts-refclk:ptp=traceable
+ a=recvonly
+
+### JSON RTP sink ###
+
+Example:
+
+ {
+ "name": "ALSA Sink 0",
+ "io": "Audio Device",
+ "delay": 576,
+ "use_sdp": false,
+ "source": "http://127.0.0.1:8080/api/source/sdp/0",
+ "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
+ "ignore_refclk_gmid": false,
+ "map": [0, 1]
+ }
+
+where:
+
+> **name**
+> JSON string specifying the source name.
+
+> **io**
+> JSON string specifying the IO name.
+
+> **delay**
+> JSON number specifying the playout delay of the sink in samples.
+> **_NOTE:_** The specified delay cannot be less than the source max samples size announced by the SDP file.
+
+> **use\_sdp**
+> JSON boolean specifying whether the source SDP file is fetched from the HTTP URL specified in the **source** parameter or the SDP in the **sdp** parameter is used.
+
+> **source**
+> JSON string specifying the HTTP URL of the source SDP file. This parameter is mandatory if **use\_sdp** is false.
+
+> **sdp**
+> JSON string specifying the SDP of the source. This parameter is mandatory if **use\_sdp** is true.
+> See [example SDP file for a source](#rtp-source-sdp)
+
+> **ignore\_refclk\_gmid**
+> JSON boolean specifying whether the grand master reference clock ID specified in the SDP file of the source must be compared with the master reference clock to which the current PTP slave clock is syncronized.
+
+> **map**
+> JSON array of integers specifying the mapping between the RTP sink and the ALSA capture device channels used during recording. The length of this map determines the number of channels of the sink.
+
+### JSON RTP sink status ###
+
+Example:
+
+ {
+ "sink_flags":
+ {
+ "rtp_seq_id_error": false,
+ "rtp_ssrc_error": false,
+ "rtp_payload_type_error": false,
+ "rtp_sac_error": false,
+ "receiving_rtp_packet": false,
+ "some_muted": false,
+ "muted": true
+ },
+ "sink_min_time": 0
+}
+
+where:
+
+> **sink\_flags**
+> JSON object containing a set of flags reporting the RTP sink status.
+
+> - **rtp\_seq\_id\_error** JSON boolean specifying whether a wrong RTP sequence was detected.
+
+> - **rtp\_ssrc\_error** JSON boolean specifying whether a wrong RTP source is contributing to the incoming stream.
+
+> - **rtp\_payload\_type\_error** JSON boolean specifying whether a wrong payload type was received.
+
+> - **rtp\_sac\_error** JSON boolean specifying whether a packet with a wrong RTP timestamp was received.
+
+> - **receiving\_rtp\_packet** JSON boolean specifying whether the sink is currently receiving RTP packets from the source.
+
+> - **some\_muted** JSON boolean (not used)
+
+> - **muted** JSON boolean specifying whether the sink is currently muted.
+
+> **sink\_min\_time** JSON number specifying the minimum source RTP packet arrival time.
+
+### JSON RTP Sources ###
+
+Example:
+
+ {
+ "sources": [
+ {
+ "id": 0,
+ "enabled": true,
+ "name": "ALSA Source 0",
+ "io": "Audio Device",
+ "max_samples_per_packet": 48,
+ "codec": "L16",
+ "ttl": 15,
+ "payload_type": 98,
+ "dscp": 34,
+ "refclk_ptp_traceable": true,
+ "map": [ 0, 1 ]
+ } ]
+ }
+
+where:
+
+> **sources**
+> JSON array of the configured sources.
+> Every source is identified by the JSON number **id** (in the range 0 - 63).
+> See [RTP Source params](#rtp-source) for all the other parameters.
+
+### JSON RTP Sinks ###
+
+Example:
+
+ {
+ "sinks": [
+ {
+ "id": 0,
+ "name": "ALSA Sink 0",
+ "io": "Audio Device",
+ "use_sdp": true,
+ "source": "http://127.0.0.1:8080/api/source/sdp/0",
+ "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
+ "delay": 576,
+ "ignore_refclk_gmid": false,
+ "map": [ 0, 1 ]
+ } ]
+ }
+
+where:
+
+> **sinks**
+> JSON array of the configured sinks.
+> 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 RTP Streams ###
+
+Example:
+
+ {
+ "sources": [
+ {
+ "id": 0,
+ "enabled": true,
+ "name": "ALSA Source 0",
+ "io": "Audio Device",
+ "max_samples_per_packet": 48,
+ "codec": "L16",
+ "ttl": 15,
+ "payload_type": 98,
+ "dscp": 34,
+ "refclk_ptp_traceable": true,
+ "map": [ 0, 1 ]
+ } ],
+ "sinks": [
+ {
+ "id": 0,
+ "name": "ALSA Sink 0",
+ "io": "Audio Device",
+ "use_sdp": true,
+ "source": "http://127.0.0.1:8080/api/source/sdp/0",
+ "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=ALSA Source 0\nc=IN IP4 239.1.0.1/15\nt=0 0\na=clock-domain:PTPv2 0\nm=audio 5004 RTP/AVP 98\nc=IN IP4 239.1.0.1/15\na=rtpmap:98 L16/44100/2\na=sync-time:0\na=framecount:48\na=ptime:1.08843537415\na=mediaclk:direct=0\na=ts-refclk:ptp=traceable\na=recvonly\n",
+ "delay": 576,
+ "ignore_refclk_gmid": false,
+ "map": [ 0, 1 ]
+ } ]
+ }
+
+where:
+
+> **sources**
+> JSON array of the configured sources.
+> Every source is identified by the JSON number **id** (in the range 0 - 63).
+> See [RTP Source params](#rtp-source) for all the other parameters.
+
+> **sinks**
+> JSON array of the configured sinks.
+> 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 ###
+
+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.
+
diff --git a/daemon/browser.cpp b/daemon/browser.cpp
new file mode 100644
index 0000000..a49a704
--- /dev/null
+++ b/daemon/browser.cpp
@@ -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 .
+//
+
+#include
+#include "browser.hpp"
+
+using namespace std::chrono;
+using second_t = std::chrono::duration >;
+
+std::shared_ptr Browser::create(
+ std::shared_ptr config) {
+ // no need to be thread-safe here
+ static std::weak_ptr instance;
+ if (auto ptr = instance.lock()) {
+ return ptr;
+ }
+ auto ptr =
+ std::shared_ptr(new Browser(config));
+ instance = ptr;
+ return ptr;
+}
+
+std::list Browser::get_remote_sources() {
+ std::list 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(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(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(steady_clock::now() - sap_timepoint).count())
+ > sap_interval) {
+ sap_timepoint = steady_clock::now();
+ // remove all sessions no longer announced
+ auto offset = duration_cast(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;
+}
+
diff --git a/daemon/browser.hpp b/daemon/browser.hpp
new file mode 100644
index 0000000..23d0632
--- /dev/null
+++ b/daemon/browser.hpp
@@ -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 .
+//
+
+#ifndef _BROWSER_HPP_
+#define _BROWSER_HPP_
+
+#include
+#include
+#include
+#include
+
+#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 create(
+ std::shared_ptr 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 get_remote_sources();
+
+ protected:
+ // singleton, use create() to build
+ Browser(std::shared_ptr config)
+ : config_(config){};
+
+ bool worker();
+
+ std::shared_ptr config_;
+ std::future res_;
+ std::atomic_bool running_{false};
+
+ /* current sources */
+ std::map sources_;
+ mutable std::shared_mutex sources_mutex_;
+
+ SAP sap_{config_->get_sap_mcast_addr()};
+ IGMP igmp_;
+};
+
+#endif
diff --git a/daemon/config.cpp b/daemon/config.cpp
index 294a0db..ae27562 100644
--- a/daemon/config.cpp
+++ b/daemon/config.cpp
@@ -68,6 +68,10 @@ std::shared_ptr 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;
diff --git a/daemon/config.hpp b/daemon/config.hpp
index 005278d..d3b03fc 100644
--- a/daemon/config.hpp
+++ b/daemon/config.hpp
@@ -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};
diff --git a/daemon/daemon.conf b/daemon/daemon.conf
index 1fb4f2c..370318c 100644
--- a/daemon/daemon.conf
+++ b/daemon/daemon.conf
@@ -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",
diff --git a/daemon/http_server.cpp b/daemon/http_server.cpp
index 714339c..5b9bfcd 100644
--- a/daemon/http_server.cpp
+++ b/daemon/http_server.cpp
@@ -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 << " "
diff --git a/daemon/http_server.hpp b/daemon/http_server.hpp
index 532604b..22db076 100644
--- a/daemon/http_server.hpp
+++ b/daemon/http_server.hpp
@@ -24,19 +24,23 @@
#include "config.hpp"
#include "session_manager.hpp"
+#include "browser.hpp"
class HttpServer {
public:
HttpServer() = delete;
HttpServer(std::shared_ptr session_manager,
+ std::shared_ptr browser,
std::shared_ptr config)
: session_manager_(session_manager),
+ browser_(browser),
config_(config) {};
bool start();
bool stop();
private:
std::shared_ptr session_manager_;
+ std::shared_ptr browser_;
std::shared_ptr config_;
httplib::Server svr_;
std::future res_;
diff --git a/daemon/json.cpp b/daemon/json.cpp
index 218421b..b29eb1d 100644
--- a/daemon/json.cpp
+++ b/daemon/json.cpp
@@ -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& 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& 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());
} else if (key == "ptp_dscp") {
config.set_ptp_dscp(val.get_value());
+ } else if (key == "sap_mcast_addr") {
+ config.set_sap_mcast_addr(remove_undesired_chars(val.get_value()));
} else if (key == "sap_interval") {
config.set_sap_interval(val.get_value());
} else if (key == "status_file") {
diff --git a/daemon/json.hpp b/daemon/json.hpp
index 08357b0..c75214f 100644
--- a/daemon/json.hpp
+++ b/daemon/json.hpp
@@ -22,6 +22,7 @@
#include
#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& sources);
std::string sinks_to_json(const std::list& sinks);
std::string streams_to_json(const std::list& sources,
const std::list& sinks);
+std::string remote_source_to_json(const RemoteSource& source);
+std::string remote_sources_to_json(const std::list& sources);
/* JSON deserializers */
Config json_to_config(std::istream& jstream, const Config& curCconfig);
diff --git a/daemon/main.cpp b/daemon/main.cpp
index 1695d33..9f91f50 100644
--- a/daemon/main.cpp
+++ b/daemon/main.cpp
@@ -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;
@@ -114,9 +115,16 @@ int main(int argc, char* argv[]) {
throw std::runtime_error(
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(
@@ -161,7 +175,7 @@ int main(int argc, char* argv[]) {
if (!driver->terminate()) {
throw std::runtime_error(
std::string("DriverManager:: terminate failed"));
- }
+ }
} catch (std::exception& e) {
BOOST_LOG_TRIVIAL(fatal) << "main:: fatal exception error: " << e.what();
diff --git a/daemon/netlink.hpp b/daemon/netlink.hpp
index 40e4809..5afb471 100644
--- a/daemon/netlink.hpp
+++ b/daemon/netlink.hpp
@@ -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); }
};
diff --git a/daemon/sap.cpp b/daemon/sap.cpp
new file mode 100644
index 0000000..eb542af
--- /dev/null
+++ b/daemon/sap.cpp
@@ -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 .
+//
+
+#include
+#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;
+}
diff --git a/daemon/sap.hpp b/daemon/sap.hpp
index a13dda3..330bd4c 100644
--- a/daemon/sap.hpp
+++ b/daemon/sap.hpp
@@ -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
diff --git a/daemon/session_manager.hpp b/daemon/session_manager.hpp
index 5cbe64b..d59c13d 100644
--- a/daemon/session_manager.hpp
+++ b/daemon/session_manager.hpp
@@ -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_;
};
diff --git a/daemon/tests/daemon.conf b/daemon/tests/daemon.conf
index 0c3d755..e2be564 100644
--- a/daemon/tests/daemon.conf
+++ b/daemon/tests/daemon.conf
@@ -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",
diff --git a/daemon/tests/daemon_test.cpp b/daemon/tests/daemon_test.cpp
index b5d6b4a..09a7ca1 100644
--- a/daemon/tests/daemon_test.cpp
+++ b/daemon/tests/daemon_test.cpp
@@ -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 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("sdp") == sdp.second,
+ "returned source " + v.second.get("id"));
+ }
+ BOOST_REQUIRE_MESSAGE(cli.remove_source(0), "removed source 0");
+ cli.sap_wait_deletion(0, sdp.second, 3);
+ json = cli.get_remote_sources();
+ BOOST_REQUIRE_MESSAGE(json.first, "got remote sources");
+ 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("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));
diff --git a/webui/public/index.html b/webui/public/index.html
index aab5e3b..2d2dafa 100644
--- a/webui/public/index.html
+++ b/webui/public/index.html
@@ -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`.
-->
- React App
+ AES64 Daemon WebUI
diff --git a/webui/public/info.png b/webui/public/info.png
new file mode 100755
index 0000000..25ac2a1
Binary files /dev/null and b/webui/public/info.png differ
diff --git a/webui/src/Config.js b/webui/src/Config.js
index 900983c..81af1b8 100644
--- a/webui/src/Config.js
+++ b/webui/src/Config.js
@@ -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 {
diff --git a/webui/src/ConfigTabs.js b/webui/src/ConfigTabs.js
index ffbb22f..2ed4090 100644
--- a/webui/src/ConfigTabs.js
+++ b/webui/src/ConfigTabs.js
@@ -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 {
+
+
+
);
diff --git a/webui/src/RemoteSources.js b/webui/src/RemoteSources.js
new file mode 100644
index 0000000..7ebc232
--- /dev/null
+++ b/webui/src/RemoteSources.js
@@ -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 .
+//
+//
+
+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 (
+