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