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:
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 ----- */
|
||||||
|
|||||||
Reference in New Issue
Block a user