Player: runtime source override and play-a-URL on /api/player

The player now owns the effective mode (config or override) and passes it
down: spotify_apply(enable) and hls_set_url(url) instead of both reading
source.mode from the config. /api/player/source forces spotify|hls|off
(config returns to the saved mode), /api/player/url plays an m3u8 now;
neither is saved. GET reports it in `forced`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 23:15:38 +10:00
parent fd88e02e96
commit e5c3c0ad8a
6 changed files with 105 additions and 31 deletions
+73 -8
View File
@@ -29,6 +29,7 @@ static const char *TAG = "player";
typedef enum { MODE_TONE, MODE_OFF, MODE_HLS, MODE_SPOTIFY, MODE_AUTO } mode_t_;
static const char *const SRC_NAME[] = { "tone", "off", "hls", "spotify" };
static const char *const MODE_NAME[] = { "tone", "off", "hls", "spotify", "auto" };
static volatile mode_t_ s_mode = MODE_OFF;
static volatile player_src_t s_src = PLAYER_SRC_OFF; // what is playing now
@@ -40,6 +41,12 @@ static volatile bool s_sp_playing; // Spotify has started playback and is n
static volatile bool s_hls_paused; // HLS paused via the API: not fetching, silence
static audio_conv_t *s_spotify_conv;
// Effective mode: the config's, or a runtime override from the API (not saved). Web server task.
static mode_t_ s_cfg_mode = MODE_OFF;
static char s_cfg_url[HLS_URL_MAX];
static int s_forced = -1; // mode forced via /api/player/source|url, -1 = none
static char s_forced_url[HLS_URL_MAX]; // from /api/player/url; "" = the config's URL
// Failover (mode auto)
static volatile int s_failover_delay_s = 5;
static volatile bool s_failover_on_pause;
@@ -173,14 +180,22 @@ static void select_source(player_src_t src)
s_target = src; // player_read fades out and switches
}
void player_apply(const cJSON *source)
// Apply the effective mode: Spotify on/off, the HLS URL, and the source to play.
static void apply_mode(void)
{
if (s_spotify_conv) { // after player_init: Spotify follows mode / name / credentials live
spotify_apply();
static char applied_url[HLS_URL_MAX];
mode_t_ mode = s_forced >= 0 ? (mode_t_)s_forced : s_cfg_mode;
if (s_spotify_conv) { // after spotify_init: follows mode / name / credentials live
spotify_apply(mode == MODE_SPOTIFY || mode == MODE_AUTO);
}
const char *url = mode != MODE_HLS && mode != MODE_AUTO ? "" : s_forced_url[0] ? s_forced_url : s_cfg_url;
if (strcmp(url, applied_url)) {
strlcpy(applied_url, url, sizeof(applied_url));
hls_set_url(url);
if (s_src == PLAYER_SRC_HLS) {
s_flush = true; // another stream: don't finish the old one's buffer
}
}
s_failover_delay_s = (int)cJSON_GetObjectItemCaseSensitive(source, "failover_delay_s")->valuedouble;
s_failover_on_pause = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(source, "failover_on_pause"));
mode_t_ mode = mode_of(cJSON_GetObjectItemCaseSensitive(source, "mode")->valuestring);
if (mode == s_mode) {
return;
}
@@ -196,6 +211,15 @@ void player_apply(const cJSON *source)
}
}
void player_apply(const cJSON *source)
{
s_failover_delay_s = (int)cJSON_GetObjectItemCaseSensitive(source, "failover_delay_s")->valuedouble;
s_failover_on_pause = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(source, "failover_on_pause"));
s_cfg_mode = mode_of(cJSON_GetObjectItemCaseSensitive(source, "mode")->valuestring);
strlcpy(s_cfg_url, cJSON_GetObjectItemCaseSensitive(source, "hls_url")->valuestring, sizeof(s_cfg_url));
apply_mode();
}
/* ----- source side ----- */
// Write converted 48 kHz frames, waiting for space at playback speed. Frames of a source that is
@@ -369,7 +393,7 @@ static esp_err_t send_player(httpd_req_t *req)
bool sp = src == PLAYER_SRC_SPOTIFY && spotify_now_playing(&t);
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "source", SRC_NAME[src]);
cJSON_AddNullToObject(o, "forced");
add_str_or_null(o, "forced", s_forced >= 0 ? MODE_NAME[s_forced] : NULL);
cJSON_AddStringToObject(o, "state", src == PLAYER_SRC_OFF ? "stopped" : source_state(src));
add_str_or_null(o, "artist", sp ? t.artist : NULL);
add_str_or_null(o, "title", sp ? t.title : NULL);
@@ -438,7 +462,7 @@ static const char *transport(const char *cmd)
// Small JSON body of a POST; NULL if missing or invalid.
static cJSON *read_body(httpd_req_t *req)
{
char buf[128];
char buf[HLS_URL_MAX + 128]; // fits {"url": ...}
if (req->content_len == 0 || req->content_len >= sizeof(buf)) {
return NULL;
}
@@ -490,6 +514,43 @@ static esp_err_t seek_or_volume(httpd_req_t *req, bool seek)
return send_player(req);
}
// POST /api/player/source {source: spotify|hls|off|config}, /api/player/url {url}: runtime override
static esp_err_t force_source(httpd_req_t *req, bool url)
{
cJSON *body = read_body(req);
const cJSON *v = cJSON_GetObjectItemCaseSensitive(body, url ? "url" : "source");
const char *s = cJSON_IsString(v) ? v->valuestring : "";
const char *err = NULL;
if (url) {
if (strncmp(s, "http://", 7) && strncmp(s, "https://", 8)) {
err = "expected {\"url\": \"http(s)://...m3u8\"}";
} else if (strlen(s) >= sizeof(s_forced_url)) {
err = "url too long";
} else {
s_forced = MODE_HLS;
strlcpy(s_forced_url, s, sizeof(s_forced_url));
if (s_hls_paused && s_src == PLAYER_SRC_HLS) {
hls_pause(false); // a new stream starts playing
}
}
} else if (!strcmp(s, "config")) {
s_forced = -1;
s_forced_url[0] = '\0';
} else if (!strcmp(s, "spotify") || !strcmp(s, "hls") || !strcmp(s, "off")) {
s_forced = mode_of(s);
s_forced_url[0] = '\0';
} else {
err = "expected {\"source\": \"spotify|hls|off|config\"}";
}
if (!err) {
ESP_LOGI(TAG, "override: %s%s%s", s_forced >= 0 ? MODE_NAME[s_forced] : "none (config)",
s_forced_url[0] ? " " : "", s_forced_url);
apply_mode();
}
cJSON_Delete(body);
return err ? httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, err) : send_player(req);
}
// POST /api/player/<cmd>
static esp_err_t player_post(httpd_req_t *req)
{
@@ -511,6 +572,9 @@ static esp_err_t player_post(httpd_req_t *req)
if (!strcmp(name, "seek") || !strcmp(name, "volume")) {
return seek_or_volume(req, name[0] == 's');
}
if (!strcmp(name, "source") || !strcmp(name, "url")) {
return force_source(req, name[0] == 'u');
}
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "unknown player command");
}
@@ -535,6 +599,7 @@ esp_err_t player_init(void)
return ESP_ERR_NO_MEM;
}
spotify_init(spotify_pcm, spotify_event, audio_ring_written, audio_ring_read_pos);
apply_mode(); // now with Spotify
aes67_ota_on_update(on_ota);
err = hls_start(decoder_feed);
if (err != ESP_OK) {