ESPHome 2026.3.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 {
15namespace audio {
16
17static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
18
19static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
20static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6;
21
22static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
23
24static const uint8_t MAX_REDIRECTIONS = 5;
25
26static const char *const TAG = "audio_reader";
27
28// Some common HTTP status codes - borrowed from http_request component accessed 20241224
55
57
58esp_err_t AudioReader::add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer) {
59 if (current_audio_file_ != nullptr) {
60 // A transfer buffer isn't ncessary for a local file
61 this->file_ring_buffer_ = output_ring_buffer.lock();
62 return ESP_OK;
63 }
64
65 if (this->output_transfer_buffer_ != nullptr) {
66 this->output_transfer_buffer_->set_sink(output_ring_buffer);
67 return ESP_OK;
68 }
69
70 return ESP_ERR_INVALID_STATE;
71}
72
73esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) {
74 file_type = AudioFileType::NONE;
75
76 this->current_audio_file_ = audio_file;
77
78 this->file_current_ = audio_file->data;
79 file_type = audio_file->file_type;
80
81 return ESP_OK;
82}
83
84esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
85 file_type = AudioFileType::NONE;
86
87 this->cleanup_connection_();
88
89 if (uri.empty()) {
90 return ESP_ERR_INVALID_ARG;
91 }
92
93 esp_http_client_config_t client_config = {};
94
95 client_config.url = uri.c_str();
96 client_config.cert_pem = nullptr;
97 client_config.disable_auto_redirect = false;
98 client_config.max_redirection_count = MAX_REDIRECTIONS;
99 client_config.event_handler = http_event_handler;
100 client_config.user_data = this;
101 client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
102 client_config.keep_alive_enable = true;
103 client_config.timeout_ms = CONNECTION_TIMEOUT_MS; // Shouldn't trigger watchdog resets if caller runs in a task
104
105#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
106 if (uri.find("https:") != std::string::npos) {
107 client_config.crt_bundle_attach = esp_crt_bundle_attach;
108 }
109#endif
110
111 this->client_ = esp_http_client_init(&client_config);
112
113 if (this->client_ == nullptr) {
114 return ESP_FAIL;
115 }
116
117 esp_err_t err = esp_http_client_open(this->client_, 0);
118
119 if (err != ESP_OK) {
120 ESP_LOGE(TAG, "Failed to open URL");
121 this->cleanup_connection_();
122 return err;
123 }
124
125 int64_t header_length = esp_http_client_fetch_headers(this->client_);
126 uint8_t reattempt_count = 0;
127 while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) {
128 this->cleanup_connection_();
129 if (header_length != -ESP_ERR_HTTP_EAGAIN) {
130 // Serious error, no recovery
131 return ESP_FAIL;
132 } else {
133 // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available
134 this->client_ = esp_http_client_init(&client_config);
135 esp_http_client_open(this->client_, 0);
136 header_length = esp_http_client_fetch_headers(this->client_);
137 ++reattempt_count;
138 }
139 }
140
141 if (header_length < 0) {
142 ESP_LOGE(TAG, "Failed to fetch headers");
143 this->cleanup_connection_();
144 return ESP_FAIL;
145 }
146
147 int status_code = esp_http_client_get_status_code(this->client_);
148
149 if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
150 this->cleanup_connection_();
151 return ESP_FAIL;
152 }
153
154 ssize_t redirect_count = 0;
155
156 while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) {
157 err = esp_http_client_open(this->client_, 0);
158 if (err != ESP_OK) {
159 this->cleanup_connection_();
160 return ESP_FAIL;
161 }
162
163 header_length = esp_http_client_fetch_headers(this->client_);
164 if (header_length < 0) {
165 this->cleanup_connection_();
166 return ESP_FAIL;
167 }
168
169 status_code = esp_http_client_get_status_code(this->client_);
170
171 if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
172 this->cleanup_connection_();
173 return ESP_FAIL;
174 }
175
176 ++redirect_count;
177 }
178
180 // Failed to determine the file type from the header, fallback to using the url
181 char url[500];
182 err = esp_http_client_get_url(this->client_, url, 500);
183 if (err != ESP_OK) {
184 this->cleanup_connection_();
185 return err;
186 }
187
188 if (str_endswith_ignore_case(url, ".wav")) {
189 file_type = AudioFileType::WAV;
190 }
191#ifdef USE_AUDIO_MP3_SUPPORT
192 else if (str_endswith_ignore_case(url, ".mp3")) {
193 file_type = AudioFileType::MP3;
194 }
195#endif
196#ifdef USE_AUDIO_FLAC_SUPPORT
197 else if (str_endswith_ignore_case(url, ".flac")) {
198 file_type = AudioFileType::FLAC;
199 }
200#endif
201#ifdef USE_AUDIO_OPUS_SUPPORT
202 else if (str_endswith_ignore_case(url, ".opus")) {
203 file_type = AudioFileType::OPUS;
204 }
205#endif
206 else {
207 file_type = AudioFileType::NONE;
208 this->cleanup_connection_();
209 return ESP_ERR_NOT_SUPPORTED;
210 }
211 } else {
212 file_type = this->audio_file_type_;
213 }
214
215 this->last_data_read_ms_ = millis();
216
218 if (this->output_transfer_buffer_ == nullptr) {
219 return ESP_ERR_NO_MEM;
220 }
221
222 return ESP_OK;
223}
224
226 if (this->client_ != nullptr) {
227 return this->http_read_();
228 } else if (this->current_audio_file_ != nullptr) {
229 return this->file_read_();
230 }
231
233}
234
236#ifdef USE_AUDIO_MP3_SUPPORT
237 if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
238 strcasecmp(content_type, "audio/mpeg") == 0) {
239 return AudioFileType::MP3;
240 }
241#endif
242 if (strcasecmp(content_type, "audio/wav") == 0) {
243 return AudioFileType::WAV;
244 }
245#ifdef USE_AUDIO_FLAC_SUPPORT
246 if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
247 return AudioFileType::FLAC;
248 }
249#endif
250#ifdef USE_AUDIO_OPUS_SUPPORT
251 // Match "audio/ogg" with a codecs parameter containing "opus"
252 // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc.
253 // Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams
254 if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) {
255 return AudioFileType::OPUS;
256 }
257#endif
258 return AudioFileType::NONE;
259}
260
261esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
262 // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
263 AudioReader *this_reader = (AudioReader *) evt->user_data;
264
265 switch (evt->event_id) {
266 case HTTP_EVENT_ON_HEADER:
267 if (strcasecmp(evt->header_key, "Content-Type") == 0) {
268 this_reader->audio_file_type_ = get_audio_type(evt->header_value);
269 }
270 break;
271 default:
272 break;
273 }
274 return ESP_OK;
275}
276
278 size_t remaining_bytes = this->current_audio_file_->length - (this->file_current_ - this->current_audio_file_->data);
279 if (remaining_bytes > 0) {
280 size_t bytes_written = this->file_ring_buffer_->write_without_replacement(this->file_current_, remaining_bytes,
281 pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
282 this->file_current_ += bytes_written;
283
285 }
286
288}
289
291 this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
292
293 if (esp_http_client_is_complete_data_received(this->client_)) {
294 if (this->output_transfer_buffer_->available() == 0) {
295 this->cleanup_connection_();
297 }
298 } else if (this->output_transfer_buffer_->free() > 0) {
299 int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(),
300 this->output_transfer_buffer_->free());
301
302 if (received_len > 0) {
303 this->output_transfer_buffer_->increase_buffer_length(received_len);
304 this->last_data_read_ms_ = millis();
306 } else if (received_len <= 0) {
307 // HTTP read error
308 if (received_len == -1) {
309 // A true connection error occured, no chance at recovery
310 this->cleanup_connection_();
312 }
313
314 // Read timed out, manually verify if it has been too long since the last successful read
315 if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) {
316 ESP_LOGE(TAG, "Timed out");
317 this->cleanup_connection_();
319 }
320
321 delay(READ_WRITE_TIMEOUT_MS);
322 }
323 }
324
326}
327
329 if (this->client_ != nullptr) {
330 esp_http_client_close(this->client_);
331 esp_http_client_cleanup(this->client_);
332 this->client_ = nullptr;
333 }
334}
335
336} // namespace audio
337} // namespace esphome
338
339#endif
AudioReaderState http_read_()
AudioReaderState file_read_()
std::unique_ptr< AudioSinkTransferBuffer > output_transfer_buffer_
const uint8_t * file_current_
static AudioFileType get_audio_type(const char *content_type)
Determines the audio file type from the http header's Content-Type key.
std::shared_ptr< RingBuffer > file_ring_buffer_
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.
esp_http_client_handle_t client_
esp_err_t add_sink(const std::weak_ptr< RingBuffer > &output_ring_buffer)
Adds a sink ring buffer for audio data.
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
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
bool str_endswith_ignore_case(const char *str, size_t str_len, const char *suffix, size_t suffix_len)
Case-insensitive check if string ends with suffix (no heap allocation).
Definition helpers.cpp:179
void HOT delay(uint32_t ms)
Definition core.cpp:27
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
const uint8_t * data
Definition audio.h:123
AudioFileType file_type
Definition audio.h:125