ESPHome  2025.2.0
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_, "ann", 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  esp_err_t err = ESP_OK;
142 
143  if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) {
144  bool new_url = media_command.new_url.has_value() && media_command.new_url.value();
145  bool new_file = media_command.new_file.has_value() && media_command.new_file.value();
146 
147  if (new_url || new_file) {
148  bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
149 
150  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
151  // Announcement playlist/pipeline
152 
153  if (!enqueue) {
154  // Clear the queue and ensure the loaded next item doesn't start playing
155  this->cancel_timeout("next_ann");
156  this->announcement_playlist_.clear();
157  }
158 
159  PlaylistItem playlist_item;
160  if (new_url) {
161  playlist_item.url = this->announcement_url_;
162  if (!enqueue) {
163  // Not adding to the queue, so directly start playback and internally unpause the pipeline
164  this->announcement_pipeline_->start_url(playlist_item.url.value());
165  this->announcement_pipeline_->set_pause_state(false);
166  }
167  } else {
168  playlist_item.file = this->announcement_file_;
169  if (!enqueue) {
170  // Not adding to the queue, so directly start playback and internally unpause the pipeline
171  this->announcement_pipeline_->start_file(playlist_item.file.value());
172  this->announcement_pipeline_->set_pause_state(false);
173  }
174  }
175  this->announcement_playlist_.push_back(playlist_item);
176  } else {
177  // Media playlist/pipeline
178 
179  if (!enqueue) {
180  // Clear the queue and ensure the loaded next item doesn't start playing
181  this->cancel_timeout("next_media");
182  this->media_playlist_.clear();
183  }
184 
185  this->is_paused_ = false;
186  PlaylistItem playlist_item;
187  if (new_url) {
188  playlist_item.url = this->media_url_;
189  if (!enqueue) {
190  // Not adding to the queue, so directly start playback and internally unpause the pipeline
191  this->media_pipeline_->start_url(playlist_item.url.value());
192  this->media_pipeline_->set_pause_state(false);
193  }
194  } else {
195  playlist_item.file = this->media_file_;
196  if (!enqueue) {
197  // Not adding to the queue, so directly start playback and internally unpause the pipeline
198  this->media_pipeline_->start_file(playlist_item.file.value());
199  this->media_pipeline_->set_pause_state(false);
200  }
201  }
202  this->media_playlist_.push_back(playlist_item);
203  }
204 
205  if (err != ESP_OK) {
206  ESP_LOGE(TAG, "Error starting the audio pipeline: %s", esp_err_to_name(err));
207  this->status_set_error();
208  } else {
209  this->status_clear_error();
210  }
211 
212  return; // Don't process the new file play command further
213  }
214 
215  if (media_command.volume.has_value()) {
216  this->set_volume_(media_command.volume.value());
217  this->publish_state();
218  }
219 
220  if (media_command.command.has_value()) {
221  switch (media_command.command.value()) {
223  if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) {
224  this->media_pipeline_->set_pause_state(false);
225  }
226  this->is_paused_ = false;
227  break;
229  if ((this->media_pipeline_ != nullptr) && (!this->is_paused_)) {
230  this->media_pipeline_->set_pause_state(true);
231  }
232  this->is_paused_ = true;
233  break;
235  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
236  if (this->announcement_pipeline_ != nullptr) {
237  this->cancel_timeout("next_ann");
238  this->announcement_playlist_.clear();
239  this->announcement_pipeline_->stop();
240  }
241  } else {
242  if (this->media_pipeline_ != nullptr) {
243  this->cancel_timeout("next_media");
244  this->media_playlist_.clear();
245  this->media_pipeline_->stop();
246  }
247  }
248  break;
250  if (this->media_pipeline_ != nullptr) {
251  if (this->is_paused_) {
252  this->media_pipeline_->set_pause_state(false);
253  this->is_paused_ = false;
254  } else {
255  this->media_pipeline_->set_pause_state(true);
256  this->is_paused_ = true;
257  }
258  }
259  break;
261  this->set_mute_state_(true);
262 
263  this->publish_state();
264  break;
265  }
267  this->set_mute_state_(false);
268  this->publish_state();
269  break;
271  this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_));
272  this->publish_state();
273  break;
275  this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_));
276  this->publish_state();
277  break;
279  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
280  this->announcement_repeat_one_ = true;
281  } else {
282  this->media_repeat_one_ = true;
283  }
284  break;
286  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
287  this->announcement_repeat_one_ = false;
288  } else {
289  this->media_repeat_one_ = false;
290  }
291  break;
293  if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
294  if (this->announcement_playlist_.empty()) {
295  this->announcement_playlist_.resize(1);
296  }
297  } else {
298  if (this->media_playlist_.empty()) {
299  this->media_playlist_.resize(1);
300  }
301  }
302  break;
303  default:
304  break;
305  }
306  }
307  }
308 }
309 
311  this->watch_media_commands_();
312 
313  // Determine state of the media player
314  media_player::MediaPlayerState old_state = this->state;
315 
316  AudioPipelineState old_media_pipeline_state = this->media_pipeline_state_;
317  if (this->media_pipeline_ != nullptr) {
318  this->media_pipeline_state_ = this->media_pipeline_->process_state();
319  this->decoded_playback_ms_ = this->media_pipeline_->get_playback_ms();
320  }
321 
323  ESP_LOGE(TAG, "The media pipeline's file reader encountered an error.");
325  ESP_LOGE(TAG, "The media pipeline's audio decoder encountered an error.");
326  }
327 
328  AudioPipelineState old_announcement_pipeline_state = this->announcement_pipeline_state_;
329  if (this->announcement_pipeline_ != nullptr) {
330  this->announcement_pipeline_state_ = this->announcement_pipeline_->process_state();
331  }
332 
334  ESP_LOGE(TAG, "The announcement pipeline's file reader encountered an error.");
336  ESP_LOGE(TAG, "The announcement pipeline's audio decoder encountered an error.");
337  }
338 
341  } else {
342  if (!this->announcement_playlist_.empty()) {
343  uint32_t timeout_ms = 0;
344  if (old_announcement_pipeline_state == AudioPipelineState::PLAYING) {
345  // Finished the current announcement file
346  if (!this->announcement_repeat_one_) {
347  // Pop item off the playlist if repeat is disabled
348  this->announcement_playlist_.pop_front();
349  }
350  // Only delay starting playback if moving on the next playlist item or repeating the current item
351  timeout_ms = this->announcement_playlist_delay_ms_;
352  }
353 
354  if (!this->announcement_playlist_.empty()) {
355  // Start the next announcement file
356  PlaylistItem playlist_item = this->announcement_playlist_.front();
357  if (playlist_item.url.has_value()) {
358  this->announcement_pipeline_->start_url(playlist_item.url.value());
359  } else if (playlist_item.file.has_value()) {
360  this->announcement_pipeline_->start_file(playlist_item.file.value());
361  }
362 
363  if (timeout_ms > 0) {
364  // Pause pipeline internally to facilitiate delay between items
365  this->announcement_pipeline_->set_pause_state(true);
366  // Internally unpause the pipeline after the delay between playlist items
367  this->set_timeout("next_ann", timeout_ms,
368  [this]() { this->announcement_pipeline_->set_pause_state(this->is_paused_); });
369  }
370  }
371  } else {
372  if (this->is_paused_) {
377  // Reset playback durations
378  this->decoded_playback_ms_ = 0;
379  this->playback_us_ = 0;
380  this->playback_ms_ = 0;
381  this->remainder_us_ = 0;
382  this->pending_ms_ = 0;
383 
384  if (!media_playlist_.empty()) {
385  uint32_t timeout_ms = 0;
386  if (old_media_pipeline_state == AudioPipelineState::PLAYING) {
387  // Finished the current media file
388  if (!this->media_repeat_one_) {
389  // Pop item off the playlist if repeat is disabled
390  this->media_playlist_.pop_front();
391  }
392  // Only delay starting playback if moving on the next playlist item or repeating the current item
393  timeout_ms = this->announcement_playlist_delay_ms_;
394  }
395  if (!this->media_playlist_.empty()) {
396  PlaylistItem playlist_item = this->media_playlist_.front();
397  if (playlist_item.url.has_value()) {
398  this->media_pipeline_->start_url(playlist_item.url.value());
399  } else if (playlist_item.file.has_value()) {
400  this->media_pipeline_->start_file(playlist_item.file.value());
401  }
402 
403  if (timeout_ms > 0) {
404  // Pause pipeline internally to facilitiate delay between items
405  this->media_pipeline_->set_pause_state(true);
406  // Internally unpause the pipeline after the delay between playlist items
407  this->set_timeout("next_media", timeout_ms,
408  [this]() { this->media_pipeline_->set_pause_state(this->is_paused_); });
409  }
410  }
411  } else {
413  }
414  }
415  }
416  }
417 
418  if (this->state != old_state) {
419  this->publish_state();
420  ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
421  }
422 }
423 
424 void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) {
425  if (!this->is_ready()) {
426  // Ignore any commands sent before the media player is setup
427  return;
428  }
429 
430  MediaCallCommand media_command;
431 
432  media_command.new_file = true;
433  if (this->single_pipeline_() || announcement) {
434  this->announcement_file_ = media_file;
435  media_command.announce = true;
436  } else {
437  this->media_file_ = media_file;
438  media_command.announce = false;
439  }
440  media_command.enqueue = enqueue;
441  xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
442 }
443 
445  if (!this->is_ready()) {
446  // Ignore any commands sent before the media player is setup
447  return;
448  }
449 
450  MediaCallCommand media_command;
451 
452  if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) {
453  media_command.announce = true;
454  } else {
455  media_command.announce = false;
456  }
457 
458  if (call.get_media_url().has_value()) {
459  std::string new_uri = call.get_media_url().value();
460 
461  media_command.new_url = true;
462  if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) {
463  this->announcement_url_ = new_uri;
464  } else {
465  this->media_url_ = new_uri;
466  }
467 
468  if (call.get_command().has_value()) {
470  media_command.enqueue = true;
471  }
472  }
473 
474  xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
475  return;
476  }
477 
478  if (call.get_volume().has_value()) {
479  media_command.volume = call.get_volume().value();
480  // Wait 0 ticks for queue to be free, volume sets aren't that important!
481  xQueueSend(this->media_control_command_queue_, &media_command, 0);
482  return;
483  }
484 
485  if (call.get_command().has_value()) {
486  media_command.command = call.get_command().value();
487  TickType_t ticks_to_wait = portMAX_DELAY;
490  ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important!
491  }
492  xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait);
493  return;
494  }
495 }
496 
498  auto traits = media_player::MediaPlayerTraits();
499  if (!this->single_pipeline_()) {
500  traits.set_supports_pause(true);
501  }
502 
503  if (this->announcement_format_.has_value()) {
504  traits.get_supported_formats().push_back(this->announcement_format_.value());
505  }
506  if (this->media_format_.has_value()) {
507  traits.get_supported_formats().push_back(this->media_format_.value());
508  } else if (this->single_pipeline_() && this->announcement_format_.has_value()) {
509  // Only one pipeline is defined, so use the announcement format (if configured) for the default purpose
512  traits.get_supported_formats().push_back(media_format);
513  }
514 
515  return traits;
516 };
517 
519  VolumeRestoreState volume_restore_state;
520  volume_restore_state.volume = this->volume;
521  volume_restore_state.is_muted = this->is_muted_;
522  this->pref_.save(&volume_restore_state);
523 }
524 
525 void SpeakerMediaPlayer::set_mute_state_(bool mute_state) {
526  if (this->media_speaker_ != nullptr) {
527  this->media_speaker_->set_mute_state(mute_state);
528  }
529  if (this->announcement_speaker_ != nullptr) {
530  this->announcement_speaker_->set_mute_state(mute_state);
531  }
532 
533  bool old_mute_state = this->is_muted_;
534  this->is_muted_ = mute_state;
535 
537 
538  if (old_mute_state != mute_state) {
539  if (mute_state) {
540  this->defer([this]() { this->mute_trigger_->trigger(); });
541  } else {
542  this->defer([this]() { this->unmute_trigger_->trigger(); });
543  }
544  }
545 }
546 
547 void SpeakerMediaPlayer::set_volume_(float volume, bool publish) {
548  // Remap the volume to fit with in the configured limits
549  float bounded_volume = remap<float, float>(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_);
550 
551  if (this->media_speaker_ != nullptr) {
552  this->media_speaker_->set_volume(bounded_volume);
553  }
554 
555  if (this->announcement_speaker_ != nullptr) {
556  this->announcement_speaker_->set_volume(bounded_volume);
557  }
558 
559  if (publish) {
560  this->volume = volume;
562  }
563 
564  // Turn on the mute state if the volume is effectively zero, off otherwise
565  if (volume < 0.001) {
566  this->set_mute_state_(true);
567  } else {
568  this->set_mute_state_(false);
569  }
570 
571  this->defer([this, volume]() { this->volume_trigger_->trigger(volume); });
572 }
573 
574 } // namespace speaker
575 } // namespace esphome
576 
577 #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
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 status_set_error(const char *message="unspecified")
Definition: component.cpp:159
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 status_clear_error()
Definition: component.cpp:172
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)