ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
sendspin_media_source.cpp
Go to the documentation of this file.
2
3#if defined(USE_ESP32) && defined(USE_SENDSPIN_CONTROLLER) && defined(USE_SENDSPIN_PLAYER)
4
6#include "esphome/core/log.h"
7
8#include <cmath>
9
10namespace esphome::sendspin_ {
11
12static const char *const TAG = "sendspin.media_source";
13
14static constexpr char URI_PREFIX[] = "sendspin://";
15
17 this->player_role_ = this->parent_->get_player_role();
18 if (!this->player_role_) {
19 ESP_LOGE(TAG, "Failed to get player role from hub");
20 this->mark_failed();
21 return;
22 }
23
24 // Push cached states to player role. They may have been set before setup() ran.
25 this->player_role_->update_volume(std::roundf(this->cached_volume_ * 100.0f));
26 this->player_role_->update_muted(this->cached_muted_);
27 this->player_role_->set_static_delay_adjustable(this->static_delay_adjustable_);
28}
29
31 ESP_LOGCONFIG(TAG, "Sendspin Media Source: static_delay_adjustable=%s", YESNO(this->static_delay_adjustable_));
32}
33
34// THREAD CONTEXT: Main loop (invoked from ESPHome actions / config)
36 this->static_delay_adjustable_ = adjustable;
37 if (this->player_role_) {
38 this->player_role_->set_static_delay_adjustable(adjustable);
39 }
40}
41
42// --- MediaSource interface ---
43
44bool SendspinMediaSource::can_handle(const std::string &uri) const { return uri.starts_with(URI_PREFIX); }
45
46// THREAD CONTEXT: Main loop (media_source.h documents play_uri as main-loop only)
47bool SendspinMediaSource::play_uri(const std::string &uri) {
48 if (!this->is_ready() || this->is_failed() || !this->has_listener()) {
49 return false;
50 }
51
53 ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
54 return false;
55 }
56
57 if (!uri.starts_with(URI_PREFIX)) {
58 ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
59 return false;
60 }
61
62 std::string sendspin_id = uri.substr(sizeof(URI_PREFIX) - 1);
63
64 if (sendspin_id.empty()) {
65 ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
66 return false;
67 }
68
69 ESP_LOGD(TAG, "sendspin_id: %s", sendspin_id.c_str());
70
71 if (sendspin_id != "current") {
72 // Connect to a new server as a websocket client
73 this->parent_->connect_to_server("ws://" + sendspin_id);
74 }
75
76 // Tell the orchestrator we're now playing so it routes audio output from us
77 this->pending_start_ = false;
79
80 return true;
81}
82
83// THREAD CONTEXT: Main loop (media_source.h documents handle_command as main-loop only)
85 switch (command) {
87 if (!this->pending_start_) {
88 // Ignore stop commands if we have a pending start, since the orchestrator may send a stop command before
89 // play_uri
90 ESP_LOGD(TAG, "Received STOP command, updating Sendspin state to EXTERNAL_SOURCE");
91 this->parent_->update_state(sendspin::SendspinClientState::EXTERNAL_SOURCE);
92 }
93 break;
94 }
95 case media_source::MediaSourceCommand::PLAY: // NOLINT(bugprone-branch-clone)
96 this->parent_->send_client_command(sendspin::SendspinControllerCommand::PLAY, std::nullopt, std::nullopt);
97 break;
99 this->parent_->send_client_command(sendspin::SendspinControllerCommand::PAUSE, std::nullopt, std::nullopt);
100 break;
102 this->parent_->send_client_command(sendspin::SendspinControllerCommand::NEXT, std::nullopt, std::nullopt);
103 break;
105 this->parent_->send_client_command(sendspin::SendspinControllerCommand::PREVIOUS, std::nullopt, std::nullopt);
106 break;
108 this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ALL, std::nullopt, std::nullopt);
109 break;
111 this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ONE, std::nullopt, std::nullopt);
112 break;
114 this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_OFF, std::nullopt, std::nullopt);
115 break;
117 this->parent_->send_client_command(sendspin::SendspinControllerCommand::SHUFFLE, std::nullopt, std::nullopt);
118 break;
120 this->parent_->send_client_command(sendspin::SendspinControllerCommand::UNSHUFFLE, std::nullopt, std::nullopt);
121 break;
122 default:
123 break;
124 }
125}
126
127// THREAD CONTEXT: Main loop (orchestrator -> source notification)
129 this->cached_volume_ = volume;
130 if (this->player_role_) {
131 this->player_role_->update_volume(std::roundf(volume * 100.0f));
132 }
133}
134
135// THREAD CONTEXT: Main loop (orchestrator -> source notification)
137 this->cached_muted_ = is_muted;
138 if (this->player_role_) {
139 this->player_role_->update_muted(is_muted);
140 }
141}
142
143// THREAD CONTEXT: Speaker playback callback thread (forwarded from the speaker).
144// PlayerRole::notify_audio_played() is documented as thread-safe for this use.
146 if (this->player_role_) {
147 this->player_role_->notify_audio_played(frames, timestamp);
148 }
149}
150
151// --- Sendspin PlayerRoleListener overrides ---
152
153// THREAD CONTEXT: Sendspin sync task background thread. May block up to timeout_ms.
154size_t SendspinMediaSource::on_audio_write(uint8_t *data, size_t length, uint32_t timeout_ms) {
156 vTaskDelay(pdMS_TO_TICKS(timeout_ms));
157 return 0;
158 }
159
160 // PlayerRole::get_current_stream_params() is safe to call from the sync task.
161 auto &params = this->player_role_->get_current_stream_params();
162 if (!params.bit_depth.has_value() || !params.channels.has_value() || !params.sample_rate.has_value()) {
163 vTaskDelay(pdMS_TO_TICKS(timeout_ms));
164 return 0;
165 }
166 audio::AudioStreamInfo stream_info(*params.bit_depth, *params.channels, *params.sample_rate);
167
168 return this->write_output(data, length, timeout_ms, stream_info);
169}
170
171// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback)
173 this->parent_->update_state(sendspin::SendspinClientState::SYNCHRONIZED);
174
175 if (!this->pending_start_) {
176 // Dedup rapid on_stream_start() calls
177 this->pending_start_ = true;
178 // Request the orchestrator to start this source
179 this->request_play_uri_("sendspin://current");
180 }
181}
182
183// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback)
186 // Only set to IDLE if we were previously in a non-IDLE state, to avoid duplicate state changes
188 }
189}
190
191// THREAD CONTEXT: Main loop (PlayerRoleListener callback)
192void SendspinMediaSource::on_volume_changed(uint8_t volume) { this->request_volume_(volume / 100.0f); }
193
194// THREAD CONTEXT: Main loop (PlayerRoleListener callback)
195void SendspinMediaSource::on_mute_changed(bool muted) { this->request_mute_(muted); }
196
197} // namespace esphome::sendspin_
198
199#endif // USE_ESP32 && USE_SENDSPIN_PLAYER && USE_SENDSPIN_CONTROLLER
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:284
bool is_ready() const
void request_volume_(float volume)
Request the orchestrator to change volume.
void set_state_(MediaSourceState state)
Update state and notify listener This is the only way to change state_, ensuring listener notificatio...
void request_play_uri_(const std::string &uri)
Request the orchestrator to play a new URI.
MediaSourceState get_state() const
Get current playback state.
size_t write_output(const uint8_t *data, size_t length, uint32_t timeout_ms, const audio::AudioStreamInfo &stream_info)
Write audio data to the listener.
void request_mute_(bool is_muted)
Request the orchestrator to change mute state.
bool has_listener() const
Check if a listener has been registered.
void on_stream_end() override
Called when the audio stream ends (main loop thread).
void on_stream_start() override
Called when a new audio stream starts (main loop thread).
void on_mute_changed(bool muted) override
Called when mute state changes (main loop thread).
bool can_handle(const std::string &uri) const override
void handle_command(media_source::MediaSourceCommand command) override
size_t on_audio_write(uint8_t *data, size_t length, uint32_t timeout_ms) override
Writes decoded PCM audio to ESPHome's media source output pipeline.
void on_volume_changed(uint8_t volume) override
Called when volume changes (main loop thread).
bool play_uri(const std::string &uri) override
void notify_audio_played(uint32_t frames, int64_t timestamp) override
MediaSourceCommand
Commands that are sent from the orchestrator to a media source.
static void uint32_t
uint16_t length
Definition tt21100.cpp:0
uint16_t timestamp
Definition tt21100.cpp:2