fix(ncm §33): 流媒专项批六项——流式SIGSEGV防御日志/档位保真(无损级必须flac+显示档按实际)/封面4xx不重试/收藏显隐/缓存命名迁移/topbar位图图标
- §2① ncm_api.c level_attempt: 无损级(hires/lossless及以上)命中必须 type=flac,返回 mp3 视为该档不可用续降 (修「hires 档静默给 mp3 被当命中」) - §2② player_manager.c eff_level_for_resp: 显示档按响应实际格式/码率——mp3 绝不显示 Hi-Res(≤128k→标准, 否则极高), flac 采信链 eff; br 未知钳 ≤EXHIGH - §1 流式 SIGSEGV(《十面埋伏》sig=11) 审计+UBSAN 探针未复现 → 落地防御日志(dl start/done/interrupt + play done fin/exp/fail/underrun) - §3 cover.c: download_cover 回填 rc/status; 确定性 4xx(404等)不重试立即跳下一张, 仅瞬时错误退避(rc=%d status=%d 日志) - §4 playlist.c: pl_show_fav 我创建(is_collected==0)隐藏收藏钮, 我收藏保留, 搜索歌单保守显示(评论槽并入无空洞) - §5 ncm_cache.c 双点→单点迁移 + player_manager song_local_file 旧键→当前档位键(不重下) - §6 home.c: topbar 动作 ⚙↻⌕ 迁移 sb_icons 位图(meow_topappbar_set_item_icon, 修 4k 字库豆腐) - 测试: test_ncm_core 新 test_grade_flac_gate + test_cache 双点迁移断言; degrade_sim/player_flow_sim 桩 flac 化 - 验证: stream/player_flow/window/search/cache_evict/degrade sim 全 PASS + test_ncm_core host+mipsel(qemu) + mem_sim + build_ncm.sh 0 warning - 新 ncm md5 cb818786 (text 1017110/bss 1362500), 未部署留主会话; 0 真实 NCM 业务请求
This commit is contained in:
1 parent
68ce46a5d8
commit
44a790dc1d
13 files changed
+425
-24
No files matched your search
@@ -66,8 +66,10 @@ static int cover_sink_write(void *ctx, const unsigned char *data, size_t len)
|
||||
}
|
||||
|
||||
/* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。
|
||||
* 失败返回 NULL(不 commit,调用方 abort)。写入 *out_len(body 字节数)。 */
|
||||
static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len)
|
||||
* 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status
|
||||
* 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3)。 */
|
||||
static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len,
|
||||
int *out_rc, int *out_status)
|
||||
{
|
||||
ncm_http_req_t req;
|
||||
memset(&req, 0, sizeof req);
|
||||
@@ -80,6 +82,8 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *
|
||||
ncm_http_resp_t resp;
|
||||
cover_sink_t sink = { w, 0 };
|
||||
int rc = ncm_http_download(&req, cover_sink_write, &sink, NCM_COVER_MAX, &resp);
|
||||
if (out_rc) *out_rc = rc;
|
||||
if (out_status) *out_status = resp.status;
|
||||
if (rc != NCM_OK || resp.status != 200 || sink.total == 0) return NULL;
|
||||
if (out_len) *out_len = sink.total;
|
||||
/* 从写者临时文件解码(未 commit,文件仍在 .tmp;解码到内存 RGBA,与落盘无关) */
|
||||
@@ -150,6 +154,7 @@ static void *cover_worker(void *arg)
|
||||
cover_cache_id(url, key, sizeof key);
|
||||
size_t blen = 0;
|
||||
meow_img *img = NULL;
|
||||
int drc = NCM_ERR_NET, dstatus = 0; /* 本目标最近一次下载 rc/HTTP status(4xx 判定用) */
|
||||
|
||||
/* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */
|
||||
char cpath[512];
|
||||
@@ -165,7 +170,7 @@ static void *cover_worker(void *arg)
|
||||
ncm_cache_writer_t *w = NULL;
|
||||
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
|
||||
blen = 0;
|
||||
img = download_cover(url, w, &blen);
|
||||
img = download_cover(url, w, &blen, &drc, &dstatus);
|
||||
if (img) {
|
||||
pthread_mutex_lock(&g_cm);
|
||||
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
|
||||
@@ -184,7 +189,20 @@ static void *cover_worker(void *arg)
|
||||
/* 内存瘦身:全尺寸封面解码后降采样到 ≤480px 驻留(省 ~7MB anon,OOM 修复)。 */
|
||||
if (img) img = cover_cap(img);
|
||||
if (!img) {
|
||||
/* 失败:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等;
|
||||
/* 失败分诊(REPORT §NEW 任务3):确定性 4xx(404 死链等)**不重试**,立即清目标
|
||||
* 跳下一张(重试也白费);仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
|
||||
* 日志打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么)。 */
|
||||
int deterministic = (dstatus >= 400 && dstatus < 500 && dstatus != 408 && dstatus != 429);
|
||||
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d%s",
|
||||
url, drc, dstatus, deterministic ? " (4xx, no retry)" : "");
|
||||
if (deterministic) {
|
||||
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;
|
||||
}
|
||||
/* 瞬时错误:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等;
|
||||
* 目标保留期间 UI 每帧重请求由 cover_request 去重挡住)。 */
|
||||
fails++;
|
||||
if (fails >= COVER_RETRY_MAX) {
|
||||
|
||||
@@ -276,13 +276,18 @@ static void song_src_dispose(song_src_t *ss)
|
||||
if (ss->w) {
|
||||
if (ss->dl_rc == NCM_OK) {
|
||||
/* 下载完整:commit 转正进 LRU 池(rename 不影响读端已开 fd,此处已近曲终) */
|
||||
int64_t fin = (int64_t)stream_src_final(ss->src);
|
||||
meow_log(MEOW_LOG_INFO, "play: dl done id? final=%lld commit", (long long)fin);
|
||||
if (ncm_cache_commit(ss->w) == 0)
|
||||
ncm_cache_evict(&g_st.ctx.cache); /* 写后检查:新落盘可能推过上限 */
|
||||
} else if (ss->need_fresh) {
|
||||
/* 服务器拒绝续传(416/其它):残体不可再续,删(下首整首重下) */
|
||||
meow_log(MEOW_LOG_WARN, "play: dl 416/refuse resume -> drop .part (dl_rc=%d)", ss->dl_rc);
|
||||
ncm_cache_abort(ss->w);
|
||||
} else {
|
||||
/* 中断(切歌/断流):保留 .part 残体,供下次播放 Range 续传(§5.2) */
|
||||
meow_log(MEOW_LOG_INFO, "play: dl interrupt dl_rc=%d kept bytes=%lld -> .part",
|
||||
ss->dl_rc, (long long)stream_src_written(ss->src));
|
||||
ncm_cache_abort_part(ss->w);
|
||||
}
|
||||
ss->w = NULL;
|
||||
@@ -298,6 +303,21 @@ static void audio_cache_key(char *out, size_t sz, const char *id, int level)
|
||||
snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level));
|
||||
}
|
||||
|
||||
/* §档位保真(REPORT §NEW 任务2②):显示档以响应实际格式/码率为准——
|
||||
* - type=flac:采信降级链 eff(无损档链已保证 flac,br/采样由解码后定)。
|
||||
* - type=mp3:绝不显示 Hi-Res 及以上(Hi-Res 需 >16bit/高采样,mp3 达不到)。按响应 br
|
||||
* 参与判定:≤128k→标准;否则→极高 HQ(≤320k,仅降级链中间档「较高 192k」不进 UI,不上屏)。
|
||||
* - br 未知:钳 chain_eff 到 ≤EXHIGH(保守不夸大)。
|
||||
* 返回 UI 显示用档位(g_play.eff_level)。 */
|
||||
static int eff_level_for_resp(const ncm_play_url_t *u, int chain_eff, int want)
|
||||
{
|
||||
if (u && ncm_streq(u->type, "flac")) return (chain_eff >= 0) ? chain_eff : want;
|
||||
int br = u ? u->br : 0;
|
||||
if (br > 0) return (br <= 128000) ? NCM_LEVEL_STANDARD : NCM_LEVEL_EXHIGH; /* 按实际码率 */
|
||||
int e = (chain_eff >= 0) ? chain_eff : want;
|
||||
return (e < NCM_LEVEL_EXHIGH) ? e : NCM_LEVEL_EXHIGH; /* 未知:钳到 ≤EXHIGH */
|
||||
}
|
||||
|
||||
/* 同曲重播护栏(§25 切音质后重播同一曲,替代 §18 的 still_current):仅 cur/state 不够——
|
||||
* 切音质时 play_manager_play
|
||||
* 用的 idx 与在播曲相同(cur 不变)且置 state=BUFFER,仍会被 still_current 当「我的 intent
|
||||
@@ -330,7 +350,7 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = (eff >= 0) ? eff : want;
|
||||
g_play.eff_level = eff_level_for_resp(&u, eff, want); /* §2②:显示档按实际格式/码率 */
|
||||
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
/* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */
|
||||
@@ -364,6 +384,11 @@ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const ch
|
||||
}
|
||||
ncm_strlcpy(path, ncm_cache_writer_tmp_path(ss->w), psz); /* 日志用 */
|
||||
ss->total = (u.size > 0) ? u.size : 0;
|
||||
/* §NEW 任务1(SIGSEGV 定位):下载链路诊断日志——起播即留档,断流/崩溃后从 log 回溯。 */
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"play: dl start id=%s lv=%s eff=%s size=%lld resume=%s url=%.56s",
|
||||
id, ncm_api_level_key(want), ncm_api_level_key(eff >= 0 ? eff : want),
|
||||
(long long)u.size, resuming ? "yes" : "no", u.url);
|
||||
/* 上限:Hi-Res flac 单曲可达数百 MB,flac 上限 1GB;mp3 维持 64MB。
|
||||
* size 可信时按 size 限幅(防超额),未知按格式默认上限(续传 body ≤ size 亦安全)。 */
|
||||
size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20);
|
||||
@@ -436,13 +461,31 @@ static int song_local_file(const ncm_song_t *s, int idx, int seq, song_src_t *ss
|
||||
ncm_cache_lookup(&g_st.ctx.cache, key, "mp3", path, psz) == NCM_OK) {
|
||||
hit = 1;
|
||||
} else if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
|
||||
/* 旧键温和兼容(§19 前存量:无档位后缀的 song_<id>[.]mp3,含历史双点名):
|
||||
* 命中即播,不主动改名/删除,由 LRU 自然淘汰。 */
|
||||
/* §19 前存量:无档位后缀的 song_<id>[.]mp3(含历史双点名 song_<id>..mp3)。
|
||||
* §NEW 任务5:命中即**迁移重命名**到当前档位键 song_<id>_<level>.mp3(下方 hit==2
|
||||
* 分支做 rename,不重下),之后不再回退旧键。 */
|
||||
hit = 2;
|
||||
}
|
||||
if (hit) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) return NCM_ERR_NOENT;
|
||||
if (hit == 2) {
|
||||
/* §NEW 任务5:存量旧键(无档位后缀 song_<id>.mp3 / 历史双点 song_<id>..mp3)
|
||||
* 迁移重命名到当前档位键 song_<id>_<g_level>.<ext>(**不重下**,仅改名)。
|
||||
* 目标已存在(该档已有正式条目)则删旧;迁移失败回退旧路径照常播。 */
|
||||
char nk[64], np[512];
|
||||
audio_cache_key(nk, sizeof nk, id, g_level);
|
||||
const char *dot = strrchr(path, '.');
|
||||
const char *ext = dot ? dot + 1 : "mp3";
|
||||
if (ncm_cache_path(&g_st.ctx.cache, nk, ext, np, sizeof np) == NCM_OK) {
|
||||
struct stat nst;
|
||||
if (stat(np, &nst) == 0) unlink(path); /* 目标已在(或并发建成):删旧双点/旧名 */
|
||||
else rename(path, np);
|
||||
ncm_strlcpy(path, np, psz); /* 迁移后即用新路径(无论 rename 成败,
|
||||
* 覆盖到同一目标;失败则下次会再试) */
|
||||
if (stat(np, &st) != 0) return NCM_ERR_NOENT;
|
||||
}
|
||||
}
|
||||
if (stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) != 0)
|
||||
return NCM_ERR_IO;
|
||||
ss->total = (int64_t)st.st_size;
|
||||
@@ -646,7 +689,15 @@ static void run_song(int idx)
|
||||
|
||||
int64_t frames = ncm_dec_read(dec, pcm, PCM_CHUNK_FRAMES);
|
||||
if (frames == 0) { /* 播完(下载中断时=截断点,F5) */
|
||||
meow_log(MEOW_LOG_INFO, "play: done idx=%d frames=%lld", idx, (long long)frames_done);
|
||||
/* §NEW 任务1(SIGSEGV 定位):播完时区分自然完成 vs 截断(下载未满/失败)。
|
||||
* expected=标称 size;final=实际落盘;failed<0=中断。供真机崩溃回溯。 */
|
||||
int64_t fin = (int64_t)stream_src_final(ss.src);
|
||||
int frc = stream_src_failed(ss.src);
|
||||
int64_t exp = ss.total > 0 ? ss.total : 0;
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"play: done idx=%d frames=%lld fin=%lld exp=%lld fail=%d underrun=%d",
|
||||
idx, (long long)frames_done, (long long)fin, (long long)exp, frc,
|
||||
(int)stream_src_written(ss.src));
|
||||
break;
|
||||
}
|
||||
/* 写失败(流死/audiod 断链):置错停播——旧版静默 break 会被外层当「自然播完」
|
||||
|
||||
@@ -336,6 +336,11 @@ static void home_page_create(meow_view *v, void *ctx)
|
||||
g_tb = ncm_topbar_create("我的音乐", 0, "\xe2\x86\xbb"); /* 刷新(动作0) */
|
||||
meow_topappbar_add_action(g_tb, "\xe2\x9a\x99"); /* 设置(动作1) */
|
||||
meow_topappbar_add_action(g_tb, "\xe2\x8c\x95"); /* 搜索(动作2, 最右) */
|
||||
/* §307 topbar 图标迁移:三位码点(↻ ⚙ ⌕)在 4k 字库缺字形渲染成豆腐(WORK_STATE 310 前置
|
||||
* 症状①),改为 sb_icons 位图(Material Symbols Rounded,字符标签仅作占位,绘制走位图)。 */
|
||||
meow_topappbar_set_item_icon(g_tb, 0, MEOW_ICON_REFRESH);
|
||||
meow_topappbar_set_item_icon(g_tb, 1, MEOW_ICON_SETTINGS);
|
||||
meow_topappbar_set_item_icon(g_tb, 2, MEOW_ICON_SEARCH);
|
||||
g_tabs = meow_tabbar_create(r_tabs.x, r_tabs.y, r_tabs.w, r_tabs.h);
|
||||
meow_tabbar_add_tab(g_tabs, tab_label(0));
|
||||
meow_tabbar_add_tab(g_tabs, tab_label(1));
|
||||
|
||||
@@ -66,6 +66,23 @@ static const ncm_playlist_t *pl_cur(void)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 收藏按钮显隐(WORK_STATE 2026-09-02 用户报 ①):「我创建的」歌单不显示收藏按钮。
|
||||
* 判定:home 打开的真实歌单(active_playlist >= 0)is_collected==0(=我创建的)隐藏;
|
||||
* 搜索歌单(NCM_PL_SEARCH)无法判定归属,保守显示(外部歌单可收藏)。 */
|
||||
static int pl_show_fav(void)
|
||||
{
|
||||
const ncm_playlist_t *pl = pl_cur();
|
||||
if (!pl) return 1;
|
||||
if (g_st.active_playlist >= 0 && pl->is_collected == 0) return 0; /* 我创建的:隐藏 */
|
||||
return 1;
|
||||
}
|
||||
/* 评论按钮实际矩形:收藏隐藏时其槽并入评论(拉宽覆盖原收藏位+评论位,不留空洞)。 */
|
||||
static ncm_rect_t pl_cmt_rect(void)
|
||||
{
|
||||
if (pl_show_fav()) return r_cmt;
|
||||
return NR(r_fav.x, r_fav.y, r_fav.w + r_cmt.w + 12, r_cmt.h);
|
||||
}
|
||||
|
||||
static int pl_src2(int idx, meow_list_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
@@ -155,16 +172,19 @@ static void pl_draw(void *ctx)
|
||||
}
|
||||
meow_ui_text(nmx, r_card.y + 16 + th->font_body + 8, meta, th->font_body, th->onSurfaceVariant);
|
||||
|
||||
/* 操作条:播放全部(红 FILLED) + 收藏(OUTLINED) + 评论(红 TEXT+badge) */
|
||||
/* 操作条:播放全部(红 FILLED) + 收藏(OUTLINED,我收藏的才显示) + 评论(红 TEXT+badge) */
|
||||
ncm_draw_button(r_plall.x, r_plall.y, r_plall.w, r_plall.h, "播放全部", pa->red, pa->on_red);
|
||||
meow_ui_draw_rect_round(r_fav.x + 1, r_fav.y + 1, r_fav.w - 2, r_fav.h - 2,
|
||||
r_fav.h / 2 - 1, th->outlineVariant);
|
||||
meow_ui_text_center(r_fav.x + r_fav.w / 2, r_fav.y + (r_fav.h - th->font_body) / 2,
|
||||
"收藏", th->font_body, th->onSurface);
|
||||
meow_ui_text_center(r_cmt.x + r_cmt.w / 2, r_cmt.y + (r_cmt.h - th->font_body) / 2,
|
||||
ncm_rect_t rcmt = pl_cmt_rect(); /* 评论实际矩形(收藏隐藏时并入其槽,无空洞) */
|
||||
if (pl_show_fav()) {
|
||||
meow_ui_draw_rect_round(r_fav.x + 1, r_fav.y + 1, r_fav.w - 2, r_fav.h - 2,
|
||||
r_fav.h / 2 - 1, th->outlineVariant);
|
||||
meow_ui_text_center(r_fav.x + r_fav.w / 2, r_fav.y + (r_fav.h - th->font_body) / 2,
|
||||
"收藏", th->font_body, th->onSurface);
|
||||
}
|
||||
meow_ui_text_center(rcmt.x + rcmt.w / 2, rcmt.y + (rcmt.h - th->font_body) / 2,
|
||||
"评论", th->font_body, pa->red);
|
||||
/* 红色小 badge(评论置灰入口) */
|
||||
meow_ui_fill_rect(r_cmt.x + r_cmt.w / 2 + 34, r_cmt.y + 10, 6, 6, pa->red);
|
||||
meow_ui_fill_rect(rcmt.x + rcmt.w / 2 + 34, rcmt.y + 10, 6, 6, pa->red);
|
||||
|
||||
/* 曲目行:红 mini-vinyl 缩略 + 按压红条(文本由 listview z=10 画) */
|
||||
if (g_load_err == 0 && g_st.track_ids_count > 0) {
|
||||
@@ -411,10 +431,12 @@ int playlist_on_tap(int x, int y)
|
||||
}
|
||||
if (x >= r_plall.x && x < r_plall.x + r_plall.w &&
|
||||
y >= r_plall.y && y < r_plall.y + r_plall.h) { pl_drag_reset(); player_play_song(0); return 1; }
|
||||
if (x >= r_fav.x && x < r_fav.x + r_fav.w && y >= r_fav.y && y < r_fav.y + r_fav.h) {
|
||||
if (pl_show_fav() &&
|
||||
x >= r_fav.x && x < r_fav.x + r_fav.w && y >= r_fav.y && y < r_fav.y + r_fav.h) {
|
||||
pl_drag_reset(); ncm_snack("收藏功能开发中"); return 1;
|
||||
}
|
||||
if (x >= r_cmt.x && x < r_cmt.x + r_cmt.w && y >= r_cmt.y && y < r_cmt.y + r_cmt.h) {
|
||||
ncm_rect_t rcmt = pl_cmt_rect(); /* 评论实际矩形(与绘制同口径) */
|
||||
if (x >= rcmt.x && x < rcmt.x + rcmt.w && y >= rcmt.y && y < rcmt.y + rcmt.h) {
|
||||
pl_drag_reset(); ncm_snack("评论功能开发中"); return 1;
|
||||
}
|
||||
if (g_load_err != 0) {
|
||||
|
||||
Reference in new issue
Block a user