status: hls download stats (last segment speed, failed requests, last error)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 20:09:58 +10:00
parent 7419cde174
commit 856d062269
4 changed files with 54 additions and 3 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ source (cspot Spotify Connect | HLS player) -> decode (Vorbis/AAC/MP3) -> SRC 44
## Project config group ## Project config group
- source: {mode: spotify|hls|auto|tone|pink|off, spotify_name, spotify_bitrate, hls_url, autoplay, gain_db, failover_delay_s, failover_on_pause, spotify_client_id, spotify_client_secret} - source: {mode: spotify|hls|auto|tone|pink|off, spotify_name, spotify_bitrate, hls_url, autoplay, gain_db, failover_delay_s, failover_on_pause, spotify_client_id, spotify_client_secret}
- spotify_client_id / spotify_client_secret: each user's own Spotify developer app (developer.spotify.com, Premium account), needed by the maintained cspot fork (philippe44/cspot) since Spotify's 2025 API restrictions. The secret is write-only (never returned by GET /api/config). spotify_state reports "no client credentials" while either is missing. - spotify_client_id / spotify_client_secret: each user's own Spotify developer app (developer.spotify.com, Premium account), needed by the maintained cspot fork (philippe44/cspot) since Spotify's 2025 API restrictions. The secret is write-only (never returned by GET /api/config). spotify_state reports "no client credentials" while either is missing.
- Project status fields: active_source, source_state, spotify_state, buffer_ms; spotify_error (last session error with the uptime it happened at, e.g. "Can't connect to spotify servers (at 412 s)"; only present after one) - Project status fields: active_source, source_state, spotify_state, buffer_ms; spotify_error (last session error with the uptime it happened at, e.g. "Can't connect to spotify servers (at 412 s)"; only present after one); hls {seg_bytes, seg_ms, seg_mbit (last segment download incl. connect + TLS), errors (failed requests since boot), error (last one with its uptime, only present after one)}
- mode "tone": the core's 1 kHz / -18 dBFS test tone, phase-locked to PTP (commissioning, e.g. Riedel import tests). "pink": the core's pink noise, -18 dBFS RMS (peaks about -6 dBFS), same on L and R (level/EQ checks). Neither is affected by volume or gain. "off": silence. - mode "tone": the core's 1 kHz / -18 dBFS test tone, phase-locked to PTP (commissioning, e.g. Riedel import tests). "pink": the core's pink noise, -18 dBFS RMS (peaks about -6 dBFS), same on L and R (level/EQ checks). Neither is affected by volume or gain. "off": silence.
## Player control API (project routes) ## Player control API (project routes)
+49 -2
View File
@@ -1,9 +1,11 @@
#include "hls.h" #include "hls.h"
#include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "cJSON.h"
#include "esp_crt_bundle.h" #include "esp_crt_bundle.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_http_client.h" #include "esp_http_client.h"
@@ -39,6 +41,44 @@ static char s_media_url[URL_MAX];
static uint8_t *s_seg; // current segment (PSRAM) static uint8_t *s_seg; // current segment (PSRAM)
static volatile unsigned s_suspended; // HLS_SUSPEND_* bits static volatile unsigned s_suspended; // HLS_SUSPEND_* bits
// Download statistics for the status (hls_status), guarded by s_stat_lock.
static portMUX_TYPE s_stat_lock = portMUX_INITIALIZER_UNLOCKED;
static struct {
uint32_t seg_bytes, seg_ms; // last complete segment
uint32_t errors; // failed requests since boot
char error[128]; // last one, with the uptime it happened at
} s_stat;
// Log a failed request and keep it for the status.
static void fail(const char *fmt, ...)
{
char msg[96];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
ESP_LOGW(TAG, "%s", msg);
taskENTER_CRITICAL(&s_stat_lock);
s_stat.errors++;
snprintf(s_stat.error, sizeof(s_stat.error), "%s (at %lld s)", msg, esp_timer_get_time() / 1000000);
taskEXIT_CRITICAL(&s_stat_lock);
}
void hls_status(cJSON *st)
{
taskENTER_CRITICAL(&s_stat_lock);
typeof(s_stat) t = s_stat;
taskEXIT_CRITICAL(&s_stat_lock);
cJSON *o = cJSON_AddObjectToObject(st, "hls");
cJSON_AddNumberToObject(o, "seg_bytes", t.seg_bytes);
cJSON_AddNumberToObject(o, "seg_ms", t.seg_ms);
cJSON_AddNumberToObject(o, "seg_mbit", t.seg_ms ? t.seg_bytes * 8.0 / t.seg_ms / 1000 : 0);
cJSON_AddNumberToObject(o, "errors", t.errors);
if (t.error[0]) {
cJSON_AddStringToObject(o, "error", t.error);
}
}
void hls_suspend(unsigned reason, bool suspend) void hls_suspend(unsigned reason, bool suspend)
{ {
if (suspend) { if (suspend) {
@@ -89,7 +129,7 @@ static esp_http_client_handle_t http_open(const char *url)
for (int i = 0; i <= MAX_REDIRECTS; i++) { for (int i = 0; i <= MAX_REDIRECTS; i++) {
esp_err_t err = esp_http_client_open(c, 0); esp_err_t err = esp_http_client_open(c, 0);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "open %s: %s", url, esp_err_to_name(err)); fail("open %s: %s", url, esp_err_to_name(err));
break; break;
} }
esp_http_client_fetch_headers(c); esp_http_client_fetch_headers(c);
@@ -103,7 +143,7 @@ static esp_http_client_handle_t http_open(const char *url)
if (st == 200) { if (st == 200) {
return c; return c;
} }
ESP_LOGW(TAG, "GET %s: HTTP %d", url, st); fail("GET %s: HTTP %d", url, st);
break; break;
} }
esp_http_client_cleanup(c); esp_http_client_cleanup(c);
@@ -238,10 +278,17 @@ static bool fetch_segment(long long seq, const char *url)
} }
if (r < 0 || total == SEG_MAX) { if (r < 0 || total == SEG_MAX) {
ok = false; ok = false;
fail("segment %lld: %s after %u bytes", seq, r < 0 ? "read error" : "too large", (unsigned)total);
} }
esp_http_client_close(c); esp_http_client_close(c);
esp_http_client_cleanup(c); esp_http_client_cleanup(c);
int64_t us = esp_timer_get_time() - t0; int64_t us = esp_timer_get_time() - t0;
if (ok) {
taskENTER_CRITICAL(&s_stat_lock);
s_stat.seg_bytes = total;
s_stat.seg_ms = us / 1000;
taskEXIT_CRITICAL(&s_stat_lock);
}
ESP_LOGD(TAG, "segment %lld: %u bytes in %.2f s (%.1f Mbit/s)%s", seq, (unsigned)total, us / 1e6, ESP_LOGD(TAG, "segment %lld: %u bytes in %.2f s (%.1f Mbit/s)%s", seq, (unsigned)total, us / 1e6,
us ? total * 8.0 / us : 0.0, ok ? "" : ", failed"); us ? total * 8.0 / us : 0.0, ok ? "" : ", failed");
if (ok && s_sink) { if (ok && s_sink) {
+3
View File
@@ -5,6 +5,7 @@
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include "cJSON.h"
#include "esp_err.h" #include "esp_err.h"
// Receives segment data in chunks (MPEG-TS etc.). Return false to abort the segment. // Receives segment data in chunks (MPEG-TS etc.). Return false to abort the segment.
@@ -13,6 +14,8 @@ typedef bool (*hls_sink_t)(const uint8_t *data, size_t len, bool segment_start);
#define HLS_URL_MAX 384 #define HLS_URL_MAX 384
esp_err_t hls_start(hls_sink_t sink); esp_err_t hls_start(hls_sink_t sink);
// Adds status.hls: last segment download (bytes, ms, Mbit/s incl. connect + TLS), failed requests.
void hls_status(cJSON *st);
// The playlist URL to play ("" = none). A different URL restarts fetching with it. // The playlist URL to play ("" = none). A different URL restarts fetching with it.
void hls_set_url(const char *url); void hls_set_url(const char *url);
// Stop fetching (current download is abandoned) until resumed. Reasons are independent: // Stop fetching (current download is abandoned) until resumed. Reasons are independent:
+1
View File
@@ -395,6 +395,7 @@ static void player_status(cJSON *st)
cJSON_AddStringToObject(st, "spotify_error", err); cJSON_AddStringToObject(st, "spotify_error", err);
} }
cJSON_AddNumberToObject(st, "buffer_ms", (double)(audio_ring_level() * 1000 / RATE)); cJSON_AddNumberToObject(st, "buffer_ms", (double)(audio_ring_level() * 1000 / RATE));
hls_status(st);
} }
/* ----- Player API ----- */ /* ----- Player API ----- */