From 466f6b4fc40b972d4d2c58106cd667940599128c Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Sat, 6 Jul 2024 17:27:49 +0200 Subject: [PATCH 1/5] First import of HTTP Streamer functionality in the daemon 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. 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. --- README.md | 14 + daemon/CMakeLists.txt | 7 +- daemon/README.md | 96 ++++- daemon/browser.hpp | 3 +- daemon/config.cpp | 35 +- daemon/config.hpp | 37 ++ daemon/daemon.conf | 5 + daemon/error_code.cpp | 6 + daemon/error_code.hpp | 15 +- daemon/http_server.cpp | 91 ++++ daemon/http_server.hpp | 8 +- daemon/json.cpp | 58 ++- daemon/json.hpp | 3 + daemon/main.cpp | 30 +- daemon/mdns_server.cpp | 4 +- daemon/mdns_server.hpp | 2 +- daemon/rtsp_server.hpp | 4 +- daemon/scripts/fetch_streamer_files.sh | 8 + daemon/session_manager.cpp | 52 ++- daemon/session_manager.hpp | 24 +- daemon/streamer.cpp | 565 +++++++++++++++++++++++++ daemon/streamer.hpp | 115 +++++ daemon/tests/daemon.conf | 5 + daemon/tests/daemon_test.cpp | 15 +- daemon/utils.cpp | 2 +- systemd/aes67-daemon.service | 8 +- systemd/daemon.conf | 5 + systemd/install.sh | 2 +- test/daemon.conf | 5 + ubuntu-packages.sh | 1 + webui/src/Config.jsx | 46 +- webui/src/Services.js | 9 +- 32 files changed, 1208 insertions(+), 72 deletions(-) create mode 100755 daemon/scripts/fetch_streamer_files.sh create mode 100644 daemon/streamer.cpp create mode 100644 daemon/streamer.hpp diff --git a/README.md b/README.md index 1730560..4c27901 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. + +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/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..836d4a5 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": true, "auto_sinks_update": true } 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..348a9b0 --- /dev/null +++ b/daemon/streamer.cpp @@ -0,0 +1,565 @@ +// 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 result = 0; + 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) { + result += r; + 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_; + })); + } + + for (auto& res : ress) { + res.wait(); + (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..b8511c1 100644 --- a/systemd/daemon.conf +++ b/systemd/daemon.conf @@ -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": true, "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.setState({streamerEnabled: e.target.checked})} checked={this.state.streamerEnabled ? true : undefined}/>
this.setState({streamerChannels: e.target.value, streamerChIntervalErr: !e.currentTarget.checkValidity()})} required/>
this.setState({streamerFiles: e.target.value, streamerFilesIntervalErr: !e.currentTarget.checkValidity()})} required/>
this.setState({streamerFileDuration: e.target.value, streamerFileDurationIntervalErr: !e.currentTarget.checkValidity()})} required/>
+
{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 => { From dc1d6970809b3c4bfdede94b928baeed8a4a8f63 Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Sat, 6 Jul 2024 17:43:30 +0200 Subject: [PATCH 2/5] Added libfaac-dev to Dockerfile.daemon_tests --- Dockerfile.daemon_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a9d8ed5e3ab35cd84e103dc324bbc8c44a066481 Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli <56439183+bondagit@users.noreply.github.com> Date: Sat, 6 Jul 2024 17:52:57 +0200 Subject: [PATCH 3/5] Update README.md with HTTP Streamer diagram --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c27901..2f874e3 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ The HTTP Streamer was introduced with the daemon version 2.0 and it is used to r 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. From 49bf65a3d95e93d62d560cabeb24d6fefbea968c Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Sat, 6 Jul 2024 18:05:48 +0200 Subject: [PATCH 4/5] Removed compilation warnings from streamer.cpp --- daemon/streamer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/daemon/streamer.cpp b/daemon/streamer.cpp index 348a9b0..70011cf 100644 --- a/daemon/streamer.cpp +++ b/daemon/streamer.cpp @@ -150,7 +150,6 @@ bool Streamer::pcm_suspend() { ssize_t Streamer::pcm_read(uint8_t* data, size_t rcount) { ssize_t r; - size_t result = 0; size_t count = rcount; if (count != chunk_samples_) { @@ -174,7 +173,6 @@ ssize_t Streamer::pcm_read(uint8_t* data, size_t rcount) { return -1; } if (r > 0) { - result += r; count -= r; data += r * bytes_per_frame_; } @@ -443,6 +441,7 @@ void Streamer::close_files(uint8_t files_id) { std::ostream_iterator( output_streams_[std::make_pair(sink.id, files_id)])); output_ids_[files_id] = file_counter_; + return true; })); } From 107d36af61e219aa727c2f3adfb9257eeb0a6306 Mon Sep 17 00:00:00 2001 From: Andrea Bondavalli Date: Sun, 7 Jul 2024 11:44:21 +0200 Subject: [PATCH 5/5] build.sh script chaged to build with WITH_SYSTEMD=ON. Changed default daemon configuration with HTTP streamer disabled. Additional cosmetic changes. --- build.sh | 2 +- daemon/daemon.conf | 2 +- daemon/driver_manager.cpp | 2 +- daemon/streamer.cpp | 1 - systemd/daemon.conf | 4 ++-- 5 files changed, 5 insertions(+), 6 deletions(-) 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/daemon.conf b/daemon/daemon.conf index 836d4a5..245ac61 100644 --- a/daemon/daemon.conf +++ b/daemon/daemon.conf @@ -24,6 +24,6 @@ "streamer_files_num": 8, "streamer_file_duration": 1, "streamer_player_buffer_files_num": 1, - "streamer_enabled": true, + "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/streamer.cpp b/daemon/streamer.cpp index 70011cf..a32dfa4 100644 --- a/daemon/streamer.cpp +++ b/daemon/streamer.cpp @@ -446,7 +446,6 @@ void Streamer::close_files(uint8_t files_id) { } for (auto& res : ress) { - res.wait(); (void)res.get(); } } diff --git a/systemd/daemon.conf b/systemd/daemon.conf index b8511c1..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, @@ -24,6 +24,6 @@ "streamer_files_num": 8, "streamer_file_duration": 1, "streamer_player_buffer_files_num": 1, - "streamer_enabled": true, + "streamer_enabled": false, "auto_sinks_update": true }