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
@@ -66,6 +66,9 @@ int appstate_song_copy(int gidx, ncm_song_t *out)
|
||||
return 0;
|
||||
}
|
||||
int appstate_song_fetch(int gidx, ncm_song_t *out) { return appstate_song_copy(gidx, out) == 0 ? NCM_OK : NCM_ERR_ARG; }
|
||||
/* §23:player_manager 改走 appstate_queue_*(队列来源)——sim 恒 PLAYLAST 队列 = 窗口 */
|
||||
int appstate_queue_count(void) { return g_st.track_ids_count; }
|
||||
int appstate_queue_song(int idx, ncm_song_t *out) { return appstate_song_copy(idx, out); }
|
||||
|
||||
/* ---- 缓存/网络 stub ----
|
||||
* g_cache_ok=1 → lookup 命中(给真实存在的 /tmp 文件,免下载);
|
||||
@@ -80,6 +83,15 @@ static char g_last_ext[8]; /* ncm_cache_open 收到的扩展名 */
|
||||
static int g_url_calls; /* §20:song_url_auto 调用数(fee 门禁不得发请求) */
|
||||
static int g_url_req_level = -1;/* §20:song_url_auto 收到的档位(VIP 钳顶断言用) */
|
||||
static int g_vip = NCM_VIP_NONE;/* §20:account stub 注入的会员态 */
|
||||
/* ---- 阶段3 续传/重试 stub 状态 ---- */
|
||||
static uint64_t g_part_size; /* >0=模拟存在 .part 残体(续传起点) */
|
||||
static int g_http_status; /* ncm_http_download_status 回填的响应状态(默认 200) */
|
||||
static char g_last_range[64];/* 收到的 Range 头("bytes=...";无则空串) */
|
||||
static int64_t g_resume_opens; /* ncm_cache_open_resume 调用数 */
|
||||
static int64_t g_part_removes; /* ncm_cache_part_remove 调用数 */
|
||||
static int g_writer_resume;/* 当前 writer 是否续传打开(模拟 .part 被删/保留) */
|
||||
static int g_416_first; /* >0: 首个下载请求回 416(续传被拒),其后按 g_http_status */
|
||||
static int64_t now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec*1000 + ts.tv_nsec/1000000; }
|
||||
|
||||
/* §20:account stub(真实实现在 app/logic/account.c,不链入本 sim) */
|
||||
int account_vip(void) { return g_vip; }
|
||||
@@ -100,22 +112,41 @@ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char
|
||||
return NCM_OK;
|
||||
}
|
||||
static char g_tmp_path[] = "/tmp/pf_sim_dl.tmp"; /* 下载路径 stub 的 .tmp(§21:stream_src 要真实打开) */
|
||||
static int g_commits; /* §21:commit 移到曲尾 dispose(边下边播),P7 等收尾用 */
|
||||
static void pf_make_tmp(void) { FILE *f = fopen(g_tmp_path, "wb"); if (f) fclose(f); } /* stream_src 要真实打开 */
|
||||
int ncm_cache_open(const ncm_cache_t *c, const char *id, const char *ext, ncm_cache_writer_t **w)
|
||||
{
|
||||
(void)c;
|
||||
snprintf(g_last_key, sizeof g_last_key, "%s", id);
|
||||
snprintf(g_last_ext, sizeof g_last_ext, "%s", ext ? ext : "");
|
||||
FILE *f = fopen(g_tmp_path, "wb"); /* 建空 .tmp:stream_src_open_stream 需存在 */
|
||||
if (f) fclose(f);
|
||||
pf_make_tmp();
|
||||
g_writer_resume = 0;
|
||||
*w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK;
|
||||
}
|
||||
const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w) { (void)w; return g_tmp_path; }
|
||||
int ncm_cache_write(ncm_cache_writer_t *w, const unsigned char *d, size_t l) { (void)w; (void)d; (void)l; return NCM_OK; }
|
||||
static int g_commits; /* §21:commit 移到曲尾 dispose(边下边播),P7 等收尾用 */
|
||||
int ncm_cache_commit(ncm_cache_writer_t *w) { free(w); g_cache_ok = 1; g_commits++; return NCM_OK; }
|
||||
int ncm_cache_abort(ncm_cache_writer_t *w) { free(w); return NCM_OK; }
|
||||
int ncm_cache_abort(ncm_cache_writer_t *w)
|
||||
{
|
||||
if (g_writer_resume) g_part_size = 0; /* 模拟:删除续传 writer 的 .part(续传失败→整首重下) */
|
||||
g_writer_resume = 0; free(w); return NCM_OK;
|
||||
}
|
||||
int ncm_cache_abort_part(ncm_cache_writer_t *w) { g_writer_resume = 0; free(w); return NCM_OK; } /* 保留 .part */
|
||||
int ncm_cache_evict(const ncm_cache_t *c) { (void)c; return NCM_OK; }
|
||||
void ncm_cache_set_limit_gb(ncm_cache_t *c, int gb) { (void)c; (void)gb; }
|
||||
/* ---- 阶段3 续传 stub ---- */
|
||||
int ncm_cache_part_size(const ncm_cache_t *c, const char *id, const char *ext, uint64_t *sz)
|
||||
{ (void)c; (void)id; (void)ext; if (!g_part_size) return NCM_ERR_NOENT; *sz = g_part_size; return NCM_OK; }
|
||||
int ncm_cache_open_resume(const ncm_cache_t *c, const char *id, const char *ext, ncm_cache_writer_t **w)
|
||||
{
|
||||
(void)c;
|
||||
snprintf(g_last_key, sizeof g_last_key, "%s", id);
|
||||
snprintf(g_last_ext, sizeof g_last_ext, "%s", ext ? ext : "");
|
||||
pf_make_tmp(); g_writer_resume = 1; g_resume_opens++;
|
||||
*w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK;
|
||||
}
|
||||
int ncm_cache_part_remove(const ncm_cache_t *c, const char *id, const char *ext)
|
||||
{ (void)c; (void)id; (void)ext; g_part_removes++; g_part_size = 0; return NCM_OK; }
|
||||
|
||||
int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int level,
|
||||
ncm_play_url_t *out, int *out_level)
|
||||
@@ -133,20 +164,42 @@ int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int l
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
|
||||
size_t maxb, ncm_http_resp_t *resp)
|
||||
static int g_dl_fail; /* 阶段3:>0 时下载返回 NCM_ERR_NET(模拟预缓冲前网络失败) */
|
||||
int ncm_http_download_status(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
|
||||
size_t maxb, ncm_http_resp_t *resp, int *status_out)
|
||||
{
|
||||
(void)req; (void)maxb;
|
||||
g_downloads++;
|
||||
if (g_dl_delay_ms > 0) usleep((useconds_t)g_dl_delay_ms * 1000);
|
||||
int st = (g_416_first && g_downloads == 1) ? 416 : (g_http_status ? g_http_status : 200);
|
||||
if (status_out) *status_out = st; /* 状态感知:首 body 前写入(续传分支读它) */
|
||||
g_last_range[0] = 0;
|
||||
if (req && req->extra_hdrs) { /* 捕获 Range 头(续传断言) */
|
||||
const char *r = strstr(req->extra_hdrs, "Range: bytes=");
|
||||
if (r) {
|
||||
r += strlen("Range: bytes=");
|
||||
int i = 0;
|
||||
while (r[i] && r[i] != '\r' && r[i] != '\n' && i < (int)sizeof g_last_range - 1) { g_last_range[i] = r[i]; i++; }
|
||||
g_last_range[i] = 0;
|
||||
}
|
||||
}
|
||||
if (st == 416) g_part_size = 0; /* 模拟:续传被拒后残体删除(重试走整首) */
|
||||
memset(resp, 0, sizeof *resp);
|
||||
resp->status = st;
|
||||
if (g_dl_fail) return NCM_ERR_NET; /* 预缓冲前网络失败(无 body 投放) */
|
||||
unsigned char chunk[65536];
|
||||
memset(chunk, 0x5A, sizeof chunk);
|
||||
for (int i = 0; i < 8; i++) /* 512KB 总分发(进度回调会走 8 次) */
|
||||
if (cb(ctx, chunk, sizeof chunk) != 0) break;
|
||||
memset(resp, 0, sizeof *resp);
|
||||
resp->status = 200;
|
||||
if (st == 416) { /* 416:投一个 error body → cb 置 need_fresh 并 abort */
|
||||
if (cb(ctx, chunk, 8) != 0) return NCM_ERR_FULL;
|
||||
return NCM_ERR_HTTP;
|
||||
}
|
||||
for (int i = 0; i < 8; i++) /* 512KB 总分发(进度回调走 8 次) */
|
||||
if (cb(ctx, chunk, sizeof chunk) != 0) return NCM_ERR_FULL;
|
||||
return NCM_OK;
|
||||
}
|
||||
int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
|
||||
size_t maxb, ncm_http_resp_t *resp)
|
||||
{ return ncm_http_download_status(req, cb, ctx, maxb, resp, NULL); }
|
||||
|
||||
/* ---- 解码器 stub(§19:player_manager 走 ncm_dec_* 分发层;§21:open 改 ncm_dec_open_src):
|
||||
* g_eof 前恒有数据(P1 用 g_reads_left 控制播完点) ---- */
|
||||
@@ -366,6 +419,7 @@ int main(void)
|
||||
int cb = g_commits;
|
||||
for (int t = 0; t < 3000 && g_commits == cb; t += 10) usleep(10 * 1000);
|
||||
}
|
||||
g_eof = 0; g_reads_left = -1; /* 上一步非 VIP 部分设了 g_eof=1 让 song0 收尾;SVIP song1 需正常播 */
|
||||
g_vip = NCM_VIP_SVIP; g_cache_ok = 0; g_url_calls = 0; g_url_req_level = -1;
|
||||
player_manager_play(1);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "svip song playing");
|
||||
@@ -406,6 +460,54 @@ int main(void)
|
||||
g_fee = 0; g_eof = 1;
|
||||
player_manager_set_quality(NCM_LEVEL_STANDARD, 10);
|
||||
|
||||
printf("== P9: .part 续传 -> Range 头 + 206 追加(阶段3 §5.2)==\n");
|
||||
usleep(300 * 1000);
|
||||
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_url_rc = NCM_OK; g_fee = 0;
|
||||
g_part_size = 200 * 1024; /* .part 残体 = 200KB(> 预缓冲门槛 128KB) */
|
||||
g_http_status = 206; g_416_first = 0; g_dl_fail = 0; g_dl_delay_ms = 0;
|
||||
g_resume_opens = 0; g_last_range[0] = 0; g_vip = NCM_VIP_NONE;
|
||||
int cb0 = g_commits;
|
||||
player_manager_play(0);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 4000), "206 resume song playing");
|
||||
for (int t = 0; t < 200 && !g_last_range[0]; t += 10) usleep(10 * 1000); /* 等 dl 线程写 Range */
|
||||
CHECK(g_resume_opens >= 1, "resume opened (ncm_cache_open_resume=%lld)", (long long)g_resume_opens);
|
||||
CHECK(strcmp(g_last_range, "204800-") == 0, "Range header=%s (expect 204800- = 200KB)", g_last_range);
|
||||
g_eof = 1;
|
||||
player_manager_stop(); /* 阻断顺延,等 commit 落盘 */
|
||||
for (int t = 0; t < 3000 && g_commits == cb0; t += 10) usleep(10 * 1000);
|
||||
CHECK(g_commits > cb0, "206 resume committed (commits %d->%d)", cb0, g_commits);
|
||||
|
||||
printf("== P10: 续传 416 -> 删 .part 整首重下(重试无 Range,阶段3 §5.2)==\n");
|
||||
usleep(300 * 1000);
|
||||
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_url_rc = NCM_OK; g_fee = 0;
|
||||
g_part_size = 64 * 1024; /* .part = 64KB(< 预缓冲门槛 → 触发 prebuffer 失败 → 重试) */
|
||||
g_http_status = 200; g_416_first = 1; g_dl_fail = 0; g_dl_delay_ms = 0;
|
||||
g_resume_opens = 0; g_downloads = 0; g_last_range[0] = 0; g_vip = NCM_VIP_NONE;
|
||||
g_eof = 0; g_reads_left = -1;
|
||||
player_manager_play(0);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 5000), "416 -> fresh retry song playing");
|
||||
CHECK(g_downloads == 2, "downloads=%d (expect 2: 续传 416 + 整首重下)", g_downloads);
|
||||
CHECK(g_resume_opens == 1, "resume_opens=%lld (expect 1: 仅首试续传,重试整首)",
|
||||
(long long)g_resume_opens);
|
||||
CHECK(g_last_range[0] == 0, "retry no Range (残体已删,整首重下)");
|
||||
g_eof = 1; player_manager_stop();
|
||||
|
||||
printf("== P11: 预缓冲前网络失败 -> 重试退避序列(阶段3 §4.1 风控)==\n");
|
||||
usleep(300 * 1000);
|
||||
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_url_rc = NCM_OK; g_fee = 0;
|
||||
g_part_size = 0; g_http_status = 200; g_416_first = 0; g_dl_fail = 1; g_dl_delay_ms = 0;
|
||||
g_downloads = 0; g_vip = NCM_VIP_NONE;
|
||||
int64_t t0 = now_ms();
|
||||
player_manager_play(0);
|
||||
/* 首试瞬时失败(NCM_ERR_NET,无 body)→ 退避 800ms → 重试 1 次 → 仍失败 → set_err 停播 */
|
||||
CHECK(wait_downloads(2, 4000), "retry happened (downloads=2)");
|
||||
int64_t dt = now_ms() - t0;
|
||||
CHECK(dt >= 700,
|
||||
"backoff sequence: 2nd download at +%lldms (expect >= 700ms 退避,非连环轰炸)", (long long)dt);
|
||||
CHECK(wait_state(NCM_PC_STOPPED, 4000), "retry exhausted -> STOPPED");
|
||||
CHECK(player_manager_error() != 0, "err set after retry exhausted (rc=%d)", player_manager_error());
|
||||
g_dl_fail = 0;
|
||||
|
||||
usleep(200 * 1000);
|
||||
player_manager_stop_thread();
|
||||
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
|
||||
|
||||
Reference in new issue
Block a user