Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/logic/cover.c
T
MeowOS 7bd6962ccb feat(ncm §22): 播放页重做——封面页/歌词页双页(NCM_CACHE_DESIGN §6 定案)
- 去标题栏(不再用 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
2026-09-01 18:52:14 +08:00

251 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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