diff --git a/Dockerfile.daemon_tests b/Dockerfile.daemon_tests
index 4173851..8c9e5d4 100644
--- a/Dockerfile.daemon_tests
+++ b/Dockerfile.daemon_tests
@@ -3,7 +3,7 @@ RUN echo 'APT::Install-Suggests "0";' >> /etc/apt/apt.conf.d/00-docker
RUN echo 'APT::Install-Recommends "0";' >> /etc/apt/apt.conf.d/00-docker
RUN DEBIAN_FRONTEND=noninteractive \
apt-get update && \
- apt-get install -qq -f -y build-essential clang git cmake libboost-all-dev valgrind linux-sound-base alsa-base alsa-utils libasound2-dev libavahi-client-dev \
+ apt-get install -qq -f -y build-essential clang git cmake libboost-all-dev valgrind linux-sound-base alsa-base alsa-utils libasound2-dev libavahi-client-dev libfaac-dev \
&& rm -rf /var/lib/apt/lists/*
COPY . .
RUN ./buildfake.sh
diff --git a/README.md b/README.md
index 1730560..2f874e3 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ The daemon uses the following open source:
* **cpp-httplib** licensed under the [MIT License](https://github.com/yhirose/cpp-httplib/blob/master/LICENSE)
* **Avahi common & client libraries** licensed under the [LGPL License](https://github.com/lathiat/avahi/blob/master/LICENSE)
* **Boost libraries** licensed under the [Boost Software License](https://www.boost.org/LICENSE_1_0.txt)
+* **Freeware Advanced Audio Coder** licensed under the [LGPL License](https://github.com/knik0/faac?tab=License-1-ov-file)
## Prerequisite ##
@@ -48,6 +49,7 @@ The daemon and the test have been verified starting from **Ubuntu 18.04** distro
* cmake version >= 3.7
* boost libraries version >= 1.65
* Avahi service discovery (if enabled) >= 0.7
+* Freeware Advanced Audio Coder (if streamer enabled) libfaac >= 1.30
The following platforms have been used for testing:
@@ -104,9 +106,21 @@ The daemon should work on all Ubuntu starting from 18.04 onward, it's possible t
## Devices and interoperability tests ##
See [Devices and interoperability tests with the AES67 daemon](DEVICES.md)
+## HTTP Streamer ##
+The HTTP Streamer was introduced with the daemon version 2.0 and it is used to receive AES67 audio streams via HTTP file streaming.
+
+The HTTP Streamer can be enabled via the _streamer_enabled_ daemon parameter.
+When the Streamer is active the daemon starts capturing the configured _Sinks_ up to the maximum number of channels configured by the _streamer_channels_ parameters.
+The captured PCM samples are split into _streamer_files_num_ files of _streamer_file_duration_ duration (in seconds) for each sink, compressed using AAC LC codec and served via HTTP.
+![Screenshot 2024-06-15 at 15 36 48](https://github.com/bondagit/aes67-linux-daemon/assets/56439183/3341b05e-daed-4541-b0a1-28839d5b9a6b)
+The HTTP streamer requires the libfaac-dev package to compile.
+
+Please note that since the HTTP Streamer uses the RAVENNA ALSA device for capturing it's not possible to use such device for other audio captures.
+
## AES67 USB Receiver and Transmitter ##
See [Use your board as AES67 USB Receiver and Transmitter](USB_GADGET.md)
+
## Repository content ##
### [daemon](daemon) directory ###
diff --git a/build.sh b/build.sh
index 1ead070..c4535c2 100755
--- a/build.sh
+++ b/build.sh
@@ -43,7 +43,7 @@ cd ..
cd daemon
echo "Building aes67-daemon ..."
-cmake -DCPP_HTTPLIB_DIR="$TOPDIR"/3rdparty/cpp-httplib -DRAVENNA_ALSA_LKM_DIR="$TOPDIR"/3rdparty/ravenna-alsa-lkm -DENABLE_TESTS=ON -DWITH_AVAHI=ON -DFAKE_DRIVER=OFF -DWITH_SYSTEMD=OFF .
+cmake -DCPP_HTTPLIB_DIR="$TOPDIR"/3rdparty/cpp-httplib -DRAVENNA_ALSA_LKM_DIR="$TOPDIR"/3rdparty/ravenna-alsa-lkm -DENABLE_TESTS=ON -DWITH_AVAHI=ON -DFAKE_DRIVER=OFF -DWITH_SYSTEMD=ON .
make
cd ..
diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt
index 2b20e2e..38a0a83 100644
--- a/daemon/CMakeLists.txt
+++ b/daemon/CMakeLists.txt
@@ -24,6 +24,9 @@ if (NOT CPP_HTTPLIB_DIR)
find_path( CPP_HTTPLIB_DIR "httplib.h" REQUIRED)
endif()
+find_library(ALSA_LIBRARY NAMES asound)
+find_library(AAC_LIBRARY NAMES faac)
+
find_library(AVAHI_LIBRARY-COMMON NAMES avahi-common)
find_library(AVAHI_LIBRARY-CLIENT NAMES avahi-client)
find_path(AVAHI_INCLUDE_DIR avahi-client/publish.h)
@@ -34,7 +37,7 @@ find_package(Boost COMPONENTS system thread log program_options REQUIRED)
include_directories(aes67-daemon ${RAVENNA_ALSA_LKM_DIR}/common ${RAVENNA_ALSA_LKM_DIR}/driver ${CPP_HTTPLIB_DIR} ${Boost_INCLUDE_DIR})
add_definitions( -DBOOST_LOG_DYN_LINK -DBOOST_LOG_USE_NATIVE_SYSLOG )
add_compile_options( -Wall )
-set(SOURCES error_code.cpp json.cpp main.cpp session_manager.cpp http_server.cpp config.cpp interface.cpp log.cpp sap.cpp browser.cpp rtsp_client.cpp mdns_client.cpp mdns_server.cpp rtsp_server.cpp utils.cpp)
+set(SOURCES error_code.cpp json.cpp main.cpp session_manager.cpp http_server.cpp config.cpp interface.cpp log.cpp sap.cpp browser.cpp rtsp_client.cpp mdns_client.cpp mdns_server.cpp rtsp_server.cpp utils.cpp streamer.cpp)
if(FAKE_DRIVER)
MESSAGE(STATUS "FAKE_DRIVER")
@@ -49,7 +52,7 @@ if(ENABLE_TESTS)
add_subdirectory(tests)
endif()
-target_link_libraries(aes67-daemon ${Boost_LIBRARIES})
+target_link_libraries(aes67-daemon ${Boost_LIBRARIES} ${ALSA_LIBRARY} ${AAC_LIBRARY})
if(WITH_AVAHI)
MESSAGE(STATUS "WITH_AVAHI")
add_definitions(-D_USE_AVAHI_)
diff --git a/daemon/README.md b/daemon/README.md
index 2317182..fda9fb0 100644
--- a/daemon/README.md
+++ b/daemon/README.md
@@ -148,13 +148,31 @@ In case of failure the server returns a **text/plain** content type with the cat
* **Body type** application/json
* **Body** [RTP Remote Sources params](#rtp-remote-sources)
+### Get streamer info for a Sink ###
+* **Description** retrieve the streamer info for the specified Sink
+* **URL** /api/streamer/info/:id
+* **Method** GET
+* **URL Params** id=[integer in the range (0-63)]
+* **Body Type** application/json
+* **Body** [Streamer info params](#streamer-info)
+
+### Get streamer AAC audio file ###
+* **Description** retrieve the AAC audio frames for the specified Sink and file id
+* **URL** /api/streamer/streamer/:sinkId/:fileId
+* **Method** GET
+* **URL Params** sinkId=[integer in the range (0-63)], fileId=[integer in the range (0-*streamer_files_num*)]
+* **HTTP headers** the headers _X-File-Count_, _X-File-Current-Id_, _X-File-Start-Id_ return the current global file count, the current file id and the start file id for the file returned
+* **Body Type** audio/aac
+* **Body** Binary body containing ADTS AAC LC audio frames
+
+
## HTTP REST API structures ##
### JSON Version ###
Example
{
- "version:" "bondagit-1.5"
+ "version:" "bondagit-2.0"
}
where:
@@ -189,7 +207,12 @@ Example
"node_id": "AES67 daemon d9aca383",
"custom_node_id": "",
"ptp_status_script": "./scripts/ptp_status.sh",
- "auto_sinks_update": true
+ "auto_sinks_update": true,
+ "streamer_enabled": false,
+ "streamer_channels": 8,
+ "streamer_files_num": 6,
+ "streamer_file_duration": 1,
+ "streamer_player_buffer_files_num": 1
}
where:
@@ -284,6 +307,23 @@ where:
> JSON string specifying the path to the script executed in background when the PTP slave clock status changes.
> The PTP clock status is passed as first parameter to the script and it can be *unlocked*, *locking* or *locked*.
+> **streamer\_enabled**
+> JSON boolean specifying whether the HTTP Streamer is enabled or disabled.
+> Once activated, the HTTP Streamer starts capturing samples for number of channels specified by *streamer_channels* starting from channel 0, then it splits them into *streamer_files_num* files of a *streamer_file_duration* duration for each configured Sink and it serves them via HTTP.
+
+> **streamer\_channels**
+> JSON number specifying the number of channels captured by the HTTP Streamer starting from channel 0, 8 by default.
+
+> **streamer\_files\_num**
+> JSON number specifying the number of files into which the stream gets split.
+
+> **streamer\_file\_duration**
+> JSON number specifying the maximum duration of each streamer file in seconds.
+
+> **streamer\_player\_buffer\_files\_num**
+> JSON number specifying the player buffer in number of files.
+
+
### JSON PTP Config ###
Example
@@ -658,3 +698,55 @@ where:
> 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.
+### JSON Streamer info ###
+
+Example:
+
+ {
+ "status": 0,
+ "file_duration": 1,
+ "files_num": 8,
+ "player_buffer_files_num": 1,
+ "start_file_id": 3,
+ "current_file_id": 0,
+ "channels": 2,
+ "format": "s16",
+ "rate": 48000
+ }
+
+where:
+
+> **status**
+> JSON number containing the streamer status code.
+> Status is 0 in case the streamer is able to provide the audio samples, othrewise the specific error code is returned.
+> 0 - OK
+> 1 - PTP clock not locked
+> 2 - Channel/s not captured
+> 3 - Buffering
+> 4 - Streamer not enabled
+> 5 - Invalid Sink
+> 6 - Cannot retrieve Sink
+
+> **file_duration_sec**
+> JSON number specifying the duration of each file.
+
+> **files_num**
+> JSON number specifying the number of files. The streamer will use these files as a circular buffer.
+
+> **start_file_id**
+> JSON number specifying the file id to use to start the playback.
+
+> **current_file_id**
+> JSON number specifying the file id that is beeing created by the daemon.
+
+> **player_buffer_files_num**
+> JSON number specifying the number of files to use for buffering.
+
+> **channels**
+> JSON number specifying the number of channels of the stream.
+
+> **format**
+> JSON string specifying the PCM encoding of the AAC compressed stream.
+
+> **rate**
+> JSON number specifying the sample rate of the stream.
diff --git a/daemon/browser.hpp b/daemon/browser.hpp
index 3915af0..985eaea 100644
--- a/daemon/browser.hpp
+++ b/daemon/browser.hpp
@@ -100,7 +100,8 @@ class Browser : public MDNSClient {
SAP sap_{config_->get_sap_mcast_addr()};
IGMP igmp_;
- std::chrono::time_point startup_{std::chrono::steady_clock::now()};
+ std::chrono::time_point startup_{
+ std::chrono::steady_clock::now()};
uint32_t last_update_{0}; /* seconds from daemon startup */
};
diff --git a/daemon/config.cpp b/daemon/config.cpp
index c1c6113..08bd7c5 100644
--- a/daemon/config.cpp
+++ b/daemon/config.cpp
@@ -66,6 +66,15 @@ std::shared_ptr Config::parse(const std::string& filename,
config.max_tic_frame_size_ = 1024;
if (config.sample_rate_ == 0)
config.sample_rate_ = 48000;
+ if (config.streamer_channels_ < 2 || config.streamer_channels_ > 16)
+ config.streamer_channels_ = 8;
+ if (config.streamer_file_duration_ < 1 || config.streamer_file_duration_ > 4)
+ config.streamer_file_duration_ = 1;
+ if (config.streamer_files_num_ < 4 || config.streamer_files_num_ > 16)
+ config.streamer_files_num_ = 8;
+ if (config.streamer_player_buffer_files_num_ < 1 || config.streamer_player_buffer_files_num_ > 2)
+ config.streamer_player_buffer_files_num_ = 1;
+
boost::system::error_code ec;
ip::address_v4::from_string(config.rtp_mcast_base_.c_str(), ec);
if (ec) {
@@ -126,16 +135,22 @@ bool Config::save(const Config& config) {
get_max_tic_frame_size() != config.get_max_tic_frame_size() ||
get_interface_name() != config.get_interface_name();
- daemon_restart_ = driver_restart_ ||
- get_http_port() != config.get_http_port() ||
- get_rtsp_port() != config.get_rtsp_port() ||
- get_http_base_dir() != config.get_http_base_dir() ||
- get_rtp_mcast_base() != config.get_rtp_mcast_base() ||
- get_sap_mcast_addr() != config.get_sap_mcast_addr() ||
- get_rtp_port() != config.get_rtp_port() ||
- get_status_file() != config.get_status_file() ||
- get_mdns_enabled() != config.get_mdns_enabled() ||
- get_custom_node_id() != config.get_custom_node_id();
+ daemon_restart_ =
+ driver_restart_ || get_http_port() != config.get_http_port() ||
+ get_rtsp_port() != config.get_rtsp_port() ||
+ get_http_base_dir() != config.get_http_base_dir() ||
+ get_rtp_mcast_base() != config.get_rtp_mcast_base() ||
+ get_sap_mcast_addr() != config.get_sap_mcast_addr() ||
+ get_rtp_port() != config.get_rtp_port() ||
+ get_status_file() != config.get_status_file() ||
+ get_mdns_enabled() != config.get_mdns_enabled() ||
+ get_custom_node_id() != config.get_custom_node_id() ||
+ get_streamer_channels() != config.get_streamer_channels() ||
+ get_streamer_file_duration() != config.get_streamer_file_duration() ||
+ get_streamer_files_num() != config.get_streamer_files_num() ||
+ get_streamer_player_buffer_files_num() !=
+ config.get_streamer_player_buffer_files_num() ||
+ get_streamer_enabled() != config.get_streamer_enabled();
if (!daemon_restart_)
*this = config;
diff --git a/daemon/config.hpp b/daemon/config.hpp
index abff5a1..90a56e8 100644
--- a/daemon/config.hpp
+++ b/daemon/config.hpp
@@ -37,6 +37,15 @@ class Config {
uint16_t get_http_port() const { return http_port_; };
uint16_t get_rtsp_port() const { return rtsp_port_; };
const std::string& get_http_base_dir() const { return http_base_dir_; };
+ uint8_t get_streamer_files_num() const { return streamer_files_num_; };
+ uint16_t get_streamer_file_duration() const {
+ return streamer_file_duration_;
+ };
+ uint8_t get_streamer_player_buffer_files_num() const {
+ return streamer_player_buffer_files_num_;
+ };
+ uint8_t get_streamer_channels() const { return streamer_channels_; };
+ bool get_streamer_enabled() const { return streamer_enabled_; };
int get_log_severity() const { return log_severity_; };
uint32_t get_playout_delay() const { return playout_delay_; };
uint32_t get_tic_frame_size_at_1fs() const { return tic_frame_size_at_1fs_; };
@@ -78,6 +87,22 @@ class Config {
void set_http_base_dir(std::string_view http_base_dir) {
http_base_dir_ = http_base_dir;
};
+ void set_streamer_channels(uint8_t streamer_channels) {
+ streamer_channels_ = streamer_channels;
+ };
+ void set_streamer_files_num(uint8_t streamer_files_num) {
+ streamer_files_num_ = streamer_files_num;
+ };
+ void set_streamer_file_duration(uint16_t streamer_file_duration) {
+ streamer_file_duration_ = streamer_file_duration;
+ };
+ void set_streamer_player_buffer_files_num(
+ uint8_t streamer_player_buffer_files_num) {
+ streamer_player_buffer_files_num_ = streamer_player_buffer_files_num;
+ };
+ void set_streamer_enabled(uint8_t streamer_enabled) {
+ streamer_enabled_ = streamer_enabled;
+ };
void set_log_severity(int log_severity) { log_severity_ = log_severity; };
void set_playout_delay(uint32_t playout_delay) {
playout_delay_ = playout_delay;
@@ -137,6 +162,13 @@ class Config {
lhs.get_http_port() != rhs.get_http_port() ||
lhs.get_rtsp_port() != rhs.get_rtsp_port() ||
lhs.get_http_base_dir() != rhs.get_http_base_dir() ||
+ lhs.get_streamer_channels() != rhs.get_streamer_channels() ||
+ lhs.get_streamer_files_num() != rhs.get_streamer_files_num() ||
+ lhs.get_streamer_file_duration() !=
+ rhs.get_streamer_file_duration() ||
+ lhs.get_streamer_player_buffer_files_num() !=
+ rhs.get_streamer_player_buffer_files_num() ||
+ lhs.get_streamer_enabled() != rhs.get_streamer_enabled() ||
lhs.get_log_severity() != rhs.get_log_severity() ||
lhs.get_playout_delay() != rhs.get_playout_delay() ||
lhs.get_tic_frame_size_at_1fs() != rhs.get_tic_frame_size_at_1fs() ||
@@ -166,6 +198,11 @@ class Config {
uint16_t http_port_{8080};
uint16_t rtsp_port_{8854};
std::string http_base_dir_{"../webui/dist"};
+ uint8_t streamer_channels_{8};
+ uint8_t streamer_files_num_{8};
+ uint16_t streamer_file_duration_{1};
+ uint8_t streamer_player_buffer_files_num_{1};
+ bool streamer_enabled_{false};
int log_severity_{2};
uint32_t playout_delay_{0};
uint32_t tic_frame_size_at_1fs_{48};
diff --git a/daemon/daemon.conf b/daemon/daemon.conf
index dd54cb0..245ac61 100644
--- a/daemon/daemon.conf
+++ b/daemon/daemon.conf
@@ -20,5 +20,10 @@
"mdns_enabled": true,
"custom_node_id": "",
"ptp_status_script": "./scripts/ptp_status.sh",
+ "streamer_channels": 8,
+ "streamer_files_num": 8,
+ "streamer_file_duration": 1,
+ "streamer_player_buffer_files_num": 1,
+ "streamer_enabled": false,
"auto_sinks_update": true
}
diff --git a/daemon/driver_manager.cpp b/daemon/driver_manager.cpp
index f1fe34c..e85d6fe 100644
--- a/daemon/driver_manager.cpp
+++ b/daemon/driver_manager.cpp
@@ -258,7 +258,7 @@ std::error_code DriverManager::get_number_of_outputs(int32_t& outputs) {
void DriverManager::on_command_done(enum MT_ALSA_msg_id id,
size_t size,
const uint8_t* data) {
- BOOST_LOG_TRIVIAL(info) << "driver_manager:: cmd " << alsa_msg_str[id]
+ BOOST_LOG_TRIVIAL(debug) << "driver_manager:: cmd " << alsa_msg_str[id]
<< " done data len " << size;
memcpy(recv_data_, data, size);
retcode_ = std::error_code{};
diff --git a/daemon/error_code.cpp b/daemon/error_code.cpp
index 95bcf7c..7f7ce5b 100644
--- a/daemon/error_code.cpp
+++ b/daemon/error_code.cpp
@@ -117,6 +117,12 @@ std::string DaemonErrCategory::message(int ev) const {
return "failed to receive event from driver";
case DaemonErrc::invalid_driver_response:
return "unexpected driver command response code";
+ case DaemonErrc::streamer_invalid_ch:
+ return "sink channel not captured";
+ case DaemonErrc::streamer_retry_later:
+ return "not enough samples buffered, retry later";
+ case DaemonErrc::streamer_not_running:
+ return "not running, check PTP lock";
default:
return "(unrecognized daemon error)";
}
diff --git a/daemon/error_code.hpp b/daemon/error_code.hpp
index e492a11..6f0f86f 100644
--- a/daemon/error_code.hpp
+++ b/daemon/error_code.hpp
@@ -53,12 +53,15 @@ enum class DaemonErrc {
cannot_parse_sdp = 45, // daemon cannot parse SDP
stream_name_in_use = 46, // daemon source or sink name in use
cannot_retrieve_mac = 47, // daemon cannot retrieve MAC for IP
- send_invalid_size = 50, // daemon data size too big for buffer
- send_u2k_failed = 51, // daemon failed to send command to driver
- send_k2u_failed = 52, // daemon failed to send event response to driver
- receive_u2k_failed = 53, // daemon failed to receive response from driver
- receive_k2u_failed = 54, // daemon failed to receive event from driver
- invalid_driver_response = 55 // unexpected driver command response code
+ streamer_invalid_ch = 48, // daemon streamer sink channel not captured
+ streamer_retry_later = 49, // daemon streamer not enough samples buffered
+ streamer_not_running = 50, // daemon streamer not running
+ send_invalid_size = 60, // daemon data size too big for buffer
+ send_u2k_failed = 61, // daemon failed to send command to driver
+ send_k2u_failed = 62, // daemon failed to send event response to driver
+ receive_u2k_failed = 63, // daemon failed to receive response from driver
+ receive_k2u_failed = 64, // daemon failed to receive event from driver
+ invalid_driver_response = 65 // unexpected driver command response code
};
namespace std {
diff --git a/daemon/http_server.cpp b/daemon/http_server.cpp
index cdadc41..ee56d5e 100644
--- a/daemon/http_server.cpp
+++ b/daemon/http_server.cpp
@@ -322,6 +322,97 @@ bool HttpServer::init() {
res.body = remote_sources_to_json(sources);
});
+ /* retrieve streamer info and position */
+ svr_.Get("/api/streamer/info/([0-9]+)", [this](const Request& req,
+ Response& res) {
+ uint32_t id;
+ StreamerInfo info;
+ enum class streamer_info_status {
+ ok,
+ ptp_not_locked,
+ invalid_channels,
+ buffering,
+ not_enabled,
+ invalid_sink,
+ cannot_retrieve
+ };
+
+ info.status = static_cast(streamer_info_status::ok);
+ if (!config_->get_streamer_enabled()) {
+ info.status = static_cast(streamer_info_status::not_enabled);
+ } else {
+ try {
+ id = std::stoi(req.matches[1]);
+ } catch (...) {
+ info.status = static_cast(streamer_info_status::invalid_sink);
+ }
+ }
+
+ if (info.status == static_cast(streamer_info_status::ok)) {
+ StreamSink sink;
+ auto ret = session_manager_->get_sink(id, sink);
+ if (ret) {
+ info.status =
+ static_cast(streamer_info_status::cannot_retrieve);
+ } else {
+ ret = streamer_->get_info(sink, info);
+ switch (ret.value()) {
+ case static_cast(DaemonErrc::streamer_not_running):
+ info.status =
+ static_cast(streamer_info_status::ptp_not_locked);
+ break;
+ case static_cast(DaemonErrc::streamer_invalid_ch):
+ info.status =
+ static_cast(streamer_info_status::invalid_channels);
+ break;
+ case static_cast(DaemonErrc::streamer_retry_later):
+ info.status = static_cast(streamer_info_status::buffering);
+ break;
+ default:
+ info.status = static_cast(streamer_info_status::ok);
+ break;
+ }
+ }
+ }
+ set_headers(res, "application/json");
+ res.body = streamer_info_to_json(info);
+ });
+
+ /* retrieve streamer file */
+ svr_.Get("/api/streamer/stream/([0-9]+)/([0-9]+)", [this](const Request& req,
+ Response& res) {
+ if (!config_->get_streamer_enabled()) {
+ set_error(400, "streamer not enabled", res);
+ return;
+ }
+ uint8_t sinkId, fileId;
+ try {
+ sinkId = std::stoi(req.matches[1]);
+ fileId = std::stoi(req.matches[2]);
+ } catch (...) {
+ set_error(400, "failed to convert id", res);
+ return;
+ }
+ StreamSink sink;
+ auto ret = session_manager_->get_sink(sinkId, sink);
+ if (ret) {
+ set_error(ret, "failed to retrieve sink " + std::to_string(sinkId), res);
+ return;
+ }
+ uint8_t currentFileId, startFileId;
+ uint32_t fileCount;
+ ret = streamer_->get_stream(sink, fileId, currentFileId, startFileId,
+ fileCount, res.body);
+ if (ret) {
+ set_error(ret, "failed to fetch stream " + std::to_string(sinkId), res);
+ return;
+ }
+ set_headers(res, "audio/aac");
+ res.set_header("X-File-Count", std::to_string(fileCount));
+ res.set_header("X-File-Current-Id", std::to_string(currentFileId));
+ res.set_header("X-File-Start-Id", std::to_string(startFileId));
+ });
+
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 1b561f5..65a6667 100644
--- a/daemon/http_server.hpp
+++ b/daemon/http_server.hpp
@@ -25,20 +25,26 @@
#include "browser.hpp"
#include "config.hpp"
#include "session_manager.hpp"
+#include "streamer.hpp"
class HttpServer {
public:
HttpServer() = delete;
explicit HttpServer(std::shared_ptr session_manager,
std::shared_ptr browser,
+ std::shared_ptr streamer,
std::shared_ptr config)
- : session_manager_(session_manager), browser_(browser), config_(config){};
+ : session_manager_(session_manager),
+ browser_(browser),
+ streamer_(streamer),
+ config_(config){};
bool init();
bool terminate();
private:
std::shared_ptr session_manager_;
std::shared_ptr browser_;
+ std::shared_ptr streamer_;
std::shared_ptr config_;
httplib::Server svr_;
std::future res_;
diff --git a/daemon/json.cpp b/daemon/json.cpp
index f4fbde9..27cd0ec 100644
--- a/daemon/json.cpp
+++ b/daemon/json.cpp
@@ -111,6 +111,16 @@ std::string config_to_json(const Config& config) {
<< ",\n \"mac_addr\": \"" << escape_json(config.get_mac_addr_str())
<< "\""
<< ",\n \"ip_addr\": \"" << escape_json(config.get_ip_addr_str()) << "\""
+ << ",\n \"streamer_channels\": "
+ << unsigned(config.get_streamer_channels())
+ << ",\n \"streamer_files_num\": "
+ << unsigned(config.get_streamer_files_num())
+ << ",\n \"streamer_file_duration\": "
+ << unsigned(config.get_streamer_file_duration())
+ << ",\n \"streamer_player_buffer_files_num\": "
+ << unsigned(config.get_streamer_player_buffer_files_num())
+ << ",\n \"streamer_enabled\": " << std::boolalpha
+ << config.get_streamer_enabled()
<< ",\n \"auto_sinks_update\": " << std::boolalpha
<< config.get_auto_sinks_update() << "\n}\n";
return ss.str();
@@ -257,7 +267,10 @@ std::string remote_source_to_json(const RemoteSource& source) {
<< ",\n \"domain\": \"" << escape_json(source.domain) << "\""
<< ",\n \"address\": \"" << escape_json(source.address) << "\""
<< ",\n \"sdp\": \"" << escape_json(source.sdp) << "\""
- << ",\n \"last_seen\": " << unsigned(duration_cast(steady_clock::now() - source.last_seen_timepoint).count())
+ << ",\n \"last_seen\": "
+ << unsigned(duration_cast(steady_clock::now() -
+ source.last_seen_timepoint)
+ .count())
<< ",\n \"announce_period\": " << unsigned(source.announce_period)
<< " \n }";
return ss.str();
@@ -277,6 +290,21 @@ std::string remote_sources_to_json(const std::list& sources) {
return ss.str();
}
+std::string streamer_info_to_json(const StreamerInfo& info) {
+ std::stringstream ss;
+ ss << "{"
+ << "\n \"status\": " << unsigned(info.status)
+ << ",\n \"file_duration\": " << unsigned(info.file_duration)
+ << ",\n \"files_num\": " << unsigned(info.files_num)
+ << ",\n \"player_buffer_files_num\": " << unsigned(info.player_buffer_files_num)
+ << ",\n \"start_file_id\": " << unsigned(info.start_file_id)
+ << ",\n \"current_file_id\": " << unsigned(info.current_file_id)
+ << ",\n \"channels\": " << unsigned(info.channels)
+ << ",\n \"format\": \"" << info.format << "\""
+ << ",\n \"rate\": " << unsigned(info.rate) << "\n}\n";
+ return ss.str();
+}
+
Config json_to_config_(std::istream& js, Config& config) {
try {
boost::property_tree::ptree pt;
@@ -290,6 +318,16 @@ Config json_to_config_(std::istream& js, Config& config) {
} else if (key == "http_base_dir") {
config.set_http_base_dir(
remove_undesired_chars(val.get_value()));
+ } else if (key == "streamer_channels") {
+ config.set_streamer_channels(val.get_value());
+ } else if (key == "streamer_files_num") {
+ config.set_streamer_files_num(val.get_value());
+ } else if (key == "streamer_file_duration") {
+ config.set_streamer_file_duration(val.get_value());
+ } else if (key == "streamer_player_buffer_files_num") {
+ config.set_streamer_player_buffer_files_num(val.get_value());
+ } else if (key == "streamer_enabled") {
+ config.set_streamer_enabled(val.get_value());
} else if (key == "log_severity") {
config.set_log_severity(val.get_value());
} else if (key == "interface_name") {
@@ -347,9 +385,11 @@ Config json_to_config_(std::istream& js, Config& config) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
} catch (std::invalid_argument& e) {
- throw std::runtime_error("error parsing JSON: cannot perform number conversion");
+ throw std::runtime_error(
+ "error parsing JSON: cannot perform number conversion");
} catch (std::out_of_range& e) {
- throw std::runtime_error("error parsing JSON: number conversion out of range");
+ throw std::runtime_error(
+ "error parsing JSON: number conversion out of range");
} catch (std::exception& e) {
throw std::runtime_error("error parsing JSON: " + std::string(e.what()));
}
@@ -417,9 +457,11 @@ StreamSource json_to_source(const std::string& id, const std::string& json) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
} catch (std::invalid_argument& e) {
- throw std::runtime_error("error parsing JSON: cannot perform number conversion");
+ throw std::runtime_error(
+ "error parsing JSON: cannot perform number conversion");
} catch (std::out_of_range& e) {
- throw std::runtime_error("error parsing JSON: number conversion out of range");
+ throw std::runtime_error(
+ "error parsing JSON: number conversion out of range");
} catch (std::exception& e) {
throw std::runtime_error("error parsing JSON: " + std::string(e.what()));
}
@@ -461,9 +503,11 @@ StreamSink json_to_sink(const std::string& id, const std::string& json) {
throw std::runtime_error("error parsing JSON at line " +
std::to_string(je.line()) + " :" + je.message());
} catch (std::invalid_argument& e) {
- throw std::runtime_error("error parsing JSON: cannot perform number conversion");
+ throw std::runtime_error(
+ "error parsing JSON: cannot perform number conversion");
} catch (std::out_of_range& e) {
- throw std::runtime_error("error parsing JSON: number conversion out of range");
+ throw std::runtime_error(
+ "error parsing JSON: number conversion out of range");
} catch (std::exception& e) {
throw std::runtime_error("error parsing JSON: " + std::string(e.what()));
}
diff --git a/daemon/json.hpp b/daemon/json.hpp
index abf8164..1dd8b04 100644
--- a/daemon/json.hpp
+++ b/daemon/json.hpp
@@ -24,6 +24,7 @@
#include "browser.hpp"
#include "session_manager.hpp"
+#include "streamer.hpp"
/* JSON serializers */
std::string config_to_json(const Config& config);
@@ -38,6 +39,7 @@ 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);
+std::string streamer_info_to_json(const StreamerInfo& info);
/* JSON deserializers */
Config json_to_config(std::istream& jstream, const Config& curCconfig);
@@ -57,4 +59,5 @@ void json_to_streams(std::istream& jstream,
void json_to_streams(const std::string& json,
std::list& sources,
std::list& sinks);
+
#endif
diff --git a/daemon/main.cpp b/daemon/main.cpp
index 45244b6..cc77e42 100644
--- a/daemon/main.cpp
+++ b/daemon/main.cpp
@@ -30,6 +30,7 @@
#include "mdns_server.hpp"
#include "rtsp_server.hpp"
#include "session_manager.hpp"
+#include "streamer.hpp"
#ifdef _USE_SYSTEMD_
#include
@@ -39,7 +40,7 @@ namespace po = boost::program_options;
namespace postyle = boost::program_options::command_line_style;
namespace logging = boost::log;
-static const std::string version("bondagit-1.7.0");
+static const std::string version("bondagit-2.0.0");
static std::atomic terminate = false;
void termination_handler(int signum) {
@@ -61,12 +62,11 @@ int main(int argc, char* argv[]) {
po::options_description desc("Options");
desc.add_options()("version,v", "Print daemon version and exit")(
"config,c", po::value()->default_value("/etc/daemon.conf"),
- "daemon configuration file")(
- "http_addr,a",po::value(),
- "HTTP server addr")("http_port,p", po::value(),
- "HTTP server port")("help,h",
- "Print this help "
- "message");
+ "daemon configuration file")("http_addr,a", po::value(),
+ "HTTP server addr")(
+ "http_port,p", po::value(), "HTTP server port")("help,h",
+ "Print this help "
+ "message");
int unix_style = postyle::unix_style | postyle::short_allow_next;
bool driver_restart(true);
@@ -180,8 +180,15 @@ int main(int argc, char* argv[]) {
throw std::runtime_error(std::string("RtspServer:: init failed"));
}
+ /* start streamer */
+ auto streamer = Streamer::create(session_manager, config);
+ if (config->get_streamer_enabled() &&
+ (streamer == nullptr || !streamer->init())) {
+ throw std::runtime_error(std::string("Streamer:: init failed"));
+ }
+
/* start http server */
- HttpServer http_server(session_manager, browser, config);
+ HttpServer http_server(session_manager, browser, streamer, config);
if (!http_server.init()) {
throw std::runtime_error(std::string("HttpServer:: init failed"));
}
@@ -239,6 +246,13 @@ int main(int argc, char* argv[]) {
throw std::runtime_error(std::string("HttpServer:: terminate failed"));
}
+ /* stop streamer */
+ if (config->get_streamer_enabled()) {
+ if (!streamer->terminate()) {
+ throw std::runtime_error(std::string("Streamer:: terminate failed"));
+ }
+ }
+
/* stop rtsp server */
if (!rtsp_server.terminate()) {
throw std::runtime_error(std::string("RtspServer:: terminate failed"));
diff --git a/daemon/mdns_server.cpp b/daemon/mdns_server.cpp
index 6626e6d..531dc8c 100644
--- a/daemon/mdns_server.cpp
+++ b/daemon/mdns_server.cpp
@@ -323,12 +323,12 @@ bool MDNSServer::init() {
#endif
session_manager_->add_source_observer(
- SessionManager::ObserverType::add_source,
+ SessionManager::SourceObserverType::add_source,
std::bind(&MDNSServer::add_service, this, std::placeholders::_2,
std::placeholders::_3));
session_manager_->add_source_observer(
- SessionManager::ObserverType::remove_source,
+ SessionManager::SourceObserverType::remove_source,
std::bind(&MDNSServer::remove_service, this, std::placeholders::_2));
running_ = true;
diff --git a/daemon/mdns_server.hpp b/daemon/mdns_server.hpp
index a2aa875..3e02fe6 100644
--- a/daemon/mdns_server.hpp
+++ b/daemon/mdns_server.hpp
@@ -51,7 +51,7 @@ class MDNSServer {
virtual bool init();
virtual bool terminate();
- bool add_service(const std::string& name, const std::string &sdp);
+ bool add_service(const std::string& name, const std::string& sdp);
bool remove_service(const std::string& name);
protected:
diff --git a/daemon/rtsp_server.hpp b/daemon/rtsp_server.hpp
index b170edc..971b7f4 100644
--- a/daemon/rtsp_server.hpp
+++ b/daemon/rtsp_server.hpp
@@ -98,12 +98,12 @@ class RtspServer {
res_ = std::async([this]() { io_service_.run(); });
session_manager_->add_source_observer(
- SessionManager::ObserverType::add_source,
+ SessionManager::SourceObserverType::add_source,
std::bind(&RtspServer::update_source, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3));
session_manager_->add_source_observer(
- SessionManager::ObserverType::update_source,
+ SessionManager::SourceObserverType::update_source,
std::bind(&RtspServer::update_source, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3));
diff --git a/daemon/scripts/fetch_streamer_files.sh b/daemon/scripts/fetch_streamer_files.sh
new file mode 100755
index 0000000..ead8930
--- /dev/null
+++ b/daemon/scripts/fetch_streamer_files.sh
@@ -0,0 +1,8 @@
+curl http://10.0.0.13:8080/api/streamer/stream/0/0 --output 0.aac
+curl http://10.0.0.13:8080/api/streamer/stream/0/1 --output 1.aac
+curl http://10.0.0.13:8080/api/streamer/stream/0/2 --output 2.aac
+curl http://10.0.0.13:8080/api/streamer/stream/0/3 --output 3.aac
+curl http://10.0.0.13:8080/api/streamer/stream/0/4 --output 4.aac
+curl http://10.0.0.13:8080/api/streamer/stream/0/5 --output 5.aac
+curl http://10.0.0.13:8080/api/streamer/stream/0/6 --output 6.aac
+curl http://10.0.0.13:8080/api/streamer/stream/0/7 --output 7.aac
diff --git a/daemon/session_manager.cpp b/daemon/session_manager.cpp
index 7e70c61..27bfc25 100644
--- a/daemon/session_manager.cpp
+++ b/daemon/session_manager.cpp
@@ -443,24 +443,40 @@ uint8_t SessionManager::get_source_id(const std::string& name) const {
return it != source_names_.end() ? it->second : (stream_id_max + 1);
}
-void SessionManager::add_source_observer(ObserverType type,
- const Observer& cb) {
+void SessionManager::add_ptp_status_observer(const PtpStatusObserver& cb) {
+ ptp_status_observers_.push_back(cb);
+}
+
+void SessionManager::add_source_observer(SourceObserverType type,
+ const SourceObserver& cb) {
switch (type) {
- case ObserverType::add_source:
- add_source_observers.push_back(cb);
+ case SourceObserverType::add_source:
+ add_source_observers_.push_back(cb);
+ break;
+ case SourceObserverType::remove_source:
+ remove_source_observers_.push_back(cb);
+ break;
+ case SourceObserverType::update_source:
+ update_source_observers_.push_back(cb);
break;
- case ObserverType::remove_source:
- remove_source_observers.push_back(cb);
+ }
+}
+
+void SessionManager::add_sink_observer(SinkObserverType type,
+ const SinkObserver& cb) {
+ switch (type) {
+ case SinkObserverType::add_sink:
+ add_sink_observers_.push_back(cb);
break;
- case ObserverType::update_source:
- update_source_observers.push_back(cb);
+ case SinkObserverType::remove_sink:
+ remove_sink_observers_.push_back(cb);
break;
}
}
void SessionManager::on_add_source(const StreamSource& source,
const StreamInfo& info) {
- for (const auto& cb : add_source_observers) {
+ for (const auto& cb : add_source_observers_) {
cb(source.id, source.name, get_source_sdp_(source.id, info));
}
if (IN_MULTICAST(info.stream.m_ui32DestIP)) {
@@ -471,7 +487,7 @@ void SessionManager::on_add_source(const StreamSource& source,
}
void SessionManager::on_remove_source(const StreamInfo& info) {
- for (const auto& cb : remove_source_observers) {
+ for (const auto& cb : remove_source_observers_) {
cb((uint8_t)info.stream.m_uiId, info.stream.m_cName, {});
}
if (IN_MULTICAST(info.stream.m_ui32DestIP)) {
@@ -715,6 +731,9 @@ uint8_t SessionManager::get_sink_id(const std::string& name) const {
void SessionManager::on_add_sink(const StreamSink& sink,
const StreamInfo& info) {
+ for (const auto& cb : add_sink_observers_) {
+ cb(sink.id, sink.name);
+ }
if (IN_MULTICAST(info.stream.m_ui32DestIP)) {
igmp_.join(config_->get_ip_addr_str(),
ip::address_v4(info.stream.m_ui32DestIP).to_string());
@@ -723,6 +742,9 @@ void SessionManager::on_add_sink(const StreamSink& sink,
}
void SessionManager::on_remove_sink(const StreamInfo& info) {
+ for (const auto& cb : remove_sink_observers_) {
+ cb((uint8_t)info.stream.m_uiId, info.stream.m_cName);
+ }
if (IN_MULTICAST(info.stream.m_ui32DestIP)) {
igmp_.leave(config_->get_ip_addr_str(),
ip::address_v4(info.stream.m_ui32DestIP).to_string());
@@ -858,8 +880,8 @@ std::error_code SessionManager::add_sink(const StreamSink& sink) {
}
return ret;
}
-
on_add_sink(sink, info);
+
// update sinks map
sinks_[sink.id] = info;
BOOST_LOG_TRIVIAL(info) << "session_manager:: added sink "
@@ -885,8 +907,6 @@ std::error_code SessionManager::remove_sink(uint32_t id) {
const auto& info = (*it).second;
auto ret = driver_->remove_rtp_stream(info.handle);
if (!ret) {
- igmp_.leave(config_->get_ip_addr_str(),
- ip::address_v4(info.stream.m_ui32DestIP).to_string());
on_remove_sink(info);
sinks_.erase(id);
}
@@ -1083,7 +1103,7 @@ void SessionManager::on_update_sources() {
// trigger sources SDP file update
sources_mutex_.lock();
for (auto& [id, info] : sources_) {
- for (const auto& cb : update_source_observers) {
+ for (const auto& cb : update_source_observers_) {
info.session_version++;
cb(id, info.stream.m_cName, get_source_sdp_(id, info));
}
@@ -1098,6 +1118,10 @@ void SessionManager::on_ptp_status_changed(const std::string& status) const {
(void)driver_->set_sample_rate(driver_->get_current_sample_rate());
}
+ for (const auto& cb : ptp_status_observers_) {
+ (void)cb(status);
+ }
+
static std::string g_ptp_status;
if (g_ptp_status != status && !config_->get_ptp_status_script().empty()) {
diff --git a/daemon/session_manager.hpp b/daemon/session_manager.hpp
index 8ead203..5e9ed7e 100644
--- a/daemon/session_manager.hpp
+++ b/daemon/session_manager.hpp
@@ -145,10 +145,18 @@ class SessionManager {
std::error_code remove_source(uint32_t id);
uint8_t get_source_id(const std::string& name) const;
- enum class ObserverType { add_source, remove_source, update_source };
- using Observer = std::function<
+ enum class SourceObserverType { add_source, remove_source, update_source };
+ using SourceObserver = std::function<
bool(uint8_t id, const std::string& name, const std::string& sdp)>;
- void add_source_observer(ObserverType type, const Observer& cb);
+ void add_source_observer(SourceObserverType type, const SourceObserver& cb);
+
+ enum class SinkObserverType { add_sink, remove_sink };
+ using SinkObserver = std::function<
+ bool(uint8_t id, const std::string& name)>;
+ void add_sink_observer(SinkObserverType type, const SinkObserver& cb);
+
+ using PtpStatusObserver = std::function;
+ void add_ptp_status_observer(const PtpStatusObserver& cb);
std::error_code add_sink(const StreamSink& sink);
std::error_code get_sink(uint8_t id, StreamSink& sink) const;
@@ -240,9 +248,13 @@ class SessionManager {
PTPStatus ptp_status_;
mutable std::shared_mutex ptp_mutex_;
- std::list add_source_observers;
- std::list remove_source_observers;
- std::list update_source_observers;
+ std::list add_source_observers_;
+ std::list remove_source_observers_;
+ std::list update_source_observers_;
+ std::list ptp_status_observers_;
+ std::list add_sink_observers_;
+ std::list remove_sink_observers_;
+ std::list update_sink_observers_;
SAP sap_{config_->get_sap_mcast_addr()};
IGMP igmp_;
diff --git a/daemon/streamer.cpp b/daemon/streamer.cpp
new file mode 100644
index 0000000..a32dfa4
--- /dev/null
+++ b/daemon/streamer.cpp
@@ -0,0 +1,563 @@
+// streamer.cpp
+//
+// Copyright (c) 2019 2024 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 "utils.hpp"
+#include "streamer.hpp"
+
+std::shared_ptr Streamer::create(
+ std::shared_ptr session_manager,
+ 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 Streamer(session_manager, config));
+ instance = ptr;
+ return ptr;
+}
+
+bool Streamer::init() {
+ BOOST_LOG_TRIVIAL(info) << "Streamer: init";
+ session_manager_->add_ptp_status_observer(
+ std::bind(&Streamer::on_ptp_status_change, this, std::placeholders::_1));
+ session_manager_->add_sink_observer(
+ SessionManager::SinkObserverType::add_sink,
+ std::bind(&Streamer::on_sink_add, this, std::placeholders::_1));
+ session_manager_->add_sink_observer(
+ SessionManager::SinkObserverType::remove_sink,
+ std::bind(&Streamer::on_sink_remove, this, std::placeholders::_1));
+
+ running_ = false;
+
+ PTPStatus status;
+ session_manager_->get_ptp_status(status);
+ on_ptp_status_change(status.status);
+
+ return true;
+}
+
+bool Streamer::on_sink_add(uint8_t id) {
+ return true;
+}
+
+bool Streamer::on_sink_remove(uint8_t id) {
+ if (faac_[id]) {
+ std::unique_lock faac_lock(faac_mutex_[id]);
+ faacEncClose(faac_[id]);
+ faac_[id] = 0;
+ }
+ total_sink_samples_[id] = 0;
+ return true;
+}
+
+bool Streamer::on_ptp_status_change(const std::string& status) {
+ BOOST_LOG_TRIVIAL(info) << "Streamer: new ptp status " << status;
+ if (status == "locked") {
+ return start_capture();
+ }
+ if (status == "unlocked") {
+ return stop_capture();
+ }
+ return true;
+}
+
+#ifndef timersub
+#define timersub(a, b, result) \
+ do { \
+ (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
+ (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
+ if ((result)->tv_usec < 0) { \
+ --(result)->tv_sec; \
+ (result)->tv_usec += 1000000; \
+ } \
+ } while (0)
+#endif
+
+bool Streamer::pcm_xrun() {
+ snd_pcm_status_t* status;
+ int res;
+ snd_pcm_status_alloca(&status);
+ if ((res = snd_pcm_status(capture_handle_, status)) < 0) {
+ BOOST_LOG_TRIVIAL(error)
+ << "streamer:: pcm_xrun status error: " << snd_strerror(res);
+ return false;
+ }
+ if (snd_pcm_status_get_state(status) == SND_PCM_STATE_XRUN) {
+ struct timeval now, diff, tstamp;
+ gettimeofday(&now, 0);
+ snd_pcm_status_get_trigger_tstamp(status, &tstamp);
+ timersub(&now, &tstamp, &diff);
+ BOOST_LOG_TRIVIAL(error)
+ << "streamer:: pcm_xrun overrun!!! (at least "
+ << diff.tv_sec * 1000 + diff.tv_usec / 1000.0 << " ms long";
+
+ if ((res = snd_pcm_prepare(capture_handle_)) < 0) {
+ BOOST_LOG_TRIVIAL(error)
+ << "streamer:: pcm_xrun prepare error: " << snd_strerror(res);
+ return false;
+ }
+ return true; /* ok, data should be accepted again */
+ }
+ if (snd_pcm_status_get_state(status) == SND_PCM_STATE_DRAINING) {
+ BOOST_LOG_TRIVIAL(error)
+ << "streamer:: capture stream format change? attempting recover...";
+ if ((res = snd_pcm_prepare(capture_handle_)) < 0) {
+ BOOST_LOG_TRIVIAL(error)
+ << "streamer:: pcm_xrun xrun(DRAINING) error: " << snd_strerror(res);
+ return false;
+ }
+ return true;
+ }
+ BOOST_LOG_TRIVIAL(error) << "streamer:: read/write error, state = "
+ << snd_pcm_state_name(
+ snd_pcm_status_get_state(status));
+ return false;
+}
+
+/* I/O suspend handler */
+bool Streamer::pcm_suspend() {
+ int res;
+ BOOST_LOG_TRIVIAL(info) << "streamer:: Suspended. Trying resume. ";
+ while ((res = snd_pcm_resume(capture_handle_)) == -EAGAIN)
+ sleep(1); /* wait until suspend flag is released */
+ if (res < 0) {
+ BOOST_LOG_TRIVIAL(error) << "streamer:: Failed. Restarting stream. ";
+ if ((res = snd_pcm_prepare(capture_handle_)) < 0) {
+ BOOST_LOG_TRIVIAL(error)
+ << "streamer:: suspend: prepare error: " << snd_strerror(res);
+ return false;
+ }
+ }
+ return true;
+}
+
+ssize_t Streamer::pcm_read(uint8_t* data, size_t rcount) {
+ ssize_t r;
+ size_t count = rcount;
+
+ if (count != chunk_samples_) {
+ count = chunk_samples_;
+ }
+
+ while (count > 0) {
+ r = snd_pcm_readi(capture_handle_, data, count);
+ if (r == -EAGAIN || (r >= 0 && (size_t)r < count)) {
+ if (!running_)
+ return -1;
+ snd_pcm_wait(capture_handle_, 1000);
+ } else if (r == -EPIPE) {
+ if (!pcm_xrun())
+ return -1;
+ } else if (r == -ESTRPIPE) {
+ if (!pcm_suspend())
+ return -1;
+ } else if (r < 0) {
+ BOOST_LOG_TRIVIAL(error) << "streamer:: read error: " << snd_strerror(r);
+ return -1;
+ }
+ if (r > 0) {
+ count -= r;
+ data += r * bytes_per_frame_;
+ }
+ }
+ return rcount;
+}
+
+bool Streamer::start_capture() {
+ if (running_)
+ return true;
+
+ BOOST_LOG_TRIVIAL(info) << "Streamer: starting audio capture ... ";
+ int err;
+ if ((err = snd_pcm_open(&capture_handle_, device_name, SND_PCM_STREAM_CAPTURE,
+ SND_PCM_NONBLOCK)) < 0) {
+ BOOST_LOG_TRIVIAL(fatal) << "streamer:: cannot open audio device "
+ << device_name << " : " << snd_strerror(err);
+ return false;
+ }
+
+ snd_pcm_hw_params_t* hw_params;
+ if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer:: cannot allocate hardware parameter structure: "
+ << snd_strerror(err);
+ return false;
+ }
+
+ if ((err = snd_pcm_hw_params_any(capture_handle_, hw_params)) < 0) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer:: cannot initialize hardware parameter structure: "
+ << snd_strerror(err);
+ return false;
+ }
+
+ if ((err = snd_pcm_hw_params_set_access(capture_handle_, hw_params,
+ SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer:: cannot set access type: " << snd_strerror(err);
+ return false;
+ }
+
+ if ((err = snd_pcm_hw_params_set_format(capture_handle_, hw_params, format)) <
+ 0) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer:: cannot set sample format: " << snd_strerror(err);
+ return false;
+ }
+
+ rate_ = config_->get_sample_rate();
+ if ((err = snd_pcm_hw_params_set_rate_near(capture_handle_, hw_params, &rate_,
+ 0)) < 0) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer:: cannot set sample rate: " << snd_strerror(err);
+ return false;
+ }
+
+ channels_ = config_->get_streamer_channels();
+ if ((err = snd_pcm_hw_params_set_channels(capture_handle_, hw_params,
+ channels_)) < 0) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer:: cannot set channel count: " << snd_strerror(err);
+ return false;
+ }
+
+ files_num_ = config_->get_streamer_files_num();
+ file_duration_ = config_->get_streamer_file_duration();
+ player_buffer_files_num_ = config_->get_streamer_player_buffer_files_num();
+
+ if ((err = snd_pcm_hw_params(capture_handle_, hw_params)) < 0) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer:: cannot set parameters: " << snd_strerror(err);
+ return false;
+ }
+
+ snd_pcm_hw_params_get_period_size(hw_params, &chunk_samples_, 0);
+ chunk_samples_ = 6144; // AAC 6 channels input
+ bytes_per_frame_ = snd_pcm_format_physical_width(format) * channels_ / 8;
+
+ snd_pcm_hw_params_free(hw_params);
+
+ if ((err = snd_pcm_prepare(capture_handle_)) < 0) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer:: cannot prepare audio interface for use: "
+ << snd_strerror(err);
+ return false;
+ }
+
+ buffer_samples_ = rate_ * file_duration_ / chunk_samples_ * chunk_samples_;
+ BOOST_LOG_TRIVIAL(info) << "streamer: buffer_samples " << buffer_samples_;
+ buffer_.reset(new uint8_t[buffer_samples_ * bytes_per_frame_]);
+ if (buffer_ == nullptr) {
+ BOOST_LOG_TRIVIAL(fatal) << "streamer: cannot allocate audio buffer";
+ return false;
+ }
+
+ buffer_offset_ = 0;
+ total_sink_samples_.clear();
+ file_id_ = 0;
+ file_counter_ = 0;
+ running_ = true;
+
+ open_files(file_id_);
+
+ /* start capturing on a separate thread */
+ res_ = std::async(std::launch::async, [&]() {
+ BOOST_LOG_TRIVIAL(debug)
+ << "streamer: audio capture loop start, chunk_samples_ = "
+ << chunk_samples_;
+ while (running_) {
+ if ((pcm_read(buffer_.get() + buffer_offset_ * bytes_per_frame_,
+ chunk_samples_)) < 0) {
+ break;
+ }
+
+ save_files(file_id_);
+ buffer_offset_ += chunk_samples_;
+
+ /* check id buffer is full */
+ if (buffer_offset_ + chunk_samples_ > buffer_samples_) {
+ close_files(file_id_);
+ /* increase file id */
+ file_id_ = (file_id_ + 1) % files_num_;
+ file_counter_++;
+ buffer_offset_ = 0;
+
+ open_files(file_id_);
+ }
+ }
+ BOOST_LOG_TRIVIAL(debug) << "streamer: audio capture loop end";
+ return true;
+ });
+
+ return true;
+}
+
+void Streamer::open_files(uint8_t files_id) {
+ BOOST_LOG_TRIVIAL(debug) << "streamer: opening files with id "
+ << std::to_string(files_id) << " ...";
+ for (const auto& sink : session_manager_->get_sinks()) {
+ tmp_streams_[sink.id].str("");
+ std::unique_lock faac_lock(faac_mutex_[sink.id]);
+ if (!faac_[sink.id]) {
+ setup_codec(sink);
+ }
+ }
+}
+
+void Streamer::save_files(uint8_t files_id) {
+ auto sample_size = bytes_per_frame_ / channels_;
+
+ for (const auto& sink : session_manager_->get_sinks()) {
+ total_sink_samples_[sink.id] += chunk_samples_;
+ for (size_t offset = 0; offset < chunk_samples_; offset++) {
+ for (uint16_t ch : sink.map) {
+ auto in = buffer_.get() + (buffer_offset_ + offset) * bytes_per_frame_ +
+ ch * sample_size;
+ std::copy(in, in + sample_size,
+ std::ostream_iterator(tmp_streams_[sink.id]));
+ }
+ }
+ }
+}
+
+bool Streamer::setup_codec(const StreamSink& sink) {
+ /* open and setup the encoder */
+ faac_[sink.id] = faacEncOpen(config_->get_sample_rate(), sink.map.size(),
+ &codec_in_samples_[sink.id],
+ &codec_out_buffer_size_[sink.id]);
+ if (!faac_[sink.id]) {
+ BOOST_LOG_TRIVIAL(fatal) << "streamer:: cannot open codec";
+ return false;
+ }
+ BOOST_LOG_TRIVIAL(debug) << "streamer: codec samples in "
+ << codec_in_samples_[sink.id] << " out buffer size "
+ << codec_out_buffer_size_[sink.id];
+ faacEncConfigurationPtr faac_cfg;
+ /* check faac version */
+ faac_cfg = faacEncGetCurrentConfiguration(faac_[sink.id]);
+ if (!faac_cfg) {
+ BOOST_LOG_TRIVIAL(fatal) << "streamer:: cannot get codec configuration";
+ return false;
+ }
+
+ faac_cfg->aacObjectType = LOW;
+ faac_cfg->mpegVersion = MPEG4;
+ faac_cfg->useTns = 0;
+ faac_cfg->useLfe = sink.map.size() > 6 ? 1 : 0;
+ faac_cfg->shortctl = SHORTCTL_NORMAL;
+ faac_cfg->allowMidside = 2;
+ faac_cfg->bitRate = 64000 / sink.map.size();
+ // faac_cfg->bandWidth = 18000;
+ // faac_cfg->quantqual = 50;
+ // faac_cfg->pnslevel = 4;
+ // faac_cfg->jointmode = JOINT_MS;
+ faac_cfg->outputFormat = 1;
+ faac_cfg->inputFormat = FAAC_INPUT_16BIT;
+
+ if (!faacEncSetConfiguration(faac_[sink.id], faac_cfg)) {
+ BOOST_LOG_TRIVIAL(fatal) << "streamer:: cannot set codec configuration";
+ return false;
+ }
+
+ out_buffer_size_[sink.id] = 0;
+ return true;
+}
+
+void Streamer::close_files(uint8_t files_id) {
+ uint16_t sample_size = bytes_per_frame_ / channels_;
+
+ std::list > ress;
+ for (const auto& sink : session_manager_->get_sinks()) {
+ ress.emplace_back(std::async(std::launch::async, [=]() {
+ uint32_t out_len = 0;
+ {
+ std::unique_lock faac_lock(faac_mutex_[sink.id]);
+ if (!faac_[sink.id])
+ return false;
+
+ auto codec_in_samples = codec_in_samples_[sink.id];
+ uint32_t out_size = codec_out_buffer_size_[sink.id] * buffer_samples_ *
+ sink.map.size() / codec_in_samples;
+
+ if (out_size > out_buffer_size_[sink.id]) {
+ out_buffer_[sink.id].reset(new uint8_t[out_size]);
+ if (out_buffer_[sink.id] == nullptr) {
+ BOOST_LOG_TRIVIAL(fatal)
+ << "streamer: cannot allocate output buffer";
+ return false;
+ }
+ out_buffer_size_[sink.id] = out_size;
+ }
+
+ uint32_t in_samples = 0;
+ bool end = false;
+ while (!end) {
+ if (in_samples + codec_in_samples >=
+ buffer_samples_ * sink.map.size()) {
+ uint16_t diff = buffer_samples_ * sink.map.size() - in_samples;
+ codec_in_samples = diff;
+ end = true;
+ }
+
+ auto ret = faacEncEncode(
+ faac_[sink.id],
+ (int32_t*)(tmp_streams_[sink.id].str().c_str() +
+ in_samples * sample_size),
+ codec_in_samples, out_buffer_[sink.id].get() + out_len,
+ codec_out_buffer_size_[sink.id]);
+ if (ret < 0) {
+ BOOST_LOG_TRIVIAL(error)
+ << "streamer: cannot encode file id "
+ << std::to_string(files_id) << " for sink id "
+ << std::to_string(sink.id);
+ return false;
+ }
+
+ in_samples += codec_in_samples;
+ out_len += ret;
+ }
+ }
+ std::unique_lock streams_lock(streams_mutex_[sink.id]);
+ output_streams_[std::make_pair(sink.id, files_id)].str("");
+ std::copy(out_buffer_[sink.id].get(),
+ out_buffer_[sink.id].get() + out_len,
+ std::ostream_iterator(
+ output_streams_[std::make_pair(sink.id, files_id)]));
+ output_ids_[files_id] = file_counter_;
+ return true;
+ }));
+ }
+
+ for (auto& res : ress) {
+ (void)res.get();
+ }
+}
+
+bool Streamer::stop_capture() {
+ if (!running_)
+ return true;
+
+ BOOST_LOG_TRIVIAL(info) << "streamer: stopping audio capture ... ";
+ running_ = false;
+ bool ret = res_.get();
+ for (const auto& sink : session_manager_->get_sinks()) {
+ if (faac_[sink.id]) {
+ faacEncClose(faac_[sink.id]);
+ faac_[sink.id] = 0;
+ }
+ }
+ snd_pcm_close(capture_handle_);
+ return ret;
+}
+
+bool Streamer::terminate() {
+ BOOST_LOG_TRIVIAL(info) << "streamer: terminating ... ";
+ return stop_capture();
+}
+
+std::error_code Streamer::get_info(const StreamSink& sink, StreamerInfo& info) {
+ if (!running_) {
+ BOOST_LOG_TRIVIAL(warning) << "streamer:: not running";
+ return std::error_code{DaemonErrc::streamer_not_running};
+ }
+
+ for (uint16_t ch : sink.map) {
+ if (ch >= channels_) {
+ BOOST_LOG_TRIVIAL(error) << "streamer:: channel is not captured for sink "
+ << std::to_string(sink.id);
+ return std::error_code{DaemonErrc::streamer_invalid_ch};
+ }
+ }
+
+ if (total_sink_samples_[sink.id] < buffer_samples_ * (files_num_ - 1)) {
+ BOOST_LOG_TRIVIAL(warning)
+ << "streamer:: not enough samples buffered for sink "
+ << std::to_string(sink.id);
+ return std::error_code{DaemonErrc::streamer_retry_later};
+ }
+
+ auto file_id = file_id_.load();
+ uint8_t start_file_id = (file_id + files_num_ / 2) % files_num_;
+
+ switch (format) {
+ case SND_PCM_FORMAT_S16_LE:
+ info.format = "s16";
+ break;
+ case SND_PCM_FORMAT_S24_3LE:
+ info.format = "s24";
+ break;
+ case SND_PCM_FORMAT_S32_LE:
+ info.format = "s32";
+ break;
+ default:
+ info.format = "invalid";
+ break;
+ }
+
+ info.files_num = files_num_;
+ info.file_duration = file_duration_;
+ info.player_buffer_files_num = player_buffer_files_num_;
+ info.rate = config_->get_sample_rate();
+ info.channels = sink.map.size();
+ info.start_file_id = start_file_id;
+ info.current_file_id = file_id;
+
+ BOOST_LOG_TRIVIAL(debug) << "streamer:: returning position "
+ << std::to_string(file_id);
+ return std::error_code{};
+}
+
+std::error_code Streamer::get_stream(const StreamSink& sink,
+ uint8_t files_id,
+ uint8_t& current_file_id,
+ uint8_t& start_file_id,
+ uint32_t& file_counter,
+ std::string& out) {
+ if (!running_) {
+ BOOST_LOG_TRIVIAL(warning) << "streamer:: not running";
+ return std::error_code{DaemonErrc::streamer_not_running};
+ }
+
+ for (uint16_t ch : sink.map) {
+ if (ch >= channels_) {
+ BOOST_LOG_TRIVIAL(error) << "streamer:: channel is not captured for sink "
+ << std::to_string(sink.id);
+ return std::error_code{DaemonErrc::streamer_invalid_ch};
+ }
+ }
+
+ if (total_sink_samples_[sink.id] < buffer_samples_ * (files_num_ - 1)) {
+ BOOST_LOG_TRIVIAL(warning)
+ << "streamer:: not enough samples buffered for sink "
+ << std::to_string(sink.id);
+ return std::error_code{DaemonErrc::streamer_retry_later};
+ }
+
+ current_file_id = file_id_.load();
+ start_file_id = (current_file_id + files_num_ / 2) % files_num_;
+ if (files_id == current_file_id) {
+ BOOST_LOG_TRIVIAL(error)
+ << "streamer: requesting current file id " << std::to_string(files_id);
+ }
+ std::shared_lock streams_lock(streams_mutex_[sink.id]);
+ out = output_streams_[std::make_pair(sink.id, files_id)].str();
+ file_counter = output_ids_[files_id];
+ return std::error_code{};
+}
diff --git a/daemon/streamer.hpp b/daemon/streamer.hpp
new file mode 100644
index 0000000..58edb3b
--- /dev/null
+++ b/daemon/streamer.hpp
@@ -0,0 +1,115 @@
+// streamer.hpp
+//
+// Copyright (c) 2019 2024 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 _STREAMER_HPP_
+#define _STREAMER_HPP_
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "session_manager.hpp"
+
+struct StreamerInfo {
+ uint8_t status;
+ uint16_t file_duration{0};
+ uint8_t files_num{0};
+ uint8_t player_buffer_files_num{0};
+ uint8_t channels{0};
+ uint8_t start_file_id{0};
+ uint8_t current_file_id{0};
+ uint32_t rate{0};
+ std::string format;
+};
+
+class Streamer {
+ public:
+ static std::shared_ptr create(
+ std::shared_ptr session_manager,
+ std::shared_ptr config);
+ Streamer() = delete;
+ Streamer(const Browser&) = delete;
+ Streamer& operator=(const Browser&) = delete;
+
+ bool init();
+ bool terminate();
+
+ std::error_code get_info(const StreamSink& sink, StreamerInfo& info);
+ std::error_code get_stream(const StreamSink& sink,
+ uint8_t file_id,
+ uint8_t& current_file_id,
+ uint8_t& start_file_id,
+ uint32_t& file_count,
+ std::string& out);
+
+ protected:
+ explicit Streamer(std::shared_ptr session_manager,
+ std::shared_ptr config)
+ : session_manager_(session_manager), config_(config){};
+
+ private:
+ constexpr static const char device_name[] = "plughw:RAVENNA";
+ constexpr static snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;
+
+ bool pcm_xrun();
+ bool pcm_suspend();
+ ssize_t pcm_read(uint8_t* data, size_t rcount);
+
+ bool on_ptp_status_change(const std::string& status);
+ bool on_sink_add(uint8_t id);
+ bool on_sink_remove(uint8_t id);
+ bool start_capture();
+ bool stop_capture();
+ bool setup_codec(const StreamSink& sink);
+ void open_files(uint8_t files_id);
+ void close_files(uint8_t files_id);
+ void save_files(uint8_t files_id);
+
+ std::shared_ptr session_manager_;
+ std::shared_ptr config_;
+ snd_pcm_uframes_t chunk_samples_{0};
+ size_t bytes_per_frame_{0};
+ uint16_t file_duration_{1};
+ uint8_t files_num_{8};
+ uint8_t player_buffer_files_num_{1};
+ size_t buffer_samples_{0};
+ std::unordered_map total_sink_samples_;
+ uint32_t buffer_offset_{0};
+ std::unordered_map streams_mutex_;
+ std::unordered_map tmp_streams_;
+ std::map, std::stringstream> output_streams_;
+ std::unordered_map output_ids_;
+ uint32_t file_counter_{0};
+ std::atomic file_id_{0};
+ std::unique_ptr buffer_;
+ std::unordered_map > out_buffer_;
+ std::unordered_map out_buffer_size_{0};
+ uint8_t channels_{8};
+ uint32_t rate_{0};
+ std::future res_;
+ snd_pcm_t* capture_handle_;
+ std::atomic_bool running_{false};
+ std::unordered_map faac_;
+ std::unordered_map faac_mutex_;
+ std::unordered_map codec_in_samples_;
+ std::unordered_map codec_out_buffer_size_;
+};
+
+#endif
diff --git a/daemon/tests/daemon.conf b/daemon/tests/daemon.conf
index 7831362..ae6a95f 100644
--- a/daemon/tests/daemon.conf
+++ b/daemon/tests/daemon.conf
@@ -23,5 +23,10 @@
"ptp_status_script": "",
"mac_addr": "00:00:00:00:00:00",
"ip_addr": "127.0.0.1",
+ "streamer_channels": 8,
+ "streamer_files_num": 6,
+ "streamer_file_duration": 3,
+ "streamer_player_buffer_files_num": 2,
+ "streamer_enabled": false,
"auto_sinks_update": true
}
diff --git a/daemon/tests/daemon_test.cpp b/daemon/tests/daemon_test.cpp
index 2bac33b..0dc994d 100644
--- a/daemon/tests/daemon_test.cpp
+++ b/daemon/tests/daemon_test.cpp
@@ -1,7 +1,6 @@
-//
// daemon_test.cpp
//
-// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
+// Copyright (c) 2019 2024 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
@@ -400,6 +399,12 @@ BOOST_AUTO_TEST_CASE(get_config) {
auto mac_addr = pt.get("mac_addr");
auto ip_addr = pt.get("ip_addr");
auto auto_sinks_update = pt.get("auto_sinks_update");
+ auto mdns_enabled = pt.get("mdns_enabled");
+ auto streamer_enabled = pt.get("streamer_enabled");
+ auto streamer_channels = pt.get("streamer_channels");
+ auto streamer_files_num = pt.get("streamer_files_num");
+ auto streamer_file_duration = pt.get("streamer_file_duration");
+ auto streamer_player_buffer_files_num = pt.get("streamer_player_buffer_files_num");
BOOST_CHECK_MESSAGE(http_port == 9999, "config as excepcted");
// BOOST_CHECK_MESSAGE(log_severity == 5, "config as excepcted");
BOOST_CHECK_MESSAGE(playout_delay == 0, "config as excepcted");
@@ -422,6 +427,12 @@ BOOST_AUTO_TEST_CASE(get_config) {
BOOST_CHECK_MESSAGE(node_id == "test node", "config as excepcted");
BOOST_CHECK_MESSAGE(custom_node_id == "test node", "config as excepcted");
BOOST_CHECK_MESSAGE(auto_sinks_update == true, "config as excepcted");
+ BOOST_CHECK_MESSAGE(mdns_enabled == true, "config as excepcted");
+ BOOST_CHECK_MESSAGE(streamer_enabled == false, "config as excepcted");
+ BOOST_CHECK_MESSAGE(streamer_channels == 8, "config as excepcted");
+ BOOST_CHECK_MESSAGE(streamer_files_num == 6, "config as excepcted");
+ BOOST_CHECK_MESSAGE(streamer_file_duration == 3, "config as excepcted");
+ BOOST_CHECK_MESSAGE(streamer_player_buffer_files_num == 2, "config as excepcted");
}
BOOST_AUTO_TEST_CASE(get_ptp_status) {
diff --git a/daemon/utils.cpp b/daemon/utils.cpp
index 30fc8c2..3941082 100644
--- a/daemon/utils.cpp
+++ b/daemon/utils.cpp
@@ -81,7 +81,7 @@ std::string get_host_node_id(uint32_t ip_addr) {
std::stringstream ss;
ip_addr = htonl(ip_addr);
/* we create an host ID based on the current IP */
- ss << "AES67 daemon "
+ ss << "Daemon "
<< boost::format("%08x") % ((ip_addr << 16) | (ip_addr >> 16));
return ss.str();
}
diff --git a/systemd/aes67-daemon.service b/systemd/aes67-daemon.service
index 35c4128..c7cde77 100644
--- a/systemd/aes67-daemon.service
+++ b/systemd/aes67-daemon.service
@@ -10,22 +10,24 @@ WatchdogSec=10
# Run as separate user created via sysusers.d
User=aes67-daemon
-
ExecStart=/usr/local/bin/aes67-daemon
+#ExecStart=strace -e trace=file -o /home/aes67-daemon/trace_erronly_fail.log -Z -f -tt /usr/local/bin/aes67-daemon
# Security filters.
CapabilityBoundingSet=
DevicePolicy=closed
+DeviceAllow=char-alsa
+DeviceAllow=/dev/snd/*
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
-PrivateDevices=yes
+PrivateDevices=no
PrivateMounts=yes
PrivateTmp=yes
PrivateUsers=yes
# interface::get_mac_from_arp_cache() reads from /proc/net/arp
ProcSubset=all
-ProtectClock=yes
+ProtectClock=no
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
diff --git a/systemd/daemon.conf b/systemd/daemon.conf
index d942a77..34c7930 100644
--- a/systemd/daemon.conf
+++ b/systemd/daemon.conf
@@ -2,7 +2,7 @@
"http_port": 8080,
"rtsp_port": 8854,
"http_base_dir": "/usr/local/share/aes67-daemon/webui/",
- "log_severity": 2,
+ "log_severity": 1,
"playout_delay": 0,
"tic_frame_size_at_1fs": 64,
"max_tic_frame_size": 1024,
@@ -20,5 +20,10 @@
"mdns_enabled": true,
"custom_node_id": "",
"ptp_status_script": "/usr/local/share/aes67-daemon/scripts/ptp_status.sh",
+ "streamer_channels": 8,
+ "streamer_files_num": 8,
+ "streamer_file_duration": 1,
+ "streamer_player_buffer_files_num": 1,
+ "streamer_enabled": false,
"auto_sinks_update": true
}
diff --git a/systemd/install.sh b/systemd/install.sh
index 9990737..13a5ba7 100755
--- a/systemd/install.sh
+++ b/systemd/install.sh
@@ -4,7 +4,7 @@
#
#create a user for the daemon
-sudo useradd -M -l aes67-daemon -c "AES67 Linux daemon"
+sudo useradd -g audio -M -l aes67-daemon -c "AES67 Linux daemon"
#copy the daemon binary, make sure -DWITH_SYSTEMD=ON
sudo cp ../daemon/aes67-daemon /usr/local/bin/aes67-daemon
#create the daemon webui and script directories
diff --git a/test/daemon.conf b/test/daemon.conf
index bec67fe..ded6ffb 100644
--- a/test/daemon.conf
+++ b/test/daemon.conf
@@ -23,5 +23,10 @@
"node_id": "AES67 daemon 007f0100",
"custom_node_id": "",
"ptp_status_script": "",
+ "streamer_channels": 8,
+ "streamer_files_num": 8,
+ "streamer_file_duration": 1,
+ "streamer_player_buffer_files_num": 1,
+ "streamer_enabled": true,
"auto_sinks_update": true
}
diff --git a/ubuntu-packages.sh b/ubuntu-packages.sh
index b32bf72..5c12ae1 100755
--- a/ubuntu-packages.sh
+++ b/ubuntu-packages.sh
@@ -20,4 +20,5 @@ sudo apt-get install -y linuxptp
sudo apt-get install -y libavahi-client-dev
sudo apt install -y linux-headers-$(uname -r)
sudo apt-get install -y libsystemd-dev
+sudo apt-get install -y libfaac-dev
diff --git a/webui/src/Config.jsx b/webui/src/Config.jsx
index 29f2309..f7fdb4f 100644
--- a/webui/src/Config.jsx
+++ b/webui/src/Config.jsx
@@ -53,6 +53,13 @@ class Config extends Component {
sapInterval: '',
sapIntervalErr: false,
mdnsEnabled: false,
+ streamerEnabled: false,
+ streamerChannels: 0,
+ streamerChIntervalErr: false,
+ streamerFiles: 0,
+ streamerFilesIntervalErr: false,
+ streamerFileDuration: 0,
+ streamerFileDurationIntervalErr: false,
syslogProto: '',
syslogServer: '',
syslogServerErr: false,
@@ -103,6 +110,11 @@ class Config extends Component {
sapMcastAddr: data.sap_mcast_addr,
sapInterval: data.sap_interval,
mdnsEnabled: data.mdns_enabled,
+ streamerEnabled: data.streamer_enabled,
+ streamerChannels: data.streamer_channels,
+ streamerFiles: data.streamer_files_num,
+ streamerFileDuration: data.streamer_file_duration,
+ streamerPlayerBufferFiles: data.streamer_player_buffer_files_num,
syslogProto: data.syslog_proto,
syslogServer: data.syslog_server,
statusFile: data.status_file,
@@ -132,6 +144,9 @@ class Config extends Component {
!this.state.rtpPortErr &&
!this.state.rtspPortErr &&
!this.state.sapIntervalErr &&
+ !this.state.streamerChIntervalErr &&
+ !this.state.streamerFilesIntervalErr &&
+ !this.state.streamerFileDurationIntervalErr &&
!this.state.syslogServerErr &&
(!this.state.customNodeIdErr || this.state.customNodeId === '') &&
!this.state.isVersionLoading &&
@@ -155,7 +170,12 @@ class Config extends Component {
this.state.sapInterval,
this.state.mdnsEnabled,
this.state.customNodeId,
- this.state.autoSinksUpdate)
+ this.state.autoSinksUpdate,
+ this.state.streamerEnabled,
+ this.state.streamerChannels,
+ this.state.streamerFiles,
+ this.state.streamerFileDuration,
+ this.state.streamerPlayerBufferFiles)
.then(response => toast.success('Applying new configuration ...'));
}
@@ -207,6 +227,30 @@ class Config extends Component {
+ {this.state.isConfigLoading ? : HTTP Streamer Config
}
+
+
{this.state.isConfigLoading ? : Network Config
}
diff --git a/webui/src/Services.js b/webui/src/Services.js
index d3a6174..0ee1854 100644
--- a/webui/src/Services.js
+++ b/webui/src/Services.js
@@ -84,7 +84,7 @@ export default class RestAPI {
});
}
- static setConfig(log_severity, syslog_proto, syslog_server, rtp_mcast_base, rtp_port, rtsp_port, playout_delay, tic_frame_size_at_1fs, sample_rate, max_tic_frame_size, sap_mcast_addr, sap_interval, mdns_enabled, custom_node_id, auto_sinks_update) {
+ static setConfig(log_severity, syslog_proto, syslog_server, rtp_mcast_base, rtp_port, rtsp_port, playout_delay, tic_frame_size_at_1fs, sample_rate, max_tic_frame_size, sap_mcast_addr, sap_interval, mdns_enabled, custom_node_id, auto_sinks_update, streamer_enabled, streamer_channels, streamer_files_num, streamer_file_duration, streamer_player_buffer_files_num) {
return this.doFetch(config, {
body: JSON.stringify({
log_severity: parseInt(log_severity, 10),
@@ -101,7 +101,12 @@ export default class RestAPI {
sap_interval: parseInt(sap_interval, 10),
custom_node_id: custom_node_id,
mdns_enabled: mdns_enabled,
- auto_sinks_update: auto_sinks_update
+ auto_sinks_update: auto_sinks_update,
+ streamer_enabled: streamer_enabled,
+ streamer_channels: parseInt(streamer_channels, 10),
+ streamer_files_num: parseInt(streamer_files_num, 10),
+ streamer_file_duration: parseInt(streamer_file_duration, 10),
+ streamer_player_buffer_files_num: parseInt(streamer_player_buffer_files_num, 10),
}),
method: 'POST'
}).catch(err => {