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