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

39 KiB
Raw Blame History

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 硬上限」保证,未改为后台线程(避免范围扩散)。