ESPHome  2025.2.0
audio_reader.cpp
Go to the documentation of this file.
1 #include "audio_reader.h"
2 
3 #ifdef USE_ESP_IDF
4 
5 #include "esphome/core/defines.h"
6 #include "esphome/core/hal.h"
7 #include "esphome/core/helpers.h"
8 
9 #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
10 #include "esp_crt_bundle.h"
11 #endif
12 
13 namespace esphome {
14 namespace audio {
15 
16 static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
17 
18 // The number of times the http read times out with no data before throwing an error
19 static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100;
20 
21 static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
22 
23 static const uint8_t MAX_REDIRECTION = 5;
24 
25 // Some common HTTP status codes - borrowed from http_request component accessed 20241224
26 enum HttpStatus {
30 
31  /* 3xx - Redirection */
39 
40  /* 4XX - CLIENT ERROR */
48 
49  /* 5xx - Server Error */
51 };
52 
54 
55 esp_err_t AudioReader::add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer) {
56  if (current_audio_file_ != nullptr) {
57  // A transfer buffer isn't ncessary for a local file
58  this->file_ring_buffer_ = output_ring_buffer.lock();
59  return ESP_OK;
60  }
61 
62  if (this->output_transfer_buffer_ != nullptr) {
63  this->output_transfer_buffer_->set_sink(output_ring_buffer);
64  return ESP_OK;
65  }
66 
67  return ESP_ERR_INVALID_STATE;
68 }
69 
70 esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) {
71  file_type = AudioFileType::NONE;
72 
73  this->current_audio_file_ = audio_file;
74 
75  this->file_current_ = audio_file->data;
76  file_type = audio_file->file_type;
77 
78  return ESP_OK;
79 }
80 
81 esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
82  file_type = AudioFileType::NONE;
83 
84  this->cleanup_connection_();
85 
86  if (uri.empty()) {
87  return ESP_ERR_INVALID_ARG;
88  }
89 
90  esp_http_client_config_t client_config = {};
91 
92  client_config.url = uri.c_str();
93  client_config.cert_pem = nullptr;
94  client_config.disable_auto_redirect = false;
95  client_config.max_redirection_count = 10;
96  client_config.event_handler = http_event_handler;
97  client_config.user_data = this;
98  client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
99  client_config.keep_alive_enable = true;
100  client_config.timeout_ms = 5000; // Shouldn't trigger watchdog resets if caller runs in a task
101 
102 #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
103  if (uri.find("https:") != std::string::npos) {
104  client_config.crt_bundle_attach = esp_crt_bundle_attach;
105  }
106 #endif
107 
108  this->client_ = esp_http_client_init(&client_config);
109 
110  if (this->client_ == nullptr) {
111  return ESP_FAIL;
112  }
113 
114  esp_err_t err = esp_http_client_open(this->client_, 0);
115 
116  if (err != ESP_OK) {
117  this->cleanup_connection_();
118  return err;
119  }
120 
121  int64_t header_length = esp_http_client_fetch_headers(this->client_);
122  if (header_length < 0) {
123  this->cleanup_connection_();
124  return ESP_FAIL;
125  }
126 
127  int status_code = esp_http_client_get_status_code(this->client_);
128 
129  if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
130  this->cleanup_connection_();
131  return ESP_FAIL;
132  }
133 
134  ssize_t redirect_count = 0;
135 
136  while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) {
137  err = esp_http_client_open(this->client_, 0);
138  if (err != ESP_OK) {
139  this->cleanup_connection_();
140  return ESP_FAIL;
141  }
142 
143  header_length = esp_http_client_fetch_headers(this->client_);
144  if (header_length < 0) {
145  this->cleanup_connection_();
146  return ESP_FAIL;
147  }
148 
149  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  ++redirect_count;
157  }
158 
159  if (this->audio_file_type_ == AudioFileType::NONE) {
160  // Failed to determine the file type from the header, fallback to using the url
161  char url[500];
162  err = esp_http_client_get_url(this->client_, url, 500);
163  if (err != ESP_OK) {
164  this->cleanup_connection_();
165  return err;
166  }
167 
168  std::string url_string = str_lower_case(url);
169 
170  if (str_endswith(url_string, ".wav")) {
171  file_type = AudioFileType::WAV;
172  }
173 #ifdef USE_AUDIO_MP3_SUPPORT
174  else if (str_endswith(url_string, ".mp3")) {
175  file_type = AudioFileType::MP3;
176  }
177 #endif
178 #ifdef USE_AUDIO_FLAC_SUPPORT
179  else if (str_endswith(url_string, ".flac")) {
180  file_type = AudioFileType::FLAC;
181  }
182 #endif
183  else {
184  file_type = AudioFileType::NONE;
185  this->cleanup_connection_();
186  return ESP_ERR_NOT_SUPPORTED;
187  }
188  } else {
189  file_type = this->audio_file_type_;
190  }
191 
192  this->no_data_read_count_ = 0;
193 
195  if (this->output_transfer_buffer_ == nullptr) {
196  return ESP_ERR_NO_MEM;
197  }
198 
199  return ESP_OK;
200 }
201 
203  if (this->client_ != nullptr) {
204  return this->http_read_();
205  } else if (this->current_audio_file_ != nullptr) {
206  return this->file_read_();
207  }
208 
210 }
211 
212 AudioFileType AudioReader::get_audio_type(const char *content_type) {
213 #ifdef USE_AUDIO_MP3_SUPPORT
214  if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
215  strcasecmp(content_type, "audio/mpeg") == 0) {
216  return AudioFileType::MP3;
217  }
218 #endif
219  if (strcasecmp(content_type, "audio/wav") == 0) {
220  return AudioFileType::WAV;
221  }
222 #ifdef USE_AUDIO_FLAC_SUPPORT
223  if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
224  return AudioFileType::FLAC;
225  }
226 #endif
227  return AudioFileType::NONE;
228 }
229 
230 esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
231  // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
232  AudioReader *this_reader = (AudioReader *) evt->user_data;
233 
234  switch (evt->event_id) {
235  case HTTP_EVENT_ON_HEADER:
236  if (strcasecmp(evt->header_key, "Content-Type") == 0) {
237  this_reader->audio_file_type_ = get_audio_type(evt->header_value);
238  }
239  break;
240  default:
241  break;
242  }
243  return ESP_OK;
244 }
245 
247  size_t remaining_bytes = this->current_audio_file_->length - (this->file_current_ - this->current_audio_file_->data);
248  if (remaining_bytes > 0) {
249  size_t bytes_written = this->file_ring_buffer_->write_without_replacement(this->file_current_, remaining_bytes,
250  pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
251  this->file_current_ += bytes_written;
252 
254  }
255 
257 }
258 
260  this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
261 
262  if (esp_http_client_is_complete_data_received(this->client_)) {
263  if (this->output_transfer_buffer_->available() == 0) {
264  this->cleanup_connection_();
266  }
267  } else {
268  size_t bytes_to_read = this->output_transfer_buffer_->free();
269  int received_len =
270  esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);
271 
272  if (received_len > 0) {
273  this->output_transfer_buffer_->increase_buffer_length(received_len);
274 
275  this->no_data_read_count_ = 0;
276  } else if (received_len < 0) {
277  // HTTP read error
278  this->cleanup_connection_();
280  } else {
281  if (bytes_to_read > 0) {
282  // Read timed out
283  ++this->no_data_read_count_;
284  if (this->no_data_read_count_ >= ERROR_COUNT_NO_DATA_READ_TIMEOUT) {
285  // Timed out with no data read too many times, so the http read has failed
286  this->cleanup_connection_();
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
std::unique_ptr< AudioSinkTransferBuffer > output_transfer_buffer_
Definition: audio_reader.h:70
static std::unique_ptr< AudioSinkTransferBuffer > create(size_t buffer_size)
Creates a new sink transfer buffer.
const uint8_t * data
Definition: audio.h:120
AudioFileType audio_file_type_
Definition: audio_reader.h:79
std::string str_lower_case(const std::string &str)
Convert the string to lower case.
Definition: helpers.cpp:291
AudioReaderState file_read_()
esp_err_t add_sink(const std::weak_ptr< RingBuffer > &output_ring_buffer)
Adds a sink ring buffer for audio data.
const uint8_t * file_current_
Definition: audio_reader.h:80
AudioFileType file_type
Definition: audio.h:122
bool str_endswith(const std::string &str, const std::string &end)
Check whether a string ends with a value.
Definition: helpers.cpp:268
AudioReaderState read()
Reads new file data from the source and sends to the ring buffer sink.
esp_err_t start(const std::string &uri, AudioFileType &file_type)
Starts reading an audio file from an http source.
static AudioFileType get_audio_type(const char *content_type)
Determines the audio file type from the http header&#39;s Content-Type key.
std::shared_ptr< RingBuffer > file_ring_buffer_
Definition: audio_reader.h:69
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
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_
Definition: audio_reader.h:76
AudioReaderState http_read_()
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26