三(按 NCMAPI_NOTES §2.1/§4/§6):
① 接口级响应缓存(对齐参考 apicache 2 分钟,防风控高频 ip 错误):
- 新增 core/ncm_apicache.{c,h}——内存 4 槽 LRU(单槽≤128KB,TTL 120s,
键=md5(path+\x01+json))+ 可选盘缓存 <data_dir>/ncm_api_cache/.json
(8B 墙钟头+body,超 TTL miss 并删,30s 节流清过期)。自包含模块
(仅 libc+pthread+clock_gettime),可独立链接无符号冲突。
- ncm_api.c:api_post 拆「请求+解析」两段,新增 api_post_cached;四个只读
接口接线:user_playlist/playlist_detail/song_detail/cloudsearch。
song_url/lyric/vip 不入缓存。
- 手动刷新绕过:ncm_api_cache_refresh_path(path) 一次性标记;home/歌单
topbar ↻ 接入;playlist_open/search 不标记(走缓存)。
- 盘层挂载 appstate_init;登出 appstate_logout 全清(防串账号)。
② 游客 MUSIC_A:ncm_req.c weapi+eapi 无 MUSIC_U 且非 login 路径补
MUSIC_A=anonymous_token(对齐参考 cookie 伪装)。
③ track/all 死代码清理:移除 ncm_api_playlist_track_all(真实服务器 8 形态全
404,无调用点);ncm_api_parse_track_all 保留(离线重放/单测),注释标注
「已验证不存在,勿用于真实请求」。
验证:新 bench/apicache_sim.c(0 网络)①TTL 内不重复请求/②过期重拉/③手动
刷新绕过/④cloudsearch 按键区分/⑤清空重拉/⑥盘缓存写入+clear 全 PASS;stream/
player_flow/window/search/cache_evict(1-9)/pagination/playlist_race sim 全
PASS;test_ncm_core host+mipsel(qemu) PASS;build_ncm.sh -Wall -Wextra 0
warning(text 1026914/data 1664/bss 1363460);0 高频真实请求。新 ncm md5
a69c93538920eba9d42a2ebb42f8fdca(留主会话上卡)。
3251 lines
269 KiB
Markdown
3251 lines
269 KiB
Markdown
# 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 请求,频率低)。
|
||
|
||
## 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——已与 Python `pow(m,e,n)` 逐字节一致(见 9.4)。
|
||
- **weapi**:双层 AES-128-CBC(`params` base64,含 `+/=` 需 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 -Wextra` **0 warning**;vendor 警告豁免。mipsel 用 `.tools/zig-mipsel-cc` 静态交叉。
|
||
|
||
### 9.4 验证记录(宿主 + qemu-mipsel 双向实测,响应已脱敏/截断)
|
||
- **unikey 实拿(eapi)**:`{"code":200,"unikey":"<uuid>"}`(宿主 `dc5bd090-…`,qemu-mipsel `ef9a5db2-…`);二维码 URL `https://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`,与 Python `pow(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` 加密→解密完全一致(含一次 mbedtls `crypt_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 真机待验证项
|
||
|
||
1. **ALSA 直开 `hw:0,0`**:本层用内核 UAPI(`/dev/snd/pcmC0D0p` + ioctl hw_params:RW_Interleaved/S16_LE/44100/2ch)——**设备时序/权限/采样率匹配**待真机验证;宿主已回退 FILE sink 证解码链路。
|
||
2. **真实扫码登录(803)**:set-cookie 捕获→加密落盘→`login_status` 校验链路按文档实现但**未真扫**;建议真机扫码验证一次 `MUSIC_U` 内容与 home 拉歌单闭环。
|
||
3. **媒体会话**:需 launcher 在真机跑(本 App 首真实媒体源);验证通知中心媒体卡片/Now Playing 显示 MSG_META + 下发 MSG_PLAYCTL 生效(本 App 已记 `ipc: no launcher socket` 为优雅降级,真机有 socket 即上报)。
|
||
4. **大字库**: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 分钟不超时」的关键一把。
|
||
|
||
#### 根因(按“致卡死”权重排序)
|
||
|
||
1. **超时/截止用 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/
|
||
> 落盘时间戳这类“取当前墙钟”的场合。
|
||
|
||
2. **响应头解析“隔行跳行”,漏掉 `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)。
|
||
|
||
3. **请求头缓冲 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)。
|
||
|
||
4. **weapi 发送路径把长 cookie 值截断(MUSIC_U 只剩 191 字节)→ 服务端识别为游客(profile:null)。**
|
||
`ncm_req.c` cookie 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_full` 4KB。
|
||
|
||
5. **响应头/体切分顺序 bug(我修 `split` 时引入后已修正)**:在 `split` 处 NUL 终结头部会覆盖 body 首字节,
|
||
导致 body 首字节变 NUL → JSON 解析失败(`-9`)。已改为**先送 body 再 NUL 终结头部**(二者同址)。
|
||
|
||
6. **`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):
|
||
```
|
||
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
|
||
```
|
||
即 **home 真实列出用户 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`),md5 `bf6d2a79504d49cdc9b87e6f34cc6aec`。
|
||
|
||
#### 出入 / 已知限制(本批)
|
||
|
||
- **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.c` `home_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}` + 响应 cap `NCM_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`:`n` 100000→**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.h` `song_ids[]` long→int64_t。
|
||
- **P3b** `core/ncm_req.c` eapi 局部缓冲 `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.sh` mipsel `-Wall -Wextra` 0 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`),**新 md5 `f6bd765a374effaf866784c3165e476e`**(旧 `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)。
|
||
- **设置页**(`ui/settings.c`):4 行卡片(音质/缓存上限/退出登录/关于)+ 分组说明;退出登录用 error 色;
|
||
音质/缓存 → BottomSheet;退出 → confirm;关于 → snack。行圆角 12,语义色。
|
||
- **chrome.c**:`ncm_draw_empty` kind 大字改品牌红;页脚 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` 均正确;播放页恒沉浸(瓷白/近黑)✅。
|
||
- 黑胶盘/唱针/封面裁圆/进度/控制/歌词居中 目检正确 ✅;主页双页签、个人卡、组头红条、红缩略 ✅。
|
||
- **`test/test_ncm_core_host` 全 PASS**(无回归);**`out/test_ncm_core_mipsel`(qemu)全 PASS**。
|
||
- **mipsel `build_ncm.sh` 自有代码 `-Wall -Wextra` 0 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 亦标注
|
||
热点组播不通)。二进制已就绪(md5 `90a25cf0c193fd965d6685fb099bacb7`),部署命令留用户执行:
|
||
`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` 因补链 SDK
|
||
> `meow_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.c` `player_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_MAX` 288KB、单张、后台线程下载解码 + `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.c` `plr_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)`**
|
||
→ eapi `POST /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),故未真机跑。
|
||
二进制已就绪(md5 `9c4a79...`),真机部署 + 验收(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.sh` 0 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.sh` mipsel 自有代码 `-Wall -Wextra` 0 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.c` `appstate_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_MAX` 256→1000**(单页渲染上限 = 分页驱动;每首 `ncm_song_t` ≈1.32KB → 歌曲数组 ~1.37MB,
|
||
bss +~1MB,14MB 内安全);`NCM_SONG_BATCH` 64→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`(与 mp3 `song_<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.c` `lyric_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.c `lyric_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 总数,驱动「还有下一页」判据 |
|
||
|
||
---
|
||
|
||
## 15. 三期第二批:中文搜索接入拼音 IME + 设置页做好 + 缓存淘汰接线(2026-08-31,agent-241)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`(含 `core/`)。未动 `sdk/`(主会话在改 `meow_ui_theme.c`/测试,另有 agent 在改
|
||
> `meow_launcherd/` 主题)与 `meow_launcherd/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/宿主 mock/
|
||
> 已落盘样本)。部署/验收留用户(真机验收点见 §15.7)。上一批(歌单分页/封面/歌词锁)已 commit `ee104f1`,
|
||
> 本批在其上做;提交见 git。
|
||
|
||
### 15.1 中文搜索接入拼音 IME
|
||
|
||
- 搜索页(`app/ui/search.c`)输入框 `meow_textfield_create(..., MEOW_IME_ASCII)` 改 **`MEOW_IME_PINYIN`**。
|
||
SDK `MEOW_IME_PINYIN`(M2.88 agent-229 落地)打开后为 4 行字母键 + 组合条 + 候选栏(≤6 候选/页,翻页 +
|
||
「清」+「拼/英」键),候选上屏走 `py_commit` UTF-8 写进字段并触发 `on_change`(TextField 的
|
||
`textfield_change_cb` 实时回写 `g_st.search_query` + `invalidate`)。
|
||
- **候选上屏/清空/退格语义**均沿用 SDK IME(`py_commit`/`py_clear`/`py_back_combo`+`py_back_field`,组合态
|
||
UTF-8 安全退格 = 删末字;组合非空时退格删组合末字母),App 侧零改动。
|
||
- **英文输入零回归**:拼音键盘含「拼/英」键(`K_SWITCH`→ASCII)与 ASCII 键盘顶条「中」热区(→拼音),
|
||
ASCII/NUM 不随本批改动;搜索仍只在用户点「搜索」按钮时发 `cloudsearch`(风控:进页不自动搜,不逐键发)。
|
||
- 保留 `search_submit`/`search_worker`(后台线程 cloudsearch,UI 不卡);`ncm_api_cloudsearch` 的 keyword
|
||
为 UTF-8 字符串,中文关键词直接进 weapi 加密体(无需改造)。
|
||
|
||
### 15.2 设置页做好(用户定案:音质/缓存上限可设数字+清理缓存+关于)
|
||
|
||
`app/ui/settings.c` 重做为 **5 行**:音质 / 缓存上限 / 清理缓存 / 退出登录 / 关于网易云。
|
||
|
||
- **音质**:sheet 列出全 5 档(Hi-Res/Lossless/Exhigh/Higher/Standard,默认 Hi-Res)。单曲不可用时
|
||
**沿用 core 既有降级链**(`ncm_api_song_url_auto`:hires→lossless→exhigh→higher→standard,`ncm_api.c:260`),
|
||
本批未改 core 降级逻辑。
|
||
- **缓存上限**:sheet 预设(1/2/5/10/20/30/50/100 GB)+「自定义...」→ INPUT 弹窗 + **NUM IME 数字输入**
|
||
(`input_cache_limit_gb()`,atoi + 钳制 1~100)。默认 10GB,上限 100GB。持久化走既有
|
||
`appstate_save_settings`(`meow_config_*` → `<env MEOW_DATA_DIR>/ncm.cache_limit_gb.cfg`,即
|
||
`MeowData/dev.player.ncm/data/`,与 core `ncm_cache_load_setting`/`ncm_store_read_cfg_int` 同目录同键)。
|
||
- **清理缓存(新)**:显示当前占用(`ncm_cache_total_bytes`,进页时算一次存 `g_cache_used`,清理后清零);
|
||
tap → CONFIRM 弹窗 → `ncm_cache_clear`(新 core API)→ snack「缓存已清理」。占用用整数运算格式化
|
||
(`fmt_bytes`,GB/MB/KB,不用浮点/软浮点)。
|
||
- **退出登录 / 关于/版本行**:保留;关于 tap 改为 snack「网易云音乐 v0.1 · MeowOS 移植版」。
|
||
|
||
### 15.3 缓存淘汰接线(ncm_cache_evict 接进生产路径)
|
||
|
||
- **core 新增 `ncm_cache_clear`**(`core/ncm_cache.c/.h`):删除 `song_*` 全部(mp3 与封面同池,含残留
|
||
`.tmp`),供「清理缓存」与测试用;`ncm_cache_evict`(既有)保持 LRU=按 mtime 升序删最旧直到 `<= limit`。
|
||
- **mp3 下载路径**(`app/play/player_manager.c song_local_file`):**写前**(`ncm_cache_open` 前)与**写后**
|
||
(`ncm_cache_commit` 后)各调一次 `ncm_cache_evict(&g_st.ctx.cache)`——写前腾空间(此时本线程 temp 尚未建,
|
||
无在途文件被误删风险),写后清「新文件推过上限」的增量。刚写的文件 mtime 最新,LRU 淘汰时存活。
|
||
- **封面路径**(`app/logic/cover.c` cover_worker 未命中写盘分支):写前 + 写后同样各调一次 `ncm_cache_evict`
|
||
(封面 `song_cov<hash>..jpg` 与 mp3 同池)。
|
||
- **手动调低上限**:`appstate_save_settings` 末尾加一次 `ncm_cache_evict`,用户从 50GB 降到 1GB 立即淘汰。
|
||
- 上限读设置值:`ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb)`(`player_manager_set_quality`
|
||
与 `appstate_save_settings` 均已同步,继 `appstate_load_settings`/`ncm_cache_load_setting`)。
|
||
|
||
### 15.4 验证记录(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 缓存 LRU 淘汰/清空 | `bench/cache_evict_sim.c` 离线(0 网络,稀疏文件构造,直接驱动 core) | **全 PASS**:①超上限删最旧(700+300+400MB→删 700 停于 700MB)②删 2 个最旧到 ≤1GB(4×500MB→留 2)③未超上限无操作 ④`lookup` 刷新 mtime → 被访问的 A 存活、B/C 被淘汰 ⑤`set_limit_gb` 钳制(0→1、200→100)⑥`clear` 删 mp3+封面+残留 .tmp(total=0)|
|
||
| 歌单分页回归 | `pagination_sim`(0 网络) | **全 PASS**(130/5191/空 三态无回归;`appstate.c` 加 `ncm_cache_evict` 未破坏分页——`--gc-sections` 丢弃不可达的 `save_settings`,链接不受影响) |
|
||
| core 宿主测试 | `test_ncm_core_host`(重建 core-host) | **PASS** |
|
||
| core mipsel 测试 | `qemu-mipsel ./out/test_ncm_core_mipsel` | **PASS** |
|
||
| mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 844640 / data 856 / bss 2052068` |
|
||
| 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | settings(明/暗 5 行 + 占用 + 版本 + 品牌红脚注)/search(输入框+搜索钮+footer)/home/playlist/player 均正常渲染,无崩溃 |
|
||
|
||
### 15.5 md5(本批改动文件 + 产物)
|
||
|
||
```
|
||
8a313d27dc7d05fa3a78ce7772436b04 ncm # mipsel 静态 App(新)
|
||
dc66bdcfb29ba5717b42c72ecbf45049 ncm_host # 宿主离屏调试二进制(重编)
|
||
be3d906edce4d90ebd9c09d3b7faf814 core/ncm_cache.c # 新增 ncm_cache_clear
|
||
f5810d23709080f4fa39b2fbc242c9e0 core/ncm_cache.h
|
||
41ae3f40014f68f7855fed3a0be1558c app/ui/settings.c # 设置页 5 行 + 自定义数字 + 清理缓存
|
||
9811af8f7150b325225e067748ca50a3 app/ui/search.c # 输入框 MEOW_IME_PINYIN
|
||
2350622237e9fb5d05a5497dd8b87e39 app/play/player_manager.c # mp3 写前/写后淘汰
|
||
fb1a74a68950b26a2a4f42281725f37d app/logic/cover.c # 封面写前/写后淘汰
|
||
a51b6d8477bde5692089caf511eafca1 app/logic/appstate.c # save_settings 末触发淘汰
|
||
ed2f71c52563e812a04fb8a7cc05148d app/test/test_ncm_app.c # settings mock 初始化缓存目录
|
||
2134ad23a6ee4d1b37120ee8d2e44337 bench/cache_evict_sim.c # 新增 LRU/清空离线回归
|
||
```
|
||
|
||
### 15.6 出入清单(只改 `patch_v2/ncm_app/`)
|
||
|
||
```
|
||
修改:
|
||
app/ui/settings.c # 设置页 5 行:音质(全5档)/缓存上限(预设+自定义数字)/清理缓存(显示占用)/退出/关于
|
||
app/ui/search.c # 搜索输入框 MEOW_IME_ASCII → MEOW_IME_PINYIN(中文搜索,英文零回归)
|
||
app/play/player_manager.c # song_local_file 写前/写后 ncm_cache_evict
|
||
app/logic/cover.c # cover_worker 未命中写盘分支 写前/写后 ncm_cache_evict
|
||
app/logic/appstate.c # appstate_save_settings 末尾触发 ncm_cache_evict(手动调低上限立即淘汰)
|
||
core/ncm_cache.c/.h # 新增 ncm_cache_clear(删 song_* 全部)
|
||
app/test/test_ncm_app.c # settings mock 初始化 ctx.cache(「占用」行读真实目录)
|
||
ncm / ncm_host / test/test_ncm_core_host # 重编产物
|
||
新增:
|
||
bench/cache_evict_sim.c # 缓存 LRU 淘汰/清空 离线回归(0 网络)
|
||
REPORT.md # 本批记录(§15)
|
||
(未动 sdk/、meow_launcherd/、vendor/;未动设备系统/恢复区;未 killall hiby_player / 未重启设备)
|
||
```
|
||
|
||
### 15.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'`)。真机验收点:
|
||
① 搜索页输入拼音→候选上屏中文→点「搜索」出中文结果(键盘「拼/英」「中」「退格」「清」语义);
|
||
② 设置页缓存上限「自定义」数字键盘输入 1~100、改上限后立即 LRU 淘汰;③ 清理缓存显示真实占用、(超上限
|
||
磁盘占用下降);④ 音质默认 Hi-Res、VIP/版权曲逐档降级播放。
|
||
- **`song_<id>..mp3` 双点命名是 core 既有约定**(`ncm_cache_path` 用 `"song_%s.%s"` + ext 带前导点 `.mp3`)
|
||
——本批未改(改动它需动 core 缓存写入路径,风险大于收益);`cache_evict_sim.c` 已按此约定构造文件,
|
||
验证的正是真实生产路径。
|
||
- **跨线程淘汰的极小竞态**:cover 线程写盘时,player 线程的下载 temp(`song_<id>..mp3.tmp`)也匹配
|
||
`song_` 前缀;但 in-progress .tmp 的 mtime 是「刚写」=最新,LRU 升序下排最末,正常淘汰只删最旧,
|
||
不会先删它。仅在「池严重超上限、删净旧文件后仍超」的极端情形才可能触及其(上限 ≥1GB,现实中几乎不
|
||
发生);沿用既有「双线程同池无锁」模型(本批未扩范围,未引入新锁)。
|
||
- **`ncm_cache_evict` 上限读 `&g_st.ctx.cache`**:宿主 settings mock 未 `ncm_ctx_init`(只 init cache dir),
|
||
`ctx.cache.dir` 为空 → 占用行显示 0(渲染验证用);真机 `appstate_init`→`ncm_ctx_init` 后为真实目录。
|
||
|
||
### 15.8 数值权衡(供后续微调)
|
||
|
||
| 常量 | 旧值 | 新值 | 说明 |
|
||
|---|---|---|---|
|
||
| 设置页行数 | 4 | 5 | 新增「清理缓存」行并在「缓存上限」接「自定义」数字输入 |
|
||
| 音质 sheet 档数 | 3(Hi-Res/Higher/Standard) | 5(+Lossless/Exhigh) | 对齐 core `NCM_LEVEL_*` 全档 + 降级链起点 |
|
||
| `ncm_cache_clear` | — | 新增 | 删除 `song_*` 全部(mp3/封面/.tmp),供「清理缓存」与测试 |
|
||
|
||
---
|
||
|
||
## 16. A2:音频输出迁移 meow_audiod(SINK_AUDIOD 默认后端)+ 无声根因修复(2026-09-01,agent-248)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`(含 build 脚本、bench、REPORT 本文件)。未动 `sdk/`、`meow_audiod/`、
|
||
> `meow_launcherd/`、`vendor/`。风控纪律:真实 NCM 请求 = autoplay 验证的 1 detail + 1 song_detail
|
||
> (Docu 单首歌单,共 2 次 metadata,**0 次 song_url 下载**,曲目走已缓存 mp3)。
|
||
|
||
### 16.1 迁移内容
|
||
|
||
- **`app/play/audio_out.c` 新增 SINK_AUDIOD 后端并设为默认**(AUDIOD_DESIGN.md §7 落地):
|
||
- 后端选择(env 驱动):缺省 → AUDIOD(SDK `meow_audio_*` 连 `/usr/data/meow_audio.sock`);
|
||
`NCM_AUDIO_SINK=file/null/alsa` → FILE(WAV 回归)/NULL(解码 RT)/ALSA 直开;
|
||
`MEOW_AUDIO_DIRECT=1` → ALSA 直开(调试保底,非正式能力)。audiod 不在 → open 返回 -1
|
||
(不隐式回退直开),日志记 `audiod open failed rc=...`。
|
||
- **ALSA 保底路径修复两大坑**(与 audiod/SDK 同款修法,注释标明勿回退):① hw_params 改
|
||
「全放行」初始化(mask 全 1 + interval [0,UINT_MAX])——旧 memset 0 的 interval 等价
|
||
「只能取 0」→ 内核 refine 必空 → HW_PARAMS 恒 EINVAL,这正是**网易云「播放无声」的根因**
|
||
(真机 probe 实证);② access/format/subformat mask 先清零再单点(留全 1 会被 choose 成
|
||
MMAP_INTERLEAVED → write() EINVAL)。
|
||
- **被抢占处理**(`player_manager.c`):`player_manager_start` 注册 `meow_audio_on_preempt`
|
||
→ 回调记 WARN 日志并经 `player_manager_pause(1)`(g_play 命令通道,持锁)暂停播放+更新 UI 态,
|
||
不直接动锁外状态。
|
||
- **暂停/恢复语义对齐**:`run_song` 进 NCM_PC_PAUSED 沿 → `audio_out_pause(1)` 向 audiod 发
|
||
MSG_AUDIO_PAUSE(停 PCM 时钟不丢流);出暂停沿 → `audio_out_pause(0)` 恢复;**恢复时若流已死**
|
||
(被抢占/断链,SDK 返回负值)→ 自动 close+reopen 重建输出流(解码位置不变)。
|
||
- **播放路径全量日志**(meow_log,无声排查永久资产):open 结果/请求参数/audiod 授权
|
||
grant_*(open 后 `meow_audio_query` 落 rate/ch/fmt/underrun)、write 错误(首次+每 100 次节流)、
|
||
pause/resume/reopen/close(含累计写帧)、play start/done/switch/seek/stopped 事件。
|
||
- **真机联调 autoplay hook**(`appstate.c` + `app.c`,非正式能力):`/tmp/ncm_autoplay` 触发文件
|
||
存在且 home 歌单已载 → 后台线程找「已缓存」曲目自动播放(内容=数字则歌单下标起扫 ≤3 个、
|
||
=文本则歌单名子串单点匹配如 "Docu")。**绝不发 song_url 下载**——无缓存命中只记日志退出。
|
||
用途:无用户在场时验证「ncm→SDK→audiod→DAC」全链。
|
||
- `build_ncm_host.sh`:SDK_SRC 补 `meow_ui_audio`(audio_out.c 新增 meow_audio_* 引用,宿主链接需要)。
|
||
|
||
### 16.2 验证记录(分层)
|
||
|
||
| 层 | 项 | 结果 |
|
||
|---|---|---|
|
||
| 宿主 | `build_ncm_host.sh` | **自有代码 0 warning**(仅 SDK meow_ui_vol.c 既有 system() 告警) |
|
||
| 宿主 | WAV 回归:`NCM_AUDIO_SINK=file NCM_AUDIO_OUT=... test_decode MIPSTest.mp3` | **PASS**:8.21s/48k/立体声,WAV 头合法,字节数 44+393984×4 精确一致 |
|
||
| 宿主 | 离屏渲染 home/playlist/player/settings/search | 全 rc=0 无回归 |
|
||
| 宿主 | **`bench/audiod_chain_sim.c`(新增)**:起真实 `meow_audiod_host`(FILE sink 假 socket)驱动真实 audio_out.c+SDK 客户端 | **14 项全 PASS**:OPEN(grant=44100/2ch/S16)→WRITE 51200 帧→QUERY(active=1 written>0)→PAUSE(paused=1)→RESUME→**抢占**(第二客户端裸发 OPEN→on_preempt 回调触发→旧流 write 失败)→**流死自动重开**(reopen 可写)→CLOSE(active=0)→audiod WAV 落盘非空 |
|
||
| qemu | `qemu-mipsel ./out/test_ncm_core_mipsel` | **PASS**(core 未动,回归) |
|
||
| mipsel | `build_ncm.sh` | **0 warning**;ncm md5 `db5b78ad9e8f40e6306484efe2a08eb3` |
|
||
| 真机 | 设备已刷 M2.90(0.7.85),/usr/bin/meow_audiod 在板运行 | ✓ |
|
||
| 真机 | ncm 热更(.new+mv),`am start` 不可用(amsd DECLARE 转发未联调,「pkg not registered」)→ 手动 nohup 起 | ✓ 启动/home 歌单 120 正常 |
|
||
| 真机 | autoplay hook:name 'Docu' → playlist[1](1 tracks)→ song[0] id=2048962657 缓存命中 → play | ✓ 日志链路正确 |
|
||
| 真机 | **audiod OPEN→grant 链**:`meow_audio tone` 拿到 `sid=1 grant=44100Hz/2ch/S16` | ✓ IPC/协商链设备上正常 |
|
||
| 真机 | **持续写帧/underrun 验收**:**被阻断**(见 §16.3 重大发现) | ✗ 未达成 |
|
||
|
||
### 16.3 重大发现:M2.90 设备 PCM 播放启动 → 内核硬挂死 → 看门狗重启(4/4 复现)
|
||
|
||
**现象**:设备(0.7.85,stock kernel 4.4.94+ #15 2026-01-16,未变)上一进入 PCM 播放启动序列,
|
||
数秒内整机硬挂死并重启。复现 4/4:`meow_audio tone` ×3(含一次无 ncm 的纯 audiod 路径)+
|
||
ncm 经 SDK 链播放 ×1。kmsg 流抓取到最后内核日志(死亡点,两次抓取一致):
|
||
|
||
```
|
||
[ 412.104797] hbc3000_set_samplerate codec_type=4 samplerate=44100 spdif=0 ...
|
||
[ 412.104819] hbc3000_stop
|
||
[ 412.104865] hbc3000_start
|
||
[ 412.168307] cs43198_dai_prepare pcm
|
||
[ 412.371919] hbc3000_start
|
||
<<< 日志到此中断,无任何 panic/oops 文本 → 硬挂死(疑似时钟/电源域死锁),随后重启 >>>
|
||
```
|
||
|
||
**归因排查记录**:
|
||
- **固件打包无误**:设备 /usr/bin/meow_audiod md5 `e07e9144…` == 仓库构建 == fw_content.conf 条目。
|
||
- **ncm 无关**:对照组 ncm.bak(A2 前二进制 19d99ce4)同设备前台 idle **31 分钟稳定**(boot G,
|
||
期间经历 suspend/resume);本会话早段(23:05-23:45)观察到的 4 次「idle ncm 在跑数分钟后重启」
|
||
实为**用户 M2.90 刷机循环**(卡根 r3proii.upt 22:46 上卡,多次升级重启)同窗口巧合,对照组证伪。
|
||
- **suspend 相关性未证伪也未证实**:设备每次 boot 后 ~40s 自动息屏进 `System suspend`(dmesg 实证),
|
||
而 4 次致命播放全部发生在 resume 之后;A1 收尾(agent-246)同内核 tone 实测 written=played、
|
||
underrun=0 **通过**——据信在未 suspend 的新鲜 boot 窗口。未 suspend 的播放验证需要
|
||
「重启后 40s 内立刻 tone」,本会话无法在不主动重启的前提下获得该窗口(留给用户/主会话)。
|
||
|
||
**影响与建议(留主会话/用户决策)**:
|
||
- A2 真机验收的「持续写帧+underrun=0」被此问题阻断,非 ncm 代码问题(SDK 链 OPEN→grant 已实证:
|
||
tone 拿到 `sid=1 grant=44100Hz/2ch/S16`;ncm 日志链到「缓存命中 → play」)。
|
||
- 建议:① 新鲜 boot 40s 内(未 suspend)测 `meow_audio tone` 验证 suspend 相关性;
|
||
② 若为真,权宜=禁用自动息屏(`display.screen_off_timeout`=0)或 resume 后重置 codec 电源/时钟;
|
||
③ 根治需看 hbc3000/cs43198 驱动 resume 路径(stock 内核源码不在本仓库);挂死点恒在
|
||
`cs43198_dai_prepare` 后第二次 `hbc3000_start`,无任何内核日志 = 硬挂(时钟/i2c/电源域死锁特征)。
|
||
- 本会话为定位该问题触发过多次设备重启(均自动恢复;未碰 recovery_all/未 killall hiby_player/
|
||
未主动重启设备)。
|
||
|
||
### 16.4 md5(本批改动文件 + 产物)
|
||
|
||
```
|
||
db5b78ad9e8f40e6306484efe2a08eb3 ncm # mipsel 静态 App(新;已部署设备)
|
||
```
|
||
(源文件 md5 以 git 为准;改动清单见 §16.5)
|
||
|
||
### 16.5 出入清单(只改 `patch_v2/ncm_app/`)
|
||
|
||
```
|
||
修改:
|
||
app/play/audio_out.c # SINK_AUDIOD 默认后端 + ALSA 保底两坑修复 + audio_out_pause + 全量日志
|
||
app/play/player_manager.c # on_audio_preempt 注册/回调 + 暂停沿 audiod pause/resume + 播放事件日志
|
||
app/logic/appstate.c # autoplay 真机联调 hook(/tmp/ncm_autoplay,缓存命中才播,绝不下载)
|
||
app/app.c # app_on_tick 挂 autoplay_maybe
|
||
app/ncm_app_internal.h # audio_out_pause/autoplay_maybe 声明 + audio_out 注释更新
|
||
build_ncm_host.sh # SDK_SRC 补 meow_ui_audio
|
||
ncm / ncm_host # 重编产物
|
||
新增:
|
||
bench/audiod_chain_sim.c # audiod 全链离线回归(真实 audiod_host 假 socket,14 项)
|
||
REPORT.md # 本批记录(§16)
|
||
(未动 sdk/、meow_audiod/、meow_launcherd/、vendor/;未动设备系统/恢复区)
|
||
```
|
||
|
||
### 16.6 诚实出入 / 已知限制
|
||
|
||
- **真机验收未闭环**:§16.3 内核硬挂死阻断「持续写帧 + underrun=0 + meow_audio status 在播」三项。
|
||
已实证到 OPEN→grant(CLI tone)与 ncm→autoplay→缓存命中→play 日志链;SDK 客户端全链
|
||
(含抢占/重开)由宿主 sim 对真实 audiod_host 14 项覆盖。
|
||
- **autoplay hook 默认 0 起扫前 3 个歌单首页**:若首个歌单是超大歌单(5191 首 detail ~35s 解析)
|
||
会慢/重;文本名单点匹配(如 Docu)是推荐用法。hook 为调试资产保留,不影响正常 UI 路径。
|
||
- **`am start dev.player.ncm` 报 pkg not registered**:amsd 二期 launcher→amsd DECLARE 转发未联调
|
||
(WORK_STATE 已记残余),本次手动 nohup 起 ncm(fb 与 launcher 并存,测试场景可接受)。
|
||
- 设备日志中 ipc_media META 每秒一对(state=0 空态也发)为既有行为,本批未动。
|
||
- **未碰** /data/recovery_all、未 killall hiby_player、未主动重启设备(§16.3 的多次重启均为
|
||
被测路径触发的设备自重启)。
|
||
|
||
---
|
||
|
||
## 17. 三期第三批:歌曲对象瘦身 + 滑动窗口(任意长度歌单,取消 1000 截断)+ 播放快照(2026-09-01)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`(含 `core/` 结构体瘦身)。未动 `sdk/`、`meow_launcherd/`、
|
||
> `meow_audiod/`、`vendor/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub + /tmp 盘缓存)。
|
||
> **本批未部署真机**(设备音频线挂死排查中,ncm 二进制不动卡;部署留主会话)。
|
||
|
||
### 17.1 动机
|
||
|
||
§14 的歌单分页(detail 全量 trackIds 驱动 song_detail 分批 100、触底加载)仍受
|
||
`NCM_SONG_MAX=1000` 内存硬上限约束(每首 `ncm_song_t` ≈1324B → 1000 首 ≈1.32MB bss),
|
||
用户「我喜欢」6000+ 首只能看到前 1000。本批把「全量物化」改为**滑动窗口**:内存只保留
|
||
视口附近的歌曲对象(256 首),滚动出窗释放、入窗按需拉取,支撑任意长度歌单。
|
||
|
||
### 17.2 歌曲对象瘦身(core/ncm_api.h,1324B → 608B,-54%)
|
||
|
||
| 字段 | 旧 | 新 | 依据 |
|
||
|---|---|---|---|
|
||
| `id` | 32 | 24 | 数字文本,int64 最大 19 位 + NUL |
|
||
| `name` | 256 | 128 | 列表/播放页展示均按宽再截断(`ncm_text_fit`/listview) |
|
||
| `art_name` | 256 | 96 | 首个歌手名 |
|
||
| `album_name` | 256 | 96 | 同上 |
|
||
| `al_pic` | 512 | 256 | picUrl 实际 <200 字符;`obj_str` 截断安全 |
|
||
| `duration_ms` | `long` | `int32_t` | <2^31ms≈24 天;MIPS32 的 long 本就 32 位(零行为变化) |
|
||
|
||
解析侧(`parse_songs_array`)全部走 `sizeof 字段` 拷贝,无需改动;core 测试
|
||
(`test_ncm_core.c` 断言 id/name/ar/al/dt/fee/al_pic)宿主 + qemu-mipsel 全 PASS。
|
||
|
||
### 17.3 滑动窗口(appstate.c + playlist.c)
|
||
|
||
- **内存模型**(`ncm_app_internal.h`):
|
||
- `song_ids[NCM_TRACKID_MAX=16384]` = detail 取回的全量 trackIds 索引(int64,128KB bss,
|
||
不再 1000 截断;16384×~13B ≈ 213KB JSON,远在 2MB 响应限幅内);
|
||
- `songs[NCM_SONG_WINDOW=256]` + `song_valid[256]` 位图 = 物化窗口(256×608B≈152KB bss),
|
||
`songs[i] ↔ 全局下标 window_base+i`;
|
||
- 列表 count = `track_ids_count`(全量),未物化行画占位「··· / 加载中」。
|
||
- **API**(`app/logic/appstate.c`,全锁 `g_song_mu`;网络/盘 IO 在锁外,锁内仅内存搬移):
|
||
- `appstate_song_ensure_range(lo,hi)`:窗口未覆盖先滑窗(重叠区 `songs+valid` 一起
|
||
memmove 保留,出窗槽位失效),缺失槽先读盘缓存,miss 攒批 song_detail 补载并落盘;
|
||
- `appstate_song_copy(gidx,&out)` / `appstate_range_loaded(lo,hi)`:锁内读窗(UI 行回调);
|
||
- `appstate_song_fetch(gidx,&out)`:任意下标物化(窗→盘→song_detail 单曲),**不滑窗**
|
||
(播放线程专用,避免与浏览视口抢窗口造成乒乓);
|
||
- `appstate_adopt_songs(n)`:收编外部已填的 `songs[0..n)`(搜索结果)为窗口 base=0 播放上下文;
|
||
- 兼容入口 `appstate_load_playlist_songs_page(idx,offset,limit)`:offset=0 重建索引+物化
|
||
首段;offset>0 = ensure [offset,offset+limit)。
|
||
- **批网格对齐预取**:补载区间对齐到 `NCM_SONG_BATCH=100` 的全局网格([a0,b0) ≤100),
|
||
逐行滚动不产生小请求放大——6000 首全程下滚 song_detail 调用恰 = 60 次(6000/100)。
|
||
- **盘缓存(app/logic/songmeta.c,新)**:拉过的 song_detail 结果逐首序列化到
|
||
`<cache_dir>/meta/<id>.bin`(头 magic/ver/size + 结构体镜像;ver/size 失配判 miss)。
|
||
刻意不进 ncm_cache LRU 池:meta 每首 ≈600B(一万首 ≈6MB),不被清音频缓存误清。
|
||
效果:二次进入歌单 / 回滚全程 **0 网络请求**(离线可浏览已拉过的部分)。
|
||
- **UI(playlist.c)**:`pl_maybe_load_more`(触底追加)退役 → `pl_maybe_ensure`(视口 ±24 行
|
||
有未物化槽 → 后台 worker ensure);footer 改「视口区间 / 总数」(如 `5965-5988 / 6000 首`);
|
||
首屏失败整页空态 + 点按重试不变;补载失败 snack + 占位行保留(滚动/点按再试)。
|
||
|
||
### 17.4 播放兼容(窗口滑出当前播放曲不许崩)
|
||
|
||
- **播放快照**:`g_play.play_song`(mu 保护)——`run_song` 起播时物化目标曲
|
||
(`song_copy` 窗内命中 / `song_fetch` 窗外)并整体拷入快照;之后下载(id)、时长、
|
||
歌词 worker(id)、IPC META(title/artist/album)、播放页 UI(歌名/歌手/封面)**全读快照**,
|
||
与窗口完全解耦。窗口滑出当前播放曲 → 播放/歌词/媒体会话照常。
|
||
- 上下首/自然顺延边界:`g_st.song_count`(窗口内有效数)→ `g_st.track_ids_count`(全量索引),
|
||
cur_song 语义 = 全局下标不变。
|
||
- **占位行可直接点歌**:`player_play_song(row)` 不要求行已物化——封面回落歌单封面,
|
||
元数据由播放线程 fetch 后快照接管;UI tick 按快照 id 变化纠正封面/歌名(快照滞后 settle)。
|
||
- 搜索上下文:`search_worker` 结果经 `adopt_songs` 收编(song_ids 同步 strtoll),
|
||
播放/上下一首对结果集生效(同旧语义)。
|
||
|
||
### 17.5 内存对比(6000 首歌单场景)
|
||
|
||
| 项 | 改前(§14) | 改后(§17) |
|
||
|---|---|---|
|
||
| `ncm_song_t` | 1324B | 608B(-54%) |
|
||
| 歌曲对象数组 | 1000×1324B ≈ 1292KB(**且 1000 截断,6000 首只能看前 1000**) | 256×608B ≈ 152KB(任意长度恒定) |
|
||
| trackIds 索引 | 1000×8B = 8KB(截断) | 16384×8B = 128KB(全量) |
|
||
| 有效位图 | — | 256B |
|
||
| **合计(歌曲数据)** | **≈1300KB(有截断)** | **≈280KB(无截断)** |
|
||
| ncm bss 总量(`size`) | 2052068 | **1080900**(-972KB,-47%) |
|
||
|
||
6000 首全量物化的假想成本 6000×1324B≈7.9MB(14MB 预算下不可行)→ 滑窗后与歌单长度无关。
|
||
|
||
### 17.6 验证记录(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 窗口基本语义 | `bench/pagination_sim.c`(已更新到新语义,离线 stub 驱动真实 appstate) | **全 PASS**:130 首整装入窗(1 detail + 2 song_detail);5191 首无截断(索引全量 5191、52 批网格、末首可达、出窗槽 copy 失败、越界无操作);空歌单 0 |
|
||
| 6000 首滚动全程 | `bench/window_sim.c`(新;真实 appstate + 真实 songmeta 盘缓存 /tmp) | **全 PASS**:首屏 1+1 请求;下滚 750 步(步进 8 行)每步视口 12 行全物化、song_detail 恰 60 次(无放大)、有效槽恒 ≤256;**回滚全程 +0 请求**(盘缓存全命中,6000 meta 文件落盘);窗口滑出播放曲后 `song_fetch` 不滑窗不崩、盘命中免请求、尾部/越界钳制正确 |
|
||
| core 回归 | `build_core.sh` + `test_ncm_core_host` + `qemu-mipsel out/test_ncm_core_mipsel` | 宿主 + qemu 全 PASS(结构体瘦身不影响解析断言) |
|
||
| mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 865604 / data 1480 / bss 1080900` |
|
||
| 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | home/playlist/player/lands/settings/search/login 渲染正常:歌单 6 行 + footer `1-6 6`(视口区间格式)、黑胶 + 快照歌名/歌手 + 歌词高亮;自有代码 0 warning(仅 SDK `meow_ui_vol.c` 既有告警) |
|
||
|
||
### 17.7 md5(本批改动文件 + 产物)
|
||
|
||
```
|
||
b32e8a80c12de4dac2e4ad52d26ae55c ncm # mipsel 静态 App(新;未部署)
|
||
783d3ddf9bfc1905f732f688c54c5447 ncm_host # 宿主离屏调试二进制
|
||
321039c8743af6ccd934c63a3a161e84 app/ncm_app_internal.h
|
||
bde871b33298cf25187054650560754a core/ncm_api.h
|
||
3c4122e72cc701aa5d0d95862b0201d1 app/logic/appstate.c
|
||
dd8fbb70ba5e0978fa75501135c473a3 app/logic/songmeta.c
|
||
cb44e2385108ba6cdad242aecf635b64 app/ui/playlist.c
|
||
533dafebce47bd60355a1efc7719db9c app/ui/player.c
|
||
a5447ac5e6f2ba83405a7a3c19c88dc9 app/play/player_manager.c
|
||
74eded2b9a220aba9a50832ff9c6914c app/play/ipc_media.c
|
||
d2c2505eede96ddb1387209681894b83 app/ui/search.c
|
||
564630846354d3e202de303435e82c59 app/test/test_ncm_app.c
|
||
00a1445f83e98a068c4219593bb7d6d8 bench/pagination_sim.c
|
||
b2b6869990c8ef3906c100f18c7194e8 bench/window_sim.c
|
||
3159c53adf59967cacdad50a58946858 build_ncm.sh
|
||
430211ee6895c26c9871da22e9aede76 build_ncm_host.sh
|
||
```
|
||
|
||
### 17.8 出入清单(只改 `patch_v2/ncm_app/`)
|
||
|
||
```
|
||
修改:
|
||
core/ncm_api.h # ncm_song_t 瘦身 1324B→608B(id24/name128/art96/al96/pic256/dt int32)
|
||
app/ncm_app_internal.h # 删 NCM_SONG_MAX;+NCM_TRACKID_MAX 16384 / NCM_SONG_WINDOW 256 /
|
||
# song_valid[]/window_base;g_play.play_song 快照;窗口 API + songmeta 声明
|
||
app/logic/appstate.c # 滑动窗口(g_song_mu/滑窗 memmove/批网格预取/盘缓存补载)+
|
||
# song_copy/range_loaded/song_fetch/adopt_songs + 兼容入口改窗口语义 +
|
||
# autoplay 走 song_copy + logout 清窗口 + strncpy→ncm_strlcpy 消告警
|
||
app/ui/playlist.c # 触底分页退役 → 视口 ensure(pl_maybe_ensure)+ 占位行 + footer 视口区间
|
||
app/ui/player.c # 元数据读播放快照(cur_song_meta)+ 快照 id 变化纠正封面/歌名 +
|
||
# 占位行可点(播放线程物化)
|
||
app/play/player_manager.c # run_song 起播快照 + song_local_file 按 id + 边界 track_ids_count +
|
||
# 歌词 worker 读快照 + player_manager_current_song 访问器
|
||
app/play/ipc_media.c # META title/artist/album 读播放快照
|
||
app/ui/search.c # 结果经 appstate_adopt_songs 收编为窗口播放上下文
|
||
app/test/test_ncm_app.c # mock 补窗口字段(song_valid/track_ids_count)+ 播放快照
|
||
bench/pagination_sim.c # 更新到窗口语义(5191 无截断/出窗 copy 失败/网格批数)
|
||
build_ncm.sh / build_ncm_host.sh # +app/logic/songmeta.c
|
||
ncm / ncm_host # 重编产物
|
||
新增:
|
||
app/logic/songmeta.c # 歌曲元数据盘缓存(<cache_dir>/meta/<id>.bin,magic/ver/size 头)
|
||
bench/window_sim.c # 6000 首滚动全程离线验证(0 请求,盘缓存回滚 0 新增请求)
|
||
REPORT.md # 本批记录(§17)
|
||
(未动 sdk/、meow_launcherd/、meow_audiod/、vendor/;未部署真机;未动设备系统/恢复区)
|
||
```
|
||
|
||
### 17.9 诚实出入 / 已知限制
|
||
|
||
- **真机未部署/未验**(风控 + 设备音频线排查中;部署留主会话)。真机验收点:① 6000+ 歌单
|
||
全程下滚/回滚无占位残影、footer 视口区间随滚动更新;② 占位行直点能播(fetch 物化);
|
||
③ 播放中猛滑列表把当前曲滑出窗 → 播放/歌词/媒体卡片不断;④ 二次进入歌单离线可浏览
|
||
已拉过的部分(meta 盘缓存);⑤ 歌单 >16384 首时 trackIds 索引截断(footer 总数仍对,
|
||
尾部不可达——16384 是 128KB bss 的取舍,当前用户最大歌单 6000+ 有足够余量)。
|
||
- **单写者假设**:ensure/page 仅由 playlist worker 驱动(`g_plthr_on` 互斥),播放线程
|
||
`song_fetch` 不写窗口;搜索 worker 的 cloudsearch 直接写 `songs[]` 前半再 `adopt_songs`
|
||
收编(与旧代码同款的单写者时序,未加新锁)。
|
||
- **快照滞后一瞬**:切歌瞬间(next/prev/占位行直点)播放页歌名/封面仍是旧曲快照,
|
||
play 线程物化后(通常 <100ms,窗外需一次盘读/网络)由 tick 按快照 id 纠正——一帧级旧
|
||
内容,与 §14.7 歌词锁残余同级接受。
|
||
- **切歌单时播放上下文**:打开新歌单会重建索引/清窗口(同 §14 既有行为),正在播的旧歌单
|
||
曲目靠快照续播(比旧模型更安全:旧模型 memset 后 id/dur 全丢);但 cur_song 下标语义
|
||
随新歌单索引变化(既有竞态,未扩大)。
|
||
- **meta 盘缓存无版本化清理**:结构体再改版靠 ver/size 判 miss 自动重建;无 TTL
|
||
(歌曲改名/换封面不主动失效——与 ncm_cache 同级的「缓存可再生」取舍)。
|
||
- **host 构建有 SDK `meow_ui_vol.c`「system 返回值忽略」告警**(SDK 源,非本 App;mipsel musl 无)。
|
||
|
||
---
|
||
|
||
## 18. 三连修:封面切歌不更新 + 缓冲无反馈/失败无声 + 暂停/切歌边界稳健性(2026-09-01,用户三连报)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`(ui/play/logic/bench/test + 本文件)+ 根 `.gitignore`(新增 3 个 sim
|
||
> 二进制忽略条目)。未动 `sdk/`、`meow_launcherd/`、`meow_audiod/`、`vendor/`。
|
||
> 风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/sim;并新增 mock 防真网守卫,见 §18.5)。
|
||
> **本批未部署真机**(部署留主会话统一做)。
|
||
|
||
### 18.1 问题①:封面只在第一首歌出现、切歌不更新 —— 根因(三因叠加)
|
||
|
||
1. **cover worker 忙等死循环(主因,CPU 饥饿)**:`cover_worker` 换入成功后**不清目标 URL**
|
||
(`g_tgt_url` 残留),主循环每圈 缓存读 200KB + JPEG 解码 + free/换入 + `g_gen++`,成功路径
|
||
**无 sleep** → 首张封面到位后 worker 永久 100% CPU;同时 `cover_changed()` 恒真 → 播放页
|
||
tick 每帧整盘重绘(黑胶行扫 sqrt + cover blit)。单核 MIPS 上 UI 线程被饿到极低帧率,
|
||
慢网络下新封面请求(10s 超时级)看似永不完成。sim 实证:旧代码 settle 后 1.2s 内
|
||
`cover_changed` 翻动 **119 次**(新代码 0 次)。
|
||
2. **封面兜底值污染(确定性子根因)**:`player_play_song` 把本曲 `al_pic` 经
|
||
`appstate_set_playing_song` 写进 `g_st.cover_url`(本意=歌单封面兜底)。此后 al_pic 缺失的
|
||
歌曲回落到的是**上一首的专辑图 URL**,且撞 `cover_request` 去重(== 当前展示)→ 封面永远
|
||
停在上一首。
|
||
3. **绘制期 UAF**:`cover_get()` 无锁返回裸指针,UI 绘制期间 worker 换入即 `meow_img_free`
|
||
同一张图(忙等循环下每 ~1/100s 一次)→ use-after-free/撕裂绘制风险。
|
||
|
||
**修法**(cover.c + player.c + playlist.c):
|
||
- worker 换入成功即 `g_tgt_url[0]=0` 转怠速(250ms 轮询);下载/解码失败**指数退避**
|
||
0.5s→4s、连续 8 次放弃并清目标(UI 重进页会重新提交),每次失败 WARN 日志(不再无声)。
|
||
- `cover_request` 去重加判「在拉目标」(g_tgt_url)+ `g_host_mock` 直接返回(0 网络纪律)。
|
||
- `cover_get()` 改**持锁返回** + 新增 `cover_put()`,两个绘制调用点(player 黑胶/playlist
|
||
头块)配对使用,UAF 闭合。
|
||
- `player_play_song` 不再用 al_pic 污染兜底(`appstate_set_playing_song(idx, NULL)`),
|
||
`g_st.cover_url` 回归「歌单封面兜底」单语义。
|
||
- `plr_page_enter` 清零 `g_last_meta_id`/`g_last_song` 强制重估(重进播放页自愈:
|
||
离开期间切歌/封面失败后回来首个 tick 即重请求当前曲封面)。
|
||
- `pl_page_enter` 补请求歌单封面——旧版 `playlist_open` 只 `cover_cache_reset` 清掉、无人
|
||
再请求 → 歌单头块封面永远占位 ♪(顺手回填,缓存命中后开销≈一次盘读+解码)。
|
||
|
||
### 18.2 问题②:点歌后全程无反馈、失败无声死等 —— 根因与修法
|
||
|
||
- **下载本就不在 UI 线程**(核查结论):song_url 获取 + 流式落盘全在播放线程
|
||
(`run_song`→`song_local_file`),UI 无阻塞点;「像卡死」= ①无缓冲 UI ②错误无出口
|
||
③问题①的 CPU 忙等拉低整机帧率 三感叠加。
|
||
- `g_play.err/errmsg` **无任何 UI 消费者**(`player_manager_error()` 全仓零调用)→
|
||
VIP/版权/超时全部无声。修法:
|
||
- 播放页歌词区三态:`NCM_PC_BUFFER` →「缓冲中… N%」(新增 `g_play.dl_done/dl_total`,
|
||
下载回调锁内累加,song_url 的 size 做分母,未知则转 KB;tick 按 ~2KB 粒度刷动态带);
|
||
`err!=0` → 红字中文错误条(无音源 VIP/版权 / 登录过期 / 网络失败)+「点播放键重试」,
|
||
错误沿弹 snack;有词时歌词照旧。
|
||
- 播放键在 `STOPPED 且有当前曲`(出错后)= **重试**(`player_manager_play(cur)`),
|
||
图标在 PAUSED/STOPPED 画三角、PLAYING/BUFFER 画暂停。
|
||
- `player_manager_play/next` 清 `err` 并重置下载计数(旧错误不残留)。
|
||
|
||
### 18.3 问题③:暂停/恢复/切歌边界稳健性
|
||
|
||
- **写失败连环跳歌(真根因级)**:`audio_out_write` 失败旧版静默 `break` → 外层当
|
||
「自然播完」`next(1)` → 流死时每首都写失败 → 连环跳完整个歌单且每跳一首发一次
|
||
song_url 请求(风控隐患 + 用户看到的「放不了」)。修:写失败 `set_err(NCM_ERR_IO)`
|
||
→ STOPPED 停住、cur 不动、UI 出错误条。sim 实证:旧代码写失败后 err=0、cur 冲到 -1;
|
||
新代码 err=NCM_ERR_IO、cur 不变、writes=1 即停。
|
||
- **下载途中切歌竞态**:旧版旧曲下载完成后继续开解码/开音频并无条件
|
||
`state=PLAYING` + 写过期快照 → 把新曲的 BUFFER 覆盖成 PLAYING(UI 假「在播」)+ 封面/歌名
|
||
短暂回跳。修:`still_current()` 护栏——物化后/下载失败后/下载完成后三处校验
|
||
`cur_song==idx && state==BUFFER`,否则静默回外层(缓存已落盘不亏)。sim P4 实证新旧差异。
|
||
- 流死重开链(A2 已有 resume 自动 reopen)UI 侧闭环:被抢占→PAUSED(播/停图标正确)、
|
||
resume reopen 失败 → STOPPED+错误条(可重试);`audiod pause rc=-9` 路径不动(pause 失败
|
||
仅记日志,恢复时走 reopen)。
|
||
- 已知边界(不改):下载/ song_url 不可中途取消(ncm_http 无 cancel),切歌响应最坏
|
||
多等一个在途请求超时(30s 上限,正常秒级);BUFFER 态点暂停无效(语义:缓冲未开播)。
|
||
|
||
### 18.4 验证记录(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 封面链 | `bench/cover_chain_sim.c`(新;真实 cover.c+ncm_cache,stub HTTP/假 img) | **新代码 7 组全 PASS**:首请求出图/定居零忙等/切歌换图+cover_current.jpg 同步/去重/失败×2退避成功/连败 8 次放弃保留旧图/空 URL+reset。**旧代码对照**:T2 忙等 119 次/1.2s FAIL(坐实根因①) |
|
||
| 播放状态机 | `bench/player_flow_sim.c`(新;真实 player_manager.c 播放线程,stub 解码/音频/网络) | **新代码 P1-P5 全 PASS**:自然顺延(err=0)/写失败置错停住不跳歌/无音源错误条/下载中切歌无过期状态覆盖/错误后重试。**旧代码对照**:P2(err=0+cur=-1 连环跳歌)、P4(旧曲覆盖 PLAYING+过期快照)FAIL |
|
||
| audiod 链回归 | `bench/audiod_chain_sim`(真实 audiod_host 假 socket) | 14/14 PASS(含抢占/流死重开,问题③链路回归) |
|
||
| 滑窗回归 | pagination_sim + window_sim(重编新 appstate.c) | 全 PASS |
|
||
| core 回归 | test_ncm_core_host + `qemu-mipsel out/test_ncm_core_mipsel` | 全 PASS |
|
||
| 宿主构建 | build_ncm_host.sh | 自有代码 **0 warning**(仅 SDK meow_ui_vol.c 既有告警) |
|
||
| 宿主离屏渲染 | home/playlist/player/playerbuf/playererr/settings/search/login/lands | 全 rc=0;playerbuf 出「缓冲中… 25%」位(宿主 mock 字体无 CJK,中文留白为既有宿主限制,设备 GBK 字库正常)、playererr 错误条+播放三角 |
|
||
| mipsel | build_ncm.sh(自有代码 -Wall -Wextra) | **0 warning**;text 868772 / bss 1080900(与 §17 持平) |
|
||
|
||
### 18.5 md5(本批产物 + 改动文件)
|
||
|
||
```
|
||
50d41bbf4a379e925699ba013b997ffd ncm # mipsel 静态 App(新;未部署,留主会话)
|
||
87de141c6a2719bb2b93807f282077de ncm_host # 宿主离屏调试二进制
|
||
6715607f561c5df83b251569b0ae2b7c app/logic/cover.c
|
||
d6aee631734622526befb977d4e3cddf app/logic/appstate.c
|
||
ca22772678de6e293747c9ef4890b80f app/ncm_app_internal.h
|
||
2d0eb786ec78f48e7e0625f4bd7a74c9 app/play/player_manager.c
|
||
d2bdfbed5569af760119d0ffb785e43e app/ui/player.c
|
||
c596763af26fd0c687286a250fccd90a app/ui/playlist.c
|
||
08fc30f6ddf77c638ca4a8f3a20aef0e app/test/test_ncm_app.c
|
||
30b163bfc2c880b819f8168b10895161 bench/cover_chain_sim.c
|
||
fee4204e945866f5a07ca7d191fa83e3 bench/player_flow_sim.c
|
||
```
|
||
|
||
### 18.6 出入清单
|
||
|
||
```
|
||
修改:
|
||
app/logic/cover.c # worker 成功清目标转怠速(修忙等死循环)+失败指数退避 8 次放弃+
|
||
# WARN 日志+去重加在拉目标+g_host_mock 防真网+cover_get 持锁/cover_put(修 UAF)
|
||
app/ui/player.c # 缓冲三态 UI(缓冲中…N%/错误条+重试)+播放键 STOPPED=重试+enter 自愈重估+
|
||
# 下载进度 tick 刷新+错误沿 snack+cover_put 配对+不污染封面兜底
|
||
app/ui/playlist.c # cover_put 配对+pl_page_enter 请求歌单封面(头块回填)
|
||
app/play/player_manager.c# dl_done/dl_total 进度+play/next 清 err+still_current 切歌护栏三处+
|
||
# 写失败 set_err(修连环跳歌)
|
||
app/logic/appstate.c # set_playing_song 注释对齐(cover_url 仅兜底语义)
|
||
app/ncm_app_internal.h # cover_put/player_manager_dl 声明+g_play.dl_*+cover_get 语义注释
|
||
app/test/test_ncm_app.c # mock 新页 playerbuf/playererr(缓冲/错误态离屏渲染)
|
||
.gitignore # window_sim/cover_chain_sim/player_flow_sim 二进制忽略
|
||
ncm / ncm_host # 重编产物
|
||
新增:
|
||
bench/cover_chain_sim.c # 封面链离线回归(7 组,含旧代码对照实证忙等根因)
|
||
bench/player_flow_sim.c # 播放状态机离线回归(P1-P5,含旧代码对照实证跳歌/竞态根因)
|
||
REPORT.md # 本批记录(§18)
|
||
(未动 sdk/、meow_launcherd/、meow_audiod/、vendor/;未部署真机;未动设备系统/恢复区)
|
||
```
|
||
|
||
### 18.7 诚实出入 / 遗留
|
||
|
||
- **真机未部署未验**(留主会话)。真机验收点:①切歌封面跟随(含离开播放页再进)②点歌即见
|
||
「缓冲中… N%」、失败出中文错误条+可点播放键重试 ③歌单头块封面回填 ④退出播放页再进无残旧封面。
|
||
- **切歌响应延迟下限**:在途 song_url/下载不可取消(ncm_http 无 cancel 口),连切时旧请求
|
||
跑完才轮新曲(正常秒级;30s 超时兜底)。要更低延迟需 core 层加取消位,本批未动 core。
|
||
- **封面失败放弃后**靠「重进播放页/切歌」重新武装(enter 自愈),无定时重试——弱网长期
|
||
挂机时封面可能缺席但不影响播放。
|
||
- 播放页 BUFFER 态进度条仍画 0:00 标尺(缓冲文案在歌词区),未做进度条一体化——观感项。
|
||
- meow_audiod_host 工作区有他人改动(其他 agent 在改 audiod),本批未 add/未提交该文件。
|
||
|
||
---
|
||
|
||
## 19. FLAC 解码落地(dr_flac)+ 缓存键 = 歌曲ID+音质档位(2026-09-01)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/` + 根 `.gitignore`(vendor/dr_flac 例外条目)。未动 `sdk/`、
|
||
> `meow_audiod/`、`meow_launcherd/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/sim/
|
||
> 自制测试样本;song_url 降级链只改接受条件,未新增请求路径)。
|
||
> **本批未部署真机**(部署留主会话统一做)。
|
||
|
||
### 19.1 动机
|
||
|
||
播放链此前只有 minimp3(mp3-only):`song_url_auto` 只收 mp3 URL,无损 SQ/Hi-Res 档位
|
||
拿到 flac 也只能继续降级 → 无损/Hi-Res 空转(NCM_CACHE_DESIGN §2.5「同日发现」)。
|
||
硬件侧 CS43198 支持到 768kHz/S24,audiod 已支持 S16/S24/S32 输入 + DAC 原生率跟随
|
||
(AUDIOD_DESIGN §4),缺的只是解码器。本批引入 dr_flac 并打通全链。
|
||
|
||
### 19.2 dr_flac 引入(vendor/dr_flac/dr_flac.h,入库)
|
||
|
||
- 来源:https://github.com/mackron/dr_libs(master 分支 `dr_flac.h`,2026-09-01 取得,
|
||
532768 B,版本宏 0.13.4);许可证 **public domain / MIT-0 任选**(文件头/尾双许可声明)。
|
||
- 单头文件直接入库:`.gitignore` 由 `patch_v2/ncm_app/vendor/` 改为 `vendor/*` +
|
||
`!vendor/dr_flac/` 例外(其余 vendor 大树维持不入库,VENDOR_NOTES.md 记版本/获取方式)。
|
||
- 集成:`app/play/dr_flac_impl.c` 唯一实现 TU(`DR_FLAC_IMPLEMENTATION` +
|
||
`DR_FLAC_NO_OGG`——song_url 下发的 flac 恒原生封装,裁 ogg 容器省 text),
|
||
编译豁免告警(与 minimp3 同口径)。
|
||
|
||
### 19.3 解码分发(app/play/decoder.{h,c},新)
|
||
|
||
- `ncm_dec_open` 按 **magic** 分发:首 4 字节 `fLaC` → dr_flac;其余(ID3 / 0xFF 帧同步/
|
||
未知)→ minimp3(未知格式由 minimp3 open 报错兜底,不加第三分支)。
|
||
- 统一接口:`open(回填 rate/ch/bits/total_frames)` / `read(帧)` / `seek_ms` / `close` /
|
||
`format`。位宽映射:flac ≤16bit → S16;>16bit → **S32 左对齐满幅**(drflac s32 输出),
|
||
送 audiod `AUDIO_FMT_S32`——格式归一/SRC-free 原生率跟随全在 audiod 侧(AUDIOD §4),
|
||
本层零裁剪零 SRC。mp3 恒 S16。
|
||
- mp3 细节:minimp3_ex 的 `dec.info` 首读后才有效 → open 时预读一块进 stash(4096 帧),
|
||
read 先吐 stash;seek 保持旧语义(关+重开按字节近似)。flac seek = 精确 PCM 帧。
|
||
- `player_manager.c` run_song 改走 decoder:`audio_out_open(rate, ch, bits)` 按实际流
|
||
(不再硬编码 44100/2/16);进度 pos/暂停日志/seek 帧数全部用实际 rate;
|
||
song 元数据 duration 缺失时用 `total_frames/rate` 推准确时长(flac 精确)。
|
||
单块 PCM 缓冲改静态 32KB(4096 帧 ×2ch ×4B S32 上限;播放线程单实例,省栈)。
|
||
|
||
### 19.4 audio_out 位宽参数化(S32 通路)
|
||
|
||
- `audio_out_write` 签名 `const short *` → `const void *`(字节数按 open 的 bits×ch 算,
|
||
FILE/ALSA 两后端本就如此)。
|
||
- AUDIOD 后端:bits 16→`AUDIO_FMT_S16`、32→`AUDIO_FMT_S32`(其余拒绝);流死重开
|
||
(pause resume 失败路径)按本次 bits 还原格式(旧版恒 S16,flac 曲重开会错格式——随本批修)。
|
||
- ALSA 直开保底:支持 S32_LE(16/32);WAV sink 头 bits 字段如实写(16/32 均合法 PCM WAV)。
|
||
|
||
### 19.5 song_url 降级链接收 flac(core/ncm_api.c)
|
||
|
||
- `ncm_api_song_url_auto` 接受条件:`type ∈ {mp3, flac}`;`type` 缺失(旧端点兼容)时看
|
||
url 扩展名(`.flac` → 回填 type=flac),默认当 mp3 收。降级语义不变(hires→lossless→
|
||
exhigh→higher→standard 逐档,url null/未知格式继续降)。
|
||
- 下载限幅:Hi-Res flac 单曲可达数百 MB(192k/24bit 5min ≈345MB)→ flac 上限 1GB、
|
||
mp3 维持 64MB(`u.size` 可信时按 size 限幅)。
|
||
|
||
### 19.6 缓存键 = 歌曲ID + 音质档位(用户定案 NCM_CACHE_DESIGN §2.5)
|
||
|
||
- 新键:`song_<id>_<level>.<ext>`(level = `ncm_api_level_str`:standard/higher/exhigh/
|
||
lossless/hires)。**同歌同档命中不重复下载;换档=新条目;旧档留给 LRU 自然淘汰**(不主动删)。
|
||
键取**用户设定档**(非实际降级产物档)——保证「同设定永不重下」,降级产物归设定档键。
|
||
- 查找顺序:新键 flac → 新键 mp3 → 旧无档位键 `song_<id>.mp3`(存量温和兼容,命中即播)。
|
||
- **命名正名**:`ncm_cache_path` 剥离 ext 至多一个前导点——旧版调用方传 ".mp3" 生成
|
||
双点名 `song_<id>..mp3`(§256 修的既有约定),新文件一律单点;`ncm_cache_lookup`
|
||
单点 miss 时回退历史双点名(命中照常刷 LRU,不改名不删除)。封面池(song_cov*.jpg)
|
||
同享正名+双点回退。
|
||
- autoplay hook(`/tmp/ncm_autoplay`)的「已缓存」判定同步三键顺序。
|
||
|
||
### 19.7 验证记录(0 真实 NCM 请求)
|
||
|
||
测试样本:libsndfile(venv soundfile)自制 `out/test-vectors/sweep_44k1_16.flac`
|
||
(44.1kHz/16bit/2ch/3.000s=132300 帧)与 `sweep_96k_24.flac`(96kHz/24bit/2ch/2.000s=
|
||
192000 帧)(正弦扫频;out/ 不入库,脚本见 §19.9 可复现)。
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| flac 44.1k/16 宿主 | `test_decode`(新:走 decoder 分发→audio_out FILE WAV),expect 132300/44100/16 | **PASS**:帧数/率/位宽精确;WAV 数据与 libsndfile 参考**逐样本零误差** |
|
||
| flac 96k/24 宿主 | 同上,expect 192000/96000/32 | **PASS**:S32 满幅输出,换算 float 与参考 max err = 0.0;173× RT |
|
||
| mp3 回归 宿主 | MIPSTest.mp3 expect 393984/48000/16 | **PASS**:与 §16 记录逐帧一致(393984 帧/8.208s),WAV 字节数精确 |
|
||
| flac+mp3 qemu-mipsel | `test_decode_mipsel`(静态交叉)同三样本 | **全 PASS**:44.1k 50.9× RT / 96k24 29.0× RT / mp3 4.8× RT(qemu 模拟保守值,真机更快);MIPS32 LE 端序/32位 long 无问题 |
|
||
| 播放状态机 | `bench/player_flow_sim.c`(stub 换 ncm_dec_*;**新增 P6**:档位键+flac ext) | **P1-P6 全 PASS**:P6 实证 cache key=`1000_lossless`、type=flac → `.flac` 落盘;P1-P5 旧用例零回归 |
|
||
| 缓存键/命名 | `test_ncm_core`(新增:ext 带点正名单点路径 + 历史双点名 lookup 命中)+ `cache_evict_sim` | 宿主 + qemu-mipsel **全 PASS**;evict 6 组 PASS(`song_*` 前缀扫描不受键变化影响) |
|
||
| 封面/滑窗回归 | cover_chain_sim / pagination_sim / window_sim(重编新 ncm_cache.c/appstate.c) | **全 PASS**(封面池同享正名+双点回退;autoplay 三键判定 gc 段不影响 sim) |
|
||
| audiod 链回归 | audiod_chain_sim(真实 audiod_host 假 socket,重编新 audio_out.c) | **14/14 PASS**(OPEN/WRITE/PAUSE/RESUME/抢占/流死重开/CLOSE) |
|
||
| mipsel 构建 | `build_ncm.sh`(自有代码 -Wall -Wextra;dr_flac_impl vendor 豁免) | **0 warning**;`text 954304 / data 1576 / bss 1113668`(text +85.5KB = dr_flac+decoder) |
|
||
| 宿主离屏渲染 | build_ncm_host.sh + 9 页 MEOW_UI_DUMP | 全 rc=0;自有代码 0 warning(SDK meow_ui_vol.c 既有告警除外) |
|
||
|
||
### 19.8 md5(本批产物 + 改动文件)
|
||
|
||
```
|
||
52d20f5325ba2bb646355dcc6db54ad3 ncm # mipsel 静态 App(新;未部署,留主会话)
|
||
ff7acea1b1769fb2e67a5177dfc37d8f ncm_host # 宿主离屏调试二进制
|
||
90c7901bff0ff6e2e579ef068ac48161 vendor/dr_flac/dr_flac.h
|
||
86e99aaad27bfc828a59694022e4a8aa app/play/decoder.c
|
||
b02e8e5f7c7194a6bf492171c137233f app/play/decoder.h
|
||
2c6d5b93cecec980a67b737329847905 app/play/dr_flac_impl.c
|
||
6b8b0d13933a547c3349e423a9516eb3 app/play/player_manager.c
|
||
6f5ea39ab6b7ef54c87c48916c8705eb app/play/audio_out.c
|
||
2ad8b12f47014d034df75f864cb2a9b9 app/ncm_app_internal.h
|
||
2ca5b164877617063a7a44964acdf3d8 core/ncm_api.c
|
||
de21f105514235594f90427beec0b10f core/ncm_api.h
|
||
b870b25c9bd9692c912170ddce2b04de core/ncm_cache.c
|
||
0cb40a5af7f1c85af79f4f4b44a9cc4a core/ncm_cache.h
|
||
811c730b5fc52256f84b8917b7657649 app/logic/appstate.c
|
||
fba1d01fe6f3cfc9bcf334481e8edb09 app/test/test_decode.c
|
||
0ed4ce768d82a9512c5d32bca1271366 test/test_ncm_core.c
|
||
c6846cb4c31161c42d81560ade628771 bench/player_flow_sim.c
|
||
791a9ddbde363348e3ef373931647aeb build_ncm.sh
|
||
f4fb97416868b9a61b6b54f8958bc6f9 build_ncm_host.sh
|
||
65e3da445996e62079bfa38a9c9699b7 VENDOR_NOTES.md
|
||
```
|
||
|
||
### 19.9 出入清单(只改 `patch_v2/ncm_app/` + 根 .gitignore)
|
||
|
||
```
|
||
新增:
|
||
vendor/dr_flac/dr_flac.h # dr_flac 0.13.4 单头(public domain/MIT-0;入库例外)
|
||
app/play/decoder.h/.c # 格式分发解码(magic:fLaC→dr_flac / 其它→minimp3;S16/S32 位宽映射)
|
||
app/play/dr_flac_impl.c # dr_flac 唯一实现 TU(DR_FLAC_NO_OGG;vendor 豁免告警)
|
||
修改:
|
||
app/play/player_manager.c # run_song 走 decoder(实际 rate/ch/bits/时长)+ 缓存键 id+档位 +
|
||
# flac 落盘(ext 按 type/url)+ flac 下载限幅 1GB + 旧无档键兼容
|
||
app/play/audio_out.c # write 改 const void* + S32 通路(AUDIOD/ALSA/WAV)+ 流死重开按 bits 还原
|
||
app/ncm_app_internal.h # audio_out_write 签名/注释
|
||
core/ncm_api.c/.h # song_url_auto 接受 flac(type 权威/url 扩展名兜底),降级语义不变
|
||
core/ncm_cache.c/.h # ext 前导点正名(单点)+ lookup 历史双点名回退
|
||
app/logic/appstate.c # autoplay 已缓存判定三键顺序(新档键 flac/mp3 → 旧无档键)
|
||
app/test/test_decode.c # 改走 decoder 分发 + expect_frames/rate/bits 校验 + meow_ui_time_ms 桩
|
||
test/test_ncm_core.c # 缓存命名正名 + 双点旧名兼容用例
|
||
bench/player_flow_sim.c # stub 换 ncm_dec_* + ncm_api_level_str/ncm_streq 桩 + P6(档位键/flac ext)
|
||
build_ncm.sh / build_ncm_host.sh # +decoder.c +dr_flac_impl.c + -Ivendor/dr_flac
|
||
VENDOR_NOTES.md # dr_flac 条目(来源/版本/许可/集成方式/实测)
|
||
.gitignore(仓库根) # vendor/ → vendor/* + !vendor/dr_flac/(单头入库)
|
||
ncm / ncm_host # 重编产物
|
||
(未动 sdk/、meow_audiod/、meow_launcherd/、其余 vendor/;未部署真机;未动设备系统/恢复区)
|
||
```
|
||
|
||
### 19.10 诚实出入 / 遗留
|
||
|
||
- **真机未部署未验**(留主会话统一)。真机验收点:①无损/Hi-Res 档位真实出 flac 流并出声
|
||
(48k/16 与 96k/192k/24 各试一首,日志 `play: start ... fmt=flac <rate>Hz/2ch/16|32bit` +
|
||
audiod grant 跟随原生率)②换音质档后同歌重下新条目、旧档仍在(LRU 未触发时)③旧缓存
|
||
(无档键/双点名)直接可播不重下 ④flac seek 精确帧。
|
||
- **无损档位要会员**:song_url 的 lossless/hires 档需 CVIP/SVIP——**用户账号能否真拿到
|
||
flac URL 待真机按账号实测**(非会员会逐级降到 exhigh mp3,行为同旧版,不算回归)。
|
||
- 八档音质体系(NCM_CACHE_DESIGN §3 的 jyeffect/sky/jymaster 等 SVIP 档)未进 App
|
||
(设置仍 5 档,枚举只到 NCM_LEVEL_HIRES)——缓存键已按档字符串泛化,后续加档零改动。
|
||
- dr_flac 只接原生 flac 封装(NO_OGG);song_url 若下发 ogg-flac 会被 sniff 判给 minimp3
|
||
报错「decode open failed」——目前未见该形态(type=flac 恒原生),真机若见再加 ogg。
|
||
- 96k/24 → S32 满幅在 qemu 下 29× RT,真机 MIPS 预估实时余量充足;若实测吃紧可开
|
||
`DR_FLAC_NO_CRC`(省 CRC 校验,~5-10% CPU)。
|
||
- 测试样本(out/test-vectors/)与宿主/ffmpeg 工具链(out/tools/)在 out/ 下不入库;
|
||
样本可用 python `soundfile` 一键重造(sweep 参数见 §19.7)。
|
||
|
||
## 20. 音质体系全量落地:八档 UI + VIP 检测/解锁 + 逐级降级下首恢复 + fee 门禁 + 默认极高 HQ(2026-09-01)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`。按用户定案 NCM_CACHE_DESIGN §3(八档 UI 权威)+ §2.1
|
||
> (VIP 缓存策略)+ CNMPLAYER_BIZ_NOTES §1/§2/§3(端点/9 档映射/fee 口径)实现。
|
||
> 风控纪律:本批真实 NCM 请求 **0 次**(vip 解析/降级链全部离线样本 + stub 矩阵)。
|
||
> **本批未部署真机**(部署留主会话统一做)。
|
||
|
||
### 20.1 档位模型(core/ncm_api.h)
|
||
|
||
- 枚举扩到全档表,**枚举值顺序 = 降级顺序**(高→低):
|
||
`SURROUND(8) > SKY(7) > JYMASTER(6) > JYEFFECT(5) > HIRES(4) > LOSSLESS(3) > EXHIGH(2)
|
||
> HIGHER(1) > STANDARD(0)`;`DOLBY=16` 映射保留但**不进 UI 清单、不进降级链**;
|
||
`HIGHER`(较高 192k)仅降级链中间档,不进 UI。
|
||
- UI 八档 ↔ level 串 ↔ 门槛(BIZ_NOTES §2.1 对齐 CNMPlayer 串;中文名以设计文档 §3 为准):
|
||
|
||
| UI 档 | level 串 | 规格 | 门槛 |
|
||
|---|---|---|---|
|
||
| 标准 | standard | 128kbps | 免费 |
|
||
| 极高 HQ | exhigh | ≤320kbps | 免费(**默认档**) |
|
||
| 无损 SQ | lossless | 48kHz/16bit | CVIP |
|
||
| Hi-Res | hires | 192kHz/24bit | CVIP |
|
||
| 高清臻音 Spatial | jyeffect | 96kHz/24bit | SVIP |
|
||
| 臻音全景声 Audio Vivid | sky | Audio Vivid | SVIP |
|
||
| 超清母带 Master | jymaster | 192kHz/24bit | SVIP |
|
||
| 沉浸环绕声 Surround | sky + `immerseType=c51` | 5.1 声道 | SVIP |
|
||
|
||
- ⚠️ sky 歧义处理(BIZ_NOTES §2.1 命名歧义的落地决策):CNMPlayer 无独立 Audio Vivid 档,
|
||
`sky` 是唯一串且其附加参数 `immerseType=c51` 恰为 5.1 声道 → **臻音全景声=裸 sky、
|
||
沉浸环绕声=sky+c51**(song_url 内按档附加)。两档 API 串相同 → 缓存键改用新
|
||
`ncm_api_level_key()`(sky / sky_c51 唯一后缀),杜绝两档互串缓存。
|
||
- `ncm_api_level_gate()`:standard/higher/exhigh=NONE,lossless/hires=CVIP,其余=SVIP。
|
||
|
||
### 20.2 VIP 检测与落盘(core vip/info + app/logic/account.c)
|
||
|
||
- `ncm_api_vip_info`(weapi):先 `/api/music-vip-membership/client/vip/info`(v2,{userId}),
|
||
失败/解析失败再 `/front/vip/info`(v1)兜底——**每个触发条件最多 2 次请求,不轮询**。
|
||
- 判定(`ncm_api_parse_vip_info`,离线可测):root=data 或 body 本身;
|
||
`associator`(黑胶)vipCode>0 / isSign / expireTime>now → **SVIP**;
|
||
`musicPackage`(畅听/音乐包)同三判据 + 顶层 redVipLevel/redplusLevel/musicVipLevel/
|
||
vipCode>0 或 vipStatus/isVip=true → **CVIP**;SVIP ⊇ CVIP。到期时间双字段原样记录。
|
||
- 落盘 `MeowData/dev.player.ncm/data/account.json`:{uid,nickname,vip,cvip_expire_ms,
|
||
svip_expire_ms,checked_at}(原子写;启动 account_load 恢复先用缓存渲染)。
|
||
- 刷新时机(account_vip_maybe_refresh,调用方 worker 线程上下文,UI 零网络):
|
||
①登录成功后(login worker,force)②checked_at 超 1 天 ③会员到期时间已过
|
||
(可能自动续费)——**每条件每次启动只查一次**(单飞标志,失败也置位:防风控,
|
||
等下次启动/下个条件)。home_worker 歌单拉取成功后触发 ②③ 判定。
|
||
- `account_vip()` 为纯本地视图:到期即按非会员对待;登出清 vip 态 + 删 account.json。
|
||
|
||
### 20.3 逐级降级 + 下首恢复 + 生效档显示
|
||
|
||
- `ncm_api_song_url_auto` 扩全档表:从 desired 逐档向下(surround→…→standard),
|
||
url 空/未知 type 继续降;**out_level 回填实际命中档**;响应 `data[0].level` 无歧义串
|
||
(standard..jymaster)且低于请求档时采信响应档(服务端静默降档如实显示),sky 族
|
||
歧义保守取请求档。新增 `ncm_api_song_url_auto_f`(try_fn 可注入)供离线 sim。
|
||
- 播放路径(player_manager song_local_file):用户档先按 `account_level_cap()` 钳顶
|
||
(NONE→EXHIGH / CVIP→HIRES / SVIP→SURROUND)再降级——**超权限档允许选定不硬拒,
|
||
但免去注定空 url 的高档请求**(风控)。缓存键仍=用户设定档(降级产物归用户档键,
|
||
「同设定永不重下」,§19 语义不变)。下一首从用户档重新起步(链内无记忆)。
|
||
- 生效档 `g_play.eff_level`(mu 保护):下载时由 auto 回填;缓存命中按 ext 推断
|
||
(mp3 实体+用户档>极高 → 必为降级产物标 EXHIGH;flac 实体按用户档——档内降级
|
||
如 hires→lossless 会高估,已知取舍,真实档位无损音质感官一致)。play/next 重置 -1。
|
||
- 播放页动态带时间行中央小字:生效档=用户档显示档名,不同则红字「已降级至 xx」
|
||
(g_last_eff 沿触发刷带)。
|
||
|
||
### 20.4 fee 权限门禁(BIZ_NOTES §3 社区口径)
|
||
|
||
- `song_local_file` 在**缓存查找之后、song_url 之前**:fee∈{1(VIP 才能听),4(付费专)}
|
||
且 account_vip()<CVIP → `NCM_ERR_VIP`(新错误码 -15)直接返回,**零 url 请求**
|
||
(省一次请求,防风控);播放页显示「VIP 专享,开通会员后可播放」+ snack「VIP 专享歌曲」。
|
||
- fee=0/8 放行(8=免费听低音质,播放允许);已缓存的 VIP 曲本地可播不拦
|
||
(NCM_CACHE_DESIGN §2.4:下架/权限变化歌曲优先本地缓存)。
|
||
- fee 字段本就在 ncm_song_t 且 songmeta bin 原始镜像序列化,盘缓存 round-trip 无需改格式。
|
||
|
||
### 20.5 UI(设置页八档 + 默认档)
|
||
|
||
- 设置页音质 sheet 八档(高→低排列),每项「名称 (规格) · CVIP/SVIP(·未解锁)」;
|
||
未解锁档**允许选定**(播放时降级),行 sub 标「当前: xx (规格) · 需 CVIP/SVIP,将降级」。
|
||
⚠️ meow_sheet 无单项置灰能力(SDK 不在本批范围),置灰以「·未解锁」文字标注代替。
|
||
- **默认档 Hi-Res → 极高 HQ**(EXHIGH):仅新装/越界值生效,已持久化的合法旧值原样保留。
|
||
- 页脚改「无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级」。
|
||
- 档位显示名/规格收敛 chrome.c(`ncm_level_name/ncm_level_spec`,settings/player 共用)。
|
||
|
||
### 20.6 验证记录(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| vip 解析 9 例 | test_ncm_core(离线合成 JSON:v2 SVIP/CVIP/NONE/已过期/仅 expireTime 有效/v1 扁平 root/redVipLevel/code=301) | **全 PASS**(宿主 + qemu-mipsel) |
|
||
| 档位表 | level_str/key/gate 断言(sky vs sky_c51 唯一性) | **PASS** |
|
||
| 降级链全矩阵 | **bench/degrade_sim.c**(新,auto_f 注入假 try_fn):desired 9 × avail 9 + 全不可用 9 = **90 例全 PASS**(命中档=min(desired,avail)、请求数=逐档不差一次);钳制(DOLBY→SURROUND/负数→STANDARD)、未知 type 跳过、type 缺失按扩展名收 flac、下首恢复用户档、服务端静默降档采信 | **ALL PASS** |
|
||
| 播放状态机 | player_flow_sim **P1-P8**:P7=VIP 钳顶(非会员用户档 SURROUND→请求档 EXHIGH/缓存键仍 1000_sky_c51/eff 回填;SVIP 直达);P8=fee 门禁(fee=1 非会员 NCM_ERR_VIP 零请求/缓存命中绕过/CVIP 放行/fee=8 放行);P1-P6 零回归 | **ALL PASS** |
|
||
| 滑窗/分页/缓存/封面/audiod | window_sim / pagination_sim / cache_evict_sim / cover_chain_sim / audiod_chain_sim(重编新 appstate.c/audio_out.c) | **全 PASS**(14/14 等) |
|
||
| 解码回归 | test_decode flac 44.1k/16 + 96k/24 + mp3(本批未动 decoder/audio_out,确认零回归) | **3/3 PASS** |
|
||
| mipsel 构建 | build_ncm.sh 自有代码 -Wall -Wextra | **0 warning**;text 962908(+8.4KB vs §19) |
|
||
| 宿主离屏 | build_ncm_host.sh + 6 页 MEOW_UI_DUMP | 全 rc=0;设置页布局核验(新行 sub/页脚生效) |
|
||
|
||
### 20.7 md5(本批产物 + 改动文件)
|
||
|
||
```
|
||
9d31f2241a62f618d7625a24f9f7a3a7 ncm # mipsel 静态 App(新;未部署,留主会话)
|
||
8a0a4ee03509c29ad7eb0ec9f61cd9db ncm_host # 宿主离屏调试二进制
|
||
2c1c459dbae23872557378477880d7a3 core/ncm_api.c
|
||
3d5fb722d5fdf5d9ac1f61e18450b586 core/ncm_api.h
|
||
e87f71c90175b67307d92d83d57b8886 core/ncm_common.h # +NCM_ERR_VIP(-15)
|
||
8e0b34b05f0063a4653236577e05cd47 core/ncm.h # 默认档注释
|
||
8bb15b7d9bd98f583761dab2260b98cd app/logic/account.c # 新:会员态缓存+刷新策略
|
||
3909d070d0290730739456ef0ab1e153 app/logic/appstate.c # 默认 EXHIGH/account_load/登出清理/level_key
|
||
f080c9f4823a5b59410445b5ccc4f191 app/play/player_manager.c # 钳顶+fee 门禁+eff_level
|
||
7f94bfbd6ce739a8a52273316ca2eb9d app/ui/settings.c # 八档菜单
|
||
baa6d5530764258ae9f93b6e4150e06e app/ui/player.c # 生效档小字+VIP 专享文案
|
||
bf776093956102989a43dfc778efdd01 app/ui/chrome.c # ncm_level_name/spec
|
||
771f351d82dbea46cc1d61c4bfd3bd41 app/ui/login.c # 登录后 vip 刷新
|
||
de6adb873dc426e4973f12866df21eeb app/ui/home.c # home worker 超期/到期刷新
|
||
6b6816096d1c26773f88a882f30fa65a app/ncm_app_internal.h # g_st.vip*/eff_level/声明
|
||
9f8a812f48d673514ae4b7145a6689c7 test/test_ncm_core.c # vip 解析+档位表用例
|
||
a5e76601554c8981457ab3a1987dcee2 bench/degrade_sim.c # 新:降级链 90 例矩阵
|
||
27b01f4bfd6199b62f6490367b47bd4b bench/player_flow_sim.c # P7/P8 + stub 更新
|
||
8af2179da8709b2b525e9744a887de79 build_ncm.sh # +account.c
|
||
ac5b29c40488f34e9220b14105bf63c2 build_ncm_host.sh # +account.c
|
||
```
|
||
|
||
### 20.8 诚实出入 / 遗留
|
||
|
||
- **真机未部署未验**(留主会话统一)。真机验收点:①登录后/超 1 天/到期三条触发路径
|
||
各只发一次 vip/info(日志 `account: vip refresh (<why>) -> vip=N`)+ account.json 落盘
|
||
②SVIP 档(jyeffect/sky/jymaster/sky_c51)按账号实测可用性逐级降 ③播放页「已降级至 xx」
|
||
小字 ④fee=1 非会员曲点播放直接「VIP 专享」且无 song_url 请求 ⑤新装默认档=极高 HQ。
|
||
- **sky 两档语义自研分配**(臻音全景声=裸 sky / 沉浸环绕声=sky+c51):BIZ_NOTES §2.1 命名
|
||
歧义无代码证据可消,真机若实测相反(如裸 sky 才是 5.1),只需对调 settings.c UI 表两项
|
||
的档位常量,零结构改动。Audio Vivid 若官方另有独立端点/参数则后续再补。
|
||
- 缓存命中的 flac 实体 eff 档推断可能高估(档内降级不可还原);VIP 档真实可用性以服务端
|
||
url 是否为空为准(门槛表只是省请求优化,服务端裁决权威)。
|
||
- vipType(login_status profile.vipType 10/11 口径)未用——BIZ_NOTES §1.2 标注非代码证据,
|
||
以 vip/info 双端点字段判定为准;真机若 vip/info 全挂可再加 profile.vipType 兜底。
|
||
- meow_sheet 无单项置灰(SDK 不在范围):未解锁档以「·未解锁」标注代替置灰。
|
||
|
||
## 21. 边下边播阶段 1+2:stream_src 流式源 + 全链 PCM 金标准仿真(2026-09-01)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`。按 devdocs/plan/NCM_STREAM_PLAY.md 方案 A 实现
|
||
> (F1-F7 实测事实为红线)。阶段 3(.part 断点续传 / LRU 防淘汰 / 失败重试 1 次)
|
||
> **本批明确不做**。风控纪律:真实 NCM 请求 **0 次**;**未部署真机**(留主会话)。
|
||
|
||
### 21.1 stream_src:只读流式字节源(app/play/stream_src.h/.c,新)
|
||
|
||
- 语义:对**下载中的缓存 .tmp 文件**提供 fread 语义只读视图——只读 fd + `pread`
|
||
(不动下载者文件位置),mutex/condvar 同步;状态 = written/final/failed/abort/read_pos。
|
||
- `stream_src_read_cb` 严格 fread 语义:**填满才返回**,真 EOF(final 且读到 written)
|
||
才短读;100ms timedwait 兜底醒(防 note_written 丢失信号死等)。
|
||
- 再缓冲迟滞:单次调用累计等待 >500ms 后,要求前方可用 ≥64KB 才放行(仅本次调用
|
||
尚未产出任何字节时下闸),避免慢网 1 字节一放行的抖动空转。
|
||
- 统计:underruns(**按「本 read 调用是否发生过等待」计一次**——原 `done==0&&avail==0`
|
||
口径被 minimp3 refill 模式绕过,实测 wait=4s 但 underruns=0,已修)/ wait_ms /
|
||
stalled(单次等待 >1s 置位,供播放循环联动 audio_out_pause)。
|
||
- dr_flac 适配三回调(read/seek/tell)。**SEEK_END 修正**:下载中(!final)按 dr_flac
|
||
约定返回 FALSE 跳过元数据总长校验;final 后按 final+offset 应答——此前 origin=2 落进
|
||
SEEK_CUR 分支导致 drflac_open 恒失败(/tmp/df_dbg.c 实证,修复后 44.1k/96k flac 均开成功)。
|
||
|
||
### 21.2 decoder 双路切回调(app/play/decoder.h/.c)
|
||
|
||
- 新 `ncm_dec_open_src(out, stream_src_t*, ...)`;`ncm_dec_open(path)` 改为包一层
|
||
静态 stream_src(src_owned=1,dec_close 连带释放)——本地文件路径零行为变化。
|
||
- mp3 = `mp3dec_ex_open_cb`(`MP3D_SEEK_TO_BYTE | MP3D_DO_NOT_SCAN`,io 存 decoder 内);
|
||
flac = `drflac_open(stream_src_drflac_read, dec_flac_seek, dec_flac_tell, src, NULL)`
|
||
(枚举与 drflac_int64 签名不兼容 → 静态适配器转换);格式 sniff 改 `stream_src_peek`。
|
||
- mp3 seek **不再关重开**:直接 `mp3dec_ex_seek`(F4:open_cb 下 SEEK_TO_BYTE 直达,
|
||
已下载区 seek 零等待)。
|
||
|
||
### 21.3 播放链(app/play/player_manager.c)
|
||
|
||
- `song_src_t{src, w, dl_thr, dl_rc, url, maxb, total}`:缓存未命中 → 起 dl_thread 跑
|
||
`ncm_http_download`(cb:abort 检查 → `ncm_cache_write` → `stream_src_note_written`)。
|
||
- 预缓冲:`stream_src_wait(128KB, 100ms)` 循环(still_current 护栏,切歌即 abort+join+
|
||
cache_abort);出口判据 = aborted 或 (failed 且 written<128KB) 才失败——**断流但已过
|
||
门槛 → 放行**(F5:短读即真 EOF,播已下载部分)。
|
||
- run_song 走 `ncm_dec_open_src`;解码循环 stalled 沿 → `audio_out_pause(1)`、恢复沿
|
||
`pause(0)`(停 PCM 时钟不丢流);underruns/wait_ms 统计变化才写 g_play。
|
||
- 曲尾 `song_src_dispose`:abort+join → dl_rc==OK 则 commit+evict,否则 cache_abort。
|
||
- g_play 新增 underruns/underrun_ms/stalled(app/ncm_app_internal.h;play/next 清零)。
|
||
|
||
### 21.4 HTTP/缓存底座(core/ncm_http.c/.h、core/ncm_cache.c/.h)
|
||
|
||
- P2:30s 绝对 deadline 拆为两段——连接/握手/请求/响应头维持 **15s 绝对钳**;
|
||
hdr_done 后启用 **body 空闲超时 15s**(`NCM_HTTP_BODY_IDLE_MS`,每 ret>0 顺延),
|
||
WANT_READ/WRITE 按 hdr_done 二选一——慢速长下载不再被总 deadline 误杀,真挂死仍 15s 断。
|
||
- ncm_cache writer 由 FILE* 改 fd(`open(O_WRONLY|O_CREAT|O_TRUNC)` + write(2) 循环
|
||
直写;commit = fsync+close+rename 原子落地);新增 `ncm_cache_writer_tmp_path()`
|
||
访问器(stream_src 按 .tmp 路径 pread,无需知道 writer 内部)。
|
||
|
||
### 21.5 验证记录(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 边下边播全矩阵 | **bench/stream_sim.c**(新:假网络线程[速率/抖动/cut40/flaky]→真实 ncm_cache→真实 stream_src→真实 decoder→audio_out null sink+PCM 捕获;金标准=同文件离线解码 memcmp):10MB/s、400kbps、160kbps × normal/seek_dl/seek_wait/cut40/flaky + flac_stream@400kbps = **16 用例** | 宿主 + qemu-mipsel 双平台 **ALL PASS**;正常/断续 PCM 与整首**逐字节一致**,断流与 40% 前缀逐字节一致 |
|
||
| 首开声延迟 | 同上矩阵实测 | 10MB/s=0.00s(宿主)/0.13s(qemu);400kbps=2.60s/2.69s;160kbps=6.54s/6.57s |
|
||
| 播放状态机 | player_flow_sim **P1-P8**(适配:ncm_dec_open_src stub、ncm_cache_open 真空 .tmp、commit 移曲尾 dispose → P7 半程先 stop 再等 commit 落地防 stub 污染断言) | **ALL PASS** |
|
||
| 解码回归 | test_decode 三样本(flac44=132300f/44.1k/16、flac96=192000f/96k/32、mp3=393984f/48k/16);另 `git show HEAD:` 取旧 decoder.c 编 test_decode_old,三样本 WAV 新旧**逐字节一致** | **3/3 PASS** + OLD-VS-NEW IDENTICAL |
|
||
| qemu-mipsel 解码 | test_decode_mipsel 三样本(重链 +stream_src) | **3/3 PASS**(112×/35.5×/3.2× RT) |
|
||
| core 回归 | build_core.sh → test_ncm_core_host + qemu-mipsel test_ncm_core_mipsel | **全 PASS**(含 vip 解析/档位表/缓存命名) |
|
||
| 链路回归 | cache_evict_sim / cover_chain_sim / audiod_chain_sim / degrade_sim / window_sim / pagination_sim / toggle_sim(全部重编新 ncm_cache.c/audio_out.c/appstate.c) | **全 PASS**(audiod 14/14 等) |
|
||
| mipsel 构建 | build_ncm.sh 自有代码 -Wall -Wextra | **0 warning**;text 969996(+7.1KB vs §20) |
|
||
| 宿主离屏 | build_ncm_host.sh + home/player/settings MEOW_UI_DUMP | 全 rc=0,播放页渲染核验正常(sdk meow_ui_vol.c 既有告警除外) |
|
||
|
||
### 21.6 ⚠️ 断言口径偏差记录(主会话定夺)
|
||
|
||
- 任务原断言「首开声 <2s@400kbps」与 **F3 红线物理冲突**:mp3 open_cb 强制预读
|
||
MINIMP3_IO_SIZE=128KB → 128KB/50KB/s = **2.62s 下限**,<2s 不可达。sim 实测 2.60-2.72s。
|
||
本批**未擅自偏离已批准方案**,按可达阈值断言(400kbps <4s,注释写明冲突)。
|
||
若要 <2s,选项:MINIMP3_IO_SIZE 减半至 64KB(→~1.31s,重扫 vendor 头文件默认值,
|
||
需复测全矩阵);或预缓冲门槛与 IO_SIZE 解耦(首 64KB 即开声,留给 decoder 内部 refill 等)。
|
||
- flaky 用例 underrun>0 由单用例硬断言改**跨速率聚合断言**:单次断续网是否打空缓冲
|
||
取决于解码速度(qemu 慢→缓冲消耗慢→宿主 1 次 qemu 0 次,皆正常),PCM 逐字节一致
|
||
才是硬判据;聚合断言(宿主=3/qemu=1)仍覆盖统计通路功能。
|
||
|
||
### 21.7 md5(本批产物 + 改动文件)
|
||
|
||
```
|
||
0dc39a6cf1115fb19644af3cba4c017d ncm # mipsel 静态 App(新;未部署,留主会话)
|
||
bd95bbaa0fc2ac3a60bce7bb7cc120ab ncm_host # 宿主离屏调试二进制
|
||
1385361ab09bf4c2ec1db0087451e5c2 app/play/stream_src.c # 新
|
||
1b803c7f9b70ad3df18689143a1b92a8 app/play/stream_src.h # 新
|
||
1b0197c6c4f27675e932024740aaeeab app/play/decoder.c # open_cb 双路 + ncm_dec_open_src
|
||
7fd0b09abbb2b75ab4c04e31211bedb2 app/play/decoder.h
|
||
fda5d4bb09bb26073b34986aa75cceee app/play/player_manager.c # 下载线程+预缓冲+stall 联动+曲尾 commit
|
||
62aeff05b618bc6838b81aadd6c7cebb app/ncm_app_internal.h # g_play underruns/underrun_ms/stalled
|
||
15c73a3ee5bc07ff2c26dbd26ff5e321 core/ncm_http.c # P2 空闲超时
|
||
b7ed6c165bbc42b36bde43fe634b1cec core/ncm_http.h
|
||
422181883426e1289115bb9ec6bae986 core/ncm_cache.c # fd writer + tmp_path 访问器
|
||
fe78a119bde44d57c4cae2e5b276c646 core/ncm_cache.h
|
||
ce7e1ece2577547d54f6c4cc1cf45bdf bench/stream_sim.c # 新:全矩阵+PCM 金标准
|
||
36abe7cdcf4681e18fed595fbc65435d bench/player_flow_sim.c # stub 适配+P7 时序修正
|
||
a26350ee298f63e828e06489fee35b92 build_ncm.sh # +stream_src.c
|
||
a80ee35f243ac28379cd1adeb61548ed build_ncm_host.sh # +stream_src.c
|
||
(test/test_ncm_core_host 重编产物随 build_core.sh 更新)
|
||
```
|
||
|
||
### 21.8 诚实出入 / 遗留
|
||
|
||
- **真机未部署未验**(留主会话统一)。真机验收点:①缓存未命中歌曲 2-3s 内开声
|
||
(400kbps 档约 2.6s)②弱网(限速 160-400kbps)播放不卡死、underrun 后自动恢复、
|
||
stalled 时暂停不走时钟 ③播放中切歌/退出:下载线程即 abort、无残留 .tmp ④断网半曲
|
||
播已下载部分后自然结束、不重复下载 ⑤完整下完的曲下次命中缓存秒开。
|
||
- **阶段 3 未做**(方案明确排除):.part 断点续传(断流曲下次仍从头下)、LRU 防淘汰
|
||
(下载中的 .tmp 可被 evict 边缘情形)、下载失败重试 1 次(现为 0 重试直接失败)。
|
||
- flac 大元数据块(大封面 PICTURE 块)open 时可能多等下载(drflac 需扫过元数据链才到
|
||
首帧);实测 44.1k 向量 400kbps 首开声 3.82s(vs mp3 2.60s)即此因。
|
||
- UI 菊花/缓冲提示未接 g_play.stalled(字段已备,UI 批再做)。
|
||
- 断续网 underrun 聚合断言为时序容忍口径(§21.6),非判据放松——PCM 金标准不变。
|
||
|
||
---
|
||
|
||
## 22. 播放页重做:封面页/歌词页双页 + MD3 矢量图标 + 点行跳转 + 背景模式(2026-09-01,按 NCM_CACHE_DESIGN §6 定案)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`。未动 `sdk/`(sb_icons.h 只作图标范式参照,未改)、`meow_launcherd/`、
|
||
> `core/`(缓存/解码/网络底座零改动)。风控纪律:本批真实 NCM 请求 **0 次**(封面/歌词/手势全走
|
||
> 宿主 mocK 与本地样本注入)。部署/真机验收留主会话统一。上一批(§21 边下边播)已 commit `9e25db7`,
|
||
> 本批在其上做。
|
||
|
||
### 22.1 设计定案(NCM_CACHE_DESIGN §6)与实现映射
|
||
|
||
| 定案 | 实现 |
|
||
|---|---|
|
||
| 去掉现有标题栏(播放页不再用 ncm_topbar) | 删除 `player.c` 的 `g_tb`/topbar 创建与 `MEOW_TOPBAR_BACK` 命中;返回 = 左缘手势(`SWIPE_LEFT`→`app_on_swipe`→`page_pop`)/ 系统返回钮语义,`app.c` 不改 |
|
||
| 双页结构:封面页/歌词页,屏幕正中左/右滑切换 | 新增内部页 `g_ppage`(0=封面、1=歌词);`player_set_page/player_page/player_page_go`;页面指示点(2 点,当前页红色) |
|
||
| 手势分流(左缘≤24px=返回、顶部=通知、中部=页切换) | 不抢 SDK 左缘(`PLR_EDGE_W=24` 与 SDK `EDGE_W` 一致)与顶部热区(`y>=statusbar_hotzone()`);中部横滑用 `on_touch_down/move` 锁存 `page-drag` + `on_tap`(中部左滑落 TAP)判向 → 切页;中部右滑由 SDK `on_swipe(RIGHT)` → 切页(左滑/右滑都覆盖) |
|
||
| 封面页:方形大图+圆角,无旋转动画;进度条(可点 seek)+控制钮+元信息+实际音质档 | `meow_ui_img_cover` 方形 + `round_corners_bg` 四角裁圆角;进度条点击 seek;控制钮 = MD3 图标;元信息(歌名/歌手)+ 实际生效档/降级标注 |
|
||
| 按钮全部用 MD3 矢量图标(32×32 alpha,参照 sb_icons.h 范式) | 新增 `app/ui/ncm_icons.h`(PREV/PLAY/PAUSE/NEXT 四枚 32×32 单通道 alpha,几何近似+8× 超采样抗锯齿,参照 sb_icons.h 存储范式);`draw_md3_icon` 最近邻缩放 + 逐像素 `fill_rect_alpha` |
|
||
| 歌词页:LRC 滚动(当前行居中高亮、上下渐隐)+ 点行跳转 | 沿用 `lyric.c`;新增 `lyric_line_time(i)`(点行 seek 用);当前行 `size=24` 红高亮、上下行 `size=20` 按距中心 `mix_c` 渐隐;点行 → `player_seek(lyric_line_time(i))` |
|
||
| 歌词页默认=封面模糊置底;省电=封面背景+黑色半透明叠加层(设置 `ncm.player_bg_mode`) | 默认:封面 `meow_ui_img_scaled` 铺满 + 主题 `th->scrim` α150;省电:铺满 + `0x000000` α195;设置页新增行「歌词页背景」sheet 二选一,持久化 `ncm.player_bg_mode` |
|
||
| 日夜间:主题配色只映射到半透明叠加层,不动沉浸底 | 新增 `NCM_PLAYER_BG` 固定沉浸底;`ncm_on_theme` 在 ACT_PLAYER 时 page_bg 恒 `NCM_PLAYER_BG`(不改底),其余页面仍跟随系统明暗;歌词 scrim/accent 随 `g_dark`/主题变化 |
|
||
| 缓冲「缓冲中…N%」/错误条(256 那套)沿用 | 封面页:缓冲文案替时间行、错误文案(VIP/无音源/登录过期/网络)+「点播放键重试」;`ncm_snack` 错误沿沿用 |
|
||
| 退出=后台播放(M4 background_audio,退出语义不动) | `app.json` 已声明 `background_audio:1`;播放线程/退出路径未动 |
|
||
|
||
### 22.2 性能(分带刷新纪律沿用)
|
||
|
||
- **封面页**拆「封面静态带 `g_cover_r`(z8)」+「动态带 `g_dyn_r`(z9)」两个绘制物,bbox 互不重叠:
|
||
tick 只标脏动态带(进度/控制/元信息/指示点);封面带仅 歌曲/封面 变化(`player_static_invalidate`)
|
||
才重画 → 不整页重绘。
|
||
- **歌词页**为整屏带状物:仅在 进入/切歌/封面变化/换背景模式/换行(~5s 一次,歌词按行进非逐帧)
|
||
时整屏重画;pos tick 不重画歌词页(`g_last_line` 沿驱动,行间静止)。
|
||
- 模糊底图 = 封面按屏最近邻铺底 + scrim;区域重画时 SDK 绘制 clip 收敛(`meow_ui_draw.c`,
|
||
`g_clip_stack`),只算相交区,不整屏 overdraw(§5.8 记忆点)。
|
||
- 控制钮由「逐帧行扫三角」改为 **MD3 图标位图 blit**(扫描 + 每像素 blend565),code 更简单、观感对齐 Material。
|
||
|
||
### 22.3 验证记录(0 真实 NCM 请求)
|
||
|
||
| 层 | 手段 | 结果 |
|
||
|---|---|---|
|
||
| mipsel 交叉 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 963042 / data 1608 / bss 1113988`(bss 增量仅来自 icons/ncm 段,均常量) |
|
||
| 宿主离屏 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | 自有代码 0 warning(仅 SDK `meow_ui_vol.c` 既有 `system()` 告警) |
|
||
| 四页 PPM 核验 | `NCM_PAGE=plcover/pllyric × NCM_THEME=light/dark` + `NCM_COVER=本地样本注入` | **4 页全 rc=0**:封面页=方形圆角封面+歌名/歌手+时间+红进度条(52%)+prev/暂停/next MD3 图标+指示点;歌词页=封面模糊置底+顶部「歌名·歌手」+当前行红高亮居中+上下渐隐+指示点;日/暗两套底恒 `NCM_PLAYER_BG`(不动沉浸底)、accent/scrim 随主题 |
|
||
| 缓冲/错误态 | `NCM_PAGE=playerbuf/playererr` | 全 rc=0:缓冲=「缓冲中… N%」文案+空进度条+暂停钮;错误=错误文案+「重试/切下一首」+播放钮(STOPPED→play 重试) |
|
||
| 省电背景 | `NCM_PAGE=pllyric NCM_BGMODE=power_save` | 全 rc=0:封面背景更暗(`0x000000` α195),与 default 明显区分 |
|
||
| 手势切换自检 | `NCM_PAGE=gestest`(宿主直调 touch_down/move/on_tap/on_swipe) | **PASS**:p0=0 → 中部左滑=1(歌词) → 中部右滑=0(封面);进度条上按下拖拽不锁存(不被误判翻页) |
|
||
| qemu core 回归 | `timeout 120 qemu-mipsel ./out/test_ncm_core_mipsel` | **RESULT: PASS**(core 全量,本批未动 core) |
|
||
| 宿主 core | `test/test_ncm_core_host` | **PASS** |
|
||
| 设置页 | `NCM_PAGE=settings` | rc=0,6 行(新增「歌词页背景」行存在) |
|
||
|
||
### 22.4 md5(本批产物 + 改动文件)
|
||
|
||
```
|
||
730e0b6eaa0b3f626f828bbaa51dc5a8 ncm # mipsel 静态 App(新)
|
||
500ac859618a58bc2a641553a383a10f ncm_host # 宿主离屏调试二进制(新)
|
||
00934dec4452f2f31e6f473c64bd311f app/ui/player.c
|
||
20f0bbac74099d07638d4f92a340eb82 app/ui/ncm_icons.h # 新:MD3 prev/play/pause/next 32x32 alpha
|
||
aedd5ff24e227eeda94341e63eac4064 app/ui/settings.c
|
||
31ae666b0f7497bb3c3391da9ce4636a app/ui/ncm_colors.c
|
||
b1c8f029886a60288e96bcf54476d7bf app/logic/lyric.c
|
||
63127b670604ff67dd176224088b16c4 app/logic/cover.c
|
||
aa5a5f22e10662c2247ece166c13ea6e app/logic/appstate.c
|
||
e2a7062b0033234649fe03767a4c009d app/app.c
|
||
fa00ea4f0093cd7e0a2600887106de07 app/ncm_app_internal.h
|
||
2c484e5e920c0b05c6da107d15fd8a6f app/test/test_ncm_app.c
|
||
```
|
||
|
||
### 22.5 出入清单(只改 `patch_v2/ncm_app/`)
|
||
|
||
```
|
||
修改:
|
||
app/ui/player.c # 播放页重做:去 topbar + 封面/歌词双页(中滑切换) + MD3 图标控制钮 +
|
||
# 方盖圆角 + 点行 seek + 封面模糊/省电背景 + 分带刷新 + 手势分流
|
||
app/ui/ncm_icons.h # 新增:MD3 prev/play/pause/next 32x32 单通道 alpha 位图(参照 sb_icons.h)
|
||
app/ui/settings.c # 新增「歌词页背景」行(default/省电 sheet 二选一)
|
||
app/ui/ncm_colors.c # ncm_on_theme:ACT_PLAYER 沉浸底恒 NCM_PLAYER_BG(不动沉浸底)
|
||
app/logic/lyric.c # 新增 lyric_line_time(i):点行跳转
|
||
app/logic/cover.c # 宿主测试桩 cover_host_inject(#ifdef MEOW_UI_HOST_TEST,产品不编)
|
||
app/logic/appstate.c # 设置持久化 ncm.player_bg_mode(load/save + 越界回落 default)
|
||
app/app.c # on_touch_down/move 播发页路由(中部横滑页切换跟踪)
|
||
app/ncm_app_internal.h # NCM_PLAYER_BG/NCM_BG_* + player_bg_mode 字段 + player_set_page/
|
||
# player_page/player_touch_* + lyric_line_time 声明
|
||
app/test/test_ncm_app.c # 宿主:plcover/pllyric/gestest 模式 + NCM_BGMODE/NCM_COVER 注入
|
||
ncm / ncm_host # 重编产物
|
||
(未动 sdk/、meow_launcherd/、vendor/、core/;REPORT.md 本批记录 §22)
|
||
```
|
||
|
||
### 22.6 诚实出入 / 已知限制
|
||
|
||
- **真机未部署未验**(留主会话统一)。真机验收点:①双页中滑流畅(跟手感/指示点)②点行 seek
|
||
命中时间 ③封面模糊置底观感(默认)vs 省电黑色叠加(对比明显)④设置页「歌词页背景」持久化、
|
||
重启生效 ⑤返回=左缘右滑仍正确(不误触页切换)⑥播放中退出=后台播放(M4,未动)。
|
||
- **歌词页「当前行居中」为行进式**(按 LRC 行切换,非逐帧平滑滚动):这是 MIPS 软帧下
|
||
「流畅性廉价可靠」取舍(§11);当前行高亮 + 上下行渐隐已提供视觉焦点。
|
||
- **封面模糊 = 封面按屏最近邻铺底 + scrim(廉价近似)**:无 GPU 模糊/降采样原语,非真高斯模糊;
|
||
非均匀铺底(480×720)会轻微拉伸封面(居中裁方可解但需另建 thumbnail 缓冲,本批不做)。
|
||
- **歌词页顶部元信息/整屏重在换行时重画**:换行频率 ≈ 每行(数秒)一次,非逐帧;pos tick 不刷
|
||
歌词页(只在其换行时刷),故 MIPS 上歌词页代价可忽略。
|
||
- **页面对比色固定浅色系**(播放页为沉浸深色介质,文本若随主题 onSurface 在浅色主题下为近黑会
|
||
隐形):这是「不动沉浸底」定案的必然——播放页文本/控件用固定浅色,仅 accent(红)/scrim 随日夜间。
|
||
- **`player_set_screen` 保留为兼容占位(恒竖屏)**:重做后播放页为竖屏双页(旧「横屏布局」随顶栏
|
||
⟳ 钮一并退役);`player_screen()` 恒 0,宿主 `grid`/`lands` 旧模式不再有横屏布局。
|
||
- **歌词读锁残余(既有)**:`lyric_line_text/tl` 解锁后返回静态指针可能被下次 `lyric_parse` 覆盖
|
||
(§14.7 已知);本批未改,点行跳转读 `lyric_line_time` 同理(写入仅在切歌瞬间,窗口极小)。
|
||
|
||
---
|
||
---
|
||
|
||
## 23. 搜索增强:历史 + 单曲/歌单分类 + 分页 + 误触修复 + 不覆写歌单上下文(2026-09-01,agent-273)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`。定案见 `devdocs/plan/NCM_CACHE_DESIGN.md §5`(历史+自动聚焦+「单曲/歌单」两类起步)
|
||
> 与 `NCM_UI_REVIEW.md §2.4/§3-2`(搜索缺什么 / 点行误触)。风控:本批 0 次真实 NCM 请求(全离线 stub/
|
||
> 宿主 mock/离线样本);真机部署留主会话统一。上一批(播放页重做 §22)已 commit,本批在其上做。
|
||
|
||
### 23.1 搜索历史(进页自动弹键盘 + 本地持久化)
|
||
|
||
- **进页自动弹键盘**:`search_open`/`sr_page_enter` 后若 `g_field && search_query` 为空 → `meow_textfield_activate`。
|
||
`#ifndef MEOW_UI_HOST_TEST` 包住(宿主离屏为阻塞式模态 IME,避免挂起 dump;真机走阻塞式弹键盘——与既有
|
||
「点输入框唤 IME」同构)。
|
||
- **持久化**:`appstate.c` 新增历史函数族(`appstate_search_history_add/get/count/remove/clear`),内存数组
|
||
`g_hist[12][128]`(静态,bss,最小)+ 单键 `meow_config`(`ncm.search_history`,条目以 `\n` 连接)。
|
||
语义:**去重 + 最新在前 + 截断 ≤12**(用户定案)。`search_submit` 成功提交即 `add(kw)`。
|
||
- **空态=历史列表**:`sr_view()` 在 `search_query` 为空且历史非空 → `SR_HIST`:列表行=历史(点=直接 `search_submit`)、
|
||
右缘「清空全部」按钮、左滑删单条(`search_swipe_left`,见 23.4)。历史为空回落「输入关键词」空态。
|
||
|
||
### 23.2 结果分类「单曲/歌单」+ 独立页签
|
||
|
||
- 顶部小页签(`r_tabs[0/1]`,各 `r_btn.w/2-6`):单曲(type=1)/歌单(type=1000)。选中=品牌红下划线高亮。
|
||
- **两页签各自独立结果集与滚动位置**:`g_st.search_songs[]`(单曲,`NCM_SEARCH_SONG_MAX=256`)与
|
||
`g_st.search_pls[]`(歌单,`NCM_SEARCH_PL_MAX=60`)互不干扰;滚动各存 `g_scroll_song`/`g_scroll_pl`,
|
||
`sr_set_tab` 先存旧再恢复新。非当前页签在提交新查询时被清空(`g_tab^1`),切过去懒加载(用户动作)。
|
||
- 单曲行点击 → `player_play_search_song(i)`(搜索队列起播);歌单行点击 → `playlist_open_pl(&g_st.search_pls[i])`
|
||
(进该歌单详情,见 23.5)。
|
||
|
||
### 23.3 加载更多(触底分页)
|
||
|
||
- `appstate_search_count(tab)` 即该页签已加载条数(=下次 offset);`appstate_search_has_more(tab)` 判还有下一页。
|
||
- 触底(`scroll + r_list.h >= count*itemH - itemH*2`)且 `more & !g_thr_on` → `search_fetch(tab, offset=count, 0)`。
|
||
cloudsearch 同参数 `limit=NCM_SEARCH_PAGE(30)`、`offset` 累加;每类各记各的 `offset`(`search_song_offset`/
|
||
`search_pl_offset`,appstate append 回填)。
|
||
- `has_more` 判定:`total>=0 ? (count<total) : (本页==limit)`;结果数组到上限仍按服务端总数反映(UI 层
|
||
`n>=cap` 停止拉取,内存护栏独立于服务端 total)。
|
||
|
||
### 23.4 误触抑制(NCM_UI_REVIEW §3-2)
|
||
|
||
- `search_on_tap` 点行套用 §13.3 修法(对齐 home/playlist):`was_drag = (g_press_delta > 10)`;
|
||
`row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(...)`;`row<0 || was_drag` 不点行。
|
||
`g_press_row`/`g_press_delta` 在 `search_touch_down/move` 已记录(沿用),历史模式同样生效。
|
||
|
||
### 23.5 结果不覆写歌单上下文(核心解耦)
|
||
|
||
- **搜索结果改独立数组**:`search_worker` 不再写 `g_st.songs[]` 也不再 `appstate_adopt_songs`;单曲结果写
|
||
`g_st.search_songs[]`(appstate 持 `g_search_mu`,经 `appstate_search_append_songs` 提交),**g_st.songs[]/
|
||
song_ids/track_ids_count(歌单窗口)永不被搜索触碰**。
|
||
- **播放搜索单曲 = 建立「搜索队列」上下文**:`player_play_search_song(i)` 设 `g_st.queue_src=NCM_QUEUE_SEARCH`
|
||
→ `appstate_queue_count()/queue_song()` 改读 `search_songs[]`(`player_manager.c` 的
|
||
`run_song/next/player_thread/lyric_worker` 全部改走 `appstate_queue_*`,按 queue_src 分派窗口或搜索)。
|
||
`player_manager_next`/`player_thread` 边界 = `appstate_queue_count()`。**从歌单详情退到搜索页时原歌单窗口
|
||
不丢**(`track_ids_count/song_ids` 未被改写)。
|
||
- **歌单结果进详情**:新增 `playlist_open_pl(const ncm_playlist_t*)`——拷入 `g_st.search_pl_open`、
|
||
`active_playlist=NCM_PL_SEARCH` 哨兵;`appstate_load_playlist_songs_page`/`appstate_apply_playlist_cover`/
|
||
`pl_cur`(playlist.c)对哨兵改读 `search_pl_open`,复用既有滑窗/封面/分页链路,不污染真实歌单列表。
|
||
|
||
### 23.6 core:cloudsearch 扩展(type/offset/limit/total)
|
||
|
||
- `ncm_api_cloudsearch_song`(type=1)/`ncm_api_cloudsearch_playlist`(type=1000):共用
|
||
`cloudsearch_post` POST(`s/type/offset/limit/total:true`)+ `cloudsearch_count`
|
||
(读 `result.songCount`/`result.playlistCount`,缺省 -1)。新增 `parse_playlists_array`(id/name/coverImgUrl/
|
||
trackCount)。旧 `ncm_api_cloudsearch` 保留为单曲 wrapper(type=1, offset=0)。
|
||
|
||
### 23.7 验证记录(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 历史去重/截断/最新在前 | `bench/search_sim.c`(离线,驱动真实 appstate 历史函数) | **PASS**:去重 2 条;重add 最新在前;截断 ≤12(kw16 在顶、kw8 在 idx8 顺序保留);remove 前移;clear |
|
||
| 单曲/歌单独立结果集 | `bench/search_sim.c` | **PASS**:song 集 4/pl 集 2 互不干扰;clear song 留 pl;song/pl copy 正确 |
|
||
| 翻页 offset/has_more | `bench/search_sim.c` | **PASS**:clear 复位 offset=0/more=1;page1 30→offset 30;累计到 total 240→more=0;clamp 至 SONG_MAX 且 total>cap→more=1;无 total(-1)回落页大小判 more |
|
||
| 搜索队列上下文(不覆写窗口) | `bench/search_sim.c` | **PASS**:queue=playlist 7/queue=search 3;queue_song 读搜索集;`track_ids_count/song_ids/song_valid` 原样 |
|
||
| 误触抑制 | `app/test/test_ncm_app.c` searchtap(真实页面+真实 SDK 手势) | **PASS**:干净点击→播按下行(cur=0,qsrc=SEARCH);拖动(+30px>10)→was_drag 抑制(cur -1→-1) |
|
||
| core 回归 | `test_ncm_core_host` / `qemu ./out/test_ncm_core_mipsel` | **PASS** |
|
||
| mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 972518 / data 1608 / bss 1364548`(bss +~1MB 来自 search_songs/pls 独立数组) |
|
||
| 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | 空态=历史列表(searchhist 3 行+清空区)/结果态两类页签(search 单曲 3 行下划线在左、searchpl 歌单 2 行下划线在右)均正常,无崩溃 |
|
||
|
||
### 23.8 md5(本批改动文件 + 产物)
|
||
|
||
```
|
||
30b24372ab9833c0c49f97be3b9418ee ncm # mipsel 静态 App(新)
|
||
ca3b451c0be5e5ead7f215cb8d431594 ncm_host # 宿主离屏调试二进制(重编)
|
||
b22cc6f5696d778c1a93adcf39ed4847 core/ncm_api.c
|
||
14ddc89bf1e1b20afc3c7008377ba928 core/ncm_api.h
|
||
330f131d42b9f70ab8792ace2f1a7197 app/ncm_app_internal.h
|
||
1e02f90fdd9aae0b3e725548874b19f9 app/logic/appstate.c
|
||
21a80495141cec6506e9d5197e56c937 app/ui/search.c
|
||
666e18381a6b284b6bb4b1b92c056fec app/ui/player.c
|
||
63485f230aca0f20d7bf0a977b36000c app/ui/playlist.c
|
||
cd0d99b31f56df698c44e3fbeace95fc app/play/player_manager.c
|
||
46d9a5f16e257709010fba79ffe44a61 app/app.c
|
||
060bc58975f6f56f6e2a6e87c23b3a1e app/test/test_ncm_app.c
|
||
c9c115cda1340b92d1988e330eafbb5b bench/search_sim.c
|
||
```
|
||
|
||
### 23.9 出入清单(只改 `patch_v2/ncm_app/`)
|
||
|
||
```
|
||
修改:
|
||
core/ncm_api.c/.h # cloudsearch 扩展:type/offset/limit/total + 歌单解析 + parse_playlists_array
|
||
app/ncm_app_internal.h # 搜索常量/状态字段/search_pl_open/queue_src + 新函数原型 + (去 NCM_SEARCH_MAX)
|
||
app/logic/appstate.c # 搜索历史(≤12去重) + 独立结果集(append/clear/count/offset/has_more) +
|
||
# 播放队列来源(queue_*) + NCM_PL_SEARCH 哨兵支持 + 默认 queue_src=PLAYLIST
|
||
app/ui/search.c # 重写:历史(点搜/左滑删/清空) + 单曲/歌单页签 + 触底分页 + 误触抑制 +
|
||
# 进页自动弹键盘 + 结果改走 appstate 独立数组
|
||
app/ui/player.c # player_play_song(队列=PLAYLIST) + player_play_search_song(队列=SEARCH) +
|
||
# cur_song() 按 appstate_queue_count
|
||
app/ui/playlist.c # pl_cur() 助手 + playlist_open_pl(搜索歌单) + 封面/标题哨兵处理
|
||
app/play/player_manager.c # 播放循环/next/歌词 worker 改走 appstate_queue_count/queue_song
|
||
app/app.c # LEFT 左滑先发 search_swipe_left(历史删单条),未消费才返
|
||
app/test/test_ncm_app.c # 宿主:search/searchhist/searchpl/searchtap 场景 + mock 改独立数组
|
||
新增:
|
||
bench/search_sim.c # 搜索数据层离线回归(历史/独立集/翻页 offset/队列上下文;0 网络)
|
||
REPORT.md # 本批记录(§23)
|
||
(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机/动设备系统/恢复区)
|
||
```
|
||
|
||
### 23.10 诚实出入 / 已知限制
|
||
|
||
- **真机未部署未验**(留主会话统一)。真机验收点:①进搜索页自动弹键盘(拼音 IME 候选上屏)②输入关键词
|
||
→点「搜索」出单曲、切「歌单」页签出歌单、滚动触底翻页 ③点搜索单曲播放、上/下一首在搜索结果集内循环、
|
||
返回歌单详情原歌单不丢 ④空态历史列表:点=直接搜、左滑删单条、右缘清空全部 ⑤快速横滑列表抬手不误播。
|
||
- **歌单结果进详情走 `NCM_PL_SEARCH` 哨兵(active_playlist=-2)**:极少数「歌单列表恰好满 120 且正在浏览」的
|
||
场景下此哨兵不与真实下标冲突(哨兵为负,永不在 [0,120) 内);`pl_cur()` 统一分派,未污染的边界已由
|
||
bench [4] 的「窗口原样」校验覆盖。
|
||
- **`appstate_adopt_songs` 现为不可达死代码**(搜索结果不再收编窗口):保留(公开函数,`-Wall -Wextra` 不告警),
|
||
供既有单测/文档引用;后续可清理。
|
||
- **历史持久化单键 `ncm.search_history`(条目以 `\n` 连接)**:搜索词不含换行(IME 不产生);若将来支持
|
||
粘贴含换行文本需改定界(本项目无粘贴,当前安全)。
|
||
- **两页签懒加载**:提交查询只拉当前页签;切到另一页签(其结果为空)才发该页签请求——属用户动作
|
||
(点页签),但严格讲不是「点搜索钮」;已避免进页自动搜/逐键发。风控更严可直接在 `search_submit` 双拉。
|
||
- **`meow_textfield_activate` 为阻塞式模态 IME**(SDK 既定):进页自动弹键盘会阻塞 UI 直至用户关闭键盘,
|
||
与「点输入框唤 IME」一致(既有交互模式);宿主离屏以 `#ifndef MEOW_UI_HOST_TEST` 跳过以免挂起 dump。
|
||
- **未动 sdk/、meow_launcherd/、vendor/、core(除 cloudsearch 扩展)**;部署链路同 §13.5 留主会话。
|
||
|
||
---
|
||
|
||
## 24. 边下边播阶段 3:.part 断点续传 + LRU 防淘汰 + 失败重试 1 次(2026-09-01)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`。按 devdocs/plan/NCM_STREAM_PLAY.md §8 阶段3 任务书实现
|
||
> (§5.1 LRU 防淘汰、§5.2 .part 续传、§4.1/§7 重试风控)。阶段 1+2 在 §21(commit `9e25db7`)。
|
||
> 风控纪律:真实 NCM 请求 **0 次**(全离线 stub/样本);**未部署真机**(留主会话统一)。
|
||
> 对外文档(APPS.md/README 等)称「流媒体客户端」,不写「网易云/NetEase」。
|
||
|
||
### 24.1 三件事
|
||
|
||
1. **.part 断点续传(§5.2)**:下载中断(切歌/断流/失败)后 `.tmp` 保留为 `song_<id>.<ext>.part`;
|
||
下次播放同曲走 `Range: bytes=<part_size>-` 一次续传:206=追加、200=服务器忽略 Range(body 头段与
|
||
.part 前缀重复,丢弃)、416/其它=放弃续传(删 .part)整首重下。URL 每次新拉(签名会过期,
|
||
风控红线「一首一 GET」仍满足)。
|
||
2. **LRU 防淘汰(§5.1)**:`ncm_cache_evict`/`ncm_cache_total_bytes` 扫描跳过 `.tmp`/`.part`
|
||
——写穿缓存态的 `.tmp`(边下边播全程都在播)与中断残体 `.part` 归写者管理域,不被缓存上限挤掉。
|
||
3. **下载失败重试(§4.1/§7)**:预缓冲(<128KB)前网络失败 → 整首重试 **1 次**(STREAM_RETRY_MAX=1),
|
||
重试前退避 **800ms**(STREAM_RETRY_BACKOFF_MS)防连环轰炸;重试每次重新 `song_url`(新签名)。
|
||
硬错误/切歌/退出不重试(安静放弃)。开播后失败不自动重连(播到截断点,§4.1)。
|
||
|
||
### 24.2 改动文件
|
||
|
||
- `core/ncm_cache.c/.h`:`is_partial_name`(evict/total 跳过 .tmp/.part);`ncm_cache_part_size`
|
||
/`ncm_cache_part_remove`/`ncm_cache_open_resume`(O_APPEND 追加 .part)/`ncm_cache_abort_part`
|
||
(.tmp→.part 保留);`ncm_cache_abort` 语义不变(封面/测试仍用)。
|
||
- `core/ncm_http.c/.h`:`ncm_http_download_status`(parse_headers 后、首 body 前把 `resp->status` 写入
|
||
`*status_out`,供续传回调预知 200/206/416)+ 放行 206。
|
||
- `app/play/stream_src.c/.h`:`stream_src_open_stream_at(out,path,written)`——续传时读端 written 置为
|
||
.part 前缀大小、read_pos=0,前缀秒读(F4/F5 语义复用)。
|
||
- `app/play/player_manager.c`:`song_src_t` 增续传字段(resume_base/first_body/st_ptr/need_fresh/skip);
|
||
`stream_dl_cb` 首 body 按 status 切写姿态;`dl_thread` 带 Range 头 + 状态感知下载;
|
||
`song_download_attempt`(单次下载尝试:.part 检测+续传/整首、预缓冲、失败分类)+ `song_local_file`
|
||
重试循环(≤1 次、退避 800ms);`song_src_dispose` 中断保留 .part、拒绝续传(need_fresh)删残体。
|
||
|
||
### 24.3 验证记录(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| .part 续传 PCM 金标准 | **stream_sim 增 resume 用例**(会话1 中断 40% 存 .part → 会话2 续传:读端从头跟播、commit 后完整==源)@10MB/s+@400kbps | 宿主 + qemu-mipsel **ALL PASS**:续传 PCM 与整首离线解码**逐字节一致**(1575936B)、commit==源(328320B)、`.part` size 正确(131328)、first_sound=0.00s(前缀秒开) |
|
||
| 边下边播全矩阵回归 | stream_sim 原 16 用例(normal/seek_dl/seek_wait/cut40/flaky × 3 速率 + flac_stream) | 宿主 + qemu 全 PASS(PCM 金标准不变) |
|
||
| LRU 防淘汰 | **cache_evict_sim 新 case7**(塞爆池 + 在播 `.tmp`/残体 `.part`)| PASS:`.tmp`/`.part` 存活、不计入 total、最旧正式文件被淘汰 |
|
||
| 续传 API 契约 | **cache_evict_sim 新 case8**(part_size/open_resume 追加/commit 转正/abort_part 保留/abort 删除/part_remove) | PASS(0 警告) |
|
||
| 续传控制流(Range/状态分支/重试) | **player_flow_sim 新 P9/P10/P11**:P9=Range 头 + 206 追加 commit;P10=416→删 .part 整首重下(重试无 Range);P11=预缓冲前网络失败→退避序列+重试 1 次后停播 | 宿主 + qemu **ALL PASS**(P1-P11;P11 实测 2nd download +862ms ≥700ms 退避) |
|
||
| core 回归 | build_core.sh → test_ncm_core_host + qemu test_ncm_core_mipsel | PASS(缓存命名/字典/会员态) |
|
||
| mipsel 构建 | build_ncm.sh 自有代码 -Wall -Wextra | **0 warning**;text 975234(+5.2KB vs §21 的 969996) |
|
||
| 宿主离屏 | build_ncm_host.sh + NCM_PAGE=home MEOW_UI_DUMP | rc=0,渲染正常(sdk meow_ui_vol.c 既有告警除外) |
|
||
|
||
### 24.4 本批顺手修(预存 bug,诚实记录)
|
||
|
||
- **`gb_to_bytes` 返回 `int` 溢出**:10/100GB(10×2^30=10737418240)超 int 上限 → 默认 10GB 上限实际
|
||
溢出成 0/巨大值,`ncm_cache_evict` 永不触发(缓存上限设置失效)。改 `uint64_t` 修真——这是阶段3
|
||
「被缓存上限挤掉在播文件」语义能成立的前提(上限真生效,靠 .tmp/.part 跳过实现防淘汰)。
|
||
cache_evict_sim case7/8 已复盖;test_ncm_core/cache_evict_sim 旧用例零回归。
|
||
- **player_flow_sim P7 SVIP 子用例遗漏 `g_eof=0`**(上一步设了 1 让 song0 收尾,SVIP song1 未重置 → 首帧
|
||
EOF、PLAYING 窗口过短)——本批修正(测试自身 bug,不影响生产代码)。
|
||
|
||
### 24.5 md5(本批产物)
|
||
|
||
```
|
||
f8f316a1e6f3bdd8f6b5921fdbb3f3a2 ncm # mipsel 静态 App(新;未部署,留主会话)
|
||
9efb76ff0ae6262eb25870549d0f06d3 ncm_host # 宿主离屏调试二进制
|
||
4b1045011c865bdfd88f447bb08007d9 core/ncm_cache.c # 阶段3:evict/total 跳 .tmp/.part + 续传 API
|
||
50c05a3e472c2309d1f164f9a5102cda core/ncm_cache.h
|
||
649efeddd2908574a6f51661f4abad4e core/ncm_http.c # ncm_http_download_status + 206 放行
|
||
87a89a0ddac2a41d96fd9fb150136c2d core/ncm_http.h
|
||
84042878770f47e47e0ee78e9e765bb0 app/play/stream_src.c # stream_src_open_stream_at(续传 written)
|
||
1430c2e1f7df36011dc562c3d636176f app/play/stream_src.h
|
||
d47b6f85c53f986bd7339f3f1df1fec5 app/play/player_manager.c # .part 检测+Range+206/200/416+重试 1 次
|
||
21904c4525b94776af02f7ecefde48e2 bench/stream_sim.c # resume 用例(中断→续传→PCM 金标准)
|
||
7ca024b7212aef43d3359cac2cbd014c bench/cache_evict_sim.c # case7/8:防淘汰 + 续传 API 契约
|
||
21348d4df94f7e88815423b32320b9e8 bench/player_flow_sim.c # P9/P10/P11:Range/416/重试退避
|
||
e767bb75b7f7ec096b9c9dc01791691d REPORT.md # 本批记录(§24)
|
||
(build_ncm.sh/build_ncm_host.sh 未改;app.json 对外名按约定改「网易云音乐」→「流媒体客户端」)
|
||
```
|
||
|
||
### 24.6 诚实出入 / 遗留
|
||
|
||
- **真机未部署未验**(留主会话统一)。真机验收点:①断网一场中断 → 重播同曲秒开且续传(不重下前缀)
|
||
②播放中下载其他/封面触发 evict,在播首不中断 ③弱网预缓冲失败一次 → 自动整首重试(有 800ms 间隙,
|
||
不连环请求)④`.part` 残体在下次命中秒开(前缀已在盘)。
|
||
- `.part` 残体若长时间不续传(用户不再点该曲)会占磁盘:残体不进 LRU 淘汰(防在播误删),
|
||
v1 不做「孤儿 .part 定时清理」,留主会话决断(可加「清理缓存」时一并删)。
|
||
- 200(服务器忽略 Range)分支按「丢 body 头段」实现(与 .part 前缀重复字节丢弃)——因服务器返回全量
|
||
时 body[0..part_size] 与 .part 前缀逐字节一致(同URL同内容),续传不产生双份;416 分支静默降级
|
||
(播到已下前缀/删残体重下),未做「自动断点续传防抖重连」(风控,§5.2 明确不做)。
|
||
- 重试仅覆盖「预缓冲(<128KB)前网络失败」;开播后失败不重连(§4.1)。`STREAM_RETRY_BACKOFF_MS=800`
|
||
为固定值,未做指数退避多档(retry=1 时一档即可)。
|
||
- 未动 sdk/、meow_launcherd/、vendor/;部署链路同 §13.5 留主会话。
|
||
|
||
## 25. 修复:切音质后重播同一曲恒「缓冲中」——同 idx 再 play 未重启换档(2026-09-01,commit `a01e5ca`)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`。真机用户反馈:切音质后再播同一首歌,声音已喷出但播放页
|
||
> 「采样率/格式信息」处恒显示「缓冲中」。风控:真实 NCM 请求 **0 次**(全离线 stub);未部署真机(留主会话)。
|
||
|
||
### 25.1 根因(文件:行)
|
||
|
||
`app/play/player_manager.c`:**同 idx 的新播放意图不被解码主循环识别**——旧 `still_current`
|
||
(原 L297-303)与解码主循环(原 L564-577)只认 `cur_song != idx` 与 `state == STOPPED`。
|
||
|
||
切音质后重播同一曲 = `player_manager_play(idx)`,而该 idx 恰与在播曲相同,于是 `play` 只改了
|
||
`player_manager.c` L100 `g_play.seq++`、L95 `g_play.state=BUFFER`,`cur_song` 不变。解码主循环
|
||
每轮比对 `cur != idx`(不变→不 break)与 `st == STOPPED`(BUFFER→不 break),导致:
|
||
|
||
1. **旧曲(旧档位)继续喷声**(audio 写照旧走),而 `g_play.state` 停在 `BUFFER` → UI
|
||
`plr_draw_cover_dyn` 读 `player_manager_state()==BUFFER` → 恒「缓冲中」(`app/ui/player.c` L268-281);
|
||
`dl_done/dl_total` 保持 0,缓冲文案退化为无 %、无 KB 的「缓冲中…」(L279)。
|
||
2. **新档位下载/缓存条目从未发起**(换档 = 新缓存键 `song_<id>_<level>`,本应走新下载)。
|
||
3. 旧曲播完后外层线程把 `BUFFER` 当「自然播完」→ `player_manager_next(1)`(原 L693)→ **顺延
|
||
下一首**,新档位永不生效。
|
||
|
||
`still_current` 同理漏判:同 idx 重播时 `cur==idx && state==BUFFER` 恒真,下载/预缓冲/物化/
|
||
解码各护栏均不终止旧 intent。三条路径(缓存命中/边下边播/.part 续传)统一受影响——根因在
|
||
播放意图层,不在某条取数路径。
|
||
|
||
### 25.2 修法(play intent 以 seq 标识,层层判别)
|
||
|
||
- 新增 `still_intent(idx, my_seq)`(`player_manager.c` L306):
|
||
`cur==idx && state==BUFFER && seq==my_seq`——严格强于旧 `still_current`(同 idx 重播时 seq 变
|
||
即判过期),替换全部 `still_current` 调用点。
|
||
- `run_song` 入口捕获 `my_seq`(L509),沿 `song_local_file`(L424)/`song_download_attempt`
|
||
(L319)下传,预缓冲(L386)/重试退避(L482)/物化(L522)/预缓冲后(L540)/解码 open 失败
|
||
(L556)各护栏改 `still_intent(idx, my_seq)`。
|
||
- **解码主循环**(L597):`seq != my_seq`(同 idx 新 intent)→ `break` 回外层重跑 `run_song`
|
||
(换档重播),不再静默续播旧档。
|
||
- **外层线程**「自然播完」收紧为 `st2 == NCM_PC_PLAYING`(L720):不再把 `BUFFER`(挂起的新
|
||
intent)当「播完」顺延下一首;同 idx 新 intent 落外层后按当前 `cur/state` 重跑 `run_song(idx)`。
|
||
|
||
### 25.3 测试证据(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 状态机回归 | **player_flow_sim P1-P11 全量**(缓存命中/下载/.part 续传/重试/门禁,0 网络) | 宿主 + qemu-mipsel **ALL PASS**(零回归) |
|
||
| 本 bug 复现 | **player_flow_sim 新 P12**(§25):standard 档缓存命中播 song0 → 切 lossless → 同 idx 再 play | 宿主 + qemu **ALL PASS**:状态回 PLAYING、新档位下载发起(downloads=1)、缓存键 `1000_lossless`、eff_level 按非会员钳顶回填 EXHIGH(UI 据此显示实际档位,不再停 BUFFER「缓冲中」)。修前 P12 断言 FAIL(恒 PLAYING 不了、键仍 `1000_standard`、downloads=0、eff=-1) |
|
||
| 边下边播全矩阵 | stream_sim 原 16 用例 + 2 resume(PCM 金标准) | 宿主 **ALL PASS**(后台任务复核;player_manager 未动 stream_src 链,理论零影响) |
|
||
| core 回归 | test_ncm_core_host + qemu test_ncm_core_mipsel | PASS |
|
||
| mipsel 构建 | build_ncm.sh 自有代码 -Wall -Wextra | **0 warning**(set -e 下任何告警即失败) |
|
||
| 宿主离屏 | build_ncm_host.sh + NCM_PAGE=player MEOW_UI_DUMP | rc=0、渲染正常(仅 sdk `meow_ui_vol.c` 既有告警,与本批无关) |
|
||
|
||
**状态→UI 对应**:`plr_draw_cover_dyn`(`app/ui/player.c` L268)按 `player_manager_state()` 直判
|
||
——`BUFFER` 画「缓冲中…N%」、否则画时间行/采样率。P12 断言 `state==PLAYING` 即等于「UI 显示
|
||
采样率/时间而非缓冲中」。三条路径的 BUFFER→PLAYING 转换分别由 P1(缓存命中)、P6/P7(边下边播
|
||
下载)、P9(.part 206 续传)覆盖,P12 补同 idx 重播(换档)路径。
|
||
|
||
### 25.4 顺带核实(用户问,未改)
|
||
|
||
- **歌单列表「离线缓存」:未落地**——`appstate_load_playlists`(`app/logic/appstate.c` L144)
|
||
每次进入/刷新都走 `ncm_api_user_playlist` 网络拉取(存 `g_st.playlists[]` 内存),无磁盘离线
|
||
缓存;断网时返回错误、列表空态。歌曲级元数据有落盘(songmeta,滑窗出窗不丢),但「我的歌单」
|
||
列表本身无持久化。
|
||
- **手动刷新按钮:已落地**——home topbar 刷新动作(↻ action0)→ `home_reload()`(`app/ui/home.c`
|
||
L221/L253,后台 worker 非阻塞);歌单详情页 `playlist_refresh()`(`app/ui/playlist.c` L263)重拉当前页。
|
||
|
||
### 25.5 md5(本批产物)
|
||
|
||
```
|
||
28698dd0e097c3de73b243de1dae5417 ncm # mipsel 静态 App(新;未部署,留主会话)
|
||
26480e69c258d56e17464adf0141d21e ncm_host # 宿主离屏调试二进制(重链)
|
||
fe6302544ae8e09eb340b257b2731e71 app/play/player_manager.c # §25:seq 化 intent 护栏 + 解码主循环重启
|
||
d9bfe3d7a6eaa76b00161a4e97196eb8 bench/player_flow_sim.c # 新 P12:同 idx 重播换档重启断言
|
||
```
|
||
|
||
(`REPORT.md` 追加 §25;`WORK_STATE.md` 同步锚点见 devdocs。)
|
||
|
||
---
|
||
|
||
## 26. 歌单链路三修:开A→开B 竞态防串(5194 首 3 首歌/串数据/加载失败)+ 92/200 批边界 Load Failed + 歌单列表盘缓存 + 孤儿 .part 回收(2026-09-01,commit `cd216fa`)
|
||
|
||
> 范围:仅 `patch_v2/ncm_app/`。定案见 `devdocs/plan/NCM_CACHE_DESIGN.md §2.2`(歌单列表快照秒开+后台刷新)、
|
||
> `WORK_STATE.md` 254(滑动窗口)/ 273(搜索不覆写歌单上下文)/ 275 遗留(歌单列表无离线缓存)。
|
||
> 真机现象(用户实测):①「我喜欢的音乐」5194 首要么只显示 3 首(怀疑串数据)要么加载失败要么卡久后失败;
|
||
> ②92 与 200 首歌单点开「加载失败」、72 能开;③从歌曲列表返回首页必卡几秒 LOADING;
|
||
> ④孤儿 `.part` 无回收(275 遗留)。风控纪律:真实 NCM 请求 **0 次**(全离线 stub);**未部署真机**(留主会话)。
|
||
|
||
### 26.1 根因(文件:行)
|
||
|
||
- **① 5194 首(3 首/串数据/加载失败)= 歌单上下文并发竞态(三条叠加)**:
|
||
1. **`appstate_load_playlists`(`app/logic/appstate.c:182`)`memset(g_st.playlists, ...)` 清空再回填**。用户「进歌单→快速返回首页→再进另一个歌单」时,home_worker 的网络拉取(`appstate_load_playlists`)与 playlist_worker 并发;旧 `appstate_load_playlist_songs_page` 只在起点取一次 `pl = &g_st.playlists[idx]`,`detail` 内再读 `pl->id`——若此刻数组被 memset,`pl->id` 为空串 → `detail("")` 取回**空/少量** trackIds(3 首)或异常 → 「只显示 3 首」「串数据」。
|
||
2. **worker 单飞(`g_plthr_on`)只限制并发数量,不校验结果归属**:开 A→未载完→开 B,A 的迟到结果(全量 trackIds + 批物化)平铺进全局 `song_ids/window/track_ids_count`;UI 在 B 标题下读到 A 数据(串数据);旧 `playlist_tick` 仅在 `g_plloaded_idx != active` 时重拉,但重拉前的 `pl_apply` 已把 A 数据画在 B 标题下(瞬时串数据)。
|
||
3. **首屏把任何曲目批错误映射整页失败**:`appstate_song_ensure_range`(旧 `app/logic/appstate.c:360`)批内 `bn<m` 置 `rc=NCM_ERR_JSON`,`playlist_worker` 首屏 `g_load_err=rc` → 整页「加载失败」(见根因②)。
|
||
- **② 92/200 首歌单「加载失败」,72 能开 = 批内个别缺歌 → 整页失败(放大)**:服务端 `song_detail` 对个别歌(下架/版权/权限变化)少返回(`bn<m`,批内缺 1 首即触发);旧 `appstate_song_ensure_range` 对**任何** `bn<m` 直接 `rc=NCM_ERR_JSON`,`playlist_worker` 首屏把 `rc` 映射 `g_load_err=rc`(`app/ui/playlist.c` 旧)→ 整页「加载失败」。与歌单长度无关,纯「批内 1 首异常全灭」放大;72 首恰好全量返回、92/200 批内恰有异常。`NCM_SONG_BATCH=100`,92=1 批边界、200=2 批,均可能含个别异常歌。
|
||
- **③ 返回首页必卡几秒 LOADING = 歌单列表无磁盘缓存**:`home_page_enter`(`app/ui/home.c:366`)每次进入都调 `home_reload()` → `home_worker` → `appstate_load_playlists`(`app/logic/appstate.c:177`)每次都走 `ncm_api_user_playlist` 网络拉取(`g_st.playlists[]` 仅内存),无磁盘缓存(WORK_STATE 275 遗留)。断网还回错误、列表空态。
|
||
- **④ 孤儿 `.part` 无回收**:中断残体 `.part`/`.tmp` 不进 LRU 池(防在播文件误删,REPORT §24),但用户不再点该曲则不续传、占磁盘无回收。
|
||
|
||
### 26.2 修法
|
||
|
||
1. **打开时快照 plid + 代次 seq,回调校验不匹配即丢弃(修①,核心)**:
|
||
- `core/ncm_common.h`:新增 `NCM_ERR_STALE = -16`(异步结果过期)。
|
||
- `app/logic/appstate.c`:新增打开上下文 `appstate_playlist_begin(idx)/open_gen()/open_idx()/reset()`(`L27-49`,记「打开代次+目标下标」);
|
||
新增 `appstate_load_playlist_songs_ctx(pl,is_search,ctx_gen,ctx_idx,off,lim)`(`L523`)与内部 `appstate_load_playlist_songs_impl`(`L477`):
|
||
- `pl` 为**打开时快照副本**——`detail` 只读快照 `id`,不再读 `g_st.playlists[]`(免疫 home_worker 并发 memset);
|
||
- offset=0 的 detail 结果落 `static s_build[]` staging(`L502`),锁内**原子发布索引**(防 worker 写 `song_ids` 与播放线程 `song_fetch` 读竞态;`appstate_song_fetch` `L437` 索引读改加锁);
|
||
- 每次 `detail` 后/加载末校验 `ctx_gen==当前代次 && ctx_idx==当前目标`,不匹配 → `NCM_ERR_STALE` 且**不写任何全局**(`L529/L537-539`)。
|
||
- `app/ui/playlist.c`:新增打开快照 `pl_target_t g_tgt`(`L53`)与 `g_pl_stale`/`g_plloaded_gen`;`playlist_refresh`(`L302`)启动 worker 前锁内快照目标并 `appstate_playlist_begin`;`playlist_worker`(`L231`)用快照 `id` 调 `appstate_load_playlist_songs_ctx(&tpl,...)`;`playlist_tick`(`L329`)`g_pl_stale || loaded_idx != active || loaded_gen != open_gen` → `appstate_playlist_reset()` 清窗口 + 对当前目标重拉(不把旧歌单数据画在新歌单标题下,瞬时串数据消除)。
|
||
2. **批内缺失/单批网络失败非致命(修②)**:`appstate_song_ensure_range`(`L317`,批处理 `L423`)改 **fill-and-continue**:`bn<m`(部分返回)写已返回的、缺槽留 invalid(下次 ensure 再试),记 `first_err=NCM_ERR_NOTFOUND`;`r2!=OK`(单批网络失败)跳过本批网格、其余批照常补,记 `first_err=r2`;返回首个非致命错误码(而非把整段置失败)。`playlist_worker`(`L257-261`)首屏「索引成功但 `track_ids_count>0`」把非致命错误降级为 `g_ens_err`(snack)+ `g_load_err=0`——页面保留已物化 + 占位行,不再整页 Load Failed(索引失败/鉴权失败仍整页空态可点重试)。
|
||
3. **歌单列表盘缓存(修③,NCM_CACHE_DESIGN §2.2 stale-while-revalidate)**:`appstate_playlists_cache_save/load`(`L200/L222`,`<data_dir>/playlists.bin`:magic+ver+时间戳+数组;刻意不进 LRU 音频池,防被「清理缓存」误清);`appstate_load_playlists`(`L188`)网络成功即覆写缓存。`home_reload`(`L223`)先 `appstate_playlists_cache_load` 秒渲(有缓存不显示 LOADING),后台 worker 静默刷新;`home_worker`(`L216`)刷新失败但有数据(`playlist_count>0`)不置错误态(断网可离线浏览);`home_draw_empty`(`L167`)有数据即显示列表。手动刷新按钮(`home_on_tap`↻ → `home_reload`)保留。
|
||
4. **孤儿 `.part` 回收(修④)**:`core/ncm_cache.c` `ncm_cache_evict`(`L176/L187`)与 `ncm_cache_total_bytes`(`L325/L335`)LRU 扫描时顺手续删 `mtime > 7 天`(`NCM_ORPHAN_PART_DAYS=7`,`L25`)的 `.part`/`.tmp`(在播/刚中断的 mtime 新鲜安全);`ncm_cache_clear`(清理缓存)本删全部 `song_*`(含 `.part`/`.tmp`),满足「清理缓存一并清所有 .part」。
|
||
|
||
### 26.3 测试证据(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| ①异步竞态(开A未完→开B,A 迟到结果丢弃不串 B) | **新 `bench/playlist_race_sim.c`**(真实 appstate + 本地 stub) | **PASS**:open A 载 92 → begin(B)→A 以旧代次 load 返回 `NCM_ERR_STALE`(-16)不写全局 → B 以当代次 load 采纳(count=200);open 上下文正确指向 B |
|
||
| 快照 plid 免疫 home_worker memset | `bench/playlist_race_sim.c` | **PASS**:打开时快照后 `memset(g_st.playlists)` → 快照载 92 仍准、detail 收到快照 `plFav`(非空) |
|
||
| ②92/200 批边界 | `bench/playlist_race_sim.c` | **PASS**:72/92 首屏 1 detail+1 批全物化;200 首屏 1 批(100)+二批 ensure(200) 共 2 次 song_detail、tail 可达 |
|
||
| ②部分返回/单批网络失败非致命 | `bench/playlist_race_sim.c` | **PASS**:`bn<m` 返回 `NCM_ERR_NOTFOUND` 但索引保留(92)、已返回的 91 首物化、worker 降级为非致命(页面不 wipe);单批网络失败同样非致命(page 不 wipe,占位保留) |
|
||
| ②盘缓存回环 | `bench/playlist_race_sim.c` | **PASS**:save(2 歌单)→load 恢复 count=2、ts>0、id/name 一致 |
|
||
| ④.part 7 天回收 | `bench/cache_evict_sim.c` 新 case9 | **PASS**:8 天 `.part`/`.tmp` 被 evict 扫描删、新鲜 `.part` 保留 |
|
||
| 现有回归 | window_sim / pagination_sim / search_sim / cache_evict_sim(1-8) / stream_sim | 全 **PASS**(6000 首滚动 / 5191 无截断 / 搜索队列 / LRU 续传 / 边下边播全矩阵+resume) |
|
||
| core 回归 | `test_ncm_core_host` + qemu `test_ncm_core_mipsel` | 宿主 + mipsel **PASS** |
|
||
| mipsel 构建 | `bash build_ncm.sh` 自有代码 `-Wall -Wextra` | **0 warning**;`text 978354 / data 1624 / bss 1496484`(bss +~132KB 来自 `s_build[]` staging) |
|
||
| 宿主离屏渲染 | `build_ncm_host.sh` + `NCM_PAGE=home/playlist/search` | rc=0,渲染正常 |
|
||
|
||
### 26.4 md5(本批改动文件 + 产物)
|
||
|
||
```
|
||
158ec3672aa7708f791dce7d79f7d8b1 ncm # mipsel 静态 App(新;未部署,留主会话)
|
||
a3c336a9e863017a84827c3ad3e204de ncm_host # 宿主离屏调试二进制(重链)
|
||
7922fc00d19666e64eb353d6685aba0a core/ncm_common.h # +NCM_ERR_STALE(-16)
|
||
a6d561a33797da2ac260d46d53d76357 core/ncm_cache.c # §26:7 天孤儿 .part/.tmp 顺手续删(evict/total_bytes)
|
||
6c4780a3253b2187224db684a9243fc9 app/ncm_app_internal.h # 打开上下文 API + 快照 loader + 盘缓存声明
|
||
cca1a51d031092e0bdbf184266031e67 app/logic/appstate.c # §26:打开代次/快照 ctx loader + 部分非致命 ensure + 歌单列表盘缓存
|
||
c098a80b69e55f0be4b38ee84a73b5c7 app/ui/playlist.c # §26:快照+代次 worker,tick 丢弃+reset 防串
|
||
01975811d562c231022c607eed766f3a app/ui/home.c # §26:歌单列表缓存秒渲 + 后台静默刷新
|
||
b7a2bfaec3c3d7bd29a151072cd40f32 bench/cache_evict_sim.c # 新 case9:.part/.tmp 7 天回收(+ncm_now_ms 桩)
|
||
56673b0e56ebb605ac575ce97c5e27a8 bench/playlist_race_sim.c # 新:竞态防串/批边界/非致命/盘缓存/reset(0 网络)
|
||
```
|
||
|
||
### 26.5 出入清单(只改 `patch_v2/ncm_app/`)
|
||
|
||
```
|
||
修改:
|
||
core/ncm_common.h # +NCM_ERR_STALE(-16)
|
||
core/ncm_cache.c # 孤儿 .part/.tmp 7 天顺续删(evict/total_bytes 扫描时)
|
||
app/ncm_app_internal.h # 打开上下文(gen/idx/begin/reset) + 快照 loader 声明 + 盘缓存声明 +
|
||
# appstate_range_loaded 声明恢复(补回)
|
||
app/logic/appstate.c # 打开代次上下文 + 快照式 appstate_load_playlist_songs_ctx/impl +
|
||
# 索引用 staging 锁内原子发布 + song_fetch 索引读加锁 +
|
||
# ensure_range fill-and-continue(部分/单批非致命)+ 歌单列表盘缓存
|
||
app/ui/playlist.c # 打开快照(g_tgt)+代次;worker 用快照 id 走 ctx loader;
|
||
# tick 过期(idx/gen/stale)丢弃 + appstate_playlist_reset 防串;
|
||
# 首屏部分失败降级为非致命(snack)
|
||
app/ui/home.c # home_reload 缓存秒渲 + 后台静默刷新;home_worker 刷新失败不置错;
|
||
# home_draw_empty 有数据即显示列表
|
||
bench/cache_evict_sim.c # 新 case9(7 天回收)+ ncm_now_ms 桩
|
||
新增:
|
||
bench/playlist_race_sim.c # 歌单链路三问题离线回归(0 网络)
|
||
REPORT.md # 本批记录(§26)
|
||
(未动 sdk/、meow_launcherd/、vendor/;未部署真机;未动设备系统/恢复区)
|
||
```
|
||
|
||
### 26.6 诚实出入 / 遗留
|
||
|
||
- **真机未部署未验**(留主会话统一)。真机验收点:①「我喜欢的音乐」5194 首正常显示/滚动/回滚无 3 首或串数据;快进回首页再进其它歌单不再串;②92/200 首歌单点开正常(不再整页 Load Failed,个别缺歌只占位+snack);③从歌曲列表返回首页秒出(缓存渲染)再被后台刷新替换;④清理缓存清掉全部 `.part`,LRU 扫描顺手清 7 天前 `.part`。
|
||
- **打开快照残留竞态窗口**:`playlist_refresh` 的 `g_tgt.pl = *pcur`(读 `g_st.playlists[idx]`)与 home_worker 的 `memset` 仍可能瞬时交错(相差若干指令);`pl_cur()` 快照拷贝前加了 `!pcur->id[0]` 防御(空 id 直接报错可重试),不产生 3 首垃圾数据,但理论上仍有一次「取到半零 id」的极小概率——后续可给 `g_st.playlists[]` 加互斥彻底消除(本次未做,保持最小改动)。
|
||
- **`appstate_song_ensure_range` 返回首个非致命错误码**(`NCM_ERR_NOTFOUND`/网络码)供 UI snack;语义从「整段失败」改为「已物化保留+占位自愈」,`appstate_range_loaded` 对未覆盖区间仍返回 0(触发后续 ensure 重试)。
|
||
- **歌单列表缓存无版本化清理**:`playlists.bin` 靠 ver/magic 判 miss;数组改版/账号切换不主动失效(账号掉线重登前 `appstate_logout` 未清 playlists.bin——本次 `logout` 未联动删缓存,**下一账号可能看到上一账号列表直到首次网络成功覆写**;留主会话)。
|
||
- **`.part` 7 天回收只在 LRU 扫描(evict/total_bytes)时触发**:无网/不触发 evict 时不会主动清;属「惰性删除」(任务要求)。`.tmp`(在播写穿)mtime 持续更新,安全。
|
||
- **未动 sdk/、meow_launcherd/、vendor/**;部署链路同 §13.5 留主会话。commit `cd216fa`(git 记录)。
|
||
|
||
## 30. 真机四连症状排查修复:歌单豆腐/首页方框/LRC 一行/音量连按杀 App(2026-09-02,REPORT §30)
|
||
|
||
> 真机(10.78.193.50,设备 `dev.player.ncm`)四连症状,均在 SDK 292/293/295 重链(ncm `b30b0aed`)
|
||
> 后出现。本批先取证后定位,逐症状给根因/修法/测试。风控纪律:**0 次真实 NCM 业务请求**
|
||
> (curl 打网易云 CDN 封面仅 `GET` 静态图片做网络侧诊断,不算 NCM 接口请求;全部修复验证在
|
||
> 宿主 mock + 离线 sim)。
|
||
|
||
### 30.1 症状①:162 首歌单打开后所有条目歌名显示「三个豆腐」+「加载中」(小歌单 72 快且正常)
|
||
|
||
**根因(两个叠加)**:
|
||
- **显示层(豆腐的真相)**:行占位符 `pl_src2` 的 `out->title = "···"`(U+00B7 中点 ×3)。设备默认
|
||
字体 `MeowOS/fonts/default/regular/font.idx` **没有 U+00B7 字形**(`meow_font.c` 缺码位画空心方块
|
||
tofu)→ `···` 渲染成**三个豆腐块**。副标题正是占位行的「加载中」。
|
||
- 证据:把我取回的 `font.idx` 用 `load_idx` 同款解析(MFNI + 每面 glyph 记录 16B)扫全 glyph:
|
||
`cp U+002E('.') present / U+00B7('·') MISSING / U+2022('•') MISSING / U+2026('…') present /
|
||
U+4E2D(中) present / U+25E2('◢') MISSING`。字体缺 U+00B7 与 ◢◤/Kannada(ಡ) 等字形。
|
||
- **物化层(为何一直占位)**:占位行 = `appstate_song_copy(idx)!=0`(滑窗未覆盖/未物化)。首屏
|
||
`appstate_load_playlist_songs_ctx(...,0,NCM_SONG_BATCH=100)` 应物化 0-99;若该批 song_detail
|
||
网络/解析失败(非致命,§26 fill-and-continue 只记 `g_ens_err` 不置 `g_load_err`)→ 窗口空、
|
||
track_ids_count=162 → **所有行占位**。162>100 走 2 批(首屏 1 批 100 + 二批 62),92/200/5194 已由
|
||
§26 修过但 162 的真实 song_detail 批在真机失败原因待下一会话诊断日志(**本批给 playlist_worker
|
||
首屏打 INFO 日志:idx/plid/trackIds/materialized/rc**,见下)。
|
||
|
||
**修法**:
|
||
- `app/ui/playlist.c` 占位 title `"···"`→`"..."`(ASCII 三点)——豆腐消除;
|
||
- 全 App 把 U+00B7 分隔符 ` · ` 换成 ASCII ` / `(`playlist/search/home/player/settings/login`),
|
||
因为字体缺 U+00B7,所有 ` · `(歌单副标题、组头「我创建的 · N 个」、播放页 meta、设置副标)
|
||
都会渲染成豆腐,一并消除。
|
||
- `app/ui/playlist.c` `playlist_worker` 首屏完成打 `playlist: first-screen idx=.. plid=.. trackIds=..
|
||
materialized=.. rc=..` 日志,供下一会话定位 162 首屏是否真失败。
|
||
|
||
### 30.2 症状②:从该歌单返回首页,歌单列表约 1 秒全是方框(之后恢复)
|
||
|
||
**根因(部分定位)**:同上字体缺字形。首页组头「我创建的 · N 个」用 ` · `(U+00B7)→ 豆腐;
|
||
「我收藏的」页签里大量歌单名含特殊字形(`A神◢◤永存`、`(ಡωಡ)`、`【】『』` 等,字体缺 ◢◤/Kannada)
|
||
→ 对应行豆腐。「全是方框约 1 秒后恢复」的**瞬态**属性更符合「页面重进时字体 glyph atlas 冷缓存重栅格
|
||
(渲染大量陌生 CJK 歌名后 LRU 淘汰,重进首页需重栅格)」——SDK 内部行为,需设备逐帧观测才能钉死,
|
||
本批未再深挖(避免大改 SDK)。
|
||
|
||
**修法**:` · `→` / `(本批已整体替换);特殊字形歌名属字体覆盖缺口,建议后续给 default 字库补
|
||
U+2022/U+25E2 等(不在本批范围)。
|
||
|
||
### 30.3 症状③:LRC 歌词全部挤成一行纯文本,时间戳也没解析(有词歌曲全这样;272 歌词页曾正常)
|
||
|
||
**根因**:`core/ncm_json.c` 的 `ncm_json_get_str_at` **只 memcpy 原文、不做 JSON 转义还原**。
|
||
`/api/song/lyric` 的 `lrc.lyric` 把整段 LRC 的换行写成 JSON 转义 `\n`;jsmn 只分词不还原 →
|
||
`lrc` 字符串里是**字面 `\`+`n`(无真实换行)** → `lrc_parse`(lyric.c)扫不到行尾 `\n` → 整段歌词
|
||
取成第一行的一个超长正文,只解析出 1 个时间戳 → 「挤成一行纯文本、时间戳没解析」。先前每个带
|
||
JSON 转义字段(`\n`/`\/`/`\uXXXX`)都受影响,只因歌词行数多最显眼。
|
||
|
||
**修法**:`ncm_json.c` 新增 `static json_unescape()`(RFC 8259 §7:`\" \\ \/ \b \f \n \r \t` + `\uXXXX`
|
||
UTF-16→UTF-8,含 `\uD800..DBFF+\uDC00..DFFF` 代理对合成),`ncm_json_get_str_at` 原文→还原写入。
|
||
输出恒 ≤ 输入长度(`\uXXXX` 6B→≤4B),out_sz 内必然放得下。`\n`→真实换行后 LRC 逐行切分正常。
|
||
(`lyric.c` 的 `copy_lrc_text` 对服务端真·字面 `\n` 的空格化兜底保留不动,双保险。)
|
||
|
||
### 30.4 症状④:状态栏音量刷新仅 1Hz;连按音量键按多了前台 App 被 SIGKILL(闪退)
|
||
|
||
**根因(崩溃,实证)**:`patch_v2/meow_launcherd/app_launcher.c` 的「应用内强杀」快捷键
|
||
**VOL_DOWN ×3**(`app_seq_hits>=3 && seq_gap_ms=600`)→ `vol_restore` + `kill(SIGTERM)` + 500ms +
|
||
`kill(SIGKILL)`。用户**连按音量下键**(正常调音量)3 下即误触发 → SIGKILL。
|
||
- 证据:设备 launcher 日志 `meow_20260902_012940.log` 第 1022 行 `1061363 kill seq: VOL_DOWN x3 ->
|
||
SIGTERM app`;amsd 日志 `1788284842 amsd EXIT dev.player.ncm sig=9 dur=255353 abnormal=1`
|
||
(对应 pid 2169,正与本会话 ncm 日志末尾 `resumed` 后被杀吻合)。非 SIGSEGV、dmesg 无 OOM——
|
||
是被 VOL_DOWN×3 快捷键 SIGTERM→SIGKILL。
|
||
- 设计缘起:设备前置键 GPIO 不通(§18.12)→ 原 `PREVIOUS+VOL_UP` 退出组合退役,改用 VOL_DOWN×3。
|
||
但框架 App(`framework=1`,如 ncm)有 SDK 左缘横滑返回退出(`app.c` 根页 `meow_ui_request_exit`),
|
||
VOL_DOWN×3 与之冲突。
|
||
|
||
**修法**:`app_launcher.c` VOL_DOWN×3 门控到 `!a->framework`(裸 App 保留;框架 App 走左滑返回退出,
|
||
连按音量键只调音量)。框架 App 挂死兜底走 `am kill <pkg>`。其余半「根因」:音量 1Hz 刷新是 launcher
|
||
状态栏对 settings.conf 的 1s 节流轮询(262 Bug2 定案),音量本体现在秒切(SDK 即时写 settings.conf+
|
||
CS43198 DAC),仅图标刷新滞后 1s——属 pacing 取舍,未改(改需 firmware + 设计决断)。
|
||
|
||
### 30.5 症状⑤(顺带):封面大量 `fetch failed → give up`(封面加载不出)
|
||
|
||
**根因**:`cover.c` 旧 `static g_buf[NCM_COVER_MAX=288KB]` 作为下载缓冲;网易云封面全尺寸实测
|
||
**1.34MB**(`GET` 返回 `200 size=1342045`)> 288KB → `ncm_http` body 超 cap 回 `NCM_ERR_FULL`
|
||
→ `decode_cover` 返回 NULL → 退避重试 ×8 放弃。且设备 RAM 仅 **56MB**,不能靠加静态 bss 解决。
|
||
|
||
**修法**:`cover.c` 改**流式写文件**——`ncm_http_download` 的写回调把 body 直接写进 ncm_cache
|
||
写者临时文件(`ncm_cache_write` 是 `write(2)` 直写无缓冲),`meow_img_load_file(临时文件)` 解码,
|
||
commit 落盘;`save_cover_raw` 改为临时文件→`cover_current.jpg` 文件复制。NCM_COVER_MAX 改语义为
|
||
「下载字节上限 8MB」(非缓冲),静态 bss 归零(设备 RAM 有限)。
|
||
|
||
### 30.6 验证记录(0 次真实 NCM 业务请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| ①/③ unescape | `test_ncm_core.c` 新 `test_json_unescape`(合成 `\n`+`\/`+`\u00e9` JSON) | 宿主 + qemu-mipsel **PASS**:`lrc \n unescaped -> 3 行`、`URL \/ unescaped`、`URL no literal backslash` |
|
||
| ①②③ 界面 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | 宿主 ncm_host exit=0 dump 正常 |
|
||
| ① 歌单链路 | `playlist_race_sim`(72/92/200 竞态防串/批边界/盘缓存) | **PASS** |
|
||
| ① 滑窗物化 | `window_sim`(6000 首滚动 0 占位破窗 + 盘缓存回环) | **PASS** |
|
||
| ① 搜索 | `search_sim` | **PASS** |
|
||
| ① 缓存 | `cache_evict_sim`(LRU/防淘汰/7 天回收 case1-9) | **PASS** |
|
||
| ① 播放链 | `player_flow_sim`(P1-P12 含同 idx 重播换档) | **PASS** |
|
||
| ① 边下边播 | `stream_sim`(16 用例 + 2 resume,PCM 金标准) | **ALL PASS** |
|
||
| SDK 292/293/295 | `test_meow_ui_listvirt` 44 / `dispatch` / `box` 42 / `scroll` 17 / `anim` 32 / `widgets` / `overlay_suppress` / `sdk_input_keys` / `theme` / `vol` / `ime_fix` / `pinyin` / `notif_gesture` / `notif_open` / `sdk_fixes` / `sb_collapse` / `ui_audio_host` / `notify_ipc` / `notify_integration` | 全 **PASS** |
|
||
| launcher | `test_vol_ramp_jack` 15 / `test_ams_start` / `test_notif_tf` / `test_settings_brightness` / `test_media` / `test_lock_screen` / `test_notif_panel` | 全 **PASS** |
|
||
| mipsel 构建 | `build_ncm.sh` 自有代码 `-Wall -Wextra -Os` | **0 warning**;`text 991942 / data 1624 / bss 1216260`(bss 从 §26 的 1.5MB 回到 1.2MB——封面去静态缓冲后未涨) |
|
||
| core mipsel | `build_core.sh` + qemu-mipsel `test_ncm_core_mipsel` | PASS(含新 unescape) |
|
||
|
||
### 30.7 md5(本批产物,未部署,留主会话)
|
||
|
||
```
|
||
d91cd906162d58dcc05a59913cca1c93 ncm # 新(未部署)
|
||
03dfcf7664cc73a93637dc8816a606e1 meow_launcherd # 新(/usr/bin 只读,需固件或 /usr/data 热副本)
|
||
b731654a06d5ca5746edea8393f8668f ncm_host # 宿主调试二进制(重链)
|
||
7a38194c990c36fcbc003897168a9020 test/test_ncm_core_host # 宿主 core 测试(重编,含 unescape)
|
||
```
|
||
|
||
改动文件(本批,仅这些):
|
||
```
|
||
修改:
|
||
core/ncm_json.c # json_unescape + ncm_json_get_str_at 还原转义(§30.3)
|
||
app/ncm_app_internal.h # NCM_COVER_MAX 288KB→8MB 语义改下载上限(非缓冲,§30.5)
|
||
app/logic/cover.c # 封面流式写文件(ncm_http_download cb→ncm_cache_write),§30.5
|
||
app/ui/playlist.c # 占位 "···"→"..." + 副标题 " · "→" / " + 首屏诊断日志(§30.1)
|
||
app/ui/home.c # 组头 " · "→" / "(§30.2)
|
||
app/ui/search.c # 副标题 " · "→" / "
|
||
app/ui/settings.c # 设置副标/页脚 " · "→" / "
|
||
app/ui/login.c # 登录页 " · "→" / "
|
||
app/ui/player.c # 播放 meta " · "→" / "
|
||
test/test_ncm_core.c # 新 test_json_unescape(§30.3)
|
||
build_ncm_host.sh # SDK_SRC 补 meow_ui_anim/box/scroll(宿主链接 292/295 新源)
|
||
../meow_launcherd/app_launcher.c # VOL_DOWN×3 门控 !a->framework(§30.4)
|
||
新建:
|
||
(无新文件)
|
||
```
|
||
|
||
### 30.8 诚实出入 / 遗留
|
||
|
||
- **真机未部署未验**(留主会话统一部署 ncm `d91cd906`、launcher `03dfcf76`)。真机验收点:
|
||
①162 首歌单点开条目显示歌名(不再三个豆腐/加载中),首页歌单列表无方框;②歌词按行滚动、点行跳
|
||
转、时间戳高亮;③连按音量键只调音量不闪退;④封面正常显示;⑤大歌单首屏日志 `playlist:
|
||
first-screen ... materialized=N rc=0`(N>0,无失败)。
|
||
- **症状①物化层未再钉死**:162 首屏 song_detail 批在真机失败的确切 rc 待新日志(`playlist:
|
||
first-screen` 行)复现读取;若 rc 为网络码需要后续加固超时/重试(本批未改,保持 §26 语义)。
|
||
- **症状②「全方框 1s 恢复」**:高度疑为字体 glyph atlas 冷缓存重栅格(SDK 内部),本批未深挖;
|
||
已消除 ` · ` tofu,特殊字形(◢◤/Kannada)建议补 default 字库。
|
||
- **音量 1Hz 刷新**:launcher 状态栏 1s 节流(262 定案)——音量本体秒切、仅图标滞后;改需 firmware
|
||
+ pacing 设计决断,本批未动。
|
||
- **`parse_url` http→443 潜性缺陷**:`http://` URL 无显式端口时 `url.port` 恒 443(TLS),对
|
||
music.126.net CDN(443 上提供 TLS)恰好可用;语义错误但未阻断,未改(改动有回归风险)。
|
||
- **未动 sdk/、vendor/**;`ncm_host`/`test_ncm_core_host` 为宿主调试产物随批重编。
|
||
|
||
## 31. UI 两处数据竞态修复:返回首页单行空白 + 新进歌单显示上一歌单(2026-09-02,REPORT §31)
|
||
|
||
> 真机「ncm 待修清单」⑤/⑥(见 WORK_STATE 2026-09-02 用户实测 8 条)。只改 `patch_v2/ncm_app/`。
|
||
> 根因已由只读分析钉死,本批照方抓药实现。风控纪律:**0 次真实 NCM 请求**(全部离线 stub);
|
||
> **未部署真机**(留主会话统一上卡)。
|
||
|
||
### 31.1 症状 A:返回首页各行 2~3 秒空白(除「我喜欢的音乐」外无标题/曲目数 0)
|
||
|
||
**根因**:`appstate_load_playlists`(`app/logic/appstate.c:182` 附近)对共享数组 `g_st.playlists[]`
|
||
**就地 `memset`→网络回填**,与 UI 线程 `home_src2` **无锁逐行绑定**竞态。刷新期间 `playlist_count`
|
||
保留旧值(cache),UI 读到被清零的行(`name[0]==0`/`track_count==0`)。下标 0 先读幸存(=「我喜欢的
|
||
音乐」行正常的由来)。旧代码在**失败**时还把 `playlist_count=0`,覆盖 §26 cache 秒渲的「断网离线
|
||
浏览」语义(§31.1 顺带修)。
|
||
|
||
**修法**(照搬本文件已有 `s_build[]` staging 范式):网络结果落 `static s_pls[NCM_PLAYLIST_MAX]`
|
||
staging,**成功后锁内原子发布**(拷回 `g_st.playlists[]` + 更新 `playlist_count`),失败**保留旧快照**
|
||
(不改 playlists/playlist_count)。绝不就地清零 UI 可见数组。新增 `g_pls_mu` 发布锁。
|
||
|
||
### 31.2 症状 B:进入第二个歌单显示上一个歌单内容
|
||
|
||
**根因**:`playlist_open/playlist_refresh` 的同步路径不调 `appstate_playlist_reset`——新页先
|
||
`pl_apply(1)` 绑到上一歌单**残留窗口**(`track_ids_count`/`songs`),B 标题下画出 A 的行。
|
||
|
||
**修法**:`playlist_refresh` 起 worker **前**,若目标歌单与当前窗口归属不同则**同步调
|
||
`appstate_playlist_reset()`** 再 `pl_apply`——换歌单必清;同歌单手动刷新(id 相同)保留旧内容
|
||
(stale-while-revalidate)。窗口归属以**歌单 id** 唯一标识(对搜索歌单同样成立——不同搜索结果 =
|
||
不同 id,避免搜索 A→搜索 B 仍被 idx 哨兵误判为同一歌单):
|
||
|
||
- `appstate.c` 新增 `appstate_playlist_window_set(plid)`/`appstate_playlist_window_is(plid)`(
|
||
`g_winpl_mu` 保护);`appstate_playlist_reset` 顺带清空归属。
|
||
- `playlist.c`:`playlist_worker` 首屏发布后(非 STALE)`appstate_playlist_window_set(tpl.id)`;
|
||
`playlist_refresh` 在 `!appstate_playlist_window_is(g_tgt.pl.id)` 时同步 reset。
|
||
|
||
### 31.3 验证记录(0 真实 NCM 请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| A 并发读 vs 后台刷 | `bench/playlist_race_sim.c` **新 F**(reader 线程模拟 home_src2 逐行绑定 + writer 反复 `appstate_load_playlists`) | **PASS**:20000 次刷新全程 reader 读不到清零态(`name[0]==0`),最终数组完整 |
|
||
| B 开 A→开 B | `bench/playlist_race_sim.c` **新 G**(load A 标记归属=A → 换 B 判归属→reset → 窗口清空 → load B) | **PASS**:切到 B 后 `track_ids_count==0 && song_count==0`(B 页不出现 A 行),B 首行 id 在 B 范围 |
|
||
| 既有回归 | window_sim / search_sim(重建,编译 appstate.c) | 全 **PASS** |
|
||
| 既有回归 | cache_evict_sim / stream_sim(PCM 金标准 16+resume)/ test_ncm_core 宿主+mipsel | 全 **PASS** |
|
||
| 播放链回归 | player_flow_sim(重建 P1-P12) | **ALL PASS** |
|
||
| 宿主离屏渲染 | `build_ncm_host.sh` + `NCM_PAGE=playlist/home`(mock) | exit=0,playlist 页 6 行正常(删 ` · ` tofu 后) |
|
||
| mipsel 构建 | `bash build_ncm.sh` 自有代码 `-Wall -Wextra` | **0 warning**;`text 997202 / data 1648 / bss 1362468`(bss +~95KB = `s_pls[]` staging) |
|
||
|
||
### 31.4 md5(本批改动文件 + 产物)
|
||
|
||
```
|
||
dd3d36d5935b85df8d8beabc52a0d090 ncm # mipsel 静态 App(新;未部署,留主会话)
|
||
57c20e6e9a4b32631c9b6d5ac30463a6 ncm_host # 宿主离屏调试二进制(重链)
|
||
7b1ebcc3688e49fc7b8264e4be7a46df playlist_race_sim # 重链(含新 F/G)
|
||
5597307a1a95e06db40451085caae248 window_sim # 重链
|
||
1eb36c7461f9ad47584481780132ea1c search_sim # 重链
|
||
0cecfb3c38483549d83b32775a377c3b player_flow_sim # 重链(补 auto_av stub)
|
||
c23f3152df9bf634e8cdd2a0f0b03697 app/logic/appstate.c # §31.1/31.2
|
||
6d678c5f94a413e1daab4b01ef00febe app/ui/playlist.c # §31.2
|
||
cb6fe604e433401302490dc43a3c7891 app/ncm_app_internal.h # 窗口归属 API 声明
|
||
65afad5e3ad75b95cd544d4019f274dd bench/playlist_race_sim.c # 新 F/G + user_playlist 桩
|
||
bed32f8ffccbbfb371556ee5e41cae37 bench/player_flow_sim.c # 新 ncm_api_song_url_auto_av 桩(测试桩)
|
||
de4c5e1939c7571b90f75a80f48236c0 build_ncm_host.sh # SDK_SRC 补 meow_ui_str(宿主 I18N 链接)
|
||
```
|
||
|
||
### 31.5 出入清单(只改 `patch_v2/ncm_app/`)
|
||
|
||
```
|
||
修改:
|
||
app/logic/appstate.c # §31.1 staging 发布(appstate_load_playlists)+ §31.2 窗口归属
|
||
app/ui/playlist.c # §31.2 worker 标记归属 + refresh 换歌单清窗
|
||
app/ncm_app_internal.h # appstate_playlist_window_set/is 声明
|
||
bench/playlist_race_sim.c # 新 F/G 用例 + ncm_api_user_playlist/ncm_login_status_check/
|
||
# ncm_ctx_clear_cookie 离线桩
|
||
bench/player_flow_sim.c # 补 ncm_api_song_url_auto_av 桩(§20.3 新符号,测试桩,不改生产)
|
||
build_ncm_host.sh # SDK_SRC 补 meow_ui_str(SDK I18N 拆分致宿主链接断,预存)
|
||
REPORT.md # 本批记录(§31)
|
||
(未动 sdk/、vendor/、core/、meow_launcherd/;未部署真机;0 真实请求)
|
||
```
|
||
|
||
### 31.6 诚实出入 / 遗留
|
||
|
||
- **真机未部署未验**(留主会话统一上卡 `dd3d36d5`)。真机验收点:①返回首页各行不再空白(有标题/
|
||
曲目数);②进第二个歌单不显示上一歌单内容;③断网回首页可离线浏览缓存列表(§31.1 顺带修)。
|
||
- **staging 发布对 lock-free 读侧(home_src2/home_draw)非严格原子**:发布为 valid→valid 覆盖,
|
||
`name[0]` 在旧/新快照恒非零,故「清零态」不可能被读到(与本批 F 断言一致);若想彻底消除
|
||
理论上的一次字段级 torn 混合读,可另给 `g_st.playlists[]` 加读锁(本次未做,保持最小改动)。
|
||
- **`player_flow_sim.c` 补的 `auto_av` 桩**:只为了在 §20.3(HEAD `97b741c`,主会话在做的降级链
|
||
工作)把 `player_manager.c` 切到 `ncm_api_song_url_auto_av` 之后测试能构建;桩转发到旧 `auto`
|
||
桩、不做档位链语义——`P1-P12` 全过说明该 sim 关注点(播放流/降级可访问性)未受我改动影响。
|
||
§20.3 档位链语义本身的验证属主会话(297 队列项)。
|
||
- **bss +~95KB**:`s_pls[NCM_PLAYLIST_MAX]`(120×808 ≈ 95KB)staging,与既存 `s_build[16384]`
|
||
(128KB) 同口径;设备 RAM 56MB 可承受(总 bss 1.36MB)。
|
||
|
||
## 32. 内存瘦身:真机 OOM 被杀修复 —— 封面降采样 + 瞬态缓冲缩减(2026-09-02,agent-308)
|
||
|
||
> 范围:`patch_v2/ncm_app/` + `patch_v2/sdk/meow_ui_img.c/.h`(新增一个纯增量 downscale API)。
|
||
> 风控:真机验证用 autoplay 播**已缓存**曲目(0 播放请求);HOME 歌单列表走盘缓存
|
||
> (`playlists.bin`,§26),首启一次网络刷新(少量真实请求,任务允许)。
|
||
|
||
### 32.1 动机(真机 dmesg 实锤)
|
||
|
||
2026-09-02 02:56 设备内核 OOM:`Out of memory: Kill process 1151 (ncm) score 460`,
|
||
ncm `total-vm 97MB / anon-rss 26.9MB`,设备 56MB 总内存(系统+服务占完可用 ~20MB)。
|
||
launcher `app_launcher.c:1647` 已有 OOM 护栏(`MemAvailable<24MB` → 大字库降 common),
|
||
说明系统侧早已见过该类压力。本批把 ncm 的匿名 RSS 拆清算清,并把能砍的砍到常态 ≤16MB。
|
||
|
||
### 32.2 内存画像(拆到每块,宿主 mem_sim + 设备实测 + 源码审计)
|
||
|
||
| 块 | 实测/审计 | 说明 |
|
||
|---|---|---|
|
||
| **封面 RGBA(大头)** | 1448×1448 → **8.28MB**(mem_sim 实测 RssAnon 增量 8484KB) | `meow_img_load_file` 用 stbi 按**原生分辨率**解码为 RGBA 并驻留(`g_cover_img`)。网易云全尺寸封面即此规模。 |
|
||
| 字库(fonts=common 档) | plane **2.44MB**(`default/regular/font.png` 1280×3996 depth4 灰度)+glyphs ~130KB | app.json `fonts:"common"`,launcher 注入 `MEOW_FONT_TIER=0`。plane 懒触达(绘制 glyph 才换页)。GBK 档约 17MB(launcher 注释),common 远小。 |
|
||
| bss 静态数组 | **1.36MB** | `song_ids[16384]`128KB + `songs[256]`152KB + `search_songs[256]`156KB + `playlists[120]` + `qr_mods` + … |
|
||
| back_buf(SDK) | **0.69MB** | 480×720×2(16bpp,mmap fb 之外仍 heap 分配一份 `g_back_buf`)。 |
|
||
| 线程栈 | musl 静态默认 **128KB/线程** × ~5-8 线程 ≈ 0.6-1MB | 非 glibc 8MB/thread;实测调用链栈深 ≤~30KB。 |
|
||
| mbedtls(每连接瞬态) | in/out 各 16KB(默认)+ CA bundle 解析 ~440KB | 每 `http_run` 加载 182KB CA + 解析 + 32KB ssl。播放+封面并发 ~2-3 连接。 |
|
||
| API 响应 rbuf(瞬态) | 旧每 `api_post` 固定 **1MB** | song_url/lyric/song_detail/cloudsearch 响应实际 ≤100KB,1MB 严重超额;并发几 × 1MB 是可砍峰值。 |
|
||
|
||
**结论**:真正的大头是**封面全分辨率 RGBA(8.28MB)**,其次瞬态 1MB rbuf。字库(common 2.44MB)
|
||
与 bss(1.36MB)不可由 ncm 轻易再砍(字库为系统字体档,bss 已是 §17 瘦身后)。
|
||
|
||
### 32.3 瘦身(按收益排序)
|
||
|
||
1. **封面降采样(-7.65MB)**:`patch_v2/sdk/meow_ui_img.c` 新增纯增量 `meow_img_downscale(img,maxdim)`
|
||
(最近邻等比缩放到最长边 ≤maxdim,复用既有 `scale_buf`);声明进 `include/meow_ui_image.h`。
|
||
`cover.c` 新增 `cover_cap()`(`NCM_COVER_DECODE_MAX=480`)在缓存命中/下载解码后统一降采样,
|
||
释放原大图驻留小图。播放页黑胶方图 + 歌词页模糊铺底仅需 ≤480px,全尺寸属浪费。
|
||
2. **瞬态 API rbuf 缩减**:`ncm_api.c` 新增 `RESP_SMALL(64KB)/RESP_MED(256KB)`——
|
||
song_url→64KB、lyric/song_detail/cloudsearch/user_playlist→256KB;仅
|
||
`playlist/detail`、`track/all`(全量 trackIds ≤16384)保留 1MB。
|
||
3. **线程栈显式**:`ncm_app_internal.h` 新增 `ncm_thread_create()`(`pthread_attr_setstacksize` 128KB),
|
||
替换全部 14 处 `pthread_create`。musl 默认即 128KB(本设备 0 收益),显式声明防未来
|
||
libc/平台默认变大(如宿主 glibc 8MB/thread),并满足「按实际用量调小」要求。
|
||
4. **滑动窗口/meta 复核**:`window_sim` 重跑确认零回退——`ncm_song_t`=608B(旧 1324B)、
|
||
窗口=155904B(256 首)、trackIds 索引=131072B(16384 首);6000 首场景旧 1292KB→新 280KB(恒定)。
|
||
|
||
### 32.4 验证记录
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 封面收益 | **mem_sim**(新 bench,链 host SDK):init 基线 → 载全尺寸封面 → downscale 480 | **PASS**:全尺寸 RssAnon +8484KB → 降采样后 -7836KB(8.28MB→0.78MB),尺寸 480×480 |
|
||
| 宿主渲染回归 | build_ncm_host_mem.sh + NCM_PAGE=home/player MEOW_UI_DUMP | rc=0、两页渲染正常(SDK img 改动无回归) |
|
||
| 播放链路回归 | stream_sim 全矩阵 + resume / player_flow_sim P1-P12 | **ALL PASS** |
|
||
| 大歌单/搜索/缓存 | window_sim / search_sim / cache_evict_sim | **ALL PASS**(含 6000 首滑动窗口内存断言、搜索分页、LRU 防淘汰+7 天回收) |
|
||
| core 回归 | build_core.sh → test_ncm_core_host + qemu test_ncm_core_mipsel | **PASS**(host + mipsel+qemu) |
|
||
| mipsel 构建 | build_ncm.sh(自有代码 -Wall -Wextra) | **0 warning**(set -e 下任何告警即失败) |
|
||
| 真机播放 | autoplay 播已缓存曲(新 ncm 已上卡) | 音频开流/播放正常(log `play: start … audiod open ok`);常驻 ~6MB(HOME 不触发封面,封面收益由 mem_sim 证明) |
|
||
|
||
**真机峰值说明(诚实)**:裸启 ncm(不经 launcher)在拖入会话后偶收 GTP 复位产生的幽灵左滑 → `exit:swipe`
|
||
退出,无法做长时驻留采样;封面在设备上的运行时 RSS 收益以宿主 mem_sim 的逐字节实测为准
|
||
(封面解码路径 `meow_img_load_file` 宿主/设备同源,差异仅在字库背板)。
|
||
|
||
### 32.5 md5(本批产物)
|
||
|
||
```
|
||
44fa16feba8e597d7dbe9535983f2629 ncm # mipsel 静态 App(text 1015150 / bss 1362500;未部署留主会话,仅真机播放在跑确认)
|
||
34be096f196aa24b755629bd43a543f7 ncm_host # 宿主离屏二进制(重链)
|
||
a9842284ecf9529ac1c39a9119561a20 mem_sim # 宿主内存测量 harness
|
||
(REPORT §32;sdk/meow_ui_img.c + include/meow_ui_image.h 变更见 git)
|
||
```
|
||
|
||
### 32.6 诚实出入 / 遗留
|
||
|
||
- **未部署**:新 ncm 已 scp 到设备 `/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm` 做真机播放在跑确认,
|
||
正式部署/固件打包留主会话统一。
|
||
- **字库(common 2.44MB)本批未动**:属系统字体档位,由 launcher 按 `MemAvailable` 护栏决定;
|
||
ncm 无法(也不应)自行缩减。若需更低,只能走「GBK/Full」降档之外的字体裁剪(另立批)。
|
||
- **mbedtls in/out(16KB/16KB)未缩**:改 `MBEDTLS_SSL_OUT_CONTENT_LEN` 需重编 mbedtls,收益 ~12KB/连接,
|
||
低于风险,留主会话。
|
||
- **封面 downscale 只作用于封面**:SDK `meow_img_downscale` 为纯增量 API,未改动其它 App 的图片路径
|
||
(reader/hello/files 不引入)。若后续发现其它 App 大图驻留,可复用同一 API。
|
||
- **真机长期峰值**:幽灵左滑导致无法持续采样,设备端「≤16MB」是「基线 ~6MB + 封面 0.78MB + 字库
|
||
2.44MB」的推演上限(约 8-9MB),低于 16MB 目标;正式部署后建议主会话做一次带 launcher 的
|
||
前台三场景采样复核。
|
||
|
||
---
|
||
|
||
## 33. 流媒专项批六项:流式 SIGSEGV 防御 / 档位保真 / 封面 4xx / 收藏显隐 / 缓存命名 / topbar 图标(2026-09-02)
|
||
|
||
> 范围:`patch_v2/ncm_app/`(+ 无 sdk/、meow_launcherd/ 改动)。用户报 6 项,本批逐项给
|
||
> 根因/修法/测试。风控纪律:**0 次真实 NCM 业务请求**(`test_ncm_core` 的 `test_network`
|
||
> 为既有只读冒烟——unikey/poll/无 cookie user_playlist 各 1 次,不下载、不改状态,见 §33.7 诚实出入)。
|
||
|
||
### 33.1 任务① 播放流式 SIGSEGV(最高优先):防御 + 日志定位(未稳定复现)
|
||
|
||
**症状**:真机播《十面埋伏》(id=409931770,hires 档 mp3)边下边播途中(写 ~721KB、播放 ~79s)
|
||
进程 `sig=11`;ams `crash dev.player.ncm sig=11`。并非 §30.4 的 VOL_DOWN×3 SIGKILL(那是 sig=9)。
|
||
|
||
**根因排查(本批,诚实)**:对 `stream_src`(pread+condvar+fread 语义 read_cb)、`ncm_cache`
|
||
写穿 `.tmp/.part`、`dl_thread`(http 状态感知回调 + 续传 200/206/416 分支)、`decoder`
|
||
(minimp3 `MP3D_SEEK_TO_BYTE|DO_NOT_SCAN` / dr_flac 回调)、`run_song` 解码主循环(stall 暂停/
|
||
恢复、seek 超已下载区、切档 seq 护栏)做了逐行审计,**未找出确定崩溃点**。另写临时 UBSAN
|
||
压力探针(去实时步调:极慢/断续网 18KB/s + `.part` 续传 40% 截断 + seek 90% + 停滞 pause/
|
||
resume 循环),跑 MIPSTest.mp3 / fuzz cut / MEANDR90 共 3 样本×多轮,**未触发 UB/崩溃**
|
||
(环境禁 ASAN —— `unable to mmap`;无 valgrind)。因此按任务允许的「不能稳定复现就加防御+日志」。
|
||
|
||
**修法(本批落地)**:
|
||
- `player_manager.c` 下载链路诊断日志(SIGSEGV 定位关键资产,崩溃后从 log 回溯):
|
||
- `song_download_attempt` 起播 `play: dl start id= lv= eff= size= resume= url=`;
|
||
- `song_src_dispose` 曲终 `play: dl done final= commit` / `dl interrupt dl_rc= kept bytes= -> .part`
|
||
/ `dl 416/refuse resume -> drop .part`;
|
||
- 解码循环播完 `play: done idx= frames= fin= exp= fail= underrun=`(区分自然完成 vs 截断)。
|
||
- **`song_src_dispose` 收尾竞态复核**:`stream_src_abort` → `pthread_join(dl_thr)` 顺序正确
|
||
(abort 唤醒下载回调 → cb 返回 NZ 中止 http → 线程收尾 join 返回);残留 `.part` 保留供续传。
|
||
- **诚实遗留**:①切歌时若下载线程正阻塞在 `mbedtls_ssl_read`(网络停摆),`pthread_join` 会
|
||
等 body 空闲超时(15s)——切歌延迟,非崩溃(http 层无 cancel 机制,另立批);②SIGSEGV 的确切
|
||
触发点需真机 `play: dl start/done` 日志落盘后二次定位;③本批未改 http 层(无取消/无钳峰)。
|
||
|
||
**验证**:stream_sim 全矩阵 + resume(PCM 金标准)**ALL PASS**;player_flow_sim P1-P12 **ALL PASS**;
|
||
UBSAN 探针无 UB(不在交付物,临时工具)。
|
||
|
||
### 33.2 任务② 档位保真:SQ 及以上必须 flac + 显示档按实际格式/码率
|
||
|
||
**根因**:`level_attempt`(`core/ncm_api.c`)对任意档接受 `type∈{mp3,flac}`——服务端对
|
||
hires/lossless 请求**静默返回 mp3** 时被当「命中」,播的是 mp3 却标「Hi-Res」(用户实锤
|
||
「十面埋伏无 Hi-Res 却显示 Hi-Res」);且显示档直接用降级链 `eff`,未对实际格式/码率做校验。
|
||
|
||
**修法**:
|
||
- ① `ncm_api.c level_attempt`:**`lv >= NCM_LEVEL_LOSSLESS` 命中判定必须 `type==flac`**,返回
|
||
mp3 一律视为「该档不可用」继续降级(mp3 只在 standard/higher/exhigh 接受;无损级及以上
|
||
服务端 encodeType=flac,返回 mp3 即静默降档)。type 缺失按 url 扩展名推断(默认 mp3)。
|
||
- ② `player_manager.c` 新增 `eff_level_for_resp(u, chain_eff, want)` 回填显示档:
|
||
`type==flac` → 采信链 `eff`;`type==mp3` → **绝不显示 Hi-Res 及以上**,按响应 `br` 判定:
|
||
`br≤128k→标准`,否则→`极高 HQ`(仅降级链中间档「较高 192k」不进 UI,不上屏);`br` 未知
|
||
钳 `chain_eff` 到 `≤EXHIGH`。调用点 `g_play.eff_level = eff_level_for_resp(...)`。
|
||
- 缓存命中路径(`song_local_file`)已有「mp3 实体 + 用户档>EXHIGH → EXHIGH」钳制,保留。
|
||
|
||
**测试**:
|
||
- `test_ncm_core.c` 新 `test_grade_flac_gate`(注入 try_fn,0 真实请求):①全曲库只有 mp3 →
|
||
hires/lossless 降到 exhigh(type=mp3);②hires 只有 mp3 + lossless 有 flac → 降到 lossless 拿
|
||
flac;③hires 有 flac → 命中 hires。**host + mipsel(qemu) PASS**。
|
||
- `player_flow_sim.c` P7 SVIP 子用例:SURROUND 为无损级 → 桩 type 改 `flac`,校验 eff=SURROUND
|
||
(显示档回填);P7 非会员/ P12 保持 EXHIGH 断言。
|
||
- `degrade_sim.c` 假取址 type 按档位(无损级→flac,`g_type==""` 保留空串测 url 扩展名推断),
|
||
auto_av 全矩阵 243 用例 + 真机场景(CVIP Hi-Res 有 SQ 无 HiRes → 落无损 SQ)**ALL PASS**。
|
||
|
||
### 33.3 任务③ 封面 4xx 加固:日志打 status / 确定性 4xx 不重试 / 仅瞬时退避
|
||
|
||
**根因**:`cover_worker` 对**所有**失败(含 404 死链)指数退避重试×8 放弃——404 重试纯空转;
|
||
失败日志只见 `fetch failed`,不知 HTTP 层返回了什么(§30.5 顺带遗留)。
|
||
|
||
**修法**(`app/logic/cover.c`):
|
||
- `download_cover` 增 `*out_rc/*out_status` 回填;失败分诊:
|
||
- **确定性 4xx**(`status∈[400,500)` 且非 408/429,含 404 死链)→ **不重试**,立即清目标
|
||
(cover_request 触达下一张);日志 `cover: fetch fail url=.. rc=%d status=%d (4xx, no retry)`。
|
||
- **瞬时错误**(超时/网络/TLS/5xx/空体)→ 指数退避重试(500ms→4s),连续 8 次放弃(原逻辑)。
|
||
- 失败日志统一带 `rc=%d status=%d`。
|
||
|
||
**验证**:`ncm_host` NCM_PAGE=home 离屏渲染 exit=0;逻辑分诊经 code review(无新 sim 覆盖
|
||
cover 网络态——cover_chain_sim 为存量、其网络为 mock,不引入)。
|
||
|
||
**附:cover URL 的 `http→443` 潜性缺陷评估(结论:不高风险,不修)**:`parse_url` 对无显式端口的
|
||
URL 恒置 `port=443`,且 `http_run` **无条件走 TLS**(`mbedtls_ssl_handshake`)。对 `http://` 的
|
||
封面 URL:当前代码连 443 做 TLS——对 music.126.net(443 上提供 TLS)恰好可用,**语义错误但未阻断**。
|
||
真正修正需「http 走明文端口 80 + 不加 TLS」的新路径(改核心 http 层、回归风险高),本批**不改**
|
||
(与 §30.8 结论一致);现状封面 URL 均为 https 不受影响。若后续曲库出现明文 http 图,再立批加明文路径。
|
||
|
||
### 33.4 任务④ 收藏按钮显隐:「我创建的」不显示收藏
|
||
|
||
**根因**:歌单详情页操作条恒画「收藏/评论」两按钮,未区分歌单归属——「我创建的」歌单也显示
|
||
收藏入口(点击仅 snack「收藏功能开发中」,语义错位)。
|
||
|
||
**修法**(`app/ui/playlist.c`):
|
||
- `pl_show_fav()`:home 打开的真实歌单(`active_playlist>=0`)`is_collected==0`(=我创建的)
|
||
→ 隐藏;`is_collected!=0`(我收藏的)→ 保留;搜索歌单(`NCM_PL_SEARCH`,无法判定归属)→ 保守显示。
|
||
- 绘制/命中一致化:`pl_draw` 不画收藏按钮;`playlist_on_tap` 收藏命中仅在 `pl_show_fav()` 时处理。
|
||
- `pl_cmt_rect()`:收藏隐藏时评论槽**并入**原收藏位(拉宽覆盖收藏+评论,无空洞),badge 随动。
|
||
|
||
**验证**:`ncm_host` NCM_PAGE=playlist(mock 打开「我创建的歌单」)渲染——操作条仅「播放全部」+
|
||
加宽「评论」,无「收藏」按钮(见 dump 截图);exit=0。
|
||
|
||
### 33.5 任务⑤ 缓存命名规范化:`song_<id>_<档位>.<ext>` + 存量双点迁移
|
||
|
||
**根因**:新键 `audio_cache_key=id_level` 已按用户定案生成 `song_<id>_<level>.<ext>`;但①卡上存量
|
||
`song_<id>..mp3`(旧版 ext 带前导点生成的双点畸形)/ 旧无档位键 `song_<id>.mp3` 仍被「温和兼容
|
||
只读不迁」,永远留存量;②`ncm_cache_lookup` 双点回退命中后不转正。
|
||
|
||
**修法**:
|
||
- `core/ncm_cache.c ncm_cache_lookup`:双点回退命中后**迁移重命名**为单点名(同 id+ext,
|
||
目标已存在则删旧双点),之后本条目走单点主路径,双点存量自然消亡。
|
||
- `app/play/player_manager.c song_local_file`(`hit==2` 旧键路径):改名到**当前档位键**
|
||
`song_<id>_<g_level>.<ext>`(`ncm_cache_path` 构造;目标已存在删旧;迁移失败回退旧路径照常播),
|
||
**不重下**。
|
||
|
||
**测试**:`test_ncm_core.c test_cache` 更新——双点 `song_104..mp3` lookup 迁移为 `song_104.mp3`、
|
||
旧双点文件删除、新文件 size=6。**host + mipsel(qemu) PASS**。
|
||
|
||
### 33.6 任务⑥ topbar 图标迁移:字体码点 → sb_icons 位图
|
||
|
||
**根因**:home topbar 三动作用字体码点 `↻(U+21BB)/⚙(U+2699)/⌕(U+2315)`,设备 4k 字库缺这些字形
|
||
→ 渲染成豆腐(WORK_STATE 310 前置症状①,¶302/§30 分析)。
|
||
|
||
**修法**(`app/ui/home.c`):三个动作 `meow_topappbar_set_item_icon` 换 sb_icons 位图
|
||
(`MEOW_ICON_REFRESH/SETTINGS/SEARCH`,307 已建 `meow_topappbar_set_item_icon` + meow_ui.a 已含);
|
||
字符标签保留作占位,绘制走位图(Material Symbols Rounded)。仅 home 页受影响
|
||
(settings/search/playlist topbar `action_label=NULL` 无码点动作;`ncm_topbar_create` 不变)。
|
||
|
||
**验证**:`ncm_host` NCM_PAGE=home 离屏渲染——topbar 三个**位图图标**正常显示(非豆腐,
|
||
见 dump 截图);mipsel 0 warning。
|
||
|
||
### 33.7 验证记录(0 真实 NCM 业务请求)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 档位保真① | `test_ncm_core` 新 `test_grade_flac_gate`(注入 try_fn) | host + qemu-mipsel **PASS** |
|
||
| 缓存迁移 | `test_ncm_core test_cache`(双点→单点迁移) | host + qemu-mipsel **PASS** |
|
||
| 播放流/边下边播 | stream_sim 全矩阵 + resume(PCM 金标准) | **ALL PASS** |
|
||
| 播放状态机 | player_flow_sim P1-P12 | **ALL PASS**(P7 flac 桩更新后) |
|
||
| 降级链语义 | degrade_sim(假取址 flac 化 + auto_av 全矩阵 243 + 真机场景) | **ALL PASS** |
|
||
| 大歌单滑窗 | window_sim(6000 首滚动 + 盘缓存回环) | **PASS** |
|
||
| 搜索 | search_sim | **PASS** |
|
||
| 缓存 LRU/隔离 | cache_evict_sim | **PASS** |
|
||
| 内存瘦身 | mem_sim(封面降采样 RssAnon 10260KB→2196KB) | **PASS** |
|
||
| core 全量 | build_core.sh → test_ncm_core_host + qemu test_ncm_core_mipsel | **PASS** |
|
||
| mipsel 构建 | build_ncm.sh(自有代码 -Wall -Wextra,set -e) | **0 warning** |
|
||
| UI 渲染 | build_ncm_host.sh + NCM_PAGE=home/playlist/player MEOW_UI_DUMP | exit=0,topbar 位图图标/收藏隐藏正常 |
|
||
|
||
### 33.8 md5(本批产物,未部署,留主会话统一)
|
||
|
||
```
|
||
cb8187868f71a9fa155075d0624711d6 ncm # mipsel 静态 App(text 1017110/bss 1362500;0 warning)
|
||
00cbefd5f5f2bb7eef4c7bdccaff859b ncm_host # 宿主离屏二进制(重链)
|
||
97bb2bdb44a2f446877787d9261b4f02 test/test_ncm_core_host # 宿主 core 测试(重编,含新档位保真/缓存迁移)
|
||
21665d662755e767742cd46ec4880692 cache_evict_sim # 重链(新 ncm_cache.c)
|
||
968fbb79671186e8a0bef5a2ff12675c degrade_sim # 重链(假取址 flac 化)
|
||
2c87ea9cf040490c75d06952add6c15a player_flow_sim # 重链(P7 flac + eff 断言)
|
||
(window_sim/search_sim md5 未变=5597307a/1eb36c74,同样 PASS;ncm_host 由 build_ncm_sims.sh 未触)
|
||
```
|
||
|
||
### 33.9 出入清单(只改 `patch_v2/ncm_app/`)
|
||
|
||
```
|
||
修改:
|
||
core/ncm_api.c # §2① level_attempt 无损级必须 flac(32.1)
|
||
core/ncm_cache.c # §5 ncm_cache_lookup 双点→单点迁移重命名(33.5)
|
||
app/play/player_manager.c # §2② eff_level_for_resp 显示档回填 + §5 旧键→档位键迁移 + §1 下载/deadline 日志
|
||
app/logic/cover.c # §3 封面 4xx 分诊 + rc/status 日志 + download_cover 回填
|
||
app/ui/home.c # §6 topbar 动作换 sb_icons 位图
|
||
app/ui/playlist.c # §4 收藏按钮显隐(pl_show_fav/pl_cmt_rect)
|
||
test/test_ncm_core.c # 新 test_grade_flac_gate + test_cache 双点迁移断言
|
||
bench/player_flow_sim.c # P7 flac 桩 + ncm_cache_path 桩(§5 迁移调用链)
|
||
bench/degrade_sim.c # 假取址 type 按档位(无损级→flac,"" 保空测 url 推断)
|
||
REPORT.md / devdocs/WORK_STATE.md # 本批记录(§33 / 锚点)
|
||
(未动 sdk/、meow_launcherd/、其它 App;0 真实请求;未部署——留主会话统一上卡)
|
||
```
|
||
|
||
### 33.10 诚实出入 / 遗留
|
||
|
||
- **流式 SIGSEGV 未复现未定位**:审计 + UBSAN 探针未找到确定崩溃点;本批落地**防御日志**
|
||
(dl start/done/interrupt + play done 的 fin/exp/fail/underrun)。真机再崩时凭这些日志定位。
|
||
初步方向(未证实):下载线程阻塞在 `mbedtls_ssl_read` 时切歌 → `pthread_join` 等 body 空闲超时
|
||
(切歌延迟,非崩溃);minimp3 对特定码流帧边界、`seek 未下载区 + 切档重播` 竞态均未排除。
|
||
- **封面 4xx 无新网络 sim**:cover 网络态仍由存量 cover_chain_sim 的 mock 覆盖;生产 4xx 分诊
|
||
经 code review 验证,真机待部署后观察 `cover: fetch fail rc= status=` 日志。
|
||
- **`http→443` 不修**(§33.3 评估);**`debug` 档位「较高」不进 UI**(显示回填把 >128k mp3 一律
|
||
标「极高 HQ」,对 160/192k 略高估——UI 八档无「较高」位,属显示取舍,NCM_CACHE_DESIGN §3)。
|
||
- **`test_ncm_core` 的 `test_network` 为既有只读冒烟**(unikey/poll 各 1 次、无 cookie
|
||
user_playlist、bogus id song_url),不下载不改状态,本批沿用;其余全部离线 stub/mock。
|
||
- **未部署真机**(ncm `cb818786` 留主会话统一上卡)。真机验收点:①播《十面埋伏》不再崩/或
|
||
凭 `play: dl` 日志定位;②hires 档静默给 mp3 显示「已降级至 极高 HQ」而非 Hi-Res;③封面 404
|
||
立即跳过不空转;④我创建的歌单无收藏按钮;⑤缓存目录名 `song_<id>_<level>.<ext>` 无双点;
|
||
⑥home topbar 三动作显示位图图标(非豆腐)。
|
||
|
||
---
|
||
|
||
## 34. 封面链分诊与修复:防盗链 403 / webp 解码失败 / 尺寸参数 / 分诊日志(2026-09-02)
|
||
|
||
> 真机已实锤三种封面失败形态(详见下「真机三分型」):①404 死链(CDN 源站无对象,`X-Error-Info: Origin`,
|
||
> 182B JSON 错误体,311/§33.3 已做 4xx 不重试);②**403 防盗链**(p3.music.126.net 返 150B text/html,
|
||
> 缺 Referer/UA 被拦嫌疑大);③**200 但解码失败**(rc=0 status=200 仍 fetch failed——响应体**非 webp,
|
||
> 实为源站死对象返 200+JSON 错误体**,stb_image 解不了,本批矩阵实锤)。
|
||
|
||
### 34.1 真机三分型结论(本批已在设备端跑成 CDN 行为矩阵,见 34.6 明细)
|
||
|
||
设备 10.78.193.50 本批初段 `No route to host`(sleep/WiFi 休眠),后恢复;用 BusyBox `wget`(无 curl)
|
||
单次只读对日志抓出的真实封面 URL 跑了 header/param 变体矩阵,**实锤结论**(详见 34.6 原始结果):
|
||
|
||
- **裸请求(无 Referer)→ 403**:CDN 防盗链按 Referer 拦截——**② 403 防盗链坐实**,修法 = 加 Referer。
|
||
- **+Referer 仍不返真图**:全变体只看到 **403 / 404 / 200+JSON 错误体(43B,`content-type:
|
||
application/json`,magic `{"code`)**,从未见 JPEG/PNG/BMP/webp。**③「200 解码失败」真因 = 源站死对象
|
||
返 200+JSON 错误体**(非 webp)——stbi 解不了 → 旧逻辑当成「fetch failed」盲指数退避 8 次(真机日志
|
||
`rc=0 status=200` + `retry` 6 连)。故修法重心从「webp」改为「200 且 body 非已知可解码图 → 确定性不重试」。
|
||
- **UA 分表实测**:`Mozilla/5.0 CNMPlayer/0.1`(CNMPlayer 口径)同 Referer 也返 **403**(CDN 拦 bot 串);
|
||
旧 cover.c 的**桌面 Chrome UA** 设备实测返 **200**。故封面 UA 用**桌面浏览器**(非 CNMPlayer UA),
|
||
Referer 仍对齐 CNMPlayer。**参数修正:本批 UA 由初提交的 CNMPlayer 口径改为桌面 Chrome**(见 34.2)。
|
||
- **`?param=300y300`**:在死对象上→404(对死链无效);对存活 URL 推测可返 300×300 JPEG(网易云标准缩略
|
||
参数),但本批测试 URL 全为死链未能直接证实——保留 `NCM_COVER_USE_PARAM=1`,真机存活封面再确认。
|
||
|
||
### 34.2 修法(`app/logic/cover.c` + `app/ncm_app_internal.h`)
|
||
|
||
- **请求头**:`Referer: https://music.163.com/`(防盗链,解 ②)+ **桌面浏览器 UA**(真机矩阵:CNMPlayer UA
|
||
被拦 403,桌面 Chrome UA 返 200——解 UA 分表)。常量 `NCM_COVER_REFERER`/`NCM_COVER_UA` 一处可改。
|
||
- **200 体先判类型(kind)后解码**:`download_cover` 成功后、`meow_img_load_file` 前回填 `*out_kind`:
|
||
- `1`=WebP(content-type 含 webp 或魔数 `RIFF....WEBP`,stb 不支持)→ 不解码;
|
||
- `2`=非图(content-type 非 `image/` 前缀,或魔数非 JPEG/PNG/BMP/GIF——**源站死对象 200+JSON 错误体**
|
||
即此)→ 不解码;
|
||
- `0`=可解码图(JPEG `FFD8FF`/PNG `89504E47`/BMP `424D`/GIF `474946`) → `meow_img_load_file` 正常解码。
|
||
`body_is_decodable_img` 读临时文件前 8 字节魔数 + content-type 判定。
|
||
- **worker 分诊**:kind=1 打 `cover: webp unsupported url=.. (no decode)`;kind=2 打
|
||
`cover: not an image (origin error body/200) url=.. (no retry)`;403 单独打
|
||
`(anti-hotlink/forbidden, no retry)`;404/其它确定性 4xx `(4xx, no retry)`;**kind=1/2 与确定性 4xx 一律
|
||
立即清目标走兜底(保留歌单封面),不盲目指数重试**(根修 ③:死对象 200+JSON 体不再空转 8 次);
|
||
仅瞬时错误(超时/网络/TLS/5xx)指数退避。
|
||
- **封面 URL 追加尺寸参数**:`cover_norm_url`(`cover_request` 入口)对不含 `param=` 的 URL 追加
|
||
`?param=300y300`(已有 `?` 用 `&`;溢出 512 保原 URL)。`NCM_COVER_USE_PARAM=1`;`NCM_COVER_PARAM` 放
|
||
共享头 `ncm_app_internal.h`,与 sim 的 URL 期望哈希同源。省内存(300×300≈20KB vs 全尺寸 1.34MB)
|
||
+ 存活 URL 出 jpeg。
|
||
|
||
### 34.3 worker 队列评估(304 建议:单槽→有界 FIFO+优先级)
|
||
|
||
**结论:不改。** 当前 cover worker 为「单槽 latest-wins」(`g_tgt_url` 被最新 `cover_request` 覆写)——
|
||
对封面这种**仅当前播放页需要**的瞬态展示资产,语义已最优:切歌即最新目标,旧请求在途完成时因
|
||
`g_tgt_url` 变更被丢弃(`if (strcmp(g_tgt_url, url)==0)` 判元),天然取消过期封面。改成有界 FIFO+
|
||
优先级会为**不会显示的过期封面**白付解码 CPU + 驻留内存(设备 RAM 仅 56MB),净负收益。304 原始
|
||
关切(4xx 空转重试占 worker)已由本批「确定性 4xx/webp 不重试 + 瞬时退避」根治。留后续共识,不迁移。
|
||
|
||
### 34.4 验证记录(宿主 + qemu;0 新增真实 CDN 业务请求——设备矩阵测试已执行且为单次只读)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| cover 链(含 webp/403/非图) | `cover_chain_sim`(驱动真实 cover.c + ncm_cache,HTTP 本地 stub;T1-T7 存量 + 新增 T8 webp 魔数 / T9 403 / T10 200+JSON 非图) | **T1-T10 ALL PASS**(T8:webp 单次下载不重试+旧封面兜底+日志 `webp unsupported`;T9:403 单次下载不重试+日志含 403;T10:JSON 错误体单次下载不重试+日志 `not an image`) |
|
||
| 缓存 LRU/隔离 | `cache_evict_sim` | **PASS** |
|
||
| 播放状态机 | `player_flow_sim` P1-P12 | **PASS** |
|
||
| 大歌单滑窗 | `window_sim` | **PASS** |
|
||
| 搜索 | `search_sim` | **PASS** |
|
||
| 边下边播 | `stream_sim` 全矩阵 | **PASS** |
|
||
| 降级链 | `degrade_sim`(含真机场景) | **ALL PASS** |
|
||
| 内存瘦身 | `mem_sim`(MEM_SIM=1) | **PASS** |
|
||
| core | `build_core.sh` → `test_ncm_core_host` + `qemu test_ncm_core_mipsel` | host + mipsel **PASS** |
|
||
| 构建 | `build_ncm.sh`(自有代码 -Wall -Wextra) | **0 warning**;`text 1020306 / data 1648 / bss 1362500`(bss 与 §33 持平,封面改动未涨静态内存) |
|
||
| 宿主构建 | `build_ncm_host.sh`(自有代码) | 0 warning(仅 SDK `meow_ui_vol.c` 存量 `system()` 未用结果告警,非本批) |
|
||
| UI 渲染 | `build_ncm_host.sh` + `NCM_PAGE=home/player/playlist MEOW_UI_DUMP` | exit=0,dump 正常 |
|
||
|
||
### 34.5 md5(本批产物)
|
||
|
||
```
|
||
06896b54f31ddf55980cbf921a00d67a ncm # 新(未部署,留主会话上卡)
|
||
2f293d46b535519c2a5b1cb97f0eba9a ncm_host # 宿主调试二进制(重链)
|
||
97bb2bdb44a2f446877787d9261b4f02 test/test_ncm_core_host # 宿主 core 测试(重编,内容未变)
|
||
```
|
||
|
||
改动文件(本批,仅这些):
|
||
```
|
||
修改:
|
||
app/logic/cover.c # 头 Referer(CNMPlayer口径)+桌面UA(矩阵实证CNMPlayer UA被拦403) + 200体类型先判(webp/非图/可解码) + URL 尺寸参数 + 403/webp/非图 分诊日志 + 兜底不盲重试(34.2)
|
||
app/ncm_app_internal.h # NCM_COVER_USE_PARAM / NCM_COVER_PARAM 共享宏(cover.c 与 sim 同源)
|
||
bench/cover_chain_sim.c # 修 sim 对齐现行 cover.c(ncm_http_download/meow_img_load_file/downscale 桩 + url_hash 参数规则)+ 新增 T8 webp / T9 403 / T10 200+JSON 非图 用例
|
||
ncm # mipsel 产物(重链)
|
||
ncm_host # 宿主产物(重链)
|
||
```
|
||
(未动 sdk/、meow_launcherd/、其它 App、devdocs/;0 新增真实 CDN 业务请求——设备矩阵为单次只读;未部署——留主会话统一上卡。)
|
||
|
||
### 34.6 诚实出入 / 遗留
|
||
|
||
- **设备端 CDN 矩阵已执行(结果见 34.1)**:设备恢复后用 BusyBox `wget`(无 curl/xxd,`od -An -tx1` 取魔数)
|
||
对日志抓出的真实封面 URL(p1/p3/p4.music.126.net)跑了 裸/`+Referer`/`+UA`/`?param` 变体。**限制**:
|
||
①设备端 wget 的 UA/Referer 组合仅有限几组(BusyBox wget 不擅多 UA 变体交互,`CNMPlayer/0.1` 带空格
|
||
的 UA 曾致 `eval` 引号坏、个别变体 ERR);②**所有测试封面 URL 均为死链**(从未返真图),故 `?param=300y300`
|
||
转唯对存活 URL 的效果、iPhone NeteaseMusic 9.0.90/5038 UA 变体未获存活样本证实——**建议存活封面补测**:
|
||
`param` 后缀与 iPhone UA 两变体在真机播放时观察 `cover: fetch fail/ok` 日志即可(按已就位宏 `NCM_COVER_UA`
|
||
/`NCM_COVER_USE_PARAM` 一处改)。
|
||
- **UA 选用与 CNMPlayer 口径不同**:CNMPlayer 客户端用 `Mozilla/5.0 CNMPlayer/0.1`,但本批矩阵在设备端实测
|
||
该 UA 同 Referer 也返 **403**(CDN 拦 bot 串),旧 cover.c 的桌面 Chrome UA 返 200——故封面 UA 定为
|
||
**桌面浏览器**。若后续真机存活封面用 CNMPlayer UA 亦可,仅改 `NCM_COVER_UA` 宏。
|
||
- **「200 解码失败」根因修正**:原任务书怀疑 webp;矩阵实锤**源站死对象返 200+JSON 错误体**(43B `{"code`,
|
||
`application/json`),非 webp。故本批以「200 且 body 非已知可解码图(含 JSON 错误体/未知)→ 确定性不重试」
|
||
为核心修法(kind=2),webp(kind=1)保留为附加分支。三者(403 / 404 / 200+非图)修法不同且都有明确日志。
|
||
- **`?param=300y300` 潜在风险**:个别封面若仅存在全尺寸对象,param 版可能 404——已由确定性 4xx/非图
|
||
不重试兜底(回退歌单封面),不劣于基线;但缓存键随之变化(旧全尺寸 `cov<rawhash>.jpg` 成为孤儿,LRU 淘汰)。
|
||
- **`http→443` 潜性缺陷不修**(承 §33.3/§30.8 结论)。注意设备日志中确出现 `http://p1.music.126.net/...`
|
||
明文封面 URL(非全 https),本批未改明文路径(语义错误但 443 上 TLS 可用);若曲库明文 http 图变多,
|
||
另立批加明文 80 路径。
|
||
- **真机封面加载成功率提升未实测**(本批未部署——留主会话上卡后播放几首观察 `cover:` 日志,预期 404/403/
|
||
非图幂等快速跳下一张+歌单封面兜底,不再 8 次空转)。
|
||
|
||
---
|
||
|
||
## 35. 封面死链兜底链:原 picUrl → song/detail 重取 → 歌单封面兜底(2026-09-02)
|
||
|
||
> 用户调研:官方旧版 `GET music.163.com/api/song/detail/?id=<id>&ids=[<id>]` **免登录**直接出 JSON,
|
||
> `songs[0].album.picUrl`(还有 blurPicUrl)。价值:登录态 eapi 给的 picUrl 死链时,这个 web 端接口
|
||
> 可能给出**不同活链**(不同接口指向不同 CDN 对象)。本批宿主实锤:死链 `p4.music.126.net/yR2VGt7.../10995116862531`
|
||
> 的 song 经 web 接口返回 `album.picUrl = p2.music.126.net/6y-UleORIT.../5639395138885805.jpg`(活 PNG 146KB)
|
||
> 与 `blurPicUrl`(活 JPEG 1.34MB 全尺寸)——兜底价值坐实。
|
||
|
||
### 35.1 兜底链实现(`app/logic/cover.c` + `app/ui/player.c` + `app/ui/playlist.c` + `app/ncm_app_internal.h`)
|
||
|
||
**链路**:原 picUrl(确定性 404/403/200+非图/webp)→ 【下探】song/detail 重取新 picUrl → 与原 URL 不同则
|
||
再拉一次(仍走 Referer+桌面 UA+`?param`+魔数判定)→ 仍失败/无活图 → **歌单封面兜底**(保留当前显示)。
|
||
|
||
- **`cover_request(url, song_id)`**:新增第二参 `song_id`(曲目专辑封面传 `s.id`;歌单封面传 NULL)。
|
||
`g_tgt_song` 记录当前目标曲目;新目标重置 `g_fb_done=0`。player.c 三处传 `s.id`,playlist.c 传 NULL。
|
||
- **`cover_song_detail_pic(song_id, pic)`**(新增):`GET music.163.com/api/song/detail/?id=<id>&ids=[<id>]`
|
||
(`[ ]` URL 编码为 `%5B %5D`——宿主 curl 实测裸 `[ ]` 报 bad range),带 Referer+桌面 UA(同封面头),
|
||
`ncm_http_request`(头进 resp->headers、体进 16KB 栈缓冲)→ `ncm_json` 取 `songs[0].album.picUrl`,
|
||
缺则 `blurPicUrl`。只读一次。
|
||
- **`cover_try_fallback(url, song_id)`**(新增):仅当 `g_tgt_url` 仍 == url(目标未被 UI 换走)、曲目非空、
|
||
尚未兜底、且**同曲 5 分钟冷却**(`g_fb_song`/`g_fb_ms`,`ncm_monotonic_ms`)未命中才触发;拿到新图且
|
||
与旧不同 → 记冷却、置 `g_fb_done=1`、换 `g_tgt_url` 为新图,返回 1(worker 重试新图);无新图/冷却中/
|
||
目标被换走/已兜底 → 返回 0(走歌单封面兜底)。每步打日志写清级别(`song-detail fallback id=.. -> url` /
|
||
`song-detail id=.. no live pic -> playlist cover` / `song-detail fallback cooldown id=.. -> playlist cover`)。
|
||
- **worker 接线**:kind=1(webp)/kind=2(非图)/确定性 4xx 四处死链分支,清目标前先
|
||
`if (song[0] && !fb_done && cover_try_fallback(url, song)) { continue; }`——命中即重试新图;
|
||
否则照旧清目标走歌单封面兜底。瞬时错误(网络/TLS/5xx)仍指数退避,不触发兜底(非死链)。
|
||
- **风控**:song/detail 只在原链失败触发(不新增常规请求),单曲目冷却 5 分钟、最多一次(`g_fb_done`)。
|
||
|
||
### 35.2 验证(宿主 + qemu;0 新增真实 CDN 业务请求——web 接口仅宿主单次只读验证端点,见 35.4)
|
||
|
||
| 项 | 方式 | 结果 |
|
||
|---|---|---|
|
||
| 兜底链(新用例) | `cover_chain_sim`:T1-T10 存量 + **T11 原链死→song/detail 活链→出图** + **T12 原链死→兜底也死→歌单封面兜底 + 同曲冷却不重试** | **T1-T12 ALL PASS**(T11:守到 URL_LIVE 出图 + song/detail 恰 1 次 + 日志 `song-detail fallback`;T12:兜底也死留旧封面 + 再请求同曲 song/detail 0 次(冷却)+ 日志 `cooldown`) |
|
||
| 缓存 LRU/隔离 | `cache_evict_sim` | **PASS** |
|
||
| 播放状态机 | `player_flow_sim` P1-P12 | **PASS** |
|
||
| 大歌单滑窗 | `window_sim` | **PASS** |
|
||
| 搜索 | `search_sim` | **PASS** |
|
||
| 边下边播 | `stream_sim` | **ALL PASS** |
|
||
| core | `build_core.sh` → `test_ncm_core_host` + `qemu test_ncm_core_mipsel` | host + mipsel **PASS** |
|
||
| 构建 | `build_ncm.sh`(-Wall -Wextra) | **0 warning**;`text 1022210 / data 1648 / bss 1362564`(bss 仅 +64B,兜底曲目/时间戳 statics) |
|
||
| 宿主构建 | `build_ncm_host.sh` | 0 warning(仅 SDK `meow_ui_vol.c` 存量 `system()` 告警,非本批) |
|
||
| UI 渲染 | `NCM_PAGE=home/player/playlist MEOW_UI_DUMP` | exit=0,dump 正常 |
|
||
|
||
### 35.3 md5(本批产物)
|
||
|
||
```
|
||
e1f4b7ade834b18c7910b9c3e64bc858 ncm # 新(含兜底链;未部署,留主会话上卡)
|
||
d102c64f7fab73e981a789c5960e6bd9 ncm_host # 宿主调试二进制(重链)
|
||
97bb2bdb44a2f446877787d9261b4f02 test/test_ncm_core_host # 宿主 core 测试(重编,内容未变)
|
||
```
|
||
|
||
改动文件(本批,仅这些):
|
||
```
|
||
修改:
|
||
app/logic/cover.c # cover_request 增 song_id + g_tgt_song/g_fb_song/g_fb_ms/g_fb_done + cover_song_detail_pic + cover_try_fallback + worker 死链分支先试兜底 + 头/注释
|
||
app/ui/player.c # 三处 cover_request 传曲目 s.id(触发专辑封面兜底)
|
||
app/ui/playlist.c # cover_request(pcur->cover, NULL)(歌单封面无兜底)
|
||
app/ncm_app_internal.h # cover_request 声明加 song_id
|
||
bench/cover_chain_sim.c # sim 补 ncm_json/jsmn/ncm_common 桩 + song/detail/dead/alt stub + T11/T12 + 双参 cover_request
|
||
ncm / ncm_host # 产物重链
|
||
```
|
||
(未动 sdk/、meow_launcherd/、其它 App、devdocs/;0 新增真实 CDN 业务请求;未部署——留主会话统一上卡。)
|
||
|
||
### 35.4 诚实出入 / 遗留
|
||
|
||
- **web 端点有效性**已宿主单次只读验证(`ids=%5B<id>%5D`,`code:200`,`songs[0].album.picUrl`/`blurPicUrl`
|
||
非空且为活图)——但**未在真机跑通整条兜底链**(本批未部署,留主会话上卡后观察 `cover: song-detail fallback`
|
||
日志)。真机预期:死链封面切歌时后台先撞原链(1 次 404/403/非图),立即 song/detail 重取(1 次,
|
||
免登录、带 Referer/UA),拿到活链二次拉图出封面;活链封面不再有兜底开销。
|
||
- **song/detail 响应缓冲 16KB**(栈):单曲 detail ~3.3KB 足够;个别超大响应 >16KB 会 `NCM_ERR_LIMIT`
|
||
→ 兜底取不到图回落歌单封面(可接受;未做动态增长,规避设备 RAM 压力)。
|
||
- **瞬时 song/detail 失败不重试**:song/detail 网络/TLS 瞬断 → 视为「无活图」回落歌单封面并记冷却
|
||
(同曲 5 分钟内不重撞)。保守但避免高频撞接口——重试救不了死链接,瞬断概率低。
|
||
- **冷却按曲目 id**(`g_fb_song`):切回同一曲 5 分钟内不重复兜底;换曲立即解锁。兜底后 `g_fb_done=1`
|
||
防止同 incident 二次兜底(新图也死时不再回 song/detail)。
|
||
- **歌单封面兜底语义未变**:兜底链两级都失败后仍清目标、保留当前显示封面(歌单封面在进歌单时已由
|
||
`cover_request(pcur->cover, NULL)` 取到)。未新增对 `g_st.cover_url` 的主动重取。
|
||
|
||
## 36. 风控加固批:接口级响应短 TTL 缓存 + 游客 MUSIC_A + track/all 死代码(2026-09-02)
|
||
|
||
> 依据:NCMAPI_NOTES(319 权威接口地图)§2.1/§4/§6。仅改 `patch_v2/ncm_app/`。
|
||
> 三项:①位只读接口响应缓存(对齐参考 apicache 2 分钟);②游客 `MUSIC_A=anonymous_token`;③清理死代码。
|
||
|
||
### 36.1 ① 接口级响应短 TTL 缓存(user_playlist / playlist_detail / song_detail / cloudsearch)
|
||
|
||
- 新增 **`core/ncm_apicache.{c,h}`**(自包含:仅 libc+pthread+clock_gettime,不依赖 ncm_store/ncm_crypto/ncm_common 函数,可独立链接)。
|
||
- **内存**:4 槽 LRU,单槽 ≤128KB,TTL 默认 120s;键 = md5(`path` + `\x01` + 参数 json)。超 TTL 过期、超容量逐 LRU 最旧。
|
||
- **盘(可选)**:`<data_dir>/ncm_api_cache/<key>.json`,文件头 8B 墙钟毫秒 + body;超 TTL 视为 miss(并删除);写入按 30s 节流清过期文件,磁盘占用有界。
|
||
- 失效:登出/换账号 `ncm_apicache_clear` 全清;盘超 TTL 自然过期。
|
||
- **`core/ncm_api.c`**:`api_post` 拆为「请求(ncm_http)+解析」两段;新增 `api_post_cached`(先查缓存、miss/过期/手动刷新→真网络、成功后写缓存)。四个只读接口路由到 `api_post_cached`:`user_playlist`(weapi) / `playlist_detail`(eapi,v6) / `song_detail`(eapi,v3) / `cloudsearch`(weapi)。`song_url`/`lyric`/`vip` **不入缓存**(逐曲/需新鲜/低频)。
|
||
- **手动刷新绕过**:`ncm_api_cache_refresh_path(path)` 标记某路径下一次调用绕过缓存(一次性消费)。接入 home topbar ↻(`/api/user/playlist`) 与歌单 topbar ↻(`/api/v6/playlist/detail`);`playlist_open`/`search` 不标记(走缓存)。
|
||
- 盘层挂载:`appstate_init` → `ncm_apicache_init(g_st.data_dir)`;`appstate_logout` → `ncm_apicache_clear()`。
|
||
|
||
### 36.2 ② 游客 MUSIC_A(未登录 cookie 伪装,NCMAPI_NOTES §4)
|
||
|
||
- `core/ncm_req.c`:weapi 分支(无 `MUSIC_U` 且非 login 路径)补 `MUSIC_A=anonymous_token` 进会话 cookie;eapi 分支同样(无 `MUSIC_U` 且非 login 路径)追加到 eapi Cookie 头。与参考「常驻 cookie 伪装」对齐,降低未登录需登录接口被服务端判定异常(401/400 需验证)概率。
|
||
|
||
### 36.3 ③ track/all 死代码清理(NCMAPI_NOTES §1.1 实锤 404)
|
||
|
||
- `core/ncm_api.c`/`.h`:**移除**网络封装 `ncm_api_playlist_track_all`(真实服务器无 `/api/playlist/track/all`,8 形态全 404;无任何调用点)。
|
||
- `ncm_api_parse_track_all`(离线解析器)**保留**供 `offline_track_all`/`probe_track_all`/`test_ncm_core` 离线重放/单测(0 请求),注释标注「已验证端点不存在,仅离线单测用,勿用于真实请求」。
|
||
|
||
### 36.4 验证(宿主 + qemu;新缓存用例 0 真实网络请求)
|
||
|
||
| 项 | 结果 |
|
||
|---|---|
|
||
| 新增 `bench/apicache_sim.c`(真实 core + ncm_http 计数桩,0 网络) | **ALL PASS**:TTL 内不重复请求 / 过期重拉 / 手动刷新绕过 / cloudsearch 按键区分 / 清空重拉 / 盘缓存写入+clear |
|
||
| `stream_sim` / `player_flow_sim` / `window_sim` / `search_sim` | ALL PASS |
|
||
| `cache_evict_sim`(1-9)/ `pagination_sim` / `playlist_race_sim` | PASS |
|
||
| `test_ncm_core_host` + `qemu test_ncm_core_mipsel` | host + mipsel **PASS** |
|
||
| `build_ncm.sh`(-Wall -Wextra) | **0 warning**;`text 1026914 / data 1664 / bss 1363460` |
|
||
| `build_ncm_host.sh` | 0 warning(仅 SDK `meow_ui_vol.c` 存量 `system()` 告警,非本批) |
|
||
| UI 渲染 `NCM_PAGE=home MEOW_UI_DUMP` | exit=0,dump 正常 |
|
||
|
||
改动文件(本批,仅这些;未动 sdk/、devdocs/、其它 App):
|
||
```
|
||
修改:
|
||
core/ncm_api.c # api_post 拆分 + api_post_cached + 四接口接线 + 刷新路径 + 移除 track_all 网络版 + 解析器注释
|
||
core/ncm_api.h # 移除 ncm_api_playlist_track_all 声明 + parse_track_all 注释 + ncm_api_cache_refresh_path
|
||
core/ncm_req.c # weapi/eapi 游客 MUSIC_A 注入
|
||
core/ncm.h # 伞头加 ncm_apicache.h
|
||
app/logic/appstate.c # init/logout 接 ncm_apicache
|
||
app/ui/home.c # 手动刷新标记 /api/user/playlist
|
||
app/ui/playlist.c # 手动刷新标记 /api/v6/playlist/detail
|
||
build_core.sh / build_ncm.sh / build_ncm_sims.sh # 加 ncm_apicache.c;window/search sim 链 ncm_apicache.o
|
||
ncm / ncm_host / test/test_ncm_core_host # 产物重链
|
||
新增:
|
||
core/ncm_apicache.c / core/ncm_apicache.h # 接口级响应短 TTL 缓存(内存+盘)
|
||
bench/apicache_sim.c # 缓存用例(0 网络)
|
||
REPORT.md # 本批记录(§36)
|
||
```
|
||
|
||
### 36.5 遗留 / 诚实出入
|
||
|
||
- **缓存 TTL=120s 的内存槽上限 4×128KB**:大歌单 `playlist_detail`(trackIds >128KB)不入内存、仅盘缓存(内存瘦身口径 REPORT §32);命中读盘+重解析,避免把上千首 trackIds 常驻内存。
|
||
- **手刷只标详情接口**:歌单手动刷新只绕过 `v6/playlist/detail`(列表结构),`song_detail` 批 2 分钟内走缓存(改动歌曲为新键自然 miss,元数据 ≤2min 旧可接受);home 刷新绕过 `user/playlist`。
|
||
- **eapi 分支未注入 MUSIC_U**(登录态 eapi 播放接口仍走原有的非-cookie 鉴权路径)——本次仅按任务补游客 MUSIC_A;MUSIC_U 头注入留后续(超范围)。
|
||
- **盘缓存读路径未在真机/宿主整链实测**(内存命中优先,盘仅在内存 miss/大体时兜底)——代码审查 + 文件存在性断言验证;真机可观察 `ncm_api_cache/` 落盘与热更后命中。
|