ESPHome  2025.3.3
speaker_media_player.cpp
Go to the documentation of this file.
1 #include "speaker_media_player.h"
2 
3 #ifdef USE_ESP_IDF
4 
5 #include "esphome/core/log.h"
6 
8 #ifdef USE_OTA
10 #endif
11 
12 namespace esphome {
13 namespace 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 
46 static const uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20;
47 
48 static const UBaseType_t MEDIA_PIPELINE_TASK_PRIORITY = 1;
49 static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1;
50 
51 static const float FIRST_BOOT_DEFAULT_VOLUME = 0.5f;
52 
53 static 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 
93  make_unique<AudioPipeline>(this->announcement_speaker_, this->buffer_size_, this->task_stack_in_psram_, "ann",
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_()) {
102  this->media_pipeline_ = make_unique<AudioPipeline>(this->media_speaker_, this->buffer_size_,
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  // Setup callback to track the duration of audio played by the media pipeline
111  this->media_speaker_->add_audio_output_callback(
112  [this](uint32_t new_playback_ms, uint32_t remainder_us, uint32_t pending_ms, uint32_t write_timestamp) {
113  this->playback_ms_ += new_playback_ms;
114  this->remainder_us_ = remainder_us;
115  this->pending_ms_ = pending_ms;
116  this->last_audio_write_timestamp_ = write_timestamp;
117  this->playback_us_ = this->playback_ms_ * 1000 + this->remainder_us_;
118  });
119  }
120 
121  ESP_LOGI(TAG, "Set up speaker media player");
122 }
123 
124 void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms) {
125  switch (pipeline_type) {
127  this->announcement_playlist_delay_ms_ = delay_ms;
128  break;
130  this->media_playlist_delay_ms_ = delay_ms;
131  break;
132  }
133 }
134 
136  if (!this->is_ready()) {
137  return;
138  }
139 
140  MediaCallCommand media_command;
141 
142  if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) {
143  bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
144 
145  if (media_command.url.has_value() || media_command.file.has_value()) {
146  PlaylistItem playlist_item;
147  if (media_command.url.has_value()) {
148  playlist_item.url = *media_command.url.value();
149  delete media_command.url.value();
150  }
151  if (media_command.file.has_value()) {
152  playlist_item.file = media_command.file.value();
153  }
154 
155  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
156  if (!enqueue) {
157  // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
158  this->cancel_timeout("next_ann");
159  this->announcement_playlist_.clear();
160  if (media_command.file.has_value()) {
161  this->announcement_pipeline_->start_file(playlist_item.file.value());
162  } else if (media_command.url.has_value()) {
163  this->announcement_pipeline_->start_url(playlist_item.url.value());
164  }
165  this->announcement_pipeline_->set_pause_state(false);
166  }
167  this->announcement_playlist_.push_back(playlist_item);
168  } else {
169  if (!enqueue) {
170  // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
171  this->cancel_timeout("next_media");
172  this->media_playlist_.clear();
173  if (this->is_paused_) {
174  // If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a
175  // short segment of the paused file before starting the new one.
176  this->media_pipeline_->stop();
177  this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
179  this->media_pipeline_->set_pause_state(false);
180  this->is_paused_ = false;
181  return RetryResult::DONE;
182  }
183  return RetryResult::RETRY;
184  });
185  } else {
186  // Not paused, just directly start the file
187  if (media_command.file.has_value()) {
188  this->media_pipeline_->start_file(playlist_item.file.value());
189  } else if (media_command.url.has_value()) {
190  this->media_pipeline_->start_url(playlist_item.url.value());
191  }
192  this->media_pipeline_->set_pause_state(false);
193  this->is_paused_ = false;
194  }
195  }
196  this->media_playlist_.push_back(playlist_item);
197  }
198 
199  return; // Don't process the new file play command further
200  }
201 
202  if (media_command.volume.has_value()) {
203  this->set_volume_(media_command.volume.value());
204  this->publish_state();
205  }
206 
207  if (media_command.command.has_value()) {
208  switch (media_command.command.value()) {
210  if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) {
211  this->media_pipeline_->set_pause_state(false);
212  }
213  this->is_paused_ = false;
214  break;
216  if ((this->media_pipeline_ != nullptr) && (!this->is_paused_)) {
217  this->media_pipeline_->set_pause_state(true);
218  }
219  this->is_paused_ = true;
220  break;
222  // Pipelines do not stop immediately after calling the stop command, so confirm its stopped before unpausing.
223  // This avoids an audible short segment playing after receiving the stop command in a paused state.
224  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
225  if (this->announcement_pipeline_ != nullptr) {
226  this->cancel_timeout("next_ann");
227  this->announcement_playlist_.clear();
228  this->announcement_pipeline_->stop();
229  this->set_retry("unpause_ann", 50, 3, [this](const uint8_t remaining_attempts) {
231  this->announcement_pipeline_->set_pause_state(false);
232  return RetryResult::DONE;
233  }
234  return RetryResult::RETRY;
235  });
236  }
237  } else {
238  if (this->media_pipeline_ != nullptr) {
239  this->cancel_timeout("next_media");
240  this->media_playlist_.clear();
241  this->media_pipeline_->stop();
242  this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
244  this->media_pipeline_->set_pause_state(false);
245  this->is_paused_ = false;
246  return RetryResult::DONE;
247  }
248  return RetryResult::RETRY;
249  });
250  }
251  }
252 
253  break;
255  if (this->media_pipeline_ != nullptr) {
256  if (this->is_paused_) {
257  this->media_pipeline_->set_pause_state(false);
258  this->is_paused_ = false;
259  } else {
260  this->media_pipeline_->set_pause_state(true);
261  this->is_paused_ = true;
262  }
263  }
264  break;
266  this->set_mute_state_(true);
267 
268  this->publish_state();
269  break;
270  }
272  this->set_mute_state_(false);
273  this->publish_state();
274  break;
276  this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_));
277  this->publish_state();
278  break;
280  this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_));
281  this->publish_state();
282  break;
284  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
285  this->announcement_repeat_one_ = true;
286  } else {
287  this->media_repeat_one_ = true;
288  }
289  break;
291  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
292  this->announcement_repeat_one_ = false;
293  } else {
294  this->media_repeat_one_ = false;
295  }
296  break;
298  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
299  if (this->announcement_playlist_.empty()) {
300  this->announcement_playlist_.resize(1);
301  }
302  } else {
303  if (this->media_playlist_.empty()) {
304  this->media_playlist_.resize(1);
305  }
306  }
307  break;
308  default:
309  break;
310  }
311  }
312  }
313 }
314 
316  this->watch_media_commands_();
317 
318  // Determine state of the media player
319  media_player::MediaPlayerState old_state = this->state;
320 
321  AudioPipelineState old_media_pipeline_state = this->media_pipeline_state_;
322  if (this->media_pipeline_ != nullptr) {
323  this->media_pipeline_state_ = this->media_pipeline_->process_state();
324  this->decoded_playback_ms_ = this->media_pipeline_->get_playback_ms();
325  }
326 
328  ESP_LOGE(TAG, "The media pipeline's file reader encountered an error.");
330  ESP_LOGE(TAG, "The media pipeline's audio decoder encountered an error.");
331  }
332 
333  AudioPipelineState old_announcement_pipeline_state = this->announcement_pipeline_state_;
334  if (this->announcement_pipeline_ != nullptr) {
335  this->announcement_pipeline_state_ = this->announcement_pipeline_->process_state();
336  }
337 
339  ESP_LOGE(TAG, "The announcement pipeline's file reader encountered an error.");
341  ESP_LOGE(TAG, "The announcement pipeline's audio decoder encountered an error.");
342  }
343 
346  } else {
347  if (!this->announcement_playlist_.empty()) {
348  uint32_t timeout_ms = 0;
349  if (old_announcement_pipeline_state == AudioPipelineState::PLAYING) {
350  // Finished the current announcement file
351  if (!this->announcement_repeat_one_) {
352  // Pop item off the playlist if repeat is disabled
353  this->announcement_playlist_.pop_front();
354  }
355  // Only delay starting playback if moving on the next playlist item or repeating the current item
356  timeout_ms = this->announcement_playlist_delay_ms_;
357  }
358 
359  if (!this->announcement_playlist_.empty()) {
360  // Start the next announcement file
361  PlaylistItem playlist_item = this->announcement_playlist_.front();
362  if (playlist_item.url.has_value()) {
363  this->announcement_pipeline_->start_url(playlist_item.url.value());
364  } else if (playlist_item.file.has_value()) {
365  this->announcement_pipeline_->start_file(playlist_item.file.value());
366  }
367 
368  if (timeout_ms > 0) {
369  // Pause pipeline internally to facilitate the delay between items
370  this->announcement_pipeline_->set_pause_state(true);
371  // Internally unpause the pipeline after the delay between playlist items. Announcements do not follow the
372  // media player's pause state.
373  this->set_timeout("next_ann", timeout_ms, [this]() { this->announcement_pipeline_->set_pause_state(false); });
374  }
375  }
376  } else {
377  if (this->is_paused_) {
382  // Reset playback durations
383  this->decoded_playback_ms_ = 0;
384  this->playback_us_ = 0;
385  this->playback_ms_ = 0;
386  this->remainder_us_ = 0;
387  this->pending_ms_ = 0;
388 
389  if (!media_playlist_.empty()) {
390  uint32_t timeout_ms = 0;
391  if (old_media_pipeline_state == AudioPipelineState::PLAYING) {
392  // Finished the current media file
393  if (!this->media_repeat_one_) {
394  // Pop item off the playlist if repeat is disabled
395  this->media_playlist_.pop_front();
396  }
397  // Only delay starting playback if moving on the next playlist item or repeating the current item
398  timeout_ms = this->announcement_playlist_delay_ms_;
399  }
400  if (!this->media_playlist_.empty()) {
401  PlaylistItem playlist_item = this->media_playlist_.front();
402  if (playlist_item.url.has_value()) {
403  this->media_pipeline_->start_url(playlist_item.url.value());
404  } else if (playlist_item.file.has_value()) {
405  this->media_pipeline_->start_file(playlist_item.file.value());
406  }
407 
408  if (timeout_ms > 0) {
409  // Pause pipeline internally to facilitate the delay between items
410  this->media_pipeline_->set_pause_state(true);
411  // Internally unpause the pipeline after the delay between playlist items, if the media player state is
412  // not paused.
413  this->set_timeout("next_media", timeout_ms,
414  [this]() { this->media_pipeline_->set_pause_state(this->is_paused_); });
415  }
416  }
417  } else {
419  }
420  }
421  }
422  }
423 
424  if (this->state != old_state) {
425  this->publish_state();
426  ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
427  }
428 }
429 
430 void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) {
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  media_command.file = media_file;
439  if (this->single_pipeline_() || announcement) {
440  media_command.announce = true;
441  } else {
442  media_command.announce = false;
443  }
444  media_command.enqueue = enqueue;
445  xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
446 }
447 
449  if (!this->is_ready()) {
450  // Ignore any commands sent before the media player is setup
451  return;
452  }
453 
454  MediaCallCommand media_command;
455 
456  if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) {
457  media_command.announce = true;
458  } else {
459  media_command.announce = false;
460  }
461 
462  if (call.get_media_url().has_value()) {
463  media_command.url = new std::string(
464  call.get_media_url().value()); // Must be manually deleted after receiving media_command from a queue
465 
466  if (call.get_command().has_value()) {
468  media_command.enqueue = true;
469  }
470  }
471 
472  xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
473  return;
474  }
475 
476  if (call.get_volume().has_value()) {
477  media_command.volume = call.get_volume().value();
478  // Wait 0 ticks for queue to be free, volume sets aren't that important!
479  xQueueSend(this->media_control_command_queue_, &media_command, 0);
480  return;
481  }
482 
483  if (call.get_command().has_value()) {
484  media_command.command = call.get_command().value();
485  TickType_t ticks_to_wait = portMAX_DELAY;
488  ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important!
489  }
490  xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait);
491  return;
492  }
493 }
494 
496  auto traits = media_player::MediaPlayerTraits();
497  if (!this->single_pipeline_()) {
498  traits.set_supports_pause(true);
499  }
500 
501  if (this->announcement_format_.has_value()) {
502  traits.get_supported_formats().push_back(this->announcement_format_.value());
503  }
504  if (this->media_format_.has_value()) {
505  traits.get_supported_formats().push_back(this->media_format_.value());
506  } else if (this->single_pipeline_() && this->announcement_format_.has_value()) {
507  // Only one pipeline is defined, so use the announcement format (if configured) for the default purpose
510  traits.get_supported_formats().push_back(media_format);
511  }
512 
513  return traits;
514 };
515 
517  VolumeRestoreState volume_restore_state;
518  volume_restore_state.volume = this->volume;
519  volume_restore_state.is_muted = this->is_muted_;
520  this->pref_.save(&volume_restore_state);
521 }
522 
523 void SpeakerMediaPlayer::set_mute_state_(bool mute_state) {
524  if (this->media_speaker_ != nullptr) {
525  this->media_speaker_->set_mute_state(mute_state);
526  }
527  if (this->announcement_speaker_ != nullptr) {
528  this->announcement_speaker_->set_mute_state(mute_state);
529  }
530 
531  bool old_mute_state = this->is_muted_;
532  this->is_muted_ = mute_state;
533 
535 
536  if (old_mute_state != mute_state) {
537  if (mute_state) {
538  this->defer([this]() { this->mute_trigger_->trigger(); });
539  } else {
540  this->defer([this]() { this->unmute_trigger_->trigger(); });
541  }
542  }
543 }
544 
545 void SpeakerMediaPlayer::set_volume_(float volume, bool publish) {
546  // Remap the volume to fit with in the configured limits
547  float bounded_volume = remap<float, float>(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_);
548 
549  if (this->media_speaker_ != nullptr) {
550  this->media_speaker_->set_volume(bounded_volume);
551  }
552 
553  if (this->announcement_speaker_ != nullptr) {
554  this->announcement_speaker_->set_volume(bounded_volume);
555  }
556 
557  if (publish) {
558  this->volume = volume;
560  }
561 
562  // Turn on the mute state if the volume is effectively zero, off otherwise
563  if (volume < 0.001) {
564  this->set_mute_state_(true);
565  } else {
566  this->set_mute_state_(false);
567  }
568 
569  this->defer([this, volume]() { this->volume_trigger_->trigger(volume); });
570 }
571 
572 } // namespace speaker
573 } // namespace esphome
574 
575 #endif
value_type const & value() const
Definition: optional.h:89
void control(const media_player::MediaPlayerCall &call) override
bool single_pipeline_()
Returns true if the media player has only the announcement pipeline defined, false if both the announ...
std::unique_ptr< AudioPipeline > media_pipeline_
bool cancel_timeout(const std::string &name)
Cancel a timeout function.
Definition: component.cpp:73
virtual void set_volume(float volume)
Definition: speaker.h:71
optional< media_player::MediaPlayerSupportedFormat > announcement_format_
std::unique_ptr< AudioPipeline > announcement_pipeline_
optional< audio::AudioFile * > file
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:69
void defer(const std::string &name, std::function< void()> &&f)
Defer a callback to the next loop() call.
Definition: component.cpp:130
optional< media_player::MediaPlayerCommand > command
void save_volume_restore_state_()
Saves the current volume and mute state to the flash for restoration.
bool has_value() const
Definition: optional.h:87
optional< audio::AudioFile * > file
void trigger(Ts... x)
Inform the parent automation that the event has triggered.
Definition: automation.h:95
void set_mute_state_(bool mute_state)
Sets the mute state.
bool save(const T *src)
Definition: preferences.h:21
bool is_ready() const
Definition: component.cpp:144
const optional< float > & get_volume() const
Definition: media_player.h:81
const char * media_player_state_to_string(MediaPlayerState state)
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:60
ESPPreferences * global_preferences
media_player::MediaPlayerTraits get_traits() override
std::deque< PlaylistItem > media_playlist_
const optional< bool > & get_announcement() const
Definition: media_player.h:82
optional< media_player::MediaPlayerSupportedFormat > media_format_
void add_on_state_callback(std::function< void(OTAState, float, uint8_t, OTAComponent *)> &&callback)
Definition: ota_backend.h:82
const optional< MediaPlayerCommand > & get_command() const
Definition: media_player.h:79
std::deque< PlaylistItem > announcement_playlist_
const optional< std::string > & get_media_url() const
Definition: media_player.h:80
void set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms)
void set_volume_(float volume, bool publish=true)
Updates this->volume and saves volume/mute state to flash for restortation if publish is true...
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:118
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
OTAGlobalCallback * get_global_ota_callback()
Definition: ota_backend.cpp:9
uint32_t get_object_id_hash()
Definition: entity_base.cpp:76
virtual void set_mute_state(bool mute_state)
Definition: speaker.h:81
void play_file(audio::AudioFile *media_file, bool announcement, bool enqueue)