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

+122 -2
View File
@@ -116,6 +116,7 @@ typedef struct {
int jitter_ms; /* 每块延迟 ±jitter */
double cut_frac; /* >0: 写到该比例即断流(finish 带错误) */
int flaky; /* 断续网:每 ~6 块额外停 100-400ms */
uint64_t start; /* 起点偏移(续传:从 .part 大小续写;0=从头) */
stream_src_t *src;
ncm_cache_writer_t *w;
int rc; /* NCM_OK / 负 */
@@ -125,7 +126,7 @@ static void *net_thread(void *arg)
{
net_t *nt = (net_t *)arg;
const size_t CH = 4096;
size_t off = 0;
size_t off = (size_t)nt->start;
size_t cut_at = (nt->cut_frac > 0) ? (size_t)(nt->size * nt->cut_frac) : (size_t)-1;
unsigned rng = 12345;
int chunk_no = 0;
@@ -211,7 +212,7 @@ static int run_case(const char *tag, const unsigned char *data, size_t size,
printf(" FAIL stream open\n"); return -1;
}
net_t nt = { data, size, rate_Bps, jitter_ms, cut_frac, flaky, src, w, NCM_OK };
net_t nt = { data, size, rate_Bps, jitter_ms, cut_frac, flaky, 0, src, w, NCM_OK };
pthread_t nth;
double t0 = now_s();
if (pthread_create(&nth, NULL, net_thread, &nt) != 0) { printf(" FAIL pthread\n"); return -1; }
@@ -312,6 +313,119 @@ static int run_case(const char *tag, const unsigned char *data, size_t size,
return 0;
}
/* 续传用例(阶段3 §5.2):会话1 中断下载存 .part,会话2 从 .part 续传 → 完整 PCM 金标准比对。
* 不含 http/Range(那是 player_flow_sim 的职责)——本用例验证「.part 前缀已在盘上、续写不被重复、
* 读端从头跟播、commit 后完整等于源文件」的写穿缓存续传契约。 */
static int run_resume_case(const char *tag, const unsigned char *data, size_t size,
uint64_t rate_Bps, int jitter_ms, int flaky, double resume_frac,
const pcm_buf_t *golden)
{
case_result_t rr;
memset(&rr, 0, sizeof rr);
char dir[] = "/tmp/ncm_stream_resume_XXXXXX";
if (!mkdtemp(dir)) { printf(" FAIL mkdtemp\n"); return -1; }
char cdir[600];
snprintf(cdir, sizeof cdir, "%s/cache", dir);
ncm_cache_t cache;
ncm_cache_init(&cache, cdir);
char key[32];
static int rcase_no = 0;
snprintf(key, sizeof key, "rs%d", rcase_no++);
/* ---- 会话1:中断下载,写 resume_frac 比例 → abort_part 存 .part ---- */
ncm_cache_writer_t *w1 = NULL;
if (ncm_cache_open(&cache, key, "mp3", &w1) != NCM_OK) { printf(" FAIL s1 open\n"); return -1; }
stream_src_t *src1 = NULL;
if (stream_src_open_stream(&src1, ncm_cache_writer_tmp_path(w1)) != 0) { printf(" FAIL s1 stream\n"); return -1; }
net_t nt1 = { data, size, rate_Bps, jitter_ms, resume_frac, flaky, 0, src1, w1, NCM_OK };
pthread_t n1;
pthread_create(&n1, NULL, net_thread, &nt1);
pthread_join(n1, NULL);
stream_src_close(src1);
ncm_cache_abort_part(w1); /* .tmp → .part(保留前缀字节) */
uint64_t part_size = 0;
if (ncm_cache_part_size(&cache, key, "mp3", &part_size) != NCM_OK || part_size == 0) {
printf(" FAIL .part not parked\n"); return -1;
}
uint64_t exp_part = (uint64_t)((double)size * resume_frac);
CHECK(part_size == exp_part, ".part size=%llu (expect %llu 前缀)",
(unsigned long long)part_size, (unsigned long long)exp_part);
/* ---- 会话2:续传(.part 追加)+ 读端从头跟播 → 完整 PCM ---- */
ncm_cache_writer_t *w2 = NULL;
if (ncm_cache_open_resume(&cache, key, "mp3", &w2) != NCM_OK) { printf(" FAIL s2 resume open\n"); return -1; }
stream_src_t *src2 = NULL;
if (stream_src_open_stream_at(&src2, ncm_cache_writer_tmp_path(w2), part_size) != 0) {
printf(" FAIL s2 stream\n"); return -1;
}
net_t nt2 = { data, size, rate_Bps, jitter_ms, 0, flaky, part_size, src2, w2, NCM_OK };
pthread_t n2;
double t0 = now_s();
pthread_create(&n2, NULL, net_thread, &nt2);
/* 预缓冲(前缀 ≥128KB 则立即满足;不足则等续写下足) */
double wd_t0 = now_s();
int wr;
while ((wr = stream_src_wait(src2, 128 * 1024, 100)) == 0)
if (now_s() - wd_t0 > 120) { printf(" FAIL resume prebuffer watchdog\n"); g_fail = 1; _exit(1); }
ncm_decoder_t *dec = NULL;
int rate = 0, ch = 0, bits = 0;
if (ncm_dec_open_src(&dec, src2, &rate, &ch, &bits, NULL) != 0) {
printf(" FAIL resume dec open\n"); return -1;
}
if (audio_out_open(rate, ch, bits) != 0) { printf(" FAIL audio open\n"); g_fail = 1; _exit(1); }
static unsigned char pcm[4096 * 2 * 4];
int64_t decoded = 0;
for (;;) {
if (now_s() - t0 > 150) { printf(" FAIL resume watchdog\n"); g_fail = 1; _exit(1); }
int64_t fr = ncm_dec_read(dec, pcm, 4096);
if (fr == 0) break;
if (rr.first_sound_s < 0) rr.first_sound_s = now_s() - t0;
if (audio_out_write(pcm, (int)fr) != (int)fr) { printf(" FAIL audio write\n"); g_fail = 1; _exit(1); }
pcm_put(&rr.pcm, pcm, (size_t)fr * (size_t)ch * (size_t)bits / 8);
decoded += fr;
if (rate > 0) usleep((useconds_t)(fr * 1000000 / rate));
}
rr.frames = decoded;
rr.wall_s = now_s() - t0;
stream_src_stats(src2, &rr.underruns, &rr.wait_ms);
audio_out_close();
ncm_dec_close(dec);
pthread_join(n2, NULL);
stream_src_abort(src2); /* 已完成则无副作用 */
/* 续传玩法:读端从头跟播,产出 PCM 应与整首离线解码逐字节一致(金标准) */
CHECK(rr.pcm.n == golden->n && memcmp(rr.pcm.b, golden->b, golden->n) == 0,
"续传 PCM 与整首离线解码逐字节一致 (%zu/%zu)", rr.pcm.n, golden->n);
free(rr.pcm.b);
/* commit 转正 + 校验最终文件与源逐字节一致 */
int ck = ncm_cache_commit(w2);
CHECK(ck == 0, "resume commit rc=%d", ck);
if (ck == 0) {
char fp[700];
if (ncm_cache_path(&cache, key, "mp3", fp, sizeof fp) == NCM_OK) {
FILE *f = fopen(fp, "rb");
if (!f) { printf(" FAIL committed missing\n"); g_fail = 1; }
else {
unsigned char *got = (unsigned char *)malloc(size);
size_t rn = fread(got, 1, size, f);
int extra = fgetc(f);
fclose(f);
CHECK(rn == size && extra == EOF && memcmp(got, data, size) == 0,
"committed == source (%zu/%zu bytes)", rn, size);
free(got);
}
}
}
stream_src_close(src2);
printf(" [%s] resume .part=%llu wall=%.2fs first_sound=%.2fs frames=%lld underruns=%d\n",
tag, (unsigned long long)part_size, rr.wall_s, rr.first_sound_s,
(long long)rr.frames, rr.underruns);
return 0;
}
int main(int argc, char **argv)
{
(void)argc;
@@ -447,6 +561,12 @@ int main(int argc, char **argv)
free(fg.b);
}
/* 阶段3:.part 续传——中断 40% → 存 .part → 续传(略过已下前缀)→ 完整 PCM 金标准比对 */
printf("== CASE resume@10MB/s (40%% cut) ==\n");
run_resume_case("resume@10MB/s", data, size, 10485760u, 0, 0, 0.4, &golden);
printf("== CASE resume@400kbps (40%% cut, 慢网续传) ==\n");
run_resume_case("resume@400kbps", data, size, 50000u, 10, 1, 0.4, &golden);
free(golden.b); free(golden_cut.b); free(data);
printf(g_fail ? "== STREAM SIM: FAIL ==\n" : "== STREAM SIM: ALL PASS ==\n");
return g_fail;