ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
sendspin_media_source.cpp
Go to the documentation of this file.
2
3#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_CONTROLLER) && defined(USE_SENDSPIN_PLAYER)
4
6#include "esphome/core/log.h"
7
8#include <cmath>
9
10namespace esphome::sendspin_ {
11
12ESPHOME_LOG_TAG(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_output_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_output_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 // The queued request has been delivered, whatever the outcome, so the next stream start may request again
49 this->pending_start_ = false;
50 if (!this->is_ready() || this->is_failed() || !this->has_listener()) {
51 return false;
52 }
53
55 ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
56 return false;
57 }
58
59 if (!this->parent_->is_client_running()) {
60 ESP_LOGE(TAG, "Cannot play '%s': Sendspin is disabled", uri.c_str());
61 return false;
62 }
63
64 if (!uri.starts_with(URI_PREFIX)) {
65 ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
66 return false;
67 }
68
69 std::string sendspin_id = uri.substr(sizeof(URI_PREFIX) - 1);
70
71 if (sendspin_id.empty()) {
72 ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
73 return false;
74 }
75
76 ESP_LOGD(TAG, "sendspin_id: %s", sendspin_id.c_str());
77
78 if (sendspin_id != "current") {
79 // Connect to a new server as a websocket client
80 this->parent_->connect_to_server("ws://" + sendspin_id);
81 }
82
83 // Tell the orchestrator we're now playing so it routes audio output from us
85
86 return true;
87}
88
89// THREAD CONTEXT: Main loop (media_source.h documents handle_command as main-loop only)
91 if (!this->parent_->is_client_running()) {
93 // Nothing is playing, so the orchestrator gets its pipeline back straight away
94 this->on_stream_end();
95 } else {
96 ESP_LOGW(TAG, "Cannot handle command: Sendspin is disabled");
97 }
98 return;
99 }
100 switch (command) {
102 if (!this->pending_start_) {
103 // Ignore stop commands if we have a pending start, since the orchestrator may send a stop command before
104 // play_uri
105 ESP_LOGD(TAG, "Received STOP command, leaving the Sendspin group");
106 this->parent_->leave_group();
107 }
108 break;
109 }
110 case media_source::MediaSourceCommand::PLAY: // NOLINT(bugprone-branch-clone)
111 this->parent_->send_client_command(sendspin::SendspinControllerCommand::PLAY, std::nullopt, std::nullopt);
112 break;
114 this->parent_->send_client_command(sendspin::SendspinControllerCommand::PAUSE, std::nullopt, std::nullopt);
115 break;
117 this->parent_->send_client_command(sendspin::SendspinControllerCommand::NEXT, std::nullopt, std::nullopt);
118 break;
120 this->parent_->send_client_command(sendspin::SendspinControllerCommand::PREVIOUS, std::nullopt, std::nullopt);
121 break;
123 this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ALL, std::nullopt, std::nullopt);
124 break;
126 this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ONE, std::nullopt, std::nullopt);
127 break;
129 this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_OFF, std::nullopt, std::nullopt);
130 break;
132 this->parent_->send_client_command(sendspin::SendspinControllerCommand::SHUFFLE, std::nullopt, std::nullopt);
133 break;
135 this->parent_->send_client_command(sendspin::SendspinControllerCommand::UNSHUFFLE, std::nullopt, std::nullopt);
136 break;
137 default:
138 break;
139 }
140}
141
142// THREAD CONTEXT: Main loop (orchestrator -> source notification)
144 this->cached_volume_ = volume;
145 if (this->player_role_) {
146 this->player_role_->update_volume(std::roundf(volume * 100.0f));
147 }
148}
149
150// THREAD CONTEXT: Main loop (orchestrator -> source notification)
152 this->cached_muted_ = is_muted;
153 if (this->player_role_) {
154 this->player_role_->update_muted(is_muted);
155 }
156}
157
158// THREAD CONTEXT: Speaker playback callback thread (forwarded from the speaker).
159// PlayerRole::notify_audio_played() is documented as thread-safe for this use.
161 if (this->player_role_) {
162 this->player_role_->notify_audio_played(frames, timestamp);
163 }
164}
165
166// --- Sendspin PlayerRoleListener overrides ---
167
168// THREAD CONTEXT: Sendspin sync task background thread. May block up to timeout_ms.
169size_t SendspinMediaSource::on_audio_write(uint8_t *data, size_t length, uint32_t timeout_ms) {
171 vTaskDelay(pdMS_TO_TICKS(timeout_ms));
172 return 0;
173 }
174
175 // PlayerRole::get_current_stream_params() is safe to call from the sync task.
176 auto &params = this->player_role_->get_current_stream_params();
177 if (!params.bit_depth.has_value() || !params.channels.has_value() || !params.sample_rate.has_value()) {
178 vTaskDelay(pdMS_TO_TICKS(timeout_ms));
179 return 0;
180 }
181 audio::AudioStreamInfo stream_info(*params.bit_depth, *params.channels, *params.sample_rate);
182
183 return this->write_output(data, length, timeout_ms, stream_info);
184}
185
186// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback)
188 if (!this->pending_start_) {
189 // Dedup rapid on_stream_start() calls
190 this->pending_start_ = true;
191 // Request the orchestrator to start this source
192 this->request_play_uri_("sendspin://current");
193 }
194}
195
196// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback)
199 // Only set to IDLE if we were previously in a non-IDLE state, to avoid duplicate state changes
201 }
202}
203
204// THREAD CONTEXT: Main loop (PlayerRoleListener callback)
205void SendspinMediaSource::on_volume_changed(uint8_t volume) { this->request_volume_(volume / 100.0f); }
206
207// THREAD CONTEXT: Main loop (PlayerRoleListener callback)
208void SendspinMediaSource::on_mute_changed(bool muted) { this->request_mute_(muted); }
209
210} // namespace esphome::sendspin_
211
212#endif // USE_ESP_IDF && USE_SENDSPIN_PLAYER && USE_SENDSPIN_CONTROLLER
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
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.
ESPHOME_LOG_TAG(TAG, "sendspin.image")
static void uint32_t
uint16_t length
Definition tt21100.cpp:0
uint16_t timestamp
Definition tt21100.cpp:2