[agent-239] 仅改 patch_v2/ncm_app/。 1. 歌单分页(REPORT §14.1): track/all 全 404(§13.1) 弃用, 回 detail(n=1)全量 trackIds + song_detail 分批(≤NCM_SONG_BATCH=100) 触底逐页加载。appstate_load_playlist_songs_page offset=0 重建 song_ids[]/track_ids_count 载第0页, offset>0 追加(失败保留已载); playlist.c g_pl_more=(song_count<track_ids_count && <NCM_SONG_MAX), 触底 g_ploffset=song_count 起下一页(保留滚动), 首屏失败整页空态+点按重试, 追加失败 snack+保留已载。NCM_SONG_MAX 256→1000(单页渲染上限, bss +~1MB), NCM_SONG_BATCH 64→100, 删 NCM_PLAYLIST_PAGE(track/all 遗留)。 2. 专辑封面(§14.2): cover.c 封面改走 ncm_cache(LRU, 键=djb2(url)→song_cov<h>.jpg 同池), 缓存命中免下载/未命中下载→open/write/commit 落盘, 仍写 cover_current.jpg 供 launcher; 顺带修真 bug: save_cover_raw 在已持 g_cm 临界区内再 lock(非递归死锁)→封面永不刷新, 改调用方持锁。 歌单列表/详情头块封面保持现状。 3. 歌词后台加载(§14.3): lyric_worker 已异步(§11)未改线程模型; 补 lyric.c g_lyric_mu 数据竞争锁, 写侧 lyric_parse 整段持锁(清空用 lyric_clear_nolock 免二次取锁), 读侧 lyric_count/line_at/ line_text/line_tl 逐次取锁, 消除 UI/IPC 与 worker 间数据竞争(原 §10.9 FM4 良性竞态)。 验证(0 真实 NCM 请求): bench/pagination_sim.c 离线 stub 驱动真实 appstate 分页函数全 PASS (130首读全程/5191首封顶1000/空/越界无操作); mipsel build_ncm.sh 0 warning(text 842820/bss 2052068); 宿主 build_ncm_host.sh+MEOW_UI_DUMP home/playlist/player/lands 渲染正常无崩溃; host SDK_SRC 补 meow_pinyin(拼音IME py_predict 漏编致链接断)。ncm md5 b34bc1ed, ncm_host md5 1a2327e0。 未动 sdk/(主会话改 meow_ui_ime.c)/meow_launcherd/meow_amsd/vendor/core; 未 commit 它们。
88 KiB
os.meow.ncm 技术栈预研 + 构建验证报告
日期:2026-08-31。工作区:
/home/Aska/Project/R3ProII,仅改动patch_v2/ncm_app/。 目标:为 MeowOS 网易云 App(v1 = 扫码登录 + 我的歌单 + 播放)在 HiBy R3 Pro II (MIPS32r2 单核、~14MB 可用内存、无 GPU、ALSA 直开 hw:0,0)上的纯 C 静态栈做选型与真机交叉编译验证。 工具链:.tools/zig-mipsel-cc(zig 包装 mipsel-linux-musleabi soft-float 静态交叉)。全程未 commit/push/刷机/动设备。
1. 选型结论表
| 能力 | 选型 | 版本 | 许可 | mipsel 体积(text) | 说明 |
|---|---|---|---|---|---|
| TLS + 加密 | mbedtls(裁剪) | 3.6.2 | Apache-2.0 | 链接 Demo ≈ 388 KB | TLS1.2 client + AES-128-CBC/ECB/GCM + MD5 + RSA(1024 raw) + Base64 + X509,单库全包 |
| TLS(备选) | BearSSL | master@7bea48e | MIT | TLS client 子集 ≈ 96 KB | 更省 flash,但无 base64、AES-ECB 需自行逐块、RSA raw 用 br_rsa_i31_public |
| JSON | jsmn | 1.1.0 | MIT | 2.2 KB | 分词器,无 malloc/无 DOM;cJSON 对比 20 KB |
| 二维码 | qrcodegen | nayuki@993ccbb | MIT | 10.8 KB | 纯 C 单文件,无图形依赖 |
| MP3 解码 | minimp3 | master@ea99364 | CC0 | 37.4 KB | minimp3_ex 流式/可 seek;dr_mp3 对比 40.7 KB |
HTTPS GET/POST 可达性:mbedtls 已实测连通(见 §2)。crypto 是否 mbedtls 自带就够:是——
AES-128-ECB/CBC、MD5、Base64、RSA(raw) 全有;RSA raw 用 mbedtls_rsa_public()(无 padding 纯模幂)已交叉校验与 pow(m,e,n) 完全一致(见 §3),这是文档标注最易踩坑处,已验证可行。
2. HTTPS 实测(宿主 = 本机 aarch64;另跑通 MIPS 版)
demo:demo/https_unikey.c(mbedtls TLS1.2 + eapi 加密 + jsmn)。
目标:POST https://interface.music.163.com/eapi/login/qrcode/unikey,body params=<eapi hex>。
deviceId=68DFA27096B7A5DC2CB393C77D8C194FDC0459B743081D091EBD
---- response body ----
{"code":200,"unikey":"33a1c6a6-28ad-4e1e-bc8e-ebe769953913"}
UNIKEY = 33a1c6a6-28ad-4e1e-bc8e-ebe769953913
- 宿主:
code:200+ unikey 顶层字段,与 NCM_API_NOTES §7 一致 ✅ - MIPS 版交叉编译后,在
qemu-mipsel下全链路跑通(DNS→TCP→TLS→POST→JSON 解析),同样拿到真实 unikey:{"code":200,"unikey":"6731b1ac-71a2-41eb-a704-a2de39d6d0b7"}。即 mipsel 静态产物的网络+crypto+TLS 路径正确。 - eapi 加密逻辑与原版
crypto::eapi一致:digest=md5("nobody"+url+"use"+json+"md5forencrypt"),plain=url+"-36cd479b6b5-"+json+"-36cd479b6b5-"+digest,params=HEX_UPPER(AES128ECB(plain,"e82ckenh8dichen8"));header(osver/deviceId/os/appver/versioncode/mobilename/buildver/resolution/__csrf/channel/requestId)与e_r:false均入加密体(§8 要求,去掉会 4xx)。
3. RSA raw(weapi 无 padding 模幂)校验
mbedtls_rsa_public(ctx, 128B输入右对齐16B明文, 128B输出),n=1024-bit、e=65537。
- mipsel(qemu) 与宿主输出一致,且与 Python
pow(m,e,n)逐一字节相同(256 hex 完全匹配):CT = 55B33E7E3229B8D977137D120B3C7A5B5FB8E933325CAA5BB8CEF6C132D57E5E...B2B83 - 结论:mbedtls_rsa_public() 可用作 weapi 的 textbook RSA,避开 PKCS#1 padding 陷阱。demo:
demo/rsa_raw_test.c。
4. 总体积账(mipsel,-Os,flash≈text)
选定的库合计(≈438 KB):
| 项 | text | 备注 |
|---|---|---|
| mbedtls TLS+crypto+x509(链接 demo) | 388 KB | 含 ~6 KB musl libc 基线;库文本合计 410 KB,gc-sections 未再降 |
| jsmn | 2.2 KB | |
| qrcodegen | 10.8 KB | |
| minimp3 | 37.4 KB | |
| 库合计 | ≈ 438 KB | 另需 App/UI/封面/音频输出层(未计入,自研) |
可选裁剪方向(能明显压 mbedtls):限制密码套件到 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(+1 个 CBC+SHA 兜底)、
ECP 只留 P-256、去掉 ECDSA(服务器给 RSA 证书)、MBEDTLS_SSL_IN/OUT_CONTENT_LEN=4096。估可降到 ~250 KB。
若固件预算仍紧,切 BearSSL(≈96KB) + 自带 base64/AES-ECB 例程,能再省 ~300 KB。
对比参考项体积:cJSON 20 KB、dr_mp3 40.7 KB、BearSSL 全库 310 KB(裁剪子集 96 KB)。空静态 mipsel 基线 text=5888。
5. 内存 / 性能估算(14MB 可用)
-
TLS 握手 buffer:mbedtls 默认
SSL_IN/OUT_CONTENT_LEN=16KB 各一,共 ~32KB;demo 二进制bss=26888(≈26KB)+堆内 SSL context/ECDH 临时密钥。对 14MB 无压力;建议调成 4096 更省。 -
JSON 解析:jsmn 无 malloc,token 数组按需分配(响应小,几百字节)。不要全量缓冲歌单详情,只取需要的键(文档 §4.4)。
-
解码:minimp3 流式,输入 ~每帧几 KB + 输出
MINIMP3_MAX_SAMPLES_PER_FRAME*2*ch,整体 <20KB。 -
封面:必须限幅——NCM 建议 5MB,MeowOS 应限 ~256KB 并降采样/调色板(无 GPU 只能 CPU 软缩放)。
-
二维码位图:41×41 符号渲染成 UI 尺寸(如 400×400,灰度 1B/px=160KB,1-bit 可 20KB)。
-
预算结论:TLS+解码+二维码+封面(256KB cap)+UI 帧缓冲,总工作集应在数百 KB 量级,14MB 富余;唯一要盯的是封面/歌单响应别全量驻留。
-
性能:
- MP3 解码实时性:宿主(aarch64) minimp3 = 568×RT、dr_mp3 = 627×RT;
qemu-mipsel(TCG 模拟,远慢于真机,通常 10–50×) 下 minimp3 = 4.8×RT、dr_mp3 = 4.7×RT。 → 真机 MIPS32 单核跑 44.1k/64kbps 解码余量很大(估计数十倍 RT),解码不是瓶颈。⚠️ 该数字为宿主/模拟估算,标注不准。 - TLS 握手:ECDHE-RSA-AES128-GCM,单核软 RSA 1024 校验 + ECDH P-256,估几十~几百 ms,可接受(仅首次/重连)。
- weapi RSA raw 模幂:1024-bit 软模幂每次约几 ms~几十 ms(只对 weapi 请求,频率低)。
- MP3 解码实时性:宿主(aarch64) minimp3 = 568×RT、dr_mp3 = 627×RT;
6. 风险与 fallback
| 风险 | 评估 | 对策/fallback |
|---|---|---|
| weapi RSA raw 最易踩坑 | ✅ 已交叉校验正确 | 保持 mbedtls_rsa_public()+左补零到 256 hex;用 rsa_raw_test 做回归 |
| deviceId/UA 不稳定触发风控 | 高 | deviceId(52 hex) 首生成后持久化;UA 按 eapi(iPhone)/weapi(Chrome) 分别固定;轮询≥1s、失败退避 |
| session(MUSIC_U)丢失即需重扫码 | 高 | cookie 持久化;启动用 /weapi/w/nuser/account/get 校验 |
eapi 的 header/e_r 缺失 → 4xx |
中 | demo 已带,必须保留(§8) |
| 无版权/VIP 曲目 url=null | 中 | song_url_v1 返回 null → 提示 + 降级到 song/enhance/player/url(br=320000) 兜底 |
| 音质策略(Hi-res 降级) | v1 只保 mp3 | 账号非免费档 level 被钳到免费档;v1 默认 standard(128k)/higher(192k);不引入 flac 解码(14MB 内存+单核不划算);后续再评估 dr_flac |
| 触发高频风控 | 中 | 请求限频、重试退避、必要时随机国内 IP(X-Real-IP/X-Forwarded-For) |
| mbedtls flash ~388KB 偏大 | 中 | 见 §4 裁剪;若预算吃紧切 BearSSL(≈96KB) |
7. 出入清单(只改 patch_v2/ncm_app/)
patch_v2/ncm_app/
├── vendor/ # 各库源码(上游 clone/tar + 许可证)
│ ├── mbedtls/ (3.6.2, 含 framework 子模块, 裁剪 config 在 config/mbedtls_config_ncm.h)
│ ├── bearssl/ (官方 git, 无语义版本)
│ ├── jsmn/ cjson/ qrcodegen/ minimp3/ dr_mp3/
├── libs_common.sh # 公共 zig/ar/size 环境
├── build_mbedtls.sh # 裁剪 config + 静态编 mipsel → out/mbedtls/mipsel/*.a
├── build_bearssl.sh # make lib → out/bearssl/mipsel/bm/libbearssl.a
├── build_cjson.sh build_jsmn.sh build_qrcodegen.sh build_minimp3.sh build_dr_mp3.sh
├── build_demo.sh # 宿主 + mipsel 的 HTTPS demo 复现
├── bench/ # minimp3/dr_mp3 基准(sample-15s.mp3 来自 samplelib)
├── demo/https_unikey.c # eapi + TLS1.2 + jsmn(打 unikey)
├── demo/rsa_raw_test.c # weapi RSA raw 校验
├── out/https_unikey_mipsel(. _s) # mipsel 静态 demo(qemu-mipsel 跑通)
├── out/mbedtls/{mipsel,host}/ # 库产物 out/bearssl/mipsel/bm/
├── VENDOR_NOTES.md # 版本/许可/裁剪/体积
└── REPORT.md # 本报告
未做/待主会话:真机 hw:0,0 播放链路、扫码后真实登录(803+set-cookie)、weapi 真实登录态请求、
CA bundle 内置方案、音质档位映射实测。这些属设备/账号侧联调,由主会话在设备上执行(本预研不动设备)。
8. 结论
- 可行:纯 C + mbedtls(裁剪) + jsmn + qrcodegen + minimp3,静态 mipsel 交叉编译全通,HTTPS POST 拿到真实 unikey,MIPS(qemu) 版全链路跑通。
- 推荐 TLS/加密一体用 mbedtls(单库一次解决 TLS + AES/MD5/RSA raw/Base64 + X509,规避 RNSS-raw 手写风险);BearSSL 为省 flash 备选。
- v1 体积增量 ≈ 438 KB(库)/ 只做 mp3,内存估计数百 KB 工作集,14MB 可行;主要优化点是 mbedtls 裁剪与封面限幅。
- 最关键风险(RSA raw)已验证;其余风险集中在风控(deviceId/UA 稳定持久化)与无版权降级策略。
9. 核心库批次(core/,纯 C 静态库,无 UI)
日期:2026-08-31。在 §1-8 预研(候选库/工具链已定型)基础上落地核心库:http/crypto/login/api/cache 五模块
- 公共/存储/JSON/请求构建支撑模块 + ncm_ctx 上下文。仅改动
patch_v2/ncm_app/。未 commit/push/刷机/动设备。
9.1 模块与公开 API(头文件签名)
| 模块 | 文件 | 主要公开函数 |
|---|---|---|
| 公共工具 | core/ncm_common.h/.c |
ncm_hex_encode/decode ncm_b64_encode/decode ncm_md5_hex ncm_sha256_hex ncm_url_encode/decode ncm_random_bytes ncm_now_ms 等 |
| JSON | core/ncm_json.h/.c |
ncm_json_parse ncm_json_obj_get ncm_json_arr_get/len ncm_json_path[_str/_int/_bool] ncm_json_get_str_at/get_int_at |
| HTTP | core/ncm_http.h/.c |
ncm_http_request(一次性 POST/GET,限幅)ncm_http_download(流式回调 + max_bytes)ncm_http_parse_set_cookie ncm_http_set_verify_default |
| 存储 | core/ncm_store.h/.c |
ncm_store_init ncm_store_get/set_device_id ncm_store_machine_key ncm_store_cookie_encrypt/decrypt ncm_store_save/load/clear_session ncm_store_write/read_file ncm_store_read_cfg_int |
| 加密 | core/ncm_crypto.h/.c |
ncm_crypto_eapi ncm_crypto_weapi ncm_crypto_linuxapi ncm_crypto_rsa_raw_selfcheck |
| 请求构建 | core/ncm_req.h/.c |
ncm_req_build(eapi/weapi/linux,注入 header/csrf_token/UA/Cookie)ncm_req_cookie_get |
| 登录 | core/ncm_login.h/.c |
ncm_login_qr_url ncm_login_unikey ncm_login_qr_matrix(qrcodegen)ncm_login_poll ncm_login_status_check ncm_login_save/load/clear_cookie |
| 歌单/播放 | core/ncm_api.h/.c |
ncm_api_user_playlist ncm_api_playlist_detail ncm_api_song_detail ncm_api_song_url ncm_api_song_url_auto ncm_api_lyric ncm_api_level_str |
| 缓存 | core/ncm_cache.h/.c |
ncm_cache_init ncm_cache_set_limit_gb/limit_gb ncm_cache_load_setting ncm_cache_lookup ncm_cache_open/write/commit/abort ncm_cache_total_bytes ncm_cache_evict ncm_cache_path |
| 上下文 | core/ncm.h + ncm_ctx.c |
ncm_ctx_t(deviceId/cookie/CA/默认音质/cache)ncm_ctx_init ncm_ctx_set/load/clear_cookie ncm_ctx_default_cache_dir |
关键常量:NCM_EAPI_KEY/NCM_WEAPI_PRESET/NCM_WEAPI_IV/NCM_LINUXAPI_KEY/NCM_RSA_DER_B64(ncm_crypto.h);
UA 按加密类型分流(eapi=iPhone NeteaseMusic 9.0.90/5038,weapi=Chrome/Edg 124),请求头注入对齐 CNMPlayer request.rs(deviceId/UA 稳定)。
9.2 关键实现取舍
- CA 方案(
ncm_http.h注释):verify_ca=1(默认)加载系统 CA bundle(或ca_path),失败即拒绝(防 MITM);verify_ca=0跳过校验(仅调试/受限网)。全局默认由ncm_http_set_verify_default设定,ncm_ctx_init以其verify_ca/ca_path设置。设备侧需内置 CA 根(VENDOR_NOTES §备注),否则接口 eapi/weapi 会被拒。 - RSA raw:用
mbedtls_rsa_public()无 padding 纯模幂,16 字节 secret 右对齐 128 字节(前导 0),输出 256 小写 hex——已与 Pythonpow(m,e,n)逐字节一致(见 9.4)。 - weapi:双层 AES-128-CBC(
paramsbase64,含+/=需 urlencode)+ RSA(raw) 加密随机 16 位 base62 密钥(RSA 前反转)。 - 会话cookie:加密落盘
MeowData/<pkg>/data/session.enc,密钥 =sha256(MAC + 盐);无 MAC(qemu/无网卡)回退到设备私有.mkey派生。仅为防裸读模糊,非真安全(头文件已注明)。 - 缓存:目录默认
MeowData/os.meow.ncm/cache/;上限默认 10GB(1~100 可配),读设置键ncm.cache_limit_gb(<data_dir>/ncm.cache_limit_gb.cfg,对齐 SDK meow_config);LRU 按文件 mtime,ncm_cache_lookup/commit刷新;命中即离线可播。 - 音质:
ncm_api_song_url_auto从 desired(默认 Hi-res)往下降级hires→lossless→exhigh→higher→standard,只返回可解码 mp3(url 非空且 type=mp3);无该档对当前歌曲降级。
9.3 构建
./build_core.sh → out/core-host/、core-mipsel/(自有代码对象 + vendor qrcodegen + jsmn 唯一实现 TU),链接 out/mbedtls/{host,mipsel}/*.a,产出:
test/test_ncm_core_host(宿主,可跑)out/test_ncm_core_mipsel(mipsel 静态)。size:text 462662 / data 212 / bss 27688(含测试 printf 基建 + mbedtls + qrcodegen;核心库本身链接更小)。- 自有代码
-Wall -Wextra0 warning;vendor 警告豁免。mipsel 用.tools/zig-mipsel-cc静态交叉。
9.4 验证记录(宿主 + qemu-mipsel 双向实测,响应已脱敏/截断)
- unikey 实拿(eapi):
{"code":200,"unikey":"<uuid>"}(宿主dc5bd090-…,qemu-mipselef9a5db2-…);二维码 URLhttps://music.163.com/login?codekey=<uuid>。 - 轮询(未扫码):
{"code":801,"message":"等待扫码"}→state=等待扫码(801),两平台一致。 - login_status(伪造 cookie):返回 weapi 可解析 JSON(code 200 但 profile null)→ 库判
NCM_ERR_AUTH(-13),证明 weapi 请求被服务端接受。 - RSA raw 交叉:
ncm_crypto_rsa_raw_selfcheck输出55b33e7e…b2b83,与 Pythonpow(m,65537,n)(§3 已交叉验证值)逐字节一致。weapi params 为合法 base64(RSA 封装的 16 字节随机密钥无法无私钥解密,其 AES-CBC 为标准用法)。 - eapi 往返:
ncm_crypto_eapi产物可用 AES-ECB 解回含url+json的明文(/api/login/qrcode/unikey+{"type":3})。 - cookie 加解密:38 字节串
MUSIC_U=…; __csrf=xyz; os=pc加密→解密完全一致(含一次 mbedtlscrypt_cbc就地改写 IV 的坑,已修)。 - 缓存:写入/命中/abort 不留档/强制小上限触发 LRU 淘汰(总字节降至 ≤ 上限)。
- 二维码:41×41 内矩阵生成,暗模块 >0。
- 无凭据错误处理:
user_playlist(uid=0,无cookie)优雅返回(公开数据或错误码,不崩溃);song_url(id=0)返回无 url。
9.5 md5(自有源码)
778847a8419325af2b95ad0ef7749781 core/ncm_api.h c07d246c9b64e4d3b9078b9e33d19fe4 core/ncm_cache.h
6e878566f6a4c4377c98e87e9967fdcc core/ncm_common.h 9056253bfb635d8224e7454c6b1796a2 core/ncm_crypto.h
157807f07a96f43249f6aeb9707d053f core/ncm.h 477dd9f5eab61e2940cb432d1b215b3a core/ncm_http.h
398e62a0b641f13bc80c02d5e42a0401 core/ncm_json.h 05fb336d3b65b041095bd8fb3c64bc3c core/ncm_login.h
96f423ef1a36bda7b5cb3961fbe22028 core/ncm_req.h 02c7ff38dd856455f32a4cc7bbc5edd5 core/ncm_store.h
f8f8e96464d3c4ebcf1898eb4212c55a core/jsmn_impl.c a00e9b9b1adf7c6cf924eb9ecfc90a67 core/ncm_common.c
e75c29217ef5dd3f0e23c740b79210c1 core/ncm_crypto.c 9ebfa83e31a530b4dc8fd7877c2f5fb9 core/ncm_ctx.c
319df449ecd7727f328e01adb518ba57 core/ncm_http.c d69ff302ec111a74f621571818da4622 core/ncm_json.c
a53e55f9cab828fd59b694330ec7371e core/ncm_login.c d81eda09640ff748b0a1c14607447d1b core/ncm_req.c
1ceda66d30d44a3d8850df2e876c0195 core/ncm_store.c 7cdb84fcf87384eb5125e5aae01eac84 test/test_ncm_core.c
94a4dd07fba98a6e7b70021aaeb8cb19 build_core.sh
产物:test/test_ncm_core_host md5 0e41d3d413819768d65d2e9ca9e082ed;out/test_ncm_core_mipsel md5 e05f1937e5fc49a2590b830c28ee6372。
9.6 出入清单(只改 patch_v2/ncm_app/)
新增:
core/ncm.h ncm_ctx.c // 伞头 + 上下文
core/ncm_common.h/.c // hex/base64/md5/sha256/urlencode/errcode
core/ncm_json.h/.c // jsmn 路径导航
core/ncm_crypto.h/.c // eapi/weapi/linuxapi + RSA raw
core/ncm_http.h/.c // mbedtls HTTPS POST/GET + CA取舍 + 超时 + chunked
core/ncm_store.h/.c // deviceId/MAC机器密钥/cookie加解密/配置读取
core/ncm_req.h/.c // 加密请求构建(md5/UA/cookie 注入)
core/ncm_login.h/.c // unikey/二维码矩阵/轮询状态机/cookie捕获/login_status
core/ncm_api.h/.c // 歌单/详情/歌曲/播放地址/歌词
core/ncm_cache.h/.c // LRU 缓存 + 离线可播
core/jsmn_impl.c // jsmn 唯一实现 TU(防多 TU 重复定义)
test/test_ncm_core.c // 宿主 + mipsel 综合实测
build_core.sh // 宿主 + mipsel 编译链接
修改:
REPORT.md // 追加本批记录
(未动 vendor/,未动其余目录)
9.7 未做/诚实出入
- 803 真登录未实测(需真实扫码):code 803 分支的 set-cookie 捕获/合并逻辑按 document 实现,但未真跑;联调时建议真扫码验证一次
MUSIC_U内容与login_status校验链路。 - weapi params 无法无 RSA 私钥解密核验其内部 AES 双层,但 RSA 封路径已逐字节交叉,AES-CBC 为标准用法(可用 CNMPlayer 同源实现比对)。
- 播放地址/缓存仅验证到“请求-解析-无版权返回 null”;未做真实音频下载落盘(缓存 open/write/commit 已由单元测试覆盖;真实 mp3 下载留给带 UI 的播放层用
ncm_http_download+ncm_cache_*串联)。 - 栈用量:单次 http 调用涉及 mbedtls ssl/净栈,峰值栈估几十 KB(与预研 demo 同级);App 线程栈需 ≥64KB,联调时以真机线程栈为准。
- JSON 响应限幅:歌单/歌曲详情走 1MB~2MB 堆缓冲 + token 计数后精确分配,不算常驻;封面/大 JSON 不整体驻留,封面下载另有
max_bytes限幅(建议 ~256KB,未在本次接线)。
10. 网易云客户端 v1 = App + UI + 播放链路(os.meow.ncm)
日期:2026-08-31。在 §9 核心库之上落地 App:UI(登录/歌单/播放/设置)+ 播放链路(URL 降级→缓存→minimp3→audio_out)+ 媒体会话 IPC。 仅改动
patch_v2/ncm_app/app/;未动 sdk/、meow_launcherd/(并行域冻结);未碰 vendor/、core/(§9 核心库原样使用)。 宿主 + qemu-mipsel 双边实测见 §10.5。未 commit/push/刷机/动设备。
10.1 包结构与页面
patch_v2/ncm_app/
├── app.json # {"name":"网易云音乐",exec:.../os.meow.ncm/ncm,package,version:0.1,version_code:1,framework:1,immersive:1,fonts:gbk}
├── build_ncm.sh # mipsel 静态 0 warning
├── build_ncm_host.sh # 宿主离屏自测(MEOW_UI_DUMP)
├── ncm # mipsel 静态 App(md5 见 §10.6)
├── app/app.c # main + 装配 + 事件分发(物理键不映射;左缘左滑=返回/退出)
├── app/ncm_app_internal.h # 内部共享头(ACT_*/布局/数据上限/播放运行态/各页与 play/ipc 接口)
├── app/ui/
│ ├── chrome.c # TopAppBar/内容区几何/空态/页脚/底部提示条(ncm_snack)
│ ├── login.c # 登录页:后台 worker: unikey→二维码矩阵→轮询→803 落 cookie→login_status
│ ├── home.c # 我的歌单(meow_listview:folder图标+名称+曲目数副标题+箭头)
│ ├── playlist.c # 歌单详情(曲目列表:歌名+歌手;点击起播并推进播页)
│ ├── player.c # 播放页(封面+标题/歌手+进度条seek+播放/暂停/上下曲+滚动歌词;竖/横布局可切)
│ └── settings.c # 设置(音质 Hi-res/较高/标准 + 缓存上限 1~100GB + 退出登录)
├── app/logic/
│ ├── appstate.c # ctx/cookie/uid/歌单/歌曲/设置加载与持久化(meow_config_*)
│ ├── lyric.c # LRC 时间轴解析(原文+翻译合并、排序、行定位;流式有界 NCM_LYRIC_LINES)
│ └── cover.c # 封面后台线程下载(≤288KB)+解码单张缓存 + 落盘 cover_current.jpg 供 launcher
├── app/play/
│ ├── player_manager.c # 播放线程:URL 降级→缓存(命中即播/否则流式落盘)→minimp3→audio_out;暂停冻结/seek重开/上下曲
│ ├── audio_out.c # 音频输出:ALSA 直开 hw:0,0(内核UAPI) / FILE(WAV) / NULL(测RT)
│ ├── ipc_media.c # 媒体会话 IPC:MSG_META 上报 + MSG_PLAYCTL 接收(meow_ipc.h 直接帧格式)
│ └── minimp3_impl.c # minimp3_ex 唯一实现 TU
└── app/test/
├── test_ncm_app.c # 宿主真实页面渲染(NCM_PAGE=login|loginlive|home|playlist|player|lands|settings)
└── test_decode.c # 解码链路(minimp3→audio_out FILE)验证
页面/导航:根页 = 已登录 ? ACT_HOME : ACT_LOGIN。栈:HOME → PLAYLIST → PLAYER;SETTINGS 任意页可进。
左缘左滑(on_swipe LEFT):PLAYER→pop 回 PLAYLIST;SETTINGS→pop;PLAYLIST→pop 回 HOME;HOME/LOGIN→meow_ui_request_exit() 退出 App。
10.2 关键实现
登录(ui/login.c):进入即起后台 worker(网络全在独立线程,UI 不卡)。ncm_login_unikey(eapi) → ncm_login_qr_url + ncm_login_qr_matrix(qrcodegen) → 渲染 37×37 位图(白底+黑模块 6px,居中)→ 每 1.5s ncm_login_poll(801/802/803/800)。803:捕获 set-cookie 合并 → ncm_ctx_set_cookie 加密落盘 → ncm_login_status_check 取 uid/nickname → 置 done → UI tick meow_view_page_replace(ACT_HOME)。800(过期)自动刷新二维码;网络失败 tap 重试。
主页(ui/home.c):meow_listview 虚拟化(folder 图标 + 名字 + N tracks 副标题 + 右侧箭头);空态/会话过期/错误绘制物;topbar 标题=昵称 + 「↻」刷新;行数据轻量(不下载封面缩略图)。点击行 → playlist_open(idx)。
歌单详情(ui/playlist.c):ncm_api_playlist_detail(trackIds) + 分批 ncm_api_song_detail(≤256 首,NCM_SONG_MAX);行=歌名+歌手;点击行 → player_play_song(row)(起播 + 推播放页)。
播放页(ui/player.c):封面(cover_request 后台解码,meow_ui_img_cover 居中裁方 220px)+ 题/歌手 + 进度条(手绘,点 bar seek)+ 控制钮(prev/play-pause/next)+ 滚动歌词(lyric_line_at(pos) 居中高亮)。横竖屏可切:device LCD 为竖屏 480×720,故为布局切换(竖=封面上/歌词下;横=封面左/信息歌词右),topbar 「⟳」触发,player_set_screen()。进度/状态/歌词每 tick 读 player_manager_*(跨线程 mu)。
设置(ui/settings.c):3 行卡片(音质/缓存上限/退出登录)。音质→BottomSheet(Hi-res/Higher/Standard);缓存→BottomSheet(1/2/5/10/20/30/50/100 GB);退出→confirm 弹窗 → appstate_logout() + replace ACT_LOGIN。持久化 MeowData/os.meow.ncm/data/ 下 ncm.level/ncm.cache_limit_gb(键与核心库 ncm_cache_load_setting 对齐)。
10.3 播放链路(app/play/player_manager.c)
player_manager_play(idx) → 播放线程外层循环 → run_song(idx):
① song_local_file: ncm_cache_lookup 命中 → 直接播(离线);否则
ncm_api_song_url_auto(desired=设置音质, 逐档降级 hires→…→standard, 只收 mp3)
+ ncm_http_download 流式写缓存 temp → ncm_cache_commit(先落盘再播)
② mp3dec_ex_open(path, MP3D_SEEK_TO_BYTE) → audio_out_open(44.1k/16/2)
③ 循环 mp3dec_ex_read → audio_out_write(pcm, frames) → frames_done→pos_ms(~200ms 节流更新)
暂停=冻结(停读停写停推 pos,状态 PAUSED);seek=重开解码器+audio_out 并字节 offset 近似定位;
上下曲/停止=打断 run_song 回外层;播完=自动顺延下一首(无则 STOPPED)
- 缓存:目录
MeowData/os.meow.ncm/cache/(ncm_cache_*),LRU 上限可配(默认 10GB,1~100)。 - audio_out:宿主/qemu 无
/dev/snd/pcmC0D0p→ 回退 FILE WAV(NCM_AUDIO_SINK=file/NCM_AUDIO_OUT=<path>)或 NULL(NCM_AUDIO_SINK=null,仅测 RT)。设备走 ALSA 内核 UAPI 直开(sound/asound.h,masks/intervals 重索引已按 musl 头修正),真机时序待验证。
10.4 媒体会话 IPC(app/play/ipc_media.c,SDK 无媒体封装自写最小客户端)
- 连接
MEOW_IPC_SOCK(缺省AF_UNIX /usr/data/meow_ipc.sock)。 MSG_HELLO(app_id=os.meow.ncm, flags=MF_PLAYBACK) → 读 ACK。- 同 fd 每 ~1s 发
MSG_META(m_meta_t:title/artist/album/pos_ms/dur_ms/state=_1播 2暂停 0停 3缓冲,flags=MF_ART|MF_COVER(有 cover_current.jpg 时),artwork=该文件路径,app_id)。launcher 以最近活跃会话为准 → 通知中心媒体卡片 + Now Playing 页。 poll(fd,POLLIN,200ms)收MSG_PLAYCTL→ cmd 1播放/2暂停/3上一首/4下一首/5seek →player_manager_*。- 无 launcher socket(宿主/qemu 无守护)→ log
ipc: no launcher socket并优雅返回。
10.5 验证记录(宿主 + qemu-mipsel 双向实测)
| 项 | 方式 | 结果 |
|---|---|---|
| 各页 UI | 宿主离屏 MEOW_UI_DUMP + PIL 转 PNG 目检 |
login/home/playlist/player/lands/settings 全部正确(截图 app/out/shots/*.png) |
| 登录真实二维码 | NCM_PAGE=loginlive:核心库真实 ncm_login_unikey→qr_matrix→渲染 |
真 unikey 40287b2e-9ef8-4f2e-8bf5-04febdaf631a→37×37 可扫 QR(out/shots/loginlive.png) |
| mipsel 二进制 | qemu-mipsel ./ncm(离屏+MEOW_UI_DUMP,timeout SIGTERM 干净退出) |
真实 mipsel 静态二进制跑通:start→ipc: no launcher socket(优雅)→登录页渲染真实可扫 QR(out/shots/mipsel_qemu.png) |
| 解码链路 | test_decode(minimp3→audio_out FILE WAV) |
MIPSTest.mp3 → 8.21s/48k/立体声/16bit PCM,WAV 合法(ch=2 rate=48000 frames=393984),446× RT(解码余量巨大) |
| 构建 | build_ncm.sh mipsel |
0 warning(自有代码 -Wall -Wextra);text 764KB |
宿主截图:app/out/shots/{login,loginlive,home,playlist,player,lands,settings,mipsel_qemu}.png(PPM→PNG)。
注:宿主 5×7 点阵字仅 ASCII(中文/歌名在设备上以 gbk 大字库渲染;桌面截图为 ASCII label 布局自查)。
10.6 md5
1e331964b62b6f5b0a593c5d97a4e030 ncm # mipsel 静态 App
(app 源码 md5)
50dc58dc62a9d71a23e90bb00c79a36b app/app.c cf5e3283065dfce798d668c22ee87f65 app/ncm_app_internal.h
b961bc769951208a09017f278c05a95b app/ui/login.c 5098bd843f6943bcda5aacfb7fae961f app/ui/home.c
ef600c9d3a88eb2463d1c8f312a5a202 app/ui/playlist.c a55846822deb0246c6780d8a7ac99f46 app/ui/player.c
05fef4b6fa5692abf0da57db40ad6f90 app/ui/settings.c 013111dea3172fdeb5353a2ec69a71a5 app/ui/chrome.c
203a9932bde8a739b436e246ba634638 app/logic/appstate.c 48a92829ac414ffad5dbe27e0836c21b app/logic/lyric.c
ed2d9658bc8707a5fd689b386da472f9 app/logic/cover.c e7cef918d2be04f6d4c5a25f62d2a9ab app/play/player_manager.c
4ceeb2f4f1b0a312c9468b9f77886dc7 app/play/audio_out.c 8c1716ca99cd773a0cf9a105f793b845 app/play/ipc_media.c
6ca4b2137c4a424d823f94a8b3fcd037 app/play/minimp3_impl.c a2ca48b2c6afa6c36f045ba8b2687fc5 app/test/test_ncm_app.c
9ebfb5401c7818cfd0c21d640b07c521 app/test/test_decode.c
10.7 出入清单
新增(仅 patch_v2/ncm_app/):
app.json # 应用描述
build_ncm.sh / build_ncm_host.sh # mipsel / 宿主构建
app/app.c ncm_app_internal.h
app/ui/{chrome,login,home,playlist,player,settings}.c
app/logic/{appstate,lyric,cover}.c
app/play/{player_manager,audio_out,ipc_media,minimp3_impl}.c
app/test/{test_ncm_app,test_decode}.c
out/shots/*.png # 验证截图
修改:
REPORT.md # 追加本批记录
(未动 sdk/、meow_launcherd/、vendor/、core/;核心库 §9 原样使用)
10.8 真机待验证项
- ALSA 直开
hw:0,0:本层用内核 UAPI(/dev/snd/pcmC0D0p+ ioctl hw_params:RW_Interleaved/S16_LE/44100/2ch)——设备时序/权限/采样率匹配待真机验证;宿主已回退 FILE sink 证解码链路。 - 真实扫码登录(803):set-cookie 捕获→加密落盘→
login_status校验链路按文档实现但未真扫;建议真机扫码验证一次MUSIC_U内容与 home 拉歌单闭环。 - 媒体会话:需 launcher 在真机跑(本 App 首真实媒体源);验证通知中心媒体卡片/Now Playing 显示 MSG_META + 下发 MSG_PLAYCTL 生效(本 App 已记
ipc: no launcher socket为优雅降级,真机有 socket 即上报)。 - 大字库:app.json
fonts:gbk声明已加;真机验证歌名/歌词中文渲染(宿主 ASCII 5×7 不含中文)。
10.9 诚实出入 / 已知限制
- 封面为歌单封面(
playlist_detail的 coverImgUrl)而非逐歌曲专辑封面:ncm_song_t未含al.picUrl,为不改 §9 核心库(已驯化契约)故 v1 用歌单封面代为 artwork;逐曲封面需给ncm_song_t增al.picUrl字段(附加、向后兼容,建议二期)。 - 歌词为同步加载:播放页 enter 时
player_manager_load_lyric同步网络拉取(UI 短暂阻塞);建议后续改后台线程+就绪标志(音频线程独立不受影响)。 - 歌单曲目截断:
NCM_SONG_MAX=256(大歌单显示前 256 首);歌单数NCM_PLAYLIST_MAX=120。 - launcher 媒体卡片封面:经 MSG_META
artwork传<cache>/cover_current.jpg路径;若 launcher 读不到该路径则卡片仅题/艺/进度(App 内封面始终有)。 - IPC 仅处理 MSG_META/MSG_PLAYCTL(未接 MSG_SYS,媒体会话最小闭环,符合 v1)。
- bss≈924KB / text≈764KB:mipsel 静态;14MB 内存富余,但 bss 含封面下载缓冲(288KB)+QR 矩阵(31KB)+mbedtls 静态,已留充足余量。
meow_snack/页脚为 App 自绘(非 SDK 全局通知,避免与 launcher 系统 UI 冲突)。
10.10 真机 weapi 挂死根因定位 + 修复(2026-08-31,已真机闭环)
现象:真机
music.163.com/weapi/...(login_status_check/user_playlist)请求永久卡死, 线程wchan=poll_schedule_timeout,>10 分钟不超时;同机的 eapi(interface.music.163.com)正常。 定为 多个独立缺陷叠加,其中「超时用墙钟算」是那把导致「>10 分钟不超时」的关键一把。
根因(按“致卡死”权重排序)
-
超时/截止用 CLOCK_REALTIME(墙钟,非单调)—— 这是「>10 分钟不超时」的真根因。
ncm_http.c的wait_io()与deadline都用ncm_now_ms()(CLOCK_REALTIME)。设备在请求进行中被 NTP 把墙钟拨回了 Δ(校时 step,方向可正可负)。于是rem = (uint32_t)(deadline - now) = (now0 + tmo) - (now0 - Δ) = tmo + Δ;若 Δ 为小时级/天数级,rem变成巨大的 uint32_t(甚至溢出回绕),mbedtls_net_poll()因此拿到远大于 15s 的等待窗口, 线程长期阻塞在poll_schedule_timeout。墙钟拨回越大,阻塞越久——这正解释了“>10 分钟且与 eapi 行为无关”(eapi 恰好在该时刻没撞上拨回,或撞上后被验证时序放行)。 宿主演示(bench/monotonic_test.c):拨回 6 小时。- 墙钟
rem = 21615000 ms ≈ 6.0 h(poll 阻塞 6 小时) - 单调钟
rem = 15000 ms = 15 s(不变)
结论:超时/截止计算必须用
CLOCK_MONOTONIC(永不跳变)。墙钟只留给 request_id/_ntes_nnid/ 落盘时间戳这类“取当前墙钟”的场合。 - 墙钟
-
响应头解析“隔行跳行”,漏掉
Content-Length。parse_headers()旧循环while((p=strstr(p,"\r\n"))){ p+=2; ...; p=eol+2; }每轮把下一行吞掉: 处理Server后直接跳到Connection,中间Date/Content-Length/Vary… 被跳过。 导致content_length停留在SIZE_MAX→body_st_init落入BS_CLOSE(读到对端关闭)。 若服务器 keep-alive + 无 chunked(网易常见),body 将一直读到对端关;配合根因①的墙钟异常, 阻塞被拉长成“永久卡死”。宿主复现:/-11 FULL(见根因③)前先看到status=0 content_length=0。 修法:逐行遍历(先定位状态行\r\n,再逐 header 行到空行),并顺带修正状态码解析(旧代码取"1.1 200 OK"的首空后即报200,实际取到1.1→status恒 0)。 -
请求头缓冲 1024 太窄 → 真机大 cookie 时
NCM_ERR_FULL,请求根本没发出。 weapi 带真实会话 cookie 时extra_hdrs(UA + Content-Type + Cookie + Referer)可达 ~1.1KB, 旧char headbuf[1024]溢出,http_run返回-11 FULL(真机实测dev_repro2:rc=-11,status=0 content_length=0 headers_len=0,即请求未发)。修法:headbuf[8192](≥ extra_hdrs + ~200)。 -
weapi 发送路径把长 cookie 值截断(MUSIC_U 只剩 191 字节)→ 服务端识别为游客(profile:null)。
ncm_req.ccookie map 的ck_t.val[192]会把 834 字节的MUSIC_U截到 191 → 服务端看成 guest 会话,login_status返回{"code":200,"account":null,"profile":null}(-13 AUTH)。修法:cookie map 改存 值指针 + 长度((const char*)val, size_t vlen,不复制不截断),并按名去重;生成值(_ntes_nnid/ WNMCID/NMTID)buffer 提升到函数作用域保证生命周期。cookie_full4KB。 -
响应头/体切分顺序 bug(我修
split时引入后已修正):在split处 NUL 终结头部会覆盖 body 首字节, 导致 body 首字节变 NUL → JSON 解析失败(-9)。已改为先送 body 再 NUL 终结头部(二者同址)。 -
Content-Length: 0没立即结束:补body_st_init对content_length==0直接置BS_DONE,避免等对端关。
修法(全部在 patch_v2/ncm_app/)
core/ncm_common.h/.c:新增ncm_monotonic_ms()(CLOCK_MONOTONIC);ncm_now_ms注明仅用于墙钟。core/ncm_http.c:wait_io+deadline改用ncm_monotonic_ms();- 重写
parse_headers(逐行、状态码用strtol忽略 reason、去掉 16KB 栈缓冲改 128B); headbuf[1024]->[8192];split处先送 body 再 NUL 终结;Content-Length:0直接BS_DONE。
core/ncm_req.c:cookie map 改(ptr,len)存储(不截断、按名去重);生成 cookie 值 buffer 提作用域;cookie_full[2048]->[4096]。core/ncm_login.c:login_status_check设timeout_ms=6000(硬上限)且respbuf[16384](容纳真实 profile)。app/ui/login.c:803 后即时status_check取 uid/nickname 并appstate_set_logged_in落盘 (修复“803 后 uid 恒空”;worker 线程执行,不卡 UI)。app/logic/appstate.c:appstate_load_playlists记home: playlists rc=? count=? uid=?(供真机日志核对;appstate_ensure_uid因 uid 已落盘而短路,不再阻塞歌单页;status_check 有 6s 硬上限兜底)。
验证记录(宿主 + 真机)
宿主(./test/test_ncm_core_host,全 PASS;另 benchmark 复现工具)
bench/monotonic_test.c:墙钟拨回 6h →rem=21615000ms;单调钟 →rem=15000ms。根因①钉死。bench/host_repro.c(weapi account/get,空 cookie):修复后rc=0 status=200 content_length=42 body_len=42, body 正确为{"code":200,"account":null,"profile":null}(修复前status=0 content_length=0 body=0)。bench/host_fullcookie.c+ 真机解密 cookie:全量 cookie(未截断、去重)→ 服务端返回body_len=1161(真实 profile); 被截断/带旧重复 token 的 cookie → 服务端返回body_len=42(guest)。根因④钉死:去重+全量才被认可。- 宿主测试套件:cache/qrcodegen/network(unikey/poll)/no-credential 各用例全 PASS(无回归)。
真机(最终确认 1 次,风控:真实 cookie 请求严格受限)
bench/verify_home.c(复刻 App home 链路:status_check→user_playlist,链接修复后 core):即 home 真实列出用户 64 个歌单,满足验收。status_check rc=0 elapsed_ms=721 uid=408996274 nick=Any无言 user_playlist rc=0 elapsed_ms=2562 count=64 [0] Any无言喜欢的音乐 (5191 tracks) [1] Docu (1 tracks) ... [7] Ethereal (2 tracks) VERIFY OK: user=408996274 playlists=64- 真机中途定位记录(修复前):
dev_repro2见rc=-11(全量时 status=0/content_length=0)(根因③);dev_repro见cookie_len=1842(重复 cookie,根因④由 host_fullcookie 证明需去重+全量);修复后dev_repro见status_check rc=0 uid=408996274 nick=Any无言(根因②③④⑤全消)。 - 部署:
/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm(先ncm.new再mv),md5bf6d2a79504d49cdc9b87e6f34cc6aec。
出入 / 已知限制(本批)
- mipsel
-Wall -Wextra自有代码 0 warning(build_ncm.sh复核);vendor 豁免。 - 未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机;未动设备系统/恢复区。
- 历史遗留:真机
session.enc里是旧版重复 cookie(MUSIC_R_T/A_T 等重复多次);本次通过发送路径 “按名去重 + 全量不截断”规避(verify_home已证明服务端认可)。未追改merge_cookie/落盘去重 (发送端已去重,功能不受影响);如需落盘也干净,可后续在ncm_login_poll的merge_cookie处按名去重。 appstate_ensure_uid仍同步跑在 UI 线程,首次(uid 未落盘时)会占 UI 至多 ~6s 硬上限(正常 ~0.7s); 真正的“不阻塞歌单页”可由「uid 落盘后短路」+「6s 硬上限」保证,未改为后台线程(避免范围扩散)。
10.11 真机三问题修复:启动卡顿 + 大歌单 Load Failed + 小歌单 No tracks(2026-08-31,已真机闭环)
现象(用户真机实测):登录通、歌单列表 64 个。三个问题: ① 有登录态点开 App 卡数秒才进;②「我喜欢的音乐」(5191 首) 点开 Load Failed;③ 其余歌单全部 No tracks。 本批定位多处独立缺陷并修复,全部经宿主复现 + 真机验证。仅改动
patch_v2/ncm_app/。
根因(按现象对应)
- P1 启动卡数秒:
app/ui/home.chome_page_enter → home_reload → appstate_load_playlists(weapi user/playlist)在 UI 线程同步执行;真机该 weapi 往返 ~2.5s+(verify_home:elapsed_ms=2562),uid 未落盘时还叠 status_check(6s 硬上限)。期间 UI 冻死(paint_full不执行)。 - P2 大歌单 Load Failed:
core/ncm_api.c:ncm_api_playlist_detail发{"id":..,"n":100000,"s":8}+ 响应 capNCM_RESP_MAX(1MB)。5191 曲歌单 detail 真机实测status=200 cl=3168819(3.0MB:playlist.tracks塞满歌曲对象),1MB cap 溢出 →NCM_ERR_FULL(-11)→ Load Failed。关键:trackIds数组与n无关(n=1 时仍完整返回 5191 条,实测),是tracks(完整歌曲对象)撑大响应。 - P3 小歌单 No tracks:
ncm_api_song_detail把曲目 id 数组塞进{"c":"[...]"}时内部引号未转义,实发{"c":"[{"id":2048962657}]"}(坏 JSON),服务端回200 Content-Length:0→ jsmn 空体 →NCM_ERR_JSON(-9)→ 0 首。宿主复刻:转义后{"c":"[{\"id\":2048962657}]"}→ 服务端回2146B正常 songs。 - P0(连带新发现):网易云歌曲 id 可 >2^31(如
2746301825),而 App 用long存 song_ids;MIPS32 的 long=32 位,strtol溢出饱和到LONG_MAX(2147483647)→ 大歌单 trackIds 全错(2746301825被解析成2147483647),song_detail 拿错误 id 只回 30 首。宿主 aarch64(long=64 位)不暴露,真机 MIPS32 才炸。 - P3b(连带):批量 song_detail 转义后 json 变长 → eapi
params[2048]溢出 →ncm_crypto_eapi返NCM_ERR_CRYPTO(-8)(batch=64 实测 rc=-8)。
修法(全部在 patch_v2/ncm_app/)
- P1
app/ui/home.c:加载改后台 worker(home_worker,detached);home_reload立即返回并画 LOADING;home_tick按g_home_gen完成代数回填。app_on_tick路由home_tick。ensure_uid(status_check)+ user_playlist 全在 worker,UI 不阻塞。(顺带消掉 §10.10 遗留的「ensure_uid 同步跑 UI 线程」。) - P2
core/ncm_api.c:ncm_api_playlist_detail:n100000→1(tracks只带 1 首,3MB→~900KB);响应 cap 1MB→2MB(trackIds 大的歌单留余量)。 - P3
core/ncm_api.c:ncm_api_song_detail:c数组内部引号转义({\\\"id\\\":%lld})。 - P0
core/ncm_common.{h,c}新增ncm_parse_i64(strtoll);core/ncm_json.{h,c}新增ncm_json_get_i64_at;core/ncm_api.h两处 id 签名long*→int64_t*;app/ncm_app_internal.hsong_ids[]long→int64_t。 - P3b
core/ncm_req.ceapi 局部缓冲full2[3072]→[4096]、params[2048]→[4096]。 - 防大歌单冻 UI:
app/ui/playlist.c歌单详情同样后台化(playlist_worker+playlist_tick);appstate_load_playlist_songs不再覆写g_st.active_playlist(防后台 worker 覆盖用户已切换的歌单)。
验证记录
宿主(同 core 库 + 真实服务器)
bench/host_chain_probe(detail→song_detail 背靠背,小歌单18206253175):修前song_detail rc=-9;修后rc=0 bn=1 song_count=1。bench/host_big(大歌单581857874):修前 big detail 溢出;修后detail rc=0 idn=256+ 4×64 批全rc=0→song_count=256,首曲Chrono Rewind / CVRCH。- 宿主 eapi 复刻:未转义 c → 服务端
200 CL:0;转义后200 len=2146。P3 钉死。 test/test_ncm_core_host全 PASS;build_ncm.shmipsel-Wall -Wextra0 warning。
真机(bench/verify_fixes,一次运行,小歌单 2 请求 + 大歌单 5 请求)
device_id=BD6DE5CC2DBCB617253A00438897DC6C02FFC7CFDB99DB0FA08E cookie_len=1842
[small(Docu/1)] detail rc=0 idn=1 ids[0]=2048962657
song off=0 batch=1 rc=0 bn=1 RESULT song_count=1
[0] Verve (feat. FJØRA) / Ryan Taubert
[big(5191)] detail rc=0 idn=256 ids[0]=2746301825 ← 修 P0 后正确(修前=2147483647)
song off=0/64/128/192 batch=64 全 rc=0 bn=64
RESULT song_count=256 [0] Chrono Rewind (时空回溯) / CVRCH
→ P2/P3/P0/P3b 真机全消:小歌单 1 首、大歌单 256 首(不再 Load Failed / No tracks)。
真机启动(部署后后台运行 5s)
6213257 [os.meow.ncm] INFO start
6217019 [os.meow.ncm] INFO home: playlists rc=0 count=120 uid=408996274 ← 后台 worker 完成
6218319 [os.meow.ncm] INFO exit
→ 启动即进 home;home: playlists 由 worker 与 UI 并发产出,UI 不再被 weapi 阻塞(P1)。修前旧二进制日志 start→home playlists 同链路为 UI 线程同步(home.c 阻塞)。
MIPS32 溢出佐证(P0):修 P0 前真机 verify_fixes:big detail idn=256 ids[0]=2147483647(strtol 溢出饱和);修后 ids[0]=2746301825。
部署 + 出入清单(本批,只改 patch_v2/ncm_app/)
- 部署:
/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm(先ncm.new再mv),新 md5f6bd765a374effaf866784c3165e476e(旧bf6d2a79504d49cdc9b87e6f34cc6aec)。
修改:
core/ncm_api.c # P2 n=1+2MB cap;P3 c 转义;P0 int64 签名
core/ncm_api.h # int64_t ids 签名
core/ncm_req.c # P3b eapi 局部缓冲提升
core/ncm_common.h/.c # 新增 ncm_parse_i64
core/ncm_json.h/.c # 新增 ncm_json_get_i64_at
app/app.c # app_on_tick 路由 home_tick/playlist_tick
app/ncm_app_internal.h # song_ids→int64_t;声明 home_tick/playlist_tick
app/ui/home.c # P1 home 加载后台化
app/ui/playlist.c # 歌单详情后台化(防大歌单冻 UI)
app/logic/appstate.c # load_playlist_songs 不覆写 active_playlist
新增:
bench/verify_fixes.c # 真机复验小歌单+大歌单链路(回归工具)
REPORT.md # 本批记录
(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区)
变更文件 md5:
ceb89ee9e83752208056e2eadb5330fd core/ncm_api.c 63d4af4307c5cdb3922e59dc70116407 core/ncm_api.h
cc1ab202c8d603e296f02715718a8a25 core/ncm_req.c 45efe7071b8cbff552ac89fe8b2e9495 core/ncm_common.c
8fc658f656a1004f80ab5bc1555c2048 core/ncm_common.h df46e0cd3215d359025519b4d41316d6 core/ncm_json.c
6d473afe13176230268c68ddc655cb34 core/ncm_json.h 3d2daea41204c887915d8c078543afe0 app/app.c
282c24806fe0e9c97adf464f6a5c2776 app/ncm_app_internal.h 10c3b4b1097f1b0d82550659d0a87c4f app/ui/home.c
0efef753b32eef144f2c30e4592fbfea app/ui/playlist.c 94d10237c1aa73f7273f82ff319792f5 app/logic/appstate.c
bcfd75e5eb872a751c166c456eea0184 bench/verify_fixes.c f6bd765a374effaf866784c3165e476e ncm
诚实出入 / 已知限制
- 大歌单 load 慢(真机 detail ≈30s):
/v6/playlist/detail无论n/s都返回全量 trackIds(5191 个 ≈900KB,实测 n=1/0/100000 均 5191 条),无法用该端点分页/限流。真机 MIPS32 下载 + jsmn 解析 ≈30s(宿主 4.7s,~7× 慢,符合 MIPS vs aarch64 预期)。已用「playlist 后台化」避免冻 UI(显示 LOADING),但大歌单首屏仍 30s 级等待;要根治需改走/api/playlist/track/all(服务端分页)或流式 jsmn——建议二期。 n只限tracks不影响 trackIds:n=1对 ≤256 曲歌单仍完整返回全部 trackIds(idn=实际曲数,无回归);仅把「多出的完整歌曲对象」从响应里去掉,从而把 3MB 压回 ~900KB。- 曲目截断仍
NCM_SONG_MAX=256(现协议):大歌单显示前 256 首,未做滚动加载(二期)。 g_st.songs[]/song_count由 playlist worker 写、play/IPC 线程读(§10.9 FM4 旧已知风险):入播放页仍在同歌单,竞态窗口极小;沿用未加锁(与项目现状一致)。- 真机真实请求计数:本批新增 ~20 次(detail_probe 抓样 1 次(5 请求,样本落盘)+ song_probe 1 + app_path_probe 2 + verify_fixes 2 次 11 + 启动验证 2)。已尽量「一次请求存样本、离线重放」,但修复前后复验需真实响应,属必要开销。
- 未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区。
11. 网易云 UI 重做(按 NCM_UI_DESIGN.md,2026-08-31)—— 双套配色 / 黑胶播放页 / 质感
依据
devdocs/plan/NCM_UI_DESIGN.md将 App 从「文件管理器列表风」重做为网易云风格: 白红/黑红双套配色、主页双页签「我创建的/我收藏的」、独立搜索页、歌单详情封面头块+「播放全部」、 播放页黑胶唱盘(fill_rect 行扫构造圆 + 方盖内切裁圆封面 + 唱针)。仅改动patch_v2/ncm_app/。 未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机。
11.1 双套配色机制(§3.1/§3.3)
- 新增
app/ui/ncm_colors.c:ncm_pal_t{ncm_palette(dark)}两套 App 调色板(白红 / 黑红 + 播放页/登录专属色)+g_dark全局 +ncm_on_theme(mode)主题侦听。- 品牌红走 App 侧(SDK 全局 primary 是 MD3 蓝,App 无权改——红只落 App 自绘层)。
- 中性色(surface/onSurface/onSurfaceVariant/outlineVariant/error)一律
meow_theme_get(); 全程语义色 + App 调色板,零硬编码散落(§3.1 契约)。
app/app.c:meow_theme_add_listener(ncm_on_theme)(旧代码传 NULL 无效)+ 启动同步g_dark = (meow_theme_get_mode()==MEOW_THEME_DARK)。系统明暗切换 →ncm_on_theme→ 整 App 重绘实时换肤。- 色值(§3.2):day red
#C20C0C/ red_lit#EC4141/ red_ctr#FBE9E9; night red#EC4141/ red_lit#FF6A6A/ red_ctr#3A1919;登录 hero night#7A0E0E; 播放页底 day#FFFDF9/ night#0C0D10;盘体 day#1A1A1A/ night#141414。 验证(宿主抽样像素):day 组头红条(198,12,8)≈#C20C0C,night(239,65,66)≈#EC4141。✅
11.2 数据层(§7.0 明示缺口)
core/ncm_api.h:ncm_playlist_t增int is_collected;(1=收藏的/订阅他人,0=自己创建)。core/ncm_api.c:ncm_api_user_playlist解析响应subscribed(obj_bool)写入is_collected, 支撑「我创建的/我收藏的」分组(原实现只取 id/name/coverImgUrl/trackCount)。core/ncm_api.h/.c:新增ncm_api_cloudsearch(weapi/api/cloudsearch/pc,type=1 单曲), 解析result.songs[](id 用 int64/name/ar[0]/al/dt),关键词"\转义防坏 JSON。
11.3 页面改动
- 登录页(
ui/login.c):品牌红 hero 带(y=0 起,沉浸式)+ 红点 logo + 「网易云音乐/扫码登录」 标题区 + QR 卡(圆角 16 白底/surfaceContainer + 内描边)+ 状态字(day red/night red_lit)+ 底部提示。 - 主页(
ui/home.c):红系 topbar(标题「我的音乐」+ 刷新/设置/搜索动作,搜索按钮最右)+ 个人区卡片(首字圆形头像 red_ctr + 昵称 + ID + 箭头)+ 双页签「我创建的/我收藏的」 (meow_tabbar,红下划线/红选中字叠层注入)+ 组头红条(red_ctr 底 + 红字 + 红侧条)+ 歌单列表(rounded card 行 + 红 mini-vinyl 缩略 + 红色按压竖条);列表数据源按is_collected过滤分组。MEOW_LIST_SEL_NONE+ 自定义红态叠层(§4.2:SDK listview MD3 蓝选中不可改,改叠红条)。- 保留 home_worker 后台异步加载(§10.11 P1 修复),未回退成同步。
- 搜索页(
ui/search.c新增,ACT_SEARCH):TextField(SDK IME,仅英文/数字)+ 红色「搜索」确认钮 + 结果列表(点击可播,复用g_st.songs[]作为当前播放上下文)。风控纪律:搜索只在用户点「搜索」钮时发 (进入页面不自动搜索、不逐键发请求);网络走后台 worker(search_worker)。 - 歌单详情(
ui/playlist.c):封面头块(圆角封面/占位 + 歌单名 + 「创建者 · N 首」)+ 操作条 (红色「播放全部」 FILLED + 「收藏」OUTLINED(v1 置灰→snack) + 「评论」红字+红badge(v1 置灰→snack))+ 曲目列表(歌名 + 歌手·专辑 副标题);保留了 §10.11 的后台playlist_worker。 - 播放页(
ui/player.c):黑胶唱盘(fill_rect行扫构造圆盘 + 环槽减淡 + 方盖内切圆 + 四角把「圆外-方外」盖回盘色 → 封面成圆,§5.1 技巧)+ 唱针(3px 唱臂 + 端头, 播放压盘/暂停微抬 6px)+ 高光弧 + 进度条(已播红)+ prev/play/next 控制钮(play 红)+ 滚动歌词 (当前行红高亮居中,裁剪到歌词带)+ 竖/横布局可切(沿用 player_set_screen)。- 恒沉浸:播放页底 day 瓷白
#FFFDF9/ night 近黑#0C0D10,不随系统明暗整体反转;accent (进度红/play 钮/歌词高亮)日夜换红明暗(day#C20C0C/ night#EC4141,§5.4)。
- 恒沉浸:播放页底 day 瓷白
- 设置页(
ui/settings.c):4 行卡片(音质/缓存上限/退出登录/关于)+ 分组说明;退出登录用 error 色; 音质/缓存 → BottomSheet;退出 → confirm;关于 → snack。行圆角 12,语义色。 - chrome.c:
ncm_draw_emptykind 大字改品牌红;页脚 dim。
11.4 性能(§6 专项)
- 播放页分带刷新:内容拆「盘带
g_disc_r」+「动态带g_dyn_r」两个绘制物,bbox 互不重叠。player_tick只标脏变化的那一条带——进度/歌词/时间只刷动态带,盘带仅 歌曲/封面/播停态 变化才重画 (修原player_tick每 tick 刷整r_cont的隐患,§6.1/§6.2)。 - 黑胶圆盘仅 封面/状态变化 时重画;歌词行裁剪到歌词带(自定义绘制物不按 bbox 裁剪,防溢出叠控制钮)。
- 封面限幅沿用(
NCM_COVER_MAX=288KB,单张);搜索结果上限NCM_SEARCH_MAX=32(无大缓冲,§6.7)。
11.5 验证记录(宿主 + mipsel)
- 宿主离屏
MEOW_UI_DUMP双套配色各截一轮:7 页 × 2 主题 = 14 张app/out/shots/{login,home,playlist,player,lands,settings,search}_{dark,light}.png(PPM→PNG)。- 红色抽样:day
#C20C0C(组头红条)、night#EC4141均正确;播放页恒沉浸(瓷白/近黑)✅。 - 黑胶盘/唱针/封面裁圆/进度/控制/歌词居中 目检正确 ✅;主页双页签、个人卡、组头红条、红缩略 ✅。
- 红色抽样:day
test/test_ncm_core_host全 PASS(无回归);out/test_ncm_core_mipsel(qemu)全 PASS。- mipsel
build_ncm.sh自有代码-Wall -Wextra0 warning;ncm静态,text 803084 / data 824 / bss 928356。 - qemu-mipsel 冒烟
timeout 10 qemu-mipsel ./ncm:start→ipc: no launcher socket(优雅)→登录页 unikey 重试(rc=-5 无 CA,预期)→exit干净退出,无崩溃。 - 未做(诚实):真机搜索/歌单请求仅留最少验证或不打(风控约束);未真机扫码/播放出声(用户联调项)。
- 真机部署未执行:本机无可达设备主机(
meowos.local不解析;ssh 无设备别名/IP,WORK_STATE 亦标注 热点组播不通)。二进制已就绪(md590a25cf0c193fd965d6685fb099bacb7),部署命令留用户执行:scp ncm root@<device-ip>:/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm.new && ssh root@<device-ip> 'mv /usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm.new /usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm'(先 .new 再 mv)。
11.6 md5(自有源码 + 二进制)
90a25cf0c193fd965d6685fb099bacb7 ncm # mipsel 静态 App(新)
c2921983e878ce0cefe1f279f6d16d3f app/app.c 8585677b732420ead899c34d07823a05 app/ncm_app_internal.h
359ad93833d3ef1914c97da28d249d09 app/ui/login.c 970e4ec732036ae8d8de137c960863c4 app/ui/home.c
ac9e1585e96b1a1a336cbbc9a82a8c86 app/ui/playlist.c 3023fb024ea096e458b52fb4255db709 app/ui/player.c
c4f6cd8569d6ddc4dd15d4111752ed77 app/ui/settings.c ba78835b3fbe6166bbe39f2e9ea9041a app/ui/search.c
54f10365ed640130ee33fba2e674b547 app/ui/ncm_colors.c aa93822cd735f3912556df6a301a3580 app/ui/chrome.c
94d10237c1aa73f7273f82ff319792f5 app/logic/appstate.c 539252a2ea6a6a675f94356898176b99 core/ncm_api.h
1c1c05b2fada19c40e8a83a2742afbd7 core/ncm_api.c 30ab44e6f6cbc8b07eb477e7b8ee82a3 build_ncm.sh
71ed280698b67cba76a90d19a43f8458 build_ncm_host.sh 2a9663abe58609fafcc22949b8e8771c app/test/test_ncm_app.c
注:
app/logic/appstate.c本批未改(md5 未变94d10237...);build_ncm_host.sh因补链 SDKmeow_ui_nav/meow_ui_media/meow_notify_ipc而更新(宿主 SDK 源后来新增这些文件,原脚本缺链导致meow_notify_ipc_notif_open未定义;属构建脚本适配,非 SDK 改动)。
11.7 出入清单(只改 patch_v2/ncm_app/)
新增:
app/ui/ncm_colors.c # ncm_palette/g_dark/ncm_on_theme/ncm_fill_round/ncm_draw_button/first_glyph/ncm_text_fit
app/ui/search.c # 搜索页(IME + cloudsearch + 结果列表)
修改:
app/ncm_app_internal.h # ncm_pal_t/g_dark/ACT_SEARCH/搜索状态/新页声明/调色板助手声明
app/app.c # 主题侦听 + search 页建/路由
app/ui/{login,home,playlist,player,settings,chrome}.c # 重做/重着色
core/ncm_api.h/.c # is_collected(§7.0) + ncm_api_cloudsearch
app/test/test_ncm_app.c # is_collected mock + NCM_THEME 双主题 + search mock
build_ncm.sh / build_ncm_host.sh# 源列表加 search/ncm_colors;host 补链 SDK nav/media/notify_ipc
(核心库 §9 其余、vendor/、sdk/、meow_launcherd/ 未动;未 commit/push/刷机)
12. 二期四件功能批:专辑封面 / 歌词后台加载 / 大歌单分页 / UI 遗留修复(2026-08-31)
依据任务批(REPORT §9-11 之上)做四项:①
ncm_song_t加al.picUrl,播放页黑胶封面用真实专辑图 (沿用cover.c缓存/限幅);② 播放页歌词改后台 worker + 就绪标志,enter 不再同步阻塞拉词; ③ 大歌单playlist/detail全量 trackIds 太大(5191 首 ≈900KB,§10.11 已记录)→ 改走/api/playlist/track/all(eapi 服务端分页),曲目列表滚动加载(50 首/页);④ 修 UI 遗留: 歌词字面\n未转换 + 播放页底部提示豆腐块(字库缺字)。 仅改动patch_v2/ncm_app/;未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机/动设备。
12.1 专辑封面(ncm_song_t.al_pic + 播放页用真实专辑图)
core/ncm_api.h:ncm_song_t增char al_pic[512](al.picUrl,追加字段向后兼容)。core/ncm_api.c:抽出parse_songs_array()统一解析歌曲对象数组(id/name/ar[0].name/al.name/al.picUrl/dt/fee, id 数字/字符串均以文本拷贝,兼容 >2^31)。ncm_api_song_detail、ncm_api_cloudsearch改共用该函数 → 歌单/搜索两路都带上专辑封面。app/ui/player.cplayer_play_song:黑胶封面 URL 改为s->al_pic[0] ? s->al_pic : g_st.cover_url(无 al_pic 回落歌单封面);player_tick在sc != g_last_song(切歌/自动顺延)时对新曲cover_request()换封面。- 封面缓存/限幅沿用
app/logic/cover.c(≤NCM_COVER_MAX288KB、单张、后台线程下载解码 +cover_current.jpg供 launcher),未改。
12.2 歌词后台加载(player_manager.c + player.c)
- 新增持久歌词 worker 线程(
lyric_worker,detached):每 300ms 读g_play.cur_song,曲目 id 变化即lyric_clear()→ncm_api_lyric→lyric_parse()(网络+解析全在后台);状态g_lyr_state(0 空闲/1 加载中/2 已拉完)。 player_manager_load_lyric()改为非阻塞触发:只lyric_ensure_worker()启动 worker 即返回(UI 不再同步阻塞)。 换曲/自动顺延由 worker 轮询自捕获,无需逐处触发。- 新增
int lyric_state(void);player.cplr_draw_dyn歌词空态改为:lyric_state()==2 ? "no lyric" : "loading lyric..."(消除“无词”与“加载中”歧义)。 - 竞态(§10.9 FM4 已接受)沿用:UI 读 lyric 模块与 worker 写为良性(数组定长、越界已防护,最坏一帧旧/空行自愈)。
12.3 大歌单分页(/api/playlist/track/all 服务端分页 + 触底加载)
core/ncm_api.h/.c:新增ncm_api_playlist_track_all(device_id, cookie, pid, offset, limit, songs, max, &n, &total)→ eapiPOST /api/playlist/track/all,body{"id","offset","limit","n"=limit,"s":8},直返该页歌曲对象(含al.picUrl, 免去 detail 瓶颈 + 二次 song_detail)。数组路径防御songs→playlist.tracks→data.songs;total取total→count→playlist.trackCount→musicCount。 另导出ncm_api_parse_track_all(json,len,...)纯解析版(离线样本重放/单测,不打网络)。app/logic/appstate.c:新增appstate_load_playlist_songs_page(idx, offset, limit)(offset==0重头清空、>0追加到g_st.songs[offset..],封顶NCM_SONG_MAX);appstate_load_playlist_songs(idx)保留为兼容整批(内部按页循环)。app/ui/playlist.c:首屏playlist_refresh()拉 offset=0 一页(NCM_PLAYLIST_PAGE=50);playlist_tick按g_ploffset==0决定「回顶/保滚动」;新增pl_maybe_load_more()(触底且g_pl_more且未到NCM_SONG_MAX时加载下一页), 在playlist_touch_move/playlist_swipe滚动后触发;追加失败仅ncm_snack("加载失败")并保留已载歌曲(不整页清空)。app/ncm_app_internal.h:NCM_PLAYLIST_PAGE 50;声明分页函数 +lyric_state。
12.4 UI 遗留修复
- 歌词字面
\n(app/logic/lyric.c):lrc_parse复制正文改经copy_lrc_text()——把字面\n/\r(服务器把多行文本以转义折进一行)转空格,\r\n/\n\r折叠为单空格;有界 ≤LRC_MAX_TEXT。 - 播放页底部提示豆腐块(
app/ui/player.c):页脚"左右滑切歌 · 点顶部切横屏"的·(U+00B7) 不在 common 字库 → 豆腐块。root cause:common tier 按 GB2312 解码A1A4得・(U+30FB)(gb2312 无 U+00B7,gbk 才有),故 common 图集只有 U+30FB。 已替换为・(U+30FB)(在 common 图集内,meow_font_class_of→OTHER→路由主槽 slot0 即命中)。其余页面的·(U+00B7) 为同一 bug(settings/login/playlist/search 也有),为避免范围扩散 + 保设计原貌,本期只改播放页页脚(用户实测报点); 已在「诚实出入」标注。
12.5 验证记录(宿主为主;风控纪律:新增真实 NCM 请求 = 0 次)
| 项 | 方式 | 结果 |
|---|---|---|
| track_all 离线解析 | bench/offline_track_all.c(合成样本,纯解析,0 网络) |
全 PASS:songs 命中、id>2^31 不溢出(2746301825)、al.picUrl 正确(song0/song1)、total=5191;变体 playlist.tracks+trackCount 容错命中 |
歌词字面 \n |
宿主编译 app/logic/lyric.c + 定向输入(\\n/\\r\\n) |
PASS:"hello\nworld"→"hello world"、"second\r\nline\nhere"→"second line here" |
| App 三页 | 宿主离屏 MEOW_UI_DUMP(mock,0 网络) |
player/playlist/home 渲染正常(黑胶/进度/控制/歌词、分页 footer 6/128、红行),无崩溃 |
| 歌词后台状态位 | lyric_state() 语义 + mock 下 g_host_mock 短路(worker 未启、count>0 → 显示歌词) |
正常 |
| mipsel 构建 | bash build_ncm.sh(自有代码 -Wall -Wextra) |
0 warning(仅脚本 echo);text 812856 / data 840 / bss 1059556 |
| qemu-mipsel 冒烟 | 未跑(避免触发真实 unikey 请求;启动路径未改,§11.5 已验) | 未做 |
注:
out/test_ncm_core_mipsel已由build_core.sh重建(含新增test_track_all_parse),但未运行(其test_network会打真实 NCM 请求);track_all 解析已由bench/offline_track_all(0 网络)覆盖。
12.6 md5(自有源码 + 二进制)
9c4a79f669967d193cba9fdaa41f8442 ncm # mipsel 静态 App(新)
9d1ff1ad5027c1932c7f4f0163df8b92 core/ncm_api.h 6b2daef7a48215fcd7369351e09ee25f core/ncm_api.c
8067ef74af7db49340b693ec62c654f0 app/ncm_app_internal.h 7e54b48012fdfb392ba5910d22153078 app/logic/appstate.c
c3a4aa5cd3da493d5cdb75965aaaa4fe app/ui/playlist.c 610a223232d250d2e0413031bb8613ae app/play/player_manager.c
9bf68c8420bd8c819cf318b5a16d3d9f app/ui/player.c 612f6e92cade6a59d33f3b5d6aee7b13 app/logic/lyric.c
58ee655751a8efb69df850dc96afdcc1 test/test_ncm_core.c 61b019c64a805e8873db21482def3408 bench/offline_track_all.c
12.7 出入清单(只改 patch_v2/ncm_app/)
修改:
core/ncm_api.h .c # ncm_song_t.al_pic + parse_songs_array(解析 al.picUrl)
# + ncm_api_playlist_track_all(分页) + ncm_api_parse_track_all(离线解析)
app/ncm_app_internal.h # NCM_PLAYLIST_PAGE + appstate_load_playlist_songs_page + lyric_state
app/logic/appstate.c # 分页加载 appstate_load_playlist_songs_page(兼容整批保留)
app/ui/playlist.c # 触底加载更多(50/页,cap 256);追加失败保留已载
app/play/player_manager.c # 歌词持久 worker + 异步 load_lyric + lyric_state
app/ui/player.c # 黑胶封面用 al_pic;异步歌词空态(loading/no lyric);页脚 ·→・
app/logic/lyric.c # LRC 字面 \n 转空格(copy_lrc_text)
test/test_ncm_core.c # 新增 test_track_all_parse 离线断言
新增:
bench/offline_track_all.c # track_all 离线样本重放(0 网络)
REPORT.md # 本批记录(§12)
(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机/动设备)
12.8 诚实出入 / 已知限制
- 未真机验证:本机无可达设备主机(与 §11.5 同:
meowos.local不解析、无设备别名/IP),故未真机跑。 二进制已就绪(md59c4a79...),真机部署 + 验收(1-2 次请求)留用户执行:scp ncm root@<device-ip>:/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm.new && ssh root@<device-ip> 'mv ...ncm.new ...ncm'(先 .new 再 mv)。 /api/playlist/track/all端点形态未 live 验证(无宿主 cookie,且 NCM 真实 + 风控纪律不允许多请求): 请求/解析按 ncm-api-rs 生成的 eapi 路由/playlist/track/all+ API.md 的limit/offset文档语义实现, 并对数组路径(songs→playlist.tracks→data.songs)与total做了多路径容错;若真实响应字段有出入, 解析器会据实际样本在下一轮对齐(离线单测已覆盖本实现所依据的形态)。- http 封面 URL 旧疾:
core/ncm_http.c对http://也走 TLS 握手(当前实现只判 scheme 再统一 TLS), 若个别al.picUrl/coverImgUrl为纯 http 会握手失败。这是 v1 同源遗留(v1 已用 coverImgUrl), 本期未改 core(避免动已验证的 TLS 层);实测 NCM 专辑/歌单封面多为 https,可后续统一跳转 https 或加 http 明文分支。 - 页脚等
·(U+00B7) 残留:settings/login/playlist/search 的·同为 common 缺字 → 会豆腐块(用户仅报播放页, 本期修播放页);如需全面修,把全 App 的 U+00B7 换成 U+30FB 即可(同一 bug,非功能缺失)。 - topbar 图标字符潜在同病:home/player topbar 动作标签
⟳(U+21BB)/⚙(U+2699)/⌕(U+2315) 走meow_font_class_of→OTHER 类,若 common 图集无对应字形同样会豆腐块;用户未报(可能在 topbar 用了符号字库槽或未留意), 本期未动(避免改非报点区域),后续如需一并确认/替换(如改用 ASCII 或 in-atlas 字符)。 song_ids[]字段曾在 §12 被闲置(track_all直返歌曲对象);§13 已回退 detail+song_detail,重新用于 暂存playlist/detail返回的 trackIds 以驱动song_detail分批(§13 为此更新)。- 歌词竞态:UI 读 lyric 与 worker 写保留良性竞态(§10.9 FM4 同接受);真机如需彻底消除可后续给 lyric 模块加锁。
- 构建方差异:宿主
build_ncm_host.sh对cover.c有 glibc-fortify-Wformat-truncation告警(改动前即存在, 仅宿主 glibc 触发;mipsel musl 无此告警,build_ncm.sh0 warning)。
13. 真机两修:歌单详情 Load Failed 根因(track/all 端点不存在)+ 歌单列表误触(2026-08-31,已真机闭环)
现象(用户真机验收):① 登录/主页 64 歌单正常,但进任意歌单都 Load Failed——§12.3 新换的
/api/playlist/track/all分页端点从未真机验过,真机响应字段存疑(§12.8 已自标风险); ② 偶发误触:歌单列表点到非触摸坐标的行(列表触摸坐标有偏差/串行)。 本批先真机用最小请求量抓 track/all 真实响应样本、对齐结论,再修复并用真机验证。仅改patch_v2/ncm_app/, 未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区。
13.1 根因①:/api/playlist/track/all 端点根本不存在(404「接口未找到」)
按 §12.8 的“真机抓真实响应样本对齐”计划:设备侧链路(bench/probe_track_all.c,用与 App 相同的
ncm_req_build+ncm_http_request)对小歌单 18206253175(Docu/1 首)抓原始响应体。为把“端点是否存在/响应字段”
与“解析对错”解耦,探针绕过解析器直发请求,先打原始 body。三次(风控 ≤3)真实请求结果:
| 路由 | 加密 | URL | 原始响应 |
|---|---|---|---|
/api/playlist/track/all |
eapi | https://interface.music.163.com/eapi/playlist/track/all |
{"code":404,"message":"接口未找到!"} |
/api/v6/playlist/track/all |
weapi | https://music.163.com/weapi/v6/playlist/track/all |
{"code":404,"message":"接口未找到!"} |
/api/playlist/track/all |
weapi | https://music.163.com/weapi/playlist/track/all |
{"code":404,"message":"接口未找到!"} |
→ NCM 服务端没有 track/all 这条路由(eapi/weapi、v6/无v6 全 404)。三次请求 body 均为
{"code":404,"message":"接口未找到!"}(HTTP 200 但业务 code=404),故 api_post 判
NCM_ERR_HTTP(-10) → playlist_worker 置 g_load_err → 任意歌单都 Load Failed。
§12.3 把歌单详情从“验证过的 /v6/playlist/detail + song_detail(§10.11)”换成基于一条不存在的路由做“服务端分页”,
是回归根因;ncm_api_parse_track_all 的“多路径容错”救不了路由不存在——收到的是 404 业务码,不是歌曲数组。
13.2 根因②:歌单列表误触 = 拖动滚动后 release 被 SDK 判 TAP,命中“释放点+新 scroll”而非按下行
先验证“触摸坐标与显示是否几何不一致”(任务提示的方向)。设备侧 bench/probe_input.c(本地 ioctl,零 NCM 请求):
ABS_MT_POSITION_X min=0 max=480 ABS_MT_POSITION_Y min=0 max=720
fb0 xres=480 yres=720 bpp=16
→ 触摸面板与帧缓冲 1:1(无缩放/无固定偏移),故“坐标有偏差”不是面板标定/缩放问题,App 侧命中公式
(w->y + w->scroll 与绘同式)也一致——命中数学本身是对的。
真正根因是手势时序:SDK 抬手判 adx/ady < 65px(SWIPE_THRESH)为 TAP;而拖动过程中 touch_move 已
meow_listview_scroll_by(-dy) 滚动列表。于是“小拖动(0..64px)= 列表已滚 + release 仍触 TAP”,
playlist_on_tap/home_on_tap 用 meow_listview_hit(释放x, 释放y) = “释放点 + 更新后的 scroll”命中,
打到的是“已经滚动到手指下方”的那一行,与用户按下的行不一致 → “点到非触摸坐标的行”(偶发,手指微动即触发)。
(§12.5 宿主离屏 MEOW_UI_DUMP 只验静态渲染、不发真实触摸,故未暴露。)
13.3 修法
- Load Failed(
app/logic/appstate.c):appstate_load_playlist_songs_page回退 §10.11 已验证路径——/api/v6/playlist/detail(n=1,取前NCM_SONG_MAX首 trackIds)+/api/v3/song/detail分批取歌曲对象 (parse_songs_array仍带出al.picUrl,保留 §12.1 专辑封面)。offset==0重头整批载入(idn≤NCM_SONG_MAX);offset>0(触底“加页”)只要NCM_OK无操作——detail 一次已取满至上限,无独立分页。 - 误触(
app/ui/home.c与app/ui/playlist.c):点行改用按下行g_press_row(touch_down 命中行, 与“红条按压”同源);手指移动量g_press_delta > 10(与按压红条清行同阈值)判为滚动收尾,不触发点行。 干净点击仍播放按下行(更符合“按哪行播哪行”);拖动手势只滚动不再误播。search.c同款模式, 非本次报点,未改(避免扩范围,见 §13.8)。 - 仅在
patch_v2/ncm_app/;app//core/均未动其它逻辑;ncm_api_playlist_track_all/ncm_api_parse_track_all保留在 core(现为不可达死代码;offline_track_all/test_track_all_parse仍可作离线回归)。
13.4 真机验证记录
触摸=fb(probe_input,本地 ioctl):ABS_MT[0,480]×[0,720],fb0 480×720 16bpp → 1:1,排除标定。
修复后数据链路(bench/probe_fix.c,复刻修复后 appstate_load_playlist_songs_page(offset=0) 的
detail→song_detail 完整路径;device_id=BD6DE5…, cookie_len=1842):
=== small (Docu/1, 18206253175) ===
detail rc=0 elapsed_ms=1175 idn=1 ids[0]=2048962657
song off=0 batch=1 rc=0 elapsed_ms=717 bn=1
RESULT song_count=1
[0] Verve (feat. FJØRA) / Ryan Taubert / Verve (feat. FJØRA)
FIX VERIFY OK
=== big (我喜欢的音乐, 581857874, 5191) ===
detail rc=0 elapsed_ms=34672 idn=256 ids[0]=36190432
song off=0/64/128/192 batch=64 rc=0 bn=64 ×4
RESULT song_count=256
[0] Isla Blanca (Aerosoul Chillout Remix) / Aerosoul / Isla Blanca
FIX VERIFY OK
→ 小歌单 1 首、大歌单 256 首(NCM_SONG_MAX 截断,§10.9 上限)真机都出曲目,不再 Load Failed。
大歌单 detail 真机 ~34.7s(5191 首 trackIds ≈900KB),但由 playlist_worker 后台跑、UI 显示 LOADING 不冻结(§10.11 P1 已修)。
部署二进制冒烟(ncm.new→mv,起 8s 再 SIGTERM):
start → home: playlists rc=0 count=120 uid=408996274 → exit
→ 新二进制启动正常、主页加载 120 歌单、干净退出,无崩溃。
请求量:track/all 诊断恰 3 次(风控 ≤3);修复后验证走 detail+song_detail(验收必需)。 真实请求总计数:诊断 3(track/all)+ 复验 7(small detail/song 2 + big detail/4×song 5)+ 冒烟 1(user/playlist)= ≈11 次, 均为必要开销。另有一次无效运行(data_dir 误传致 TLS 失败),未发服务器请求。
13.5 真机部署
/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm(先 ncm.new 再 mv;ncm.bak2=§12 旧回归二进制,
ncm.bak=§10.11 前)。新 md5 734221661ce547393c03ae3568ca3f36(旧 §12 9c4a79f669967d193cba9fdaa41f8442)。
13.6 出入清单(只改 patch_v2/ncm_app/)
修改:
app/logic/appstate.c # track_all 404 回归:appstate_load_playlist_songs_page 回退 detail+song_detail
app/ui/playlist.c # 误触:点行用按下行 g_press_row + 拖动(g_press_delta>10)抑制
app/ui/home.c # 误触(同 playlist.c):点行用按下行 + 拖动抑制
新增:
bench/probe_track_all.c # track_all 原始响应探针(诊断 404;抓样本用)
bench/probe_input.c # 触摸 ABS 范围 + fb0 分辨率探针(本地 ioctl,零 NCM)
bench/probe_fix.c # 修复后数据链路真机复验(detail→song_detail)
REPORT.md # 本批记录(§13)
(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区)
13.7 md5(本批改动文件 + 产物)
734221661ce547393c03ae3568ca3f36 ncm # mipsel 静态 App(新;已部署设备)
453eeb5d29809854061d8e4e0300855a app/logic/appstate.c
2ce92315d0e27492b36374f512fabaeb app/ui/playlist.c
4217763fbf766c77917f86f63abb634c app/ui/home.c
279ebbcfca65f9225b93b04213f8fde4 bench/probe_track_all.c 7dc88e448fb10a6da24ad48511023307 bench/probe_input.c
5ad3107059bca1f8e14776afdf6abf82 bench/probe_fix.c
13.8 诚实出入 / 已知限制
track/all不存在使 §12.3 的整体翻页目标落空:/api/playlist/track/all(eapi/weapi、v6/无v6)真机全 404, 故“服务端分页 + 第 256 首之后继续加载”无法实现。本期回退为 §10.11 的 detail(全量 trackIds,浏览器首屏 ~34.7s, UI 后台 LOADING 不冻结)+NCM_SONG_MAX=256截断。若仍要超过 256 首滚动加载,只能改用 detail 已返回的全量 trackIds 驱动 song_detail 分页(不依赖track/all),列入后续;本期只恢复到“能出曲目”的可靠基线。ncm_api_playlist_track_all/ncm_api_parse_track_all现为不可达死代码(服务端无路由),保留以不破坏offline_track_all.c/test_track_all_parse(离线样本回归,0 网络)。后续可连同 §12.3 一并清理。- 误触修复未真机触摸复现:无法经 SSH 驱动真实触摸;修复基于「按下行 + 拖动抑制」的明确逻辑 + 触摸=fb 1:1 的 实测佐证(排除标定)。建议用户真机复验:快速横滑列表后抬手,应只滚动不误播;干净点行仍播按下行。
search.c列表点行同款误触模式(未报点,本期未改,避免扩范围):search_on_tap仍用meow_listview_hit(释放点); 如需一并修,套用 §13.3 同名改法即可。- 大歌单首屏 ~34.7s:detail 全量 trackIds(≈900KB)在 MIPS32 上解析慢(§10.11 已记录,宿主 4.7s,~7×);后台 worker 避免冻 UI。已恢复到 §10.11 用户接受的基线。
al.picUrl封面保留:song_detail 经parse_songs_array解析出al.picUrl,§12.1 黑胶真专辑封面功能不退。- 构建:
build_ncm.shmipsel 自有代码-Wall -Wextra0 warning(本批改动文件逐文件复核无告警);vendor 豁免。 - 未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区;部署留用户后续验收。
14. 三期第一批:歌单真分页 + 专辑封面 ncm_cache 化 + 歌词后台加载补锁(2026-08-31,agent-239)
范围:仅
patch_v2/ncm_app/(含 build 脚本与 REPORT 本文件)。未动sdk/(主会话在改meow_ui_ime.c)、meow_launcherd/、vendor/、core/。风控纪律:本批真实 NCM 请求 0 次(全部离线 stub/宿主导出/已落盘样本)。 部署/验收留用户(真机验收点见 §14.7)。
14.1 歌单分页(detail 全量 trackIds + song_detail 分批,取消 256 硬截断,分页驱动)
- 背景:§13.1 真机确认
/api/playlist/track/all(eapi/weapi、v6/无v6)全 404,§12.3 的“服务端分页”不可行; §13 回退到 detail(n=1)+ song_detail 但NCM_SONG_MAX=256硬截断、一次拉满(首屏 34.7s)。本期把 “256 截断 + 一次拉满”改为“detail 全量 trackIds 驱动 + song_detail 分批 + 触底逐页加载”。 app/logic/appstate.cappstate_load_playlist_songs_page(idx,offset,limit)重写:offset==0(重头):detail(n=1) 取全量 trackIds 入song_ids[](≤NCM_SONG_MAX),记track_ids_count;song_count/songs[]清空;只载第 0 页(song_detail 前limit首,其余待触底再拉,不冻 UI、不一次拉满)。offset>0(触底追加):从song_ids[offset..]载 song_detail,追加写songs[offset..],song_count = offset + 本页实际数;失败返回不修改 song_count(保留已载页)。offset>=track_ids_count或offset>=NCM_SONG_MAX时无操作返回。track_ids_count为“是否还有下一页”唯一判据(song_count < track_ids_count)。
app/ui/playlist.c:playlist_worker按页(NCM_SONG_BATCH)调appstate_call;g_pl_more = (rc==NCM_OK) && song_count<track_ids_count && song_count<NCM_SONG_MAX;pl_maybe_load_more()视口触底 →g_ploffset = g_st.song_count起下一页(append 成功保留滚动位置防跳顶);首屏失败整页空态 + 点按重试; 追加失败 snack + 保留已载歌曲 + 重置g_pl_more(可再触底/点击重试)。- 加载/失败/空态三态:
LOADING 正在加载曲目.../LOAD FAILED·SESSION EXPIRED·NETWORK ERROR(点按重试)/NO TRACKS。 点按重试:playlist_on_tap在g_load_err!=0时命中内容区触发playlist_refresh()。 - 上限:
NCM_SONG_MAX256→1000(单页渲染上限 = 分页驱动;每首ncm_song_t≈1.32KB → 歌曲数组 ~1.37MB, bss +~1MB,14MB 内安全);NCM_SONG_BATCH64→100(≤500 安全窗内取保守 100;clist[4096]装 100 首 ≈2KB, 如需 200 首需先扩 clist);删除NCM_PLAYLIST_PAGE(track/all 遗留,已无引用)。footer 显示已载/总数(总数取playlists[..].track_count,如 5191 歌单读到 1000 时显示1000/5191)。
14.2 专辑封面(黑胶用真实专辑图 + 缓存进 ncm_cache LRU + 修一个真死锁)
ncm_song_t.al_pic+parse_songs_array解析al.picUrl(§12.1 已落地,本期未改 core);player 黑胶盘封面取自s->al_pic[0] ? s->al_pic : g_st.cover_url(§12.1),歌单列表/详情头块封面保持现状(cover_get(),未改)。app/logic/cover.c封面图改走 ncm_cache(LRU 口径沿用 ncm_cache):键 = djb2(封面 URL) → 文件song_cov<hex>.jpg(与 mp3song_<id>.mp3同池,ncm_cache_lookup/commit刷新 mtime=LRU)。 worker 流程:① 缓存命中直接解码(免下载、二次进入离线可看);② 未命中 → 下载解码 →ncm_cache_open/write/commit落盘;仍写cover_current.jpg(save_cover_raw)供 launcher 媒体卡片。- 顺带修真 bug:旧
save_cover_raw在已持g_cm的临界区内再pthread_mutex_lock(&g_cm)(非递归锁自锁死)→ cover worker 永久卡死、g_gen永不 +1 → 黑胶封面永不刷新。改为“调用方持有g_cm”(不再二次取锁); 封面写盘缓冲扩到NCM_CACHE_DIR_MAX+32(顺带消宿主 glibc fortify-Wformat-truncation告警,mipsel musl 本就无)。
14.3 歌词后台加载(确认已异步;补数据竞争锁)
- 歌词后台加载 §11(
player_manager.clyric_worker:轮询当前曲 → 异步ncm_api_lyric→lyric_parse, UI 不阻塞;lyric_state()判「加载中/无词」)已落地,本批未改后台线程模型(确实不阻塞 UI)。 - 补
app/logic/lyric.c数据竞争锁g_lyric_mu:写侧lyric_parse整段持锁(清空用lyric_clear_nolock避免二次取锁自锁死);读侧lyric_count/lyric_line_at/lyric_line_text/lyric_line_tl每次取锁。 消除 UI 线程(player.c 歌词区)与 IPC 线程(ipc_media.clyric_count)同 lyric worker 间的数据竞争 (早前为“良性竞态”§10.9 FM4;本期按“禁止数据竞争”要求收紧)。 - 与现有
g_play.mu同模式(函数级锁、无嵌套)。已知残余:worker 读g_st.songs[idx].id无锁——songs[]写读竞态(§10.9 FM4 接受,与播放线程同源),本期未扩范围。
14.4 验证记录(0 真实 NCM 请求)
| 项 | 方式 | 结果 |
|---|---|---|
| 歌单分页状态机 | bench/pagination_sim.c 离线 stub(0 网络)驱动真实 appstate_load_playlist_songs_page |
全 PASS:130 首读全程(1 detail + 2 song_detail 页,song_count=130/track_ids_count=130,末首含 al_pic);5191 首封顶 NCM_SONG_MAX(1000,10 页,越界无操作);空歌单 0;越过尾部/上限再 load 均无副作用 |
| mipsel 构建 | bash build_ncm.sh(自有代码 -Wall -Wextra) |
0 warning;text 842820 / data 856 / bss 2052068(bss +~1MB 来自 NCM_SONG_MAX 1000) |
| 宿主离屏渲染 | build_ncm_host.sh + MEOW_UI_DUMP(mock,0 网络) |
home/playlist/player/lands 渲染正常(歌单 6 行 + footer 6/128、黑胶 + 歌词当前行红色高亮),无崩溃 |
| 宿主链接 | build_ncm_host.sh |
补 meow_pinyin.o(主会话 229 拼音 IME 引入 py_predict,旧 host SDK_SRC 漏编 → 链接断);cover.c 截断告警已消 |
14.5 md5(本批改动文件 + 产物)
b34bc1ed2c3539f1ca80a005884833cd ncm # mipsel 静态 App(新)
1a2327e0ccd352d11bea88ebd35df549 ncm_host # 宿主离屏调试二进制
0b0a4dd5e208d7b5eb504befb374a29e app/logic/appstate.c
6d66eed99c556e301498c5823b43c294 app/logic/cover.c
ace0def0e8c3ec991013b0675957f269 app/logic/lyric.c
1a1871032c2ecae7a1cc80c7aa00381a app/ncm_app_internal.h
ab87bd5f74b82fd3d62c49a16a4c9fe5 app/ui/playlist.c
c73366373b3a372d96b2218e528c6381 build_ncm_host.sh
141e9098a7cabeade9aa6b442ba20910 bench/pagination_sim.c
14.6 出入清单(只改 patch_v2/ncm_app/)
修改:
app/logic/appstate.c # 歌单分页:offset=0 重建 trackIds 索引+载第0页;offset>0 追加;track_ids_count
app/ui/playlist.c # 触底逐页加载 + g_pl_more 判据 + 加载/失败/空态 + 点按重试 + 追加失败保留已载
app/ncm_app_internal.h # NCM_SONG_MAX 1000 / NCM_SONG_BATCH 100 / 删 NCM_PLAYLIST_PAGE + track_ids_count
app/logic/cover.c # 封面走 ncm_cache(LRU) + 修 save_cover_raw 自锁死 + 缓冲扩容消截断告警
app/logic/lyric.c # 加 g_lyric_mu(写 parse 整段持锁、读逐次持锁)消除数据竞争
build_ncm_host.sh # host SDK_SRC 补 meow_pinyin(拼音 IME py_predict 漏编→链接断)
ncm / ncm_host # 重编产物
新增:
bench/pagination_sim.c # 歌单分页离线状态机回归(stub 网络端点,0 请求)
REPORT.md # 本批记录(§14)
(未动 sdk/、meow_launcherd/、vendor/、core/;未 commit/push/刷机;未动设备系统/恢复区)
14.7 诚实出入 / 已知限制
- 真机未部署/未验(风控:0 新增真实请求)。部署链路同 §13.5(
scp ncm root@<ip>:/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm.new→ssh root@<ip> '. /etc/profile; mv ...ncm.new ...ncm',先.new再mv防 ETXTBSY)。真机验收点: ① 小歌单全程可读;大歌单(如 5191)下滑触底逐页加载到 1000 封顶(footer已载/总数),不冻 UI; ② 黑胶显示真实专辑封面(首次下载→ncm_cache 落盘,二次进入免下载;第三方封面http://若握手失败回落歌单封面, 为 §12.8 既有“http 封面走 TLS”旧疾,本批未改 core 规避);③ 歌词后台加载不卡 UI、切歌刷新、下/上一首顺延。 - 大歌单仍有内存上限(NCM_SONG_MAX=1000):这是 14MB 硬约束下的取舍(每 +100 首 ≈ +132KB)。
若要全量 5000+ 首,需:a) 歌曲对象瘦身(
al_pic[512]每首占大头,可拆到按需加载),或 b) 列表滑动窗口 (meow_listview需 O(1)src2(idx)重映射,复杂度高)。footer「已载/总数」已向用户标明进度。 ncm_cache_evict生产路径未显式触发超限淘汰(§10.9 既有遗留:evict 仅 test 调用;song/cover commit 后不自动 触发)。封面走 ncm_cache 同池后同样受该路径约束——超限淘汰非本批引入,本批也未扩大行为(不主动加 evict 调用, 避免改已验证的缓存写入路径)。- 歌词锁残余:读侧函数取锁后返回存量指针(
s_txt/s_tl静态数组),解锁后可能被下一次lyric_parse覆盖; 写入仅发生在切歌瞬间(一次/歌),本地读在一两帧内完成,峰值可能显示一帧旧/空行(与 §10.9 FM4 同接受)。 严格消除需调用方持锁拷贝(本批未做,接口需改,避免扩大 diff)。 - host 构建有 SDK
meow_ui_vol.c「system 返回值忽略」告警(SDK 源,非本 App;mipsel musl 无)。本 App 自有代码 0 warning。 - 分页触底加载与误触/点按为逻辑推断(无法经 SSH 驱动真实触摸/滚动);误触修复(§13.3)未在分页下回归真机。 建议真机快速下滑到底验证逐页加载、点按重试、追加失败保留已载。
14.8 数值权衡(供后续微调)
| 常量 | 旧值 | 新值 | 说明 |
|---|---|---|---|
NCM_SONG_MAX |
256 | 1000 | 单页渲染上限(分页驱动),歌曲数组 ~1.32KB/首 → +100 ≈ +132KB bss |
NCM_SONG_BATCH |
64 | 100 | song_detail 每批(≤500 安全窗内保守 100;clist[4096] 装 100 首 ≈2KB) |
NCM_PLAYLIST_PAGE |
50 | 删除 | track/all 服务端分页遗留(该端点 404),改由 NCM_SONG_BATCH 承担页粒度 |
g_st.track_ids_count |
— | 新增 | detail 取回 trackIds 总数,驱动「还有下一页」判据 |