From 856d0622698dce3a77d81c2d95821d4cf1012d63 Mon Sep 17 00:00:00 2001 From: Ben Nicholson Date: Fri, 2 Oct 2026 20:09:58 +1000 Subject: [PATCH] status: hls download stats (last segment speed, failed requests, last error) Co-Authored-By: Claude Opus 5.5 --- docs/hardware-and-design-notes.md | 2 +- main/hls.c | 51 +++++++++++++++++++++++++++++-- main/hls.h | 3 ++ main/player.c | 1 + 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/docs/hardware-and-design-notes.md b/docs/hardware-and-design-notes.md index 16e90f6..c907371 100644 --- a/docs/hardware-and-design-notes.md +++ b/docs/hardware-and-design-notes.md @@ -33,7 +33,7 @@ source (cspot Spotify Connect | HLS player) -> decode (Vorbis/AAC/MP3) -> SRC 44 ## 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} - 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. ## Player control API (project routes) diff --git a/main/hls.c b/main/hls.c index cb0bde2..4faf895 100644 --- a/main/hls.c +++ b/main/hls.c @@ -1,9 +1,11 @@ #include "hls.h" +#include #include #include #include +#include "cJSON.h" #include "esp_crt_bundle.h" #include "esp_heap_caps.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 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) { 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++) { esp_err_t err = esp_http_client_open(c, 0); 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; } esp_http_client_fetch_headers(c); @@ -103,7 +143,7 @@ static esp_http_client_handle_t http_open(const char *url) if (st == 200) { return c; } - ESP_LOGW(TAG, "GET %s: HTTP %d", url, st); + fail("GET %s: HTTP %d", url, st); break; } 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) { 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_cleanup(c); 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, us ? total * 8.0 / us : 0.0, ok ? "" : ", failed"); if (ok && s_sink) { diff --git a/main/hls.h b/main/hls.h index dcde933..7252bfa 100644 --- a/main/hls.h +++ b/main/hls.h @@ -5,6 +5,7 @@ #include #include +#include "cJSON.h" #include "esp_err.h" // 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 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. void hls_set_url(const char *url); // Stop fetching (current download is abandoned) until resumed. Reasons are independent: diff --git a/main/player.c b/main/player.c index 904a143..065e62e 100644 --- a/main/player.c +++ b/main/player.c @@ -395,6 +395,7 @@ static void player_status(cJSON *st) cJSON_AddStringToObject(st, "spotify_error", err); } cJSON_AddNumberToObject(st, "buffer_ms", (double)(audio_ring_level() * 1000 / RATE)); + hls_status(st); } /* ----- Player API ----- */