ESPHome 2026.9.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
14static const char *const 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}
37
39 this->disable_loop();
40
41 micro_decoder::DecoderConfig config;
42 config.ring_buffer_size = this->buffer_size_;
43 config.persistent_ring_buffer = this->persistent_ring_buffer_;
44 // Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring
45 // while the decoder is still draining it, instead of oscillating between empty and full.
46 config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2);
47 config.http_timeout_ms = HTTP_TIMEOUT_MS;
48 config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS;
49 config.reader_write_timeout_ms = READER_WRITE_TIMEOUT_MS;
50 config.reader_priority = READER_TASK_PRIORITY;
51 config.decoder_priority = DECODER_TASK_PRIORITY;
52 config.reader_stack_size = READER_TASK_STACK_SIZE;
53 config.decoder_stack_size = DECODER_TASK_STACK_SIZE;
54 config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_;
55
56 this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config);
57 if (this->decoder_ == nullptr) {
58 ESP_LOGE(TAG, "Failed to allocate decoder");
59 this->mark_failed();
60 return;
61 }
62 this->decoder_->set_listener(this); // We inherit from micro_decoder::DecoderListener
63}
64
65void AudioHTTPMediaSource::loop() { this->decoder_->loop(); }
66
67bool AudioHTTPMediaSource::can_handle(const std::string &uri) const {
68 return uri.starts_with(HTTP_URI_PREFIX) || uri.starts_with(HTTPS_URI_PREFIX);
69}
70
71// Called from the orchestrator's main loop, so no synchronization needed with loop()
72bool AudioHTTPMediaSource::play_uri(const std::string &uri) {
73 if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) {
74 return false;
75 }
76
77 // Check if source is already playing
79 ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
80 return false;
81 }
82
83 // Validate URI starts with "http://" or "https://"
84 if (!uri.starts_with(HTTP_URI_PREFIX) && !uri.starts_with(HTTPS_URI_PREFIX)) {
85 ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
86 return false;
87 }
88
89 if (this->decoder_->play_url(uri)) {
90 this->pause_.store(false, std::memory_order_relaxed);
91 this->enable_loop();
92 return true;
93 }
94
95 ESP_LOGE(TAG, "Failed to start playback of '%s'", uri.c_str());
96 return false;
97}
98
99// Called from the orchestrator's main loop, so no synchronization needed with loop()
101 switch (command) {
103 this->decoder_->stop();
104 break;
106 // Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state
107 // machine from getting stuck in PAUSED when no playback is active (which would block the
108 // next play_uri() call via its IDLE-state precondition).
110 break;
111 // PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily
112 // yields, which fills the ring buffer and applies back pressure that effectively pauses both
113 // the decoder and HTTP reader tasks.
115 this->pause_.store(true, std::memory_order_relaxed);
116 break;
118 // Only resume from PAUSED; don't fabricate a PLAYING state from IDLE/ERROR.
120 break;
122 this->pause_.store(false, std::memory_order_relaxed);
123 break;
124 default:
125 break;
126 }
127}
128
129// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for
130// being thread-safe with respect to its own audio writer.
131size_t AudioHTTPMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) {
132 if (this->pause_.load(std::memory_order_relaxed)) {
133 vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS));
134 return 0;
135 }
136 return this->write_output(data, length, timeout_ms, this->stream_info_);
137}
138
139// Called from the decoder task before the first on_audio_write().
140void AudioHTTPMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) {
141 this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate());
142}
143
144// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main
145// loop thread and it's safe to call set_state_() directly.
146void AudioHTTPMediaSource::on_state_change(micro_decoder::DecoderState state) {
147 switch (state) {
148 case micro_decoder::DecoderState::IDLE:
150 this->disable_loop();
151 break;
152 case micro_decoder::DecoderState::PLAYING:
154 break;
155 case micro_decoder::DecoderState::FAILED:
157 break;
158 default:
159 break;
160 }
161}
162
163} // namespace esphome::audio_http
164
165#endif // USE_ESP32
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:272
bool is_ready() const
void enable_loop()
Enable this component's loop.
Definition component.h:246
bool status_has_error() const
Definition component.h:280
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
MediaSourceCommand
Commands that are sent from the orchestrator to a media source.
static void uint32_t
uint16_t length
Definition tt21100.cpp:0