feat(ncm §24): 流媒体客户端边下边播阶段3——.part断点续传(Range/206/200/416)+LRU防淘汰(.tmp/.part跳过)+失败重试1次退避

按 devdocs/plan/NCM_STREAM_PLAY.md §8 阶段3:
- .part 断续传:中断留 .part,续传带 Range,206 追加 / 200 丢头段 / 416 删残留整首重下;URL 每次新拉
- LRU 防淘汰:evict/total_bytes 跳过 .tmp/.part(在播文件不被动过)
- 失败重试:预缓冲前网络失败重试 1 次,退避 800ms,重试拿新签名 URL(风控:0 真实请求,未部署)
- 顺手修:gb_to_bytes 返回 int 溢出致默认 10GB 上限失效 -> uint64_t
- 验证:stream_sim(续传 PCM 金标准+全矩阵)/player_flow_sim(P9/P10/P11)/cache_evict_sim(case7/8)/test_ncm_core 宿主+qemu-mipsel 全 PASS;build_ncm.sh 0 warning;ncm_host 离屏正常

md5 ncm f8f316a1e6f3bdd8f6b5921fdbb3f3a2
This commit is contained in:
MeowOS committed 2026-09-01 20:00:14 +08:00
1 parent fcff0b0ac1
commit 19e8fc93ed
14 files changed
+735 -93

No files matched your search

+90
View File
@@ -2228,3 +2228,93 @@ c9c115cda1340b92d1988e330eafbb5b bench/search_sim.c
- **`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 留主会话。