ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
audio_reader.cpp
Go to the documentation of this file.
1#include "audio_reader.h"
2
3#ifdef USE_ESP32
4
6#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
9
10#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
11#include "esp_crt_bundle.h"
12#endif
13
14namespace esphome::audio {
15
16static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
17
18static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
19static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6;
20
21static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
22
23static const uint8_t MAX_REDIRECTIONS = 5;
24
25static const char *const TAG = "audio_reader";
26
27// Some common HTTP status codes - borrowed from http_request component accessed 20241224
54
56
57esp_err_t AudioReader::add_sink(const std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer) {
58 if (current_audio_file_ != nullptr) {
59 // A transfer buffer isn't ncessary for a local file
60 this->file_ring_buffer_ = output_ring_buffer.lock();
61 if (this->file_ring_buffer_ == nullptr) {
62 return ESP_ERR_INVALID_STATE;
63 }
64 return ESP_OK;
65 }
66
67 if (this->output_transfer_buffer_ != nullptr) {
68 this->output_transfer_buffer_->set_sink(output_ring_buffer);
69 return ESP_OK;
70 }
71
72 return ESP_ERR_INVALID_STATE;
73}
74
75esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) {
76 file_type = AudioFileType::NONE;
77
78 this->current_audio_file_ = audio_file;
79
80 this->file_current_ = audio_file->data;
81 file_type = audio_file->file_type;
82
83 return ESP_OK;
84}
85
86esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
87 file_type = AudioFileType::NONE;
88
89 this->cleanup_connection_();
90
91 if (uri.empty()) {
92 return ESP_ERR_INVALID_ARG;
93 }
94
95 esp_http_client_config_t client_config = {};
96
97 client_config.url = uri.c_str();
98 client_config.cert_pem = nullptr;
99 client_config.disable_auto_redirect = false;
100 client_config.max_redirection_count = MAX_REDIRECTIONS;
101 client_config.event_handler = http_event_handler;
102 client_config.user_data = this;
103 client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
104 client_config.keep_alive_enable = true;
105 client_config.timeout_ms = CONNECTION_TIMEOUT_MS; // Shouldn't trigger watchdog resets if caller runs in a task
106
107#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
108 if (uri.find("https:") != std::string::npos) {
109 client_config.crt_bundle_attach = esp_crt_bundle_attach;
110 }
111#endif
112
113 this->client_ = esp_http_client_init(&client_config);
114
115 if (this->client_ == nullptr) {
116 return ESP_FAIL;
117 }
118
119 esp_err_t err = esp_http_client_open(this->client_, 0);
120
121 if (err != ESP_OK) {
122 ESP_LOGE(TAG, "Failed to open URL");
123 this->cleanup_connection_();
124 return err;
125 }
126
127 int64_t header_length = esp_http_client_fetch_headers(this->client_);
128 uint8_t reattempt_count = 0;
129 while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) {
130 this->cleanup_connection_();
131 if (header_length != -ESP_ERR_HTTP_EAGAIN) {
132 // Serious error, no recovery
133 return ESP_FAIL;
134 } else {
135 // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available
136 this->client_ = esp_http_client_init(&client_config);
137 esp_http_client_open(this->client_, 0);
138 header_length = esp_http_client_fetch_headers(this->client_);
139 ++reattempt_count;
140 }
141 }
142
143 if (header_length < 0) {
144 ESP_LOGE(TAG, "Failed to fetch headers");
145 this->cleanup_connection_();
146 return ESP_FAIL;
147 }
148
149 int status_code = esp_http_client_get_status_code(this->client_);
150
151 if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
152 this->cleanup_connection_();
153 return ESP_FAIL;
154 }
155
156 ssize_t redirect_count = 0;
157
158 while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) {
159 err = esp_http_client_open(this->client_, 0);
160 if (err != ESP_OK) {
161 this->cleanup_connection_();
162 return ESP_FAIL;
163 }
164
165 header_length = esp_http_client_fetch_headers(this->client_);
166 if (header_length < 0) {
167 this->cleanup_connection_();
168 return ESP_FAIL;
169 }
170
171 status_code = esp_http_client_get_status_code(this->client_);
172
173 if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
174 this->cleanup_connection_();
175 return ESP_FAIL;
176 }
177
178 ++redirect_count;
179 }
180
182 // Failed to determine the file type from the header, fallback to using the url
183 char url[500];
184 err = esp_http_client_get_url(this->client_, url, 500);
185 if (err != ESP_OK) {
186 this->cleanup_connection_();
187 return err;
188 }
189
190 file_type = detect_audio_file_type(nullptr, url);
191 if (file_type == AudioFileType::NONE) {
192 this->cleanup_connection_();
193 return ESP_ERR_NOT_SUPPORTED;
194 }
195 } else {
196 file_type = this->audio_file_type_;
197 }
198
199 this->last_data_read_ms_ = millis();
200
202 if (this->output_transfer_buffer_ == nullptr) {
203 return ESP_ERR_NO_MEM;
204 }
205
206 return ESP_OK;
207}
208
210 if (this->client_ != nullptr) {
211 return this->http_read_();
212 } else if (this->current_audio_file_ != nullptr) {
213 return this->file_read_();
214 }
215
217}
218
219esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
220 // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
221 AudioReader *this_reader = (AudioReader *) evt->user_data;
222
223 switch (evt->event_id) {
224 case HTTP_EVENT_ON_HEADER:
225 if (strcasecmp(evt->header_key, "Content-Type") == 0) {
226 this_reader->audio_file_type_ = detect_audio_file_type(evt->header_value, nullptr);
227 }
228 break;
229 default:
230 break;
231 }
232 return ESP_OK;
233}
234
236 size_t remaining_bytes = this->current_audio_file_->length - (this->file_current_ - this->current_audio_file_->data);
237 if (remaining_bytes > 0) {
238 size_t bytes_written = this->file_ring_buffer_->write_without_replacement(this->file_current_, remaining_bytes,
239 pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
240 this->file_current_ += bytes_written;
241
243 }
244
246}
247
249 this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
250
251 if (esp_http_client_is_complete_data_received(this->client_)) {
252 if (this->output_transfer_buffer_->available() == 0) {
253 this->cleanup_connection_();
255 }
256 } else if (this->output_transfer_buffer_->free() > 0) {
257 int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(),
258 this->output_transfer_buffer_->free());
259
260 if (received_len > 0) {
261 this->output_transfer_buffer_->increase_buffer_length(received_len);
262 this->last_data_read_ms_ = millis();
264 } else if (received_len <= 0) {
265 // HTTP read error
266 if (received_len == -1) {
267 // A true connection error occured, no chance at recovery
268 this->cleanup_connection_();
270 }
271
272 // Read timed out, manually verify if it has been too long since the last successful read
273 if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) {
274 ESP_LOGE(TAG, "Timed out");
275 this->cleanup_connection_();
277 }
278
279 delay(READ_WRITE_TIMEOUT_MS);
280 }
281 }
282
284}
285
287 if (this->client_ != nullptr) {
288 esp_http_client_close(this->client_);
289 esp_http_client_cleanup(this->client_);
290 this->client_ = nullptr;
291 }
292}
293
294} // namespace esphome::audio
295
296#endif
AudioReaderState http_read_()
AudioReaderState file_read_()
std::unique_ptr< AudioSinkTransferBuffer > output_transfer_buffer_
const uint8_t * file_current_
esp_err_t add_sink(const std::weak_ptr< ring_buffer::RingBuffer > &output_ring_buffer)
Adds a sink ring buffer for audio data.
AudioReaderState read()
Reads new file data from the source and sends to the ring buffer sink.
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
Monitors the http client events to attempt determining the file type from the Content-Type header.
std::shared_ptr< ring_buffer::RingBuffer > file_ring_buffer_
esp_http_client_handle_t client_
esp_err_t start(const std::string &uri, AudioFileType &file_type)
Starts reading an audio file from an http source.
static std::unique_ptr< AudioSinkTransferBuffer > create(size_t buffer_size)
Creates a new sink transfer buffer.
__int64 ssize_t
Definition httplib.h:178
AudioFileType detect_audio_file_type(const char *content_type, const char *url)
Detect audio file type from a Content-Type header value and/or URL extension.
Definition audio.cpp:66
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
const uint8_t * data
Definition audio.h:125
AudioFileType file_type
Definition audio.h:127