Player: transport commands on /api/player

play/pause/toggle/stop/next/prev. Spotify commands run on the session task
(same thread as cspot's frame handling) and the POST waits until they ran,
so it answers the new state. HLS pause stops fetching and resume rejoins at
the live edge; next/prev on HLS and anything on tone/off return 409.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 22:59:14 +10:00
parent 9e1ca98a5d
commit 8485ecb5b7
6 changed files with 146 additions and 16 deletions
+54 -5
View File
@@ -56,6 +56,11 @@ static std::deque<Boundary> s_bounds;
static std::string s_last_id; // track of the most recent data callback
static std::atomic<bool> s_depleted{false};
static std::mutex s_cmd_mutex;
static std::deque<spotify_cmd_t> s_cmds; // from spotify_command(), run by the session loop
static uint32_t s_cmd_queued; // commands queued so far (s_cmd_mutex)
static std::atomic<uint32_t> s_cmd_done{0}; // commands run so far
static std::mutex s_track_mutex;
static cspot::TrackInfo s_track; // audible track (TRACK_INFO), guarded by s_track_mutex
@@ -124,13 +129,51 @@ extern "C" void spotify_set_volume(int pct)
}
}
extern "C" void spotify_pause(bool pause)
extern "C" bool spotify_command(spotify_cmd_t cmd)
{
auto h = s_handler;
if (h) {
h->setPause(pause);
ESP_LOGI(TAG, "%s", pause ? "paused" : "resumed");
if (!s_handler) {
return false;
}
uint32_t seq;
{
std::lock_guard<std::mutex> lock(s_cmd_mutex);
s_cmds.push_back(cmd);
seq = ++s_cmd_queued;
}
// Wait until it has run (the loop runs every <= 200 ms), so callers can report the new state.
for (int i = 0; i < 50 && (int32_t)(s_cmd_done - seq) < 0 && s_handler; i++) {
vTaskDelay(pdMS_TO_TICKS(20));
}
return true;
}
// Session loop: run queued commands on the task that also handles Spotify's frames.
static void run_commands(const std::shared_ptr<cspot::SpircHandler> &handler)
{
std::deque<spotify_cmd_t> cmds;
uint32_t seq;
{
std::lock_guard<std::mutex> lock(s_cmd_mutex);
cmds.swap(s_cmds);
seq = s_cmd_queued;
}
for (auto cmd : cmds) {
switch (cmd) {
case SPOTIFY_CMD_PLAY:
case SPOTIFY_CMD_PAUSE:
handler->setPause(cmd == SPOTIFY_CMD_PAUSE); // notifies the app, sends PLAY_PAUSE
break;
case SPOTIFY_CMD_NEXT:
case SPOTIFY_CMD_PREV:
// The new track's load notifies the app and flushes our buffer (PLAYBACK_START).
if (!(cmd == SPOTIFY_CMD_NEXT ? handler->nextSong() : handler->previousSong())) {
ESP_LOGW(TAG, "no %s track", cmd == SPOTIFY_CMD_NEXT ? "next" : "previous");
}
break;
}
ESP_LOGI(TAG, "command %d", (int)cmd);
}
s_cmd_done = seq;
}
extern "C" bool spotify_session_active(void)
@@ -394,8 +437,14 @@ static void session(void)
ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str());
clear_boundaries(true);
s_depleted = false;
{
std::lock_guard<std::mutex> lock(s_cmd_mutex);
s_cmds.clear();
s_cmd_done = s_cmd_queued;
}
while (!s_stop) {
ctx->session->handlePacket(); // waits at most 200 ms
run_commands(handler);
check_playback(handler);
}
// Stops the queue and player tasks (waits for them), then closes the connection.