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.
This commit is contained in:
Andrea Bondavalli 2024-07-06 17:27:49 +02:00
parent 45a39a4d2e
commit 466f6b4fc4
32 changed files with 1208 additions and 72 deletions

View File

@ -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 ##
<a name="prerequisite"></a>
@ -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 ###

View File

@ -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_)

View File

@ -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<a name="version"></a> ###
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<a name="ptp-config"></a> ###
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<a name="streamer-info"></a> ###
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.

View File

@ -100,7 +100,8 @@ class Browser : public MDNSClient {
SAP sap_{config_->get_sap_mcast_addr()};
IGMP igmp_;
std::chrono::time_point<std::chrono::steady_clock> startup_{std::chrono::steady_clock::now()};
std::chrono::time_point<std::chrono::steady_clock> startup_{
std::chrono::steady_clock::now()};
uint32_t last_update_{0}; /* seconds from daemon startup */
};

View File

@ -66,6 +66,15 @@ std::shared_ptr<Config> 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;

View File

@ -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};

View File

@ -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
}

View File

@ -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)";
}

View File

@ -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 {

View File

@ -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<uint8_t>(streamer_info_status::ok);
if (!config_->get_streamer_enabled()) {
info.status = static_cast<uint8_t>(streamer_info_status::not_enabled);
} else {
try {
id = std::stoi(req.matches[1]);
} catch (...) {
info.status = static_cast<uint8_t>(streamer_info_status::invalid_sink);
}
}
if (info.status == static_cast<uint8_t>(streamer_info_status::ok)) {
StreamSink sink;
auto ret = session_manager_->get_sink(id, sink);
if (ret) {
info.status =
static_cast<uint8_t>(streamer_info_status::cannot_retrieve);
} else {
ret = streamer_->get_info(sink, info);
switch (ret.value()) {
case static_cast<int>(DaemonErrc::streamer_not_running):
info.status =
static_cast<uint8_t>(streamer_info_status::ptp_not_locked);
break;
case static_cast<int>(DaemonErrc::streamer_invalid_ch):
info.status =
static_cast<uint8_t>(streamer_info_status::invalid_channels);
break;
case static_cast<int>(DaemonErrc::streamer_retry_later):
info.status = static_cast<uint8_t>(streamer_info_status::buffering);
break;
default:
info.status = static_cast<uint8_t>(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 << " "

View File

@ -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<SessionManager> session_manager,
std::shared_ptr<Browser> browser,
std::shared_ptr<Streamer> streamer,
std::shared_ptr<Config> 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<SessionManager> session_manager_;
std::shared_ptr<Browser> browser_;
std::shared_ptr<Streamer> streamer_;
std::shared_ptr<Config> config_;
httplib::Server svr_;
std::future<bool> res_;

View File

@ -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<second_t>(steady_clock::now() - source.last_seen_timepoint).count())
<< ",\n \"last_seen\": "
<< unsigned(duration_cast<second_t>(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<RemoteSource>& 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<std::string>()));
} else if (key == "streamer_channels") {
config.set_streamer_channels(val.get_value<uint8_t>());
} else if (key == "streamer_files_num") {
config.set_streamer_files_num(val.get_value<uint8_t>());
} else if (key == "streamer_file_duration") {
config.set_streamer_file_duration(val.get_value<uint16_t>());
} else if (key == "streamer_player_buffer_files_num") {
config.set_streamer_player_buffer_files_num(val.get_value<uint8_t>());
} else if (key == "streamer_enabled") {
config.set_streamer_enabled(val.get_value<bool>());
} else if (key == "log_severity") {
config.set_log_severity(val.get_value<int>());
} 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()));
}

View File

@ -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<StreamSource>& sources,
const std::list<StreamSink>& sinks);
std::string remote_source_to_json(const RemoteSource& source);
std::string remote_sources_to_json(const std::list<RemoteSource>& sources);
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<StreamSource>& sources,
std::list<StreamSink>& sinks);
#endif

View File

@ -30,6 +30,7 @@
#include "mdns_server.hpp"
#include "rtsp_server.hpp"
#include "session_manager.hpp"
#include "streamer.hpp"
#ifdef _USE_SYSTEMD_
#include <systemd/sd-daemon.h>
@ -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<bool> 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<std::string>()->default_value("/etc/daemon.conf"),
"daemon configuration file")(
"http_addr,a",po::value<std::string>(),
"HTTP server addr")("http_port,p", po::value<int>(),
"HTTP server port")("help,h",
"Print this help "
"message");
"daemon configuration file")("http_addr,a", po::value<std::string>(),
"HTTP server addr")(
"http_port,p", po::value<int>(), "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"));

View File

@ -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;

View File

@ -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:

View File

@ -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));

View File

@ -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

View File

@ -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 ObserverType::remove_source:
remove_source_observers.push_back(cb);
case SourceObserverType::remove_source:
remove_source_observers_.push_back(cb);
break;
case ObserverType::update_source:
update_source_observers.push_back(cb);
case SourceObserverType::update_source:
update_source_observers_.push_back(cb);
break;
}
}
void SessionManager::add_sink_observer(SinkObserverType type,
const SinkObserver& cb) {
switch (type) {
case SinkObserverType::add_sink:
add_sink_observers_.push_back(cb);
break;
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()) {

View File

@ -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<bool(const std::string& status)>;
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<Observer> add_source_observers;
std::list<Observer> remove_source_observers;
std::list<Observer> update_source_observers;
std::list<SourceObserver> add_source_observers_;
std::list<SourceObserver> remove_source_observers_;
std::list<SourceObserver> update_source_observers_;
std::list<PtpStatusObserver> ptp_status_observers_;
std::list<SinkObserver> add_sink_observers_;
std::list<SinkObserver> remove_sink_observers_;
std::list<SinkObserver> update_sink_observers_;
SAP sap_{config_->get_sap_mcast_addr()};
IGMP igmp_;

565
daemon/streamer.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#include <boost/algorithm/string.hpp>
#include "utils.hpp"
#include "streamer.hpp"
std::shared_ptr<Streamer> Streamer::create(
std::shared_ptr<SessionManager> session_manager,
std::shared_ptr<Config> config) {
// no need to be thread-safe here
static std::weak_ptr<Streamer> instance;
if (auto ptr = instance.lock()) {
return ptr;
}
auto ptr = std::shared_ptr<Streamer>(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<uint8_t>(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<std::future<bool> > 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<uint8_t>(
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{};
}

115
daemon/streamer.hpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#ifndef _STREAMER_HPP_
#define _STREAMER_HPP_
#include <cstdlib>
#include <iostream>
#include <memory>
#include <vector>
#include <alsa/asoundlib.h>
#include <faac.h>
#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<Streamer> create(
std::shared_ptr<SessionManager> session_manager,
std::shared_ptr<Config> 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<SessionManager> session_manager,
std::shared_ptr<Config> 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<SessionManager> session_manager_;
std::shared_ptr<Config> 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<uint8_t, size_t> total_sink_samples_;
uint32_t buffer_offset_{0};
std::unordered_map<uint8_t, std::shared_mutex> streams_mutex_;
std::unordered_map<uint8_t, std::stringstream> tmp_streams_;
std::map<std::pair<uint8_t, uint8_t>, std::stringstream> output_streams_;
std::unordered_map<uint8_t, uint32_t> output_ids_;
uint32_t file_counter_{0};
std::atomic<uint8_t> file_id_{0};
std::unique_ptr<uint8_t[]> buffer_;
std::unordered_map<uint8_t, std::unique_ptr<uint8_t[]> > out_buffer_;
std::unordered_map<uint8_t, uint32_t> out_buffer_size_{0};
uint8_t channels_{8};
uint32_t rate_{0};
std::future<bool> res_;
snd_pcm_t* capture_handle_;
std::atomic_bool running_{false};
std::unordered_map<uint8_t, faacEncHandle> faac_;
std::unordered_map<uint8_t, std::mutex> faac_mutex_;
std::unordered_map<uint8_t, unsigned long> codec_in_samples_;
std::unordered_map<uint8_t, unsigned long> codec_out_buffer_size_;
};
#endif

View File

@ -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
}

View File

@ -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<std::string>("mac_addr");
auto ip_addr = pt.get<std::string>("ip_addr");
auto auto_sinks_update = pt.get<bool>("auto_sinks_update");
auto mdns_enabled = pt.get<bool>("mdns_enabled");
auto streamer_enabled = pt.get<bool>("streamer_enabled");
auto streamer_channels = pt.get<int>("streamer_channels");
auto streamer_files_num = pt.get<int>("streamer_files_num");
auto streamer_file_duration = pt.get<int>("streamer_file_duration");
auto streamer_player_buffer_files_num = pt.get<int>("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) {

View File

@ -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();
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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 {
</tr>
</tbody></table>
<br/>
{this.state.isConfigLoading ? <Loader/> : <h3>HTTP Streamer Config</h3>}
<table><tbody>
<tr height="35">
<th align="left"> <label>Streamer enabled</label> </th>
<th align="left"> <input type="checkbox" onChange={e => this.setState({streamerEnabled: e.target.checked})} checked={this.state.streamerEnabled ? true : undefined}/> </th>
</tr>
<tr>
<th align="left"> <label>Streamer channels</label> </th>
<th align="left"> <input type='number' min='2' max='16' className='input-number' value={this.state.streamerChannels} onChange={e => this.setState({streamerChannels: e.target.value, streamerChIntervalErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>Streamer files</label> </th>
<th align="left"> <input type='number' min='4' max='16' className='input-number' value={this.state.streamerFiles} onChange={e => this.setState({streamerFiles: e.target.value, streamerFilesIntervalErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>Streamer file duration (secs)</label> </th>
<th align="left"> <input type='number' min='1' max='4' className='input-number' value={this.state.streamerFileDuration} onChange={e => this.setState({streamerFileDuration: e.target.value, streamerFileDurationIntervalErr: !e.currentTarget.checkValidity()})} required/> </th>
</tr>
<tr>
<th align="left"> <label>Streamer player buffer files</label> </th>
<th align="left"> <input type='number' min='1' max='2' className='input-number' value={this.state.streamerPlayerBufferFiles} disabled required/> </th>
</tr>
</tbody></table>
<br/>
{this.state.isConfigLoading ? <Loader/> : <h3>Network Config</h3>}
<table><tbody>
<tr>

View File

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