ESPHome 2025.5.0
Loading...
Searching...
No Matches
audio_reader.cpp
Go to the documentation of this file.
1#include "audio_reader.h"
2
3#ifdef USE_ESP_IDF
4
6#include "esphome/core/hal.h"
8
9#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
10#include "esp_crt_bundle.h"
11#endif
12
13namespace esphome {
14namespace audio {
15
16static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
17
18static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
19
20// The number of times the http read times out with no data before throwing an error
21static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100;
22
23static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
24
25static const uint8_t MAX_REDIRECTION = 5;
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<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 return ESP_OK;
62 }
63
64 if (this->output_transfer_buffer_ != nullptr) {
65 this->output_transfer_buffer_->set_sink(output_ring_buffer);
66 return ESP_OK;
67 }
68
69 return ESP_ERR_INVALID_STATE;
70}
71
72esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) {
73 file_type = AudioFileType::NONE;
74
75 this->current_audio_file_ = audio_file;
76
77 this->file_current_ = audio_file->data;
78 file_type = audio_file->file_type;
79
80 return ESP_OK;
81}
82
83esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
84 file_type = AudioFileType::NONE;
85
86 this->cleanup_connection_();
87
88 if (uri.empty()) {
89 return ESP_ERR_INVALID_ARG;
90 }
91
92 esp_http_client_config_t client_config = {};
93
94 client_config.url = uri.c_str();
95 client_config.cert_pem = nullptr;
96 client_config.disable_auto_redirect = false;
97 client_config.max_redirection_count = 10;
98 client_config.event_handler = http_event_handler;
99 client_config.user_data = this;
100 client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
101 client_config.keep_alive_enable = true;
102 client_config.timeout_ms = CONNECTION_TIMEOUT_MS; // Shouldn't trigger watchdog resets if caller runs in a task
103
104#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
105 if (uri.find("https:") != std::string::npos) {
106 client_config.crt_bundle_attach = esp_crt_bundle_attach;
107 }
108#endif
109
110 this->client_ = esp_http_client_init(&client_config);
111
112 if (this->client_ == nullptr) {
113 return ESP_FAIL;
114 }
115
116 esp_err_t err = esp_http_client_open(this->client_, 0);
117
118 if (err != ESP_OK) {
119 this->cleanup_connection_();
120 return err;
121 }
122
123 int64_t header_length = esp_http_client_fetch_headers(this->client_);
124 if (header_length < 0) {
125 this->cleanup_connection_();
126 return ESP_FAIL;
127 }
128
129 int status_code = esp_http_client_get_status_code(this->client_);
130
131 if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
132 this->cleanup_connection_();
133 return ESP_FAIL;
134 }
135
136 ssize_t redirect_count = 0;
137
138 while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) {
139 err = esp_http_client_open(this->client_, 0);
140 if (err != ESP_OK) {
141 this->cleanup_connection_();
142 return ESP_FAIL;
143 }
144
145 header_length = esp_http_client_fetch_headers(this->client_);
146 if (header_length < 0) {
147 this->cleanup_connection_();
148 return ESP_FAIL;
149 }
150
151 status_code = esp_http_client_get_status_code(this->client_);
152
153 if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
154 this->cleanup_connection_();
155 return ESP_FAIL;
156 }
157
158 ++redirect_count;
159 }
160
162 // Failed to determine the file type from the header, fallback to using the url
163 char url[500];
164 err = esp_http_client_get_url(this->client_, url, 500);
165 if (err != ESP_OK) {
166 this->cleanup_connection_();
167 return err;
168 }
169
170 std::string url_string = str_lower_case(url);
171
172 if (str_endswith(url_string, ".wav")) {
173 file_type = AudioFileType::WAV;
174 }
175#ifdef USE_AUDIO_MP3_SUPPORT
176 else if (str_endswith(url_string, ".mp3")) {
177 file_type = AudioFileType::MP3;
178 }
179#endif
180#ifdef USE_AUDIO_FLAC_SUPPORT
181 else if (str_endswith(url_string, ".flac")) {
182 file_type = AudioFileType::FLAC;
183 }
184#endif
185 else {
186 file_type = AudioFileType::NONE;
187 this->cleanup_connection_();
188 return ESP_ERR_NOT_SUPPORTED;
189 }
190 } else {
191 file_type = this->audio_file_type_;
192 }
193
194 this->last_data_read_ms_ = millis();
195
197 if (this->output_transfer_buffer_ == nullptr) {
198 return ESP_ERR_NO_MEM;
199 }
200
201 return ESP_OK;
202}
203
205 if (this->client_ != nullptr) {
206 return this->http_read_();
207 } else if (this->current_audio_file_ != nullptr) {
208 return this->file_read_();
209 }
210
212}
213
215#ifdef USE_AUDIO_MP3_SUPPORT
216 if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
217 strcasecmp(content_type, "audio/mpeg") == 0) {
218 return AudioFileType::MP3;
219 }
220#endif
221 if (strcasecmp(content_type, "audio/wav") == 0) {
222 return AudioFileType::WAV;
223 }
224#ifdef USE_AUDIO_FLAC_SUPPORT
225 if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
226 return AudioFileType::FLAC;
227 }
228#endif
229 return AudioFileType::NONE;
230}
231
232esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
233 // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
234 AudioReader *this_reader = (AudioReader *) evt->user_data;
235
236 switch (evt->event_id) {
237 case HTTP_EVENT_ON_HEADER:
238 if (strcasecmp(evt->header_key, "Content-Type") == 0) {
239 this_reader->audio_file_type_ = get_audio_type(evt->header_value);
240 }
241 break;
242 default:
243 break;
244 }
245 return ESP_OK;
246}
247
249 size_t remaining_bytes = this->current_audio_file_->length - (this->file_current_ - this->current_audio_file_->data);
250 if (remaining_bytes > 0) {
251 size_t bytes_written = this->file_ring_buffer_->write_without_replacement(this->file_current_, remaining_bytes,
252 pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
253 this->file_current_ += bytes_written;
254
256 }
257
259}
260
262 this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
263
264 if (esp_http_client_is_complete_data_received(this->client_)) {
265 if (this->output_transfer_buffer_->available() == 0) {
266 this->cleanup_connection_();
268 }
269 } else if (this->output_transfer_buffer_->free() > 0) {
270 size_t bytes_to_read = this->output_transfer_buffer_->free();
271 int received_len =
272 esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);
273
274 if (received_len > 0) {
275 this->output_transfer_buffer_->increase_buffer_length(received_len);
276 this->last_data_read_ms_ = millis();
277 } else if (received_len < 0) {
278 // HTTP read error
279 this->cleanup_connection_();
281 } else {
282 if (bytes_to_read > 0) {
283 // Read timed out
284 if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) {
285 this->cleanup_connection_();
287 }
288
289 delay(READ_WRITE_TIMEOUT_MS);
290 }
291 }
292 }
293
295}
296
298 if (this->client_ != nullptr) {
299 esp_http_client_close(this->client_);
300 esp_http_client_cleanup(this->client_);
301 this->client_ = nullptr;
302 }
303}
304
305} // namespace audio
306} // namespace esphome
307
308#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:175
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string str_lower_case(const std::string &str)
Convert the string to lower case.
Definition helpers.cpp:290
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:28
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:27
bool str_endswith(const std::string &str, const std::string &end)
Check whether a string ends with a value.
Definition helpers.cpp:267
const uint8_t * data
Definition audio.h:120
AudioFileType file_type
Definition audio.h:122