Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP streamer #164

Merged
merged 5 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile.daemon_tests
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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:

Expand Down Expand Up @@ -104,9 +106,21 @@ The daemon should work on all Ubuntu starting from 18.04 onward, it's possible t
## Devices and interoperability tests ##
See [Devices and interoperability tests with the AES67 daemon](DEVICES.md)

## HTTP Streamer ##
The HTTP Streamer was introduced with the daemon version 2.0 and it is used to receive AES67 audio streams via HTTP file streaming.

The HTTP Streamer can be enabled via the _streamer_enabled_ daemon parameter.
When the Streamer is active the daemon starts capturing the configured _Sinks_ up to the maximum number of channels configured by the _streamer_channels_ parameters.
The captured PCM samples are split into _streamer_files_num_ files of _streamer_file_duration_ duration (in seconds) for each sink, compressed using AAC LC codec and served via HTTP.
![Screenshot 2024-06-15 at 15 36 48](https://github.com/bondagit/aes67-linux-daemon/assets/56439183/3341b05e-daed-4541-b0a1-28839d5b9a6b)
The HTTP streamer requires the libfaac-dev package to compile.

Please note that since the HTTP Streamer uses the RAVENNA ALSA device for capturing it's not possible to use such device for other audio captures.

## AES67 USB Receiver and Transmitter ##
See [Use your board as AES67 USB Receiver and Transmitter](USB_GADGET.md)


## Repository content ##

### [daemon](daemon) directory ###
Expand Down
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ..

7 changes: 5 additions & 2 deletions daemon/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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_)
Expand Down
96 changes: 94 additions & 2 deletions daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
3 changes: 2 additions & 1 deletion daemon/browser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
};

Expand Down
35 changes: 25 additions & 10 deletions daemon/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions daemon/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_; };
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() ||
Expand Down Expand Up @@ -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};
Expand Down
5 changes: 5 additions & 0 deletions daemon/daemon.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion daemon/driver_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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{};
Expand Down
6 changes: 6 additions & 0 deletions daemon/error_code.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
}
Expand Down
15 changes: 9 additions & 6 deletions daemon/error_code.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading