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:
+85
-7
@@ -37,6 +37,7 @@ static volatile bool s_playing; // ring output running (after prefill)
|
||||
static volatile bool s_flush; // consumer drops buffered audio on the next read
|
||||
static volatile bool s_paused; // Spotify paused: silence, buffer kept, no underrun
|
||||
static volatile bool s_sp_playing; // Spotify has started playback and is not paused
|
||||
static volatile bool s_hls_paused; // HLS paused via the API: not fetching, silence
|
||||
static audio_conv_t *s_spotify_conv;
|
||||
|
||||
// Failover (mode auto)
|
||||
@@ -107,7 +108,8 @@ static size_t read_current(int32_t *buf, size_t frames)
|
||||
s_flush = false;
|
||||
s_playing = false;
|
||||
}
|
||||
if (s_src == PLAYER_SRC_OFF || (s_src == PLAYER_SRC_SPOTIFY && s_paused)) {
|
||||
if (s_src == PLAYER_SRC_OFF || (s_src == PLAYER_SRC_SPOTIFY && s_paused) ||
|
||||
(s_src == PLAYER_SRC_HLS && s_hls_paused)) {
|
||||
memset(buf, 0, frames * CHANNELS * sizeof(int32_t));
|
||||
return frames;
|
||||
}
|
||||
@@ -158,6 +160,10 @@ static void select_source(player_src_t src)
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "source: %s -> %s", SRC_NAME[s_src], SRC_NAME[src]);
|
||||
if (s_hls_paused) { // an API pause of HLS lasts until the next switch
|
||||
s_hls_paused = false;
|
||||
hls_suspend(HLS_SUSPEND_USER, false);
|
||||
}
|
||||
if (src == PLAYER_SRC_TONE || s_src == PLAYER_SRC_TONE) {
|
||||
s_target = s_src = src;
|
||||
s_flush = true;
|
||||
@@ -198,6 +204,9 @@ size_t player_write(player_src_t src, const int32_t *frames, size_t n)
|
||||
{
|
||||
size_t done = 0;
|
||||
while (done < n) {
|
||||
if (src == PLAYER_SRC_HLS && s_hls_paused) {
|
||||
return n; // paused: let the decoder finish the segment it is on
|
||||
}
|
||||
if (s_src != src) {
|
||||
if (s_target != src) {
|
||||
return n; // not this source's turn: drop
|
||||
@@ -327,7 +336,8 @@ static const char *source_state(player_src_t src)
|
||||
{
|
||||
return src == PLAYER_SRC_TONE ? "playing" : src == PLAYER_SRC_OFF ? "idle" :
|
||||
src == PLAYER_SRC_SPOTIFY && !spotify_session_active() ? "idle" :
|
||||
src == PLAYER_SRC_SPOTIFY && s_paused ? "paused" : s_playing ? "playing" : "buffering";
|
||||
src == PLAYER_SRC_SPOTIFY && s_paused ? "paused" :
|
||||
src == PLAYER_SRC_HLS && s_hls_paused ? "paused" : s_playing ? "playing" : "buffering";
|
||||
}
|
||||
|
||||
static void player_status(cJSON *st)
|
||||
@@ -373,7 +383,7 @@ static esp_err_t send_player(httpd_req_t *req)
|
||||
}
|
||||
cJSON_AddNumberToObject(o, "volume", s_volume_pct);
|
||||
cJSON *can = cJSON_AddObjectToObject(o, "can");
|
||||
cJSON_AddBoolToObject(can, "pause", sp);
|
||||
cJSON_AddBoolToObject(can, "pause", sp || src == PLAYER_SRC_HLS);
|
||||
cJSON_AddBoolToObject(can, "next", sp);
|
||||
cJSON_AddBoolToObject(can, "prev", sp);
|
||||
cJSON_AddBoolToObject(can, "seek", sp);
|
||||
@@ -387,6 +397,69 @@ static esp_err_t player_get(httpd_req_t *req)
|
||||
return send_player(req);
|
||||
}
|
||||
|
||||
static void hls_pause(bool pause)
|
||||
{
|
||||
if (pause == s_hls_paused) {
|
||||
return;
|
||||
}
|
||||
s_hls_paused = pause;
|
||||
hls_suspend(HLS_SUSPEND_USER, pause);
|
||||
s_flush = true; // live: resume rejoins at the live edge, never plays the old buffer
|
||||
ESP_LOGI(TAG, "hls %s", pause ? "paused" : "resumed");
|
||||
}
|
||||
|
||||
// Transport on the active source. Returns NULL, or the reason it can't be done (409).
|
||||
static const char *transport(const char *cmd)
|
||||
{
|
||||
player_src_t src = s_src;
|
||||
bool pause = !strcmp(cmd, "pause") || !strcmp(cmd, "stop");
|
||||
bool play = !strcmp(cmd, "play");
|
||||
bool next = !strcmp(cmd, "next"), prev = !strcmp(cmd, "prev");
|
||||
if (!strcmp(cmd, "toggle")) {
|
||||
bool paused = src == PLAYER_SRC_SPOTIFY ? s_paused : s_hls_paused;
|
||||
pause = !paused;
|
||||
play = paused;
|
||||
}
|
||||
if (src == PLAYER_SRC_SPOTIFY) {
|
||||
spotify_cmd_t c = pause ? SPOTIFY_CMD_PAUSE : play ? SPOTIFY_CMD_PLAY :
|
||||
next ? SPOTIFY_CMD_NEXT : SPOTIFY_CMD_PREV;
|
||||
return spotify_command(c) ? NULL : "no Spotify session";
|
||||
}
|
||||
if (src == PLAYER_SRC_HLS) {
|
||||
if (next || prev) {
|
||||
return "live stream: no next/previous";
|
||||
}
|
||||
hls_pause(pause);
|
||||
return NULL;
|
||||
}
|
||||
return "no player source active";
|
||||
}
|
||||
|
||||
// POST /api/player/<cmd>
|
||||
static esp_err_t player_post(httpd_req_t *req)
|
||||
{
|
||||
static const char *const TRANSPORT[] = { "play", "pause", "toggle", "stop", "next", "prev" };
|
||||
const char *cmd = req->uri + strlen("/api/player/");
|
||||
char name[16];
|
||||
size_t len = strcspn(cmd, "?");
|
||||
if (len >= sizeof(name)) {
|
||||
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "unknown player command");
|
||||
}
|
||||
memcpy(name, cmd, len);
|
||||
name[len] = '\0';
|
||||
for (int i = 0; i < sizeof(TRANSPORT) / sizeof(TRANSPORT[0]); i++) {
|
||||
if (!strcmp(name, TRANSPORT[i])) {
|
||||
const char *err = transport(name);
|
||||
if (err) {
|
||||
httpd_resp_set_status(req, "409 Conflict");
|
||||
return httpd_resp_sendstr(req, err);
|
||||
}
|
||||
return send_player(req);
|
||||
}
|
||||
}
|
||||
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "unknown player command");
|
||||
}
|
||||
|
||||
esp_err_t player_init(void)
|
||||
{
|
||||
esp_err_t err = audio_ring_init(RING_FRAMES, CHANNELS);
|
||||
@@ -416,10 +489,15 @@ esp_err_t player_init(void)
|
||||
if (xTaskCreate(control_task, "player_ctl", 3072, NULL, 4, NULL) != pdPASS) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
static const httpd_uri_t get = { .uri = "/api/player", .method = HTTP_GET, .handler = player_get };
|
||||
err = web_register_uri(&get);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
static const httpd_uri_t uris[] = {
|
||||
{ .uri = "/api/player", .method = HTTP_GET, .handler = player_get },
|
||||
{ .uri = "/api/player/*", .method = HTTP_POST, .handler = player_post },
|
||||
};
|
||||
for (int i = 0; i < sizeof(uris) / sizeof(uris[0]); i++) {
|
||||
err = web_register_uri(&uris[i]);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
return status_register(player_status);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user