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:
1 parent
fcff0b0ac1
commit
19e8fc93ed
14 files changed
+735
-93
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user