commit
acbb385516
@ -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
|
||||
|
14
README.md
14
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 ##
|
||||
<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 ###
|
||||
|
2
build.sh
2
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 ..
|
||||
|
||||
|
@ -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_)
|
||||
|
@ -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.
|
||||
|
@ -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 */
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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};
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{};
|
||||
|
@ -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)";
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 << " "
|
||||
|
@ -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_;
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"));
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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));
|
||||
|
||||
|
8
daemon/scripts/fetch_streamer_files.sh
Executable file
8
daemon/scripts/fetch_streamer_files.sh
Executable 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
|
@ -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()) {
|
||||
|
@ -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_;
|
||||
|
563
daemon/streamer.cpp
Normal file
563
daemon/streamer.cpp
Normal file
@ -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 <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 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<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_;
|
||||
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{};
|
||||
}
|
115
daemon/streamer.hpp
Normal file
115
daemon/streamer.hpp
Normal 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
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user