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.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 => {