ESPHome 2026.1.0-dev
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
55
56 this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand));
57
59
60 VolumeRestoreState volume_restore_state;
61 if (this->pref_.load(&volume_restore_state)) {
62 this->set_volume_(volume_restore_state.volume);
63 this->set_mute_state_(volume_restore_state.is_muted);
64 } else {
65 this->set_volume_(this->volume_initial_);
66 this->set_mute_state_(false);
67 }
68
69#ifdef USE_OTA_STATE_LISTENER
71#endif
72
74 make_unique<AudioPipeline>(this->announcement_speaker_, this->buffer_size_, this->task_stack_in_psram_, "ann",
75 ANNOUNCEMENT_PIPELINE_TASK_PRIORITY);
76
77 if (this->announcement_pipeline_ == nullptr) {
78 ESP_LOGE(TAG, "Failed to create announcement pipeline");
79 this->mark_failed();
80 }
81
82 if (!this->single_pipeline_()) {
83 this->media_pipeline_ = make_unique<AudioPipeline>(this->media_speaker_, this->buffer_size_,
84 this->task_stack_in_psram_, "med", MEDIA_PIPELINE_TASK_PRIORITY);
85
86 if (this->media_pipeline_ == nullptr) {
87 ESP_LOGE(TAG, "Failed to create media pipeline");
88 this->mark_failed();
89 }
90 }
91
92 ESP_LOGI(TAG, "Set up speaker media player");
93}
94
95void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms) {
96 switch (pipeline_type) {
98 this->announcement_playlist_delay_ms_ = delay_ms;
99 break;
101 this->media_playlist_delay_ms_ = delay_ms;
102 break;
103 }
104}
105
107 if (!this->is_ready()) {
108 return;
109 }
110
111 MediaCallCommand media_command;
112
113 if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) {
114 bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
115
116 if (media_command.url.has_value() || media_command.file.has_value()) {
117 PlaylistItem playlist_item;
118 if (media_command.url.has_value()) {
119 playlist_item.url = *media_command.url.value();
120 delete media_command.url.value();
121 }
122 if (media_command.file.has_value()) {
123 playlist_item.file = media_command.file.value();
124 }
125
126 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
127 if (!enqueue) {
128 // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
129 this->cancel_timeout("next_ann");
130 this->announcement_playlist_.clear();
131 if (media_command.file.has_value()) {
132 this->announcement_pipeline_->start_file(playlist_item.file.value());
133 } else if (media_command.url.has_value()) {
134 this->announcement_pipeline_->start_url(playlist_item.url.value());
135 }
136 this->announcement_pipeline_->set_pause_state(false);
137 }
138 this->announcement_playlist_.push_back(playlist_item);
139 } else {
140 if (!enqueue) {
141 // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
142 this->cancel_timeout("next_media");
143 this->media_playlist_.clear();
144 if (this->is_paused_) {
145 // If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a
146 // short segment of the paused file before starting the new one.
147 this->media_pipeline_->stop();
148 this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
150 this->media_pipeline_->set_pause_state(false);
151 this->is_paused_ = false;
152 return RetryResult::DONE;
153 }
154 return RetryResult::RETRY;
155 });
156 } else {
157 // Not paused, just directly start the file
158 if (media_command.file.has_value()) {
159 this->media_pipeline_->start_file(playlist_item.file.value());
160 } else if (media_command.url.has_value()) {
161 this->media_pipeline_->start_url(playlist_item.url.value());
162 }
163 this->media_pipeline_->set_pause_state(false);
164 this->is_paused_ = false;
165 }
166 }
167 this->media_playlist_.push_back(playlist_item);
168 }
169
170 return; // Don't process the new file play command further
171 }
172
173 if (media_command.volume.has_value()) {
174 this->set_volume_(media_command.volume.value());
175 this->publish_state();
176 }
177
178 if (media_command.command.has_value()) {
179 switch (media_command.command.value()) {
181 if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) {
182 this->media_pipeline_->set_pause_state(false);
183 }
184 this->is_paused_ = false;
185 break;
187 if ((this->media_pipeline_ != nullptr) && (!this->is_paused_)) {
188 this->media_pipeline_->set_pause_state(true);
189 }
190 this->is_paused_ = true;
191 break;
193 // Pipelines do not stop immediately after calling the stop command, so confirm its stopped before unpausing.
194 // This avoids an audible short segment playing after receiving the stop command in a paused state.
195 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
196 if (this->announcement_pipeline_ != nullptr) {
197 this->cancel_timeout("next_ann");
198 this->announcement_playlist_.clear();
199 this->announcement_pipeline_->stop();
200 this->set_retry("unpause_ann", 50, 3, [this](const uint8_t remaining_attempts) {
202 this->announcement_pipeline_->set_pause_state(false);
203 return RetryResult::DONE;
204 }
205 return RetryResult::RETRY;
206 });
207 }
208 } else {
209 if (this->media_pipeline_ != nullptr) {
210 this->cancel_timeout("next_media");
211 this->media_playlist_.clear();
212 this->media_pipeline_->stop();
213 this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
215 this->media_pipeline_->set_pause_state(false);
216 this->is_paused_ = false;
217 return RetryResult::DONE;
218 }
219 return RetryResult::RETRY;
220 });
221 }
222 }
223
224 break;
226 if (this->media_pipeline_ != nullptr) {
227 if (this->is_paused_) {
228 this->media_pipeline_->set_pause_state(false);
229 this->is_paused_ = false;
230 } else {
231 this->media_pipeline_->set_pause_state(true);
232 this->is_paused_ = true;
233 }
234 }
235 break;
237 this->set_mute_state_(true);
238
239 this->publish_state();
240 break;
241 }
243 this->set_mute_state_(false);
244 this->publish_state();
245 break;
247 this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_));
248 this->publish_state();
249 break;
251 this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_));
252 this->publish_state();
253 break;
255 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
256 this->announcement_repeat_one_ = true;
257 } else {
258 this->media_repeat_one_ = true;
259 }
260 break;
262 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
263 this->announcement_repeat_one_ = false;
264 } else {
265 this->media_repeat_one_ = false;
266 }
267 break;
269 if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
270 if (this->announcement_playlist_.empty()) {
271 this->announcement_playlist_.resize(1);
272 }
273 } else {
274 if (this->media_playlist_.empty()) {
275 this->media_playlist_.resize(1);
276 }
277 }
278 break;
279 default:
280 break;
281 }
282 }
283 }
284}
285
286#ifdef USE_OTA_STATE_LISTENER
288 ota::OTAComponent *comp) {
289 if (state == ota::OTA_STARTED) {
290 if (this->media_pipeline_ != nullptr) {
291 this->media_pipeline_->suspend_tasks();
292 }
293 if (this->announcement_pipeline_ != nullptr) {
294 this->announcement_pipeline_->suspend_tasks();
295 }
296 } else if (state == ota::OTA_ERROR) {
297 if (this->media_pipeline_ != nullptr) {
298 this->media_pipeline_->resume_tasks();
299 }
300 if (this->announcement_pipeline_ != nullptr) {
301 this->announcement_pipeline_->resume_tasks();
302 }
303 }
304}
305#endif
306
308 this->watch_media_commands_();
309
310 // Determine state of the media player
311 media_player::MediaPlayerState old_state = this->state;
312
313 AudioPipelineState old_media_pipeline_state = this->media_pipeline_state_;
314 if (this->media_pipeline_ != nullptr) {
315 this->media_pipeline_state_ = this->media_pipeline_->process_state();
316 }
317
319 ESP_LOGE(TAG, "The media pipeline's file reader encountered an error.");
321 ESP_LOGE(TAG, "The media pipeline's audio decoder encountered an error.");
322 }
323
324 AudioPipelineState old_announcement_pipeline_state = this->announcement_pipeline_state_;
325 if (this->announcement_pipeline_ != nullptr) {
326 this->announcement_pipeline_state_ = this->announcement_pipeline_->process_state();
327 }
328
330 ESP_LOGE(TAG, "The announcement pipeline's file reader encountered an error.");
332 ESP_LOGE(TAG, "The announcement pipeline's audio decoder encountered an error.");
333 }
334
337 } else {
338 if (!this->announcement_playlist_.empty()) {
339 uint32_t timeout_ms = 0;
340 if (old_announcement_pipeline_state == AudioPipelineState::PLAYING) {
341 // Finished the current announcement file
342 if (!this->announcement_repeat_one_) {
343 // Pop item off the playlist if repeat is disabled
344 this->announcement_playlist_.pop_front();
345 }
346 // Only delay starting playback if moving on the next playlist item or repeating the current item
347 timeout_ms = this->announcement_playlist_delay_ms_;
348 }
349
350 if (!this->announcement_playlist_.empty()) {
351 // Start the next announcement file
352 PlaylistItem playlist_item = this->announcement_playlist_.front();
353 if (playlist_item.url.has_value()) {
354 this->announcement_pipeline_->start_url(playlist_item.url.value());
355 } else if (playlist_item.file.has_value()) {
356 this->announcement_pipeline_->start_file(playlist_item.file.value());
357 }
358
359 if (timeout_ms > 0) {
360 // Pause pipeline internally to facilitate the delay between items
361 this->announcement_pipeline_->set_pause_state(true);
362 // Internally unpause the pipeline after the delay between playlist items. Announcements do not follow the
363 // media player's pause state.
364 this->set_timeout("next_ann", timeout_ms, [this]() { this->announcement_pipeline_->set_pause_state(false); });
365 }
366 }
367 } else {
368 if (this->is_paused_) {
373 if (!media_playlist_.empty()) {
374 uint32_t timeout_ms = 0;
375 if (old_media_pipeline_state == AudioPipelineState::PLAYING) {
376 // Finished the current media file
377 if (!this->media_repeat_one_) {
378 // Pop item off the playlist if repeat is disabled
379 this->media_playlist_.pop_front();
380 }
381 // Only delay starting playback if moving on the next playlist item or repeating the current item
382 timeout_ms = this->announcement_playlist_delay_ms_;
383 }
384 if (!this->media_playlist_.empty()) {
385 PlaylistItem playlist_item = this->media_playlist_.front();
386 if (playlist_item.url.has_value()) {
387 this->media_pipeline_->start_url(playlist_item.url.value());
388 } else if (playlist_item.file.has_value()) {
389 this->media_pipeline_->start_file(playlist_item.file.value());
390 }
391
392 if (timeout_ms > 0) {
393 // Pause pipeline internally to facilitate the delay between items
394 this->media_pipeline_->set_pause_state(true);
395 // Internally unpause the pipeline after the delay between playlist items, if the media player state is
396 // not paused.
397 this->set_timeout("next_media", timeout_ms,
398 [this]() { this->media_pipeline_->set_pause_state(this->is_paused_); });
399 }
400 }
401 } else {
403 }
404 }
405 }
406 }
407
408 if (this->state != old_state) {
409 this->publish_state();
410 ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
411 }
412}
413
414void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) {
415 if (!this->is_ready()) {
416 // Ignore any commands sent before the media player is setup
417 return;
418 }
419
420 MediaCallCommand media_command;
421
422 media_command.file = media_file;
423 if (this->single_pipeline_() || announcement) {
424 media_command.announce = true;
425 } else {
426 media_command.announce = false;
427 }
428 media_command.enqueue = enqueue;
429 xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
430}
431
433 if (!this->is_ready()) {
434 // Ignore any commands sent before the media player is setup
435 return;
436 }
437
438 MediaCallCommand media_command;
439
440 if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) {
441 media_command.announce = true;
442 } else {
443 media_command.announce = false;
444 }
445
446 if (call.get_media_url().has_value()) {
447 media_command.url = new std::string(
448 call.get_media_url().value()); // Must be manually deleted after receiving media_command from a queue
449
450 if (call.get_command().has_value()) {
451 if (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE) {
452 media_command.enqueue = true;
453 }
454 }
455
456 xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
457 return;
458 }
459
460 if (call.get_volume().has_value()) {
461 media_command.volume = call.get_volume().value();
462 // Wait 0 ticks for queue to be free, volume sets aren't that important!
463 xQueueSend(this->media_control_command_queue_, &media_command, 0);
464 return;
465 }
466
467 if (call.get_command().has_value()) {
468 media_command.command = call.get_command().value();
469 TickType_t ticks_to_wait = portMAX_DELAY;
470 if ((call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) ||
471 (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) {
472 ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important!
473 }
474 xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait);
475 return;
476 }
477}
478
480 auto traits = media_player::MediaPlayerTraits();
481 if (!this->single_pipeline_()) {
482 traits.set_supports_pause(true);
483 }
484
485 if (this->announcement_format_.has_value()) {
486 traits.get_supported_formats().push_back(this->announcement_format_.value());
487 }
488 if (this->media_format_.has_value()) {
489 traits.get_supported_formats().push_back(this->media_format_.value());
490 } else if (this->single_pipeline_() && this->announcement_format_.has_value()) {
491 // Only one pipeline is defined, so use the announcement format (if configured) for the default purpose
494 traits.get_supported_formats().push_back(media_format);
495 }
496
497 return traits;
498};
499
501 VolumeRestoreState volume_restore_state;
502 volume_restore_state.volume = this->volume;
503 volume_restore_state.is_muted = this->is_muted_;
504 this->pref_.save(&volume_restore_state);
505}
506
508 if (this->media_speaker_ != nullptr) {
509 this->media_speaker_->set_mute_state(mute_state);
510 }
511 if (this->announcement_speaker_ != nullptr) {
512 this->announcement_speaker_->set_mute_state(mute_state);
513 }
514
515 bool old_mute_state = this->is_muted_;
516 this->is_muted_ = mute_state;
517
519
520 if (old_mute_state != mute_state) {
521 if (mute_state) {
522 this->defer([this]() { this->mute_trigger_->trigger(); });
523 } else {
524 this->defer([this]() { this->unmute_trigger_->trigger(); });
525 }
526 }
527}
528
529void SpeakerMediaPlayer::set_volume_(float volume, bool publish) {
530 // Remap the volume to fit with in the configured limits
531 float bounded_volume = remap<float, float>(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_);
532
533 if (this->media_speaker_ != nullptr) {
534 this->media_speaker_->set_volume(bounded_volume);
535 }
536
537 if (this->announcement_speaker_ != nullptr) {
538 this->announcement_speaker_->set_volume(bounded_volume);
539 }
540
541 if (publish) {
542 this->volume = volume;
544 }
545
546 // Turn on the mute state if the volume is effectively zero, off otherwise
547 if (volume < 0.001) {
548 this->set_mute_state_(true);
549 } else {
550 this->set_mute_state_(false);
551 }
552
553 this->defer([this, volume]() { this->volume_trigger_->trigger(volume); });
554}
555
556} // namespace speaker
557} // namespace esphome
558
559#endif
virtual void mark_failed()
Mark this component as failed.
bool cancel_timeout(const std::string &name)
Cancel a timeout function.
bool is_ready() const
void defer(const std::string &name, std::function< void()> &&f)
Defer a callback to the next loop() call.
void set_timeout(const std::string &name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function< RetryResult(uint8_t)> &&f, float backoff_increase_factor=1.0f)
Set an retry function with a unique name.
bool save(const T *src)
Definition preferences.h:21
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
uint32_t get_preference_hash()
Get a unique hash for storing preferences/settings for this entity.
void trigger(const Ts &...x)
Inform the parent automation that the event has triggered.
Definition automation.h:204
const optional< bool > & get_announcement() const
bool has_value() const
Definition optional.h:92
value_type const & value() const
Definition optional.h:94
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_
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...
bool state
Definition fan.h:0
const char * media_player_state_to_string(MediaPlayerState state)
OTAGlobalCallback * get_global_ota_callback()
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
ESPPreferences * global_preferences
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:367
optional< media_player::MediaPlayerCommand > command
optional< audio::AudioFile * > file
optional< audio::AudioFile * > file