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