ESPHome 2026.3.2
Loading...
Searching...
No Matches
speaker_media_player.cpp
Go to the documentation of this file.
2
3#ifdef USE_ESP32
4
5#include "esphome/core/log.h"
6
8#ifdef USE_OTA
10#endif
11
12namespace esphome {
13namespace speaker {
14
15// Framework:
16// - Media player that can handle two streams: one for media and one for announcements
17// - Each stream has an individual speaker component for output
18// - Each stream is handled by an ``AudioPipeline`` object with two parts/tasks
19// - ``AudioReader`` handles reading from an HTTP source or from a PROGMEM flash set at compile time
20// - ``AudioDecoder`` handles decoding the audio file. All formats are limited to two channels and 16 bits per sample
21// - FLAC
22// - MP3 (based on the libhelix decoder)
23// - WAV
24// - Each task runs until it is done processing the file or it receives a stop command
25// - Inter-task communication uses a FreeRTOS Event Group
26// - The ``AudioPipeline`` sets up a ring buffer between the reader and decoder tasks. The decoder task outputs audio
27// directly to a speaker component.
28// - The pipelines internal state needs to be processed by regularly calling ``process_state``.
29// - Generic media player commands are received by the ``control`` function. The commands are added to the
30// ``media_control_command_queue_`` to be processed in the component's loop
31// - Local file play back is initiatied with ``play_file`` and adds it to the ``media_control_command_queue_``
32// - Starting a stream intializes the appropriate pipeline or stops it if it is already running
33// - Volume and mute commands are achieved by the ``mute``, ``unmute``, ``set_volume`` functions.
34// - Volume commands are ignored if the media control queue is full to avoid crashing with rapid volume
35// increases/decreases.
36// - These functions all send the appropriate information to the speakers to implement.
37// - Pausing is implemented in the decoder task and is also sent directly to the media speaker component to decrease
38// latency.
39// - The components main loop performs housekeeping:
40// - It reads the media control queue and processes it directly
41// - It determines the overall state of the media player by considering the state of each pipeline
42// - announcement playback takes highest priority
43// - Handles playlists and repeating by starting the appropriate file when a previous file is finished
44// - Logging only happens in the main loop task to reduce task stack memory usage.
45
46static const uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20;
47
48static const UBaseType_t MEDIA_PIPELINE_TASK_PRIORITY = 1;
49static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1;
50
51static const char *const TAG = "speaker_media_player";
52
54#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
56#else
58#endif
59
60 this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand));
61
63
64 VolumeRestoreState volume_restore_state;
65 if (this->pref_.load(&volume_restore_state)) {
66 this->set_volume_(volume_restore_state.volume);
67 this->set_mute_state_(volume_restore_state.is_muted);
68 } else {
69 this->set_volume_(this->volume_initial_);
70 this->set_mute_state_(false);
71 }
72
73#ifdef USE_OTA_STATE_LISTENER
75#endif
76
78 make_unique<AudioPipeline>(this->announcement_speaker_, this->buffer_size_, this->task_stack_in_psram_, "ann",
79 ANNOUNCEMENT_PIPELINE_TASK_PRIORITY);
80
81 if (this->announcement_pipeline_ == nullptr) {
82 ESP_LOGE(TAG, "Failed to create announcement pipeline");
83 this->mark_failed();
84 }
85
86 if (!this->single_pipeline_()) {
87 this->media_pipeline_ = make_unique<AudioPipeline>(this->media_speaker_, this->buffer_size_,
88 this->task_stack_in_psram_, "med", MEDIA_PIPELINE_TASK_PRIORITY);
89
90 if (this->media_pipeline_ == nullptr) {
91 ESP_LOGE(TAG, "Failed to create media pipeline");
92 this->mark_failed();
93 }
94 }
95
96 ESP_LOGI(TAG, "Set up speaker media player");
97}
98
100 switch (pipeline_type) {
102 this->announcement_playlist_delay_ms_ = delay_ms;
103 break;
105 this->media_playlist_delay_ms_ = delay_ms;
106 break;
107 }
108}
109
111 this->media_pipeline_->stop();
112 this->unpause_media_remaining_ = 3;
113 this->set_interval("unpause_med", 50, [this]() {
115 this->cancel_interval("unpause_med");
116 this->media_pipeline_->set_pause_state(false);
117 this->is_paused_ = false;
118 } else if (--this->unpause_media_remaining_ == 0) {
119 this->cancel_interval("unpause_med");
120 }
121 });
122}
123
125 if (!this->is_ready()) {
126 return;
127 }
128
129 MediaCallCommand media_command;
130
131 if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) {
132 bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
133
134 if (media_command.url.has_value() || media_command.file.has_value()) {
135#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
139 }
140#endif
141 PlaylistItem playlist_item;
142 if (media_command.url.has_value()) {
143 playlist_item.url = *media_command.url.value();
144 delete media_command.url.value();
145 }
146 if (media_command.file.has_value()) {
147 playlist_item.file = media_command.file;
148 }
149
150 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
151 if (!enqueue) {
152 // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
153 this->cancel_timeout("next_ann");
154 this->announcement_playlist_.clear();
155 if (media_command.file.has_value()) {
156 this->announcement_pipeline_->start_file(playlist_item.file.value());
157 } else if (media_command.url.has_value()) {
158 this->announcement_pipeline_->start_url(playlist_item.url.value());
159 }
160 this->announcement_pipeline_->set_pause_state(false);
161 }
162 this->announcement_playlist_.push_back(playlist_item);
163 } else {
164 if (!enqueue) {
165 // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
166 this->cancel_timeout("next_media");
167 this->media_playlist_.clear();
168 if (this->is_paused_) {
169 // If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a
170 // short segment of the paused file before starting the new one.
172 } else {
173 // Not paused, just directly start the file
174 if (media_command.file.has_value()) {
175 this->media_pipeline_->start_file(playlist_item.file.value());
176 } else if (media_command.url.has_value()) {
177 this->media_pipeline_->start_url(playlist_item.url.value());
178 }
179 this->media_pipeline_->set_pause_state(false);
180 this->is_paused_ = false;
181 }
182 }
183 this->media_playlist_.push_back(playlist_item);
184 }
185
186 return; // Don't process the new file play command further
187 }
188
189 if (media_command.volume.has_value()) {
190 this->set_volume_(media_command.volume.value());
191 this->publish_state();
192 }
193
194 if (media_command.command.has_value()) {
195 switch (media_command.command.value()) {
197#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
201 }
202#endif
203 if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) {
204 this->media_pipeline_->set_pause_state(false);
205 }
206 this->is_paused_ = false;
207 break;
209 if ((this->media_pipeline_ != nullptr) && (!this->is_paused_)) {
210 this->media_pipeline_->set_pause_state(true);
211 }
212 this->is_paused_ = true;
213 break;
214#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
218 this->publish_state();
219 }
220 break;
222 this->is_turn_off_ = true;
223 // Intentional Fall-through
224#endif
226 // Pipelines do not stop immediately after calling the stop command, so confirm its stopped before unpausing.
227 // This avoids an audible short segment playing after receiving the stop command in a paused state.
228#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
229 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value()) ||
230 (this->is_turn_off_ && this->announcement_pipeline_state_ != AudioPipelineState::STOPPED)) {
231#else
232 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
233#endif
234 if (this->announcement_pipeline_ != nullptr) {
235 this->cancel_timeout("next_ann");
236 this->announcement_playlist_.clear();
237 this->announcement_pipeline_->stop();
239 this->set_interval("unpause_ann", 50, [this]() {
241 this->cancel_interval("unpause_ann");
242 this->announcement_pipeline_->set_pause_state(false);
243 } else if (--this->unpause_announcement_remaining_ == 0) {
244 this->cancel_interval("unpause_ann");
245 }
246 });
247 }
248 } else {
249 if (this->media_pipeline_ != nullptr) {
250 this->cancel_timeout("next_media");
251 this->media_playlist_.clear();
253 }
254 }
255
256 break;
258 if (this->media_pipeline_ != nullptr) {
259 if (this->is_paused_) {
260 this->media_pipeline_->set_pause_state(false);
261 this->is_paused_ = false;
262 } else {
263 this->media_pipeline_->set_pause_state(true);
264 this->is_paused_ = true;
265 }
266 }
267 break;
269 this->set_mute_state_(true);
270
271 this->publish_state();
272 break;
273 }
275 this->set_mute_state_(false);
276 this->publish_state();
277 break;
279 this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_));
280 this->publish_state();
281 break;
283 this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_));
284 this->publish_state();
285 break;
287 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
288 this->announcement_repeat_one_ = true;
289 } else {
290 this->media_repeat_one_ = true;
291 }
292 break;
294 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
295 this->announcement_repeat_one_ = false;
296 } else {
297 this->media_repeat_one_ = false;
298 }
299 break;
301 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
302 if (this->announcement_playlist_.empty()) {
303 this->announcement_playlist_.resize(1);
304 }
305 } else {
306 if (this->media_playlist_.empty()) {
307 this->media_playlist_.resize(1);
308 }
309 }
310 break;
311 default:
312 break;
313 }
314 }
315 }
316}
317
318#ifdef USE_OTA_STATE_LISTENER
319void SpeakerMediaPlayer::on_ota_global_state(ota::OTAState state, float progress, uint8_t error,
320 ota::OTAComponent *comp) {
321 if (state == ota::OTA_STARTED) {
322 if (this->media_pipeline_ != nullptr) {
323 this->media_pipeline_->suspend_tasks();
324 }
325 if (this->announcement_pipeline_ != nullptr) {
326 this->announcement_pipeline_->suspend_tasks();
327 }
328 } else if (state == ota::OTA_ERROR) {
329 if (this->media_pipeline_ != nullptr) {
330 this->media_pipeline_->resume_tasks();
331 }
332 if (this->announcement_pipeline_ != nullptr) {
333 this->announcement_pipeline_->resume_tasks();
334 }
335 }
336}
337#endif
338
340 this->watch_media_commands_();
341
342 // Determine state of the media player
343 media_player::MediaPlayerState old_state = this->state;
344
345 AudioPipelineState old_media_pipeline_state = this->media_pipeline_state_;
346 if (this->media_pipeline_ != nullptr) {
347 this->media_pipeline_state_ = this->media_pipeline_->process_state();
348 }
349
351 ESP_LOGE(TAG, "The media pipeline's file reader encountered an error.");
353 ESP_LOGE(TAG, "The media pipeline's audio decoder encountered an error.");
354 }
355
356 AudioPipelineState old_announcement_pipeline_state = this->announcement_pipeline_state_;
357 if (this->announcement_pipeline_ != nullptr) {
358 this->announcement_pipeline_state_ = this->announcement_pipeline_->process_state();
359 }
360
362 ESP_LOGE(TAG, "The announcement pipeline's file reader encountered an error.");
364 ESP_LOGE(TAG, "The announcement pipeline's audio decoder encountered an error.");
365 }
366
369 } else {
370 if (!this->announcement_playlist_.empty()) {
371 uint32_t timeout_ms = 0;
372 if (old_announcement_pipeline_state == AudioPipelineState::PLAYING) {
373 // Finished the current announcement file
374 if (!this->announcement_repeat_one_) {
375 // Pop item off the playlist if repeat is disabled
376 this->announcement_playlist_.pop_front();
377 }
378 // Only delay starting playback if moving on the next playlist item or repeating the current item
379 timeout_ms = this->announcement_playlist_delay_ms_;
380 }
381
382 if (!this->announcement_playlist_.empty()) {
383 // Start the next announcement file
384 PlaylistItem playlist_item = this->announcement_playlist_.front();
385 if (playlist_item.url.has_value()) {
386 this->announcement_pipeline_->start_url(playlist_item.url.value());
387 } else if (playlist_item.file.has_value()) {
388 this->announcement_pipeline_->start_file(playlist_item.file.value());
389 }
390
391 if (timeout_ms > 0) {
392 // Pause pipeline internally to facilitate the delay between items
393 this->announcement_pipeline_->set_pause_state(true);
394 // Internally unpause the pipeline after the delay between playlist items. Announcements do not follow the
395 // media player's pause state.
396 this->set_timeout("next_ann", timeout_ms, [this]() { this->announcement_pipeline_->set_pause_state(false); });
397 }
398 }
399 } else {
400 if (this->is_paused_) {
401#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
402 if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) {
404 }
405#else
407#endif
411 if (!media_playlist_.empty()) {
412 uint32_t timeout_ms = 0;
413 if (old_media_pipeline_state == AudioPipelineState::PLAYING) {
414 // Finished the current media file
415 if (!this->media_repeat_one_) {
416 // Pop item off the playlist if repeat is disabled
417 this->media_playlist_.pop_front();
418 }
419 // Only delay starting playback if moving on the next playlist item or repeating the current item
420 timeout_ms = this->media_playlist_delay_ms_;
421 }
422 if (!this->media_playlist_.empty()) {
423 PlaylistItem playlist_item = this->media_playlist_.front();
424 if (playlist_item.url.has_value()) {
425 this->media_pipeline_->start_url(playlist_item.url.value());
426 } else if (playlist_item.file.has_value()) {
427 this->media_pipeline_->start_file(playlist_item.file.value());
428 }
429
430 if (timeout_ms > 0) {
431 // Pause pipeline internally to facilitate the delay between items
432 this->media_pipeline_->set_pause_state(true);
433 // Internally unpause the pipeline after the delay between playlist items, if the media player state is
434 // not paused.
435 this->set_timeout("next_media", timeout_ms,
436 [this]() { this->media_pipeline_->set_pause_state(this->is_paused_); });
437 }
438 }
439 } else {
440#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
441 if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) {
443 }
444#else
446#endif
447 }
448 }
449 }
450 }
451
452 if (this->state != old_state) {
453 this->publish_state();
454 ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
455 }
456#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
457 if (this->is_turn_off_ && (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED ||
459 this->is_turn_off_ = false;
460 if (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED) {
462 this->publish_state();
463 ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
464 }
466 this->publish_state();
467 ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
468 }
469#endif
470}
471
472void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) {
473 if (!this->is_ready()) {
474 // Ignore any commands sent before the media player is setup
475 return;
476 }
477
478 MediaCallCommand media_command;
479
480 media_command.file = media_file;
481 if (this->single_pipeline_() || announcement) {
482 media_command.announce = true;
483 } else {
484 media_command.announce = false;
485 }
486 media_command.enqueue = enqueue;
487 xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
488}
489
490void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) {
491 if (!this->is_ready()) {
492 // Ignore any commands sent before the media player is setup
493 return;
494 }
495
496 MediaCallCommand media_command;
497
498 auto ann = call.get_announcement();
499 if (this->single_pipeline_() || (ann.has_value() && *ann)) {
500 media_command.announce = true;
501 } else {
502 media_command.announce = false;
503 }
504
505 auto media_url = call.get_media_url();
506 if (media_url.has_value()) {
507 media_command.url =
508 new std::string(*media_url); // Must be manually deleted after receiving media_command from a queue
509
510 auto cmd = call.get_command();
511 if (cmd.has_value()) {
513 media_command.enqueue = true;
514 }
515 }
516
517 xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
518 return;
519 }
520
521 auto vol = call.get_volume();
522 if (vol.has_value()) {
523 media_command.volume = vol;
524 // Wait 0 ticks for queue to be free, volume sets aren't that important!
525 xQueueSend(this->media_control_command_queue_, &media_command, 0);
526 return;
527 }
528
529 auto cmd = call.get_command();
530 if (cmd.has_value()) {
531 media_command.command = cmd;
532 TickType_t ticks_to_wait = portMAX_DELAY;
535 ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important!
536 }
537 xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait);
538 return;
539 }
540}
541
542media_player::MediaPlayerTraits SpeakerMediaPlayer::get_traits() {
543 auto traits = media_player::MediaPlayerTraits();
544 if (!this->single_pipeline_()) {
545 traits.set_supports_pause(true);
546 }
547#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
548 traits.set_supports_turn_off_on(true);
549#endif
550
551 if (this->announcement_format_.has_value()) {
552 traits.get_supported_formats().push_back(this->announcement_format_.value());
553 }
554 if (this->media_format_.has_value()) {
555 traits.get_supported_formats().push_back(this->media_format_.value());
556 } else if (this->single_pipeline_() && this->announcement_format_.has_value()) {
557 // Only one pipeline is defined, so use the announcement format (if configured) for the default purpose
558 media_player::MediaPlayerSupportedFormat media_format = this->announcement_format_.value();
560 traits.get_supported_formats().push_back(media_format);
561 }
562
563 return traits;
564};
565
567 VolumeRestoreState volume_restore_state;
568 volume_restore_state.volume = this->volume;
569 volume_restore_state.is_muted = this->is_muted_;
570 this->pref_.save(&volume_restore_state);
571}
572
573void SpeakerMediaPlayer::set_mute_state_(bool mute_state) {
574 if (this->media_speaker_ != nullptr) {
575 this->media_speaker_->set_mute_state(mute_state);
576 }
577 if (this->announcement_speaker_ != nullptr) {
578 this->announcement_speaker_->set_mute_state(mute_state);
579 }
580
581 bool old_mute_state = this->is_muted_;
582 this->is_muted_ = mute_state;
583
585
586 if (old_mute_state != mute_state) {
587 if (mute_state) {
588 this->defer([this]() { this->mute_trigger_.trigger(); });
589 } else {
590 this->defer([this]() { this->unmute_trigger_.trigger(); });
591 }
592 }
593}
594
595void SpeakerMediaPlayer::set_volume_(float volume, bool publish) {
596 // Remap the volume to fit with in the configured limits
597 float bounded_volume = remap<float, float>(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_);
598
599 if (this->media_speaker_ != nullptr) {
600 this->media_speaker_->set_volume(bounded_volume);
601 }
602
603 if (this->announcement_speaker_ != nullptr) {
604 this->announcement_speaker_->set_volume(bounded_volume);
605 }
606
607 if (publish) {
608 this->volume = volume;
610 }
611
612 // Turn on the mute state if the volume is effectively zero, off otherwise
613 if (volume < 0.001) {
614 this->set_mute_state_(true);
615 } else {
616 this->set_mute_state_(false);
617 }
618
619 this->defer([this, volume]() { this->volume_trigger_.trigger(volume); });
620}
621
622} // namespace speaker
623} // namespace esphome
624
625#endif
void mark_failed()
Mark this component as failed.
ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") void defer(const std voi defer)(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call.
Definition component.h:501
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:451
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(const std voi set_interval)(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a unique name.
Definition component.h:358
bool is_ready() const
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(const std boo cancel_timeout)(const char *name)
Cancel a timeout function.
Definition component.h:473
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_interval(const std boo cancel_interval)(const char *name)
Cancel an interval function.
Definition component.h:380
bool save(const T *src)
Definition preferences.h:21
ESPPreferenceObject make_entity_preference(uint32_t version=0)
Create a preference object for storing this entity's state/settings.
void trigger(const Ts &...x)
Inform the parent automation that the event has triggered.
Definition automation.h:325
void add_global_state_listener(OTAGlobalStateListener *listener)
virtual void set_volume(float volume)
Definition speaker.h:71
virtual void set_mute_state(bool mute_state)
Definition speaker.h:81
optional< media_player::MediaPlayerSupportedFormat > announcement_format_
void save_volume_restore_state_()
Saves the current volume and mute state to the flash for restoration.
std::deque< PlaylistItem > announcement_playlist_
void set_volume_(float volume, bool publish=true)
Updates this->volume and saves volume/mute state to flash for restortation if publish is true.
void play_file(audio::AudioFile *media_file, bool announcement, bool enqueue)
void set_mute_state_(bool mute_state)
Sets the mute state.
std::deque< PlaylistItem > media_playlist_
std::unique_ptr< AudioPipeline > media_pipeline_
void stop_and_unpause_media_()
Stops the media pipeline and polls until stopped to unpause it, avoiding an audible glitch.
optional< media_player::MediaPlayerSupportedFormat > media_format_
void control(const media_player::MediaPlayerCall &call) override
void set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms)
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override
std::unique_ptr< AudioPipeline > announcement_pipeline_
media_player::MediaPlayerTraits get_traits() override
bool single_pipeline_()
Returns true if the media player has only the announcement pipeline defined, false if both the announ...
const char * media_player_state_to_string(MediaPlayerState state)
OTAGlobalCallback * get_global_ota_callback()
const char *const TAG
Definition spi.cpp:7
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
T remap(U value, U min, U max, T min_out, T max_out)
Remap value from the range (min, max) to (min_out, max_out).
Definition helpers.h:605
static void uint32_t