feat: M6.10–M6.19 批量收编 + M6.19 推卡验收
- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘) - OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复 - ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640 - 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫) - 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
This commit is contained in:
1 parent
c2ee7626a4
commit
3d70a3dd2c
176 files changed
+15507
-597
No files matched your search
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -1 +1,14 @@
|
||||
{"name":"流媒体客户端","exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm","icon":"icon.png","package":"dev.player.ncm","version":"0.1","version_code":1,"framework":1,"immersive":1,"fonts":"common","author":"MeowOS","background_audio":1}
|
||||
{
|
||||
"name":"流媒体客户端",
|
||||
"exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm",
|
||||
"icon":"icon.png",
|
||||
"package":"dev.player.ncm",
|
||||
"version":"0.1",
|
||||
"version_code":1,
|
||||
"framework":1,
|
||||
"immersive":1,
|
||||
"fonts":"common",
|
||||
"author":"MeowOS",
|
||||
"background_audio":1,
|
||||
"awake.player":1
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
ncm_app_state_t g_st;
|
||||
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
|
||||
.state = NCM_PC_STOPPED, .cur_song = -1 };
|
||||
static int g_reported_act = -1; /* 任务4 keep_awake:已上报 launcher 的顶层 activity(差分防重发) */
|
||||
volatile int g_app_quit = 0;
|
||||
volatile int g_net_ready = 0;
|
||||
int g_host_mock = 0;
|
||||
@@ -103,7 +104,15 @@ static void app_on_swipe(int dir, int dx, int dy)
|
||||
if (settings_is_active()) { meow_view_page_pop(); return; }
|
||||
if (playlist_is_active()) { meow_view_page_pop(); return; }
|
||||
if (search_is_active()) { meow_view_page_pop(); return; }
|
||||
/* home / login = 根 → 退出 */
|
||||
if (home_is_active()) {
|
||||
/* 任务2:home 根页左滑不再退出进程 → 请求 launcher 退到桌面/后台(进程+媒体会话保留)。
|
||||
* launcher M4 据 app.json background_audio:1 + 媒体会话 PLAYING 判保活;不保活才真终止。
|
||||
* 这里**不**置 g_app_quit / 不 meow_ui_request_exit——有歌在播则后台继续播(§M4)。 */
|
||||
meow_log(MEOW_LOG_INFO, "swipe home -> background (nav_exit)");
|
||||
ipc_media_nav_exit();
|
||||
return;
|
||||
}
|
||||
/* login 根(未登录):退出 */
|
||||
meow_log(MEOW_LOG_INFO, "exit:swipe");
|
||||
meow_ui_request_exit();
|
||||
return;
|
||||
@@ -139,6 +148,15 @@ static void app_on_swipe_progress(int x_start, int dx)
|
||||
|
||||
static void app_on_tick(void)
|
||||
{
|
||||
/* 任务4 keep_awake:顶层 activity 变化 → 上报 launcher(MSG_ACT_FOCUS,差分一次)。
|
||||
* 页面切统一走 meow_view_page_push/pop/replace(更新 meow_ui_activity_active),此处差分上报:
|
||||
* 进播放页 active=ACT_PLAYER → ipc_act_report("player") → launcher fg_keep_awake 判常亮;
|
||||
* 退出播放页(弹回 home/playlist)→ 上报对应活动名(未声明 awake)→ 相机照常超时息屏。
|
||||
* 只在**当前顶层**变化时发一次(无 IPC 风暴;播放中 active 不变不重发)。 */
|
||||
{
|
||||
int act = meow_ui_activity_active();
|
||||
if (act != g_reported_act) { g_reported_act = act; ipc_act_report(act); }
|
||||
}
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
/* §NEW cookie 续期判会话失效 → 清登录态回登录页(worker 在后台续期发现 301/-460 时置标志;
|
||||
* 在此 UI 线程执行 logout+切页——meow_view_page_replace 非线程安全,只能 UI 线程做) */
|
||||
@@ -154,6 +172,16 @@ static void app_on_tick(void)
|
||||
int ai = autoplay_ui_consume();
|
||||
if (ai >= 0) player_play_song(ai);
|
||||
}
|
||||
/* 任务3:通知中心媒体卡片点卡片体(launcher 发 NCM_CTL_FOCUS)→ 回到播放页。
|
||||
* ipc_media 线程只置标志(跨线程 page 导航非线程安全),UI 线程消费后 push ACT_PLAYER;
|
||||
* 无在播曲则忽略(聚焦无意义)。 */
|
||||
if (ipc_media_focus_consume()) {
|
||||
if (player_manager_state() != NCM_PC_STOPPED &&
|
||||
meow_ui_activity_active() != ACT_PLAYER) {
|
||||
meow_log(MEOW_LOG_INFO, "ipc: focus -> player page");
|
||||
meow_view_page_push(ACT_PLAYER);
|
||||
}
|
||||
}
|
||||
if (login_is_active()) { login_tick(now); return; }
|
||||
if (player_is_active()) { player_tick(now); return; }
|
||||
if (search_is_active()) { search_tick(now); return; }
|
||||
|
||||
@@ -88,14 +88,14 @@ void account_load(void)
|
||||
int64_t last_refresh = ncm_json_get_i64_at(&j, ncm_json_path(&j, "last_refresh_ts"), &rc);
|
||||
if (rc != NCM_OK) last_refresh = 0;
|
||||
|
||||
if (vip < NCM_VIP_NONE || vip > NCM_VIP_SVIP) vip = NCM_VIP_NONE;
|
||||
if (vip < NCM_VIP_NONE || vip > NCM_VIP_SUPER) vip = NCM_VIP_NONE;
|
||||
g_st.vip = (int)vip;
|
||||
g_st.vip_cvip_exp_ms = cvip_exp;
|
||||
g_st.vip_svip_exp_ms = svip_exp;
|
||||
g_st.vip_checked_at = checked;
|
||||
g_st.cookie_last_refresh_ts = last_refresh;
|
||||
/* 有效到期时间 = 当前档对应的到期字段 */
|
||||
g_st.vip_expire_ms = (vip == NCM_VIP_SVIP) ? svip_exp
|
||||
/* 有效到期时间 = 当前档对应的到期字段(SUPER 无独立到期字段,沿黑胶 svip 场)。 */
|
||||
g_st.vip_expire_ms = (vip == NCM_VIP_SVIP || vip == NCM_VIP_SUPER) ? svip_exp
|
||||
: (vip == NCM_VIP_CVIP) ? cvip_exp : 0;
|
||||
meow_log(MEOW_LOG_INFO, "account: loaded vip=%d expire=%lld last_refresh_ts=%lld",
|
||||
g_st.vip, (long long)g_st.vip_expire_ms, (long long)last_refresh);
|
||||
@@ -129,9 +129,10 @@ int account_vip(void)
|
||||
int account_level_cap(void)
|
||||
{
|
||||
switch (account_vip()) {
|
||||
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
|
||||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||||
default: return NCM_LEVEL_EXHIGH;
|
||||
case NCM_VIP_SVIP:
|
||||
case NCM_VIP_SUPER: return NCM_LEVEL_SURROUND;
|
||||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||||
default: return NCM_LEVEL_EXHIGH;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +165,7 @@ void account_vip_maybe_refresh(int force)
|
||||
g_st.vip = info.vip;
|
||||
g_st.vip_cvip_exp_ms = info.cvip_expire_ms;
|
||||
g_st.vip_svip_exp_ms = info.svip_expire_ms;
|
||||
g_st.vip_expire_ms = (info.vip == NCM_VIP_SVIP) ? info.svip_expire_ms
|
||||
g_st.vip_expire_ms = (info.vip == NCM_VIP_SVIP || info.vip == NCM_VIP_SUPER) ? info.svip_expire_ms
|
||||
: (info.vip == NCM_VIP_CVIP) ? info.cvip_expire_ms : 0;
|
||||
g_st.vip_checked_at = now_s;
|
||||
account_persist(info.vip, info.cvip_expire_ms, info.svip_expire_ms, now_s);
|
||||
|
||||
@@ -278,8 +278,11 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *
|
||||
/* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。
|
||||
* 从 cache 写者临时文件把已下载字节复制到 cover_current.jpg(早前版本在此内再 lock g_cm
|
||||
* → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。
|
||||
* §NEW 缓存分目录:与歌曲分池,cover_current.jpg 落 covers/(封面条目 song_cov* 也在 covers/)。 */
|
||||
static void save_cover_raw(const char *path, size_t len)
|
||||
* §NEW 缓存分目录:与歌曲分池,cover_current.jpg 落 covers/(封面条目 song_cov* 也在 covers/)。
|
||||
* REPORT §封面混用:is_song=0(歌单封面,无曲目 id)时**只写 cover_current.jpg 镜像、不更新
|
||||
* g_cover_path**——g_cover_path 是上报 launcher 通知中心媒体卡片的「当前播放曲」封面指针;
|
||||
* 歌单页浏览加载歌单封面不应覆盖正在播放曲目的封面(用户真机:通知中心显示另一歌单封面)。 */
|
||||
static void save_cover_raw(const char *path, size_t len, int is_song)
|
||||
{
|
||||
if (!path || len == 0) return;
|
||||
const char *covdir = g_st.ctx.cache.covers_dir[0] ? g_st.ctx.cache.covers_dir
|
||||
@@ -301,17 +304,20 @@ static void save_cover_raw(const char *path, size_t len)
|
||||
}
|
||||
fclose(in);
|
||||
}
|
||||
ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path);
|
||||
if (is_song) ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path);
|
||||
}
|
||||
}
|
||||
|
||||
#define COVER_RETRY_MAX 8 /* 连续失败放弃次数(UI 重进页/切歌会重新提交目标) */
|
||||
|
||||
/* 内存瘦身:封面解码后按显示上限降采样驻留。网易云全尺寸封面(如 1448×1448)解码成
|
||||
* RGBA = 8MB,而应用显示只需 ≤480px(播放页黑胶方图+歌词页模糊铺底均 ≤ 屏幕 480 宽)——
|
||||
* 缩到 480×480 = 0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀,最高优先级)。
|
||||
* 若已 ≤上限(小缩略图)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */
|
||||
#define NCM_COVER_DECODE_MAX 480
|
||||
* RGBA = 8MB,而应用显示(歌词页 cover-fit 铺满 480×720 全屏,方图实际渲染到 720×720;封面页
|
||||
* 方图仅 304×304)——请求 param=640y640(见 ncm_app_internal.h)即 ≤640 边长,故此上限须 ≥ 640
|
||||
* 否则解码后又被降采样回 480、仍不够铺满 480 宽(480 图铺 480+ 显示照旧放大)。640×640 RGBA
|
||||
* ≈ 1.6MB 单张驻留(设备 RAM 仅 56MB,OOM 真机被杀;比 308 瘦身 8.28MB 仍省 6.7MB,单张 1.6MB
|
||||
* 在 32bit OOM 预算内——换封面仍先 free 旧再解码)。
|
||||
* 若已 ≤上限(小缩略图/无 param 全尺寸在 640 内)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */
|
||||
#define NCM_COVER_DECODE_MAX 640
|
||||
static meow_img *cover_cap(meow_img *im)
|
||||
{
|
||||
if (!im) return NULL;
|
||||
@@ -362,7 +368,7 @@ static void *cover_worker(void *arg)
|
||||
img = download_cover(url, w, &blen, &drc, &dstatus, &dkind);
|
||||
if (img) {
|
||||
pthread_mutex_lock(&g_cm);
|
||||
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
|
||||
save_cover_raw(ncm_cache_writer_tmp_path(w), blen, song[0] ? 1 : 0);
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
if (ncm_cache_commit(w) != 0) { ncm_cache_abort(w); }
|
||||
} else {
|
||||
@@ -452,6 +458,21 @@ static void *cover_worker(void *arg)
|
||||
if (g_cover_img) meow_img_free(g_cover_img);
|
||||
g_cover_img = img;
|
||||
ncm_strlcpy(g_cover_url, url, sizeof g_cover_url);
|
||||
/* 上报给 launcher 的封面路径 = 本目标专辑的缓存条目(per-URL 唯一)。
|
||||
* 旧码只在 save_cover_raw(下载路径)更新 g_cover_path;缓存命中路径从不更新 →
|
||||
* cover_current_path() 恒空 → MSG_META artwork 恒空 → 通知中心媒体卡拿不到封面路径
|
||||
* → 回退显示歌名。此处统一在「目标达成换入」时把 g_cover_path 指向已落盘的缓存条目
|
||||
* (下载 commit 后 / 缓存命中时均真实存在);commit 失败则保留 save_cover_raw 已写的
|
||||
* cover_current.jpg 镜像(仍在)。路径随专辑变化 → launcher 侧「按 artwork 文本缓存」
|
||||
* 的现有实现能随切歌重载,不残留旧封面。 */
|
||||
if (song[0] && g_st.ctx.cache.dir[0]) {
|
||||
char cpath[512];
|
||||
if (ncm_cache_path(&g_st.ctx.cache, key, ".jpg", cpath, sizeof cpath) == NCM_OK) {
|
||||
struct stat cs;
|
||||
if (stat(cpath, &cs) == 0 && cs.st_size > 0)
|
||||
ncm_strlcpy(g_cover_path, cpath, sizeof g_cover_path);
|
||||
}
|
||||
}
|
||||
g_gen++;
|
||||
g_tgt_url[0] = 0; /* 目标达成即清:worker 转怠速(修忙等死循环,§18) */
|
||||
} else {
|
||||
@@ -464,7 +485,8 @@ static void *cover_worker(void *arg)
|
||||
|
||||
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。song_id:曲目 id(专辑封面死链时走
|
||||
* song/detail 兜底重取,见 REPORT §NEW 任务1);NULL/空=歌单封面(无兜底)。
|
||||
* 先 cover_norm_url 规范化(追加 ?param=300y300 缩略后缀,REPORT §NEW 任务2),使
|
||||
* 先 cover_norm_url 规范化(追加 ?param=640y640 缩略后缀,REPORT §NEW 任务2;640 适配歌词页
|
||||
* cover-fit 铺满 480×720,见 ncm_app_internal.h),使
|
||||
* g_tgt_url/g_cover_url/缓存键/下载 URL 走同一参数版本(小图省内存、命中高)。 */
|
||||
void cover_request(const char *url, const char *song_id)
|
||||
{
|
||||
|
||||
@@ -50,6 +50,41 @@ static void copy_lrc_text(char *dst, const char *src, size_t n, size_t max)
|
||||
dst[oi] = 0;
|
||||
}
|
||||
|
||||
/* 解析单个 [mm:ss.ff] 时间标签为毫秒。返回 1=合法时间标签(*out_ms、*next=']' 之后);
|
||||
* 0=非时间标签(*next=']' 之后;NULL=无闭合 ']',调用方按残缺处理)。
|
||||
* 小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1,
|
||||
* 网易云 LRC 毫秒)——此前固定 ×10 把网易云 3 位毫秒当 2 位厘秒解析,时间戳放大 ~10 倍,
|
||||
* 歌词行按绝对时间激活滞后 ~4~10s(REPORT「英文歌歌词滚动慢」)。 */
|
||||
static int parse_lrc_tag(const char *p, long *out_ms, const char **next)
|
||||
{
|
||||
const char *q = p + 1; /* 跳过 '[' */
|
||||
int m = 0, md = 0, s = 0, sd = 0;
|
||||
while (*q >= '0' && *q <= '9') { m = m * 10 + (*q - '0'); md++; q++; }
|
||||
if (md == 0 || *q != ':') goto notime;
|
||||
q++;
|
||||
while (*q >= '0' && *q <= '9') { s = s * 10 + (*q - '0'); sd++; q++; }
|
||||
if (sd == 0) goto notime;
|
||||
long frac = 0; int fd = 0; long scale = 10;
|
||||
if (*q == '.') {
|
||||
q++;
|
||||
while (*q >= '0' && *q <= '9') { frac = frac * 10 + (*q - '0'); fd++; q++; }
|
||||
if (fd == 1) scale = 100;
|
||||
else if (fd == 2) scale = 10;
|
||||
else if (fd >= 3) { scale = 1; while (fd > 3) { frac /= 10; fd--; } } /* >3 位截断到 ms */
|
||||
else scale = 0; /* 孤立 '.' 无数字:忽略小数 */
|
||||
}
|
||||
if (*q != ']') goto notime;
|
||||
*out_ms = (long)m * 60000L + (long)s * 1000L + frac * scale;
|
||||
*next = q + 1;
|
||||
return 1;
|
||||
notime:
|
||||
{
|
||||
const char *r = strchr(p, ']');
|
||||
*next = r ? r + 1 : NULL;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void lrc_parse(const char *lrc, int is_trans)
|
||||
{
|
||||
if (!lrc) return;
|
||||
@@ -62,19 +97,16 @@ static void lrc_parse(const char *lrc, int is_trans)
|
||||
int have = 0;
|
||||
long first_ms = 0;
|
||||
while (*p == '[') {
|
||||
int m = 0, s = 0, cs = 0;
|
||||
if (sscanf(p, "[%d:%d.%d]", &m, &s, &cs) >= 2) {
|
||||
long ms = (long)m * 60000L + (long)s * 1000L + (long)cs * 10L;
|
||||
long ms;
|
||||
const char *after;
|
||||
if (parse_lrc_tag(p, &ms, &after)) {
|
||||
if (!have) { first_ms = ms; have = 1; }
|
||||
/* 跳到本标签结尾 */
|
||||
const char *q = strchr(p, ']');
|
||||
if (!q) break;
|
||||
p = q + 1;
|
||||
p = after;
|
||||
} else {
|
||||
/* 非时间标签([ti:..] 等)——跳过到行尾 */
|
||||
const char *q = strchr(p, ']');
|
||||
if (!q) return;
|
||||
p = q + 1;
|
||||
if (!after) return; /* 无闭合 ']':残缺文件 */
|
||||
p = after;
|
||||
/* 若行内还有时间标签继续解析(极少),否则整行的正文以第一个文本为准 */
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -32,11 +32,19 @@
|
||||
#define ACT_SEARCH 5 /* 搜索页(顶栏搜索按钮进入) */
|
||||
|
||||
/* ---- 封面 CDN 尺寸参数(REPORT §NEW 任务2:网易云图片 CDN 对 URL 追加 ?param=WxH 返等比
|
||||
* 缩略 JPEG——省内存(300×300≈20KB vs 全尺寸 1.34MB)+解 webp 三分型③+命中率高;已含 param= 的
|
||||
* URL 尊重原样)。共享头声明:cover.c 与 bench/cover_chain_sim.c(URL 期望哈希须与实测 URL 同规则)
|
||||
* 同源,避免两处字符串漂移。 */
|
||||
* 缩略 JPEG——省内存 +解 webp 三分型③+命中率高;已含 param= 的 URL 尊重原样)。
|
||||
* 共享头声明:cover.c 与 bench/cover_chain_sim.c(URL 期望哈希须与实测 URL 同规则)
|
||||
* 同源,避免两处字符串漂移。
|
||||
* 值选择(封面分辨率适配歌词页铺满,REPORT:歌词页 cover-fit 铺满 480×720 全屏,方图实际渲染到
|
||||
* 720×720;旧默认 300y300 → 300 放大到 720 = 2.4× 最近邻放大 → 锐锯齿):
|
||||
* 640y640 → 歌词页 640→720 = 1.125× 轻微放大(铺满基本无锯齿),封面页方图 640→304 裁方降采样
|
||||
* 依旧锐利;解码 RGBA ≈ 640×640×4 ≈ 1.6MB 单张驻留(比 300 的 0.36MB +1.2MB anon,
|
||||
* 仍远低于 308 瘦身前 8.28MB;32bit OOM 已由 meow_cachegov 治理 page cache,封面 anon
|
||||
* 单张 1.6MB 在预算内——平衡清晰度与内存纪律)。
|
||||
* 注意 cover.c 的 NCM_COVER_DECODE_MAX 必须 ≥ 本参数边长,否则解码后仍被降采样回 480(不够铺
|
||||
* 满 480 宽 → 480 图铺 480+ 显示仍放大)。 */
|
||||
#define NCM_COVER_USE_PARAM 1
|
||||
#define NCM_COVER_PARAM "300y300"
|
||||
#define NCM_COVER_PARAM "640y640"
|
||||
|
||||
/* ---- 布局 ---- */
|
||||
#define APP_W 480
|
||||
@@ -120,6 +128,9 @@ enum {
|
||||
NCM_CTL_NEXT = 4,
|
||||
NCM_CTL_SEEK = 5,
|
||||
NCM_CTL_TOGGLE = 6, /* 播放/暂停切换(物理键 164 恒发此;按当前态翻转) */
|
||||
NCM_CTL_FOCUS = 7, /* launcher 通知中心媒体卡片点卡片体 → 请求 App 回到播放页
|
||||
(任务3 §re-entry:launcher 侧点卡片体发本 cmd;App 置标志,
|
||||
UI 线程消费后导航 ACT_PLAYER,进程已后台保活则直接聚焦) */
|
||||
};
|
||||
|
||||
/* 跨线程播放运行时状态(main 只读展示;play 线程写,IPC 线程读)。 */
|
||||
@@ -142,6 +153,7 @@ typedef struct ncm_play_run {
|
||||
int underruns; /* 本曲 read_cb 追上下载的等待次数 */
|
||||
uint64_t underrun_ms; /* 本曲 read_cb 累计等待 ms */
|
||||
int stalled; /* 1=下载停滞(单次等待>1s,播放线程已对 audiod pause;UI 缓冲菊花) */
|
||||
int refaults; /* 本曲 DONTNEED 丢页重读 TF 次数(§2.3 取证) */
|
||||
/* 播放曲快照(REPORT §17):run_song 起播时把歌曲对象整体拷入(mu 保护),
|
||||
* 之后播放/歌词/IPC/UI 全读快照而非窗口——窗口滑出当前播放曲也不影响播放。 */
|
||||
ncm_song_t play_song;
|
||||
@@ -481,6 +493,9 @@ int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功;bi
|
||||
int audio_out_write(const void *samples, int frames); /* <0=错误/结束;样本位宽=open 的 bits */
|
||||
int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢复(流死自动重开);非 AUDIOD 后端恒 0 */
|
||||
void audio_out_close(void);
|
||||
int audio_out_sync_volume(void); /* 任务1:读 settings.conf 当前系统音量并下发到 audiod
|
||||
(MSG_AUDIO_VOLUME→写 DAC+持久化)。0=下发成功/-1=失败。
|
||||
audio_out_open SINK_AUDIOD 成功后已自动调;独立导出供宿主验证与复调。 */
|
||||
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
|
||||
int audio_out_buffered_ms(void);
|
||||
|
||||
@@ -515,6 +530,13 @@ int songmeta_save(const char *cache_dir, const char *song_id, const ncm_song_t
|
||||
/* ipc_media:媒体会话上报/收指令 */
|
||||
int ipc_media_start(void); /* 起 IPC 线程;0=成功 */
|
||||
void ipc_media_stop(void);
|
||||
int ipc_media_nav_exit(void); /* 任务2:发 MSG_NAV{EXIT} 给 launcher 请求退后台(进程+媒体会话保留;
|
||||
launcher M4 据 background_audio+PLAYING 判保活)。0=发出/-1=失败。 */
|
||||
int ipc_media_focus_consume(void); /* 任务3:UI 线程消费「回播放页」标志(0=无;1=已消费一次) */
|
||||
int ipc_act_report(int act); /* 任务4 keep_awake:向 launcher 上报当前顶层 activity(MSG_ACT_FOCUS)。
|
||||
act=ACT_* 值,映射 "login/home/playlist/player/settings/search"(对齐
|
||||
app.json awake.<act> 键);launcher fg_keep_awake 依声明判常亮。
|
||||
0=发出/-1=无 socket 或写失败。 */
|
||||
|
||||
/* ---- 宿主测试面(app/test/test_ncm_app.c 驱动真实页面)---- */
|
||||
void app_host_set_mock(int mode); /* 0=无 1=home 2=playlist 3=player 4=settings 5=login */
|
||||
|
||||
@@ -22,13 +22,15 @@
|
||||
* (无声排查的永久资产,勿省)。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include "meow_ipc.h" /* AUDIO_FMT_S16 */
|
||||
#include "meow_ipc.h" /* AUDIO_FMT_S16 / MSG_AUDIO_VOLUME / MEOW_IPC_HDR_LEN */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sound/asound.h>
|
||||
@@ -143,6 +145,111 @@ static int file_open(int rate, int ch, int bits)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 任务1:开播前把「系统当前音量」同步下发到 audiod */
|
||||
/* ============================================================ */
|
||||
/* 背景(用户实测 2026-09-02):未开始播放时音量不是系统设定值;按一下音量键才跳到设定值。
|
||||
* 根因= ncm 启动并打开 audiod/DAC 时,没有把系统当前音量下发到 audiod(meow_audiod 只在
|
||||
* MSG_AUDIO_VOLUME 收到时才写 DAC,流开/重配上不主动应用 settings 音量),导致 DAC 停在
|
||||
* 默认/上次残留音量;用户按音量键时 launcher/watchdog 那条音量链才更新并下发到 audiod。
|
||||
* 修法= SINK_AUDIOD open 成功后读 settings.conf 的 audio.volume,经 MSG_AUDIO_VOLUME 下发
|
||||
* (audiod 写 DAC numid=1/2 + 持久化 settings,同源同键)。语意:CS43198 寄存器=衰减量
|
||||
* (0 最响/255 静音),audiod dac_vol_set 负责反转与真静音(写 255),本处只传 pct。
|
||||
* 宿主验证:fake audiod socket 收帧断言 pct==系统设定(bench/vol_sync_sim.c)。 */
|
||||
|
||||
#define NCM_SETTINGS_CONF "/usr/data/meow/settings.conf" /* 与 audiod SETTINGS_CONF 同源 */
|
||||
#define NCM_AUDIO_SOCK_DEF "/usr/data/meow_audio.sock" /* 与 audiod / SDK 客户端同缺省 */
|
||||
#define NCM_VOL_DEFAULT 60 /* 与 audiod VOL_DEFAULT/SDK 缺省一致 */
|
||||
#define NCM_VOL_MAX 100
|
||||
|
||||
static const char *settings_conf_path(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_SETTINGS_FILE"); /* 宿主单测指向临时文件;设备缺省系统 conf */
|
||||
return (e && e[0]) ? e : NCM_SETTINGS_CONF;
|
||||
}
|
||||
|
||||
/* 读 settings.conf 的 audio.volume(扁平 "key = value";兼容无空格/多空格)。
|
||||
* 与 launcher meow_settings / SDK meow_ui_vol_read 同款解析。缺省 60;夹到 [0,100]。 */
|
||||
static int settings_read_volume(void)
|
||||
{
|
||||
static const char key[] = "audio.volume";
|
||||
const int kl = (int)sizeof key - 1;
|
||||
FILE *f = fopen(settings_conf_path(), "r");
|
||||
int vol = NCM_VOL_DEFAULT;
|
||||
if (!f) return vol;
|
||||
char line[160];
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
char *eq = strchr(line, '=');
|
||||
if (!eq) continue; /* 非 key=value 行跳过 */
|
||||
char *k = line;
|
||||
while (*k == ' ' || *k == '\t') k++; /* key 左去空白 */
|
||||
int n = (int)(eq - k);
|
||||
while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--; /* key 右去空白 */
|
||||
if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; /* 精确匹配 */
|
||||
char *v = eq + 1;
|
||||
while (*v == ' ' || *v == '\t') v++; /* value 左去空白 */
|
||||
int x = atoi(v);
|
||||
vol = (x < 0) ? 0 : (x > NCM_VOL_MAX ? NCM_VOL_MAX : x);
|
||||
break;
|
||||
}
|
||||
fclose(f);
|
||||
return vol;
|
||||
}
|
||||
|
||||
/* 把 pct(0-100) 下发到 audiod。独立连接(SDK 客户端连接私有不可复用);MSG_AUDIO_VOLUME
|
||||
* 不要求开流,audiod 侧写 DAC+持久化后 ACK(meow_audiod.c MSG_AUDIO_VOLUME)。失败仅日志,
|
||||
* 不影响播放本身(音量是次要正确性,非播放成败)。
|
||||
*
|
||||
* §2026-09-02 连接风暴根因:audiod_sync_volume 每次 audio_out_open 都新建一条 AF_UNIX
|
||||
* 短连接(socket→connect→write→close)。边下边播/切歌/seek 反复 open/close 时,audiod 端表现为
|
||||
* 「同一 fd=N 被反复 accept」(真机 1489 次 "io: client connected (fd=4)")。音量值实际只在
|
||||
* 用户按音量键时变化,且 audiod 收到已持久化(settings_set_volume),无需每次 open 重发——
|
||||
* 修法:每进程首次 open 同步一次;此后仅当 settings 音量**变化**才重发(同一连接数坍缩为
|
||||
* O(1),风暴消除)。失败不记 status(下次 open 仍会尝试,幂等)。 */
|
||||
static void audiod_sync_volume(int pct)
|
||||
{
|
||||
const char *sp = getenv("MEOW_AUDIO_SOCK");
|
||||
if (!sp || !sp[0]) sp = NCM_AUDIO_SOCK_DEF;
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, sp, sizeof a.sun_path - 1);
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return; }
|
||||
|
||||
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, len = sizeof(int32_t), req = 1;
|
||||
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_VOLUME;
|
||||
memcpy(frame + 0, &magic, 4);
|
||||
memcpy(frame + 4, &ver, 2);
|
||||
memcpy(frame + 6, &type, 2);
|
||||
memcpy(frame + 8, &req, 4);
|
||||
memcpy(frame + 12, &len, 4);
|
||||
int32_t v32 = (int32_t)pct;
|
||||
memcpy(frame + 16, &v32, sizeof v32);
|
||||
if (write(fd, frame, sizeof frame) != (ssize_t)sizeof frame)
|
||||
meow_log(MEOW_LOG_WARN, "audio: sync volume write failed pct=%d", pct);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* 每进程同步一次 + 值变才重发:杜绝每次 audio_out_open 新建短连接(fd=N accept 风暴根因)。
|
||||
* g_vol_synced=1 即已同步过;g_vol_last=上次下发的 pct,二者相同则零连接直接返回。 */
|
||||
static int g_vol_synced = 0;
|
||||
static int g_vol_last = -1;
|
||||
|
||||
int audio_out_sync_volume(void)
|
||||
{
|
||||
int pct = settings_read_volume();
|
||||
if (g_vol_synced && pct == g_vol_last) return 0; /* 已同步且未变:零新建连接 */
|
||||
g_vol_synced = 1;
|
||||
g_vol_last = pct;
|
||||
audiod_sync_volume(pct);
|
||||
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s)",
|
||||
pct, settings_conf_path());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_out_open(int sample_rate, int channels, int bits)
|
||||
{
|
||||
g_rate = sample_rate; g_ch = channels; g_bits = bits;
|
||||
@@ -202,6 +309,9 @@ int audio_out_open(int sample_rate, int channels, int bits)
|
||||
else
|
||||
meow_log(MEOW_LOG_INFO, "audio: audiod open ok sid=%d req=%dHz/%dch/%dbit",
|
||||
sid, sample_rate, channels, bits);
|
||||
/* 任务1:开播/打开音频会话时把系统当前音量同步下发到 audiod(DAC 停在默认/上次残留音量的根治)。
|
||||
* 幂等(同值重写无害);失败不阻断播放。 */
|
||||
audio_out_sync_volume();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
static int g_quit;
|
||||
static volatile int g_req_focus_player = 0; /* 任务3:launcher 请求「回到播放页」(UI 线程消费) */
|
||||
|
||||
static const char *sock_path(void)
|
||||
{
|
||||
@@ -151,6 +152,11 @@ static void apply_playctl(const m_playctl_t *c)
|
||||
* (停止/缓冲)时 pause() 内部按态过滤,天然 no-op 安全。 */
|
||||
player_manager_pause(player_manager_state() == NCM_PC_PAUSED ? 0 : 1);
|
||||
break;
|
||||
case NCM_CTL_FOCUS:
|
||||
/* 任务3:通知中心媒体卡片点「卡片体」→ launcher 发本 cmd(需要 launcher 接线,见任务书):
|
||||
* 请求 App 回到播放页。只置标志(跨线程),UI 线程 app_on_tick 消费后导航(page nav 非线程安全)。 */
|
||||
g_req_focus_player = 1;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -201,6 +207,78 @@ static void *ipc_thread(void *arg)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 任务2:home 根页左滑「回桌面后台」——不再进程退出,改发 MSG_NAV{EXIT} 给 launcher。
|
||||
* launcher M4 据 app.json background_audio + 媒体会话 PLAYING(meow_media_app_playing)判
|
||||
* 保活(进程+音频流保留转后台,UI 走 §10.5 SDK present 自抑,非 SIGSTOP);否则才真终止。
|
||||
* 本函数**不**置 g_app_quit、**不**停线程——进程保留,媒体会话继续上报 PLAYING(任务2 ③)。
|
||||
* 独立连接发一帧(launcher ipc_dispatch MSG_NAV{EXIT} → meow_nav_request_exit,app_launcher.c:571)。
|
||||
* 返回 0=发出 / -1=无 socket 或写失败。 */
|
||||
int ipc_media_nav_exit(void)
|
||||
{
|
||||
int fd = connect_path(sock_path());
|
||||
if (fd < 0) {
|
||||
meow_log(MEOW_LOG_WARN, "ipc: nav_exit no launcher socket");
|
||||
return -1;
|
||||
}
|
||||
m_nav_t n;
|
||||
memset(&n, 0, sizeof n);
|
||||
ncm_strlcpy(n.app_id, "dev.player.ncm", sizeof n.app_id);
|
||||
n.nav_type = MEOW_NAV_EXIT;
|
||||
n.depth = 0;
|
||||
int rc = send_frame(fd, MSG_NAV, 9, &n, sizeof n);
|
||||
close(fd);
|
||||
meow_log(MEOW_LOG_INFO, "ipc: nav_exit sent rc=%d (request launcher background)", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 任务4 keep_awake:ACT_* 值 → launcher app.json awake/route 键用的活动名。
|
||||
* 命中的只有这些(ncm_app_internal.h ACT_* 枚举);未知/空栈(0) 返回 NULL(不起作用)。 */
|
||||
static const char *ncm_act_name(int act)
|
||||
{
|
||||
switch (act) {
|
||||
case ACT_LOGIN: return "login";
|
||||
case ACT_HOME: return "home";
|
||||
case ACT_PLAYLIST: return "playlist";
|
||||
case ACT_PLAYER: return "player";
|
||||
case ACT_SETTINGS: return "settings";
|
||||
case ACT_SEARCH: return "search";
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* 任务4 keep_awake:把当前顶层 activity 上报 launcher(MSG_ACT_FOCUS,独立连接发一帧,无需 HELLO)。
|
||||
* launcher util.c ipc_dispatch MSG_ACT_FOCUS → meow_fg_act_set(app,act);fg_keep_awake() 依
|
||||
* app.json awake.<act>=1 判定该 activity 是否常亮(超时不自动息屏)。无 socket/launcher 不跑 → 优雅跳过。
|
||||
* 返回 0=发出 / -1=无 socket 或写失败。 */
|
||||
int ipc_act_report(int act)
|
||||
{
|
||||
const char *name = ncm_act_name(act);
|
||||
if (!name) return -1;
|
||||
int fd = connect_path(sock_path());
|
||||
if (fd < 0) {
|
||||
meow_log(MEOW_LOG_WARN, "ipc: act_focus(%s) no launcher socket", name);
|
||||
return -1;
|
||||
}
|
||||
m_act_focus_t f;
|
||||
memset(&f, 0, sizeof f);
|
||||
ncm_strlcpy(f.app_id, "dev.player.ncm", sizeof f.app_id);
|
||||
ncm_strlcpy(f.activity, name, sizeof f.activity);
|
||||
int rc = send_frame(fd, MSG_ACT_FOCUS, 10, &f, sizeof f);
|
||||
close(fd);
|
||||
meow_log(MEOW_LOG_INFO, "ipc: act_focus act=%s rc=%d", name, rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 任务3:UI 线程消费「回到播放页」标志。返回 1=有未消费的聚焦请求(消费一次)。 */
|
||||
int ipc_media_focus_consume(void)
|
||||
{
|
||||
if (g_req_focus_player) {
|
||||
g_req_focus_player = 0;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ipc_media_start(void)
|
||||
{
|
||||
if (g_thr_on) return 0;
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/* play/mem_watch.c — 边播边监控**取证**实现(接口见 mem_watch.h;agent 32bit 大文件 OOM 取证)。
|
||||
*
|
||||
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
|
||||
* A(DONTNEED 治理)+ C(/proc/meminfo Cached 监控)分别归 SDK meow_cachegov / meow_svc_perf;
|
||||
* 本模块退化为「RssAnon 取证」:低频读 /proc/self/status(优先 RssAnon,缺失回退 VmRSS),
|
||||
* 记录匿名内存峰值,**仅供日志**。不再驱动下载/预取降载(那由 meow_cachegov_download_paused
|
||||
* / prefetch_paused 查询)。**绝不打断解码/播放**。
|
||||
*
|
||||
* 阈值滞后(仅用于取证 level 标注):
|
||||
* kb >= CRITICAL → level=2;kb >= HIGH 且当前<2 → level=1;kb <= LOW → level=0;其它保持。
|
||||
* 本模块自含:只依赖 pthread + meow_log(SDK 日志),不依赖 core/app 其它 TU。
|
||||
*/
|
||||
#include "mem_watch.h"
|
||||
#include "../sdk/include/meow_ui_log.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* 默认阈值(设备缺省;宿主单测用 env/force 桩覆盖) */
|
||||
#define MEM_WATCH_PERIOD_MS 500
|
||||
#define MEM_WATCH_HIGH_KB (30 * 1024)
|
||||
#define MEM_WATCH_LOW_KB (20 * 1024)
|
||||
#define MEM_WATCH_CRITICAL_KB (40 * 1024)
|
||||
|
||||
struct mw_cfg {
|
||||
int period_ms;
|
||||
long high_kb;
|
||||
long low_kb;
|
||||
long critical_kb;
|
||||
};
|
||||
|
||||
static struct mw_cfg g_cfg = {
|
||||
.period_ms = MEM_WATCH_PERIOD_MS,
|
||||
.high_kb = MEM_WATCH_HIGH_KB,
|
||||
.low_kb = MEM_WATCH_LOW_KB,
|
||||
.critical_kb = MEM_WATCH_CRITICAL_KB,
|
||||
};
|
||||
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
static volatile int g_quit;
|
||||
static long g_rss_anon_kb; /* 最近一次采样(KB;-1=不可读) */
|
||||
static long g_peak_song_kb; /* 本曲匿名峰值(song_begin 清零) */
|
||||
static int g_level; /* NCM_MEM_LVL_*(仅取证日志) */
|
||||
static long g_forced_kb = -1; /* 宿主桩 ≥0 时替代真读 */
|
||||
|
||||
/* env 覆盖阈值:非法值回落默认。 */
|
||||
static int env_int(const char *k, int def)
|
||||
{
|
||||
const char *e = getenv(k);
|
||||
if (!e || !e[0]) return def;
|
||||
int v = atoi(e);
|
||||
return v > 0 ? v : def;
|
||||
}
|
||||
static long env_long(const char *k, long def)
|
||||
{
|
||||
const char *e = getenv(k);
|
||||
if (!e || !e[0]) return def;
|
||||
long v = atol(e);
|
||||
return v > 0 ? v : def;
|
||||
}
|
||||
|
||||
/* 读匿名 RSS(KB)。优先 RssAnon;缺则回退 VmRSS;都无 → -1。 */
|
||||
static long read_rss_kb(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
long forced = g_forced_kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (forced >= 0) return forced;
|
||||
|
||||
FILE *fp = fopen("/proc/self/status", "r");
|
||||
if (!fp) return -1;
|
||||
char line[256];
|
||||
long anon = -1, rss = -1;
|
||||
while (fgets(line, sizeof line, fp))
|
||||
if (!strncmp(line, "RssAnon:", 8)) anon = atol(line + 8);
|
||||
else if (rss < 0 && !strncmp(line, "VmRSS:", 6)) rss = atol(line + 6);
|
||||
fclose(fp);
|
||||
return anon >= 0 ? anon : rss;
|
||||
}
|
||||
|
||||
static const char *level_str(int lvl)
|
||||
{
|
||||
switch (lvl) {
|
||||
case NCM_MEM_LVL_HIGH: return "high";
|
||||
case NCM_MEM_LVL_CRITICAL: return "critical";
|
||||
default: return "normal";
|
||||
}
|
||||
}
|
||||
|
||||
static void *watch_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (!g_quit) {
|
||||
long kb = read_rss_kb();
|
||||
int lvl;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_rss_anon_kb = kb;
|
||||
if (kb > g_peak_song_kb) g_peak_song_kb = kb;
|
||||
long hi = g_cfg.high_kb, lo = g_cfg.low_kb, cr = g_cfg.critical_kb;
|
||||
/* 滞后三档(仅取证 level 标注) */
|
||||
if (kb >= cr) lvl = NCM_MEM_LVL_CRITICAL;
|
||||
else if (kb >= hi) lvl = (g_level == NCM_MEM_LVL_CRITICAL) ? NCM_MEM_LVL_CRITICAL : NCM_MEM_LVL_HIGH;
|
||||
else if (kb <= lo) lvl = NCM_MEM_LVL_NORMAL;
|
||||
else lvl = g_level; /* 滞后带:保持 */
|
||||
if (lvl != g_level) {
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"[mem] rss_anon=%ldKB peak=%ldKB level=%s (H=%ldK/L=%ldK/C=%ldK)",
|
||||
kb, g_peak_song_kb, level_str(lvl), hi, lo, cr);
|
||||
}
|
||||
g_level = lvl;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
usleep((useconds_t)g_cfg.period_ms * 1000u);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int mem_watch_start(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_thr_on) { pthread_mutex_unlock(&g_mu); return 0; }
|
||||
/* 开关(默认开;NCM_MEM_WATCH_ENABLED=0/off 关闭监控,行为退化为旧版零侵入) */
|
||||
const char *en = getenv("NCM_MEM_WATCH_ENABLED");
|
||||
if (en && (!en[0] || !strcmp(en, "0") || !strcmp(en, "off") || !strcmp(en, "false"))) {
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO, "[mem] watch disabled (NCM_MEM_WATCH_ENABLED=%s)", en);
|
||||
return 0;
|
||||
}
|
||||
/* 阈值 env 覆盖(宿主单测) */
|
||||
g_cfg.period_ms = env_int("NCM_MEM_WATCH_PERIOD_MS", MEM_WATCH_PERIOD_MS);
|
||||
g_cfg.high_kb = env_long("NCM_MEM_WATCH_HIGH_KB", MEM_WATCH_HIGH_KB);
|
||||
g_cfg.low_kb = env_long("NCM_MEM_WATCH_LOW_KB", MEM_WATCH_LOW_KB);
|
||||
g_cfg.critical_kb = env_long("NCM_MEM_WATCH_CRITICAL_KB", MEM_WATCH_CRITICAL_KB);
|
||||
g_quit = 0;
|
||||
pthread_attr_t a;
|
||||
pthread_attr_init(&a);
|
||||
pthread_attr_setstacksize(&a, (size_t)(128 * 1024)); /* 同 ncm_thread_create 128KB(内存瘦身,OOM 修复) */
|
||||
int rc = pthread_create(&g_thr, &a, watch_thread, NULL);
|
||||
pthread_attr_destroy(&a);
|
||||
if (rc != 0) { pthread_mutex_unlock(&g_mu); return -1; }
|
||||
g_thr_on = 1;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"[mem] watch started period=%dms H=%ldK L=%ldK C=%ldK (RssAnon 优先)",
|
||||
g_cfg.period_ms, g_cfg.high_kb, g_cfg.low_kb, g_cfg.critical_kb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mem_watch_stop(void)
|
||||
{
|
||||
g_quit = 1;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_thr_on) { g_thr_on = 0; pthread_detach(g_thr); }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
int mem_watch_level(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int v = g_level;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return v;
|
||||
}
|
||||
|
||||
long mem_watch_rss_anon_kb(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
long v = g_rss_anon_kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return v;
|
||||
}
|
||||
|
||||
void mem_watch_song_begin(void)
|
||||
{
|
||||
long kb = read_rss_kb(); /* 起播即采样当前内存,本曲峰值从当前起算 */
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_peak_song_kb = kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO, "[mem] song begin rss_anon=%ldKB", kb);
|
||||
}
|
||||
|
||||
/* 播完/切歌释放钩子之后调用:落本曲峰值 + 重置计数。 */
|
||||
void mem_watch_song_end(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
long peak = g_peak_song_kb;
|
||||
g_peak_song_kb = g_rss_anon_kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO, "[mem] song end peak_anon=%ldKB (released)", peak);
|
||||
}
|
||||
|
||||
void mem_watch_set_forced_kb(long kb)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_forced_kb = kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* play/mem_watch.h — 边播边监控**取证**(agent 32bit 大文件 OOM 取证;OOM_PAGECACHE_GOVERNANCE)。
|
||||
*
|
||||
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
|
||||
* A(DONTNEED 治理)+ C(/proc/meminfo Cached 监控)分别归 SDK meow_cachegov / 系统服务
|
||||
* meow_svc_perf;本模块退化为「RssAnon 取样取证」:一个轻量线程(ncm_thread_create 同款
|
||||
* 128KB 栈)低频(默认 500ms)读 /proc/self/status 的 RssAnon(缺失回退 VmRSS),记录匿名
|
||||
* 内存峰值(RssAnon 贴近「应用自有不可回收内存」,是 OOM kill 高危口径),**仅供日志/取证**,
|
||||
* 不再驱动下载/预取降载(那由 meow_cachegov_download_paused/prefetch_paused 查询)。
|
||||
*
|
||||
* 调度器结合释放钩子:mem_watch_song_begin/end 在 run_song 的起播/收尾(song_src_dispose +
|
||||
* ncm_dec_close + audio_out_close,即「彻底释放」钩子)处调用,重置本曲峰值计数并落日志
|
||||
* (峰值 RSS 即 RssAnon/RssFile 分离取证锚点;Cached/Dirty 侧取证归 svc_perf/meow_cachegov)。
|
||||
*
|
||||
* 阈值/开关可通过环境变量覆盖(宿主单测确定性驱动):NCM_MEM_WATCH_ENABLED /
|
||||
* NCM_MEM_WATCH_PERIOD_MS / NCM_MEM_WATCH_HIGH_KB / NCM_MEM_WATCH_LOW_KB / NCM_MEM_WATCH_CRITICAL_KB。
|
||||
* 宿主测试桩:mem_watch_set_forced_kb() 注入读值(>=0 时监控线程不再读真 proc,仅测取证逻辑)。
|
||||
*/
|
||||
#ifndef NCM_MEM_WATCH_H
|
||||
#define NCM_MEM_WATCH_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 内存压力档(仅取证日志用;不再驱动降载) */
|
||||
enum {
|
||||
NCM_MEM_LVL_NORMAL = 0, /* < HIGH */
|
||||
NCM_MEM_LVL_HIGH = 1, /* ≥ HIGH */
|
||||
NCM_MEM_LVL_CRITICAL = 2, /* ≥ CRITICAL */
|
||||
};
|
||||
|
||||
/* 启动监控线程(幂等;ncm_thread_create 128KB 栈)。返回 0=ok / -1=线程建失败。 */
|
||||
int mem_watch_start(void);
|
||||
/* 停止(置退出标志;线程 detach,不 join——同 ipc_media 模式)。 */
|
||||
void mem_watch_stop(void);
|
||||
|
||||
/* 当前压力档(0/1/2;仅取证日志)。 */
|
||||
int mem_watch_level(void);
|
||||
/* 最近一次采样的匿名 RSS(KB);-1=不可读。 */
|
||||
long mem_watch_rss_anon_kb(void);
|
||||
|
||||
/* ---- 调度器:与播放释放钩子联动 ---- */
|
||||
/* 起播一首曲时调用:本曲峰值计数清零,开始记录本曲内存画像。 */
|
||||
void mem_watch_song_begin(void);
|
||||
/* 播完/切歌、即「彻底释放」钩子(decoder 关+下载线程 join+源关+.part 收尾)执行后调用:
|
||||
* 落本曲峰值日志 + 重置计数,给下一首让出统计(内存本身已由释放钩子归还)。 */
|
||||
void mem_watch_song_end(void);
|
||||
|
||||
/* ---- 宿主测试桩(设备永不调用;≥0 时监控线程用注入值替代真 /proc 读)---- */
|
||||
void mem_watch_set_forced_kb(long kb);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* NCM_MEM_WATCH_H */
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <sys/stat.h>
|
||||
#include "decoder.h"
|
||||
#include "stream_src.h"
|
||||
#include "mem_watch.h"
|
||||
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
@@ -43,6 +44,10 @@ static char g_path[512]; /* 本地音频路径(缓存,mp3/flac
|
||||
static int64_t g_file_bytes;
|
||||
static int g_level; /* 音质档(来自设置) */
|
||||
static int g_cache_gb;
|
||||
/* 任务3(§re-entry):pending_replay=1 表示「切音质/缓存后点了正在播的同曲」——此时重播须换档
|
||||
* (P12 语义,seq++),不得被「同曲不重播」护栏吞掉。set_quality 置位;player_manager_play 的
|
||||
* 同曲护栏据它分流:pending=1 走重播(换档),pending=0 只聚焦不重播(点正在播的曲)。 */
|
||||
static int g_pending_replay;
|
||||
|
||||
/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁),
|
||||
* 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */
|
||||
@@ -90,12 +95,25 @@ void player_manager_set_quality(int level, int cache_gb)
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.err = 0;
|
||||
ncm_cache_set_limit_gb(&g_st.ctx.cache, cache_gb);
|
||||
g_pending_replay = 1; /* 任务3:改档后点正在播的同曲 → 强制换档重播(§25 P12) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
void player_manager_play(int song_idx)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
/* 任务3:点「正在播的曲」(同 idx 且 PLAYING/PAUSED,非切档重播)→ 只聚焦不重播:
|
||||
* 不 seq++、不置 BUFFER——旧曲继续出声/停原状态,UI 由调用方 nav 到播放页。
|
||||
* 注意:set_quality 已置 g_pending_replay=1(切音质后重播同曲须换档,P12),此护栏放行。
|
||||
* 非 PLAYING/PAUSED(STOPPED/另一曲)不受影响,照旧起播。 */
|
||||
if (song_idx == g_play.cur_song && !g_pending_replay &&
|
||||
(g_play.state == NCM_PC_PLAYING || g_play.state == NCM_PC_PAUSED)) {
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
meow_log(MEOW_LOG_INFO, "play: re-enter current idx=%d state=%d -> no restart",
|
||||
song_idx, g_play.state);
|
||||
return;
|
||||
}
|
||||
g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */
|
||||
g_play.cur_song = song_idx;
|
||||
g_play.cmd = 0;
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
@@ -108,6 +126,7 @@ void player_manager_play(int song_idx)
|
||||
g_play.underruns = 0; /* §21:边下边播统计随新播放意图清零 */
|
||||
g_play.underrun_ms = 0;
|
||||
g_play.stalled = 0;
|
||||
g_play.refaults = 0; /* §2.3:refault 取证随新播放意图清零 */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
@@ -135,6 +154,7 @@ void player_manager_next(int dir)
|
||||
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
|
||||
if (c >= cnt) c = cnt - 1;
|
||||
if (c != g_play.cur_song) {
|
||||
g_pending_replay = 0; /* 新播放意图落地:清「切档重播」待办 */
|
||||
g_play.cur_song = c;
|
||||
g_play.cmd = 0;
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
@@ -147,6 +167,7 @@ void player_manager_next(int dir)
|
||||
g_play.underruns = 0;
|
||||
g_play.underrun_ms = 0;
|
||||
g_play.stalled = 0;
|
||||
g_play.refaults = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
@@ -154,6 +175,7 @@ void player_manager_next(int dir)
|
||||
void player_manager_stop(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_pending_replay = 0; /* 停止:清「切档重播」待办 */
|
||||
g_play.cur_song = -1;
|
||||
g_play.state = NCM_PC_STOPPED;
|
||||
g_play.pos_ms = 0;
|
||||
@@ -189,8 +211,19 @@ typedef struct {
|
||||
int *st_ptr; /* 指向 dl_thread 的 status 局部变量(同线程,无竞态) */
|
||||
int need_fresh; /* 416/其它:服务器拒绝续传 → 放 .part、交调用方整首重下 */
|
||||
uint64_t skip; /* 200 忽略 Range:body 前 skip 字节与 .part 前缀重复,丢弃 */
|
||||
/* page-cache 治理(A:meow_cachegov)登记路径:播放实际读的文件(流式=.tmp/.part,命中=final)。 */
|
||||
char gov_path[512];
|
||||
/* 曲终 L0 用:commit 后的正式路径(流式 commit rename 后 = final;命中=gov_path 同)。 */
|
||||
char gov_final_path[512];
|
||||
} song_src_t;
|
||||
|
||||
/* page-cache 治理(A):SDK meow_cachegov 的 read_pos 回调——返回当前解码逻辑读指针
|
||||
* (stream_src 内部维护,经 stream_src_read_pos 读取;回调上下文=stream_src*)。 */
|
||||
static uint64_t gov_read_pos(void *ctx)
|
||||
{
|
||||
return stream_src_read_pos((stream_src_t *)ctx);
|
||||
}
|
||||
|
||||
/* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 +
|
||||
* 锁内推进 written + broadcast 唤醒读端 + 进度反馈。
|
||||
* 阶段3 续传(§5.2):首 body 字节按 response status 切换写姿态——
|
||||
@@ -201,6 +234,11 @@ static int stream_dl_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
{
|
||||
song_src_t *ss = (song_src_t *)ctx;
|
||||
if (stream_src_aborted(ss->src)) return 1; /* 切歌/停止:中止 http */
|
||||
/* page-cache 治理(§2.3/§3):下载 cb 查 meow_cachegov_download_paused()(A 回退硬暂停
|
||||
* OR svc_perf C Cached 降载)→ 暂停下载(限速),优先解码播放;不打断播放线程
|
||||
* (读端追上下载走既有 underrun/停滞联动)。每 50ms 查一次中止,切歌可立即脱身。 */
|
||||
while (meow_cachegov_download_paused() && !stream_src_aborted(ss->src))
|
||||
usleep(50 * 1000);
|
||||
if (ss->first_body) {
|
||||
ss->first_body = 0;
|
||||
int st = ss->st_ptr ? *ss->st_ptr : 0;
|
||||
@@ -295,6 +333,15 @@ static void song_src_dispose(song_src_t *ss)
|
||||
if (ss->src) { stream_src_close(ss->src); ss->src = NULL; }
|
||||
}
|
||||
|
||||
/* page-cache 治理(A)曲终清理:注销 L1 登记条目 + 下载完整(commit 成功)则对正式文件做
|
||||
* L0 整文件 DONTNEED(仅曲终安全窗口;未 commit/中断保留 .part 不丢)。 */
|
||||
static void song_gov_cleanup(song_src_t *ss)
|
||||
{
|
||||
if (ss->gov_path[0]) meow_cachegov_unregister(ss->gov_path);
|
||||
if (ss->dl_rc == NCM_OK && ss->gov_final_path[0])
|
||||
meow_cachegov_drop_finished(ss->gov_final_path);
|
||||
}
|
||||
|
||||
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
|
||||
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。
|
||||
* §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */
|
||||
@@ -587,6 +634,7 @@ static void run_song(int idx)
|
||||
g_play.play_song = s;
|
||||
g_play.play_song_valid = 1;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
mem_watch_song_begin(); /* 边播边监控:本曲峰值从当前起算(下载/解码/播放全程记录) */
|
||||
|
||||
song_src_t ss;
|
||||
int64_t file_bytes = 0;
|
||||
@@ -602,11 +650,22 @@ static void run_song(int idx)
|
||||
}
|
||||
if (!still_intent(idx, my_seq)) { /* 预缓冲完成但已切歌/同曲重播:勿开解码/勿动状态 */
|
||||
song_src_dispose(&ss);
|
||||
song_gov_cleanup(&ss); /* 未登记(register 在其后)时为 no-op,统一清理 */
|
||||
mem_watch_song_end(); /* 释放钩子:join 下载线程+关源+缓冲 free+.part 收尾 */
|
||||
return;
|
||||
}
|
||||
ncm_strlcpy(g_path, path, sizeof g_path);
|
||||
g_file_bytes = file_bytes;
|
||||
|
||||
/* page-cache 治理(A):登记本曲缓存文件(播放实际读的路径)+ read_pos 回调,供 L1 丢已读
|
||||
* 前缀 / refault 回退。cache 命中(ss.w==NULL)path=正式文件;流式 path=.tmp/.part
|
||||
* (cur file 即正被 pread 的文件,DONTNEED 只作用于它;正式路径待 commit 后 L0 用)。 */
|
||||
ncm_strlcpy(ss.gov_path, path, sizeof ss.gov_path);
|
||||
if (ss.w) ncm_strlcpy(ss.gov_final_path, ncm_cache_writer_final_path(ss.w), sizeof ss.gov_final_path);
|
||||
else ncm_strlcpy(ss.gov_final_path, path, sizeof ss.gov_final_path);
|
||||
meow_cachegov_register(ss.gov_path, (uint64_t)(file_bytes > 0 ? file_bytes : 0),
|
||||
gov_read_pos, ss.src);
|
||||
|
||||
int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0;
|
||||
|
||||
/* 解码分发(§19/§21):stream_src 上 magic 探测 fLaC→dr_flac / 其它→minimp3(open_cb
|
||||
@@ -616,6 +675,8 @@ static void run_song(int idx)
|
||||
int64_t total_frames = 0;
|
||||
if (ncm_dec_open_src(&dec, ss.src, &rate, &ch, &bits, &total_frames) != 0) {
|
||||
song_src_dispose(&ss);
|
||||
song_gov_cleanup(&ss);
|
||||
mem_watch_song_end(); /* 释放钩子 */
|
||||
if (!still_intent(idx, my_seq)) return; /* open 期间(flac 元数据等待)已切歌/重播 */
|
||||
set_err(NCM_ERR_IO, "decode open failed"); return;
|
||||
}
|
||||
@@ -623,7 +684,8 @@ static void run_song(int idx)
|
||||
|
||||
/* 音频输出:按实际流参数开(flac ≤16bit→S16,>16bit→S32;audiod 侧格式归一+原生率跟随) */
|
||||
if (audio_out_open(rate, ch, bits) != 0) {
|
||||
ncm_dec_close(dec); song_src_dispose(&ss);
|
||||
ncm_dec_close(dec); song_src_dispose(&ss); song_gov_cleanup(&ss);
|
||||
mem_watch_song_end(); /* 释放钩子 */
|
||||
set_err(NCM_ERR_IO, "audio open failed"); return;
|
||||
}
|
||||
|
||||
@@ -645,6 +707,7 @@ static void run_song(int idx)
|
||||
int paused_edge = 0; /* 暂停沿:进暂停已向 audiod 发 pause */
|
||||
int stalled_edge = 0; /* §21 停滞沿:已对 audiod pause(1),待数据恢复 pause(0) */
|
||||
int underruns_seen = 0;
|
||||
int refaults_seen = 0; /* §2.3:本曲 DONTNEED 丢页重读 TF 计数(取证) */
|
||||
|
||||
for (;;) {
|
||||
if (g_app_quit) break;
|
||||
@@ -751,6 +814,18 @@ static void run_song(int idx)
|
||||
idx, u, (unsigned long long)wm);
|
||||
}
|
||||
}
|
||||
/* page-cache 治理(A):refault 取证落 g_play + 节拍执行 L1 丢弃/refault 回退 */
|
||||
{
|
||||
int rf = stream_src_refaults(ss.src);
|
||||
if (rf != refaults_seen) {
|
||||
refaults_seen = rf;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.refaults = rf;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
meow_log(MEOW_LOG_INFO, "play: refault idx=%d count=%d", idx, rf);
|
||||
}
|
||||
meow_cachegov_tick();
|
||||
}
|
||||
/* 更新进度(~200ms 节流)*/
|
||||
int64_t pos = rate > 0 ? frames_done * 1000 / rate : 0;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
@@ -763,6 +838,9 @@ static void run_song(int idx)
|
||||
ncm_dec_close(dec);
|
||||
/* 下载完成由播放线程 commit(rename) / 中断 abort(阶段 2 语义,§21) */
|
||||
song_src_dispose(&ss);
|
||||
song_gov_cleanup(&ss); /* page-cache 治理(A)曲终:注销 L1 条目 + commit 成功则 L0 整文件 DONTNEED */
|
||||
mem_watch_song_end(); /* 调度器释放钩子:上一首缓冲完全清空(decoder 关 + 下载线程
|
||||
* join + 源关 + 缓冲 free + .part 收尾)——给下一首让内存 */
|
||||
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
|
||||
}
|
||||
|
||||
@@ -835,7 +913,9 @@ static void *lyric_worker(void *arg)
|
||||
}
|
||||
if (!id[0]) continue;
|
||||
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
|
||||
|
||||
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
|
||||
* (A 回退硬暂停 OR svc_perf C Cached 降载),
|
||||
* 不置 last_id → 降载恢复后下轮再拉。 */
|
||||
ncm_strlcpy(last_id, id, sizeof last_id);
|
||||
lyric_clear();
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
@@ -898,6 +978,7 @@ int player_manager_start(void)
|
||||
g_cache_gb = g_st.cache_limit_gb;
|
||||
pthread_mutex_init(&g_play.mu, NULL);
|
||||
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
|
||||
mem_watch_start(); /* 边播边监控(防 OOM):起监控线程读 RSS,驱下载降载 */
|
||||
if (ncm_thread_create(&g_thr, player_thread, NULL) != 0) return -1;
|
||||
g_thr_on = 1;
|
||||
return 0;
|
||||
@@ -908,6 +989,7 @@ void player_manager_stop_thread(void)
|
||||
g_want_run = 0;
|
||||
g_app_quit = 1;
|
||||
audio_out_close(); /* 打断阻塞写 */
|
||||
mem_watch_stop(); /* 停监控线程 */
|
||||
if (g_thr_on) {
|
||||
pthread_join(g_thr, NULL);
|
||||
g_thr_on = 0;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define REBUFFER_AFTER_MS 500 /* 单次调用累计等待超此值后启用再缓冲迟滞 */
|
||||
#define REBUFFER_MIN (64 * 1024) /* 迟滞放行门槛(≈1.6s@320kbps) */
|
||||
#define STALL_MS 1000 /* 单次调用等待超此值 → 置 stalled(播放线程 pause 联动) */
|
||||
#define STREAM_SRC_REFAULT_MS 50 /* pread 慢于此阈且数据已在盘上 → 计一次 refault(§2.3) */
|
||||
|
||||
struct stream_src {
|
||||
int fd; /* 只读 fd(pread;写端另有自己的写 fd) */
|
||||
@@ -36,8 +37,16 @@ struct stream_src {
|
||||
uint64_t wait_ms_total;
|
||||
int underruns;
|
||||
int stalled;
|
||||
int refaults; /* 数据在盘上但 pread 慢(>50ms)= DONTNEED 丢页重读 TF(§2.3) */
|
||||
};
|
||||
|
||||
/* SDK meow_cachegov 提供的 refault 上报(strong 时覆盖弱符号;未链 SDK 时为空指针,调用需判空)。 */
|
||||
#if defined(__GNUC__)
|
||||
extern void meow_cachegov_note_refault(void) __attribute__((weak));
|
||||
#else
|
||||
extern void meow_cachegov_note_refault(void);
|
||||
#endif
|
||||
|
||||
static int64_t mono_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
@@ -182,9 +191,18 @@ static size_t src_read(void *buf, size_t size, void *ud)
|
||||
if (avail > 0 && !gated) {
|
||||
size_t n = (size - done) < (size_t)avail ? (size - done) : (size_t)avail;
|
||||
uint64_t pos = s->read_pos;
|
||||
int had_data = (pos < s->written); /* 数据已在盘上(放锁前采样;勿在放锁后读 written) */
|
||||
pthread_mutex_unlock(&s->mu); /* pread 期间放锁,不阻塞写端 broadcast */
|
||||
int64_t t0 = mono_ms();
|
||||
ssize_t r = pread(s->fd, (char *)buf + done, n, (off_t)pos);
|
||||
int64_t pread_ms = mono_ms() - t0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
if (r > 0 && had_data && pread_ms > STREAM_SRC_REFAULT_MS) {
|
||||
/* 数据在盘上但 pread 慢 = DONTNEED 丢页后重读 TF(underrun 确认是 refault,
|
||||
* 非网络等待)。上报 SDK meow_cachegov(弱符号,未链时为空指针需判空)。 */
|
||||
s->refaults++;
|
||||
if (meow_cachegov_note_refault) meow_cachegov_note_refault();
|
||||
}
|
||||
if (r <= 0) { /* 盘 IO 错:按中断收尾(真 EOF 语义) */
|
||||
if (!s->failed) s->failed = -3; /* NCM_ERR_IO(本层不依赖 core 头) */
|
||||
s->final = s->written ? s->written : 1;
|
||||
@@ -290,6 +308,24 @@ uint64_t stream_src_written(stream_src_t *s)
|
||||
return w;
|
||||
}
|
||||
|
||||
int stream_src_refaults(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
int r = s->refaults;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return r;
|
||||
}
|
||||
|
||||
uint64_t stream_src_read_pos(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
uint64_t p = s->read_pos;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return p;
|
||||
}
|
||||
|
||||
uint64_t stream_src_final(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 1;
|
||||
|
||||
@@ -72,6 +72,15 @@ void stream_src_stats(stream_src_t *s, int *underruns, uint64_t *wait_ms);
|
||||
* 取走即清零。 */
|
||||
int stream_src_take_stalled(stream_src_t *s);
|
||||
|
||||
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE §2.3:underrun/refault 检测)----
|
||||
* refault = pread 处「数据已在盘上(written>read_pos)但读出耗时 >50ms」——DONTNEED 丢页后
|
||||
* 重读 TF 的信号,区别于网络 underrun(avail==0 等待)。SDK meow_cachegov 据此做 A 回退。
|
||||
* 播放线程读 refaults 落 g_play 取证;回调由 SDK(meow_cachegov_note_refault)提供,
|
||||
* 未链 SDK 时弱符号为空(host sim 兼容)。 */
|
||||
int stream_src_refaults(stream_src_t *s);
|
||||
/* 当前解码逻辑读指针(stream_src_seek_cb 改写)。 */
|
||||
uint64_t stream_src_read_pos(stream_src_t *s);
|
||||
|
||||
uint64_t stream_src_written(stream_src_t *s);
|
||||
uint64_t stream_src_final(stream_src_t *s); /* 0=下载中;>0=最终大小(完成=全长,中断=截断点) */
|
||||
int stream_src_failed(stream_src_t *s); /* 0=无错误;<0=下载/IO 错误码 */
|
||||
|
||||
@@ -266,10 +266,11 @@ int main(int argc, char **argv)
|
||||
player_touch_move(340, 200); /* 右移 100px */
|
||||
player_on_swipe(MEOW_SWIPE_RIGHT, 100); /* SDK 中部右滑 → SWIPE_RIGHT */
|
||||
int p2 = player_page();
|
||||
/* 起点在进度条上不锁存(防拖进度条误判翻页);(240,504)=进度条行 */
|
||||
player_touch_down(240, 504);
|
||||
player_touch_move(140, 504);
|
||||
player_on_tap(140, 504); /* 拖进度条抬手 → 应仍为封面页(seek 而非翻页) */
|
||||
/* 起点在进度条上不锁存(防拖进度条误判翻页);(240,484)=进度条行(§22 时间行下移后
|
||||
* g_bar.y=484,时间行在 500——拖时间行会误判翻页,故取进度条行本身) */
|
||||
player_touch_down(240, 484);
|
||||
player_touch_move(140, 484);
|
||||
player_on_tap(140, 484); /* 拖进度条抬手 → 应仍为封面页(seek 而非翻页) */
|
||||
int latched_change = player_page();
|
||||
int ok = (p0 == 0 && p1 == 1 && p2 == 0 && latched_change == 0);
|
||||
printf("gesture: p0=%d p1(左滑)=%d p2(右滑)=%d 省电锁存=%d -> %s\n",
|
||||
|
||||
@@ -120,6 +120,27 @@ static void home_draw(void *ctx)
|
||||
meow_ui_text(pc.x + asz + 28, pc.y + 6, g_st.nickname[0] ? g_st.nickname : "我的歌单",
|
||||
th->font_body, th->onSurface);
|
||||
meow_ui_text(pc.x + asz + 28, pc.y + 6 + th->font_body + 4, idtxt, th->font_body, th->onSurfaceVariant);
|
||||
/* §22 会员等级小字:account_vip() 枚举(用户语义 0=无 / 1=畅听会员 / 2=黑胶 VIP /
|
||||
* 3=超级会员);昵称右侧画小红徽章(red_ctr 底 + 红字),无会员则不画。 */
|
||||
{
|
||||
int vip = account_vip();
|
||||
if (vip != NCM_VIP_NONE) {
|
||||
const char *vip_lab = (vip == NCM_VIP_CVIP) ? "畅听会员"
|
||||
: (vip == NCM_VIP_SVIP) ? "黑胶 VIP"
|
||||
: (vip == NCM_VIP_SUPER) ? "超级会员" : "";
|
||||
const char *nm = g_st.nickname[0] ? g_st.nickname : "我的歌单";
|
||||
int nmw = meow_ui_text_width(nm, th->font_body);
|
||||
int bs = th->font_body - 2; /* 徽章字号略小 */
|
||||
if (bs < 16) bs = 16;
|
||||
int bw = meow_ui_text_width(vip_lab, bs) + 12;
|
||||
int bx = pc.x + asz + 28 + nmw + 8;
|
||||
int by = pc.y + 6 + (th->font_body - (bs + 4)) / 2;
|
||||
if (bx + bw <= pc.x + pc.w - 24) { /* 不越卡片右缘(避开“>”) */
|
||||
ncm_fill_round(bx, by, bw, bs + 4, 6, pa->red_ctr);
|
||||
meow_ui_text_center(bx + bw / 2, by + 2, vip_lab, bs, pa->red);
|
||||
}
|
||||
}
|
||||
}
|
||||
meow_ui_text_right(pc.x + pc.w - 16, pc.y + (pc.h - 28) / 2, ">", 28, th->onSurfaceVariant);
|
||||
|
||||
/* 双页签(g_tabs=meow_tabbar widget 建)——红下划线/红选中字叠层。 */
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
* - 封面页:方形大图+圆角(cover.c LRU,无旋转动画)+ 进度条(可点 seek)+ MD3 矢量控制钮
|
||||
* (prev/play-pause/next,ncm_icons.h 32x32 单通道 alpha,参照 sb_icons.h 范式)
|
||||
* + 元信息 + 实际音质档 + 页面指示点;缓冲/错误态沿用(「缓冲中…N%」/中文错误条+重试)。
|
||||
* - 歌词页:LRC 滚动(当前行高亮居中,上下行渐隐)+ 点行跳转;默认=封面模糊置底,
|
||||
* 省电=封面背景+黑色半透明叠加层(设置 ncm.player_bg_mode: default/power_save)。
|
||||
* - 歌词页:LRC 滚动(活动行纯白偏上锚定,上下行逐级灰阶)+ 点行跳转;默认=封面模糊置底
|
||||
* (低透明非纯黑中性叠加层,需求1 加深),省电=封面背景+黑色半透明叠加层
|
||||
* (设置 ncm.player_bg_mode: default/power_save)。
|
||||
* - 日夜间切换:主题配色只映射到半透明叠加层(歌词页 scrim)与 accent(红),
|
||||
* 不动沉浸底(恒 NCM_PLAYER_BG)。
|
||||
*
|
||||
@@ -39,6 +40,35 @@
|
||||
#define PLR_EDGE_W 24 /* 左缘返回热区(与 SDK EDGE_W 一致,防抢返回) */
|
||||
#define PLR_SWIPE_MS 65 /* 中部横滑判页阈值(与 SDK SWIPE_THRESH 一致) */
|
||||
|
||||
/* 歌词页布局(§22 重做批):左边距 / 子行高 / 可见行数上限 / 折行段缓冲。
|
||||
* 超宽歌词折行显示(中西文兼容)+ 活动行高亮(纯白)偏上锚定 + 上下行逐级灰阶,
|
||||
* 顶部元信息与歌词正文同左边距。 */
|
||||
#define LYR_PAD 24 /* 歌词页左边距(顶部元信息与歌词正文一致) */
|
||||
#define LYR_SUB_LH 44 /* 一个歌词条目(含折行)的子行高 */
|
||||
#define LYR_NAME_SZ 28 /* 顶部歌名字号 */
|
||||
#define LYR_ART_SZ 20 /* 顶部歌手字号 */
|
||||
#define LYR_CUR_SZ 28 /* 当前歌词行字号 */
|
||||
#define LYR_OTH_SZ 24 /* 上下行歌词字号 */
|
||||
#define LYR_TL_SZ 20 /* 当前行翻译字号 */
|
||||
#define LYR_MAX_ROWS 9 /* 歌词页可见视觉行数上限(含折行,≤9) */
|
||||
#define LYR_SEG_SZ 256 /* 折行段缓冲 */
|
||||
/* 需求3 活动行锚定:块中心相对屏中线偏上 LYR_ABOVE_OFF px;上方至少 LYR_ABOVE_MIN 行已过歌词。 */
|
||||
#define LYR_ABOVE_OFF 50
|
||||
#define LYR_ABOVE_MIN 2
|
||||
/* 需求1 叠加层加深(非纯黑,保留封面微透出 → 歌词清晰、封面隐约):
|
||||
* default=近黑中性,alpha 132→176、色值略暗;power_save=纯黑 alpha 195→200。 */
|
||||
#define LYR_SCRIM_D 0x10101A /* 默认层暗色 scrim */
|
||||
#define LYR_SCRIM_L 0x201A16 /* 默认层亮色 scrim(暖黑) */
|
||||
#define LYR_SCRIM_PS 0x000000 /* 省电层 scrim(纯黑) */
|
||||
#define LYR_SCRIM_A 176
|
||||
#define LYR_SCRIM_PS_A 200
|
||||
|
||||
/* 需求2 歌词行灰阶:按距活动行行距 0..5 分级(0=活动行纯白,越远越灰;最远可见 0x555555)。
|
||||
* 锚点对照用户需求:±1 行 0xBBBBBB、中深 0x808080、最远 0x555555,逐级单调变灰。 */
|
||||
static const uint32_t lyr_gray_tbl[6] = {
|
||||
0xFFFFFF, 0xBBBBBB, 0x929292, 0x808080, 0x6A6A6A, 0x555555
|
||||
};
|
||||
|
||||
typedef struct { meow_view *view; int active; } player_ctrl;
|
||||
static player_ctrl g_plc;
|
||||
|
||||
@@ -63,8 +93,14 @@ static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_r_side, g_r_play, g_dots_y;
|
||||
|
||||
/* ---- 歌词页几何 ---- */
|
||||
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
|
||||
static int g_lyr_lh; /* 行高 */
|
||||
static int g_lyr_meta_y; /* 顶部元信息 y */
|
||||
static int g_lyr_anchor; /* 活动行块中心 y(屏中线偏上,需求3) */
|
||||
static int g_lyr_meta_y; /* 顶部元信息 y(避开状态栏) */
|
||||
/* 歌词布局暂存(每次 plr_draw_lyrics 重建;UI 线程串行,静态复用) */
|
||||
static char s_lyr_seg[LYR_MAX_ROWS][LYR_SEG_SZ]; /* 折行段复用缓冲(一次画一个条目) */
|
||||
static int s_lyr_ent[LYR_MAX_ROWS]; /* 可见条目 → 歌词行下标 */
|
||||
static int s_lyr_rw[LYR_MAX_ROWS]; /* 条目折行段数 */
|
||||
static int s_lyr_sz[LYR_MAX_ROWS]; /* 条目标题字号 */
|
||||
static int s_lyr_y[LYR_MAX_ROWS]; /* 条目块顶 y */
|
||||
|
||||
#define CTL_RR (g_r_side + 8) /* 侧钮命中容差(圆外一点仍可点) */
|
||||
|
||||
@@ -90,6 +126,108 @@ static void fmt_ms(int64_t ms, char *out, size_t sz)
|
||||
snprintf(out, sz, "%d:%02d", sec / 60, sec % 60);
|
||||
}
|
||||
|
||||
/* 折行:把 txt 按「词/字符」折成 ≤max_rows 段,每段 ≤maxw px。中文按字符、英文按词断行;
|
||||
* 宽测量用 meow_ui_text_width(宿主/生产同口径)。段写入 seg[][LYR_SEG_SZ],返回段数(≥1)。 */
|
||||
static int lyr_wrap_lines(const char *txt, int size, int maxw,
|
||||
char seg[][LYR_SEG_SZ], int max_rows)
|
||||
{
|
||||
if (!txt || !txt[0] || max_rows <= 0) { seg[0][0] = 0; return 1; }
|
||||
int n = 0;
|
||||
size_t len = strlen(txt), pos = 0;
|
||||
while (pos < len && n < max_rows) {
|
||||
size_t start = pos;
|
||||
int last_ws = -1; /* 本行最后一个空格(相对 start 的字节偏移) */
|
||||
int overflow = 0;
|
||||
while (pos < len) {
|
||||
unsigned char c = (unsigned char)txt[pos];
|
||||
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
|
||||
if (pos + (size_t)ch > len) ch = (int)(len - pos);
|
||||
size_t cand = (pos + (size_t)ch) - start;
|
||||
size_t cp = cand < LYR_SEG_SZ - 1 ? cand : LYR_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
if (meow_ui_text_width(seg[n], size) > maxw && cand > 0) { overflow = 1; break; }
|
||||
if (txt[pos] == ' ' || txt[pos] == '\t') last_ws = (int)(pos - start);
|
||||
pos += (size_t)ch;
|
||||
}
|
||||
if (overflow) {
|
||||
size_t linelen = (last_ws > 0) ? (size_t)last_ws : (pos - start);
|
||||
if (linelen == 0) { /* 单字符超宽(罕见):至少放一个字符防死循环 */
|
||||
unsigned char c = (unsigned char)txt[start];
|
||||
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
|
||||
linelen = (size_t)ch;
|
||||
}
|
||||
size_t cp = linelen < LYR_SEG_SZ - 1 ? linelen : LYR_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
pos = start + linelen;
|
||||
if (last_ws > 0) {
|
||||
size_t sp = start + (size_t)last_ws + 1;
|
||||
if (sp > pos) pos = sp; /* 跳过断点后的一个空格 */
|
||||
}
|
||||
n++;
|
||||
} else {
|
||||
size_t cp = (len - start) < LYR_SEG_SZ - 1 ? (len - start) : LYR_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
n++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n == 0) { seg[0][0] = 0; n = 1; }
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 歌词可见条目布局:活动行块中心锚定在 g_lyr_anchor(屏中线偏上,需求3 中间偏上),
|
||||
* 活动行上方保底 ≥LYR_ABOVE_MIN 行已过歌词,下方填满,总视觉行数 ≤ LYR_MAX_ROWS(≤9)。
|
||||
* 结果写 s_lyr_ent/rw/sz/y(s_lyr_y 自上向下排好;当前行块中心 = g_lyr_anchor)。 */
|
||||
static int lyr_layout(int n, int cur, int maxw)
|
||||
{
|
||||
/* 清静态暂存(防帧间残留:上帧条目数 > 本周,绘制循环读到陈旧 s_lyr_* 越界复用) */
|
||||
for (int k = 0; k < LYR_MAX_ROWS; k++) { s_lyr_rw[k] = 0; s_lyr_ent[k] = -1; s_lyr_y[k] = 0; }
|
||||
if (cur < 0 || cur >= n) return 0;
|
||||
|
||||
int rc = lyr_wrap_lines(lyric_line_text(cur), LYR_CUR_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
|
||||
if (rc > LYR_MAX_ROWS) rc = LYR_MAX_ROWS;
|
||||
|
||||
int cap = LYR_MAX_ROWS - rc; /* 当前行之外可用的视觉行数 */
|
||||
int above_want = (cap * 2) / 5; /* 偏上锚定:约 40% 行给上方 */
|
||||
if (above_want > cur) above_want = cur; /* 上方行数上限 = 已过行数 */
|
||||
if (above_want < LYR_ABOVE_MIN) above_want = (cur >= LYR_ABOVE_MIN) ? LYR_ABOVE_MIN : cur;
|
||||
if (above_want > cap) above_want = cap; /* 总行数 ≤ LYR_MAX_ROWS */
|
||||
|
||||
/* 1) 上方(cur-1 向上)前插到头部,保持自顶向下 */
|
||||
int a_cnt = 0, usedA = 0;
|
||||
for (int i = cur - 1; i >= 0 && usedA < above_want; i--) {
|
||||
int rr = lyr_wrap_lines(lyric_line_text(i), LYR_OTH_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
|
||||
if (usedA + rr > above_want) break;
|
||||
for (int k = a_cnt; k > 0; k--) {
|
||||
s_lyr_ent[k] = s_lyr_ent[k - 1]; s_lyr_rw[k] = s_lyr_rw[k - 1];
|
||||
s_lyr_sz[k] = s_lyr_sz[k - 1];
|
||||
}
|
||||
s_lyr_ent[0] = i; s_lyr_rw[0] = rr; s_lyr_sz[0] = LYR_OTH_SZ; a_cnt++; usedA += rr;
|
||||
}
|
||||
/* 2) 当前行(下标 a_cnt) */
|
||||
s_lyr_ent[a_cnt] = cur; s_lyr_rw[a_cnt] = rc; s_lyr_sz[a_cnt] = LYR_CUR_SZ;
|
||||
/* 3) 下方(cur+1 向下):余量 = cap - actual 上方行数 */
|
||||
int below_want = cap - usedA;
|
||||
if (below_want > (n - 1 - cur)) below_want = n - 1 - cur; /* 下方行数上限 */
|
||||
int b_cnt = 0, usedB = 0;
|
||||
for (int i = cur + 1; i < n && usedB < below_want; i++) {
|
||||
int rr = lyr_wrap_lines(lyric_line_text(i), LYR_OTH_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
|
||||
if (usedB + rr > below_want) break;
|
||||
int k = a_cnt + 1 + b_cnt;
|
||||
s_lyr_ent[k] = i; s_lyr_rw[k] = rr; s_lyr_sz[k] = LYR_OTH_SZ; b_cnt++; usedB += rr;
|
||||
}
|
||||
int cnt = a_cnt + 1 + b_cnt;
|
||||
if (cnt == 0) return 0;
|
||||
|
||||
/* 4) y 位置:活动行块中心 = g_lyr_anchor;上方向上加、下方向下加(块间无缝隙) */
|
||||
s_lyr_y[a_cnt] = g_lyr_anchor - rc * LYR_SUB_LH / 2;
|
||||
for (int k = a_cnt + 1; k < cnt; k++)
|
||||
s_lyr_y[k] = s_lyr_y[k - 1] + s_lyr_rw[k - 1] * LYR_SUB_LH;
|
||||
for (int k = a_cnt - 1; k >= 0; k--)
|
||||
s_lyr_y[k] = s_lyr_y[k + 1] - s_lyr_rw[k] * LYR_SUB_LH;
|
||||
return cnt;
|
||||
}
|
||||
|
||||
/* 颜色插值(t=0..255 混向 b)。播放页歌词渐隐/叠加用(无 GPU,逐通道整数混合)。 */
|
||||
static uint32_t mix_c(uint32_t a, uint32_t b, int t)
|
||||
{
|
||||
@@ -177,19 +315,22 @@ static void player_compute_geom(void)
|
||||
g_title_y = g_cover_r.y + g_cover_r.h + 32; /* 400 */
|
||||
g_artist_y = g_title_y + 32; /* 432 */
|
||||
g_qual_y = g_artist_y + 26; /* 458 */
|
||||
g_times_y = g_qual_y + 26; /* 484 */
|
||||
g_bar = NR(48, g_times_y + 20, W - 96, 4); /* 504 (h=4) */
|
||||
g_ctl_y = g_bar.y + 64; /* 568 */
|
||||
/* §22 时间行:放大字号并下移到进度条下方,不与进度条相交。
|
||||
* 布局:进度条(484,h=4) → 时间行(500, font 24) → 控制钮(564)。 */
|
||||
g_bar = NR(48, g_qual_y + 26, W - 96, 4); /* 484 (h=4) */
|
||||
g_times_y = g_bar.y + g_bar.h + 12; /* 500 */
|
||||
g_ctl_y = g_times_y + 64; /* 564 */
|
||||
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
|
||||
g_r_side = 24; g_r_play = 34;
|
||||
g_dots_y = g_ctl_y + 62; /* 630 */
|
||||
g_dots_y = g_ctl_y + 62; /* 626 */
|
||||
|
||||
/* 动态带 = 封面下方(元信息/进度/控制/指示点),与封面静态带不重叠。 */
|
||||
g_dyn_r = NR(0, g_cover_r.y + g_cover_r.h, W, H - (g_cover_r.y + g_cover_r.h));
|
||||
|
||||
g_lyr_cx = H / 2; /* 360 */
|
||||
g_lyr_lh = 30;
|
||||
g_lyr_meta_y = 22;
|
||||
/* 歌词页几何:中心线 / 活动行块中心(屏中线偏上,需求3)/ 顶部元信息 y。 */
|
||||
g_lyr_cx = H / 2; /* 360 */
|
||||
g_lyr_anchor = g_lyr_cx - LYR_ABOVE_OFF; /* 310 活动行偏上锚定 */
|
||||
g_lyr_meta_y = ncm_statusbar() + 20;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
@@ -280,8 +421,9 @@ static void plr_draw_cover_dyn(void *ctx)
|
||||
}
|
||||
meow_ui_text_center(W / 2, g_times_y - 8, bbuf, 20, PLR_TEXT_DIM);
|
||||
} else {
|
||||
meow_ui_text(48, g_times_y, lp, 20, PLR_TEXT_DIM);
|
||||
meow_ui_text_right(W - 48, g_times_y, rp, 20, PLR_TEXT_DIM);
|
||||
/* §22 时间字号放大(20→24)且下移到进度条下方,不再与进度条相交 */
|
||||
meow_ui_text(48, g_times_y, lp, 24, PLR_TEXT_DIM);
|
||||
meow_ui_text_right(W - 48, g_times_y, rp, 24, PLR_TEXT_DIM);
|
||||
}
|
||||
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, PLR_TRACK_BG);
|
||||
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
|
||||
@@ -297,73 +439,75 @@ static void plr_draw_cover_dyn(void *ctx)
|
||||
draw_page_dots(g_dots_y);
|
||||
}
|
||||
|
||||
/* 歌词页(整屏带状物):封面模糊置底 + scrim + 顶部元信息 + 滚动歌词(当前行高亮居中 +
|
||||
* 上下行渐隐)+ 指示点。整屏在 进入/切歌/封面变化/换背景模式/换行 时重画;pos tick 不画。 */
|
||||
/* 歌词页(整屏带状物):封面模糊置底 + 加深 scrim + 顶部元信息 + 滚动歌词(活动行纯白偏上锚定 +
|
||||
* 上下行逐级灰阶)+ 指示点。整屏在 进入/切歌/封面变化/换背景模式/换行 时重画;pos tick 不画。 */
|
||||
static void plr_draw_lyrics(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 1) return;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||||
int W = g_W, H = g_H;
|
||||
|
||||
/* 底:封面等比居中放大(contain:等比缩放到屏内最大、居中;四周主题底),无封面 → 沉浸底。
|
||||
* 不拉伸铺满(方图硬拉 480×720 变形丑,2026-09-02 用户报)。
|
||||
* ⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏 → 下次 paint 自锁死,§18)。 */
|
||||
/* 底:封面铺满全屏(cover-fit,四周无黑边;无 GPU 高斯模糊,用低透明中性叠层近似「模糊底」),
|
||||
* 无封面 → 主题中性底(非纯黑)。⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏,
|
||||
* 下次 paint 自锁死,§18)。§22:default=封面+半透明中性叠层;power_save=封面+黑色更重叠层。 */
|
||||
meow_img *cov = cover_get();
|
||||
meow_ui_fill_rect(0, 0, W, H, NCM_PLAYER_BG);
|
||||
meow_ui_fill_rect(0, 0, W, H, 0x1A1A20); /* 无封面兜底主题底(中性深灰,非纯黑) */
|
||||
if (cov) {
|
||||
int cw = meow_img_w(cov), ch = meow_img_h(cov);
|
||||
if (cw > 0 && ch > 0) {
|
||||
int dw = W, dh = (int)((long)ch * W / cw);
|
||||
if (dh > H) { dh = H; dw = (int)((long)cw * H / ch); }
|
||||
meow_ui_img_scaled((W - dw) / 2, (H - dh) / 2, dw, dh, cov);
|
||||
long dw = W, dh = (long)ch * W / cw; /* cover-fit:铺满宽度,高按等比超出裁剪 */
|
||||
if (dh < H) { dh = H; dw = (long)cw * H / ch; }
|
||||
meow_ui_img_scaled((W - (int)dw) / 2, (H - (int)dh) / 2, (int)dw, (int)dh, cov);
|
||||
}
|
||||
}
|
||||
cover_put();
|
||||
/* 半透明叠加层:default=主题 scrim(日/夜跟随主题);power_save=黑色更重(§22)。 */
|
||||
/* 半透明叠加层:default=主题中性(近黑但不纯黑,封面色彩微透出 → 模糊底观感);
|
||||
* power_save=黑色更重(§22 保留原「封面背景+黑叠加」语义)。需求1 加深:alpha/色值加暗,
|
||||
* 但仍保留「封面隐约」而非纯黑整体吞掉。 */
|
||||
if (g_st.player_bg_mode == NCM_BG_POWER_SAVE)
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 195);
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, LYR_SCRIM_PS, LYR_SCRIM_PS_A);
|
||||
else
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, th->scrim, 150);
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, g_dark ? LYR_SCRIM_D : LYR_SCRIM_L, LYR_SCRIM_A);
|
||||
|
||||
/* 顶部元信息(title · artist,Apple Music 式顶部小字) */
|
||||
/* 顶部元信息(两行居左):第一行歌名(大字)、第二行歌手名(小字,多歌手保留已拼串)。
|
||||
* 与歌词正文同左边距 LYR_PAD;下移避开状态栏(g_lyr_meta_y = statusbar + 20)。 */
|
||||
ncm_song_t sm;
|
||||
if (cur_song_meta(&sm) == 0) {
|
||||
char mb[300];
|
||||
snprintf(mb, sizeof mb, "%s / %s", sm.name, sm.art_name);
|
||||
meow_ui_text_center(W / 2, g_lyr_meta_y, ncm_text_fit(mb, W - 48, 18), 18, PLR_TEXT);
|
||||
meow_ui_text(LYR_PAD, g_lyr_meta_y,
|
||||
ncm_text_fit(sm.name, W - 2 * LYR_PAD, LYR_NAME_SZ), LYR_NAME_SZ, PLR_TEXT);
|
||||
meow_ui_text(LYR_PAD, g_lyr_meta_y + LYR_NAME_SZ + 6,
|
||||
ncm_text_fit(sm.art_name, W - 2 * LYR_PAD, LYR_ART_SZ), LYR_ART_SZ, PLR_TEXT_DIM);
|
||||
}
|
||||
|
||||
/* 歌词:当前行居中高亮;上下行渐隐(距中心越远越接近底色)。 */
|
||||
/* 歌词:活动行纯白居中(含折行,块中心偏上锚定);上下行按行距逐级灰阶;≤LYR_MAX_ROWS。 */
|
||||
int n = lyric_count();
|
||||
int64_t ps = player_manager_pos_ms();
|
||||
int cur = (n > 0) ? lyric_line_at(ps) : -1;
|
||||
int cy = g_lyr_cx;
|
||||
int lo = g_lyr_meta_y + 44, hi = g_dots_y - 6; /* 歌词窗口(避顶部元信息/底部指示点) */
|
||||
for (int i = cur - 7; i <= cur + 6; i++) {
|
||||
if (i < 0 || i >= n) continue;
|
||||
int yy = cy + (i - cur) * g_lyr_lh;
|
||||
if (yy < lo || yy + g_lyr_lh > hi) continue;
|
||||
int maxw = W - 2 * LYR_PAD;
|
||||
int cnt = lyr_layout(n, cur, maxw);
|
||||
/* 渐隐目标色 = 歌词页背底层(与 default/power_save 叠层一致),供翻译行融入背景 */
|
||||
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? LYR_SCRIM_PS
|
||||
: (g_dark ? LYR_SCRIM_D : LYR_SCRIM_L);
|
||||
for (int k = 0; k < cnt; k++) {
|
||||
int i = s_lyr_ent[k], rows = s_lyr_rw[k], sz = s_lyr_sz[k], y = s_lyr_y[k];
|
||||
int is_cur = (i == cur);
|
||||
int dist = (i - cur) < 0 ? (cur - i) : (i - cur);
|
||||
int fade = dist * 42; /* 0..255,越远越淡 */
|
||||
if (fade > 214) fade = 214; /* 远行仍隐约可见,不全隐 */
|
||||
uint32_t base = is_cur ? pa->red_lit : PLR_TEXT;
|
||||
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? 0x000000 : th->scrim;
|
||||
uint32_t col = mix_c(base, bgcol, fade);
|
||||
int size = is_cur ? 24 : 20;
|
||||
meow_ui_text_center(W / 2, yy, ncm_text_fit(lyric_line_text(i), W - 24, size), size, col);
|
||||
int dist = is_cur ? 0 : ((i - cur) < 0 ? (cur - i) : (i - cur)); /* 距活动行行距 */
|
||||
uint32_t col = lyr_gray_tbl[dist > 5 ? 5 : dist]; /* 0=纯白,越远越灰 */
|
||||
int rr = lyr_wrap_lines(lyric_line_text(i), sz, maxw, s_lyr_seg, rows);
|
||||
if (rr > rows) rr = rows;
|
||||
for (int r = 0; r < rr; r++)
|
||||
meow_ui_text(LYR_PAD, y + r * LYR_SUB_LH, s_lyr_seg[r], sz, col);
|
||||
if (is_cur) {
|
||||
const char *tl = lyric_line_tl(i);
|
||||
if (tl && tl[0])
|
||||
meow_ui_text_center(W / 2, yy + 28, ncm_text_fit(tl, W - 24, 18), 18,
|
||||
mix_c(pa->red_lit, bgcol, 120));
|
||||
meow_ui_text(LYR_PAD, y + rows * LYR_SUB_LH + 2,
|
||||
ncm_text_fit(tl, W - 2 * LYR_PAD, LYR_TL_SZ), LYR_TL_SZ,
|
||||
mix_c(0xFFFFFF, bgcol, 120));
|
||||
}
|
||||
}
|
||||
if (n == 0) {
|
||||
meow_ui_text_center(W / 2, cy - 10,
|
||||
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…", 20, PLR_TEXT_DIM);
|
||||
meow_ui_text(LYR_PAD, g_lyr_cx - 10,
|
||||
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…", 20, PLR_TEXT_DIM);
|
||||
}
|
||||
|
||||
draw_page_dots(g_dots_y);
|
||||
@@ -466,21 +610,27 @@ static void player_reset_touch(void)
|
||||
g_touch_on = 0; g_drag_latched = 0; g_drag_dx = 0;
|
||||
}
|
||||
|
||||
/* 歌词页:点行 seek(把 tap y 归到最近一行的下标)。 */
|
||||
/* 歌词页:点行 seek(用与绘制一致的可见条目布局把 tap y 命中最临近行;
|
||||
* 活动行已偏上锚定,非屏中线,故按 s_lyr_y/rw 逐块扫描而非 cx 偏移)。 */
|
||||
static int lyrics_line_tap(int y)
|
||||
{
|
||||
int n = lyric_count();
|
||||
if (n <= 0) return -1;
|
||||
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。
|
||||
* 元信息块 = 歌名(28)+6+歌手(20)+边距 → g_lyr_meta_y+66。 */
|
||||
if (y < g_lyr_meta_y + 66 || y > g_dots_y - 6) return -1;
|
||||
int64_t ps = player_manager_pos_ms();
|
||||
int cur = lyric_line_at(ps);
|
||||
int off = y - g_lyr_cx;
|
||||
int dioff = (off >= 0) ? (off + g_lyr_lh / 2) / g_lyr_lh : (off - g_lyr_lh / 2) / g_lyr_lh;
|
||||
int i = cur + dioff;
|
||||
if (i < 0) i = 0;
|
||||
if (i >= n) i = n - 1;
|
||||
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。 */
|
||||
if (y < g_lyr_meta_y + 44 || y > g_dots_y - 6) return -1;
|
||||
return i;
|
||||
int maxw = g_W - 2 * LYR_PAD;
|
||||
int cnt = lyr_layout(n, cur, maxw);
|
||||
if (cnt <= 0) return -1;
|
||||
for (int k = 0; k < cnt; k++) {
|
||||
int yb = s_lyr_y[k];
|
||||
int yh = s_lyr_rw[k] * LYR_SUB_LH;
|
||||
if (y >= yb && y < yb + yh) return s_lyr_ent[k];
|
||||
}
|
||||
/* 落在可见块之外 → 就近取整(顶部元信息/底部指示点已在上面排除) */
|
||||
return (y < s_lyr_y[0]) ? s_lyr_ent[0] : s_lyr_ent[cnt - 1];
|
||||
}
|
||||
|
||||
static int in_circle(int cx, int cy, int r, int x, int y)
|
||||
|
||||
@@ -268,6 +268,17 @@ static void *playlist_worker(void *arg)
|
||||
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */
|
||||
if (tidx < 0) rc = NCM_ERR_ARG;
|
||||
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
|
||||
/* §22 整张歌单逐渐物化:首屏成功后把「能装进滑动窗口」的歌单一次性补填入窗
|
||||
* (≤NCM_SONG_WINDOW 的歌单整张物化,末叶行不再恒「.../加载中」——真机反馈
|
||||
* 「曲目稍多时行一直加载中,点开播放这首才回填」);超窗口的歌单仍按视口滑动补载
|
||||
* (内存纪律:窗口 ≤256 首)。窗口基址=0 覆盖 [0,min(track,WINDOW)),不滑窗不挤占视口。 */
|
||||
if (rc == NCM_OK) {
|
||||
int full = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
|
||||
if (full > NCM_SONG_BATCH) {
|
||||
int r2 = appstate_song_ensure_range(0, full);
|
||||
if (r2 != NCM_OK && rc == NCM_OK) rc = r2; /* 补填失败非致命:已物化槽保留 */
|
||||
}
|
||||
}
|
||||
/* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc,
|
||||
* 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命
|
||||
* g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* ui/settings.c — 设置页(ACT_SETTINGS)。
|
||||
*
|
||||
* 行项:音质(§20 八档:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声,
|
||||
* 默认极高 HQ;每档标注规格+会员门槛 CVIP/SVIP;超权限档允许选定,播放时钳顶+逐级降级,
|
||||
* 默认极高 HQ;每档标注规格+会员门槛(畅听会员/黑胶 VIP/超级会员);超权限档允许选定,播放时钳顶+逐级降级,
|
||||
* 见 player_manager.c song_local_file)+ 缓存上限(默认 10GB,上限 100GB,支持预设与
|
||||
* 「自定义」数字输入)+ 清理缓存(显示当前占用)+ 退出登录 + 关于/版本。
|
||||
* 设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
|
||||
@@ -80,9 +80,12 @@ static void settings_draw(void *ctx)
|
||||
rows[4].title = "退出登录"; rows[4].red = 1;
|
||||
rows[5].title = "关于网易云"; rows[5].red = 0;
|
||||
{
|
||||
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20) */
|
||||
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20)。
|
||||
* 门槛串按用户语义对齐:1=畅听会员 / 2=黑胶 VIP / 3=超级会员(门槛恒 ≤2,SUPER 兜底)。 */
|
||||
int gate = ncm_api_level_gate(g_st.level);
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : "";
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "黑胶 VIP"
|
||||
: (gate == NCM_VIP_CVIP) ? "畅听会员"
|
||||
: (gate == NCM_VIP_SUPER) ? "超级会员" : "";
|
||||
int locked = (account_vip() < gate);
|
||||
if (gate_s[0])
|
||||
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s) / 需 %s%s",
|
||||
@@ -112,7 +115,7 @@ static void settings_draw(void *ctx)
|
||||
|
||||
/* 品牌红页脚小字。 */
|
||||
int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4;
|
||||
meow_ui_text(x, ny, "无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级", 20, th->onSurfaceVariant);
|
||||
meow_ui_text(x, ny, "无损/Hi-Res 需畅听会员 ・ 臻音/母带需黑胶 VIP,不支持时自动降级", 20, th->onSurfaceVariant);
|
||||
meow_ui_text(x, ny + 24, "网易云音乐 / MeowOS 移植版", 20, pa->red);
|
||||
}
|
||||
|
||||
@@ -170,9 +173,11 @@ int settings_on_tap(int x, int y)
|
||||
int lv = g_ui_levels[i];
|
||||
int gate = ncm_api_level_gate(lv);
|
||||
char lbl[96];
|
||||
/* 每档标注 规格+会员门槛;未解锁档标注「需 CVIP/SVIP」(sheet 无置灰能力,
|
||||
/* 每档标注 规格+会员门槛;未解锁档标注「需 畅听会员/黑胶 VIP」(sheet 无置灰能力,
|
||||
* 以文字标注代替——允许选定,实际播放逐级降级到可用档) */
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : NULL;
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "黑胶 VIP"
|
||||
: (gate == NCM_VIP_CVIP) ? "畅听会员"
|
||||
: (gate == NCM_VIP_SUPER) ? "超级会员" : NULL;
|
||||
if (gate_s)
|
||||
snprintf(lbl, sizeof lbl, "%s (%s) / %s%s", ncm_level_name(lv),
|
||||
ncm_level_spec(lv), gate_s, (vip < gate) ? "/未解锁" : "");
|
||||
|
||||
@@ -413,6 +413,24 @@ int main(void)
|
||||
CHECK(cur_cover_hash() == 0, "reset -> cover cleared");
|
||||
g_fail_next = 0;
|
||||
|
||||
/* TX: 缓存命中路径——重请求已下载过的 URL(硬盘还有其 song_cov* 条目,无网络)。
|
||||
* 旧码: 缓存命中分支从不更新 g_cover_path → cover_current_path() 恒 NULL → MSG_META
|
||||
* artwork 恒空 → 通知中心媒体卡无封面路径 → 回退显示歌名(真机症状)。
|
||||
* 修复后: 换入时 g_cover_path 指向已落盘的缓存条目 → artwork 非空且可加载出该封面。 */
|
||||
printf("== TX: cache-hit path -> cover_current_path() non-empty (launcher artwork) ==\n");
|
||||
int bx = g_http_calls;
|
||||
cover_request(URL_A, NULL);
|
||||
CHECK(wait_cover_hash(url_hash(URL_A), 3000), "cache-hit: cover A shown (no download)");
|
||||
CHECK(g_http_calls == bx, "cache-hit: 0 http calls (from cache, calls=%d)", g_http_calls - bx);
|
||||
const char *cpx = cover_current_path();
|
||||
CHECK(cpx && cpx[0], "cache-hit: cover_current_path() non-empty (before fix: NULL)");
|
||||
if (cpx && cpx[0]) {
|
||||
meow_img *im = meow_img_load_file(cpx);
|
||||
CHECK(im && im->hash == url_hash(URL_A), "cache-hit: reported path loads cover A");
|
||||
if (im) meow_img_free(im);
|
||||
}
|
||||
cover_ack();
|
||||
|
||||
printf(g_fail ? "== COVER CHAIN SIM: FAIL ==\n" : "== COVER CHAIN SIM: ALL PASS ==\n");
|
||||
return g_fail;
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/* http_truncate_sim.c — 截断守卫宿主测试(0 真实网络)
|
||||
*
|
||||
* 背景(agent-355 挖出):core/ncm_http.c 的 http_run() 在 body 读取循环里,
|
||||
* 对端于 Content-Length 未收满时提前关闭(mbedtls_ssl_read 返回 0 /
|
||||
* MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY),loop 后仅判 overflow,否则一律 NCM_OK,
|
||||
* 于是残缺 body 被调用方当完整成功 → ncm_cache_commit 把半截 .part/.tmp 转正。
|
||||
*
|
||||
* 本 sim mock 整个 mbedtls 传输层(仅符号桩,不链真实 libmbedtls),脚本化喂给
|
||||
* ncm_http.c 一个可分块的对端响应流,验证 http_run(经公开入口
|
||||
* ncm_http_download_status)对以下语义的判定:
|
||||
* 定长(Content-Length) 未收满即关闭 → 截断 → 非 NCM_OK(NCM_ERR_IO)
|
||||
* 定长(Content-Length) 收满 → BS_DONE → NCM_OK
|
||||
* Content-Length: 0(空 body) → BS_DONE → NCM_OK
|
||||
* 无 Content-Length、靠对端关闭结束(BS_CLOSE) → 合法 → NCM_OK
|
||||
* chunked 完整(0 分块末端) → BS_DONE → NCM_OK
|
||||
* chunked 中途关闭 → 截断 → 非 NCM_OK(NCM_ERR_IO)
|
||||
*
|
||||
* 编译(在 ncm_app 下):
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
* -c bench/http_truncate_sim.c -o /tmp/t.o
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
* -ffunction-sections -c core/ncm_http.c -o /tmp/t_http.o
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
* -ffunction-sections -c core/ncm_common.c -o /tmp/t_common.o
|
||||
* gcc -Wl,--gc-sections -o http_truncate_sim /tmp/t.o /tmp/t_http.o /tmp/t_common.o
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ncm_http.h"
|
||||
#include "ncm_common.h"
|
||||
|
||||
#include "mbedtls/net_sockets.h"
|
||||
#include "mbedtls/ssl.h"
|
||||
#include "mbedtls/x509_crt.h"
|
||||
|
||||
/* ============================================================
|
||||
* 传输层桩:脚本化"对端读流"。
|
||||
* 每次 ncm_http_download_status 调用前用 tr_script(...) 装填脚本,
|
||||
* 脚本用尽后 mbedtls_ssl_read 返回 g_close_rc(0=有序关闭;或
|
||||
* MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY 模拟 TLS close_notify)。
|
||||
* ============================================================ */
|
||||
typedef struct { const unsigned char *buf; size_t len; } tr_chunk_t;
|
||||
|
||||
static const tr_chunk_t *g_script;
|
||||
static int g_script_n;
|
||||
static int g_script_i;
|
||||
static int g_close_rc; /* 脚本耗尽后的 ssl_read 返回值 */
|
||||
|
||||
static void tr_script(const tr_chunk_t *chunks, int n, int close_rc) {
|
||||
g_script = chunks; g_script_n = n; g_script_i = 0; g_close_rc = close_rc;
|
||||
}
|
||||
|
||||
int mbedtls_ssl_read(mbedtls_ssl_context *ssl, unsigned char *buf, size_t len) {
|
||||
(void)ssl;
|
||||
if (g_script_i < g_script_n) {
|
||||
const tr_chunk_t *c = &g_script[g_script_i];
|
||||
size_t n = c->len;
|
||||
if (n > len) n = len; /* 脚本块恒 <= 4096(tmp),一般整块发出 */
|
||||
memcpy(buf, c->buf, n);
|
||||
g_script_i++;
|
||||
return (int)n;
|
||||
}
|
||||
return g_close_rc;
|
||||
}
|
||||
int mbedtls_ssl_write(mbedtls_ssl_context *ssl, const unsigned char *buf, size_t len) {
|
||||
(void)ssl; (void)buf; return (int)len; /* 请求头/体全部写成功 */
|
||||
}
|
||||
|
||||
/* 其余 mbedtls 桩:连接/握手/配置全部成功,不触碰任何真实网络。 */
|
||||
void mbedtls_net_init(mbedtls_net_context *ctx) { (void)ctx; }
|
||||
int mbedtls_net_connect(mbedtls_net_context *ctx, const char *host, const char *port, int proto) {
|
||||
(void)ctx; (void)host; (void)port; (void)proto; return 0;
|
||||
}
|
||||
int mbedtls_net_set_nonblock(mbedtls_net_context *ctx) { (void)ctx; return 0; }
|
||||
int mbedtls_net_poll(mbedtls_net_context *ctx, uint32_t rw, uint32_t timeout) {
|
||||
(void)ctx; (void)rw; (void)timeout; return 1; /* 就绪 */
|
||||
}
|
||||
void mbedtls_net_free(mbedtls_net_context *ctx) { (void)ctx; }
|
||||
int mbedtls_net_recv(void *ctx, unsigned char *buf, size_t len) {
|
||||
(void)ctx; (void)buf; (void)len; return MBEDTLS_ERR_SSL_WANT_READ; /* 实际读由 mbedtls_ssl_read 桩接管 */
|
||||
}
|
||||
int mbedtls_net_send(void *ctx, const unsigned char *buf, size_t len) {
|
||||
(void)ctx; (void)buf; return (int)len;
|
||||
}
|
||||
|
||||
void mbedtls_ssl_init(mbedtls_ssl_context *ssl) { (void)ssl; }
|
||||
void mbedtls_ssl_config_init(mbedtls_ssl_config *conf) { (void)conf; }
|
||||
int mbedtls_ssl_config_defaults(mbedtls_ssl_config *conf, int endpoint, int transport, int preset) {
|
||||
(void)conf; (void)endpoint; (void)transport; (void)preset; return 0;
|
||||
}
|
||||
void mbedtls_ssl_conf_authmode(mbedtls_ssl_config *conf, int authmode) { (void)conf; (void)authmode; }
|
||||
void mbedtls_ssl_conf_ca_chain(mbedtls_ssl_config *conf, mbedtls_x509_crt *ca_chain, mbedtls_x509_crl *ca_crl) {
|
||||
(void)conf; (void)ca_chain; (void)ca_crl;
|
||||
}
|
||||
void mbedtls_ssl_conf_rng(mbedtls_ssl_config *conf,
|
||||
int (*f_rng)(void *, unsigned char *, size_t), void *p_rng) {
|
||||
(void)conf; (void)f_rng; (void)p_rng;
|
||||
}
|
||||
int mbedtls_ssl_setup(mbedtls_ssl_context *ssl, const mbedtls_ssl_config *conf) {
|
||||
(void)ssl; (void)conf; return 0;
|
||||
}
|
||||
int mbedtls_ssl_set_hostname(mbedtls_ssl_context *ssl, const char *hostname) {
|
||||
(void)ssl; (void)hostname; return 0;
|
||||
}
|
||||
void mbedtls_ssl_set_bio(mbedtls_ssl_context *ssl, void *p_bio,
|
||||
mbedtls_ssl_send_t *f_send, mbedtls_ssl_recv_t *f_recv,
|
||||
mbedtls_ssl_recv_timeout_t *f_recv_timeout) {
|
||||
(void)ssl; (void)p_bio; (void)f_send; (void)f_recv; (void)f_recv_timeout;
|
||||
}
|
||||
int mbedtls_ssl_handshake(mbedtls_ssl_context *ssl) { (void)ssl; return 0; }
|
||||
void mbedtls_ssl_free(mbedtls_ssl_context *ssl) { (void)ssl; }
|
||||
void mbedtls_ssl_config_free(mbedtls_ssl_config *conf) { (void)conf; }
|
||||
|
||||
void mbedtls_x509_crt_init(mbedtls_x509_crt *crt) { (void)crt; }
|
||||
int mbedtls_x509_crt_parse_file(mbedtls_x509_crt *crt, const char *path) {
|
||||
(void)crt; (void)path; return 0; /* CA 加载成功(validate=0 时不走此径,桩兜底) */
|
||||
}
|
||||
void mbedtls_x509_crt_free(mbedtls_x509_crt *crt) { (void)crt; }
|
||||
|
||||
/* ============================================================
|
||||
* 下载回调 sink:累加接收到的 body 字节。
|
||||
* ============================================================ */
|
||||
typedef struct { unsigned char data[1 << 16]; size_t len; } sink_cb_t;
|
||||
|
||||
static int on_body(void *ctx, const unsigned char *data, size_t len) {
|
||||
sink_cb_t *s = (sink_cb_t *)ctx;
|
||||
if (s->len + len > sizeof s->data) return -1; /* 上限(不触发) */
|
||||
memcpy(s->data + s->len, data, len);
|
||||
s->len += len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 运行一个用例:装填脚本 → 发起 GET 下载 → 记录 rc / body_len / content_length */
|
||||
typedef struct { int rc; size_t body_len; size_t content_length; } case_result_t;
|
||||
|
||||
static case_result_t run_case(const tr_chunk_t *script, int script_n, int close_rc) {
|
||||
ncm_http_req_t req;
|
||||
memset(&req, 0, sizeof req);
|
||||
req.url = "https://ex.test/song.mp3";
|
||||
req.method = "GET";
|
||||
req.verify_ca = 0; /* 跳过 CA 校验,聚焦 body 终态判定 */
|
||||
req.timeout_ms = 3000;
|
||||
|
||||
sink_cb_t sink; memset(&sink, 0, sizeof sink);
|
||||
ncm_http_resp_t resp; memset(&resp, 0, sizeof resp);
|
||||
|
||||
tr_script(script, script_n, close_rc);
|
||||
int rc = ncm_http_download_status(&req, on_body, &sink, 0, &resp, NULL);
|
||||
|
||||
case_result_t r;
|
||||
r.rc = rc;
|
||||
r.body_len = sink.len;
|
||||
r.content_length = resp.content_length;
|
||||
return r;
|
||||
}
|
||||
|
||||
static int g_fail = 0;
|
||||
static void check(const char *name, int ok, const char *detail) {
|
||||
if (!ok) g_fail++;
|
||||
printf(" %s %s\n", ok ? "PASS" : "FAIL", name);
|
||||
if (detail) printf(" %s\n", detail);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
char dbg[160];
|
||||
|
||||
/* ---- 场景 1:定长(CL=100) 对端提前关闭(只发 50 字节)→ 截断 ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
|
||||
static unsigned char chunk[512];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(chunk, hdrs, hlen);
|
||||
for (size_t i = 0; i < 50; i++) chunk[hlen + i] = 'A';
|
||||
tr_chunk_t script[1];
|
||||
script[0].buf = chunk; script[0].len = hlen + 50;
|
||||
case_result_t r = run_case(script, 1, 0 /* 对端关闭 */);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu content_length=%zu",
|
||||
r.rc, r.body_len, r.content_length);
|
||||
check("truncate_fixed(CL=100, 50B, close) -> NCM_ERR_IO",
|
||||
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO) && (r.body_len == 50), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 2:定长(CL=100) 完整收满 → BS_DONE → NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
|
||||
static unsigned char chunk[512];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(chunk, hdrs, hlen);
|
||||
for (size_t i = 0; i < 100; i++) chunk[hlen + i] = 'B';
|
||||
tr_chunk_t script[1];
|
||||
script[0].buf = chunk; script[0].len = hlen + 100;
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("normal_fixed(CL=100, 100B) -> NCM_OK",
|
||||
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 3:定长(CL=100) 两次读到收满(多块)→ NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
|
||||
static unsigned char part1[512], part2[64];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(part1, hdrs, hlen);
|
||||
for (size_t i = 0; i < 60; i++) part1[hlen + i] = 'C';
|
||||
for (size_t i = 0; i < 40; i++) part2[i] = 'D';
|
||||
tr_chunk_t script[2];
|
||||
script[0].buf = part1; script[0].len = hlen + 60;
|
||||
script[1].buf = part2; script[1].len = 40;
|
||||
case_result_t r = run_case(script, 2, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("normal_fixed_multi(CL=100, 60+40B) -> NCM_OK",
|
||||
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 4:Content-Length: 0(空 body)→ BS_DONE → NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char chunk[] =
|
||||
"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n";
|
||||
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("cl_zero(CL=0) -> NCM_OK empty",
|
||||
(r.rc == NCM_OK) && (r.body_len == 0), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 5:无 Content-Length、靠对端关闭结束(BS_CLOSE) → 合法 → NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\n\r\n"; /* 无 CL、无 chunked */
|
||||
static unsigned char chunk[512];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(chunk, hdrs, hlen);
|
||||
for (size_t i = 0; i < 100; i++) chunk[hlen + i] = 'E';
|
||||
tr_chunk_t script[1];
|
||||
script[0].buf = chunk; script[0].len = hlen + 100;
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("close_delimited(no CL, 100B, close) -> NCM_OK",
|
||||
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 6:定长(CL=100) 提前关闭,且用 TLS close_notify 模拟 → 截断 ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
|
||||
static unsigned char chunk[512];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(chunk, hdrs, hlen);
|
||||
for (size_t i = 0; i < 50; i++) chunk[hlen + i] = 'F';
|
||||
tr_chunk_t script[1];
|
||||
script[0].buf = chunk; script[0].len = hlen + 50;
|
||||
case_result_t r = run_case(script, 1, MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("truncate_close_notify(CL=100, 50B) -> NCM_ERR_IO",
|
||||
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO) && (r.body_len == 50), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 7:chunked 完整(0 分块末端)→ BS_DONE → NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char chunk[] =
|
||||
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
|
||||
"A\r\n0123456789\r\n" /* 16 进制 0xA = 10 字节 */
|
||||
"0\r\n\r\n";
|
||||
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("chunked_full(10B + 0-chunk) -> NCM_OK",
|
||||
(r.rc == NCM_OK) && (r.body_len == 10), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 8:chunked 中途关闭(数据未收完)→ 截断 → 非 NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char chunk[] =
|
||||
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
|
||||
"5\r\nhel"; /* 宣称 5 字节,只发了 3,未到 0 分块 */
|
||||
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("chunked_truncated(3/5B, close) -> NCM_ERR_IO",
|
||||
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO), dbg);
|
||||
}
|
||||
|
||||
printf("\n== HTTP TRUNCATE SIM: %s ==\n", g_fail ? "FAIL" : "ALL PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/* bench/mem_release_sim.c — 宿主「彻底释放」路径验证(agent 32bit OOM 专项)。
|
||||
*
|
||||
* 目的:验证「32bit(>16bit)→S32 解码 播完/切歌 是否真正释放」——ncm_dec_open(整文件)→
|
||||
* ncm_dec_read 全量(dr_flac S32 满幅路径)→ ncm_dec_close(dr_flac_close + stream_src_close)
|
||||
* 循环 N 次,断言进程匿名 RSS(RssAnon,OOM 高危口径)**不单调累积**(播完回落基线),
|
||||
* 且解码中峰值有界(32bit 解码缓冲存在但有限,非 40MB 级)。
|
||||
*
|
||||
* 依赖:仅 app/play 解码层(decoder.c + stream_src.c + dr_flac_impl + minimp3_impl),
|
||||
* 无 core/SDK/app.c——本 sim 是解码链路的最小区块,直接量「解码器 buff 播完 free」。
|
||||
* 编译(ncm_app 目录下):
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
|
||||
* -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \
|
||||
* -c bench/mem_release_sim.c -o /tmp/mr.o
|
||||
* gcc -O2 -Wall -Wextra ... -c app/play/stream_src.c -o /tmp/mr_ss.o
|
||||
* gcc -O2 -Wall -Wextra ... -c app/play/decoder.c -o /tmp/mr_dec.o
|
||||
* gcc -O2 -c -Ivendor/minimp3 app/play/minimp3_impl.c -o /tmp/mr_mp3.o
|
||||
* gcc -O2 -c -Ivendor/dr_flac app/play/dr_flac_impl.c -o /tmp/mr_flac.o
|
||||
* gcc -O2 -o mem_release_sim /tmp/mr.o /tmp/mr_ss.o /tmp/mr_dec.o /tmp/mr_mp3.o /tmp/mr_flac.o -lpthread -lm
|
||||
* 运行:./mem_release_sim <flac 24bit 路径> [iters] (iters 默认 5)
|
||||
* 断言(PASS 才 exit 0):
|
||||
* - RssAnon[最后] - RssAnon[基准] < 256KB(播完回落,无累积);
|
||||
* - 解码中峰值有界(peak - 基准 < 8MB,32bit 解码缓冲不是 40MB 级);
|
||||
* - 总帧数 > 0 且 bits==32(确实走了 S32 满幅路径——本 sim 只受 24bit 输入触发)。
|
||||
*/
|
||||
#include "../app/play/decoder.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define READ_CHUNK_FRAMES 4096
|
||||
#define ITER_DEFAULT 5
|
||||
#define SETTLE_TOLERANCE_KB (256) /* 播完回落允许抖动(<256KB 视为无累积) */
|
||||
#define PEAK_BOUND_KB (8 * 1024) /* 解码中峰值必须 <8MB(区别于 40MB 级异常) */
|
||||
|
||||
/* 静默 meow_log(本 sim 不链 core;decoder/stream_src 不调它,仅占位防漏链) */
|
||||
void meow_log(int lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
|
||||
static long rss_anon_kb(void)
|
||||
{
|
||||
FILE *f = fopen("/proc/self/status", "r");
|
||||
if (!f) return -1;
|
||||
char line[256];
|
||||
long v = -1;
|
||||
while (fgets(line, sizeof line, f))
|
||||
if (!strncmp(line, "RssAnon:", 8)) { v = atol(line + 8); break; }
|
||||
fclose(f);
|
||||
return v;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
printf("usage: mem_release_sim <flac_path> [iters]\n");
|
||||
return 2;
|
||||
}
|
||||
const char *path = argv[1];
|
||||
int iters = (argc > 2) ? atoi(argv[2]) : ITER_DEFAULT;
|
||||
if (iters < 2) iters = 2;
|
||||
|
||||
long base = rss_anon_kb();
|
||||
if (base < 0) { printf("FAIL cannot read RssAnon\n"); return 1; }
|
||||
printf("mem_release[0] baseline RssAnon=%ldKB\n", base);
|
||||
|
||||
int first_dec_ok = 0;
|
||||
long prev_after = 0, last_after = 0; /* 最近两次播完 close 后的 RssAnon */
|
||||
for (int i = 0; i < iters; i++) {
|
||||
ncm_decoder_t *dec = NULL;
|
||||
int rate = 0, ch = 0, bits = 0;
|
||||
int64_t tf = 0;
|
||||
if (ncm_dec_open(&dec, path, &rate, &ch, &bits, &tf) != 0) {
|
||||
printf("FAIL iter%d ncm_dec_open (%s)\n", i, path);
|
||||
return 1;
|
||||
}
|
||||
if (i == 0) {
|
||||
printf("mem_release: fmt=%s %dHz/%dch/%dbit total_frames=%lld (S32 path=%d)\n",
|
||||
ncm_dec_format(dec), rate, ch, bits, (long long)tf, bits > 16);
|
||||
first_dec_ok = 1;
|
||||
}
|
||||
|
||||
/* 全量读(S32 满幅路径),测解码中匿名峰值 */
|
||||
static int32_t buf[READ_CHUNK_FRAMES * 2];
|
||||
long peak = 0;
|
||||
int64_t frames = 0, got = 0;
|
||||
do {
|
||||
got = ncm_dec_read(dec, buf, READ_CHUNK_FRAMES);
|
||||
frames += got;
|
||||
long r = rss_anon_kb();
|
||||
if (r > peak) peak = r;
|
||||
} while (got > 0);
|
||||
|
||||
printf("mem_release[%d] after decode: frames=%lld peak_RssAnon=%ldKB\n",
|
||||
i + 1, (long long)frames, peak);
|
||||
|
||||
ncm_dec_close(dec); /* drflac_close + stream_src_close(整文件静态源) */
|
||||
long r = rss_anon_kb();
|
||||
prev_after = last_after;
|
||||
last_after = r;
|
||||
printf("mem_release[%d] after close RssAnon=%ldKB (delta=%ldKB)\n",
|
||||
i + 1, r, r - base);
|
||||
|
||||
/* 解码中峰值有界:32bit 解码缓冲不是 40MB 级 */
|
||||
if (peak - base > PEAK_BOUND_KB) {
|
||||
printf("FAIL iter%d decode peak %ldKB - base %ldKB = %ldKB > %dKB (异常高)\n",
|
||||
i, peak, base, peak - base, PEAK_BOUND_KB);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 累积断言:最后两次播完后的 RssAnon 相对基线的增量都 < 容忍(无单调上升) */
|
||||
long d1 = last_after - base;
|
||||
long d2 = prev_after - base;
|
||||
int no_accum = (d1 < SETTLE_TOLERANCE_KB) && (d2 < SETTLE_TOLERANCE_KB);
|
||||
printf("mem_release: settle delta last=%ldKB prev=%ldKB -> %s\n",
|
||||
d1, d2, no_accum ? "PASS(无累积)" : "FAIL(有残留)");
|
||||
|
||||
int ok = first_dec_ok && no_accum;
|
||||
printf("mem_release: %s\n", ok ? "PASS" : "FAIL");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/* bench/mem_watch_sim.c — 宿主「边播监控取证」阈值/峰值验证(agent 32bit OOM 取证)。
|
||||
*
|
||||
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
|
||||
* A + C 分别归 SDK meow_cachegov / meow_svc_perf;本 sim 只验证 mem_watch 的**取证**语义:
|
||||
* 三档 level 标注滞回(normal→high→critical→滞回→normal)+ song_end 重置峰值 + song_begin 起算。
|
||||
* (旧版「dl_paused/prefetch_paused 降载断言」已随降载上收移除——那由 meow_cachegov_sim
|
||||
* 与 svc_perf test_cachegov_monitor 覆盖。)
|
||||
*
|
||||
* 依赖:仅 app/play/mem_watch.c(+ meow_log 桩)。
|
||||
* 编译(ncm_app 目录下):
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
* -c bench/mem_watch_sim.c -o /tmp/mw.o
|
||||
* gcc -O2 -Wall -Wextra -I. -I../sdk -c app/play/mem_watch.c -o /tmp/mw_mod.o
|
||||
* gcc -O2 -o mem_watch_sim /tmp/mw.o /tmp/mw_mod.o -lpthread -lm
|
||||
* 运行:./mem_watch_sim (exit 0=PASS / 1=FAIL)
|
||||
*/
|
||||
#include "../app/play/mem_watch.h"
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* meow_log 桩:打印(便于看阈值过渡;sim 仅验证 mem_watch 逻辑,非真实日志系统) */
|
||||
void meow_log(int lvl, const char *fmt, ...)
|
||||
{
|
||||
(void)lvl;
|
||||
char b[256]; va_list ap; va_start(ap, fmt);
|
||||
vsnprintf(b, sizeof b, fmt, ap); va_end(ap);
|
||||
printf(" [mem.log] %s\n", b);
|
||||
}
|
||||
|
||||
static int fails;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (!(cond)) { printf("FAIL %s\n", msg); fails++; } \
|
||||
else { printf("ok %s\n", msg); } } while (0)
|
||||
|
||||
/* 等监控线程至少走过 2 个周期(feed 后确保已采样) */
|
||||
static void settle_ms(int m) { usleep((useconds_t)m * 1000u); }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* 用 env 设定短周期(50ms)与干净阈值,加速确定性判定 */
|
||||
setenv("NCM_MEM_WATCH_PERIOD_MS", "50", 1);
|
||||
setenv("NCM_MEM_WATCH_HIGH_KB", "30720", 1); /* 30MB */
|
||||
setenv("NCM_MEM_WATCH_LOW_KB", "20480", 1); /* 20MB */
|
||||
setenv("NCM_MEM_WATCH_CRITICAL_KB", "40960", 1); /* 40MB */
|
||||
|
||||
if (mem_watch_start() != 0) { printf("FAIL mem_watch_start\n"); return 1; }
|
||||
printf("started (period=50ms H=30M L=20M C=40M)\n");
|
||||
|
||||
/* 1) 正常区 */
|
||||
mem_watch_set_forced_kb(10000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "level==NORMAL @10MB");
|
||||
|
||||
/* 2) 升到 HIGH */
|
||||
mem_watch_set_forced_kb(35000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_HIGH, "level==HIGH @35MB");
|
||||
|
||||
/* 3) 升到 CRITICAL */
|
||||
mem_watch_set_forced_kb(45000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_CRITICAL, "level==CRITICAL @45MB");
|
||||
|
||||
/* 4) 滞回:落在 LOW..HIGH 之间 -> 保持 CRITICAL(防抖动) */
|
||||
mem_watch_set_forced_kb(25000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_CRITICAL, "level stays CRITICAL @25MB (hysteresis)");
|
||||
|
||||
/* 5) 回到正常:低于 LOW -> NORMAL */
|
||||
mem_watch_set_forced_kb(15000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "level==NORMAL @15MB");
|
||||
|
||||
/* 6) 调度器释放钩子:song_end 重置峰值 + song_begin 起算 + rss_anon 反映注入值 */
|
||||
mem_watch_set_forced_kb(20000);
|
||||
settle_ms(100);
|
||||
mem_watch_song_begin();
|
||||
mem_watch_set_forced_kb(37000); /* 本曲峰值冲到 37MB */
|
||||
settle_ms(100);
|
||||
long anon = mem_watch_rss_anon_kb();
|
||||
CHECK(anon == 37000, "rss_anon_kb reflects forced value");
|
||||
mem_watch_song_end(); /* 释放钩子:落峰值+重置 */
|
||||
settle_ms(100);
|
||||
mem_watch_song_begin();
|
||||
mem_watch_set_forced_kb(12000); /* 新曲:峰值应从新起算 */
|
||||
settle_ms(100);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "after song_end+begin, level resets to NORMAL");
|
||||
|
||||
mem_watch_stop();
|
||||
printf("mem_watch_sim: %s\n", fails ? "FAIL" : "PASS");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* bench/nav_exit_sim.c — 任务2:home 根页左滑「回桌面后台」宿主验证。
|
||||
*
|
||||
* 场景(0 真实请求/0 真实 launcher):fake launcher IPC socket(MEOW_IPC_SOCK)。调
|
||||
* ipc_media_nav_exit(),断言:
|
||||
* ① fake launcher 收到一条 MSG_NAV 帧,nav_type==MEOW_NAV_EXIT、app_id=="dev.player.ncm";
|
||||
* ② 本函数**不**置 g_app_quit(进程保留,媒体会话继续上报 PLAYING,任务2 ③ 保活前提)。
|
||||
* 这验证「从网易云主页面侧滑返回不再退出进程,而请求 launcher 退后台(有歌在播则 M4 保活)」。
|
||||
*
|
||||
* 编译(宿主,ncm_app 目录下):
|
||||
* gcc -O1 -DJSMN_HEADER -I. -Icore -Iapp -I../sdk -I../meow_launcherd \
|
||||
* -c bench/nav_exit_sim.c -o /tmp/ne.o
|
||||
* gcc -O1 -DJSMN_HEADER -I. -Icore -Iapp -I../sdk -I../meow_launcherd \
|
||||
* -c app/play/ipc_media.c -o /tmp/ne_ipc.o
|
||||
* gcc -O1 -o nav_exit_sim /tmp/ne.o /tmp/ne_ipc.o -lpthread
|
||||
* 运行:./nav_exit_sim (0 网络;PASS/FAIL 退出码)
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h"
|
||||
#include "meow_ipc.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
ncm_app_state_t g_st;
|
||||
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
|
||||
volatile int g_app_quit = 0;
|
||||
volatile int g_net_ready = 1;
|
||||
int g_host_mock = 0;
|
||||
|
||||
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||
{
|
||||
size_t n = 0;
|
||||
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
|
||||
while (src[n]) n++;
|
||||
return n;
|
||||
}
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
|
||||
/* ipc_media.c 的其余依赖(本 sim 不驱动播放/封面/歌词,仅服务链接与消息类型) */
|
||||
uint64_t meow_ui_time_ms(void) { return 0; }
|
||||
int player_manager_playing_song(void) { return g_play.cur_song; }
|
||||
int player_manager_current_song(ncm_song_t *out) { (void)out; return -1; }
|
||||
void player_manager_pause(int on) { (void)on; }
|
||||
void player_manager_next(int dir) { (void)dir; }
|
||||
void player_manager_seek(int64_t ms) { (void)ms; }
|
||||
int player_manager_state(void) { return g_play.state; }
|
||||
const char *cover_current_path(void) { return NULL; }
|
||||
int lyric_count(void) { return 0; }
|
||||
|
||||
/* ---- fake launcher ---- */
|
||||
static const char *g_sock = "/tmp/nav_exit_launcher.sock";
|
||||
static uint16_t g_got_type = 0;
|
||||
static int g_got_nav_type = -1;
|
||||
static char g_got_app[MEOW_APP_ID_MAX];
|
||||
static int g_got = 0;
|
||||
|
||||
static void *server(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
unlink(g_sock);
|
||||
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (lfd < 0) return NULL;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, g_sock, sizeof a.sun_path - 1);
|
||||
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) { close(lfd); return NULL; }
|
||||
listen(lfd, 1);
|
||||
int fd = accept(lfd, NULL, NULL);
|
||||
if (fd < 0) { close(lfd); return NULL; }
|
||||
uint8_t rbuf[MEOW_IPC_MAX];
|
||||
ssize_t n = read(fd, rbuf, sizeof rbuf);
|
||||
if (n >= (ssize_t)MEOW_IPC_HDR_LEN) {
|
||||
uint32_t magic, len;
|
||||
uint16_t ver, type;
|
||||
memcpy(&magic, rbuf, 4); memcpy(&ver, rbuf + 4, 2); memcpy(&type, rbuf + 6, 2);
|
||||
memcpy(&len, rbuf + 12, 4);
|
||||
if (magic == MEOW_IPC_MAGIC && ver == MEOW_IPC_VERSION && len <= (uint32_t)(n - MEOW_IPC_HDR_LEN)) {
|
||||
g_got_type = type;
|
||||
if (type == MSG_NAV && len >= sizeof(m_nav_t)) {
|
||||
m_nav_t nv;
|
||||
memcpy(&nv, rbuf + MEOW_IPC_HDR_LEN, sizeof nv);
|
||||
g_got_nav_type = (int)nv.nav_type;
|
||||
nv.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||||
snprintf(g_got_app, sizeof g_got_app, "%s", nv.app_id);
|
||||
}
|
||||
g_got = 1;
|
||||
}
|
||||
}
|
||||
close(fd); close(lfd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int fail = 0;
|
||||
setenv("MEOW_IPC_SOCK", g_sock, 1);
|
||||
|
||||
pthread_t th;
|
||||
if (pthread_create(&th, NULL, server, NULL) != 0) { printf("FAIL server thread\n"); return 1; }
|
||||
usleep(50000);
|
||||
|
||||
int rc = ipc_media_nav_exit();
|
||||
pthread_join(th, NULL);
|
||||
|
||||
if (rc != 0) { printf("FAIL ipc_media_nav_exit rc=%d\n", rc); fail = 1; }
|
||||
if (!g_got) { printf("FAIL fake launcher received no frame\n"); fail = 1; }
|
||||
else if (g_got_type != MSG_NAV) {
|
||||
printf("FAIL expected MSG_NAV (0x%04x) got 0x%04x\n", MSG_NAV, g_got_type);
|
||||
fail = 1;
|
||||
} else if (g_got_nav_type != MEOW_NAV_EXIT) {
|
||||
printf("FAIL expected MEOW_NAV_EXIT got %d\n", g_got_nav_type);
|
||||
fail = 1;
|
||||
} else if (strcmp(g_got_app, "dev.player.ncm") != 0) {
|
||||
printf("FAIL expected app_id dev.player.ncm got \"%s\"\n", g_got_app);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS nav_exit: type=0x%04x nav_type=EXIT app=%s\n", g_got_type, g_got_app);
|
||||
}
|
||||
if (g_app_quit != 0) { printf("FAIL g_app_quit set (process must stay alive for bg keepalive)\n"); fail = 1; }
|
||||
else printf("PASS nav_exit did NOT set g_app_quit (process/alive, media keeps reporting PLAYING)\n");
|
||||
|
||||
unlink(g_sock);
|
||||
if (fail) { printf("nav_exit_sim FAIL\n"); return 1; }
|
||||
printf("nav_exit_sim PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -151,6 +151,7 @@ int ncm_cache_open(const ncm_cache_t *c, const char *id, const char *ext, ncm_ca
|
||||
*w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK;
|
||||
}
|
||||
const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w) { (void)w; return g_tmp_path; }
|
||||
const char *ncm_cache_writer_final_path(const ncm_cache_writer_t *w) { (void)w; return g_tmp_path; } /* 链接占位:drop_finished 对 .tmp 无害 */
|
||||
int ncm_cache_write(ncm_cache_writer_t *w, const unsigned char *d, size_t l) { (void)w; (void)d; (void)l; return NCM_OK; }
|
||||
int ncm_cache_commit(ncm_cache_writer_t *w) { free(w); g_cache_ok = 1; g_commits++; return NCM_OK; }
|
||||
int ncm_cache_abort(ncm_cache_writer_t *w)
|
||||
@@ -617,6 +618,32 @@ int main(void)
|
||||
g_eof = 1;
|
||||
wait_state(NCM_PC_STOPPED, 3000);
|
||||
|
||||
printf("== P14: 点「正在播的同曲」-> 只聚焦不重播(任务3 re-entry,非 P12 切档)==\n");
|
||||
usleep(300 * 1000);
|
||||
g_eof = 0; g_reads_left = -1; g_url_rc = NCM_OK; g_fee = 0;
|
||||
g_part_size = 0; g_http_status = 200; g_416_first = 0; g_dl_delay_ms = 0; g_dl_fail = 0;
|
||||
g_vip = NCM_VIP_NONE; g_downloads = 0; g_url_calls = 0; g_last_key[0] = 0; g_last_ext[0] = 0;
|
||||
player_manager_stop(); /* 清 P13 可能残留(STOPPED 或已自动顺延) */
|
||||
player_manager_set_quality(NCM_LEVEL_STANDARD, 10);
|
||||
g_cache_ok = 1; /* standard 缓存命中 */
|
||||
player_manager_play(0);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "song0 playing at standard (cache hit)");
|
||||
int opens_before = g_opens;
|
||||
int writes_before = g_writes;
|
||||
/* 任务3:歌单里点「正在播的同曲」(**未** set_quality → pending_replay=0)→ 只聚焦不重播:
|
||||
* 同步返回后状态仍是 PLAYING(若是重播此刻必为 BUFFER),线程窗口内不得新增 audio_out_open。 */
|
||||
player_manager_play(0);
|
||||
CHECK(player_manager_state() == NCM_PC_PLAYING,
|
||||
"re-enter same idx -> state stays PLAYING immediately (no restart to BUFFER)");
|
||||
usleep(300 * 1000); /* 若真发生重播,线程会重跑 run_song 再开 audio */
|
||||
CHECK(g_opens == opens_before,
|
||||
"re-enter same idx -> no new audio_out_open (no restart), opens=%d/%d", g_opens, opens_before);
|
||||
CHECK(g_writes > writes_before,
|
||||
"re-enter same idx -> same stream kept playing (writes %d->%d)", writes_before, g_writes);
|
||||
player_manager_stop();
|
||||
wait_state(NCM_PC_STOPPED, 2000); /* 干净收尾(任务3 复验后置 STOPPED,勿自动顺延) */
|
||||
g_cache_ok = 0; g_cache_real[0] = 0;
|
||||
|
||||
usleep(200 * 1000);
|
||||
player_manager_stop_thread();
|
||||
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,171 @@
|
||||
/* bench/vol_sync_sim.c — 任务1:开播前「系统当前音量 → audiod」下发宿主验证。
|
||||
*
|
||||
* 场景(0 真实请求/0 真实 audiod):fake audiod socket(AF_UNIX SOCK_STREAM)+ 临时 settings.conf
|
||||
* (audio.volume = <V>,读改写单键同源同键)。调 audio_out_sync_volume(),断言 fake audiod 收到
|
||||
* 一条 MSG_AUDIO_VOLUME 帧,且 pct == settings 的 audio.volume。这验证「打开音频会话时把系统
|
||||
* 音量同步下发到 audiod」的根因修复(未播放时 DAC 停在默认/残留音量 → 动音量键才追设定值)。
|
||||
*
|
||||
* 另验证:音量语义只传 pct(0-100)——反转/真静音(255) 由 audiod dac_vol_set 负责,本层不碰。
|
||||
*
|
||||
* 编译(宿主,ncm_app 目录下):
|
||||
* gcc -O1 -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
|
||||
* -c bench/vol_sync_sim.c -o /tmp/vs.o
|
||||
* gcc -O1 -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
|
||||
* -c app/play/audio_out.c -o /tmp/vs_ao.o
|
||||
* gcc -O1 -o vol_sync_sim /tmp/vs.o /tmp/vs_ao.o -lpthread
|
||||
* 运行:./vol_sync_sim (0 网络;PASS/FAIL 退出码)
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h"
|
||||
#include "meow_ipc.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
ncm_app_state_t g_st;
|
||||
ncm_play_run_t g_play;
|
||||
volatile int g_app_quit = 0;
|
||||
volatile int g_net_ready = 1;
|
||||
int g_host_mock = 0;
|
||||
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||
{
|
||||
size_t n = 0;
|
||||
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
|
||||
while (src[n]) n++;
|
||||
return n;
|
||||
}
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
|
||||
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接 */
|
||||
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return -1; }
|
||||
int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; }
|
||||
int meow_audio_pause(int sid, int paused) { (void)sid;(void)paused; return 0; }
|
||||
int meow_audio_close(int sid) { (void)sid; return 0; }
|
||||
int meow_audio_query(meow_audio_state *out) { (void)out; return -1; }
|
||||
|
||||
/* ---- fake audiod ---- */
|
||||
static const char *g_sock = "/tmp/vol_sync_audiod.sock";
|
||||
static uint16_t g_got_type = 0;
|
||||
static int32_t g_got_pct = -1;
|
||||
static int g_got = 0;
|
||||
static volatile int g_nconn = 0; /* 收到多少条连接 */
|
||||
static volatile int g_nframe = 0; /* 收到多少条 VOLUME 帧 */
|
||||
static volatile int g_stop = 0;
|
||||
|
||||
/* fake audiod:接受多条连接(上限 32,超过即停——失败才触发,正常不该超),每条只读一帧
|
||||
* VOLUME 然后等对端 EOF(ncm 侧 close)。统计连接数与帧数,供「重复 sync 不新建连接」断言。 */
|
||||
static void *server(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
unlink(g_sock);
|
||||
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (lfd < 0) return NULL;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, g_sock, sizeof a.sun_path - 1);
|
||||
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) { close(lfd); return NULL; }
|
||||
listen(lfd, 16);
|
||||
int lfl = fcntl(lfd, F_GETFL, 0);
|
||||
if (lfl >= 0) fcntl(lfd, F_SETFL, lfl | O_NONBLOCK); /* 非阻塞:g_stop 能及时退出 */
|
||||
while (!g_stop && g_nconn < 32) {
|
||||
int fd = accept(lfd, NULL, NULL);
|
||||
if (fd < 0) { usleep(20000); continue; } /* EAGAIN:无连接,短睡重试 */
|
||||
g_nconn++;
|
||||
/* 读满一帧(20B)或 EOF:stream socket read 可分段返回,须循环凑满再判。 */
|
||||
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
|
||||
ssize_t n = 0;
|
||||
while (n < (ssize_t)sizeof frame) {
|
||||
ssize_t r = read(fd, frame + n, sizeof frame - (size_t)n);
|
||||
if (r <= 0) break; /* EOF / 错 */
|
||||
n += r;
|
||||
}
|
||||
if (n >= (ssize_t)(MEOW_IPC_HDR_LEN + sizeof(int32_t))) {
|
||||
uint32_t magic, len;
|
||||
uint16_t ver, type;
|
||||
memcpy(&magic, frame, 4); memcpy(&ver, frame + 4, 2); memcpy(&type, frame + 6, 2);
|
||||
memcpy(&len, frame + 12, 4);
|
||||
if (magic == MEOW_IPC_MAGIC && ver == MEOW_IPC_VERSION && type == MSG_AUDIO_VOLUME) {
|
||||
g_got_type = type;
|
||||
memcpy(&g_got_pct, frame + 16, sizeof g_got_pct);
|
||||
g_got = 1;
|
||||
g_nframe++;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
close(lfd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int fail = 0;
|
||||
/* 临时 settings.conf:audio.volume = 24(读改写单键同源键),另带 max 干扰键 */
|
||||
const char *cfg = "/tmp/vol_sync_settings.conf";
|
||||
FILE *f = fopen(cfg, "w");
|
||||
if (!f) { printf("FAIL cannot write settings\n"); return 1; }
|
||||
fprintf(f, "# meow-settings 1\naudio.volume = 24\naudio.volume_max = 50\ndisplay.brightness = 80\n");
|
||||
fclose(f);
|
||||
setenv("MEOW_SETTINGS_FILE", cfg, 1);
|
||||
setenv("MEOW_AUDIO_SOCK", g_sock, 1);
|
||||
|
||||
pthread_t th;
|
||||
if (pthread_create(&th, NULL, server, NULL) != 0) { printf("FAIL server thread\n"); return 1; }
|
||||
usleep(50000); /* 等 fake audiod listen */
|
||||
|
||||
/* 第一次 sync(首次 open):应下发一帧 VOLUME,pct==settings */
|
||||
audio_out_sync_volume();
|
||||
usleep(50000); /* 等服务线程 accept+读帧(主/服务线程异步) */
|
||||
if (!g_got) { printf("FAIL fake audiod received no frame\n"); fail = 1; }
|
||||
else if (g_got_type != MSG_AUDIO_VOLUME) {
|
||||
printf("FAIL expected MSG_AUDIO_VOLUME (0x%04x) got 0x%04x\n", MSG_AUDIO_VOLUME, g_got_type);
|
||||
fail = 1;
|
||||
} else if (g_got_pct != 24) {
|
||||
printf("FAIL expected pct=24 (settings audio.volume) got %d\n", (int)g_got_pct);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS audiod sync volume: type=0x%04x pct=%d == settings.audio.volume\n",
|
||||
g_got_type, (int)g_got_pct);
|
||||
}
|
||||
|
||||
/* 连接风暴回归(§2026-09-02 fd=N accept 根因):音量未变时反复 sync 不得新建连接。
|
||||
* 模拟边下边播/切歌期频繁 audio_out_open → audio_out_sync_volume(同值,不重发)。 */
|
||||
for (int i = 0; i < 50; i++) audio_out_sync_volume(); /* 同值 50 次:应零新增连接 */
|
||||
usleep(50000);
|
||||
if (g_nconn != 1) {
|
||||
printf("FAIL unchanged-volume re-sync created %d connections (expect 1)\n", (int)g_nconn);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS unchanged-volume re-sync: 50 calls -> 1 connection (storm eliminated)\n");
|
||||
}
|
||||
|
||||
/* 值变才重发:settings 改成 50 → 下一次 sync 应新建第 2 条连接 */
|
||||
FILE *g = fopen(cfg, "w");
|
||||
if (!g) { printf("FAIL cannot rewrite settings\n"); fail = 1; }
|
||||
else {
|
||||
fprintf(g, "# meow-settings 1\naudio.volume = 50\naudio.volume_max = 50\ndisplay.brightness = 80\n");
|
||||
fclose(g);
|
||||
}
|
||||
audio_out_sync_volume();
|
||||
usleep(50000);
|
||||
if (g_nconn != 2 || g_got_pct != 50) {
|
||||
printf("FAIL changed-volume re-sync: conns=%d (expect 2) last_pct=%d (expect 50)\n",
|
||||
(int)g_nconn, (int)g_got_pct);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n");
|
||||
}
|
||||
|
||||
g_stop = 1;
|
||||
pthread_join(th, NULL);
|
||||
unlink(g_sock);
|
||||
unlink(cfg);
|
||||
if (fail) { printf("vol_sync_sim FAIL\n"); return 1; }
|
||||
printf("vol_sync_sim PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -21,7 +21,8 @@ APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
|
||||
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
|
||||
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
|
||||
app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \
|
||||
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c"
|
||||
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c \
|
||||
app/play/mem_watch.c"
|
||||
|
||||
echo "==> app+core (strict)"
|
||||
for f in $APP $CORE; do
|
||||
|
||||
@@ -20,7 +20,7 @@ echo "==> host SDK objects"
|
||||
SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \
|
||||
meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \
|
||||
meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin \
|
||||
meow_ui_audio meow_ui_anim meow_ui_box meow_ui_scroll meow_ui_str"
|
||||
meow_ui_audio meow_ui_anim meow_ui_box meow_ui_scroll meow_ui_str meow_cachegov"
|
||||
for f in $SDK_SRC; do
|
||||
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \
|
||||
"$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; }
|
||||
|
||||
@@ -49,9 +49,21 @@ gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
-Icore -Ivendor/jsmn -Ivendor/mbedtls/include -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include -c app/play/player_manager.c -o /tmp/pf_pm.o
|
||||
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
-Icore -c app/play/stream_src.c -o /tmp/pf_ss.o
|
||||
gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o /tmp/pf_ss.o -lpthread -lm
|
||||
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
-Icore -c app/play/mem_watch.c -o /tmp/pf_mw.o # player_manager 引用(边播边监控取证,agent 32bit OOM)
|
||||
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore \
|
||||
-c ../sdk/meow_cachegov.c -o /tmp/pf_cg.o # player_manager 引 meow_cachegov_*(page-cache 治理 A)
|
||||
gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o /tmp/pf_ss.o /tmp/pf_mw.o /tmp/pf_cg.o -lpthread -lm
|
||||
run_sim player_flow_sim
|
||||
|
||||
echo "==> build mem_watch_sim(边播边监控阈值/滞回/调度器,宿主桩驱动;agent 32bit OOM。"
|
||||
echo " mem_release_sim 无累积断言在 bench/ 头注释手动构建:需预生成 24bit flac 素材)"
|
||||
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
-c bench/mem_watch_sim.c -o /tmp/mw.o
|
||||
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -I../sdk -c app/play/mem_watch.c -o /tmp/mw_mod.o
|
||||
gcc -O2 -o mem_watch_sim /tmp/mw.o /tmp/mw_mod.o -lpthread -lm
|
||||
run_sim mem_watch_sim
|
||||
|
||||
echo "==> build window_sim"
|
||||
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn -Ivendor/mbedtls/include \
|
||||
-c bench/window_sim.c -o /tmp/ws.o
|
||||
@@ -113,4 +125,14 @@ gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Iapp -Icore -I../sdk -I../meow_launcher
|
||||
gcc -O2 -o account_cookie_sim /tmp/acs.o /tmp/acs_account.o -lpthread -lm
|
||||
run_sim account_cookie_sim
|
||||
|
||||
echo "==> build http_truncate_sim (ncm_http 截断守卫宿主测试;mock mbedtls 传输层,0 真实网络)"
|
||||
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
-c bench/http_truncate_sim.c -o /tmp/t.o
|
||||
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
-ffunction-sections -c core/ncm_http.c -o /tmp/t_http.o
|
||||
gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
-ffunction-sections -c core/ncm_common.c -o /tmp/t_common.o
|
||||
gcc -Wl,--gc-sections -o http_truncate_sim /tmp/t.o /tmp/t_http.o /tmp/t_common.o
|
||||
run_sim http_truncate_sim
|
||||
|
||||
echo "===== all sims done ====="
|
||||
Executable
BIN
Binary file not shown.
@@ -196,9 +196,54 @@ static int obj_bool(const ncm_json_t *j, int tok, const char *key) {
|
||||
return (strcmp(s, "true") == 0);
|
||||
}
|
||||
|
||||
/* UTF-8 整序列安全追加:把 src 按「多字节字符整序列」边界拷入 dst[*pos..],放不下整字符则
|
||||
* 停止(不切半个字)。sz 为 dst 总大小(含 NUL 位)。用于 art_name 多歌手拼接的字节截断。 */
|
||||
static void append_artist_name(char *dst, size_t *pos, size_t sz, const char *src)
|
||||
{
|
||||
size_t cap = sz - 1; /* 最多写 sz-1 字节,留 NUL */
|
||||
size_t i = 0;
|
||||
while (src[i] && *pos < cap) {
|
||||
unsigned char c = (unsigned char)src[i];
|
||||
size_t k = 1;
|
||||
if (c < 0x80) k = 1;
|
||||
else if ((c & 0xE0) == 0xC0) k = 2;
|
||||
else if ((c & 0xF0) == 0xE0) k = 3;
|
||||
else if ((c & 0xF8) == 0xF0) k = 4;
|
||||
else { dst[(*pos)++] = src[i++]; continue; } /* 非法起始字节:单字节原样收 */
|
||||
if (*pos + k > cap) break; /* 整字符放不下:丢弃(保住 UTF-8 完整性) */
|
||||
memcpy(dst + *pos, src + i, k);
|
||||
*pos += k; i += k;
|
||||
}
|
||||
dst[*pos] = 0;
|
||||
}
|
||||
|
||||
/* 歌曲 ar[](歌手数组)多歌手名用 '/' 拼接进 art_name(≤ out_sz,UTF-8 整序列截断)。
|
||||
* 单歌手行为不变(无 '/');空歌手条目跳过(避免连写分隔符「//」)。 */
|
||||
static void fill_artist(const ncm_json_t *j, int e, char *out, size_t out_sz)
|
||||
{
|
||||
out[0] = 0;
|
||||
int ar = ncm_json_obj_get(j, e, "ar");
|
||||
if (ar < 0 || ncm_json_type(j, ar) != JSMN_ARRAY) return;
|
||||
int nart = ncm_json_arr_len(j, ar);
|
||||
size_t pos = 0;
|
||||
for (int k = 0; k < nart; k++) {
|
||||
int a = ncm_json_arr_get(j, ar, k);
|
||||
if (a < 0) break;
|
||||
char nm[128]; /* 单歌手名缓冲(obj_str 已截断到 127 字节) */
|
||||
obj_str(j, a, "name", nm, sizeof nm);
|
||||
if (!nm[0]) continue; /* 空条目:跳过,不加分隔符 */
|
||||
if (pos > 0) { /* 第 2+ 歌手:先补 '/'(需 1 字节 + NUL) */
|
||||
if (pos >= out_sz - 1) break; /* 无空间放分隔符 */
|
||||
out[pos++] = '/';
|
||||
out[pos] = 0;
|
||||
}
|
||||
append_artist_name(out, &pos, out_sz, nm);
|
||||
}
|
||||
}
|
||||
|
||||
/* 解析一处「歌曲对象数组」(song_detail / cloudsearch / track_all 离线解析 共用)。
|
||||
* 元素字段:id/name/ar[0].name/al.name/al.picUrl/dt/fee。id 可为数字或字符串(均以文本拷贝,
|
||||
* 兼容 >2^31 的歌曲 id)。返回 NCM_OK,*out_n=写入数。 */
|
||||
* 元素字段:id/name/ar[].name(多歌手 '/' 拼接) /al.name/al.picUrl/dt/fee。id 可为数字或
|
||||
* 字符串(均以文本拷贝,兼容 >2^31 的歌曲 id)。返回 NCM_OK,*out_n=写入数。 */
|
||||
static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int max, int *out_n)
|
||||
{
|
||||
*out_n = 0;
|
||||
@@ -213,11 +258,7 @@ static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int
|
||||
obj_str(j, e, "name", out[i].name, sizeof out[i].name);
|
||||
out[i].duration_ms = obj_int(j, e, "dt");
|
||||
out[i].fee = (int)obj_int(j, e, "fee");
|
||||
int ar = ncm_json_obj_get(j, e, "ar");
|
||||
if (ar >= 0 && ncm_json_type(j, ar) == JSMN_ARRAY) {
|
||||
int a0 = ncm_json_arr_get(j, ar, 0);
|
||||
if (a0 >= 0) obj_str(j, a0, "name", out[i].art_name, sizeof out[i].art_name);
|
||||
}
|
||||
fill_artist(j, e, out[i].art_name, sizeof out[i].art_name);
|
||||
int al = ncm_json_obj_get(j, e, "al");
|
||||
if (al >= 0) {
|
||||
obj_str(j, al, "name", out[i].album_name, sizeof out[i].album_name);
|
||||
@@ -371,13 +412,15 @@ static int level_from_str(const char *s) {
|
||||
}
|
||||
|
||||
/* 账户权限允许的最高档(§20.3,与 app/logic/account.c account_level_cap 同口径):
|
||||
* NONE→EXHIGH / CVIP→HIRES / SVIP→SURROUND。core 侧权威实现,供降级链自钳顶。 */
|
||||
* NONE→EXHIGH / CVIP→HIRES / SVIP|SUPER→SURROUND。core 侧权威实现,供降级链自钳顶。
|
||||
* SUPER(超级会员)为最高档,与 SVIP 同享全景声顶档。 */
|
||||
static int level_cap_for_vip(int vip)
|
||||
{
|
||||
switch (vip) {
|
||||
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
|
||||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||||
default: return NCM_LEVEL_EXHIGH;
|
||||
case NCM_VIP_SVIP:
|
||||
case NCM_VIP_SUPER: return NCM_LEVEL_SURROUND;
|
||||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||||
default: return NCM_LEVEL_EXHIGH;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,13 +40,19 @@ enum {
|
||||
const char *ncm_api_level_str(int level); /* API level 串(SKY/SURROUND 同为 "sky") */
|
||||
const char *ncm_api_level_key(int level); /* 缓存键唯一后缀(SKY="sky"/SURROUND="sky_c51") */
|
||||
|
||||
/* 会员等级(vip/info 判定,BIZ_NOTES §1):SVIP(黑胶)权限 ⊇ CVIP(畅听/音乐包)。 */
|
||||
/* 会员等级(vip/info 判定,BIZ_NOTES §1):用户语义 0=非会员 / 1=畅听会员(VIP) /
|
||||
* 2=黑胶会员(CVIP) / 3=超级会员(SVIP)。枚举名保留旧名(CVIP/SVIP)避免大改,新增
|
||||
* NCM_VIP_SUPER=3 为最高档;权限 SUPER ⊇ SVIP(黑胶) ⊇ CVIP(畅听/音乐包)。
|
||||
* 注:ncm_api_parse_vip_info 仍只产出 NONE/CVIP/SVIP(关联音乐包=CVIP、黑胶=SVIP,
|
||||
* 无独立“超级”字段);3 档目前经 account.json 持久化进入/显示(account_load 钳位已放行)。 */
|
||||
enum {
|
||||
NCM_VIP_NONE = 0,
|
||||
NCM_VIP_CVIP = 1,
|
||||
NCM_VIP_SVIP = 2,
|
||||
NCM_VIP_NONE = 0,
|
||||
NCM_VIP_CVIP = 1, /* 畅听会员/音乐包 */
|
||||
NCM_VIP_SVIP = 2, /* 黑胶会员 */
|
||||
NCM_VIP_SUPER = 3, /* 超级会员(最高档) */
|
||||
};
|
||||
/* 档位所需会员门槛:standard/higher/exhigh=NONE;lossless/hires=CVIP;其余=SVIP。 */
|
||||
/* 档位所需会员门槛:standard/higher/exhigh=NONE;lossless/hires=CVIP;其余=SVIP。
|
||||
* 门槛最高只到 SVIP(2)(超级会员=3 为 ⊇ 黑胶,天然通过所有门槛,无需单独门禁)。 */
|
||||
int ncm_api_level_gate(int level);
|
||||
|
||||
typedef struct ncm_playlist {
|
||||
@@ -65,7 +71,7 @@ typedef struct ncm_playlist {
|
||||
typedef struct ncm_song {
|
||||
char id[24];
|
||||
char name[128];
|
||||
char art_name[96]; /* 首个歌手名 */
|
||||
char art_name[96]; /* 歌手名:多歌手用 '/' 拼接(UTF-8 整序列截断) */
|
||||
char album_name[96];
|
||||
char al_pic[256]; /* 专辑封面 picUrl(song_detail/cloudsearch 解析;播放页黑胶封面) */
|
||||
int32_t duration_ms;
|
||||
|
||||
@@ -232,6 +232,11 @@ const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w) {
|
||||
return w ? w->tmp_path : "";
|
||||
}
|
||||
|
||||
/* 写者 final(正式)路径(commit 后 rename 目标;曲终 drop_finished L0 DONTNEED 用)。 */
|
||||
const char *ncm_cache_writer_final_path(const ncm_cache_writer_t *w) {
|
||||
return w ? w->final_path : "";
|
||||
}
|
||||
|
||||
/* 半成品判定(阶段3,§5.1):.tmp(下载中)/ .part(中断残体)不属正式缓存条目,
|
||||
* 不进 LRU 扫描/计数——否则正被下载播放的文件会被缓存上限误删(写者管理域)。 */
|
||||
static int is_partial_name(const char *name)
|
||||
|
||||
@@ -61,6 +61,8 @@ int ncm_cache_commit(ncm_cache_writer_t *w); /* win; 返回 0=成功 */
|
||||
int ncm_cache_abort(ncm_cache_writer_t *w);
|
||||
/* 写者 .tmp 路径(只读):边下边播解码侧用它开只读 fd 建 stream_src。 */
|
||||
const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w);
|
||||
/* 写者 final(正式)路径(只读):commit 后目标文件路径,用于曲终 drop_finished(L0 DONTNEED)。 */
|
||||
const char *ncm_cache_writer_final_path(const ncm_cache_writer_t *w);
|
||||
|
||||
/* ---- 阶段3:.part 断点续传 + 在播文件防淘汰(NCM_STREAM_PLAY §5) ----
|
||||
* 命名约定:中断的 .tmp 保留为 final+".part"(残体),供下次播放「Range 续传」;
|
||||
|
||||
@@ -491,6 +491,13 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
|
||||
}
|
||||
}
|
||||
if (sink->overflow) { ret = NCM_ERR_FULL; goto done; }
|
||||
/* 截断守卫:body 未到达合法终态却已跳出读取循环(对端提前关闭 / PEER_CLOSE_NOTIFY)。
|
||||
* 合法终态只有两种:BS_DONE(定长收满 / chunked 到 0 分块 / Content-Length:0,body_st_init 即置)
|
||||
* 与 BS_CLOSE(无 Content-Length、依 RFC 靠对端关闭定界——关闭即合法结束)。
|
||||
* body_st_init 里 BS_CLOSE 仅在 content_length==SIZE_MAX 且非 chunked 时进入;若 Content-Length
|
||||
* 存在而未收满(此时必为 BS_LEN/BS_CHUNK_*),或 chunked 未到 0 分块就关闭,即为截断:
|
||||
* 此时返回错误,调用方(song_src_dispose)走 abort_part 保留 .part,不得 commit 残缺文件。 */
|
||||
if (bst.st != BS_DONE && bst.st != BS_CLOSE) { ret = NCM_ERR_IO; goto done; }
|
||||
ret = NCM_OK;
|
||||
done:
|
||||
mbedtls_net_free(&net);
|
||||
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Reference in new issue
Block a user