ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
audio_http_media_source.cpp
Go to the documentation of this file.
2
3#ifdef USE_ESP32
4
5#include "esphome/core/log.h"
6
7#include <freertos/FreeRTOS.h>
8#include <freertos/task.h>
9
10#include <algorithm>
11
13
14ESPHOME_LOG_TAG(TAG, "audio_http_media_source");
15
16// Decoder task / buffer tuning. Kept here as constants so the header stays free of magic numbers.
17static constexpr size_t DEFAULT_TRANSFER_BUFFER_SIZE = 8 * 1024; // Staging buffer between HTTP reader and decoder
18static constexpr uint32_t HTTP_TIMEOUT_MS = 5000; // HTTP connect/read timeout
19static constexpr uint32_t AUDIO_WRITE_TIMEOUT_MS = 50; // Max blocking time per on_audio_write() call
20static constexpr uint32_t READER_WRITE_TIMEOUT_MS = 50; // Max blocking time when writing into the ring buffer
21static constexpr uint8_t READER_TASK_PRIORITY = 2;
22static constexpr uint8_t DECODER_TASK_PRIORITY = 2;
23static constexpr size_t READER_TASK_STACK_SIZE = 4096;
24static constexpr size_t DECODER_TASK_STACK_SIZE = 5120;
25static constexpr uint32_t PAUSE_POLL_DELAY_MS = 20;
26static constexpr const char *const HTTP_URI_PREFIX = "http://";
27static constexpr const char *const HTTPS_URI_PREFIX = "https://";
28
30 ESP_LOGCONFIG(TAG,
31 "Audio HTTP Media Source:\n"
32 " Buffer Size: %zu bytes\n"
33 " Persistent Ring Buffer: %s\n"
34 " Decoder Task Stack in PSRAM: %s",
35 this->buffer_size_, YESNO(this->persistent_ring_buffer_), YESNO(this->decoder_task_stack_in_psram_));
36#ifdef USE_AUDIO_HTTP_CA_CERTIFICATE
37 ESP_LOGCONFIG(TAG, " Custom CA Certificate: %s", YESNO(this->http_ca_certificate_ != nullptr));
38#endif
39}
40
42 this->disable_loop();
43
44 micro_decoder::DecoderConfig config;
45 config.ring_buffer_size = this->buffer_size_;
46 config.persistent_ring_buffer = this->persistent_ring_buffer_;
47 // Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring
48 // while the decoder is still draining it, instead of oscillating between empty and full.
49 config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2);
50 config.http_timeout_ms = HTTP_TIMEOUT_MS;
51 config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS;
52 config.reader_write_timeout_ms = READER_WRITE_TIMEOUT_MS;
53 config.reader_priority = READER_TASK_PRIORITY;
54 config.decoder_priority = DECODER_TASK_PRIORITY;
55 config.reader_stack_size = READER_TASK_STACK_SIZE;
56 config.decoder_stack_size = DECODER_TASK_STACK_SIZE;
57 config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_;
58#ifdef USE_AUDIO_HTTP_CA_CERTIFICATE
59 // micro-decoder verifies HTTPS against this PEM only, skipping the built-in certificate bundle.
60 if (this->http_ca_certificate_ != nullptr) {
61 config.http_ca_certificate = this->http_ca_certificate_;
62 }
63#endif
64
65 this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config);
66 if (this->decoder_ == nullptr) {
67 ESP_LOGE(TAG, "Failed to allocate decoder");
68 this->mark_failed();
69 return;
70 }
71 this->decoder_->set_listener(this); // We inherit from micro_decoder::DecoderListener
72}
73
74void AudioHTTPMediaSource::loop() { this->decoder_->loop(); }
75
76bool AudioHTTPMediaSource::can_handle(const std::string &uri) const {
77 return uri.starts_with(HTTP_URI_PREFIX) || uri.starts_with(HTTPS_URI_PREFIX);
78}
79
80// Called from the orchestrator's main loop, so no synchronization needed with loop()
81bool AudioHTTPMediaSource::play_uri(const std::string &uri) {
82 if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) {
83 return false;
84 }
85
86 // Check if source is already playing
88 ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
89 return false;
90 }
91
92 // Validate URI starts with "http://" or "https://"
93 if (!uri.starts_with(HTTP_URI_PREFIX) && !uri.starts_with(HTTPS_URI_PREFIX)) {
94 ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
95 return false;
96 }
97
98 if (this->decoder_->play_url(uri)) {
99 this->pause_.store(false, std::memory_order_relaxed);
100 this->enable_loop();
101 return true;
102 }
103
104 ESP_LOGE(TAG, "Failed to start playback of '%s'", uri.c_str());
105 return false;
106}
107
108// Called from the orchestrator's main loop, so no synchronization needed with loop()
110 switch (command) {
112 this->decoder_->stop();
113 break;
115 // Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state
116 // machine from getting stuck in PAUSED when no playback is active (which would block the
117 // next play_uri() call via its IDLE-state precondition).
119 break;
120 // PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily
121 // yields, which fills the ring buffer and applies back pressure that effectively pauses both
122 // the decoder and HTTP reader tasks.
124 this->pause_.store(true, std::memory_order_relaxed);
125 break;
127 // Only resume from PAUSED; don't fabricate a PLAYING state from IDLE/ERROR.
129 break;
131 this->pause_.store(false, std::memory_order_relaxed);
132 break;
133 default:
134 break;
135 }
136}
137
138// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for
139// being thread-safe with respect to its own audio writer.
140size_t AudioHTTPMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) {
141 if (this->pause_.load(std::memory_order_relaxed)) {
142 vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS));
143 return 0;
144 }
145 return this->write_output(data, length, timeout_ms, this->stream_info_);
146}
147
148// Called from the decoder task before the first on_audio_write().
149void AudioHTTPMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) {
150 this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate());
151}
152
153// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main
154// loop thread and it's safe to call set_state_() directly.
155void AudioHTTPMediaSource::on_state_change(micro_decoder::DecoderState state) {
156 switch (state) {
157 case micro_decoder::DecoderState::IDLE:
159 this->disable_loop();
160 break;
161 case micro_decoder::DecoderState::PLAYING:
163 break;
164 case micro_decoder::DecoderState::FAILED:
166 break;
167 default:
168 break;
169 }
170}
171
172} // namespace esphome::audio_http
173
174#endif // USE_ESP32
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
bool is_ready() const
void enable_loop()
Enable this component's loop.
Definition component.h:248
bool status_has_error() const
Definition component.h:282
void disable_loop()
Disable this component's loop.
size_t on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) override
bool can_handle(const std::string &uri) const override
bool play_uri(const std::string &uri) override
std::unique_ptr< micro_decoder::DecoderSource > decoder_
void handle_command(media_source::MediaSourceCommand command) override
void on_state_change(micro_decoder::DecoderState state) override
void on_stream_info(const micro_decoder::AudioStreamInfo &info) override
void set_state_(MediaSourceState state)
Update state and notify listener This is the only way to change state_, ensuring listener notificatio...
MediaSourceState get_state() const
Get current playback state.
size_t write_output(const uint8_t *data, size_t length, uint32_t timeout_ms, const audio::AudioStreamInfo &stream_info)
Write audio data to the listener.
bool has_listener() const
Check if a listener has been registered.
bool state
Definition fan.h:2
ESPHOME_LOG_TAG(TAG, "audio_http_media_source")
MediaSourceCommand
Commands that are sent from the orchestrator to a media source.
static void uint32_t
uint16_t length
Definition tt21100.cpp:0