Files
MeowOS-For-R3P2/patch_v2/ncm_app/REPORT.md
T

359 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 冲突)。