fix(ncm): 三连修 — 封面切歌不更新/缓冲无反馈失败无声/暂停切歌边界 (REPORT §18)
- cover worker 忙等死循环(成功后不清目标,每圈解码+换入+gen++,CPU 100%+每帧整盘重绘) → 成功清目标转怠速; 失败指数退避 0.5-4s 连 8 次放弃+WARN 日志; 去重加在拉目标; cover_get 持锁+cover_put 修绘制期 UAF; g_host_mock 防真网守卫 - 封面兜底污染修复: 不再把单曲 al_pic 写进 g_st.cover_url(否则 al_pic 缺失曲回落成 上一首封面且撞去重 → 封面永停); plr_page_enter 强制重估自愈; pl_page_enter 回填歌单封面 - 缓冲 UI: BUFFER 态「缓冲中… N%」(g_play.dl_done/total, 下载回调累加); 播放错误中文 错误条+snack(旧版 err 无 UI 消费者=无声死等); STOPPED 态播放键=重试; play/next 清 err - 写失败 set_err 修连环跳歌(旧版静默 break 被外层当自然播完,流死时跳完歌单+狂发 song_url); still_current 护栏修下载途中切歌的状态/快照过期覆盖 - 验证(0 真实请求): cover_chain_sim 7 组+player_flow_sim P1-P5 全 PASS, 旧代码对照 T2/P2/P4 必败坐实根因; audiod_chain_sim 14/14; pagination/window/core+qemu 回归 PASS; 宿主+mipsel 自有代码 0 warning; 9 页离屏渲染 rc=0 (含新 mock 页 playerbuf/playererr) - ncm md5 50d41bbf4a379e925699ba013b997ffd (未部署真机, 留主会话)
This commit is contained in:
1 parent
614694ad4a
commit
6200a41b71
13 files changed
+820
-20
No files matched your search
@@ -397,7 +397,8 @@ void appstate_apply_playlist_cover(int idx)
|
||||
ncm_strlcpy(g_st.cover_url, g_st.playlists[idx].cover, sizeof g_st.cover_url);
|
||||
}
|
||||
|
||||
/* 设置播放页当前歌曲 + 封面 URL(player 页 UI 读)。idx=-1 表示清除。 */
|
||||
/* 设置播放页当前歌曲下标。cover_url 非空时一并更新 g_st.cover_url(歌单封面兜底值);
|
||||
* §18 起调用方传 NULL——兜底值由 appstate_apply_playlist_cover 维护,勿用单曲 al_pic 污染。 */
|
||||
void appstate_set_playing_song(int idx, const char *cover_url)
|
||||
{
|
||||
g_st.playing_song = idx;
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
* 线程模型:一个持久的 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>
|
||||
@@ -78,12 +87,20 @@ static void save_cover_raw(const char *url, size_t len)
|
||||
}
|
||||
}
|
||||
|
||||
#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); usleep(250 * 1000); continue; }
|
||||
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);
|
||||
@@ -104,7 +121,25 @@ static void *cover_worker(void *arg)
|
||||
if (!img) {
|
||||
blen = 0;
|
||||
img = decode_cover(url, &blen);
|
||||
if (!img) { usleep(500 * 1000); continue; }
|
||||
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);
|
||||
@@ -117,6 +152,7 @@ static void *cover_worker(void *arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
backoff_ms = 0; fails = 0;
|
||||
|
||||
pthread_mutex_lock(&g_cm);
|
||||
/* 目标仍是 url 才换入(期间未提交更早) */
|
||||
@@ -126,6 +162,7 @@ static void *cover_worker(void *arg)
|
||||
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);
|
||||
}
|
||||
@@ -138,8 +175,11 @@ static void *cover_worker(void *arg)
|
||||
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);
|
||||
if (g_cover_url[0] && strcmp(g_cover_url, url) == 0) { pthread_mutex_unlock(&g_cm); return; }
|
||||
/* 去重:已在展示 或 已在拉取途中的同一 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) {
|
||||
@@ -151,14 +191,17 @@ void cover_request(const char *url)
|
||||
}
|
||||
}
|
||||
|
||||
/* UI 线程:读取当前封面(不拥有)。 */
|
||||
/* UI 线程:读取当前封面——**持锁返回**,绘制完必须配对 cover_put()(§18 修 UAF:
|
||||
* 旧版 get 后无锁绘制,worker 换入时 free 同一张图 → use-after-free)。 */
|
||||
meow_img *cover_get(void)
|
||||
{
|
||||
meow_img *im;
|
||||
pthread_mutex_lock(&g_cm);
|
||||
im = g_cover_img;
|
||||
return g_cover_img;
|
||||
}
|
||||
|
||||
void cover_put(void)
|
||||
{
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
return im;
|
||||
}
|
||||
|
||||
/* 是否有新封面待绘制(UI tick 判变)。 */
|
||||
|
||||
@@ -85,6 +85,8 @@ typedef struct ncm_play_run {
|
||||
int seq; /* 播放代际(每次 start 递增,用于区分过期信号)*/
|
||||
int err; /* 最近一次播放错误(0=无) */
|
||||
char errmsg[96];
|
||||
int64_t dl_done; /* 本曲已下载字节(缓冲期进度反馈,§18) */
|
||||
int64_t dl_total; /* 本曲标称大小(song_url 返回;0=未知) */
|
||||
int has_track; /* 当前是否有可播曲目(含 url 获取中) */
|
||||
/* 播放曲快照(REPORT §17):run_song 起播时把歌曲对象整体拷入(mu 保护),
|
||||
* 之后播放/歌词/IPC/UI 全读快照而非窗口——窗口滑出当前播放曲也不影响播放。 */
|
||||
@@ -303,7 +305,8 @@ int lyric_state(void); /* 0=空闲/未拉 1=加载中 2=已拉完(可能
|
||||
|
||||
/* cover */
|
||||
void cover_request(const char *url); /* 提交目标封面 URL(后台线程下载+解码) */
|
||||
meow_img *cover_get(void); /* 当前封面(不拥有;NULL=无) */
|
||||
meow_img *cover_get(void); /* 当前封面(**持锁返回**;NULL=无;用后必须 cover_put) */
|
||||
void cover_put(void); /* 配对 cover_get 释放锁(§18 修绘制期 UAF) */
|
||||
int cover_changed(void); /* 1=有新封面待绘制(UI tick 判变) */
|
||||
void cover_ack(void); /* UI 消费一次(已见当前版本) */
|
||||
const char *cover_current_path(void); /* 封面原始文件路径(供 launcher 媒体卡片读);NULL=无 */
|
||||
@@ -335,6 +338,7 @@ int64_t player_manager_dur_ms(void);
|
||||
int player_manager_load_lyric(void); /* 为当前曲目拉歌词并解析 */
|
||||
int player_manager_error(void);
|
||||
int player_manager_current_song(ncm_song_t *out); /* 播放曲快照锁内拷贝;0=有效 -1=无(REPORT §17) */
|
||||
void player_manager_dl(int64_t *done, int64_t *total); /* 本曲下载进度(字节;total=0 未知,§18) */
|
||||
|
||||
/* songmeta(app/logic/songmeta.c):歌曲元数据本地缓存(<cache_dir>/meta/<id>.bin),
|
||||
* 拉过的 song_detail 结果序列化落盘,再次入窗免重复请求(REPORT §17)。 */
|
||||
|
||||
@@ -41,6 +41,15 @@ int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu);
|
||||
int64_t player_manager_dur_ms(void) { int64_t d; pthread_mutex_lock(&g_play.mu); d = g_play.dur_ms; pthread_mutex_unlock(&g_play.mu); return d; }
|
||||
int player_manager_error(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.err; pthread_mutex_unlock(&g_play.mu); return e; }
|
||||
|
||||
/* 本曲下载进度(UI 缓冲反馈,§18)。total=0 表示未知(song_url 未给 size / 缓存命中)。 */
|
||||
void player_manager_dl(int64_t *done, int64_t *total)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
if (done) *done = g_play.dl_done;
|
||||
if (total) *total = g_play.dl_total;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
/* 播放曲快照(REPORT §17):run_song 起播时物化并拷入 g_play.play_song(mu 保护),
|
||||
* 之后播放/歌词/IPC/UI 一律读快照——窗口滑出当前播放曲不影响播放,不许崩。 */
|
||||
int player_manager_current_song(ncm_song_t *out)
|
||||
@@ -70,6 +79,9 @@ void player_manager_play(int song_idx)
|
||||
g_play.cmd = 0;
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
g_play.pos_ms = 0;
|
||||
g_play.err = 0; /* 新播放意图清旧错误(UI 错误条随新点歌消失) */
|
||||
g_play.dl_done = 0;
|
||||
g_play.dl_total = 0;
|
||||
g_play.seq++;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
@@ -101,6 +113,9 @@ void player_manager_next(int dir)
|
||||
g_play.cmd = 0;
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
g_play.pos_ms = 0;
|
||||
g_play.err = 0;
|
||||
g_play.dl_done = 0;
|
||||
g_play.dl_total = 0;
|
||||
g_play.seq++;
|
||||
}
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
@@ -120,6 +135,10 @@ void player_manager_stop(void)
|
||||
/* ---- 每首歌的播放体(在外层循环内调用)---- */
|
||||
static int download_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
{
|
||||
/* 下载进度反馈(UI「缓冲中… N%」,§18):回调在播放线程,锁内仅累加。 */
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.dl_done += (int64_t)len;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
return ncm_cache_write((ncm_cache_writer_t *)ctx, data, len);
|
||||
}
|
||||
|
||||
@@ -138,6 +157,9 @@ static int song_local_file(const char *id, char *path, size_t psz, int64_t *byte
|
||||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, g_level, &u);
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
|
||||
/* 写前检查:池大小超限先按 LRU 淘汰(此线程尚未开 temp,无在途文件被误删风险) */
|
||||
ncm_cache_evict(&g_st.ctx.cache);
|
||||
@@ -183,6 +205,16 @@ static void set_err(int err, const char *msg)
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
/* 切歌竞态护栏(§18):物化/下载期间用户可能已切歌——只有仍是当前目标且处于 BUFFER
|
||||
* (本次播放意图未过期)才继续,否则直接回外层(外层会以新 cur 重跑 run_song)。 */
|
||||
static int still_current(int idx)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
int ok = (g_play.cur_song == idx) && (g_play.state == NCM_PC_BUFFER);
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* 一首歌:下载→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
|
||||
static void run_song(int idx)
|
||||
{
|
||||
@@ -193,6 +225,7 @@ static void run_song(int idx)
|
||||
int fr = appstate_song_fetch(idx, &s);
|
||||
if (fr != NCM_OK) { set_err(fr, "load failed"); return; }
|
||||
}
|
||||
if (!still_current(idx)) return; /* 物化期间已切歌/停止:不写过期快照 */
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.play_song = s;
|
||||
g_play.play_song_valid = 1;
|
||||
@@ -202,11 +235,13 @@ static void run_song(int idx)
|
||||
char path[512];
|
||||
int rc = song_local_file(s.id, path, sizeof path, &file_bytes);
|
||||
if (rc != NCM_OK) {
|
||||
if (!still_current(idx)) return; /* 下载期间已切歌:静默放弃(缓存已落盘不亏) */
|
||||
const char *m = (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
|
||||
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
|
||||
set_err(rc, m);
|
||||
return;
|
||||
}
|
||||
if (!still_current(idx)) return; /* 下载完成但已切歌:勿开解码/勿动状态 */
|
||||
ncm_strlcpy(g_path, path, sizeof g_path);
|
||||
g_file_bytes = file_bytes;
|
||||
|
||||
@@ -287,7 +322,12 @@ static void run_song(int idx)
|
||||
break;
|
||||
}
|
||||
int frames = (int)(got / 2);
|
||||
if (audio_out_write(pcm, frames) != frames) break;
|
||||
/* 写失败(流死/audiod 断链):置错停播——旧版静默 break 会被外层当「自然播完」
|
||||
* 自动顺延下一首,流死时连环跳歌并狂发 song_url 请求(§18 修)。 */
|
||||
if (audio_out_write(pcm, frames) != frames) {
|
||||
if (!g_app_quit) set_err(NCM_ERR_IO, "audio write failed");
|
||||
break;
|
||||
}
|
||||
frames_done += frames;
|
||||
/* 更新进度(~200ms 节流)*/
|
||||
int64_t pos = frames_done * 1000 / 44100;
|
||||
|
||||
@@ -90,6 +90,8 @@ static int page_id(const char *name)
|
||||
if (strcmp(name, "playlist") == 0) return ACT_PLAYLIST;
|
||||
if (strcmp(name, "player") == 0) return ACT_PLAYER;
|
||||
if (strcmp(name, "lands") == 0) return ACT_PLAYER; /* 横屏布局 */
|
||||
if (strcmp(name, "playerbuf") == 0) return ACT_PLAYER; /* §18 缓冲态 */
|
||||
if (strcmp(name, "playererr") == 0) return ACT_PLAYER; /* §18 错误态 */
|
||||
if (strcmp(name, "settings") == 0) return ACT_SETTINGS;
|
||||
if (strcmp(name, "search") == 0) return ACT_SEARCH;
|
||||
return -1;
|
||||
@@ -146,6 +148,25 @@ int main(int argc, char **argv)
|
||||
g_play.play_song = g_st.songs[0]; /* 播放快照(§17) */
|
||||
g_play.play_song_valid = 1;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
} else if (strcmp(name, "playerbuf") == 0) { /* §18:缓冲态(25% 下载进度) */
|
||||
mock_playlists(); mock_songs();
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
g_play.cur_song = 0;
|
||||
g_play.play_song = g_st.songs[0];
|
||||
g_play.play_song_valid = 1;
|
||||
g_play.dl_done = 1048576;
|
||||
g_play.dl_total = 4194304;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
} else if (strcmp(name, "playererr") == 0) { /* §18:错误态(无音源) */
|
||||
mock_playlists(); mock_songs();
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.state = NCM_PC_STOPPED;
|
||||
g_play.cur_song = 0;
|
||||
g_play.play_song = g_st.songs[0];
|
||||
g_play.play_song_valid = 1;
|
||||
g_play.err = NCM_ERR_NOTFOUND;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
} else if (strcmp(name, "settings") == 0) {
|
||||
g_st.logged_in = 1;
|
||||
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
|
||||
@@ -181,7 +202,7 @@ int main(int argc, char **argv)
|
||||
if (strcmp(name, "login") == 0 || strcmp(name, "loginlive") == 0) v = login_view_create();
|
||||
else if (strcmp(name, "home") == 0) v = home_view_create();
|
||||
else if (strcmp(name, "playlist") == 0) v = playlist_view_create();
|
||||
else if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) v = player_view_create();
|
||||
else if (page_id(name) == ACT_PLAYER) v = player_view_create(); /* player/lands/playerbuf/playererr */
|
||||
else if (strcmp(name, "settings") == 0) v = settings_view_create();
|
||||
else if (strcmp(name, "search") == 0) v = search_view_create();
|
||||
if (!v) { fprintf(stderr, "bad page\n"); return 1; }
|
||||
@@ -192,7 +213,7 @@ int main(int argc, char **argv)
|
||||
meow_view_page_push(pid);
|
||||
/* player 页:其 on_tick 通常驱动进度;这里直接再画一次确保状态就绪 */
|
||||
meow_ui_paint_full();
|
||||
if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) {
|
||||
if (page_id(name) == ACT_PLAYER) {
|
||||
player_tick(meow_ui_time_ms());
|
||||
}
|
||||
if (strcmp(name, "search") == 0) {
|
||||
|
||||
@@ -25,6 +25,7 @@ static ncm_rect_t r_cont;
|
||||
static int g_custom_disc, g_custom_dyn;
|
||||
static int g_landscape;
|
||||
static int g_last_pos_ms, g_last_state, g_last_line, g_last_song;
|
||||
static int g_last_dl, g_last_err; /* 缓冲进度粒度值 / 错误沿(§18) */
|
||||
static char g_last_meta_id[24]; /* 已上封面的元数据 id(快照滞后 settle 时纠正封面/歌名) */
|
||||
|
||||
/* 运行期几何(player_compute_geom 填充;各带绘制物/命中共用)。 */
|
||||
@@ -155,7 +156,7 @@ static void plr_draw_disc(void *ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/* 封面:方盖内切于圆 + 四角盖回盘色 → 封面成圆。 */
|
||||
/* 封面:方盖内切于圆 + 四角盖回盘色 → 封面成圆。(cover_get 持锁,绘完 cover_put,§18) */
|
||||
meow_img *cov = cover_get();
|
||||
int hS = S / 2;
|
||||
if (cov) meow_ui_img_cover(cx - hS, cy - hS, S, cov);
|
||||
@@ -167,6 +168,7 @@ static void plr_draw_disc(void *ctx)
|
||||
}
|
||||
meow_ui_text_center(cx, cy - 10, "\u266a", th->font_body, pa->red);
|
||||
}
|
||||
cover_put();
|
||||
for (int dy = -R; dy <= R; dy++) {
|
||||
int half = (int)sqrt((double)(R*R - dy*dy));
|
||||
if (half <= hS) continue;
|
||||
@@ -232,7 +234,9 @@ static void plr_draw_dyn(void *ctx)
|
||||
|
||||
int st = player_manager_state();
|
||||
draw_ctl(g_cprev_x, g_ctl_y, g_cr - 6, 0, th->surfaceContainer, th->onSurface);
|
||||
draw_ctl(g_cplay_x, g_ctl_y, g_cr, (st == NCM_PC_PAUSED) ? 1 : 2, pa->red, pa->on_red);
|
||||
/* 暂停/停止(出错待重试)都画「播放」三角;播放/缓冲画暂停符 */
|
||||
draw_ctl(g_cplay_x, g_ctl_y, g_cr,
|
||||
(st == NCM_PC_PAUSED || st == NCM_PC_STOPPED) ? 1 : 2, pa->red, pa->on_red);
|
||||
draw_ctl(g_cnext_x, g_ctl_y, g_cr - 6, 3, th->surfaceContainer, th->onSurface);
|
||||
|
||||
/* 歌词区(分隔线 + 滚动歌词,当前行红高亮居中) */
|
||||
@@ -241,7 +245,32 @@ static void plr_draw_dyn(void *ctx)
|
||||
int line_h = (g_landscape) ? 24 : 26;
|
||||
int center_y = g_lyr.y + g_lyr.h / 2;
|
||||
int lcx = g_lyr.x + g_lyr.w / 2;
|
||||
if (n == 0) {
|
||||
int perr = player_manager_error();
|
||||
if (perr != 0) {
|
||||
/* §18:播放错误明示(旧版 err 无 UI 消费者 → 失败无声死等) */
|
||||
const char *em = (perr == NCM_ERR_NOTFOUND) ? "无法播放:无音源(VIP/版权)"
|
||||
: (perr == NCM_ERR_AUTH) ? "登录已过期,请重新登录"
|
||||
: "加载失败,请检查网络";
|
||||
meow_ui_text_center(lcx, center_y - 20, em, 20, pa->red);
|
||||
meow_ui_text_center(lcx, center_y + 10, "点播放键重试 / 切下一首", 20, th->onSurfaceVariant);
|
||||
}
|
||||
else if (st == NCM_PC_BUFFER) {
|
||||
/* §18:缓冲反馈(song_url 获取 + 下载全程不再无提示) */
|
||||
int64_t dd = 0, dt = 0;
|
||||
player_manager_dl(&dd, &dt);
|
||||
char bbuf[48];
|
||||
if (dt > 0) {
|
||||
int pct = (int)(dd * 100 / dt);
|
||||
if (pct > 100) pct = 100;
|
||||
snprintf(bbuf, sizeof bbuf, "缓冲中… %d%%", pct);
|
||||
} else if (dd > 0) {
|
||||
snprintf(bbuf, sizeof bbuf, "缓冲中… %lldKB", (long long)(dd / 1024));
|
||||
} else {
|
||||
snprintf(bbuf, sizeof bbuf, "缓冲中…");
|
||||
}
|
||||
meow_ui_text_center(lcx, center_y - 10, bbuf, 20, th->onSurfaceVariant);
|
||||
}
|
||||
else if (n == 0) {
|
||||
/* 二期:歌词后台加载,状态区分「加载中 / 真的无词」 */
|
||||
meow_ui_text_center(lcx, center_y - 10,
|
||||
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…",
|
||||
@@ -293,8 +322,10 @@ void player_on_tap(int x, int y)
|
||||
if (ah >= 0) { player_set_screen(!g_landscape); return; }
|
||||
}
|
||||
if (x >= g_cplay_x - g_cr && x <= g_cplay_x + g_cr && y >= g_ctl_y - g_cr && y <= g_ctl_y + g_cr) {
|
||||
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
|
||||
else player_manager_pause(1);
|
||||
int st = player_manager_state();
|
||||
if (st == NCM_PC_PAUSED) player_manager_pause(0);
|
||||
else if (st == NCM_PC_STOPPED && cur_song() >= 0) player_manager_play(cur_song()); /* 出错后点播放=重试(§18) */
|
||||
else player_manager_pause(1);
|
||||
return;
|
||||
}
|
||||
if (x >= g_cprev_x - CTL_RR && x <= g_cprev_x + CTL_RR && y >= g_ctl_y - CTL_RR && y <= g_ctl_y + CTL_RR) {
|
||||
@@ -314,12 +345,15 @@ void player_play_song(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_st.track_ids_count) return;
|
||||
/* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅);
|
||||
* 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。 */
|
||||
* 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。
|
||||
* §18:g_st.cover_url 只作「歌单封面兜底」——不再把本曲 al_pic 写进去
|
||||
* (旧写法污染兜底值:al_pic 缺失的歌会回落成**上一首的封面**,且撞上
|
||||
* cover_request 去重 → 封面永远停在上一首,切歌不更新的确定性子根因)。 */
|
||||
ncm_song_t s;
|
||||
const char *cover = (appstate_song_copy(idx, &s) == 0 && s.al_pic[0])
|
||||
? s.al_pic : g_st.cover_url;
|
||||
player_manager_play(idx);
|
||||
appstate_set_playing_song(idx, cover);
|
||||
appstate_set_playing_song(idx, NULL);
|
||||
cover_request(cover);
|
||||
meow_view_page_push(ACT_PLAYER);
|
||||
}
|
||||
@@ -375,6 +409,22 @@ void player_tick(uint64_t now)
|
||||
g_last_line = line;
|
||||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌词 */
|
||||
}
|
||||
/* §18:缓冲进度前进(~2KB 粒度)刷动态带(「缓冲中… N%」);仅缓冲态需要。 */
|
||||
int64_t dd = 0, dtot = 0;
|
||||
player_manager_dl(&dd, &dtot);
|
||||
int dq = (int)(dd / 2048);
|
||||
if (dq != g_last_dl) {
|
||||
g_last_dl = dq;
|
||||
if (st == NCM_PC_BUFFER)
|
||||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||||
}
|
||||
/* §18:错误沿——错误条出现/消失刷动态带 + 新错误弹 snack(旧版 err 无任何 UI 出口)。 */
|
||||
int perr = player_manager_error();
|
||||
if (perr != g_last_err) {
|
||||
g_last_err = perr;
|
||||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||||
if (perr) ncm_snack("播放失败");
|
||||
}
|
||||
}
|
||||
|
||||
static void plr_page_create(meow_view *v, void *ctx)
|
||||
@@ -390,6 +440,7 @@ static void plr_page_create(meow_view *v, void *ctx)
|
||||
g_custom_dyn = meow_draw_custom_register(11, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h,
|
||||
plr_draw_dyn, NULL);
|
||||
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1; g_last_song = -1;
|
||||
g_last_dl = -1; g_last_err = 0;
|
||||
g_last_meta_id[0] = 0;
|
||||
}
|
||||
|
||||
@@ -399,6 +450,11 @@ static void plr_page_enter(meow_view *v, void *ctx)
|
||||
((player_ctrl *)ctx)->active = 1;
|
||||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||||
meow_ui_set_page_bg(pa->player_bg); /* 播放页恒沉浸,不随系统明暗整体反转 */
|
||||
/* §18 自愈:重进播放页强制重估(g_last_meta_id 只在 create 清过一次——离开期间
|
||||
* 切歌/封面失败后回来没有重评估路径;清零后首个 tick 按当前快照重请求封面+刷歌名)。 */
|
||||
g_last_meta_id[0] = 0;
|
||||
g_last_song = -1;
|
||||
g_last_dl = -1;
|
||||
meow_ui_paint_full();
|
||||
if (!g_host_mock) player_manager_load_lyric();
|
||||
}
|
||||
|
||||
@@ -99,13 +99,14 @@ static void pl_draw(void *ctx)
|
||||
|
||||
/* 封面头块卡片 */
|
||||
ncm_fill_round(r_card.x, r_card.y, r_card.w, r_card.h, 16, th->surfaceContainer);
|
||||
meow_img *cov = cover_get();
|
||||
meow_img *cov = cover_get(); /* 持锁返回,绘完 cover_put(§18) */
|
||||
ncm_fill_round(r_cov.x, r_cov.y, r_cov.w, r_cov.h, 12, pa->red_ctr);
|
||||
if (cov) meow_ui_img_cover(r_cov.x, r_cov.y, r_cov.w, cov);
|
||||
else {
|
||||
meow_ui_text_center(r_cov.x + r_cov.w / 2, r_cov.y + (r_cov.h - th->font_body) / 2,
|
||||
"\u266a", th->font_body, pa->red); /* ♪ */
|
||||
}
|
||||
cover_put();
|
||||
const ncm_playlist_t *pl = (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count)
|
||||
? &g_st.playlists[g_st.active_playlist] : NULL;
|
||||
const char *nm = pl ? pl->name : "歌单名";
|
||||
@@ -418,6 +419,11 @@ static void pl_page_enter(meow_view *v, void *ctx)
|
||||
((playlist_ctrl *)ctx)->active = 1;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_set_page_bg(th->bg);
|
||||
/* §18:头块封面回填——封面槽全局单张(与播放页黑胶共享),进歌单页重新请求歌单封面
|
||||
* (旧版只在 playlist_open 里 cover_cache_reset 清掉,无人再请求 → 头块永远占位 ♪)。 */
|
||||
if (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count &&
|
||||
g_st.playlists[g_st.active_playlist].cover[0])
|
||||
cover_request(g_st.playlists[g_st.active_playlist].cover);
|
||||
playlist_refresh();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user