- 去标题栏(不再用 ncm_topbar);返回=左缘手势/系统返回钮语义 - 双页结构:封面页/歌词页,屏幕正中左/右滑切换(与左缘≤24px返回、顶部通知分流), 页面指示点显示当前页;中部横滑=on_touch_move 锁存 page-drag + on_tap/on_swipe 判向 - 封面页:方形大图+圆角(无旋转动画)+进度条(点 seek)+MD3 矢量控制钮(prev/play/pause/next, 新增 app/ui/ncm_icons.h 32x32 单通道 alpha,参照 sb_icons.h 范式)+元信息+实际音质档 - 歌词页:LRC 滚动(当前行居中高亮、上下渐隐)+点行跳转(新增 lyric_line_time); 默认=封面模糊置底(封面铺底+主题 scrim),省电=封面背景+黑色半透明叠加层(设置 ncm.player_bg_mode) - 日夜间:主题配色只映射到半透明叠加层/scrim/accent(红),不动沉浸底(NCM_PLAYER_BG 恒定) - 分带刷新沿用:封面页=封面静态带+动态带(互不重叠 bbox,tick 只刷动态带);歌词页换行(~5s)才重画 - 性能:宿主 4 页 PPM 核验+手势自检 PASS;mipsel -Wall -Wextra 0 warning;qemu core 回归 PASS; 0 真实请求(封面/歌词用本地样本注入)。未部署真机(主会话统一) md5: ncm 730e0b6e / ncm_host 500ac85 / player.c 00934de / ncm_icons.h 20f0bba
251 lines
10 KiB
C
251 lines
10 KiB
C
/* logic/cover.c — 封面下载(HTTPS)+ 限幅 + 解码缓存(单张,URL 变更即换;后台线程)。
|
||
*
|
||
* 内存纪律:下载限幅 NCM_COVER_MAX(~288KB);解码后的 RGBA 保持单张(换封面先 free 旧再解码)。
|
||
* 线程模型:一个持久的 cover 工作线程负责「目标 URL → 下载 → 解码 → 换入」;UI 线程只经
|
||
* cover_request(url)(提交目标)与 cover_get()/cover_changed()(读取/判变)+ cover_ack()(消费)。
|
||
* 工作线程不碰 meow_ui(绘制在 UI 线程 tick 判 cover_changed 后触发 invalidate)。
|
||
*
|
||
* §18 修复(切歌封面不更新/CPU 忙等根因):
|
||
* - worker 换入成功后**清目标**(g_tgt_url[0]=0)转怠速——旧版成功后不清目标,每圈
|
||
* 缓存读+JPEG 解码+换入+g_gen++ 无 sleep 死循环:CPU 占满、cover_changed() 恒真导致
|
||
* 播放页每帧整盘重绘,慢网络下新封面请求被饿到表现如「切歌封面不更新」。
|
||
* - 下载/解码失败**指数退避**重试(0.5s→4s),连续 8 次放弃(UI 重进页会重新提交目标);
|
||
* 失败写 WARN 日志(排查资产),不再无声死等。
|
||
* - 绘制安全:cover_get() 持锁返回、cover_put() 解锁——旧版 UI 绘制期 worker 可 free
|
||
* 同一 meow_img(use-after-free);调用方须在 cover_get 后配对 cover_put。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <unistd.h>
|
||
#include <sys/stat.h>
|
||
|
||
static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
|
||
static meow_img *g_cover_img;
|
||
static char g_cover_url[512]; /* g_cover_img 对应的 URL */
|
||
static char g_tgt_url[512]; /* 目标 URL(worker 拉取) */
|
||
static pthread_t g_thr;
|
||
static int g_thr_on;
|
||
static int g_quit;
|
||
static volatile int g_gen; /* 每次换入 +1 */
|
||
static int g_acked; /* UI 已消费的版本 */
|
||
static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */
|
||
|
||
static unsigned char g_buf[NCM_COVER_MAX]; /* 单张下载缓冲(常驻) */
|
||
|
||
static meow_img *decode_cover(const char *url, size_t *out_len)
|
||
{
|
||
if (!url || !url[0]) return NULL;
|
||
ncm_http_req_t req;
|
||
memset(&req, 0, sizeof req);
|
||
req.url = url;
|
||
req.method = "GET";
|
||
req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
|
||
req.timeout_ms = 10000;
|
||
ncm_http_resp_t resp;
|
||
int rc = ncm_http_request(&req, (char *)g_buf, sizeof g_buf, &resp);
|
||
if (rc != NCM_OK || resp.status != 200 || resp.body_len == 0) return NULL;
|
||
if (out_len) *out_len = resp.body_len;
|
||
return meow_img_load_mem((const void *)resp.body, (int)resp.body_len);
|
||
}
|
||
|
||
/* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。
|
||
* 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件
|
||
* `song_cov<hex>.jpg`,与 mp3(song_<id>.mp3)同池,lookup/commit 刷新 mtime(LRU)。 */
|
||
static void cover_cache_id(const char *url, char *out, size_t out_sz)
|
||
{
|
||
unsigned long h = 5381;
|
||
for (const char *p = url; *p; p++) h = ((h << 5) + h) + (unsigned char)*p;
|
||
snprintf(out, out_sz, "cov%lx", h);
|
||
}
|
||
|
||
/* 读整文件到 buf(< cap),成功返回 1,写入 *len。 */
|
||
static int read_file_into_buf(const char *path, unsigned char *buf, size_t cap, size_t *len)
|
||
{
|
||
FILE *f = fopen(path, "rb");
|
||
if (!f) return 0;
|
||
size_t n = fread(buf, 1, cap, f);
|
||
fclose(f);
|
||
if (n == 0) return 0;
|
||
if (len) *len = n;
|
||
return 1;
|
||
}
|
||
|
||
/* 写 cover_current.jpg 供 launcher 媒体卡片读(兼容保留)。调用方须已持有 g_cm
|
||
* (早前版本在此内再 lock g_cm → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */
|
||
static void save_cover_raw(const char *url, size_t len)
|
||
{
|
||
if (!url || len == 0) return;
|
||
if (g_st.cache_dir[0]) {
|
||
char path[NCM_CACHE_DIR_MAX + 32]; /* >512:宿主 glibc fortify 对 %s 展开宽证明可放 */
|
||
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
|
||
ncm_store_write_file(path, g_buf, len);
|
||
ncm_strlcpy(g_cover_path, path, sizeof g_cover_path);
|
||
}
|
||
}
|
||
|
||
#define COVER_RETRY_MAX 8 /* 连续失败放弃次数(UI 重进页/切歌会重新提交目标) */
|
||
|
||
static void *cover_worker(void *arg)
|
||
{
|
||
(void)arg;
|
||
int backoff_ms = 0, fails = 0;
|
||
while (!g_quit) {
|
||
pthread_mutex_lock(&g_cm);
|
||
if (!g_tgt_url[0]) {
|
||
pthread_mutex_unlock(&g_cm);
|
||
backoff_ms = 0; fails = 0;
|
||
usleep(250 * 1000);
|
||
continue;
|
||
}
|
||
char url[512];
|
||
ncm_strlcpy(url, g_tgt_url, sizeof url);
|
||
pthread_mutex_unlock(&g_cm);
|
||
|
||
char key[32];
|
||
cover_cache_id(url, key, sizeof key);
|
||
size_t blen = 0;
|
||
meow_img *img = NULL;
|
||
|
||
/* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */
|
||
char cpath[512];
|
||
if (g_st.ctx.cache.dir[0] && ncm_cache_lookup(&g_st.ctx.cache, key, ".jpg",
|
||
cpath, sizeof cpath) == NCM_OK &&
|
||
read_file_into_buf(cpath, g_buf, sizeof g_buf, &blen)) {
|
||
img = meow_img_load_mem(g_buf, (int)blen);
|
||
}
|
||
/* ② 未命中 → 下载解码;成功后落进 ncm_cache(LRU 池,超限由 ncm_cache_evict 淘汰) */
|
||
if (!img) {
|
||
blen = 0;
|
||
img = decode_cover(url, &blen);
|
||
if (!img) {
|
||
/* 失败:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等;
|
||
* 目标保留期间 UI 每帧重请求由 cover_request 去重挡住)。 */
|
||
fails++;
|
||
if (fails >= COVER_RETRY_MAX) {
|
||
meow_log(MEOW_LOG_WARN, "cover: give up after %d fails url=%.64s", fails, url);
|
||
pthread_mutex_lock(&g_cm);
|
||
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0;
|
||
pthread_mutex_unlock(&g_cm);
|
||
backoff_ms = 0; fails = 0;
|
||
continue;
|
||
}
|
||
meow_log(MEOW_LOG_WARN, "cover: fetch failed (%d) url=%.64s retry in %dms",
|
||
fails, url, backoff_ms ? backoff_ms : 500);
|
||
if (g_app_quit) return NULL;
|
||
usleep(backoff_ms ? backoff_ms * 1000 : 500 * 1000);
|
||
backoff_ms = backoff_ms ? (backoff_ms * 2 > 4000 ? 4000 : backoff_ms * 2) : 500;
|
||
continue;
|
||
}
|
||
if (g_st.ctx.cache.dir[0]) {
|
||
/* 写前检查:池大小超限先按 LRU 淘汰(与 mp3 同池;封面约 200KB,先腾空间) */
|
||
ncm_cache_evict(&g_st.ctx.cache);
|
||
ncm_cache_writer_t *w = NULL;
|
||
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
|
||
ncm_cache_write(w, g_buf, blen);
|
||
ncm_cache_commit(w);
|
||
/* 写后检查:新封面可能把池推过上限 → 再按 LRU 淘汰最旧 */
|
||
ncm_cache_evict(&g_st.ctx.cache);
|
||
}
|
||
}
|
||
}
|
||
backoff_ms = 0; fails = 0;
|
||
|
||
pthread_mutex_lock(&g_cm);
|
||
/* 目标仍是 url 才换入(期间未提交更早) */
|
||
if (g_tgt_url[0] && strcmp(g_tgt_url, url) == 0) {
|
||
if (g_cover_img) meow_img_free(g_cover_img);
|
||
g_cover_img = img;
|
||
ncm_strlcpy(g_cover_url, url, sizeof g_cover_url);
|
||
save_cover_raw(url, blen);
|
||
g_gen++;
|
||
g_tgt_url[0] = 0; /* 目标达成即清:worker 转怠速(修忙等死循环,§18) */
|
||
} else {
|
||
meow_img_free(img);
|
||
}
|
||
pthread_mutex_unlock(&g_cm);
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。 */
|
||
void cover_request(const char *url)
|
||
{
|
||
if (!url || !url[0]) return;
|
||
if (g_host_mock) return; /* 宿主 mock = 0 网络纪律(mock URL 不得发真实 HTTPS) */
|
||
pthread_mutex_lock(&g_cm);
|
||
/* 去重:已在展示 或 已在拉取途中的同一 URL 不重复提交。 */
|
||
if ((g_cover_url[0] && strcmp(g_cover_url, url) == 0) ||
|
||
(g_tgt_url[0] && strcmp(g_tgt_url, url) == 0)) { pthread_mutex_unlock(&g_cm); return; }
|
||
ncm_strlcpy(g_tgt_url, url, sizeof g_tgt_url);
|
||
pthread_mutex_unlock(&g_cm);
|
||
if (!g_thr_on) {
|
||
g_thr_on = 1;
|
||
if (pthread_create(&g_thr, NULL, cover_worker, NULL) == 0)
|
||
pthread_detach(g_thr);
|
||
else
|
||
g_thr_on = 0;
|
||
}
|
||
}
|
||
|
||
/* UI 线程:读取当前封面——**持锁返回**,绘制完必须配对 cover_put()(§18 修 UAF:
|
||
* 旧版 get 后无锁绘制,worker 换入时 free 同一张图 → use-after-free)。 */
|
||
meow_img *cover_get(void)
|
||
{
|
||
pthread_mutex_lock(&g_cm);
|
||
return g_cover_img;
|
||
}
|
||
|
||
void cover_put(void)
|
||
{
|
||
pthread_mutex_unlock(&g_cm);
|
||
}
|
||
|
||
/* 是否有新封面待绘制(UI tick 判变)。 */
|
||
int cover_changed(void) { return g_gen != g_acked; }
|
||
/* UI 消费一次:标记已见当前版本。 */
|
||
void cover_ack(void) { g_acked = g_gen; }
|
||
|
||
/* 封面原始文件路径(供 launcher 媒体卡片读);NULL=无。 */
|
||
const char *cover_current_path(void)
|
||
{
|
||
const char *p;
|
||
pthread_mutex_lock(&g_cm);
|
||
p = g_cover_path[0] ? g_cover_path : NULL;
|
||
pthread_mutex_unlock(&g_cm);
|
||
return p;
|
||
}
|
||
|
||
void cover_cache_reset(void)
|
||
{
|
||
pthread_mutex_lock(&g_cm);
|
||
if (g_cover_img) { meow_img_free(g_cover_img); g_cover_img = NULL; }
|
||
g_cover_url[0] = 0;
|
||
g_tgt_url[0] = 0;
|
||
g_cover_path[0] = 0;
|
||
pthread_mutex_unlock(&g_cm);
|
||
g_gen++;
|
||
g_acked = g_gen;
|
||
}
|
||
|
||
#ifdef MEOW_UI_HOST_TEST
|
||
/* 宿主测试桩(仅宿主构建;产品构建无此符号):注入一张本地封面图(不经网络),
|
||
* 验证封面页方形圆角大图与歌词页模糊置底路径(§22)。0 真实请求纪律不变。 */
|
||
void cover_host_inject(const char *path)
|
||
{
|
||
if (!path || !path[0]) return;
|
||
meow_img *img = meow_img_load_file(path);
|
||
if (!img) { meow_log(MEOW_LOG_WARN, "cover host inject: load failed %s", path); return; }
|
||
pthread_mutex_lock(&g_cm);
|
||
if (g_cover_img) meow_img_free(g_cover_img);
|
||
g_cover_img = img;
|
||
ncm_strlcpy(g_cover_url, path, sizeof g_cover_url);
|
||
g_cover_path[0] = 0;
|
||
g_gen++;
|
||
pthread_mutex_unlock(&g_cm);
|
||
}
|
||
#endif
|